Control flow
$if and $or branch on comptime-evaluable conditions. Only the taken arm is compiled; the discarded arms are never resolved, type-checked, or emitted - unlike runtime if / or, which always generates a branch.
The $if / $or form
A $if chain mirrors runtime if / or in shape, but each gate is a comptime condition and only the selected branch reaches the binary. $or with a condition is an else-if; $or with no condition is the comptime else.
$if (cond) {
...
}
$or (cond) {
...
}
$or {
... # comptime else
}
Target gating is the most common use: select the right import or backend per build, and leave the rest out of the binary entirely.
$if ($mach.build.os == $mach.os.linux) {
use full.os.linux;
}
$or ($mach.build.os == $mach.os.windows) {
use full.os.windows;
}
$or {
$error("unsupported OS");
}
Comptime conditions
The gate must be a comptime expression. Common shapes:
$mach.*reads for target and build conditions- comparisons of comptime constants (
pub valdeclarations) - comparisons of a comptime function parameter - see below
A comptime comparison or arithmetic relates the mathematical values of its operands, exactly as the runtime operators do. A constant in the range 2^63 to 2^64 - 1 carries its true unsigned magnitude, so $if (0xFFFFFFFFFFFFFFFF > 0) is taken, and any cross-sign comparison agrees with the runtime form: $if (X < Y) never selects a branch that if (X < Y) would not.
Comptime arithmetic that overflows the value's range is a compile error, not a silent wrap.
Branching on a comptime parameter
A comptime function parameter ($mode: u8) is a compile-time-known argument, fixed per call site. $if / $or may branch on it: the compiler monomorphizes the body once per distinct comptime-argument value, and each instance compiles only the arm its value selects.
val MODE_DOUBLE: u8 = 0;
val MODE_SQUARE: u8 = 1;
fun apply($mode: u8, n: i64) i64 {
$if (mode == MODE_DOUBLE) {
ret n + n;
}
$or (mode == MODE_SQUARE) {
ret n * n;
}
ret 0;
}
# apply(MODE_DOUBLE, ..) and apply(MODE_SQUARE, ..) emit two distinct bodies,
# each carrying only its selected arm.
Rules
- The argument bound to a
$-parameter must be a compile-time constant at the call site - a literal, apub val, or another comptime parameter. A runtime value is rejected withcomptime argument is not a compile-time constant. Cross-module constants work, whether imported by bare name or as a qualified member (alias.CONST). - Each arm gate must itself be comptime-foldable: its identifiers must all be comptime (parameters or constants). A gate that references a runtime local or parameter is rejected.
- A comptime parameter has no storage, so its address cannot be taken:
?$modeis rejected withcannot take the address of a comptime parameter. - A comptime-parameter function is a template, not a value - it can only be called, never assigned, passed, or compared.
val fp = apply;is rejected withcannot reference a comptime-parameter function as a value. - Comptime parameters carry no runtime cost: they are stripped from the lowered signature and ABI, so only the runtime parameters are passed. They may be mixed freely with runtime parameters in any order.
- An instance is emitted against its declaring module and folds its gates against that module's own comptime constants, so a library can export a comptime-parameter function gated on its own
pub vals. - Combining a comptime parameter with a generic function (
fun f[T]($mode: u8, ...)) is planned - it is reported today with a clear diagnostic.
Because each instance compiles only its taken arm, a comptime parameter can safely gate per-target asm blocks: a register one backend does not recognize lives only in the arm the other backend never compiles.
pub fun load($order: Order, ptr: *i64) i64 {
var result: i64 = 0;
$if ($mach.build.arch == $mach.arch.aarch64) {
$if (order == RELAXED) {
asm aarch64 { ldr {result}, [{ptr}] }
}
$or (order == ACQUIRE) {
asm aarch64 { ldar {result}, [{ptr}] }
}
}
ret result;
}
Discarded branches
An untaken $if branch is absent from the compiled output, but what "not taken" means for name resolution depends on what the gate reads.
$mach.*or constant gates - the untaken arms are entirely absent and the compiler does not even resolve names inside them. This is what makes per-targetasmblocks safe when one arm references registers the other backend has never heard of.- comptime-parameter gates - the one exception. Because arm selection happens per call site at monomorphization, name resolution and type checking run over all arms structurally, and only the selected arm is emitted into each instance. Every arm must therefore be independently resolvable and type-checkable.
$type_oftype gates - not an exception. At monomorphization the operand's concrete type is known, so provably-dead arms are pruned and only the selected arm is type-checked and emitted. Each arm may use its value at its own concrete type with no per-arm cast.
See also
- The comptime channel -
$mach.*reads for target and build conditions - Intrinsics -
$type_ofdispatch and the comptime toolkit - Statements - the runtime
if/orcounterpart - Variadic packs -
$if/$orinside$eachbodies