Metaprogramming
Expression reification — lambdas as data
A lambda literal in a position typed expr::Expr<F> compiles to an ordinary closure plus a walkable tree of its checked body, which is what query builders translate to other languages.
since 0.1.0-alpha.1linuxwindowswasm
Description
Most of the time a lambda is only code: you can call it, and that is all. Expression reification
makes a lambda inspectable as well. When a lambda literal is written where the expected type is
expr::Expr<F> (F being the function type, such as (User) => bool), the compiler produces one
expr::Expr<F> value that holds four things:
| member | what it is |
|---|---|
fn |
the lambda as an ordinary closure, callable as usual |
tree |
an expr::Node tree that mirrors the lambda's body after type checking |
binds |
the values the lambda captured from its surroundings, evaluated once when the Expr is built |
siteId |
a number identifying the place in the program where the lambda was written |
The tree is plain data. A library walks it with match and turns it into something else: a SQL
WHERE clause, a search filter, a validation rule, a printable description. This is the reason the
feature exists. A query builder can accept (u) => u.age >= 18 && u.name.like("A%"), translate it to
text for a database, and still be written as ordinary, type-checked code.
The language guarantees one thing about the tree: it mirrors the checked body, with member accesses resolved, constants folded and enum members replaced by their integer values. How a tree is turned into SQL or anything else is the consumer's business, and the consumer owns the tests for it.
Only a small, fixed part of the language can appear in such a lambda; anything else is a compile error that
names the construct. The sections below cover where a lambda is converted, what may appear in it, how
captured values are stored, which method calls are allowed, the diagnostics, and the compiler's
--expand view of the result.
A lambda and its tree
class User {
bool active;
string name;
new User(bool a, string n) { active = a; name = n; }
}
string dump(expr::Node n) {
match (n) {
expr::Bin => {
expr::Bin b = n;
return "Bin(${b.op}, ${dump(b.l)}, ${dump(b.r)})";
}
expr::Call => {
expr::Call c = n;
return "Call(${c.name}, ${dump(c.recv)}, [${dump(c.args[0])}])";
}
expr::Field => {
string p = n.path.joinToString(".");
return "Field(${p})";
}
expr::Lit => {
string | int | float | bool | None v = n.v;
match (v) {
string => { return "Lit(${v})"; }
else => { return "Lit(?)"; }
}
}
else => { return "?"; }
}
}
expr::Expr<(User) => bool> q = (u) => u.active && u.name.like("A%");
console.writeln(dump(q.tree));
console.writeln(q.fn(User(true, "Ada")));
console.writeln(q.fn(User(true, "Bob")));
Bin(&&, Field(active), Call(like, Field(name), [Lit(A%)]))
true
false
Rules
- Reification applies only to a lambda literal whose target type is
expr::Expr<F>. A variable that holds a lambda is not reified. - The lambda body must be a single expression drawn from the reifiable subset. The compiler rejects any other construct at compile time, naming it.
- The tree reflects the checked program, not the source text: operands are never reordered and constants are folded.
- The closure in
fnbehaves exactly like the same lambda written withoutexpr::Expr. expr::Expris a library-level capability likeregex::andjson::. It is not tied to any one library; a database layer is only its first consumer.
Examples
A tree is an ordinary object graph, so a function can walk it. This one counts the nodes, then the same
lambda is called as a closure. The worked interpreter on the expr page evaluates a tree against real
objects.
Counting the nodes of a tree
class Account {
int balance;
bool frozen;
new Account(int b, bool f) { balance = b; frozen = f; }
}
int count(expr::Node n) {
match (n) {
expr::Bin => {
expr::Bin b = n;
return 1 + count(b.l) + count(b.r);
}
expr::Un => {
expr::Un u = n;
return 1 + count(u.e);
}
else => { return 1; }
}
}
expr::Expr<(Account) => bool> usable = (a) => a.balance > 100 && !a.frozen;
console.writeln(count(usable.tree));
console.writeln(usable.fn(Account(500, false)));
console.writeln(usable.fn(Account(500, true)));
6
true
false
Notes
Reification is available on the interpreters and the native (LLVM) backend. The exact tree shapes, the
rules for captured values and the list of allowed method calls are on the pages linked from this one.
The ordinary methods string.like and string.ilike are usable in any program, and have the same meaning
inside a reified lambda.
See also
- expr — The expression-reification tree: runtime, walkable descriptions of lambda bodies.
- Where a lambda literal is reified — A lambda literal becomes an expr::Expr<F> in any position whose expected type is expr::Expr<F>: a call argument, a declared variable, a return value or a field initializer.
- The reifiable subset — The expressions that may appear in a reified lambda and the tree node each one becomes.
- Captured values and the binds array — How a reified lambda records the outside values it uses, once and in order, in the binds array, and what that guarantees.
- Method calls in a reified lambda, and like / ilike — The six method calls that may appear in a reified lambda, and the exact matching rules of string.like and string.ilike.
- Reification errors — The compile errors a reified lambda can produce, what each means, and how to fix it.
- siteId — identifying a lambda in the source — Every reified lambda literal gets a number that is fixed at compile time, distinct for each lambda in the source, and the same on every run.
- Reification in --expand output — How the compiler's --expand view shows a reified lambda as an explicit expr::Expr construction, and what it guarantees about the checked tree.
- Expr — A lambda together with a walkable description of its body.