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

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