1 points par GN⁺ 2025-11-08 | 1 commentaires | Partager sur WhatsApp
  • Le compilateur Zig, qui intègre nativement la compilation de code C et la compilation croisée, est, parmi tous les langages qu’a connus l’auteur en 45 ans de carrière, le plus étonnant
  • Avec des fonctionnalités uniques comme l’exécution à la compilation, les variables de taille de bit arbitraire et les blocs de test, il va bien au-delà d’un simple remplaçant de C/C++ et propose une toute nouvelle manière de programmer
  • Grâce à une syntaxe concise et claire — déclaration de variables avec inférence de type, structures anonymes, break étiqueté — on peut l’apprendre rapidement
  • Il prend en charge le débogage de code optimisé grâce à des tests de modules indépendants via les blocs de test et la fonction intégrée @breakpoint
  • Avec sa prise en charge de la programmation bas niveau via les champs de bits et les opérations sur les bits, il atteint à la fois efficacité et robustesse, tout en intégrant dans un langage compilé des avantages habituellement associés aux langages interprétés

Préface

  • En 45 ans de carrière, l’auteur n’a jamais vu de langage aussi étonnant que Zig
    • Zig n’est pas simplement un nouveau langage, mais un outil qui transforme fondamentalement la manière de programmer
  • Le considérer seulement comme un remplaçant de C ou C++ serait une forte sous-estimation
  • Le but de cet article est de présenter des fonctionnalités simples mais séduisantes de Zig et d’aider les programmeurs à démarrer rapidement
  • Il existe encore bien d’autres fonctionnalités qui influencent l’adoption de Zig dans l’industrie

Le compilateur Zig

  • En fournissant nativement la compilation de code C et la compilation croisée sans configuration séparée, il a un impact majeur sur l’industrie
  • L’installation consiste à télécharger le compilateur correspondant au processeur/à l’OS depuis la page de téléchargement de Ziglang, à le décompresser, puis à le copier dans le répertoire souhaité
    • Sous Windows 10, il est conseillé de copier le fichier zip x86_64 dans « Program Files » et de renommer le répertoire racine en « zig-windows-x86_64 » afin d’éviter de devoir modifier la variable d’environnement Path lors des mises à jour de version
    • Après avoir ajouté le chemin du répertoire racine à la variable d’environnement Path, le compilateur peut être utilisé en mode CLI
  • Pour construire un programme « Hello World! », il est recommandé de consulter la section « Getting Started » du site officiel

Concepts et commandes clés

Déclaration de variables

  • Une déclaration de variable se compose d’une première partie avec la visibilité (pub ou omission), var/const et le nom de la variable, d’une deuxième partie pour le type, puis d’une troisième pour l’initialisation
    • Seules la première et la troisième parties sont obligatoires, et le type peut être inféré à partir de la valeur d’initialisation
    • Exemple : var sum : usize = 0;
  • Une variable déclarée sans pub n’est accessible qu’à l’intérieur du module (similaire à une variable static en C)
  • Il est déconseillé de déclarer des variables pub, et il est recommandé de limiter les fonctions pub afin de réduire le couplage et d’augmenter la cohésion

Structures, structures anonymes, blocs de test

  • Les littéraux de structure anonyme entourés par .{ et } servent à initialiser des éléments d’une autre structure ou à créer une nouvelle structure avec ses éléments initialisés
  • .{ } est un littéral de structure anonyme vide
  • La forme struct { } correspond à une déclaration de structure
  • Les blocs de test permettent de compiler et d’exécuter des tests sans produire d’exécutable

Champs de bits

  • Les champs de bits sont déclarés comme des champs ayant un type d’une taille spécifique dans une packed struct
  • Les pointeurs peuvent pointer vers un champ de bits spécifique

Boucles for

  • La syntaxe Zig est plus claire que celle du C, mais utilise un intervalle ouvert [0..9) au lieu de [0..8]
  • Le type, l’initialisation, le test et l’incrémentation de la variable de boucle i sont gérés automatiquement

Tableaux

  • [_] définit un tableau dont la taille n’est pas spécifiée, suivi du type des éléments et de l’initialisation
    • Exemple : var grid = [_]u8{0} ** 81; initialise 81 éléments de type u8 à 0
    • La taille du tableau est inférée à partir de l’argument de répétition de l’initialisation
  • Dans un environnement de test, on peut parcourir les éléments d’un tableau et en calculer la somme
  • Les variables déclarées entre | dans une boucle for sont automatiquement considérées comme ayant le même type que les éléments du tableau
  • usize est l’entier non signé naturel de la plateforme (u64 sur 64 bits, u32 sur 32 bits)

Pointeurs multi-éléments

  • Pour qu’un pointeur vers tableau puisse utiliser l’arithmétique des pointeurs, il doit être explicitement déclaré comme pointeur multi-éléments, par exemple [*]const i32
  • Même si le tableau est const, le pointeur peut être déclaré en var

Déréférencement de pointeur

  • Un pointeur auquel est assignée l’adresse d’une position précise dans un tableau ne peut pas être mis à jour par arithmétique des pointeurs
  • Le déréférencement d’un pointeur s’écrit ptr.*

Break étiqueté

  • Il est possible d’effectuer à la compilation diverses opérations, comme l’initialisation d’un tableau
  • Le break étiqueté consiste à ajouter : après le nom d’un bloc et à renvoyer une valeur hors du bloc avec break
    • Exemple : break :init m;
  • 0.. représente une plage infinie à partir de 0
  • Dans une boucle for, les variables sont automatiquement initialisées et incrémentées, et la boucle se termine après le traitement de la dernière position du tableau
  • Un tableau peut ne pas être explicitement initialisé avec undefined

Les fonctions en Zig

  • Les fonctions sont déclarées avec fn et sont statiques par défaut (utilisables uniquement dans le fichier)
    • Déclarées comme pub fn, elles peuvent être importées depuis d’autres fichiers
  • Les fonctions peuvent être « inlined »
  • Les pointeurs de fonction s’écrivent avec const en tête, suivi du prototype de fonction

La programmation orientée objet en Zig

  • Les structures peuvent contenir des fonctions
  • Dans l’exemple de pile, on peut stocker jusqu’à 81 éléments (de type StkNode)
  • Les opérateurs ++ et -- n’existent pas en Zig ; on utilise += et -=
  • Le pointeur de pile est un entier utilisé comme indice du tableau stk
  • Le pointeur self n’est pas passé explicitement comme paramètre ; il est implicitement supposé être le pointeur de l’instance de pile sur laquelle la fonction est appelée
    • Dans un appel comme stack.pop(), self est un pointeur vers stack (semblable à this en Java/C++)
  • La fonction init() est le constructeur de la pile
  • Les fonctions pop et push sont « inlined »

Construire et exécuter un programme Zig

Construire un exécutable

  • Pour produire un exécutable, il faut une fonction main qui représente le point d’entrée du programme
  • Un programme simple peut inclure la fonction main dans le même fichier
  • Pour déboguer un module indépendamment, on peut insérer une fonction main à la fin du fichier, puis la commenter une fois le débogage terminé
  • Commande de compilation : zig build-exe -O ReleaseFast program.zig

Exécuter les blocs de test d’un module

  • C’est l’une des meilleures fonctionnalités de Zig, utile pour les tests et le prototypage
  • Un bloc de test commence par test "message" { et se termine par }
    • « message » est la chaîne affichée lors de l’exécution du test
  • Les blocs de test s’exécutent indépendamment de l’exécutable, et l’exécutable final n’exécute pas les tests
  • Commande de test : zig test module.zig
  • Le bloc de test de example.zig teste les fonctions set et print ; set prend en paramètre une chaîne décimale, et print affiche l’en-tête « Input Grid » avant d’afficher la grille

La sortie de Zig

  • L’instruction std.debug.print appelle la fonction print située dans debug.zig de la bibliothèque standard Zig std
  • Le premier paramètre est une chaîne de format, le second une structure anonyme contenant la liste des variables à afficher
  • Lorsqu’il n’y a pas de format, la structure est vide
  • L’affichage se fait sur stderr par défaut
  • Contrairement au printf du C, Zig peut traiter à la compilation les chaînes littérales et la liste des variables

Déboguer un exécutable

  • L’usage d’un débogueur n’est pas simple, sauf dans un IDE avec débogueur intégré (Eclipse, IntelliJ IDEA) ou un kit de développement intégré (w64devkit)
  • L’intégration des symboles alourdit le code et impose une compilation en mode Debug, produisant un code exécutable nettement moins efficace
  • Zig fournit une solution pratique pour éviter ces problèmes

Fonction intégrée @breakpoint

  • En insérant @breakpoint(); dans le code source, le programme s’arrête à cet endroit lorsqu’il est exécuté dans le débogueur
  • C’est une fonctionnalité utile pour déboguer du code Zig optimisé, sans symboles
  • Juste avant @breakpoint();, on peut utiliser std.debug.print pour afficher les variables à suivre et vérifier leur valeur à cet instant
  • Dans l’exemple debug_example.zig, du code affichant grid et diverses variables est inséré dans la fonction set, avec @breakpoint();
  • Commande de build : zig build-exe debug_example.zig
  • On lance ensuite debug_example.exe dans un débogueur comme gdb, puis on exécute le programme avec la commande r
  • La commande c permet de continuer l’exécution tout en suivant le contenu de la grille et les variables
  • En répétant la touche Entrée pour continuer, on peut vérifier que les valeurs de la grille correspondent à celles du bloc de test de example.zig

La programmation bas niveau en Zig

Représentation matricielle

  • Les chiffres décimaux sont stockés dans la matrice sous forme d’entiers u8 standard
  • La grille d’entrée est une chaîne, mais les caractères ASCII sont convertis en interne en entiers u8
  • Le stockage des nombres se fait linéairement, ligne par ligne, dans le tableau grid de 81 positions : var grid = [_]u8{0} ** 81;
  • Pour vérifier la validité de la grille, il faut accéder aux éléments par ligne et par colonne
  • On crée un tableau de 9 pointeurs, chacun pointant vers le début de chaque ligne
  • Le break étiqueté permet de renvoyer une valeur depuis un bloc de code : break :fill9x9 m; initialise matrix avec m
  • Notation d’accès aux éléments : element = matrix[i][j]

Représenter les chiffres décimaux sous forme de bits

  • Concept clé : remplacer le chiffre décimal entier i par l’entier code
    • i ∈ [1,9] → code = 2ⁱ⁻¹
    • i = 0 → code = 0
  • La position du seul bit mis à 1 dans code est i-1 (quand i est entre 1 et 9), sinon tous les bits valent 0
  • Un tableau fournit la valeur de code pour chaque chiffre (1→1, 2→2, 3→4, ..., 9→256)

Calculer code en Zig

  • La valeur code est calculée avec l’opérateur de décalage à gauche uniquement si c n’est pas nul : code = @as(u9,1) << (c-1);
  • En Zig, les constantes doivent avoir une taille adaptée pour que l’opération soit compilée et que le résultat soit assigné à une variable
  • code est déclaré de type u9 (la valeur maximale 256 nécessite au moins 9 bits)
  • Zig permet d’avoir des variables avec une taille de bit arbitraire
  • La fonction intégrée @as sert à caster la constante 1 en type u9

Représentation de la grille avec des champs de bits

Grille en champs de bits par ligne

  • Le tableau lines reflète toute la grille en représentant chaque ligne par un entier de 9 bits : var lines = [_]u9{0} ** 9;
  • En accédant au tableau avec la ligne i, on vérifie par un ET bit à bit (&) si un chiffre donné est déjà présent dans cette ligne : lines[i] & code
  • Si le résultat vaut 0, le chiffre n’est pas encore présent dans la ligne i, sinon c’est un doublon

Grille en champs de bits par colonne

  • Le tableau columns reflète toute la grille en représentant chaque colonne par un entier de 9 bits : var columns = [_]u9{0} ** 9;
  • En accédant au tableau avec la colonne j, on vérifie par un ET bit à bit si un chiffre donné est déjà présent dans cette colonne : columns[j] & code
  • Si le résultat vaut 0, le chiffre n’est pas encore présent dans la colonne j, sinon c’est un doublon

Règles du Sudoku

  • Lorsqu’on insère un nouveau chiffre dans une grille de Sudoku vide, il ne doit pas déjà exister dans toute la ligne, la colonne et la cellule contenant ce nouvel élément
  • Les cellules correspondent à chacun des 9 blocs 3x3 délimités par des traits épais
  • Dans une grille 9x9, chaque élément appartient à une ligne, une colonne et une cellule uniques
  • Dans la grille d’exemple, la première cellule contient 3, 5, 6, 8 et 9, tandis que 1, 2, 4 et 7 en sont absents
  • Les tableaux lines et columns gèrent les contrôles de doublons pour les lignes et les colonnes
  • Un nouveau tableau est nécessaire pour vérifier les doublons au niveau des cellules

Grille en champs de bits par cellule

  • Le tableau cells reflète toute la grille en représentant chaque cellule par un entier de 9 bits : var cells = [_]u9{0} ** 9;
  • Il est plus simple d’accéder à cells comme à une matrice 3x3
  • On remplit le tableau cell de manière similaire à ce qui a été fait pour la matrice 9x9
  • Il faut déterminer, à partir de la ligne et de la colonne d’un élément de la grille 9x9 d’origine, la ligne et la colonne correspondantes dans la matrice cell
  • Comme la division entière est très lente, on utilise le tableau cindx = [_]usize{ 0,0,0, 1,1,1, 2,2,2 }; pour fournir le résultat de la division
  • En accédant à la matrice avec la ligne i et la colonne j de l’élément de la grille 9x9, on vérifie par un ET bit à bit si un chiffre donné existe déjà dans la cellule correspondante : cell[cindx[i]][cindx[j]] & code
  • Si le résultat vaut 0, le chiffre n’est pas encore présent dans la cellule ; sinon c’est un doublon

Test des doublons d’un élément

  • Une fois combinés par OU bit à bit (|) tous les éléments précédents de la même ligne, colonne et cellule, il suffit d’effectuer un ET bit à bit avec le code de l’élément pour valider l’absence de doublon
if (((lines[i]|columns[j]|cell[cindx[i]][cindx[j]])&code) != 0) {  
    unreachable;  
}  
  • Si le résultat vaut 0, l’élément n’existe pas encore dans la ligne, la colonne ou la cellule
  • Si le résultat n’est pas 0, le programme s’arrête en exécutant l’instruction unreachable
  • C’est le moyen le plus simple d’exprimer explicitement une erreur d’exécution en Zig
  • Le code réel affiche également des détails sur l’endroit où l’erreur s’est produite
  • Exemple : si l’on remplace le 0 juste après le premier 8 de la chaîne d’entrée par 5, une erreur survient, car il y a déjà un 5 à la ligne 3 de la colonne 1

Mise à jour des structures de données

  • Dans la fonction set, une double boucle for parcourt les lignes pour copier chaque nouvel élément de la chaîne d’entrée s dans la grille
    • La variable k conserve l’indice du nouveau caractère d’entrée dans la chaîne s
  • On convertit le caractère en retirant '0' pour obtenir un u4 (variable c)
  • Si le nouvel élément à insérer dans la grille n’est pas 0 (c != 0), le code calculé par décalage à gauche est copié dans chacune des grilles miroir
    • On effectue un OU bit à bit (|=) avec la grille miroir correspondante :
lines[i] |= code;  
columns[j] |= code;  
cell[cindx[i]][cindx[j]] |= code;  
  • Il n’est pas nécessaire de vérifier explicitement que c est compris entre 1 et 9, car l’opération de décalage déclenchera un overflow lors de son exécution
  • Exemple : si l’on remplace le 0 juste après le premier 8 dans la chaîne d’entrée par :, une erreur d’exécution se produit
  • Remplacer ce même 0 par / provoque également une erreur similaire
  • Le programme ne fonctionne que si les valeurs sont comprises entre 1 et 9, c’est-à-dire si la grille d’entrée ne contient que des chiffres décimaux
  • Comme beaucoup de grilles de Sudoku sur le web utilisent . à la place de 0, la fonction set contient la ligne if (s[k] == '.') c = 0;
  • Cela permet de contourner commodément l’opération de décalage, puisque c vaut alors 0

Prototypage et robustesse

  • Les erreurs forcées dans les deux sections précédentes illustrent une fonctionnalité importante de Zig
  • La première est la robustesse de Zig : dans le cas de l’opération de décalage, un comportement incorrect n’est pas autorisé et est détecté à l’exécution
  • Tout semble orienté vers l’efficacité, mais il s’agit d’un cas typique où la performance n’est pas échangée contre la robustesse
  • En C, si une opération de décalage perd des bits, c’est le problème du programmeur, ce qui peut se traduire par de meilleures performances d’une instruction assembleur donnée
  • L’autre fonctionnalité est la possibilité d’utiliser les blocs de test pour le prototypage
  • Les applications possibles sont innombrables, et celle montrée ici n’est qu’un débogage d’une situation précise lorsqu’une erreur survient
  • Rien qu’avec ces fonctionnalités, Zig offre des capacités étonnantes, très rares dans un langage de programmation, en particulier dans un langage compilé

Conclusion

  • Zig repose sur trois éléments clés : compatibilité C, compilation croisée et installation simple
  • Ces caractéristiques montrent qu’il pourrait devenir un nouveau standard des langages de programmation système
  • De nombreux avantages autrefois réservés aux langages interprétés migrent progressivement vers les langages compilés afin d’offrir de meilleures performances
  • Zig se distingue particulièrement par son concept d’exécution à la compilation, qui le rapproche fortement des langages interprétés
  • C’est ce qui rend Zig à la fois particulièrement différent et puissant, tout en le rendant parfois difficile à comprendre

1 commentaires

 
GN⁺ 2025-11-08
Avis Hacker News
  • Cet article affirme au départ que « Zig n’est pas simplement un langage, mais une manière totalement nouvelle de programmer », mais en réalité il n’aborde presque aucune fonctionnalité propre à Zig
    L’inférence de types, les structs anonymes, les labeled break, etc. existent déjà depuis longtemps dans d’autres langages
    La vraie particularité, c’est comptime, et ce point n’est même pas mentionné
    Ce n’est pas un concept entièrement nouveau comme les macros Lisp, mais la façon dont Zig l’utilise à la place des génériques est intéressante
    Malgré tout, la thèse de l’article paraît fortement exagérée

    • On pourrait dire la même chose de Rust, comme d’une « manière totalement nouvelle »
      Rust permet d’exprimer clairement le moment où le code s’exécute, et sa conception semblable à un moteur de requêtes parcourant tout l’espace du code est impressionnante
    • Le langage D prenait déjà en charge l’exécution de fonctions à la compilation depuis 2007
      Voir la documentation D
      Si c’est une const-expression, l’exécution se fait automatiquement
    • Regrouper C/C++ n’a plus vraiment de sens aujourd’hui
      Ce sont des langages totalement différents, comme Java et Scala
    • comptime n’est pas une invention magique, mais plutôt une version moderne de la métaprogrammation
      Zig est plus propre que les templates C++, mais cela ressemble davantage à une alternative pratique qu’à une révolution
      Personnellement, je comprends mal cet enthousiasme excessif, comme à l’époque de Rust
    • En voyant l’expression « manière totalement nouvelle », je m’attendais à un nouveau paradigme comme LISP ou Prolog, mais il n’y avait rien de tel
      J’ai lu toute la documentation de Zig et je suis resté perplexe de n’y trouver rien de vraiment surprenant
  • Le plus gros problème de Zig, c’est qu’on ne peut pas attacher de données aux erreurs
    Les erreurs ne sont transmises que par un canal auxiliaire, ce qui rend le débogage difficile, et au final les développeurs finissent par omettre les données d’erreur
    Voir cette issue liée
    Avec un simple code comme AccessDenied, il est difficile de comprendre la cause

    • J’ai lu l’article de matklad, et son approche consistant à séparer les codes d’erreur des informations de diagnostic m’a paru convaincante
      En pratique, même avec un objet Error complexe, on a souvent besoin d’un canal de diagnostic distinct
    • Dans les langages système, attacher des données aux erreurs n’est pas toujours une bonne idée
      À cause du coût en performance ou de l’état du système, il est parfois plus sûr de gérer cela via un binding tardif selon le contexte
      Zig privilégie cette philosophie de précision et de déterminisme
    • Dans Zig, l’ajout d’informations définies par l’utilisateur dans les stack traces d’erreur est aussi en discussion
      Voir cette issue liée
      Mais ce qu’il faut vraiment, c’est du logging structuré et un suivi du contexte fondé sur la pile d’appels
    • std.zon est souvent cité comme bon exemple, et la communauté cherche à rassembler différents patterns de gestion d’erreurs pour les intégrer au standard
    • Empêcher d’attacher des données aux erreurs pousse au contraire à concevoir des erreurs plus claires
      Cela peut éviter que des développeurs paresseux ajoutent systématiquement des données partout
  • Je suis d’accord avec l’idée que la manière même dont Zig est développé constitue une nouvelle façon de développer un langage
    Ce processus d’évolution lent, où les fonctionnalités sont examinées avec soin et le superflu éliminé, est impressionnant

    • Mais ce type d’approche est aussi courant en Java, Rust et ailleurs
      J’aimerais entendre plus concrètement ce qui serait vraiment propre à Zig
  • J’aime le fait qu’on puisse installer Zig via PyPI
    Le paquet ziglang s’installe avec pip install ziglang et peut être utilisé immédiatement
    On peut aussi compiler du code C avec uvx

    • Ce mode d’installation est possible parce qu’un wheel Python peut embarquer un logiciel arbitraire
    • Mais cette approche donne aussi l’impression d’une réinvention moins pratique que nix
    • J’aimerais que Nim propose lui aussi cette option d’installation
    • Personnellement, je trouve que micromamba ou pixi sont de meilleures solutions de gestion de paquets que pip/uv
    • Grâce aux outils d’IA, il est désormais beaucoup plus facile d’apprendre n’importe quel langage
  • C’est dommage de présenter comme des « innovations » de Zig des fonctionnalités déjà présentes dans Ada, Object Pascal ou Modula-2
    Le fait de les réhabiller avec une syntaxe de style C rend intéressant ce phénomène où des idées vieilles de 40 ans paraissent neuves

  • L’introduction de l’article était bonne, mais ensuite cela se résume à une simple liste de fonctionnalités de Zig
    La syntaxe intuitive de Zig et son contrôle de flux explicite (defer, etc.) sont séduisants
    Grâce à comptime, il n’est pas nécessaire d’apprendre une syntaxe de macros séparée

    • Le vrai charme de Zig, c’est une conception sans redondances inutiles
      Tous les composants s’emboîtent naturellement, si bien que même lors d’une première utilisation, on a l’impression d’employer un outil familier depuis longtemps
    • L’analyse de la syntaxe de Zig par matklad vaut aussi le détour
  • La syntaxe for (0..9) de Zig est intuitive, mais comme il s’agit d’un intervalle ouvert, elle prête souvent à confusion
    Comme avec range(0, 9) en Python, on oublie facilement si la dernière valeur est incluse ou non

    • Rust distingue clairement 0..9 et 0..=9
    • La cohérence de Zig, qui n’utilise que des intervalles semi-ouverts, réduit au contraire les erreurs
      La taille d’un intervalle se calcule simplement comme une différence, et le parcours en sens inverse devient lui aussi plus simple
    • Odin distingue plus explicitement 0..<5 (ouvert) et 0...5 (fermé)
  • Je n’aime pas les règles d’identifiants de Zig
    Le mélange de snake_case et de camelCase est étrange
    En revanche, le système de build, les allocateurs mémoire et l’expérience de compilation sont excellents
    J’utilise surtout Rust, mais ma curiosité pour Zig reste intacte

    • Pareil pour moi. Personnellement, je ne respecte pas la convention de nommage des fonctions privées
      Les conventions de préfixes des bibliothèques C sont tout aussi pénibles
  • L’attrait de Zig ne vient pas d’une seule fonctionnalité, mais de l’accumulation de décisions pragmatiques
    Des choix qui semblaient radicaux au départ deviennent convaincants à mesure qu’on les comprend mieux
    Zig est un langage qui récompense les développeurs curieux

    • J’ai créé un petit jeu avec Odin, et l’expérience a été vraiment agréable
  • L’une des raisons pour lesquelles Zig est appréciable, c’est qu’il reconnaît la réalité du code système bas niveau
    Beaucoup de langages ignorent cet aspect pour des raisons esthétiques, mais pas Zig

    • Si l’on va voir la définition de la bibliothèque standard, on peut même observer directement la gestion de cas particuliers comme l’OS Plan9
      Voir la documentation de page_allocator
    • Cela dit, il faudrait davantage d’exemples concrets pour étayer ce genre d’affirmation