Lexers
The lexer splits the input into tokens, one per terminal match. LALR’s default lexer is the contextual one, Earley’s the dynamic one; Choosing a lexer describes each.
The samples on this page use this grammar as GRAMMAR:
start: NUMBER (OP NUMBER)*OP: "+" | "-" | "*" | "/" // string alternativesNUMBER: /[0-9]+/ // a regex%ignore " " // drop spaces between tokensIgnoring text
Section titled “Ignoring text”%ignore names a terminal, or an inline pattern. All occurrences of the
terminal will be ignored, and won’t be part of the parse. It is especially
important for LALR(1), because adding whitespace (or comments) explicitly
in the grammar harms its predictive abilities. To collect ignored tokens
such as comments, see Lexer callbacks.
Seeing the tokens
Section titled “Seeing the tokens”lex only lexes (and postlexes) the text, without parsing it. It always
runs the basic lexer, which ignores the parser state, so under the
contextual lexer a token can differ from the one the parser sees. With
dont_ignore=True in Python, or true as the second argument in Rust and
TypeScript, it returns all tokens, even those marked for %ignore.
import hyperlark as larkp = lark.Lark(GRAMMAR, parser="lalr")for t in p.lex("1 + 22"): print(t.type, repr(t.value)) # NUMBER '1' OP '+' NUMBER '22'use hyperlark::Lark;let p = Lark::lalr(GRAMMAR)?;let table = p.parse_table();for t in p.lex("1 + 22", false)? { // true: include the ignored tokens println!("{} {:?}", table.id_to_token[t.type_id], t.value); // NUMBER "1" OP "+" NUMBER "22"}import { Lark } from "@hyperlark/hyperlark";const p = new Lark(GRAMMAR, { parser: "lalr" });for (const t of p.lex("1 + 22")) console.log(t.type, t.value); // NUMBER 1 OP + NUMBER 22To see the tokens in C, drive an interactive parser
with lark_interactive_feed_next, which pulls one token at a time.
Lexer callbacks
Section titled “Lexer callbacks”lexer_callbacks can be used to interface with the lexer as it generates
tokens (Python and Rust), to alter or collect them. It accepts a dictionary
of the form {TOKEN_TYPE: callback}, where the callback is of type
f(Token) -> Token. The callback also receives ignored tokens; its return
value for them is discarded. The samples add a comment terminal to the
grammar:
start: NUMBER (OP NUMBER)*OP: "+" | "-" | "*" | "/"NUMBER: /[0-9]+/COMMENT: /#[^\n]*/%ignore " "%ignore COMMENTcomments = []p = lark.Lark(GRAMMAR, parser="lalr", lexer_callbacks={"COMMENT": comments.append})p.parse("1 + 22 # a note")comments # [Token('COMMENT', '# a note')]use hyperlark::{Lark, LarkOptions, ParserKind, Token, TokenCallback};use std::collections::HashMap;use std::sync::{Arc, Mutex};
let comments = Arc::new(Mutex::new(Vec::<String>::new()));let sink = Arc::clone(&comments);let on_comment: TokenCallback = Arc::new(move |t: Token| { sink.lock().unwrap().push(t.value.to_string()); Ok(t)});let p = Lark::from_lark_source( GRAMMAR, LarkOptions { parser: Some(ParserKind::Lalr), lexer_callbacks: HashMap::from([("COMMENT".to_string(), on_comment)]), ..Default::default() },)?;p.parse("1 + 22 # a note")?; // comments now holds "# a note"It only works with the basic and contextual lexers (a custom lexer gets
them only through a BasicLexer it wraps). The dynamic lexers, Earley’s
default, refuse callbacks with GrammarError; under Earley, pass
lexer="basic". TypeScript has no lexer callbacks; lex(text, true)
returns the ignored tokens, so pick the comments out of that.
Terminal priority
Section titled “Terminal priority”Terminals can be assigned a priority to influence lexing, as in
IF.2: "if". Priorities are signed integers with a default value of 0.
When using a lexer (basic or contextual), it is your responsibility to
make sure the terminals don’t collide, or that if they do, they are matched
in the desired order:
- Highest priority first.
- Length of match (for regexps, the longest theoretical match is used).
- Length of the literal / pattern definition.
- Name.
Keywords are the exception to this order: at equal priority a string literal beats a regex that matches the same text, so keywords need no priority.
NAME: /[a-z]+/IF: "if"if lexes as IF and iffy as NAME. Under the contextual lexer a keyword
is tried only where the parser state accepts it, so elsewhere if lexes as
NAME.
Consider setting a priority only as a last resort. BOOL: /true|false/
loses to NAME: /[a-z]+/ by rule 2. BOOL.2 wins, but then trueish
lexes as BOOL followed by the NAME ish. To get true as a keyword and
trueish as a name, give each keyword its own string terminal
(TRUE: "true"). BOOL: "true" | "false" does not help: a terminal with
alternatives counts as a regex, so NAME still wins against it.
When using Earley’s dynamic lexing, terminal priorities are used to prefer certain lexings and resolve ambiguity.
Keeping punctuation in the tree
Section titled “Keeping punctuation in the tree”The tree builder filters out punctuation by default: unnamed literals (like
"+") and terminals whose name starts with an underscore (like _NEWLINE);
see Shaping the tree. keep_all_tokens
prevents that removal in every rule, and a rule prefixed with !
(!start: ...) keeps all its terminals. Named terminals such as
OP are kept either way.
p = lark.Lark(GRAMMAR, parser="lalr", keep_all_tokens=True)use hyperlark::{Lark, LarkOptions, ParserKind};let p = Lark::from_lark_source( GRAMMAR, LarkOptions { parser: Some(ParserKind::Lalr), keep_all_tokens: true, ..Default::default() },)?;const p = new Lark(GRAMMAR, { parser: "lalr", keep_all_tokens: true });Lark *p = NULL;lark_from_source(GRAMMAR, strlen(GRAMMAR), LARK_OPT_KEEP_ALL_TOKENS, &p);In C the flag applies to lark_from_source; lark_from_json uses the
setting the grammar was saved with.
Choosing a lexer
Section titled “Choosing a lexer”lexer="auto", the default, chooses based on the parser. Most grammars
never change it:
| Parser | auto resolves to | Also available |
|---|---|---|
| LALR | contextual | basic |
| Earley | dynamic (basic with a postlex) | basic, dynamic_complete |
contextual(LALR only) uses the parser’s lookahead prediction to narrow its choice of terminals: at each point it matches only the terminals that are legal in that parser state, resolving common terminal collisions.basicignores the parser state and works with both parsers. Under Earley it is faster, but tokenizes without Earley’s ambiguity resolution, so choose it only if you know why. Under LALR, choose it whenlexmust return the parser’s tokens.dynamic(Earley only) has no fixed tokenization: at each position it matches every terminal the parse expects, each regex once, taking the regex’s own match.dynamic_completealso tries the shorter prefixes of that match, whichstart: A BwithA: /a+/andB: "a"needs to parseaaa. It can be much slower, especially for open-ended terminals such as/.*/.
To override the choice:
p = lark.Lark(GRAMMAR, parser="earley", lexer="dynamic_complete")use hyperlark::{Lark, LarkOptions, LexerSpec, ParserKind};let p = Lark::from_lark_source( GRAMMAR, LarkOptions { parser: Some(ParserKind::Earley), lexer: LexerSpec::DynamicComplete, ..Default::default() },)?;const p = new Lark(GRAMMAR, { parser: "earley", lexer: "dynamic_complete" });C has no lexer option: lark_from_source uses the contextual lexer, and
lark_from_json the lexer the grammar was saved with. The
feature matrix has the per-target table.
Indentation: postlex and Indenter
Section titled “Indentation: postlex and Indenter”A postlex post-processes the token stream between the lexer and the parser.
The built-in Indenter is a postlex that “injects” indent/dedent tokens
based on indentation, which no regex can produce. Inside brackets, newlines
are dropped and the indentation is ignored, so no indent/dedent tokens get
generated. Your grammar must:
- Include the next line’s leading whitespace in the newline terminal, as
_NEWLINE: /(\r?\n[\t ]*)+/does. The indenter measures it there. %declarethe indent and dedent terminals, declaring them without defining them, since the indenter produces them.- Name the newline, indent and dedent terminals as the indenter is configured. A missing one is an error; a missing bracket name is ignored.
The samples in this section use this grammar as GRAMMAR:
?start: _NEWLINE* treetree: NAME _NEWLINE [_INDENT tree+ _DEDENT]NAME: /\w+/_NEWLINE: /(\r?\n[\t ]*)+/%declare _INDENT _DEDENT%import common.WS_INLINE%ignore WS_INLINEfrom hyperlark import Lark, PythonIndenterp = Lark(GRAMMAR, parser="lalr", postlex=PythonIndenter())p.parse("root\n branch\n leaf\n")use hyperlark::{Indenter, Lark, LarkOptions, ParserKind};let indenter = Indenter { nl_type: "_NEWLINE".into(), open_paren_types: vec!["LPAR".into(), "LSQB".into(), "LBRACE".into()], close_paren_types: vec!["RPAR".into(), "RSQB".into(), "RBRACE".into()], indent_type: "_INDENT".into(), dedent_type: "_DEDENT".into(), tab_len: 8,};let p = Lark::from_lark_source( GRAMMAR, LarkOptions { parser: Some(ParserKind::Lalr), postlex: Some(Box::new(indenter)), ..Default::default() },)?;const p = new Lark(GRAMMAR, { parser: "lalr", indenter: "python" });p.parse("root\n branch\n leaf\n");All three samples use PythonIndenter’s values, spelled out in the Rust
sample. To change them in Python, inherit from Indenter and implement
NL_type, OPEN_PAREN_types,
CLOSE_PAREN_types, INDENT_type, DEDENT_type and tab_len. In
TypeScript the "python" preset is the only postlex and cannot be changed.
C has no postlex.
For a postlex of your own in Python, subclass PostLex, implement
process(stream) as a generator over the incoming tokens, and set
always_accept to the terminals the lexer must accept in every parser state.
In Rust, implement the Postlex trait: session(names) returns the stream
for one pass over the input, and always_accept() lists the same terminals.
Yield each token before pulling the next one. A postlex that reads ahead
runs ahead of the parser, so under the contextual lexer a terminal that
only the parser state disambiguates can fail to match.
The dynamic lexers refuse a postlex: requesting one raises
ConfigurationError, and under auto, Earley with a postlex uses basic.
The indentation example is a complete Python program.
Custom lexers
Section titled “Custom lexers”You can use a custom lexer to tokenize text when the built-in lexers are too slow, or not flexible enough, or in Python to tokenize a stream of objects. It works under LALR and Earley, in Python, Rust and TypeScript.
In Python and TypeScript you pass a lexer class. It is constructed once,
with the parser, and its lex runs once per parse; both samples wrap the
built-in BasicLexer. In Rust you implement TokenSource, and pass one
over each input to parse_custom in place of parse. parse_custom also
takes an optional start rule and on_error callback; the built-in lexer as
a TokenSource is BasicTokenSource::new(p.basic_lexer()?, text).
Each token names a grammar terminal and carries its text:
Token("NUMBER", "42") in Python, { type: "NUMBER", value: "42" } in
TypeScript, CustomToken::new("NUMBER", "42") or the full struct in Rust.
A type the grammar does not know fails the parse with UnexpectedToken.
Your lexer can read the parser state before each token. Under LALR it is the
state number: position on the parser_state (parserState) argument in
Python and TypeScript, state in Rust. Under Earley it is the terminals the
parser expects next: a set of Terminal objects in Python (compare
t.name), a set of names in TypeScript, and expects, a slice of terminal
ids, in Rust. That set can be incomplete: before OP in start: NUMBER (OP NUMBER)*
it is empty. In Python and TypeScript, parser_state is one object the
parser updates in place, so read position or the set before each token
rather than copying it once at the top of lex.
from hyperlark import Larkfrom hyperlark.lexer import BasicLexer, Lexer
class MyLexer(Lexer): def __init__(self, lexer_conf): self.lexer = BasicLexer(lexer_conf) # reuse the built-in lexer
def lex(self, lexer_state, parser_state): for tok in self.lexer.lex(lexer_state, parser_state): yield tok # change or drop tokens here
# 2 selects lex(lexer_state, parser_state). Omit it and define # lex(self, text) when you do not need the parser state. __future_interface__ = 2
p = Lark(GRAMMAR, parser="lalr", lexer=MyLexer)use hyperlark::{CustomLexError, CustomToken, Lark, LarkOptions, LexerSpec, ParserKind, TokenSource};use std::sync::Mutex;
struct MySource { /* your cursor over the input */ }
impl TokenSource for MySource { fn next_token(&mut self, _state: Option<usize>, _expects: Option<&[u32]>) -> Result<Option<CustomToken>, CustomLexError> { // Return Ok(None) at end of input. One fixed token shown here: Ok(Some(CustomToken { type_name: "NUMBER".into(), value: "42".into(), start_pos: Some(0), end_pos: Some(2), line: Some(1), end_line: Some(1), column: Some(1), end_column: Some(3), })) }}
let p = Lark::from_lark_source( GRAMMAR, LarkOptions { parser: Some(ParserKind::Lalr), lexer: LexerSpec::Custom, ..Default::default() },)?;let src = Mutex::new(MySource { /* ... */ });let tree = p.parse_custom(&src, None, None)?;import { Lark, BasicLexer, type LexerConf, type CustomLexer, type CustomLexerParserState, type CustomLexerToken } from "@hyperlark/hyperlark";
class MyLexer implements CustomLexer { lexer: InstanceType<typeof BasicLexer>; constructor(conf: LexerConf) { this.lexer = new BasicLexer(conf); } *lex(input: string, parserState: CustomLexerParserState): Generator<CustomLexerToken> { for (const tok of this.lexer.lex(input)) yield tok as CustomLexerToken; // reshape tokens here }}const p = new Lark(GRAMMAR, { parser: "lalr", lexer: MyLexer });A custom lexer cannot be used with interactive parsing:
parse_interactive, OwnedInteractiveParser::new and parseInteractive
raise an error.