From a4a4cd7c1651bcd52259287cede65583e1176567 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 12 Jun 2026 21:11:35 +0900 Subject: [PATCH] wcc/ww: cgcall drain pops widened GP words before the float arm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The arg-drain loop checked node_isfloat before popping a widened arg's GP words, so a float arg adjacent to a widened (tagged) arg read the wrong stack slot: the f64 took the widened payload (#30), or the float arm ate the tag word into X0 and the payload landed in DI as the tag (#48). One missing branch, two manifestations — mirror cstage's widen-first pop (cgen.c:9650-9665). Review items #30+#48 (fold reviewer-verified one-mechanism against the cstage twin). --- Makefile | 7 + selfhost/cmd/w6c/main.combined.ww | 82 ++++++++++ selfhost/cmd/wcc/cgenexpr.ww | 30 ++++ selfhost/cmd/wcc/cgenutil.ww | 52 +++++++ selfhost/cmd/wwdump/main.combined.ww | 82 ++++++++++ test/wcc/949_f9_float_run.c | 221 +++++++++++++++++++++++++++ 6 files changed, 474 insertions(+) create mode 100644 test/wcc/949_f9_float_run.c diff --git a/Makefile b/Makefile index 79f5c5a6..b3e356ac 100644 --- a/Makefile +++ b/Makefile @@ -379,6 +379,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_append_structlit_evalorder_run \ $(BIN)/test_tuple_index_read_run \ $(BIN)/test_f6_header_run \ + $(BIN)/test_f9_float_run \ $(BIN)/test_xmod_fnptr_const_run \ $(BIN)/test_tuple_slot_layout_run \ $(BIN)/test_tagged_tuple_widen_run \ @@ -2140,6 +2141,12 @@ $(BIN)/test_f6_header_run: test/wcc/949_f6_header_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_f9_float_run: test/wcc/949_f9_float_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_xmod_fnptr_const_run: test/wcc/949_xmod_fnptr_const_run.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 799ef7ab..dde0d15d 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -17820,6 +17820,58 @@ fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = { // skips the other's args; the return value counts only own-phase // slot words. Mirrors cstage cgcall's mem pre-pass; ABI shape per // ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit). +// argtaggedwidensz — the SysV slot width (16/24/32) a CONCRETE arg is +// widened to when passed to a tagged-union PARAM, or 0 when no register-class +// widen happens: param not tagged / not a fixed param, an already-matching- +// slot tagged source (natural push), the nullable 8B fold (handled by the +// scalar path), or a >48B memory-class slot (staged below the register words). +// This is the SSoT the cgcall DRAIN consults for its widen-first POP count; +// it MUST agree word-for-word with pushargsrev's widen-PUSH count below +// (same param-tagged + !aistagged gates, same taggedcastpeel) or the drain +// desyncs — the #30/#48 root was a drain with no widen branch: the widened +// box's words were under-drained (#30: a following float read the leftover +// payload word) or the float-source box was misclassified as a float arg +// (#48: MOVSD ate the tag word into X0). Mirrors cstage's precomputed +// widen[i]/widen_sz[i] (cmd/w6c/cgen.c cgcall). +fn argtaggedwidensz(c: *cgen, arg0: *node, param: *node) i32 = { + if (param == nil) { return 0; }; + if (param.kind != nkind.N_PARAM) { return 0; }; + if (param.op == tkind.TK_ELLIPSIS) { return 0; }; + let ptype: *node = param.lhs; + if (ptype == nil) { return 0; }; + if (!istaggedtype(c, ptype)) { return 0; }; + // >48B slot is memory-class: pushargsrev stages it below the register + // words and the drain skips it (dmemsz) — never a register widen. + if (taggedmemargsize(ptype.type_: *tinfo) > 0) { return 0; }; + let arg: *node = taggedcastpeel(c, arg0); + let pslot: i32 = slotsize(c, ptype); + // Already a matching-slot tagged source → natural push, no widen + // (mirrors pushargsrev's aistagged gates: ident/call/index/dot/deref). + if (arg.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, arg.str); + if (lc != nil) { + if (istaggedtype(c, lc.tnode)) { + if (slotsize(c, lc.tnode) == pslot) { return 0; }; + }; + }; + }; + if (taggedcallslot(c, arg) == pslot) { return 0; }; + if (arg.kind == nkind.N_INDEX) { + if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; }; + }; + if (arg.kind == nkind.N_DOT) { + if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; }; + }; + if (arg.kind == nkind.N_UN && arg.op == tkind.TK_STAR) { + if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; }; + }; + // The 8B nullable fold pushes one pointer word the scalar drain path + // already pops correctly; only the multi-word tagged widen needs the + // drain's dedicated branch. + if (pslot < 16) { return 0; }; + return pslot; +}; + fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { if (arg == nil) { return 0; }; let nextparam: *node = nil; @@ -30907,6 +30959,36 @@ fn cgcall(c: *cgen, n: *node) void = { a = a.next; continue; }; + // Widen-first pop (#30/#48): a concrete arg widened into a + // tagged-union param was pushed as slotsize/8 GP words (tag + + // payload). Drain those words into the INTEGER arg cursor + // BEFORE the float check below — else a widened f64-source box + // gets misclassified as a float arg (its tag word drained into + // X0, #48), and a float arg FOLLOWING a widened arg reads the + // widened box's leftover payload word (#30). Mirror of cstage's + // precomputed widen[i] branch (cmd/w6c/cgen.c cgcall, popped + // before node_isfloat). argtaggedwidensz is the shared SSoT with + // pushargsrev's push count. + let dwsz: i32 = argtaggedwidensz(c, a, dparam); + if (dwsz >= 16) { + let dwb: i32 = dwsz / 8; + let dwk: i32 = 0; + for (dwk < dwb) { + if (intidx < 6) { + emitline("\tPOPQ\t"); + emitline(argregname(intidx)); + emitline("\n"); + intidx += 1; + } else { + stackslots += 1; + }; + popped += 1; + dwk += 1; + }; + if (dparam != nil) { dparam = dparam.next; }; + a = a.next; + continue; + }; let fk: i32 = 0; if (a != nil) { let at: *tinfo = a.type_: *tinfo; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index d2876511..607c9135 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -7756,6 +7756,36 @@ fn cgcall(c: *cgen, n: *node) void = { a = a.next; continue; }; + // Widen-first pop (#30/#48): a concrete arg widened into a + // tagged-union param was pushed as slotsize/8 GP words (tag + + // payload). Drain those words into the INTEGER arg cursor + // BEFORE the float check below — else a widened f64-source box + // gets misclassified as a float arg (its tag word drained into + // X0, #48), and a float arg FOLLOWING a widened arg reads the + // widened box's leftover payload word (#30). Mirror of cstage's + // precomputed widen[i] branch (cmd/w6c/cgen.c cgcall, popped + // before node_isfloat). argtaggedwidensz is the shared SSoT with + // pushargsrev's push count. + let dwsz: i32 = argtaggedwidensz(c, a, dparam); + if (dwsz >= 16) { + let dwb: i32 = dwsz / 8; + let dwk: i32 = 0; + for (dwk < dwb) { + if (intidx < 6) { + emitline("\tPOPQ\t"); + emitline(argregname(intidx)); + emitline("\n"); + intidx += 1; + } else { + stackslots += 1; + }; + popped += 1; + dwk += 1; + }; + if (dparam != nil) { dparam = dparam.next; }; + a = a.next; + continue; + }; let fk: i32 = 0; if (a != nil) { let at: *tinfo = a.type_: *tinfo; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 60be7e6e..bc635d5a 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -106,6 +106,58 @@ fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = { // skips the other's args; the return value counts only own-phase // slot words. Mirrors cstage cgcall's mem pre-pass; ABI shape per // ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit). +// argtaggedwidensz — the SysV slot width (16/24/32) a CONCRETE arg is +// widened to when passed to a tagged-union PARAM, or 0 when no register-class +// widen happens: param not tagged / not a fixed param, an already-matching- +// slot tagged source (natural push), the nullable 8B fold (handled by the +// scalar path), or a >48B memory-class slot (staged below the register words). +// This is the SSoT the cgcall DRAIN consults for its widen-first POP count; +// it MUST agree word-for-word with pushargsrev's widen-PUSH count below +// (same param-tagged + !aistagged gates, same taggedcastpeel) or the drain +// desyncs — the #30/#48 root was a drain with no widen branch: the widened +// box's words were under-drained (#30: a following float read the leftover +// payload word) or the float-source box was misclassified as a float arg +// (#48: MOVSD ate the tag word into X0). Mirrors cstage's precomputed +// widen[i]/widen_sz[i] (cmd/w6c/cgen.c cgcall). +fn argtaggedwidensz(c: *cgen, arg0: *node, param: *node) i32 = { + if (param == nil) { return 0; }; + if (param.kind != nkind.N_PARAM) { return 0; }; + if (param.op == tkind.TK_ELLIPSIS) { return 0; }; + let ptype: *node = param.lhs; + if (ptype == nil) { return 0; }; + if (!istaggedtype(c, ptype)) { return 0; }; + // >48B slot is memory-class: pushargsrev stages it below the register + // words and the drain skips it (dmemsz) — never a register widen. + if (taggedmemargsize(ptype.type_: *tinfo) > 0) { return 0; }; + let arg: *node = taggedcastpeel(c, arg0); + let pslot: i32 = slotsize(c, ptype); + // Already a matching-slot tagged source → natural push, no widen + // (mirrors pushargsrev's aistagged gates: ident/call/index/dot/deref). + if (arg.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, arg.str); + if (lc != nil) { + if (istaggedtype(c, lc.tnode)) { + if (slotsize(c, lc.tnode) == pslot) { return 0; }; + }; + }; + }; + if (taggedcallslot(c, arg) == pslot) { return 0; }; + if (arg.kind == nkind.N_INDEX) { + if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; }; + }; + if (arg.kind == nkind.N_DOT) { + if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; }; + }; + if (arg.kind == nkind.N_UN && arg.op == tkind.TK_STAR) { + if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; }; + }; + // The 8B nullable fold pushes one pointer word the scalar drain path + // already pops correctly; only the multi-word tagged widen needs the + // drain's dedicated branch. + if (pslot < 16) { return 0; }; + return pslot; +}; + fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { if (arg == nil) { return 0; }; let nextparam: *node = nil; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index c70e3eab..ce54df58 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -17820,6 +17820,58 @@ fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = { // skips the other's args; the return value counts only own-phase // slot words. Mirrors cstage cgcall's mem pre-pass; ABI shape per // ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit). +// argtaggedwidensz — the SysV slot width (16/24/32) a CONCRETE arg is +// widened to when passed to a tagged-union PARAM, or 0 when no register-class +// widen happens: param not tagged / not a fixed param, an already-matching- +// slot tagged source (natural push), the nullable 8B fold (handled by the +// scalar path), or a >48B memory-class slot (staged below the register words). +// This is the SSoT the cgcall DRAIN consults for its widen-first POP count; +// it MUST agree word-for-word with pushargsrev's widen-PUSH count below +// (same param-tagged + !aistagged gates, same taggedcastpeel) or the drain +// desyncs — the #30/#48 root was a drain with no widen branch: the widened +// box's words were under-drained (#30: a following float read the leftover +// payload word) or the float-source box was misclassified as a float arg +// (#48: MOVSD ate the tag word into X0). Mirrors cstage's precomputed +// widen[i]/widen_sz[i] (cmd/w6c/cgen.c cgcall). +fn argtaggedwidensz(c: *cgen, arg0: *node, param: *node) i32 = { + if (param == nil) { return 0; }; + if (param.kind != nkind.N_PARAM) { return 0; }; + if (param.op == tkind.TK_ELLIPSIS) { return 0; }; + let ptype: *node = param.lhs; + if (ptype == nil) { return 0; }; + if (!istaggedtype(c, ptype)) { return 0; }; + // >48B slot is memory-class: pushargsrev stages it below the register + // words and the drain skips it (dmemsz) — never a register widen. + if (taggedmemargsize(ptype.type_: *tinfo) > 0) { return 0; }; + let arg: *node = taggedcastpeel(c, arg0); + let pslot: i32 = slotsize(c, ptype); + // Already a matching-slot tagged source → natural push, no widen + // (mirrors pushargsrev's aistagged gates: ident/call/index/dot/deref). + if (arg.kind == nkind.N_IDENT) { + let lc: *local = localfindnode(c, arg.str); + if (lc != nil) { + if (istaggedtype(c, lc.tnode)) { + if (slotsize(c, lc.tnode) == pslot) { return 0; }; + }; + }; + }; + if (taggedcallslot(c, arg) == pslot) { return 0; }; + if (arg.kind == nkind.N_INDEX) { + if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; }; + }; + if (arg.kind == nkind.N_DOT) { + if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; }; + }; + if (arg.kind == nkind.N_UN && arg.op == tkind.TK_STAR) { + if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; }; + }; + // The 8B nullable fold pushes one pointer word the scalar drain path + // already pops correctly; only the multi-word tagged widen needs the + // drain's dedicated branch. + if (pslot < 16) { return 0; }; + return pslot; +}; + fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { if (arg == nil) { return 0; }; let nextparam: *node = nil; @@ -30907,6 +30959,36 @@ fn cgcall(c: *cgen, n: *node) void = { a = a.next; continue; }; + // Widen-first pop (#30/#48): a concrete arg widened into a + // tagged-union param was pushed as slotsize/8 GP words (tag + + // payload). Drain those words into the INTEGER arg cursor + // BEFORE the float check below — else a widened f64-source box + // gets misclassified as a float arg (its tag word drained into + // X0, #48), and a float arg FOLLOWING a widened arg reads the + // widened box's leftover payload word (#30). Mirror of cstage's + // precomputed widen[i] branch (cmd/w6c/cgen.c cgcall, popped + // before node_isfloat). argtaggedwidensz is the shared SSoT with + // pushargsrev's push count. + let dwsz: i32 = argtaggedwidensz(c, a, dparam); + if (dwsz >= 16) { + let dwb: i32 = dwsz / 8; + let dwk: i32 = 0; + for (dwk < dwb) { + if (intidx < 6) { + emitline("\tPOPQ\t"); + emitline(argregname(intidx)); + emitline("\n"); + intidx += 1; + } else { + stackslots += 1; + }; + popped += 1; + dwk += 1; + }; + if (dparam != nil) { dparam = dparam.next; }; + a = a.next; + continue; + }; let fk: i32 = 0; if (a != nil) { let at: *tinfo = a.type_: *tinfo; diff --git a/test/wcc/949_f9_float_run.c b/test/wcc/949_f9_float_run.c new file mode 100644 index 00000000..a88c5735 --- /dev/null +++ b/test/wcc/949_f9_float_run.c @@ -0,0 +1,221 @@ +/* + * 949_f9_float_run — F9 argument widen/drain ABI (float) cluster. + * + * The cgcall push/drain handling of a CONCRETE arg widened into a + * tagged-union parameter slot, where a float is involved: + * + * #30 + #48 (align-UP, wwstage-only): the cgcall DRAIN loop had no + * widen-first pop branch. A concrete arg widened into a tagged param + * was pushed as slotsize/8 GP words (tag + payload), but the drain + * classified per the ARG's source type: + * #30 — a float arg AFTER a widened (i64|void) arg: the widened + * box under-drained by one GP word, so `MOVSD (SP),X0` for + * the following f64 read the box's leftover payload word. + * #48 — the widened arg IS an f64 source: source-type=f64 hit the + * float arm, so `MOVSD (SP),X0` ate the TAG word into X0 and + * the payload landed in DI as the tag → callee fell through. + * Both are one missing branch — a widen-first GP pop placed BEFORE + * the float check, draining slotsize/8 words into the integer arg + * cursor (DI=tag, SI=payload). cstage already does this (precomputed + * widen[i]); this aligns wwstage UP and the rows pin cs==ww byte-id. + * + * REGISTER-CURSOR INVARIANT (rob): a lucky-but-wrong float shuffle can pass + * exit codes, so the run rows use values that only survive if the tag/ + * payload land in the right GP regs AND the float in the right XMM: + * #30 returns 0 iff f's b (the f64 arg following the widen) == 1.5; + * #48 returns 2 iff the (i64|f64) box's tag selects the f64 arm (DI=tag). + * Both also assert cstage/wwstage .s byte-identical. + * + * NNN<950, self-contained (/tmp, no imports; #30 returns i32 rather than + * importing os, so the asm byte-id row can run w6c/w6c_ww directly). + */ +#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; +} + +static int +slurp_eq(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } + int rc = 0; + for (;;) { + int ca = fgetc(fa), cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + return rc; +} + +#define K_RUN 0 +#define K_BUILDERR 1 + +struct row { const char *label; const char *src; int want; + int kind; const char *experr; }; + +static const struct row rows[] = { + /* #30: a widened (i64|void) arg FOLLOWED by an f64 arg. Pre-fix the + * f64 drain (MOVSD (SP),X0) read the widened box's leftover payload + * word (int 7) instead of 1.5, so r != 1.5 → 1. */ + { "widen_then_float", + "package main;\n" + "fn f(a: (i64 | void), b: f64) f64 = { return b; };\n" + "export fn main() i32 = {\n" + " let r: f64 = f(7, 1.5);\n" + " if (r == 1.5) { return 0; };\n" + " return 1;\n" + "};\n", 0, K_RUN, NULL }, + /* #48: the widened arg IS an f64 source (i64|f64). Pre-fix the float + * arm ate the tag word into X0 and the payload landed in DI as the + * tag, so the match fell through both arms → 9. Correct = 2 (f64). */ + { "float_source_widen", + "package main;\n" + "fn g(v: (i64 | f64)) i32 = {\n" + " match (v) {\n" + " case let n: i64 =>\n" + " return 1;\n" + " case let d: f64 =>\n" + " return 2;\n" + " };\n" + " return 9;\n" + "};\n" + "export fn main() i32 = {\n" + " let d: f64 = 3.5;\n" + " return g(d);\n" + "};\n", 2, K_RUN, NULL }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[96], tmpdir[96], errf[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/f9f_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/f9f_%d_d_%d", getpid(), i); + snprintf(errf, sizeof errf, "/tmp/f9f_%d_e_%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 >/dev/null 2>%s", + tmpdir, driver, src, errf); + int brc = runwait(cmd); + if (brc != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); unlink(errf); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[256]; + 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); unlink(errf); rmdir(tmpdir); + if (got != r->want) { + fprintf(stderr, "row[%s]: %s exit %d, want %d\n", + r->label, driver, got, r->want); + return 1; + } + return 0; +} + +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[96], cs[96], ws[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/f9f_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/f9f_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/f9f_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + 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; + } + int rc = slurp_eq(cs, ws); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + unlink(src); unlink(cs); unlink(ws); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[2080]; + 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[2120], wdrv[2120]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + total++; + if (run_driver(cdrv, &rows[i], i) != 0) fail++; + } + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + total++; + if (run_driver(wdrv, &rows[i], i) != 0) fail++; + } + for (int i = 0; i < n; i++) { + if (rows[i].kind == K_BUILDERR) + continue; + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) fail++; + } + } + + if (fail) { + fprintf(stderr, "f9_float: %d/%d checks failed\n", + fail, total); + return 1; + } + printf("f9_float: %d/%d ok\n", total, total); + return 0; +}