Arquitectura y Manejo de Estado
El motor léxico principal en gram.core.lexer está estructurado sobre un diseño limpio de dos clases:
LexerState(gram.core.lexer.control): El gestor de estado fundamental. Hereda deDiagnosticsy gestiona las líneas fuente, las coordenadas del cursor físico (lineycol), la pila de indentación semántica (indents), el colector de comentarios (comments) y la resolución dinámica de configuraciones.Lexer(gram.core.lexer.core): La fachada de alto nivel. Hereda deLexerStatee implementa el bucle de escaneo principalprocess(), la validación de condiciones previas del catálogo (check_state()) y métodos de fábrica (tokenize(),stream()).
1. Carga de Código Fuente (_load_source)
Lexer acepta código fuente en tres formatos distintos:
from pathlib import Path
from gram.core.lexer import Lexer, words
# Siempre asegúrate de registrar al menos una palabra clave antes de ejecutar el lexer
words.add_keyword("fn", allow_override=True)
# 1. Desde una cadena directa en memoria
lexer_str = Lexer("fn principal() { return 0; }")
# 2. Desde una secuencia o lista de líneas
lexer_lines = Lexer(["fn calcular(x) {", " return x * 2;", "}"])
# 3. Desde un objeto pathlib.Path en disco
source_path = Path("ejemplos/script.gram")
lexer_archivo = Lexer(source_path)
Semántica de Carga
| Tipo de Entrada | Detalles de Procesamiento | Manejo de Errores |
|---|---|---|
str |
Se divide en líneas individuales mediante el método nativo .splitlines() de Python. |
Ninguno. Una cadena vacía produce una lista vacía de líneas que resulta en un único Token.EOF. |
Sequence[str] |
A cada elemento de la secuencia se le eliminan los saltos de línea finales \r y \n mediante .rstrip('\r\n'). |
Conserva espacios en blanco iniciales y líneas vacías para el cálculo de indentación. |
Path |
Valida que el archivo exista en disco. Lee el archivo con codificación UTF-8 y lo divide en líneas. Configura además Format.FileTar para trazas de error. |
Si el archivo no existe, registra un fallo en diagnósticos y lanza LexerError (LEXER_FILE_NOT_FOUND). |
2. Coordenadas del Cursor y Primitivas de Navegación
LexerState mantiene punteros de posición para líneas y caracteres:
self.line(int): Índice de línea actual (base 0 internamente). Al emitirse dentro de unTokenTypeo al reportarse en errores de usuario, se presenta en base 1 (self.line + 1).self.col(int): Desplazamiento de columna del carácter actual en la línea activa (índice crudo en base 0).self.source(list[str]): Lista de cadenas con las líneas físicas cargadas.
API de Navegación
| Método / Propiedad | Tipo de Retorno | Descripción |
|---|---|---|
lexer.currline |
str |
Retorna self.source[self.line] si self.line está en rango; en caso contrario, retorna "". |
lexer.has_lines() |
bool |
Retorna True si quedan líneas pendientes (self.line < len(self.source)). |
lexer.is_empty() |
bool |
Retorna True si self.currline.strip() == "". |
lexer.peek(offset=0) |
str |
Inspecciona el carácter en self.col + offset de la línea actual sin desplazar el cursor. Retorna "" si la columna queda fuera de rango. |
lexer.advance(steps=1) |
str |
Retorna el carácter bajo el cursor actual (peek()) e incrementa self.col en steps. |
# Ejemplo de inspección manual durante un escaneo personalizado
lexer = Lexer("alpha := 10")
print(lexer.currline) # "alpha := 10"
print(lexer.peek()) # "a"
print(lexer.peek(1)) # "l"
caracter = lexer.advance(5) # Consume "alpha", retorna "a"
print(lexer.col) # 5
print(lexer.peek()) # " "
3. Configuración Dinámica y Enlace de Catálogos
Las instancias de LexerState pueden ejecutarse de manera global (leyendo de gram.config y del catálogo global words.MAP_KEYWORDS) o encapsuladas dentro de una instancia de Session. LexerState abstrae esta dualidad mediante métodos de acceso uniformes:
# Patrón de resolución interna en LexerState
def get_config(self, name: str, default=None):
if self.session is not None:
return self.session.config.get(name, getattr(config, name, default))
return getattr(config, name, default)
def get_keyword(self, name: str):
if self.session is not None:
return self.session.get_keyword(name)
from gram.core.lexer import words
return words.get_keyword(name)
def get_symbols(self):
if self.session is not None:
return self.session.symbols
return tokens.MAP_SYMBOLS
Esto garantiza que un lexer configurado dentro de una sesión nunca contaminará palabras clave, símbolos o banderas de configuración hacia el estado global u otros hilos de ejecución.
4. Validación de Precondiciones: check_state()
Antes de iniciar el bucle de escaneo léxico, Gram ejecuta check_state(self.session).
def check_state(session=None) -> int:
count = session.keyword_count() if session is not None else words.count_keywords()
if count == 0:
errors.LexerError(
"EmptyKeywords",
errors.LEXER_EMPTY_KEYWORDS,
"No se encontró ninguna palabra clave registrada.",
"Registre al menos una palabra clave mediante words.add_keyword() antes de ejecutar el Lexer.",
).raise_error()
return count
[!IMPORTANT] Gram exige al menos una palabra clave registrada antes de ejecutar el análisis léxico. Si se llama a
Lexer(source).process()con un catálogo vacío, se lanza inmediatamente unLexerErrorcon códigoLEXER_EMPTY_KEYWORDS. Si tu DSL no posee aún palabras clave reservadas, registra una palabra comodín temporal:words.add_keyword("dummy", allow_override=True)
5. El Bucle Principal de Escaneo (process())
Invocar Lexer.process() ejecuta la tubería de análisis en una sola pasada:
def process(self) -> list[TokenType]:
Diagrama de Flujo de Ejecución
Llamada a process()
│
▼
check_state(self.session)
│
▼
Inicializar tokens_list: list[TokenType]
│
┌──────────────┴──────────────┐
▼ │
while self.has_lines(): │
│ │
¿Línea vacía? ───► SÍ ───► Emitir NEWLINE (si no se ignoran)
│ Avanzar línea, reset col = 0
▼ NO │
¿Sólo comentario? ──► SÍ ──► Guardar comentario (si está habilitado)
│ Avanzar línea, reset col = 0
▼ NO (NO modifica la pila indents)
1. process_indent() ──► Agregar tokens INDENT / DEDENT
│
2. while self.col < len(currline):
│
├─► char en " \r\t" ──► advance()
├─► multiline_match() ──► process_multiline()
├─► is_comment_start() ──► process_comment()
└─► visitors.visit(self, char) ──► tokens_list.append(token)
│
3. Emitir NEWLINE (si LEXER_IGNORE_NEWLINES es False)
Avanzar línea, reset col = 0
│
└──────────────┬──────────────┘
│
(Fin de todas las líneas)
▼
4. Desenrollar indentación pendiente:
while len(self.indents) > 1:
emitir Token.DEDENT
│
▼
5. Agregar token terminal Token.EOF
│
▼
return tokens_list
Desglose Paso a Paso
- Aserción Previa: Ejecuta
check_state(). Si no se detectan keywords registradas, la ejecución se detiene de inmediato. - Enmarcado de Diagnósticos: Si
LEXER_ADD_INFOestá activo, crea y anida un nodo de telemetría"LexingProcess"en la pila deDiagnostics. - Comprobación de Líneas Vacías: Si
self.currline.strip() == "":- Si
LEXER_IGNORE_EMPTY_LINES=FalseyLEXER_IGNORE_NEWLINES=False, agrega unTokenType(Token.NEWLINE, None, line, 0). - Salta inmediatamente a la siguiente línea (
self.line += 1,self.col = 0) sin alterar los niveles de indentación.
- Si
- Líneas Puras de Comentarios: Si una línea sólo contiene espacios seguidos de un marcador de comentario unilínea, el comentario es extraído, almacenado en
self.commentssi corresponde, y la línea avanza. Las líneas de sólo comentarios nunca alteran la pila de indentación. - Cálculo de Indentación: Llama a
self.process_indent(). Los espacios y tabuladores iniciales se convierten a un ancho visual de columna. Si el nivel crece, se emiteToken.INDENT; si decrece, se emite uno o másToken.DEDENT. - Escaneo de Caracteres: El bucle interno procesa de izquierda a derecha la cadena
self.currline:- Los espacios en blanco en línea (
' ','\t','\r') se avanzan e ignoran. - Los delimitadores multilínea registrados (
""",'''o personalizados víaadd_multline) tienen máxima prioridad y pueden consumir caracteres a través de múltiples líneas físicas. - Los comentarios unilínea (
#,//) o de bloque (/*) consumen el contenido restante y continúan o rompen la línea física. - Cualquier otro carácter se delega al despachador
visitors.visit(self, char).
- Los espacios en blanco en línea (
- Terminación de Línea: Si
LEXER_IGNORE_NEWLINES=False, se agrega unTokenType(Token.NEWLINE, None, line, col)antes de pasar a la siguiente línea. - Desenrollado de Indentación al Final (EOF): Una vez procesadas todas las líneas, cualquier bloque de indentación que permanezca abierto en la pila (
len(self.indents) > 1) se desapila, emitiendo los tokensToken.DEDENTde cierre correspondientes. - Centinela Final EOF: Emite siempre
TokenType(Token.EOF, None, line, col)como último token garantizado de la secuencia.
6. Métodos Auxiliares y Fábricas
Además de process(), Lexer proporciona dos utilidades de alto nivel:
tokenize(source, logfile=None) -> list[TokenType]
Crea una nueva instancia de Lexer compartiendo la sesión y configuración activas, tokenizando source sin alterar el estado del objeto lexer actual:
lexer = Lexer("fn primero()", session=mi_sesion)
tokens1 = lexer.process()
# Tokeniza otra fuente usando exactamente la misma configuración de sesión:
tokens2 = lexer.tokenize("fn segundo()")
stream() -> TokenStream
Ejecuta process() y envuelve la lista resultante de tokens directamente en una instancia navegable de TokenStream:
from gram.core.lexer import Lexer, TokenStream
stream: TokenStream = Lexer("let x = 42;").stream()
print(stream.current()) # TokenType(token=Token.KEYWORD, value='let', line=1, col=0)