Vous êtes sur la page 1sur 1

Editado por Aleardo Manacero Jr.

Captulo 3 - Anlise sinttica


1. 2. 3. 4. O autmato a pilha Top-down parsing Bottom-up parsing Tratamento e recuperao de erros

A anlise sinttica, ou parsing, o ncleo principal do front-end de um compilador. O parser o responsvel pelo controle de fluxo do compilador atravs do cdigo fonte em compilao. ele quem ativa o lxico para que este lhe retorne tokens que faam sentido dentro da gramtica especificada pela linguagem e tambm ativa a anlise semntica e a gerao de cdigo. Diferente do scanner, o parser definido atravs de uma gramtica livre de contexto, o que nos obriga a utilizar um autmato a pilha na sua implementao. Alm disso, se olharmos para a rvore de derivao sinttica temos que o parser pode percorre-la de duas formas distintas, segundo o sentido em que feita a anlise. A primeira delas quando a leitura feita da esquerda para a direita (como tem que ser sempre) e a anlise sinttica feita sempre atravs do smbolo no-terminal mais a esquerda da expresso. Este tipo de busca (LL) faz com que a anlise seja feita top-down, isto , parte-se do smbolo inicial em direo s folhas da rvore de derivao (terminais). J a segunda forma de anlise (LR) a bottomup, que faz o parsing a partir do smbolo no-terminal mais a direita, embora a leitura tambm seja feita a partir da esquerda. Nos ocuparemos inicialmente da anlise top-down, que conceitualmente mais simples do que a bottom-up. Entretanto temos que deixar claro aqui que, a menos que se queira implementar o parser de forma manual, a tcnica aplicada atualmente a bottom-up, que permite o uso de ferramentas automticas de implementao, tais como o YACC, em que a gramtica especificada e codificada no padro do gerador e este se encarrega de criar o cdigo fonte do parser. A seguir temos a apresentao do autmato a pilha, que a ferramenta bsica na construo do parser (LL ou LR), para depois entrarmos nos detalhes da especificao do analisador sinttico top-down e a seguir o bottom-up, conclundo ento nosso estudo sobre estratgias de parsing, com uma reviso sobre tratamento de erros sintticos.

3.1 - O autmato a pilha


Enquanto o autmato de estado finito no usa estruturas auxiliares, o autmato a pilha necessita de uma pilha para fazer a leitura do string de entrada. Com isso temos que ampliar a definio dada para o AEF de modo a comportar a definio e manipulao da pilha. Assim, um autmato a pilha pode ser definido atravs da composio de uma tupla de sete elementos, descritos a seguir: P = (\SIGMA, Q, \DELTA, H, h0, q0, F) em que, \SIGMA o alfabeto do autmato definidos tambm para AEF's para AEF's Q o conjunto de estados do autmato \DELTA o seu conjunto de regras de transio F o conjunto de estados finais para o autmato q0 o seu estado inicial H o alfabeto definido para a pilha h0 o smbolo inicial na pilha

definidos para pilha

As regras de transio tambm assumem nova forma, uma vez que agora tm que ser definidas operaes sobre a pilha. Uma regra de transio passa a ser: \delta ( q_i, \alfa, \eta) = (q_j, x) que significa que a partir de um dado estado q_i o autmato avana para um outro estado q_j removendo o string \eta da pilha, adicionando ento o string x (que pode ser vazio) pilha, tendo sido disparado pelo smbolo de entrada \alfa. O estado de um autmato a pilha pode ento ser representado atravs de sua configurao, que nada mais do que a tripla dada por q, \omega e x, em que q o estado com a string \omega de smbolos a serem lidos e x sendo uma string definida na pilha. Por exemplo, suponha que um dado autmato pode ser definido por: P0 = ( {a,b,c},{A,B,C},\DELTA, {h,i}, i, A, {} ), onde \DELTA : \delta(A , a , i) = (B , h) \delta(A , c , i) = (A , \lambda) \delta(B , a , h) = (B , hh) \delta(B , c , h) = (C , h) \delta(C , b , h) = (C , \lambda) Que reconhece uma linguagem com igual nmero de 'a's e 'b's separados por um nico 'c'. O autmato parte do estado inicial A, com o smbolo ' em sua pilha. Vejamos ento o que ocorre com suas configuraes durante o reconhecimento da string "aacbb" : -> Aes sobre a pilha config. inicial , l a , pop i , push h , vai para (A , aacbb , i) -> B (B , acbb , h) -> l a , pop h , push hh , vai para B (B , cbb , hh) -> l c , pop h , push h , vai para C (C , bb , hh) -> l b , pop h , vai para C (C , b , h) -> l b , pop h , vai para C (C , \lambda , \lambda) -> para h h PILHA i h h h h vazia CONFIGURAO

ESTADO

3.1.1 - Construndo um autmato a pilha a partir de uma gramtica livre de contexto


Podemos construir um autmato a pilha a partir de uma gramtica livre de contexto de forma bastante simples. Como alfabeto do autmato usamos o prprio alfabeto da gramtica. J para a pilha, temos que seu alfabeto dado pela unio dos smbolos terminais e no-terminais da gramtica, ou seja, H = \SIGMA U N. O smbolo inicial da pilha o smbolo inicial da gramtica. O seu conjunto de estados reduzido a apenas o estado q e suas transies so definidas a partir das seguintes regras: N1. Constroi-se uma transio \delta(q,x,x) = (q,\lambda) para cada smbolo terminal em \SIGMA. N2. Para cada transio (A -> \omega) na gramtica acrescente uma transio \delta(q,\lambda,A) = (q,\omega). EXEMPLO: Dada a gramtica definida por: \SIGMA = {a,b} N = {S} P: S -> aSb S -> \lambda Pela regra N2 temos: R1 \delta(q,\lambda,S) = (q,aSb) R2 \delta(q,\lambda,S) = (q,\lambda) Pela regra N1 temos: R3 \delta(q,a,a) = (q,\lambda) R4 \delta(q,b,b) = (q,\lambda) {devido a S -> aSb} {devido a S -> \lambda}

O problema com as regras de transformao que o autmato obtido , na maioria da vezes, indeterminstico, como mostra o prximo exemplo, baseado na gramtica dada acima e que tenta ler "aabb". Assim como no AEF no queremos trabalhar com autmatos indeterminsticos, pois assim poderiamos deixar de aceitar uma string por termos tomado um caminho errado no processo de anlise sinttica. Diferente do que ocorre com os AEF's, aqui no temos um algoritmo de reduo do autmato indeterminstico para outro determinstico e de mesma funcionalidade. A seguir apresentamos um mtodo de remoo do indeterminismo baseado na leitura antecipada de caracteres (tokens) da sequncia de entrada. EXEMPLO: Dada a gramtica anterior, o parsing para a frase "aabb" dado por: Passo 1 2 3 4a 4b 5 6a 6b 7 8 String (ponto marca o prximo caracter) .aabb .aabb a.abb a.abb a.abb aa.bb aa.bb aa.bb aab.b aabb. Pilha S aSb Sb b aSbb Sbb bb b R1 R3 R2 ? R1 R3 R2 R4 R4 Regr a

aSbbb R1 ?

Como podemos perceber os passos 4 e 6 apresentam duas produes aplicveis cuja escolha pode ser decidida a partir do primeiro caracter ainda no lido.

3.1.2 - Remoo de indeterminismo em autmatos a pilha


Para fazer a remoo do indeterminismo em autmatos a pilha usamos o critrio LL(k), que consiste em olhar os prximos k smbolos da string de entrada para decidir qual produo se utilizaria no processo de aceitao da string. Formalmente, o critrio LL(k) expresso da seguinte maneira: Uma gramtica LL(k) se: S =>* uAz => uxz =>* uvw e S =>* uAz => uyz =>* uvw onde u,v e w so strings de terminais e x,y,z so strings de terminais e no terminais, e |v| igual a k elementos, ento x = y. O que este critrio quer dizer na realidade que se um dado no-terminal apresenta duas possveis transies (A->x e A->y), ento elas ou so distintas ou so a mesma transio. Conjuntos FIRST k e FOLLOW k Para determinar se uma gramtica LL(k), e por consequncia determinstica para k tokens, temos antes que construir alguns conjuntos de verificao, para identificar quantos smbolos so necessrios para que todas as produes da gramtica possam ser diferenciadas apenas pelos primeiros k smbolos da string. Isto feito atravs da definio dos conjuntos First e Follow, segundo as regras apresentadas a seguir. Porm, antes de definirmos estes conjuntos precisamos definir um elemento que aparece constantemente em ambos, que o final da string, representada pelo smbolo \EOS (graficamente seria um T de cabea para baixo). Determinao do First k: Para cada w nas produes (A -> w) constroi-se o conjunto de todas as strings de terminais que tenham k ou menos caracteres, usando para tanto as regras dadas a seguir: R1. FIRST k (u v) = FIRST k (FIRST k (u) FIRST k (v)) R2. FIRST k (N) = U (FIRST k (w)) R3. FIRST k (x) = {x} R4. FIRST k (\lambda) = {\lambda} Determinao do Follow k: Para todo smbolo no-terminal A constroi-se o conjunto de strings de tamanho menor ou igual a k smbolos, em que se usem todas as strings contendo A no lado direito. Para obter esta lista usa-se a regra a seguir: R5. FOLLOW k (A) = U (FIRST k (FIRST k (v) FOLLOW k (B))) Conjunto SELECT k: A definio sobre se uma gramtica ou no LL(k) feita examinando-se o conjunto SELECT k das produes com mesmo smbolo no-terminal a esquerda, isto , produes do tipo A->w1 e A->w2. Caso a interseco dos conjuntos SELECT de tais produes seja vazia, ento a gramtica LL(k). Caso exista algum elemento em comum, ento esse elemento se torna um ponto de ambiguidade da gramtica. O conjunto SELECT k determinado a partir dos conjuntos FIRST e FOLLOW da gramtica, usando para isso a regra abaixo em cada uma das produes da gramtica. SELECT k (A -> w) = FIRST k (FIRST k (w) FOLLOW k (A)) Uma vez determinados todos os SELECT k, passa-se ao exame dos mesmos. Se para cada no-terminal, tivermos diferentes SELECT k para cada uma das produes em que o no-terminal aparece a esquerda, dizemos que a gramtica LL(k). OBSERVAO: Em geral, se uma gramtica no LL(1), ento tambm no ser LL(k) para nenhum k>1. para toda produo B -> uAv para todo w tal que N -> w para todo e qualquer x pertencente ao alfabeto.

3.1.3 - Recurso a esquerda e a direita


Muitas vezes uma gramtica no LL(k) apenas pelo fato de ser recursiva a esquerda, pois neste caso poderiamos ter pares de produes do tipo A -> Ax e A -> y quando se tornaria impossvel sabermos quantas vezes aplicariamos a produo (A -> Ax) antes de aplicarmos a produo (A -> y) no processo de leitura da string "yxxxxxx". Se aplicarmos a transformao desta gramtica recursiva a esquerda em outra recursiva a direita atravs da transformao intermediria em expresses regulares, teremos uma gramtica LL(1), como mostra o desenvolvimento a seguir: 1. Gramtica original para expresso regular: A -> Ax, A -> y ===> yx* 2. Expresso regular para gramtica recursiva a direita: yx* ===> A -> yB B -> xB B -> \lambda 3. Determinao dos conjuntos FIRST, FOLLOW e SELECT: FIRST 1 FOLLOW 1 SELECT 1 A -> yB {y} {\EOS} {y} B -> xB {x} {\EOS} {x} B -> \lambda {\lambda} {\EOS} Claramente podemos ver que no existem coincidncias nos vrios conjuntos SELECT para a gramtica em questo, que se torna portanto LL(1) e determinstica para um caracter adiante daquele em exame. Entretanto, esta transformao acaba por gerar pares de produes que tenham seus fatores esquerda comuns, o que cria novo indeterminismo. A seguir veremos como eliminar fatores comuns esquerda.

3.1.4 - Eliminao de fatores comuns


O processo de eliminao destes fatores tambm se baseia em expresses regulares. Neste caso trabalharemos com um exemplo para ilustrar o processo. Dada a gramtica abaixo, em que ocorrem fatores comuns esquerda, temos: G= A -> xy A -> xz A gramtica pode ser traduzida pela seguinte expresso regular: xy | xz Que pode ser transformada em x(y|z) Que pode ento reescrever a gramtica como: G = A -> xB B -> y B -> z Que no tem fatores comuns esquerda, sendo ento LL(1). Algumas consideraes sobre gramticas LL(k) A partir do exposto no decorrer desta seo podemos concluir que se uma dada gramtica no for LL(1), ento podemos tentar transform-la numa outra gramtica que tenha a mesma funcionalidade da original, i.e. aceite a mesma linguagem, atravs das transformaes de recurso esquerda e da eliminao de fatores comuns esquerda. Isso, entretanto, nem sempre verdade. Num outro aspecto, gramticas ambguas nunca podero ser LL(k), pois exatamente no ponto de ambiguidade teremos que duas produes ambguas levam a caminhos totalmente distintos para representar exatamente a mesma frase na linguagem em questo. Caso a ambiguidade tenha surgido apenas por um engano no momento da especificao, os passos de transformao de recurso esquerda e de eliminao de fatores comuns devero eliminar a ambiguidade. Logo, se aps estas tranformaes a gramtica permanecer ambgua temos que ela inerentemente ambgua.

3.1.5 - Adicionando operadores de expresses regulares


Uma gramtica livre de contexto pode ser expandida com o acrscimo dos operadores de expresses regulares mesma. Os operadores em questo so os de repetio e de alternao ( "*" e "|" respectivamente). O problema nesta alterao est na forma de construrmos os conjuntos SELECT(k), que determinaro se uma gramtica determinstica ou no. No podemos mais calcular os conjuntos SELECT da mesma forma, pois tanto o operador de repetio como o de alternao introduzem pontos de deciso que no apareciam sem os mesmos. Assim, temos as novas regras para calcular o conjunto SELECT apresentadas a seguir: S.1 Se w = x|y ==> SELECT k (x) = FIRST k ( FIRST k (x) FOLLOW k (w)) SELECT k (y) = FIRST k ( FIRST k (y) FOLLOW k (w)) S.2 Se w = x* ==> SELECT k (repete) = SELECT k (x) = FIRST k ( FIRST k (x) FOLLOW k (w)) SELECT k (quit) = FOLLOW k (w)

A partir destas novas expresses para o clculo do conjunto SELECT podemos verificar se uma gramtica ou no LL(k) usando a mesma regra adotada anteriormente, isto , ela LL(k) se e apenas se toda produo que tenha o no-terminal A na sua parte esquerda tiver SELECT k (A -> w) diferentes.

3.2 Top-down parsing


Como j afirmamos, a anlise sinttica feita atravs de uma abordagem top-down mais simples de ser entendida do que a feita bottom-up. O problema dela que a mesma apenas implementada de forma manual, isto , sem o uso de geradores automticos como o YACC. Na realidade, a implementao de um parser top-down consiste basicamente na implementao de um autmato a pilha, no qual para cada smbolo no-terminal criamos uma rotina para caminharmos pelo autmato (e sua pilha evidentemente) a partir das regras de produo disparveis naquele instante. A idia de usar uma rotina para cada um dos no-terminais segue a filosofia dos conjuntos SELECT, pois a anlise LL(k) feita em cima destes conjuntos. Assim, criamos um programa cujo corpo principal representa as produes em que o smbolo inicial aparea na parte esquerda, enquanto que os demais smbolos no-terminais so representados em funes distintas. Alm destas funes principais temos tambm que ter funes auxiliares (ferramentas) para a manipulao da pilha e para a leitura dos tokens de entrada e do prximo token (visto porm marcado como no lido). Logo o parser bastante simples, porm sua implementao acaba sendo tediosa pois temos que tomar cuidado com cada smbolo no-terminal e tambm com todas as possveis alteraes na pilha (inseres e remoes), exigindo mais trabalho do que o que teramos com o uso de um gerador automtico de parsers.

3.3 Bottom-up parsing


Diferentemente do que ocorre com o parsing top-down, aqui a anlise feita a partir do no-terminal mais a direita numa string de terminais e no-terminais. Isto nos coloca o problema de termos que antecipar a regra que geraria tal string para que possamos passar ao nvel acima do atual na rvore de derivao, at chegarmos ao seu topo. Por exemplo, dada a gramtica para expresses aritmticas definida a seguir, encontramos na tabela 1 a decomposio top-down para a expresso "n + n * n" e logo depois a tabela 2, gerada pela anlise bottom-up. G: 1. E -> TS 2. S -> +TS 3. S -> \lambda

4. T -> FP 5. P -> *FP 6. P -> \lambda

7. F -> (E) 8. F -> n

passo 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

caracteres lidos --------n n n n+ n+ n+ n+n n+n n+n* n+n* n+n*n n+n*n n+n*n

string no lido .n+n*n .n+n*n .n+n*n .n+n*n .+n*n .+n*n .+n*n .n*n .n*n .n*n .*n .*n .n .n . . .

pilha .E .TS .FPS .n PS .PS .S .+TS .TS .FPS .n PS .PS

Produo --1 4 8 le entrada 6 2 le entrada 4 8 le entrada le entrada 8 le entrada 6 3

.*FPS 5 .FPS .n PS .PS .S .

Tabela 1 - Traceamento da anlise top-down Como se pode observar, a anlise top-down segue uma estratgia que faz com que ele tente predizer qual ser o prximo caracter a ser lido (alis, se ele for LL(1) ele realmente precisar saber qual o prximo caracter a ser lido). Esta sua caracterstica faz com que algumas vezes ele seja chamado "preditivo". passo 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 caracteres lidos nenhum n. n. n. n. n+. n+n. n+n. n+n*. n+n*n. n+n*n. n+n*n. n+n*n. n+n*n. n+n*n. n+n*n. n+n*n. string no lido n+n*n +n*n +n*n +n*n +n*n n*n *n *n n ----------------n. F. FP. T. T+. T+n. T+F. T+F*. T+F*n. T+F*F. T+FP. T+T. T+TS. TS. E. pilha . Produo --le entrada 8 6 4 le entrada le entrada 8 le entrada le entrada 8 5 4 3 2 1

T+F*FP. 6

Tabela 2 - Traceamento da anlise bottom-up

3.3.1 - Construo de um autmato LR(k)


Um parser bottom-up pode ser construido tambm de forma manual. O processo de construo consiste em criar um AEF para a manipulao dos estados da pilha. Assim, cada estado do AEF vai representar uma classe das configuraes (itens) do autmato a pilha do parser. Um item, que seria ento a unidade fundamental para a operao do AEF pode ser definido por: A -> u.v ; \sigma em que "A -> uv" uma produo na gramtica da linguagem e \sigma uma string de k smbolos terminais. O ponto da expresso acima pode ser entendido como representando a cabea de leitura do AEF quando ele opera sobre a pilha, embora esta no seja exatamente a interpretao correta. As strings u e v representam terminais e no-terminais a esquerda e a direita do ponto, podendo inclusive ser vazias (denotando necessidade de leitura por exemplo). J o string \sigma originado a partir do conjunto FOLLOW k para o no-terminal que aparece no contexto do item e vai ser usado para determinar o lookahead necessrio para aplicar cada produo. Mais adiante veremos como construir os itens e classes de itens para uma dada gramtica. Porm, devemos deixar claro aqui que embora factvel, este processo de construo no feito manualmente por ser extremamente complicado e tedioso (mais do que a construo top-down) e tambm pelo fato de que dispomos hoje em dia de vrios geradores automticos de parsers, tais como o YACC, disponvel no ambiente UNIX. A partir da anlise de como a tabela 2 foi obtida pode-se ver que temos que fazer alguns lookaheads para determinar qual produo seria aplicada naquele instante. Podemos automatizar estas operaes de lookahead atravs de operaes sobre o contedo da pilha, de forma a realizar as operaes de leitura e lookahead sempre que no for possvel aplicar uma produo. As operaes em questo so a de "reduo", "deslocamento" e "aceitao", descritas a seguir:

REDUO -> se os elementos prximos ao topo da pilha casam com o lado direito de uma produo, ento remova-os e coloque o lado esquerdo da produo no topo da pilha. DESLOCAMENTO -> caso no exista produo aplicvel ao topo da pilha, isto , no se possa mais REDUZIR a pilha, fazse a leitura de um token e acrescenta-se o mesmo ao topo da pilha. ACEITAO -> se a ltima operao realizada foi de REDUO e o elemento colocado no topo da pilha for o smbolo inicial, ento a sequncia ser aceita SE e SOMENTE SE no existirem mais tokens para serem lidos.

Ento, o processo de anlise de um autmato LR pode ser descrito atravs destas tres operaes, em que a operao de aceitao indica que ou o string acabou e foi aceito, ou que no foi aceito (se chegamos ao seu final e a pilha no contm o smbolo inicial), ou ainda indica que as produes que tenham o smbolo inicial no seu lado esquerdo apenas devem ser aplicadas quando o string chega ao final. J a operao de deslocamento indica que no podemos mais reduzir a pilha para os tokens j lidos, ou seja, precisamos ler um ou mais tokens para que possamos ter um novo no terminal gerado atravs de alguma produo que use os tokens lidos e o restante da pilha. Por fim, a operao de reduo o nosso principal operador, e atravs dela que conseguimos descobrir quando temos que fazer uma operao de lookahead, que vai ocorrer exatamente no instante em que uma dada configurao no topo da pilha poder ser gerada por mais do que uma produo. Com isso, o processo de construo dos estados do AEF que manipula a pilha do autmato a pilha (e portanto do parser) vai consistir em analisar todas as possveis configuraes da pilha, indicando os itens que compe cada um dos estados e tambm quais aes (reduo, deslocamento e aceitao) que cabem em cada item. Por exemplo, para a gramtica G definida por: S -> E T -> F E -> E+T F -> (E) T -> T*F E -> T F -> n

temos inicialmente o estado q0, que conter inicialmente todos os itens que contenham o smbolo inicial em sua parte da esquerda, ou seja, todas as produes do tipo S -> \alfa, em que o ponto estar a esquerda de \alfa (nada foi colocado na pilha ainda) e o conjunto follow para S. Para a nossa gramtica isso significa: S -> .E; \EOS A partir disso, para o estado em questo (q0), acrescentam-se os itens em que o no-terminal imediatamente a direita do ponto no item acima apaream no lado esquerdo da produo. Esta regra aplicada de forma recursiva at que no existam mais itens que possam ser acresentados por ela olhando-se todos os itens que j faam parte do estado. Assim, para este exemplo teramos os seguintes itens no estado q0: S -> .E; \EOS E -> .E+T; \EOS,+ E -> .T; \EOS,+ T -> .T*F; \EOS,+,* T -> .F; \EOS,+,* F -> .(E); \EOS,+,* F -> .n; \EOS,+, Neste ponto falta ainda determinar quais as aes que cabem ao autmato em cada um dos itens que compoem o estado. As aes possveis so a transio para um novo estado do AEF, que feita movendo-se o ponto um smbolo a direita (e.g. T -> .T*F ==> T -> T.*F), ou ento com a aplicao de reduo, deslocamento ou aceitao quando o ponto no puder ser movido. No caso da transio para um novo estado, sempre que o smbolo (terminal ou no-terminal) a direita do ponto num item do estado atual for igual ao de um outro item do mesmo estado, as transies sero para um mesmo estado. Por exemplo, para o estado q0, tanto "S -> .E" como "E -> .E+T" levam ao mesmo estado, digamos q1. Com isso, o estado q0 fica finalmente definido por: S -> .E; \EOS \delta(q0,E) = q1 E -> .E+T; \EOS,+ \delta(q0,E) = q1 E -> .T; \EOS,+ \delta(q0,T) = q2 T -> .T*F; \EOS,+,* \delta(q0,T) = q2 T -> .F; \EOS,+,* \delta(q0,F) = q3 F -> .(E); \EOS,+,* \delta(q0,() = q4 F -> .n; \EOS,+,* \delta(q0,n) = q5 A partir do estado q0 constroem-se todos os demais estados do AEF. Por exemplo, o estado q1 dado por: S -> E.; \EOS E -> E.+T; \EOS,+ reduo "S -> E" \delta(q1,+) = q6

Enquanto que o estado q2 dado por: E -> T.; \EOS,+ T -> T.*F; \EOS,+,* reduo "E -> T" \delta(q2,*) = q7

Desta forma podemos ir construindo todos os estados para o AEF, num processo bastante demorado para ser feito de forma manual. Por exemplo, para essa gramtica seriam gerados 22 estados para o autmato e um total de 59 itens.

3.3.2 - Outros parsers LR(k)


O parser construdo dessa forma chamado LR cannico, pois construdo usando todos os recursos disponveis para evitar conflitos (incluindo-se especificao de caracteres examinados no caso de lookahead). Entretanto ele gera um nmero exagerado de estados (22 no pequeno exemplo dado), o que o torna um tanto quanto complicado. Existem diversas modificaes nessa forma de tratar o problema com o objetivo de diminuir o nmero de estados no AEF. Os vrios mtodos diferem basicamente na forma como so tratados os conjuntos FOLLOW em cada caso. Assim sendo, temos inicialmente o mtodo SLR (de Simple LR), em que simplesmente so ignorados os conjuntos FOLLOW. Assim, diversos estados que se diferenciavam apenas por seus FOLLOW agora passam a ser indistinguveis e portanto so aglutinados num nico estado. Com isso, no exemplo dado teramos apenas 12 estados usando a construo SLR. O problema com esta construo que a possibilidade de conflitos aumenta e muito, fazendo com que algumas linguagens que so LR (1) deixem de ser SLR(1), isto , no possam mais resolver conflitos apenas com a leitura de um caracter de lookahead. A nossa conhecida linguagem C cai nesta categoria, ou seja, ela LR(1) mas no SLR(1). Como a reduo no nmero de estados obtida com o SLR bastante elevada alguns pesquisadores buscaram abordagens intermedirias, isto , abordagens que reduzissem o nmero de estados (nem que no fosse de forma dramtica como o SLR) mas que tambm diminussem a possibilidade de conflitos na gramtica. Disso surgiu a abordagem LALR (de LookAhead LR). No LALR estados que difiram apenas em seus FOLLOW so unidos, sendo que agora os conjuntos FOLLOW passam a conter a uniao dos FOLLOW dos itens aglutinados. Assim ocorre uma reduo acentuada no nmero de estados mas se mantm uma certa capacidade na resoluo de conflitos, muito embora ainda ocorram casos em que uma gramtica seja LR(1) mas no LALR(1).

3.4. Tratamento e recuperao de erros


Entre as vrias funes exercidas pelo parser, uma das mais importantes o tratamento de erros de sintaxe, que so aqueles em que a gramtica da linguagem no obedecida, ou ainda, aqueles em que o processo de parsing no produz uma pilha vazia ao final do string de entrada. Assim, no esto sendo considerados aqui os erros de escopo de variveis, os quais so deixados para o analisador semntico (muito embora implementados em conjunto pelo YACC, atravs da gramtica de atributos). Os erros sintticos no so difceis de serem encontrados, uma vez que surgem do prprio processo de parsing. Ocorre no entanto, que apesar de facilmente localizveis eles no so particularmente fceis de serem tratados, isto , existem alguns problemas no seu tratamento que esto relacionados com a extenso do erro, ou seja, onde que o erro comea e onde que ele acaba. Problemas com a determinao da extenso do erro so facilmente percebidos se tentarmos fazer o parsing de um programa como o dado abaixo:

main () {int a,b=1; float c,e, if (b) a=0; c= b-a; e= c**2; } Nela, aps a deteo do erro ao lermos o token if, a partir do qual o autmato bloquearia, temos que encontrar uma forma de retomar o processo de compilao, para irmos at o final do arquivo. Isso feito mais por aspectos histricos do que por necessidade real. Acontece que nos primrdios da computao os programadores tinham que esperar muito tempo pelo resultado da compilao. Assim, se a cada erro o compilador fosse abortar todo o processo, teramos que tentar a compilao muitas vezes, ocupando dias de atividade, at depurarmos a sintaxe do programa. Com isso, era necessrio que o compilador sempre indicasse todos os erros que fossem identificveis numa nica passagem, para agilizar o processo de depurao. Isso persiste at hoje, muito embora os computadores atuais no nos forcem a ter tal atitude. O maior problema para fazer com que o compilador chegasse ao final do programa era determinar a partir de que token o mesmo teria que retomar a compilao, isto , quando que o atual erro deixaria de influenciar no restante do programa. A soluo para esse problema pode ser encontrada de maneiras distintas, iremos aqui apresentar quatro delas. 1. Recuperao no nvel de frase: Embora este mtodo no tenha muita aplicao prtica, o mesmo serve como base para entendermos qual , de fato, o processo de recuperao de erros. O que se faz nesse mtodo tentar corrigir o fonte (de modo imaginrio claro), atravs de insero, deleo ou modificao de tokens, para atingir um parsing correto. Se o mesmo for feito no nvel de frase, ou comando, o que se busca a alterao da frase para que ela passe a ser considerada como gramaticalmente correta, num processo de minimizao de erros ou alteraes. O problema desse mtodo que o mesmo complexo e ainda pode causar a entrada em loops, ao inserir tokens que acabem exigindo sempre a insero de mais tokens para fazer a correo da frase. 2. Correo global de erros: Esse outro mtodo que, embora teoricamente vlido, no usado na prtica. Aqui o processo de correo do fonte (para atingir um parsing correto) feito sobre todo o programa e no apenas sobre frases isoladas. Como se pode perceber, os problemas apresentados no mtodo anterior apenas so agravados com essa estratgia. 3. Recuperao por pnico: Aqui temos o mais simples de todos os mtodos de recuperao, que vai simplesmente ignorar todos os tokens seguintes ao ponto em que o erro foi determinado, at encontrar um token que tenha, de fato, uma posio precisamente definida dentro de qualquer programa. Assim, tokens como '{' , '}' , 'while' , 'for', etc., que marcam precisamente ou o incio ou o final de um bloco de comandos (frases) podem ser usados para determinar o momento de reincio do parsing. A simplicidade desse modelo faz com que ele seja bastante usado quando fazemos a implementao do compilador de forma manual, mas peca pelo fato de que pode acabar por ignorar alguns erros que estejam embutidos entre os tokens no analisados pelo parser. 4. Produes de erros: A contrapartida ao mtodo de recuperao por pnico dada pelos mtodos de implementao automtica de compiladores , justamente, a gerao de produes de erros, que nada mais so do que produes inseridas na gramtica, disparveis apenas quando o autmato bloquearia em determinados estados. Assim, o disparo de uma dada regra faz com que o autmato no bloqueie naquele estado, mas que ao invs disso seja gerada uma mensagem de erro, indicando seu tipo, e que o processo de parsing possa prosseguir normalmente, como se nada tivesse ocorrido. O grande obstculo para esse mtodo a dificuldade em se obter as produes de erros, isso , obter produes que identifiquem o erro e faam a recuperao do mesmo. Alm disso, muito difcil usarmos essa estratgia para compiladores feitos manualmente, pois elas apenas estariam aumentando (e muito) o nmero de produes e estados do autmato. Para a implementao usando YACC ou outro gerador qualquer, o acrscimo dessas produes no representa obstculo para a implementao.

Vous aimerez peut-être aussi