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

Módulo 12: Plano de Aula — Análise Semântica

Documento exclusivo do professor. Este é o guia operacional das seis aulas do módulo 12: 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 e o programa defeituoso da abertura perdem função assim que o estudante os lê antes da aula.

Visão Geral do Módulo

Onde este módulo fica — é a última fase da análise, e a que fecha a conta aberta no primeiro módulo.

flowchart LR
    M10["Módulo 10<br/>Análise sintática<br/>descendente<br/>árvore pronta"]
    M11["Módulo 11<br/>Análise sintática<br/>ascendente<br/>conceitual"]
    M12["Módulo 12<br/>Análise semântica<br/>ÚLTIMA FASE<br/>DA ANÁLISE"]
    M13["Módulo 13<br/>Representações<br/>intermediárias e<br/>ambientes de execução"]

    M10 --> M11 --> M12 --> M13

    subgraph ENTREGA["O que o módulo 12 deixa pronto"]
        D1["Tabela de símbolos<br/>com escopos e as duas<br/>estratégias comparadas"]
        D2["Árvore anotada<br/>com tipos"]
        D3["Diagnósticos semânticos<br/>com nome, posição<br/>e natureza"]
        D4["Decisão registrada<br/>sobre número e ordem<br/>dos percursos"]
    end

    M12 --- ENTREGA
    D1 -.->|"ambiente das regras de tipo"| D2
    D2 -.->|"entrada da síntese"| M13
    M06["Módulo 6<br/>operações booleanas<br/>sobre autômatos"] -.->|"decidem inclusão de linguagens"| M12
    M09["Módulo 9<br/>limites das linguagens<br/>livres de contexto"] -.->|"por que esta fase existe"| M12
Figura 1: O módulo 12 fecha a análise e entrega à síntese a árvore anotada.

Este módulo tem uma vantagem que nenhum outro do semestre tem: a turma chega a ele com todas as peças do argumento já na mão. O resultado de impossibilidade que justifica a fase foi visto no módulo 9, a árvore sobre a qual ela opera foi construída no módulo 10, e as operações booleanas sobre autômatos que decidem uma pergunta de tipos foram implementadas no módulo 6, por razão inteiramente diferente. Sua tarefa é menos apresentar conteúdo novo e mais fazer a turma reencontrar o que já sabe em um lugar onde não esperava.

Isso muda a condução. Um módulo assim se conduz por convergência: cada bloco fecha uma pendência anunciada antes, e o pior desfecho é a aula virar exposição de estruturas de dados — tabela, escopos, atributos — como se fossem uma lista de componentes a implementar. O que fica não é a estrutura; é o critério que a escolhe.

Há uma assimetria a respeitar no peso dos tópicos. Tabela de símbolos e verificação de tipos são tratamento instrumental: os grupos implementam os dois nesta mesma semana. Gramáticas de atributos são tratamento conceitual aprofundado: quase nenhum grupo vai construir um grafo de dependências explícito, e mesmo assim o tópico não pode ser abreviado, porque é ele que explica por que a recursão que todos vão escrever funciona — e em que caso ela deixaria de funcionar.

O andaime já é baixo aqui: você dá o resultado esperado e a tutoria opera como revisão crítica. Não devolva ao grupo, neste módulo, a decomposição em percursos pronta.

Objetivos, Competências e Habilidades

Objetivos de aprendizagem. Estabelecer o que a gramática não captura e por quê. Introduzir a tabela de símbolos e a tradução dirigida por sintaxe como o arcabouço da fase. Desenvolver a verificação de tipos como aplicação concreta.

Competências a desenvolver. Capacidade de identificar os limites de um formalismo e de escolher o mecanismo adequado ao que está fora deles. Capacidade de projetar estruturas de dados a partir do padrão de uso previsto, e não da estrutura conceitual do problema.

Habilidades a adquirir. Distinguir correção sintática de correção semântica com exemplos. Projetar e implementar uma tabela de símbolos com tratamento de escopos aninhados. Especificar traduções com atributos sintetizados e herdados, e determinar a ordem de avaliação a partir das dependências. Implementar verificações de declaração e de compatibilidade de tipos. Produzir diagnósticos semânticos informativos e prosseguir após o primeiro erro.

Estrutura das Aulas

Aulas 1 e 2 — Aula Teórica

Roteiro por blocos — cada bloco resolve um dos defeitos que a abertura deixou sem dono.

Bloco de abertura — zero erros, quatro absurdos

Chegue com um programa curto já projetado, na linguagem de exemplo que você vier usando, e com o relatório do analisador sintático ao lado dizendo zero erros. O programa precisa conter quatro defeitos de naturezas diferentes: um nome usado sem nunca ter sido declarado; um nome declarado duas vezes no mesmo escopo; uma operação numérica aplicada a um valor que não é numérico; e uma declaração que ninguém usa.

Não explique nada. Pergunte apenas: quem deveria ter reclamado? A resposta que quase sempre aparece primeiro é “o analisador sintático está com defeito”, e ela é o gancho — responda que o analisador está perfeito, que ele fez exatamente o que lhe cabia, e escreva no canto do quadro as quatro palavras declaração, redeclaração, tipo e uso. Não apague durante as duas aulas; você volta a elas no fechamento, uma a uma.

Feche com a promessa que segura a atenção: ao fim da segunda aula cada uma das quatro palavras terá um mecanismo com nome, e três delas virarão mensagem de erro escrita por eles na tutoria.

Bloco seguinte — a demonstração de que a gramática não alcança

O erro de condução mais comum aqui é ficar na versão intuitiva — “a gramática não lembra os nomes” — e seguir adiante. Ela é vaga e a turma sente. Faça o argumento formal, que é curto e eles têm ferramenta para acompanhar desde o módulo 9.

Escreva no quadro, sobre o alfabeto \{a, b, c\}, a linguagem

L_{du} = \{\, wcw \mid w \in \{a,b\}^+ \,\},

e diga em voz alta a leitura: à esquerda do c está o nome na declaração, à direita o mesmo nome no uso, e o c é tudo o que existe entre os dois. Então mostre que ela não é livre de contexto pelo lema do bombeamento, com número concreto no quadro. Tome p = 3 e escreva z = aaabbb\,c\,aaabbb, de comprimento 13. O lema garante uma decomposição z = uvxyz' com |vxy| \le 3; uma janela de três símbolos consecutivos nessa cadeia não toca ao mesmo tempo uma posição da metade esquerda e a posição correspondente da direita, que estão a sete símbolos de distância. Logo, bombear altera uma metade sem alterar a outra, e a cadeia sai da linguagem.

Imediatamente depois, faça o contraste que fixa o conceito: se a segunda metade fosse o reverso da primeira, a linguagem seria livre de contexto e a gramática sairia em duas produções. Escreva as duas no quadro — S \to a\,S\,a \mid b\,S\,b \mid c — e pergunte por que o reverso cabe na pilha e a cópia não. A resposta que você quer ouvir é que a pilha reconhece correspondência aninhada, e declaração e uso é correspondência paralela: o primeiro nome declarado não é o último a ser usado.

Feche antecipando a objeção do estudante que leu adiante: e o andar de cima da hierarquia? Três razões em sequência rápida — legibilidade, porque simular a lista de nomes declarados custa dezenas de produções de contabilidade; custo, porque a pertinência é decidível mas exponencial no caso geral; e a decisiva, que costuma faltar no repertório da turma: a gramática responderia sim ou não, e o que o compilador precisa é da árvore anotada com tipos, da tabela preenchida e de uma mensagem que nomeie o identificador ofensor.

Primeira questão de discussão em duplas. “A verificação de que todo nome usado foi declarado não cabe na análise sintática porque: (a) a gramática ficaria grande demais para o gerador processar; (b) a linguagem dos programas com declaração e uso correspondentes não é livre de contexto; (c) o analisador sintático não tem acesso ao texto do programa; (d) a verificação depende do valor das variáveis em execução.”

Aplique o procedimento inteiro: voto individual sem comentário seu, discussão em duplas, segundo voto. A resposta é (b). Quem vota (a) trocou impossibilidade por inconveniência, e é o erro mais produtivo da questão — vale nomeá-lo em voz alta, porque é a diferença entre “é caro” e “não existe”. Quem vota (d) confundiu a fronteira de cima, entre estático e dinâmico, com a de baixo, entre forma e sentido. Não revele a resposta antes da discussão em duplas.

Bloco seguinte — a tabela de símbolos pelo consumidor

Conduza este bloco de trás para frente, e diga que está fazendo isso. A pergunta não é “o que uma tabela de símbolos guarda”; é “que verificação precisa deste campo”. Monte a tabela no quadro com a sala, um campo por vez, e só escreva o campo depois que alguém disser quem o consome.

O nome é a chave. A espécie distingue categorias que compartilham o espaço de nomes, e sem ela um programa que usa um nome de tipo como se fosse valor passa batido, porque a consulta encontra a entrada e o verificador não percebe que encontrou a coisa errada. O tipo é o que a verificação de tipos consome. A posição da declaração existe por diagnóstico: dizer “já declarado” sem dizer onde estava a primeira obriga quem lê a sair procurando. E o marcador de uso só pode ser lido depois que a árvore inteira foi percorrida — anote isso no quadro, porque é a primeira evidência de que um percurso só não basta.

Então dê a lição que é literalmente uma das competências do módulo: a estrutura de dados sai do padrão de operações previsto, não da estrutura conceitual do problema. Conceitualmente a tabela é uma árvore, porque escopos aninham, e quem deixa o conceito escolher obtém algo elegante e lento. O padrão real é dominado pela consulta por nome, uma por ocorrência de identificador, contra um punhado de inserções — o que aponta para a tabela de dispersão, com a árvore balanceada reservada ao caso em que a listagem ordenada importa.

Termine enunciando a interface mínima de quatro operações e insista no par que a turma acha redundante: consulta visível, do escopo mais interno para o mais externo, e consulta local, restrita ao escopo corrente. A primeira responde “o que este nome significa aqui?”; a segunda, “este nome já foi declarado neste escopo?”. Diga a consequência de fundi-las: quem usa a visível para detectar redeclaração obtém um compilador que recusa programas válidos.

Bloco seguinte — escopos, e a conta que decide entre as duas estratégias

Comece separando os dois casos que os grupos confundem: declarar duas vezes o mesmo nome no mesmo escopo é erro; declarar num escopo interno um nome que existe no externo é sombreamento, legítimo, e no máximo rende aviso de estilo.

Apresente as duas organizações clássicas e não as apresente como preferência. A empilhada mantém uma tabela por escopo: entrar empilha, sair desempilha e descarta, e a consulta caminha do topo para a base parando no primeiro acerto — a regra do bloco mais interno é a ordem do percurso, sem código nenhum para sombreamento. A encadeada mantém uma tabela única do nome para a pilha de declarações ativas daquele nome: a consulta é uma sondagem seguida da leitura do topo, e sair de escopo exige desempilhar cada nome declarado ali, o que obriga a manter uma trilha de desfazimento por nível.

Aqui entra o exemplo numérico do bloco, e ele tem de ser feito no quadro, não afirmado. Suponha 10 consultas ao nome mais externo — o pior caso da empilhada — em profundidade d. Cada consulta sonda d escopos sem achar e acerta no último, custando d + 1 sondagens, de modo que as dez custam 10(d+1). Preencha a tabela na frente deles:

Profundidade d Sondagens na empilhada Sondagens na encadeada
1 20 10
2 30 10
4 50 10
8 90 10

Leia a tabela do jeito certo, que não é “a encadeada ganha”. Sendo d a profundidade e m o número de declarações do escopo que se fecha, a empilhada consulta em O(d) e sai em O(1); a encadeada consulta em O(1) médio e sai em O(m). O custo mudou de lugar, não desapareceu. E dê o critério que os grupos precisam ouvir antes da tutoria: com dois níveis de escopo, a primeira linha da tabela é o mundo inteiro e a estratégia mais simples de destruir corretamente vence; a conclusão se inverte com blocos profundamente aninhados.

Segunda questão de discussão em duplas. “Um programa declara um nome no escopo externo e declara o mesmo nome dentro de um bloco interno. Um compilador que usa a consulta visível para decidir se há redeclaração vai: (a) aceitar o programa, corretamente; (b) rejeitar o programa, corretamente; (c) rejeitar o programa, incorretamente; (d) aceitar o programa, incorretamente.”

A resposta é (c). Quem vota (b) tratou sombreamento como redeclaração, que é exatamente o defeito da questão. Quem vota (a) acertou o veredito sobre o programa e errou sobre o compilador descrito — vale mostrar isso no quadro, porque distingue “o programa é válido” de “este compilador o aceita”. Esta questão costuma ser a de maior migração entre o primeiro e o segundo voto no módulo; guarde os dois histogramas.

Bloco de abertura da segunda aula — retomada pela dívida

Não faça resumo. Aponte para as quatro palavras no canto do quadro e pergunte quais já têm mecanismo: declaração e redeclaração têm, com a tabela e as duas consultas; tipo e uso não. Anuncie então a inversão: “para verificar tipos eu preciso, antes, de um arcabouço que diga em que ordem a informação pode ser calculada”.

Bloco seguinte — tradução dirigida por sintaxe, com o grafo desenhado

Introduza os dois fluxos com um exemplo de cada, e não com a definição. O tipo de uma expressão nasce nas folhas e sobe: é sintetizado, e sozinho pediria pós-ordem. O escopo visível num ponto nasce onde o bloco foi aberto e desce: é herdado, e sozinho pediria pré-ordem. Credite a formalização a Donald Knuth, no trabalho de 1968 sobre a semântica de linguagens livres de contexto, e diga qual foi a contribuição — não foi pendurar informação nos nós, que já se fazia; foi dar à especificação forma declarativa, em que se escrevem as equações e não a ordem, e mostrar que a ordem sai delas.

Este é o bloco em que o desenho no quadro decide o aprendizado, e ele precisa estar preparado. Tome a expressão x + 3 > y dentro de um bloco que declara x e y, e desenhe a árvore: a raiz > com filhos + e y, e o + com filhos x e 3. São cinco nós; com dois atributos por nó, escopo e tipo, o grafo tem 5 \times 2 = 10 ocorrências de atributo.

Trace as arestas na frente deles, em três levas e com cores ou traços diferentes. O escopo desce: da raiz para +, da raiz para y, do + para x e do + para 3, quatro arestas. O tipo sobe: de x e de 3 para o +, e do + e de y para o >, mais quatro. E então as duas que fazem o bloco valer a pena: do escopo de x para o tipo de x, e do escopo de y para o tipo de y, porque descobrir o tipo de um identificador exige saber a que declaração ele se refere. Total de dez arestas.

Peça agora uma ordem de avaliação. Deixe a turma tentar pós-ordem e falhar: o tipo de x não pode ser calculado antes de o escopo ter descido até ele. Deixe tentar pré-ordem e falhar também: o tipo do + não existe antes dos filhos. Só então escreva uma ordem que funciona — escopo da raiz, escopo do +, escopo de y, escopo de x, escopo do 3, tipo de x, tipo do 3, tipo do +, tipo de y, tipo do > — e nomeie o que acabaram de fazer: uma ordem topológica do grafo de dependências.

O enunciado que precisa ficar escrito no quadro. Existe atribuição consistente de valores aos atributos se e somente se o grafo de dependências é acíclico, e nesse caso qualquer ordem topológica serve. Um ciclo não é erro do programa que está sendo compilado: é erro da especificação da tradução, e o programa de entrada só teve o azar de produzir a árvore em que a inconsistência se materializa.

Acrescente dois fatos e pare por aí, sem demonstração. Knuth mostrou que decidir a não circularidade é possível sem examinar árvore alguma; Jazayeri, Ogden e Rounds estabeleceram, em 1975, que o problema é intrinsecamente exponencial. É por isso que as ferramentas restringem a classe. Uma gramática é S-atribuída quando todos os atributos são sintetizados, e então a pós-ordem já é ordem topológica; é L-atribuída quando cada herdado de um filho depende apenas de herdados do pai e de atributos dos irmãos à esquerda, e então basta um percurso em profundidade da esquerda para a direita — inclusive durante a análise descendente, sem construir a árvore. Classifique o exemplo do quadro com a turma: não é S-atribuída, porque tem herdados; é L-atribuída, porque nenhum herdado depende de irmão à direita. E diga o que a classificação compra: saber que a especificação é L-atribuída é saber que existe a opção de traduzir durante a análise — opção que a maioria dos compiladores não exerce, porque as fases seguintes precisam da árvore de qualquer modo.

Terceira questão de discussão em duplas. “Numa árvore com atributos sintetizados e herdados que se alimentam mutuamente, a ordem de avaliação correta é: (a) sempre pós-ordem; (b) sempre pré-ordem; (c) pré-ordem para os herdados seguida de pós-ordem para os sintetizados; (d) uma ordem topológica do grafo de dependências entre ocorrências de atributo.”

A resposta é (d). O distrator que importa é (c), o mais votado no segundo turno, porque parece resolver as duas exigências fazendo uma de cada vez. Refute com o próprio grafo do quadro: as duas travessias separadas funcionam naquele exemplo por acaso e deixam de funcionar assim que um herdado depender de um sintetizado de um irmão à esquerda. Quem chegar sozinho a esse contraexemplo na discussão em duplas encontrou o que separa L-atribuída de gramática de atributos geral; nomeie o aluno e siga.

Bloco seguinte — verificação de tipos, e o tipo de erro que absorve

Escreva a forma do julgamento, \Gamma \vdash e : \tau, e leia em voz alta: “no ambiente \Gamma, a expressão e tem tipo \tau”. Então pare e faça a pergunta que amarra o bloco anterior a este: quem é \Gamma? É a tabela de símbolos, na sua forma matemática. Toda decisão de tipo é relativa às declarações visíveis, e é exatamente por isso que o atributo de escopo tinha de ser herdado.

Escreva três regras no quadro, com as premissas acima da barra:

\frac{\Gamma(x) = \tau}{\Gamma \vdash x : \tau} \qquad \frac{\Gamma \vdash e_1 : \text{num} \qquad \Gamma \vdash e_2 : \text{num}}{\Gamma \vdash e_1 + e_2 : \text{num}} \qquad \frac{\Gamma \vdash e_1 : \tau \qquad \Gamma \vdash e_2 : \tau}{\Gamma \vdash e_1 = e_2 : \text{bool}}

e dê a orientação que os grupos levam para a tutoria: escreva as regras antes de implementá-las, porque uma regra que não se consegue escrever é uma decisão de projeto ainda não tomada. A implementação segue-as depois quase literalmente, com um caso por forma de expressão.

Trate equivalência de tipos com o exemplo que convence: sob equivalência por nome, declarar um tipo para metros e outro para segundos, ambos numéricos, faz o compilador recusar a soma dos dois; sob equivalência estrutural, os dois são o mesmo tipo. Registre o episódio que mostra o custo de não decidir: o relatório original de Pascal não deixou claro qual noção valia, e o resultado foram programas que compilavam num compilador e não em outro. Sobre coerção, dê a política numa frase — coerção que perde informação deve ser explícita; coerção que preserva informação pode ser implícita — e o caso doloroso da comparação entre valor com sinal e sem sinal em linguagens que promovem ambos a sem sinal: o negativo vira um positivo enorme e a comparação dá o resultado oposto ao pretendido, sem mensagem alguma.

Feche com a técnica de melhor retorno da fase inteira, e faça a conta que a justifica. Reserve um tipo de erro distinguido, atribuído a toda expressão cujo tipo não foi determinado por causa de um defeito já reportado, com a propriedade de absorção: toda operação com um operando desse tipo produz esse tipo e não gera diagnóstico novo. Mostre o custo de não ter isso com números: um operando maltipado no fundo de uma expressão com três operadores acima dele gera um diagnóstico verdadeiro mais três consequências, quatro mensagens para um defeito; com absorção, uma. E avise do defeito típico — implementar a absorção na aritmética, esquecê-la na atribuição e obter um relatório quase limpo com duas mensagens espúrias, pior de depurar do que a cascata inteira.

Bloco de construção ao vivo — a tabela e a invariante que a protege

Este é o bloco de code-along. Avise a sala para abrir o editor e digitar junto, e circule para conferir em vez de perguntar “todo mundo conseguiu?”, que sempre recebe silêncio afirmativo. Construa a estrutura de entrada da tabela com os campos que a turma justificou no primeiro bloco, depois as quatro operações, e pare em cada uma para verbalizar a decisão.

Duas coisas precisam ser digitadas por eles, não vistas por eles. A primeira é o par de entrada e saída de escopo escrito na mesma função, sem nenhum caminho de retorno entre os dois — não é preciosismo de estilo, é a defesa contra a classe inteira de defeitos de escopo desbalanceado. A segunda é a verificação de simetria: a profundidade da tabela tem de ser a mesma antes e depois de cada construção que abre escopo. São duas linhas, e pegam um defeito de sintoma perverso — o compilador passa a aceitar programas que referenciam nomes locais de uma construção dentro de outra.

Bloco de fechamento — quantos percursos, e a volta ao quadro da abertura

Feche pela decisão operacional que a tutoria vai cobrar: quantas travessias da árvore. Não dê o número. Dê a pergunta que o produz, e escreva-a no quadro: que informação preciso já ter coletado para poder fazer esta verificação? Agrupe as verificações pelo que exigem, ordene os grupos pela dependência, e o número de grupos é o número de percursos — sai da análise em vez de ser arbitrado.

Mostre em seguida o caso que quebra a passagem única, porque é o que os grupos encontram na tutoria sem reconhecer: quando a linguagem permite referenciar um nome declarado adiante, nenhuma esperteza local resolve, porque a informação não existe no momento da consulta. Ou se exige declaração antes do uso — restrição imposta ao usuário para conveniência do compilador, e não é acidente que várias linguagens antigas a imponham, Pascal à frente — ou se fazem dois percursos. E nomeie o que não é percurso: o aviso de declaração sem uso só pode sair depois de todos eles, varrendo a tabela completa.

Volte então às quatro palavras do canto do quadro e pague a dívida, uma a uma, apontando o mecanismo. Declaração: consulta visível que falha. Redeclaração: consulta local que acerta. Tipo: a regra de inferência que não fecha. Uso: a varredura final da tabela. Termine com a frase que abre o próximo módulo e que vale escrever: a partir daqui a árvore está anotada com tipos e a tabela está preenchida, e isso não é subproduto — é exatamente a entrada que a síntese consome.

Aulas 3 a 6 — Tutoria do Projeto Integrador

Quatro aulas de tutoria — tabela de símbolos com escopo, verificações de declaração e de tipos, corpus de erros deliberados e o documento das decisões sobre percursos.

O andaime aqui é baixo por desenho: você dá o resultado esperado e a tutoria opera como revisão crítica. Na prática isso significa recusar duas perguntas que os grupos farão — “quantos percursos a gente faz?” e “qual das duas estratégias de tabela a gente usa?” — e devolvê-las como perguntas de dependência e de medição. Diga isso na abertura da primeira sessão, para que a recusa não seja lida como indisponibilidade.

Tenha a implementação de referência aberta e projetada nas duas sessões, e use-a do jeito que rende: não como código a copiar, mas como formato de argumento. O que o grupo precisa ver é uma decisão justificada por um número ou por uma dependência, e não por gosto.

Primeira sessão de tutoria — decidir a tabela, e medir em vez de opinar

Abra pedindo a cada grupo uma coisa só, por escrito, antes de qualquer código: a lista dos campos da tabela de símbolos com o consumidor de cada um ao lado. Circule conferindo essa folha. Campo sem consumidor sai da lista na hora — é o exercício da competência do módulo, e é barato fazer agora e caro desfazer depois.

Em seguida, a decisão de escopo. Faça sempre a mesma pergunta a cada grupo, nesta ordem: a sua linguagem tem escopo aninhado? onde? quantos níveis, no pior programa que ela permite escrever? Grupos cuja linguagem tem um único nível não precisam de tabela com escopos, e precisam registrar isso por escrito com a justificativa — essa é a entrega deles, e vale nota integral. Grupos com dois níveis são a maioria e caem exatamente na primeira linha da tabela numérica da aula teórica.

Projete então a medição da referência, porque é o que transforma a discussão em decisão. A demonstração de escopos faz dez consultas ao nome mais externo em profundidades crescentes e imprime as sondagens de cada estratégia: 20 contra 10 na profundidade 1, 30 contra 10 na 2, 50 contra 10 na 4, 90 contra 10 na 8. Diga em voz alta a conclusão que a referência tirou e por quê: para a linguagem do projeto, que vive na primeira linha — padrões no escopo global e a variável de ligação de cada ação num escopo próprio —, a escolha foi a empilhada, porque a diferença é de uma sondagem por consulta e sair de escopo é um desempilhamento, sem trilha de desfazimento para manter em dia. Aponte que a conclusão se inverteria com blocos aninhados profundos, e que é por isso que a medição importa mais que a preferência.

Feche a sessão com o teste que os grupos não escrevem sozinhos: o teste de simetria. Depois de entrar e sair de um escopo, a tabela tem de responder exatamente o que respondia antes. Quem escolheu a estratégia encadeada precisa desse teste, sem exceção, porque é ele que pega a trilha de desfazimento incompleta — e o caminho feliz passa sem ele.

Segunda sessão de tutoria — os percursos, o tipo que se decide e o corpus

Comece pela pergunta de dependência, não pela arquitetura: que informação você precisa já ter coletado para poder verificar isto? Deixe o grupo listar as verificações e agrupar. Só depois que o grupo tiver produzido a própria decomposição, mostre a da referência e o motivo dela, que são dois e ambos concretos: a linguagem permite que o bloco de regras apareça antes dos padrões que ele referencia, de modo que um verificador de passagem única acusaria padrão não declarado para um padrão declarado três linhas abaixo; e o tipo da variável de ligação depende do autômato do padrão, construído no primeiro percurso, do qual o segundo depende para verificar a extração de valor. Dois percursos, e a ordem entre eles não é preferência — é o grafo de dependências mandando.

O momento alto da sessão é a inferência do tipo do casamento, e vale projetá-la inteira. A pergunta é se um padrão casa apenas números, porque é disso que depende a legalidade da extração de valor. As duas saídas óbvias foram descartadas na referência: pedir anotação transfere ao autor uma verificação que o compilador tem informação para fazer, e inspecionar o texto da expressão regular procurando dígitos é heurística. A saída certa é a inclusão de linguagens, L(\text{padrao}) \subseteq L(\text{NUMERO}), equivalente a L(\text{padrao}) \setminus L(\text{NUMERO}) = \emptyset — e a diferença de autômatos e o teste de vacuidade estão prontos desde o módulo 6. Mostre a saída da demonstração de tipos, com o autômato de referência do número em 5 estados, o padrão numérico com diferença vazia classificado como número e o padrão de endereço eletrônico com diferença não vazia classificado como texto, sem uma única anotação no programa.

Diga também a armadilha, porque ela passa nos primeiros testes: inverter a diferença. Calcular L(\text{NUMERO}) \setminus L(\text{padrao}) responde se o padrão casa todos os números, que é a pergunta errada — e a inversão passa exatamente no primeiro caso que todo mundo testa, o padrão idêntico ao da categoria numérica. Peça a cada grupo que teste com um padrão estritamente mais restrito.

A segunda metade da sessão é o corpus de erros, e aqui há uma ordem a impor. O corpus útil é escrito antes das verificações, a partir do que a linguagem permite escrever de errado; escrito depois, ele só contém o que o compilador já detecta e não revela nada. Exija de cada grupo o número esperado de diagnósticos por programa antes de rodar. Um a mais é cascata, e a correção é o tipo de erro absorvente; um a menos é verificação faltando.

O fechamento, e o que registrar no diário

Reserve o final da última sessão para duas conferências rápidas e sempre iguais. A primeira é a invariante de profundidade da tabela antes e depois de cada construção que abre escopo — duas linhas de código que pegam a classe inteira de defeitos de escopo desbalanceado, cujo sintoma é o compilador aceitar demais em silêncio. A segunda é a regressão: as demonstrações dos módulos anteriores continuam corretas depois de a fase nova ter entrado no sistema.

Conte aos grupos os dois defeitos de compilação que a referência colecionou neste módulo, porque os dois são de projeto cumulativo e não de descuido. O primeiro foi um choque de nomes com três módulos de distância: o nome óbvio para a entrada da tabela de símbolos já estava ocupado desde o módulo 3 pelo símbolo do alfabeto do autômato, e a correção foi qualificar o novo conceito. O segundo foi mais interessante: o registro do padrão compilado contém um autômato, e o autômato não tem construtor padrão porque no módulo 3 ficou decidido que todo autômato nasce com nome — uma invariante de nove módulos atrás bloqueando o código de agora, e com razão, já que um autômato sem nome não apareceria em diagnóstico nenhum. Grupos que estão no décimo segundo módulo de um sistema cumulativo vão encontrar os seus, e a lição é que o verificador estrito é quem avisa.

Peça, por fim, o registro no diário de três itens por grupo: a estratégia de tabela escolhida com o número que a sustentou, o número de percursos com a dependência que o fixou, e o formato do que a fase produz. Este último não é burocracia — o vetor de padrões compilados que sai daqui é a primeira metade do programa objeto, e a decisão sobre o formato dele precisa estar tomada antes de a geração de código começar a consumi-lo no módulo 13.

Entregáveis e Avaliação

Ao fim do módulo, cada grupo entrega a tabela de símbolos com o tratamento de escopo adequado à própria linguagem, as verificações de declaração e de compatibilidade de tipos funcionando, um conjunto de programas com erros semânticos deliberados acompanhado das mensagens que o compilador produz para cada um, e o documento com as decisões sobre a organização dos percursos.

Confira cada entrega na mesma ordem para todos os grupos, com seis perguntas. Cada campo da tabela tem um consumidor nomeado, ou entrou por completude? A linguagem tem escopos aninhados e eles são tratados, ou a ausência deles está registrada e justificada? O uso de nome não declarado é detectado com nome e posição? A operação sobre tipo incompatível é detectada com os dois tipos na mensagem? O compilador reporta todos os defeitos do programa de teste em uma execução? E o número de percursos está justificado por uma dependência concreta, e não por preferência de arquitetura?

Dois critérios merecem peso explícito. O primeiro é a qualidade do relato de erro, exigível desde o módulo 7 e que aqui tem, pela primeira vez, informação suficiente para ser boa: o compilador conhece o identificador, a posição, a posição da declaração conflitante e os tipos dos dois lados, e uma mensagem que se limita a “tipo incompatível” descarta tudo isso. O segundo é a contagem exata de diagnósticos no conjunto de erros deliberados: um a mais indica cascata, sinal de que o tipo de erro não está sendo absorvido; um a menos indica verificação faltando. Peça o número esperado por escrito antes de rodar.

Registre no componente contínuo a pontualidade da entrega, a contribuição nas discussões em duplas e o engajamento na tutoria. E deixe claro que redução de escopo consciente e documentada não é penalizada: um grupo cuja linguagem tem um só nível de escopo entrega uma tabela mais simples e o registro do porquê, e isso vale nota integral — o que não vale é a tabela simples sem o registro.

Orientações Sobre o Aplicativo

Use o aplicativo para as três votações, com a projeção anônima. O dado que mais interessa é o da segunda questão: o primeiro voto costuma se dividir entre a alternativa certa e a que trata sombreamento como redeclaração, e a distância entre os dois histogramas diz se a distinção entrou. Se a turma convergir para a alternativa errada já no primeiro voto, não abra a discussão em duplas — a instrução por pares só corrige quando há quem tenha entendido. Refaça a definição no quadro e vote de novo.

Na terceira questão, muitos votos em pré-ordem seguida de pós-ordem indicam que a turma entendeu os dois fluxos e ainda não entendeu a dependência cruzada — é o único ponto do módulo que vale reabrir na tutoria, porque reaparece no módulo 13, quando a ordem de emissão de código voltar a ser ditada por dependências.

Acompanhe também o engajamento no estudo do material, que responde por metade do componente contínuo, e compare-o com o do módulo 11: como aquele módulo não teve implementação, alguns grupos entram neste com atraso acumulado que o número revela antes de a entrega revelar.

Pontos de Atenção Específicos

O risco número um é a fase virar catálogo de estruturas. Tabela, escopos, atributos e regras de tipo formam uma lista fácil de expor e inútil de decorar. O antídoto é verificável em sala: se você conseguir reconstruir a tabela perguntando “quem consome este campo?” e a decomposição em percursos perguntando “o que preciso ter coletado antes?”, os blocos funcionaram.

Não abrevie as gramáticas de atributos por serem conceituais. A tentação é grande, porque quase nenhum grupo vai construir o grafo explicitamente e o tópico parece luxo. É o oposto: o grafo é o que explica por que a recursão que todos vão escrever funciona, e é o que dá nome ao defeito quando ela deixar de funcionar. O desenho no quadro com as dez ocorrências e as dez arestas é o investimento de maior retorno das duas aulas.

Cuidado com a digressão sobre inferência de tipos. Assim que você escrever os julgamentos, alguém pergunta como funcionam as linguagens que dispensam anotação. Responda em duas frases — que existe um algoritmo de unificação para sistemas de tipos com essa propriedade, e que a disciplina não o cobre — e reconduza ao ponto do módulo, que é decidir a pergunta com a maquinaria já existente em vez de anotar ou adivinhar.

A confusão entre redeclaração e sombreamento reaparece no código. Ela não morre na aula teórica. Circule na tutoria olhando por uma única função de consulta usada para as duas perguntas: é o defeito mais frequente do módulo, e o sintoma é o compilador recusar um programa válido, o que os grupos costumam atribuir ao analisador sintático.

Grupos travam tentando um percurso só. Acontece com precisão de relógio. Não entregue a decomposição: faça a pergunta de dependência e deixe o grupo descobrir que a informação não existe no momento em que ele a consulta. A descoberta é o conteúdo.

O escopo desbalanceado falha em silêncio. É a classe de defeito característica desta fase e a única que não aparece como erro: o compilador simplesmente aceita mais do que devia. Cobre a verificação de simetria de profundidade mesmo dos grupos cujo código está funcionando — sobretudo deles, porque o defeito só se manifesta em programas que ninguém pensou em testar.

Não deixe a semântica rodar sobre árvore quebrada. Grupos com recuperação de erro sintático tendem a encadear as fases sem condição, e a semântica sobre uma árvore gravemente incompleta produz erros que são consequência do defeito sintático — ruído que esconde a causa real. Exija a condição: a semântica só roda quando não houve erro sintático.