Professor: Gabriel Soares Baptista
Hoje você vai ver por que uma gramática pode parecer correta e ainda assim causar três problemas bem diferentes.
Antes dos problemas, precisamos fixar a leitura de uma gramática livre de contexto.
$$ G = (N, T, P, S) $$
Em cada passo, você escolhe um não terminal da forma atual e o substitui por um lado direito permitido.
Se ao final só restarem terminais, a derivação produziu uma cadeia da linguagem.
Se ainda sobrarem não terminais, você tem apenas uma forma sentencial intermediária.
Considere a gramática:
$$ G = (N, T, P, S) $$
$$ \begin{split} N &= \{S\} \\ T &= \{a, b\} \\ S &= S \end{split} \hspace{1cm} \begin{split} P = S &\to aS \mid b \end{split} $$
Para gerar $aaab$:
$$ \begin{split} S &\Rightarrow aS \\ &\Rightarrow aaS \\ &\Rightarrow aaaS \\ &\Rightarrow aaab \end{split} $$
Forma parcial possível:
$$ S \Rightarrow aS \Rightarrow aaS $$
Aqui $aaS$ ainda não é uma cadeia final porque sobra o não terminal $S$.
Em compiladores, não basta uma gramática gerar cadeias válidas. Ela precisa impor a estrutura correta e permitir uma análise sintática viável.
Uma gramática é ambígua quando a mesma cadeia admite mais de uma árvore sintática.
Isso não é apenas um detalhe visual.
Se a árvore muda, a estrutura muda. Se a estrutura muda, o significado também pode mudar.
Considere:
$$ G = (N, T, P, S) $$
$$ \begin{split} N &= \{E\} \\ T &= \{id, +\} \\ S &= E \end{split} \hspace{1cm} \begin{split} P = E &\to E + E \mid id \end{split} $$
Para a cadeia $id + id + id$, existe mais de uma leitura?
Sim.
Leitura 1
Leitura 2
Como as duas árvores são válidas para a mesma cadeia, a gramática é ambígua.
Considere a gramática:
$$ \begin{split} E &\to E + E \mid E * E \mid (E) \mid id \end{split} $$
Para a cadeia $id + id * id$, qual agrupamento a gramática impõe?
A multiplicação fica naturalmente mais funda que a soma ou a gramática ainda permite as duas leituras?
Ela ainda permite as duas:
Uma forma clássica de corrigir isso é separar os operadores por níveis de precedência.
Antes
$$ E \to E + E \mid E * E \mid (E) \mid id $$
Depois
$$ \begin{split} E &\to E + T \mid T \\ T &\to T * F \mid F \\ F &\to (E) \mid id \end{split} $$
Agora $*$ aparece em um nível mais profundo que $+$.
Para a cadeia $id + id * id$:
$$ \begin{split} E &\Rightarrow E + T \\ &\Rightarrow T + T \\ &\Rightarrow F + T \\ &\Rightarrow id + T \\ &\Rightarrow id + T * F \\ &\Rightarrow id + F * F \\ &\Rightarrow id + id * id \end{split} $$
Essa derivação não produz a estrutura $(id + id) * id$.
Precedência responde qual operador fica mais fundo.
Associatividade responde como operadores de mesma precedência se agrupam.
Como manter $+$ e $*$ à esquerda e tornar $\text{\textasciicircum}$ associativo à direita?
$$ \begin{split} E &\to E + T \mid T \\ T &\to T * P \mid P \\ P &\to F^{P} \mid F \\ F &\to (E) \mid id \end{split} $$
Como $P$ reaparece à direita em $P \to F^P$, a leitura induzida é:
$$ id^{(id^{id})} $$
O problema também aparece em comandos.
Considere:
$$ \begin{split} S &\to \text{if } c\ \text{then } S \\ &\mid \text{if } c\ \text{then } S\ \text{else } S \\ &\mid a \end{split} $$
e a cadeia:
$$ \text{if } c\ \text{then if } c\ \text{then } a\ \text{else } a $$
Else no if interno
Else no if externo
Sem uma reescrita apropriada, a gramática permite as duas interpretações.
Separamos comandos em duas categorias.
$$ \begin{split} S &\to \text{Matched} \mid \text{Unmatched} \\ \text{Matched} &\to \text{if } c \text{ then Matched else Matched} \mid a \\ \text{Unmatched} &\to \text{if } c \text{ then } S \mid \text{if } c \text{ then Matched else Unmatched} \end{split} $$
Essa organização codifica a regra prática do compilador.
else fecha o if mais próximo ainda não fechadoAgora o problema já não é de interpretação.
Ele é operacional.
Uma produção como
$$ A \to A\alpha \mid \beta $$
faz um parser descendente chamar $A$ de novo antes de consumir entrada.
Considere:
$$ \begin{split} E &\to E + T \mid T \\ T &\to id \end{split} $$
Por que isso trava um parser descendente?
Porque a expansão pode ficar assim:
$$ E \Rightarrow E + T \Rightarrow E + T + T \Rightarrow E + T + T + T \Rightarrow \cdots $$
O parser volta para $E$ antes de consumir qualquer token.
O molde clássico é:
Antes
$$ A \to A\alpha \mid \beta $$
Depois
$$ \begin{split} A &\to \beta A' \\ A' &\to \alpha A' \mid \epsilon \end{split} $$
Primeiro o parser reconhece uma base segura. Depois ele processa as repetições.
Elimine a recursão à esquerda de:
$$ A \to Aa \mid b $$
$$ \begin{split} A &\to bA' \\ A' &\to aA' \mid \epsilon \end{split} $$
Agora o parser consome primeiro $b$ e depois aceita zero ou mais $a$.
A gramática abaixo já resolve precedência e associatividade, mas ainda não serve bem para parser top-down.
$$ \begin{split} E &\to E + T \mid E - T \mid T \\ T &\to T * F \mid T / F \mid F \\ F &\to (E) \mid id \end{split} $$
O problema restante é a recursão à esquerda em $E$ e $T$.
$$ \begin{split} E &\to T\ E' \\ E' &\to +\ T\ E' \mid -\ T\ E' \mid \epsilon \\ T &\to F\ T' \\ T' &\to *\ F\ T' \mid /\ F\ T' \mid \epsilon \\ F &\to (E) \mid id \end{split} $$
Essa transformação preserva a linguagem e remove o obstáculo operacional para o parser descendente.
O terceiro problema surge quando duas produções começam com o mesmo prefixo.
O parser olha a entrada e ainda não consegue decidir qual regra usar.
$$ A \to \alpha\beta_1 \mid \alpha\beta_2 $$
Antes
$$ A \to \alpha\beta_1 \mid \alpha\beta_2 $$
Depois
$$ \begin{split} A &\to \alpha A' \\ A' &\to \beta_1 \mid \beta_2 \end{split} $$
A ideia é simples. Primeiro consumimos a parte comum. Depois escolhemos a continuação.
Considere:
$$ A \to ab \mid ac $$
O parser consegue decidir entre as duas produções só olhando o primeiro símbolo?
Não.
As duas começam com $a$, então fatoramos:
$$ \begin{split} A &\to aA' \\ A' &\to b \mid c \end{split} $$
Considere:
$$ D \to \text{int } id\ ; \mid \text{int } id = \text{num}\ ; $$
O prefixo comum é $\text{int } id$.
$$ \begin{split} D &\to \text{int } id\ D' \\ D' &\to ; \mid = \text{num} ; \end{split} $$
Agora a decisão fica para depois da parte compartilhada.
Fatoração à esquerda melhora a previsibilidade local do parser. Ela não garante, sozinha, interpretação semântica única.
Agora que a gramática já está preparada para análise descendente, o próximo passo é estudar o algoritmo que explora exatamente essa estrutura. Na próxima aula, Recursive Descent Parsing, vamos transformar não terminais em funções, ver como o parser consome tokens com match e implementar o primeiro analisador sintático recursivo.