selfhost+test: route tagged-CALL arg through natural push (#21)

Wwstage call-arg-emit recognized tagged args only when the source
was an IDENT (already-materialized var). For N_CALL returning a
tagged-union, the natural-push path mis-routed: AX (tag) pushed
twice, AX clobbered with widentag(=0) between pushes, DX (payload)
dropped entirely. After POP, DI ← 0, SI ← tag — both reversed and
the payload word lost. Class A runtime miscompile, masked by zero
in-tree call sites of the shape until lib/encoding/utf8's iterator
API surfaced it via pre-flight A probe.

Fix aligns wwstage DOWN to cstage (rule 10). cgenutil.ww:pushargsrev
aistagged guard now fires for N_CALL whose callee returns a tagged
whose slot matches the param's tagged slot (mirrors cmd/w6c/cgen.c:
4216-4221's type_eq guard), and the natural-push fallthrough adds a
tagged-CALL arm pushing R8/CX/DX/AX high→low by slot size (mirrors
cmd/w6c/cgen.c:4373-4387). cgenexpr.ww:cgcall's per-arg pop-count
picks up the same taggedcallslot helper so the next arg's POPQ
doesn't land on residual tag/payload words.

Sister-family to #11/#14 in the variant-widen ABI chain — call-site/
caller-side surface, distinct from callee-side #11 (param decompose)
and scratch-side #14 (return slot). Fifth corpus-coverage-blind
unmask this session (catalog: i64 div/mod CQO #16; wwstage IDENT-
local /= no-op #16-B2; cstage signed-DATA module-scope #19; wwstage
silent-zero arrays #19 mirror; #21 call-arg DX drop).

Test: 720_tagged_call_arg asm-presence row (PUSHQ DX appears
between CALL and next CALL, before PUSHQ AX) + 924_tagged_call_arg_
run 9xx semantic row (5 rows: 4-variant CALL-source, 4-variant
IDENT-source regression guard, 2-variant ptr/err, multi-arg tagged
+ scalar). Bootstrap byte-id (ww2 == ww3 == ww4) holds.
This commit is contained in:
2026-05-17 07:33:55 +09:00
parent 9bd1d0d734
commit 6ab865d933
7 changed files with 654 additions and 0 deletions

View File

@@ -3024,6 +3024,12 @@ fn cgcall(c: *cgen, n: *node) void = {
let extra: i32 = 0;
if (nodeisstr(c, a)) { extra = 1; };
if (nodeisslice(c, a)) { extra = 2; };
// #21: tagged-CALL arg was pushed AX/DX/CX/R8 high→low
// by pushargsrev; size the per-arg pop to match so the
// next arg's POPQ doesn't land on residual tag/payload
// words and shift intidx out of sync.
let tcs: i32 = taggedcallslot(c, a);
if (tcs > 0) { extra = tcs / 8 - 1; };
let words: i32 = 1 + extra;
let w: i32 = 0;
for (w < words) {

View File

@@ -138,6 +138,18 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
aistagged = istaggedtype(c, lc.tnode);
};
};
// #21: a CALL returning a tagged-union must
// skip widening — cgexpr leaves AX=tag,
// DX=word0, CX=word1, R8=word2 per the
// tagged-return ABI; the widening branch would
// treat AX as a concrete payload and silently
// drop DX/CX/R8. Restrict to the matching-slot
// case (mirrors cstage type_eq at
// cmd/w6c/cgen.c:4216-4221); tagged-source
// widening into a wider slot is out of scope.
if (taggedcallslot(c, arg) == slotsize(c, ptype)) {
aistagged = true;
};
if (!aistagged) {
widensz = slotsize(c, ptype);
let tagged: *node = resolvetagged(c, ptype);
@@ -424,10 +436,40 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
emitline("\tPUSHQ\tAX\n");
return rest + 2;
};
// #21: CALL returning a tagged-union — the aistagged guard
// above kept us out of the widening path. Push the tagged-
// return ABI registers (AX=tag, DX=word0, CX=word1, R8=word2)
// high → low so the left-to-right POPQ into argregs drains the
// tag first. Mirrors cstage at cmd/w6c/cgen.c:4373-4387.
let tcs: i32 = taggedcallslot(c, arg);
if (tcs > 0) {
if (tcs > 24) { emitline("\tPUSHQ\tR8\n"); };
if (tcs > 16) { emitline("\tPUSHQ\tCX\n"); };
if (tcs > 8) { emitline("\tPUSHQ\tDX\n"); };
emitline("\tPUSHQ\tAX\n");
return rest + tcs / 8;
};
emitline("\tPUSHQ\tAX\n");
return rest + 1;
};
// taggedcallslot — if `n` is an N_CALL whose callee returns a tagged
// type, returns the slot size in bytes; else 0. Used by pushargsrev's
// aistagged guard and natural-push arm, and by cgcall's pop sizer, to
// route a tagged-return call result through the AX/DX/CX/R8 high→low
// push convention rather than the concrete-variant widening path
// (which drops DX/CX/R8). See task #21.
export fn taggedcallslot(c: *cgen, n: *node) i32 = {
if (n == nil) { return 0; };
if (n.kind != nkind.N_CALL) { return 0; };
let callee: *node = n.lhs;
if (callee == nil) { return 0; };
if (callee.kind != nkind.N_IDENT) { return 0; };
let rt: *node = fnretlookup(c, callee.str);
if (!istaggedtype(c, rt)) { return 0; };
return slotsize(c, rt);
};
fn nodeisslice(c: *cgen, n: *node) bool = {
if (n == nil) { return false; };
let k: nkind = n.kind;