Aprenda a Matéria

Compiladores · Prova 2 · Tradução, Análise Semântica, Tipagem, Runtime e Geração de Código · leitura de ~60 minutos

Como usar esta página. Ela é organizada por tipo de questão. Cada parte traz o que costuma cair (com enunciados reais das listas e provas), o mínimo de conceito, um método numerado, um exemplo resolvido e as armadilhas. No fim de cada parte há um Teste-se: responda antes de abrir. Se a base estiver fraca (fases, sintaxe × semântica, registradores), leia antes o resumo 00 - Base e Introdução.

1. O mapa da prova 3 min

A Prova 1 terminou na árvore sintática: o programa está bem escrito. A Prova 2 responde ao que vem depois: o programa faz sentido? E como ele vira instruções de máquina que rodam de verdade?

Léxicacaracteres → tokensSintáticatokens → ASTSemânticaAST → AST com tiposCód. intermediárioAST → IROtimizaçãoIR → IR melhorGeração de códigoIR → MIPSfront-endback-endTabela desímbolosTabela demétodos■ P1■ P2 (esta prova)■ P3
As fases. A P2 cobre a TDS (slide 05, o mecanismo que liga a árvore a tudo isso), a semântica e a tipagem (slides 06 e 07), a organização do runtime (slide 08) e a geração de MIPS (slide 09). Na disciplina, o código intermediário fica no back-end.

São duas metades:

  1. Entender o programa (front-end, fase semântica): atributos na árvore (TDS), escopo e tabela de símbolos, regras de tipo. Pergunta típica: "compila? por quê?", "escreva a regra", "complete a tabela".
  2. Produzir o código (back-end): como a memória fica organizada em execução (pilha, heap, registros de ativação, objetos) e como cada construção vira MIPS. Pergunta típica: "desenhe a pilha", "gere o MIPS do caller e do callee", "qual o layout do objeto".
O que as provas antigas tiveram. As três (2026/1, 2025/2, 2025/1) tiveram MIPS de função completa com caller e callee (ternário, laço com return, global com chamada aninhada). O resto variou: 2026/1 trouxe regras semânticas (binário → decimal), TDS de int[2][3] e despacho em C++; 2025/2, gerador para pares, pilha com links e tradução de literais; 2025/1, ladrilhamento, compilado × interpretado, estática × dinâmica e 11 itens de Certo/Incorreto. As listas 3 e 4 cobrem o mesmo terreno, com mais erros, tabela de símbolos e layout de objetos.
Duas ideias que atravessam tudo. (1) Estático × dinâmico: o compilador decide em compilação o que dá (tipos, offsets, tamanho do registro de ativação); o resto só se sabe em execução (qual método roda, valores). (2) Contrato: cada molde de código deixa o resultado em $a0 e devolve a pilha como encontrou. Quase todo erro de MIPS é quebra desse contrato.

2. Atributos e TDS 8 min

O que cai. "B → B0 | B1 | 1 {B.valorDecimal = 1}. Escreva as regras que calculam o valor decimal (111 → 7). Herdado ou sintetizado?" (2026/1). "Mostre a AST e a sequência de regras para int[2][3]" (2026/1). "Classifique cada regra da tabela" e "escreva a TDS que calcula as exceções possíveis" (Lista 3).

A ideia

A gramática diz só a forma. Para calcular algo enquanto se percorre a árvore (um valor, um tipo, um pedaço de código), cada símbolo ganha atributos e cada produção ganha regras semânticas que os calculam. Isso é a Tradução Dirigida por Sintaxe (TDS).

Regra de bolso para classificar. Olhe o lado esquerdo do "=" na regra. Se o atributo definido é do símbolo antes da seta, é sintetizado. Se é de um símbolo depois da seta, é herdado. Regra que só chama função (addTipo(id, L.her)) não define atributo: diga que ela usa o herdado.

Método: escrever as regras

  1. Descubra o que o atributo representa (valor decimal, tipo, lista de exceções).
  2. Para cada produção, escreva o atributo da cabeça em função do corpo.
  3. Recursão à esquerda que acrescenta um dígito: valor antigo × base + dígito.
  4. Escreva o caso base. Teste com uma string curta, na mão.
Exemplo resolvido (2026/1). Binário para decimal:
B → B1 0   { B.valorDecimal = 2 · B1.valorDecimal }
B → B1 1   { B.valorDecimal = 2 · B1.valorDecimal + 1 }
B → 1      { B.valorDecimal = 1 }
Teste "111": 1 → 2·1+1 = 3 → 2·3+1 = 7. O atributo é sintetizado: o valor da cabeça vem do filho B1 e do dígito.
Bv = 7Bv = 31Bv = 111sobe
Atributo sintetizado em 111: cada B calcula seu valor a partir do filho B1 e do dígito (2·1+1 = 3, depois 2·3+1 = 7). O valor sobe das folhas para a raiz.

Método: mostrar a ordem de avaliação

  1. Desenhe a árvore de derivação (não terminais nos nós internos, tokens nas folhas). Numa questão de TDS é essa a "AST" esperada, porque os atributos ficam nos não terminais.
  2. Percorra em profundidade, da esquerda para a direita.
  3. Ao descer para um filho, calcule os herdados dele.
  4. Ao voltar de todos os filhos, calcule os sintetizados do nó.
  5. Numere as regras na ordem e mostre o valor de cada uma.

Esse percurso vale para gramática L-atribuída (herdados só usam herdados do pai e atributos dos irmãos à esquerda), que é o caso de todos os exemplos da disciplina. No caso geral, avalia-se numa ordenação topológica do grafo de dependências.

Exemplo resolvido (2026/1): int[2][3]. Regras: T → B C {T.tipo = C.tipo; C.basico = B.tipo}; B → int {B.tipo = integer}; C → [num] C1 {C.tipo = array(num.valor, C1.tipo); C1.basico = C.basico}; C → λ {C.tipo = C.basico}.
  1. B.tipo = integer
  2. C.basico = B.tipo = integer
  3. C1.basico = C.basico = integer
  4. C2.basico = C1.basico = integer
  5. C2.tipo = C2.basico = integer
  6. C1.tipo = array(3, integer)
  7. C.tipo = array(2, array(3, integer))
  8. T.tipo = array(2, array(3, integer))
basico desce (herdado) antes; tipo sobe (sintetizado) depois.
TBCC1C2int[2][3]λbasico = integertipo = integerarray(3, integer)array(2, array(3, integer))
int[2][3]: o tipo básico desce como atributo herdado (C.basico, C1.basico, C2.basico); o tipo montado sobe como sintetizado (C2.tipo, C1.tipo, C.tipo, T.tipo).

Outros moldes que já caíram

Armadilhas. Chamar de herdado só porque aparece um não terminal à direita: o que decide é qual atributo está sendo definido. Esquecer o caso base. Calcular o sintetizado do pai antes dos filhos. Em binário, usar ×10.
Teste-se: regras para contar quantos 1 há num binário (B → B1 0 | B1 1 | 1). B → B1 1 {B.n = B1.n + 1}; B → B1 0 {B.n = B1.n}; B → 1 {B.n = 1}. Sintetizado. Teste "101": 1 → 1 → 2.
Teste-se: em L → L1 , id {L1.her = L.her; addTipo(id, L.her)}, o atributo é herdado ou sintetizado? Herdado: a regra define L1.her, atributo de um símbolo do lado direito, a partir do pai. addTipo é efeito colateral e não define atributo.

3. Erros, escopo e tabela de símbolos 7 min

O que cai. "Classifique o erro de cada linha (léxico, sintático, semântico, execução)" (Lista 3, Q1 e Q3). "Complete a tabela de símbolos no ponto da linha 13" (Lista 3, Q5). "O que imprime com escopo dinâmico?" (Bash). "Quais usos de Repeated são tipo, método, variável ou rótulo?" (Lista 3, Q10).

Classificar um erro

  1. Algum caractere ou literal não forma token (@ em C, string sem fechar, 3.4.5)? → léxico.
  2. Os tokens existem, mas a ordem é impossível pela gramática (falta ;, (*f)..h, e.2[h], palavra-chave no lugar de expressão)? → sintático.
  3. A frase está bem formada, mas não faz sentido com as declarações e tipos (tipo incompatível, nome não declarado ou duplicado, &(c.a + 1), campo inexistente, número de argumentos)? → semântico.
  4. Compila, mas falha ao rodar com certos valores (índice fora do vetor, divisão por zero, ponteiro nulo, downcast inválido)? → execução.
Armadilha. e.2 parece erro léxico, mas .2 é uma constante float válida em C: os tokens são e .2 e o erro é de ordem (sintático). Índice fora do vetor em C compila; em execução é comportamento indefinido (pode corromper memória sem avisar).

Escopo

Exemplo (Bash). outer declara local var2="outer_local_var" e chama inner, que imprime $var2. Com escopo dinâmico, inner procura var2 descendo a pilha e acha a de outer: imprime outer_local_var. Com escopo estático, inner não enxerga var2 e imprimiria vazio.

Tabela de símbolos

É uma pilha de escopos: enter_scope() ao abrir bloco, add_symbol(x) na declaração, check_scope(x) para detectar declaração duplicada no mesmo escopo, find_symbol(x) procura do topo para o fundo, exit_scope() descarta o escopo. O ambiente de tipos O das regras de tipo é a formalização dela.

Método: completar a tabela num ponto do programa

  1. Marque a linha pedida.
  2. Liste os escopos abertos naquele ponto, do mais externo ao mais interno.
  3. Em cada escopo aberto, liste o que foi declarado até ali: variáveis, parâmetros e nomes de procedimentos internos.
  4. Escopos já encerrados somem; o nome do procedimento encerrado continua no escopo pai.
  5. Tamanho: tipo básico = 1 palavra (4 B, salvo se o enunciado disser outra coisa); vetor [l..u] = (u − l + 1) × tamanho do elemento; procedimento = sem tamanho.
  6. Resolva os nomes da linha pelo aninhado mais próximo.
Exemplo (Lista 3, Q5, linha 13 b := a + c).
LexemaTipoTam.Escopo
aarray[1..5] of real20S
creal4S
Rprocedure(integer)—S
minteger4R
barray[1..5] of integer20R
Eprocedure()—R
Qfunction(integer): integer—R
ninteger4Q
ainteger4Q
Pprocedure()—Q
breal4P
I e c de E saíram quando E fechou (linha 8). Na linha 13: b → b de P (real); a → a de Q (integer); c → c de S (real); integer + real → real.

Espaços de nomes (Java)

Tipos, métodos, variáveis e rótulos vivem em espaços separados; o contexto sintático decide qual consultar: seguido de ( → método; antes de um identificador declarado ou depois de class → tipo; seguido de : no início do comando, ou depois de break → rótulo; resto → variável. Nome antes de . → variável se existir uma com esse nome; senão tipo (a variável obscurece o tipo).

Teste-se: int x = "abc"; e x = 1 + ; — que erro é cada um? O primeiro é semântico (tipos incompatíveis; a frase é bem formada). O segundo é sintático (falta operando: tokens válidos em ordem impossível).
Teste-se: na tabela de símbolos, por que um procedimento aparece com tamanho "—"? Porque o nome do procedimento não ocupa espaço de dados no registro de ativação de quem o declara; ele só é registrado para checar chamadas.

4. Regras de tipo e checagem 10 min

O que cai. "Escreva a regra de inferência para raise/try" e "implemente a checagem do nullish e1 ?? e2" (Lista 3). "Pessoa* p = new Professor(); p->lancarNota(25); compila? Como corrigir? Como o método certo é achado?" (2026/1). "Mostre um programa que a tipagem dinâmica aceita e a estática rejeita" (2025/1). Certo/Incorreto sobre nominal × estrutural, coerção, LUB, soundness.

Os quatro eixos (independentes)

Armadilha de C/I. Desconfie de "todos", "somente", "garante", "exclusiva", "independentemente". Ex.: "nominal checa em execução e estrutural em compilação" é Incorreto: são eixos independentes.
Exemplo (2025/1): a dinâmica aceita, a estática rejeita.
let v = 42
if (v > 0) { v = "positivo" } else { v = 0 }
print(v + "!")
Em Python ou JS roda e imprime positivo!: cada operação é checada com o valor real. Em Java ou C++, v tem tipo fixo (int) em todo o escopo; atribuir string é erro de compilação, embora nunca haja erro de tipo na execução. Outro: let a = true ? 1 : "um"; print(a + 1) (o ramo string nunca executa, mas o checador estático não sabe).
Certo/Incorreto que já caiu (semântica e tipos).

Ler e escrever uma regra de inferência

O ⊢ e : T lê-se "no ambiente O (que dá o tipo de cada variável), prova-se que e tem tipo T". Hipóteses ficam em cima do traço, a conclusão embaixo. Tipos sobem (dos filhos para o pai) e o ambiente desce. Nas regras abaixo, T1 ≤ T0 lê-se "T1 é subtipo de T0" e lub(T1, T2) é o menor ancestral comum dos dois (detalhes em Subtipos, lub e despacho).

O ⊢ e1 : INTO ⊢ e2 : INTO ⊢ e1 + e2 : INT[Add] O(x) = TO ⊢ x : T[Var] O(x) = T0O ⊢ e : T1T1 ≤ T0O ⊢ x = e : T1[Atrib] O ⊢ e0 : T0O[T/x] ⊢ e1 : T1T0 ≤ TO ⊢ let x:T = e0 in e1 : T1[Let] O ⊢ e0 : BOOLO ⊢ e1 : T1O ⊢ e2 : T2O ⊢ if e0 then e1 else e2 : lub(T1, T2)[If] O ⊢ e1 : INTO ⊢ e2 : INTO ⊢ e1 == e2 : BOOL[Eq]

Método: escrever a regra de uma construção nova

  1. Uma hipótese O ⊢ ei : Ti para cada subexpressão.
  2. Condições sobre os tipos (igualdade, ≤, BOOL na condição).
  3. Se a construção declara variável, o corpo é checado em O[T/x].
  4. Conclusão: a expressão inteira com o tipo resultante (lub se há ramos).
Exemplo (Lista 3): exceções. Um tipo próprio Exn para o valor lançado.
O ⊢ e : ExnO ⊢ raise e : T (qualquer T)[Raise] O ⊢ e : TO ⊢ F : Exn → TO ⊢ try e with F : T[Try]
raise nunca devolve valor, então pode ter qualquer tipo (age como ⊥). O tratador F é uma função aplicada à exceção; seu resultado tem o tipo de e.

Árvore de prova

A prova tem a forma da AST: escolha a regra pelo nó raiz e suba até as folhas ([INT], [Var]).

O[int/x](x) = intO[int/x] ⊢ x : intVAR  O[int/x] ⊢ 3 : intINT O[int/x] ⊢ x + 3 : intADD O ⊢ fun (x:int) → x + 3 : int → intFUN  O ⊢ 5 : intINT O ⊢ (fun (x:int) → x + 3) 5 : intAPP

Ordem de aplicação: APP, FUN, ADD, VAR, INT (3), INT (5). Regras: [Fun] O[T/x] ⊢ e : S ⇒ O ⊢ fun (x:T) → e : T → S; [App] O ⊢ e1 : T → S, O ⊢ e2 : T ⇒ O ⊢ e1 e2 : S.

Subtipos, lub e despacho

ObjectABCDlub(B, C) = Alub(D, C) = Aseta = "é subtipo de" (≤)
lub = menor ancestral comum. D ≤ B ≤ A ≤ Object; lub(B, C) = A, e não Object, porque A é o menor supertipo comum.
Exemplo (2026/1). Pessoa* p = new Professor(); p->seApresentar(); p->lancarNota(25);
  1. Não compila na chamada de lancarNota: o tipo estático de p é Pessoa*, e Pessoa não tem lancarNota. A chamada de seApresentar é válida.
  2. Correção: downcast explícito static_cast<Professor*>(p)->lancarNota(25), ou dynamic_cast testando nullptr; ou declarar virtual lancarNota em Pessoa.
  3. Qual seApresentar roda? O objeto guarda um ponteiro para a tabela de despacho da sua classe dinâmica (Professor). seApresentar tem o mesmo offset (0) nas duas tabelas; em compilação fixa-se o offset, em execução lê-se a tabela do objeto (seção 8).
  4. Em Python o erro só apareceria em execução (aqui o método existe e roda). Isso não é "mais sound": soundness é garantia estática, e Python não a dá.
Casts em Java (Lista 3, Q8 e Q9d). Collection<String> c = lista; · Set<String> s = (Set<String>) c; · Set<Integer> i = (Set<Integer>) s;
  1. Erro de compilação no cast para Set<Integer>: genéricos são invariantes; Set<String> e Set<Integer> não têm relação de subtipo, então o cast é impossível.
  2. O cast (Set<String>) c compila, mas pode falhar em execução: se o objeto for ArrayList ou LinkedList (List, não Set), ClassCastException. Só roda se o tipo dinâmico implementar List e Set. Pela type erasure, só Set é conferido.
  3. Downcast errado em geral: Animal a = new Gato(); Cachorro c = (Cachorro) a; compila; Java lança ClassCastException; em C++, static_cast lê offsets errados (comportamento indefinido) e dynamic_cast devolve nullptr.

Soundness, SELF_TYPE e ANY

Implementar uma função de checagem

ChecaTipo([O,M,C], e1 OP e2) =
  T1 = ChecaTipo([O,M,C], e1)
  T2 = ChecaTipo([O,M,C], e2)
  se condição(T1, T2): retorna tipo_resultado
  senão: erro   (linguagem fraca: tenta coerção antes)

Coerção em C (Lista 3)

Teste-se: "Na incompatibilidade de tipos, o checador visita a AST para achar o LUB, garantindo soundness." C ou I? Incorreto. O LUB serve para juntar ramos (if, ternário); não conserta incompatibilidade (se der Object/ANY, a operação continua inválida) e não garante soundness sozinho.
Teste-se (Lista 3, Q12d): (3) levantar(levantar E) → levantar E; (4) E1 + levantar E2 → levantar E2; (5) tentar (levantar E) com F → F(E); (6) tentar E com (fun X → levantar X) → E. Quais valem? (3) válida. (4) inválida em geral: E1 pode ter efeitos, não terminar ou lançar outra exceção antes. (5) válida só se E não lança outra exceção; se E lança E', o original chama F(E'). (6) válida: capturar e relançar é o mesmo que não capturar.
Teste-se: escreva a regra do ternário e0 ? e1 : e2. O ⊢ e0 : BOOL, O ⊢ e1 : T1, O ⊢ e2 : T2 ⇒ O ⊢ e0 ? e1 : e2 : lub(T1, T2).

5. Memória, registros de ativação e links 7 min

O que cai. "Em que região fica cada variável?" (Lista 4, Q4). "Desenhe a pilha de ativação" (Lista 4, Q1, Q6, Q10a). "O que é a pilha de execução? O que são links de controle e estáticos? Desenhe a pilha de f(17) até h e diga como acessar k e x" (2025/2). Certo/Incorreto sobre registro de ativação, $sp, $fp, alocação dinâmica. (Atenção: o enunciado de 2025/2 diz "links de controle (ou links estáticos)", juntando os dois. Responda distinguindo: quem resolve o acesso a variáveis de funções aninhadas é o link de acesso, ou estático.)
Códigofixo, só leituraDados estáticosglobais e staticHeap ↓new / mallocespaço livrePilha ↑um AR por chamadaendereçosmenoresendereçosmaioresheap cresce para endereços maiorespilha cresce para endereços menores(addiu $sp $sp -4)
Layout no modelo MIPS: pilha e heap crescem um em direção ao outro. Desenho com endereços menores em cima.

Onde cada variável fica

O registro de ativação (AR)

Cada chamada ativa tem um AR na pilha, com tudo o que é preciso para terminar a função e voltar para quem chamou. No modelo MIPS da disciplina, de cima (endereços maiores) para baixo:

  1. $fp antigo (o link de controle) — empilhado pelo caller;
  2. argumentos xn … x1 (ordem reversa, x1 fica mais perto do $fp) — caller;
  3. endereço de retorno ($ra) — callee; $fp aponta aqui;
  4. locais e temporários — callee.

Por isso x1 está em 4($fp), x2 em 8($fp), o $fp antigo em 4(n+1)($fp), e os locais em −4($fp), −8($fp)…

Método: desenhar a pilha

  1. Escreva a sequência de chamadas até o ponto pedido (main → f(3) → f(2)).
  2. Um bloco por ativação, da mais antiga (em cima) para a mais recente (embaixo).
  3. Dentro do bloco: $fp antigo, argumentos com valores, endereço de retorno (diga para qual linha volta), locais (valor ou "?"), temporários salvos.
  4. Marque $fp no endereço de retorno do bloco mais recente e $sp logo abaixo do último item.
  5. Globais ficam fora (área estática). Função inline não cria AR. Objeto local C++ fica inteiro dentro do AR.
Exemplo: f(x) = x == 0 ? g() : f(x−1), chamada f(3), parada em f(2).
BlocoConteúdo (de cima para baixo)
main… locais de main
f(3)$fp antigo = $fp de main · x = 3 · retorno → main, depois de f(3)
f(2)$fp antigo = $fp de f(3) · x = 2 · retorno → dentro de f, depois de f(x−1) ← $fp
$sp logo abaixo

Links de controle e de acesso

f'(17)x = 17g'(18)z = 18h'(36)k = 36controlecontroleacessoacesso
f(x) declara g e h; f chama g(x+1), g chama h(x+z+1). Controle aponta para quem chamou (h → g). Acesso aponta para quem envolve no texto (h → f). Em h, x é lido seguindo 1 link de acesso: h retorna 36 · 17 = 612.

Certo ou Incorreto que já caiu

Teste-se: static int g = rand(); dentro de uma função. Onde fica g? Na área estática: static desacopla a variável das chamadas. Inicializar em execução não muda o lugar.

6. Máquina de pilha e moldes cgen 8 min

O que cai. "Gere o código para break" e "que informação o gerador precisa" (Lista 4, Q2). "Quem empilha o quê: caller ou callee? Por quê?" (Lista 4, Q3). "Descreva o gerador para uma linguagem com pares" (2025/2). Toda questão de MIPS usa estes moldes.

O modelo

Uma máquina de pilha com um registrador acumulador, emulada em MIPS: o resultado de toda expressão fica em $a0; valores intermediários vão para a pilha na memória. A pilha cresce para endereços menores; $sp aponta para a próxima posição livre e o topo está em 4($sp).

push $a0:   sw $a0 0($sp)
            addiu $sp $sp -4
topo → $t1: lw $t1 4($sp)
pop:        addiu $sp $sp 4

C/I que caiu duas vezes (Lista 4 Q5, 2025/1): "a máquina de pilha é preferida por ser mais eficiente / usar menos instruções" → I (é mais simples de gerar, mas usa mais instruções e acessos à memória); "a máquina de registradores tem melhor desempenho" → C.

Contrato de cgen(e). Gera código que calcula e em $a0 e deixa $sp e o conteúdo da pilha exatamente como encontrou. É por isso que os moldes podem ser encaixados uns dentro dos outros.

Os moldes

ConstruçãoCódigo
constante 7li $a0 7
argumento xilw $a0 4i($fp)
local jlw $a0 −4j($fp); atribuição: cgen(e) / sw $a0 −4j($fp)
e1 + e2cgen(e1) / push / cgen(e2) / lw $t1 4($sp) / add $a0 $t1 $a0 / pop
e1 − e2igual, com sub $a0 $t1 $a0 (nessa ordem: e1 − e2)
e1 * e2igual, com mult $t1 $a0 / mflo $a0
Por que empilhar e1 em vez de guardar em $t1? Porque cgen(e2) pode usar $t1 (se e2 tiver outra soma dentro) e destruir e1. A pilha é o único lugar seguro.

Comparações e desvios

Depois do pop: $t1 = e1, $a0 = e2. O desvio vai para o senão (ou para o fim do laço) quando a condição é falsa:

CondiçãoCódigo
e1 < e2slt $t1 $t1 $a0 / beq $t1 $zero SENAO_k
e1 > e2slt $t1 $a0 $t1 / beq $t1 $zero SENAO_k
e1 >= e2slt $t1 $t1 $a0 / bne $t1 $zero SENAO_k
e1 <= e2slt $t1 $a0 $t1 / bne $t1 $zero SENAO_k
e1 == e2bne $a0 $t1 SENAO_k
e1 != e2beq $a0 $t1 SENAO_k

if, while, for, break

# if (cond) e3 else e4          # while (cond) corpo
  código da cond → desvia SENAO_k    INICIO_k:
  cgen(e3)                             código da cond → desvia FIM_k
  b FIM_k                              cgen(corpo)
SENAO_k:                               b INICIO_k
  cgen(e4)                           FIM_k:
FIM_k:

# for (init; cond; passo) corpo
  cgen(init)
TESTE_k:  código da cond → desvia FIM_k
  cgen(corpo)
PASSO_k:  cgen(passo)      # continue salta para cá
  b TESTE_k
FIM_k:                     # break salta para cá

Chamada e definição de função

# caller: cgen(f(e1, …, en))     # callee: f(x1, …, xn) com k locais
sw $fp 0($sp)                   F_ENTRADA:
addiu $sp $sp -4                  move $fp $sp
cgen(en) / push                   sw $ra 0($sp)
…                                 addiu $sp $sp -4
cgen(e1) / push                   addiu $sp $sp -4k     # locais
jal F_ENTRADA                     cgen(corpo)           # → $a0
# resultado em $a0             F_SAIDA:
                                  addiu $sp $sp 4k      # libera locais
                                  lw $ra 4($sp)
                                  addiu $sp $sp z       # z = 4n + 8
                                  lw $fp 0($sp)
                                  jr $ra

Temporários com posição fixa (NT)

Em vez de push/pop, os valores intermediários podem ficar em posições fixas do AR, reservadas na entrada. NT(e1 + e2) = max(NT(e1), 1 + NT(e2)); NT(int) = NT(id) = 0; NT(f(e1..en)) = max(NT(ei)). Com nt começando em 4: cgen(e1 + e2, nt) = cgen(e1, nt) / sw $a0 −nt($fp) / cgen(e2, nt+4) / lw $t1 −nt($fp) / add $a0 $t1 $a0. Isso vale sem locais; com k locais, nt começa em 4k + 4. Na entrada, reserve addiu $sp $sp −4·NT(e) logo depois de salvar o $ra; na saída, libere 4·NT(e) antes do lw $ra 4($sp) (z continua 4n + 8). Ex.: fib(x−1) + fib(x−2) → NT = 2.

Exemplo (2025/2): gerador para uma linguagem com pares. Tipos int e t1 * t2; size(int) = 1 palavra; size(t1 * t2) = size(t1) + size(t2). Um par é a concatenação: reservar addiu $sp $sp −4·size, avaliar e1 e gravar no começo, avaliar e2 e gravar logo depois. fst e fica com as size(t1) primeiras palavras; snd e com as palavras a partir de 4·size(t1). Facilita: funções não aninhadas (sem links de acesso) e tamanho de todo valor conhecido em compilação pelo tipo. Ordem de avaliação: com ? (leitura), (?, ?) tem de ler a 1ª entrada para fst; avaliar da esquerda para a direita mantém isso, e da direita para a esquerda inverte as leituras. Retorno: valor de 1 palavra volta em $a0; um par, o caller reserva espaço no próprio AR e o callee copia para lá (ou devolve em $a0 o endereço da cópia). z = tamanho do argumento + 8.
Teste-se: gere o código de 3 + (7 + 5) e diga o valor de $a0 e de $sp no fim.
li $a0 3
sw $a0 0($sp)
addiu $sp $sp -4
li $a0 7
sw $a0 0($sp)
addiu $sp $sp -4
li $a0 5
lw $t1 4($sp)
add $a0 $t1 $a0      # 12
addiu $sp $sp 4
lw $t1 4($sp)
add $a0 $t1 $a0      # 15
addiu $sp $sp 4
$a0 = 15 e $sp volta ao valor inicial (dois push, dois pop).

7. MIPS de função, passo a passo 10 min

O que cai, em toda prova. "function f(a, b) { i := a == b ? 5 : 0; return i + 1 }; f(10, 15). Escreva o MIPS do caller e do callee" (2026/1). Variações: laço while/for com return no meio (2025/2), variável global num endereço fixo e chamada aninhada (2025/1), trechos soltos (x = b * c, y = *q, a[i]) (Lista 4, Q11).

Receita

  1. Convenções primeiro, por escrito: parâmetros em 4($fp), 8($fp)…; locais em −4($fp), −8($fp)…; n = número de parâmetros; z = 4n + 8; k = número de locais.
  2. Caller: push $fp, push dos argumentos do último para o primeiro, jal.
  3. Entrada do callee: move $fp $sp, push $ra, addiu $sp $sp −4k.
  4. Corpo: traduza comando a comando com os moldes da seção 6, criando rótulos únicos. Todo return deixa o valor em $a0 e vai para a saída.
  5. Saída: addiu $sp $sp 4k, lw $ra 4($sp), addiu $sp $sp z, lw $fp 0($sp), jr $ra.
  6. Confira o saldo: chame de S o $sp antes da chamada (no caller). Na entrada do callee, $sp = S − 4(n + 1). Antes do lw $ra 4($sp), $sp tem de ser S − 4(n + 2) (só o $ra abaixo dos argumentos). Depois do addiu z, $sp = S, apontando para o $fp antigo. Ao voltar, $sp é o mesmo de antes da chamada.
A folha da prova não tem pseudo-instruções. A tabela de 2026/1 lista só instruções reais: sem li, move, b, la, jalr, e sem $fp na lista de registradores. Os moldes valem do mesmo jeito; para ficar só com o que está na folha: li $a0 5 = addiu $a0 $zero 5; move $fp $sp = addu $fp $sp $zero; b L = beq $zero $zero L (ou j L). jalr é instrução real do MIPS (chamada indireta), só não está na folha.

Exemplo completo (2026/1)

Convenções: a em 4($fp), b em 8($fp), i em −4($fp); n = 2, z = 16; k = 1.

# caller: f(10, 15)
sw $fp 0($sp)
addiu $sp $sp -4
li $a0 15             # b (o último argumento vai primeiro)
sw $a0 0($sp)
addiu $sp $sp -4
li $a0 10             # a
sw $a0 0($sp)
addiu $sp $sp -4
jal F_ENTRADA         # resultado em $a0

# callee
F_ENTRADA:
move $fp $sp
sw $ra 0($sp)
addiu $sp $sp -4
addiu $sp $sp -4      # espaço de i
lw $a0 4($fp)         # a
lw $t1 8($fp)         # b
beq $a0 $t1 VERDADEIRO_1
li $a0 0
b FIM_1
VERDADEIRO_1:
li $a0 5
FIM_1:
sw $a0 -4($fp)        # i = …
lw $a0 -4($fp)
addiu $a0 $a0 1       # i + 1
addiu $sp $sp 4       # libera i
lw $ra 4($sp)
addiu $sp $sp 16      # z = 4·2 + 8
lw $fp 0($sp)
jr $ra

Duas simplificações em relação aos moldes da seção 6: como a e b são variáveis, vão direto para $a0 e $t1, sem push/pop; e o desvio vai para o ramo verdadeiro (forma do slide 09), com o falso logo abaixo. A forma da tabela também vale: bne $a0 $t1 SENAO_1 / li $a0 5 / b FIM_1 / SENAO_1: li $a0 0 / FIM_1:.

Como 10 ≠ 15, i = 0 e f devolve 1. A pilha em cada momento (deslize para o lado):

1. Antes da chamadao caller ainda não empilhou nada
S$sp
S−4
S−8
S−12
S−16
S−20
$sp = S; $a0 = 0
2. Caller prontopush $fp, push b (15), push a (10)
S$fp antigo: fp do main
S−4b: 15
S−8a: 10
S−12$sp
S−16
S−20
$sp = S−12; $a0 = 10
3. Entrada do calleemove $fp $sp; push $ra; reserva i
S$fp antigo: fp do main
S−4b: 15
S−8a: 10
S−12$ra: → main$fp
S−16i: ?
S−20$sp
$sp = S−20; $a0 = 10
4. i gravadoa ≠ b → i = 0; sw em −4($fp)
S$fp antigo: fp do main
S−4b: 15
S−8a: 10
S−12$ra: → main$fp
S−16i: 0
S−20$sp
$sp = S−20; $a0 = 0
5. i liberado$a0 = i + 1 = 1; addiu $sp $sp 4
S$fp antigo: fp do main
S−4b: 15
S−8a: 10
S−12$ra: → main$fp
S−16i: 0 (lixo)$sp
S−20
$sp = S−16; $a0 = 1
6. addiu $sp $sp 16z = 4·2 + 8; $sp no $fp antigo
S$fp antigo: fp do main (lixo)$sp
S−4b: 15 (lixo)
S−8a: 10 (lixo)
S−12$ra: → main (lixo)$fp
S−16i: 0 (lixo)
S−20
$sp = S; $a0 = 1
7. De volta ao caller$fp restaurado; $a0 = 1
S$fp antigo: fp do main (lixo)$sp
S−4b: 15 (lixo)
S−8a: 10 (lixo)
S−12$ra: → main (lixo)
S−16i: 0 (lixo)
S−20
$sp = S; $a0 = 1

Gerado executando o código acima num simulador MIPS: S é o $sp antes da chamada. Linhas em destaque estão empilhadas (acima de $sp); em cinza, valores que ficaram na memória depois de desempilhar (desempilhar não limpa).

Erros que mais custam ponto. Ler 0($sp) achando que é o topo (o topo é 4($sp)). Esquecer o pop depois de usar o valor empilhado. Empilhar os argumentos na ordem direta (x1 tem de ficar em 4($fp)). sub $a0 $a0 $t1 em vez de sub $a0 $t1 $a0. Somar os locais no z sem liberá-los antes. Reusar o mesmo rótulo em dois ifs.

Outras variações já cobradas

Laço com return no meio (2025/2)

int f() { int i = 0; while (i < 10) { int j = i + 1; for (; j < i − 1; j++) { if (j == 0) return −10; } i++; } return i; } — i em −4, j em −8; sem parâmetros, z = 8.

F_INICIO:
move $fp $sp
sw $ra 0($sp)
addiu $sp $sp -4
addiu $sp $sp -8          # i, j
sw $zero -4($fp)          # i = 0
WHILE_INICIO:
lw $a0 -4($fp)
slti $t1 $a0 10
beq $t1 $zero WHILE_FIM
lw $a0 -4($fp)
addiu $a0 $a0 1
sw $a0 -8($fp)            # j = i + 1
FOR_TESTE:
lw $a0 -8($fp)
lw $t1 -4($fp)
addiu $t1 $t1 -1
slt $t2 $a0 $t1           # j < i − 1 ?
beq $t2 $zero FOR_FIM
lw $a0 -8($fp)
bne $a0 $zero IF_FIM
li $a0 -10
b RETORNO                 # return −10
IF_FIM:
lw $a0 -8($fp)
addiu $a0 $a0 1
sw $a0 -8($fp)            # j++
b FOR_TESTE
FOR_FIM:
lw $a0 -4($fp)
addiu $a0 $a0 1
sw $a0 -4($fp)            # i++
b WHILE_INICIO
WHILE_FIM:
lw $a0 -4($fp)            # return i
RETORNO:
addiu $sp $sp 8
lw $ra 4($sp)
addiu $sp $sp 8
lw $fp 0($sp)
jr $ra

Como j = i + 1 > i − 1, o for nunca executa e f devolve 10. O código mantém a estrutura, que é o que a questão pede.

Global em endereço fixo (2025/1)

int b[10] em 0x600000: b[6] fica em 0x600000 + 24. lui $t1 0x0060 põe 0x00600000 em $t1; lw $a0 24($t1) lê b[6]. Em g(y, 5) + b[6] + b[7]: chama g (resultado em $a0), push, carrega b[6], lw $t1 4($sp), add, pop; repete para b[7].

Código completo da 2025/1 (callee que chama outra função)
# caller: f(2)
sw $fp 0($sp)
addiu $sp $sp -4
li $a0 2
sw $a0 0($sp)
addiu $sp $sp -4
jal F_ENTRADA
G_ENTRADA:               # g(x, m) = x * m
move $fp $sp
sw $ra 0($sp)
addiu $sp $sp -4
lw $a0 4($fp)             # x
lw $t1 8($fp)             # m
mult $a0 $t1
mflo $a0
lw $ra 4($sp)
addiu $sp $sp 16          # z = 4·2 + 8
lw $fp 0($sp)
jr $ra
F_ENTRADA:               # f(y) = g(y, 5) + b[6] + b[7]
move $fp $sp
sw $ra 0($sp)
addiu $sp $sp -4
sw $fp 0($sp)             # chamada g(y, 5)
addiu $sp $sp -4
li $a0 5                  # m
sw $a0 0($sp)
addiu $sp $sp -4
lw $a0 4($fp)             # x = y ($fp ainda é o de f)
sw $a0 0($sp)
addiu $sp $sp -4
jal G_ENTRADA             # $a0 = g(y, 5)
sw $a0 0($sp)             # push g(…)
addiu $sp $sp -4
lui $t1 0x0060
lw $a0 24($t1)            # b[6]
lw $t1 4($sp)
add $a0 $t1 $a0
addiu $sp $sp 4
sw $a0 0($sp)
addiu $sp $sp -4
lui $t1 0x0060
lw $a0 28($t1)            # b[7]
lw $t1 4($sp)
add $a0 $t1 $a0
addiu $sp $sp 4
lw $ra 4($sp)
addiu $sp $sp 12          # z = 4·1 + 8
lw $fp 0($sp)
jr $ra
Com b[6] = 100 e b[7] = 1000, f(2) = 10 + 100 + 1000 = 1110. g desempilha 16 (2 argumentos); f desempilha 12 (1 argumento).

Trechos soltos (Lista 4, Q11)

x = b * c:   lw $a0 off_b($fp) / lw $t1 off_c($fp) / mult $a0 $t1 / mflo $a0 / sw $a0 off_x($fp)
y = *q:      lw $t1 off_q($fp) / lw $a0 0($t1) / sw $a0 off_y($fp)
*p = y:      lw $t1 off_p($fp) / lw $a0 off_y($fp) / sw $a0 0($t1)
x = a[i]:    lw $t1 off_i($fp) / sll $t1 $t1 2 / lw $t2 off_a($fp) / add $t1 $t2 $t1 / lw $a0 0($t1) / sw $a0 off_x($fp)
q = q + 4:   lw $a0 off_q($fp) / addiu $a0 $a0 4 / sw $a0 off_q($fp)

No a[i], lw $t2 off_a($fp) vale quando a variável guarda o endereço-base (ponteiro ou parâmetro). Se o vetor está inteiro dentro do AR (int a[N] local em −4N … −4($fp)), a base é addiu $t2 $fp −4N.

Teste-se: em f(a, b, c) sem locais, quanto vale z (o tamanho a desempilhar) e onde está b? z = 4·3 + 8 = 20. b é o 2º argumento: 8($fp).

8. Objetos: layout e despacho 6 min

O que cai. "Mostre o layout dos objetos e as tabelas de despacho de Contador, ContadorParam, ContadorAleatorio; e com herança múltipla?" (Lista 4, Q9). "Gere cgen(e.f(arg))" (Lista 4, Q8). "Como o método certo é encontrado em execução?" (2026/1).

Layout

A0tag A4tamanho 58disp → A12a16dB : A0tag B4tamanho 68disp → B12a16d20bC : A0tag C4tamanho 68disp → C12a16d20ctabela A0: f_Atabela B0: f_B4: gtabela C0: f_A4: h
Atributo e método têm o mesmo offset em toda a hierarquia: a em 12 e d em 16 em A, B e C; f na posição 0 das três tabelas. B redefine f (f_B) e acrescenta g; C herda f_A e acrescenta h.

Despacho dinâmico em MIPS (Lista 4, Q8)

cgen(e.f(arg)) com e já avaliado em $a0; f recebe self e arg:

sw $a0 0($sp)          # guarda e
addiu $sp $sp -4
sw $fp 0($sp)          # início da chamada
addiu $sp $sp -4
cgen(arg)
sw $a0 0($sp)          # arg (x2)
addiu $sp $sp -4
lw $a0 12($sp)         # recupera e
beq $a0 $zero ERRO_NULO
sw $a0 0($sp)          # self (x1, o último)
addiu $sp $sp -4
lw $t1 8($a0)          # tabela de despacho do objeto
lw $t1 OF_f($t1)       # endereço de f (offset fixo)
jalr $t1               # chamada indireta
addiu $sp $sp 4        # descarta o e guardado
Objeto local dentro do AR (Lista 4, Q10). Contador c(10) em count() ocupa 4 palavras do AR, com o cabeçalho no endereço menor, para os offsets do objeto continuarem positivos: tag em −16($fp) (c começa aqui), tamanho em −12, disp em −8, cont em −4. countdown é inline: sem AR próprio.
COUNT_ENTRADA:
move $fp $sp
sw $ra 0($sp)
addiu $sp $sp -4
addiu $sp $sp -16         # c inteiro
li $a0 TAG_CONTADOR
sw $a0 -16($fp)
li $a0 4
sw $a0 -12($fp)
la $a0 CONTADOR_DISP
sw $a0 -8($fp)
li $a0 10
sw $a0 -4($fp)            # cont = 10
LACO:
lw $a0 -4($fp)
slt $t1 $zero $a0         # 0 < cont ?
beq $t1 $zero FIM
lw $a0 -4($fp)
addiu $a0 $a0 -1
sw $a0 -4($fp)            # countdown inline
b LACO
FIM:
addiu $sp $sp 16
lw $ra 4($sp)
addiu $sp $sp 8
lw $fp 0($sp)
jr $ra

Herança múltipla e interfaces

Teste-se: B herda de A {a, d} e acrescenta b. Em que offset está b? E o tamanho no cabeçalho? a em 12, d em 16, b em 20. Tamanho = 6 palavras (tag, tamanho, disp, a, d, b).

9. Avulsas 4 min

Seleção de instruções por ladrilhamento (2025/1)

Cada regra de tradução é um padrão de árvore (tile) com custo. Cobrir a AST com tiles de custo total mínimo. Tiles da prova: #1 Ri ← Add(Ri, Rj) custo 2; #2 Ri ← Add(Ri, Kc) custo 3; #3 Ri ← Kc custo 2; #4 Ri ← Add(Ri, Add(Rj, Kc)) custo 4. Expressão Add(Add(K1, K2), Add(K3, K4)):

AddAddAddK1K2K3K4#3 (2)#2 (3)#3 (2)#4 (4)custo = 2 + 3 + 2 + 4 = 11
Ladrilhamento ótimo da prova 2025/1: o tile #4 (Ri ← Add(Ri, Add(Rj, Kc))) cobre a raiz e o Add da direita de uma vez. Sem ele, o melhor custo seria 12.
  1. #3 LOADI R1, K1 (2)
  2. #2 ADDI R1, K2 (3) → R1 = K1 + K2
  3. #3 LOADI R2, K3 (2)
  4. #4 ADDRI R1, R2, K4 (4) → R1 = R1 + (R2 + K4)

Por que código ótimo nem sempre sai: em DAGs (subexpressões comuns) a seleção ótima é NP-completa; a escolha interage com alocação de registradores e escalonamento; os custos reais dependem do contexto. Na prática: maximal munch (maior tile a partir da raiz) ou programação dinâmica em árvores.

Literais maiores que a palavra (2025/2)

Um unsigned int32 numa máquina de w bits ocupa k = ⌈32/w⌉ palavras: parte[i] = (n >> i·w) & (2ʷ − 1). w = 8 → 4 partes; w = 16 → 2; w = 64 → 1 (extensão com zeros). Faixa 0 ≤ n ≤ 2³² − 1, senão erro semântico. 4294967295 = 0xFFFFFFFF → em 16 bits, [0xFFFF, 0xFFFF]; nunca tratar como int32 com sinal (viraria −1). Operações propagam o carry entre as partes.

Compilado × interpretado

Qualidade de um gerador de código

Corretude (preserva a semântica) em primeiro lugar; depois velocidade e tamanho do código, uso de registradores e memória, tempo de compilação, energia. O gerador depende da arquitetura (instruções, registradores, alinhamento).

10. Roteiro de prova 2 min

As 12 frases para lembrar.
  1. Sintetizado sobe; herdado desce.
  2. Estático = declaração aninhada mais próxima; dinâmico = última declaração ativa.
  3. Tipos sobem, ambiente desce.
  4. Sound: dinâmico ≤ estático.
  5. Argumentos reais ≤ parâmetros formais; o método é procurado no tipo estático.
  6. lub = menor ancestral comum.
  7. Caller empilha $fp e argumentos ao contrário; callee empilha $ra.
  8. cgen deixa o valor em $a0 e preserva $sp.
  9. xi em 4i($fp); locais em −4j($fp).
  10. z = 4n + 8, depois de liberar os locais.
  11. Objeto: tag 0, tamanho 4, tabela 8, atributos a partir de 12.
  12. Mesmo método, mesmo offset em toda a hierarquia; a tabela escolhida é a da classe dinâmica.