Problem
ScopeTree::resolve_call has a case 4 fallback (ndc_analyser/src/scope.rs:534–543): when no type-compatible overload is found (exact or loose) but at least one same-name callable exists anywhere in scope, it bundles all those callables into a Binding::Dynamic but unconditionally returns StaticType::Any as the result type:
// Case 4: last-resort same-name callables
if !walk.all_by_name.is_empty() {
return ResolvedCall {
binding: Binding::Dynamic(walk.all_by_name.into_iter().map(Candidate::Scalar).collect()),
return_type: StaticType::Any, // ← always Any
};
}
This fires primarily when a function is stored in a variable of type Any (the most common cause being an unannotated parameter), or when a function is referenced across a closure boundary before its type is resolved.
Example
fn apply(f, x) { f(x) }
// ^^^^^^^^ f is Any-typed (unannotated param)
// resolve_call hits case 4 because no type-compatible signature for f,
// but there may be same-named functions in scope
// → f(x): Any
Impact
Severity: Medium. This cascades from the unannotated-parameter problem. Any function received as a parameter and then called produces Any for its result. Higher-order patterns are essentially opaque to the analyzer.
Approaches
Low-effort improvement
When all candidates in all_by_name share the same return type, use that type instead of Any. This is sound (all possible callees return the same type, so the actual return type is known statically) and captures the common case of a single function being passed as an argument.
Medium fix
Compute the LUB of all candidate return types. This is sound as an over-approximation: the actual return type is at least as specific as the LUB of all candidates in scope. In practice when there is only one candidate or all candidates return the same type, this gives an exact result.
// Proposed change in case 4:
let return_type = self.lub_scalar_returns(&walk.all_by_name);
lub_scalar_returns already exists in scope.rs and does exactly this.
Full fix (high effort)
Track the specific function value stored in each variable. This is essentially flow-sensitive type analysis / type narrowing at call sites and requires significant infrastructure.
Notes
- This issue is largely a downstream consequence of the unannotated-parameter issue. If parameters had inferred or annotated types, many callees that currently hit case 4 would instead get exact matches (case 1). But even with better parameter inference, case 4 will still arise for genuinely dynamic dispatch patterns.
- The
lub_scalar_returns medium fix has essentially zero risk of unsoundness and is a one-line change.
Problem
ScopeTree::resolve_callhas a case 4 fallback (ndc_analyser/src/scope.rs:534–543): when no type-compatible overload is found (exact or loose) but at least one same-name callable exists anywhere in scope, it bundles all those callables into aBinding::Dynamicbut unconditionally returnsStaticType::Anyas the result type:This fires primarily when a function is stored in a variable of type
Any(the most common cause being an unannotated parameter), or when a function is referenced across a closure boundary before its type is resolved.Example
Impact
Severity: Medium. This cascades from the unannotated-parameter problem. Any function received as a parameter and then called produces
Anyfor its result. Higher-order patterns are essentially opaque to the analyzer.Approaches
Low-effort improvement
When all candidates in
all_by_nameshare the same return type, use that type instead ofAny. This is sound (all possible callees return the same type, so the actual return type is known statically) and captures the common case of a single function being passed as an argument.Medium fix
Compute the LUB of all candidate return types. This is sound as an over-approximation: the actual return type is at least as specific as the LUB of all candidates in scope. In practice when there is only one candidate or all candidates return the same type, this gives an exact result.
lub_scalar_returnsalready exists inscope.rsand does exactly this.Full fix (high effort)
Track the specific function value stored in each variable. This is essentially flow-sensitive type analysis / type narrowing at call sites and requires significant infrastructure.
Notes
lub_scalar_returnsmedium fix has essentially zero risk of unsoundness and is a one-line change.