Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.
Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).
Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.
zig build test: 426/426; examples suite: 595/595.
108 lines
5.0 KiB
Markdown
108 lines
5.0 KiB
Markdown
# RESOLVED — 0108: `defer` silently skipped on `break` / `continue` loop exits
|
|
|
|
**Root cause:** `lowerBreak`/`lowerContinue` emitted a bare `br`; the enclosing
|
|
block's `emitBlockDefers` then saw the terminator and discarded the pending
|
|
entries on the assumption they were already emitted (true only for
|
|
return/raise).
|
|
|
|
**Fix:** `Lowering.loop_defer_base` records the defer-stack height at each
|
|
loop's body start (`lowerWhile` / `lowerFor` / `lowerRuntimeRangeFor`,
|
|
saved/restored like `break_target`); `lowerBreak`/`lowerContinue` drain
|
|
non-`onfail` entries down to it in LIFO order via the new, non-truncating
|
|
`emitLoopExitDefers` (`src/ir/lower/stmt.zig`) before branching — truncation
|
|
stays with the lexical block exits, since the same entries still belong to the
|
|
fall-through path. `break`/`continue` outside a loop now diagnose
|
|
(`` `break` outside a loop ``) instead of silently no-op'ing.
|
|
|
|
**Regression test:** `examples/0049-basic-defer-break-continue.sx` (`for`
|
|
break + continue, `while` break + continue, nested-block LIFO drain; the
|
|
breaking iteration's cleanups were missing pre-fix).
|
|
|
|
---
|
|
|
|
# 0108 — `defer` silently skipped on `break` / `continue` loop exits
|
|
|
|
**Symptom.** A `defer` registered inside a loop body does not run when the
|
|
iteration exits via `break` or `continue`. Observed: the cleanup for the
|
|
breaking/continuing iteration never executes. Expected (specs.md §6 Defer:
|
|
"`defer expr;` schedules `expr` to execute when the enclosing scope block
|
|
exits"): `break`/`continue` exit the loop-body scope, so all pending defers of
|
|
that iteration must fire before the jump. The normal fall-through end of an
|
|
iteration DOES run them — only the `break`/`continue` paths skip.
|
|
|
|
Resource impact: `for ... { f := open(...); defer close(f); if cond { break; } }`
|
|
leaks the handle on the break path. Same for `continue` (leaks once per
|
|
continued iteration). Affects `for` (collection, range) and `while` equally —
|
|
all share `lowerBreak`/`lowerContinue`.
|
|
|
|
## Reproduction
|
|
|
|
```sx
|
|
#import "modules/std.sx";
|
|
|
|
main :: () -> i32 {
|
|
for 0..3: (i) {
|
|
defer print("cleanup {}\n", i);
|
|
if i == 1 { break; }
|
|
print("body {}\n", i);
|
|
}
|
|
print("after break loop\n");
|
|
|
|
for 0..3: (i) {
|
|
defer print("c2 {}\n", i);
|
|
if i == 1 { continue; }
|
|
print("b2 {}\n", i);
|
|
}
|
|
print("done\n");
|
|
0
|
|
}
|
|
```
|
|
|
|
- **Observed** (current master):
|
|
`body 0 / cleanup 0 / after break loop / b2 0 / c2 0 / b2 2 / c2 2 / done`
|
|
— `cleanup 1` and `c2 1` are missing.
|
|
- **Expected**:
|
|
`body 0 / cleanup 0 / cleanup 1 / after break loop / b2 0 / c2 0 / c2 1 / b2 2 / c2 2 / done`
|
|
|
|
Repro co-located: `issues/0108-defer-skipped-on-break-continue.sx` (unpinned —
|
|
pin as the regression once fixed, with the expected output above).
|
|
|
|
## Root cause (suspected area)
|
|
|
|
`src/ir/lower/control_flow.zig` — `lowerBreak` / `lowerContinue` (~864-876)
|
|
emit a bare `self.builder.br(target)` without draining the defer stack.
|
|
Contrast `lowerReturn` (`src/ir/lower/stmt.zig` ~501), which calls
|
|
`self.emitBlockDefers(self.func_defer_base)` before `ret`. After the bare
|
|
`br`, the enclosing `lowerBlock`'s scope-exit `emitBlockDefers` sees
|
|
`currentBlockHasTerminator()` and **discards** the entries under the
|
|
assumption "cleanups were already emitted" (`stmt.zig` ~1016) — true for
|
|
return/raise, false for break/continue. So the cleanups are dropped, not
|
|
deferred-elsewhere.
|
|
|
|
## Investigation prompt (paste into a fresh session)
|
|
|
|
> Fix issue 0108: `defer` is skipped on `break`/`continue` exits.
|
|
>
|
|
> 1. Record the loop's defer base: in `lowerFor` / `lowerRuntimeRangeFor` /
|
|
> `lowerWhile` (`src/ir/lower/control_flow.zig`), alongside the existing
|
|
> save/restore of `break_target`/`continue_target`, save
|
|
> `self.defer_stack.items.len` into a new `Lowering` field (e.g.
|
|
> `loop_defer_base: usize`), restoring the old value after the body.
|
|
> 2. In `lowerBreak`/`lowerContinue`, before the `br`, emit pending non-onfail
|
|
> cleanups from `defer_stack.items.len` down to `loop_defer_base` in LIFO
|
|
> order **without truncating the stack** (mirror `emitErrorCleanup`'s
|
|
> non-truncating walk in `src/ir/lower/stmt.zig`, success-exit filtering
|
|
> like `emitBlockDefers`). Truncation must stay with the lexical
|
|
> `lowerBlock` scope exits — the same defer entries still belong to the
|
|
> fall-through lowering path after the `if { break; }` arm.
|
|
> 3. `inline for` (`lowerInlineRangeFor`) bodies lower through `lowerBlock`
|
|
> per unrolled iteration; check a `break` inside one targets the enclosing
|
|
> runtime loop with the same drain (and that `break` with no enclosing loop
|
|
> gets a diagnostic rather than the current silent no-op `Ref.none`).
|
|
>
|
|
> Verify: run the repro in `issues/0108-defer-skipped-on-break-continue.sx`,
|
|
> expect `cleanup 1` after `body 0`/`cleanup 0`, and `c2 1` between `c2 0` and
|
|
> `b2 2`. Add a `while`-loop break/continue + defer case. Then promote to
|
|
> `examples/00xx-basic-defer-break-continue.sx` per the resolution flow, and
|
|
> run `zig build && zig build test && bash tests/run_examples.sh` (all ok).
|