LEVIATHAN v962456e · 962456eee1

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 to bind. The binding is optional: @Attr alone 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 are method, function, class, struct, field, constructor, interface, namespace and type.
  • in <kind> <bind> names an enclosing declaration, and : Type optionally requires that it implements or extends Type, checked against its resolved base chain. A rule may have several in clauses.
  • match one requires that the declaration carries at most one matching attribute; two of them is a compile error.
  • on class C and on struct C each match only their own kind. on type C matches a class, a struct or an interface in one rule, and C is still bound to the actual declaration. An attribute whose rule matches a different kind gets a warning that suggests on type, on struct or 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 example m.returnType != "void"). A declaration that where excludes 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 several inject clauses. A single rule is either additive (inject) or a body rewriter (replace, see lang.rewrites and lang.generates), never both.
  • The template is a quasiquote; see lang.quasiquote for holes and lang.splices for $for, $if and 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 reentrant opts in; see lang.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.lev lists each firing as <rule> fired at <line>:<column>.

See also