Intrinsics
Intrinsics are compiler-shipped comptime functions. They share the ordinary $name(args) call shape, but their names are reserved and their bodies live in the compiler. The set is closed: adding one requires a compiler change.
Value intrinsics
These fold to a comptime constant unsigned integer. The storage type is whatever the binding declares - Mach has no compiler-known usize.
$size_of(T) # byte size of type T
$align_of(T) # byte alignment of type T
$offset_of(T, field) # byte offset of T's field
pub val POINT_SIZE: i64 = $size_of(Point);
pub val POINT_X: i64 = $offset_of(Point, x);
Type intrinsic
$type_of(expr) produces a comptime type value: the resolved type of its argument. Type values have no runtime representation; they are meaningful only as operands in comptime type comparisons.
$type_of(expr) # comptime type value of expr
Compare type values with == or != inside a $if condition. A bare type name (i64, str, Point) is the other valid operand. The comparison selects one branch at compile time per monomorphization instance - useful for per-element type dispatch inside $each bodies.
$if ($type_of(arg) == i64) { write_i64(w, arg); }
$or ($type_of(arg) == str) { write_str(w, arg); }
$or { $error("unsupported type"); }
Provably-dead arms are pruned before type-checking, so each arm uses arg at its own concrete type with no per-arm cast: the str arm above is never checked against a u64 element. Only the selected arm is type-checked and emitted.
Field intrinsic and projection
$fields(T) produces a comptime sequence of field descriptors for record or union type T. Each descriptor carries three readable properties.
| Property | Type | Value |
|---|---|---|
f.name | *u8 | field name as a NUL-terminated string |
f.type | type value | comptime type value of the field's type |
f.offset | integer | byte offset of the field in T's layout |
The sequence is consumed by $each f in $fields(T). Inside the body, v.[f] projects the concrete field off an instance v. It is an lvalue: readable and writable, including through a pointer receiver.
$fields(T) # comptime field sequence for record/union T
v.[f] # comptime field projection: access the field f on v
rec Pair { x: i64; y: i64; }
fun sum(p: Pair) i64 {
var total: i64 = 0;
$each f in $fields(Pair) {
total = total + p.[f]; # p.x on iteration 1, p.y on iteration 2
}
ret total;
}
$each f in $fields(Empty) expands to nothing when T has no fields.
Heterogeneous fields
Because each iteration re-types v.[f] to the concrete field type, heterogeneous records work naturally - cast each field as you fold it.
rec Mixed { a: i64; b: u8; }
fun total(m: Mixed) i64 {
var t: i64 = 0;
$each f in $fields(Mixed) {
t = t + m.[f]::i64; # m.a (i64) on iter 1, m.b (u8) cast to i64 on iter 2
}
ret t;
}
Descriptor reads
A descriptor's properties can be read inside the loop body - f.offset for layout math, f.type for type comparisons.
fun offsum(m: Mixed) i64 {
var s: i64 = 0;
$each f in $fields(Mixed) {
s = s + f.offset::i64; # 0 + 8 = 8 for Mixed { a: i64; b: u8; }
}
ret s;
}
fun count_i64(m: Mixed) i64 {
var n: i64 = 0;
$each f in $fields(Mixed) {
$if (f.type == i64) { n = n + 1; }
$or { }
}
ret n;
}
A field literally named type is unaffected: ordinary v.type access still works. The v.[f] projection uses the $each loop variable, which is always a field descriptor, never a regular member.
Nested $each
$each can be nested to walk a record's fields against another's.
fun cross(p: Pair, q: Pair) i64 {
var t: i64 = 0;
$each f in $fields(Pair) {
$each g in $fields(Pair) {
t = t + p.[f] * q.[g];
}
}
ret t;
}
$each: compile-time unroll
$each is a statement form that splices its body once per element of a comptime sequence. There are two sequence forms.
$each f in $fields(T) { ... } # one iteration per field of T
$each a in va { ... } # one iteration per element of pack va
It is valid only in statement scope, inside a function body. It is not a loop: the body is duplicated at compile time, not iterated at runtime. Enclosing runtime variables (an index, an accumulator) are shared across all unrolled copies. See Variadic packs for the pack form.
Diagnostic intrinsics
$error("msg") fails compilation with msg when it is reached on a live path: an unconditional position, or a $if / $or arm the compiler selects. A $error in a discarded arm never fires, so it is the natural total-coverage fallback for a $type_of dispatch: the unhandled-type $or {} arm fails the build at compile time instead of falling through to a runtime error. It is valid in both declaration and statement scope and takes one string-literal message.
$error("msg") # fails compilation when reached
$if (!supported) {
$error("this target is not supported");
}
$if ($type_of(arg) == i64) { write_i64(w, arg); }
$or ($type_of(arg) == str) { write_str(w, arg); }
$or { $error("no writer for this argument type"); } # compile error on an unhandled type
$assert planned parses as a comptime directive but the compiler does not yet evaluate it. It is intended as sugar over $if plus $error: $assert(cond, "msg") is equivalent to $if (!cond) { $error("msg"); }, e.g. $assert($size_of(i64) == 8, "expected 64-bit i64");.
Not provided as intrinsics
Code intrinsics - runtime-instruction emitters like trap, fence, and pause - are not in the compiler-shipped set. They belong in the standard library as functions with per-arch asm bodies.
See also
- The comptime channel - what runs at compile time and how it folds
- Control flow -
$ifand$orbranch selection - Variadic packs -
$each a in vaover argument sequences