• Nenhum resultado encontrado

CLASSES DE GRAMÁTICAS LIVRES DE CONTEXTO E SEUS PARSERS

VISÃO GERAL DO CAPÍTULO

CLASSES DE GRAMÁTICAS LIVRES DE CONTEXTO E SEUS PARSERS

Podemos particionar o universo das gramáticas livres de contexto em uma hierarquia com base na dificuldade da análise sintática das gramáticas. Essa hierarquia tem muitos níveis. Este capítulo menciona quatro deles, a saber: CFGs arbitrárias, gramáticas LR(1), gramáticas LL(1) e gramáticas regulares (RGs). Esses conjuntos são aninhados conforme mostra o diagrama a seguir.

CFGs arbitrárias exigem mais tempo para fazer a análise do que as gramáticas LR(1) e LL(1), mais restritas. Por exemplo, o algoritmo de Earley analisa sintaticamente as CFGs arbitrárias, no pior caso, em tempo O(n3), onde n é o número de palavras no fluxo de entrada. Naturalmente, o tempo de execução real pode ser melhor. Historicamente, os construtores de compilador têm se esquivado de técnicas “universais”, devido à sua ineficiência percebida.

As gramáticas LR(1) incluem um grande subconjunto de CFGs não ambíguas. Elas podem ser analisadas, de baixo para cima (bottom-up), em uma varredura linear da esquerda para a direita, examinando no máximo uma palavra à frente

do símbolo de entrada atual. A grande disponibilidade de ferramentas que derivam parsers a partir de gramáticas LR(1) transformou os parsers LR(1) nos “favoritos de todos”. Gramáticas LL(1) são um subconjunto importante das gramáticas LR(1), e podem ser analisadas, de cima para baixo (top-down), em uma varredura linear da esquerda para a direita, com antecipação (“lookahead”) de uma palavra. Gramáticas LL(1) podem ser analisadas com um parser de descida recursiva, codificado à mão, ou com um parser LL(1) gerado. Muitas linguagens de programação podem ser escritas em uma gramática LL(1).

As gramáticas regulares (RGs) são CFGs que geram linguagens regulares. Estas são uma CFG na qual as produções são restritas a duas formas, ou A → a ou A → aB, onde A, B ∈ NT, e a ∈ T. Gramáticas regulares são equivalentes a expressões regulares; e codificam exatamente aquelas linguagens que podem ser reconhecidas por um DFA. O uso principal para as linguagens regulares na construção de compiladores é especificar scanners.

Quase todas as construções de linguagens de programação podem ser expressas na forma LR(1), e frequentemente na forma LL(1). Assim, a maioria dos compiladores usa um algoritmo de análise rápida baseado em uma dessas duas classes restritas de CFG.

para a direita, ao fluxo de palavras retornado pelo scanner. A parte difícil da análise sintática está na descoberta da conexão gramatical entre as folhas e a raiz. Duas técnicas distintas e opostas para construir a árvore são sugeridas:

1. Parsers top-down começam com a raiz e fazem a árvore crescer em direção

às folhas. A cada etapa, ele seleciona um nó para algum não terminal na borda inferior da árvore e o estende como uma subárvore que representa o lado direito de uma produção que reescreve o não terminal.

2. Parsers bottom-up começam com as folhas e fazem a árvore crescer em direção

à raiz. Em cada etapa, ele identifica uma substring contígua da borda superior da árvore, que corresponde ao lado direito de alguma produção; depois, constrói um nó para o lado esquerdo da regra e o conecta à árvore.

Em qualquer cenário, o parser faz uma série de escolhas sobre quais produções aplicar. A maior parte da complexidade intelectual na análise sintática encontra-se nos me- canismos para fazer essas escolhas. A Seção 3.3 explora os aspectos e os algoritmos que surgem na análise sintática top-down, enquanto a Seção 3.4 examina a análise bottom-up em detalhes.

3.3 ANÁLISE SINTÁTICA DESCENDENTE (TOP-DOWN)

Um parser top-down começa com a raiz da árvore sintática e a estende sistematicamente para baixo até que suas folhas correspondam às palavras classificadas retornadas pelo scanner. Em cada ponto, o processo considera uma árvore sintática parcialmente cons- truída. Ele seleciona um símbolo não terminal na borda inferior da árvore e o estende acrescentando filhos, que correspondem ao lado direito de alguma produção para esse não terminal. Ele não pode estender a fronteira a partir de um terminal. Esse processo continua até que:

a. A borda da árvore sintática contenha apenas símbolos terminais e o fluxo de

entrada tenha sido esgotado.

b. Uma divergência clara ocorra entre a borda da árvore sintática parcialmente

construída e o fluxo de entrada.

No primeiro caso, a análise tem sucesso. No segundo, duas situações são possíveis. O parser pode ter selecionado a produção errada em alguma etapa anterior no processo, e pode retroceder, reconsiderando sistematicamente as decisões anteriores. Para uma string de entrada que é uma sentença válida, o retrocesso (backtrack) levará o parser a uma sequência correta de escolhas e permitirá que ele construa uma árvore sintática correta. Alternativamente, se a string de entrada não for uma sentença válida, o retroces- so falhará e o parser deverá relatar o erro de sintaxe ao usuário.

Uma ideia-chave torna a análise sintática top-down eficiente: um grande subconjunto das gramáticas livres de contexto pode ser analisado sem retrocesso. A Seção 3.3.1 mostra transformações que normalmente podem converter uma gramática qualquer em outra adequada para a análise sintática top-down livre de retrocesso. As duas seções seguintes introduzem duas técnicas distintas para construir os parsers top-down: anali- sadores sintáticos de descida recursiva codificados à mão, e analisadores LL(1) gerados. A Figura 3.2 mostra um algoritmo concreto para um parser top-down que constrói uma derivação mais à esquerda. Ele monta uma árvore sintática, ancorada na variável root. Ele usa uma pilha, com funções de acesso push( ) e pop( ), para rastrear a parte não correspondida da borda.

A parte principal do parser consiste em um laço que focaliza o símbolo não corres- pondido mais à esquerda na borda inferior da árvore sintática parcialmente construída. Se o símbolo em foco é um não terminal, ele expande a árvore para baixo; escolhe uma produção, monta a parte correspondente da árvore de análise e move o foco para o símbolo mais à esquerda dessa nova parte da borda. Se o símbolo em foco for um terminal, ele compara o foco contra a próxima palavra na entrada. Uma correspondência move o foco para o próximo símbolo na borda e avança o fluxo de entrada.

Se o foco é um símbolo terminal que não corresponde à entrada, o parser precisa retroceder. Primeiro, ele sistematicamente considera alternativas para a regra escolhida mais recentemente. Se esgotar essas alternativas, retrocede de volta na árvore sintática e reconsidera as escolhas em um nível mais alto. Se este processo não conseguir corres-

Ao realizar o retrocesso, o parser também precisa recuar o fluxo de entrada. Felizmente, a árvore sintática parcial codifica informações suficientes para tornar esta ação eficiente. O parser precisa colocar cada terminal correspondido na produção descartada de volta ao fluxo de entrada, ação que pode ser realizada enquanto os desconecta da árvore sintática na travessia da esquerda para a direita dos filhos descartados.

3.3.1 Transformação de uma gramática para análise sintática