diff --git a/Makefile b/Makefile index 3d5751bf..ad84013a 100644 --- a/Makefile +++ b/Makefile @@ -252,6 +252,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_nested_union_widen_run \ $(BIN)/test_sret_struct_return \ $(BIN)/test_sret_struct_return_run \ + $(BIN)/test_global_sret_run \ $(BIN)/test_sret_narrow_field \ $(BIN)/test_sret_narrow_field_run \ $(BIN)/test_match_slice_variant \ @@ -846,6 +847,12 @@ $(BIN)/test_sret_struct_return_run: test/wcc/925_sret_struct_return_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_global_sret_run: test/wcc/940_global_sret_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_sret_narrow_field: test/wcc/730_sret_narrow_field.c \ $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 32faf6ab..ad54d9f4 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index fa8cc679..dd7c60db 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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 diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index f09689f6..786f66f3 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -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 diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index fc8180b2..4e497221 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -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 diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 9be928cc..407d6f5d 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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 diff --git a/test/wcc/940_global_sret_run.c b/test/wcc/940_global_sret_run.c new file mode 100644 index 00000000..2e8c56b0 --- /dev/null +++ b/test/wcc/940_global_sret_run.c @@ -0,0 +1,277 @@ +/* + * 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 +#include +#include +#include +#include +#include + +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 .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; +}