Ruta graveolens  ·  notes from a language experiment  ·  cultivated since 2025

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" "{" block "}" ;
block         = { statement } [ expression ] ;

The block inside a comptime expression is evaluated during compilation. It may contain let statements followed by a tail expression; the comptime expression evaluates to the value of the tail expression. The following operations are supported within comptime blocks:

  • Integer literals
  • Boolean literals (true, false)
  • Arithmetic operators (+, -, *, /, %) and unary negation (-)
  • Comparison operators (==, !=, <, <=, >, >=)
  • Logical operators (&&, ||, !)
  • Bitwise operators (&, |, ^, <<, >>, ~)
  • let bindings of comptime values
  • References to file-level constants and to comptime parameters in scope

Evaluation follows runtime semantics exactly: arithmetic is checked at the operands' type (Chapter 8.1), the full value range of every integer type is supported (including negative results and u64 values above i64::MAX), shift amounts are masked modulo the bit width and shift results truncate to the operand width (4.3a:10), and &&/|| short-circuit.

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
  • Function calls (except to comptime-evaluable functions in future versions)
  • Operations that would panic at runtime: integer overflow at the operands' type (including in intermediate results), division by zero, and remainder by zero
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.

A type parameter may appear anywhere within a composite parameter or return type — as an array element type ([T; N]), a pointer pointee (ptr const T, ptr mut T), or a nesting of these ([[T; 2]; 3]) — and is substituted recursively at each call site, in both parameter and return position.

fn first(comptime T: type, a: [T; 3]) -> T {
    a[0]
}

fn pair(comptime T: type, x: T) -> [T; 2] {
    [x, x]
}

fn main() -> i32 {
    let xs = [10, 20, 30];
    first(i32, xs) + pair(i32, 6)[0]  // 16
}

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
}

Within a specialized function body, an if expression whose condition can be evaluated at compile time (because it references comptime parameters in scope) selects its branch at compile time: only the taken branch is analyzed and compiled. This permits comptime-recursive functions, whose recursive call sits in a branch that is not taken once the recursion reaches its base case.

fn fact(comptime n: i32) -> i32 {
    if n <= 1 { 1 } else { n * fact(n - 1) }
}

fn main() -> i32 {
    fact(5)  // 120: fact(5) .. fact(1) are specialized; fact(1) takes the
             // then-branch, so the recursion terminates
}

It is a compile-time error when specialization exceeds the implementation's maximum nesting depth (at least 64 levels), as happens when a comptime-recursive function never reaches a compile-time-known base case or a generic function recursively instantiates itself with new types. Implementations MUST diagnose this instead of failing to terminate.

fn runaway(comptime n: i32) -> i32 {
    runaway(n + 1)  // ERROR: exceeds the maximum specialization depth
}

Within a specialized function body, a match expression whose scrutinee can be evaluated at compile time likewise selects its arm at compile time: only the body of the first arm whose pattern matches the comptime value is analyzed and compiled. Comptime recursion may therefore equivalently be written with match. The pattern set itself must still be exhaustive (4.7:9) — exhaustiveness is a property of the patterns, which are checked even though unselected arm bodies are not.

fn fact(comptime n: i32) -> i32 {
    match n {
        0 => 1,
        1 => 1,
        _ => n * fact(n - 1),  // not analyzed once n reaches 1
    }
}

fn main() -> i32 {
    fact(5)  // 120: the recursion terminates exactly as with `if`
}

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:

  1. The same field names in the same order with the same types, AND
  2. 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)
}

A comptime function that returns type can construct an anonymous enum (sum) type using the following syntax:

anon_enum_type = "enum" "{" [ enum_variant { "," enum_variant } ] "}" ;
enum_variant = IDENT [ "(" type { "," type } ")" ] ;

Anonymous enums are the sum-type analog of anonymous struct types (rule 4.14:7) and may be parameterized by comptime type parameters, which makes generic sum types such as Option and Result expressible as ordinary library functions rather than compiler builtins:

fn Option(comptime T: type) -> type {
    enum { Some(T), None }
}

fn main() -> i32 {
    let O = Option(i32);
    let x: O = O::Some(5);
    match x { O::Some(n) => n, O::None => 0 }
}

Each instantiation is monomorphized: Option(i32) and Option(bool) are distinct types with independent tagged-union layouts (the payload types differ). A variant that carries a payload requires the enum_payloads preview feature, exactly as for a named enum declaration.

Two anonymous enum types are structurally equal if and only if they have the same variant names in the same order carrying the same payload types. Consequently, two instantiations of the same comptime type function with the same type arguments (for example Option(i32) evaluated twice) denote the same type, while instantiations with different type arguments denote different, non-assignable types.

fn Option(comptime T: type) -> type { enum { Some(T), None } }

fn main() -> i32 {
    let A = Option(i32);
    let B = Option(i32);
    let x: A = A::Some(10);
    let y: B = x;  // OK: A and B are structurally equal
    match y { B::Some(n) => n, B::None => 0 }
}