análise léxica

94
1 Análise Léxica Prof. Alexandre Monteiro Baseado em material cedido pelo Prof. Euclides Arcoverde Recife

Upload: derick

Post on 05-Jan-2016

88 views

Category:

Documents


3 download

DESCRIPTION

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] [email protected] - PowerPoint PPT Presentation

TRANSCRIPT

Page 1: Análise Léxica

1

Análise Léxica

Prof. Alexandre Monteiro

Baseado em material cedido pelo Prof. Euclides Arcoverde

Recife

Page 2: Análise Léxica

Contatos

Prof. Guilherme Alexandre Monteiro Reinaldo

Apelido: Alexandre Cordel

E-mail/gtalk: [email protected]

[email protected]

Site: http://www.alexandrecordel.com.br/fbv

Celular: (81) 9801-1878

Page 3: Análise Léxica

3

Agenda

Definição de Compilador

Etapas da Compilação

Introdução à Análise Léxica

Implementação Manual de um Lexer

Page 4: Análise Léxica

Definição de Compilador

Page 5: Análise Léxica

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

Page 6: Análise Léxica

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

Page 7: Análise Léxica

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

Page 8: Análise Léxica

Etapas da Compilação

Page 9: Análise Léxica

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

Page 10: Análise Léxica

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

Page 11: Análise Léxica

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

Page 12: Análise Léxica

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

Page 13: Análise Léxica

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)

Page 14: Análise Léxica

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

Page 15: Análise Léxica

Introdução à Análise Léxica

Page 16: 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

Page 17: Análise Léxica

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

Page 18: Análise Léxica

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]+

Page 19: Análise Léxica

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

Page 20: Análise Léxica

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

Page 21: Análise Léxica

Implementação Manual de um Lexer

Page 22: Análise Léxica

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

Page 23: Análise Léxica

23

Exemplo de Implementação

Tokens de XPR-0

NUMERO → [0-9]

PT_VIRG → ;

ADD → +

MULT → *

ABRE_PAR → (

FECHA_PAR → )

Page 24: Análise Léxica

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

Page 25: Análise Léxica

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)

Page 26: Análise Léxica

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)

Page 27: Análise Léxica

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)

Page 28: Análise Léxica

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

Page 29: Análise Léxica

Melhorando o Lexer Manual

Page 30: Análise Léxica

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

Page 31: Análise Léxica

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 ...

Page 32: Análise Léxica

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

Page 33: Análise Léxica

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; } ...

Page 34: Análise Léxica

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

Page 35: Análise Léxica

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 } } ...

Page 36: Análise Léxica

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

Page 37: Análise Léxica

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");

Page 38: Análise Léxica

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); ... }

Page 39: Análise Léxica

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; } }

Page 40: Análise Léxica

Buffers de Leitura

Page 41: Análise Léxica

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

Page 42: Análise Léxica

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)

Page 43: Análise Léxica

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

Page 44: Análise Léxica

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

Page 45: Análise Léxica

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

Page 46: Análise Léxica

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...

Page 47: Análise Léxica

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

Page 48: Análise Léxica

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

Page 49: Análise Léxica

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

Page 50: Análise Léxica

Expressões Regulares

Page 51: Análise Léxica

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

Page 52: Análise Léxica

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]+

Page 53: Análise Léxica

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?

Page 54: Análise Léxica

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

Page 55: Análise Léxica

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

Page 56: Análise Léxica

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”

Page 57: Análise Léxica

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?

Page 58: Análise Léxica

Autômatos Finitos

Page 59: Análise Léxica

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

Page 60: Análise Léxica

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

Page 61: Análise Léxica

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

Page 62: Análise Léxica

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

Page 63: Análise Léxica

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

Page 64: Análise Léxica

64

Expressão Regular → NFA

Conversão de cada expressão regular básica r

Page 65: Análise Léxica

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:

Page 66: Análise Léxica

66

Expressão Regular → NFA

Operador de união – r1|r2

• Converter r1 para M1 e r2 para M2

• Depois, criar o autômato:

Page 67: Análise Léxica

67

Expressão Regular → NFA

Operador de concatenação sucessiva – r*

• Converter r para M1

• Depois, criar o autômato:

Page 68: Análise Léxica

68

Expressão Regular → NFA

Exercício

• Criar NFA para a(b*|c*)

Page 69: Análise Léxica

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

Page 70: Análise Léxica

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

Page 71: Análise Léxica

71

DFA

Exemplo

Page 72: Análise Léxica

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

Page 73: Análise Léxica

73

DFA

Exemplo

• Diagrama de estados

• Tabela

Page 74: Análise Léxica

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...

Page 75: Análise Léxica

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?

Page 76: Análise Léxica

Geradores Automáticos de Lexers

Page 77: Análise Léxica

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

Page 78: Análise Léxica

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

Page 79: Análise Léxica

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

Page 80: Análise Léxica

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

Page 81: Análise Léxica

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

Page 82: Análise Léxica

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

Page 83: Análise Léxica

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; }

Page 84: Análise Léxica

84

Exemplo

NFAs para cada expressão regular

Page 85: Análise Léxica

85

Exemplo

NFA combinado

Page 86: Análise Léxica

86

Exemplo

Conversão para DFA

Page 87: Análise Léxica

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?

Page 88: Análise Léxica

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

Page 89: Análise Léxica

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

Page 90: Análise Léxica

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

Page 91: Análise Léxica

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

Page 92: Análise Léxica

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

Page 93: Análise Léxica

93

Exemplos de Geradores

Para C

• Lex e Flex

Para Java

• JLex e JFlex

Para C#

• C# Lex, C# Flex

Page 94: Análise Léxica

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)