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 (
rewritesorgenerates). - 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.levlists every rule firing with its location,leviathan --expand file.levprints the post-rule program as compilable source, andleviathan --ast-after-rules file.levprints 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
--rulesalso includes firings of rules that the standard library itself ships, so a short program may list more firings than you wrote rules.
See also
- Attributes — inert, typed annotations — Declare an attribute with
attribute Name { fields }, attach it with@Name(args), and let rules read it. - Rules — match a shape, inject code — A rule matches declarations by shape and injects quasiquoted code at a named anchor, with
match,where, andinject … at. - 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. - comptime — run the language at compile time —
comptimevariables, expressions andifstatements evaluate ordinary code during compilation and fold the result into the program. - target:: — the compilation-target constants —
target::os,target::archandtarget::tripleare compile-time strings that describe the platform being compiled for. - import() — comptime file inclusion —
std::import(path)reads a project file at compile time and folds to its contents as acomptimestring. - 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. - 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.