flowchart LR
M12["Módulo 12<br/>Análise semântica<br/>tabela de símbolos<br/>e verificação de tipos"]
M13["Módulo 13<br/>Representação intermediária<br/>e ambientes de execução<br/>ESPECIFICAÇÃO DO OBJETO"]
M14["Módulo 14<br/>Geração de código<br/>PRIMEIRO ARQUIVO<br/>QUE SOBREVIVE"]
M15["Módulo 15<br/>Otimização e<br/>integração final"]
M12 --> M13 --> M14 --> M15
subgraph ENTREGA["O que o módulo 14 deixa pronto"]
D1["Conjunto de instruções<br/>com efeito declarado"]
D2["Gerador por percurso<br/>em pós-ordem"]
D3["Curto-circuito por<br/>desvios e preenchimento<br/>retroativo"]
D4["Executor mínimo escrito<br/>contra a especificação"]
end
M14 --- ENTREGA
M13 -.->|"formato e modelo de execução"| D1
D2 -.->|"código sobre o qual otimizar"| M15
D4 -.->|"executor completo"| M15
Módulo 14: Plano de Aula — Geração de Código
Documento exclusivo do professor. Este é o guia operacional das seis aulas do módulo 14: roteiro por blocos das duas aulas teóricas, plano das quatro aulas de tutoria, entregáveis e riscos antecipados. Não distribua à turma — as questões de discussão perdem função assim que o estudante lê a resposta antes de votar, e o efeito do fechamento conceitual depende de ele não estar anunciado.
Visão Geral do Módulo
Onde este módulo fica — é aqui que o compilador passa a produzir um arquivo, e é aqui que a teoria do primeiro bloco reaparece dentro dele.
Este módulo tem duas naturezas, e conduzi-lo bem depende de não confundir uma com a outra. Tecnicamente, é onde a geração deixa de ser tradução e passa a exigir construção de estrutura de controle — a avaliação com curto-circuito é a maior dificuldade técnica do semestre e é onde os grupos travam. Pedagogicamente, é o módulo do fechamento: os autômatos do início do semestre passam a ser gravados no arquivo que o compilador produz. Anuncie o segundo apenas no fim; ele perde força se você contar a piada na abertura.
A dependência do módulo anterior é mais rígida do que qualquer outra do curso: o gerador é literalmente uma função da representação intermediária, do formato do objeto e do modelo de execução, todos especificados no módulo 13. Grupos cuja especificação ficou incompleta descobrem isso aqui, com dois componentes a depurar ao mesmo tempo. Comece a primeira tutoria por essa triagem, e não pelo código.
Objetivos, Competências e Habilidades
Objetivos de aprendizagem. Completar a síntese, produzindo saída concreta a partir da representação intermediária. Apresentar os subproblemas clássicos da geração de código e o modelo de máquina de destino adotado. Evidenciar o reencontro entre o conteúdo do primeiro bloco e o produto final do compilador.
Competências a desenvolver. Capacidade de reconhecer, num problema de otimização com subproblemas interdependentes, por que a decomposição é necessária apesar de subótima. Capacidade de projetar um conjunto de instruções adequado a um propósito, avaliando o compromisso entre simplicidade de geração e eficiência de execução.
Habilidades a adquirir. Enunciar os três subproblemas da geração de código e explicar sua interdependência. Comparar máquinas de pilha e de registradores quanto ao que facilitam. Projetar um conjunto de instruções para uma máquina abstrata de pilha. Gerar código por percurso sobre a árvore, tratando expressões aninhadas e curto-circuito em operadores lógicos. Explicar a alocação de registradores pelo modelo de coloração. Resolver referências pendentes.
Estrutura das Aulas
Aulas 1 e 2 — Aula Teórica
Roteiro por blocos — a primeira aula constrói a máquina e o esquema simples; a segunda quebra o esquema simples e o reconstrói com fluxo de controle.
Bloco de abertura — o compilador que não deixa rastro
Comece rodando, projetado, o compilador do módulo anterior sobre um programa correto. Ele analisa, verifica, constrói a representação intermediária e imprime que está tudo certo. Então pergunte à turma o que sobrou no disco. A resposta é nada, e a frase que resume o módulo é essa: até aqui, tudo o que o compilador produziu morria junto com o processo. Deixe a sala nomear o que falta antes de você nomear; alguém dirá “gerar o executável”, e aí peça precisão — gerar o quê, para qual máquina, em que formato.
Feche o bloco com uma pendência declarada, e não a resolva antes do último bloco da segunda aula: o arquivo que vocês vão gerar tem uma surpresa dentro, porque a maior parte dele, contada em bytes, não vai ser código. Escreva “o que é a maior parte do arquivo?” no canto do quadro e não apague durante as duas aulas.
Bloco seguinte — o espaço de soluções e os três subproblemas
Escreva o contrato no quadro, e escreva-o como reaparição, não como novidade:
\llbracket G(r) \rrbracket_M(x) = \llbracket r \rrbracket_R(x).
É a mesma igualdade do primeiro módulo, agora no último trecho da cadeia. O ponto a verbalizar é que ela não determina o gerador: infinitas sequências a satisfazem, porque basta acrescentar instruções sem efeito observável ou reordenar operações independentes. Nomeie a inversão de tom em voz alta — até aqui as perguntas tinham resposta única, e daqui em diante o trabalho é escolher uma boa entre muitas corretas. A turma sente isso como perda de chão, e quem não sabe que a mudança é do objeto passa a desconfiar do próprio raciocínio.
Apresente os três subproblemas — seleção de instruções, alocação de registradores, ordenação da avaliação — e feche o ciclo de dependências em voz alta: a melhor seleção depende de quantos registradores há; a alocação depende da ordem, porque é a ordem que decide quem está vivo ao mesmo tempo; e a melhor ordem depende das instruções selecionadas. Só então diga que a prática decompõe e aceita o subótimo, e defenda isso como decisão consciente: troca-se otimalidade por tratabilidade e modularidade, e um gerador que resolvesse os três juntos seria um bloco indepurável que ainda assim usaria heurística.
O exemplo numérico da ordenação precisa ser feito no quadro, com os rótulos calculados na frente da turma. Tome a expressão que subtrai, de um produto, uma soma — quatro folhas, um produto à esquerda, uma soma à direita. Pela numeração de Sethi–Ullman, cada folha vale 1; no produto os dois filhos empatam em 1, logo o rótulo é 1 + 1 = 2; na soma, idem, 2; e na raiz os dois filhos empatam em 2, logo \ell = 3. Três registradores. Agora mude a árvore na frente deles: troque a soma por uma folha simples. A raiz passa a ter filhos de rótulos 2 e 1, que diferem, e o rótulo vira \max(2,1) = 2. Um registrador a menos, sem mudar nenhuma operação — só a forma da árvore. E enuncie a regra que sai do teorema: avalie primeiro a subárvore de maior rótulo, porque assim o resultado da mais barata é que espera durante o cálculo mais caro.
Diga também o que as hipóteses excluem, porque a turma generaliza sozinha: com um valor compartilhado por duas subexpressões a estrutura não é árvore e a garantia se perde, e com operadores de efeito colateral a liberdade de reordenar não existe.
Bloco seguinte — os dois modelos de máquina
Conduza por comparação, não por definição sequencial. Na máquina de pilha as operações não nomeiam operandos: a posição faz o trabalho que o nome faria, e o gerador não precisa decidir onde pôr nada. Na máquina de registradores as instruções nomeiam de onde leem e onde escrevem, executam menos instruções para o mesmo trabalho, e transferem para o gerador a decisão de qual valor mora em qual registrador.
Monte a tabela comparativa com a turma — operandos, alocação, ordenação, tamanho do código, instruções executadas, dificuldade do gerador, dificuldade do executor, verificação estática — e leia o resultado como redistribuição, não como ranking: a dificuldade total é aproximadamente constante e o que muda é onde ela mora.
O argumento histórico explica o presente: no início dos anos 1970, Niklaus Wirth e seu grupo distribuíram um compilador de Pascal que gerava código para uma máquina de pilha hipotética, e portar a linguagem para outro computador passou a se reduzir a escrever um interpretador pequeno. Código compacto, executor fácil de acertar e verificabilidade são as mesmas três razões pelas quais as máquinas virtuais de uso corrente adotam esse modelo.
Primeira questão de discussão em duplas. “Escolher uma máquina de pilha como destino faz desaparecer qual ou quais dos três subproblemas? (a) apenas a alocação de registradores; (b) apenas a ordenação da avaliação; (c) os dois, alocação e ordenação; (d) nenhum dos três, porque eles apenas mudam de lugar.”
Aplique o procedimento inteiro: voto individual sem comentário seu, discussão em duplas, segundo voto. A resposta é (c) — não há registradores para alocar, e a ordem de emissão é fixada pela pós-ordem. Quem vota (a) não percebe que a pós-ordem também tirou uma decisão do gerador. Quem vota (d) generalizou a frase “a dificuldade se redistribui” para além do que ela afirma, e esse é o erro produtivo: a redistribuição é entre gerador e executor, não entre os três subproblemas. Deixe claro que a seleção de instruções continua existindo, ainda que trivial.
Bloco seguinte — projetar o conjunto de instruções
Este bloco muda o papel do estudante: ele deixa de ser usuário de uma máquina e passa a ser projetista de uma. Estabeleça que uma instrução se caracteriza por código de operação, argumento opcional e — a parte que quase todo mundo omite — a declaração do seu efeito: quantos valores consome, quantos deposita, o que faz com o contador de instruções. Sem o efeito declarado, quem gera e quem executa discordam de boa-fé, cada lado testa contra a própria interpretação e os dois passam.
Faça a conta do efeito de pilha no quadro. Com \delta(i) = p(i) - c(i), a sequência que carrega dois valores, multiplica, carrega mais dois, soma e subtrai tem efeitos +1, +1, -1, +1, +1, -1, -1, e as alturas acumuladas são 1, 2, 1, 2, 3, 2, 1. Ponha os dois vetores um sob o outro. A altura máxima é 3 — o mesmo número que a numeração de Sethi–Ullman deu para essa expressão. Aponte a coincidência e não a explique; ela será paga no bloco de registradores da segunda aula, e a pergunta aberta melhora aquele bloco.
Enuncie então a condição que mantém a altura calculável mesmo com desvios: se todos os caminhos que chegam a um ponto o alcançam com a mesma altura, a altura é função apenas do ponto. É requisito de projeto do esquema de geração, e satisfazê-lo é fácil quando se sabe dele desde o começo.
Feche com duas decisões de formato e o critério de completude. Instruções de tamanho fixo compram executor simples, indexação direta do código e a impossibilidade estrutural de um desvio cair no meio de uma instrução; a área de constantes resolve o literal que não cabe no campo de argumento e uniformiza a decodificação. E o critério de completude é operacional: a especificação está pronta quando outra pessoa, lendo apenas ela, consegue escrever um executor compatível. Diga a consequência metodológica em voz alta, porque é dela que depende a tutoria — escrever o executor lendo o gerador produz dois testemunhos idênticos em vez de dois independentes.
Bloco de fechamento da primeira aula — a pós-ordem e a leitura única
Mostre que, com a máquina projetada, o gerador fica curto — e mostre por quê. O percurso em pós-ordem emite, para cada folha, a instrução que carrega o valor, e para cada nó interno a instrução da operação. A demonstração por indução cabe em três frases no quadro: cada subsequência deixa exatamente um valor e não perturba o que está abaixo; após os k filhos, os k valores estão na pilha; a instrução da raiz consome exatamente esses k e deposita um. Insista que a pós-ordem não é escolha estética — emitir a operação antes dos operandos produziria uma instrução que consome de uma pilha vazia. É consequência do modelo de máquina.
Registre a armadilha da ordem de desempilhamento: o segundo operando entrou por último e sai primeiro. Numa soma ou num produto, trocar a ordem não muda nada e o defeito fica invisível; numa subtração ou numa comparação de ordem, o resultado sai errado. Diga que a expressão do bloco anterior termina em subtração de propósito — é uma lição sobre escolher o caso de teste que revela o defeito.
Termine com a invariante de leitura única, que sustenta o esquema inteiro e quase sempre fica implícita: numa máquina de pilha, ler é desempilhar, e um valor lido some. Se duas instruções pretenderem ler o mesmo valor, a segunda lerá o que estiver abaixo — que é outro valor, provavelmente do tipo certo, e portanto um erro que não se anuncia. Avise que código de três endereços não satisfaz essa invariante automaticamente, porque nada nele impede que um temporário seja definido uma vez e lido duas. É a advertência que salva a tutoria de metade dos grupos.
Bloco de abertura da segunda aula — a instrução que não pode existir
Não recapitule. Escreva no quadro uma condição com conjunção — dois testes ligados por um “e” — e pergunte: para executar uma instrução de conjunção, o que precisa estar na pilha? A sala responde: os dois valores. Puxe a conclusão delas mesmas: então os dois operandos foram avaliados, e isso é exatamente o oposto de curto-circuito. Logo, nenhuma instrução lógica implementa curto-circuito, quaisquer que sejam as instruções da máquina.
Segunda questão de discussão em duplas. “Uma máquina de pilha tem a instrução de conjunção, que consome dois valores lógicos do topo e deposita a conjunção deles. Um gerador que traduza o operador lógico da linguagem para essa instrução implementa: (a) curto-circuito, porque a conjunção é falsa assim que o primeiro operando é falso; (b) avaliação ansiosa, necessariamente; (c) curto-circuito, desde que o executor teste o topo antes de desempilhar o segundo valor; (d) depende da ordem em que o gerador emitiu os operandos.”
A resposta é (b). Quem vota (a) confunde o valor da conjunção com o trabalho feito para obtê-lo. Quem vota (c) rende a melhor discussão: a proposta é engenhosa e falha porque, no instante em que a instrução executa, o segundo valor já está na pilha, e para estar lá foi calculado. Feche com a conclusão que abre o bloco seguinte: curto-circuito não é operação sobre valores, é fluxo de controle.
Bloco de construção ao vivo — curto-circuito por fluxo de controle
Este é o bloco mais difícil do semestre e é de construção ao vivo, do início ao fim. Avise a turma para abrir o editor e digitar junto, pause a cada peça e circule para conferir; quem só assiste não aprende este bloco, e o custo de não ter digitado aparece na tutoria.
Antes do código, fixe a inversão de perspectiva com todas as letras: em vez de traduzir a expressão lógica para código que calcula um valor, traduz-se para código que desvia para um lugar quando ela é verdadeira e para outro quando é falsa. O valor lógico deixa de ser dado na pilha e passa a ser a posição para onde o controle foi.
Introduza as duas listas de desvios pendentes — a de verdadeiro e a de falso — e apresente o esquema recursivamente, sem casos especiais. Para uma comparação simples, emite-se o cálculo dos operandos, a comparação e um desvio condicional com destino em branco: ele entra na lista de falso, e a de verdadeiro fica vazia porque, não sendo o desvio tomado, o controle já está no lugar certo. Para a conjunção, traduz-se o operando esquerdo, preenche-se a lista de verdadeiro dele com a posição em que o direito vai começar, traduz-se o direito e devolve-se como lista de falso a união das duas. Leia isso em português, porque a leitura é literalmente a semântica: se o esquerdo for verdadeiro siga para o direito; se qualquer um dos dois for falso, a conjunção é falsa e o destino é o mesmo. A disjunção é o espelho, com as listas trocando de papel.
Faça o exemplo numérico completo no quadro, com endereços. Tome a condição “valor maior que 100 e valor menor que 500” e numere as instruções a partir de zero: carrega valor, carrega 100, compara maior, desvia se falso para destino em branco na posição 3; carrega valor, carrega 500, compara menor, desvia se falso para destino em branco na posição 7; corpo controlado pela condição nas posições 8 e 9; posição 10 é o fim. Agora preencha retroativamente na frente deles: os dois desvios das posições 3 e 7 estão na mesma lista de falso e recebem o mesmo destino, 10. Duas instruções incompletas, um único preenchimento, e o desvio da posição 3 salta as seis instruções seguintes — é essa a economia que o curto-circuito significa concretamente.
Termine pela verificação, porque curto-circuito é notoriamente difícil de testar: o resultado da expressão é frequentemente o mesmo com e sem ele, e testes que olham só o resultado não distinguem as duas implementações. O que torna a diferença observável é a contagem de instruções executadas. E o critério de projeto dos casos de teste vai para o quadro: para cada operando de cada operador lógico, uma entrada que falhe por causa daquele operando.
Bloco seguinte — referências pendentes e o mapa de endereços
Generalize o que acabou de ser construído: uma referência pendente é qualquer campo de instrução já emitida cujo valor correto ainda não se conhece — o destino de um desvio para a frente é o caso típico, endereços de dados e nomes de outra unidade de compilação são os outros. O problema é estrutural: a emissão é sequencial e a referência pode apontar para a frente.
Apresente o mapa de endereços como a peça que traduz a numeração da representação intermediária para a do objeto, construído durante a emissão e consultado numa segunda passagem. Corte na hora a tentação de corrigir os destinos na mesma passagem: no instante da emissão o endereço do alvo não é difícil de calcular, é inexistente.
Dois detalhes do mapa produzem defeito real quando esquecidos. Ele tem um elemento a mais que o número de instruções da representação, porque “logo depois da última” é destino legítimo — exatamente o destino 10 do exemplo anterior. E nem toda instrução da representação produz uma instrução do objeto: um rótulo não emite nada, e uma operação pode exigir duas instruções da máquina. O mapa existe para absorver essas discrepâncias.
Terceira questão de discussão em duplas. “Um grupo verifica que, no seu compilador, cada instrução da representação intermediária virou exatamente uma instrução do objeto, e o mapa de endereços ficou sendo a identidade. Ele deve: (a) remover o mapa, que é código morto; (b) manter o mapa, porque a coincidência é contingente; (c) remover o mapa e acrescentar uma verificação que confira a identidade a cada geração; (d) remover o mapa apenas se a linguagem não tiver desvios.”
A resposta é (b). Quem vota (a) aplica uma regra boa — apague o que não faz nada — a um caso em que ela não vale. Quem vota (c) chegou perto e escolheu o caminho mais caro: a verificação custa o mesmo que o mapa e falha no dia em que a identidade se quebrar, enquanto o mapa continua correto nesse dia. Feche com o critério geral: quando duas grandezas coincidem por acidente da configuração atual, mantenha o mecanismo que as relaciona. Coincidência não é invariante.
Bloco seguinte — alocação de registradores por coloração
Comece admitindo o que a turma vai perceber sozinha: numa máquina de pilha este subproblema não existe. Trate-o assim mesmo — a redução a coloração é um dos exemplos mais limpos de modelagem da área, e qualquer geração para hardware real passa por aqui.
Defina faixa de vida como o intervalo semiaberto entre a instrução que define o temporário e a última que o lê, e justifique o extremo aberto na hora, porque dele depende o resto do bloco: um valor morre na instrução que o lê pela última vez, e o registrador que ele ocupava pode receber o valor que essa mesma instrução acabou de produzir. Defina interferência como sobreposição de faixas, monte o grafo e enuncie a equivalência: uma atribuição de registradores é válida se e somente se é uma coloração própria do grafo de interferência. Chame a atenção para a redução ser exata — as duas condições são a mesma, com vocabulário trocado — e diga que é isso que significa modelar bem: achar um problema matemático que é o mesmo problema, e não um parecido. A correspondência e o primeiro alocador baseado nela devem-se a Gregory Chaitin e colaboradores, na IBM, no início da década de 1980.
Faça o exemplo numérico no quadro, com a mesma expressão da primeira aula, agora em três endereços com sete temporários nas instruções 0 a 6: t_0 e t_1 carregam as duas primeiras folhas, t_2 é o produto, t_3 e t_4 carregam as outras duas, t_5 é a soma e t_6 a subtração. As faixas semiabertas são [0,2), [1,2), [2,6), [3,5), [4,5), [5,6) e [6,7). Marque as sobreposições uma a uma: t_0 com t_1; t_2 com t_3, com t_4 e com t_5; e t_3 com t_4 — cinco arestas. Como t_2, t_3 e t_4 são mutuamente adjacentes, nenhuma coloração usa menos de três cores, e a gulosa encontra três. Três registradores — o mesmo número da altura máxima da pilha e do rótulo de Sethi–Ullman. Pague então a dívida da primeira aula: com cada temporário lido uma única vez, o número de valores vivos num ponto é a altura da pilha ali; as duas máquinas precisam do mesmo espaço de trabalho, e o que muda é quem o endereça.
Refaça então o cálculo com intervalos fechados, ao lado do primeiro, sem apagar nada. Com o extremo direito incluído, t_5 passa a tocar t_3 e t_4 no índice 5, os quatro temporários t_2 a t_5 ficam mutuamente adjacentes e a resposta vira quatro registradores. O que interessa não é o erro: é que ele não produz código incorreto. Produz código correto e conservador demais, nenhum teste o acusa, e quatro registradores para uma expressão dessas é número plausível. Enuncie a técnica que o pega, a mais barata do curso: sempre que uma grandeza puder ser calculada de duas maneiras independentes, calcule das duas e imprima lado a lado.
Complete com a má notícia: decidir se um grafo admite coloração com k cores é NP-completo para todo k \ge 3, resultado da lista clássica de Richard Karp, de 1972. Daí a heurística de simplificação — remover e empilhar vértices de grau menor que k — e o derramamento quando ela trava. E delimite: em código linear as faixas são intervalos, o grafo é de intervalos e a gulosa é ótima; a dificuldade real está entre blocos.
Bloco de fechamento — o que é a maior parte do arquivo
Volte ao quadro da abertura e pague a pendência. Um programa objeto tem seções de código e seções de dados, e a novidade mora na segunda. Quando a linguagem compilada tem reconhecimento de padrões no seu domínio, cada padrão vira um autômato finito determinístico — pelo mesmo caminho dos primeiros módulos, da notação para a árvore, desta para o não determinístico por Thompson, deste para o determinístico por subconjuntos e deste para o mínimo por refinamento de partições — e o gerador o serializa na seção de dados, para o executor consultar uma vez por caractere da entrada.
Faça a conta que fecha o gancho, no quadro. Um padrão com seis estados sobre um alfabeto de bytes ocupa 6 \times 256 = 1536 células de transição; um segundo padrão com cinco estados ocupa 5 \times 256 = 1280; juntos, 2816 células. As instruções que implementam as regras do programa são algumas dezenas. A maior parte do arquivo, em bytes, é autômato — o produto da compilação é literalmente mais teoria de autômatos do que código. Não é analogia nem paralelo didático: é o mesmo objeto matemático, com a mesma tabela de transição, atravessando o semestre inteiro e reaparecendo como produto.
Encerre com o conselho de estudo, que a turma precisa ouvir antes da tutoria: os módulos de análise podiam ser verificados por raciocínio, e este não pode. A distância entre entender o esquema de curto-circuito e implementá-lo só se atravessa gerando código, executando e conferindo a saída à mão contra o que se esperava — escrita antes de rodar.
Aulas 3 a 6 — Tutoria do Projeto Integrador
Quatro aulas de tutoria — gerador emitindo o objeto, curto-circuito tratado com desvios, executor mínimo escrito contra a especificação e três programas compilados, executados e conferidos.
O andaime aqui é quase nulo, e essa é a diferença deste módulo para os do primeiro bloco. Você não fornece esquema pronto nem critério novo: o esquema foi construído ao vivo na aula teórica e o critério de completude é o mesmo do módulo anterior. O que você fornece é triagem, contraexemplo e a recusa de aceitar demonstração que não demonstra.
Tenha o compilador Peneira aberto e projetado nas duas sessões, e tenha em mãos o objeto textual gerado por ele. O grupo que vê um objeto real na tela — a tabela de padrões com os estados e as células, e as onze instruções das regras logo abaixo — entende em um minuto o que precisa produzir; o grupo que só ouve a descrição do formato escreve um arquivo com outra estrutura e descobre no módulo 15.
Primeira sessão de tutoria — triagem da especificação e o gerador
Abra pela triagem, e não pelo código. Pergunte a cada grupo, com a especificação do módulo 13 na tela: outra pessoa escreveria um executor compatível lendo apenas isto? Faça a pergunta concreta em três pontos que quase sempre estão faltando — qual operando de uma comparação sai primeiro da pilha, o que acontece quando dois padrões casam com o mesmo comprimento, e o que significa um casamento de comprimento zero. Grupo que não sabe responder tem pendência do módulo anterior, e a instrução é saldar a especificação antes de escrever gerador. Custa uma parte da sessão; não saldar custa o módulo 15 inteiro, quando houver dois componentes divergentes e nenhum documento para arbitrar.
Feita a triagem, o gerador em si é curto e os grupos avançam rápido. Circule cobrando duas coisas que eles não fazem sozinhos. A primeira é a verificação da invariante de leitura única, que quase todo grupo considera desnecessária porque “a nossa representação já garante isso” — a resposta é a que dei em aula, e vale repeti-la individualmente: garantias por construção são precisamente as que se perdem em manutenção, porque a construção muda e a garantia não estava escrita em lugar nenhum. Mostre no meu projeto que a verificação existe apesar de a invariante ser garantida, e mostre também que ela despacha por operação em vez de varrer campos, senão conta literais como se fossem temporários.
A segunda é o mapa de endereços. Vários grupos verão a correspondência sair um para um e concluirão que o mapa sobra. Não discuta em abstrato: peça que acrescentem um rótulo à representação intermediária, na sua frente, e mostrem o que acontece. O contador da representação avança e o do objeto não, a identidade quebra na hora, e o desvio passa a apontar para a instrução errada sem que nada reclame.
Reserve o fim da sessão para o curto-circuito, porque é o que trava. Sente-se com cada grupo que estiver emitindo uma operação lógica em vez de desvios e refaça com ele o esquema das duas listas sobre a condição concreta da linguagem dele — não sobre a minha. O erro mais comum não é errar o esquema: é preencher a lista de verdadeiro e esquecer de devolver a de falso a quem chamou, deixando desvios pendentes que nunca serão preenchidos. Cobre a verificação final de que nenhuma lista sobrou, que custa uma linha e evita um desvio para posição arbitrária na primeira execução que passar por ali.
Segunda sessão de tutoria — executor, três programas e a conferência à mão
Abra com a exigência que dá sentido a tudo o que vem depois: o executor é escrito contra a especificação, sem abrir o código do gerador. Diga por quê com a formulação que usei no meu projeto, porque é a que convence: escrito contra o gerador, o executor concorda com os defeitos do gerador, os dois estarão errados juntos e todos os testes passarão. Vale instituir a divisão dentro do grupo — quem escreveu o gerador não escreve o executor —, e vale insistir que essa é a razão de a especificação ter vindo antes.
Antecipe três armadilhas do laço principal, porque as três aparecem e nenhuma se anuncia. O casamento de comprimento zero é a pior: um padrão que aceita a cadeia vazia casa sem consumir nada, a posição de leitura não avança, e o executor trava sem erro, sem mensagem e sem sintoma. Tratar comprimento zero como ausência de casamento é o que garante a terminação, e no meu projeto isso está no código com comentário justamente porque não é dedutível de olhar. A segunda é o desempate entre padrões de mesmo comprimento, que sai de graça se estiver especificado e custa dias de confusão se não estiver. A terceira é a ordem dos operandos nas comparações: o segundo foi empilhado por último e sai primeiro, e inverter passa despercebido em todos os casos simétricos — igualdade e desigualdade continuam certas, e só as comparações de ordem denunciam.
A segunda metade da sessão é a demonstração, e é onde você aplica o critério anunciado. Exija, para cada um dos três programas, a característica que ele exercita e os outros dois não. No meu projeto, um exercita o curto-circuito da conjunção com desvios para a frente, outro o da disjunção com desvio incondicional, e o terceiro dois padrões competindo pelo casamento mais longo. Projete a saída e faça a conferência à mão na frente da turma, porque é ela que dá valor à demonstração: dos sete números da entrada do primeiro programa, apenas três estão estritamente dentro da faixa, quatro falham por um dos dois lados, e três desses quatro falham já na primeira comparação — com o segundo teste nunca avaliado. E mostre que o curto-circuito é observável na contagem de instruções executadas, não no resultado: sessenta e oito instruções para sete casamentos dão menos de dez por casamento, quando o caminho completo tem doze.
Cobre também a saída esperada escrita antes de rodar. Grupo que produz a tabela de conferência depois da execução está confirmando o que o programa fez, não verificando o que ele deveria fazer, e isso é fácil de detectar: peça o arquivo de expectativas com data anterior à do objeto.
O fechamento, e onde ele acontece
Deixe a demonstração dos autômatos dentro do objeto para o fim da segunda sessão, com o arquivo aberto na tela. Mostre a tabela de padrões — no meu compilador, dois padrões de seis e cinco estados, mil quinhentas e trinta e seis e mil duzentas e oitenta células — e ao lado as onze instruções das duas regras. A conta se faz sozinha e não precisa de retórica: a maior parte do arquivo, em bytes, é autômato.
Peça então que cada grupo aponte, no próprio objeto, onde está a estrutura que veio do primeiro bloco do semestre. Grupos cuja linguagem não tem reconhecimento de padrões apontarão outra coisa — uma tabela de símbolos serializada, um mapa de rótulos —, e está certo: o ponto é que dados calculados na análise sobrevivem no produto. Vale documentar isso explicitamente na entrega, com a menção ao módulo em que a estrutura foi estudada.
Registre no seu diário, ao fim da segunda sessão, dois itens por grupo: se o executor foi mesmo escrito contra a especificação, e quantas características distintas os três programas exercitam. Os dois são os melhores preditores de como o grupo vai atravessar o módulo 15, em que o conjunto de teste passa a ser a única defesa contra otimização que altera comportamento.
Entregáveis e Avaliação
Ao fim do módulo, cada grupo entrega o gerador emitindo o programa objeto no formato especificado no módulo anterior, um executor mínimo capaz de rodá-lo, ao menos três programas compilados e executados com entrada e saída registradas, e os objetos salvos para inspeção. Havendo operadores lógicos na linguagem do grupo, o curto-circuito precisa estar tratado com desvios, e não com operações.
Confira cada entrega contra sete itens, na mesma ordem para todos os grupos: o objeto está no formato que a especificação do módulo anterior descreve; o curto-circuito é demonstrado pela contagem de instruções executadas, e não pelo resultado; nenhuma referência pendente sobrou e todo destino de desvio é índice válido; o executor foi escrito contra a especificação; os três programas exercitam coisas diferentes e a saída esperada foi escrita antes de rodar; o objeto salvo é legível e o grupo aponta nele código e dados; e a compilação segue limpa sob o modo estrito.
Um critério merece ser anunciado antes e aplicado literalmente: três programas parecidos demonstram uma coisa só. Peça, para cada programa, qual característica ele exercita que os outros dois não. Grupo que não responde tem conjunto de teste que não distingue nada, e é melhor descobrir agora do que no módulo 15, quando as otimizações dependerem de uma regressão que denuncie mudança de comportamento.
Registre no componente contínuo a pontualidade, a contribuição nas discussões em duplas e o engajamento na tutoria — em particular o revezamento de papéis, que é neste módulo que mais se abandona, porque o curto-circuito assusta e o grupo entrega o teclado a quem entendeu primeiro.
Orientações Sobre o Aplicativo
Use o aplicativo da disciplina para as três votações das aulas teóricas, com a votação anônima na projeção. A segunda questão, a da instrução de conjunção, produz o histograma mais informativo do módulo: uma turma que converge para a alternativa do executor esperto está a um passo de entender o assunto, porque errou pelo lado engenhoso; uma turma dispersa entre as duas primeiras alternativas ainda confunde valor com trabalho realizado, e aí refaça a definição antes de liberar a discussão em duplas — a instrução por pares só corrige quando há quem tenha entendido.
Guarde os dois histogramas de cada questão. A dispersão na questão do mapa de endereços prevê bem quantos grupos vão implementar o gerador supondo que os índices coincidem, e dimensiona quanto do assunto retomar na primeira tutoria.
Acompanhe também o engajamento no estudo do material, que neste módulo costuma cair — sinal de conteúdo denso, não de desistência. O que preocupa é o inverso: grupo com engajamento alto e nenhum avanço no gerador quase sempre está travado numa especificação incompleta do módulo anterior.
Pontos de Atenção Específicos
O bloco de curto-circuito não cabe em explicação. É o único bloco do módulo em que pular a construção ao vivo custa o módulo inteiro. Se a aula ficar apertada, corte profundidade na alocação de registradores, que é conceitual e não tem entregável dependente, e não no curto-circuito, que é exatamente o que a tutoria vai cobrar.
O reaproveitamento de temporários é o defeito que passa em todos os testes simétricos. Grupos cuja representação intermediária lê um temporário duas vezes geram código que desempilha o valor errado, e como o valor errado costuma ter o tipo certo, nada estoura. Cobre a verificação em código — contar as leituras de cada temporário e recusar a geração quando alguma passar de um — e antecipe a armadilha dela: varrer todos os campos de operando conta literais e índices como se fossem temporários, de modo que a contagem precisa despachar por tipo de operação.
O fechamento conceitual perde efeito se for anunciado cedo. A frase de que os autômatos do início do semestre estão dentro do arquivo gerado é o momento mais forte que a disciplina tem, e ela só funciona se a turma chegar nele por conta própria. Não a use como abertura, não a use na apresentação do módulo e não deixe escapar na tutoria da semana anterior.
A digressão sobre otimização vai aparecer. Assim que a turma vir a contagem de instruções executadas, alguém pergunta se não dava para gerar menos. A pergunta é legítima e é o módulo seguinte. Responda em duas frases, reafirme a ordem de prioridades ao julgar um gerador — correção, depois previsibilidade, só então qualidade do código — e diga que invertê-la é a forma mais comum de terminar o semestre com um gerador sofisticado que não funciona.