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

@@ -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 <expr> catch e {{ ... }}` or `#run <expr> or <default>`\n", .{});
std.debug.print("help: handle it at the `#run` site — `#run <expr> catch (e) {{ ... }}` or `#run <expr> or <default>`\n", .{});
}
/// Run comptime side-effect functions (e.g., `#run main();` at top level).

View File

@@ -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 <expr>;
// Onfail statement: onfail { body } | onfail (e) { body } | onfail <expr>;
// 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;
\\}

View File

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