/* * 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 *cdrv, const char *wdrv, const struct scenario *sc) { (void)bin; /* 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); } /* #94 sep layout: cstage --sep build + run pins runtime; pin * WW_PKGCACHE under the scratch dir so out/.pkgcache is untouched. */ snprintf(cmd, sizeof cmd, "cd %s && WW_PKGCACHE=%s/pkgc_c %s build --sep -I %s -o %s/main %s/main.ww", dir, dir, cdrv, dir, dir, dir); if (runwait(cmd) != 0) { fprintf(stderr, "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): the wwstage driver's --sep * build emits per-package w6c_ww asm; pre-fix the dispatcher diverges. * The root + each imported pkg compile to separate .sepwork/.s; * concat (sorted glob, identical set both stages) for the compare. */ char cs_s[1024], ws_s[1024]; snprintf(cmd, sizeof cmd, "cd %s && WW_PKGCACHE=%s/pkgc_w %s build --sep -I %s -o %s/mainww %s/main.ww", dir, dir, wdrv, dir, dir, dir); if (runwait(cmd) != 0) { fprintf(stderr, "784[%s]: ww_ww build failed\n", sc->label); rc = -1; goto done; } snprintf(cs_s, sizeof cs_s, "%s/all_cs.s", dir); snprintf(ws_s, sizeof ws_s, "%s/all_ww.s", dir); snprintf(cmd, sizeof cmd, "cat %s/main.sepwork/*.s > %s 2>/dev/null", dir, cs_s); if (system(cmd)) {} snprintf(cmd, sizeof cmd, "cat %s/mainww.sepwork/*.s > %s 2>/dev/null", dir, ws_s); if (system(cmd)) {} if (slurp_eq(cs_s, ws_s) != 0) { fprintf(stderr, "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 cdrv[2100], wdrv[2100]; snprintf(cdrv, sizeof cdrv, "%s/ww", bin); snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); if (access(wdrv, X_OK) != 0) { fprintf(stderr, "784: ww_ww missing — cannot run the cs==ww " "byte-id gate (the whole point of this test)\n"); return 1; } int n = (int)(sizeof scenarios / sizeof scenarios[0]); int fail = 0; for (int i = 0; i < n; i++) { if (run_scenario(bin, cdrv, wdrv, &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; }