Metaprogramming
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.
since 0.1.0-alpha.1linuxwindowswasm
Description
A rule is declared in a namespace. It names a shape to look for and a template to inject where it matched:
rule name {
match <shape>
inject `<template>` at <anchor>
}
This rule turns every @Route method of a controller into a registration call at the end of the
controller's constructor. The injected call is ordinary code that goes through the normal checker and
compiler:
Registering routes from attributes
namespace Web {
interface IController { }
attribute Route { string method; string path; }
rule registerRoutes {
match @Route(r) on method m in class C : IController
inject `router.record($r.method, $r.path)` at bottom of C.constructor
}
}
uses Web;
class Router {
string routes = "";
Router record(string method, string path) {
routes = routes + method + " " + path + "\n";
return this;
}
}
class UserController : IController {
Router router;
new UserController(Router shared) { router = shared; }
@Route("GET", "/users") int list() => 1;
@Route("POST", "/users") int create() => 2;
int helper() => 3;
}
Router r = Router();
UserController uc = UserController(r);
console.write(r.routes);
GET /users
POST /users
The rule matched the two @Route methods but not helper, because helper has no @Route. The
template reads the attribute's values as $r.method and $r.path (see lang.quasiquote).
A rule fires on a declaration only in files that import the rule's namespace: a bulk uses NS; or a
selective use NS::name; both opt the file in. The same import graph scopes attributes (see
lang.attributes), so reading a file's imports tells you every rule that can touch it.
Rules
The match clause.
- The shape is
match [one] [@Attr(bind)] on <kind> <bind> [in <kind> <bind> [: Type]]… [where <test>]. @Attr(bind)requires the declaration to carry the attribute and binds its evaluated arguments tobind. The binding is optional:@Attralone only requires the attribute. A rule with no attribute at all matches by kind and enclosing declaration alone.on <kind> <bind>names the declaration to match and binds it. The kinds aremethod,function,class,struct,field,constructor,interface,namespaceandtype.in <kind> <bind>names an enclosing declaration, and: Typeoptionally requires that it implements or extendsType, checked against its resolved base chain. A rule may have severalinclauses.match onerequires that the declaration carries at most one matching attribute; two of them is a compile error.on class Candon struct Ceach match only their own kind.on type Cmatches a class, a struct or an interface in one rule, andCis still bound to the actual declaration. An attribute whose rule matches a different kind gets a warning that suggestson type,on structor the right kind word.where <test>filters the match with an ordinary compile-time boolean over the bindings, written with plain names rather than$holes (for examplem.returnType != "void"). A declaration thatwhereexcludes is intentionally skipped and does not trigger the "matched no imported rule" warning.
The inject clause.
inject `<template>` at <anchor>splices the template at the anchor. A rule may carry severalinjectclauses. A single rule is either additive (inject) or a body rewriter (replace, seelang.rewritesandlang.generates), never both.- The template is a quasiquote; see
lang.quasiquotefor holes andlang.splicesfor$for,$ifand name synthesis. - When several rules inject at the same anchor, they apply in the order the rules are declared.
Anchors.
| Anchor | Where the template lands |
|---|---|
top of C.constructor, bottom of C.constructor |
The first or last statements of the constructor of the bound class C. If the class has no constructor, a nullary one is synthesized. |
top of body, bottom of body |
The first or last statements of the matched declaration's own body. bottom of body is an error when the body already ends in a value return, because the code would be unreachable. |
member of C |
A new member of the class C. The template may declare several members. A member with the same name and type as an existing one is a compile error. |
marker "name" |
At a statement @anchor("name"); that you place in the body of the matched declaration. The marker statement stays in place, and several rules can target the same marker. A missing marker is a compile error. |
splice Name [multi] |
At a statement @Name(); anywhere in the program, where Name is a declared attribute. See the next section. |
namespace N |
New declarations in the namespace N, which is reopened and merged like any namespace. |
Splice sites. A statement @Name(); in a function or method body is a named splice site. A rule that
injects at splice Name lands its statements at that site, in the site's own scope, so the injected code
sees the site's parameters and locals like hand-written code. Unlike a marker, which resolves inside the
matched declaration, a splice site can be in a different function from the one the rule matched. A site
that no rule fires on is silent, so it works as an intentional extension point. A rule whose site does not
exist is a compile error, and so is a rule that finds two sites unless it says at splice Name multi,
which injects into every site. A @Name(); whose Name is not a declared attribute is a compile error.
Hygiene.
- Ordinary rules only add code. Injected code never captures or is captured by names at the use site: a local that the template declares is renamed to a fresh symbol.
- By default a rule does not match code that another rule injected. A rule declared
reentrantopts in; seelang.rewrites.
Examples
Each anchor in one program. The marker is placed by the author and several rules land at the top, the bottom and the marker of the same method, while a fourth rule adds a member:
Anchors: top, bottom, marker and member
namespace Trace {
attribute Traced { }
rule enter {
match @Traced on method m
inject `log = log + "enter;";` at top of body
}
rule leave {
match @Traced on method m
inject `log = log + "leave;";` at bottom of body
}
rule midpoint {
match @Traced on method m
inject `log = log + "audit;";` at marker "audit"
}
rule describe {
match @Traced on method m in class C
inject `string name() => "traced class";` at member of C
}
}
uses Trace;
class Worker {
string log = "";
@Traced
void run() {
log = log + "work;";
@anchor("audit");
log = log + "more;";
}
}
Worker w = Worker();
w.run();
console.writeln(w.log);
console.writeln(w.name());
enter;work;audit;more;leave;
traced class
A rule can match by kind alone and let where decide. Only the methods whose return type is not void
count here:
A where clause
namespace Count {
interface IService { }
rule onlyNonVoid {
match on method m in class C : IService
where m.returnType != "void"
inject `hits = hits + 1;` at bottom of C.constructor
}
}
uses Count;
class Svc : IService {
int hits = 0;
int a() => 1;
void b() { }
string c() => "x";
}
console.writeln(Svc().hits);
2
on type covers classes and structs with one rule:
on type matches a class and a struct
namespace Marker {
attribute Serializable { }
rule tag {
match @Serializable on type C
inject `int kindTag() => 1;` at member of C
}
}
uses Marker;
@Serializable
class Widget { }
@Serializable
struct Point { int x; int y; }
console.writeln(Widget().kindTag());
console.writeln(Point(1, 2).kindTag());
1
1
A splice site lets a rule contribute statements to a function the rule does not match. Each @Route
method registers itself at the single @InjectRoutes(); site:
A named splice site
namespace App {
attribute InjectRoutes { }
attribute Route { string path; }
rule emitRoutes {
match @Route(r) on method m in class C
inject `sink = sink + $r.path + ";";` at splice InjectRoutes
}
}
uses App;
class HomeController {
@Route("/") void index() { }
}
class AboutController {
@Route("/about") void about() { }
}
string addRoutes() {
string sink = "";
@InjectRoutes();
return sink;
}
console.writeln(addRoutes());
/;/about;
The namespace anchor injects whole declarations. Here every @Register class gets a function of the
same name in the namespace Gen ($C splices the matched class's name):
Injecting at namespace scope
namespace Reg {
attribute Register { string label; }
rule addToRegistry {
match @Register(r) on class C
inject `Array<string> $C() => [$r.label];` at namespace Gen
}
}
uses Reg;
@Register("widget")
class Widget { }
@Register("gadget")
class Gadget { }
console.writeln(Gen::Widget());
console.writeln(Gen::Gadget());
[widget]
[gadget]
A rule can match fields, constructors and free functions as well as methods. This program uses field,
constructor and function subjects and one rule with two inject clauses:
Subjects other than methods
namespace Kinds {
attribute Mark { }
rule onField {
match @Mark on field f in class C
inject `string fieldMark() => $f.name;` at member of C
}
rule onCtor {
match @Mark on constructor k in class C
inject `string ctorMark() => "ctor";` at member of C
}
rule onFunction {
match @Mark on function f
inject `console.writeln("function marked");` at top of body
}
rule twoClauses {
match @Mark on class C
inject `string first() => "one";` at member of C
inject `string second() => "two";` at member of C
}
}
uses Kinds;
@Mark
class Thing {
@Mark int size;
@Mark new Thing() { size = 1; }
}
@Mark
void hello() { console.writeln("hello"); }
Thing t = Thing();
console.writeln(t.fieldMark());
console.writeln(t.ctorMark());
console.writeln(t.first() + t.second());
hello();
size
ctor
onetwo
function marked
hello
Notes
leviathan --rules file.levlists each firing as<rule> fired at <line>:<column>.
See also
- Attributes — inert, typed annotations — Declare an attribute with
attribute Name { fields }, attach it with@Name(args), and let rules read it. - Quasiquote templates and holes — Backtick-delimited templates hold the code a rule injects, and
$holes fill them from the match. - 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. - Body-replacing rules — rewrites, replace and $body — A
rewrites body ofrule replaces a method's body with a template that splices the original back in with$body. - Body-generating rules — generates and replace — A
generates body ofrule replaces a method's body outright and discards the original, for stubs whose body is machine-filled. - Compile-time metaprogramming — A map of the four compile-time layers, procedural macros, and the
--expandflag that shows what a rule produced. - Class — A class that a rule has matched, as a rule sees it.