Programming Language

Nocter

A self-contained systems language built around simplicity, encapsulation, and foolproof design.

/spec/17-argument-packs-literals-sequence-spread.md

Argument Packs, Literal Definitions, and Sequence Spread

This file is part of the Nocter language specification. The specification entry point is README.md.

Purpose

A callable may accept a statically typed, variable number of final arguments without converting them into a source-level collection:

func total(seed: i32, ...items: i32): i32 {
    var result = seed
    for item in items { result += item }
    return result
}

let answer = total(10, 20, 12)

The same argument-pack contract implements typed sequence literals. One pack model therefore owns arity, inference, ownership, provenance, cleanup, exact-size spread, and native calling convention for functions, methods, construction functions, interface methods, and sequence literals. Argument packs are typed and compiler-owned. They are not C variadics, arrays, slices, Vec<T>, or an erased runtime argument list.

Declaration Form

...name: T declares an argument pack whose element type is T.

pub func log(prefix: &str, ...values: &str): void

instance Logger {
    pub method &self.write(level: Level, ...values: &str): void
}

Rules:

  • A supported callable has at most one pack parameter.
  • The pack is the final declared parameter. Ordinary parameters may precede it.
  • Functions, construction functions, methods, interface methods, and their matching implementations may declare a pack.
  • Primitives, operators, coercions, drop declarations, tests, variants, and string-literal definitions cannot declare a pack.
  • Contract and implementation declarations must agree on the pack position and element type.
  • A pack marker participates in callable-type identity. func(i32): void and func(...i32): void are different structural callable contracts.
  • Closure literals do not declare argument packs in the current language, so a structural callable-value contract containing a pack has no invocable source value yet. Pack invocation in this phase is statically dispatched through named functions, construction functions, methods, and literals.

A sequence-literal definition is the restricted construction form with exactly one pack and no ordinary parameter:

construct Vec<T> {
    pub default literal [](...items: T): Self {
        var result = Self.with_capacity(items.len())
        for item in items { result.push(move item) }
        return move result
    }
}

A string-literal definition retains its one ordinary &str parameter and cannot use a pack.

Calls and Spread

Fixed arguments satisfy the ordinary parameters. Every remaining argument contributes one pack element:

write(header, first, second, third)

A spread contributes the elements produced by an exact-size iterator:

write(header, 0, ...copyable, ...&borrowed, ...move owned, 4)
let values = Vec [0, ...copyable, 4]
  • A fixed expression contributes one owned value of the pack element type.
  • ...source iterates readonly and copies each yielded element; the item type must be Copy.
  • ...&source iterates readonly and contributes the yielded readonly references.
  • ...move source consumes a collection or direct iterator and contributes owned yielded values.
  • ...&+source is rejected because a retained pack could hold overlapping mutable element loans.
  • Spread is invalid when the selected callable has no argument pack.

Each spread uses the expansion and iterator rules in Expansion Operators. The selected iterator must also satisfy the exact-size contract because the total pack length is fixed before the callee begins. The compiler does not fall back to another expansion after selecting a direct iterator that lacks exact size.

An argument-pack parameter is not an ordinary value or a general expansion source. It may appear in one dedicated tail-forwarding form:

func forward(prefix: &str, ...items: &str): void {
    write(prefix, ...items)
}

The forwarded pack must be the only contribution to the destination pack. Fixed destination parameters may precede it, but new pack values or sequence spreads cannot be mixed before or after ...items. Tail forwarding passes the remaining compiler-owned descriptor through unchanged; it does not expose the descriptor as a value, copy it, or build an implicit adapter. A body may tail-forward its pack once and may not also iterate that pack; these rules ensure that the descriptor's cached total length still describes the complete forwarded stream.

Body Surface

A pack is non-escaping and supports three dedicated body uses:

  • items.len() returns the total checked element count cached before body execution.
  • for item in items consumes owned T values once from left to right.
  • target(fixed, ...items) tail-forwards the remaining descriptor under the rule above.

The pack cannot be returned, stored, borrowed, dropped as an ordinary value, or passed as one ordinary argument. A returning tail-forwarding call exhausts the stream: the callee either iterates values or performs its mandatory residual cleanup before returning. The source body may still read items.len() as the immutable original count, but cannot iterate or forward the pack again. Every unconsumed value and iterator suffix retains its normal destruction obligation until that cleanup.

An identifier after from may name a pack. It denotes storage provenance carried by the pack's elements, not the lifetime of the ephemeral descriptor:

func first(...items: &str): &str? from items

The ordinary provenance-elision rules may omit this clause when the pack is the only eligible external origin.

Evaluation, Ownership, and Failure

Evaluation is left to right:

  1. 1. resolve the callable and its complete generic substitution
  2. evaluate a receiver and any explicit allocation override
  3. evaluate fixed arguments, fixed pack elements, and spread sources in source order; tail forwarding evaluates no additional source value
  4. construct every selected spread iterator once
  5. compute one checked total pack length
  6. invoke the body through its ordinary inputs and one hidden pack input

Moving a fixed element or a ...move source follows normal explicit-move rules. A bare spread never guesses that a move-only source should be consumed. Readonly loans created for spread remain active until the call finishes.

Failure propagation, early return, and partial iteration destroy the current element, remaining iterator suffixes, later prepared segments, and completed temporaries exactly once. The caller transfers pack ownership to the callee for the duration of the invocation; the pack cannot escape. Tail forwarding transfers access to the same remaining descriptor for that nested invocation, so residual cleanup remains owned exactly once by the original descriptor.

Native ABI

An argument pack occupies one compiler-owned hidden ABI lane independent of ordinary arguments. The lane carries a descriptor pointer whose closed contract provides:

  • the immutable original total length
  • a callback that yields the next T?
  • a callback that destroys the unconsumed suffix

Fixed parameters retain their normal ABI locations. The pack is never lowered as one ordinary T, a slice, a Vec<T>, or platform variadic arguments. TargetProgram fixes the element type, iterator dispatch, destruction plans, ordinary/pack split, and whether a call creates or forwards the descriptor. MIR transports those facts; MachineProgram and the target backend may not reconstruct semantic dispatch.

Literal Definitions

A nominal type can expose construction from a language-defined literal shape without revealing its representation:

let values = Vec [1, 2, 3]
let text = String "hello"

Literal entries are public construction members. A private implementation of a public bodyless contract omits visibility. Literal construction always executes the selected body, returns Self, and permits at most one member per shape. Same-module attachment prevents orphan definitions.

The current shapes are sequence Type [elements...] and string Type "text". Bare [1, 2, 3] remains a fixed-size array, while bare string syntax remains static &str. Numeric, byte, mapping, tuple-like, and custom delimiter definitions are unsupported.

A generic target may omit all owner arguments when pack elements or the expected result determine them uniquely. Otherwise it uses the complete owner type, such as Vec<i32> []. Partial owner arguments are invalid.

Literal Allocation Context

An allocating literal inherits the current aborting allocation context. A call-site override uses:

let values = Vec [1, 2, 3] using arena

The override is evaluated before pack elements and becomes current only for the literal body. The previous context is restored on success, failure propagation, return, and partial cleanup. Recoverable allocation uses explicit named try_* APIs rather than a second literal-failure form.

Unsupported ... Contexts

Argument packs and call spread do not introduce aggregate-initializer spread, mapping spread, tuple spread, pattern rest capture, struct embedding, mutable spread, untyped variadics, or a source-level pack value type.