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:
@@ -880,11 +880,18 @@ struct Local {
|
||||
Local *next;
|
||||
};
|
||||
|
||||
/* localoff — push a fresh stack slot for this binding and return its
|
||||
* BP offset. Never dedups by name (post-#27): two `let a: T` in disjoint
|
||||
* scopes within one fn must each get their own slot, sized to their own
|
||||
* declared T. Pre-fix the dedup loop returned the first-allocated slot
|
||||
* regardless of the new declaration's size, so an outer `let a: [128]u8`
|
||||
* after an inner `let a: i64` would collapse onto the 8B slot and
|
||||
* `a[127]` would land at +119(BP), past the saved RIP, into the
|
||||
* caller's frame. localfind walks from the head, so the most recent
|
||||
* binding still wins lookups inside its scope. */
|
||||
static int
|
||||
localoff(Cg *c, Local **head, const char *name, int size, int *frame)
|
||||
{
|
||||
for (Local *l = *head; l; l = l->next)
|
||||
if (strcmp(l->name, name) == 0) return l->off;
|
||||
int al = 8;
|
||||
*frame = (*frame + size + al - 1) & ~(al - 1);
|
||||
int off = -*frame;
|
||||
@@ -896,12 +903,11 @@ localoff(Cg *c, Local **head, const char *name, int size, int *frame)
|
||||
return off;
|
||||
}
|
||||
|
||||
/* local_alloc — always push a fresh slot, never dedup by name. Used for
|
||||
* match-arm bindings, where `case let e: T` must shadow any outer `e`
|
||||
* with a slot sized to T — localoff's dedup would reuse the outer's
|
||||
* (possibly smaller) slot and let multi-word writes overflow into the
|
||||
* saved BP / return address. localfind walks from the head, so the
|
||||
* fresh entry still wins inside the arm body. */
|
||||
/* local_alloc — synonym for localoff. Pre-#27 localoff deduped by name
|
||||
* and local_alloc was the always-fresh escape hatch (match-arm bindings,
|
||||
* synthetic scratch slots). Post-#27 localoff is also always-fresh, so
|
||||
* the two are functionally identical; both names are kept so the call
|
||||
* sites read intentfully (let-decl vs scratch). */
|
||||
static int
|
||||
local_alloc(Cg *c, Local **head, const char *name, int size, int *frame)
|
||||
{
|
||||
@@ -5723,10 +5729,27 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
||||
{
|
||||
if (n == NULL) return;
|
||||
switch (n->kind) {
|
||||
case N_BLOCK:
|
||||
case N_BLOCK: {
|
||||
/* Save/restore the locals head across the block (post-#27).
|
||||
* Inner-scope `let` bindings prepend to *locals via localoff;
|
||||
* 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 — slot
|
||||
* lifetimes don't overlap with later siblings observably (the
|
||||
* popped stubs' offsets are no longer reachable by name), but
|
||||
* we don't reclaim the frame bytes; that's the conservative
|
||||
* choice C compilers make for simple 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. */
|
||||
Local *saved = *locals;
|
||||
for (Node *s = n->list; s; s = s->next)
|
||||
cgstmt(c, s, locals, frame);
|
||||
*locals = saved;
|
||||
break;
|
||||
}
|
||||
case N_EXPRSTMT:
|
||||
cgexpr(c, n->lhs, *locals);
|
||||
break;
|
||||
@@ -6700,7 +6723,19 @@ cgfn(Cg *c, FILE *out, Node *fn)
|
||||
if (tp) tp = tp->next;
|
||||
}
|
||||
|
||||
cgstmt(c, fn->body, &locals, &frame);
|
||||
/* Iterate the fn body's statements directly rather than dispatching
|
||||
* the outermost N_BLOCK through cgstmt — N_BLOCK now save/restores
|
||||
* the locals head to scope inner shadows, 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 *locals when we get there. */
|
||||
if (fn->body && fn->body->kind == N_BLOCK) {
|
||||
for (Node *s = fn->body->list; s; s = s->next)
|
||||
cgstmt(c, s, &locals, &frame);
|
||||
} else {
|
||||
cgstmt(c, fn->body, &locals, &frame);
|
||||
}
|
||||
|
||||
/* implicit return for void functions */
|
||||
if (c->tail->as != A_RET) {
|
||||
|
||||
Reference in New Issue
Block a user