wcc/ww: cgcall drain pops widened GP words before the float arm

The arg-drain loop checked node_isfloat before popping a widened
arg's GP words, so a float arg adjacent to a widened (tagged) arg
read the wrong stack slot: the f64 took the widened payload (#30), or
the float arm ate the tag word into X0 and the payload landed in DI
as the tag (#48). One missing branch, two manifestations — mirror
cstage's widen-first pop (cgen.c:9650-9665). Review items #30+#48
(fold reviewer-verified one-mechanism against the cstage twin).
This commit is contained in:
2026-06-12 21:11:35 +09:00
parent f00775759d
commit a4a4cd7c16
6 changed files with 474 additions and 0 deletions

View File

@@ -106,6 +106,58 @@ fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = {
// skips the other's args; the return value counts only own-phase
// slot words. Mirrors cstage cgcall's mem pre-pass; ABI shape per
// ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit).
// argtaggedwidensz — the SysV slot width (16/24/32) a CONCRETE arg is
// widened to when passed to a tagged-union PARAM, or 0 when no register-class
// widen happens: param not tagged / not a fixed param, an already-matching-
// slot tagged source (natural push), the nullable 8B fold (handled by the
// scalar path), or a >48B memory-class slot (staged below the register words).
// This is the SSoT the cgcall DRAIN consults for its widen-first POP count;
// it MUST agree word-for-word with pushargsrev's widen-PUSH count below
// (same param-tagged + !aistagged gates, same taggedcastpeel) or the drain
// desyncs — the #30/#48 root was a drain with no widen branch: the widened
// box's words were under-drained (#30: a following float read the leftover
// payload word) or the float-source box was misclassified as a float arg
// (#48: MOVSD ate the tag word into X0). Mirrors cstage's precomputed
// widen[i]/widen_sz[i] (cmd/w6c/cgen.c cgcall).
fn argtaggedwidensz(c: *cgen, arg0: *node, param: *node) i32 = {
if (param == nil) { return 0; };
if (param.kind != nkind.N_PARAM) { return 0; };
if (param.op == tkind.TK_ELLIPSIS) { return 0; };
let ptype: *node = param.lhs;
if (ptype == nil) { return 0; };
if (!istaggedtype(c, ptype)) { return 0; };
// >48B slot is memory-class: pushargsrev stages it below the register
// words and the drain skips it (dmemsz) — never a register widen.
if (taggedmemargsize(ptype.type_: *tinfo) > 0) { return 0; };
let arg: *node = taggedcastpeel(c, arg0);
let pslot: i32 = slotsize(c, ptype);
// Already a matching-slot tagged source → natural push, no widen
// (mirrors pushargsrev's aistagged gates: ident/call/index/dot/deref).
if (arg.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, arg.str);
if (lc != nil) {
if (istaggedtype(c, lc.tnode)) {
if (slotsize(c, lc.tnode) == pslot) { return 0; };
};
};
};
if (taggedcallslot(c, arg) == pslot) { return 0; };
if (arg.kind == nkind.N_INDEX) {
if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; };
};
if (arg.kind == nkind.N_DOT) {
if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; };
};
if (arg.kind == nkind.N_UN && arg.op == tkind.TK_STAR) {
if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; };
};
// The 8B nullable fold pushes one pointer word the scalar drain path
// already pops correctly; only the multi-word tagged widen needs the
// drain's dedicated branch.
if (pslot < 16) { return 0; };
return pslot;
};
fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = {
if (arg == nil) { return 0; };
let nextparam: *node = nil;