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:
agra
2026-06-01 01:14:24 +03:00
parent 66740fa95b
commit f9dd965b69
4 changed files with 117 additions and 0 deletions

View 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; }

View File

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

View File

@@ -0,0 +1 @@
1

View 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
| ^^^^^^