LEVIATHAN v962456e · 962456eee1

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.field reads a field of a bound attribute and puts its value in as a literal: $e.table became the string "people" and $e.version the integer 2.
  • $m or $C, where the binding is a declaration, splices the declaration's name as an identifier. In member-selector position this selects the member, as in this.$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.name and $m.name give the declaration's simple name as a string literal.
  • $C in type position carries the matched class through to the checker, so $C copy = $C(); declares a local of the matched class and constructs one.
  • $_params and $_args forward the matched method's own parameter list and argument list; see lang.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 as name!(arg). Each argument is captured as a syntax node and spliced where $e appears. 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, see lang.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 a comptime if.
  • Generated code is never scanned again for macro calls: a macro whose template contains another macro call is a compile error.

See also