Capítulo 5 – Análise sintática
1. Objetivo
2. Estratégias gerais de parsing
3. Análise sintática descendente (top-down)
3.1. Analisador sintático com retrocesso (backtracking)
3.2. Analisador sintático predicativo recursivo
3.3. Analisador sintático predicativo não recursivo – Parser LL
3.4. Construção de tabelas sintáticas predicativas (Tabelas Oráculo)
4. Análise sintática ascendente (bottom-up)
4.1. Parser por transição-redução (método geral)
4.2. Parser LR
Gramática da linguagem
árvore de sintaxe Sequência
de símbolos PARSER Aceitaçãoou Erro
Dado
- uma gramática livre do contexto G
- uma sequência a de símbolos terminais (frase)
pretende-se
- verificar se a é uma frase válida de L(G)
a a a a em que:
a → sequência já percorrida
a → próximo símbolo a analisar (token)
a → sequência por analisar a é uma frase de L(G) se e só se
Em cada momento do processo, - ou existe uma derivação
*
S m a d , com m T* e d (S T)*
onde m a . { Fase de possível aceitação }
Existem duas estratégias gerais de parsing:
- reconhecimento descendente (“top-down”) - reconhecimento ascendente (“bottom-up”).
Reconhecimento descendente (“Top-Down”)
- A árvore é construída da raiz para as folhas
- Em cada vértice (com um símbolo não terminal A):
- selecionar uma produção com A à esquerda e construir os vértices filhos de A com os símbolos à direita nessa produção;
- selecionar o vértice onde continuar.
- Termina quando todas as folhas são símbolos terminais. - A aceitação é obtida se a sequência a for esgotada.
- Os símbolos de a são associados até se reconhecer o lado direito de uma produção. - A aceitação é obtida se,
Análise sintática
- construir uma árvore gramatical para a a partir da raiz, criando os nós em pré-ordem.
Desta forma,
- a frase a é percorrida da esquerda para a direita, - vão-se identificando derivações esquerdas,
1. Análise sintática descendente com retrocesso (backtracking)
O reconhecimento é feito por expansão de regras sintáticas, substituindo - símbolos não-terminais do lado esquerdo de produções,
- pelos símbolos do lado direito das produções. Depois, a expansão das regras sintáticas é confirmada
- por emparelhamento
- dos símbolos da frase de entrada (a)
- com os símbolos terminais das regras sintáticas. - se não emparelhar,
então terá que haver retrocesso no processo (“backtracking”).
Saída: Árvore de derivação
1. Criar um nó com o símbolo inicial da gramática.
2. Substituir o símbolo não-terminal situado mais à esquerda da árvore de derivação pelos símbolos do lado direito da produção cujo lado esquerdo é o símbolo substituído.
Este passo termina quando o primeiro símbolo da produção for um símbolo terminal.
3. Se o primeiro símbolo ainda não processado de a não emparelhar com o primeiro símbolo da produção selecionada no passo anterior, recuar para o passo 2 e pesquisar outra alternativa (“backtrack”); se não existir outra alternativa, o algoritmo termina sem sucesso.
4. Emparelhar, da esquerda para a direita, os símbolos de a com os símbolos terminais nas folhas da árvore de derivação, até que aconteça uma das três hipóteses:
- o emparelhamento de símbolos falha terminar sem sucesso;
Vantagens:
analisador entra em ciclo infinito de expansão duma produção recursiva à esquerda - A ordem da escolha das alternativas determina a linguagem aceite pelo analisador;
por ex., a frase aa é aceite pelo analisador se for expandida em primeiro a produção S ® B, o que não acontece se a primeira produção a ser expandida for S ® aA, pois o primeiro símbolo da entrada, a, emparelha com o primeiro símbolo terminal da produção S ® aA, mas o segundo símbolo de entrada, a, não pode ser derivado a partir de A.
- Em caso de entrada com erro, não é possível identificar as causas de erro, pois não há distinção entre retrocesso por erro e por necessidade de pesquisa de alternativas - A operação de retrocesso é muito demorada, degradando o desempenho
2. Analisador sintático predicativo recursivo
Definição:
Um Parser recursivo-descendente diz-se profético (ou predicativo) quando:
- a análise do a, ou lookahead, determina univocamente a produção a utilizar;
- a execução de um procedimento simula a sequência de símbolos do lado direito dessa produção.
Um Parser predicativo é caracterizado por tomar sempre decisões irrevogáveis, isto é, sem backtracking.
Para se construir um analisador predicativo, é preciso:
- dado o lookahead a (a = a) e o símbolo não terminal A a ser expandido,
Algoritmo:
{ X Î T } Reconhecer_X:
Se (X = a) Então
avançar (a) { reconhecimento parcial } Senão ERRO
{ X Î S } Reconhecer_X:
{ em função de a, escolher uma produção p : X ® X1 X2 ... Xn } Reconhecer_X1
Partindo do símbolo inicial (S) e do início de a: Parser_RD: Avançar (a); Reconhecer_S ; Se ( a = e) Então RECONHECIMENTO Senão ERRO
Para a construção de um Parser predicativo é necessário estabelecer um método que, - conhecidos o lookahead (a) e o símbolo a reconhecer,
- determine univocamente a produção a utilizar.
Uma das formas é utilizar uma tabela sintática (Tabela Oráculo), M[X,a] = p, que significa: a produção p será aplicada se pretender-se expandir o símbolo não terminal
- constituído por uma única função, onde o símbolo é passado como parâmetro; - eficiente; Reconhecer(X): Se (X Î T) Então Se (X = a) Então Da_Simbolo (a) Senão ERRO
Senão np ¬ Oráculo(X, a) Se (np ¹ ERRO) Então
- ainda é recursivo;
- mas é mais cómodo e mais independente da linguagem: a informação relativa à linguagem está guardada em duas estruturas de dados.
Otimização da Função Oráculo:
ORÁCULO : (S È T) x T ® { skip, np, erro } (X, a) ® ação
Se (X Î T e X = a) Então
p' p'' Situação de conflito
Reconhecer (X):
ação ¬ ORÁCULO (X, a) Caso (ação) Seja
erro : ERRO
skip : Da_Simbolo(a)
np : Para (" Y Î Produção[np]) Fazer Reconhecer (Y)
Analisador sintático
- uma frase de entrada, - uma pilha,
A frase de entrada é seguida por um $ à direita (indica o fim da sequência de símbolos) A pilha Z:
- contém uma sequência de símbolos gramaticais, - com $ a indicar o fundo da pilha;
- inicialmente, a pilha contém o símbolo inicial da gramática acima de $. A tabela sintática é um “array” bidimensional M[A, a] onde,
- A é um símbolo não terminal, terminal ou $, - a é um símbolo terminal ou $.
O analisador sintático é um programa que se comporta da seguinte forma:
2. Se X = a (a é terminal), o analisador sintático remove X da pilha e avança o
apontador da entrada para o próximo símbolo (próximo lookahead), a = a.
3. Se X é um não terminal, o programa consulta a entrada M[X, a] da tabela
sintática M, que será uma produção-X (X ® a) da gramática ou ERRO.
Por ex., se M[X,a] = { X ® UVW } substitui-se X no topo da pilha por WVU (com U no topo); se M[X, a] = erro chama-se uma rotina de recuperação de erros.
Algoritmo: Análise sintática predicativa não recursiva
Entrada: Uma frase a e uma tabela sintática M para a gramática G = (S, T, P, S). Saída: Uma derivação mais à esquerda (se a L(G)) ou uma indicação de erro.
Repetir
Se (Topo(Z) Î T) Então
Se (Topo(Z) = a) Então Pop(Z)
Da_Simbolo(a) Senão ERRO
Senão
Se (Topo(Z) Î S) Então
Se M[Topo(Z), a] = { X ® Y1Y2 … Yn } Então Pop(Z)
Para i desde n até 1 Fazer Push(Z, Yi)
Senão ERRO
a : sequência de símbolos terminais já analisados
Z : sequência de símbolos terminais e não terminais a reconhecer, mantém-se invariante: * S a Z no início : a = e e Z = S
e no fim : a = a e Z = e { se tudo correr bem } Deste modo:
- não é necessário construir a árvore de derivação
A este analisador sintático dá-se o nome de Parser LL (scan input from Left to right and construct Leftmost derivation).
Falta apenas incluir uma ação de aceitação: Ação = { skip, np, erro, ac }. A aceitação ocorre quando:
(Z = $) e (a = $)
Push(Z, S)
Da_Simbolo(a)
Repetir
Acção ¬ Oráculo(Topo(Z), a); Pop(Z);
Caso Acção Seja erro : Erro; ac : ;
skip : Da_Simbolo(a);
- logo, o analisador sintático irá expandir A através de a quando o símbolo de entrada corrente (lookahead) for a (a = a).
A única complicação ocorre quando a = e ou a e.
Neste caso, deve-se expandir A de novo através de a se: - a Î Follow(A) ou
Algoritmo:
Entrada : Gramática G
Saída : Tabela Oráculo M[X, Y] em que, X (S T { $ }) e
Y (T { $ }) Descrição do método:
1. Para cada produção A ® a de G, executar os passos 2 e 3.
2. Para cada símbolo terminal a Î First(a), fazer M[A, a] = A ® a.
3. Se e Î First(a), então para cada b Î Follow(A), fazer M[A, b] = A ® a.
Se e Î First(a) e $ Î Follow(A), fazer M[A, $] = A ® a.
4. Fazer a cada entrada da tabela M do tipo M[x, x] = SKIP, em que x T. 5. Fazer à entrada da tabela M, M[$, $] = AC.
6. Fazer a cada entrada indefinida da tabela M, M[k, j] = ERRO.
Os passos 2 e 3 podem ser substituídos pelo seguinte único passo:
1) S DC→
3) A → Cb
9) D A→ B
Push(Z, S)
Push(Z, e)
Da_Simbolo(a)
Da_Simbolo(a)
Push(Z, f)
Da_Simbolo(a)
e b d f b e $
Pop(Z)
Push(Z, e)
Da_Simbolo(a)
Análise Sintática
O objetivo da análise sintática ascendente é a construção da árvore de derivação, a partir das folhas, em direção à raiz.
A árvore de derivação é construída percorrendo a sequência a de símbolos de entrada (frase) da esquerda para a direita (e armazenada numa string s): esta operação é designada por deslocamento (“shift”) da posição do analisador sintático.
Exemplo: Considere-se a gramática com as seguintes 6 produções :
1) S ® A B c 2) S B A c 3) A ® a 4) A a A 5) B ® b 6) B b B
A frase abbc pode ser reduzida até ao símbolo inicial da gramática S em 4 etapas: S Þ A B c Þ A b B c Þ A b b c Þ a b b c
Etapa Símbolos GramaticaisSequência de Regra GramaticalReduzida
1 a b b c 3
2 A b b c 5
3 A b B c 6
4 A B c 1
5 S
s = m b e p : A b s = m A Redução de b em A pela produção p Cada redução corresponde a uma subida de nível na árvore de derivação.
p : A ® X Y … Z A
m X Y … Z depois disso, se :
Um parser “bottom-up” é caracterizado por dois tipos de operações básicas: Redução: s = m b s = m A
Transição: s = m a e Avançar( ) No início do processo: s = e e = a
No fim do processo: s = S e = e { Aceitação } Num parser “top-down”:
No início do processo: s = S e = a
- Ou não existe nenhuma derivação a efetuar: estado de erro.
Assumindo (por enquanto) que todas as decisões podem ser tomadas apenas em função dos valores de S e de a, define-se:
Parser_Trans_Red:
Inicializar(s) ; Da_Simbolo(a) ; Repetir
ação ¬ ORÁCULO(s, a) ; Caso ação Seja
s a Ação
a + ( b * c ) $ Shift a + ( b * c ) $ Reduce p4
3. Parser LR
Os analisadores sintáticos LR apresentam diversas vantagens:
- Reconhecem praticamente todos os construtores de linguagens de programação estruturadas em blocos, expressas em gramáticas independentes do contexto.
- São mais gerais que outros analisadores sintáticos e revelam o mesmo grau de eficiência.
- Podem detetar cedo erros sintáticos, quando tal é possível, ao pesquisar a entrada da esquerda para a direita.
Maior inconveniente destes analisadores: - a complexidade da sua implementação.
“Rightmost derivation”, derivação da direita para a esquerda,
- pesquisa os elementos da frase da esquerda para a direita (tal como os analisadores descendentes) e
Arquitetura do analisador sintático LR
Entrada Frase$
Pilha Reconhecedor Saída
As duas tabelas do analisador sintático ascendente LR são designadas por: - tabela de ações (“actions”) e
- tabela de saltos (“goto”).
A tabela de ações A[estado, entrada] é indexada pelo estado e pelo símbolo de entrada (terminal), e possui um dos quatro valores :
1. Desloca s (“shift”), na qual s é um estado.
2. Reduz n (“reduce”), na qual n é a referência a uma produção X → a. 3. Aceita (“accept”).
A tabela de saltos G[estado, símbolo]:
- é indexada pelo estado e pelo símbolo não-terminal,
- possui, nalgumas células, uma indicação da referência a um estado.
A construção das tabelas pode ser efetuada utilizando três metodologias distintas:
1. SLR (“Simple LR”) — é o método mais simples, mas também o menos poderoso, porque não é possível construir as tabelas para algumas gramáticas.
2. LR canónico — é o método mais poderoso e, em simultâneo, o mais complexo, sendo aquele que mais memória necessita para armazenar a tabela.
Um parser LR implementa o autómato determinístico de pilha, capaz de reconhecer uma dada gramática do tipo LR(k), onde:
- os estados do autómato “representam” valores da string s - cada transição: s s a
corresponde à determinação do próximo estado, em função do atual e de a T∈
- cada redução:
s s − b determina o estado anterior quando do atual se retira b (∈ S T)*,∪ s s A determina o próximo estado em função do atual e de A ∈ S
- transições são implementadas por uma tabela chamada Oráculo_T ou Ação: Oráculo_T : Q × T { shift, reduce, ac, erro } × (Q P) ∪
- os estados anteriores são recuperados se, numa pilha F de estados forem efetuados Pop’s em número igual ao dos símbolos em β
- mudanças de estado por redução são implementadas por um Oráculo_S ou Goto: Oráculo_S : Q × S Q
Inicializar(F) ; Da_Simbolo(a) ; Repetir
acção Oráculo_T(Topo(F), a) ; Caso ação Seja
erro: ERRO ; ac: ; shift: q ação.estado ; Push(F, q) ; Da_Simbolo(a) ; reduce: np ação.Num_prod ; (A, nsd) Prod[np] ;
Para i desde 1 até nsd Fazer Pop(F) ;
- Como associar estados a valores de s ? - Como construir os Oráculos ?
- Será sempre possível ?
Cada passo do processo LR é caracterizado por :
( s, a ) CONFIGURAÇÃO LR
Cada decisão é tomada em função de:
A d.b produção em reconhecimento e ponto (.) de decisão; w o prefixo de s, com s = w d;
Uma configuração ( s, a ) satisfaz a condição não-erro LR se : *
b m a
A cada configuração não-erro, está associado um e um só item LR. No início : (e, a) [ e, Z .S$, e ]
Só existem três formas de itens LR (a que correspondem três tipos de ações): 1) [ ω, A d.ab, m ]
a que corresponde a transição: (wd , ar) (wd a, r)
s
2) [ ω, A d.Bb, m ]
a ação a tomar depende de B, e virá a ser tomada em função de [ wd, B .g, bm ]
3) [ w, A d.e, m ]
a que corresponde a redução (wd , m) (wA, m)
r
A cada configuração (s, a ) está associado um item [ w, A d.b, m ] que determina
Para um dado item LR [ w, A d.b, m ] chama-se item LR(k) a [ A d.b, First(k, m) ]
Os estados de um autómato LR(k) são constituídos por itens LR(k). Num autómato LR(0) os estados são [ A d.b ].
Exemplo:
Construir o autómato não determinístico LR(0) para a gramática G com as seguintes produções:
1) Z S $→ 2) S a→
Algoritmo para construir o autómato determinístico:
Alfabeto : S T ∪
Estado inicial : [ Z . S $, e ] Cada estado é formado por :
- Um item LR(k) : [ A d . b , n ]
- Pelo seu fecho, isto é, todos os itens da forma
[ B . g, m ] sempre que b = B r, com m = First(k, r n) - E pelo fecho de cada um desses itens.
A função de transição : D = Q × (T ∪ S) Q
para cada item LR(k) associado ao estado q [ A → d . B r, n ] ∀ B T ∈ ∪ S
D (q, B) = q’ com q’ = [ A d B . r, n ]
NOTAS:
- Uma redução corresponde a recuar no grafo (com o conhecimento da produção).
{ redução por S → bAA }
{ redução por A → e } { redução por S → bAA }
{ redução por A → Aa } Por exemplo, ao estado 5 estão associados:
[ A → e, m ] e [ A → A.a, a ] Se a First(∈ m) é impossível decidir:
redução por A → e, ou
transição para [ A → Aa., a ].
A partir do autómato determinístico A constroem-se as Tabelas Oráculo, da seguinte forma:
- q Q, X S
Oráculo_S (q, X) = sq', se (q, q')X A Oráculo_S (q, X) = erro, caso contrário - q Q, t T
Oráculo_T (q, t) = sq', se (q, q')t A
Oráculo_T (q, t) = rp, se [ A → a., n ] : t First(k, n) Oráculo_T (q, t) = erro, caso contrário
a a b $ S A e s t a d o s 1 s3 s4 erro s2 erro
2 erro erro AC erro erro
Construir o autómato determinístico LR(1) para a mesma gramática: First(S) = { a, b }
1 [ Z .S$, ]
1 [ Z .S$, ] S 2 [ Z S.$, ] AC ( = $)
[ S .a, $ ]
[ S .a, $ ]
[ S .bAA, $ ] a 3 [ S a., $ ] red#2 ( = $) b
1 [ Z .S$, ] S 2 [ Z S.$, ] AC
[ S .a, $ ]
[ S .bAA, $ ] a 3 [ S a., $ ] red#2 ( = $) b
4 [ S b.AA, $ ]
[ A ., a/$ ] First(A$) = { a, $ } [ A .Aa, a/$ ] First(A$) = { a, $ } red#4
[ S .a, $ ]
[ S .bAA, $ ] a 3 [ S a., $ ] red#2 ( = $) b
5 [ S bA.A, $ ] 4 [ S b.AA, $ ] A [ A A.a, a/$ ]
[ A ., a/$ ] [ A .Aa, a/$ ] red#4
1 [ Z .S$, ] S 2 [ Z S.$, ] AC
[ S .a, $ ]
[ S .bAA, $ ] a 3 [ S a., $ ] red#2 ( = $) b
5 [ S bA.A, $ ] 4 [ S b.AA, $ ] A [ A A.a, a/$ ]
[ A ., a/$ ] red#4 [ A ., $ ] First($) = { $ } [ A .Aa, a/$ ] ( = $) [ A .Aa, $ ] First($) = { $ } red#4
[ S .a, $ ]
[ S .bAA, $ ] a 3 [ S a., $ ] red#2 ( = $) b
5 [ S bA.A, $ ] 4 [ S b.AA, $ ] A [ A A.a, a/$ ]
[ A ., a/$ ] red#4 [ A ., $ ] First($) = { $ } [ A .Aa, a/$ ] ( = $) [ A .Aa, $ ] First($) = { $ } red#4
( = a/$) ( = a)red#4 [ A ., a ] [ A .Aa, a ]
1 [ Z .S$, ] S 2 [ Z S.$, ] AC
[ S .a, $ ]
[ S .bAA, $ ] a 3 [ S a., $ ] red#2 ( = $) b
5 [ S bA.A, $ ] 4 [ S b.AA, $ ] A [ A A.a, a/$ ]
[ A ., a/$ ] red#4 [ A ., a/$ ] [ A .Aa, a/$ ] (= a/$) [ A .Aa, a/$ ] red#4
[ S .a, $ ]
[ S .bAA, $ ] a 3 [ S a., $ ] red#2 ( = $) b
5 [ S bA.A, $ ] 4 [ S b.AA, $ ] A [ A A.a, a/$ ]
[ A ., a/$ ] red#4 [ A ., a/$ ] [ A .Aa, a/$ ] (= a/$) [ A .Aa, a/$ ] red#4
( = a/$) a
1 [ Z .S$, ] S 2 [ Z S.$, ] AC
[ S .a, $ ]
[ S .bAA, $ ] a 3 [ S a., $ ] red#2 ( = $) b
5 [ S bA.A, $ ]
A 7 [ S bAA., $ ]
4 [ S b.AA, $ ] A [ A A.a, a/$ ] [ A A.a, a/$ ]
[ A ., a/$ ] red#4 [ A ., a/$ ] red#3 ( = $) [ A .Aa, a/$ ] (= a/$) [ A .Aa, a/$ ]
red#4
( = a/$) a
[ S .a, $ ]
[ S .bAA, $ ] a 3 [ S a., $ ] red#2 ( = $) b
5 [ S bA.A, $ ]
A 7 [ S bAA., $ ]
4 [ S b.AA, $ ] A [ A A.a, a/$ ] [ A A.a, a/$ ]
[ A ., a/$ ] red#4 [ A ., a/$ ] red#3 ( = $) [ A .Aa, a/$ ] (= a/$) [ A .Aa, a/$ ] a
red#4
( = a/$) a
1 [ Z .S$, ] S 2 [ Z S.$, ] AC
[ S .a, $ ]
[ S .bAA, $ ] a 3 [ S a., $ ] red#2 ( = $) b
5 [ S bA.A, $ ]
A 7 [ S bAA., $ ]
4 [ S b.AA, $ ] A [ A A.a, a/$ ] [ A A.a, a/$ ]
[ A ., a/$ ] red#4 [ A ., a/$ ] red#3 ( = $) [ A .Aa, a/$ ] (= a/$) [ A .Aa, a/$ ] a
red#4
( = a/$) a
6 [ A Aa., a/$ ] red#5 ( = a/$)
a b $ S A e s t a d o s 1 s3 s4 erro s2 erro
2 erro erro AC erro erro
3 erro erro r2 erro erro
4 r4 erro r4 erro s5
5 s6 erro r4 erro s7
6 r5 erro r5 erro erro
7 s8 erro r3 erro erro
8 erro erro r5 erro erro
Conflito redução/transição
Resumindo e concluindo: - Um item LR(k) da forma
[ A → a.Xb, m ]
determina uma transição por X - Um item LR(k) da forma
[ A → a., m ]
determina uma redução por A → a para todo o a First(k, ∈ m). As possíveis situações de conflito são
- Conflito redução-transição
ao mesmo estado estão associados 2 itens LR(k) da forma: [ A → a.ab, m ]
[ A → a., m ] e [ B → b., n ]
com First(k, m) First(k, n) ≠ .
Exercício:
Construção do autómato LR(1):
First(S) = First(E) = First(T) = { (, id } Follow(S) = { $ }
- (Goto) → símbolos não-terminais: S, E, T - Não ocorrem reduções no estado 1
surgem na maioria das linguagens.
- As exigências de memória destes autómatos são aceitáveis.
- O autómato LR(1) contem informação suficiente para resolver os conflitos na maioria das linguagens.
- A memória necessária neste autómatos é impraticável.
4. Parser SLR(1) — Simple LR(1)
- Os seus estados são basicamente os mesmos que os do autómato LR(0)
- A certos items é associada informação “necessária” para identificar reduções - A cada item LR(0) da forma:
[ A → a. ] é associado o Follow(A)
Haverá redução por A → a se a Follow(A) - O correspondente item LR(1) seria:
Tabela Oráculo SLR(1):
a a b c $ S A e s t a d o s1 erro s3 erro erro s2 erro
2 erro s4 erro AC erro erro
3 s6 erro erro erro erro s5
4 erro r1 r1 r1 erro erro
5 s7 erro erro erro erro erro
6 r5 s3 erro erro s8 erro
7 erro r2 r2 r2 erro erro
8 erro s10 s9 erro erro erro
9 r3 erro erro erro erro erro
10 r4 r1 r1 r1 erro erro
O método SLR(1) resolve conflitos LR(0) quando Conflito redução-transição: quando a Ï Follow(B) [ A ® a.ab ] [ B ® d. ] Conflito redução-redução:
quando Follow(A) Follow(B) = Æ [ A ® a. ]
5. Parser LALR(1) — LookAhead LR(1)
- Ainda os mesmos estados que os autómatos LR(0). - Com mais informação associada a cada item
- Cada item LR(0) da forma [ A ® a. ] do estado q é substituído por [ A ® a., n ], onde n = L (q, A ® a.)
- Cada item LR(0) da forma [ A ® a.Xb ] do estado q é substituído por [ A ® a.Xb, n ], onde n = L (q, A ® a.Xb)
- Deste modo, a redução num estado com o item [ A ® a., n ]
Cálculo dos L() LALR(1):
Regra 1: Propagação por transição de estados :
L(q’, A → aX.b) = È L(q, A → a.Xb)
[A → a . X b ] Î q
Uma transição de q para q’ pelo símbolo X, propaga o L() do item [A → a.Xb] para o item [A → aX.b].
Regra 2: Concatenação por cálculo do fecho [A → a.Br]
---[B → .d]
L(q, B → .d) = È First(r L(q, A → a.Br))
[A → a . B r ] Î q
L(qf, Z → S.$) = { $ }
Exemplo:
Verificar se a gramática G = ( { S, L, R }, { id, =, * }, S, P ) é LALR(1), em que P contém as seguintes produções:
p0: Z S$→ (não faz parte de G) p1: S L=R→
p2: S R→ p3: L *R→ p4: L id→ p5: R L→
a id = * $ S L R e s t a d o s 1 s5 erro s6 erro s2 s4 s3
2 erro erro erro AC erro erro erro
3 erro erro erro r2 erro erro erro
4 erro s7 erro r5 erro erro erro
5 erro r4 erro r4 erro erro erro
6 s5 erro s6 erro erro s9 s10
7 s5 erro s6 erro erro s9 s8
8 erro erro erro r1 erro erro erro
9 erro r5 erro r5 erro erro erro
Relação entre LL(k), LR(k), SLR(k) e LALR(k):
- LL(k) é o parser mais simples e, consequentemente, o com mais problemas, pos nem todas as gramáticas podem ser tratadas com este parser (as recursivas)
- As fraquezas do parser LL(k) são superadas pelo parser LR(k)
- O parser SLR(1) – LR(1) simples; aumenta o poder de análise do LR(0)
- O parser LR(1) aumenta o poder de análise do SLR(1), pois ajuda a resolver conflitos redução-redução e redução-transição
Seja a gramática com as seguintes produções: A ( A )→
Otimização das Tabelas de Parsing:
- São geralmente matrizes esparsas - Tentativas de compactação.