From 3e10809d7ef873307c6baff4852223118a11c8f4 Mon Sep 17 00:00:00 2001 From: agra Date: Thu, 11 Jun 2026 15:46:49 +0300 Subject: [PATCH] =?UTF-8?q?issues:=20file=200119=20=E2=80=94=20UFCS=20gene?= =?UTF-8?q?ric=20free=20functions=20unresolved?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...9-ufcs-generic-free-function-unresolved.md | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 issues/0119-ufcs-generic-free-function-unresolved.md diff --git a/issues/0119-ufcs-generic-free-function-unresolved.md b/issues/0119-ufcs-generic-free-function-unresolved.md new file mode 100644 index 0000000..690a35e --- /dev/null +++ b/issues/0119-ufcs-generic-free-function-unresolved.md @@ -0,0 +1,80 @@ +# 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 ''`. 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) → **works** +- `xs.first_of()` (generic `[]$T` fn, slice receiver) → `unresolved 'first_of'` +- `p.pick(s32)` (generic `$T: Type` fn, 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 + +```sx +#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: +1. The repro above prints `1` twice, exit 0. +2. Matrix probe: generic-on-struct (`p.pick(s32)`), generic-on-slice + (`xs.first_of()`), generic-on-protocol-value + (`a.create(Session)` with `create :: (a: Allocator, $T: Type) -> *T`) + all dispatch; concrete UFCS unchanged. +3. `bash tests/run_examples.sh` — 582/582 baseline must hold + (UFCS-heavy suite: protocols, packs, List methods). +4. 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.