1 análise léxica prof. alexandre monteiro baseado em material cedido pelo prof. euclides arcoverde...

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

Upload: internet

Post on 22-Apr-2015

106 views

Category:

Documents


0 download

TRANSCRIPT

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]

[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)