From 3f7452814b2ede0bb0982a69738c3c3f644b5356 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 8 Aug 2026 01:07:23 +0900 Subject: [PATCH] cgen: resolve local fn-ptr callees for variadic arg prep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wwstage variadic call classification was name-keyed: a fn-ptr FIELD call whose local base shadowed the current module name (lib/log's log.println(log, args...)) picked the module fn's signature — nfixed off by one, the fixed arg boxed into the gather, the spread emitted as zeros — and a no-collision fn-ptr callee missed the registry entirely, leaking the raw N_SPREAD as a single $0 word (SIGSEGV / exit 255 in 8 of 11 logtest tests on the wwstage leg). fnptrcalleetfn resolves a local fn-ptr callee (bare local or struct field) to its N_TFN once, shared by the CALL-target choice, callee_variadic_param (with the []T wrap registry params get from installparams), calleecvariadic, and the widening param lookup, so target and arg prep can never disagree. Graduates the #59.8 logtest pin — DATABYTEID_DIVERGED-era M_DIVERGE count is now zero. --- internal/wwfixture/types.ww | 8 +- selfhost/cmd/wcc/cgenexpr.ww | 60 +++------- selfhost/cmd/wcc/cgenutil.ww | 103 +++++++++++++++++- test/wcc/989_lib_byteid.c | 11 +- .../data/r598_fnptr_field_variadic/case.ww | 36 ++++++ 5 files changed, 167 insertions(+), 51 deletions(-) create mode 100644 test/wcc/data/r598_fnptr_field_variadic/case.ww diff --git a/internal/wwfixture/types.ww b/internal/wwfixture/types.ww index f65056c3..70252814 100644 --- a/internal/wwfixture/types.ww +++ b/internal/wwfixture/types.ww @@ -1,13 +1,13 @@ package wwfixture; def protocolversion: i32 = 1; -def corpuscount: i32 = 1238; +def corpuscount: i32 = 1239; def errorcount: i32 = 314; def compilecount: i32 = 12; -def runcount: i32 = 145; +def runcount: i32 = 146; def runexitcount: i32 = 767; -def nativecount: i32 = 2476; -def corpushash: str = "a78db801f50ad02de11fed981bd6d9c2d67509e69a69615744f1b2ad3ef4814d"; +def nativecount: i32 = 2478; +def corpushash: str = "369aca2f1c0081faf7297d8fa976a7374d97a07b76d2df079f7eea60660bda2a"; type directive = enum i32 { ERROR = 0, diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 20d70994..04719f6b 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -7830,7 +7830,16 @@ fn cgcall(c: *cgen, n: *syntax.node) void = { // the name-driven registry entirely (cmd/w6c/cgen.c:4161-4165). let calleeparams: *syntax.node = nil; if (callee != nil) { - if (callee.kind == syntax.nkind.N_IDENT) { + // #59.8: a LOCAL fn-ptr callee (bare local or struct-field) + // reads its own N_TFN params — the registry lookup below + // mis-hits when the local's name shadows the current module + // (lib/log's `log.println(log, args...)`), handing the + // module fn's signature to arg prep while the CALL target + // resolved the field. One resolver for both (fnptrcalleetfn). + let lft: *syntax.node = fnptrcalleetfn(c, callee); + if (lft != nil) { + calleeparams = lft.list; + } else { if (callee.kind == syntax.nkind.N_IDENT) { calleeparams = fnparamslookup(c, callee.str); } else { if (callee.kind == syntax.nkind.N_DOT) { let cmod: str; @@ -7841,7 +7850,7 @@ fn cgcall(c: *cgen, n: *syntax.node) void = { }; }; calleeparams = fnparamslookupmod(c, callee.str, cmod); - }; }; + }; }; }; }; // Hare-style variadic last param: gather N tail args into a // frame-resident [N]T (vararg_d slot) plus a 24B slice @@ -8419,48 +8428,11 @@ fn cgcall(c: *cgen, n: *syntax.node) void = { isfnptrcall = true; }; if (callee.kind == syntax.nkind.N_DOT) { - let base: *syntax.node = callee.lhs; - let fld: str = callee.str; - if (base != nil) { - if (base.kind == syntax.nkind.N_IDENT) { - let bn: str = base.str; - let lc: *local = localfindnode(c, bn); - if (lc != nil) { - let tn: *syntax.node = lc.tnode; - if (tn != nil) { - let lkind: syntax.nkind = tn.kind; - let sname: str; - sname.ptr = nil; sname.len = 0; - if (lkind == syntax.nkind.N_TNAME) { sname = tn.str; }; - if (lkind == syntax.nkind.N_TPTR) { - let inner: *syntax.node = tn.lhs; - if (inner != nil) { - if (inner.kind == syntax.nkind.N_TNAME) { sname = inner.str; }; - }; - }; - if (sname.len > 0) { - let si: *structinfo = structlookup(c, sname); - if (si != nil) { - let fi: *fieldinfo = si.fields; - for (fi != nil) { - let fn_: str = fi.fname; - if (syntax.streq(fn_, fld)) { - let ft: *syntax.node = fi.tnode; - if (ft != nil) { - if (ft.kind == syntax.nkind.N_TFN) { - isfnptrcall = true; - }; - }; - fi = nil; - } else { - fi = fi.finext; - }; - }; - }; - }; - }; - }; - }; + // #59.8: same resolver as the arg-classification sites + // (fnptrcalleetfn) so CALL target and arg prep cannot + // disagree within one call. + if (fnptrcalleetfn(c, callee) != nil) { + isfnptrcall = true; }; }; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index f959a766..18f0e35a 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -50,6 +50,72 @@ fn findvariadicparam(ps: *syntax.node, nfixed_out: *i32) *syntax.node = { return nil; }; +// fnptrcalleetfn — resolve a callee that names a LOCAL fn pointer +// (bare local `fp(...)`, or field call `w.emit(...)` where w is a +// local struct value / *struct and emit is an N_TFN field) to its +// N_TFN type node. Returns nil for every other callee shape — the +// name registry stays primary for module fns. Extracted from the +// cgcall isfnptrcall detection so the CALL-target choice and the +// arg classification key off ONE resolution and can never disagree +// within a call (#59.8: a local whose name shadows the current +// module made fnparamslookupmod match the module's own same-leaf +// variadic fn — nfixed off by one, the fixed arg boxed into the +// gather and the spread emitted as zeros; with no collision the +// lookup missed entirely and the raw N_SPREAD leaked to pushargsrev +// as a single $0 word). Cstage mirror: cgcall's callee_params from +// the checker-stamped n->lhs->type (cmd/w6c/cgen.c TY_FN cu->params). +fn fnptrcalleetfn(c: *cgen, callee: *syntax.node) *syntax.node = { + if (callee == nil) { return nil; }; + if (callee.kind == syntax.nkind.N_IDENT) { + let lc0: *local = localfindnode(c, callee.str); + if (lc0 == nil) { return nil; }; + let tn0: *syntax.node = lc0.tnode; + if (tn0 == nil) { return nil; }; + if (tn0.kind == syntax.nkind.N_TFN) { return tn0; }; + if (tn0.kind == syntax.nkind.N_TPTR) { + if (tn0.lhs != nil) { + if (tn0.lhs.kind == syntax.nkind.N_TFN) { + return tn0.lhs; + }; + }; + }; + return nil; + }; + if (callee.kind != syntax.nkind.N_DOT) { return nil; }; + let base: *syntax.node = callee.lhs; + if (base == nil) { return nil; }; + if (base.kind != syntax.nkind.N_IDENT) { return nil; }; + let lc: *local = localfindnode(c, base.str); + if (lc == nil) { return nil; }; + let tn: *syntax.node = lc.tnode; + if (tn == nil) { return nil; }; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (tn.kind == syntax.nkind.N_TNAME) { sname = tn.str; }; + if (tn.kind == syntax.nkind.N_TPTR) { + if (tn.lhs != nil) { + if (tn.lhs.kind == syntax.nkind.N_TNAME) { + sname = tn.lhs.str; + }; + }; + }; + if (sname.len == 0) { return nil; }; + let si: *structinfo = structlookup(c, sname); + if (si == nil) { return nil; }; + let fi: *fieldinfo = si.fields; + for (fi != nil) { + if (syntax.streq(fi.fname, callee.str)) { + let ft: *syntax.node = fi.tnode; + if (ft != nil) { + if (ft.kind == syntax.nkind.N_TFN) { return ft; }; + }; + return nil; + }; + fi = fi.finext; + }; + return nil; +}; + // callee_variadic_param — convenience wrapper: looks up the callee // by name and finds its variadic param + nfixed. Returns nil if the // callee isn't registered or has no variadic param. @@ -65,6 +131,34 @@ fn findvariadicparam(ps: *syntax.node, nfixed_out: *i32) *syntax.node = { fn callee_variadic_param(c: *cgen, callee: *syntax.node, nfixed_out: *i32) *syntax.node = { *nfixed_out = 0; if (callee == nil) { return nil; }; + // #59.8: a LOCAL fn-ptr callee resolves through its own N_TFN, + // never the name registry (which the collision shape mis-hits). + // A raw field-N_TFN variadic param carries the bare element T + // (registry params are installparams-promoted to []T); wrap it + // so the vararg_sl descriptor slot's tnode classifies as a + // slice downstream (mirror check.ww installparams / cstage + // check.c:455). + let ft: *syntax.node = fnptrcalleetfn(c, callee); + if (ft != nil) { + let vp: *syntax.node = findvariadicparam(ft.list, nfixed_out); + if (vp == nil) { return nil; }; + if (vp.lhs != nil) { + if (vp.lhs.kind != syntax.nkind.N_TSLICE) { + let w: *syntax.node = syntax.newnode( + syntax.nkind.N_PARAM, "", 0, 0); + w.str = vp.str; + w.op = vp.op; + let sl: *syntax.node = syntax.newnode( + syntax.nkind.N_TSLICE, "", 0, 0); + sl.lhs = vp.lhs; + sl.type_ = syntax.typeslice( + vp.lhs.type_: *syntax.tinfo): *void; + w.lhs = sl; + return w; + }; + }; + return vp; + }; let ps: *syntax.node = nil; if (callee.kind == syntax.nkind.N_IDENT) { if (callee.str.len == 0) { return nil; }; @@ -96,7 +190,12 @@ fn calleecvariadic(c: *cgen, callee: *syntax.node, nfixed_out: *i32) bool = { *nfixed_out = 0; if (callee == nil) { return false; }; let ps: *syntax.node = nil; - if (callee.kind == syntax.nkind.N_IDENT) { + // #59.8: a LOCAL fn-ptr callee reads its own N_TFN params — the + // name registry mis-hits when the local shadows a module name. + let lft: *syntax.node = fnptrcalleetfn(c, callee); + if (lft != nil) { + ps = lft.list; + } else { if (callee.kind == syntax.nkind.N_IDENT) { if (callee.str.len == 0) { return false; }; ps = fnparamslookup(c, callee.str); } else { if (callee.kind == syntax.nkind.N_DOT) { @@ -109,7 +208,7 @@ fn calleecvariadic(c: *cgen, callee: *syntax.node, nfixed_out: *i32) bool = { }; }; ps = fnparamslookupmod(c, callee.str, cmod); - }; }; + }; }; }; let p: *syntax.node = ps; for (p != nil) { if (p.kind == syntax.nkind.N_PARAM) { diff --git a/test/wcc/989_lib_byteid.c b/test/wcc/989_lib_byteid.c index 5c6e02e3..0c20e469 100644 --- a/test/wcc/989_lib_byteid.c +++ b/test/wcc/989_lib_byteid.c @@ -169,8 +169,17 @@ static const struct ent ents[] = { .mode = M_ID, .cite = "#59.5 graduated by the stamp-keyed recognizers" }, /* #59.6 fmt graduated to M_ID above (#129 fix) */ /* #59.7 siphash graduated to M_ID above (#61 fix) */ + /* #59.8 graduated: the wwstage variadic call classification was + * name-keyed — a local fn-ptr field callee whose base shadowed the + * current module name (lib/log's `log.println(log, args...)`) + * picked the module fn's signature (nfixed off by one; the fixed + * arg boxed into the gather, the spread emitted as zeros), and a + * no-collision fn-ptr callee missed entirely (raw N_SPREAD leaked + * as one $0 word). fnptrcalleetfn now resolves local fn-ptr + * callees for CALL target and arg prep alike. Runtime pin: + * r598_fnptr_field_variadic. */ { .fixture = "lib/log/logtest.ww", - .mode = M_DIVERGE, .cite = "#59.8" }, + .mode = M_ID, .cite = "#59.8 graduated by the local-first callee resolver" }, /* #59.9 graduated: the wwstage checker never typed an N_BIN operand * of `as`, so an enum OR-fold reached cgen unstamped and lowered as * a phantom tagged assert (unconditional exit 1). checkisas now diff --git a/test/wcc/data/r598_fnptr_field_variadic/case.ww b/test/wcc/data/r598_fnptr_field_variadic/case.ww new file mode 100644 index 00000000..daada510 --- /dev/null +++ b/test/wcc/data/r598_fnptr_field_variadic/case.ww @@ -0,0 +1,36 @@ +//ww:run +// #59.8: a variadic call through a struct fn-pointer FIELD with a +// spread-forwarded tail (`v.cb(v, args...)`). The pre-fix wwstage +// classified the callee by NAME: with no registry hit the raw +// N_SPREAD leaked to pushargsrev as a single $0 word (callee saw +// args.ptr=0), and when the base local shadowed the module name it +// picked the module fn's signature and mis-counted nfixed. +package main; + +type vt = struct { + cb: fn(v: *vt, args: i64...) void, + acc: i64, +}; + +fn sum(v: *vt, args: i64...) void = { + let i: i32 = 0; + for (i < args.len) { + v.acc += args[i]; + i += 1; + }; +}; + +fn dispatch(v: *vt, args: i64...) void = { + v.cb(v, args...); +}; + +fn main() i32 = { + let v: vt; + v.cb = sum; + v.acc = 0i64; + dispatch(&v, 1i64, 2i64, 3i64); + if (v.acc != 6i64) { return 1; }; + v.cb(&v, 10i64); + if (v.acc != 16i64) { return 2; }; + return 0; +};