LEVIATHAN v962456e · 962456eee1

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 or await access. Reading standard input in a comptime initializer is a compile error, and so is await. The one exception is output: console works and prints during compilation, before any run-time output. Use import() (see lang.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 N raises 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 a comptime initializer whose value is an instance of a class is a compile error.
  • comptime variables are visible to later comptime sites by name.
  • A uses inside a taken comptime if branch is supported: the imports map is recomputed after the fold.
  • A macro call in the condition of a comptime if is a compile error, because the condition would feed the very import map that macro resolution needs. Compute the macro's value into a comptime variable first and test that.
  • Inside a rule template, $if is the expansion-time counterpart of comptime if; see lang.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);
not run — a compile error on purpose

Notes

  • A compile-time console call is visible during compilation only. A compiled binary does not print it again.
  • --expand shows the folded program: a comptime variable appears as an ordinary variable with its computed value.

See also