cstage+selfhost+test: zero unused ABI words in cgreturn variant-widen (#18)

cgreturn's variant-widen arm only filled the registers each variant's
payload needed: scalar variants left CX and R8 stale; str variant
left R8 stale. The receiver (cg_widen_tagged_store non-N_IDENT
branch) writes all four ABI words to the dst slot unconditionally,
so caller-side residue in CX/R8 (the array-index IMULQ being the
canonical primer) landed at slot+16 and slot+24.

Worker-fmtparser surfaced this through fprintf's loop body where
array indexing primed CX and a 24B-return helper failed to clear
it; bug isn't loop-specific — straight-line repro at /tmp/wcrs_repro/
repro8.ww confirms.

Patch: emit `MOVQ $0, CX` after the variant's register shuffle when
the slot exceeds 16B and the variant doesn't fill CX; same for R8
when the slot exceeds 24B. Symmetric across cstage cgen.c and
wwstage cgenstmt.ww. Order: zero-MOVQs precede `MOVQ $tag, AX` so
AX-as-staging stays safe. Inline comment at cg_widen_tagged_store
non-N_IDENT branch documents the producer-zero contract.

Test 707 (cgreturn_variant_zero): 6 rows × both stages = 12 fixtures.
Covers scalar/bool/str returns after array-index priming in
straight-line / single-loop body / nested-loop body / mixed-variant
loop. Asm byte-identity check intentionally omitted; wwstage
taggedvariantindex divergence on str/bool N_IDENT is filed as task
#20. 995_self_rebuild covers the broader cross-stage drift surface.

ww2 == ww3 == ww4 byte-identical post-fix. 67/67 green.

Deferred (followups filed): #20 wwstage taggedvariantindex,
#21 [N]str/[N]bool array-literal non-pointer-half writes, #22
consolidate variant-widen into uniform scratch-slot path.
This commit is contained in:
2026-05-16 01:37:15 +09:00
parent 6b6d7dd283
commit 166431a2da
6 changed files with 383 additions and 1 deletions

View File

@@ -1082,6 +1082,12 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
amem(D_BP, write_off + k));
}
} else {
/* Tagged source returned via the tagged-return ABI
* (AX=tag, DX=word0, CX=word1, R8=word2). The unused
* ABI words are zeroed by the producer (#18 cgreturn
* variant-widen) so the unconditional store here is
* safe even when the source variant has fewer payload
* words than the dst slot. */
cgexpr(c, src, *locals_p);
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + 0));
@@ -6065,8 +6071,15 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
* Tagged-return ABI: AX=tag, DX=word0,
* CX=word1, R8=word2. Slice payload uses
* all four; str uses three; scalar uses
* two. */
* two. Unused ABI words must still be
* zeroed because the receiver
* (cg_widen_tagged_store call-source arm)
* writes AX/DX/CX/R8 unconditionally sized
* by the dst slot; stale CX/R8 from the
* caller (e.g. a slice-stride IMULQ) would
* land in slot+16 / slot+24. (Task #18.) */
int tag = cg_tag_for_variant(rt, vt);
int rsz = (int)rt->size;
cgexpr(c, n->lhs, *locals);
if (type_isslice(vt)) {
/* cgexpr leaves (AX=ptr, BX=len,
@@ -6083,9 +6096,23 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
areg(D_CX));
ins2(c, A_MOVQ, areg(D_AX),
areg(D_DX));
/* str fills DX,CX. Zero R8 if dst
* slot covers slot+24. */
if (rsz > 24)
ins2(c, A_MOVQ, aimm(0),
areg(D_R8));
} else {
ins2(c, A_MOVQ, areg(D_AX),
areg(D_DX));
/* scalar fills DX only. Zero
* CX / R8 if dst slot covers
* slot+16 / slot+24. */
if (rsz > 16)
ins2(c, A_MOVQ, aimm(0),
areg(D_CX));
if (rsz > 24)
ins2(c, A_MOVQ, aimm(0),
areg(D_R8));
}
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
areg(D_AX));