cstage+selfhost+test: enforce single-slot @retscr both stages (#14)
wwstage's $64 frame was 24B below required — the second struct-return's @retscr write at -88(BP) landed below SP. Silent miscompile masked by bootstrap-window luck. The fix retires the stomp by enforcing single-slot @retscr at emit-time. cstage was per-site-fresh (wasteful but safe, frame $96); aligned UP to single-slot for ABI consistency with wwstage's @-prefix convention, not for correctness. Both stages now produce $64 frame; second return reuses the first's -64..-48(BP) slot. Generalizes #38's c.tagscrsz SSoT pattern to c.retscroff (wwstage) and cg_retscr (cstage). Returns are terminal — only one fires per call, so the two slots' lifetimes never overlap; single-slot is structurally correct. wwstage's emit-side dedup was incomplete post-#27 (cgblock save/restore unwinds the @-prefix stub); the @retscr fast path in localadd bypasses the c.locals walk. Test 718: 4 rows × {cstage runtime, wwstage runtime, byte-id, stomp sentinel}. Stomp sentinel scans .s for any -N(BP) where N>64 and fails the row if found — catches below-SP writes that bootstrap byte-id would miss in a lucky window. Row 2 (3-return) byte-id disabled per task #15 (pre-existing label-counter skew, unrelated to #14). Polarity catalog this session: - #9 wwstage OVER (tagged-return slot) - #11 wwstage UNDER (struct-by-value param decompose) - #14 wwstage UNDER (struct multi-return @retscr — silent stomp)
This commit is contained in:
@@ -422,6 +422,15 @@ type cgen = struct {
|
||||
// big enough for every later user. Single source of truth — pins
|
||||
// rob's "scan + emit lockstep" invariant. Reset per cgfn.
|
||||
tagscrsz: i32,
|
||||
// Live @retscr offset (#14). c.locals-based `@`-prefix dedup in
|
||||
// localadd is unwound by cgblock save/restore (post-#27), so a
|
||||
// second `return` in a sibling/outer block reallocates a fresh
|
||||
// slot — emit grew the frame past what scanlocals reserved, and
|
||||
// the stomp landed below SP. retscroff is the persistent SSoT:
|
||||
// 0 means "not yet allocated"; first emit-site sets it, every
|
||||
// later emit reuses. Mirrors c.tagscrsz pattern (#38) but tracks
|
||||
// offset, not size (per-fn return type is fixed, so size is too).
|
||||
retscroff: i32,
|
||||
};
|
||||
|
||||
// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar.
|
||||
@@ -444,6 +453,7 @@ fn cgeninit(c: *cgen, a: *arena) void = {
|
||||
c.labelseq = 0;
|
||||
c.varargseq = 0;
|
||||
c.tagscrsz = 0;
|
||||
c.retscroff = 0;
|
||||
// Note: strlit_seq, strlits, ffis are *not* reset here; they
|
||||
// persist across cgfn calls within one file. cgfile resets them
|
||||
// at the start of each compilation unit.
|
||||
@@ -510,8 +520,23 @@ fn localadd(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
|
||||
// reservation and emit-time allocation stay in sync. The
|
||||
// `@`-prefix carve-out preserves that contract; user names
|
||||
// can never start with `@` (lexer-rejected).
|
||||
//
|
||||
// @retscr (#14) routes through c.retscroff instead of c.locals.
|
||||
// The c.locals-based dedup is unwound by cgblock save/restore
|
||||
// (post-#27): a return inside an `if` block adds @retscr to
|
||||
// c.locals; on block exit, c.locals reverts and a sibling/outer
|
||||
// return reallocates a fresh slot. Scan had reserved one slot;
|
||||
// emit grew the frame past the reservation and the second
|
||||
// site's writes landed below SP. c.retscroff is per-fn state
|
||||
// that survives cgblock save/restore and pins single-slot.
|
||||
if (name.len > 0) {
|
||||
if (name[0] == 64u8) { // '@'
|
||||
if (streq(name, "@retscr")) {
|
||||
if (c.retscroff != 0) { return c.retscroff; };
|
||||
let off: i32 = localalloc(c, name, sz, tnode);
|
||||
c.retscroff = off;
|
||||
return off;
|
||||
};
|
||||
let cur: *local = c.locals;
|
||||
for (cur != nil) {
|
||||
let cn: str = cur.name;
|
||||
|
||||
Reference in New Issue
Block a user