Design: Variadic Functions (P2-2)¶
Status: accepted. Implemented across two PRs — variadic-extern (P2-2c) and
variadic-native (P2-2b).
Summary¶
Sushi gains two distinct, deliberately separated variadic mechanisms, mirroring how FFI already
separates ptr (unmanaged, foreign) from Own<T> (RAII, native):
- Foreign / unsafe world — untyped C varargs. A bare trailing
...is allowed only inside anunsafe external "C"block. It maps directly to an LLVMvar_arg=Truedeclaration so that variadic libc functions such asprintfcan be bound. This is Rust's stance:...exists only at theextern "C"boundary, never in safe native code. - Native / safe world — homogeneous array sugar. A typed
...Ttrailing parameter on a plain native function collects the trailing call arguments into an owned dynamic arrayT[]. The callee iterates it with the existing.iter()/foreach/.len()API, and it is RAII-destroyed at scope exit. This is the Go/Java/Swift consensus model.
A va_list-style intrinsic for native code was rejected: it would import C's unsafety into safe
Sushi, contradicting the bounds-checked / RAII / no-null safety model.
Syntax¶
# Native: prefix marker on the last parameter, element type T
fn log_all(string prefix, ...i32 values) ~:
foreach(v in values.iter()):
println(v)
log_all("nums", 1, 2, 3) # values = [1, 2, 3]
log_all("empty") # values = [] (zero variadic args allowed)
# Extern: bare trailing ... after at least one fixed parameter
unsafe external "C" as libc because "formatted output via libc":
fn printf(string fmt, ...) i32 = "printf"
Semantics¶
- Element type is a single concrete
T(homogeneous). Reference element types (&peek/&poke) are rejected withCE0114. A dynamic-array element...T[]is allowed: each trailing array argument is moved (not copied) into the synthesized array-of-arrays, so there is no double-free. Only the individual-args call form (total(a, b)) is reachable in practice, since a truei32[][]source value cannot be constructed in surface syntax — see "Spread / forwarding (bloom)" below for the array-of-Tcase. - Zero trailing arguments is valid; the native callee receives an empty
T[]. - Native ownership. The call site synthesizes a
T[], which is moved into the callee; the callee owns and destroys it via the normal dynamic-array RAII path. The LLVM function itself stays non-variadic — the variadic parameter lowers to one extraT[]struct parameter. - Extern lowering. The extern declaration lowers to an LLVM
var_arg=Truedeclaration. Trailing arguments undergo C default-argument promotion:i8/i16/bool→i32,f32→f64;stringis marshalled to achar*and freed at scope exit on every path;ptris passed as-is. Externs remain the single exception to implicitResultwrapping (they return raw C values). - Extern requires ≥1 fixed parameter — the C ABI needs a named argument for
va_start.
Diagnostics¶
CE5004— variadic external requires at least one fixed parameter.CE5005— non-C-ABI type passed as a variadic argument to an external call.CE0114— variadic parameter must be the last parameter; a function may declare at most one; its element type must not be a reference. A dynamic-array element (...T[]) is allowed. Also rejected in generic functions (generic variadics are out of scope for v1).CE0115— variadic parameter not allowed in a perk method or extension method.CE0116— a public native variadic (...T) function cannot appear in a.slibpublic API. A native variadic collects its trailing args into a runtimeT[]inside one concrete function, so there is no template to monomorphize at the consumer; analogous to the CE5002 FFI boundary block. This blocks only v1...T(is_variadic); v2 type packs (...Ts) ship as templates and are exportable (see "Cross-library packs" under the Phase-1 section below).CE0120— a bloom argumentarr...used somewhere illegal: into a non-variadic parameter, or not as the sole, last trailing argument at the call site.- Type mismatch when blooming (a non-array source, or an array of the wrong element type) reuses
CE2006. Native call arity/type errors otherwise reuse existingCE2009/CE2006.
Spread / forwarding (bloom)¶
An existing array can be forwarded into a ...T slot with the postfix ... operator, written
directly after the array expression: arr.... The operation is called bloom — the array
"opens" and its elements fan out to fill the trailing call arguments — mirroring Go's f(arr...).
fn sum(...i32 nums) i32:
let i32 total = 0
foreach(n in nums.iter()):
total := total + n
return Result.Ok(total)
fn main() i32:
let i32[] xs = from([1, 2, 3])
let i32 s = sum(xs...).realise(0) # bloom: xs is MOVED into the variadic slot
println("sum = {s}") # sum = 6
return Result.Ok(0)
Semantics:
- Move, not copy. The bloomed array is moved into the callee's synthesized
T[]— the same ownership transfer as a native variadic's own array construction, just skipping the copy. The caller must not use the source array after the call. - Source must be a bare variable.
arr...requiresarrto be aNamereferring to an array-typed local/parameter; blooming an arbitrary expression (a call result, a field access, a literal array) is not supported in v1. - Sole, last trailing argument. A bloom must be the only trailing argument — it cannot be
mixed with individual trailing arguments (
sum(1, xs...)is not a bloom call shape), and it cannot appear anywhere but the variadic slot. Any other placement isCE0120. - Type-checked like any other argument. The element type of the bloomed array must match the
variadic's declared element type; a mismatch (or blooming a non-array value) is
CE2006. - Only into
...T. Blooming into a non-variadic parameter isCE0120— there is no fixed-arity spread.
...T[] combined with bloom is a degenerate case: since a true i32[][] (array of dynamic arrays)
value cannot be constructed in surface syntax, rows... blooming into a ...i32[] parameter is
moot in practice — that variadic form is only reachable via individual array arguments
(total(a, b)), each moved in per element (see Semantics above).
Deferred (additive, not in v1)¶
- Generic variadics (
...Tin a generic function). - Variadics in perk / extension methods (rejected with
CE0115for now). - Public
.slibexport of a native variadic function — blocked for v1 withCE0116(theis_variadicflag is not serialized into the library format yet), analogous to the CE5002 FFI boundary block. - Pack forwarding (
f(pack...), forwarding a parameter pack into another variadic) and pack indexing — bloom covers only a single...Tarray source, not...Tspacks.
Variadic generics / parameter packs (Phase 1, landed)¶
Status: implemented. Distinct from and coexisting with the v1 ...T (homogeneous array sugar) and
extern ... (libc varargs) mechanisms.
Syntax¶
perk Display:
fn display() string
extend i32 with Display:
fn display() string:
return "int:42"
extend string with Display:
fn display() string:
return self
extend bool with Display:
fn display() string:
return "yes"
fn print_all<...Ts: Display>(...Ts args) ~:
expand(a in args):
println(a.display())
return Result.Ok(~)
fn main() i32:
print_all(42, "hi", true) # monomorphizes print_all__i32_string_bool.pack3
print_all() # arity-0 allowed; expand body runs 0 times
return Result.Ok(0)
Semantics¶
...Tsin the type-parameter list declares a type pack. An optional perk constraint (...Ts: Display) requires every bound element type to implement the named perk....Ts argsin the parameter list declares the corresponding value pack. The type-pack must appear last in the type-parameter list and the value pack must appear last in the parameter list.expand(x in pack): BODYis a compile-time-unrolled construct (the static analog offoreach): one copy of BODY is emitted per pack element, eachxbound to that element's concrete type. It is not a runtime loop — no iterator or array is created.??and earlyreturnare allowed insideexpandbodies with correct RAII.- Local variables declared inside
expandare scoped to each unrolled copy (straight-line + nested-block-scope model). - Monomorphization: each distinct (arity, type-tuple) call site produces a separate specialized
function. The mangled symbol uses a
.pack{N}suffix to distinguish pack specializations from regular-generic symbols and to remain collision-free across arities. All specializations uselinkonce_odrlinkage for linker deduplication in multi-unit builds. - Pack elements are passed as separate positional arguments — they are not boxed or collected into an array.
- Arity zero is valid:
print_all()monomorphizes an arity-0 specialization; theexpandbody executes zero times.
Diagnostics¶
- CE0117 — type-pack
...Tsmust be the last type parameter; at most one pack per function. - CE0118 — cannot mix a type-pack
...Tswith a v1 homogeneous...Tin the same function. - CE0119 — malformed
expandstatement (wrong syntax, iterator variable, or target). - CE2090 — a pack element type at the call site does not satisfy the pack's perk constraint.
Phase-1 limitations¶
- Perk-constrained packs only: an unconstrained
...Ts(no: PerkName) can be declared and called, but theexpandbody cannot usefully operate on the elements without a perk (no methods are available). Unconstrained forwarding and pack indexing are deferred. - Cross-library packs (Phase 3, landed): a public
...Tspack ships in a.slibas an instantiable template (templates.generic_functions) and is monomorphized at the consumer's call sites, exactly like a regular cross-library generic. CE0116 still blocks v1 native...Texport (a runtime array, not a template). - Plain function definitions only: perk methods and extension methods may not declare a value
pack (CE0115 applies to both v1
...Tand Phase-1...Ts). - No pack forwarding: a value pack cannot be forwarded into another variadic (
g(pack...)) — bloom (see "Spread / forwarding (bloom)" above) only spreads a single...Tarray, not a...Tspack. - No pack indexing: individual pack elements cannot be addressed by index.
- Same-enum-type element gap: if all pack elements resolve to the same enum type, the instantiation-collection pass may raise CE2061 (a narrow limitation, separate from the perk-constraint mechanism).
Internal representation¶
TypePackinsemantics/generics/types.pyrepresents the variable-length type sequence.monomorphize_functionbuilds a pack-aware substitution map;expand_pack_paramfans a pack-typed value parameter into one concrete parameter per element.- Name mangling:
.pack{N}marker (e.g..pack3for a three-element pack) encodes arity in a collision-free way distinct from regular-generic symbols.
Phase-0 unit tests (test_p0t*) cover the monomorphizer infrastructure; Phase-1 integration tests
(tests/variadic/test_variadic_pack_*.sushi) exercise the full compiler pipeline.