Baixe o app para aproveitar ainda mais
Prévia do material em texto
UNIVERSIDADE TÉCNICA DE LISBOA INSTITUTO SUPERIOR TÉCNICO COMPILADORES: da teoria à prática Pedro Reis dos Santos Departamento de Engenharia Informática (Fevereiro, 2006) 18 CAPÍTULO 1. INTRODUÇÃO � � � �HHHHHHHH - PPPPPq - interpretadorprograma resultados dados - ? - ? " # ! dados resultados compiladorexecutávelprograma 1.2 Linguagens e gramáticas Foram os matemáticos Axel Thue, Emil Post e Stephen Kleene, entre outros, que come- çaram a estudar as propriedades matemáticas das cadeias de caracteres e sequências destas. No entanto, foi Noam Chomsky que, em 1956, que procurou caracterizar de uma forma precisa a estrutura das linguagens naturais. O seu objectivo consistia em utilizar regras matemáticas precisas para especificar a sintaxe das linguagens. Com a divulgação dos computadores, nos anos seguintes, veio-se a verificar que as linguagens a utilizar em computadores podiam ser especificadas por um dos modelos gramati- cais identificados por Chomsky. Estas linguagens, designadas por livres de contexto, podem ser linguagens de programação, podem ser linguagens de descrição de dados, como imagens ou som, ou outro tipo de processos. Nesta secção abordaremos as propriedades matemáticas fundamentais das linguagens e dos sistemas usados para as gerar, tais como as gramáticas. As linguagens de pro- gramação, desde o Fortran, podem ser especificadas de uma forma precisa através de uma gramática. Além disso, a gramática permite escrever programas, designados por analisadores sintácticos, que permitem determinar se a cadeia de caracteres está sin- tacticamente correcta de acordo com a linguagem. Apesar dos avanços efectuados nos últimos anos, a análise precisa de linguagens naturais, como o português ou o inglês, 1.2. LINGUAGENS E GRAMÁTICAS 19 ainda é limitada. O problema reside, principalmente, na inexistência de um acordo so- bre quais as frases sintacticamente correctas, nem foi ainda proposta uma gramática suficientemente precisa e consensual. Nesta secção apresentaremos alguns sistemas formais que permitem definir famílias de linguagens utilizadas frequentemente em computação. A nossa atenção principal orienta-se para as linguagens livres de contexto, pois são as mais usadas para descrever a sintaxe das linguagens de computação. 1.2.1 Terminologia Antes de introduzir a especificação formal de linguagens apresentam-se algumas defi- nições e terminologia. alfabeto Um alfabeto, designado por Σ, é um conjunto finito não vazio de símbolos indivisíveis. Por exemplo, a lingua inglesa contém as 26 letras alfabéticas e alguns sinais de pontuação. A lingua portuguesa usa apenas 23 letras, mas inclui certos símbolos de acentuação para algumas letras. O alfabeto ASCII ( American Standard Code for Infor- mation Interchange ) utiliza 128 símbolos, sendo utilizado na maioria das comunicações entre computadores. O alfabeto UNICODE, mais recente que o ASCII, pretende supor- tar os símbolos de todas as linguagens utilizadas no planeta e possui, no momento da escrita deste documento, mais de 38 mil símbolos. No entanto, os aspectos mais impor- tantes das linguagens formais podem ser modelados com apenas duas letras. cadeia Uma cadeia de um alfabeto Σ é uma sequência finita de símbolos de Σ. O número de símbolos na cadeia x é designada por comprimento da cadeia e designado por |x|. A cadeia vazia, de comprimento 0 ( zero ), é designada por ε. conjuntos de cadeias O conjunto de todas as possíveis cadeias de um alfabeto desig- naremos por Σ∗, enquanto Σ+ designa todas as cadeias não vazias e ∅ o conjunto vazio. linguagem Uma linguagem sobre um alfabeto Σ é o conjunto de cadeias consideradas válidas sobre esse alfabeto. Os membros da linguagem são também designados por frases da linguagem. Uma vez que as linguagens são apenas conjuntos é aceitável aplicar as operações tra- dicionais, como a união, intersecção e complemento. Além disso, existem mais duas operações base sobre linguagens: a concatenação e o fecho de Kleene. 20 CAPÍTULO 1. INTRODUÇÃO concatenação A concatenação de duas linguagens L1 e L2 sobre um alfabeto Σ é o conjunto {xy|x ∈ L1, y ∈ L2}. Onde x = a1a2 . . . an e y = b1b2 . . . bn são cadeias do mesmo alfabeto e a sua concatenação, designada por xy, é a cadeia a1a2 . . . anb1b2 . . . bn. fecho de Kleene Seja L0 = {ε} e Li = LLi para i ≥ 1, designa-se por fecho de Kleene de L, representado por L∗, a linguagem L∗ = ⋃ i≥0 L i. O fecho transitivo de L, repre- sentado por L+, é a linguagem L+ = ⋃ i≥1 L i. Por outras palavras, o fecho de Kleene da linguagem L consiste em todas as cadeias que podem ser formadas pela concatenação de zero ou mais frases de L. 1.2.2 Representação de linguagens Uma linguagem é, em geral, um subconjunto de Σ∗ sobre alfabeto Σ. Enquanto uma linguagem finita pode ser definida através da enumeração dos seus elementos, uma linguagem infinita não pode ser exaustivamente enumerada. Algumas linguagens in- finitas podem ser definidas por regras que caracterizam todos os seus elementos. Uma larga gama de linguagens pode ser caracterizada por regras definidas por métodos sis- temáticos. De entre estes métodos, as gramáticas são o formalismomais frequentemente utilizado. As gramáticas formais foram introduzidas em 1943 por Emil Post, que se baseou no trabalho de Axel Thue e outros. No entanto, foi Noam Chomsky em 1956 que estudou a sua utilização rigorosa na descrição formal e natural das linguagens. O sistema mais genérico e útil para representar linguagens é baseado na noção formal de gramática. Uma gramática é um quádruplo (Σ, V, S, P ), onde 1. Σ é um conjunto finito não vazio designado por alfabeto terminal, onde cada ele- mento é designado por símbolo terminal. 2. V é um conjunto finito não vazio disjunto de Σ, cujos elementos são designados por símbolos não terminais. 3. S ∈ V é um símbolo não terminal específico designado por símbolo inicial. 4. P é um conjunto finito de regras ( ou produções ) da forma α → β onde α ∈ (Σ ⋃ V )∗V (Σ ⋃ V )∗ e β ∈ (Σ ⋃ V ), ou seja, α é uma cadeia de terminais e não ter- minais contendo pelo menos um não terminal e β é uma cadeia de terminais e não terminais. 1.2. LINGUAGENS E GRAMÁTICAS 21 Exemplo 1.1 A gramática G = ({0, 1, 2}, {S,A}, S, P ), onde P é constituído pelas regras S → 0 S A 2 S → ε 2 A → A 2 0 A → 0 1 1 A → 1 1 descreve as frases {0n1n2n, n ≥ 0}. Hierarquia de gramáticas As gramáticas podem ser divididas em quatro classes de restrições gradualmente cres- centes à forma das regras. Esta classificação é designada por hierarquia de Chomsky. Seja uma gramática G = (Σ, V, S, P ) 1. G é também designada gramática tipo-0 ou gramática sem restrições. 2. G é tipo-1 ou gramática dependente do contexto se cada regra α → β de P obe- dece a |α| ≤ |β|. Uma gramática ainda é do tipo-1 se tiver uma regra S → ε, desde que S não surja no lado direito de nenhuma regra. 3. G é tipo-2 ou gramática livre do contexto se cada regra α → β de P obedece a |α| = 1, ou seja, α é constituído por um só não terminal. 4. G é tipo-3 ou gramática regular se cada regra tiver uma das formasA→ cB,A→ c ou A→ ε, onde A e B são não terminais ( podendo B = A ) e c é um terminal. Cada classe de linguagens do tipo-i contém a classe de linguagens do tipo-i+1, para i = 0, 1, 2. Contudo, as classes de linguagens identificadas por Chomsky não reflectem apenas as propriedades formais das mesmas mas também traduzem as características fundamen- tais da sua computação. De facto, cada classe de gramática é processável por uma classe de ferramentas, máquinas de Turing processam gramáticas sem restrições. autómatos linearmente limitados processam gramáticas dependentes do contexto. autómatos de pilha processam gramáticas livre do contexto. 22 CAPÍTULO 1. INTRODUÇÃO autómatos finitosprocessam gramáticas regulares. Destes apenas trataremos os autómatos finitos e autómatos de pilha, bem como os ana- lisadores que realizam esses autómatos. Propriedades das gramáticas fecho Uma classe de linguagens diz-se fechada numa operação particular se cada apli- cação da operação nas linguagens da classe ainda é uma linguagem dessa classe. As operações designam operações de união, intersecção, complemento, concatenação e fe- cho de Kleene. As propriedades de fecho são úteis na construção de novas linguagens a partir de linguagens existentes e para provar muitas propriedades teóricas das lingua- gens e gramáticas. As gramáticas sem restrições só não são fechadas pela operação de complemento. As gramáticas livres de contexto não são fechadas pelas operações de complemento e inter- secção. Finalmente, as linguagens regulares e as linguagens dependentes do contexto são fechadas em todas as cinco operações. Capítulo 2 Linguagens regulares As linguagens regulares, designadas por tipo-3 na hierarquia de Chomsky ( ver 1.2.2 ), são linguagens muito simples. Assim, a maioria das linguagens utilizadas em computa- ção não são regulares, ou seja, não podem ser completamente descritas por gramáticas regulares. No entanto, como o seu processamento é simples e eficiente, as gramáticas re- gulares são frequentemente utilizadas para processar partes, por vezes significativas, de linguagens mais complexas. Desta separação do processamento da linguagem por uma gramática regular e por outra gramática, em geral livre de contexto, resulta uma maior eficiência em termos de espaço ocupado pelo analisador e do tempo gasto na análise das descrições. Resulta, ainda, uma simplificação da gramática livre de contexto, facilitando a sua descrição, realização e execução. O processamento da parte regular de uma linguagem designa-se por análise lexical. A análise lexical tem a vantagem de ser um processo que podendo ser formalmente des- crito através de expressões regulares pode produzir uma rotina que realiza essa análise. Essa rotina modela um autómato finito derivado matematicamente das expressões re- gulares especificadas. A análise lexical é utilizada essencialmente na categorização dos elementos de uma lin- guagem em classes de símbolos, em vez de caracteres individuais. Nomeadamente, permite separar nomes em palavra reservadas, identificadores e literais, bem como clas- sificar outros símbolos como terminadores, separadores e operadores. Permite ainda encapsular dependências do sistema operativo, como por exemplo, o carácter ou carac- teres de mudança de linha. Localiza a manipulação dos diferentes formatos de repre- sentação de caracteres, como o ASCII, UNICODE ou, no caso da lingua portuguesa, o ISO-LATIN-15 ou a página 860. Esconde do restante compilador elementos como co- mentários e caracteres brancos ( em geral designam o espaço e o tabulador horizontal ). Realiza a substituição de macros simples. Em resumo, facilita o processamento transfor- mando uma linguagem descrita por uma sequência de caracteres numa sequência de 25 26 CAPÍTULO 2. LINGUAGENS REGULARES elementos lexicais, designados por tokens. A restante análise da linguagem pode assim ser baseada em elementos categorizados. 2.1 Expressões regulares Expressões regulares foram introduzidas em 1956 por Stephen Kleene para o estudo das propriedades das redes neuronais. As linguagens representadas por expressões re- gulares são designadas por linguagens regulares. As expressões regulares apresentam representações das linguagens que são frequentemente claras e concisas, contudo, mui- tas linguagens não são regulares. Uma expressão regular sobre um alfabeto Σ, e a linguagem regular que a expressão regular representa, pode ser definida por: 1. o símbolo ∅ é uma expressão regular e representa a linguagem vazia. 2. o símbolo ε é uma expressão regular e representa a linguagem cujo único membro é a cadeia vazia, ou seja, {ε}. 3. para cada c ∈ Σ, c é uma expressão regular e representa a linguagem {c}, onde o único membro é a cadeia de um só carácter c. 4. se r e s são expressões regulares e representam as linguagensR e S, então (r|s), (rs) e (r∗) são expressões regulares que representam R ⋃ S, RS e R∗, respectivamente. Exemplo 2.1 A expressão regular ((0(0|1)∗)|((0|1) ∗ 0)) sobre o alfabeto {0, 1} representa a linguagem regular que consiste em todas as cadeias de digitos binários que começam ou terminam com 0. 2.1.1 Operadores das expressões regulares De acordo com a definição acima, as expressões regulares são construídas por operado- res com determinadas propriedades. A esses operadores atribuiremos prioridades por forma a simplificar a escrita das expressões regulares. Considerando as propriedades algébricas dos operadores base é possível introduzir identidades algébricas para expres- sões regulares por forma a construir expressões equivalentes. Duas ou mais expressões regulares que representam a mesma linguagem são designadas por equivalentes. 2.1. EXPRESSÕES REGULARES 27 Uma expressão regular pode ser formada pelos seguintes operadores, considerando duas expressões regulares p, q: união: designado por p|q é comutativo, associativo e idempotente ( p|p = p ), represen- tando a união das expressões regulares originais. ∅ é o elemento neutro da união. concatenação: designado por p q é associativo e distributivo em realção à escolha e mais prioritário que a escolha. ε é elemento neutro e ∅ é o elemento absorvente na concatenação. fecho de Kleene: designado por p∗ é idempotente ( p ∗ ∗ = p∗ ) e mais prioritário que a concatenação. Representa o conjunto de frases constituídas por zero ou mais repetições das frases de p. Além disso p∗ = (p|ε)∗. parênteses: permite alterar a prioridade dos operadores. Exemplo 2.2 Atendendo às prioridades dos operadores, a expressão regular a|bca∗ sobre o alfa- beto {a, b, c} corresponde a a|(b(c(a∗))). Além dos operadores base é ainda usual utilizar outros operadores, construídos a partir dos anteriores, que simplificam a descrição das linguagens através de expressões regu- lares. opção: designado por p? designa zero ou uma ocorrências de p, ou seja, p|ε. fecho transitivo: designado por p+ designa uma ou mais ocorrências de p. p+ é equivalente a p p∗, e p∗ é equivalente a p+ |ε. parênteses rectos: designado por [pq] ou [p − q] designam p|q e os símbolos de ordem entre p e q (inclusivé), respectivamente. A ordem depende da representação utilizada ( ASCII, UNICODE, EBCDIC, etc. ) pelo que apenas se garante a ordem das letras maiúsculas [A − Z], minúsculas [a− z] e digitos decimais [0− 9]. complemento: designado por [ˆpq] ou [ˆp−q] designam todos os símbolos deΣ excepto p|q e todos os símbolos de Σ excepto os símbolos de ordem entre p e q (inclusivé), respectivamente. 28 CAPÍTULO 2. LINGUAGENS REGULARES 2.1.2 Gramáticas regulares Uma linguagem regular é descrita por uma gramática regular. Tal como vimos em 1.2.2 uma gramática regular tem apenas regras da forma A→ c,A→ ε, eA→ cB ouA→ Bc, onde A e B são não terminais ( podendo B = A ) e c é um terminal. A gramática diz-se linear à direita se usar regras da forma A → cB ou linear à esquerda se usar regras da forma A → Bc, não podendo usar ambas as formas simultaneamente. Além disso, a regra A→ c pode ser deduzida por substituição da regra B → ε numa das outras. Se L tem uma gramática regular, então L é uma expressão regular. 1 Se L é uma ex- pressão regular, então L é gerado por alguma gramática linear à esquerda e por alguma gramática linear à direita. 2 Em resumo, gramáticas regulares caracterizam expressões regulares, pois uma linguagem é regular se e só se tiver uma gramática linear à es- querda e se e só se tiver uma gramática linear à direita. Ou seja, gramáticas regulares e expressões regulares são representações equivalentes de linguagens regulares. 2.1.3 Propriedades das expressõesregulares As expressões regulares são apenas uma forma mais concisa de descrever linguagens regulares que as gramáticas regulares. Como as gramáticas regulares são fechadas nas 5 operações base ( ver 1.2.2 ) pode-se combinar livremente as expressões regulares que ainda se tem uma expressão regular. Como uma expressão regular é fechada na con- catenação qualquer técnica aplicada às expressões regulares a e b pode ser aplicada a ab. Como é fechada na união, o que se pode fazer a uma expressão regular a1|a2|...|an pode-se fazer individualmente a a1, a a2 etc. Como é fechada no fecho de Kleene pode- se escrever regras concisas sem indicar o limite, por exemplo, criar expressões regulares para padrões de dimensão finita mas arbitrária. Nomeadamente, os identificadores não têm número limite de caracteres em muitas linguagens. 2.2 Autómatos finitos A modelação de linguagens regulares, quer tenham sido especificadas por expressões regulares como por gramáticas regulares, pode ser efectuada por autómatos finitos. Da mesma forma, uma linguagem reconhecida por um autómato finito é uma linguagem 1teorema 9.1 de [HU79] 2teorema 9.2 de [HU79] 2.2. AUTÓMATOS FINITOS 29 regular. Existem diversos algoritmos, representados pelas setas da figura seguinte, que permitem converter expressões regulares de e para diversos tipos de autómatos finitos, cada um com o seu campo de aplicação. - ? ff HH HH HH HH HH HY ~Z Z Z Z ZZ} � � � � �� Thompson subconjuntos Hopcroft autómato finitofinito det. minimizado regulares expressões autómato autómato finito não determinista determinista Kleene gramática regular Nas secções seguintes abordaremos esses autómatos e os algoritmos que permitem obtê- los. Os autómatos finitos são constituídos por um conjunto de estados e por transições di- rigidas e etiquetadas entre esses estados. As etiquetas são símbolos da gramática ou ε. Um desses estados é designado por inicial, podendo ter um mais estados finais. O autómato finito pode ser representado graficamente ou por tabela. 2.2.1 Diagrama de transição Um autómato finito pode ser presentado graficamente por um diagrama de transição, ou seja, um quádruplo D = (S, δ, SF , q0) onde: • S é um conjunto de estados, representados por círculos. • δ(qi, x) = qj , onde qi ∈ S, qj ∈ S e x ∈ Σ ∪ {ε}, são transições entre estados representadas por setas etiquetadas por um dos símbolos de entrada ou ε. • SF ∈ S e #S ≥ 1, é um conjunto constituído por um ou mais estados finais, representados por círculos duplos concêntricos. • q0 ∈ S é um estado inicial, indicado por uma seta sem origem em outro estado. 30 CAPÍTULO 2. LINGUAGENS REGULARES O reconhecimento de uma frase pode ser efectuado por simulação da rede, começando o processamento no estado inicial. Se no fim do processamento nos encontrarmos num estado final a frase está correcta e faz parte da linguagem em questão. Se o estado corrente, no fim do processamento, não for um estado final devem-se procurar outros caminhos. Exemplo 2.3 A expressão regular (a|b) ∗ abb, que designa as frases do alfabeto Σ = {′a′,′ b′} que terminam em abb, pode ser representada por �� ff� I �� ff� I �� ff� I�� ff� ffifl �fi - -- - ll ##A AU � � bb� �� 0 ’b’ 1 I 2 3 ’b’ ’a’ ’b’ ’a’ 2.2.2 Tabela de transição Uma outra representação, menos intuitiva mas mais útil do ponto de vista computaci- onal, é a tabela de transição. Na tabela de transição os estados representam uma das dimensões da tabela bidimensional e as etiquetas a outra dimensão. Cada posição da tabela representa os estados que podem ser atingidos a partir desse estado com esse símbolo ou etiqueta. Os estados finais são representados dentro de caixas e o estado inicial assume-se ser o primeiro estado da tabela. Exemplo 2.4 A expressão regular do exemplo 2.3, pode ser representada por estado ’a’ ’b’ 0 {0, 1} {0} 1 ∅ {2} 2 ∅ {3} 0 ∅ ∅ 2.2.3 Autómato finito não determinista Um autómato finito não determista é um quintuplo (S,Σ ∪ {ε}, δ, SF , s0), onde S são os estados, Σ o alfabeto, uma função de transição δ, um estado inicial s0 ∈ S e um conjunto de estados finais SF ⊆ S. A função de transição δ : S × Σ 7→ 2S faz corresponder a cada par estado e símbolo de entrada um conjunto, eventualmente vazio, de estados. Onde o conjunto de todos os subconjuntos de S, designado por powerset, é representado por 2S. 2.2. AUTÓMATOS FINITOS 31 A utilização de transições vazias, que não consomem nenhum símbolo da sequência de entrada, facilita a construção do autómato, mas existe sempre um autómato não deter- minista equivalente sem transições vazias 3. Quer o exemplo 2.3 como o exemplo 2.4 são duas representações de um autómato finito não determinista. Para descobrir se uma frase, ou sequência de entrada, faz parte da linguagem é neces- sário procurar um caminho entre o estado inicial e um dos estados finais do autómato. A procura pode ser efectuada de diversas formas, como por exemplo em profundidade e em largura. Se a procura for em profundidade segue-se um caminho possível e, caso não seja atingido um estado terminal, recua-se e tenta-se outra possível solução. Na procura em largura vão sendo simultaneamente consideradas as diversas opções em cada estado. Computacionalmente a solução de procura em profundidade parece mais simples de realizar mas a sua execução é pesada. Algoritmo de Thompson Existe sempre um autómato não determinista, com transições vazias, que aceita a lin- guagem descrita por uma expressão regular 4. O algoritmo de Thompson não é mais que a demonstração da afirmação anterior. Para tal começamos pela três expressões com que definimos as expressões regulares ( ver 2.1 ), ffifl �fi �� ff� - q 0 �� ff� - q 0�� ff� - q 0 ffifl �fi �� ff� ffifl �fi �� ff� -q qf f ’a’ r = ε r = ∅ r = a como as expressões regulares são fechadas nas operações base ( ver 1.2.2 ), temos que a operação de união de duas expressões regulares r e s é também a união das linguagens descritas por cada uma destas expressões regulares L(r|s) = L(r) ∪ L(s), �� ff� - q 0 �� ff� q 2 �� ff� 2f �� ff� 1f�� ff� q 1 ffifl �fi �� ff� Z Z Z~ � ��3 Q QQs� � �> 0f ε ε ε ε L(s) L(r) 3teorema 2.2 de [HU79] 4teorema 2.3 de [HU79] 32 CAPÍTULO 2. LINGUAGENS REGULARES a operação de concatenação de duas expressões regulares r e s é a concatenação das linguagens descritas por cada uma destas expressões regulares L(rs) = L(r)L(s), �� ff� q 2 �� ff� 2f�� ff� 1f�� ff� q 1 ffifl �fi - -ε L(r) L(s) na prática, o estados f1 e q2 podem ser fundidos num só, poupando um estado e elimi- nando a transição vazia. O fecho de Kleene de uma expressão regular s é o fecho de Kleene da linguagem descrita pela expressão regular L(s∗) = L(s)∗, �� ff� - q 0 ffifl �fi �� ff� 0f�� ff� 1f�� ff� q 1 -- 6 ? ε ε L(s) ε ε Notar o sentido das setas nas transições vazias. A construção de uma expressão regular complexa resume-se à sucessiva aplicação das diversas ocorrências das operações por ordem decrescente de prioridade. Exemplo 2.5 A expressão regular do exemplo 2.3 é representada pelo autómato finito não deter- minista da figura, construído pelo algoritmo de thompson, �� ff� �� ff��� ff� �� ff� �� ff� �� ff��� ff� �� ff� �� ff� �� ff� �� ff� �� ff� ffifl �fi �� ff� �� ff� - - ��* - - HHj Z Z~ � �> - - ? ff ? 6 ff ff ff 10 3 2 4 5 6 7 8 910 a b ε ε ε ε ε ε ε ε 13 b 12 ε 11 b ε ε a 2.2. AUTÓMATOS FINITOS 33 2.2.4 Autómato finito determinista Um autómato finito determista é um quintuplo (S,Σ, δ, SF , s0), onde S são os estados, Σ o alfabeto, uma função de transição δ, um estado inicial s0 ∈ S e um conjuntode estados finais iSF ⊆ S. A função de transição δ : S × Σ 7→ S faz corresponder a cada par estado e símbolo de entrada um conjunto, eventualmente vazio, de estados. Num autómato finito não determinista as transições a partir de cada estado são disjun- tas e não vazias. Ou seja, não podem existir transições vazias e, no máximo, só pode haver um transição para cada símbolo a partir de um estado. Do ponto de vista da tabela de transição, a restrição introduzida pelos autómatos fini- tos determinista, implica que cada posição da tabela só contém um único estado. Se cada estado for representado pelo seu número, basta representar o conjunto vazio por um número inválido, correspondente a uma situação de erro, por exemplo -1. Assim, cada expressão regular é representada por uma tabela de transição, a que teremos de acrescentar uma coluna para indicar os estados finais. Por outro lado, como o autómato é determinista, não existe necessidade de tentar di- versas transições para encontrar uma solução válida. De facto, ou existe uma transição válida ou a sequência de entrada não obedece à expressão regular que originou a tabela. Desta forma, por cada carácter lido da sequência de entrada, o analisador executa um passo, consultando a tabela que indica o estado seguinte. Ou seja, em linguagem C, algo do tipo, int lexical(int tabela[][256], int final[]) { int ch, estado = 0; while ((ch = getchar()) != EOF) if ((estado = tabela[estado][ch]) == EOF) return FALSE; return final[estado]; } onde a tabela suporta todos os 256 caracteres que podem ser devolvidos pela função de leitura getchar() e EOF representa quer o fim da sequência de entrada, quer o estado de erro na tabela. A tabela final tem tantos elementos quantos os estados, ou seja o número de linhas da tabela, indicando se cada estado é final ou não. 34 CAPÍTULO 2. LINGUAGENS REGULARES 2.2.5 Conversão por construção de subconjuntos Se uma linguagem é aceite por um autómato finito não determinista então existe um autómato finito determinista que aceita essa mesma linguagem. 5 O algoritmo de cons- trução de subconjuntos permite obter um autómato finitos determinista a partir de um não determinista, através de uma procura em largura. A construção de subconjuntos inicia-se no estado inicial e, a partir deste estado, calcula- se todos os restantes estados que se podem atingir a partir deste estado apenas através de transições vazia. Este conjunto de estados, incluindo o estado inicial, é designado por fecho − ε({q0}) e passa a representar o estado inicial do autómato finito determi- nista, que designaremos por I0. A partir de cada um dos estados que contituem I0 vamos determinar quais os estados que são atingidos através de uma só transição para cada um dos símbolos do alfabetoΣ. Estes novos conjuntos, designados pormove(I0, a), onde a ∈ Σ, formam o núcleo dos novos estados deterministas. O estado determinista é completamente calculado através do fecho − ε(move(I0, a)), para cada símbolo. No entanto, se o núcleo de um novo estado é igual ao núcleo de um estado já existente, não é necessário calcular o seu fecho, pois trata-se de uma repetição do estado anterior, que identificaremos colocando o estado entre parênteses e colocaremos ... para designar os restantes estados do fecho. O processo termina quando não houver mais estados deter- ministas por calcular. Os estados deterministas considerados finais são todos aqueles que contenham pelo menos um estado final do autómato não determinista, que repre- sentaremos colocando um quadrado à volta do estado. Para construir os subconjuntos utilizaremos uma tabela com 5 colunas, que representam respectivamente: o estado determinista a calcular, o símbolo de entrada a considerar, o núcleo não determinista que obtém, o restante fecho − ε e, finalmente, o novo estado determinista. Notar que o estado determinista indicado na última coluna é constituído pelos estados não deterministas das duas colunas anteriores. Exemplo 2.6 A tabela de construção de subconjuntos da expressão regular do exemplo 2.3 re- presentada pelo autómato finito não determinista do exemplo 2.5 é 5teorema 2.1 de [HU79] 2.2. AUTÓMATOS FINITOS 35 estado entrada move fecho− ε\move novo estado 0 1, 2, 3, 7, 8 I0 I0 a 4, 9 1, 2, 3, 6, 7, 8, 10 I1 b 5 1, 2, 3, 6, 7, 8 I2 I1 a 4, 9 . . . (I1) b 5, 11 1, 2, 3, 6, 7, 8, 12 I3 I2 a 4, 9 . . . (I1) b 5 . . . (I2) I3 a 4, 9 . . . (I1) b 5, 13 1, 2, 3, 6, 7, 8 I4 I4 a 4, 9 . . . (I1) b 5 . . . (I2) A partir da tabela, e eliminando as duas colunas referentes aos estados do autómato não determinista, ficamos com as transições do autómato determinista. O estado da primeira coluna tem uma transição etiquetada pelo símbolo da segunda coluna para o estado da última coluna. A partir desta informação é possível construir a tabela de transição ou a representação gráfica. Exemplo 2.7 A tabela do exemplo 2.6 permite determinar o diagrama de transição, de onde eliminámos o prefixo I pois já não existe confusão com os estados não deterministas, ffifl �fi �� ff� �� ff� �� ff� �� ff� �� ff� - - - - ?��/ SSo ff ��7 - J J^ 0 3 4 2 b b b a bb a a a a 1 A correspondente tabela de transições pode ser igualmente deduzida da tabela do exemplo 2.6 ou do diagrama de transição acima, estado a b 0 1 2 1 1 3 2 1 2 3 1 4 4 1 2 36 CAPÍTULO 2. LINGUAGENS REGULARES A representação computacional da tabela é directa, contendo apenas duas colunas pois o alfabeto tem apenas dois elementos. 2.2.6 Minimização da tabela O número de estados do autómato finito determinista obtido através da construção de subconjuntos não é, necessariamente, mínimo. De facto, é frequente encontrar estados absolutamente equivalentes a outros, podendo ser fundidos num só estado. O número destes estados pode ser significativo para tabelas que representam linguagens regulares de interesse prático. O algoritmo de Hopcroft permite determinar os estados equivalentes a partir de suces- sivas partições dos estados calculados. A partição inicial é constituída por três grupos, um contendo os estados finais, outra os estados não finais e uma última contendo o estado de erro, correspondente ao conjunto vazio. De facto, mesmo que existissem esta- dos equivalentes entre esses grupos a sua fusão implicaria a perda da informação sobre se a sequência era aceite ( estado final ) ou não. O algoritmo consiste em fragmentar os grupos iniciais em outros subgrupos, se os res- pectivos estados não poderem ficar juntos. Para que os estado possam permanecer num mesmo grupo é necessário que, qualquer que seja o símbolo de entrada, todos esses es- tados transitem para estados que, também eles, pertençam a um mesmo grupo. Como em cada nova interação, que se designa por partição, vão sendo criados novos grupos, estados que antes pertenciam ao mesmo grupo podem não poder continuar juntos. O processo repete-se até que não sejam criados novos subgrupos. Notar que os grupos criados podem conter vários estados, desde que estes contenham todos transições para os mesmos estados da partição anterior. Também não se deve assumir que apenas estados com entradas iguais na tabela é que podem ser agrupados, podendo ter transições para estados distintos de um mesmo grupo para cada símbolo. Exemplo 2.8 A tabela do exemplo 2.7 não tem estado de erro pelo que a partição inicial contém apenas dois grupos. Para mais, todos os estados transitam para o estado 1 com o símbolo de entrada a. Desta forma, e neste caso, apenas o símbolo b pode forçar a separação dos estados. Dos quatro estados não terminais, apenas o estado 3 transita com b para um grupo diferente, ou seja o grupo dos estados finais. Assim, o estado 3 deve ser separado dos restantes passando a haver 3 grupos na nova partição. Agora é o estado 1 que transita também com b para umgrupo distinto dos outros dois estados, pois na nova partição o estado 3 já está separado. 2.2. AUTÓMATOS FINITOS 37 ffifl �fi �� ff� 4 ffifl �fi �� ff� 4 ffifl �fi �� ff� 4 �� ff� �� ff� �� ff� � � � � � � � � ? 6 - ? 6 ��7 - @ @ - ? ZZ} @ @ ff � � � � � � � � � � � � � � � � 6 ��7 ��7 1 3 0, 20, 1, 2 3 b a b b aa ab b a,b 0, 1, 2, 3 aa,b P PP0 1 2 a a Notar que os estados 0, 1, 2, 3 da partição P0 transitam para eles próprio com o símbolo a, mas divergem quanto às transições com o símbolo b. Conseqeuntemente, na partição P1 o estado 3 é separado pois é o único que transita para fora do grupo com o símbolo b. No entanto, os três estados restantes ainda não concordam quanto à transição com o símbolo b, pois com a saída do estado 3 do grupo apareceram novas inconsistências. Desta forma, na partição P2 o estado 1 é igualmente isolado, já sendo possível representar todas as transições do autómato inicial. Assim, apenas os estados 0 e 2 podem ser agrupados. Graficamente podemos representar as sucessivas partições, de uma forma mais simples, por uma árvore, � � �� @ @ @R - S S SSw -PPPPPPq - -- 44 3 1 3 4 0, 2 0P P P1 2 0, 1, 2, 3 0, 1, 2 A partir do autómato finito determinista, na sua representação gráfica ou por tabela, pode-se obter directamente uma gramática regular. Para tal basta considerar os tipos de regras apresentadas em 2.1.2, transformando cada transição δ(qi, a) = qj numa regra Si → aSj e acrescentando para cada estado final qf uma regra Sf → ε. 38 CAPÍTULO 2. LINGUAGENS REGULARES Exemplo 2.9 Considerando a tabela, já minimizada, do exemplo 2.7, obtém-se directamente a gramática regular correspondente, que representaremos com 4 símbolos não terminais cujo índice reproduz o número do estado do autómato, estado a b 0 1 0 1 1 3 3 1 4 4 1 0 S0 → a S1 | b S0 S1 → a S1 | b S3 S3 → a S1 | b S4 S4 → a S1 | b S0 | ε 2.2.7 Construção de Kleene∗ Se uma linguagem é aceite por um autómato finito determinista, então pode ser descrita por uma expressão regular 6. Ou seja, se a partir do autómato finito determinista gerado é possível produzir a expressão regular inicial, ou uma expressão regular equivalente. Assim, fecha-se o ciclo e garante-se a correcção de todo o processo. Considere-se que Rkij é o conjunto de todas as sequências do autómato entre os estado qi e qj sem passar por nenhum estado com número superior a k. Notar que para passar por um estado é necessário entrar e sair. Logo i ou j podem ser maiores que k. Uma vez que nenhum estado tem um número superior a n, Rnij representa todas as sequências entre qi e qj . O autómato não pode ter entradas no estado inicial, logo caso tais entradas existam é necessário acrescentar um novo estado inicial com uma transição vazia para o anterior estado inicial. O algoritmo consiste em introduzir as transições existentes em R0ij = {a|δ(qi, a) = qj} ∪ {ε|i = j}, ou seja, se i = j então ε faz parte de R0ij e se existirem transições directas entre os estados qi e qj então os símbolos dessas transições também fazem parte de R0ij . Os restantes valores são calculados recursivamente com base nos anteriores através da expressão Rkij = R k−1 ik (R k−1 kk ) ∗R k−1 kj |R k−1 ij para k ≤ n onde n é o estado de maior número. Ou seja, construindo caminhos sucessivamente mais compridos e que passam por mais estados. As expressão regular final é a união de todas as expressões regulares entre o estado inicial e cada um dos estados finais L = |sj∈SFR n 0j . Exemplo 2.10 Considerando a tabela do exemplo 2.7, verifica-se que como existe um só estado terminal 4 , uma expressão regular que representa o autómato é obtida por R404. A expressões R0ij que representam as todas as transições possíveis entre os vários estados, 6teorema 2.4 de [HU79] 2.3. ANALISADOR LEXICAL 39 R0ij j = 0 j = 1 j = 2 j = 3 j = 4 i = 0 ε a b ∅ ∅ i = 1 ∅ a | ε ∅ b ∅ i = 2 ∅ a b | ε ∅ ∅ i = 3 ∅ a ∅ ε b i = 4 ∅ a b ∅ ε Calculando apenas as expressões intermédias necessárias ao cálculo de R404, e simplificando R404 = R 3 04(R 3 44) ∗R 3 44 | R 3 04 = R 3 04(R 3 44)∗, temos, Rkij expressão resultante simplificação R102 a(a | ε) ∗ ∅ | b b R103 a(a | ε) ∗ b | ∅ a+ b R104 a(a | ε) ∗ ∅ | ∅ ∅ R122 a(a | ε) ∗ ∅ | (b | ε) b | ε R123 a(a | ε) ∗ b | ∅ a+ b R124 a(a | ε) ∗ ∅ | ∅ ∅ R132 a(a | ε) ∗ ∅ | ∅ ∅ R133 a(a | ε) ∗ b | ε (a+ b) | ε R134 a(a | ε) ∗ ∅ | b b R142 a(a | ε) ∗ ∅ | b b R143 a(a | ε) ∗ b | ∅ a+ b R144 a(a | ε) ∗ ∅ | ε ε R203 b(b | ε) ∗ (a+ b) | (a + b) b ∗ a + b R204 b(b | ε) ∗ ∅ | ∅ ∅ R233 ∅(b | ε) ∗ (a+ b) | (a+ b | ε) a+ b | ε R234 ∅(b | ε) ∗ ∅ | b b R243 b(b | ε) ∗ (a+ b) | (a + b) b ∗ a + b R244 b(b | ε) ∗ ∅ | ε ε R304 (b ∗ a+ b)(a + b | ε) ∗ b | ∅ b ∗ (a + b) + b R344 (b ∗ a+ b)(a + b | ε) ∗ b | ε b ∗ (a + b) + b | ε R404 (b ∗ (a+ b) + b)(b ∗ (a + b) + b | ε)∗ (b ∗ (a + b) + b)+ A expressão resultante obtida (b ∗ (a+ b) + b)+ pode ser convertida em (b ∗ (a ∗ ab) ∗ a ∗ abb)+ que é equivalente à expressão original (a|b) ∗ abb. 2.3 Analisador lexical Saber se uma frase, ou sequência de entrada, obedece a uma gramática regular é apenas parte do problema. Na realidade, a análise lexical de linguagens deve conseguir identi- 40 CAPÍTULO 2. LINGUAGENS REGULARES ficar repetições de subconjuntos da linguagem e permitir associar acções à identificação desses conjuntos. Só dessa forma é possível produzir resultados como consequência da identificação de componentes da linguagem. A solução consiste em dividir a linguagem numa união de expressões regulares L = (r1|r2| . . . |rn)∗. Devido às propriedades de fecho das expressões regulares sobre a ope- ração de união, podemos subdividir o problema. Para tal basta considerar um estado inicial que deriva, através de transições vazias, cada uma das expressões regulares. O autómato resultante considera, simultaneamente, as diversas expressões regulares. Para determinar qual a expressão regular reconhecida, é necessário associar cada estado final com a respectiva expressão regular. O processamento de uma frase pode atravessar diversos estados finais, só parando quando se atinge um estado de erro. Neste ponto é necessário procurar o último estado final atingido e considerar reconhecida a respectiva expressão regular. O analisador recomeça o processamento no estado inicial e os sím- bolos lidos após o último estado final considerado devem ser novamente processados. Notar que este processamento considera sempre o reconhecimento da sequência de en- trada mais comprida. Convém não confundir com a expressão regular mais comprida. Caso contrário, as sequências de entrada curtas mas válidas impediriam o reconheci- mento das restantes. A minimização da tabela segue o mesmo princípio enunciado atrás, mas a partição ini- cial tem de considerar os estados finais de expressões regulares distintas como perten- cendo a grupos iniciais distintos. Assim, além do grupo de estados não terminais e do grupo de erro, é necessário considerar tantos grupos de estados finais quantas as ex- pressões regulares existentes. Se o grupo de estados finais de uma expressão regular for um conjunto vazio, então essa expressão regular nunca é reconhecida. Exemplo 2.11 O autómato finito não determinista que descreve a linguagem composta pela união das expressões regulares aaa e (a|b) pode ser representada por, �� ff� -a�� ff��� ff� �� ff� �� ff� �� ff� ffifl �fi �� ff� �� ff� �� ff� �� ff� ffifl �fi �� ff�- � � ��� Z Z Z~ -- - � �3 - Q QQs ZZ~ � �> 0 e e aa1 2 a 43 r1 b e e e e 5 10 r2 97 86 A tabela de análise resultanteé 2.3. ANALISADOR LEXICAL 41 estado a b regra 0 1 2 1 3 E 2 2 E E 2 3 4 E 4 E E 1 Notar que os estados 2 e 4 não podem ser agrupados, embora sejam ambos finais e contenham as mesmas transições, pois pertencem a grupos finais distintos. Relembra-se que os estados podem ser agrupados mesmo que não tenham transições iguais, basta que tenham transições para estados que pertençam ao mesmo grupo. A análise da frase aa termina no estado 3, depois de ter passado pelo estado 1. Como este estado não é final é necessário procurar o último estado final, neste caso o estado 1. Caso não exista um estado final no caminho percorrido a sequência de entrada é incorrecta. No caso da frase aa é necessário recuar um estado, e consequentemente repor o último símbolo de volta na sequência a processar, aceitando a expressão regular (a|b). O analisador é depois reiniciado no estado 0 e processa novamente a letra a, que acabou de repor, aceitando mais uma vez a mesma expressão regular e terminando o processamento por ter atingido o fim da frase. No caso de um estado final, do autómato determinista, conter mais de um estado fi- nal do autómato não determinista, existe um conflito. O conflito é resolvido, em geral, optando por uma das possíveis expressões regulares. A solução mais frequentemente adoptada pelas ferramentas consiste em considerar prioritária a expressão regular que surje em primeiro lugar na especificação da gramática. Contudo, esta solução obriga a uma cuidada escrita das expressões regulares, por forma a garantir que todas são devi- damente reconhecidas. Notar que os conflitos surjem apenas entre expressões regulares que possam reconhecer sequências de igual comprimento. Exemplo 2.12 Considere-se a linguagem descritas pela união das seguintes expressões regulares identificadas por ordem de prioridade decrescente, bem como a tabela de análise resultante, r1 aab r2 aa r3 (a|b)* estado a b regra 0 1 2 3 1 3 2 3 2 4 2 3 3 4 5 2,3 4 4 2 3 5 4 2 1,3 da tabela de análise conclui-se que caso a expressão regular 3 tivesse prioridade sobre as restantes, estas nunca seriam reconhecidas. 42 CAPÍTULO 2. LINGUAGENS REGULARES 2.3.1 Compactação da tabela Na prática, na tabela de um linguagem, com um alfabeto constituídos pormuitos símbo- los distintos, é frequente encontrar colunas iguais. Também podem existir linhas iguais se pertencerem a grupos de minimização distintos, mas tais ocorrências são escassas. Notar que a utilização de digitos decimais é em geral indistinto do seu valor, ficando cada uma das 10 colunas com entradas iguais na tabela. O mesmo pode acontecer com certas classes de caracteres e operadores ou separadores. Estes conjuntos de caracteres, cujas entradas na tabela têm os mesmos valores para to- dos os estados do autómato finito determinista minimizado, são designados por classes de equivalência. A compactação da tabela é efectuada introduzindo um vector de com- primento igual ao alfabeto da linguagem, onde cada entrada indica a coluna da tabela a utilizar. Nesta situação podem-se remover todas as colunas duplicadas, apontando todos os símbolos da mesma classe de equivalência para a mesma coluna. Do ponto de vista do analisador lexical é desejável que todos os operadores e separado- res constituídos por um único carácter possam ser reconhecidos por uma só regra, por exemplo [− + ∗/(); , :], em vez de se reconhecer individualmente cada um. Na grande maioria dos casos, esta solução não tem implicações na restante análise das linguagens pois os analisadores sintácticos já reconhecem caracteres individuais. O reconhecimento individual de cada carácter obriga a considerar expressões regulares distintas, o que obriga criar mais estados finais distintos. A tabela de análise resultante, após a compressão das colunas iguais, pode ainda ser mais comprimida através de métodos genéricos de compressão de matrizes esparsas. Estes métodos, que trataremos mais adiante, permitem tirar partido do facto de certo valor, por exemplo 0, existir em mais de metade das entradas da tabela. Embora as tabelas de análise lexical não apresentem tais características, ao contrário das tabelas de análise sintáctica, pode-se considerar entradas por omissão ( default, em inglês ). Para tal cria-se uma nova coluna na qual se coloca o valor mais frequente em cada linha. As ocorrências desse valor em cada linha são substituídas por uma entrada especial, em geral codificada com o valor 0, o que significa que quando este valor é encontrado deve- se considerar a entrada por omissão. A tabela resultante passa a conter uma quantidade significativa de valores por omissão, podendo em casos reais atingir 90% de todas as entradas da tabela. A tabela pode agora ser compactada por algoritmos genéricos de compressão de matrizes esparsas. 2.3. ANALISADOR LEXICAL 43 2.3.2 Analisadores explicitamente codificados Até ao momento considerámos que o autómato finito determinista era codificado como uma tabela, como a do exemplo 2.7, e que um pequeno programa, como o apresentado na secção 2.2.4, determina o estado seguinte com base na tabela e carácter lido. Con- tudo, existem soluções que não utilizam tabelas e que se designam por explicitamente codificados ou hardcoded. Estas soluções são em geral mais rápidas à custa de um exe- cutável de maiores dimensões, em especial no caso de máquinas RISC. Consideremos uma abordagem baseada em saltos (gotos) que, contudo, não pode ser utilizada em linguagens sem essa instrução como o Java. Cada estado é identificado por uma etiqueta, efectuando saltos de acordo com carácter lido logo após a etiqueta. Por exemplo, a partir do estado 4 da tabela do exemplo 2.9 obtém-se state4: in = *input++; if (in == ’a’) goto state1; if (in == ’b’) goto state0; if (in == 0) return 1; /* só para estados finais */ goto error; /* termina cada estado */ Outra solução possível, sem recurso a saltos, consiste em codificar cada estado como uma função, sendo os saltos para as etiquetas subtituídos por chamadas às rotinas que representam os respectivos estados. No entanto, esta solução não é tão eficiente, pois as chamadas e retorno das rotinas são em geral operações lentas. Além disso, utiliza a pilha do processador para coleccionar os estados por onde vai passando. Assim, a pro- fundidade máxima da pilha limita a maior sequência a ser reconhecida. Tal limitação não tem vantagem pois, ao contrário dos analisadores sintácticos descendentes predi- tivos que veremos adiante, a informação deixada na pilha não tem interesse prático (excepto, eventualmente, para efeitos de backtracking). A partir da gramática regular do exemplo 2.9, a regra S4 correspondente ao estado 4 pode ser codificada como static int state4() { register char in = *input++; if (in == ’a’) return s1(); if (in == ’b’) return s0(); return 1; /* estados não finais retornam 0 */ } 44 CAPÍTULO 2. LINGUAGENS REGULARES 2.4 Exercícios Exercício 2.1 Considere o diagrama de transição do autómato finito não determinista do exem- plo 2.3. Determine, através do método dos subconjuntos, a tabela de transição do autómato finito determinista. Compare o resultado que obteve com a tabela do exemplo 2.9. Exercício 2.2 Considere a expressão regular (aa|ab|ba|bb)∗ definida sobre o alfabetoΣ = {a, b}: 1. Construa o autómato finito não determinista (NFA) pelo algoritmo de Thompson a que corresponde à expressão regular. 2. Construa algoritmicamente o autómato finito determinista (DFA) equivalente a partir do NFA da alínea anterior, e represente o seu diagrama de estados. Explicite quais os estados do NFA realizado em cada estado do DFA. 3. Minimize algoritmicamente o número de estados do autómato finito determinista (DFA), indicando todas as partições intermédias e respectivos grupos. Exercício 2.3 Construa o diagrama de estados do autómato finito determinista da expressãoregular (ε|b)((ε|a)b)∗a, indicando em quantos passos é analisada a sequência de entrada bbabba. Construa a tabela de transição minimizada e justifique quantos passos são agora necessários para processar a mesma sequência de entrada. Exercício 2.4 Construa a tabela de transição determinista minimizada das seguintes expressões regulares: 1. a(a|b) ∗ a 2. (a?b∗)∗ 3. a ∗ ba ∗ ba∗ 4. (a|b) ∗ abb(a|b)∗ 5. a(a|b) + b 6. (aba|bab)∗ 2.4. EXERCÍCIOS 45 Exercício 2.5 Construa a tabela de transição determinista minimizada da gramática regular, sobre o alfabeto {x, y, z} e onde A é símbolo não terminal inicial, A → xB B → y C | z D | ε C → y C | z D | ε D → y C | z D | ε Indique em quantos passos é processada a sequência de entrada xxzy. Exercício 2.6 Considere a sequência ordenada de expressões regulares aa, a∗, a|b definida sobre o alfabeto Σ = {a, b}: 1. Determine algoritmicamente os subconjuntos do autómato finito determinista (DFA) equi- valente a partir do autómato finito não determinista (NFA) construído pelo algoritmo de Thompson a que corresponde a sequência ordenada de expressões regulares. Explicite quais os estados do NFA realizado em cada estado do DFA. 2. Minimize algoritmicamente o número de estados do autómato finito determinista (DFA), indicando todas as partições intermédias e respectivos grupos. Represente a tabela de transição de estados resultante. 3. Indique, justificando, em quantos passos é processada a sequência de entrada: aabbaaa. Exercício 2.7 Construa a tabela de análise da linguagem definida pela sequência ordenada das seguintes expressões regulares ab, ab∗, a|b definida sobre o alfabeto Σ = {a, b}. Indique, justifi- cando, em quantos passos é processada a sequência de entrada: abaabb. Exercício 2.8 ∗ Determine uma expressão regular que defina a linguagem descrita pela tabela de análise: est a b 0 1 2 1 E 2 2 1 2 Exercício 2.9 Apresente a tabela de análise minimizada e compactada que analisa a linguagem definida pela expressão regular u(a(e|o)) ∗ i, definida sobre o alfabeto Σ = {a, e, i, o, u}. Capítulo 5 Análise sintáctica ascendente por tabela Um analisador ascendente constroí a árvore sintáctica das folhas para a raiz. Ao con- trário da análise descendente, que necessita prever qual a regra a utilizar, a análise ascendente atrasa a escolha da regra até ter lido todos os símbolos que constituem a derivação ( lado direito de uma produção ), mais os símbolos de antevisão necessários. A principal decisão de um analisador ascendente consiste em determinar quando uma sequência de símbolos corresponde a alguma das regras da gramática. Esta tarefa não é trivial, pois pode haver mais de uma regra com a mesma derivação e casos em que, apesar da semelhança, não se tratam de derivações possíveis. Como os analisadores ascendentes substituem uma derivação de uma regra pelo sím- bolo não terminal que a origina são globalmente designados por LR(k). A análise LR(k) é efectuada lendo a sequência de entrada da esquerda para a direita (Left to right), em- parelhando as derivações das regras da direita para esquerda (Right to left), usando no máximo k símbolos de antevisão. Estes analisadores são os mais utilizados pois são menos limitativos que os correspondentes analisadores preditivos descentes LL. Neste capítulo estudaremos em detalhe apenas os analisadores mais simples e menos exigentes, em termos da dimensão das tabelas necessárias. Contudo, a análise determi- nista da grande maioria das linguagens usadas em computação pode ser efectuada com base em gramáticas processáveis por analisadores LALR(1). Os analisadores LALR(1) apresentam tabelas da mesma dimensão das formas mais simples SLR(1) e LR(0), mas permitem analisar um maior número de gramáticas. Relembra-se que uma mesma lin- guagem pode ser descrita por diversas gramáticas equivalentes, bastando encontrar uma dessas gramáticas que seja pelo menos LALR(1). O funcionamento de um analisador ascendente baseia-se em duas operações fundamen- tais: o deslocamento (shift) de símbolos da sequência de entrada para a pilha auxiliar e a redução (reduce) dos símbolos de derivação de uma regra ao símbolo não terminal que 57 58 CAPÍTULO 5. ANÁLISE SINTÁCTICA ASCENDENTE POR TABELA a origina. Inicialmente a pilha começa vazia, sendo deslocados símbolos até ser encon- trada uma derivação válida. O processamento termina quando se atínge uma operação de aceitação (accept) ou de erro (error). 5.1 Construção do analisador Para que a análise determinista de uma linguagem por um analisador ascendente seja correcta basta que os deslocamentos e a aceitação sejam permitidos apenas em certas situações. A diferença entre os analisadores LR(0), SLR(1) e LALR(1) consiste apenas na colocação das reduções. No entanto, se uma redução for erradamente executada, o seu resultado não poderá ser deslocado, pelo que as sequências de entrada erradas são igualmente detectadas, mas com algum atraso. A determinação dos deslocamentos é efectuada com base num autómato determinista muito semelhante ao utilizado na análise das linguagens regulares. gramática aumentada Para que a sequência de entrada seja correctamente detectada pela gramática é necessário introduzir uma regra auxiliar que explicite o fim do pro- cessamento. Desta forma, não é possível terminar prematuramente o processamento, deixando símbolos por processar. Esta regra auxiliar contém uma só derivação com o símbolo inicial da gramática e o símbolo $, símbolo que utilizaremos para designar o fim da sequência de entrada (end of file). Exemplo 5.1 Considerando uma gramática com símbolo inicial S, S → ’(’ A ’)’ A → ’a’ | S | A ’,’ ’a’ a gramática aumentada inclui também a regra S’→ S $, passando S’ a ser o novo símbolo inicial da gramática aumentada, ou seja, S’ → S $ S → ’(’ A ’)’ A → ’a’ | S | A ’,’ ’a’ 5.1. CONSTRUÇÃO DO ANALISADOR 59 estados do autómato A análise ascendente baseia-se na construção de um autómato não determinista e na sua conversão em determinista, pelos mesmos processos utili- zados para o tratamento de linguagens regulares. Para tal, cada regra é dividida nos diversos estados em que o processamento da regra se pode encontrar, designados por itens, que correspondem aos estados do autómato não determinista. Assim, uma regra em que a derivação é composta porN símbolos, terminais ou não terminais, é composta por N+1 itens, sendo o primeiro correspondente ao estado antes de iniciar o processa- mento da regra, o segundo após consumir o primeiro símbolo e o último após o proces- samento de todos os símbolos da derivação da regra. A cada um destes itens é atribuído um número único que destingue cada estado do autómato finito não determinista. Os diversos itens de cada regra são ligados, pela ordem de processamento, por transições que consomem os símbolos da derivação da regra. Exemplo 5.2 Considerando a primeira regra do exemplo 5.1 S→ ’(’ A ’)’ , são gerados qua- tro itens, onde o símbolo • representa o ponto de processamento da regra no respectivo estado do autómato não determinista, S→ • ’(’ A ’)’ 1 ′(′ −→ S→ ’(’ • A ’)’ 2 A −→ S→ ’(’ A • ’)’ 3 ′)′ −→ S→ ’(’ A ’)’ • 4 Para tornar a leitura mais legível e compacta representaremos cada estado, do autómato finito não determinista, pelo seu número colocado no ponto de processamento da regra, ficando a regra do exemplo 5.2 reduzida a S→ 1 ’(’ 2 A 3 ’)’ 4. transições vazias Além das transições entre os diversos itens de uma regra, etiqueta- das pelos símbolos consumidos, é necessário considerar que ao processar um símbolo não terminal devem ser consideradas todas as regras que esse símbolo pode derivar. Assim, no item que antecede o processamento de um símbolo não terminal inserem-se transições vazias para todos os itens iniciais das regras que esse símbolo não terminal deriva. Na representaçãoque utilizaremos, não etiquetaremos essas transições com o símbolo ε, como fizemos nas linguagens regulares, nem representaremos por completo o arco que une os estados. Desta forma, o item 2 da regra do exemplo 5.2 será repre- sentado por 2 ր A, significando que no item 2 existem transições vazias para os itens iniciais das três regras que o símbolo A deriva. Para posterior referência, indicaremos antes de cada regra o seu número de ordem, correspondendo r0 ( regra zero ) à regra au- xiliar da gramática aumentada. Tal representação pretende apenas simplificar e tornar mais compacto e legível o autómato, sendo funcionamente equivalente à representação utilizada nas linguagens regulares. 60 CAPÍTULO 5. ANÁLISE SINTÁCTICA ASCENDENTE POR TABELA Exemplo 5.3 O autómato finito não determinista da gramática aumentada usada no exemplo 5.1 fica r0 S’ → 0 ր S 1 $ r1 S → 2 ’(’ 3 ր A 4 ’)’ 5 r2 A → 6 ’a’ 7 r3 | 8 ր S 9 r4 | 10 ր A 11 ’,’ 12 ’a’ 13 aconselha-se a numerar os itens sequencialmente dentro de cada regra por forma a facilitar a consistência das transições, embora apenas se exija que cada item tenha um número distinto de todos os outros do autómato finito não determinista. Notar que na regra auxiliar da gramática aumentada existem apenas dois itens, pois não é possí- vel passar além do fim da sequência de entrada. autómato determinista A construção do autómato finito determinista segue exacta- mente o mesmo processo utilizado nas linguagens regulares. No entanto, como número de símbolos é potencialmente grande omitiremos todas as transições que produzam es- tados deterministas que sejam conjuntos vazios. Exemplo 5.4 A determinação dos estados do autómato determinista do exemplo 5.3 resume-se a estado entrada move fecho− ε\move novo estado 0 2 I0 I0 S 1 I1 ’(’ 3 6, 8, 10, 3 I2 I2 A 4, 11 I3 ’a’ 7 I4 S 9 I5 ’(’ 3 . . . (I2) I3 ’)’ 5 I6 ’,’ 12 I7 I7 ’a’ 13 I8 5.1. CONSTRUÇÃO DO ANALISADOR 61 construção da tabela de análise Tal como no caso das linguagens regulares, também a análise sintáctica ascendente utiliza uma tabela de análise. No entanto, para o seu processamento é necessária uma pilha de dados auxiliar, onde são colocados tempora- riamente os símbolos até que uma regra seja identificada e a sua derivação substituída pelo símbolo não terminal que lhe dá origem. Neste caso, a tabela inclui cinco tipos de operações distintas: deslocamento (shift) : corresponde a retirar o símbolo da sequência de entrada e co-lo- cá-lo no topo da pilha, seguido do estado indicado na operação de deslocamento. Por exemplo s2 ( ou seja, shift 2 ), corresponde a deslocar o símbolo que estiver à cabeça da sequência de entrada e mover para o estado 2 ( do autómato finito determinista ). movimento (goto) : corresponde a mover para o estado indicado, colocando-o no topo da pilha, representado por g2 ( ou seja, goto 2 ). erro (error) : não é representado na tabela de análise, correspondendo às posições dei- xadas em branco. redução (reduce) : corresponde a substituir os símbolos de derivação de uma regra exis- tente no topo da pilha pelo símbolo não terminal que deriva essa mesma regra. É representado por r2 ( ou seja, reduce 2 ), mas apenas neste caso o número 2 não representa um estado do autómato mas sim o número da regra aceitação (accept) : corresponde à redução da regra auxiliar da gramática aumentada, representando o fim do processamente, podendo apenas existir na coluna corres- pondente ao fim do processamento ( representado por $ ). A tabela de análise é constituída por todos símbolos terminais, incluindo o fim de pro- cessamento $ ( que pode ser lido de um ficheiro ), e todos os símbolos não terminais da gramática inicial ( exclui-se a regra auxiliar da gramática aumentada ). Muita da literatura separa a tabela de deslocamentos da tabela de movimentos pelo que, embora utilizaremos uma única tabela, explicitaremos a sua individualização por uma dupla barra. O autómato finito determinista permite preencher as operações de deslocamento e mo- vimento da tabela de análise. O preenchimento é idêntico, diferindo apenas no facto de se tratar de um deslocamento para símbolo terminais ( aqueles que podem ser ob- tidos da sequência de entrada ) e de um movimento para os símbolos não terminais. Com base no cálculo do autómato finito determinista, e ignorando as duas colunas que contêm os estados não deterministas, ficamos com os valores a preencher 62 CAPÍTULO 5. ANÁLISE SINTÁCTICA ASCENDENTE POR TABELA Exemplo 5.5 Eliminando os estados do autómato finito não determinista da tabela do exemplo 5.4, o preen- chimento das operações de deslocamento e movimento na tabela de análise é directo. As duas primeiras colunas designam, por ordem, a linha e a coluna da tabela do autómato finito determi- nista e a última designa o valor a preencher nessa posição. estado entrada novo estado I0 I0 S I1 ’(’ I2 I2 A I3 ’a’ I4 S I5 ’(’ (I2) I3 ’)’ I6 ’,’ I7 I7 ’a’ I8 ′(′ ′a′ ′)′ ′,′ $ S A 0 s2 g1 1 2 s2 s4 g5 g3 3 s6 s7 4 5 6 7 s8 8 5.1.1 Gramáticas LR(0) O autómato LR(0) anterior permite construir tabelas para analisadores LR(0), SLR(1) e LALR(1), diferindo apenas na forma como as reduções são inseridas na tabela. Como já foi referido, são os deslocamentos e movimentos que determinam se a sequência de entrada está correcta ou não, pelo que mesmo que sejam colocadas reduções desneces- sárias, mais tarde ou mais cedo a falha será detectada. O analisador mais simples é o analisador LR(0), que como o nome sugere não usa sím- bolos de antevisão ( representado pelo número entre parenteses, ou seja 0 símbolos de antevisão ). Para compreender o seu funcionamento basta representar graficamente o autómato finito determinista, considerando que uma vez atingido o último estado de uma regra esta fica pronta a ser reduzida. Tal como no processamento de expressões regulares, a determinação de quais os estados do autómato finito determinista em que as diversas regras são reduzidas baseia-se na identificação dos estados finais de cada regra no autómato finito não determinista. As- sim, todos os estados deterministas, que incluam esse estado não determinista, reduzem a regra. Exemplo 5.6 A representação gráfica do autómato finito determinista cálculado no exemplo 5.4 pode ser representado pela figura abaixo. 5.1. CONSTRUÇÃO DO ANALISADOR 63 I I I I I I II I r35 r1 r4 0 1 2 3 4 6 7 8 r2 S A S ’(’ ’)’ ’a’ ’a’ ’,’ ’(’ acc (r0) Como nos analisadores LR(0) não existem símbolos de antevisão, a redução será efectu- ada para todos os símbolos terminais da linguagem em questão. Excepção feita à regra auxiliar da gramática aumentada, que apenas pode ser reduzida para o símbolo de fim de processamento $, pois para qualquer outro símbolo significaria que a sequência não estava completamente processada. Exemplo 5.7 A tabela de análise LR(0) para a gramática do exemplo 5.1 fica concluída com o preenchimento das reduções das regras e da operação de aceitação para a regra auxiliar da gramática aumentada. Todas as posições deixadas em branco representam situações de erro. ′(′ ′a′ ′)′ ′,′ $ S A 0 s2 g1 1 acc 2 s2 s4 g5 g3 3 s6 s7 4 r2 r2 r2 r2 r2 5 r3 r3 r3 r3 r3 6 r1 r1 r1 r1 r1 7 s8 8 r4 r4 r4 r4 r4 Notar que nenhuma linha da tabela pode ficar totalmente em branco, pois esse estado não seria necessário. Da mesma forma, nenhuma coluna da tabela pode ficar totalmente em branco por esse símbolo nunca seria processado. No caso de um símbolo terminal, significa que esse símbolo nunca pode ser lido, enquanto no caso de um símbolo não terminal, significa que nenhuma das regras que ele deriva é alguma vez reduzida ( uma vez que após uma redução existe sempre um movimento, excepto paraa regra auxiliar ). 64 CAPÍTULO 5. ANÁLISE SINTÁCTICA ASCENDENTE POR TABELA 5.1.2 Análise ascendente por tabela Oprocessamento de uma sequência de entrada por um analisador ascendente por tabela utiliza as operações atrás referidas. Tal como no caso da análise preditiva descendente por tabela, recorre-se a uma pilha. Neste caso, a pilha irá conter alternadamente estados do autómato finito determinista e símbolos, terminais e não terminais, da gramática. Inicialmente a pilha contém apenas o estado inicial ( sempre designado por 0 nos exem- plos aqui apresentados ). A determinação da operação a realizar utiliza o elemento no topo da pilha e o símbolo à cabeça da sequência de entrada, correspondendo respec- tivamente às linha e coluna da tabela de análise. Após uma redução, o topo da pilha fica com o símbolo não terminal que deriva a regra reduzida, utilizando-se neste caso os dois elementos do topo da pilha para determinar a operação a realizar. Exemplo 5.8 Considere-se a análise da sequência de entrada (a,a) através da tabela do exem- plo 5.7 para a gramática do exemplo 5.1: pilha entrada acção 0 (a, a)$ shift-2 0(2 a, a)$ shift-4 0(2a4 , a)$ reduce-2 0(2A , a)$ goto-3 0(2A3 , a)$ shift-7 0(2A3, 7 a)$ shift-8 0(2A3, 7a8 )$ reduce-4 0(2A )$ goto-3 0(2A3 )$ shift-6 0(2A3)6 $ reduce-1 0S $ goto-1 0S1 $ accept No primeiro passo da análise procura-se na tabela o estado 0 ( topo da pilha ), correspondente à primeira linha, e na coluna ’(’ a operação a executar, ou seja s2 ( shift-2 ). No terceiro passo, para efectuar a redução da regra 2 necessitamos retirar do topo da pilha todos os símbolo que derivam a regra e respectivos estados colocando em seu lugar o símbolo não terminal que deriva essa regra. Neste caso apenas a 4 é retirado da pilha, sendo colocado o símbolo A, pois a regra 2 é apenas A→ a. Para as restantes reduções do exemplo acima o processo é idêntico, mas para as regras 4 e 1, ou seja, A→ A′,′ a e S →′ (′A′)′, respectivamente. Notar que, ao contrário dos analisadores preditivos descendentes, a pilha cresce da esquerda para a direita para que as derivações da regra 5.1. CONSTRUÇÃO DO ANALISADOR 65 apareçam na pilha pela mesma ordem que são escritas na gramática, apenas contendo os números dos estados entre os símbolos. Após cada redução existe sempre uma operação de movimento, utilizando-se os dois elementos do topo da pilha para determinar as linha e coluna na tabela. Assim, no quarto passo, o topo da pilha indica linha 2 e coluna A, ou seja, g3 ( goto-3 ). 5.1.3 Gramáticas SLR(1) As gramáticas LR(0), ou seja aquelas que podem produzem tabelas LR(0) sem gerar conflitos ( mais de uma operação por posição na tabela de análise ), são bastante restriti- vas. Embora não tenha as limitações de recursividade ou factorização dos analisadores preditivos descendentes, o método de construção LR(0) produz frequentemente con- flitos. Para uma sequência de entrada correcta, uma regra só pode ser seguida pelos seus símbolos de FOLLOW, todas as restantes reduções estão erradas. Assim, em vez de efectuar a redução e vir a detectar o erro no próximo deslocamento, o método de construção SLR(1) apenas preenche as reduções para os símbolos de FOLLOW. Exemplo 5.9 Considerando o exemplo 5.1 temos que FOLLOW(S) = {$ , ′)′ , ′,′ } e FOL- LOW(A) = {′)′ , ′,′ } logo a regra 1 reduz apenas nos três símbolos de FOLLOW e as regras 2, 3 e 4 nos dois símbolos atrás calculados, ficando a tabela ′(′ ′a′ ′)′ ′,′ $ S A 0 s2 g1 1 acc 2 s2 s4 g5 g3 3 s6 s7 4 r2 r2 5 r3 r3 6 r1 r1 r1 7 s8 8 r4 r4 Notar que a análise da tabela gerada pelo método SLR(1) é igual à análise da tabela LR(0), apenas em caso de redução com símbolo de antevisão que não pertença ao con- junto FOLLOW é imediatamente gerado um erro e parado o processamento. Além disso, caso existam operações de deslocamento nessas posições, deixa de haver confli- tos, sendo essas gramáticas SLR(1) mas não LR(0). 66 CAPÍTULO 5. ANÁLISE SINTÁCTICA ASCENDENTE POR TABELA 5.2 Gramáticas LALR(1) O método de construção LALR(1) leva o princípio do SLR(1) um passo mais adiante. Nomeadamente, nem todos os símbolos de FOLLOW são utilizados em todas as regras, mas apenas um subconjunto destes, ou seja os símbolos de antevisão ( LOOKAHEAD ). Cada ocorrência da regra, e não apenas a regra em si, pode apresentar símbolos de antevisão distintos. Determinando caso a caso, quais são efectivamente os símbolos de antevisão na activação regra, permite-se que em cada redução se possa restringir ainda mais o número de posições preenchidas com reduções. Tal facto permite, em primeiro lugar evitar conflitos com outras reduções ou com deslocamentos, tornando as gramáticas LALR(1) mais gerais que as que vimos até ao momento. Em segundo lugar, torna a tabela mais esparsa, ou seja contém mais posições vazias, o que acelera a determinação de erros e permite uma maior compressão da tabela resultante. Exemplo 5.10 Considere-se a gramática seguinte e o respectivo autómato finito não determi- nista LR(0) S → A ’b’ ’b’ | ’a’ ’a’ ’b’ | ’b’ A ’a’ A → ’a’ r0 S’ → 0 ր S 1 $ r1 S → 2 ր A 3 ’b’ 4 ’b’ 5 r2 | 6 ’a’ 7 ’a’ 8 ’b’ 9 r3 | 10 ’b’ 11 ր A 12 ’a’ 13 r4 A → 14 ’a’ 15 a conversão em autómato finito determinista LR(0) estado entrada move fecho− ε\move novo estado 0 2, 6, 10, 14 I0 I0 S 1 I1 A 3 I2 ’a’ 7, 15 I3 ’b’ 11 14 I4 I2 ’b’ 4 I5 I3 ’a’ 8 I6 I4 A 12 I7 ’a’ 15 I8 I5 ’b’ 5 I9 I6 ’b’ 9 I10 I7 ’a’ 13 I11 a que corresponde a tabela de análise SLR(1), sabendo que FOLLOW(S) = {$} FOLLOW(A) = {′a′ , ′b′} 5.2. GRAMÁTICAS LALR(1) 67 ′a′ ′b′ $ S A 0 s3 s4 g1 g2 1 acc 2 s5 3 s6/r4 r4 4 s8 g7 5 s9 6 s10 7 s11 8 r4 r4 9 r1 10 r2 11 r3 Pode-se verificar que no estado 3 para o símbolo ’a’ existe um conflito deslocamento-redução. No entanto, fazendo o processamento mental, verificamos que ao ler o primeiro carácter ’a’, ficamos no fim da regra 4 vindo da regra 1, bem como no segundo estado da regra 2. Porém, dependendo do carácter seguinte podemos determinar qual das duas opções é efectivamente a correcta. Tal deve-se ao facto de quando se processa a regra 4 vindo da regra 1 apenas o símbolo ’b’ se pode seguir, embora quer ’a’ como ’b’ serem FOLLOW do símbolo não terminal A. O método de construção de tabelas de análise LALR(1) produz autómatos de igual di- mensão que o LR(0), mas onde cada ocorrência do estado não determinista transporta o conjunto de símbolos de antevisão necessários. A redução é efectuada apenas para os símbolos de antevisão associados a cada ocorrência do estado não determinista, corres- pondente ao final da regra. Notar que o estado 14 da regra 4 do exemplo 5.10 terá símbolos de antevisão distintos consoante têm origem na regra 1 ou na regra 3, respectivamente ’b’ e ’a’. Os símbolos de antevisão devem ser depois ser sucessivamente transportados até ao último item da regra, o estado 15 no exemplo, por forma a poderem ser utilizados na sua redução. Desta forma, os símbolos de antevisão necessitam apenas ser calculados nas transições vazias, coorespondendo à antevisão após o não terminal que lhe está associado. Caso esse não terminal seja o último símbolo da regra, então são símbolos de antevisão todos os símbolos de antevisão do estado anterior à transição vazia. O estado inicial tem por antevisão o fim do ficheiro, representado por 0$. Exemplo 5.11 Associando os símbolos de antevisão às transições vazias ficamos com um autó- mato finito não determinista semelhante ao do exemplo 5.10 68 CAPÍTULO 5. ANÁLISE SINTÁCTICA ASCENDENTE POR TABELA r0 S’ → 0 ր $ S 1 $ r1 S → 2 ր ′b′ A 3 ’b’ 4 ’b’ 5 r2 | 6 ’a’ 7 ’a’ 8 ’b’ 9 r3 | 10 ’b’ 11 ր ′a′ A 12 ’a’ 13 r4 A → 14 ’a’ 15 o primeiro passo no cálculo do autómato determinista tem presente que o estado0 transporta o símbolo de antevisão $ para os estados 2, 6, 10, mas que o estado 2 apenas transporta o símbolo de antevisão ’b’ para o estado 14, pois quando a regra A →′ a′ reduzir só poderá ter a seguir o símbolo ′b′, estado entrada move fecho− ε\move novo estado 0$ 2$, 6$, 10$, 14 ′b′ I0 no segundo passo todos os símbolos de antevisão são transportados para o respectivo estado se- guinte da regra, que de acordo com a sequência de numeração utilizada corresponde também ao número seguinte. Apenas a transição vazia do estado 11 para o estado 14 necessita calcular novos símbolos de antevisão, estado entrada move fecho− ε\move novo estado 0$ 2$, 6$, 10$, 14 ′b′ I0 I0 S 1 $ I1 A 3$ I2 ’a’ 7$, 15 ′b′ I3 ’b’ 11$ 14 ′a′ I4 a tabela fica completa transportando os restantes símbolos de antevisão, estado entrada move fecho− ε\move novo estado 0$ 2$, 6$, 10$, 14 ′b′ I0 I0 S 1 $ I1 A 3$ I2 ’a’ 7$, 15 ′b′ I3 ’b’ 11$ 14 ′a′ I4 I2 ’b’ 4 $ I5 I3 ’a’ 8 $ I6 I4 A 12 $ I7 ’a’ 15 ′a′ I8 I5 ’b’ 5 $ I9 I6 ’b’ 9 $ I10 I7 ’a’ 13 $ I11 5.2. GRAMÁTICAS LALR(1) 69 Notar que agora o estado 15 aparece com o símbolo de antevisão ’b’ em I3, mas com ’a’ em I8. Desta forma, a redução da regra 4 é apenas colocada na coluna ’b’ para o estado I3 e apenas na coluna ’a’ para o estado I8. Podemos verificar, através de uma observação cuidada da gramá- tica do exemplo que este é efectivamente o caso, ficando a tabela de análise gerada pelo método LALR(1) ′a′ ′b′ $ S A 0 s3 s4 g1 g2 1 acc 2 s5 3 s6 r4 4 s8 g7 5 s9 6 s10 7 s11 8 r4 9 r1 10 r2 11 r3 Quando os símbolos de antevisão não podem ser identificados no autómato não deter- minista, quando o não terminal é o último símbolo da regra, utilizaremos o símbolo ∝ para indicar o transporte dos símbolos de antevisão anteriores para os estados atingidos pela transição vazia. Por exemplo, S→ 25 ր ∝ A 26. Como foi referido, os autómatos não determinista e determinista LALR(1) são iguais aos LR(0), com a excepção da anotação dos símbolos de antevisão a cada ocorrência do estado não determinista. Assim, quando se repete um conjunto de estados não determi- nista é porque se está na presença do mesmo estado determinista. Contudo, o cálculo dos símbolos de antevisão tem de ser completamente calculada, pois pode diferir da ocorrência inicial. Sempre que existe discrepância entre os símbolos de antevisão dos diversos estados não determinista, de um mesmo estado determinista, estes devem ser fundidos. No entanto, caso já tenham sido calculados novos estados, os símbolos de antevisão devem ser propagados a esses estados. Exemplo 5.12 Considere-se uma simplificação da instrução if com e sem else da linguagem C r0 S’ → 0 ր $ S 1 $ r1 S → 2 ’i’ 3 ր ∝ S 4 r2 | 5 ’i’ 6 ր ′e′ S 7 ’e’ 8 ր ∝ S 9 r3 | 10 ’x’ 11 70 CAPÍTULO 5. ANÁLISE SINTÁCTICA ASCENDENTE POR TABELA o cálculo do autómato finito determinista até à repetição do estado I2 é idêntica ao exemplo 5.11 estado entrada move fecho− ε\move novo estado 0$ 2$, 5$, 10$ I0 I0 S 1 $ I1 ’i’ 3$, 6$ 2$, 5$, 10$ I2 ’x’ 11$ I3 I0 S 4 $, 7$ I4 ’i’ 3$, 6$ 2 ′e′ , 5 ′e′ , 10 ′e′ (I2) neste ponto surje uma nova ocorrência do estado I2 = {3, 6, 2, 5, 10} mas com símbolos de antevisão distintos nos estados 2, 5, 10. Para que os símbolos de antevisão das duas ocorrências fiquem coerentes é necessário que esses três estados passem a ter ambos os símbolos de antevisão, passando I2 = {3 $, 6$, 2$, ′e′ , 5$, ′e′ , 10$, ′e′} em ambas as ocorrências. No entanto, os estados I3 e I4 já foram entretanto calculados com base nos anteriores símbolos de antevisão de I2, embora haja situações em os novos símbolos de antevisão de I2 não afectam I3 e I4 este não é o caso. Assim, o processo deve ser repetido a partir da primeira ocorrência de I2, mas agora com os novos símbolos de antevisão estado entrada move fecho− ε\move novo estado 0$ 2$, 5$, 10$ I0 I0 S 1 $ I1 ’i’ 3$, 6$ 2$, ′e′ , 5$, ′e′ , 10$, ′e′ I2 ’x’ 11$, ′e′ I3 I0 S 4 $,′e′ , 7$, ′e′ I4 ’i’ 3$, ′e′ , 6$, ′e′ 2$, ′e′ , 5$, ′e′ , 10$, ′e′ (I2) notar que mais uma vez a segunda ocorrência de I2 ficou com símbolos de antevisão distintos da primeira ocorrência, mas desta vez nos estados 3 e 6. O processo repete-se, mas desta vez sem consequências para os estados entretanto já re-calculados, ficando a tabela completa 5.3. ELIMINAÇÃODE CONFLITOS 71 estado entrada move fecho− ε\move novo estado 0$ 2$, 5$, 10$ I0 I0 S 1 $ I1 ’i’ 3$, ′e′ , 6$, ′e′ 2$, ′e′ , 5$, ′e′ , 10$, ′e′ I2 ’x’ 11$, ′e′ I3 I0 S 4 $,′e′ , 7$, ′e′ I4 ’i’ 3$, ′e′ , 6$, ′e′ 2$, ′e′ , 5$, ′e′ , 10$, ′e′ (I2) ’x’ 11$, ′e′ (I3) I4 ’e’ 8 $,′e′ , 7$, ′e′ 2$, ′e′ , 5$, ′e′ , 10$, ′e′ I5 I5 S 9 $,′e′ I6 ’i’ 3$, ′e′ , 6$, ′e′ 2$, ′e′ , 5$, ′e′ , 10$, ′e′ (I2) ’x’ 11$, ′e′ (I3) finalmente a tabela de análise apresenta, como seria de esperar, um conflito deslocamento-redução, ′i′ ′e′ ′x′ $ S 0 s2 s3 g1 1 acc 2 s2 s3 g4 3 r3 r3 4 s5/r1 r1 5 s2 s3 g6 6 r2 r2 5.3 Eliminação de conflitos Se surjem conflitos na construção de tabelas de análise pelo método LALR(1) é neces- sário determinar a origem desses conflitos. Uma gramática ambígua produz sempre conflitos e, muito provavelemente, não reflecte a linguagem que pretende modelar. De facto, as linguagens utilizadas em computadores, sejam linguagens de programação ou outras, não são ambíguas, excepto em casos muitos particulares. Noutras situações apenas, um símbolo de antevisão pode não ser suficiente, sendo necessário resolver o problema noutras fases do processamento, como a análise lexical ou a análise semântica. No entanto, a maioria das linguagens utilizadas pelos computadores não só não é am- bígua como pode ser processada com apenas um símbolo de antevisão. Tal deve-se ao facto de quem criou a linguagem ter tido presente as ferramentas de análise existen- tes, salvo poucas excepções. O problema reside no facto de encontrar uma gramática LALR(1) que descreva a linguagem em questão. De facto, mesmo que tal gramática, ou 72 CAPÍTULO 5. ANÁLISE SINTÁCTICA ASCENDENTE POR TABELA gramáticas, existam nada me garante que a minha análise da linguagem produza uma dessas gramáticas. Nesta secção tentaremos ver alguns procedimentos simples que permitem modificar a gramática por forma a evitar conflitos desnecessários e que resultam, em geral, de vícios do utilizador ou de descrições que dirigem o utilizador para soluções menos conflituo- sas. Os conflitos têm sempre origem em reduções, sendo estas efectuadas quando se atínge o último estado de uma regra, para certos símbolos de antevisão. Se o estado determinista contiver outros estados não deterministas em posição de redução ou deslocamento, para o mesmo símbolo de antevisão, então esse estado tem um conflito. Desta forma a pri- meira regra para evitar conflitos é criar regras compridas, ou seja com muitos símbolos, face a muitas regras curtas, pois cada regra usada necessita ser reduzida. Existe ainda uma vantagem no desempenho do analisador já que cada regra usada consome dois passos de processamento, um para a redução e outro para o movimento, e provavel- mente necessitará de mais estados na tabela de análise. Além disso cada novo símbolo não terminal acrescenta mais uma coluna à tabela. 5.3.1 Conflitos deslocamento-redução Um conflito deslocamento-redução tem origem numa regra estar em condições de ser reduzida para o símbolo de antevisão em questão, enquanto outra regra ou regras po- dem deslocar esse símbolo. Exemplo 5.13 Considere-se a gramática S → ’x’ ’y’ ’z’ | A ’y’ | A ’z’ A → ’x’ onde a sequência xy pode ser reconhecida pela primeira regra ou pela segunda regra seguida da quarta.
Compartilhar