Análise Léxica (parte 1)
“O Senhor olha dos céus para os filhos dos homens, para ver se há alguém que tenha entendimento, alguém que busque a Deus.”
Sumário da Aula
Definição de Compilador
Etapas da Compilação
Introdução à Análise Léxica
“Compilador (geral)”
Definição mais abrangente:
É 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/objeto (na ling. alvo)
Dependendo do propósito, podem ter nomes
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 (def. tradicional)
Traduz de uma linguagem de alto nível para uma de
baixo nível
Em muitos casos, o compilador gera um código de
máquina incompleto
Precisa passar por um linker para virar executável Exemplo: gcc
Em outros casos, o compilador gerar um código em
Tipos de “Compiladores”
Tradutor Fonte-para-Fonte (source-to-source)
Traduz de uma linguagem de alto nível para outra
linguagem de alto nível
Tipos de “Compiladores”
Decompilador
Traduz de baixo nível para alto nível
Faz o inverso de um compilador tradicional
Disassembler
Traduz de código de máquina para linguagem
assembly
Etapas da Compilação
Os livros apresentam uma arquitetura para os
compiladores que os divide em um conjunto de
etapas ou camadas
Mostraremos as cinco etapas básicas a seguir
Etapas de otimização de código (após/durante as
Etapas da Compilação
Análise Léxica Análise Sintática Analise Semântica Geração de Código Intermediário Geração de Código FinalDuas Fases da Compilação
Fase de Análise
Subdivide o programa em partes constituintes Cria uma estrutura abstrata do programa
Feita pelo front-end
Fase de Síntese
Reconstrói o programa na linguagem-alvo Gera código final
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ódigo Intermediário Geração de Código FinalEtapas 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)
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ódigo Intermediário Geração de Código FinalAnálise Léxica
Objetivo
Analisar o código fonte, dividindo-o em trechos
indivisíveis e relevantes para a linguagem (os
lexemas) e classificando esses trechos (em tokens)
Entrada: seqüência de caracteres do código
fonte
Relembrando...
Lexema: seqüência de caracteres que são
considerados indivisíveis na linguagem
Token: classificação dada ao lexema
Geralmente retornado junto com o próprio lexema ou
outro atributo associado ao lexema (como um ponteiro ou um valor numérico)
Relembrando...
Tokens especificados como expressões
regulares:
ABRE_PAR → ( FECHA_PAR → ) EQ → = ADD → + MULT → * DEF → def IDENTIFIER → [_a-z][_a-z0-9]* NUM_LITERAL → [0-9][0-9]* PT_VG → ;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 caracteres de “whitespace” Contar linhas e colunas
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
Implementação Manual de
um Lexer
Implementação Manual
Vamos começar implementando tokens em uma
linguagem simples, chamada Xpress-0
Linguagem para especificar expressões Lexemas/tokens de 1 caractere apenas Sem tratamento de espaços em branco
Exemplo de Implementação
Tokens de Xpress-0
NUM_LITERAL → [0-9] PT_VIRG → ; ADD → + MULT → * ABRE_PAR → ( FECHA_PAR → )Exemplo de Implementação
Vamos dar exemplo em Java
Passos para implementar o lexer
Implementar os tipos dos tokens
Implementar uma classe para o token Implementar o lexer
Exemplo de Implementação
Como implementar os tipos de tokens
Criar um “enum” TokenType
Criar um valor nele para indicar fim de arquivo
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/float/char/etc. ligado ao token Etc.
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 de Implementação
O exemplo completo pode ser baixado do site
http://sites.google.com/site/profpablosampaio
Implementação Manual
O exemplo anterior foi bem simples, para iniciar
Questões mais avançadas que costumam ser
tratadas por um lexer real:
Tratamento de espaço em branco Tokens de vários caracteres
Tokens com prefixos comuns
Melhorando o Lexer
Tratando espaços em branco (whitespaces)
Fazer um laço para ler todo caractere considerado
como espaço em branco
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 aqui...
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
Tokens de Vários Caracteres
Considere 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
Prefixos Comuns
Exemplo: 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
Tratando palavras-chaves
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,
Hash Table
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));
Tratando Palavras-Chaves
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); ... }
Tratando Palavras-Chaves
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; } }
Alternativa
Ao invés de uma tabela Hash, pode ser usada
qualquer outra estrutura que funcione como um
dicionário
Pares chave-valor A chave é uma string
Sugestão
Como exercício, sugiro que tentem melhorar o
projeto do site (Xpress-0)
Faça o lexer da linguagem que chamo de
Xpress-1
Tem caracteres brancos
Xpress-1
Especificação dos tokens
ABRE_PAR → ( FECHA_PAR → ) ADD → + MULT → * NUM_LITERAL → [0-9]+ PT_VG → ; WHITESPACE → [ \t\n\r]+
Passadas
É desejável que o lexer rode estritamente em
uma passada
Cada caractere do código fonte é lido uma única vez A menos, talvez, do tratamento das palavras
reservadas
Exemplo ruim:
Buffers
O livro ensina uma técnica para carregar o
arquivo, por partes, em buffers
Hoje em dia, não é necessário
As bibliotecas de arquivos provavelmente já fazem
t e m p = a eof u x * 1 0 eof
código fonte código fonte
Conclusão