lang: catch/onfail error bindings take parens

try foo() catch (e) { }   // legal
try foo() catch e { }     // parse error with a migration hint

Same capture style as the for-loop. All four catch shapes keep working
with the parenthesized binding — block, bare-expression body, and the
== match sugar — and the no-binding forms are unchanged. onfail follows
the same rule (onfail (e) { }); its expression-cleanup form is
disambiguated by the paren-group-before-brace lookahead, so
onfail (f()); stays an expression cleanup.

AST unchanged; the printer renders the parens; the #run escape help
text updated. Corpus migrated (57 catch + 3 onfail bindings, in-source
parser test strings, specs incl. grammar rules, readme untouched —
no catch examples there).

Regression: examples/1157-diagnostics-catch-binding-needs-parens.sx;
re-captured stderr for 1010/1013/1037/1123 (migrated source echoed in
carets + help text).
This commit is contained in:
agra
2026-06-10 23:05:02 +03:00
parent 12149eb548
commit 83ec2536af
42 changed files with 158 additions and 115 deletions

View File

@@ -24,7 +24,7 @@ clamp :: (x: s64) -> s64 { if x > 10 { return 10; } return x; }
main :: () -> s32 {
print("const_one={}\n", const_one()); // 1
print("raised={}\n", always_raise(5) catch e 0); // 0
print("raised={}\n", always_raise(5) catch (e) 0); // 0
print("clamp_hi={}\n", clamp(42)); // 10
print("clamp_lo={}\n", clamp(7)); // 7

View File

@@ -169,7 +169,7 @@ sm_pair :: (a: s32, b: s32) -> (s32, s32, !) {
// `catch` block that diverges (logs the tag, then returns a fallback)
sm_or_default :: (n: s32) -> s32 {
return sm_parse(n) catch e {
return sm_parse(n) catch (e) {
print(" logged {}\n", e);
return -1;
};

View File

@@ -21,7 +21,7 @@ IoErr :: error { Disk }
main :: () -> s32 {
fail_own := closure(() -> !IoErr { raise error.Disk; });
fail_own() catch e {
fail_own() catch (e) {
if e == error.Disk { print("own=Disk\n"); }
};
d := dep_err();

View File

@@ -20,7 +20,7 @@ must :: (n: s32) -> !E {
// Diverging body — returns from `classify` on error.
classify :: (n: s32) -> s32 {
must(n) catch e {
must(n) catch (e) {
if e == error.Bad { return 1; }
if e == error.Empty { return 2; }
return 9;
@@ -28,9 +28,9 @@ classify :: (n: s32) -> s32 {
return 0; // must(n) succeeded
}
// Match-body form — sugar for `catch e { if e == { case ... } }`.
// Match-body form — sugar for `catch (e) { if e == { case ... } }`.
mclassify :: (n: s32) -> s32 {
must(n) catch e == {
must(n) catch (e) == {
case .Bad: return 11;
case .Empty: return 22;
else: return 99;
@@ -41,7 +41,7 @@ mclassify :: (n: s32) -> s32 {
// Selective handle + re-raise (failable enclosing fn; `raise e` is the
// variable form). Swallows Bad → success; re-raises everything else.
handle_some :: (n: s32) -> !E {
must(n) catch e {
must(n) catch (e) {
if e == error.Bad { return; } // swallow → success
raise e; // re-raise the rest
};
@@ -50,7 +50,7 @@ handle_some :: (n: s32) -> !E {
main :: () -> s32 {
r : s32 = 0;
must(-1) catch e { if e == error.Bad { r = r + 1; } }; // Bad → +1
must(-1) catch (e) { if e == error.Bad { r = r + 1; } }; // Bad → +1
must(5) catch { r = r + 100; }; // success → body skipped
r = r + classify(0); // Empty → 2
r = r + classify(8); // success → 0

View File

@@ -8,6 +8,6 @@
plain :: () -> s32 { return 0; }
main :: () -> s32 {
plain() catch e { return 1; }; // error: operand has type s32 (not failable)
plain() catch (e) { return 1; }; // error: operand has type s32 (not failable)
return 0;
}

View File

@@ -2,7 +2,7 @@
// the consumer side of the error-channel tuple ABI). `try f()` on a
// `-> (T, !E)` callee binds the value slot on success and propagates the error
// on failure (a pure-failable caller returns the tag; a value-carrying caller
// returns `{undef, tag}`). `f() catch e BODY` yields the value slot on success
// returns `{undef, tag}`). `f() catch (e) BODY` yields the value slot on success
// or the handler body's value on failure, merged through a block parameter.
// The producer side is `examples/228-value-failable.sx`.
@@ -31,12 +31,12 @@ relay :: (n: s32) -> !E {
// value-carrying `catch`, bare-expression fallback.
safe :: (n: s32) -> s32 {
return parse(n) catch e 0;
return parse(n) catch (e) 0;
}
// value-carrying `catch`, match-body value.
classify :: (n: s32) -> s32 {
return parse(n) catch e == {
return parse(n) catch (e) == {
case .Bad: 1;
case .Empty: 2;
else: 3

View File

@@ -14,6 +14,6 @@ parse :: (n: s32) -> (s32, !E) {
}
main :: () -> s32 {
x := parse(-1) catch e { print("oops\n") }; // error: body yields no value
x := parse(-1) catch (e) { print("oops\n") }; // error: body yields no value
return x;
}

View File

@@ -2,7 +2,7 @@
// (ERR step E1.7). Unlike `defer` (which runs on every exit), `onfail` fires
// on an error exit — a `raise` or a propagating `try` — and is skipped on
// success. On an error exit `defer` and `onfail` run interleaved in reverse
// declaration order. `onfail e { … }` binds the in-flight error tag.
// declaration order. `onfail (e) { … }` binds the in-flight error tag.
// (Per-attempt-`try` gating and `or`-chain absorption refine this in E2.4b.)
#import "modules/std.sx";
@@ -25,7 +25,7 @@ run :: (n: s32) -> !E {
// `onfail e` binds the tag.
classify :: (n: s32) -> !E {
onfail e { if e == error.Bad { print("cleanup: bad\n"); } }
onfail (e) { if e == error.Bad { print("cleanup: bad\n"); } }
if n < 0 { raise error.Bad; }
return;
}

View File

@@ -26,13 +26,13 @@ inc :: (n: s32) -> (s32, s32, !E) {
// Multi-value `catch`, bare-expression tuple fallback (absorbs the failure).
safe :: (n: s32) -> s32 {
v, b := parse(n) catch e (40, 50);
v, b := parse(n) catch (e) (40, 50);
return v + b;
}
// Multi-value `catch` match-body — per-tag dispatch, each arm a value-tuple.
classify :: (n: s32) -> s32 {
v, b := parse(n) catch e == {
v, b := parse(n) catch (e) == {
case .Bad: (1, 1);
case .Empty: (2, 2);
else: (9, 9);

View File

@@ -18,7 +18,7 @@ f :: () -> !E {
defer { return; } // ERROR: return in defer body
onfail { try g(); } // ERROR: try in onfail body
defer { for 0..1 (i) { break; } } // ERROR: break in defer body (transitive through loop)
onfail e { if e == error.Bad { continue; } } // ERROR: continue in onfail body
onfail (e) { if e == error.Bad { continue; } } // ERROR: continue in onfail body
try g();
return;
}

View File

@@ -20,7 +20,7 @@ main :: () -> s32 {
print("a={} b={}\n", a, b); // a=BadDigit b=Overflow
// A tag bound by `catch` interpolates too (diverging handler).
v := parse(0) catch e {
v := parse(0) catch (e) {
print("parse failed with {}\n", e); // parse failed with Empty
return 0;
};

View File

@@ -28,13 +28,13 @@ main :: () -> s32 {
// cleared when the handler completes (a non-diverging exit), not on entry.
// So inside the handler the frames are still visible (here: the `raise` in
// `fail` + the `try fail` propagation in `propagate` = 2 frames)...
propagate(-1) catch e {
propagate(-1) catch (e) {
print("in catch: len={}\n", sx_trace_len()); // 2 (handler sees the chain)
};
print("after catch: len={}\n", sx_trace_len()); // 0 (absorbed at handler exit)
// A success leaves the buffer empty (nothing pushed).
propagate(1) catch e { };
propagate(1) catch (e) { };
print("after success: len={}\n", sx_trace_len()); // 0
return 0;
}

View File

@@ -27,7 +27,7 @@ mid :: (n: s32) -> !E {
}
main :: () -> s32 {
mid(-1) catch e {
mid(-1) catch (e) {
print("[stdout] caught {}\n", e); // tag name via the always-linked table
trace.print_current(); // [stderr] the 2-frame trace
};

View File

@@ -30,8 +30,8 @@ main :: () -> (s32, !E) {
r = r + (try fa(0) or try fa(7)); // a fails → b succeeds → 7
r = r + (try fa(0) or try fa(0) or try fa(3)); // first two fail → third → +3 = 10
r = r + (fa(0) or fa(0) or 96); // bare chain + value terminator → +96 = 106
r = r + ((try fa(0) or try fa(0)) catch e 5); // both fail → catch handler → +5 = 111
r = r + ((try fa(0) or try fa(9)) catch e 0); // second succeeds → catch skipped → +9 = 120
r = r + ((try fa(0) or try fa(0)) catch (e) 5); // both fail → catch handler → +5 = 111
r = r + ((try fa(0) or try fa(9)) catch (e) 0); // second succeeds → catch skipped → +9 = 120
try fv(0) or try fv(1); // void chain: first fails → second succeeds

View File

@@ -20,7 +20,7 @@ mid :: () -> !TErr {
}
probe :: () {
mid() catch e {
mid() catch (e) {
print("comptime caught {}\n", e);
trace.print_current();
};

View File

@@ -31,7 +31,7 @@ sm_pair :: (a: s32, b: s32) -> (s32, s32, !) {
// catch with a diverging block body
sm_or_default :: (n: s32) -> s32 {
return sm_parse(n) catch e {
return sm_parse(n) catch (e) {
print(" logged {}\n", e);
return -1;
};
@@ -61,7 +61,7 @@ sm_run :: (cb: Closure(s32) -> (s32, !SmokeErr), n: s32) -> (s32, !SmokeErr) {
// bare fn-type param: a NON-failable closure literal widens into the failable
// slot (the ∅-widening adapter wraps `{value, 0}`)
sm_widen :: (cb: (s32) -> (s32, !SmokeErr), n: s32) -> s32 {
return cb(n) catch e -1;
return cb(n) catch (e) -1;
}
// generic ($T) value-carrying failable composition, monomorphized per call
@@ -81,11 +81,11 @@ main :: () {
if err2 == error.BadDigit { print("got: {}\n", err2); }
// catch — bare-expr body
ce := sm_parse(0) catch e 100;
ce := sm_parse(0) catch (e) 100;
print("catch-expr: {}\n", ce);
// catch — match-body per-tag dispatch
cm := sm_parse(200) catch e == {
cm := sm_parse(200) catch (e) == {
case .Overflow: 1;
case .Empty: 2;
else: 3;
@@ -105,9 +105,9 @@ main :: () {
if !gerr { print("or-chain: {}\n", g); }
// multi-value failable consumed by catch (tuple body)
p, q := sm_pair(0, 3) catch e (0, 0);
p, q := sm_pair(0, 3) catch (e) (0, 0);
print("pair-catch: {} {}\n", p, q);
p2, q2 := sm_pair(4, 5) catch e (0, 0);
p2, q2 := sm_pair(4, 5) catch (e) (0, 0);
print("pair-ok: {} {}\n", p2, q2);
// pure failable: absorb with no-binding catch
@@ -120,15 +120,15 @@ main :: () {
iv, ierr := sm_acquire(false);
// composition: inline failable closure literal through a Closure(...) param
cl := sm_run(closure((x: s32) -> (s32, !SmokeErr) { if x < 0 { raise error.BadDigit; } return x * 2; }), 6) catch e -1;
cl := sm_run(closure((x: s32) -> (s32, !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: s32) -> (s32, !SmokeErr) { raise error.Empty; }), 1) catch e -9); // -9
print("closure-run-err: {}\n", sm_run(closure((x: s32) -> (s32, !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: s32) -> s32 => x + 1), 9)); // 10
// generic failable composition (monomorphized at s32)
print("wrap: {}\n", sm_wrap(s32, closure(() -> (s32, !SmokeErr) { return 42; })) catch e 0); // 42
print("wrap: {}\n", sm_wrap(s32, closure(() -> (s32, !SmokeErr) { return 42; })) catch (e) 0); // 42
print("errors ok\n");
}

View File

@@ -20,10 +20,10 @@ guard :: (ok: bool) -> !E {
}
ok_v :: #run parse(5); // success → 10 (value, error stripped)
caught :: #run parse(-1) catch e 99; // Bad → 99
caught :: #run parse(-1) catch (e) 99; // Bad → 99
ored :: #run parse(0) or 55; // Empty → 55
#run guard(false) catch e { }; // onfail fires during the comptime unwind
#run guard(false) catch (e) { }; // onfail fires during the comptime unwind
main :: () -> s32 {
print("ok={} caught={} ored={}\n", ok_v, caught, ored);

View File

@@ -10,13 +10,13 @@
E :: error { Neg }
runwith :: (cb: Closure(s64) -> (s64, !E), n: s64) -> s64 { return cb(n) catch e -1; }
runwith :: (cb: Closure(s64) -> (s64, !E), n: s64) -> s64 { return cb(n) catch (e) -1; }
main :: () -> s32 {
// block-body and arrow-body failable closures, called directly
m := closure((x: s64) -> (s64, !E) { if x < 0 { raise error.Neg; } return x * 2; });
n := closure((x: s64) -> (s64, !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
print("param ok={} err={}\n", runwith(m, 5), runwith(m, -1)); // 10 -1

View File

@@ -12,7 +12,7 @@
E :: error { Neg }
bare :: (cb: (s64) -> (s64, !E), n: s64) -> s64 { return cb(n) catch e -1; }
bare :: (cb: (s64) -> (s64, !E), n: s64) -> s64 { return cb(n) catch (e) -1; }
chain :: (cb: Closure(s64) -> (s64, !E), n: s64) -> (s64, !E) { return try cb(n); }
dbl :: (x: s64) -> (s64, !E) { if x < 0 { raise error.Neg; } return x * 2; }
@@ -25,8 +25,8 @@ main :: () -> s32 {
// Closure(...) param, try-propagated, then caught at the call site
print("chain ok={} err={}\n",
chain(closure((x: s64) -> (s64, !E) => x + 6), 4) catch e 0, // 10
chain(closure((x: s64) -> (s64, !E) { raise error.Neg; }), 1) catch e 0); // 0
chain(closure((x: s64) -> (s64, !E) => x + 6), 4) catch (e) 0, // 10
chain(closure((x: s64) -> (s64, !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: s64) -> s64 => x + 1), 9)); // 10

View File

@@ -24,13 +24,13 @@ main :: () -> s32 {
handlers.append(closure((x: s32) -> (s32, !) { if x == 0 { raise error.Other; } return x + 100; }));
// success paths
print("ok0={}\n", dispatch(handlers.items[0], 5) catch e 0); // 10
print("ok1={}\n", dispatch(handlers.items[1], 7) catch e 0); // 107
print("ok0={}\n", dispatch(handlers.items[0], 5) catch (e) 0); // 10
print("ok1={}\n", dispatch(handlers.items[1], 7) catch (e) 0); // 107
// failure paths: each closure raises its own tag, which propagates
// through `try` and is absorbed by the call-site `catch` fallback
print("err0={}\n", dispatch(handlers.items[0], -1) catch e -1); // raised Negative → -1
print("err1={}\n", dispatch(handlers.items[1], 0) catch e -2); // raised Other → -2
print("err0={}\n", dispatch(handlers.items[0], -1) catch (e) -1); // raised Negative → -1
print("err1={}\n", dispatch(handlers.items[1], 0) catch (e) -2); // raised Other → -2
}
return 0;
}

View File

@@ -19,7 +19,7 @@ main :: () -> s32 {
handlers : List(Closure(s32) -> (s32, !)) = .{};
handlers.append(closure((x: s32) -> (s32, !) { if x < 0 { raise error.Negative; } return x; }));
handlers.append(closure((x: s32) -> (s32, !) { 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;
}

View File

@@ -9,7 +9,7 @@
E :: error { Neg }
take :: (cb: Closure(s32) -> (s32, !E), x: s32) -> s32 { return cb(x) catch e -1; }
take :: (cb: Closure(s32) -> (s32, !E), x: s32) -> s32 { return cb(x) catch (e) -1; }
main :: () -> s32 {
// `-> s32` (non-failable) but the body raises → lambda-specific hint:

View File

@@ -13,7 +13,7 @@ wrap :: ($T: Type, f: Closure() -> (T, !E)) -> (T, !E) { return try f(); }
main :: () -> s32 {
// success, consumed by catch
print("catch={}\n", wrap(s32, closure(() -> (s32, !E) { return 7; })) catch e -1); // 7
print("catch={}\n", wrap(s32, closure(() -> (s32, !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)
@@ -21,9 +21,9 @@ main :: () -> s32 {
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(s32, closure(() -> (s32, !E) { raise error.Bad; }) ) catch e -1); // -1
print("fail={}\n", wrap(s32, closure(() -> (s32, !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: (s64) -> s64, n: s64) -> s64 { return cb(n); }
baref :: (cb: (s64) -> (s64, !E), n: s64) -> s64 { return cb(n) catch e -1; }
baref :: (cb: (s64) -> (s64, !E), n: s64) -> s64 { return cb(n) catch (e) -1; }
main :: () -> s32 {
inc := closure((x: s64) -> s64 => x + 1); // capture-free closure var

View File

@@ -53,7 +53,7 @@ main :: () -> s32 {
// (4) early-return / raise helpers
total = total + guarded(4); // +40
total = total + guarded(-1); // -1
total = total + (relay(2) catch e 0); // parse(2)=20 → +1 = 21
total = total + (relay(2) catch (e) 0); // parse(2)=20 → +1 = 21
print("liveness total: {}\n", total); // 50+70+30+40-1+21 = 210
return total;

View File

@@ -13,7 +13,7 @@ recover :: () -> (s32, !E) { raise error.Bad; }
work :: (n: s32) -> !E {
defer print("defer: always\n"); // plain cleanup
onfail { failing() catch e print("onfail: caught (catch)\n"); } // catch absorbs
onfail { failing() catch (e) print("onfail: caught (catch)\n"); } // catch absorbs
onfail { x := recover() or 7; print("onfail: x={} (or)\n", x); } // or-value absorbs
if n < 0 { raise error.Bad; }
return;

View File

@@ -16,7 +16,7 @@ run :: () {
defer {
v, e := probe(); // destructure decl
if !e { print("defer: v={}\n", v); } // value live under the guard
failing() catch x print("defer: caught\n"); // catch-statement absorbs
failing() catch (x) print("defer: caught\n"); // catch-statement absorbs
}
print("body\n");
}

View File

@@ -37,7 +37,7 @@ work :: () {
if !err { print("defer closure: v={}\n", v); } // E1.8: live under guard
try failing();
};
emit() catch e print("defer closure: raised\n");
emit() catch (e) print("defer closure: raised\n");
}
print("body\n");
}

View File

@@ -16,7 +16,7 @@ parse :: (n: s32) -> (s32, !E) {
// `catch` tag binding spelled `s2`, referenced in the match body.
classify :: (n: s32) -> s32 {
return parse(n) catch `s2 == {
return parse(n) catch (`s2) == {
case .Bad: 1;
case .Empty: 2;
else: 3
@@ -25,7 +25,7 @@ classify :: (n: s32) -> s32 {
// `onfail` tag binding spelled `u8`, referenced in the cleanup body.
cleanup :: (n: s32) -> !E {
onfail `u8 { if `u8 == error.Bad { print("cleanup: bad\n"); } }
onfail (`u8) { if `u8 == error.Bad { print("cleanup: bad\n"); } }
if n < 0 { raise error.Bad; }
return;
}

View File

@@ -17,8 +17,8 @@ must :: (n: s32) -> !E {
}
classify :: (n: s32) -> !E {
onfail s64 { } // onfail tag binding
must(n) catch u8 { return; }; // catch tag binding
onfail (s64) { } // onfail tag binding
must(n) catch (u8) { return; }; // catch tag binding
return;
}

View File

@@ -0,0 +1,14 @@
// The catch error binding is parenthesized — `catch (e) { }`; a bare
// binding is rejected with a migration hint. (Same rule as the for-loop
// capture; `onfail (e) { }` follows it too.)
#import "modules/std.sx";
E :: error { Bad };
f :: () -> (s64, !E) { raise error.Bad; }
main :: () {
v := f() catch e { 0 };
print("{}\n", v);
}

View File

@@ -1,5 +1,5 @@
error: `catch` requires a failable expression; operand has type 's32'
--> /Users/agra/projects/sx/examples/1010-errors-catch-rejections.sx:11:5
--> examples/1010-errors-catch-rejections.sx:11:5
|
11 | plain() catch e { return 1; }; // error: operand has type s32 (not failable)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
11 | plain() catch (e) { return 1; }; // error: operand has type s32 (not failable)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -1,5 +1,5 @@
error: `catch` body must produce a value of type 's32' (or diverge with `return` / `raise`)
--> /Users/agra/projects/sx/examples/1013-errors-value-failable-reject.sx:17:10
--> examples/1013-errors-value-failable-reject.sx:17:10
|
17 | x := parse(-1) catch e { print("oops\n") }; // error: body yields no value
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
17 | x := parse(-1) catch (e) { print("oops\n") }; // error: body yields no value
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -1,4 +1,4 @@
error: comptime `#run` (x) raised an unhandled error: error.Bad
error return trace (most recent call last):
parse at 1037-errors-comptime-run-escape.sx:11:17
help: handle it at the `#run` site — `#run <expr> catch e { ... }` or `#run <expr> or <default>`
help: handle it at the `#run` site — `#run <expr> catch (e) { ... }` or `#run <expr> or <default>`

View File

@@ -1,11 +1,11 @@
error: 's64' is a reserved type name and cannot be used as an identifier
--> examples/1123-diagnostics-reserved-name-catch-onfail.sx:20:12
--> examples/1123-diagnostics-reserved-name-catch-onfail.sx:20:13
|
20 | onfail s64 { } // onfail tag binding
| ^^^
20 | onfail (s64) { } // onfail tag binding
| ^^^
error: 'u8' is a reserved type name and cannot be used as an identifier
--> examples/1123-diagnostics-reserved-name-catch-onfail.sx:21:19
--> examples/1123-diagnostics-reserved-name-catch-onfail.sx:21:20
|
21 | must(n) catch u8 { return; }; // catch tag binding
| ^^
21 | must(n) catch (u8) { return; }; // catch tag binding
| ^^

View File

@@ -0,0 +1,5 @@
error: the catch error binding needs parens: `catch (e) { ... }`
--> examples/1157-diagnostics-catch-binding-needs-parens.sx:12:20
|
12 | v := f() catch e { 0 };
| ^