Skip to content

Interactive parsing

The interactive parser gives you advanced control over parsing and error handling when parsing with LALR: feed it tokens one at a time, ask which terminals fit next, and fork it to try a continuation. Use it for autocomplete, REPLs and editors. Python, TypeScript and Rust also have a simpler interface to error handling: on_error, in the errors guide.

The tabs use this grammar as GRAMMAR:

start: NUMBER (OP NUMBER)*
OP: "+" | "-" | "*" | "/"
NUMBER: /[0-9]+/
%ignore " "
import hyperlark as lark
p = lark.Lark(GRAMMAR, parser="lalr") # interactive parsing needs LALR
ip = p.parse_interactive() # no text: tokens are fed by hand
print(ip.accepts()) # {'NUMBER'}
toks = list(p.lex("1 + 2")) # anything with .type and .value can be fed
ip.feed_token(toks[0]) # feed "1"
print(ip.accepts()) # {'OP', '$END'}: an operator, or the end of the input
branch = ip.copy() # feed this to test a token without moving ip
for tok in toks[1:]:
ip.feed_token(tok) # feed "+ 2"
tree = ip.feed_eof() # append $END, get the tree
print(tree.pretty())

A complete autocomplete program per language: the Examples page.

parse_interactive starts a session. Given text, the parser lexes it for you (Driving the built-in lexer); given none, you feed each token with feed_token. The start rule defaults to the grammar’s only one.

feed_token advances the parser by one token, as if it came from the lexer. A token that cannot follow what was fed raises UnexpectedToken, as from a failed parse (errors guide).

accepts() returns the set of tokens that will advance the parser into a new valid state. $END, the end-of-input token, is among them when the input may end here. choices() returns the raw parse-table row instead, {name: (kind, id)}. It also lists rule names, and terminals the parser would still reject, so test a token against accepts().

copy creates a new parser with a separate state, so feeding the fork tries a token or a continuation without moving the original.

feed_eof feeds $END and returns the tree; whatever has not been fed is discarded. resume_parse resumes automated parsing, lexing and feeding the rest of the text. Both finish the parser; fork it first to continue from the same state. Tokens you pass to feed_token do not advance the parser’s own lexer, so after feeding tokens by hand resume_parse would lex the text they stood for again: finish with feed_eof instead.

OperationPythonRustTypeScriptC
Createparse_interactive(text=None, start=None)OwnedInteractiveParser::newparseInteractive(text?, start?)lark_parse_interactive
Feed one tokenfeed_tokenfeed_tokenfeedTokenlark_interactive_feed_token
What fits nextaccepts / choicesaccepts / choicesaccepts / choiceslark_interactive_accepts_count / _get
Lex and feed the next tokenfeed_next / iter_parsefeed_nextfeedNext / iterParselark_interactive_feed_next
Lex and feed the restexhaust_lexerexhaust_lexerexhaustLexer(loop feed_next)
Peek before feedingpeek_nextpeek_next / retype_peeked(none)(none)
Finishfeed_eof / resume_parsefeed_eof / resume_parsefeedEof / resumeParselark_interactive_feed_eof / lark_interactive_resume_parse
Forkcopycopycopylark_interactive_copy
Release(GC)(drop)free() or usinglark_interactive_free
  • Python. feed_eof and resume_parse return a Tree, or with transformer= whatever the start rule’s callback produced.
  • Rust. They return a ParsedTree; to walk it with the cursor API (trees guide), pair it with the parse table: ParseResult { tree: Arc::new(tree), table: ip.parse_table() }.
  • TypeScript. feedEof() and resumeParse() return a ParseHandle. InteractiveParser and ParseHandle hold WASM memory: free() them when done, or declare them with using (see Large trees in TypeScript).
  • C. Check every LarkStatus; on failure feed_eof and resume_parse leave the result NULL. Free each parser handle, forks included, with lark_interactive_free, and the result with lark_result_free.

When the parser was given text, feed_next reads one token from the lexer, feeds it and returns it; exhaust_lexer feeds the rest, without $END. Python and TypeScript also have iter_parse / iterParse, which steps through the parse one token per iteration and yields each. Python’s yields each token before feeding it, so accepts() inside the loop reflects the parse before that token; TypeScript’s yields each token after feeding it.

A token feed_next rejects is already consumed, so a loop that ignores the error parses on without it and accepts input the grammar rejects. An unmatched character is not consumed, so the next feed_next reports it again.

ip = p.parse_interactive("1 + 2") # the parser lexes this text
ip.exhaust_lexer() # feed every token
print(ip.accepts()) # {'OP', '$END'}
tree = ip.feed_eof()

When the lexer cannot tell two terminals apart, set the token’s type yourself before it is fed.

Python’s iter_parse yields each token before feeding it, so set tok.type there. With a grammar that accepts NAME or KW:

for tok in ip.iter_parse():
if tok.type == "NAME" and tok.value == "select":
tok.type = "KW" # fed as KW

Outside a loop, Python’s peek_next returns the next token without feeding it; change it and pass it to feed_next(tok). In Rust, retype_peeked(type_id, value) rewrites the peeked token; the next feed_next, exhaust_lexer or resume_parse feeds it, feed_eof discards it, and feed_token inserts a token ahead of it.

TypeScript’s iterParse yields each token after feeding it, too late to retype it. Instead, lex with lex() and feed each token with feedToken({ type, value }), retyping as needed. The parser’s own lexer types some tokens by context (contextual lexer) and lex() cannot, so retype those too. C has no peek call or lex(): lex the text yourself and feed each token by name with lark_interactive_feed_token.

  • TypeScript: parseInteractive ignores the constructor’s transformer; feedEof() and resumeParse() return the untransformed tree.
  • Python: copy() does not carry a postlex into the fork, so the fork parses the raw tokens and can return a wrong tree or error, with no warning. An Indenter subclass forks correctly unless it overrides process, _process, handle_NL or always_accept.
  • Rust: copy() drops a custom postlex whose stream does not implement clone_stream; the fork continues without it. The built-in Indenter forks correctly.
  • Python, TypeScript and C: a token fed with feed_token carries no line or column into the tree or errors, even one from lex(); tokens the parser lexes itself keep theirs. In Rust a fed Token keeps its positions; Token::synthetic has none.
  • Python: calling an interactive parser from one of its own callbacks (lexer callback, postlex or transformer= method), or from another thread while a call is running, raises RuntimeError.

In Python and Rust, an on_error handler can insert a token before resuming, through e.interactive_parser.feed_token(..) (except with a custom lexer) and cx.interactive_parser().feed_token(..) (errors guide). TypeScript handlers can only skip or stop, and C has no on_error, so there you drive the parser yourself. Test a token against accepts() or on a copy() first: a rejected feed_token is not undone, so a replacement that would have fit before it can be refused after it.