selfhost/cmd/wcc/check: per-block scope push/pop in resolvewalk

resolvewalk had no per-block scoping: inner-block `let i: u64`
persisted past the block end and shadowed the outer `let i: i32`,
which then false-positived as u64→i32 not-assignable on the next
reference. The TODO at the N_LET tail explicitly deferred per-block
scoping; this discharges it.

N_BLOCK case mirrors cstage cmd/wcc/check.c:1559-1566: save c.cur,
newscope under saved, walk body via n.list, restore. Sole exit is
the return after restore — push/pop balanced by structure.

All 5 selfhost main.combined.ww files (wwdump, w6c, w6a, w6l, ww)
now resolve clean via wwdump_ww -r. Reviewer's independent probe
across every .combined.ww outside ref/ confirmed no cascade: only
selfhost/cmd/ww went 1→0 (the targeted bug); the other 14
files-with-errors are pre-existing assignability/match-typing
issues unrelated to scope resolution.

Discharges TODO at N_LET tail. Same-scope dup detection
(`let a=1; let a=2;` in one block) stays queued behind #11.
Unblocks #50.
This commit is contained in:
2026-05-20 04:13:49 +09:00
parent 1b7fde21cd
commit 01775f2ccc
3 changed files with 81 additions and 24 deletions

View File

@@ -7346,6 +7346,27 @@ fn resolvewalk(c: *checker, n: *node) void = {
return;
};
// #53: lexical block. Push a child scope so locals introduced by
// inner-block lets (and the `let` install at the tail of this fn) go
// out of scope at block exit. Without this, a deeply nested
// `let i: u64 = 0u64;` survived to shadow a same-named outer
// `let i: i32 = 1;` for the whole fn body, and exprtype handed
// stale primitive types to checkletassign — silent miscompile
// becomes a false-positive on the next driver (`wwdump_ww -r`
// flagged the u64→i32 pair in selfhost/cmd/ww/enumeratedir).
// Mirrors cstage cstmt N_BLOCK at cmd/wcc/check.c:1559-1566.
if (k == nkind.N_BLOCK) {
let outer: *scope = c.cur;
c.cur = newscope(c.a, outer);
let m: *node = n.list;
for (m != nil) {
resolvewalk(c, m);
m = m.next;
};
c.cur = outer;
return;
};
if (k == nkind.N_DOT) {
// Walk only the base; the .field name is a member, not a
// free identifier.
@@ -7383,14 +7404,12 @@ fn resolvewalk(c: *checker, n: *node) void = {
// are installed in installdecl, so this duplicate install at
// the file scope just no-ops (scopedefine returns nil on dup).
//
// TODO(#11): cstage check.c (post-#32) errors `let '%s' redeclared
// in same scope` here. Wwstage resolvewalk has no per-block scope
// (see resolvefnbody's docstring) so a same-fn-body
// `let a=1; { let a=2; };` would falsely trip if we guarded
// scopedefine's nil return today. Silent-accept matches the
// deferred-check design until #11 adds per-block scoping; see
// test/wcc/708 and test/wcc/696 for the same cstage-only neg-case
// precedent.
// Cross-block `let a; { let a; };` no longer trips dup-silence
// since #53 added N_BLOCK push/pop above — the inner `a` lands in
// the inner block's scope. Same-scope dup `let a=1; let a=2;`
// still silent-accepts here; promoting that to an error stays
// queued behind #11 (test/wcc/708 + test/wcc/696 are the cstage-
// only neg-case precedent).
if (k == nkind.N_LET) {
let nm: str = n.str;
if (nm.len > 0) {

View File

@@ -352,6 +352,27 @@ fn resolvewalk(c: *checker, n: *node) void = {
return;
};
// #53: lexical block. Push a child scope so locals introduced by
// inner-block lets (and the `let` install at the tail of this fn) go
// out of scope at block exit. Without this, a deeply nested
// `let i: u64 = 0u64;` survived to shadow a same-named outer
// `let i: i32 = 1;` for the whole fn body, and exprtype handed
// stale primitive types to checkletassign — silent miscompile
// becomes a false-positive on the next driver (`wwdump_ww -r`
// flagged the u64→i32 pair in selfhost/cmd/ww/enumeratedir).
// Mirrors cstage cstmt N_BLOCK at cmd/wcc/check.c:1559-1566.
if (k == nkind.N_BLOCK) {
let outer: *scope = c.cur;
c.cur = newscope(c.a, outer);
let m: *node = n.list;
for (m != nil) {
resolvewalk(c, m);
m = m.next;
};
c.cur = outer;
return;
};
if (k == nkind.N_DOT) {
// Walk only the base; the .field name is a member, not a
// free identifier.
@@ -389,14 +410,12 @@ fn resolvewalk(c: *checker, n: *node) void = {
// are installed in installdecl, so this duplicate install at
// the file scope just no-ops (scopedefine returns nil on dup).
//
// TODO(#11): cstage check.c (post-#32) errors `let '%s' redeclared
// in same scope` here. Wwstage resolvewalk has no per-block scope
// (see resolvefnbody's docstring) so a same-fn-body
// `let a=1; { let a=2; };` would falsely trip if we guarded
// scopedefine's nil return today. Silent-accept matches the
// deferred-check design until #11 adds per-block scoping; see
// test/wcc/708 and test/wcc/696 for the same cstage-only neg-case
// precedent.
// Cross-block `let a; { let a; };` no longer trips dup-silence
// since #53 added N_BLOCK push/pop above — the inner `a` lands in
// the inner block's scope. Same-scope dup `let a=1; let a=2;`
// still silent-accepts here; promoting that to an error stays
// queued behind #11 (test/wcc/708 + test/wcc/696 are the cstage-
// only neg-case precedent).
if (k == nkind.N_LET) {
let nm: str = n.str;
if (nm.len > 0) {

View File

@@ -7346,6 +7346,27 @@ fn resolvewalk(c: *checker, n: *node) void = {
return;
};
// #53: lexical block. Push a child scope so locals introduced by
// inner-block lets (and the `let` install at the tail of this fn) go
// out of scope at block exit. Without this, a deeply nested
// `let i: u64 = 0u64;` survived to shadow a same-named outer
// `let i: i32 = 1;` for the whole fn body, and exprtype handed
// stale primitive types to checkletassign — silent miscompile
// becomes a false-positive on the next driver (`wwdump_ww -r`
// flagged the u64→i32 pair in selfhost/cmd/ww/enumeratedir).
// Mirrors cstage cstmt N_BLOCK at cmd/wcc/check.c:1559-1566.
if (k == nkind.N_BLOCK) {
let outer: *scope = c.cur;
c.cur = newscope(c.a, outer);
let m: *node = n.list;
for (m != nil) {
resolvewalk(c, m);
m = m.next;
};
c.cur = outer;
return;
};
if (k == nkind.N_DOT) {
// Walk only the base; the .field name is a member, not a
// free identifier.
@@ -7383,14 +7404,12 @@ fn resolvewalk(c: *checker, n: *node) void = {
// are installed in installdecl, so this duplicate install at
// the file scope just no-ops (scopedefine returns nil on dup).
//
// TODO(#11): cstage check.c (post-#32) errors `let '%s' redeclared
// in same scope` here. Wwstage resolvewalk has no per-block scope
// (see resolvefnbody's docstring) so a same-fn-body
// `let a=1; { let a=2; };` would falsely trip if we guarded
// scopedefine's nil return today. Silent-accept matches the
// deferred-check design until #11 adds per-block scoping; see
// test/wcc/708 and test/wcc/696 for the same cstage-only neg-case
// precedent.
// Cross-block `let a; { let a; };` no longer trips dup-silence
// since #53 added N_BLOCK push/pop above — the inner `a` lands in
// the inner block's scope. Same-scope dup `let a=1; let a=2;`
// still silent-accepts here; promoting that to an error stays
// queued behind #11 (test/wcc/708 + test/wcc/696 are the cstage-
// only neg-case precedent).
if (k == nkind.N_LET) {
let nm: str = n.str;
if (nm.len > 0) {