Moacyr Francischetti Corrêa

1 Módulo 11: Análise Sintática Ascendente

Bem-vindo ao módulo mais lápis-e-papel do curso. Aqui não há arquivo para compilar: há uma tabela para construir à mão e uma decisão de engenharia para tomar com os dois lados na mesa. Ao fim desta leitura você deve conseguir abrir o relatório de conflitos de um gerador de analisadores e entender o que ele diz — de longe a habilidade deste assunto que mais gente usa na vida profissional e menos gente possui.

1.1 O problema: a gramática que tivemos de estragar

Vamos por partes, começando por uma confissão sobre o módulo anterior.

Para fazer o analisador descendente funcionar, nós mexemos na gramática. Eliminamos recursão à esquerda, porque uma produção que começa pela própria variável faz o analisador recursivo chamar a si mesmo sem consumir nada e afundar até estourar a pilha de chamadas. Fatoramos prefixos comuns, porque duas produções que começam iguais não dá para distinguir olhando um símbolo à frente. Cada transformação preservou a linguagem e estragou outra coisa: a gramática cresceu, ganhou variáveis auxiliares que não significam nada e passou a produzir árvores que já não refletem a estrutura que tínhamos em mente.

Eu aceitei aquilo como se fosse o preço da análise sintática. Não é: é o preço de um método de análise sintática. Existe uma segunda família para a qual a recursão à esquerda não é obstáculo — é a forma preferida. Escreva E -> E + T e o analisador processa sem artifício nenhum.

O nome da família vem da direção: o método anterior partia da raiz e descia até as folhas, adivinhando qual produção usar antes de ver a evidência; este parte das folhas e vai combinando pedaços já reconhecidos até sobrar só a raiz, e nunca adivinha, porque só decide depois de ver. Se a família ascendente é mais poderosa, por que aprendemos a outra primeiro? A resposta honesta é a lição mais transferível do módulo, e eu a construo ao longo do texto para enunciá-la no fim.

Antes de começar, quero ser explícito sobre o que este capítulo pressupõe e sobre o lugar que ele ocupa no percurso, porque o segundo ponto costuma gerar desconfiança legítima.

Pressuponho o vocabulário de gramáticas livres de contexto — produções, variáveis, terminais, derivação, árvore de derivação, ambiguidade —, os conjuntos de primeiros e de seguidores com o algoritmo de ponto fixo que os calcula, e a experiência de ter escrito à mão um analisador descendente recursivo para uma gramática pequena, com uma função por variável. Pressuponho também a noção de autômato de pilha, que é o modelo abstrato ao qual tudo o que vem a seguir corresponde. Se algum desses itens estiver frouxo, o ponto mais econômico de revisão é o cálculo dos seguidores: ele reaparece aqui com uma função diferente, e quem o calcula errado constrói uma tabela errada sem perceber.

E aqui está o ponto que precisa ser dito de frente. O compilador de referência que acompanha este percurso segue o caminho descendente, escrito à mão, sem gerador de analisadores — decisão de projeto tomada para que cada peça do sistema seja código legível e explicável, e não uma caixa cujo interior o leitor nunca abre. Isso significa que o método deste capítulo não vai virar código no compilador de referência. Estudá-lo é estudar a alternativa descartada.

Não é desperdício, e a razão é prática. A família ascendente é a que sustenta a maior parte dos compiladores de produção e é a que está por trás de praticamente todo gerador de analisadores que alguém vai encontrar na vida profissional. Quem nunca construiu uma dessas tabelas à mão lê um relatório de conflito como quem lê um horóscopo: percebe que algo está errado, não sabe o quê nem onde. Quem construiu uma, ainda que minúscula, reconhece no relatório os itens, os estados e a decisão que não pôde ser tomada. A diferença entre as duas situações é a diferença entre operar uma ferramenta e ser operado por ela.

Há ainda um segundo motivo, menos utilitário e mais formativo. Boa parte do trabalho do capítulo anterior consistiu em transformar a gramática para caber no método descendente: eliminar recursão à esquerda, fatorar prefixos comuns, tolerar variáveis anuláveis que não existiam na versão original. Neste capítulo você vai descobrir que a recursão à esquerda, que era fatal lá, é a forma preferida aqui. Nenhuma daquelas dificuldades era dificuldade do problema; todas eram dificuldade da abordagem escolhida. Saber distinguir uma coisa da outra é o que separa quem escolhe uma ferramenta de quem apenas usa a que aprendeu primeiro.

1.2 Deslocar, reduzir, e a pilha que virou do avesso

O método tem uma economia notável: duas operações, mais dois casos terminais. Um deslocamento transfere o próximo símbolo da entrada para o topo da pilha. Uma redução por uma produção A \to \beta remove do topo da pilha os símbolos que formam \beta e empilha A no lugar. O analisador aceita quando a entrada acabou e a pilha contém só o símbolo inicial, e rejeita quando nenhuma das duas se aplica. Repare: a redução exige que o topo da pilha coincida com o corpo da produção, mas não que essa coincidência seja o momento certo de reduzir — e é aí que mora o problema inteiro do módulo.

Agora o ponto em que eu perco mais gente. No método descendente dirigido por tabela, a pilha guardava o que ainda falta reconhecer: começava com o símbolo inicial e era, a todo instante, uma previsão do resto da entrada. Aqui ela guarda o que já foi reconhecido: começa vazia, cresce à medida que símbolos são consumidos e agrupados, e é um resumo do prefixo já processado. Mesma estrutura de dados, significado exatamente oposto.

flowchart LR
    subgraph DESC["Método descendente"]
        direction TB
        D1["A pilha guarda<br/>o que ainda falta reconhecer"]
        D2["Começa com o<br/>símbolo inicial"]
        D3["Cada passo expande<br/>uma previsão"]
        D4["Termina vazia"]
        D2 --> D1 --> D3 --> D4
    end
    subgraph ASC["Método ascendente"]
        direction TB
        A1["A pilha guarda<br/>o que já foi reconhecido"]
        A2["Começa vazia"]
        A3["Cada passo condensa<br/>uma evidência"]
        A4["Termina com o<br/>símbolo inicial"]
        A2 --> A1 --> A3 --> A4
    end
    DESC -.->|"mesma estrutura de dados,<br/>significado oposto"| ASC
Figura 1: A mesma estrutura de dados com significados espelhados: uma encolhe onde a outra cresce.

Pare e faça isto agora. Pegue uma cadeia curta, trace o reconhecimento pelos dois métodos lado a lado na mesma folha e observe as duas pilhas evoluindo em espelho. Custa poucos minutos e imuniza você contra a confusão que estraga todos os traçados seguintes.

O trecho que uma redução consome tem nome próprio: alça. A alça de uma forma sentencial à direita é o par formado por uma produção A \to \beta e a posição de \beta correspondente ao último passo de uma derivação mais à direita — o que está à direita da alça é sempre só terminais. Traduzindo: reduzir a alça é desfazer o último passo de uma derivação mais à direita. Daí sai o resultado que organiza tudo: se o analisador reduz sempre a alça, a sequência de produções aplicadas, lida ao contrário, é uma derivação mais à direita da cadeia. Não é enfeite teórico, é o teste de verificação mais barato que existe — terminado um traçado à mão, leia as reduções de trás para a frente e veja se a derivação regenera a cadeia original.

O problema, então, é este e só este: decidir, em tempo constante, se o topo da pilha é uma alça e por qual produção reduzi-la.

Antes da maquinaria, o conceito que delimita o espaço do problema, e que eu considero a ideia mais elegante do assunto. Um prefixo viável é uma sequência de símbolos que pode legitimamente aparecer na pilha durante o reconhecimento de alguma cadeia da linguagem: enquanto o que está lá for prefixo viável, ainda há esperança. E aqui vem o teorema que sustenta a família inteira, publicado por Donald Knuth em 1965: para toda gramática livre de contexto, o conjunto dos prefixos viáveis é uma linguagem regular — e portanto reconhecível por um autômato finito.

Por que isso não é uma contradição. Reconhecer uma linguagem livre de contexto exige memória ilimitada, como demonstramos ao estudar os limites das linguagens regulares. A resolução está na divisão de trabalho: a memória ilimitada mora na pilha; o autômato não guarda o aninhamento, guarda apenas em que ponto de que produções estamos — informação finita, porque a gramática tem um número finito de produções de corpo finito.

1.3 Itens, fecho e desvio: montando o autômato

A gramática do módulo é a clássica das expressões com dois níveis de precedência, escrita com recursão à esquerda — a forma que o método anterior não tolerava. A única preparação exigida é acrescentar um símbolo inicial novo, para que exista um ponto de aceitação único:

(0) S' -> E
(1) E  -> E + T
(2) E  -> T
(3) T  -> T * F
(4) T  -> F
(5) F  -> ( E )
(6) F  -> id

Uma linha acrescentada, e seguimos.

O objeto central da construção é o item: uma produção com um ponto marcando uma posição no corpo. A leitura é direta — já empilhei a parte à esquerda do ponto, espero encontrar a parte à direita. O item T \to T \cdot {*} F diz que uma T já está na pilha e que, se vier * seguido de F, o corpo da produção (3) estará completo. Com o ponto no fim, o item é completo e sinaliza alça possível.

Um estado do autômato não é um item, é um conjunto de itens, porque o analisador, olhando a pilha, em geral não sabe em qual produção está: carrega todas as posições plausíveis até que a evidência elimine as inadequadas. É o mesmo raciocínio da determinização por subconjuntos, e não é coincidência — esta construção é, no fundo, uma determinização.

Duas operações constroem o autômato. O fecho acrescenta a um conjunto de itens as expectativas implicadas: se o ponto de algum item precede uma variável, entram os itens iniciais de todas as produções dela, e a regra se reaplica até estabilizar — esperar uma variável significa esperar tudo por onde ela possa começar. O desvio por um símbolo seleciona os itens cujo ponto o precede, avança o ponto e aplica o fecho. A coleção canônica sai do fecho do item inicial, calculando desvios até não surgirem estados novos.

Os dois erros que arruínam a construção à mão. O primeiro é calcular o fecho uma vez só: os itens que o próprio fecho acrescentou também podem exigir fecho. O sintoma é um estado sem transição por um símbolo que deveria tê-la. O segundo é não reconhecer estados repetidos — dois conjuntos de itens iguais são o mesmo estado, ainda que alcançados por caminhos diferentes. Quem não identifica o desvio de I_4 por T com I_2 constrói uma árvore infinita em vez de um autômato finito.

Para a nossa gramática, a coleção tem doze estados. Vale conferir a sua contra a minha.

A coleção canônica da gramática de expressões
Estado Itens Desvios
0 S'→.E E→.E+T E→.T T→.T*F T→.F F→.(E) F→.id E→1, T→2, F→3, (→4, id→5
1 S'→E. E→E.+T +→6
2 E→T. T→T.*F *→7
3 T→F.
4 F→(.E) E→.E+T E→.T T→.T*F T→.F F→.(E) F→.id E→8, T→2, F→3, (→4, id→5
5 F→id.
6 E→E+.T T→.T*F T→.F F→.(E) F→.id T→9, F→3, (→4, id→5
7 T→T*.F F→.(E) F→.id F→10, (→4, id→5
8 F→(E.) E→E.+T )→11, +→6
9 E→E+T. T→T.*F *→7
10 T→T*F.
11 F→(E).
flowchart LR
    I0(("I0")) -->|"id"| I5(("I5"))
    I0 -->|"("| I4(("I4"))
    I0 -->|"E"| I1(("I1"))
    I0 -->|"T"| I2(("I2"))
    I0 -->|"F"| I3(("I3"))
    I1 -->|"+"| I6(("I6"))
    I2 -->|"*"| I7(("I7"))
    I4 -->|"E"| I8(("I8"))
    I4 -->|"T"| I2
    I4 -->|"F"| I3
    I4 -->|"id"| I5
    I4 -->|"("| I4
    I6 -->|"T"| I9(("I9"))
    I6 -->|"F"| I3
    I6 -->|"id"| I5
    I6 -->|"("| I4
    I7 -->|"F"| I10(("I10"))
    I7 -->|"id"| I5
    I7 -->|"("| I4
    I8 -->|")"| I11(("I11"))
    I8 -->|"+"| I6
    I9 -->|"*"| I7
Figura 2: O autômato de prefixos viáveis com os doze estados e seus desvios.

Dois testes baratos localizam a maioria dos enganos. Coerência local: em todo estado alcançado por desvio sobre um símbolo, todo item ou tem esse símbolo imediatamente à esquerda do ponto, ou é item inicial trazido pelo fecho. Completude do fecho: se algum ponto precede uma variável, os itens iniciais de todas as produções dela estão presentes.

E agora o ponto que separa o assunto em duas metades: os estados 1, 2 e 9 têm, cada um, um item completo mandando reduzir e um item pedindo deslocamento. O autômato sozinho não decide entre os dois, e é aqui que a família se divide em níveis.

1.4 Da tabela ao traçado

A tabela de análise é a coleção canônica em forma consultável em tempo constante, com duas metades. A de desvio, indexada por estado e variável, sai direto do autômato. A de ação, indexada por estado e terminal, é onde mora a dificuldade: a pergunta difícil é em quais colunas registrar a redução por um item completo. Por ora adoto a resposta mais econômica — registrá-la nas colunas dos terminais que podem seguir a variável reduzida. Com \mathrm{SEG}(E) = \{+, ), \$\} e \mathrm{SEG}(T) = \mathrm{SEG}(F) = \{+, *, ), \$\}, a tabela sai assim, com d para deslocamento e r para redução:

A tabela de análise, sem uma única célula em disputa
Estado id + * ( ) $ E T F
0 d5 d4 1 2 3
1 d6 aceita
2 r2 d7 r2 r2
3 r4 r4 r4 r4
4 d5 d4 8 2 3
5 r6 r6 r6 r6
6 d5 d4 9 3
7 d5 d4 10
8 d6 d11
9 r1 d7 r1 r1
10 r3 r3 r3 r3
11 r5 r5 r5 r5

Olhe a linha 2, porque ela contém a decisão inteira. A redução por (2) aparece em +, ) e $, os seguidores de E; na coluna * aparece deslocamento, porque * não segue E. Com * à frente, não se reduz T a E — exatamente do que precisávamos. Repare no efeito colateral: se as linhas 2 e 9 trouxessem redução em *, a multiplicação deixaria de agrupar mais forte. No método ascendente, precedência é propriedade da tabela.

O analisador propriamente dito é um laço curto e independente da gramática. A pilha guarda estados intercalados com símbolos e começa contendo só o estado zero.

flowchart TD
    INI["Pilha com o estado 0<br/>entrada com marcador de fim"] --> CONS{"Consulta a célula de ação<br/>do estado do topo com o<br/>símbolo de antecipação"}
    CONS -->|"deslocar para t"| DESL["Empilha o símbolo e o estado t<br/>e avança na entrada"]
    CONS -->|"reduzir por A → beta"| RED["Desempilha o dobro do<br/>comprimento do corpo<br/>empilha A e o desvio<br/>sem avançar na entrada"]
    CONS -->|"aceitar"| OK["Reconhecimento concluído"]
    CONS -->|"célula vazia"| ERR["Erro de sintaxe no ponto<br/>mais cedo possível"]
    DESL --> CONS
    RED --> CONS
Figura 3: O laço dirigido por tabela, idêntico para qualquer gramática.

Dois detalhes são fonte recorrente de engano. A redução não consome entrada: o símbolo de antecipação que motivou a decisão continua lá e será reexaminado. E o estado consultado para o desvio é o que ficou exposto depois do desempilhamento, porque é o estado a partir do qual a variável recém-formada começou a ser construída.

Tracei id * id + id, e o momento decisivo é este: quando a pilha contém apenas T e a antecipação é *, o item completo E \to T \cdot está disponível e o analisador não reduz, porque a tabela manda deslocar; só com + à frente a redução por (2) acontece. As reduções, na ordem, foram (6), (4), (6), (3), (2), (6), (4), (1); lidas ao contrário regeneram a cadeia, e é assim que eu sei que o traçado está certo.

Como cada operação custa uma consulta constante, o analisador processa a entrada em tempo linear e sem voltar atrás — a mesma garantia do método anterior. O ganho da família ascendente não está em velocidade; está em quais gramáticas ela aceita.

E as células em branco não são desperdício: cada uma é um erro detectável, e o analisador nunca desloca um símbolo que torne inviável o conteúdo da pilha, anunciando o erro no primeiro token que não admite continuação válida. Nenhum método detecta mais cedo. Só não confunda detectar cedo com explicar bem: ali o estado representa várias produções simultaneamente, e a mensagem que a tabela sustenta sozinha é uma lista de terminais aceitáveis — verdadeira, e de utilidade limitada.

1.5 A escala dos quatro métodos

O que separa os métodos da família é uma pergunta só: o que fazer num estado que tem item completo e item de deslocamento ao mesmo tempo. Cada nível é uma resposta diferente, e a progressão é motivada — cada degrau existe porque o anterior falhou num caso concreto.

flowchart LR
    N0["Sem antecipação<br/>reduz sempre que houver<br/>item completo"]
    N1["Por seguidores globais<br/>reduz nas colunas<br/>dos seguidores"]
    N2["Com fusão de núcleos<br/>reduz nas antecipações<br/>locais fundidas"]
    N3["Canônico<br/>reduz nas antecipações<br/>locais exatas"]
    N0 -->|"falha nos estados com<br/>item completo e deslocamento"| N1
    N1 -->|"falha quando o seguidor<br/>global é generoso demais"| N2
    N2 -->|"pode criar conflito<br/>de redução-redução"| N3
    N0 -.->|"os três primeiros níveis<br/>compartilham os mesmos estados"| N2
Figura 4: Cada nível da família resolve o conflito que derrubou o anterior.

O nível mais fraco, o LR(0), decide sem olhar a entrada: se há item completo, reduz. Na nossa gramática ele quebra nos estados 1, 2 e 9. Praticamente nenhuma gramática de linguagem de programação o satisfaz, e a nossa, minúscula, já não satisfaz. O valor dele é conceitual: deixa visível o que falta ao autômato de itens.

O primeiro refinamento, o SLR(1), é o mais barato possível: olhe um símbolo à frente e só reduza por A \to \beta se esse símbolo puder seguir A. É o método da tabela acima, e os três conflitos somem. Ele reusa os estados anteriores, e os conjuntos de seguidores de que precisa já tinham sido calculados para o método descendente. Foi Frank DeRemer, na tese de doutorado de 1969 no MIT, quem sistematizou essa simplificação e a seguinte, e foram elas que tiraram a família do papel: as tabelas de Knuth eram grandes demais para as máquinas da época, e as simplificadas cabiam.

A limitação é que a aproximação é generosa demais: pergunta se o símbolo pode seguir A em algum lugar da gramática, quando o correto seria perguntar se pode segui-la naquele estado. O contraexemplo clássico é uma gramática mínima de atribuição com indireção, em que L designa o lado esquerdo de uma atribuição, R o lado direito, e uma produção diz que todo L serve de R. No estado alcançado pelo desvio por L há um item pedindo deslocamento de = e um item completo mandando reduzir L a R; como = está globalmente nos seguidores de R, as duas ações caem na mesma célula. O conflito é espúrio: ali reduzir nunca é correto, porque não existe forma sentencial começando com R =.

A correção, que é o LR(1) canônico de Knuth, é óbvia e cara: carregue dentro de cada item o conjunto de símbolos que podem segui-lo naquele contexto. Um item com antecipação é o par formado por um item e um terminal, e a segunda componente só tem efeito quando o item é completo, autorizando a redução apenas diante daquele símbolo. No exemplo acima o conflito some, porque o item completo entra com antecipação de fim de entrada e não com =. O preço é o tamanho, e é por isso que o método foi considerado impraticável por quase uma década.

A saída elegante é o LALR(1), segundo trabalho de DeRemer. O núcleo de um estado é o conjunto de itens sem as antecipações; estados distintos da coleção canônica frequentemente têm o mesmo núcleo, e a ideia é fundi-los, unindo as antecipações item a item. O resultado tem o número de estados do nível mais fraco com quase todo o poder do caro, e é o que os geradores usam.

O preço da fusão, delimitado com precisão

Fundir estados de mesmo núcleo nunca introduz conflito de deslocamento-redução que não existisse antes: os itens de deslocamento pertencem ao núcleo, idêntico em todos os estados fundidos, e a antecipação do item completo veio de um estado onde já coexistia com aquele deslocamento. Mas a fusão pode introduzir conflito de redução-redução, quando dois estados de mesmo núcleo têm antecipações disjuntas e trocadas entre si. Ainda assim o analisador fundido não aceita cadeias fora da linguagem: apenas adia a detecção do erro. A fusão degrada o diagnóstico, não a correção.

As letras desses nomes dizem o que o método faz: leitura da entrada da esquerda para a direita, produzindo uma derivação mais à direita, com o número de símbolos de antecipação entre parênteses.

A escala, com o que cada degrau custa e compra
Método Estados Critério de redução Poder relativo
LR(0) núcleos sempre que houver item completo o mais fraco; quase nenhuma gramática real
SLR(1) núcleos nas colunas dos seguidores globais resolve a maioria dos casos simples
LALR(1) núcleos nas antecipações locais fundidas o padrão dos geradores
LR(1) canônico muito mais nas antecipações locais exatas o mais forte com um símbolo

Duas consequências teóricas fecham a seção, ambas de Knuth. Aumentar a antecipação além de um símbolo não amplia a classe de linguagens alcançável. E a família com um símbolo caracteriza exatamente as linguagens livres de contexto determinísticas, as reconhecidas por autômato de pilha determinístico: o método é a realização algorítmica daquele modelo. Quanto às duas famílias, toda gramática tratável pelo descendente é tratável pelo ascendente canônico, e a recíproca é falsa, com a recursão à esquerda como contraexemplo.

1.6 Conflitos: ler, classificar, resolver

Um conflito é uma célula à qual a construção tenta atribuir dois valores. Há dois tipos, e a distinção é o melhor instrumento de diagnóstico que temos. Deslocamento-redução: um estado tem item completo cuja antecipação inclui um terminal e, ao mesmo tempo, um item que pede deslocamento desse terminal. Redução-redução: dois itens completos com antecipações que se intersectam.

A experiência associa a cada tipo uma causa provável — heurística, não lei. Deslocamento-redução costuma ser questão de poder ou de precedência. Redução-redução costuma ser questão de projeto da gramática: quase sempre indica duas categorias sintáticas indistinguíveis pela forma, cuja diferença real é semântica. Se a gramática declara duas variáveis distintas derivando o mesmo identificador, nenhum nível resolve, porque a informação necessária não está na sintaxe; o certo é fundir as produções e distinguir os usos depois, na tabela de símbolos.

E há uma distinção de natureza que, ignorada, custa horas. Quando a gramática não é ambígua, o problema é de informação, e um método mais fino resolve. Quando ela é ambígua, o problema é de resposta: existem genuinamente duas árvores, e nenhuma antecipação escolhe entre elas — um analisador determinístico produz uma única derivação mais à direita, e a gramática ambígua tem duas.

flowchart TD
    C["O gerador acusou um conflito"] --> T{"Que tipo?"}
    T -->|"deslocamento-redução"| A{"A gramática admite<br/>duas árvores para<br/>alguma cadeia curta?"}
    T -->|"redução-redução"| B["Suspeite primeiro do projeto<br/>da gramática: duas categorias<br/>indistinguíveis pela forma"]
    A -->|"não consegui construir"| P["Falta de informação:<br/>subir um nível na escala<br/>costuma resolver"]
    A -->|"consegui"| M["Ambiguidade genuína:<br/>nível nenhum resolve"]
    M --> M1["Reescrever a gramática<br/>desambiguando"]
    M --> M2["Declarar precedência<br/>e associatividade"]
    B --> B1["Fundir as produções e<br/>distinguir os usos na<br/>fase seguinte"]
    P --> V{"A contagem de conflitos<br/>caiu no relatório?"}
    M1 --> V
    M2 --> V
    B1 --> V
    V -->|"sim"| OK["Conflito resolvido"]
    V -->|"não"| NAO["A alteração apenas<br/>mudou o problema de lugar"]
Figura 5: O procedimento que converte um conflito de sintoma em causa.

O teste prático para separar os dois casos: tente construir uma cadeia curta com duas árvores. Conseguiu, é ambiguidade; não conseguiu, é aproximação grosseira, e subir de nível resolve.

O caso mais comum de ambiguidade genuína é o dos operadores escritos em um nível só, como E -> E + E | E * E | ( E ) | id. Há duas saídas. A primeira é reescrever desambiguando, que é o que a gramática de dois níveis faz: ao introduzir T e F, torna estruturalmente impossível agrupar a soma antes do produto. A segunda é manter a gramática ambígua e declarar precedência e associatividade, deixando o gerador resolver as células — convenção estabelecida pelo gerador que Stephen Johnson escreveu nos laboratórios Bell em meados dos anos 1970 e distribuiu com o Unix, em que os operadores são declarados em ordem crescente de precedência, com a associatividade de cada um; a não associatividade produz erro, e é assim que se recusa a < b < c.

Declarar precedência não é atalho para entender o conflito. É a mesma decisão, tomada em outro lugar. Quem declara sem examinar o estado conflituoso está resolvendo por sorte, e o sintoma clássico é o analisador que passa a compilar sem aviso e aceita programas cuja árvore não corresponde ao que quem os escreveu pretendia. O defeito aparece muitas fases depois, como resultado errado em tempo de execução.

O conflito mais famoso da literatura é o do condicional com alternativa opcional: com dois condicionais aninhados e uma única alternativa, ela pode pertencer ao interno ou ao externo. A convenção universal das linguagens é associá-la ao mais próximo, o que corresponde a deslocar — e a resolução padrão dos geradores, diante de um conflito de deslocamento-redução não declarado, é justamente preferir o deslocamento. Daí o conflito mais conhecido do assunto ser um que quase todo mundo resolve corretamente sem perceber, ignorando o aviso. Não recomendo o hábito: o aviso ignorado por sorte treina o hábito de ignorar avisos, e o próximo pode não ser sorte.

Chegamos, enfim, à habilidade que este módulo existe para formar. Um gerador, ao encontrar conflitos, resolve por regras padrão — deslocamento vence redução, e entre duas reduções vence a produção escrita primeiro — e avisa. O aviso é uma linha de resumo, e é onde a maioria para de ler. O que interessa está no relatório detalhado, que lista cada estado com seus itens e, nos conflituosos, a ação escolhida e a descartada: é o objeto que você construiu à mão, impresso por um programa.

O terceiro passo é o que quase ninguém faz. Localize o estado citado no aviso e leia seus itens; identifique o item completo e o que pede deslocamento, anotando o terminal em disputa; e então reconstrua um programa de exemplo que leve o analisador àquele estado, lendo os símbolos à esquerda dos pontos dos itens do núcleo. É ele que transforma uma lista de itens em uma construção concreta da linguagem, sobre a qual dá para decidir. Critério de verificação: um conflito genuinamente resolvido some do relatório.

1.7 As duas famílias, critério a critério

Com tudo construído, posso montar a comparação sem declarar vencedor antes do fim.

Os quatro critérios, sem vencedor declarado antes da hora
Critério Descendente recursivo Ascendente com gerador
Poder menor; exige eliminar recursão à esquerda e fatorar maior; nenhuma transformação da gramática
Legibilidade a estrutura do código é a da gramática laço genérico mais matriz de inteiros
Diagnóstico mensagem sai do nome da função, de graça detecção ótima, explicação pobre
Manutenção verificação por disciplina humana verificação automática a cada alteração

O argumento mais forte do lado ascendente vai além da economia de trabalho: uma gramática que não precisa ser transformada pode ser escrita na forma que expressa a semântica pretendida. No caminho descendente, a gramática implementada é uma versão contorcida da que se quis escrever, e a documentação costuma apresentar a original — discrepância permanente entre o documentado e o implementado.

Em legibilidade e diagnóstico a direção se inverte com folga. O analisador recursivo tem uma função por variável, e cada uma sabe o que está tentando reconhecer, de modo que a mensagem sai na forma “esperava tal coisa ao analisar tal construção”. No gerado, o ponto de parada cai dentro do laço genérico, e mensagens boas custam trabalho manual sobre os estados de erro, refeito sempre que a gramática muda.

Em manutenção o quadro é misto, e é a resposta menos óbvia. No caminho descendente, acrescentar uma construção exige verificar à mão que a condição de decisão continua satisfeita — o tipo de verificação que se pula quando o prazo aperta. No ascendente, acrescenta-se a produção e o gerador diz se quebrou: rede de segurança real, sobretudo em projetos com várias pessoas mexendo na gramática ao longo de anos. Em contrapartida, entender o que quebrou exige a habilidade da seção anterior. A rede avisa cedo e explica mal, que é o perfil da família inteira.

flowchart TD
    Q{"O que estou construindo?"}
    Q -->|"aprender a análise sintática<br/>construindo-a"| D["Descendente recursivo à mão"]
    Q -->|"compilador de produção com<br/>gramática que vai evoluir"| A["Ascendente com gerador"]
    D --> D1["Ganha: código legível<br/>correspondência direta com a gramática<br/>mensagens de erro naturais"]
    D --> D2["Paga: preparar a gramática<br/>verificar à mão a cada alteração"]
    A --> A1["Ganha: nenhuma transformação<br/>verificação automática do gerador<br/>gramática que expressa a semântica"]
    A --> A2["Paga: mensagens de erro trabalhadas à mão<br/>depuração via relatório de estados"]
Figura 6: O critério de escolha, com o que cada caminho ganha e o que cobra.

1.8 Síntese

A análise ascendente constrói a árvore das folhas para a raiz com duas operações apenas, e a pilha guarda o que já foi reconhecido — inversão exata em relação ao método anterior, e fonte de metade das confusões de quem estuda o assunto. A sequência de reduções lida ao contrário é uma derivação mais à direita, e essa correspondência é o teste mais barato de qualquer traçado feito à mão. O problema central é localizar a alça, e a resposta veio de um resultado surpreendente: o conjunto dos prefixos viáveis é regular, logo reconhecível por um autômato finito construído com itens, fecho e desvio. Sozinho, ele não decide entre deslocar e reduzir — daí a escala de quatro níveis, cada um resolvendo o conflito concreto que derrubou o anterior, com a fusão de núcleos como ponto de equilíbrio adotado pelos geradores. E sobre conflitos, a distinção que importa não é entre os dois tipos, e sim entre as duas causas: falta de informação sobe de nível, ambiguidade não sobe nível nenhum.

Volto, então, à pergunta da abertura. Se o objetivo é entender a análise sintática construindo-a, o caminho descendente recursivo é superior — não por ser mais fácil, mas porque o código é legível, as mensagens saem boas de graça e nada é delegado a uma ferramenta cujo interior permanece fechado. Se o objetivo é um compilador de produção com uma gramática que vai evoluir, o ascendente com gerador é superior, pela ausência de transformações e pela verificação automática a cada alteração. O que não muda entre os dois cenários é a fronteira do que é humano: as decisões de precedência continuam sendo decisões de projeto de linguagem, e o relatório de conflitos continua exigindo alguém capaz de ler itens e estados. A ferramenta automatiza a construção da tabela, não a compreensão de por que ela não pôde ser construída.

E fica a lição mais transferível do percurso. Passamos um módulo inteiro eliminando recursão à esquerda como se ela fosse defeito da gramática, e este descobrindo que ela era a forma preferida de outro método. Nenhuma das dificuldades que enfrentamos era dificuldade do problema; todas eram dificuldade da abordagem escolhida. Distinguir uma coisa da outra é o hábito mental que separa quem escolhe uma técnica de quem apenas aplica a primeira que aprendeu.