Files
ww/test/wcc/797_xmod_struct_field_layout_collide_run.c
Hojun-Cho a95a7a316b test/wcc: carrier ownership repair and driver-contract adaptation
Every surviving carrier now owns its artifacts: checked mkdir/mkdtemp/
fopen acquisition, one all-exit cleanup funnel per carrier, ENOENT-
tolerant checked unlinks, exact-path deletion (rm -rf only for an
owned pid-keyed dir or a .sepwork beneath one), and cleanup failure
fails a passing carrier without overwriting its diagnostic. In the
same pass the carriers adapt to the driver contract this branch lands:
--sep and WW_PKGCACHE are gone, -S and the /tmp/ww_run_<pid> scratch
contract are asserted, and rows whose runtime or reject coverage moved
to test/wcc/data fixtures or test/lang @test owners are trimmed to the
byte/artifact/diagnostic observations only they can make.

Repair and adaptation ride together because most files interleave both
in the same hunks; splitting would manufacture intermediate carrier
states that never existed and cannot run against either driver.
2026-08-07 23:21:04 +09:00

453 lines
16 KiB
C

/*
* 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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
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;
}