From 83ec2536afa278d9daf29f5645c76c3a0df3513c Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 10 Jun 2026 23:05:02 +0300 Subject: [PATCH] lang: catch/onfail error bindings take parens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../0038-basic-dead-code-after-terminator.sx | 2 +- examples/0415-protocols-protocols.sx | 2 +- ...les-same-name-error-set-lambda-own-wins.sx | 2 +- examples/1009-errors-catch.sx | 10 +-- examples/1010-errors-catch-rejections.sx | 2 +- .../1012-errors-value-failable-consume.sx | 6 +- examples/1013-errors-value-failable-reject.sx | 2 +- examples/1016-errors-onfail.sx | 4 +- examples/1018-errors-multi-value-failable.sx | 4 +- .../1020-errors-cleanup-body-restrictions.sx | 2 +- examples/1023-errors-tag-interpolation.sx | 2 +- examples/1024-errors-trace-buffer.sx | 4 +- examples/1025-errors-trace-format.sx | 2 +- examples/1028-errors-failable-or-chain.sx | 4 +- examples/1035-errors-comptime-trace.sx | 2 +- examples/1036-errors-failable-smoke.sx | 18 +++--- examples/1038-errors-comptime-run-handled.sx | 4 +- .../1039-errors-failable-closure-literal.sx | 4 +- ...040-errors-failable-closure-composition.sx | 6 +- ...041-errors-failable-closure-shape-union.sx | 8 +-- ...ors-failable-closure-shape-union-reject.sx | 2 +- ...043-errors-lambda-raise-annotation-hint.sx | 2 +- ...044-errors-generic-failable-composition.sx | 6 +- ...045-errors-closure-var-bare-slot-reject.sx | 2 +- examples/1046-errors-value-slot-liveness.sx | 2 +- examples/1048-errors-cleanup-absorption.sx | 2 +- examples/1050-errors-defer-block-body.sx | 2 +- .../1051-errors-cleanup-closure-boundary.sx | 2 +- .../1054-errors-backtick-reserved-binding.sx | 4 +- ...-diagnostics-reserved-name-catch-onfail.sx | 4 +- ...-diagnostics-catch-binding-needs-parens.sx | 14 +++++ .../1010-errors-catch-rejections.stderr | 6 +- .../1013-errors-value-failable-reject.stderr | 6 +- .../1037-errors-comptime-run-escape.stderr | 2 +- ...gnostics-reserved-name-catch-onfail.stderr | 12 ++-- ...iagnostics-catch-binding-needs-parens.exit | 1 + ...gnostics-catch-binding-needs-parens.stderr | 5 ++ ...gnostics-catch-binding-needs-parens.stdout | 1 + specs.md | 38 ++++++------ src/ir/emit_llvm.zig | 2 +- src/parser.zig | 62 ++++++++++++------- src/print.zig | 6 +- 42 files changed, 158 insertions(+), 115 deletions(-) create mode 100644 examples/1157-diagnostics-catch-binding-needs-parens.sx create mode 100644 examples/expected/1157-diagnostics-catch-binding-needs-parens.exit create mode 100644 examples/expected/1157-diagnostics-catch-binding-needs-parens.stderr create mode 100644 examples/expected/1157-diagnostics-catch-binding-needs-parens.stdout diff --git a/examples/0038-basic-dead-code-after-terminator.sx b/examples/0038-basic-dead-code-after-terminator.sx index b5adc9a..027aa59 100644 --- a/examples/0038-basic-dead-code-after-terminator.sx +++ b/examples/0038-basic-dead-code-after-terminator.sx @@ -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 diff --git a/examples/0415-protocols-protocols.sx b/examples/0415-protocols-protocols.sx index 4ab8f53..13ad6d1 100644 --- a/examples/0415-protocols-protocols.sx +++ b/examples/0415-protocols-protocols.sx @@ -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; }; diff --git a/examples/0813-modules-same-name-error-set-lambda-own-wins.sx b/examples/0813-modules-same-name-error-set-lambda-own-wins.sx index 088ed55..4b7e177 100644 --- a/examples/0813-modules-same-name-error-set-lambda-own-wins.sx +++ b/examples/0813-modules-same-name-error-set-lambda-own-wins.sx @@ -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(); diff --git a/examples/1009-errors-catch.sx b/examples/1009-errors-catch.sx index a97e26a..5792877 100644 --- a/examples/1009-errors-catch.sx +++ b/examples/1009-errors-catch.sx @@ -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 diff --git a/examples/1010-errors-catch-rejections.sx b/examples/1010-errors-catch-rejections.sx index 7e91a1c..76de09b 100644 --- a/examples/1010-errors-catch-rejections.sx +++ b/examples/1010-errors-catch-rejections.sx @@ -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; } diff --git a/examples/1012-errors-value-failable-consume.sx b/examples/1012-errors-value-failable-consume.sx index 3b9de31..776457f 100644 --- a/examples/1012-errors-value-failable-consume.sx +++ b/examples/1012-errors-value-failable-consume.sx @@ -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 diff --git a/examples/1013-errors-value-failable-reject.sx b/examples/1013-errors-value-failable-reject.sx index a8be283..297d824 100644 --- a/examples/1013-errors-value-failable-reject.sx +++ b/examples/1013-errors-value-failable-reject.sx @@ -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; } diff --git a/examples/1016-errors-onfail.sx b/examples/1016-errors-onfail.sx index 905d985..1ba1d97 100644 --- a/examples/1016-errors-onfail.sx +++ b/examples/1016-errors-onfail.sx @@ -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; } diff --git a/examples/1018-errors-multi-value-failable.sx b/examples/1018-errors-multi-value-failable.sx index 94adbc1..88f5b0a 100644 --- a/examples/1018-errors-multi-value-failable.sx +++ b/examples/1018-errors-multi-value-failable.sx @@ -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); diff --git a/examples/1020-errors-cleanup-body-restrictions.sx b/examples/1020-errors-cleanup-body-restrictions.sx index f2c82a3..fb267c6 100644 --- a/examples/1020-errors-cleanup-body-restrictions.sx +++ b/examples/1020-errors-cleanup-body-restrictions.sx @@ -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; } diff --git a/examples/1023-errors-tag-interpolation.sx b/examples/1023-errors-tag-interpolation.sx index b16adb6..426a529 100644 --- a/examples/1023-errors-tag-interpolation.sx +++ b/examples/1023-errors-tag-interpolation.sx @@ -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; }; diff --git a/examples/1024-errors-trace-buffer.sx b/examples/1024-errors-trace-buffer.sx index db89a54..2fcef58 100644 --- a/examples/1024-errors-trace-buffer.sx +++ b/examples/1024-errors-trace-buffer.sx @@ -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; } diff --git a/examples/1025-errors-trace-format.sx b/examples/1025-errors-trace-format.sx index cb95da8..be40c5c 100644 --- a/examples/1025-errors-trace-format.sx +++ b/examples/1025-errors-trace-format.sx @@ -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 }; diff --git a/examples/1028-errors-failable-or-chain.sx b/examples/1028-errors-failable-or-chain.sx index 5ad0faa..188ade8 100644 --- a/examples/1028-errors-failable-or-chain.sx +++ b/examples/1028-errors-failable-or-chain.sx @@ -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 diff --git a/examples/1035-errors-comptime-trace.sx b/examples/1035-errors-comptime-trace.sx index 16ae40c..e8892fb 100644 --- a/examples/1035-errors-comptime-trace.sx +++ b/examples/1035-errors-comptime-trace.sx @@ -20,7 +20,7 @@ mid :: () -> !TErr { } probe :: () { - mid() catch e { + mid() catch (e) { print("comptime caught {}\n", e); trace.print_current(); }; diff --git a/examples/1036-errors-failable-smoke.sx b/examples/1036-errors-failable-smoke.sx index 76c354e..d274bf7 100644 --- a/examples/1036-errors-failable-smoke.sx +++ b/examples/1036-errors-failable-smoke.sx @@ -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"); } diff --git a/examples/1038-errors-comptime-run-handled.sx b/examples/1038-errors-comptime-run-handled.sx index 73a4128..38cd763 100644 --- a/examples/1038-errors-comptime-run-handled.sx +++ b/examples/1038-errors-comptime-run-handled.sx @@ -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); diff --git a/examples/1039-errors-failable-closure-literal.sx b/examples/1039-errors-failable-closure-literal.sx index 5a16233..81a1f80 100644 --- a/examples/1039-errors-failable-closure-literal.sx +++ b/examples/1039-errors-failable-closure-literal.sx @@ -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 diff --git a/examples/1040-errors-failable-closure-composition.sx b/examples/1040-errors-failable-closure-composition.sx index b6ef790..5405639 100644 --- a/examples/1040-errors-failable-closure-composition.sx +++ b/examples/1040-errors-failable-closure-composition.sx @@ -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 diff --git a/examples/1041-errors-failable-closure-shape-union.sx b/examples/1041-errors-failable-closure-shape-union.sx index 9de5ec2..20d37e4 100644 --- a/examples/1041-errors-failable-closure-shape-union.sx +++ b/examples/1041-errors-failable-closure-shape-union.sx @@ -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; } diff --git a/examples/1042-errors-failable-closure-shape-union-reject.sx b/examples/1042-errors-failable-closure-shape-union-reject.sx index 8b07637..91ca31a 100644 --- a/examples/1042-errors-failable-closure-shape-union-reject.sx +++ b/examples/1042-errors-failable-closure-shape-union-reject.sx @@ -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; } diff --git a/examples/1043-errors-lambda-raise-annotation-hint.sx b/examples/1043-errors-lambda-raise-annotation-hint.sx index 0ab482b..00c66e6 100644 --- a/examples/1043-errors-lambda-raise-annotation-hint.sx +++ b/examples/1043-errors-lambda-raise-annotation-hint.sx @@ -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: diff --git a/examples/1044-errors-generic-failable-composition.sx b/examples/1044-errors-generic-failable-composition.sx index 59e5b24..a0445ff 100644 --- a/examples/1044-errors-generic-failable-composition.sx +++ b/examples/1044-errors-generic-failable-composition.sx @@ -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; } diff --git a/examples/1045-errors-closure-var-bare-slot-reject.sx b/examples/1045-errors-closure-var-bare-slot-reject.sx index a200580..29b017c 100644 --- a/examples/1045-errors-closure-var-bare-slot-reject.sx +++ b/examples/1045-errors-closure-var-bare-slot-reject.sx @@ -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 diff --git a/examples/1046-errors-value-slot-liveness.sx b/examples/1046-errors-value-slot-liveness.sx index 4aa1a35..995920c 100644 --- a/examples/1046-errors-value-slot-liveness.sx +++ b/examples/1046-errors-value-slot-liveness.sx @@ -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; diff --git a/examples/1048-errors-cleanup-absorption.sx b/examples/1048-errors-cleanup-absorption.sx index 6741792..f1b16b6 100644 --- a/examples/1048-errors-cleanup-absorption.sx +++ b/examples/1048-errors-cleanup-absorption.sx @@ -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; diff --git a/examples/1050-errors-defer-block-body.sx b/examples/1050-errors-defer-block-body.sx index f0c5716..17cd104 100644 --- a/examples/1050-errors-defer-block-body.sx +++ b/examples/1050-errors-defer-block-body.sx @@ -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"); } diff --git a/examples/1051-errors-cleanup-closure-boundary.sx b/examples/1051-errors-cleanup-closure-boundary.sx index 51527ae..aab9bfa 100644 --- a/examples/1051-errors-cleanup-closure-boundary.sx +++ b/examples/1051-errors-cleanup-closure-boundary.sx @@ -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"); } diff --git a/examples/1054-errors-backtick-reserved-binding.sx b/examples/1054-errors-backtick-reserved-binding.sx index d323ada..a5cff1d 100644 --- a/examples/1054-errors-backtick-reserved-binding.sx +++ b/examples/1054-errors-backtick-reserved-binding.sx @@ -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; } diff --git a/examples/1123-diagnostics-reserved-name-catch-onfail.sx b/examples/1123-diagnostics-reserved-name-catch-onfail.sx index 83a84e2..53e18f3 100644 --- a/examples/1123-diagnostics-reserved-name-catch-onfail.sx +++ b/examples/1123-diagnostics-reserved-name-catch-onfail.sx @@ -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; } diff --git a/examples/1157-diagnostics-catch-binding-needs-parens.sx b/examples/1157-diagnostics-catch-binding-needs-parens.sx new file mode 100644 index 0000000..21c52f1 --- /dev/null +++ b/examples/1157-diagnostics-catch-binding-needs-parens.sx @@ -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); +} diff --git a/examples/expected/1010-errors-catch-rejections.stderr b/examples/expected/1010-errors-catch-rejections.stderr index 952eff1..f78a52f 100644 --- a/examples/expected/1010-errors-catch-rejections.stderr +++ b/examples/expected/1010-errors-catch-rejections.stderr @@ -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) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/examples/expected/1013-errors-value-failable-reject.stderr b/examples/expected/1013-errors-value-failable-reject.stderr index 08b45fc..22c9581 100644 --- a/examples/expected/1013-errors-value-failable-reject.stderr +++ b/examples/expected/1013-errors-value-failable-reject.stderr @@ -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 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/examples/expected/1037-errors-comptime-run-escape.stderr b/examples/expected/1037-errors-comptime-run-escape.stderr index 896c4f5..a00e143 100644 --- a/examples/expected/1037-errors-comptime-run-escape.stderr +++ b/examples/expected/1037-errors-comptime-run-escape.stderr @@ -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 catch e { ... }` or `#run or ` +help: handle it at the `#run` site — `#run catch (e) { ... }` or `#run or ` diff --git a/examples/expected/1123-diagnostics-reserved-name-catch-onfail.stderr b/examples/expected/1123-diagnostics-reserved-name-catch-onfail.stderr index f63404e..e277613 100644 --- a/examples/expected/1123-diagnostics-reserved-name-catch-onfail.stderr +++ b/examples/expected/1123-diagnostics-reserved-name-catch-onfail.stderr @@ -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 + | ^^ diff --git a/examples/expected/1157-diagnostics-catch-binding-needs-parens.exit b/examples/expected/1157-diagnostics-catch-binding-needs-parens.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/examples/expected/1157-diagnostics-catch-binding-needs-parens.exit @@ -0,0 +1 @@ +1 diff --git a/examples/expected/1157-diagnostics-catch-binding-needs-parens.stderr b/examples/expected/1157-diagnostics-catch-binding-needs-parens.stderr new file mode 100644 index 0000000..41ddda7 --- /dev/null +++ b/examples/expected/1157-diagnostics-catch-binding-needs-parens.stderr @@ -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 }; + | ^ diff --git a/examples/expected/1157-diagnostics-catch-binding-needs-parens.stdout b/examples/expected/1157-diagnostics-catch-binding-needs-parens.stdout new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/1157-diagnostics-catch-binding-needs-parens.stdout @@ -0,0 +1 @@ + diff --git a/specs.md b/specs.md index 42def10..5eeda29 100644 --- a/specs.md +++ b/specs.md @@ -109,7 +109,7 @@ M :: union { `s1: s32; } // union tag `u8, rest := pair(); // destructure name if `s16 := maybe() { } // optional binding for xs, 0.. (`bool, `u16) { } // for captures -x catch `s2 { } // catch tag binding +x catch (`s2) { } // catch tag binding ``` In the **member-name positions** among these — struct field, union tag, and @@ -2819,7 +2819,7 @@ Statement form. Terminates the immediately enclosing failable function (like ```sx if bad raise error.BadDigit; // literal tag -v := foo() catch e { +v := foo() catch (e) { if e == error.Specific return default; raise e; // variable tag — re-raise }; @@ -2854,37 +2854,39 @@ tag — use `catch` for that. ### `catch` -Expression form. Handles the error inline. The binding is a **bare name, no -parens** (`catch e`), and is **optional**. Four shapes, disambiguated by the -token after `catch`: +Expression form. Handles the error inline. The binding is **parenthesized** +(`catch (e)`) — like a for-loop capture — and is **optional**. Four shapes, +disambiguated by the token after `catch`: | Form | Binding | Body | |---|---|---| | `catch { ... }` | none (tag ignored) | block — braces required | -| `catch e { ... }` | `e` | block | -| `catch e EXPR` | `e` | bare expression (no braces) | -| `catch e == { case ... }` | `e` | match over `e` (sugar for `{ if e == { ... } }`) | +| `catch (e) { ... }` | `e` | block | +| `catch (e) EXPR` | `e` | bare expression (no braces) | +| `catch (e) == { case ... }` | `e` | match over `e` (sugar for `{ if e == { ... } }`) | + +A bare binding (`catch (e) { }`) is a parse error with a migration hint. ```sx -v := parse_digit(s) catch e { +v := parse_digit(s) catch (e) { log.warn("bad input: {}", e); return default; // noreturn body }; -v := parse_digit(s) catch e compute_fallback(e); // value-producing body +v := parse_digit(s) catch (e) compute_fallback(e); // value-producing body -v, n := parse(s) catch e { +v, n := parse(s) catch (e) { log.warn("parse failed: {}", e); (0, 0) // tuple body for a multi-value failable }; -v := parse(s) catch e == { // match-body form +v := parse(s) catch (e) == { // match-body form case .Empty: 0; case .BadDigit: -1; else: raise e; }; -v := (try foo() or try boo()) catch e { return 0; }; // catch over an `or` chain +v := (try foo() or try boo()) catch (e) { return 0; }; // catch over an `or` chain ``` **Body type rule.** The body (block-as-expression) must produce the failable's @@ -2928,7 +2930,7 @@ of the other markers directly on `X`) is required. ```sx a := parse(s) or 0; // OK — terminator on the path -a := parse(s) catch e {...}; // OK — catch marks +a := parse(s) catch (e) {...}; // OK — catch marks v, err := failable(); // OK — destructure marks a := try foo() or try boo(); // OK — each try marks its own exit @@ -2972,7 +2974,7 @@ Dropping the error slot is a compile error: v, _ := failable(); // ERROR: the error slot cannot be dropped — handle it ``` -Value slots may be discarded (`_, n := parse(s) catch e { return; }`). The +Value slots may be discarded (`_, n := parse(s) catch (e) { return; }`). The statement form `try foo();` is the explicit "propagate, use no value." On a value-carrying failable, the value slot is live only where the compiler can prove the error slot is null (path-sensitive flow-check). @@ -2995,7 +2997,7 @@ make_handle :: () -> (Handle, !) { open :: (path: string) -> (Handle, !) { 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); } ... } ``` @@ -3089,7 +3091,7 @@ return_stmt = 'return' expr? ';' break_stmt = 'break' ';' continue_stmt = 'continue' ';' raise_stmt = 'raise' expr ';' -onfail_stmt = 'onfail' IDENT? block +onfail_stmt = 'onfail' ('(' IDENT ')')? (block | expr ';') defer_stmt = 'defer' expr ';' insert_stmt = '#insert' expr ';' push_stmt = 'push' expr block @@ -3103,7 +3105,7 @@ for_iter = expr [range_op [expr]] range_op = '..' | '..=' | '..<' | '<..' | '<..=' | '<..<' | '=..' | '=..=' | '=..<' for_capture = '(' ['*'] IDENT (',' ['*'] IDENT)* ')' binary = catch_expr (binop catch_expr)* // binop includes `or` (fallback / chain) -catch_expr = unary ('catch' IDENT? (block | '==' '{' case_arm* else_arm? '}' | unary))? +catch_expr = unary ('catch' ('(' IDENT ')')? (block | '==' '{' case_arm* else_arm? '}' | unary))? unary = ('-' | '!' | 'xx' | 'try' | 'cast' '(' type ')') postfix | postfix postfix = primary ('(' args? ')' | '.' IDENT | '.{' field_init_list '}')* diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 966c34d..ecfcb8b 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -807,7 +807,7 @@ pub const LLVMEmitter = struct { std.debug.print(" {s} at {s}:{d}:{d}\n", .{ fname, file, line, col }); } } - std.debug.print("help: handle it at the `#run` site — `#run catch e {{ ... }}` or `#run or `\n", .{}); + std.debug.print("help: handle it at the `#run` site — `#run catch (e) {{ ... }}` or `#run or `\n", .{}); } /// Run comptime side-effect functions (e.g., `#run main();` at top level). diff --git a/src/parser.zig b/src/parser.zig index 89b1bdd..58c1db5 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -2158,7 +2158,7 @@ pub const Parser = struct { return try self.createNode(start, .{ .raise_stmt = .{ .tag = tag_expr } }); } - // Onfail statement: onfail { body } | onfail e { body } | onfail ; + // Onfail statement: onfail { body } | onfail (e) { body } | onfail ; // A binding is present only when an identifier is immediately followed // by `{`; otherwise the text after `onfail` is the (no-binding) body. if (self.current.tag == .kw_onfail) { @@ -2168,10 +2168,20 @@ pub const Parser = struct { var binding_span: ?ast.Span = null; var binding_is_raw = false; if (self.current.tag == .identifier and self.peekNext() == .l_brace) { + return self.fail("the onfail error binding needs parens: `onfail (e) { ... }`"); + } + // `(e)` followed by `{` is the binding form; any other paren + // group is an ordinary expression cleanup (`onfail (f());`). + if (self.current.tag == .l_paren and self.tagAfterParenGroup() == .l_brace) { + self.advance(); + if (self.current.tag != .identifier) { + return self.fail("expected an error binding name in `onfail (e)`"); + } binding = self.tokenSlice(self.current); binding_span = .{ .start = self.current.loc.start, .end = self.current.loc.end }; binding_is_raw = self.current.is_raw; self.advance(); + try self.expect(.r_paren); } const saved_onfail = self.in_onfail_body; self.in_onfail_body = true; @@ -2328,7 +2338,7 @@ pub const Parser = struct { // looking THROUGH a `try` prefix / `catch` postfix / `or` fallback // and leaving the wrapper intact: // a |> try f(x) → try f(a, x) - // a |> f(x) catch e {...} → f(a, x) catch e {...} + // a |> f(x) catch (e) {...} → f(a, x) catch (e) {...} // a |> f(x) or default → f(a, x) or default (only f gets a) if (self.current.tag == .pipe_arrow and Prec.pipe >= min_prec) { self.advance(); @@ -2629,21 +2639,29 @@ pub const Parser = struct { self.advance(); expr = try self.createNode(expr.span.start, .{ .force_unwrap = .{ .operand = expr } }); } else if (self.current.tag == .kw_catch) { - // `X catch [binding] BODY` — postfix failure handler. + // `X catch [(binding)] BODY` — postfix failure handler. // Four shapes, disambiguated by peeking after `catch`: - // catch { block } — no binding (braces required) - // catch e { block } — binding + block body - // catch e == { case ... } — binding + match body (sugar) - // catch e EXPR — binding + bare-expression body + // catch { block } — no binding (braces required) + // catch (e) { block } — binding + block body + // catch (e) == { case ... } — binding + match body (sugar) + // catch (e) EXPR — binding + bare-expression body self.advance(); // consume 'catch' var binding: ?[]const u8 = null; var binding_span: ?ast.Span = null; var binding_is_raw = false; if (self.current.tag == .identifier) { + return self.fail("the catch error binding needs parens: `catch (e) { ... }`"); + } + if (self.current.tag == .l_paren) { + self.advance(); + if (self.current.tag != .identifier) { + return self.fail("expected an error binding name in `catch (e)`"); + } binding = self.tokenSlice(self.current); binding_span = .{ .start = self.current.loc.start, .end = self.current.loc.end }; binding_is_raw = self.current.is_raw; self.advance(); + try self.expect(.r_paren); } var is_match_body = false; const body: *Node = if (self.current.tag == .l_brace) @@ -4533,7 +4551,7 @@ test "E0.2 catch no binding, braced body" { test "E0.2 catch with binding, block body" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); - const v = try e02FirstValue(arena.allocator(), "f :: () { v := foo() catch e { bar(); }; }"); + const v = try e02FirstValue(arena.allocator(), "f :: () { v := foo() catch (e) { bar(); }; }"); try std.testing.expect(v.data == .catch_expr); try std.testing.expectEqualStrings("e", v.data.catch_expr.binding.?); try std.testing.expect(v.data.catch_expr.body.data == .block); @@ -4542,7 +4560,7 @@ test "E0.2 catch with binding, block body" { test "E0.2 catch with binding, bare-expression body" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); - const v = try e02FirstValue(arena.allocator(), "f :: () { v := foo() catch e bar(); }"); + const v = try e02FirstValue(arena.allocator(), "f :: () { v := foo() catch (e) bar(); }"); try std.testing.expect(v.data == .catch_expr); try std.testing.expectEqualStrings("e", v.data.catch_expr.binding.?); try std.testing.expect(v.data.catch_expr.is_match_body == false); @@ -4552,7 +4570,7 @@ test "E0.2 catch with binding, bare-expression body" { test "E0.2 catch match-body desugars to match_expr over the binding" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); - const v = try e02FirstValue(arena.allocator(), "f :: () { v := foo() catch e == { case .Empty: 0; else: 1; }; }"); + const v = try e02FirstValue(arena.allocator(), "f :: () { v := foo() catch (e) == { case .Empty: 0; else: 1; }; }"); try std.testing.expect(v.data == .catch_expr); try std.testing.expect(v.data.catch_expr.is_match_body); try std.testing.expect(v.data.catch_expr.body.data == .match_expr); @@ -4565,7 +4583,7 @@ test "E0.2 catch match-body desugars to match_expr over the binding" { test "E0.2 catch over a parenthesized or-chain" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); - const v = try e02FirstValue(arena.allocator(), "f :: () { v := (try foo() or try boo()) catch e { }; }"); + const v = try e02FirstValue(arena.allocator(), "f :: () { v := (try foo() or try boo()) catch (e) { }; }"); try std.testing.expect(v.data == .catch_expr); try std.testing.expect(v.data.catch_expr.operand.data == .binary_op); try std.testing.expect(v.data.catch_expr.operand.data.binary_op.op == .or_op); @@ -4645,7 +4663,7 @@ test "E1.7 break rejected inside a defer body (transitive through a loop)" { test "E1.7 continue rejected inside an onfail body" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); - var parser = Parser.init(arena.allocator(), "f :: () { onfail e { continue; } }"); + var parser = Parser.init(arena.allocator(), "f :: () { onfail (e) { continue; } }"); try std.testing.expectError(error.ParseError, parser.parse()); } @@ -4669,7 +4687,7 @@ test "E1.7 control-flow legal again after the cleanup body (flag restored)" { test "E0.2 onfail with binding and block body" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); - const s = try e02FirstStmt(arena.allocator(), "f :: () { onfail e { close(h); } }"); + const s = try e02FirstStmt(arena.allocator(), "f :: () { onfail (e) { close(h); } }"); try std.testing.expect(s.data == .onfail_stmt); try std.testing.expectEqualStrings("e", s.data.onfail_stmt.binding.?); try std.testing.expect(s.data.onfail_stmt.body.data == .block); @@ -4701,10 +4719,10 @@ test "E0.2 consumer-aware pipe: x |> try f() inserts x into the head call" { try std.testing.expectEqualStrings("x", call.data.call.args[0].data.identifier.name); } -test "E0.2 consumer-aware pipe: x |> f() catch e { } preserves the catch" { +test "E0.2 consumer-aware pipe: x |> f() catch (e) { } preserves the catch" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); - const v = try e02FirstValue(arena.allocator(), "f :: () { v := x |> g() catch e { }; }"); + const v = try e02FirstValue(arena.allocator(), "f :: () { v := x |> g() catch (e) { }; }"); try std.testing.expect(v.data == .catch_expr); try std.testing.expectEqualStrings("e", v.data.catch_expr.binding.?); try std.testing.expect(v.data.catch_expr.operand.data == .call); @@ -4741,19 +4759,19 @@ test "E0.2 round-trip print: try / or precedence / raise / catch / onfail" { try e02ExpectPrints(a, try e02FirstValue(a, "f :: () { v := try foo() or try boo(); }"), "try foo() or try boo()"); try e02ExpectPrints(a, try e02FirstStmt(a, "f :: () { raise error.BadDigit; }"), "raise error.BadDigit"); try e02ExpectPrints(a, try e02FirstStmt(a, "f :: () { raise e; }"), "raise e"); - try e02ExpectPrints(a, try e02FirstValue(a, "f :: () { v := foo() catch e bar(); }"), "foo() catch e bar()"); - try e02ExpectPrints(a, try e02FirstValue(a, "f :: () { v := foo() catch e { bar(); }; }"), "foo() catch e { bar(); }"); + try e02ExpectPrints(a, try e02FirstValue(a, "f :: () { v := foo() catch (e) bar(); }"), "foo() catch (e) bar()"); + try e02ExpectPrints(a, try e02FirstValue(a, "f :: () { v := foo() catch (e) { bar(); }; }"), "foo() catch (e) { bar(); }"); try e02ExpectPrints(a, try e02FirstValue(a, "f :: () { v := foo() catch { bar(); }; }"), "foo() catch { bar(); }"); try e02ExpectPrints(a, try e02FirstStmt(a, "f :: () { onfail close(h); }"), "onfail close(h)"); - try e02ExpectPrints(a, try e02FirstStmt(a, "f :: () { onfail e { close(h); } }"), "onfail e { close(h); }"); + try e02ExpectPrints(a, try e02FirstStmt(a, "f :: () { onfail (e) { close(h); } }"), "onfail (e) { close(h); }"); } test "E0.2 round-trip print: catch match-body form" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator(); - const v = try e02FirstValue(a, "f :: () { v := foo() catch e == { case .Empty: 0; else: 1; }; }"); - try e02ExpectPrints(a, v, "foo() catch e == { case .Empty: 0; else: 1; }"); + const v = try e02FirstValue(a, "f :: () { v := foo() catch (e) == { case .Empty: 0; else: 1; }; }"); + try e02ExpectPrints(a, v, "foo() catch (e) == { case .Empty: 0; else: 1; }"); } // ── ERR step E0.3 — coverage consolidation (gaps + integration) ── @@ -4788,9 +4806,9 @@ test "E0.3 or value-terminator: parse(s) or 0" { test "E0.3 full failable function parses end-to-end (all E0 forms)" { const source = \\parse :: (s: string) -> (s32, !ParseErr) { - \\ onfail e { cleanup(s); } + \\ onfail (e) { cleanup(s); } \\ v := try inner(s) or 0; - \\ w := other(s) catch e2 { return 0; }; + \\ w := other(s) catch (e2) { return 0; }; \\ if bad(s) { raise error.BadDigit; } \\ return v; \\} diff --git a/src/print.zig b/src/print.zig index b205c61..bb76a2c 100644 --- a/src/print.zig +++ b/src/print.zig @@ -77,8 +77,9 @@ pub fn printExpr(node: *const Node, writer: Writer) anyerror!void { try printExpr(c.operand, writer); try writer.writeAll(" catch"); if (c.binding) |bnd| { - try writer.writeByte(' '); + try writer.writeAll(" ("); try writer.writeAll(bnd); + try writer.writeByte(')'); } if (c.is_match_body) { try writer.writeAll(" == "); @@ -95,8 +96,9 @@ pub fn printExpr(node: *const Node, writer: Writer) anyerror!void { .onfail_stmt => |o| { try writer.writeAll("onfail"); if (o.binding) |bnd| { - try writer.writeByte(' '); + try writer.writeAll(" ("); try writer.writeAll(bnd); + try writer.writeByte(')'); } try writer.writeByte(' '); try printExpr(o.body, writer);