From 606c16a28fc22d012cabd1ef4dcaacdba56b6b7a Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 9 Jun 2026 01:18:32 +0900 Subject: [PATCH] wcc/cgen: #19+#22 uniform index element-size for non-ident bases (wwstage) Indexing a non-ident pointer-yielding base -- a direct cast ((&a):*[4]u32)[i], a call result mk(&a)[i], a slice, a type-assertion -- used wwstage's default 8-byte element stride/load instead of the real element type's, reading garbage (cast-base i32 index: cs=30, ww=0). The cgindex esz derivation gated on a whitelist of base node-kinds (DOT / UN-deref / INDEX); an N_CAST/N_CALL/N_SLICE/N_TYPEASSERT base matched none. Rather than extend the whitelist (whack-a-mole), this mirrors cstage's uniform idx_eff read: N_INDEX keeps its own arm (chained-index byte-id preserved), and every other non-ident base now derives esz/stride/load- width/signedness from the stamped n.type_ -- closing the class by construction (base set ident/dot/un/index/cast/call/slice/typeassert). cstage was already correct (uniform); w6c md5 unchanged. byte-id 990-997 8/8, no lib pin flips. test/wcc/830 (9 base shapes, byte-id per width, signed + unsigned). Folds the N_CALL sibling #22. --- Makefile | 7 + selfhost/cmd/w6c/main.combined.ww | 45 +++-- selfhost/cmd/wcc/cgenexpr.ww | 45 +++-- selfhost/cmd/wwdump/main.combined.ww | 45 +++-- test/wcc/830_cast_base_index.c | 284 +++++++++++++++++++++++++++ 5 files changed, 369 insertions(+), 57 deletions(-) create mode 100644 test/wcc/830_cast_base_index.c diff --git a/Makefile b/Makefile index cf045dab..214e7c46 100644 --- a/Makefile +++ b/Makefile @@ -530,6 +530,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_sort_run \ $(BIN)/test_bufio_run $(BIN)/test_random_run \ $(BIN)/test_asserttyped_gap \ + $(BIN)/test_cast_base_index \ $(BIN)/test_ascii_run $(BIN)/test_tok_run $(BIN)/test_nkname_run $(BIN)/test_smoke: test/wcc/000_smoke.c $(LIB)/libwcc.a | $(BIN) @@ -660,6 +661,12 @@ $(BIN)/test_arr_ptr_global: test/wcc/818_arr_ptr_global.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_cast_base_index: test/wcc/830_cast_base_index.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_def_str_index_reject: test/wcc/821_def_str_index_reject.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 834ca39b..0af0f1f8 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -23562,29 +23562,14 @@ fn cgindex(c: *cgen, n: *node) void = { if (!elem_isarray) { elem_isarray = tinfoisarray(n.type_: *tinfo); }; - } else { if (base.kind == nkind.N_DOT - || (base.kind == nkind.N_UN && base.op == tkind.TK_STAR)) { - // `s.ptr[i]` / struct-field index: stride is the - // checker-stamped element tinfo's natural size, the - // same idiom as the N_INDEX-base arm below (#60/#72). - // cstage idx_eff(base->type)->sub->size (cmd/w6c/ - // cgen.c:3517-18). Signedness from the same element tinfo - // so a signed-narrow field element sign-extends on load - // (loadopsz keys on (signed,sz)); cstage's fldloadop reads - // it from the element type — align ww up (#255). - // N_UN deref base (`(*p)[i]`, #61 C): same stamped-tinfo - // source — cstage reads base->type uniformly; without - // this arm esz fell to the 8B default (wrong stride for - // narrow elements once the deref base materializes). - let dt: *tinfo = n.type_: *tinfo; - if (dt != nil) { esz = dt.size: i32; signed_elem = typeissigned(dt); elemisstr = typeisstr(dt); elemisslice = typeisslice(dt); float_elem = typeisfloat(dt); f32_elem = typeisf32(dt); }; - elem_isarray = tinfoisarray(dt); } else { if (base.kind == nkind.N_INDEX) { // #60: chained `names[i][k]` — n.type_ is the checker- // stamped outer element tinfo (indexresult over the inner // index's value type). cstage reads base->type->sub->size // for esz (cmd/w6c/cgen.c:2070-2071). Drops the - // indexvaluetnode walk. + // indexvaluetnode walk. Kept as its OWN arm (it does not set + // elemisstr/elemisslice) so the chained-index byte-id is + // preserved verbatim; the catch-all below would re-stamp it. let et: *tinfo = n.type_: *tinfo; if (et != nil) { esz = et.size: i32; @@ -23593,7 +23578,29 @@ fn cgindex(c: *cgen, n: *node) void = { f32_elem = typeisf32(et); elem_isarray = tinfoisarray(et); }; - };};}; + } else { + // Every OTHER non-ident base — N_DOT (`s.arr[i]`), N_UN-deref + // (`(*p)[i]`, #61), N_CAST (`(e:*[N]T)[i]`, #19old), N_CALL + // (`f()[i]`), N_SLICE (`s[a:b][i]`), N_TYPEASSERT + // (`(v as *[N]T)[i]`), … — derives esz/stride/load-width/ + // signedness from the checker-stamped index-RESULT tinfo + // n.type_ (the element T: u32->4). cstage reads it UNIFORMLY + // via idx_eff(base->type)->sub->size with NO node-kind gate + // (cmd/w6c/cgen.c:3517-18); wwstage's prior node-kind whitelist + // (DOT/UN/CAST only) silently left N_CALL/N_SLICE/N_TYPEASSERT + // (and any future base kind) at the 8B default — wrong stride + // AND full-word MOVQ load for narrow elements. One stamped-tinfo + // read mirrors cstage and closes the class by construction + // (#19old + its CALL/SLICE/TYPEASSERT residuals). esz via + // dt.size (type table, rule-13). Signedness from the same tinfo + // so a signed-narrow element sign-extends on load (loadopsz keys + // on (signed,sz); cstage's fldloadop reads it from the element + // type — align ww up, #255). The base ADDRESS materialization + // below is unchanged; only the stride/load-width was wrong. + let dt: *tinfo = n.type_: *tinfo; + if (dt != nil) { esz = dt.size: i32; signed_elem = typeissigned(dt); elemisstr = typeisstr(dt); elemisslice = typeisslice(dt); float_elem = typeisfloat(dt); f32_elem = typeisf32(dt); }; + elem_isarray = tinfoisarray(dt); + };}; }; // Tagged-union element: load slot words into (AX=tag, DX=val0, // CX=val1) matching the tagged-return ABI so call-arg / let / diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 3b84be37..b8447ee9 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1900,29 +1900,14 @@ fn cgindex(c: *cgen, n: *node) void = { if (!elem_isarray) { elem_isarray = tinfoisarray(n.type_: *tinfo); }; - } else { if (base.kind == nkind.N_DOT - || (base.kind == nkind.N_UN && base.op == tkind.TK_STAR)) { - // `s.ptr[i]` / struct-field index: stride is the - // checker-stamped element tinfo's natural size, the - // same idiom as the N_INDEX-base arm below (#60/#72). - // cstage idx_eff(base->type)->sub->size (cmd/w6c/ - // cgen.c:3517-18). Signedness from the same element tinfo - // so a signed-narrow field element sign-extends on load - // (loadopsz keys on (signed,sz)); cstage's fldloadop reads - // it from the element type — align ww up (#255). - // N_UN deref base (`(*p)[i]`, #61 C): same stamped-tinfo - // source — cstage reads base->type uniformly; without - // this arm esz fell to the 8B default (wrong stride for - // narrow elements once the deref base materializes). - let dt: *tinfo = n.type_: *tinfo; - if (dt != nil) { esz = dt.size: i32; signed_elem = typeissigned(dt); elemisstr = typeisstr(dt); elemisslice = typeisslice(dt); float_elem = typeisfloat(dt); f32_elem = typeisf32(dt); }; - elem_isarray = tinfoisarray(dt); } else { if (base.kind == nkind.N_INDEX) { // #60: chained `names[i][k]` — n.type_ is the checker- // stamped outer element tinfo (indexresult over the inner // index's value type). cstage reads base->type->sub->size // for esz (cmd/w6c/cgen.c:2070-2071). Drops the - // indexvaluetnode walk. + // indexvaluetnode walk. Kept as its OWN arm (it does not set + // elemisstr/elemisslice) so the chained-index byte-id is + // preserved verbatim; the catch-all below would re-stamp it. let et: *tinfo = n.type_: *tinfo; if (et != nil) { esz = et.size: i32; @@ -1931,7 +1916,29 @@ fn cgindex(c: *cgen, n: *node) void = { f32_elem = typeisf32(et); elem_isarray = tinfoisarray(et); }; - };};}; + } else { + // Every OTHER non-ident base — N_DOT (`s.arr[i]`), N_UN-deref + // (`(*p)[i]`, #61), N_CAST (`(e:*[N]T)[i]`, #19old), N_CALL + // (`f()[i]`), N_SLICE (`s[a:b][i]`), N_TYPEASSERT + // (`(v as *[N]T)[i]`), … — derives esz/stride/load-width/ + // signedness from the checker-stamped index-RESULT tinfo + // n.type_ (the element T: u32->4). cstage reads it UNIFORMLY + // via idx_eff(base->type)->sub->size with NO node-kind gate + // (cmd/w6c/cgen.c:3517-18); wwstage's prior node-kind whitelist + // (DOT/UN/CAST only) silently left N_CALL/N_SLICE/N_TYPEASSERT + // (and any future base kind) at the 8B default — wrong stride + // AND full-word MOVQ load for narrow elements. One stamped-tinfo + // read mirrors cstage and closes the class by construction + // (#19old + its CALL/SLICE/TYPEASSERT residuals). esz via + // dt.size (type table, rule-13). Signedness from the same tinfo + // so a signed-narrow element sign-extends on load (loadopsz keys + // on (signed,sz); cstage's fldloadop reads it from the element + // type — align ww up, #255). The base ADDRESS materialization + // below is unchanged; only the stride/load-width was wrong. + let dt: *tinfo = n.type_: *tinfo; + if (dt != nil) { esz = dt.size: i32; signed_elem = typeissigned(dt); elemisstr = typeisstr(dt); elemisslice = typeisslice(dt); float_elem = typeisfloat(dt); f32_elem = typeisf32(dt); }; + elem_isarray = tinfoisarray(dt); + };}; }; // Tagged-union element: load slot words into (AX=tag, DX=val0, // CX=val1) matching the tagged-return ABI so call-arg / let / diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 098076c2..a14b2cbd 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -23562,29 +23562,14 @@ fn cgindex(c: *cgen, n: *node) void = { if (!elem_isarray) { elem_isarray = tinfoisarray(n.type_: *tinfo); }; - } else { if (base.kind == nkind.N_DOT - || (base.kind == nkind.N_UN && base.op == tkind.TK_STAR)) { - // `s.ptr[i]` / struct-field index: stride is the - // checker-stamped element tinfo's natural size, the - // same idiom as the N_INDEX-base arm below (#60/#72). - // cstage idx_eff(base->type)->sub->size (cmd/w6c/ - // cgen.c:3517-18). Signedness from the same element tinfo - // so a signed-narrow field element sign-extends on load - // (loadopsz keys on (signed,sz)); cstage's fldloadop reads - // it from the element type — align ww up (#255). - // N_UN deref base (`(*p)[i]`, #61 C): same stamped-tinfo - // source — cstage reads base->type uniformly; without - // this arm esz fell to the 8B default (wrong stride for - // narrow elements once the deref base materializes). - let dt: *tinfo = n.type_: *tinfo; - if (dt != nil) { esz = dt.size: i32; signed_elem = typeissigned(dt); elemisstr = typeisstr(dt); elemisslice = typeisslice(dt); float_elem = typeisfloat(dt); f32_elem = typeisf32(dt); }; - elem_isarray = tinfoisarray(dt); } else { if (base.kind == nkind.N_INDEX) { // #60: chained `names[i][k]` — n.type_ is the checker- // stamped outer element tinfo (indexresult over the inner // index's value type). cstage reads base->type->sub->size // for esz (cmd/w6c/cgen.c:2070-2071). Drops the - // indexvaluetnode walk. + // indexvaluetnode walk. Kept as its OWN arm (it does not set + // elemisstr/elemisslice) so the chained-index byte-id is + // preserved verbatim; the catch-all below would re-stamp it. let et: *tinfo = n.type_: *tinfo; if (et != nil) { esz = et.size: i32; @@ -23593,7 +23578,29 @@ fn cgindex(c: *cgen, n: *node) void = { f32_elem = typeisf32(et); elem_isarray = tinfoisarray(et); }; - };};}; + } else { + // Every OTHER non-ident base — N_DOT (`s.arr[i]`), N_UN-deref + // (`(*p)[i]`, #61), N_CAST (`(e:*[N]T)[i]`, #19old), N_CALL + // (`f()[i]`), N_SLICE (`s[a:b][i]`), N_TYPEASSERT + // (`(v as *[N]T)[i]`), … — derives esz/stride/load-width/ + // signedness from the checker-stamped index-RESULT tinfo + // n.type_ (the element T: u32->4). cstage reads it UNIFORMLY + // via idx_eff(base->type)->sub->size with NO node-kind gate + // (cmd/w6c/cgen.c:3517-18); wwstage's prior node-kind whitelist + // (DOT/UN/CAST only) silently left N_CALL/N_SLICE/N_TYPEASSERT + // (and any future base kind) at the 8B default — wrong stride + // AND full-word MOVQ load for narrow elements. One stamped-tinfo + // read mirrors cstage and closes the class by construction + // (#19old + its CALL/SLICE/TYPEASSERT residuals). esz via + // dt.size (type table, rule-13). Signedness from the same tinfo + // so a signed-narrow element sign-extends on load (loadopsz keys + // on (signed,sz); cstage's fldloadop reads it from the element + // type — align ww up, #255). The base ADDRESS materialization + // below is unchanged; only the stride/load-width was wrong. + let dt: *tinfo = n.type_: *tinfo; + if (dt != nil) { esz = dt.size: i32; signed_elem = typeissigned(dt); elemisstr = typeisstr(dt); elemisslice = typeisslice(dt); float_elem = typeisfloat(dt); f32_elem = typeisf32(dt); }; + elem_isarray = tinfoisarray(dt); + };}; }; // Tagged-union element: load slot words into (AX=tag, DX=val0, // CX=val1) matching the tagged-return ABI so call-arg / let / diff --git a/test/wcc/830_cast_base_index.c b/test/wcc/830_cast_base_index.c new file mode 100644 index 00000000..3f62d3cd --- /dev/null +++ b/test/wcc/830_cast_base_index.c @@ -0,0 +1,284 @@ +/* + * 830_cast_base_index — indexing a DIRECT-CAST base `(e:*[N]T)[i]` must + * stride by the cast element type's size, not the 8B default (task + * #19old, .ai/rob-19old-spec.md). wwstage-only fix; cstage was already + * correct. + * + * The bug: cgindex (selfhost/cmd/wcc/cgenexpr.ww) derived the element + * stride/load-width `esz` only for N_DOT / N_UN-deref / N_INDEX base + * node-kinds (read off the stamped index-result tinfo n.type_). An + * N_CAST base matched NO arm, so esz stayed at the 8B default — for a + * narrow element (u32/u16/u8) the stride and load width were both + * wrong and the read returned 0/garbage. cstage reads it uniformly via + * idx_eff(base->type)->sub->size (cmd/w6c/cgen.c N_INDEX); the fix adds + * `|| base.kind == nkind.N_CAST` to the stamped-tinfo esz arm, which + * fixes stride + load width + signedness together (all read from dt). + * + * row | shape | stride | want + * ---------+-------------------------------+--------+------ + * u32_idx | [4]u32, ((&a):*[4]u32)[i] | 4 | 30 + * u16_idx | [4]u16, ((&a):*[4]u16)[i] | 2 | 30 + * u8_idx | [4]u8, ((&a):*[4]u8)[i] | 1 | 40 + * u64_ctl | [4]u64, ((&a):*[4]u64)[i] | 8 | 20 (control) + * + * The whitelist of base node-kinds was whack-a-mole (#19old residuals): + * N_CALL, N_SLICE and N_TYPEASSERT bases ALSO fell to the 8B default. The + * fix routes every non-ident, non-N_INDEX base through the stamped-n.type_ + * arm (mirrors cstage's uniform idx_eff, no node-kind gate). These rows + * pin the residual base kinds: + * + * call_idx | mk(&a)[i] (N_CALL *[4]u32) | 4 | 30 + * slc_idx | a[0:4][i] (N_SLICE []u16) | 2 | 30 + * asrt_idx | (v as *[4]u16)[i] (N_TYPEASSERT) | 2 | 30 + * + * The <8B rows are the mutation-sane rows: pre-fix wwstage strides by 8 + * and reads 0/garbage. u64_ctl pins no-regress for the already-correct + * 8B-stride case. A byte-id row pins cstage==wwstage `.s` for the + * cast-base index source (the convergence the fix delivers). + */ +#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[] = { + /* u32_idx — stride 4 / MOVL. THE bug: pre-fix wwstage strides by + * 8 and reads 0. */ + { "u32_idx", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet a: [4]u32 = [10u32, 20u32, 30u32, 40u32];\n" + "\tlet i: int = 2;\n" + "\treturn ((&a): *[4]u32)[i]: i32;\n" + "};\n", + 30 }, + + /* u16_idx — stride 2 / MOVW. */ + { "u16_idx", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet a: [4]u16 = [10u16, 20u16, 30u16, 40u16];\n" + "\tlet i: int = 2;\n" + "\treturn ((&a): *[4]u16)[i]: i32;\n" + "};\n", + 30 }, + + /* u8_idx — stride 1 / MOVB. */ + { "u8_idx", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet a: [4]u8 = [10u8, 20u8, 30u8, 40u8];\n" + "\tlet i: int = 3;\n" + "\treturn ((&a): *[4]u8)[i]: i32;\n" + "};\n", + 40 }, + + /* u64_ctl — stride 8 control: already correct (matches the 8B + * default), guards no-regress for the wide-element cast base. */ + { "u64_ctl", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet a: [4]u64 = [10u64, 20u64, 30u64, 40u64];\n" + "\tlet i: int = 1;\n" + "\treturn ((&a): *[4]u64)[i]: i32;\n" + "};\n", + 20 }, + + /* call_idx — N_CALL base mk(&a)[i], stride 4 / MOVL. #19old residual: + * a call-returning-pointer base also fell to the 8B default. */ + { "call_idx", + "package main;\n" + "fn mk(p: *[4]u32) *[4]u32 = { return p; };\n" + "export fn main() i32 = {\n" + "\tlet a: [4]u32 = [10u32, 20u32, 30u32, 40u32];\n" + "\tlet i: int = 2;\n" + "\treturn mk(&a)[i]: i32;\n" + "};\n", + 30 }, + + /* slc_idx — N_SLICE base a[0:4][i], stride 2 / MOVZWQ. #19old + * residual: a slice-expression base also fell to the 8B default. */ + { "slc_idx", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet a: [4]u16 = [10u16, 20u16, 30u16, 40u16];\n" + "\tlet i: int = 2;\n" + "\treturn a[0:4][i]: i32;\n" + "};\n", + 30 }, + + /* asrt_idx — N_TYPEASSERT base (v as *[4]u16)[i], stride 2 / MOVZWQ. + * #19old residual: a type-assert base also fell to the 8B default. */ + { "asrt_idx", + "package main;\n" + "type IP = (*[4]u16 | int);\n" + "export fn main() i32 = {\n" + "\tlet a: [4]u16 = [10u16, 20u16, 30u16, 40u16];\n" + "\tlet v: IP = &a;\n" + "\tlet i: int = 2;\n" + "\treturn (v as *[4]u16)[i]: i32;\n" + "};\n", + 30 }, +}; + +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/cbi_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/cbi_%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 2>/dev/null", + 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; +} + +/* asm_byte_identical — w6c vs w6c_ww .s for the same source must match. */ +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/cbi_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/cbi_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/cbi_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; + } + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + int rc = 0; + 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); + 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, "cast_base_index: 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, + "cast_base_index[%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, + "cast_base_index: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("cast_base_index: %d/%d ok\n", total, total); + return 0; +}