From 10de5a5e0e389edcdf0896a554926274116dd7d8 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 16 May 2026 00:07:00 +0900 Subject: [PATCH] selfhost+test: cgdot LEAQ-of-fn for module-qualified N_DOT (#12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wwstage cgdot lacked a TY_FN branch for module-qualified N_DOT rvalues. `let p = mod1.ping` fell through to the MOVQ/LEAQ-narrow fallback, loading 8 prologue bytes from the fn's first instruction instead of taking its address. Cstage cgdot already handled this case (wired during #9, f1440bf). Mirror cstage: gate on fnretlookup(c, fld) before the localloadop fallback; emit `LEAQ .(SB), AX` via emitfnname with the hint from lhs.str. Extend 706_fnlabel_mangle: pos.ww now stores mod1.ping/mod2.ping into local fn-pointer slots and dispatches through them in addition to the existing direct calls. Expected exit 56 → 112. Regression shape: without the new branch, MOVQ leaf(SB) loads the prologue bytes; indirect call jumps into garbage → SIGSEGV. ww2 == ww3 == ww4 byte-identical at the new emit. --- selfhost/cmd/w6c/main.combined.ww | 12 ++++++++ selfhost/cmd/wcc/cgenexpr.ww | 12 ++++++++ selfhost/cmd/wwdump/main.combined.ww | 12 ++++++++ test/wcc/706_fnlabel_mangle.c | 36 +++++++++++++----------- test/wcc/data/fnlabelmangle/mod1/mod1.ww | 10 +++---- test/wcc/data/fnlabelmangle/pos.ww | 16 +++++++---- 6 files changed, 70 insertions(+), 28 deletions(-) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 91c5a112..8e47805a 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -10723,6 +10723,18 @@ fn cgdot(c: *cgen, n: *node) void = { // fallback the C cgen takes when bt is NULL/tyerr. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { + // `let p = mod.fn` — fn rvalue via N_DOT. Mirror of + // cstage cgdot's TY_FN branch (mafn with module hint). + // 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); + if (frt != nil) { + emitline("\tLEAQ\t"); + emitfnname(c, fld, lhs.str); + emitline("(SB), AX\n"); + return; + }; let mqop: str = localloadop(c, letvartnode(c, fld)); if (streq(mqop, "MOVQ")) { emitline("\tMOVQ\t"); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 07607a0a..acc5100c 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1707,6 +1707,18 @@ fn cgdot(c: *cgen, n: *node) void = { // fallback the C cgen takes when bt is NULL/tyerr. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { + // `let p = mod.fn` — fn rvalue via N_DOT. Mirror of + // cstage cgdot's TY_FN branch (mafn with module hint). + // 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); + if (frt != nil) { + emitline("\tLEAQ\t"); + emitfnname(c, fld, lhs.str); + emitline("(SB), AX\n"); + return; + }; let mqop: str = localloadop(c, letvartnode(c, fld)); if (streq(mqop, "MOVQ")) { emitline("\tMOVQ\t"); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 73f8ddf8..0f207d15 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -10723,6 +10723,18 @@ fn cgdot(c: *cgen, n: *node) void = { // fallback the C cgen takes when bt is NULL/tyerr. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { + // `let p = mod.fn` — fn rvalue via N_DOT. Mirror of + // cstage cgdot's TY_FN branch (mafn with module hint). + // 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); + if (frt != nil) { + emitline("\tLEAQ\t"); + emitfnname(c, fld, lhs.str); + emitline("(SB), AX\n"); + return; + }; let mqop: str = localloadop(c, letvartnode(c, fld)); if (streq(mqop, "MOVQ")) { emitline("\tMOVQ\t"); diff --git a/test/wcc/706_fnlabel_mangle.c b/test/wcc/706_fnlabel_mangle.c index b2702417..55e6c114 100644 --- a/test/wcc/706_fnlabel_mangle.c +++ b/test/wcc/706_fnlabel_mangle.c @@ -3,30 +3,33 @@ * modules can each `export fn ping` (and each define a private * `fn helper`) without colliding at link time. * - * The fixture in test/wcc/data/fnlabelmangle/ pins three of the four - * label-emit sites #9 touches: + * The fixture in test/wcc/data/fnlabelmangle/ pins all four + * label-emit sites #9 / #12 touch: * mod{1,2}/mod{1,2}.ww * export fn ping = bareval + helper() + fpi(); * fn helper — private, same-leaf across modules * fn fpi — `let h = helper; h();` pins LEAQ N_IDENT * pos.ww - * return mod1.ping() + mod2.ping(); // CALL N_DOT × 2 + * let p1 = mod1.ping; let p2 = mod2.ping; // LEAQ N_DOT × 2 + * return mod1.ping() + mod2.ping() // CALL N_DOT × 2 + * + p1() + p2(); // through fn ptr * * mod1.ping = 3 + 11 + 11 = 25 * mod2.ping = 5 + 13 + 13 = 31 - * total = 56 + * total = 25 + 31 + 25 + 31 = 112 * - * Pre-fix (cgen emitted bare `TEXT ping` for both modules' exports; - * non-exported `helper` already mangled, so that half is incidental - * regression coverage), the linker collapsed both `ping` symbols and - * one dispatch landed on the wrong body. The bare-IDENT helper / - * `let h = helper` callsites would also pick whichever module the - * lookup found first, silently miscompiling fpi's intra-module call. + * Pre-#9 (cgen emitted bare `TEXT ping` for both modules' exports), + * the linker collapsed both `ping` symbols and one dispatch landed + * on the wrong body. The bare-IDENT helper / `let h = helper` call- + * sites would also pick whichever module the lookup found first, + * silently miscompiling fpi's intra-module call. + * + * Pre-#12 wwstage cgdot's `let p = mod.ping` fell through to the + * MOVQ leaf(SB) module-qualified-value fallback, loading 8 bytes + * of fn-prologue into AX; `p()` then jumped into mid-prologue + * garbage. Cstage cgdot already had a TY_FN LEAQ branch from #9. * * Coverage notes: - * - LEAQ N_DOT for fn rvalue (`let p = mod1.ping`) is not exercised. - * wwstage cgdot has no working LEAQ-of-fn N_DOT branch — a row - * would diverge across stages. Tracked as a follow-up. * - The skip rule's three remaining cases (@symbol / main / empty- * module) are not pinned by a dedicated row because every passing * run of `make test` already exercises them: the bootstrap and @@ -70,10 +73,11 @@ run_pos(const char *driver, const char *fixdir, const char *tag) snprintf(bin, sizeof bin, "%s/pos", fixdir); int got = runwait(bin); unlink(bin); - if (got != 56) { + if (got != 112) { fprintf(stderr, - "fnlabel_mangle[%s]: pos.ww exit=%d want=56 — " - "fn labels likely collapsed at link\n", tag, got); + "fnlabel_mangle[%s]: pos.ww exit=%d want=112 — " + "fn labels likely collapsed at link, or LEAQ-of-fn " + "N_DOT branch missing in cgdot\n", tag, got); return 1; } return 0; diff --git a/test/wcc/data/fnlabelmangle/mod1/mod1.ww b/test/wcc/data/fnlabelmangle/mod1/mod1.ww index 97801899..7c98302a 100644 --- a/test/wcc/data/fnlabelmangle/mod1/mod1.ww +++ b/test/wcc/data/fnlabelmangle/mod1/mod1.ww @@ -1,13 +1,11 @@ // Two modules each `export fn ping` and each define a private -// `helper` with the same leaf. Together with mod2 they pin three of -// the four label-emit sites #9 touches: +// `helper` with the same leaf. Together with mod2 they pin all four +// label-emit sites #9 touches: // - CALL N_DOT : main calls mod1.ping / mod2.ping // - CALL N_IDENT : ping bare-calls helper inside its own module // - LEAQ N_IDENT : fpi takes `helper` by value, then calls it -// -// The LEAQ N_DOT path (`let p = mod1.ping`) is intentionally not -// exercised here — wwstage cgdot has no working LEAQ-of-fn N_DOT -// branch, so a row would diverge across stages. +// - LEAQ N_DOT : main does `let p = mod1.ping` then `p()` +// (pos.ww — wired via #12 wwstage cgdot fix) fn helper() i32 = { return 11i32; }; diff --git a/test/wcc/data/fnlabelmangle/pos.ww b/test/wcc/data/fnlabelmangle/pos.ww index bc5fd2ae..e4ff471c 100644 --- a/test/wcc/data/fnlabelmangle/pos.ww +++ b/test/wcc/data/fnlabelmangle/pos.ww @@ -1,18 +1,22 @@ // Positive case: import both modules, dispatch through the explicit // module qualifier. Each module's ping = bare-value + helper() (CALL // N_IDENT) + fpi() (which exercises LEAQ N_IDENT via `let h = helper`). +// `p1` / `p2` pin the LEAQ N_DOT path (`let p = mod.ping` for fn +// rvalue) — cgdot must emit `LEAQ mod.ping(SB), AX`, not +// `MOVQ ping(SB), AX`. Pre-#12 wwstage cgdot fell through to the +// latter and `p1() / p2()` jumped into mid-prologue garbage. // // mod1.ping = 3 + 11 + 11 = 25 // mod2.ping = 5 + 13 + 13 = 31 -// total = 56 -// -// Pre-fix the four `helper` / `ping` symbols would collapse at link -// or the hint-less lookup would silently grab the wrong module's -// body — exit would land at 50/62/some other value, never 56. +// p1() = mod1.ping = 25 +// p2() = mod2.ping = 31 +// total = 112 use mod1; use mod2; fn main() i32 = { - return mod1.ping() + mod2.ping(); + let p1: fn() i32 = mod1.ping; + let p2: fn() i32 = mod2.ping; + return mod1.ping() + mod2.ping() + p1() + p2(); };