From 547148b8b621b3a20a9ede0fb537934e66d6f57f Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 1 Jun 2026 22:28:15 +0300 Subject: [PATCH] fix(lower): free-fn UFCS auto-address-of + lazy lowering (issue 0063) A free function called via UFCS (recv.fn(args)) whose first param is *T was passed the receiver by value (LLVM "Call parameter type does not match function signature"), and a function reached only via UFCS was declared but never emitted (undefined symbol at link). The bare-name UFCS fallback now mirrors the qualified-method path: it lazily lowers the target body and calls fixupMethodReceiver + coerceCallArgs, so the value receiver gets the same implicit address-of as a struct-defined method and mutations through *T are visible. Regression: 0039-basic-free-fn-ufcs-pointer-receiver.sx. --- ...039-basic-free-fn-ufcs-pointer-receiver.sx | 26 +++++++++++++++++++ ...9-basic-free-fn-ufcs-pointer-receiver.exit | 1 + ...basic-free-fn-ufcs-pointer-receiver.stderr | 1 + ...basic-free-fn-ufcs-pointer-receiver.stdout | 2 ++ ...e-fn-ufcs-pointer-param-passes-by-value.md | 11 ++++++++ src/ir/lower.zig | 17 +++++++++++- 6 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 examples/0039-basic-free-fn-ufcs-pointer-receiver.sx create mode 100644 examples/expected/0039-basic-free-fn-ufcs-pointer-receiver.exit create mode 100644 examples/expected/0039-basic-free-fn-ufcs-pointer-receiver.stderr create mode 100644 examples/expected/0039-basic-free-fn-ufcs-pointer-receiver.stdout diff --git a/examples/0039-basic-free-fn-ufcs-pointer-receiver.sx b/examples/0039-basic-free-fn-ufcs-pointer-receiver.sx new file mode 100644 index 0000000..ecc5811 --- /dev/null +++ b/examples/0039-basic-free-fn-ufcs-pointer-receiver.sx @@ -0,0 +1,26 @@ +// Free-function UFCS with a pointer first-param (issue 0063). `recv.fn(args)` +// on a value `recv` whose matching free function takes `*T` now takes the +// receiver's address (the same implicit address-of as a struct-defined method), +// so mutations through the pointer are visible. Also: a function reached ONLY +// via UFCS is lazily lowered (previously declared-but-never-emitted → undefined +// symbol at link). + +#import "modules/std.sx"; + +Counter :: struct { n: s32; } + +// FREE functions (defined outside the struct), pointer first param. +bump :: (c: *Counter) -> s32 { c.n += 1; return c.n; } +// reached ONLY via UFCS — must still be emitted. +reset :: (c: *Counter) { c.n = 0; } + +main :: () -> s32 { + c := Counter.{ n = 10 }; + a := c.bump(); // 11, mutates c + b := c.bump(); // 12 + print("a={} b={} n={}\n", a, b, c.n); // a=11 b=12 n=12 + + c.reset(); // UFCS-only free fn + print("after reset n={}\n", c.n); // after reset n=0 + return 0; +} diff --git a/examples/expected/0039-basic-free-fn-ufcs-pointer-receiver.exit b/examples/expected/0039-basic-free-fn-ufcs-pointer-receiver.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0039-basic-free-fn-ufcs-pointer-receiver.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0039-basic-free-fn-ufcs-pointer-receiver.stderr b/examples/expected/0039-basic-free-fn-ufcs-pointer-receiver.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0039-basic-free-fn-ufcs-pointer-receiver.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0039-basic-free-fn-ufcs-pointer-receiver.stdout b/examples/expected/0039-basic-free-fn-ufcs-pointer-receiver.stdout new file mode 100644 index 0000000..09a1392 --- /dev/null +++ b/examples/expected/0039-basic-free-fn-ufcs-pointer-receiver.stdout @@ -0,0 +1,2 @@ +a=11 b=12 n=12 +after reset n=0 diff --git a/issues/0063-free-fn-ufcs-pointer-param-passes-by-value.md b/issues/0063-free-fn-ufcs-pointer-param-passes-by-value.md index 060c53a..736d0f8 100644 --- a/issues/0063-free-fn-ufcs-pointer-param-passes-by-value.md +++ b/issues/0063-free-fn-ufcs-pointer-param-passes-by-value.md @@ -1,5 +1,16 @@ # 0063 — free-function UFCS with a pointer first-param passes the struct by value +> **✅ RESOLVED (2026-06-01).** The free-function UFCS fallback in +> [src/ir/lower.zig](../src/ir/lower.zig) ("Try to resolve as bare function +> name") built `method_args` with the value receiver but never called +> `fixupMethodReceiver`, and never lazily lowered the target — so the receiver +> was passed by value (LLVM signature mismatch) and a UFCS-only function was +> declared but never emitted (link error). Fix: that path now (1) lazily lowers +> `fa.field` if it's a known fn not yet lowered, and (2) calls +> `fixupMethodReceiver` + `coerceCallArgs` exactly like the qualified-method +> path. The explicit `bump(@p)` form was always fine. Regression: +> [examples/0039-basic-free-fn-ufcs-pointer-receiver.sx](../examples/0039-basic-free-fn-ufcs-pointer-receiver.sx). + ## Symptom Calling a **free** function via UFCS where the function's first parameter is a diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 35e6e9f..029228d 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -7444,11 +7444,26 @@ pub const Lowering = struct { } } - // Try to resolve as bare function name (method) + // Try to resolve as bare function name (free-function UFCS: + // `recv.fn(args)` → `fn(recv, args)`). Lazily lower the body — + // a function reached ONLY via UFCS would otherwise be declared + // but never emitted (issue 0063: undefined symbol at link). + if (self.fn_ast_map.get(fa.field)) |_| { + if (!self.lowered_functions.contains(fa.field)) { + self.lazyLowerFunction(fa.field); + } + } if (self.resolveFuncByName(fa.field)) |fid| { const func = &self.module.functions.items[@intFromEnum(fid)]; const ret_ty = func.ret; + const params = func.params; + // Same implicit address-of as a struct-defined method: if the + // free function's first param is `*T` and the receiver is a + // value `T`, pass its address instead of a by-value copy + // (issue 0063). + self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty); const final_args = self.prependCtxIfNeeded(func, method_args.items); + self.coerceCallArgs(final_args, params); return self.builder.call(fid, final_args, ret_ty); } return self.emitError(fa.field, c.callee.span);