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

@@ -663,6 +663,63 @@ fn cgreturn(c: *cgen, n: *node) void = {
};
};
};
// #38b: sret-classified tagged return (slot > the
// AX/DX/CX/R8 cursor) — write through *(@sretarg) and
// return the dest pointer. Three shapes mirror cstage
// cgen.c N_RETURN #38b: exact-type N_CALL forward
// (c.sretforward), widening from a >32B tagged source
// (#40 loud-stop), everything else through
// cgwidentaggedstore's non-BP base.
if (sretretsize(c, c.fnret) > 0) {
let sa38v: i32 = localfind(c, "@sretarg");
if (forwardtagged) {
// exact type, but only an N_CALL source
// sret's into outer's dest; a cursor
// source (N_INDEX/N_DOT) can't carry
// >32B (rule 7, #38b residual).
if (rhs.kind != nkind.N_CALL) {
let m38d: str = "#38b: >32B tagged return from a cursor source (N_INDEX/N_DOT) unsupported\n";
os.write(2, m38d.ptr, m38d.len: u64);
os.exit(1);
};
c.sretforward = 1;
cgexpr(c, rhs);
emitline("\tMOVQ\t");
emitoff(sa38v: i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
let ru38: *tinfo = rhs.type_: *tinfo;
for (ru38 != nil && ru38.kind == tykind.TY_NAMED) {
ru38 = ru38.under;
};
if (ru38 != nil) {
if (ru38.kind == tykind.TY_TAGGED
&& rhs.kind != nkind.N_IDENT
&& ru38.size: i32 > TUPLE_GPCAP * 8) {
let m38e: str = "#40: widening tagged return-forward of a >32B source needs mem-to-mem tag-remap (unwired)\n";
os.write(2, m38e.ptr, m38e.len: u64);
os.exit(1);
};
};
emitline("\tMOVQ\t");
emitoff(sa38v: i64);
emitline("(BP), BX\n");
cgwidentaggedstore(c, c.fnret.type_: *tinfo, rhs,
"BX", 0, slotsize(c, c.fnret));
emitline("\tMOVQ\t");
emitoff(sa38v: i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
// Struct payload or tagged-subset return — materialise
// the widened value in scratch via cgwidentaggedstore
// (handles tag remap and zero pad), then load AX/DX/CX
@@ -1438,6 +1495,30 @@ fn cgreturn(c: *cgen, n: *node) void = {
// the void variant: emit its tag. Payload is undefined
// (void has size 0). Otherwise zero AX for determinism.
if (istaggedtype(c, c.fnret)) {
// #38b: an sret-classified tagged return (slot > the
// AX/DX/CX/R8 cursor) writes the void-variant tag
// through *(@sretarg) and returns the dest pointer —
// the cursor can't carry the slot and the caller reads
// memory. Mirrors cstage cgen.c N_RETURN bare arm.
if (sretretsize(c, c.fnret) > 0) {
let sa38: i32 = localfind(c, "@sretarg");
let vidx38: i32 = voidvariantindex(c.fnret);
if (vidx38 < 0) { vidx38 = 0; };
emitline("\tMOVQ\t");
emitoff(sa38: i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t$");
emitint(vidx38: i64);
emitline(", (BX)\n");
emitline("\tMOVQ\t");
emitoff(sa38: i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
c.lastwasreturn = 1;
return;
};
if (isnullabletype(c.fnret)) {
// null = void variant; AX = 0.
emitline("\tMOVQ\t$0, AX\n");
@@ -1855,10 +1936,44 @@ fn cglet(c: *cgen, n: *node) void = {
// handles nullable fold, tagged source (ident or AX/DX/CX
// ABI call), struct payload (literal/ident), str payload,
// scalar payload — with tag remap for tagged-subset widening.
//
// #38b: an sret-classified tagged CALL result is in memory,
// not the cursor — an exact-type receive falls through to the
// generic sret receive below (the let's slot IS the dest); a
// widening receive needs mem-to-mem tag-remap (#40, unwired).
// Mirrors cstage cgen.c N_LET tagged arm.
if (istaggedtype(c, tn)) {
cgwidentaggedstore(c, tn.type_: *tinfo, rhs, "BP", off, sz);
c.lastwasreturn = 0;
return;
let letsret: i32 = 0;
if (rhs.kind == nkind.N_CALL) {
letsret = callsretsize(c, rhs);
};
if (letsret == 0) {
cgwidentaggedstore(c, tn.type_: *tinfo, rhs,
"BP", off, sz);
c.lastwasreturn = 0;
return;
};
let lru: *tinfo = rhs.type_: *tinfo;
for (lru != nil && lru.kind == tykind.TY_NAMED) {
lru = lru.under;
};
let llu: *tinfo = tn.type_: *tinfo;
for (llu != nil && llu.kind == tykind.TY_NAMED) {
llu = llu.under;
};
let exact38: bool = false;
if (lru != nil && lru == llu) { exact38 = true; }
else {
if (typeeq(rhs.type_: *tinfo, tn.type_: *tinfo)) {
exact38 = true;
};
};
if (!exact38) {
let m40c: str = "#40: sret-class call result cannot be widened into a tagged slot (mem-to-mem widen unwired)\n";
os.write(2, m40c.ptr, m40c.len: u64);
os.exit(1);
};
// fall through to the generic sret receive below.
};
// 32B tuple init for `let t: (scalar, str) = call()` /
// `let t: (str, scalar) = call()` (#105 / #164/#107). Each