Token Stream Navigation (`TokenStream`)
While Lexer.process() returns a raw Python list[TokenType], Gram provides TokenStream (gram.core.lexer.items.TokenStream) as a cursor-managed, inspectable, and filterable stream wrapper.
1. Instantiating a TokenStream
You can obtain a TokenStream directly from Lexer.stream() or by wrapping an existing sequence of tokens:
from gram.core.lexer import Lexer, TokenStream, words
words.add_keyword("let", allow_override=True)
# 1. Directly from Lexer
stream = Lexer("let x = 10;").stream()
# 2. By wrapping an existing token list
tokens = Lexer("let y = 20;").process()
stream = TokenStream(tokens)
The underlying token sequence is stored as an immutable tuple[TokenType, ...], ensuring the tokens cannot be accidentally mutated during stream navigation.
2. Cursor Inspection & Status
TokenStream tracks an internal integer cursor _pos that advances as tokens are consumed.
Status Properties & Methods
| Member | Return Type | Description |
|---|---|---|
stream.position |
int |
Current cursor index ($0 \le \text{pos} \le \text{len}$). Can be read or assigned directly. |
stream.tokens |
tuple[TokenType, ...] |
Immutable tuple containing all tokens in the stream. |
stream.has_next() |
bool |
Returns True if unconsumed non-EOF tokens remain in the stream. |
stream.is_eof() |
bool |
Returns True if the cursor has reached Token.EOF or the stream end. |
stream.current() |
TokenType | None |
Returns the token under the current cursor without advancing. Returns None if out of bounds. |
stream.peek(offset=0) |
TokenType | None |
Inspects a token at position + offset without advancing. Returns None if out of bounds. |
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. Consumption & Pattern Matching API
TokenStream provides three complementary methods for consuming tokens: advance(), check(), match(), and consume().
advance() -> TokenType
Consumes the current token and advances the cursor by 1. If called when the cursor has reached the end, it returns a synthetic TokenType(Token.EOF, None, line, col):
tok1 = stream.advance() # Consumes 'let'
tok2 = stream.advance() # Consumes 'a'
check(*expected: Token | TokenType | str) -> bool
Returns True if the token under the current cursor matches any of the criteria in expected without moving the cursor:
- Passing a
Tokencheckscurr.token == exp. - Passing a
TokenTypechecks structural equalitycurr == exp. - Passing a
strcheckscurr.value == exporcurr.token.name == exp.
if stream.check(Token.IDENT, "let"):
print("Matches identifier or 'let'")
match(*expected: Token | TokenType | str) -> bool
Conditional consumption: calls check(*expected). If it matches, consumes the token with advance() and returns True; otherwise leaves the cursor unchanged and returns False:
# Consume '=' only if present:
if stream.match(Token.ASSIGN, "="):
print("Assignment operator consumed!")
consume(expected=None, message="") -> TokenType
Mandatory consumption. Verifies that the current token matches expected before advancing. If the condition fails, it raises a structured errors.ParserError:
- If the stream reached EOF prematurely, raises
PARSER_EARLY_EOF. - If the token does not match, raises
PARSER_UNEXPECTED_TOKENwith coordinates and details.
# Strict consumption:
token_let = stream.consume(Token.KEYWORD, "Expected 'let' keyword")
token_name = stream.consume(Token.IDENT, "Expected variable name")
stream.consume("=", "Expected '=' after variable name")
4. Cursor Movement: rewind() & reset()
TokenStream allows transactional rollback and cursor resets:
stream = Lexer("let x = 42;").stream()
stream.advance() # Consumes 'let' (pos = 1)
stream.advance() # Consumes 'x' (pos = 2)
# Rewind 1 position:
stream.rewind(1)
print(stream.current().value) # 'x' (pos = 1)
# Reset cursor back to the start:
stream.reset()
print(stream.current().value) # 'let' (pos = 0)
5. Slicing & Token Filtering
slice(start: int, end: int | None = None) -> list[TokenType]
Extracts a sub-list of tokens between start and 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
Returns a new TokenStream excluding all tokens whose type matches any member in 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)
# Generate a stream containing NEWLINE tokens:
stream = Lexer("a = 1\nb = 2").stream()
# Filter out NEWLINE tokens:
clean_stream = stream.filter_tokens(Token.NEWLINE)
print([t.token for t in clean_stream])
# [Token.IDENT, Token.ASSIGN, Token.NUMBER,
# Token.IDENT, Token.ASSIGN, Token.NUMBER, Token.EOF]
6. Python Sequence Protocol Integration
TokenStream implements the standard Python sequence protocols:
stream = Lexer("let x = 10;").stream()
# 1. Total token count
total = len(stream)
# 2. Direct indexing and slicing
first = stream[0]
middle = stream[1:3]
# 3. Iteration
for token in stream:
if token.token == Token.EOF:
break
print(token)