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,81 @@
# 0065 — block-expression body does not parse a destructure decl (`v, e := f();`)
## Symptom
A destructure declaration (`v, e := f();`) inside a **block used in
expression position** fails to parse with `expected ';'`. Two surfaced
forms:
- `defer { v, e := f(); ... }` — a `defer` body is parsed via `parseExpr`
(so its `{ ... }` is a block-EXPRESSION), and the block-expression
statement loop doesn't recognize the `name, name :=` destructure form.
- `y := { v, e := f(); v };` — a value-producing block bound to a name.
Observed: `error: expected ';'` pointing at the statement *after* the
destructure (the parser bails at the `:=` and resyncs). Expected: the
destructure parses exactly as it does in a normal statement block (an
`if` body, a plain `{ }` statement block, or an `onfail { }` body — all of
which use `parseBlock` and handle it fine).
This is the same family as the pre-existing "value-producing block body
in binding position doesn't parse" note in `current/CHECKPOINT-ERR.md`
(E2.4b log). `onfail { }` is unaffected because it parses its body with
`parseBlock` (src/parser.zig ~2063); `defer` is affected because it uses
`parseExpr` (~2029).
## Reproduction
```sx
#import "modules/std.sx";
E :: error { Bad }
val :: () -> (s32, !E) { return 5; }
f :: () -> !E {
defer {
v, e := val(); // ← error: expected ';'
print("v={}\n", v);
}
return;
}
main :: () -> s32 { return 0; }
```
Also reproduces with no `defer`, as a plain value block:
```sx
y := {
v, e := val(); // ← error: expected ';'
v
};
```
## Investigation prompt
The block-expression statement loop (the parser path reached from
`parseExpr` when it hits `{` — see `src/parser.zig`, the block-as-value
parsing around the `parsePrimary`/`parseBlockExpr` path, distinct from
`parseBlock` at ~1931) parses each inner statement but does not run the
destructure-decl detection that `parseStmt` does. Find where
`parseStmt`/`parseBlock` recognizes the `ident (, ident)+ :=` lookahead
and make the block-expression statement loop use the same statement
parser (ideally route block-expression bodies through `parseStmt` so
every statement form — destructure, var/const decl, etc. — is handled
uniformly).
For `defer` specifically: the simplest aligned fix is to parse a
braced `defer` body with `parseBlock` (like `onfail` does) while keeping
the bare-expression form (`defer expr;`) on `parseExpr`. That removes the
defer-body manifestation even if the general block-expression path is
handled separately.
Verification: run the repro above — expect it to compile and run
(`exit 0`), with the destructure-bound value usable under an `if !e { … }`
guard (ERR E1.8). Add a regression example under `examples/` once fixed.
## Status
OPEN. Orthogonal to ERR E1.7/E1.8 — the spec'd cleanup-body absorbers are
`catch` / `or <value>` (both parse fine in a `defer` body), so this does
not block the error-handling work. Filed while implementing E1.7.