Examples
Each section is one task, with a complete program for each language the table marks. Grammar and input are inline, so a file runs on its own, and its header comment has the run command. To install, see getting started for Python, TypeScript, Rust or C.
Hyperlark parses with LALR or Earley. Python, TypeScript and Rust default to Earley, so their programs select LALR where they use it. C has only LALR for now.
| Task | Python | TypeScript | Rust | C |
|---|---|---|---|---|
| Calculator: compute the value instead of a tree | ✅ | ✅ | ✅ | ✅ |
| JSON: transform, query or stream | transform | stream, query | query | |
| Interpreter: choose which children to visit | ✅ | ✅ | ✗ | |
| Indentation: INDENT and DEDENT tokens | ✅ | ✗ | ||
| Ambiguity: keep every reading | ✅ | ✅ | ✅ | ✗ |
| Interactive: ask what comes next | ✅ | ✅ | ✅ | ✅ |
| Error reporting: classify a failure by example | ✅ | ✗ | ✗ | ✗ |
An empty cell means no program yet; ✗ means the language lacks the feature (feature matrix). The JSON cells name what each program does.
Calculator
Section titled “Calculator”Evaluate arithmetic: each program’s transformer (in Rust and C, its fold function) computes the value instead of a tree (During the parse). This needs LALR (LALR and Earley).
"""Calculator: parse arithmetic, print the tree, then evaluate during the parsewith transformer=, so no tree is built. Adapted from Lark's calculator example,without the variables and the REPL.
Run: pip install hyperlark python calc.py"""
import hyperlarkfrom hyperlark import Transformer, v_args
GRAMMAR = r"""?start: sum?sum: product | sum "+" product -> add | sum "-" product -> sub?product: atom | product "*" atom -> mul | product "/" atom -> div?atom: NUMBER -> number | "-" atom -> neg | "(" sum ")"
%import common.NUMBER%import common.WS_INLINE%ignore WS_INLINE"""
@v_args(inline=True) # children arrive as positional argumentsclass Calc(Transformer): # Each rule name is bound to the matching operator function. from operator import add, mul, neg, sub from operator import truediv as div
number = float
# A plain parser first, to look at the tree: aliases (`-> add`) label the nodes.parser = hyperlark.Lark(GRAMMAR, parser="lalr")print(parser.parse("2 + 3 * 4").pretty())
# transformer= runs Calc during the parse: no tree is built, and parse()# returns the finished number.calc = hyperlark.Lark(GRAMMAR, parser="lalr", transformer=Calc())for expr in ["2 + 3 * 4", "(2 + 3) * 4", "-(10 / 4) + 0.5"]: print(f"{expr} = {calc.parse(expr)}")
# Parse errors carry a position and the caret context.try: calc.parse("1 + * 2")except hyperlark.exceptions.UnexpectedInput as e: print(f"as expected, '1 + * 2' fails at line {e.line}, column {e.column}:") print(e.get_context("1 + * 2"))// Calculator in TypeScript: parse arithmetic, print the tree, then evaluate// during the parse with a transformer, so no tree is built. The transformer's// result type is inferred, and the error classes narrow under instanceof.//// Run in a new directory (Node 22.6 or later):// npm install @hyperlark/hyperlark@beta// npm pkg set type=module// node --experimental-strip-types calc.ts
import { Lark, pretty, UnexpectedInput, type ReduceTransformer, type Token,} from "@hyperlark/hyperlark";
const GRAMMAR = String.raw`?start: sum?sum: product | sum "+" product -> add | sum "-" product -> sub?product: atom | product "*" atom -> mul | product "/" atom -> div?atom: NUMBER -> number | "-" atom -> neg | "(" sum ")"
%import common.NUMBER%import common.WS_INLINE%ignore WS_INLINE`;
const parser = new Lark(GRAMMAR, { parser: "lalr" });
// pretty() takes any Node (Tree | Token | null).console.log(pretty(parser.parse("2 + 3 * 4")));
// Children arrive as unknown[]; each method declares the shape its rule// produces. `satisfies` keeps each method's concrete type, so the parse// result below is inferred as number.const calc = { number: (c: Token[]) => Number(c[0].value), neg: (c: number[]) => -c[0], add: (c: number[]) => c[0] + c[1], sub: (c: number[]) => c[0] - c[1], mul: (c: number[]) => c[0] * c[1], div: (c: number[]) => c[0] / c[1],} satisfies ReduceTransformer;
for (const expr of ["2 + 3 * 4", "(2 + 3) * 4", "-(10 / 4) + 0.5"]) { const value = parser.parse(expr, { transformer: calc }); console.log(`${expr} = ${value}`);}
// instanceof narrows to UnexpectedInput, which carries line, column and// getContext.try { parser.parse("1 + * 2");} catch (e) { if (e instanceof UnexpectedInput) { console.log(`as expected, "1 + * 2" fails at line ${e.line}, column ${e.column}:`); console.log(e.getContext("1 + * 2")); } else { throw e; }}//! Calculator: parse arithmetic, print the tree, then evaluate during the//! parse with `parse_fold_fn`, so no tree is built.//!//! Run from `docs/examples/rust`://!//! cargo run --bin calc
use hyperlark::Lark;
/// Lark grammar syntax: `?rule` inlines single-child nodes, `-> name` gives an/// alternative its own tree label, `%import common.X` pulls a stdlib terminal.const GRAMMAR: &str = r#"?start: sum?sum: product | sum "+" product -> add | sum "-" product -> sub?product: atom | product "*" atom -> mul | product "/" atom -> div?atom: NUMBER -> number | "-" atom -> neg | "(" sum ")"
%import common.NUMBER%import common.WS_INLINE%ignore WS_INLINE"#;
fn main() -> Result<(), Box<dyn std::error::Error>> { // Lark::lalr and Lark::earley pick the engine; Lark::new defaults to Earley. // Folding during the parse needs LALR. Other options go through LarkOptions // and from_lark_source. let parser = Lark::lalr(GRAMMAR)?;
// The tree first: aliases (`-> add`) label the nodes. println!("{}", parser.parse("2 + 3 * 4")?.pretty());
// A fold is two functions: the value of each token type, and how each rule // (aliases included) combines its children's already-folded values. let mut on_leaf = |name: &str, leaf: hyperlark::FoldLeaf<'_>| -> f64 { match name { "NUMBER" => leaf.text().parse().unwrap(), // NUMBER is the only terminal kept in this grammar; nothing else // reaches here, so an unhandled name is a bug, not a value to invent. other => unreachable!("unexpected terminal: {other}"), } }; let mut on_rule = |name: &str, kids: Vec<f64>| -> f64 { match name { "add" => kids[0] + kids[1], "sub" => kids[0] - kids[1], "mul" => kids[0] * kids[1], "div" => kids[0] / kids[1], "neg" => -kids[0], "number" => kids[0], // passes its lexed child through // Named, not a catch-all: `_ => kids[0]` would silently return the // LEFT operand if a two-child rule were ever added and its arm // forgotten. Same reasoning as the leaf match above. other => unreachable!("unexpected rule: {other}"), } };
// parse_fold_fn runs the closures as the parser reduces, so no tree is // built. To fold a finished tree instead, use `parse(..)?.fold(..)`. for expr in ["2 + 3 * 4", "(2 + 3) * 4", "-(10 / 4) + 0.5"] { let value: f64 = parser.parse_fold_fn(expr, None, &mut on_leaf, &mut on_rule)?; println!("{expr} = {value}"); } // A fold visits every node. To choose what to visit (an if/else that // evaluates one branch), see `ParseResult::eval` in the interpreter example.
// The failure's error carries line(), column() and get_context(). let Err(failure) = parser.parse("1 + * 2") else { unreachable!("\"1 + * 2\" must fail to parse"); }; let err = failure.error; println!( "as expected, \"1 + * 2\" fails at line {}, column {}:", err.line().unwrap(), err.column().unwrap() ); print!("{}", err.get_context("1 + * 2"));
Ok(())}/* * Calculator in C: build a parser, print the tree, register fold callbacks by * rule name and token-type name, and evaluate during the parse with no tree * built. Also shows the last-error buffer and what to free. * * Build the library once from the Hyperlark source distribution (<hyperlark>), * then compile against it; see Getting started: C for other platforms. * * cargo build -p hyperlark-c --release * cc calc.c -I <hyperlark>/crates/hyperlark-c/include \ * <hyperlark>/target/release/libhyperlark_c.a -lpthread -ldl -lm -o calc * ./calc */
#include <stdbool.h>#include <stdio.h>#include <stdlib.h> /* strtod */#include <string.h>
#include "hyperlark.h"
static const char GRAMMAR[] = "?start: sum\n" "?sum: product\n" " | sum \"+\" product -> add\n" " | sum \"-\" product -> sub\n" "?product: atom\n" " | product \"*\" atom -> mul\n" " | product \"/\" atom -> div\n" "?atom: NUMBER -> number\n" " | \"-\" atom -> neg\n" " | \"(\" sum \")\"\n" "\n" "%import common.NUMBER\n" "%import common.WS_INLINE\n" "%ignore WS_INLINE\n";
/* Token text is (pointer, length), not NUL-terminated: copy it before strtod. * A literal too long for the buffer trips the userdata flag, since a fold * callback has no error channel. */static LarkValue number(void *u, uint32_t token_id, const char *text, size_t len) { (void)token_id; bool *ok = (bool *)u; char buf[64]; if (len >= sizeof buf) { fprintf(stderr, "number: literal too long for the buffer\n"); *ok = false; return (LarkValue){ .d = 0 }; } memcpy(buf, text, len); buf[len] = '\0'; return (LarkValue){ .d = strtod(buf, NULL) };}/* Rule callbacks get their children's values. */static LarkValue add(void *u, uint32_t rule_id, const LarkValue *k, size_t c){ (void)u; (void)rule_id; (void)c; return (LarkValue){ .d = k[0].d + k[1].d }; }static LarkValue sub(void *u, uint32_t rule_id, const LarkValue *k, size_t c){ (void)u; (void)rule_id; (void)c; return (LarkValue){ .d = k[0].d - k[1].d }; }static LarkValue mul(void *u, uint32_t rule_id, const LarkValue *k, size_t c){ (void)u; (void)rule_id; (void)c; return (LarkValue){ .d = k[0].d * k[1].d }; }static LarkValue divide(void *u, uint32_t rule_id, const LarkValue *k, size_t c){ (void)u; (void)rule_id; (void)c; return (LarkValue){ .d = k[0].d / k[1].d }; }static LarkValue neg(void *u, uint32_t rule_id, const LarkValue *k, size_t c){ (void)u; (void)rule_id; (void)c; return (LarkValue){ .d = -k[0].d }; }
int main(void) { /* LARK_OPT_NONE = the defaults (LALR, start rule "start"). */ Lark *parser = NULL; if (lark_from_source(GRAMMAR, sizeof(GRAMMAR) - 1, LARK_OPT_NONE, &parser) != LARK_OK) { fprintf(stderr, "grammar error: %s\n", lark_last_error()); return 1; }
/* The tree first. lark_result_pretty's string is owned by the result, so * only the result is freed. */ LarkParseResult *shape = NULL; if (lark_parse(parser, "2 + 3 * 4", 9, NULL, &shape) == LARK_OK) { printf("%s\n", lark_result_pretty(shape)); lark_result_free(shape); }
/* Register once; names resolve to ids here. The `number` rule needs no * callback: an unregistered rule passes its only child's value through. */ LarkReduceFold *calc = NULL; bool ok = lark_reduce_fold_new(parser, &calc) == LARK_OK; /* userdata: the flag `number` trips if a literal doesn't fit its buffer */ bool numbers_ok = true; ok &= lark_reduce_fold_token(calc, "NUMBER", number, &numbers_ok) == LARK_OK; ok &= lark_reduce_fold_rule(calc, "add", add, NULL) == LARK_OK; ok &= lark_reduce_fold_rule(calc, "sub", sub, NULL) == LARK_OK; ok &= lark_reduce_fold_rule(calc, "mul", mul, NULL) == LARK_OK; ok &= lark_reduce_fold_rule(calc, "div", divide, NULL) == LARK_OK; ok &= lark_reduce_fold_rule(calc, "neg", neg, NULL) == LARK_OK; if (!ok) { fprintf(stderr, "fold setup: %s\n", lark_last_error()); return 1; }
const char *exprs[] = {"2 + 3 * 4", "(2 + 3) * 4", "-(10 / 4) + 0.5"}; /* lark_parse_fold runs the callbacks as the parser reduces: no tree, no * result to free. lark_fold_apply folds a finished tree instead. */ for (size_t i = 0; i < sizeof(exprs) / sizeof(exprs[0]); i++) { LarkValue v = { .d = 0 }; numbers_ok = true; /* Two checks: the status covers the parse and the fold, numbers_ok * covers a literal the callback could not convert. */ if (lark_parse_fold(parser, exprs[i], strlen(exprs[i]), NULL, calc, &v) != LARK_OK) fprintf(stderr, "parse/fold error: %s\n", lark_last_error()); else if (numbers_ok) printf("%s = %g\n", exprs[i], v.d); else printf("%s = <unevaluated: a literal did not fit>\n", exprs[i]); }
/* A failing parse returns a status. The message, line and column, caret * context and expected terminals are on the thread-local last-error buffer. * The context needs no source text, because the buffer captured it. */ LarkParseResult *result = NULL; if (lark_parse(parser, "1 + * 2", 7, NULL, &result) != LARK_OK) { uint32_t line = 0, column = 0; lark_last_error_position(&line, &column); printf("as expected, \"1 + * 2\" fails at line %u, column %u:\n", line, column); const char *caret = lark_last_error_context(); if (caret) printf("%s", caret); /* ends in a newline */ /* The expected terminals, one per index. */ printf("expected:"); for (size_t i = 0; i < lark_last_error_expected_count(); i++) printf(" %s", lark_last_error_expected(i)); printf("\n"); } else { lark_result_free(result); /* not reached */ }
lark_reduce_fold_free(calc); lark_free(parser); return 0;}Get data out of a parsed document: convert it to native values, query the tree for the nodes you want, or stream the subtrees you choose to a callback as each completes. C has no program here yet; it can query and transform a tree, but not stream subtrees.
The program’s transformer turns the document into a dict, run both ways: on the finished tree, then during the parse (Transformers).
"""Parse JSON into plain Python objects with a Transformer, run two ways: 1. on the finished tree: ToDict().transform(parser.parse(text)) 2. during the parse: Lark(..., transformer=ToDict()), which builds no tree and is faster on large inputs
Adapted from Lark's JSON tutorial.
Run: pip install hyperlark python json_to_dict.py"""
import json
import hyperlarkfrom hyperlark import Transformer, v_args
GRAMMAR = r"""?start: value?value: object | array | string | SIGNED_NUMBER -> number | "true" -> true | "false" -> false | "null" -> nullarray : "[" [value ("," value)*] "]"object : "{" [pair ("," pair)*] "}"pair : string ":" valuestring : ESCAPED_STRING
%import common.ESCAPED_STRING%import common.SIGNED_NUMBER%import common.WS%ignore WS"""
DOC = """{ "name": "hyperlark", "engines": ["lalr", "earley"], "bindings": {"python": true, "js": true, "c": true}, "empty": [], "holes": [null, 1], "version": 0.1}"""
class ToDict(Transformer): """Each method replaces the rule of the same name with a Python value; rules whose children are already final map straight onto builtins."""
@v_args(inline=True) def string(self, s): # Tokens are FastToken by default, not a str subclass, so json.loads # needs str() first (or pass fast_tokens=False to Lark). json.loads # then unquotes and unescapes. return json.loads(str(s))
@v_args(inline=True) def number(self, n): return float(n)
array = list pair = tuple object = dict
def true(self, _): return True
def false(self, _): return False
def null(self, _): return None
# maybe_placeholders=False: otherwise an empty `[...]` optional inserts a None# child, indistinguishable from a JSON null after the transformer runs.OPTS = dict(parser="lalr", maybe_placeholders=False)
# 1. Post-parse: parse to a tree, then transform it.tree_parser = hyperlark.Lark(GRAMMAR, **OPTS)via_tree = ToDict().transform(tree_parser.parse(DOC))
# 2. During the parse: no tree is built.folding_parser = hyperlark.Lark(GRAMMAR, transformer=ToDict(), **OPTS)via_fold = folding_parser.parse(DOC)
assert via_tree == via_fold == json.loads(DOC)print(json.dumps(via_fold, indent=2))print("matches json.loads:", via_fold == json.loads(DOC))Read a large document without building its whole tree in JavaScript. The
program streams each completed pair subtree to a callback, then keeps
the tree in WebAssembly memory and copies out only the nodes it reads
(large trees in TypeScript).
Streaming needs LALR.
// Big documents without building the whole tree in JavaScript:// 1. parseStream: a callback receives each completed subtree as it reduces,// and can stop the parse early.// 2. parseHandle: the tree stays in wasm memory; cursors copy out only the// nodes you ask for.//// Run in a new directory (Node 22 or later):// npm install @hyperlark/hyperlark@beta// node json_stream.mjs
import { Lark } from "@hyperlark/hyperlark";
const GRAMMAR = String.raw`?start: value?value: object | array | ESCAPED_STRING -> string | SIGNED_NUMBER -> number | "true" -> true | "false" -> false | "null" -> nullarray : "[" [value ("," value)*] "]"object : "{" [pair ("," pair)*] "}"pair : ESCAPED_STRING ":" value
%import common.ESCAPED_STRING%import common.SIGNED_NUMBER%import common.WS%ignore WS`;
const DOC = JSON.stringify( Array.from({ length: 5000 }, (_, i) => ({ id: i, name: `user_${i}`, active: i % 3 === 0, })),);
// Streaming needs LALR; the default engine is Earley.const parser = new Lark(GRAMMAR, { parser: "lalr" });
// An ESCAPED_STRING token's value is the raw quoted slice (`'"name"'`);// JSON.parse unquotes it.const keyOf = (t) => JSON.parse(t.value);
// --- parseStream: every completed `pair` flows through the callback ---------let names = 0;const emitted = parser.parseStream( DOC, (pair) => { if (keyOf(pair.children[0]) === "name") names++; }, { on: "pair" },);console.log(`streamed ${emitted} pairs, ${names} of them "name" keys`);
// Returning false stops the parse.let firstName;parser.parseStream( DOC, (pair) => { if (keyOf(pair.children[0]) === "name") { // the value side is a `string` tree (the `-> string` alias), so the // raw token is one level down firstName = keyOf(pair.children[1].children[0]); return false; } }, { on: "pair" },);console.log(`first name in the document: ${firstName}`);
// --- parseHandle: the tree stays in wasm memory; copy out only what you need -// The handle holds wasm memory: free() it when done, or on Node 24 or later// write `using handle = parser.parseHandle(DOC)` to free it on scope exit.const handle = parser.parseHandle(DOC);let active = 0;// findData yields matching node positions; the cursor frees itself when the// loop ends.for (const pos of handle.findData("pair")) { const key = handle.token(handle.childPos(pos, 0)); if (keyOf(key) === "active" && handle.data(handle.childPos(pos, 1)) === "true") { active++; }}handle.free();console.log(`active users: ${active}`);Query the finished tree: the program lists every object key with its line and column, then counts the string tokens (Queries).
//! Query a parse tree: parse a JSON document, then pull out nodes by rule or//! terminal name.//!//! Run from `docs/examples/rust`://!//! cargo run --bin json_queries
use hyperlark::Lark;
const GRAMMAR: &str = r#"?start: value?value: object | array | ESCAPED_STRING -> string | SIGNED_NUMBER -> number | "true" -> true | "false" -> false | "null" -> nullarray : "[" [value ("," value)*] "]"object : "{" [pair ("," pair)*] "}"pair : ESCAPED_STRING ":" value
%import common.ESCAPED_STRING%import common.SIGNED_NUMBER%import common.WS%ignore WS"#;
const DOC: &str = r#"{ "name": "hyperlark", "engines": ["lalr", "earley"], "bindings": { "python": true, "js": true, "c": true }, "version": 0.1}"#;
fn main() -> Result<(), Box<dyn std::error::Error>> { let parser = Lark::lalr(GRAMMAR)?; let result = parser.parse(DOC)?;
// find_data("pair"): every object entry, as cursors, bottom-up (inner // pairs before the pairs that contain them). println!("all object keys:"); for pair in result.find_data("pair") { // pair: ESCAPED_STRING ":" value, so children[0] is the key token. let key = pair.children()[0].token().expect("pair key is a token"); println!( " {} (line {}, column {})", key.value, key.line(), key.column() ); }
// find_token("ESCAPED_STRING"): every token of that type in document // order, keys and string values alike. let strings = result.find_token("ESCAPED_STRING").count(); println!("string tokens in the document: {strings}");
Ok(())}Interpreter
Section titled “Interpreter”Run a program from its tree. A loop or an if must decide which children to
visit and how often. An Interpreter walks the tree starting at the root
(top-down), calling the method for each node according to tree.data.
Unlike Transformer and Visitor, it doesn’t automatically visit its
sub-branches: you call visit or visit_children explicitly, which allows
branching and loops. In Rust, eval does the same; C has none. See
Top-down.
Python’s program interprets a turtle language whose repeat visits its block
N times; Rust’s evaluates only the branch an if takes. TypeScript has the
same Interpreter class as Python.
"""A small turtle language, run with an Interpreter. Adapted from Lark's turtleexample, without the graphics window: it computes the pen's path instead.
An Interpreter visits top-down, and each method decides which children tovisit and how often, so `repeat N { ... }` runs its block N times. ATransformer visits every node once, bottom-up: by the time `repeat` runs, itsblock has already been transformed. (Compare rust/src/bin/interpreter.rs.)
Run: pip install hyperlark python dsl_interpreter.py"""
import math
import hyperlarkfrom hyperlark.visitors import Interpreter
# f/b = forward/back, l/r = turn left/right (degrees). `repeat N { ... }` takes# a nested block.GRAMMAR = r"""start: instruction+
?instruction: MOVE NUMBER -> move | TURN NUMBER -> turn | "repeat" NUMBER block -> repeat
block: "{" instruction+ "}"
MOVE: "f" | "b"TURN: "l" | "r"
%import common.NUMBER%import common.WS%ignore WS"""
PROGRAM = """repeat 4 { f 100 r 90}f 50"""
class Turtle(Interpreter): """Walks the tree top-down, keeping pen state as it goes. Each method is named after the rule, or the `-> alias`, it handles. `repeat` re-visits its block. `start` and `block` only recurse, as the inherited __default__ would."""
def __init__(self): self.x = self.y = 0.0 self.heading = 0.0 # degrees; 0 = +x axis self.segments = [] # (x0, y0, x1, y1) for each 'f'/'b' move
def start(self, tree): self.visit_children(tree) # run each top-level instruction in order
def move(self, tree): direction, dist = tree.children # MOVE token, NUMBER token step = float(dist) * (1 if direction == "f" else -1) rad = math.radians(self.heading) x1 = self.x + step * math.cos(rad) y1 = self.y + step * math.sin(rad) self.segments.append((self.x, self.y, x1, y1)) self.x, self.y = x1, y1
def turn(self, tree): direction, degrees = tree.children self.heading += float(degrees) * (1 if direction == "l" else -1)
def repeat(self, tree): count, block = tree.children # NUMBER token, block subtree for _ in range(int(count)): self.visit(block) # visit the same block N times
def block(self, tree): self.visit_children(tree)
parser = hyperlark.Lark(GRAMMAR, parser="lalr")turtle = Turtle()turtle.visit(parser.parse(PROGRAM))
# `+ 0.0` normalises the -0.0 that trig at right angles produces, so the closing# corner reads (0.0, 0.0) rather than (-0.0, -0.0).def _z(v, places=1): return round(v, places) + 0.0
print(f"{len(turtle.segments)} pen strokes:")for x0, y0, x1, y1 in turtle.segments: print(f" ({_z(x0):6.1f}, {_z(y0):6.1f}) -> ({_z(x1):6.1f}, {_z(y1):6.1f})")
xs = [c for seg in turtle.segments for c in (seg[0], seg[2])]ys = [c for seg in turtle.segments for c in (seg[1], seg[3])]print(f"bounding box: x [{_z(min(xs), 0):.0f}, {_z(max(xs), 0):.0f}] " f"y [{_z(min(ys), 0):.0f}, {_z(max(ys), 0):.0f}]")//! A top-down evaluator: `ParseResult::eval` lets each match arm decide which//! children to evaluate, so `if` evaluates only the branch it takes. A//! bottom-up fold (see `calc.rs`) has both branches folded before the `if`//! rule runs.//!//! Run from `docs/examples/rust`://!//! cargo run --bin interpreter
use std::cell::Cell;
use hyperlark::Lark;
const GRAMMAR: &str = r#"?start: expr?expr: "if" expr "then" expr "else" expr -> if_expr | sum?sum: product | sum "+" product -> add | sum "-" product -> sub?product: atom | product "*" atom -> mul | product "/" atom -> div?atom: NUMBER -> number | "(" expr ")"
%import common.NUMBER%import common.WS%ignore WS"#;
fn main() -> Result<(), Box<dyn std::error::Error>> { let parser = Lark::lalr(GRAMMAR)?;
// Same expression twice; only the condition differs. The visit counter // shows the untaken branch is never entered. for expr in [ "if 1 then 2 + 3 else 100 * (4 - 5) / 6", "if 0 then 2 + 3 else 100 * (4 - 5) / 6", ] { let result = parser.parse(expr)?;
// One match covers rule aliases (lowercase) and token types (uppercase). // `n.eval(i)` evaluates one child; children not asked for are never visited. let visited = Cell::new(0u32); let value: f64 = result.eval(|n| { visited.set(visited.get() + 1); match n.name() { "if_expr" => { if n.eval(0) != 0.0 { n.eval(1) } else { n.eval(2) } } "add" => n.eval(0) + n.eval(1), "sub" => n.eval(0) - n.eval(1), "mul" => n.eval(0) * n.eval(1), "div" => n.eval(0) / n.eval(1), "number" => n.eval(0), "NUMBER" => n.text().parse().unwrap(), other => unreachable!("unexpected node: {other}"), } }); println!("{expr} => {value} ({} nodes visited)", visited.get()); }
// When every node should be visited, use the bottom-up fold in calc.rs. Ok(())}Indentation
Section titled “Indentation”Languages like Python and YAML carry structure in their leading whitespace. The
grammar declares _INDENT / _DEDENT; the bundled Indenter postlexer generates these tokens based on the
indentation it sees.
Rust has the same Indenter. TypeScript has only a fixed preset, whose
newline terminal is _NEWLINE, not this program’s _NL. C has no postlex.
See the lexers guide.
"""Parse an indentation-based language with the Indenter postlex: the grammardeclares _INDENT and _DEDENT, and the Indenter emits them from each line'sleading whitespace.
Run: pip install hyperlark python indented_language.py"""
import hyperlarkfrom hyperlark.indenter import Indenter
GRAMMAR = r"""?start: _NL* treetree: NAME _NL [_INDENT tree+ _DEDENT]
NAME: /\w+/_NL: /(\r?\n[\t ]*)+/
%declare _INDENT _DEDENT
// _NL matches the newline and the next line's indentation. WS_INLINE// matches spaces inside a line and is ignored.%import common.WS_INLINE%ignore WS_INLINE"""
DOC = """\root branch leaf leaf2 branch2"""
class TreeIndenter(Indenter): NL_type = "_NL" OPEN_PAREN_types = [] # brackets that suspend indentation, e.g. ["LPAR"] CLOSE_PAREN_types = [] INDENT_type = "_INDENT" DEDENT_type = "_DEDENT" tab_len = 8
parser = hyperlark.Lark(GRAMMAR, parser="lalr", postlex=TreeIndenter())print(parser.parse(DOC).pretty())Ambiguity
Section titled “Ambiguity”Some grammars can be interpreted in more than one way, and for some inputs can result in many possible derivations. Hyperlark’s Earley can store each derivation in an efficiently packed forest.
With the explicit ambiguity option, the parser returns all derivations,
wrapped in _ambig tree nodes; the default, resolve, chooses one. Python’s
program runs CollapseAmbiguities, which transforms a tree that
contains any number of _ambig nodes into a list of trees, each one
unambiguous. TypeScript and Rust have no CollapseAmbiguities: their
programs read the one _ambig node this grammar produces, and with nested
ones you combine the readings yourself.
This needs Earley (LALR and Earley), which C does not support yet.
"""Some inputs parse more than one way, and the Earley parser can keep everyreading. Adapted from Lark's "fruit flies like bananas" example.
ambiguity="explicit" the tree keeps an `_ambig` node wherever more than one derivation exists, with each reading as a child CollapseAmbiguities expands that tree into the list of plain trees it stands for
Only Earley can report ambiguity. LALR parses this grammar too, committing toone reading with no way to ask for the others.
Run: pip install hyperlark python earley_ambiguity.py"""
import hyperlarkfrom hyperlark.visitors import CollapseAmbiguities
# "fruit flies like bananas" reads two ways: fruit flies (the insect) that like# bananas, or fruit that flies the way bananas do. NOUN, VERB and ADJ overlap# on purpose.GRAMMAR = r"""sentence: noun verb noun -> simple | noun verb "like" noun -> comparative
noun: adj? NOUNverb: VERBadj: ADJ
NOUN: "flies" | "bananas" | "fruit"VERB: "like" | "flies"ADJ: "fruit"
%import common.WS%ignore WS"""
# Earley is the default engine, so no parser= is needed.parser = hyperlark.Lark(GRAMMAR, start="sentence", ambiguity="explicit")
tree = parser.parse("fruit flies like bananas")
# The `_ambig` node at the root holds both readings side by side.print("packed tree (both readings under one _ambig node):")print(tree.pretty())
# CollapseAmbiguities expands the tree into one plain tree per reading, for a# Transformer or any tree code that does not expect `_ambig`.readings = CollapseAmbiguities().transform(tree)print(f"{len(readings)} distinct readings:")for i, reading in enumerate(readings, 1): # reading.data is the alias of the top alternative: 'simple' or 'comparative' print(f" {i}. {reading.data}")
# The default, ambiguity="resolve", returns one tree. This grammar declares no# priorities, so the alternative written first wins: swap the two `sentence`# lines and the answer flips to `comparative`.resolving = hyperlark.Lark(GRAMMAR, start="sentence") # ambiguity="resolve"chosen = resolving.parse("fruit flies like bananas")assert chosen.data != "_ambig"print(f"with the default resolver, the engine commits to: {chosen.data}")// Some inputs parse more than one way, and the Earley parser can keep every// reading. The "fruit flies like bananas" example.//// ambiguity: "explicit" the tree keeps an `_ambig` node wherever more than// one derivation exists, with each reading as a child//// Only Earley can report ambiguity, and it is the default engine, so no// parser option is needed. LALR parses this grammar too, committing to one// reading.//// Run in a new directory (Node 22 or later):// npm install @hyperlark/hyperlark@beta// node earley_ambiguity.mjs
import { Lark, pretty, findData } from "@hyperlark/hyperlark";
const GRAMMAR = String.raw`sentence: noun verb noun -> simple | noun verb "like" noun -> comparative
noun: adj? NOUNverb: VERBadj: ADJ
NOUN: "flies" | "bananas" | "fruit"VERB: "like" | "flies"ADJ: "fruit"
%import common.WS%ignore WS`;
const parser = new Lark(GRAMMAR, { start: "sentence", ambiguity: "explicit" });
const tree = parser.parse("fruit flies like bananas");
// The `_ambig` node holds both readings side by side.console.log("packed tree (both readings under one _ambig node):");console.log(pretty(tree));
// findData yields matching nodes bottom-up, root last, so the last match is// the outermost `_ambig`. This grammar has one; nested ambiguity would have more.const ambigs = [...findData(tree, "_ambig")];const ambig = ambigs.at(-1);const readings = ambig ? ambig.children : [tree];console.log(`${readings.length} distinct readings:`);for (const [i, reading] of readings.entries()) { console.log(` ${i + 1}. ${reading.data}`);}
// The default, ambiguity: "resolve", returns one tree. This grammar declares// no priorities, so the alternative written first wins: swap the two// `sentence` lines and this flips.const resolving = new Lark(GRAMMAR, { start: "sentence" });const chosen = resolving.parse("fruit flies like bananas");console.log(`with the default resolver, the engine commits to: ${chosen.data}`);//! Some inputs parse more than one way, and the Earley parser can keep every//! reading. The "fruit flies like bananas" example.//!//! `Ambiguity::Explicit` keeps an `_ambig` node in the tree wherever more than//! one derivation exists, with each reading as a child. Only Earley can report//! ambiguity, so the parser is built with `ParserKind::Earley`. LALR parses//! this grammar too, committing to one reading.//!//! Run from `docs/examples/rust`://!//! cargo run --bin earley_ambiguity
use hyperlark::{Ambiguity, Lark, LarkOptions, NodeKind, ParserKind};
const GRAMMAR: &str = r#"sentence: noun verb noun -> simple | noun verb "like" noun -> comparative
noun: adj? NOUNverb: VERBadj: ADJ
NOUN: "flies" | "bananas" | "fruit"VERB: "like" | "flies"ADJ: "fruit"
%import common.WS%ignore WS"#;
fn main() -> Result<(), Box<dyn std::error::Error>> { // LarkOptions over Default: Earley, explicit ambiguity, start rule "sentence". let parser = Lark::from_lark_source( GRAMMAR, LarkOptions { parser: Some(ParserKind::Earley), ambiguity: Ambiguity::Explicit, start: vec!["sentence".to_string()], ..Default::default() }, )?;
let result = parser.parse("fruit flies like bananas")?;
// The `_ambig` node holds both readings side by side. println!("packed tree (both readings under one _ambig node):"); print!("{}", result.pretty());
// The readings are the children of the `_ambig` node. find_data does not // match `_ambig`, so match on the root's kind directly. let root = result.root(); let readings: Vec<_> = match root.kind() { NodeKind::Tree { data: "_ambig" } => root.children(), _ => vec![root], // unambiguous: the whole tree is the one reading }; println!("{} distinct readings:", readings.len()); for (i, reading) in readings.iter().enumerate() { // each reading's data is its rule alias: "simple" or "comparative" if let NodeKind::Tree { data } = reading.kind() { println!(" {}. {data}", i + 1); } }
// The default `Ambiguity::Resolve` returns one tree. This grammar declares // no priorities, so the alternative written first wins: swap the two // `sentence` lines and this flips to `comparative`. let resolving = Lark::from_lark_source( GRAMMAR, LarkOptions { parser: Some(ParserKind::Earley), start: vec!["sentence".to_string()], ..Default::default() }, )?; let chosen = resolving.parse("fruit flies like bananas")?; if let NodeKind::Tree { data } = chosen.root().kind() { println!("with the default resolver, the engine commits to: {data}"); }
Ok(())}Interactive parsing
Section titled “Interactive parsing”Drive an LALR parse programmatically, one token at a time. At any point, ask what could legally come next?, or fork the instance, in order to explore several avenues.
Useful for debugging, error recovery, writing autocomplete, storing “savepoints” for IDEs, and more.
Each program completes partial queries, as an autocomplete would. This needs LALR and the built-in lexer. Python’s loop pauses before each token is fed; the others pause after (driving the built-in lexer). The interactive guide covers every operation per language.
"""The interactive parser: drive an LALR parse one token at a time and ask, atany point, what could come next. That is what an autocomplete needs.
parse_interactive(text) -> InteractiveParser .iter_parse() yields each token from the built-in lexer before feeding it; a generator, so you can inspect state, retype the yielded token, and `break` to stop .accepts() the terminal names valid in the current state .copy() fork, to try a token without committing .exhaust_lexer() feed every remaining token in one go .feed_eof() finish with a synthesized $END -> parse tree .resume_parse() drive whatever is left + $END -> parse tree
Interactive parsing needs LALR, so parser="lalr".
Run: pip install hyperlark python interactive_autocomplete.py"""
import hyperlarkfrom hyperlark import Token
# A toy query language. The keywords become terminals named after themselves# (SELECT, FROM, WHERE), so the accept-set reads like a list of suggestions.GRAMMAR = r"""start: "SELECT" columns "FROM" NAME where?columns: "*" | NAME ("," NAME)*where: "WHERE" NAME COMP valuevalue: NUMBER | ESCAPED_STRINGCOMP: ">" | "<" | "="NAME: /[a-z_]+/
%import common.NUMBER%import common.ESCAPED_STRING%import common.WS%ignore WS"""
parser = hyperlark.Lark(GRAMMAR, parser="lalr")
QUERY = "SELECT id, name FROM users WHERE id = 42"
# Labels for the terminal names the parser reports: keywords stand for# themselves, open-ended terminals get a placeholder, anything else falls# through to its raw name.LABELS = { "SELECT": "SELECT", "FROM": "FROM", "WHERE": "WHERE", "STAR": "*", "COMMA": ",", "COMP": "> < =", "NAME": "<column/table>", "NUMBER": "<number>", "ESCAPED_STRING": '"<text>"', "$END": "(end of query)",}
def labelled(names): return ", ".join(LABELS.get(n, n) for n in sorted(names))
# --- 1. Walk the parse with iter_parse --------------------------------------# iter_parse() yields each token before feeding it, so accepts() in the loop# body answers "what could stand at this token's spot"; the yielded token's# own type is always among them.print(f"walking {QUERY!r} token by token:\n")ip = parser.parse_interactive(QUERY)for tok in ip.iter_parse(): print(f" {tok.type:15} {str(tok.value):8} ← could be: {labelled(ip.accepts())}")
# iter_parse stops at end of input without feeding EOF; finish explicitly.tree = ip.feed_eof()print(f"\nfeed_eof() completed the parse: {tree.data}")
# --- 2. Stop early, then take over ------------------------------------------# `break` leaves the parser suspended mid-input: inspect it, fork it, or drive# it to the end. The yielded but unfed token (FROM here) stays queued, and the# next driving call feeds it first.ip = parser.parse_interactive(QUERY)for tok in ip.iter_parse(): if tok.type == "FROM": break # pause the parse at the FROM keyword (yielded, not yet fed)print(f"\npaused at FROM — the parser expects: {labelled(ip.accepts())}")
# copy() forks an independent parser: try a token on the fork to see whether it# would be legal here, leaving the real parse untouched.fork = ip.copy()try: fork.feed_token(Token("COMMA", ",")) comma_ok = Trueexcept hyperlark.UnexpectedToken: comma_ok = Falseprint(f"would ',' be legal at this point? {comma_ok}")
# resume_parse feeds the rest of the input, starting with the queued FROM,# then $END, and returns the tree. (In Lark the yielded token is lost and# this resume fails.)print("resume_parse() finished the rest:")print(ip.resume_parse().pretty())
# --- 3. Completion for a half-typed query -----------------------------------# Feed everything typed so far with exhaust_lexer(), then read the accept-set.def suggest(prefix): ip = parser.parse_interactive(prefix) ip.exhaust_lexer() return labelled(ip.accepts())
print("completion after each prefix:")for prefix in ["", "SELECT", "SELECT name", "SELECT name, email FROM users", "SELECT * FROM users WHERE age"]: print(f" {prefix or '(empty)':32} → {suggest(prefix)}")// The interactive parser: drive an LALR parse one token at a time and ask, at// any point, what could come next. That is what an autocomplete needs.//// parser.parseInteractive(text) -> InteractiveParser// .iterParse() feeds each token from the built-in lexer, then// yields it; a generator, so you can inspect state// between tokens and `break` to stop// .accepts() terminal names valid in the current state// .copy() fork, to try a token without committing// .exhaustLexer() feed every remaining token in one go// .feedEof() finish with a synthesized $END -> parse tree// .resumeParse() drive whatever is left + $END -> parse tree//// Interactive parsing needs LALR, so parser: "lalr" (the default is Earley).// These handles hold wasm memory: free() each one, or on Node 24 or later// write `using handle = ...` to free it on scope exit. feedEof and resumeParse// return a ParseHandle, as parseHandle does in json_stream.mjs.//// Run in a new directory (Node 22 or later):// npm install @hyperlark/hyperlark@beta// node interactive.mjs
import { Lark, pretty, UnexpectedToken } from "@hyperlark/hyperlark";
// A toy query language. The keywords become terminals named after themselves// (SELECT, FROM, WHERE), so the accept-set reads like a list of suggestions.const GRAMMAR = String.raw`start: "SELECT" columns "FROM" NAME where?columns: "*" | NAME ("," NAME)*where: "WHERE" NAME COMP valuevalue: NUMBER | ESCAPED_STRINGCOMP: ">" | "<" | "="NAME: /[a-z_]+/
%import common.NUMBER%import common.ESCAPED_STRING%import common.WS%ignore WS`;
const parser = new Lark(GRAMMAR, { parser: "lalr" });
const QUERY = "SELECT id, name FROM users WHERE id = 42";
// Labels for the terminal names the parser reports.const LABELS = { SELECT: "SELECT", FROM: "FROM", WHERE: "WHERE", STAR: "*", COMMA: ",", COMP: "> < =", NAME: "<column/table>", NUMBER: "<number>", ESCAPED_STRING: '"<text>"', $END: "(end of query)",};
const labelled = (names) => names.sort().map((n) => LABELS[n] ?? n).join(", ");
// --- 1. Walk the parse with iterParse ---------------------------------------// iterParse() feeds each token and then yields it, so accepts() in the loop// body answers "what could follow this token".console.log(`walking ${JSON.stringify(QUERY)} token by token:\n`);let ip = parser.parseInteractive(QUERY);for (const tok of ip.iterParse()) { console.log(` ${tok.type.padEnd(15)} ${String(tok.value).padEnd(8)} → next: ${labelled(ip.accepts())}`);}
// iterParse stops at end of input without feeding EOF; finish explicitly.let handle = ip.feedEof();console.log(`\nfeedEof() completed the parse: ${handle.data(handle.rootPos())}`);handle.free();ip.free();
// --- 2. Stop early, then take over ------------------------------------------// `break` leaves the parser suspended mid-input: inspect it, fork it, or// drive it to the end.ip = parser.parseInteractive(QUERY);for (const tok of ip.iterParse()) { if (tok.type === "FROM") break; // pause the parse right after the FROM keyword}console.log(`\npaused after FROM — the parser now expects: ${labelled(ip.accepts())}`);
// copy() forks an independent parser: try a token on the fork to see whether it// would be legal here, leaving the real parse untouched.const fork = ip.copy();let commaOk = true;try { fork.feedToken({ type: "COMMA", value: "," });} catch (e) { if (!(e instanceof UnexpectedToken)) throw e; commaOk = false;} finally { fork.free(); // released on the rethrow path too}console.log(`would ',' be legal at this point? ${commaOk}`);
// The suspended parser still has the rest of the input queued; resumeParse// drives it plus $END to a finished tree.handle = ip.resumeParse();console.log("resumeParse() finished the rest:");console.log(pretty(handle.toJs(handle.rootPos())));handle.free();ip.free();
// --- 3. Completion for a half-typed query -----------------------------------// Feed everything typed so far with exhaustLexer(), then read the accept-set.function suggest(prefix) { const p = parser.parseInteractive(prefix); try { p.exhaustLexer(); return labelled(p.accepts()); } finally { p.free(); // wasm memory: release it even if the lex throws }}
console.log("completion after each prefix:");for (const prefix of ["", "SELECT", "SELECT name", "SELECT name, email FROM users", "SELECT * FROM users WHERE age"]) { console.log(` ${(prefix || "(empty)").padEnd(32)} → ${suggest(prefix)}`);}//! The interactive parser: drive an LALR parse one token at a time and ask, at//! any point, what could come next. That is what an autocomplete needs.//!//! `OwnedInteractiveParser` owns its input and what it needs of the grammar, so//! you can hold it, move it between threads, and `copy()` it.//!//! feed_next() pull one token from the built-in lexer, feed it, return//! it; looping on it is Rust's `iter_parse`, and `break` stops//! peek_next() borrow the next token as `&mut` without feeding it//! accepts() the terminal names valid in the current state//! feed_token(tok) feed a token you built yourself//! copy() fork, to try a token without committing//! exhaust_lexer() feed every remaining token in one go//! feed_eof() / resume_parse() finish into a tree//!//! Run from `docs/examples/rust`://!//! cargo run --bin interactive
use std::sync::Arc;
use hyperlark::{Lark, OwnedInteractiveParser, ParseError, ParseResult, Token};
// A toy query language. The keywords become terminals named after themselves// (SELECT, FROM, WHERE), so the accept-set reads like a list of suggestions.const GRAMMAR: &str = r#"start: "SELECT" columns "FROM" NAME where?columns: "*" | NAME ("," NAME)*where: "WHERE" NAME COMP valuevalue: NUMBER | ESCAPED_STRINGCOMP: ">" | "<" | "="NAME: /[a-z_]+/
%import common.NUMBER%import common.ESCAPED_STRING%import common.WS%ignore WS"#;
const QUERY: &str = "SELECT id, name FROM users WHERE id = 42";
/// Labels for the terminal names the parser reports.fn label(name: &str) -> &str { match name { "STAR" => "*", "COMMA" => ",", "COMP" => "> < =", "NAME" => "<column/table>", "NUMBER" => "<number>", "ESCAPED_STRING" => "\"<text>\"", "$END" => "(end of query)", other => other, // keywords are named after themselves }}
fn labelled(mut names: Vec<String>) -> String { names.sort(); names .iter() .map(|n| label(n)) .collect::<Vec<_>>() .join(", ")}
fn main() -> Result<(), Box<dyn std::error::Error>> { let lark = Lark::lalr(GRAMMAR)?; // The parse table maps terminal names to the ids Token carries. let table = lark.parse_table(); let from_id = table.token_id("FROM").expect("declared terminal");
// --- 1. Walk the parse, one token at a time ----------------------------- // feed_next() feeds the next token and then returns it, so accepts() in // the loop body answers "what could follow this token". println!("walking {QUERY:?} token by token:\n"); let mut ip = OwnedInteractiveParser::new(&lark, QUERY.to_string(), None)?; while let Some(tok) = ip.feed_next()? { // Token carries a type id; the table maps it back to the name. let ty = &table.id_to_token[tok.type_id]; println!( " {:15} {:8} -> next: {}", ty, tok.value, labelled(ip.accepts()) ); }
// The loop ends at end of input without feeding EOF; finish explicitly. // feed_eof returns a bare ParsedTree; paired with the table it is the same // ParseResult a plain parse returns. let done = ParseResult { tree: Arc::new(ip.feed_eof()?), table: ip.parse_table(), }; println!( "\nfeed_eof() completed the parse: {}", done.root().data().unwrap_or("?") );
// --- 2. Stop early, then take over -------------------------------------- // `break` leaves the parser suspended mid-input: inspect it, fork it, or // drive it to the end. let mut ip = OwnedInteractiveParser::new(&lark, QUERY.to_string(), None)?; while let Some(tok) = ip.feed_next()? { // Match on the token's type id, which identifies the terminal. if tok.type_id == from_id { break; // pause the parse right after the FROM keyword } } println!( "\npaused after FROM - the parser now expects: {}", labelled(ip.accepts()) );
// copy() forks an independent parser: try a token on the fork to see // whether it would be legal here, leaving the real parse untouched. let comma = Token::synthetic(table.token_id("COMMA").expect("declared terminal"), ","); let mut fork = ip.copy(); // UnexpectedToken is the answer; any other error is a bug in this file. let comma_ok = match fork.feed_token(comma) { Ok(()) => true, Err(ParseError::UnexpectedToken { .. }) => false, Err(e) => return Err(e.into()), }; println!("would ',' be legal at this point? {comma_ok}");
// The suspended parser still has the rest of the input queued; resume_parse // drives it plus $END to a finished tree. let rest = ParseResult { tree: Arc::new(ip.resume_parse()?), table: ip.parse_table(), }; println!("resume_parse() finished the rest:"); print!("{}", rest.pretty());
// --- 3. Completion for a half-typed query ------------------------------- // Feed everything typed so far with exhaust_lexer(), then read the accept-set. println!("\ncompletion after each prefix:"); for prefix in [ "", "SELECT", "SELECT name", "SELECT name, email FROM users", "SELECT * FROM users WHERE age", ] { let mut ip = OwnedInteractiveParser::new(&lark, prefix.to_string(), None)?; ip.exhaust_lexer()?; let shown = if prefix.is_empty() { "(empty)" } else { prefix }; println!(" {shown:32} -> {}", labelled(ip.accepts())); }
Ok(())}/* * Interactive parsing in C: drive an LALR parse one token at a time and ask, at * any point, what could come next. That is what an autocomplete needs. * * lark_parse_interactive() seat a parser over the text * lark_interactive_feed_next() pull one token from the built-in lexer, * feed it, report it; looping on it is * iter_parse, and `break` stops * lark_interactive_accepts_count/get the accept-set, as a count plus * indexed reads * lark_interactive_feed_token() feed a token by terminal name and value * lark_interactive_copy() fork, to try a token without committing * lark_interactive_feed_eof() / finish into a tree * lark_interactive_resume_parse() * * Build the library once from the Hyperlark source distribution (<hyperlark>), * then compile against it; see Getting started: C for other platforms. * * cargo build -p hyperlark-c --release * cc interactive.c -I <hyperlark>/crates/hyperlark-c/include \ * <hyperlark>/target/release/libhyperlark_c.a -lpthread -ldl -lm -o interactive * ./interactive */
#include <stdbool.h>#include <stdio.h>#include <stdlib.h> /* exit */#include <string.h>
#include "hyperlark.h"
/* A toy query language. The keywords become terminals named after themselves * (SELECT, FROM, WHERE), so the accept-set reads like a list of suggestions. */static const char GRAMMAR[] = "start: \"SELECT\" columns \"FROM\" NAME where?\n" "columns: \"*\" | NAME (\",\" NAME)*\n" "where: \"WHERE\" NAME COMP value\n" "value: NUMBER | ESCAPED_STRING\n" "COMP: \">\" | \"<\" | \"=\"\n" "NAME: /[a-z_]+/\n" "\n" "%import common.NUMBER\n" "%import common.ESCAPED_STRING\n" "%import common.WS\n" "%ignore WS\n";
static const char QUERY[] = "SELECT id, name FROM users WHERE id = 42";
/* Every feed below is one the grammar accepts, so a failure is a bug in this * file: abort. */static void die(const char *what) { fprintf(stderr, "%s: %s\n", what, lark_last_error()); exit(1);}
/* accepts_count snapshots the current state's terminals (sorted); accepts_get * reads them back. Each pointer is valid until the next accepts_count on this * handle. */static void show_accepts(LarkInteractive *ip) { size_t n = lark_interactive_accepts_count(ip); for (size_t i = 0; i < n; i++) printf(" %s", lark_interactive_accepts_get(ip, i)); putchar('\n');}
int main(void) { Lark *parser = NULL; if (lark_from_source(GRAMMAR, sizeof(GRAMMAR) - 1, LARK_OPT_NONE, &parser) != LARK_OK) die("grammar error");
/* --- 1. Walk the parse, one token at a time --------------------------- */ /* feed_next() feeds a token, then fills a LarkTokenEvent with it, so the * accept-set printed below is what could follow it. The event's value is a * borrowed (ptr,len) span; lark_token_name maps its type id to a name. */ printf("walking \"%s\" token by token:\n\n", QUERY); LarkInteractive *ip = NULL; if (lark_parse_interactive(parser, QUERY, sizeof(QUERY) - 1, NULL, &ip) != LARK_OK) die("interactive setup"); for (;;) { LarkTokenEvent ev; bool has_token; if (lark_interactive_feed_next(ip, &ev, &has_token) != LARK_OK) die("feed_next"); if (!has_token) break; const char *name = NULL; size_t name_len = 0; if (lark_token_name(parser, ev.type_id, &name, &name_len) != LARK_OK) die("token_name"); printf(" %-15.*s %-8.*s -> next:", (int)name_len, name, (int)ev.value_len, ev.value); show_accepts(ip); }
/* The walk ends at end of input without feeding EOF; finish explicitly. * feed_eof synthesizes "$END" and returns the completed tree. */ LarkParseResult *result = NULL; if (lark_interactive_feed_eof(ip, &result) != LARK_OK) die("feed_eof"); printf("\nfeed_eof() completed the parse:\n%s", lark_result_pretty(result)); lark_result_free(result); /* frees the arena and every node view into it */ lark_interactive_free(ip);
/* --- 2. Stop early, speculate, then take over ------------------------- */ /* `break` leaves the parser suspended mid-input: inspect it, fork it, or * drive it to the end. Match on the token's type id; lark_token_id resolves * the name once, up front. */ uint32_t from_id = 0; if (lark_token_id(parser, "FROM", 4, &from_id) != LARK_OK) die("token_id"); if (lark_parse_interactive(parser, QUERY, sizeof(QUERY) - 1, NULL, &ip) != LARK_OK) die("interactive setup"); for (;;) { LarkTokenEvent ev; bool has_token; if (lark_interactive_feed_next(ip, &ev, &has_token) != LARK_OK) die("feed_next"); if (!has_token || ev.type_id == from_id) break; /* pause the parse right after the FROM keyword */ } printf("\npaused after FROM - the parser now expects:"); show_accepts(ip);
/* copy() forks an independent parser. Test whether a stray comma would be * legal right after FROM — on the fork, so `ip` is left untouched. The * fork is fed BY NAME: feed_token builds the token from a terminal name * and a value span. Note the two failure modes are distinct: * LARK_UNEXPECTED_TOKEN means the state rejected a real terminal (the * answer we want), while LARK_INVALID_ARGUMENT would mean "COMMA" isn't a * declared terminal at all — a typo, not a parse answer. */ LarkInteractive *fork = NULL; if (lark_interactive_copy(ip, &fork) != LARK_OK) die("copy"); LarkStatus comma = lark_interactive_feed_token(fork, "COMMA", ",", 1); if (comma != LARK_OK && comma != LARK_UNEXPECTED_TOKEN) die("speculative feed"); printf("would ',' be legal at this point? %s\n", comma == LARK_OK ? "yes" : "no"); lark_interactive_free(fork);
/* The suspended parser still has the rest of the input queued; * resume_parse drives it plus "$END" to a finished tree. */ if (lark_interactive_resume_parse(ip, &result) != LARK_OK) die("resume_parse"); printf("resume_parse() finished the rest:\n%s", lark_result_pretty(result)); lark_result_free(result); lark_interactive_free(ip);
/* --- 3. Completion for a half-typed query ----------------------------- */ /* Seat a parser over everything typed so far, feed_next until has_token * goes false (C's exhaust_lexer), then read the accept-set. */ printf("\ncompletion after each prefix:\n"); static const char *PREFIXES[] = { "", "SELECT", "SELECT name", "SELECT name, email FROM users", "SELECT * FROM users WHERE age", }; for (size_t i = 0; i < sizeof(PREFIXES) / sizeof(PREFIXES[0]); i++) { const char *prefix = PREFIXES[i]; if (lark_parse_interactive(parser, prefix, strlen(prefix), NULL, &ip) != LARK_OK) die("interactive setup"); for (;;) { LarkTokenEvent ev; bool has_token; if (lark_interactive_feed_next(ip, &ev, &has_token) != LARK_OK) die("feed_next"); if (!has_token) break; } printf(" %-32s ->", *prefix ? prefix : "(empty)"); show_accepts(ip); lark_interactive_free(ip); }
lark_free(parser); return 0;}Error reporting
Section titled “Error reporting”Detect 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, match_examples returns the label for the example
that best matches the current error. The program classifies four malformed
JSON documents.
match_examples is Python only, and LALR only, because it compares the
parser state at the error. Rust errors carry that state too, so you can write
the comparison yourself
(custom error messages); TypeScript
and C errors do not.
"""Example-driven error reporting: group a few broken inputs under each messageyou want to show, and a parse failure gets the message of the example thatfails in the same parser state. Adapted from Lark's error-reporting recipe.
The call is UnexpectedInput.match_examples(): it re-parses each example andcompares its parser state with the real failure.
Run: pip install hyperlark python error_reporting.py"""
import hyperlarkfrom hyperlark import UnexpectedInput
JSON_GRAMMAR = r"""?start: value?value: object | array | ESCAPED_STRING -> string | SIGNED_NUMBER -> number | "true" -> true | "false" -> false | "null" -> nullarray : "[" [value ("," value)*] "]"object : "{" [pair ("," pair)*] "}"pair : ESCAPED_STRING ":" value
%import common.ESCAPED_STRING%import common.SIGNED_NUMBER%import common.WS%ignore WS"""
json_parser = hyperlark.Lark(JSON_GRAMMAR, parser="lalr")
# Each subclass is a diagnosis. match_examples() returns one of these classes# (or None); we raise it with the caret context filled in.class JsonSyntaxError(SyntaxError): def __str__(self): context, line, column = self.args return f"{self.label} at line {line}, column {column}.\n\n{context}"
class JsonMissingOpening(JsonSyntaxError): label = "Missing opening bracket"
class JsonMissingClosing(JsonSyntaxError): label = "Missing closing bracket"
class JsonMissingComma(JsonSyntaxError): label = "Missing comma"
class JsonTrailingComma(JsonSyntaxError): label = "Trailing comma"
def parse(json_text): try: return json_parser.parse(json_text) except UnexpectedInput as u: # The keys are the diagnosis classes; the values are broken inputs that # fail the same way. match_examples re-parses each and picks the label # whose parser state matches the real failure. exc_class = u.match_examples( json_parser.parse, { JsonMissingOpening: ['{"foo": ]}', '{"foo": }'], JsonMissingClosing: ["{", '{"a": 1', "[1", '{"foo": [}'], # Several variants per label, because the parser state depends # on more than the mistake: '[1 2]' and '["a" 1]' fail in # different states (after SIGNED_NUMBER vs after ESCAPED_STRING). JsonMissingComma: ['[1 2]', '["a" 1]', '{"a":1 "b":2}'], # '[1,]' and '[1,2,]' fail in different states, so both are # needed: the second is what classifies '[1, 2, ]'. JsonTrailingComma: ["[,]", "[1,]", "[1,2,]", '{"foo":1,}'], }, use_accepts=True, ) if not exc_class: raise # unrecognized shape: re-raise the raw UnexpectedInput raise exc_class(u.get_context(json_text), u.line, u.column) from None
CASES = [ '{"example1": "value"', # never closed '{"example2": ] ', # value expected, got a bracket '[1 2]', # comma missing between elements '[1, 2, ]', # trailing comma]
for case in CASES: try: parse(case) except JsonSyntaxError as e: print(f"input: {case!r}") print(e) print()