Download

14. Compile-time Execution

@comptime#

@comptime computes a value at compile time:

build_lookup_table :: fn() -> u32[] {
    result: u32[]: []
    for i in 0..<256 {
        result.push(u32(i * i))
    }
    return result
}

@comptime
TABLE :: build_lookup_table()

The call runs during compilation, and TABLE is a constant computed once. The value is a scalar, a string, an array, a struct or a tuple, nested at will; a function value, a map, a set, or a value depending on itself (directly or through other constants) is an error.

In its inline form, @comptime prefixes the right-hand side of a constant:

TABLE :: @comptime {
    result: u32[]: []
    for i in 0..<256 {
        result.push(u32(i * i))
    }
    return result
}

// typed form: built at compile time
SQUARES: u32[]: @comptime { ... }

A block runs as a function body, where return or a trailing expression gives the value. The typed form NAME: T: @comptime <expr> gives the value the type T.

In its decorator form, @comptime above NAME :: <expr> computes what NAME :: @comptime <expr> does, unless <expr> has the shape of a type (a name, Mod.T, Box<T>, T[], A | B, [A, B], a function type, Int in 0..=23). Such an <expr> runs no function body:

  • naming a type, the declaration is a type alias (§4 Distinct types);
  • naming a value, such as a constant, it is folded and cannot read a constant computed by running code (a loop, a statement body), which the inline form can.

Sandbox#

A @comptime value can:

  • compute, allocate and build data structures;
  • print with println. The output is not part of the value.
  • read the clock, the environment and the filesystem. The build is then no longer reproducible.
  • read a file relative to its own source. A @comptime value runs with the directory of the file that declares it as working directory: read_file_bytes("sprite.png", ...) reads the file next to that source, and current_working_directory() returns that directory.

In a cross build, a @comptime value runs the @target bodies of the build machine, while VADER_OS and VADER_ARCH keep the target's values.

A @comptime value cannot:

  • ask which machine the compiler runs on (current_os, current_arch, the dynamic FFI: ffi_open, ffi_symbol, ffi_call), or drive the async scheduler, which only runs with the program: both are rejected;
  • call a @singleton function, or anything that reaches one, such as spawning a process.

Reflection / comptime intrinsics#

A reflection intrinsic @name(args) is an expression answered from the types of the program, a constant once its type arguments are known, except @field.

An argument T: type is a type, a value of the metatype type (§4), fixed at the call site; only @size_of also accepts one known at run time, such as a parameter t: type. A wrong argument count or unknown name is an error.

Signature Result
@size_of(T: type) -> i64, @align_of(T: type) -> i64 Width and alignment in bytes of a slot holding a T.
@type_name(T: type) -> string Name of T as diagnostics print it: "i32 | string".
@type_kind(T: type) -> string Shape of T.
@min(T: type) -> T, @max(T: type) -> T Least and greatest value of T.
@field_count(T: type) -> i64 Number of fields of a struct or elements of a tuple, else 0.
@variant_count(T: type) -> i64 Number of variants of a union (nested unions flattened) or an enum, else 0.
@field_index(T: type, name: string) -> i64 Position of a field in the struct T.
@satisfies(T: type, Trait: type) -> bool Whether T implements Trait.
@type_of(x) -> type Static type of x, unevaluated.
@fields(T: type) -> Field[] Fields of the struct T.
@type_args(T: type) -> type[] Type arguments of a generic struct or trait instance, else [].
@field(x, name: string) -> <field type> Field name of x.

@assert checks them at build time:

@assert(@size_of(MyHotStruct) <= 64,
        "MyHotStruct must fit in a cache line")
@assert(@type_kind(KeyType) == "primitive" || @type_kind(KeyType) == "struct",
        "KeyType is not hashable")

@size_of and @align_of#

@size_of(T) measures the slot (a field, a local, a parameter), not the object a reference points at:

  • 1 for i8, u8, bool; 2 for i16, u16; 4 for i32, u32, f32, char, string; 8 for i64, u64, f64;
  • for an enum, an integer range type or a distinct type, the size of its representation (§4 Representation, Range types, Distinct types);
  • 8 for the references: a struct, an array, a tuple, a function value;
  • 16 for a union, a trait value and any (a value and its type);
  • 0 for null, void, type and any type without a value at run time.

@align_of(T) is a scalar's width, 8 for a 16-byte slot.

@type_kind#

@type_kind(T) is one of "primitive", "struct", "enum", "union", "array", "tuple", "fn", "trait", "type", "any" and "unknown", a fixed set compared as a whole. A range type is "primitive", and a distinct type has the kind of its backing type (§4 Distinct types).

@min and @max#

@min(T) and @max(T) are constants of T, for an integer type, a range, char, a type parameter bounded by Int (§4 Range types) or an enum without variant data (§4 Values); any other T is an error.

@satisfies#

@satisfies(T, Trait) is true when T implements Trait by an impl the calling module sees or a built-in one (an enum's Equals and Hash), the same under @assert and in a function body.

  • A generic impl counts only for the arguments its bounds admit: under Box<T> implements<T: Display> Display, Box<Point> satisfies Display only when Point does.
  • Arguments written on Trait are compared, its defaults filled in: Money implements Add<Money, Money> satisfies Add<Money, Money>, not Add<i32, i32>.
  • A Trait that is not a trait gives false.

Fields: @fields, @field_index and @field#

@fields and @field_index take a struct; a T known to be another type is an error.

  • @fields(T) gives a Field[] (std/core), each with a name: string and a type: type.
  • @field_index(T, "name") counts from 0, and name is a string literal naming a field of T.
  • @field(x, "name") reads x.name, under the same type, @internal and narrowing rules; name is a string literal, or f.name in @comptime for f in @fields(T).

@comptime for over reflected fields#

@comptime for x in e { ... } copies its body at compile time once per element of e, with x bound to it. e is an array known at compile time, of at most 256 elements, such as an array literal, @fields(T) or @type_args(T).

In @comptime for f in @fields(T), f.name and f.type are constants in each copy, and @field(x, f.name) is a plain field access.

Pair :: struct<K, V> {
    key:   K
    value: V
}

print_sizes :: fn() -> void {
    @comptime for f in @fields(Pair<u8, i64>) {
        println("${f.name}: ${@size_of(f.type)}")    // "key: 1", then "value: 8"
    }
}
  • continue goes on with the next copy, break leaves every copy, and a copy's defers run when it is left.
  • A destructuring binding ([k, v]) is an error.