Parse errors
When hyperlark can’t parse an input it tells you exactly where and why. Every failure is one of two errors, each carrying a precise location and the set of things the grammar would have accepted instead. The class names and payload mirror Lark’s, so error-handling code ports across unchanged.
The two parse errors
Section titled “The two parse errors”UnexpectedToken— the lexer produced a token, but the grammar had no move for it in the current parser state (an operator where a number was due, a)with nothing open, input that ends early). The synthetic end-of-input$ENDis reported this way too under LALR.UnexpectedCharacters— the lexer itself got stuck: no terminal matched the input at some position (a stray@, an unterminated string).
Both carry the same shape of payload:
- line and column — 1-based, at the failure site;
- position — the offset into the input, counted in Unicode code points;
- the offending token (
UnexpectedToken) or character (UnexpectedCharacters); - the expected / allowed set — the terminal names the parser would have accepted at that point.
Catching a parse error
Section titled “Catching a parse error”The examples below all use one grammar and feed it 1 + + 2 — the second +
is a token where a NUMBER was expected:
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 both errors print(type(e).__name__) # UnexpectedToken print(e.line, e.column) # 1 5 print(repr(e.token), e.expected) # Token('OP', '+') {'NUMBER'} print(e.get_context(text)) # caret view (see below) # UnexpectedCharacters carries `.char` and `.allowed` instead; # UnexpectedToken also offers `.accepts` — the precise accepted set.use hyperlark::{Lark, ParseError};
let parser = Lark::new(GRAMMAR)?;let text = "1 + + 2";
match parser.parse(text) { Ok(tree) => println!("{}", tree.pretty()), Err(err) => { // line()/column() are None only for a position-less synthetic $END 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";
const parser = new Lark(GRAMMAR, { parser: "lalr" });const text = "1 + + 2";
try { parser.parse(text);} catch (err) { if (err instanceof UnexpectedInput) { // thrown values are real Errors 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); } }}// errorClasses() returns the same constructors for dynamic instanceof checks.#include "hyperlark.h"
LarkParseResult *res = NULL;LarkStatus st = lark_parse(lark, text, strlen(text), /*start=*/NULL, &res);if (st != LARK_OK) { /* e.g. LARK_UNEXPECTED_TOKEN / LARK_UNEXPECTED_CHARACTERS */ uint32_t line = 0, col = 0; lark_last_error_position(&line, &col); /* structured location */ fprintf(stderr, "%s at %u:%u: %s\n", lark_status_name(st), line, col, lark_last_error()); fputs(lark_last_error_context(), stderr); /* caret view */ size_t n = lark_last_error_expected_count(); /* expected/allowed 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 and errno-style: read it on the
same thread, before the next failing lark_* call overwrites it.
The caret context
Section titled “The caret context”Every error can render the source line it sits on with a ^ under the
offending column — Lark’s get_context. The parser keeps no copy of the input,
so you pass the original text back in. For 1 + + 2 it produces:
1 + + 2 ^It is e.get_context(text) in Python, err.get_context(text) in Rust,
err.getContext(text) in TypeScript, and lark_last_error_context() in C
(rendered at parse time, so no text is re-supplied).
Classifying errors with match_examples
Section titled “Classifying errors with match_examples”Raw expected-sets are noisy to branch on. In Python, UnexpectedInput.match_examples
re-parses a set of labelled malformed inputs and returns the label whose failure
matches yours — turning a low-level error into a friendly, actionable message.
It relies on the LALR parser state, so it is an LALR feature.
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")Recovering from errors
Section titled “Recovering from errors”hyperlark can also attempt to recover and keep parsing past an error, so one bad token doesn’t sink the whole parse. Recovery is LALR + built-in lexer only (Earley and custom lexers raise), mirroring Lark’s own restriction.
- Python —
parser.parse(text, on_error=handler). The handler receives theUnexpectedInputand returns truthy to resume. Noteerr.interactive_parseris alwaysNonehere (hyperlark does not attach a live interactive parser); the error’s position andacceptsset —allowedonUnexpectedCharacters— carry the context a handler needs. - TypeScript —
parser.parse(text, { on_error }), an(err) => booleancallback (trueresumes,falsere-raises); the error exposes the interactive-parseracceptsset at the failure site. - Rust / C — drive the incremental parser directly:
Lark::parse_interactivein Rust, andlark_parse_interactive+lark_interactive_feed_token/_accepts_count/_resume_parsein C — feeding tokens and inspecting the accepted set at each step.
See the feature matrix for interactive and recovery support per target.