cstage+selfhost+test: scope-correct localoff via block save/restore (#27)
localoff (cstage) / localadd (wwstage) deduped stack slots by name alone, ignoring scope. Outer `let a: [128]u8` and an inner-block `let a: *u8` shared one 8B slot; prologue truncated to inner size and outer-scope writes past saved RIP corrupted the frame. Worker-19 hit it during #19 (selfhost/cmd/w6a/main.ww carries a defensive asm→s rename pointing at this task). Drop the name-dedup. Each let allocates fresh. Then preserve outer-scope visibility across inner blocks: cgstmt's N_BLOCK case saves `*locals` head, walks body, restores. cgfn iterates fn->body ->list directly (bypassing the outermost N_BLOCK) so defers and the implicit-return epilogue still see fn-body locals after the loop. Wwstage symmetric: localadd keeps dedup only for `@`-prefixed synthetic scratches (`@tagscr` / `@retscr` / `@tagbase`) which need single-slot semantics; user names get fresh stubs. scanlocals always counts + always appends a fresh stub for N_LET / N_MLET / N_FORRANGE so prologue SUBQ stays in sync with emit-time offsets. cgblock and cgfn mirror cstage. ww2 == ww3 == ww4 byte-identical post-fix. Test 709 (localoff_scope): 8 rows × 2 drivers = 16 fixtures — inner_first_outer_bigger, outer_first_inner_writes, nested_3_deep, same_name_diff_type, same_block_redecl_pin, defer_shadow, forrange_body_shadow, if_body_shadow. defer_shadow pins the cgfn body-bypass; if_body_shadow pins the save/restore independently. Asm byte-id not diffed in 709 — 995_self_rebuild covers cross-stage drift more broadly. Follow-ups (filed): #32 (check: refuse same-block let-redecl), w6a `s`→`asm` revert sibling commit.
This commit is contained in:
@@ -74,11 +74,24 @@ fn cgyield(c: *cgen, n: *node) void = {
|
||||
};
|
||||
|
||||
fn cgblock(c: *cgen, n: *node) void = {
|
||||
// Save/restore the locals head across the block (post-#27).
|
||||
// Inner-scope `let` bindings prepend to c.locals via localadd;
|
||||
// without this restore, the prepended stubs leak into sibling
|
||||
// and ancestor scopes, and localfind (head-first) returns the
|
||||
// inner binding's offset for an identifier that semantically
|
||||
// belongs to the outer scope. The frame is left grown — we
|
||||
// don't reclaim popped slots, matching cstage's lowering.
|
||||
//
|
||||
// cgfn iterates fn_.body.list directly to bypass this save/
|
||||
// restore at the function's outermost block — defers (and the
|
||||
// implicit-return epilogue) need locals intact.
|
||||
let saved: *local = c.locals;
|
||||
let s: *node = n.list;
|
||||
for (s != nil) {
|
||||
cgstmt(c, s);
|
||||
s = s.next;
|
||||
};
|
||||
c.locals = saved;
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user