feat: tuple syntax cutover — Tuple(...) type + .(...) value

Replace the bare-paren tuple grammar with explicit, position-unambiguous
forms, mirroring how structs work:

  type     `(A, B)`        -> `Tuple(A, B)`          (named keeps `:`)
  value    `(a, b)`        -> `.(a, b)`              (named uses `=`)
  typed    (new)           -> `Tuple(A, B).(a, b)`   (like `Point.{...}`)
  failable `-> (T, !)`     -> `-> T !`
           `-> (T1, T2, !)`-> `-> Tuple(T1, T2) !`   (channel outside 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, and match bindings are unaffected.

`Tuple(...)` is strictly a TYPE in every position (including `size_of` /
`type_info` args); a tuple VALUE comes only from `.(...)` (anonymous) or
`Tuple(...).(...)` (explicitly typed). A bare `Tuple(1, 2)` is a tuple
type with non-type elements -> rejected.

The ~110 tuple-bearing corpus files were migrated with a one-shot
AST-aware migrator (the `sx migrate` tool from the prior commit, removed
here). New examples: 0130 (new syntax), 0131 (typed construction), 1060
(named-tuple failable return). 1116 golden updated for the new hint text.
This commit is contained in:
agra
2026-06-25 17:53:57 +03:00
parent c882c6c63e
commit 989e18b760
124 changed files with 941 additions and 1236 deletions

View File

@@ -23,7 +23,7 @@ main :: () -> i32 {
print("size_of((i32)->i32) = {}\n", size_of((i32) -> i32));
// Tuple literal reinterpreted as tuple type at the type-demanding site.
print("size_of((i32, i32)) = {}\n", size_of((i32, i32)));
print("size_of((i32, i32)) = {}\n", size_of(Tuple(i32, i32)));
// Aliases.
print("size_of(Ptr) = {}\n", size_of(Ptr));

View File

@@ -6,18 +6,18 @@
#import "modules/std.sx";
Box :: struct { xs: (i32, i32); }
Box :: struct { xs: Tuple(i32, i32); }
swap :: (a: i64, b: i64) -> (i64, i64) { (b, a) }
fst :: (t: (i64, i64)) -> i64 { t.0 }
swap :: (a: i64, b: i64) -> Tuple(i64, i64) { .(b, a) }
fst :: (t: Tuple(i64, i64)) -> i64 { t.0 }
main :: () -> i32 {
// Inferred positional tuple + numeric field access.
pair := (40, 2);
pair := .(40, 2);
print("pair {} {}\n", pair.0, pair.1);
// Named tuple: named + numeric access.
named := (x: 10, y: 20);
named := .(x = 10, y = 20);
print("named {} {} {}\n", named.x, named.0, named.1);
// Element into a typed local (access path, not just print).
@@ -27,7 +27,7 @@ main :: () -> i32 {
// Tuple-typed struct field: store a tuple value, read both elements.
box : Box = ---;
box.xs = (7, 9);
box.xs = .(7, 9);
print("field {} {}\n", box.xs.0, box.xs.1);
// Return a tuple from a function.
@@ -35,21 +35,21 @@ main :: () -> i32 {
print("ret {} {}\n", s.0, s.1);
// Pass a tuple by value.
print("pass {}\n", fst((11, 22)));
print("pass {}\n", fst(.(11, 22)));
// Operators: equality, concatenation, repetition, membership, lex.
print("eq {}\n", (1, 2) == (1, 2));
c := (1, 2) + (3, 4);
print("eq {}\n", .(1, 2) == .(1, 2));
c := .(1, 2) + .(3, 4);
print("concat {} {}\n", c.0, c.3);
r := (1, 2) * 3;
r := .(1, 2) * 3;
print("rep {} {}\n", r.0, r.5);
print("mem {}\n", 3 in (1, 2, 3));
print("lex {}\n", (1, 2) < (1, 3));
print("mem {}\n", 3 in .(1, 2, 3));
print("lex {}\n", .(1, 2) < .(1, 3));
// Mixed-size fields: a tuple with both an i64 and a string (16-byte fat
// pointer). Field types are tracked per-position, so reading each back is
// typed correctly (i64 prints as a number, string as text).
mixed := (42, "hi");
mixed := .(42, "hi");
print("mixed {} {}\n", mixed.0, mixed.1);
0
}

View File

@@ -9,13 +9,13 @@
main :: () -> i32 {
// Positional element assignment.
a : (i32, string) = ---;
a : Tuple(i32, string) = ---;
a.0 = 11;
a.1 = "x";
print("a: {} {}\n", a.0, a.1);
// Named tuple: write + read by name, and read by position.
p : (x: i32, y: string) = ---;
p : Tuple(x: i32, y: string) = ---;
p.x = 22;
p.y = "y";
print("p: x={} y={} .0={}\n", p.x, p.y, p.0);

View File

@@ -81,26 +81,26 @@ main :: () {
// Basic tuple destructuring
{
da, db := (10, 20);
da, db := .(10, 20);
print("basic: {} {}\n", da, db);
}
// Destructure from function return
{
dswap :: (a: i64, b: i64) -> (i64, i64) { (b, a) }
dswap :: (a: i64, b: i64) -> Tuple(i64, i64) { .(b, a) }
dx, dy := dswap(1, 2);
print("fn: {} {}\n", dx, dy);
}
// Discard with _
{
_, dsecond := (100, 200);
_, dsecond := .(100, 200);
print("discard: {}\n", dsecond);
}
// Three elements
{
da3, db3, dc3 := (1, 2, 3);
da3, db3, dc3 := .(1, 2, 3);
print("triple: {} {} {}\n", da3, db3, dc3);
}
}

View File

@@ -9,18 +9,18 @@ main :: () {
// --- Tuples ---
{
print("=== Tuples ===\n");
pair := (40, 2);
pair := .(40, 2);
print("{}\n", pair.0);
print("{}\n", pair.1);
named := (x: 10, y: 20);
named := .(x = 10, y = 20);
print("{}\n", named.x);
print("{}\n", named.0);
single := (42,);
single := .(42);
print("{}\n", single.0);
zeroed : (i32, i32) = ---;
zeroed : Tuple(i32, i32) = ---;
print("{}\n", zeroed.0);
print("{}\n", zeroed.1);
}

View File

@@ -44,20 +44,20 @@ main :: () {
print("=== Tuple Operators ===\n");
// Equality
print("{}\n", (1, 2) == (1, 2)); // true
print("{}\n", (1, 2) == (1, 3)); // false
print("{}\n", (1, 2) != (1, 3)); // true
print("{}\n", (1, 2) != (1, 2)); // false
print("{}\n", .(1, 2) == .(1, 2)); // true
print("{}\n", .(1, 2) == .(1, 3)); // false
print("{}\n", .(1, 2) != .(1, 3)); // true
print("{}\n", .(1, 2) != .(1, 2)); // false
// Concatenation
c := (1, 2) + (3, 4);
c := .(1, 2) + .(3, 4);
print("{}\n", c.0); // 1
print("{}\n", c.1); // 2
print("{}\n", c.2); // 3
print("{}\n", c.3); // 4
// Repetition
r := (1, 2) * 3;
r := .(1, 2) * 3;
print("{}\n", r.0); // 1
print("{}\n", r.1); // 2
print("{}\n", r.2); // 1
@@ -66,16 +66,16 @@ main :: () {
print("{}\n", r.5); // 2
// Lexicographic comparison
print("{}\n", (1, 2) < (1, 3)); // true
print("{}\n", (1, 3) < (1, 2)); // false
print("{}\n", (1, 2) < (1, 2)); // false
print("{}\n", (1, 2) <= (1, 2)); // true
print("{}\n", (2, 0) > (1, 9)); // true
print("{}\n", (1, 2) >= (1, 2)); // true
print("{}\n", .(1, 2) < .(1, 3)); // true
print("{}\n", .(1, 3) < .(1, 2)); // false
print("{}\n", .(1, 2) < .(1, 2)); // false
print("{}\n", .(1, 2) <= .(1, 2)); // true
print("{}\n", .(2, 0) > .(1, 9)); // true
print("{}\n", .(1, 2) >= .(1, 2)); // true
// Membership
print("{}\n", 2 in (1, 2, 3)); // true
print("{}\n", 5 in (1, 2, 3)); // false
print("{}\n", 2 in .(1, 2, 3)); // true
print("{}\n", 5 in .(1, 2, 3)); // false
}
// --- Directory imports ---
@@ -189,15 +189,15 @@ main :: () {
}
{
if 1 == (1,) {
if 1 == .(1) {
print("1 == (1)\n");
}
if (1,) == (1) {
if .(1) == (1) {
print("(1) == 1\n");
}
if (1,) == 1 {
if .(1) == 1 {
print("1 == 1\n");
}
}

View File

@@ -0,0 +1,42 @@
// New tuple syntax (additive over the legacy `(a, b)` forms):
// - tuple TYPE `Tuple(A, B)` and named `Tuple(x: A, y: B)`
// - tuple VALUE `.(a, b)`, named `.(x = a, y = b)`, 1-tuple `.(n)`
// - element access by index `.0` and by name `.x`
// - a `-> Tuple(i64, i64)` return type with a `.(b, a)` body
// - tuple equality operator over two `.(...)` literals
// The `Tuple(...)` type mirrors the inline `(A, B)` tuple_type_expr and
// `.(...)` mirrors the inline `(a, b)` tuple_literal, so both self-type
// structurally and reuse the existing tuple lowering.
#import "modules/std.sx";
swap :: (a: i64, b: i64) -> Tuple(i64, i64) {
.(b, a)
}
main :: () -> i32 {
// Positional value + index access.
p := .(1, 2);
print("p {} {}\n", p.0, p.1);
// Named value (`=`) + name access.
n := .(x = 10, y = 20);
print("n {} {}\n", n.x, n.y);
// 1-tuple.
one := .(7);
print("one {}\n", one.0);
// Tuple return type with a `.(...)` body.
s := swap(3, 4);
print("swap {} {}\n", s.0, s.1);
// Named tuple TYPE annotation, filled by a named `.(...)` literal.
nt : Tuple(x: i64, y: i64) = .(x = 5, y = 6);
print("named-type {} {}\n", nt.x, nt.y);
// Tuple equality operator over two `.(...)` literals.
print("eq {}\n", .(1, 2) == .(1, 2));
0
}

View File

@@ -0,0 +1,39 @@
// Explicitly-typed tuple construction `Tuple(...).( ... )` — the `Tuple(...)`
// TYPE followed by a `.( ... )` initializer, exactly like `Point.{ ... }` for
// structs. Symmetric trio (mirrors structs `Point` / `Point.{...}` / `.{...}`):
// - tuple TYPE `Tuple(A, B)` (annotation / return / arg)
// - anonymous VALUE `.(a, b)` (contextually typed)
// - typed VALUE `Tuple(A, B).(a, b)` (explicit type + initializer)
// A `Tuple(...).(...)` value equals the anonymous `.(...)` against that type.
// Named forms keep `:` in the type and `=` in the value.
#import "modules/std.sx";
// A `-> Tuple(i64, i64)` return type with a `.(b, a)` body.
swap :: (a: i64, b: i64) -> Tuple(i64, i64) {
.(b, a)
}
main :: () -> i32 {
// Annotation + anonymous value.
t : Tuple(i64, i64) = .(1, 2);
print("t = {} {}\n", t.0, t.1); // t = 1 2
// Explicitly-typed construction — same value as `.(3, 4)` against the type.
u := Tuple(i64, i64).(3, 4);
print("u = {} {}\n", u.0, u.1); // u = 3 4
// Named: annotation + value uses `=` for the value fields.
p : Tuple(x: i64, y: i64) = .(x = 5, y = 6);
print("p = {} {}\n", p.x, p.y); // p = 5 6
// Named: explicitly-typed construction.
q := Tuple(x: i64, y: i64).(x = 7, y = 8);
print("q = {} {}\n", q.x, q.y); // q = 7 8
// Function returning a tuple via a `.(b, a)` body.
s := swap(10, 20);
print("s = {} {}\n", s.0, s.1); // s = 20 10
0
}

View File

@@ -10,7 +10,7 @@
// Regression (issue 0089 — attempt-2 completeness across binding forms).
#import "modules/std.sx";
pair :: () -> (i64, i64) { (1, 2) }
pair :: () -> Tuple(i64, i64) { .(1, 2) }
maybe :: () -> ?i64 { return 42; }
// Function named with a reserved spelling — bare-callable (no backtick at call).

View File

@@ -27,7 +27,7 @@ big_host :: () -> i32 {
}
d_host :: () -> i32 {
a, b := (1, 2);
a, b := .(1, 2);
print("a: {} b: {}\n", type_name(type_of(a)), type_name(type_of(b)));
0
}

View File

@@ -21,7 +21,7 @@ main :: () -> i32 {
print("tag={}\n", b.tag);
// A tuple with a void element.
t : (void, i32) = .{ {}, 9 };
t : Tuple(void, i32) = .{ {}, 9 };
print("t1={}\n", t.1);
return 0;
}

View File

@@ -11,24 +11,24 @@
main :: () {
// Optional + float fields.
t : (?i64, f64) = .{ 7, 3.0 };
t : Tuple(?i64, f64) = .{ 7, 3.0 };
print("{} {}\n", t.0 ?? -1, t.1); // 7 3.000000
// int -> float coercion on a tuple element.
u : (f64, i64) = .{ 3, 4 };
u : Tuple(f64, i64) = .{ 3, 4 };
print("{} {}\n", u.0, u.1); // 3.000000 4
// Named tuple.
n : (x: ?i64, y: f64) = .{ 5, 2.5 };
n : Tuple(x: ?i64, y: f64) = .{ 5, 2.5 };
print("{} {}\n", n.x ?? -1, n.y); // 5 2.500000
// Variable elements flowing into an optional tuple field.
a := 9;
b := 1.5;
v : (?i64, f64) = .{ a, b };
v : Tuple(?i64, f64) = .{ a, b };
print("{} {}\n", v.0 ?? -1, v.1); // 9 1.500000
// A bare `null` element into an optional tuple field.
w : (?i64, i64) = .{ null, 8 };
w : Tuple(?i64, i64) = .{ null, 8 };
print("{} {}\n", w.0 ?? -1, w.1); // -1 8
}

View File

@@ -29,10 +29,10 @@ main :: () {
print("{}\n", fns[0](3, 4)); // 7
// A 1-tuple type still requires the trailing comma.
one : (i64,) = (9,);
one : Tuple(i64) = .(9);
print("{}\n", one.0); // 9
// A 2-tuple is unaffected.
two : (i64, i64) = (40, 2);
two : Tuple(i64, i64) = .(40, 2);
print("{}\n", two.0 + two.1); // 42
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,6 @@
p 1 2
n 10 20
one 7
swap 4 3
named-type 5 6
eq true

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,5 @@
t = 1 2
u = 3 4
p = 5 6
q = 7 8
s = 20 10