Metaprogramming
The reifiable subset
The expressions that may appear in a reified lambda and the tree node each one becomes.
since 0.1.0-alpha.1linuxwindowswasm
Description
A reified lambda body is limited to the constructs in this table. Each becomes one node kind of the
expr tree. Anything else is a compile error that names the construct.
| in the lambda body | tree node |
|---|---|
u.f, u.f.g: a member chain rooted at a lambda parameter |
Field(["f"]), Field(["f", "g"]) |
a chain that does not start at a lambda parameter: a captured local, this.x, obj.field, this.x.y |
one Bind(slot) for the value of the whole chain, read once |
an int, float, bool or string literal, None, or an enum member E::M |
Lit(value); an enum member is Lit of its integer value |
l op r with op one of ==, !=, <, <=, >, >=, &&, ||, +, -, *, /, % |
Bin("op", l, r); operands are never reordered |
!e, -e |
Un("!", e), Un("-", e) |
| a call to one of the allowed methods | Call("name", receiver, [arguments]) |
u.field = value as the entire body, when the target type is expr::Expr<(E) => int> |
Assign(Field(["field"]), value), the "set" shape |
| anything else | a compile error |
A captured value must have type string, int, float or bool. The only exception is an Array<T> used as the receiver of contains: it gets a Bind slot, but the
binds array holds None there because it cannot hold an array. The closure still sees the array, and a
consumer that needs its elements keeps its own reference. A capture of any other type is a compile error
(capture of a non-value type).
Fields, literals, operators and calls
class Address {
string city = "Oslo";
}
class User {
string name;
int age;
bool active;
Address address = Address();
new User(string n, int a, bool act) { name = n; age = a; active = act; }
}
string lit(string | int | float | bool | None v) {
match (v) {
string => { return "\"" + v + "\""; }
int => { return v.toString(); }
float => { return v.toString(); }
bool => { return v.toString(); }
else => { return "None"; }
}
}
string dump(expr::Node n) {
match (n) {
expr::Bin => {
expr::Bin b = n;
return "Bin(${b.op}, ${dump(b.l)}, ${dump(b.r)})";
}
expr::Un => {
expr::Un x = n;
return "Un(${x.op}, ${dump(x.e)})";
}
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 => { return "Lit(" + lit(n.v) + ")"; }
expr::Bind => { return "Bind(${n.slot})"; }
else => { return "?"; }
}
}
void show(expr::Expr<(User) => bool> e) {
console.writeln(dump(e.tree));
}
show((u) => u.address.city == "Oslo");
show((u) => !u.active || -u.age < 0);
show((u) => None != u.name);
show((u) => u.age % 2 == 0 && u.age * 2 - 1 > 0);
show((u) => u.name.startsWith("A") || u.name.contains("da"));
show((u) => u.age == 1 || u.age == 1.5);
Bin(==, Field(address.city), Lit("Oslo"))
Bin(||, Un(!, Field(active)), Bin(<, Un(-, Field(age)), Lit(0)))
Bin(!=, Lit(None), Field(name))
Bin(&&, Bin(==, Bin(%, Field(age), Lit(2)), Lit(0)), Bin(>, Bin(-, Bin(*, Field(age), Lit(2)), Lit(1)), Lit(0)))
Bin(||, Call(startsWith, Field(name), Lit("A")), Call(contains, Field(name), Lit("da")))
Bin(||, Bin(==, Field(age), Lit(1)), Bin(==, Field(age), Lit(1.500000)))
Rules
- Operands of a
Binkeep the order in which they are written:None != u.nameisBin("!=", Lit(None), Field(["name"])), not a swapped comparison. - A captured expression becomes a single
Bindfor its entire value. Arithmetic on captured values stays in the tree: with a capturedlo,u.age < lo + 50isBin("<", Field(["age"]), Bin("+", Bind(0), Lit(50))). - A chain such as
this.owner.limitis oneBind, not aField, because it does not start at a lambda parameter. - A string interpolation inside a reified lambda is rejected, because it expands to calls that are not in the allowed list.
- The set shape is only recognized when the assignment is the whole body and the function type returns
int. The same assignment in aboollambda is a compile error.
Examples
Captures, enums and the set shape
enum Role { Admin, Guest }
class Owner {
string name = "Ada";
int limit = 40;
}
class Account {
int age;
Role role;
new Account(int a, Role r) { age = a; role = r; }
}
class Filters {
Owner owner = Owner();
string text(expr::Node n) {
match (n) {
expr::Bin => {
expr::Bin b = n;
return "(${this.text(b.l)} ${b.op} ${this.text(b.r)})";
}
expr::Field => {
string p = n.path.joinToString(".");
return p;
}
expr::Bind => { return "$${n.slot}"; }
expr::Lit => {
string | int | float | bool | None v = n.v;
match (v) {
int => { return "int ${v}"; }
else => { return "lit"; }
}
}
expr::Assign => {
expr::Assign a = n;
return "set ${this.text(a.target)} = ${this.text(a.value)}";
}
else => { return "?"; }
}
}
void run() {
expr::Expr<(Account) => bool> mine = (a) => a.age < this.owner.limit && a.age > 0;
console.writeln(this.text(mine.tree));
console.writeln(mine.binds.length());
expr::Expr<(Account) => bool> admin = (a) => a.role == Role::Admin;
console.writeln(this.text(admin.tree));
console.writeln(admin.fn(Account(30, Role::Admin)));
expr::Expr<(Account) => int> setter = (a) => a.age = 41;
console.writeln(this.text(setter.tree));
}
}
Filters().run();
((age < $0) && (age > int 0))
1
(role == int 0)
true
set age = int 41
class User { string name; int age; }
// error: cannot reify block body
expr::Expr<(User) => bool> a = (u) => { return u.age > 1; };
// error: cannot reify conditional expression
expr::Expr<(User) => bool> b = (u) => u.age > 1 ? true : false;
// error: cannot reify capture of a non-value type
User other = User();
expr::Expr<(User) => bool> c = (u) => other == u;
Notes
The allowed method calls are listed on the whitelist page. Constructs that do not appear in the table,
such as indexing, match, is, nested lambdas, await and conditional expressions, are errors; the
diagnostics page lists the messages.
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.
- 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.
- expr — The expression-reification tree: runtime, walkable descriptions of lambda bodies.