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:
agra
2026-06-01 23:24:15 +03:00
parent 296c809d85
commit c3bc6acd42
10 changed files with 198 additions and 4 deletions

View File

@@ -0,0 +1,28 @@
// Failable calls in cleanup bodies must be absorbed locally (ERR step E1.7). A
// `defer` / `onfail` body runs while the block is already exiting, so a failable
// it calls has nowhere to propagate — it must be handled in place with `catch`
// or an `or <value>` terminator. This file shows the accepted forms; the bare
// (un-absorbed) form is rejected in 1049.
#import "modules/std.sx";
E :: error { Bad }
failing :: () -> !E { raise error.Bad; }
recover :: () -> (s32, !E) { raise error.Bad; }
work :: (n: s32) -> !E {
defer print("defer: always\n"); // plain cleanup
onfail { failing() catch e print("onfail: caught (catch)\n"); } // catch absorbs
onfail { x := recover() or 7; print("onfail: x={} (or)\n", x); } // or-value absorbs
if n < 0 { raise error.Bad; }
return;
}
main :: () -> s32 {
print("[error]\n");
a := work(-1); // raises → onfail bodies fire, then defer (reverse decl order)
print("[ok]\n");
b := work(2); // success → only defer fires
return 0;
}

View File

@@ -0,0 +1,23 @@
// Rejection counterpart to 1048 (ERR step E1.7). A bare (un-absorbed) failable
// call in a `defer` / `onfail` body is a compile error — the block is already
// exiting, so the error has nowhere to propagate. It must be absorbed locally
// with `catch` or `or <value>`. Both a `defer` and an `onfail` bare call are
// flagged; the program never runs (exit 1).
#import "modules/std.sx";
E :: error { Bad }
failing :: () -> !E { raise error.Bad; }
work :: (n: s32) -> !E {
defer failing(); // REJECTED: bare failable in a defer body
onfail { failing(); } // REJECTED: bare failable in an onfail body
if n < 0 { raise error.Bad; }
return;
}
main :: () -> s32 {
a := work(-1);
return 0;
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,6 @@
[error]
onfail: x=7 (or)
onfail: caught (catch)
defer: always
[ok]
defer: always

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,11 @@
error: a bare failable call in a `defer` body has nowhere to send its error — the block is already exiting; absorb it locally with `catch` or `or <value>`
--> /Users/agra/projects/sx/examples/1049-errors-cleanup-absorption-reject.sx:14:12
|
14 | defer failing(); // REJECTED: bare failable in a defer body
| ^^^^^^^^^
error: a bare failable call in a `onfail` body has nowhere to send its error — the block is already exiting; absorb it locally with `catch` or `or <value>`
--> /Users/agra/projects/sx/examples/1049-errors-cleanup-absorption-reject.sx:15:14
|
15 | onfail { failing(); } // REJECTED: bare failable in an onfail body
| ^^^^^^^^^