Metaprogramming
Quasiquote templates and holes
Backtick-delimited templates hold the code a rule injects, and $ holes fill them from the match.
since 0.1.0-alpha.1linuxwindowswasm
Description
The code that a rule injects is written as a quasiquote: source text between backticks. The text is
parsed as a fragment of the kind its anchor needs, which is statements for the constructor, body, marker and
splice anchors and for replace, members for member of, and declarations for namespace N. A
fixed-template macro (below) holds a single expression.
Holes mark the places in a template that are filled in per match. A hole is $ followed by the name of a
binding from the rule's match clause or from a $for (see lang.splices). This rule adds a describe
method and a runChecks method to every @Entity class. The first one reads attribute values with $e.table
and $e.version; the second one calls each @Check method of the class by name:
Holes in a template
namespace Meta {
attribute Entity { string table; int version = 1; }
attribute Check { }
rule describe {
match @Entity(e) on class C
inject `string describe() => $e.table + " v" + $e.version.toString() + " (" + $C.name + ")";`
at member of C
}
rule runChecks {
match @Entity(e) on class C
inject `Array<string> runChecks() {
Array<string> out = [];
$for m in C.methods.where((x) => x.hasAttr("Check")) : out = out.add($m.name + "=" + this.$m().toString());
return out;
}` at member of C
}
}
uses Meta;
@Entity("people", 2)
class Person {
@Check int idPositive() => 1;
@Check int namePresent() => 0;
int other() => 5;
}
Person p = Person();
console.writeln(p.describe());
console.writeln(p.runChecks().joinToString(", "));
people v2 (Person)
idPositive=1, namePresent=0
The holes used there are the ones described below.
Rules
$bind.fieldreads a field of a bound attribute and puts its value in as a literal:$e.tablebecame the string"people"and$e.versionthe integer2.$mor$C, where the binding is a declaration, splices the declaration's name as an identifier. In member-selector position this selects the member, as inthis.$m(). A$for-bound method or field ($for m in C.methods … this.$m()) does the same, so a rule can discover members and also call them.$C.nameand$m.namegive the declaration's simple name as a string literal.$Cin type position carries the matched class through to the checker, so$C copy = $C();declares a local of the matched class and constructs one.$_paramsand$_argsforward the matched method's own parameter list and argument list; seelang.splices.- A local variable that a template declares is renamed to a fresh symbol. Injected code therefore neither captures nor is captured by names at the use site (see the hygiene example below).
- The template ends at the next backtick, so a backtick cannot appear inside it.
- A
///doc comment inside a template must start on the line after the opening backtick and must not contain a backtick. The injected declaration carries the doc comment. - A fixed-template expression macro is declared with
macro name(e) => `<expr>`;and called asname!(arg). Each argument is captured as a syntax node and spliced where$eappears. A macro may splice each parameter only once, so that the argument is evaluated once; splicing it twice is a compile error, and the way around it is to bind the argument to a local in the template. For a macro that computes its expansion, seelang.procedural-macros.
Examples
$C in type position:
A matched class as a type
namespace Clone {
attribute Fresh { }
rule fresh {
match @Fresh on class C
inject `$C fresh() {
$C copy = $C();
return copy;
}` at member of C
}
}
uses Clone;
@Fresh
class Counter {
int n = 7;
}
Counter c = Counter();
c.n = 99;
console.writeln(c.fresh().n);
7
Hygiene: the rule declares a local named tmp, and the method it lands in already has one. The two do not
interfere:
An injected local does not capture a name
namespace Audit {
attribute Audited { }
rule audit {
match @Audited on method m
inject `int tmp = 100;
log = log + "audit:" + tmp.toString() + ";";` at marker "audit"
}
}
uses Audit;
class Account {
string log = "";
@Audited
int balance() {
int tmp = 5;
@anchor("audit");
log = log + "mine:" + tmp.toString() + ";";
return tmp;
}
}
Account a = Account();
console.writeln(a.balance());
console.writeln(a.log);
5
audit:100;mine:5;
An expression macro. The argument is a single expression, and the template can use it in any expression position:
A fixed-template expression macro
namespace Text {
macro safeStrip(e) => `($e ?? "").trim()`;
}
uses Text;
string? typed = " hi there ";
console.writeln(safeStrip!(typed));
string? absent = None;
console.writeln("[" + safeStrip!(absent) + "]");
hi there
[]
A doc comment in a template starts on the line after the backtick:
A doc comment inside a template
namespace Docs {
attribute Answer { }
rule answer {
match @Answer on class C
inject `
/// The answer to everything.
int answer() => 42;` at member of C
}
}
uses Docs;
@Answer
class Oracle { }
console.writeln(Oracle().answer());
42
Notes
- A macro call
name!(arg)is expanded before the program is checked, so a macro cannot appear inside the condition of acomptime if. - Generated code is never scanned again for macro calls: a macro whose template contains another macro call is a compile error.
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. - 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. - Attributes — inert, typed annotations — Declare an attribute with
attribute Name { fields }, attach it with@Name(args), and let rules read it. - Procedural macros — comptime code returns code —
macro name(string payload) comptime { … }runs at compile time, receives a string, and returns the code that replaces the call. - Class — A class that a rule has matched, as a rule sees it.
- Method — A method of a class, as a rule sees it.
- Field — A field of a class, as a rule sees it.