1
Análise Léxica
Prof. Alexandre Monteiro
Baseado em material cedido pelo Prof. Euclides Arcoverde
Recife
Contatos
Prof. Guilherme Alexandre Monteiro Reinaldo
Apelido: Alexandre Cordel
E-mail/gtalk: [email protected]
Site: http://www.alexandrecordel.com.br/fbv
Celular: (81) 9801-1878
3
Agenda
Definição de Compilador
Etapas da Compilação
Introdução à Análise Léxica
Implementação Manual de um Lexer
Definição de Compilador
5
Compilador
Definição geral:• É um programa que traduz um texto escrito
em uma linguagem computacional (fonte) para um texto equivalente em outra linguagem computacional (alvo)
- Entrada: código fonte (na ling. fonte)- Saída: código alvo (na ling. alvo)
Dependendo do propósito, podem ter nomes específicos
6
Tipos de Compiladores
Assembler ou Montador• Tipo simples de compilador
• A linguagem fonte é uma linguagem assembly (ou linguagem de montagem)
- Correspondência direta com a linguagem de máquina
• A linguagem alvo é uma linguagem de máquina
7
Tipos de Compiladores
Compilador tradicional• Traduz de uma linguagem de alto nível para
uma de baixo nível
• Em muitos casos, o compilador gera código objeto, que é um código de máquina incompleto
- Precisa passar por um linker para virar executável- Exemplo: gcc
• Em outros casos, o compilador gera um código em linguagem de montagem
Etapas da Compilação
9
Etapas da Compilação
Tradicionalmente, os compiladores se dividem em um conjunto de etapas
Mostraremos as cinco etapas básicas a seguir
• Podem existir também outras etapas intermediárias de otimização de código, omitidas na figura
10
Etapas da Compilação
Análise Léxica
Análise Sintática
Analise Semântica
Geração de CódigoIntermediário
Geração de CódigoFinal
11
Fases da Compilação
Fase de Análise
• Subdivide o programa em partes constituintes
• Cria uma estrutura abstrata do programa
Fase de Síntese
• Constrói o programa na linguagem alvo• Gera código final
12
Etapas da Compilação
Front-End(Análise)
Back-End(Síntese)
Análise Léxica
Análise Sintática
Analise Semântica
Geração de CódigoIntermediário
Geração de CódigoFinal
13
Etapas da Compilação
Mas por que essa divisão em etapas?• Modularidade – deixa o compilador mais
legível e mais fácil de manter
• Eficiência – permite tratar mais a fundo cada etapa com técnicas especializadas
• Portabilidade – facilita adaptar um compilador para
- Receber outro código fonte (muda o front-end)- Gerar código para outra máquina (muda o back-end)
14
Etapas da Compilação
Front-End(Análise)
Back-End(Síntese)
Análise Léxica
Análise Sintática
Analise Semântica
Geração de CódigoIntermediário
Geração de CódigoFinal
Introdução à Análise Léxica
16
Análise Léxica
Objetivo
• Ler os caracteres do código fonte agrupando-os de maneira significativa (em lexemas) e classificando esses agrupamentos (em tokens)
Em outras palavras
• Entrada: sequência de caracteres• Saída: sequência de tokens
17
Relembrando...
Lexema: sequência de caracteres com significado interligado
Token: classificação dada ao lexema
• Geralmente retornado junto com o próprio lexema ou outro atributo, como um ponteiro ou um valor numérico associado
18
Relembrando...
Tokens especificados como expressões regulares:
ABRE_PAR → (
FECHA_PAR → )
ATRIB → =
ADD → +
MULT → *
DEF → def
ID → [_a-z][_a-z0-9]*
NUM_INT → [0-9][0-9]*
PT_VG → ;
WHITESPACE → [ \t\n\r]+
19
Análise Léxica
O módulo de software responsável por essa etapa pode ser chamado de:• Analisador Léxico, Lexer ou Scanner
Além de retornar os tokens, ele pode:• Remover espaços em branco• Contar linhas e colunas, para identificar a
localização de erros• Expandir macros
20
Análise Léxica
Existem várias técnicas para construção de um lexer, inclusive técnicas de construção (semi) automática
Porém, iniciaremos vendo como fazer um lexer simples manualmente
Implementação Manual de um Lexer
22
Implementação Manual
Vamos começar implementando tokens em uma linguagem simples, chamada XPR-0
• Linguagem para especificar expressões
• Tokens de 1 caractere apenas
• Sem tratamento de espaços em branco
23
Exemplo de Implementação
Tokens de XPR-0
NUMERO → [0-9]
PT_VIRG → ;
ADD → +
MULT → *
ABRE_PAR → (
FECHA_PAR → )
24
Exemplo de Implementação
Passos para implementar o lexer de XPR-0
• Implementar os tipos dos tokens
• Implementar uma classe para o token
• Implementar o lexer
25
Exemplo de Implementação
Como implementar os tipos de tokens
• Java: criar uma classe separada TokenType- Sugestão: usar “enum” de Java >= 5
• C: usar vários defines, com valores diferentes
• Definir um token especial para indicar fim de arquivo
Exemplo em IDE (Eclipse ou Netbeans)
26
Exemplo de Implementação
Como implementar os tokens• Criar classe Token
• Precisa guardar pelo menos o tipo
• Pode ter outras informações- O lexema- Uma subclassificação- O valor inteiro do token- Etc.
Exemplo em IDE (Eclipse ou Netbeans)
27
Exemplo de Implementação
Como implementar o lexer
• Criar classe Lexer
• Método “nextToken()”
- Lê o próximo caractere, classifica-o e retorna o token
Exemplo em IDE (Eclipse ou Netbeans)
28
Implementação Manual
O exemplo anterior foi bem simples, apenas para dar uma noção de construção de um lexer
Complicações adicionais que podem surgir• Tratamento de espaço em branco• Tokens de vários caracteres• Tokens com prefixos comuns• Diferenciar identificadores de palavras-chave
Melhorando o Lexer Manual
30
Melhorando o Lexer
Espaço em branco
• Fazer um laço para ler todo caractere considerado como espaço em branco
• Analisar antes de qualquer outro token
31
Melhorando o Lexer
Espaços em branco
// ignora os espaços em branco e quebras de linha while (nextChar == ' ' || nextChar == '\t' || nextChar == '\r' || nextChar == '\n') { nextChar = this.readByte(); }
// testar fim de arquivo ...
// testar outros tokens ...
32
Melhorando o Lexer
Tokens de vários caracteres
• Faz uma decisão externa com base no primeiro símbolo
- Usar switch (mais eficiente) ou if-else’s encadeados
• Dentro, faz um laço do-while (ou while)
• Cada símbolo válido deve ser concatenado ao lexema
33
Melhorando o Lexer
Tokens de vários caracteres- Assuma que “lexema” é um objeto StringBuilder
... else if (Character.isDigit(nextChar)) { do { lexema.append((char) nextChar); nextChar = this.readByte(); } while (Character.isDigit(nextChar));
tipo = TokenType.NUMERO; } ...
34
Melhorando o Lexer
Prefixos comuns
• Se tokens de múltiplos caracteres tiverem parte em comum
• Adiar a decisão sobre o tipo e deixa para fazer a decisão num nível mais interno
• Continuar lendo os caracteres e armazenando no lexema até poder decidir
35
Melhorando o Lexer
Prefixos comuns- Exemplo: tokens dos operadores “>=“ e “>”
... else if (nextChar == '>') { nextChar = this.readByte();
if (nextChar == '=') { tipo = TokenType.GREATER_EQ; nextChar = this.readByte(); } else { tipo = TokenType.GREATER; //não precisa ler o próximo char } } ...
36
Melhorando o Lexer
Diferenciando identificadores de palavras-chave
• Ler todo o lexema, como se fosse um identificador
• Depois, compara o lexema inteiro com a lista de palavras-chave
- Pode usar uma tabela hash (Hashtable, em Java)- Adicionar as palavras-chave com seus tipos de token- Após ler o lexema, é só consultar a tabela
• Se não existir palavra-chave para aquele lexema, então é um identificador
37
Hashtable
Estrutura de dados que mapeia chaves (keys) a valores (values)
Classe Hashtable (Java)• Método “put” recebe o par (chave,valor)• Método “get” recebe a chave e retorna o valor• Exemplo: associar “String” com “Integer”
Hashtable numbers = new Hashtable(); numbers.put("one", new Integer(1)); numbers.put("two", new Integer(2));
Integer v = (Integer) numbers.get("one");
38
Melhorando o Lexer
Palavras-chave- Criação da hash
class Lexer { ... private Hashtable keywords;
Lexer() { keywords.put(“if” , TokenType.IF); keywords.put(“else”, TokenType.ELSE); keywords.put(“int” , TokenType.INT); ... }
39
Melhorando o Lexer
Palavras-chave- Reconhecimento dos tokens, em nextToken()
if (Character.isLetter(nextChar)) { do { lexema.append((char)nextChar); nextChar = this.readByte(); } while (Character.isLetter(nextChar));
if (keywords.containsKey(lexema.toString())) { tipo = keywords.get(lexema.toString()); } else { tipo = TokenType.IDENTIFICADOR; } }
Buffers de Leitura
41
Por que usar buffers?
Para tratar situações em que é preciso olhar caracteres à frente• Na leitura de um ID, por exemplo, é preciso
chegar num caractere inválido para parar• Como devolver este último caractere?
Para melhorar a performance• Ler um bloco de caracteres do disco é mais
eficiente do que ler caractere a caractere
42
Buffer Único
Lê um bloco de caracteres do arquivo para um array na memória
• Geralmente, usa-se como tamanho do buffer o tamanho do bloco de disco
• Exemplo: 4096 bytes (4 KB)
43
Buffer Único
t e m p = a
lexemeBegin forward
Variáveis para controlar o buffer• lexemeBegin: início do lexema atual• forward: próximo caractere a ser analisado
pelo lexer O lexema final fica entre lexemeBegin e
forward
44
Buffer Único
Vantagens• Leitura mais eficiente do arquivo (em blocos)• Permite devolver um caractere, retornando o
apontador forward
Porém, o uso de buffer único ainda traz problemas• Lexemas podem ficar “quebrados” no fim do
buffer
45
Buffers Duplos
Dois buffers de mesmo tamanho
• Exemplo: dois buffers de 4kB
Evita que um lexema fique incompleto, desde que tokens não possam passar do tamanho de um buffer
forward
t e m p = a u x * 1 0
lexemeBegin
46
Buffers Duplos
Um buffer é carregado quando o outro já foi completamente lido
A cada leitura de caractere (por meio da variável forward), é preciso testar os limites
• Se chegou ao fim de um buffer, muda para o próximo e recarrega
Esse teste ainda pode ser otimizado...
47
Sentinelas
São caracteres especiais usados para demarcar o fim do buffer• Não precisa testar o fim do buffer a cada
passo, basta testar quando achar esse caractere
Geralmente se usa o mesmo símbolo usado para fim de arquivo – eof
t e m p = a u x * 1 0eof eof
código fonte código fonte
sentinela sentinela
48
Sentinelas
Como diferenciar um sentinela de um fim de arquivo real?• Basta consultar a posição do caractere• Sentinelas ficam em posições fixas no fim do
buffer• Um fim de arquivo real aparece em qualquer
outra posição
t e m p = a u x ; eofeof eof
sentinela sentinelafim de arquivo
49
Sobre Buffers e Sentinelas
São técnicas para quem está muito preocupado com eficiência na compilação
• Não é para quem faz um compilador em Java, é para quem faz em C ou Assembly
Expressões Regulares
51
Expressões Regulares
Tokens: classificação de um lexema• São vistos como unidades atômicas da
linguagem
Para especificar quais lexemas são associados a cada token, podem ser usadas expressões regulares• Cada token é associado a uma expressão
regular
52
Expressões Regulares
Exemplo de especificação dos tokens
ABRE_PAR → (
FECHA_PAR → )
ATRIB → =
ADD → +
MULT → *
DEF → def
ID → [_a-z][_a-z0-9]*
NUM_INT → [0-9][0-9]*
PT_VG → ;
WHITESPACE → [ \t\n\r]+
53
Lexer Manual
Fazer manualmente envolve diversas dificuldades, como já vimos
• Tratar espaços em branco• Tratar tokens de múltiplos caracteres• Diferenciar identificadores de palavras
reservadas• Criação de buffer
Não seria possível criar o lexer automaticamente a partir da especificação?
54
Lexer Automático
Cada expressão regular da especificação deve ser transformada em um reconhecedor
• Recebe uma sequência de caracteres de entrada
• Retorna se ela casa ou não com a expressão
Reconhecedorpalavra (encontrada no código fonte)
sim/não
55
Expressões Regulares
Vimos diversos tipos de expressões regulares
• a*, a+, a{3,5}, a?, (a|b), ab, ...
Todos eles podem ser reduzidos a um conjunto menor de expressões regulares básicas e de operadores sobre elas
56
Expressões Regulares
Expressões básicas• Para representar um caractere “x” qualquer: x• Para representar a palavra vazia: ε
Operadores• ab – concatenação: “a” seguido de “b”• a|b – união: “a” ou “b”• a* – zero ou mais ocorrências de “a”
57
Lexer Automático
É suficiente converter estas expressões básicas e operadores para algum reconhecedor
• Todos os outros operadores poderão serão convertidos a partir destes
Mas que reconhecedor é este? E como converter?
Autômatos Finitos
59
Autômatos Finitos
Formalismo reconhecedor de linguagens• Linguagem: conjunto de palavras
Foram vistos em Teoria da Computação, junto com expressões regulares
São equivalentes às expressões regulares, ou seja, representam as mesmas linguagens
60
Autômatos Finitos
O primeiro tipo que veremos é o autômato finito não-determinístico (AFN ou NFA)
• Seguiremos a definição do livro texto para AFN, porém usando a sigla em inglês NFA
61
NFA
Um estado inicial e vários estados de aceitação
Mudanças entre estados• Pode ler o próximo símbolo da palavra• Ou pode acontecer sem leitura de símbolo: ε
Se existir algum caminho que pare em um estado de aceitação, a palavra é dita “aceita”• Se não houver nenhum caminho, rejeita
62
NFA
Exemplos:
• id -> he | she | his | hers
• Exercício:- comparacao -> < | > | <= | >= | = | <>
0 1 2 8 9
6 7
3 4 2
h e r s
is
s
h e
63
Expressão Regular → NFA
Como vimos em Teoria da Computação, é possível converter de uma expressão regular para um NFA
Veremos
• Conversão de expressões básicas• Conversão de cada operador
64
Expressão Regular → NFA
Conversão de cada expressão regular básica r
65
Expressão Regular → NFA
Operador de concatenação – r1r2
• Converter r1 para um autômato M1 e r2 para M2
• Depois, criar o autômato:
66
Expressão Regular → NFA
Operador de união – r1|r2
• Converter r1 para M1 e r2 para M2
• Depois, criar o autômato:
67
Expressão Regular → NFA
Operador de concatenação sucessiva – r*
• Converter r para M1
• Depois, criar o autômato:
68
Expressão Regular → NFA
Exercício
• Criar NFA para a(b*|c*)
69
NFA
Autômatos não-determinísticos (NFA) permitem múltiplos caminhos para leitura de uma mesma palavra• Pode ser usado, mas é computacionalmente
ineficiente, devido à necessidade de expandir todos os caminhos
O ideal seria uma autômato sem ambiguidades no processo de reconhecimento
70
DFA
Autômato finito determinístico (AFD ou DFA) é um caso especial de NFA
• Sem transições vazias
• Com apenas uma opção de transição para cada caractere/símbolo
• Para todo caractere possível, há uma transição
71
DFA
Exemplo
72
DFA
O caminho de reconhecimento para uma dada palavra é único e bem-definido• Mais simples de implementar o
reconhecimento
As transições podem ser representadas simplesmente como uma tabela• Implementada como um array bidimensional
73
DFA
Exemplo
• Diagrama de estados
• Tabela
74
NFA → DFA
A partir do NFA pode ser feita uma conversão para um DFA por um processo genérico• Cada estado no DFA vai representar um
conjuntos de estados no NFA
O DFA resultante pode ficar com mais estados, mas existe um processo genérico de minimização de estados também
Não veremos esses dois procedimentos...
75
Autômatos
Assim, os autômatos são usados para criar “reconhecedores” a partir de expressões regulares dadas
Mas como usar autômatos para criar um scanner? Como fazer isso automaticamente?
Geradores Automáticos de Lexers
77
Gerador Automático de Lexers
Recebe uma especificação (na forma de regras léxicas)
Como saída, gera o código do lexer
Gerador deAnalisadores Léxicos
RegrasLéxicas Scanner
78
Regras Léxicas
Servem para especificar as expressões regulares e os tokens associados a elas
Na prática, associa a expressão regular com algum código definido pelo usuário
"(" { return new Token(A_PARENT); }")" { return new Token(F_PARENT); }[0-9]+ { return new Token(NUMERO, yytext()); }
* public String yytext(): returns the matched input text region
79
Lexer Gerado
Capaz de tratar automaticamente ambiguidades• Quando um trecho da entrada (lexema) pode
casar com duas ou mais expressões regulares
Duas regras são usadas no caso de ambiguidades • Casar com a expressão regular que reconhece
o maior lexema possível• Se ainda houver mais de uma opção, casa
com a expressão regular que vem primeiro na lista
80
Gerador Automático de Lexer
Discutiremos a seguir dois tópicos relacionados à geração automática de um analisador léxico
• Como gerar o lexer automaticamente
• Como funcionará o lexer gerado
81
Gerando um Lexer...
Converte cada expressão regular (associada a cada token) para um NFA
Une todos os NFAs em um só, criando um estado inicial e usando transições vazias• Um só estado inicial, mas vários estados de
aceitação• Cada estado de aceitação vai indicar o
reconhecimento de um token específico
82
Gerando um Lexer...
Em seguida, converte o NFA combinado para um DFA• Cria estados combinando os estados do NFA
• Se dois estados de aceitação do NFA forem combinados em um estado do DFA, apenas o token definido primeiro será retornado
- Trata a regra “primeiro na lista”
Depois, ainda pode minimizar os estados do DFA por questões de eficiência
83
Exemplo
Seja a seguinte especificação de entrada para o gerador de lexers
Convertendo cada expressão para NFAs...
a { return TOKEN_X; }
abb { return TOKEN_Y; }
a*b+ { return TOKEN_Z; }
84
Exemplo
NFAs para cada expressão regular
85
Exemplo
NFA combinado
86
Exemplo
Conversão para DFA
87
Reconhecimento no Lexer...
Vimos como gerar o autômato para reconhecer as expressões regulares associadas aos tokens
Mas como esse autômato é usado no lexer gerado automaticamente?
88
Reconhecimento no Lexer...
Um DFA pode ser facilmente representado por um array bidimensional• Para cada par (estado, símbolo) indica o
próximo estado
Para simular o funcionamento do autômato é preciso usar uma variável para guardar o estado atual
89
Reconhecimento no Lexer...
A cada chamada da função nextToken(), começa um novo processo de reconhecimento• Reinicia o autômato – volta ao estado inicial• Retoma a leitura de onde parou na última
chamada
Lê o arquivo de entrada, fazendo as transições no autômato para cada caractere• Basta consultar a tabela de transição
90
Reconhecimento no Lexer...
O reconhecimento pára quando não houver mais nenhuma transição possível
• Trata a regra “maior lexema”
O lexer, então, recupera o último estado de aceitação atingido
• Retorna o token associado a esse estado
91
Exemplo Reconhecimento de “abbba” no AFD anterior
• Primeira chamada a nextToken()- Início no estado q0- Lê a → q2q4q7- Lê b → q5q8- Lê b → q6q8- Lê b → q8- Lê a → erro, então volta a q8 e retorna TOKEN_Z
• Segunda chamada a nextToken()- Início no estado q0- Lê a → q2q4q7- Fim da leitura, então retorna TOKEN_X
92
Reconhecimento no Lexer...
Atenção: Não confundam o gerador com o lexer que ele cria
• O gerador lê as regras léxicas, cria um autômato e, então, gera o código fonte do lexer baseado no autômato
• O lexer, depois de compilado, é que vai operar como descrito na última parte da aula
93
Exemplos de Geradores
Para C
• Lex e Flex
Para Java
• JLex e JFlex
Para C#
• C# Lex, C# Flex
94
Bibliografia
AHO, A., LAM, M. S., SETHI, R., ULLMAN, J. D., Compiladores: princípios, técnicas e ferramentas. Ed. Addison Wesley. 2a Edição, 2008 (Capítulo 2)