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.
This commit is contained in:
26
examples/0039-basic-free-fn-ufcs-pointer-receiver.sx
Normal file
26
examples/0039-basic-free-fn-ufcs-pointer-receiver.sx
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
a=11 b=12 n=12
|
||||||
|
after reset n=0
|
||||||
@@ -1,5 +1,16 @@
|
|||||||
# 0063 — free-function UFCS with a pointer first-param passes the struct by value
|
# 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
|
## Symptom
|
||||||
|
|
||||||
Calling a **free** function via UFCS where the function's first parameter is a
|
Calling a **free** function via UFCS where the function's first parameter is a
|
||||||
|
|||||||
@@ -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| {
|
if (self.resolveFuncByName(fa.field)) |fid| {
|
||||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||||
const ret_ty = func.ret;
|
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);
|
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.builder.call(fid, final_args, ret_ty);
|
||||||
}
|
}
|
||||||
return self.emitError(fa.field, c.callee.span);
|
return self.emitError(fa.field, c.callee.span);
|
||||||
|
|||||||
Reference in New Issue
Block a user