LEÇON 3 / 55 minutes

Valeurs, variables et types

Une valeur représente une donnée ; un type précise quelle sorte de donnée c’est et quelles opérations ont du sens dessus. Les variables donnent des noms à ces valeurs.

À retenir·En Vitte, les variables sont immuables par défaut ; on demande explicitement le droit de modifier une liaison.

Une variable est une liaison nommée

On déclare une variable locale avec let. Ici, le type est écrit après les deux-points :

proc calculer() -> i64 {
    let prix: i64 = 12;
    let quantite: i64 = 3;
    let total: i64 = prix * quantite;
    return total;
}

Le nom total désigne la valeur calculée par prix * quantite. L’annotation : i64 précise qu’il s’agit d’un entier signé sur 64 bits. L’écrire explicitement est utile pendant l’apprentissage : vous voyez quels types circulent et vous obtenez des diagnostics plus faciles à comprendre.

Vitte sait aussi inférer certains types à partir de la valeur initiale dans les contextes pris en charge. En cours, gardez les annotations dans les premiers exemples. Elles rendent l’intention visible et évitent de confondre « le compilateur peut le deviner » avec « la valeur n’a pas de type ».

Immuable par défaut, mutable sur demande

Une déclaration simple avec let ne peut pas être remplacée ensuite. Pour une valeur qu’on doit faire évoluer, écrivez let mut et utilisez set pour l’affectation :

proc compteur() -> i64 {
    let mut nombre: i64 = 0;
    set nombre = nombre + 1;
    set nombre = nombre + 1;
    return nombre;
}

La différence est intentionnelle. Si une variable ne change pas, le lecteur peut suivre une seule valeur pendant toute la durée de son bloc. La mutabilité est une décision locale qu’on peut repérer dans le code. Si le compilateur signale qu’une variable est immuable, demandez-vous d’abord si elle a réellement besoin de changer ; ajoutez mut uniquement dans ce cas.

Exemple vérifié : type, liaison mutable et affectation

examples/tutorials/valeurs.vit calcule 12 × 3 + 6, puis renvoie 0 uniquement si le total vaut 42.

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

Résultat confirmé : code de sortie 0.

Les types courants

Les programmes du dépôt utilisent des entiers signés comme i8, i32 et i64, des entiers non signés comme u8 et u64, des flottants comme f32 et f64, ainsi que bool, char et string. Le type int est aussi utilisé pour le code de sortie du programme d’exemple.

Le choix du type décrit une contrainte réelle. Un âge ou une quantité ne sont pas des nombres arbitraires : leur plage et leur signe comptent. Une conversion explicite comme valeur as u64 indique au lecteur qu’on change de représentation. Elle ne garantit pas à elle seule que la valeur convient à la nouvelle plage ; pour une conversion sensible, préférez une fonction de conversion vérifiée de la bibliothèque quand elle est utilisable.

proc main() -> i64 {
    let mut octets: [2]u8 = [7 as u8, 255 as u8];
    set octets[0] = 9 as u8;

    if octets[0] == 9 and octets[1] == 255 {
        return 0;
    }
    return 1;
}
Conversion vérifiée dans ce contexte

examples/tutorials/conversions.vit vérifie un cast vers u8 dans un tableau fixe, puis une écriture indexée.

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

Résultat confirmé : code de sortie 0. Cela ne confirme pas automatiquement tous les sens et contextes de conversion.

Constantes, littéraux et tableaux

Une constante nommée au niveau du module utilise const. Elle convient à une valeur stable qu’on veut nommer et réutiliser. static est une autre déclaration de valeur globale dans la grammaire ; ne la choisissez pas par habitude : vérifiez le comportement attendu par le compilateur et la convention du projet.

const TAILLE: i64 = 3;
const COULEURS: [3]i64 = [12, 24, 36];

proc premier() -> i64 {
    return COULEURS[0];
}

[3]i64 signifie un tableau de trois éléments de type i64. La longueur fait partie du type. Les tableaux sont pratiques quand le nombre d’éléments est connu ; les tranches et collections de la bibliothèque ont d’autres contrats et sont abordées plus loin.

Une habitude utile : suivre le type de chaque étape

Quand vous lisez une expression, demandez-vous : quel type a chaque opérande ? Quel type produit l’opération ? Est-ce que la procédure appelée attend le même type ? Par exemple, ajouter un u8 à un i64 peut nécessiter une conversion explicite. Cette discipline vous protège des erreurs de largeur et des mélanges signe/non-signé.

À retenir

Un type n’est pas une décoration. Il définit une plage de valeurs et les opérations disponibles. Une conversion devrait correspondre à une décision que vous pouvez expliquer.

Comprendre la différence entre valeur et liaison

Dans let total: i64 = prix * quantite;, total est le nom d’une liaison et prix * quantite produit une valeur. Si vous écrivez ensuite let avec_tax: i64 = total + taxe;, vous créez une autre liaison à partir de la valeur précédente. Rien n’a modifié total.

Pour changer la valeur associée à un nom existant, Vitte rend la décision visible dans deux endroits : la déclaration porte mut, puis l’instruction de mise à jour porte set. Cette paire évite de confondre la création d’un calcul avec la modification d’un état.

proc compter() -> i64 {
    let initial: i64 = 2;
    let double: i64 = initial * 2;
    let mut cumul: i64 = double;
    set cumul = cumul + 1;
    return cumul;
}

Le parcours est : initial = 2, double = 4, cumul = 4, puis cumul = 5. Les noms immuables gardent leur valeur ; seul cumul est réaffecté.

Choisir un type numérique avec méthode

Pour une quantité calculable, commencez par identifier si elle peut être négative, si elle peut contenir une fraction et quelle précision ou plage le domaine exige. Un i64 est un entier signé ; un u64 est non signé ; f64 représente une valeur à virgule flottante. Ne choisissez pas un type uniquement parce qu’il semble « assez grand » : les conversions et les opérations doivent rester cohérentes entre les paramètres, les variables et le résultat.

  • Une comparaison telle que quantite >= 0 traduit une règle métier si les valeurs négatives n’ont aucun sens.
  • Une conversion valeur as u8 demande une nouvelle représentation ; elle n’est pas à elle seule une validation de plage.
  • Une constante nommée donne un endroit unique à une limite fixe, mais il faut encore vérifier que sa valeur est correcte pour le calcul.

Le comportement du dépassement arithmétique dépend du chemin de compilation et des vérifications effectuées. Un dépassement ne doit pas être supposé s’enrouler : les bornes dépendent du compilateur, de la bibliothèque et des cas réellement ciblés.

Comment lire un diagnostic de type

Lorsqu’un type est refusé, partez de la valeur la plus proche du diagnostic. Vérifiez d’abord le type déclaré à gauche, ensuite le type de l’initialiseur ou de l’expression, puis les types des opérandes. Une conversion ne doit arriver qu’après que vous avez choisi la représentation voulue.

Par exemple, si une procédure attend un i64 et reçoit un u8, demandez-vous si la quantité est réellement toujours dans la plage signée attendue. Une conversion forcée peut satisfaire une signature sans satisfaire la règle métier. Pour les données venant de l’extérieur, préférez une conversion contrôlée et un cas d’erreur explicite lorsqu’ils sont disponibles.

Une liaison, une valeur et une portée

Une liaison est un nom associé à une valeur pendant une partie de l’exécution. Dans let prix: i64 = 12;, le nom prix sert à retrouver l’entier 12 dans son contexte. La liaison n’est pas la valeur elle-même : plusieurs noms peuvent parfois désigner des valeurs égales, tandis qu’une même liaison reste accessible seulement dans sa portée. Dans les exemples du cours, les déclarations locales sont placées dans le corps d’une procédure et sont utilisées par les instructions de ce corps.

Avec let, la liaison ne peut pas recevoir une nouvelle valeur après sa création. Avec let mut, le code indique que le nom représente un état susceptible d’évoluer ; set rend ensuite l’affectation explicite. Cette différence aide à lire un calcul : une valeur immuable décrit un résultat qui reste stable, alors qu’une valeur mutable représente un état qui change à des moments identifiables.

let prix: i64 = 12;
let quantite: i64 = 3;
let total: i64 = prix * quantite;

Ici, total reçoit le résultat 36. Ce calcul ne modifie ni prix ni quantite. Les trois liaisons donnent des noms à des valeurs distinctes et rendent explicites les étapes du raisonnement.

Le type borne les opérations et la représentation

i64 désigne un entier signé de largeur déterminée ; u64 désigne un entier non signé ; f64 désigne une représentation en virgule flottante. Le type ne constitue pas une étiquette décorative : il détermine quelles valeurs peuvent être représentées et quelles opérations ont un sens. Une quantité qui ne peut pas être négative et un décalage pouvant l’être ne décrivent pas forcément le même domaine, même si les deux sont stockés sous forme numérique.

Les limites numériques ont des conséquences sur les calculs composés. Le produit de deux valeurs valides peut dépasser la plage du type. La conversion vers une largeur plus petite peut également perdre une partie de l’information selon la règle appliquée par l’outil. Un cast comme valeur as u8 exprime un changement de représentation ; il n’est pas à lui seul une preuve que valeur est comprise entre 0 et 255. Le domaine, la plage et le traitement d’un dépassement doivent donc être considérés ensemble.

Inférence : pratique, mais pas absence de type

Lorsqu’une annotation est omise, l’analyseur peut déduire un type à partir de l’initialiseur et du contexte. Cela ne transforme pas la variable en donnée sans type : le compilateur doit toujours déterminer quelle opération s’applique et quelle représentation sera générée. L’annotation explicite est particulièrement utile lorsque plusieurs conversions sont possibles ou lorsque le type du littéral n’est pas évident.

Les tableaux fixes associent également une longueur au type. [3]i64 contient trois éléments de type i64 ; sa taille ne se confond pas avec la capacité d’une collection dynamique. La lecture par indice doit respecter cette taille, et une valeur au-delà de la dernière case reste invalide même si l’indice est lui-même un entier bien typé.

Types entiers : largeur, signe et domaine

Les noms comme i8, i32, i64, u8 et u64 indiquent une largeur et une interprétation du signe. Un entier signé réserve une partie de sa représentation aux valeurs négatives ; un entier non signé décrit une plage sans nombres négatifs. À largeur égale, les plages disponibles diffèrent donc. Le type ne se choisit pas seulement selon la taille maximale : il doit correspondre au domaine que la valeur décrit.

Une addition ou une multiplication peut sortir de cette plage même lorsque chaque opérande est valide. La stratégie de dépassement — diagnostic, enroulement, saturation ou autre comportement — appartient au compilateur et à la cible, et ne doit pas être supposée à partir du seul nom du type. Un programme qui manipule des quantités importantes doit établir les limites de ses entrées et de ses opérations avec les outils réellement disponibles.

Caractères, chaînes, booléens et valeur absente

bool exprime un état vrai ou faux ; il convient aux résultats de comparaison et aux décisions. Un littéral caractère comme 'v' désigne une unité textuelle de la grammaire, tandis qu’une chaîne comme "vitte" contient une séquence de texte. Ces deux catégories ne sont pas interchangeables, même si leur contenu visible se ressemble.

null représente une absence lorsque le type nullable correspondant est admis. Cette valeur n’est pas un zéro spécial ni une chaîne vide : elle décrit qu’aucune valeur de la catégorie n’est disponible. Les opérations qui consomment une donnée nullable doivent distinguer la présence de l’absence. Le support d’exécution des valeurs nullable étant partiel selon le backend audité, la signification conceptuelle ne doit pas être confondue avec une garantie runtime universelle.