From 00d9580c9fae338eb3b82bdaea1a7065ed234f66 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 6 Jun 2026 14:10:17 +0900 Subject: [PATCH] wcc/cgen: #84 uninit [N]T array zero-fill (both-stage) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the `!TY_ARRAY` exclusion in the bare-let no-rhs zero-fill (cgen.c N_LET else + cgenstmt.ww cglet, both gated `sz>8 && !TY_ARRAY`) so an uninit `[N]T` array local zero-fills like every other composite (Go-zero per user ruling). The zero-fill extent is the array's chased ABI size (lu->size / chased tinfo.size, rule-13 — never a hardcoded count*esz), NOT the slot-padded letslotsize, so a non-8-multiple array ([20]u8 = 20) zeroes its exact bytes instead of over-zeroing to the 24B slot. The unrolled MOVQ/MOVL/MOVB run mirrors the existing composite path; the largest real local array ([256]u8) is 32 MOVQs (pathbuf[4096] is a module GLOBAL, BSS-filled — never on this stack path, so no large-fill case exists). Closes a gate-blind #263-class bug: `let a: [3]int;` (no init) read whatever the stack held — a clean frame masked it (fresh stack = 0), a dirtied frame exposed it (d_array=165 garbage). BOTH stages emitted no fill, both-wrong-IDENTICAL, so the cs==ww byte-id net could not see it. The load-bearing net is therefore a RUNTIME dirtied-stack zero-read (944_array_zeroinit_run: array-elem / narrow [4]u32 / non-8-mult [20]u8 / 2D + an initialized control), not asm presence. Deliberate byte-id EVENT: every uninit-array source site gains zero-fill insns, so the 990-997 .s MOVE vs the prior tree; cs==ww HOLDS (both add the identical insns). The 990-997 byte-id + 995 self-rebuild staying GREEN is the fixpoint proof — it proves every uninit compiler-array is write-before-read, so the zero-fill is purely additive and the ww1->ww2->ww3 self-rebuild fixpoint holds by construction. w6c/wwdump main.combined.ww regenerated (cgenstmt.ww embeds there). #84 is ARRAY-ONLY; the no-default reject-set (uninit tagged / plain-*T) is split to #113, parked behind a ruling — selfhost relies on the current (void|T) zero-fill (the "not-set-yet" idiom). --- Makefile | 7 + cmd/w6c/cgen.c | 16 +- selfhost/cmd/w6c/main.combined.ww | 53 +++---- selfhost/cmd/wcc/cgenstmt.ww | 53 +++---- selfhost/cmd/wwdump/main.combined.ww | 53 +++---- test/wcc/944_array_zeroinit_run.c | 227 +++++++++++++++++++++++++++ 6 files changed, 312 insertions(+), 97 deletions(-) create mode 100644 test/wcc/944_array_zeroinit_run.c diff --git a/Makefile b/Makefile index 895a822b..c7d55658 100644 --- a/Makefile +++ b/Makefile @@ -304,6 +304,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_deref_slice_store_run \ $(BIN)/test_alias_decl_order_size_run \ $(BIN)/test_alias_structlit_init_run \ + $(BIN)/test_array_zeroinit_run \ $(BIN)/test_alias_accept_run \ $(BIN)/test_alias_idx_family_run \ $(BIN)/test_alias_amp_idx_run \ @@ -1481,6 +1482,12 @@ $(BIN)/test_alias_structlit_init_run: test/wcc/944_alias_structlit_init_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_array_zeroinit_run: test/wcc/944_array_zeroinit_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_alias_accept_run: test/wcc/944_alias_accept_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/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index f041e2e4..6e689ace 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -12265,12 +12265,20 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) } } else if (sz == 8) { ins2(c, A_MOVQ, aimm(0), amem(D_BP, off)); - } else if (!n->rhs && sz > 8 && lu && lu->kind != TY_ARRAY) { + } else if (!n->rhs && sz > 8) { /* `let x: T;` with no rhs for a multi-word composite - * (str/slice/tuple/struct/tagged). Zero the slot so + * (str/slice/tuple/struct/tagged/ARRAY). Zero the slot so * reads after the bare let see {0...} rather than - * whatever the stack already held. Arrays keep the - * per-index-write contract — leave them uninit. */ + * whatever the stack already held. + * + * #84: arrays were excluded here (`!TY_ARRAY`), so a + * dirtied-stack `let a: [3]int;` read garbage — BOTH + * stages, both-wrong-IDENTICAL, gate-blind (#263). Go-zero + * (user ruling): arrays zero-fill like every other + * composite. Extent is lu->size (chased ABI size, rule-13 + * — never a hardcoded count×elemsize). The unrolled + * word/dword/byte run mirrors the composite path; the + * largest real local array ([256]u8) is 32 MOVQs. */ ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); int zi = 0; while (zi + 8 <= sz) { diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index ed16ca43..840a66e2 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -35525,33 +35525,14 @@ fn cglet(c: *cgen, n: *node) void = { // `XORQ AX,AX` + a run of `MOVQ AX, ...` over the slot // so reads after the bare let see {0...} rather than // stack garbage. - // `[N]T` arrays of size != 8 keep the per-index-write - // contract — they're left uninit. - let isarr: bool = false; - if (n.lhs != nil) { - if (n.lhs.kind == nkind.N_TARRAY) { isarr = true; }; - }; - // #79 rider (#60 family): alias-NAMED bare let — the array - // classify must see through the N_TNAME leaf or an alias- - // to-array takes the composite zero-fill cstage doesn't - // emit (cstage keys on the chased lu->kind: arrays keep the - // per-index-write contract). An 8B alias-array stays - // isarr=false so it falls to the zsz==8 MOVQ $0 arm, - // matching cstage's 8B single-store shape. - if (!isarr && n.lhs != nil) { - if (n.lhs.kind == nkind.N_TNAME) { - let ati79: *tinfo = n.lhs.type_: *tinfo; - if (ati79 != nil) { if (ati79.kind == tykind.TY_NAMED) { - let au79: *tinfo = tichase(ati79); - if (au79 != nil) { - if (au79.kind == tykind.TY_ARRAY - && au79.size: i32 != 8) { - isarr = true; - }; - }; - };}; - }; - }; + // #84 (user ruling, Go-zero): `[N]T` arrays zero-fill like + // every other composite. They were excluded here, so a + // dirtied-stack `let a: [3]int;` read garbage — BOTH stages, + // both-wrong-IDENTICAL, gate-blind (#263). Dropping the + // exclusion (cstage dropped `!TY_ARRAY` symmetrically) routes + // arrays into the zsz>8 / zsz==8 arms below; an 8B array is + // already caught by typeis8byteprimitive (TY_ARRAY size==8) → + // single MOVQ $0, matching cstage's sz==8 store. // Zero-fill extent. cstage sizes the run on `lu->size` // (the natural ABI size from the type table, cgen.c:8397); // wwstage's `sz` from letslotsize is slot-padded (round-to-8), @@ -35567,20 +35548,30 @@ fn cglet(c: *cgen, n: *node) void = { // UNTOUCHED — moving the fix there would shift field offsets. let zsz: i32 = sz; if (n.lhs != nil) { - if (n.lhs.kind == nkind.N_TNAME) { + // #84: an array's zero-fill extent is its chased ABI + // size (cstage `lu->size`), NOT the slot-padded sz from + // letslotsize — a non-8-multiple array (e.g. [20]u8 = 20) + // would over-zero MOVQ-rounded to 24 and diverge from + // cstage's exact 20-byte run. Handles direct N_TARRAY and + // alias-to-array (N_TNAME chasing through TY_NAMED) alike. + let zti: *tinfo = n.lhs.type_: *tinfo; + zti = tichase(zti); + if (zti != nil && zti.kind == tykind.TY_ARRAY) { + zsz = zti.size: i32; + } else { if (n.lhs.kind == nkind.N_TNAME) { let szi: *structinfo = structlookupchain(c, n.lhs); if (szi != nil) { let ti: *tinfo = n.lhs.type_: *tinfo; ti = tichase(ti); if (ti != nil) { zsz = ti.size: i32; }; }; - }; + }; }; }; if (typeis8byteprimitive(c, n.lhs)) { emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); - } else { if (!isarr) { if (zsz > 8) { + } else { if (zsz > 8) { emitline("\tXORQ\tAX, AX\n"); let zi: i32 = 0; for (zi + 8 <= zsz) { @@ -35614,7 +35605,7 @@ fn cglet(c: *cgen, n: *node) void = { emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); - }; }; }; }; + }; }; }; }; c.lastwasreturn = 0; return; diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 514b5953..2387fb6e 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -2982,33 +2982,14 @@ fn cglet(c: *cgen, n: *node) void = { // `XORQ AX,AX` + a run of `MOVQ AX, ...` over the slot // so reads after the bare let see {0...} rather than // stack garbage. - // `[N]T` arrays of size != 8 keep the per-index-write - // contract — they're left uninit. - let isarr: bool = false; - if (n.lhs != nil) { - if (n.lhs.kind == nkind.N_TARRAY) { isarr = true; }; - }; - // #79 rider (#60 family): alias-NAMED bare let — the array - // classify must see through the N_TNAME leaf or an alias- - // to-array takes the composite zero-fill cstage doesn't - // emit (cstage keys on the chased lu->kind: arrays keep the - // per-index-write contract). An 8B alias-array stays - // isarr=false so it falls to the zsz==8 MOVQ $0 arm, - // matching cstage's 8B single-store shape. - if (!isarr && n.lhs != nil) { - if (n.lhs.kind == nkind.N_TNAME) { - let ati79: *tinfo = n.lhs.type_: *tinfo; - if (ati79 != nil) { if (ati79.kind == tykind.TY_NAMED) { - let au79: *tinfo = tichase(ati79); - if (au79 != nil) { - if (au79.kind == tykind.TY_ARRAY - && au79.size: i32 != 8) { - isarr = true; - }; - }; - };}; - }; - }; + // #84 (user ruling, Go-zero): `[N]T` arrays zero-fill like + // every other composite. They were excluded here, so a + // dirtied-stack `let a: [3]int;` read garbage — BOTH stages, + // both-wrong-IDENTICAL, gate-blind (#263). Dropping the + // exclusion (cstage dropped `!TY_ARRAY` symmetrically) routes + // arrays into the zsz>8 / zsz==8 arms below; an 8B array is + // already caught by typeis8byteprimitive (TY_ARRAY size==8) → + // single MOVQ $0, matching cstage's sz==8 store. // Zero-fill extent. cstage sizes the run on `lu->size` // (the natural ABI size from the type table, cgen.c:8397); // wwstage's `sz` from letslotsize is slot-padded (round-to-8), @@ -3024,20 +3005,30 @@ fn cglet(c: *cgen, n: *node) void = { // UNTOUCHED — moving the fix there would shift field offsets. let zsz: i32 = sz; if (n.lhs != nil) { - if (n.lhs.kind == nkind.N_TNAME) { + // #84: an array's zero-fill extent is its chased ABI + // size (cstage `lu->size`), NOT the slot-padded sz from + // letslotsize — a non-8-multiple array (e.g. [20]u8 = 20) + // would over-zero MOVQ-rounded to 24 and diverge from + // cstage's exact 20-byte run. Handles direct N_TARRAY and + // alias-to-array (N_TNAME chasing through TY_NAMED) alike. + let zti: *tinfo = n.lhs.type_: *tinfo; + zti = tichase(zti); + if (zti != nil && zti.kind == tykind.TY_ARRAY) { + zsz = zti.size: i32; + } else { if (n.lhs.kind == nkind.N_TNAME) { let szi: *structinfo = structlookupchain(c, n.lhs); if (szi != nil) { let ti: *tinfo = n.lhs.type_: *tinfo; ti = tichase(ti); if (ti != nil) { zsz = ti.size: i32; }; }; - }; + }; }; }; if (typeis8byteprimitive(c, n.lhs)) { emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); - } else { if (!isarr) { if (zsz > 8) { + } else { if (zsz > 8) { emitline("\tXORQ\tAX, AX\n"); let zi: i32 = 0; for (zi + 8 <= zsz) { @@ -3071,7 +3062,7 @@ fn cglet(c: *cgen, n: *node) void = { emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); - }; }; }; }; + }; }; }; }; c.lastwasreturn = 0; return; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index e9fa0859..b0424502 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -35525,33 +35525,14 @@ fn cglet(c: *cgen, n: *node) void = { // `XORQ AX,AX` + a run of `MOVQ AX, ...` over the slot // so reads after the bare let see {0...} rather than // stack garbage. - // `[N]T` arrays of size != 8 keep the per-index-write - // contract — they're left uninit. - let isarr: bool = false; - if (n.lhs != nil) { - if (n.lhs.kind == nkind.N_TARRAY) { isarr = true; }; - }; - // #79 rider (#60 family): alias-NAMED bare let — the array - // classify must see through the N_TNAME leaf or an alias- - // to-array takes the composite zero-fill cstage doesn't - // emit (cstage keys on the chased lu->kind: arrays keep the - // per-index-write contract). An 8B alias-array stays - // isarr=false so it falls to the zsz==8 MOVQ $0 arm, - // matching cstage's 8B single-store shape. - if (!isarr && n.lhs != nil) { - if (n.lhs.kind == nkind.N_TNAME) { - let ati79: *tinfo = n.lhs.type_: *tinfo; - if (ati79 != nil) { if (ati79.kind == tykind.TY_NAMED) { - let au79: *tinfo = tichase(ati79); - if (au79 != nil) { - if (au79.kind == tykind.TY_ARRAY - && au79.size: i32 != 8) { - isarr = true; - }; - }; - };}; - }; - }; + // #84 (user ruling, Go-zero): `[N]T` arrays zero-fill like + // every other composite. They were excluded here, so a + // dirtied-stack `let a: [3]int;` read garbage — BOTH stages, + // both-wrong-IDENTICAL, gate-blind (#263). Dropping the + // exclusion (cstage dropped `!TY_ARRAY` symmetrically) routes + // arrays into the zsz>8 / zsz==8 arms below; an 8B array is + // already caught by typeis8byteprimitive (TY_ARRAY size==8) → + // single MOVQ $0, matching cstage's sz==8 store. // Zero-fill extent. cstage sizes the run on `lu->size` // (the natural ABI size from the type table, cgen.c:8397); // wwstage's `sz` from letslotsize is slot-padded (round-to-8), @@ -35567,20 +35548,30 @@ fn cglet(c: *cgen, n: *node) void = { // UNTOUCHED — moving the fix there would shift field offsets. let zsz: i32 = sz; if (n.lhs != nil) { - if (n.lhs.kind == nkind.N_TNAME) { + // #84: an array's zero-fill extent is its chased ABI + // size (cstage `lu->size`), NOT the slot-padded sz from + // letslotsize — a non-8-multiple array (e.g. [20]u8 = 20) + // would over-zero MOVQ-rounded to 24 and diverge from + // cstage's exact 20-byte run. Handles direct N_TARRAY and + // alias-to-array (N_TNAME chasing through TY_NAMED) alike. + let zti: *tinfo = n.lhs.type_: *tinfo; + zti = tichase(zti); + if (zti != nil && zti.kind == tykind.TY_ARRAY) { + zsz = zti.size: i32; + } else { if (n.lhs.kind == nkind.N_TNAME) { let szi: *structinfo = structlookupchain(c, n.lhs); if (szi != nil) { let ti: *tinfo = n.lhs.type_: *tinfo; ti = tichase(ti); if (ti != nil) { zsz = ti.size: i32; }; }; - }; + }; }; }; if (typeis8byteprimitive(c, n.lhs)) { emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); - } else { if (!isarr) { if (zsz > 8) { + } else { if (zsz > 8) { emitline("\tXORQ\tAX, AX\n"); let zi: i32 = 0; for (zi + 8 <= zsz) { @@ -35614,7 +35605,7 @@ fn cglet(c: *cgen, n: *node) void = { emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); - }; }; }; }; + }; }; }; }; c.lastwasreturn = 0; return; diff --git a/test/wcc/944_array_zeroinit_run.c b/test/wcc/944_array_zeroinit_run.c new file mode 100644 index 00000000..e7cb85fb --- /dev/null +++ b/test/wcc/944_array_zeroinit_run.c @@ -0,0 +1,227 @@ +/* + * 944_array_zeroinit_run — #84: uninit `[N]T` ARRAY locals were NEVER + * zero-filled. The bare-let no-rhs zero-fill (cmd/w6c/cgen.c N_LET else + * + selfhost/cmd/wcc/cgenstmt.ww cglet) was gated `sz>8 && !TY_ARRAY`, + * so `let a: [3]int;` read whatever the stack held — BOTH stages, + * both-wrong-IDENTICAL, byte-id-blind (#263-class): the cs==ww gate + * cannot see a shared-garbage read. The fix drops the `!TY_ARRAY` + * exclusion symmetrically (arrays zero-fill like every other + * composite, Go-zero per user ruling); the zero-fill extent is the + * array's chased ABI size (lu->size / chased tinfo.size, NOT the + * slot-padded size), so a non-8-multiple array zeroes its exact byte + * count. + * + * GATE-BLIND → the load-bearing net is a RUNTIME zero-read, NOT asm + * presence: a clean (fresh) stack frame is already 0, masking the bug. + * So every row DIRTIES the stack first (a filler fn writes 165 across + * a 512B frame), then declares the uninit array in a sibling frame + * that reuses that region, and asserts the read is 0. byte-id won't + * catch a regression here — only run-and-check-value will. + * + * row | shape (after a 165-dirtied stack) | want + * -----------------+------------------------------------------+----- + * array_elem_0 | let a: [3]int; sum a[0..2] | 0 + * narrow_array_0 | let a: [4]u32; sum a[0..3] (esz 4) | 0 + * nonmult8_array_0 | let b: [20]u8; sum b[0..19] (ABI 20 != | + * | slot 24 — extent-source guard) | 0 + * array_2d_0 | let m: [2][2]int; sum all 4 | 0 + * control_initd | let c: [3]int = [7,8,9]; sum (no-regress)| 24 + * + * Each row also asserts cstage/wwstage .s byte-id (rule 10 — the fix + * adds the SAME insns to both). NNN<950, self-contained (/tmp, no + * imports) — rule-14's selfhost-sibling race does not apply (941/944 + * precedent). #84 array-only; the tagged/plain-*T reject-set is #113. + */ +#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; +} + +struct row { const char *label; const char *src; int want; }; + +/* Every src dirties the stack via dirty() (a 512B [64]int filled with + * 165) before the reader's uninit array lands on that region. */ +#define DIRTY \ + "fn dirty() int = {\n" \ + " let j: [64]int;\n" \ + " for (let i: int = 0; i < 64; i += 1) { j[i] = 165; };\n" \ + " return j[0];\n" \ + "};\n" + +static const struct row rows[] = { + { "array_elem_0", + "package main;\n" DIRTY + "fn rd() i32 = { let a: [3]int; return (a[0]+a[1]+a[2]): i32; };\n" + "export fn main() i32 = { dirty(); return rd(); };\n", 0 }, + { "narrow_array_0", + "package main;\n" DIRTY + "fn rd() i32 = { let a: [4]u32;\n" + " return (a[0]+a[1]+a[2]+a[3]): i32; };\n" + "export fn main() i32 = { dirty(); return rd(); };\n", 0 }, + { "nonmult8_array_0", + "package main;\n" DIRTY + "fn rd() i32 = { let b: [20]u8; let s: i32 = 0;\n" + " for (let i: int = 0; i < 20; i += 1) { s += b[i]: i32; };\n" + " return s; };\n" + "export fn main() i32 = { dirty(); return rd(); };\n", 0 }, + { "array_2d_0", + "package main;\n" DIRTY + "fn rd() i32 = { let m: [2][2]int;\n" + " return (m[0][0]+m[0][1]+m[1][0]+m[1][1]): i32; };\n" + "export fn main() i32 = { dirty(); return rd(); };\n", 0 }, + /* no-regress control: an INITIALIZED array keeps its values. */ + { "control_initd", + "package main;\n" DIRTY + "fn rd() i32 = { let c: [3]int = [7, 8, 9];\n" + " return (c[0]+c[1]+c[2]): i32; };\n" + "export fn main() i32 = { dirty(); return rd(); };\n", 24 }, +}; + +/* build+run via a driver (ww / ww_ww); returns 0 pass, nonzero fail. */ +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/azi_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/azi_%d_d_%d", getpid(), i); + snprintf(errf, sizeof errf, "/tmp/azi_%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 && timeout 20 %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; +} + +/* cs==ww .s byte-id (rule 10). */ +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/azi_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/azi_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/azi_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++) { + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) fail++; + } + } + + if (fail) { + fprintf(stderr, "array_zeroinit: %d/%d checks failed\n", + fail, total); + return 1; + } + printf("array_zeroinit: %d/%d ok\n", total, total); + return 0; +}