Compiladores

Gramáticas

Professor: Gabriel Soares Baptista

Introdução

Hoje você vai ver por que uma gramática pode parecer correta e ainda assim causar três problemas bem diferentes.

  • Ela pode aceitar a mesma cadeia com mais de uma interpretação.
  • Ela pode fazer um parser descendente entrar em loop.
  • Ela pode obrigar o parser a decidir cedo demais qual produção seguir.

A base formal mínima

Antes dos problemas, precisamos fixar a leitura de uma gramática livre de contexto.

$$ G = (N, T, P, S) $$

  • $N$: não terminais, como $E$, $T$ e $Cmd$
  • $T$: terminais, isto é, os tokens da linguagem
  • $P$: produções, como $A \to \alpha$
  • $S$: símbolo inicial

Como ler uma derivação

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.

Exemplo mínimo de derivação

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} $$

Derivando uma cadeia

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$.

Três problemas clássicos

Ideia central

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.

  • Ambiguidade: mais de uma árvore para a mesma cadeia
  • Recursão à esquerda: loop em parser top-down
  • Fatoração à esquerda: prefixo comum atrapalha a escolha da produção

Ambiguidade

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.

O primeiro sinal de ambiguidade

Questão

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.

  • $(id + id) + id$
  • $id + (id + id)$

Duas árvores para a mesma cadeia

Leitura 1

AST error: name 'hashlib' is not defined

Leitura 2

AST error: name 'hashlib' is not defined

Como as duas árvores são válidas para a mesma cadeia, a gramática é ambígua.

Quando a ambiguidade muda o significado

Questão

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?

Reflita

A multiplicação fica naturalmente mais funda que a soma ou a gramática ainda permite as duas leituras?

Ela ainda permite as duas:

  • $(id + id) * id$
  • $id + (id * id)$

Desambiguando por níveis

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 $+$.

O que essa reescrita força

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$.

Associatividade também importa

Precedência responde qual operador fica mais fundo.

Associatividade responde como operadores de mesma precedência se agrupam.

  • à esquerda: $a - b - c = (a - b) - c$
  • à direita: $a^{b^{c}} = a^{(b^{c})}$

Potência associativa à direita

Questão

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})} $$

Ambiguidade fora das expressões

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 $$

O caso do dangling else

Else no if interno

AST error: name 'hashlib' is not defined

Else no if externo

AST error: name 'hashlib' is not defined

Sem uma reescrita apropriada, a gramática permite as duas interpretações.

Como desambiguar o else

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.

  • todo else fecha o if mais próximo ainda não fechado

Recursão à esquerda

Agora 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.

Onde o loop aparece

Questão

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.

Eliminando a recursão à esquerda

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.

Exemplo simples de eliminação

Questão

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$.

O caso clássico das expressões

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$.

Expressões sem recursão à esquerda

$$ \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.

Fatoração à esquerda

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 $$

A ideia da fatoração

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.

Exemplo direto de fatoração

Questão

Considere:

$$ A \to ab \mid ac $$

Reflita

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} $$

Exemplo de declaração

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.

Fatorar não é desambiguar

Cuidado conceitual

Fatoração à esquerda melhora a previsibilidade local do parser. Ela não garante, sozinha, interpretação semântica única.

  • Desambiguação: resolve mais de uma árvore para a mesma cadeia
  • Eliminação de recursão à esquerda: evita loop em parser descendente
  • Fatoração à esquerda: adia uma decisão local entre produções com prefixo comum

Colocando lado a lado

Aula9GGramática comalgum problemaAAmbiguidadeG->ARRecursãoà esquerdaG->RNNão determinismolocalG->ND1Desambiguação(precedência + associatividade)A->D1D2Eliminação darecursão à esquerdaR->D2D3Fatoraçãoà esquerdaN->D3

Próximos Passos

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.