LEVIATHAN v962456e · 962456eee1

Metaprogramming

Splices — $for, $if, $ident and friends

Repeat, choose and name pieces of a template at expansion time with $for, $if, $ident(...), composite identifiers and $_params.

since 0.1.0-alpha.1linuxwindowswasm

Description

A template is more than a fixed piece of text with holes. A few $ forms let a single rule produce a different expansion for each declaration it matches. All of them are evaluated when the rule fires, before the program is checked, and none of them exists in the generated code.

$for name in <list> : <fragment> repeats a fragment once for each element of a compile-time list, binding each element to name. The list is an ordinary compile-time expression over the match's bindings, usually C.fields or C.methods filtered with where, and the elements are meta objects (see std.meta.Field and std.meta.Method). The fragment after the colon is one statement in a body, one element in an array literal, or one member in a member of template. This rule uses $for in all three positions:

$for in a member, an array element and a constructor

namespace Ser {
    attribute Row { }
    attribute Col { }
    rule buildRow {
        match @Row on class C
        inject `Array<string> names() => [ $for f in C.fields.where((x) => x.hasAttr("Col")) : $f.name ];
                Array<string> toRow() {
            Array<string> out = [];
            $for f in C.fields.where((x) => x.hasAttr("Col")) : out = out.add($f.name + "=" + this.$f.toString());
            return out;
        }` at member of C
    }
    rule traceCtor {
        match @Row on class C
        inject `$for f in C.fields.where((x) => x.hasAttr("Col")) : console.writeln("init " + $f.name);`
            at bottom of C.constructor
    }
}
uses Ser;

@Row
class Point {
    @Col int x;
    @Col int y;
    string label;
    new Point(int a, int b) { x = a; y = b; label = "p"; }
}

Point p = Point(3, 4);
console.writeln(p.names());
console.writeln(p.toRow().joinToString(", "));
init x
init y
[x, y]
x=3, y=4

$if (<test>) { <fragment> } $else { <fragment> } chooses one fragment per firing. The test is an ordinary compile-time boolean over the match's bindings, and only the chosen fragment appears in the output. The $else if chain is shorthand for nested $if. One rule fires here for three classes, and each class takes a different branch:

$if, $else if and $else

namespace Pick {
    attribute Kind { }
    rule pick {
        match @Kind on class C
        inject `string kind() {
            $if (C.name == "A") {
                return "first";
            } $else if (C.name == "B") {
                return "second";
            } $else {
                return "other";
            }
        }` at member of C
    }
}
uses Pick;

@Kind class A { }
@Kind class B { }
@Kind class Z { }

console.writeln(A().kind() + "," + B().kind() + "," + Z().kind());
first,second,other

Rules

  • $for binds one element per iteration. In member-selector position the bound meta::Method or meta::Field splices its name, as in this.$f. $f.name is the name as a string literal.
  • $if, $else if and $else select exactly one branch at expansion time, per firing. The test is evaluated against the firing's bindings, the same environment that where and comptime if use, and the branch that is not chosen is never emitted. Each { <fragment> } has the same fragment kind as its position: statements in a body, members in member of, declarations at namespace scope, or a single expression in an array element. $if composes with $for, so one rule can emit a different fragment for each field.
  • A $if whose test is not a bool is a compile error that names the rule. A $else with no preceding $if, or a branch whose contents do not fit its position, is a parse error.
  • A bound name as a type or an identifier. There are three ways to splice a bound value's string where a type or a name is expected:
    • $f.type and $f.name, for a $for-bound field, parameter or method, stand in for a type token: $f.type copy = this.$f; declares a local of the field's type. Only .type and .name can be used this way; any other field is a compile error.
    • A literal prefix glued to a hole, such as copy_$f or local_$idx, is one identifier whose name is the concatenation (copy_x).
    • $ident(a, b, …), in any declaration-name position, builds a name from its compile-time string arguments: class $ident(C.name, "Cols") declares UserCols for a class User. An argument that is not a string, or a name that is not a legal identifier, is a compile error. A synthesized name that collides with an existing declaration in the target namespace is a compile error that names both places; nothing is silently shadowed.
  • $_params and $_args inside a template stand for the matched method's parameter list and its argument list. ($_params) => this.$m($_args) is a lambda that forwards its arguments to the matched method, whatever its signature.
  • A member of template may inject several members; each one is subject to the same collision check.

Examples

$if in an expression position: a $for over fields where each element is chosen by $if. The @Hi fields come out with a ! prefix:

$if inside an array element

namespace Tag {
    attribute Row { }
    attribute Hi { }
    rule buildRow {
        match @Row on class C
        inject `Array<string> row() => [ $for f in C.fields :
                    $if (f.hasAttr("Hi")) { "!" + $f.name } $else { $f.name } ];`
            at member of C
    }
}
uses Tag;

@Row
class Point { @Hi int x; int y; @Hi int z; }

console.writeln(Point().row().joinToString(","));
!x,y,!z

A bound field as a type, and a prefix glued to a hole. The first $for declares one typed local per field, and the second one reads them back through the same composite names:

$f.type and copy_$f

namespace Ser {
    attribute Copyable { }
    rule mkCopy {
        match @Copyable on class C
        inject `Array<string> dump() {
            Array<string> out = [];
            $for f in C.fields : $f.type copy_$f = this.$f;
            $for f in C.fields : out = out.add($f.name + "=" + copy_$f.toString());
            return out;
        }` at member of C
    }
}
uses Ser;

@Copyable
class Point {
    int x = 3;
    int y = 7;
}

console.writeln(Point().dump().joinToString(", "));
x=3, y=7

$ident builds a class name. The injected class UserCols has one field per field of User, each holding its own name, and a helper function in the same namespace consumes it:

Name synthesis with $ident

namespace App {
    attribute Entity { }
    rule cols {
        match @Entity on class C
        inject `class $ident(C.name, "Cols") {
                    $for f in C.fields : string $f = $f.name;
                }
                string probe() {
                    UserCols c = UserCols();
                    return c.fullName + "," + c.age;
                }` at namespace Gen
    }
}
uses App;

@Entity
class User {
    string fullName;
    string age;
}

console.writeln(Gen::probe());
fullName,age

$_params and $_args forward a method's parameters. Each @Route method of the controller is registered with a lambda that calls it:

Forwarding parameters

namespace Web {
    interface IController { }
    attribute Route { string path; }
    rule register {
        match @Route(r) on method m in class C : IController
        inject `router.record($r.path, ($_params) => this.$m($_args))`
            at bottom of C.constructor
    }
}
uses Web;

class Router {
    string log = "";
    Router record(string path, (int) => int handler) {
        log = log + path + "=" + handler(10).toString() + ";";
        return this;
    }
}

class Calc : IController {
    Router router;
    new Calc(Router r) { router = r; }
    @Route("/double") int twice(int n) => n * 2;
    @Route("/square") int square(int n) => n * n;
}

Router r = Router();
Calc c = Calc(r);
console.writeln(r.log);
/double=20;/square=100;

Notes

  • A class injected at namespace N can be used from outside N by calling it (Gen::UserCols()), but not yet as a type (Gen::UserCols c). Consume a synthesized descriptor inside its own namespace.
  • Attributes on a $for-bound field or method are read with hasAttr and attr, not with a hole; see std.meta.Field and the example on lang.attributes.

See also

  • Rules — match a shape, inject code — A rule matches declarations by shape and injects quasiquoted code at a named anchor, with match, where, and inject … at.
  • Quasiquote templates and holes — Backtick-delimited templates hold the code a rule injects, and $ holes fill them from the match.
  • comptime — run the language at compile time — comptime variables, expressions and if statements evaluate ordinary code during compilation and fold the result into the program.
  • Class — A class that a rule has matched, as a rule sees it.
  • Field — A field of a class, as a rule sees it.
  • Method — A method of a class, as a rule sees it.
  • Param — One parameter of a method, as a rule sees it.