diff --git a/Makefile b/Makefile index 93c4b36e..67e73d7b 100644 --- a/Makefile +++ b/Makefile @@ -430,6 +430,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_amp_fn_assign_run \ $(BIN)/test_type_value_shadow_run \ $(BIN)/test_xmod_alias_struct_collide_run \ + $(BIN)/test_xmod_struct_argpush_collide_run \ $(BIN)/test_xmod_variant_match \ $(BIN)/test_xmod_qualstructlit_run \ $(BIN)/test_spread_variant_match \ @@ -1521,6 +1522,16 @@ $(BIN)/test_xmod_alias_struct_collide_run: test/wcc/784_xmod_alias_struct_collid $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 793 #21/#224: inferred-let struct LOCAL whose leaf collides cross-module +# miscompiled the by-value arg push + recv + direct field read (all +# name-keyed); fix routes them off the stamped tinfo. Own 2-module fixtures +# in a private mktemp dir — not a selfhost-driver test. +$(BIN)/test_xmod_struct_argpush_collide_run: test/wcc/793_xmod_struct_argpush_collide_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 $@ $< + # #13: cross-module decomposition of an imported union's variants # (`case pkg.a`). Pre-fix the wwstage checker rejected it; cstage built # it. cstage driver build + run pins routing; ww-sep build (w6c_ww must diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 08c47ce8..1bd8fcbf 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -3512,9 +3512,13 @@ fn cgdot(c: *cgen, n: *syntax.node) void = { // same-module struct (a genuine struct-value receiver); a // same-module alias keeps peeling; a foreign leaf (in // neither registry for c.curmod) falls back to the prior - // any-module heuristic. The broader cross-module same-leaf - // STRUCT name-keying at the direct-struct arm below is - // filed separately as #224 (not FLIP-triggered). + // any-module heuristic. #21/#224: the direct-struct arm + // below no longer name-keys — it resolves field offsets off + // the stamped struct tinfo (dotlhs.type_), closing the + // cross-module same-leaf STRUCT mis-read there. This peel + + // same-module break still scopes the *struct (N_TPTR) and + // array arms, which remain structlookupchain-keyed (out of + // the #21 scoped slice). for (tn != nil && tn.kind == syntax.nkind.N_TNAME) { if (structsamemod(c, tn.str) != nil) { break; }; let nx: *syntax.node = aliassamemod(c, tn.str); @@ -3567,69 +3571,77 @@ fn cgdot(c: *cgen, n: *syntax.node) void = { }; // Direct struct local: field load at off+foff. if (lkind == syntax.nkind.N_TNAME) { - // structlookupchain walks the alias chain on - // miss so a transitively-aliased struct (`type - // b = a; a = struct`) still resolves to the - // underlying fieldinfo (#22). - let si: *structinfo = structlookupchain(c, tn); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (syntax.streq(fn_, fld)) { - // tagged-union field: emit the AX=tag, - // DX=word0, CX=word1[, R8=word2] load - // sequence so the match / let-init / - // call-arg consumers see the same shape - // as a tagged-returning fn. Pre-#28 fell - // through to the scalar fieldloadop and - // only AX (tag) was loaded — payload - // words came from whatever the caller - // left in DX/CX/R8. - if (istaggedtype(c, fi.tnode)) { - let tsz: i32 = slotsize(c, fi.tnode); + // #21/#224: resolve the field OFFSET + field type off + // the checker-STAMPED struct tinfo + // (tichase(dotlhs.type_).fields), NOT the name-keyed + // structlookupchain(tn). On a cross-module same-leaf + // collision lc.tnode is a bare leaf that structlookup + // mis-resolves to a FOREIGN same-leaf struct → fields + // read at the WRONG offsets / wrong load-op (the #224 + // direct-struct arm flagged at the peel-loop comment + // above). The stamped tinfo carries the right layout + // regardless of leaf collision; mirrors cstage's + // `t->fields` walk (cgen.c N_DOT, type-keyed) — align + // ww UP. tfield {name, type_, offset} is the tinfo twin + // of fieldinfo {fname, tnode, foff}; the dispatch keys + // off syntax.typeis* on the field tinfo, byte-id with + // the prior is*type(fi.tnode)=typeis*(fi.tnode.type_). + let sbu: *syntax.tinfo = nil; + if (dotlhs != nil) { sbu = tichase(dotlhs.type_: *syntax.tinfo); }; + if (sbu != nil) { if (sbu.kind == syntax.tykind.TY_STRUCT) { + let tf: *syntax.tfield = sbu.fields; + for (tf != nil) { + if (syntax.streq(tf.name, fld)) { + let foff: i32 = tf.offset: i32; + let ftraw: *syntax.tinfo = tf.type_; + // tagged-union field: AX=tag, DX=word0, + // CX=word1[, R8=word2]; slot = ti.size + // (slotsize's TAGGED arm, cgenutil.ww:2680). + if (syntax.typeistagged(ftraw)) { + let ftc: *syntax.tinfo = tichase(ftraw); + let tsz: i32 = 0; + if (ftc != nil) { tsz = ftc.size: i32; }; cgloadtaggedfield(c, "BP", - lc.off + fi.foff, tsz, true); + lc.off + foff, tsz, true); return; }; - // str IS []u8 — same 3-word {ptr,len,cap} - // as a slice field: load (ptr, len, cap) - // into (AX, BX, CX). Base is BP so no - // aliasing — order doesn't matter. str - // folds onto the slice arm (#1/Phase 3 - // collapse; cite cstage cgen.c N_DOT S1). - if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { + // str IS []u8 — 3-word {ptr,len,cap} into + // (AX,BX,CX). Base is BP so order is harmless. + if (syntax.typeisstr(ftraw) || syntax.typeisslice(ftraw)) { emitline("\tMOVQ\t"); - emitoff((lc.off + fi.foff): i64); + emitoff((lc.off + foff): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\t"); - emitoff((lc.off + fi.foff + 8): i64); + emitoff((lc.off + foff + 8): i64); emitline("(BP), BX\n"); emitline("\tMOVQ\t"); - emitoff((lc.off + fi.foff + 16): i64); + emitoff((lc.off + foff + 16): i64); emitline("(BP), CX\n"); - } else { if (isfloattype(c, fi.tnode)) { + } else { if (syntax.typeisfloat(ftraw)) { // f64/f32 field: route through X0. let mov: str = "MOVSD"; - if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; + if (syntax.typeisf32(ftraw)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); - emitoff((lc.off + fi.foff): i64); + emitoff((lc.off + foff): i64); emitline("(BP), X0\n"); } else { - let op: str = fieldloadop(c, fi); + let ftc: *syntax.tinfo = tichase(ftraw); + let fsz: i32 = 0; + if (ftc != nil) { fsz = ftc.size: i32; }; + let op: str = loadopsz(syntax.typeissigned(ftraw), fsz); emitline("\t"); emitline(op); emitline("\t"); - emitoff((lc.off + fi.foff): i64); + emitoff((lc.off + foff): i64); emitline("(BP), AX\n"); }; }; return; }; - fi = fi.finext; + tf = tf.tnext; }; - }; + }; }; }; // Array pseudo-fields: `.ptr` is the array's // address (LEAQ); `.len` is the static element diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index d293eb7a..18f9da5c 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -2583,29 +2583,38 @@ fn cgletbody(c: *cgen, n: *syntax.node, off: i32) void = { }; }; if (rhs.kind == syntax.nkind.N_CALL) { - let sname: str; - sname.ptr = nil; sname.len = 0; if (tn != nil) { if (tn.kind == syntax.nkind.N_TNAME) { - sname = tn.str; - }; - }; - if (sname.len > 0) { - let lsi: *structinfo = structlookup(c, sname); - if (lsi != nil) { // ≤24B register RECV: the value arrives packed // in AX/DX/CX, so size by the maxalign-rounded // ABI size (cstage lu->size), not the natural // extent — see structabisize (#169). - let lsz: i32 = structabisize(lsi); - let tlm: i32 = lsz - (lsz / 8) * 8; - if (lsz <= 24) { - if (tlm == 0 || tlm == 1 - || tlm == 2 || tlm == 4) { - cgexpr(c, rhs); - cgaggregstore(c, "BP", off, lsz, true); - c.lastwasreturn = 0; - return; + // + // #21/#224: size off the checker-STAMPED tinfo + // (structabisizetn = tichase(tn.type_).size, the + // maxalign-rounded ABI size = check.ww:2467), + // NOT structlookup(tn.str). On a cross-module + // same-leaf collision the inferred-let's tn.str + // is a bare leaf that structlookup mis-resolves to + // a FOREIGN same-leaf struct → the recv copied that + // struct's word count (a 24B foreign over-copies a + // 16B local, spilling into a neighbour slot). The + // stamped tinfo carries the right size regardless of + // collision; byte-id with structabisize on a + // resolving lookup. Mirrors the sibling array arm + // below (already tn.type_-keyed). cstage is + // type-keyed (lu->size) — align ww UP. + let lsz: i32 = structabisizetn(tn.type_: *syntax.tinfo); + if (lsz > 0) { + let tlm: i32 = lsz - (lsz / 8) * 8; + if (lsz <= 24) { + if (tlm == 0 || tlm == 1 + || tlm == 2 || tlm == 4) { + cgexpr(c, rhs); + cgaggregstore(c, "BP", off, lsz, true); + c.lastwasreturn = 0; + return; + }; }; }; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index cd3eacde..02b8a1e3 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -844,8 +844,18 @@ fn pushargsrev(c: *cgen, arg: *syntax.node, param: *syntax.node, memphase: bool, // `cgexpr(c, arg)` + scalar PUSHQ AX — only the first // 8B word made it across, and the callee's second-arg // slots picked up the wrong neighbour's value. - let stsz: i32 = structparamsize(c, lc.tnode); - if (stsz > 0) { + // #21/#224: COUNT the push off the checker-STAMPED tinfo + // (structabisizetn), not the name-keyed structparamsize(lc.tnode). + // On a cross-module same-leaf collision the inferred-let's tnode + // is a bare leaf that structlookup mis-resolves (returns 0 or a + // foreign struct >16B → 0), so the fast path was skipped and the + // arg dropped to the scalar single-PUSHQ default — word1 lost. + // cstage counts via struct_arg_size(args[i]->type) (TYPE-keyed), + // never colliding; align ww UP. The <=16B gate keeps >16B structs + // + arrays on the #271 aggregate arm below (the fast path emits + // at most 2 words). + let stsz: i32 = structabisizetn(arg.type_: *syntax.tinfo); + if (stsz > 0 && stsz <= 16) { if (stsz > 8) { emitline("\tMOVQ\t"); emitoff((off + 8): i64); @@ -951,14 +961,15 @@ fn pushargsrev(c: *cgen, arg: *syntax.node, param: *syntax.node, memphase: bool, // aggregate) and stack-imbalanced against the type-based drain. let aggsz: i32 = aggargsizetn(arg.type_: *syntax.tinfo); if (aggsz > 0) { - // Exclude a ≤16B-struct IDENT — it owns the structparamsize - // fast path above (or, when a cross-module same-leaf collision - // makes the name-keyed structparamsize miss it, the scalar - // default below, byte-id with cstage's 1-word struct push; - // #784/#223). The exclusion is TYPE-keyed via the stamped - // tinfo, mirroring cstage node_isstructarg (struct_arg_size on - // args[i]->type) — a name-keyed gate here re-opens the #211/#13 - // name-keyed divergence the cstage type gate doesn't have. + // Exclude a ≤16B-struct IDENT — it owns the structabisizetn + // fast path above. That path is now TYPE-keyed (#21/#224): a + // cross-module same-leaf collision no longer misses (the prior + // name-keyed structparamsize returned 0 → the arg dropped to the + // scalar default → word1 lost; #784/#223). The exclusion is + // TYPE-keyed via the stamped tinfo, mirroring cstage + // node_isstructarg (struct_arg_size on args[i]->type) — a + // name-keyed gate here re-opens the #211/#13 name-keyed + // divergence the cstage type gate doesn't have. let structident: bool = false; if (arg.kind == syntax.nkind.N_IDENT) { let st: *syntax.tinfo = arg.type_: *syntax.tinfo; @@ -2046,6 +2057,28 @@ fn structabisize(si: *structinfo) i32 = { return (n + maxaln - 1) & ~(maxaln - 1); }; +// structabisizetn — the maxalign-rounded ABI size of a by-value STRUCT, +// read off the checker-STAMPED tinfo (.size), else 0. The tinfo twin of +// structabisize(*structinfo): check.ww:2467 computes the struct's +// `r.size = (off+maxalign-1)&~(maxalign-1)` — identical to structabisize's +// formula — so `tichase(t).size` IS the ABI size, byte-id with the +// name-keyed structabisize on a resolving lookup. The #21/#224 choke-point +// for the caller-side register-ABI sites (struct-arg push, inferred-let +// struct call-result recv) that previously keyed the COUNT through a +// name-keyed structlookup/structparamsize: a cross-module same-leaf +// collision makes that lookup return 0 (or a foreign struct's size), so the +// push under-counted and the recv over-copied. The stamped tinfo carries +// the right size regardless of leaf collision (the documented #211/#13/#784 +// name-keyed cluster; align wwstage UP to cstage's type-keyed struct_arg_size +// / lu->size). STRUCT-only — arrays own their own #271/#267 arms. +fn structabisizetn(t: *syntax.tinfo) i32 = { + if (t == nil) { return 0; }; + let u: *syntax.tinfo = tichase(t); + if (u == nil) { return 0; }; + if (u.kind == syntax.tykind.TY_STRUCT) { return u.size: i32; }; + return 0; +}; + // sretretsize — if `t` ultimately denotes a plain TY_STRUCT > 24B, // return its natural size; else 0. Tagged unions, tuples, str, // slices, scalars route through their existing register-return ABIs diff --git a/test/wcc/793_xmod_struct_argpush_collide_run.c b/test/wcc/793_xmod_struct_argpush_collide_run.c new file mode 100644 index 00000000..938f8c47 --- /dev/null +++ b/test/wcc/793_xmod_struct_argpush_collide_run.c @@ -0,0 +1,301 @@ +/* + * 793_xmod_struct_argpush_collide_run — project #21/#224 runtime + byte-id + * net. An inferred-let struct LOCAL whose type leaf collides with a + * DIFFERENT module's same-leaf struct miscompiled in wwstage at THREE + * caller-side register-ABI sites, all keyed off the name-resolved + * (collision-prone) struct instead of the checker-STAMPED tinfo: + * + * - push (cgenutil.ww pushargsrev): the by-value struct call-arg + * COUNT came from name-keyed structparamsize(lc.tnode). On the + * collision lc.tnode is a bare leaf structlookup mis-resolves to the + * foreign same-leaf struct (>16B → 0), so the fast path was skipped + * and the arg dropped to a scalar single-PUSHQ — the 2nd eightbyte + * (the sub-8 tail) was lost. [the original #21 site] + * - recv (cgenstmt.ww cgletbody): `let s = m1.mk()` sized the + * register receive via structlookup(tn.str) → the foreign struct's + * word count, OVER-copying a 24B foreign over a 16B local (spilling + * CX into a neighbour slot). + * - field (cgenexpr.ww cgdot direct-struct arm): `s.` resolved + * the offset via structlookupchain(tn) → the foreign struct's layout, + * reading the field at the WRONG offset + wrong load-op. [#224, filed + * not-fixed by 784/#223] + * + * cstage is correct at all three — it keys on args[i]->type / lu->size / + * t->fields (the checker-STAMPED type), never a name re-lookup. Fix + * (wwstage-only align-UP, #21/#224, ONE commit): route all three off the + * stamped tinfo — structabisizetn(arg/tn.type_) for the push+recv COUNT, + * tichase(dotlhs.type_).fields (tfield) for the field offset+type. + * + * GATE-BLIND in the bootstrap (same class as 784/#223): the trigger needs + * a SECOND module contributing a same-leaf struct of a DIFFERENT size, so + * the name-keyed lookup mis-resolves. A single-module synthetic resolves + * correctly (structlookup's same-module pass) and stays byte-id — it does + * NOT redden. Hence this 2-module probe. + * + * row | m1.pair (local) | m2.pair (foreign) | site(s) | exit + * -----------------+----------------------+-------------------+---------------+----- + * recvpush_t16 | {u64,u16} 16B | 24B | recv + push | 107 + * fieldread_t16 | {u64,u16} 16B | 24B | recv + field | 107 + * combined_t16 | {u64,u16} 16B | 24B | recv+field+push| 114 + * recvpush_t12 | {u32,u32,u32} 12B | 24B | recv + push | 6 + * + * cstage `ww build --sep` + run pins runtime; the wwstage binary is run + * too (the bug WAS a wrong wwstage runtime value); raw cs.s vs ww.s cmp + * over the driver-produced per-package asm pins rule-10 byte-id (the + * discriminating net — pre-fix __root.s diverges at the push/recv/field). + */ +#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); + int cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + return rc; +} + +struct file { const char *name; const char *src; }; + +struct scenario { + const char *label; + const struct file *files; /* name==NULL terminates */ + int want_exit; +}; + +/* m2.pair is a 24B struct sharing the leaf `pair` with m1.pair — the + * name-keyed lookup's any-module fallback mis-resolves m1.pair to it. */ +static const char m2_24[] = + "package m2;\n" + "export type pair = struct { hi: u64, mid: u64, lo: u64 };\n" + "export fn use2(p: pair) i32 = { return (p.hi + p.mid + p.lo): i32; };\n"; + +/* ---- recvpush_t16: inferred-let recv + by-value push (the #21 site) -- */ +static const struct file recvpush_t16_files[] = { + { "m1.ww", + "package m1;\n" + "export type pair = struct { hi: u64, lo: u16 };\n" + "export fn mk() pair = { return pair { hi = 100: u64, lo = 7: u16 }; };\n" + "export fn consume(p: pair) i32 = { return (p.hi + (p.lo: u64)): i32; };\n" }, + { "m2.ww", m2_24 }, + { "main.ww", + "package main;\n" + "import m1;\n" + "import m2;\n" + "fn main() i32 = {\n" + " let dummy: m2.pair;\n" + " dummy.hi = 0: u64;\n" + " let s = m1.mk();\n" + " return m1.consume(s);\n" + "};\n" }, + { NULL, NULL } +}; + +/* ---- fieldread_t16: inferred-let recv + direct field read (#224) ----- */ +static const struct file fieldread_t16_files[] = { + { "m1.ww", + "package m1;\n" + "export type pair = struct { hi: u64, lo: u16 };\n" + "export fn mk() pair = { return pair { hi = 100: u64, lo = 7: u16 }; };\n" }, + { "m2.ww", m2_24 }, + { "main.ww", + "package main;\n" + "import m1;\n" + "import m2;\n" + "fn main() i32 = {\n" + " let dummy: m2.pair;\n" + " dummy.hi = 0: u64;\n" + " let s = m1.mk();\n" + " return (s.hi + (s.lo: u64)): i32;\n" + "};\n" }, + { NULL, NULL } +}; + +/* ---- combined_t16: recv + field + push in one fn -------------------- */ +static const struct file combined_t16_files[] = { + { "m1.ww", + "package m1;\n" + "export type pair = struct { hi: u64, lo: u16 };\n" + "export fn mk() pair = { return pair { hi = 100: u64, lo = 7: u16 }; };\n" + "export fn consume(p: pair) i32 = { return (p.hi + (p.lo: u64)): i32; };\n" }, + { "m2.ww", m2_24 }, + { "main.ww", + "package main;\n" + "import m1;\n" + "import m2;\n" + "fn main() i32 = {\n" + " let dummy: m2.pair;\n" + " dummy.hi = 0: u64;\n" + " let s = m1.mk();\n" + " let x: i32 = (s.lo: i32);\n" + " return m1.consume(s) + x;\n" + "};\n" }, + { NULL, NULL } +}; + +/* ---- recvpush_t12: a maxalign-4 12B sub-8-tail tail shape ----------- */ +static const struct file recvpush_t12_files[] = { + { "m1.ww", + "package m1;\n" + "export type pair = struct { a: u32, b: u32, c: u32 };\n" + "export fn mk() pair = { return pair { a = 1: u32, b = 2: u32, c = 3: u32 }; };\n" + "export fn consume(p: pair) i32 = { return (p.a + p.b + p.c): i32; };\n" }, + { "m2.ww", m2_24 }, + { "main.ww", + "package main;\n" + "import m1;\n" + "import m2;\n" + "fn main() i32 = {\n" + " let dummy: m2.pair;\n" + " dummy.hi = 0: u64;\n" + " let s = m1.mk();\n" + " return m1.consume(s);\n" + "};\n" }, + { NULL, NULL } +}; + +static const struct scenario scenarios[] = { + { "recvpush_t16", recvpush_t16_files, 107 }, + { "fieldread_t16", fieldread_t16_files, 107 }, + { "combined_t16", combined_t16_files, 114 }, + { "recvpush_t12", recvpush_t12_files, 6 }, +}; + +static int +run_scenario(const char *cdrv, const char *wdrv, const struct scenario *sc) +{ + /* Private fixture dir — the driver's srcd-first import search can't + * pick up a polluting same-name file (mirrors 784/#215). */ + char dir[] = "/tmp/ww793_XXXXXX"; + if (mkdtemp(dir) == NULL) { + fprintf(stderr, "793[%s]: mkdtemp failed\n", sc->label); + return -1; + } + + char path[1024], cmd[4096]; + int rc = 0; + + for (int i = 0; sc->files[i].name; i++) { + snprintf(path, sizeof path, "%s/%s", dir, sc->files[i].name); + FILE *f = fopen(path, "wb"); + if (!f) { fprintf(stderr, "793[%s]: write %s\n", sc->label, + sc->files[i].name); rc = -1; goto done; } + fputs(sc->files[i].src, f); + fclose(f); + } + + /* cstage --sep build + run pins runtime; pin WW_PKGCACHE under the + * scratch dir so out/.pkgcache is untouched. */ + snprintf(cmd, sizeof cmd, + "cd %s && WW_PKGCACHE=%s/pkgc_c %s build --sep -I %s -o %s/main %s/main.ww", + dir, dir, cdrv, dir, dir, dir); + if (runwait(cmd) != 0) { + fprintf(stderr, "793[%s]: cstage build failed\n", sc->label); + rc = -1; goto done; + } + snprintf(path, sizeof path, "%s/main", dir); + int gotc = runwait(path); + if (gotc != sc->want_exit) { + fprintf(stderr, "793[%s]: cstage exit %d, want %d\n", + sc->label, gotc, sc->want_exit); + rc = -1; + } + + /* wwstage --sep build + run: the bug WAS a wrong wwstage runtime + * value (a dropped/over-copied field), so run the wwstage binary too. */ + snprintf(cmd, sizeof cmd, + "cd %s && WW_PKGCACHE=%s/pkgc_w %s build --sep -I %s -o %s/mainww %s/main.ww", + dir, dir, wdrv, dir, dir, dir); + if (runwait(cmd) != 0) { + fprintf(stderr, "793[%s]: ww_ww build failed\n", sc->label); + rc = -1; goto done; + } + snprintf(path, sizeof path, "%s/mainww", dir); + int gotw = runwait(path); + if (gotw != sc->want_exit) { + fprintf(stderr, "793[%s]: wwstage exit %d, want %d\n", + sc->label, gotw, sc->want_exit); + rc = -1; + } + + /* Byte-id net (the discriminator): per-package asm under + * .sepwork/.s; concat (sorted glob, identical set both + * stages) for the compare. Pre-fix __root.s diverges. */ + char cs_s[1024], ws_s[1024]; + snprintf(cs_s, sizeof cs_s, "%s/all_cs.s", dir); + snprintf(ws_s, sizeof ws_s, "%s/all_ww.s", dir); + snprintf(cmd, sizeof cmd, "cat %s/main.sepwork/*.s > %s 2>/dev/null", + dir, cs_s); if (system(cmd)) {} + snprintf(cmd, sizeof cmd, "cat %s/mainww.sepwork/*.s > %s 2>/dev/null", + dir, ws_s); if (system(cmd)) {} + if (slurp_eq(cs_s, ws_s) != 0) { + fprintf(stderr, "793[%s]: cs.s/ww.s DIFFER (rule-10 byte-id " + "violation — #21/#224 regression)\n", sc->label); + rc = -1; + } + +done: + snprintf(cmd, sizeof cmd, "rm -rf %s", dir); + (void)runwait(cmd); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[2048]; + 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[2100], wdrv[2100]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + if (access(wdrv, X_OK) != 0) { + fprintf(stderr, "793: ww_ww missing — cannot run the cs==ww " + "byte-id gate (the whole point of this test)\n"); + return 1; + } + + int n = (int)(sizeof scenarios / sizeof scenarios[0]); + int fail = 0; + for (int i = 0; i < n; i++) { + if (run_scenario(cdrv, wdrv, &scenarios[i]) != 0) + fail++; + } + + if (fail) { + fprintf(stderr, "793 xmod_struct_argpush_collide: %d/%d " + "scenarios failed\n", fail, n); + return 1; + } + printf("xmod_struct_argpush_collide: %d/%d ok (cstage+wwstage run + " + "cs==ww byte-id)\n", n, n); + return 0; +}