Summary
An overloaded function works when called directly, but when its name is used as a first-class value only the last registered overload is retained. This breaks callbacks such as map(split).
Reproduction
On commit 6ec48567632dd6b134cfc7bab6ecd5429c11ce31:
print(split("a b"));
print(["a b"].map(split));
The direct call resolves the one-argument overload correctly. The callback form fails at runtime:
error[vm]: expected 2 arguments, got 1
The same problem is visible when the function is stored first:
let f = split;
print(f("a b"));
Here the analyser reports that no overload of f accepts one String, because f was inferred as the two-argument split overload.
Expected behavior
Using an overloaded function as a value should preserve its viable overloads, or use the expected callback signature to select the right overload. map invokes its callback with one element, so the one-argument split(String) overload should run and produce [["a", "b"]].
Cause
The string standard library registers these functions under the same name, in this order:
split(String) in ndc_stdlib/src/string.rs
split(String, String) via split_with_pattern
Direct call expressions go through argument-aware overload resolution. A bare identifier instead goes through ScopeTree::get_binding_any in ndc_analyser/src/scope.rs. Its name lookup uses rposition, so it selects the later two-argument binding, and ndc_analyser/src/analyser.rs records it as a single resolved scalar candidate.
The compiler and VM already support dynamic overload sets, but that path is not used for a bare overloaded identifier.
Real-world failure
This breaks the Advent of Code expression:
let ins = input.replace(",", "").lines.map(split);
An explicit wrapper works and lets the complete program finish with 1 247:
let ins = input.replace(",", "").lines.map(fn(line) => split(line));
map(words) is another workaround for this particular case.
A historical build from October 2025 runs the original expression unchanged, so this appears to have been introduced during the later type and overload resolution work.
Summary
An overloaded function works when called directly, but when its name is used as a first-class value only the last registered overload is retained. This breaks callbacks such as
map(split).Reproduction
On commit
6ec48567632dd6b134cfc7bab6ecd5429c11ce31:The direct call resolves the one-argument overload correctly. The callback form fails at runtime:
The same problem is visible when the function is stored first:
Here the analyser reports that no overload of
faccepts oneString, becausefwas inferred as the two-argumentsplitoverload.Expected behavior
Using an overloaded function as a value should preserve its viable overloads, or use the expected callback signature to select the right overload.
mapinvokes its callback with one element, so the one-argumentsplit(String)overload should run and produce[["a", "b"]].Cause
The string standard library registers these functions under the same name, in this order:
split(String)inndc_stdlib/src/string.rssplit(String, String)viasplit_with_patternDirect call expressions go through argument-aware overload resolution. A bare identifier instead goes through
ScopeTree::get_binding_anyinndc_analyser/src/scope.rs. Its name lookup usesrposition, so it selects the later two-argument binding, andndc_analyser/src/analyser.rsrecords it as a single resolved scalar candidate.The compiler and VM already support dynamic overload sets, but that path is not used for a bare overloaded identifier.
Real-world failure
This breaks the Advent of Code expression:
An explicit wrapper works and lets the complete program finish with
1 247:map(words)is another workaround for this particular case.A historical build from October 2025 runs the original expression unchanged, so this appears to have been introduced during the later type and overload resolution work.