LEVIATHAN v962456e · 962456eee1

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 fn behaves exactly like the same lambda written without expr::Expr.
  • expr::Expr is a library-level capability like regex:: and json::. 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.