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.
|
the convert into itself.
|
||||||
|
|
||||||
### Tuple Types
|
### 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
|
#### Construction
|
||||||
```sx
|
```sx
|
||||||
@@ -966,7 +966,130 @@ path_join :: (..parts: []string) -> string { ... }
|
|||||||
- A `..` spread at the call site unpacks an existing slice/array into the variadic
|
- A `..` spread at the call site unpacks an existing slice/array into the variadic
|
||||||
tail: `sum(..arr)`.
|
tail: `sum(..arr)`.
|
||||||
- The heterogeneous comptime-pack form `..$args: []Type` binds per-position
|
- 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
|
### Type Inference
|
||||||
- `::` bindings infer type from the right-hand side
|
- `::` 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) -> R // e.g. Closure(s32, s32) -> s32
|
||||||
Closure(param_types) // void return: Closure(s64) -> void
|
Closure(param_types) // void return: Closure(s64) -> void
|
||||||
?Closure(s32) -> s32 // optional closure (null = none)
|
?Closure(s32) -> s32 // optional closure (null = none)
|
||||||
|
Closure(..Ts) -> R // pack-expanded params (see Variadic Heterogeneous Type Packs)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Creating Closures — `closure()` intrinsic
|
#### Creating Closures — `closure()` intrinsic
|
||||||
|
|||||||
Reference in New Issue
Block a user