Metaprogramming
comptime — run the language at compile time
comptime variables, expressions and if statements evaluate ordinary code during compilation and fold the result into the program.
since 0.1.0-alpha.1linuxwindowswasm
Description
comptime makes the compiler evaluate a piece of ordinary Leviathan code before the program runs and put
the result into the program as a literal. There are three forms.
- A variable:
comptime int TABLE = nextPrime(1000);evaluates the initializer at compile time and folds it to a literal. - An expression:
1 + comptime sumTo(10)folds everything to the right of the keyword. - An
if:comptime if (cond) { … } else { … }evaluates the condition at compile time and compiles only the branch that is taken. The other branch is parsed but not compiled.
The code that runs at compile time is the same language, including your own functions and the standard library, closures included:
comptime variables and expressions
int sumTo(int n) {
int acc = 0;
for (int i in 1..n) acc = acc + i;
return acc;
}
comptime int N = 5 + 5;
comptime int M = N * 4;
comptime string GREETING = "hello".toUpper() + "!";
comptime Array<int> EVENS = [1, 2, 3, 4, 5, 6].where((x) => x % 2 == 0);
comptime int SUM = sumTo(100);
console.writeln(N);
console.writeln(M);
console.writeln(GREETING);
console.writeln(EVENS);
console.writeln(SUM);
int shifted = 1 + comptime sumTo(10);
console.writeln(shifted);
10
40
HELLO!
[2, 4, 6]
5050
56
Everything after the fold line operates on literals that were computed during compilation. A later
comptime site sees the earlier comptime variables by name, as M sees N above.
The untaken branch of a comptime if is not compiled, so it may mention things that do not exist in the
configuration you are building. The second if below refers to a name that is not declared anywhere:
comptime if compiles one branch
comptime int LEVEL = 3;
comptime if (LEVEL > 2) {
console.writeln("verbose logging");
} else {
console.writeln("quiet logging");
}
comptime if (LEVEL > 5) {
console.writeln(thisNameDoesNotExist);
}
console.writeln("done");
verbose logging
done
A comptime if can also choose imports. A uses inside the taken branch is in effect for the rest of the
file, and it takes part in the file's rule and attribute lookup too:
Choosing an import at compile time
namespace Fast { public string name() => "fast path"; }
namespace Safe { public string name() => "safe path"; }
comptime bool FAST = 2 > 1;
comptime if (FAST) {
uses Fast;
} else {
uses Safe;
}
console.writeln(name());
fast path
Rules
- Evaluation is hermetic. The
std::sys*floor is denied at compile time: there is no file, socket, standard-input, timer orawaitaccess. Reading standard input in acomptimeinitializer is a compile error, and so isawait. The one exception is output:consoleworks and prints during compilation, before any run-time output. Useimport()(seelang.comptime-import) to read a file at compile time. - Evaluation is step-bounded. A runaway compile-time loop is a compile error, and
--comptime-budget Nraises the limit above its default of about 100 million steps. Exhausting the budget cannot be caught. - The result must reify as source text. The supported results are
int,float,bool,string,None, arrays of those, and value structs whose constructor takes the fields one for one, by name and in order; a struct reifies as a constructor call. A reference class does not reify, and acomptimeinitializer whose value is an instance of a class is a compile error. comptimevariables are visible to latercomptimesites by name.- A
usesinside a takencomptime ifbranch is supported: the imports map is recomputed after the fold. - A macro call in the condition of a
comptime ifis a compile error, because the condition would feed the very import map that macro resolution needs. Compute the macro's value into acomptimevariable first and test that. - Inside a rule template,
$ifis the expansion-time counterpart ofcomptime if; seelang.splices.
Examples
A struct result reifies as a constructor call:
A struct from comptime
struct Point {
int x;
int y;
new Point(int x, int y) { this.x = x; this.y = y; }
}
Point midpoint(Point p, Point q) => Point((p.x + q.x) / 2, (p.y + q.y) / 2);
comptime Point m = midpoint(Point(0, 0), Point(10, 20));
console.writeln("${m.x},${m.y}");
5,10
Compile-time output appears while the program is being compiled, not when it runs:
comptime console output
comptime console.writeln("[build] table ready");
console.writeln("runtime output");
Compiling that file prints [build] table ready immediately, and the program's own output follows when it
runs.
Reading standard input is denied at compile time, so this does not compile:
The sys floor is denied
comptime string line = std::sysReadLine(0);
console.writeln(line);
Notes
- A compile-time
consolecall is visible during compilation only. A compiled binary does not print it again. --expandshows the folded program: acomptimevariable appears as an ordinary variable with its computed value.
See also
- Compile-time metaprogramming — A map of the four compile-time layers, procedural macros, and the
--expandflag that shows what a rule produced. - 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. - 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. - 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.