diff --git a/Makefile b/Makefile index d65fe49b..dcb354dd 100644 --- a/Makefile +++ b/Makefile @@ -254,6 +254,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_composite_call_arg_run \ $(BIN)/test_letdecl_zeroinit \ $(BIN)/test_nested_if_labels \ + $(BIN)/test_alias_leaf_collision \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -577,6 +578,10 @@ $(BIN)/test_nested_if_labels: test/wcc/725_nested_if_labels.c \ $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_alias_leaf_collision: test/wcc/726_alias_leaf_collision.c \ + $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_use_promote_alias: test/wcc/699_use_promote_alias.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 5aad623d..9dfa8b81 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -18206,10 +18206,23 @@ fn collectaliases(c: *cgen, file: *node) void = { }; fn aliaslookup(c: *cgen, name: str) *node = { + // Same-module first, then any. Mirrors cstage's scope_lookup_prefer + // (cmd/wcc/check.c:65); without the prefer pass a bare `invalid` + // in module M with `type invalid = !void;` can collapse onto a + // strconv-style `type invalid = !i32;` registered earlier in + // c.aliases (head-first walk). The leaf-collision then drives a + // narrow MOVSXD load of a slot the let-decl zero-inits 8B-wide + // (task #27 silent-correct-by-zero-init). let a: *aliasent = c.aliases; for (a != nil) { - let an: str = a.aname; - if (streq(an, name)) { return a.target; }; + if (streq(a.aname, name)) { + if (streq(a.amod, c.curmod)) { return a.target; }; + }; + a = a.aanext; + }; + a = c.aliases; + for (a != nil) { + if (streq(a.aname, name)) { return a.target; }; a = a.aanext; }; // Module-qualified form: `pkg.alias` → match the leaf name diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 15e83f47..10df0b91 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -73,10 +73,23 @@ fn collectaliases(c: *cgen, file: *node) void = { }; fn aliaslookup(c: *cgen, name: str) *node = { + // Same-module first, then any. Mirrors cstage's scope_lookup_prefer + // (cmd/wcc/check.c:65); without the prefer pass a bare `invalid` + // in module M with `type invalid = !void;` can collapse onto a + // strconv-style `type invalid = !i32;` registered earlier in + // c.aliases (head-first walk). The leaf-collision then drives a + // narrow MOVSXD load of a slot the let-decl zero-inits 8B-wide + // (task #27 silent-correct-by-zero-init). let a: *aliasent = c.aliases; for (a != nil) { - let an: str = a.aname; - if (streq(an, name)) { return a.target; }; + if (streq(a.aname, name)) { + if (streq(a.amod, c.curmod)) { return a.target; }; + }; + a = a.aanext; + }; + a = c.aliases; + for (a != nil) { + if (streq(a.aname, name)) { return a.target; }; a = a.aanext; }; // Module-qualified form: `pkg.alias` → match the leaf name diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 82614b22..9d659204 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -18206,10 +18206,23 @@ fn collectaliases(c: *cgen, file: *node) void = { }; fn aliaslookup(c: *cgen, name: str) *node = { + // Same-module first, then any. Mirrors cstage's scope_lookup_prefer + // (cmd/wcc/check.c:65); without the prefer pass a bare `invalid` + // in module M with `type invalid = !void;` can collapse onto a + // strconv-style `type invalid = !i32;` registered earlier in + // c.aliases (head-first walk). The leaf-collision then drives a + // narrow MOVSXD load of a slot the let-decl zero-inits 8B-wide + // (task #27 silent-correct-by-zero-init). let a: *aliasent = c.aliases; for (a != nil) { - let an: str = a.aname; - if (streq(an, name)) { return a.target; }; + if (streq(a.aname, name)) { + if (streq(a.amod, c.curmod)) { return a.target; }; + }; + a = a.aanext; + }; + a = c.aliases; + for (a != nil) { + if (streq(a.aname, name)) { return a.target; }; a = a.aanext; }; // Module-qualified form: `pkg.alias` → match the leaf name diff --git a/test/wcc/726_alias_leaf_collision.c b/test/wcc/726_alias_leaf_collision.c new file mode 100644 index 00000000..ccd1616d --- /dev/null +++ b/test/wcc/726_alias_leaf_collision.c @@ -0,0 +1,277 @@ +/* + * 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", + "// MODULE: gamma\n" + "use alpha;\n" + "use beta;\n" + "export fn main() i32 = { return 0; };\n" + "// MODULE: 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" + "// MODULE: 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; +}