Etapa 2: Analisador Léxico Manual

Published

17/03/2026

Modified

17/03/2026

Objetivos

  • Entender o funcionamento interno de um Scanner.
  • Implementar a transformação de um fluxo de caracteres (CharStream) em um fluxo de tokens (TokenStream).
  • Gerenciar autômatos finitos determinísticos (DFA) via código.

Fundamentação Teórica

O Scanner (ou Lexer) é a primeira fase do compilador. Ele lê o arquivo fonte caractere por caractere e agrupa esses caracteres em Tokens. - Lexema: A sequência bruta de caracteres (ex: var, x, 123). - Token: Um par <Tipo, Valor> (ex: <KEYWORD, "var">, <ID, "x">, <NUMBER, "123">).

Observação importante: regex como apoio (sem geradores)

Nesta etapa, não é recomendado usar geradores/ferramentas de lexer como Flex/JFlex/ANTLR.

Você pode usar expressões regulares (regex) como apoio, desde que a lógica principal do scanner continue manual e incremental (um token por chamada de nextToken()).

Permitido (exemplos): - Usar regex para validar um lexema já extraído (ex.: decidir se é IDENTIFIER ou NUMBER). - Usar regex para ajudar a pular whitespace/comentários, desde que você mantenha linha e coluna corretamente.

Não permitido: - Implementar o lexer inteiro apenas com Pattern/Matcher.find() “varrendo tudo” e emitindo tokens sem o comportamento incremental de nextToken(). - Usar uma “regex gigante” que tokenize todo o programa sem que você implemente as regras de prioridade, lookahead, e rastreio de posição.

E se eu insistir em usar JFlex/Flex?

Você até pode usar, mas a correção será feita por testes que assumem o contrato desta etapa (Scanner.nextToken(), Token(type,text), TokenType, erro com linha/coluna, etc.). Um lexer gerado dificilmente encaixa “automaticamente” nesse contrato. Portanto, se você optar por usar JFlex/Flex, você será responsável por adaptar o código/saída para passar nos testes.

Em outras palavras: regex pode ser uma ferramenta auxiliar, mas você ainda precisa implementar o comportamento do scanner (prioridade de tokens, lookahead para :=, <=, >=, atualização de line/column, e tratamento de erro).

Atividades Práticas

1. Implementação do Scanner

Crie um pacote br.com.comcet.tp2. Implemente nele a classe Scanner que recebe o código fonte (String ou File). O método principal é public Token nextToken(). A cada chamada, ele deve:

  1. Ignorar Espaços em Branco: Pular espaços, tabs (, quebras de linha (, e comentários.
  2. Detectar Fim de Arquivo (EOF): Retornar um token especial EOF quando não houver mais caracteres.
  3. Identificar o próximo Token:
    • Se começar com letra: Pode ser um Identificador ou Palavra Reservada. (Dica: Leia até não ser mais letra/dígito, depois consulte um Map de palavras reservadas).
    • Se começar com dígito: É um Número Inteiro (leia enquanto for dígito).
    • Se for um operador (+, -, *, /, (…): Retorne o token correspondente.
    • Atenção: Operadores compostos (:=, <=, >=) exigem olhar um caractere à frente (Lookahead).

2. Tratamento de Erros Léxicos

Se o Scanner encontrar um caractere que não inicia nenhum token válido (ex: @, $ ou _ isolado, dependendo da linguagem), ele deve lançar uma LexicalException. A exceção deve conter: - A mensagem de erro (“Caractere inválido”). - O caractere problemático. - A linha e coluna onde o erro ocorreu.

3. Contrato mínimo (recomendado) da implementação

Para deixar sua implementação mais simples (e testável), é recomendável que o scanner mantenha:

  • Um índice pos para a posição atual no texto.
  • Variáveis line e column (iniciando em 1) para rastrear onde você está.
  • Métodos auxiliares:
    • peek() para ver o próximo caractere sem consumir.
    • advance() para consumir um caractere e atualizar pos/line/column.

Isso facilita lidar com: - Operadores compostos (:=, <=, >=) via lookahead. - Mensagens de erro com linha/coluna. - Ignorar whitespace e comentários.

3. Loop de Teste

Crie um programa MainScanner que: 1. Lê um arquivo .pas. 2. Chama nextToken() num laço while (token.type != EOF). 3. Imprime cada token encontrado no formato: [Tipo, "Lexema"].

4. Testes Automatizados (JUnit)

Além do teste manual com MainScanner, crie ao menos uma classe de teste JUnit pública para validar o comportamento básico do scanner.

Crie, por exemplo, src/test/java/br/com/comcet/tp2/ScannerTest.java:

package br.com.comcet.tp2;

import br.com.comcet.tp1.Token;
import br.com.comcet.tp1.TokenType;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

public class ScannerTest {

    @Test
    void reconheceDeclaracaoSimples() {
        String codigo = "var x : integer;";
        Scanner scanner = new Scanner(codigo);

        Token t1 = scanner.nextToken();
        assertEquals(TokenType.KEYWORD, t1.type());
        assertEquals("var", t1.text());

        Token t2 = scanner.nextToken();
        assertEquals(TokenType.IDENTIFIER, t2.type());
        assertEquals("x", t2.text());

        Token t3 = scanner.nextToken();
        assertEquals(TokenType.DELIMITER, t3.type());
        assertEquals(":", t3.text());

        Token t4 = scanner.nextToken();
        assertEquals(TokenType.KEYWORD, t4.type());
        assertEquals("integer", t4.text());

        Token t5 = scanner.nextToken();
        assertEquals(TokenType.DELIMITER, t5.type());
        assertEquals(";", t5.text());

        Token eof = scanner.nextToken();
        assertEquals(TokenType.EOF, eof.type());
    }
}

Observação: ajuste o pacote/imports conforme você organizar os arquivos das Etapas 1 e 2. O importante é ter: - um tipo Token com métodos type() e text(); - um TokenType com valores como KEYWORD, IDENTIFIER, DELIMITER, NUMBER, EOF; - uma classe Scanner na Etapa 2 com o método nextToken().

Exemplo de Entrada e Saída

Entrada (teste.pas):

var x : integer;
x := 10;

Saída Esperada:

[KEYWORD, "var"]
[IDENTIFIER, "x"]
[DELIMITER, ":"]
[KEYWORD, "integer"]
[DELIMITER, ";"]
[IDENTIFIER, "x"]
[OPERATOR, ":="]
[NUMBER, "10"]
[DELIMITER, ";"]
[EOF, ""]

Dicas de Implementação

  • Use um StringBuilder para acumular caracteres de identificadores e números.

  • Mantenha variáveis currentLine e currentColumn atualizadas a cada caractere lido.

  • Crie um método peek() para ver o próximo caractere sem consumi-lo (útil para :=).

  • Crie um mapa estático para palavras reservadas:

    static final Map<String, TokenType> keywords = new HashMap<>();
    static {
        keywords.put("program", TokenType.PROGRAM);
        keywords.put("var", TokenType.VAR);
        // ...
    }
Back to top