diff --git a/specs.md b/specs.md index 1f8274e..51e028d 100644 --- a/specs.md +++ b/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