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.
349 lines
10 KiB
C
349 lines
10 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 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;
|
|
}
|