Skip to content

Interactive parsing

Most of the time you hand hyperlark a whole string and get a tree back. The interactive parser turns that inside out: you drive an LALR parse one token at a time, and between tokens you can ask what the grammar would accept next, copy the parser’s state to explore a branch, and resume when you’re ready. That makes it the right tool for autocomplete, REPLs, editors, and error recovery — anywhere you’re parsing input that isn’t finished yet.

The moving parts:

  • Create an interactive parser, optionally seeded with text (omit the text to feed tokens entirely by hand).
  • Feed tokens with feed_token, or drive the built-in lexer over the seed text with iter_parse / exhaust_lexer.
  • Inspect the current state: accepts() lists the terminals that fit next (including $END), and choices() gives the full shift/reduce action map.
  • Fork the whole parser with copy — an independent branch for speculative feeds or backtracking. (One Python refusal: a parser carrying an embedded transformer= cannot fork — copy raises.)
  • Finish with feed_eof (append $END and stop) or resume_parse (drive the rest of the seed text plus $END), both returning a parse tree.

(This mirrors Lark’s parse_interactive / InteractiveParser; Python adds peek_next / feed_next / last_token on top.)

OperationPythonRustTypeScriptC
Create (seed optional)parse_interactive(text=None)OwnedInteractiveParser::newparseInteractive(text?)lark_parse_interactive
Feed one tokenfeed_tokenfeed_tokenfeedTokenlark_interactive_feed_token
What fits nextaccepts / choicesaccepts / choicesaccepts / choiceslark_interactive_accepts_count / _get
Drive the lexeriter_parse / exhaust_lexerfeed_next / exhaust_lexeriterParse / exhaustLexerlark_interactive_feed_next
Finishfeed_eof / resume_parsefeed_eof / resume_parsefeedEof / resumeParselark_interactive_feed_eof / lark_interactive_resume_parse
Forkcopycopycopylark_interactive_copy
Release(GC)(drop)(GC)lark_interactive_free

A self-contained calculator grammar — call it GRAMMAR below — so the same tokens parse identically on every target:

start: NUMBER (OP NUMBER)*
OP: "+" | "-" | "*" | "/"
NUMBER: /[0-9]+/
%ignore " "

Here we create a parser with no seed text, feed tokens by hand while watching accepts(), fork a branch, and finish. At the start only a NUMBER fits; after one number, an OP or end-of-input do — exactly the set you’d offer as autocomplete suggestions.

import hyperlark as lark
p = lark.Lark(GRAMMAR, parser="lalr") # interactive is LALR-only
ip = p.parse_interactive() # no seed: we feed tokens ourselves
print(ip.accepts()) # {'NUMBER'} — only a number fits here
toks = list(p.lex("1 + 2")) # any object with .type/.value feeds in
ip.feed_token(toks[0]) # feed "1"
print(ip.accepts()) # {'OP', '$END'} — operator or end
branch = ip.copy() # an independent fork to explore
for tok in toks[1:]:
ip.feed_token(tok) # feed "+ 2"
tree = ip.feed_eof() # append $END -> a parse tree
print(tree.pretty())

A few per-target notes:

  • accepts() shape differs idiomatically — a Python set, a Rust Vec<String>, a TypeScript string[], and (in C) a count+index list (lark_interactive_accepts_count / _get) — but the members are the same terminal names. choices() returns the richer {name: (kind, id)} action map (Shift / Reduce); it is not exposed on the C surface.
  • TypeScriptfeedEof() / resumeParse() return a ParseHandle (the WASM retained-tree handle), so call handle.toJs(handle.rootPos()) for the plain { data, children } tree, or walk it with the handle’s cursors.
  • RustOwnedInteractiveParser is the owning handle the Python, WASM, and C bindings all wrap; feed_eof / resume_parse hand back a ParsedTree you walk through its arena.
  • C — every handle you create (including each copy fork) is freed with lark_interactive_free; the finished LarkParseResult with lark_result_free.

Seed the parser with text and you don’t have to produce tokens yourself — drive the built-in lexer instead. iter_parse steps token by token so you can react per token — in Python it yields each token before feeding it (as Lark does), so retyping the yielded token steers the parse, while TypeScript’s iterParse feeds first and yields after; exhaust_lexer feeds them all at once; resume_parse runs the remainder plus $END straight to a tree. For example, in Python:

ip = p.parse_interactive("1 + 2") # seat over text
ip.exhaust_lexer() # drive the lexer to the end of the seed
print(ip.accepts()) # {'OP', '$END'} — a complete expression
tree = ip.feed_eof()

copy() forks at any point, so you can seed with a common prefix and branch to explore alternative continuations independently (in Python, unless the parser carries an embedded transformer= — that one combination refuses to fork).

The interactive parser also underpins error recovery: on the LALR parse path you can pass an on_error callback that receives the failure, inspects its accepts set and position, and decides whether to feed a corrective token and continue or let the error propagate. In Python that’s parser.parse(text, on_error=handler); in TypeScript, parser.parse(text, { on_error }) where on_error is an (err) => boolean (true resumes). The errors guide covers recovery in depth, including the Rust and C spellings.