- Projeto que reimplementa o tutorial ‘Let’s Build a Compiler’, de Jack Crenshaw, publicado entre 1988 e 1995, usando Python e WebAssembly
- O original usava Pascal e assembly Motorola 68000, mas nesta releitura foi convertido para código executável em um ambiente moderno
- O ponto central do tutorial é implementar diretamente um parser recursivo descendente (recursive-descent parser) e adotar uma abordagem que gera código assembly real desde as etapas iniciais
- A abordagem de Crenshaw usa tradução dirigida pela sintaxe (syntax-directed translation), mas revela limitações na etapa de tratamento de tipos
- Esta reimplementação se destaca por restaurar o tutorial clássico em uma forma que desenvolvedores modernos podem executar e experimentar diretamente
Contexto do tutorial clássico
- ‘Let’s Build a Compiler’, de Jack Crenshaw, é um guia introdutório sobre construção de compiladores publicado entre 1988 e 1995, ainda frequentemente citado hoje
- O texto original foi escrito em Pascal e emitia código assembly Motorola 68000
- Mesmo 35 anos depois, em 2025, ele continua sendo lembrado com frequência em lugares como o Hacker News
- O autor do texto conheceu esse tutorial pela primeira vez em 2003 e ficou profundamente impressionado; em 2025, resolveu adaptá-lo novamente para Python e WebAssembly
Reimplementação em Python e WebAssembly
- Em vez de apenas reler o material, ele traduziu o compilador do tutorial para Python e o implementou para gerar WebAssembly (WASM)
- O resultado foi publicado no repositório GitHub (eliben/letsbuildacompiler)
- O arquivo
TUTORIAL.md explica como cada parte do original foi mapeada para código Python
- Os leitores podem acompanhar o tutorial original e experimentar código realmente executável por conta própria
Exemplo da linguagem KISS e saída em WASM
- É apresentado o resultado de compilar, com a versão em Python do compilador, um programa de exemplo da linguagem KISS projetada por Crenshaw
- O exemplo inclui
procedure, laço while, passagem por valor e por referência
- O texto WASM gerado inclui a lógica de tratamento de parâmetros por referência (by-reference parameter), com quase nenhuma otimização aplicada
- A parte em que a variável global
X é retornada implicitamente pela função main é um recurso para facilitar os testes
- O código WASM gerado é usado em testes que verificam os resultados esperados por meio de execução real
Pontos fortes do tutorial
- O tutorial de Crenshaw constrói passo a passo um parser recursivo descendente e prioriza geração de código funcional na prática em vez de teoria complexa
- Na época,
lex e yacc eram vistos como padrão, mas a simplicidade e clareza de um parser escrito manualmente causaram grande impacto
- Depois disso, ao longo de 20 anos, o autor passou a preferir parsers recursivos descendentes manuais
- Além disso, numa época em que a maioria dos cursos se concentrava em análise sintática, Crenshaw já tratava de geração de código assembly desde as fases iniciais
Limitações e possibilidades de melhoria
- O tutorial usa a abordagem de tradução dirigida pela sintaxe (syntax-directed translation), gerando código diretamente durante o parsing
- Essa abordagem é útil para aprendizado, mas tem a limitação de dificultar otimizações, já que as informações de tipo não são conhecidas antecipadamente
- Em especial, a partir da parte 14, quando tipos são introduzidos, fica claro que é necessário um método mais estruturado com representação intermediária (IR) ou AST
- Crenshaw não explicou explicitamente por que interrompeu o tutorial após a parte 14, mas é mencionado que isso pode estar relacionado a essa limitação
- O autor avalia que usar a parte 14 como ponto de virada para gerar uma AST e depois fazer verificação de tipos e geração de código seria uma abordagem mais adequada
Conclusão
- O tutorial original continua oferecendo excelente legibilidade e utilidade prática como introdução à construção de compiladores
- Esta versão em Python e WASM complementa o material para que desenvolvedores modernos possam aprender sem depender de tecnologias antiquadas
- Com o repositório no GitHub, qualquer pessoa pode experimentar, e o projeto é visto como uma releitura moderna que permite vivenciar diretamente a estrutura de um compilador
1 comentários
Comentários do Hacker News
Gosto do fato de o tutorial não se perder nos detalhes do frontend e já começar gerando código assembly funcional desde o início
A abordagem de Ghuloum, de construir tudo a partir de uma parte bem pequena da linguagem e ir expandindo gradualmente, é especialmente atraente
Um bom livro que descobri recentemente é este aqui; embora C e x86 não sejam os melhores alvos para iniciantes, foi um ótimo material para fazer um primeiro compilador
Para referência, o artigo de Ghuloum está aqui
No passado, parsing era a parte mais importante, então o currículo foi estruturado assim, mas hoje em dia o mais importante é como implementar os recursos da linguagem
Mesmo que você vá criar um compilador por conta própria, agora vivemos numa época em que basta dizer “Claude, crie um parser recursivo descendente com esta gramática” e quase sempre sai algo utilizável de primeira
A academia tende a pensar em termos de camadas (layers), enquanto quem trabalha na prática tende a pensar em pipes
A abordagem em camadas entrega código compilável, mas difícil de executar; a abordagem em pipe facilita executar, mas tem menos estrutura
No fim, o essencial é desenvolver dividindo o problema em unidades mínimas executáveis
Acho que esse texto acertou em cheio no ponto central
Eu me interessava por compiladores desde antes de entrar na faculdade, e este foi o material introdutório mais acessível que encontrei
Aos 17 anos, ao implementar eu mesmo um parser recursivo descendente, percebi que até problemas que pareciam complexos ficam simples quando você os divide nas primitivas corretas
Há muito material sobre algoritmos, mas pouco sobre como abordar problemas a partir de primitivas
Antigamente, parsers baseados em tabelas como yacc eram o padrão, mas hoje recursive descent é mais prático e flexível
Com geração de código por LLM, essa abordagem ficou ainda mais fácil
Em compiladores didáticos, dá para pular AST, IR e otimizações e ir direto para geração de código sem perder valor educacional
No passado, ao criar na empresa uma ferramenta de testes que suportava um subconjunto de C, fizemos o parser retornar uma estrutura de dados executável em vez de gerar código
Por exemplo, uma classe ForLoopStatement continha testExpression e o método Execute() a chamava diretamente
Também é um artigo importante mencionado em The Mythical Man-Month, de Fred Brooks
Conceitualmente, é algo muito natural e elegante
Achei interessante a explicação de que o tutorial do Jack Crenshaw usa a abordagem de syntax-directed translation
Fiquei curioso se isso significa apenas um compilador single-pass ou se é um conceito mais específico
Entendo que, em linguagens estaticamente tipadas, otimizações baseadas em tipos sejam difíceis, mas queria saber se isso também impõe limitações ao nível da verificação de tipos
Mas sem IR, otimizações avançadas como LICM, CSE e GVN são impossíveis
Pessoalmente, prefiro uma compilação em 2 passes por função — no primeiro passo, gero o IR; depois gero o código imediatamente e descarto o estado, o que dá resultados rápidos e eficientes
Mesmo um compilador single-pass pode separar as etapas e só fazer a geração de código no final
O tutorial do Crenshaw não chega a um nível prático, mas fica muito mais útil se for estendido com a técnica de precedence climbing
Um bom texto sobre isso está neste link
Dei mais uma olhada em threads sobre “Let’s Build a Compiler”
Há vários registros de postagens no HN de 2007 até 2023 neste link
Em especial, a entrevista com Jack Crenshaw não tem comentários, mas foi uma leitura muito interessante
Divulgação e apresentação de um projeto pessoal: cwerg.org
Usa Python e parsing recursivo descendente, e separa frontend e backend com IR
Gera binários ELF para x86 e ARM e tem como objetivo uso real
Só que é complexo e não tem estilo de tutorial
Tenho lembranças de ter aprendido com o tutorial do Jack Crenshaw no grupo da USENET comp.compilers
Para referência, o grupo ainda está ativo
Como abordagem moderna para compiladores, recomendo este post do blog de Cornell
Toda vez que uso, parece que estou só empilhando algumas pedras em cima de uma pirâmide
Linguagens que usam LLVM como backend têm em comum compilação lenta
O Zig reconheceu isso e está desenvolvendo seu próprio backend, enquanto Rust continua preso a compilações lentas
O LLVM passa a impressão de que complexidade garante qualidade e, na minha opinião, é uma tecnologia que reduz a autonomia do desenvolvedor
Isso se parece com outras tecnologias populares cujas iniciais são parecidas
Eu também escrevi um tutorial chamado tiny-optimizing-compiler por motivos semelhantes
É um exemplo conciso que trata apenas das etapas intermediárias e do backend de um compilador
Link do GitHub
Quando era mais novo, eu imprimia esse tutorial e andava com ele para ler
Ver um compilador ficar pronto em tão pouco tempo era realmente incrível
Materiais desse tipo, como o Beej’s Guide, estão entre os documentos mais valiosos para o ensino de ciência da computação, mas não recebem o reconhecimento merecido