Está en la página 1de 311
Como Construir um Compilador Marcio Eduardo Delamaro © 1 Introdugio 11 12 1.3 © que faz um compilador ... 2-2-2 Os componentes de um compilador.. 2.0.2. 20.00.00.00- SIRS AS ree aptae AAS cu tec el nl kw gen a ned, whan Hh eee WS oi areebradoe wintRlioo) ee we ee ee Tee onal senate ee 1.24 0 gerador de codigo 0 JavaCC eo Jasmin 2 A Linguagem X++ Pi) 2.2 2.3 24 2.5 2.6 2h Alfabetos, palavras e linguagens........- 0. Gramitica livre de contexto Forma de Backus-Naur... 22.0002 0-020 ee eee BNF para a linguagem X**. 0.02 ee ee IG rar ES ec a ere Semantica da linguagem X44. oo.0f Gian suldueae atrcurisy § 8 Urmprogramacem Xt a ogy a ete ear g 3 Anflise Léxica 3.1 3.2 3.3 3.4 3.5 3.6 pecstomnatoertinitoc ve i Woe eae eg coe Expresses regulares . O analisador léxico da linguagem X+* Comentarios. . . . Recuperagio de erros Iéxicos . ‘Arquivos-fonte do compilador ..... . . i 12 15 37 37 46 ar 54 56 59 vi COMO CONSTRUIR UM COMPILADOR 4 Anilise Sintatica 61 4.1 Anélise sintética descendente recursiva..- 2... 0... e cee ee 61 4.2 O analisador sintético da linguagem X++ 2.2.2... .....0... 68 4.3 Arquivos-fonte do compilador 81 5 Tratamento de Erros Sintaticos 85 5.1 O método de ressincronizagio.........-.-.......0-5 85 5.2 Implementag&o da recuperagio de eros... 2... ee 89 5.3 Algumas possfveis melhorias... . . . ng LSS eee ee 107 5.4 Arquivos-fonte do compilador .. . . . <4 a ere 117 6 Geragdo da Arvore Sintatica : 123 6.1 Oqueéa arvoresintatica 2.2.2... 2... eee e128 6.2 Implementagio da Arvore sintética ... 2... ..2....2.00.- 125 6.3 Arquivos-fonte do compilador ....-..-........--2000- 167 7 Exibigdo da Arvore Sintatica 169 eee en AO SE 8.2 A tabela de simbolos para X++.... . . ie eee 187 83 Implementagdo da tabela de simbolos .................. 191 9 Anélise Semantica — Primeira Parte 199 9.1 Anélise semfntica em fases .. 2... ....-.....0000- 2199 9.2 Anélise da declaragio de classes... 6-20... 0000-20000 202 9.sieImplementacno =e ene. = ee ee eee ens) 9'die © foropramasprincipaleg = ee est. rr 208 9.5 Arquivos-fonte do compilador . ee Aer eee - 208 SUMARIO vii 10 Andlise Semantica — Segunda Parte 209 10.1 Andlise da hierarquia de classes... . 0. eee eee eee 209 10.2 Anéllise da declaracéo de varidveis e métodos ...--......... 212 10: Ojprosramnypriucipal’ sages se ee 218 10.4 Arquivos-fonte do compilador ........-...--.0.- e219) 11 Andlise Semantica — Parte Final 221 11.1 Checagem de tipos a2 11.2 Anéllise das declaragdes . 226 11.3 Anflise dos comandos ...... 0-000 e eee ee eee Be e235 11.4 Anélise de expressdes.... . . eet rte res i oe 248, 11.5 Arquivos-fonte do compilador ...........-0-.0...2005 259 12 Geragao de Cédigo 261 de ipAG Meu uiuahvictunl avalon etree ett te ey ae 261 a7 (Oy Aseanblan para Gavan ss atari @ece, uncon. 0 tf 265 Hop einplementacanee ea eee ee ter ves 267 12.3.1 Geragio de eédigo para os comandos..............- 27 12.3.2 Geragio de codigo para as expressbes =... 7... 283 12.3.3 Algumas restrigbes . . + 288 12.4 O runtime X*+.. . 289 12.5 Arquivos-fonte do compilador . 291 : T2i6 ecemplosiee rs epee . 292 | A Exercicios 297 B JavaCC 299 © Jasmin 305 Prefacio Um livro sobre compiladores deveria apresentar uma visio geral sobre técnicas de compilagio, ferramentas, aplicages etc. Este livro ¢ bem mais modesto, visto que mostra como construir um compilador, utiliza técnica e ferramentas especificas para tal intento. Mesmo assim, ser4 atil a cursos de graduagéo que se proponham também a uma abordagem prética, a incentivarem os estudantes a “colocarem as miios na massa” ¢ construfrem seus préprios compiladores. Dessa forma, poderd servir como um guia a ser utilizado em cursos de compiladores, em paralelo a aulas que ensinem a teoria de forma mais ampla. ‘Também poder4 auxiliar aqueles que, embora no sendo “da area” de compiladores, necessitem em algum momento da vida académica ou profissional utilizar técnicas de compilagéo. Este ¢ o caso do autor, cuja drea principal de atuag&o é a de teste de software. Atuando nessa area, o desenvolvimento de ferramentas de andlise de programas é essencial, assim como 0 dominio, mesmo que de forma. basica, de técnicas para a construgéo de compiladores. Com esses objetivos em mente, procurou-se manter o mais simples possivel o estilo deste livro. Escolheu-se um método de anilise sintatica ¢ ferramentas para sua implementacao féceis e que possam ser compreendidos com algum esforgo mesmo por “principiantes”. E mostrada, de forma detalhada, toda a implementagio de um compilador para uma linguagem, embora restrita, com caracteristicas importantes. Em particular, uma linguagem orientada a objetos. Ao final do “projeto” teré o leitor a satisfagio de desenvolver seu proprio compilador, que poderé ser utilizado para implementar programas reais, que executem em um ambiente real, que é a Maquina Virtual Java. Pré-requisitos Espera-se do leitor conhecimentos prévios sobre diversos assuntos. © primeiro tépico seria para que serve e para que desejamos construir um compilador. O segundo item necessério & utilizagio deste livro é 0 dominio de alguma linguagem de programagéo, de preferéncia Java, pois, além de ser utilizada como linguagem para a implementagdo do compilador, também 0 ¢ como referéncia ¢ comparagao para a linguagem-alvo do compilador. Conhecimentos basicos de linguagens formais xii COMO CONSTRUIR UM COMPILADOR © autOmatos finitos, embora ndo indispensaveis, certamente ajudardo na compreensio do texto. Além de, 6 claro, muita paciéncia para seguir os intermindveis trechos da imple- tmentagao inclufdos no texto... Agradecimentos Agradeco todos os que colaboraram de alguma forma na elaboragio deste livro. Em particular, a meus alunos de compiladores Lift, Beto, Gustavo(s), Dani(s), Gisele, Andréia, Paulinha, Neves, Juliano, Juliana. Este talver seja o primeizo e iltimo livro que escrevi. Assim, esta é uma oportuni- dade tnica de expressar minha gratidao ao prof. José Carlos Maldonado pela amizade, orientagao e oportunidade de trabalharmos juntos. Se em cada uma das conquistas de minha carreira académica posso sentir a influéncia do amigo Maldonado, neste livro ndo seria diferente. Por isso, muito obrigado. Download dos programas Todos os cédigos-fonte dos programas que aparecem neste livro podem ser encontrados em http: //www novateceditora.com.br/downloads..php. Capitulo 1 Introducao leitor que se interessa por construgio de compiladores provavelmente j& saiba 0 que @e para que serve um compilador. Por isso, vamos apenas brevemente discutir quais so as fungdes desempenhadas por um compilador. Também abordaremos como se estrutura internamente um compilador e comentaremos sobre a ferramenta JavaCC, um gerador de compiladores que sera utilizado neste texto. 1.1. O que faz um compilador Hi uma frase famosa que diz: “Um programa de computador é uma seqiiéncia de 0s ¢ 1s armazenada na sua memséria”. Essa seqiéncia de 0s ¢ 1s pode representar dados tais como mimetos inteitos, strings, registros etc., ou pode ser uma instrucao, que indica como 0 computador deve se comportar. A meméria é dividida em palavras, cada qual possuindo um endereco (por exemplo, 0, 1, 2, ...) que a identifica de maneira tinica. Embora seja armazenado na meméria, todo programa 6 executado na CPU do computador. A CPU possui registradores que guardam temporariamente dados sobre os quais se desejam realizar operagdes. Por exemplo, para somar um ntimero arma- zenado na meméria no endereco 100 com o nfimero armazenado no endereco 101 colocar o resultado no endereco 102, a CPU deveria executar operagdes como: * copiar o contetido da posigiio de meméria 100 para o registrador A; ¢ copiar 0 contetido da posigéo de memoria 101 para o registrador B; © somar o contetido de B em A; * copiar 0 contetido de A para a posigéo de meméria 102. Com 0 aumento da complexidade dos programas de computador, tornou-se neces- sério desenvolvé-los num nivel de abstracdo um pouco mais elevado, menos dependente das instrugdes de uma determinada maquina. Foram criadas, assim, as linguagens de 2 COMO CONSTRUIR UM COMPILADOR alto nfvel, que substituem as instruges dos computadores por comandos cujas utili- zagio e compreensio so mais faceis. Por exemplo, o comando e=ath; indica que queremos somar o contetido de uma posicio de meméria que foi chamada pelo programador de a com o contetido da posicio de meméria chamada de b ¢ colocar o resultado na posicao de meméria chamada de ¢. Se as linguagens de programagao mudaram radicalmente, os computadores num certo sentido permanecem inalterados e s6 conseguem “entender” instrugdes simples € que estejam no seu repertério. Por isso, programas escritos em linguagens de alto nivel precisam ser convertidos nessas instrugdes antes de serem executados. Essa é a principal atribuicao de um compilador: transformar um programa escrito numa lin- guagem de alto nivel — que chamamos de linguagem fonte ~ em instrugdes executaveis por uma determinada maquina ~ que chamamos de cédigo objeto. PROGRAMA PROGRAMA, FONTE. > omer (a) oat oy. 1 SES fo NG (b) Fonts Figura 1.1 ~ Processos de (a) compilaco e (b) linkedigao Isso 6 resumido na figura 1.1(a). Em geral, 0 compilador recebe como entrada um arquivo contendo 0 programa na linguagem fonte (0 programa fonte) ¢ produz como safda um outro arquivo com o c6digo objeto (0 programa objeto). Muitas vezes esse programa objeto 6 apenas parte do programa como um todo. A maioria das linguagens de programac&o e seus respectivos compiladores permitem que um programa seja dividido em diversos arquivos-fonte e respectivos objetos. Nesse caso @ necessdrio mais um passo para preparar o programa para execugao (Figura 1.1(b)). Um linkeditor junta. todos os programas objetos, formando o programa executavel, que est pronto para ser carregado na meméria do computador e executado. CAPITULO 1-INTRODUGAO 3 1.2 Os componentes de um compilador ‘Tradicionalmente, a construgéo de um compilador 6 dividida em partes, cada uma com uma fungao espectfica. Em geral, podemos identificar em um compilador: * 0 analisador léxico; © o analisador sintatico; © o analisador semantico; © o gerador de cédigo. 1.2.1. Oanalisador léxico © analisador léxico (AIL) encarrega-se de separar no programa fonte cada simbolo que tenha algum significado para a linguagem ou de avisar quando um stmbolo que nao faz parte da linguagem é encontrado. Por exemplo, vamos supor um programa fonte com a seguinte seqiiéncia de s{mbolos: 193 x1 ; y2 true begin Ao analisar tal entrada, o analisador léxico deveria identificar a ocorréncia de 6 sim- bolos. Além disso, o analisador deve categorizar cada um deles, indicando de que tipo 6 aquele simbolo. No caso anterior, teriamos: © 123 ~ constante inteira; « x1 — nome de variavel ou procedimento; * 5 —simbolo especial “ponto-e-virgula”; @ y2— nome de variavel ou procedimento; * true — constante booleana; © begin ~ palavra reservada. Obviamente essa classificagao depende de qual linguagem fonte est sendo anali- sada. A classificagao pode ser real para a linguagem Pascal, mas certamente no 0 6 para Java, onde a palavra “begin” nfo tem nenhum significado especial e pode ser utilizada, por exemplo, como nome de varidvel. Assim, para determinar quais sio os simbolos que devem ser reconhecidos pelo AL, devemos recorrer descrigo da linguagem. O AL deve também avisar quando um simbolo invélido aparece no programa fonte. Por exemplo, se o simbolo ‘@’ aparecer num programa sendo analisado por um compilador Java, 0 AL deve avisar que um simbolo invélido foi encontrado, pois ‘? no faz, parte de nenhuma categoria de simbolos aceita pelo AL. 4 COMO CONSTRUIR UM COMPILADOR O funcionamento do AL parece bastante simples. Tudo o que ele deve fazer “quebrar” o programa. fonte em s{mbolos e verificar a que categoria eles pertencem. A verdade nao ¢ bem essa, Existem diversos complicadores para essa tarefa. O primeito deles 6 que a entrada nem sempre esté to bem arrumada como a que mostramos. Por exemplo, como seria analisada a seguinte entrada? 123xibegin{end Ela deveria ser dividida (em Pascal): 123 — constante inteira; x1begin ~ nome de variavel ou procedimento; { —simbolo especial “abre chave”; end — palavra reservada. Outro fator que dificulta a construc do AL 6 que este pode encontrar-se em “estados” diferentes, de acordo com os caracteres que sfio encontrados no programa fonte. Por exemplo, dissemos h4 pouco que a presenca de um ‘@” no programa fonte causa um erro denominado erro léxico. Mas isso ndo ¢ verdade sempre. Se aparecer no programa fonte “Aqui @ temos uma arroba" o AL no deve apontar um erro léxico em “Aqui @ temos uma arroba’, ¢, sim, reco- nhecer uma constante do tipo string. Algo semelhante acontece com os comentarios. Ao encontrar, por exemplo, o simbolo (* o AL entra num estado em que deve ignorar tudo que aparega até o préximo *), até mesmo simbolos normalmente nfo validos na linguagem. No capftulo 3 veremos como identificar e classificar os simbolos de uma linguagem e como construir um AL. 1.2.2 Oanalisador sintatico © analisador sintatico (AS) ¢ 0 “coracao” do compilador, responsavel por verificar se a seqiiéncia de simbolos contida no programa fonte forma um programa valido ou nao. Note o leitor que o AL se encarrega de identificar os simbolos que aparecem no programa fonte, mas no se preocupa em verificar se a ordem em que eles aparecem € valida ou no. Essa é uma das atribuigdes do AS. Por exemplo, considere o seguinte trecho de programa: if (a - 10>b * 2) a CAPITULO 1-INTRODUGAO 5 O AS deve ser capaz de analisar esse programa e reconhecé-lo como vilido. Para isso, o AS precisa saber que apés a palavra reservada if deve vir um “(”, uma expresso eum)”, Depois disso, deve vir um comando qualquer, por exemplo, uma atribuigao Para isso, o AS é construfdo sobre uma gramAtica que descreve a linguagem fonte. Essa gramética é composta de uma série de regras que descrevem quais sio as cons- trugdes validas da linguagem. O AS deve aceitar aqueles programas que seguem essas regras ¢ rejeitar — indicando a ocorréncia de um erro sintatico — aqueles que as violam. No capitulo 2 descreveremos por meio de uma gramatica a linguagem X*++, que seré utilizada neste texto para a construgdo de um compilador. Também comentaremos mais sobre a utilizagdo de gramaticas para descrever linguagens. Além de aceitar os programas sintaticamente corretos e rejeitar os incorretos, 0 AS desempenha ainda outra importante funcdo que ¢ a construgio da arvore sintti- cado programa fonte. Uma arvore sintdticaé uma estrutura em forma de rvore que descreve as construgées da linguagem reconhecidas pelo AS no programa fonte. Se 0 programa fonte possui um comando if como aquele visto ha pouco, sua drvore sinté- tica deve espelhar esse fato e descrever como esse comando ¢ formado. Poderiamos ter para aquele comando a arvore sintatica da figura 1.2, que indica que 0 comando if ¢ formado por uma expressiio e um comando de atribuig&o. A expressio, por sua vez, ¢ também representada por uma subérvore cuja raiz é uma operagiio de comparagio >, que tem como sub Arvores uma sub expressao de subtragao e uma sub expressao de multiplicagao. if Figura 1.2 — Exemplo de rvore sintatica. Nos capitulos 4 e 5 estaremos tratando com detalhes do AS. Nos capitulos 6 e 7 trataremos de érvore sintatica. 6 COMO CONSTRUIR UM COMPILADOR. 1.2.3 Oanalisador semantico O analisador semAntico (ASem) ir4 verificar se os aspectos semAnticos do programa estao corretos, ou seja, se nao existem incoeréncias quanto ao significado das constru- Ges utilizadas pelo programador. O analisador semantico nao utiliza mais o programa fonte para fazer tal verificacdo. Em vez disso, utiliza a érvore sintatica como representacdo do programa. O analisador semantico deve procurar por incoeréncias como: © tipos de operandos incompattveis com operadores. Se tivermos o comando a=b * cea varidvel c foi declarada do tipo string, entao o analisador semantico deve apontar um erro semantico, pois esse tipo de operando nao compativel com o operador *; © variveis ndo declaradas; © redeclaragao de variéveis; « chamadas de fungdes ou métodos com o nfimero incorreto de parametros} * comandos colocados fora de contexto. Por exemplo, a utilizagao de um co- mando continue fora de um comando de laco deve ser apontada como um erro semantico. Esses erros no sfio detectados pelo AS, pois nao constituem erros sintdticos. De acordo com a gramatica da linguagem fonte, uma varidvel c pode ser utilizada em uma expresso como a = b * c, nao importando se foi declarada anteriormente ou nao, ou qual é 0 seu tipo. Para desempenhar seu papel, o ASem depende de uma tabela de stmbolos. Nela so armazenadas informagées de varidveis declaradas, funcdes ou métodos, tipos ow classes. Somente com a tabela de simbolos é possivel realizar a validagio semantica do programa. Por exemplo, ao analisar o comando a = b * c (ou melhor, a rvore sintética correspondente a esse comando), o ASem precisa saber se cada uma das varidveis envolvidas foi previamente declarada e o tipo de cada uma delas. Para tanto, 0 mesmo ASem deve, ao analisar um comando de declaragéo como int c, incluir na tabela de s{mbolos a varidvel c, indicando, entre outras coisas, que seu tipo é int. No capitulo 8 trataremos de tabela de simbolos. Nos capitulos 9 a 11, iremos estudar como funciona um analisador semantico. 1.2.4 Ogerador de codigo Uma vez verificado que no existem erros sintaticos ou semanticos, 0 compilador pode realizar sua tarefa, que 6 a criag&o do programa objeto. Em geral, o programa objeto 6 armazenado num arquivo que pode ser posteriormente linkeditado com outros pro- gramas objetos ou simplesmente “carregado” na meméria do computador e executado pelo sistema operacional. CAPITULO 1-INTRODUGAO 7 O programa objeto reflete, mediante instrugdes de baixo nivel, os comandos do programa fonte. Como cada maquina ou cada plataforma possui um conjunto dife- rente de instrugdes e de meios de acesso ao sistema operacional, em geral é necessArio que exista um gerador de c6digo distinto para cada plataforma. A otimizagao do cédigo também pode fazer parte do processo de geragio de ¢6- digo. Nela sao aplicadas diversas técnicas para otimizar algumas caracterfsticas do programa objeto, como, por exemplo, seu tamanho ou sua velocidade. A geracio de cédigo seré tratada no capitulo 12. Em vez de nos concentrarmos em uma arquitetura particular, iremos utilizar a Maquina Virtual Java (JVM) como méquina-alvo do nosso compilador. A JVM é um programa capaz de executar (na verdade, interpretar) arquivos “.class” que sao gerados por compiladores Java. Assim como qualquer outra maquina, possui um conjunto de instrucdes que sero utilizadas em nosso compilador para representar os comandos de alto nivel da nossa linguagem fonte. O nosso compilador nao gera diretamente um arquivo “class”, mas, sim, um arquivo em que as instrucdes da JVM podem ser visualizadas, como numa linguagem de montagem. Tal arquivo pode ser posteriormente processado e transformado num “class”, utilizando-se 0 programa Jasmin, descrito a seguir. Na verdade, essa abor- dagem de gerar-se um c6digo intermediério utilizando uma linguagem de montagem, que depois ¢ transformado no programa objeto, ¢ também utilizada em compiladores reais, 1.3 OJavaCC eo Jasmin Neste texto estaremos utilizando o programa JavaCC para criar um compilador para uma linguagem simples, apresentada no capitulo 2. Esse programa 6 um gerador de compiladores, ou mais precisamente um gerador de analisador sintatico. Ele toma como entrada uma gramAtica ¢ transforma-a num programa Java capaz de analisar um arquivo e dizer se satisfaz ou nao as regras especificadas nessa gramatica. Ele também oferece facilidades para a construgao da arvore sintatica. Ao descrever a gramética, pode-se também indicar como a Arvore sintatica deve ser construida, incorporando-se cédigo para realizar tal tarefa ao analisador sintatico gerado, . Diferentemente de outros geradores de analisador sintético, 0 programa gerado pelo JavaCC realiza um tipo de anilise sintatica chamada de top down ou anélise descendente. Outro ponto que difere o JavaCG de outros programas similares 6 que aquele permite que sojam definidos o analisador Iéxico e o analisador sintatico de uma so vez. Em geral, outros programas sfo utilizados aos pares (Lex ¢ Yace; Flex ¢ Byson), um para gerar o analisador léxico e outro, para o analisador sintatico. O JavaCC ¢ um produto de propriedade da Sun Microsystems e liberado sob a li- cenga Berkeley Software Distribution (BSD). Pode ser obtido na Internet no enderego https: //javacc.dev. java.net/. Neste texto trataremos dos aspectos do JavaCC que interessam A construg&o de nosso pequeno compilador. A cada etapa da cons- trugéo do compilador serio dados mais detalhes do funcionamento do JavaCC. Nao temos, porém, a intencio de abordar todos os detalhes de uso deste software. Para tal, o leitor deve verificar a documentacao que acompanha o software e as indicagbes de outras fontes de informacio que se encontram no Apéndice B. 8 COMO CONSTRUIR UM COMPILADOR O programa Jasmin é uma interface de montagem para Java (Java ASseMbler IN- terface), que toma como entrada um arquivo ASCII com instrugdes JVM e produz um arquivo executével JVM (arquivo “class”). Esse programa pode também ser obtido na Internet, no enderego http://www. cat .nyu.edu/“neyer/jasmin. Ele sera utili- zado caso o leitor queira produzir um arquivo executével a partir do cédigo gerado pelo compilador que iremos construir neste texto. Em outras palavras, nosso compi- lador gera um programa JVM que esta no formato ASCII e que pode ser facilmente visualizado. Assim podemos verificar se 0 cédigo gerado esta correto ou nao. Porém, para que possamos executar esse cédigo, precisamos executar mais uma tarefa, que 6 transformé-lo num arquivo executével Java, utilizando o programa Jasmin. Jasmin 6 parte do material utilizado no livro Java Virtual Machine de Troy Drowing e Jon Meyer, publicado pela O’Reilly e que descreve a Maquina Virtual Java. O Apéndice C apresenta também uma pequena introdugio ao uso do Jasmin. Capitulo 2 A Linguagem x++ Antes de continuarmos com a construg&o do nosso compilador, estudando como fun- ciona cada uma de suas partes, precisamos definir a linguagem-alvo dele. Bo que faremos neste capitulo. Poderfamos utilizar linguagens de programagao como Java ou C++. Contudo, essas linguagens so complexas demais para os objetivos deste livro. Em vez disso, iremos definir uma linguagem que seja mais simples, mas que possa dar ao leitor uma visio completa de como um compilador funciona. A linguagem que utilizaremos — e chamaremos de X** — 6 uma linguagem orientada a objetos semelhante & Java, porém ligeiramente mais simples. Para definir a sintaxe da linguagem, iremos antes relembrar alguns conceitos sobre linguagens formais e como podemos defini-las. Iremos tratar de alfabetos, linguagens, graméticas e outras formas comumente usadas para definir-se a sintaxe de linguagens de programacio. 2.1 Alfabetos, palavras e linguagens Inicialmente, definimos alfabeto como um conjunto finito ¢ nfo vazio de s{mbolos. ‘Assim, por exemplo, os seguintes conjuntos so alfabetos: * {0.1}; © {0,1,2,3,4,5,6,7,8,9,0}; # {abeynz}s © {a, b, ab, abc}. Cada elemento de um alfabeto ¢ chamado de uma letra. Note-se que neste contexto 0 termo letra tem um significado diverso daquele que estamos acostumados a usar ¢ que poderia ter como sindnimo algo como “caraetere”. Assim, 0 quarto alfabeto possui 4 letras: a, 8, ab e abc (essas duas tltimas com trés “caracteres”). 2 10 COMO CONSTRUIR UM COMPILADOR ‘Uma palayra ou cadeia sobre um alfabeto © é uma tupla ordenada de letras de 5. Por exemplo: * (0,1,0,1,1,0) é uma palavra sobre {0,1}; * (2,1,0,8) ¢ uma palavra sobre {0,1,2,3,4,5,6,7,8,9}; © (¢,0,m,p,i,l,¢,r) 6 uma palavra sobre {a,b,..., 2}; * (a,ab,b, abe) 6 uma palavra sobre {a, b, ab, abc}. Em geral, podemos representar uma palavra apenas aglutinando, na ordem cor- reta, as letras que a compéem. Por exemplo, podemos escrever © (0,1,0,1,1,0) como 010110; * (2,1,0,8) como 2108; © (c,0,m,p,i,l,e,r) como compiler. Porém, (a,ab,b, abe) ndio pode ser representada simplesmente por aabbabe, pois essa representacao poderia indicar outras palavras além daquela que desejamos repre- sentar, como, por exemplo, (a,a,b,abe). Nesse caso, podemos utilizar espagos entre as letras para indicar como “separar” as letras da palavra. A palavra (a, ab,b, abc) seria, entdo, representada como a ab b abe e a palavra (a,a,b,abe), como a a b abe. Note-se, porém, que os espacos nao fazem parte da palavra. Define-se 0 tamanho de uma palavra « (denotado por 2) como o numero de letras de 2. Nos exemplos anteriores, terfamos: * [010110] # [2108) # |compiler| © a ab b abel Sobre qualquer alfabeto 5, define-se uma tinica palavra de tamanho 0 que deno- tamos por . Definem-se, também, os conjuntos: © DK = {palavras « sobre 5 | |e| =k}: ems Jxt=murtun.; 0 ont aE {)}. Chegamos, entio, & definigao do que é uma linguagem: dado o alfabeto 5, uma linguagem Z sobre 5) é um subconjunto qualquer de E*. Vejamos os exemplos a seguir CAPITULO 2- A LINGUAGEM X**+ 11 © {0, 1, 00, 01, 10, 11} é uma linguagem sobre {0,1} que contém seis palavras; * {x € {0,1,2,3,4,5,6,7,8,9}* | « representa um nimero decimal impar} ¢ uma linguagem sobre {0,1,2,3,4,5,6,7,8,9} com um némero infinito de palavras; © 0 conjunto de todos os programas Java validos (sintaticamente corretos) 6 uma linguagem sobre um alfabeto © composto de — palavras reservadas como for, while, if etc. — simbolos especiais como { }, *,+ ete. — nomes de variéveis, métodos, classes ete. Para definirmos formalmente uma linguagem de programago, devemos, entao, definir qual 0 alfabeto sobre o qual essa linguagem 6 formada e definir quais so as palavras vélidas para essa linguagem. Nas préximas sees veremos algumas manciras de fazer isso. 2.2 Gramatica livre de contexto Em geral, uma linguagem de programagao pertence a uma classe de linguagens chama- das de linguagens livres de contexto. Uma das maneiras de se definirem tais linguagens por meio das gramaticas livres de contexto. Uma gramatica livre de contexto (GLC) éuma quadrupla (0,2, 5, P), onde ¥ ~60 alfabeto sobre o qual a linguagem ¢ definida; © ~ 6 um conjunto nao vazio de sfmbolos nao terminais; P ~6 um conjunto de produgées da forma A+ a, onde AE Me a (ZUM)*; S -€0 simbolo inicial da gramética, S € Q. O conjunto © de uma gramatica ¢ 0 alfabeto sobre o qual as palavras da linguagem desejada so formadas. No contexto de GLCs, costuma-se chamar os elementos de = de simbolos terminais. O conjunto © é disjunto de ©. Seus elementos nao entram na formagdo das cadeias da linguagem. Sao utilizados como simbolos auxiliares para definirem-se as produgées da gramatica. ‘As produgdes P so regras de substituigao que tém do lado esquerdo do + um simbolo no terminal e do lado direito, uma cadeia formada por simbolos terminais e simbolos no terminais ou A. A partir do simbolo inicial S, essas regras so aplicadas substituindo-se o néio-terminal que aparece do lado esquerdo de uma produgio pela palavra que esté do lado direito desta. Se a palavra obtida possui somente simbolos terminais, entéo essa palavra pertence & linguagem definida pela gramatica. Se a cadeia obtida contém ainda simbolos néo terminais, ento nova(s) substitui¢io(6es) deve(m) ser feita(s) até que se chegue a uma cadeia que tenha apenas simbolos ter- minais. Vamos tomar como exemplo a gramatica Gy = ({S, A,B}, {a,b,c},5,P), onde P & 12 COMO CONSTRUIR UM COMPILADOR (1) S = AB (2) A = aAb Gyan ol (4) B+ cB (G) 2 oN Assim, a partir do simbolo inicial S podemos aplicar uma seqiiéncia de produces de P até que uma cadeia 2 que possua apenas simbolos terminais seja alcangada. Entdo © pertence a linguagem definida por G1, 0 que podemos denotar como: x € L(G1). Por exemplo: Iniciamos com $ aplicamos (1) obtemos AB temos AB aplicamos (3) obtemos B temos B aplicamos (5) obtemos 4 € L(G:) Iniciamos com $ aplicamos (1) obtemos AB temos AB aplicamos (2) obtemos aAbB temos aAbB aplicamos (2) obtemos aaAbbB temos aaAbbB —aplicamos (3) obtemos aabbB temos aabbB —aplicamos (4) _obtemos aabbeB temos aabbeB —_aplicamos (4) _obtemos aabbecB temos aabbecB — aplicamos (5) _obtemos aabbee € L(G) Pode-se facilmente notar que |L(@1)| = 60, pois dependendo do numero de vezes que so aplicadas as produgées (2) e (4), podem ser criadas palavras de tamanho tio grande quanto se deseje. Cada uma das palavras intermedidrias geradas até se chegar 4 palavra que s6 contém terminais 6 chamada de uma forma sentencial. Se uma cadeia ? pode ser obtida a partir da forma sentencial a mediante a aplicagéo de uma. tinica produgao, dizemos que @ deriva diretamente 9, ou que f é derivada diretamente de a, 0 que denotamos por a = 8. Se f pode ser obtida mediante a aplicagéo de um mimero finito de produgées, dizemos que a deriva f ¢ denotamos a = Como a linguagem L(G), definida pela gramética livre de contexto G = (2,5, S, P), € 0 conjunto de todas as palavras formadas apenas por elementos de © que podem ser derivadas a partir do simbolo inicial S, isso equivale dizer que L(G) = {ceS*|SSc}. 2.3. Forma de Backus-Naur A Forma de Backus-Naur (BNF) é uma outra maneira de se definir linguagens livres de contexto. Ela ¢ semelhante a uma gramatica livre de contexto, mas permite que o lado direito das produgées possua alguns operadores. Esses operadores serio apresentados a seguir. Selegio (« | 8) ~ Um dos elementos entre parénteses (a ou f) pode ser utilizado na aplicagio da produgéo. CAPITULO 2- A LINGUAGEM X*+ 13 Por exemplo, se tivéssemos a produga S alble|de poderfamos ter as derivacdes: > abe => ace => ade ante © que significa que a linguagem gerada é {abe, ace, ade}. Uma GLC equivalente a essa BNF seria S = abe S > ace S — ade Vejamos outro exemplo: S > (c(aSa| bSb)c| A) gera cadeias do tipo: S = caSac= cacaSacac => cacachSbcacac > cacachbeacac Nesse caso, a GLC equivalente seria S >) S = caSac So ebSbe Opcional [a] - O que estiver entre os colchetes pode ser utilizado ou no na aplicagio da produgio. Por exemplo, se tivéssemos a produgio: S —albedje poderfamos ter as derivagées: S>ae S = abcde o que significa que a linguagem gerada 6 {ae, abcde}. Vejamos outro exemplo: sce COMO CONSTRUIR UM COMPILADOR. S = af(A| Bde A = alfa] B> (i gera as cadeias: S=>ad S = aAcd => aacd S = aAcd = aaacd S => aBed = abed S = aBed > acd ou seja, gera a linguagem {ad, aacd, aaacd, abed, acd}. Uma GLC equivalente a essa BNF seria: ad aAcd aBed aa Wy RRL TT ll dk a Sen Note ainda que uma produgio do tipo « — f[y7]é 6 sempre equivalente & produgéo a By | 6. Repeticao 0 ou mais vezes (a)* — O que estiver entre paréntese pode ser usado um nimero qualquer de ‘veres na aplicagao da produgao e pode também no ser usado (repetido nenhuma ver). Por exemplo, se tivéssemos a produgio: S 5 alt}tc terfamos como resultado a linguagem {ac, abe, abe, abbbe, . Como qualquer outra BNF, podemos encontrar uma GLC correspondente, como, por exemplo: Ze Ze NN glee: voe Como outro exemplo, podemos ter a BNF: S -» alb|e)td CAPITULO 2- A LINGUAGEM X++ 15 que gera todas as cadeias que iniciam com a, terminam com d e tém entre estes o \ ou qualquer cadeia formada por be c. Repeticao 1 ou mais vezes (a)* - O que estiver entre paréntese pode ser usado uma ou mais vezes na aplicacao da producdo. A producio a — 8(7)6 ¢ equivalente a a = fy(7)"°6. A seguir, utilizaremos a BNF para definir a nossa linguagem X*+. Ser efetuada uma, descrigao de quais sfo as estruturas da linguagem representadas em cada uma das produgdes. 2.4 BNF paraa linguagem X** Nossa linguagem é construfda sobre uma alfabeto que possui simbolos que podem ser confundidos com os metassimbolos utilizados como operadores na notiagio BNF, como, por exemplo, |e ]. Por isso, vamos adotar a seguinte convencio: * (symbol) representa o ndo-terminal symbol; simbolos terminais sao representados com “symbol” ; metassimbolos da notacdo BNF sao representados grafados com [ }. Iniciaremos a definigio pelo simbolo inicial da BNF, que representa a estrutura de um programa escrito em X++, (program) — [ (classtist) ] Esse néo-terminal indica que um programa em X++é composto de uma lista de classes ou se trata de uma cadeia vazia. Assim, 0 nosso compilador, ao tentar com- pilar um programa armazenado num arquivo que esteja vazio, vai aceité-lo como um programa, vélido, como ocorre com a maioria dos compiladores. (classlist) — ( (classdecl) )* ou (classlist) + (classdecl) { {classlist) ] Q nao-terminal classlist representa uma repeticao de declaragées de classes, como num programa em Java, que basicamente é composto de uma seqiiéncia de declaragdes de classes como: class a {....} class b {....} class c {. 16 COMO CONSTRUIR UM COMPILADOR Foram utilizados dois estilos diferentes para definir-se classlist, mas ambos geram as mesmas cadeias. O primeiro estilo utiliza 0 operador de repetigao ( )* eo segundo, © proprio nfo-terminal classlist recursivamente para obter-se essa repeticéo. A nossa BNF iré utilizar ora um estilo, ora outro, para que o leitor possa avaliar quais sao as implicagSes, quando formos implementar o analisador sintético, de se utilizar um ou outro estilo. Continuando com a proxima produgéo, (classdecl) — “class” “ident” [ “extends” “ident” | (classbody) Esse nfo-terminal classdecl representa a declarago de uma classe. Ele indica que tal estrutura se inicia com a palavra reservada’ class que yem seguida por um identificador (outro terminal) que ira dar nome & classe sendo declarada. Depois do nome da classe, pode ou nao aparecer a palavra extends ¢ o nome da superclasse da qual a classe sendo declarada descende. Seriam cadeias do tipo class a... ou class a extends b ... O nao-terminal classbody representa 0 corpo da declaragao da classe: {classbody) —+ “{" [ (classtist) | ( (vardect) “" )* ((constructdecl) )* ( (methoddecl) )* “}"" Ele se inicia com um { que (opcionalmente) vem seguido por um néo-terminal classlist, declarado anteriormente. Isso significa que nossa linguagem permite a declaraciio de classes aninhadas, como acontece na linguagem Java. Depois, seguem-se as de- claragGes de varidveis, construtores e métodos, mediante a repeticao (possivelmente nenhuma vez) dos no-terminais vardecl, constructdecl e methoddecl, terminando com um }. Note que nossa linguagem exige que-as declaracoes sejam feitas exata- mente nessa ordem. Ou seja, classes aninhadas, depois varidveis da classe, depois os construtores e, finalmente, os métodos. Uma declaragao de varidveis, representada pelo nao-terminal vardecl, 6 semelhante 4 declaragao de varidveis em Java. Por exemplo: int a; ou string a,b; ou nytype al], bLIO; Temos entio: (vardecl) + ( “int!” | string!’ | “ident” ) “ident! ( *f" 5)" )* (" ident” (4 J" J )* 1 Gostumam-se chamar esses simbolos como class, for, while de palavra reservada da linguagem. Porém, 0 leitor deve lembrar que cada um desses simbolos 6, formalmente falando, uma letra do alfabeto de entrada. CAPITULO 2- A LINGUAGEM X++ 17 Essa produgao gera cadeias que comegam com 0 tipo da variével a ser declarada, que pode ser int, string, ou um identificador que ¢ 0 nome de uma classe declarada no proprio programa. Em seguida, vém os nomes das variéveis sendo declaradas, que podem ser seguidos ou nfo por colchetes que indicam a dimens&o de cada varidvel. Note que o “;” no final da lista de varidveis nao foi inclufdo nesta produgo, mas, sim, na produgéo do nfo-terminal classbody, ap6s cada aparigao de um vardecl. De maneira semelhante, temos as declaragdes de construtores ¢ métodos: (constructdecl) — “constructor" (methodbody) (methoddecl) + ( “int” | “string” | “ident!” ) ( “[" “ident” (methodbody) ye A declaracéo de um construtor comega com a palavra constructor. Um construtor nao tem tipo de retorno ou um nome. Assim, depois de constructor, inicia-se 0 corpo do construtor, representado pelo nao-terminal methodbody. Jé a declaracio de um método inicia-se com 0 tipo de retorno do método, que pode ou nao ter varias dimensées, seguido pelo nome do método ¢ pelo seu corpo. corpo de um método ¢ dividido em duas partes, conforme mostra a seguinte produgéo: (methodbody) — *(" (paramlist) “)" (statement) ‘A primeira parte ¢ a lista de parametros formais do método, representada pelo nio-terminal paramlist. A segunda parte so os comandos que compéem 0 método. (paramlist) —+ [( “int!” | “string” | “ident” ) “ident” ( *{" 4" )* (1 (#int!!| “string!” | “ident!” ) “ident” Cals Essa lista de parametros, que 6 opcional, quando presente ¢ formada por seqii¢ncias de declaracées que tém o tipo do parametro ¢ o nome da varidvel associada a ele, separadas por virgulas. Por exemplo, as seguintes cadeias podem ser geradas por paramlist: int aow int a, string b ou string b[][], int a, MyType c A segunda parte do corpo do método é um statement. Isso significa que essa parte € composta de um tinico comando, o que pode parecer estranho. Na verdade, como veremos na definigdo do nfo-terminal statement a seguir, uma construgao do tipo a "comando 1" "comando 2" “comando n" 18 COMO CONSTRUIR UM COMPILADOR é um comando composto e pode ser gerado a partir do néo-terminal statement. (statement) > ( (vardecl) “" | (atribstat) *y (prinistat) °;"" | (readstat) * | (returnstat) (superstat) “ (if stat) | (forstat} | “" (statlist) “}"" | “break” §" | ) Esse ndo-terminal gera os seguintes tipos de cadeias, que correspondem a comandos da nossa linguagem e que sero definidos a seguir: © declaragdo de varidveis locais; © comando de atribuigéo; * comando de impressio; * comando de leitura; * comand de término do método e retorno de valor; * comando de selecdo; ® comando de repeticao; © comando de interrupgio de laco; * comando vazio. O nio-terminal vardeel foi definido anteriormente para declaragao de variaveis de classe. Usaremos esse mesmo terminal pata a declaragao de varidveis locais, pois sintaticamente essas duas construgies so iguais. Vejamos 0s outros comandos validos: (atribstat) + {Ivalue) « =" ( (expression) | (alocexpression) ) Essa ¢ a definigao de um comando de atribuigéo, que tem uma referencia a uma posicéio de meméria (representada pelo nfo-terminal Ivalue), seguida por um = ¢, depois, uma expresso ou uma referéncia a um novo objeto, utilizando o operador new. Por exemplo: a = 00u af10] = btc.a a[10].b = new MyType() CAPITULO 2- A LINGUAGEM X*++ 19 (printstat) — “print!” (expression) Esse ndo-terminal gera as cadeias que representam comandos de impressio. Blas contém a palavra reservada print, seguida de uma expresso qualquer. Por exemplo print 123 print a print a[i0].b * c.d[e] (readstat) —+ “read” (Ivalue) Essa produc&o corresponde ao comando de leitura read. Ele 6 semelhante ao print, ‘mas a segunda parte da produgao (Ivalue) representa uma referencia a uma posicio de meméria, para que o comando de leitura faga sentido. Nao podemos utilizar 0 nao-terminal expression, pois queremos que as seguintes cadeias possam ser geradas por essa produgao: read a ‘reab a.b read a.b[c+2] ‘mas no queremos gerar read a+b read 123 (returnstat) — “return” [ (expression) ] Produz cadeias correspondentes aos comandos de retorno de uma chamada de método. A expresso que segue a palavra return € opcional, pois, embora todos os métodos tenham que ser declarados como retornando algum valor ou objeto (veja. o nao-terminal methoddecl), isso no se aplica aos construtores (veja. constructdecl) que ndo retornam nada. Assim, comandos do tipo return 0 return a + b.c so usados dentro dos métodos ¢ 0 comando return sem expressiio de retorno é utilizado nos construtores. Nos construtores pode ser utilizado também 0 comando super para chamada do construtor da superclasse. Sua sintaxe ¢: (eupersiat) —+ “ouper’’ *(" (arglist) *)” O ifstat 6 0 n&o-terminal que gera os comando de selegéo. Como na maioria das Inguagens, podemos ter ou néo a parte else do comando. 20 COMO CONSTRUIR UM COMPILADOR (ifstat) —» “if” “(" (expression) “)" (statement) [ “else” (statement) ] Como comentamos anteriormente, 0 nao-terminal statement pode gerar comandos compostos (ou blocos de comandos). Por isso, so vélidos ambos os comandos: if (a > 0) read b; ou if (a + b == c) £ read 4; print d; return 0; a else return 1; O nico comando repetitivo na linguagem X*+ € 0 for, semelhante ao existente na linguagem Java. Sua deseri¢io & (forstat) + “for’’ “(" [ (atribstat) | “;” [ (expression) ] “;" [ (atribstat) | “)" (statement) Ele inicia com a palavra for, seguida de um “(” e os trés elementos: um comando de inicializacfio, uma expresso de controle e um comando de incremento, todos opcionais separados por “”. © primeiro e 0 tiltimo correspondem a atribstats, enquanto © segundo @ uma expression. O comando termina com um “)” e com 0 comando (statement) que deve ser executado. So comandos validos: for (3) 3 ou for (a = 0; 5 ) read bla; ou for (a= 0; a b) else read b; Finalmente, o comando composto, definido dentro do nao-terminal statement, icia-se com um “{ que vem seguido de uma lista de comandos, representado pelo 4o-terminal statlist, e termina com um “}”. O statlist é: (statlist) — (statement) [ (statlist) | Note que A néo 6 gerado por tal produgao. Isso significa que um bloco de comando ‘io como int mymethod(int a) q > nio é vilido. Porém © programador pode utilizar int mymethod(int a) 4 + ou int mymethod(int a) Continuando com os nao-terminais que ainda faltam definir, temos: (Wwatue) + “ident” ( “[" (expression) *” | 41 ident!" |" {arglist) *)"] )* Esse néo-terminal representa uma referencia a uma. posigéo de memoria e foi usado em atribstat e readstat. Tal cadeia inicia-se sempre com um identificador que é 0 nome de uma varidvel ou um método. Esse identificador pode vir seguido varias vezes por um indice no caso de se estar referindo a uma variavel indexada; uma referéncia a um campo, no caso de se querer referenciar um campo de objeto; ou um nome de um método seguido por uma lista de argumentos, no caso em que se esta fazendo uma chamada de um método. Por exemplo: 22 COMO CONSTRUIR. UM COMPILADOR * read a; contém somente a referéncia a uma varidvel simples; ¢ read a[0] [1] é uma referéncia a uma varidvel indexada; * read a.b € uma referéncia ao campo b da varidvel a; ¢ read a.b{0] [1] ¢ uma combinagdo dos dois tipos anteriores; read a.b(12).c é uma combinacio dos trés tipos. Nesse caso, a é uma variével que referencia um objeto. Com esse objeto estamos invocando o método 6, passando 12 como argumento. Esse método deve retornar um outro objeto do qual estamos referenciando o campo c. Note que utilizando este no-terminal num comando de atribuigdo poderiamos ter a.b(12) = 10, o que é ilegal pois estarfamos tentando atribuir valor a uma chamada de método. Sintaticamente isso, porém, ser permitido na nossa linguagem. Tal erro seré apontado em fases posteriores da andlise. Num comando de atribuig&o podemos ter uma alocepression que 6 uma referencia @ um novo objeto ou array. Sua definigio é: (alocexpression) —+ ‘new ( “ident! “(" (arglist) *)" | ( “int” | “string” | “ident!” ) (+{" (eapression) *") ) Esse niio-terminal produz express6es do tipo new MyType(10, a * b) new string[10] [i] [x] new MyType(10, i - k) O nao-terminal expression é utilizado diversas vezes para representar as possiveis expresses que podem ser construfdas na linguagem X, Vamos ver como sao essas expressbes. (expression) —+ (numezpr) [Ce [£5 [Seat [eer | (numezpr) ] As expresses na nossa gramitica sao definidas em diversos “ntveis” diferentes. O nivel mais alto € este, que define como é uma expresso que possui operadores rela- cionais. Bla é composta de uma subexpressio que corresponde ao primeiro numexpr seguida por um operador relacional e uma segunda subexpressiio que corresponde ao segundo numexpr. Cada subexpressio, como veremos adiante, pode conter também outras subexpressdes e outros operadores, por exemplo + ott +. Assim, para a cadeiaa + b < c * d, teriamos a seguinte derivacao: expression => numexpr < numexpr => a+b numexpr > a+b oque significa quea + b também 6 uma expressiio valida, gerada a partir de expression, embora no utilize operadores relacionais. O nivel seguinte ¢ o nfio-terminal numezpr. (numezpr) > (term) ( (“4 | “=! ) (term) )* Essa produgao ¢ semelhante 4 anterior. A diferenga é que a segunda parte nao é apenas opcional, mas pode ser repetida um nimero finito de vezes. Essa diferenga baseia-se no fato de que queremos gerar expressoes do tipo a + b + c * d, mas ndo cadeias do tipo a < b < c, pois, em geral, tal expresso nao faz muito sentido. A derivacdo para a + b + ¢ * dseria: expression = numexpr = term + term +term 4 a+term+term 3 a+b+ term a+b+erd nao-terminal term segue o mesmo raciocinio para expressOes com os operadores *, / % (vesto da divisdo). (term) > (unaryexpr) (( “=! | “/" | “%") {unaryeapr) )* Uma expressiio que contenha um operador undrio como a + -1 utiliza a seguinte produga {unaryexpr) — [(*+" | *—" )] (factor) Aqui, novamente, vemos que unaryexpr pode gerar cadeias que se iniciem com os operadores + ou — seguidos por uma cadeia gerada pelo ndo-terminal factor, e pode gerar também aquelas cadeias geradas por factor sem os operadores como prefixo. Por exemplo, para a + -1 terfamos expression => numexpr = term + term 4 a+ term = a+ unaryexpr > a+ —factor + a+-1 O nao-terminal factor representa as subexpressées mais simples que podemos ter, tais como constantes ou referéncias a varidveis. (factor) — ( “int-constant” | “string-constant” | “null” | (Ivalue) | °(! (eapression) *)” ) Assim, esse ndio-terminal produz constantes inteiras, constantes string, a constante null, referencias a varidveis e chamadas de métodos (ndo-terminal Ivalue) e, ainda, expressdes entre parénteses. Esse iltimo caso 6 necessdrio para que a nossa gramética 24 COMO CONSTRUIR UM COMPILADOR possa gerar cadeias como a + (b + c). A derivagio seria: expression > numexpr = term + term > a+ term > a+ unaryexpr > a+ factor + a+ (expression) = a+ (numezpr) = a+(term-+ term) + a+(b-+term) > at(b+e) E, finalmente, o ndo-terminal que deriva cadeias que representam listas de argu- mentos, usadas em chamadas de métodos, 6: (arglist) -» [ (erpression) (“," (expression) )* ] 2.5 Grafos sintaticos ‘Veremos ainda, uma outra maneira de representar uma linguagem, chamada de grafo sintético. ssa representacio facilita a visualizagdo do tipo de cadeias ou formas sentenciais que cada néo-terminal pode gerar. Para isso, define-se para cada simbolo néo terminal um grafo direcionado com dois tipos de vértices: os que sio rotulados com s{mbolos terminais e aqueles que s4o rotulados com simbolos nao terminais. Cada um desses grafos corresponde ao lado direito de uma produgSo numa gra- mética na BNF. Uma aresta de um n6 (vértice) com rétulo A para um né com rétulo B indica que a subcadeia AB é parte da producao desse terminal, ou seja, tal aresta indica a concatenagéo da cadeia A com a cadeia B na produg&o desse nao-terminal. Todo grafo possui um tinico nd inicial, sobre o qual nao incide nenhuma aresta, e um ‘nico né final de onde nenhuma aresta parte, ambos rotulados com a cadeia A. Assim, se tivermos uma producéo: A - abe teremos 0 grafo correspondente ao nao-terminal A: A Note que os nés inicial e final ndo so representados no diagrama do grafo. Todas as arestas que nfo possuem um né de origem (destino) no diagrama tém 0 n6 inicial (final) como origem (destino). Os nés correspondentes a simbolos nao terminais sio Tepresentados no diagrama por retangulos. Por exemplo, para as produgdes A = aBe B= de temos: CAPITULO 2 - A LINGUAGEM X++ 25 Para sabermos quais so as formas sentenciais que podem ser geradas por um niio- erminal, devemos percorrer todos os caminhos que vao do né inicial a0 né final do afo correspondente ao néo-terminal. Se tivermos operadores de selecdo na produgio 9 ndo-terminal teremos caminhos alternativos no grafo, cada um passando por uma s cadleias que estfio dentro do operador de selecio. Por exemplo, A > Blalb|oB B = (de|A) Note que um né rotulado com 2 pode ser sempre evitado no grafo, como fizemos © néo-terminal B. Dessa forma, sabemos também como representar uma producao e utilize cadeias opcionais, por meio do operador [ ]. Por exemplo, para A + BlabB B = (de|d) 26 COMO CONSTRUIR UM COMPILADOR B Loo E 0s operadores de repeti¢ao podem ser representados por meio de lacos no grafo. Por exemplo, A = BalbleB B => (de)* pode ser representado como A on eg B ‘Veremo entio como fica a gramética da linguagem X++ que definimos na seco anterior representada por meio de um grafo sintdtico. program Uy classlist 2 classlist class \+ classlist J classdecl classbody CAPITULO 2- A LINGUAGEM X+*+ ms can? classlist. classbody J 27 28 COMO CONSTRUIR UM COMPILADOR. constructdect nebo} methoddect methodbody —+(©>[paramlist ) }y statement paramlist statement vardecl atribstat, printstat readstat, returnstat superstat ifstat forstat statlist atribstat CAPITULO 2 - A LINGUAGEM X*+ printstat Ivalue hoor eee expression ——(print)-+| expression |— readstat —~(read}+{ lvalue }—~ 29 30 COMO CONSTRUIR UM COMPILADOR returnstat expression superstat Gaper)O-[aeeist -O— ifstat expression Os statement aaa forstat atribstat, expression statement atribstat. statlist —+4 statement, oat statlist, CAPITULO 2- A LINGUAGEM X+* 31 value +(ident expression aloceapression (nen) (dent) arglist C+] expression expression numexpr numexpr J 32 COMO CONSTRUIR UM COMPILADOR numespr — term term term UnAEyeRDE (+) unaryexpr unaryespr 1 factor }— factor oa ce string-constant an lvalue expression CAPITULO 2- A LINGUAGEM X**+ 33 arglist expression 2.6 Seméantica da linguagem X** N&o tentaremos aqui elaborar uma definic&o formal da semantica da linguagem X++ pois tal assunto nao se restringe os objetivos deste livro. Porém, o leitor familiarizado com algumas linguagens de programagao certamente ser capaz, por meio da sintaxe definida, de identificar estruturas conhecidas que tem a mesma semantica na nossa linguagem. Alguns pontos sobre a semAntica da linguagem foram abordados na explicacio da sua gramatica na seco 2.4. Outros pontos que nao foram abordados e que neces- sitam de esclarecimento sero tratados em capftulos posteriores, pois & medida que progredirmos na implementacio do nosso compilador, algumas decisdes de implemen- taco deverfo ser feitas para refletir exatamente a semantica que se deseja atribuir & linguagem. Assim, a propria implementagio do compilador ird espelhar a semantica da linguagem. 2.7. Um programa em X** programa a seguir é um exemplo de como se utiliza a linguagem X++. Ele im- plementa uma arvore de busca binéria cuja chave 6 uma data. Vamos brevemente explicar como esse programa foi implementado. A classe biniree é a classe que implementa a 4rvore binéria. Ela possui um “nico construtor que recebe como parametro um objeto do tipo data. Possui ainda trés métodos. O primeiro é 0 int start(). Esse método seré por convengio 0 ponto de entrada de qualquer programa em X**, ou seja, 0 método principal. Ele solicita a0 usuario do programa que digite onze triplas de nfimeros inteiros que representam um dia, um més e um ano. Com esses inteiros criam-se objetos do tipo data que sio inseridos na 4tvore binaria por meio do método insert desta classe bintree. No final, ‘0 método treeprint é chamado para imprimir a arvore criada. A classe data tem trés varidveis que representa um dia, um més e um ano. Dois construtores so 0s responséveis pela criagio dos objetos dessa classe. Note-se que nenhuma verificacéo é feita quanto A validade da data que est sendo criada. O método compara dessa classe compara dois objetos do tipo data ¢ é usado no método insert da classe bintree para encontrar 0 “lado” da arvore em que um determinado elemento deve ser inserido. 34 COMO CONSTRUIR UM COMPILADOR. PROOF CORO OR IIIA OSORIO AAA AIDA A IA A Esse programa implementa uma arvore de busca binaria Hoops roiodonaoennonneaonnad ania obidoonaacks/ class bintree { /* define o no da arvore binaria */ class data { // define um classe aninhada do tipo data (dia, mes, ano) int dia, mes, ano; constructor() // construtor 1, sem parametros { 1900; // inicializa em 1/1/1900 13 + constructor(int d, int m, int a) // construtor 2 - dia, més e ano como { // paxémetros dia = d; mes = m3 ano = a; } int compara(data x) // compara duas datas < // retorna < 0 - menor > 0 maior 0 igual if ( ano < x.ano) then return -1; if (ano > x.ano) then return 1; if (mes < x.mes) then return -1; if (mes > x.mes) then return 1; if (dia < x.dia) then return -1; if (dia > x.dia) then return 1; return 0; } // final classe data // variaveis da classe bintree data key; // chave de comparagao bintree left,right; // referéncia para os filhos constructor (data x) t key = x left = null; CAPITULO 2 - A LINGUAGEM X++ 35 right = null; + int insert(data k) // adiciona um elemento na arvore t ant x; x = k.compara(key) ; if (x < 0) then t if (left != null) then return left.insert(k); left = new bintree(k); return 1; a if (x > 0) then a if (right != null) then return right. insert (k); right = new bintree(k); return 1; ai return 0; int treeprint(int x) // imprime a arvore t int i; Af (left != null) then i = left.treeprint (x+4) ; for G@=0;i 37 38 COMO CONSTRUIR UM COMPILADOR Figura 3.1 ~ Visio de um AFD como uma maquina. F 60 conjunto de estados finais, F C 3. ‘Um AFD pode ser interpretado como uma “maquina de reconhecer cadeias”, como a mostrada na figura 3.1. Ble recebe como entrada uma cadeia ¢ diz se ela pertence ou nao a linguagem desejada. Para fazer isso, essa mAquina possui um controle finito de estados (CFE) ¢ um conjunto de estados S. 0 CFE coloca sempre a maquina em um estado pertencente ao conjunto $. A funcio 5 diz como o AFD deve mudar de estado, a medida que as letras da cadeia de entrada vao sendo analisadas. Ao fnal da cadeia, isso ¢, depois de analisar uma por uma, todas as letras da entrada e realizar as mudangas de estados determinadas, o AFD aceita a cadeia — acendendo o indicador superior no painel — se 0 estado em que ele se encontra pertence ao subconjunto F. Caso contrario, a cadeia é rejeitada ~ acendendo a luz inferior do painel -, indicando que nao pertence & linguagem definida pelo AFD. ‘Vamos tomar como exemplo 0 AFD A que reconhece a linguagem {x € {0, 1,2,3,4, 5,6,7,8,9}* | x representa um ntimero decimal divistvel por 3}. A: = ({0, 1, 2,3,4,5,6, 7,8,9},{80, $1, 52}, 8056, {80}), onde 5 6: CAPITULO 3 - ANALISE LEXICA 39 (0,0) = $0 i(s1,0) = #1 (62,0) = 52 6(s9,1) = sy (81,1) 82 6(s2,1) 80 H(s0,2) = 52 i{s1,2) = 50 5(s2,2) = 51 Hs0,3) = 90 S(s1,3) = 91 5(s2,3) = 9 Hso,4) = 1 S{s14) = 82 5(s2,4) = 50 H{s0,5) = 4{s1,5) = 50 5(62,5) = 1 (50,6) = 90 5(s1,8) = s1 5(eni8) = es 4(s0,7) = 91 d(s,7) = 92 (27) = 90 (60,8) = 6(s1,8) = 90 i{s2,8) = st 5(80,9) = 80 6(81,9) 8 6(82,9) 5 Assim, por exemplo, quando executado com o string 01452, 0 AFD A; teria 0 seguinte comportamento: Estado corrente | Letra lida | Préximo estado 39 (estado inicial) 0 50 so 1 SL a 4 82 8 5 st 51 2 60 € F Como o AFD termina sua execugéio no estado s9 que pertence ao conjunto de estados finais, entio essa cadeia pertence a linguagem definida por Ay. JA para a cadeia 79612, terfamos: Estado corrente | Letra lida | Proximo estado 50 (estado inicial) i 31 Ss. # Sy SL 6 SL SL a 32 sa 2 ai gF © que indica que tal cadeia néo perience A linguagem definida por Ay. Um AFD pode ser representado por meio de uma tabela de transicio de estados. Nessa tabela existe uma linha para cada estado e uma colina para cada letra do alfabeto de entrada. Dados 0 estado s e a letra a € ©, coloca-se na posigio da tabela sx a0 valor de 6(s, a). Por exemplo, para Ay, terfamos: Uma das vantagens de se utilizar um AFD para definir quais sao os tokens @ serem reconhecidos pelo AL é que é facil implementar um AL baseado na tabela de transig4o de estados. Tal analisador deve apenas ler uma letra da entrada e, baseado na tabela, fazer a mudanga de estado. Se o estado em que o AFD se encontrar for um estado 40 COMO CONSTRUIR UM COMPILADOR final, entdo a cadeia lida até aquele ponto ¢ um token vélido. Antes de tratarmos de mais detalhes sobre esse tipo de AL vejamos uma outra forma de representar um AFD que € 0 diagrama de transigéo de estados. Esse diagrama 6 um grafo direcionado cujos vértices, representados por cfrculos Correspondem aos estados do AFD e cujas arestas correspondem as transicoes entre estados. Por exemplo, a transiciio (so, 1) = s; 6 representada por: No caso em que temos diversas transigdes definidas entre dois estados, podemos tepresenté-las todas com uma tinica seta, rotulada com todas as letras associadas as transigdes. Por exemplo, se temos 5(s0, 1) = 5(50,4) = 6(s0,7) = s1, representamos: eo © Ha uma representagao especial para indicar o estado inicial. B uma seta chegando ao estado, sem nenhum estado de origem. Os estados finais sao identificados por circulos duplos. O diagrama para o AFD A; é dado na figura 3.2 03,69 0369 03.69 Figura 3.2 — Diagrama de transig&o de estado para A). Para utilizar um AFD como AL vamos faremos algumas pequenas modificagSes ho comportamento desses autOmatos. A primeira é que vamos permitir que a fungéo de transic&o seja uma fungdo parcial, ou seja, ndo definida para todos os pontos do dominio $ x ©. Por exemplo, vamos tomar 0 AFD Ap que é igual a Ay, mas para o qual 0 valor de 6(s0,0) 6 indefinido. Terfamos, entao, para esse AFD o diagrama da figura 3.3. Esse autOmato reconhece todas as cadeias que representam miiltiplos de 3 CAPITULO 3- ANALISE LEXICA 41. 3,69 03.69 03.6.9 Figura 3.3 — Diagrama de transigéio de estado para Ap. menos algumas que tém o digito 0 como 2409. Ao processar tal cadeia, o comporta- mento de Ao seria: Estado corrente_| Letra lida | Proximo estado 30 (estado inicial) 2 30 Isso significa que o AFD para antes de chegar ao final da cadeia. No nosso AL, o AFD processa a cadeia de entrada até que nao existam mais transigdes possiveis, entao ele para. Se o ultimo estado em que o AFD estava quando parou for um estado final, entéo um token valido foi lido, Caso contrério, um erro léxico ocorreu. ‘Tomaremos como exemplo um AL que deve reconhecer apenas trés tipos de tokens: ntimeros inteiros; identificadores, formados por letras ¢ digitos, sempre iniciados por uma letra; e a palavra reservada if. Podemos construir o AFD As mostrado na figura 3.4. Veja que o AFD da figura 3.4 consome todos os “brancos” como espago, tab, etc, no estado 1. Depois, dependendo de qual ¢ a letra seguinte, ele desvia para os estados 2, 3 ou 4 para reconhecer, respectivamente, um mimero, a palavra if ou o nome de um identificador. Ele entao continua, consumindo letras até que nenhuma transigao exista. Quando isso ocorre, um token foi identificado se 0 estado em que ele parou é final. Um erro Iéxico foi achado se 0 estado nao é final. Portanto, cada vez que 0 AL € executado, um novo token é reconhecido. E exatamente assim que o analisador sintdtico utiliza o AL. A cada vez que o AS necessita de um token, ele executa o AL que analisa a entrada e Ihe fornece um token novo. Uma das caracteristicas do AFD A3 é que ele ira reconhecer como token a maior palavra que ele puder formar com as letras da entrada. Por exemplo, se tivermos na entrada: af 1234 42 COMO CONSTRUIR UM COMPILADOR digito letra - (£), digito letra = (i} letra, digito letra, digito Figura 3.4 — Diagrama de transigdo de estado para As. o AFD iré retornar na sua primeira execugio 0 identificador i, na segunda, o identi- ficador f e, na terceira, o nimero 1234. Se a entrada for if 1234 a primeira chamada iré retornar a palavra reservada if e, depois, o ntimero 1234. E se a entrada for if1234 teremos como token retornado 0 identificador 11234. ‘Um outro ponto importante dessa implementagio do AL é que, em virtude de sua execugo parar quando nao existem mais transicdes, algumas palavras da entrada no precisam ser separadas por espagos para que sejam corretamente reconhecidas. Por exemplo, se tivermos a entrada 123411234 iremos obter na primeira execugdo do AL o inteiro 1234 e, numa segunda execucao, 0 identificador if1234. Para completarmos o nosso AL baseado num AFD, precisamos ainda, associar a cada né qual o tipo de token associado a ele. Por exemplo, no AFD Ag, sabemos que se sta execugao termina no estado 2, um ntimero foi reconhecido; se termina nos estados 3 ou 5, um identificador foi reconhecido; e se termina no estado 4, um if foi reconhecido. Além disso, precisamos saber mais algumas informagGes sobre a palavra que fez com que tal token fosse reconhecido. Principalmente em tokens como identificadores e niimeros, precisamos saber qual foi a palavra que originou o token (por exemplo, se a cadeia 6 10 ou 10000000). E precisamos saber também qual é a localizac&o do token, por exemplo, em quais linha e coluna do arquivo de entrada essa. palavra se encontra. Essa informagiio é util para que o AS possa indicar —- em caso de erro, por exemplo — 0 ponto onde esté a palavra. CAPITULO 3- ANALISE LEXICA 43 Assim, para construir um AL baseado no AFD Ag, deverfamos construir a seguinte tabela, onde representamos apenas as transigdes validas. Em qualquer estado, se for lida na entrada uma letra para qual nao exista transigao, considera-se como proximo estado um valor nao existente, como 0, por exemplo. A ultima coluna da tabela indica 0 tipo do token associado ao estado. Estado Letra: préximo estado ‘Tipo 1 | brancor | digito2 | 3 | letra-{ij:o | ERRO 2 | digito® NUMERO Si f4 | letras | digitoss IDENT 4 Tetras | digito:s IF 5 Tetra: | digit DENT E 0 programa para executé-lo seria parecido com 0 mostrado no programa 3.1. 19 28 Programa 3.1 enquanto ¢ eh branco // c armazena a filtima letra lida | leiac 71 ignore brancos | se ¢ == fim de linha // controla linha e coluna correntes ' 1 coluna = 1 || Limba = linha + 4 | senao | | coluna = coluna + 1 ke4 // estado inicial s=u // palavra lida LinhaO = linha // Vinha onde se inicia o token colunad = coluna -—// coluna onde se inicia o token uek // inicializa iltimo estado visitade k = proximo estado //préximo estado na tabela (de acordo com c) enquanto k > 0 1 ste J/ concatena ¢ & palavra leia c // 16 préxima letra da entrada se c == fim de linha I coluna | linha = linha + 1 xk // wtimo estado visitado proximo estado // préximo estado (de acordo com c) 1 1 1 1 I ! It 1 1 I ! 1 1 e tabela[u] .Token ERRO // u nfo é estado final Erro Lexico // tratamento de erro léxico t.tipo = tabela[k] .Token t.palavra = 5 t.linha = linha t.coluna = colunad retorne t // xetorna,o token obtido Nas linhas 1 ¢ 2, a transigo que consome os brancos iniciais é tratada de maneira especial, pois precisamos saber em que linha e que coluna inicia-se a palavra que ori- nou o token. Esses brancos podem, na verdade, ser vistos como aqueles separadores 44 COMO CONSTRUIR UM COMPILADOR de palavras a que nos referimos no inicio do capitulo, em vez de letras que compéem as palavras. Néo podemos, porém, ignorar todos os brancos que aparecerem na entrada, pois alguns deles podem fazer parte de tokens como numa constante do tipo string. As varidveis Tinka, cotuna e ¢ devem ser inicializadas antes de se utilizar pela primeira vez o AL. Elas retém seus valores entre duas execucdes do AL. No inicio de cada execugao, seus valores correspondem a proxima letra a ser utilizada. Na linguagem X*++, que 6 bastante simples, temos nada menos que 39 simbolos terminais que teriam que ser reconhecidos pelo AL. Outras linguagens mais complexas podem ter um niimero ainda superior, o que dificulta bastante a construcio do AFD correspondente, Uma das maneiras de diminuir essa dificuldade ¢ utilizando um automato finito nao deterministico (AFND) que ¢ um modelo ligeiramente diferente para representar a linguagem do AL. Um AFND difere de um AFD em relagio ao fato de ter diversos estados iniciais e que a fungao de transigao de estados ¢ definida 5 : Sx 5 — conjunto de subconjuntos finitos de (5), ou seja, pode existir mais de uma transi¢ao saindo de um estado para a mesma letra do alfabeto. Por exemplo, o AFND A, mostrado na figura 3.5 ¢ 0 AFND As da figura 3.6 reconhecem a mesma linguagem que o AFD Ag mostrado na figura 3.4. letra, digito Figura 3.5 — Diagrama de transi¢&io de estado para Ay. Quando executado, um AFND pode ter mais de um estado corrente a0 mesmo tempo. Por exemplo, ao processar as cadeia iff2 e if no AFND Ag, a seqiiéncia de estados ativos para cada um seria: apt {1} + {3,5} 4 (4,5) 3 {5} 3 (5} if (1) 4 13,5) 4 (4,5) Eno AEND As, seria: CAPITULO 3 - ANALISE LEXICA 45 brinco (7) ago > we branco =8—@-—© =e ler, digito Figura 3.6 — Diagrama de transico de estado para As. #12 {1,3,6} 4 {4,7} 4 {5,7} 4 (7) 3 {7} {1,3,6} + {4,7} 4 {5,7} Um AEND reconhece uma cadeia se, ao final da sua execugdo, algum dos estados rentes pertence ao conjunto de estados finais F’. Executando A, com ifi2, terfamos estado 5 ativo no final da execugio do AFND e, portanto, ifi2 deveria ser reco- ecido como identificador. J& no segundo caso, ao final da cadeia if, terfamos dois ados correntes, ambos pertencentes ao conjunto F. Se fossemos utilizar esse AFND -a implementar no AL, terfamos um problema, pois nao saberfamos qual 0 token espondente a palavra lida, se seria um identificador ou um if. Terfamos, entdo, ie estabelecer prioridades para decidir entre dois estados que terminaram ativos. Apesar de facilitar a definigSo da linguagem desejada, um AFND nfio ¢ to facil- ite implementado quanto um AFD. Tente, por exemplo, adaptar 0 algoritmo do que percorre a tabela de transigdes para um AFND. Por outro lado, as caracte- ticas de nao-determinismo nao adicionam aos AFNDs nenhum “poder” extra em Jo aos AFDs. Isso significa que a classe de linguagens que podem ser definidas meio de AFNDs 6 a mesma das que podem ser definidas por meio de AFDs. Assim sendo, qualquer AFND pode ser transformado num AFD equivalente. Po- ‘demos, ento, definir nossa linguagem por meio de um AFND, transformé-lo num AFD e, ento, montar a tabela do AL. Ou melhor ainda, podemos escrever um pro- ‘gama que faca isso para nés (ou usar um que jé exista). E exatamente essa a filosofia ‘dos programas que geram ALs mediante a descricao da linguagem numa forma mais emigavel. Esses programas acabam transformando essa descri¢éo num AFD. Na sessfio 3.2 veremos outra forma de descrever linguagens regulares. Sao as ‘expressbes regulares, que, com os AFNDs, serdo usadas na construgao do AL para a Bnguagem X** na segio 3.3. 46 COMO CONSTRUIR UM COMPILADOR 3.2 Expressées regulares Outra maneira de se representarem linguagens regulares ¢ por meio de expresses regulares. Uma expresséo regular (ER) utiliza linguagens regulares “primitivas? e combina-as por meio de alguns operadores. Essa expressio formada define, entéo, uma outra linguagem. Os operadores utilizados nessas expressbes so os seguintes: * Unido (U): dadas as linguagens L1 ¢ La, sobre 0 alfabeto B, define-se Ly U Le como {x €Z* | ce Ly V2 € La}; © Concatenacao (-): dadas as linguagens L; e Le, sobre 0 alfabeto ©, define-se Ly - Lz como {x.y € E* | xe Li Ay € Lo}; * Fecho de Kleene (*): dada a linguagem L sobre o alfabeto D, define-se L* como sendo AULU(L-L)U(L-L-L)U(L+ DL + LU. Alguns exemplos da aplicagéio desses operadores sio: {a,b} U {c,d} = {a,b, c,d} {100, 010, 110} U {00, 01, 11} = {100, 010, 110, 00, 01, 11} {101, 110}. {00, 11} = {10100, 10111, 11000, 11011} {abc}? = {A, abc, abcabe, abeabcabc, abcabeabeabe, ...} {a,b}* = {,a,b, aa, ab, ba, bb, aaa, aab, aba, ab, baa, bab, bba, bbb, ...} E para definirmos 0 que é uma expresso regular, vamos partir das linguagens mais simples que existem e utilizar esses operadores. Temos, entio, dado o alfabeto B= {ay,a2,...0,}: © G1, @2, ... a» sio ERs que correspondem as linguagens {a1}, {a2}, ....{@n}, respectivamente; © é uma ER que corresponde a linguagem {A}; ¢ 9 6 uma ER que corresponde a linguagem {}; @ se e1 © €2 sdio ERs que correspondem as linguagens L; ¢ Lo, ento ¢, Ver ¢ uma expresso regular que corresponde a linguagem L; U La; * se e1 € ¢2 sio ERs que correspondem as linguagens L; ¢ Lo, entdo ey +e é uma expresso regular que corresponde a linguagem L; - La; * se e 6 uma ER que corresponde a linguagem L, entéo e* é uma ER que corres- ponde a linguagem L*. Por exemplo, (aU bUcU d) indica a unio das linguagens {a} , {6}, {c}, {a}, ou seja, {a,b, c,d}. Para evitar confusto quando misturamos os operadores dentro de uma mesma expresso regular, devemos estabelecer uma prioridade entre os operadores. ‘Temos, entdo, * com a mais alta prioridade, depois -e, finalmente, U. Podemos ainda utilizar parénteses para determinar a ordem de aplicagdo dos operadores. Vejamos alguns exemplos: CAPITULO 3- ANALISE LEXICA 47 * (aUb)-(a@UbUc)* - (bUc) define a linguagem formada por todas as cadeias sobre {a,0,c} que se iniciam com as letras a ou be torminam com b ou c; © (aU2)-((aUd)-(aUb)-(aUb))* define a linguagem {a € {a,)}* | jz| mod 3 = 1}. Assim como os AFNDs, as ERs podem ser muito mais simples para representar a linguagem a ser reconhecida por um AL do que um AFD. Por exemplo, para a linguagem definida pelo AFD Ay, poderfamos utilizar a seguinte ER: (digito - digito") U (6 f) U (letra (letra U digito)") Qu melhor que isso, poderiamos definir cada um dos tokens a serem reconhecidos por meio de uma ER. Por exemplo: NUMERO: digito- digito*; IF: if; IENTIFICADOR: letra: (letraU digito)*. Mas também como acontece com os AFNDs, ¢ mais dificil implementar um pro- grama que, dadas as ERs, consiga identificar cadeias que a elas correspondem. Por outro lado, 6 possfvel também transformar uma ER num AFD, a exemplo do que ocorre com os AFNDs. E melhor ainda, existem programas que ja fazem isso. Um deles ¢ o JavaCC, que utilizaremos a seguir para implementar 0 AL para a linguagem X++, Esse programa permite que especifiquemos os tokens do nosso AL por meio de ERs ¢ cria um programa Java capaz de ler ¢ reconhecer esses tokens. 3.3. Oanalisador léxico da linguagem X ** Nesta segio mostraremos como implementar o AL para a linguagem X++ usando o gerador de compiladores JavaCC. No Apéndice B é feita uma descri¢do de como funci- ona esse programa, incluindo alguns exemplos de sua utilizacao. Assim, recomenda-se que 0 leitor consulte esse apéndice antes de continuar a leitura desta segio. © programa JavaCC aceita como entrada um arquivo que tem a descri¢ao da linguagem a ser reconhecida ¢ iré gerar um analisador sintatico e um analisador léxico para essa linguagem. Assim, além da gramética da linguagem-alvo, deve-se descrever também qual é a linguagem, ou o conjunto de tokens a serem reconhecidos pel AL. Esse arquivo pode ter qualquer nome, mas costuma-se utilizar uma extensio Vamos nos referir a esse arquivo como “arquivo i”. arquivo a ser tratado pelo JavaCC ¢ dividido em diversas segdes. A primeira delas determina alguns parmetros para a geragao do AL e do AS. Essa seco inicia-se com a palavra “options”, como mostrado a seguir. Cada um desses parametros possui um valor-padrio ¢ s6 precisamos incluir na nossa definigao aqueles parametros cujo valor desejamos alterar. No caso da linguagem X*+, iremos usar apenas a opgao: 48 COMO CONSTRUIR UM COMPILADOR, options { STATIC = false; a Essa opedo afeta o modo que o AL eo AS so gerados. Ao gerar os analisadores, 0 JavaCG cria uma classe Java para o AL e uma para o AS. Se essa opeao tiver o valor true, que é 0 valor-padrio, tanto a classe correspondente ao AS quanto ao AL serdo classes cujos componentes (varidveis e métodos) sio static. Como conseqiiéncia, sé um AS eum AL podem ser criados. Utilizando a opgiio STATIC = false tem-se maior flexibilidade, pois diversas c6pias do AS (e, como conseqiiéncia, do AL) podem ser criadas. Por outro lado, de acordo com a documentagio do JavaCC, o desempenho de um compilador estético 6 superior ao de um néo estatico. Embora utilizemos apenas uma c6pia do AS no nosso compilador, preferimos um néo estético apenas para que © leitor possa ver quais sfo as implicagdes desta opc&o na implementagao. A segunda parte 6 delimitada pelas palavras PARSER_ BEGIN e PARSER_END segui- das por um nome. Esse nome corresponde & classe que ira abrigar o AS. Por exemplo, um PARSER_BEGIN (LangX) ira originar a uma classe chamada Lang, que seré.a classe do AS, eum arquivo Lang. java que contém essa classe. Todo cédigo colocado nesse trecho do arquivo jj sera incluido nessa classe. Além disso, seré criada uma classe que corresponde ao AL do compilador. Essa classe (e o arquivo onde ela é armazenada) tem o nome do AS mais TokenManager. No nosso caso, por exemplo, a classe do AL seria chamada langXTokenManager. No jargao utilizado pelo JavaCC, o AL é chamado de Token Manager, por isso 0 nome da classe. No programa 3.2, 0 cédigo dentro dessa segao do arquivo .jj define o método main da classe Langy. Ele é responsavel por verificar quais so os parametros de entrada, criar © objeto do tipo LangX ¢ executar a andlise sobre um arquivo cujo nome foi passado como parametro. Se o nome utilizado foi “”, entdo a andlise ser4 feita sobre aentrada-padrao (System.in). Ele aceita também a opsao -short, que modifica a saida produzida pelo compilador. Programa 3.2 PARSER_BEGIN langX) package parser; import java.io.«; public class langX { final static String Version = "K++ Compiler - Version 1.0 - 2004"; boolean Menosshort = false; // saida resumida = falso nu // Define o método "main" da classe langx. 12 public static void main(String args[]) throws ParseException as t 14 String filename = ""; // nome do arquivo'a ser analisado ws langX parser; // analisador lérico/sintatico 16 int i; CAPITULO 3 - ANALISE LEXICA 49 boolean ms = falses System. out .print in (Version) ; // 18 0s parfmetros passados para o compilador for (i = 0; i < args.length - 1; i++) 2 Hi 2 if ( args[i] .toLowerCase() .equals("-short") ) a ms = trues 2s else 26 { System.out.printin("Usage is: java langX [-short] inputfile"); a System.exit (0); 2 Bs 2 = if (args [i] -equals("-")) { // 18 da entrada-padrao System.out.printin("Reading from standard input . . . parser = new langX(System. in) ; 28 3 s else 3 { 11 18 do arquivo 2 filename = args[args.length-1]; * System.out.printin ("Reading from file " + filename +". a try { parser = new langX(new java.io.FileInputStrean(filename)) ; 3 catch (java.io.FilelotFoundException ) { System.out.printin("File " + filename + " not found."); return; + - parser-Menosshort = ms; parser-program(); // chama 0 método que faz a andlise // veritica se houve erro léxico if ( parser. token_source.foundLexError() ) System. out.printIn(parser.token_source.foundLexError() + " Lexical Errors found") else System.out.printIn("Program successfully analized.") ; 3} // main static public String im(int x) // metodo auxiliar { int k; String s; 5 = tokenImage[s] ; k = s.lastIndex0("\") ; try {s = s.substring(1,k);} catch (StringIndexOutOfBoundsException e) o return 8; OOUMORPO HOVE ERR EER S ESE tsetse 50 | COMO CONSTRUIR UM COMPILADOR. + 2} // langk 7a PARSER_END (langX) O método estatico im é utilizado apenas para relacionar um token reconhecido com 0 sett “nome”. Por exemplo, para um identificador, o nome ser IDENT, para uma constante inteira, seré int_constant, para a palavra reservada class, seré class. Exsses nomes sio definidos no arquivo .jj, na seco que descreve os tokens a serem reconhecidos. Em seguida, vem a seciio demarcada pela palavra-chave TOKEN_MGR_DECLS. Essa seco serve para que possamos introduzir cédigo Java na classe correspon- dente ao AL. No nosso AL, iremos introduzir uma varidvel no ptiblica chamada countLeError. Essa varivel incrementada a cada erro léxico encontrado no ar- quivo sendo analisado. E introduzimos também um método que simplesmente retorna o valor dessa variavel. Esse método seré usado pelo AS, no final da andlise para de- terminar se ocorreram erros léxicos ou nfo. Programa 3.3 1 TOKEN NGR_DECLS =: at 8 int countLexError = 0; 5 public int foundlexError() coms 7 return countLexErrer; o } 0 } A seco seguinte é a mais importante para a definigaio do AL. Nela so definidos quais os tokens a serem reconhecidos, quais 0s caracteres a serem desprezados ¢ outros comportamentos que o AL deva ter. Iniciaremos mencionando quais so os caracteres que devem ser desprezados antes de se iniciar o reconhecimento de um token, como fizemos quando utilizamos um AFD para definir 0 AL. Isso é feito da seguinte forma: Programa 3.4 1 SKIP : ae al s I ole Pe leenee s } Todas as definigdes nesta seco utilizam a representacio de expresses regulares. A palayra SKIP indica ao JavaOC que desejamos definir quais sio as cadeias que CAPITULO 3- ANALISE LEXICA 51 devem ser ignoradas. No caso, estamos dizendo que um “branco” ou um tab, ou um final de linha, ou um carriage return, ou um line feed, devem ser ignorados e no entrar no reconhecimento de um token. ‘Além do SKIP, 0 JavaCC emprega também a palavra TOKEN que € utilizada para definir, por meio de expressdes rogulares, quais as cadeias a serem reconhecidas © quais os tipos de tokens que a clas correspondem. Vejamos o caso das palavras reservadas. Sua definicao é bastante simples, pois a expressio regular que as define 6 a propria palavra. Temé Programa 3.5 7> Palavras reservadas */ TOKEN : a s < BREAK: "break" > «| 1 | < CONSTRUCTOR: "constructor" > s | < ELSE: "else" > | < EXTENDS: "extends" > 10 «| < FOR: "for" > | < IF: "ig" > ws | < INT: "int" > 2 | 1 | < PRINT: "print" > as | < READ: "read" > to | < RETURN: "return" > i | < STRING: "string" > 1s | < SUPER: "super" > 3 Essa definigdo diz que os possiveis tokens a serem reconhecidos serdo as cadeias break, class, constructor etc. A cada uma dessas cadeias ¢ associado um tipo de token, que, no caso, chamamos de BREAK, CLASS, CONSTRUCTOR etc. Esses tipos ‘de tokens irdo corresponder, na implementagao do AS, a constantes inteiras defi- ‘nidas no arquivo LangxConstants, que é a superclasse imediata da classe Langs. Assim, podemos nos referenciar a essas constantes como langXConstants. BREAK ou LangXConstants. CLASS, ou LangXConstants. CONSTRUCTOR. Assim como descrito no inicio deste capitulo, o AL é construido de modo que a ‘maior cadeia possivel seja reconhecida. Por isso no hé nenhum problema, quanto a, ‘ama expressio regular poder gerar subcadeias de outra expresso regular. Sempre ser considerada a maior cadeia da entrada que casar com alguma expressio regular. ‘Isso acontece, por exemplo, a seguir com os tokens G7 ¢ GE. Se a entrada possuir um. Sting >=, este ser identificado como um GE, mas se possuir um > apenas, entio GT ‘seré.0 casamento realizado. 52 COMO CONSTRUIR UM COMPILADOR 7* Operadores +/ meals Gis TOKEN : 4 < ASSIGN: "=" > {4074 2". > I< Loe es I< les iba I< I< le I< + /+ Simbolos especiais +/ TOKEN : 4 < LPAREN: "(" > 1 < RPAREN: ")" > I< LBRACE: "{" > | < RBRACE: "}" > | < BRACKET: "[" > | < RBRACKET: "]" > |< sEMtcotow. > 1 < COMMA: > I< por: "." > a ‘ Note que o arquivo .jj pode possuir diversas segdes SKIP ou TOKEN. Aqui divi- dimos conforme nossa conveniéncia, de acordo com a similaridade entre os tokens. A seguir descreveremos os tokens relacionados a constantes e nomes de identificadores. Programa 3.7 // nimeros decimais, octais, hexadecimais ou binaérios ar ie 3 hcg a Sa (Lro"="7"1)4 fo", "0" ) | (CHOOT MAN -OFS mane") OR", MH] ) | (Droleanly= [be ear) < string_constant: // constante string como "abcd beda" Nan \et)e H\m > CAPITULO 3 - ANALISE LEXICA 53 u | 15 < null_constant: "null" > // constante null aw 1s /* Identificadores «/ 20 TOKEN : ai ke 22 < IDENT: (|)+ > = 2¢ << #LETTER: ["A"-"Z","a"—"2"] > ao | 2 < #DIGIT: a + ng") > Uma constante inteira ¢ um namero decimal, ou um nimero octal seguido pela letra o ou O, ou um numero hexadecimal seguido por h ou H, ou, ainda, um nimero bindrio seguido por b ou B, todos sempre iniciados por um digito (constantes hexade- cimais no podem comecar com A-F ou a-f). Aqui, novamente, aplica-se a regra de que o AL deve identificar como token a maior cadeia posstvel. Assim, por exemplo, vejamos alguns tokens “‘estranhos” que podem ser reconhecidos por essa definicao: 128afhoje deve ser reconhecido como uma constante inteira 123afh seguida de um identificador oje; ObaCh também é uma, constante hexadecimal; 762000 6 uma constante octal seguida por um identificador O; 10141tb10b @ uma constante decimal 1011 seguida pelo identificador th10b. Uma constante string, segundo a definigéo dada, inicia-se com um abre aspas que ‘vem seguido por qualquer caractere, exceto um fim de linha (\n e \r so constantes reconhecidas pelo JavaCC), ou um fecha aspas, que serve para finalizar a constante. E a palavra null também é reconhecida como uma constante, no caso, uma referéncia a.um objeto. Os identificadores so definido como sendo iniciados por uma letra, seguida por letras ou digitos. Foram utilizados dois tokens #LETTER e #DIGIT para definir IDEWT. Esses tokens no so utilizados na gramatica da linguagem X+*, mas servem como auxiliares na definigao do proprio AL. Note também que palavras como for, class, etc, casam com a definicao de identificadores. Ou seja, quando uma dessas palavras aparece na entrada, ela casa tanto com a definigao mostrada anteriormente de palavra seservada quanto com a definigSo de identificador, gerando uma dupla interpretagao. Esso 6 0 caso em que precisamos definir uma prioridade e indicar qual das opgdes deve ser assumida. O JavaCC utiliza uma regra bastante simples: a definic&o que aparecer primeiro no arquivo .jj sera utilizada no caso de dupla interpretagao. Quando hé o casamento de uma cadeia da entrada com um dos tokens definidos no arquivo .jj, o AL reconhece essa cadeia como um token e produz um objeto do tipo Token, que é uma classe Java definida com as seguintes variaveis: 54 | COMO CONSTRUIR UM COMPILADOR. int kind; Contém o tipo do token reconhecido. Cada um dos tokens descritos no arquivo .jj como IF ou IDENT 6 definido na classe tangXConstants como sendo uma constante inteira. Assim, supondo que LangXConstants. IDENT foi definido com 0 valor 9, entao ao reconhecer um identificador, o AL iré produzir um objeto Token cuja varivel kind tem o valor 9; int beginbine, beginColum, endbine, endColumn; Bssas variaveis indicam, res- pectivamente, a linha ¢ a coluna dentro do arquivo de entrada, onde se inicia e onde termina 0 token reconhecido; String image; E a cadeia que foi lida e reconhecida como token. Por exemplo, se a cadeia funci0 foi lida na entrada e reconhecida como um IDENT, entao essa varidvel contém a cadeia lida, ou seja, func10; Token newt; Uma referencia para o préximo token reconhecido apés ele. Se o AL ainda no leu nenhum outro token ou se esse 6 0 tiltimo token da entrada, entao seu valor 6 null; Token spectalfoken; E um apontador para o tiltimo token especial reconhecido antes deste. Veja mais adiante os comentarios sobre o que sao os tokens especiais. Esses objetos siio enviados para o AS, cada vex que ele necessita de um novo token para analisar sintaticamente a entrada. Os campos como linha e coluna em que aparecem os tokens so iiteis para que se possa emitir mensagens no caso de erros sintaticos. Com eles, 0 AS pode indicar em que ponto do arquivo de entrada aparece 0 token que viola as regras sintaticas da linguagem. 3.4 Comentarios ‘Também 6 uma das atribuig6es do AL reconhecer os comentarios do programa e tomar a atitude correta em relacdo a eles, que pode ser, por exemplo, simplesmente igno- ré-los. Note que um comentario ndo é um item léxico, ou seja, nfio deve ser enviado para o AS, uma vez que a gramtica da linguagem nem sequer faz referéncia aos comentérios. E nem poderia, pois na maioria das linguagens, até mesmo a nossa, um comentario pode aparecer em qualquer ponto do programa, como, por exemplo, no meio de um comando ou expressio: a = b.myMethod(10, /* esse 6 um comentario */ c) + 2; Na linguagem X ++ utilizamos dois tipos de comentarios. O primeiro é 0 comenta- rio de uma tinica linha que se inicia com // e vai até o final da linha. E 0 segundo é 0 comentario que pode conter diversas linhas e que se inicia com /* e termina com */. Ambos os tipos sao também encontrados em Java. Ao encontrar esses comentarios, 0 nosso AL iré simplesmente ignoré-los. Para implementar essa funcionalidade no AL, iremos utilizar uma outra caracte- ristica do JavaCC que € 0 conceito de estado. A idéia 6 que ao encontrar uma cadeia que inicia um comentario, por exemplo um /*, o AL passa a operar em um estado CAPITULO 3- ANALISE LEXICA 55 diferente. Nesse estado, as cadeias lidas nfio tém o mesmo significado que no estado normal do AL. Elas nao sao reconhecidas como tokens, mas, sim, sfo lidas e descar- tadas, até que se chegue & cadeia que fecha o comentdrio. Quando isso acontece, 0 AL ¢ colocado de novo no seu estado normal, onde reconhece os tokens especificados. Para cada estado que utilizarmos, devemos atribuir um nome. O estado “normal” do AL tem um nome predefinido que 6 DEFAULT. Vamos, entao, alterar o nosso AL, fa- zendo com que, ao achar um /*, o AL mude para um estado chamado multilinecomment. Isso 6 feito com a seguinte definigao: Programa 3.8 SKIP : a t “/*" : mltilinecomment + Note que utilizamos um SKIP no estado DEFAULT, pois queremos que 0 inicio do comentério seja ignorado e que o AL passe para o estado mul¢ilinecomment. Porém, essa mudanga de estado pode ser feita em qualquer casamento, por exemplo, na definigdo de um token como TOKEN: { < WHILE: "while" > : WHILENODE a Nesse caso, se o AL estiver no estado DEFAULT e encontrar a cadeia while, esta & reconhecida como um token WHILE ¢ 0 AL passa para o estado WHILEMODE. No caso do nosso comentario, falta especificar o que o AL deve fazer no estado multilinecomment. Queremos que tudo que for lido nesse estado seja jogado fora pelo AL, até que seja encontrado um */ que termina o comentério e coloca o AL de volta no estado DEFAULT. Fazemos isso com: Programa 3.9 SKIP: 3 £ 4/" + DEFAULT 1 x Definimos, nesse estado, 0 que deve ser ignorado, por meio de um SKIP. Pode- rfamos também definir um TOKEN em qualquer estado, como fazemos no estado DEFAULT. Queremos que o */ faca o AL voltar ao estado DEFAULT e que qualquer outra cadeia seja ignorada. A definigo ~[ ] é utilizada para fazer 0 casamento com qualquer caractere que nao esteja entre os colchetes. Por exemplo, ~ [0-9] ira casar com qualquer caractere da entrada que nao seja um digito ¢ (~["0"-"9"] )» iré ca- sar com qualquer cadeia que nfo possua digitos. E importante notar que, na nossa 56 COMO CONSTRUIR UM COMPILADOR definig&o do comentario, as cadeias * e / também formam um casamento com 0 pa- dro ~[]. Porém, vale a regra de que sempre a maior cadeia possfvel 6 utilizada no casamento. Como o segundo padrao tem apenas um caractere, entdo ao aparecer a cadeia */, o casamento é sempre feito no primeiro padrao. A definicdo de comentarios de uma linha sé ¢ feita de maneira semelhante: Programa 3.10 1 SKIP: cs a 3 "//" : singlelineconment a} 5 SKIP: WE 7 : DEFAULT of -—<0 > aE Note que, nesse caso, um final de linha volta o AL, ao estado DEFAULT. Aqui vale a regra de prioridade para os padrdes que sao definidos antes. Assim, as cadeias \n ou \r na entrada poderiam casar tanto com o primeiro quanto com o segundo padrao, mas, pela prioridade, o casamento ¢ feito com o primeiro. 3.5 Recuperacao de erros léxicos ‘Um erro léxico ocorre quando alguma cadeia que aparece na entrada ndo pode ser reconhecida como um token vélido. Por exemplo, se tivermos na entrada a cadeia @OG##@G, veremos que o AL que definimos para a linguagem X+* ira apontar um erro. Nesse caso, 0 AL gerado pelo JavaCC toma uma atitude radical, que é langar um erro do tipo TokenMgrError. Como no podemos tratar esse erro, 0 que ocorre € que ele 6 propagado até os niveis mais altos do compilador, que teré um término anormal. Esse comportamento muitas vezes nao é adequado. Gostarfamos, por exemplo, de que o AL emitisse uma mensagem de erro, passasse por cima da cadeia invalida e continuasse com a anilise. Para isso, devemos evitar que 0 AL chegue a identificar um erro. Devemos es- pecificar casamentos também para as cadeias que so invélidas e devemos fazer o tratamento de erro desejado para essas cadeias. Vamos, entSo, tentar identificar quais so os casos em que uma cadeia pode provocar um erro léxico. A primeira delas ¢ quando tentamos iniciar 0 reconhecimento de um token e deparamo-nos com um caractere que nao casa com nenhuma das expressdes de ca- samento definidas. Devemos, entao, passar sobre os caracteres da entrada até que ‘encontremos um que possa ser utilizado num casamento. Por exemplo, se tivermos a cadeia 1234000\#\#@0" abcd" na entrada, o AL deve reconhecer o token 1284 ¢ parar ao encontrar o primeiro @. Na proxima execugao do AL, temos um erro, pois @ nao inicia nenhum padrao de casamento. Devemos, entdo, pular toda a cadeia até que um caractere vilido seja encontrado, no caso a aspa. A prxima execugao retorna 0 token "abed", CAPITULO 3- ANALISE LEXICA 57 Para identificar esse tipo de erro faremos 0 casamento de todas as cadeias formadas por caracteres que nao iniciam nenhuma,expressao de casamento para cadeias validas. Para isso, olhamos as expressdes usadas na. nossa definicao ¢ vemos que os tokens ¢ SKIPs definidos anteriormente se iniciam sempre por: © uma letra; um digito; uma aspa; @ caracteres especiais como ([) ] ; < > © um espaco ou delimitador de linha. Assim, definiremos uma expressdo que case com as cadeias invilidas. Podemos definir, ent&o, o token que chamamos INVALID_LEXICAL como: Programa 3.11 a 2 f 33 System.err.printla("Line " + input_stream.getEndLine() + CAPITULO 3 - ANALISE LEXICA 59 a ” . Invalid string found: "+ image); s countLexErrort+; ry ae a | ss 0 NED “ Systen.err-printin("Line " + input_strean.getEndLine() + 2 “2 String constant has a \\n: " + image); 4 countLezErrort+; feet O eédigo associado ao token especial mostra a mensagem de erro e inerementa a varidvel countLeaError, que foi definida na classe LangXTokenMgr, por meio da secao TOKEN_MGR_DECLS, mostrada anteriormente A definig&o do token especial incorpora também o tratamento de um segundo tipo de erro léxico, identificado pelo token especial INVALID. CONST. Ele ocorre quando uma, constante string é iniciada (uma cadeia iniciada por aspa), mas nao termina antes do final da linha. Nesse caso, tem-se um erro léxico, pois nao existe casamento valido posstvel. Esse casamento é definido, entio, no token especial como sendo qualquer cadeia iniciada por aspa e terminada por um fim de linha. 3.6 Arquivos-fonte do compilador Na pagina de downloads da editora (attp: //uww .novateceditora.com.br/downloads. php), 0 leitor encontrar4 todos os arquivos-fonte relativos 4 implementacao do compi- Jador para X++. No final de cada capitulo, uma se¢Go como esta descreve quais so ‘0s arquivos relatives Aquele capitulo e como devem ser usados. No subdiretério cap03/parser, encontra-se o arquivo langX++.Jj, que contém a definiggo do AL estudado neste capitulo. Para gerar a primeira versio do nosso compilador, o leitor deve executar os seguintes comandos: cap03/parser$ javacc langk++.jj Java Compiler Compiler Version 2.1 (Parser Generator) Copyright (c) 1996-2001 Sun Microsystems, Inc. Copyright (c) 1997-2001 WebGain, Inc. (type "javacc" with no arguments for help) Reading from file langk++.jj . . . File "TokenMgrError.java" does not exist. Will create one. File "ParseException. java" does not exist. Will create one. File "Token. java" does not exist. Will create one. File "SimpleCharStream.java" does not exist. Will create one. Parser generated successfully. cap03$ javac parser/langX. java 60 COMO CONSTRUIR UM COMPILADOR O primeiro processa 0 arquivo com a definigdo do nosso AL e gera os arquivos com 0 cédigo Java do AS e do AL, além de alguns outros arquivos auxiliares. O segundo comando compila a classe LangX e todas as classes que ela utiliza. Como resultado, obtém-se 0 arquivo langX.class que pode ser executado utilizando uma méquina virtual Java. Essa primeira versio do compilador somente 1é a entrada, identifica os tokens ¢ mostra-os na safda-padrao. Ble aceita a opc&o short, que reduz a quantidade de informagio mostrada. O leitor pode experimentar o programa, utilizando o programa bintree.c, colocado no diretério ssamples da pagina de downloads ou, ainda, com o programa biniree-erro-lesico. que contém alguns erros léxicos: cap03$ java parser.langX -short ../ssample/bintree.x X++ Compiler - Version 1.0 - 2004 Reading from file ../ssamples/bintree.x . . class class 13 bintree 29 { {34 class class 13 data 29 fies int int 19 dia 29 ees mes 29 men3s ano 29 treeprint 29 ( (32 0 26 ) ag} 33 38 return return 23 0 26 3 38 } 35 3 35 0 Program successfully analyzed. eas cap03$ java parser.langk ../ssample/bintree-erro-lexico.x > out Line 9 - Invalid string found: # Line 18 - Invalid string found: ##i# Line 103 - String constant has a \n Elemento ja existe Capitulo 4 Analise Sintatica Neste capitulo comegaremos a estudar 0 “coracdo” do compilador, que ¢ 0 anali- sador sintatico. E ele quem analisa o programa fonte e verifica se este pertence ou ndo a linguagem desejada. E também a base para as demais fases do compilador, como a anélise semfntica e a geracio de cédigo, mediante a construgdo da Arvore sintética. Neste capitulo veremos somente os aspectos relacionados & anilise sintética, propriamente dita. A recuperacdo de erros e a construgéo da arvore sintatica serao tratados nos capitulos seguintes. Existem diversas técnicas para se implementar um analisador sintético. Vamos aqui nos concentrar numa técnica chamada de descendente recursiva. Essa técnica é bastante simples e flexivel. E também eficiente no sentido de que um analisador sintdtico construido usando essa técnica tende a ser rapido ao analisar programas. Ini- cialmente iremos estudar com mais cuidado o que ¢ a anélise descondente recursiva- Depois veremos como implementar um analisador sintético descendente recursivo uti- lizando 0 JavaCC. 4.1 Analise sintatica descendente recursiva Dada uma GLC, a andlise sintatica baseada nessa GLC pode ser ascendente ou des- cendente. Um analisador ascendente agrupa os simbolos da entrada para formar os simbolos nao terminais mais distantes do simbolo inicial da GLC ou que esto em niveis inferiores na estrutura da linguagem. Ja na andlise descendente, parte-se do simbolo inicial da GLC e busca-se identificar na entrada as construgées que corres- pondem As producées da GLC. ‘Um analisador descendente recursivo utiliza essa tiltima estratégia. Ele 6 chamado de recursivo, pois a cada simbolo nao terminal da GLC corresponde uma fungao (ou procedimento, ou método) recursiva, responsavel por verificar se a entrada “casa” com a estrutura daquele ndo-terminal. Vamos tomar como exemplo a produgao vista no capitulo 2: 61 62 COMO CONSTRUIR UM COMPILADOR (classdecl) —+ “class” “ident” [ “extends” “ident” | (classbody) ‘Uma entrada que case com essa produgio deve iniciar-se com a cadeia class, que vem seguida de um identificador, pode ter um eztends e um identificadot e termina com o corpo da classe, representado na producéo pelo nao-terminal classbody. No analisador descendente recursivo, 0 método correspondente a essa produgéo deveria entao: * verificar se 0 proximo token da entrada é class. Se for, deve continuar com a analise. Se nfo for, um erro sintético ocorreu, pois a entrada nao corresponde produgéo esperada; © verificar se 0 préximo token ¢ ident. Se for, deve continuar com a anélise. Se no for, um erro sintatico ocorreu, pois a entrada nao corresponde a produgio esperada; verificar se o préximo token 6 extends. Se nao for, deve continuar a anilise, pois essa parte da produgéo ¢ opcional. Se for, deve verificar se 0 token seguinte é um ident. Se for, deve continuar com a andlise. Se nfo for, um erro sintético ocorreu, pois a entrada nao corresponde & produgio esperada; © verificar se o resto da entrada corresponde ao ndo-terminal classhoby. Isso néio 6 feito diretamente pelo método, mas, sim, invocando o método correspondente Aquele nfio-terminal. A cada ver que o método necesita de um token da entrada, ele invoca o AL, que Ihe fornece o token. Terfamos algo semelhante ao programa 4.1. Programa 4.1 void classdecl() are 3 if ( curfoken.type == CLASS) // token corrente @ "class" ‘ analex(); // chama AL. curToken recebe novo token 5 else * SintaticError(); __// ocorreu erro sintatico + if ( curToken.type == IDENT) // token corrente @ identificador a analex(); 2 alse 10 SintaticError() ; 1: if ( curToken.type == EXTENDS) // token corrente @ "extends"! caine uel 1 analex(); “ if’ ( curToken.type == IDENT) // token é identificador 1 analex(); 16 else ” SintaticError() ; 18 3 » classboay(); » + CAPITULO 4 - ANALISE SINTATICA 63 A variével curToken contém sempre o iiltimo token devolvido pelo AL (anatea). Ao se invocar o método classdecl, curToken jé possui o token correspondente ao primeiro sfmbolo correspondente a esse ndo-terminal, no caso, o token que deve corres- ponder & palavra “class”. Essa politica 6 mantida ao chamar-se 0 método classbody, pois curfoken j4 possui o token que deve casar com 0 infcio da produgao daquele no terminal. B importante notar que o token lido na entrada serve para indicar qual 0 caminho a ser seguido no reconhecimento dos préximos tokens. Uma vez que um token foi lido da entrada e casado com algum simbolo de uma produco, nao h4 como retroceder, caso algum token posterior ndo combine com a producdo escolhida. Por exemplo, na produgao (classdecl) -+ “class” “ident” (classbody) | “class! “ident” “extends” “ident” (classbody) se utilizarmos 0 token de entrada class para fazer 0 casamento com a primeira pro dugiio, ent&o teremos escolhido seguir essa produgao, e entradas que possuam extends ident nfo poderdo ser reconhecidas. E analogamente, se escolhermos a segunda pro- dugao, nao serao aceitas cadeias sem extends ident. Portanto, precisamos ter cuidado com as produgées que utilizamos na construgao de um analisador descendente recur- sivo. As gramaticas que podem ser utilizadas na construgao de um analisador descen- dente recursivo so chamadas de LL(1). O primeiro L significa left to right e indica que a leitura da entrada é feita da esquerda para a direita. O segundo L significa left linear e indica que sempre o simbolo nfo terminal que estiver mais & esquerda seré reconhecido primeiro. E 0 niimero 1 indica que o néimero de simbolos de lookahead necesséirios 6 1, ou seja, que se conhecendo apenas um sfmbolo da entrada ¢ posstvel decidir que producio utilizar. Na produgdo citada seriam necessdrios tres simbolos para se decidir qual produgo utilizar. Para auxiliar na construgao de um analisador descendente recursivo, costuma-se construir uma tabela preditiva. Essa tabela possui uma linha para cada ndo-terminal da GLC e uma coluna para cada terminal. Se tivermos 0 néo-terminal A e o terminal a, na posiggo A x a devemos colocar qual é a produgio que devemos utilizar se, ao tentarmos reconhecer A, encontrarmos na entrada o simbolo a. Por exemplo, se tivermos S — aSAb| bAa A — bAbjc teremos a seguinte tabela preditiva: ‘Terminais ‘Nao-terminal @ b ec s S$ aSAb | S > bAa A a S—bAb| Se 64 COMO CONSTRUIR UM COMPILADOR. As posigdes vazias na tabela indicam que esse terminal ndo pode aparecer no inicio do nao-terminal. Se tivermos mais do que uma produg&o em alguma posigao da tabela, entao a GLC nao pode ser usada na construgao de um analisador descendente recursivo com um tinico s{mbolo de lookahead. Muitas vezes, porém, podemos alterar a GLC de forma a eliminar esses problemas, sem alterar a linguagem gerada. ‘Um caso tipico de problemas ao construir a tabela preditiva é relativo a a existencia de recurso & esquerda. Isso acontece quando, para algum néo-terminal B, temos B 4 Ba, mediante a aplicagéo de uma ou mais produces. Isso significa que a partir de B podemos gerar uma forma sentencial em que o proprio B aparece como primeiro simbolo. Intuitivamente é facil verificar que isso causa problemas. Ao tentar reconhecer B, 0 AS descendente recursivo invoca 0 método correspondente 20 n&o- terminal. Olhando para o token da entrada, esse método pode escolher um caminho que leve ao aparecimento de outro B, sem que nenhum token tenha sido consumido —o que caracteriza a recursao A esquerda. Isso faz com que o mesmo caminho seja seguido, o que vai levar novamente a outro B, e assim por diante. Isso faz com que 0 AS permaneca numa recursao infinita, sem consumir nenhum token. A recurso a esquerda pode aparecer direta ou indiretamente. Ela é direta quando temos B = Bee, indireta, quando B > Aa => Bf. A eliminagSo da recursao indireta pode ser bastante complicada e nao seré tratada aqui. A eliminagao da recursao direta, por outro lado, simples e pode ser resolvida da seguinte maneira: © dividem-se as produgées do n&o-terminal B em dois subconjuntos N = {ai,...,n}, das produgdes que no possuem recurso a esquerdae R = {Bf,..., Bn}, das produgdes com recurso 8 esquerda; « climinam-se as produgées de R: ¢ adicionam-se as produgdes B + 0B" | ... | anB’, onde B’ 6 um novo simbolo nfo terminal, que nao pertencia A gramética; ¢ adicionam-se as seguintes produg6es para B’: BY + 61 | ...| Sm | $B" |... | mB’. Com esse procedimento, trocam-se as recursdes & esquerda por recursdes & direita, que nao so problematicas para a andlise descendente recursiva. Um exemplo classico de eliminagéio de recurs&o 4 esquerda ¢ para a gramatica: expression — expression +term | expression — term | term term = term « factor | term/ factor | factor + ident | constant | (expression) Aplicando a eliminagao descrita, teriamos: expression + term | term expression! expression’ —+ —term| +term | — term expression’ | + term expression’ term — factor | factor term’ term! = xfator | /factor | + factor term’ | /factor term’ factor — ident | constant | (expression) CAPITULO 4 - ANALISE SINTATICA 65 Outro modo de modificar uma GLC para que possa ser usada num analisador descendente recursivo é fazendo a fatoracdo & esquerda daquelas produces de um mesmo ndo-terminal que tém um prefixo em comum. E 0 caso do exemplo visto anteriormente: {classdecl) + “class!’ “ident” (classbody) | “class” “ident” “extends” “ident” (classbody) Se fatorarmos & esquerda, teremos a producao que usamos na GLC da linguagem ase guage at: {classdecl) —+ “class” “ident” [ “extends” “ident” | (classbody) Ou na GLC das expressées, terfamos: expression — term expression! expression’ —> —term [expression’] | +term [expression’| | d term — factor term’ term’ — factor {term'] | / factor [term’] | ‘factor + ident | constant | (expression) A primeira vista, pode parecer facil calcular a tabela preditiva de uma GLC. Basta olhar quais so os tokens que iniciam as produgées do nfo-terminal B e colocar na linha de B, na coluna desses tokens, as produgdes que eles iniciam. Porém, existem diversas situagdes que podem complicar essa tarefa. Vejamos alguns desses casos: « se temos B + Aa | C8, entao 6 preciso saber quais so os tokens que iniciam as produgdes de Ae G para decidir qual producio utilizar. B essa situagdo pode se propagar, uma vez que A e @ podem também se iniciar com ndo-terminais; se temos B — Aj A...Anac, precisamos saber se Aj,... An podem gerar a cadeia varia. Se isso acontecer, entio o token a também deve ser utilizado para decidir qual produgio de B utilizar; « setemos A — aBb; B > aea + A, entao b deve ser considerado como um token que deve ser utilizado para decidir pela produgio B — a ao tentar reconhecer B. Vamos, entdo, definir exatamente como a tabela preditiva deve ser construida. Primeiro, adicionamos um novo simbolo terminal 4 GLC. Esse terminal denotado por $ aparece sempre no final da cadeia de entrada. A seguir, vamos associar 4 GLC duas fungdes, FIRST e FOLLOW. Seja a uma forma sentencial da gramAtica. Entao FIRST(a) 6 definido como o conjunto de todos os simbolos terminais a tal que « + af, ou seja, o conjunto de terminais que iniciam alguma cadeia derivada a partir de a. Para calcular 0 FIRST de um string, devemos seguir as seguintes regras: 66 COMO CONSTRUIR UM COMPILADOR. © para o terminal a, FIRST(a) = {a}; @ para o néo-terminal B, tal que B > Aj Ap...An, onde A; séo simbolos terminais ‘ou nao terminais, fazemos: — inicialmente FIRST(B) = {} — repetimos FIAST(B) = FIRST(B) U FIRSI(A;), para i = 1,2,... até que encontremos algum i tal que A; ndo deriva A; © para um string « = A, Ap...An, onde A; so simbolos terminais ou nfo terminais, fazemos: — repetimos FIRST(c:) = FIRST(a) U FIRSI(A;), para i = 1,2,... até que encontremos algum i tal que A; no deriva 2. Na GLC fatorada que mostramos anteriormente, terfamos os seguintes conjuntos, para os terminais e no-terminais: Frrst($) = {$} FIRST(+-) = {+} FrRst(-) = {-} Frrst(*) = {*} FrRST(/) = {/} FIRST(ident) = {ident} FIRST(constant) = {constant} Frrst(() = {(} FIRST(factor) = {ident, constant, (} FIRST(term’) = {*, /} FIRST(term) = {ident, constant, (} FIRST(expression’) = {+, -} FIRST(expression) = {ident, constant, (} Seja B um néo-terminal da gramdtica e 5 o seu simbolo inicial. O conjuato FOLLOW(B) 6 formado pelos terminais a tal que S > aBa(, ou seja, existe uma forma sentencial derivvel a partir do simbolo inicial em que « aparece imediatamente & direita de B. A importancia do FOLLOW pode nao ser tao intuitiva como a do FIRST, mas se tivermos um simbolo nao terminal B que possui uma produgdo do tipo B > a, e a + A, entdo @ importante que saibamos quais sdo os simbolos de FOLLOWB). Vamos tomar, por exemplo, o terminal a e supor que a € FOLLOW(B). Isso significa CAPITULO 4- ANALISE SINTATICA 67 que no analisador descendente recursivo, ao executarmos 0 método correspondente a B, iremos consumir um certo nimero de tokens e, se esses tokens casarem com a definig&éo de B, podemos ter um a na entrada. Porém se B + A, isso significa que nao consumindo nenhum token da entrada, a pode aparecer na entrada e seré casado em algum ponto, apés 0 reconhecimento (nulo) de B. Portanto, se no inicio do reconhecimento de B tivermos um a na entrada, devemos seguir a producio que faz com que B derive a cadeia vazia. Se tivermos outra produco como B > Aa ea € FIRST(A), entdo ocorrer4 um problema na GLC, pois com o mesmo terminal temos duas produgées a aplicar. Para calcular os conjuntos FOLLOW para os nao-terminais da GLC, devemos: adicionar o indicador de fim de cadeia $ ao conjunto FOLLOWS), onde § 60 simbolo inicial da GLC; © dada a produgéo B > aA{, fazemos: FOLLOW A) = FOLLOWA) U FIRST(S) se § + 2, entio fazemos FOLLOW(A) = FOLLOW(A) U FOLLOWB). Calculando 0 FOLLOW para o nosso exemplo, terfamos: FOLLOW expression) = {8, )} FOLLOWMexpression') = {8, )} FoLLOWterm) = {+,-, 8, )} FoLLoMterm’) = {+, -, $, )} FOLLOW factor) = {*, /,+,-,$,)} Uma vez calculados os conjuntos FIRST e FOLLOW, podemos, entao, construir a tabela preditiva da GLC. Para isso, vamos examinar as produgées da linguagem e tentar descobrir em que posicdes colocé-las na tabela. Devemos fazer: © dada a produgio B —+ a, para todo terminal a € FIRST(a), devemos colocar essa produgo na posigao (B, a) da tabela; © dada a produgio B — a, onde a + A, para todo terminal a € FOLLOW .B), devemos colocar essa produgéo na posigéo (B, a) da tabela. Seguindo essas regras, construimos, entdo, a tabela preditiva para a GLC de ex- pressdes, dada na tabela 4.1. A tabela preditiva pode ser usada, além da. verificacdo ‘da gramitica, para construir o AS. Para implementar 0 método de um néo-terminal, ‘podemos olhar a tabela e determinar para cada terminal valido quais so as seqtiéncias de ages a tomar, como, por exemplo, consumir um terminal ou invocar os métodos & outros n&o-terminais. Porém, se a linguagem ¢ complexa, o célculo da tabela preditiva e a construgdo 4 AS de forma manual so tarefas 4rduas. Ainda mais se estivermos trabalhando 68 COMO CONSTRUIR UM COMPILADOR Tabela 4.1 — Tabela preditiva para a GLC de expressées. Terminals Nao terminais | ident | constant | +,- | *,/] (|) [$ expression I 1 1 expression’ 2 3] 3 term 4 4 4 term’ 6 | 5 6] 6 factor 7 8& 9 (Iexpression — term expression’ (2)expression' + +term expression’ (3)expression’ — (A)term — factor term! (5)term’ — «factor term’ (G)term! > d (7) factor — ident (8) factor — constant (9) factor + (expression) com uma gramética na BNF, pois, como vimos, 0 cdlculo dos conjuntos FIRST e FOLLOW consideram apenas produgdes normais, sem os operadores da BNF. Nesse ponto entram os programas como 0 JavaCC, que, baseado na definicio da linguagem em BNF, gera todos os métodos correspondentes aos nao-terminais e verifica possfveis problemas, avisando ao implementador os pontos da gramatica que os originaram. Na segfo seguinte abordaremos a implementagao do AS para alinguagem X++utili- zando 0 JavaCC. 4.2 Oanalisador sintatico da linguagem X ** ‘Veremos, a seguir, como implementar o AS, com base na gramética vista no capitulo 2, utilizando 0 JavaCC. Inicialmente, adicionaremos uma nova op¢ao na primeira parte do arquivo .ij Programa 4.2 options < STATIC = fals: DEBUG_LOOKAHEAD = true; # A opgdo DEBUG_LOOKAHEAD = true habilita o mecanismo de depuragao do AS que serd gerado pelo JavaCC. Isso faz com que o AS mostre na safda-padrao quais sao os ndo-terminais que esto sendo “executados” e quais so os tokens que so consumidos em cada um deles. Além disso, mostra também as tentativas de casamentos feitas a0 analisar lookaheads. No final deste capitulo, mostraremos um exemplo em que esse tipo de safda esta habilitada. CAPITULO 4 - ANALISE SINTATICA 69 Nem sempre queremos ver quais so as ages tomadas pelo AS. Em geral, estamos mais interessados em saber se existe um erro sintatico e qual é esse erro. O JavaCC permite que o mecanismo de depuragao do AS seja desabilitado em tempo de execugao. Assim, nosso compilador aceita como parametro de entrada a opgao -debug_AS. Se essa opgao for fornecida quando 0 nosso compilador é executado, entao as informagoes de depuragao serao mostradas. Se essa opgao nao 6 utilizada, entaéo o AS mostra apenas se existe e qual é 0 erro sintatico. Para isso, alteramos também 0 método main do nosso AS. Ele verifica se a op¢ao foi dada ou nao e, em caso negativo, invoca 0 método langX.disable_tracing() do AS criado (varidvel parser do programa 4.3). Programa 4.3 PARSER_BEGIN (langX) package parse: import java.io.*; public class langk { final static String Version = "I++ Compiler - Version 1.0 - 2004"; int contParseError = 0; // contador de erros sintéticos 11 // Define o método "main" da classe langX. 12 public static void main(String args[]) throws ParseException a3, t 34 boolean debug = false; 16 String filename = ""; // nome do arquivo a ser analisado 1 langX parser; // analisador léxico/sintatico 18 int 4; » boolean ms = false; a System. out.printin (Version) ; 2 // 1 os parametros passados para o compilador 23 for (4 = 0; 4 < args.length - 1; i++) Bry t 2 if (args[i] .equals("-debug_AS") ) 2 debug = true; a else 2 { 20 System.out.println("Usage is: " + 30 "java langX [-debug_AS] inputfile"); a System.exit(0); 2 } a8 ~ a if (args [i] -equals("-")) 30 { // 18 da entrada-padro # System. out.printIn ("Reading from standard input . . ."); a parser = new langX(System.in); // cria AS 0 + “0 else a { // 8 do arquivo 70 COMO CONSTRUIR UM COMPILADOR 2 filenane = args[args-length-1]; a System.out.printin ("Reading from file " + filename +"... 4“ try { // cria AS ss parser = new langX(nev java.io.FileInputStream(filename)) ; “ + a” catch (Java.io.FileNotFoundException e) { a System. out.printIn("File "+ filename + " not found."); 2 return; 50 } s 3 8 if (1 debug) parser.disable_tracing(); // desabilita verbose do AS os try { we parser.program(); // chama o método que faz a andlise a i 88 catch (ParseException e) 80 { © System. orr.printin(e.getNessage()); a parser.contParseError = 1; // no existe recuperagéo de erros ° FS ca finally { a System. out.printIn(parser.token_source.foundLexError() + os " Lexical Errors found"); oo System.out.println(parser.contParseError + o " Syntactic Errors found"); oe 3 © 70} // main v2 static public String im(int x) wo f mint ks m String s; m0 8 = tokenImage[x]; 7 & = s.lastIndexOf("\""); ms try {s = s.substring(1,k) 3} catch (StringIndex0ut0fSoundsException e) o 0 es return s; a + 88 a} /f Langk 26 PARSER_END(1angx) No método maén, temos também a chamada a0 método parser.program. Como veremos a seguir, o JavaCC ir4 definir um método para cada simbolo nao terminal da gramatica. A partir das produgées descritas no arquivo jj, sio criados os métodos que tentam fazer o casamento da entrada com os respectivos ndo-terminais. Assim, a chamada do método parser. program ira tentar reconhecer na entrada uma cadeia a — CAPITULO 4- ANALISE SINTATICA = 71 que case com a descrigéo dada a seguir no arquivo .jj para o ndo-terminal program. Esse 6 0 simbolo inicial da gramética. A entrada utilizada ¢ a que est no arquivo passado como parametro na criagio do objeto parser, no inicio do método main (inhas 38 e 45). ‘Uma das vantagens do AS gerado pelo JavaCC € que o reconhecimento néio pre- cisa ser feito necessariamente a partir de algum nfo-terminal especifico. Uma cha- mada a qualquer método que corresponda a um nfo-terminal iré tentar reconhe- cor esse nao-terminal na entrada. Por exemplo, se tivéssemos feito a chamada a parser. eepression, © nosso compilador iria consumir os tokens da entrada proct- rando reconhecer uma expresso apenas, € néo um programa. A chamada a parser.program esta dentro de um comando try porque a sua execugéo ot a de algum outro método correspondente a outro ndo-terminal pode Jangar uma excegéo do tipo ParseEzception. Isso acontece quando o AS detecta na entrada algum erro sintatico. Por exemplo, 0 método correspondente ao nao- terminal classdecl espera encontrar na entrada o token class. Caso esse método seja invocado e no encontre tal token na entrada, entdo ele langa uma excegio. Como nenhum método correspondente aos n&o-terminais trata essa excecdo, ela se propaga até o método que primeiro chamou um desses métodos, no caso 0 método main. No método main, uma mensagem referente ao erro € emitida ¢ a andlise do programa deve terminar. N&o hé como continuar a andlise, pois ¢ impossivel restaurar 0 estado do AS no momento que o erro foi detectado. Assim, ao encontrar um erro sintatico, a execug&o do nosso compilador simplesmente termina. No capitulo 5 veremos como podemos implementar 0 que se chama de recuperagao de erros, que permite que o AS continue sua execugdo mesmo que um erro sintatico seja encontrado. Vejamos, ento, como definir os métodos que iro compor 0 nosso AS. Devemos definir cada néo-terminal da nossa gramAtica mediante uma declaragéo que é uma mistura de cédigo Java e de produgdes na BNF. Cada uma dessas declaragées tem a seguinte forma: (ndo terminal) + (tipo) “nome"" “("" (argumentos) 4{"" (decl. locais) *}" 6 (BNF) J" syrew Essa declaragio inicia-se com um tipo, que ¢ 0 tipo retornado pelo método corres- pondente a esse nao-terminal. Pode ser qualquer tipo acesstvel dentro da classe do AS, no nosso caso, a classe Lang. Depois vern o nome do no-terminal (e do método que serd criado) e os parimetros formais desse método. Em seguida, vem um “ e uma sesso de cédigo Java colocada entre chaves, que corresponde As declaragées locais desse método. As varidveis ai declaradas poderao ser utilizadas dentro do método. E depois vém as producées desse nao-terminal na BNF. A principal incumbéncia do JavaCC é transformar essas produgdes em cédigo Java, que ir tentar reconhecer na entrada uma seqiiéncia de tokens que casem com essa descrigéo. Vejamos, entao, 0 simbolo inicial da nossa linguagem, que 6 0 ndo-terminal program: 72 COMO CONSTRUIR UM COMPILADOR Programa 4.4 1 void progranQ + x a 4. we a ft 5 [ classiist() ] o } Na descricao das produgGes podem aparecer os tokens definidos na se¢do do AL do arquivo .jj (0 HOF é uma excego) e “chamadas” a outros ndo-terminais como classlist. Esse ndo-terminal program 6 utilizado aqui para indicar quando um EOF (fim de arquivo) ¢ valido na nossa entrada. O token EOF nfo foi definido explicitamente nas declaragdes de tokens do AL. Trata-se de um token predefinido que € passado para o AS quando o arquivo de entrada foi completamente lido ¢ néo possui mais nenhum token disponfvel. Entao, esse ndo-terminal program diz que um programa na nossa, linguagem 6 formado por uma lista de classes, seguida pelo HOF. A definicao de classlist € mostrada no programa 4.5. Ela simplesmente segue a definicéio da BNF vista no capitulo 2. Isto vale para classdecl. Sua producio inicia-se com class, vem seguida por um identificador, e assim por diante. Programa 4.5 void classlist(): 2 ¢ 2} aoa s _ classdecl() [ classlist() ] each »® void classdecl(): w { u } a ft 12 [ ] classbedy() u } E bom observar que, em geral, 0 cdigo Java gerado pelo JavaCC para os métodos que implementam os nao-terminais nao tem muita semelhanca com as suas produgses e pode ser dificil de ser compreendido. Por isso, ndo é uma politica recomendavel gerar © AS por meio do JavaCC e, depois, fazer alteragdes no cédigo gerado. Alem disso, se for necessatia a alteracao do arquivo .jj, um novo cédigo Java deve ser gerado e as alteragGes feitas anteriormente diretamente nesse cédigo sao perdidas. Apenas como exemplo, apresentamos no programa 4.6 um trecho de cédigo gerado pelo JavaCC para o método correspondente ao néio-terminal classdecl. CAPITULO 4 - ANALISE SINTATICA 73 Programa 4.6 1 final public void classdecl() throws ParseException { 2 trace_call ("'classdecl") ; 2 try { ‘ jjconsume_token (CLASS) ; . jconsume_token (IDENT) ; ° switch ((jj_ntk 1 case EXTENDS: . jjsconsume_token (EXTENDS) ; ° jj-sconsume_token (IDENT) ; 10 break; n default: 12 jjlet(1] = jj_gen; “ + a classbody() ; 10 } finally { a trace_return("classdecl") ; a8 } 9 + Continuando com a nossa implementagio, temos o nao-terminal classbody. De ‘do com a nossa BNF, esse n&o-terminal deveria ser definido como: Programa 4.7 1 void classbody(): Bp ft 2 } et 5 . [classlist()] 7 (vardecl() )* s (constructdeci ())* ° (methoddecl ()+ ho =} Porém, se tentarmos processar 0 nosso arquivo que 0 JavaCC produz a seguinte adverténcia: contendo essa declaragao, vere- ing: Choice conflict in (...)* construct at line 304, colum 7 Expansion nested within construct and expansion following construct have common prefixes, one of which is: "int" Consider using a lookahead of 2 or more for nested expansion. Essa mensagem indica que existe um conflito na construgo (vardecl() )* da nossa produco. Diz, ainda, que dentro da construcao e fora dela existem cos comuns, entre eles o terminal int. Isso acontece porque o FIRST (vardecl) i o terminal int, que também est4 no FIRST(methoddecl), como mostra 0 pro- 48. 74 COMO CONSTRUIR UM COMPILADOR Programa 4.8 1 void vardeclQ: aft et oa. 3 . ( | | ) e ( )+ 7 ( ( )* )+ = ° 10 void constructdecl(): ae aes nae “4 methodbody() w + 19 void methoddecl() : meat Stair ce 23 ( | | ) ( )* 4 methodbody () a } Assim, 0 JavaCC nao pode decidir se, ao ler 0 token int, 0 método classbody deve prosseguir reconhecendo um vardecl ou um metoddecl. E bom notar que vardecl, constructdecl e methoddecl podem ou nao aparecer numa classdecl, 0 que gera esse conflito. Resumindo, essa adverténcia do JavaCC nos informa que nossa gramética no é LL(1) e, portanto, nfo é adequada para esse tipo de AS Porém, a ultima linha da mensagem nos dé uma indieagéio de como resolver esse problema, sem ter que alterar nossa gramatica. O JavaCC nao consegue decidir qual caminho seguir olhando apenas um simbolo da entrada, pois ele pode ser 0 mesmo para vardecl ou methoddecl. Mas talvez, utilizando mais alguns simbolos da entrada, seja capaz de tomar a deciséio. Vamos verificar as construgdes que esto em conflito: * vardecl: inicia-se com o tipo da varidvel que pode ser um int, um string ou um identificador. Depois tem um identificador, que 6 0 nome de uma variével, e, depois, pode ter um abre colchete ou uma virgula, ou um ponto-e-virgula, ¢ dependendo desse terceiro s{mbolo, pode ter um fecha colchete, ou um identifi- cador, ou o fim da producio, respectivamente; © methoddecl: inicia-se com o tipo do método, depois pode ou nao ter uma seqiiéncia de abre ¢ fecha colchetes e, depois, tem um identificador que 6 o nome do método e, depois, 0 corpo do método, que se inicia com um abre parénteses. Assim, para saber se a entrada corresponde ou nao a um vardecl, precisamos olhar trés simbolos para frente em vez de apenas um. Se o primeiro sfmbolo for um iné, um CAPITULO 4- ANALISE SINTATICA 75 string ou um identificador, ficamos em divida entre os dois ndio-terminais em questao. Olhamos, entdo, o segundo, que se for um identificador, ainda pode ser qualquer um dos dois (um método que retorna um tipo sem dimenséo tem como segundo simbolo um identificador). Mas se olharmos 0 terceiro simbolo, veremos que para o caso da declaragiio de uma varidvel, ele deve ser um abre colchete, uma virgula ou um ponto- e-virgula, e que nenhum destes pode aparecer como terceiro simbolo da declaracaio de método. Para esta iiltima, poderfamos ter um fecha colchete ou um abre parénteses do inicio do n&o-terminal methodbody. Entéo, adicionaremos & nossa implementagdo o seguinte comando: Programa 4.9 void classboay(): ae oe + 5 af ° 6 [classlist ()] 7 (LOOKAHEAD(3) vardecl() )+ ® (constructdecl())* ° (methoddecl ())* 10 a * © LOOKAREAD(3) utilizado na frente do vardecl diz ao JavaCC que deve gerar um método que analisa trés sfmbolos a frente para decidir se deve ou nao tentar casar a entrada com um vardecl. E muito importante notar que o AS olha trés simbolos adiante e verifica se cles podem ser casados com o que esta aps o LOOKAHEAD, neste caso, o nfo-terminal vardecl. Se esses trés simbolos casarem, ele segue por esse caminho e se algum deles nfo casar, um caminho alternativo 6 tomado. Assim, © LOOKAHEAD deve ser utilizado sempre num ponto de decisio do AS. Além disso, devemos ser muito cuidadosos ao estipularmos 0 valor do LOOKAHEAD, pois se o ntimero de simbolos estipulados for menor que o necessério, podemos ter entradas casadas com produgées inadequadas, ¢ nenhum aviso do JavaCC, pois 0 uso do LOOKAHEAD desliga a verificagio de conflitos naquele ponto. Por exemplo, se na produgao anterior tivéssemos utilizado um LOOKAHEAD(2) ou mesmo um LOQKAHEAD (1), ¢ tivéssemos na entrada a seguinte cadeiz int 2() .. teriamos gerado um AS que iria tentar reconhecer essa cadeia como um vardecl. Iss0 porque o AS iria olhar os proximos dois (ou um, se uséssemos LOOKAHEAD (1)) simbolos que combinam com vardecl e isso seria o suficiente para decidir que esse seria 0 caminho correto para tentar reconhecer a entrada. Entao, ndo se engane se introduzir uum comando de LOOKAHEAD no arquivo {jj fizer com que uma adverténcia como aquela vista hé pouco deixe de ocorrer. Isso nfio garante que a decisdo que o implementador forcou o AS a tomar esteja correta. O JavaCC possui uma opcao global que é LOOKAHEAD = n, que determina que du- rante toda a andlise o nimero de tokens consultados para decidir sobre que produgio 76 COMO CONSTRUIR UM COMPILADOR. aplicar é igual a n. O valor-padrao 6 1, e quanto maior for esse valor, mais ineficiente seré.o AS. Assim, recomenda-se utilizar o valor 1 como lookahead global e utilizar comandos LOOKAHEAD nos pontos que forem necessérios. Os préximos nao-terminais sao simplesmente implementacao do que ja foi descrito na nossa gramatica e ndo devem representar problema para 0 leitor. Programa 4.10 1 void methodbody (): and. neh ak 5 paramlist() statement() mek © void paramlist(): o f wo a 12 C 8 ( | | ) ( )* 4 ( ( | | ) 6 ( ) + 16 »* wv 1 Wie No nao-terminal statement, encontramos novamente um comando LOOKAHEAD, em frente de vardecl. Nesse caso, vardecl entra em conflito com atribstat, pois ambos po- dem iniciar-se com um identificador. Mas 0 vardecl sempre tem, depois da declaraco do tipo, um outro identificador, 0 que ndo pode ocorrer no comando representado por airibstat. Assim, um lookahead de dois simbolos é suficiente para resolver 0 problema. Programa 4.11 void statement (): ae oF af 5 LOOKAHEAD (2) 6 vardecl () yt 8 atribstat() e | 20 printstat() athe 2 readstat() w | “ returnstat() a | 16 superstat() CAPITULO 4-ANALISE SINTATICA 77 18 ifstat() » | Pa forstat() and 2 statlist() =} By a 2» Os comandos da linguagem sao reconhecidos pelos nao-terminais seguintes: Programa 4.12 1 Void atribstatO: 2 ¢ cues at 8 Ivalue() ( alocexpression() | expression()) « } void printstas(): ot w oe a expression() » ts void readstat(): wf uv} wf 1 1value() 2 29 void returnstat(): a ft = a» ft e [expression()] a» > a1 void superstat(): ef nu} u ft s arglist() w } as void ifstat(): 78 COMO CONSTRUIR UM COMPILADOR. 5G ao (OF ot a expression() statement () 43 (LOOKAHEAD(1) statement ()] ee 4s void forstat(): ao f oo Bot 2s [atribstat()] sa [expression()] os [atribstat()] 5s statement () so} se void statlist() : oo ft co } a ¢ 2 statement() [statlist()] oo + «void Ivalue() : eo { o o oo ( ° expression() | n [ arglist() ] 2 » a } 7 void alocexpression() : w ft es w ft 7” ( 2 LOOKAHEAD(2) arglist() | n C | | ) 2 ( expression() )+ cy ) sat O no-terminal alocerpression possui um comando LOOKAHEAD cuja utilizagio 0 leitor pode facilmente identificar. Um caso diferente ocorre no nao-terminal ifstat. Se nio utilizarmos o comando LOOKAHEAD especificado naquele nao-terminal, obteremos a seguinte adverténcia ao processar 0 arquivo jj: CAPITULO 4 - ANALISE SINTATICA 79 Warning: Choice conflict in [...] construct at line 425, colum 5. Expansion nested within construct and expansion following construct have common prefixes, one of vhich is: "else" Consider using a lookahead of 2 or more for nested expansion. Isso acontece em virtude de a parte else do comando ser opcional. E depois de um comando if sem a parte else, podemos ter um else que pertence a um comando ‘if mais externo. Por exemplo, if (a--0) if (b == 0) asat4; else b=bt+1; Essa cadeia pode, de acordo com a definigao de éfstatement (sem 0 LOOKAHEAD), ser interpretada como um comando #f mais externo que no possui a parte else ¢ que tem interno a ele um outro comando éf. Esse comando interno possui, na parte “then”, uma atribuigio ¢ = a + 1 e, na parte else, uma outra atribuigio b = b + 4. Outra interpretacao 6 um comando externo que tem um comando interno if no ramo verdadeiro e um comando de atribuigéo b = ) + 4 na parte else. O comando if interno possui apenas uma atribuigio dentro dele, sem ter a parte else. Para tentar resolver esse conflito, poderfamos tentar aumentar o némero de tokens utilizados como lookahead. Porém, isso se mostraria imitil. O JavaCC nao consegue decidir que caminho tomar quando préximo token for um else, pois nao sabe se deve consumir 0 simbolo else dentro do comando #f ou pular toda a parte else do comando e casar a entrada com a estrutura que est4 mais externa ao ifstatement. Mas essa estrutura é a propria parte else de um outro ifstat mais externo. Assim, se tentarmos aumentar 0 ntimero de lookaheads, iremos casar esses simbolos sempre com as duas construgdes que esto em conflito (na verdade, a mesma construciio) ou com nenhuma delas. Isso significa que a decisio ser a de utilizar sempre a produgao do ifstat mais interno, pois 0 LOOKAHEAD induz a isso. Felizmente, esse é 0 comportamento que desejamos. Qu seja, o else estard sempre associado com 0 #f anterior a ele que estiver mais proximo. No exemplo, terfamos a situago mostrada na figura 4.1. Mais externamente, temos a chamada ao método correspondente ao statement, que, ao verificar 0 token corrente na cadeia de entrada, chama 0 método ifstatement. Este, por sua vez, consome os tokens if ¢ (, depois chama expression e, ao retornar daquela chamada, consome J. Em seguida, chama recursivamente 0 statement, que, por sua vez, chama de novo o ifstatement. Essa segunda chamada consome if e (, chama expression e consome ). Chama, entdo, statement que, olhando o lookahead, chama airibstat. Ao retornar do atribstat do statement, 0 método ifstatement verifica 0 token corrente, ¢ como é um else, ele consome esse token (ou seja, decidiu por nao pular a parte else” da produgao) e chama statement para a segunda atribuicdo. Esse € 0 comportamento desejado. Note, entao, que 0 comando LOOKAHEAD(1) serve para forcar o AS a consumir o else dentro do método ifstatement. 80 COMO CONSTRUIR UM COMPILADOR fase sousones if epresion consone: m=O nome: ra cnsome: i consone: == eT nar ensome: he arent ma Sonne bb Figura 4.1 - Chamadas do AS para ifs aninhados. E, para coneluir, temos as implementagdes dos ndo-terminais correspondentes 8s expressOes da nossa linguagem. Aqui também nao ha nada de importante a ser comen- tado, pois esses nfo-terminais sfio apenas uma reprodugdo das produgdes mostradas no capitulo 2 ¢ af comentadas. Programa 4.13 void expression) : { + { numexpr() [(
  • | | | | | ) numexpr()] } void numexpr(): { 3 { term() (( | ) term())* + void term(): < + * unaryexpr() (( | | ) unaryexpr())+ if 4.3 CAPITULO 4- ANALISE SINTATICA 81 void unaryexpr() : [( | )] factor() void factor(): { b £ © | | | Ivalue() | expression() ) i void arglist(): 4 } 4 [expression() ( expression())#] + Arquivos-fonte do compilador No diret6rio eap04/parser se encontra a nova versdo do nosso arquivo .jj que imple- menta a andlise sintatica do nosso compilador. Os comandos para, criar 0 AS s&0 0s mesmos mostrados no capitulo 3: cap04/parser$ javacc langX++.jj Java Compiler Compiler Version 2.1 (Parser Generator) Copyright (c) 1996-2001 Sun Microsystems, Inc. Copyright (c) 1997-2001 WebGain, Inc. (type "javacc" with no arguments for help) Reading from file langk++.jj . File File File File "TokenMgrError.java" does not exist. Will create one. "parseException. java" does not exist. Will create one. "Token. java" does not exist. Will create one. "SimpleCharStream.java" does not exist. Will create one. Parser generated successfully. cap048 javac parser/langX. java 82 COMO CONSTRUIR UM COMPILADOR Incluiu-se, também, no diretério sample um programa bintree-erro-sintatico que possui um versio do bintree com alguns erros sintaticos. Ao executarmos 0 nosso compilador para analisar esse arquivo, obtemos o seguinte resultado: cap04$ java parser.langX ../ssamples/bintree-erro-sintatico.x X++ Compiler - Version 1.0 - 2004 Reading from file ../ssamples/bintree-erro-sintatico.x .. . Encountered "mes" at line 21, column 4. Was expecting one of: “pn “0 ne mn 0 Lexical Errors found 1 Syntactic Errors found ‘A mensagem mostra-nos quais seriam os possiveis tokens esperados apés a atri- buig&o dia = d. Esquecemo-nos de colocar 0 ponto-e-virgula apés esse comando, 0 que originou 0 erro. Note que, apesar de conter diversos erros sintaticos, somente 0 primeiro é mostrado ao analisarmos 0 arquivo bintree-erro-sintatico. Isto porque 0 nosso AS nio se recupera do erro e termina sua execugéo. No capitulo 5 veremos como implementar recuperacao de erro no nosso compilador. Incluiu-se, também, no diretério sample o arquivo debugAS.x que contém um programa extremamente simples: class A { int a, b; int mQ t Aqui poderia haver um conflito, pois a declaragio de variéveis que inicia a classe poderia ser tomada com uma declaragio de varidvels ou como uma declaracao de método, se apenas os dois primeiros tokens fossem considerados para decidir qual producdo utilizar. Algo semelhante ocorre com a declaragéo do método m. Mas CAPITULO 4- ANALISE SINTATICA 83 © AS foi gerado com um lookahead de trés tokens nesse ponto, o que soluciona 0 conflito. Vamos executar 0 nosso compilador utilizando a op¢ao -debug_AS para esse programa. A safda obtida é a seguinte: cap04 java parser.langk -debug_AS ../ssamples/debugAS.x %++ Compiler - Version 1.0 - 2004 Reading from file ../ssamples/debugAS.x . . . all: program Call: classlist Call: classdecl Consumed token: <"class"> Consumed token: <: "A"> Call: classbody Consumed token: <"{"> Call: vardecl (LOOKING AHEAD...) Visited token: <"int">; Expected token: <"int"> Visited token: <: "a'>; Expected token: <> Visited token: <",">; Expected token: <"["> Visited token: <",">; Expected token: < Return: vardecl (LOOKAHEAD SUCCEEDED) Call: vardecl Consumed token: <"int"> Consumed token: <: Consumed token: <","> Consumed token: <: "b"> Return: vardecl Consumed token: <";"> Call: vardecl (LOOKING AHEAD...) Visited token: <"int">; Expected token: <"int"> Visited token: <: "m">; Expected token: <> Visited token: <"(">; Expected token: <"[!"'> Visited token: <"(">; Expected token: <","> Return: vardecl(LOOKAHEAD SUCCEEDED) Visited token: <"(">; Expected token: <";"> Call: methoddec] Consumed token: <"int"> Consumed token: <: "m"> Call: methodbody Consumed token: <"("> Call: paramlist Return: paramlist Consumed token: <")"'> Call: statement Call: vardecl (LOOKING AHEAD...) Visited token: <";">; Expected token: <"int"> Visited token: <";">3 Expected token: <"string"> Visited token: <";">; Expected token: <> Return: vardecl (LOOKAHEAD FAILED) ae Divers uum 84 COMO CONSTRUIR UM COMPILADOR Consumed token: <"5"> Return: statement Return: methodbody Return: methoddecl Consumed token: <"}"> Return: classbody Return: classdecl. Return: classlist Consumed token: <> Return: program 0 Lexical Errors found 0 Syntactic Errors found Veja que a saida mostra. quais si os métodos chamados quais so os tokens con- sumidos em cada um deles. Mostra, também, quando existe um comando LOOKAHEAD, quais so os tokens visitados na verificagao dos lookaheads. E 0 caso do comando an- tes de vardecl no método classdecl. Na primeira tentativa, esse comando LOOKAHEAD termina com éxito e decide-se por tentar analisar a entrada por meio de um var- decl. Ja no segundo caso, embora trés simbolos tenham sido visitados, o terceiro nao casa com 0 néo-terminal vardecl e, por isso, o casamento é tentado com a proxima possibilidade, que é methoddecl. Capitulo 5 Tratamento de Erros Sintaticos © compilador que temos até agora desempenha a simples tarefa de tentar reco- nhecer uma cadeia de acordo com uma gramatica e avisar 0 usudrio caso isso no seja possivel. Se a cadeia de entrada nao se enquadra em nenhuma producéo da gramé- tica, nosso AS mostra em que ponto da entrada nao foi possivel fazer o casamento e, entéio, termina sua execucao. Em geral, essa indicac&o serve para que 0 usuario possa identificar 0 erro sintAtico ocorrido, corrigir a entrada e analis4-la novamente. Esse processo pode ser entediante se 0 nfimero de erros sintaticos ¢ grande, pois a cada execugao do compilador, o usuério deve editar 0 arquivo de entrada, localizar © erro, cortigi-lo e reiniciar o proceso. Seria interessante se nosso compilador fosse capaz, de mostrar todos ou, pelo menos, diversos erros sintéticos de uma s6 vez. Assim, 0 ntimero de iteragSes compila-corrige seria reduzido. Para que isso acontega, 0 compilador deve ser capaz de recuperar-se do erro ocorrido e continuar na andlise sintdtica. Isso nem sempre é facil, pois enquanto a entrada casa com as produgées da gramética, é simples para o AS decidir quais produgdes devem ser tomadas para reconhecer a cadeia. Porém, quando um erro sintatico ocorre, torna-se mais dificil decidir como continuar no reconhecimento do restante da entrada. Infelizmente, o JavaCC nao oferece ao desenvolvedor muitos recursos para que seja implementada uma recuperagio de erros eficiente. O método que utilizaremos € que estudaremos aqui ¢ conhecido como método de ressincronizagio ou método do panico. Veremos inicialmente qual o conceito desse método e, ent&o, discutiremos sua implementago utilizando o JavaCC. 5.1 Ométodo de ressincronizagao Iniciaremos o estudo desse método com um exemplo. Vamos tomar a seguinte gra- matica: ———— a 86 COMO CONSTRUIR UM COMPILADOR. S + adcd A gh Queremos analisar a entrada agabed. Terfamos no nosso AS descendente recursivo: * uma chamada ao método $ que consome 0 token a da entradas © uma chamada ao método 4 que consome 0 9; © 0 método 4 tenta achar na entrada um h que ndo est af e, entéio, um erro sintatico ocorre. A idéia do método de ressincronizagao é fazer com que o método § nfo seja afetado por esse erro sintatico e que possa continuar a analisar a entrada. Porém, se 0 método 4 simplesmente emitir uma mensagem de erro e retornar a execucdo para 0 método que 0 chamou, um outro erro sintatico ira ocorrer, pois 0 método $ espera que depois da chamada a 4 exista na entrada o token c, 0 que néo ocorre, visto que ainda temos na entrada a cadeia abed. Por isso 0 método 4, ao detectar um erro sintético, deve ressincronizar a entrada com o nao-terminal que se espera reconhecer. Uma tentativa de se fazer isso € con- sumindo tokens da entrada até que apareca algum que possibilite a continuidade da andlise. Mas quais seriam esses tokens? Uma boa idéia seria utilizar o conjunto FOLLOW de A. Assim, 0 método A deve consumir tokens da entrada até que aparega um simbolo pertencente ao seu conjunto FOLLOW e s6 depois retornar a execugao para 5. Bm $, tudo se passa como se a execugio de A tivesse sido bem-sucedida ¢ a andlise continua, com a certeza de que, pelo menos, o pr6ximo token da entrada ird casar com a produgio sendo utilizada. Com essas modificagGes, 0 AS descendente recursivo ficaria algo como 0 c6digo mostrado no programa 5.1 on Programa 6.1 art 3 if (curToken.type == ’a’) ie tee . analex(); e AQs ' if (curToken.type == ’¢’) s if ° analex(); » if (curToken.type == °d’) un analex(); x else 1s SintaticError("Encontrado "+ curToken.type() + a " esperado: a); as } 0 else ” SintaticError("Encontrado " + curToken.type() + 1" " esperado: c"); 19 3 20 else CAPITULO 5 - TRATAMENTO DE ERROS SINTATICOS 87 Fa SintaticError("Encontrado " + curToken.type() + 22 " esperado: a"); 2 } as void AQ) a a try { 23 if (curToken.type == *g’) 20 t 20 analex() ; on if (curToken. type 2 analex(); 38 else SintaticError("Encontrado " + curToken.type + “ " esperado: h"); 38 } 28 else a SintaticError("Encontado " + curToken.type() + 28 esperado: g"); » } 40 catch (ASException e) “ { 2 System. err. printin(e.getNessage()); “ consume_until(?¢’); ae), ss } 46 41 void SintaticError (String s) wo f 40 throw new ASException(s) ; wo } m void consume_until(int t) e f s1 while (curToken.type != t) 5 analex(); od: Nessa nossa implementagio, o método Sintatickrror 6 responsdvel por langar uma excecao do tipo ASException. O método consume_until consome a entrada até que um determinado token seja encontrado. E 0 método 4 utiliza uma construgio try/catch para tentar reconhecer a producdo correspondente ao seu néo-terminal ¢, em caso de erro, ira chamar 0 método consume_until que faz a ressincronizacdo desejada. ‘Mas podemos fazer ainda um pouco melhor do que isso. Vamos tomar como exemplo a seguinte gramatica: S + aAcd| bAcf A = gh Nesse caso, FOLLOWA) = {c,e} e podemos utilizar esses dois tokens para efetuar a ressincronizagio. Porém, se estivermos utilizando a primeira produgdo de S, dese- 88 COMO CONSTRUIR UM COMPILADOR. jaremos utilizar o ¢ para fazer a recuperagio de erros, e nao o e. E se estivermos utilizando a segunda produgao de $, desejaremos utilizar o ¢ para fazer a recuperacao de erros, e ndo 0 ¢. Isso porque se fizermos a ressincronizago com o token errado, um novo erro sintatico surgiré em $. Assim, podemos alterar o nosso AS, permitindo que a cada chamada de 4 seja informado, por meio de um parametro, qual é 0 token de sincronizagio a ser utilizado. Essa alteragio 6 mostrada no programa 5.2 4 5 ° 19 26 20 22 4 3s ar 30 2 44 void 80 ees { if (curToken.type == a’) { eeetee( le ier; if (curToken.type 9c?) t analex(); if (curToken.type == °d”) analex(); else SintaticError("Encontado "+ curToken.type() + * esperado: a"); } else SintaticError("Encontado " + curToken.type() + wi ceparade=4cl) > else if (curToken.type *b?) { analex(); Ae) if (curToken.type == ’e”) { analex(); if (curToken.type == °£°) analex(); else SintaticError("Encontado “ + curToken.type() + " esperado: #"); 3 else Sintaticirror ("Encontado “+ curToken.type() + " esperado: e"); 3 alze + Sintatickrror("Encontado " + curToken.type() + " esperado: >"); void ACint i) 4 CAPITULO 5 - TRATAMENTO DE ERROS SINTATICOS 89. 48 try { os if (curToken.type == °g?) a £ 43 analex() ; “ if (curToken.type == 7h’) so analex(); st else SintaticError("Encontrado "+ cuxToken.type + w " esperado: 88 2 ot else a SintaticError("Encontado " + curToken.type() + 1 " esperado: g"); or Sf ss catch (ASException e) 38 % és System. err-printin(e.gettlessage()) ; a consume_until(k) ; o + os 3 es void SintaticError(String s) o { or throw new ASException(s) ; o yo void consume_until(int t) nf 2 while (curToken.type != t) cy analex(); A cada produg&o em $ corresponde uma chamada distinta de 4 e, por isso, ‘mos passar pardmetros distintos para recuperaco para cada uma delas. Note que, na verdade, ao tentarmos fazer a ressincronizagio, nem sempre teremos uma tinica pos- sibilidade, ou seja, nem sempre temos um ‘inico token que serve como sincronizador. Na maioria das vezes, utilizamos um conjunto de sincronizagao. Assim, os parame- tros que sio passados para os métodos 4 e consume_until deveriam ser conjuntos de tokens, e no um s6 token. Esse exemplo simples apresenta o conceito basico da técnica de recuperacao de eros que iremos utilizar com o JavaCC. Muitos pontos devem ainda ser abordados ¢ procuraremos fazé-lo a seguir, quando discutiremos a implementagdo da recuperacdo de erros sintaticos para o AS da linguagem X**. 5.2 Implementagao da recuperagao de erros A facilidade que 0 JavaCC nos oferece para tratarmos erros sintéticos ¢ bastante semelhante A abordagem mostrada nos exemplos. 0 JavaCC permite que as produgdes na BNF sejam colocadas entre construgdes try/catch ¢ caso algum erro sintatico seja detectado ao tentar casar a entrada com essas produgdes, podemos tratar esse erro.
  • También podría gustarte