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.
This commit is contained in:
2026-05-31 17:00:42 +09:00
parent 2e760d070d
commit 39292b3c47
5 changed files with 306 additions and 1 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)
{

View File

@@ -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;

View File

@@ -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 <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;
}
#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;
}