w6c+wwstage: route sret dest to the global symbol on struct-return into a global (#220)

Assigning a >24B by-value struct-return into a GLOBAL lvalue dropped the
struct body: the sret dest was routed to a BP scratch temp and only the
8-byte return pointer was stored (`MOVQ AX, g(SB)`); the callee wrote the full
struct to the scratch, which never reached the global. A BP-relative dest
offset cannot name a global symbol. Pre-existing GATE-BLIND silent miscompile
— both stages emit the same broken store, so byte-id (990-997) stays green
while runtime is wrong — latent until the eFinal io surface put a global
`cgoutstream: memio.stream` (>24B) on the path, where it made cgen.ww's
self-built w6c_ww buffer every function body into a corrupt global (pos stayed
0) and emit prologue-only output.

Fix, both stages, byte-identical: route the sret dest pointer to the global
symbol so the callee writes the full struct through RDI straight into the
global. cstage adds cg_sret_dest_sym, mirroring the existing str/slice global
arm (skip the @sretscr scratch, emit `LEAQ masym(sym), DI`). wwstage carries
the lhs IDENT node (sretdestnode) and emits `LEAQ name(SB), DI` via emitsymname
— identical to cstage's symbol mangling, verified cs.s==ww.s on the probe and
across 990-997. #211-family (by-value struct + global/pointer), but a distinct
site: the cstage assignment-store into a global, not the wwstage call-return.

N_LET-global static-init (`let g: T = mk()` at top level) is a separate,
independently-broken path (#221) — link-fails for init-via-call, returns 0 for
constant init — not the sret-receive gap and not on the eFinal path; deferred.

test/wcc/940_global_sret_run: global assign (plus a branched callee to defeat
const-fold), through-pointer mutation (the io vtable-callback shape that
surfaced this), and local-init/assign regressions — runtime asserts on both
stages (the net, since byte-id is gate-blind here) plus cs.s==ww.s.
Discrimination confirmed by revert+rebuild: with the global arm disabled,
global_assign emits the truncated store and exits 1.
This commit is contained in:
2026-05-30 00:46:57 +09:00
parent 176904dffc
commit 1175711021
7 changed files with 446 additions and 10 deletions

View File

@@ -22365,14 +22365,20 @@ fn cgcall(c: *cgen, n: *node) void = {
// slot, sized at first use per #15/#26c.
let sretcs: i32 = callsretsize(c, n);
let sretcalloff: i32 = 0;
// #220: GLOBAL dest — RDI gets `LEAQ name(SB)` below; no @sretscr
// slot (the callee writes the struct straight into g's storage).
let sretdestn: *node = nil;
if (sretcs > 0) {
if (c.sretdestoff != 0) {
if (c.sretdestnode != nil) {
sretdestn = c.sretdestnode;
c.sretdestnode = nil;
} else { if (c.sretdestoff != 0) {
sretcalloff = c.sretdestoff;
c.sretdestoff = 0;
} else {
sretcalloff = localadd(c, "@sretscr",
sretcs, nil);
};
};};
};
// Pop forward. Float args were pushed as 8 bytes from X0 via
// SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else
@@ -22644,11 +22650,16 @@ fn cgcall(c: *cgen, n: *node) void = {
emitoff(sretargoff: i64);
emitline("(BP), DI\n");
c.sretforward = 0;
} else { if (sretdestn != nil) {
// #220: sret into a GLOBAL — RDI = &g(SB).
emitline("\tLEAQ\t");
emitsymname(c, sretdestn.str);
emitline("(SB), DI\n");
} else {
emitline("\tLEAQ\t");
emitoff(sretcalloff: i64);
emitline("(BP), DI\n");
};
};};
};
if (isfnptrcall) {
// Load fn-ptr field value into AX; CALL AX. We emit the
@@ -25084,6 +25095,30 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("\tX1, (CX)\n");
return;
};
// #220: `g = f();` where g is a GLOBAL struct >24B.
// No BP slot for the sret dest, so route RDI to g's
// symbol; the callee writes the struct straight into
// g's storage. The scalar store below would emit a
// truncated `MOVQ AX, g(SB)` and drop the body. Mirror
// of the C cgen N_ASSIGN global arm (cmd/w6c/cgen.c).
if (n.op == tkind.TK_ASSIGN && n.rhs != nil
&& n.rhs.kind == nkind.N_CALL && lvftn != nil) {
if (lvftn.kind == nkind.N_TNAME) {
let gsi: *structinfo = structlookup(c, lvftn.str);
if (gsi != nil) {
let gsz: i32 = structabisize(gsi);
if (gsz > 24) {
let rscs: i32 = callsretsize(c, n.rhs);
if (rscs > 0) {
c.sretdestnode = lhs;
cgexpr(c, n.rhs);
c.sretdestnode = nil;
return;
};
};
};
};
};
cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) {
// str/slice top-level let: str IS []u8, so both store the
@@ -28704,6 +28739,11 @@ type cgen = struct {
// cgenexpr resolve `@sretarg` via localfind when they need the
// saved RDI.
sretdestoff: i32,
// #220: sret receive into a GLOBAL lvalue. A BP-relative i32
// (sretdestoff) can't name a top-level let, so the lhs IDENT node
// is carried and emitted as `LEAQ name(SB), DI`. nil means no
// global receiver wired; mutually exclusive with sretdestoff.
sretdestnode: *node,
sretforward: i32,
};
@@ -28727,6 +28767,7 @@ fn cgeninit(c: *cgen) void = {
c.labelseq = 0;
c.varargseq = 0;
c.sretdestoff = 0;
c.sretdestnode = nil;
c.sretforward = 0;
// Note: strlit_seq, strlits, ffis are *not* reset here; they
// persist across cgfn calls within one file. cgfile resets them

View File

@@ -505,6 +505,11 @@ type cgen = struct {
// cgenexpr resolve `@sretarg` via localfind when they need the
// saved RDI.
sretdestoff: i32,
// #220: sret receive into a GLOBAL lvalue. A BP-relative i32
// (sretdestoff) can't name a top-level let, so the lhs IDENT node
// is carried and emitted as `LEAQ name(SB), DI`. nil means no
// global receiver wired; mutually exclusive with sretdestoff.
sretdestnode: *node,
sretforward: i32,
};
@@ -528,6 +533,7 @@ fn cgeninit(c: *cgen) void = {
c.labelseq = 0;
c.varargseq = 0;
c.sretdestoff = 0;
c.sretdestnode = nil;
c.sretforward = 0;
// Note: strlit_seq, strlits, ffis are *not* reset here; they
// persist across cgfn calls within one file. cgfile resets them

View File

@@ -4002,14 +4002,20 @@ fn cgcall(c: *cgen, n: *node) void = {
// slot, sized at first use per #15/#26c.
let sretcs: i32 = callsretsize(c, n);
let sretcalloff: i32 = 0;
// #220: GLOBAL dest — RDI gets `LEAQ name(SB)` below; no @sretscr
// slot (the callee writes the struct straight into g's storage).
let sretdestn: *node = nil;
if (sretcs > 0) {
if (c.sretdestoff != 0) {
if (c.sretdestnode != nil) {
sretdestn = c.sretdestnode;
c.sretdestnode = nil;
} else { if (c.sretdestoff != 0) {
sretcalloff = c.sretdestoff;
c.sretdestoff = 0;
} else {
sretcalloff = localadd(c, "@sretscr",
sretcs, nil);
};
};};
};
// Pop forward. Float args were pushed as 8 bytes from X0 via
// SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else
@@ -4281,11 +4287,16 @@ fn cgcall(c: *cgen, n: *node) void = {
emitoff(sretargoff: i64);
emitline("(BP), DI\n");
c.sretforward = 0;
} else { if (sretdestn != nil) {
// #220: sret into a GLOBAL — RDI = &g(SB).
emitline("\tLEAQ\t");
emitsymname(c, sretdestn.str);
emitline("(SB), DI\n");
} else {
emitline("\tLEAQ\t");
emitoff(sretcalloff: i64);
emitline("(BP), DI\n");
};
};};
};
if (isfnptrcall) {
// Load fn-ptr field value into AX; CALL AX. We emit the
@@ -6721,6 +6732,30 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("\tX1, (CX)\n");
return;
};
// #220: `g = f();` where g is a GLOBAL struct >24B.
// No BP slot for the sret dest, so route RDI to g's
// symbol; the callee writes the struct straight into
// g's storage. The scalar store below would emit a
// truncated `MOVQ AX, g(SB)` and drop the body. Mirror
// of the C cgen N_ASSIGN global arm (cmd/w6c/cgen.c).
if (n.op == tkind.TK_ASSIGN && n.rhs != nil
&& n.rhs.kind == nkind.N_CALL && lvftn != nil) {
if (lvftn.kind == nkind.N_TNAME) {
let gsi: *structinfo = structlookup(c, lvftn.str);
if (gsi != nil) {
let gsz: i32 = structabisize(gsi);
if (gsz > 24) {
let rscs: i32 = callsretsize(c, n.rhs);
if (rscs > 0) {
c.sretdestnode = lhs;
cgexpr(c, n.rhs);
c.sretdestnode = nil;
return;
};
};
};
};
};
cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) {
// str/slice top-level let: str IS []u8, so both store the

View File

@@ -22365,14 +22365,20 @@ fn cgcall(c: *cgen, n: *node) void = {
// slot, sized at first use per #15/#26c.
let sretcs: i32 = callsretsize(c, n);
let sretcalloff: i32 = 0;
// #220: GLOBAL dest — RDI gets `LEAQ name(SB)` below; no @sretscr
// slot (the callee writes the struct straight into g's storage).
let sretdestn: *node = nil;
if (sretcs > 0) {
if (c.sretdestoff != 0) {
if (c.sretdestnode != nil) {
sretdestn = c.sretdestnode;
c.sretdestnode = nil;
} else { if (c.sretdestoff != 0) {
sretcalloff = c.sretdestoff;
c.sretdestoff = 0;
} else {
sretcalloff = localadd(c, "@sretscr",
sretcs, nil);
};
};};
};
// Pop forward. Float args were pushed as 8 bytes from X0 via
// SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else
@@ -22644,11 +22650,16 @@ fn cgcall(c: *cgen, n: *node) void = {
emitoff(sretargoff: i64);
emitline("(BP), DI\n");
c.sretforward = 0;
} else { if (sretdestn != nil) {
// #220: sret into a GLOBAL — RDI = &g(SB).
emitline("\tLEAQ\t");
emitsymname(c, sretdestn.str);
emitline("(SB), DI\n");
} else {
emitline("\tLEAQ\t");
emitoff(sretcalloff: i64);
emitline("(BP), DI\n");
};
};};
};
if (isfnptrcall) {
// Load fn-ptr field value into AX; CALL AX. We emit the
@@ -25084,6 +25095,30 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("\tX1, (CX)\n");
return;
};
// #220: `g = f();` where g is a GLOBAL struct >24B.
// No BP slot for the sret dest, so route RDI to g's
// symbol; the callee writes the struct straight into
// g's storage. The scalar store below would emit a
// truncated `MOVQ AX, g(SB)` and drop the body. Mirror
// of the C cgen N_ASSIGN global arm (cmd/w6c/cgen.c).
if (n.op == tkind.TK_ASSIGN && n.rhs != nil
&& n.rhs.kind == nkind.N_CALL && lvftn != nil) {
if (lvftn.kind == nkind.N_TNAME) {
let gsi: *structinfo = structlookup(c, lvftn.str);
if (gsi != nil) {
let gsz: i32 = structabisize(gsi);
if (gsz > 24) {
let rscs: i32 = callsretsize(c, n.rhs);
if (rscs > 0) {
c.sretdestnode = lhs;
cgexpr(c, n.rhs);
c.sretdestnode = nil;
return;
};
};
};
};
};
cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) {
// str/slice top-level let: str IS []u8, so both store the
@@ -28704,6 +28739,11 @@ type cgen = struct {
// cgenexpr resolve `@sretarg` via localfind when they need the
// saved RDI.
sretdestoff: i32,
// #220: sret receive into a GLOBAL lvalue. A BP-relative i32
// (sretdestoff) can't name a top-level let, so the lhs IDENT node
// is carried and emitted as `LEAQ name(SB), DI`. nil means no
// global receiver wired; mutually exclusive with sretdestoff.
sretdestnode: *node,
sretforward: i32,
};
@@ -28727,6 +28767,7 @@ fn cgeninit(c: *cgen) void = {
c.labelseq = 0;
c.varargseq = 0;
c.sretdestoff = 0;
c.sretdestnode = nil;
c.sretforward = 0;
// Note: strlit_seq, strlits, ffis are *not* reset here; they
// persist across cgfn calls within one file. cgfile resets them