Variadic packs
A trailing va: ... parameter collects a variable number of call-site arguments into a compile-time sequence. The compiler monomorphizes the function once per distinct argument type-list. There is no runtime structure, no va_list, and no any.
Declaring a pack
A pack parameter is a named parameter whose type is .... It must be last; any number of fixed, typed parameters may precede it.
fun name(va: ...) RetType { ... }
fun name(fixed: T, va: ...) RetType { ... } # leading fixed params are allowed
Iterating with $each
$each a in va unrolls the body once per element, with a bound to the element and re-typed to that element's concrete type per instantiation. This is the only way to consume a pack.
fun sum(va: ...) i64 {
var t: i64 = 0;
$each a in va {
t = t + a;
}
ret t;
}
sum(1, 2, 3) # 6
sum() # 0, the body never runs on an empty pack
Because each element has its own concrete type at monomorphization, the body can handle a heterogeneous pack by casting each element:
$each a in va {
t = t + a::i64; # cast each element's concrete type to i64
}
A $each body is a normal statement block. Runtime variables in the enclosing scope (a cursor, an accumulator) thread across every unrolled iteration: each one reads where the previous left off.
Counting with va.len
va.len folds to the instance's element count at compile time.
fun count(va: ...) i64 { ret va.len::i64; }
count(1, 2, 3) # 3
Forwarding a whole pack
Inside a pack instance, g(va...) forwards the whole pack to another pack-tailed function, which is monomorphized for the forwarded type-list.
fun outer(va: ...) i64 { ret sum(va...); }
va... is valid only as the sole trailing argument of a pack-tailed callee. Spreading into a callee with no pack parameter, spreading with other arguments after it, or spreading only part of a pack are all rejected.
Monomorphization and ABI
A pack-tailed function is compiled once per distinct argument type-list at each call site. Different arities, or the same arity with different types, produce separate instances.
| Call | Instance |
|---|---|
sum(1, 2, 3) | (i64, i64, i64) |
sum(10, 20) | (i64, i64) |
sum(5::u8, 1::u32) | (u8, u32) |
A pack-tailed function has no single entry point, so it is not a stable-ABI symbol. It cannot be the target of an ext fun or a function pointer shared across compilation units. It is source-level only.
Example: vformat
The standard library's vformat is a pack-tailed function. Each $each iteration handles one format argument in order, with $type_of dispatch selecting the right writer per element type.
$each arg in va {
$if ($type_of(arg) == str) { write_str(w, arg); }
$or ($type_of(arg) == i64) { write_i64(w, arg); }
$or { $error("no writer for this argument type"); }
}
The runtime format cursor threads across all unrolled iterations; the argument sequence is consumed entirely at compile time.
See also
- Functions - declarations, generic and comptime parameters
- Intrinsics -
$type_of,$fields,$each - Control flow -
$ifand$orinside pack bodies