From 80184a3acfd7a321cdbacae06367ba1c700a93bb Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 30 May 2026 02:00:07 +0900 Subject: [PATCH] wwstage: make the cgdot alias-peel struct-break module-aware (#223) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wwstage cgdot #191 alias-peel loop broke on a name-keyed any-module structlookup, so a receiver whose alias name collides with a struct of the same name in ANOTHER module resolved to the foreign struct and fell through to an undefined `name(SB)` global instead of the field load. The eFinal FLIP renames io's `vstream` -> `stream`, which collides with memio's `stream` struct, so io.read/io.write/io.close's `match (s.reader)` emitted `MOVQ reader(SB), AX` (reader is also a type-alias) -> cs != ww (cstage chases the nominal TY_NAMED.under pointer chain, module-correct). Gate-blind: on master both stages emit the same wrong store so byte-id stays green; the FLIP corpus is the first to put the io-alias/memio-struct collision in one build. Fix (wwstage-only align-down; cstage is the authority and is untouched): make the peel's struct-break MODULE-AWARE — break only on a same-module struct (a genuine struct-value receiver); a same-module alias keeps peeling to its underlying (io.stream -> *vtable -> the pointer field-load arm); a foreign leaf keeps the prior any-module heuristic. New structsamemod / aliassamemod mirror the same-module-first pass already in structlookup / aliaslookup. This is not a naive alias-first reorder (which would reintroduce the mirror collision: a same-module struct plus a foreign same-leaf alias). Peel-only — the direct- struct arm's broader cross-module same-leaf-STRUCT name-keying is filed as #224. #208-family (name-keyed resolution dropping to a wrong global) but in cgen, not the checker; #213 is distinct (cosmetic local-struct-match divergence). test/wcc/784_xmod_alias_struct_collide_run: collision (cross-module alias-vs- struct, same leaf), symmetric (guards the same-module-struct break against a naive reorder), and a no-collision control — branched callee. The discriminating net is cs.s == ww.s (the path is gate-blind and cstage is correct, so byte-id flips when wwstage is fixed); confirmed by source-revert. The FLIP's combined.ww is now cs.s == ww.s byte-identical. --- Makefile | 12 + selfhost/cmd/w6c/main.combined.ww | 59 +++- selfhost/cmd/wcc/cgen.ww | 16 + selfhost/cmd/wcc/cgenexpr.ww | 26 +- selfhost/cmd/wcc/cgenutil.ww | 17 + selfhost/cmd/wwdump/main.combined.ww | 59 +++- test/wcc/784_xmod_alias_struct_collide_run.c | 348 +++++++++++++++++++ 7 files changed, 528 insertions(+), 9 deletions(-) create mode 100644 test/wcc/784_xmod_alias_struct_collide_run.c diff --git a/Makefile b/Makefile index ad84013a..7e7256df 100644 --- a/Makefile +++ b/Makefile @@ -335,6 +335,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_fmt_vstream_compositions_run \ $(BIN)/test_fieldfn_leaf_collide_run \ $(BIN)/test_amp_fn_assign_run \ + $(BIN)/test_xmod_alias_struct_collide_run \ $(BIN)/test_bufio_vstream_run \ $(BIN)/test_log_vstream_run \ $(BIN)/test_use_promote_alias \ @@ -728,6 +729,17 @@ $(BIN)/test_amp_fn_assign_run: test/wcc/783_amp_fn_assign_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #223: a struct-field access through a pointer-ALIAS receiver whose +# leaf collides with another module's same-leaf STRUCT. cstage driver +# build + run for runtime, raw w6c vs w6c_ww .s cmp on the combined.ww +# for rule-10 byte-id (the #223 discriminator). Builds its own 2-module +# fixtures in a private mktemp dir — not a selfhost-driver test. +$(BIN)/test_xmod_alias_struct_collide_run: test/wcc/784_xmod_alias_struct_collide_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/w6c_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_fmt_vstream_mods_run: test/wcc/780_fmt_vstream_mods_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/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index dd7c60db..d1c80acd 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -16142,6 +16142,23 @@ fn structlookup(c: *cgen, name: str) *structinfo = { return nil; }; +// #223: same-module-ONLY struct lookup. structlookup's any-module +// fallback returns a foreign same-leaf struct; the cgdot alias-peel +// needs to break ONLY on a struct that THIS module defines (a genuine +// struct-value receiver), not on a foreign struct that merely shares a +// leaf with a same-module alias (io.stream alias vs memio.stream +// struct). Returns the struct only when it lives in c.curmod. +fn structsamemod(c: *cgen, name: str) *structinfo = { + let s: *structinfo = c.structs; + for (s != nil) { + if (streq(s.sname, name)) { + if (streq(s.smod, c.curmod)) { return s; }; + }; + s = s.sinext; + }; + return nil; +}; + // primsize — size in bytes of a primitive type name (or 0 if not // recognised as a primitive — the caller falls back to other paths). // fldnumidx — parse a tuple field name like "0" / "1" / "12" into an @@ -20129,10 +20146,30 @@ fn cgdot(c: *cgen, n: *node) void = { // builds NAMED chains (project_tinfo_lossy_nominal). Stops // at struct aliases so the existing direct-struct arm below // stays byte-id with pre-fix #22 callers. #191. + // + // #223: the break is MODULE-AWARE. cstage type_chase_named + // follows the resolved NAMED.under pointer (module-correct); + // wwstage re-resolves by name (lossy), so a same-module + // alias whose leaf collides with a FOREIGN struct of the + // same name (io.stream = *vtable vs memio.stream struct) + // would wrongly halt the peel at the foreign struct via + // structlookup's any-module fallback → field load drops to a + // bogus `MOVQ (SB)`. Sibling of #208 (lossy name-keyed + // resolution leaking to a global leaf). Break ONLY on a + // 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). for (tn != nil && tn.kind == nkind.N_TNAME) { - if (structlookup(c, tn.str) != nil) { break; }; - let nx: *node = aliaslookup(c, tn.str); - if (nx == nil) { break; }; + if (structsamemod(c, tn.str) != nil) { break; }; + let nx: *node = aliassamemod(c, tn.str); + if (nx == nil) { + if (structlookup(c, tn.str) != nil) { break; }; + nx = aliaslookup(c, tn.str); + if (nx == nil) { break; }; + }; tn = nx; }; if (tn == nil) { return; }; @@ -28373,6 +28410,22 @@ fn aliaslookup(c: *cgen, name: str) *node = { return nil; }; +// #223: same-module-ONLY alias resolution. aliaslookup's any-module +// fallback can return a foreign same-leaf alias; the alias-peel in +// cgdot needs to know whether THIS module defines the name as an alias +// (so the peel continues) without that cross-module fallback. Returns +// the alias target only when an alias of `name` lives in c.curmod. +fn aliassamemod(c: *cgen, name: str) *node = { + let a: *aliasent = c.aliases; + for (a != nil) { + if (streq(a.aname, name)) { + if (streq(a.amod, c.curmod)) { return a.target; }; + }; + a = a.aanext; + }; + return nil; +}; + // ---- enum registry -------------------------------------------------- // // Mirrors cmd/wcc/check.c's enum resolution at collect time: walk diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 786f66f3..ba4af657 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -139,6 +139,22 @@ fn aliaslookup(c: *cgen, name: str) *node = { return nil; }; +// #223: same-module-ONLY alias resolution. aliaslookup's any-module +// fallback can return a foreign same-leaf alias; the alias-peel in +// cgdot needs to know whether THIS module defines the name as an alias +// (so the peel continues) without that cross-module fallback. Returns +// the alias target only when an alias of `name` lives in c.curmod. +fn aliassamemod(c: *cgen, name: str) *node = { + let a: *aliasent = c.aliases; + for (a != nil) { + if (streq(a.aname, name)) { + if (streq(a.amod, c.curmod)) { return a.target; }; + }; + a = a.aanext; + }; + return nil; +}; + // ---- enum registry -------------------------------------------------- // // Mirrors cmd/wcc/check.c's enum resolution at collect time: walk diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 4e497221..89c81f86 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1766,10 +1766,30 @@ fn cgdot(c: *cgen, n: *node) void = { // builds NAMED chains (project_tinfo_lossy_nominal). Stops // at struct aliases so the existing direct-struct arm below // stays byte-id with pre-fix #22 callers. #191. + // + // #223: the break is MODULE-AWARE. cstage type_chase_named + // follows the resolved NAMED.under pointer (module-correct); + // wwstage re-resolves by name (lossy), so a same-module + // alias whose leaf collides with a FOREIGN struct of the + // same name (io.stream = *vtable vs memio.stream struct) + // would wrongly halt the peel at the foreign struct via + // structlookup's any-module fallback → field load drops to a + // bogus `MOVQ (SB)`. Sibling of #208 (lossy name-keyed + // resolution leaking to a global leaf). Break ONLY on a + // 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). for (tn != nil && tn.kind == nkind.N_TNAME) { - if (structlookup(c, tn.str) != nil) { break; }; - let nx: *node = aliaslookup(c, tn.str); - if (nx == nil) { break; }; + if (structsamemod(c, tn.str) != nil) { break; }; + let nx: *node = aliassamemod(c, tn.str); + if (nx == nil) { + if (structlookup(c, tn.str) != nil) { break; }; + nx = aliaslookup(c, tn.str); + if (nx == nil) { break; }; + }; tn = nx; }; if (tn == nil) { return; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 3cf70309..882933cd 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -1381,6 +1381,23 @@ fn structlookup(c: *cgen, name: str) *structinfo = { return nil; }; +// #223: same-module-ONLY struct lookup. structlookup's any-module +// fallback returns a foreign same-leaf struct; the cgdot alias-peel +// needs to break ONLY on a struct that THIS module defines (a genuine +// struct-value receiver), not on a foreign struct that merely shares a +// leaf with a same-module alias (io.stream alias vs memio.stream +// struct). Returns the struct only when it lives in c.curmod. +fn structsamemod(c: *cgen, name: str) *structinfo = { + let s: *structinfo = c.structs; + for (s != nil) { + if (streq(s.sname, name)) { + if (streq(s.smod, c.curmod)) { return s; }; + }; + s = s.sinext; + }; + return nil; +}; + // primsize — size in bytes of a primitive type name (or 0 if not // recognised as a primitive — the caller falls back to other paths). // fldnumidx — parse a tuple field name like "0" / "1" / "12" into an diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 407d6f5d..c35baf6f 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -16142,6 +16142,23 @@ fn structlookup(c: *cgen, name: str) *structinfo = { return nil; }; +// #223: same-module-ONLY struct lookup. structlookup's any-module +// fallback returns a foreign same-leaf struct; the cgdot alias-peel +// needs to break ONLY on a struct that THIS module defines (a genuine +// struct-value receiver), not on a foreign struct that merely shares a +// leaf with a same-module alias (io.stream alias vs memio.stream +// struct). Returns the struct only when it lives in c.curmod. +fn structsamemod(c: *cgen, name: str) *structinfo = { + let s: *structinfo = c.structs; + for (s != nil) { + if (streq(s.sname, name)) { + if (streq(s.smod, c.curmod)) { return s; }; + }; + s = s.sinext; + }; + return nil; +}; + // primsize — size in bytes of a primitive type name (or 0 if not // recognised as a primitive — the caller falls back to other paths). // fldnumidx — parse a tuple field name like "0" / "1" / "12" into an @@ -20129,10 +20146,30 @@ fn cgdot(c: *cgen, n: *node) void = { // builds NAMED chains (project_tinfo_lossy_nominal). Stops // at struct aliases so the existing direct-struct arm below // stays byte-id with pre-fix #22 callers. #191. + // + // #223: the break is MODULE-AWARE. cstage type_chase_named + // follows the resolved NAMED.under pointer (module-correct); + // wwstage re-resolves by name (lossy), so a same-module + // alias whose leaf collides with a FOREIGN struct of the + // same name (io.stream = *vtable vs memio.stream struct) + // would wrongly halt the peel at the foreign struct via + // structlookup's any-module fallback → field load drops to a + // bogus `MOVQ (SB)`. Sibling of #208 (lossy name-keyed + // resolution leaking to a global leaf). Break ONLY on a + // 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). for (tn != nil && tn.kind == nkind.N_TNAME) { - if (structlookup(c, tn.str) != nil) { break; }; - let nx: *node = aliaslookup(c, tn.str); - if (nx == nil) { break; }; + if (structsamemod(c, tn.str) != nil) { break; }; + let nx: *node = aliassamemod(c, tn.str); + if (nx == nil) { + if (structlookup(c, tn.str) != nil) { break; }; + nx = aliaslookup(c, tn.str); + if (nx == nil) { break; }; + }; tn = nx; }; if (tn == nil) { return; }; @@ -28373,6 +28410,22 @@ fn aliaslookup(c: *cgen, name: str) *node = { return nil; }; +// #223: same-module-ONLY alias resolution. aliaslookup's any-module +// fallback can return a foreign same-leaf alias; the alias-peel in +// cgdot needs to know whether THIS module defines the name as an alias +// (so the peel continues) without that cross-module fallback. Returns +// the alias target only when an alias of `name` lives in c.curmod. +fn aliassamemod(c: *cgen, name: str) *node = { + let a: *aliasent = c.aliases; + for (a != nil) { + if (streq(a.aname, name)) { + if (streq(a.amod, c.curmod)) { return a.target; }; + }; + a = a.aanext; + }; + return nil; +}; + // ---- enum registry -------------------------------------------------- // // Mirrors cmd/wcc/check.c's enum resolution at collect time: walk diff --git a/test/wcc/784_xmod_alias_struct_collide_run.c b/test/wcc/784_xmod_alias_struct_collide_run.c new file mode 100644 index 00000000..b385a759 --- /dev/null +++ b/test/wcc/784_xmod_alias_struct_collide_run.c @@ -0,0 +1,348 @@ +/* + * 784_xmod_alias_struct_collide_run — project #223 runtime + byte-id + * net. A struct-field access through a pointer-ALIAS receiver + * (`s.` where `s` is a `type s = *vtable` alias) miscompiled in + * wwstage when the alias leaf collided with a DIFFERENT module's + * same-leaf STRUCT (the eFinal io.stream = *vtable alias vs the + * memio.stream struct). cgen's #191 alias-peel loop + * (selfhost/cmd/wcc/cgenexpr.ww cgdot) broke on the name-keyed + * structlookup's any-module fallback — it found the FOREIGN struct, + * halted the peel with the receiver still N_TNAME, skipped the + * pointer-to-struct field-load arm, and the field load fell through to + * the SB-global fallback: `MOVQ (SB), AX` (an undefined symbol → + * w6l link-fail / garbage). cstage is correct — type_chase_named + * (cmd/w6c/cgen.c) follows the resolved NAMED.under POINTER chain + * (module-correct), never a name re-lookup. Sibling of #208 (lossy + * name-keyed resolution leaking to a global leaf, but in the CHECKER). + * + * Fix (wwstage-only align-down, cgenexpr.ww:1769 peel loop): the + * struct-break is now MODULE-AWARE — break ONLY on a struct that THIS + * module defines (structsamemod), a same-module ALIAS keeps peeling + * (aliassamemod), and a foreign leaf falls back to the prior + * any-module heuristic. + * + * GATE-BLIND in the bootstrap: the io.stream-alias vs memio.stream- + * struct collision is the only same-leaf alias-vs-struct cross-module + * pair, and it only entered the corpus at the #94 eFinal FLIP (master + * never had it). A single-module synthetic does NOT trigger — the + * collision needs a SECOND module contributing the same-leaf struct + * into the corpus. Hence this 2-module probe. + * + * row | shape | exit | byte-id + * -----------+----------------------------------------+------+-------- + * collision | sa: type s=*vtable + vtable{reader} + | 42 | cs==ww + * | dispatcher `read(x:s) match(x.reader)`| | + * | sb: type s=struct{} (same leaf, NO | | + * | reader field) — the #223 trigger | | + * symmetric | sa: type s=struct{} + value-receiver | 21 | cs==ww + * | `geta(x:s)`; sb: type s=*vtable alias | | + * | — guards the same-module-struct break | | + * | still fires (a naive alias-first | | + * | reorder would mis-peel A's struct) | | + * control | sa: alias+vtable+dispatcher, NO foreign | 30 | cs==ww + * | same-leaf struct — common io-like case| | + * + * The dispatcher uses a BRANCHED callee (void arm → -1, fn-ptr arm → + * v+100) so the field load actually feeds control flow (gate-blind + * discipline). cstage `ww build` + run pins runtime; raw w6c vs w6c_ww + * `.s` cmp on the driver-produced combined.ww pins rule-10 byte-id + * (the discriminating net for #223 — pre-fix the dispatcher diverges + * `MOVQ reader(SB)` vs the pointer-field load). + * + * Filed-not-fixed here: #224 (the broader cross-module same-leaf STRUCT + * name-keying at structlookupchain) — distinct, not FLIP-triggered. + */ +#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; +}; + +/* ---- collision: the #223 bug ------------------------------------- */ +static const struct file collision_files[] = { + { "sa.ww", + "package sa;\n" + "\n" + "export type reader = fn(x: s, v: i32) i32;\n" + "\n" + "export type vtable = struct {\n" + " reader: (*reader | void),\n" + "};\n" + "\n" + "export type s = *vtable;\n" + "\n" + "export fn read(x: s, v: i32) i32 = {\n" + " match (x.reader) {\n" + " case void => return -1;\n" + " case let f: *reader => return (*f)(x, v);\n" + " };\n" + "};\n" + "\n" + "export fn mkvt(f: *reader) vtable = {\n" + " return vtable { reader = f };\n" + "};\n" + "\n" + "export fn mkempty() vtable = {\n" + " return vtable { reader = void };\n" + "};\n" }, + /* sb's `s` is a STRUCT sharing the leaf `s` with sa's alias — the + * name-keyed structlookup any-module fallback used to halt sa.read's + * alias-peel here. No `reader` field, so the mis-resolved field walk + * fell through to `MOVQ reader(SB)`. */ + { "sb.ww", + "package sb;\n" + "\n" + "export type s = struct {\n" + " a: i32,\n" + " b: i32,\n" + "};\n" }, + { "main.ww", + "package main;\n" + "\n" + "import sa;\n" + "import sb;\n" + "\n" + "fn cb(x: sa.s, v: i32) i32 = { return v + 100; };\n" + "\n" + "export fn main() i32 = {\n" + " let vt = sa.mkvt((&cb): *sa.reader);\n" + " let st: sa.s = &vt;\n" + " let r = sa.read(st, 5);\n" + " let vt2 = sa.mkempty();\n" + " let st2: sa.s = &vt2;\n" + " let r2 = sa.read(st2, 5);\n" + " if (r != 105) { return 11; };\n" + " if (r2 != -1) { return 12; };\n" + " return 42;\n" + "};\n" }, + { NULL, NULL } +}; + +/* ---- symmetric: A struct, B alias; receiver is A's struct value -- */ +static const struct file symmetric_files[] = { + { "sa.ww", + "package sa;\n" + "\n" + "export type s = struct {\n" + " a: i32,\n" + " b: i32,\n" + "};\n" + "\n" + "export fn mk(av: i32, bv: i32) s = {\n" + " return s { a = av, b = bv };\n" + "};\n" + "\n" + "export fn geta(x: s) i32 = {\n" + " return x.a;\n" + "};\n" }, + { "sb.ww", + "package sb;\n" + "\n" + "export type vtable = struct {\n" + " q: i32,\n" + "};\n" + "\n" + "export type s = *vtable;\n" }, + { "main.ww", + "package main;\n" + "\n" + "import sa;\n" + "import sb;\n" + "\n" + "export fn main() i32 = {\n" + " let o = sa.mk(21, 8);\n" + " let r = sa.geta(o);\n" + " if (r != 21) { return 11; };\n" + " return 21;\n" + "};\n" }, + { NULL, NULL } +}; + +/* ---- control: same-module alias+vtable, no foreign same-leaf ----- */ +static const struct file control_files[] = { + { "sa.ww", + "package sa;\n" + "\n" + "export type reader = fn(x: s, v: i32) i32;\n" + "\n" + "export type vtable = struct {\n" + " reader: (*reader | void),\n" + "};\n" + "\n" + "export type s = *vtable;\n" + "\n" + "export fn read(x: s, v: i32) i32 = {\n" + " match (x.reader) {\n" + " case void => return -1;\n" + " case let f: *reader => return (*f)(x, v);\n" + " };\n" + "};\n" + "\n" + "export fn mkvt(f: *reader) vtable = {\n" + " return vtable { reader = f };\n" + "};\n" }, + { "main.ww", + "package main;\n" + "\n" + "import sa;\n" + "\n" + "fn cb(x: sa.s, v: i32) i32 = { return v + 100; };\n" + "\n" + "export fn main() i32 = {\n" + " let vt = sa.mkvt((&cb): *sa.reader);\n" + " let st: sa.s = &vt;\n" + " let r = sa.read(st, 5);\n" + " if (r != 105) { return 11; };\n" + " return 30;\n" + "};\n" }, + { NULL, NULL } +}; + +static const struct scenario scenarios[] = { + { "collision", collision_files, 42 }, + { "symmetric", symmetric_files, 21 }, + { "control", control_files, 30 }, +}; + +static int +run_scenario(const char *bin, const char *w6c, const char *w6c_ww, + const struct scenario *sc) +{ + /* Private fixture dir — #215: srcd is this dir (not shared /tmp), + * so the driver's srcd-first import search can't pick up a + * polluting same-name file. mkdtemp gives a unique path. */ + char dir[] = "/tmp/ww784_XXXXXX"; + if (mkdtemp(dir) == NULL) { + fprintf(stderr, "784[%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, "784[%s]: write %s\n", sc->label, + sc->files[i].name); rc = -1; goto done; } + fputs(sc->files[i].src, f); + fclose(f); + } + + /* cstage driver build + run: pins runtime. */ + snprintf(cmd, sizeof cmd, "cd %s && %s/ww build -I %s %s/main.ww", + dir, bin, dir, dir); + if (runwait(cmd) != 0) { + fprintf(stderr, "784[%s]: cstage build failed\n", sc->label); + rc = -1; goto done; + } + snprintf(path, sizeof path, "%s/main", dir); + int got = runwait(path); + if (got != sc->want_exit) { + fprintf(stderr, "784[%s]: cstage exit %d, want %d\n", + sc->label, got, sc->want_exit); + rc = -1; + } + + /* Byte-id net (the #223 discriminator): raw w6c vs w6c_ww on the + * driver-produced combined.ww. Pre-fix the dispatcher diverges. */ + char cs_s[1024], ws_s[1024], comb[1024]; + snprintf(comb, sizeof comb, "%s/main.combined.ww", dir); + snprintf(cs_s, sizeof cs_s, "%s/cs.s", dir); + snprintf(ws_s, sizeof ws_s, "%s/ww.s", dir); + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, cs_s, comb); + if (runwait(cmd) != 0) { + fprintf(stderr, "784[%s]: w6c on combined failed\n", sc->label); + rc = -1; goto done; + } + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c_ww, ws_s, comb); + if (runwait(cmd) != 0) { + fprintf(stderr, "784[%s]: w6c_ww on combined failed\n", + sc->label); + rc = -1; goto done; + } + if (slurp_eq(cs_s, ws_s) != 0) { + fprintf(stderr, "784[%s]: cs.s/ww.s DIFFER (rule-10 byte-id " + "violation — #223 regression)\n", sc->label); + rc = -1; + } + +done: + /* Best-effort cleanup of the private dir. */ + 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 w6c[2100], w6c_ww[2100]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + if (access(w6c_ww, X_OK) != 0) { + fprintf(stderr, "784: w6c_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(bin, w6c, w6c_ww, &scenarios[i]) != 0) + fail++; + } + + if (fail) { + fprintf(stderr, "784 xmod_alias_struct_collide: %d/%d " + "scenarios failed\n", fail, n); + return 1; + } + printf("xmod_alias_struct_collide: %d/%d ok (cstage run + cs==ww " + "byte-id)\n", n, n); + return 0; +}