Académique Documents
Professionnel Documents
Culture Documents
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.
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
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.
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.
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 . . .
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
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.
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.