docs: tuple syntax cutover — Tuple(...) type, .(...) value, channel-outside-Tuple failables

Rewrite specs.md tuple/failable/pack/UFCS/grammar sections to the new
syntax, update readme.md, and refresh stale tuple references in example
header comments. Also fixes two pre-existing doc inaccuracies surfaced in
review: drop the value-discarding `;` in the tuple-return examples, and
correct the §13 function-type grammar production (optional param list +
optional trailing `!` channel). Optional semantics unchanged.

current/CHECKPOINT-LANG.md logs the cutover.
This commit is contained in:
agra
2026-06-25 18:41:22 +03:00
parent 1dfc22794e
commit 40b5fb5f7e
17 changed files with 184 additions and 103 deletions

View File

@@ -0,0 +1,51 @@
# CHECKPOINT-LANG — user-facing language features
Companion to [PLAN-LANG.md](PLAN-LANG.md). Update after every step (one step at
a time, per the cadence rule).
## Last completed step
**Tuple syntax cutover — `Tuple(...)` type + `.(...)` value (commit 989e18b7).**
The bare-paren tuple grammar was replaced with explicit, position-unambiguous
forms that mirror how structs work:
- type `(A, B)``Tuple(A, B)` (named keeps `:``Tuple(x: A, y: B)`)
- value `(a, b)``.(a, b)` (named uses `=``.(x = a, y = b)`)
- typed (new) → `Tuple(A, B).(a, b)` (like `Point.{...}`)
- failable `-> (T, !)``-> T !`
`-> (T1, T2, !)``-> Tuple(T1, T2) !` (error channel OUTSIDE the Tuple)
Bare `(...)` is now grouping ONLY, everywhere; a comma in bare parens is a hard
error with a migration hint. Grouping, function types `(A, B) -> R`, param lists,
lambdas, match bindings, and `?(?T)` grouping are unaffected. `Tuple(...)` is
strictly a TYPE in every position (incl. `size_of` / `type_info` args); a tuple
VALUE comes only from `.(...)` or `Tuple(...).(...)`. A bare `Tuple(1, 2)`
(non-type elements) is rejected. Field access is unchanged (`.0`/`.1` positional,
`.x` named). Optional semantics are untouched — `??T ≡ ?T` was NOT done; nested
optionals (`?(?i64)`) stay genuine.
The ~110 tuple-bearing corpus files were migrated by a one-shot AST-aware
migrator; new examples landed (0130 new syntax, 0131 typed construction, 1060
named-tuple failable return). Issue **0189** filed (non-type expression in type
position silently fabricates an empty struct — surfaced while validating the
`Tuple(i32, g.a)` rejection path).
Docs updated to the new syntax: `specs.md` (Tuple Types section, function
multi-return note, all error-channel sections, Variadic Heterogeneous Type Packs,
Tuple UFCS Splatting, and the normative Grammar block) and `readme.md` (inline-asm
named-tuple return + the `N → a tuple` rule). Stale old-syntax mentions in example
header comments were corrected (comments only — no code touched). Suite green
(810 ran, 0 failed).
## Current state
Tuple syntax cutover shipped and documented. `Tuple(...)` / `.(...)` are the only
tuple spellings across the corpus, specs, and readme.
## Next step
Pick up the next incomplete LANG step from [PLAN-LANG.md](PLAN-LANG.md).
## Log
- **Tuple syntax cutover** (commit 989e18b7): `(A,B)`/`(a,b)` tuples replaced by
`Tuple(A,B)` type + `.(a,b)` value; failable `!` moved outside the Tuple
(`-> T !` / `-> Tuple(...) !`); bare parens are grouping-only. Docs (specs.md +
readme.md) and stale example-comment mentions migrated to the new syntax. Issue
0189 filed. Suite green (810 ran, 0 failed).

View File

@@ -35,7 +35,7 @@ main :: () {
print("{}\n", a); // 2 print("{}\n", a); // 2
print("{}\n", b); // 1 print("{}\n", b); // 1
wrap :: (x: i64) -> Tuple(i64) { .(x) } // 1-tuple needs trailing comma; (i64) groups wrap :: (x: i64) -> Tuple(i64) { .(x) } // 1-tuple type `Tuple(i64)`; bare `(i64)` groups
t := wrap(99); t := wrap(99);
print("{}\n", t.0); // 99 print("{}\n", t.0); // 99
} }

View File

@@ -3,7 +3,7 @@
// tuple here). Tuples are POSITIONAL, so `TupleInfo` is just a `[]Type` (no field // tuple here). Tuples are POSITIONAL, so `TupleInfo` is just a `[]Type` (no field
// names). Two paths: // names). Two paths:
// 1. Programmatic build: `define(declare("Pair"), .tuple(.{ elements = … }))`. // 1. Programmatic build: `define(declare("Pair"), .tuple(.{ elements = … }))`.
// 2. Round-trip: `define(declare("TripleCopy"), type_info((i64, bool, f64)))` // 2. Round-trip: `define(declare("TripleCopy"), type_info(Tuple(i64, bool, f64)))`
// reflects a source tuple type INTO a `.tuple(TupleInfo)` value and // reflects a source tuple type INTO a `.tuple(TupleInfo)` value and
// reconstructs it — no literal element list. // reconstructs it — no literal element list.
#import "modules/std.sx"; #import "modules/std.sx";

View File

@@ -1,6 +1,6 @@
// A tuple literal used in a type position (`(i32, i32)` reinterpreted as a tuple // A tuple type (`Tuple(i32, i32)` at a type-demanding site like `size_of`) must
// type at a type-demanding site like `size_of`) must list only types. A non-type // list only types. A non-type
// element — here the `1` in `(i32, 1)` — is rejected with a user-facing // element — here the `1` in `Tuple(i32, 1)` — is rejected with a user-facing
// diagnostic instead of silently fabricating an `i64` field for that slot. // diagnostic instead of silently fabricating an `i64` field for that slot.
// Regression (issue 0067). // Regression (issue 0067).
// Expected: a clean "tuple type element is not a type" error at the `1`; exit 1. // Expected: a clean "tuple type element is not a type" error at the `1`; exit 1.

View File

@@ -1,5 +1,5 @@
// Feature 1 — materialize a tuple from a pack via `(..xs.method)` (Decision 2: // Feature 1 — materialize a tuple from a pack via `.(..xs.method)` (Decision 2:
// a pack is stored by materializing a tuple). `(..xs.get)` projects `get` over // a pack is stored by materializing a tuple). `.(..xs.get)` projects `get` over
// the pack and collects the results into a real tuple value, which can then be // the pack and collects the results into a real tuple value, which can then be
// stored, indexed, and (for `Box(T)`) is heterogeneous per position. // stored, indexed, and (for `Box(T)`) is heterogeneous per position.

View File

@@ -1,6 +1,6 @@
// Feature 1 — TYPE-position pack projection `xs.T`. The per-element protocol // Feature 1 — TYPE-position pack projection `xs.T`. The per-element protocol
// type-arg `T` projects into a Pack of types, usable in type/signature // type-arg `T` projects into a Pack of types, usable in type/signature
// positions: a tuple type `(..xs.T)` and a closure signature // positions: a tuple type `Tuple(..xs.T)` and a closure signature
// `Closure(..xs.T) -> R`. (`T` of each element comes from its // `Closure(..xs.T) -> R`. (`T` of each element comes from its
// `impl Box(T) for <elem>`.) // `impl Box(T) for <elem>`.)
@@ -17,8 +17,8 @@ impl Box(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
impl Box(string) for StrCell { get :: (self: *StrCell) -> string => self.s; } impl Box(string) for StrCell { get :: (self: *StrCell) -> string => self.s; }
impl Box(i64) for Dbl { get :: (self: *Dbl) -> i64 => self.n * 2; } impl Box(i64) for Dbl { get :: (self: *Dbl) -> i64 => self.n * 2; }
// Tuple type `(..xs.T)` — heterogeneous (i64, string), matched by the // Tuple type `Tuple(..xs.T)` — heterogeneous (i64, string), matched by the
// value-projection `(..xs.get)`. // value-projection `.(..xs.get)`.
snap :: (..xs: Box) -> void { snap :: (..xs: Box) -> void {
t : Tuple(..xs.T) = .(..xs.get); t : Tuple(..xs.T) = .(..xs.get);
print("0={} 1={}\n", t.0, t.1); print("0={} 1={}\n", t.0, t.1);

View File

@@ -1,8 +1,8 @@
// Phase 4.2 — the canonical `Combined` struct's storage layer: a generic // Phase 4.2 — the canonical `Combined` struct's storage layer: a generic
// struct whose field is a pack of PARAMETERIZED-protocol values, // struct whose field is a pack of PARAMETERIZED-protocol values,
// `sources: (..VL(Ts))` → `(VL(T0), VL(T1), …)`. Each `VL(Ti)` is a real // `sources: Tuple(..VL(Ts))` → `Tuple(VL(T0), VL(T1), …)`. Each `VL(Ti)` is a real
// 16-byte protocol value (issue: parameterized-protocol value types), and // 16-byte protocol value (issue: parameterized-protocol value types), and
// `(..VL(Ts))` applies `VL` per pack element. Instantiate + whole-tuple store // `Tuple(..VL(Ts))` applies `VL` per pack element. Instantiate + whole-tuple store
// of `xx`-erased values + per-element method dispatch all work. // of `xx`-erased values + per-element method dispatch all work.
#import "modules/std.sx"; #import "modules/std.sx";

View File

@@ -1,7 +1,7 @@
// Phase 6 — `c.sources = (..sources)`: materialize a pack into a // Phase 6 — `c.sources = .(..sources)`: materialize a pack into a
// protocol-typed tuple field, erasing each concrete pack element to the field's // protocol-typed tuple field, erasing each concrete pack element to the field's
// protocol slot. The pack `..sources: VL` holds concrete cells; `(..sources)` // protocol slot. The pack `..sources: VL` holds concrete cells; `.(..sources)`
// into a `(..VL(Ts))` field `xx`-erases each to its `VL(Ti)` value. // into a `Tuple(..VL(Ts))` field `xx`-erases each to its `VL(Ti)` value.
#import "modules/std.sx"; #import "modules/std.sx";

View File

@@ -5,7 +5,7 @@
// - `$R` is inferred at the call site from the lowered mapper's closure ret, // - `$R` is inferred at the call site from the lowered mapper's closure ret,
// bound into the mono (`-> VL($R)` ⇒ `VL(i64)`, `Combined($R, ..)` ⇒ // bound into the mono (`-> VL($R)` ⇒ `VL(i64)`, `Combined($R, ..)` ⇒
// `Combined(i64, ..)`), and folded into the mangle. // `Combined(i64, ..)`), and folded into the mangle.
// - `(..sources)` materializes the pack into the `(..VL(Ts))` field (per-element // - `.(..sources)` materializes the pack into the `Tuple(..VL(Ts))` field (per-element
// erase) and `mapper(..sources.get)` projects+spreads; `xx c` erases the // erase) and `mapper(..sources.get)` projects+spreads; `xx c` erases the
// generic-struct instance to `VL(i64)` via the generic impl's monomorphized // generic-struct instance to `VL(i64)` via the generic impl's monomorphized
// thunk. // thunk.

View File

@@ -6,12 +6,12 @@
// arrive in Phase 2 — do NOT expect this to compile/run yet. The authoritative // arrive in Phase 2 — do NOT expect this to compile/run yet. The authoritative
// checks are the parser unit tests in src/parser.zig ("parse pack expansion: …"). // checks are the parser unit tests in src/parser.zig ("parse pack expansion: …").
// 1. Tuple value position — `(..pack)` / `(..pack.field)`: // 1. Tuple value position — `.(..pack)` / `.(..pack.field)`:
tv1 :: () => .(..xs); tv1 :: () => .(..xs);
tv2 :: () => .(..xs.value); tv2 :: () => .(..xs.value);
tv3 :: () => .(a, ..xs, b); // mixed positional + spread tv3 :: () => .(a, ..xs, b); // mixed positional + spread
// 2. Tuple type position — `(..F(Ts))` / `(..F(Ts.Arg))`: // 2. Tuple type position — `Tuple(..F(Ts))` / `Tuple(..F(Ts.Arg))`:
tt1 :: (x: Tuple(..ValueListenable(Ts))) => x; tt1 :: (x: Tuple(..ValueListenable(Ts))) => x;
tt2 :: (x: Tuple(..ValueListenable(Ts.Arg))) => x; tt2 :: (x: Tuple(..ValueListenable(Ts.Arg))) => x;

View File

@@ -46,17 +46,17 @@ main :: () -> i32 {
// ── GAPS (Feature 1 work — intentionally NOT exercised above) ────── // ── GAPS (Feature 1 work — intentionally NOT exercised above) ──────
// //
// G1. Tuple field projection across elements: // G1. Tuple field projection across elements:
// t := (Listenable.{value=1}, Listenable.{value=2}); // t := .(Listenable.{value=1}, Listenable.{value=2});
// v := t.value; // expected: (1, 2) — Decision 3 "tuple.field" // v := t.value; // expected: .(1, 2) — Decision 3 "tuple.field"
// Today: `error: field 'value' not found on type 'tuple'`. // Today: `error: field 'value' not found on type 'tuple'`.
// Needed by canonical `self.sources.value`. // Needed by canonical `self.sources.value`.
// //
// G2. Tuple spread into call args: // G2. Tuple spread into call args:
// p := (10, 20); // p := .(10, 20);
// add(..p); // expected: add(10, 20) — Decision 3 "..tuple" // add(..p); // expected: add(10, 20) — Decision 3 "..tuple"
// Today: lowers to one `undef` arg → LLVM arity verification failure. // Today: lowers to one `undef` arg → LLVM arity verification failure.
// Needed by canonical `mapper(..sources.value)` and `(..sources)`. // Needed by canonical `mapper(..sources.value)` and `.(..sources)`.
// //
// Both are already scheduled: parsing in Phase 1.2 (PackExpansion node covers // Both are already scheduled: parsing in Phase 1.2 (PackExpansion node covers
// `(..pack)` / `..pack.field`), sema in Phase 2.3 ("tuple-spread parallels"). // `.(..pack)` / `..pack.field`), sema in Phase 2.3 ("tuple-spread parallels").
// No separate Feature 1.5 needed — see Step 0.4 triage in CHECKPOINT-LANG.md. // No separate Feature 1.5 needed — see Step 0.4 triage in CHECKPOINT-LANG.md.

View File

@@ -10,7 +10,7 @@
// - wrapper-alias element `BoxPtr :: *Box` (engine wrapper aliasing) // - wrapper-alias element `BoxPtr :: *Box` (engine wrapper aliasing)
// - union body-builder child `WrapU :: union { b: Box }` (C1, registerUnionDecl) // - union body-builder child `WrapU :: union { b: Box }` (C1, registerUnionDecl)
// - enum body-builder child `WrapE :: enum { V: Box }` (C1, registerEnumDecl) // - enum body-builder child `WrapE :: enum { V: Box }` (C1, registerEnumDecl)
// - tuple-literal element `size_of((Box, i32))` (O1/K5) // - tuple-type element `size_of(Tuple(Box, i32))` (O1/K5)
// - inline-anonymous body child `x : union { b: Box }` (inline-anon engine arm) // - inline-anonymous body child `x : union { b: Box }` (inline-anon engine arm)
// //
// Fail-before (pre-E6b-R): each of these resolved `Box` through the stateless // Fail-before (pre-E6b-R): each of these resolved `Box` through the stateless

View File

@@ -1,6 +1,6 @@
// Compound type literals in expression position — `size_of` / // Compound type literals in expression position — `size_of` /
// `align_of` accept pointer (`*T`), optional (`?T`), array (`[N]T`), // `align_of` accept pointer (`*T`), optional (`?T`), array (`[N]T`),
// function (`(A) -> B`), and tuple (`(A, B)`) types directly. Also // function (`(A) -> B`), and tuple (`Tuple(A, B)`) types directly. Also
// const-decl RHS aliases through the same forms (`Ptr :: *u8;` etc). // const-decl RHS aliases through the same forms (`Ptr :: *u8;` etc).
// Same shape as the existing `size_of(i32)` baseline path. // Same shape as the existing `size_of(i32)` baseline path.
@@ -22,7 +22,7 @@ main :: () -> i32 {
// Function-type literal in expression position. // Function-type literal in expression position.
print("size_of((i32)->i32) = {}\n", size_of((i32) -> i32)); print("size_of((i32)->i32) = {}\n", size_of((i32) -> i32));
// Tuple literal reinterpreted as tuple type at the type-demanding site. // Tuple type at the type-demanding site.
print("size_of((i32, i32)) = {}\n", size_of(Tuple(i32, i32))); print("size_of((i32, i32)) = {}\n", size_of(Tuple(i32, i32)));
// Aliases. // Aliases.

View File

@@ -2,7 +2,7 @@
// - `t.0 = v` writes one element in place (was a known gap: the lvalue path // - `t.0 = v` writes one element in place (was a known gap: the lvalue path
// looked the element up by name via getStructFields and left the pointee // looked the element up by name via getStructFields and left the pointee
// `.unresolved`; now it indexes the tuple positionally like the read path). // `.unresolved`; now it indexes the tuple positionally like the read path).
// - Named tuples `(x: T, y: U)` keep their field names through parsing and // - Named tuples `Tuple(x: T, y: U)` keep their field names through parsing and
// type resolution, so `t.x` reads/writes by name (and `.0` by position). // type resolution, so `t.x` reads/writes by name (and `.0` by position).
#import "modules/std.sx"; #import "modules/std.sx";

View File

@@ -1,12 +1,12 @@
// Parenthesized type grouping: in type position `(T)` (single element, no // Parenthesized type grouping: bare parens `(T)` are GROUPING ONLY (in every
// trailing comma) is a GROUPING that resolves to the inner type — mirroring // position) and resolve to the inner type. A tuple type is `Tuple(...)`:
// value position where `(expr)` groups and `(expr,)` is a 1-tuple. A 1-tuple // `Tuple(T)` is a 1-tuple, `Tuple(A, B)` a 2-tuple. Bare parens never form a
// type requires the trailing comma `(T,)`; `(A, B)` is a 2-tuple. // tuple.
// //
// This lets a closure/optional type be parenthesized for readability: // This lets a closure/optional type be parenthesized for readability:
// [1](Closure(i64,i64) -> i64) // array of closures (grouped element type) // [1](Closure(i64,i64) -> i64) // array of closures (grouped element type)
// ?(?i64) // nested optional // ?(?i64) // nested optional
// without the parens silently turning it into a 1-tuple. // without ambiguity — grouping is decoupled from the tuple grammar.
#import "modules/std.sx"; #import "modules/std.sx";
@@ -28,7 +28,7 @@ main :: () {
fns : [1](Closure(i64, i64) -> i64) = .[ add ]; fns : [1](Closure(i64, i64) -> i64) = .[ add ];
print("{}\n", fns[0](3, 4)); // 7 print("{}\n", fns[0](3, 4)); // 7
// A 1-tuple type still requires the trailing comma. // A 1-tuple type is `Tuple(i64)`.
one : Tuple(i64) = .(9); one : Tuple(i64) = .(9);
print("{}\n", one.0); // 9 print("{}\n", one.0); // 9

View File

@@ -498,11 +498,11 @@ operands or to give a value a name distinct from its register. A label that just
echoes its register (`[rax] "={rax}"`) is rejected. echoes its register (`[rax] "={rax}"`) is rejected.
Outputs decide the result: **0** → `void` (and the asm must be `volatile`); Outputs decide the result: **0** → `void` (and the asm must be `volatile`);
**1** → that type; **N** → a tuple, named by each operand's name. **1** → that type; **N** → a `Tuple`, named by each operand's name.
```sx ```sx
// multiple value outputs → a destructurable tuple // multiple value outputs → a destructurable tuple
split :: (x: u64) -> (lo: u64, hi: u64) { split :: (x: u64) -> Tuple(lo: u64, hi: u64) {
return asm { return asm {
#string ASM #string ASM
and %[l], %[x], #0xff and %[l], %[x], #0xff

166
specs.md
View File

@@ -827,35 +827,54 @@ 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. Tuples also support **spread** (`..tuple` / `(..tuple)`) and **field projection** (`tuple.field` across all elements) — see "Variadic Heterogeneous Type Packs". Anonymous product types with optional field names. Tuples are first-class values — they can be stored in variables, passed to functions, and returned. A **named tuple** `Tuple(x: A, y: B)` is sx's anonymous *structural* record — it carries field names but has no nominal identity (distinct from a `struct`, which is nominal). Tuples also support **spread** (`..tuple` / `.(..tuple)`) and **field projection** (`tuple.field` across all elements) — see "Variadic Heterogeneous Type Packs".
The tuple TYPE is always written `Tuple(...)`; a tuple VALUE is always written
`.(...)` (mirroring how a struct type `Point` pairs with a value literal
`Point.{...}` / `.{...}`). Bare parentheses `(...)` are **grouping only,
everywhere** — a comma inside bare parens is a hard error with a migration hint.
#### Construction #### Construction
```sx ```sx
pair := (40, 2); // positional tuple: (i64, i64) pair := .(40, 2); // positional tuple value: Tuple(i64, i64)
named := (x: 10, y: 20); // named tuple: (x: i64, y: i64) named := .(x = 10, y = 20); // named tuple value: Tuple(x: i64, y: i64)
single := (42,); // 1-tuple (trailing comma in value position) single := .(42); // 1-tuple value
zeroed : (i32, i32) = ---; // zero-initialized tuple empty := .(); // empty tuple value
zeroed : Tuple(i32, i32) = ---; // zero-initialized tuple
// Explicitly typed value (like `Point.{...}`):
p := Tuple(i64, i64).(40, 2);
n := Tuple(x: i64, y: i64).(x = 10, y = 20);
``` ```
Note: In value position, `(expr)` without a comma is a grouping expression, not a tuple. Use `(expr,)` for a 1-tuple value. A named tuple value uses `=` for its fields (`.(x = a, y = b)`); a named tuple
type keeps `:` (`Tuple(x: A, y: B)`).
#### Type Syntax #### Type Syntax
In type position, parentheses mirror value position: `(T)` (a single element, no The tuple type is `Tuple(...)`:
trailing comma) is a **grouping** that resolves to the inner type `T`, while
`(T,)` (trailing comma) is a 1-tuple. `(A, B)` is a 2-tuple. The `->` arrow
disambiguates function types from grouped/tuple types:
```sx ```sx
(i64) // grouping: resolves to i64 (NOT a tuple) Tuple(i64) // 1-tuple type
(i64,) // tuple type with one field Tuple(i64, i64) // 2-tuple type
(i64, i64) // tuple type with two fields Tuple(x: i64, y: i64) // named tuple type
Tuple() // empty tuple type
Tuple(..F(Ts)) // pack-spread tuple type (see Variadic Heterogeneous Type Packs)
```
`Tuple(...)` is strictly a **type** in every position — including `size_of(Tuple(...))`
and `type_info(...)` arguments. A tuple **value** comes only from `.(...)` (anonymous)
or `Tuple(...).(...)` (explicitly typed); a bare `Tuple(1, 2)` (non-type elements) is
rejected as a tuple type with non-type elements.
Bare parentheses are grouping and never a tuple:
```sx
(i64) // grouping: resolves to i64
(i64) -> i64 // function type: takes i64, returns i64 (i64) -> i64 // function type: takes i64, returns i64
(i64, i64) -> i64 // function type: takes two i64, returns i64 (i64, i64) -> i64 // function type: takes two i64, returns i64
?(?i64) // grouping → a genuine nested optional ?(?i64) // grouping → a genuine nested optional
[1](Closure(i64,i64) -> i64) // grouping → array of one closure [1](Closure(i64,i64) -> i64) // grouping → array of one closure
``` ```
Grouping lets a closure/optional/function type be parenthesized for readability Grouping lets a closure/optional/function type be parenthesized for readability.
without silently becoming a 1-tuple. A named single element `(x: T)` stays a Function types `(A, B) -> R`, parameter lists, lambdas, and `match` bindings keep
(named) tuple. using bare parens — they are unaffected by the tuple grammar.
#### Field Access #### Field Access
```sx ```sx
@@ -867,49 +886,49 @@ named.0; // 10 — numeric index also works on named tuples
#### As Return Type #### As Return Type
```sx ```sx
swap :: (a: i64, b: i64) -> (i64, i64) { (b, a); } swap :: (a: i64, b: i64) -> Tuple(i64, i64) { .(b, a) }
wrap :: (x: i64) -> (i64,) { (x,); } // 1-tuple return needs the trailing comma wrap :: (x: i64) -> Tuple(i64) { .(x) } // 1-tuple return
s := swap(1, 2); // s.0 = 2, s.1 = 1 s := swap(1, 2); // s.0 = 2, s.1 = 1
t := wrap(42); // t.0 = 42 t := wrap(42); // t.0 = 42
``` ```
#### Representation #### Representation
Tuples are represented as anonymous LLVM struct types (same layout as named structs). A tuple `(i64, i64)` has LLVM type `{ i64, i64 }`. Tuples are represented as anonymous LLVM struct types (same layout as named structs). A tuple `Tuple(i64, i64)` has LLVM type `{ i64, i64 }`.
#### Tuple Operators #### Tuple Operators
**Equality and inequality** — element-wise comparison, both sides must have the same field count: **Equality and inequality** — element-wise comparison, both sides must have the same field count:
```sx ```sx
(1, 2) == (1, 2) // true .(1, 2) == .(1, 2) // true
(1, 2) != (1, 3) // true .(1, 2) != .(1, 3) // true
``` ```
**Concatenation** (`+`) — creates a new tuple with fields from both sides: **Concatenation** (`+`) — creates a new tuple with fields from both sides:
```sx ```sx
c := (1, 2) + (3, 4); // c : (i64, i64, i64, i64) c := .(1, 2) + .(3, 4); // c : Tuple(i64, i64, i64, i64)
c.0; // 1 c.0; // 1
c.3; // 4 c.3; // 4
``` ```
**Repetition** (`*`) — repeats a tuple N times (N must be a compile-time integer literal): **Repetition** (`*`) — repeats a tuple N times (N must be a compile-time integer literal):
```sx ```sx
r := (1, 2) * 3; // r : (i64, i64, i64, i64, i64, i64) r := .(1, 2) * 3; // r : Tuple(i64, i64, i64, i64, i64, i64)
r.0; // 1 r.0; // 1
r.5; // 2 r.5; // 2
``` ```
**Lexicographic comparison** (`<`, `<=`, `>`, `>=`) — compares element-by-element left to right: **Lexicographic comparison** (`<`, `<=`, `>`, `>=`) — compares element-by-element left to right:
```sx ```sx
(1, 2) < (1, 3) // true (first fields equal, 2 < 3) .(1, 2) < .(1, 3) // true (first fields equal, 2 < 3)
(2, 0) > (1, 9) // true (2 > 1, rest ignored) .(2, 0) > .(1, 9) // true (2 > 1, rest ignored)
(1, 2) <= (1, 2) // true (all equal, <= allows tie) .(1, 2) <= .(1, 2) // true (all equal, <= allows tie)
``` ```
**Membership** (`in`) — checks if a value exists in a tuple: **Membership** (`in`) — checks if a value exists in a tuple:
```sx ```sx
3 in (1, 2, 3) // true 3 in .(1, 2, 3) // true
5 in (1, 2, 3) // false 5 in .(1, 2, 3) // false
``` ```
### Array Types ### Array Types
@@ -1470,8 +1489,8 @@ may do, regardless of the concrete arg types at any particular call site.
| Comptime unroll (element + index) | `inline for xs, 0.. (x, i) { ... }` | multi-iterable parity with the runtime `for`: position 0 drives the count, a trailing open range pairs the cursor | | Comptime unroll (element + index) | `inline for xs, 0.. (x, i) { ... }` | multi-iterable parity with the runtime `for`: position 0 drives the count, a trailing open range pairs the cursor |
| Projection | `xs.field` | see "Pack projection" | | Projection | `xs.field` | see "Pack projection" |
| Spread → call args | `..xs` / `..xs.field` | expands to N positional args | | Spread → call args | `..xs` / `..xs.field` | expands to N positional args |
| Spread → tuple value | `(..xs)` / `(..xs.field)` | materializes a tuple | | 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 → tuple type | `Tuple(..F(Ts))` / `Tuple(..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 | | Spread → callable sig | `Closure(..Ts) -> R` / `Closure(..Ts.Arg) -> R` | positional params of the callable |
#### Pack projection #### Pack projection
@@ -1484,7 +1503,7 @@ Resolution is **position-driven** (no cross-namespace shadowing):
type-arg `T`, so `..xs.T` is the pack of element value-types. 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 - In **value** position, `xs.field` looks `field` up in the constraint's
**runtime-field** namespace and yields a *tuple* of the projected values **runtime-field** namespace and yields a *tuple* of the projected values
(e.g. `xs.value` → `(xs[0].value, xs[1].value, ...)`). (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** 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 compiles, but emits a soft warning at the protocol declaration (the human is
@@ -1499,13 +1518,13 @@ tuple rather than a pack:
- `tuple.field` projects `field` out of every element (when all elements have a - `tuple.field` projects `field` out of every element (when all elements have a
same-named field), returning a tuple of the projected values. same-named field), returning a tuple of the projected values.
This lets a pack be materialized once (`stored := (..xs)`) and later re-spread This lets a pack be materialized once (`stored := .(..xs)`) and later re-spread
(`f(..stored)`) or re-projected (`stored.value`). (`f(..stored)`) or re-projected (`stored.value`).
#### Pack of zero (N = 0) #### Pack of zero (N = 0)
`xs.len == 0` is valid: `inline for` over an empty range doesn't execute, spreads `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. 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 `map`) must handle N=0 — typically by producing a constant result that never
changes. changes.
@@ -1515,10 +1534,10 @@ Because a pack has no runtime representation, using the **bare pack name** where
a runtime value is required is a compile error with a context-tailored a runtime value is required is a compile error with a context-tailored
suggestion: suggestion:
- storing/binding it (`x := xs;`, `self.f = xs;`) → materialize a tuple `(..xs)`; - storing/binding it (`x := xs;`, `self.f = xs;`) → materialize a tuple `.(..xs)`;
- passing it to a runtime call (`f(xs)`) → declare the parameter as a *slice* - passing it to a runtime call (`f(xs)`) → declare the parameter as a *slice*
variadic `..xs: []P` (a runtime slice) instead of a pack `..xs: P`; variadic `..xs: []P` (a runtime slice) instead of a pack `..xs: P`;
- returning it (`return xs;`) → return a tuple `(..xs)` (and make the return - returning it (`return xs;`) → return a tuple `.(..xs)` (and make the return
type that tuple); type that tuple);
- iterating it (`for xs (x)`, `xs[runtime_i]`) → `inline for xs (x)` (or - iterating it (`for xs (x)`, `xs[runtime_i]`) → `inline for xs (x)` (or
`inline for 0..xs.len (i)` for the index) for a comptime unroll, or take `inline for 0..xs.len (i)` for the index) for a comptime unroll, or take
@@ -1533,8 +1552,8 @@ place.
#### Storage and protocol conformance #### Storage and protocol conformance
To **store** a pack, materialize a tuple: a pack-shaped struct field is To **store** a pack, materialize a tuple: a pack-shaped struct field is
tuple-typed, `sources: (..ValueListenable(Ts))`, assigned `self.sources = tuple-typed, `sources: Tuple(..ValueListenable(Ts))`, assigned `self.sources =
(..sources)`. To **return** a struct as a protocol value, `xx` requires an .(..sources)`. To **return** a struct as a protocol value, `xx` requires an
explicit impl (protocol erasure is impl-driven, not structural) — e.g. explicit impl (protocol erasure is impl-driven, not structural) — e.g.
`impl ValueListenable($R) for Combined($R, ..$Ts) { ... }`. `impl ValueListenable($R) for Combined($R, ..$Ts) { ... }`.
@@ -1542,7 +1561,7 @@ explicit impl (protocol erasure is impl-driven, not structural) — e.g.
```sx ```sx
Combined :: struct($R: Type, ..$Ts: []Type) { Combined :: struct($R: Type, ..$Ts: []Type) {
sources: (..ValueListenable(Ts)); // pack-spread in tuple type position sources: Tuple(..ValueListenable(Ts)); // pack-spread in tuple type position
mapper: Closure(..Ts) -> $R; // pack-spread in callable sig mapper: Closure(..Ts) -> $R; // pack-spread in callable sig
value: $R; value: $R;
own_allocator: Allocator; own_allocator: Allocator;
@@ -1559,7 +1578,7 @@ map :: (mapper: Closure(..sources.T) -> $R, ..sources: ValueListenable)
c := context.allocator.alloc(Combined($R, ..sources.T)); c := context.allocator.alloc(Combined($R, ..sources.T));
c.own_allocator = context.allocator; c.own_allocator = context.allocator;
c.mapper = mapper; c.mapper = mapper;
c.sources = (..sources); // pack-to-tuple materialization c.sources = .(..sources); // pack-to-tuple materialization
inline for 0..sources.len (i) { // comptime unroll over the pack inline for 0..sources.len (i) { // comptime unroll over the pack
sources[i].addListener((_) => c.recompute()); sources[i].addListener((_) => c.recompute());
} }
@@ -1827,15 +1846,16 @@ name :: (params) -> return_type {
``` ```
- Parameters: `name: type` separated by commas - Parameters: `name: type` separated by commas
- Return type: `-> type` (omit for void). A multi-value return is a tuple: `-> (T1, T2)`. - Return type: `-> type` (omit for void). A multi-value return is a tuple: `-> Tuple(T1, T2)`.
- Body: a block whose **value** is its last statement when that statement is a - Body: a block whose **value** is its last statement when that statement is a
trailing expression with **no** `;` (see [Block values](#block-values)). That trailing expression with **no** `;` (see [Block values](#block-values)). That
value is the implicit return; an explicit `return` works too. value is the implicit return; an explicit `return` works too.
A trailing `!` in the return type marks the function **failable** — it adds a A trailing `!` in the return type marks the function **failable** — it adds a
separate error channel alongside the normal returns (`-> (T, !)`, `-> !`, separate error channel alongside the normal returns. The `!` sits **outside**
`-> (T1, T2, !)`). The `!` is not a wrapper around the value; it is one more the tuple: `-> T !` (one value), `-> Tuple(T1, T2) !` (multi value), `-> !`
return slot. See [§12 Error Handling](#12-error-handling). (void). The `!` is not a wrapper around the value; it is one more return slot.
See [§12 Error Handling](#12-error-handling).
Examples: Examples:
```sx ```sx
@@ -2462,8 +2482,8 @@ When a tuple is used as the receiver of a UFCS call, its elements are unpacked a
num_add :: (a: i64, b: i64) -> i64 { a + b; } num_add :: (a: i64, b: i64) -> i64 { a + b; }
add :: ufcs num_add; add :: ufcs num_add;
(40, 2).add(); // splats to num_add(40, 2) → 42 .(40, 2).add(); // splats to num_add(40, 2) → 42
(40,).add(2); // partial: num_add(40, 2) → 42 .(40).add(2); // partial: num_add(40, 2) → 42
40.add(2); // normal UFCS: num_add(40, 2) → 42 40.add(2); // normal UFCS: num_add(40, 2) → 42
``` ```
@@ -2472,8 +2492,8 @@ With more arguments:
compute :: (a: i64, b: i64, c: i64, d: i64) -> i64 { a + b * c - d; } compute :: (a: i64, b: i64, c: i64, d: i64) -> i64 { a + b * c - d; }
calc :: ufcs compute; calc :: ufcs compute;
(1, 2, 3, 4).calc(); // full splat → compute(1, 2, 3, 4) .(1, 2, 3, 4).calc(); // full splat → compute(1, 2, 3, 4)
(1, 2).calc(3, 4); // partial splat → compute(1, 2, 3, 4) .(1, 2).calc(3, 4); // partial splat → compute(1, 2, 3, 4)
1.calc(2, 3, 4); // normal UFCS → compute(1, 2, 3, 4) 1.calc(2, 3, 4); // normal UFCS → compute(1, 2, 3, 4)
``` ```
@@ -3101,7 +3121,7 @@ main :: () {
`main` takes no arguments. Its return type may be any of: void (`()`, `main` takes no arguments. Its return type may be any of: void (`()`,
`-> ()`, `-> void`, or no annotation), an integer type (POSIX exit code), `-> ()`, `-> void`, or no annotation), an integer type (POSIX exit code),
`-> !` (pure failable), or `-> (int_type, !)` (value-carrying failable). `-> !` (pure failable), or `-> int_type !` (value-carrying failable).
The exit code is `0` for void / `-> !` success, the integer return The exit code is `0` for void / `-> !` success, the integer return
truncated to `u8` otherwise. An error that escapes a failable `main` truncated to `u8` otherwise. An error that escapes a failable `main`
prints the unhandled-error header + return trace to stderr and exits `1`. prints the unhandled-error header + return trace to stderr and exits `1`.
@@ -3114,8 +3134,9 @@ See [§12 Error Handling](#12-error-handling).
sx models recoverable errors as a **separate return channel**, not a wrapped sx models recoverable errors as a **separate return channel**, not a wrapped
result type. A trailing `!` in a function's return type adds one extra return result type. A trailing `!` in a function's return type adds one extra return
slot — a `u32` error tag — alongside the normal value slots. This keeps sx's slot — a `u32` error tag — alongside the normal value slots. This keeps sx's
native multi-return ergonomics: `-> (i32, i64, !)` is a function returning two native multi-return ergonomics: `-> Tuple(i32, i64) !` is a function returning
values *and* an error, with no tuple-in-a-wrapper. two values *and* an error, with no tuple-in-a-wrapper. The `!` sits **outside**
the `Tuple`.
This section is the canonical surface reference. The design rationale, This section is the canonical surface reference. The design rationale,
trade-offs, and implementation breakdown live in `current/PLAN-ERR.md`. trade-offs, and implementation breakdown live in `current/PLAN-ERR.md`.
@@ -3123,10 +3144,10 @@ trade-offs, and implementation breakdown live in `current/PLAN-ERR.md`.
### Failable signatures ### Failable signatures
```sx ```sx
parse_digit :: (s: string) -> (i32, !) { ... } // one value + error parse_digit :: (s: string) -> i32 ! { ... } // one value + error
parse :: (s: string) -> (i32, i64, !) { ... } // multi-value + error parse :: (s: string) -> Tuple(i32, i64) ! { ... } // multi-value + error
must_init :: () -> ! { ... } // pure failable, no value must_init :: () -> ! { ... } // pure failable, no value
divide :: (a: i32, b: i32) -> (i32, !MathErr) { ... } // named set divide :: (a: i32, b: i32) -> i32 !MathErr { ... } // named set
``` ```
The `!` is always the **last** slot. `0` in the error slot means "no error"; The `!` is always the **last** slot. `0` in the error slot means "no error";
@@ -3141,7 +3162,7 @@ Two forms of error set:
ParseErr :: error { BadDigit, Overflow, Empty }; ParseErr :: error { BadDigit, Overflow, Empty };
// Inferred set — bare `!` collects whatever tags the body raises. // Inferred set — bare `!` collects whatever tags the body raises.
quick :: () -> (i32, !) { quick :: () -> i32 ! {
if cond raise error.SomeAdHocTag; // mints into the inferred set if cond raise error.SomeAdHocTag; // mints into the inferred set
return 0; return 0;
} }
@@ -3228,7 +3249,7 @@ v := parse_digit(s) catch (e) compute_fallback(e); // value-producing body
v, n := parse(s) catch (e) { v, n := parse(s) catch (e) {
log.warn("parse failed: {}", e); log.warn("parse failed: {}", e);
(0, 0) // tuple body for a multi-value failable .(0, 0) // tuple body for a multi-value failable
}; };
v := parse(s) catch (e) == { // match-body form v := parse(s) catch (e) == { // match-body form
@@ -3264,7 +3285,7 @@ the RHS shape decides the result:
v := parse_digit(s) or 0; // value terminator → non-failable v := parse_digit(s) or 0; // value terminator → non-failable
v := try foo() or try boo(); // chain, propagate if both fail v := try foo() or try boo(); // chain, propagate if both fail
v := foo() or boo() or 0; // bare operands, 0 absorbs all v := foo() or boo() or 0; // bare operands, 0 absorbs all
v, n := parse_pair(s) or (0, 0); // tuple terminator (multi-value) v, n := parse_pair(s) or .(0, 0); // tuple terminator (multi-value)
``` ```
A **void** failable (`-> !`) rejects a plain-value RHS (no success type to A **void** failable (`-> !`) rejects a plain-value RHS (no success type to
@@ -3339,14 +3360,14 @@ On success exit (fall-through, `return`, `break` / `continue` without an error)
it is skipped — only `defer` runs. it is skipped — only `defer` runs.
```sx ```sx
make_handle :: () -> (Handle, !) { make_handle :: () -> Handle ! {
h := try open(); h := try open();
onfail close(h); // close ONLY on a subsequent failure onfail close(h); // close ONLY on a subsequent failure
try configure(h); // fails → onfail runs → close(h) try configure(h); // fails → onfail runs → close(h)
return h; // success → onfail skipped; caller owns h return h; // success → onfail skipped; caller owns h
} }
open :: (path: string) -> (Handle, !) { open :: (path: string) -> Handle ! {
h := try sys_open(path); h := try sys_open(path);
onfail (e) { log.warn("init failed for {}: {}", path, e); sys_close(h); } onfail (e) { log.warn("init failed for {}: {}", path, e); sys_close(h); }
... ...
@@ -3367,9 +3388,9 @@ function, or at top level, is rejected.
- **Explicit annotation required.** A closure literal's value type is inferred - **Explicit annotation required.** A closure literal's value type is inferred
as today, but if its body raises or `try`-escapes, the `!` channel is **not** as today, but if its body raises or `try`-escapes, the `!` channel is **not**
inferred — declare it (`closure((x: i32) -> (i32, !) { ... })`). This keeps inferred — declare it (`closure((x: i32) -> i32 ! { ... })`). This keeps
adding a `raise` from silently changing a lambda's type. adding a `raise` from silently changing a lambda's type.
- **Program-wide union per shape.** All `Closure(<sig>) -> (T, !)` occurrences - **Program-wide union per shape.** All `Closure(<sig>) -> T !` occurrences
with the same signature share one inferred-set node; the SCC pass unions with the same signature share one inferred-set node; the SCC pass unions
every closure flowing into any matching slot. every closure flowing into any matching slot.
- **FFI boundary.** A failable closure cannot be assigned to a non-failable - **FFI boundary.** A failable closure cannot be assigned to a non-failable
@@ -3427,8 +3448,9 @@ const_decl = IDENT '::' expr ';'
var_decl = IDENT ':=' expr ';' var_decl = IDENT ':=' expr ';'
| IDENT ':' type '=' expr ';' | IDENT ':' type '=' expr ';'
| IDENT ':' type ';' | IDENT ':' type ';'
fn_decl = IDENT '::' '(' params? ')' ('->' type)? block fn_decl = IDENT '::' '(' params? ')' ('->' ret_type)? block
| IDENT '::' block | IDENT '::' block
ret_type = type ('!' IDENT?)? // trailing `!` = failable; channel outside any Tuple
enum_decl = IDENT '::' 'enum' '{' (IDENT ';')* '}' enum_decl = IDENT '::' 'enum' '{' (IDENT ';')* '}'
struct_decl = IDENT '::' 'struct' '{' struct_member* '}' struct_decl = IDENT '::' 'struct' '{' struct_member* '}'
struct_member = field_group | '#using' IDENT ';' struct_member = field_group | '#using' IDENT ';'
@@ -3459,12 +3481,16 @@ binary = catch_expr (binop catch_expr)* // binop includes `or` (fall
catch_expr = unary ('catch' ('(' IDENT ')')? (block | '==' '{' case_arm* else_arm? '}' | unary))? catch_expr = unary ('catch' ('(' IDENT ')')? (block | '==' '{' case_arm* else_arm? '}' | unary))?
unary = ('-' | '!' | 'xx' | 'try' | 'cast' '(' type ')') postfix unary = ('-' | '!' | 'xx' | 'try' | 'cast' '(' type ')') postfix
| postfix | postfix
postfix = primary ('(' args? ')' | '.' IDENT | '.{' field_init_list '}')* postfix = primary ('(' args? ')' | '.' IDENT | '.{' field_init_list '}'
| '.(' tuple_elem_list? ')')*
primary = INT | HEX_INT | BIN_INT | FLOAT | STRING | BOOL | IDENT | '---' primary = INT | HEX_INT | BIN_INT | FLOAT | STRING | BOOL | IDENT | '---'
| '.' IDENT | '.' '{' field_init_list '}' | '.' IDENT | '.' '{' field_init_list '}'
| '(' expr ')' | block | '#run' expr | '.(' tuple_elem_list? ')' // tuple value (anonymous): .(a, b) / .(x = a) / .() / .(..xs)
| '(' expr ')' | block | '#run' expr // bare parens = grouping ONLY
field_init_list = field_init (',' field_init)* ','? field_init_list = field_init (',' field_init)* ','?
field_init = IDENT '=' expr | IDENT | expr field_init = IDENT '=' expr | IDENT | expr
tuple_elem_list = tuple_elem (',' tuple_elem)* ','?
tuple_elem = IDENT '=' expr | '..' expr | expr
if_expr = 'if' expr 'then' expr ('else' expr)? if_expr = 'if' expr 'then' expr ('else' expr)?
| 'if' expr block ('else' block)? | 'if' expr block ('else' block)?
match_expr = 'if' expr '==' '{' case_arm* else_arm? '}' match_expr = 'if' expr '==' '{' case_arm* else_arm? '}'
@@ -3475,8 +3501,12 @@ lambda = '(' params? ')' ('->' type)? '=>' expr
args = expr (',' expr)* ','? args = expr (',' expr)* ','?
type = '$' IDENT | 'i32' | 'f32' | 'f64' | 'bool' | 'string' type = '$' IDENT | 'i32' | 'f32' | 'f64' | 'bool' | 'string'
| 'Any' | 'Type' | '..' type | '[' expr ']' type | IDENT | 'Any' | 'Type' | '..' type | '[' expr ']' type | IDENT
| '(' type (',' type)* ',' '!' IDENT? ')' // value-carrying failable | 'Tuple' '(' tuple_type_list? ')' // tuple type: Tuple(A, B) / Tuple(x: A) / Tuple() / Tuple(..F(Ts))
| '(' type ')' // grouping (bare parens never form a tuple)
| '(' (type (',' type)*)? ')' '->' type ('!' IDENT?)? // function type (params optional; optional error channel)
| '!' IDENT? // pure failable (`!` / `!Named`) | '!' IDENT? // pure failable (`!` / `!Named`)
tuple_type_list = tuple_type_elem (',' tuple_type_elem)* ','?
tuple_type_elem = IDENT ':' type | '..' type | type
``` ```
--- ---