1 points par GN⁺ 2025-01-08 | 1 commentaires | Partager sur WhatsApp
  • La métaprogrammation à la compilation (comptime) de Zig se lit comme du code runtime, tout en permettant de produire du code d’exécution aussi efficace qu’une fonction C écrite à la main pour des tâches répétitives comme sommer les champs d’une structure
  • Au lieu d’une syntaxe dédiée aux génériques, l’usage de comptime T: type et de anytype pour gérer la programmation générique est l’un des principaux cas d’utilisation du comptime de Zig
  • Le runtime, le comptime et le système de build partagent le même langage Zig, ce qui permet même d’exécuter une fonction comme Fizz Buzz pendant la compilation pour produire un tableau précalculé
  • Le compilateur évalue d’abord les parties possibles, et ne conserve comme bytecode de sortie destiné à devenir du code machine que les parties nécessitant des valeurs runtime
  • Le comptime de Zig peut résoudre des problèmes similaires à la génération de code textuel, mais sans remplacer arbitrairement des mots-clés comme les macros C, ce qui réduit la charge de lecture et de débogage

Comment lire le comptime de Zig

  • Le comptime de Zig est une fonctionnalité de métaprogrammation, mais lorsqu’on lit le comportement final, il n’est pas nécessaire de se préoccuper fortement de la distinction entre compilation et runtime
  • Le troisième point de zig zen, « Favor reading code over writing code », sert de critère important
    • La métaprogrammation fondée sur les macros ou la génération de code peut créer deux couches : le code source et le code étendu
    • Cette couche d’indirection complique la lecture du code, le débogage et la modification du comportement
  • Le code runtime qui calcule la somme du tableau [3]i64 = .{1,2,3} est comparé côte à côte avec du code comptime qui additionne les champs a, b, c de la structure MyStruct
    • inline for (comptime std.meta.fieldNames(MyStruct)) parcourt les noms de champs
    • @field(my_struct, field_name) accède à un champ à partir d’un nom de champ connu à la compilation
  • L’exemple de somme des champs d’une structure ressemble à de la réflexion runtime, mais l’exécutable ne conserve qu’un code aussi efficace qu’une fonction de somme écrite à la main pour ce type de structure
  • Le processus de transformation de l’exemple ne reproduit pas exactement l’implémentation de Zig : il s’agit plutôt d’une explication conceptuelle
  • L’équipe cœur de Zig travaille sur un débogueur capable d’exécuter pas à pas du code mêlant comptime et runtime comme dans l’exemple

Traiter les génériques avec comptime

  • Zig ne possède pas de fonctionnalité générique distincte
  • Pour rendre un type générique, on écrit une fonction qui reçoit un type et renvoie un type
    • Exemple : pub fn GenericMyStruct(comptime T: type) type
    • Les champs a, b, c de la struct renvoyée utilisent tous le type T
  • Les fonctions génériques peuvent être écrites de la même manière
    • Exemple : fn quadratic(comptime T: type, a: T, b: T, c: T, x: T) T
    • On peut préciser explicitement l’argument de type, comme dans quadratic(f32, ...) et quadratic(i64, ...)
  • Si le type d’un argument n’est pas important pour d’autres parties de la signature de fonction, il peut être inféré avec le type spécial anytype
  • Cette approche ne suffit pas à expliquer tout comptime, mais elle constitue un point de départ pour comprendre les opérations génériques courantes en Zig

Exécuter le même code Zig à la compilation

  • Zig utilise le même langage pour le runtime, le comptime et le système de build
  • L’exemple Fizz Buzz commence par écrire la fonction fizzBuzz(writer) comme une fonction d’affichage runtime ordinaire
    • Elle parcourt les nombres de 1 à 100
    • Elle affiche fizzbuzz pour les multiples de 3 et de 5, fizz pour les multiples de 3, buzz pour les multiples de 5, et le nombre dans les autres cas
  • Si l’on exécute cette même fonction fizzBuzz dans un bloc comptime, toute la sortie peut être précalculée
    • std.io.countingWriter(std.io.null_writer) calcule d’abord le nombre d’octets nécessaires
    • Un tableau buffer: [cw.bytes_written]u8 est créé avec la longueur calculée
    • On écrit de nouveau dans std.io.fixedBufferStream(&buffer) pour créer la valeur full_fizzbuzz
  • Si l’on ne mesure que la section centrale, la version précalculée s’exécute environ 9 fois plus vite
    • L’exemple lui-même étant petit, le temps d’exécution total dépend davantage d’autres facteurs
  • Le comptime et le runtime diffèrent par les valeurs et fonctionnalités accessibles
    • Les valeurs comptime_int, comptime_float et type ne sont accessibles qu’en comptime
    • Certaines fonctions n’acceptent que des arguments comptime et deviennent de fait réservées au comptime
    • Les appels système et les fonctionnalités qui les utilisent ne sont accessibles qu’au runtime
  • Le code qui n’utilise pas ces fonctionnalités réservées se comporte de la même manière en comptime et au runtime

Comprendre comptime comme une évaluation partielle

  • On peut voir comptime comme une évaluation partielle (partial evaluation) qui a lieu pendant la compilation
  • L’évaluation partielle consiste, lorsqu’une fonction ne reçoit qu’une partie de ses arguments, à remplacer et calculer d’abord les expressions qui n’utilisent que des valeurs déjà connues, puis à créer une nouvelle fonction qui ne prend que les arguments inconnus restants
  • L’exemple de somme d’un tableau montre le processus d’évaluation étape par étape
    • La boucle for est décomposée en instructions individuelles pour chaque itération
    • array[0], array[1] et array[2] sont respectivement remplacés par 1, 2 et 3
    • sum est mis à jour de 0 → 1 → 3 → 6
    • À la fin, le code est simplifié jusqu’à une forme comme std.debug.print(..., .{6})
  • L’exemple de somme des champs d’une structure se lit de la même manière
    • Comme inline for est exécuté en comptime, il est déplié en instructions individuelles pour les champs "a", "b", "c"
    • @field(my_struct, "a") devient my_struct.a
    • La fonction finale contient directement sum += my_struct.a, sum += my_struct.b, sum += my_struct.c
  • Cette approche permet d’exprimer directement l’intention dans le code, tout en réduisant la nécessité de laisser des commentaires demandant de mettre à jour manuellement la fonction de somme lorsque les champs changent

Évaluation à la compilation et émission de code runtime

  • Le comptime de Zig peut aussi être vu comme une combinaison d’évaluation à la compilation et d’émission de code runtime
  • Le code d’entrée est exécuté par le compilateur
    • Les instructions connues à la compilation sont évaluées immédiatement
    • Les instructions qui nécessitent des valeurs runtime sont ajoutées au code de sortie
  • Cette perspective est la plus proche du fonctionnement réel du compilateur Zig
    • Zig commence par analyser la syntaxe
    • Il transforme le code en bytecode pour une machine virtuelle
    • La VM évalue les parties possibles et émet un nouveau bytecode pour les parties qui doivent être traitées au runtime
    • Ce nouveau bytecode est ensuite converti en code machine
  • Une condition comme if avec une entrée runtime émet les deux chemins
  • Le code mort ne fait pas l’objet d’une analyse sémantique
    • Même si l’on écrit une fonction incorrecte, elle peut ne provoquer aucune erreur de compilation tant qu’elle n’est pas effectivement utilisée
    • Cette propriété rend la compilation plus efficace et permet une compilation conditionnelle naturelle sans #ifdef
  • Les noms de types en Zig sont eux aussi des expressions qui évaluent des valeurs type en comptime
    • Même un nom de type qui semble simple, comme le type d’un argument de fonction, est le résultat d’une évaluation comptime
    • C’est cette propriété qui rend possibles les exemples de génériques précédents
    • Si nécessaire, on peut calculer un type avec des expressions plus complexes
  • L’analyse statique de Zig est plus complexe que celle de nombreux autres langages à typage statique
    • Pour déterminer tous les types, une grande partie du compilateur doit être exécutée
    • Tant que les outils n’ont pas rattrapé leur retard, les fonctionnalités d’éditeur comme l’autocomplétion ne fonctionnent pas toujours parfaitement

Rapport avec la génération de code textuel

  • Le comptime de Zig diffère dans sa forme de la génération de code textuel, mais il peut résoudre des problèmes similaires
  • La fonction de génération de code writeSumFn affiche le code source de la fonction sumFields avec writer.print
    • Le code exécuté dans le générateur correspond à la partie comptime de Zig
    • Le code produit par le générateur correspond au code runtime
  • La version comptime de Zig exprime la même logique de manière plus directe
    • std.meta.fieldNames(MyStruct) récupère les noms des champs
    • inline for et @field construisent le code d’accès aux champs
  • La génération de code qui utilise des informations de type devient plus simple en Zig
    • L’approche par génération textuelle doit résoudre séparément la question de l’origine des informations sur les noms de types et de champs
    • Zig permet de manipuler directement le type lui-même et les informations de champs en comptime
  • Lorsqu’on utilise des entrées comme une spécification externe, on peut inclure le contenu du fichier avec @embedFile, puis le parser normalement
  • Dans l’exemple de structure générique, comptime traite les références aux noms de types plus directement que la génération de code textuel
    • La génération textuelle doit concaténer des chaînes comme MyStruct_{s} pour maintenir des noms cohérents
    • Comptime utilise directement une fonction comme GenericMyStruct(T)
  • Il existe aussi des exceptions
    • Il est possible de créer un type dont les noms de champs sont déterminés en comptime
    • Dans ce cas, il faut appeler une fonction intégrée avec une spécification contenant la liste des définitions de champs
    • On ne peut pas définir de déclarations comme des méthodes sur un tel type
    • Cela ne limite pas l’expressivité du code lui-même, mais limite la forme de l’API qui peut être exposée à d’autres codes
  • La plupart des opérations raisonnables possibles avec des macros textuelles comme en C sont aussi possibles avec comptime, mais la forme du code peut être très différente
  • Zig ne fournit pas de mécanisme permettant de remplacer des mots-clés par d’autres noms comme le font les macros textuelles

Pour aller plus loin et exemples dans la bibliothèque standard

  • Zig lui-même n’est pas un langage qui repose uniquement sur comptime, et l’on peut en apprendre davantage sur le site officiel
  • La référence du langage couvre les fonctionnalités concrètes de comptime
  • La bibliothèque standard de Zig contient des exemples d’utilisation de comptime
    • La fonction de formatage utilisée par std.debug.print est une puissante fonction générique
      • Beaucoup de langages parsèrent la chaîne de formatage au runtime et peuvent ajouter un validateur distinct pour détecter les erreurs tôt
      • Zig parse la chaîne de formatage en comptime, produit un code de sortie efficace et effectue aussi toutes les validations à la compilation
    • ArrayList est un conteneur générique relativement simple mais complet
    • La fonction main de Zig peut avoir plusieurs types de retour, et ce n’est pas de la magie du compilateur : c’est géré par du code comptime ordinaire

1 commentaires

 
GN⁺ 2025-01-08
Avis sur Hacker News
  • J’aurais aimé que l’on traite plus en profondeur les problèmes déjà identifiés de la programmation au moment de la compilation, plutôt que de la louer sans esprit critique
    La programmation par étapes n’a rien de nouveau, et la famille Lisp fait des choses similaires depuis des décennies, ce qui a déjà mis en lumière de nombreux compromis de conception et problèmes. Par exemple, ce type de généricité casse la paramétricité (parametricity), ce qui rend l’inférence à partir de la seule signature de type d’une fonction plus difficile. On ne sait pas non plus clairement comment Zig traite les types génériques récursifs, ni à quel moment et dans quel ordre interagissent la vérification de types et le calcul au moment de la compilation. L’article dit que le code de compilation peut générer non seulement des valeurs mais aussi du code, sans toutefois aborder l’hygiène (hygiene). Pour une discussion liée, https://typesanitizer.com/blog/zig-generics.html est un bon texte

    • J’aime beaucoup Zig, je le suis depuis plusieurs années et je l’utilise parfois, mais comptime brille surtout pour les génériques, l’initialisation à la compilation de structures de données complexes et la génération de code selon la cible
      En dehors de ça, voir comptime dans du code Zig me fait parfois penser « ah… ». Comme avec les macros Lisp, on peut facilement s’en servir pour éviter un problème qui n’existerait pas, ou disparaîtrait avec une meilleure structure du code. L’exemple de l’article où l’on parcourt les champs d’une structure pour additionner des valeurs est typique de la manière dont les gens utilisent comptime en pratique, alors que dans bien des cas il vaut mieux utiliser une structure de données réellement itérable comme std.enums.EnumArray
    • Je me demande si le fait de dire que la paramétricité est cassée relève de la compréhension du comportement d’une fonction par un programmeur en pratique, ou d’une inférence dans un système de théorie des types comme le calcul lambda typé
      La préoccupation est claire d’un point de vue théorique, mais son importance dans un langage pratique est une autre question. Les templates C++ cassent eux aussi la paramétricité via la spécialisation de templates, sans que cela pose en général de gros problèmes en pratique, et cela permet même des optimisations. Par exemple, une implémentation comme std::vector<bool>, qui stocke réellement chaque élément sur 1 bit, n’est possible qu’avec ce type de souplesse
    • En tant qu’auteur, j’ai écrit ce texte pour remettre en ordre ce que je n’avais pas réussi à bien expliquer lors d’une rencontre, donc il vise davantage à expliquer qu’à critiquer
      Je vois le comptime à la Zig comme un compromis direct entre faculté d’inférence et expressivité, comparé à des signatures de fonctions définies de manière plus statique. Cela n’affecte que le code qui fait ce genre d’inférence à partir des types, et c’est quelque chose qu’on peut choisir selon les besoins. Quand la signature de type ne suffit pas, lire quelques dizaines de lignes de source m’a généralement permis de comprendre comment l’utiliser, sans rester confus très longtemps. Pour les types génériques récursifs, utiliser un nom en cours de déclaration produit « dependency loop detected », mais il existe des contournements, et l’exemple générique de l’article se référence lui-même. La vérification de types se fait pendant comptime ; par exemple, le premier @compileLog("Hi") s’exécute, mais si const a: u32 = "42"; produit ensuite une erreur de type, on n’atteint pas le second log. Il peut y avoir des problèmes subtils de vérification de types entre comptime et l’exécution, mais on ne les voit que dans du code assez ésotérique, ils se résolvent facilement, et l’équipe principale en est consciente, donc on peut espérer que ce sera traité avant la 1.0. Il faudrait aussi expliquer plus précisément ce que signifie exactement l’hygiène
    • Le fait de pouvoir inférer des choses en regardant uniquement la déclaration d’une fonction est distinct de l’exécution au moment de la compilation
      Si une fonction a un objectif logique clair, avec un bon nom et des noms de paramètres bien choisis, on peut déjà inférer beaucoup de choses depuis sa déclaration seule. Les labels de paramètres, les noms de types et d’autres éléments que le programmeur peut choisir peuvent aussi être vus comme faisant partie du nom. Je suis d’accord avec la conclusion du titre de l’article lié, mais l’argumentation ressemble plus à un texte de débat cherchant à marquer des points qu’à un raisonnement substantiel. Un meilleur cadrage serait souplesse contre complexité. Un système de génériques figé, s’il est bien conçu, est plus simple qu’un système programmable mais moins flexible. La force de Zig est que son langage de métaprogrammation est presque identique au langage général, et que ce langage général est lui-même simple, ce qui réduit le coût de complexité supplémentaire. En revanche, cela introduit une complexité où le code de compilation et le code d’exécution se mélangent avec peu d’indices, ce qui rend plus difficile pour le programmeur de distinguer ce qui s’exécute, quand. Peut-être qu’un serveur de langage ou un plugin d’éditeur pourrait fournir un « shader comptime » donnant une couleur de fond différente au code comptime
    • Il est frappant que ce sujet soit discuté à partir d’une démo de 15 minutes, avec une compréhension insuffisante des compromis réels. Le texte de Varun Gandhi mis en lien est excellent
      D’après mon expérience avec Rust, beaucoup de choses que les gens essaient de faire avec les génériques constants seraient plus faciles avec une fonctionnalité comme comptime. Autoriser de l’arithmétique sur les génériques constants tout en conservant la paramétricité est difficile à implémenter ; et si ce que l’on veut vraiment, c’est par exemple « un trait de fonction de hachage dont la taille de sortie est N », alors abandonner la paramétricité pour ce besoin précis et transformer N en génération de code à une étape antérieure peut être acceptable. Mais les macros Rust sont à la fois trop flexibles et trop pénibles pour être utilisées ainsi. Malgré tout, dès qu’on remplace le polymorphisme paramétrique par une simple fonctionnalité naïve de génération de code, les ennuis commencent
  • D l’a depuis 17 ans. Les fonctionnalités de D migrent régulièrement vers d’autres langages
    Zig marque les blocs exécutés pendant la compilation avec le mot-clé comptime, tandis qu’en D, ce n’est pas un mot-clé mais le fait qu’il s’agisse d’une expression constante qui détermine l’exécution. Par exemple, int s = sum(3, 4); s’exécute à l’exécution, alors que enum e = sum(3, 4); s’exécute au moment de la compilation. En évitant les variables globales non constantes, les entrées/sorties et les appels à des fonctions système comme malloc(), un grand nombre de fonctions peuvent être exécutées à la compilation sans modification. Grâce à la gestion automatique de la mémoire de D, l’allocation mémoire est également possible

    • L’un de mes usages préférés est la génération de tables statiques
      Autrefois, il fallait écrire un programme séparé pour créer une table statique, mais l’exécution de fonctions à la compilation a supprimé ce besoin. Les valeurs d’initialisation du tableau tytab proviennent d’une lambda qui calcule et renvoie le tableau. L’exemple complet se trouve sur https://github.com/dlang/dmd/blob/master/compiler/src/dmd/ba.... Un autre usage courant de la CTFE est la création de DSL
    • Je ne veux pas manquer l’occasion de dire merci pour avoir créé D. Cela dit, je m’interroge sur les limites de l’exécution au moment de la compilation
      Si la machine sur laquelle tourne le compilateur est différente de celle sur laquelle le programme sera exécuté, comment le compilateur D garantit-il la validité du résultat ? Par exemple, je me demande comment le compilateur sait que int s = sum(100000, 1000000) donnera la même valeur sur toutes les machines x86. Il peut y avoir de subtiles différences selon les générations de CPU ; comment garantit-on qu’un calcul effectué sur l’hôte donnera bien le même résultat sur la machine cible, ou bien suppose-t-on simplement que si l’architecture est la même, l’hôte et la cible sont suffisamment proches ?
    • Le ImportC de D permet aussi de faire de la CTFE avec du code C
      Avec int sum(int a, int b) { return a + b; }, on peut écrire quelque chose comme _Static_assert(sum(3, 4) == 7, "look ma, check at compile time!");. Je ne sais pas pourquoi la norme C n’a pas ajouté ça, et ça fonctionne bien
    • Pour être juste, Zig permet lui aussi d’appeler une même fonction à la fois dans un contexte d’exécution et dans un contexte comptime
      square(2) est un appel à l’exécution, et comptime square(3) un appel au moment de la compilation. Un appel comptime produit une erreur de compilation si quelque chose n’est pas compatible avec la compilation, et c’est à mon avis une fonctionnalité importante. C’est un signal d’alerte utile quand un code censé être exécuté à la compilation bascule par inadvertance vers une évaluation à l’exécution à cause d’un changement des arguments d’entrée
  • Zig a l’air intéressant, mais j’aimerais qu’il y ait une surcharge d’opérateurs
    La plupart des arguments contre la surcharge d’opérateurs ne me convainquent pas vraiment. L’idée qu’on ne sache pas ce qui se passe réellement en interne n’est pas suffisante, puisqu’on peut tout aussi bien faire en sorte qu’une fonction appelée add effectue une multiplication. iostreams de C++ ou boost::spirit sont souvent cités comme exemples d’abus, mais dans d’autres langages qui ont la surcharge d’opérateurs, on voit rarement ce genre de choses, et cela ressemble plutôt à un problème propre à C++

    • Une solution à la OCaml semblerait bien adaptée aux usages de Zig
      En OCaml, on peut redéfinir des opérateurs, mais seulement dans le contexte d’un autre module. Par exemple, si + a été redéfini dans le module Vec3, on peut écrire Vec3.(a + b + c + d) ou let open Vec3 in a + b + c + d. En lisant le code, on ne se demande jamais « d’où vient ce + ? », et c’est bien mieux que a.add(b).add(c).add(d). Cela dit, Zig semble déjà en train de se figer, donc il paraît difficile qu’un grand changement vienne encore résoudre ce problème
    • Avec les fonctions __ de Python et PHP, on peut ignorer toute la magie qui tourne derrière
      L’approche de Zig est rafraîchissante, et la capacité à suivre le code est plus importante que les quelques secondes perdues à taper quelques caractères de plus
    • Je pense qu’une grande partie du rejet de la surcharge d’opérateurs est liée aux performances à l’exécution inattendues
      La solution idéale serait que le langage fournisse des opérateurs définis par l’utilisateur qui rendent explicitement visible la surcharge. Par exemple, avec une forme préfixée ou suffixée comme let c = a |+| b, la personne qui lit le code sait que l’opération |+| est en réalité un appel de fonction. Cela peut toujours être abusé, mais au moins cela réduit l’une des principales inquiétudes
    • En C++, je n’ai utilisé la surcharge d’opérateurs que pour les maths vectorielles/matricielles, et dans ce cas c’était vraiment utile
      Ce serait probablement acceptable si le langage offrait directement une syntaxe de maths vectorielles, comme les langages de shading. Zig a au moins un type @Vector() un peu similaire à la Vector Extension de Clang, mais malheureusement pas à l’Extended Vector Extension. Voir https://ziglang.org/documentation/master/#Vector et https://clang.llvm.org/docs/LanguageExtensions.html#vectors-...
    • Les opérations de base d’algèbre linéaire sur des tableaux de nombres devraient peut-être simplement être intégrées au langage au lieu de passer par la surcharge
      Je ne sais pas s’il existe déjà une proposition en ce sens
  • fieldNames ressemble beaucoup à fieldPairs de Nim, et c’est une structure extrêmement pratique
    Cela rend la sérialisation efficace très facile à mettre en place, et récemment j’ai même utilisé fieldPairs pour implémenter en une vingtaine de lignes une vérification de sûreté des threads à la compilation pour un type. À mon avis, ce genre de fonctionnalité devrait faire partie du socle standard des langages de programmation. C’est l’un des points qui me manquent le plus en Rust, où, la dernière fois que j’ai essayé, on restait limité à des macros non typées. C’est beaucoup trop contraignant. On n’a plus qu’à espérer qu’une struct d’un crate ait serde implémenté, et on ne peut pas non plus construire soi-même par programme une struct ayant les mêmes champs

    • Il y a eu à une époque des discussions autour de la réflexion à la compilation, et cela aurait sans doute pu inclure ce genre de fonctionnalité, mais le tout semble avoir disparu avec le drame associé
      C’est vraiment dommage. Des choses comme serde seraient bien plus faciles à implémenter avec de la réflexion à la compilation
    • Avec de la réflexion à la compilation, on peut créer des frameworks comme des ORM ou des frameworks web
      Le seul compromis, c’est qu’il faut inclure ce genre de bibliothèques sous forme de code source
  • Depuis que j’ai essayé d’écrire une bibliothèque de parsing C à peu près aboutie, je ne suis plus vraiment convaincu par l’idée que le métaprogrammation soit absolument nécessaire au niveau du langage
    Si on veut générer des structures, de la sérialisation, des propriétés, de l’instrumentation, etc., il suffit d’écrire un programme C classique qui traite des fichiers source et produit des fichiers source, puis de l’exécuter d’abord dans le script de build. Je me demande d’ailleurs comment les gens déboguent et testent ce genre de méta-programmes. Le mien étant un programme C ordinaire, j’utilise simplement le même débogueur et les mêmes outils que pour le reste du code

    • C’est précisément le genre de chose que les gens n’ont pas envie de faire
    • C# — ou plus exactement Roslyn/dotnet — fournit cela plutôt bien
      Le compilateur lui-même est écrit dans le même langage, donc on peut brancher un plugin qui accède à l’AST en lecture seule et émet du code source C#. Pour le débogage, il faut un peu plus de travail afin de mettre en place un bon framework de test, mais ensuite on peut exécuter le compilateur avec le plugin depuis un framework de tests unitaires standard et l’inspecter avec un débogueur interactif
    • C’est la même approche que celle défendue par Ryan Fleury et d’autres, et c’est une méthode tout à fait valable
      « Exécution arbitraire au moment de la compilation en C : cl /nologo /Zi metaprogram.c && metaprogram.exe, cl /nologo /Zi program.c. Le code exécuté à la compilation tourne à vitesse native, peut être débogué, et reste entièrement procédural et arbitraire. Il n’est pas nécessaire que le compilateur exécute le code. » https://x.com/ryanjfleury/status/1875824288487571873
    • Je ne connais pas assez Zig, mais la force de Lisp vient des S-expressions, autrement dit de la manipulation de l’AST
      En C, pour faire cela, il faut écrire un parseur C complet pour le programme C qui traite le fichier source
    • J’ai déjà fait quelque chose de similaire en Python avec le JIT numba
      J’écrivais du code Python qui générait du code Python, lequel était ensuite recompilé. C’était fragile et affreusement chaotique, et c’est l’une des grandes raisons qui m’ont fait quitter Python. C’est un peu comme demander pourquoi on ne passe pas simplement tous les arguments de fonction sous forme de chaînes. Certaines personnes écrivent du code basé sur des chaînes, mais cela devrait rester rare, et le langage devrait offrir un moyen de l’éviter
  • Un motif intéressant, c’est la possibilité de générer des structures au moment de la compilation
    J’ai expérimenté avec PyTorch en générant un fichier JSON, puis en le lisant via @embedFile dans Zig, avant de produire une structure avec une méthode run spécifique pour implémenter un réseau de neurones. En théorie, cela permettrait au compilateur d’optimiser directement le réseau de neurones. Je n’ai pas encore réussi à démontrer de gros bénéfices, mais tout le réseau se trouve sur la pile, sans allocation dynamique. Je ne sais pas si c’est réellement une bonne chose

    • J’ai déjà fait ce genre de choses en écrivant un générateur de code en Python plutôt qu’en utilisant comptime
      Je ne suis pas certain que comptime dans Zig soit particulièrement plus rapide, et je n’ai pas non plus envie d’exécuter à chaque fois le parseur JSON qui génère la structure
    • Je me demande quel impact cela a sur le temps de compilation
  • C’est intéressant, mais dans ce cas je me retrouve à être d’accord avec les commentaires du genre « D l’a fait avant »

    • Oui, et c’est géré de manière hygiénique, sans passer par une approche de préprocesseur
  • Si comptime de Zig vous a impressionné, Nim mérite vraiment aussi qu’on s’y intéresse, avec son évaluation de code à la compilation et son système complet de macros sur l’AST

    • Nim est un langage amusant, mais je ne pense pas l’envisager pour un travail « sérieux »
      Il a les mêmes problèmes d’écosystème que d’autres langages de niche, avec en plus des mainteneurs controversés, des contributeurs clés qui ne semblent pas rester longtemps, et un financement principalement assuré par une entreprise crypto. Il y a dix ans, ce genre de choses m’aurait moins dérangé, mais aujourd’hui c’est différent
    • Zig a l’avantage de ne pas avoir d’exceptions
      Nim semble aussi vouloir s’éloigner des exceptions, mais les exceptions contaminent les fonctions et obligent à y penser même lorsqu’on n’utilise pas de fonctions qui en lancent[1]. La vie est trop courte pour gérer des flux de contrôle invisibles. [1]: https://github.com/status-im/nim-stew
  • Dans l’ensemble, Zig est un langage plutôt bon et il fait ce dont on a besoin
    Le fait de ne pas s’éloigner de son objectif est très important, et c’est pour cela que je n’aime pas trop voir certains langages utilisés partout simplement parce qu’ils sont populaires

  • J’aimerais qu’il existe quelque chose qui combine les capacités de métaprogrammation de Zig avec l’énorme écosystème, la communauté et la sûreté de Rust
    En termes de conception du langage, Zig me plaît bien davantage que Rust, mais en tant que programmeur amateur assez maladroit, je n’ai pas confiance dans ma capacité à écrire avec Zig quelque chose de réellement utile ou fiable, du moins pour l’instant

    • En venant de Rust, j’ai brièvement essayé Zig avant d’abandonner assez vite
      Parce que le compilateur ne m’aidait pas à détecter ce genre de problèmes au moment de la compilation. Je sais que Zig ne fait pas cette promesse, mais pour moi c’est un défaut rédhibitoire, donc Zig n’est probablement pas le bon langage. En revanche, le concept de comptime me plaît davantage que les macros de Rust
    • J’aimerais que la communauté Rust reste bien loin de Zig. C’est une blague, mais seulement à moitié.