machdocs
Home GitHub

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
}
Note

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...); }
Restriction

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.

CallInstance
sum(1, 2, 3)(i64, i64, i64)
sum(10, 20)(i64, i64)
sum(5::u8, 1::u32)(u8, u32)
Not a stable ABI

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