- Proyecto que reimplementa el tutorial ‘Let’s Build a Compiler’ de Jack Crenshaw, publicado entre 1988 y 1995, usando Python y WebAssembly
- El original usaba Pascal y ensamblador Motorola 68000, pero en este trabajo se convirtió a código ejecutable en un entorno moderno
- El núcleo del tutorial es implementar directamente un parser descendente recursivo (recursive-descent parser) y generar código ensamblador real desde etapas tempranas
- El enfoque de Crenshaw usa traducción dirigida por la sintaxis (syntax-directed translation), pero muestra limitaciones en la etapa de manejo de tipos
- Esta nueva implementación destaca por restaurar el tutorial clásico en una forma que los desarrolladores actuales pueden ejecutar y experimentar por sí mismos
Contexto del tutorial clásico
- ‘Let’s Build a Compiler’ de Jack Crenshaw es una guía introductoria para crear compiladores publicada entre 1988 y 1995, y sigue siendo una referencia mencionada con frecuencia
- El texto original fue escrito en Pascal y generaba código ensamblador para Motorola 68000
- Incluso 35 años después, en 2025, sigue apareciendo con frecuencia en Hacker News y otros espacios
- El autor del artículo conoció este tutorial por primera vez en 2003 y le causó una gran impresión; en 2025 volvió a llevarlo a Python y WebAssembly
Reimplementación en Python y WebAssembly
- En lugar de limitarse a leerlo, tradujo el compilador del tutorial a Python y lo implementó para generar WebAssembly (WASM)
- El resultado está publicado en el repositorio de GitHub (eliben/letsbuildacompiler)
- El archivo
TUTORIAL.md explica cómo se corresponde cada parte del original con el código en Python
- Los usuarios pueden leer el tutorial original y al mismo tiempo experimentar con código realmente ejecutable
Ejemplo del lenguaje KISS y salida WASM
- Se muestra el resultado de compilar con la versión en Python del compilador un programa de ejemplo del lenguaje KISS diseñado por Crenshaw
- El ejemplo incluye
procedure, bucles while, y paso de parámetros por valor y por referencia
- El texto WASM generado incluye la lógica para manejar parámetros por referencia (by-reference parameter), con muy poca optimización aplicada
- La parte en la que la variable global
X se devuelve implícitamente desde la función main es un recurso pensado para facilitar las pruebas
- El código WASM generado se usa en pruebas que verifican los resultados esperados mediante ejecución real
Fortalezas del tutorial
- El tutorial de Crenshaw construye paso a paso un parser descendente recursivo y se enfoca más en generar código que funcione de verdad que en la teoría compleja
- En esa época
lex y yacc se consideraban el estándar, pero la simplicidad y claridad de un parser escrito a mano tuvo un gran impacto
- Después de eso, durante 20 años, el autor pasó a preferir los parsers descendentes recursivos escritos manualmente
- Además, en una época en la que la mayoría de los cursos se concentraban en el análisis sintáctico, Crenshaw abordaba la generación de código ensamblador desde etapas tempranas
Limitaciones y posibilidades de mejora
- El tutorial usa un enfoque de traducción dirigida por la sintaxis (syntax-directed translation) para generar código directamente durante el parsing
- Este enfoque es útil para aprender, pero tiene la limitación de dificultar la optimización porque la información de tipos no se conoce de antemano
- En particular, a partir de la parte 14, donde se introducen los tipos, se hace evidente la necesidad de un enfoque más estructurado usando una representación intermedia (IR) o un AST
- No se explica de forma explícita por qué Crenshaw interrumpió el tutorial después de la parte 14, pero se menciona que podría estar relacionado con esta limitación
- El autor considera que tomar la parte 14 como punto de inflexión para generar un AST y luego realizar verificación de tipos y generación de código sería un enfoque más adecuado
Conclusión
- El tutorial original sigue conservando una legibilidad y utilidad sobresalientes como introducción a la construcción de compiladores
- Esta versión en Python y WASM lo adapta para que los desarrolladores actuales puedan aprender sin depender de tecnologías obsoletas
- Cualquiera puede experimentar con él a través del repositorio de GitHub, y se presenta como una reinterpretación moderna que permite experimentar directamente la estructura de un compilador
1 comentarios
Comentarios en Hacker News
Me gusta que sea un tutorial que genera código ensamblador funcional desde el principio, sin quedarse atrapado en los detalles del frontend.
Al estilo del enfoque de Ghuloum, resulta especialmente atractiva la manera de construir todo el lenguaje a partir de una porción muy pequeña e ir ampliándolo gradualmente.
Un buen libro que descubrí hace poco es este enlace; aunque C y x86 no sean el mejor objetivo para principiantes, fue un recurso excelente para hacer un primer compilador.
Como referencia, el paper de Ghuloum está aquí
Antes el parsing era la parte más importante, así que el currículo se organizaba de esa manera, pero hoy importa más cómo implementar las características del lenguaje.
Incluso si haces un compilador por tu cuenta, ya vivimos en una época en la que puedes decir “Claude, hazme un parser descendente recursivo con esta gramática” y casi siempre obtienes algo útil al primer intento.
La academia tiende a abordar esto con base en capas (layers), mientras que en la práctica se tiende a pensar en tuberías (pipes).
El enfoque por capas te da código que compila, pero es difícil de ejecutar; el enfoque de tubería facilita ejecutarlo, pero tiene menos estructura.
Al final, la clave es dividir el desarrollo en unidades mínimas ejecutables.
Creo que este texto realmente dio en el clavo.
Me interesaban los compiladores desde antes de entrar a la universidad, y este material fue la introducción más accesible que encontré.
Cuando tenía 17 años, al hacer yo mismo un parser descendente recursivo, entendí que incluso un problema que parece complejo se vuelve simple si lo divides en las primitivas correctas.
Hay mucho material sobre algoritmos, pero poco sobre cómo abordar un problema desde sus primitivas.
Antes predominaban los parsers basados en tablas como yacc, pero hoy el descenso recursivo es más práctico y flexible.
Gracias a la generación de código con LLM, este enfoque se volvió todavía más fácil.
En compiladores de juguete para aprender, incluso si omites AST, IR y optimizaciones y vas directo a generar código, sigue siendo muy provechoso.
Hace tiempo, cuando en una empresa hice una herramienta de pruebas que soportaba un subconjunto de C, hice que el parser devolviera estructuras de datos ejecutables en vez de generar código.
Por ejemplo, una clase ForLoopStatement contenía testExpression y su método Execute() lo invocaba directamente.
También es un paper importante mencionado en The Mythical Man-Month de Fred Brooks.
Conceptualmente se sienten demasiado naturales y limpios.
Me pareció interesante la explicación de que el tutorial de Jack Crenshaw usa un enfoque de syntax-directed translation.
Me pregunto si eso simplemente significa compilador de una sola pasada, o si se trata de un concepto más específico.
Entiendo que en lenguajes de tipado estático hay dificultades para las optimizaciones basadas en tipos, pero me gustaría saber si también hay limitaciones a nivel de verificación de tipos.
Pero sin IR no se pueden hacer optimizaciones avanzadas como LICM, CSE o GVN.
En lo personal prefiero una compilación de 2 pasadas por función: en la primera pasada se construye el IR y luego se genera código de inmediato y se descarta el estado, lo que da resultados rápidos y eficientes.
Incluso un compilador de una sola pasada puede separar sus etapas y dejar la generación de código hasta el final.
El tutorial de Crenshaw no llega a un nivel práctico, pero si se extiende con la técnica de precedence climbing se vuelve mucho más útil.
Este artículo lo explica muy bien.
Volví a revisar los hilos relacionados con “Let’s Build a Compiler”.
Hay un registro de publicaciones en HN desde 2007 hasta 2023.
En particular, la entrevista a Jack Crenshaw no tiene comentarios, pero fue una lectura muy interesante.
Presentación y un poco de autopromoción de mi proyecto personal: cwerg.org
Usa Python y parsing descendente recursivo, y separa frontend y backend mediante IR.
Genera binarios ELF para x86 y ARM, y está pensado para uso real.
Eso sí, es complejo y no tiene estilo de tutorial.
Me da nostalgia recordar que aprendí el tutorial de Jack Crenshaw en el grupo de noticias comp.compilers de USENET.
Por cierto, ese grupo sigue activo.
Como enfoque moderno para compiladores, recomiendo esta entrada del blog de Cornell.
Cada vez que lo uso se siente como poner unas cuantas piedras encima de una pirámide.
Los lenguajes que usan LLVM como backend tienen en común compilaciones lentas.
Zig reconoce esto y está desarrollando su propio backend, mientras que Rust sigue atado a compilaciones lentas.
Creo que LLVM da la ilusión de que la complejidad garantiza calidad y es una tecnología que debilita la autonomía del desarrollador.
También se parece a otra tecnología popular con iniciales parecidas.
Por razones parecidas, yo también escribí un tutorial llamado tiny-optimizing-compiler.
Es un ejemplo conciso que solo trata las etapas intermedias y el backend del compilador.
Enlace a GitHub
Cuando era más joven, imprimí este tutorial y lo llevaba conmigo para leerlo.
Era realmente genial ver cómo se completaba un compilador en tan poco tiempo.
Materiales como este, o como Beej’s Guide, son de los documentos más valiosos en la educación en CS, pero no reciben el reconocimiento que merecen.