selfhost+test: graduate bare-leaf fnparamslookup same-module-first (#4d)

Class A silent miscompile, latent until two modules export the same
fn leaf name with diverging tagged-vs-scalar param shapes. Wwstage's
fnparamslookup (selfhost/cmd/wcc/cgen.ww) walked c.fnrets head-first
by fname and returned the FIRST match's params. cgcall's N_IDENT
branch (cgenexpr.ww:2875) handed it the bare leaf; pushargsrev's
istaggedtype(c, pt) then fired against the wrong-module foo's
param-type. A foo(7) call against a same-leaf (i32 | void) param
re-laid the i32 arg into a 2-word tagged slot (MOVQ $7 push + MOVQ
$0 tag push + 2 POPs into DI/SI) instead of the caller-intended
single push (MOVQ $7 push + POPQ DI).

Cstage carries no sister bug: cmd/wcc/check.c N_CALL routes
cexpr(c, n->lhs) through scope_lookup_prefer for an N_IDENT callee,
then cmd/w6c/cgen.c reads params from the typed n->lhs->type's
TY_FN sig — module-aware via typed AST, sidestepping any bare-leaf
table. cs vs ws diverged on every same-leaf fn collision but no
in-tree corpus declares two same-leaf fns with diverging tagged-vs-
scalar param shapes (same surfacing pattern as #4a enumlookup
post-strings, #4b structlookup, #4c def): 995 stays green.

Seventh leaf of the trio graduation (after #27 aliaslookup, #28
fnparams *mod*-variant, #31 fnret *mod*-variant, #4a enum, #4b
struct, #4c def). fnparamslookupmod (the N_DOT consumer at
cgenexpr.ww:2876) already exists post-#28; this commit graduates
only the BARE-LEAF entry point with a same-module-first walk
mirroring aliaslookup's two-pass shape (cgen.ww:75). Three bare-
leaf callsites consume the graduated lookup uniformly: cgcall
N_IDENT branch at cgenexpr.ww:2875 (load-bearing for the tagged-
widening shape), cglocalsize scratch reservation at cgendecl.ww:420
(fires only on tagged-param + struct-payload arg), and
callee_variadic_param at cgenutil.ww:66 (fires only on variadic
callee). The latter two also accept N_DOT callees and feed the
bare leaf — pre-graduation those head-picked, post-graduation
they prefer same-module. NOT separately re-routed to
fnparamslookupmod in this commit: the only in-tree N_DOT cross-
module fn collisions (strings.next vs utf8.next; bytes.hasprefix
vs strings.hasprefix and equivalents) all have invariant param
shape across the colliding overloads, so widening/scratch/variadic
behavior is invariant either way for sites 2 and 3 on the present
corpus. A future stdlib port introducing a tagged-vs-scalar or
variadic-vs-non-variadic same-leaf N_DOT collision shape will
need the *mod re-routing — file at that surfacing.

732_fnparams_bare_leaf_shadow pins the fix with 1 row: alpha
defines fn foo(x: i32) i32 and fn alphacaller() i32 = {
return foo(7); }, beta defines fn foo(x: (i32|void)) i32
declared LAST in source so beta.foo prepends to the head of
c.fnrets. alphacaller's bare foo(7) must compile against
alpha.foo's i32 param (single PUSHQ/POPQ DI shape) even with
beta.foo at the head of c.fnrets. Asserts the matching POPQ DI
inside the right TEXT sym + bad_imm POPQ SI anti-check on each
stage plus cs-vs-ws byte-id per row.
This commit is contained in:
2026-05-18 15:06:20 +09:00
parent 4bd4ed925a
commit 862715d7df
5 changed files with 293 additions and 9 deletions

View File

@@ -1596,11 +1596,24 @@ fn fnretlookupmod(c: *cgen, name: str, mod: str) *node = {
};
// fnparamslookup — head of the declared param-list for a fn, or nil
// if the name isn't a registered fn. Used by cgcall / pushargsrev to
// detect implicit widening from a concrete variant into a tagged-union
// parameter slot.
// if the name isn't a registered fn. Same-module-first walk before the
// head-walk fallback. Trio-leaf graduation (#4d) mirroring aliaslookup
// (#27), fnret/fnparamslookupmod (#28/#31), enum/struct/deflookup
// (#4a/#4b/#4c): without the prefer pass a bare-leaf `foo(x)` call in
// module M (callee N_IDENT) silently picks another module's same-leaf
// `foo` from the head of c.fnrets, then pushargsrev's widening
// detection fires (or doesn't) against the wrong param-type — `foo(7)`
// against a same-leaf `(i32 | void)` param re-layouts 7 into a 2-word
// tagged slot vs the same-module `i32` param's single push.
fn fnparamslookup(c: *cgen, name: str) *node = {
let f: *fnret = c.fnrets;
for (f != nil) {
if (streq(f.fname, name)) {
if (streq(f.fmod, c.curmod)) { return f.params; };
};
f = f.frnext;
};
f = c.fnrets;
for (f != nil) {
if (streq(f.fname, name)) { return f.params; };
f = f.frnext;