Files
ww/test/wcc/940_global_sret_run.c
Hojun-Cho 1175711021 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.
2026-05-30 00:46:57 +09:00

278 lines
8.8 KiB
C

/*
* 940_global_sret_run — project #220. sret (>24B by-value struct
* return) assigned into a GLOBAL lvalue must land the whole struct,
* not a truncated 8-byte store.
*
* Gate-blind family (sibling of #211): pre-fix BOTH stages emitted the
* SAME broken `MOVQ AX, g(SB)` (only the sret return pointer reached
* the global; the struct body, written to a scratch temp, was lost), so
* asm byte-id stayed GREEN while runtime was wrong — g.pos == 0 for the
* #94 io vtable cgoutstream pattern. The runtime rows below are the net.
*
* Surfacing case: the eFinal FLIP repointed cgen.ww's per-fn body output
* through a GLOBAL `let cgoutstream: memio.stream` (>24B) wired once via
* `cgoutstream = memio.dynamic()`; the truncated store left pos==0 so
* the buffered body never flushed and w6c_ww emitted prologue-only.
*
* The fix routes the sret dest pointer to the global's symbol address
* (LEAQ g(SB), DI) — zero-copy, the same discipline the local case
* already used (LEAQ off(BP), DI). Local rows pin no regression at the
* shared cgassign / cglet sret-receive sites.
*
* Each row runs on BOTH cstage (ww) and wwstage (ww_ww), and the
* generated asm is checked byte-identical (cs.s == ww.s, rule 10).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return -1;
}
struct row { const char *label; const char *src; int want; };
static const struct row rows[] = {
/* GLOBAL sret assign: `let g: quad;` at module scope, `g = mk()`
* in main. Pre-fix the global got only `MOVQ AX, g(SB)` and the
* field reads returned garbage. */
{ "global_assign",
"type quad = struct { a: i64, b: i64, c: i64, d: i64 };\n"
"fn mk(x: i64) quad = {\n"
" return quad { a = x, b = x + 1i64, c = x + 2i64, d = x + 3i64 };\n"
"};\n"
"let g: quad;\n"
"export fn main() i32 = {\n"
" g = mk(10i64);\n"
" if (g.a != 10i64) { return 1; };\n"
" if (g.b != 11i64) { return 2; };\n"
" if (g.c != 12i64) { return 3; };\n"
" if (g.d != 13i64) { return 4; };\n"
" return 0;\n"
"};\n",
0 },
/* GLOBAL sret with a BRANCHED callee (#105): the returned value
* is selected at runtime so a constant-fold coincidence can't mask
* the register/dest routing. */
{ "global_branched",
"type quad = struct { a: i64, b: i64, c: i64, d: i64 };\n"
"fn mk1() quad = { return quad { a = 1i64, b = 2i64, c = 3i64, d = 4i64 }; };\n"
"fn mk2() quad = { return quad { a = 50i64, b = 0i64, c = 0i64, d = 0i64 }; };\n"
"fn pick(w: i64) quad = {\n"
" if (w == 1i64) { return mk1(); };\n"
" return mk2();\n"
"};\n"
"let g: quad;\n"
"export fn main() i32 = {\n"
" g = pick(1i64);\n"
" if (g.a != 1i64) { return 1; };\n"
" if (g.b != 2i64) { return 2; };\n"
" if (g.c != 3i64) { return 3; };\n"
" if (g.d != 4i64) { return 4; };\n"
" return 0;\n"
"};\n",
0 },
/* GLOBAL mutated THROUGH A POINTER after the sret assign — the io
* vtable-callback shape that surfaced #220 (callback receives &g,
* mutates a field). */
{ "global_through_pointer",
"type quad = struct { a: i64, b: i64, c: i64, d: i64 };\n"
"fn mk(x: i64) quad = {\n"
" return quad { a = x, b = x + 1i64, c = x + 2i64, d = x + 3i64 };\n"
"};\n"
"fn bump(p: *quad) void = { p.a += 100i64; };\n"
"let g: quad;\n"
"export fn main() i32 = {\n"
" g = mk(7i64);\n"
" bump(&g);\n"
" if (g.a != 107i64) { return 1; };\n"
" if (g.b != 8i64) { return 2; };\n"
" if (g.c != 9i64) { return 3; };\n"
" if (g.d != 10i64) { return 4; };\n"
" return 0;\n"
"};\n",
0 },
/* LOCAL decl+assign regression: `let g: quad; g = mk();` routes
* through the same cgassign sret-receive site the fix touches. */
{ "local_assign_regression",
"type quad = struct { a: i64, b: i64, c: i64, d: i64 };\n"
"fn mk(x: i64) quad = {\n"
" return quad { a = x, b = x + 1i64, c = x + 2i64, d = x + 3i64 };\n"
"};\n"
"export fn main() i32 = {\n"
" let g: quad;\n"
" g = mk(20i64);\n"
" if (g.a != 20i64) { return 1; };\n"
" if (g.b != 21i64) { return 2; };\n"
" if (g.c != 22i64) { return 3; };\n"
" if (g.d != 23i64) { return 4; };\n"
" return 0;\n"
"};\n",
0 },
/* LOCAL let-init regression: `let g: quad = mk();` routes through
* cglet's sret-receive branch (the sister site). */
{ "local_init_regression",
"type quad = struct { a: i64, b: i64, c: i64, d: i64 };\n"
"fn mk(x: i64) quad = {\n"
" return quad { a = x, b = x + 1i64, c = x + 2i64, d = x + 3i64 };\n"
"};\n"
"export fn main() i32 = {\n"
" let g: quad = mk(30i64);\n"
" if (g.a != 30i64) { return 1; };\n"
" if (g.b != 31i64) { return 2; };\n"
" if (g.c != 32i64) { return 3; };\n"
" if (g.d != 33i64) { return 4; };\n"
" return 0;\n"
"};\n",
0 },
};
/* Compile the combined.ww (left next to the source by `ww build`) with
* both compilers and assert byte-identical asm. Returns 0 ok, 1 differ,
* -1 harness error. */
static int
byteid(const char *bin, const char *combined, const char *dir, int i)
{
char cs[256], ws[256], cmd[1024];
snprintf(cs, sizeof cs, "%s/bid_cs_%d.s", dir, i);
snprintf(ws, sizeof ws, "%s/bid_ww_%d.s", dir, i);
snprintf(cmd, sizeof cmd, "%s/w6c %s > %s", bin, combined, cs);
if (runwait(cmd) != 0) return -1;
snprintf(cmd, sizeof cmd, "%s/w6c_ww %s > %s", bin, combined, ws);
if (runwait(cmd) != 0) return -1;
snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs, ws);
int rc = runwait(cmd);
unlink(cs); unlink(ws);
return rc == 0 ? 0 : 1;
}
/* Build `r` with `driver` in `dir`, run the binary, return its exit
* code (or -1 on build failure). `combined_out` receives the path of
* the generated <stem>.combined.ww. */
static int
run_driver(const char *driver, const struct row *r, const char *dir,
int i, char *combined_out, size_t combined_sz)
{
char src[256], cmd[1024];
snprintf(src, sizeof src, "%s/gsret_%d.ww", dir, i);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "cd %s && %s build %s", dir, driver, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: build via %s failed\n",
r->label, driver);
return -1;
}
char outbin[256];
snprintf(outbin, sizeof outbin, "%s/gsret_%d", dir, i);
if (combined_out)
snprintf(combined_out, combined_sz,
"%s/gsret_%d.combined.ww", dir, i);
return runwait(outbin);
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[512];
if (bin[0] != '/') {
char cwd[256];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char cdrv[640], wdrv[640];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
char wwc[640];
snprintf(wwc, sizeof wwc, "%s/w6c_ww", bin);
int have_ww = (access(wdrv, X_OK) == 0);
int have_wwc = (access(wwc, X_OK) == 0);
char dir[] = "/tmp/gsret_XXXXXX";
if (mkdtemp(dir) == NULL) {
perror("mkdtemp");
return 1;
}
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0;
for (int i = 0; i < n; i++) {
char combined[300];
combined[0] = '\0';
/* cstage: build + run + capture combined.ww path. */
int gc = run_driver(cdrv, &rows[i], dir, i,
combined, sizeof combined);
total++;
if (gc != rows[i].want) {
fprintf(stderr, "global_sret_run[cstage][%s]: "
"exit=%d want=%d\n", rows[i].label, gc,
rows[i].want);
fail++;
}
/* byte-id (rule 10): cs.s == ww.s on the same combined.ww. */
if (have_wwc && combined[0]) {
total++;
int bid = byteid(bin, combined, dir, i);
if (bid != 0) {
fprintf(stderr, "global_sret_run[byteid][%s]: "
"%s\n", rows[i].label,
bid == 1 ? "cs.s != ww.s" : "harness error");
fail++;
}
}
/* wwstage: build + run. */
if (have_ww) {
int gw = run_driver(wdrv, &rows[i], dir, i, NULL, 0);
total++;
if (gw != rows[i].want) {
fprintf(stderr, "global_sret_run[wwstage][%s]: "
"exit=%d want=%d\n", rows[i].label, gw,
rows[i].want);
fail++;
}
}
char p[256];
snprintf(p, sizeof p, "%s/gsret_%d.ww", dir, i); unlink(p);
snprintf(p, sizeof p, "%s/gsret_%d", dir, i); unlink(p);
snprintf(p, sizeof p, "%s/gsret_%d.combined.ww", dir, i); unlink(p);
snprintf(p, sizeof p, "%s/gsret_%d.s", dir, i); unlink(p);
snprintf(p, sizeof p, "%s/gsret_%d.o", dir, i); unlink(p);
}
rmdir(dir);
if (!have_ww)
fprintf(stderr, "global_sret_run: skip wwstage (no %s)\n", wdrv);
if (fail) {
fprintf(stderr, "global_sret_run: %d/%d checks failed\n",
fail, total);
return 1;
}
printf("global_sret_run: %d/%d ok\n", total, total);
return 0;
}