Skip to content

war on Any: all_by_name dynamic dispatch fallback returns Any #152

@timfennis

Description

@timfennis

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions