Moacyr Francischetti Corrêa

1 Módulo 09: Projeto do Professor — O Analisador Sintático em Embrião

Este é o projeto de referência resolvido pelo professor: a mesma atividade que cada grupo vai executar neste módulo, feita por inteiro. Módulo sem tarefa de implementação — a entrega é teórica, e este documento é o modelo do que o seu grupo deve escrever, não algo a copiar.

1.1 Visão Geral do Módulo 09

Este é um dos dois módulos do semestre em que o projeto não recebe código novo, e vale dizer de saída por que a decisão está certa. O autômato de pilha é o modelo abstrato correspondente às gramáticas do módulo anterior. A realização concreta dele, neste projeto, é o analisador sintático de descida recursiva do módulo 10. Implementar um autômato de pilha genérico produziria código que nenhuma outra parte do compilador usaria — contrariando o caráter cumulativo da implementação de referência, em que tudo que se escreve continua em uso até o último módulo.

O que a atividade pede é um documento de fundamentação: mostrar como a gramática escrita no módulo 8 se traduz no modelo de pilha, ilustrar com o traçado manual do reconhecimento de um programa curto, e discutir por que a análise sintática prática se restringe a subclasses determinísticas — com as consequências disso para as escolhas do módulo seguinte.

Resolvi a atividade e, ao fazê-lo, encontrei três coisas que valem mais que o exercício em si. A primeira é que o autômato de pilha construído a partir da gramática já é o analisador do módulo 10, faltando apenas a parte que elimina as adivinhações. A segunda é que as adivinhações que ele precisa fazer são exatamente aquelas que a antecipação de um símbolo vai resolver. E a terceira é que a recursão à esquerda, inofensiva no modelo não determinístico, é fatal na implementação determinística — o que explica, com precisão, por que o módulo 10 começa eliminando-a.

1.2 Tarefa 1: Traduzir a gramática para o modelo de pilha

A atividade — explicar como a gramática da Peneira se traduziria em um autômato de pilha, seguindo a construção vista em aula.

A construção que parte da gramática é a mais instrutiva das duas, e é enganosamente simples. O autômato resultante tem um único estado. Toda a informação vive na pilha, e o controle finito não precisa lembrar de nada — o que é, em si, um resultado curioso: a memória que faltava ao autômato finito estava toda na estrutura, não nos estados.

A construção tem duas famílias de transições, e só duas.

A primeira expande: para cada produção da gramática, uma transição que não consome símbolo algum da entrada, retira a variável do topo da pilha e coloca no lugar o corpo da produção, com o primeiro símbolo do corpo ficando no topo. Para a produção declPadrao -> pattern ID = PADRAO ;, a transição tira declPadrao do topo e empilha os cinco símbolos, com pattern acima.

A segunda casa: para cada terminal do alfabeto, uma transição que consome esse terminal da entrada quando ele está no topo da pilha, e o retira. É o passo que faz progresso na leitura; o primeiro nunca lê nada.

O autômato começa com o símbolo inicial da gramática sozinho na pilha e aceita quando a pilha esvazia junto com a entrada. Nada mais.

Vale enunciar o que essa construção significa, porque é o coração do módulo: a pilha guarda o que ainda falta reconhecer. Em qualquer instante, o conteúdo da pilha, lido do topo para a base, é a parte da forma sentencial que ainda não foi casada com a entrada. As expansões substituem uma obrigação por obrigações menores; os casamentos cumprem obrigações. O autômato aceita quando não sobra obrigação nenhuma.

Essa leitura é a inversão exata do que acontecerá no módulo 11, com a análise ascendente, em que a pilha guarda o que foi reconhecido. Duas estratégias, dois significados opostos para a mesma estrutura, e é útil ter os dois na cabeça antes de estudar o segundo.

Onde é fácil errar aqui. Empilhar o corpo da produção na ordem em que está escrito. O primeiro símbolo do corpo precisa terminar no topo, o que significa empilhar de trás para frente. Quem empilha na ordem direta obtém um autômato que reconhece a linguagem dos corpos invertidos — e, o que é pior, ele funciona para produções de um símbolo só, que costumam ser as primeiras testadas.

Como verificar. O autômato construído precisa aceitar exatamente as cadeias que a gramática deriva. Como cada expansão corresponde a uma aplicação de produção e cada casamento a um terminal já derivado, a sequência de expansões executadas em uma aceitação é precisamente uma derivação mais à esquerda. Conferir isso num exemplo é a tarefa 2.

1.3 Tarefa 2: O traçado manual

A atividade — traçar à mão o reconhecimento de um programa curto, mostrando o conteúdo da pilha a cada passo.

Tomei o menor programa Peneira que exercita uma declaração completa:

pattern email = /[a-z]+/;

O analisador léxico do módulo 7 o converte em cinco símbolos, que a gramática vê como os terminais pattern, ID, =, PADRAO e ;. O traçado abaixo mostra, a cada passo, o que resta na pilha (topo à esquerda), o que resta da entrada, e a transição aplicada.

Passo Pilha (topo à esquerda) Entrada restante Transição
0 programa pattern ID = PADRAO ; início
1 listaDecl pattern ID = PADRAO ; expande programa -> listaDecl
2 decl pattern ID = PADRAO ; expande listaDecl -> decl
3 declPadrao pattern ID = PADRAO ; expande decl -> declPadrao
4 pattern ID = PADRAO ; pattern ID = PADRAO ; expande declPadrao -> pattern ID = PADRAO ;
5 ID = PADRAO ; ID = PADRAO ; casa pattern
6 = PADRAO ; = PADRAO ; casa ID
7 PADRAO ; PADRAO ; casa =
8 ; ; casa PADRAO
9 vazia vazia casa ;

Aceita, por pilha vazia, em nove passos. Quatro expansões e cinco casamentos — um casamento por terminal, como tem de ser.

Duas observações que o traçado torna concretas.

Os quatro primeiros passos não consomem nada da entrada. O autômato passa um bom tempo reorganizando a pilha antes de ler o primeiro símbolo. Isso é característico da estratégia descendente e é o que a distingue da ascendente: aqui, a decisão sobre qual produção usar é tomada antes de ver a evidência que a justificaria.

E a sequência de expansões — programa, listaDecl, decl, declPadrao — é exatamente a derivação mais à esquerda que o enumerador do módulo 8 produziu para este mesmo programa. Não é semelhança: é o mesmo objeto. O autômato de pilha, executando, é a derivação mais à esquerda sendo construída passo a passo.

1.3.1 Onde estão as adivinhações

O traçado acima parece determinístico, e não é. No passo 2, a variável listaDecl tem duas produções:

listaDecl -> decl listaDecl
listaDecl -> decl

Nada no topo da pilha distingue os dois casos, e o autômato não olhou a entrada — a transição de expansão não consome símbolo. Ele adivinha. No modelo não determinístico isso é legítimo: a máquina explora todos os caminhos ao mesmo tempo e aceita se algum der certo. Com uma declaração só, o caminho certo é o segundo; com duas, seria o primeiro.

A mesma adivinhação aparece no passo 3, entre declPadrao e blocoRegra, e apareceria dentro das expressões, entre os quatro tipos de primaria.

Repare no que resolveria cada uma delas. Para escolher entre declPadrao e blocoRegra, bastaria olhar um símbolo à frente: pattern indica o primeiro, rule o segundo. Para escolher entre as duas formas de listaDecl, bastaria saber se ainda há entrada. É isso que o módulo 10 vai formalizar com os conjuntos de primeiros e de seguidores, e é por isso que aquele módulo é a continuação direta deste, e não um assunto novo.

Onde é fácil errar aqui. Traçar o reconhecimento consultando a entrada nas expansões — isto é, escrevendo o traçado como se o autômato já fosse determinístico. O traçado fica certo e esconde exatamente o que o módulo quer mostrar. O honesto é registrar, em cada expansão com mais de uma produção possível, que houve escolha, e qual informação a teria dispensado.

Como verificar. O número de casamentos tem de ser igual ao número de terminais da entrada, e o número de expansões tem de ser igual ao número de nós internos da árvore de derivação. Neste exemplo, cinco e quatro — e a árvore que o módulo 8 desenhou tem exatamente quatro nós internos.

1.4 Tarefa 3: Determinismo, e o que ele custa

A atividade — discutir por que a análise sintática prática se restringe a subclasses determinísticas e o que isso implica para o módulo 10.

Aqui está o resultado que distingue este nível da hierarquia de tudo que veio antes, e ele contraria a intuição formada no módulo 5.

Lá, autômatos finitos determinísticos e não determinísticos se mostraram equivalentes: a construção de subconjuntos converte qualquer um no outro, e o preço é tamanho, não expressividade. Aqui, a equivalência não vale. Existem linguagens livres de contexto que nenhum autômato de pilha determinístico reconhece.

O exemplo canônico é a linguagem dos palíndromos de comprimento par sobre dois símbolos — cadeias formadas por uma parte seguida do seu reverso. Um autômato de pilha não determinístico a reconhece empilhando a primeira metade e desempilhando na segunda; o problema é saber onde é o meio. A máquina não determinística adivinha, e acerta em algum ramo. Uma máquina determinística teria de decidir com base no que já leu, e nenhuma informação local revela o ponto de virada.

A consequência prática é grande e é o que organiza os dois módulos seguintes. Como não sabemos executar não determinismo em tempo razoável, a análise sintática de verdade se restringe a subclasses determinísticas das linguagens livres de contexto. Isso significa aceitar de antemão que existem gramáticas legítimas que nenhum analisador prático vai tratar, e que a escolha da gramática é parte do projeto do compilador — não uma formalidade posterior.

É também a razão de existir uma família de métodos de análise, em vez de um só. As subclasses determinísticas formam uma escala, e cada método cobre uma faixa: os descendentes com um símbolo de antecipação cobrem menos, os ascendentes cobrem mais, e ainda assim nenhum cobre tudo. No módulo 5, um algoritmo resolvia o problema inteiro; daqui em diante, é preciso escolher.

1.4.1 A recursão à esquerda, e por que ela é fatal adiante

Esta é a descoberta que mais rende para o módulo 10, e ela sai do traçado.

A gramática da Peneira tem recursão à esquerda nas expressões, e por decisão consciente — foi ela que deu associatividade à esquerda aos operadores:

expr  -> expr or exprE | exprE

Trace o autômato de pilha sobre isso. Com expr no topo, uma das expansões possíveis põe de volta expr or exprE, com expr novamente no topo. Nada foi consumido da entrada, e o estado da pilha piorou. Nada impede que a máquina faça isso de novo, e de novo.

No modelo não determinístico isso não é problema. A definição de aceitação diz que basta algum ramo levar à aceitação, e o ramo que expande a quantidade certa de vezes existe. O ramo infinito também existe, e simplesmente não importa.

Numa implementação determinística, importa muito. Uma função que, para reconhecer uma expressão, começa chamando a si mesma sem antes consumir símbolo algum, não retorna nunca. É recursão infinita, e é o modo mais comum de o analisador descendente de um grupo travar na primeira execução.

Daí a ordem de trabalho do módulo 10: eliminar a recursão à esquerda antes de qualquer implementação. E daí, também, um custo que precisa ser aceito de olhos abertos — a transformação preserva a linguagem e muda as árvores. A gramática transformada não é mais à esquerda-recursiva, e a associatividade que a recursão à esquerda garantia terá de ser reconstruída na hora de montar a árvore sintática abstrata. O que era estrutura da gramática vira responsabilidade do código.

Registro isso aqui, no módulo 9, porque é agora que a razão fica clara. No módulo 10, no meio da transformação, ela pareceria um detalhe técnico.

1.5 Referência teórica: as duas convenções de aceitação

O modelo admite duas definições de aceitação — por estado final e por pilha vazia — e elas reconhecem a mesma classe de linguagens. A demonstração é uma construção em cada sentido, e vale saber que ela é mais sutil do que parece.

Para converter aceitação por pilha vazia em aceitação por estado final, não basta marcar como final o estado em que a pilha esvazia: é preciso um símbolo de fundo, empilhado antes de tudo, que só é retirado quando a pilha “real” já está vazia — e é a retirada dele que leva ao estado final. Sem o símbolo de fundo, o autômato aceitaria prefixos, porque a pilha passa por configurações vazias no meio do caminho em algumas gramáticas.

No sentido inverso, converter aceitação por estado final em pilha vazia exige um estado de limpeza que esvazia a pilha ao chegar num final, e de novo um símbolo de fundo — desta vez para impedir que o autômato original esvazie a pilha por acidente e aceite antes da hora.

A construção que usei na tarefa 1 aceita por pilha vazia, que é a mais natural quando se parte da gramática: a pilha guarda o que falta reconhecer, e não faltar nada é exatamente a condição de aceitação. Escolher a outra convenção teria exigido o símbolo de fundo e um estado a mais, sem ganho.

1.6 Referência teórica: o que a classe perde ao subir de nível

Duas propriedades que valiam para as linguagens regulares deixam de valer aqui, e a diferença é instrutiva.

A classe das linguagens livres de contexto é fechada sob união, concatenação e fecho, como a regular. Mas não é fechada sob interseção nem sob complemento.

O contraexemplo para a interseção é conhecido e vale reproduzir porque é limpo. A linguagem das cadeias com um número qualquer de letras c precedido por igual número de a e de b é livre de contexto — a pilha conta os a, desconta com os b, e os c passam livres. A linguagem simétrica, com um número qualquer de a seguido de igual número de b e c, também é. A interseção das duas exige contagem igual nos três, e essa não é livre de contexto: uma pilha só não conta duas coisas ao mesmo tempo.

Como a classe é fechada sob união, a não clausura sob interseção implica a não clausura sob complemento — se fosse fechada sob complemento, a interseção poderia ser obtida por complemento de união de complementos, e acabamos de ver que não pode.

Vale conectar com o módulo 6, porque a diferença tem consequência prática direta neste projeto. Lá implementei interseção e complemento sobre autômatos finitos, e as construções eram diretas: produto de estados para a interseção, inversão de finalidade para o complemento. Nenhuma das duas se estende para cá. O produto de dois autômatos de pilha precisaria de duas pilhas, e duas pilhas dão poder de máquina de Turing. A inversão de finalidade falha porque o autômato não determinístico pode ter ramos que aceitam e ramos que não, e negar isso não é negar a aceitação.

Se em algum momento o projeto precisar decidir inclusão ou equivalência entre gramáticas — coisa que o módulo 6 tornou trivial para expressões regulares —, a resposta é que não dá: equivalência de gramáticas livres de contexto é indecidível. A ferramenta que existe para o nível regular não tem contrapartida aqui, e saber disso evita procurar por ela.

1.7 Referência teórica: bombeamento no nível livre de contexto

O lema do bombeamento tem uma versão para esta classe, com uma diferença estrutural em relação à do módulo 6: em vez de um trecho bombeável, há dois, e eles são bombeados juntos e na mesma quantidade.

A intuição vem da árvore de derivação, não do autômato. Numa árvore suficientemente alta, algum caminho da raiz até uma folha repete uma variável — casa dos pombos de novo, agora sobre a altura em vez do comprimento. O trecho da árvore entre as duas ocorrências dessa variável pode ser removido ou duplicado quantas vezes se queira, e o que ele deriva à esquerda e à direita da subárvore interna são os dois trechos bombeados.

A consequência para o uso é que as demonstrações ficam mais trabalhosas: o adversário escolhe a decomposição em cinco partes, não em três, e há mais casos a descartar. Não implementei uma versão do decompositor do módulo 6 para este nível, e o motivo é o mesmo que justifica o módulo inteiro não ter código: seria uma ferramenta que nenhuma outra parte do compilador usaria. A decomposição do módulo 6 existia porque operava sobre os autômatos que o projeto já tinha.

1.8 Tarefa 4: O registro de pendências

A atividade — registrar o que foi saldado em pendências durante o módulo.

O módulo 9 é o último respiro antes do mais denso do semestre, e a entrega inclui o registro do que foi acertado. Auditei o projeto de referência e o estado é este.

O código está limpo: as trinta unidades de tradução compilam sem nenhum aviso sob o modo estrito, e as demonstrações dos oito módulos anteriores rodam. Os corpora das seis categorias léxicas passam integralmente, e a gramática do módulo 8 não tem símbolo inútil nem produção vazia.

A pendência de verdade foi saldada no módulo 7 e vale relembrá-la aqui, porque é o tipo de coisa que o registro existe para capturar: a notação de PONTUACAO estava escrita como prosa legível e reconhecia a linguagem errada, tendo atravessado cinco módulos sem ser executada. A correção mudou números publicados em dois documentos, que foram atualizados.

O que continua em aberto é de natureza editorial, não técnica: os documentos dos módulos 7 e 8 ainda não estão registrados nos perfis de publicação, o que significa que existem e não são gerados. Não afeta o código nem o conteúdo, e precisa ser resolvido antes de o material ir para a turma.

E há uma dívida deliberada, que registro como dívida e não como esquecimento: as decisões de precedência e associatividade tomadas no módulo 8 vão ser parcialmente desfeitas no módulo 10, pela eliminação da recursão à esquerda. A associatividade terá de ser reconstruída na montagem da árvore sintática abstrata. Está previsto, o motivo está escrito acima, e o custo é conhecido.

Onde é fácil errar aqui. Usar o registro de pendências como formalidade e escrever “nada pendente”. Um projeto de oito módulos sempre tem algo — e o valor do registro está justamente em nomear o que se escolheu adiar, para que a escolha não vire esquecimento.

1.9 Verificação da entrega

Item Como conferir Estado nesta referência
Tradução da gramática Duas famílias de transição descritas, com a ordem de empilhamento correta Atende
Traçado manual Pilha e entrada a cada passo, com a transição aplicada Nove passos, quatro expansões e cinco casamentos
Adivinhações identificadas Cada expansão com mais de uma produção possível está marcada Passos 2 e 3, com a informação que as resolveria
Correspondência com a derivação Sequência de expansões igual à derivação mais à esquerda do módulo 8 Confirmado
Discussão sobre determinismo Não equivalência enunciada, com exemplo e consequência prática Atende
Consequência para o módulo 10 Recursão à esquerda identificada como fatal, com o motivo Atende
Convenções de aceitação As duas descritas, com a sutileza do símbolo de fundo Atende
Propriedades de fechamento O que se perde ao subir de nível, com contraexemplo Atende
Registro de pendências Auditoria honesta, incluindo dívida deliberada Atende
Código de projeto Nenhum, conforme a decisão registrada para este módulo Nada foi criado

O que quero deixar registrado sobre esta entrega é que ela não produz artefato executável e mesmo assim é trabalho de projeto. O traçado de nove passos que fiz à mão é o analisador do módulo 10 rodando em câmera lenta, e as duas adivinhações que identifiquei nele são exatamente o que a antecipação de um símbolo vai eliminar. Quem chega ao módulo 10 tendo feito este traçado reconhece a estrutura do que vai implementar; quem pulou vai encontrar um algoritmo que parece surgir do nada.