From 2c09d13ca349d816482f9c26c61599272b0e3e40 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sun, 7 Jun 2026 12:10:38 +0900 Subject: [PATCH] =?UTF-8?q?wcc/cgen:=20#59=20append/insert=20struct-litera?= =?UTF-8?q?l=20value=20eval-order=20=E2=80=94=20eval-to-scratch=20pre-grow?= =?UTF-8?q?=20+=20precise=20copy=20(both-stage)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit append/insert of a struct-LITERAL value evaluated the literal's field exprs AFTER the grow, so a field reading the destination (e.g. len(xs)) saw the grown length. Both stages, #263 gate-blind (cs==ww byte-identical, both wrong — runtime is the only net). #50 fixed the scalar/boxing value arm; the struct-lit arm still post-grew. Fix (mirror #50, both stages): resolve the struct, fill the literal into a fresh per-site scratch (@appendstructscr, sized esz, survives rt_ensure + nested-append clobber) BEFORE the grow, then copy scratch -> post-grow slot. The copy uses the precise descending 8/4/2/1 ladder (the proven N_IDENT struct arm directly below), NOT a raw 8B-word block copy: a struct's size rounds to maxalign (check.c:916), so a sub-8B struct packs at a 4/2/1B slice stride and an 8B copy over-writes past the slot — at a power-of-2 capacity boundary that clobbers the adjacent allocation (heap corruption, both stages). The ladder never reads past esz (no uninit high bytes) nor writes past the slot; esz=8 stays a single MOVQ (byte-id preserved). insert() rides by construction: both stages desugar it to append and re-dispatch into this arm. The #49 aplace path already uses the precise ladder (verified, not exposed). #59 closes the last composite-value eval-order hole in append/insert. Pin: 946_append_structlit_evalorder_run — append / insert / narrow-neighbor (i32-field at the cap boundary with an adjacent-allocation survival assert) rows, each base-fail at 39432f7 and post-pass with cs==ww byte-id. --- Makefile | 7 + cmd/w6c/cgen.c | 68 +++-- selfhost/cmd/w6c/main.combined.ww | 70 ++++- selfhost/cmd/wcc/cgenexpr.ww | 70 ++++- selfhost/cmd/wwdump/main.combined.ww | 70 ++++- test/wcc/946_append_structlit_evalorder_run.c | 252 ++++++++++++++++++ 6 files changed, 491 insertions(+), 46 deletions(-) create mode 100644 test/wcc/946_append_structlit_evalorder_run.c diff --git a/Makefile b/Makefile index 8edf338d..480fe229 100644 --- a/Makefile +++ b/Makefile @@ -327,6 +327,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tuple_in_union_run \ $(BIN)/test_nonlit_tuple_widen_run \ $(BIN)/test_const_slice_aggregate_run \ + $(BIN)/test_append_structlit_evalorder_run \ $(BIN)/test_tuple_index_read_run \ $(BIN)/test_xmod_fnptr_const_run \ $(BIN)/test_tuple_slot_layout_run \ @@ -1651,6 +1652,12 @@ $(BIN)/test_const_slice_aggregate_run: test/wcc/946_const_slice_aggregate_run.c $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_append_structlit_evalorder_run: test/wcc/946_append_structlit_evalorder_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_tuple_index_read_run: test/wcc/947_tuple_index_read_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 84e638ef..07b2956f 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -8570,26 +8570,62 @@ cgexpr(Cg *c, Node *n, Local *locals) } continue; } + if (vn->kind == N_STRUCTLIT) { + /* #59 (#50's eval-order kin): + * fill the literal into a fresh + * per-SITE scratch (must survive + * rt_ensure + a nested append in a + * field expr; the @apptagscr + * rationale, #25/#31) BEFORE the + * grow, so the field exprs see the + * pre-grow len. Then grow, slot, + * raw-copy scratch->slot (mirror + * the #50 tagged arm above). */ + int st_scr = local_alloc(c, + &locals, "@appendstructscr", + esz, cg_frame); + cg_structlit_fill_bp(c, &locals, + esubu, vn, st_scr); + cg_append_grow(c, sn_direct, sn_off, + sn_scr, esz); + cg_append_slot(c, sn_direct, sn_off, + sn_scr, esz, D_BX); + /* Descending 8/4/2/1 ladder, not an + * 8B-word loop: a plain struct's esz + * rounds to maxalign (check.c:916), + * not 8, so a sub-8B / non-8B-multiple + * element packs at its own stride — + * an 8B copy of the last slot writes + * past the slice buffer (the tagged + * arm above is safe only because boxes + * are 8B-padded; #59). Mirrors the + * N_IDENT source arm below. */ + int ck = 0; + for (; ck + 8 <= esz; ck += 8) { + ins2(c, A_MOVQ, amem(D_BP, st_scr + ck), areg(D_AX)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, ck)); + } + if (ck + 4 <= esz) { + ins2(c, A_MOVL, amem(D_BP, st_scr + ck), areg(D_AX)); + ins2(c, A_MOVL, areg(D_AX), amem(D_BX, ck)); + ck += 4; + } + if (ck + 2 <= esz) { + ins2(c, A_MOVW, amem(D_BP, st_scr + ck), areg(D_AX)); + ins2(c, A_MOVW, areg(D_AX), amem(D_BX, ck)); + ck += 2; + } + if (ck + 1 <= esz) { + ins2(c, A_MOVB, amem(D_BP, st_scr + ck), areg(D_AX)); + ins2(c, A_MOVB, areg(D_AX), amem(D_BX, ck)); + ck += 1; + } + continue; + } cg_append_grow(c, sn_direct, sn_off, sn_scr, esz); cg_append_slot(c, sn_direct, sn_off, sn_scr, esz, D_BX); - if (vn->kind == N_STRUCTLIT) { - /* #59 (#50's eval-order kin): - * the literal's field exprs - * still eval POST-grow here — - * filed, not folded. */ - if (cg_appendscr == 0) - cg_appendscr = local_alloc(c, - &locals, "@appendscr", 8, - cg_frame); - ins2(c, A_MOVQ, areg(D_BX), - amem(D_BP, cg_appendscr)); - cg_structlit_fill(c, &locals, esubu, - vn, DST_PTR_LOCAL, cg_appendscr, - NULL, 0); - continue; - } if (vn->kind == N_IDENT) { int soff = localfind(locals, vn->str); if (soff == 0) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index c282f1be..e23f7595 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -27470,16 +27470,16 @@ fn cgappend(c: *cgen, n: *node) void = { vn = vn.next; continue; }; - cgappendgrow(c, sndirect, sn_off, snscr, esz); - cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); if (vn.kind == nkind.N_STRUCTLIT) { - // #59 (#50's eval-order kin): the literal's field - // exprs still eval POST-grow here — filed, not - // folded. - let scroff: i32 = localadd(c, "@appendscr", 8, nil); - emitline("\tMOVQ\tBX, "); - emitoff(scroff: i64); - emitline("(BP)\n"); + // #59 (#50's eval-order kin): resolve the struct + // info, then fill the literal into a fresh + // per-SITE scratch (must stay live across + // rt_ensure, and a nested append in a field expr + // would clobber a dedup'd slot — the @apptagscr + // rationale, #25/#31) BEFORE the grow, so the + // field exprs see the pre-grow len. Then grow, + // slot, raw-copy scratch->slot (mirror the #50 + // tagged arm above). let esi: *structinfo = structlookupchain(c, etnode); if (esi == nil && !sndirect && esubnamed != nil) { // FA1: no declared tnode to chain through — @@ -27494,10 +27494,60 @@ fn cgappend(c: *cgen, n: *node) void = { os.write(2, m34s.ptr, m34s.len: u64); os.exit(1); }; - cgstructlitfill(c, esi, vn, 1, scroff, "", 0); + let stscr: i32 = localalloc(c, "@appendstructscr", esz, nil); + cgstructlitfillbp(c, esi, vn, stscr); + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); + // Descending 8/4/2/1 ladder, not an 8B-word + // loop: a plain struct's esz rounds to maxalign + // (check.c:916), not 8, so a sub-8B / non-8B- + // multiple element packs at its own stride — an + // 8B copy of the last slot writes past the slice + // buffer (the tagged arm above is safe only + // because boxes are 8B-padded; #59). Mirrors the + // N_IDENT source arm below. + let ck: i32 = 0; + for (ck + 8 <= esz) { + emitline("\tMOVQ\t"); + emitoff((stscr + ck): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg(ck: i64, "BX"); + emitline("\n"); + ck += 8; + }; + if (ck + 4 <= esz) { + emitline("\tMOVL\t"); + emitoff((stscr + ck): i64); + emitline("(BP), AX\n"); + emitline("\tMOVL\tAX, "); + emitdispreg(ck: i64, "BX"); + emitline("\n"); + ck += 4; + }; + if (ck + 2 <= esz) { + emitline("\tMOVW\t"); + emitoff((stscr + ck): i64); + emitline("(BP), AX\n"); + emitline("\tMOVW\tAX, "); + emitdispreg(ck: i64, "BX"); + emitline("\n"); + ck += 2; + }; + if (ck + 1 <= esz) { + emitline("\tMOVB\t"); + emitoff((stscr + ck): i64); + emitline("(BP), AX\n"); + emitline("\tMOVB\tAX, "); + emitdispreg(ck: i64, "BX"); + emitline("\n"); + ck += 1; + }; vn = vn.next; continue; }; + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); if (vn.kind == nkind.N_IDENT) { let sl: *local = localfindnode(c, vn.str); if (sl == nil) { diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 2be69b80..bc096fd3 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -6004,16 +6004,16 @@ fn cgappend(c: *cgen, n: *node) void = { vn = vn.next; continue; }; - cgappendgrow(c, sndirect, sn_off, snscr, esz); - cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); if (vn.kind == nkind.N_STRUCTLIT) { - // #59 (#50's eval-order kin): the literal's field - // exprs still eval POST-grow here — filed, not - // folded. - let scroff: i32 = localadd(c, "@appendscr", 8, nil); - emitline("\tMOVQ\tBX, "); - emitoff(scroff: i64); - emitline("(BP)\n"); + // #59 (#50's eval-order kin): resolve the struct + // info, then fill the literal into a fresh + // per-SITE scratch (must stay live across + // rt_ensure, and a nested append in a field expr + // would clobber a dedup'd slot — the @apptagscr + // rationale, #25/#31) BEFORE the grow, so the + // field exprs see the pre-grow len. Then grow, + // slot, raw-copy scratch->slot (mirror the #50 + // tagged arm above). let esi: *structinfo = structlookupchain(c, etnode); if (esi == nil && !sndirect && esubnamed != nil) { // FA1: no declared tnode to chain through — @@ -6028,10 +6028,60 @@ fn cgappend(c: *cgen, n: *node) void = { os.write(2, m34s.ptr, m34s.len: u64); os.exit(1); }; - cgstructlitfill(c, esi, vn, 1, scroff, "", 0); + let stscr: i32 = localalloc(c, "@appendstructscr", esz, nil); + cgstructlitfillbp(c, esi, vn, stscr); + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); + // Descending 8/4/2/1 ladder, not an 8B-word + // loop: a plain struct's esz rounds to maxalign + // (check.c:916), not 8, so a sub-8B / non-8B- + // multiple element packs at its own stride — an + // 8B copy of the last slot writes past the slice + // buffer (the tagged arm above is safe only + // because boxes are 8B-padded; #59). Mirrors the + // N_IDENT source arm below. + let ck: i32 = 0; + for (ck + 8 <= esz) { + emitline("\tMOVQ\t"); + emitoff((stscr + ck): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg(ck: i64, "BX"); + emitline("\n"); + ck += 8; + }; + if (ck + 4 <= esz) { + emitline("\tMOVL\t"); + emitoff((stscr + ck): i64); + emitline("(BP), AX\n"); + emitline("\tMOVL\tAX, "); + emitdispreg(ck: i64, "BX"); + emitline("\n"); + ck += 4; + }; + if (ck + 2 <= esz) { + emitline("\tMOVW\t"); + emitoff((stscr + ck): i64); + emitline("(BP), AX\n"); + emitline("\tMOVW\tAX, "); + emitdispreg(ck: i64, "BX"); + emitline("\n"); + ck += 2; + }; + if (ck + 1 <= esz) { + emitline("\tMOVB\t"); + emitoff((stscr + ck): i64); + emitline("(BP), AX\n"); + emitline("\tMOVB\tAX, "); + emitdispreg(ck: i64, "BX"); + emitline("\n"); + ck += 1; + }; vn = vn.next; continue; }; + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); if (vn.kind == nkind.N_IDENT) { let sl: *local = localfindnode(c, vn.str); if (sl == nil) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 0df74c0e..ee3a1a23 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -27470,16 +27470,16 @@ fn cgappend(c: *cgen, n: *node) void = { vn = vn.next; continue; }; - cgappendgrow(c, sndirect, sn_off, snscr, esz); - cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); if (vn.kind == nkind.N_STRUCTLIT) { - // #59 (#50's eval-order kin): the literal's field - // exprs still eval POST-grow here — filed, not - // folded. - let scroff: i32 = localadd(c, "@appendscr", 8, nil); - emitline("\tMOVQ\tBX, "); - emitoff(scroff: i64); - emitline("(BP)\n"); + // #59 (#50's eval-order kin): resolve the struct + // info, then fill the literal into a fresh + // per-SITE scratch (must stay live across + // rt_ensure, and a nested append in a field expr + // would clobber a dedup'd slot — the @apptagscr + // rationale, #25/#31) BEFORE the grow, so the + // field exprs see the pre-grow len. Then grow, + // slot, raw-copy scratch->slot (mirror the #50 + // tagged arm above). let esi: *structinfo = structlookupchain(c, etnode); if (esi == nil && !sndirect && esubnamed != nil) { // FA1: no declared tnode to chain through — @@ -27494,10 +27494,60 @@ fn cgappend(c: *cgen, n: *node) void = { os.write(2, m34s.ptr, m34s.len: u64); os.exit(1); }; - cgstructlitfill(c, esi, vn, 1, scroff, "", 0); + let stscr: i32 = localalloc(c, "@appendstructscr", esz, nil); + cgstructlitfillbp(c, esi, vn, stscr); + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); + // Descending 8/4/2/1 ladder, not an 8B-word + // loop: a plain struct's esz rounds to maxalign + // (check.c:916), not 8, so a sub-8B / non-8B- + // multiple element packs at its own stride — an + // 8B copy of the last slot writes past the slice + // buffer (the tagged arm above is safe only + // because boxes are 8B-padded; #59). Mirrors the + // N_IDENT source arm below. + let ck: i32 = 0; + for (ck + 8 <= esz) { + emitline("\tMOVQ\t"); + emitoff((stscr + ck): i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tAX, "); + emitdispreg(ck: i64, "BX"); + emitline("\n"); + ck += 8; + }; + if (ck + 4 <= esz) { + emitline("\tMOVL\t"); + emitoff((stscr + ck): i64); + emitline("(BP), AX\n"); + emitline("\tMOVL\tAX, "); + emitdispreg(ck: i64, "BX"); + emitline("\n"); + ck += 4; + }; + if (ck + 2 <= esz) { + emitline("\tMOVW\t"); + emitoff((stscr + ck): i64); + emitline("(BP), AX\n"); + emitline("\tMOVW\tAX, "); + emitdispreg(ck: i64, "BX"); + emitline("\n"); + ck += 2; + }; + if (ck + 1 <= esz) { + emitline("\tMOVB\t"); + emitoff((stscr + ck): i64); + emitline("(BP), AX\n"); + emitline("\tMOVB\tAX, "); + emitdispreg(ck: i64, "BX"); + emitline("\n"); + ck += 1; + }; vn = vn.next; continue; }; + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); if (vn.kind == nkind.N_IDENT) { let sl: *local = localfindnode(c, vn.str); if (sl == nil) { diff --git a/test/wcc/946_append_structlit_evalorder_run.c b/test/wcc/946_append_structlit_evalorder_run.c new file mode 100644 index 00000000..2c33ab11 --- /dev/null +++ b/test/wcc/946_append_structlit_evalorder_run.c @@ -0,0 +1,252 @@ +/* + * 946_append_structlit_evalorder_run — task #59 (#50's eval-order kin): + * an append/insert of a STRUCT-LITERAL value used to fill the literal's + * fields into the destination slot AFTER cg_append_grow had already bumped + * xs.len. So a field expression that reads `len(xs)` saw the POST-grow + * length. Hare evaluates the value BEFORE the grow. + * + * #263 both-wrong-IDENTICAL: cstage and wwstage emitted byte-identical asm + * that was wrong on BOTH (exit 6 vs the correct 3) — the asm-diff and + * byte-id gates are BLIND to it, so a RUNTIME row is the only net. + * + * Fix (both stages, byte-identical per rule 10): fill the struct literal + * into a fresh per-SITE scratch BEFORE the grow (mirror #50's tagged arm), + * then grow, slot, raw-copy scratch->slot. insert() desugars in-place to + * append and re-dispatches into THIS exact arm, so it is fixed by + * construction — R2 proves the desugar twin (both stages). + * + * Rows assert RUNTIME on BOTH drivers AND cs==ww .s byte-identical: + * append_eval `append(xs, rec{f=len(xs):i64})` x3 -> sum == 3 (pre-grow); + * base bug = 6 (post-grow), main returns 1. + * insert_eval `insert(xs[1], rec{f=len(xs):i64})` -> inserted.f == 2 + * (pre-grow len at insert time); base bug = 3. + * + * All K_RUN: build+run exit 0 on BOTH drivers AND cs==ww byte-identical. + * NNN<950, self-contained (/tmp, no imports), so rule-14's selfhost-sibling + * race does not apply (903/940/945 precedent). + */ +#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; }; + +static const struct row rows[] = { + { "append_eval", + "package main;\n" + "type rec = struct { f: i64 };\n" + "export fn main() i32 = {\n" + " let xs: []rec = [];\n" + " append(xs, rec{ f = len(xs): i64 });\n" + " append(xs, rec{ f = len(xs): i64 });\n" + " append(xs, rec{ f = len(xs): i64 });\n" + " let s: i64 = xs[0].f + xs[1].f + xs[2].f;\n" + " if (s != 3) { return 1; };\n" + " return 0;\n" + "};\n", 0 }, + { "insert_eval", + "package main;\n" + "type rec = struct { f: i64 };\n" + "export fn main() i32 = {\n" + " let xs: []rec = [];\n" + " append(xs, rec{ f = 100 });\n" + " append(xs, rec{ f = 200 });\n" + " insert(xs[1], rec{ f = len(xs): i64 });\n" + " if (xs[1].f != 2) { return 1; };\n" + " if (xs[0].f != 100) { return 2; };\n" + " if (xs[2].f != 200) { return 3; };\n" + " return 0;\n" + "};\n", 0 }, + /* + * narrow_neighbor — a sub-8B struct (`{a: i32}`, esz=4, packs at + * stride 4) appended to fill the slice exactly to capacity. Catches + * BOTH halves of #59 in one row: + * pre-fix (pristine 39432f7): field exprs eval post-grow → sum + * wrong → returns 1. + * 8B-block-copy fix (the over-copy regression): eval-order is + * correct but the last slot's 8-byte copy writes 4 bytes PAST + * the 32-byte buffer into the adjacent `victim` allocation → + * victim[0] clobbered → returns 2. + * precise 8/4/2/1 ladder: both correct → 0. + * The 8-element fill makes len==cap==8 so the tail over-write lands + * past the buffer; `victim` is alloc'd right after xs's buffer (bump + * allocator) so the clobber is observable. + */ + { "narrow_neighbor", + "package main;\n" + "type rec = struct { a: i32 };\n" + "export fn main() i32 = {\n" + " let xs: []rec = [];\n" + " append(xs, rec{ a = len(xs): i32 });\n" + " let victim: []i64 = [];\n" + " append(victim, 1234605616436508552i64);\n" + " let i: i32 = 1;\n" + " for (i < 8) {\n" + " append(xs, rec{ a = len(xs): i32 });\n" + " i += 1;\n" + " };\n" + " let s: i32 = 0;\n" + " let j: i32 = 0;\n" + " for (j < 8) { s += xs[j].a; j += 1; };\n" + " if (s != 28) { return 1; };\n" + " if (victim[0] != 1234605616436508552i64) { return 2; };\n" + " return 0;\n" + "};\n", 0 }, +}; + +/* 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/ase_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/ase_%d_d_%d", getpid(), i); + snprintf(errf, sizeof errf, "/tmp/ase_%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; +} + +/* cs==ww .s byte-id (rule 10). */ +static int +byteid(const char *w6c, const char *w6c_ww, const struct row *r, int i) +{ + char src[96], cs_s[96], ws_s[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/ase_bi_%d_%d.ww", getpid(), i); + snprintf(cs_s, sizeof cs_s, "/tmp/ase_bi_%d_%d_cs.s", getpid(), i); + snprintf(ws_s, sizeof ws_s, "/tmp/ase_bi_%d_%d_ww.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 -o %s %s 2>/dev/null", w6c, cs_s, src); + if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: w6c failed\n", r->label); rc = 1; } + else { + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c_ww, ws_s, src); + if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: w6c_ww failed\n", r->label); rc = 1; } + else if (slurp_eq(cs_s, ws_s) != 0) { + fprintf(stderr, "row[%s]: cstage/wwstage .s DIFFER " + "(rule-10 byte-id)\n", r->label); + rc = 1; + } + } + unlink(src); unlink(cs_s); unlink(ws_s); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640], wdrv[640], w6c[640], w6c_ww[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + + struct { const char *name; const char *path; int gated; } + 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 && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "append_structlit_evalorder: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + total++; + if (run_driver(drivers[d].path, &rows[i], i) != 0) fail++; + } + } + + if (access(w6c_ww, X_OK) == 0) { + for (int i = 0; i < n; i++) { + total++; + if (byteid(w6c, w6c_ww, &rows[i], i) != 0) fail++; + } + } + + if (fail) { + fprintf(stderr, "append_structlit_evalorder: %d/%d checks failed\n", + fail, total); + return 1; + } + printf("append_structlit_evalorder: %d/%d ok\n", total, total); + return 0; +}