ERR/E1.7: onfail — cleanup on error-exit, interleaved with defer
`onfail [e] BODY` runs cleanup only when an error LEAVES the enclosing block
(a `raise` or a propagating `try`), and is skipped on success — unlike `defer`,
which runs on every exit. On an error exit, defers and onfails run interleaved
in reverse declaration order; `onfail e` binds the in-flight error tag.
- Cleanup stack: defer_stack now holds CleanupEntry { body, is_onfail, binding }
(one declaration-ordered stack so defer/onfail interleave). lowerDefer pushes
a defer entry; lowerOnFail (new `.onfail_stmt` arm) pushes an onfail entry,
rejecting `onfail` outside a failable function.
- emitBlockDefers (success exits — return / normal block exit) now emits only
`defer` entries and discards onfails.
- emitErrorCleanup (new; wired at the error exits — lowerRaise pure +
value-carrying, lowerTry propagation) emits both kinds interleaved in reverse,
binding the in-flight tag for `onfail e`.
Block-rooted: an error propagating to the function drains all enclosing blocks'
onfails; a block that exits normally discards its onfails. Per-attempt-`try`
gating is moot for now (no compilable `or` chain can absorb a mid-block try
failure yet — E2.4b). Body restrictions beyond the parser's raise-in-onfail
ban are deferred.
Tests: examples/233-onfail.sx (interleave order on error vs success + binding;
deterministic trace), examples/234-onfail-reject.sx (onfail outside a failable
fn rejected; exit 1). Gates: zig build, zig build test, 272/272 examples.
This commit is contained in:
42
examples/233-onfail.sx
Normal file
42
examples/233-onfail.sx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// `onfail` — cleanup that runs only when an error LEAVES the enclosing block
|
||||||
|
// (ERR step E1.7). Unlike `defer` (which runs on every exit), `onfail` fires
|
||||||
|
// on an error exit — a `raise` or a propagating `try` — and is skipped on
|
||||||
|
// success. On an error exit `defer` and `onfail` run interleaved in reverse
|
||||||
|
// declaration order. `onfail e { … }` binds the in-flight error tag.
|
||||||
|
// (Per-attempt-`try` gating and `or`-chain absorption refine this in E2.4b.)
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
E :: error { Bad }
|
||||||
|
|
||||||
|
inner :: (n: s32) -> !E {
|
||||||
|
if n < 0 { raise error.Bad; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// defer + onfail interleave on the error path; only defers on success.
|
||||||
|
run :: (n: s32) -> !E {
|
||||||
|
defer print("defer A\n");
|
||||||
|
onfail print("onfail B\n");
|
||||||
|
defer print("defer C\n");
|
||||||
|
try inner(n); // n<0 → propagates → onfail fires
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `onfail e` binds the tag.
|
||||||
|
classify :: (n: s32) -> !E {
|
||||||
|
onfail e { if e == error.Bad { print("cleanup: bad\n"); } }
|
||||||
|
if n < 0 { raise error.Bad; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
print("[fail]\n");
|
||||||
|
a := run(-1); // error → defer C, onfail B, defer A
|
||||||
|
print("[ok]\n");
|
||||||
|
b := run(7); // success → defer C, defer A (no onfail)
|
||||||
|
print("[bound]\n");
|
||||||
|
c := classify(-1); // onfail binding sees Bad
|
||||||
|
print("[done]\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
15
examples/234-onfail-reject.sx
Normal file
15
examples/234-onfail-reject.sx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// `onfail` rejection (ERR step E1.7): `onfail` is only valid inside a failable
|
||||||
|
// function. A non-failable function never error-exits, so an `onfail` could
|
||||||
|
// never fire — use `defer` for unconditional cleanup. The positive cases live
|
||||||
|
// in `examples/233-onfail.sx`.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
non_failable :: () -> s32 {
|
||||||
|
onfail print("never fires\n"); // error: onfail outside a failable function
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
return non_failable();
|
||||||
|
}
|
||||||
@@ -76,6 +76,16 @@ const Scope = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// A pending block-scoped cleanup: `defer` (runs on every block exit) or
|
||||||
|
/// `onfail` (runs only when an error leaves the block, binding the in-flight
|
||||||
|
/// tag). Both share one declaration-ordered stack so error-exit cleanup runs
|
||||||
|
/// them interleaved in reverse order (ERR E1.7).
|
||||||
|
const CleanupEntry = struct {
|
||||||
|
body: *const Node,
|
||||||
|
is_onfail: bool,
|
||||||
|
binding: ?[]const u8 = null,
|
||||||
|
};
|
||||||
|
|
||||||
// ── Lowering ────────────────────────────────────────────────────────────
|
// ── Lowering ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub const Lowering = struct {
|
pub const Lowering = struct {
|
||||||
@@ -121,7 +131,7 @@ pub const Lowering = struct {
|
|||||||
current_match_tags: ?[]const u64 = null, // type tags for current match arm (for runtime dispatch)
|
current_match_tags: ?[]const u64 = null, // type tags for current match arm (for runtime dispatch)
|
||||||
force_block_value: bool = false, // set by lowerBlockValue to extract if-else values
|
force_block_value: bool = false, // set by lowerBlockValue to extract if-else values
|
||||||
block_terminated: bool = false, // set when constant-folded if emits a return/br into current block
|
block_terminated: bool = false, // set when constant-folded if emits a return/br into current block
|
||||||
defer_stack: std.ArrayList(*const Node) = std.ArrayList(*const Node).empty, // block-scoped defer stack
|
defer_stack: std.ArrayList(CleanupEntry) = std.ArrayList(CleanupEntry).empty, // block-scoped defer + onfail cleanup stack
|
||||||
func_defer_base: usize = 0, // defer stack base for current function (lowerReturn drains to this)
|
func_defer_base: usize = 0, // defer stack base for current function (lowerReturn drains to this)
|
||||||
global_names: std.StringHashMap(GlobalInfo) = std.StringHashMap(GlobalInfo).init(std.heap.page_allocator), // #run global name → GlobalId
|
global_names: std.StringHashMap(GlobalInfo) = std.StringHashMap(GlobalInfo).init(std.heap.page_allocator), // #run global name → GlobalId
|
||||||
deferred_type_fns: std.ArrayList([]const u8) = std.ArrayList([]const u8).empty, // functions deferred until all types registered
|
deferred_type_fns: std.ArrayList([]const u8) = std.ArrayList([]const u8).empty, // functions deferred until all types registered
|
||||||
@@ -1472,6 +1482,7 @@ pub const Lowering = struct {
|
|||||||
.raise_stmt => |rs| self.lowerRaise(&rs, node.span),
|
.raise_stmt => |rs| self.lowerRaise(&rs, node.span),
|
||||||
.assignment => |asgn| self.lowerAssignment(&asgn),
|
.assignment => |asgn| self.lowerAssignment(&asgn),
|
||||||
.defer_stmt => |ds| self.lowerDefer(&ds),
|
.defer_stmt => |ds| self.lowerDefer(&ds),
|
||||||
|
.onfail_stmt => |ofs| self.lowerOnFail(&ofs, node.span),
|
||||||
.push_stmt => |ps| self.lowerPush(&ps),
|
.push_stmt => |ps| self.lowerPush(&ps),
|
||||||
.multi_assign => |ma| self.lowerMultiAssign(&ma),
|
.multi_assign => |ma| self.lowerMultiAssign(&ma),
|
||||||
.destructure_decl => |dd| self.lowerDestructureDecl(&dd),
|
.destructure_decl => |dd| self.lowerDestructureDecl(&dd),
|
||||||
@@ -8084,17 +8095,42 @@ pub const Lowering = struct {
|
|||||||
// ── Defer/Push/MultiAssign ──────────────────────────────────────
|
// ── Defer/Push/MultiAssign ──────────────────────────────────────
|
||||||
|
|
||||||
fn lowerDefer(self: *Lowering, ds: *const ast.DeferStmt) void {
|
fn lowerDefer(self: *Lowering, ds: *const ast.DeferStmt) void {
|
||||||
// Push deferred expression onto the stack — will be emitted at block exit in LIFO order
|
// Push deferred expression onto the stack — emitted at every block exit, LIFO.
|
||||||
self.defer_stack.append(self.alloc, ds.expr) catch {};
|
self.defer_stack.append(self.alloc, .{ .body = ds.expr, .is_onfail = false }) catch {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Emit deferred expressions from saved_len..current in reverse (LIFO) order,
|
/// `onfail [e] BODY` (ERR E1.7) — cleanup that runs only when an error
|
||||||
/// then truncate the defer stack back to saved_len.
|
/// leaves the enclosing block. Recorded on the shared cleanup stack;
|
||||||
|
/// emitted (interleaved with defers, reverse) at error exits by
|
||||||
|
/// `emitErrorCleanup`, and discarded — never run — on a success exit.
|
||||||
|
fn lowerOnFail(self: *Lowering, ofs: *const ast.OnFailStmt, span: ast.Span) void {
|
||||||
|
// `onfail` is only meaningful inside a failable function — a
|
||||||
|
// non-failable function never error-exits, so it could never fire.
|
||||||
|
const ret_ty = self.effectiveReturnType() orelse {
|
||||||
|
self.diagOnFailNotFailable(span);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if (self.errorChannelOf(ret_ty) == null) {
|
||||||
|
self.diagOnFailNotFailable(span);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.defer_stack.append(self.alloc, .{ .body = ofs.body, .is_onfail = true, .binding = ofs.binding }) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diagOnFailNotFailable(self: *Lowering, span: ast.Span) void {
|
||||||
|
if (self.diagnostics) |diags| {
|
||||||
|
diags.addFmt(.err, span, "`onfail` is only valid inside a failable function (a return type with `!` or `!Named`) — use `defer` for unconditional cleanup", .{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit cleanups from saved_len..current in reverse (LIFO) order on a
|
||||||
|
/// SUCCESS exit: only `defer` entries run; `onfail` entries are skipped
|
||||||
|
/// (and discarded by the truncation). Truncates the stack to saved_len.
|
||||||
fn emitBlockDefers(self: *Lowering, saved_len: usize) void {
|
fn emitBlockDefers(self: *Lowering, saved_len: usize) void {
|
||||||
// Guard: if stack was already drained (e.g., by a return that emitted all defers)
|
// Guard: if stack was already drained (e.g., by a return that emitted all defers)
|
||||||
if (saved_len > self.defer_stack.items.len) return;
|
if (saved_len > self.defer_stack.items.len) return;
|
||||||
if (self.currentBlockHasTerminator()) {
|
if (self.currentBlockHasTerminator()) {
|
||||||
// Block already terminated (e.g., by return) — defers were already emitted
|
// Block already terminated (e.g., by return) — cleanups were already emitted
|
||||||
self.defer_stack.shrinkRetainingCapacity(saved_len);
|
self.defer_stack.shrinkRetainingCapacity(saved_len);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -8102,11 +8138,43 @@ pub const Lowering = struct {
|
|||||||
var i = stack.len;
|
var i = stack.len;
|
||||||
while (i > saved_len) {
|
while (i > saved_len) {
|
||||||
i -= 1;
|
i -= 1;
|
||||||
_ = self.lowerExpr(stack[i]);
|
if (!stack[i].is_onfail) _ = self.lowerExpr(stack[i].body);
|
||||||
}
|
}
|
||||||
self.defer_stack.shrinkRetainingCapacity(saved_len);
|
self.defer_stack.shrinkRetainingCapacity(saved_len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Emit cleanups from `base`..current in reverse order on an ERROR exit
|
||||||
|
/// (raise / try-propagation): BOTH `defer` and `onfail` entries run,
|
||||||
|
/// interleaved in reverse declaration order. `err_tag` is the in-flight
|
||||||
|
/// error tag, bound to each `onfail e`'s binding. Does not truncate — the
|
||||||
|
/// terminating `ret` + the unwinding block-scope `emitBlockDefers` (which
|
||||||
|
/// then see the terminator and skip) leave the stack consistent.
|
||||||
|
fn emitErrorCleanup(self: *Lowering, base: usize, err_tag: Ref) void {
|
||||||
|
if (base > self.defer_stack.items.len) return;
|
||||||
|
const tag_ty = self.builder.getRefType(err_tag);
|
||||||
|
const stack = self.defer_stack.items;
|
||||||
|
var i = stack.len;
|
||||||
|
while (i > base) {
|
||||||
|
i -= 1;
|
||||||
|
const entry = stack[i];
|
||||||
|
if (entry.is_onfail) {
|
||||||
|
if (entry.binding) |name| {
|
||||||
|
var ofscope = Scope.init(self.alloc, self.scope);
|
||||||
|
const saved = self.scope;
|
||||||
|
self.scope = &ofscope;
|
||||||
|
ofscope.put(name, .{ .ref = err_tag, .ty = tag_ty, .is_alloca = false });
|
||||||
|
_ = self.lowerExpr(entry.body);
|
||||||
|
self.scope = saved;
|
||||||
|
ofscope.deinit();
|
||||||
|
} else {
|
||||||
|
_ = self.lowerExpr(entry.body);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_ = self.lowerExpr(entry.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn lowerPush(self: *Lowering, ps: *const ast.PushStmt) void {
|
fn lowerPush(self: *Lowering, ps: *const ast.PushStmt) void {
|
||||||
// push Context.{...} { body } — allocates a fresh Context on the
|
// push Context.{...} { body } — allocates a fresh Context on the
|
||||||
// stack frame, rebinds the lowering's `current_ctx_ref` to it for
|
// stack frame, rebinds the lowering's `current_ctx_ref` to it for
|
||||||
@@ -15154,9 +15222,9 @@ pub const Lowering = struct {
|
|||||||
// (3) Emit the failure return. Pure-failable: the return type IS the
|
// (3) Emit the failure return. Pure-failable: the return type IS the
|
||||||
// error set, so return the tag value directly.
|
// error set, so return the tag value directly.
|
||||||
if (ret_ty == err_set) {
|
if (ret_ty == err_set) {
|
||||||
self.emitBlockDefers(self.func_defer_base);
|
|
||||||
const tag_ty = self.builder.getRefType(tag_ref);
|
const tag_ty = self.builder.getRefType(tag_ref);
|
||||||
const coerced = if (tag_ty != err_set) self.coerceToType(tag_ref, tag_ty, err_set) else tag_ref;
|
const coerced = if (tag_ty != err_set) self.coerceToType(tag_ref, tag_ty, err_set) else tag_ref;
|
||||||
|
self.emitErrorCleanup(self.func_defer_base, coerced);
|
||||||
if (self.inline_return_target) |iri| {
|
if (self.inline_return_target) |iri| {
|
||||||
self.builder.store(iri.slot, coerced);
|
self.builder.store(iri.slot, coerced);
|
||||||
self.builder.br(iri.done_bb, &.{});
|
self.builder.br(iri.done_bb, &.{});
|
||||||
@@ -15166,15 +15234,15 @@ pub const Lowering = struct {
|
|||||||
} else {
|
} else {
|
||||||
// Value-carrying `-> (T..., !)`: the error path leaves the value
|
// Value-carrying `-> (T..., !)`: the error path leaves the value
|
||||||
// slots undefined and carries the tag in the error slot (ERR E2.1).
|
// slots undefined and carries the tag in the error slot (ERR E2.1).
|
||||||
self.emitBlockDefers(self.func_defer_base);
|
const tag_ty = self.builder.getRefType(tag_ref);
|
||||||
|
const coerced_tag = if (tag_ty != err_set) self.coerceToType(tag_ref, tag_ty, err_set) else tag_ref;
|
||||||
|
self.emitErrorCleanup(self.func_defer_base, coerced_tag);
|
||||||
const fields = self.module.types.get(ret_ty).tuple.fields;
|
const fields = self.module.types.get(ret_ty).tuple.fields;
|
||||||
var slots = std.ArrayList(Ref).empty;
|
var slots = std.ArrayList(Ref).empty;
|
||||||
defer slots.deinit(self.alloc);
|
defer slots.deinit(self.alloc);
|
||||||
for (fields[0 .. fields.len - 1]) |vty| {
|
for (fields[0 .. fields.len - 1]) |vty| {
|
||||||
slots.append(self.alloc, self.builder.constUndef(vty)) catch unreachable;
|
slots.append(self.alloc, self.builder.constUndef(vty)) catch unreachable;
|
||||||
}
|
}
|
||||||
const tag_ty = self.builder.getRefType(tag_ref);
|
|
||||||
const coerced_tag = if (tag_ty != err_set) self.coerceToType(tag_ref, tag_ty, err_set) else tag_ref;
|
|
||||||
const tup = self.buildFailableTuple(ret_ty, slots.items, coerced_tag);
|
const tup = self.buildFailableTuple(ret_ty, slots.items, coerced_tag);
|
||||||
self.emitTupleRet(ret_ty, tup);
|
self.emitTupleRet(ret_ty, tup);
|
||||||
}
|
}
|
||||||
@@ -15310,11 +15378,11 @@ pub const Lowering = struct {
|
|||||||
const ok_bb = self.freshBlock("try.ok");
|
const ok_bb = self.freshBlock("try.ok");
|
||||||
self.builder.condBr(is_err, prop_bb, &.{}, ok_bb, &.{});
|
self.builder.condBr(is_err, prop_bb, &.{}, ok_bb, &.{});
|
||||||
|
|
||||||
// Propagation: run the function's defers, then return the caller's
|
// Propagation: run the function's cleanups (defers + onfails, since
|
||||||
// failure carrying this tag (pure caller → `ret(tag)`; value-carrying
|
// this is an error exit), then return the caller's failure carrying
|
||||||
// caller → `ret {undef..., tag}`).
|
// this tag (pure caller → `ret(tag)`; value-carrying → `ret {undef…, tag}`).
|
||||||
self.builder.switchToBlock(prop_bb);
|
self.builder.switchToBlock(prop_bb);
|
||||||
self.emitBlockDefers(self.func_defer_base);
|
self.emitErrorCleanup(self.func_defer_base, err_val);
|
||||||
self.emitErrorReturn(caller_ret, caller_set, err_val);
|
self.emitErrorReturn(caller_ret, caller_set, err_val);
|
||||||
|
|
||||||
// Success: a value-carrying callee yields its value slot; a
|
// Success: a value-carrying callee yields its value slot; a
|
||||||
|
|||||||
1
tests/expected/233-onfail.exit
Normal file
1
tests/expected/233-onfail.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
10
tests/expected/233-onfail.txt
Normal file
10
tests/expected/233-onfail.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[fail]
|
||||||
|
defer C
|
||||||
|
onfail B
|
||||||
|
defer A
|
||||||
|
[ok]
|
||||||
|
defer C
|
||||||
|
defer A
|
||||||
|
[bound]
|
||||||
|
cleanup: bad
|
||||||
|
[done]
|
||||||
1
tests/expected/234-onfail-reject.exit
Normal file
1
tests/expected/234-onfail-reject.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
5
tests/expected/234-onfail-reject.txt
Normal file
5
tests/expected/234-onfail-reject.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
error: `onfail` is only valid inside a failable function (a return type with `!` or `!Named`) — use `defer` for unconditional cleanup
|
||||||
|
--> /Users/agra/projects/sx/examples/234-onfail-reject.sx:9:5
|
||||||
|
|
|
||||||
|
9 | onfail print("never fires\n"); // error: onfail outside a failable function
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
Reference in New Issue
Block a user