960 likes | 1.2k Views
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 : alexandrecordel@gmail.com greinaldo@fbv.edu.br
E N D
Análise Léxica Prof. AlexandreMonteiro Baseadoem material cedidopelo Prof. EuclidesArcoverde Recife
Contatos • Prof. Guilherme Alexandre Monteiro Reinaldo • Apelido: Alexandre Cordel • E-mail/gtalk: alexandrecordel@gmail.com greinaldo@fbv.edu.br • Site: http://www.alexandrecordel.com.br/fbv • Celular: (81) 9801-1878
Agenda • Definição de Compilador • Etapas da Compilação • Introdução à Análise Léxica • Implementação Manual de um Lexer
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
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
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 • 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
Análise Léxica Análise Sintática Analise Semântica Geração de Código Intermediário Geração de Código Final Etapas da Compilação
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
Análise Léxica Análise Sintática Analise Semântica Geração de Código Intermediário Geração de Código Final Etapas da Compilação Front-End (Análise) Back-End (Síntese)
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)
Análise Léxica Análise Sintática Analise Semântica Geração de Código Intermediário Geração de Código Final Etapas da Compilação Front-End (Análise) Back-End (Síntese)
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
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
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]+
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
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 • 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
Exemplo de Implementação • Tokens de XPR-0 NUMERO →[0-9] PT_VIRG→; ADD →+ MULT →* ABRE_PAR→ ( FECHA_PAR→)
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
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)
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)
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)
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 • Espaço em branco • Fazer um laço para ler todo caractere considerado como espaço em branco • Analisar antes de qualquer outro token
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 ...
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
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; } ...
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
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 } } ...
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
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");
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); ... }
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; } }
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
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)
t e m p = a Buffer Único • 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 lexemeBegin forward
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
t u e x m p * = 1 0 a 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 lexemeBegin forward
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...
t e m p = a eof u x * 1 0 eof 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 código fonte código fonte sentinela sentinela
t e m p = a eof u x ; eof eof 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 sentinela fim de arquivo sentinela
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