diff --git a/examples/basic/0036-basic-ufcs-aliases.sx b/examples/basic/0036-basic-ufcs-aliases.sx index 4da31770..adc10312 100644 --- a/examples/basic/0036-basic-ufcs-aliases.sx +++ b/examples/basic/0036-basic-ufcs-aliases.sx @@ -28,14 +28,14 @@ main :: () { print("{}\n", 1 |> calc(2, 3, 4)); // same = 3 — pipe UFCS // Tuple return type - swap :: (a: i64, b: i64) -> (i64, i64) { (b, a) } + swap :: (a: i64, b: i64) -> Tuple(i64, i64) { .(b, a) } s := swap(1, 2); a := s.0; b := s.1; print("{}\n", a); // 2 print("{}\n", b); // 1 - wrap :: (x: i64) -> (i64,) { (x,) } // 1-tuple needs trailing comma; (i64) groups + wrap :: (x: i64) -> Tuple(i64) { .(x) } // 1-tuple needs trailing comma; (i64) groups t := wrap(99); print("{}\n", t.0); // 99 } diff --git a/examples/basic/0038-basic-dead-code-after-terminator.sx b/examples/basic/0038-basic-dead-code-after-terminator.sx index bc630e9d..d7ae375e 100644 --- a/examples/basic/0038-basic-dead-code-after-terminator.sx +++ b/examples/basic/0038-basic-dead-code-after-terminator.sx @@ -17,7 +17,7 @@ E :: error { Neg } const_one :: () -> i64 { return 1; return 99; } // dead `return x;` after an unconditional raise (the failable closure shape) -always_raise :: (x: i64) -> (i64, !E) { raise error.Neg; return x; } +always_raise :: (x: i64) -> i64 !E { raise error.Neg; return x; } // guard: a conditional return must still fall through to the trailing return clamp :: (x: i64) -> i64 { if x > 10 { return 10; } return x; } diff --git a/examples/basic/0042-basic-block-value-destructure.sx b/examples/basic/0042-basic-block-value-destructure.sx index f61d0258..d853394a 100644 --- a/examples/basic/0042-basic-block-value-destructure.sx +++ b/examples/basic/0042-basic-block-value-destructure.sx @@ -8,7 +8,7 @@ #import "modules/std.sx"; -pair :: () -> (i32, i32) { (5, 7) } +pair :: () -> Tuple(i32, i32) { .(5, 7) } main :: () -> i32 { // destructure decl inside a value-bound block diff --git a/examples/comptime/0623-comptime-metatype-tuple.sx b/examples/comptime/0623-comptime-metatype-tuple.sx index 9f7ce5de..88f47ccb 100644 --- a/examples/comptime/0623-comptime-metatype-tuple.sx +++ b/examples/comptime/0623-comptime-metatype-tuple.sx @@ -11,7 +11,7 @@ Pair :: define(declare("Pair"), .tuple(.{ elements = .[ i64, f64 ] })); -TripleCopy :: define(declare("TripleCopy"), type_info((i64, bool, f64))); +TripleCopy :: define(declare("TripleCopy"), type_info(Tuple(i64, bool, f64))); main :: () -> i32 { p : Pair = .{ 3, 2.5 }; diff --git a/examples/diagnostics/1101-diagnostics-err-tuple-oob.sx b/examples/diagnostics/1101-diagnostics-err-tuple-oob.sx index 3c182d56..8a71eada 100644 --- a/examples/diagnostics/1101-diagnostics-err-tuple-oob.sx +++ b/examples/diagnostics/1101-diagnostics-err-tuple-oob.sx @@ -2,6 +2,6 @@ // `error: field 'N' not found on type '(i64, i64)'` diagnostic and exit 1. main :: () -> i32 { - t := (10, 20); + t := .(10, 20); return xx t.42; } 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 125baec6..06b94955 100644 --- a/examples/diagnostics/1116-diagnostics-tuple-type-nontype-element-rejected.sx +++ b/examples/diagnostics/1116-diagnostics-tuple-type-nontype-element-rejected.sx @@ -8,6 +8,6 @@ #import "modules/std.sx"; main :: () -> i32 { - print("bad tuple type size = {}\n", size_of((i32, 1))); + print("bad tuple type size = {}\n", size_of(Tuple(i32, 1))); 0 } diff --git a/examples/diagnostics/1121-diagnostics-reserved-name-control-flow.sx b/examples/diagnostics/1121-diagnostics-reserved-name-control-flow.sx index d716396f..46ef097e 100644 --- a/examples/diagnostics/1121-diagnostics-reserved-name-control-flow.sx +++ b/examples/diagnostics/1121-diagnostics-reserved-name-control-flow.sx @@ -11,7 +11,7 @@ // offending name; exit 1 — NOT an LLVM verifier abort. #import "modules/std.sx"; -pair :: () -> (i64, i64) { (1, 2) } +pair :: () -> Tuple(i64, i64) { .(1, 2) } maybe :: () -> ?i64 { return null; } main :: () -> i32 { diff --git a/examples/diagnostics/1124-diagnostics-imported-reserved-destructure/mod.sx b/examples/diagnostics/1124-diagnostics-imported-reserved-destructure/mod.sx index 5ea960c0..02e4f513 100644 --- a/examples/diagnostics/1124-diagnostics-imported-reserved-destructure/mod.sx +++ b/examples/diagnostics/1124-diagnostics-imported-reserved-destructure/mod.sx @@ -1,6 +1,6 @@ #import "modules/std.sx"; -pair :: () -> (i64, i64) { (1, 2) } +pair :: () -> Tuple(i64, i64) { .(1, 2) } run :: () -> i32 { i2, rest := pair(); // destructure name in an IMPORTED module diff --git a/examples/diagnostics/1157-diagnostics-catch-binding-needs-parens.sx b/examples/diagnostics/1157-diagnostics-catch-binding-needs-parens.sx index 4eb3ee02..4963efb1 100644 --- a/examples/diagnostics/1157-diagnostics-catch-binding-needs-parens.sx +++ b/examples/diagnostics/1157-diagnostics-catch-binding-needs-parens.sx @@ -6,7 +6,7 @@ E :: error { Bad }; -f :: () -> (i64, !E) { raise error.Bad; } +f :: () -> i64 !E { raise error.Bad; } main :: () { v := f() catch e { 0 }; diff --git a/examples/diagnostics/1202-diagnostics-null-coalesce-tuple-default.sx b/examples/diagnostics/1202-diagnostics-null-coalesce-tuple-default.sx index 61683bef..411de85d 100644 --- a/examples/diagnostics/1202-diagnostics-null-coalesce-tuple-default.sx +++ b/examples/diagnostics/1202-diagnostics-null-coalesce-tuple-default.sx @@ -8,7 +8,7 @@ #import "modules/std.sx"; main :: () { - o : ?(i32,) = null; + o : ?Tuple(i32) = null; x := o ?? 5; // default 'i64' vs payload '(i32,)' -> diagnostic print("{}\n", x.0); } diff --git a/examples/diagnostics/expected/1116-diagnostics-tuple-type-nontype-element-rejected.stderr b/examples/diagnostics/expected/1116-diagnostics-tuple-type-nontype-element-rejected.stderr index a89a5432..8ac69c86 100644 --- a/examples/diagnostics/expected/1116-diagnostics-tuple-type-nontype-element-rejected.stderr +++ b/examples/diagnostics/expected/1116-diagnostics-tuple-type-nontype-element-rejected.stderr @@ -1,5 +1,5 @@ -error: tuple type element is not a type (found `int_literal`); a tuple used as a type must list only types, e.g. `(i32, i32)` - --> examples/diagnostics/1116-diagnostics-tuple-type-nontype-element-rejected.sx:11:55 +error: tuple type element is not a type (found `int_literal`); a tuple used as a type must list only types, e.g. `Tuple(i32, i32)` + --> examples/diagnostics/1116-diagnostics-tuple-type-nontype-element-rejected.sx:11:60 | -11 | print("bad tuple type size = {}\n", size_of((i32, 1))); - | ^ +11 | print("bad tuple type size = {}\n", size_of(Tuple(i32, 1))); + | ^ diff --git a/examples/errors/1011-errors-value-failable.sx b/examples/errors/1011-errors-value-failable.sx index e24f8d5e..8fbe3c2f 100644 --- a/examples/errors/1011-errors-value-failable.sx +++ b/examples/errors/1011-errors-value-failable.sx @@ -10,7 +10,7 @@ E :: error { Bad, Empty } -parse :: (n: i32) -> (i32, !E) { +parse :: (n: i32) -> i32 !E { if n < 0 { raise error.Bad; } if n == 0 { raise error.Empty; } return n * 10; // success → {n*10, 0} diff --git a/examples/errors/1012-errors-value-failable-consume.sx b/examples/errors/1012-errors-value-failable-consume.sx index be81a50e..8ef34fb6 100644 --- a/examples/errors/1012-errors-value-failable-consume.sx +++ b/examples/errors/1012-errors-value-failable-consume.sx @@ -10,14 +10,14 @@ E :: error { Bad, Empty } -parse :: (n: i32) -> (i32, !E) { +parse :: (n: i32) -> i32 !E { if n < 0 { raise error.Bad; } if n == 0 { raise error.Empty; } return n * 2; } // value-carrying `try` in a value-carrying caller — propagates {undef, tag}. -inc :: (n: i32) -> (i32, !E) { +inc :: (n: i32) -> i32 !E { v := try parse(n); return v + 1; } diff --git a/examples/errors/1013-errors-value-failable-reject.sx b/examples/errors/1013-errors-value-failable-reject.sx index 68145f72..98f80e14 100644 --- a/examples/errors/1013-errors-value-failable-reject.sx +++ b/examples/errors/1013-errors-value-failable-reject.sx @@ -8,7 +8,7 @@ E :: error { Bad } -parse :: (n: i32) -> (i32, !E) { +parse :: (n: i32) -> i32 !E { if n < 0 { raise error.Bad; } return n; } diff --git a/examples/errors/1014-errors-failable-or.sx b/examples/errors/1014-errors-failable-or.sx index 6ca41625..6860668f 100644 --- a/examples/errors/1014-errors-failable-or.sx +++ b/examples/errors/1014-errors-failable-or.sx @@ -9,7 +9,7 @@ E :: error { Bad, Empty } -parse :: (n: i32) -> (i32, !E) { +parse :: (n: i32) -> i32 !E { if n < 0 { raise error.Bad; } if n == 0 { raise error.Empty; } return n * 2; diff --git a/examples/errors/1018-errors-multi-value-failable.sx b/examples/errors/1018-errors-multi-value-failable.sx index 646e7fe4..4d486f1c 100644 --- a/examples/errors/1018-errors-multi-value-failable.sx +++ b/examples/errors/1018-errors-multi-value-failable.sx @@ -12,37 +12,37 @@ E :: error { Bad, Empty } -parse :: (n: i32) -> (i32, i32, !E) { +parse :: (n: i32) -> Tuple(i32, i32) !E { if n < 0 { raise error.Bad; } if n == 0 { raise error.Empty; } - return (n * 2, n + 1); // success → {n*2, n+1, 0} + return .(n * 2, n + 1); // success → {n*2, n+1, 0} } // Multi-value `try` in a multi-value caller — propagates {undef, undef, tag}. -inc :: (n: i32) -> (i32, i32, !E) { +inc :: (n: i32) -> Tuple(i32, i32) !E { v, b := try parse(n); - return (v + 1, b + 1); + return .(v + 1, b + 1); } // Multi-value `catch`, bare-expression tuple fallback (absorbs the failure). safe :: (n: i32) -> i32 { - v, b := parse(n) catch (e) (40, 50); + v, b := parse(n) catch (e) .(40, 50); return v + b; } // Multi-value `catch` match-body — per-tag dispatch, each arm a value-tuple. classify :: (n: i32) -> i32 { v, b := parse(n) catch (e) == { - case .Bad: (1, 1); - case .Empty: (2, 2); - else: (9, 9); + case .Bad: .(1, 1); + case .Empty: .(2, 2); + else: .(9, 9); }; return v + b; } // Multi-value `or (tuple)` value-terminator (absorbs the failure). ortest :: (n: i32) -> i32 { - v, b := parse(n) or (7, 8); + v, b := parse(n) or .(7, 8); return v + b; } diff --git a/examples/errors/1019-errors-failable-discard-reject.sx b/examples/errors/1019-errors-failable-discard-reject.sx index e1ba307d..eb37bf73 100644 --- a/examples/errors/1019-errors-failable-discard-reject.sx +++ b/examples/errors/1019-errors-failable-discard-reject.sx @@ -13,12 +13,12 @@ E :: error { Bad, Empty } -pair :: (n: i32) -> (i32, i32, !E) { +pair :: (n: i32) -> Tuple(i32, i32) !E { if n < 0 { raise error.Bad; } - return (n, n + 1); + return .(n, n + 1); } -parse :: (n: i32) -> (i32, !E) { +parse :: (n: i32) -> i32 !E { if n < 0 { raise error.Bad; } return n * 2; } diff --git a/examples/errors/1023-errors-tag-interpolation.sx b/examples/errors/1023-errors-tag-interpolation.sx index 908746d7..3464ead7 100644 --- a/examples/errors/1023-errors-tag-interpolation.sx +++ b/examples/errors/1023-errors-tag-interpolation.sx @@ -8,7 +8,7 @@ E :: error { BadDigit, Empty, Overflow } -parse :: (n: i32) -> (i32, !E) { +parse :: (n: i32) -> i32 !E { if n < 0 { raise error.BadDigit; } if n == 0 { raise error.Empty; } return n * 2; diff --git a/examples/errors/1026-errors-failable-main.sx b/examples/errors/1026-errors-failable-main.sx index 79ec4d46..e4e595b4 100644 --- a/examples/errors/1026-errors-failable-main.sx +++ b/examples/errors/1026-errors-failable-main.sx @@ -14,7 +14,7 @@ ParseErr :: error { Empty, BadDigit }; -inner :: (n: i32) -> (i32, !ParseErr) { +inner :: (n: i32) -> i32 !ParseErr { if n == 0 { raise error.Empty; } // pushes a frame if n < 0 { raise error.BadDigit; } return n * 2; diff --git a/examples/errors/1027-errors-failable-main-value.sx b/examples/errors/1027-errors-failable-main-value.sx index cdab14a1..58a4608c 100644 --- a/examples/errors/1027-errors-failable-main-value.sx +++ b/examples/errors/1027-errors-failable-main-value.sx @@ -9,13 +9,13 @@ ParseErr :: error { Empty, BadDigit }; -inner :: (n: i32) -> (i32, !ParseErr) { +inner :: (n: i32) -> i32 !ParseErr { if n == 0 { raise error.Empty; } if n < 0 { raise error.BadDigit; } return n * 2; } -main :: () -> (i32, !ParseErr) { +main :: () -> i32 !ParseErr { v := try inner(32); // succeeds → v = 64 print("v = {}\n", v); return v; // success → exit code 64 diff --git a/examples/errors/1028-errors-failable-or-chain.sx b/examples/errors/1028-errors-failable-or-chain.sx index 6a930528..0c96cb75 100644 --- a/examples/errors/1028-errors-failable-or-chain.sx +++ b/examples/errors/1028-errors-failable-or-chain.sx @@ -12,7 +12,7 @@ E :: error { A, B }; -fa :: (n: i32) -> (i32, !E) { +fa :: (n: i32) -> i32 !E { if n == 0 { raise error.A; } if n < 0 { raise error.B; } return n; @@ -23,7 +23,7 @@ fv :: (n: i32) -> !E { // void (pure) failable return; } -main :: () -> (i32, !E) { +main :: () -> i32 !E { onfail print("onfail fired (BUG)\n"); // must NOT fire — every chain below absorbs r : i32 = 0; diff --git a/examples/errors/1029-errors-failable-or-chain-propagate.sx b/examples/errors/1029-errors-failable-or-chain-propagate.sx index 5668b97b..fa5f4c87 100644 --- a/examples/errors/1029-errors-failable-or-chain-propagate.sx +++ b/examples/errors/1029-errors-failable-or-chain-propagate.sx @@ -10,12 +10,12 @@ E :: error { A }; -fa :: (n: i32) -> (i32, !E) { +fa :: (n: i32) -> i32 !E { if n == 0 { raise error.A; } return n; } -main :: () -> (i32, !E) { +main :: () -> i32 !E { v := try fa(0) or try fa(0) or try fa(0); // all fail → propagate to main return v; } diff --git a/examples/errors/1036-errors-failable-smoke.sx b/examples/errors/1036-errors-failable-smoke.sx index f3348869..e324dbdb 100644 --- a/examples/errors/1036-errors-failable-smoke.sx +++ b/examples/errors/1036-errors-failable-smoke.sx @@ -9,7 +9,7 @@ SmokeErr :: error { Empty, BadDigit, Overflow } // value-carrying, named set -sm_parse :: (n: i32) -> (i32, !SmokeErr) { +sm_parse :: (n: i32) -> i32 !SmokeErr { if n < 0 { raise error.BadDigit; } if n == 0 { raise error.Empty; } if n > 99 { raise error.Overflow; } @@ -23,10 +23,10 @@ sm_check :: (ok: bool) -> ! { } // multi-value, inferred set: `try` propagates; SCC absorbs SmokeErr -sm_pair :: (a: i32, b: i32) -> (i32, i32, !) { +sm_pair :: (a: i32, b: i32) -> Tuple(i32, i32) ! { x := try sm_parse(a); y := try sm_parse(b); - return (x, y); + return .(x, y); } // catch with a diverging block body @@ -38,7 +38,7 @@ sm_or_default :: (n: i32) -> i32 { } // onfail + defer interleave: cleanup runs only on the error path -sm_acquire :: (fail: bool) -> (i32, !) { +sm_acquire :: (fail: bool) -> i32 ! { defer print(" defer A\n"); onfail print(" onfail B\n"); if fail { raise error.Acquire; } @@ -46,7 +46,7 @@ sm_acquire :: (fail: bool) -> (i32, !) { } // or-chain: try a, fall to try b; propagate if both fail -sm_first :: (a: i32, b: i32) -> (i32, !) { +sm_first :: (a: i32, b: i32) -> i32 ! { v := try sm_parse(a) or try sm_parse(b); return v; } @@ -54,18 +54,18 @@ sm_first :: (a: i32, b: i32) -> (i32, !) { // --- Composition (ERR E5.1): failable closures, widening, generics --- // Closure(...) param, try-propagated (the env is carried) -sm_run :: (cb: Closure(i32) -> (i32, !SmokeErr), n: i32) -> (i32, !SmokeErr) { +sm_run :: (cb: Closure(i32) -> i32 !SmokeErr, n: i32) -> i32 !SmokeErr { return try cb(n); } // bare fn-type param: a NON-failable closure literal widens into the failable // slot (the ∅-widening adapter wraps `{value, 0}`) -sm_widen :: (cb: (i32) -> (i32, !SmokeErr), n: i32) -> i32 { +sm_widen :: (cb: (i32) -> i32 !SmokeErr, n: i32) -> i32 { return cb(n) catch (e) -1; } // generic ($T) value-carrying failable composition, monomorphized per call -sm_wrap :: ($T: Type, f: Closure() -> (T, !SmokeErr)) -> (T, !SmokeErr) { +sm_wrap :: ($T: Type, f: Closure() -> T !SmokeErr) -> T !SmokeErr { return try f(); } @@ -105,9 +105,9 @@ main :: () { if !gerr { print("or-chain: {}\n", g); } // multi-value failable consumed by catch (tuple body) - p, q := sm_pair(0, 3) catch (e) (0, 0); + p, q := sm_pair(0, 3) catch (e) .(0, 0); print("pair-catch: {} {}\n", p, q); - p2, q2 := sm_pair(4, 5) catch (e) (0, 0); + p2, q2 := sm_pair(4, 5) catch (e) .(0, 0); print("pair-ok: {} {}\n", p2, q2); // pure failable: absorb with no-binding catch @@ -120,15 +120,15 @@ main :: () { iv, ierr := sm_acquire(false); // composition: inline failable closure literal through a Closure(...) param - cl := sm_run(closure((x: i32) -> (i32, !SmokeErr) { if x < 0 { raise error.BadDigit; } return x * 2; }), 6) catch (e) -1; + cl := sm_run(closure((x: i32) -> i32 !SmokeErr { if x < 0 { raise error.BadDigit; } return x * 2; }), 6) catch (e) -1; print("closure-run: {}\n", cl); // 12 - print("closure-run-err: {}\n", sm_run(closure((x: i32) -> (i32, !SmokeErr) { raise error.Empty; }), 1) catch (e) -9); // -9 + print("closure-run-err: {}\n", sm_run(closure((x: i32) -> i32 !SmokeErr { raise error.Empty; }), 1) catch (e) -9); // -9 // non-failable closure literal widened into the failable bare slot print("widen: {}\n", sm_widen(closure((x: i32) -> i32 => x + 1), 9)); // 10 // generic failable composition (monomorphized at i32) - print("wrap: {}\n", sm_wrap(i32, closure(() -> (i32, !SmokeErr) { return 42; })) catch (e) 0); // 42 + print("wrap: {}\n", sm_wrap(i32, closure(() -> i32 !SmokeErr { return 42; })) catch (e) 0); // 42 print("errors ok\n"); } diff --git a/examples/errors/1037-errors-comptime-run-escape.sx b/examples/errors/1037-errors-comptime-run-escape.sx index c5e6fe86..e959fe26 100644 --- a/examples/errors/1037-errors-comptime-run-escape.sx +++ b/examples/errors/1037-errors-comptime-run-escape.sx @@ -7,7 +7,7 @@ E :: error { Bad, Empty } -parse :: (n: i32) -> (i32, !E) { +parse :: (n: i32) -> i32 !E { if n < 0 { raise error.Bad; } if n == 0 { raise error.Empty; } return n * 2; diff --git a/examples/errors/1038-errors-comptime-run-handled.sx b/examples/errors/1038-errors-comptime-run-handled.sx index 76399b82..e86dc352 100644 --- a/examples/errors/1038-errors-comptime-run-handled.sx +++ b/examples/errors/1038-errors-comptime-run-handled.sx @@ -7,7 +7,7 @@ E :: error { Bad, Empty } -parse :: (n: i32) -> (i32, !E) { +parse :: (n: i32) -> i32 !E { if n < 0 { raise error.Bad; } if n == 0 { raise error.Empty; } return n * 2; diff --git a/examples/errors/1039-errors-failable-closure-literal.sx b/examples/errors/1039-errors-failable-closure-literal.sx index ac3b061d..f4510802 100644 --- a/examples/errors/1039-errors-failable-closure-literal.sx +++ b/examples/errors/1039-errors-failable-closure-literal.sx @@ -10,12 +10,12 @@ E :: error { Neg } -runwith :: (cb: Closure(i64) -> (i64, !E), n: i64) -> i64 { return cb(n) catch (e) -1; } +runwith :: (cb: Closure(i64) -> i64 !E, n: i64) -> i64 { return cb(n) catch (e) -1; } main :: () -> i32 { // block-body and arrow-body failable closures, called directly - m := closure((x: i64) -> (i64, !E) { if x < 0 { raise error.Neg; } return x * 2; }); - n := closure((x: i64) -> (i64, !E) => x + 1); + m := closure((x: i64) -> i64 !E { if x < 0 { raise error.Neg; } return x * 2; }); + n := closure((x: i64) -> i64 !E => x + 1); print("{} {} {} {}\n", m(5) catch (e) 0, m(-1) catch (e) 99, m(-1) or 7, n(40) catch (e) 0); // 10 99 7 41 // failable closure passed as a Closure(...) parameter diff --git a/examples/errors/1040-errors-failable-closure-composition.sx b/examples/errors/1040-errors-failable-closure-composition.sx index 6bbbd694..d3934aa6 100644 --- a/examples/errors/1040-errors-failable-closure-composition.sx +++ b/examples/errors/1040-errors-failable-closure-composition.sx @@ -12,21 +12,21 @@ E :: error { Neg } -bare :: (cb: (i64) -> (i64, !E), n: i64) -> i64 { return cb(n) catch (e) -1; } -chain :: (cb: Closure(i64) -> (i64, !E), n: i64) -> (i64, !E) { return try cb(n); } +bare :: (cb: (i64) -> i64 !E, n: i64) -> i64 { return cb(n) catch (e) -1; } +chain :: (cb: Closure(i64) -> i64 !E, n: i64) -> i64 !E { return try cb(n); } -dbl :: (x: i64) -> (i64, !E) { if x < 0 { raise error.Neg; } return x * 2; } +dbl :: (x: i64) -> i64 !E { if x < 0 { raise error.Neg; } return x * 2; } main :: () -> i32 { // failable closure literal through a bare fn-type param (matching ABI) print("bare ok={} err={}\n", - bare(closure((x: i64) -> (i64, !E) { if x < 0 { raise error.Neg; } return x * 2; }), 5), - bare(closure((x: i64) -> (i64, !E) => x * 2), -1)); // ok=10; err: arrow never raises → cb(-1) = -2 + bare(closure((x: i64) -> i64 !E { if x < 0 { raise error.Neg; } return x * 2; }), 5), + bare(closure((x: i64) -> i64 !E => x * 2), -1)); // ok=10; err: arrow never raises → cb(-1) = -2 // Closure(...) param, try-propagated, then caught at the call site print("chain ok={} err={}\n", - chain(closure((x: i64) -> (i64, !E) => x + 6), 4) catch (e) 0, // 10 - chain(closure((x: i64) -> (i64, !E) { raise error.Neg; }), 1) catch (e) 0); // 0 + chain(closure((x: i64) -> i64 !E => x + 6), 4) catch (e) 0, // 10 + chain(closure((x: i64) -> i64 !E { raise error.Neg; }), 1) catch (e) 0); // 0 // NON-failable closure literal widened into the failable bare slot print("widen={}\n", bare(closure((x: i64) -> i64 => x + 1), 9)); // 10 diff --git a/examples/errors/1041-errors-failable-closure-shape-union.sx b/examples/errors/1041-errors-failable-closure-shape-union.sx index 442fffb5..74046ace 100644 --- a/examples/errors/1041-errors-failable-closure-shape-union.sx +++ b/examples/errors/1041-errors-failable-closure-shape-union.sx @@ -10,7 +10,7 @@ All :: error { Negative, Other } // `h` is a bare-`!` Closure slot; the caller declares the union as `!All`. -dispatch :: (h: Closure(i32) -> (i32, !), x: i32) -> (i32, !All) { +dispatch :: (h: Closure(i32) -> i32 !, x: i32) -> i32 !All { return try h(x); } @@ -19,9 +19,9 @@ main :: () -> i32 { push Context.{ allocator = xx gpa } { // Two literals of the SAME shape raising DIFFERENT tags both feed the // one shared `Closure(i32)->(i32,!)` union node. - handlers : List(Closure(i32) -> (i32, !)) = .{}; - handlers.append(closure((x: i32) -> (i32, !) { if x < 0 { raise error.Negative; } return x * 2; })); - handlers.append(closure((x: i32) -> (i32, !) { if x == 0 { raise error.Other; } return x + 100; })); + handlers : List(Closure(i32) -> i32 !) = .{}; + handlers.append(closure((x: i32) -> i32 ! { if x < 0 { raise error.Negative; } return x * 2; })); + handlers.append(closure((x: i32) -> i32 ! { if x == 0 { raise error.Other; } return x + 100; })); // success paths print("ok0={}\n", dispatch(handlers.items[0], 5) catch (e) 0); // 10 diff --git a/examples/errors/1042-errors-failable-closure-shape-union-reject.sx b/examples/errors/1042-errors-failable-closure-shape-union-reject.sx index 0ead4abb..f9d75f0e 100644 --- a/examples/errors/1042-errors-failable-closure-shape-union-reject.sx +++ b/examples/errors/1042-errors-failable-closure-shape-union-reject.sx @@ -9,16 +9,16 @@ Small :: error { Unrelated } -reject :: (h: Closure(i32) -> (i32, !), x: i32) -> (i32, !Small) { +reject :: (h: Closure(i32) -> i32 !, x: i32) -> i32 !Small { return try h(x); // Negative, Other ∉ Small → two diagnostics } main :: () -> i32 { gpa := GPA.init(); push Context.{ allocator = xx gpa } { - handlers : List(Closure(i32) -> (i32, !)) = .{}; - handlers.append(closure((x: i32) -> (i32, !) { if x < 0 { raise error.Negative; } return x; })); - handlers.append(closure((x: i32) -> (i32, !) { if x == 0 { raise error.Other; } return x; })); + handlers : List(Closure(i32) -> i32 !) = .{}; + handlers.append(closure((x: i32) -> i32 ! { if x < 0 { raise error.Negative; } return x; })); + handlers.append(closure((x: i32) -> i32 ! { if x == 0 { raise error.Other; } return x; })); print("r={}\n", reject(handlers.items[0], 5) catch (e) 0); } return 0; diff --git a/examples/errors/1043-errors-lambda-raise-annotation-hint.sx b/examples/errors/1043-errors-lambda-raise-annotation-hint.sx index 20074adc..095cb8dc 100644 --- a/examples/errors/1043-errors-lambda-raise-annotation-hint.sx +++ b/examples/errors/1043-errors-lambda-raise-annotation-hint.sx @@ -9,7 +9,7 @@ E :: error { Neg } -take :: (cb: Closure(i32) -> (i32, !E), x: i32) -> i32 { return cb(x) catch (e) -1; } +take :: (cb: Closure(i32) -> i32 !E, x: i32) -> i32 { return cb(x) catch (e) -1; } main :: () -> i32 { // `-> i32` (non-failable) but the body raises → lambda-specific hint: diff --git a/examples/errors/1044-errors-generic-failable-composition.sx b/examples/errors/1044-errors-generic-failable-composition.sx index 635e5274..495a0728 100644 --- a/examples/errors/1044-errors-generic-failable-composition.sx +++ b/examples/errors/1044-errors-generic-failable-composition.sx @@ -9,21 +9,21 @@ E :: error { Bad } -wrap :: ($T: Type, f: Closure() -> (T, !E)) -> (T, !E) { return try f(); } +wrap :: ($T: Type, f: Closure() -> T !E) -> T !E { return try f(); } main :: () -> i32 { // success, consumed by catch - print("catch={}\n", wrap(i32, closure(() -> (i32, !E) { return 7; })) catch (e) -1); // 7 + print("catch={}\n", wrap(i32, closure(() -> i32 !E { return 7; })) catch (e) -1); // 7 // success, consumed by destructure (binds value + error slot); the value // slot is read only under an `if !err` guard (ERR E1.8 path-sensitivity) - r, err := wrap(i32, closure(() -> (i32, !E) { return 9; })); + r, err := wrap(i32, closure(() -> i32 !E { return 9; })); if !err { print("destr={} ok=true\n", r); } // destr=9 ok=true // failure path: the raised tag propagates through the generic `try` - print("fail={}\n", wrap(i32, closure(() -> (i32, !E) { raise error.Bad; }) ) catch (e) -1); // -1 + print("fail={}\n", wrap(i32, closure(() -> i32 !E { raise error.Bad; }) ) catch (e) -1); // -1 // a second monomorphization at a different T - print("u8={}\n", wrap(u8, closure(() -> (u8, !E) { return 200; })) catch (e) 0); // 200 + print("u8={}\n", wrap(u8, closure(() -> u8 !E { return 200; })) catch (e) 0); // 200 return 0; } diff --git a/examples/errors/1045-errors-closure-var-bare-slot-reject.sx b/examples/errors/1045-errors-closure-var-bare-slot-reject.sx index c23927ee..8fecfb18 100644 --- a/examples/errors/1045-errors-closure-var-bare-slot-reject.sx +++ b/examples/errors/1045-errors-closure-var-bare-slot-reject.sx @@ -13,7 +13,7 @@ E :: error { Z } bare :: (cb: (i64) -> i64, n: i64) -> i64 { return cb(n); } -baref :: (cb: (i64) -> (i64, !E), n: i64) -> i64 { return cb(n) catch (e) -1; } +baref :: (cb: (i64) -> i64 !E, n: i64) -> i64 { return cb(n) catch (e) -1; } main :: () -> i32 { inc := closure((x: i64) -> i64 => x + 1); // capture-free closure var diff --git a/examples/errors/1046-errors-value-slot-liveness.sx b/examples/errors/1046-errors-value-slot-liveness.sx index 433c3b14..62ca002c 100644 --- a/examples/errors/1046-errors-value-slot-liveness.sx +++ b/examples/errors/1046-errors-value-slot-liveness.sx @@ -15,7 +15,7 @@ E :: error { Bad, Empty } -parse :: (n: i32) -> (i32, !E) { +parse :: (n: i32) -> i32 !E { if n < 0 { raise error.Bad; } if n == 0 { raise error.Empty; } return n * 10; @@ -29,7 +29,7 @@ guarded :: (n: i32) -> i32 { } // `if err { raise }` in a failable function: same fall-through proof. -relay :: (n: i32) -> (i32, !E) { +relay :: (n: i32) -> i32 !E { v, err := parse(n); if err { raise err; } return v + 1; // err proven absent here diff --git a/examples/errors/1047-errors-value-slot-liveness-reject.sx b/examples/errors/1047-errors-value-slot-liveness-reject.sx index 2f91346a..b0cde6aa 100644 --- a/examples/errors/1047-errors-value-slot-liveness-reject.sx +++ b/examples/errors/1047-errors-value-slot-liveness-reject.sx @@ -11,7 +11,7 @@ E :: error { Bad } -parse :: (n: i32) -> (i32, !E) { +parse :: (n: i32) -> i32 !E { if n < 0 { raise error.Bad; } return n * 10; } diff --git a/examples/errors/1048-errors-cleanup-absorption.sx b/examples/errors/1048-errors-cleanup-absorption.sx index d49b6f6a..8f8064cb 100644 --- a/examples/errors/1048-errors-cleanup-absorption.sx +++ b/examples/errors/1048-errors-cleanup-absorption.sx @@ -9,7 +9,7 @@ E :: error { Bad } failing :: () -> !E { raise error.Bad; } -recover :: () -> (i32, !E) { raise error.Bad; } +recover :: () -> i32 !E { raise error.Bad; } work :: (n: i32) -> !E { defer print("defer: always\n"); // plain cleanup diff --git a/examples/errors/1050-errors-defer-block-body.sx b/examples/errors/1050-errors-defer-block-body.sx index b4513158..f20ac955 100644 --- a/examples/errors/1050-errors-defer-block-body.sx +++ b/examples/errors/1050-errors-defer-block-body.sx @@ -9,7 +9,7 @@ E :: error { Bad } -probe :: () -> (i32, !E) { return 21; } +probe :: () -> i32 !E { return 21; } failing :: () -> !E { raise error.Bad; } run :: () { diff --git a/examples/errors/1051-errors-cleanup-closure-boundary.sx b/examples/errors/1051-errors-cleanup-closure-boundary.sx index 1dbc42db..7fbdc4f7 100644 --- a/examples/errors/1051-errors-cleanup-closure-boundary.sx +++ b/examples/errors/1051-errors-cleanup-closure-boundary.sx @@ -24,7 +24,7 @@ E :: error { Bad } failing :: () -> !E { raise error.Bad; } -recover :: () -> (i32, !E) { return 21; } +recover :: () -> i32 !E { return 21; } work :: () { defer { diff --git a/examples/errors/1053-errors-nested-lambda-liveness-reject.sx b/examples/errors/1053-errors-nested-lambda-liveness-reject.sx index 243eb3a3..806b440d 100644 --- a/examples/errors/1053-errors-nested-lambda-liveness-reject.sx +++ b/examples/errors/1053-errors-nested-lambda-liveness-reject.sx @@ -12,7 +12,7 @@ E :: error { Bad } -parse :: (n: i32) -> (i32, !E) { +parse :: (n: i32) -> i32 !E { if n < 0 { raise error.Bad; } return n * 10; } diff --git a/examples/errors/1054-errors-backtick-reserved-binding.sx b/examples/errors/1054-errors-backtick-reserved-binding.sx index 8528e0e7..4ac6a24f 100644 --- a/examples/errors/1054-errors-backtick-reserved-binding.sx +++ b/examples/errors/1054-errors-backtick-reserved-binding.sx @@ -8,7 +8,7 @@ E :: error { Bad, Empty } -parse :: (n: i32) -> (i32, !E) { +parse :: (n: i32) -> i32 !E { if n < 0 { raise error.Bad; } if n == 0 { raise error.Empty; } return n * 2; diff --git a/examples/errors/1055-errors-enum-value-failable-error-slot.sx b/examples/errors/1055-errors-enum-value-failable-error-slot.sx index 6e74c1a3..5b406996 100644 --- a/examples/errors/1055-errors-enum-value-failable-error-slot.sx +++ b/examples/errors/1055-errors-enum-value-failable-error-slot.sx @@ -16,7 +16,7 @@ Color :: enum { red; green; blue; } E :: error { Nope } -pick :: (s: string) -> (Color, !E) { +pick :: (s: string) -> Color !E { if s == "red" { return .red; } if s == "blue" { return .blue; } // non-zero ordinal (2) raise error.Nope; diff --git a/examples/errors/1056-errors-enum-value-failable-tuple-and-comptime.sx b/examples/errors/1056-errors-enum-value-failable-tuple-and-comptime.sx index 7248a413..49a629d0 100644 --- a/examples/errors/1056-errors-enum-value-failable-tuple-and-comptime.sx +++ b/examples/errors/1056-errors-enum-value-failable-tuple-and-comptime.sx @@ -22,13 +22,13 @@ Color :: enum { red; green; blue; } E :: error { Nope } // F1: bare-value success path AND explicit-tuple error path in one function. -classify :: (s: string) -> (Color, !E) { +classify :: (s: string) -> Color !E { if s == "ok" { return .blue; } // bare value → {2, 0} - return (.red, error.Nope); // explicit full tuple → {0, 1} + return .(.red, error.Nope); // explicit full tuple → {0, 1} } // F2: comptime parameter forces inline lowering of the body. -ct_pick :: ($n: i32, s: string) -> (Color, !E) { +ct_pick :: ($n: i32, s: string) -> Color !E { if s == "red" { return .red; } // bare value, inline path → {0, 0} if s == "blue" { return .blue; } // bare value, inline path → {2, 0} raise error.Nope; // inline error path → {undef, 1} diff --git a/examples/errors/1057-errors-negated-error-binding.sx b/examples/errors/1057-errors-negated-error-binding.sx index 3e1ee8bd..e07c8581 100644 --- a/examples/errors/1057-errors-negated-error-binding.sx +++ b/examples/errors/1057-errors-negated-error-binding.sx @@ -7,7 +7,7 @@ E :: error { Boom } -f :: (fail: bool) -> (i64, !E) { +f :: (fail: bool) -> i64 !E { if fail { raise error.Boom; } return 42; } diff --git a/examples/errors/1058-errors-reexport-value-failable-channel/lib.sx b/examples/errors/1058-errors-reexport-value-failable-channel/lib.sx index 848f3c22..75f65d79 100644 --- a/examples/errors/1058-errors-reexport-value-failable-channel/lib.sx +++ b/examples/errors/1058-errors-reexport-value-failable-channel/lib.sx @@ -5,4 +5,4 @@ LE :: error { Bad } Box :: struct ($R: Type) { v: R; } // Returns `($R, !LE)` — a value-failable. `$R` is inferred from the arg. -get :: ufcs (b: *Box($R)) -> ($R, !LE) { return b.v; } +get :: ufcs (b: *Box($R)) -> $R !LE { return b.v; } diff --git a/examples/errors/1060-errors-named-tuple-failable.sx b/examples/errors/1060-errors-named-tuple-failable.sx new file mode 100644 index 00000000..6ec048cd --- /dev/null +++ b/examples/errors/1060-errors-named-tuple-failable.sx @@ -0,0 +1,26 @@ +// A failable function returning a NAMED tuple value `-> Tuple(x: A, y: B) !E` +// flattens its value fields into the result tuple (`{ x: A, y: B, err }`), +// keeping the `.x`/`.y` names addressable on both the success value and the +// `or` fallback. Exercises the success path, a `raise` path, and an +// `or .(x=.., y=..)` terminator. +// +// Regression (issue 0179-adjacent named-tuple-failable miscompile): a named +// failable tuple used to WRAP as `{ {A,B}, err }` while the value-return +// lowering inserted the value slots FLAT, producing invalid LLVM +// (`Invalid InsertValueInst operands`). +#import "modules/std.sx"; + +E :: error { Bad } + +two :: (n: i64) -> Tuple(x: i64, y: i64) !E { + if n < 0 { raise error.Bad; } + return .(x = n, y = n + 1); +} + +main :: () { + ok := two(5) or .(x = 0, y = 0); + print("{} {}\n", ok.x, ok.y); + + bad := two(-1) or .(x = 0, y = 0); + print("{} {}\n", bad.x, bad.y); +} diff --git a/examples/errors/expected/1060-errors-named-tuple-failable.exit b/examples/errors/expected/1060-errors-named-tuple-failable.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/errors/expected/1060-errors-named-tuple-failable.exit @@ -0,0 +1 @@ +0 diff --git a/examples/errors/expected/1060-errors-named-tuple-failable.stderr b/examples/errors/expected/1060-errors-named-tuple-failable.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/errors/expected/1060-errors-named-tuple-failable.stderr @@ -0,0 +1 @@ + diff --git a/examples/errors/expected/1060-errors-named-tuple-failable.stdout b/examples/errors/expected/1060-errors-named-tuple-failable.stdout new file mode 100644 index 00000000..1d38af6e --- /dev/null +++ b/examples/errors/expected/1060-errors-named-tuple-failable.stdout @@ -0,0 +1,2 @@ +5 6 +0 0 diff --git a/examples/packs/0533-packs-pack-tuple-materialize.sx b/examples/packs/0533-packs-pack-tuple-materialize.sx index b2ec35e5..67eca071 100644 --- a/examples/packs/0533-packs-pack-tuple-materialize.sx +++ b/examples/packs/0533-packs-pack-tuple-materialize.sx @@ -14,7 +14,7 @@ impl Box(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; } impl Box(string) for StrCell { get :: (self: *StrCell) -> string => self.s; } snapshot :: (..xs: Box) -> void { - t := (..xs.get); // tuple (i64, string) materialized from the pack + t := .(..xs.get); // tuple (i64, string) materialized from the pack print("0={} 1={}\n", t.0, t.1); } diff --git a/examples/packs/0534-packs-pack-type-projection.sx b/examples/packs/0534-packs-pack-type-projection.sx index 60da85e0..60b80c0a 100644 --- a/examples/packs/0534-packs-pack-type-projection.sx +++ b/examples/packs/0534-packs-pack-type-projection.sx @@ -20,7 +20,7 @@ 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)`. snap :: (..xs: Box) -> void { - t : (..xs.T) = (..xs.get); + t : Tuple(..xs.T) = .(..xs.get); print("0={} 1={}\n", t.0, t.1); } diff --git a/examples/packs/0538-packs-generic-struct-pack-field.sx b/examples/packs/0538-packs-generic-struct-pack-field.sx index 0a34c469..1c94d188 100644 --- a/examples/packs/0538-packs-generic-struct-pack-field.sx +++ b/examples/packs/0538-packs-generic-struct-pack-field.sx @@ -7,19 +7,19 @@ Box :: struct($R: Type, ..$Ts: []Type) { r: $R; - pair: (..$Ts); // tuple of the pack's element types + pair: Tuple(..$Ts); // tuple of the pack's element types } main :: () -> i32 { // Box(i64, i32, string): R=i64, Ts=[i32, string], pair: (i32, string). a : Box(i64, i32, string) = ---; a.r = 7; - a.pair = (42, "hi"); // whole-tuple field store + a.pair = .(42, "hi"); // whole-tuple field store print("a: r={} 0={} 1={}\n", a.r, a.pair.0, a.pair.1); // A different shape → a different per-position tuple field. b : Box(bool, string, bool) = ---; // Ts=[string, bool], pair: (string, bool) - b.pair = ("x", true); + b.pair = .("x", true); print("b: 0={} 1={}\n", b.pair.0, b.pair.1); 0 } diff --git a/examples/packs/0539-packs-combined-pack-field.sx b/examples/packs/0539-packs-combined-pack-field.sx index ad2925a2..8c27fc7e 100644 --- a/examples/packs/0539-packs-combined-pack-field.sx +++ b/examples/packs/0539-packs-combined-pack-field.sx @@ -14,7 +14,7 @@ impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; } impl VL(string) for StrCell { get :: (self: *StrCell) -> string => self.s; } Combined :: struct($R: Type, ..$Ts: []Type) { - sources: (..VL(Ts)); // (VL(T0), VL(T1), …) — tuple of protocol values + sources: Tuple(..VL(Ts)); // (VL(T0), VL(T1), …) — tuple of protocol values value: $R; } @@ -22,7 +22,7 @@ main :: () -> i32 { // Combined(i64, i64, string): R=i64, Ts=[i64, string], // sources: (VL(i64), VL(string)). c : Combined(i64, i64, string) = ---; - c.sources = (xx IntCell.{ v = 10 }, xx StrCell.{ s = "hi" }); + c.sources = .(xx IntCell.{ v = 10 }, xx StrCell.{ s = "hi" }); c.value = 99; print("{} {} {}\n", c.sources.0.get(), c.sources.1.get(), c.value); // 10 hi 99 0 diff --git a/examples/packs/0540-packs-pack-type-arg-spread.sx b/examples/packs/0540-packs-pack-type-arg-spread.sx index ce394182..03507faa 100644 --- a/examples/packs/0540-packs-pack-type-arg-spread.sx +++ b/examples/packs/0540-packs-pack-type-arg-spread.sx @@ -11,7 +11,7 @@ IntCell :: struct { v: i64; } impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; } Combined :: struct($R: Type, ..$Ts: []Type) { - sources: (..VL(Ts)); + sources: Tuple(..VL(Ts)); value: $R; } diff --git a/examples/packs/0541-packs-pack-to-protocol-tuple.sx b/examples/packs/0541-packs-pack-to-protocol-tuple.sx index 752d0dd6..6064b95c 100644 --- a/examples/packs/0541-packs-pack-to-protocol-tuple.sx +++ b/examples/packs/0541-packs-pack-to-protocol-tuple.sx @@ -12,13 +12,13 @@ impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; } impl VL(string) for StrCell { get :: (self: *StrCell) -> string => self.s; } Combined :: struct($R: Type, ..$Ts: []Type) { - sources: (..VL(Ts)); + sources: Tuple(..VL(Ts)); value: $R; } build :: (..sources: VL) -> void { c : Combined(i64, ..sources.T) = ---; - c.sources = (..sources); // pack → tuple, per-element erase + c.sources = .(..sources); // pack → tuple, per-element erase print("{} {}\n", c.sources.0.get(), c.sources.1.get()); } diff --git a/examples/packs/0543-packs-canonical-map.sx b/examples/packs/0543-packs-canonical-map.sx index 5be21fc4..b398dff9 100644 --- a/examples/packs/0543-packs-canonical-map.sx +++ b/examples/packs/0543-packs-canonical-map.sx @@ -17,14 +17,14 @@ IntCell :: struct { v: i64; } impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; } Combined :: struct($R: Type, ..$Ts: []Type) { - sources: (..VL(Ts)); + sources: Tuple(..VL(Ts)); value: $R; } impl VL($R) for Combined($R, ..$Ts) { get :: (self: *Combined) -> $R => self.value; } map :: (mapper: Closure(..sources.T) -> $R, ..sources: VL) -> VL($R) { c : Combined($R, ..sources.T) = ---; - c.sources = (..sources); + c.sources = .(..sources); c.value = mapper(..sources.get); return xx c; } diff --git a/examples/platform/1640-platform-asm-parse.sx b/examples/platform/1640-platform-asm-parse.sx index 288c9968..a2923fb2 100644 --- a/examples/platform/1640-platform-asm-parse.sx +++ b/examples/platform/1640-platform-asm-parse.sx @@ -5,7 +5,7 @@ // `.build`: ir-only on a non-x86 host (the `.ir` snapshot locks the struct // return + `%[name]` rewrite); runs natively on x86_64-linux. See 1647 for a // multi-output example that executes on aarch64. -divmod :: (n: u64, d: u64) -> (quot: u64, rem: u64) { +divmod :: (n: u64, d: u64) -> Tuple(quot: u64, rem: u64) { return asm { "divq %[d]", [quot] "={rax}" -> u64, diff --git a/examples/platform/1647-platform-asm-aarch64-multi.sx b/examples/platform/1647-platform-asm-aarch64-multi.sx index f6e8baf9..b556f14e 100644 --- a/examples/platform/1647-platform-asm-aarch64-multi.sx +++ b/examples/platform/1647-platform-asm-aarch64-multi.sx @@ -3,7 +3,7 @@ // a `(lo, hi)` tuple. The two outputs become an LLVM `{ i64, i64 }` struct = // sx's tuple. aarch64-pinned via `.build`: executes on a matching host (exit // reflects lo+hi), ir-only elsewhere. -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/examples/probes/pack-expansion-parses.sx b/examples/probes/pack-expansion-parses.sx index 3984d64b..532ac683 100644 --- a/examples/probes/pack-expansion-parses.sx +++ b/examples/probes/pack-expansion-parses.sx @@ -7,13 +7,13 @@ // checks are the parser unit tests in src/parser.zig ("parse pack expansion: …"). // 1. Tuple value position — `(..pack)` / `(..pack.field)`: -tv1 :: () => (..xs); -tv2 :: () => (..xs.value); -tv3 :: () => (a, ..xs, b); // mixed positional + spread +tv1 :: () => .(..xs); +tv2 :: () => .(..xs.value); +tv3 :: () => .(a, ..xs, b); // mixed positional + spread // 2. Tuple type position — `(..F(Ts))` / `(..F(Ts.Arg))`: -tt1 :: (x: (..ValueListenable(Ts))) => x; -tt2 :: (x: (..ValueListenable(Ts.Arg))) => x; +tt1 :: (x: Tuple(..ValueListenable(Ts))) => x; +tt2 :: (x: Tuple(..ValueListenable(Ts.Arg))) => x; // 3. Call-arg position — `..pack` / `..pack.field` (reuses spread_expr): ca1 :: () => f(..xs); diff --git a/examples/probes/tuple-baseline.sx b/examples/probes/tuple-baseline.sx index d4977dd2..cec7e2d3 100644 --- a/examples/probes/tuple-baseline.sx +++ b/examples/probes/tuple-baseline.sx @@ -11,35 +11,35 @@ #import "modules/std.sx"; Listenable :: struct { value: i64; } // stand-in element struct -Combined :: struct { sources: (i32, i32); } // tuple-typed field (Decision 2) +Combined :: struct { sources: Tuple(i32, i32); } // tuple-typed field (Decision 2) -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 { // ── Block A — primitives (WORKS) ─────────────────────────────── - pair := (40, 2); // inferred positional + pair := .(40, 2); // inferred positional print("A.idx {} {}\n", pair.0, pair.1); - named := (x: 10, y: 20); // named + numeric access + named := .(x = 10, y = 20); // named + numeric access print("A.named {} {} {}\n", named.x, named.0, named.1); - one := (42,); // 1-tuple + one := .(42); // 1-tuple print("A.one {}\n", one.0); a : i64 = pair.0; // element into typed local print("A.local {}\n", a); // ── Block B — storage in a struct field (WORKS; core of Decision 2) c : Combined = ---; - c.sources = (7, 9); // assign tuple value to field + c.sources = .(7, 9); // assign tuple value to field print("B.field {} {}\n", c.sources.0, c.sources.1); // ── Block C — return / pass / operators (WORKS) ──────────────── s := swap(1, 2); print("C.ret {} {}\n", s.0, s.1); - print("C.pass {}\n", fst((11, 22))); - print("C.eq {}\n", (1, 2) == (1, 2)); - cc := (1, 2) + (3, 4); + print("C.pass {}\n", fst(.(11, 22))); + print("C.eq {}\n", .(1, 2) == .(1, 2)); + cc := .(1, 2) + .(3, 4); print("C.concat {} {}\n", cc.0, cc.3); - print("C.mem {}\n", 3 in (1, 2, 3)); + print("C.mem {}\n", 3 in .(1, 2, 3)); 0 } diff --git a/examples/protocols/0414-protocols-generic-struct-protocol-erase.sx b/examples/protocols/0414-protocols-generic-struct-protocol-erase.sx index 4de4039f..d14ba7a7 100644 --- a/examples/protocols/0414-protocols-generic-struct-protocol-erase.sx +++ b/examples/protocols/0414-protocols-generic-struct-protocol-erase.sx @@ -14,7 +14,7 @@ IntCell :: struct { v: i64; } impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; } Combined :: struct($R: Type, ..$Ts: []Type) { - sources: (..VL(Ts)); + sources: Tuple(..VL(Ts)); value: $R; } impl VL($R) for Combined($R, ..$Ts) { get :: (self: *Combined) -> $R => self.value; } @@ -22,7 +22,7 @@ impl VL($R) for Combined($R, ..$Ts) { get :: (self: *Combined) -> $R => self.val make :: (..sources: VL) -> VL(i64) { c : Combined(i64, ..sources.T) = ---; c.value = 99; - c.sources = (..sources); + c.sources = .(..sources); return xx c; // Combined__i64_i64 -> VL(i64) } diff --git a/examples/protocols/0415-protocols-protocols.sx b/examples/protocols/0415-protocols-protocols.sx index bf88a050..77a039f5 100644 --- a/examples/protocols/0415-protocols-protocols.sx +++ b/examples/protocols/0415-protocols-protocols.sx @@ -141,7 +141,7 @@ SmokeErr :: error { Empty, BadDigit, Overflow } // value-carrying, named set: raise three tags or succeed // value-carrying, named set: raise three tags or succeed -sm_parse :: (n: i32) -> (i32, !SmokeErr) { +sm_parse :: (n: i32) -> i32 !SmokeErr { if n < 0 { raise error.BadDigit; } if n == 0 { raise error.Empty; } if n > 99 { raise error.Overflow; } @@ -159,10 +159,10 @@ sm_check :: (ok: bool) -> ! { // multi-value, inferred set: `try` propagates; the SCC pass absorbs SmokeErr // multi-value, inferred set: `try` propagates; the SCC pass absorbs SmokeErr -sm_pair :: (a: i32, b: i32) -> (i32, i32, !) { +sm_pair :: (a: i32, b: i32) -> Tuple(i32, i32) ! { x := try sm_parse(a); y := try sm_parse(b); - return (x, y); + return .(x, y); } // `catch` block that diverges (logs the tag, then returns a fallback) @@ -178,7 +178,7 @@ sm_or_default :: (n: i32) -> i32 { // `onfail` + `defer` interleave: cleanup runs only on the error path // `onfail` + `defer` interleave: cleanup runs only on the error path -sm_acquire :: (fail: bool) -> (i32, !) { +sm_acquire :: (fail: bool) -> i32 ! { defer print(" smoke defer A\n"); onfail print(" smoke onfail B\n"); if fail { raise error.Acquire; } @@ -188,7 +188,7 @@ sm_acquire :: (fail: bool) -> (i32, !) { // `or`-chain: try a, fall to try b; propagate if both fail // `or`-chain: try a, fall to try b; propagate if both fail -sm_first :: (a: i32, b: i32) -> (i32, !) { +sm_first :: (a: i32, b: i32) -> i32 ! { v := try sm_parse(a) or try sm_parse(b); return v; } diff --git a/examples/protocols/0824-protocols-same-name-method-wrapped-own-wins.sx b/examples/protocols/0824-protocols-same-name-method-wrapped-own-wins.sx index 66ddbe88..de9329cb 100644 --- a/examples/protocols/0824-protocols-same-name-method-wrapped-own-wins.sx +++ b/examples/protocols/0824-protocols-same-name-method-wrapped-own-wins.sx @@ -23,27 +23,27 @@ Provider :: protocol { // discriminating wrapped/compound RETURNS getp :: (self: *Self) -> *Box; geto :: (self: *Self) -> ?Box; - gett :: (self: *Self) -> (Box, Box); + gett :: (self: *Self) -> Tuple(Box, Box); geta :: (self: *Self) -> [2]Box; // routing-only wrapped/compound PARAMS sump :: (self: *Self, p: *Box) -> i32; sumo :: (self: *Self, o: ?Box) -> i32; sums :: (self: *Self, s: []Box) -> i32; suma :: (self: *Self, a: [2]Box) -> i32; - sumt :: (self: *Self, t: (Box, Box)) -> i32; + sumt :: (self: *Self, t: Tuple(Box, Box)) -> i32; sumn :: (self: *Self, n: *?[]Box) -> i32; } impl Provider for Holder { getp :: (self: *Holder) -> *Box { @self.b } geto :: (self: *Holder) -> ?Box { self.b } - gett :: (self: *Holder) -> (Box, Box) { (self.b, self.b) } + gett :: (self: *Holder) -> Tuple(Box, Box) { .(self.b, self.b) } geta :: (self: *Holder) -> [2]Box { r : [2]Box = ---; r[0] = self.b; r[1] = self.b; r } sump :: (self: *Holder, p: *Box) -> i32 { p.m } sumo :: (self: *Holder, o: ?Box) -> i32 { o!.m } sums :: (self: *Holder, s: []Box) -> i32 { s[0].m } suma :: (self: *Holder, a: [2]Box) -> i32 { a[0].m } - sumt :: (self: *Holder, t: (Box, Box)) -> i32 { t.0.m } + sumt :: (self: *Holder, t: Tuple(Box, Box)) -> i32 { t.0.m } sumn :: (self: *Holder, n: *?[]Box) -> i32 { if n == null { 0 } else { 6 } } } @@ -63,7 +63,7 @@ main :: () -> i32 { arr : [2]Box = ---; arr[0].m = 2; arr[1].m = 3; sl : []Box = arr[0..2]; osl : ?[]Box = sl; - tup : (Box, Box) = (one, one); + tup : Tuple(Box, Box) = .(one, one); sp := p.sump(@one); so := p.sumo(one); diff --git a/examples/route/0815-route-all-new-surfaces-ambiguous.sx b/examples/route/0815-route-all-new-surfaces-ambiguous.sx index 98e5f82a..04621a57 100644 --- a/examples/route/0815-route-all-new-surfaces-ambiguous.sx +++ b/examples/route/0815-route-all-new-surfaces-ambiguous.sx @@ -29,7 +29,7 @@ WrapU :: union { b: Box; n: i32; } WrapE :: enum { V: Box; } main :: () -> i32 { - sz := size_of((Box, i32)); + sz := size_of(Tuple(Box, i32)); x : union { b: Box; n: i32 } = ---; 0 } diff --git a/examples/route/0822-route-all-own-wins-surfaces.sx b/examples/route/0822-route-all-own-wins-surfaces.sx index 9121e6fe..69d3ce6e 100644 --- a/examples/route/0822-route-all-own-wins-surfaces.sx +++ b/examples/route/0822-route-all-own-wins-surfaces.sx @@ -23,7 +23,7 @@ main :: () -> i32 { bp : BoxPtr = @own; // tuple element own-wins - t : (Box, i32) = ---; + t : Tuple(Box, i32) = ---; t.0.m = 12; // enum body-builder child own-wins (payload must be main's `Box`) diff --git a/examples/types/0115-types-compound-type-in-expression.sx b/examples/types/0115-types-compound-type-in-expression.sx index 021a6a00..dddbe2a5 100644 --- a/examples/types/0115-types-compound-type-in-expression.sx +++ b/examples/types/0115-types-compound-type-in-expression.sx @@ -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)); diff --git a/examples/types/0119-types-tuple-values.sx b/examples/types/0119-types-tuple-values.sx index a71c3384..a5828be9 100644 --- a/examples/types/0119-types-tuple-values.sx +++ b/examples/types/0119-types-tuple-values.sx @@ -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 } diff --git a/examples/types/0120-types-tuple-element-assign.sx b/examples/types/0120-types-tuple-element-assign.sx index c066d1a9..0de753a8 100644 --- a/examples/types/0120-types-tuple-element-assign.sx +++ b/examples/types/0120-types-tuple-element-assign.sx @@ -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); diff --git a/examples/types/0122-types-flags.sx b/examples/types/0122-types-flags.sx index 5b8220db..7f547a4b 100644 --- a/examples/types/0122-types-flags.sx +++ b/examples/types/0122-types-flags.sx @@ -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); } } diff --git a/examples/types/0128-types-tuples.sx b/examples/types/0128-types-tuples.sx index 92b4d652..61033b46 100644 --- a/examples/types/0128-types-tuples.sx +++ b/examples/types/0128-types-tuples.sx @@ -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); } diff --git a/examples/types/0129-types-tuple-operators.sx b/examples/types/0129-types-tuple-operators.sx index 739a058f..41b02bee 100644 --- a/examples/types/0129-types-tuple-operators.sx +++ b/examples/types/0129-types-tuple-operators.sx @@ -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"); } } diff --git a/examples/types/0130-types-tuple-new-syntax.sx b/examples/types/0130-types-tuple-new-syntax.sx new file mode 100644 index 00000000..d1f58efd --- /dev/null +++ b/examples/types/0130-types-tuple-new-syntax.sx @@ -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 +} diff --git a/examples/types/0131-types-tuple-typed-construction.sx b/examples/types/0131-types-tuple-typed-construction.sx new file mode 100644 index 00000000..2eea9b87 --- /dev/null +++ b/examples/types/0131-types-tuple-typed-construction.sx @@ -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 +} diff --git a/examples/types/0152-types-backtick-control-flow.sx b/examples/types/0152-types-backtick-control-flow.sx index 91ef9fd1..bc86ab6b 100644 --- a/examples/types/0152-types-backtick-control-flow.sx +++ b/examples/types/0152-types-backtick-control-flow.sx @@ -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). diff --git a/examples/types/0173-types-int-literal-default-i64.sx b/examples/types/0173-types-int-literal-default-i64.sx index 68e7e89d..f85007de 100644 --- a/examples/types/0173-types-int-literal-default-i64.sx +++ b/examples/types/0173-types-int-literal-default-i64.sx @@ -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 } diff --git a/examples/types/0190-types-void-struct-field-zero-sized.sx b/examples/types/0190-types-void-struct-field-zero-sized.sx index b8766a6d..c4d2eaaa 100644 --- a/examples/types/0190-types-void-struct-field-zero-sized.sx +++ b/examples/types/0190-types-void-struct-field-zero-sized.sx @@ -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; } diff --git a/examples/types/0199-types-tuple-positional-optional-element.sx b/examples/types/0199-types-tuple-positional-optional-element.sx index 1b74038a..ce7d545f 100644 --- a/examples/types/0199-types-tuple-positional-optional-element.sx +++ b/examples/types/0199-types-tuple-positional-optional-element.sx @@ -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 } diff --git a/examples/types/0201-types-parenthesized-type-grouping.sx b/examples/types/0201-types-parenthesized-type-grouping.sx index b3bbf870..c6d314ee 100644 --- a/examples/types/0201-types-parenthesized-type-grouping.sx +++ b/examples/types/0201-types-parenthesized-type-grouping.sx @@ -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 } diff --git a/examples/types/expected/0130-types-tuple-new-syntax.exit b/examples/types/expected/0130-types-tuple-new-syntax.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/types/expected/0130-types-tuple-new-syntax.exit @@ -0,0 +1 @@ +0 diff --git a/examples/types/expected/0130-types-tuple-new-syntax.stderr b/examples/types/expected/0130-types-tuple-new-syntax.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/types/expected/0130-types-tuple-new-syntax.stderr @@ -0,0 +1 @@ + diff --git a/examples/types/expected/0130-types-tuple-new-syntax.stdout b/examples/types/expected/0130-types-tuple-new-syntax.stdout new file mode 100644 index 00000000..1beb611f --- /dev/null +++ b/examples/types/expected/0130-types-tuple-new-syntax.stdout @@ -0,0 +1,6 @@ +p 1 2 +n 10 20 +one 7 +swap 4 3 +named-type 5 6 +eq true diff --git a/examples/types/expected/0131-types-tuple-typed-construction.exit b/examples/types/expected/0131-types-tuple-typed-construction.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/types/expected/0131-types-tuple-typed-construction.exit @@ -0,0 +1 @@ +0 diff --git a/examples/types/expected/0131-types-tuple-typed-construction.stderr b/examples/types/expected/0131-types-tuple-typed-construction.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/types/expected/0131-types-tuple-typed-construction.stderr @@ -0,0 +1 @@ + diff --git a/examples/types/expected/0131-types-tuple-typed-construction.stdout b/examples/types/expected/0131-types-tuple-typed-construction.stdout new file mode 100644 index 00000000..5e6bf0bb --- /dev/null +++ b/examples/types/expected/0131-types-tuple-typed-construction.stdout @@ -0,0 +1,5 @@ +t = 1 2 +u = 3 4 +p = 5 6 +q = 7 8 +s = 20 10 diff --git a/library/modules/std/cli.sx b/library/modules/std/cli.sx index 11f4c6af..0aeb5b1f 100644 --- a/library/modules/std/cli.sx +++ b/library/modules/std/cli.sx @@ -263,7 +263,7 @@ is_long_flag :: (s: string) -> bool { // Parse `args` (the logical argv) against the `commands` table, writing // the offending token into `diag` on the error path. See the section // header for grammar, failure contract, and heap discipline. -parse :: (args: []string, commands: []Command, diag: *Diag) -> (Parsed, !CliError) { +parse :: (args: []string, commands: []Command, diag: *Diag) -> Parsed !CliError { // ── Dispatch: match (args[0], args[1]) against the command table ── if args.len < 2 { diag.index = if args.len == 0 then -1 else 0; diff --git a/library/modules/std/event.sx b/library/modules/std/event.sx index 1728de80..3270673a 100644 --- a/library/modules/std/event.sx +++ b/library/modules/std/event.sx @@ -52,7 +52,7 @@ Event :: struct { Loop :: struct { kq: i32 = -1; - init :: () -> (Loop, !EventErr) { + init :: () -> Loop !EventErr { q := kqb.kqueue(); if q < 0 { raise error.Init; } return Loop.{ kq = q }; @@ -96,7 +96,7 @@ Loop :: struct { // Fill `out` with ready events, waiting at most `timeout_ms` // (negative = forever). Returns the count; 0 is a timeout. - wait :: (self: *Loop, out: []Event, timeout_ms: i64) -> (i64, !EventErr) { + wait :: (self: *Loop, out: []Event, timeout_ms: i64) -> i64 !EventErr { raw : [64]kqb.Kevent = ---; cap : i64 = 64; if xx out.len < cap { cap = xx out.len; } diff --git a/library/modules/std/http.sx b/library/modules/std/http.sx index 98588388..2430fad5 100644 --- a/library/modules/std/http.sx +++ b/library/modules/std/http.sx @@ -263,7 +263,7 @@ Server :: struct { ctx: usize = 0; ps: *PoolState = null; // non-null iff cfg.thread_pool_count > 0 - init :: (cfg: Config, handler: (*Request, *Response, usize) -> void, ctx: usize) -> (Server, !HttpErr) { + init :: (cfg: Config, handler: (*Request, *Response, usize) -> void, ctx: usize) -> Server !HttpErr { lfd := socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0); if lfd < 0 { raise error.Bind; } one : i32 = 1; diff --git a/library/modules/std/io.sx b/library/modules/std/io.sx index 55b2c764..bc9aa1d3 100644 --- a/library/modules/std/io.sx +++ b/library/modules/std/io.sx @@ -113,7 +113,7 @@ async :: ufcs (io: Io, worker: Closure(..$args) -> $R, ..$args) -> Future($R) { // `await(f)` — value-carrying failable. `.ready` → the result; `.failed` // / `.canceled` → raise the stored / cancellation error. -await :: ufcs (f: *Future($R)) -> ($R, !IoErr) { +await :: ufcs (f: *Future($R)) -> $R !IoErr { if f.canceled.load(.acquire) { raise error.Canceled; } if f.state == .canceled { raise error.Canceled; } if f.state == .failed { raise error.Failed; } diff --git a/library/modules/std/json.sx b/library/modules/std/json.sx index a67635cb..c4afd267 100644 --- a/library/modules/std/json.sx +++ b/library/modules/std/json.sx @@ -321,7 +321,7 @@ write_object :: (obj: Object, sink: *Sink) -> !JsonError { // bytes written. Raises `error.Overflow` if `dst` is too small (the // partial contents of `dst` are then undefined — nothing is truncated // silently). No allocation. -write_to_buffer :: (v: Value, dst: []u8) -> (i64, !JsonError) { +write_to_buffer :: (v: Value, dst: []u8) -> i64 !JsonError { sink := Sink.{ dst = dst }; try write_value(v, @sink); return sink.pos; @@ -386,7 +386,7 @@ JsonParseError :: error { UnexpectedToken, UnexpectedEnd, BadEscape, BadNumber, // Lowercase/uppercase hex nibble value (0..15) of an ASCII byte; a non-hex // byte in a `\uXXXX` escape is a `BadEscape`. -hex_value :: (c: u8) -> (i64, !JsonParseError) { +hex_value :: (c: u8) -> i64 !JsonParseError { if c >= 48 and c <= 57 { return (cast(i64) c) - 48; } // '0'..'9' if c >= 97 and c <= 102 { return (cast(i64) c) - 97 + 10; } // 'a'..'f' if c >= 65 and c <= 70 { return (cast(i64) c) - 65 + 10; } // 'A'..'F' @@ -450,7 +450,7 @@ Parser :: struct { // Read 4 hex digits at `i` (which must lie within [.., end)); returns // the 16-bit value. Fewer than 4 digits before `end` is a BadEscape. - read_hex4 :: (self: *Parser, i: i64, end: i64) -> (i64, !JsonParseError) { + read_hex4 :: (self: *Parser, i: i64, end: i64) -> i64 !JsonParseError { if i + 4 > end { raise error.BadEscape; } v := 0; k := 0; @@ -464,7 +464,7 @@ Parser :: struct { // Decode the escaped string body in [start, end) into `out`, returning // the decoded byte length. Pass 1 (in parse_string) guarantees there is // no dangling backslash, so the byte after every `\` is in range. - decode_into :: (self: *Parser, start: i64, end: i64, out: [*]u8) -> (i64, !JsonParseError) { + decode_into :: (self: *Parser, start: i64, end: i64, out: [*]u8) -> i64 !JsonParseError { di := 0; i := start; while i < end { @@ -511,7 +511,7 @@ Parser :: struct { // a zero-copy VIEW into `src` when the body has no escapes; otherwise // decodes into an `alloc`-ed buffer (bounded by the raw span). `pos` // ends just past the closing quote. - parse_string :: (self: *Parser) -> (string, !JsonParseError) { + parse_string :: (self: *Parser) -> string !JsonParseError { self.pos += 1; // consume opening quote start := self.pos; has_escape := false; @@ -547,7 +547,7 @@ Parser :: struct { // Parse an i64 integer (optional '-', then digits). Rejects leading // zeros, a fraction/exponent tail, and any value outside i64 — all // `BadNumber`. Accumulates in NEGATIVE space so i64 MIN parses exactly. - parse_number :: (self: *Parser) -> (i64, !JsonParseError) { + parse_number :: (self: *Parser) -> i64 !JsonParseError { // i64 bounds, built positionally because |MIN| is not a // representable positive i64 literal. `min_div10` is `MIN / 10` // truncated toward zero (remainder -8) — the digit loop's overflow @@ -585,7 +585,7 @@ Parser :: struct { } // Parse an array starting at '['. Builds an `Array` through `alloc`. - parse_array :: (self: *Parser) -> (Value, !JsonParseError) { + parse_array :: (self: *Parser) -> Value !JsonParseError { self.pos += 1; // consume '[' arr : Array = .{}; self.skip_ws(); @@ -609,7 +609,7 @@ Parser :: struct { // Parse an object starting at '{'. Keys must be strings; insertion // order is preserved (duplicate keys are kept, never merged). - parse_object :: (self: *Parser) -> (Value, !JsonParseError) { + parse_object :: (self: *Parser) -> Value !JsonParseError { self.pos += 1; // consume '{' obj : Object = .{}; self.skip_ws(); @@ -640,7 +640,7 @@ Parser :: struct { } // Parse any single value (after skipping leading whitespace). - parse_value :: (self: *Parser) -> (Value, !JsonParseError) { + parse_value :: (self: *Parser) -> Value !JsonParseError { self.skip_ws(); if self.pos >= self.src.len { raise error.UnexpectedEnd; } c := self.src[self.pos]; @@ -659,7 +659,7 @@ Parser :: struct { // `alloc` for composite nodes and decoded (escaped) strings. Un-escaped // string values are VIEWS into `src` and are valid only while `src` lives. // Trailing non-whitespace after the value raises `error.TrailingGarbage`. -parse :: (src: string, alloc: Allocator) -> (Value, !JsonParseError) { +parse :: (src: string, alloc: Allocator) -> Value !JsonParseError { p := Parser.{ src = src, alloc = alloc }; v := try p.parse_value(); p.skip_ws(); diff --git a/library/modules/std/sched.sx b/library/modules/std/sched.sx index 5f56338d..c2f4b564 100644 --- a/library/modules/std/sched.sx +++ b/library/modules/std/sched.sx @@ -804,7 +804,7 @@ go :: ufcs (self: *Scheduler, work: Closure() -> $R) -> *Task($R) { // Suspend the caller until the task completes; return its value (or raise on // cancel). MUST be called from inside a fiber (so there is a `self.current` to // park) — typically from a fiber spawned via `s.spawn(...)`. -wait :: ufcs (t: *Task($R)) -> ($R, !TaskErr) { +wait :: ufcs (t: *Task($R)) -> $R !TaskErr { if t.canceled != 0 { raise error.Canceled; } if t.state == .pending { // ONE waiter per task (enforced). A `Task` holds a single `waiter` slot; diff --git a/library/modules/std/socket.sx b/library/modules/std/socket.sx index aac317d7..d96710a6 100644 --- a/library/modules/std/socket.sx +++ b/library/modules/std/socket.sx @@ -93,7 +93,7 @@ SockErr :: error { // Accept one pending connection on a nonblocking listener. A connection // that died between queueing and accept (ECONNABORTED) is skipped, not // surfaced — the listener is fine. -accept_nb :: (fd: i32) -> (i32, !SockErr) { +accept_nb :: (fd: i32) -> i32 !SockErr { while true { c := accept(fd, null, null); if c >= 0 { return c; } @@ -107,7 +107,7 @@ accept_nb :: (fd: i32) -> (i32, !SockErr) { // Read up to `cap` bytes. Returns the byte count (> 0); an orderly EOF // or a peer reset is Closed. -read_nb :: (fd: i32, buf: [*]u8, cap: usize) -> (i64, !SockErr) { +read_nb :: (fd: i32, buf: [*]u8, cap: usize) -> i64 !SockErr { while true { n := read(fd, buf, cap); if n > 0 { return xx n; } @@ -123,7 +123,7 @@ read_nb :: (fd: i32, buf: [*]u8, cap: usize) -> (i64, !SockErr) { // Write up to `len` bytes, returning how many the kernel took (possibly // fewer — the caller continues from there on the next writability). -write_nb :: (fd: i32, buf: [*]u8, len: usize) -> (i64, !SockErr) { +write_nb :: (fd: i32, buf: [*]u8, len: usize) -> i64 !SockErr { while true { n := write(fd, buf, len); if n >= 0 { return xx n; } diff --git a/library/modules/std/thread.sx b/library/modules/std/thread.sx index 40e29bf3..7e8de867 100644 --- a/library/modules/std/thread.sx +++ b/library/modules/std/thread.sx @@ -106,7 +106,7 @@ Thread :: struct { // `entry` is the C->sx boundary: abi(.c), fabricates its own // Context before touching default-conv sx code (examples/1636). - spawn :: (entry: (*void) -> *void abi(.c), arg: *void) -> (Thread, !ThreadErr) { + spawn :: (entry: (*void) -> *void abi(.c), arg: *void) -> Thread !ThreadErr { t : Thread = .{}; if pthread_create(@t.handle, null, entry, arg) != 0 { raise error.Spawn; } return t; @@ -144,7 +144,7 @@ Pool :: struct { // Heap-allocate (the pool must never move: workers hold its address, // and it embeds a live mutex), init in place, spawn the workers. - create :: (workers: i64, backlog: i64) -> (*Pool, !ThreadErr) { + create :: (workers: i64, backlog: i64) -> *Pool !ThreadErr { alloc := context.allocator; p : *Pool = xx alloc.alloc_bytes(size_of(Pool)); p.* = Pool.{}; diff --git a/library/modules/ui/animation.sx b/library/modules/ui/animation.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/button.sx b/library/modules/ui/button.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/dock.sx b/library/modules/ui/dock.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/events.sx b/library/modules/ui/events.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/font.sx b/library/modules/ui/font.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/gesture.sx b/library/modules/ui/gesture.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/glyph_cache.sx b/library/modules/ui/glyph_cache.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/image.sx b/library/modules/ui/image.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/label.sx b/library/modules/ui/label.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/layout.sx b/library/modules/ui/layout.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/modifier.sx b/library/modules/ui/modifier.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/pipeline.sx b/library/modules/ui/pipeline.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/render.sx b/library/modules/ui/render.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/renderer.sx b/library/modules/ui/renderer.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/scroll_view.sx b/library/modules/ui/scroll_view.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/stacks.sx b/library/modules/ui/stacks.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/state.sx b/library/modules/ui/state.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/stats_panel.sx b/library/modules/ui/stats_panel.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/types.sx b/library/modules/ui/types.sx old mode 100755 new mode 100644 diff --git a/library/modules/ui/view.sx b/library/modules/ui/view.sx old mode 100755 new mode 100644 diff --git a/library/vendors/sqlite/sqlite.sx b/library/vendors/sqlite/sqlite.sx index d303ea6e..cfbef864 100644 --- a/library/vendors/sqlite/sqlite.sx +++ b/library/vendors/sqlite/sqlite.sx @@ -461,7 +461,7 @@ SqliteStmt :: struct { // ── execution ── // SQLITE_ROW / SQLITE_DONE on success; anything else raises with the // detail left in the connection's errmsg. - step :: (self: *SqliteStmt) -> (i32, !SqliteErr) { + step :: (self: *SqliteStmt) -> i32 !SqliteErr { rc := sqlite3_step(self.handle); if rc != SQLITE_ROW and rc != SQLITE_DONE { raise error.Step; } return rc; @@ -564,7 +564,7 @@ ColumnMeta :: struct { Sqlite :: struct { handle: usize; - open :: (path: string) -> (Sqlite, !SqliteErr) { + open :: (path: string) -> Sqlite !SqliteErr { h : usize = 0; rc := sqlite3_open(to_cstring(path), @h); if rc != SQLITE_OK { @@ -574,7 +574,7 @@ Sqlite :: struct { return Sqlite.{ handle = h }; } - open_v2 :: (path: string, flags: i32) -> (Sqlite, !SqliteErr) { + open_v2 :: (path: string, flags: i32) -> Sqlite !SqliteErr { h : usize = 0; rc := sqlite3_open_v2(to_cstring(path), @h, flags, 0); if rc != SQLITE_OK { @@ -603,7 +603,7 @@ Sqlite :: struct { return; } - prepare :: (self: *Sqlite, sql: string) -> (SqliteStmt, !SqliteErr) { + prepare :: (self: *Sqlite, sql: string) -> SqliteStmt !SqliteErr { sh : usize = 0; rc := sqlite3_prepare_v2(self.handle, sql.ptr, xx sql.len, @sh, 0); if rc != SQLITE_OK { raise error.Prepare; } @@ -611,7 +611,7 @@ Sqlite :: struct { } // prepare with SQLITE_PREPARE_* flags (e.g. PERSISTENT for the // statement cache a storage layer keeps hot). - prepare_v3 :: (self: *Sqlite, sql: string, flags: u32) -> (SqliteStmt, !SqliteErr) { + prepare_v3 :: (self: *Sqlite, sql: string, flags: u32) -> SqliteStmt !SqliteErr { sh : usize = 0; rc := sqlite3_prepare_v3(self.handle, sql.ptr, xx sql.len, flags, @sh, 0); if rc != SQLITE_OK { raise error.Prepare; } @@ -685,7 +685,7 @@ Sqlite :: struct { } // Schema introspection for one column of "main".`table`. - table_column_metadata :: (self: *Sqlite, table: string, column: string) -> (ColumnMeta, !SqliteErr) { + table_column_metadata :: (self: *Sqlite, table: string, column: string) -> ColumnMeta !SqliteErr { dt : usize = 0; cs : usize = 0; nn : i32 = 0; @@ -703,7 +703,7 @@ Sqlite :: struct { // ── serialization ── // The whole "main" database as bytes (a valid database image). - serialize :: (self: *Sqlite) -> (string, !SqliteErr) { + serialize :: (self: *Sqlite) -> string !SqliteErr { size : i64 = 0; p := sqlite3_serialize(self.handle, to_cstring("main"), @size, 0); if p == null { raise error.Serialize; } @@ -734,7 +734,7 @@ Sqlite :: struct { SqliteBlob :: struct { handle: usize; - open :: (db: *Sqlite, table: string, column: string, rowid: i64, writable: bool) -> (SqliteBlob, !SqliteErr) { + open :: (db: *Sqlite, table: string, column: string, rowid: i64, writable: bool) -> SqliteBlob !SqliteErr { h : usize = 0; rc := sqlite3_blob_open(db.handle, to_cstring("main"), to_cstring(table), to_cstring(column), rowid, if writable then 1 else 0, @h); @@ -751,7 +751,7 @@ SqliteBlob :: struct { bytes :: (self: *SqliteBlob) -> i32 { return sqlite3_blob_bytes(self.handle); } - read :: (self: *SqliteBlob, offset: i32, n: i32) -> (string, !SqliteErr) { + read :: (self: *SqliteBlob, offset: i32, n: i32) -> string !SqliteErr { len : i64 = n; raw : [*]u8 = xx context.allocator.alloc_bytes(len + 1); rc := sqlite3_blob_read(self.handle, raw, n, offset); @@ -778,7 +778,7 @@ SqliteBlob :: struct { SqliteBackup :: struct { handle: usize; - init :: (dst: *Sqlite, src: *Sqlite) -> (SqliteBackup, !SqliteErr) { + init :: (dst: *Sqlite, src: *Sqlite) -> SqliteBackup !SqliteErr { h := sqlite3_backup_init(dst.handle, to_cstring("main"), src.handle, to_cstring("main")); if h == 0 { raise error.Backup; } return SqliteBackup.{ handle = h }; diff --git a/src/ast.zig b/src/ast.zig index 02781d53..db6c468d 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -869,6 +869,10 @@ pub const TupleTypeExpr = struct { pub const TupleLiteral = struct { elements: []const TupleElement, + // Explicit tuple type for the `Tuple(...).( ... )` typed-construction form + // (mirrors `StructLiteral.type_expr` for `Name.{ ... }`). null for the + // anonymous, contextually-typed `.( ... )` form. + type_expr: ?*Node = null, }; pub const TupleElement = struct { diff --git a/src/ir/expr_typer.zig b/src/ir/expr_typer.zig index 567646c5..511c05dd 100644 --- a/src/ir/expr_typer.zig +++ b/src/ir/expr_typer.zig @@ -361,6 +361,13 @@ pub const ExprTyper = struct { return self.l.target_type orelse .unresolved; }, .tuple_literal => |tl| { + // Explicitly-typed `Tuple(A, B).( ... )`: the literal's type is + // the carried tuple type (preserves field names for the named + // form), exactly like `Name.{ ... }` infers to `Name`. + if (tl.type_expr) |te| { + const tuple_ty = self.l.resolveTypeWithBindings(te); + if (tuple_ty != .unresolved) return tuple_ty; + } var field_types = std.ArrayList(TypeId).empty; defer field_types.deinit(self.l.alloc); for (tl.elements) |elem| { diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 40c1c731..9930cb7e 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -950,6 +950,38 @@ pub const Lowering = struct { } } } + // A `Tuple(...)` element must denote a TYPE; a VALUE-literal element — + // e.g. the `1` in `Tuple(i32, 1)` — is a user error. Diagnose it loudly + // here (the same message the `.( ... )`-in-type path emits) BEFORE + // `resolveCompound` would intern a tuple carrying an `.unresolved` + // field. Only the unambiguous value literals are rejected: an + // `error_type_expr` element (`-> Tuple(A, B) !` desugaring), names, + // and the structural type shapes are all legitimate tuple elements. + if (node.data == .tuple_type_expr) { + for (node.data.tuple_type_expr.field_types) |ft| { + // A signed numeric literal (`Tuple(i32, -1)`) arrives as a + // `negate` unary over an int/float literal — reject it as the + // literal it wraps, not as a generic non-type. + const probe = if (ft.data == .unary_op and ft.data.unary_op.op == .negate) + ft.data.unary_op.operand + else + ft; + switch (probe.data) { + .int_literal, + .float_literal, + .string_literal, + .bool_literal, + .null_literal, + => { + if (self.diagnostics) |diags| { + diags.addFmt(.err, ft.span, "tuple type element is not a type (found `{s}`); a tuple used as a type must list only types, e.g. `Tuple(i32, i32)`", .{@tagName(probe.data)}); + } + return .unresolved; + }, + else => {}, + } + } + } // Structural type shapes — `*T`, `[*]T`, `[]T`, `?T`, `[N]T`, functions, // PLAIN closures, and PLAIN tuples — are owned by // `TypeResolver.resolveCompound` (A2.3b). Element types recurse through diff --git a/src/ir/lower/error.zig b/src/ir/lower/error.zig index 8987bbac..e482495a 100644 --- a/src/ir/lower/error.zig +++ b/src/ir/lower/error.zig @@ -323,12 +323,20 @@ pub fn buildFailableTuple(self: *Lowering, ret_ty: TypeId, value_refs: []const R /// dropped) for a multi-value one. Callers must pass a value-carrying /// tuple — a pure `-> !`'s success type is `void`, handled separately. pub fn failableSuccessType(self: *Lowering, op_ty: TypeId) TypeId { - const fields = self.module.types.get(op_ty).tuple.fields; + const tup = self.module.types.get(op_ty).tuple; + const fields = tup.fields; const n_vals = fields.len - 1; if (n_vals == 1) return fields[0]; + // Carry the value-field names through, dropping the trailing error-slot + // name, so a named failable tuple `-> Tuple(x: A, y: B) !` yields a value + // type `(x: A, y: B)` whose `.x`/`.y` fields stay addressable. + const succ_names: ?[]const types.StringId = if (tup.names) |ns| + self.alloc.dupe(types.StringId, ns[0..n_vals]) catch unreachable + else + null; return self.module.types.intern(.{ .tuple = .{ .fields = self.alloc.dupe(TypeId, fields[0..n_vals]) catch unreachable, - .names = null, + .names = succ_names, } }); } diff --git a/src/ir/lower/expr.zig b/src/ir/lower/expr.zig index 901dcf9a..3437bcb1 100644 --- a/src/ir/lower/expr.zig +++ b/src/ir/lower/expr.zig @@ -1932,6 +1932,26 @@ pub fn lowerTupleLiteral(self: *Lowering, tl: *const ast.TupleLiteral) Ref { if (elem.value.data == .spread_expr) has_spread = true; } + // Explicitly-typed construction `Tuple(A, B).( ... )`: the literal carries + // its tuple type, exactly like `Name.{ ... }` for structs. Resolve it and + // drive element lowering through it as the target tuple — the produced + // value equals what the anonymous `.( ... )` form yields against that type. + // An ambient contextual `target_type` (annotation / call slot), if present + // and a tuple, is honored over the explicit one only when the explicit type + // fails to resolve; otherwise the explicit type wins. + const saved_explicit_target = self.target_type; + var restore_explicit_target = false; + if (tl.type_expr) |te| { + const tuple_ty = self.resolveTypeWithBindings(te); + if (tuple_ty != .unresolved) { + self.target_type = tuple_ty; + restore_explicit_target = true; + } + } + defer if (restore_explicit_target) { + self.target_type = saved_explicit_target; + }; + // Contextual target tuple field types. Without a spread we require // exact arity (existing behavior); with a spread we index positionally // by output position (so `(..sources)` into a `(VL(T0), …)` field coerces @@ -2771,10 +2791,14 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref { .optional_type_expr, .array_type_expr, .function_type_expr, + .tuple_type_expr, => blk: { const ty = self.resolveTypeWithBindings(node); // The resolver diagnosed any unresolved leaf; don't mint a Type - // value around the failure sentinel. + // value around the failure sentinel. For `Tuple(...)` this is also + // where a standalone `Tuple(1, 2)` value-expression is rejected — + // `resolveTupleTypeWithBindings` diagnoses the non-type element and + // returns `.unresolved`, so no value is fabricated. if (ty == .unresolved) break :blk self.emitError("unknown_expr", node.span); break :blk self.builder.constType(ty); }, diff --git a/src/ir/lower/generic.zig b/src/ir/lower/generic.zig index 68d98290..b09ca097 100644 --- a/src/ir/lower/generic.zig +++ b/src/ir/lower/generic.zig @@ -288,6 +288,7 @@ pub fn isStaticTypeArg(self: *Lowering, node: *const Node) bool { .optional_type_expr, .function_type_expr, .tuple_literal, + .tuple_type_expr, .call, => return true, else => return false, @@ -355,7 +356,7 @@ pub fn resolveTupleLiteralTypeArg(self: *Lowering, node: *const Node) TypeId { for (node.data.tuple_literal.elements) |el| { if (!type_bridge.isTypeShapedAstNode(el.value, &self.module.types)) { if (self.diagnostics) |diags| { - diags.addFmt(.err, el.value.span, "tuple type element is not a type (found `{s}`); a tuple used as a type must list only types, e.g. `(i32, i32)`", .{@tagName(el.value.data)}); + diags.addFmt(.err, el.value.span, "tuple type element is not a type (found `{s}`); a tuple used as a type must list only types, e.g. `Tuple(i32, i32)`", .{@tagName(el.value.data)}); } return .unresolved; } @@ -468,6 +469,7 @@ pub fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId { // `(COnly, i64)`) is rejected exactly as in a normal annotation, instead // of `type_bridge.resolveAstType`'s ungated global lookup (E4). .tuple_literal, + .tuple_type_expr, .pointer_type_expr, .many_pointer_type_expr, .array_type_expr, diff --git a/src/ir/type_resolver.zig b/src/ir/type_resolver.zig index 12afd839..5b04ce89 100644 --- a/src/ir/type_resolver.zig +++ b/src/ir/type_resolver.zig @@ -254,7 +254,18 @@ pub const TypeResolver = struct { for (tt.field_types) |ft| if (ft.data == .spread_expr) break :blk null; var field_ids = std.ArrayList(TypeId).empty; defer field_ids.deinit(table.alloc); - for (tt.field_types) |ft| field_ids.append(table.alloc, inner.resolveInner(ft)) catch return .unresolved; + for (tt.field_types) |ft| { + const fid = inner.resolveInner(ft); + // A non-type tuple element (e.g. the `1` in `Tuple(i32, 1)`) + // resolves to `.unresolved`; never intern a tuple carrying it + // — that bogus type would reach LLVM emission and panic. The + // user-facing diagnostic is emitted by the literal-rejection + // arm in `resolveTypeArg` (lower.zig, the `tuple_type_expr` + // check); here we just refuse to fabricate the type, + // propagating the sentinel up. + if (fid == .unresolved) break :blk .unresolved; + field_ids.append(table.alloc, fid) catch return .unresolved; + } // Preserve field names for a named tuple `(x: T, y: U)` when the // name and field counts agree (so `t.x` resolves). var name_ids: ?[]const StringId = null; diff --git a/src/main.zig b/src/main.zig index f8534b65..ad79863b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -23,13 +23,6 @@ pub fn main(init: std.process.Init) !void { return; } - // `migrate` has its own flag (`--dry-run`) the generic flag loop below would - // reject, so dispatch it here before that loop runs. - if (std.mem.eql(u8, command, "migrate")) { - runMigrate(allocator, io, args[2..]); - return; - } - // Parse flags and positional arguments var input_path: ?[]const u8 = null; var target_config = sx.target.TargetConfig{}; @@ -414,7 +407,6 @@ fn printUsage() void { \\ ir Print LLVM IR to stdout \\ asm Emit assembly (.s) file \\ lsp Start language server (LSP) - \\ migrate Rewrite old tuple syntax to new (`(a,b)`->`.(a,b)`, type `(A,B)`->`Tuple(A,B)`); `--dry-run` prints only the worklist, `--force` emits output despite unmigrated ambiguous sites \\ \\Options: \\ --target Target triple or shorthand: wasm, macos, linux, windows, ios, ios-sim (default: host) @@ -525,72 +517,6 @@ fn compilePipeline(allocator: std.mem.Allocator, io: std.Io, input_path: []const return comp; } -/// `sx migrate [--dry-run] [--force] ` — tuple-syntax migration tool. -/// -/// Without flags: parse-only, rewrite the old tuple syntax, print the migrated -/// source to stdout and any ambiguous-site worklist entries to stderr. A -/// NON-EMPTY worklist is a hard failure (exit 2) — the migration is incomplete, -/// so we do NOT print the rewritten source (which could be redirected over the -/// input, silently shipping half-migrated code) unless `--force` is passed. -/// -/// With `--dry-run`: print ONLY the worklist (to stderr), no rewritten source — -/// so ambiguous sites can be audited first. A non-empty worklist still exits 2. -/// -/// With `--force`: print the rewritten source even when the worklist is -/// non-empty (the ambiguous sites are left in the OLD syntax). Exit is still 2 -/// so a script can detect the partial migration. -fn runMigrate(allocator: std.mem.Allocator, io: std.Io, sub_args: []const []const u8) void { - var dry_run = false; - var force = false; - var input_path: ?[]const u8 = null; - for (sub_args) |a| { - if (std.mem.eql(u8, a, "--dry-run")) { - dry_run = true; - } else if (std.mem.eql(u8, a, "--force")) { - force = true; - } else if (std.mem.startsWith(u8, a, "-")) { - std.debug.print("error: unknown flag '{s}' for migrate\n", .{a}); - std.process.exit(1); - } else { - input_path = a; - } - } - const path = input_path orelse { - std.debug.print("usage: sx migrate [--dry-run] [--force] \n", .{}); - std.process.exit(1); - }; - - const source = readSource(allocator, io, path) catch |err| { - std.debug.print("error: cannot read '{s}': {}\n", .{ path, err }); - std.process.exit(1); - }; - const result = sx.migrate.migrateSource(allocator, io, path, source) catch |err| { - std.debug.print("error: migrate failed for '{s}': {}\n", .{ path, err }); - std.process.exit(1); - }; - - // Worklist (ambiguous sites) always goes to stderr. - for (result.worklist) |w| { - std.debug.print("{s}:{d}:{d}: {s}: {s}\n", .{ path, w.line, w.col, w.reason, w.text }); - } - - const has_worklist = result.worklist.len > 0; - - // Emit the rewritten source unless we'd be shipping a half-migrated file: a - // non-empty worklist in non-dry-run mode suppresses output unless --force. - if (!dry_run and (!has_worklist or force)) { - _ = std.c.write(1, result.output.ptr, result.output.len); - } - - if (has_worklist) { - std.debug.print( - "{d} ambiguous site(s) unmigrated; resolve by hand or pass --force\n", - .{result.worklist.len}, - ); - std.process.exit(2); - } -} - fn dumpSxIR(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, stdlib_paths: []const []const u8) !void { const source = try readSource(allocator, io, input_path); var comp = sx.core.Compilation.init(allocator, io, input_path, source, .{}, stdlib_paths); diff --git a/src/migrate.test.zig b/src/migrate.test.zig deleted file mode 100644 index 9e50c77e..00000000 --- a/src/migrate.test.zig +++ /dev/null @@ -1,330 +0,0 @@ -// Tests for migrate.zig — the `sx migrate` tuple-syntax rewriter. -// -// Each case parses an in-memory snippet (full decls, so it parses standalone), -// runs the AST-walk migrator, and asserts the rewritten text and/or worklist. -// The compiler grammar is UNCHANGED here: the migrator READS the old tuple -// syntax `(a, b)` / `(A, B)` and EMITS the new `.(a, b)` / `Tuple(A, B)` text. - -const std = @import("std"); -const Parser = @import("parser.zig").Parser; -const migrate = @import("migrate.zig"); - -/// Parse `src` (must be valid old-syntax sx decls), migrate, return the -/// rewritten text. Asserts the worklist is empty (use `runWith` for ambiguous -/// cases). -fn run(alloc: std.mem.Allocator, src: [:0]const u8) ![]const u8 { - const res = try runWith(alloc, src); - try std.testing.expectEqual(@as(usize, 0), res.worklist.len); - return res.output; -} - -fn runWith(alloc: std.mem.Allocator, src: [:0]const u8) !migrate.MigrationResult { - var parser = Parser.init(alloc, src); - const root = try parser.parse(); - return migrate.migrateRoot(alloc, src, root); -} - -/// Assert that `needle` appears in `haystack` (substring), with a helpful -/// failure message that prints the full migrated text. -fn expectContains(haystack: []const u8, needle: []const u8) !void { - if (std.mem.indexOf(u8, haystack, needle) == null) { - std.debug.print("\nexpected to find:\n {s}\nin migrated output:\n{s}\n", .{ needle, haystack }); - return error.NotFound; - } -} - -fn expectNotContains(haystack: []const u8, needle: []const u8) !void { - if (std.mem.indexOf(u8, haystack, needle) != null) { - std.debug.print("\nexpected NOT to find:\n {s}\nin migrated output:\n{s}\n", .{ needle, haystack }); - return error.UnexpectedlyFound; - } -} - -// ── VALUE tuples → .(...) ──────────────────────────────────────────────── - -test "migrate value: positional (40,2) -> .(40,2)" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: () { x := (40, 2); }\n"); - try expectContains(out, ".(40, 2)"); - try expectNotContains(out, " (40, 2)"); // the old, un-dotted form is gone -} - -test "migrate value: named (x:1,y:2) -> .(x = 1, y = 2)" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: () { x := (x: 1, y: 2); }\n"); - try expectContains(out, ".(x = 1, y = 2)"); -} - -test "migrate value: 1-tuple (x,) -> .(x)" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: () { y := 9; x := (y,); }\n"); - try expectContains(out, ".(y)"); - try expectNotContains(out, "(y,)"); -} - -test "migrate value: spread (..xs) -> .(..xs)" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: (xs: i32) { t := (..xs); }\n"); - try expectContains(out, ".(..xs)"); -} - -test "migrate value: operator operands (1,2)==(1,2)" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: () { b := (1, 2) == (1, 2); }\n"); - // Both operands rewritten. - try expectContains(out, ".(1, 2) == .(1, 2)"); -} - -test "migrate value+type: return body -> Tuple(i64,i64){ .(b,a) }" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), - \\swap :: (a: i64, b: i64) -> (i64, i64) { (b, a) } - \\ - ); - try expectContains(out, "-> Tuple(i64, i64)"); - try expectContains(out, ".(b, a)"); -} - -test "migrate value: empty () value -> .()" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - // `x := ()` — empty tuple value. - const out = try run(arena.allocator(), "f :: () { x := (); }\n"); - try expectContains(out, ".()"); -} - -// ── TYPE tuples → Tuple(...) ───────────────────────────────────────────── - -test "migrate type: annotation a:(i32,string) -> a:Tuple(i32,string)" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: () { a : (i32, string) = ---; }\n"); - try expectContains(out, "Tuple(i32, string)"); -} - -test "migrate type: named (x:i32,y:string) -> Tuple(x: i32, y: string) keeps colon" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: () { a : (x: i32, y: string) = ---; }\n"); - try expectContains(out, "Tuple(x: i32, y: string)"); -} - -test "migrate type: struct field xs:(i32,i32) -> Tuple(i32,i32)" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "S :: struct { xs: (i32, i32); }\n"); - try expectContains(out, "Tuple(i32, i32)"); -} - -test "migrate type: pack (..Ts) -> Tuple(..Ts)" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "S :: struct { xs: (..Ts); }\n"); - try expectContains(out, "Tuple(..Ts)"); -} - -test "migrate type: 1-tuple (T,) -> Tuple(T) drops comma" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "S :: struct { xs: (i32,); }\n"); - try expectContains(out, "Tuple(i32)"); - try expectNotContains(out, "(i32,)"); -} - -// ── Worklist: ambiguous value-vs-type call arg ────────────────────────── - -test "migrate worklist: size_of((Box,i32)) is NOT rewritten, records worklist" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const res = try runWith(arena.allocator(), - \\f :: () { n := size_of((Box, i32)); } - \\ - ); - // Ambiguous inner tuple left untouched: no `.(` rewrite of `(Box, i32)`. - try expectNotContains(res.output, ".(Box, i32)"); - try expectContains(res.output, "(Box, i32)"); - // One worklist entry recorded. - try std.testing.expectEqual(@as(usize, 1), res.worklist.len); - try expectContains(res.worklist[0].text, "(Box, i32)"); -} - -test "migrate value: call arg with literal-only tuple IS rewritten" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - // `take((1, 2))` — all elements are concrete values → safe to rewrite. - const res = try runWith(arena.allocator(), "f :: () { take((1, 2)); }\n"); - try expectContains(res.output, ".(1, 2)"); - try std.testing.expectEqual(@as(usize, 0), res.worklist.len); -} - -// ── Nested tuples (recursive rewrite, ONE edit per outermost tuple) ────── - -test "migrate nested value: ((1,2),(3,4)) -> .(.(1, 2), .(3, 4))" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: () { x := ((1, 2), (3, 4)); }\n"); - try expectContains(out, ".(.(1, 2), .(3, 4))"); - // No stray un-migrated inner tuple, no trailing junk paren. - try expectNotContains(out, ".(1, 2), 3)"); -} - -test "migrate nested value: ((1,2),3) -> .(.(1, 2), 3)" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: () { x := ((1, 2), 3); }\n"); - try expectContains(out, ".(.(1, 2), 3)"); - try expectNotContains(out, "(1, 2), 3))"); // the broken old output -} - -test "migrate nested named value: (a:(p:1,q:2),b:3) -> .(a = .(p = 1, q = 2), b = 3)" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: () { n := (a: (p: 1, q: 2), b: 3); }\n"); - try expectContains(out, ".(a = .(p = 1, q = 2), b = 3)"); -} - -test "migrate nested type: ((i32,i32),i64) -> Tuple(Tuple(i32, i32), i64)" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: () { a : ((i32, i32), i64) = ---; }\n"); - try expectContains(out, "Tuple(Tuple(i32, i32), i64)"); -} - -// ── Failable multi-returns: `!` channel stays OUTSIDE Tuple(...) ───────── - -test "migrate failable: -> (T, !) -> -> T !" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: () -> (i32, !) { }\n"); - try expectContains(out, "-> i32 !"); - try expectNotContains(out, "Tuple("); - try expectNotContains(out, ".("); -} - -test "migrate failable: -> (T, !Named) keeps the named set" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), - \\E :: error { Bad } - \\f :: () -> (i32, !E) { } - \\ - ); - try expectContains(out, "-> i32 !E"); - try expectNotContains(out, "Tuple("); -} - -test "migrate failable: -> (T1, T2, !) -> -> Tuple(T1, T2) !" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: () -> (i32, i64, !) { }\n"); - try expectContains(out, "-> Tuple(i32, i64) !"); -} - -test "migrate failable: bare -> ! unchanged" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: () -> ! { }\n"); - try expectContains(out, "-> !"); - try expectNotContains(out, "Tuple"); -} - -// ── Inverted call-arg classification (conservative) ───────────────────── - -test "migrate worklist: empty () call arg is worklisted (unit type ambiguity)" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const res = try runWith(arena.allocator(), "f :: () { n := size_of(()); }\n"); - // NOT silently rewritten to `.()`. - try expectNotContains(res.output, "size_of(.())"); - try expectContains(res.output, "size_of(())"); - try std.testing.expectEqual(@as(usize, 1), res.worklist.len); -} - -test "migrate worklist: Vec(3) call-arg element is worklisted" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const res = try runWith(arena.allocator(), "f :: () { n := size_of((Vec(3), i32)); }\n"); - try expectNotContains(res.output, ".(Vec(3), i32)"); - try expectContains(res.output, "(Vec(3), i32)"); - try std.testing.expectEqual(@as(usize, 1), res.worklist.len); -} - -test "migrate worklist: pkg.T qualified path call-arg element is worklisted" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const res = try runWith(arena.allocator(), "f :: () { n := size_of((pkg.T, i32)); }\n"); - try expectNotContains(res.output, ".(pkg.T, i32)"); - try expectContains(res.output, "(pkg.T, i32)"); - try std.testing.expectEqual(@as(usize, 1), res.worklist.len); -} - -// ── Negatives: distinct AST nodes must NOT be touched ──────────────────── - -test "migrate negative: function type (i32,i32)->i32 unchanged" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: () { g : (i32, i32) -> i32 = ---; }\n"); - try expectContains(out, "(i32, i32) -> i32"); - try expectNotContains(out, "Tuple(i32, i32)"); -} - -test "migrate negative: function param list (self:*T,x:i32) unchanged" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "S :: struct {}\nm :: (self: *S, x: i32) { }\n"); - try expectContains(out, "(self: *S, x: i32)"); - try expectNotContains(out, "Tuple("); - try expectNotContains(out, ".(self"); -} - -test "migrate negative: array literal .[1,2,3] unchanged" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: () { a := .[1, 2, 3]; }\n"); - try expectContains(out, ".[1, 2, 3]"); -} - -test "migrate negative: struct literal .{x=1} unchanged" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: () { a := .{ x = 1 }; }\n"); - try expectContains(out, ".{ x = 1 }"); -} - -test "migrate negative: Closure(i32)->i32 type unchanged" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: () { c : Closure(i32) -> i32 = ---; }\n"); - try expectContains(out, "Closure(i32) -> i32"); - try expectNotContains(out, "Tuple("); -} - -test "migrate negative: grouping (a+b)*c unchanged" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), "f :: (a: i32, b: i32, c: i32) { x := (a + b) * c; }\n"); - try expectContains(out, "(a + b) * c"); - try expectNotContains(out, ".(a + b)"); -} - -test "migrate negative: match capture case .some: (val) unchanged" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - const out = try run(arena.allocator(), - \\check :: (v: ?i32) -> i32 { - \\ return if v == { - \\ case .some: (val) { val } - \\ case .none: { 0 } - \\ }; - \\} - \\ - ); - try expectContains(out, "case .some: (val)"); - try expectNotContains(out, ".(val)"); -} diff --git a/src/migrate.zig b/src/migrate.zig deleted file mode 100644 index 9fe79cbb..00000000 --- a/src/migrate.zig +++ /dev/null @@ -1,512 +0,0 @@ -//! Tuple-syntax migration tool (`sx migrate`). -//! -//! Reads OLD-syntax `.sx` source (tuple TYPES `(A, B)`, tuple VALUES `(a, b)`) -//! and emits NEW-syntax text (`Tuple(A, B)` / `.(a, b)`). The compiler grammar -//! is UNCHANGED — this tool only reads the old syntax and rewrites it as text. -//! -//! Strategy: parse-only (read -> Compilation -> parse), then walk the parsed -//! AST with a comptime-reflection child walker that recurses into every -//! `*Node`-bearing field of every node variant. Two node kinds drive a rewrite: -//! -//! * `tuple_type_expr` — produced by the parser in grammatically-forced TYPE -//! positions (`-> (...)`, `: (...)` annotations, struct-field/param types). -//! Rewritten to `Tuple(...)`. SPECIAL CASE: a failable multi-return whose -//! last element is the error-channel marker `!` keeps the channel OUTSIDE -//! the `Tuple(...)` (see `rewriteTupleType`). -//! -//! * `tuple_literal` — produced in VALUE positions. Rewritten to `.(...)`. -//! In CALL-ARG position the value/type distinction is ambiguous, so we only -//! auto-rewrite when EVERY element is a concrete value literal; anything -//! else (bare identifier, `Vec(3)`, `pkg.T`, empty `()`, ...) is recorded on -//! the worklist and left untouched — never guess (CLAUDE.md silent-fallback -//! rule). -//! -//! Nesting: the rewrite is RECURSIVE but emits exactly ONE edit per OUTERMOST -//! tuple. The replacement text for a tuple is built by recursively migrating its -//! nested tuple elements (and any non-tuple subexpressions, e.g. calls) directly -//! into that text. We never emit a separate, overlapping child edit for anything -//! inside a tuple's span — `applyEdits` asserts non-overlap as a tripwire. -//! -//! Edits are collected against the ORIGINAL source byte offsets and applied -//! DESCENDING by start offset so earlier offsets stay valid; comments and -//! formatting outside the edited spans are preserved verbatim. - -const std = @import("std"); -const ast = @import("ast.zig"); -const core = @import("core.zig"); - -const Node = ast.Node; - -/// A single text replacement against the original source: `source[start..end]` -/// becomes `replacement`. -pub const Edit = struct { - start: u32, - end: u32, - replacement: []const u8, -}; - -/// An ambiguous site we refused to rewrite. `line`/`col` are 1-based. -pub const Worklist = struct { - line: u32, - col: u32, - text: []const u8, - reason: []const u8, -}; - -pub const MigrationResult = struct { - /// The rewritten source (a fresh allocation owning its bytes). - output: []const u8, - /// Ambiguous sites left untouched, in source order. - worklist: []const Worklist, -}; - -/// Walk state: collects edits + worklist entries while recursing the AST. -const Walker = struct { - allocator: std.mem.Allocator, - source: []const u8, - edits: std.ArrayList(Edit) = .empty, - worklist: std.ArrayList(Worklist) = .empty, - - /// Recurse into `node`. `is_call_arg` is true when this node is a DIRECT - /// argument of a `call` / `ffi_intrinsic_call` — the only context in which a - /// `tuple_literal` may be value-vs-type ambiguous. - /// - /// On hitting an OUTERMOST tuple we compute its full replacement (recursively - /// baking any nested tuples / subexprs into the text) and emit a SINGLE edit; - /// we do NOT continue the edit-emitting walk into the tuple's span (that would - /// produce overlapping edits). Worklist collection for ambiguous nested - /// call-args still happens, inside the recursive text builder. - fn walk(self: *Walker, node: *const Node, is_call_arg: bool) anyerror!void { - switch (node.data) { - .tuple_type_expr => |tt| { - const replacement = try self.buildTupleTypeText(node, tt); - if (replacement) |rep| { - try self.edits.append(self.allocator, .{ - .start = node.span.start, - .end = node.span.end, - .replacement = rep, - }); - } - // Do NOT recurse into the tuple's element subtrees here — they - // are already baked into `replacement`. (A `null` replacement - // means "leave unchanged"; that only happens for `-> !`, which - // has no value elements to rewrite anyway.) - return; - }, - .tuple_literal => |tl| { - if (is_call_arg and !tupleIsAllConcreteValues(tl)) { - // Ambiguous in call-arg position (could be a type argument, - // a parameterized type, a qualified path, the unit type - // `()`, ...). Refuse to guess — record + leave untouched, and - // keep walking into elements so nested unambiguous tuples are - // still migrated. - try self.recordWorklist(node); - for (tl.elements) |el| try self.walk(el.value, false); - } else { - const rep = try self.buildTupleValueText(node, tl); - try self.edits.append(self.allocator, .{ - .start = node.span.start, - .end = node.span.end, - .replacement = rep, - }); - } - return; - }, - // A `call`'s direct args get the call-arg flag; the callee does not. - .call => |c| { - try self.walk(c.callee, false); - for (c.args) |a| try self.walk(a, true); - return; - }, - .ffi_intrinsic_call => |c| { - try self.walk(c.return_type, false); - for (c.args) |a| try self.walk(a, true); - return; - }, - else => {}, - } - // Generic recursion for every other node: visit each child *Node found - // by reflection over the active union payload. Call-arg context does NOT - // propagate past a non-call node. - try self.walkChildren(node); - } - - /// Reflect over the active payload of `node.data` and recurse into every - /// `*Node` reachable through its fields (directly, through optionals, - /// slices, and nested aggregate structs/unions). - fn walkChildren(self: *Walker, node: *const Node) anyerror!void { - switch (node.data) { - inline else => |payload| { - try self.walkValue(@TypeOf(payload), payload); - }, - } - } - - /// Recurse into any `*Node` reachable from `value` of type `T`. - fn walkValue(self: *Walker, comptime T: type, value: T) anyerror!void { - if (T == *Node or T == *const Node) { - try self.walk(value, false); - return; - } - switch (@typeInfo(T)) { - .pointer => |ptr| { - switch (ptr.size) { - .slice => { - if (comptime containsNode(ptr.child)) { - for (value) |elem| try self.walkValue(ptr.child, elem); - } - }, - // Non-slice pointers other than *Node (handled above) carry - // no AST children we rewrite. - else => {}, - } - }, - .optional => |opt| { - if (comptime containsNode(opt.child)) { - if (value) |inner| try self.walkValue(opt.child, inner); - } - }, - .@"struct" => |st| { - inline for (st.fields) |f| { - if (comptime containsNode(f.type)) { - try self.walkValue(f.type, @field(value, f.name)); - } - } - }, - .@"union" => |un| { - if (comptime unionContainsNode(un)) { - switch (value) { - inline else => |inner| try self.walkValue(@TypeOf(inner), inner), - } - } - }, - else => {}, - } - } - - /// Build the replacement text for a `tuple_type_expr`, baking nested tuples - /// recursively. Returns `null` when the node should be left unchanged. - /// - /// Failable multi-return handling — the error channel `!` (an - /// `error_type_expr` element, always last) stays OUTSIDE the `Tuple(...)`: - /// * `(!)` → unchanged (no value tuple). - /// * `(T, !)` → `T !` (single value: drop the parens). - /// * `(T1, T2, !)` → `Tuple(T1, T2) !`. - fn buildTupleTypeText(self: *Walker, node: *const Node, tt: ast.TupleTypeExpr) !?[]const u8 { - // Detect a trailing error-channel marker. - const n = tt.field_types.len; - const has_err = n > 0 and tt.field_types[n - 1].data == .error_type_expr; - - if (has_err) { - const err_node = tt.field_types[n - 1]; - // Raw text of the error marker, e.g. `!` or `!JsonError`. - const err_text = self.source[err_node.span.start..err_node.span.end]; - const value_count = n - 1; - if (value_count == 0) { - // `-> !` (no value tuple) — leave unchanged. - return null; - } - if (value_count == 1) { - // `(T, !)` → `T !` — strip the parens, no Tuple wrapper. - const t_text = try self.migratedTypeElement(tt.field_types[0]); - return try std.fmt.allocPrint(self.allocator, "{s} {s}", .{ t_text, err_text }); - } - // `(T1, T2, ..., !)` → `Tuple(T1, T2, ...) !`. - const inner = try self.buildTypeInner(node, tt, value_count); - return try std.fmt.allocPrint(self.allocator, "Tuple{s} {s}", .{ inner, err_text }); - } - - // Ordinary type tuple: `Tuple(...)`, names keep `:`. - const inner = try self.buildTypeInner(node, tt, n); - return try std.fmt.allocPrint(self.allocator, "Tuple{s}", .{inner}); - } - - /// Build the parenthesized inner `(...)` for a type tuple covering the first - /// `count` field types (a failable return passes `count < field_types.len` to - /// exclude the trailing `!`). Names keep their `:`. A 1-tuple drops its - /// trailing comma. - fn buildTypeInner(self: *Walker, node: *const Node, tt: ast.TupleTypeExpr, count: usize) ![]const u8 { - var out = std.ArrayList(u8).empty; - try out.append(self.allocator, '('); - for (tt.field_types[0..count], 0..) |ft, i| { - if (i != 0) try out.appendSlice(self.allocator, ", "); - // Named type tuple keeps `name: ` verbatim. - if (tt.field_names) |names| { - // Synthetic `_` names mark positional slots — emit nothing. - if (!isSyntheticName(names[i], i)) { - try out.appendSlice(self.allocator, names[i]); - try out.appendSlice(self.allocator, ": "); - } - } - const el_text = try self.migratedTypeElement(ft); - try out.appendSlice(self.allocator, el_text); - } - try out.append(self.allocator, ')'); - _ = node; - return out.toOwnedSlice(self.allocator); - } - - /// Migrate a single TYPE element subtree to text. A nested tuple type is - /// baked recursively; everything else is copied verbatim from source but with - /// any nested tuples inside it rewritten. - fn migratedTypeElement(self: *Walker, ft: *const Node) anyerror![]const u8 { - if (ft.data == .tuple_type_expr) { - const rep = try self.buildTupleTypeText(ft, ft.data.tuple_type_expr); - return rep orelse self.source[ft.span.start..ft.span.end]; - } - return self.migratedSubtree(ft, false); - } - - /// Build the replacement text for a `tuple_literal`, baking nested tuples - /// recursively. Names flip `:` → ` = `. - fn buildTupleValueText(self: *Walker, node: *const Node, tl: ast.TupleLiteral) ![]const u8 { - var out = std.ArrayList(u8).empty; - try out.appendSlice(self.allocator, ".("); - for (tl.elements, 0..) |el, i| { - if (i != 0) try out.appendSlice(self.allocator, ", "); - if (el.name) |name| { - try out.appendSlice(self.allocator, name); - try out.appendSlice(self.allocator, " = "); - } - // Spread element: `..xs` — the parser models it as a spread_expr - // whose operand is the spread target; copy its source verbatim - // (its own nested tuples, if any, get migrated by migratedSubtree). - const el_text = try self.migratedValueElement(el.value); - try out.appendSlice(self.allocator, el_text); - } - try out.append(self.allocator, ')'); - _ = node; - return out.toOwnedSlice(self.allocator); - } - - /// Migrate a single VALUE element subtree to text. A nested tuple literal is - /// baked recursively; everything else is copied verbatim with nested tuples - /// inside rewritten. - fn migratedValueElement(self: *Walker, value: *const Node) anyerror![]const u8 { - if (value.data == .tuple_literal) { - const tl = value.data.tuple_literal; - // A nested tuple in a VALUE position is unambiguously a value (it is - // never itself a direct call-arg), so always rewrite it. - return self.buildTupleValueText(value, tl); - } - return self.migratedSubtree(value, false); - } - - /// Return the migrated text for an arbitrary subtree by collecting the edits - /// its descendants produce (relative to `node.span`) and splicing them into - /// the raw source slice. Worklist entries discovered inside are appended to - /// the shared worklist. This is how a NON-tuple element of a tuple (e.g. a - /// `call` with its own nested tuple args) gets its inner tuples migrated - /// while preserving its surrounding formatting verbatim. - fn migratedSubtree(self: *Walker, node: *const Node, is_call_arg: bool) ![]const u8 { - // Sub-walk with a private edit list but the SHARED worklist. - var sub = Walker{ - .allocator = self.allocator, - .source = self.source, - .worklist = self.worklist, - }; - try sub.walk(node, is_call_arg); - // Carry any worklist entries the sub-walk found back to the parent. - self.worklist = sub.worklist; - - const base = node.span.start; - const raw = self.source[node.span.start..node.span.end]; - if (sub.edits.items.len == 0) return raw; - // Splice sub-edits (offsets are absolute; rebase to the slice). - return applyEditsRebased(self.allocator, raw, base, sub.edits.items); - } - - fn recordWorklist(self: *Walker, node: *const Node) !void { - const lc = lineCol(self.source, node.span.start); - try self.worklist.append(self.allocator, .{ - .line = lc.line, - .col = lc.col, - .text = self.source[node.span.start..node.span.end], - .reason = "ambiguous value-vs-type call arg; resolve to `Tuple(...)` or `.(...)` by hand", - }); - } -}; - -/// A synthetic positional name is exactly `_` for slot `i` (the parser -/// fills these in for positional slots of an otherwise-named tuple). Treat such -/// a name as "no name" so a mixed tuple's positional slots stay positional. -fn isSyntheticName(name: []const u8, i: usize) bool { - if (name.len < 2 or name[0] != '_') return false; - var buf: [24]u8 = undefined; - const expect = std.fmt.bufPrint(&buf, "_{d}", .{i}) catch return false; - return std.mem.eql(u8, name, expect); -} - -/// True when EVERY element of a call-arg `tuple_literal` is a concrete value -/// literal (or an unambiguous value-operator expression over such). Only then is -/// it safe to auto-rewrite the tuple to `.(...)` in call-arg position — anything -/// else (bare identifier, parameterized type `Vec(3)`, qualified path `pkg.T`, -/// empty `()`, ...) is ambiguous and goes to the worklist. -fn tupleIsAllConcreteValues(tl: ast.TupleLiteral) bool { - // An empty `()` in call-arg position is ambiguous (unit type vs empty value). - if (tl.elements.len == 0) return false; - for (tl.elements) |el| { - if (!nodeIsConcreteValue(el.value)) return false; - } - return true; -} - -/// A node is a "concrete value" when it can only denote a runtime value — never -/// a type. Conservative: int/float/string/bool/char literals, null/undef, enum -/// literals, array/struct literals, and value-operator expressions (binary / -/// unary ops, comparisons) whose operands are themselves concrete values. A -/// nested tuple literal of concrete values is concrete too. Everything else -/// (identifiers, calls, field access, parameterized/qualified type syntax, ...) -/// is NOT — it could be or contain a type. -fn nodeIsConcreteValue(node: *const Node) bool { - return switch (node.data) { - .int_literal, - .float_literal, - .bool_literal, - .string_literal, - .null_literal, - .undef_literal, - .enum_literal, - .array_literal, - .struct_literal, - => true, - .binary_op => |b| nodeIsConcreteValue(b.lhs) and nodeIsConcreteValue(b.rhs), - .chained_comparison => |c| blk: { - for (c.operands) |o| { - if (!nodeIsConcreteValue(o)) break :blk false; - } - break :blk true; - }, - .unary_op => |u| nodeIsConcreteValue(u.operand), - .tuple_literal => |t| tupleIsAllConcreteValues(t), - else => false, - }; -} - -/// Comptime: does type `T` (transitively) contain a `*Node` we'd recurse into? -/// Prunes the reflection walk so we never descend into pure-scalar payloads. -fn containsNode(comptime T: type) bool { - if (T == *Node or T == *const Node or T == Node) return true; - return switch (@typeInfo(T)) { - .pointer => |ptr| switch (ptr.size) { - .slice => containsNode(ptr.child), - .one => ptr.child == Node, // *Node handled above; other *X: no - else => false, - }, - .optional => |opt| containsNode(opt.child), - .array => |arr| containsNode(arr.child), - .@"struct" => |st| blk: { - inline for (st.fields) |f| { - if (containsNode(f.type)) break :blk true; - } - break :blk false; - }, - .@"union" => |un| unionContainsNode(un), - else => false, - }; -} - -fn unionContainsNode(comptime un: std.builtin.Type.Union) bool { - inline for (un.fields) |f| { - if (containsNode(f.type)) return true; - } - return false; -} - -const LineCol = struct { line: u32, col: u32 }; - -fn lineCol(source: []const u8, offset: u32) LineCol { - var line: u32 = 1; - var col: u32 = 1; - var i: usize = 0; - while (i < offset and i < source.len) : (i += 1) { - if (source[i] == '\n') { - line += 1; - col = 1; - } else { - col += 1; - } - } - return .{ .line = line, .col = col }; -} - -/// Migrate a source string in memory. Parse-only; never resolves imports or -/// lowers. Returns the rewritten text + any ambiguous worklist entries. -/// -/// `file_path` is used only for diagnostics labeling. -pub fn migrateSource( - allocator: std.mem.Allocator, - io: std.Io, - file_path: []const u8, - source: [:0]const u8, -) !MigrationResult { - var comp = core.Compilation.init(allocator, io, file_path, source, .{}, &.{}); - defer comp.deinit(); - comp.parse() catch { - comp.renderErrors(); - return error.ParseFailed; - }; - const root = comp.root orelse return error.ParseFailed; - return migrateRoot(allocator, source, root); -} - -/// Migrate from an already-parsed `root`. Split from `migrateSource` so unit -/// tests can parse in memory (via `Parser.init`) without an `std.Io`. -pub fn migrateRoot( - allocator: std.mem.Allocator, - source: []const u8, - root: *const Node, -) !MigrationResult { - var walker = Walker{ .allocator = allocator, .source = source }; - for (root.data.root.decls) |decl| { - try walker.walk(decl, false); - } - const output = try applyEdits(allocator, source, walker.edits.items); - return .{ - .output = output, - .worklist = try walker.worklist.toOwnedSlice(allocator), - }; -} - -/// Apply edits to a COPY of the original source. Edits are sorted DESCENDING by -/// start so each splice leaves earlier offsets valid. Overlapping edits are a -/// hard error — the recursive rewrite must emit exactly one edit per outermost -/// tuple, so two edits sharing any byte is a bug. -pub fn applyEdits(allocator: std.mem.Allocator, source: []const u8, edits_in: []const Edit) ![]const u8 { - const edits = try allocator.dupe(Edit, edits_in); - std.mem.sort(Edit, edits, {}, struct { - fn lessThan(_: void, a: Edit, b: Edit) bool { - return a.start > b.start; // descending - } - }.lessThan); - - // Tripwire: after the descending sort, each edit's end must not exceed the - // next (lower-start) edit's start. Any overlap means the recursive rewrite - // double-emitted — refuse to produce corrupt output. - var prev_start: ?u32 = null; - for (edits) |e| { - if (prev_start) |ps| { - if (e.end > ps) return error.OverlappingEdits; - } - prev_start = e.start; - } - - var out = try std.ArrayList(u8).initCapacity(allocator, source.len); - try out.appendSlice(allocator, source); - for (edits) |e| { - // Splice source[e.start..e.end] -> e.replacement. - try out.replaceRange(allocator, e.start, e.end - e.start, e.replacement); - } - return out.toOwnedSlice(allocator); -} - -/// Apply edits whose `start`/`end` are ABSOLUTE source offsets to a `slice` that -/// begins at absolute offset `base`. Used by `migratedSubtree` to splice a -/// non-tuple subtree's inner tuple rewrites into its raw slice. -fn applyEditsRebased(allocator: std.mem.Allocator, slice: []const u8, base: u32, edits_in: []const Edit) ![]const u8 { - var rebased = try allocator.alloc(Edit, edits_in.len); - for (edits_in, 0..) |e, i| { - rebased[i] = .{ .start = e.start - base, .end = e.end - base, .replacement = e.replacement }; - } - return applyEdits(allocator, slice, rebased); -} diff --git a/src/parser.test.zig b/src/parser.test.zig index 230c07bf..2b4543ee 100644 --- a/src/parser.test.zig +++ b/src/parser.test.zig @@ -217,3 +217,202 @@ test "parser: plain struct leaves abi == .default, extern_lib == null" { try std.testing.expectEqual(ast.ABI.default, sd.abi); try std.testing.expect(sd.extern_lib == null); } + +// ── New tuple syntax (additive; the inline `(a, b)` forms stay valid) ── + +// `Tuple(A, B)` magic type id → positional tuple_type_expr, mirroring `(A, B)`. +// Exercised in a genuine type position (a fn return type), since a `::` RHS is +// an EXPRESSION position where `Tuple(...)` is an ordinary call. +test "parser: Tuple(A, B) type parses to positional tuple_type_expr" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var parser = Parser.init(arena.allocator(), "f :: () -> Tuple(i64, i32) { 0 }"); + const root = try parser.parse(); + const rt = root.data.root.decls[0].data.fn_decl.return_type.?; + try std.testing.expect(rt.data == .tuple_type_expr); + const t = rt.data.tuple_type_expr; + try std.testing.expectEqual(@as(usize, 2), t.field_types.len); + try std.testing.expect(t.field_names == null); +} + +// `Tuple(x: A, y: B)` keeps `:` and stores field names. +test "parser: named Tuple(x: A, y: B) stores field names" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var parser = Parser.init(arena.allocator(), "f :: () -> Tuple(x: i64, y: i32) { 0 }"); + const root = try parser.parse(); + const t = root.data.root.decls[0].data.fn_decl.return_type.?.data.tuple_type_expr; + try std.testing.expectEqual(@as(usize, 2), t.field_types.len); + try std.testing.expect(t.field_names != null); + try std.testing.expectEqualStrings("x", t.field_names.?[0]); + try std.testing.expectEqualStrings("y", t.field_names.?[1]); +} + +// 1-tuple `Tuple(T)` and empty `Tuple()`. A `Tuple(T)` stays a 1-tuple — unlike +// the inline `(T)` which is a grouping; my block never unwraps. +test "parser: Tuple(T) is a 1-tuple, Tuple() is empty" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var p1 = Parser.init(arena.allocator(), "f :: () -> Tuple(i64) { 0 }"); + const r1 = try p1.parse(); + const t1 = r1.data.root.decls[0].data.fn_decl.return_type.?.data.tuple_type_expr; + try std.testing.expectEqual(@as(usize, 1), t1.field_types.len); + + var p2 = Parser.init(arena.allocator(), "f :: () -> Tuple() { 0 }"); + const r2 = try p2.parse(); + const t2 = r2.data.root.decls[0].data.fn_decl.return_type.?.data.tuple_type_expr; + try std.testing.expectEqual(@as(usize, 0), t2.field_types.len); +} + +// `Tuple(..Ts)` reuses the spread/pack machinery (spread_expr field). Checked +// in a PARAM type position (the inline `(..Ts)` form parses there too — a pack +// tuple in bare RETURN position is a separate pre-existing parser limitation +// that affects `(..Ts)` and `Tuple(..Ts)` identically). +test "parser: Tuple(..Ts) pack field is a spread_expr" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var parser = Parser.init(arena.allocator(), "f :: (t: Tuple(..Ts)) { }"); + const root = try parser.parse(); + const t = root.data.root.decls[0].data.fn_decl.params[0].type_expr.data.tuple_type_expr; + try std.testing.expectEqual(@as(usize, 1), t.field_types.len); + try std.testing.expect(t.field_types[0].data == .spread_expr); +} + +// A trailing `->` after `Tuple(...)` is a hard error (no return type). +test "parser: Tuple(A, B) -> C is rejected" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var parser = Parser.init(arena.allocator(), "f :: () -> Tuple(i64, i64) -> i64 { 0 }"); + try std.testing.expectError(error.ParseError, parser.parse()); +} + +// A bare `Tuple` not followed by `(` stays an ordinary identifier. +test "parser: bare Tuple (no paren) is an identifier, not a tuple type" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var parser = Parser.init(arena.allocator(), "f :: () -> i64 { Tuple := 1; Tuple }"); + const root = try parser.parse(); + // Parses without error; the body references `Tuple` as a value name. + try std.testing.expect(root.data.root.decls[0].data == .fn_decl); +} + +// `.(a, b)` value literal → tuple_literal, same node as inline `(a, b)`. +test "parser: .(a, b) parses to tuple_literal" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var parser = Parser.init(arena.allocator(), "f :: () { x := .(1, 2); }"); + const root = try parser.parse(); + const body = root.data.root.decls[0].data.fn_decl.body; + const stmt = body.data.block.stmts[0]; + const val = stmt.data.var_decl.value.?; + try std.testing.expect(val.data == .tuple_literal); + try std.testing.expectEqual(@as(usize, 2), val.data.tuple_literal.elements.len); + try std.testing.expect(val.data.tuple_literal.elements[0].name == null); +} + +// Named `.(x = a, y = b)` uses `=` and binds names onto TupleElement. +test "parser: named .(x = a, y = b) uses = and stores names" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var parser = Parser.init(arena.allocator(), "f :: () { x := .(x = 1, y = 2); }"); + const root = try parser.parse(); + const val = root.data.root.decls[0].data.fn_decl.body.data.block.stmts[0].data.var_decl.value.?; + try std.testing.expect(val.data == .tuple_literal); + const els = val.data.tuple_literal.elements; + try std.testing.expectEqual(@as(usize, 2), els.len); + try std.testing.expectEqualStrings("x", els[0].name.?); + try std.testing.expectEqualStrings("y", els[1].name.?); +} + +// 1-tuple `.(x)` and empty `.()`. +test "parser: .(x) is a 1-tuple, .() is empty" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var p1 = Parser.init(arena.allocator(), "f :: () { x := .(7); }"); + const r1 = try p1.parse(); + const v1 = r1.data.root.decls[0].data.fn_decl.body.data.block.stmts[0].data.var_decl.value.?; + try std.testing.expect(v1.data == .tuple_literal); + try std.testing.expectEqual(@as(usize, 1), v1.data.tuple_literal.elements.len); + + var p2 = Parser.init(arena.allocator(), "f :: () { x := .(); }"); + const r2 = try p2.parse(); + const v2 = r2.data.root.decls[0].data.fn_decl.body.data.block.stmts[0].data.var_decl.value.?; + try std.testing.expect(v2.data == .tuple_literal); + try std.testing.expectEqual(@as(usize, 0), v2.data.tuple_literal.elements.len); +} + +// `-> T !` folds to the same `(T, !)` representation: tuple_type_expr whose +// last field is an error_type_expr. +test "parser: -> T ! folds to (T, !) tuple_type_expr" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var parser = Parser.init(arena.allocator(), "f :: () -> i64 ! { 0 }"); + const root = try parser.parse(); + const rt = root.data.root.decls[0].data.fn_decl.return_type.?; + try std.testing.expect(rt.data == .tuple_type_expr); + const fields = rt.data.tuple_type_expr.field_types; + try std.testing.expectEqual(@as(usize, 2), fields.len); + try std.testing.expect(fields[0].data == .type_expr); + try std.testing.expect(fields[1].data == .error_type_expr); + try std.testing.expect(fields[1].data.error_type_expr.name == null); +} + +// `-> Tuple(T1, T2) !` flattens to (T1, T2, !). +test "parser: -> Tuple(A, B) ! flattens to (A, B, !)" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var parser = Parser.init(arena.allocator(), "f :: () -> Tuple(i64, i32) !ParseErr { 0 }"); + const root = try parser.parse(); + const rt = root.data.root.decls[0].data.fn_decl.return_type.?; + try std.testing.expect(rt.data == .tuple_type_expr); + const fields = rt.data.tuple_type_expr.field_types; + try std.testing.expectEqual(@as(usize, 3), fields.len); + try std.testing.expect(fields[0].data == .type_expr); + try std.testing.expect(fields[1].data == .type_expr); + try std.testing.expect(fields[2].data == .error_type_expr); + try std.testing.expectEqualStrings("ParseErr", fields[2].data.error_type_expr.name.?); +} + +// `-> !` (void + error) stays a bare error_type_expr — the trailing-`!` fold +// must NOT double-wrap it. +test "parser: -> ! stays a bare error_type_expr" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var parser = Parser.init(arena.allocator(), "f :: () -> ! { }"); + const root = try parser.parse(); + const rt = root.data.root.decls[0].data.fn_decl.return_type.?; + try std.testing.expect(rt.data == .error_type_expr); +} + +// Old inline `-> (T, !)` failable form is gone — rejected with the new-form hint. +test "parser: old inline -> (T, !) is rejected" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var parser = Parser.init(arena.allocator(), "f :: () -> (i64, !) { 0 }"); + try std.testing.expectError(error.ParseError, parser.parse()); +} + +// Bare-paren tuple TYPE `(A, B)` is gone — rejected (tuple types use `Tuple(...)`). +test "parser: bare-paren tuple type (A, B) is rejected" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var parser = Parser.init(arena.allocator(), "f :: (t: (i64, i32)) { }"); + try std.testing.expectError(error.ParseError, parser.parse()); +} + +// Bare-paren tuple VALUE `(a, b)` is gone — rejected (tuple values use `.(...)`). +test "parser: bare-paren tuple value (a, b) is rejected" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var parser = Parser.init(arena.allocator(), "f :: () { x := (1, 2); }"); + try std.testing.expectError(error.ParseError, parser.parse()); +} + +// Bare-paren grouping `(a + b)` still works — single inner, no top-level comma. +test "parser: bare-paren grouping (a + b) still parses" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var parser = Parser.init(arena.allocator(), "f :: () -> i64 { (1 + 2) }"); + const root = try parser.parse(); + try std.testing.expect(root.data.root.decls[0].data == .fn_decl); +} diff --git a/src/parser.zig b/src/parser.zig index 4e85ad3e..ad55bff9 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -431,6 +431,68 @@ pub const Parser = struct { return self.fail("expected ':', '=', ';', or 'extern' after type annotation"); } + /// Parse a function/method/lambda return type, folding a trailing `!` + /// error channel that sits OUTSIDE the value type into the same + /// representation the inline `(T, !)` form produces. + /// + /// `-> T !` ⇒ tuple_type_expr { [T, error_type_expr] } (== `(T, !)`) + /// `-> Tuple(A, B) !` ⇒ tuple_type_expr { [A, B, error_type_expr] } (== `(A, B, !)`) + /// `-> !` ⇒ error_type_expr (bare; handled by parseTypeExpr directly) + /// + /// The old inline `(T, !)` / `(T1, T2, !)` forms keep working unchanged — + /// this only ADDS the trailing-`!`-after-the-type spelling. + fn parseFnReturnType(self: *Parser) anyerror!*Node { + const start = self.current.loc.start; + const ty = try self.parseTypeExpr(); + + // A trailing `!` (optionally `!Named`) after the return TYPE denotes the + // error channel sitting OUTSIDE the value type. A bare `-> !` is already + // an error_type_expr (no value), so a `!` after one would be a doubled + // error channel — leave it for the normal "unexpected token" path. + if (self.current.tag != .bang or ty.data == .error_type_expr) return ty; + + self.advance(); // skip '!' + var set_name: ?[]const u8 = null; + if (self.current.tag == .identifier) { + set_name = self.tokenSlice(self.current); + self.advance(); + } + const err_node = try self.createNode(start, .{ .error_type_expr = .{ .name = set_name } }); + + // Build the value+error result list. If the value type is itself a + // tuple — `Tuple(A, B)` (positional) or `Tuple(x: A, y: B)` (named) — + // flatten its fields and append the error channel, so `-> Tuple(A,B) !` + // is identical to `(A, B, !)` and `-> Tuple(x: A, y: B) !` keeps the + // `x`/`y` names on the flattened value fields. The value-return lowering + // inserts the value slots FLAT into the result tuple, so the result type + // must list the value fields flat too — wrapping a named tuple as + // `{ {A,B}, err }` would miscompile the flat 2-tuple insert. Otherwise + // wrap the single value as `(T, !)`. + var fields = std.ArrayList(*Node).empty; + var names = std.ArrayList([]const u8).empty; + var has_names = false; + if (ty.data == .tuple_type_expr) { + const tt = ty.data.tuple_type_expr; + for (tt.field_types) |f| try fields.append(self.allocator, f); + if (tt.field_names) |fn_names| { + has_names = true; + for (fn_names) |nm| try names.append(self.allocator, nm); + // The trailing error channel needs a placeholder name so the + // names slice stays 1:1 with field_types. It is identified by + // position (last field), never by this name (see + // lower/error.zig errorChannelOf). + try names.append(self.allocator, "!"); + } + } else { + try fields.append(self.allocator, ty); + } + try fields.append(self.allocator, err_node); + return try self.createNode(start, .{ .tuple_type_expr = .{ + .field_types = try fields.toOwnedSlice(self.allocator), + .field_names = if (has_names) try names.toOwnedSlice(self.allocator) else null, + } }); + } + fn parseTypeExpr(self: *Parser) anyerror!*Node { const start = self.current.loc.start; @@ -597,9 +659,12 @@ pub const Parser = struct { } try self.expect(.r_paren); if (self.current.tag == .arrow) { - // '->' present: function type + // '->' present: function type. Accept a trailing `!`/`!Named` + // error channel after the return type (`(i64) -> i64 !E`), folded + // to the SAME `(T, !)` / `(A, B, !)` representation the inline form + // produces — the old `-> (T, !)` spelling keeps working too. self.advance(); // skip '->' - const return_type = try self.parseTypeExpr(); + const return_type = try self.parseFnReturnType(); const abi = try self.parseOptionalAbi(); return try self.createNode(start, .{ .function_type_expr = .{ .param_types = try param_types.toOwnedSlice(self.allocator), @@ -608,33 +673,25 @@ pub const Parser = struct { .abi = abi, } }); } - // No '->': GROUPING vs tuple. Mirror value position (`(expr)` groups, - // `(expr,)` is a 1-tuple): a single UNNAMED, non-spread element with - // NO trailing comma is a grouping — resolve to the inner type. This - // lets `(Closure(i64,i64) -> i64)`, `?(?i64)`, etc. parenthesize a - // type for grouping/readability. A 1-tuple type now requires the - // trailing comma `(T,)`; named `(x: T)` and spread `(..Ts)` stay - // tuples. + // No '->': bare `(...)` in type position is GROUPING ONLY. A single + // UNNAMED, non-spread element with NO trailing comma resolves to the + // inner type. This lets `(Closure(i64,i64) -> i64)`, `?(?i64)`, etc. + // parenthesize a type for readability. if (param_types.items.len == 1 and !had_trailing_comma and !has_names and param_types.items[0].data != .spread_expr) { return param_types.items[0]; } - // Tuple type. Keep field names for a named tuple `(x: T, y: U)` so - // `t.x` resolves. `field_names` is non-optional per slot, so - // synthesize `_` for any unnamed one. - var field_names: ?[]const []const u8 = null; - if (has_names) { - var fns = std.ArrayList([]const u8).empty; - for (param_names.items, 0..) |pn, i| { - try fns.append(self.allocator, pn orelse try std.fmt.allocPrint(self.allocator, "_{d}", .{i})); - } - field_names = try fns.toOwnedSlice(self.allocator); + // Anything else (a top-level comma, a `(T,)` 1-tuple, names, a + // spread) used to build a bare-paren `tuple_type_expr`. That grammar + // is gone: tuple types are written `Tuple( … )`. If the group ends in + // an error channel `!`, it is the old failable spelling `-> (T, !)`. + const last_is_err = param_types.items.len > 0 and + param_types.items[param_types.items.len - 1].data == .error_type_expr; + if (last_is_err) { + return self.fail("failable returns use `-> T !` or `-> Tuple(T1,T2) !`"); } - return try self.createNode(start, .{ .tuple_type_expr = .{ - .field_types = try param_types.toOwnedSlice(self.allocator), - .field_names = field_names, - } }); + return self.fail("tuple types use `Tuple( … )` (e.g. `Tuple(A, B)`)"); } if (self.current.tag.isTypeKeyword() or self.isIdentLike()) { @@ -668,6 +725,17 @@ pub const Parser = struct { } } + // Tuple type: `Tuple(A, B)` / `Tuple(T)` / `Tuple()` / + // named `Tuple(x: A, y: B)` / pack `Tuple(..Ts)` / `Tuple(..F(Ts))`. + // Magic contextual id — only a `Tuple` IMMEDIATELY followed by `(` + // builds a tuple type; a bare `Tuple` stays an ordinary identifier + // (mirrors `Closure`). Lowers to the SAME `tuple_type_expr` the + // inline `(A, B)` / `(x: A, y: B)` / `(..Ts)` forms produce. + // Unlike `Closure`, a trailing `->` is REJECTED (no return type). + if (std.mem.eql(u8, name, "Tuple") and self.current.tag == .l_paren) { + return self.parseTupleTypeBody(start); + } + // Closure type: Closure(params...) -> R // Variadic-pack trailing form: `Closure(Prefix..., ..$pack) -> R` // binds `pack` to a heterogeneous comptime type list at impl @@ -724,7 +792,11 @@ pub const Parser = struct { var return_type: ?*Node = null; if (self.current.tag == .arrow) { self.advance(); - return_type = try self.parseTypeExpr(); + // Accept a trailing `!`/`!Named` error channel after the + // closure return type (`Closure(i64) -> i64 !E`, `... -> T !`), + // folded to the same `(T, !)` / `(A, B, !)` representation; the + // old `-> (T, !)` form keeps working. + return_type = try self.parseFnReturnType(); } return try self.createNode(start, .{ .closure_type_expr = .{ .param_types = try param_types.toOwnedSlice(self.allocator), @@ -1267,7 +1339,7 @@ pub const Parser = struct { var return_type: ?*Node = null; if (self.current.tag == .arrow) { self.advance(); - return_type = try self.parseTypeExpr(); + return_type = try self.parseFnReturnType(); } // Optional body (default method) or semicolon @@ -1565,7 +1637,7 @@ pub const Parser = struct { var return_type: ?*Node = null; if (self.current.tag == .arrow) { self.advance(); - return_type = try self.parseTypeExpr(); + return_type = try self.parseFnReturnType(); } // Optional `#jni_method_descriptor("(Sig)Ret")` — explicit JNI descriptor override. @@ -1974,7 +2046,7 @@ pub const Parser = struct { var return_type: ?*Node = null; if (self.current.tag == .arrow) { self.advance(); - return_type = try self.parseTypeExpr(); + return_type = try self.parseFnReturnType(); } // Optional `#get` / `#set` property-accessor marker: @@ -2601,7 +2673,18 @@ pub const Parser = struct { expr = try self.createNode(expr.span.start, .{ .call = .{ .callee = expr, .args = try args.toOwnedSlice(self.allocator) } }); } else if (self.current.tag == .dot) { self.advance(); - if (self.current.tag == .l_brace) { + if (self.current.tag == .l_paren and expr.data == .tuple_type_expr) { + // Typed tuple-value construction: `Tuple(A, B).( v1, v2 )` + // — the `Tuple(...)` type followed by a `.( ... )` + // initializer, exactly like `Name.{ ... }` for structs. + // Same element rules as the anonymous `.( ... )` form + // (positional, named `=`, spread); the resulting + // `tuple_literal` carries the explicit tuple type so it + // lowers against that type instead of self-typing. + const lit = try self.parseDotTupleLiteral(expr.span.start); + lit.data.tuple_literal.type_expr = expr; + expr = lit; + } else if (self.current.tag == .l_brace) { // Struct literal: Type.{ ... } if (expr.data == .identifier) { // Simple name: Vec4.{ ... } @@ -2988,6 +3071,19 @@ pub const Parser = struct { .identifier => { const name = self.tokenSlice(self.current); const is_raw = self.current.is_raw; + // `Tuple(...)` in expression position is a tuple TYPE node — + // identical to the one the type parser produces — NOT a value. + // It is first-class in `size_of` / `type_info` / generic type + // args (a non-type element like `Tuple(i32, 1)` is rejected at + // the type-demanding site), can stand alone as a `Type` value, + // and a postfix `.( ... )` (handled in `parsePostfix`) + // constructs a typed tuple VALUE of that type — exactly like + // `Name.{ ... }` for structs. A bare `Tuple` not followed by `(` + // (or a backtick-raw `` `Tuple ``) stays an ordinary identifier. + if (!is_raw and std.mem.eql(u8, name, "Tuple") and self.peekNext() == .l_paren) { + self.advance(); // skip `Tuple`; `current` is now `(` + return self.parseTupleTypeBody(start); + } // A backtick raw identifier (`` `i2 ``) is NEVER type-classified — // it is always a value identifier, bypassing the reserved-type-name // rule. Only a bare spelling is checked for a type name @@ -3027,11 +3123,20 @@ pub const Parser = struct { try self.expect(.r_bracket); return try self.createNode(start, .{ .array_literal = .{ .elements = try elements.toOwnedSlice(self.allocator) } }); } + // Tuple value literal: .( ... ) + // positional `.(a, b)` / 1-tuple `.(x)` / empty `.()` / + // named `.(x = a, y = b)` (uses `=`, like struct-literal field + // init) / spread `.(..xs)` / nested `.( .(a,b), c )`. + // Produces the SAME `tuple_literal` node the inline `(a, b)` + // form produces, so it self-types structurally with no target. + if (self.current.tag == .l_paren) { + return self.parseDotTupleLiteral(start); + } // Enum literal: .variant_name. A reserved keyword is a valid // variant name here — the leading dot disambiguates (`.enum`, // `.struct`), so no backtick escape is needed. const name = self.dotMemberName() orelse - return self.fail("expected variant name, '{', or '[' after '.'"); + return self.fail("expected variant name, '{', '[', or '(' after '.'"); // Enum literal: .variant_name — parsePostfix handles optional (...) as a call return try self.createNode(start, .{ .enum_literal = .{ .name = name } }); }, @@ -3052,34 +3157,25 @@ pub const Parser = struct { self.in_for_header = false; defer self.in_for_header = saved_hdr_grp; - // Check for named tuple: (name: expr, ...) + // Bare `(...)` is GROUPING ONLY. Tuple VALUES are written + // `.( … )` / `Tuple(T..).( … )`. A named element, an empty group, + // a leading spread, or a top-level comma all used to build a + // bare-paren `tuple_literal`; that grammar is gone. if (self.current.tag == .identifier and self.peekNext() == .colon) { - return self.parseTupleLiteralNamed(start); + return self.fail("tuple values use `.( … )` (e.g. `.(a, b)`) or `Tuple(T..).( … )`"); } - - // Empty parens or first expression if (self.current.tag == .r_paren) { - self.advance(); - // () — empty tuple - return try self.createNode(start, .{ .tuple_literal = .{ .elements = &.{} } }); + return self.fail("tuple values use `.( … )` (e.g. `.(a, b)`) or `Tuple(T..).( … )`"); } - - // Leading pack/tuple spread: `(..xs)` / `(..xs.field)` materializes - // a tuple from a pack. The spread reuses `spread_expr`; its operand - // carries the projection (`xs.field`) shape. if (self.current.tag == .dot_dot) { - const spread_start = self.current.loc.start; - self.advance(); // skip '..' - const operand = try self.parseExpr(); - const spread = try self.createNode(spread_start, .{ .spread_expr = .{ .operand = operand } }); - return self.finishTupleAfterFirst(start, spread); + return self.fail("tuple values use `.( … )` (e.g. `.(a, b)`) or `Tuple(T..).( … )`"); } const first = try self.parseExpr(); - // Check for comma → tuple + // A top-level comma was a tuple; now it is an error. if (self.current.tag == .comma) { - return self.finishTupleAfterFirst(start, first); + return self.fail("tuple values use `.( … )` (e.g. `.(a, b)`) or `Tuple(T..).( … )`"); } // No comma → grouping @@ -3581,45 +3677,156 @@ pub const Parser = struct { return try self.createNode(start_pos, .{ .match_expr = .{ .subject = subject, .arms = try arms.toOwnedSlice(self.allocator) } }); } - /// Parse a named tuple literal: (name: expr, name: expr, ...) - /// Called after '(' has been consumed and we've verified identifier + colon pattern. - fn parseTupleLiteralNamed(self: *Parser, start: u32) anyerror!*Node { - var elements = std.ArrayList(ast.TupleElement).empty; - while (self.current.tag != .r_paren and self.current.tag != .eof) { - if (self.current.tag != .identifier) { - return self.fail("expected field name in named tuple"); - } - const name = self.tokenSlice(self.current); - self.advance(); - try self.expect(.colon); - const value = try self.parseExpr(); - try elements.append(self.allocator, .{ .name = name, .value = value }); - if (self.current.tag == .comma) { - self.advance(); - if (self.current.tag == .r_paren) break; - } else break; + + /// Parse a `.( ... )` tuple value literal. `current` is the `(`. + /// A token that can only begin a VALUE literal, never a type. Used to give + /// `Tuple(...)` a precise lowering-time "element is not a type" diagnostic + /// (instead of a generic parse error) when a literal is supplied as a tuple + /// element. + fn currentTokenIsValueLiteral(self: *Parser) bool { + switch (self.current.tag) { + .int_literal, + .float_literal, + .string_literal, + .raw_string_literal, + .kw_true, + .kw_false, + .kw_null, + => return true, + // A signed numeric literal — `Tuple(i32, -1)` / `Tuple(i32, +2)` — + // is value-shaped too, so the precise "tuple type element is not a + // type" diagnostic fires instead of the generic "expected type + // name" parse error. Only a leading sign DIRECTLY before a number + // counts (not `-T`, which is never a valid type anyway). + .minus, .plus => { + const next = self.peekNext(); + return next == .int_literal or next == .float_literal; + }, + else => return false, } - try self.expect(.r_paren); - return try self.createNode(start, .{ .tuple_literal = .{ .elements = try elements.toOwnedSlice(self.allocator) } }); } - /// Finish parsing a tuple after the first positional element and a comma. - /// Called with first element already parsed and current token is ','. - fn finishTupleAfterFirst(self: *Parser, start: u32, first: *Node) anyerror!*Node { - var elements = std.ArrayList(ast.TupleElement).empty; - try elements.append(self.allocator, .{ .name = null, .value = first }); - while (self.current.tag == .comma) { - self.advance(); // skip ',' - if (self.current.tag == .r_paren) break; // trailing comma: (42,) - // Spread element: `(a, ..xs, b)` — reuses `spread_expr`. + /// Parse a `Tuple(...)` tuple-TYPE body. On entry `current` is the `(` + /// immediately after the `Tuple` contextual id (the caller has already + /// consumed `Tuple`). Used in BOTH type position (the type parser) and + /// expression position (`parsePrimary`'s `.identifier` arm) — `Tuple(...)` + /// always denotes a TYPE, never a value. A postfix `.( ... )` after the + /// returned `tuple_type_expr` constructs a typed tuple VALUE (handled in + /// `parsePostfix`, mirroring `Name.{ ... }` typed struct literals). + /// `Tuple(A, B)` / `Tuple(T)` / `Tuple()` / named `Tuple(x: A, y: B)` / + /// pack `Tuple(..Ts)` / `Tuple(..F(Ts))`. Lowers to the SAME + /// `tuple_type_expr` the inline `(A, B)` / `(x: A, y: B)` / `(..Ts)` + /// forms produce. Unlike `Closure`, a trailing `->` is REJECTED. + fn parseTupleTypeBody(self: *Parser, start: u32) anyerror!*Node { + self.advance(); // skip '(' + var field_types = std.ArrayList(*Node).empty; + var field_name_opt = std.ArrayList(?[]const u8).empty; + var has_names = false; + while (self.current.tag != .r_paren and self.current.tag != .eof) { + if (field_types.items.len > 0) { + try self.expect(.comma); + if (self.current.tag == .r_paren) break; // trailing comma ok + } + // Pack-spread field: `Tuple(..Ts)` / `Tuple(..F(Ts))` / + // `Tuple(..Ts.Arg)`. Reuses `spread_expr` (same machinery as + // the inline tuple-type and Closure pack paths). if (self.current.tag == .dot_dot) { - const spread_start = self.current.loc.start; + const sp_start = self.current.loc.start; + self.advance(); // skip '..' + const operand = try self.parseTypeExpr(); + try field_name_opt.append(self.allocator, null); + try field_types.append(self.allocator, try self.createNode(sp_start, .{ .spread_expr = .{ .operand = operand } })); + continue; + } + // Named field: `name: Type` (keeps `:`). + if (self.isIdentLike() and self.peekNext() == .colon) { + const fname = self.tokenSlice(self.current); + self.advance(); // skip name + self.advance(); // skip ':' + try field_name_opt.append(self.allocator, fname); + has_names = true; + } else { + try field_name_opt.append(self.allocator, null); + } + // A literal element (`Tuple(i32, 1)`) is NOT a type. Parse it as a + // value expression so the lowering type-arg check rejects it with + // the precise "tuple type element is not a type" diagnostic, rather + // than `parseTypeExpr` bailing here with a generic "expected type + // name" parse error. Type-shaped elements still go through the type + // parser (so `*T`, `[N]T`, `Tuple(...)`, names all parse). + if (self.currentTokenIsValueLiteral()) { + // A leading `+` on a signed literal (`Tuple(i32, +1)`) has no + // unary-op parse; consume it so the number parses as a bare + // value literal. `parseUnary` handles the `-` case and falls + // through to `parsePrimary` for an unsigned literal. + if (self.current.tag == .plus) self.advance(); + try field_types.append(self.allocator, try self.parseUnary()); + } else { + try field_types.append(self.allocator, try self.parseTypeExpr()); + } + } + try self.expect(.r_paren); + // A `Tuple(...)` has NO return type — reject `-> R` loudly rather + // than silently swallowing it the way `Closure` consumes it. + if (self.current.tag == .arrow) { + return self.fail("`Tuple` has no return type — remove the `->`"); + } + // Per-slot field names are non-optional in the AST; synthesize + // `_` for any unnamed slot (mirrors the inline named-tuple path). + var field_names: ?[]const []const u8 = null; + if (has_names) { + var fns = std.ArrayList([]const u8).empty; + for (field_name_opt.items, 0..) |fn_opt, i| { + try fns.append(self.allocator, fn_opt orelse try std.fmt.allocPrint(self.allocator, "_{d}", .{i})); + } + field_names = try fns.toOwnedSlice(self.allocator); + } + return try self.createNode(start, .{ .tuple_type_expr = .{ + .field_types = try field_types.toOwnedSlice(self.allocator), + .field_names = field_names, + } }); + } + + /// Builds the SAME `tuple_literal` node as the inline `(a, b)` form so the + /// two are structurally identical (operators / splat / `.0` all work with no + /// target type). Supports positional, 1-tuple, empty, named (`x = a`, using + /// `=`), spread (`..xs` / `..xs.field`) and nesting. + fn parseDotTupleLiteral(self: *Parser, start: u32) anyerror!*Node { + self.advance(); // skip '(' + + // `.(` always opens a tuple/grouping, so calls inside parse normally even + // within a for header (mirrors the bare `(` primary path). + const saved_hdr_grp = self.in_for_header; + self.in_for_header = false; + defer self.in_for_header = saved_hdr_grp; + + var elements = std.ArrayList(ast.TupleElement).empty; + while (self.current.tag != .r_paren and self.current.tag != .eof) { + if (elements.items.len > 0) { + try self.expect(.comma); + if (self.current.tag == .r_paren) break; // trailing comma ok + } + // Spread element: `.(..xs)` / `.(a, ..xs, b)` — reuses `spread_expr`, + // whose operand carries any `xs.field` projection. + if (self.current.tag == .dot_dot) { + const sp_start = self.current.loc.start; self.advance(); // skip '..' const operand = try self.parseExpr(); - const spread = try self.createNode(spread_start, .{ .spread_expr = .{ .operand = operand } }); + const spread = try self.createNode(sp_start, .{ .spread_expr = .{ .operand = operand } }); try elements.append(self.allocator, .{ .name = null, .value = spread }); continue; } + // Named field: `name = value` (uses `=`, distinct from the inline + // named-tuple value form which uses `:`). The name is stored on the + // TupleElement exactly like the `:` path. + if (self.isIdentLike() and self.peekNext() == .equal) { + const fname = self.tokenSlice(self.current); + self.advance(); // skip name + self.advance(); // skip '=' + const value = try self.parseExpr(); + try elements.append(self.allocator, .{ .name = fname, .value = value }); + continue; + } const value = try self.parseExpr(); try elements.append(self.allocator, .{ .name = null, .value = value }); } @@ -3751,7 +3958,7 @@ pub const Parser = struct { var return_type: ?*Node = null; if (self.current.tag == .arrow) { self.advance(); - return_type = try self.parseTypeExpr(); + return_type = try self.parseFnReturnType(); } // Optional ABI annotation: abi(.c) / abi(.zig) / abi(.naked) @@ -4569,8 +4776,8 @@ test "parse comptime type-pack is NOT a protocol pack (..$args)" { // type-application); closure-sig packs use ClosureTypeExpr.pack_name + // pack_projection. Arrow bodies wrap the expression in a block. -test "parse pack expansion: tuple value (..xs)" { - const source = "f :: () => (..xs);"; +test "parse pack expansion: tuple value .(..xs)" { + const source = "f :: () => .(..xs);"; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); var parser = Parser.init(arena.allocator(), source); @@ -4585,8 +4792,8 @@ test "parse pack expansion: tuple value (..xs)" { try std.testing.expectEqualStrings("xs", el.data.spread_expr.operand.data.identifier.name); } -test "parse pack expansion: tuple value projection (..xs.value)" { - const source = "f :: () => (..xs.value);"; +test "parse pack expansion: tuple value projection .(..xs.value)" { + const source = "f :: () => .(..xs.value);"; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); var parser = Parser.init(arena.allocator(), source); @@ -4601,8 +4808,8 @@ test "parse pack expansion: tuple value projection (..xs.value)" { try std.testing.expectEqualStrings("xs", op.data.field_access.object.data.identifier.name); } -test "parse pack expansion: tuple type (..F(Ts))" { - const source = "g :: (x: (..F(Ts))) => x;"; +test "parse pack expansion: tuple type Tuple(..F(Ts))" { + const source = "g :: (x: Tuple(..F(Ts))) => x;"; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); var parser = Parser.init(arena.allocator(), source); @@ -4707,8 +4914,8 @@ test "parse bare failable return: named `!Foo`" { try std.testing.expectEqualStrings("ParseErr", rt.data.error_type_expr.name.?); } -test "parse multi-return with inferred `!` as trailing element" { - const source = "f :: () -> (i32, !) { 0; }"; +test "parse failable with inferred `!` (new `-> T !` form)" { + const source = "f :: () -> i32 ! { 0; }"; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); var parser = Parser.init(arena.allocator(), source); @@ -4723,8 +4930,8 @@ test "parse multi-return with inferred `!` as trailing element" { try std.testing.expect(fields[1].data.error_type_expr.name == null); } -test "parse multi-return with named `!Foo` as trailing element" { - const source = "f :: () -> (i32, i64, !ParseErr) { 0; }"; +test "parse failable with named `!Foo` (new `-> Tuple(...) !` form)" { + const source = "f :: () -> Tuple(i32, i64) !ParseErr { 0; }"; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); var parser = Parser.init(arena.allocator(), source); @@ -4737,7 +4944,7 @@ test "parse multi-return with named `!Foo` as trailing element" { try std.testing.expectEqualStrings("ParseErr", fields[2].data.error_type_expr.name.?); } -test "parse error type rejected when not the trailing result element" { +test "parse old bare-paren failable `-> (!, i32)` is rejected" { const source = "f :: () -> (!, i32) { 0; }"; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); @@ -4745,7 +4952,7 @@ test "parse error type rejected when not the trailing result element" { try std.testing.expectError(error.ParseError, parser.parse()); } -test "parse error type rejected in the middle of a result list" { +test "parse old bare-paren failable `-> (i32, !, i64)` is rejected" { const source = "f :: () -> (i32, !, i64) { 0; }"; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); @@ -4764,8 +4971,11 @@ test "round-trip print: error-set decl" { try std.testing.expectEqualStrings(source, aw.writer.toArrayList().items); } -test "round-trip print: multi-return result list with pointer + named error" { - const source = "open :: () -> (*Handle, !IoErr) { 0; }"; +test "print: failable result list with pointer + named error folds to tuple repr" { + // New `-> T !` form: a single value + named error channel folds to the SAME + // internal `tuple_type_expr` the old `(*Handle, !IoErr)` spelling produced, + // so printType still renders the canonical tuple representation. + const source = "open :: () -> *Handle !IoErr { 0; }"; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); var parser = Parser.init(arena.allocator(), source); @@ -5115,7 +5325,7 @@ test "E0.3 or value-terminator: parse(s) or 0" { test "E0.3 full failable function parses end-to-end (all E0 forms)" { const source = - \\parse :: (s: string) -> (i32, !ParseErr) { + \\parse :: (s: string) -> i32 !ParseErr { \\ onfail (e) { cleanup(s); } \\ v := try inner(s) or 0; \\ w := other(s) catch (e2) { return 0; }; diff --git a/src/root.zig b/src/root.zig index 360dce40..fb8af253 100644 --- a/src/root.zig +++ b/src/root.zig @@ -20,8 +20,6 @@ pub const core = @import("core.zig"); pub const c_import = @import("c_import.zig"); pub const c_import_tests = @import("c_import.test.zig"); pub const corpus_run_tests = @import("corpus_run.test.zig"); -pub const migrate = @import("migrate.zig"); -pub const migrate_tests = @import("migrate.test.zig"); pub const ir = @import("ir/ir.zig"); pub const lsp = struct {