ERR/E1.7: reject bare failable calls in defer/onfail cleanup bodies
A `defer`/`onfail` body runs while the block is already exiting, so a
failable call there has nowhere to propagate its error. The parser
already bans `try`/`raise`/`return`/`break`/`continue` in cleanup bodies
(f9dd965); this adds the remaining sema rule — a bare (un-absorbed)
failable call must be absorbed locally with `catch` or `or <value>`.
Implemented in the shared error-flow pass (`checkCleanupBody` /
`checkCleanupNode` / `cleanupReject` in ir/lower.zig): when the walk hits
a `defer`/`onfail`, it scans the body transitively (through blocks, `if`,
loops, match arms, `catch` handlers; stopping at nested closures) and
flags any still-failable expression. `catch` / `or value` strip the
error channel, so `exprIsFailable` is false for them — only an unhandled
failable trips the check. This completes ERR PLAN E0–E5 plus the two
deferred E1 follow-ups (E1.7 + E1.8).
New regressions: 1048 (catch/or-value absorbed forms compile + run) and
1049 (bare failable in defer and onfail rejected, exit 1).
Filed issue 0065: a braced `defer { … }` / value-block body routes
through `parseExpr` (not `parseBlock` like `onfail`), so it can't parse a
destructure or `catch`-statement inside. Orthogonal to E1.7 — the spec'd
cleanup absorbers (`catch` / `or value`) parse fine in a `defer` body.
Gates: zig build, zig build test, run_examples.sh -> 340 passed, 0 failed.
This commit is contained in:
@@ -788,11 +788,52 @@ pub const Lowering = struct {
|
||||
/// a bare failable call has nowhere to send its error. Reject any failable
|
||||
/// expression-statement that isn't absorbed locally by `catch` / `or value`
|
||||
/// / a destructure binding. (Parser already bans `try`/`raise`/`return`/
|
||||
/// `break`/`continue` here.) Stub until slice B.
|
||||
/// `break`/`continue` here, so the only escape route left for a failable is
|
||||
/// local absorption.) The check is transitive through nested blocks, `if`,
|
||||
/// loops, match arms, and `catch` handlers, but stops at a nested closure
|
||||
/// (its own function boundary).
|
||||
fn checkCleanupBody(self: *Lowering, body: *const Node, kind: []const u8) void {
|
||||
_ = self;
|
||||
_ = body;
|
||||
_ = kind;
|
||||
self.checkCleanupNode(body, kind);
|
||||
}
|
||||
|
||||
fn checkCleanupNode(self: *Lowering, node: *const Node, kind: []const u8) void {
|
||||
switch (node.data) {
|
||||
.block => |b| for (b.stmts) |s| self.checkCleanupNode(s, kind),
|
||||
.if_expr => |ie| {
|
||||
self.cleanupReject(ie.condition, kind);
|
||||
self.checkCleanupNode(ie.then_branch, kind);
|
||||
if (ie.else_branch) |eb| self.checkCleanupNode(eb, kind);
|
||||
},
|
||||
.while_expr => |we| {
|
||||
self.cleanupReject(we.condition, kind);
|
||||
self.checkCleanupNode(we.body, kind);
|
||||
},
|
||||
.for_expr => |fe| self.checkCleanupNode(fe.body, kind),
|
||||
.match_expr => |me| for (me.arms) |arm| self.checkCleanupNode(arm.body, kind),
|
||||
.push_stmt => |ps| self.checkCleanupNode(ps.body, kind),
|
||||
// A destructure binds the error slot → absorbed (explicit ownership).
|
||||
.destructure_decl => {},
|
||||
.var_decl => |vd| if (vd.value) |v| self.cleanupReject(v, kind),
|
||||
.const_decl => |cd| self.cleanupReject(cd.value, kind),
|
||||
.assignment => |a| self.cleanupReject(a.value, kind),
|
||||
// Closures are their own boundary; the parser-banned control-flow
|
||||
// exits are handled elsewhere; nested cleanup is independent.
|
||||
.lambda, .return_stmt, .raise_stmt, .break_expr, .continue_expr, .defer_stmt, .onfail_stmt => {},
|
||||
else => self.cleanupReject(node, kind),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reject `expr` if it is a bare (un-absorbed) failable in cleanup position.
|
||||
/// `catch` / `or value` strip the error channel (so `exprIsFailable` is
|
||||
/// false for them); only a still-failable expression has an unhandled error.
|
||||
fn cleanupReject(self: *Lowering, expr: *const Node, kind: []const u8) void {
|
||||
if (expr.data == .catch_expr) {
|
||||
// The operand is absorbed; the handler body still runs in cleanup.
|
||||
self.checkCleanupNode(expr.data.catch_expr.body, kind);
|
||||
return;
|
||||
}
|
||||
if (!self.exprIsFailable(expr)) return;
|
||||
if (self.diagnostics) |d| d.addFmt(.err, expr.span, "a bare failable call in a `{s}` body has nowhere to send its error — the block is already exiting; absorb it locally with `catch` or `or <value>`", .{kind});
|
||||
}
|
||||
|
||||
/// On Android, the OS loads the .so via a Java-side Activity declared
|
||||
|
||||
Reference in New Issue
Block a user