Lexer (Analizador Léxico)
Archivo: src/lexer.rs
El Lexer (Analizador Léxico) es la primera fase del compilador. Convierte una cadena plana de caracteres del código fuente .fa en un vector estructurado de Tokens. Su diseño maneja comentarios de documentación de tres barras, interpolación de strings compleja con balanceo de delimitadores y detección educativa de errores ortográficos en el código.
Estructura del Lexer
Definición de Lexer
pub struct Lexer {
source: Vec<char>,
pos: usize,
linea: usize,
columna: usize,
tokens_pendientes: Vec<Token>,
} source: Colección indexable de caracteres unicode que componen el código fuente. Se utiliza unVec<char>para evitar problemas de segmentación UTF-8.pos: Cursor del carácter bajo análisis.lineaycolumna: Contadores espaciales para reportar diagnósticos.tokens_pendientes: Búfer FIFO utilizado para encolar tokens generados durante la expansión de expresiones interpoladas dentro de strings.
Breakdown de Funciones y Métodos
pub fn new(source: &str) -> Self
- ¿Por qué?: Inicializa la estructura del analizador léxico.
- ¿Cómo?: Convierte el string
&strde entrada en un vector de caracteresVec<char>e inicializa la línea y columna en 1. - ¿Para qué?: Instanciar el Lexer al inicio del pipeline del compilador.
pub fn tokenize(&mut self) -> Result<Vec<Token>, Vec<ErrorForja>>
- ¿Por qué?: Orquestar y procesar completamente el flujo de caracteres para producir la lista final de tokens.
- ¿Cómo?:
- Ejecuta un bucle infinito que omite espacios y comentarios antes de procesar el siguiente carácter.
- Si alcanza el final de
source, añade un token de fin de archivo (TokenKind::EOF) y sale del bucle. - Llama a
next_token()para extraer el siguiente lexema estructurado. - En caso de errores, los acumula en un vector de fallos en lugar de abortar inmediatamente, lo que permite reportar múltiples errores de una sola vez.
- ¿Para qué?: Método de entrada principal llamado en
lib.rs.
pub fn peek(&self) -> Option<char>
- ¿Por qué?: Inspeccionar el carácter inmediatamente siguiente sin consumirlo.
- ¿Cómo?: Retorna el carácter ubicado en
self.posmediante una lectura segura utilizandoget(). - ¿Para qué?: Lookahead simple necesario en decisiones del Lexer (por ejemplo, para distinguir entre
&y&&).
fn advance(&mut self) -> Option<char>
- ¿Por qué?: Consumir el carácter actual y avanzar los punteros de posición física.
- ¿Cómo?: Incrementa
pos. Si el carácter consumido es un salto de línea (\n), incrementalineay reiniciacolumnaa 1; de lo contrario, simplemente incrementacolumna. - ¿Para qué?: Consumo progresivo del código fuente carácter a carácter.
fn current(&self) -> Option<char>
- ¿Por qué?: Obtener el carácter que el cursor está apuntando en la iteración actual.
- ¿Cómo?: Retorna de forma segura el elemento en la posición
pos. - ¿Para qué?: Evaluar condiciones de control de loops.
fn skip_whitespace(&mut self)
- ¿Por qué?: Omitir caracteres sin semántica para la compilación.
- ¿Cómo?: Llama a
advance()repetidamente mientras el carácter actual sea espacio, tabulación, salto de línea o retorno de carro. - ¿Para qué?: Limpiar el flujo antes de procesar un nuevo token.
fn skip_comentarios(&mut self)
- ¿Por qué?: Ignorar comentarios de código sin perder la capacidad de capturar comentarios de documentación (
///). - ¿Cómo?:
- Comentarios de línea (
//): Si detecta//pero el tercer carácter NO es/, avanza hasta encontrar\no EOF. - Comentarios de bloque (
/* */): Avanza progresivamente hasta encontrar el par de cierre*/.
- Comentarios de línea (
- ¿Para qué?: Limpiar el flujo de comentarios normales.
fn identificar_keyword(&self, palabra: &str) -> TokenKind
- ¿Por qué?: Distinguir si un texto escaneado es una palabra reservada o un identificador de usuario.
- ¿Cómo?: Mapea strings estáticos exactos con variantes del enum
TokenKindmediante pattern matching. Adicionalmente, mapea "soft keywords" comoTexto,Entero,DecimalyBooleanoa identificadores normales para que sean resueltos por el sistema de tipos en el Parser. - ¿Para qué?: Decidir el tipo final de un lexema de texto.
fn leer_identificador_o_keyword(&mut self) -> TokenKind
- ¿Por qué?: Escanear palabras completas compuestas de caracteres alfanuméricos y guiones bajos.
- ¿Cómo?: Avanza y acumula caracteres en un buffer de string mientras cumplan con
is_alphanumeric()o sean_. Luego, evalúa el buffer conidentificar_keyword(). - ¿Para qué?: Extraer variables, nombres de funciones o palabras clave.
fn leer_numero(&mut self) -> TokenKind
- ¿Por qué?: Distinguir números enteros (
i64) de números decimales (f64) con precisión. - ¿Cómo?:
- Acumula dígitos numéricos ascii en un buffer de string.
- Si encuentra un punto decimal (
.) y el carácter siguiente es un dígito, consume el punto y continúa acumulando los dígitos de la parte fraccionaria. - Realiza el parsing a
i64of64según corresponda. Si falla, genera un tokenError.
- ¿Para qué?: Tokenizar literales numéricos aritméticos.
fn leer_caracter(&mut self, linea: usize, columna: usize) -> Result<Option<Token>, ErrorForja>
- ¿Por qué?: Escanear literales de carácter único entre comillas simples (ej:
'a') y secuencias de escape (ej:'\n'). - ¿Cómo?: Consume el carácter. Si es un backslash (
\), resuelve secuencias de escape estándar comon,t,r, o comillas. Si encuentra caracteres adicionales antes de la comilla de cierre, infiere que es una cadena inmutable y la tokeniza comoTokenKind::Textopara evitar pánicos. - ¿Para qué?: Soporte de tipos tipo char en Forja.
fn leer_texto(&mut self) -> Result<TokenKind, ErrorForja>
- ¿Por qué?: Procesar cadenas de texto entre comillas dobles y expandir la interpolación de variables al vuelo.
- ¿Cómo?:
- Si encuentra
$seguido de, interrumpe el buffer del fragmento de texto actual. - Si encuentra un delimitador de interpolación, interrumpe el buffer del fragmento de texto actual.
- Añade el fragmento actual como token de texto en la cola de tokens pendientes.
- Llama a
escanear_expresion_interpolada()para tokenizar todo lo que esté dentro de las llaves. - Continúa procesando el resto de la cadena.
- Devuelve el primer fragmento de texto; el parser luego concatenará los tokens pendientes en tiempo de análisis sintáctico.
- Si encuentra
- ¿Para qué? Soporte de interpolación nativa, permite incluir expresiones dentro de strings.
fn escanear_expresion_interpolada(&mut self) -> Result<(), ErrorForja>
- ¿Por qué?: Tokenizar el código fuente embebido dentro de la interpolación de una cadena.
- ¿Para qué?: Permitir expresiones arbitrarias dentro de
$de forma segura.