Etapa 2: Analisador Léxico Manual
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 deline/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:
- Ignorar Espaços em Branco: Pular espaços, tabs (, quebras de linha (, e comentários.
- Detectar Fim de Arquivo (EOF): Retornar um token especial
EOFquando não houver mais caracteres. - 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
pospara a posição atual no texto. - Variáveis
lineecolumn(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 atualizarpos/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
Tokencom métodostype()etext(); - umTokenTypecom valores comoKEYWORD,IDENTIFIER,DELIMITER,NUMBER,EOF; - uma classeScannerna Etapa 2 com o métodonextToken().
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
StringBuilderpara acumular caracteres de identificadores e números.Mantenha variáveis
currentLineecurrentColumnatualizadas 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); // ... }