3.5 KiB
0119 — UFCS dot-call on a GENERIC free function: "unresolved ''"
Symptom
obj.func(args) where func is a generic free function (any $T in its
signature — a []$T/*$T param or a $T: Type value param) fails with
unresolved '<name>'. The same call spelled directly —
func(obj, args) — compiles and runs correctly. Concrete (non-generic)
free functions rewrite through UFCS fine.
Observed (one probe, all three failures):
xs.sum_all()(concrete fn, slice receiver) → worksxs.first_of()(generic[]$Tfn, slice receiver) →unresolved 'first_of'p.pick(s32)(generic$T: Typefn, struct receiver) →unresolved 'pick'a.create(Session)(generic fn, protocol-value receiver) →unresolved 'create'
Expected: specs.md §UFCS promises the rewrite unconditionally ("When
object.func(args) is encountered and func is not a field of
object's type, the compiler rewrites the call to func(object, args)"). A generic free function called via dot must monomorphize and
dispatch exactly as the direct spelling does.
Note: issue-0040 (fixed) covered generic STRUCT METHODS via dot — that is the method path, not the free-function UFCS rewrite.
Reproduction
#import "modules/std.sx";
first_of :: (xs: []$T) -> T { xs[0] }
main :: () {
arr := .[1, 2, 3];
xs : []s64 = arr;
print("{}\n", first_of(xs)); // 1 — direct call works
print("{}\n", xs.first_of()); // error: unresolved 'first_of'
}
Investigation prompt
The UFCS rewrite lives in the call-lowering path (src/ir/calls.zig /
src/ir/lower/call.zig — the field-access-callee handling that falls
back to "func is not a field → try func(object, args)"). The fallback
resolves the bare name against DECLARED functions (resolveFuncByName /
the lowered-function registry). A generic free function is never
declared (it is a TEMPLATE in fn_ast_map, fd.type_params.len > 0,
monomorphized per call shape) — so the lookup misses and the call is
reported unresolved instead of routing through the generic machinery.
The fix likely: in the UFCS fallback, when the bare name resolves to a
fn_ast_map entry with type_params.len > 0 (the same gate
declareFunction uses), rewrite to the direct-call shape and route
through the SAME generic-call path a direct func(obj, args) takes
(mangleGenericName + binding inference from args + monomorphize). The
direct spelling already works, so the machinery exists — the UFCS arm
just never reaches it. Mind the resolution order: scope locals and
protocol/struct methods must keep winning over a same-named free
template (mirror the existing concrete-UFCS precedence), and visibility
gating (import-graph) must apply to the template exactly as for
concrete fns.
Verification:
- The repro above prints
1twice, exit 0. - Matrix probe: generic-on-struct (
p.pick(s32)), generic-on-slice (xs.first_of()), generic-on-protocol-value (a.create(Session)withcreate :: (a: Allocator, $T: Type) -> *T) all dispatch; concrete UFCS unchanged. bash tests/run_examples.sh— 582/582 baseline must hold (UFCS-heavy suite: protocols, packs, List methods).- Pin the repro as a regression example per CLAUDE.md.
Context: BLOCKS MEM Phase 2.2 — the plan's memory helpers are "free
functions in mem.sx, UFCS-callable" with canonical call sites
context.allocator.create(Session) / slice.clone(context.allocator)
(plan Appendix A). The helpers themselves work via direct calls; the
step is paused rather than shipping a direct-call-only API that the
plan would immediately re-churn.