wcc: exprtype resolves SK_USE module-qual N_DOT call results (#16, #17)

Re-arming the wwstage asserttyped bail surfaced 94 nil-stamp warns in the
checked corpus: let (a,b) = mod.fn() left its destructure bindings (and every
use) unstamped because exprtype's N_CALL arm resolved an N_DOT callee by bare
leaf — the gap its own comment flagged (#16/#17). Fix at the root: when an
N_DOT callee's lhs resolves to SK_USE, resolve the result via
scopelookupinmodule (mirror cstage cexpr check.c:1035 + cgen fnretlookupmod
cgen.ww:2263). The N_MLET backfill then just consumes the resolved tuple,
matching harec create_unpack_bindings (check.c:1354-1419), which does no callee
resolution — single path, no third copy.

The SK_USE gate leaves the module-leaf==type/fn-name collision cases
(random/fnmatch) on bare lookup — that nominal-resolution gap is a separate
fold. Beyond destructure, the root fix also closes a latent cs!=ww divergence
on non-destructure cross-module same-leaf calls (a head-ordered shadow was
mis-sizing the receive slot).

asserttyped is ww-stage only, so the live ww-driver suite can't see this — the
net is the warn count (checked 94->0, collision cases unchanged) + cs==ww .s
(probe 956). Compiler binary unchanged; 990-997 byte-id hold.
This commit is contained in:
2026-05-28 03:55:22 +09:00
parent c882bcf27c
commit ea1579a6fd
5 changed files with 439 additions and 72 deletions

View File

@@ -10379,29 +10379,31 @@ fn resolvewalk(c: *checker, n: *node) void = {
// (commit 2's bridge) needs: without it a `let (f,i)=mk()` f64 binding
// reads stamp nil → would disagree with the structural f64.
//
// GUARD bare N_IDENT callee only: exprtype's N_CALL N_DOT arm is the
// shelved #16/#17 cross-module-shadow gap (check.ww:2250-2253), so a
// module-qualified callee resolves to the wrong/nil return type —
// stamping off it would be worse than the nil it replaces. Bare same-
// module callees resolve correctly today. Faithful to harec
// create_unpack_bindings (ref/harec/src/check.c:1354-1416) and the
// cstage twin (cmd/wcc/check.c:1912 N_MLET, which uses cexpr ungated);
// narrowed to N_IDENT pending #16/#17. Annotated bindings keep their
// own type. Mirrors the N_FORRANGE binding-install shape above; the
// bindings would otherwise install (unstamped) via the generic N_LET
// walk, so this early return must register them itself.
// #6a-A: backfill off ANY call rhs, not just a bare N_IDENT callee, so
// a module-qualified `let (res, ov) = checked.addi64(a, b)` (N_DOT
// callee) stamps its bindings too. This is now a SINGLE path: just call
// exprtype(rhs) and consume the resolved N_TTUPLE — no callee resolution
// here. Harec's create_unpack_bindings does the same: ZERO callee
// resolution, it walks an already-typed tuple result (ref/harec/src/
// check.c:1354-1419). The module-qualified resolution that makes this
// correct for an N_DOT callee lives at the ROOT, in exprtype's N_CALL
// arm (the SK_USE-gated scopelookupinmodule there), so the binding just
// consumes. A D-class module whose leaf collides with a type/fn name
// resolves to nil/wrong-kind at the exprtype root (the SK_USE gate
// fails) → no N_TTUPLE → those destructures stay unstamped, a separate
// nominal-collision fold (#6a-D), not this one. Annotated bindings keep
// their own type. Mirrors the N_FORRANGE binding-install shape above;
// the bindings would otherwise install (unstamped) via the generic
// N_LET walk, so this early return must register them itself.
if (k == nkind.N_MLET) {
if (n.rhs != nil) { resolvewalk(c, n.rhs); };
let pt: *node = nil;
if (n.rhs != nil) { if (n.rhs.kind == nkind.N_CALL) {
let callee: *node = n.rhs.lhs;
if (callee != nil) { if (callee.kind == nkind.N_IDENT) {
// `rt` would shadow the imported lib/rt module
// (checkmoduleshadow errors); `rty` avoids it.
let rty: *node = exprtype(c, n.rhs, nil);
if (rty != nil) { if (rty.kind == nkind.N_TTUPLE) {
pt = rty.list;
}; };
// `rt` would shadow the imported lib/rt module
// (checkmoduleshadow errors); `rty` avoids it.
let rty: *node = exprtype(c, n.rhs, nil);
if (rty != nil) { if (rty.kind == nkind.N_TTUPLE) {
pt = rty.list;
}; };
}; };
let l: *node = n.list;
@@ -12236,15 +12238,40 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
// `foo()` inside module M binds to M.foo rather than another
// module's same-leaf foo at the head of the flat scope bucket.
// Mirrors cstage cexpr N_IDENT routing through
// scope_lookup_prefer with c->cur_mod. N_DOT keeps the bare
// scopelookup — its module-qualified resolution is a separate
// gap (parser stores the leaf in callee.str; mod is in
// callee.lhs.str, not consumed here yet).
// scope_lookup_prefer with c->cur_mod.
//
// #6a-A: a module-qualified `mod.fn()` callee resolves via the
// callee.lhs module hint when `mod` is an import (SK_USE) —
// scopelookupinmodule(mod, leaf) — mirroring cstage cexpr N_DOT
// (cmd/wcc/check.c:1035 scope_lookup_in_module) and cgen's
// rettupleof (cgen.ww:2263 fnretlookupmod). Closing this at the
// N_CALL root (vs the N_MLET backfill) makes every consumer of a
// module-qual call result — destructure binding AND a bare
// `mod.fn().0` rvalue — read the right return type via the one
// expr path, harec-faithful (binding-unpack does zero callee
// resolution, ref/harec/src/check.c:1354-1419). Without it the
// bare-leaf scopelookup grabbed whichever same-leaf fn heads the
// flat scope — wrong on a cross-module shadow (753_convwrap_audit:
// alpha.foo (i64,str) vs beta.foo (i64,i64)).
//
// THE SK_USE GATE is the #6a-D separator: a D-class callee whose
// `mod` leaf is itself a type/fn (SK_TYPE/SK_FN — random/fnmatch's
// module-leaf==type-name collision) does NOT resolve to SK_USE, so
// it falls through to the bare scopelookup and stays mis/unresolved.
// That nominal-collision is its own fold (#6a-D); not fixed here.
let s: *sym = nil;
if (callee.kind == nkind.N_IDENT) {
s = scopelookupprefer(c.cur, c.curmod, nm);
} else {
s = scopelookup(c.cur, nm);
let ms: *sym = nil;
if (callee.lhs != nil && callee.lhs.kind == nkind.N_IDENT) {
ms = scopelookupprefer(c.cur, c.curmod, callee.lhs.str);
};
if (ms != nil && ms.skind == skind.SK_USE) {
s = scopelookupinmodule(c.cur, callee.lhs.str, nm);
} else {
s = scopelookup(c.cur, nm);
};
};
if (s == nil) { return nil; };
if (s.skind != skind.SK_FN) { return nil; };

View File

@@ -390,29 +390,31 @@ fn resolvewalk(c: *checker, n: *node) void = {
// (commit 2's bridge) needs: without it a `let (f,i)=mk()` f64 binding
// reads stamp nil → would disagree with the structural f64.
//
// GUARD bare N_IDENT callee only: exprtype's N_CALL N_DOT arm is the
// shelved #16/#17 cross-module-shadow gap (check.ww:2250-2253), so a
// module-qualified callee resolves to the wrong/nil return type —
// stamping off it would be worse than the nil it replaces. Bare same-
// module callees resolve correctly today. Faithful to harec
// create_unpack_bindings (ref/harec/src/check.c:1354-1416) and the
// cstage twin (cmd/wcc/check.c:1912 N_MLET, which uses cexpr ungated);
// narrowed to N_IDENT pending #16/#17. Annotated bindings keep their
// own type. Mirrors the N_FORRANGE binding-install shape above; the
// bindings would otherwise install (unstamped) via the generic N_LET
// walk, so this early return must register them itself.
// #6a-A: backfill off ANY call rhs, not just a bare N_IDENT callee, so
// a module-qualified `let (res, ov) = checked.addi64(a, b)` (N_DOT
// callee) stamps its bindings too. This is now a SINGLE path: just call
// exprtype(rhs) and consume the resolved N_TTUPLE — no callee resolution
// here. Harec's create_unpack_bindings does the same: ZERO callee
// resolution, it walks an already-typed tuple result (ref/harec/src/
// check.c:1354-1419). The module-qualified resolution that makes this
// correct for an N_DOT callee lives at the ROOT, in exprtype's N_CALL
// arm (the SK_USE-gated scopelookupinmodule there), so the binding just
// consumes. A D-class module whose leaf collides with a type/fn name
// resolves to nil/wrong-kind at the exprtype root (the SK_USE gate
// fails) → no N_TTUPLE → those destructures stay unstamped, a separate
// nominal-collision fold (#6a-D), not this one. Annotated bindings keep
// their own type. Mirrors the N_FORRANGE binding-install shape above;
// the bindings would otherwise install (unstamped) via the generic
// N_LET walk, so this early return must register them itself.
if (k == nkind.N_MLET) {
if (n.rhs != nil) { resolvewalk(c, n.rhs); };
let pt: *node = nil;
if (n.rhs != nil) { if (n.rhs.kind == nkind.N_CALL) {
let callee: *node = n.rhs.lhs;
if (callee != nil) { if (callee.kind == nkind.N_IDENT) {
// `rt` would shadow the imported lib/rt module
// (checkmoduleshadow errors); `rty` avoids it.
let rty: *node = exprtype(c, n.rhs, nil);
if (rty != nil) { if (rty.kind == nkind.N_TTUPLE) {
pt = rty.list;
}; };
// `rt` would shadow the imported lib/rt module
// (checkmoduleshadow errors); `rty` avoids it.
let rty: *node = exprtype(c, n.rhs, nil);
if (rty != nil) { if (rty.kind == nkind.N_TTUPLE) {
pt = rty.list;
}; };
}; };
let l: *node = n.list;
@@ -2247,15 +2249,40 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
// `foo()` inside module M binds to M.foo rather than another
// module's same-leaf foo at the head of the flat scope bucket.
// Mirrors cstage cexpr N_IDENT routing through
// scope_lookup_prefer with c->cur_mod. N_DOT keeps the bare
// scopelookup — its module-qualified resolution is a separate
// gap (parser stores the leaf in callee.str; mod is in
// callee.lhs.str, not consumed here yet).
// scope_lookup_prefer with c->cur_mod.
//
// #6a-A: a module-qualified `mod.fn()` callee resolves via the
// callee.lhs module hint when `mod` is an import (SK_USE) —
// scopelookupinmodule(mod, leaf) — mirroring cstage cexpr N_DOT
// (cmd/wcc/check.c:1035 scope_lookup_in_module) and cgen's
// rettupleof (cgen.ww:2263 fnretlookupmod). Closing this at the
// N_CALL root (vs the N_MLET backfill) makes every consumer of a
// module-qual call result — destructure binding AND a bare
// `mod.fn().0` rvalue — read the right return type via the one
// expr path, harec-faithful (binding-unpack does zero callee
// resolution, ref/harec/src/check.c:1354-1419). Without it the
// bare-leaf scopelookup grabbed whichever same-leaf fn heads the
// flat scope — wrong on a cross-module shadow (753_convwrap_audit:
// alpha.foo (i64,str) vs beta.foo (i64,i64)).
//
// THE SK_USE GATE is the #6a-D separator: a D-class callee whose
// `mod` leaf is itself a type/fn (SK_TYPE/SK_FN — random/fnmatch's
// module-leaf==type-name collision) does NOT resolve to SK_USE, so
// it falls through to the bare scopelookup and stays mis/unresolved.
// That nominal-collision is its own fold (#6a-D); not fixed here.
let s: *sym = nil;
if (callee.kind == nkind.N_IDENT) {
s = scopelookupprefer(c.cur, c.curmod, nm);
} else {
s = scopelookup(c.cur, nm);
let ms: *sym = nil;
if (callee.lhs != nil && callee.lhs.kind == nkind.N_IDENT) {
ms = scopelookupprefer(c.cur, c.curmod, callee.lhs.str);
};
if (ms != nil && ms.skind == skind.SK_USE) {
s = scopelookupinmodule(c.cur, callee.lhs.str, nm);
} else {
s = scopelookup(c.cur, nm);
};
};
if (s == nil) { return nil; };
if (s.skind != skind.SK_FN) { return nil; };

View File

@@ -10379,29 +10379,31 @@ fn resolvewalk(c: *checker, n: *node) void = {
// (commit 2's bridge) needs: without it a `let (f,i)=mk()` f64 binding
// reads stamp nil → would disagree with the structural f64.
//
// GUARD bare N_IDENT callee only: exprtype's N_CALL N_DOT arm is the
// shelved #16/#17 cross-module-shadow gap (check.ww:2250-2253), so a
// module-qualified callee resolves to the wrong/nil return type —
// stamping off it would be worse than the nil it replaces. Bare same-
// module callees resolve correctly today. Faithful to harec
// create_unpack_bindings (ref/harec/src/check.c:1354-1416) and the
// cstage twin (cmd/wcc/check.c:1912 N_MLET, which uses cexpr ungated);
// narrowed to N_IDENT pending #16/#17. Annotated bindings keep their
// own type. Mirrors the N_FORRANGE binding-install shape above; the
// bindings would otherwise install (unstamped) via the generic N_LET
// walk, so this early return must register them itself.
// #6a-A: backfill off ANY call rhs, not just a bare N_IDENT callee, so
// a module-qualified `let (res, ov) = checked.addi64(a, b)` (N_DOT
// callee) stamps its bindings too. This is now a SINGLE path: just call
// exprtype(rhs) and consume the resolved N_TTUPLE — no callee resolution
// here. Harec's create_unpack_bindings does the same: ZERO callee
// resolution, it walks an already-typed tuple result (ref/harec/src/
// check.c:1354-1419). The module-qualified resolution that makes this
// correct for an N_DOT callee lives at the ROOT, in exprtype's N_CALL
// arm (the SK_USE-gated scopelookupinmodule there), so the binding just
// consumes. A D-class module whose leaf collides with a type/fn name
// resolves to nil/wrong-kind at the exprtype root (the SK_USE gate
// fails) → no N_TTUPLE → those destructures stay unstamped, a separate
// nominal-collision fold (#6a-D), not this one. Annotated bindings keep
// their own type. Mirrors the N_FORRANGE binding-install shape above;
// the bindings would otherwise install (unstamped) via the generic
// N_LET walk, so this early return must register them itself.
if (k == nkind.N_MLET) {
if (n.rhs != nil) { resolvewalk(c, n.rhs); };
let pt: *node = nil;
if (n.rhs != nil) { if (n.rhs.kind == nkind.N_CALL) {
let callee: *node = n.rhs.lhs;
if (callee != nil) { if (callee.kind == nkind.N_IDENT) {
// `rt` would shadow the imported lib/rt module
// (checkmoduleshadow errors); `rty` avoids it.
let rty: *node = exprtype(c, n.rhs, nil);
if (rty != nil) { if (rty.kind == nkind.N_TTUPLE) {
pt = rty.list;
}; };
// `rt` would shadow the imported lib/rt module
// (checkmoduleshadow errors); `rty` avoids it.
let rty: *node = exprtype(c, n.rhs, nil);
if (rty != nil) { if (rty.kind == nkind.N_TTUPLE) {
pt = rty.list;
}; };
}; };
let l: *node = n.list;
@@ -12236,15 +12238,40 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
// `foo()` inside module M binds to M.foo rather than another
// module's same-leaf foo at the head of the flat scope bucket.
// Mirrors cstage cexpr N_IDENT routing through
// scope_lookup_prefer with c->cur_mod. N_DOT keeps the bare
// scopelookup — its module-qualified resolution is a separate
// gap (parser stores the leaf in callee.str; mod is in
// callee.lhs.str, not consumed here yet).
// scope_lookup_prefer with c->cur_mod.
//
// #6a-A: a module-qualified `mod.fn()` callee resolves via the
// callee.lhs module hint when `mod` is an import (SK_USE) —
// scopelookupinmodule(mod, leaf) — mirroring cstage cexpr N_DOT
// (cmd/wcc/check.c:1035 scope_lookup_in_module) and cgen's
// rettupleof (cgen.ww:2263 fnretlookupmod). Closing this at the
// N_CALL root (vs the N_MLET backfill) makes every consumer of a
// module-qual call result — destructure binding AND a bare
// `mod.fn().0` rvalue — read the right return type via the one
// expr path, harec-faithful (binding-unpack does zero callee
// resolution, ref/harec/src/check.c:1354-1419). Without it the
// bare-leaf scopelookup grabbed whichever same-leaf fn heads the
// flat scope — wrong on a cross-module shadow (753_convwrap_audit:
// alpha.foo (i64,str) vs beta.foo (i64,i64)).
//
// THE SK_USE GATE is the #6a-D separator: a D-class callee whose
// `mod` leaf is itself a type/fn (SK_TYPE/SK_FN — random/fnmatch's
// module-leaf==type-name collision) does NOT resolve to SK_USE, so
// it falls through to the bare scopelookup and stays mis/unresolved.
// That nominal-collision is its own fold (#6a-D); not fixed here.
let s: *sym = nil;
if (callee.kind == nkind.N_IDENT) {
s = scopelookupprefer(c.cur, c.curmod, nm);
} else {
s = scopelookup(c.cur, nm);
let ms: *sym = nil;
if (callee.lhs != nil && callee.lhs.kind == nkind.N_IDENT) {
ms = scopelookupprefer(c.cur, c.curmod, callee.lhs.str);
};
if (ms != nil && ms.skind == skind.SK_USE) {
s = scopelookupinmodule(c.cur, callee.lhs.str, nm);
} else {
s = scopelookup(c.cur, nm);
};
};
if (s == nil) { return nil; };
if (s.skind != skind.SK_FN) { return nil; };