diff --git a/Makefile b/Makefile index 2b55b0ac..0dd3444d 100644 --- a/Makefile +++ b/Makefile @@ -241,6 +241,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tagged_widen_f64 \ $(BIN)/test_match_spill_pointer_payload \ $(BIN)/test_struct_byval_param \ + $(BIN)/test_struct_multi_return_scratch \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -498,6 +499,12 @@ $(BIN)/test_struct_byval_param: test/wcc/717_struct_byval_param.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_struct_multi_return_scratch: test/wcc/718_struct_multi_return_scratch.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_use_promote_alias: test/wcc/699_use_promote_alias.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 0e65c974..b4549169 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -32,6 +32,13 @@ static Type *cg_ret_type; * needs this to allocate scratch slots (e.g. match bindings) without * threading it through every signature. */ static int *cg_frame; +/* Per-fn @retscr offset (single-slot SSoT, task #14). Returns are + * terminal: at most one return path fires per call, so all retscr + * uses share one slot. Mirrors wwstage's `@retscr` convention + * (cgen.ww localadd '@'-prefix dedup; #38 ratified single-slot + * semantics for synthetic scratches). 0 means "not yet allocated"; + * negative offsets returned by local_alloc are the live value. */ +static int cg_retscr; /* Per-fn defer stack: pushed in registration order, popped (emitted) * in reverse at each return. */ @@ -6322,11 +6329,23 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) * stores; struct ident: word copy; * tagged subset: copy + tag remap. * 4th word in R8 covers slice payload - * variants (slot >= 32B). */ + * variants (slot >= 32B). + * + * Single-slot @retscr (#14): returns are + * terminal, so all retscr uses in this fn + * share one slot. Pre-fix per-site fresh + * allocation over-grew the frame by sz + * bytes per extra return. */ int sz = (int)rt->size; - const char *scrn = mklabel(c, "retscr"); - int scr = local_alloc(c, locals, scrn, - sz, cg_frame); + int scr; + if (cg_retscr != 0) { + scr = cg_retscr; + } else { + const char *scrn = mklabel(c, "retscr"); + scr = local_alloc(c, locals, scrn, + sz, cg_frame); + cg_retscr = scr; + } ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); for (int k = 0; k < sz; k += 8) ins2(c, A_MOVQ, areg(D_AX), @@ -6372,9 +6391,17 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) && (n->lhs->kind == N_IDENT || n->lhs->kind == N_STRUCTLIT)) { int sz = (int)rt->size; - const char *scrn = mklabel(c, "retscr"); - int scr = local_alloc(c, locals, scrn, 24, - cg_frame); + /* Single-slot @retscr (#14): see tagged arm + * above for rationale. */ + int scr; + if (cg_retscr != 0) { + scr = cg_retscr; + } else { + const char *scrn = mklabel(c, "retscr"); + scr = local_alloc(c, locals, scrn, 24, + cg_frame); + cg_retscr = scr; + } ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, scr + 0)); @@ -6782,6 +6809,7 @@ cgfn(Cg *c, FILE *out, Node *fn) ndefers = 0; nloops = 0; cg_ret_type = fn->type ? fn->type->ret : NULL; + cg_retscr = 0; int frame = 0; Local *locals = NULL; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index fe095a1e..860249a0 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -17935,6 +17935,15 @@ type cgen = struct { // big enough for every later user. Single source of truth — pins // rob's "scan + emit lockstep" invariant. Reset per cgfn. tagscrsz: i32, + // Live @retscr offset (#14). c.locals-based `@`-prefix dedup in + // localadd is unwound by cgblock save/restore (post-#27), so a + // second `return` in a sibling/outer block reallocates a fresh + // slot — emit grew the frame past what scanlocals reserved, and + // the stomp landed below SP. retscroff is the persistent SSoT: + // 0 means "not yet allocated"; first emit-site sets it, every + // later emit reuses. Mirrors c.tagscrsz pattern (#38) but tracks + // offset, not size (per-fn return type is fixed, so size is too). + retscroff: i32, }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. @@ -17957,6 +17966,7 @@ fn cgeninit(c: *cgen, a: *arena) void = { c.labelseq = 0; c.varargseq = 0; c.tagscrsz = 0; + c.retscroff = 0; // Note: strlit_seq, strlits, ffis are *not* reset here; they // persist across cgfn calls within one file. cgfile resets them // at the start of each compilation unit. @@ -18023,8 +18033,23 @@ fn localadd(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { // reservation and emit-time allocation stay in sync. The // `@`-prefix carve-out preserves that contract; user names // can never start with `@` (lexer-rejected). + // + // @retscr (#14) routes through c.retscroff instead of c.locals. + // The c.locals-based dedup is unwound by cgblock save/restore + // (post-#27): a return inside an `if` block adds @retscr to + // c.locals; on block exit, c.locals reverts and a sibling/outer + // return reallocates a fresh slot. Scan had reserved one slot; + // emit grew the frame past the reservation and the second + // site's writes landed below SP. c.retscroff is per-fn state + // that survives cgblock save/restore and pins single-slot. if (name.len > 0) { if (name[0] == 64u8) { // '@' + if (streq(name, "@retscr")) { + if (c.retscroff != 0) { return c.retscroff; }; + let off: i32 = localalloc(c, name, sz, tnode); + c.retscroff = off; + return off; + }; let cur: *local = c.locals; for (cur != nil) { let cn: str = cur.name; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index a6d1040b..a380f499 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -422,6 +422,15 @@ type cgen = struct { // big enough for every later user. Single source of truth — pins // rob's "scan + emit lockstep" invariant. Reset per cgfn. tagscrsz: i32, + // Live @retscr offset (#14). c.locals-based `@`-prefix dedup in + // localadd is unwound by cgblock save/restore (post-#27), so a + // second `return` in a sibling/outer block reallocates a fresh + // slot — emit grew the frame past what scanlocals reserved, and + // the stomp landed below SP. retscroff is the persistent SSoT: + // 0 means "not yet allocated"; first emit-site sets it, every + // later emit reuses. Mirrors c.tagscrsz pattern (#38) but tracks + // offset, not size (per-fn return type is fixed, so size is too). + retscroff: i32, }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. @@ -444,6 +453,7 @@ fn cgeninit(c: *cgen, a: *arena) void = { c.labelseq = 0; c.varargseq = 0; c.tagscrsz = 0; + c.retscroff = 0; // Note: strlit_seq, strlits, ffis are *not* reset here; they // persist across cgfn calls within one file. cgfile resets them // at the start of each compilation unit. @@ -510,8 +520,23 @@ fn localadd(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { // reservation and emit-time allocation stay in sync. The // `@`-prefix carve-out preserves that contract; user names // can never start with `@` (lexer-rejected). + // + // @retscr (#14) routes through c.retscroff instead of c.locals. + // The c.locals-based dedup is unwound by cgblock save/restore + // (post-#27): a return inside an `if` block adds @retscr to + // c.locals; on block exit, c.locals reverts and a sibling/outer + // return reallocates a fresh slot. Scan had reserved one slot; + // emit grew the frame past the reservation and the second + // site's writes landed below SP. c.retscroff is per-fn state + // that survives cgblock save/restore and pins single-slot. if (name.len > 0) { if (name[0] == 64u8) { // '@' + if (streq(name, "@retscr")) { + if (c.retscroff != 0) { return c.retscroff; }; + let off: i32 = localalloc(c, name, sz, tnode); + c.retscroff = off; + return off; + }; let cur: *local = c.locals; for (cur != nil) { let cn: str = cur.name; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 3b9f28cd..22fa5040 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -17935,6 +17935,15 @@ type cgen = struct { // big enough for every later user. Single source of truth — pins // rob's "scan + emit lockstep" invariant. Reset per cgfn. tagscrsz: i32, + // Live @retscr offset (#14). c.locals-based `@`-prefix dedup in + // localadd is unwound by cgblock save/restore (post-#27), so a + // second `return` in a sibling/outer block reallocates a fresh + // slot — emit grew the frame past what scanlocals reserved, and + // the stomp landed below SP. retscroff is the persistent SSoT: + // 0 means "not yet allocated"; first emit-site sets it, every + // later emit reuses. Mirrors c.tagscrsz pattern (#38) but tracks + // offset, not size (per-fn return type is fixed, so size is too). + retscroff: i32, }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. @@ -17957,6 +17966,7 @@ fn cgeninit(c: *cgen, a: *arena) void = { c.labelseq = 0; c.varargseq = 0; c.tagscrsz = 0; + c.retscroff = 0; // Note: strlit_seq, strlits, ffis are *not* reset here; they // persist across cgfn calls within one file. cgfile resets them // at the start of each compilation unit. @@ -18023,8 +18033,23 @@ fn localadd(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { // reservation and emit-time allocation stay in sync. The // `@`-prefix carve-out preserves that contract; user names // can never start with `@` (lexer-rejected). + // + // @retscr (#14) routes through c.retscroff instead of c.locals. + // The c.locals-based dedup is unwound by cgblock save/restore + // (post-#27): a return inside an `if` block adds @retscr to + // c.locals; on block exit, c.locals reverts and a sibling/outer + // return reallocates a fresh slot. Scan had reserved one slot; + // emit grew the frame past the reservation and the second + // site's writes landed below SP. c.retscroff is per-fn state + // that survives cgblock save/restore and pins single-slot. if (name.len > 0) { if (name[0] == 64u8) { // '@' + if (streq(name, "@retscr")) { + if (c.retscroff != 0) { return c.retscroff; }; + let off: i32 = localalloc(c, name, sz, tnode); + c.retscroff = off; + return off; + }; let cur: *local = c.locals; for (cur != nil) { let cn: str = cur.name; diff --git a/test/wcc/718_struct_multi_return_scratch.c b/test/wcc/718_struct_multi_return_scratch.c new file mode 100644 index 00000000..cd9f9966 --- /dev/null +++ b/test/wcc/718_struct_multi_return_scratch.c @@ -0,0 +1,448 @@ +/* + * 718_struct_multi_return_scratch — single-slot @retscr SSoT across + * both stages for struct-by-value return scratch (task #14). + * + * Pre-fix: + * - cstage cgreturn struct arm called local_alloc per return site + * (mklabel("retscr") + local_alloc grows cg_frame). N return sites + * reserved N×scratch_size bytes — over-allocation, but safe. + * - wwstage scanlocals already deduped via scanseenmark("@retscr") + * → frame reserved a single 24B slot. Emit-time localadd("@retscr") + * was supposed to dedup via the `@`-prefix path that walks + * c.locals, but cgblock save/restore (post-#27) unwound the + * @retscr stub on block exit. The second `return r` (outside the + * if-body's block) hit localadd with c.locals lacking @retscr → + * fell to localalloc → fresh slot, growing c.frame past the + * scan-reserved bound. Frame size (taken from scan) was correct + * for ONE slot but the emit code referenced TWO slots — the + * second site's stores landed BELOW SP. + * + * That under-allocation was a silent stomp-on-OS-stack: signal + * delivery / interrupt in the second-return window would clobber + * the scratch writes. Bootstrap byte-id survived only because + * nothing fired in those windows during self-compile. + * + * Fix (#14): + * - cstage: cg_retscr static (per-fn, reset in cgfn). First retscr + * allocation runs the existing local_alloc path AND stores the + * offset; subsequent uses reuse cg_retscr. Returns are terminal, + * so all retscr uses in a fn share one slot — single-slot is + * structurally correct, not "best-effort merge". + * - wwstage: c.retscroff i32 field. localadd's `@`-prefix dedup + * special-cases "@retscr" to consult c.retscroff (set on first + * emit, reused after). c.retscroff survives cgblock save/restore. + * + * Polarity catalog: + * - #9 wwstage OVER (tagged return slot sized 16B for 1-word + * payload; fixed by sizing match-spill to scrutinee). + * - #11 wwstage UNDER (struct-by-value param decompose missed + * user-defined TY_STRUCT branch; fixed by adding it). + * - #14 wwstage UNDER (struct multi-return @retscr stomp post-#27; + * emit/scan disagreement on `@`-prefix dedup across blocks; + * fixed by retscroff SSoT). cstage was per-site-fresh — + * wasteful-but-safe, aligned UP to single-slot for ABI + * consistency with wwstage's now-correct enforcement, not + * for correctness. + * + * What this test pins: + * 1. Asm byte-identity between cstage and wwstage on multi-return + * and single-return struct-return fns (rows 1-3). + * 2. **Negative**: no `.s` instruction references `-N(BP)` with + * N > frame-size on any multi-struct-return row. This is the + * stomp regression sentinel — a future emit/scan disagreement + * that re-introduces below-SP writes would slip past byte-id + * alone (both stages could stomp the same way and stay + * byte-identical). + * 3. Runtime: each row's main returns 0 only when both return + * arms of the fn-under-test produce the correct value. + * 4. Regression guard (row 4): a tagged-return multi-return shape + * uses a different scratch family (@tagscr, NOT @retscr) and + * must stay byte-identical pre- and post-fix. + */ +#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; + int frame; /* expected `TEXT f,$N` — 0 = don't check */ + int max_off; /* assert no `-K(BP)` with K > max_off — 0 = don't check */ + int check_byte_id; /* 1 = assert cstage-vs-wwstage asm byte-identical */ +}; + +static const struct row rows[] = { + /* 1. Smallest divergent shape: two return sites returning a + * 16B struct. Pre-fix cstage $96, wwstage $64 (stomp). + * Post-fix: both $64, no offset deeper than -64. */ + { "two_returns_16B", + "type inst = struct { sec: i64, nsec: i64 };\n" + "fn f(i: inst, x: i64) inst = {\n" + " let r: inst;\n" + " if (x > 0i64) {\n" + " r.sec = i.sec + x;\n" + " r.nsec = i.nsec + x;\n" + " return r;\n" + " };\n" + " r.sec = i.sec - x;\n" + " r.nsec = i.nsec - x;\n" + " return r;\n" + "};\n" + "fn main() i32 = {\n" + " let i: inst = inst { sec = 10i64, nsec = 20i64 };\n" + " let p: inst = f(i, 5i64);\n" + " if (p.sec != 15i64) { return 1; };\n" + " if (p.nsec != 25i64) { return 2; };\n" + " let q: inst = f(i, -3i64);\n" + " if (q.sec != 13i64) { return 3; };\n" + " if (q.nsec != 23i64) { return 4; };\n" + " return 0;\n" + "};\n", + 0, + /* frame: i(16) + x(8) + r(16) + retscr(24) = 64 */ + 64, + 64, + 1 }, + /* 2. Three return sites returning a 16B struct. Pre-fix cstage + * would have grown the frame by another 24B (to $112 or so); + * wwstage would have stomped TWO additional 24B slots below SP. + * Post-fix: still $64 — single slot across three sites. */ + { "three_returns_16B", + "type inst = struct { sec: i64, nsec: i64 };\n" + "fn f(i: inst, k: i32) inst = {\n" + " let r: inst;\n" + " if (k == 1i32) {\n" + " r.sec = i.sec + 1i64;\n" + " r.nsec = i.nsec + 1i64;\n" + " return r;\n" + " };\n" + " if (k == 2i32) {\n" + " r.sec = i.sec * 2i64;\n" + " r.nsec = i.nsec * 2i64;\n" + " return r;\n" + " };\n" + " r.sec = i.sec;\n" + " r.nsec = i.nsec;\n" + " return r;\n" + "};\n" + "fn main() i32 = {\n" + " let i: inst = inst { sec = 10i64, nsec = 20i64 };\n" + " let a: inst = f(i, 1i32);\n" + " if (a.sec != 11i64) { return 1; };\n" + " if (a.nsec != 21i64) { return 2; };\n" + " let b: inst = f(i, 2i32);\n" + " if (b.sec != 20i64) { return 3; };\n" + " if (b.nsec != 40i64) { return 4; };\n" + " let c: inst = f(i, 9i32);\n" + " if (c.sec != 10i64) { return 5; };\n" + " if (c.nsec != 20i64) { return 6; };\n" + " return 0;\n" + "};\n", + 0, + /* frame: i(16) + k(8) + r(16) + retscr(24) = 64 */ + 64, + 64, + /* Byte-id disabled: pre-existing label-counter skew between + * stages for nested-if shapes (cstage's _ct_/_ce_/_end_ seq + * leads wwstage by one). Filed as #15 (sister of #14); not + * introduced by #14, so the disable is scoped to byte-id + * only — frame+stomp asserts still pin the #14 invariant. */ + 0 }, + /* 3. Single-return regression guard. Frame should match the + * multi-return shape — i.e. the fix doesn't perturb the + * one-return base case (still allocates the retscr slot + * exactly once). */ + { "single_return_16B", + "type inst = struct { sec: i64, nsec: i64 };\n" + "fn f(i: inst) inst = {\n" + " let r: inst;\n" + " r.sec = i.sec + 1i64;\n" + " r.nsec = i.nsec + 1i64;\n" + " return r;\n" + "};\n" + "fn main() i32 = {\n" + " let i: inst = inst { sec = 100i64, nsec = 200i64 };\n" + " let r: inst = f(i);\n" + " if (r.sec != 101i64) { return 1; };\n" + " if (r.nsec != 201i64) { return 2; };\n" + " return 0;\n" + "};\n", + 0, + /* frame: i(16) + r(16) + retscr(24) = 56 -> aligned to 64 */ + 64, + 64, + 1 }, + /* 4. Tagged-return multi-return — orthogonal scratch family + * (@tagscr, not @retscr). Pin that byte-identity holds; the + * #14 fix does NOT alter this shape's frame or body. If a + * future change accidentally routes tagged-return through + * @retscr, this row catches the divergence. */ + { "tagged_multi_return", + "fn f(k: i32) (i64 | i32) = {\n" + " if (k > 0i32) { return 1i64; };\n" + " return 2i32;\n" + "};\n" + "fn main() i32 = {\n" + " let r: (i64 | i32) = f(5i32);\n" + " match (r) {\n" + " case let v: i64 => if (v != 1i64) { return 1; };\n" + " case let v: i32 => return 2;\n" + " };\n" + " let s: (i64 | i32) = f(-1i32);\n" + " match (s) {\n" + " case let v: i64 => return 3;\n" + " case let v: i32 => if (v != 2i32) { return 4; };\n" + " };\n" + " return 0;\n" + "};\n", + 0, + /* Frame/max_off depend on the tagged ABI shape; pin byte-id + * only, not specific values. The negative-offset bound here + * still catches a stomp regression because byte-identity + * forces both stages to agree, and if a future emit/scan + * disagreement appears it'll surface as either a frame + * mismatch OR a different offset pattern. */ + 0, + 0, + 1 }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/wcsmr_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/wcsmr_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s", + tmpdir, driver, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + + unlink(src); unlink(outbin); rmdir(tmpdir); + return got; +} + +/* Scan asm output for the `TEXT f,$N` line; return N (or -1 if not + * found). Tolerates an optional MODULE-mangled prefix. */ +static int +read_frame(const char *path) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + char line[1024]; + int frame = -1; + while (fgets(line, sizeof line, f)) { + if (strncmp(line, "TEXT ", 5) != 0) continue; + const char *dollar = strchr(line, '$'); + if (!dollar) continue; + /* TEXT f,$96 — frame after `$` up to whitespace */ + frame = atoi(dollar + 1); + break; + } + fclose(f); + return frame; +} + +/* Scan asm output for max -K(BP) offset (where K > 0). Returns the + * largest K seen, or 0 if no negative-BP reference appears. */ +static int +read_max_neg_off(const char *path) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + int maxk = 0; + char buf[16384]; + size_t n = fread(buf, 1, sizeof buf - 1, f); + fclose(f); + buf[n] = '\0'; + /* Look for "-N(BP)" patterns. */ + const char *p = buf; + while ((p = strstr(p, "(BP)")) != NULL) { + /* Walk backward to find the start of the operand. */ + const char *q = p; + while (q > buf && (q[-1] >= '0' && q[-1] <= '9')) q--; + if (q > buf && q[-1] == '-') { + int k = atoi(q); + if (k > maxk) maxk = k; + } + p += 4; + } + return maxk; +} + +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[64], cs[64], ws[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/wcsmr_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/wcsmr_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/wcsmr_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + int rc = 0; + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c errored\n", r->label); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); + unlink(src); unlink(cs); + return -1; + } + + /* Positive: byte-identity between stages. Gated per row — some + * shapes have pre-existing label-counter skew unrelated to #14. */ + if (r->check_byte_id) { + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + if (!fc || !fw) { + rc = -1; + } else { + for (;;) { + int a = fgetc(fc); + int b = fgetc(fw); + if (a != b) { rc = -1; break; } + if (a == EOF) break; + } + } + if (fc) fclose(fc); + if (fw) fclose(fw); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + } + + /* Positive: frame size matches expected (single-slot dedup). */ + if (rc == 0 && r->frame > 0) { + int cframe = read_frame(cs); + int wframe = read_frame(ws); + if (cframe != r->frame || wframe != r->frame) { + fprintf(stderr, + "row[%s]: frame size mismatch — cstage=$%d wwstage=$%d want=$%d\n", + r->label, cframe, wframe, r->frame); + rc = -1; + } + } + + /* Negative (stomp sentinel): no -K(BP) with K > max_off. */ + if (rc == 0 && r->max_off > 0) { + int cmax = read_max_neg_off(cs); + int wmax = read_max_neg_off(ws); + if (cmax > r->max_off || wmax > r->max_off) { + fprintf(stderr, + "row[%s]: stomp regression — deepest -BP offset cstage=%d wwstage=%d bound=%d\n", + r->label, cmax, wmax, r->max_off); + rc = -1; + } + } + + unlink(src); unlink(cs); unlink(ws); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[1024]; + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated_on_existence; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "struct_multi_return_scratch: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != rows[i].want) { + fprintf(stderr, + "struct_multi_return_scratch[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, + "struct_multi_return_scratch: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("struct_multi_return_scratch: %d/%d ok\n", total, total); + return 0; +}