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

@@ -46,33 +46,20 @@
* | (mirror of 775's branched_readers
* | row for the vtable-init side).
*
* IMPORT-ORDER WORKAROUND (pre-existing wwstage gap, NOT introduced
* here): every row places `import os;` FIRST. ww_ww's combined.ww
* concatenation follows the import-discovery order; an os-late
* ordering (io → rt → os) trips the wwstage checker on os.tryread /
* trywrite / tryopen's bare `return r;` over a tagged return type
* (3 false-positive "return: not assignable (i64 → )"/"(i32 → )"
* errors). The os-first ordering matches selfhost's combined.ww
* (time → os → rt → …) where the checker resolves cleanly. The 980
* memio_run / 990 / 995 byte-id gates do not hit this path because
* they either drive cs-only (980) or operate on a much wider type
* surface (990/995). Filed as a sibling task; bare `os` import in a
* small probe context is what surfaces it. Workaround drops out once
* the wwstage checker stops ordering-sensitively on bare-int return
* to tagged-int union.
* IMPORT ORDER (#208 CLOSED): every row imports `memio; io; os;` —
* os LAST. This is the formerly-failing os-late ordering. Pre-#208 it
* tripped the wwstage checker on io.read/write/close's `return s.read(
* s, buf)` (a fn-pointer field call) with 3 false "return: not
* assignable (i64 → )"/"(i32 → )" — because exprtype re-bound the field
* leaf to a same-named scalar global (os.read:i64) instead of the
* field's tagged fn type. #208 fixed exprtype to resolve value-receiver
* field calls from the field type (cmd/wcc/check.c:1378-1433 twin), so
* the order no longer matters and all 4 rows now run STAGE_CS|STAGE_WW
* byte-id. The earlier `import os;`-FIRST workaround is retired here.
*
* SIBLINGS (filed inline, NOT fixed here — fold-e2 is purely
* additive over fold-e1's frozen io.* surface):
*
* - WWSTAGE-IMPORT-ORDER: `import os;` must appear before `import
* memio;` / `import io;` in a small probe context. ww_ww's
* combined.ww concat order trips the wwstage checker's
* `(i64 | T)` / `(i32 | T)` return-assignability when bare
* int-returning fns (os.tryread / trywrite / tryopen) are
* checked before certain ordering-sensitive pre-resolved types.
* cstage accepts unconditionally. Symmetric to the wwstage
* ordering gaps already filed as #189 / #190 / #202.
*
* - #173 (TRY-on-tagged-return both-stages broken) blocks
* fixed_write_v from returning Hare's `nomem` when full;
* vstream.ww surfaces 0 instead (memio.ww:155 OLD divergence).
@@ -120,9 +107,9 @@ struct row {
static const struct row rows[] = {
{ "fixed_read_5",
"package main;\n"
"import os;\n"
"import memio;\n"
"import io;\n"
"import os;\n"
"export fn main() i32 = {\n"
" let buf: [8]u8;\n"
" buf[0] = 65u8; buf[1] = 66u8; buf[2] = 67u8; buf[3] = 68u8;\n"
@@ -144,9 +131,9 @@ static const struct row rows[] = {
STAGE_CS | STAGE_WW, 1 },
{ "dynamic_write_grow",
"package main;\n"
"import os;\n"
"import memio;\n"
"import io;\n"
"import os;\n"
"export fn main() i32 = {\n"
" let r = memio.dynamic_vstream();\n"
" let s: io.vstream = nil: *io.vtable;\n"
@@ -167,9 +154,9 @@ static const struct row rows[] = {
STAGE_CS | STAGE_WW, 1 },
{ "dynamicfrom_alt_rw",
"package main;\n"
"import os;\n"
"import memio;\n"
"import io;\n"
"import os;\n"
"export fn main() i32 = {\n"
" let seed: [4]u8;\n"
" seed[0] = 1u8; seed[1] = 2u8; seed[2] = 3u8; seed[3] = 4u8;\n"
@@ -195,9 +182,9 @@ static const struct row rows[] = {
STAGE_CS | STAGE_WW, 1 },
{ "branched_fixed",
"package main;\n"
"import os;\n"
"import memio;\n"
"import io;\n"
"import os;\n"
"export fn main() i32 = {\n"
" let a: [3]u8;\n"
" a[0] = 10u8; a[1] = 11u8; a[2] = 12u8;\n"