w6c+w6c_ww: tagged sret for slot>32B returns (fix #38)

A tagged-union RETURN rides a fixed AX(tag)+DX/CX/R8 cursor (TUPLE_GPCAP
eightbytes = 32B slot); wider slots were silently truncated at the
return crossing — payload word 4+ built in the callee frame and died
there, byte-identical on both stages (gate-blind). Blocks regex fold-2a
((regex | error | nomem) = 64B slot).

Classifier: cg_sret_retsize / sretretsize gain a TY_TAGGED arm
(<= TUPLE_GPCAP*8 stays register-ABI — the (str|nomem)/(s3|bool) 32B
boundary class is pinned unchanged byte-for-byte vs master). Callee:
cgreturn writes the slot through *(@sretarg) via the existing widener
non-BP base (bare return stores the void tag); exact-type 'return f();'
rides the #9 sret-forward. Receive: let/assign/discard reuse the
generic #23/#10 sret protocol; the match scrutinee passes its spill
slot as the sret dest (tagged-specific, no tuple precedent).

This could NOT land as a gate-first interim loud-stop (the planned
#38a): lib/errors/errors.ww errno() already returns a 40B
(errors.error) slot in-tree — the cgenstmt.ww-documented #222 latent —
so a bare gate breaks the build. errno graduates to sret here instead;
errnotest pins it at runtime (its cstage run; the wwstage run was
already failing at master via an unrelated pre-existing indirect-call
arg-classification divergence, reported separately) and test/926's
errno-shaped row reads the previously-dropped tail word on both stages.

The unwired cursor consumers of an sret-class call result loud-stop
(rule 7) rather than read a cursor the callee no longer fills:
widening forward/receive ((A|B)->(A|B|C) mem-to-mem tag-remap, filed
#40), ?/!/is/as operands, argument position, and the >48B tagged-arg
class both stages previously mishandled silently. One-class-one-commit
per the #133 carve-out: post-flip those consumers would read AX (now
the dest pointer) as the tag — a gates-trailing commit would leave a
silently-wrong bisect point, so the flip and its gates are not
separable.

test/926: 15 rows — 56B regex-shaped round-trips (literal/local/
assign/match-scrutinee/forward/str-variant/multi-call), 40B repro +
bare-return-void, the errno-shaped tail-read graduation row, 32B
boundary rows pinned register-ABI by asm sentinel, and 3 loud-stop
rows pinned as build failures on both stages.
This commit is contained in:
2026-06-04 03:43:44 +09:00
parent 5f15eb3d09
commit 4f3967835e
8 changed files with 1660 additions and 22 deletions

View File

@@ -104,6 +104,27 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
let nextparam: *node = nil;
if (param != nil) { nextparam = param.next; };
let rest: i32 = pushargsrev(c, arg.next, nextparam);
// #38b residual (rule 7): a tagged arg slot past the 6-reg arg
// capacity has no push shape — cstage tagged_arg_size returns 0
// ("too large") and both stages fell to divergent silent pushes
// (cs one word, ww slot words). Mirrors cstage cgcall's gate.
{
let a48: *tinfo = arg.type_: *tinfo;
for (a48 != nil && a48.kind == tykind.TY_NAMED) {
a48 = a48.under;
};
if (a48 != nil) {
if (a48.kind == tykind.TY_TAGGED && a48.nullable == 0) {
// sizelint-ok: 6 SysV int arg regs (DI..R9) x
// 8B words = the cstage tagged_arg_size cap.
if (a48.size: i32 > 6 * 8) {
let m48: str = "#38b: tagged arg exceeds the register arg capacity (>48B slot) — unwired\n";
os.write(2, m48.ptr, m48.len: u64);
os.exit(1);
};
};
};
};
// Implicit widening from a concrete variant to a tagged-union
// parameter slot. Skips when the arg is already a tagged local
// (line 121's slice-or-tagged shortcut handles that).
@@ -650,6 +671,15 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
// tag first. Mirrors cstage at cmd/w6c/cgen.c:4373-4387.
let tcs: i32 = taggedcallslot(c, arg);
if (tcs > 0) {
// #38b residual (rule 7): an sret-class call result is in
// memory, not the cursor — the @aggargscr-style receive-then-
// push is the #40-family follow-up. Mirrors cstage cgen.c
// cgcall tagged arg-push gate.
if (callsretsize(c, arg) > 0) {
let m38r: str = "#38b: >32B tagged call result as a call argument unwired (#40-family follow-up)\n";
os.write(2, m38r.ptr, m38r.len: u64);
os.exit(1);
};
if (tcs > 24) { emitline("\tPUSHQ\tR8\n"); };
if (tcs > 16) { emitline("\tPUSHQ\tCX\n"); };
if (tcs > 8) { emitline("\tPUSHQ\tDX\n"); };
@@ -1403,6 +1433,18 @@ export fn sretretsize(c: *cgen, t: *node) i32 = {
r = r.lhs;
if (r == nil) { return 0; };
};
// #38: a tagged union rides AX(tag)+DX/CX/R8 = TUPLE_GPCAP
// eightbytes; a wider slot was silently truncated (payload word
// 4+ died in the callee frame). The ≤cap boundary is load-bearing:
// (str|nomem)-shaped 32B slots MUST stay register-ABI or every
// such consumer in the tree flips. Nullable folds to one word.
// Mirrors cstage cg_sret_retsize TY_TAGGED arm.
if (istaggedtype(c, r)) {
if (isnullabletype(r)) { return 0; };
let tsz38: i32 = slotsize(c, r);
if (tsz38 <= TUPLE_GPCAP * 8) { return 0; };
return tsz38;
};
if (r.kind == nkind.N_TTUPLE) {
// #10: over-cap tuple → sret. Walk the element TYPE nodes
// (pt.lhs) over the SAME caps the SEND/receive use; a float =
@@ -2961,6 +3003,17 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s
ck += 8;
};
} else {
// #38b: an sret-classified call result is in
// memory (AX = dest pointer), not the cursor —
// the spill below would store the pointer as
// the payload. Mem-to-mem widen is #40.
if (src.kind == nkind.N_CALL) {
if (callsretsize(c, src) > 0) {
let m40a: str = "#40: sret-class call result cannot be cursor-widened into a tagged slot (mem-to-mem widen unwired)\n";
os.write(2, m40a.ptr, m40a.len: u64);
os.exit(1);
};
};
cgexpr(c, src);
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + 8): i64);
@@ -3024,6 +3077,15 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s
// of tagged element). R8 carries the 4th word for slice-payload
// variants (slot 32B).
if (rhstaggedabicall(c, src)) {
// #38b: an sret-classified call result is in memory (AX =
// dest pointer), not the cursor. #40.
if (src.kind == nkind.N_CALL) {
if (callsretsize(c, src) > 0) {
let m40b: str = "#40: sret-class call result cannot be cursor-widened into a tagged slot (mem-to-mem widen unwired)\n";
os.write(2, m40b.ptr, m40b.len: u64);
os.exit(1);
};
};
cgexpr(c, src);
emitline("\tMOVQ\tAX, ");
emitoff(slot_off: i64);
@@ -3212,7 +3274,7 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s
// str IS []u8 and a slice is the
// same 3-word {ptr,len,cap} header
// from cgexpr's AX/BX/CX (#1/Phase
// 3). Slice arm rides #38's regex-
// 3). Slice arm rides #38b's regex-
// shaped consumer — the prior str-
// only gate dropped .len/.cap (#24
// gap's widener twin).