Skip to content

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.

  • 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 $END is 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.

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.

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).

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")

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.

  • Pythonparser.parse(text, on_error=handler). The handler receives the UnexpectedInput and returns truthy to resume. Note err.interactive_parser is always None here (hyperlark does not attach a live interactive parser); the error’s position and accepts set — allowed on UnexpectedCharacters — carry the context a handler needs.
  • TypeScriptparser.parse(text, { on_error }), an (err) => boolean callback (true resumes, false re-raises); the error exposes the interactive-parser accepts set at the failure site.
  • Rust / C — drive the incremental parser directly: Lark::parse_interactive in Rust, and lark_parse_interactive + lark_interactive_feed_token / _accepts_count / _resume_parse in C — feeding tokens and inspecting the accepted set at each step.

See the feature matrix for interactive and recovery support per target.