/* * 797_xmod_struct_field_layout_collide_run — project #31 (RULING R2, * commit-1: the OFFSET/SIZE align-up arms) runtime + byte-id net. The * direct continuation of 793 (#21): #21 type-keyed the THREE caller-side * sites the 793 collision drove (push/recv/field-read); #31 closes the * field-LAYOUT *receiver* arms #21 left name-keyed. Commit-1 covers the * offset/size half: * * - R1 (cgenexpr cgdot, local *struct READ): `p.f` where p is an * inferred-let *struct resolved the field OFFSET via * structlookupchain(inner) → the FOREIGN same-leaf struct's layout * (wrong offset + wrong load-op). Now tichase(dotlhs.type_)->.sub * ->fields (the #21 R0 tfield template), via the shared read * choke-point cgptrfieldloadtf. * - C1 (cgenstmt cgletbody, let-COPY size): `let p2 = p1` sized the * struct memcpy run via structlookup(tn.str) → the foreign 24B * struct, OVER-reading a 16B source + OVER-writing p2's slot (a * SILENT OOB whose *value* coincidentally round-trips — the asm * diverges, cs!=ww). Now structabisizetn(rhs.type_) (the COPY * source's stamped tinfo). * - A1/A2 (cgenexpr cgun, addr-of `&p.f` / `&o.f`): the LEAQ field * offset came from structlookupchain → wrong offset. Now * tichase(opnd.lhs.type_) (->.sub for the *struct A1 arm). * * cstage is correct at all of them — it keys on type_chase_named(base-> * type)->fields / lu->size (the checker-STAMPED type), never a name * re-lookup. #31 aligns wwstage UP (ww-only change; cstage cgen.c is the * untouched oracle). * * GATE-BLIND in the bootstrap (same class as 793/784): the trigger needs * a SECOND module contributing a same-leaf struct of a DIFFERENT size so * the name-keyed lookup mis-resolves. The reddening surface is INFERRED * LOCALS (a global struct decl is always explicitly qualified, so its * qualifier resolves correctly — non-reddenable; the global value/ptr * arms R2/R3/A3/W3/W4a are converted-for-construction and ride the * corpus byte-id, not pinned here). * * row | m1 (local) | m2 (foreign) | arm | exit * ----------------+-------------------+--------------+-----+----- * ptrread_t16 | pair{u64,u16} 16B | pair 24B | R1 | 107 * letcopy_t16 | pair{u64,u16} 16B | pair 24B | C1 | 107 (value coincides; byte-id is the net) * addrptr_pq | pq{u64,u64} 16B | pq 24B | A1 | 10 * addrval_pq | pq{u64,u64} 16B | pq 24B | A2 | 10 * ptrwrite_t16 | pair{u64,u16} 16B | pair 24B | W1 | 109 (commit-2 STORE) * valwrite_t16 | pair{u64,u16} 16B | pair 24B | W2 | 109 (commit-2 STORE) * nestfill_box | box{tag,ir:nst} 32B | box 24B | W2 | 66 (commit-2 cgstructlitfilltn nested recursion) * * Commit-2 (RULING R2 / Opt-2) converts the W1/W2/W5/W4b store-loop arms * off the name-keyed receiver walk to a tinfo-native fill (cgstructlitfilltn * + sretretsizetn), closing the in-loop #32 nested struct-receive/structlit/ * ident sub-arms by construction. The two scalar WRITE rows STORE through the * mis-resolved offset: at commit-1 (R1 read already fixed) wwstage stored at * the FOREIGN offset 16 while reading off 8 -> 100+7=107 WRONG vs cstage 109. * They reach only the scalar storeopsz arm, NOT cgstructlitfilltn; the * nestfill_box row is the cgstructlitfilltn fill + nested-recursion value pin * (a struct-literal stored into a struct-typed field, whose own field is a * nested struct literal) — the riskiest new helper, which no bootstrap * construct exercises. * * Each row reddens under an INDEPENDENT revert of its arm (verified * impl-side: R1 ww=101, C1 cs!=ww OOB, A1/A2 ww=8, W1/W2 ww=107 cs!=ww). * cstage `ww build` + run pins runtime; the wwstage binary is run too * (the bug WAS a wrong wwstage runtime value / OOB); raw cs.s vs ww.s over * the driver-produced per-package asm pins rule-10 byte-id. * * NOTE the addr rows use an all-u64 `pq` (offset-only collision): the * canonical {u64,u16} field would, after `&p.lo`, force a `*q = v:u16` * narrow deref-STORE which wwstage currently emits as MOVQ where cstage * emits MOVW — a SEPARATE pre-existing wwstage narrow-deref-store-width * divergence (NOT #31; surfaced + filed by impl-31). An all-u64 store is * MOVQ in both stages, so the row isolates the #31 offset fix. */ #include #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; } if (ferror(fa) || ferror(fb)) rc = -1; if (fclose(fa) != 0) rc = -1; if (fclose(fb) != 0) rc = -1; 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; }; /* m2 contributes 24B same-leaf `pair` and `pq` — the name-keyed lookup's * any-module fallback mis-resolves m1's 16B versions to these. */ static const char m2_src[] = "package m2;\n" "export type pair = struct { hi: u64, mid: u64, lo: u64 };\n" "export type pq = struct { a: u64, b: u64, v: u64 };\n" "export fn use2(p: pair) i32 = { return (p.hi + p.mid + p.lo): i32; };\n"; /* m1: 16B `pair` (sub-8 tail) + 16B all-u64 `pq` (offset-only). */ static const char m1_src[] = "package m1;\n" "export type pair = struct { hi: u64, lo: u16 };\n" "export type pq = struct { a: u64, v: u64 };\n" "let g: pair = pair { hi = 100: u64, lo = 7: u16 };\n" "let gq: pq = pq { a = 1: u64, v = 7: u64 };\n" "export fn mk() pair = { return pair { hi = 100: u64, lo = 7: u16 }; };\n" "export fn mkp() *pair = { return &g; };\n" "export fn mkq() pq = { return pq { a = 1: u64, v = 7: u64 }; };\n" "export fn mkpq() *pq = { return &gq; };\n"; /* ---- ptrread_t16 (R1): inferred-let *struct field read ------------- */ static const struct file ptrread_files[] = { { "m1.ww", m1_src }, { "m2.ww", m2_src }, { "main.ww", "package main;\n" "import m1;\n" "import m2;\n" "fn main() i32 = {\n" " let dummy: m2.pair;\n" " dummy.hi = 0: u64;\n" " let p = m1.mkp();\n" " return (p.hi + (p.lo: u64)): i32;\n" "};\n" }, { NULL, NULL } }; /* ---- letcopy_t16 (C1): inferred-let local-to-local struct copy ----- */ static const struct file letcopy_files[] = { { "m1.ww", m1_src }, { "m2.ww", m2_src }, { "main.ww", "package main;\n" "import m1;\n" "import m2;\n" "fn main() i32 = {\n" " let dummy: m2.pair;\n" " dummy.hi = 0: u64;\n" " let p1 = m1.mk();\n" " let p2 = p1;\n" " return (p2.hi + (p2.lo: u64)): i32;\n" "};\n" }, { NULL, NULL } }; /* ---- addrptr_pq (A1): &p.f over an inferred-let *struct ------------ */ static const struct file addrptr_files[] = { { "m1.ww", m1_src }, { "m2.ww", m2_src }, { "main.ww", "package main;\n" "import m1;\n" "import m2;\n" "fn main() i32 = {\n" " let dummy: m2.pq;\n" " dummy.a = 0: u64;\n" " let p = m1.mkpq();\n" " let q = &p.v;\n" " *q = 9: u64;\n" " return (p.a + p.v): i32;\n" "};\n" }, { NULL, NULL } }; /* ---- addrval_pq (A2): &o.f over an inferred-let value struct ------- */ static const struct file addrval_files[] = { { "m1.ww", m1_src }, { "m2.ww", m2_src }, { "main.ww", "package main;\n" "import m1;\n" "import m2;\n" "fn main() i32 = {\n" " let dummy: m2.pq;\n" " dummy.a = 0: u64;\n" " let s = m1.mkq();\n" " let q = &s.v;\n" " *q = 9: u64;\n" " return (s.a + s.v): i32;\n" "};\n" }, { NULL, NULL } }; /* ---- ptrwrite_t16 (W1, commit-2): inferred-let *struct field STORE. * `p.lo = 9: u16` resolved the field OFFSET via the name-keyed lookup -> * the FOREIGN 24B pair (lo @ off 16, u64) -> the store landed at off 16 * (a u64 MOVQ into g's DATA neighbour) while the read (R1, fixed in * commit-1) reads off 8 -> p.lo stays 7 -> 100+7=107 WRONG (cstage 109, * cs!=ww). The {u64,u16} field stores MOVW in both stages (a direct * field store, NOT the addr-of `*q=v:u16` narrow-deref-store divergence * the A1/A2 pq rows dodge), so it isolates the #31 W1 offset/width fix. */ static const struct file ptrwrite_files[] = { { "m1.ww", m1_src }, { "m2.ww", m2_src }, { "main.ww", "package main;\n" "import m1;\n" "import m2;\n" "fn main() i32 = {\n" " let dummy: m2.pair;\n" " dummy.hi = 0: u64;\n" " let p = m1.mkp();\n" " p.lo = 9: u16;\n" " return (p.hi + (p.lo: u64)): i32;\n" "};\n" }, { NULL, NULL } }; /* ---- valwrite_t16 (W2, commit-2): inferred-let value-struct field STORE. * `s.lo = 9: u16` resolved the OFFSET via the FOREIGN 24B pair -> stored * 8 bytes at off 16 (past s's 16B slot) while s.lo (off 8) stays 7 -> * 100+7=107 WRONG (cstage 109). The write-twin #21 missed. */ static const struct file valwrite_files[] = { { "m1.ww", m1_src }, { "m2.ww", m2_src }, { "main.ww", "package main;\n" "import m1;\n" "import m2;\n" "fn main() i32 = {\n" " let dummy: m2.pair;\n" " dummy.hi = 0: u64;\n" " let s = m1.mk();\n" " s.lo = 9: u16;\n" " return (s.hi + (s.lo: u64)): i32;\n" "};\n" }, { NULL, NULL } }; /* ---- nestfill_box (W2 -> cgstructlitfilltn, commit-2): the nested- * recursion VALUE pin for the new fill helper. `s.ir = nst{...}` stores a * struct-LITERAL into a struct-typed field of a value-struct local, * driving cgstructlitfilltn (the W2 N_STRUCTLIT sub-arm, mode 0); the * literal's `dp = deep{...}` field is itself a struct literal, so * cgstructlitfilltn RECURSES — cgstructlitfilltn(tichase(tf.type_)) at * the inner offset (box.ir.dp @ +16). This is the riskiest new code and * is exercised by NO bootstrap construct (the corpus has zero `x.f = * Y{...}` struct-lit field stores) and by NO other 797 row (they all * store scalars/idents/calls, hitting only the storeopsz scalar arm). * Want = i0 + dp.d0 + dp.d1 = 11 + 22 + 33 = 66; a broken recursion * drops dp -> 11, a mis-offset -> a wrong sum. * * COLLISION-FREE by necessity (NOT an oversight): a cross-module * same-leaf box collision CANNOT drive this pin because TWO pre-existing * wwstage CHECKER gaps (orthogonal to #31, which is cgen-only) block the * wwstage readback under collision — (a) the checker mis-resolves a * nested value-struct field read to the FOREIGN field type (`u64 -> nst * not assignable`); (b) a chained `s.ir.dp.d0` read trips `asserttyped: * dot`. So the value is read back via single-dot-through-param helpers * (sumnst/sumdeep), the only nested-read shape wwstage accepts. The pin * still catches every cgstructlitfilltn fill/recursion miscompile (cs == * ww runtime + cs.s == ww.s byte-id); the collision surface for the * SCALAR store stays covered by ptrwrite/valwrite above. */ static const struct file nestfill_files[] = { { "main.ww", "package main;\n" "type deep = struct { d0: u64, d1: u64 };\n" "type nst = struct { i0: u64, dp: deep };\n" "type box = struct { tag: u64, ir: nst };\n" "fn mkbox() box = { return box { tag = 0: u64, ir = nst { i0 = 0: u64, dp = deep { d0 = 0: u64, d1 = 0: u64 } } }; };\n" "fn sumdeep(d: deep) u64 = { return d.d0 + d.d1; };\n" "fn sumnst(n: nst) u64 = { return n.i0 + sumdeep(n.dp); };\n" "fn main() i32 = {\n" " let s = mkbox();\n" " s.ir = nst { i0 = 11: u64, dp = deep { d0 = 22: u64, d1 = 33: u64 } };\n" " return sumnst(s.ir): i32;\n" "};\n" }, { NULL, NULL } }; static const struct scenario scenarios[] = { { "ptrread_t16", ptrread_files, 107 }, { "letcopy_t16", letcopy_files, 107 }, { "addrptr_pq", addrptr_files, 10 }, { "addrval_pq", addrval_files, 10 }, { "ptrwrite_t16", ptrwrite_files, 109 }, { "valwrite_t16", valwrite_files, 109 }, { "nestfill_box", nestfill_files, 66 }, }; static int run_scenario(const char *cdrv, const char *wdrv, const struct scenario *sc) { char dir[] = "/tmp/ww797_XXXXXX"; if (mkdtemp(dir) == NULL) { fprintf(stderr, "797[%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, "797[%s]: write %s\n", sc->label, sc->files[i].name); rc = -1; goto done; } int bad = fputs(sc->files[i].src, f) == EOF; if (fclose(f) != 0) bad = 1; if (bad) { fprintf(stderr, "797[%s]: write %s\n", sc->label, sc->files[i].name); rc = -1; goto done; } } snprintf(cmd, sizeof cmd, "cd %s && %s build -I %s -o %s/main %s/main.ww", dir, cdrv, dir, dir, dir); if (runwait(cmd) != 0) { fprintf(stderr, "797[%s]: cstage build failed\n", sc->label); rc = -1; goto done; } snprintf(path, sizeof path, "%s/main", dir); int gotc = runwait(path); if (gotc != sc->want_exit) { fprintf(stderr, "797[%s]: cstage exit %d, want %d\n", sc->label, gotc, sc->want_exit); rc = -1; } snprintf(cmd, sizeof cmd, "cd %s && %s build -I %s -o %s/mainww %s/main.ww", dir, wdrv, dir, dir, dir); if (runwait(cmd) != 0) { fprintf(stderr, "797[%s]: ww_ww build failed\n", sc->label); rc = -1; goto done; } snprintf(path, sizeof path, "%s/mainww", dir); int gotw = runwait(path); if (gotw != sc->want_exit) { fprintf(stderr, "797[%s]: wwstage exit %d, want %d\n", sc->label, gotw, sc->want_exit); rc = -1; } char cs_s[1024], ws_s[1024]; 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 && test -s %s", dir, cs_s, cs_s); if (runwait(cmd) != 0) { fprintf(stderr, "797[%s]: cstage asm aggregation failed\n", sc->label); rc = -1; goto done; } snprintf(cmd, sizeof cmd, "cat %s/mainww.sepwork/*.s > %s 2>/dev/null && test -s %s", dir, ws_s, ws_s); if (runwait(cmd) != 0) { fprintf(stderr, "797[%s]: wwstage asm aggregation failed\n", sc->label); rc = -1; goto done; } if (slurp_eq(cs_s, ws_s) != 0) { fprintf(stderr, "797[%s]: cs.s/ww.s DIFFER (rule-10 byte-id " "violation — #31 regression)\n", sc->label); rc = -1; } done: snprintf(cmd, sizeof cmd, "rm -rf %s/main.sepwork", dir); if (runwait(cmd) != 0) { fprintf(stderr, "797[%s]: cleanup main.sepwork\n", sc->label); rc = -1; } snprintf(cmd, sizeof cmd, "rm -rf %s/mainww.sepwork", dir); if (runwait(cmd) != 0) { fprintf(stderr, "797[%s]: cleanup mainww.sepwork\n", sc->label); rc = -1; } for (int i = 0; sc->files[i].name; i++) { snprintf(path, sizeof path, "%s/%s", dir, sc->files[i].name); if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "797[%s]: cleanup %s\n", sc->label, sc->files[i].name); rc = -1; } } const char *children[] = { "main", "mainww", "all_cs.s", "all_ww.s", NULL }; for (int i = 0; children[i]; i++) { snprintf(path, sizeof path, "%s/%s", dir, children[i]); if (unlink(path) != 0 && errno != ENOENT) { fprintf(stderr, "797[%s]: cleanup %s\n", sc->label, children[i]); rc = -1; } } if (rmdir(dir) != 0) { fprintf(stderr, "797[%s]: cleanup directory\n", sc->label); rc = -1; } 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, "797: 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(cdrv, wdrv, &scenarios[i]) != 0) fail++; } if (fail) { fprintf(stderr, "797 xmod_struct_field_layout_collide: %d/%d " "scenarios failed\n", fail, n); return 1; } printf("xmod_struct_field_layout_collide: %d/%d ok (cstage+wwstage run + " "cs==ww byte-id)\n", n, n); return 0; }