From 40b5fb5f7eb7cd5b314736e4502f07c349ceab2d Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 25 Jun 2026 18:41:22 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20tuple=20syntax=20cutover=20=E2=80=94=20?= =?UTF-8?q?Tuple(...)=20type,=20.(...)=20value,=20channel-outside-Tuple=20?= =?UTF-8?q?failables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- current/CHECKPOINT-LANG.md | 51 ++++++ examples/basic/0036-basic-ufcs-aliases.sx | 2 +- .../comptime/0623-comptime-metatype-tuple.sx | 2 +- ...ics-tuple-type-nontype-element-rejected.sx | 6 +- .../0533-packs-pack-tuple-materialize.sx | 4 +- .../packs/0534-packs-pack-type-projection.sx | 6 +- .../packs/0539-packs-combined-pack-field.sx | 4 +- .../0541-packs-pack-to-protocol-tuple.sx | 6 +- examples/packs/0543-packs-canonical-map.sx | 2 +- examples/probes/pack-expansion-parses.sx | 4 +- examples/probes/tuple-baseline.sx | 10 +- .../0815-route-all-new-surfaces-ambiguous.sx | 2 +- .../0115-types-compound-type-in-expression.sx | 4 +- .../types/0120-types-tuple-element-assign.sx | 2 +- .../0201-types-parenthesized-type-grouping.sx | 12 +- readme.md | 4 +- specs.md | 166 +++++++++++------- 17 files changed, 184 insertions(+), 103 deletions(-) create mode 100644 current/CHECKPOINT-LANG.md diff --git a/current/CHECKPOINT-LANG.md b/current/CHECKPOINT-LANG.md new file mode 100644 index 00000000..79d2cb6a --- /dev/null +++ b/current/CHECKPOINT-LANG.md @@ -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). diff --git a/examples/basic/0036-basic-ufcs-aliases.sx b/examples/basic/0036-basic-ufcs-aliases.sx index adc10312..821b3827 100644 --- a/examples/basic/0036-basic-ufcs-aliases.sx +++ b/examples/basic/0036-basic-ufcs-aliases.sx @@ -35,7 +35,7 @@ main :: () { print("{}\n", a); // 2 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); print("{}\n", t.0); // 99 } diff --git a/examples/comptime/0623-comptime-metatype-tuple.sx b/examples/comptime/0623-comptime-metatype-tuple.sx index 88f47ccb..daf10654 100644 --- a/examples/comptime/0623-comptime-metatype-tuple.sx +++ b/examples/comptime/0623-comptime-metatype-tuple.sx @@ -3,7 +3,7 @@ // tuple here). Tuples are POSITIONAL, so `TupleInfo` is just a `[]Type` (no field // names). Two paths: // 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 // reconstructs it — no literal element list. #import "modules/std.sx"; diff --git a/examples/diagnostics/1116-diagnostics-tuple-type-nontype-element-rejected.sx b/examples/diagnostics/1116-diagnostics-tuple-type-nontype-element-rejected.sx index 06b94955..1f1a4021 100644 --- a/examples/diagnostics/1116-diagnostics-tuple-type-nontype-element-rejected.sx +++ b/examples/diagnostics/1116-diagnostics-tuple-type-nontype-element-rejected.sx @@ -1,6 +1,6 @@ -// A tuple literal used in a type position (`(i32, i32)` reinterpreted as a tuple -// type at a type-demanding site like `size_of`) must list only types. A non-type -// element — here the `1` in `(i32, 1)` — is rejected with a user-facing +// A tuple type (`Tuple(i32, i32)` at a type-demanding site like `size_of`) must +// list only types. A non-type +// 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. // Regression (issue 0067). // Expected: a clean "tuple type element is not a type" error at the `1`; exit 1. diff --git a/examples/packs/0533-packs-pack-tuple-materialize.sx b/examples/packs/0533-packs-pack-tuple-materialize.sx index 67eca071..bd5efe0a 100644 --- a/examples/packs/0533-packs-pack-tuple-materialize.sx +++ b/examples/packs/0533-packs-pack-tuple-materialize.sx @@ -1,5 +1,5 @@ -// 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 +// 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 // 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. diff --git a/examples/packs/0534-packs-pack-type-projection.sx b/examples/packs/0534-packs-pack-type-projection.sx index 60b80c0a..de345e1a 100644 --- a/examples/packs/0534-packs-pack-type-projection.sx +++ b/examples/packs/0534-packs-pack-type-projection.sx @@ -1,6 +1,6 @@ // 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 -// 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 // `impl Box(T) for `.) @@ -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(i64) for Dbl { get :: (self: *Dbl) -> i64 => self.n * 2; } -// Tuple type `(..xs.T)` — heterogeneous (i64, string), matched by the -// value-projection `(..xs.get)`. +// Tuple type `Tuple(..xs.T)` — heterogeneous (i64, string), matched by the +// value-projection `.(..xs.get)`. snap :: (..xs: Box) -> void { t : Tuple(..xs.T) = .(..xs.get); print("0={} 1={}\n", t.0, t.1); diff --git a/examples/packs/0539-packs-combined-pack-field.sx b/examples/packs/0539-packs-combined-pack-field.sx index 8c27fc7e..ca8e6708 100644 --- a/examples/packs/0539-packs-combined-pack-field.sx +++ b/examples/packs/0539-packs-combined-pack-field.sx @@ -1,8 +1,8 @@ // Phase 4.2 — the canonical `Combined` struct's storage layer: a generic // 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 -// `(..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. #import "modules/std.sx"; diff --git a/examples/packs/0541-packs-pack-to-protocol-tuple.sx b/examples/packs/0541-packs-pack-to-protocol-tuple.sx index 6064b95c..f1aa86c2 100644 --- a/examples/packs/0541-packs-pack-to-protocol-tuple.sx +++ b/examples/packs/0541-packs-pack-to-protocol-tuple.sx @@ -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 slot. The pack `..sources: VL` holds concrete cells; `(..sources)` -// into a `(..VL(Ts))` field `xx`-erases each to its `VL(Ti)` value. +// protocol slot. The pack `..sources: VL` holds concrete cells; `.(..sources)` +// into a `Tuple(..VL(Ts))` field `xx`-erases each to its `VL(Ti)` value. #import "modules/std.sx"; diff --git a/examples/packs/0543-packs-canonical-map.sx b/examples/packs/0543-packs-canonical-map.sx index b398dff9..80a3f8d1 100644 --- a/examples/packs/0543-packs-canonical-map.sx +++ b/examples/packs/0543-packs-canonical-map.sx @@ -5,7 +5,7 @@ // - `$R` is inferred at the call site from the lowered mapper's closure ret, // bound into the mono (`-> VL($R)` ⇒ `VL(i64)`, `Combined($R, ..)` ⇒ // `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 // generic-struct instance to `VL(i64)` via the generic impl's monomorphized // thunk. diff --git a/examples/probes/pack-expansion-parses.sx b/examples/probes/pack-expansion-parses.sx index 532ac683..35ca0c42 100644 --- a/examples/probes/pack-expansion-parses.sx +++ b/examples/probes/pack-expansion-parses.sx @@ -6,12 +6,12 @@ // 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: …"). -// 1. Tuple value position — `(..pack)` / `(..pack.field)`: +// 1. Tuple value position — `.(..pack)` / `.(..pack.field)`: tv1 :: () => .(..xs); tv2 :: () => .(..xs.value); 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; tt2 :: (x: Tuple(..ValueListenable(Ts.Arg))) => x; diff --git a/examples/probes/tuple-baseline.sx b/examples/probes/tuple-baseline.sx index cec7e2d3..ee56efb8 100644 --- a/examples/probes/tuple-baseline.sx +++ b/examples/probes/tuple-baseline.sx @@ -46,17 +46,17 @@ main :: () -> i32 { // ── GAPS (Feature 1 work — intentionally NOT exercised above) ────── // // G1. Tuple field projection across elements: -// t := (Listenable.{value=1}, Listenable.{value=2}); -// v := t.value; // expected: (1, 2) — Decision 3 "tuple.field" +// t := .(Listenable.{value=1}, Listenable.{value=2}); +// v := t.value; // expected: .(1, 2) — Decision 3 "tuple.field" // Today: `error: field 'value' not found on type 'tuple'`. // Needed by canonical `self.sources.value`. // // G2. Tuple spread into call args: -// p := (10, 20); +// p := .(10, 20); // add(..p); // expected: add(10, 20) — Decision 3 "..tuple" // 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 -// `(..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. diff --git a/examples/route/0815-route-all-new-surfaces-ambiguous.sx b/examples/route/0815-route-all-new-surfaces-ambiguous.sx index 04621a57..55c7dee4 100644 --- a/examples/route/0815-route-all-new-surfaces-ambiguous.sx +++ b/examples/route/0815-route-all-new-surfaces-ambiguous.sx @@ -10,7 +10,7 @@ // - wrapper-alias element `BoxPtr :: *Box` (engine wrapper aliasing) // - union body-builder child `WrapU :: union { b: Box }` (C1, registerUnionDecl) // - 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) // // Fail-before (pre-E6b-R): each of these resolved `Box` through the stateless diff --git a/examples/types/0115-types-compound-type-in-expression.sx b/examples/types/0115-types-compound-type-in-expression.sx index dddbe2a5..51b1fdec 100644 --- a/examples/types/0115-types-compound-type-in-expression.sx +++ b/examples/types/0115-types-compound-type-in-expression.sx @@ -1,6 +1,6 @@ // Compound type literals in expression position — `size_of` / // `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). // Same shape as the existing `size_of(i32)` baseline path. @@ -22,7 +22,7 @@ main :: () -> i32 { // Function-type literal in expression position. 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))); // Aliases. diff --git a/examples/types/0120-types-tuple-element-assign.sx b/examples/types/0120-types-tuple-element-assign.sx index 0de753a8..7fdfa942 100644 --- a/examples/types/0120-types-tuple-element-assign.sx +++ b/examples/types/0120-types-tuple-element-assign.sx @@ -2,7 +2,7 @@ // - `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 // `.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). #import "modules/std.sx"; diff --git a/examples/types/0201-types-parenthesized-type-grouping.sx b/examples/types/0201-types-parenthesized-type-grouping.sx index c6d314ee..ea0dfe9e 100644 --- a/examples/types/0201-types-parenthesized-type-grouping.sx +++ b/examples/types/0201-types-parenthesized-type-grouping.sx @@ -1,12 +1,12 @@ -// Parenthesized type grouping: in type position `(T)` (single element, no -// trailing comma) is a GROUPING that resolves to the inner type — mirroring -// value position where `(expr)` groups and `(expr,)` is a 1-tuple. A 1-tuple -// type requires the trailing comma `(T,)`; `(A, B)` is a 2-tuple. +// Parenthesized type grouping: bare parens `(T)` are GROUPING ONLY (in every +// position) and resolve to the inner type. A tuple type is `Tuple(...)`: +// `Tuple(T)` is a 1-tuple, `Tuple(A, B)` a 2-tuple. Bare parens never form a +// tuple. // // This lets a closure/optional type be parenthesized for readability: // [1](Closure(i64,i64) -> i64) // array of closures (grouped element type) // ?(?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"; @@ -28,7 +28,7 @@ main :: () { fns : [1](Closure(i64, i64) -> i64) = .[ add ]; 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); print("{}\n", one.0); // 9 diff --git a/readme.md b/readme.md index d6ba8b52..58cb7f35 100644 --- a/readme.md +++ b/readme.md @@ -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. 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 // multiple value outputs → a destructurable tuple -split :: (x: u64) -> (lo: u64, hi: u64) { +split :: (x: u64) -> Tuple(lo: u64, hi: u64) { return asm { #string ASM and %[l], %[x], #0xff diff --git a/specs.md b/specs.md index 347a85ec..bd0b820f 100644 --- a/specs.md +++ b/specs.md @@ -827,35 +827,54 @@ 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. 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 ```sx -pair := (40, 2); // positional tuple: (i64, i64) -named := (x: 10, y: 20); // named tuple: (x: i64, y: i64) -single := (42,); // 1-tuple (trailing comma in value position) -zeroed : (i32, i32) = ---; // zero-initialized tuple +pair := .(40, 2); // positional tuple value: Tuple(i64, i64) +named := .(x = 10, y = 20); // named tuple value: Tuple(x: i64, y: i64) +single := .(42); // 1-tuple value +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 -In type position, parentheses mirror value position: `(T)` (a single element, no -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: +The tuple type is `Tuple(...)`: ```sx -(i64) // grouping: resolves to i64 (NOT a tuple) -(i64,) // tuple type with one field -(i64, i64) // tuple type with two fields +Tuple(i64) // 1-tuple type +Tuple(i64, i64) // 2-tuple type +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) -> i64 // function type: takes two i64, returns i64 ?(?i64) // grouping → a genuine nested optional [1](Closure(i64,i64) -> i64) // grouping → array of one closure ``` -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 -(named) tuple. +Grouping lets a closure/optional/function type be parenthesized for readability. +Function types `(A, B) -> R`, parameter lists, lambdas, and `match` bindings keep +using bare parens — they are unaffected by the tuple grammar. #### Field Access ```sx @@ -867,49 +886,49 @@ named.0; // 10 — numeric index also works on named tuples #### As Return Type ```sx -swap :: (a: i64, b: i64) -> (i64, i64) { (b, a); } -wrap :: (x: i64) -> (i64,) { (x,); } // 1-tuple return needs the trailing comma +swap :: (a: i64, b: i64) -> Tuple(i64, i64) { .(b, a) } +wrap :: (x: i64) -> Tuple(i64) { .(x) } // 1-tuple return s := swap(1, 2); // s.0 = 2, s.1 = 1 t := wrap(42); // t.0 = 42 ``` #### 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 **Equality and inequality** — element-wise comparison, both sides must have the same field count: ```sx -(1, 2) == (1, 2) // true -(1, 2) != (1, 3) // true +.(1, 2) == .(1, 2) // true +.(1, 2) != .(1, 3) // true ``` **Concatenation** (`+`) — creates a new tuple with fields from both sides: ```sx -c := (1, 2) + (3, 4); // c : (i64, i64, i64, i64) -c.0; // 1 -c.3; // 4 +c := .(1, 2) + .(3, 4); // c : Tuple(i64, i64, i64, i64) +c.0; // 1 +c.3; // 4 ``` **Repetition** (`*`) — repeats a tuple N times (N must be a compile-time integer literal): ```sx -r := (1, 2) * 3; // r : (i64, i64, i64, i64, i64, i64) -r.0; // 1 -r.5; // 2 +r := .(1, 2) * 3; // r : Tuple(i64, i64, i64, i64, i64, i64) +r.0; // 1 +r.5; // 2 ``` **Lexicographic comparison** (`<`, `<=`, `>`, `>=`) — compares element-by-element left to right: ```sx -(1, 2) < (1, 3) // true (first fields equal, 2 < 3) -(2, 0) > (1, 9) // true (2 > 1, rest ignored) -(1, 2) <= (1, 2) // true (all equal, <= allows tie) +.(1, 2) < .(1, 3) // true (first fields equal, 2 < 3) +.(2, 0) > .(1, 9) // true (2 > 1, rest ignored) +.(1, 2) <= .(1, 2) // true (all equal, <= allows tie) ``` **Membership** (`in`) — checks if a value exists in a tuple: ```sx -3 in (1, 2, 3) // true -5 in (1, 2, 3) // false +3 in .(1, 2, 3) // true +5 in .(1, 2, 3) // false ``` ### 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 | | 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 → tuple value | `.(..xs)` / `.(..xs.field)` | materializes a tuple | +| 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 | #### 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. - 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, ...)`). + (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 @@ -1499,13 +1518,13 @@ tuple rather than a pack: - `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 +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. +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. @@ -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 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* 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); - 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 @@ -1533,8 +1552,8 @@ place. #### 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 +tuple-typed, `sources: Tuple(..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) { ... }`. @@ -1542,7 +1561,7 @@ explicit impl (protocol erasure is impl-driven, not structural) — e.g. ```sx 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 value: $R; own_allocator: Allocator; @@ -1559,7 +1578,7 @@ map :: (mapper: Closure(..sources.T) -> $R, ..sources: ValueListenable) c := context.allocator.alloc(Combined($R, ..sources.T)); c.own_allocator = context.allocator; 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 sources[i].addListener((_) => c.recompute()); } @@ -1827,15 +1846,16 @@ name :: (params) -> return_type { ``` - 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 trailing expression with **no** `;` (see [Block values](#block-values)). That value is the implicit return; an explicit `return` works too. A trailing `!` in the return type marks the function **failable** — it adds a -separate error channel alongside the normal returns (`-> (T, !)`, `-> !`, -`-> (T1, T2, !)`). The `!` is not a wrapper around the value; it is one more -return slot. See [§12 Error Handling](#12-error-handling). +separate error channel alongside the normal returns. The `!` sits **outside** +the tuple: `-> T !` (one value), `-> Tuple(T1, T2) !` (multi value), `-> !` +(void). The `!` is not a wrapper around the value; it is one more return slot. +See [§12 Error Handling](#12-error-handling). Examples: ```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; } add :: ufcs num_add; -(40, 2).add(); // splats to num_add(40, 2) → 42 -(40,).add(2); // partial: 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); // 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; } calc :: ufcs compute; -(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, 3, 4).calc(); // full 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) ``` @@ -3101,7 +3121,7 @@ main :: () { `main` takes no arguments. Its return type may be any of: void (`()`, `-> ()`, `-> 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 truncated to `u8` otherwise. An error that escapes a failable `main` 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 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 -native multi-return ergonomics: `-> (i32, i64, !)` is a function returning two -values *and* an error, with no tuple-in-a-wrapper. +native multi-return ergonomics: `-> Tuple(i32, i64) !` is a function returning +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, 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 ```sx -parse_digit :: (s: string) -> (i32, !) { ... } // one value + error -parse :: (s: string) -> (i32, i64, !) { ... } // multi-value + error +parse_digit :: (s: string) -> i32 ! { ... } // one value + error +parse :: (s: string) -> Tuple(i32, i64) ! { ... } // multi-value + error 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"; @@ -3141,7 +3162,7 @@ Two forms of error set: ParseErr :: error { BadDigit, Overflow, Empty }; // Inferred set — bare `!` collects whatever tags the body raises. -quick :: () -> (i32, !) { +quick :: () -> i32 ! { if cond raise error.SomeAdHocTag; // mints into the inferred set return 0; } @@ -3228,7 +3249,7 @@ v := parse_digit(s) catch (e) compute_fallback(e); // value-producing body v, n := parse(s) catch (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 @@ -3264,7 +3285,7 @@ the RHS shape decides the result: v := parse_digit(s) or 0; // value terminator → non-failable v := try foo() or try boo(); // chain, propagate if both fail 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 @@ -3339,14 +3360,14 @@ On success exit (fall-through, `return`, `break` / `continue` without an error) it is skipped — only `defer` runs. ```sx -make_handle :: () -> (Handle, !) { +make_handle :: () -> Handle ! { h := try open(); onfail close(h); // close ONLY on a subsequent failure try configure(h); // fails → onfail runs → close(h) return h; // success → onfail skipped; caller owns h } -open :: (path: string) -> (Handle, !) { +open :: (path: string) -> Handle ! { h := try sys_open(path); 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 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. -- **Program-wide union per shape.** All `Closure() -> (T, !)` occurrences +- **Program-wide union per shape.** All `Closure() -> T !` occurrences with the same signature share one inferred-set node; the SCC pass unions every closure flowing into any matching slot. - **FFI boundary.** A failable closure cannot be assigned to a non-failable @@ -3427,8 +3448,9 @@ const_decl = IDENT '::' expr ';' var_decl = IDENT ':=' expr ';' | IDENT ':' type '=' expr ';' | IDENT ':' type ';' -fn_decl = IDENT '::' '(' params? ')' ('->' type)? block +fn_decl = IDENT '::' '(' params? ')' ('->' ret_type)? block | IDENT '::' block +ret_type = type ('!' IDENT?)? // trailing `!` = failable; channel outside any Tuple enum_decl = IDENT '::' 'enum' '{' (IDENT ';')* '}' struct_decl = IDENT '::' 'struct' '{' struct_member* '}' 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))? unary = ('-' | '!' | 'xx' | 'try' | 'cast' '(' type ')') 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 | '---' | '.' 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 = 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 block ('else' block)? match_expr = 'if' expr '==' '{' case_arm* else_arm? '}' @@ -3475,8 +3501,12 @@ lambda = '(' params? ')' ('->' type)? '=>' expr args = expr (',' expr)* ','? type = '$' IDENT | 'i32' | 'f32' | 'f64' | 'bool' | 'string' | '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`) +tuple_type_list = tuple_type_elem (',' tuple_type_elem)* ','? +tuple_type_elem = IDENT ':' type | '..' type | type ``` ---