Compile-Time Expressions
A compile-time expression is an expression marked with the comptime keyword that MUST be fully evaluated at compile time.
comptime_expr = "comptime" "{" expression "}" ;
The expression inside a comptime block is evaluated during compilation. The following operations are supported within comptime blocks:
- Integer literals
- Boolean literals (
true,false) - Arithmetic operators (
+,-,*,/,%) - Comparison operators (
==,!=,<,<=,>,>=) - Logical operators (
&&,||,!) - Bitwise operators (
&,|,^,<<,>>)
A comptime expression can be used anywhere an expression is expected. The result of the comptime evaluation replaces the comptime block.
fn main() -> i32 {
let x: i32 = comptime { 21 * 2 };
x
}
Comptime Restrictions
It is a compile-time error if an expression inside a comptime block cannot be evaluated at compile time. This includes:
- References to runtime variables
- Calls to generic functions (functions with
comptime T: typeparameters) in a non-generic comptime context - Operations that would overflow or panic at runtime
- System calls, external functions, or raw pointer dereferences
fn main() -> i32 {
let x = 10;
comptime { x + 1 } // ERROR: x cannot be known at compile time
}
Comptime Parameters
Function parameters can be marked with comptime, requiring the caller to provide a compile-time known value. The parameter's value is available as a compile-time constant within the function body.
parameter = [ "comptime" ] IDENT ":" type ;
Comptime parameters can have any type, including the special type type (see below).
fn multiply(comptime n: i32, value: i32) -> i32 {
n * value
}
fn main() -> i32 {
multiply(6, 7) // n is known at compile time
}
Comptime parameters enable monomorphization: each unique combination of comptime arguments creates a specialized version of the function.
The keyword type is a comptime-only type whose values are types themselves. A parameter of type type must be marked comptime.
fn identity(comptime T: type, x: T) -> T {
x
}
fn main() -> i32 {
identity(i32, 42)
}
When a function has a comptime T: type parameter, occurrences of T in parameter types and return types are substituted with the concrete type at each call site.
It is a compile-time error to pass a runtime value to a comptime parameter.
fn double(comptime n: i32) -> i32 { n * 2 }
fn main() -> i32 {
let x = 21;
double(x) // ERROR: comptime parameter requires a compile-time known value
}
Type values cannot exist at runtime. It is a compile-time error to attempt to store a type value in a runtime variable.
fn main() -> i32 {
let t = comptime { i32 }; // ERROR: type values cannot exist at runtime
0
}
Anonymous Struct Types
A comptime function that returns type can construct an anonymous struct type using the following syntax:
anon_struct_type = "struct" "{" struct_field { "," struct_field } "}" ;
struct_field = IDENT ":" type ;
fn Point() -> type {
struct { x: i32, y: i32 }
}
fn main() -> i32 {
let P = Point();
let p: P = P { x: 10, y: 32 };
p.x + p.y
}
Anonymous structs can be parameterized using comptime type parameters:
fn Pair(comptime T: type) -> type {
struct { first: T, second: T }
}
fn main() -> i32 {
let IntPair = Pair(i32);
let p: IntPair = IntPair { first: 20, second: 22 };
p.first + p.second
}
Two anonymous struct types are structurally equal if and only if they have the same field names in the same order with the same types.
fn make_point1() -> type { struct { x: i32, y: i32 } }
fn make_point2() -> type { struct { x: i32, y: i32 } }
fn main() -> i32 {
let P1 = make_point1();
let P2 = make_point2();
let p1: P1 = P1 { x: 10, y: 20 };
let p2: P2 = p1; // OK: P1 and P2 are structurally equal
p2.x + p2.y
}
Anonymous structs with different field names or different field types are different types and are not assignable to each other.
It is a compile-time error to define an anonymous struct type with no fields and no methods.
fn empty() -> type {
struct { } // ERROR: empty struct
}
Anonymous Struct Methods
An anonymous struct type can include method definitions using the following syntax:
anon_struct_type = "struct" "{" [ struct_field { "," struct_field } ] [ method_def { method_def } ] "}" ;
method_def = "fn" IDENT "(" [ param { "," param } ] ")" [ "->" type ] block ;
Methods defined inside an anonymous struct type become methods on that struct type:
fn Counter() -> type {
struct {
value: i32,
fn increment(self) -> Self {
Self { value: self.value + 1 }
}
fn get(self) -> i32 {
self.value
}
}
}
fn main() -> i32 {
let C = Counter();
let c: C = C { value: 0 };
let c2 = c.increment();
c2.get()
}
Inside an anonymous struct's method definitions, Self refers to the anonymous struct type being defined. Self can be used as a type annotation, in struct literal expressions, and as a return type.
fn Pair(comptime T: type) -> type {
struct {
first: T,
second: T,
fn swap(self) -> Self {
Self { first: self.second, second: self.first }
}
}
}
Methods inside anonymous structs can access comptime parameters from the enclosing function:
fn Array(comptime T: type, comptime N: i32) -> type {
struct {
len: i32,
fn capacity(self) -> i32 {
N // Captured from enclosing comptime context
}
}
}
Functions defined without a self parameter are associated functions, called using the Type::function() syntax:
fn Point() -> type {
struct {
x: i32,
y: i32,
fn origin() -> Self {
Self { x: 0, y: 0 }
}
}
}
fn main() -> i32 {
let P = Point();
let p = P::origin();
p.x
}
It is a compile-time error to define two methods with the same name in an anonymous struct type.
Two anonymous struct types are structurally equal if and only if they have:
- The same field names in the same order with the same types, AND
- The same method names with the same parameter types and return types
Method bodies do not affect structural equality—only signatures matter.
fn A() -> type {
struct { x: i32, fn get(self) -> i32 { self.x } }
}
fn B() -> type {
struct { x: i32, fn get(self) -> i32 { self.x + 1 } } // Same type as A()
}
fn C() -> type {
struct { x: i32, fn get(self) -> i64 { self.x as i64 } } // Different type (i64 vs i32)
}
Comptime Blocks with Local State
A comptime block can contain local variable declarations (let) and assignments. Variables declared inside a comptime block are only accessible within that block. The block evaluates all statements in order and returns the value of the final expression.
fn main() -> i32 {
comptime {
let a = 20;
let b = 22;
a + b // evaluates to 42 at compile time
}
}
Mutable variables may be re-assigned within a comptime block:
fn main() -> i32 {
comptime {
let mut x = 40;
x = x + 2;
x // evaluates to 42 at compile time
}
}
A comptime block can contain if and if/else expressions. The condition must be a compile-time evaluable boolean. Both branches are compile-time evaluable expressions.
fn main() -> i32 {
comptime {
let x = 10;
if x > 5 { x * 4 + 2 } else { 0 } // evaluates to 42 at compile time
}
}
Comptime Loops
A comptime block can contain while loops. The condition and body must be compile-time evaluable. The loop is unrolled at compile time.
fn main() -> i32 {
comptime {
let mut sum = 0;
let mut i = 1;
while i <= 9 {
sum = sum + i;
i = i + 1;
}
sum // evaluates to 45 at compile time
}
}
A comptime block can contain loop expressions. The body must be compile-time evaluable. A break statement exits the loop.
fn main() -> i32 {
comptime {
let mut x = 0;
loop {
x = x + 1;
if x == 42 { break; }
}
x // evaluates to 42 at compile time
}
}
break and continue are supported within comptime loops and have their usual semantics: break exits the innermost loop, continue skips to the next iteration.
It is a compile-time error if a comptime loop executes more than 1,000,000 iterations. This prevents infinite loops from causing the compiler to hang.
fn main() -> i32 {
comptime {
let mut x = 0;
while true { x = x + 1; } // ERROR: exceeds step budget
x
}
}
Comptime Function Calls
A comptime block can call non-generic functions. The called function's body is evaluated at compile time. All arguments must be compile-time evaluable.
fn double(x: i32) -> i32 {
x * 2
}
fn main() -> i32 {
comptime { double(21) } // evaluates to 42 at compile time
}
A comptime block can call functions that themselves call other functions, forming a call chain. Each function in the chain is evaluated at compile time.
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn sum_of_products(x: i32, y: i32, z: i32) -> i32 {
add(x * y, y * z)
}
fn main() -> i32 {
comptime { sum_of_products(2, 3, 7) } // evaluates to 27 at compile time
}
A comptime function call can use return to exit the function early. The returned value becomes the result of the call.
fn clamp(x: i32, lo: i32, hi: i32) -> i32 {
if x < lo { return lo; }
if x > hi { return hi; }
x
}
fn main() -> i32 {
comptime { clamp(100, 0, 42) } // evaluates to 42 at compile time
}
It is a compile-time error if the comptime call stack exceeds 64 frames.
A comptime parameter may receive the result of a function call, provided the call can be fully evaluated at compile time. The callee must be a non-generic function whose arguments are themselves compile-time known. This prevents infinite recursion from causing the compiler to hang.
fn infinite(x: i32) -> i32 {
infinite(x + 1) // ERROR: call stack depth exceeded
}
fn main() -> i32 {
comptime { infinite(0) }
}
Comptime Composite Values
A comptime block can create struct instances and access their fields. All field expressions must be compile-time evaluable. The struct type must be statically known.
struct Point { x: i32, y: i32 }
fn main() -> i32 {
comptime {
let p = Point { x: 10, y: 32 };
p.x + p.y // evaluates to 42 at compile time
}
}
A comptime block can create array instances and read their elements. All element expressions and the index must be compile-time evaluable. The index must be within bounds.
fn main() -> i32 {
comptime {
let arr = [10, 20, 12];
arr[0] + arr[1] + arr[2] // evaluates to 42 at compile time
}
}
It is a compile-time error if an array index is out of bounds in a comptime expression.
fn main() -> i32 {
comptime {
let arr = [1, 2, 3];
arr[5] // ERROR: array index 5 out of bounds (length 3)
}
}