Dispatch by type (formerly overloading)
There is no overload keyword. “Same name, different implementation per type” is just dispatch by type at comptime, expressed with constructs that already exist (match over type, whitelist generics), exactly as match absorbed switch and select. There are two forms, depending on whether the variations are small and together or large and separate.
Internal form: match over the type
Section titled “Internal form: match over the type”When the variations are small, a whitelist generic with match T in the body. The [...] restricts T to a closed set; the match T chooses the branch at compile time; the compiler monomorphizes and inlines:
fn to_string[T: {bool, int, float}](x: T) -> string { match T { bool => return x ? "true" : "false" int => return int_to_str(x) float => return float_to_str(x) }}
to_string(42) // T=int → int branch (resolved at compile time)to_string(true) // T=boolSince type is a first-class value (section 10), match T is the same match as always: it matches a type-value against type-literals, just like match code { 200 => ... }. And the whitelist is a closed domain, so the match T is exhaustive and forbids _ (compile error): adding a type to the whitelist breaks compilation until you handle it.
External form: function set
Section titled “External form: function set”When the implementations are large or live in different files, they live as separate functions and a set gathers them under a name:
fn bool_to_string(b: bool) -> string { ... }fn int_to_string(i: int) -> string { ... }
fn to_string = { bool_to_string, int_to_string } // the setfn name = { ... } gathers existing functions into a router dispatched by type, resolved at compile time (with no runtime cost). It is the equivalent of Odin’s proc{...}, with keywords we already have.
Mutual exclusivity rule: the signatures in the set have to be distinguishable by type, and two functions that could match the same argument are an error at the set declaration, not a tie-break by precedence. The set is a 1-to-1 router by type, never a search with implicit coercion. It is what separates this from C++’s overload-hell.
Sharing a name is only through the set. Two bare free functions with the same name — even with distinct receiver/first-parameter types — are a “defined twice” error, not an implicit overload. There is no last-wins and no by-argument-type resolution among loose definitions; if you want one name to route to several bodies, you name them separately and gather them with fn name = { ... } (or use an internal match T). This keeps name resolution unambiguous and reserves all overload-like behavior for the explicit set.
Which to use, and what not to do
Section titled “Which to use, and what not to do”Small variations that are together call for an internal match T (less boilerplate). Large or separate variations call for the external set. The old “generic vs overload” distinction becomes a choice of body: uniform logic uses a normal generic (fn f[T + Display](x: T)), and logic that differs per type uses one of the two above.
Both dispatch by type, with fixed arity. “Same name, different number of arguments” is not overload here: it is default parameter (fn f(x: int = 5), where calling f() uses the 5) or variadic (fn f(xs: ...int)), which cover the case without reintroducing overload resolution.
The ... is prefix on the parameter (fn f(xs: ...int)) and postfix on the call site, where it spreads an existing slice or tuple into positional arguments: f(...xs) passes the elements of xs as if written out one by one (so add3(...triple()) feeds a three-element result straight into a three-arity call, and f(a, ...rest) mixes fixed and spread arguments). It is the dual of the variadic parameter — one gathers, the other scatters — and the same ... token names both directions.