O que você vai aprender
Justificar o uso de uma representação intermediária no compilador.
Ler, escrever e gerar código de três endereços (TAC).
Conhecer formas de implementar a RI (quádruplas, triplas) e tipos de RI.
Conectar a geração de RI à árvore sintática via SDT.
Por que não ir direto ao assembly?
Seria possível traduzir a árvore sintática diretamente para o assembly de cada máquina. Mas com m linguagens e n máquinas isso exigiria m×n tradutores. A representação intermediária (RI) quebra esse produto em uma soma.
Ao gerar primeiro uma forma neutra, independente de máquina, reaproveitamos otimizações e back-ends. É exatamente a estratégia do LLVM e a razão de a RI ser a peça central do compilador.
O meio do caminho
A RI fica entre a análise (front-end) e a síntese (back-end):
3 endereços→Otimização→Código-alvo
Tudo o que vem antes depende da linguagem; tudo o que vem depois depende da máquina. A RI é o ponto de encontro neutro.
O que é o código de três endereços
x = y op z, com no máximo um operador por instrução e até três operandos (dois de origem, um de destino). Resultados parciais ficam em variáveis temporárias (t1, t2, …).O nome vem dos três "endereços" envolvidos: dois operandos e um destino. É uma linguagem-assembly idealizada, sem as restrições de uma máquina real.
Por que decompor em passos mínimos
Cada instrução TAC faz uma operação só. Uma expressão complexa vira uma sequência linear de operações simples, com temporários ligando uma à outra.
- Facilita a otimização: operações isoladas são fáceis de reordenar, eliminar ou combinar.
- Facilita a geração de código: cada instrução TAC mapeia para poucas instruções de máquina.
- É independente de máquina: não assume número de registradores nem conjunto de instruções.
Traduzindo uma expressão
A expressão a + b * c respeita a precedência: multiplica antes de somar.
E (a + b) * c, com parênteses, inverte a ordem:
Gere TAC passo a passo
A RI como atributo na SDT
Você já viu isso na aula 8, com a mesma gramática de expressões: lá, a regra de E → E1 + T era E.val = E1.val + T.val — o compilador fazia a conta. Só que, para gerar código, a conta não pode ser feita agora: a só tem valor quando o programa rodar. Então trocamos o atributo. Em vez do valor, cada nó calcula lugar (onde o valor vai estar) e cod (quais instruções o deixam lá). O esquema completo para expressões fica assim:
||é concatenação: junta duas listas de instruções, uma depois da outra. Não é o "ou" lógico.gerar(…)cria uma instrução de três endereços com as peças entre parênteses; o que está entre aspas entra como está.novoTemp()devolve um nome que ainda não foi usado: t1, t2, t3…E1eT1só distinguem o filho da esquerda do pai, que tem o mesmo símbolo.
Na nomenclatura da aula 8: como todas as regras ficam no fim da produção e só usam o que vem dos filhos, isto é ao mesmo tempo uma definição dirigida pela sintaxe e um esquema de tradução pós-fixo — cada regra roda logo depois de o nó inteiro ser reconhecido.
Os atributos lugar e cod
Cada nó responde a duas perguntas:
lugar— onde o valor deste nó vai estar quando o programa rodar. Numa folha, é o próprio nome (a) ou a constante (4); num nó com operador, é um temporário novo.cod— quais instruções deixam esse valor pronto: o código dos filhos, na ordem, seguido da instrução do próprio nó.
Os dois são sintetizados (aula 8): cada nó só usa o que os filhos entregam. A informação sobe das folhas para a raiz, e um percurso pós-ordem — filhos antes do pai — calcula tudo numa passada só. Uma gramática assim, só com atributos sintetizados, é S-atribuída.
Por que x = a + b * c tem 9 nós com regra, mas só 2 temporários? Porque só as produções com operador chamam novoTemp() e gerar(). As folhas (F → id) não geram código, e as regras de cópia — E → T, T → F, F → ( E ) — só repassam lugar e cod para cima.
novoTemp() precisa de um nome que ainda não foi usado. A bancada abaixo mostra a tradução passo a passo — e o que dá errado quando uma dessas regras é desobedecida.A receita decomposta
Quádruplas × triplas
O TAC pode ser representado internamente de formas diferentes:
| Quádrupla | Tripla | |
|---|---|---|
| Campos | op, arg1, arg2, resultado | op, arg1, arg2 (sem resultado nomeado) |
| Temporários | Nomeados explicitamente (t1, t2) | Referência ao número da tripla |
| Reordenar código | Fácil | Difícil (referências por posição) |
| Memória | Maior | Menor |
Da árvore à sequência linear
pós-ordem→Emite TAC
por nó→Sequência
linear
A árvore (hierárquica) vira uma lista (sequencial) de instruções. O percurso pós-ordem garante que os operandos de cada instrução já foram calculados quando ela é emitida.
Outros estilos de RI
O TAC linear não é a única RI possível. As principais famílias:
- RI gráfica: a própria árvore sintática abstrata (AST) ou o DAG (que compartilha subexpressões repetidas).
- RI linear: código de três endereços, bytecode de pilha (como o da JVM).
- SSA (Static Single Assignment): cada variável recebe valor uma única vez; facilita muitas otimizações modernas.
Verifique seu entendimento
Quantos operadores no máximo aparecem em uma instrução de três endereços?
Uma atribuição completa
Veja como x = a + b * 2 vira TAC, incluindo a atribuição final:
A última instrução copia o resultado para a variável de destino. Em uma versão otimizada, t2 poderia ser eliminado e escreveríamos x = a + t1 diretamente.
Tropeços ao gerar TAC
Reusar temporários cedo demais. Sobrescrever um temporário ainda necessário corrompe o cálculo. Use novoTemp() para nomes frescos.
Emitir a instrução do pai antes da dos filhos. O operando precisa existir antes de ser usado.
Achar que t1, t2 são registradores. São temporários lógicos; a alocação real vem só na geração de código.
Gerando TAC com segurança
Use um contador global para novoTemp(): nomes sempre frescos, sem colisão.
Concatene o cod dos filhos antes da instrução do pai.
Mantenha o TAC independente de máquina: nada de supor número de registradores nesta fase.
Pense antes de revelar
Por que a multiplicação aparece antes da soma no TAC de "a + b * c", se a soma está mais à esquerda no texto?
Revise os termos
Como esta aula se liga ao curso
- Usa a aula 5 (GLC): a precedência codificada na gramática determina a ordem das instruções geradas.
- Usa a aula 8 (atributos): gerar TAC é uma SDT com atributos lugar e cod.
- Usa a aula 2 (front/back-end): a RI é justamente o que desacopla as duas pontas.
- Prepara a aula 12 (código): o TAC é a entrada da geração de código de máquina.
O essencial da aula
lugar e cod percorrendo a árvore. A precedência da gramática define a ordem das instruções.Atividade em grupo · Tradutores humanos
Em trios, gerem TAC à mão e comparem com o simulador.
Roteiro
- Escolham 3 expressões de complexidade crescente (ex.: a*b+c, (a+b)*(c-d), a+b*c-d/e).
- Gerem o código de três endereços à mão, respeitando precedência.
- Confiram no simulador da aula.
- Discutam onde surgiram os temporários e por quê.
Mini-quiz · Aula 10
20 questões sobre esta aula. Escolha e veja a explicação na hora.
📌 Resumo — leve isto para a prova
- A RI desacopla front-end e back-end (m+n em vez de m×n) e habilita otimização reaproveitável.
- TAC: instruções x = y op z, um operador cada, com temporários.
- A geração de RI é uma SDT que sintetiza os atributos lugar e cod.
- Quádruplas, triplas, DAG e SSA são formas distintas de representar a RI.
- A precedência da gramática (via árvore) determina a ordem das instruções geradas.