Revisiter Let’s Build a Compiler
(eli.thegreenplace.net)- Le projet réimplémente le tutoriel « Let’s Build a Compiler » de Jack Crenshaw, publié entre 1988 et 1995, en Python et WebAssembly.
- L’original utilisait Pascal et l’assembleur Motorola 68000, mais ce travail le transforme en code exécutable dans un environnement moderne.
- Le cœur du tutoriel est la mise en œuvre directe d’un analyseur descendant récursif (recursive-descent parser) et une approche qui génère du code assembleur réel dès les premières étapes.
- La méthode de Crenshaw repose sur la traduction dirigée par la syntaxe (syntax-directed translation), mais montre ses limites au stade du traitement des types.
- Cette réimplémentation est notable car elle restaure ce classique sous une forme que les développeurs modernes peuvent exécuter et expérimenter directement.
Contexte du tutoriel classique
- Le « Let’s Build a Compiler » de Jack Crenshaw, publié entre 1988 et 1995, est un guide d’initiation à la construction de compilateurs qui reste fréquemment cité aujourd’hui.
- Le texte original est rédigé en Pascal et produit du code assembleur Motorola 68000.
- Même en 2025, 35 ans plus tard, il continue d’être régulièrement évoqué, notamment sur Hacker News.
- L’auteur de l’article l’a découvert en 2003 et en a été impressionné; en 2025, il l’a ensuite remis en forme en Python et WebAssembly.
Réécriture en Python et WebAssembly
- Plutôt qu’une simple lecture, le compilateur du tutoriel a été traduit en Python et implémenté pour générer de la WebAssembly (WASM).
- Le résultat est publié dans le dépôt GitHub (eliben/letsbuildacompiler).
- Le fichier
TUTORIAL.mdexplique comment chaque partie de l’original est mappée vers du code Python. - Les lecteurs peuvent tester du code directement exécutable tout en suivant le tutoriel original.
- Le fichier
Exemple de langage KISS et génération WASM
- Il présente le résultat de la compilation, avec le compilateur Python, d’un programme exemple du langage KISS conçu par Crenshaw.
- L’exemple inclut
procedure, des boucleswhileainsi que le passage par valeur et par référence.
- L’exemple inclut
- Le texte WASM généré contient une logique de gestion des paramètres passés par référence (by-reference parameter), avec très peu d’optimisations appliquées.
- La partie où la variable globale
Xest renvoyée implicitement depuis la fonctionmainconstitue un dispositif pratique pour les tests. - Le code WASM généré est utilisé dans des tests de validation des résultats attendus par exécution réelle.
Atouts du tutoriel
- Le tutoriel de Crenshaw construit progressivement un analyseur descendant récursif, en mettant l’accent sur la génération de code réellement opérationnel plutôt que sur une théorie trop complexe.
- À l’époque,
lexetyaccétaient considérés comme des standards, mais la simplicité et la clarté d’un parseur écrit à la main ont eu un impact majeur. - Par la suite, pendant 20 ans, l’auteur est devenu adepte des analyseurs descendants récursifs faits à la main.
- À l’époque,
- Alors que la plupart des cours de l’époque se concentraient sur l’analyse syntaxique, Crenshaw abordait la génération de code assembleur dès les premières phases.
Limites et améliorations possibles
- Le tutoriel utilise la traduction dirigée par la syntaxe (syntax-directed translation) pour générer directement le code pendant l’analyse syntaxique.
- Cette approche est utile pour l’apprentissage, mais elle a la limite que, sans disposer au préalable des informations de type, il est difficile d’optimiser.
- À partir de la partie 14, où sont introduits les types, il apparaît qu’une approche structurée basée sur une représentation intermédiaire (IR) ou un AST est nécessaire.
- Il n’est pas précisé pourquoi Crenshaw a interrompu le tutoriel après la partie 14, mais une relation possible avec cette limite est évoquée.
- L’auteur considère qu’une approche où la partie 14 sert de pivot, avec génération d’un AST suivie de la vérification des types et de la génération de code, serait plus adaptée.
Conclusion
- Le tutoriel original conserve une lisibilité et une utilité remarquables comme introduction à la construction de compilateurs.
- Cette version Python/WASM complète l’implémentation pour permettre aux développeurs modernes d’apprendre sans dépendre de technologies obsolètes.
- Mise à disposition via le dépôt GitHub, elle permet à chacun de faire des essais et est saluée comme une réinterprétation moderne offrant une expérience concrète de la structure d’un compilateur.
1 commentaires
Discussions sur Hacker News
J’aime ce tutoriel qui ne s’enlise pas dans les détails du frontend et génère dès le départ du code assembleur fonctionnel
L’approche de Ghuloum est particulièrement séduisante : partir d’une toute petite portion du langage, puis étendre progressivement l’ensemble
Un bon livre que j’ai découvert récemment est celui-ci ; même si C et x86 ne sont pas des cibles idéales pour les débutants, c’était une excellente ressource pour construire un premier compilateur
À noter que l’article de Ghuloum se trouve ici
Autrefois, le parsing était la partie la plus importante, donc les cursus étaient construits autour de ça ; aujourd’hui, ce qui compte davantage, c’est la manière d’implémenter les fonctionnalités du langage
Même si on écrit soi-même un compilateur, on vit désormais à une époque où il suffit de dire : « Claude, génère-moi un parser en descente récursive pour cette grammaire », et on obtient presque le bon résultat du premier coup
Les premiers ont tendance à raisonner en couches (layers), les seconds en pipeline (pipes)
L’approche en couches donne du code qui se compile, mais difficile à exécuter, tandis que l’approche en pipeline s’exécute facilement mais manque de structure
Au final, l’essentiel est de découper le développement en unités minimales exécutables
Je trouve que ce texte vise vraiment juste
Je m’intéressais déjà aux compilateurs avant l’université, et cette ressource était l’introduction la plus accessible que j’aie trouvée
À 17 ans, en construisant moi-même un parser en descente récursive, j’ai compris que même un problème qui paraît complexe devient simple si on le découpe en primitives adaptées
On trouve beaucoup de ressources sur les algorithmes, mais très peu sur la manière d’aborder un problème à travers ses primitives
Autrefois, les parsers à base de tables comme yacc dominaient, mais aujourd’hui la descente récursive est plus pratique et plus flexible
La génération de code par les LLM a encore facilité cette approche
Pour un compilateur jouet à vocation pédagogique, on peut tout à fait ignorer l’AST, l’IR et les optimisations, et passer directement à la génération de code
Dans le passé, en entreprise, quand nous avions créé un outil de test prenant en charge un sous-ensemble de C, nous faisions retourner au parser une structure de données exécutable au lieu de générer du code
Par exemple, une classe ForLoopStatement contenait testExpression, puis sa méthode Execute() l’appelait directement
C’est aussi un article important, cité dans The Mythical Man-Month de Fred Brooks
C’est conceptuellement très naturel et élégant
L’explication selon laquelle le tutoriel de Jack Crenshaw utilise une approche de syntax-directed translation m’a intrigué
Je me demande si cela signifie simplement compilateur en un seul passage, ou s’il s’agit d’un concept plus précis
Je comprends qu’il soit difficile de faire des optimisations fondées sur les types dans un langage statiquement typé, mais j’aimerais savoir s’il y a aussi des limites au niveau de la vérification de types
En revanche, sans IR, des optimisations avancées comme LICM, CSE ou GVN sont impossibles
Personnellement, je préfère une compilation en 2 passes par fonction : on construit d’abord l’IR, puis on génère immédiatement le code avant de jeter l’état, ce qui donne un résultat rapide et efficace
Même un compilateur en un seul passage peut séparer les étapes et ne faire la génération de code qu’à la fin
Le tutoriel de Crenshaw n’atteint pas un niveau vraiment pratique, mais il devient bien plus utile si on l’étend avec la technique de precedence climbing
Un bon article sur le sujet est disponible ici
J’ai revisité d’anciens fils sur « Let’s Build a Compiler »
Il y a eu plusieurs passages sur HN entre 2007 et 2023
En particulier, l’interview de Jack Crenshaw était très intéressante à lire, même s’il n’y avait pas de commentaires
Petite présentation / promo d’un projet perso : cwerg.org
Il utilise Python et la descente récursive pour le parsing, et sépare frontend et backend via un IR
Il génère des binaires ELF pour x86 et ARM, avec un objectif d’usage réel
En revanche, c’est complexe et pas du tout au format tutoriel
J’ai aussi un souvenir nostalgique d’avoir appris le tutoriel de Jack Crenshaw sur le newsgroup USENET comp.compilers
Pour info, ce groupe est toujours actif
Pour une approche moderne des compilateurs, je recommande cet article de blog de Cornell
À chaque utilisation, j’ai l’impression d’ajouter quelques pierres au sommet d’une pyramide
Les langages qui l’utilisent comme backend ont en commun des temps de compilation lents
Zig en est conscient et développe son propre backend, tandis que Rust reste encore prisonnier de compilations lentes
LLVM donne l’illusion que la complexité garantit la qualité et je pense que c’est une technologie qui affaiblit l’autonomie des développeurs
Cela me rappelle aussi d’autres technologies populaires aux initiales similaires
Pour des raisons proches, j’ai moi aussi écrit un tutoriel appelé tiny-optimizing-compiler
C’est un exemple concis qui ne couvre que les étapes intermédiaires et le backend d’un compilateur
Lien GitHub
Quand j’étais jeune, j’avais imprimé ce tutoriel pour l’emporter partout et le lire
Voir un compilateur se construire en si peu de temps était vraiment formidable
Des ressources comme Beej’s Guide font partie des documents les plus précieux de la formation en informatique, mais elles ne sont pas reconnues à leur juste valeur