LEÇON 8 / 70 minutes

Tableaux, références et pointeurs

Les données occupent une place en mémoire. Pour manipuler une séquence ou laisser une procédure consulter un objet, Vitte fournit des tableaux, des références et des pointeurs.

À retenir·Choisissez la représentation la plus simple qui exprime le contrat attendu ; un pointeur brut demande une prudence particulière.

Tableaux de taille connue

Le type [N]T décrit un tableau de N éléments de type T. Par exemple, [3]i64 contient trois entiers signés de 64 bits. On peut écrire la valeur avec une liste littérale puis lire un élément avec un indice entre crochets :

proc deuxieme() -> i64 {
    let notes: [3]i64 = [14, 17, 19];
    return notes[1];
}

Les indices commencent à zéro dans les exemples Vitte : notes[0] est le premier élément et notes[2] le troisième. L’accès notes[3] dépasse la longueur de ce tableau ; considérez les limites comme une condition à vérifier, pas comme une convention facultative.

Exemple vérifié : tableau fixe, lecture et écriture

examples/tutorials/tableaux.vit remplace le deuxième élément puis vérifie que la somme vaut 42.

build/bin/vitte run examples/tutorials/tableaux.vit --quiet --no-color

Résultat confirmé avec le frontend natif : code de sortie 0.

La bibliothèque core fournit également des opérations sur des tableaux et des tranches. Les fonctions de tranches attendent une vue empruntée sur une séquence existante au lieu d’allouer une nouvelle collection. Les détails de durée de vie et les opérations disponibles sont documentés dans les modules concernés.

Références : emprunter une valeur

Le type &T représente une référence vers une valeur de type T. La notation &valeur demande une référence à une expression, et un paramètre de type &Point permet d’inspecter un point existant. Le préfixe mut apparaît dans des types comme &mut T lorsque le contrat autorise une modification à travers la référence.

proc somme(valeurs: &[3]i64) -> i64 {
    return valeurs[0] + valeurs[1] + valeurs[2];
}

proc modifier(premier: &mut i64) {
    set *premier = *premier + 1;
}

Les références expriment un accès à des données qui appartiennent ailleurs. Il est utile de les apprendre en séparant deux questions : qui possède la donnée et qui a le droit de la lire ou la changer ? Le compilateur Vitte et ses bibliothèques évoluent encore ; vérifiez quelles règles de durée de vie et de mutabilité sont appliquées par la version que vous utilisez.

Références : chemin compilateur testé

examples/tutorials/references.vit vérifie qu’une procédure reçoit un point par référence et calcule 42 avec build/bin/vitte-bootstrap run examples/tutorials/references.vit --quiet --no-color (code de sortie 0). Le frontend natif rejette actuellement ce même cas à l’abaissement HIR (E1011).

Pointeurs : une abstraction plus basse

Un type *T est un pointeur vers T. Les pointeurs apparaissent dans les interfaces système, l’interopérabilité et les bibliothèques qui contrôlent directement les adresses mémoire. L’expression *pointeur déréférence un pointeur. Une adresse incorrecte, désalignée ou déjà invalidée peut rendre le programme dangereux ; la syntaxe seule ne rend pas l’opération sûre.

La grammaire prévoit des blocs unsafe { ... } afin de rendre explicites certaines opérations qui demandent des garanties supplémentaires. Un bloc unsafe ne corrige pas l’adresse et ne prouve pas que l’objet existe : il marque un endroit où la personne qui écrit le programme prend la responsabilité de respecter les invariants mémoire.

Repère de prudence

N’utilisez pas un pointeur brut pour remplacer une référence ou un tableau lorsqu’ils expriment déjà votre besoin. Le code bas niveau doit préciser la durée de vie, l’alignement, la taille de la zone et les règles d’alias qu’il suppose.

Valeurs optionnelles et absence

La bibliothèque fondamentale emploie le type nullable ?T : il contient soit une valeur de type T, soit null. L’alias générique Option<T> désigne cette représentation dans le module core::types::option. Avant de lire la valeur, vérifiez qu’elle n’est pas null.

proc valeur_ou_zero(valeur: ?i64) -> i64 {
    if valeur != null {
        return valeur;
    }
    return 0;
}

Une option représente l’absence comme une donnée, plutôt que d’inventer une valeur spéciale comme zéro qui pourrait également être un résultat légitime. La représentation nullable ne distingue pas « option absente » et « option présente avec une valeur elle-même nulle » si le type contenu accepte aussi null.

La grammaire prévoit aussi un point d’interrogation postfixé sur certaines expressions. Ne déduisez pas son comportement à partir du symbole seul : vérifiez sa sémantique dans la documentation de l’outil et dans les exemples de la bibliothèque installée.

L’opérateur ?? représente une autre idée : choisir une valeur de repli lorsque la partie gauche est absente. On peut lire age ?? 0 comme « utilise l’âge s’il existe, sinon zéro ». C’est utile quand le repli a réellement du sens dans le domaine ; sinon, traitez l’absence comme un cas distinct au lieu de la masquer.

Pointeurs et valeurs nullable

Les extraits de pointeurs, ?T, ? et ?? sur cette page sont explicatifs : les suites exécutées pour le site n’ont pas confirmé un programme complet avec ces constructions. Le tableau fixe et la référence ci-dessus ont des commandes et des résultats testés.

Penser aux frontières

Les bugs mémoire se cachent souvent sur les limites : tableau vide, indice égal à la longueur, référence conservée après la durée de vie de la valeur, ou pointeur converti vers un type plus étroit. Décrivez le contrat d’une procédure qui reçoit une zone mémoire : combien d’éléments sont accessibles, qui peut les modifier et qui garde la propriété de la zone ?

Conservez un invariant lisible

Lorsqu’une opération touche directement à la mémoire, ses conditions requises doivent apparaître à proximité. Une personne qui relit le code doit pouvoir vérifier la taille, la validité et l’alignement avant de suivre la déréférence.

La longueur d’un tableau appartient à son type

Avec [3]i64, la longueur est connue : le tableau contient exactement trois valeurs de type i64. Les indices valides sont 0, 1 et 2. L’indice 3 est déjà hors de la plage, même s’il ressemble à une position « après le dernier élément ».

Pour parcourir les éléments avec un while confirmé, le corps maintient un indice mutable et le teste avant l’accès :

let mut index: i64 = 0;
while index < 3 {
    let valeur: i64 = notes[index];
    set index = index + 1;
}

Ce fragment illustre l’ordre indispensable : tester la borne, lire la case, puis faire progresser l’indice. Le parcours for sur tableau n’est pas confirmé au backend dans l’audit ; ne le substitue pas silencieusement à cette forme lorsque vous voulez un exemple exécutable avec le frontend natif.

Choisir entre un tableau, une référence et un pointeur

Ces formes ne répondent pas à la même question. Un tableau fixe exprime une séquence dont la taille est connue. Une référence exprime un accès à une valeur existante sans écrire directement une adresse. Un pointeur brut donne accès à une adresse et demande que le programme établisse lui-même davantage de garanties.

Pour chaque paramètre de données, précisez qui garde la valeur, qui peut la lire, qui peut la modifier et combien de temps l’accès reste valide. Le type &T et le type &mut T signalent une différence de contrat d’accès, mais la présence de ces formes ne prouve pas à elle seule que le compilateur vérifie toutes les règles de durée de vie ou d’alias attendues.

Absence : garder la distinction avec une valeur normale

Une valeur nullable de forme ?T peut représenter une donnée présente ou null. Si zéro est une valeur autorisée pour T, elle ne doit pas servir de code implicite pour l’absence : zéro et null expriment deux situations différentes.

Une valeur de repli, telle que age ?? 0, simplifie le calcul seulement si zéro est réellement le comportement voulu lorsque l’âge manque. Si l’absence change la décision à prendre, conservez-la comme cas distinct et traitez-la explicitement. La syntaxe nullable et les opérateurs associés ne sont pas tous prouvés en compilation/exécution dans le backend audité.

Contrat mental avant une lecture mémoire

Avant une lecture indexée, vérifiez l’intervalle ; avant une écriture, vérifiez également que le propriétaire autorise la modification ; avant une déréférence, vérifiez la validité de l’adresse, l’alignement, la taille accessible et la durée de vie. Pour un tableau fixe, ces questions se ramènent principalement à la borne de l’indice. Pour un pointeur, elles deviennent des obligations explicites de la procédure qui le reçoit.

Déroulement du parcours sûr

Pour un tableau de longueur 3, commencez à l’indice 0, continuez tant que l’indice est strictement inférieur à 3 et accédez seulement après le test. La progression doit augmenter l’indice exactement une fois par passage. Le dernier accès a lieu à l’indice 2.

Longueur, disposition et accès au tableau

Le type [N]T décrit une séquence fixe de N valeurs de type T. La longueur fait partie de sa forme, tandis que l’indice désigne une position à l’intérieur de cette longueur. Pour trois valeurs, les indices valides sont 0, 1 et 2. La condition index < 3 exprime donc précisément qu’il reste une case accessible ; index <= 3 autoriserait une lecture de trop.

Un tableau fixe se distingue d’une collection dont la taille varie au fil du programme. Les tranches représentent plutôt une vue sur une séquence existante : elles permettent de transmettre un accès à une partie des données sans que le type exprime nécessairement la propriété de toute la collection. Les bibliothèques et backends déterminent quelles opérations sont réellement disponibles. La distinction conceptuelle entre taille fixe, vue empruntée et stockage extensible demeure utile même lorsque l’implémentation ne prend en charge qu’un sous-ensemble.

Référence et propriété ne sont pas la même chose

Une référence donne accès à une valeur existante sans exposer directement son adresse. &T exprime une lecture référencée ; &mut T exprime une intention d’accès modifiable. Cela ne suffit pas à conclure que le compilateur applique toutes les règles de durée de vie et d’exclusivité présentes dans des systèmes d’emprunt plus développés. La version du frontend et le chemin de compilation définissent les vérifications réelles.

Une question utile reste celle de la propriété : quelle partie du programme est responsable de la durée de vie de l’objet ? Une procédure recevant une référence ne devient pas automatiquement propriétaire de la donnée. Si elle garde un accès au-delà de l’appel ou si plusieurs alias peuvent la modifier, les invariants nécessaires doivent être documentés et vérifiés par les capacités effectivement disponibles.

Pointeurs et absence demandent des contrats séparés

Un pointeur brut *T désigne une adresse. Avant une déréférence, le programme doit établir que l’adresse est valide, alignée, correctement typée et accessible pendant toute l’opération. La taille de la zone, la durée de vie et les autres accès concurrents ou alias font également partie du contrat. Un bloc unsafe rend visible la responsabilité de l’opération, mais ne prouve aucune de ces propriétés.

À l’opposé de cette représentation basse, ?T représente une valeur éventuellement absente. Le cas null est alors un état à traiter, pas une adresse. Cette absence ne doit pas être confondue avec un zéro si zéro reste une valeur légitime du type. Les opérateurs nullable décrits par la grammaire et leur comportement runtime ne sont pas tous confirmés dans le backend audité ; les exemples vérifiés de cette leçon restent ceux des tableaux et du chemin bootstrap de référence.

Tranches : une vue sur une séquence

Une tranche représente un accès à une portion d’une séquence, souvent sans copier tout le tableau. Elle peut être transmise à une procédure qui n’a besoin que de lire une suite de valeurs. La longueur de cette vue fait partie du raisonnement : l’indice doit rester à l’intérieur de la tranche, même s’il serait valide dans le tableau original. Une vue courte ne doit pas autoriser l’accès à des éléments situés ailleurs dans la mémoire.

Les modules de core documentent des opérations sur les tableaux et tranches, mais leur disponibilité dépend du backend et du type concret. Un appel de bibliothèque ne rend pas automatiquement une collection dynamique disponible. Le contrat de stockage, de durée de vie et de mutation doit être lu dans le module concerné et comparé à ce que le compilateur sait générer.

Référence mutable et alias concurrents

Une référence mutable implique que l’accès peut modifier la valeur visée. Si un autre accès observe le même objet pendant cette modification, la cohérence dépend des règles d’alias et de synchronisation. Certains langages imposent une exclusivité vérifiée statiquement ; d’autres laissent davantage de responsabilité au programme. La notation &mut T dans la grammaire ne suffit pas à attribuer à Vitte des garanties qui n’ont pas été testées sur le frontend choisi.

Dans un code système, une opération atomique ou un verrou peut être nécessaire lorsque plusieurs tâches partagent la donnée. Ces mécanismes sont distincts d’une simple référence mutable locale. Ils demandent un contrat de concurrence, une politique d’ordre mémoire et une API de plateforme. La documentation actuelle présente ces questions comme des limites de conception à expliciter, sans prétendre que les exemples bas niveau sont tous exécutables.