From 45339d2f5b78b23e4929943c58efcb25ead81311 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 18 May 2026 13:32:56 +0900 Subject: [PATCH] selfhost+cstage+test: graduate enumlookup same-module-first + N_DOT enumlookupmod (#4a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Class A silent miscompile, latent until two modules export the same enum leaf name. Wwstage's enumlookup (selfhost/cmd/wcc/cgen.ww) walked c.enums head-first by ename; cgdot handed it the bare leaf from N_DOT.lhs.str for both `Color.MEMBER` (lhs N_IDENT) and `pkg.Color.MEMBER` (lhs N_DOT) shapes, silently dropping the explicit qualifier on the second. Cstage's enum-member fold (cmd/wcc/check.c cexpr N_DOT) was carrying the same head-pick on the lhs-ident lookup — pre-fix the mismatch surfaced as a "not assignable to " checker error rather than a silent wrong-constant because resolve_typename for the fn return spec already used scope_lookup_prefer correctly, so the rhs's wrong- module-Color clashed with the return type's right-module-Color. No in-tree corpus currently declares two same-leaf enums, so 995_self_rebuild stayed green and the latent miscompile only surfaces once a stdlib port introduces the collision (same shape as #27 surfacing when lib/strings dragged utf8's invalid alias into the chain alongside strconv's invalid). Fourth leaf of the trio leaf-name lookup graduation (after #27 aliaslookup, #28 fnparamslookupmod, #31 fnretlookupmod): wwstage enumlookup grows a same-module-first walk before the head-walk fallback, mirroring aliaslookup's two-pass shape (cgen.ww:75). The N_DOT consumer surface — `pkg.Enum.MEMBER`, already used in-corpus by os.flag.RDONLY, temp.mode.RDWR, os.whence.SET etc. — routes through a new enumlookupmod variant with the explicit N_DOT.lhs.lhs.str as the mod qualifier (mirror of fnret/ fnparamslookupmod). Cstage's check.c cexpr N_DOT lhs lookup graduates from scope_lookup to scope_lookup_prefer to align symmetrically (rule 10: both stages pick same-module-first on the bare-leaf shape). 733_enum_modshadow pins both surfaces with 3 rows: row 1 bare-leaf in module M must fold against M's own Color even with another module's same-leaf Color at the head of c.enums; row 2 same-module `mod.Color.MEMBER` from inside that mod pins the API surface; row 3 cross-module `othermod.Color.MEMBER` from a third module with no local Color sentinel-flips the cgdot etmod tracking + enumlookupmod path independently of row 1's same-module-first fallback. Asserts the matching \$N, immediate inside the right TEXT sym + bad_imm NOT-presence anti-check on both stages plus byte-id between stages per row. --- Makefile | 5 + cmd/wcc/check.c | 9 +- selfhost/cmd/w6c/main.combined.ww | 50 ++++-- selfhost/cmd/wcc/cgen.ww | 39 ++++- selfhost/cmd/wcc/cgenexpr.ww | 11 +- selfhost/cmd/wwdump/main.combined.ww | 50 ++++-- test/wcc/733_enum_modshadow.c | 236 +++++++++++++++++++++++++++ 7 files changed, 368 insertions(+), 32 deletions(-) create mode 100644 test/wcc/733_enum_modshadow.c diff --git a/Makefile b/Makefile index c23bf0b0..359c0a68 100644 --- a/Makefile +++ b/Makefile @@ -262,6 +262,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_modcall_widen_slice \ $(BIN)/test_match_4arm_cross_module \ $(BIN)/test_match_4arm_cross_module_run \ + $(BIN)/test_enum_modshadow \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -608,6 +609,10 @@ $(BIN)/test_match_4arm_cross_module: test/wcc/728_match_4arm_cross_module.c \ $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_enum_modshadow: test/wcc/733_enum_modshadow.c \ + $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_match_4arm_cross_module_run: test/wcc/929_match_4arm_cross_module_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/wcc/check.c b/cmd/wcc/check.c index 030c1601..90d10d6d 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -737,9 +737,14 @@ cexpr(Checker *c, Node *n) /* module-qualified: lhs is an N_IDENT bound as SK_USE. * Resolve to the symbol with the same leaf name. With * driver-side concatenation, all symbols live in flat - * scope, so we lookup `n->str` directly. */ + * scope, so we lookup `n->str` directly. Same-module- + * first via _prefer keeps a bare-leaf enum `Color.M` + * inside module M from collapsing onto another module's + * Color sitting at the head of the flat scope chain — + * symmetric with wwstage's enumlookup graduation. */ if (n->lhs && n->lhs->kind == N_IDENT) { - Sym *ms = scope_lookup(c->cur, n->lhs->str); + Sym *ms = scope_lookup_prefer(c->cur, c->cur_mod, + n->lhs->str); if (ms && (ms->kind == SK_USE || ms->use_alias)) { /* Module-qualified ref. `use_alias` covers * the self-import case where the module's diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 3f9e5b68..e96fcaab 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -11843,11 +11843,15 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; // Enum member access: `EnumName.MEMBER` or `pkg.EnumName.MEMBER` - // → inline the pre-computed constant. With driver-side - // concatenation, both forms key off the leaf type name. + // → inline the pre-computed constant. `pkg.Enum.MEMBER` keeps + // `pkg` so enumlookupmod can prefer the explicit module on a + // leaf collision; bare `Enum.MEMBER` falls back to c.curmod via + // enumlookup's same-module-first walk. if (lhs != nil) { let etname: str; + let etmod: str; etname.ptr = nil; etname.len = 0; + etmod.ptr = nil; etmod.len = 0; if (lhs.kind == nkind.N_IDENT) { etname = lhs.str; }; @@ -11855,11 +11859,12 @@ fn cgdot(c: *cgen, n: *node) void = { if (lhs.lhs != nil) { if (lhs.lhs.kind == nkind.N_IDENT) { etname = lhs.str; + etmod = lhs.lhs.str; }; }; }; if (etname.len > 0) { - let en: *enumtype = enumlookup(c, etname); + let en: *enumtype = enumlookupmod(c, etname, etmod); if (en != nil) { let v: u64; if (enummemberval(en, fld, &v)) { @@ -18972,18 +18977,26 @@ fn collectenums(c: *cgen, file: *node) void = { }; fn enumlookup(c: *cgen, name: str) *enumtype = { - // Exact match first: bare-from-source idents and already-leafed - // names hit here directly. + // Same-module first, then any. Trio-leaf graduation mirroring + // aliaslookup (#27) and fnret/fnparamslookupmod (#28/#31): without + // the prefer pass a bare-leaf enum ident in module M can collapse + // onto another module's same-leaf enum prepended earlier in + // c.enums, silently folding `Foo.MEMBER` to the wrong constant. let e: *enumtype = c.enums; + for (e != nil) { + if (streq(e.ename, name)) { + if (streq(e.emod, c.curmod)) { return e; }; + }; + e = e.etnext; + }; + e = c.enums; for (e != nil) { if (streq(e.ename, name)) { return e; }; e = e.etnext; }; - // Module-qualified form: `pkg.enum` → match the leaf scoped to - // its originating module. Mirrors aliaslookup's mod-filter; the - // `emod == pkg` guard is what prevents two modules with same- - // leaf-name enums from collapsing into whichever entry appears - // first in the chain. + // Module-qualified form embedded in name (`pkg.enum`): scope the + // leaf to its originating module. The `emod == pkg` guard prevents + // same-leaf enums in two modules from collapsing. let i: i32 = name.len - 1; for (i >= 0) { if (name[i] == 46u8) { // '.' @@ -19009,6 +19022,23 @@ fn enumlookup(c: *cgen, name: str) *enumtype = { return nil; }; +// enumlookupmod — same-module-first leaf walk for `pkg.Enum.MEMBER` +// where the qualifier is an explicit N_IDENT module name. Mirrors +// fnparamslookupmod / fnretlookupmod (#28 / #31). Falls back to the +// bare enumlookup so a missing or empty mod still finds the leaf. +fn enumlookupmod(c: *cgen, name: str, mod: str) *enumtype = { + if (mod.len > 0) { + let e: *enumtype = c.enums; + for (e != nil) { + if (streq(e.ename, name)) { + if (streq(e.emod, mod)) { return e; }; + }; + e = e.etnext; + }; + }; + return enumlookup(c, name); +}; + fn enummemberval(en: *enumtype, mname: str, out: *u64) bool = { let m: *enummember = en.members; for (m != nil) { diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 06054e81..c40b6c58 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -256,18 +256,26 @@ fn collectenums(c: *cgen, file: *node) void = { }; fn enumlookup(c: *cgen, name: str) *enumtype = { - // Exact match first: bare-from-source idents and already-leafed - // names hit here directly. + // Same-module first, then any. Trio-leaf graduation mirroring + // aliaslookup (#27) and fnret/fnparamslookupmod (#28/#31): without + // the prefer pass a bare-leaf enum ident in module M can collapse + // onto another module's same-leaf enum prepended earlier in + // c.enums, silently folding `Foo.MEMBER` to the wrong constant. let e: *enumtype = c.enums; + for (e != nil) { + if (streq(e.ename, name)) { + if (streq(e.emod, c.curmod)) { return e; }; + }; + e = e.etnext; + }; + e = c.enums; for (e != nil) { if (streq(e.ename, name)) { return e; }; e = e.etnext; }; - // Module-qualified form: `pkg.enum` → match the leaf scoped to - // its originating module. Mirrors aliaslookup's mod-filter; the - // `emod == pkg` guard is what prevents two modules with same- - // leaf-name enums from collapsing into whichever entry appears - // first in the chain. + // Module-qualified form embedded in name (`pkg.enum`): scope the + // leaf to its originating module. The `emod == pkg` guard prevents + // same-leaf enums in two modules from collapsing. let i: i32 = name.len - 1; for (i >= 0) { if (name[i] == 46u8) { // '.' @@ -293,6 +301,23 @@ fn enumlookup(c: *cgen, name: str) *enumtype = { return nil; }; +// enumlookupmod — same-module-first leaf walk for `pkg.Enum.MEMBER` +// where the qualifier is an explicit N_IDENT module name. Mirrors +// fnparamslookupmod / fnretlookupmod (#28 / #31). Falls back to the +// bare enumlookup so a missing or empty mod still finds the leaf. +fn enumlookupmod(c: *cgen, name: str, mod: str) *enumtype = { + if (mod.len > 0) { + let e: *enumtype = c.enums; + for (e != nil) { + if (streq(e.ename, name)) { + if (streq(e.emod, mod)) { return e; }; + }; + e = e.etnext; + }; + }; + return enumlookup(c, name); +}; + fn enummemberval(en: *enumtype, mname: str, out: *u64) bool = { let m: *enummember = en.members; for (m != nil) { diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 5731472f..2b72cf24 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1177,11 +1177,15 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; // Enum member access: `EnumName.MEMBER` or `pkg.EnumName.MEMBER` - // → inline the pre-computed constant. With driver-side - // concatenation, both forms key off the leaf type name. + // → inline the pre-computed constant. `pkg.Enum.MEMBER` keeps + // `pkg` so enumlookupmod can prefer the explicit module on a + // leaf collision; bare `Enum.MEMBER` falls back to c.curmod via + // enumlookup's same-module-first walk. if (lhs != nil) { let etname: str; + let etmod: str; etname.ptr = nil; etname.len = 0; + etmod.ptr = nil; etmod.len = 0; if (lhs.kind == nkind.N_IDENT) { etname = lhs.str; }; @@ -1189,11 +1193,12 @@ fn cgdot(c: *cgen, n: *node) void = { if (lhs.lhs != nil) { if (lhs.lhs.kind == nkind.N_IDENT) { etname = lhs.str; + etmod = lhs.lhs.str; }; }; }; if (etname.len > 0) { - let en: *enumtype = enumlookup(c, etname); + let en: *enumtype = enumlookupmod(c, etname, etmod); if (en != nil) { let v: u64; if (enummemberval(en, fld, &v)) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index d7d599de..f40ba114 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -11843,11 +11843,15 @@ fn cgdot(c: *cgen, n: *node) void = { }; }; // Enum member access: `EnumName.MEMBER` or `pkg.EnumName.MEMBER` - // → inline the pre-computed constant. With driver-side - // concatenation, both forms key off the leaf type name. + // → inline the pre-computed constant. `pkg.Enum.MEMBER` keeps + // `pkg` so enumlookupmod can prefer the explicit module on a + // leaf collision; bare `Enum.MEMBER` falls back to c.curmod via + // enumlookup's same-module-first walk. if (lhs != nil) { let etname: str; + let etmod: str; etname.ptr = nil; etname.len = 0; + etmod.ptr = nil; etmod.len = 0; if (lhs.kind == nkind.N_IDENT) { etname = lhs.str; }; @@ -11855,11 +11859,12 @@ fn cgdot(c: *cgen, n: *node) void = { if (lhs.lhs != nil) { if (lhs.lhs.kind == nkind.N_IDENT) { etname = lhs.str; + etmod = lhs.lhs.str; }; }; }; if (etname.len > 0) { - let en: *enumtype = enumlookup(c, etname); + let en: *enumtype = enumlookupmod(c, etname, etmod); if (en != nil) { let v: u64; if (enummemberval(en, fld, &v)) { @@ -18972,18 +18977,26 @@ fn collectenums(c: *cgen, file: *node) void = { }; fn enumlookup(c: *cgen, name: str) *enumtype = { - // Exact match first: bare-from-source idents and already-leafed - // names hit here directly. + // Same-module first, then any. Trio-leaf graduation mirroring + // aliaslookup (#27) and fnret/fnparamslookupmod (#28/#31): without + // the prefer pass a bare-leaf enum ident in module M can collapse + // onto another module's same-leaf enum prepended earlier in + // c.enums, silently folding `Foo.MEMBER` to the wrong constant. let e: *enumtype = c.enums; + for (e != nil) { + if (streq(e.ename, name)) { + if (streq(e.emod, c.curmod)) { return e; }; + }; + e = e.etnext; + }; + e = c.enums; for (e != nil) { if (streq(e.ename, name)) { return e; }; e = e.etnext; }; - // Module-qualified form: `pkg.enum` → match the leaf scoped to - // its originating module. Mirrors aliaslookup's mod-filter; the - // `emod == pkg` guard is what prevents two modules with same- - // leaf-name enums from collapsing into whichever entry appears - // first in the chain. + // Module-qualified form embedded in name (`pkg.enum`): scope the + // leaf to its originating module. The `emod == pkg` guard prevents + // same-leaf enums in two modules from collapsing. let i: i32 = name.len - 1; for (i >= 0) { if (name[i] == 46u8) { // '.' @@ -19009,6 +19022,23 @@ fn enumlookup(c: *cgen, name: str) *enumtype = { return nil; }; +// enumlookupmod — same-module-first leaf walk for `pkg.Enum.MEMBER` +// where the qualifier is an explicit N_IDENT module name. Mirrors +// fnparamslookupmod / fnretlookupmod (#28 / #31). Falls back to the +// bare enumlookup so a missing or empty mod still finds the leaf. +fn enumlookupmod(c: *cgen, name: str, mod: str) *enumtype = { + if (mod.len > 0) { + let e: *enumtype = c.enums; + for (e != nil) { + if (streq(e.ename, name)) { + if (streq(e.emod, mod)) { return e; }; + }; + e = e.etnext; + }; + }; + return enumlookup(c, name); +}; + fn enummemberval(en: *enumtype, mname: str, out: *u64) bool = { let m: *enummember = en.members; for (m != nil) { diff --git a/test/wcc/733_enum_modshadow.c b/test/wcc/733_enum_modshadow.c new file mode 100644 index 00000000..d504a356 --- /dev/null +++ b/test/wcc/733_enum_modshadow.c @@ -0,0 +1,236 @@ +/* + * 733_enum_modshadow — sentinel for the trio leaf-name pattern's + * 4th leaf: wwstage's enumlookup. Pins (a) bare-leaf `Color.MEMBER` + * to a same-module-first walk so the constant folds against the + * caller's own Color rather than another module's same-leaf-name + * Color sitting at the head of c.enums, and (b) `pkg.Color.MEMBER` + * to the new enumlookupmod variant so the explicit qualifier wins + * the leaf-collision on the same shape as fnparamslookupmod (#28) + * and fnretlookupmod (#31). + * + * Pre-fix wwstage's `enumlookup` (selfhost/cmd/wcc/cgen.ww) walked + * c.enums head-first by ename, returning the FIRST match. The + * cgdot enum branch handed it `lhs.str` (bare leaf) for both the + * `Color.MEMBER` shape (lhs N_IDENT) and the `pkg.Color.MEMBER` + * shape (lhs N_DOT) — explicit pkg silently dropped. Either + * collision direction folded `Color.RED` to the wrong constant. + * Silent miscompile, not byte-id: cstage folds at the checker via + * scope_lookup_prefer (cur_mod, "Color") for the bare case and via + * the inner N_DOT's typed SK_USE.SK_TYPE lookup for the qualified + * case, so cs vs ws asm diverged on every collision shape — but no + * stdlib in-tree currently declares two same-leaf enums, so 995 + * stayed green. + * + * Asserts the post-fix constants land inside the matching TEXT + * sym on BOTH stages and that cs vs ws stay byte-identical on + * each row. Sentinel-flips if any of the three pieces revert: + * the enumlookup same-module-first walk, the enumlookupmod + * variant, or the cgdot N_DOT routing. + */ +#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; + const char *textsym; /* TEXT sym containing the load */ + const char *want_imm; /* the immediate that MUST appear */ + const char *bad_imm; /* the immediate that MUST NOT appear */ +}; + +/* Source orderings pick which module's Color sits at the head of + * c.enums after collectenums' head-prepend walk. The LAST `type + * Color = enum ...` in source order ends at the head — that's the + * leaf-collision the same-module-first walk must beat. */ +static const struct row rows[] = { + { "bare_leaf_same_module", + "// MODULE: gamma\n" + "use alpha;\n" + "use beta;\n" + "export fn main() i32 = { return 0; };\n" + "// MODULE: beta\n" + "type Color = enum i32 { RED = 7, };\n" + "export fn readred() Color = { return Color.RED; };\n" + "// MODULE: alpha\n" + "type Color = enum i32 { RED = 100, };\n", + "TEXT beta.readred", "$7,", "$100," }, + { "dot_qualified_explicit_module", + "// MODULE: gamma\n" + "use alpha;\n" + "use beta;\n" + "export fn main() i32 = { return 0; };\n" + "// MODULE: alpha\n" + "type Color = enum i32 { RED = 100, };\n" + "export fn readalphared() Color = { return alpha.Color.RED; };\n" + "// MODULE: beta\n" + "type Color = enum i32 { RED = 7, };\n", + "TEXT alpha.readalphared", "$100,", "$7," }, + /* Caller's module is gamma — neither alpha nor beta — so the + * enumlookup same-module-first walk finds NO Color match in + * gamma. Without the cgdot etmod tracking + enumlookupmod + * variant, enumlookup falls back to head-walk and picks beta's + * Color (last declared → prepended → head). With the new path, + * enumlookupmod("Color", "alpha") prefers alpha. Pins the + * second piece of the trio-leaf fix independent of row 1. */ + { "dot_qualified_cross_module", + "// MODULE: gamma\n" + "use alpha;\n" + "use beta;\n" + "export fn readalpharedgamma() i32 = { return alpha.Color.RED: i32; };\n" + "export fn main() i32 = { return 0; };\n" + "// MODULE: alpha\n" + "type Color = enum i32 { RED = 100, };\n" + "// MODULE: beta\n" + "type Color = enum i32 { RED = 7, };\n", + "TEXT gamma.readalpharedgamma", "$100,", "$7," }, +}; + +static int +slurp(const char *path, char *buf, size_t cap) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + size_t n = fread(buf, 1, cap - 1, f); + fclose(f); + buf[n] = '\0'; + return (int)n; +} + +static int +emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) +{ + char src[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/ems_%d_%d.ww", getpid(), i); + snprintf(out_s, cap, "/tmp/ems_%d_%d_%s.s", + getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); + int rc = runwait(cmd); + unlink(src); + return rc; +} + +/* Inside the named TEXT sym, before its first RET, the want_imm + * MUST appear and the bad_imm MUST NOT. bad_imm flags pre-fix + * head-walk picking the wrong-module Color. */ +static int +check_imm(const char *spath, const struct row *r, const char *stage) +{ + char buf[1 << 14]; + if (slurp(spath, buf, sizeof buf) < 0) { + fprintf(stderr, "row[%s][%s]: cannot read %s\n", + r->label, stage, spath); + return -1; + } + const char *fn = strstr(buf, r->textsym); + if (!fn) { + fprintf(stderr, "row[%s][%s]: no %s in %s\n", + r->label, stage, r->textsym, spath); + return -1; + } + const char *ret = strstr(fn, "\tRET"); + if (!ret) { + fprintf(stderr, "row[%s][%s]: no RET inside %s\n", + r->label, stage, r->textsym); + return -1; + } + const char *good = strstr(fn, r->want_imm); + if (!good || good >= ret) { + fprintf(stderr, + "row[%s][%s]: want_imm %s missing inside %s\n", + r->label, stage, r->want_imm, r->textsym); + return -1; + } + const char *bad = strstr(fn, r->bad_imm); + if (bad && bad < ret) { + fprintf(stderr, + "row[%s][%s]: bad_imm %s present inside %s — wrong-module Color\n", + r->label, stage, r->bad_imm, r->textsym); + return -1; + } + return 0; +} + +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 w6c[640], w6c_ww[640]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + + int have_ww = (access(w6c_ww, X_OK) == 0); + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + char cs_path[128], ws_path[128]; + + if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { + fprintf(stderr, + "enum_modshadow[cstage][%s]: w6c failed\n", + rows[i].label); + fail++; total++; continue; + } + total++; + if (check_imm(cs_path, &rows[i], "cstage") != 0) fail++; + + if (!have_ww) { unlink(cs_path); continue; } + + if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { + fprintf(stderr, + "enum_modshadow[wwstage][%s]: w6c_ww failed\n", + rows[i].label); + fail++; total++; + unlink(cs_path); continue; + } + total++; + if (check_imm(ws_path, &rows[i], "wwstage") != 0) fail++; + + total++; + char cmd[512]; + snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); + if (runwait(cmd) != 0) { + fprintf(stderr, + "enum_modshadow[%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + + unlink(cs_path); unlink(ws_path); + } + + if (fail) { + fprintf(stderr, + "enum_modshadow: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("enum_modshadow: %d/%d ok\n", total, total); + return 0; +}