Skip to content

Transformers

A parse tree is a means, not an end — you almost always want a value: a number, an AST node, a config object. Every tree-to-value job answers two questions, and hyperlark gives the same answers in every language:

  1. When? Walk the tree after the parse, or fold during the parse — no tree is ever built (the fast path).
  2. How? Bottom-up (a value per node), top-down (you drive the descent), or just for side effects.
PatternPython / TypeScriptRustC
Bottom-up value per nodeTransformerfold / fold_fnlark_fold_*
Top-down, evaluate on demandInterpretereval
Side effects onlyVisitorCursor walkcursor walk
Fold during the parsetransformer=parse_fold_fnlark_parse_fold

The class-based toolkit (Python, TypeScript) and the closure/primitive forms (Rust, C) express the same patterns: a Rust closure keyed on the node name is the idiomatic spelling of a Transformer method, and Rust’s lazy eval is the idiomatic spelling of an Interpreter. Pick your language; the shapes below line up row for row.

A four-function calculator whose tree evaluates to a number, self-contained (no imports) so it parses identically on every target — call it GRAMMAR below:

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

The -> aliases name each node (add, mul, number, …); the ? prefix inlines a rule with a single child, so operands flow straight into the operator nodes. You supply one function per node name.

Leaves first, then each rule after its children — so a rule’s function receives its children already turned into values. Return what the node is worth; the root’s value is the result.

from hyperlark import Lark, Transformer, v_args
@v_args(inline=True) # children arrive as positional args
class 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.0

v_args — adapting the call shape (Python & TypeScript)

Section titled “v_args — adapting the call shape (Python & TypeScript)”

By default a rule method receives its children as one list. v_args changes that, per method or per class: inline spreads the children as positional args (used above), meta prepends the node’s Meta (positions), tree hands over the whole Tree. Discard (return it to drop a node from its parent) and merge_transformers (Python) round out the toolkit. Rust and C key on the node name directly, so they need no call adapter.

Sometimes you want to visit the root first and decide what to descend into — to skip a dead branch or an unevaluated operand instead of folding the whole tree.

An Interpreter visits top-down; call visit_children to descend.

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

A Visitor walks the tree and returns it unchanged — reach for it to collect or check, not to rewrite. In Python and TypeScript it is a Visitor class; in Rust and C, walk a Cursor (see working with trees).

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) # 2

Hand the transform to the parser and it folds as the parser reduces — no parse tree is materialized. This is the fast path for straight tree-to-value work, and it is LALR-only in every target.

Pass the Calc transformer from the top of this page as transformer= — one definition, two ways to run it. parse then returns the value directly:

parser = Lark(GRAMMAR, parser="lalr", transformer=Calc())
print(parser.parse("2 + 3 * 4")) # 14.0 — no Tree built

See the feature matrix for the full per-target picture.