• Nenhum resultado encontrado

2.2 COMPILADORES

2.2.2 Analisador Léxico

O analisador léxico constitui a primeira parte do compilador. É também conhecido como scanner por fazer uma varredura caractere a caractere no programa fonte, identificando e agrupando os caracteres em unidades léxicas, os tokens.

Segundo Aho, Sethi & Ullman (1995), o analisador léxico organiza os caracteres do programa fonte em tokens para serem utilizados pelo analisador sintático, de modo que o analisador léxico e o parser formam um par produtor-consumidor, onde o analisador léxico produz tokens e o analisador sintático os consome. Os tokens produzidos podem ser guardados em uma lista de tokens até que sejam consumidos. A lista de tokens controla essa interação, uma vez que o analisador léxico não poderá prosseguir se a lista estiver cheia e o parser não poderá prosseguir com a lista de tokens vazia. Dependendo da implementação, o analisador léxico pode ser acionado pelo analisador sintático, retornando os tokens por demanda. Essa forma de tradução é conhecida como tradução dirigida pela sintaxe.

Além de fazer o agrupamento de tokens, o analisador léxico executa uma série de outras tarefas importantes, como:

Contar as linhas e caracteres – Essa tarefa tem grande importância, visto que a identificação do posicionamento do token no programa fonte é uma informação importante para auxiliar na etapa de correção de erros identificados na compilação.

Remover espaços em branco e comentários – Tendo em vista que na análise léxica todos os caracteres são analisados, espaços em branco (assim como avanços de linha e tabulações) poderiam fazer a análise falhar. Assim, o analisador léxico faz com que esses caracteres sejam ignorados no processo de análise.

Identificar e tratar erros léxicos - Na análise léxica um erro acontece quando o analisador léxico se depara com um lexema (seqüências de símbolos) que não se enquadram em nenhum padrão.

Usualmente, para fazer a definição e o reconhecimento de padrões, o analisador léxico utiliza expressões regulares e autômatos finitos (LOUDEN, 2004).

2.2.2.1 Expressões Regulares

Segundo Aho, Sethi & Ullman (1995), expressões regulares são formalismos denotacionais que estabelecem um modo declarativo – descrição algébrica – de representar sentenças de uma linguagem, sendo também considerados dispositivos geradores, pois a partir de uma expressão regular é possível inferir como gerar sentenças de uma determinada linguagem.

Uma expressão regular r define um padrão de seqüências de símbolos, sendo que esse padrão de sentenças é denominado Linguagem gerada pela expressão regular, sendo denotada por L(r) (LOUDEN, 2004).

Regras de definição de expressões regulares

Segundo Aho, Sethi e Ullman (1995), uma expressão regular sobre o alfabeto Σ é definida pelas regras:

1. ε é uma expressão regular que denota {ε}, isto é, o conjunto que contém a sentença vazia.

2. Se a é um símbolo em Σ, então a é uma expressão regular que denota {a}, isto é, o conjunto contendo a sentença ‘a’3.

3. Se r e s são expressões regulares que denotam as linguagens L(r) e L(s) a) (r) | (s) é uma expressão regular denotando L(r) U L(s);

3 A expressão regular a é diferente da sentença ‘a’ e do símbolo ‘a’.

b) (r)(s) é uma expressão regular denotando L(r) L(s);

c) (r)* é uma expressão regular denotando (L(r))*;

d) (r) é uma expressão regular denotando L(r).

Operações de expressões regulares

De acordo com Louden (2004) há três tipos de operações básicas em expressões regulares:

Escolha entre alternativas (denotada pela barra vertical) Se r e s são expressões regulares, então r | s é uma expressão regular que envolve qualquer sentença que atenda aos padrões de r ou de s. Exemplo: seja uma expressão regular r = a | b, então r corresponde a um dos símbolos ‘a’ oub’; ou seja, L(a | b) = {a, b}.

Concatenação (denotada pela justaposição) Concatena os operandos, onde o operando da direita é concatenado ao final do operando da esquerda. Exemplo: dados os conjuntos de sentenças L(r) = {aa, b} e L(s) = {a, bb}, a concatenação L(rs) seria definida como: L(rs) = L(r)L(s) = {aa, b} {a, bb} = {aaa, aabb, ba, bbb}.

Repetição (denotada pelo asterisco) – A operação de repetição de uma expressão regular é denominada de r*, onde r é uma expressão regular. Essa expressão regular abrange toda concatenação finita de sentenças que atenda aos padrões de r. Exemplo:

a* denota o conjunto de todas as sentenças de zero ou mais a´s, isto é {ε, a, aa, aaa, aaaa, ...}.

Simplificações de expressões regulares

Algumas convenções podem ser adotadas na especificação de uma expressão regular r:

• Uma ou mais ocorrências (+) → r+ = rr*

• Zero ou mais ocorrências (*) → r* = r+ | ε

• Zero ou uma ocorrência (?) → r? = r | ε

• Classe de símbolos [A-Z]= A|B|...|Z

É possível atribuir nomes a uma expressão regular e definir outras expressões regulares utilizando esses nomes. Assim, as expressões regulares podem ser empregadas como se fossem símbolos do alfabeto, denominadas, nessa situação, definições regulares.

Exemplo: sentenças correspondentes aos números constituídos por um ou mais dígitos podem ser denotadas pela expressão regular

(0|1|2|3|4|5|6|7|8|9) (0|1|2|3|4|5|6|7|8|9)*

ou pela definição regular

número => dígito dígito*

onde dígito => 0|1|2|3|4|5|6|7|8|9 é também uma definição regular (Aho, Sethi & Ullman 1995;

LOUDEN, 2004).

2.2.2.2 Autômatos Finitos

Autômatos finitos (AF) ou máquinas de estados são uma forma matemática de descrever tipos particulares de algoritmos que podem ser utilizados no processo de reconhecimento de padrões. Usualmente são empregados na implementação da etapa inicial de construção de um compilador – a análise léxica – que pode ser feita através da compilação de expressões regulares em autômatos finitos (AHO, SETHI e ULLMAN, 1995; JOSÉ NETO, 1987; LOUDEN, 2004).

Um AF é um reconhecedor definido através de uma 5-upla da forma M = (Q, Σ, δ, q0, F)

onde, Q é um conjunto finito não vazio de estados do autômato;

Σ é denominado alfabeto de entrada do autômato e corresponde a um conjunto finito não vazio de símbolos de entrada que compõem a sentença submetida ao autômato para reconhecimento;

δ é uma função de transição de estados do autômato e seu papel é indicar as transições possíveis em cada configuração do autômato.

Essa função mapeia o produto cartesiano Q x (Σ ∪ {ε}) em Q, ou seja, fornece para cada par (estado, símbolo de entrada) um novo estado para onde o autômato deverá mover-se;

q0 é denominado de estado inicial do autômato finito e corresponde a um elemento de Q. É o estado para o qual o reconhecedor deve ser levado antes de iniciar sua operação;

F é um subconjunto de Q e contém todos os estados de aceitação ou estado finais do autômato finito. Esses estados são aqueles que o autômato deve terminar o reconhecimento das sentenças que pertencem à linguagem definida pelo autômato.

O processamento de um autômato finito para o reconhecimento de uma sentença qualquer corresponde à sucessiva aplicação da função transição para cada símbolo da sentença analisada, até que uma condição de parada seja alcançada. Uma sentença é dada como válida quando o processo de reconhecimento termina em um estado dito final (ou de aceitação). Um erro no reconhecimento de uma sentença é caracterizado pelo aparecimento de um símbolo da sentença sem transição associada, ou pelo término do processo de reconhecimento em um estado não final (LOUDEN, 2004).

A Figura 5 ilustra um exemplo de um autômato finito para o reconhecimento de um identificador denotado pela expressão regular:

identificador => letra (letra | dígito) *, onde letra => [A-Z] [a-z]

dígito=> [0-9]

Figura 5. Autômato finito para reconhecimento de um identificador.

Fonte: Louden (2004)

Esse autômato reconhece como identificador qualquer cadeia de caracteres que inicia com uma letra, podendo ser seguido por seqüências de letras e/ou dígitos intercalados onde,

Q = {início, in_id}, Σ = {letra, dígito}, δ (início, letra) = in_id δ (in_id, letra) = in_id δ (in_id, dígito) = in_id q0 = início

F = {in_id}

Os autômatos finitos além de serem representados através de diagramas de transição, como o exemplo da Figura 5, também podem ser representados por meio de tabelas de transição, que para o exemplo anterior assume a configuração expressa na Tabela 1.

Tabela 1. Tabela transição de um autômato finito

Fonte: Adaptado de Louden (2004).

δ letra dígito

→ início in_id -

* in_id in_id in_id

onde o estado inicial é identificado por uma seta () e o(s) estado(s) final(is) é(são) identificado(s) pelo asterisco.

2.2.2.2.1 Tipos de Autômatos Finitos

Os autômatos finitos são classificados em:

• Autômato Finito Determinístico (AFD) – aquele onde o estado seguinte é, exclusivamente, determinado pelo estado corrente e pelo símbolo de entrada (Figura 6).

Figura 6. Autômato Finito Determinístico.

Fonte: Louden (2004)

O autômato da Figura 6 reconhece qualquer sentença que inicie com ‘:’ ou ‘<’ e seja seguido por ‘=’ ou que inicie e termine com o símbolo ‘=’. Um AFD é caracterizado pela existência de, no máximo, um movimento para cada símbolo de entrada da sentença.

• Autômato Finito Não-Determinístico (AFN) – caracterizado pela existência de um conjunto finito de transições que podem ser ativadas a partir do mesmo símbolo de entrada (Figura 7).

Figura 7. Autômato Finito Não-Determinístico.

Fonte: Louden (2004)

O autômato da Figura 7 reconhece qualquer cadeia que comece com o símbolo ‘<’ e seja seguido, ou não, por ‘=’ ou por ‘>’. O não determinismo ocorre quando a partir do estado S1 é lido o símbolo ‘<’, possibilitando transições para os estado S2, S3 ou S4.

• Autômatos Finitos Não-Determinísticos com Movimento Vazio (AFN-ε) – caracterizado pela existência de ε−transição, que é uma transição que pode ocorrer sem que haja consumo de símbolos de entrada (Figura 8).

Figura 8. Autômato Finito Não-Determinístico com Movimento Vazio.

Fonte: Louden (2004)

Esses autômatos oferecem como vantagens a representação da sentença vazia e a possibilidade de expressar alternativas sem fazer uma combinação de estados (LOUDEN, 2004).

O poder de reconhecimento dos AFN é equivalente ao dos AFD, contudo os AFN são mais abrangentes e menos restritivos, enquanto que os AFD, que em geral são mais difíceis de serem especificados, são mais eficientes como reconhecedores de unidades léxicas de uma linguagem de programação (MENEZES, 2002).

Qualquer AFN pode ser convertido para um AFD. O autômato resultante dessa conversão pode apresentar um número consideravelmente maior de estados que o AFN equivalente, exigindo assim, mais espaço para sua representação. Entretanto, é possível encontrar um autômato finito determinístico mínimo (AFDmin) equivalente a qualquer AFD que apresente um número reduzido de estados e ainda assim reconheça as mesmas sentenças aceitas pelo AFD.

Terminada a análise léxica dá-se inicio a segunda fase do processo de compilação que é a análise sintática descrita a seguir.

Documentos relacionados