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

@@ -15796,6 +15796,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).
@@ -16342,6 +16363,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"); };
@@ -17095,6 +17125,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 =
@@ -18653,6 +18695,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);
@@ -18716,6 +18769,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);
@@ -18904,7 +18966,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).
@@ -19970,6 +20032,25 @@ fn cgtrytupleshift(c: *cgen, n: *node) bool = {
// Success tag = 0 (#216 tracks the legacy/flag-aware success-tag
// divergence — out of scope here, success check stays `CMPQ $0`).
fn cgtryprop(c: *cgen, n: *node) void = {
// #38b residuals (rule 7): the cursor read below cannot see an
// sret-classified call result (AX = dest pointer), and the
// propagate-RET cannot speak an sret-classified enclosing return
// (the caller reads memory, not the cursor). #40-family follow-ups.
// Mirrors cstage cgen.c N_TRYPROP gates.
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_CALL) {
if (callsretsize(c, n.lhs) > 0) {
let m38p: str = "#38b: `?` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n";
os.write(2, m38p.ptr, m38p.len: u64);
os.exit(1);
};
};
};
if (sretretsize(c, c.fnret) > 0) {
let m38q: str = "#38b: `?` propagation into a >32B tagged return unwired (sret error-propagate is a #40-family follow-up)\n";
os.write(2, m38q.ptr, m38q.len: u64);
os.exit(1);
};
cgexpr(c, n.lhs);
// AX = tag. If non-zero, this is an error; pop frame and RET.
let cl: str = mklabel(c, "tryprop_ok");
@@ -20088,6 +20169,16 @@ fn cgtryprop(c: *cgen, n: *node) void = {
// cgtryunw — `e!` aborts on the error variant via exit(1). Legacy
// semantics (success tag = 0).
fn cgtryunw(c: *cgen, n: *node) void = {
// #38b residual (rule 7): see the cgtryprop twin.
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_CALL) {
if (callsretsize(c, n.lhs) > 0) {
let m38u: str = "#38b: `!` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n";
os.write(2, m38u.ptr, m38u.len: u64);
os.exit(1);
};
};
};
cgexpr(c, n.lhs);
let cl: str = mklabel(c, "tryunw_ok");
emitline("\tCMPQ\t$0, AX\n");
@@ -20157,6 +20248,18 @@ fn cgtypetest(c: *cgen, n: *node) void = {
// stored via *i32 in this context — direct assignment of the
// local works, indirection through &scrutoff drops sign bits.
let lhs: *node = n.lhs;
// #38b residual (rule 7): an sret-class call result leaves AX =
// dest pointer, not the tag — mem-based test is a #40-family
// follow-up. Mirrors cstage cgen.c N_TYPETEST gate.
if (lhs != nil) {
if (lhs.kind == nkind.N_CALL) {
if (callsretsize(c, lhs) > 0) {
let m38t: str = "#38b: `is` on an sret-class call result unwired (#40-family follow-up)\n";
os.write(2, m38t.ptr, m38t.len: u64);
os.exit(1);
};
};
};
let scrutoff: i32 = 0;
let scrutt: *node = nil;
if (lhs != nil) {
@@ -20245,6 +20348,18 @@ fn cgtypeassert(c: *cgen, n: *node) void = {
// str → (AX, BX). Mirrors cgmatch's slot-based value load.
// Slot resolution inlined; see cgtypetest comment.
let lhs: *node = n.lhs;
// #38b residual (rule 7): the spill below reads the cursor, which
// an sret-class call result never fills. Mirrors cstage cgen.c
// N_TYPEASSERT gate.
if (lhs != nil) {
if (lhs.kind == nkind.N_CALL) {
if (callsretsize(c, lhs) > 0) {
let m38a: str = "#38b: `as` on an sret-class call result unwired (#40-family follow-up)\n";
os.write(2, m38a.ptr, m38a.len: u64);
os.exit(1);
};
};
};
let scrutoff: i32 = 0;
let scrutt: *node = nil;
if (lhs != nil) {
@@ -21680,6 +21795,20 @@ fn cgmatch(c: *cgen, n: *node) void = {
scrutt = matchscrutt(c, scrut);
let spillsz: i32 = matchspillsz(c, scrutt);
scrutoff = localalloc(c, "@match_spill", spillsz, nil);
// #38b: sret-classified tagged call scrutinee — pass
// the scrut slot itself as the sret dest and skip the
// cursor spill; downstream tag dispatch / case-let
// binds already read the slot from memory. Mirrors
// cstage cgen.c N_MATCH.
let msret: i32 = 0;
if (scrut.kind == nkind.N_CALL) {
msret = callsretsize(c, scrut);
};
if (msret > 0) {
c.sretdestoff = scrutoff;
cgexpr(c, scrut);
c.sretdestoff = 0;
} else {
cgexpr(c, scrut);
emitline("\tMOVQ\tAX, ");
emitoff(scrutoff: i64);
@@ -21704,6 +21833,7 @@ fn cgmatch(c: *cgen, n: *node) void = {
emitline("(BP)\n");
};
};
};
};
};
let endl: str = mklabel(c, "match_end");
@@ -25041,12 +25171,68 @@ fn cgassign(c: *cgen, n: *node) void = {
let lc: *local = localfindnode(c, lhs.str);
if (lc != nil) {
if (istaggedtype(c, lc.tnode)) {
// #38b: an sret-classified tagged CALL
// result is in memory, not the cursor —
// an exact-type reassign sret's into the
// local's own slot; a widening receive
// needs mem-to-mem tag-remap (#40).
// Mirrors cstage cgen.c N_ASSIGN tagged
// arm + the generic sret receive.
let asret: i32 = 0;
if (n.rhs != nil) {
if (n.rhs.kind == nkind.N_CALL) {
asret = callsretsize(c, n.rhs);
};
};
if (asret > 0) {
let aru: *tinfo = n.rhs.type_: *tinfo;
for (aru != nil && aru.kind == tykind.TY_NAMED) {
aru = aru.under;
};
let alu: *tinfo = lc.tnode.type_: *tinfo;
for (alu != nil && alu.kind == tykind.TY_NAMED) {
alu = alu.under;
};
let aexact: bool = false;
if (aru != nil && aru == alu) { aexact = true; }
else {
if (typeeq(n.rhs.type_: *tinfo,
lc.tnode.type_: *tinfo)) {
aexact = true;
};
};
if (!aexact) {
let m40d: str = "#40: sret-class call result cannot be widened into a tagged slot (mem-to-mem widen unwired)\n";
os.write(2, m40d.ptr, m40d.len: u64);
os.exit(1);
};
c.sretdestoff = lc.off;
cgexpr(c, n.rhs);
c.sretdestoff = 0;
return;
};
let lsz: i32 = slotsize(c, lc.tnode);
cgwidentaggedstore(c, lc.tnode.type_: *tinfo,
n.rhs, "BP", lc.off, lsz);
return;
};
};
// #38b: sret receive into a tagged GLOBAL
// lvalue unwired (rule 7; cstage twin fatals).
if (lc == nil && n.rhs != nil) {
if (n.rhs.kind == nkind.N_CALL) {
let gru: *tinfo = lhs.type_: *tinfo;
for (gru != nil && gru.kind == tykind.TY_NAMED) {
gru = gru.under;
};
if (gru != nil && gru.kind == tykind.TY_TAGGED
&& callsretsize(c, n.rhs) > 0) {
let m38g: str = "#38b: sret receive into a tagged GLOBAL lvalue unwired\n";
os.write(2, m38g.ptr, m38g.len: u64);
os.exit(1);
};
};
};
};
};
};
@@ -28934,6 +29120,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
@@ -29709,6 +29952,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");
@@ -30126,10 +30393,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

View File

@@ -172,6 +172,25 @@ fn cgtrytupleshift(c: *cgen, n: *node) bool = {
// Success tag = 0 (#216 tracks the legacy/flag-aware success-tag
// divergence — out of scope here, success check stays `CMPQ $0`).
fn cgtryprop(c: *cgen, n: *node) void = {
// #38b residuals (rule 7): the cursor read below cannot see an
// sret-classified call result (AX = dest pointer), and the
// propagate-RET cannot speak an sret-classified enclosing return
// (the caller reads memory, not the cursor). #40-family follow-ups.
// Mirrors cstage cgen.c N_TRYPROP gates.
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_CALL) {
if (callsretsize(c, n.lhs) > 0) {
let m38p: str = "#38b: `?` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n";
os.write(2, m38p.ptr, m38p.len: u64);
os.exit(1);
};
};
};
if (sretretsize(c, c.fnret) > 0) {
let m38q: str = "#38b: `?` propagation into a >32B tagged return unwired (sret error-propagate is a #40-family follow-up)\n";
os.write(2, m38q.ptr, m38q.len: u64);
os.exit(1);
};
cgexpr(c, n.lhs);
// AX = tag. If non-zero, this is an error; pop frame and RET.
let cl: str = mklabel(c, "tryprop_ok");
@@ -290,6 +309,16 @@ fn cgtryprop(c: *cgen, n: *node) void = {
// cgtryunw — `e!` aborts on the error variant via exit(1). Legacy
// semantics (success tag = 0).
fn cgtryunw(c: *cgen, n: *node) void = {
// #38b residual (rule 7): see the cgtryprop twin.
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_CALL) {
if (callsretsize(c, n.lhs) > 0) {
let m38u: str = "#38b: `!` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n";
os.write(2, m38u.ptr, m38u.len: u64);
os.exit(1);
};
};
};
cgexpr(c, n.lhs);
let cl: str = mklabel(c, "tryunw_ok");
emitline("\tCMPQ\t$0, AX\n");
@@ -359,6 +388,18 @@ fn cgtypetest(c: *cgen, n: *node) void = {
// stored via *i32 in this context — direct assignment of the
// local works, indirection through &scrutoff drops sign bits.
let lhs: *node = n.lhs;
// #38b residual (rule 7): an sret-class call result leaves AX =
// dest pointer, not the tag — mem-based test is a #40-family
// follow-up. Mirrors cstage cgen.c N_TYPETEST gate.
if (lhs != nil) {
if (lhs.kind == nkind.N_CALL) {
if (callsretsize(c, lhs) > 0) {
let m38t: str = "#38b: `is` on an sret-class call result unwired (#40-family follow-up)\n";
os.write(2, m38t.ptr, m38t.len: u64);
os.exit(1);
};
};
};
let scrutoff: i32 = 0;
let scrutt: *node = nil;
if (lhs != nil) {
@@ -447,6 +488,18 @@ fn cgtypeassert(c: *cgen, n: *node) void = {
// str → (AX, BX). Mirrors cgmatch's slot-based value load.
// Slot resolution inlined; see cgtypetest comment.
let lhs: *node = n.lhs;
// #38b residual (rule 7): the spill below reads the cursor, which
// an sret-class call result never fills. Mirrors cstage cgen.c
// N_TYPEASSERT gate.
if (lhs != nil) {
if (lhs.kind == nkind.N_CALL) {
if (callsretsize(c, lhs) > 0) {
let m38a: str = "#38b: `as` on an sret-class call result unwired (#40-family follow-up)\n";
os.write(2, m38a.ptr, m38a.len: u64);
os.exit(1);
};
};
};
let scrutoff: i32 = 0;
let scrutt: *node = nil;
if (lhs != nil) {
@@ -1882,6 +1935,20 @@ fn cgmatch(c: *cgen, n: *node) void = {
scrutt = matchscrutt(c, scrut);
let spillsz: i32 = matchspillsz(c, scrutt);
scrutoff = localalloc(c, "@match_spill", spillsz, nil);
// #38b: sret-classified tagged call scrutinee — pass
// the scrut slot itself as the sret dest and skip the
// cursor spill; downstream tag dispatch / case-let
// binds already read the slot from memory. Mirrors
// cstage cgen.c N_MATCH.
let msret: i32 = 0;
if (scrut.kind == nkind.N_CALL) {
msret = callsretsize(c, scrut);
};
if (msret > 0) {
c.sretdestoff = scrutoff;
cgexpr(c, scrut);
c.sretdestoff = 0;
} else {
cgexpr(c, scrut);
emitline("\tMOVQ\tAX, ");
emitoff(scrutoff: i64);
@@ -1906,6 +1973,7 @@ fn cgmatch(c: *cgen, n: *node) void = {
emitline("(BP)\n");
};
};
};
};
};
let endl: str = mklabel(c, "match_end");
@@ -5243,12 +5311,68 @@ fn cgassign(c: *cgen, n: *node) void = {
let lc: *local = localfindnode(c, lhs.str);
if (lc != nil) {
if (istaggedtype(c, lc.tnode)) {
// #38b: an sret-classified tagged CALL
// result is in memory, not the cursor —
// an exact-type reassign sret's into the
// local's own slot; a widening receive
// needs mem-to-mem tag-remap (#40).
// Mirrors cstage cgen.c N_ASSIGN tagged
// arm + the generic sret receive.
let asret: i32 = 0;
if (n.rhs != nil) {
if (n.rhs.kind == nkind.N_CALL) {
asret = callsretsize(c, n.rhs);
};
};
if (asret > 0) {
let aru: *tinfo = n.rhs.type_: *tinfo;
for (aru != nil && aru.kind == tykind.TY_NAMED) {
aru = aru.under;
};
let alu: *tinfo = lc.tnode.type_: *tinfo;
for (alu != nil && alu.kind == tykind.TY_NAMED) {
alu = alu.under;
};
let aexact: bool = false;
if (aru != nil && aru == alu) { aexact = true; }
else {
if (typeeq(n.rhs.type_: *tinfo,
lc.tnode.type_: *tinfo)) {
aexact = true;
};
};
if (!aexact) {
let m40d: str = "#40: sret-class call result cannot be widened into a tagged slot (mem-to-mem widen unwired)\n";
os.write(2, m40d.ptr, m40d.len: u64);
os.exit(1);
};
c.sretdestoff = lc.off;
cgexpr(c, n.rhs);
c.sretdestoff = 0;
return;
};
let lsz: i32 = slotsize(c, lc.tnode);
cgwidentaggedstore(c, lc.tnode.type_: *tinfo,
n.rhs, "BP", lc.off, lsz);
return;
};
};
// #38b: sret receive into a tagged GLOBAL
// lvalue unwired (rule 7; cstage twin fatals).
if (lc == nil && n.rhs != nil) {
if (n.rhs.kind == nkind.N_CALL) {
let gru: *tinfo = lhs.type_: *tinfo;
for (gru != nil && gru.kind == tykind.TY_NAMED) {
gru = gru.under;
};
if (gru != nil && gru.kind == tykind.TY_TAGGED
&& callsretsize(c, n.rhs) > 0) {
let m38g: str = "#38b: sret receive into a tagged GLOBAL lvalue unwired\n";
os.write(2, m38g.ptr, m38g.len: u64);
os.exit(1);
};
};
};
};
};
};

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

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).

View File

@@ -15796,6 +15796,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).
@@ -16342,6 +16363,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"); };
@@ -17095,6 +17125,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 =
@@ -18653,6 +18695,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);
@@ -18716,6 +18769,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);
@@ -18904,7 +18966,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).
@@ -19970,6 +20032,25 @@ fn cgtrytupleshift(c: *cgen, n: *node) bool = {
// Success tag = 0 (#216 tracks the legacy/flag-aware success-tag
// divergence — out of scope here, success check stays `CMPQ $0`).
fn cgtryprop(c: *cgen, n: *node) void = {
// #38b residuals (rule 7): the cursor read below cannot see an
// sret-classified call result (AX = dest pointer), and the
// propagate-RET cannot speak an sret-classified enclosing return
// (the caller reads memory, not the cursor). #40-family follow-ups.
// Mirrors cstage cgen.c N_TRYPROP gates.
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_CALL) {
if (callsretsize(c, n.lhs) > 0) {
let m38p: str = "#38b: `?` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n";
os.write(2, m38p.ptr, m38p.len: u64);
os.exit(1);
};
};
};
if (sretretsize(c, c.fnret) > 0) {
let m38q: str = "#38b: `?` propagation into a >32B tagged return unwired (sret error-propagate is a #40-family follow-up)\n";
os.write(2, m38q.ptr, m38q.len: u64);
os.exit(1);
};
cgexpr(c, n.lhs);
// AX = tag. If non-zero, this is an error; pop frame and RET.
let cl: str = mklabel(c, "tryprop_ok");
@@ -20088,6 +20169,16 @@ fn cgtryprop(c: *cgen, n: *node) void = {
// cgtryunw — `e!` aborts on the error variant via exit(1). Legacy
// semantics (success tag = 0).
fn cgtryunw(c: *cgen, n: *node) void = {
// #38b residual (rule 7): see the cgtryprop twin.
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_CALL) {
if (callsretsize(c, n.lhs) > 0) {
let m38u: str = "#38b: `!` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n";
os.write(2, m38u.ptr, m38u.len: u64);
os.exit(1);
};
};
};
cgexpr(c, n.lhs);
let cl: str = mklabel(c, "tryunw_ok");
emitline("\tCMPQ\t$0, AX\n");
@@ -20157,6 +20248,18 @@ fn cgtypetest(c: *cgen, n: *node) void = {
// stored via *i32 in this context — direct assignment of the
// local works, indirection through &scrutoff drops sign bits.
let lhs: *node = n.lhs;
// #38b residual (rule 7): an sret-class call result leaves AX =
// dest pointer, not the tag — mem-based test is a #40-family
// follow-up. Mirrors cstage cgen.c N_TYPETEST gate.
if (lhs != nil) {
if (lhs.kind == nkind.N_CALL) {
if (callsretsize(c, lhs) > 0) {
let m38t: str = "#38b: `is` on an sret-class call result unwired (#40-family follow-up)\n";
os.write(2, m38t.ptr, m38t.len: u64);
os.exit(1);
};
};
};
let scrutoff: i32 = 0;
let scrutt: *node = nil;
if (lhs != nil) {
@@ -20245,6 +20348,18 @@ fn cgtypeassert(c: *cgen, n: *node) void = {
// str → (AX, BX). Mirrors cgmatch's slot-based value load.
// Slot resolution inlined; see cgtypetest comment.
let lhs: *node = n.lhs;
// #38b residual (rule 7): the spill below reads the cursor, which
// an sret-class call result never fills. Mirrors cstage cgen.c
// N_TYPEASSERT gate.
if (lhs != nil) {
if (lhs.kind == nkind.N_CALL) {
if (callsretsize(c, lhs) > 0) {
let m38a: str = "#38b: `as` on an sret-class call result unwired (#40-family follow-up)\n";
os.write(2, m38a.ptr, m38a.len: u64);
os.exit(1);
};
};
};
let scrutoff: i32 = 0;
let scrutt: *node = nil;
if (lhs != nil) {
@@ -21680,6 +21795,20 @@ fn cgmatch(c: *cgen, n: *node) void = {
scrutt = matchscrutt(c, scrut);
let spillsz: i32 = matchspillsz(c, scrutt);
scrutoff = localalloc(c, "@match_spill", spillsz, nil);
// #38b: sret-classified tagged call scrutinee — pass
// the scrut slot itself as the sret dest and skip the
// cursor spill; downstream tag dispatch / case-let
// binds already read the slot from memory. Mirrors
// cstage cgen.c N_MATCH.
let msret: i32 = 0;
if (scrut.kind == nkind.N_CALL) {
msret = callsretsize(c, scrut);
};
if (msret > 0) {
c.sretdestoff = scrutoff;
cgexpr(c, scrut);
c.sretdestoff = 0;
} else {
cgexpr(c, scrut);
emitline("\tMOVQ\tAX, ");
emitoff(scrutoff: i64);
@@ -21704,6 +21833,7 @@ fn cgmatch(c: *cgen, n: *node) void = {
emitline("(BP)\n");
};
};
};
};
};
let endl: str = mklabel(c, "match_end");
@@ -25041,12 +25171,68 @@ fn cgassign(c: *cgen, n: *node) void = {
let lc: *local = localfindnode(c, lhs.str);
if (lc != nil) {
if (istaggedtype(c, lc.tnode)) {
// #38b: an sret-classified tagged CALL
// result is in memory, not the cursor —
// an exact-type reassign sret's into the
// local's own slot; a widening receive
// needs mem-to-mem tag-remap (#40).
// Mirrors cstage cgen.c N_ASSIGN tagged
// arm + the generic sret receive.
let asret: i32 = 0;
if (n.rhs != nil) {
if (n.rhs.kind == nkind.N_CALL) {
asret = callsretsize(c, n.rhs);
};
};
if (asret > 0) {
let aru: *tinfo = n.rhs.type_: *tinfo;
for (aru != nil && aru.kind == tykind.TY_NAMED) {
aru = aru.under;
};
let alu: *tinfo = lc.tnode.type_: *tinfo;
for (alu != nil && alu.kind == tykind.TY_NAMED) {
alu = alu.under;
};
let aexact: bool = false;
if (aru != nil && aru == alu) { aexact = true; }
else {
if (typeeq(n.rhs.type_: *tinfo,
lc.tnode.type_: *tinfo)) {
aexact = true;
};
};
if (!aexact) {
let m40d: str = "#40: sret-class call result cannot be widened into a tagged slot (mem-to-mem widen unwired)\n";
os.write(2, m40d.ptr, m40d.len: u64);
os.exit(1);
};
c.sretdestoff = lc.off;
cgexpr(c, n.rhs);
c.sretdestoff = 0;
return;
};
let lsz: i32 = slotsize(c, lc.tnode);
cgwidentaggedstore(c, lc.tnode.type_: *tinfo,
n.rhs, "BP", lc.off, lsz);
return;
};
};
// #38b: sret receive into a tagged GLOBAL
// lvalue unwired (rule 7; cstage twin fatals).
if (lc == nil && n.rhs != nil) {
if (n.rhs.kind == nkind.N_CALL) {
let gru: *tinfo = lhs.type_: *tinfo;
for (gru != nil && gru.kind == tykind.TY_NAMED) {
gru = gru.under;
};
if (gru != nil && gru.kind == tykind.TY_TAGGED
&& callsretsize(c, n.rhs) > 0) {
let m38g: str = "#38b: sret receive into a tagged GLOBAL lvalue unwired\n";
os.write(2, m38g.ptr, m38g.len: u64);
os.exit(1);
};
};
};
};
};
};
@@ -28934,6 +29120,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
@@ -29709,6 +29952,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");
@@ -30126,10 +30393,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