diff --git a/Makefile b/Makefile index 9bb54033..bfb8d09f 100644 --- a/Makefile +++ b/Makefile @@ -267,6 +267,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_chainidx_run \ $(BIN)/test_m1mangle_run \ $(BIN)/test_m1mangle_sym \ + $(BIN)/test_m1usehint_run \ $(BIN)/test_m1union_run \ $(BIN)/test_tupfieldsize_run \ $(BIN)/test_tagtupfieldsize_run \ @@ -837,6 +838,18 @@ $(BIN)/test_m1mangle_sym: test/wcc/989_m1mangle_sym.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_m1usehint_run (M1, #40): the cgen mangle-hint must be MODULE-scoped. +# Two same-leaf exported fns across directory-packages, each `import`ed by a +# different module aliasing `math`; every `math.pick()` must route to its own +# import (not the file-global first-match). Builds+runs on BOTH stages + pins +# cstage.s==wwstage.s — the gate-VISIBLE #263-class hint miscompile. +$(BIN)/test_m1usehint_run: test/wcc/989_m1usehint_run.c \ + $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # 989_m1union_run (M1, #199b): a `match` on a cross-module fn's tagged-union # return must keep each nominal void-alias variant on a distinct tag. Builds+ # runs the utf8.next 4-arm match per-arm on BOTH stages (rule-10) and pins diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index b9f606c8..5aa6c4f7 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1175,33 +1175,36 @@ typedef struct Use Use; struct Use { const char *alias; const char *path; + const char *module; /* owning module of the `use` decl (#40) */ Use *next; }; static Use *use_map; /* - * Retained divergence (M1 #22): this map is file-global — when two - * modules in the same unit bind the same leaf alias to different paths - * (sha256's `import crypto.math` vs strconv's `import math`), the first - * match wins. The checker's twin (use_path, check.c) is module-scoped - * to fix exactly this for qualified-NAME resolution; the cgen mangle - * hint is NOT, because it is unreachable with an ambiguous alias: a - * cross-module qualified ref resolves only to an EXPORTED symbol, and - * exported non-fn decls stay bare (mod_collect skips them, so the hint - * is moot) while an exported-fn collision would require two same-leaf - * packages to export the same fn name AND a third caller — not present - * (the tree links cleanly). Left file-global to stay byte-identical - * with wwstage's symmetric usehint (cgen.ww), which is also file-global - * (rule 10). Module-scope both stages together if a real collision - * surfaces — filed as the M1 cgen-hint twin follow-up. + * use_hint — map a `use` alias to its dotted import path for the + * qualified-ref mangle hint. NOT file-global: two modules in one unit + * may bind the same leaf alias to different paths (#40 — module one's + * `import a.math` and module two's `import b.math` both alias `math`). + * The import declared in the SAME module as the reference (curmod) is + * authoritative; preferring it routes each `math.pick()` to its own + * package. Falls back to any matching alias when curmod has no own + * import (single-occurrence case). Mirrors the checker's use_path + * curmod-preference (check.c, M1 55f54fb). Returns the alias unchanged + * when no `use` matches. */ static const char * -use_hint(const char *alias) +use_hint(const char *curmod, const char *alias) { + const char *any = NULL; if (alias == NULL) return alias; - for (Use *u = use_map; u; u = u->next) - if (strcmp(u->alias, alias) == 0) return u->path; - return alias; + for (Use *u = use_map; u; u = u->next) { + if (strcmp(u->alias, alias) != 0) continue; + int same = (u->module == NULL) ? (curmod == NULL) + : (curmod != NULL && strcmp(u->module, curmod) == 0); + if (same) return u->path; + if (any == NULL) any = u->path; + } + return any ? any : alias; } /* Top-level `let` map. Populated alongside mod_map; consulted by the @@ -1428,6 +1431,7 @@ mod_collect(Cg *c, Node *file) Use *u = amalloc(c->a, sizeof *u); u->alias = d->str; u->path = d->usepath; + u->module = d->module; u->next = use_map; use_map = u; continue; @@ -4371,7 +4375,7 @@ cgexpr(Cg *c, Node *n, Local *locals) if (lu && lu->kind == TY_FN) ins2(c, A_LEAQ, mafn(c, opnd->str, - use_hint(opnd->lhs->str)), + use_hint(c->cur_mod, opnd->lhs->str)), areg(D_AX)); else /* #229: dotted-module value @@ -4380,7 +4384,7 @@ cgexpr(Cg *c, Node *n, Local *locals) * same-leaf collision. */ ins2(c, A_LEAQ, mahint(c, opnd->str, - use_hint(opnd->lhs->str)), + use_hint(c->cur_mod, opnd->lhs->str)), areg(D_AX)); break; } @@ -10232,7 +10236,7 @@ cgexpr(Cg *c, Node *n, Local *locals) * same-leaf exports resolve correctly. */ ins1(c, A_CALL, mafn(c, n->lhs->str, - use_hint(n->lhs->lhs->str))); + use_hint(c->cur_mod, n->lhs->lhs->str))); } else { cgexpr(c, n->lhs, locals); /* AX = fn ptr */ ins1(c, A_CALL, areg(D_AX)); @@ -11108,7 +11112,7 @@ cgexpr(Cg *c, Node *n, Local *locals) /* `mod.fn` address-of via N_DOT — pass the * module bareword as the disambiguation hint. */ ins2(c, A_LEAQ, - mafn(c, n->str, use_hint(n->lhs->str)), + mafn(c, n->str, use_hint(c->cur_mod, n->lhs->str)), areg(D_AX)); break; } @@ -11125,7 +11129,7 @@ cgexpr(Cg *c, Node *n, Local *locals) for (s = sdefs; s; s = s->next) { if (strcmp(s->name, n->str) != 0) continue; - if (sdef_mod_match_hint(s, use_hint(n->lhs->str))) + if (sdef_mod_match_hint(s, use_hint(c->cur_mod, n->lhs->str))) break; } if (s == NULL) { @@ -11157,11 +11161,11 @@ cgexpr(Cg *c, Node *n, Local *locals) * branch above already uses n->lhs->str via mafn. */ if (mqop == A_MOVQ) { ins2(c, A_MOVQ, - mahint(c, n->str, use_hint(n->lhs->str)), + mahint(c, n->str, use_hint(c->cur_mod, n->lhs->str)), areg(D_AX)); } else { ins2(c, A_LEAQ, - mahint(c, n->str, use_hint(n->lhs->str)), + mahint(c, n->str, use_hint(c->cur_mod, n->lhs->str)), areg(D_CX)); ins2(c, mqop, amem(D_CX, 0), areg(D_AX)); } @@ -16049,7 +16053,7 @@ node_fnptr_sym(Cg *c, Node *ev) return NULL; Type *du = type_chase_named(opnd->type); if (du == NULL || du->kind != TY_FN) return NULL; - return mod_mangle_fn(c, opnd->str, use_hint(opnd->lhs->str)); + return mod_mangle_fn(c, opnd->str, use_hint(c->cur_mod, opnd->lhs->str)); } if (opnd == NULL || opnd->kind != N_IDENT) return NULL; Type *ou = type_chase_named(opnd->type); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index afbf35ec..0390dee5 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -44986,6 +44986,7 @@ fn defisaddressable(c: *cgen, opnd: *node) bool = { type modent = struct { mname: str, // the bare ident as it appears in source nmod: str, // the originating module (`// MODULE: foo`) + omod: str, // owning module of a `use` decl (#40); unused for mods mnext: *modent, }; @@ -44998,7 +44999,7 @@ fn collectmods(c: *cgen, file: *node) void = { // M1 #22: record alias→path for the qualified-ref hint. if (d.kind == nkind.N_USE) { if (d.usepath.len > 0) { - let um: *modent = alloc(modent{mname=d.str, nmod=d.usepath, mnext=c.uses})!; + let um: *modent = alloc(modent{mname=d.str, nmod=d.usepath, omod=d.nmod, mnext=c.uses})!; c.uses = um; }; }; @@ -45022,7 +45023,7 @@ fn collectmods(c: *cgen, file: *node) void = { // M1 #32: the ROOT main (imported==0) stays bare; // an IMPORTED `fn main` mangles on its path. if (!streq(d.str, "main") || d.imported != 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; + let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; @@ -45031,7 +45032,7 @@ fn collectmods(c: *cgen, file: *node) void = { if (d.kind == nkind.N_DEF) { if (d.exported == 0) { if (d.nmod.len > 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; + let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; @@ -45039,7 +45040,7 @@ fn collectmods(c: *cgen, file: *node) void = { if (d.kind == nkind.N_TYPEDECL) { if (d.exported == 0) { if (d.nmod.len > 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; + let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; @@ -45047,7 +45048,7 @@ fn collectmods(c: *cgen, file: *node) void = { if (d.kind == nkind.N_LET) { if (d.exported == 0) { if (d.nmod.len > 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; + let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; @@ -45072,12 +45073,27 @@ fn modlookup(c: *cgen, name: str) str = { // import path (`encoding.utf8`) so the codegen hint keys the path-keyed // mods map. For single-level packages alias == path (no-op). Returns the // alias unchanged when no matching `use` exists. +// +// NOT file-global (#40): two modules in one unit may bind the same leaf +// alias to different paths (module one's `import a.math` vs module two's +// `import b.math`, both alias `math`). The `use` declared in the SAME +// module as the reference (c.curmod) is authoritative; preferring it +// routes each `math.pick()` to its own package. Falls back to any +// matching alias when c.curmod has no own import. Mirrors the checker's +// use_path curmod-preference (cstage check.c, M1 55f54fb). fn usehint(c: *cgen, alias: str) str = { let m: *modent = c.uses; + let any: str; + any.ptr = nil; + any.len = 0; for (m != nil) { - if (streq(m.mname, alias)) { return m.nmod; }; + if (streq(m.mname, alias)) { + if (streq(m.omod, c.curmod)) { return m.nmod; }; + if (any.ptr == nil && any.len == 0) { any = m.nmod; }; + }; m = m.mnext; }; + if (any.ptr != nil || any.len != 0) { return any; }; return alias; }; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 646f5e7d..0867f2ec 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -3787,6 +3787,7 @@ fn defisaddressable(c: *cgen, opnd: *node) bool = { type modent = struct { mname: str, // the bare ident as it appears in source nmod: str, // the originating module (`// MODULE: foo`) + omod: str, // owning module of a `use` decl (#40); unused for mods mnext: *modent, }; @@ -3799,7 +3800,7 @@ fn collectmods(c: *cgen, file: *node) void = { // M1 #22: record alias→path for the qualified-ref hint. if (d.kind == nkind.N_USE) { if (d.usepath.len > 0) { - let um: *modent = alloc(modent{mname=d.str, nmod=d.usepath, mnext=c.uses})!; + let um: *modent = alloc(modent{mname=d.str, nmod=d.usepath, omod=d.nmod, mnext=c.uses})!; c.uses = um; }; }; @@ -3823,7 +3824,7 @@ fn collectmods(c: *cgen, file: *node) void = { // M1 #32: the ROOT main (imported==0) stays bare; // an IMPORTED `fn main` mangles on its path. if (!streq(d.str, "main") || d.imported != 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; + let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; @@ -3832,7 +3833,7 @@ fn collectmods(c: *cgen, file: *node) void = { if (d.kind == nkind.N_DEF) { if (d.exported == 0) { if (d.nmod.len > 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; + let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; @@ -3840,7 +3841,7 @@ fn collectmods(c: *cgen, file: *node) void = { if (d.kind == nkind.N_TYPEDECL) { if (d.exported == 0) { if (d.nmod.len > 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; + let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; @@ -3848,7 +3849,7 @@ fn collectmods(c: *cgen, file: *node) void = { if (d.kind == nkind.N_LET) { if (d.exported == 0) { if (d.nmod.len > 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; + let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; @@ -3873,12 +3874,27 @@ fn modlookup(c: *cgen, name: str) str = { // import path (`encoding.utf8`) so the codegen hint keys the path-keyed // mods map. For single-level packages alias == path (no-op). Returns the // alias unchanged when no matching `use` exists. +// +// NOT file-global (#40): two modules in one unit may bind the same leaf +// alias to different paths (module one's `import a.math` vs module two's +// `import b.math`, both alias `math`). The `use` declared in the SAME +// module as the reference (c.curmod) is authoritative; preferring it +// routes each `math.pick()` to its own package. Falls back to any +// matching alias when c.curmod has no own import. Mirrors the checker's +// use_path curmod-preference (cstage check.c, M1 55f54fb). fn usehint(c: *cgen, alias: str) str = { let m: *modent = c.uses; + let any: str; + any.ptr = nil; + any.len = 0; for (m != nil) { - if (streq(m.mname, alias)) { return m.nmod; }; + if (streq(m.mname, alias)) { + if (streq(m.omod, c.curmod)) { return m.nmod; }; + if (any.ptr == nil && any.len == 0) { any = m.nmod; }; + }; m = m.mnext; }; + if (any.ptr != nil || any.len != 0) { return any; }; return alias; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 6f541b4e..3233a0ed 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -44986,6 +44986,7 @@ fn defisaddressable(c: *cgen, opnd: *node) bool = { type modent = struct { mname: str, // the bare ident as it appears in source nmod: str, // the originating module (`// MODULE: foo`) + omod: str, // owning module of a `use` decl (#40); unused for mods mnext: *modent, }; @@ -44998,7 +44999,7 @@ fn collectmods(c: *cgen, file: *node) void = { // M1 #22: record alias→path for the qualified-ref hint. if (d.kind == nkind.N_USE) { if (d.usepath.len > 0) { - let um: *modent = alloc(modent{mname=d.str, nmod=d.usepath, mnext=c.uses})!; + let um: *modent = alloc(modent{mname=d.str, nmod=d.usepath, omod=d.nmod, mnext=c.uses})!; c.uses = um; }; }; @@ -45022,7 +45023,7 @@ fn collectmods(c: *cgen, file: *node) void = { // M1 #32: the ROOT main (imported==0) stays bare; // an IMPORTED `fn main` mangles on its path. if (!streq(d.str, "main") || d.imported != 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; + let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; @@ -45031,7 +45032,7 @@ fn collectmods(c: *cgen, file: *node) void = { if (d.kind == nkind.N_DEF) { if (d.exported == 0) { if (d.nmod.len > 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; + let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; @@ -45039,7 +45040,7 @@ fn collectmods(c: *cgen, file: *node) void = { if (d.kind == nkind.N_TYPEDECL) { if (d.exported == 0) { if (d.nmod.len > 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; + let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; @@ -45047,7 +45048,7 @@ fn collectmods(c: *cgen, file: *node) void = { if (d.kind == nkind.N_LET) { if (d.exported == 0) { if (d.nmod.len > 0) { - let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; + let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; @@ -45072,12 +45073,27 @@ fn modlookup(c: *cgen, name: str) str = { // import path (`encoding.utf8`) so the codegen hint keys the path-keyed // mods map. For single-level packages alias == path (no-op). Returns the // alias unchanged when no matching `use` exists. +// +// NOT file-global (#40): two modules in one unit may bind the same leaf +// alias to different paths (module one's `import a.math` vs module two's +// `import b.math`, both alias `math`). The `use` declared in the SAME +// module as the reference (c.curmod) is authoritative; preferring it +// routes each `math.pick()` to its own package. Falls back to any +// matching alias when c.curmod has no own import. Mirrors the checker's +// use_path curmod-preference (cstage check.c, M1 55f54fb). fn usehint(c: *cgen, alias: str) str = { let m: *modent = c.uses; + let any: str; + any.ptr = nil; + any.len = 0; for (m != nil) { - if (streq(m.mname, alias)) { return m.nmod; }; + if (streq(m.mname, alias)) { + if (streq(m.omod, c.curmod)) { return m.nmod; }; + if (any.ptr == nil && any.len == 0) { any = m.nmod; }; + }; m = m.mnext; }; + if (any.ptr != nil || any.len != 0) { return any; }; return alias; }; diff --git a/test/wcc/989_m1usehint_run.c b/test/wcc/989_m1usehint_run.c new file mode 100644 index 00000000..5f56e195 --- /dev/null +++ b/test/wcc/989_m1usehint_run.c @@ -0,0 +1,255 @@ +/* + * 989_m1usehint_run — M1 (#40): the cgen mangle-hint must be MODULE-scoped, + * not unit-global. When two directory-packages in the same compilation unit + * export the same leaf fn (a.math and b.math both `export fn pick`), and two + * referencing modules each `import .math` aliasing `math`, every + * `math.pick()` call must mangle to the package its OWN module imported — + * not to whichever `use` the file-global hint collected first. + * + * Pre-#40 the hint (use_hint / usehint) was file-global first-match: both + * modules' `math.pick()` resolved to the same package, so one call landed + * the wrong body and returned the wrong value — IDENTICALLY on both stages + * (a byte-id-green #263-class miscompile). Gate-visible: each module's call + * has a distinct expected value, so a mis-route changes the runtime exit. + * + * row | proves + * -----------------+-------------------------------------------------- + * leaf_collision | two same-leaf exported fns; each module's qualified + * | call mangles to its OWN import (111 / 222) -> 0 + * collision_diff | same, with mismatched signatures — the checker's + * | curmod-preferenced resolve agrees with the hint + * | (pick(int) vs pick()) -> 0 + * + * Plus a byte-id gate: cstage.s == wwstage.s on the leaf_collision unit + * (rule-10). + */ +#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 file { const char *path; const char *content; }; + +struct row { + const char *label; + struct file files[6]; /* {NULL,NULL}-terminated package files */ + const char *root; /* root main.ww content */ + int want_exit; +}; + +static const struct row rows[] = { + { "leaf_collision", + { { "a/math/math.ww", + "package math;\n" + "export fn pick() int = { return 111; };\n" }, + { "b/math/math.ww", + "package math;\n" + "export fn pick() int = { return 222; };\n" }, + { "one/one.ww", + "package one;\n" + "import a.math;\n" + "export fn getone() int = { return math.pick(); };\n" }, + { "two/two.ww", + "package two;\n" + "import b.math;\n" + "export fn gettwo() int = { return math.pick(); };\n" }, + { NULL, NULL } }, + "package main;\n" + "import one;\n" + "import two;\n" + "export fn main() int = {\n" + " if (one.getone() != 111) { return 1; };\n" + " if (two.gettwo() != 222) { return 2; };\n" + " return 0;\n" + "};\n", + 0 }, + + { "collision_diff", + { { "a/math/math.ww", + "package math;\n" + "export fn pick(x: int) int = { return x + 100; };\n" }, + { "b/math/math.ww", + "package math;\n" + "export fn pick() int = { return 222; };\n" }, + { "one/one.ww", + "package one;\n" + "import a.math;\n" + "export fn getone() int = { return math.pick(11); };\n" }, + { "two/two.ww", + "package two;\n" + "import b.math;\n" + "export fn gettwo() int = { return math.pick(); };\n" }, + { NULL, NULL } }, + "package main;\n" + "import one;\n" + "import two;\n" + "export fn main() int = {\n" + " if (one.getone() != 111) { return 1; };\n" + " if (two.gettwo() != 222) { return 2; };\n" + " return 0;\n" + "};\n", + 0 }, +}; + +/* write_file — create `dir/rel` (mkdir -p its parents) with `content`. */ +static int +write_file(const char *dir, const char *rel, const char *content) +{ + char path[512], cmd[1024]; + snprintf(path, sizeof path, "%s/%s", dir, rel); + char parent[512]; + snprintf(parent, sizeof parent, "%s", path); + char *slash = strrchr(parent, '/'); + if (slash) { + *slash = '\0'; + snprintf(cmd, sizeof cmd, "mkdir -p '%s'", parent); + if (runwait(cmd) != 0) return -1; + } + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(content, f); + fclose(f); + return 0; +} + +/* layout — lay out the row's package tree + root under `dir`. */ +static int +layout(const char *dir, const struct row *r) +{ + char cmd[1024]; + snprintf(cmd, sizeof cmd, "rm -rf '%s' && mkdir -p '%s'", dir, dir); + if (runwait(cmd) != 0) return -1; + for (int k = 0; r->files[k].path; k++) + if (write_file(dir, r->files[k].path, r->files[k].content) != 0) + return -1; + return write_file(dir, "main.ww", r->root); +} + +/* run_build — build+run the row's root via `driver`; return the binary's + * exit code, or -1 on a build failure. */ +static int +run_build(const char *driver, const struct row *r, int i) +{ + char dir[64], cmd[2048]; + snprintf(dir, sizeof dir, "/tmp/m1usehint_%d_%d", getpid(), i); + if (layout(dir, r) != 0) return -2; + + snprintf(cmd, sizeof cmd, "cd '%s' && %s build main.ww 2>/dev/null", + dir, driver); + int brc = runwait(cmd); + + int got = -1; + if (brc == 0) { + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/main", dir); + got = runwait(outbin); + } + snprintf(cmd, sizeof cmd, "rm -rf '%s'", dir); + runwait(cmd); + return brc == 0 ? got : -1; +} + +/* byteid — build the leaf_collision unit with both drivers and cmp the + * emitted combined .s (rule-10). Returns 0 on byte-identical, nonzero else. */ +static int +byteid(const char *cdrv, const char *wdrv, const struct row *r) +{ + char dir[64], cmd[2048]; + char cs[96], ws[96]; + snprintf(dir, sizeof dir, "/tmp/m1usehint_bid_%d", getpid()); + if (layout(dir, r) != 0) return -1; + snprintf(cs, sizeof cs, "/tmp/m1usehint_c_%d.s", getpid()); + snprintf(ws, sizeof ws, "/tmp/m1usehint_w_%d.s", getpid()); + + int rc = -1; + snprintf(cmd, sizeof cmd, "cd '%s' && %s build -o '/tmp/m1usehint_c_%d' " + "main.ww 2>/dev/null", dir, cdrv, getpid()); + int cb = runwait(cmd); + snprintf(cmd, sizeof cmd, "cd '%s' && %s build -o '/tmp/m1usehint_w_%d' " + "main.ww 2>/dev/null", dir, wdrv, getpid()); + int wb = runwait(cmd); + if (cb == 0 && wb == 0) { + snprintf(cmd, sizeof cmd, "cmp -s '%s' '%s'", cs, ws); + rc = runwait(cmd); + } else { + fprintf(stderr, "m1usehint_run: byte-id build failed " + "(cstage=%d wwstage=%d)\n", cb, wb); + } + snprintf(cmd, sizeof cmd, "rm -rf '%s' '/tmp/m1usehint_c_%d'* " + "'/tmp/m1usehint_w_%d'*", dir, getpid(), getpid()); + runwait(cmd); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + 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[1024], wdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *drv; 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].drv, X_OK) != 0) { + fprintf(stderr, "m1usehint_run: skip %s (no %s)\n", + drivers[d].name, drivers[d].drv); + continue; + } + for (int i = 0; i < n; i++) { + total++; + int got = run_build(drivers[d].drv, &rows[i], i); + if (got != rows[i].want_exit) { + fprintf(stderr, "m1usehint_run[%s][%s]: exit=%d " + "want=%d\n", drivers[d].name, rows[i].label, + got, rows[i].want_exit); + fail++; + } + } + } + + /* rule-10 byte-id gate on the leaf_collision unit. */ + if (access(wdrv, X_OK) == 0) { + total++; + if (byteid(cdrv, wdrv, &rows[0]) != 0) { + fprintf(stderr, "m1usehint_run: cstage.s != wwstage.s " + "(byte-id break)\n"); + fail++; + } + } + + if (fail) { + fprintf(stderr, "m1usehint_run: %d/%d failed\n", fail, total); + return 1; + } + printf("m1usehint_run: %d/%d ok\n", total, total); + return 0; +}