2.2 COMPILADORES
2.2.3 Analisador Sintático
Figura 8. Autômato Finito Não-Determinístico com Movimento Vazio.
Fonte: Louden (2004)
Esses autômatos oferecem como vantagens a representação da sentença vazia e a possibilidade de expressar alternativas sem fazer uma combinação de estados (LOUDEN, 2004).
O poder de reconhecimento dos AFN é equivalente ao dos AFD, contudo os AFN são mais abrangentes e menos restritivos, enquanto que os AFD, que em geral são mais difíceis de serem especificados, são mais eficientes como reconhecedores de unidades léxicas de uma linguagem de programação (MENEZES, 2002).
Qualquer AFN pode ser convertido para um AFD. O autômato resultante dessa conversão pode apresentar um número consideravelmente maior de estados que o AFN equivalente, exigindo assim, mais espaço para sua representação. Entretanto, é possível encontrar um autômato finito determinístico mínimo (AFDmin) equivalente a qualquer AFD que apresente um número reduzido de estados e ainda assim reconheça as mesmas sentenças aceitas pelo AFD.
Terminada a análise léxica dá-se inicio a segunda fase do processo de compilação que é a análise sintática descrita a seguir.
Figura 9. Processo de transformação do programa fonte em arvore sintática.
O analisador sintático está diretamente associado a regras que especificam a sintaxe de uma linguagem. No processo da análise sintática, o analisador sintático recebe a lista de tokens produzida pelo analisador léxico e verifica se aqueles tokens seguem a estrutura estabelecida para uma determinada linguagem. Nessa análise – análise gramatical – o analisador sintático verifica se o posicionamento dos tokens está em concordância com as regras especificadas para a linguagem, acusando erro sintático quando alguma construção não respeita as especificações.
2.2.3.1 Gramáticas
As gramáticas podem ser entendidas como uma receita para especificação da sintaxe de uma linguagem. Uma gramática pode representar apenas uma linguagem, mas em geral, uma linguagem pode ser representada através de diferentes gramáticas.
Segundo Menezes (2002), uma gramática é uma quádrupla (N, T, P, S), onde:
• N é o conjunto dos símbolos não-terminais. Estes símbolos não fazem parte da linguagem, mas são utilizados na definição de regras de produção.
• T é o conjunto dos símbolos terminais. Estes são exatamente os símbolos da linguagem representada e constituem os símbolos do alfabeto.
• P é o conjunto de regras de produção. Essas produções relacionam símbolos terminais e não-terminais e são representadas por α:: β ou α → β, onde α e β são sentenças ou formas sentenciais sobre o alfabeto da linguagem que a gramática representa, e denotam α produz β.
• S é o símbolo inicial da gramática, a partir do qual as sentenças de uma linguagem podem ser geradas.
Exemplo: uma gramática para expressões simples de soma e subtração, N = {<expressão>, <operador>}
T = {número, +, -, (, )}
P = {<expressão>→ <expressão> <operador> <expressão> | (<expressão>) |
número
<operador>→ + | - }
S = <expressão>
As gramáticas foram agrupadas por Chomsky em quatro classes, de acordo com as suas capacidades de representação (MENEZES, 2002; LOUDEN, 2004).
• Tipo 0 (Gramática sem Restrições): esta classe engloba todas as gramáticas possíveis;
• Tipo 1 (Gramática Sensível ao Contexto): possui a restrição de que para toda produção X ::= Y pertencente à gramática, |X| ≤ |Y| (o tamanho de X é menor que o tamanho de Y) e X deve conter, ao menos um símbolo não-terminal;
• Tipo 2 (Gramática Livre de Contexto): possui a restrição das gramáticas Tipo 1, e suas produções devem ser da forma A ::= Y, com A N, Y (N U T) e |A| = 1;
• Tipo 3 (Gramática Regular): além das restrições das gramáticas Tipo 2, somente pode conter produções das formas:
A::= aB, com A e B N, e a T, ou
A::= a, com A N, e a T.
As linguagens, assim como as gramáticas, estão agrupadas nos tipos 0, 1, 2 e 3, representadas pelas gramáticas dos tipos 0, 1, 2 e 3, respectivamente.
A maior parte das linguagens de programação têm a sua estrutura sintática especificada por meio de gramáticas livre de contexto, isto se deve, de acordo com Aho, Sethi e Ullman (1995), a sua estrutura recursiva que a torna mais propicia para essa atividade.
A utilização de gramáticas pode ser formalizada por suas operações de substituição, a derivação e a redução. Derivação é o processo de substituição de uma parte de uma sentença por um
conjunto de símbolos, no sentido símbolo inicial => sentença. Exemplo: para a sentença: ‘(número + número) - <expressão>’, uma possível derivação seria ‘(número + número) – número’, porque existe uma produção que indica que é possível substituir o não-terminal <expressão> pelo terminal número. Redução é a operação de substituição de uma sentença ou parte dela por um conjunto de símbolos no sentido sentença => símbolo inicial, correspondendo ao processo inverso da derivação.
Após a análise sintática, o programa fonte assume a estrutura de uma árvore sintática ou árvore gramatical, que mostra como ocorreram as derivações ou reduções. Para a sentença ‘(número + número) – número’ e, considerando a gramática anterior, seria construída a seguinte árvore sintática (Figura 10):
Figura 10. Árvore sintática
Fonte: Adaptado de Louden, (2004)
onde na estrutura da árvore os nós internos são rotulados com os nomes das estruturas que representam e os nós folhas da árvore correspondem aos tokens recebidos do analisador léxico.
Para se especificar uma gramática existem diversas notações, como a notação BNF (Backus- Naur Form) que consiste em representar uma gramática através do agrupamento dos símbolos não- terminais e suas derivações, expondo as características que os distinguem. Nessa notação, a seguinte simbologia é utilizada:
• <X> representa um símbolo não-terminal;
• <X>::= β representa uma regra de produção do não-terminal <X> com derivação β, que representa uma combinação de símbolos terminais e/ou não-terminais;
• O símbolo | separa as regras de produção associadas a um determinado símbolo não-terminal;
• x ou X representa um símbolo terminal usado na constituição das sentenças.
2.2.3.2 Tipos de analisadores sintáticos
Dependendo da forma como a análise sintática é construída, ela pode ser classificada em análise sintática ascendente ou a análise sintática descendente, as quais possuem estratégias diferentes de análise, mas como um objetivo comum que é a validação sintática da sentença de entrada para a gramática específica (LOUDEN 2004).
Análise sintática ascendente (BOTTOM-UP)
Nessa categoria de análise sintática, o analisador sintático tenta alcançar o símbolo inicial da gramática a partir da sentença de entrada. De acordo com Louden (2004), um analisador sintático ascendente para efetuar a verificação da sentença usa uma pilha de análise sintática, que conterá tanto símbolos terminais como não-terminais durante o processo de análise e, no final do processo, deve restar apenas o símbolo inicial da gramática, caracterizando sucesso na análise sintática da sentença de entrada. Os analisadores sintáticos ascendentes são de maneira geral mais poderosos que os descendentes, exigindo, no entanto, construções mais complexas.
Os analisadores sintáticos ascendentes são também conhecidos como análise de empilhar e reduzir, isso porque no processo de análise o analisador empilha os tokens uma a um em uma pilha e vai efetuando reduções até chegar ao símbolo inicial da gramática, caso seja possível. A cada passo da redução, se o analisador reconhece uma forma sentencial do lado direito de uma produção, é efetuada a substituição desta pelo símbolo à esquerda da produção correspondente. O processo deve ser repetido até a análise de todos os símbolos da entrada. Se ao final do processo de análise constar na pilha apenas o símbolo inicial da gramática, a sentença estará sintáticamente correta.
Existem diversos tipos de analisadores sintáticos ascendentes e o LR(1), segundo Louden (2004), é exemplo mais geral dessa categoria. Este algoritmo analisa a entrada da esquerda para a direita, derivando sempre o símbolo mais à direita, token a token.
Análise sintática descendente (TOP - DOWN)
O nome dessa classe de analisadores sintáticos sugere a analogia de como a análise sintática é percorrida, já que o analisador sintático parte do símbolo inicial da gramática e tenta chegar à sentença de entrada. Esse tipo de análise pode ser entendido como uma tentativa de se encontrar a derivação mais à esquerda para a sentença de entrada, ou ainda, como a tentativa de se construir a árvore gramatical para a sentença de entrada priorizando as construções mais à esquerda, (AHO, SETHI e ULLMAN 1995).
Segundo Louden (2004), os analisadores sintáticos descendentes podem assumir as formas de analisadores recursivos ou preditivos, sendo que os recursivos são mais usados.
Nos analisadores recursivos para validar a sentença de entrada, é feito um teste pelas diferentes possibilidades de análise sintática da entrada. Quando algum teste falha acontece um retrocesso para a próxima construção no nó acima por onde o processo deverá continuar. O término da análise é anunciado com sucesso quando toda a sentença de entrada for produzida ou com fracasso, quando não for possível gerar a sentença a partir do símbolo inicial da gramática.
Por outro lado, os analisadores preditivos tentam prever a construção seguinte com base no símbolo de entrada. Nesses tipos de algoritmos é necessário conhecer um ou mais tokens para que a seleção da alternativa da próxima construção seja feita. O analisador verifica qual a única produção que deriva o símbolo corrente (AHO, SETHI & ULLMAN 1995).
O analisador sintático LL(1) é um exemplo de algoritmo de análise sintática descendente preditivo. No nome desse algoritmo o primeiro L (left) se refere ao processamento que ocorre da esquerda para a direita, o segundo L (left most derivation) se refere à derivação do símbolo mais à esquerda e o 1 (lookahead) indica que apenas um símbolo de entrada é utilizado para a previsão da direção da análise. Genericamente, tem-se a análise sintática LL(k), onde o k indica que k símbolos serão utilizados para a verificação sintática (LOUDEN, 2004).
Tendo em vista que os analisadores recursivos efetuam uma busca exaustiva, é garantido que a solução, caso exista, será encontrada. Por essa razão os analisadores descendentes recursivos são mais poderosos que os preditivos, mas em contrapartida mais lentos e de tempo de execução exponencial, o que na prática torna a sua utilização inadequada para a construção de compiladores (LOUDEN, 2004).
Após a execução da análise léxica e da análise sintática dá-se início à análise semântica, onde são efetuadas verificações que estão além da capacidade das gramáticas livres de contexto e de algoritmos padrão de análise sintática.