From 8dda8ea76c07c6291596755ffbeb0017527212ce Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 3 Jun 2026 22:29:57 +0900 Subject: [PATCH] w6c+w6c_ww: global-base arm for indexed struct-element field read (fix #21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `arr[i].field` N_DOT read branch in both stages was gated on a LOCAL base lookup (cstage `localfind != 0`, wwstage `localfindnode != nil`). A module-GLOBAL base (`let g: [2]pt = [...]`) missed it: - cstage fell to a generic index-load that drops f->offset — it read element[i] at offset 0, so `g[i].b` returned a's value (g[0].b -> 1, g[1].b -> 3 instead of 2, 4). - wwstage fell to the module-qualified SB fallback — garbage, no main.g load at all. Silent, byte-id-divergent. This is the READ twin of #11 (the global `g[i] = v` write fix) and the #15 sibling. Local `[N]struct` bases read correctly (tests 680/681 cover only those), which is why it was never caught. Fix (both stages, converged byte-identical): resolve the global the same way the N_INDEX arm does — cstage `let_islet || def_isarraydef`, wwstage `letvartnode || defvartnode` — and dispatch the base load by shape: array -> LEAQ name(SB) (the symbol IS the storage), slice/ptr -> MOVQ name(SB) (the symbol's first word IS the .ptr). The field then loads at f->offset exactly as the local arm does. esz (element stride) and f->offset both come from the type table (rule 13). Mirrors #11's write-side global-base resolution. combined.ww embeds (w6c + wwdump) regenerate. 688_global_arr_elem_field: global `[2]pt` reads of .a/.b on both elements (the .b reads are the bug), a non-8-aligned `[2]rec {tag:u8,x:i32,y:i64}` to stress f->offset + a u8 sub-word leaf, and a slice-base read (`let g: []rec = arr;`) that exercises the MOVQ-deref .ptr arm. Runtime (cstage build+run) + cstage==wwstage byte-id per row. The slice row is byte-id ONLY: its read asm is correct and identical on both stages, but a slice-of-struct module global does not data-emit a symbol yet (a separate, pre-existing data-emission gap, sibling of #10/#20), so it cannot link/run. --- Makefile | 7 + cmd/w6c/cgen.c | 25 ++- selfhost/cmd/w6c/main.combined.ww | 52 ++++- selfhost/cmd/wcc/cgenexpr.ww | 52 ++++- selfhost/cmd/wwdump/main.combined.ww | 52 ++++- test/wcc/688_global_arr_elem_field.c | 281 +++++++++++++++++++++++++++ 6 files changed, 437 insertions(+), 32 deletions(-) create mode 100644 test/wcc/688_global_arr_elem_field.c diff --git a/Makefile b/Makefile index b7919a86..86cc6c17 100644 --- a/Makefile +++ b/Makefile @@ -245,6 +245,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_arr_infer_len \ $(BIN)/test_slice_str_global_zero \ $(BIN)/test_slice_literal_global \ + $(BIN)/test_global_arr_elem_field \ $(BIN)/test_dot_str_chained_arg \ $(BIN)/test_dot_slice_arg \ $(BIN)/test_dot_tagged_source \ @@ -589,6 +590,12 @@ $(BIN)/test_slice_literal_global: test/wcc/687_slice_literal_global.c $(BIN)/ww $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_global_arr_elem_field: test/wcc/688_global_arr_elem_field.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_dot_str_chained_arg: test/wcc/692_dot_str_chained_arg.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index bf5e8b15..a30373e0 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -8361,8 +8361,21 @@ cgexpr(Cg *c, Node *n, Local *locals) int is_sl = bu && bu->kind == TY_SLICE; int is_ptr = bu && bu->kind == TY_PTR; int off = localfind(locals, idxbase->str); + /* #21 (READ twin of #11): a module-GLOBAL base + * makes localfind return 0, so the field-offset- + * aware branch was skipped and `g[i].field` fell to + * a generic index-load that drops f->offset (reads + * element[i] at offset 0). Resolve the global the + * same way the N_INDEX arm does (let_islet || + * def_isarraydef) and dispatch the base load by + * shape: array -> LEAQ name(SB) (the symbol IS the + * storage), slice/ptr -> MOVQ name(SB) (the symbol's + * first word IS the .ptr). */ + int isglobal = (off == 0) + && (let_islet(idxbase->str) + || def_isarraydef(idxbase->str)); if (f != NULL && (is_arr || is_sl || is_ptr) - && off != 0) { + && (off != 0 || isglobal)) { int esz = (int)elemt->size; cgexpr(c, n->lhs->rhs, locals); if (esz > 1) { @@ -8371,7 +8384,15 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); } - if (is_arr) + if (isglobal && is_arr) + ins2(c, A_LEAQ, + masym(c, idxbase->str), + areg(D_BX)); + else if (isglobal) + ins2(c, A_MOVQ, + masym(c, idxbase->str), + areg(D_BX)); + else if (is_arr) ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX)); else diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 3a4505cf..72324bc1 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -22306,8 +22306,28 @@ fn cgdot(c: *cgen, n: *node) void = { let idxbase: *node = lhs.lhs; if (idxbase != nil) { if (idxbase.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, idxbase.str); - if (lc != nil) { if (lc.tnode != nil) { - let tn: *node = lc.tnode; + // #21 (READ twin of #11): a module-GLOBAL base makes + // localfindnode return nil, so this field-offset-aware + // branch was skipped and `g[i].field` fell through to + // the module-qualified fallback below (garbage — no + // main.g load at all). Resolve the global's tnode via + // letvartnode/defvartnode (the same source cgindex's + // global arm uses) and dispatch the base load by shape: + // array -> LEAQ name(SB) (the symbol IS the storage), + // slice/ptr -> MOVQ name(SB) (the symbol's first word + // IS the .ptr). Mirrors cstage cgen.c's #21 arm. + let tn: *node = nil; + let isglobal: bool = false; + let gname: str; + gname.ptr = nil; gname.len = 0; + if (lc != nil) { + tn = lc.tnode; + } else { + tn = letvartnode(c, idxbase.str); + if (tn == nil) { tn = defvartnode(c, idxbase.str); }; + if (tn != nil) { isglobal = true; gname = idxbase.str; }; + }; + if (tn != nil) { let elemt: *node = nil; let baseisarray: bool = false; let tk: nkind = tn.kind; @@ -22342,14 +22362,26 @@ fn cgdot(c: *cgen, n: *node) void = { emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; - if (baseisarray) { - emitline("\tLEAQ\t"); - emitoff(lc.off: i64); - emitline("(BP), BX\n"); + if (isglobal) { + if (baseisarray) { + emitline("\tLEAQ\t"); + emitsymname(c, gname); + emitline("(SB), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitsymname(c, gname); + emitline("(SB), BX\n"); + }; } else { - emitline("\tMOVQ\t"); - emitoff(lc.off: i64); - emitline("(BP), BX\n"); + if (baseisarray) { + emitline("\tLEAQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + }; }; emitline("\tADDQ\tAX, BX\n"); if (viaptr) { @@ -22415,7 +22447,7 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; - };}; + }; };}; }; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 1aaf8246..556adaef 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -2562,8 +2562,28 @@ fn cgdot(c: *cgen, n: *node) void = { let idxbase: *node = lhs.lhs; if (idxbase != nil) { if (idxbase.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, idxbase.str); - if (lc != nil) { if (lc.tnode != nil) { - let tn: *node = lc.tnode; + // #21 (READ twin of #11): a module-GLOBAL base makes + // localfindnode return nil, so this field-offset-aware + // branch was skipped and `g[i].field` fell through to + // the module-qualified fallback below (garbage — no + // main.g load at all). Resolve the global's tnode via + // letvartnode/defvartnode (the same source cgindex's + // global arm uses) and dispatch the base load by shape: + // array -> LEAQ name(SB) (the symbol IS the storage), + // slice/ptr -> MOVQ name(SB) (the symbol's first word + // IS the .ptr). Mirrors cstage cgen.c's #21 arm. + let tn: *node = nil; + let isglobal: bool = false; + let gname: str; + gname.ptr = nil; gname.len = 0; + if (lc != nil) { + tn = lc.tnode; + } else { + tn = letvartnode(c, idxbase.str); + if (tn == nil) { tn = defvartnode(c, idxbase.str); }; + if (tn != nil) { isglobal = true; gname = idxbase.str; }; + }; + if (tn != nil) { let elemt: *node = nil; let baseisarray: bool = false; let tk: nkind = tn.kind; @@ -2598,14 +2618,26 @@ fn cgdot(c: *cgen, n: *node) void = { emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; - if (baseisarray) { - emitline("\tLEAQ\t"); - emitoff(lc.off: i64); - emitline("(BP), BX\n"); + if (isglobal) { + if (baseisarray) { + emitline("\tLEAQ\t"); + emitsymname(c, gname); + emitline("(SB), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitsymname(c, gname); + emitline("(SB), BX\n"); + }; } else { - emitline("\tMOVQ\t"); - emitoff(lc.off: i64); - emitline("(BP), BX\n"); + if (baseisarray) { + emitline("\tLEAQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + }; }; emitline("\tADDQ\tAX, BX\n"); if (viaptr) { @@ -2671,7 +2703,7 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; - };}; + }; };}; }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 75f68e3f..9832cd46 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -22306,8 +22306,28 @@ fn cgdot(c: *cgen, n: *node) void = { let idxbase: *node = lhs.lhs; if (idxbase != nil) { if (idxbase.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, idxbase.str); - if (lc != nil) { if (lc.tnode != nil) { - let tn: *node = lc.tnode; + // #21 (READ twin of #11): a module-GLOBAL base makes + // localfindnode return nil, so this field-offset-aware + // branch was skipped and `g[i].field` fell through to + // the module-qualified fallback below (garbage — no + // main.g load at all). Resolve the global's tnode via + // letvartnode/defvartnode (the same source cgindex's + // global arm uses) and dispatch the base load by shape: + // array -> LEAQ name(SB) (the symbol IS the storage), + // slice/ptr -> MOVQ name(SB) (the symbol's first word + // IS the .ptr). Mirrors cstage cgen.c's #21 arm. + let tn: *node = nil; + let isglobal: bool = false; + let gname: str; + gname.ptr = nil; gname.len = 0; + if (lc != nil) { + tn = lc.tnode; + } else { + tn = letvartnode(c, idxbase.str); + if (tn == nil) { tn = defvartnode(c, idxbase.str); }; + if (tn != nil) { isglobal = true; gname = idxbase.str; }; + }; + if (tn != nil) { let elemt: *node = nil; let baseisarray: bool = false; let tk: nkind = tn.kind; @@ -22342,14 +22362,26 @@ fn cgdot(c: *cgen, n: *node) void = { emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; - if (baseisarray) { - emitline("\tLEAQ\t"); - emitoff(lc.off: i64); - emitline("(BP), BX\n"); + if (isglobal) { + if (baseisarray) { + emitline("\tLEAQ\t"); + emitsymname(c, gname); + emitline("(SB), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitsymname(c, gname); + emitline("(SB), BX\n"); + }; } else { - emitline("\tMOVQ\t"); - emitoff(lc.off: i64); - emitline("(BP), BX\n"); + if (baseisarray) { + emitline("\tLEAQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(lc.off: i64); + emitline("(BP), BX\n"); + }; }; emitline("\tADDQ\tAX, BX\n"); if (viaptr) { @@ -22415,7 +22447,7 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; }; - };}; + }; };}; }; }; diff --git a/test/wcc/688_global_arr_elem_field.c b/test/wcc/688_global_arr_elem_field.c new file mode 100644 index 00000000..322060c5 --- /dev/null +++ b/test/wcc/688_global_arr_elem_field.c @@ -0,0 +1,281 @@ +/* + * 688_global_arr_elem_field — a module-GLOBAL indexed struct-element + * `.field` READ (`g[i].field`, g at module scope) lowers the field + * offset, not just the element stride. The READ twin of #11 (which + * fixed the global `g[i] = v` WRITE) and the #15 sibling. + * + * Pre-fix (#21): the `arr[i].field` N_DOT branch in both stages was + * gated on a LOCAL lookup (cstage localfind != 0, wwstage + * localfindnode != nil). A module-global base missed it and fell + * through — cstage to a generic index-load that drops f->offset + * (reads element[i] at offset 0, so g[0].b returned a's value), and + * wwstage to the module-qualified fallback (garbage — no g load). + * Byte-id-divergent silent miscompile. + * + * The fix (BOTH stages, converged byte-identical): resolve the global + * the same way the N_INDEX arm does (cstage let_islet || def_isarraydef; + * wwstage letvartnode || defvartnode) and dispatch the base load by + * shape — array -> LEAQ name(SB) (the symbol IS the storage), + * slice/ptr -> MOVQ name(SB) (the symbol's first word IS the .ptr) — + * then load the field at f->offset, exactly as the local arm does. + * esz = element stride and f->offset both come from the type table. + * + * Coverage: + * arr_a0/a1 | [2]pt struct array, g[0].a / g[1].a — control, + * | offset 0. RUNTIME + byte-id. + * arr_b0/b1 | [2]pt, g[0].b / g[1].b — THE bug (offset 8, the + * | reads that returned a's value pre-fix). RUNTIME. + * rec_* | [2]rec { tag:u8, x:i32, y:i64 } — non-8-aligned + * | fields (x@4, y@8) + a u8 sub-word leaf, to stress + * | f->offset and the load-width path. RUNTIME. + * slice_x1 | [2]rec backing + `let g: []rec = arr;`, g[1].x + * | read. Exercises the slice-base MOVQ-deref arm + * | (MOVQ g(SB),BX loads .ptr) — the asm is byte-id and + * | correct, but a slice-of-struct module global does + * | not data-emit a symbol yet (a separate, pre-existing + * | data-emission gap, sibling of #10/#20), so it cannot + * | LINK/run. Asserted byte-id ONLY; filed separately. + * + * Why this was never caught: 680/681 cover only LOCAL `[N]struct` + * bases (BP-relative). The global base routes through name(SB) and was + * a distinct, untested arm. + * + * Cstage and wwstage each on every runtime fixture; wwstage gated on + * access(X_OK). All fixtures additionally asm-byte-id checked. + */ +#include +#include +#include +#include +#include +#include + +/* want == BYTEID_ONLY: the row cannot LINK/run (a slice-of-struct + * module global does not data-emit yet — separate gap), so only the + * cstage/wwstage asm-byte-id is asserted, never an exit value. */ +#define BYTEID_ONLY (-2147483647 - 1) + +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[] = { + /* [2]pt, .a field at offset 0 — control. */ + { "arr_a0", + "type pt = struct { a: i64, b: i64 };\n" + "let g: [2]pt = [pt { a = 1, b = 2 }, pt { a = 3, b = 4 }];\n" + "export fn main() i32 = { return g[0].a: i32; };\n", + 1 }, + { "arr_a1", + "type pt = struct { a: i64, b: i64 };\n" + "let g: [2]pt = [pt { a = 1, b = 2 }, pt { a = 3, b = 4 }];\n" + "export fn main() i32 = { return g[1].a: i32; };\n", + 3 }, + /* [2]pt, .b at offset 8 — THE bug: pre-fix g[i].b returned a's + * value (cstage) or garbage (wwstage). */ + { "arr_b0", + "type pt = struct { a: i64, b: i64 };\n" + "let g: [2]pt = [pt { a = 1, b = 2 }, pt { a = 3, b = 4 }];\n" + "export fn main() i32 = { return g[0].b: i32; };\n", + 2 }, + { "arr_b1", + "type pt = struct { a: i64, b: i64 };\n" + "let g: [2]pt = [pt { a = 1, b = 2 }, pt { a = 3, b = 4 }];\n" + "export fn main() i32 = { return g[1].b: i32; };\n", + 4 }, + /* [2]rec, non-8-aligned i32 field (x@4) on the second element — + * stresses f->offset combined with the element stride. */ + { "rec_x1", + "type rec = struct { tag: u8, x: i32, y: i64 };\n" + "let g: [2]rec = [rec { tag = 1u8, x = 100, y = 11 }," + " rec { tag = 2u8, x = 200, y = 22 }];\n" + "export fn main() i32 = { return g[1].x: i32; };\n", + 200 }, + /* [2]rec, i64 field (y@8) on the second element. */ + { "rec_y1", + "type rec = struct { tag: u8, x: i32, y: i64 };\n" + "let g: [2]rec = [rec { tag = 1u8, x = 100, y = 11 }," + " rec { tag = 2u8, x = 200, y = 22 }];\n" + "export fn main() i32 = { return g[1].y: i32; };\n", + 22 }, + /* [2]rec, u8 leaf (tag@0) — sub-word zero-extend load on a + * global element base. */ + { "rec_tag1", + "type rec = struct { tag: u8, x: i32, y: i64 };\n" + "let g: [2]rec = [rec { tag = 1u8, x = 100, y = 11 }," + " rec { tag = 2u8, x = 200, y = 22 }];\n" + "export fn main() i32 = { return g[1].tag: i32; };\n", + 2 }, + /* slice-base MOVQ-deref arm: `let g: []rec = arr;`, g[1].b read. + * Byte-id only — a slice-of-struct module global does not + * data-emit yet (separate gap), so it cannot link/run. The .s + * holds MOVQ g(SB),BX (the .ptr load) + the correct f->offset on + * both stages. */ + { "slice_x1", + "type rec = struct { tag: u8, x: i32, y: i64 };\n" + "let arr: [2]rec = [rec { tag = 1u8, x = 100, y = 11 }," + " rec { tag = 2u8, x = 200, y = 22 }];\n" + "let g: []rec = arr;\n" + "export fn main() i32 = { return g[1].x: i32; };\n", + BYTEID_ONLY }, +}; + +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/gae_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/gae_%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 — generate .s via cstage's w6c and wwstage's + * w6c_ww and diff. Pins rule-10 convergence: pre-fix the global-base + * read diverged (cstage dropped f->offset, wwstage emitted no g load). */ +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/gae_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/gae_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/gae_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, "global_arr_elem_field: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + if (rows[i].want == BYTEID_ONLY) + continue; /* byte-id only below */ + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != rows[i].want) { + fprintf(stderr, + "global_arr_elem_field[%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, + "global_arr_elem_field: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("global_arr_elem_field: %d/%d ok\n", total, total); + return 0; +}