From d6052e08296175051e9e213a951858525e3b5bd8 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 12 Jun 2026 00:21:16 +0900 Subject: [PATCH] wcc/ww: nodefnptr keys the bare-ident arm on the stamped type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nodefnptr matched bare idents by NAME against the fn table, so a global var colliding with a fn leaf classified as a fn pointer — wwstage silently built what cstage rejects at link (review finding #14). Key on the stamped type; the #124 &mod.fn arm is preserved. 989_fnptrcollide_run pins both stages reject (red 1/2 pre-fix: wwstage built rc=7). --- Makefile | 11 +++ selfhost/cmd/w6c/main.combined.ww | 16 +++- selfhost/cmd/wcc/cgen.ww | 16 +++- selfhost/cmd/wwdump/main.combined.ww | 16 +++- test/wcc/989_fnptrcollide_run.c | 131 +++++++++++++++++++++++++++ 5 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 test/wcc/989_fnptrcollide_run.c diff --git a/Makefile b/Makefile index 62ef7685..35a55411 100644 --- a/Makefile +++ b/Makefile @@ -253,6 +253,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tupfieldsize_run \ $(BIN)/test_gunsigned_run \ $(BIN)/test_taggedidx_run \ + $(BIN)/test_fnptrcollide_run \ $(BIN)/test_arr_ptr_global \ $(BIN)/test_def_arr_infer_len \ $(BIN)/test_def_arr_len \ @@ -689,6 +690,16 @@ $(BIN)/test_taggedidx_run: test/wcc/989_taggedidx_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_fnptrcollide_run (F7-c7, #14): a value ident whose leaf collides with +# a fn name must not be mis-folded into the fn's TEXT reloc. Build-must-fail +# on BOTH driver twins (rule-10); wwstage gated. See the test header. +$(BIN)/test_fnptrcollide_run: test/wcc/989_fnptrcollide_run.c \ + $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_let_global: test/wcc/630_let_global.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index c8b6764f..4f3e649f 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -42250,8 +42250,20 @@ fn nodefnptr(c: *cgen, ev: *node) bool = { return true; }; if (opnd.kind != nkind.N_IDENT) { return false; }; - if (fnretlookup(c, opnd.str) == nil) { return false; }; - return true; + // #14 (F7-c7): type-keyed, mirroring cstage node_fnptr_sym + // (type_chase_named(opnd->type)->kind == TY_FN, cmd/w6c/cgen.c:15542- + // 15543). The prior name-keyed `fnretlookup(opnd.str)` matched a fn + // LEAF NAME even when the operand actually resolved to a same-named + // global/local VALUE — so `&g` for an `*i64` global `g` colliding with + // a fn `g` (e.g. a `mod.f` fn vs a `f` global) baked the fn's TEXT addr + // into the scalar slot (ww runs rc=42; cs fails loud at w6l). Reading + // the stamped operand type distinguishes the bare fn rvalue (TY_FN, the + // #34 fn-rvalue stamp) from a value ident, closing the leaf-name + // collision by construction. (F12 name-keyed overlap noted in the F7 + // spec — same predicate-to-stamp shape; fixed once here.) + let ou: *tinfo = tichase(opnd.type_: *tinfo); + if (ou == nil) { return false; }; + return ou.kind == tykind.TY_FN; }; // tuplerowfoldable — validate every cast-peeled element of `rhs` (an diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index df06986c..1b902a99 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -2577,8 +2577,20 @@ fn nodefnptr(c: *cgen, ev: *node) bool = { return true; }; if (opnd.kind != nkind.N_IDENT) { return false; }; - if (fnretlookup(c, opnd.str) == nil) { return false; }; - return true; + // #14 (F7-c7): type-keyed, mirroring cstage node_fnptr_sym + // (type_chase_named(opnd->type)->kind == TY_FN, cmd/w6c/cgen.c:15542- + // 15543). The prior name-keyed `fnretlookup(opnd.str)` matched a fn + // LEAF NAME even when the operand actually resolved to a same-named + // global/local VALUE — so `&g` for an `*i64` global `g` colliding with + // a fn `g` (e.g. a `mod.f` fn vs a `f` global) baked the fn's TEXT addr + // into the scalar slot (ww runs rc=42; cs fails loud at w6l). Reading + // the stamped operand type distinguishes the bare fn rvalue (TY_FN, the + // #34 fn-rvalue stamp) from a value ident, closing the leaf-name + // collision by construction. (F12 name-keyed overlap noted in the F7 + // spec — same predicate-to-stamp shape; fixed once here.) + let ou: *tinfo = tichase(opnd.type_: *tinfo); + if (ou == nil) { return false; }; + return ou.kind == tykind.TY_FN; }; // tuplerowfoldable — validate every cast-peeled element of `rhs` (an diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index ea84cff5..a358d4db 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -42250,8 +42250,20 @@ fn nodefnptr(c: *cgen, ev: *node) bool = { return true; }; if (opnd.kind != nkind.N_IDENT) { return false; }; - if (fnretlookup(c, opnd.str) == nil) { return false; }; - return true; + // #14 (F7-c7): type-keyed, mirroring cstage node_fnptr_sym + // (type_chase_named(opnd->type)->kind == TY_FN, cmd/w6c/cgen.c:15542- + // 15543). The prior name-keyed `fnretlookup(opnd.str)` matched a fn + // LEAF NAME even when the operand actually resolved to a same-named + // global/local VALUE — so `&g` for an `*i64` global `g` colliding with + // a fn `g` (e.g. a `mod.f` fn vs a `f` global) baked the fn's TEXT addr + // into the scalar slot (ww runs rc=42; cs fails loud at w6l). Reading + // the stamped operand type distinguishes the bare fn rvalue (TY_FN, the + // #34 fn-rvalue stamp) from a value ident, closing the leaf-name + // collision by construction. (F12 name-keyed overlap noted in the F7 + // spec — same predicate-to-stamp shape; fixed once here.) + let ou: *tinfo = tichase(opnd.type_: *tinfo); + if (ou == nil) { return false; }; + return ou.kind == tykind.TY_FN; }; // tuplerowfoldable — validate every cast-peeled element of `rhs` (an diff --git a/test/wcc/989_fnptrcollide_run.c b/test/wcc/989_fnptrcollide_run.c new file mode 100644 index 00000000..f20934bf --- /dev/null +++ b/test/wcc/989_fnptrcollide_run.c @@ -0,0 +1,131 @@ +/* + * 989_fnptrcollide_run — F7-c7 (#14): nodefnptr must be type-keyed, not + * name-keyed, so a value ident whose LEAF collides with a fn name is not + * mis-folded into the fn's TEXT reloc. + * + * THE BUG (cat-A silent miscompile, gate-blind): nodefnptr + * (selfhost/cmd/wcc/cgen.ww) decided whether `&x` (in a static-init / DATAR + * fold context) was the address-of a top-level fn by NAME — `fnretlookup( + * c, x.str) != nil`. So `&slot` for a data global `slot: i64` that shares a + * leaf with a fn `slot` (e.g. an imported `bar.slot`) matched the fn and + * folded into a DATAR reloc to the fn's TEXT symbol instead of the data + * symbol. cstage is type-keyed (node_fnptr_sym: type_chase_named(opnd-> + * type)->kind == TY_FN, cmd/w6c/cgen.c:15542) — `slot`'s stamped type is + * i64, not TY_FN, so it does NOT fold the reloc and the build errors loud. + * Pre-fix wwstage silently produced a binary (the cat-A divergence: cs + * loud-fails, ww builds). THE FIX: read the stamped operand type + * (opnd.type_ chased == TY_FN), aligning wwstage UP — a non-fn operand can + * never fold to a fn reloc, closing the leaf-name collision by construction. + * + * This shape is gate-blind (the corpus never collides a data-global leaf + * with a fn name on the &-fold path); only this row catches it. The + * conversion's neutrality on the corpus's REAL &fn static-inits (the + * #117/#119 reloc machinery) is proven separately by the c7 self-compile + * byte-id bind (B1/B2 zero-move) — see /tmp/implf7_result.txt. + * + * Assertion: the construction must FAIL TO BUILD on BOTH stages (cstage + * already rejects; wwstage now rejects too — rule-10 align-up). Pre-fix + * wwstage BUILT it (rc=0) — the row was RED on the pre-c7 binary. + */ +#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; +} + +/* build_must_fail — write a bar/ module (fn `slot`) and a main.ww that + * declares a data global `slot: i64` plus `let fp: *i64 = &slot`, then + * `drv build -I bar main.ww`. Returns 0 when the build correctly FAILS + * (the leaf-name collision no longer diverts &slot into the fn reloc), + * non-zero when it wrongly built. */ +static int +build_must_fail(const char *drv, int tag) +{ + int pid = getpid(); + char dir[96], bard[160], p[224], cmd[1024], rm[256]; + snprintf(dir, sizeof dir, "/tmp/fnpc_%d_%d", pid, tag); + snprintf(bard, sizeof bard, "%s/bar", dir); + mkdir(dir, 0755); + mkdir(bard, 0755); + + snprintf(p, sizeof p, "%s/bar.ww", bard); + FILE *f = fopen(p, "wb"); + if (!f) return -1; + fputs("package bar;\n" + "export fn slot() i64 = { return 99; };\n", f); + fclose(f); + + snprintf(p, sizeof p, "%s/main.ww", dir); + f = fopen(p, "wb"); + if (!f) return -1; + fputs("package main;\n" + "import bar;\n" + "let slot: i64 = 7;\n" + "let fp: *i64 = &slot;\n" + "export fn main() int = { return (*fp): int; };\n", f); + fclose(f); + + snprintf(cmd, sizeof cmd, + "cd %s && %s build -I bar main.ww >/dev/null 2>&1", dir, drv); + int rc = runwait(cmd); + + snprintf(rm, sizeof rm, "rm -rf %s", dir); + runwait(rm); + + return rc == 0 ? -1 : 0; /* build must NOT succeed */ +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + 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[1024], wdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + int total = 0, fail = 0; + + total++; + if (build_must_fail(cdrv, 1) != 0) { + fprintf(stderr, "fnptrcollide_run[cstage]: built ok, expected " + "the leaf-name collision to be rejected\n"); + fail++; + } + if (access(wdrv, X_OK) == 0) { /* wwstage gated */ + total++; + if (build_must_fail(wdrv, 2) != 0) { + fprintf(stderr, "fnptrcollide_run[wwstage]: built ok, " + "expected reject (#14 — &slot mis-folded to the " + "fn TEXT reloc by the name-keyed nodefnptr)\n"); + fail++; + } + } else { + fprintf(stderr, "fnptrcollide_run: skip wwstage (no %s)\n", wdrv); + } + + if (fail) { + fprintf(stderr, "fnptrcollide_run: %d/%d checks failed\n", + fail, total); + return 1; + } + printf("fnptrcollide_run: %d/%d ok\n", total, total); + return 0; +}