refactor: canonical failable syntax (T, !) — remove the bare -> T ! sugar

The trailing-`!`-after-the-value-type spelling (`-> T !`, `-> Tuple(A,B) !`) was a
redundant second way to write a failable return that the parser folded into the
same AST as the parenthesized `(T, !)` / `(A, B, !)` result list. Remove it so
there is ONE canonical spelling: the error channel always rides as the last slot
of the parenthesized list.

- parser: `parseFnReturnType` no longer folds a trailing `!` after a value type —
  it rejects it with a located diagnostic ("a failable return is written `(T, !)`
  … not `T !`"). This one chokepoint covers fn declarations, lambdas, fn-pointer
  types `(A) -> R`, and closure types `Closure(A) -> R`. The error-ONLY `-> !` /
  `-> !ErrSet` form is unaffected (parsed by parseTypeExpr as an error_type_expr).
- migrated every usage to canonical form across library/ + examples/ + issues/ +
  tests/: `-> T !E` → `-> (T, !E)`; the value-carrying `-> Tuple(A, B) !` (which
  FLATTENED to a multi-value failable) → `-> (A, B, !)`, preserving behavior. A
  genuine single-tuple-value failable stays `-> (Tuple(A,B), !)`.
- parser unit tests: the "bare form folds" tests become "bare form is rejected";
  canonical-form parse tests retained.
- docs: specs.md §12 + scattered refs and readme.md updated to the `(T, !)` form.

Behavior-preserving (the bare form was sugar for the same AST). Adversarial review
confirmed: rejection complete across all positions, every canonical form works on
both success/error paths, error-only `-> !` intact, no crashes. Full suite green
(unit tests + 850 corpus examples).
This commit is contained in:
agra
2026-06-27 18:11:20 +03:00
parent b322dcfe61
commit 213cedf0b5
53 changed files with 184 additions and 232 deletions

View File

@@ -17,7 +17,7 @@ E :: error { Neg }
const_one :: () -> i64 { return 1; return 99; } const_one :: () -> i64 { return 1; return 99; }
// dead `return x;` after an unconditional raise (the failable closure shape) // 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 // guard: a conditional return must still fall through to the trailing return
clamp :: (x: i64) -> i64 { if x > 10 { return 10; } return x; } clamp :: (x: i64) -> i64 { if x > 10 { return 10; } return x; }

View File

@@ -6,7 +6,7 @@
E :: error { Bad }; E :: error { Bad };
f :: () -> i64 !E { raise error.Bad; } f :: () -> (i64, !E) { raise error.Bad; }
main :: () { main :: () {
v := f() catch e { 0 }; v := f() catch e { 0 };

View File

@@ -10,7 +10,7 @@
E :: error { Bad, Empty } 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.Bad; }
if n == 0 { raise error.Empty; } if n == 0 { raise error.Empty; }
return n * 10; // success → {n*10, 0} return n * 10; // success → {n*10, 0}

View File

@@ -10,14 +10,14 @@
E :: error { Bad, Empty } 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.Bad; }
if n == 0 { raise error.Empty; } if n == 0 { raise error.Empty; }
return n * 2; return n * 2;
} }
// value-carrying `try` in a value-carrying caller — propagates {undef, tag}. // 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); v := try parse(n);
return v + 1; return v + 1;
} }

View File

@@ -8,7 +8,7 @@
E :: error { Bad } E :: error { Bad }
parse :: (n: i32) -> i32 !E { parse :: (n: i32) -> (i32, !E) {
if n < 0 { raise error.Bad; } if n < 0 { raise error.Bad; }
return n; return n;
} }

View File

@@ -9,7 +9,7 @@
E :: error { Bad, Empty } 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.Bad; }
if n == 0 { raise error.Empty; } if n == 0 { raise error.Empty; }
return n * 2; return n * 2;

View File

@@ -12,14 +12,14 @@
E :: error { Bad, Empty } E :: error { Bad, Empty }
parse :: (n: i32) -> Tuple(i32, i32) !E { parse :: (n: i32) -> (i32, i32, !E) {
if n < 0 { raise error.Bad; } if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; } 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}. // Multi-value `try` in a multi-value caller — propagates {undef, undef, tag}.
inc :: (n: i32) -> Tuple(i32, i32) !E { inc :: (n: i32) -> (i32, i32, !E) {
v, b := try parse(n); v, b := try parse(n);
return .(v + 1, b + 1); return .(v + 1, b + 1);
} }

View File

@@ -13,12 +13,12 @@
E :: error { Bad, Empty } E :: error { Bad, Empty }
pair :: (n: i32) -> Tuple(i32, i32) !E { pair :: (n: i32) -> (i32, i32, !E) {
if n < 0 { raise error.Bad; } 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; } if n < 0 { raise error.Bad; }
return n * 2; return n * 2;
} }

View File

@@ -8,7 +8,7 @@
E :: error { BadDigit, Empty, Overflow } 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.BadDigit; }
if n == 0 { raise error.Empty; } if n == 0 { raise error.Empty; }
return n * 2; return n * 2;

View File

@@ -14,7 +14,7 @@
ParseErr :: error { Empty, BadDigit }; 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.Empty; } // pushes a frame
if n < 0 { raise error.BadDigit; } if n < 0 { raise error.BadDigit; }
return n * 2; return n * 2;

View File

@@ -9,13 +9,13 @@
ParseErr :: error { Empty, BadDigit }; 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.Empty; }
if n < 0 { raise error.BadDigit; } if n < 0 { raise error.BadDigit; }
return n * 2; return n * 2;
} }
main :: () -> i32 !ParseErr { main :: () -> (i32, !ParseErr) {
v := try inner(32); // succeeds → v = 64 v := try inner(32); // succeeds → v = 64
print("v = {}\n", v); print("v = {}\n", v);
return v; // success → exit code 64 return v; // success → exit code 64

View File

@@ -12,7 +12,7 @@
E :: error { A, B }; 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.A; }
if n < 0 { raise error.B; } if n < 0 { raise error.B; }
return n; return n;
@@ -23,7 +23,7 @@ fv :: (n: i32) -> !E { // void (pure) failable
return; return;
} }
main :: () -> i32 !E { main :: () -> (i32, !E) {
onfail print("onfail fired (BUG)\n"); // must NOT fire — every chain below absorbs onfail print("onfail fired (BUG)\n"); // must NOT fire — every chain below absorbs
r : i32 = 0; r : i32 = 0;

View File

@@ -10,12 +10,12 @@
E :: error { A }; E :: error { A };
fa :: (n: i32) -> i32 !E { fa :: (n: i32) -> (i32, !E) {
if n == 0 { raise error.A; } if n == 0 { raise error.A; }
return n; 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 v := try fa(0) or try fa(0) or try fa(0); // all fail → propagate to main
return v; return v;
} }

View File

@@ -9,7 +9,7 @@
SmokeErr :: error { Empty, BadDigit, Overflow } SmokeErr :: error { Empty, BadDigit, Overflow }
// value-carrying, named set // 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.BadDigit; }
if n == 0 { raise error.Empty; } if n == 0 { raise error.Empty; }
if n > 99 { raise error.Overflow; } if n > 99 { raise error.Overflow; }
@@ -23,7 +23,7 @@ sm_check :: (ok: bool) -> ! {
} }
// multi-value, inferred set: `try` propagates; SCC absorbs SmokeErr // multi-value, inferred set: `try` propagates; SCC absorbs SmokeErr
sm_pair :: (a: i32, b: i32) -> Tuple(i32, i32) ! { sm_pair :: (a: i32, b: i32) -> (i32, i32, !) {
x := try sm_parse(a); x := try sm_parse(a);
y := try sm_parse(b); y := try sm_parse(b);
return .(x, y); return .(x, y);
@@ -38,7 +38,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(" defer A\n"); defer print(" defer A\n");
onfail print(" onfail B\n"); onfail print(" onfail B\n");
if fail { raise error.Acquire; } 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 // 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); v := try sm_parse(a) or try sm_parse(b);
return v; return v;
} }
@@ -54,18 +54,18 @@ sm_first :: (a: i32, b: i32) -> i32 ! {
// --- Composition (ERR E5.1): failable closures, widening, generics --- // --- Composition (ERR E5.1): failable closures, widening, generics ---
// Closure(...) param, try-propagated (the env is carried) // 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); return try cb(n);
} }
// bare fn-type param: a NON-failable closure literal widens into the failable // bare fn-type param: a NON-failable closure literal widens into the failable
// slot (the ∅-widening adapter wraps `{value, 0}`) // 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; return cb(n) catch (e) -1;
} }
// generic ($T) value-carrying failable composition, monomorphized per call // 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(); return try f();
} }
@@ -120,15 +120,15 @@ main :: () {
iv, ierr := sm_acquire(false); iv, ierr := sm_acquire(false);
// composition: inline failable closure literal through a Closure(...) param // 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: {}\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 // non-failable closure literal widened into the failable bare slot
print("widen: {}\n", sm_widen(closure((x: i32) -> i32 => x + 1), 9)); // 10 print("widen: {}\n", sm_widen(closure((x: i32) -> i32 => x + 1), 9)); // 10
// generic failable composition (monomorphized at i32) // 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"); print("errors ok\n");
} }

View File

@@ -7,7 +7,7 @@
E :: error { Bad, Empty } 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.Bad; }
if n == 0 { raise error.Empty; } if n == 0 { raise error.Empty; }
return n * 2; return n * 2;

View File

@@ -7,7 +7,7 @@
E :: error { Bad, Empty } 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.Bad; }
if n == 0 { raise error.Empty; } if n == 0 { raise error.Empty; }
return n * 2; return n * 2;

View File

@@ -10,12 +10,12 @@
E :: error { Neg } 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 { main :: () -> i32 {
// block-body and arrow-body failable closures, called directly // block-body and arrow-body failable closures, called directly
m := closure((x: i64) -> i64 !E { if x < 0 { raise error.Neg; } return x * 2; }); m := closure((x: i64) -> (i64, !E) { if x < 0 { raise error.Neg; } return x * 2; });
n := closure((x: i64) -> i64 !E => x + 1); 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 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 // failable closure passed as a Closure(...) parameter

View File

@@ -12,21 +12,21 @@
E :: error { Neg } E :: error { Neg }
bare :: (cb: (i64) -> i64 !E, n: i64) -> i64 { return cb(n) catch (e) -1; } 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); } 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 { main :: () -> i32 {
// failable closure literal through a bare fn-type param (matching ABI) // failable closure literal through a bare fn-type param (matching ABI)
print("bare ok={} err={}\n", 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) { 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) => x * 2), -1)); // ok=10; err: arrow never raises → cb(-1) = -2
// Closure(...) param, try-propagated, then caught at the call site // Closure(...) param, try-propagated, then caught at the call site
print("chain ok={} err={}\n", print("chain ok={} err={}\n",
chain(closure((x: i64) -> i64 !E => x + 6), 4) catch (e) 0, // 10 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) { raise error.Neg; }), 1) catch (e) 0); // 0
// NON-failable closure literal widened into the failable bare slot // NON-failable closure literal widened into the failable bare slot
print("widen={}\n", bare(closure((x: i64) -> i64 => x + 1), 9)); // 10 print("widen={}\n", bare(closure((x: i64) -> i64 => x + 1), 9)); // 10

View File

@@ -10,7 +10,7 @@
All :: error { Negative, Other } All :: error { Negative, Other }
// `h` is a bare-`!` Closure slot; the caller declares the union as `!All`. // `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); return try h(x);
} }
@@ -19,9 +19,9 @@ main :: () -> i32 {
push Context.{ allocator = xx gpa } { push Context.{ allocator = xx gpa } {
// Two literals of the SAME shape raising DIFFERENT tags both feed the // Two literals of the SAME shape raising DIFFERENT tags both feed the
// one shared `Closure(i32)->(i32,!)` union node. // one shared `Closure(i32)->(i32,!)` union node.
handlers : List(Closure(i32) -> i32 !) = .{}; 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.Negative; } return x * 2; }));
handlers.append(closure((x: i32) -> i32 ! { if x == 0 { raise error.Other; } return x + 100; })); handlers.append(closure((x: i32) -> (i32, !) { if x == 0 { raise error.Other; } return x + 100; }));
// success paths // success paths
print("ok0={}\n", dispatch(handlers.items[0], 5) catch (e) 0); // 10 print("ok0={}\n", dispatch(handlers.items[0], 5) catch (e) 0); // 10

View File

@@ -9,16 +9,16 @@
Small :: error { Unrelated } 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 return try h(x); // Negative, Other ∉ Small → two diagnostics
} }
main :: () -> i32 { main :: () -> i32 {
gpa := GPA.init(); gpa := GPA.init();
push Context.{ allocator = xx gpa } { push Context.{ allocator = xx gpa } {
handlers : List(Closure(i32) -> i32 !) = .{}; 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.Negative; } return x; }));
handlers.append(closure((x: i32) -> i32 ! { if x == 0 { raise error.Other; } 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); print("r={}\n", reject(handlers.items[0], 5) catch (e) 0);
} }
return 0; return 0;

View File

@@ -9,7 +9,7 @@
E :: error { Neg } 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 { main :: () -> i32 {
// `-> i32` (non-failable) but the body raises → lambda-specific hint: // `-> i32` (non-failable) but the body raises → lambda-specific hint:

View File

@@ -9,21 +9,21 @@
E :: error { Bad } 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 { main :: () -> i32 {
// success, consumed by catch // 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 // success, consumed by destructure (binds value + error slot); the value
// slot is read only under an `if !err` guard (ERR E1.8 path-sensitivity) // 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 if !err { print("destr={} ok=true\n", r); } // destr=9 ok=true
// failure path: the raised tag propagates through the generic `try` // 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 // 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; return 0;
} }

View File

@@ -13,7 +13,7 @@
E :: error { Z } E :: error { Z }
bare :: (cb: (i64) -> i64, n: i64) -> i64 { return cb(n); } 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 { main :: () -> i32 {
inc := closure((x: i64) -> i64 => x + 1); // capture-free closure var inc := closure((x: i64) -> i64 => x + 1); // capture-free closure var

View File

@@ -15,7 +15,7 @@
E :: error { Bad, Empty } 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.Bad; }
if n == 0 { raise error.Empty; } if n == 0 { raise error.Empty; }
return n * 10; return n * 10;
@@ -29,7 +29,7 @@ guarded :: (n: i32) -> i32 {
} }
// `if err { raise }` in a failable function: same fall-through proof. // `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); v, err := parse(n);
if err { raise err; } if err { raise err; }
return v + 1; // err proven absent here return v + 1; // err proven absent here

View File

@@ -11,7 +11,7 @@
E :: error { Bad } E :: error { Bad }
parse :: (n: i32) -> i32 !E { parse :: (n: i32) -> (i32, !E) {
if n < 0 { raise error.Bad; } if n < 0 { raise error.Bad; }
return n * 10; return n * 10;
} }

View File

@@ -9,7 +9,7 @@
E :: error { Bad } E :: error { Bad }
failing :: () -> !E { raise error.Bad; } failing :: () -> !E { raise error.Bad; }
recover :: () -> i32 !E { raise error.Bad; } recover :: () -> (i32, !E) { raise error.Bad; }
work :: (n: i32) -> !E { work :: (n: i32) -> !E {
defer print("defer: always\n"); // plain cleanup defer print("defer: always\n"); // plain cleanup

View File

@@ -9,7 +9,7 @@
E :: error { Bad } E :: error { Bad }
probe :: () -> i32 !E { return 21; } probe :: () -> (i32, !E) { return 21; }
failing :: () -> !E { raise error.Bad; } failing :: () -> !E { raise error.Bad; }
run :: () { run :: () {

View File

@@ -24,7 +24,7 @@
E :: error { Bad } E :: error { Bad }
failing :: () -> !E { raise error.Bad; } failing :: () -> !E { raise error.Bad; }
recover :: () -> i32 !E { return 21; } recover :: () -> (i32, !E) { return 21; }
work :: () { work :: () {
defer { defer {

View File

@@ -12,7 +12,7 @@
E :: error { Bad } E :: error { Bad }
parse :: (n: i32) -> i32 !E { parse :: (n: i32) -> (i32, !E) {
if n < 0 { raise error.Bad; } if n < 0 { raise error.Bad; }
return n * 10; return n * 10;
} }

View File

@@ -8,7 +8,7 @@
E :: error { Bad, Empty } 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.Bad; }
if n == 0 { raise error.Empty; } if n == 0 { raise error.Empty; }
return n * 2; return n * 2;

View File

@@ -16,7 +16,7 @@
Color :: enum { red; green; blue; } Color :: enum { red; green; blue; }
E :: error { Nope } E :: error { Nope }
pick :: (s: string) -> Color !E { pick :: (s: string) -> (Color, !E) {
if s == "red" { return .red; } if s == "red" { return .red; }
if s == "blue" { return .blue; } // non-zero ordinal (2) if s == "blue" { return .blue; } // non-zero ordinal (2)
raise error.Nope; raise error.Nope;

View File

@@ -22,13 +22,13 @@ Color :: enum { red; green; blue; }
E :: error { Nope } E :: error { Nope }
// F1: bare-value success path AND explicit-tuple error path in one function. // 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} 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. // 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 == "red" { return .red; } // bare value, inline path → {0, 0}
if s == "blue" { return .blue; } // bare value, inline path → {2, 0} if s == "blue" { return .blue; } // bare value, inline path → {2, 0}
raise error.Nope; // inline error path → {undef, 1} raise error.Nope; // inline error path → {undef, 1}

View File

@@ -7,7 +7,7 @@
E :: error { Boom } E :: error { Boom }
f :: (fail: bool) -> i64 !E { f :: (fail: bool) -> (i64, !E) {
if fail { raise error.Boom; } if fail { raise error.Boom; }
return 42; return 42;
} }

View File

@@ -5,4 +5,4 @@ LE :: error { Bad }
Box :: struct ($R: Type) { v: R; } Box :: struct ($R: Type) { v: R; }
// Returns `($R, !LE)` — a value-failable. `$R` is inferred from the arg. // 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; }

View File

@@ -1,4 +1,4 @@
// A failable function returning a NAMED tuple value `-> Tuple(x: A, y: B) !E` // A failable function returning a NAMED tuple value `-> (x: A, y: B, !E)`
// flattens its value fields into the result tuple (`{ x: A, y: B, err }`), // 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 // 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` fallback. Exercises the success path, a `raise` path, and an
@@ -12,7 +12,7 @@
E :: error { Bad } E :: error { Bad }
two :: (n: i64) -> Tuple(x: i64, y: i64) !E { two :: (n: i64) -> (x: i64, y: i64, !E) {
if n < 0 { raise error.Bad; } if n < 0 { raise error.Bad; }
return .(x = n, y = n + 1); return .(x = n, y = n + 1);
} }

View File

@@ -1,4 +1,4 @@
// Value-carrying failable functions (`-> T !E`) whose body ends in a trailing // Value-carrying failable functions (`-> (T, !E)`) whose body ends in a trailing
// success EXPRESSION (no explicit `return`) must set the success error slot to // success EXPRESSION (no explicit `return`) must set the success error slot to
// 0 — the caller's `catch` must NOT fire and the success value must be intact. // 0 — the caller's `catch` must NOT fire and the success value must be intact.
// Regression (issue 0190): `lowerValueBody` used to `coerceToType`+`ret` the // Regression (issue 0190): `lowerValueBody` used to `coerceToType`+`ret` the
@@ -10,16 +10,16 @@
E :: error { Bad } E :: error { Bad }
// Single-value trailing-expression success. // Single-value trailing-expression success.
val :: () -> i64 !E { 99 } val :: () -> (i64, !E) { 99 }
// String trailing-expression success. // String trailing-expression success.
sval :: () -> string !E { "hi" } sval :: () -> (string, !E) { "hi" }
// Multi-value (tuple) trailing-expression success. // Multi-value (tuple) trailing-expression success.
mval :: () -> Tuple(i64, i64) !E { .(1, 2) } mval :: () -> (i64, i64, !E) { .(1, 2) }
// A real error still propagates through the value-failable channel. // A real error still propagates through the value-failable channel.
fval :: (n: i64) -> i64 !E { fval :: (n: i64) -> (i64, !E) {
if n < 0 { raise error.Bad; } if n < 0 { raise error.Bad; }
n + 1 n + 1
} }

View File

@@ -1,4 +1,4 @@
// Generic value-carrying failable functions (`($T) -> T !E`) whose body ends in // Generic value-carrying failable functions (`($T) -> (T, !E)`) whose body ends in
// a trailing success EXPRESSION (no explicit `return`) must set the success // a trailing success EXPRESSION (no explicit `return`) must set the success
// error slot to 0 — the caller's `catch` must NOT fire and the value must be // error slot to 0 — the caller's `catch` must NOT fire and the value must be
// intact across instantiations (i64 / string / struct), and `or` must yield the // intact across instantiations (i64 / string / struct), and `or` must yield the
@@ -14,10 +14,10 @@ E :: error { Bad }
Point :: struct { x: i64; y: i64; } Point :: struct { x: i64; y: i64; }
// Generic trailing-expression success — instantiated at i64 / string / struct. // Generic trailing-expression success — instantiated at i64 / string / struct.
gen :: ($T: Type, v: T) -> T !E { v } gen :: ($T: Type, v: T) -> (T, !E) { v }
// Generic that RAISES — the caller's catch must still fire. // Generic that RAISES — the caller's catch must still fire.
gfail :: ($T: Type, v: T, bad: bool) -> T !E { gfail :: ($T: Type, v: T, bad: bool) -> (T, !E) {
if bad { raise error.Bad; } if bad { raise error.Bad; }
v v
} }

View File

@@ -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
// 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.BadDigit; }
if n == 0 { raise error.Empty; } if n == 0 { raise error.Empty; }
if n > 99 { raise error.Overflow; } if n > 99 { raise error.Overflow; }
@@ -159,7 +159,7 @@ 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
// 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) -> Tuple(i32, i32) ! { sm_pair :: (a: i32, b: i32) -> (i32, i32, !) {
x := try sm_parse(a); x := try sm_parse(a);
y := try sm_parse(b); y := try sm_parse(b);
return .(x, y); return .(x, y);
@@ -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
// `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"); defer print(" smoke defer A\n");
onfail print(" smoke onfail B\n"); onfail print(" smoke onfail B\n");
if fail { raise error.Acquire; } 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
// `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); v := try sm_parse(a) or try sm_parse(b);
return v; return v;
} }

View File

@@ -1,7 +1,7 @@
// A multi-return signature may carry an error channel as its LAST slot: // A multi-return signature may carry an error channel as its LAST slot:
// `-> (i32, bool, !)` returns two values OR fails. This reuses the existing // `-> (i32, bool, !)` returns two values OR fails. This reuses the existing
// value-carrying-failable machinery — the error is always the final slot. A // value-carrying-failable machinery — the error is always the final slot. A
// single-value failable `-> (T, !)` (one value + error) is exactly `-> T !`, // single-value failable `-> (T, !)` (one value + error) is exactly `-> (T, !)`,
// NOT a multi-return. // NOT a multi-return.
// //
// Consume it like any failable: `catch` / `or` / a guarded destructure or // Consume it like any failable: `catch` / `or` / a guarded destructure or

View File

@@ -263,7 +263,7 @@ is_long_flag :: (s: string) -> bool {
// Parse `args` (the logical argv) against the `commands` table, writing // Parse `args` (the logical argv) against the `commands` table, writing
// the offending token into `diag` on the error path. See the section // the offending token into `diag` on the error path. See the section
// header for grammar, failure contract, and heap discipline. // 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 ── // ── Dispatch: match (args[0], args[1]) against the command table ──
if args.len < 2 { if args.len < 2 {
diag.index = if args.len == 0 then -1 else 0; diag.index = if args.len == 0 then -1 else 0;

View File

@@ -116,7 +116,7 @@ Loop :: struct {
// long-lived-container rule). // long-lived-container rule).
own: Allocator; own: Allocator;
init :: () -> Loop !EventErr { init :: () -> (Loop, !EventErr) {
e := ep.ep_create(); e := ep.ep_create();
if e < 0 { raise error.Init; } if e < 0 { raise error.Init; }
return Loop.{ epfd = e, regs = .{}, own = context.allocator }; return Loop.{ epfd = e, regs = .{}, own = context.allocator };
@@ -216,7 +216,7 @@ Loop :: struct {
// Fill `out` with ready events, waiting at most `timeout_ms` // Fill `out` with ready events, waiting at most `timeout_ms`
// (negative = forever). Returns the count; 0 is a timeout. // (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]ep.EpollEvent = ---; raw : [64]ep.EpollEvent = ---;
cap : i64 = 64; cap : i64 = 64;
if xx out.len < cap { cap = xx out.len; } if xx out.len < cap { cap = xx out.len; }
@@ -254,7 +254,7 @@ Loop :: struct {
Loop :: struct { Loop :: struct {
kq: i32 = -1; kq: i32 = -1;
init :: () -> Loop !EventErr { init :: () -> (Loop, !EventErr) {
q := kqb.kqueue(); q := kqb.kqueue();
if q < 0 { raise error.Init; } if q < 0 { raise error.Init; }
return Loop.{ kq = q }; return Loop.{ kq = q };
@@ -298,7 +298,7 @@ Loop :: struct {
// Fill `out` with ready events, waiting at most `timeout_ms` // Fill `out` with ready events, waiting at most `timeout_ms`
// (negative = forever). Returns the count; 0 is a timeout. // (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 = ---; raw : [64]kqb.Kevent = ---;
cap : i64 = 64; cap : i64 = 64;
if xx out.len < cap { cap = xx out.len; } if xx out.len < cap { cap = xx out.len; }

View File

@@ -263,7 +263,7 @@ Server :: struct {
ctx: usize = 0; ctx: usize = 0;
ps: *PoolState = null; // non-null iff cfg.thread_pool_count > 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); lfd := socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0);
if lfd < 0 { raise error.Bind; } if lfd < 0 { raise error.Bind; }
one : i32 = 1; one : i32 = 1;

View File

@@ -172,7 +172,7 @@ async :: ufcs (io: Io, worker: Closure() -> $R) -> *Future($R) {
// resumes it. Re-checks state after the wake (the worker set `.ready` before // resumes it. Re-checks state after the wake (the worker set `.ready` before
// waking). A worker that finished BEFORE `await` leaves `.ready`, so no park, no // waking). A worker that finished BEFORE `await` leaves `.ready`, so no park, no
// lost wakeup. // lost wakeup.
await :: ufcs (f: *Future($R)) -> $R !IoErr { await :: ufcs (f: *Future($R)) -> ($R, !IoErr) {
if f.canceled.load(.acquire) { raise error.Canceled; } if f.canceled.load(.acquire) { raise error.Canceled; }
if f.state == .pending { if f.state == .pending {
// ONE awaiter per future (M:1): the single `park` slot records one parked // ONE awaiter per future (M:1): the single `park` slot records one parked

View File

@@ -321,7 +321,7 @@ write_object :: (obj: Object, sink: *Sink) -> !JsonError {
// bytes written. Raises `error.Overflow` if `dst` is too small (the // bytes written. Raises `error.Overflow` if `dst` is too small (the
// partial contents of `dst` are then undefined — nothing is truncated // partial contents of `dst` are then undefined — nothing is truncated
// silently). No allocation. // 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 }; sink := Sink.{ dst = dst };
try write_value(v, @sink); try write_value(v, @sink);
return sink.pos; 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 // Lowercase/uppercase hex nibble value (0..15) of an ASCII byte; a non-hex
// byte in a `\uXXXX` escape is a `BadEscape`. // 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 >= 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 >= 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' 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 // 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. // 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; } if i + 4 > end { raise error.BadEscape; }
v := 0; v := 0;
k := 0; k := 0;
@@ -464,7 +464,7 @@ Parser :: struct {
// Decode the escaped string body in [start, end) into `out`, returning // Decode the escaped string body in [start, end) into `out`, returning
// the decoded byte length. Pass 1 (in parse_string) guarantees there is // the decoded byte length. Pass 1 (in parse_string) guarantees there is
// no dangling backslash, so the byte after every `\` is in range. // 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; di := 0;
i := start; i := start;
while i < end { while i < end {
@@ -511,7 +511,7 @@ Parser :: struct {
// a zero-copy VIEW into `src` when the body has no escapes; otherwise // 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` // decodes into an `alloc`-ed buffer (bounded by the raw span). `pos`
// ends just past the closing quote. // ends just past the closing quote.
parse_string :: (self: *Parser) -> string !JsonParseError { parse_string :: (self: *Parser) -> (string, !JsonParseError) {
self.pos += 1; // consume opening quote self.pos += 1; // consume opening quote
start := self.pos; start := self.pos;
has_escape := false; has_escape := false;
@@ -547,7 +547,7 @@ Parser :: struct {
// Parse an i64 integer (optional '-', then digits). Rejects leading // Parse an i64 integer (optional '-', then digits). Rejects leading
// zeros, a fraction/exponent tail, and any value outside i64 — all // zeros, a fraction/exponent tail, and any value outside i64 — all
// `BadNumber`. Accumulates in NEGATIVE space so i64 MIN parses exactly. // `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 // i64 bounds, built positionally because |MIN| is not a
// representable positive i64 literal. `min_div10` is `MIN / 10` // representable positive i64 literal. `min_div10` is `MIN / 10`
// truncated toward zero (remainder -8) — the digit loop's overflow // 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 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 '[' self.pos += 1; // consume '['
arr : Array = .{}; arr : Array = .{};
self.skip_ws(); self.skip_ws();
@@ -609,7 +609,7 @@ Parser :: struct {
// Parse an object starting at '{'. Keys must be strings; insertion // Parse an object starting at '{'. Keys must be strings; insertion
// order is preserved (duplicate keys are kept, never merged). // 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 '{' self.pos += 1; // consume '{'
obj : Object = .{}; obj : Object = .{};
self.skip_ws(); self.skip_ws();
@@ -640,7 +640,7 @@ Parser :: struct {
} }
// Parse any single value (after skipping leading whitespace). // Parse any single value (after skipping leading whitespace).
parse_value :: (self: *Parser) -> Value !JsonParseError { parse_value :: (self: *Parser) -> (Value, !JsonParseError) {
self.skip_ws(); self.skip_ws();
if self.pos >= self.src.len { raise error.UnexpectedEnd; } if self.pos >= self.src.len { raise error.UnexpectedEnd; }
c := self.src[self.pos]; c := self.src[self.pos];
@@ -659,7 +659,7 @@ Parser :: struct {
// `alloc` for composite nodes and decoded (escaped) strings. Un-escaped // `alloc` for composite nodes and decoded (escaped) strings. Un-escaped
// string values are VIEWS into `src` and are valid only while `src` lives. // string values are VIEWS into `src` and are valid only while `src` lives.
// Trailing non-whitespace after the value raises `error.TrailingGarbage`. // 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 }; p := Parser.{ src = src, alloc = alloc };
v := try p.parse_value(); v := try p.parse_value();
p.skip_ws(); p.skip_ws();

View File

@@ -1079,7 +1079,7 @@ go :: ufcs (self: *Scheduler, work: Closure() -> $R) -> *Task($R) {
// Suspend the caller until the task completes; return its value (or raise on // 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 // cancel). MUST be called from inside a fiber (so there is a `self.current` to
// park) — typically from a fiber spawned via `s.spawn(...)`. // 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.canceled != 0 { raise error.Canceled; }
if t.state == .pending { if t.state == .pending {
// ONE waiter per task (enforced). A `Task` holds a single `waiter` slot; // ONE waiter per task (enforced). A `Task` holds a single `waiter` slot;

View File

@@ -93,7 +93,7 @@ SockErr :: error {
// Accept one pending connection on a nonblocking listener. A connection // Accept one pending connection on a nonblocking listener. A connection
// that died between queueing and accept (ECONNABORTED) is skipped, not // that died between queueing and accept (ECONNABORTED) is skipped, not
// surfaced — the listener is fine. // surfaced — the listener is fine.
accept_nb :: (fd: i32) -> i32 !SockErr { accept_nb :: (fd: i32) -> (i32, !SockErr) {
while true { while true {
c := accept(fd, null, null); c := accept(fd, null, null);
if c >= 0 { return c; } 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 // Read up to `cap` bytes. Returns the byte count (> 0); an orderly EOF
// or a peer reset is Closed. // 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 { while true {
n := read(fd, buf, cap); n := read(fd, buf, cap);
if n > 0 { return xx n; } 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 // Write up to `len` bytes, returning how many the kernel took (possibly
// fewer — the caller continues from there on the next writability). // 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 { while true {
n := write(fd, buf, len); n := write(fd, buf, len);
if n >= 0 { return xx n; } if n >= 0 { return xx n; }

View File

@@ -106,7 +106,7 @@ Thread :: struct {
// `entry` is the C->sx boundary: abi(.c), fabricates its own // `entry` is the C->sx boundary: abi(.c), fabricates its own
// Context before touching default-conv sx code (examples/1636). // 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 = .{}; t : Thread = .{};
if pthread_create(@t.handle, null, entry, arg) != 0 { raise error.Spawn; } if pthread_create(@t.handle, null, entry, arg) != 0 { raise error.Spawn; }
return t; return t;
@@ -144,7 +144,7 @@ Pool :: struct {
// Heap-allocate (the pool must never move: workers hold its address, // Heap-allocate (the pool must never move: workers hold its address,
// and it embeds a live mutex), init in place, spawn the workers. // 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; alloc := context.allocator;
p : *Pool = xx alloc.alloc_bytes(size_of(Pool)); p : *Pool = xx alloc.alloc_bytes(size_of(Pool));
p.* = Pool.{}; p.* = Pool.{};

View File

@@ -461,7 +461,7 @@ SqliteStmt :: struct {
// ── execution ── // ── execution ──
// SQLITE_ROW / SQLITE_DONE on success; anything else raises with the // SQLITE_ROW / SQLITE_DONE on success; anything else raises with the
// detail left in the connection's errmsg. // detail left in the connection's errmsg.
step :: (self: *SqliteStmt) -> i32 !SqliteErr { step :: (self: *SqliteStmt) -> (i32, !SqliteErr) {
rc := sqlite3_step(self.handle); rc := sqlite3_step(self.handle);
if rc != SQLITE_ROW and rc != SQLITE_DONE { raise error.Step; } if rc != SQLITE_ROW and rc != SQLITE_DONE { raise error.Step; }
return rc; return rc;
@@ -564,7 +564,7 @@ ColumnMeta :: struct {
Sqlite :: struct { Sqlite :: struct {
handle: usize; handle: usize;
open :: (path: string) -> Sqlite !SqliteErr { open :: (path: string) -> (Sqlite, !SqliteErr) {
h : usize = 0; h : usize = 0;
rc := sqlite3_open(to_cstring(path), @h); rc := sqlite3_open(to_cstring(path), @h);
if rc != SQLITE_OK { if rc != SQLITE_OK {
@@ -574,7 +574,7 @@ Sqlite :: struct {
return Sqlite.{ handle = h }; return Sqlite.{ handle = h };
} }
open_v2 :: (path: string, flags: i32) -> Sqlite !SqliteErr { open_v2 :: (path: string, flags: i32) -> (Sqlite, !SqliteErr) {
h : usize = 0; h : usize = 0;
rc := sqlite3_open_v2(to_cstring(path), @h, flags, 0); rc := sqlite3_open_v2(to_cstring(path), @h, flags, 0);
if rc != SQLITE_OK { if rc != SQLITE_OK {
@@ -603,7 +603,7 @@ Sqlite :: struct {
return; return;
} }
prepare :: (self: *Sqlite, sql: string) -> SqliteStmt !SqliteErr { prepare :: (self: *Sqlite, sql: string) -> (SqliteStmt, !SqliteErr) {
sh : usize = 0; sh : usize = 0;
rc := sqlite3_prepare_v2(self.handle, sql.ptr, xx sql.len, @sh, 0); rc := sqlite3_prepare_v2(self.handle, sql.ptr, xx sql.len, @sh, 0);
if rc != SQLITE_OK { raise error.Prepare; } if rc != SQLITE_OK { raise error.Prepare; }
@@ -611,7 +611,7 @@ Sqlite :: struct {
} }
// prepare with SQLITE_PREPARE_* flags (e.g. PERSISTENT for the // prepare with SQLITE_PREPARE_* flags (e.g. PERSISTENT for the
// statement cache a storage layer keeps hot). // 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; sh : usize = 0;
rc := sqlite3_prepare_v3(self.handle, sql.ptr, xx sql.len, flags, @sh, 0); rc := sqlite3_prepare_v3(self.handle, sql.ptr, xx sql.len, flags, @sh, 0);
if rc != SQLITE_OK { raise error.Prepare; } if rc != SQLITE_OK { raise error.Prepare; }
@@ -685,7 +685,7 @@ Sqlite :: struct {
} }
// Schema introspection for one column of "main".`table`. // 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; dt : usize = 0;
cs : usize = 0; cs : usize = 0;
nn : i32 = 0; nn : i32 = 0;
@@ -703,7 +703,7 @@ Sqlite :: struct {
// ── serialization ── // ── serialization ──
// The whole "main" database as bytes (a valid database image). // The whole "main" database as bytes (a valid database image).
serialize :: (self: *Sqlite) -> string !SqliteErr { serialize :: (self: *Sqlite) -> (string, !SqliteErr) {
size : i64 = 0; size : i64 = 0;
p := sqlite3_serialize(self.handle, to_cstring("main"), @size, 0); p := sqlite3_serialize(self.handle, to_cstring("main"), @size, 0);
if p == null { raise error.Serialize; } if p == null { raise error.Serialize; }
@@ -734,7 +734,7 @@ Sqlite :: struct {
SqliteBlob :: struct { SqliteBlob :: struct {
handle: usize; 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; h : usize = 0;
rc := sqlite3_blob_open(db.handle, to_cstring("main"), to_cstring(table), to_cstring(column), rc := sqlite3_blob_open(db.handle, to_cstring("main"), to_cstring(table), to_cstring(column),
rowid, if writable then 1 else 0, @h); rowid, if writable then 1 else 0, @h);
@@ -751,7 +751,7 @@ SqliteBlob :: struct {
bytes :: (self: *SqliteBlob) -> i32 { bytes :: (self: *SqliteBlob) -> i32 {
return sqlite3_blob_bytes(self.handle); 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; len : i64 = n;
raw : [*]u8 = xx context.allocator.alloc_bytes(len + 1); raw : [*]u8 = xx context.allocator.alloc_bytes(len + 1);
rc := sqlite3_blob_read(self.handle, raw, n, offset); rc := sqlite3_blob_read(self.handle, raw, n, offset);
@@ -778,7 +778,7 @@ SqliteBlob :: struct {
SqliteBackup :: struct { SqliteBackup :: struct {
handle: usize; 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")); h := sqlite3_backup_init(dst.handle, to_cstring("main"), src.handle, to_cstring("main"));
if h == 0 { raise error.Backup; } if h == 0 { raise error.Backup; }
return SqliteBackup.{ handle = h }; return SqliteBackup.{ handle = h };

View File

@@ -604,7 +604,7 @@ main :: () {
Tasks complete in deadline order, not spawn or await order. The runtime offers: Tasks complete in deadline order, not spawn or await order. The runtime offers:
- **`go(work) -> *Task($R)`** / **`wait() -> R !TaskErr`** / **`cancel()`** — the - **`go(work) -> *Task($R)`** / **`wait() -> (R, !TaskErr)`** / **`cancel()`** — the
task layer. `wait` rides the `!` error channel so a cancel surfaces as task layer. `wait` rides the `!` error channel so a cancel surfaces as
`error.Canceled`. `error.Canceled`.
- **`spawn`**, **`yield_now`**, **`suspend_self`**, **`wake`** — the raw fiber - **`spawn`**, **`yield_now`**, **`suspend_self`**, **`wake`** — the raw fiber

View File

@@ -903,7 +903,7 @@ VALUE: it is represented internally by a reused tuple TypeId (same ABI), but it
is valid ONLY in a function/closure return position — a parameter, field, or is valid ONLY in a function/closure return position — a parameter, field, or
variable annotation `x: (A, B)` is rejected (use `Tuple(…)` for a tuple value). variable annotation `x: (A, B)` is rejected (use `Tuple(…)` for a tuple value).
A single-value `-> (T, !)` (one value + error) is NOT a multi-return; it is A single-value `-> (T, !)` (one value + error) is NOT a multi-return; it is
exactly the failable `-> T !`. exactly the failable `-> (T, !)`.
```sx ```sx
divmod :: (a: i64, b: i64) -> (i64, i64) { return a / b, a % b; } divmod :: (a: i64, b: i64) -> (i64, i64) { return a / b, a % b; }
@@ -1886,7 +1886,7 @@ name :: (params) -> return_type {
A trailing `!` in the return type marks the function **failable** — it adds a A trailing `!` in the return type marks the function **failable** — it adds a
separate error channel alongside the normal returns. The `!` sits **outside** separate error channel alongside the normal returns. The `!` sits **outside**
the tuple: `-> T !` (one value), `-> Tuple(T1, T2) !` (multi value), `-> !` the tuple: `-> (T, !)` (one value), `-> (T1, T2, !)` (multi value), `-> !`
(void). The `!` is not a wrapper around the value; it is one more return slot. (void). The `!` is not a wrapper around the value; it is one more return slot.
See [§12 Error Handling](#12-error-handling). See [§12 Error Handling](#12-error-handling).
@@ -3154,7 +3154,7 @@ main :: () {
`main` takes no arguments. Its return type may be any of: void (`()`, `main` takes no arguments. Its return type may be any of: void (`()`,
`-> ()`, `-> void`, or no annotation), an integer type (POSIX exit code), `-> ()`, `-> void`, or no annotation), an integer type (POSIX exit code),
`-> !` (pure failable), or `-> int_type !` (value-carrying failable). `-> !` (pure failable), or `-> (int_type, !)` (value-carrying failable).
The exit code is `0` for void / `-> !` success, the integer return The exit code is `0` for void / `-> !` success, the integer return
truncated to `u8` otherwise. An error that escapes a failable `main` truncated to `u8` otherwise. An error that escapes a failable `main`
prints the unhandled-error header + return trace to stderr and exits `1`. prints the unhandled-error header + return trace to stderr and exits `1`.
@@ -3165,11 +3165,12 @@ See [§12 Error Handling](#12-error-handling).
## 12. Error Handling ## 12. Error Handling
sx models recoverable errors as a **separate return channel**, not a wrapped sx models recoverable errors as a **separate return channel**, not a wrapped
result type. A trailing `!` in a function's return type adds one extra return result type. A `!` as the **last slot** of the parenthesized result list adds one
slot — a `u32` error tag — alongside the normal value slots. This keeps sx's extra return slot — a `u32` error tag — alongside the normal value slots. This
native multi-return ergonomics: `-> Tuple(i32, i64) !` is a function returning keeps sx's native multi-return ergonomics: `-> (i32, i64, !)` is a function
two values *and* an error, with no tuple-in-a-wrapper. The `!` sits **outside** returning two values *and* an error, with no tuple-in-a-wrapper. A single-value
the `Tuple`. failable is `-> (T, !)`; an error-only failable is `-> !`. (There is no bare
`-> T !` spelling — the error channel always rides inside the `(…, !)` list.)
This section is the canonical surface reference. The design rationale, This section is the canonical surface reference. The design rationale,
trade-offs, and implementation breakdown live in `current/PLAN-ERR.md`. trade-offs, and implementation breakdown live in `current/PLAN-ERR.md`.
@@ -3177,10 +3178,10 @@ trade-offs, and implementation breakdown live in `current/PLAN-ERR.md`.
### Failable signatures ### Failable signatures
```sx ```sx
parse_digit :: (s: string) -> i32 ! { ... } // one value + error parse_digit :: (s: string) -> (i32, !) { ... } // one value + error
parse :: (s: string) -> Tuple(i32, i64) ! { ... } // multi-value + error parse :: (s: string) -> (i32, i64, !) { ... } // multi-value + error
must_init :: () -> ! { ... } // pure failable, no value must_init :: () -> ! { ... } // pure failable, no value
divide :: (a: i32, b: i32) -> i32 !MathErr { ... } // named set divide :: (a: i32, b: i32) -> (i32, !MathErr) { ... } // named set
``` ```
The `!` is always the **last** slot. `0` in the error slot means "no error"; The `!` is always the **last** slot. `0` in the error slot means "no error";
@@ -3195,7 +3196,7 @@ Two forms of error set:
ParseErr :: error { BadDigit, Overflow, Empty }; ParseErr :: error { BadDigit, Overflow, Empty };
// Inferred set — bare `!` collects whatever tags the body raises. // Inferred set — bare `!` collects whatever tags the body raises.
quick :: () -> i32 ! { quick :: () -> (i32, !) {
if cond raise error.SomeAdHocTag; // mints into the inferred set if cond raise error.SomeAdHocTag; // mints into the inferred set
return 0; return 0;
} }
@@ -3393,14 +3394,14 @@ On success exit (fall-through, `return`, `break` / `continue` without an error)
it is skipped — only `defer` runs. it is skipped — only `defer` runs.
```sx ```sx
make_handle :: () -> Handle ! { make_handle :: () -> (Handle, !) {
h := try open(); h := try open();
onfail close(h); // close ONLY on a subsequent failure onfail close(h); // close ONLY on a subsequent failure
try configure(h); // fails → onfail runs → close(h) try configure(h); // fails → onfail runs → close(h)
return h; // success → onfail skipped; caller owns h return h; // success → onfail skipped; caller owns h
} }
open :: (path: string) -> Handle ! { open :: (path: string) -> (Handle, !) {
h := try sys_open(path); h := try sys_open(path);
onfail (e) { log.warn("init failed for {}: {}", path, e); sys_close(h); } onfail (e) { log.warn("init failed for {}: {}", path, e); sys_close(h); }
... ...
@@ -3421,9 +3422,9 @@ function, or at top level, is rejected.
- **Explicit annotation required.** A closure literal's value type is inferred - **Explicit annotation required.** A closure literal's value type is inferred
as today, but if its body raises or `try`-escapes, the `!` channel is **not** as today, but if its body raises or `try`-escapes, the `!` channel is **not**
inferred — declare it (`closure((x: i32) -> i32 ! { ... })`). This keeps inferred — declare it (`closure((x: i32) -> (i32, !) { ... })`). This keeps
adding a `raise` from silently changing a lambda's type. adding a `raise` from silently changing a lambda's type.
- **Program-wide union per shape.** All `Closure(<sig>) -> T !` occurrences - **Program-wide union per shape.** All `Closure(<sig>) -> (T, !)` occurrences
with the same signature share one inferred-set node; the SCC pass unions with the same signature share one inferred-set node; the SCC pass unions
every closure flowing into any matching slot. every closure flowing into any matching slot.
- **FFI boundary.** A failable closure cannot be assigned to a non-failable - **FFI boundary.** A failable closure cannot be assigned to a non-failable

View File

@@ -876,7 +876,7 @@ pub const TupleTypeExpr = struct {
/// rejects it anywhere else), and its result is consumed only by destructuring /// rejects it anywhere else), and its result is consumed only by destructuring
/// (`a, b := f()`), never bound to a single value. Same shape as a tuple type so /// (`a, b := f()`), never bound to a single value. Same shape as a tuple type so
/// the resolver can reuse the field-resolution path. The single-value `(T, !)` /// the resolver can reuse the field-resolution path. The single-value `(T, !)`
/// (one value + error) is NOT this — it is a plain failable, `-> T !`. /// (one value + error) is NOT this — it is a plain failable.
pub const ReturnTypeExpr = struct { pub const ReturnTypeExpr = struct {
field_types: []const *Node, field_types: []const *Node,
field_names: ?[]const []const u8, // null for positional field_names: ?[]const []const u8, // null for positional

View File

@@ -341,36 +341,21 @@ test "parser: .(x) is a 1-tuple, .() is empty" {
try std.testing.expectEqual(@as(usize, 0), v2.data.tuple_literal.elements.len); try std.testing.expectEqual(@as(usize, 0), v2.data.tuple_literal.elements.len);
} }
// `-> T !` folds to the same `(T, !)` representation: tuple_type_expr whose // The legacy bare trailing-`!` spelling `-> T !` was removed — the canonical
// last field is an error_type_expr. // failable result list is `-> (T, !)`. The bare form is now a parse error.
test "parser: -> T ! folds to (T, !) tuple_type_expr" { test "parser: legacy bare `-> T !` is rejected" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator); var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit(); defer arena.deinit();
var parser = Parser.init(arena.allocator(), "f :: () -> i64 ! { 0 }"); var parser = Parser.init(arena.allocator(), "f :: () -> i64 ! { 0 }");
const root = try parser.parse(); try std.testing.expectError(error.ParseError, 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, !). // Likewise the legacy `-> Tuple(A, B) !` spelling — write `-> (A, B, !)`.
test "parser: -> Tuple(A, B) ! flattens to (A, B, !)" { test "parser: legacy bare `-> Tuple(A, B) !` is rejected" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator); var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit(); defer arena.deinit();
var parser = Parser.init(arena.allocator(), "f :: () -> Tuple(i64, i32) !ParseErr { 0 }"); var parser = Parser.init(arena.allocator(), "f :: () -> Tuple(i64, i32) !ParseErr { 0 }");
const root = try parser.parse(); try std.testing.expectError(error.ParseError, 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 // `-> !` (void + error) stays a bare error_type_expr — the trailing-`!` fold

View File

@@ -431,66 +431,27 @@ pub const Parser = struct {
return self.fail("expected ':', '=', ';', or 'extern' after type annotation"); return self.fail("expected ':', '=', ';', or 'extern' after type annotation");
} }
/// Parse a function/method/lambda return type, folding a trailing `!` /// Parse a function/method/lambda/closure/fn-pointer return type.
/// 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, !)`) /// The canonical failable / multi-return spelling wraps the result list in
/// `-> Tuple(A, B) !` ⇒ tuple_type_expr { [A, B, error_type_expr] } (== `(A, B, !)`) /// parens: `-> (T, !)`, `-> (A, B, !)`, `-> (x: A, y: B, !)` — the error
/// `-> !` ⇒ error_type_expr (bare; handled by parseTypeExpr directly) /// channel is the last slot. A bare `-> !` (error-only, no value) is parsed
/// by `parseTypeExpr` as an `error_type_expr` and is unaffected.
/// ///
/// The old inline `(T, !)` / `(T1, T2, !)` forms keep working unchanged — /// The legacy trailing-`!`-after-the-value-type spelling (`-> T !`,
/// this only ADDS the trailing-`!`-after-the-type spelling. /// `-> Tuple(A, B) !`) is REJECTED — it was a redundant second spelling of
/// `(T, !)` / `(A, B, !)` and is no longer accepted.
fn parseFnReturnType(self: *Parser) anyerror!*Node { fn parseFnReturnType(self: *Parser) anyerror!*Node {
const start = self.current.loc.start;
const ty = try self.parseTypeExpr(); const ty = try self.parseTypeExpr();
// A trailing `!` (optionally `!Named`) after the return TYPE denotes the // A trailing `!` after a VALUE return type is the removed legacy
// error channel sitting OUTSIDE the value type. A bare `-> !` is already // spelling. (`-> !` already parsed to an error_type_expr above, so a
// an error_type_expr (no value), so a `!` after one would be a doubled // `!` after one would be a doubled channel — leave that to the normal
// error channel — leave it for the normal "unexpected token" path. // "unexpected token" path.)
if (self.current.tag != .bang or ty.data == .error_type_expr) return ty; if (self.current.tag == .bang and ty.data != .error_type_expr) {
return self.fail("a failable return is written `(T, !)` — or `(A, B, !)` for multiple values — not `T !`");
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 } }); return ty;
// 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 { fn parseTypeExpr(self: *Parser) anyerror!*Node {
@@ -675,10 +636,9 @@ pub const Parser = struct {
} }
try self.expect(.r_paren); try self.expect(.r_paren);
if (self.current.tag == .arrow) { if (self.current.tag == .arrow) {
// '->' present: function type. Accept a trailing `!`/`!Named` // '->' present: function type. A failable return is the canonical
// error channel after the return type (`(i64) -> i64 !E`), folded // parenthesized list `(i64) -> (i64, !E)` (parseFnReturnType
// to the SAME `(T, !)` / `(A, B, !)` representation the inline form // rejects the bare `-> i64 !E` spelling).
// produces — the old `-> (T, !)` spelling keeps working too.
self.advance(); // skip '->' self.advance(); // skip '->'
const return_type = try self.parseFnReturnType(); const return_type = try self.parseFnReturnType();
const abi = try self.parseOptionalAbi(); const abi = try self.parseOptionalAbi();
@@ -848,10 +808,9 @@ pub const Parser = struct {
var return_type: ?*Node = null; var return_type: ?*Node = null;
if (self.current.tag == .arrow) { if (self.current.tag == .arrow) {
self.advance(); self.advance();
// Accept a trailing `!`/`!Named` error channel after the // A failable closure return is the canonical parenthesized
// closure return type (`Closure(i64) -> i64 !E`, `... -> T !`), // list `Closure(i64) -> (i64, !E)` (parseFnReturnType rejects
// folded to the same `(T, !)` / `(A, B, !)` representation; the // the bare `Closure(i64) -> i64 !E` spelling).
// old `-> (T, !)` form keeps working.
return_type = try self.parseFnReturnType(); return_type = try self.parseFnReturnType();
} }
return try self.createNode(start, .{ .closure_type_expr = .{ return try self.createNode(start, .{ .closure_type_expr = .{
@@ -5020,8 +4979,8 @@ test "parse bare failable return: named `!Foo`" {
try std.testing.expectEqualStrings("ParseErr", rt.data.error_type_expr.name.?); try std.testing.expectEqualStrings("ParseErr", rt.data.error_type_expr.name.?);
} }
test "parse failable with inferred `!` (new `-> T !` form)" { test "parse single-value failable `-> (T, !)`" {
const source = "f :: () -> i32 ! { 0; }"; const source = "f :: () -> (i32, !) { 0; }";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator); var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit(); defer arena.deinit();
var parser = Parser.init(arena.allocator(), source); var parser = Parser.init(arena.allocator(), source);
@@ -5036,20 +4995,28 @@ test "parse failable with inferred `!` (new `-> T !` form)" {
try std.testing.expect(fields[1].data.error_type_expr.name == null); try std.testing.expect(fields[1].data.error_type_expr.name == null);
} }
test "parse failable with named `!Foo` (new `-> Tuple(...) !` form)" { test "parse multi-value named failable `-> (A, B, !Foo)`" {
const source = "f :: () -> Tuple(i32, i64) !ParseErr { 0; }"; const source = "f :: () -> (i32, i64, !ParseErr) { 0; }";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator); var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit(); defer arena.deinit();
var parser = Parser.init(arena.allocator(), source); var parser = Parser.init(arena.allocator(), source);
const root = try parser.parse(); const root = try parser.parse();
const rt = root.data.root.decls[0].data.fn_decl.return_type.?; const rt = root.data.root.decls[0].data.fn_decl.return_type.?;
try std.testing.expect(rt.data == .tuple_type_expr); try std.testing.expect(rt.data == .return_type_expr or rt.data == .tuple_type_expr);
const fields = rt.data.tuple_type_expr.field_types; const fields = if (rt.data == .return_type_expr) rt.data.return_type_expr.field_types else rt.data.tuple_type_expr.field_types;
try std.testing.expectEqual(@as(usize, 3), fields.len); try std.testing.expectEqual(@as(usize, 3), fields.len);
try std.testing.expect(fields[2].data == .error_type_expr); try std.testing.expect(fields[2].data == .error_type_expr);
try std.testing.expectEqualStrings("ParseErr", fields[2].data.error_type_expr.name.?); try std.testing.expectEqualStrings("ParseErr", fields[2].data.error_type_expr.name.?);
} }
test "parse legacy bare failable `-> T !` is rejected" {
const source = "f :: () -> i32 ! { 0; }";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
try std.testing.expectError(error.ParseError, parser.parse());
}
test "parse old bare-paren failable `-> (!, i32)` is rejected" { test "parse old bare-paren failable `-> (!, i32)` is rejected" {
const source = "f :: () -> (!, i32) { 0; }"; const source = "f :: () -> (!, i32) { 0; }";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator); var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
@@ -5077,11 +5044,10 @@ test "round-trip print: error-set decl" {
try std.testing.expectEqualStrings(source, aw.writer.toArrayList().items); try std.testing.expectEqualStrings(source, aw.writer.toArrayList().items);
} }
test "print: failable result list with pointer + named error folds to tuple repr" { test "print: failable result list with pointer + named error renders canonically" {
// New `-> T !` form: a single value + named error channel folds to the SAME // A single value + named error channel `(*Handle, !IoErr)` renders back as
// internal `tuple_type_expr` the old `(*Handle, !IoErr)` spelling produced, // the canonical parenthesized result list.
// so printType still renders the canonical tuple representation. const source = "open :: () -> (*Handle, !IoErr) { 0; }";
const source = "open :: () -> *Handle !IoErr { 0; }";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator); var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit(); defer arena.deinit();
var parser = Parser.init(arena.allocator(), source); var parser = Parser.init(arena.allocator(), source);
@@ -5431,7 +5397,7 @@ test "E0.3 or value-terminator: parse(s) or 0" {
test "E0.3 full failable function parses end-to-end (all E0 forms)" { test "E0.3 full failable function parses end-to-end (all E0 forms)" {
const source = const source =
\\parse :: (s: string) -> i32 !ParseErr { \\parse :: (s: string) -> (i32, !ParseErr) {
\\ onfail (e) { cleanup(s); } \\ onfail (e) { cleanup(s); }
\\ v := try inner(s) or 0; \\ v := try inner(s) or 0;
\\ w := other(s) catch (e2) { return 0; }; \\ w := other(s) catch (e2) { return 0; };