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