LRM Prof. Mantovani ← Aulas da disciplina
Aula 6 de 14

Análise sintática descendente

Parsing recursivo-descendente: como percorrer a gramática, da raiz às folhas, para reconstruir a árvore sintática a partir dos tokens. Estudamos por que a recursão à esquerda quebra o método, como eliminá-la, e como a fatoração à esquerda prepara a gramática.

📚 Compiladores🧪 7 simulador(es)📝 mini-quiz ao final
Objetivos da aula

O que você vai aprender

1

Explicar o parsing recursivo-descendente e o método de um procedimento por não-terminal.

2

Diagnosticar e eliminar recursão à esquerda.

3

Aplicar fatoração à esquerda.

4

Reconhecer essas transformações como pré-requisitos de LL(1).

1 · Motivação

Reconstruir a árvore

A gramática gera cadeias. Mas o compilador recebe a cadeia (os tokens) e precisa do caminho inverso: reconstruir a árvore sintática. Esse é o trabalho do parser.

Há duas estratégias gerais: descendente (de cima para baixo) e ascendente (de baixo para cima). Esta aula foca na descendente, intuitiva e fácil de programar à mão.

🔑
Parsing = transformar uma sequência de tokens na árvore sintática que a gramática associa a ela.
2 · Mapa

O que veremos nesta aula

Top-down→Recursivo-descendente→Recursão à esquerda→Fatoração
  • A estratégia descendente.
  • Um procedimento por não-terminal.
  • Por que a recursão à esquerda quebra tudo e como removê-la.
  • A fatoração à esquerda.
3 · Retrospectiva

Onde estamos: as seis aulas até aqui

Antes de entrar na descida recursiva, vale parar um minuto e olhar para trás. As seis aulas até aqui não são seis assuntos soltos: são seis elos de uma corrente que termina exatamente no que esta aula faz — pegar uma fila de tokens e devolver a árvore sintática.

fases→a máquina certa→tokens→o scanner que roda→a gramática→o parser

Cada aula entregou uma peça que a seguinte usa como matéria-prima. Lendo a tabela da esquerda para a direita você vê o objetivo; lendo a última coluna você vê por que a aula precisou existir.

AulaObjetivoO que ela entrega para a seguinteO que faltaria sem ela
1
Introdução à compilação
Entender o que é um compilador, como ele difere de um interpretador e o panorama das fases.O modelo de fases — e, com ele, um erro que tem dono: cada fase reclama só do que é dela.A única mensagem possível seria “erro no programa”, sem dizer o que nem onde.
2
Linguagens, gramáticas e estrutura do compilador
Compreender os níveis de abstração, a organização front-end/back-end e a hierarquia de Chomsky.A régua que diz de que máquina cada parte precisa: token é tipo 3 (autômato finito), aninhamento é tipo 2 (pilha).Tentaríamos analisar sintaxe com expressão regular — e ela aceita programa desbalanceado.
3
Análise léxica I
Aprender a transformar caracteres em tokens, dominando a tríade lexema/token/padrão e as expressões regulares.O token: total deixa de ser cinco caracteres e vira um símbolo só, id.O analisador sintático receberia caracteres — e a gramática dele não fala em caracteres.
4
Análise léxica II
Entender como expressões regulares viram código executável (ER → AFN → AFD → código).O scanner que roda — e com ele o maior casamento, que faz >= ser um token e não dois.A expressão regular ficaria escrita e correta, mas sem máquina que a execute direito.
5
Gramáticas livres de contexto
Dominar GLCs, derivações, árvores sintáticas, ambiguidade e as notações BNF/EBNF.A gramática que dá uma árvore só — com a precedência morando na forma da árvore.A mesma cadeia teria duas árvores e dois valores: a tradução deixaria de estar definida.
6
Análise sintática descendente
Aprender o parsing recursivo-descendente e como preparar a gramática (eliminar recursão à esquerda, fatorar).O parser: um procedimento por não-terminal, que reconstrói a árvore da raiz às folhas.A gramática ficaria correta no papel e o parser giraria para sempre na primeira produção.
🔑
A pergunta que atravessa as seis aulas é sempre a mesma: que informação eu preciso ter para dar o próximo passo, e qual máquina consegue produzi-la? A resposta muda de aula para aula — tabela de reservadas, autômato, gramática, pilha de chamadas — mas a pergunta é uma só.

A bancada abaixo põe isso à prova: a mesma entrada atravessa as seis aulas, e você pode desligar qualquer uma delas para ver onde a corrente arrebenta.

4 · Definição

Análise descendente (top-down)

Parsing descendente. Constrói a árvore sintática da raiz (símbolo inicial) para as folhas (terminais), tentando casar a entrada com as produções, escolhendo qual produção aplicar a cada passo.

"Descendente" porque parte do topo da árvore e desce, prevendo a estrutura antes de ver todos os tokens.

5 · Aprofundamento

Um procedimento por não-terminal

No recursivo-descendente, cada não-terminal vira uma função. A função tenta reconhecer a parte da entrada que aquele não-terminal gera, chamando as funções dos outros não-terminais conforme a produção.

// um procedimento por nao-terminal função E(): T(); E1() função E1(): se token == '+': consome('+'); T(); E1() função T(): F(); T1() função F(): se token=='(' : consome('('); E(); consome(')') senão consome(id)
6 · Exemplo

Reconhecendo "id + id"

Acompanhe as chamadas para a entrada id + id, com a gramática E → T E', E' → + T E' | ε:

ChamadaToken atualAção
E → T E'idchama T
T → ididconsome id
E' → + T E'+consome +, chama T
T → ididconsome id
E' → εfimretorna
7 · Interativo

Passo a passo: a pilha de chamadas

Passo 1
Chama E(). Para reconhecer E, chama T() e depois E1().
Passo 2
Dentro de T(), chama F(). F vê o token id e o consome.
Passo 3
De volta em T(), chama T1(). O token seguinte não é '*', então T1 deriva ε e retorna.
Passo 4
De volta em E(), chama E1(). Não há '+', E1 deriva ε. A entrada foi reconhecida.

Abaixo, a mesma descida ao vivo: escolha uma cadeia e acompanhe a pilha de chamadas, a fita e a árvore crescendo da raiz — inclusive nas cadeias inválidas, para ver onde o erro nasce.

8 · Comparação

Gerar × reconhecer: a aula 5 e a aula 6 lado a lado

Na aula 5 você gerava: partia de E e escolhia as produções sabendo aonde queria chegar. O parser faz o caminho inverso: recebe a cadeia pronta, vinda do léxico, e precisa descobrir se existe uma derivação que a produza — e qual. Ele não pode escolher olhando a resposta: a cada passo, enxerga só o token sob a cabeça.

Aula 5 · gerarAula 6 · reconhecer
Ponto de partidao símbolo inicial Ea cadeia de tokens, inteira na fita
Quem escolhe a produçãoquem deriva, conhecendo o alvo inteiroo token sob a cabeça (o FIRST de cada alternativa)
Um terminal na árvorenasce escrito, quando a produção é aplicadanasce previsto e só vale depois de conferido na fita
Erro de sintaxenão existe: quem gera nunca escreve uma cadeia erradanasce num consome() que não bate, ou num procedimento sem alternativa para o token
A gramáticaE → E + T | T serveprecisa ser reescrita: E → T E', E' → + T E' | ε

A reescrita não muda a linguagem: as mesmas cadeias continuam aceitas. Muda só a forma das produções, para que cada procedimento avance na entrada antes de chamar a si mesmo — é o assunto das próximas seções. Quando você vê E(), T() e E1() no pseudocódigo lá em cima, não é uma gramática nova saída do nada: é a gramática de expressões da aula 5, saneada para a descida.

🔑
O procedimento de verificação da aula 6 é a derivação da aula 5 executada como programa: a pilha de chamadas faz o papel da forma sentencial.

A bancada abaixo põe as duas lado a lado sobre a mesma cadeia. Repare em três coisas: a forma sentencial é a mesma nas duas colunas em todo passo — do lado da aula 6 ela é lida da pilha de chamadas, não de uma derivação; o mesmo terminal nasce escrito à esquerda e previsto à direita, e só fica verde quando a cabeça passa por ele; e, trocando para a gramática original da aula 5, veja em que linha as colunas se separam — e por quê.

9 · Definição

Recursão à esquerda

Recursão à esquerda. Ocorre quando um não-terminal pode, em uma derivação, gerar a si mesmo como símbolo mais à esquerda (ex.: A → A α). Direta na produção, ou indireta via outros não-terminais.
10 · Aprofundamento

Por que ela quebra o parser

⚠️
A produção E → E + T faz a função E() chamar E() imediatamente, sem consumir nenhum token — um laço infinito. O parser descendente trava antes de ler qualquer coisa.

A solução é reescrever a gramática, transformando a recursão à esquerda em recursão à direita:

Com recursão à esquerdaReescrita (à direita)
E → E + T | TE → T E'
E' → + T E' | ε
11 · Analogia

A porta giratória infinita

🔧 Analogia
Recursão à esquerda é como uma porta giratória em que você empurra mas nunca avança: a função chama a si mesma sem "andar" na entrada, girando para sempre. A reescrita coloca um obstáculo que força você a dar um passo (consumir um token) antes de girar de novo.

Na bancada abaixo, veja a porta girar de verdade: E() chamando E() com a cabeça parada até a pilha estourar — e a reescrita passando na mesma cadeia. Na segunda aba, o detector encontra também a recursão indireta, a que é mais difícil de enxergar, e a elimina com o algoritmo do livro.

12 · Comparação

Descendente × ascendente

Descendente (top-down)Ascendente (bottom-up)
Constrói a árvoreDa raiz às folhasDas folhas à raiz
DecisãoQual produção aplicarQuando reduzir
Recursão à esquerdaProíbeAceita
ExemploLL(1), recursivo-descendenteLR(1), LALR
13 · Fluxo

Preparando a gramática

Antes de escrever um parser descendente, a gramática passa por duas transformações de saneamento:

Gramática original→Eliminar recursão à esquerda→Fatorar à esquerda→Parser
14 · Código

O mesmo analisador, escrito à mão

A tabela preditiva pode virar código direto — uma função por não-terminal. É o método da descida recursiva:

// E → T E' void E() { T(); Elinha(); } // E' → + T E' | ε void Elinha() { se (olhar() == '+') { casar('+'); T(); Elinha(); } // senão: ε — não consome nada e retorna } // F → ( E ) | id void F() { se (olhar() == '(') { casar('('); E(); casar(')'); } senão se (olhar() == ID) casar(ID); senão erroSintatico("esperava id ou ("); }

Compare com o simulador: a pilha explícita da tabela vira, aqui, a pilha de chamadas da própria linguagem. São o mesmo algoritmo com implementações diferentes.

15 · Aprofundamento

Fatoração à esquerda

Quando duas produções de um não-terminal começam igual, o parser não sabe, ao ver o prefixo comum, qual escolher. A fatoração à esquerda adia a decisão até depois do prefixo:

AntesDepois
S → if E then S | if E then S else SS → if E then S S'
S' → else S | ε

Agora há uma só produção começando com "if E then S", e a escolha do else fica isolada em S'.

Na bancada abaixo, o prefixo comum é diagnosticado sobre a gramática que estiver na tela, a fatoração é aplicada com um clique e a mesma cadeia roda no parser antes e depois: antes ela falha, depois passa — sem que a linguagem tenha mudado.

16 · Simulador

A pilha do analisador preditivo, passo a passo

Aqui você acompanha o analisador trabalhando: de um lado a pilha, do outro a entrada restante, e a cada passo a produção aplicada ou o token casado.

Comece com id + id * id e avance passo a passo. Depois experimente as cadeias inválidas: id + (falta operando) e id id (falta operador). Observe onde exatamente a análise trava — a mensagem de erro de um compilador nasce desse ponto.

💡
O analisador nunca volta atrás: topo da pilha mais um símbolo de entrada bastam para decidir. É a definição operacional de LL(1) — e a razão de a gramática precisar estar sem recursão à esquerda e fatorada.
17 · Verifique

Verifique você mesmo: precisa fatorar?

A produção A → a b | a c precisa de fatoração à esquerda?

Ao ver 'a', o parser não sabe se seguirá 'b' ou 'c'. Fatorando: A → a A', A' → b | c. A decisão se adia para depois do prefixo comum.
18 · Caso prático

Parsers escritos à mão

Muitos compiladores reais (incluindo versões de GCC e Clang) usam parsers recursivo-descendentes escritos à mão.

💡
A vantagem é o controle fino sobre mensagens de erro e recuperação. Como cada não-terminal é uma função, é fácil inserir lógica de diagnóstico específica em cada ponto da gramática.
19 · Erros comuns

Armadilhas do parser descendente

⚠️
Cuidado. Recursão à esquerda indireta (A → B..., B → A...) também causa laço infinito e é mais difícil de enxergar. E eliminar recursão à esquerda não dispensa fatorar: são problemas diferentes, e a gramática pode precisar das duas transformações.
20 · Boas práticas

Ordem das transformações

✅
Dica. Elimine a recursão à esquerda primeiro, depois fatore. A ordem importa: eliminar recursão pode introduzir novos prefixos comuns que precisarão de fatoração. Fazer na ordem inversa pode deixar recursão escondida.

Ponha isso à prova na bancada abaixo: oito gramáticas para diagnosticar e uma oficina em que você aplica as duas transformações na ordem que quiser, com o diagnóstico refeito a cada passo.

21 · Revelar

Reflita: descendente x ascendente

Se o parser descendente proíbe recursão à esquerda, por que ela é tão comum nas gramáticas?
Porque a recursão à esquerda expressa naturalmente a associatividade à esquerda (a - b - c = (a - b) - c), que é a mais comum nos operadores. Parsers ascendentes (LR) aceitam recursão à esquerda diretamente, o que é uma de suas vantagens. No método descendente, pagamos o preço de reescrever a gramática para recursão à direita e depois reconstruir a associatividade correta nas ações semânticas.
22 · Flashcards

Termos-chave da aula

Top-downvirar
Estratégia que constrói a árvore da raiz para as folhas.
Recursivo-descendentevirar
Parser top-down com uma função por não-terminal.
Recursão à esquerdavirar
A → A α: causa laço infinito no parser descendente; deve ser eliminada.
Fatoração à esquerdavirar
Isola o prefixo comum de produções, adiando a decisão do parser.
23 · Conexões

Onde isto se encaixa no curso

Esta aula é o elo entre gramática e algoritmo:

  • Vem da aula 5: usa a gramática de expressões e a noção de árvore.
  • Prepara a aula 7: eliminar recursão e fatorar são pré-condições para LL(1) e a tabela preditiva.
  • Conecta com a aula 8: as funções do parser serão o lugar das ações semânticas (tradução dirigida pela sintaxe).
24 · Síntese

Resumo visual

🔑
Parser descendente constrói a árvore da raiz às folhas; o recursivo-descendente usa uma função por não-terminal. Recursão à esquerda causa laço infinito e precisa ser eliminada. Fatoração à esquerda resolve prefixos comuns. Ambas são pré-requisitos para LL(1).
Mão na massa · colaborativo

Atividade em grupo · Refatorando gramáticas

Em trios, deixem uma gramática pronta para parsing descendente.

⏱️ 25 min👥 trios🧩 exercício

Roteiro

  1. Dada A → A α | β, eliminem a recursão à esquerda.
  2. Dada B → x y | x z, façam a fatoração à esquerda.
  3. Escrevam o procedimento recursivo-descendente para a gramática resultante.
  4. Testem o procedimento com uma entrada de exemplo.
Refatoradorreescreve as produções
Codificadorescreve os procedimentos
Verificadortesta com uma entrada
📤 Entrega: Gramática refatorada + pseudocódigo dos procedimentos.
Teste seu conhecimento

Mini-quiz · Aula 6

20 questões sobre esta aula. Escolha e veja a explicação na hora.

0/20

📌 Resumo — leve isto para a prova

  • Parser descendente constrói a árvore da raiz às folhas; recursivo-descendente usa uma função por não-terminal.
  • Gerar (aula 5) e reconhecer (aula 6) chegam à mesma árvore, na mesma ordem: o parser escolhe cada produção pelo token sob a cabeça, e a pilha de chamadas faz o papel da forma sentencial.
  • Recursão à esquerda (direta ou indireta) causa laço infinito e precisa ser eliminada.
  • Fatoração à esquerda resolve prefixos comuns, adiando a decisão.
  • Elimine a recursão à esquerda antes de fatorar.
  • Ambas são pré-requisitos para LL(1).