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.
5.0 KiB
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
#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 1andc2 1are 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:
deferis skipped onbreak/continueexits.
- Record the loop's defer base: in
lowerFor/lowerRuntimeRangeFor/lowerWhile(src/ir/lower/control_flow.zig), alongside the existing save/restore ofbreak_target/continue_target, saveself.defer_stack.items.leninto a newLoweringfield (e.g.loop_defer_base: usize), restoring the old value after the body.- In
lowerBreak/lowerContinue, before thebr, emit pending non-onfail cleanups fromdefer_stack.items.lendown toloop_defer_basein LIFO order without truncating the stack (mirroremitErrorCleanup's non-truncating walk insrc/ir/lower/stmt.zig, success-exit filtering likeemitBlockDefers). Truncation must stay with the lexicallowerBlockscope exits — the same defer entries still belong to the fall-through lowering path after theif { break; }arm.inline for(lowerInlineRangeFor) bodies lower throughlowerBlockper unrolled iteration; check abreakinside one targets the enclosing runtime loop with the same drain (and thatbreakwith no enclosing loop gets a diagnostic rather than the current silent no-opRef.none).Verify: run the repro in
issues/0108-defer-skipped-on-break-continue.sx, expectcleanup 1afterbody 0/cleanup 0, andc2 1betweenc2 0andb2 2. Add awhile-loop break/continue + defer case. Then promote toexamples/00xx-basic-defer-break-continue.sxper the resolution flow, and runzig build && zig build test && bash tests/run_examples.sh(all ok).