/* * 726_alias_leaf_collision — sentinel for #27. Pins wwstage's * aliaslookup to a same-module-first leaf walk so a bare `invalid` * inside module M (with `type invalid = !void;`) resolves to M's * underlying void, not to some other module's `type invalid = !i32;` * registered later in c.aliases. * * Pre-fix wwstage's `aliaslookup` (selfhost/cmd/wcc/cgen.ww) walked * c.aliases head-first by leaf name, returning the FIRST match. With * combined-source build orderings where a `type invalid = !i32;` from * lib/strconv sits ahead of utf8's `type invalid = !void;` in the * chain, `localloadop` resolved invalid → !i32 (size 4, signed) and * emitted MOVSXD on a slot the let-decl prologue zero-inited 8B-wide * (typeis8byteprimitive walks to void → MOVQ $0). Silent-correct-by- * zero-init: the upper 4B happened to be zero from the zero-init, so * the 4B sign-extend produced the right value, but the asm diverged * from cstage's MOVQ and 995_self_rebuild broke once any caller of * lib/strings/strings.byteindex (the (str|rune) match-arm shape that * dragged utf8 into the bootstrap closure) bumped the alias-chain * ordering. * * Cstage's `resolve_typename` (cmd/wcc/check.c:65) calls * `scope_lookup_prefer(cur, cur_mod, nm)` — same-module-first then * fallback. The fix mirrors that discipline in wwstage's aliaslookup. * Asserts byte-id between cstage and wwstage on a probe shape that * loses the asm divergence under the fix. */ #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; } struct row { const char *label; const char *src; /* `want` selects which assertion family runs on the emitted .s: * "movq_invalid" — post-zero-init load on beta.yield_more's * invalid-slot must be MOVQ (the collision case). * "movsxd_i32" — local i32 read in promote must be MOVSXD on * BOTH stages. Guards future "fix" attempts * that pessimize localloadop and would break * legitimate signed-narrow loads. */ const char *want; }; /* Module ordering picked so the colliding `type invalid = !i32;` lands * later in c.aliases (head-prepend → at the head), masking module M's * own `type invalid = !void;`. `gamma` brings both modules into scope * via use directives so decl_mod tags both as imports — that gates * scope_define_in_module's per-mod dedup path on cstage's side. */ static const struct row rows[] = { { "void_invalid_under_i32_collision", "package gamma;\n" "import alpha;\n" "import beta;\n" "export fn main() i32 = { return 0; };\n" "package beta;\n" "type more = void;\n" "type invalid = !void;\n" "fn yield_more() (rune | more | invalid) = {\n" " let e: invalid;\n" " return e;\n" "};\n" "package alpha;\n" "export type invalid = !i32;\n", "movq_invalid" }, /* Regression guard: a real signed-narrow `let i: i32 = ...;` read * MUST stay on MOVSXD. Any future "fix" that pessimizes * localloadop to blanket-MOVQ in the name of #27 would silently * regress this — losing sign-extension on i32 → i64 promotion * (also drops the high half of a deref-store-narrowed slot). */ { "i32_local_read_keeps_movsxd", "fn promote(x: i32) i64 = {\n" " let i: i32 = x;\n" " return (i: i64);\n" "};\n", "movsxd_i32" }, }; static int emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) { char src[64], cmd[1024]; snprintf(src, sizeof src, "/tmp/alc_%d_%d.ww", getpid(), i); snprintf(out_s, cap, "/tmp/alc_%d_%d_%s.s", getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); FILE *f = fopen(src, "wb"); if (!f) return -1; fputs(r->src, f); fclose(f); snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); int rc = runwait(cmd); unlink(src); return rc; } static int slurp(const char *path, char *buf, size_t cap) { FILE *f = fopen(path, "rb"); if (!f) return -1; size_t n = fread(buf, 1, cap - 1, f); fclose(f); buf[n] = '\0'; return (int)n; } /* The yield_more body must read its tagged-return payload through an * 8B MOVQ. A MOVSXD on the same slot is the pre-fix silent-correct- * by-zero-init shape. */ static int check_movq_after_zeroinit(const char *spath, const struct row *r, const char *stage) { char buf[1 << 14]; if (slurp(spath, buf, sizeof buf) < 0) { fprintf(stderr, "row[%s][%s]: cannot read %s\n", r->label, stage, spath); return -1; } const char *fn = strstr(buf, "TEXT beta.yield_more"); if (!fn) { fprintf(stderr, "row[%s][%s]: no TEXT beta.yield_more in %s\n", r->label, stage, spath); return -1; } const char *zi = strstr(fn, "MOVQ\t$0, "); if (!zi) { fprintf(stderr, "row[%s][%s]: no zero-init store\n", r->label, stage); return -1; } const char *bad = strstr(zi, "MOVSXD\t"); const char *good = strstr(zi, "\n\tMOVQ\t-"); const char *ret = strstr(zi, "RET"); if (bad && (ret == NULL || bad < ret)) { fprintf(stderr, "row[%s][%s]: post-zero-init slot read uses MOVSXD\n", r->label, stage); return -1; } if (!good || (ret && good > ret)) { fprintf(stderr, "row[%s][%s]: post-zero-init slot read missing MOVQ\n", r->label, stage); return -1; } return 0; } /* Real i32 local read MUST stay MOVSXD. Any MOVQ load of `i` (the * narrow signed local) inside promote means a later "fix" pessimized * localloadop and silently dropped sign-extension. */ static int check_movsxd_kept(const char *spath, const struct row *r, const char *stage) { char buf[1 << 14]; if (slurp(spath, buf, sizeof buf) < 0) { fprintf(stderr, "row[%s][%s]: cannot read %s\n", r->label, stage, spath); return -1; } const char *fn = strstr(buf, "TEXT promote"); if (!fn) { fprintf(stderr, "row[%s][%s]: no TEXT promote in %s\n", r->label, stage, spath); return -1; } const char *ret = strstr(fn, "RET"); const char *bp = fn; int sxd = 0; for (;;) { const char *p = strstr(bp, "MOVSXD\t-"); if (!p || (ret && p > ret)) break; sxd++; bp = p + 1; } if (sxd < 2) { fprintf(stderr, "row[%s][%s]: expected MOVSXD on i32 local reads, found %d\n", r->label, stage, sxd); return -1; } return 0; } int main(void) { const char *bin = getenv("BIN"); if (!bin) bin = "out/bin"; char absbin[512]; if (bin[0] != '/') { char cwd[256]; if (getcwd(cwd, sizeof cwd) == NULL) return 1; snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); bin = absbin; } char w6c[640], w6c_ww[640]; snprintf(w6c, sizeof w6c, "%s/w6c", bin); snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); int have_ww = (access(w6c_ww, X_OK) == 0); int n = (int)(sizeof rows / sizeof rows[0]); int total = 0, fail = 0; for (int i = 0; i < n; i++) { char cs_path[128], ws_path[128]; if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { fprintf(stderr, "alias_leaf_collision[cstage][%s]: w6c failed\n", rows[i].label); fail++; total++; continue; } total++; int rc; if (strcmp(rows[i].want, "movsxd_i32") == 0) { rc = check_movsxd_kept(cs_path, &rows[i], "cstage"); } else { rc = check_movq_after_zeroinit(cs_path, &rows[i], "cstage"); } if (rc != 0) fail++; if (!have_ww) { unlink(cs_path); continue; } if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { fprintf(stderr, "alias_leaf_collision[wwstage][%s]: w6c_ww failed\n", rows[i].label); fail++; total++; unlink(cs_path); continue; } total++; if (strcmp(rows[i].want, "movsxd_i32") == 0) { rc = check_movsxd_kept(ws_path, &rows[i], "wwstage"); } else { rc = check_movq_after_zeroinit(ws_path, &rows[i], "wwstage"); } if (rc != 0) fail++; /* byte-id between stages on the same input — the principled * sentinel for the polarity catalog. */ total++; char cmd[512]; snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); if (runwait(cmd) != 0) { fprintf(stderr, "alias_leaf_collision[%s]: cstage vs wwstage asm differs\n", rows[i].label); fail++; } unlink(cs_path); unlink(ws_path); } if (fail) { fprintf(stderr, "alias_leaf_collision: %d/%d fixtures failed\n", fail, total); return 1; } printf("alias_leaf_collision: %d/%d ok\n", total, total); return 0; }