Capítulo 5 1. Objetivo 2. Estratégias gerais de parsing 3. 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. ascendente (bottom-up) 4.1. Parser por transição-redução (método geral) 4.2. Parser LR 4.3. Parser SLR 4.4. Parser LALR 1/183
Objetivo Objetivo 2/183
Objetivo Gramática da linguagem árvore de sintaxe Sequência de símbolos PARSER Aceitação ou 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) - reconhecer a forma da frase (identificar as produções de G). 3/183
Objetivo Em cada momento do processo, 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 * S a 4/183
Objetivo Em cada momento do processo, - ou existe uma derivação * S m a d, onde m com m T* e d (S T)* a. { Fase de possível aceitação } - ou não existe nenhuma derivação que aceite a a. { ERRO: não é frase de L(G) } 5/183
Estratégias gerais de parsing Estratégias gerais de parsing 6/183
Estratégias gerais de parsing 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. - As decisões são tomadas por observação de a, chamado lookahead símbolo. 7/183
Estratégias gerais de parsing Reconhecimento ascendente ( Bottom-up ) - A árvore é construída das folhas para a raiz. - Os símbolos de a são associados até se reconhecer o lado direito de uma produção. - A aceitação é obtida se, esgotada a sequência a, o símbolo inicial estiver na raiz da árvore. 8/183
descendente (top-down) descendente (top-down) 9/183
descendente (top-down) Dada uma frase a, a análise descendente pode ser vista como uma tentativa de se, - encontrar uma derivação mais à esquerda para a, - 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, - enquanto a árvore de sintaxe vai sendo construída. 10/183
descendente (top-down) 1. 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 ). O retrocesso ( backtrack ) é resultado da existência de várias produções com o mesmo símbolo no lado esquerdo. 11/183
descendente (top-down) Algoritmo: Entrada: Uma gramática G e uma frase a 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; - é detetada na folha da árvore um símbolo não-terminal regressar ao passo 2; - todos os símbolos de a foram emparelhados e todas as folhas da árvore contêm apenas símbolos terminais que foram emparelhados com símbolos de a terminar com sucesso. 12/183
descendente (top-down) Vantagens: - simplicidade de implementação do algoritmo que lhe está associado. 13/183
descendente (top-down) Desvantagens: - Não aceita gramáticas recursivas à esquerda; por ex., numa entrada com, o 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, não é possível identificar as causas de, pois não há distinção entre retrocesso por e por necessidade de pesquisa de alternativas - A operação de retrocesso é muito demorada, degradando o desempenho ( performance ) da análise sintática. 14/183
descendente (top-down) 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, - conhecer qual das alternativas da produção A b1 b2 bn é a única que deriva uma cadeia que começa por a. 15/183
descendente (top-down) Ou seja, a alternativa adequada precisa ser detetável examinando-se apenas o primeiro símbolo da cadeia que a mesma deriva. 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 Reconhecer_X2... Reconhecer_Xn 16/183
descendente (top-down) 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 X e o lookahead for um símbolo terminal a (a = a). 17/183
descendente (top-down) Pretende-se elaborar um Parser predicativo que seja: - independente da gramática; - 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 Para cada Y Î Produção[np] Reconhecer (Y) Senão ERRO 18/183
descendente (top-down) - 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 (X, a ) { skip, np, } ação Se (X Î T e X = a ) Então ação = skip { o reconhecedor léxico reconhece um símbolo terminal } * Se (X Î T e X ¹ a ) ou (X Î S e $ p : X a ) Então ação = * Se (X Î S e $1 p : X a ) Então p ação = np 19/183
descendente (top-down) Nota: * * Se (X Î S e $ p', p'' : X a e X a ) Então p' p'' Situação de conflito Reconhecer (X): ação ORÁCULO (X, a ) Caso (ação) Seja : ERRO skip : Da_Simbolo(a ) np : Para (" Y Î Produção[np]) Fazer Reconhecer (Y) Em cada momento continua a haver apenas dois casos possíveis: - ERRO - Possível reconhecimento. 20/183
Analisador sintático predicativo não recursivo Analisador sintático predicativo não recursivo 21/183
Analisador sintático predicativo não recursivo Pode-se construir um analisador predicativo usando uma pilha (sem recursividade). Um analisador sintático predicativo não recursivo é composto por: - uma frase de entrada, - uma pilha, - uma tabela sintática, e - um fluxo de saída. Frase de entrada Pilha X Y Z $ a + b $ Programa de Análise Sintática Predicativa Saída Tabela Sintática M 22/183
Analisador sintático predicativo não recursivo 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: - considera o símbolo do topo da pilha X e o lookahead a (estes dois símbolos determinam a ação do analisador). 23/183
Analisador sintático predicativo não recursivo Existem três possibilidades: 1. Se X = $, o analisador pára a análise com sucesso, se a = $. 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 U V W } substitui-se X no topo da pilha por W V U (com U no topo); se M[X, a] = chama-se uma rotina de recuperação de s. Algoritmo: 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. Método: Introduzir os símbolos $ e S na pilha Z (S é o símbolo inicial de G) ¾ Topo(Z) = S a = primeiro símbolo de a 24/183
Analisador sintático predicativo não recursivo 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 Até (Topo(Z) = $) 25/183
Analisador sintático predicativo não recursivo Resumindo, temos que sendo 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 - nem guardar a - Z funciona como uma pilha, - portanto, é possível construir um algoritmo iterativo. 26/183
Analisador sintático predicativo não recursivo 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,, ac }. A aceitação ocorre quando: (Z = $) e (a = $) pois não há símbolo para reconhecer nem falta analisar nenhum símbolo. 27/183
Analisador sintático predicativo não recursivo ParserLL (algoritmo LL): Push(Z, $) Push(Z, S) Da_Simbolo(a ) Repetir Acção Oráculo(Topo(Z), a ); Pop(Z); Caso Acção Seja : Erro; ac : ; skip : Da_Simbolo(a ); np : Para i desde n até 1 Fazer { X Y1Y2 Yn } Push(Z, Yi); Até Ação = ac; 28/183
Construção de tabelas sintáticas predicativas (Tabelas Oráculo) Construção de tabelas sintáticas predicativas (Tabelas Oráculo) 29/183
Construção de tabelas sintáticas predicativas (Tabelas Oráculo) A ideia subjacente ao algoritmo associado à construção destas tabelas é a seguinte: - suponha-se que A a é uma produção e que a Î First(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 - $ (na entrada) foi atingido e $ Î Follow(A). 30/183
Construção de tabelas sintáticas predicativas (Tabelas Oráculo) 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: 2-3. Para cada símbolo terminal a Î Lookhead(A a), fazer M[A, a] = A a. 31/183
Exemplo de um analisador sintático predicativo não recursivo Exemplo de um analisador sintático predicativo não recursivo 32/183
Exemplo de um analisador sintático predicativo não recursivo Considere a seguinte gramática G = (Σ, T, P, S), em que Σ = { S, A, B, C, D }, T = { a, b, c, d, e, f }, P= { 1) S DC; 2) A a; 3) A Cb; 4) B e; 5) B c; 6) B da; 7) C e; 8) C f; 9) D AB } 33/183
Exemplo de um analisador sintático predicativo não recursivo First (a) = { a } Follow (S) = { $ } First (b) = { b } Follow (A) = { c, d, e, f } First (c) = { c } Follow (B) = { e, f } First (d) = { d } Follow (C) = { $, b } First (e) = { e } Follow (D) = { e, f } First (f) = { f } First (S) = { a, e, f } First (A) = { a, e, f } First (B) = { ε, c, d } First (C) = { e, f } First (D) = { a, e, f } 34/183
Exemplo de um analisador sintático predicativo não recursivo 1) S DC First(S DC) = First(D) = { a, e, f } (e First(D)) M[S, a] = M[S, e] = M[S, f] = 1 Oráculo S $ a b 1 c d e f 1 1 A B C D $ a b c d e f 35/183
Exemplo de um analisador sintático predicativo não recursivo 2) A a First(A a) = First(a) = { a } M[A, a] = 2 Oráculo $ a S 1 A 2 b c d e f 1 1 B C D $ a b c d e f 36/183
Exemplo de um analisador sintático predicativo não recursivo 3) A Cb First(A Cb) = First(C) = { e, f } (e First(C)) M[A, e] = M[A, f] = 3 Oráculo $ a b c d e f S 1 1 1 A 2 3 3 B C D $ a b c d e f 37/183
Exemplo de um analisador sintático predicativo não recursivo 4) B e Como e First(e), M[B, x] = 4, para x Follow(B) = { e, f } M[B, e] = M[B, f] = 4 Oráculo $ a b c d e f S 1 1 1 A 2 3 3 4 4 B C D $ a b c d e f 38/183
Exemplo de um analisador sintático predicativo não recursivo 5) B c First(B c) = First(c) = { c } M[B, c] = 5 Oráculo $ a b c d e f S 1 1 1 A 2 3 3 4 4 B 5 C D $ a b c d e f 39/183
Exemplo de um analisador sintático predicativo não recursivo 6) B da First(B da) = First(d) = { d } M[B, d] = 6 Oráculo $ a b c d e f S 1 1 1 A 2 3 3 4 4 B 5 6 C D $ a b c d e f 40/183
Exemplo de um analisador sintático predicativo não recursivo 7) C e First(C e) = First(e) = { e } M[C, e] = 7 Oráculo $ a b c d e f S 1 1 1 A 2 3 3 4 4 B C 5 6 7 D $ a b c d e f 41/183
Exemplo de um analisador sintático predicativo não recursivo 8) C f First(C f) = First(f) = { f } M[C, f] = 8 Oráculo $ a b c d e f S 1 1 1 A 2 3 3 4 4 7 8 B C 5 6 D $ a b c d e f 42/183
Exemplo de um analisador sintático predicativo não recursivo 9) D AB First(D AB) = First(A) = { a, e, f } (e First(A)) M[D, a] = M[D, e] = M[D, f] = 9 Oráculo $ a b c d e f S 1 1 1 A 2 3 3 4 4 7 8 9 9 B 5 C D 9 6 $ a b c d e f 43/183
Exemplo de um analisador sintático predicativo não recursivo M[x, x] = SKIP, para x T Oráculo $ a M[$, $] = AC b c d e f S 1 1 1 A 2 3 3 4 4 7 8 9 9 B 5 6 C D $ a b c 9 AC SKIP SKIP SKIP d e f SKIP SKIP SKIP 44/183
Exemplo de um analisador sintático predicativo não recursivo M[X, Y] = E (Erro), para as entradas vazias Oráculo $ a b c d e f S E 1 E E E 1 1 A E 2 E E E 3 3 B E E E 5 6 4 4 C E E E E E 7 8 D E 9 E E E 9 9 $ AC E E E E E E a E SKIP E E E E E b E E SKIP E E E E c E E E SKIP E E E d E E E E SKIP E E e E E E E E SKIP E f E E E E E E SKIP 45/183
Exemplo de um analisador sintático predicativo não recursivo ebdfbe$ Push(Z, $) Push(Z, S) S $ Oraculo(Topo(Z), a ) = Oraculo(S, e) = 1 46/183
Exemplo de um analisador sintático predicativo não recursivo ebdfbe$ Pop(Z) Push(Z, C) Push(Z, D) D C $ Oraculo(Topo(Z), a ) = Oraculo(D, e) = 9 47/183
Exemplo de um analisador sintático predicativo não recursivo ebdfbe$ Pop(Z) Push(Z, B) Push(Z, A) A B C $ Oraculo(Topo(Z), a ) = Oraculo(A, e) = 3 48/183
Exemplo de um analisador sintático predicativo não recursivo ebdfbe$ Pop(Z) Push(Z, b) Push(Z, C) C b B C $ Oraculo(Topo(Z), a ) = Oraculo(C, e) = 7 49/183
Exemplo de um analisador sintático predicativo não recursivo ebdfbe$ Pop(Z) Push(Z, e) e b B C $ Oraculo(Topo(Z), a ) = Oraculo(e, e) = SKIP 50/183
Exemplo de um analisador sintático predicativo não recursivo ebdfbe$ Pop(Z) Da_Simbolo(a ) b B C $ Oraculo(Topo(Z), a ) = Oraculo(b, b) = SKIP 51/183
Exemplo de um analisador sintático predicativo não recursivo ebdfbe$ Pop(Z) Push(Z, e) Da_Simbolo(a ) B C $ Oraculo(Topo(Z), a ) = Oraculo(B, d) = 6 52/183
Exemplo de um analisador sintático predicativo não recursivo ebdfbe$ Pop(Z) Push(Z, A) Push(Z, d) d A C $ Oraculo(Topo(Z), a ) = Oraculo(d, d) = SKIP 53/183
Exemplo de um analisador sintático predicativo não recursivo ebdfbe$ Pop(Z) Da_Simbolo(a ) A C $ Oraculo(Topo(Z), a ) = Oraculo(A, f) = 3 54/183
Exemplo de um analisador sintático predicativo não recursivo ebdfbe$ Pop(Z) Push(Z, b) Push(Z, C) C b C $ Oraculo(Topo(Z), a ) = Oraculo(C, f) = 8 55/183
Exemplo de um analisador sintático predicativo não recursivo ebdfbe$ Pop(Z) Push(Z, f) f b C $ Oraculo(Topo(Z), a ) = Oraculo(f, f) = SKIP 56/183
Exemplo de um analisador sintático predicativo não recursivo ebdfbe$ Pop(Z) Da_Simbolo(a ) b C $ Oraculo(Topo(Z), a ) = Oraculo(b, b) = SKIP 57/183
Exemplo de um analisador sintático predicativo não recursivo ebdfbe$ Pop(Z) Da_Simbolo(a ) C $ Oraculo(Topo(Z), a ) = Oraculo(C, e) = 7 58/183
Exemplo de um analisador sintático predicativo não recursivo ebdfbe$ Pop(Z) Push(Z, e) e $ Oraculo(Topo(Z), a ) = Oraculo(e, e) = SKIP 59/183
Exemplo de um analisador sintático predicativo não recursivo ebdfbe$ Pop(Z) Da_Simbolo(a ) $ Oraculo(Topo(Z), a ) = Oraculo($, $) = AC FIM. A frase pertence à linguagem gerada pela gramática G, L(G). 60/183
Análise Sintática Ascendente (bottom-up) 61/183
1. Princípios gerais 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. Quando é detetada uma sequência de símbolos gramaticais idêntica ao lado direito de uma produção, esses símbolos são substituídos pelo símbolo não-terminal do lado esquerdo da produção: esta operação é designada por redução ( reduce ). 62/183
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ÞABcÞAbBcÞAbbcÞabbc Etapa Sequência de Símbolos Gramaticais Regra Gramatical Reduzida 1 abbc 3 2 Abbc 5 3 AbBc 6 4 ABc 1 5 S O procedimento corresponde a uma sequência de derivação à direita. 63/183
2. Método geral: parser por transição-redução s = m b e p : A b s=ma 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 XY Z A m X Y Z depois disso, se : s = m A = f d A e p : B d A s=fb B A f d X Y Z Sequência reconhecida: ao atingir a raiz da árvore (s = S) não restam símbolos em a. 64/183
Um parser bottom-up é caracterizado por dois tipos de operações básicas: Redução: s=mb s=ma Transição: s = m a e Avançar( a ) No início do processo: s = e e a = a No fim do processo: s = S e a = e { Aceitação } Num parser top-down : No início do processo: s = S e a = a No fim do processo: s = e e a = e { Aceitação } 65/183
Em qualquer momento de parsing bottom-up : - Ou existe uma derivação: S s a b { estado possível de aceitação } - Ou não existe nenhuma derivação a efetuar: estado de. Assumindo (por enquanto) que todas as decisões podem ser tomadas apenas em função dos valores de S e de a, define-se: ORÁCULO : (T È S)* x T { shift, reduce, ac, } (s, a ) ação 66/183
Parser_Trans_Red: Inicializar(s) ; Da_Simbolo(a ) ; Repetir ação ORÁCULO(s, a ) ; Caso ação Seja : ERRO ; ac: ; shift: s s a ; Da_Simbolo(a ) ; reduce: s s - b ; { p : A b } s sa; Até ac ; 67/183
Exemplo: Dada a gramática G com as seguintes produções: p1 : E E + E p2 : E E * E p3 : E ( E ) p4 : E id analisar a expressão: a + (b * c) $ 68/183
p1 : E E + E p2 : E E * E s a a a +(b*c)$ p3 : E ( E ) p4 : E id Ação Shift E 69/183
p1 : E E + E p2 : E E * E s a a a a +(b*c)$ + (b*c)$ p3 : E ( E ) p4 : E id Ação Shift Reduce p4 70/183
p1 : E E + E p2 : E E * E s a a p3 : E ( E ) Ação a +(b*c)$ a + (b*c)$ Reduce p4 E + (b*c)$ Shift p4 : E id Shift 71/183
p1 : E E + E p2 : E E * E s a a p3 : E ( E ) Ação a +(b*c)$ a + (b*c)$ Reduce p4 E + (b*c)$ Shift E+ ( b*c)$ Shift p4 : E id Shift 72/183
p1 : E E + E p2 : E E * E s a a p3 : E ( E ) Ação a +(b*c)$ a + (b*c)$ Reduce p4 E + (b*c)$ Shift E+ ( b*c)$ Shift E+( b *c)$ Shift p4 : E id Shift 73/183
p1 : E E + E p2 : E E * E s a a p3 : E ( E ) p4 : E id Ação a +(b*c)$ a + (b*c)$ Reduce p4 E + (b*c)$ Shift E+ ( b*c)$ Shift E+( b *c)$ Shift E+(b * c)$ Shift Reduce p4 74/183
p1 : E E + E p2 : E E * E s a a p3 : E ( E ) p4 : E id Ação a +(b*c)$ a + (b*c)$ Reduce p4 E + (b*c)$ Shift E+ ( b*c)$ Shift E+( b *c)$ Shift E+(b * c)$ Reduce p4 E+(E * c)$ Shift Shift 75/183
p1 : E E + E p2 : E E * E s a p3 : E ( E ) a p4 : E id Ação a +(b*c)$ a + (b*c)$ Reduce p4 E + (b*c)$ Shift E+ ( b*c)$ Shift E+( b *c)$ Shift E+(b * c)$ Reduce p4 E+(E * c)$ Shift E+(E* c )$ Shift Shift 76/183
p1 : E E + E p2 : E E * E s a p3 : E ( E ) a p4 : E id Ação a +(b*c)$ a + (b*c)$ Reduce p4 E + (b*c)$ Shift E+ ( b*c)$ Shift E+( b *c)$ Shift E+(b * c)$ Reduce p4 E+(E * c)$ Shift E+(E* c )$ Shift E+(E*c ) $ Shift Reduce p4 77/183
p1 : E E + E p2 : E E * E s a p3 : E ( E ) a p4 : E id Ação a +(b*c)$ a + (b*c)$ Reduce p4 E + (b*c)$ Shift E+ ( b*c)$ Shift E+( b *c)$ Shift E+(b * c)$ Reduce p4 E+(E * c)$ Shift E+(E* c )$ Shift E+(E*c ) $ Reduce p4 E + (E * E ) $ Reduce p2 Shift 78/183
p1 : E E + E p2 : E E * E s a p3 : E ( E ) a p4 : E id Ação a +(b*c)$ a + (b*c)$ Reduce p4 E + (b*c)$ Shift E+ ( b*c)$ Shift E+( b *c)$ Shift E+(b * c)$ Reduce p4 E+(E * c)$ Shift E+(E* c )$ Shift E+(E*c ) $ Reduce p4 E + (E * E ) $ Reduce p2 E+(E ) $ Shift Shift 79/183
p1 : E E + E p2 : E E * E s a p3 : E ( E ) a p4 : E id Ação a +(b*c)$ a + (b*c)$ Reduce p4 E + (b*c)$ Shift E+ ( b*c)$ Shift E+( b *c)$ Shift E+(b * c)$ Reduce p4 E+(E * c)$ Shift E+(E* c )$ Shift E+(E*c ) $ Reduce p4 E + (E * E ) $ Reduce p2 E+(E ) $ Shift E+(E) $ e Reduce p3 Shift 80/183
p1 : E E + E p2 : E E * E s a p3 : E ( E ) a p4 : E id Ação a +(b*c)$ a + (b*c)$ Reduce p4 E + (b*c)$ Shift E+ ( b*c)$ Shift E+( b *c)$ Shift E+(b * c)$ Reduce p4 E+(E * c)$ Shift E+(E* c )$ Shift E+(E*c ) $ Reduce p4 E + (E * E ) $ Reduce p2 E+(E ) $ Shift E+(E) $ e Reduce p3 E+E $ e Reduce p1 Shift 81/183
p1 : E E + E p2 : E E * E s a p3 : E ( E ) a p4 : E id Ação a +(b*c)$ a + (b*c)$ Reduce p4 E + (b*c)$ Shift E+ ( b*c)$ Shift E+( b *c)$ Shift E+(b * c)$ Reduce p4 E+(E * c)$ Shift E+(E* c )$ Shift E+(E*c ) $ Reduce p4 E + (E * E ) $ Reduce p2 E+(E ) $ Shift E+(E) $ e Reduce p3 E+E $ e Reduce p1 E $ e ac Shift 82/183
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 s 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. No entanto, a existência de ferramentas que geram automaticamente analisadores sintáticos (tal como o YACC) eliminou este problema. 83/183
O analisador sintático LR: Left-scan, leitura da frase da esquerda para a direita, 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 - constrói uma derivação à direita, invertida. 84/183
Arquitetura do analisador sintático LR Entrada Pilha Frase$ Reconhecedor Saída Ações Saltos 85/183
Tabelas do analisador sintático LR 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 ). 4. Erro. 86/183
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. 3. LALR ( LookAhead LR ) este método é um compromisso entre os métodos SLR e LR canónico, e funciona para a maior parte das linguagens de programação. 87/183
Reconhecimento de uma frase 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 sa 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, } (Q P) (q, a ) ( ação, q np ) 88/183
- 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 (q, A) q 89/183
Algoritmo: Proc Parser_LR: Inicializar(F) ; Da_Simbolo(a ) ; Repetir acção Oráculo_T(Topo(F), a ) ; Caso ação Seja : 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) ; q Oráculo_S(Topo(F), A) ; Push(F, q) ; Até ac ; 90/183
- 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; m o contexto direito de A. 91/183
Ao conjunto destes três elementos chama-se: Item LR [ w, A d. b, m ] Uma configuração ( s, a ) satisfaz a condição não- LR se : * bm a A cada configuração não-, está associado um e um só item LR. No início : (e, a) [ e, Z. S $, e ] No fim : (S, $) [ e, Z S. $, e ] 92/183
Só existem três formas de itens LR (a que correspondem três tipos de ações): 1) [ ω, A d. a b, m ] a que corresponde a transição: (wd, a r ) (wd a, r) s 2) [ ω, A d. B b, 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) (w A, m) r A cada configuração (s, a ) está associado um item [ w, A d. b, m ] que determina de forma única a ação a executar, sendo necessário verificar que a é derivável de b m. 93/183
Construção do autómato 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 3) S b A A 4) A ε 5) A A a 94/183
95/183
96/183
97/183
98/183
99/183
100/183
101/183
102/183
103/183
104/183
105/183
106/183
107/183
108/183
109/183
110/183
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 ] Exemplo: construir o autómato determinístico LR(0) para a gramática G anterior. 111/183
112/183
113/183
114/183
115/183
116/183
117/183
118/183
119/183
120/183
121/183
122/183
123/183
124/183
125/183
126/183
NOTAS: - Uma redução corresponde a recuar no grafo (com o conhecimento da produção). { redução por S baa } - Num parser LR(0), a redução é feita qualquer que seja o a ; basta que tenha acabado o reconhecimento da produção atual. 127/183
- Neste parser LR(0) podem ocorrer duas situações de conflito : { 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 ]. Para fazer o reconhecimento basta conhecer: - A identificação (número) de cada estado; - Transições ou reduções definidas para esse estado. 128/183
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) =, 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) =, caso contrário q [ Z S.$, e ] e t = $ Oráculo_T (q, t) = AC 129/183
Tabela Oráculo (exemplo anterior): a a b $ S A 1 s3 s4 s2 2 AC t 3 r2 r2 r2 a 4 r4 r4 r4 s5 d 5 s7/r4 r4 r4 s6 6 s7/r3 r3 r3 7 r5 r5 r5 e s o s Oráculo_T Oráculo_S { 2 conflitos redução/transição não é LR(0) } 130/183
Construir o autómato determinístico LR(1) para a mesma gramática: First(S) = { a, b } First(A) = { a, e } Follow(S) = { $ } Follow(A) = { a, $ } 131/183
1 [ Z.S$, ] 132/183
1 [ Z.S$, ] [ S.a, $ ] First($e) = { $ } [ S. b A A, $ ] First($e) = { $ } 133/183
1 [ Z.S$, ] S 2 [ Z S. $, ] AC ( = $) [ S.a, $ ] [ S.bAA, $ ] 134/183
1 [ Z.S$, ] S 2 a 3 [ Z S. $, ] AC ( = $) [ S.a, $ ] [ S.bAA, $ ] [ S a., $ ] red#2 ( = $) 135/183
1 [ Z.S$, ] S 2 a 3 [ Z S. $, ] AC [ S.a, $ ] [ S.bAA, $ ] [ S a., $ ] red#2 ( = $) b 4 [ S b.aa, $ ] 136/183
1 [ Z.S$, ] S 2 a 3 [ Z S. $, ] AC [ S.a, $ ] [ S.bAA, $ ] [ S a., $ ] red#2 ( = $) b 4 [ S b.aa, $ ] [ A., a/$ ] First(A$) = { a, $ } [ A. A a, a/$ ] First(A$) = { a, $ } red#4 ( = a/$) 137/183
1 [ Z.S$, ] S 2 a 3 [ Z S. $, ] AC [ S.a, $ ] [ S.bAA, $ ] [ S a., $ ] red#2 ( = $) b 5 4 [ S b.aa, $ ] A [ S ba.a, $ ] [ A A. a, a/$ ] [ A., a/$ ] [ A. A a, a/$ ] red#4 ( = a/$) 138/183
1 [ Z.S$, ] S 2 a 3 [ Z S. $, ] AC [ S.a, $ ] [ S.bAA, $ ] [ S a., $ ] red#2 ( = $) b 5 4 [ S b.aa, $ ] [ A., a/$ ] [ A. A a, a/$ ] A [ S ba.a, $ ] [ A A. a, a/$ ] red#4 [ A., $ ] ( = $) [ A. A a, $ ] First($) = { $ } First($) = { $ } red#4 ( = a/$) 139/183
1 [ Z.S$, ] S 2 a 3 [ Z S. $, ] AC [ S.a, $ ] [ S.bAA, $ ] [ S a., $ ] red#2 ( = $) b 5 4 [ S b.aa, $ ] [ A., a/$ ] [ A. A a, a/$ ] red#4 ( = a/$) A [ S ba.a, $ ] [ A A. a, a/$ ] red#4 [ A., $ ] ( = $) [ A. A a, $ ] First($) = { $ } red#4 ( = a) [ A., a ] First(a$) = { a } [ A.Aa, a ] First(a$) = { a } First($) = { $ } 140/183
1 [ Z.S$, ] S 2 a 3 [ Z S. $, ] AC [ S.a, $ ] [ S.bAA, $ ] [ S a., $ ] red#2 ( = $) b 5 4 [ S b.aa, $ ] [ A., a/$ ] [ A. A a, a/$ ] A [ S ba.a, $ ] [ A A. a, a/$ ] red#4 [ A., a/$ ] ( = a/$) [ A. A a, a/$ ] red#4 ( = a/$) 141/183
1 [ Z.S$, ] S 2 a 3 [ Z S. $, ] AC [ S.a, $ ] [ S.bAA, $ ] [ S a., $ ] red#2 ( = $) b 5 4 [ S b.aa, $ ] [ A., a/$ ] [ A. A a, a/$ ] A [ S ba.a, $ ] [ A A. a, a/$ ] red#4 [ A., a/$ ] ( = a/$) [ A. A a, a/$ ] red#4 ( = a/$) a 6 [ A A a., a/$ ] red#5 ( = a/$) 142/183
1 [ Z.S$, ] S 2 a 3 [ Z S. $, ] AC [ S.a, $ ] [ S.bAA, $ ] [ S a., $ ] red#2 ( = $) b 5 4 [ S b.aa, $ ] [ A., a/$ ] [ A. A a, a/$ ] A [ S ba.a, $ ] [ A A. a, a/$ ] red#4 [ A., a/$ ] ( = a/$) [ A. A a, a/$ ] red#4 ( = a/$) A 7 [ S b A A., $ ] [ A A. a, a/$ ] red#3 ( = $) a 6 [ A A a., a/$ ] red#5 ( = a/$) 143/183
1 [ Z.S$, ] S 2 a 3 [ Z S. $, ] AC [ S.a, $ ] [ S.bAA, $ ] [ S a., $ ] red#2 ( = $) b 5 4 [ S b.aa, $ ] [ A., a/$ ] [ A. A a, a/$ ] A [ S ba.a, $ ] 7 A [ A A. a, a/$ ] red#4 [ A., a/$ ] ( = a/$) [ A. A a, a/$ ] red#4 ( = a/$) [ S b A A., $ ] [ A A. a, a/$ ] red#3 ( = $) a a 6 [ A A a., a/$ ] red#5 ( = a/$) 144/183
1 [ Z.S$, ] S 2 a 3 [ Z S. $, ] AC [ S.a, $ ] [ S.bAA, $ ] [ S a., $ ] red#2 ( = $) b 5 4 [ S b.aa, $ ] [ A., a/$ ] [ A. A a, a/$ ] A [ S ba.a, $ ] 7 A [ A A. a, a/$ ] red#4 [ A., a/$ ] ( = a/$) [ A. A a, a/$ ] red#4 ( = a/$) [ S b A A., $ ] [ A A. a, a/$ ] red#3 ( = $) a a 6 [ A A a., a/$ ] red#5 ( = a/$) Os Follow's são apenas utilizados para decidir quando devem ser feitas reduções. 145/183
Tabela Oráculo LR(1) (exemplo anterior): a a b $ S A 1 s3 s4 s2 e 2 AC s 3 r2 4 r4 r4 s5 5 s6 r4 s7 o 6 r5 r5 s 7 s8 r3 8 r5 t a d Oráculo_T Oráculo_S { não existem conflitos é LR(1) } 146/183
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 ] e [ B d., n ], com a First(k, n) 147/183
- Conflito redução-redução ao mesmo estado estão associados 2 itens LR(k) da forma: [ A a., m ] e [ B b., n ] com First(k, m) First(k, n). - Uma dada gramática satisfaz a condição LR(k) se não provocar conflitos em nenhum dos estados. 148/183
Exercício: Verificar se a gramática não-ambígua de expressões é LR(0) ou LR(1). Produções: p1: Z S $ p2: S E p3: E E + T p4: E T p5: T ( E ) p6: T id 149/183
{ conflito redução/transição no estado 5 não é LR(0) } 150/183
Construção do autómato LR(1): First(S) = First(E) = First(T) = { (, id } Follow(S) = { $ } Follow(E) = Follow(T) = { +, ), $ } 151/183
- As transições possíveis a partir do estado 1 são: - (Action) símbolos terminais :First(S) ou First(E) ou First(T) = { (, id } - (Goto) símbolos não-terminais: S, E, T - Não ocorrem reduções no estado 1 - First(k, rn) servem apenas para identificar reduções 152/183
153/183
Observações - O autómato LR(0) contem informação insuficiente para resolver os conflitos que 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. - No entanto, existem soluções de compromisso, como sejam: SLR(1) e LALR(1). 154/183
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: [ A a., m ], com m Follow(A), mas não todo o Follow(A). 155/183
Exemplo: Verificar se a gramática G = ( { S, A }, { a, b, c }, S, P ) é SLR(1), em que P contém as seguintes produções: p0: Z S$ p1: S Sb p2: S baa p3: A asc p4: A asb p5: A a Follow(S) = { $, b, c } Follow(A) = { a } Serão resolvidos os dois conflitos LR(0): No estado 6 : conflito redução/transição No estado 10: conflito redução/redução 156/183
157/183
158/183
Tabela Oráculo SLR(1): a a b c $ S A 1 s3 s2 2 s4 AC e 3 s6 s5 s 4 r1 r1 r1 5 s7 6 r5 s3 s8 o 7 r2 r2 r2 s 8 s10 s9 9 r3 10 r4 r1 r1 r1 t a d Oráculo_T Oráculo_S 159/183
Conclusão: 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. ] [ B b. ] 160/183
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. X b ] do estado q é substituído por [ A a. X b, n ], onde n = L (q, A a. X b ) - Deste modo, a redução num estado com o item [ A a., n ] só será efetuada se a Î n. 161/183
- O método LALR(1) resolve conflitos LR(0) quando: Conflito redução-transição: [ A a.ab ] [ B d. ] resolvido quando a Ï L(q, B d) Conflito redução-redução: [ A a. ] [ B b. ] resolvido quando L(q, A a) Ç L(q, B b) = Æ 162/183
Cálculo dos L() LALR(1): Regra 1: Propagação por transição de estados : L(q, A ax. b ) = È L(q, A a. X b ) [A a. X b ] Î q Uma transição de q para q pelo símbolo X, propaga o L() do item [A a. X b ] para o item [A a X. b ]. Regra 2: Concatenação por cálculo do fecho [A a. B r ] ------------------------------- [B.d ] L(q, B. d ) = È First(r L(q, A a. B r )) [A a. B r ] Î q O L() de cada item do fecho de [A a. B r ] obtém-se concatenando r com L(q, A a. B r ) e calculando o First da cadeia obtida. 163/183
Regra 3: Estado inicial q0 e estado final qf L(q0, Z. S $ ) = { e } L(qf, Z S. $ ) = { $ } As regras são aplicadas por travessias iterativas do autómato. 164/183
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 O método LALR(1) vai eliminar o conflito redução/transição no estado 4, que ocorreria por LR(0) e SLR(1). 165/183
LR(0) com conflito redução/transição no estado 4: 166/183
SLR(1) com conflito redução/transição no estado 4: 167/183
LALR(1): 168/183
LALR(1): 169/183
LALR(1): 170/183
LALR(1): 171/183
LALR(1): 172/183
LALR(1): 173/183
LALR(1): 174/183
LALR(1): 175/183
LALR(1): 176/183
LALR(1): 177/183
Tabela Oráculo LALR(1): a id = * $ S L R 1 s5 s6 s2 s4 s3 2 AC e 3 r2 s 4 s7 r5 5 r4 r4 6 s5 s6 s9 s10 o 7 s5 s6 s9 s8 s 8 r1 9 r5 r5 10 r3 r3 t a d Oráculo_T Oráculo_S 178/183
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 - O parser LALR(1) aumenta o poder de análise de LR(1); diminui o AFD e a tabela de parsing do LR(1) 179/183
Construção do AFD LALR(1) a partir do AFD LR(1): Seja a gramática com as seguintes produções: A (A) A a 180/183
LR(1): 181/183
LALR(1): (Fazer também o LALR(1) a partir do LR(0) e verificar se são iguais) 182/183
Otimização das Tabelas de Parsing: - São geralmente matrizes esparsas - Tentativas de compactação.... 183/183