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:
@@ -484,29 +484,37 @@ fn localaddstack(c: *cgen, name: str, tnode: *node, off: i32) void = {
|
||||
};
|
||||
|
||||
fn localadd(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
|
||||
// Name-based slot reuse for N_LETs and params: if `name` is
|
||||
// already declared in this function, return its existing
|
||||
// offset. Mirrors C cgen (cmd/w6c/cgen.c:localoff). Two
|
||||
// disjoint scopes that declare the same name share one slot —
|
||||
// so `escape` in wwdump (three `let cp: pos;` across separate
|
||||
// branches) reserves one slot, not three. scanlocals does
|
||||
// the matching dedup at prologue time so the SUBQ stays in
|
||||
// sync.
|
||||
// User-let path (post-#27): always allocate a fresh slot per
|
||||
// binding. Pre-fix this deduped by name to share one slot
|
||||
// across same-name lets in disjoint scopes — inherited from
|
||||
// C cgen's localoff. Both stages had the same silent-stack-
|
||||
// corruption bug: an inner 8B `let a: i64` allocated first
|
||||
// would force a later outer `let a: [128]u8` onto the 8B slot,
|
||||
// and `a[127]` would write at +119(BP), past the saved RIP.
|
||||
// Localfind walks head-first, so the most-recent binding still
|
||||
// wins lookups inside its scope. Tnode is carried on the
|
||||
// freshly-pushed entry, so type dispatch in cgenutil never
|
||||
// sees a stale predecessor.
|
||||
//
|
||||
// On a dedup hit we also overwrite the stored tnode to match
|
||||
// the new declaration's type. C reads `n->lhs->type` (filled
|
||||
// by the checker) at every nkind.N_DOT/nkind.N_CAST site; we read
|
||||
// `lc.tnode`, so it must follow source order. Without this,
|
||||
// a later `let m: *node` inside a branch keeps an earlier
|
||||
// `let m: i32`'s tnode and `m.next` falls into the SB fallback.
|
||||
let cur: *local = c.locals;
|
||||
for (cur != nil) {
|
||||
let cn: str = cur.name;
|
||||
if (streq(cn, name)) {
|
||||
cur.tnode = tnode;
|
||||
return cur.off;
|
||||
// Synthetic scratch slots (`@tagscr`, `@retscr`, `@tagbase`)
|
||||
// keep the per-fn dedup. Each scratch is sized identically
|
||||
// across its call sites and intended to be shared — the
|
||||
// scanlocals pre-pass also dedups via scanseenmark, so frame
|
||||
// reservation and emit-time allocation stay in sync. The
|
||||
// `@`-prefix carve-out preserves that contract; user names
|
||||
// can never start with `@` (lexer-rejected).
|
||||
if (name.len > 0) {
|
||||
if (name[0] == 64u8) { // '@'
|
||||
let cur: *local = c.locals;
|
||||
for (cur != nil) {
|
||||
let cn: str = cur.name;
|
||||
if (streq(cn, name)) {
|
||||
cur.tnode = tnode;
|
||||
return cur.off;
|
||||
};
|
||||
cur = cur.lnext;
|
||||
};
|
||||
};
|
||||
cur = cur.lnext;
|
||||
};
|
||||
return localalloc(c, name, sz, tnode);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user