selfhost+test: route N_DOT callee through fnparamslookupmod (#28)

Wwstage cgcall now mirrors cstage's typed-AST callee-params
lookup for module-qualified mod.fn(...) calls, restoring tagged-
union widening on cross-module slice args. Class A runtime
miscompile — masked from 995_self_rebuild because wwstage tools
don't call bytes.index directly; surfaced by lib/strings landing
dragging utf8 + bytes into the wwstage-tool dep chain via
strings.byteindex's `bytes.X(toutf8(...), n)` call sites.

Pre-fix: wwstage's cgcall (cgenexpr.ww) looked up calleeparams
only when callee.kind == N_IDENT. For N_DOT callees (the
module-qualified mod.fn() form), calleeparams stayed nil →
pushargsrev's widening detection gated on param != nil never
fired → wwstage fell through to the N_IDENT-slice fast path
pushing only 3 slot words (cap, len, ptr) WITHOUT the variant
tag. Receiving fn's `match (needle)` then dispatched on
(needle.ptr in CX) instead of needle.tag, with R8/R9 carrying
.len/.cap instead of .ptr/.len. Wrong arm + wrong payload.

Cstage handles N_DOT natively via the checker-set type on
n->lhs->type (cmd/w6c/cgen.c:4156-4165), so cg_widen_tagged_push
slice path pushes 4 words including tag.

Polarity catalog: wwstage UNDER — calleeparams lookup missing
N_DOT dispatch arm. Sister to #19 (N_TSLICE variantindex arm),
#21 (N_CALL pushargsrev arm), #24 (N_CALL nodeisslice arm), #27
(aliaslookup same-mod-first). The pattern: wwstage dispatchers
keep missing arms cstage has natively via typed-AST resolution.
Convergence wwstage → cstage (rule 10's spirit overrides letter
when correctness is at stake — Path 2 of aligning cstage DOWN
would create a runtime miscompile in both stages).

Fix: cgcall N_DOT branch pulls module from callee.lhs.str and
function name from callee.str, calls new fnparamslookupmod
helper. Helper does same-module-first walk then existing
first-match fallback (mirrors #27's aliaslookup fix shape). New
fnret.fmod field carries module identity; collectfnrets sets
f.fmod = d.module at registration. Module-qualified pkg.fn path
unchanged.

Tests:
  - 727_modcall_widen_slice pins MOVQ $1 + PUSHQ AX (tag-synth)
    before the receiving fn's CALL on canonical mod.fn(slice, ...)
    shape with the callee param widened to a tagged union. Three
    assertions per row: cstage tag-synth presence, wwstage
    tag-synth presence, cstage↔wwstage cmp -s byte-id. Sentinel-
    flip-verified: comment out fnparamslookupmod call →
    wwstage tag-synth absent + cmp diverges.

99/99 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
This commit is contained in:
2026-05-18 10:20:01 +09:00
parent 85af051cd1
commit c34abf47b1
6 changed files with 355 additions and 12 deletions

View File

@@ -12954,13 +12954,28 @@ fn cgcall(c: *cgen, n: *node) void = {
// Look up the callee's declared params for tagged-union widening.
// fn-pointer calls (callee is a local) don't get widening — the
// user must build the tagged value explicitly. Matches the most
// common case (direct named calls).
// user must build the tagged value explicitly.
//
// N_DOT (`mod.fn(...)`) covers cross-module calls; pre-#28 wwstage
// only handled N_IDENT, leaving N_DOT calls without widening
// detection — pushargsrev then fell through to the N_IDENT-slice
// fast path and dropped the variant tag word on widened slice args.
// Cstage finds params via the checker-set `n->lhs->type`, sidestepping
// the name-driven registry entirely (cmd/w6c/cgen.c:4161-4165).
let calleeparams: *node = nil;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
calleeparams = fnparamslookup(c, callee.str);
};
} else { if (callee.kind == nkind.N_DOT) {
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
calleeparams = fnparamslookupmod(c, callee.str, cmod);
}; };
};
// Hare-style variadic last param: gather N tail args into a
// frame-resident [N]T (`@vararg_d_<seq>`) plus a 24B slice
@@ -19648,6 +19663,7 @@ fn emitdatasection(c: *cgen) void = {
type fnret = struct {
fname: str,
fmod: str,
rtype: *node,
params: *node,
frnext: *fnret,
@@ -19658,8 +19674,9 @@ fn collectfnrets(c: *cgen, file: *node) void = {
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_FNDECL) {
let f: *fnret = amalloc(c.a, 48u64): *fnret;
let f: *fnret = amalloc(c.a, 64u64): *fnret;
f.fname = d.str;
f.fmod = d.module;
f.rtype = d.lhs;
f.params = d.list;
f.frnext = c.fnrets;
@@ -19692,6 +19709,25 @@ fn fnparamslookup(c: *cgen, name: str) *node = {
return nil;
};
// fnparamslookupmod — same-module-first leaf walk. Module-qualified
// `mod.fn(...)` calls go through this so a leaf collision (multiple
// modules export the same name, e.g. `os.read` and `io.read`) resolves
// to the explicit module. Falls back to the first leaf match if no
// matching module is registered — mirrors aliaslookup's two-pass shape
// (cgen.ww:75, fixed in #27).
fn fnparamslookupmod(c: *cgen, name: str, mod: str) *node = {
if (mod.len > 0) {
let f: *fnret = c.fnrets;
for (f != nil) {
if (streq(f.fname, name)) {
if (streq(f.fmod, mod)) { return f.params; };
};
f = f.frnext;
};
};
return fnparamslookup(c, name);
};
// ---- def-constant registry ------------------------------------------
//
// `def NAME: T = LIT;` becomes a DATA symbol the C-side w6c emits; an

View File

@@ -1515,6 +1515,7 @@ fn emitdatasection(c: *cgen) void = {
type fnret = struct {
fname: str,
fmod: str,
rtype: *node,
params: *node,
frnext: *fnret,
@@ -1525,8 +1526,9 @@ fn collectfnrets(c: *cgen, file: *node) void = {
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_FNDECL) {
let f: *fnret = amalloc(c.a, 48u64): *fnret;
let f: *fnret = amalloc(c.a, 64u64): *fnret;
f.fname = d.str;
f.fmod = d.module;
f.rtype = d.lhs;
f.params = d.list;
f.frnext = c.fnrets;
@@ -1559,6 +1561,25 @@ fn fnparamslookup(c: *cgen, name: str) *node = {
return nil;
};
// fnparamslookupmod — same-module-first leaf walk. Module-qualified
// `mod.fn(...)` calls go through this so a leaf collision (multiple
// modules export the same name, e.g. `os.read` and `io.read`) resolves
// to the explicit module. Falls back to the first leaf match if no
// matching module is registered — mirrors aliaslookup's two-pass shape
// (cgen.ww:75, fixed in #27).
fn fnparamslookupmod(c: *cgen, name: str, mod: str) *node = {
if (mod.len > 0) {
let f: *fnret = c.fnrets;
for (f != nil) {
if (streq(f.fname, name)) {
if (streq(f.fmod, mod)) { return f.params; };
};
f = f.frnext;
};
};
return fnparamslookup(c, name);
};
// ---- def-constant registry ------------------------------------------
//
// `def NAME: T = LIT;` becomes a DATA symbol the C-side w6c emits; an

View File

@@ -2856,13 +2856,28 @@ fn cgcall(c: *cgen, n: *node) void = {
// Look up the callee's declared params for tagged-union widening.
// fn-pointer calls (callee is a local) don't get widening — the
// user must build the tagged value explicitly. Matches the most
// common case (direct named calls).
// user must build the tagged value explicitly.
//
// N_DOT (`mod.fn(...)`) covers cross-module calls; pre-#28 wwstage
// only handled N_IDENT, leaving N_DOT calls without widening
// detection — pushargsrev then fell through to the N_IDENT-slice
// fast path and dropped the variant tag word on widened slice args.
// Cstage finds params via the checker-set `n->lhs->type`, sidestepping
// the name-driven registry entirely (cmd/w6c/cgen.c:4161-4165).
let calleeparams: *node = nil;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
calleeparams = fnparamslookup(c, callee.str);
};
} else { if (callee.kind == nkind.N_DOT) {
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
calleeparams = fnparamslookupmod(c, callee.str, cmod);
}; };
};
// Hare-style variadic last param: gather N tail args into a
// frame-resident [N]T (`@vararg_d_<seq>`) plus a 24B slice

View File

@@ -12954,13 +12954,28 @@ fn cgcall(c: *cgen, n: *node) void = {
// Look up the callee's declared params for tagged-union widening.
// fn-pointer calls (callee is a local) don't get widening — the
// user must build the tagged value explicitly. Matches the most
// common case (direct named calls).
// user must build the tagged value explicitly.
//
// N_DOT (`mod.fn(...)`) covers cross-module calls; pre-#28 wwstage
// only handled N_IDENT, leaving N_DOT calls without widening
// detection — pushargsrev then fell through to the N_IDENT-slice
// fast path and dropped the variant tag word on widened slice args.
// Cstage finds params via the checker-set `n->lhs->type`, sidestepping
// the name-driven registry entirely (cmd/w6c/cgen.c:4161-4165).
let calleeparams: *node = nil;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
calleeparams = fnparamslookup(c, callee.str);
};
} else { if (callee.kind == nkind.N_DOT) {
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
calleeparams = fnparamslookupmod(c, callee.str, cmod);
}; };
};
// Hare-style variadic last param: gather N tail args into a
// frame-resident [N]T (`@vararg_d_<seq>`) plus a 24B slice
@@ -19648,6 +19663,7 @@ fn emitdatasection(c: *cgen) void = {
type fnret = struct {
fname: str,
fmod: str,
rtype: *node,
params: *node,
frnext: *fnret,
@@ -19658,8 +19674,9 @@ fn collectfnrets(c: *cgen, file: *node) void = {
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_FNDECL) {
let f: *fnret = amalloc(c.a, 48u64): *fnret;
let f: *fnret = amalloc(c.a, 64u64): *fnret;
f.fname = d.str;
f.fmod = d.module;
f.rtype = d.lhs;
f.params = d.list;
f.frnext = c.fnrets;
@@ -19692,6 +19709,25 @@ fn fnparamslookup(c: *cgen, name: str) *node = {
return nil;
};
// fnparamslookupmod — same-module-first leaf walk. Module-qualified
// `mod.fn(...)` calls go through this so a leaf collision (multiple
// modules export the same name, e.g. `os.read` and `io.read`) resolves
// to the explicit module. Falls back to the first leaf match if no
// matching module is registered — mirrors aliaslookup's two-pass shape
// (cgen.ww:75, fixed in #27).
fn fnparamslookupmod(c: *cgen, name: str, mod: str) *node = {
if (mod.len > 0) {
let f: *fnret = c.fnrets;
for (f != nil) {
if (streq(f.fname, name)) {
if (streq(f.fmod, mod)) { return f.params; };
};
f = f.frnext;
};
};
return fnparamslookup(c, name);
};
// ---- def-constant registry ------------------------------------------
//
// `def NAME: T = LIT;` becomes a DATA symbol the C-side w6c emits; an