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:
@@ -30,24 +30,23 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
|
||||
// scanlocals must agree with localadd or the prologue
|
||||
// SUBQ undersizes the frame and lets overflow into the
|
||||
// caller's stack — corrupting whatever's at -frameSize..-1
|
||||
// of the caller. Same-name re-declarations share the first
|
||||
// slot (see scanseenmark / localadd).
|
||||
if (!scanseenmark(c, n.str)) {
|
||||
let sz: i32 = letslotsize(c, n);
|
||||
if (sz < 8) { sz = 8; };
|
||||
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
|
||||
total += sz;
|
||||
};
|
||||
// Carry the let's tnode into the stub so scanlocals can
|
||||
// dispatch on type later in the walk — e.g. detecting
|
||||
// `arr[i] = ...` where arr is a tagged-element array,
|
||||
// which needs an @tagscr scratch slot reservation.
|
||||
let stub: *local = localfindnode(c, n.str);
|
||||
if (stub != nil) {
|
||||
if (stub.tnode == nil) {
|
||||
if (n.lhs != nil) { stub.tnode = n.lhs; };
|
||||
};
|
||||
};
|
||||
// of the caller. Post-#27 every let allocates fresh (no
|
||||
// name dedup), so we always count + always append a stub.
|
||||
// The stub carries n.lhs as tnode so later scanlocals
|
||||
// nodes can dispatch on type — e.g. detecting `arr[i] = ...`
|
||||
// where arr is a tagged-element array (needs @tagscr).
|
||||
// localfindnode walks head-first, so the freshest stub
|
||||
// (innermost binding) wins lookup.
|
||||
let sz: i32 = letslotsize(c, n);
|
||||
if (sz < 8) { sz = 8; };
|
||||
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
|
||||
total += sz;
|
||||
let stub: *local = amalloc(c.a, 48u64): *local;
|
||||
stub.name = n.str;
|
||||
stub.off = 0;
|
||||
stub.tnode = n.lhs;
|
||||
stub.lnext = c.locals;
|
||||
c.locals = stub;
|
||||
};
|
||||
// Multi-let from a tuple-returning call: each binding's size
|
||||
// comes from its annotated type (l.lhs) when present, else from
|
||||
@@ -81,18 +80,25 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
|
||||
let pt: *node = p0t;
|
||||
let bidx: i32 = 0;
|
||||
for (l != nil) {
|
||||
if (!scanseenmark(c, l.str)) {
|
||||
let t: *node = l.lhs;
|
||||
if (t == nil) {
|
||||
if (bidx == 0) { t = p0t; };
|
||||
if (bidx == 1) { t = p1t; };
|
||||
};
|
||||
let sz: i32 = 8;
|
||||
if (t != nil) { sz = slotsize(c, t); };
|
||||
if (sz < 8) { sz = 8; };
|
||||
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
|
||||
total += sz;
|
||||
let t: *node = l.lhs;
|
||||
if (t == nil) {
|
||||
if (bidx == 0) { t = p0t; };
|
||||
if (bidx == 1) { t = p1t; };
|
||||
};
|
||||
let sz: i32 = 8;
|
||||
if (t != nil) { sz = slotsize(c, t); };
|
||||
if (sz < 8) { sz = 8; };
|
||||
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
|
||||
total += sz;
|
||||
// Always-fresh stub (post-#27); tnode carries the
|
||||
// binding's type so later array-index dispatch can
|
||||
// resolve the let through localfindnode.
|
||||
let stub: *local = amalloc(c.a, 48u64): *local;
|
||||
stub.name = l.str;
|
||||
stub.off = 0;
|
||||
stub.tnode = t;
|
||||
stub.lnext = c.locals;
|
||||
c.locals = stub;
|
||||
l = l.next;
|
||||
bidx += 1;
|
||||
};
|
||||
@@ -109,24 +115,35 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
|
||||
// those, so the simple count tracks C cgen for current fixtures.
|
||||
if (n.kind == nkind.N_FORRANGE) {
|
||||
total += 16; // .rgi + .rgl scratch
|
||||
// Each forrange binding gets a fresh 8B slot (post-#27).
|
||||
// Stub is also appended so the body's references resolve
|
||||
// to this binding via head-first localfindnode lookup.
|
||||
if (n.list != nil) {
|
||||
let m: *node = n.list;
|
||||
for (m != nil) {
|
||||
let bnm: str = m.str;
|
||||
if (bnm.len == 0) {
|
||||
total += 8; // discard binding still gets a slot
|
||||
} else { if (!scanseenmark(c, bnm)) {
|
||||
total += 8;
|
||||
};};
|
||||
total += 8;
|
||||
if (bnm.len > 0) {
|
||||
let stub: *local = amalloc(c.a, 48u64): *local;
|
||||
stub.name = bnm;
|
||||
stub.off = 0;
|
||||
stub.tnode = m.lhs;
|
||||
stub.lnext = c.locals;
|
||||
c.locals = stub;
|
||||
};
|
||||
m = m.next;
|
||||
};
|
||||
} else {
|
||||
let bnm: str = n.str;
|
||||
if (bnm.len == 0) {
|
||||
total += 8;
|
||||
} else { if (!scanseenmark(c, bnm)) {
|
||||
total += 8;
|
||||
};};
|
||||
total += 8;
|
||||
if (bnm.len > 0) {
|
||||
let stub: *local = amalloc(c.a, 48u64): *local;
|
||||
stub.name = bnm;
|
||||
stub.off = 0;
|
||||
stub.tnode = nil;
|
||||
stub.lnext = c.locals;
|
||||
c.locals = stub;
|
||||
};
|
||||
};
|
||||
};
|
||||
// `match (non-ident)` needs a 24B `@match_spill` scratch slot for
|
||||
@@ -804,7 +821,24 @@ fn cgfn(c: *cgen, fn_: *node) void = {
|
||||
|
||||
cgfnparams(c, fn_.list);
|
||||
c.lastwasreturn = 0;
|
||||
if (fn_.body != nil) { cgstmt(c, fn_.body); };
|
||||
// Iterate the fn body's statements directly rather than dispatching
|
||||
// the outermost N_BLOCK through cgstmt — cgblock now save/restores
|
||||
// c.locals to scope inner shadows (post-#27), but the function body
|
||||
// is not "an inner block": defers (queued during the body) and the
|
||||
// implicit-return epilogue both call cgexpr after this loop and
|
||||
// resolve identifiers via localfind, so the body's locals must
|
||||
// still be in c.locals when we get there.
|
||||
if (fn_.body != nil) {
|
||||
if (fn_.body.kind == nkind.N_BLOCK) {
|
||||
let s: *node = fn_.body.list;
|
||||
for (s != nil) {
|
||||
cgstmt(c, s);
|
||||
s = s.next;
|
||||
};
|
||||
} else {
|
||||
cgstmt(c, fn_.body);
|
||||
};
|
||||
};
|
||||
|
||||
if (c.lastwasreturn == 0) {
|
||||
// Run any registered defers in LIFO order before the
|
||||
|
||||
Reference in New Issue
Block a user