LEVIATHAN v962456e · 962456eee1

Metaprogramming

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.

since 0.1.0-alpha.1linuxwindowswasm

Description

A fixed-template macro (see lang.quasiquote) substitutes its argument into a template. A procedural macro is the programmable counterpart: its body is ordinary comptime code that can branch, loop and build a result, and it returns the code to put in place of the call.

macro name(string payload) comptime <body>

The macro takes exactly one string. The body returns an Ast, which it normally obtains from meta::parseExpr(text), a compile-time parser for a source expression. The call name!(arg) is replaced by that expression. In this program the macro builds a sum of as many + 1 terms as the payload has characters:

A macro that builds code

macro countChars(string payload) comptime {
    var parts = ["0"];
    for (int i in 0..payload.length()) parts = parts.add("+ 1");
    return meta::parseExpr(parts.joinToString(" "));
}

int n = countChars!(`four`);
int m = countChars!("a longer payload");
console.writeln(n);
console.writeln(m);
5
17

A call has the form name!(argument). The argument is evaluated at compile time and must be a string. A backtick string is accepted as a raw string in this position only, which is convenient for a payload that has quotes in it, such as markup:

A raw backtick payload

macro choose(string payload) comptime {
    if (payload == "<Text title=\"demo\"/>") return meta::parseExpr("10 + 1");
    return meta::parseExpr("20 + 2");
}

int raw = choose!(`<Text title="demo"/>`);
int other = choose!("anything else");
console.writeln(raw);
console.writeln(other);
11
22

Rules

  • macro name(string payload) comptime <body> takes exactly one comptime-evaluable string parameter and must return an Ast; any other parameter list is a compile error.
  • meta::parseExpr(string) parses an expression and meta::parseStmts(string) parses statements. Both are compile-time only: at run time they throw. A parse failure reports the macro call, shows the generated text and puts a caret at the position in it.
  • A call used as an expression needs an expression Ast; a macro that returns statements there is a kind error.
  • A backtick string is accepted as a raw string only in the argument position of a macro call. It does not become a general expression.
  • The body runs under the ordinary comptime rules: it is hermetic and bounded by the step budget (see lang.comptime), and import() is the one file input it may use. A call to an ordinary function of the same name inside the body is plain recursion at compile time and shares the same budget.
  • Generated code is never scanned again for macro calls: a generated name!(...) is a compile error, and a macro must not expand into a call of another macro.
  • Every splice clones the generated tree and gives its nodes the span of the call site, so diagnostics point at the call.
  • Hygiene is the macro author's job. A macro that declares locals in generated code should name them with a reserved prefix such as __<package>_ so that they cannot collide with names at the call site.
  • --expand prints only the program after expansion. The procedural macro body does not survive into the printed program.
  • A macro can be declared inside a namespace and is then imported like an attribute or a rule, with uses.

Examples

A macro call is an expression, so it also works under comptime. Here the generated expression is folded at compile time:

A macro under comptime

macro eval(string payload) comptime { return meta::parseExpr(payload); }

comptime int answer = eval!("6 * 7");
console.writeln(answer);
42

A macro declared in a namespace and imported with uses:

A macro in a namespace

namespace Gen {
    macro repeatPlus(string payload) comptime {
        string text = "0";
        for (int i in 0..payload.length()) text = text + " + 2";
        return meta::parseExpr(text);
    }
}
uses Gen;

int n = repeatPlus!("abc");
console.writeln(n);
8

Notes

  • A procedural macro takes only one payload in this release. To pass several values, encode them in the one string and parse them in the macro.

See also