Files
ww/test/wcc/784_xmod_alias_struct_collide_run.c
Hojun-Cho 939d0938e2 test: re-cite retired-mechanism comments in surviving carriers
The migrate-and-retire audit left the surviving carriers as the only
files still describing the retired combined.ww amalgamator as live:
m2wwi/m3sep headers claimed .wwi was dead code, four byteid carriers
carried dead cleanup of never-written .combined.ww intermediates, and
sepbuild's enumeration mirror plus lib_byteid's completeness scan
still skipped the retired artifact form (the same silent
accommodation just removed from both drivers — dropping it here
surfaced and flushed 39 stale untracked amalgamator outputs across
lib/, which the gates now reject loudly). Remaining mentions are
past-tense history or quoted diagnostics.
2026-08-08 01:34:05 +09:00

394 lines
12 KiB
C

/*
* 784_xmod_alias_struct_collide_run — project #223 runtime + byte-id
* net. A struct-field access through a pointer-ALIAS receiver
* (`s.<field>` 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 <field>(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 self-built two-module unit 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 <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;
};
/* ---- 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; }
int bad = fputs(sc->files[i].src, f) == EOF;
if (fclose(f) != 0) bad = 1;
if (bad) { fprintf(stderr, "784[%s]: write %s\n", sc->label,
sc->files[i].name); rc = -1; goto done; }
}
/* #94 sep layout: cstage build + run pins runtime; pin
* all outputs stay under the scratch dir. */
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, "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
* build emits per-package w6c_ww asm; pre-fix the dispatcher diverges.
* The root + each imported pkg compile to separate <stem>.sepwork/<pkg>.s;
* concat (sorted glob, identical set both stages) for the compare. */
char cs_s[1024], ws_s[1024];
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, "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 && test -s %s",
dir, cs_s, cs_s);
if (runwait(cmd) != 0) {
fprintf(stderr, "784[%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, "784[%s]: wwstage asm aggregation 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:
snprintf(cmd, sizeof cmd, "rm -rf %s/main.sepwork", dir);
if (runwait(cmd) != 0) { fprintf(stderr, "784[%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, "784[%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, "784[%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, "784[%s]: cleanup %s\n", sc->label,
children[i]);
rc = -1;
}
}
if (rmdir(dir) != 0) {
fprintf(stderr, "784[%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, "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;
}