wcc: resolve value-receiver field-call type via callee, not global leaf (#208)

wwstage exprtype's N_CALL arm fell into a global-leaf scopelookup for an
N_DOT callee with a value or chained receiver, binding whatever same-named
global headed the scope bucket. Under a late-os combined.ww concat order
this resolved io's `s.read(...)` to os.read (i64) instead of the field's
fn type, so checkretassign confidently rejected a valid tagged return — an
import-order-sensitive false positive. cstage resolves a call result solely
from the callee expr's own type (check.c:1378-1433, mirroring harec
check_autodereference 1566-1581); drop the global-leaf else-arm so value
and chained receivers fall through to the existing fn-VALUE path at
check.ww:2455. SK_USE module-qualified calls are unchanged.

ww has no methods, so `value.leaf()` is only ever a fn-ptr field access; the
global hit was never legitimate. Zero .s delta across all 5 bootstrap tools
(the branch is dead in the bootstrap); test 776 graduates to both stages
(os-late order, byte-identical).

The fix unmasks a pre-existing wwstage cgen bug (#211): cgen also re-derives
a call's return shape by name (fnretlookup), so a value-receiver field call
whose leaf collides with a same-named global of a different register shape
mis-resolves cs!=ww. Documented at the cgen site; pinned cstage-only by
test/wcc/782 (graduates to STAGE_WW on #211 close).
This commit is contained in:
2026-05-29 15:27:50 +09:00
parent bc9d7df002
commit 4e1181fd8c
7 changed files with 317 additions and 33 deletions

View File

@@ -12458,10 +12458,23 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
if (mu != nil) { ms = mu; };
};
};
// #208: only a module-qualified callee (SK_USE receiver)
// resolves its leaf by name here. A value receiver
// (`s.read(...)`, `(*p).read()`, `a.b.read()`) is a
// fn-pointer FIELD call whose result type comes from the
// FIELD's fn type, not a global-leaf lookup. The old
// `scopelookup(c.cur, nm)` else-arm bound whichever
// same-leaf global fn headed the flat scope bucket —
// order-sensitive (os.read:i64 vs io.read:(i32|eof|closed)
// flipped by combined.ww concat order), yielding a
// false `return: not assignable`. Leaving s nil falls
// through to the fn-VALUE path below (exprtype(callee) →
// TPTR peel → TFN.ret), matching cstage cmd/wcc/check.c
// :1378-1433 (call result IS the callee type's ret; no
// global-leaf path) and harec check_autodereference
// (ref/harec/src/check.c:1566-1581).
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) { if (s.skind == skind.SK_FN) { if (s.decl != nil) {
@@ -30087,6 +30100,26 @@ fn collectfnrets(c: *cgen, file: *node) void = {
// return type (str-pair shuffle, tagged-union ABI, tuple destructure,
// float ABI, sret slot sizing, fn-rvalue LEAQ, slice flow) fires
// against the wrong-module shape.
// fnretlookup — the called fn's declared return type, keyed by NAME
// (same-module-first, then first leaf match). The receive sites that
// re-derive a call's result SHAPE from this (cglet tagged-store,
// cgwidentaggedstore scalar-vs-tagged classify, tuple/sret/unsigned
// arms) are correct only when the leaf name uniquely picks the callee.
//
// #211 (gate-blind cgen divergence, sibling of the #208 checker fix): a
// VALUE-receiver fn-pointer FIELD call `s.f(...)` reaches the receive
// sites keyed on the field leaf `f` with the receiver VARIABLE name as
// the "module" (not a real module), so this lookup mis-binds a same-named
// GLOBAL fn. When that global's register shape differs from the field's
// (scalar global vs tagged field), the slot is stored with the wrong ABI
// shape → cstage≠wwstage asm, silent miscompile. The sound fix derives
// the result from the FIELD's fn type / the checker-stamped n.type_ (as
// cstage does, cmd/wcc/check.c:1378-1433), not by leaf name. NO guard is
// added here: same-shape leaf collisions resolve by name legitimately
// today, and a discriminating guard would need the shape-compare that IS
// the fix. Masked until #208 landed (the checker rejected the shape
// before cgen ran). test/wcc/782 pins the cstage-correct runtime
// (cstage-only) and graduates to STAGE_WW on #211 close.
fn fnretlookup(c: *cgen, name: str) *node = {
let f: *fnret = c.fnrets;
for (f != nil) {

View File

@@ -2235,6 +2235,26 @@ fn collectfnrets(c: *cgen, file: *node) void = {
// return type (str-pair shuffle, tagged-union ABI, tuple destructure,
// float ABI, sret slot sizing, fn-rvalue LEAQ, slice flow) fires
// against the wrong-module shape.
// fnretlookup — the called fn's declared return type, keyed by NAME
// (same-module-first, then first leaf match). The receive sites that
// re-derive a call's result SHAPE from this (cglet tagged-store,
// cgwidentaggedstore scalar-vs-tagged classify, tuple/sret/unsigned
// arms) are correct only when the leaf name uniquely picks the callee.
//
// #211 (gate-blind cgen divergence, sibling of the #208 checker fix): a
// VALUE-receiver fn-pointer FIELD call `s.f(...)` reaches the receive
// sites keyed on the field leaf `f` with the receiver VARIABLE name as
// the "module" (not a real module), so this lookup mis-binds a same-named
// GLOBAL fn. When that global's register shape differs from the field's
// (scalar global vs tagged field), the slot is stored with the wrong ABI
// shape → cstage≠wwstage asm, silent miscompile. The sound fix derives
// the result from the FIELD's fn type / the checker-stamped n.type_ (as
// cstage does, cmd/wcc/check.c:1378-1433), not by leaf name. NO guard is
// added here: same-shape leaf collisions resolve by name legitimately
// today, and a discriminating guard would need the shape-compare that IS
// the fix. Masked until #208 landed (the checker rejected the shape
// before cgen ran). test/wcc/782 pins the cstage-correct runtime
// (cstage-only) and graduates to STAGE_WW on #211 close.
fn fnretlookup(c: *cgen, name: str) *node = {
let f: *fnret = c.fnrets;
for (f != nil) {

View File

@@ -2428,10 +2428,23 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
if (mu != nil) { ms = mu; };
};
};
// #208: only a module-qualified callee (SK_USE receiver)
// resolves its leaf by name here. A value receiver
// (`s.read(...)`, `(*p).read()`, `a.b.read()`) is a
// fn-pointer FIELD call whose result type comes from the
// FIELD's fn type, not a global-leaf lookup. The old
// `scopelookup(c.cur, nm)` else-arm bound whichever
// same-leaf global fn headed the flat scope bucket —
// order-sensitive (os.read:i64 vs io.read:(i32|eof|closed)
// flipped by combined.ww concat order), yielding a
// false `return: not assignable`. Leaving s nil falls
// through to the fn-VALUE path below (exprtype(callee) →
// TPTR peel → TFN.ret), matching cstage cmd/wcc/check.c
// :1378-1433 (call result IS the callee type's ret; no
// global-leaf path) and harec check_autodereference
// (ref/harec/src/check.c:1566-1581).
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) { if (s.skind == skind.SK_FN) { if (s.decl != nil) {

View File

@@ -12458,10 +12458,23 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = {
if (mu != nil) { ms = mu; };
};
};
// #208: only a module-qualified callee (SK_USE receiver)
// resolves its leaf by name here. A value receiver
// (`s.read(...)`, `(*p).read()`, `a.b.read()`) is a
// fn-pointer FIELD call whose result type comes from the
// FIELD's fn type, not a global-leaf lookup. The old
// `scopelookup(c.cur, nm)` else-arm bound whichever
// same-leaf global fn headed the flat scope bucket —
// order-sensitive (os.read:i64 vs io.read:(i32|eof|closed)
// flipped by combined.ww concat order), yielding a
// false `return: not assignable`. Leaving s nil falls
// through to the fn-VALUE path below (exprtype(callee) →
// TPTR peel → TFN.ret), matching cstage cmd/wcc/check.c
// :1378-1433 (call result IS the callee type's ret; no
// global-leaf path) and harec check_autodereference
// (ref/harec/src/check.c:1566-1581).
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) { if (s.skind == skind.SK_FN) { if (s.decl != nil) {
@@ -30087,6 +30100,26 @@ fn collectfnrets(c: *cgen, file: *node) void = {
// return type (str-pair shuffle, tagged-union ABI, tuple destructure,
// float ABI, sret slot sizing, fn-rvalue LEAQ, slice flow) fires
// against the wrong-module shape.
// fnretlookup — the called fn's declared return type, keyed by NAME
// (same-module-first, then first leaf match). The receive sites that
// re-derive a call's result SHAPE from this (cglet tagged-store,
// cgwidentaggedstore scalar-vs-tagged classify, tuple/sret/unsigned
// arms) are correct only when the leaf name uniquely picks the callee.
//
// #211 (gate-blind cgen divergence, sibling of the #208 checker fix): a
// VALUE-receiver fn-pointer FIELD call `s.f(...)` reaches the receive
// sites keyed on the field leaf `f` with the receiver VARIABLE name as
// the "module" (not a real module), so this lookup mis-binds a same-named
// GLOBAL fn. When that global's register shape differs from the field's
// (scalar global vs tagged field), the slot is stored with the wrong ABI
// shape → cstage≠wwstage asm, silent miscompile. The sound fix derives
// the result from the FIELD's fn type / the checker-stamped n.type_ (as
// cstage does, cmd/wcc/check.c:1378-1433), not by leaf name. NO guard is
// added here: same-shape leaf collisions resolve by name legitimately
// today, and a discriminating guard would need the shape-compare that IS
// the fix. Masked until #208 landed (the checker rejected the shape
// before cgen ran). test/wcc/782 pins the cstage-correct runtime
// (cstage-only) and graduates to STAGE_WW on #211 close.
fn fnretlookup(c: *cgen, name: str) *node = {
let f: *fnret = c.fnrets;
for (f != nil) {