Transformers
Transformers and visitors process the parse tree: you write code for each rule you wish to process, keyed by its name. There are four ways to run it:
| Pattern | Python / TypeScript | Rust | C |
|---|---|---|---|
| Bottom-up, a value per node | Transformer | fold / fold_fn | lark_fold_* |
| During the parse, straight to a value | transformer= / { transformer } | parse_fold_fn | lark_reduce_fold_*, lark_parse_fold |
| Top-down, evaluate on demand | Interpreter | eval | none, recurse yourself |
| Side effects only | Visitor | Cursor walk | cursor walk |
Run your code during the parse when all you need is the value. This needs LALR and has other limits. Run it on the tree when you also need the tree.
The grammar
Section titled “The grammar”Every example uses this calculator grammar as GRAMMAR:
?start: sum?sum: product | sum "+" product -> add | sum "-" product -> sub?product: atom | product "*" atom -> mul | product "/" atom -> div?atom: NUMBER -> number | "-" atom -> neg | "(" sum ")"NUMBER: /[0-9]+/%ignore " "-> add names the node, and so the code that handles it. ? inlines a rule
with one child, so an add node holds its operands
(shaping the tree).
Bottom-up
Section titled “Bottom-up”A transformer works bottom-up: each node’s callback receives its children, already transformed, and its return value replaces the node. The root’s value is the result.
from hyperlark import Lark, Transformer, v_args
@v_args(inline=True) # children arrive as positional argsclass Calc(Transformer): def number(self, tok): return float(tok) def add(self, a, b): return a + b def sub(self, a, b): return a - b def mul(self, a, b): return a * b def div(self, a, b): return a / b def neg(self, a): return -a
parser = Lark(GRAMMAR, parser="lalr")tree = parser.parse("2 + 3 * 4")print(Calc().transform(tree)) # 14.0import { Lark, Transformer, v_args, type Token, type Tree } from "@hyperlark/hyperlark";
class Calc extends v_args({ inline: true })(Transformer) { number(t: Token) { return Number(t.value); } // the child is the NUMBER token add(a: number, b: number) { return a + b; } sub(a: number, b: number) { return a - b; } mul(a: number, b: number) { return a * b; } div(a: number, b: number) { return a / b; } neg(a: number) { return -a; }}
const parser = new Lark(GRAMMAR, { parser: "lalr" });const tree = parser.parse("2 + 3 * 4") as Tree;console.log(new Calc().transform(tree)); // 14fold_fn takes a closure for tokens (type name and FoldLeaf) and one for
rules (rule name and child values). An absent [...] optional reaches the
token closure with the name "". The fold is iterative, so a deep tree
cannot overflow the stack.
use hyperlark::{fold_fn, Lark};
let parser = Lark::lalr(GRAMMAR)?;let mut calc = fold_fn( |name, leaf| match name { "NUMBER" => leaf.text().parse().unwrap(), _ => 0.0, // every other token type }, |name, kids: Vec<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], _ => kids[0], // number: single child, passed through },);let value: f64 = parser.parse("2 + 3 * 4")?.fold(&mut calc); // 14.0A callback returns a LarkValue, an 8-byte union of .d, .i, .u and
.p; this example uses .d. The walk is iterative, so a deep tree cannot
overflow the stack.
/* also needs <stdio.h> and <stdlib.h> */static LarkValue num(void *ud, LarkNode leaf) { char buf[64]; if (lark_node_token_value_copy(leaf, buf, sizeof buf, NULL) != LARK_OK) { if (ud) *(bool *)ud = false; return (LarkValue){ .d = 0 }; } return (LarkValue){ .d = strtod(buf, NULL) };}static LarkValue add(void *u, LarkNode n, const LarkValue *c, size_t k) { (void)u; (void)n; (void)k; return (LarkValue){ .d = c[0].d + c[1].d }; }static LarkValue mul(void *u, LarkNode n, const LarkValue *c, size_t k) { (void)u; (void)n; (void)k; return (LarkValue){ .d = c[0].d * c[1].d }; }/* sub, div, neg follow the same shape */
bool ok = true; /* `num` clears this on failure */LarkFold *fold = NULL;bool reg = lark_fold_new(lark, &fold) == LARK_OK;reg &= lark_fold_token(fold, "NUMBER", num, &ok) == LARK_OK;reg &= lark_fold_rule(fold, "add", add, NULL) == LARK_OK;reg &= lark_fold_rule(fold, "mul", mul, NULL) == LARK_OK;/* ... register sub / div / neg ... */if (!reg) { fprintf(stderr, "fold setup: %s\n", lark_last_error()); return 1; }
LarkValue out = { .d = 0 };if (lark_fold_apply(fold, res, &out) != LARK_OK) /* res from lark_parse */ fprintf(stderr, "fold failed: %s\n", lark_last_error());else if (!ok) fprintf(stderr, "a literal did not fit\n");else printf("%g\n", out.d); /* 14 */lark_fold_free(fold);- Token text is not NUL-terminated.
lark_node_token_value_copyadds a terminator, and returnsLARK_BUFFER_TOO_SMALLinstead of truncating. - A callback has no error return. Record a failure in your userdata, as
numdoes withok. - A rule with no callback takes its first child’s value, so
numberneeds none. Check every registration: an unknown name returnsLARK_INVALID_ARGUMENT, and if unchecked a misspelled rule takes its first child’s value and a misspelled token becomes zero. - The library never frees a
.ppointer. Free it yourself, also when the fold fails.
In Python and TypeScript:
- A rule with no method calls
__default__, which by default copies the node. A token with no method is returned as is. - A method named after a terminal (
NUMBER) receives the token. - An exception in a method is wrapped in
VisitError:objis the node,orig_excthe exception. - Return
Discardand the node won’t appear in the parent. Transformeris recursive. For huge trees useTransformer_NonRecursive.- Python also has
merge_transformers, for imported grammars (feature matrix).
v_args (Python and TypeScript)
Section titled “v_args (Python and TypeScript)”By default, a rule method accepts one argument: a list of the node’s
children. v_args modifies that, for one method or a whole class:
inline: positional arguments,add(a, b), as above.meta: two arguments,metaandchildren.metaholds the node’s positions only withpropagate_positions; without it, TypeScript passesundefined.tree: the whole tree,add(tree).
In TypeScript a single method is wrapped as
add = v_args({ inline: true })(function (a: number, b: number) { return a + b; }).
During the parse
Section titled “During the parse”Give the parser your callbacks up front, and it returns the value instead of a tree. It is faster than transforming a tree; Python, Rust and C build no tree at all.
parser = Lark(GRAMMAR, parser="lalr", transformer=Calc())print(parser.parse("2 + 3 * 4")) # 14.0, no Tree builtThe transformer is a plain object, as below. Pass it to parse() or to the
constructor as transformer: calc. v_args has no effect here, so passing
the Calc instance from above gives a wrong value with no error.
import { Lark, type Token } from "@hyperlark/hyperlark";
const calc = { number: (c: Token[]) => Number(c[0].value), 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], neg: (c: number[]) => -c[0],};const parser = new Lark(GRAMMAR, { parser: "lalr" });console.log(parser.parse("2 + 3 * 4", { transformer: calc })); // 14parse_fold_fn takes the same two closures as fold_fn:
let value: f64 = parser.parse_fold_fn( "2 + 3 * 4", None, // default start rule |name, leaf| match name { "NUMBER" => leaf.text().parse().unwrap(), _ => 0.0 }, |name, kids: Vec<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], _ => kids[0], },)?; // 14.0lark_parse_fold takes a LarkReduceFold, registered like a LarkFold.
Its callbacks get ids instead of a node: the rule’s numeric id, or the
token’s type id with its text as (text, len). A catch-all
(lark_reduce_fold_default, lark_reduce_fold_leaf) serves several names:
compare its id with ids looked up with lark_rule_id or lark_token_id,
or map the id back with lark_rule_name or lark_token_name. The notes under Bottom-up apply too. The
Examples page has the complete program.
/* also needs <string.h> for memcpy */static LarkValue r_num(void *ud, uint32_t id, const char *text, size_t len) { (void)id; char b[64]; if (len >= sizeof b) { /* too long: reject it and flag `ud` */ if (ud) *(bool *)ud = false; return (LarkValue){ .d = 0 }; } memcpy(b, text, len); b[len] = '\0'; /* text is not NUL-terminated */ return (LarkValue){ .d = strtod(b, NULL) };}static LarkValue r_add(void *ud, uint32_t id, const LarkValue *c, size_t k) { (void)ud; (void)id; (void)k; return (LarkValue){ .d = c[0].d + c[1].d }; }
bool ok = true;LarkReduceFold *fold = NULL;bool reg = lark_reduce_fold_new(lark, &fold) == LARK_OK;reg &= lark_reduce_fold_token(fold, "NUMBER", r_num, &ok) == LARK_OK;reg &= lark_reduce_fold_rule(fold, "add", r_add, NULL) == LARK_OK;/* ... */if (!reg) { fprintf(stderr, "fold setup: %s\n", lark_last_error()); return 1; }
LarkValue out = { .d = 0 }; /* zero unless lark_parse_fold returns LARK_OK */if (lark_parse_fold(lark, text, len, "start", fold, &out) != LARK_OK) fprintf(stderr, "parse failed: %s\n", lark_last_error());else if (!ok) fprintf(stderr, "a literal did not fit\n");else printf("%g\n", out.d);lark_reduce_fold_free(fold);Limits of this form:
- With Earley, Python and TypeScript raise
ConfigurationError, and Rust’sparse_fold_fnreturnsParseError::Unsupported, as it also does with a custom lexer. - Python and TypeScript:
Discardis disabled; it reaches the parent as a value.- An exception in a callback propagates unchanged, with no
VisitErrorwrapper.
- Python:
- A rule method with
v_args(meta=True)raisesNotImplementedErrorat construction. __default__receivesmeta=None, even withpropagate_positions.__default_token__is not called.- Terminal methods run for every terminal the parser reads, including ones
the tree drops (such as a
","namedCOMMA), even withvisit_tokens=False. A%declared terminal, such as one a postlex inserts, gets no method call. __default__also runs for_-prefixed and internal rules (such as__start_plus_0foritem+). A method named after a_-prefixed rule raisesConfigurationErrorat construction, unless the transformer also overrides__default__. Except at the root, both must return a tree for these rules, or the parse raisesAttributeError.- A chain of transformers (
T1() * T2()) is ignored:parse()returns the tree.
- A rule method with
- TypeScript:
- A callback named after a terminal (
NUMBER) is never called. - By default the callbacks run after the whole input has parsed, so a parse
error means none of them ran.
batch_sizeonparse()(not the constructor) runs them during the parse, in groups of that many completed rules;1runs each as its rule completes. A custom lexer ignoresbatch_sizeand runs them over the finished tree.
- A callback named after a terminal (
Top-down
Section titled “Top-down”An Interpreter (Python, TypeScript) 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. A node with no method visits its children.
In Rust, one eval closure handles rules and tokens, and runs only for the
root and the nodes you reach with n.eval(i). Both Interpreter and eval
recurse, so a deep tree can overflow the stack. C has no interpreter: recurse
with lark_node_child (reading a tree). The
Examples page has Python and Rust interpreters.
from hyperlark.visitors import Interpreter
class Eval(Interpreter): def number(self, t): return float(t.children[0]) def add(self, t): a, b = self.visit_children(t); return a + b def mul(self, t): a, b = self.visit_children(t); return a * b # sub, div, neg likewise
print(Eval().visit(tree)) # 14.0import { Interpreter, type Token, type Tree } from "@hyperlark/hyperlark";
class Eval extends Interpreter { number(t: Tree) { return Number((t.children[0] as Token).value); } add(t: Tree) { const [a, b] = this.visit_children(t) as number[]; return a + b; } mul(t: Tree) { const [a, b] = this.visit_children(t) as number[]; return a * b; } // sub, div, neg likewise}console.log(new Eval().visit(tree)); // 14use hyperlark::EvalNode;
let value: f64 = parser.parse("2 + 3 * 4")?.eval(|n: &EvalNode<f64>| match n.name() { "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), "neg" => -n.eval(0), "number" => n.eval(0), "NUMBER" => n.text().parse().unwrap(), // every node name needs its own arm other => unreachable!("unhandled node: {other}"),});Side effects
Section titled “Side effects”A Visitor (Python, TypeScript) visits each node and runs the method for its
data, then returns the tree. visit goes bottom-up, visit_topdown
top-down. In Rust and C, walk the tree with a
cursor.
from hyperlark import Visitor
class CountOps(Visitor): def __init__(self): self.ops = 0 def add(self, tree): self.ops += 1 def mul(self, tree): self.ops += 1
v = CountOps(); v.visit(tree); print(v.ops) # 2import { Visitor } from "@hyperlark/hyperlark";
class CountOps extends Visitor { ops = 0; add() { this.ops++; } mul() { this.ops++; }}const v = new CountOps(); v.visit(tree); console.log(v.ops); // 2Recurse over Cursor, or use an iterative cursor from
walking in a chosen order when
the tree may be deep.
use hyperlark::{Cursor, NodeKind};
fn count_ops(c: &Cursor, ops: &mut u32) { if let NodeKind::Tree { data } = c.kind() { if matches!(data, "add" | "mul") { *ops += 1; } for child in c.children() { count_ops(&child, ops); } }}
let result = parser.parse("2 + 3 * 4")?;let mut ops = 0;count_ops(&result.root(), &mut ops);println!("{ops}"); // 2Other walk orders: walking in a chosen order.
/* also needs <string.h> for memcmp */LarkTreeCursor *cur = NULL;if (lark_cursor_new_subtrees(res, lark_root(res), &cur) != LARK_OK) /* res from lark_parse */ { fprintf(stderr, "%s\n", lark_last_error()); return 1; }LarkNode n;int ops = 0;while (lark_cursor_next(cur, &n)) { /* every rule node, bottom-up */ const char *name; size_t len; if (lark_node_data(n, &name, &len) == LARK_OK && len == 3 && (memcmp(name, "add", 3) == 0 || memcmp(name, "mul", 3) == 0)) ops++;}lark_cursor_free(cur);printf("%d\n", ops); /* 2 */