Navegación del Flujo de Tokens (`TokenStream`)
Mientras que Lexer.process() retorna una lista plana nativa de Python (list[TokenType]), Gram proporciona TokenStream (gram.core.lexer.items.TokenStream) como un contenedor navegable, inspeccionable, filtrable y gestionado mediante cursores.
1. Instanciación de TokenStream
Puedes obtener un TokenStream directamente desde Lexer.stream() o envolviendo una secuencia de tokens existente:
from gram.core.lexer import Lexer, TokenStream, words
words.add_keyword("let", allow_override=True)
# 1. Directamente desde el Lexer
stream = Lexer("let x = 10;").stream()
# 2. Envolviendo una lista de tokens existente
tokens = Lexer("let y = 20;").process()
stream = TokenStream(tokens)
La secuencia de tokens subyacente se almacena como una tupla inmutable tuple[TokenType, ...], garantizando que la colección no pueda mutar accidentalmente durante su consumo.
2. Inspección del Cursor y Estado
TokenStream gestiona un cursor entero interno _pos que avanza conforme se consumen tokens.
Propiedades y Métodos de Estado
| Miembro | Tipo de Retorno | Descripción |
|---|---|---|
stream.position |
int |
Índice actual del cursor ($0 \le \text{pos} \le \text{len}$). Puede leerse o asignarse directamente. |
stream.tokens |
tuple[TokenType, ...] |
Tupla inmutable con todos los tokens del flujo. |
stream.has_next() |
bool |
Retorna True si quedan tokens pendientes distintos de EOF. |
stream.is_eof() |
bool |
Retorna True si el cursor alcanzó Token.EOF o el final del flujo. |
stream.current() |
TokenType | None |
Retorna el token bajo el cursor actual sin consumirlo. Retorna None fuera de rango. |
stream.peek(offset=0) |
TokenType | None |
Inspecciona el token en position + offset sin desplazar el cursor. Retorna None fuera de rango. |
stream = Lexer("let a = 10;").stream()
print(stream.position) # 0
print(stream.current().token) # Token.KEYWORD ("let")
print(stream.peek(1).token) # Token.IDENT ("a")
print(stream.peek(2).token) # Token.ASSIGN ("=")
print(stream.has_next()) # True
3. API de Consumo y Coincidencia de Patrones
TokenStream ofrece cuatro métodos complementarios para consumir tokens: advance(), check(), match() y consume().
advance() -> TokenType
Consume el token actual e incrementa el cursor en 1. Si se invoca habiendo alcanzado el final, retorna un token sintético TokenType(Token.EOF, None, line, col):
tok1 = stream.advance() # Consume 'let'
tok2 = stream.advance() # Consume 'a'
check(*expected: Token | TokenType | str) -> bool
Retorna True si el token bajo el cursor coincide con cualquiera de los criterios en expected sin desplazar el cursor:
- Si es
Token, evalúacurr.token == exp. - Si es
TokenType, evalúa la igualdad estructuralcurr == exp. - Si es
str, evalúacurr.value == expocurr.token.name == exp.
if stream.check(Token.IDENT, "let"):
print("Coincide con un identificador o con 'let'")
match(*expected: Token | TokenType | str) -> bool
Consumo condicional: ejecuta check(*expected). Si coincide, avanza el cursor mediante advance() y retorna True; de lo contrario, deja el cursor intacto y retorna False:
# Consume '=' sólo si está presente:
if stream.match(Token.ASSIGN, "="):
print("¡Operador de asignación consumido!")
consume(expected=None, message="") -> TokenType
Consumo obligatorio. Verifica que el token actual coincida con expected antes de avanzar. Si no coincide, lanza un error estructurado errors.ParserError:
- Si se alcanzó el final prematuramente, lanza
PARSER_EARLY_EOF. - Si el token no concuerda, lanza
PARSER_UNEXPECTED_TOKENcon las coordenadas y detalles del error.
# Consumo estricto con aserción:
token_let = stream.consume(Token.KEYWORD, "Se esperaba la palabra clave 'let'")
token_nombre = stream.consume(Token.IDENT, "Se esperaba el nombre de la variable")
stream.consume("=", "Se esperaba '=' después del identificador")
4. Control del Cursor: rewind() y reset()
TokenStream permite retrocesos y reinicios del cursor para análisis especulativo:
stream = Lexer("let x = 42;").stream()
stream.advance() # Consume 'let' (pos = 1)
stream.advance() # Consume 'x' (pos = 2)
# Retroceder 1 posición:
stream.rewind(1)
print(stream.current().value) # 'x' (pos = 1)
# Reiniciar el cursor al inicio del flujo:
stream.reset()
print(stream.current().value) # 'let' (pos = 0)
5. Rebanadas y Filtrado de Tokens
slice(start: int, end: int | None = None) -> list[TokenType]
Extrae una sub-lista de tokens entre start y end:
stream = Lexer("let x = 10;").stream()
sub_tokens = stream.slice(0, 3)
print([t.value for t in sub_tokens])
# ['let', 'x', '=']
filter_tokens(*exclude: Token) -> TokenStream
Retorna un nuevo TokenStream excluyendo todos los tokens cuyo tipo coincida con los miembros indicados en exclude:
from gram.core.lexer import Lexer, Token, words
from gram import config
config.LEXER_IGNORE_NEWLINES = False
words.add_keyword("dummy", allow_override=True)
# Generar un flujo que contiene saltos de línea NEWLINE:
stream = Lexer("a = 1\nb = 2").stream()
# Filtrar los tokens NEWLINE:
flujo_limpio = stream.filter_tokens(Token.NEWLINE)
print([t.token for t in flujo_limpio])
# [Token.IDENT, Token.ASSIGN, Token.NUMBER,
# Token.IDENT, Token.ASSIGN, Token.NUMBER, Token.EOF]
6. Protocolos de Secuencia de Python
TokenStream implementa los protocolos nativos de colecciones de Python:
stream = Lexer("let x = 10;").stream()
# 1. Conteo total de tokens
total = len(stream)
# 2. Acceso por índice y rebanado directo
primer_token = stream[0]
sub_bloque = stream[1:3]
# 3. Iteración directa
for token in stream:
if token.token == Token.EOF:
break
print(token)