• Nenhum resultado encontrado

Análise sintática livre de retrocesso

A principal fonte de ineficiência no parser top-down mais à esquerda surge da sua necessidade de retroceder. Se o parser expandir a borda inferior com a produção errada, eventualmente encontra uma divergência entre esta borda e as folhas da árvore sintática, que correspondem às palavras retornadas pelo scanner. Quando ele descobre a divergência, precisa desfazer as ações que resultaram na borda errada e tentar outras produções. O ato de expandir, retrair e reexpandir a borda desperdiça tempo e esforço.

Na derivação da Figura 3.5, o parser escolheu a regra correta em cada etapa. Escolhendo consistentemente as regras, como considerar as produções na ordem de aparecimento na gramática, ele teria retrocedido em cada nome, primeiro tentando Fator → (Expr),

e depois Fator → num, antes de derivar nome. De modo semelhante, as expansões pelas regras 4 e 8 teriam considerado as outras alternativas antes de expandir para ε. Para esta gramática, o parser pode evitar o retrocesso com uma modificação simples. Quando ele for selecionar a próxima produção, pode considerar tanto o símbolo em foco quanto o próximo símbolo de entrada, chamado símbolo de antecipação. Usando este único símbolo de antecipação, o parser pode distinguir todas as escolhas que surgem na análise sintática pela gramática de expressão recursiva à direita. Assim, dizemos que a gramática é livre de retrocesso com antecipação de um símbolo. Uma gramática livre de retrocesso também é chamada gramática preditiva.

Podemos formalizar a propriedade que torna a gramática de expressão recursiva à direita livre de retrocesso. Em cada ponto na análise, a escolha de uma expansão é óbvia porque cada alternativa para o não terminal mais à esquerda leva a um símbolo terminal distinto. A comparação da próxima palavra no fluxo de entrada com essas escolhas revela a expansão correta.

A intuição é clara, mas formalizá-la exigirá alguma notação. Para cada símbolo a da gramática, defina FIRST(a) como o conjunto de símbolos terminais que podem aparecer como a primeira palavra em alguma string derivada de a. O domínio de FIRST é o conjunto de símbolos da gramática, T ∪ NT ∪ {∈, eof} e seu con- tradomínio é T ∪ {ε, eof}. Se a é um terminal, ε ou eof, então FIRST(a) tem exatamente um membro, a. Para um não terminal A, FIRST(A) contém o conjunto completo de símbolos terminais que podem aparecer como símbolo inicial em uma forma sentencial derivada de A.

A Figura 3.7 mostra um algoritmo que calcula os conjuntos FIRST para cada símbolo em uma gramática. Como sua etapa inicial, o algoritmo define os conjuntos FIRST para os casos simples, terminais, ε e eof. Para a gramática de expressão recursiva à direita mostrada na Figura 3.4, esta etapa inicial produz os seguintes conjuntos FIRST:

num nome + − × ÷ ( ) eof ε

FIRST num nome + − × ÷ ( ) eof ε

Em seguida, o algoritmo passa pelas produções, usando os conjuntos FIRST para o lado direito de uma produção para derivar o conjunto FIRST do não terminal em seu lado esquerdo. Este processo termina quando ele alcança um ponto fixo. Para a gramática de expressão recursiva à direita, os conjuntos FIRST dos não terminais são:

Expr Expr9 Termo Termo9 Fator

FIRST (, nome, num +, –, ε (, nome, num ×, ÷ , ε (, nome, num

Definimos conjuntos FIRST sobre símbolos únicos da gramática. Agora, é conveniente estender esta definição para strings de símbolos. Para uma string s = b1 b2 b3... bk, definimos FIRST(s) como a união dos conjuntos FIRST para b1, b2, b3,... bn, onde bn é o primeiro símbolo cujo conjunto FIRST não contém ε, e ε ∈ FIRST(s) se, e somente

se, ele estiver no conjunto para cada um dos bi, 1 ≤ i ≤ k. O algoritmo na Figura 3.7 calcula esta quantidade para a variável rhs.

Gramática livre de retrocesso

CFG para a qual o parser top-down, mais à esquerda, sempre pode prever a regra correta com antecipação de no máximo uma palavra.

Conjunto FIRST

Para um símbolo a da gramática, FIRST(a) é o conjunto de terminais que podem aparecer no início de uma sentença derivada de a;

eof ocorre implicitamente no final de cada sentença da gramática. Assim, ele está no domínio e no con- tradomínio de FIRST.

Conceitualmente, os conjuntos FIRST simplificam a implementação de um parser top-down. Considere, por exemplo, as regras para Expr9 na gramática de expressão recursiva à direita:

2 Expr9+ Termo Expr9

3 | − Termo Expr9

4 | ∈

Quando o parser tenta expandir uma Expr9, usa o símbolo de antecipação e os conjuntos FIRST para escolher entre as regras 2, 3 e 4. Com uma antecipação de +, ele expande pela regra 2 porque + está em FIRST(+ Term Expr9), e não em FIRST(– Term Expr9), ou FIRST(ε). De modo semelhante, uma antecipação de — dita a escolha da regra 3. A regra 4, ε-produção, apresenta um problema um pouco mais difícil. FIRST(ε) é simplesmente {ε}, que não corresponde a qualquer palavra retornada pelo scanner.

Intuitivamente, o parser deve aplicar a ε-produção quando o símbolo de antecipação não for um membro do conjunto FIRST de qualquer outra alternativa. Para diferenciar entre entradas legais e erros de sintaxe, o parser precisa saber quais palavras podem aparecer como símbolo inicial após uma aplicação válida da regra 4 — o conjunto de símbolos que podem vir após Expr9.

Para capturar este conhecimento, definimos o conjunto FOLLOW(Expr9) para conter todas as palavras que podem ocorrer imediatamente à direita de uma string derivada de Expr9. A Figura 3.8 apresenta um algoritmo para calcular este conjunto para cada não terminal em uma gramática, considerando a existência de conjuntos FIRST. O algoritmo inicializa cada conjunto FOLLOW como conjunto vazio, e depois, percorre as produções, calculando a contribuição dos sufixos parciais para o conjunto FOLLOW de cada símbolo em cada lado direito. O algoritmo termina quando alcança um ponto fixo. Para a gramática de expressão recursiva à direita, o algoritmo produz:

Expr Expr’ Termo Termo’ Fator

FOLLOW eof, ) eof, ) eof, +, –, ) eof, +, –, ) eof, +, –, ×, ÷, )

Conjunto FOLLOW

Para um não terminal a, FOLLOW(a) contém o conjunto de palavras que podem ocorrer imediatamen- te após a em uma sentença.

Agora, uma gramática livre de retrocesso tem a propriedade de que, para qualquer não terminal A com múltiplos lados direitos, A→b1 | b2 | ··· | bn

Qualquer gramática que tenha esta propriedade é livre de retrocesso.

Para a gramática de expressão recursiva à direita, somente as produções 4 e 8 têm conjuntos FIRST+ que diferem dos conjuntos FIRST.

Produção Conjunto FIRST Conjunto FIRST+

4 Expr9 → ε { ε } { ε, eof, ) }

8 Termo9 ε { ε } { ε, eof, +, –, ) }

A aplicação da condição livre de retrocesso a cada conjunto de lados direitos alterna- tivos prova que a gramática, realmente, é livre de retrocesso.