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
@comptimevalue 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, andcurrent_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
@singletonfunction, 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 fori16,u16; 4 fori32,u32,f32,char,string; 8 fori64,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,typeand 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>satisfiesDisplayonly whenPointdoes. - Arguments written on
Traitare compared, its defaults filled in:Money implements Add<Money, Money>satisfiesAdd<Money, Money>, notAdd<i32, i32>. - A
Traitthat 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 aField[](std/core), each with aname: stringand atype: type.@field_index(T, "name")counts from 0, andnameis a string literal naming a field ofT.@field(x, "name")readsx.name, under the same type,@internaland narrowing rules;nameis a string literal, orf.namein@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"
}
}
continuegoes on with the next copy,breakleaves every copy, and a copy's defers run when it is left.- A destructuring binding (
[k, v]) is an error.