LEVIATHAN v962456e · 962456eee1

Metaprogramming

Compile-time metaprogramming

A map of the four compile-time layers, procedural macros, and the --expand flag that shows what a rule produced.

since 0.1.0-alpha.1linuxwindowswasm

Description

Leviathan can run code, and generate code, while it compiles your program. The facilities are layered, and each layer is a separate page of this chapter.

Layer What it is Page
A Attributes: typed annotations that carry data and do nothing by themselves. lang.attributes
B Rules: match a declaration by its shape and inject quasiquoted code at a named place. lang.rules, lang.quasiquote, lang.splices
C comptime: evaluate ordinary Leviathan code at compile time and fold the result into the program. lang.comptime, lang.target-constants, lang.comptime-import
D Body-replacing rules: rewrite a body around the original, or generate a body from scratch. lang.rewrites, lang.generates
Macros Procedural macros: a comptime function that receives a string and returns code. lang.procedural-macros

The rule stage runs after names are resolved and before the program is type-checked and compiled. The code a rule injects is therefore checked and compiled exactly like code you wrote by hand, on every engine, and it costs exactly what the hand-written code costs: there is no runtime reflection. A program that uses no metaprogramming skips the stage entirely.

The result of a rule is always visible. leviathan --expand file.lev prints the program after the rules have run, as ordinary source that compiles and runs identically. This program declares a table attribute and a rule that adds a schema() method to every class carrying it:

A rule adds a method

namespace Orm {
    attribute Table { string name; }
    attribute Column { }
    rule buildSchema {
        match @Table(t) on class C
        inject `Array<string> schema() =>
            [ $t.name, $for f in C.fields.where((x) => x.hasAttr("Column")) : $f.name ];`
            at member of C
    }
}
uses Orm;

@Table("users")
class User {
    @Column int id;
    @Column string name;
    int internalCounter;
}

console.writeln(User().schema().joinToString(","));
users,id,name

Running leviathan --expand on that file prints the class with the generated member in place, marked with the rule that produced it:

namespace Orm {
    attribute Table {
        public string name;
    }
    attribute Column {
    }
}
uses Orm;
@Table("users")
class User {
    @Column
    int id;
    @Column
    string name;
    int internalCounter;
    // from rule Orm::buildSchema @ 13:1
    public Array<string> schema() => ["users", "id", "name"];
}
console.writeln(User().schema().joinToString(","));

Rules

  • Metaprogramming happens at compile time. Nothing in this chapter exists at run time except the code the rules and macros produced.
  • Rules add code. A rule never silently rewrites or deletes what you wrote; the explicit exceptions are the body-replacing rules of Layer D, which carry a loud header (rewrites or generates).
  • A rule fires only in files that import the rule's namespace. Read a file's imports to know every rule that can touch it.
  • leviathan --rules file.lev lists every rule firing with its location, leviathan --expand file.lev prints the post-rule program as compilable source, and leviathan --ast-after-rules file.lev prints the structural syntax tree of the same program.

Examples

A comptime expression folds a computation into a literal before the program runs:

Folding a computation at compile time

int sumTo(int n) {
    int acc = 0;
    for (int i in 1..n) acc = acc + i;
    return acc;
}

comptime int TOTAL = sumTo(100);
console.writeln(TOTAL);
5050

Notes

  • The rule listing from --rules also includes firings of rules that the standard library itself ships, so a short program may list more firings than you wrote rules.

See also