wwstage: make the cgdot alias-peel struct-break module-aware (#223)

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.
This commit is contained in:
2026-05-30 02:00:07 +09:00
parent 1175711021
commit 80184a3acf
7 changed files with 528 additions and 9 deletions

View File

@@ -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.<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 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 <stdio.h>
#include <stdlib.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;
}
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;
}