LEVIATHAN v962456e · 962456eee1

Metaprogramming

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.

since 0.1.0-alpha.1linuxwindowswasm

Description

A lambda often uses values from outside: a local variable, this.limit, a field of another object. The tree cannot hold a live variable, so each such value goes into the binds array of the Expr, and the tree refers to it with expr::Bind(slot), where slot is an index into binds.

The contract is:

  • binds[k] is the value of the k-th distinct captured expression. Slots are numbered in the order the captured expressions first appear in the lambda body.
  • Each captured value is evaluated once, when the Expr is built. The tree and binds never read a variable again.
  • Two uses of the same captured expression share one slot. Distinctness is by the chain as written: the root name plus the member path. lo used twice is one slot, and so is this.x used twice.
  • A captured chain that goes through several members, such as this.owner.limit, is one slot holding the value of the whole chain.

The result is that a consumer can translate the tree and take the parameter values from binds, as a database layer does when it turns the tree into a prepared statement with parameters.

Slots are shared and values are fixed at construction

class User {
    int age;
    string name;
    new User(int a, string n) { age = a; name = n; }
}

string lit(string | int | float | bool | None v) {
    match (v) {
        string => { return "\"" + v + "\""; }
        int => { return v.toString(); }
        else => { return "None"; }
    }
}

void demo() {
    int floor = 18;
    string tag = "x";
    expr::Expr<(User) => bool> e = (u) => u.age >= floor && u.age < floor + 50 && u.name != tag;
    floor = 99;
    console.writeln(e.binds.length());
    for (string | int | float | bool | None b in e.binds) {
        console.writeln(lit(b));
    }
}

demo();
2
18
"x"

Rules

  • A value is snapshotted when the Expr is constructed. Changing the variable afterwards does not change binds, and the tree still refers to the same slot.
  • Slot order is the order of first reference in the lambda, left to right.
  • Only values of type string, int, float or bool can be captured into binds. An Array<T> that is the receiver of contains gets a slot whose entry is None; the consumer keeps its own reference to the array. Any other captured type is a compile error.
  • Capturing does not run user code in the tree: a Bind holds only its slot number. The captured expression is evaluated exactly once, as part of building the binds array.
  • The closure fn is the ordinary lambda. The guarantees above are about tree and binds.

Examples

A member chain and a shared slot

class Owner {
    int limit = 40;
    string name = "Ada";
}

class Account {
    int age;
    string name;
    new Account(int a, string n) { age = a; name = n; }
}

class Checker {
    Owner owner = Owner();
    int minimum = 10;

    void run() {
        expr::Expr<(Account) => bool> e =
            (a) => a.age >= this.minimum && a.age < this.owner.limit && a.age != this.minimum;
        console.writeln(e.binds.length());
        string | int | float | bool | None first = e.binds[0];
        string | int | float | bool | None second = e.binds[1];
        match (first) {
            int => { console.writeln("slot 0 = ${first}"); }
            else => { console.writeln("slot 0 is not an int"); }
        }
        match (second) {
            int => { console.writeln("slot 1 = ${second}"); }
            else => { console.writeln("slot 1 is not an int"); }
        }
    }
}

Checker().run();
2
slot 0 = 10
slot 1 = 40

An array capture

class User {
    int age;
    new User(int a) { age = a; }
}

Array<int> allowed = [1, 2, 3];
expr::Expr<(User) => bool> e = (u) => allowed.contains(u.age);
console.writeln(e.binds.length());
string | int | float | bool | None slot = e.binds[0];
match (slot) {
    int => { console.writeln("an int"); }
    else => { console.writeln("None: the array is not stored"); }
}
console.writeln(e.fn(User(2)));
console.writeln(e.fn(User(9)));
1
None: the array is not stored
true
false

See also

  • 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.
  • The reifiable subset — The expressions that may appear in a reified lambda and the tree node each one becomes.
  • expr — The expression-reification tree: runtime, walkable descriptions of lambda bodies.