Compiladores
Análise Sintática
Segunda fase
Função: verificar se as construções
usadas no programa estão
gramaticalmente corretas.
Dada uma GLC e um programa fonte, o
objetivo é verificar se o programa pertence
a linguagem gerada pela gramática.
Recebe uma seqüência de tokens do AL e
produz uma árvore de derivação, se a
seqüência é válida
ou relata o erro encontrado.
Estratégias:
Top-down
Árvore a) corresponde a derivação
S=>cAd
Para A existem duas alternativas:
Neste ponto o analisador deve escolher uma
das alternativas
Mas deverá se lembrar das próximas
alternativas
Escolhendo a primeira alternativa
Árvore b)
S=>cabd
Falha: sentença gerada não corresponde a
sentença de entrada
Analisador deverá efetuar o retrocesso
Voltar ao último ponto de escolha
Na segunda alternativa
Árvore c)
S=>cad
Sentença reconhecida pela gramática
Problema:
O processo de backtracking é ineficiente
Difícil de identificar com precisão o local do erro
Outro exemplo:
S → a | [ L ]
L → S;L | S
Implementação:
Para cada símbolo não terminal da gramática é desenvolvida uma função que explora todas as derivações.
Pontos de recursão (marcas) são utilizados para sinalizar possível ponto de reinício.
Exemplo:
Considere a sentença: [ a ]
LETOKEN: retorna o token lido a partir da
sentença de entrada
MARCA_PONTO: marca, na sentença de
entrada, um ponto de possível reinício da
análise
RETROCEDE: volta o ponteiro de leitura
para o último ponto marcado.
O símbolo sob o cabeçote de leitura
determina exatamente qual produção
deve ser aplicada na expansão de cada
não-terminal.
Dado um símbolo a sob o cabeçote de leitura e
o não-terminal A a ser derivado, qual das
produções alternativas de A é a que deve ser
derivada, unicamente, a seqüência iniciada por
Exemplo: analisador sintático recursivo para reconhecer expressões aritméticas envolvendo identificadores, valores inteiros e reais.
Análise Sintática Descendente Tabular
Não recursivo
Utilizam pilha ao invés de chamadas recursivas
Implementa um autômato de pilha controlado por tabela
de análise
O lado direito irá substituir o símbolo não-terminal que
se encontra no topo da pilha.
O analisador busca a produção a ser aplicada na tabela
de análise, levando em conta o não-terminal no topo da
pilha e o token sob o cabeçote de leitura.
A fita: contém a sentença a ser analisada,
seguida de $, símbolo que marca o fim da
sentença.
A pilha, inicialmente, contém $, que marca sua
base, seguido do símbolo inicial da gramática.
A tabela de análise é uma matriz M com n linhas
e t+1 colunas:
n – número de símbolos não-terminais
Construção da Tabela Sintática
Maior dificuldade em construir o AS.
É necessário computar duas funções
associadas à gramática:
FIRST
Se:
Cada entrada da tabela existe apenas uma
produção
Gramática que originou a tabela é do tipo LL(1)
• Sentenças geradas pela gramática são passíveis de serem analisadas da esquerda para a direita
O analisador é controlado por um
programa que considera:
X, o símbolo no topo da pilha,
e a é um símbolo terminal da entrada.
Estes dois símbolos determinam a ação
do analisador.
Há três possibilidades então:
1) Se X = a = $, o analisador encerra o
reconhecimento, com sucesso.
2) Se X = a ≠ $, o analisador desempilha X da pilha e
avança o ponteiro de entrada ao próximo símbolo de
entrada.
3) Se X é um não-terminal, o programa consulta a
entrada M[X,a] da tabela de derivação M. Esta
entrada pode ser uma produção de X ou um erro. Se,
por exemplo, M[X,a] = { X=>UVW }, o analisador
substitui X no topo da pilha por WVU (com U no
topo).
Algoritmo de análise sintática preditiva
tabular
Entrada: para uma gramática G
uma cadeia w
Gramática LL(1)
Uma gramática não recursiva à esquerda
é LL(1) se, e somente se:
Sempre que A → α, A → β são produções,
ocorrer que:
1) a interseção dos conjuntos FIRST(α) e FIRST(β) é
vazia;
2) no máximo um dos dois, α ou β , deriva a palavra
vazia;
Se β =>* ε, então a interseção de FIRST(α) COM
FOLLOW(A) é vazia.
A → α, A → β serão usadas para preencher a
linha A da matriz.
Se as interseções não fossem vazias
Exercício 1
Para a gramática abaixo calcule FIRST,
FOLLOW. Depois gere a Tabela de Análise
Preditiva e faça a seqüência de movimentos do
analisador para a seguinte sentença id+id*id$
Exercício 2
Para a gramática abaixo calcule FIRST,
FOLLOW. Depois gere a Tabela de Análise
Preditiva e faça a seqüência de movimentos do
analisador para a seguinte sentença id+id*id$
Solução Exercício 1
TABELA
Solução Exercício 2
Análise Sintática Redutiva (Bottom-Up)
A análise Bottom-up tenta construir uma árvore
gramatical para uma cadeia de entrada
começando pelas folhas, e trabalhando árvore
acima em direção à raiz.
A cada passo de redução, uma sub-cadeia
particular que reconheça o lado direito de uma
produção, é substituída pelo símbolo à
esquerda daquela produção.
Como funciona?
Configuração inicial do analisador: fita de entrada
contém a sentença a ser analisada seguida de um
$, e a pilha contém apenas o marcador de base $.
Analisador sintático empilha zero ou mais símbolos
na pilha até que um handle (lado direito de uma
produção) surja no topo da pilha. Este então é
substituído por seu lado esquerdo. Este ciclo é
repetido até termos uma entrada vazia e o símbolo
inicial no topo da pilha.
Duas classes de analisadores do tipo
empilha-reduz:
1) analisadores de precedência de operadores,
muito eficientes no reconhecimento de expressões
aritméticas e lógicas;
2 ) analisadores LR, que reconhecem a maior parte
das linguagens livres do contexto.
Analisadores de Precedência de Operadores
Esses analisadores operam sobre a classe das
gramáticas de operadores.
Nessas gramáticas, os não-terminais aparecem sempre separados por símbolos terminais (isto é, nunca
aparecem dois não-terminais adjacentes) e, além disso, as produções não derivam a palavra vazia (ex: nenhum lado direito de produção é "ε").
Por exemplo, a gramática:
• E → E O E | ( E ) I id
• O → + I - I * I / I **
não é de precedência de operadores
lado direito E O E contém três não-terminais consecutivos.
Desvantagens: dificuldade em lidar com operadores iguais que tenham significados distintos
• por exemplo, o operador "-", que pode ser binário ou unário.
Para identificar o handle, os analisadores de precedência de operadores baseiam-se em relações de precedência existentes entre os tokens (operandos e operadores).
São três as relações de precedência entre os terminais:
• < , > e =.
Sendo a e b símbolos terminais, tem-se que:
• a < b significa que a tem precedência menor que b;
A utilidade dessas relações na análise de uma
sentença é a identificação do handle :
• < identifica o limite esquerdo do handle;
• = indica que os terminais pertencem ao mesmo handle;
• > identifica o limite direito do handle.
Esses analisadores são guiados por uma tabela de
precedência, cujas relações definem o movimento
que o analisador deve fazer:
• empilhar, reduzir, aceitar ou chamar uma rotina de atendimento a erro.
Tabela de precedência para operações lógicas: id v & ( ) $ id > > > > v < > < < > > & < > > < > > ( < < < < = ) > > > > $ < < < < E → E v E | E & E | (E) | id
A tabela é uma matriz quadrada que relaciona todos
os terminais da gramática mais o marcador $.
Desses terminais, poucos são realmente operadores
(neste exemplo, os operadores reais são apenas v e
& ).
É importante saber que, na tabela. os terminais nas
linhas representam terminais no topo da pilha, e os
terminais nas colunas representam terminais sob o
Basicamente, um analisador de precedência
funciona da seguinte maneira:
• Seja a o terminal mais ao topo da pilha e b o terminal sob o cabeçote de leitura:
• I) se a < b ou a = b então empilha;
• 2) se a > b procura o handle na pilha (o qual deve estar delimitado pelas relações < e > ) e o substitui pelo nãc-terminal correspondente.
• Na pilha. o handle vai desde o topo até (inclusive) o
primeiro terminal x que tem abaixo de si um terminal y tal que y < x.
Mais ao topo porque no topo pode estar um não-terminal.
Na verdade, os analisadores de precedência desconsideram os não-terminais da gramática, levando em conta apenas a presença dos mesmos (suas
Movimentos de um analisador de precedência de operadores:
Pilha Relação Entrada Ação Handle
$ < id v id & id $ Empilha id
$ id > v id & id $ Reduz id
$ E < v id & id $ Empilha v
$ E v < id & id $ Empilha id
$ E v id > & id $ Reduz id
$ E v E < & id $ Empilha &
$ E v E & < id $ Empilha id
$ E v E & id > $ Reduz id
$ E v E & E > $ Reduz E & E
$ E v E > $ Reduz E v E
Algoritmo:
Entrada: matriz de relações de precedência e a sentença a ser analisada s$.
Resultado: a sequência de produções aplicadas no reconhecimento de s, caso s pertença a L(G), caso contrário, uma indicação de erro.
Método: repita
se $S é o topo da pilha e $ está sob o cabeçote de leitura então aceita e pára
senão /* seja a o terminal mais ao topo da pilha e b o terminal sob o cabeçote */ se a < b ou a = b
então empilha b e avança o cabeçote senão se a > b
então repita /* reduzir */ desempilha
até encontrar a relação < entre o terminal do topo e o último desempilhado;
empilha o não-terminal correspondente imprime a produção aplicada
Construção da tabela de precedência de operadores
• Maior dificuldade
• Dois métodos:
Intuitivo: vamos ver!
Método intuitivo:
• Este método obtém as relações de precedência a partir do conhecimento da associatividade e da precedência dos operadores da gramática.
• Considere dois operadores Θ1 e Θ2.
• 1) Se o operador Θ1 tem maior precedência que o
operador Θ2, então Θ1 (na pilha) > Θ2 (na entrada), e Θ2 (pilha) < Θ1 (entrada). Por exemplo, o operador de multiplicação * tem maior precedência que o operador +, logo, * > +. e + < *.
• 2) Se Θ1 e Θ2 têm igual precedência (ou são iguais) e
são associativos à esquerda, então Θ1 > Θ2, e Θ2 > Θ1; se são associativos à direita. então Θ1 < Θ2 e Θ2 < Θ1.
• Por exemplo:
os operadores * e / têm a mesma precedência e são associativos à esquerda, portanto, * > /, e / > *.
o operador de exponenciação ** é associativo à direita, logo ** < **.
Θ < id Θ < ( Θ > ) Θ > $
id > Θ ( < Θ ) > Θ $ < Θ
• 3) As relações entre os operadores e os demais tokens
(operandos e delimitadores) são fixas. Para todos os operadores Θ, tem-se:
• 4) As relações entre os tokens que não são operadores
também são fixas:
( < ( ) > ) id > ) $ < (
( = ) ) > $ id > $ $ < id
( < id
Exemplo:
Dada a gramática:
• E → E + E | E * E | E ** E | ( E ) | id
tem-se os seguintes níveis de precedência e
associatividade entre os operadores:
• ** tem maior precedência e é associativo à direita;
• * tem precedência intermediária e é associativo à esquerda;
+ * ** ( ) id $ + > < < < > < > * > > < < > < > ** > > < < > < > ( < < < < = < ) > > > > > id > > > > > $ < < < < < Aceita
Analisadores LR(k)
K => número de símbolos de exame na entrada
(lookahead)
R => right-most derivation (derivação mais à direita)
L => left to right scanning (direção de leitura da
cadeida de entrada)
Vantagens:
• Analisadores LR servem para reconhecer a maior parte das construções de linguagens de programação.
• O método de análise LR é o mais genérico dentre os métodos de empilhar-reduzir sem retrocesso.
• Um analisador LR pode detectar um erro sintático tão cedo quanto possível numa varredura da entrada da esquerda p/ direita.
Desvantagem:
• Implementação manual bastante trabalhosa, mas
adequado para ser usado por geradores automáticos de parsers (eg, yacc/bison).
A combinação entre símbolo de estado e o símbolo da entrada é usada para indexar uma tabela sintática e determinar o
funcionamento do analisador.
A tabela sintática é dividida em duas partes funcionais: ação (action) e desvio (goto).
A partir de Sm (estado no topo da pilha) e ai (símbolo da entrada), consultamos a célula ação[sm, ai] que pode ter os seguintes
valores:
• empilhar s, no qual s é um estado
• reduzir utilizando a produção A → B
• aceitar
• erro
A função desvio toma um estado e um símbolo gramatical como argumento e produz um novo estado como saída.
O algoritmo de análise LR é utilizado na implementação de diversos tipos de analisadores: LR(0), SLR, LR(1) e LALR(1).
Algoritmo de análise sintática LR:
• Entrada: uma cadeia de entrada w e uma tabela sintática LR
• Saída: Se w estiver em L(G), produz uma decomposição ascendente (bottom-up) para w, caso contrário indica um erro.
• Método:
Inicialmente o analisador sintático possui s0 (estado incial) na pilha e w$ no buffer de entrada.
O analisador sintático executa o algoritmo a seguir até que uma ação de aceitação ou de erro seja atingida.
O analisador funciona basicamente como segue:
• Seja Em, o estado do topo da pilha e ai o token sob o
caheçote de leitura. O analisador consulta a tabela AÇÃO[Em, ai]. que pode assumir um dos valores:
a) empilha E,: causa o empilhamento de "ai Em";
b) reduz n (onde n é o número da produção A → β): causa o desempilhamento de 2 * r símbolos. onde r = | β |, e o empilhamento de "AEy" onde Ey resulta
da consulta à tabela de TRANSIÇÃO[Em-r, A ];
c) aceita: O analisador reconhece a sentença como válida;
Exercício:
• Dada a gramática abaixo e a tabela de transição, verifique os passos do analisador para a cadeia de entrada id * id + id