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:
2026-05-17 01:40:49 +09:00
parent 69a817f0f3
commit b401cced05
6 changed files with 565 additions and 7 deletions

View File

@@ -32,6 +32,13 @@ static Type *cg_ret_type;
* needs this to allocate scratch slots (e.g. match bindings) without
* threading it through every signature. */
static int *cg_frame;
/* Per-fn @retscr offset (single-slot SSoT, task #14). Returns are
* terminal: at most one return path fires per call, so all retscr
* uses share one slot. Mirrors wwstage's `@retscr` convention
* (cgen.ww localadd '@'-prefix dedup; #38 ratified single-slot
* semantics for synthetic scratches). 0 means "not yet allocated";
* negative offsets returned by local_alloc are the live value. */
static int cg_retscr;
/* Per-fn defer stack: pushed in registration order, popped (emitted)
* in reverse at each return. */
@@ -6322,11 +6329,23 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
* stores; struct ident: word copy;
* tagged subset: copy + tag remap.
* 4th word in R8 covers slice payload
* variants (slot >= 32B). */
* variants (slot >= 32B).
*
* Single-slot @retscr (#14): returns are
* terminal, so all retscr uses in this fn
* share one slot. Pre-fix per-site fresh
* allocation over-grew the frame by sz
* bytes per extra return. */
int sz = (int)rt->size;
const char *scrn = mklabel(c, "retscr");
int scr = local_alloc(c, locals, scrn,
sz, cg_frame);
int scr;
if (cg_retscr != 0) {
scr = cg_retscr;
} else {
const char *scrn = mklabel(c, "retscr");
scr = local_alloc(c, locals, scrn,
sz, cg_frame);
cg_retscr = scr;
}
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 0; k < sz; k += 8)
ins2(c, A_MOVQ, areg(D_AX),
@@ -6372,9 +6391,17 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
&& (n->lhs->kind == N_IDENT
|| n->lhs->kind == N_STRUCTLIT)) {
int sz = (int)rt->size;
const char *scrn = mklabel(c, "retscr");
int scr = local_alloc(c, locals, scrn, 24,
cg_frame);
/* Single-slot @retscr (#14): see tagged arm
* above for rationale. */
int scr;
if (cg_retscr != 0) {
scr = cg_retscr;
} else {
const char *scrn = mklabel(c, "retscr");
scr = local_alloc(c, locals, scrn, 24,
cg_frame);
cg_retscr = scr;
}
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, scr + 0));
@@ -6782,6 +6809,7 @@ cgfn(Cg *c, FILE *out, Node *fn)
ndefers = 0;
nloops = 0;
cg_ret_type = fn->type ? fn->type->ret : NULL;
cg_retscr = 0;
int frame = 0;
Local *locals = NULL;