• Nenhum resultado encontrado

LINGUAGENS DE PROGRAMAÇÃO VERSUS LINGUAGENS NATURAIS

19VISÃO GERAL DO CAPÍTULO

LINGUAGENS DE PROGRAMAÇÃO VERSUS LINGUAGENS NATURAIS

A análise léxica destaca uma das formas sutis de como as linguagens de programação diferem das naturais, como inglês ou chinês. Nestas, o relacionamento entre a representação de uma palavra — grafia ou pictograma — e seu significado não é óbvio. Em inglês, are é verbo, enquanto art é substantivo, embora as palavras sejam diferentes apenas no último caractere. Além do mais, nem todas as combinações de caracteres são palavras legítimas. Por exemplo, arz difere muito pouco de are e art, mas não ocorre como palavra no uso normal do inglês.

Um scanner para o inglês poderia usar técnicas baseadas em FA para reconhecer palavras em potencial, pois todas são retiradas de um alfabeto restrito. Depois disso, porém, ele precisa consultar uma provável palavra num dicionário para determinar se é, de fato, uma palavra. Se a palavra tiver uma classe gramatical exclusiva, a pesquisa no dicionário também resolverá este problema. Porém, muitas palavras em inglês podem ser classificadas com diversas classes gramaticais. Exemplo são love e stress; ambas podem ser substantivo ou verbo. Para estas, a classe gramatical depende do contexto ao redor. Em alguns casos, entender o contexto gramatical é suficiente para classificar a palavra. Em outros, isto exige um conhecimento do significado, tanto da palavra quanto do seu contexto.

Ao contrário, as palavras em uma linguagem de programação são quase sempre especificadas de forma léxica. Assim, qualquer string em [1…9][0…9]* é um inteiro positivo. A RE [a…z]([a…z]|[0…9])* define um subconjunto dos identificadores em Algol; arz, are e art são todos identificadores, sem necessidade de qualquer pesquisa para estabelecer este fato. Por certo, alguns identificadores podem ser reservados como palavras-chave. Porém, essas exceções também podem ser especificadas lexicamente. Nenhum contexto é necessário.

Esta propriedade resulta de uma decisão deliberada no projeto da linguagem de programação. A escolha de fazer que a grafia tenha uma única classe gramatical simplifica a análise léxica, a análise sintática e, aparentemente, abre mão de pouca coisa na expressividade da linguagem. Algumas linguagens têm permitido palavras com duas classes gramaticais — por exemplo, PL/I não possui palavras-chave reservadas. O fato de que as linguagens mais recentes tenham abandonado a ideia sugere que as complicações são maiores que a flexibilidade linguística extra.

2.3.3 Propriedades de fechamento das REs

Expressões regulares e as linguagens que geram têm sido assunto de muito estudo. Elas possuem muitas propriedades interessantes e úteis. Algumas destas desempenham um papel crítico nas construções que criam reconhecedores a partir de REs.

Expressões regulares são fechadas sob muitas operações — ou seja, se apli- carmos a operação a uma RE ou a uma coleção de REs, o resultado é uma RE. Alguns exemplos óbvios são concatenação, união e fechamento. A concatenação de duas REs x e y é simplesmente xy. A união, x | y. O fechamento de Kleene de x é simplesmente x*. Pela definição de uma RE, todas essas expressões também são REs.

Essas propriedades de fechamento desempenham papel fundamental no uso das REs para a criação de scanners. Suponha que tenhamos uma RE para cada categoria sintática na linguagem-fonte, a0, a1, a2,. . ., an. Então, para construir uma RE para todas as palavras válidas na linguagem, podemos juntá-las com alternação como a0 | a1 | a2 |. .. | an. Como as REs são fechadas sob união, o resul- tado é uma RE. Qualquer coisa que pudermos fazer em uma RE para uma única categoria sintática será igualmente aplicável à RE para todas as palavras válidas na linguagem.

O fechamento sob união implica que qualquer linguagem finita é uma linguagem regu- lar. Podemos construir uma RE para qualquer coleção finita de palavras listando-as em uma grande alternação. Como o conjunto de REs é fechado sob união, essa alternação é uma RE, e a linguagem correspondente é regular.

O fechamento sob concatenação nos permite montar REs complexas, a partir de outras mais simples, concatenando-as. Esta propriedade parece óbvia e pouco importante. Porém, nos permite juntar REs de formas sistemáticas. O fechamento garante que ab é uma RE desde que tanto a quanto b sejam REs. Assim, quais- quer técnicas que possam ser aplicadas a a ou a b podem ser aplicadas a ab; isto inclui construções que geram automaticamente um reconhecedor a partir de REs.

Expressões regulares também são fechadas sob os fechamentos de Kleene e finitos. Esta propriedade nos permite especificar tipos particulares de conjuntos grandes, ou mesmo infinitos, com padrões finitos. O fechamento de Kleene permite-nos especificar conjuntos infinitos com padrões finitos concisos; alguns exemplos incluem os inteiros e identificadores de tamanho ilimitado. Já os fechamentos finitos nos possibilitam especificar conjuntos grandes, porém finitos, com a mes- ma facilidade.

A próxima seção mostra uma sequência de construções que criam um FA para reco- nhecer a linguagem especificada por uma RE. A Seção 2.6 mostra um algoritmo que faz o contrário, de um FA para uma RE. Juntas, estas construções estabelecem a equi- valência entre REs e FAs. O fato de que REs são fechadas sob alternação, concatenação e fechamento são críticos para essas construções.

A equivalência entre REs e FAs também sugere outras propriedades de fechamento. Por exemplo, dado um FA completo, podemos construir um FA que reconhece todas as palavras w que não estão em L(FA), chamado complemento de L(FA). Para cons- truir esse novo FA para o complemento, podemos alternar a designação de estados de aceitação e de não aceitação no FA original. Este resultado sugere que REs são fechadas sob complemento. Na verdade, muitos sistemas que usam REs incluem um operador de complemento, como o ^ em lex.

Linguagens regulares

Qualquer linguagem que pode ser especificada por uma expressão regular é chamada linguagem regular.

FA completo

fechamento finito.

Reescreva-a em termos das suas três operações básicas: alternação, concatenação e fechamento.

2. Em PL/I, o programador pode inserir aspas em uma string escrevendo duas aspas em seguida. Assim, a string

seria escrita em um programa PL/I como

Crie uma RE e um FA para reconhecer strings em PL/I. Suponha que as strings come- cem e terminem com aspas e contenham apenas símbolos retirados de um alfabeto, designado como O. As aspas são o único caso especial.

2.4 DA EXPRESSÃO REGULAR AO SCANNER

O objetivo do nosso trabalho com autômatos finitos é automatizar a derivação de scan- ners executáveis a partir de uma coleção de REs. Esta seção desenvolve as construções que transformam uma RE em um FA que seja adequado para implementação direta e um algoritmo que deriva uma RE para a linguagem aceita por um FA. A Figura 2.3 mostra o relacionamento entre todas essas construções.

Para apresentar essas construções, distinguimos entre FAs determinísticos, ou DFAs, e FAs não determinísticos, ou NFAs, na Seção 2.4.1. Em seguida, apresentamos a construção de um FA determinístico a partir de uma RE em três etapas. A construção de Thompson, na Seção 2.4.2, deriva um NFA a partir de uma RE. A construção de subconjunto, na Seção 2.4.3, cria um DFA que simula um NFA. O algoritmo de Hopcroft, na Seção 2.4.4, minimiza um DFA. Para estabelecer a equivalência de REs e DFAs, também precisamos mostrar que qualquer DFA é equivalente a uma RE; a construção de Kleene deriva uma RE a partir de um DFA. Como ela não é simbolizada diretamente na construção do scanner, deixamos esse algoritmo para a Seção 2.6.1.

2.4.1 Autômatos finitos não determinísticos

Lembre-se, da definição de uma RE, que designamos a string vazia, ε, como uma RE. Nenhum dos FAs que montamos à mão incluía ε, mas algumas das REs sim. Qual papel ε tem em um FA? Podemos usar transições em ε para combinar FAs e formar FAs para REs mais complexas. Por exemplo, suponha que tenhamos FAs para as REs m e n, chamadas FAm e FAn, respectivamente.

Podemos montar um FA para mn acrescentando uma transição em ∊ a partir do estado de aceitação de FAm para o estado inicial de FAn, renumerando os estados e usando o estado de aceitação de FAn como estado de aceitação para o novo FA.

Com uma ∈-transição, a definição de aceitação precisa mudar ligeiramente para per- mitir uma ou mais ε-transições entre dois caracteres quaisquer na string de entrada. Por exemplo, em s1, o FA faz a transição s1 →∈ s2 sem consumir qualquer caractere

de entrada. Esta é uma mudança pequena, mas parece intuitiva. A inspeção mostra que podemos combinar s1 e s2 para eliminar a ∊-transição.

A fusão de dois FAs com uma ∈-transição pode complicar nosso modelo de como os FAs funcionam. Considere os FAs para as linguagens a* e ab.

Podemos combiná-los com uma ε-transição para formar um FA para a*ab. s1⟶⟶s2

-transição

Transição sobre a string vazia, ∊, que não avança a entrada.

um FA com transições de caractere exclusivas em cada estado é chamado autômato finito determinístico (DFA — Deterministic Finite Automaton).

Para um NFA fazer sentido, precisamos de um conjunto de regras que descrevam seu comportamento. Historicamente, dois modelos distintos foram dados para o compor- tamento de um NFA.

1. Toda vez que o NFA precisa fazer uma escolha não determinística, ele segue

a transição que leva a um estado de aceitação para a string de entrada, se esta transição existir. Esse modelo, usando um NFA onisciente, é interessante porque mantém (na superfície) o mecanismo de aceitação bem definido do DFA. Basicamente, o NFA escolhe a transição correta em cada ponto.

2. Toda vez que o NFA precisa fazer uma escolha não determinística, ele é clonado

para buscar cada transição possível. Assim, para determinado caractere de entrada, o NFA está em um conjunto específico de estados, tomados a partir de todos os seus clones. Neste modelo, o NFA busca todos os caminhos simultaneamente. Em qual- quer ponto, chamamos o conjunto específico de estados em que o NFA está ativo de sua configuração. Quando ele alcança uma configuração em que esgotou a entrada e um ou mais dos clones alcançaram um estado de aceitação, ele aceita a string. Em qualquer modelo, o NFA (S, O, d, s0, SA) aceita uma string de entrada x1 x2 x3… xk se, e somente se, houver pelo menos um caminho pelo diagrama de transição que começa em s0 e termina em algum sk ∈ SA tal que os rótulos de aresta ao longo do caminho combinem com a string de entrada. (As arestas rotuladas com ∊ são omitidas.) Em outras palavras, o i-ésimo rótulo de aresta precisa ser xi. Esta definição é consistente com qualquer modelo do comportamento do NFA.