ERR/E1.7: ban return/break/continue/try in defer & onfail bodies
A defer or onfail body runs while the block/function is already exiting, so it
has no target to transfer control to. `raise` was already rejected (E1.3); this
adds the rest of the locked set — `return` / `break` / `continue` / `try`.
In parseStmt, the return/break/continue/try parse sites now call a new
rejectInCleanup() helper, gated on in_onfail_body || in_defer_body (the existing
flags, whose doc-comments already scoped this follow-up). The ban is transitive
through nested catch bodies and loops, but parseLambda clears both flags for the
closure body — a closure is its own function boundary, so a `return` from a
closure created inside a cleanup body stays legal. The diagnostic names the
cleanup kind ("an `onfail`" / "a `defer`").
examples/237-cleanup-body-restrictions.sx covers the rejected forms (exit 1);
six inline parser tests cover each banned exit, the transitive-through-loop
case, the closure-boundary exception, and flag-restore after the defer.
Note: examples/213-canonical-map.sx is the user's uncommitted heterogeneous-
variadic-pack WIP (prints 40 vs expected 42); it fails on the committed parser
too, independent of this change, and is left unstaged.
Gates: zig build, zig build test (288 pass), bash tests/run_examples.sh (all
green except the unrelated 213 WIP).
This commit is contained in:
26
examples/237-cleanup-body-restrictions.sx
Normal file
26
examples/237-cleanup-body-restrictions.sx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Cleanup-body control-flow restrictions (ERR step E1.7 follow-up). A `defer`
|
||||||
|
// or `onfail` body runs while the block/function is already exiting, so it has
|
||||||
|
// no target to transfer control to: `raise` / `try` / `return` / `break` /
|
||||||
|
// `continue` are all rejected inside one. The ban is transitive through nested
|
||||||
|
// `catch` bodies and loops, but NOT through a nested closure (its own function
|
||||||
|
// boundary). `raise` was already banned (E1.3); this adds the other four.
|
||||||
|
// This file is expected to FAIL compilation (exit 1).
|
||||||
|
//
|
||||||
|
// Run: ./zig-out/bin/sx run examples/237-cleanup-body-restrictions.sx
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
E :: error { Bad }
|
||||||
|
|
||||||
|
g :: () -> !E { return; }
|
||||||
|
|
||||||
|
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
|
||||||
|
try g();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> s32 { return 0; }
|
||||||
@@ -2007,6 +2007,7 @@ pub const Parser = struct {
|
|||||||
|
|
||||||
// Return statement: return expr; or return;
|
// Return statement: return expr; or return;
|
||||||
if (self.current.tag == .kw_return) {
|
if (self.current.tag == .kw_return) {
|
||||||
|
try self.rejectInCleanup("return");
|
||||||
const start = self.current.loc.start;
|
const start = self.current.loc.start;
|
||||||
self.advance();
|
self.advance();
|
||||||
if (self.current.tag == .semicolon) {
|
if (self.current.tag == .semicolon) {
|
||||||
@@ -2071,6 +2072,7 @@ pub const Parser = struct {
|
|||||||
|
|
||||||
// Break statement: break;
|
// Break statement: break;
|
||||||
if (self.current.tag == .kw_break) {
|
if (self.current.tag == .kw_break) {
|
||||||
|
try self.rejectInCleanup("break");
|
||||||
const start = self.current.loc.start;
|
const start = self.current.loc.start;
|
||||||
self.advance();
|
self.advance();
|
||||||
try self.expect(.semicolon);
|
try self.expect(.semicolon);
|
||||||
@@ -2079,6 +2081,7 @@ pub const Parser = struct {
|
|||||||
|
|
||||||
// Continue statement: continue;
|
// Continue statement: continue;
|
||||||
if (self.current.tag == .kw_continue) {
|
if (self.current.tag == .kw_continue) {
|
||||||
|
try self.rejectInCleanup("continue");
|
||||||
const start = self.current.loc.start;
|
const start = self.current.loc.start;
|
||||||
self.advance();
|
self.advance();
|
||||||
try self.expect(.semicolon);
|
try self.expect(.semicolon);
|
||||||
@@ -2329,6 +2332,7 @@ pub const Parser = struct {
|
|||||||
// stack by adjacency (`xx try foo()` = `xx (try foo())`). Failability
|
// stack by adjacency (`xx try foo()` = `xx (try foo())`). Failability
|
||||||
// of the operand is a sema check (E1.4), not a parse-time restriction.
|
// of the operand is a sema check (E1.4), not a parse-time restriction.
|
||||||
if (self.current.tag == .kw_try) {
|
if (self.current.tag == .kw_try) {
|
||||||
|
try self.rejectInCleanup("try");
|
||||||
const start = self.current.loc.start;
|
const start = self.current.loc.start;
|
||||||
self.advance();
|
self.advance();
|
||||||
const operand = try self.parseUnary();
|
const operand = try self.parseUnary();
|
||||||
@@ -3325,6 +3329,19 @@ pub const Parser = struct {
|
|||||||
// Optional calling convention: callconv(.c)
|
// Optional calling convention: callconv(.c)
|
||||||
const call_conv = try self.parseOptionalCallConv();
|
const call_conv = try self.parseOptionalCallConv();
|
||||||
|
|
||||||
|
// A closure is its own function boundary: clear the cleanup-body flags
|
||||||
|
// so control-flow exits inside the closure body (`return` from the
|
||||||
|
// closure, etc.) are legal even when the closure literal appears inside
|
||||||
|
// a `defer` / `onfail` body. Restored after the body.
|
||||||
|
const saved_onfail = self.in_onfail_body;
|
||||||
|
const saved_defer = self.in_defer_body;
|
||||||
|
self.in_onfail_body = false;
|
||||||
|
self.in_defer_body = false;
|
||||||
|
defer {
|
||||||
|
self.in_onfail_body = saved_onfail;
|
||||||
|
self.in_defer_body = saved_defer;
|
||||||
|
}
|
||||||
|
|
||||||
// Two body forms:
|
// Two body forms:
|
||||||
// (params) => expr — expression lambda
|
// (params) => expr — expression lambda
|
||||||
// (params) { stmts } — block-body lambda
|
// (params) { stmts } — block-body lambda
|
||||||
@@ -3607,6 +3624,29 @@ pub const Parser = struct {
|
|||||||
return self.fail(msg);
|
return self.fail(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A cleanup body is a `defer` or `onfail` body. Control-flow exits
|
||||||
|
/// (`raise` / `try` / `return` / `break` / `continue`) are banned inside one:
|
||||||
|
/// cleanup runs while the block/function is already exiting, so there is
|
||||||
|
/// nothing to propagate or transfer to. The ban is transitive through nested
|
||||||
|
/// `catch` bodies and loops, but NOT through a nested closure body — a
|
||||||
|
/// closure is its own function boundary (parseLambda clears the flags).
|
||||||
|
fn inCleanupBody(self: *const Parser) bool {
|
||||||
|
return self.in_onfail_body or self.in_defer_body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The cleanup-body phrase for diagnostics, with article (`onfail` takes
|
||||||
|
/// precedence when both are set, e.g. an `onfail` nested in a `defer`).
|
||||||
|
fn cleanupKind(self: *const Parser) []const u8 {
|
||||||
|
return if (self.in_onfail_body) "an `onfail`" else "a `defer`";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reject a control-flow exit `kw` (e.g. "return") inside a cleanup body.
|
||||||
|
fn rejectInCleanup(self: *Parser, comptime kw: []const u8) error{ParseError}!void {
|
||||||
|
if (self.inCleanupBody()) {
|
||||||
|
return self.failFmt("`" ++ kw ++ "` is not allowed inside {s} body — cleanup runs while the function is already exiting, so there is nothing to transfer control to", .{self.cleanupKind()});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn tokenSlice(self: *const Parser, token: Token) []const u8 {
|
fn tokenSlice(self: *const Parser, token: Token) []const u8 {
|
||||||
return self.source[token.loc.start..token.loc.end];
|
return self.source[token.loc.start..token.loc.end];
|
||||||
}
|
}
|
||||||
@@ -4274,6 +4314,51 @@ test "E1.3 raise rejected inside a defer body" {
|
|||||||
try std.testing.expectError(error.ParseError, parser.parse());
|
try std.testing.expectError(error.ParseError, parser.parse());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "E1.7 return rejected inside a defer body" {
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
var parser = Parser.init(arena.allocator(), "f :: () { defer { return; } }");
|
||||||
|
try std.testing.expectError(error.ParseError, parser.parse());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "E1.7 try 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 { try g(); } }");
|
||||||
|
try std.testing.expectError(error.ParseError, parser.parse());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "E1.7 break rejected inside a defer body (transitive through a loop)" {
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
var parser = Parser.init(arena.allocator(), "f :: () { defer { for 0..1: (i) { break; } } }");
|
||||||
|
try std.testing.expectError(error.ParseError, parser.parse());
|
||||||
|
}
|
||||||
|
|
||||||
|
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; } }");
|
||||||
|
try std.testing.expectError(error.ParseError, parser.parse());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "E1.7 return inside a closure within a cleanup body is allowed" {
|
||||||
|
// A closure is its own function boundary: parseLambda clears the cleanup
|
||||||
|
// flags, so `return` from the closure body is legal even inside `defer`.
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
var parser = Parser.init(arena.allocator(), "f :: () { defer g((x: s32) -> s32 { return x; }); }");
|
||||||
|
_ = try parser.parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
test "E1.7 control-flow legal again after the cleanup body (flag restored)" {
|
||||||
|
// The cleanup-body flag must not leak to statements that follow the defer.
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
var parser = Parser.init(arena.allocator(), "f :: () { defer cleanup(); return; }");
|
||||||
|
_ = try parser.parse();
|
||||||
|
}
|
||||||
|
|
||||||
test "E0.2 onfail with binding and block body" {
|
test "E0.2 onfail with binding and block body" {
|
||||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||||
defer arena.deinit();
|
defer arena.deinit();
|
||||||
|
|||||||
1
tests/expected/237-cleanup-body-restrictions.exit
Normal file
1
tests/expected/237-cleanup-body-restrictions.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
5
tests/expected/237-cleanup-body-restrictions.txt
Normal file
5
tests/expected/237-cleanup-body-restrictions.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
error: `return` is not allowed inside a `defer` body — cleanup runs while the function is already exiting, so there is nothing to transfer control to
|
||||||
|
--> /Users/agra/projects/sx/examples/237-cleanup-body-restrictions.sx:18:14
|
||||||
|
|
|
||||||
|
18 | defer { return; } // ERROR: return in defer body
|
||||||
|
| ^^^^^^
|
||||||
Reference in New Issue
Block a user