Literales y Visitors Especializados
Gram utiliza un patrón de despacho modular por visitors alojado en el subpaquete gram.core.lexer.visitors. En lugar de ejecutar una expresión regular monolítica sobre todo el archivo de entrada, Lexer inspecciona el carácter actual bajo el cursor y delega el control a módulos visitor altamente especializados.
1. Arquitectura del Despachador Principal (visitors.visit)
Siempre que el bucle de Lexer.process() encuentra texto que no es espacio en blanco, ni comentario, ni delimitador multilínea, delega la tokenización a visitors.visit(lexer, char):
def visit(lexer: LexerState, char: str) -> TokenType:
Orden de Precedencia del Despacho
Carácter bajo el cursor
│
├─► ¿Cadena con prefijo? (L"...", u"...", U"...", u8"...")
│ (si LEXER_SUPPORT_PREFIXED_STRINGS es True)
│ └──► DESPACHAR A: strings.process(..., prefix=..., start_override=...)
│
├─► ¿Comillas? (' o ")
│ └──► DESPACHAR A: strings.process()
│
├─► ¿Dígito (0-9) O Punto inicial (ej. .5)?
│ (El punto inicial se evalúa si LEXER_SUPPORT_LEADING_DOT_NUMBERS es True)
│ └──► DESPACHAR A: numbers.process()
│
├─► ¿Carácter alfabético o guion bajo ([a-zA-Z_])?
│ └──► DESPACHAR A: words.process()
│
└─► Fallback (Operadores, Delimitadores, Puntuación, Unicode)
└──► DESPACHAR A: symbols.process()
Cuando la telemetría de diagnósticos está activa (LEXER_ADD_INFO=True), visit() anida un nodo de ejecución en el árbol (VISITOR-STRING, VISITOR-NUMBER, VISITOR-WORD o VISITOR-SYMBOL) para registrar la actividad del visitor, cerrando el nodo dentro de un bloque finally: lexer.down().
2. Literales Numéricos (visitors.numbers)
El visitor de números de Gram procesa literales enteros y de punto flotante compatibles con C, con soporte total para bases numéricas alternativas, notación científica, separadores de dígitos y sufijos de tipo.
Literales Numéricos Soportados
| Categoría | Sintaxis de Ejemplo | Token Emitido | Tipo de value Evaluado |
|---|---|---|---|
| Entero Decimal | 42, 100_000 |
Token.NUMBER |
int (42, 100000) |
| Base Binaria | 0b1010, 0B1100_0011 |
Token.NUMBER |
int (10, 195) |
| Base Octal | 0o755, 0O644 |
Token.NUMBER |
int (493, 420) |
| Entero Hexadecimal | 0x1A2F, 0XDEAD_BEEF |
Token.NUMBER |
int (6703, 3735928559) |
| Flotante Decimal | 3.14159, 1.5e-4, 2E+3 |
Token.DECIMAL |
float (3.14159, 0.00015, 2000.0) |
| Flotante con Punto Inicial | .5, .125e2 |
Token.DECIMAL |
float (0.5, 12.5) |
| Flotante Hexadecimal | 0x1.2p3, 0x.4p-1 |
Token.DECIMAL |
float (9.0, 0.125) |
| Entero con Sufijos | 42UL, 0x10ULL, 100u |
Token.NUMBER |
str ("42UL", "0x10ULL", "100u") |
| Flotante con Sufijos | 1.0f, 2.5L, 0x1.fp2f |
Token.DECIMAL |
str ("1.0f", "2.5L", "0x1.fp2f") |
Reglas e Invariantes Clave de Análisis
1. Distinción de Tokens: Token.DECIMAL vs. Token.NUMBER
- Cualquier literal numérico que contenga un punto decimal (
.), un exponente binario (p/Pen flotantes hexadecimales) o un exponente decimal (e/Een números decimales) se clasifica estrictamente comoToken.DECIMAL. - Todos los enteros convencionales y literales con base (
0b,0o,0x) se clasifican comoToken.NUMBER.
2. Contrato de Preservación de Sufijos
Cuando los literales numéricos incluyen sufijos de tipo al estilo C (u, U, l, L, f, F):
- Números sin sufijo: Se convierten directamente a tipos primitivos
intofloatde Python para su uso inmediato. - Números con sufijo: Conservan su representación exacta en cadena de texto (ej.
"1.0f"). Esto permite que las fases semánticas posteriores o chequeadores de tipos distingan con precisiónint,unsigned long,floatydoublesin tener que volver a escanear el código fuente.
3. Validación de Separadores por Guion Bajo (_has_invalid_separator)
Los guiones bajos (_) pueden utilizarse como separadores visuales de dígitos, pero no pueden ubicarse en posiciones inválidas:
1_000_000── Válido._123── Se escanea comoToken.IDENT("_123"), no como número.123_── Separador final inválido. EmiteToken.ERRORy reportaLEXER_INVALID_NUMBER.1__2── Separadores consecutivos inválidos. EmiteToken.ERROR.0x_12── Separador inválido tras el prefijo de base. EmiteToken.ERROR.
4. Precedencia de Base Hexadecimal
Dado que los dígitos hexadecimales incluyen e, E, f y F, el visitor evalúa los literales hexadecimales antes de verificar exponentes decimales o sufijos flotantes. Esto evita que literales válidos como 0xface o 0x1f se interpreten erróneamente como números científicos corruptos.
3. Cadenas y Literales Multilínea (visitors.strings)
El visitor de cadenas procesa cadenas unilínea entre comillas, literales con prefijo y docstrings multilínea.
Cadenas Unilínea ('...' y "...")
Delimitadas por comillas simples o dobles equivalentes:
from gram.core.lexer import Lexer, Token, flags, words
words.add_keyword("dummy", allow_override=True)
tokens = Lexer("'hola' \"mundo\"").process()
print(tokens[0]) # TokenType(token=Token.STRING, value='hola', flags=SIMPLE_QUOTE)
Resolución de Secuencias de Escape (process_escape)
Las secuencias de escape estándar de C/Python se decodifican automáticamente:
| Secuencia de Escape | Carácter Decodificado | Significado |
|---|---|---|
\n |
"\n" |
Salto de línea |
\r |
"\r" |
Retorno de carro |
\t |
"\t" |
Tabulador horizontal |
\b |
"\b" |
Retroceso (Backspace) |
\f |
"\f" |
Salto de página (Form feed) |
\v |
"\v" |
Tabulador vertical |
\0 |
"\0" |
Byte nulo |
\\ |
"\\" |
Barra invertida literal |
\" |
"\"" |
Comilla doble escapada |
\' |
"\'" |
Comilla simple escapada |
\uXXXX |
chr(0xXXXX) |
Punto de código Unicode hex de 4 dígitos (ej. \u00A9 -> ©) |
Cualquier carácter de escape no reconocido \c se conserva como la secuencia literal "\c".
Cadenas con Prefijos (Estilo C)
Cuando config.LEXER_SUPPORT_PREFIXED_STRINGS=True, Gram reconoce los prefijos antepuestos a las comillas:
L"..."/L'...'── Cadena / carácter ancho (wide).u"..."/u'...'── Cadena UTF-16.U"..."/U'...'── Cadena UTF-32.u8"..."/u8'...'── Cadena UTF-8.
El prefijo detectado se guarda directamente en token.description:
tokens = Lexer('L"texto ancho" u8"texto utf8"').process()
print(tokens[0].description) # "L"
print(tokens[0].value) # "texto ancho"
print(tokens[1].description) # "u8"
Cadenas Multilínea y Docstrings ("""...""" y '''...''')
Cuando config.LEXER_SUPPORT_DOCSTRINGS=True, los bloques entre comillas triples abarcan múltiples líneas físicas:
codigo = '''"""
Línea 1
Línea 2
"""'''
tokens = Lexer(codigo).process()
print(tokens[0].value) # "\nLínea 1\nLínea 2\n"
El escaneo multilínea actualiza lexer.line y lexer.col dinámicamente mientras conserva el start_line y columna inicial original para el TokenType generado.
Delimitadores Multilínea Personalizados (add_multline)
Puedes registrar pares de apertura y cierre arbitrarios mediante gram.core.lexer.multiline:
from gram.core.lexer import Lexer, add_multline, list_multline, remove_multline, words
words.add_keyword("dummy", allow_override=True)
# Registrar un delimitador personalizado de bloque crudo:
add_multline("<<<", ">>>")
tokens = Lexer("<<<contenido\nmultilínea\ncrudo>>>").process()
print(tokens[0].value) # "contenido\nmultilínea\ncrudo"
# Limpieza posterior
remove_multline("<<<")
4. Palabras, Keywords y Booleanos (visitors.words)
El visitor de palabras procesa secuencias contiguas de caracteres alfanuméricos y guiones bajos ([a-zA-Z_][a-zA-Z0-9_]*), reguladas por ident_underscores.
Control de Guiones Bajos Iniciales en Identificadores (ident_underscores)
Gram permite controlar con total precisión cómo se gestionan los guiones bajos al comienzo de los identificadores mediante config.LEXER_IDENT_UNDERSCORES o Session(..., ident_underscores=...):
[!NOTE] La configuración
ident_underscoresaplica estrictamente al comienzo (guiones bajos iniciales) del identificador. Los guiones bajos intermedios o finales en identificadores que comienzan con caracteres alfanuméricos (comoA______BoD____________) son completamente agnósticos a estas reglas y se conservan siempre íntegros dentro delIDENTen todos los modos.
| Modo | Semántica | Entrada de Ejemplo | Tokens Emitidos |
|---|---|---|---|
-1 (Por defecto) |
Guiones bajos iniciales ilimitados; secuencias aisladas de _ son IDENT. |
___Hola___A______B |
[IDENT("___Hola")][IDENT("___")][IDENT("A______B")] |
-2 |
Guiones bajos iniciales ilimitados sólo si van seguidos de caracteres alfanuméricos; secuencias aisladas emiten UNDERSCORE. |
___Hola___D____________ |
[IDENT("___Hola")][UNDERSCORE, UNDERSCORE, UNDERSCORE][IDENT("D____________")] |
0 |
Ningún identificador permite guiones bajos iniciales; todo _ inicial o aislado emite UNDERSCORE. |
_H__HA______B |
[UNDERSCORE, IDENT("H")][UNDERSCORE, UNDERSCORE, IDENT("H")][IDENT("A______B")] |
k >= 1 |
Máximo k guiones bajos iniciales en IDENT; el exceso previo se emite como tokens UNDERSCORE individuales. |
__H (con k=1)___H (con k=1)A______B |
[UNDERSCORE, IDENT("_H")][UNDERSCORE, UNDERSCORE, IDENT("_H")][IDENT("A______B")] |
from gram import sessions
session = sessions.Session("UnderscoreDemo", "Some", ident_underscores=1)
session.add_keyword("fn")
tokens = session.tokenize("__H = 10;", "demo")
# Emite: Token.UNDERSCORE ('_'), Token.IDENT ('_H'), Token.ASSIGN ('='), Token.NUMBER (10), Token.SEMICOLON (';')
Jerarquía Determinista de 4 Niveles
La clasificación es estrictamente determinista:
Palabra Escaneada
│
▼
1. ¿Booleano Configurado?
(coincidencia insensible a mayúsculas
contra TOKEN_BOOL_TRUE o TOKEN_BOOL_FALSE)
│ SÍ │ NO
▼ ▼
Emitir Token.BOOL 2. ¿Nulo Configurado?
(coincidencia insensible a
mayúsculas contra TOKEN_NULL)
│ SÍ │ NO
▼ ▼
Emitir Token.NULL 3. ¿Keyword Registrada?
(coincidencia exacta sensible
a mayúsculas en MAP_KEYWORDS)
│ SÍ │ NO
▼ ▼
Emitir Token.KEYWORD 4. Emitir Token.IDENT
Ejemplos de Precedencia Determinista
- Booleanos sobre Keywords: Si
TOKEN_BOOL_TRUE = "true", una palabra clave registrada llamada"true"o"TRUE"nunca sobrescribirá la clasificación del booleano. - Nulo sobre Keywords: Si
TOKEN_NULL = "null", la palabra"null"o"NULL"siempre emitiráToken.NULL. - Búsqueda Exacta de Palabras Clave: Las keywords registradas vía
words.add_keyword("if")exigen coincidencia exacta de mayúsculas/minúsculas."if"emiteToken.KEYWORD, mientras que"If"o"IF"emitiráToken.IDENT.
from gram import config
from gram.core.lexer import Lexer, Token, words
words.clear_all()
words.add_keyword("let")
words.add_keyword("null") # Quedará ensombrecida por TOKEN_NULL
tokens = Lexer("let x = true; let y = null; let z = True;").process()
print([(t.token, t.value) for t in tokens])
# [(Token.KEYWORD, 'let'), (Token.IDENT, 'x'), (Token.ASSIGN, '='), (Token.BOOL, True), (Token.SEMICOLON, ';'),
# (Token.KEYWORD, 'let'), (Token.IDENT, 'y'), (Token.ASSIGN, '='), (Token.NULL, None), (Token.SEMICOLON, ';'),
# (Token.KEYWORD, 'let'), (Token.IDENT, 'z'), (Token.ASSIGN, '='), (Token.BOOL, True), (Token.SEMICOLON, ';'),
# (Token.EOF, None)]
5. Operadores y Símbolos (visitors.symbols)
Cuando un carácter no coincide con palabras alfanuméricas, comillas o comentarios, el control se delega a visitors.symbols.process():
- Itera sobre
lexer.sorted_symbols()(ordenados descendentemente por longitud). - Compara el fragmento de la línea:
if line[start:start+len(symbol)] == symbol:. - Comprobación especial para
...: siconfig.SUPPORT_TRIPLE_DOT=False, se omite..., permitiendo que cada punto se reconozca individualmente comoToken.DOT. - Si un símbolo coincide, avanza
lexer.colenlen(symbol)y emiteTokenType(symbol_map[symbol], symbol, line, start). - Si ningún símbolo registrado coincide, avanza 1 carácter, registra un error en
Diagnostics, reportaLexerError(LEXER_UNEXPECTED_CHARACTER) y emiteToken.ERROR.