Parse errors
A failed parse tells you where it stopped and which
terminals were expected there. A mistake in the
grammar itself is reported earlier, when the parser is built: GrammarError
in Python and TypeScript, LarkBuildError in Rust, LARK_CONSTRUCTION_ERROR
in C. In Python a terminal pattern that does not compile can raise LexError
instead; catching LarkError covers both.
Error classes
Section titled “Error classes”UnexpectedCharacters: the lexer cannot match the next characters to any terminal, such as a stray@or an unterminated string. Earley’s default dynamic lexer reports a misplaced token this way as well.UnexpectedToken: the parser received a token that doesn’t match any valid step forward, such as an operator where a number was expected. Under LALR, input that ends too early is reported this way too, with the end-of-input token$END.UnexpectedEOF: the parser expected a token, but the input ended; Earley only. It carries the expected set but no position:lineandcolumnare -1 in Python and TypeScript,Nonein Rust.
By language:
- Python and TypeScript: all three extend
UnexpectedInput, which has helpers for a nicer message:get_context(getContextin TypeScript) and, in Python,match_examples. - Rust: they are variants of
ParseError, which has others (a callback failure, a configuration mistake), so amatchneeds a wildcard arm. - C: the parse call returns the status code
LARK_UNEXPECTED_TOKENorLARK_UNEXPECTED_CHARACTERS, andlark_last_error*holds the details.
| Python | Rust | TypeScript | C | |
|---|---|---|---|---|
| Token | token | token_type, token_value | token | type, in the message text |
| Character | char | ch | char | in the message text |
| Line and column, 1-based | line, column | line(), column() | line, column | lark_last_error_position |
| Offset, in code points | pos_in_stream | pos_in_stream() | pos | none |
| Expected set | expected (allowed on UnexpectedCharacters) | expected (allowed) | accepts (allowed) | lark_last_error_expected |
| Parser state, LALR only | state | state | none | none |
| Caret view | get_context(text, span=40) | get_context(text) | getContext(text, span?) | lark_last_error_context() |
Python’s UnexpectedToken also has accepts, the set of tokens that will
advance the parser into a new valid state. It is None under Earley. With
the basic lexer, allowed on
UnexpectedCharacters is not narrowed to the terminals that fit there.
Catching a parse error
Section titled “Catching a parse error”The samples parse 1 + + 2 under LALR with this grammar as GRAMMAR; the
second + is an UnexpectedToken.
start: NUMBER (OP NUMBER)*OP: "+" | "-" | "*" | "/"NUMBER: /[0-9]+/%ignore " "import hyperlark as lark
parser = lark.Lark(GRAMMAR, parser="lalr")text = "1 + + 2"
try: parser.parse(text)except lark.exceptions.UnexpectedInput as e: # base of all three print(e.line, e.column) # 1 5 print(e.get_context(text)) # caret view if isinstance(e, lark.exceptions.UnexpectedToken): print(repr(e.token), e.expected) # Token('OP', '+') {'NUMBER'} elif isinstance(e, lark.exceptions.UnexpectedCharacters): print(repr(e.char), e.allowed)use hyperlark::{Lark, ParseError};
let parser = Lark::lalr(GRAMMAR)?;let text = "1 + + 2";
match parser.parse(text) { Ok(tree) => println!("{}", tree.pretty()), Err(failure) => { let err = &failure.error; // None for an error without a position (Earley end-of-input, a callback failure) eprintln!("at line {:?}, column {:?}", err.line(), err.column()); eprint!("{}", err.get_context(text)); // caret view match err { ParseError::UnexpectedToken { token_value, expected, .. } => eprintln!("unexpected {token_value:?}; expected {expected:?}"), ParseError::UnexpectedCharacters { ch, allowed, .. } => eprintln!("no terminal matches {ch:?}; allowed {allowed:?}"), other => eprintln!("{other}"), } }}import { Lark, UnexpectedInput, UnexpectedToken, UnexpectedCharacters } from "@hyperlark/hyperlark";
const parser = new Lark(GRAMMAR, { parser: "lalr" });const text = "1 + + 2";
try { parser.parse(text);} catch (err) { if (err instanceof UnexpectedInput) { // base of all three console.log(err.line, err.column); // 1 5 console.log(err.getContext(text)); // caret view if (err instanceof UnexpectedToken) { console.log(err.token, err.accepts); // { type, value, ... }, ["NUMBER"] } else if (err instanceof UnexpectedCharacters) { console.log(err.char, err.allowed); } }}#include "hyperlark.h"
Lark *lark = NULL;if (lark_from_source(GRAMMAR, strlen(GRAMMAR), LARK_OPT_NONE, &lark) != LARK_OK) return 1; /* grammar mistake: lark_last_error() has the message */const char *text = "1 + + 2";
LarkParseResult *res = NULL;LarkStatus st = lark_parse(lark, text, strlen(text), /*start=*/NULL, &res);if (st != LARK_OK) { /* LARK_UNEXPECTED_TOKEN, LARK_UNEXPECTED_CHARACTERS, ... */ uint32_t line = 0, col = 0; lark_last_error_position(&line, &col); /* 1-based; left untouched when the error has no position */ const char *msg = lark_last_error(); fprintf(stderr, "%s at %u:%u: %s\n", lark_status_name(st), line, col, msg ? msg : ""); const char *ctx = lark_last_error_context(); /* NULL for an error without a position */ if (ctx) fputs(ctx, stderr); /* caret view */ size_t n = lark_last_error_expected_count(); /* expected set */ for (size_t i = 0; i < n; i++) fprintf(stderr, " expected: %s\n", lark_last_error_expected(i));}The lark_last_error* buffer is thread-local. Read it on the same thread,
before the next failing lark_* call overwrites it.
The caret view each tab prints:
1 + + 2 ^get_context pinpoints the error in the text, with span characters of
context around it, 40 by default and always 40 in Rust. The parser holds no
copy of the text, so you pass it again. C renders the view at parse time, so
lark_last_error_context takes no arguments.
Recovering from errors
Section titled “Recovering from errors”To parse past a bad token, pass an on_error function to parse
(parse_opts in Rust). It is called on each UnexpectedInput error, with
the error as its argument (in Rust, a context holding it); return true to
resume parsing, or false to raise it. Resuming skips the rejected token, or
one unmatched character, so three bad characters mean three calls. The
result is the tree without the skipped parts. A rejected end of input
($END) cannot be skipped; returning true there still fails.
on_error is LALR only; passing it under Earley is an error.
def handler(e): print(type(e).__name__, e.line, e.column) # UnexpectedToken 1 5 return True # continue after the token; False raises it
tree = parser.parse("1 + + 2", on_error=handler)print(tree.pretty()) # the tree for "1 + 2"When skipping is not enough, feed tokens to e.interactive_parser before
returning True: it is the parse, left where it failed, and works like any
interactive parser. An error that
parse() raises carries it too. With a custom lexer it is None, so the
handler can only skip or stop, and an error that the lexer or a lexer
callback raises does not reach the handler at all
(divergences).
use hyperlark::{Lark, OnErrorContext};
let parser = Lark::lalr(GRAMMAR)?;let mut handler = |cx: &mut OnErrorContext<'_>| -> bool { eprintln!("skipping: {}", cx.error()); // cx.interactive_parser() is the live parse: feed a corrective token // through it before returning true when a skip is not enough. true // continue after the token; false returns the error};let result = parser.parse_opts("1 + + 2", None, Some(&mut handler))?;println!("{}", result.pretty()); // the tree for "1 + 2"A failed parse carries the parse too, as failure.interactive_parser;
parse_no_resume keeps none.
parse_custom (a custom lexer) takes the same handler. If that lexer
reports the same failure without advancing, a handler that always returns
true loops forever, so count the calls and return false past a limit.
Token-by-token parsing outside a handler is in the
interactive guide.
const tree = parser.parse(text, { on_error: (err) => { if (err instanceof UnexpectedInput) console.log(err.line, err.column); // 1 5 return true; // continue after the token; false re-throws },});console.log(tree); // the tree for "1 + 2"The handler cannot feed a replacement token. Token-by-token parsing is in the interactive guide.
C has no on_error. Drive the interactive parser and act on each status.
LarkInteractive *ip = NULL;if (lark_parse_interactive(lark, text, strlen(text), /*start=*/NULL, &ip) != LARK_OK) return 1;
LarkTokenEvent ev;bool has_token = true, failed = false;for (;;) { LarkStatus st = lark_interactive_feed_next(ip, &ev, &has_token); if (st == LARK_OK && !has_token) break; /* end of input */ if (st == LARK_UNEXPECTED_TOKEN) { /* the token is already consumed; */ uint32_t line = 0, col = 0; /* the next call continues after it */ lark_last_error_position(&line, &col); fprintf(stderr, "skipped a token at %u:%u\n", line, col); } else if (st != LARK_OK) { /* anything else: stop (a bad character is not consumed) */ failed = true; break; }}LarkParseResult *res = NULL;if (!failed && lark_interactive_feed_eof(ip, &res) == LARK_OK) fputs(lark_result_pretty(res), stdout); /* the tree for "1 + 2" */lark_result_free(res);lark_interactive_free(ip);This loop skips rejected tokens and stops at a character no terminal
matches. A loop that ignored the status would accept input the grammar
rejects. To substitute a token, feed one that fits with
lark_interactive_feed_token before continuing, as in the
interactive guide.
Per-target support: the feature matrix.
Context around an error
Section titled “Context around an error”An error names the token it stopped on. The token before it, and the
statement it is in, are on the
interactive parser. Python and Rust keep it
when an LALR parse with the built-in lexer fails (e.interactive_parser,
failure.interactive_parser; see
Recovering from errors); Earley keeps none. Its
last_token is the previous token. Its stacks, on parser_state (e.state)
in Python and on the parser in Rust, are the state_stack of LALR state ids
and the value_stack: the pieces of the tree built so far. In the samples,
finished statements are already one tree, so the tokens on the stack are the
statement in progress.
In Python and Rust, an LALR error has token_history. When a built-in lexer
rejected the input, as the
contextual lexer (the LALR default) does
in the samples, it holds the previous token. It is None when the parser
rejected a token, as on $END. last_token, Hyperlark’s own, is set under
both built-in lexers.
In TypeScript and C the error carries no parser and the interactive parser
has no last_token or stacks:
drive it and record the
tokens it feeds.
The samples parse let x = 1; let y 2; under LALR with this grammar as
GRAMMAR; it fails at 2, where = was expected.
start: stmt+stmt: "let" NAME "=" NUMBER ";" -> let_stmt | "print" NAME ";" -> print_stmtNAME: /[a-z]+/NUMBER: /[0-9]+/%ignore " "parser = lark.Lark(GRAMMAR, parser="lalr")text = "let x = 1; let y 2;"
try: parser.parse(text)except lark.exceptions.UnexpectedInput as e: e.token_history # [Token('NAME', 'y')] e.interactive_parser.last_token # Token('NAME', 'y')
stack = e.state.value_stack # each read copies the parse so far # [Tree('__start_plus_0', [Tree('let_stmt', [...])]), Token('LET', 'let'), Token('NAME', 'y')] next(v for v in stack if isinstance(v, lark.Token)) # Token('LET', 'let')With a custom lexer the error keeps no
parser (divergences). Under
transformer=, value_stack raises NotImplementedError
(divergences).
use hyperlark::{Lark, NodeKind, ParseError, ParseResult};use std::sync::Arc;
let parser = Lark::lalr(GRAMMAR)?;let text = "let x = 1; let y 2;";
let failure = parser.parse(text).unwrap_err();if let ParseError::UnexpectedToken { token_history, .. } = &failure.error { println!("{token_history:?}"); // Some([CustomToken { type_name: "NAME", value: "y", .. }])}let ip = failure.interactive_parser.unwrap(); // the parse, where it failedlet prev = ip.last_token().unwrap(); // NAME "y"println!("{} {:?}", ip.parse_table().id_to_token[prev.type_id], prev.value);
// the value stack, bottom first: one tree of finished statements, then the one in progresslet stack = ParseResult { tree: Arc::new(ip.value_stack_tree().unwrap()), table: ip.parse_table() };for frame in stack.root().children() { match frame.kind() { NodeKind::Tree { data } => println!("tree {data}"), // __start_plus_0 NodeKind::Token(t) => println!("token {:?}", t.value), // "let", then "y" _ => {} }}ip.state_stack() is the state ids. value_stack_tree() is None while a
reduce hook (ReduceCallback) has values on the stack. An on_error handler’s
cx.interactive_parser() has no last_token or stacks; read
token_history from cx.error(). With on_error, the failure keeps no
parser.
import { Lark, UnexpectedToken, type Token } from "@hyperlark/hyperlark";
const parser = new Lark(GRAMMAR, { parser: "lalr" });const text = "let x = 1; let y 2;";
const ip = parser.parseInteractive(text);let prev: Token | undefined;let stmt: Token[] = []; // the tokens since the last ";"try { for (const tok of ip.iterParse()) { // yields each token after feeding it prev = tok; if (tok.type === "SEMICOLON") stmt = []; else stmt.push(tok); }} catch (err) { if (err instanceof UnexpectedToken) console.log(err.token.value, prev?.value, stmt.map(t => t.value)); // 2 y [ 'let', 'y' ]} finally { ip.free();}An on_error handler gets the same error and nothing else.
Lark *lark = NULL;if (lark_from_source(GRAMMAR, strlen(GRAMMAR), LARK_OPT_NONE, &lark) != LARK_OK) return 1;const char *text = "let x = 1; let y 2;";
LarkInteractive *ip = NULL;if (lark_parse_interactive(lark, text, strlen(text), NULL, &ip) != LARK_OK) return 1;uint32_t semi = 0;lark_token_id(lark, "SEMICOLON", 9, &semi);
LarkTokenEvent ev;bool has_token = true;char prev[32] = ""; /* the previous token's text */char stmt[256] = ""; /* the tokens since the last ";" */for (;;) { LarkStatus st = lark_interactive_feed_next(ip, &ev, &has_token); if (st == LARK_OK && !has_token) break; if (st != LARK_OK) { printf("%s; after %s, in:%s\n", lark_last_error(), prev, stmt); break; } /* ev.value is borrowed until the next feed: copy what you keep */ snprintf(prev, sizeof prev, "%.*s", (int)ev.value_len, ev.value); if (ev.type_id == semi) stmt[0] = '\0'; else snprintf(stmt + strlen(stmt), sizeof stmt - strlen(stmt), " %s", prev);}lark_interactive_free(ip);Prints Unexpected token "NUMBER" at line 1 column 18; after y, in: let y.
lark_token_name turns ev.type_id into the terminal’s name.
Custom error messages
Section titled “Custom error messages”In Python under LALR, match_examples detects what’s wrong in the input
text by matching against example errors. Given the parse function and a
dictionary mapping each label to some malformed syntax examples, it returns
the label for the example that best matches the current error, or None.
Two errors match when they have the same parser state, and preferably the
same token.
examples = { "two operators in a row": ["1 + + 2", "3 * * 4"], "trailing operator": ["1 +", "2 -"],}
try: parser.parse("1 + + 2")except lark.exceptions.UnexpectedInput as e: label = e.match_examples(parser.parse, examples) print(label or "unrecognised syntax error")A label usually needs several examples, because the state depends on what precedes the bad token. The error reporting example does this on a JSON grammar.
Rust has no helper, but under LALR with the built-in lexer both error variants
carry state: parse each example with the same parser and compare; ids are
comparable only within one parser instance. TypeScript and C have
neither the helper nor the state id, so map the token and the expected set to
a message yourself.