specs: add Variadic Heterogeneous Type Packs section

Specs the Feature 1 language surface: the three variadic forms
(`[]T` / `..$xs: []Type` / `..xs: Protocol`), the pack-ops table
(`xs.len`, `xs[i]`, `inline for` index + element forms, projection, and
the four spread targets — call args / tuple value / tuple type / closure
sig), position-driven pack projection with the same-name soft warning,
the tuple spread/projection parallels, N=0 semantics, the pack-as-value
diagnostic rule, tuple-based storage + the impl-driven `xx` requirement,
and the canonical Combined/map example. Cross-references from the Tuple
Types and Closure Type sections.
This commit is contained in:
agra
2026-05-29 12:03:51 +03:00
parent 9618f99d0d
commit 4c15fd55bb

128
specs.md
View File

@@ -560,7 +560,7 @@ impl Into(MyBuf) for []u8 { ... }
the convert into itself.
### Tuple Types
Anonymous product types with optional field names. Tuples are first-class values — they can be stored in variables, passed to functions, and returned.
Anonymous product types with optional field names. Tuples are first-class values — they can be stored in variables, passed to functions, and returned. Tuples also support **spread** (`..tuple` / `(..tuple)`) and **field projection** (`tuple.field` across all elements) — see "Variadic Heterogeneous Type Packs".
#### Construction
```sx
@@ -966,7 +966,130 @@ path_join :: (..parts: []string) -> string { ... }
- A `..` spread at the call site unpacks an existing slice/array into the variadic
tail: `sum(..arr)`.
- The heterogeneous comptime-pack form `..$args: []Type` binds per-position
comptime types — see "Variadic heterogeneous type packs" below.
comptime types — see "Variadic Heterogeneous Type Packs" below.
### Variadic Heterogeneous Type Packs
A **pack** is a comptime sequence of per-position-typed arguments. Unlike a
slice variadic (`..xs: []T`, one uniform element type, a runtime slice), a pack
binds a *distinct* type to each position and exists only at compile time. Three
declaration forms exist:
| Form | Element typing | Body view |
|---|---|---|
| `..xs: []T` | uniform `T` | runtime `[]T` slice |
| `..$xs: []Type` | per-position comptime *types* | comptime type list |
| `..xs: Protocol` | per-position — each arg conforms to `Protocol` with its own type-arg | per-position-typed pack |
The third form is the heterogeneous pack. `map :: (mapper: ..., ..sources:
ValueListenable) -> ...` accepts any number of trailing args, each some
`ValueListenable(T)` for a possibly-different `T`.
A pack is **not a runtime value** — it lowers to N typed positional parameters
(zero overhead). The body refers to elements only through the comptime forms
below; using the pack name where a runtime value is required is an error (see
"Pack as value").
#### Pack operations
| Use | Spelling | Meaning |
|---|---|---|
| Length | `xs.len` | comptime int (field-style, not `len(xs)`) |
| Index | `xs[i]` | i-th element; `i` must be comptime |
| Comptime unroll (index) | `inline for i in 0..xs.len { ... }` | unrolled loop; not `#for` |
| Comptime unroll (element) | `inline for x in xs { ... }` | desugars to index form; `x`'s type varies per iteration |
| Projection | `xs.field` | see "Pack projection" |
| Spread → call args | `..xs` / `..xs.field` | expands to N positional args |
| Spread → tuple value | `(..xs)` / `(..xs.field)` | materializes a tuple |
| Spread → tuple type | `(..F(Ts))` / `(..F(Ts.Arg))` | tuple type with per-element type application |
| Spread → callable sig | `Closure(..Ts) -> R` / `Closure(..Ts.Arg) -> R` | positional params of the callable |
#### Pack projection
`xs.field` projects the same member out of every element, preserving order.
Resolution is **position-driven** (no cross-namespace shadowing):
- In **type** position, `..xs.field` looks `field` up in the pack constraint's
**type-arg** namespace. `ValueListenable :: protocol($T: Type) { ... }` declares
type-arg `T`, so `..xs.T` is the pack of element value-types.
- In **value** position, `xs.field` looks `field` up in the constraint's
**runtime-field** namespace and yields a *tuple* of the projected values
(e.g. `xs.value` → `(xs[0].value, xs[1].value, ...)`).
A protocol that declares a type-arg and a runtime field with the **same name**
compiles, but emits a soft warning at the protocol declaration (the human is
alerted; resolution still proceeds by position).
#### Tuple parallels
The same spread/projection syntax applies to a **tuple value** whose source is a
tuple rather than a pack:
- `..tuple` / `..tuple.field` spreads a tuple's fields into call args.
- `tuple.field` projects `field` out of every element (when all elements have a
same-named field), returning a tuple of the projected values.
This lets a pack be materialized once (`stored := (..xs)`) and later re-spread
(`f(..stored)`) or re-projected (`stored.value`).
#### Pack of zero (N = 0)
`xs.len == 0` is valid: `inline for` over an empty range doesn't execute, spreads
are no-ops, and `(..xs)` is the empty tuple. A library built on packs (e.g.
`map`) must handle N=0 — typically by producing a constant result that never
changes.
#### Pack as value
Because a pack has no runtime representation, any expression of pack type in a
value-requiring position is a compile error with a tailored suggestion:
- storing/binding it (`let x = xs;`, `self.f = xs;`) → suggest `(..xs)`;
- passing to a non-pack-taking call (`f(xs)`) → suggest `..xs`;
- returning it (`return xs;`) → suggest a tuple return with `(..xs)`;
- iterating at runtime (`for x in xs`, `xs[runtime_i]`) → suggest `inline for`.
#### Storage and protocol conformance
To **store** a pack, materialize a tuple: a pack-shaped struct field is
tuple-typed, `sources: (..ValueListenable(Ts))`, assigned `self.sources =
(..sources)`. To **return** a struct as a protocol value, `xx` requires an
explicit impl (protocol erasure is impl-driven, not structural) — e.g.
`impl ValueListenable($R) for Combined($R, ..$Ts) { ... }`.
#### Canonical example
```sx
Combined :: struct($R: Type, ..$Ts: []Type) {
sources: (..ValueListenable(Ts)); // pack-spread in tuple type position
mapper: Closure(..Ts) -> $R; // pack-spread in callable sig
value: $R;
own_allocator: Allocator;
recompute :: (self: *Combined) {
new_val := self.mapper(..self.sources.value); // tuple projection + spread
if new_val == self.value return;
self.value = new_val;
}
}
map :: (mapper: Closure(..sources.T) -> $R, ..sources: ValueListenable)
-> ValueListenable($R) {
c := context.allocator.alloc(Combined($R, ..sources.T));
c.own_allocator = context.allocator;
c.mapper = mapper;
c.sources = (..sources); // pack-to-tuple materialization
inline for i in 0..sources.len { // comptime unroll over the pack
sources[i].addListener((_) => c.recompute());
}
c.value = mapper(..sources.value); // pack spread + projection in a call
return xx c; // needs impl ValueListenable for Combined
}
isReady : ValueListenable(bool) = map(
(va, vb, vc) => va and vb > 10 and vc == "cool",
a, b, c); // a,b,c : ValueListenable(bool/s32/string)
```
### Type Inference
- `::` bindings infer type from the right-hand side
@@ -1418,6 +1541,7 @@ A **closure** is a function bundled with captured state. It is represented as a
Closure(param_types) -> R // e.g. Closure(s32, s32) -> s32
Closure(param_types) // void return: Closure(s64) -> void
?Closure(s32) -> s32 // optional closure (null = none)
Closure(..Ts) -> R // pack-expanded params (see Variadic Heterogeneous Type Packs)
```
#### Creating Closures — `closure()` intrinsic