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:
128
specs.md
128
specs.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user