From 006df414aafc82c7a56c82b87064e2276352038e Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 19 May 2026 10:44:16 +0900 Subject: [PATCH] selfhost+test: route convenience-wrapper N_DOT probes via fnretlookupmod (#17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Structural close of the #4-trio convenience-wrapper audit. Session-6's #4-trio + #11/#16 graduated individual lookup helpers (fnret/fnparams/ enum/struct/def) to same-module-first via *mod variants. The close didn't enumerate every cgcall-context callsite — convenience wrappers that take a *node callee and probe its return shape via bare-leaf fnretlookup stripped the N_DOT module hint, same wedge shape as #16 (callee_variadic_param, d9b0c90) through a different family of consumers. Eight LATENT sites in selfhost/cmd/wcc fixed (each mirrors #34's nodeisslice two-arm route — N_IDENT uses cmod=c.curmod, N_DOT uses cmod=callee.lhs.str, terminal call routes through fnretlookupmod): - cgenstmt.ww cgreturn forwardtagged probe - cgenstmt.ww cgmlet tuple-return shape probe - cgenexpr.ww cgdot fn-rvalue probe (mod.fn LEAQ) - cgenexpr.ww cgtryprop succisstr probe - cgenexpr.ww cgtryunw succisstr probe - cgenutil.ww callsretsize (sret arg-prep) - cgenutil.ww inferletcalltype (let x = f()? tnode) - cgenutil.ww rhstaggedabicall N_CALL branch cstage carries no sister bug: cmd/w6c/cgen.c reads every callee return shape from the typed n->lhs->type per TY_FN sig. Mirror of #4d/#28/#31/#34/#16 cstage no-sister notes. 753_convwrap_audit: table-driven sentinel exercising cgmlet's tuple- shape probe. alpha exports foo() (i64, str); beta exports foo() (i64, i64); main calls beta.foo() — source order puts alpha LAST so alpha.foo prepends to head of c.fnrets, pre-fix bare walk picks alpha's str-branch dispatch for beta's call. Post-fix routes to beta.foo via fnretlookupmod. Asserts MOVQ\\tCX, absent in main.run TEXT (no str.len store; would fire pre-fix). Remaining 7 sites covered structurally by shape-mirror — single wedge shape, single exercise. make test 127/127; ww2==ww3==ww4 byte-id holds via 995_self_rebuild. --- Makefile | 7 + selfhost/cmd/w6c/main.combined.ww | 131 +++++++++++++--- selfhost/cmd/wcc/cgenexpr.ww | 41 ++++- selfhost/cmd/wcc/cgenstmt.ww | 36 ++++- selfhost/cmd/wcc/cgenutil.ww | 54 +++++-- selfhost/cmd/wwdump/main.combined.ww | 131 +++++++++++++--- test/wcc/753_convwrap_audit.c | 226 +++++++++++++++++++++++++++ 7 files changed, 560 insertions(+), 66 deletions(-) create mode 100644 test/wcc/753_convwrap_audit.c diff --git a/Makefile b/Makefile index 46904b97..d500b947 100644 --- a/Makefile +++ b/Makefile @@ -287,6 +287,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_mklabel_modscoped \ $(BIN)/test_vararg_seq_percall \ $(BIN)/test_modparam_callee \ + $(BIN)/test_convwrap_audit \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -734,6 +735,12 @@ $(BIN)/test_modparam_callee: test/wcc/752_modparam_callee.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_convwrap_audit: test/wcc/753_convwrap_audit.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_match_4arm_cross_module_run: test/wcc/929_match_4arm_cross_module_run.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 7ae1d07d..20b2b58f 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -8777,10 +8777,22 @@ export fn callsretsize(c: *cgen, n: *node) i32 = { if (callee == nil) { return 0; }; let cn: str; cn.ptr = nil; cn.len = 0; - if (callee.kind == nkind.N_IDENT) { cn = callee.str; }; - if (callee.kind == nkind.N_DOT) { cn = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + cn = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + cn = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cn.len == 0) { return 0; }; - let rt: *node = fnretlookup(c, cn); + let rt: *node = fnretlookupmod(c, cn, cmod); return sretretsize(c, rt); }; @@ -9023,10 +9035,22 @@ fn inferletcalltype(c: *cgen, rhs: *node) *node = { if (callee == nil) { return nil; }; let cname: str; cname.ptr = nil; cname.len = 0; - if (callee.kind == nkind.N_IDENT) { cname = callee.str; }; - if (callee.kind == nkind.N_DOT) { cname = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + cname = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + cname = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cname.len == 0) { return nil; }; - let rt: *node = fnretlookup(c, cname); + let rt: *node = fnretlookupmod(c, cname, cmod); if (rt == nil) { return nil; }; if (unwrap) { // Strip error variants — success type is the first @@ -10143,10 +10167,22 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { if (callee != nil) { let calleename: str; calleename.ptr = nil; calleename.len = 0; - if (callee.kind == nkind.N_IDENT) { calleename = callee.str; }; - if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + calleename = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + calleename = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (calleename.len > 0) { - let rt: *node = fnretlookup(c, calleename); + let rt: *node = fnretlookupmod(c, calleename, cmod); if (rt != nil) { if (istaggedtype(c, rt)) { return true; }; }; @@ -11245,10 +11281,22 @@ fn cgtryprop(c: *cgen, n: *node) void = { if (callee != nil) { let cname: str; cname.ptr = nil; cname.len = 0; - if (callee.kind == nkind.N_IDENT) { cname = callee.str; }; - if (callee.kind == nkind.N_DOT) { cname = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + cname = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + cname = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cname.len > 0) { - let rt: *node = fnretlookup(c, cname); + let rt: *node = fnretlookupmod(c, cname, cmod); if (rt != nil) { if (rt.kind == nkind.N_TTAGGED) { let first: *node = rt.list; @@ -11289,10 +11337,22 @@ fn cgtryunw(c: *cgen, n: *node) void = { if (callee != nil) { let cname: str; cname.ptr = nil; cname.len = 0; - if (callee.kind == nkind.N_IDENT) { cname = callee.str; }; - if (callee.kind == nkind.N_DOT) { cname = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + cname = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + cname = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cname.len > 0) { - let rt: *node = fnretlookup(c, cname); + let rt: *node = fnretlookupmod(c, cname, cmod); if (rt != nil) { if (rt.kind == nkind.N_TTAGGED) { let first: *node = rt.list; @@ -12822,7 +12882,10 @@ fn cgdot(c: *cgen, n: *node) void = { // Without this the MOVQ leaf(SB) fallback below would // load 8 bytes of fn-prologue code into AX instead of // the fn address. - let frt: *node = fnretlookup(c, fld); + // lhs.str is the explicit module hint so a same-leaf + // def in another module (head of c.fnrets) can't shadow + // the explicit qualifier (#17 N_DOT-arm omission audit). + let frt: *node = fnretlookupmod(c, fld, lhs.str); if (frt != nil) { emitline("\tLEAQ\t"); emitfnname(c, fld, lhs.str); @@ -16960,10 +17023,22 @@ fn cgreturn(c: *cgen, n: *node) void = { if (callee != nil) { let calleename: str; calleename.ptr = nil; calleename.len = 0; - if (callee.kind == nkind.N_IDENT) { calleename = callee.str; }; - if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + calleename = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + calleename = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (calleename.len > 0) { - let rt: *node = fnretlookup(c, calleename); + let rt: *node = fnretlookupmod(c, calleename, cmod); if (istaggedtype(c, rt)) { forwardtagged = true; }; }; }; @@ -17867,10 +17942,22 @@ fn cgmlet(c: *cgen, n: *node) void = { if (callee != nil) { let cnm: str; cnm.ptr = nil; cnm.len = 0; - if (callee.kind == nkind.N_IDENT) { cnm = callee.str; }; - if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + cnm = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + cnm = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cnm.len > 0) { - let rt: *node = fnretlookup(c, cnm); + let rt: *node = fnretlookupmod(c, cnm, cmod); if (rt != nil) { if (rt.kind == nkind.N_TTUPLE) { p0t = rt.list; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index faae00a2..e3c01aba 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -156,10 +156,22 @@ fn cgtryprop(c: *cgen, n: *node) void = { if (callee != nil) { let cname: str; cname.ptr = nil; cname.len = 0; - if (callee.kind == nkind.N_IDENT) { cname = callee.str; }; - if (callee.kind == nkind.N_DOT) { cname = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + cname = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + cname = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cname.len > 0) { - let rt: *node = fnretlookup(c, cname); + let rt: *node = fnretlookupmod(c, cname, cmod); if (rt != nil) { if (rt.kind == nkind.N_TTAGGED) { let first: *node = rt.list; @@ -200,10 +212,22 @@ fn cgtryunw(c: *cgen, n: *node) void = { if (callee != nil) { let cname: str; cname.ptr = nil; cname.len = 0; - if (callee.kind == nkind.N_IDENT) { cname = callee.str; }; - if (callee.kind == nkind.N_DOT) { cname = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + cname = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + cname = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cname.len > 0) { - let rt: *node = fnretlookup(c, cname); + let rt: *node = fnretlookupmod(c, cname, cmod); if (rt != nil) { if (rt.kind == nkind.N_TTAGGED) { let first: *node = rt.list; @@ -1733,7 +1757,10 @@ fn cgdot(c: *cgen, n: *node) void = { // Without this the MOVQ leaf(SB) fallback below would // load 8 bytes of fn-prologue code into AX instead of // the fn address. - let frt: *node = fnretlookup(c, fld); + // lhs.str is the explicit module hint so a same-leaf + // def in another module (head of c.fnrets) can't shadow + // the explicit qualifier (#17 N_DOT-arm omission audit). + let frt: *node = fnretlookupmod(c, fld, lhs.str); if (frt != nil) { emitline("\tLEAQ\t"); emitfnname(c, fld, lhs.str); diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index fd03fb12..d2d24a9e 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -169,10 +169,22 @@ fn cgreturn(c: *cgen, n: *node) void = { if (callee != nil) { let calleename: str; calleename.ptr = nil; calleename.len = 0; - if (callee.kind == nkind.N_IDENT) { calleename = callee.str; }; - if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + calleename = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + calleename = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (calleename.len > 0) { - let rt: *node = fnretlookup(c, calleename); + let rt: *node = fnretlookupmod(c, calleename, cmod); if (istaggedtype(c, rt)) { forwardtagged = true; }; }; }; @@ -1076,10 +1088,22 @@ fn cgmlet(c: *cgen, n: *node) void = { if (callee != nil) { let cnm: str; cnm.ptr = nil; cnm.len = 0; - if (callee.kind == nkind.N_IDENT) { cnm = callee.str; }; - if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + cnm = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + cnm = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cnm.len > 0) { - let rt: *node = fnretlookup(c, cnm); + let rt: *node = fnretlookupmod(c, cnm, cmod); if (rt != nil) { if (rt.kind == nkind.N_TTUPLE) { p0t = rt.list; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 6d0be67e..ba99fa50 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -1532,10 +1532,22 @@ export fn callsretsize(c: *cgen, n: *node) i32 = { if (callee == nil) { return 0; }; let cn: str; cn.ptr = nil; cn.len = 0; - if (callee.kind == nkind.N_IDENT) { cn = callee.str; }; - if (callee.kind == nkind.N_DOT) { cn = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + cn = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + cn = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cn.len == 0) { return 0; }; - let rt: *node = fnretlookup(c, cn); + let rt: *node = fnretlookupmod(c, cn, cmod); return sretretsize(c, rt); }; @@ -1778,10 +1790,22 @@ fn inferletcalltype(c: *cgen, rhs: *node) *node = { if (callee == nil) { return nil; }; let cname: str; cname.ptr = nil; cname.len = 0; - if (callee.kind == nkind.N_IDENT) { cname = callee.str; }; - if (callee.kind == nkind.N_DOT) { cname = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + cname = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + cname = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cname.len == 0) { return nil; }; - let rt: *node = fnretlookup(c, cname); + let rt: *node = fnretlookupmod(c, cname, cmod); if (rt == nil) { return nil; }; if (unwrap) { // Strip error variants — success type is the first @@ -2898,10 +2922,22 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { if (callee != nil) { let calleename: str; calleename.ptr = nil; calleename.len = 0; - if (callee.kind == nkind.N_IDENT) { calleename = callee.str; }; - if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + calleename = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + calleename = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (calleename.len > 0) { - let rt: *node = fnretlookup(c, calleename); + let rt: *node = fnretlookupmod(c, calleename, cmod); if (rt != nil) { if (istaggedtype(c, rt)) { return true; }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 91f79bc4..c4af1ed0 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -8777,10 +8777,22 @@ export fn callsretsize(c: *cgen, n: *node) i32 = { if (callee == nil) { return 0; }; let cn: str; cn.ptr = nil; cn.len = 0; - if (callee.kind == nkind.N_IDENT) { cn = callee.str; }; - if (callee.kind == nkind.N_DOT) { cn = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + cn = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + cn = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cn.len == 0) { return 0; }; - let rt: *node = fnretlookup(c, cn); + let rt: *node = fnretlookupmod(c, cn, cmod); return sretretsize(c, rt); }; @@ -9023,10 +9035,22 @@ fn inferletcalltype(c: *cgen, rhs: *node) *node = { if (callee == nil) { return nil; }; let cname: str; cname.ptr = nil; cname.len = 0; - if (callee.kind == nkind.N_IDENT) { cname = callee.str; }; - if (callee.kind == nkind.N_DOT) { cname = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + cname = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + cname = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cname.len == 0) { return nil; }; - let rt: *node = fnretlookup(c, cname); + let rt: *node = fnretlookupmod(c, cname, cmod); if (rt == nil) { return nil; }; if (unwrap) { // Strip error variants — success type is the first @@ -10143,10 +10167,22 @@ fn rhstaggedabicall(c: *cgen, src: *node) bool = { if (callee != nil) { let calleename: str; calleename.ptr = nil; calleename.len = 0; - if (callee.kind == nkind.N_IDENT) { calleename = callee.str; }; - if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + calleename = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + calleename = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (calleename.len > 0) { - let rt: *node = fnretlookup(c, calleename); + let rt: *node = fnretlookupmod(c, calleename, cmod); if (rt != nil) { if (istaggedtype(c, rt)) { return true; }; }; @@ -11245,10 +11281,22 @@ fn cgtryprop(c: *cgen, n: *node) void = { if (callee != nil) { let cname: str; cname.ptr = nil; cname.len = 0; - if (callee.kind == nkind.N_IDENT) { cname = callee.str; }; - if (callee.kind == nkind.N_DOT) { cname = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + cname = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + cname = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cname.len > 0) { - let rt: *node = fnretlookup(c, cname); + let rt: *node = fnretlookupmod(c, cname, cmod); if (rt != nil) { if (rt.kind == nkind.N_TTAGGED) { let first: *node = rt.list; @@ -11289,10 +11337,22 @@ fn cgtryunw(c: *cgen, n: *node) void = { if (callee != nil) { let cname: str; cname.ptr = nil; cname.len = 0; - if (callee.kind == nkind.N_IDENT) { cname = callee.str; }; - if (callee.kind == nkind.N_DOT) { cname = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + cname = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + cname = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cname.len > 0) { - let rt: *node = fnretlookup(c, cname); + let rt: *node = fnretlookupmod(c, cname, cmod); if (rt != nil) { if (rt.kind == nkind.N_TTAGGED) { let first: *node = rt.list; @@ -12822,7 +12882,10 @@ fn cgdot(c: *cgen, n: *node) void = { // Without this the MOVQ leaf(SB) fallback below would // load 8 bytes of fn-prologue code into AX instead of // the fn address. - let frt: *node = fnretlookup(c, fld); + // lhs.str is the explicit module hint so a same-leaf + // def in another module (head of c.fnrets) can't shadow + // the explicit qualifier (#17 N_DOT-arm omission audit). + let frt: *node = fnretlookupmod(c, fld, lhs.str); if (frt != nil) { emitline("\tLEAQ\t"); emitfnname(c, fld, lhs.str); @@ -16960,10 +17023,22 @@ fn cgreturn(c: *cgen, n: *node) void = { if (callee != nil) { let calleename: str; calleename.ptr = nil; calleename.len = 0; - if (callee.kind == nkind.N_IDENT) { calleename = callee.str; }; - if (callee.kind == nkind.N_DOT) { calleename = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + calleename = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + calleename = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (calleename.len > 0) { - let rt: *node = fnretlookup(c, calleename); + let rt: *node = fnretlookupmod(c, calleename, cmod); if (istaggedtype(c, rt)) { forwardtagged = true; }; }; }; @@ -17867,10 +17942,22 @@ fn cgmlet(c: *cgen, n: *node) void = { if (callee != nil) { let cnm: str; cnm.ptr = nil; cnm.len = 0; - if (callee.kind == nkind.N_IDENT) { cnm = callee.str; }; - if (callee.kind == nkind.N_DOT) { cnm = callee.str; }; + let cmod: str; + cmod.ptr = nil; cmod.len = 0; + if (callee.kind == nkind.N_IDENT) { + cnm = callee.str; + cmod = c.curmod; + }; + if (callee.kind == nkind.N_DOT) { + cnm = callee.str; + if (callee.lhs != nil) { + if (callee.lhs.kind == nkind.N_IDENT) { + cmod = callee.lhs.str; + }; + }; + }; if (cnm.len > 0) { - let rt: *node = fnretlookup(c, cnm); + let rt: *node = fnretlookupmod(c, cnm, cmod); if (rt != nil) { if (rt.kind == nkind.N_TTUPLE) { p0t = rt.list; diff --git a/test/wcc/753_convwrap_audit.c b/test/wcc/753_convwrap_audit.c new file mode 100644 index 00000000..f2e8e340 --- /dev/null +++ b/test/wcc/753_convwrap_audit.c @@ -0,0 +1,226 @@ +/* + * 753_convwrap_audit — sentinel for task #17 (structural close of the + * #4-trio convenience-wrapper N_DOT-arm audit). wwstage cgen* helpers + * that take a *node callee and probe its return shape via bare-leaf + * fnretlookup stripped the N_DOT module hint, same wedge shape as #16 + * (callee_variadic_param, d9b0c90) through a different family of + * consumers. Eight latent sites caught in the sweep: + * + * cgenstmt.ww:170-189 cgreturn forwardtagged probe + * cgenstmt.ww:1077-1090 cgmlet tuple-return shape probe + * cgenexpr.ww:1729-1742 cgdot fn-rvalue probe (`mod.fn` LEAQ) + * cgenexpr.ww:155-176 cgtryprop succisstr probe + * cgenexpr.ww:200-220 cgtryunw succisstr probe + * cgenutil.ww:1528-1540 callsretsize (sret arg-prep) + * cgenutil.ww:1776-1794 inferletcalltype (`let x = f()?` tnode) + * cgenutil.ww:2894-2944 rhstaggedabicall N_CALL branch + * + * Each consumed callee.str (leaf) for both N_IDENT and N_DOT arms, + * then routed through bare fnretlookup. Post-#4e the bare walk is + * same-module-first then head-walk fallback — for a cross-module + * N_DOT call from a caller whose c.curmod doesn't match either side, + * the head walk returns whichever module's same-leaf fn sits at the + * head of c.fnrets. When that head-side fn has a divergent return + * shape (tagged-vs-scalar, str-vs-scalar pair, sret-vs-flat, etc.) + * the consumer fires the wrong dispatch arm. + * + * Cstage carries no sister bug: cmd/w6c/cgen.c reads every callee + * return shape from the typed `n->lhs->type` (TY_FN sig). Mirror of + * #4d / #28 / #31 / #34 / #16: cstage sidesteps every bare-leaf + * table. + * + * Pin: row 1 exercises cgmlet (cgenstmt.ww:1077-1090). alpha exports + * a same-leaf `foo` returning (i64, str), beta exports a same-leaf + * `foo` returning (i64, i64). Source order is beta first, main next, + * alpha LAST — so alpha.foo prepends to the head of c.fnrets. Pre- + * fix wwstage's cgmlet probe walks past main (no `foo`) into the + * head and grabs alpha.foo's (i64, str) tuple, makes s0_is_str=false + * but s1_is_str=true → fires the (scalar, str) emit branch (MOVQ DX + * → b.ptr + MOVQ CX → b.len) for a beta.foo return that's actually + * (AX=i64, DX=i64) — no CX defined on this ABI. Post-fix the probe + * routes through fnretlookupmod(c, "foo", "beta") and the (scalar, + * scalar) branch fires (MOVQ AX → a, MOVQ DX → b, no CX). + * + * Asserts: inside main.run TEXT sym before its first RET, no `MOVQ + * CX,` appears — pre-fix wwstage emits the str.len store that has + * no defined source on a (i64, i64) tuple return. cstage always + * picks beta.foo via typed AST, so the same pattern check holds + * on both stages. No byte-id check — `let a, b = call();` has a + * pre-existing cosmetic cs-vs-ws divergence (cstage emits a + * defensive PUSHQ/POPQ DX around the AX-store of `a` that wwstage + * skips). Out of #17 scope. + */ +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +struct row { + const char *label; + const char *src; + const char *textsym; /* TEXT sym containing the call */ + const char *want_imm; /* substring that MUST appear */ + const char *bad_imm; /* substring that MUST NOT appear */ +}; + +/* beta.foo first (registered first → tail of c.fnrets), main next, + * alpha LAST (registered last → head). Pre-fix bare fnretlookup in + * cgmlet falls through same-module to head walk → alpha.foo → its + * (i64, str) shape drives the (scalar, str) emit branch (MOVQ CX, + * — str.len store with no source defined by beta.foo's (i64, i64) + * tuple ABI). Post-fix the probe goes through fnretlookupmod with + * "beta" hint → beta.foo → scalar-pair branch (no CX). */ +static const struct row rows[] = { + { "cgmlet_tuple_modshadow", + "package beta;\n" + "export fn foo() (i64, i64) = { return (10i64, 20i64); };\n" + "package main;\n" + "import alpha;\n" + "import beta;\n" + "export fn run() i32 = {\n" + "\tlet a, b = beta.foo();\n" + "\tif (a != 10i64) { return 1; };\n" + "\tif (b != 20i64) { return 2; };\n" + "\treturn 0;\n" + "};\n" + "export fn main() i32 = { return run(); };\n" + "package alpha;\n" + "export fn foo() (i64, str) = { return (10i64, \"hi\"); };\n", + "TEXT main.run", "MOVQ\tAX,", "MOVQ\tCX," }, +}; + +static int +slurp(const char *path, char *buf, size_t cap) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + size_t n = fread(buf, 1, cap - 1, f); + fclose(f); + buf[n] = '\0'; + return (int)n; +} + +static int +emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) +{ + char src[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/cwa_%d_%d.ww", getpid(), i); + snprintf(out_s, cap, "/tmp/cwa_%d_%d_%s.s", + getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); + int rc = runwait(cmd); + unlink(src); + return rc; +} + +static int +check_imm(const char *spath, const struct row *r, const char *stage) +{ + char buf[1 << 14]; + if (slurp(spath, buf, sizeof buf) < 0) { + fprintf(stderr, "row[%s][%s]: cannot read %s\n", + r->label, stage, spath); + return -1; + } + const char *fn = strstr(buf, r->textsym); + if (!fn) { + fprintf(stderr, "row[%s][%s]: no %s in %s\n", + r->label, stage, r->textsym, spath); + return -1; + } + const char *ret = strstr(fn, "\tRET"); + if (!ret) { + fprintf(stderr, "row[%s][%s]: no RET inside %s\n", + r->label, stage, r->textsym); + return -1; + } + const char *good = strstr(fn, r->want_imm); + if (!good || good >= ret) { + fprintf(stderr, + "row[%s][%s]: want_imm %s missing inside %s\n", + r->label, stage, r->want_imm, r->textsym); + return -1; + } + const char *bad = strstr(fn, r->bad_imm); + if (bad && bad < ret) { + fprintf(stderr, + "row[%s][%s]: bad_imm %s present inside %s — wrong-module shape probe fired\n", + r->label, stage, r->bad_imm, r->textsym); + return -1; + } + return 0; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char w6c[640], w6c_ww[640]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + + int have_ww = (access(w6c_ww, X_OK) == 0); + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + char cs_path[128], ws_path[128]; + + if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { + fprintf(stderr, + "convwrap_audit[cstage][%s]: w6c failed\n", + rows[i].label); + fail++; total++; continue; + } + total++; + if (check_imm(cs_path, &rows[i], "cstage") != 0) fail++; + + if (!have_ww) { unlink(cs_path); continue; } + + if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { + fprintf(stderr, + "convwrap_audit[wwstage][%s]: w6c_ww failed\n", + rows[i].label); + fail++; total++; + unlink(cs_path); continue; + } + total++; + if (check_imm(ws_path, &rows[i], "wwstage") != 0) fail++; + + unlink(cs_path); unlink(ws_path); + } + + if (fail) { + fprintf(stderr, + "convwrap_audit: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("convwrap_audit: %d/%d ok\n", total, total); + return 0; +}