From 39292b3c4794b012e529e813dbb429d4fb54cea7 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sun, 31 May 2026 17:00:42 +0900 Subject: [PATCH] wcc: kind-filter type-position name resolution so a value can't shadow a same-named type (#225) resolve_typename used the kind-blind scope_lookup_prefer, so a same-named value binding (param/let) in a closer scope hid the type it shadowed, wrongly rejecting valid Hare like 'fn f(off: off)'. wwstage already separates type/value namespaces; this aligns the cstage frontend up. New scope_lookup_type skips non-SK_TYPE syms and keeps scanning, preserving same-module preference. Byte-id-neutral: the new branch fires only on the old 'unknown type' error path. --- Makefile | 11 ++ cmd/wcc/check.c | 4 +- cmd/wcc/sym.c | 28 +++ cmd/wcc/ww.h | 1 + test/wcc/788_type_value_shadow_run.c | 263 +++++++++++++++++++++++++++ 5 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 test/wcc/788_type_value_shadow_run.c diff --git a/Makefile b/Makefile index 22a24ea1..bd97eb45 100644 --- a/Makefile +++ b/Makefile @@ -335,6 +335,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_fmt_vstream_compositions_run \ $(BIN)/test_fieldfn_leaf_collide_run \ $(BIN)/test_amp_fn_assign_run \ + $(BIN)/test_type_value_shadow_run \ $(BIN)/test_xmod_alias_struct_collide_run \ $(BIN)/test_xmod_variant_match \ $(BIN)/test_named_ptr_alias_variant_widen \ @@ -735,6 +736,16 @@ $(BIN)/test_amp_fn_assign_run: test/wcc/783_amp_fn_assign_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #225: a value binding (param/let/fn) that shadows a same-named TYPE +# must not hide it in type-annotation or cast position. cstage was +# kind-blind in resolve_typename; wwstage already separated the type and +# value namespaces. Driver build + run on both stages + cs.s == ww.s. +$(BIN)/test_type_value_shadow_run: test/wcc/788_type_value_shadow_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # #223: a struct-field access through a pointer-ALIAS receiver whose # leaf collides with another module's same-leaf STRUCT. cstage driver # build + run for runtime, raw w6c vs w6c_ww .s cmp on the combined.ww diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index f31cf336..d81581aa 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -65,7 +65,9 @@ resolve_typename(Checker *c, Node *n) const char *nm = n->str; Type *bi = lookup_builtin(nm); if (bi) return bi; - Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, nm); + /* #225: kind-filtered so a same-named value binding (param/let/fn) + * in a closer scope can't hide the type binding it shadows. */ + Sym *s = scope_lookup_type(c->cur, c->cur_mod, nm); if (s == NULL && nm) { /* module-qualified: io.stream → strip the last dot prefix * and look up the leaf, filtering on the importing module's diff --git a/cmd/wcc/sym.c b/cmd/wcc/sym.c index 165c2ef6..2b9501a8 100644 --- a/cmd/wcc/sym.c +++ b/cmd/wcc/sym.c @@ -116,6 +116,34 @@ scope_lookup_prefer(Scope *s, const char *mod, const char *name) return NULL; } +/* + * scope_lookup_type — kind-filtered bare-leaf lookup for type position. + * + * Same FNV bucket + hashnext chain + parent walk and same-module + * preference as scope_lookup_prefer, but skips every Sym whose kind + * isn't SK_TYPE and KEEPS scanning — so it returns the innermost + * SK_TYPE of `name`, looking past a same-named value binding (SK_VAR/ + * SK_PARAM/SK_FN) that shadows it in a closer scope. ww keeps type and + * value namespaces separate (wwstage already does; #225 conformance + * gap): a param `off` must not hide the global `type off`. + */ +Sym * +scope_lookup_type(Scope *s, const char *mod, const char *name) +{ + for (Scope *p = s; p; p = p->parent) { + u64 h = hashstr(name) % p->nbuckets; + Sym *fallback = NULL; + for (Sym *b = p->buckets[h]; b; b = b->hashnext) { + if (b->kind != SK_TYPE) continue; + if (strcmp(b->name, name) != 0) continue; + if (mod && b->mod && strcmp(b->mod, mod) == 0) return b; + if (fallback == NULL) fallback = b; + } + if (fallback) return fallback; + } + return NULL; +} + Sym * scope_define(Scope *s, const char *name, Skind k, Type *t, Node *decl) { diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index fa5f1844..d061c3f9 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -526,6 +526,7 @@ Sym *scope_lookup(Scope*, const char *name); /* walk up parents */ Sym *scope_lookup_local(Scope*, const char *name); Sym *scope_lookup_in_module(Scope*, const char *mod, const char *name); Sym *scope_lookup_prefer(Scope*, const char *mod, const char *name); +Sym *scope_lookup_type(Scope*, const char *mod, const char *name); /* ---- checker (check.c) -------------------------------------------- */ typedef struct Checker Checker; diff --git a/test/wcc/788_type_value_shadow_run.c b/test/wcc/788_type_value_shadow_run.c new file mode 100644 index 00000000..9ae406bd --- /dev/null +++ b/test/wcc/788_type_value_shadow_run.c @@ -0,0 +1,263 @@ +/* + * 788_type_value_shadow_run — project #225 close. A value binding that + * shadows a same-named TYPE must NOT hide that type in type-annotation + * or cast position. ww keeps type and value namespaces separate; + * wwstage already did, cstage did not. + * + * Pre-fix cstage rejected valid Hare code that wwstage + Hare accept: + * + * type off = i64; + * fn f(off: off) i64 = { return off: off; }; + * + * cmd/wcc/check.c resolve_typename resolved the type name via the + * kind-blind scope_lookup_prefer, which returns the first NAME match + * of ANY kind (innermost→outermost). The param `off` (SK_PARAM, inner + * scope) shadowed the global `type off` (SK_TYPE, scope 0): the inner + * value won, resolve_typename saw a non-SK_TYPE → nil → "unknown type + * 'off'". The type binding was present; the lookup just couldn't see + * past the shadowing value. + * + * Fix (additive, cstage only — aligning UP to wwstage per rule 10): + * a kind-filtered scope_lookup_type (cmd/wcc/sym.c) that skips every + * non-SK_TYPE Sym and keeps scanning, returning the innermost SK_TYPE + * of that name. resolve_typename calls it instead of scope_lookup_prefer. + * Both type-annotation (param type) and cast-target (`expr: T`, the + * N_CAST rhs) route through resolve_type→N_TNAME→resolve_typename, so + * the single swap covers both positions. + * + * ROW POLARITY: both rows build + run on BOTH stages and assert + * cs.s == ww.s (rule-10). The fix is byte-id-NEUTRAL by construction — + * the new branch fires only on the pre-fix "unknown type" error path, + * which no passing corpus reaches. + * param_and_cast — the canonical repro: a param named `off` shadows + * `type off`, used both as the param's own type and + * as a cast target inside the body. + * cast_local — a `let` value shadows `type t`, then `v: t` casts + * through the shadowed type name in the same scope. + * value_not_type_neg — the inverse guard: a value name with NO + * same-named type in any scope must still be rejected + * in type position. Proves the kind filter only looks + * PAST a value to a real type, never promotes the + * value itself. Both stages must fail the build. + * + * Not covered (not constructible): a fn-name shadowing a type — fns and + * types share the flat global scope and dedup on (name, mod), so a + * same-named global `fn`/`type` pair is a "duplicate fn" error, never a + * shadow. SK_PARAM + SK_VAR are the only value kinds that can shadow. + * + * GATE: must stay GREEN. Red on cstage means the kind filter regressed; + * red on byte-id means the two stages diverged on the shadow path. + */ +#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; +} + +#define STAGE_CS 1 +#define STAGE_WW 2 + +struct row { + const char *label; + const char *src; + int want_exit; + int stage_mask; + int byte_id; +}; + +static const struct row rows[] = { + { "param_and_cast", + "package main;\n" + "type off = i64;\n" + "fn f(off: off) i64 = { return off: off; };\n" + "export fn main() i32 = { return f(5): i32; };\n", + 5, STAGE_CS | STAGE_WW, 1 }, + + { "cast_local", + "package main;\n" + "type t = i32;\n" + "export fn main() i32 = {\n" + " let t: i32 = 7;\n" + " return t: t;\n" + "};\n", + 7, STAGE_CS | STAGE_WW, 1 }, + + /* NEG: a value in type position with NO same-named type must still + * error. Guards the inverse of the fix — scope_lookup_type must + * never PROMOTE the value `q` (SK_VAR) into a type, only look PAST + * a value to a real type that is genuinely there. want_exit -1 = + * build fails on both stages (cstage "unknown type 'q'"). */ + { "value_not_type_neg", + "package main;\n" + "export fn main() i32 = {\n" + " let q: i32 = 3;\n" + " return q: q;\n" + "};\n", + -1, STAGE_CS | STAGE_WW, 0 }, +}; + +static void +cleanup_tmp(const char *tmpdir, const char *base) +{ + char p[1024]; + snprintf(p, sizeof p, "%s/%s", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s.ww", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s.s", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s.o", tmpdir, base); unlink(p); + snprintf(p, sizeof p, "%s/%s.combined.ww", tmpdir, base); unlink(p); + rmdir(tmpdir); +} + +static int +write_source(const char *path, const char *src) +{ + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(src, f); + fclose(f); + return 0; +} + +static int +build_via_driver(const char *driver, const char *tmpdir, const char *src) +{ + char cmd[2048]; + snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %s build %s 2>/dev/null", + tmpdir, driver, src); + return runwait(cmd); +} + +/* run_row — build via driver, run the binary, return exit (or -1 on + * build failure). */ +static int +run_row(const char *driver, const struct row *r, int seq) +{ + char tmpdir[256], src[512], base[64], outbin[768]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/tvs_%d_d_%d", getpid(), seq); + snprintf(base, sizeof base, "main788"); + snprintf(src, sizeof src, "%s/%s.ww", tmpdir, base); + mkdir(tmpdir, 0755); + if (write_source(src, r->src) != 0) { cleanup_tmp(tmpdir, base); return -1; } + int rc; + if (build_via_driver(driver, tmpdir, src) == 0) { + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + rc = runwait(outbin); + } else { + rc = -1; + } + cleanup_tmp(tmpdir, base); + return rc; +} + +/* asm_byte_identical — diff cstage vs wwstage .s. Parallel trees so + * ww_ww writing intermediates next to the source doesn't clobber the + * cstage .s (CLAUDE.md rule 14 phase split). */ +static int +asm_byte_identical(const char *cdrv, const char *wdrv, const struct row *r, + int seq) +{ + char src[512], tdc[256], tdw[256], base[64], cs[512], ws[512]; + snprintf(tdc, sizeof tdc, "/tmp/tvs_%d_c_%d", getpid(), seq); + snprintf(tdw, sizeof tdw, "/tmp/tvs_%d_w_%d", getpid(), seq); + snprintf(base, sizeof base, "main788"); + mkdir(tdc, 0755); + mkdir(tdw, 0755); + snprintf(src, sizeof src, "%s/%s.ww", tdc, base); + if (write_source(src, r->src) != 0) { cleanup_tmp(tdc, base); cleanup_tmp(tdw, base); return -1; } + int rc = -1; + if (build_via_driver(cdrv, tdc, src) != 0) goto out; + snprintf(cs, sizeof cs, "%s/%s.s", tdc, base); + + snprintf(src, sizeof src, "%s/%s.ww", tdw, base); + if (write_source(src, r->src) != 0) goto out; + if (build_via_driver(wdrv, tdw, src) != 0) goto out; + snprintf(ws, sizeof ws, "%s/%s.s", tdw, base); + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + if (fc && fw) { + rc = 0; + for (;;) { + int a = fgetc(fc); + int b = fgetc(fw); + if (a != b) { rc = -1; break; } + if (a == EOF) break; + } + } + if (fc) fclose(fc); + if (fw) fclose(fw); +out: + cleanup_tmp(tdc, base); + cleanup_tmp(tdw, base); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + char absbin[512]; + if (bin[0] != '/') { + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640], wdrv[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0, seq = 0; + int wwpresent = (access(wdrv, X_OK) == 0); + + for (int i = 0; i < n; i++) { + const struct row *r = &rows[i]; + + if (r->stage_mask & STAGE_CS) { + total++; + int got = run_row(cdrv, r, seq++); + if (got != r->want_exit) { + fprintf(stderr, "type_value_shadow[cs][%s]: exit=%d want=%d\n", + r->label, got, r->want_exit); + fail++; + } + } + + if (wwpresent && (r->stage_mask & STAGE_WW)) { + total++; + int got = run_row(wdrv, r, seq++); + if (got != r->want_exit) { + fprintf(stderr, "type_value_shadow[ww][%s]: exit=%d want=%d\n", + r->label, got, r->want_exit); + fail++; + } + if (r->byte_id) { + total++; + if (asm_byte_identical(cdrv, wdrv, r, seq++) != 0) { + fprintf(stderr, "type_value_shadow[byte-id][%s]: cstage vs wwstage asm differs\n", + r->label); + fail++; + } + } + } + } + + if (fail) { + fprintf(stderr, "type_value_shadow: %d/%d checks failed\n", fail, total); + return 1; + } + printf("type_value_shadow: %d/%d ok\n", total, total); + return 0; +}