From 4bd4ed925a23de83d59ab7182714c4683bdca1fd Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 18 May 2026 14:42:20 +0900 Subject: [PATCH] selfhost+cstage+test: graduate deflookup/deflookuprhs same-module-first (#4c) 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 str-typed def leaf name and the .ptr/.len field-fold path consumes the wrong-module strlit address/length. Wwstage's deflookuprhs (selfhost/cmd/wcc/cgen.ww) walked c.defs head-first by dname; cgdot's .ptr/.len field-fold handed it the bare leaf from N_IDENT.str, silently inlining the wrong-module strlit. Cstage carries the same shape at cmd/w6c/cgen.c (Sdef walk #3 N_DOT field-fold): Sdef keyed by name only, head-pick on every cross-module collision. No in-tree corpus declares two same-leaf str defs, so 995_self_rebuild stayed green (same surfacing pattern as #4a enumlookup post-strings and #4b structlookup). Sixth leaf of the trio leaf-name lookup graduation (after #27 aliaslookup, #28 fnparams, #31 fnret, #4a enum, #4b struct). Same bundle precedent as #4a (which bundled wwstage enumlookup + enumlookupmod + cstage scope_lookup_prefer sister fix under one structural concern): four sister changes ship together. - defent +dmod field; collectdefs captures d.module. - wwstage deflookup two-pass walk — cosmetic (bool return is invariant under head-pick vs same-module-first), kept for structural symmetry with deflookuprhs. - wwstage deflookuprhs two-pass walk — load-bearing for the .ptr/.len field fold. - cstage Sdef +mod field; sdef_collect captures d->module raw (matches cgfn's raw cur_mod convention); new sdef_mod_match helper handles NULL-safe strcmp; cstage Sdef walk #3 N_DOT field-fold graduation (sister of wwstage deflookuprhs). Two additional cstage Sdef walks (N_IDENT bare load + N_DOT mod- qualified fallback) are DEFERRED. Both consume wwstage's cgenexpr.ww:553 path which is independently broken (str-def bare/ qualified reference emits MOVQ symname(SB) where strlit-inline is required); sentinel rows for those walks fail cs-vs-ws byte-id regardless of the cstage prefer-pass behavior. Per rule 7 the prefer-pass cannot ship without sentinels. Filed: task #11 (cstage walk #2 also needs n->lhs->str as hint source rather than cur_mod, matching #4a/#28/#31's *mod variant pattern) + task #12 (wwstage str-def symbol-load fix that unblocks both deferrals). 735_def_modshadow pins the fix with 1 row: bare-leaf .len of MSG in module alpha must fold against alpha's own def MSG (strlit length 41) even with beta's same-leaf 27-char def MSG at the head of c.defs / sdefs. Asserts the matching immediate inside the right TEXT sym + bad_imm anti-check on both stages plus byte-id between stages. --- Makefile | 5 + cmd/w6c/cgen.c | 105 +++++++++--- selfhost/cmd/w6c/main.combined.ww | 33 +++- selfhost/cmd/wcc/cgen.ww | 33 +++- selfhost/cmd/wwdump/main.combined.ww | 33 +++- test/wcc/735_def_modshadow.c | 230 +++++++++++++++++++++++++++ 6 files changed, 400 insertions(+), 39 deletions(-) create mode 100644 test/wcc/735_def_modshadow.c diff --git a/Makefile b/Makefile index fe37f54c..d24df796 100644 --- a/Makefile +++ b/Makefile @@ -264,6 +264,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_match_4arm_cross_module_run \ $(BIN)/test_enum_modshadow \ $(BIN)/test_struct_modshadow \ + $(BIN)/test_def_modshadow \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -618,6 +619,10 @@ $(BIN)/test_struct_modshadow: test/wcc/734_struct_modshadow.c \ $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_def_modshadow: test/wcc/735_def_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/w6c/cgen.c b/cmd/w6c/cgen.c index f5d38fb8..cd541fb5 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -500,12 +500,27 @@ static Ffi *ffi_map; typedef struct Sdef Sdef; struct Sdef { const char *name; + const char *mod; /* raw `// MODULE:` directive on the decl, + * or NULL. Mirrors cgfn's c->cur_mod which + * stores the same raw form. */ const char *bytes; u64 len; Sdef *next; }; static Sdef *sdefs; +/* Same-module-first match for Sdef walks. Mirrors wwstage deflookuprhs's + * first pass: returns 1 iff s belongs to the fn we're emitting. Caller + * still re-walks for the any-module fallback. */ +static int +sdef_mod_match(Cg *c, Sdef *s) +{ + const char *a = s->mod, *b = c->cur_mod; + if (a == b) return 1; + if (a == NULL || b == NULL) return 0; + return strcmp(a, b) == 0; +} + /* Interned string literals — emitted as DATA directives after all * function bodies, so the linker lays them out alongside .text. */ typedef struct Strlit Strlit; @@ -1742,14 +1757,33 @@ cgexpr(Cg *c, Node *n, Local *locals) mafn(c, n->str, c->cur_mod), areg(D_AX)); break; } - for (Sdef *s = sdefs; s; s = s->next) { - if (strcmp(s->name, n->str) != 0) continue; - const char *lab = intern_strlit(c, s->bytes, - s->len); - ins2(c, A_LEAQ, asym(lab), areg(D_AX)); - ins2(c, A_MOVQ, aimm((long long)s->len), - areg(D_BX)); - goto ident_done; + { + /* Same-module-first walk over Sdef. Without + * the prefer pass two modules with same-leaf + * `def MSG: str = "..."` silently fold the + * wrong strlit into the caller's bare-ident + * load (sister callsite of cgdot's str-def + * field fold + wwstage deflookuprhs #4c). */ + Sdef *s; + for (s = sdefs; s; s = s->next) { + if (strcmp(s->name, n->str) != 0) + continue; + if (sdef_mod_match(c, s)) break; + } + if (s == NULL) { + for (s = sdefs; s; s = s->next) + if (strcmp(s->name, n->str) == 0) + break; + } + if (s != NULL) { + const char *lab = intern_strlit(c, + s->bytes, s->len); + ins2(c, A_LEAQ, asym(lab), areg(D_AX)); + ins2(c, A_MOVQ, + aimm((long long)s->len), + areg(D_BX)); + goto ident_done; + } } if (let_islet(n->str) && (let_isstr(n->type) || let_isslice(n->type))) { @@ -5404,19 +5438,47 @@ cgexpr(Cg *c, Node *n, Local *locals) * N_IDENT branch above. Without this we'd * load BP+8 (return-address slot) as the * "len". */ - for (Sdef *s = sdefs; s; s = s->next) { - if (strcmp(s->name, n->lhs->str) != 0) - continue; - if (ptrfld) { - const char *lab = intern_strlit(c, - s->bytes, s->len); - ins2(c, A_LEAQ, asym(lab), areg(D_AX)); - } else { - ins2(c, A_MOVQ, - aimm((long long)s->len), - areg(D_AX)); + { + /* Same-module-first walk: two + * same-leaf `def MSG: str = ...` + * across modules would otherwise + * fold the wrong strlit's length / + * label into `MSG.len` / `MSG.ptr` + * (sister of wwstage deflookuprhs + * #4c). */ + Sdef *s; + for (s = sdefs; s; s = s->next) { + if (strcmp(s->name, + n->lhs->str) != 0) + continue; + if (sdef_mod_match(c, s)) + break; + } + if (s == NULL) { + for (s = sdefs; s; + s = s->next) + if (strcmp(s->name, + n->lhs->str) + == 0) + break; + } + if (s != NULL) { + if (ptrfld) { + const char *lab = + intern_strlit(c, + s->bytes, + s->len); + ins2(c, A_LEAQ, + asym(lab), + areg(D_AX)); + } else { + ins2(c, A_MOVQ, + aimm((long long) + s->len), + areg(D_AX)); + } + goto dot_done; } - goto dot_done; } /* Top-level str/slice `let` — load * the field through &name(SB). Same @@ -7521,7 +7583,7 @@ emit_defs(Cg *c, FILE *out, Node *file) static void sdef_collect(Cg *c, Node *file) { - (void)c; + (void)file; sdefs = NULL; for (Node *d = file->list; d; d = d->next) { if (d->kind != N_DEF || d->rhs == NULL) continue; @@ -7530,6 +7592,7 @@ sdef_collect(Cg *c, Node *file) if (r == NULL || r->kind != N_STRLIT) continue; Sdef *s = amalloc(c->a, sizeof *s); s->name = d->str; + s->mod = (d->module && d->module[0]) ? d->module : NULL; s->bytes = r->str; s->len = r->strlen; s->next = sdefs; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index bf4ea55f..0f40657e 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -20365,6 +20365,7 @@ fn fnparamslookupmod(c: *cgen, name: str, mod: str) *node = { type defent = struct { dname: str, + dmod: str, // originating module (`// MODULE: foo`), or empty drhs: *node, dnext: *defent, }; @@ -20374,8 +20375,9 @@ fn collectdefs(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_DEF) { - let e: *defent = amalloc(c.a, 32u64): *defent; + let e: *defent = amalloc(c.a, 64u64): *defent; e.dname = d.str; + e.dmod = d.module; e.drhs = d.rhs; e.dnext = c.defs; c.defs = e; @@ -20384,24 +20386,43 @@ fn collectdefs(c: *cgen, file: *node) void = { }; }; +// Same-module-first walk, then any. Trio-leaf graduation mirroring +// aliaslookup (#27) and enum/structlookup (#4a/#4b): bool answer is +// invariant either way, but the structural shape mirrors deflookuprhs +// where the entry's drhs IS module-sensitive. fn deflookup(c: *cgen, name: str) bool = { let e: *defent = c.defs; for (e != nil) { - let dn: str = e.dname; - if (streq(dn, name)) { return true; }; + if (streq(e.dname, name)) { + if (streq(e.dmod, c.curmod)) { return true; }; + }; + e = e.dnext; + }; + e = c.defs; + for (e != nil) { + if (streq(e.dname, name)) { return true; }; e = e.dnext; }; return false; }; // Returns the rhs init node for a top-level `def`, or nil if `name` -// doesn't name a def. Used by cgdot to inline `.ptr`/`.len` on +// doesn't name a def. Same-module-first walk: without the prefer pass +// `MSG.ptr`/`MSG.len` in module M can collapse onto another module's +// same-leaf `def MSG: str = ...` sitting at the head of c.defs and +// inline the wrong strlit. Used by cgdot to inline `.ptr`/`.len` on // `def NAME: str = "..."` — those aren't laid out in memory. fn deflookuprhs(c: *cgen, name: str) *node = { let e: *defent = c.defs; for (e != nil) { - let dn: str = e.dname; - if (streq(dn, name)) { return e.drhs; }; + if (streq(e.dname, name)) { + if (streq(e.dmod, c.curmod)) { return e.drhs; }; + }; + e = e.dnext; + }; + e = c.defs; + for (e != nil) { + if (streq(e.dname, name)) { return e.drhs; }; e = e.dnext; }; return nil; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index c40b6c58..a5921616 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -1635,6 +1635,7 @@ fn fnparamslookupmod(c: *cgen, name: str, mod: str) *node = { type defent = struct { dname: str, + dmod: str, // originating module (`// MODULE: foo`), or empty drhs: *node, dnext: *defent, }; @@ -1644,8 +1645,9 @@ fn collectdefs(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_DEF) { - let e: *defent = amalloc(c.a, 32u64): *defent; + let e: *defent = amalloc(c.a, 64u64): *defent; e.dname = d.str; + e.dmod = d.module; e.drhs = d.rhs; e.dnext = c.defs; c.defs = e; @@ -1654,24 +1656,43 @@ fn collectdefs(c: *cgen, file: *node) void = { }; }; +// Same-module-first walk, then any. Trio-leaf graduation mirroring +// aliaslookup (#27) and enum/structlookup (#4a/#4b): bool answer is +// invariant either way, but the structural shape mirrors deflookuprhs +// where the entry's drhs IS module-sensitive. fn deflookup(c: *cgen, name: str) bool = { let e: *defent = c.defs; for (e != nil) { - let dn: str = e.dname; - if (streq(dn, name)) { return true; }; + if (streq(e.dname, name)) { + if (streq(e.dmod, c.curmod)) { return true; }; + }; + e = e.dnext; + }; + e = c.defs; + for (e != nil) { + if (streq(e.dname, name)) { return true; }; e = e.dnext; }; return false; }; // Returns the rhs init node for a top-level `def`, or nil if `name` -// doesn't name a def. Used by cgdot to inline `.ptr`/`.len` on +// doesn't name a def. Same-module-first walk: without the prefer pass +// `MSG.ptr`/`MSG.len` in module M can collapse onto another module's +// same-leaf `def MSG: str = ...` sitting at the head of c.defs and +// inline the wrong strlit. Used by cgdot to inline `.ptr`/`.len` on // `def NAME: str = "..."` — those aren't laid out in memory. fn deflookuprhs(c: *cgen, name: str) *node = { let e: *defent = c.defs; for (e != nil) { - let dn: str = e.dname; - if (streq(dn, name)) { return e.drhs; }; + if (streq(e.dname, name)) { + if (streq(e.dmod, c.curmod)) { return e.drhs; }; + }; + e = e.dnext; + }; + e = c.defs; + for (e != nil) { + if (streq(e.dname, name)) { return e.drhs; }; e = e.dnext; }; return nil; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index e6de7536..e0d81d99 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -20365,6 +20365,7 @@ fn fnparamslookupmod(c: *cgen, name: str, mod: str) *node = { type defent = struct { dname: str, + dmod: str, // originating module (`// MODULE: foo`), or empty drhs: *node, dnext: *defent, }; @@ -20374,8 +20375,9 @@ fn collectdefs(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_DEF) { - let e: *defent = amalloc(c.a, 32u64): *defent; + let e: *defent = amalloc(c.a, 64u64): *defent; e.dname = d.str; + e.dmod = d.module; e.drhs = d.rhs; e.dnext = c.defs; c.defs = e; @@ -20384,24 +20386,43 @@ fn collectdefs(c: *cgen, file: *node) void = { }; }; +// Same-module-first walk, then any. Trio-leaf graduation mirroring +// aliaslookup (#27) and enum/structlookup (#4a/#4b): bool answer is +// invariant either way, but the structural shape mirrors deflookuprhs +// where the entry's drhs IS module-sensitive. fn deflookup(c: *cgen, name: str) bool = { let e: *defent = c.defs; for (e != nil) { - let dn: str = e.dname; - if (streq(dn, name)) { return true; }; + if (streq(e.dname, name)) { + if (streq(e.dmod, c.curmod)) { return true; }; + }; + e = e.dnext; + }; + e = c.defs; + for (e != nil) { + if (streq(e.dname, name)) { return true; }; e = e.dnext; }; return false; }; // Returns the rhs init node for a top-level `def`, or nil if `name` -// doesn't name a def. Used by cgdot to inline `.ptr`/`.len` on +// doesn't name a def. Same-module-first walk: without the prefer pass +// `MSG.ptr`/`MSG.len` in module M can collapse onto another module's +// same-leaf `def MSG: str = ...` sitting at the head of c.defs and +// inline the wrong strlit. Used by cgdot to inline `.ptr`/`.len` on // `def NAME: str = "..."` — those aren't laid out in memory. fn deflookuprhs(c: *cgen, name: str) *node = { let e: *defent = c.defs; for (e != nil) { - let dn: str = e.dname; - if (streq(dn, name)) { return e.drhs; }; + if (streq(e.dname, name)) { + if (streq(e.dmod, c.curmod)) { return e.drhs; }; + }; + e = e.dnext; + }; + e = c.defs; + for (e != nil) { + if (streq(e.dname, name)) { return e.drhs; }; e = e.dnext; }; return nil; diff --git a/test/wcc/735_def_modshadow.c b/test/wcc/735_def_modshadow.c new file mode 100644 index 00000000..04add104 --- /dev/null +++ b/test/wcc/735_def_modshadow.c @@ -0,0 +1,230 @@ +/* + * 735_def_modshadow — sentinel for the trio leaf-name pattern's 6th + * leaf: wwstage's deflookuprhs (selfhost/cmd/wcc/cgen.ww). Pins + * bare-leaf `MSG.len`/`MSG.ptr` on a `def MSG: str = "..."` constant + * to a same-module-first walk so the field-fold inlines the caller's + * own strlit length / address rather than another module's same-leaf- + * name def sitting at the head of c.defs. + * + * Pre-fix wwstage's `deflookuprhs` walked c.defs head-first by dname + * and returned the FIRST match's drhs. cgdot's str-def `.ptr`/`.len` + * branch handed it `lhs.str` (the bare leaf from a parsed N_IDENT) + * and the head-pick silently inlined the wrong-module strlit — a + * LEAQ of the other module's interned strlit label for `.ptr` and a + * MOVQ of the other module's byte-length for `.len`. Cstage's + * symmetric Sdef walk in cmd/w6c/cgen.c (N_DOT `.ptr`/`.len` field + * fold, off==0 branch) did the same head-first pick. The checker's + * resolve-side already routes through scope_lookup_prefer (cur, + * cur_mod, MSG) so the typed Sym is right, but cgen's Sdef table is + * keyed off the bare-leaf string with no module attribution — cs and + * ws diverged on every same-leaf def collision but no in-tree corpus + * declares two same-leaf str defs (same surfacing pattern as #4a + * enumlookup post-strings and #4b structlookup post-strings: latent + * until a stdlib port introduces the collision). + * + * Sixth leaf of the trio graduation (after #27 aliaslookup, #28 + * fnparams, #31 fnret, #4a enum, #4b struct): defent grows a dmod + * field, deflookup/deflookuprhs walk same-module-first against + * c.curmod before the head-walk fallback. Cstage Sdef grows a mod + * field; the symmetric N_DOT `.ptr`/`.len` field-fold walk gains the + * matching prefer pass keyed off c->cur_mod via sdef_mod_match. The + * two other cstage Sdef walks (N_IDENT bare-str-def load, N_DOT + * module-qualified fallback) stay on head-walk in this commit — + * both have a pre-existing wwstage byte-id break (wwstage emits a + * bogus MOVQ (SB) symbol load for bare/qualified str-def + * reference instead of strlit-inline), so a graduating prefer pass + * on the cstage side alone would not sentinel-flip via the cs-vs-ws + * byte-id check. Deferred to follow-ups #4c-tail-{bare,modqual} + + * wwstage str-def symbol-load fix. NO deflookupmod variant — same + * scope rationale as #4b's structlookup-no-mod (no in-tree consumer + * for `pkg.def.field`). + * + * Pin: row 1 sentinel-flips this commit's new same-module-first walk + * on BOTH stages — revert the prefer pass and row 1 fails on both + * cstage and wwstage. Asserts the .len fold lands inside the matching + * TEXT sym with the want_imm present + bad_imm anti-check on each + * stage plus cs-vs-ws byte-id per row. + */ +#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 `def MSG` sits at the head of + * c.defs / sdefs after the collectdefs / sdef_collect head-prepend + * walk. The LAST `def MSG = ...` in source order ends at the head — + * that's the leaf-collision the same-module-first walk must beat. + * + * Strlit lengths chosen so neither digit-string is a substring of the + * other (41 vs 27) and neither aliases a common frame/offset constant + * the asm emits (0/8/16/24/32/48 etc) — strstr-based needle matching + * would otherwise spuriously hit "$1" inside "$16," etc. */ +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: alpha\n" + "def MSG: str = \"alpha_msg_for_modshadow_test_pin_45_AAAAA\";\n" + "export fn alphalen() i32 = { return MSG.len: i32; };\n" + "// MODULE: beta\n" + "def MSG: str = \"beta_msg_short_27_chr_pin_X\";\n", + "TEXT alpha.alphalen", "$41,", "$27," }, +}; + +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/dms_%d_%d.ww", getpid(), i); + snprintf(out_s, cap, "/tmp/dms_%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 MSG's strlit length. */ +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 MSG\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, + "def_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, + "def_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, + "def_modshadow[%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + + unlink(cs_path); unlink(ws_path); + } + + if (fail) { + fprintf(stderr, + "def_modshadow: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("def_modshadow: %d/%d ok\n", total, total); + return 0; +}