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; }
// 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; }

View File

@@ -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 };

View File

@@ -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}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -12,14 +12,14 @@
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.Empty; }
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) -> Tuple(i32, i32) !E {
inc :: (n: i32) -> (i32, i32, !E) {
v, b := try parse(n);
return .(v + 1, b + 1);
}

View File

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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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,7 +23,7 @@ sm_check :: (ok: bool) -> ! {
}
// 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);
y := try sm_parse(b);
return .(x, y);
@@ -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();
}
@@ -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");
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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:

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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

View File

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

View File

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

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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}
}
// 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}

View File

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

View File

@@ -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; }

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 }`),
// keeping the `.x`/`.y` names addressable on both the success value and the
// `or` fallback. Exercises the success path, a `raise` path, and an
@@ -12,7 +12,7 @@
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; }
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
// 0 — the caller's `catch` must NOT fire and the success value must be intact.
// Regression (issue 0190): `lowerValueBody` used to `coerceToType`+`ret` the
@@ -10,16 +10,16 @@
E :: error { Bad }
// Single-value trailing-expression success.
val :: () -> i64 !E { 99 }
val :: () -> (i64, !E) { 99 }
// String trailing-expression success.
sval :: () -> string !E { "hi" }
sval :: () -> (string, !E) { "hi" }
// 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.
fval :: (n: i64) -> i64 !E {
fval :: (n: i64) -> (i64, !E) {
if n < 0 { raise error.Bad; }
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
// 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
@@ -14,10 +14,10 @@ E :: error { Bad }
Point :: struct { x: i64; y: i64; }
// 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.
gfail :: ($T: Type, v: T, bad: bool) -> T !E {
gfail :: ($T: Type, v: T, bad: bool) -> (T, !E) {
if bad { raise error.Bad; }
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
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,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
sm_pair :: (a: i32, b: i32) -> Tuple(i32, i32) ! {
sm_pair :: (a: i32, b: i32) -> (i32, i32, !) {
x := try sm_parse(a);
y := try sm_parse(b);
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
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;
}

View File

@@ -1,7 +1,7 @@
// A multi-return signature may carry an error channel as its LAST slot:
// `-> (i32, bool, !)` returns two values OR fails. This reuses the existing
// 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.
//
// Consume it like any failable: `catch` / `or` / a guarded destructure or