w6c+wwstage: receive over-cap tuple sret returns at the call site (#10 Fold B)

Fold A made the CALLEE emit an over-capacity tuple return (> 4 GP or > 2
SSE eightbytes) via sret, but every receive site stayed loud-stopped, so
such a fn was not yet usefully callable. Fold B wires the call/receive end
by aligning every receive gate UP to the shared cg_sret_retsize() /
callsretsize() > 0 predicate (never a kind), per Rob's (B) ruling:

  - single-var-let  `let t = f();`      cstage gate generalised from
        TY_STRUCT&&>24 to cg_sret_retsize(lt)>0; the let's slot IS the
        sret dest, the callee writes the whole tuple there, t.0/t.1 read
        by offset. wwstage already keyed callsretsize (verified).
  - N_ASSIGN-ident  `t = f();`          same generalisation; global arm
        kept TY_STRUCT-only (a tuple-global has no sret-to-symbol path in
        either stage). wwstage grows a tuple-local arm (rettupleof gates
        it apart from the >24B-struct recv, which keeps its own path).
  - destructure     `let (a,b) = f();` and `a,b = f();` — the genuinely
        new wiring: the callee sret's into the @sretscr discard slot, then
        a copy-out loop moves each element to its binding at the SAME
        packed offset the SEND wrote (foff += element size), each at its
        natural width (#169); a `_` binding skips its store but advances
        foff. Both stages, byte-identical.
  - return-forward  `return f();`        cstage forward gate generalised
        to the predicate, reusing cg_sret_forward verbatim. wwstage
        already keyed sretretsize (verified).

The escape boundary stays loud: arg-pass `g(f())` fatals identically in
both stages (tuple arg exceeds return-cursor ABI capacity).

Test 799 is the runtime net Fold A deferred (byte-id is blind to a
SEND/RECEIVE layout mismatch): the bytes.cut-shaped ([]u8,[]u8) round-trip
over destructure / single-var-let / reassign / return-forward, each both
RUN under cstage and asserted cs==ww byte-identical. Tests 945 (row F)
and 956 (f64x3) flip from asserting the old over-cap loud-stop to
asserting the now-working sret round-trip. combined.ww amalgams (w6c +
wwdump embed the wcc cgen) regenerated. Unblocks #4 bytes.cut/rcut.
This commit is contained in:
2026-06-01 13:06:33 +09:00
parent 19e6b68d03
commit a937d67377
9 changed files with 885 additions and 45 deletions

View File

@@ -25659,6 +25659,26 @@ fn cgassign(c: *cgen, n: *node) void = {
};
return;
};
// #10 Fold B: over-cap tuple reassign `t = f();`. t's
// slot (off) IS the caller-prealloc dest; the callee
// writes the whole tuple through hidden RDI. Keys on
// callsretsize (the shared sret SSoT) for a tuple-
// returning call — rettupleof distinguishes it from a
// >24B struct, which keeps its own size-aware recv
// below. Mirrors the cstage N_ASSIGN-ident over-cap arm.
if (n.op == tkind.TK_ASSIGN && n.rhs != nil
&& n.rhs.kind == nkind.N_CALL) {
let rtup: *node = rettupleof(c, n.rhs);
if (rtup != nil) {
let rscs: i32 = callsretsize(c, n.rhs);
if (rscs > 0) {
c.sretdestoff = off;
cgexpr(c, n.rhs);
c.sretdestoff = 0;
return;
};
};
};
// Detect str/slice-typed local — assignment must store
// both halves (AX=ptr at +0, BX=len at +8) for str,
// plus the cap (CX at +16) for slice.
@@ -27668,8 +27688,81 @@ fn cgmassign(c: *cgen, n: *node) void = {
// multi-assign idiom — rule-9 carve-out. Over-capacity loud-stops.
let rettuple: *node = rettupleof(c, n.rhs);
// #10 Fold B: over-cap tuple destructure REASSIGN. Same sret copy-out
// as cgmlet but the slots already exist (localfind); a `_` / missing
// binding (off == 0) SKIPS its store yet still ADVANCES foff so the
// next element stays aligned (harec `_`). Byte-identical to the
// cstage N_MASSIGN over-cap arm.
let sretrecv: i32 = 0;
if (n.rhs != nil) {
if (n.rhs.kind == nkind.N_CALL) {
sretrecv = callsretsize(c, n.rhs);
};
};
if (n.rhs != nil) { cgexpr(c, n.rhs); };
if (sretrecv > 0) {
let scr: i32 = localfind(c, "@sretscr");
let pt2: *node = nil;
if (rettuple != nil) { pt2 = rettuple.list; };
let foff: i32 = 0;
let lb: *node = n.list;
for (lb != nil) {
let tn: *node = nil;
if (pt2 != nil) { tn = pt2.lhs; };
let isflt: bool = isfloattype(c, tn);
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
let esz: i32 = 8;
if (pt2 != nil) {
let eti: *tinfo = pt2.lhs.type_: *tinfo;
if (eti != nil) { esz = eti.size: i32; };
};
let off: i32 = 0;
if (lb.kind == nkind.N_IDENT) { off = localfind(c, lb.str); };
if (off != 0) {
if (isflt) {
let mov: str = "MOVSD";
if (isf32type(c, tn)) { mov = "MOVSS"; };
emitline("\t"); emitline(mov); emitline("\t");
emitoff((scr + foff): i64);
emitline("(BP), X0\n");
emitline("\t"); emitline(mov);
emitline("\tX0, ");
emitoff(off: i64); emitline("(BP)\n");
} else {
if (wide) {
let k: i32 = 0;
for (k < esz) {
emitline("\tMOVQ\t");
emitoff((scr + foff + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + k): i64);
emitline("(BP)\n");
k += 8;
};
} else {
let lop: str = tnodeloadop(c, tn, esz);
let sop: str = tnodestoreop(c, tn, esz);
emitline("\t"); emitline(lop);
emitline("\t");
emitoff((scr + foff): i64);
emitline("(BP), AX\n");
emitline("\t"); emitline(sop);
emitline("\tAX, ");
emitoff(off: i64); emitline("(BP)\n");
};
};
};
foff += esz;
lb = lb.next;
if (pt2 != nil) { pt2 = pt2.next; };
};
c.lastwasreturn = 0;
return;
};
let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV
let gptotal: i32 = 0;
let ssetotal: i32 = 0;
@@ -27755,8 +27848,75 @@ fn cgmlet(c: *cgen, n: *node) void = {
// scalar rides 1 word into an 8B slot. Over-capacity loud-stops.
let rettuple: *node = rettupleof(c, rhs);
// #10 Fold B: over-cap tuple destructure RECEIVE. The callee sret'd
// the whole tuple into the @sretscr discard slot (cgcall sees
// callsretsize > 0, no lvalue dest wired). Copy each element out to
// its binding slot at the SAME packed offset the SEND wrote (foff +=
// element size — the t.0/t.1 layout), each at its NATURAL width
// (#169). Byte-identical to the cstage N_MLET over-cap arm.
let sretrecv: i32 = 0;
if (rhs.kind == nkind.N_CALL) { sretrecv = callsretsize(c, rhs); };
cgexpr(c, rhs);
if (sretrecv > 0) {
let scr: i32 = localfind(c, "@sretscr");
let pt2: *node = nil;
if (rettuple != nil) { pt2 = rettuple.list; };
let foff: i32 = 0;
let lb: *node = n.list;
for (lb != nil) {
let tn: *node = nil;
if (pt2 != nil) { tn = pt2.lhs; };
let isflt: bool = isfloattype(c, tn);
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
let esz: i32 = 8;
if (pt2 != nil) {
let eti: *tinfo = pt2.lhs.type_: *tinfo;
if (eti != nil) { esz = eti.size: i32; };
};
let bsz: i32 = 8;
if (wide) { bsz = tyslicesize(): i32; };
let off: i32 = localadd(c, lb.str, bsz, tn);
if (isflt) {
let mov: str = "MOVSD";
if (isf32type(c, tn)) { mov = "MOVSS"; };
emitline("\t"); emitline(mov); emitline("\t");
emitoff((scr + foff): i64);
emitline("(BP), X0\n");
emitline("\t"); emitline(mov); emitline("\tX0, ");
emitoff(off: i64); emitline("(BP)\n");
} else {
if (wide) {
let k: i32 = 0;
for (k < esz) {
emitline("\tMOVQ\t");
emitoff((scr + foff + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + k): i64);
emitline("(BP)\n");
k += 8;
};
} else {
let lop: str = tnodeloadop(c, tn, esz);
let sop: str = tnodestoreop(c, tn, esz);
emitline("\t"); emitline(lop); emitline("\t");
emitoff((scr + foff): i64);
emitline("(BP), AX\n");
emitline("\t"); emitline(sop);
emitline("\tAX, ");
emitoff(off: i64); emitline("(BP)\n");
};
};
foff += esz;
lb = lb.next;
if (pt2 != nil) { pt2 = pt2.next; };
};
c.lastwasreturn = 0;
return;
};
let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV
let gptotal: i32 = 0;
let ssetotal: i32 = 0;

View File

@@ -6971,6 +6971,26 @@ fn cgassign(c: *cgen, n: *node) void = {
};
return;
};
// #10 Fold B: over-cap tuple reassign `t = f();`. t's
// slot (off) IS the caller-prealloc dest; the callee
// writes the whole tuple through hidden RDI. Keys on
// callsretsize (the shared sret SSoT) for a tuple-
// returning call — rettupleof distinguishes it from a
// >24B struct, which keeps its own size-aware recv
// below. Mirrors the cstage N_ASSIGN-ident over-cap arm.
if (n.op == tkind.TK_ASSIGN && n.rhs != nil
&& n.rhs.kind == nkind.N_CALL) {
let rtup: *node = rettupleof(c, n.rhs);
if (rtup != nil) {
let rscs: i32 = callsretsize(c, n.rhs);
if (rscs > 0) {
c.sretdestoff = off;
cgexpr(c, n.rhs);
c.sretdestoff = 0;
return;
};
};
};
// Detect str/slice-typed local — assignment must store
// both halves (AX=ptr at +0, BX=len at +8) for str,
// plus the cap (CX at +16) for slice.

View File

@@ -1706,8 +1706,81 @@ fn cgmassign(c: *cgen, n: *node) void = {
// multi-assign idiom — rule-9 carve-out. Over-capacity loud-stops.
let rettuple: *node = rettupleof(c, n.rhs);
// #10 Fold B: over-cap tuple destructure REASSIGN. Same sret copy-out
// as cgmlet but the slots already exist (localfind); a `_` / missing
// binding (off == 0) SKIPS its store yet still ADVANCES foff so the
// next element stays aligned (harec `_`). Byte-identical to the
// cstage N_MASSIGN over-cap arm.
let sretrecv: i32 = 0;
if (n.rhs != nil) {
if (n.rhs.kind == nkind.N_CALL) {
sretrecv = callsretsize(c, n.rhs);
};
};
if (n.rhs != nil) { cgexpr(c, n.rhs); };
if (sretrecv > 0) {
let scr: i32 = localfind(c, "@sretscr");
let pt2: *node = nil;
if (rettuple != nil) { pt2 = rettuple.list; };
let foff: i32 = 0;
let lb: *node = n.list;
for (lb != nil) {
let tn: *node = nil;
if (pt2 != nil) { tn = pt2.lhs; };
let isflt: bool = isfloattype(c, tn);
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
let esz: i32 = 8;
if (pt2 != nil) {
let eti: *tinfo = pt2.lhs.type_: *tinfo;
if (eti != nil) { esz = eti.size: i32; };
};
let off: i32 = 0;
if (lb.kind == nkind.N_IDENT) { off = localfind(c, lb.str); };
if (off != 0) {
if (isflt) {
let mov: str = "MOVSD";
if (isf32type(c, tn)) { mov = "MOVSS"; };
emitline("\t"); emitline(mov); emitline("\t");
emitoff((scr + foff): i64);
emitline("(BP), X0\n");
emitline("\t"); emitline(mov);
emitline("\tX0, ");
emitoff(off: i64); emitline("(BP)\n");
} else {
if (wide) {
let k: i32 = 0;
for (k < esz) {
emitline("\tMOVQ\t");
emitoff((scr + foff + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + k): i64);
emitline("(BP)\n");
k += 8;
};
} else {
let lop: str = tnodeloadop(c, tn, esz);
let sop: str = tnodestoreop(c, tn, esz);
emitline("\t"); emitline(lop);
emitline("\t");
emitoff((scr + foff): i64);
emitline("(BP), AX\n");
emitline("\t"); emitline(sop);
emitline("\tAX, ");
emitoff(off: i64); emitline("(BP)\n");
};
};
};
foff += esz;
lb = lb.next;
if (pt2 != nil) { pt2 = pt2.next; };
};
c.lastwasreturn = 0;
return;
};
let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV
let gptotal: i32 = 0;
let ssetotal: i32 = 0;
@@ -1793,8 +1866,75 @@ fn cgmlet(c: *cgen, n: *node) void = {
// scalar rides 1 word into an 8B slot. Over-capacity loud-stops.
let rettuple: *node = rettupleof(c, rhs);
// #10 Fold B: over-cap tuple destructure RECEIVE. The callee sret'd
// the whole tuple into the @sretscr discard slot (cgcall sees
// callsretsize > 0, no lvalue dest wired). Copy each element out to
// its binding slot at the SAME packed offset the SEND wrote (foff +=
// element size — the t.0/t.1 layout), each at its NATURAL width
// (#169). Byte-identical to the cstage N_MLET over-cap arm.
let sretrecv: i32 = 0;
if (rhs.kind == nkind.N_CALL) { sretrecv = callsretsize(c, rhs); };
cgexpr(c, rhs);
if (sretrecv > 0) {
let scr: i32 = localfind(c, "@sretscr");
let pt2: *node = nil;
if (rettuple != nil) { pt2 = rettuple.list; };
let foff: i32 = 0;
let lb: *node = n.list;
for (lb != nil) {
let tn: *node = nil;
if (pt2 != nil) { tn = pt2.lhs; };
let isflt: bool = isfloattype(c, tn);
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
let esz: i32 = 8;
if (pt2 != nil) {
let eti: *tinfo = pt2.lhs.type_: *tinfo;
if (eti != nil) { esz = eti.size: i32; };
};
let bsz: i32 = 8;
if (wide) { bsz = tyslicesize(): i32; };
let off: i32 = localadd(c, lb.str, bsz, tn);
if (isflt) {
let mov: str = "MOVSD";
if (isf32type(c, tn)) { mov = "MOVSS"; };
emitline("\t"); emitline(mov); emitline("\t");
emitoff((scr + foff): i64);
emitline("(BP), X0\n");
emitline("\t"); emitline(mov); emitline("\tX0, ");
emitoff(off: i64); emitline("(BP)\n");
} else {
if (wide) {
let k: i32 = 0;
for (k < esz) {
emitline("\tMOVQ\t");
emitoff((scr + foff + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + k): i64);
emitline("(BP)\n");
k += 8;
};
} else {
let lop: str = tnodeloadop(c, tn, esz);
let sop: str = tnodestoreop(c, tn, esz);
emitline("\t"); emitline(lop); emitline("\t");
emitoff((scr + foff): i64);
emitline("(BP), AX\n");
emitline("\t"); emitline(sop);
emitline("\tAX, ");
emitoff(off: i64); emitline("(BP)\n");
};
};
foff += esz;
lb = lb.next;
if (pt2 != nil) { pt2 = pt2.next; };
};
c.lastwasreturn = 0;
return;
};
let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV
let gptotal: i32 = 0;
let ssetotal: i32 = 0;

View File

@@ -25659,6 +25659,26 @@ fn cgassign(c: *cgen, n: *node) void = {
};
return;
};
// #10 Fold B: over-cap tuple reassign `t = f();`. t's
// slot (off) IS the caller-prealloc dest; the callee
// writes the whole tuple through hidden RDI. Keys on
// callsretsize (the shared sret SSoT) for a tuple-
// returning call — rettupleof distinguishes it from a
// >24B struct, which keeps its own size-aware recv
// below. Mirrors the cstage N_ASSIGN-ident over-cap arm.
if (n.op == tkind.TK_ASSIGN && n.rhs != nil
&& n.rhs.kind == nkind.N_CALL) {
let rtup: *node = rettupleof(c, n.rhs);
if (rtup != nil) {
let rscs: i32 = callsretsize(c, n.rhs);
if (rscs > 0) {
c.sretdestoff = off;
cgexpr(c, n.rhs);
c.sretdestoff = 0;
return;
};
};
};
// Detect str/slice-typed local — assignment must store
// both halves (AX=ptr at +0, BX=len at +8) for str,
// plus the cap (CX at +16) for slice.
@@ -27668,8 +27688,81 @@ fn cgmassign(c: *cgen, n: *node) void = {
// multi-assign idiom — rule-9 carve-out. Over-capacity loud-stops.
let rettuple: *node = rettupleof(c, n.rhs);
// #10 Fold B: over-cap tuple destructure REASSIGN. Same sret copy-out
// as cgmlet but the slots already exist (localfind); a `_` / missing
// binding (off == 0) SKIPS its store yet still ADVANCES foff so the
// next element stays aligned (harec `_`). Byte-identical to the
// cstage N_MASSIGN over-cap arm.
let sretrecv: i32 = 0;
if (n.rhs != nil) {
if (n.rhs.kind == nkind.N_CALL) {
sretrecv = callsretsize(c, n.rhs);
};
};
if (n.rhs != nil) { cgexpr(c, n.rhs); };
if (sretrecv > 0) {
let scr: i32 = localfind(c, "@sretscr");
let pt2: *node = nil;
if (rettuple != nil) { pt2 = rettuple.list; };
let foff: i32 = 0;
let lb: *node = n.list;
for (lb != nil) {
let tn: *node = nil;
if (pt2 != nil) { tn = pt2.lhs; };
let isflt: bool = isfloattype(c, tn);
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
let esz: i32 = 8;
if (pt2 != nil) {
let eti: *tinfo = pt2.lhs.type_: *tinfo;
if (eti != nil) { esz = eti.size: i32; };
};
let off: i32 = 0;
if (lb.kind == nkind.N_IDENT) { off = localfind(c, lb.str); };
if (off != 0) {
if (isflt) {
let mov: str = "MOVSD";
if (isf32type(c, tn)) { mov = "MOVSS"; };
emitline("\t"); emitline(mov); emitline("\t");
emitoff((scr + foff): i64);
emitline("(BP), X0\n");
emitline("\t"); emitline(mov);
emitline("\tX0, ");
emitoff(off: i64); emitline("(BP)\n");
} else {
if (wide) {
let k: i32 = 0;
for (k < esz) {
emitline("\tMOVQ\t");
emitoff((scr + foff + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + k): i64);
emitline("(BP)\n");
k += 8;
};
} else {
let lop: str = tnodeloadop(c, tn, esz);
let sop: str = tnodestoreop(c, tn, esz);
emitline("\t"); emitline(lop);
emitline("\t");
emitoff((scr + foff): i64);
emitline("(BP), AX\n");
emitline("\t"); emitline(sop);
emitline("\tAX, ");
emitoff(off: i64); emitline("(BP)\n");
};
};
};
foff += esz;
lb = lb.next;
if (pt2 != nil) { pt2 = pt2.next; };
};
c.lastwasreturn = 0;
return;
};
let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV
let gptotal: i32 = 0;
let ssetotal: i32 = 0;
@@ -27755,8 +27848,75 @@ fn cgmlet(c: *cgen, n: *node) void = {
// scalar rides 1 word into an 8B slot. Over-capacity loud-stops.
let rettuple: *node = rettupleof(c, rhs);
// #10 Fold B: over-cap tuple destructure RECEIVE. The callee sret'd
// the whole tuple into the @sretscr discard slot (cgcall sees
// callsretsize > 0, no lvalue dest wired). Copy each element out to
// its binding slot at the SAME packed offset the SEND wrote (foff +=
// element size — the t.0/t.1 layout), each at its NATURAL width
// (#169). Byte-identical to the cstage N_MLET over-cap arm.
let sretrecv: i32 = 0;
if (rhs.kind == nkind.N_CALL) { sretrecv = callsretsize(c, rhs); };
cgexpr(c, rhs);
if (sretrecv > 0) {
let scr: i32 = localfind(c, "@sretscr");
let pt2: *node = nil;
if (rettuple != nil) { pt2 = rettuple.list; };
let foff: i32 = 0;
let lb: *node = n.list;
for (lb != nil) {
let tn: *node = nil;
if (pt2 != nil) { tn = pt2.lhs; };
let isflt: bool = isfloattype(c, tn);
let wide: bool = isstrtype(c, tn) || isslicetype(c, tn);
let esz: i32 = 8;
if (pt2 != nil) {
let eti: *tinfo = pt2.lhs.type_: *tinfo;
if (eti != nil) { esz = eti.size: i32; };
};
let bsz: i32 = 8;
if (wide) { bsz = tyslicesize(): i32; };
let off: i32 = localadd(c, lb.str, bsz, tn);
if (isflt) {
let mov: str = "MOVSD";
if (isf32type(c, tn)) { mov = "MOVSS"; };
emitline("\t"); emitline(mov); emitline("\t");
emitoff((scr + foff): i64);
emitline("(BP), X0\n");
emitline("\t"); emitline(mov); emitline("\tX0, ");
emitoff(off: i64); emitline("(BP)\n");
} else {
if (wide) {
let k: i32 = 0;
for (k < esz) {
emitline("\tMOVQ\t");
emitoff((scr + foff + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + k): i64);
emitline("(BP)\n");
k += 8;
};
} else {
let lop: str = tnodeloadop(c, tn, esz);
let sop: str = tnodestoreop(c, tn, esz);
emitline("\t"); emitline(lop); emitline("\t");
emitoff((scr + foff): i64);
emitline("(BP), AX\n");
emitline("\t"); emitline(sop);
emitline("\tAX, ");
emitoff(off: i64); emitline("(BP)\n");
};
};
foff += esz;
lb = lb.next;
if (pt2 != nil) { pt2 = pt2.next; };
};
c.lastwasreturn = 0;
return;
};
let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV
let gptotal: i32 = 0;
let ssetotal: i32 = 0;