From 01775f2ccc6e22f5d09b30e1e6393d65e0866715 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 20 May 2026 04:13:49 +0900 Subject: [PATCH] selfhost/cmd/wcc/check: per-block scope push/pop in resolvewalk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- selfhost/cmd/w6c/main.combined.ww | 35 +++++++++++++++++++++------- selfhost/cmd/wcc/check.ww | 35 +++++++++++++++++++++------- selfhost/cmd/wwdump/main.combined.ww | 35 +++++++++++++++++++++------- 3 files changed, 81 insertions(+), 24 deletions(-) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 774b9e84..b9d1ba84 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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) { diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 38465d5b..4005fe75 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -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) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 7b36abb0..28d502c3 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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) {