diff --git a/examples/237-cleanup-body-restrictions.sx b/examples/237-cleanup-body-restrictions.sx new file mode 100644 index 0000000..b47fc0d --- /dev/null +++ b/examples/237-cleanup-body-restrictions.sx @@ -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; } diff --git a/src/parser.zig b/src/parser.zig index f59cec2..501ee57 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -2007,6 +2007,7 @@ pub const Parser = struct { // Return statement: return expr; or return; if (self.current.tag == .kw_return) { + try self.rejectInCleanup("return"); const start = self.current.loc.start; self.advance(); if (self.current.tag == .semicolon) { @@ -2071,6 +2072,7 @@ pub const Parser = struct { // Break statement: break; if (self.current.tag == .kw_break) { + try self.rejectInCleanup("break"); const start = self.current.loc.start; self.advance(); try self.expect(.semicolon); @@ -2079,6 +2081,7 @@ pub const Parser = struct { // Continue statement: continue; if (self.current.tag == .kw_continue) { + try self.rejectInCleanup("continue"); const start = self.current.loc.start; self.advance(); try self.expect(.semicolon); @@ -2329,6 +2332,7 @@ pub const Parser = struct { // stack by adjacency (`xx try foo()` = `xx (try foo())`). Failability // of the operand is a sema check (E1.4), not a parse-time restriction. if (self.current.tag == .kw_try) { + try self.rejectInCleanup("try"); const start = self.current.loc.start; self.advance(); const operand = try self.parseUnary(); @@ -3325,6 +3329,19 @@ pub const Parser = struct { // Optional calling convention: callconv(.c) 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: // (params) => expr — expression lambda // (params) { stmts } — block-body lambda @@ -3607,6 +3624,29 @@ pub const Parser = struct { 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 { 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()); } +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" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); diff --git a/tests/expected/237-cleanup-body-restrictions.exit b/tests/expected/237-cleanup-body-restrictions.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/tests/expected/237-cleanup-body-restrictions.exit @@ -0,0 +1 @@ +1 diff --git a/tests/expected/237-cleanup-body-restrictions.txt b/tests/expected/237-cleanup-body-restrictions.txt new file mode 100644 index 0000000..5dfdd06 --- /dev/null +++ b/tests/expected/237-cleanup-body-restrictions.txt @@ -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 + | ^^^^^^