lang: opt-in UFCS — ufcs-marked fns + alias dot-dispatch, generic binding via receiver; one binding builder for plan-side generic returns
This commit is contained in:
@@ -10,9 +10,9 @@
|
|||||||
Counter :: struct { n: s32; }
|
Counter :: struct { n: s32; }
|
||||||
|
|
||||||
// FREE functions (defined outside the struct), pointer first param.
|
// FREE functions (defined outside the struct), pointer first param.
|
||||||
bump :: (c: *Counter) -> s32 { c.n += 1; return c.n; }
|
bump :: ufcs (c: *Counter) -> s32 { c.n += 1; return c.n; }
|
||||||
// reached ONLY via UFCS — must still be emitted.
|
// reached ONLY via UFCS — must still be emitted.
|
||||||
reset :: (c: *Counter) { c.n = 0; }
|
reset :: ufcs (c: *Counter) { c.n = 0; }
|
||||||
|
|
||||||
main :: () -> s32 {
|
main :: () -> s32 {
|
||||||
c := Counter.{ n = 10 };
|
c := Counter.{ n = 10 };
|
||||||
|
|||||||
35
examples/0053-basic-ufcs-opt-in.sx
Normal file
35
examples/0053-basic-ufcs-opt-in.sx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Free-function dot-calls are OPT-IN. Two opt-in spellings:
|
||||||
|
// name :: ufcs (params) { body } — the fn itself is dot-callable
|
||||||
|
// name :: ufcs target; — dot-callable (renaming) alias
|
||||||
|
// A plain fn is callable directly or via `|>` only (see 1166 for the
|
||||||
|
// rejection). Generic ufcs fns dispatch through normal monomorphization,
|
||||||
|
// and the plan-side return type binds from the receiver (structured
|
||||||
|
// params like `[]$T` included).
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
bump :: (x: s64) -> s64 { x + 1 }
|
||||||
|
bump2 :: ufcs (x: s64) -> s64 { x + 2 }
|
||||||
|
bump3 :: ufcs bump;
|
||||||
|
|
||||||
|
Counter :: struct { n: s64; }
|
||||||
|
inc :: ufcs (c: *Counter, by: s64) -> s64 { c.n += by; c.n }
|
||||||
|
|
||||||
|
gfirst :: ufcs (xs: []$T) -> T { xs[0] }
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
f : s64 = 40;
|
||||||
|
print("marked: {}\n", f.bump2()); // 42
|
||||||
|
print("alias: {}\n", f.bump3()); // 41
|
||||||
|
print("direct: {}\n", bump(f)); // 41 — plain fn, direct
|
||||||
|
print("pipe: {}\n", f |> bump()); // 41 — plain fn, pipe
|
||||||
|
print("marked-direct: {}\n", bump2(f)); // 42 — marked fn callable directly
|
||||||
|
|
||||||
|
c := Counter.{ n = 10 };
|
||||||
|
print("ptr-recv: {}\n", c.inc(5)); // 15 — auto address-of receiver
|
||||||
|
|
||||||
|
arr := .[7, 8, 9];
|
||||||
|
xs : []s64 = arr;
|
||||||
|
print("generic-dot: {}\n", xs.gfirst()); // 7
|
||||||
|
print("generic-direct: {}\n", gfirst(xs)); // 7 — plan types it s64, not a T stub
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
Hasher :: struct { total: s64 = 0; count: s64 = 0; }
|
Hasher :: struct { total: s64 = 0; count: s64 = 0; }
|
||||||
|
|
||||||
update :: (self: *Hasher, n: s64) {
|
update :: ufcs (self: *Hasher, n: s64) {
|
||||||
self.total += n;
|
self.total += n;
|
||||||
self.count += 1;
|
self.count += 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// a.sx authors `bump` adding 1. Imported first → first-wins winner. `from_a`'s
|
// a.sx authors `bump` adding 1. Imported first → first-wins winner. `from_a`'s
|
||||||
// `v.bump()` resolves a.sx's own author (own == winner → existing UFCS path,
|
// `v.bump()` resolves a.sx's own author (own == winner → existing UFCS path,
|
||||||
// byte-for-byte unchanged).
|
// byte-for-byte unchanged).
|
||||||
bump :: (x: s64) -> s64 { return x + 1; }
|
bump :: ufcs (x: s64) -> s64 { return x + 1; }
|
||||||
from_a_ufcs :: () -> s64 { v : s64 = 10; return v.bump(); }
|
from_a_ufcs :: () -> s64 { v : s64 = 10; return v.bump(); }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// b.sx authors its OWN `bump` adding 100. `from_b`'s `v.bump()` must dispatch
|
// b.sx authors its OWN `bump` adding 100. `from_b`'s `v.bump()` must dispatch
|
||||||
// b.sx's author (+100 → 110), not the first-wins winner from a.sx (+1).
|
// b.sx's author (+100 → 110), not the first-wins winner from a.sx (+1).
|
||||||
bump :: (x: s64) -> s64 { return x + 100; }
|
bump :: ufcs (x: s64) -> s64 { return x + 100; }
|
||||||
from_b_ufcs :: () -> s64 { v : s64 = 10; return v.bump(); }
|
from_b_ufcs :: () -> s64 { v : s64 = 10; return v.bump(); }
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
// a.sx authors `dup` (+1). One of two distinct flat authors of `dup`.
|
// a.sx authors `dup` (+1). One of two distinct flat authors of `dup`.
|
||||||
dup :: (x: s64) -> s64 { return x + 1; }
|
dup :: ufcs (x: s64) -> s64 { return x + 1; }
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
// b.sx authors its OWN `dup` (+2) — the second distinct flat author. Main
|
// b.sx authors its OWN `dup` (+2) — the second distinct flat author. Main
|
||||||
// imports both and authors neither, so `v.dup()` from main is ambiguous.
|
// imports both and authors neither, so `v.dup()` from main is ambiguous.
|
||||||
dup :: (x: s64) -> s64 { return x + 2; }
|
dup :: ufcs (x: s64) -> s64 { return x + 2; }
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
// a.sx authors `tag` returning a string; imported first → first-wins winner.
|
// a.sx authors `tag` returning a string; imported first → first-wins winner.
|
||||||
// `show_a`'s `v.tag()` is the caller's OWN author (own == winner → existing UFCS
|
// `show_a`'s `v.tag()` is the caller's OWN author (own == winner → existing UFCS
|
||||||
// path, byte-for-byte unchanged): typed AND dispatched as a.tag (string).
|
// path, byte-for-byte unchanged): typed AND dispatched as a.tag (string).
|
||||||
tag :: (x: s64) -> string { return "a-string"; }
|
tag :: ufcs (x: s64) -> string { return "a-string"; }
|
||||||
show_a :: () { v : s64 = 10; print("a: v.tag() = {}\n", v.tag()); }
|
show_a :: () { v : s64 = 10; print("a: v.tag() = {}\n", v.tag()); }
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
// dispatched AND typed as b.tag (s64 = 110), not the first-wins winner from a.sx
|
// dispatched AND typed as b.tag (s64 = 110), not the first-wins winner from a.sx
|
||||||
// (string). `print` types each arg from the call plan, so a mistype here boxes
|
// (string). `print` types each arg from the call plan, so a mistype here boxes
|
||||||
// the s64 as a string pointer → segfault before the fix.
|
// the s64 as a string pointer → segfault before the fix.
|
||||||
tag :: (x: s64) -> s64 { return x + 100; }
|
tag :: ufcs (x: s64) -> s64 { return x + 100; }
|
||||||
show_b :: () { v : s64 = 10; print("b: v.tag() = {}\n", v.tag()); }
|
show_b :: () { v : s64 = 10; print("b: v.tag() = {}\n", v.tag()); }
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// Typed allocation helpers over the Allocator protocol (std/mem.sx):
|
// Typed allocation helpers over the Allocator protocol (std/mem.sx):
|
||||||
// create/destroy (one T), alloc/free (slices), clone, resize, and the
|
// create/destroy (one T), alloc/free (slices), clone, resize, and the
|
||||||
// bytes-level mem_realloc. Free functions — direct calls and the
|
// bytes-level mem_realloc. Declared `ufcs` — the dot spelling
|
||||||
// fluent pipe spelling (`context.allocator |> create(Session)`) hit
|
// (`context.allocator.create(Session)`), the pipe spelling, and the
|
||||||
// the same generic machinery. Contents are UNINITIALISED by design
|
// direct call all hit the same generic machinery. Contents are
|
||||||
|
// UNINITIALISED by design
|
||||||
// (Zig-aligned): assign before reading. TrackingAllocator balances to
|
// (Zig-aligned): assign before reading. TrackingAllocator balances to
|
||||||
// zero across every pair.
|
// zero across every pair.
|
||||||
|
|
||||||
@@ -28,16 +29,22 @@ main :: () {
|
|||||||
print("pipe-create: {}\n", p.id);
|
print("pipe-create: {}\n", p.id);
|
||||||
a |> destroy(p);
|
a |> destroy(p);
|
||||||
|
|
||||||
|
// create — canonical dot spelling on context.allocator
|
||||||
|
q := context.allocator.create(Session);
|
||||||
|
q.id = 2;
|
||||||
|
print("dot-create: {}\n", q.id);
|
||||||
|
context.allocator.destroy(q);
|
||||||
|
|
||||||
// alloc / free — typed slice
|
// alloc / free — typed slice
|
||||||
xs := a |> alloc(s64, 4);
|
xs := a |> alloc(s64, 4);
|
||||||
xs[0] = 10; xs[1] = 20; xs[2] = 30; xs[3] = 40;
|
xs[0] = 10; xs[1] = 20; xs[2] = 30; xs[3] = 40;
|
||||||
print("alloc: {} {} len={}\n", xs[0], xs[3], xs.len);
|
print("alloc: {} {} len={}\n", xs[0], xs[3], xs.len);
|
||||||
|
|
||||||
// clone — independent copy
|
// clone — independent copy (canonical dot spelling)
|
||||||
ys := xs |> clone(a);
|
ys := xs.clone(a);
|
||||||
xs[0] = 99;
|
xs[0] = 99;
|
||||||
print("clone: {} (orig {})\n", ys[0], xs[0]);
|
print("clone: {} (orig {})\n", ys[0], xs[0]);
|
||||||
a |> free(ys);
|
a.free(ys);
|
||||||
|
|
||||||
// resize — grow (copies, old backing freed)
|
// resize — grow (copies, old backing freed)
|
||||||
zs := xs |> resize(a, 6);
|
zs := xs |> resize(a, 6);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Box :: struct { total: s64 = 0; count: s64 = 0; }
|
Box :: struct { total: s64 = 0; count: s64 = 0; }
|
||||||
|
|
||||||
update :: (self: *Box, n: s64) {
|
update :: ufcs (self: *Box, n: s64) {
|
||||||
self.total += n;
|
self.total += n;
|
||||||
self.count += 1;
|
self.count += 1;
|
||||||
}
|
}
|
||||||
|
|||||||
11
examples/1166-diagnostics-ufcs-not-opted-in.sx
Normal file
11
examples/1166-diagnostics-ufcs-not-opted-in.sx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// A dot-call on a PLAIN free function (no `ufcs` marker, no alias) is
|
||||||
|
// rejected with a tailored help: direct call, pipe, or declare it ufcs.
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
bump :: (x: s64) -> s64 { x + 1 }
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
f : s64 = 40;
|
||||||
|
print("{}\n", f.bump());
|
||||||
|
}
|
||||||
1
examples/expected/0053-basic-ufcs-opt-in.exit
Normal file
1
examples/expected/0053-basic-ufcs-opt-in.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
1
examples/expected/0053-basic-ufcs-opt-in.stderr
Normal file
1
examples/expected/0053-basic-ufcs-opt-in.stderr
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
8
examples/expected/0053-basic-ufcs-opt-in.stdout
Normal file
8
examples/expected/0053-basic-ufcs-opt-in.stdout
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
marked: 42
|
||||||
|
alias: 41
|
||||||
|
direct: 41
|
||||||
|
pipe: 41
|
||||||
|
marked-direct: 42
|
||||||
|
ptr-recv: 15
|
||||||
|
generic-dot: 7
|
||||||
|
generic-direct: 7
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
create: 7 42
|
create: 7 42
|
||||||
pipe-create: 1
|
pipe-create: 1
|
||||||
|
dot-create: 2
|
||||||
alloc: 10 40 len=4
|
alloc: 10 40 len=4
|
||||||
clone: 10 (orig 99)
|
clone: 10 (orig 99)
|
||||||
resize: 20 60 len=6
|
resize: 20 60 len=6
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
10
examples/expected/1166-diagnostics-ufcs-not-opted-in.stderr
Normal file
10
examples/expected/1166-diagnostics-ufcs-not-opted-in.stderr
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
error: 'bump' is not a ufcs function — a plain function does not dispatch via dot-call
|
||||||
|
--> examples/1166-diagnostics-ufcs-not-opted-in.sx:10:19
|
||||||
|
|
|
||||||
|
10 | print("{}\n", f.bump());
|
||||||
|
| ^^^^^^
|
||||||
|
|
||||||
|
help: call it directly (`bump(receiver, ...)`), pipe it (`receiver |> bump(...)`), or declare it `bump :: ufcs (...) { ... }`
|
||||||
|
|
|
||||||
|
10 | print("{}\n", f.bump());
|
||||||
|
| ^^^^^^
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -1,20 +1,24 @@
|
|||||||
# 0119 — UFCS dot-call on a GENERIC free function: "unresolved '<name>'"
|
# 0119 — UFCS dot-call on a GENERIC free function: "unresolved '<name>'"
|
||||||
|
|
||||||
> **RESOLVED — not a bug** (2026-06-11, Agra language ruling, same
|
> **RESOLVED** (2026-06-11, same session — Agra language ruling + the
|
||||||
> session). Dot-form UFCS on generic free functions is NOT the language
|
> opt-in implementation). Final model: free-function dot-calls are
|
||||||
> contract: UFCS free-function dot-dispatch is the annotated mechanism
|
> OPT-IN. `name :: ufcs (params) { body }` (new declaration form) and
|
||||||
> (`name :: ufcs target;`, concrete targets), and the FLUENT spelling
|
> `name :: ufcs target;` (alias) both opt in; a PLAIN fn never
|
||||||
> for free functions is the pipe operator — `xs |> first_of()` desugars
|
> dot-dispatches (tailored diagnostic steers to direct / `|>` /
|
||||||
> at parse time to `first_of(xs)`, which dispatches generics through
|
> marking it ufcs). Generic ufcs fns dispatch via dot with the
|
||||||
> the normal monomorphization machinery (verified:
|
> receiver participating in `$T` binding; a protocol-typed receiver
|
||||||
> `context.allocator |> create(Session)` works). specs.md §UFCS
|
> dispatches its own methods first and falls through to ufcs fns
|
||||||
> corrected (it overstated "works with ... generic functions" for the
|
> (`context.allocator.create(Session)` works). Bonus root-cause fix:
|
||||||
> dot form). No compiler change. MEM Phase 2.2 unblocked — helpers
|
> plan-side `inferGenericReturnType` now delegates to the SAME
|
||||||
> shipped as free functions with the direct + `|>` contract
|
> `buildTypeBindings` the monomorphizer uses, so structured generic
|
||||||
> (examples/0838-memory-helpers.sx pins both spellings).
|
> params (`[]$T`) type direct calls correctly too (was a `T{}` stub
|
||||||
> Residual corner (not contracted, unfiled): a `ufcs` ALIAS naming a
|
> through print's Any boxing — pre-existing). The previously-implicit
|
||||||
> generic target also doesn't dot-dispatch; file separately if ever
|
> unannotated dot-dispatch was REMOVED (inverted vs the model);
|
||||||
> wanted.
|
> in-tree reliance was 6 example files (audited; migrated to marked
|
||||||
|
> form), zero in m3te/game. specs.md §UFCS rewritten around the
|
||||||
|
> opt-in matrix. Regression: examples/0053-basic-ufcs-opt-in.sx +
|
||||||
|
> 1166-diagnostics-ufcs-not-opted-in.sx; mem helpers marked ufcs
|
||||||
|
> (0838 pins dot + pipe + direct). Suite 585/585.
|
||||||
|
|
||||||
## Symptom
|
## Symptom
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,16 @@
|
|||||||
//
|
//
|
||||||
// The user-facing allocation surface over the Allocator protocol's
|
// The user-facing allocation surface over the Allocator protocol's
|
||||||
// bytes-level primitives (`alloc_bytes` / `dealloc_bytes`). Free
|
// bytes-level primitives (`alloc_bytes` / `dealloc_bytes`). Free
|
||||||
// functions — call directly or fluently via the pipe operator:
|
// functions declared `ufcs` — dot-call, pipe, or call directly:
|
||||||
//
|
//
|
||||||
// s := context.allocator |> create(Session);
|
// s := context.allocator.create(Session);
|
||||||
// s.* = Session.{}; // no zero-init (Zig-aligned)
|
// s.* = Session.{}; // no zero-init (Zig-aligned)
|
||||||
// defer context.allocator |> destroy(s);
|
// defer context.allocator.destroy(s);
|
||||||
//
|
//
|
||||||
// moves := context.allocator |> alloc(Move, 64);
|
// moves := context.allocator.alloc(Move, 64);
|
||||||
// defer context.allocator |> free(moves);
|
// defer context.allocator.free(moves);
|
||||||
//
|
//
|
||||||
// copied := bytes |> clone(context.allocator);
|
// copied := bytes.clone(context.allocator);
|
||||||
//
|
//
|
||||||
// Bodies are complete for the 2-method protocol era: `mem_realloc` is
|
// Bodies are complete for the 2-method protocol era: `mem_realloc` is
|
||||||
// alloc+copy+dealloc (the only shape without resize/remap primitives),
|
// alloc+copy+dealloc (the only shape without resize/remap primitives),
|
||||||
@@ -21,17 +21,17 @@
|
|||||||
// allocation lands with the protocol expansion.
|
// allocation lands with the protocol expansion.
|
||||||
|
|
||||||
// Allocate one T. Contents are UNINITIALISED — assign before reading.
|
// Allocate one T. Contents are UNINITIALISED — assign before reading.
|
||||||
create :: (a: Allocator, $T: Type) -> *T {
|
create :: ufcs (a: Allocator, $T: Type) -> *T {
|
||||||
xx a.alloc_bytes(size_of(T))
|
xx a.alloc_bytes(size_of(T))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Free a *T obtained from `create`.
|
// Free a *T obtained from `create`.
|
||||||
destroy :: (a: Allocator, ptr: *$T) {
|
destroy :: ufcs (a: Allocator, ptr: *$T) {
|
||||||
a.dealloc_bytes(xx ptr);
|
a.dealloc_bytes(xx ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate a []T of `count` elements. Contents are UNINITIALISED.
|
// Allocate a []T of `count` elements. Contents are UNINITIALISED.
|
||||||
alloc :: (a: Allocator, $T: Type, count: s64) -> []T {
|
alloc :: ufcs (a: Allocator, $T: Type, count: s64) -> []T {
|
||||||
raw := a.alloc_bytes(count * size_of(T));
|
raw := a.alloc_bytes(count * size_of(T));
|
||||||
s : []T = ---;
|
s : []T = ---;
|
||||||
s.ptr = xx raw;
|
s.ptr = xx raw;
|
||||||
@@ -40,12 +40,12 @@ alloc :: (a: Allocator, $T: Type, count: s64) -> []T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Free a []T obtained from `alloc` / `clone` / `resize`.
|
// Free a []T obtained from `alloc` / `clone` / `resize`.
|
||||||
free :: (a: Allocator, slice: []$T) {
|
free :: ufcs (a: Allocator, slice: []$T) {
|
||||||
a.dealloc_bytes(xx slice.ptr);
|
a.dealloc_bytes(xx slice.ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy a slice into fresh storage owned by `a`.
|
// Copy a slice into fresh storage owned by `a`.
|
||||||
clone :: (src: []$T, a: Allocator) -> []T {
|
clone :: ufcs (src: []$T, a: Allocator) -> []T {
|
||||||
raw := a.alloc_bytes(src.len * size_of(T));
|
raw := a.alloc_bytes(src.len * size_of(T));
|
||||||
memcpy(raw, xx src.ptr, src.len * size_of(T));
|
memcpy(raw, xx src.ptr, src.len * size_of(T));
|
||||||
s : []T = ---;
|
s : []T = ---;
|
||||||
@@ -57,7 +57,7 @@ clone :: (src: []$T, a: Allocator) -> []T {
|
|||||||
// Reallocate a slice to `new_count` elements: fresh storage, contents
|
// Reallocate a slice to `new_count` elements: fresh storage, contents
|
||||||
// copied up to min(len, new_count), old backing freed. The returned
|
// copied up to min(len, new_count), old backing freed. The returned
|
||||||
// slice replaces the operand — the old slice is dangling after this.
|
// slice replaces the operand — the old slice is dangling after this.
|
||||||
resize :: (slice: []$T, a: Allocator, new_count: s64) -> []T {
|
resize :: ufcs (slice: []$T, a: Allocator, new_count: s64) -> []T {
|
||||||
raw := a.alloc_bytes(new_count * size_of(T));
|
raw := a.alloc_bytes(new_count * size_of(T));
|
||||||
n := if slice.len < new_count then slice.len else new_count;
|
n := if slice.len < new_count then slice.len else new_count;
|
||||||
memcpy(raw, xx slice.ptr, n * size_of(T));
|
memcpy(raw, xx slice.ptr, n * size_of(T));
|
||||||
@@ -72,7 +72,7 @@ resize :: (slice: []$T, a: Allocator, new_count: s64) -> []T {
|
|||||||
// dealloc — there is no in-place grow primitive to try yet, and
|
// dealloc — there is no in-place grow primitive to try yet, and
|
||||||
// `align` beyond the heap's natural 8 is not honored until the
|
// `align` beyond the heap's natural 8 is not honored until the
|
||||||
// protocol carries alignment.
|
// protocol carries alignment.
|
||||||
mem_realloc :: (a: Allocator, ptr: *void, old: s64, new: s64, align: s64) -> *void {
|
mem_realloc :: ufcs (a: Allocator, ptr: *void, old: s64, new: s64, align: s64) -> *void {
|
||||||
raw := a.alloc_bytes(new);
|
raw := a.alloc_bytes(new);
|
||||||
n := if old < new then old else new;
|
n := if old < new then old else new;
|
||||||
memcpy(raw, ptr, n);
|
memcpy(raw, ptr, n);
|
||||||
|
|||||||
46
specs.md
46
specs.md
@@ -2279,34 +2279,50 @@ print("hello")
|
|||||||
|
|
||||||
### UFCS (Uniform Function Call Syntax)
|
### UFCS (Uniform Function Call Syntax)
|
||||||
```sx
|
```sx
|
||||||
object.func(args) // equivalent to func(object, args)
|
object.func(args) // equivalent to func(object, args) — for OPT-IN functions
|
||||||
```
|
```
|
||||||
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)`. This enables method-like syntax without dedicated method declarations.
|
Free-function dot-calls are **opt-in**: a plain function never dispatches
|
||||||
|
via dot. The `ufcs` keyword opts a function in, with two spellings —
|
||||||
|
marking the function itself, or declaring a (renaming) alias:
|
||||||
|
|
||||||
```sx
|
```sx
|
||||||
Point :: struct { x: s32; y: s32; }
|
create :: (x: s32) -> void {} // plain — NOT dot-callable
|
||||||
point_sum :: (p: Point) -> s32 { p.x + p.y; }
|
create2 :: ufcs (x: s32) -> void {} // ufcs-marked — dot-callable
|
||||||
|
create3 :: ufcs create; // ufcs alias — dot-callable
|
||||||
|
|
||||||
p := Point.{3, 4};
|
f : s32 = 4;
|
||||||
print("{}\n", p.point_sum()); // calls point_sum(p) → 7
|
f.create(); // error: 'create' is not a ufcs function (help: call it
|
||||||
|
// directly, pipe it, or declare it `create :: ufcs (...)`)
|
||||||
|
f.create2(); // works — calls create2(f)
|
||||||
|
f.create3(); // works — calls create(f) through the alias
|
||||||
|
create2(f); // a ufcs fn is still an ordinary fn: direct calls work
|
||||||
|
f |> create(); // the pipe works on ANY fn (parse-time desugar, no opt-in)
|
||||||
```
|
```
|
||||||
|
|
||||||
UFCS works with pointer receivers (auto-deref applies). Generic struct
|
When `object.func(args)` names an opted-in function and `func` is not a
|
||||||
*methods* dispatch via dot; a generic **free function** (any `$T` in its
|
field or method of `object`'s type, the compiler rewrites the call to
|
||||||
signature) is NOT dot-rewritten — call it directly or fluently via the
|
`func(object, args)`. Fields and methods take priority over ufcs
|
||||||
pipe operator, which desugars at parse time to the direct call:
|
functions; a protocol-typed receiver dispatches its own methods first and
|
||||||
|
falls through to ufcs functions for non-members
|
||||||
|
(`context.allocator.create(Session)` — `create` is a ufcs fn taking the
|
||||||
|
protocol value as its first param).
|
||||||
|
|
||||||
|
UFCS works with pointer receivers (auto-deref, and auto address-of when
|
||||||
|
the first param is `*T` and the receiver is a value) and with **generic**
|
||||||
|
functions — the receiver participates in `$T` binding and the call
|
||||||
|
monomorphizes exactly like the direct spelling:
|
||||||
|
|
||||||
```sx
|
```sx
|
||||||
first_of :: (xs: []$T) -> T { xs[0] }
|
first_of :: ufcs (xs: []$T) -> T { xs[0] }
|
||||||
|
|
||||||
|
xs.first_of(); // dot — binds $T from the receiver
|
||||||
first_of(xs); // direct
|
first_of(xs); // direct
|
||||||
xs |> first_of(); // fluent — desugars to first_of(xs)
|
xs |> first_of(); // pipe — desugars to first_of(xs)
|
||||||
```
|
```
|
||||||
|
|
||||||
If the field name exists as both a struct field and a free function, the struct field takes priority.
|
|
||||||
|
|
||||||
#### UFCS Aliases
|
#### UFCS Aliases
|
||||||
The `ufcs` keyword creates a name alias for a function, decoupling the method name from the function name:
|
The alias form decouples the method name from the function name —
|
||||||
|
useful when the bare name reads poorly in dot position:
|
||||||
```sx
|
```sx
|
||||||
arena_alloc :: (arena: *Arena, size: s64) -> *void { ... }
|
arena_alloc :: (arena: *Arena, size: s64) -> *void { ... }
|
||||||
alloc :: ufcs arena_alloc;
|
alloc :: ufcs arena_alloc;
|
||||||
|
|||||||
@@ -142,6 +142,11 @@ pub const FnDecl = struct {
|
|||||||
/// is a REQUIRED parameter, so a parser site cannot drop it; the default
|
/// is a REQUIRED parameter, so a parser site cannot drop it; the default
|
||||||
/// here serves only post-check synthesized decls (which are never raw).
|
/// here serves only post-check synthesized decls (which are never raw).
|
||||||
is_raw: bool = false,
|
is_raw: bool = false,
|
||||||
|
/// `name :: ufcs (params) { body }` — the fn opted into dot-call
|
||||||
|
/// dispatch (`recv.name(args)`). Dot-calls on free functions are
|
||||||
|
/// OPT-IN: only `is_ufcs` fns and `ufcs` aliases dispatch; a plain
|
||||||
|
/// fn is callable directly or via `|>` only.
|
||||||
|
is_ufcs: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Param = struct {
|
pub const Param = struct {
|
||||||
|
|||||||
@@ -315,6 +315,36 @@ pub const CallResolver = struct {
|
|||||||
// the plan carries `prepends_receiver`, distinct from a true
|
// the plan carries `prepends_receiver`, distinct from a true
|
||||||
// namespace call (`pkg.fn()`), which must NOT prepend.
|
// namespace call (`pkg.fn()`), which must NOT prepend.
|
||||||
if (self.objectIsValue(cfa.object)) {
|
if (self.objectIsValue(cfa.object)) {
|
||||||
|
// Free-fn dot-dispatch is OPT-IN (mirror lowerCall's gate so
|
||||||
|
// plan and dispatch agree): only a `ufcs` alias or a fn
|
||||||
|
// declared `name :: ufcs (...)` classifies as free_fn_ufcs.
|
||||||
|
// A plain fn falls through (lowering emits the tailored
|
||||||
|
// not-a-ufcs-function diagnostic).
|
||||||
|
const alias_target = self.l.program_index.ufcs_alias_map.get(cfa.field);
|
||||||
|
const eff_field = alias_target orelse cfa.field;
|
||||||
|
const ufcs_fd = self.l.program_index.fn_ast_map.get(eff_field);
|
||||||
|
const opted_in = alias_target != null or (ufcs_fd != null and ufcs_fd.?.is_ufcs);
|
||||||
|
if (!opted_in) return .{ .kind = .unresolved, .return_type = .unresolved };
|
||||||
|
// Generic ufcs target: infer the return type with the
|
||||||
|
// RECEIVER prepended so binding positions align with
|
||||||
|
// fd.params[0] (mirrors the lowering side's eff_args).
|
||||||
|
if (ufcs_fd) |fd| {
|
||||||
|
if (fd.type_params.len > 0) {
|
||||||
|
const eff_call_args = self.l.alloc.alloc(*ast.Node, c.args.len + 1) catch
|
||||||
|
return .{ .kind = .unresolved, .return_type = .unresolved };
|
||||||
|
eff_call_args[0] = cfa.object;
|
||||||
|
@memcpy(eff_call_args[1..], c.args);
|
||||||
|
var c2 = c.*;
|
||||||
|
c2.args = eff_call_args;
|
||||||
|
return .{
|
||||||
|
.kind = .free_fn_ufcs,
|
||||||
|
.return_type = self.l.genericResolver().inferGenericReturnType(fd, &c2),
|
||||||
|
.target = .{ .named = eff_field },
|
||||||
|
.prepends_receiver = true,
|
||||||
|
.expands_defaults = defaultsFor(fd, c.args.len + 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
// Value-receiver free-fn UFCS (`recv.fn(args)` → `fn(recv, args)`)
|
// Value-receiver free-fn UFCS (`recv.fn(args)` → `fn(recv, args)`)
|
||||||
// routes through the SAME author producer `selectedFreeAuthor` as a
|
// routes through the SAME author producer `selectedFreeAuthor` as a
|
||||||
// bare call, so the planned target / return type IS the author
|
// bare call, so the planned target / return type IS the author
|
||||||
@@ -335,7 +365,7 @@ pub const CallResolver = struct {
|
|||||||
},
|
},
|
||||||
.ambiguous, .none => {},
|
.ambiguous, .none => {},
|
||||||
}
|
}
|
||||||
if (self.l.resolveFuncByName(cfa.field)) |fid| {
|
if (self.l.resolveFuncByName(eff_field)) |fid| {
|
||||||
const func = &self.l.module.functions.items[@intFromEnum(fid)];
|
const func = &self.l.module.functions.items[@intFromEnum(fid)];
|
||||||
return .{
|
return .{
|
||||||
.kind = .free_fn_ufcs,
|
.kind = .free_fn_ufcs,
|
||||||
@@ -343,14 +373,14 @@ pub const CallResolver = struct {
|
|||||||
.target = .{ .func = fid },
|
.target = .{ .func = fid },
|
||||||
.prepends_receiver = true,
|
.prepends_receiver = true,
|
||||||
.prepends_ctx = func.has_implicit_ctx,
|
.prepends_ctx = func.has_implicit_ctx,
|
||||||
.expands_defaults = if (self.l.program_index.fn_ast_map.get(cfa.field)) |fd| defaultsFor(fd, c.args.len + 1) else false,
|
.expands_defaults = if (ufcs_fd) |fd| defaultsFor(fd, c.args.len + 1) else false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (self.l.program_index.fn_ast_map.get(cfa.field)) |bfd| {
|
if (ufcs_fd) |bfd| {
|
||||||
return .{
|
return .{
|
||||||
.kind = .free_fn_ufcs,
|
.kind = .free_fn_ufcs,
|
||||||
.return_type = if (bfd.return_type) |rt| self.l.resolveType(rt) else .void,
|
.return_type = if (bfd.return_type) |rt| self.l.resolveType(rt) else .void,
|
||||||
.target = .{ .named = cfa.field },
|
.target = .{ .named = eff_field },
|
||||||
.prepends_receiver = true,
|
.prepends_receiver = true,
|
||||||
.expands_defaults = defaultsFor(bfd, c.args.len + 1),
|
.expands_defaults = defaultsFor(bfd, c.args.len + 1),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -263,53 +263,16 @@ pub const GenericResolver = struct {
|
|||||||
pub fn inferGenericReturnType(self: GenericResolver, fd: *const ast.FnDecl, c: *const ast.Call) TypeId {
|
pub fn inferGenericReturnType(self: GenericResolver, fd: *const ast.FnDecl, c: *const ast.Call) TypeId {
|
||||||
if (fd.return_type == null) return .void;
|
if (fd.return_type == null) return .void;
|
||||||
|
|
||||||
// Build ALL type bindings from call args before resolving return type
|
// ONE binding builder: the same `buildTypeBindings` the lowering /
|
||||||
var tmp_bindings = std.StringHashMap(TypeId).init(self.l.alloc);
|
// monomorphization path uses, so plan-side return typing can't
|
||||||
|
// disagree with the instance actually dispatched. (The previous
|
||||||
|
// local strategies only bound BARE `$T` value params — a structured
|
||||||
|
// param (`[]$T`, `*$T`) never bound, so the planned return type of
|
||||||
|
// e.g. `gfirst(xs: []$T) -> T` was the `T` stub and print's Any
|
||||||
|
// boxing mis-tagged the value.)
|
||||||
|
var tmp_bindings = self.buildTypeBindings(fd, c.args);
|
||||||
defer tmp_bindings.deinit();
|
defer tmp_bindings.deinit();
|
||||||
|
|
||||||
for (fd.type_params) |tp| {
|
|
||||||
// Strategy 1: direct type param decl ($T: Type) — param.name == tp.name.
|
|
||||||
// Only fires when the caller actually supplied a type expression at
|
|
||||||
// that position; otherwise fall through to value-based inference.
|
|
||||||
var found = false;
|
|
||||||
for (fd.params, 0..) |param, pi| {
|
|
||||||
if (std.mem.eql(u8, param.name, tp.name)) {
|
|
||||||
if (pi < c.args.len and type_bridge.isTypeShapedAstNode(c.args[pi], &self.l.module.types)) {
|
|
||||||
const ty = self.l.resolveTypeArg(c.args[pi]);
|
|
||||||
tmp_bindings.put(tp.name, ty) catch {};
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (found) continue;
|
|
||||||
|
|
||||||
// Strategy 2: inferred from usage (a: $T, b: T) — check ALL matching params, pick widest
|
|
||||||
var inferred_ty: ?TypeId = null;
|
|
||||||
for (fd.params, 0..) |param, pi| {
|
|
||||||
if (param.type_expr.data == .type_expr) {
|
|
||||||
const te = param.type_expr.data.type_expr;
|
|
||||||
if (std.mem.eql(u8, te.name, tp.name)) {
|
|
||||||
if (pi < c.args.len) {
|
|
||||||
const arg_ty = self.l.inferExprType(c.args[pi]);
|
|
||||||
if (inferred_ty) |prev| {
|
|
||||||
if (arg_ty == .f64 and prev != .f64) {
|
|
||||||
inferred_ty = arg_ty;
|
|
||||||
} else if (arg_ty == .f32 and prev != .f64 and prev != .f32) {
|
|
||||||
inferred_ty = arg_ty;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
inferred_ty = arg_ty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (inferred_ty) |ty| {
|
|
||||||
tmp_bindings.put(tp.name, ty) catch {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve return type with whatever bindings we built. Even an
|
// Resolve return type with whatever bindings we built. Even an
|
||||||
// empty `tmp_bindings` is a valid input — non-generic literal
|
// empty `tmp_bindings` is a valid input — non-generic literal
|
||||||
// return types (e.g. `walk(..$args) -> string`) still need to
|
// return types (e.g. `walk(..$args) -> string`) still need to
|
||||||
|
|||||||
@@ -843,10 +843,16 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if receiver is a protocol type → dispatch through vtable/fn_ptrs
|
// Check if receiver is a protocol type → dispatch through
|
||||||
|
// vtable/fn_ptrs — but only for the protocol's OWN methods. A
|
||||||
|
// non-member field falls through to the free-fn ufcs machinery
|
||||||
|
// (`context.allocator.create(Session)` — a ufcs fn taking the
|
||||||
|
// protocol value as its first param).
|
||||||
if (self.getProtocolInfo(obj_ty)) |proto_info| {
|
if (self.getProtocolInfo(obj_ty)) |proto_info| {
|
||||||
|
if (protocolHasMethod(proto_info, fa.field)) {
|
||||||
return self.emitProtocolDispatch(obj, proto_info, fa.field, args.items, obj_ty);
|
return self.emitProtocolDispatch(obj, proto_info, fa.field, args.items, obj_ty);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if receiver is `?Protocol` — for sentinel-shaped
|
// Check if receiver is `?Protocol` — for sentinel-shaped
|
||||||
// optionals (Protocol has ctx as first ptr field, and a
|
// optionals (Protocol has ctx as first ptr field, and a
|
||||||
@@ -860,10 +866,12 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
|||||||
if (opt_info == .optional) {
|
if (opt_info == .optional) {
|
||||||
const pay_ty = opt_info.optional.child;
|
const pay_ty = opt_info.optional.child;
|
||||||
if (self.getProtocolInfo(pay_ty)) |proto_info| {
|
if (self.getProtocolInfo(pay_ty)) |proto_info| {
|
||||||
|
if (protocolHasMethod(proto_info, fa.field)) {
|
||||||
return self.emitProtocolDispatch(obj, proto_info, fa.field, args.items, pay_ty);
|
return self.emitProtocolDispatch(obj, proto_info, fa.field, args.items, pay_ty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var method_args = std.ArrayList(Ref).empty;
|
var method_args = std.ArrayList(Ref).empty;
|
||||||
defer method_args.deinit(self.alloc);
|
defer method_args.deinit(self.alloc);
|
||||||
@@ -993,10 +1001,11 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to resolve as bare function name (free-function UFCS:
|
// Free-function dot-call (`recv.fn(args)` → `fn(recv, args)`)
|
||||||
// `recv.fn(args)` → `fn(recv, args)`). Lazily lower the body —
|
// is OPT-IN: only a fn declared `name :: ufcs (...) {...}` or a
|
||||||
// a function reached ONLY via UFCS would otherwise be declared
|
// `name :: ufcs target;` alias dispatches. A plain fn is
|
||||||
// but never emitted (undefined symbol at link).
|
// callable directly or via `|>` only — a dot-call on one gets a
|
||||||
|
// tailored diagnostic rather than silently becoming a method.
|
||||||
//
|
//
|
||||||
// R5 §C: a free-function UFCS target with a
|
// R5 §C: a free-function UFCS target with a
|
||||||
// genuine flat same-name collision dispatches to the author the
|
// genuine flat same-name collision dispatches to the author the
|
||||||
@@ -1008,21 +1017,72 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
|||||||
// (`sel_author` / `cplan.ambiguous_collision`, computed once above)
|
// (`sel_author` / `cplan.ambiguous_collision`, computed once above)
|
||||||
// rather than re-resolving the field name. `.ambiguous` → loud
|
// rather than re-resolving the field name. `.ambiguous` → loud
|
||||||
// diagnostic; otherwise the existing first-wins lazy path.
|
// diagnostic; otherwise the existing first-wins lazy path.
|
||||||
const ufcs_fid: ?FuncId = blk_uf: {
|
const alias_target = self.program_index.ufcs_alias_map.get(fa.field);
|
||||||
|
const eff_field = alias_target orelse fa.field;
|
||||||
|
const ufcs_fd = self.program_index.fn_ast_map.get(eff_field);
|
||||||
|
const ufcs_opted_in = alias_target != null or (ufcs_fd != null and ufcs_fd.?.is_ufcs);
|
||||||
|
|
||||||
|
if (ufcs_opted_in) {
|
||||||
if (author_ambiguous) {
|
if (author_ambiguous) {
|
||||||
if (self.diagnostics) |d|
|
if (self.diagnostics) |d|
|
||||||
d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fa.field});
|
d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fa.field});
|
||||||
return Ref.none;
|
return Ref.none;
|
||||||
}
|
}
|
||||||
|
// Generic ufcs target: monomorphize with the receiver's AST
|
||||||
|
// node prepended so bindings align with fd.params[0].
|
||||||
|
if (ufcs_fd) |fd| {
|
||||||
|
if (fd.type_params.len > 0) {
|
||||||
|
var eff_args = std.ArrayList(*const Node).empty;
|
||||||
|
defer eff_args.deinit(self.alloc);
|
||||||
|
eff_args.append(self.alloc, effective_obj_node) catch unreachable;
|
||||||
|
for (c.args) |arg| eff_args.append(self.alloc, arg) catch unreachable;
|
||||||
|
var gbindings = self.genericResolver().buildTypeBindings(fd, eff_args.items);
|
||||||
|
defer gbindings.deinit();
|
||||||
|
const gmangled = self.genericResolver().mangleGenericName(eff_field, fd, &gbindings);
|
||||||
|
if (!self.lowered_functions.contains(gmangled)) {
|
||||||
|
self.monomorphizeFunction(fd, gmangled, &gbindings);
|
||||||
|
}
|
||||||
|
if (self.resolveFuncByName(gmangled)) |gfid| {
|
||||||
|
const gfunc = &self.module.functions.items[@intFromEnum(gfid)];
|
||||||
|
const gret_ty = gfunc.ret;
|
||||||
|
const gparams = gfunc.params;
|
||||||
|
// Strip type-decl slots. method_args[0] is the
|
||||||
|
// receiver (a VALUE — a type-expr receiver
|
||||||
|
// classifies as a namespace call, never here),
|
||||||
|
// so fd.params[0] is a value param.
|
||||||
|
var gvalue_args = std.ArrayList(Ref).empty;
|
||||||
|
defer gvalue_args.deinit(self.alloc);
|
||||||
|
gvalue_args.append(self.alloc, method_args.items[0]) catch unreachable;
|
||||||
|
const types_explicit = method_args.items.len == fd.params.len;
|
||||||
|
var arg_idx: usize = 1;
|
||||||
|
for (fd.params[1..]) |p| {
|
||||||
|
if (isTypeParamDecl(&p, fd.type_params)) {
|
||||||
|
if (types_explicit) arg_idx += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg_idx < method_args.items.len) {
|
||||||
|
gvalue_args.append(self.alloc, method_args.items[arg_idx]) catch unreachable;
|
||||||
|
}
|
||||||
|
arg_idx += 1;
|
||||||
|
}
|
||||||
|
self.fixupMethodReceiver(&gvalue_args, gfunc, effective_obj_node, obj_ty);
|
||||||
|
const final_args = self.prependCtxIfNeeded(gfunc, gvalue_args.items);
|
||||||
|
self.coerceCallArgs(final_args, gparams);
|
||||||
|
return self.builder.call(gfid, final_args, gret_ty);
|
||||||
|
}
|
||||||
|
return self.emitError(eff_field, c.callee.span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ufcs_fid: ?FuncId = blk_uf: {
|
||||||
if (sel_author) |sf| {
|
if (sel_author) |sf| {
|
||||||
break :blk_uf self.selectedFuncId(sf, fa.field);
|
break :blk_uf self.selectedFuncId(sf, eff_field);
|
||||||
}
|
}
|
||||||
if (self.program_index.fn_ast_map.get(fa.field)) |_| {
|
if (ufcs_fd != null) {
|
||||||
if (!self.lowered_functions.contains(fa.field)) {
|
if (!self.lowered_functions.contains(eff_field)) {
|
||||||
self.lazyLowerFunction(fa.field);
|
self.lazyLowerFunction(eff_field);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break :blk_uf self.resolveFuncByName(fa.field);
|
break :blk_uf self.resolveFuncByName(eff_field);
|
||||||
};
|
};
|
||||||
if (ufcs_fid) |fid| {
|
if (ufcs_fid) |fid| {
|
||||||
const func = &self.module.functions.items[@intFromEnum(fid)];
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
||||||
@@ -1031,12 +1091,22 @@ pub fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
|||||||
// Same implicit address-of as a struct-defined method: if the
|
// Same implicit address-of as a struct-defined method: if the
|
||||||
// free function's first param is `*T` and the receiver is a
|
// free function's first param is `*T` and the receiver is a
|
||||||
// value `T`, pass its address instead of a by-value copy
|
// value `T`, pass its address instead of a by-value copy
|
||||||
|
|
||||||
self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty);
|
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);
|
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(eff_field, c.callee.span);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A fn by this name exists but is not dot-callable: tailored help.
|
||||||
|
if (ufcs_fd != null or self.resolveFuncByName(fa.field) != null) {
|
||||||
|
if (self.diagnostics) |d| {
|
||||||
|
const id = d.addFmtId(.err, c.callee.span, "'{s}' is not a ufcs function — a plain function does not dispatch via dot-call", .{fa.field});
|
||||||
|
d.addHelpFmt(id, c.callee.span, null, "call it directly (`{s}(receiver, ...)`), pipe it (`receiver |> {s}(...)`), or declare it `{s} :: ufcs (...) {{ ... }}`", .{ fa.field, fa.field, fa.field });
|
||||||
|
}
|
||||||
|
return Ref.none;
|
||||||
|
}
|
||||||
return self.emitError(fa.field, c.callee.span);
|
return self.emitError(fa.field, c.callee.span);
|
||||||
},
|
},
|
||||||
.enum_literal => |el| {
|
.enum_literal => |el| {
|
||||||
@@ -1188,6 +1258,14 @@ pub fn prependCtxIfNeeded(self: *Lowering, callee: *const Function, args: []Ref)
|
|||||||
return new_args;
|
return new_args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn protocolHasMethod(proto_info: anytype, name: []const u8) bool {
|
||||||
|
for (proto_info.methods) |m| {
|
||||||
|
if (std.mem.eql(u8, m.name, name)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn resolveFuncByName(self: *Lowering, name: []const u8) ?FuncId {
|
pub fn resolveFuncByName(self: *Lowering, name: []const u8) ?FuncId {
|
||||||
// Check foreign name map first (e.g., "c_abs" → "abs")
|
// Check foreign name map first (e.g., "c_abs" → "abs")
|
||||||
const effective_name = self.foreign_name_map.get(name) orelse name;
|
const effective_name = self.foreign_name_map.get(name) orelse name;
|
||||||
|
|||||||
@@ -266,11 +266,18 @@ pub const Parser = struct {
|
|||||||
return self.parseUnionDecl(name, start_pos, name_is_raw);
|
return self.parseUnionDecl(name, start_pos, name_is_raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
// UFCS alias: name :: ufcs target;
|
// UFCS forms:
|
||||||
|
// name :: ufcs (params) -> ret { body } — fn declared dot-callable
|
||||||
|
// name :: ufcs target; — dot-callable alias
|
||||||
if (self.current.tag == .kw_ufcs) {
|
if (self.current.tag == .kw_ufcs) {
|
||||||
self.advance();
|
self.advance();
|
||||||
|
if (self.current.tag == .l_paren) {
|
||||||
|
const node = try self.parseFnDecl(name, name_span, name_is_raw, start_pos);
|
||||||
|
node.data.fn_decl.is_ufcs = true;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
if (self.current.tag != .identifier) {
|
if (self.current.tag != .identifier) {
|
||||||
return self.fail("expected function name after 'ufcs'");
|
return self.fail("expected '(' (a ufcs function declaration) or a function name (a ufcs alias) after 'ufcs'");
|
||||||
}
|
}
|
||||||
const target = self.tokenSlice(self.current);
|
const target = self.tokenSlice(self.current);
|
||||||
self.advance();
|
self.advance();
|
||||||
|
|||||||
Reference in New Issue
Block a user