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:
@@ -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;
|
||||
\\}
|
||||
|
||||
Reference in New Issue
Block a user