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
$forbinds one element per iteration. In member-selector position the boundmeta::Methodormeta::Fieldsplices its name, as inthis.$f.$f.nameis the name as a string literal.$if,$else ifand$elseselect exactly one branch at expansion time, per firing. The test is evaluated against the firing's bindings, the same environment thatwhereandcomptime ifuse, 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 inmember of, declarations at namespace scope, or a single expression in an array element.$ifcomposes with$for, so one rule can emit a different fragment for each field.- A
$ifwhose test is not aboolis a compile error that names the rule. A$elsewith 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.typeand$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.typeand.namecan be used this way; any other field is a compile error.- A literal prefix glued to a hole, such as
copy_$forlocal_$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")declaresUserColsfor a classUser. 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.
$_paramsand$_argsinside 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 oftemplate 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 Ncan be used from outsideNby 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 withhasAttrandattr, not with a hole; seestd.meta.Fieldand the example onlang.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, andinject … 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 —
comptimevariables, expressions andifstatements 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.