Professor: Gabriel Soares Baptista
Considere a entrada:
x + 3 * y
O lexer responde bem a perguntas como estas:
Mas ele não responde a perguntas como estas:
+ ou *3 * y precisa ser resolvido antes da somaO lexer vê algo como:
IDENT PLUS NUMBER STAR IDENT
O parser precisa reconstruir a hierarquia implícita nessa sequência.
Leitura incorreta:
(x + 3) * y
Leitura correta:
x + (3 * y)
Toda gramática será lida como:
$$ G = (N, T, P, S) $$
Se a derivação sempre começa em algum lugar, qual símbolo da gramática precisa estar fixado antes de qualquer análise?
Considere:
$$ G = (N, T, P, S) $$
$$ \begin{split} N &= \{S\} \\ T &= \{0, 1\} \\ S &= S \end{split} \hspace{1cm} \begin{split} P = S &\to 0S \mid 1 \end{split} $$
Essa gramática gera cadeias com zero ou mais 0 seguidos de um 1 final.
Para gerar 0001:
$$ \begin{split} S &\Rightarrow 0S \\ &\Rightarrow 00S \\ &\Rightarrow 000S \\ &\Rightarrow 0001 \end{split} $$
A derivação começa em $S$ e termina apenas quando restam terminais.
Uma derivação também pode parar no meio.
$$ S \Rightarrow 0S \Rightarrow 00S $$
Isso ainda não é uma cadeia final da linguagem.
O motivo é simples:
Em compiladores, não basta a gramática gerar cadeias válidas.
Ela também precisa:
Três problemas aparecem com frequência:
Uma gramática é ambígua quando a mesma cadeia admite mais de uma árvore sintática válida.
Isso muda a estrutura da expressão e pode mudar também o significado.
Considere:
$$ E \to E + E \mid E * E \mid id $$
Para a cadeia:
$$ id + id * id $$
as duas leituras abaixo continuam possíveis:
Uma solução clássica é separar os operadores por níveis de precedência.
$$ \begin{split} E &\to E + T \mid T \\ T &\to T * F \mid F \\ F &\to (E) \mid id \end{split} $$
Agora * fica em um nível mais profundo do que +.
Considere:
$$ E \to E + E \mid id $$
A cadeia id + id + id admite mais de uma leitura?
Sim.
As leituras clássicas são:
Logo a gramática é ambígua.
Uma gramática tem recursão à esquerda quando existe uma derivação do tipo $A \Rightarrow^+ A\alpha$, isto é, quando um não terminal volta para si mesmo no começo da derivação, antes de consumir terminais.
Exemplo:
$$ \begin{split} E &\to E + T \mid T \\ T &\to id \end{split} $$
Esse formato é ruim para parser top-down porque a análise retorna ao mesmo não terminal antes de consumir entrada.
Se a regra for implementada literalmente, o comportamento conceitual é:
void E() {
E();
match('+');
T();
}
O parser entra em loop recursivo e não avança na entrada.
Uma forma completa e simples fica assim:
$$ \begin{split} E &\to T E' \\ E' &\to + T E' \mid \epsilon \\ T &\to id \end{split} $$
Agora a expansão começa por um pedaço que pode consumir entrada.
O papel de T aqui é só deixar a gramática fechada e mostrar um terminal concreto entrando na derivação.
Considere:
$$ A \to A a \mid b $$
Reescreva a gramática sem recursão à esquerda.
$$ \begin{split} A &\to b A' \\ A' &\to a A' \mid \epsilon \end{split} $$
Agora o parser reconhece primeiro b e depois continua a repetição por A'.
Esse problema aparece quando duas produções começam com o mesmo prefixo.
Exemplo:
$$ A \to ab \mid ac $$
Com lookahead em apenas a, o parser ainda não sabe qual produção escolher.
Retiramos o prefixo compartilhado e adiamos a decisão:
$$ \begin{split} A &\to a A' \\ A' &\to b \mid c \end{split} $$
Agora o parser consome a primeiro e decide depois.
Considere:
$$ \begin{split} Stmt &\to if\ Expr\ then\ Stmt\ else\ Stmt \\ &\mid if\ Expr\ then\ Stmt \end{split} $$
Fatore a gramática pelo prefixo comum.
O prefixo comum é:
$$ if\ Expr\ then\ Stmt $$
Uma fatoração possível é:
$$ \begin{split} Stmt &\to if\ Expr\ then\ Stmt\ S' \\ S' &\to else\ Stmt \mid \epsilon \end{split} $$
Classifique o problema principal de cada gramática.
$$ E \to E + E \mid id $$
$$ A \to A a \mid b $$
$$ S \to aB \mid aC $$
Ambiguidade.
Recursão à esquerda.
Fatoração à esquerda.
As duas são árvores, mas servem a papéis diferentes.
Se a sua pergunta é “como a gramática derivou a cadeia”, a parse tree ajuda.
Se a sua pergunta é “qual é a estrutura útil para verificar, otimizar ou gerar código”, a AST é a resposta.
a + b * cEssa árvore ainda carrega muita estrutura da gramática.
a + b * cAgora a árvore mostra apenas o que importa para a semântica da expressão.
Quando esses dois papéis se misturam, normalmente três coisas se confundem:
Resumo:
Uma primeira tentativa costuma ser:
$$ E \to E + T \mid T $$
Ela parece natural, mas é ruim para parser top-down.
O problema é a recursão à esquerda.
Se a implementação seguir a gramática literalmente, teremos algo conceitualmente assim:
void E() {
E();
match('+');
T();
}
O parser volta para E() antes de consumir qualquer token.
Resultado:
Uma forma clássica para expressões é:
$$ \begin{split} E &\to T\ E' \\ E' &\to +\ T\ E' \mid \epsilon \\ T &\to F\ T' \\ T' &\to *\ F\ T' \mid \epsilon \\ F &\to (E) \mid id \end{split} $$
Essa reescrita faz duas coisas ao mesmo tempo:
No parser recursivo descendente:
match consome terminais esperadosMapa direto:
E vira parse_expressionT vira parse_termF vira parse_factor ou parse_primaryAstNode *parse_expression(Parser *parser) {
AstNode *left = parse_term(parser);
while (match(parser, TOK_PLUS)) {
Token op = previous(parser);
AstNode *right = parse_term(parser);
left = make_binary(op, left, right);
}
return left;
}
O parser não apenas reconhece a estrutura.
Ele constrói a AST enquanto reconhece.
No laboratório do compilador, a AST guarda:
kindtokenfirst, second e thirdchildrenEssa escolha simplifica o parser porque evita muito código cerimonial.
Vamos acompanhar a entrada:
x + 3 * y
Usaremos a ideia geral:
$$ E \to T\ E' \qquad T \to F\ T' \qquad F \to id \mid num \mid (E) $$
Queremos observar duas coisas ao mesmo tempo:
Entrada atual:
x + 3 * y $
^
Neste ponto, parse_primary() reconhece x e devolve um nó id(x).
Depois de reconhecer x, o parser sobe um nível e olha o próximo token.
Entrada atual:
x + 3 * y $
^
Como + pertence ao nível de E', o parser decide continuar nesse nível e parsear o operando da direita.
Agora o lado direito começa em 3 * y.
Entrada atual:
x + 3 * y $
^
Aqui o parser resolve 3 * y antes de voltar ao nível da soma.
Essa árvore codifica a precedência sem exigir uma regra extra no algoritmo.
Em outras palavras, a forma da gramática controla a forma da árvore.
Considere a entrada inválida:
x +
O parser reconhece x, consome + e então precisa de outro operando.
Nesse momento, a função responsável pelo nível seguinte falha porque a entrada acabou antes do esperado.
Mensagem conceitual:
Para fechar a revisão do módulo, vale lembrar a diferença de direção:
Nesta revisão, o foco é top-down porque ele conecta diretamente:
Como o parser escolhe a produção certa sem voltar atrás?
O LL(1) organiza essa decisão por meio de três elementos:
Não é apenas uma sigla.
Ela resume exatamente a estratégia operacional do parser.
O conjunto $FIRST(A)$ responde:
Exemplo mínimo:
$$ A \to b \mid \epsilon $$
$$ FIRST(A) = \{b, \epsilon\} $$
O conjunto $FOLLOW(A)$ responde:
Lembretes importantes:
FOLLOW$ sempre entra em $FOLLOW(S)$, onde $S$ é o símbolo inicialConsidere:
$$ \begin{split} S &\to A d \\ A &\to bA \mid \epsilon \end{split} $$
Primeiro o FIRST.
$$ FIRST(A) = \{b, \epsilon\} $$
Como $A$ pode sumir, o começo de $S$ pode ser b ou d.
$$ FIRST(S) = \{b, d\} $$
Ainda na gramática:
$$ \begin{split} S &\to A d \\ A &\to bA \mid \epsilon \end{split} $$
Depois de $A$ vem d, então:
$$ FOLLOW(A) = \{d\} $$
Como $S$ é o símbolo inicial:
$$ FOLLOW(S) = \{\$\} $$
Para cada produção $A \to \alpha$:
Não existe coluna de vazio. O papel do $\epsilon$ é mandar a produção para o contexto de FOLLOW.
Considere:
$$ \begin{split} S &\to aB \mid c \\ B &\to b \mid \epsilon \end{split} $$
Conjuntos:
$$ FIRST(S)=\{a,c\}, \qquad FIRST(B)=\{b,\epsilon\} $$
$$ FOLLOW(S)=\{\$\}, \qquad FOLLOW(B)=\{\$\} $$
$$ \begin{array}{c|cccc} & a & b & c & \$ \\ \hline S & S \to aB & & S \to c & \\ B & & B \to b & & B \to \epsilon \end{array} $$
Cada célula preenchida representa uma decisão local única.
Se alguma célula recebesse duas produções, a gramática deixaria de ser LL(1).
Gramática:
$$ \begin{split} E &\to T E' \\ E' &\to + T E' \mid \epsilon \\ T &\to F T' \\ T' &\to * F T' \mid \epsilon \\ F &\to (E) \mid id \end{split} $$
Conjuntos principais:
$$ FIRST(E)=FIRST(T)=\{(, id\}, \qquad FIRST(E')=\{+,\epsilon\} $$
$$ FOLLOW(E)=\{\$, )\}, \qquad FOLLOW(T)=\{+,\$, )\} $$
$$ \begin{array}{c|cccccc} & id & + & * & ( & ) & \$ \\ \hline E & E \to T E' & & & E \to T E' & & \\ E' & & E' \to + T E' & & & E' \to \epsilon & E' \to \epsilon \\ T & T \to F T' & & & T \to F T' & & \\ T' & & T' \to \epsilon & T' \to * F T' & & T' \to \epsilon & T' \to \epsilon \\ F & F \to id & & & F \to (E) & & \end{array} $$
Agora vamos observar a execução para a entrada:
id + id * id $
A ordem da pilha será mostrada com o topo na parte superior.
Estado inicial:
O parser expande E em T E'.
Após expandir E:
O topo da pilha sempre guia a próxima decisão.
Depois de expandir F e casar o primeiro id:
Como + está em FOLLOW(T'), a produção vazia é a decisão correta.
Agora o topo é E' e a entrada continua em +.
O parser continua a expressão porque ainda existe soma pendente.
| Passo | Pilha | Entrada | Ação |
|---|---|---|---|
| 1 | [$, E] | id + id * id $ |
usa E -> T E' |
| 2 | [$, E', T] | id + id * id $ |
usa T -> F T' |
| 3 | [$, E', T', F] | id + id * id $ |
usa F -> id |
| 4 | [$, E', T', id] | id + id * id $ |
faz match de id |
| 5 | [$, E', T'] | + id * id $ |
usa T' -> epsilon |
| 6 | [$, E'] | + id * id $ |
usa E' -> + T E' |
| 7 | [$, E', T, +] | + id * id $ |
faz match de + |
| 8 | [$, E', T] | id * id $ |
usa T -> F T' |
| 9 | [$, E', T', F] | id * id $ |
usa F -> id |
| 10 | [$, E', T', id] | id * id $ |
faz match de id |
| 11 | [$, E', T'] | * id $ |
usa T' -> * F T' |
| 12 | [$, E', T', F, *] | * id $ |
faz match de * |
| 13 | [$, E', T', F] | id $ |
usa F -> id |
| 14 | [$, E', T', id] | id $ |
faz match de id |
| 15 | [$, E', T'] | $ |
usa T' -> epsilon |
| 16 | [$, E'] | $ |
usa E' -> epsilon |
| 17 | [$] | $ |
sucesso |
FIRST escolhe por onde a produção entraFOLLOW resolve o momento em que uma variável pode desaparecerO parser LL(1) é top-down, mas a decisão fica mecanizada.
Considere:
$$ \begin{split} S &\to 0S \mid 1 \end{split} $$
Derive a cadeia 001 e mostre também uma derivação parcial.
Derivação completa:
$$ \begin{split} S &\Rightarrow 0S \\ &\Rightarrow 00S \\ &\Rightarrow 001 \end{split} $$
Derivação parcial possível:
$$ S \Rightarrow 0S \Rightarrow 00S $$
Ela é parcial porque ainda resta o não terminal $S$.
Para a expressão a + b * c, explique por que a AST correta não é:
*
├── +
│ ├── a
│ └── b
└── c
Essa árvore representaria:
(a + b) * c
Mas a precedência usual da linguagem exige:
a + (b * c)
Logo a AST correta é:
+
├── a
└── *
├── b
└── c
Considere a gramática:
$$ E \to E - T \mid T $$
Explique por que ela não deve ser implementada diretamente por parser recursivo descendente.
Ela possui recursão à esquerda direta.
Na implementação literal, E() chamaria E() novamente antes de consumir qualquer token.
Consequências:
Forma adequada:
$$ \begin{split} E &\to T E' \\ E' &\to - T E' \mid \epsilon \end{split} $$
Considere:
$$ \begin{split} S &\to A d \\ A &\to bA \mid \epsilon \end{split} $$
Calcule FIRST(A), FIRST(S) e FOLLOW(A).
Como $A \to bA \mid \epsilon$:
$$ FIRST(A) = \{b, \epsilon\} $$
Como $S \to A d$ e $A$ pode sumir:
$$ FIRST(S) = \{b, d\} $$
Como depois de $A$ vem d:
$$ FOLLOW(A) = \{d\} $$
Monte a tabela LL(1) para:
$$ \begin{split} S &\to aB \mid c \\ B &\to b \mid \epsilon \end{split} $$
Primeiro os conjuntos:
$$ FIRST(S)=\{a,c\}, \qquad FIRST(B)=\{b,\epsilon\} $$
$$ FOLLOW(S)=\{\$\}, \qquad FOLLOW(B)=\{\$\} $$
Tabela:
$$ \begin{array}{c|cccc} & a & b & c & \$ \\ \hline S & S \to aB & & S \to c & \\ B & & B \to b & & B \to \epsilon \end{array} $$
Considere:
$$ A \to aB \mid aC $$
Explique por que essa gramática não é LL(1) e indique a transformação adequada.
As duas produções começam com o mesmo terminal a.
Então a célula correspondente a M[A, a] receberia mais de uma produção.
Isso produz conflito na tabela.
Transformação por fatoração:
$$ \begin{split} A &\to aA' \\ A' &\to B \mid C \end{split} $$
Agora a decisão inicial fica única.
Quando uma linguagem parece correta mas o parser LL(1) falha, o problema pode estar em três níveis diferentes:
Essa distinção é exatamente o que a revisão de hoje queria consolidar.