Autor
Afiliações

Moacyr Francischetti Corrêa, Bacharel em Ciência da Computação, Licenciado em Computação, Especialista em Ciência de Dados e Inteligência Artificial, PhD em Biotecnologia in Silico

Representações Intermediárias e Ambientes de Execução — Plano de Aula

Documento exclusivo do professor. Guia operacional das seis aulas deste módulo: 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 em duplas perdem função assim que o estudante lê a resposta antes da aula, e o mesmo vale para o traçado de preenchimento retroativo, que só ensina quando é construído no quadro com a sala.

Visão Geral do Módulo

Onde este módulo fica — entre a árvore verificada que chegou do módulo anterior e o arquivo objeto que o próximo emite.

flowchart LR
    M12["Módulo 12<br/>Análise semântica<br/>árvore verificada e<br/>tabela de símbolos"]
    M13["Módulo 13<br/>Representações intermediárias<br/>e ambientes de execução<br/>MÓDULO DE DECISÃO"]
    M14["Módulo 14<br/>Geração de código<br/>o objeto emitido<br/>conforme a especificação"]
    M15["Módulo 15<br/>Otimização e<br/>integração final"]

    M12 --> M13 --> M14 --> M15

    subgraph ENTREGA["O que o módulo 13 deixa pronto"]
        D1["Camada intermediária<br/>escolhida e justificada<br/>com os números do caso"]
        D2["Tradução da árvore para<br/>código de três endereços<br/>com preenchimento retroativo"]
        D3["Especificação do objeto<br/>e do modelo de execução"]
        D4["Mapa de memória e<br/>registro de ativação<br/>descritos"]
    end

    M13 --- ENTREGA
    D2 -.->|"entrada do gerador"| M14
    D3 -.->|"contrato do executor"| M14
    D1 -.->|"lugar das otimizações"| M15
Figura 1: O módulo 13 entre a análise semântica e a geração de código.

Este é o módulo em que o produto principal deixa de ser um programa e passa a ser um documento, e essa inversão precisa ser dita à turma em voz alta na primeira aula. O que se decide aqui — qual é a camada entre análise e síntese, o que o compilador escreve no arquivo de saída, o que a máquina faz com esse arquivo — não dá erro de compilação quando está errado. Dá comportamento divergente semanas depois, quando alguém tenta escrever um executor a partir do texto e descobre que ele não decidiu o que fazer no empate.

A consequência para a condução é direta: o rigor que nos módulos de autômatos vinha do compilador, aqui tem de vir de você. O único instrumento de controle disponível é o critério de completude aplicado literalmente — outra pessoa escreve um executor compatível lendo apenas a especificação? — e ele só funciona se for anunciado antes da tarefa e cobrado sem complacência na correção.

Há ainda uma particularidade de honestidade intelectual que dá o tom do módulo inteiro. O argumento clássico a favor da camada intermediária é combinatório, e num compilador de origem única e destino único ele condena a camada em vez de justificá-la. Você vai fazer essa conta na frente da turma e vai deixá-la desconfortável de propósito, porque é dessa incomodidade que sai o segundo argumento, o que realmente sustenta a decisão: a separação de dificuldades.

Objetivos, Competências e Habilidades

Objetivos de aprendizagem. Estabelecer a camada intermediária entre análise e síntese e o argumento de engenharia que a justifica. Apresentar a organização da memória do programa em execução e o que o compilador precisa emitir para sustentá-la. Produzir a especificação do formato de saída do compilador.

Competências a desenvolver. Capacidade de reconhecer, numa decisão de arquitetura, o ganho combinatório que justifica uma camada de indireção. Capacidade de redigir uma especificação técnica completa o bastante para permitir implementação independente — critério de qualidade documental que se aplica muito além desta disciplina.

Habilidades a adquirir. Justificar a existência de uma representação intermediária. Comparar as formas usuais quanto ao que facilitam. Traduzir expressões, condicionais e laços para código de três endereços, tratando rótulos e desvios. Descrever a organização da memória em execução e a composição de um registro de ativação. Explicar o protocolo de chamada e retorno e a divisão de responsabilidades entre chamador e chamado. Especificar formalmente um formato de programa objeto e um modelo de execução.

Estrutura das Aulas

Aulas 1 e 2 — Aula Teórica

Roteiro por blocos — cada bloco existe para resolver uma pendência que o anterior deixou explícita no quadro.

Bloco de abertura — o desvio que não sabe para onde ir

Não abra com definição. Escreva no quadro a condição v > 100 \;\wedge\; v < 500 e peça que a sala traduza, em duplas, para instruções de uma máquina, com uma exigência declarada: o segundo teste não pode ser executado se o primeiro já falhou. Dê tempo curto e circule.

A primeira instrução todo mundo escreve: compare e, se der falso, desvie. A pergunta seguinte é a armadilha, e ela é o módulo inteiro: desvie para onde? Para o ponto logo depois do comando, que ainda não foi traduzido, cujo tamanho ninguém conhece e cujo endereço, portanto, não existe. Recolha a resposta de dois ou três grupos no quadro e mostre que todas travam no mesmo lugar.

Nomeie o que acabou de acontecer, sem resolver: é preciso escrever agora um número que só se conhece daqui a algumas dezenas de instruções. Escreva no canto do quadro três palavras que ficarão as duas aulas inteiras — camada, desvio pendente, especificação — e diga que a segunda aula paga a segunda palavra. Deixar a dívida declarada segura a atenção melhor do que resolvê-la agora.

Bloco seguinte — a conta da camada, feita com honestidade

Apresente a camada intermediária pela justificativa canônica e escreva a tabela no quadro, linha a linha, começando por uma origem e um destino. Sem camada, m \times n tradutores; com camada, m + n peças. Uma origem e um destino: 1 \times 1 = 1 contra 1 + 1 = 2. Três por três: 9 contra 6. Dez por dez: 100 contra 20.

Pare na primeira linha e deixe o silêncio trabalhar. O ponto de virada é três por três; abaixo dele a camada custa mais peças do que economiza, e o caso de origem única e destino único é o caso de praticamente todo projeto da turma. Diga a frase inteira: o argumento que a literatura usa para justificar a camada, aplicado honestamente ao projeto de vocês, a condena.

Só depois disso entregue o argumento que sobra, que é o que interessa. A passagem da árvore ao código final resolve dois problemas de naturezas diferentes: a ordem de avaliação com desvios, que é fluxo de controle, e a codificação das instruções, que é formato. Resolvidos juntos, cada erro de um parece defeito do outro. Separados, cada um é tratável — e o primeiro pode ser testado antes de o segundo existir, porque a representação intermediária é uma fronteira observável, que se imprime e se confere à mão. Dê ainda o teste do apagamento, que eles vão usar em outras disciplinas: se eu apagar esta camada e ligar as duas pontas, o que fica mais difícil? Se a resposta honesta for “nada”, a camada é peso morto.

Primeira questão de discussão em duplas. “Um compilador traduz uma única linguagem para uma única máquina. Sobre a adoção de uma representação intermediária: (a) é injustificável, porque o argumento combinatório mostra que ela custa uma peça a mais; (b) é justificada pelo argumento combinatório, que vale sempre; (c) não é justificada pelo argumento combinatório neste caso, e é justificada pela separação entre fluxo de controle e codificação; (d) é indiferente, porque as duas organizações produzem o mesmo código final.”

Aplique o procedimento inteiro: voto individual sem comentário seu, discussão em duplas, segundo voto. A resposta é (c). Quem vota (b) está repetindo a frase do livro sem conferir os números — é o erro que a questão existe para expor, e é o mesmo erro que aparecerá na tutoria escrito na justificativa do grupo. Quem vota (a) fez a conta certa e parou cedo demais: acertou que o argumento combinatório não serve, e concluiu erradamente que nenhum serve. Quem vota (d) ignora que o produto final é igual mas o custo de chegar até ele não é. Não revele a resposta antes da discussão em duplas.

Bloco seguinte — três formas, a mesma expressão

Declare a regra de comparação antes de comparar, porque ela é conteúdo: as três formas serão avaliadas sobre a mesma expressão, e essa expressão é a mais difícil que a linguagem admite — a condição composta da abertura, não uma soma de dois números.

Sobre a árvore sintática abstrata, seja breve: é a mais informativa, preserva tipos e origem no texto, e a ordem de avaliação está implícita na estrutura. Registre o defeito que decide a questão: não existe, numa árvore de expressão, lugar natural para escrever “se este subteste falhar, pule aquele nó”.

Sobre a notação pós-fixada, faça o percurso no quadro. Desenhe a árvore de 2 + 3 \times 4 e produza, visitando em pós-ordem, a sequência 2 3 4 * +; enuncie que o percurso em pós-ordem de uma árvore de expressão é exatamente a notação pós-fixada dela, e que a leitura por uma pilha reconstrói a árvore. Elogie a forma com sinceridade — é a mais curta, dispensa parênteses, dá a ordem de avaliação de graça e não precisa de temporário nem de rótulo. Então derrube-a com uma frase: ela não exprime desvio. A sequência é linear, cada símbolo é consumido em ordem, não há como dizer “salte três símbolos adiante”. Sem desvio não há curto-circuito, e sem curto-circuito o conectivo avalia sempre os dois lados — o que não é ineficiência, é mudança de semântica.

Feche com o código de três endereços: no máximo um operador por instrução, dois operandos e um destino, temporários em número ilimitado, e — a diferença que decide tudo — desvios que carregam um destino. Diga por que os temporários são ilimitados e nenhuma máquina tem mil registradores: é divisão de trabalho, e quem decide quem vive em registrador é a alocação, assunto do próximo módulo.

A lição de método vale mais que a tabela comparativa e precisa ser dita: das três formas, a mais bonita perdeu. A escolha não foi feita por elegância, foi feita contra a construção mais difícil da linguagem — e quem testar contra a mais fácil escolhe errado e descobre com metade do gerador escrito.

Mencione quádruplas, triplas e triplas indiretas em ritmo rápido, com a recomendação prática: comece por quádruplas, porque o recurso escasso do projeto não é memória, é a atenção de vocês, e porque imprimir quádruplas produz texto legível — e vocês vão imprimir muitas vezes. Registre a forma de atribuição única estática como panorama, formalizada em 1991 por Ron Cytron, Jeanne Ferrante e colaboradores, e diga sem rodeios que ela existe para viabilizar análises de fluxo de dados; adotá-la num compilador que não as faz é pagar a construção e a destruição sem consumir o benefício.

Bloco seguinte — a tradução, e a contagem que verifica

A tradução de expressões cabe numa frase que você deve escrever no quadro: cada nó de expressão devolve ao pai o nome do temporário que contém o seu valor. Traduza a + b \times c ao vivo, emitindo as duas instruções com um temporário no meio, e mostre que não há tabela de precedência a consultar — a precedência já foi resolvida pela análise sintática e está gravada na forma da árvore.

Dê a verificação junto com o algoritmo, porque ela custa três linhas e paga o semestre: o número de temporários criados é igual ao número de nós de expressão visitados. Se der diferente, algum nó foi visitado duas vezes ou nenhuma, e as duas coisas são defeitos.

Passe aos comandos e derive as duas formas canônicas no quadro. No condicional, código da condição, desvio condicional para o início do ramo alternativo, código do primeiro ramo, desvio incondicional para depois do comando, código do segundo ramo: dois desvios, ambos para a frente. No laço, marque a posição atual, emita a condição, um desvio condicional para depois do laço, o corpo, e um desvio incondicional de volta à posição marcada: três desvios, dois para a frente e um para trás.

Segunda questão de discussão em duplas. “Na tradução para código de três endereços, qual caso exige mais maquinaria? (a) o desvio para trás do laço, porque laços são mais complicados; (b) o desvio para a frente do condicional, porque o destino ainda não existe quando o desvio é emitido; (c) os dois igualmente, porque todo desvio precisa de um endereço; (d) nenhum dos dois, porque os endereços são resolvidos por uma segunda passagem obrigatória.”

A resposta é (b), e ela é contraintuitiva: o desvio para trás aponta para instrução já emitida, cuja posição já é conhecida, e é o caso fácil. A maioria vota (a) no primeiro turno porque laços parecem mais complicados no texto do programa — e é exatamente essa confusão entre complexidade sintática e dificuldade de tradução que a questão desfaz. Quem vota (d) supõe uma segunda passagem que o próximo bloco vai mostrar ser dispensável.

Bloco de abertura da segunda aula — pagando a dívida do quadro

Retome pelas três palavras deixadas no canto e pergunte qual delas continua em aberto. É “desvio pendente”, e agora ela tem nome próprio: preenchimento retroativo. Emitir desvios com o campo de destino vazio, manter listas dos índices dessas instruções incompletas e preencher o campo assim que a posição se conhece. Três operações, e só três: criar uma lista com um índice, fundir duas listas, e percorrer uma lista escrevendo o mesmo destino em todas as instruções dela.

Antes do traçado, dê a regra de combinação, porque sem ela o traçado vira mágica. Uma condição não devolve valor: devolve duas listas de desvios pendentes, a dos que serão tomados se ela resultar verdadeira e a dos que serão tomados se resultar falsa. Na conjunção, a lista verdadeira do lado esquerdo recebe como destino o início do código do lado direito — é isso que significa “continue testando” —, a lista verdadeira do conjunto é a do lado direito e a lista falsa é a união das duas, porque falhar de qualquer lado faz a conjunção falhar. Na disjunção, o esquema é simétrico.

Bloco seguinte — o traçado numerado, feito no quadro

Este bloco é o coração da aula e precisa ser escrito posição por posição, com a turma copiando. Numere as instruções da tradução de “se C_1 e C_2 então S_1 senão S_2”:

    0: t0 := <avaliação de C1>
    1: se_falso t0 desvia para ?      <- entra na lista FALSA
    2: t1 := <avaliação de C2>
    3: se_falso t1 desvia para ?      <- entra na lista FALSA
    4: <código de S1>
    5: desvia para ?                  <- pendente: pula o senão
    6: <código de S2>
    7: (fim do comando)

Faça o preenchimento na frente deles, com os números ditos em voz alta. Quando a instrução da posição 6 vai ser emitida, o início do ramo alternativo passa a ser conhecido, e a lista falsa — que contém as posições 1 e 3 — é preenchida de uma vez com o valor 6. Chame a atenção para o fato de as duas instruções receberem o mesmo destino sem que nenhuma delas o conhecesse quando foi gerada. Ao terminar o comando, a posição 7 se conhece e o desvio da posição 5 recebe 7. Uma passagem só, nenhum retrocesso, nenhuma tabela de rótulos simbólicos a resolver depois.

Enuncie o invariante: ao término da tradução de um comando completo, nenhum desvio gerado dentro dele permanece pendente. E dê a contrapositiva como verificação executável, que é o que sobrevive à aula: conte os desvios com destino pendente ao final da tradução; o número tem de ser zero. Insista no motivo, porque ele é o que convence: um desvio para lugar nenhum não causa erro de compilação, causa comportamento errado na execução, muitas semanas depois, quando ninguém lembra mais desta parte do código.

Bloco seguinte — a especificação antes do código

Anuncie a inversão de ordem de trabalho que o módulo exige e que os grupos vão querer inverter de volta: a especificação do formato de saída se escreve antes do código que a produz. Dê a razão precisa, não a genérica: documentação escrita a partir do código pronto registra as decisões acidentais junto com as deliberadas e não tem como distinguir umas das outras — quem lê não sabe o que pode assumir. Pior: quem escreve não descobre as decisões que não tomou.

Enuncie o critério de completude e escreva-o no quadro, porque ele será o instrumento de correção: a especificação está completa quando alguém que leia apenas ela consegue escrever um executor compatível. E diga o que esse teste costuma revelar, porque é sempre o mesmo tipo de coisa e nunca é a lista de campos: é o comportamento nos empates e nas bordas. O que acontece quando dois reconhecedores casam trechos do mesmo comprimento na mesma posição; o que acontece quando a entrada acaba no meio de um casamento; o que acontece quando uma instrução recebe índice fora dos limites da área de constantes. A regra de desempate mais comum é a ordem de declaração, e o que importa não é qual foi escolhida, e sim que esteja escrita — sem ela, dois executores corretos produzem saídas diferentes para o mesmo programa e nenhum dos dois está errado.

Trate ainda a escolha entre máquina de pilha e máquina de registradores, e conduza-a por uma propriedade da linguagem, não da máquina: a profundidade das expressões. Faça a conta no quadro com a recorrência d(\text{folha}) = 1 e d(T) = \max(d(T_e),\, 1 + d(T_d)). Para ((a+b)+c)+d, cada nó tem folha à direita e o resultado é profundidade 2, qualquer que seja o comprimento da cadeia. Para a+(b+(c+d)), a profundidade vale 4. Duas expressões matematicamente equivalentes, custos de pilha diferentes, e a diferença está só na forma da árvore — primeira aparição do problema de ordenação que o próximo módulo trata. Termine com o corolário de engenharia que quase ninguém usa: como d(T) é calculável na compilação, o compilador sabe de quanta pilha o programa precisa e pode gravar esse número no objeto.

Encerre o bloco com a decisão de representação que costuma passar despercebida: a mesma estrutura pode ter formas diferentes dentro do compilador e dentro do objeto. Uma tabela de transição que é construída, transformada e minimizada dentro do compilador pede representação esparsa; a mesma tabela, dentro do objeto, é apenas consultada uma vez por símbolo da entrada, no laço mais interno, e pede representação densa. Avise que a defesa da forma densa tem de vir com o número medido de ocupação ao lado, e que ela tem prazo de validade.

Bloco seguinte — a memória enquanto o programa roda

Divida a memória nas quatro áreas — código, estática, pilha e monte —, e imediatamente proponha a leitura que orienta decisões: o corte útil não é o nominal, é o que separa o que o compilador sabe do que só a execução descobre. Mostre que a fronteira é menos nítida do que parece usando o teorema anterior: a profundidade máxima da pilha de avaliação é calculável na compilação, embora o conteúdo dela não seja.

Passe ao registro de ativação e enuncie a frase que dá sentido a todo o resto: cada ativação tem o seu registro, e é isso que torna a recursão possível — se cada procedimento tivesse um bloco fixo, a segunda chamada sobrescreveria os valores da primeira. Generalize na sequência, porque é o que impede o assunto de ficar abstrato em projetos pequenos: ativação não é sinônimo de chamada de procedimento; qualquer construção com ponto de entrada, valores ligados na entrada, espaço de trabalho próprio e retorno é uma ativação, ainda que a linguagem não tenha procedimentos declaráveis.

Chegue às duas cadeias com o exemplo mínimo, que precisa de três níveis de aninhamento — com dois níveis elas nunca divergem, e é por isso que tanta gente supõe que são a mesma coisa. Um principal contém um procedimento externo, que contém um interno. O principal chama o externo, o externo chama o interno, o interno chama o externo de novo: quatro registros na pilha. Desenhe os quatro e aponte o quarto. O elo de controle dele aponta para o terceiro, porque foi o interno quem chamou, e a cadeia de controle segue a ordem de execução. O elo de acesso aponta para o primeiro, porque quem contém o externo no texto do programa é o principal, e a cadeia de acesso segue a ordem léxica.

Feche com o teorema que transforma o mecanismo em aritmética: um procedimento de nível léxico n_p que acessa variável declarada em nível n_v alcança o registro certo seguindo exatamente n_p - n_v elos de acesso. Como essa diferença é conhecida na compilação, o acesso a variável não local não custa busca alguma em execução — custa um número fixo de indireções, gerado como constante. Registre o display, do ALGOL 60, como a alternativa que reduz esse custo a uma indireção ao preço de manter um vetor, e o erro clássico de implementação: montar o elo de acesso apontando para o registro do envolvente mais antigo em vez do mais recente, defeito que compila, executa e lê silenciosamente a variável da ativação errada.

Terceira questão de discussão em duplas. “Naquela pilha de quatro registros, estando no topo — a segunda ativação do procedimento externo —, uma variável declarada dentro do procedimento interno: (a) é visível, porque o registro do interno está na pilha, logo abaixo; (b) é visível, porque o interno chamou o externo e passou o controle; (c) não é visível, porque o interno não contém o externo no texto do programa; (d) não é visível, porque o registro do interno já foi desempilhado.”

A resposta é (c). Quem vota (a) ou (b) confunde “estar na pilha” com “ser visível” — que é exatamente o erro que a cadeia de acesso existe para impedir, e por isso essa é a questão mais produtiva do módulo. Quem vota (d) chega ao veredito certo pelo motivo errado: o registro do interno não foi desempilhado, ele continua fisicamente lá. Se essa alternativa receber muitos votos, refaça o desenho da pilha antes de seguir.

Passe a gerência de memória em ritmo de panorama, com um critério de decisão no fim. Alocação explícita, com os dois modos simétricos de errar — devolver cedo demais e nunca devolver — e a fragmentação externa e interna. A alocação por região como o esquema mais simples que funciona, válido exatamente quando nada sobrevive à região. E a coleta automática construída sobre a noção de alcançabilidade: contagem de referências, com o defeito insanável dos ciclos; marcação e varredura, que apareceu na primeira implementação de LISP, por John McCarthy, no início dos anos 1960, e trata ciclos corretamente ao preço de uma pausa; coletores por cópia, cujo custo é proporcional ao que sobrevive; e coletores geracionais.

Bloco de fechamento — volta ao quadro da abertura

Volte à condição da abertura e complete a tradução dela no quadro, agora com os destinos preenchidos pelo mecanismo da segunda aula. A dívida da primeira aula está paga na frente deles, e é isso que fecha o módulo.

Termine com a advertência que este módulo tem e nenhum outro tem tão forte: o produto principal aqui é um documento, e documentos não dão erro de compilação. É perfeitamente possível escrever uma especificação bonita, incompleta e que passa despercebida por semanas. Diga que o critério de completude será aplicado literalmente na correção, e que o principal defeito que ele pega não é campo ausente — é comportamento não decidido.

Aulas 3 a 6 — Tutoria do Projeto Integrador

Quatro aulas de tutoria — justificativa da camada com os números do grupo, especificação do objeto e do modelo de execução, tradução implementada e a sessão de troca de especificações.

O andaime aqui já é curto: a esta altura do semestre você fornece o resultado esperado e o critério de aceitação, não o caminho. Diga isso na abertura da primeira sessão, e diga também o que muda neste módulo em relação a todos os anteriores — o entregável principal é um documento, e a tutoria vai parecer menos “produtiva” porque haverá mais gente escrevendo do que compilando. Grupos que interpretam isso como folga chegam ao módulo seguinte sem contrato para o gerador.

Tenha a sua própria referência aberta e projetada nas duas sessões. Neste módulo em particular, o que convence não é o código: é a tabela de verificação da entrega, com as treze linhas conferidas, e a lista de decisões que só apareceram porque a especificação veio antes.

Primeira sessão de tutoria — a justificativa e a especificação

Comece pela justificativa da camada, e comece exigindo a conta. Cada grupo escreve, na frente de você, quantas linguagens de origem e quantas máquinas de destino o seu compilador tem. A resposta é sempre uma e uma, e a conta sempre dá um contra dois. Peça então que escrevam a conclusão honesta antes de escrever a justificativa: o argumento combinatório não sustenta a camada neste projeto.

Projete a sua própria demonstração da tabela, com as cinco linhas impressas e o ponto de virada em três por três, e leia em voz alta a frase que você escreveu na sua referência: escrevi a tabela inteira, e não só a linha de cinco por cinco que a literatura costuma citar, porque a primeira linha é a nossa. Depois disso, cobre a justificativa real — a separação de dificuldades — formulada com as características da linguagem do grupo, e aceite, quando for o caso, a resposta de que o grupo não precisa da camada, desde que sustentada.

A segunda metade da sessão é a especificação, e aqui você precisa cortar uma inversão antes que ela aconteça. Circule perguntando a cada grupo o que ele está escrevendo neste momento; quem estiver escrevendo código, pare. A regra do módulo é a especificação primeiro, e o argumento que convence não é de autoridade — conte a sua própria experiência: duas decisões da sua referência só existem porque o documento veio antes. A troca de representação da tabela de transição na fronteira entre compilador e objeto, esparsa dentro, densa fora, que você teria levado adiante por inércia; e o desempate entre padrões de mesmo comprimento, que estava ausente de tudo que você tinha escrito e que muda a saída do programa.

Conte também a divergência, porque ela é o melhor exemplo de conduta técnica do semestre. O design do artefato listava sete instruções, entre elas conjunção e disjunção, o que significa avaliação ansiosa; o módulo seguinte exige curto-circuito, que não é exprimível sem desvio. Você resolveu acrescentando as duas instruções de desvio e expandindo a família de comparação, chegando a quinze códigos de operação, e registrando a decisão no próprio documento. O que não fez foi mudar o design em silêncio nem torcer o conjunto para caber na lista original. Divergência entre documentos acontece; o que não pode é ser resolvida sem ficar escrita. Peça que cada grupo procure a sua própria divergência — quase todos têm uma entre o que escreveram no módulo 8 e o que descobriram no módulo 12.

Segunda sessão de tutoria — troca de especificações e tradução implementada

Abra com a atividade que é o instrumento de avaliação mais eficaz deste módulo: a troca de especificações entre grupos. Cada grupo entrega o documento a outro, sem explicação verbal — proíba explicitamente a explicação oral, porque é ela que mascara a lacuna —, e o grupo receptor tem uma tarefa concreta: descrever, por escrito, o que o executor faz com uma entrada dada, e listar tudo que precisaria perguntar para conseguir implementar. A lista de perguntas é o produto da atividade, e é ela que volta ao autor.

Circule ouvindo, porque o padrão das perguntas é sempre o mesmo e vale coletar: quase nunca falta um campo, quase sempre falta um comportamento. O que acontece no empate, o que acontece quando a entrada acaba no meio, o que acontece quando um índice sai dos limites. Quando um grupo disser “isso é óbvio”, registre como lacuna na hora — óbvio para o autor é indefinido para o implementador.

A segunda metade é a tradução, e aqui a exigência é de demonstração. Peça o código intermediário impresso para um programa de exemplo, e faça as duas verificações na frente do grupo. A contagem de temporários tem de bater com o número de nós de expressão da condição traduzida; e o contador de desvios pendentes ao fim de cada comando tem de ser zero. Projete a sua referência mostrando as duas ações traduzidas com nove temporários e zero desvios pendentes ao fim, para que eles vejam a forma que a evidência tem.

Aproveite para mostrar a decisão de tradução que não é óbvia e que a sua referência tomou: a extração de valor virou duas instruções, e não uma — primeiro o casamento é buscado, depois convertido. Poderia ser uma só, e seria mais curta; separar preserva na representação intermediária a distinção que a análise semântica do módulo 12 lutou para tornar explícita. É o tipo de decisão que os grupos colapsam sem perceber, e que só se percebe quando alguém pergunta por quê.

Se sobrar sessão, use-a no mapa de memória. Projete a sua tabela de áreas, com a estática medida em 5137 bytes, a área de código em zero porque o gerador ainda não existe, e a pilha e o monte marcados como crescendo — e conte a correção que essa linha custou: na primeira versão, tamanho zero e tamanho dinâmico apareciam iguais, e a área mais estática de todas parecia crescer em execução. Mostre o registro de ativação com os seus cinco campos e a profundidade máxima igual a um, e a demonstração das duas cadeias com os quatro quadros, apontando o quadro 3, onde o elo de controle vai para o quadro 2 e o de acesso vai para o quadro 0.

E use o número desconfortável, porque ele ensina mais que o conceito: a tabela de transição gravada no objeto tem 1280 células, 52 preenchidas, quatro por cento de ocupação, 5120 bytes por padrão. A escolha da matriz densa continua certa pela razão escrita — a consulta acontece uma vez por byte da entrada, no laço mais interno —, mas agora ela tem um número medido ao lado, e um prazo de validade. Exija de cada grupo um número medido ao lado de cada decisão de representação. Preferência com justificativa genérica não é decisão de engenharia, e a diferença aparece no dia em que alguém pergunta por quê.

Fechamento e registro

Feche o módulo com parecer explícito sobre cada especificação: aprovada, aprovada com pendências nomeadas, ou recusada. Recusar aqui custa uma sessão; aprovar um documento incompleto custa o módulo seguinte inteiro, porque o gerador será escrito contra um contrato que não decidiu o que fazer nos casos de borda.

Registre no seu diário, por grupo, três coisas: quantas perguntas o grupo receptor produziu sobre a especificação dele, se a justificativa da camada saiu das características da linguagem ou foi copiada, e se o grupo inverteu a ordem e implementou antes de especificar. A primeira anotação é a mais preditiva de todas — o grupo cuja especificação gerou muitas perguntas é o grupo que vai travar na integração do módulo 15.

Entregáveis e Avaliação

Ao fim do módulo, cada grupo entrega três coisas: a especificação do formato do programa objeto e do modelo de execução, completa o bastante para permitir implementação independente; o documento que justifica a representação intermediária escolhida à luz das alternativas e das características da própria linguagem; e a tradução para essa representação implementada e demonstrada sobre programas de exemplo.

Corrija na ordem inversa da que a turma espera, começando pela especificação, e corrija-a como executor: leia procurando pelo que falta, não pelo que está. Confira, um a um: existe regra de desempate escrita para o caso de dois reconhecedores casarem trechos de mesmo comprimento; existe comportamento definido para entrada que acaba no meio de um casamento; cada campo vem com semântica declarada e não apenas com nome; o modelo de execução diz o que o laço principal lê, em que ordem avança e qual é o efeito de cada instrução sobre a pilha; as condições de erro estão listadas, inclusive as que a análise prova não poderem ocorrer.

Na justificativa da representação, o critério é um só e é implacável: o argumento precisa sair das características da linguagem do grupo. Justificativa combinatória copiada para um compilador de origem única e destino único é reprovação do item — e vale registrar que “este projeto não precisa da camada, e aqui está por quê” é uma resposta defensável, desde que sustentada.

Na tradução implementada, exija a demonstração, não a afirmação: o código intermediário impresso para ao menos um programa de exemplo, a contagem de temporários batendo com o número de nós de expressão, e a verificação de zero desvios pendentes ao fim da tradução de cada comando. Registre no componente contínuo do módulo a pontualidade da entrega, a contribuição nas discussões em duplas das aulas teóricas e o engajamento nas atividades colaborativas da tutoria, com atenção à sessão de troca de especificações, que é observável e revela quem leu.

Orientações Sobre o Aplicativo

Use o aplicativo da disciplina para as três votações das aulas teóricas, com a projeção anônima. A leitura mais útil deste módulo está na primeira questão: uma turma que vota majoritariamente na alternativa do argumento combinatório universal está indicando, antes da tutoria, que vai copiar a justificativa do livro para a própria entrega. Quando isso acontecer, não corrija apenas a resposta — leve o resultado projetado para a primeira sessão de tutoria e comece por ele.

A terceira questão merece atenção diferente. Se a alternativa sobre o registro já desempilhado receber votos consideráveis, o problema não é a cadeia de acesso, é o desenho da pilha; refaça o desenho antes de qualquer discussão em duplas, porque a instrução por pares só corrige quando há quem tenha entendido.

Acompanhe também o engajamento no estudo do material, que responde por metade do componente contínuo. Neste módulo o número costuma cair porque parte da turma lê conteúdo de decisão como “menos técnico” e adia — mau sinal com consequência datada: quem não estuda aqui não tem o que emitir no próximo módulo.

Pontos de Atenção Específicos

O argumento combinatório vira slogan se você não fizer a conta na frente deles. É o risco número um do módulo, e ele não se resolve com advertência: resolve-se escrevendo a linha de uma origem e um destino no quadro e deixando a turma ver o argumento se voltar contra si mesmo. Se você citar apenas a linha de cinco por cinco, terá ensinado a repetir uma frase.

A pós-fixada é sedutora e alguém vai defendê-la até o fim. Sempre aparece o estudante que insiste que dá para fazer curto-circuito na pilha com um truque. Não descarte — peça que ele escreva o truque no quadro. Ele vai precisar de uma forma de pular símbolos, e nesse momento acabou de reinventar o desvio, o que prova o argumento melhor do que qualquer explicação sua.

Não deixe o traçado de preenchimento retroativo virar slide. Ele só ensina escrito posição por posição, com a turma copiando e com você dizendo os números em voz alta. Se o tempo apertar, corte a discussão de quádruplas contra triplas, não o traçado.

Ambientes de execução tendem a ficar abstratos em projetos pequenos. A saída não é omitir nem inventar procedimentos na linguagem só para exercitar o mecanismo — as duas coisas são erros de tipos opostos. É mostrar a ativação onde ela já está, sob outro nome, e manter um exemplo mínimo isolado quando a linguagem realmente não exercita o mecanismo, dizendo com clareza que ele é ilustrativo e não parte do sistema.

Alguém vai perguntar sobre coleta automática em profundidade. A pergunta é legítima e o assunto é panorâmico aqui. Responda pela alcançabilidade, dê o defeito dos ciclos na contagem de referências, e reconduza: o critério de projeto que interessa a eles é se algo sobrevive à construção que o criou; se nada sobrevive, a alocação por região resolve e recorrer a mais que isso é gastar complexidade sem comprar nada.

A inversão da ordem de trabalho é a exigência que mais resistência gera. Grupos querem implementar e documentar depois, porque é o que fizeram a vida inteira. Exija a especificação aprovada antes do código e não negocie, porque o argumento a favor só é convincente depois de vivido: são as decisões que aparecem ao escrever — o desempate, a troca de representação na fronteira — que provam o ponto, e elas não aparecem para quem inverte a ordem.