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

@@ -106,6 +106,11 @@ static int cg_tagscr_sz;
* return value. No temporary in outer's frame. */
static int cg_sret_arg_off;
static int cg_sret_dest_off;
/* #220: caller-side dest for an sret receive into a GLOBAL lvalue. A
* BP-relative i32 offset (cg_sret_dest_off) can't name a top-level let,
* so the symbol name is carried instead and emitted as LEAQ name(SB),DI.
* Mutually exclusive with cg_sret_dest_off. */
static const char *cg_sret_dest_sym;
static int cg_sretscr_off;
static int cg_sretscr_sz;
static int cg_sret_forward;
@@ -4798,6 +4803,18 @@ cgexpr(Cg *c, Node *n, Local *locals)
cg_sret_dest_off = 0;
break;
}
/* #220: `g = f();` where g is a GLOBAL struct >24B.
* No BP slot to use as the sret dest, so route RDI
* to g's symbol address. Mirrors the str/slice
* global arm above (let_islet + LEAQ masym). The
* scalar fall-through below would emit a truncated
* 8-byte `MOVQ AX, g(SB)` and drop the struct body. */
if (let_islet(n->lhs->str)) {
cg_sret_dest_sym = n->lhs->str;
cgexpr(c, n->rhs, locals);
cg_sret_dest_sym = NULL;
break;
}
}
/* Struct local reassignment: `s = expr;` where s is
* a TY_STRUCT local of size <=24B. Two rhs shapes,
@@ -5738,12 +5755,19 @@ cgexpr(Cg *c, Node *n, Local *locals)
* left RDI free — we never popped a user arg into it. */
int sret_call_sz = 0;
int sret_call_off = 0;
const char *sret_dest_sym = NULL; /* #220 */
{
Type *ret = (cu && cu->kind == TY_FN)
? cu->ret : NULL;
sret_call_sz = cg_sret_retsize(ret);
}
if (sret_call_sz > 0) {
if (sret_call_sz > 0 && cg_sret_dest_sym != NULL) {
/* #220: GLOBAL dest — RDI gets LEAQ name(SB) below; no
* @sretscr slot needed (the callee writes the struct
* straight into g's storage). */
sret_dest_sym = cg_sret_dest_sym;
cg_sret_dest_sym = NULL;
} else if (sret_call_sz > 0) {
/* @sretscr is only needed when the result is dropped
* (no `let x = f();` receiver wired the call's dest into
* cg_sret_dest_off). Allocate first-use per #15/#26c
@@ -5953,6 +5977,10 @@ cgexpr(Cg *c, Node *n, Local *locals)
amem(D_BP, cg_sret_arg_off),
areg(D_DI));
cg_sret_forward = 0;
} else if (sret_dest_sym != NULL) {
/* #220: sret into a GLOBAL — RDI = &g(SB). */
ins2(c, A_LEAQ, masym(c, sret_dest_sym),
areg(D_DI));
} else {
ins2(c, A_LEAQ,
amem(D_BP, sret_call_off),
@@ -8947,6 +8975,7 @@ cgfn(Cg *c, FILE *out, Node *fn)
cg_tagscr_sz = 0;
cg_sret_arg_off = 0;
cg_sret_dest_off = 0;
cg_sret_dest_sym = NULL;
cg_sretscr_off = 0;
cg_sretscr_sz = 0;
cg_sret_forward = 0;