Análise Léxica

Status: Concluída

A análise léxica (ou scanning) é a primeira fase do compilador. Lê o código-fonte caractere por caractere e agrupa sequências significativas em unidades chamadas tokens.

Dado o trecho:

int x = 42 + y;

O scanner produz:

Lexema Token Categoria
int Int Palavra-chave
x Identifier("x") Identificador
= Equal Operador
42 IntLiteral(42) Literal
+ Plus Operador
y Identifier("y") Identificador
; Semicolon Pontuação

Arquitetura do Scanner

O scanner é implementado na struct Scanner em src/lexer/scanner.rs:

pub struct Scanner {
    pub src: SourceFile,
    pub tokens: Vec<Token>,
    pub diagnostics: Vec<CompilerError>,
    delimiter_stack: Vec<(char, usize, usize)>,
}

SourceFile e Memory Mapping

A leitura do fonte é feita por SourceFile (src/common/input/source.rs) via memory mapping (crate memmap2), evitando cópias desnecessárias de bytes. Em testes, aceita strings diretas:

let src = SourceFile::from_path(PathBuf::from("main.c"))?;
let src = SourceFile::from_string("int x = 42;"); // usado em testes

O SourceFile rastreia três valores de posição:

ByteSpan e Token

Cada token carrega um ByteSpan com os offsets de início e fim, além de line e col:

pub struct ByteSpan { pub start: usize, pub end: usize }

pub struct Token {
    pub kind: TokenKind,
    pub span: ByteSpan,
    pub line: usize,
    pub col: usize,
}

Fluxograma

(Reservado — inserir diagrama das relações entre Scanner, SourceFile, Token e ByteSpan)


Tipos de Token

Todos os tipos são definidos em src/lexer/tokens/token_kind.rs.

Palavras-chave

Categoria Tokens
Controle de fluxo if else while for do switch case default break continue return
Tipos int char float double void struct enum union
Qualificadores / storage const static extern auto register volatile inline typedef signed unsigned short long sizeof

Operadores

Token Lexema Descrição
Plus + Adição
PlusPlus ++ Incremento
PlusEqual += Atribuição com adição
Minus - Subtração
MinusMinus -- Decremento
MinusEqual -= Atribuição com subtração
Arrow -> Acesso via ponteiro
Star * Multiplicação / dereferência
StarEqual *= Atribuição com multiplicação
Slash / Divisão
SlashEqual /= Atribuição com divisão
Equal = Atribuição
EqualEqual == Igualdade
BangEqual != Diferença
Less < Menor que
LessEqual <= Menor ou igual
LessLess << Shift left
LessLessEqual <<= Shift left + atribuição
Greater > Maior que
GreaterEqual >= Maior ou igual
GreaterGreater >> Shift right
GreaterGreaterEqual >>= Shift right + atribuição
Bang ! Negação lógica
AndAnd && E lógico
OrOr \|\| Ou lógico
Ampersand & E bit a bit / endereço
Pipe \| Ou bit a bit
Caret ^ XOR bit a bit
Tilde ~ Complemento bit a bit
Percent % Módulo
Dot . Acesso a membro

Delimitadores e Pontuação

Token Lexema
LeftParen / RightParen ( )
LeftBrace / RightBrace { }
LeftBracket / RightBracket [ ]
Semicolon ;
Comma ,
Colon :

Tokens de valor

Token Valor carregado Exemplos
IntLiteral(i64) Inteiro parsed 42, 0xFF, 0755
FloatLiteral(f64) Float parsed 3.14, 1e-10
StringLiteral(String) Conteúdo sem aspas "hello\n"
CharLiteral(char) Char Rust 'A', '\n'
Identifier(String) Nome do identificador foo, _count
Unknown(char) Char inválido @, $
Eof Fim do arquivo

Processo de Varredura

O método scan() é o ponto de entrada. Executa um loop alternando entre pular espaços/comentários e reconhecer o próximo token:

pub fn scan(&mut self) -> &[Token] {
    while !self.src.is_at_end() {
        self.skip_whitespaces_and_comments();
        if self.src.is_at_end() { break; }
        self.next_token();
    }

    // Delimitadores ainda na pilha = nunca foram fechados
    let unclosed = self.delimiter_stack.drain(..).collect::<Vec<_>>();
    for (c, line, col) in unclosed {
        self.diagnostics.push(/* UnclosedDelimiter(c) */);
    }

    self.emit_at(TokenKind::Eof, "", self.src.line(), self.src.col());
    &self.tokens
}

next_token() lê um caractere e despacha pelo match:

fn next_token(&mut self) {
    let (line, col) = (self.src.line(), self.src.col());
    let c = self.src.advance().unwrap();

    match c {
        '0'..='9'              => self.lex_number(c, line, col),
        '"'                    => self.lex_string(line, col),
        '\''                   => self.lex_char(line, col),
        c if is_ident_start(c) => self.lex_identifier(c, line, col),
        '(' | '[' | '{'        => { /* empilha + emite */ }
        ')' | ']' | '}'        => { /* desempilha + emite */ }
        '+' | '-' | '*' | '/' | '=' | '!' | '<' | '>' | '&' | '|'
                               => self.lex_operator(c, line, col),
        ';' | ',' | ':' | '.' | '%' | '^' | '~'
                               => self.emit_at(/* token simples */),
        c                      => self.emit_unknown(c, line, col),
    }
}

Fluxograma

(Reservado — inserir fluxograma do loop scan() → skip_whitespace() → next_token() → dispatch)


Espaços em Branco e Comentários

skip_whitespaces_and_comments() consome silenciosamente quatro categorias antes de cada token:

fn skip_whitespaces_and_comments(&mut self) {
    loop {
        while matches!(self.src.peek(), Some(' '|'\t'|'\r'|'\n')) {
            self.src.advance();
        }
        if self.src.peek() == Some('/') && self.src.peek_ahead() == Some('/') {
            while !matches!(self.src.peek(), Some('\n') | None) { self.src.advance(); }
            continue;
        }
        if self.src.peek() == Some('/') && self.src.peek_ahead() == Some('*') {
            // consome até '*/' ou EOF com diagnóstico
            continue;
        }
        if self.src.peek() == Some('#') {
            while !matches!(self.src.peek(), Some('\n') | None) { self.src.advance(); }
            continue;
        }
        break;
    }
}

Identificadores e Palavras-chave

Um identificador começa com letra ou _ (is_ident_start), seguido de letras, dígitos ou _ (is_ident_continue). Ao final, lookup_keyword() decide se é keyword ou Identifier:

fn lex_identifier(&mut self, first: char, line: usize, col: usize) {
    let mut buf = String::from(first);
    while let Some(c) = self.src.peek() {
        if is_ident_continue(c) { buf.push(c); self.src.advance(); } else { break; }
    }
    let kind = lookup_keyword(&buf).unwrap_or(TokenKind::Identifier(buf.clone()));
    self.emit_at(kind, &buf, line, col);
}

Design

Cada keyword tem sua própria variante no enum TokenKind (ex: TokenKind::If). Isso elimina comparações de string no parser — o match arm já é o tipo exato.


Literais

Implementado em src/lexer/rules/literals.rs.

Inteiras

Prefixo Base Exemplo Valor
0x / 0X Hexadecimal (16) 0xFF 255
0 + dígito octal Octal (8) 0755 493
nenhum Decimal (10) 42 42

Float

Se após os dígitos decimais o scanner encontrar ., e ou E, a literal é tratada como float. Parte fracionária e expoente (com sinal opcional) são consumidos e parseados como f64.

Forma Exemplo Token
Inteiro 100 IntLiteral(100)
Com ponto 3.14 FloatLiteral(3.14)
Com expoente 1e10 FloatLiteral(1e10)
Expoente com sinal 1.5e-3 FloatLiteral(0.0015)

String e Char

Strings delimitadas por "...", chars por '...'. Ambos suportam sequências de escape (\n, \t, \\, etc.) resolvidas por resolve_escape(). Se o arquivo terminar antes do fechamento, emite UnterminatedLiteral.

Limitação

A versão atual não valida que a literal de char contém exatamente um caractere. Chars multi-char como 'ab' serão tratados na análise semântica.


Operadores

Implementado em src/lexer/rules/operators.rs. Usa lookahead de um caractere para distinguir operadores simples de compostos:

'+' => match self.src.peek() {
    Some('+') => { self.src.advance(); self.emit_at(TokenKind::PlusPlus,  "++", ..); }
    Some('=') => { self.src.advance(); self.emit_at(TokenKind::PlusEqual, "+=", ..); }
    _         =>                       self.emit_at(TokenKind::Plus,       "+",  ..),
},

// Operadores de shift precisam de lookahead duplo
'<' => match self.src.peek() {
    Some('=') => { /* LessEqual */ }
    Some('<') => {
        self.src.advance();
        if self.src.peek() == Some('=') {
            self.src.advance(); // LessLessEqual <<=
        } else {
            // LessLess <<
        }
    }
    _ => { /* Less */ }
},

Comentários e /

// e /* são consumidos em skip_whitespaces_and_comments() antes de next_token(). Quando lex_operator('/') é chamado, só chegam / isolado ou /=.

Fluxograma

(Reservado — inserir árvore de decisão do lookahead por operador)


Delimitadores e Pareamento

O scanner rastreia o pareamento de ( ) [ ] { } com uma pilha interna durante a própria varredura léxica:

'(' => {
    self.delimiter_stack.push(('(', line, col));
    self.emit_at(TokenKind::LeftParen, "(", line, col);
}
')' => {
    if matches!(self.delimiter_stack.last(), Some(('(', _, _))) {
        self.delimiter_stack.pop();
    } else {
        self.emit_unexpected_delimiter(')', line, col);
    }
    self.emit_at(TokenKind::RightParen, ")", line, col);
}

Por que no léxico?

Detectar pareamento no léxico permite mensagens de erro precisas: sabemos exatamente a linha e coluna onde o delimitador foi aberto.


Tratamento de Erros

Erros são coletados em scanner.diagnostics sem interromper a varredura.

Variante Situação Exemplo
InvalidChar(char) Char não reconhecido @, $
UnclosedBlockComment /* sem */ antes do EOF /* comentário...
UnclosedDelimiter(char) Abertura sem fechamento (foo + bar
UnexpectedClosingDelimiter(char) Fechamento sem abertura x + y)
UnterminatedLiteral(String) String/char sem fechamento "hello

Cada erro é convertido em um Report estruturado via o trait ToReport:

error: unclosed delimiter
  --> main.c:3:5
   |
 3 |     (x + y
   |     ^ '(' nao foi fechado
   |
   = help: Adicione o delimitador de fechamento correspondente.

Fluxograma

(Reservado — inserir diagrama de tratamento e recuperação de erros léxicos)