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:
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user