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:
11
Makefile
11
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
263
test/wcc/788_type_value_shadow_run.c
Normal file
263
test/wcc/788_type_value_shadow_run.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user