14. Geração de código alvo (consolidação teórica)¶
Antes de construir o Assembler (capítulo 15), vale um momento de retrospectiva: o que exatamente aconteceu nos capítulos 11 a 13, e por que essa arquitetura em particular?
O que já construímos¶
Percorremos, sem perceber explicitamente, uma tradução em duas etapas dentro do que costuma ser chamado, em conjunto, de "geração de código alvo":
Código VM (IR, capítulo 7)
↓ VM Translator (capítulos 12-13)
Assembly Hack (simbólico, capítulo 11)
↓ Assembler (capítulo 15)
Código de máquina Hack (binário)
O VM Translator não é, estritamente, "geração de código alvo" no sentido mais estrito do termo (ele traduz entre duas representações textuais, ambas ainda "legíveis" por um humano treinado) — mas cumpre exatamente o papel que a geração de código alvo cumpre em um compilador tradicional: transformar uma representação independente de máquina em uma representação específica de uma arquitetura real. A diferença sutil é que aqui esse trabalho foi dividido em duas sub-etapas (VM → Assembly, Assembly → binário) em vez de uma só (IR → binário direto) — uma divisão que, como vimos no capítulo 6, tem tudo a ver com simplicidade de implementação.
Por que uma etapa intermediária extra¶
Um compilador "clássico" de livro-texto costuma ir direto de uma IR para código de máquina binário. O Nand2Tetris insere Assembly Hack como uma parada intermediária legível por humanos entre a VM e o binário, por uma razão pedagógica e prática ao mesmo tempo: cada etapa da tradução — VM → Assembly (capítulos 12-13) e Assembly → binário (capítulo 15) — se torna pequena e independentemente verificável. Você pode ler o .asm gerado pelo seu VM Translator e confirmar, linha a linha, que ele faz sentido, antes de sequer pensar no Assembler. Compiladores de produção fazem escolhas semelhantes por razões de engenharia: o LLVM, por exemplo, tem uma IR textual (a "LLVM IR") que pode ser inspecionada exatamente por esse motivo — depurar em texto é mais fácil que depurar em binário.
O que cada etapa resolve¶
Cada tradutor da cadeia elimina uma abstração específica — e só ela. Ver a cadeia desse jeito ajuda a localizar um bug: se o problema envolve nomes de variáveis, ele está no compilador; se envolve endereços de RAM, no VM Translator; se envolve bits, no Assembler.
| Etapa | Abstração eliminada | Como | Capítulos |
|---|---|---|---|
| Jack → VM | Nomes de variáveis, classes, objetos e estruturas de controle | Tabela de símbolos (nome → segmento + índice), pointer/this/that para objetos e arrays, rótulos para if/while |
7–10 |
| VM → Assembly | Pilha e segmentos virtuais | Registradores SP, LCL, ARG, THIS, THAT em RAM[0..4]; cada comando VM vira uma sequência fixa de instruções; call/return montam e desmontam o frame |
11–13 |
| Assembly → binário | Símbolos e mnemônicos | Duas passagens: rótulos → endereços de ROM, variáveis → endereços de RAM a partir de 16; mnemônicos → bits pelas tabelas do capítulo 11 | 11, 15 |
Repare que nenhuma etapa "olha para trás": o VM Translator não sabe que o código veio de Jack, e o Assembler não sabe que o Assembly veio de uma VM. É essa independência que permite testar cada tradutor sozinho, com os arquivos de teste de cada projeto do Nand2Tetris.
O que vem a seguir¶
Falta o último elo: o Assembler. O capítulo 15 mostra como converter o Assembly Hack especificado no capítulo 11 em código binário, resolvendo os símbolos em duas passagens.