Spread
The postfix spread operator expands one source value into the surrounding argument, element or field list.
expression...
Examples:
Call(arguments...)
let combined := [prefix..., 10, 20]
let updated := User {
original...
Name: "Anna"
}
Syntax
Sec spread is postfix:
source...
The spread token follows the expression being expanded.
This is not Sec spread syntax:
...source
Spread Is Contextual Syntax
A spread expression is meaningful only inside a surrounding construct that knows how to receive the expanded values.
Spread therefore does not produce a standalone expression value:
let expanded := values... // invalid
return values... // invalid
values... = other // invalid
The surrounding construct defines:
- what is expanded;
- how many values are required;
- what destination types are expected;
- whether runtime arity is allowed;
- which ownership or borrow rules apply;
- whether the destination operation itself allocates.
Canonical Spread Contexts
Sec 0.1 defines three canonical spread-aware contexts:
- function and method argument lists;
- fixed-array literals;
- struct literals.
A specialized language or library destination may define an additional spread-aware context only when its own canonical contract defines expansion count, ownership, evaluation order and allocation semantics explicitly.
Spread syntax itself does not automatically make every collection, constructor or language construct spread-aware.
What Spread Does Not Mean
Spread does not by itself mean:
- copy;
- move;
- clone;
- allocation;
- iteration;
- concatenation;
- conversion;
- borrowing.
The destination context and Sec's ordinary ownership, copy/move and borrowing rules determine what each expanded use is allowed to do.
This distinction prevents a short convenience syntax from hiding expensive or destructive behavior.
Evaluation Order
The complete surrounding construct is evaluated left to right.
Call(
First(),
values...,
Last(),
)
The semantic order is:
- evaluate
First(); - evaluate
valuesexactly once; - expand the selected elements in their defined order;
- evaluate
Last(); - complete normal call preparation and perform the call.
Backend lowering may optimize this sequence only when observable evaluation order and side effects remain unchanged.
Spread Sources Are Evaluated Exactly Once
The source expression is never re-evaluated once per expanded value.
Call(BuildArguments()...)
BuildArguments() executes once.
The resulting spread source is then expanded.
The natural expansion order is:
- arrays: increasing index order;
- struct fields: declaration order.
Struct override resolution may later determine which supplied field value becomes the final field value, but the source expression itself is still evaluated exactly once.
Function and Method Argument Spread
A fixed-size array may expand into individual call arguments:
fn Add(a: int, b: int, c: int) int {
return a + b + c
}
let values: int[3] := [10, 20, 30]
let result := Add(values...)
For a fixed-arity destination this is semantically equivalent to:
let result := Add(
values[0],
values[1],
values[2],
)
while preserving the exactly-once evaluation of the original spread source.
Ordinary and spread arguments may be mixed:
Call(prefix, values..., suffix)
Multiple spread sources are also allowed when the destination can accept the final expanded argument sequence.
Fixed-Arity Calls Require Compile-Time Expansion Count
For a fixed-arity function or method, every spread contribution must have a compile-time-known number of expanded arguments.
let values: int[3] := [10, 20, 30]
Use(values...)
has known expansion count 3.
A runtime-length sequence cannot satisfy a fixed-arity destination:
let values: ref int[] := ref storage[..]
Use(values...) // invalid for fixed-arity Use
Spread never invents a runtime argument pack for an ordinary fixed-arity function.
Runtime-Arity Destinations
A destination that explicitly supports runtime arity may define how a runtime-length spread source is received.
Native typed variadic parameters are one such destination category.
Spread does not define variadic parameters itself. It merely expands into a destination whose own function rules already define runtime-arity behavior.
Each expanded value must still satisfy the variadic element type and all normal call ownership rules.
Call Resolution Happens After Expansion
Spread expansion is resolved before final argument matching.
After expansion, normal call rules apply:
- total argument count;
- argument order;
- parameter type compatibility;
- ownership requirements;
- borrowing requirements;
- mutability requirements;
- unsafe requirements;
- generic inference;
- overload resolution.
Return type still does not participate in overload selection.
A spread that produces the wrong final arity is therefore rejected by the ordinary call-resolution rules.
Call Spread Is Non-Consuming
Fixed-array argument spread does not define a consuming-spread form.
Expanded indexed uses must be legal under ordinary non-consuming access rules.
For ordinary by-value parameters, this means the relevant array elements must be implicitly copyable.
This is invalid when satisfying the call would require per-element moves:
let resources: Resource[2] := [...]
Consume(resources...) // invalid when elements would need consuming transfer
Spread must not secretly:
- move individual elements out of the array;
- leave the array partially available;
- clone move-only elements;
- allocate a temporary owning collection;
- invent a consuming-spread interpretation.
If the expanded destination parameters borrow, the normal call-borrowing rules are applied after expansion. Spread itself does not create a new borrow mode.
Fixed-Array Literal Spread
A fixed-size array may expand into another fixed-array literal:
let first: int[2] := [1, 2]
let combined := [first..., 3, 4]
The inferred result is:
int[4]
Multiple fixed-array spreads may be mixed with ordinary elements:
let all := [first..., second..., 10]
This remains a fixed-array construction. Spread does not silently convert the literal into a dynamic collection.
Fixed-Array Result Length
The resulting fixed-array length is known at compile time.
It is the checked sum of:
- each ordinary literal element;
- the compile-time length of every spread source.
Length arithmetic overflow is a compile-time error.
A runtime-length source cannot participate:
let values: ref int[] := ref storage[..]
let combined := [values..., 10] // invalid
Sec does not allocate dynamic backing merely to make such a literal work.
Fixed-Array Element Types
Ordinary and expanded elements must satisfy the target element type.
let prefix: byte[2] := [1, 2]
let values: byte[4] := [prefix..., 3, 4]
Target context may shape untyped literals in the same way it does without spread.
Spread does not add structural conversion, common-type inference or implicit collection conversion rules of its own.
Fixed-Array Literal Spread Copies Elements
Fixed-array literal spread is non-consuming.
Expanded elements are copied according to ordinary implicit-copy rules.
Spread must not:
- clone move-only elements;
- move individual elements from the source array;
- leave the source partially available;
- invalidate the source array;
- allocate hidden storage merely to perform expansion.
Sec 0.1 has no consuming fixed-array spread syntax.
Struct Literal Spread
A struct value may provide direct fields to a literal of the same nominal struct type:
type User struct {
Name: string
Age: int
Active: bool
}
let updated := User {
original...
Name: "Anna"
}
This is a convenient same-type update/construction form.
Struct Spread Requires the Same Exact Nominal Type
The spread source must have exactly the target struct type.
Struct spread is not structural typing and does not copy fields merely because two unrelated structs happen to have compatible member names.
This is invalid:
let user := User {
userUpdate...
}
when userUpdate has type UserUpdate rather than
exactly User.
Nominal type identity is preserved.
Struct Spread Is Shallow
Struct spread expands only direct fields.
It does not recursively merge nested structs, maps, collections or payloads.
type Config struct {
Network: NetworkConfig
Logging: LoggingConfig
}
Spreading one Config into another supplies the direct
Network and Logging fields according to ordinary field
semantics.
It does not deep-merge the contents of NetworkConfig or
LoggingConfig.
Struct Entries Resolve Left to Right
A later spread or explicit field entry overrides a field supplied by an earlier spread.
let merged := User {
defaults...
stored...
Name: "Anna"
}
Resolution order is:
defaults
stored overrides defaults
Name overrides both
The ordering is explicit in source and is therefore easy to review.
Two explicit entries with the same field name remain invalid:
let invalid := User {
Name: "Anna"
Name: "Maria"
}
Spread does not legalize duplicate explicit fields.
Defaults and Missing Fields
Struct construction resolves spreads and explicit overrides first.
After that:
- each field has at most one final selected supplied value;
- every omitted Defaultable field receives its semantic default;
- every omitted NonDefaultable field is a compile-time error.
type Settings struct {
Retries: int
Enabled: bool
File: File
}
let settings := Settings {
previous...
Enabled: true
}
If previous supplies File, the construction can satisfy
that NonDefaultable field.
If no final entry supplies File and File is
NonDefaultable, construction is invalid.
Spread Does Not Bypass Visibility or Invariants
Struct spread follows ordinary struct construction rules.
It cannot bypass:
- inaccessible fields;
- nominal type identity;
- construction invariants;
- field type contracts;
- ownership restrictions.
If the target struct cannot legally be constructed from the current source location, spread does not make that representation accessible.
Struct Spread Is Non-Consuming
Struct spread requires an implicitly copyable source struct.
let updated := User {
original...
Name: "Anna"
}
The selected source fields are supplied using ordinary copy semantics.
A move-only source struct cannot be spread:
let updated := Session {
originalSession... // invalid when Session is move-only
}
Struct spread must not:
- partially move the source struct;
- hide explicit move syntax;
- invoke a hidden clone;
- allocate merely to perform expansion.
If ownership transfer is required for a particular explicit field, write that field using the ordinary copy/move rules instead of expecting spread to consume it.
Spread Does Not Replace Copy/Move Syntax
Spread describes expansion and placement. The Copying and Moving chapter still owns destructive transfer semantics.
The canonical restrictions are:
| Context | Ownership rule |
|---|---|
| Fixed-array call spread | Expanded uses must be valid without hidden per-element consumption |
| Fixed-array literal spread | Expanded source elements must be implicitly copyable |
| Struct literal spread | The source struct must be implicitly copyable |
Spread never changes from copy behavior to move behavior simply because a concrete type is move-only.
There is no hidden “consuming spread” interpretation in Sec 0.1.
Borrowing and Spread
Spread does not invent a new implicit borrow operation.
For call arguments, expansion happens first and the normal function-call borrowing rules then apply to each resulting argument position.
For example, a destination parameter declared ref T may receive the
usual call-bounded borrow when the expanded element Place satisfies the ordinary
borrowing rules.
Spread itself does not:
- extend borrow lifetime;
- upgrade shared authority to mutable authority;
- make overlapping mutable borrows legal;
- grant ownership through a reference;
- turn runtime sequence iteration into an argument pack.
Spread Does Not Allocate by Itself
Spread has no inherent allocation semantics.
The compiler must not create temporary dynamic storage merely to implement spread.
Typical lowering can remain direct:
- fixed-array call spread → ordinary argument operations;
- fixed-array literal spread → direct fixed-element construction;
- struct spread → explicit selected field operations.
If a specialized spread-aware destination allocates by its own contract, that allocation belongs to the destination operation, not to spread.
Invalid Spread Sources
Unless another canonical destination explicitly allows them, ordinary spread is invalid for source categories such as:
- scalar values;
- strings;
- maps;
- sets;
- interfaces;
- unions;
- raw pointers;
- registers;
- move-only fixed arrays when element expansion would require consuming reads;
- move-only structs for struct spread;
- values whose expansion count cannot satisfy the destination context.
Slices and owning dynamic arrays are not valid sources for fixed-array literals or fixed-arity calls because their length is runtime state.
Call(10...) // invalid
let values := [slice..., 10] // invalid fixed-array literal spread
let user := User {
rawPointer... // invalid
}
Invalid Spread Contexts
Spread is not an ordinary postfix operator that can appear anywhere an expression can.
Invalid examples include:
let value := source...
return source...
if source... {
...
}
source... = replacement
A parser may recognize the token sequence for recovery, but semantic spread exists only inside a spread-aware destination.
There Is No Prefix Spread
This is invalid:
...values
The canonical Sec operator is always postfix:
values...
Repeated spread markers are also invalid:
values......
Multiple Spreads
Multiple spread sources are allowed when the surrounding context can resolve the complete result.
Call example:
Call(prefix..., middle, suffix...)
Fixed-array example:
let combined := [first..., second..., 10]
Struct example:
let merged := User {
defaults...
stored...
Name: "Anna"
}
Every spread source is evaluated exactly once at its left-to-right source position.
Trailing Commas
Where the surrounding list syntax permits a trailing comma, spread entries may appear before that trailing comma.
Call(
values...,
)
Likewise for array literals:
let combined := [
prefix...,
10,
]
The comma belongs to the surrounding list grammar. It is not part of the spread operator itself.
Spread Is Resolved Before Backend Lowering
Semantic analysis resolves the complete spread meaning before MLIR/LLVM lowering.
The compiler must preserve enough information to guarantee:
- source evaluation exactly once;
- destination context;
- expansion order;
- expansion count when statically required;
- selected ownership operation for each expanded use;
- struct override resolution;
- observable left-to-right evaluation order.
The implementation may keep a dedicated spread representation until normalization, or normalize early into ordinary arguments, elements and field operations once all required semantic facts are explicit.
Low-level lowering must never reconstruct ownership or evaluation semantics merely by seeing a textual spread pattern.
Diagnostics
Spread diagnostics should identify the actual failed requirement:
- unsupported spread context;
- invalid source type;
- runtime expansion count where compile-time arity is required;
- wrong call arity after expansion;
- incompatible expanded argument or element;
- move-only source restriction;
- inaccessible struct fields;
- wrong nominal struct type;
- duplicate explicit struct fields;
- omitted NonDefaultable fields after spread/default resolution;
- fixed-array result-length overflow.
Unknown fixed-call expansion count
cannot spread `ref int[]` into this fixed-arity call
the number of expanded arguments is not known at compile time
help: use a fixed-size array
help: use a destination that explicitly supports runtime arity
Move-only fixed-array elements
cannot spread `Resource[2]` into these by-value arguments
the expanded elements would require destructive moves from the source array
spread is non-consuming
help: pass the elements explicitly with the required ownership syntax
help: redesign the destination to borrow when ownership transfer is not needed
Wrong struct source type
cannot spread `UserUpdate` into `User`
struct spread requires the same exact nominal struct type
help: provide `User` as the spread source
help: initialize the fields explicitly
Standalone spread
spread is not valid as a standalone expression
`values...` must appear inside a spread-aware argument, array-element or struct-field list
Best Practices
- Use spread when it makes an already well-defined fixed expansion easier to read.
- Use postfix
source...; Sec has no prefix spread. - Use fixed arrays when a fixed-arity call or fixed-array literal needs compile-time-known expansion count.
- Do not use spread when the important operation is ownership transfer; write the transfer explicitly.
- Do not expect spread to clone move-only resources.
- Keep struct overrides close to the spread they override.
- Use struct spread only for same-type shallow updates.
- Do not stack struct spreads merely to simulate deep merge semantics.
- Prefer explicit code when runtime sequence length is central to understanding the operation.
- Remember that allocation belongs to the destination operation, never to spread itself.
Summary
- Sec spread uses postfix
expression.... - Prefix
...expressionis invalid. - Spread is contextual syntax and does not produce a standalone value.
- Canonical Sec 0.1 spread contexts are call arguments, fixed-array literals and struct literals.
- A specialized destination may define another spread-aware context only through its own canonical contract.
- Spread does not inherently mean copy, move, clone, allocation, iteration, concatenation, conversion or borrowing.
- The spread source expression is evaluated exactly once.
- The surrounding construct preserves left-to-right evaluation order.
- Fixed-array elements expand in increasing index order.
- Struct fields expand in declaration order.
- A fixed-size array may expand into individual function or method arguments.
- A fixed-arity call requires every spread contribution to have compile-time-known expansion count.
- A runtime-length slice or dynamic array cannot spread into an ordinary fixed-arity call.
- A destination that explicitly supports runtime arity may define runtime-length spread behavior.
- Native typed variadic parameters are an explicit runtime-arity destination category.
- Expansion occurs before final argument matching and overload resolution.
- Normal argument count, type, ownership, borrowing, mutability, unsafe and generic rules apply after expansion.
- Fixed-array call spread is non-consuming.
- Spread cannot hide per-element moves or partial array ownership.
- By-value expanded array elements must be copyable when the destination requires ordinary by-value values.
- Normal call-borrowing rules may apply after expansion when the destination parameters borrow.
- A fixed-size array may spread into another fixed-array literal.
- Fixed-array result length is computed at compile time from ordinary elements and spread-source lengths.
- Fixed-array length overflow is a compile-time error.
- A runtime-length source cannot spread into a fixed-array literal.
- Fixed-array spread does not silently create a dynamic array or allocate hidden backing storage.
- All expanded array elements must satisfy the target element type.
- Fixed-array literal spread copies source elements according to ordinary implicit-copy rules.
- Sec 0.1 has no consuming fixed-array spread form.
- Struct spread requires the same exact nominal target struct type.
- Struct spread is shallow.
- Struct spread does not recursively merge nested values.
- Struct literal entries resolve from left to right.
- A later spread may override fields supplied by an earlier spread.
- An explicit field may override a field supplied by an earlier spread.
- Two explicit entries for the same field remain invalid.
- Omitted-field defaults are applied only after spread and explicit override resolution.
- An omitted NonDefaultable field remains a compile-time error unless some final spread/entry supplies it.
- Struct spread does not bypass field visibility, nominal identity, construction invariants or field contracts.
- Struct spread is non-consuming and requires an implicitly copyable source struct.
- A move-only struct cannot be spread through ordinary struct spread.
- Spread never silently changes from copy semantics to move semantics for a concrete type.
- Spread never invokes a hidden clone.
- Spread does not allocate by itself.
- Temporary dynamic storage must not be created merely to implement spread.
- Scalars, strings, maps, sets, interfaces, unions, raw pointers and registers are not ordinary spread sources.
- Slices and dynamic arrays are invalid fixed-array-literal or fixed-arity-call spread sources because their length is runtime state.
- Repeated spread markers and standalone spread are invalid.
- Multiple spreads may appear when the surrounding destination can resolve the complete expansion.
- Trailing commas belong to the surrounding list syntax and may follow spread entries where that grammar allows them.
- Spread semantics are fully resolved before low-level backend lowering.
- Backend lowering must preserve exactly-once evaluation, expansion order, ownership semantics and observable evaluation order.