Skip to content

Analyser: collapse single-candidate dynamic bindings to resolved at compile time #107

@timfennis

Description

@timfennis

Background

When the semantic analyser cannot statically verify that argument types match a function's signature, it emits Binding::Dynamic(Vec<ResolvedVar>) — an overload set resolved at runtime. However, if the candidates list contains exactly one entry, there is no actual dispatch decision to make: that function will always be called regardless. The runtime type check is redundant because the native function already validates its arguments and returns a meaningful error if types don't match.

Current behaviour

For a call like n - 1 where n: Any, the analyser emits Binding::Dynamic([sub_fn]) with a single candidate. At runtime this causes:

  1. Constant(OverloadSet([sub_fn])) — clones a Box<Object::OverloadSet(Vec<ResolvedVar>)> from the constant pool (heap allocation)
  2. resolve_callee() hits the OverloadSet branch and calls find_overload()
  3. find_overload() allocates a Vec<StaticType> for the argument types, iterates candidates, looks up the global, checks the type signature

All of this for a call whose target was never ambiguous.

Proposed fix

In the analyser, when building a Binding::Dynamic, check if the resulting candidates list has exactly one entry. If so, return Binding::Resolved(candidates[0]) instead.

The compiler then emits a direct GetGlobal / GetLocal for the callee, and resolve_callee() hits the fast Object::Function(f) => Ok(Some(f.clone())) path.

Precondition: The candidates list must already be filtered by arity (so a single candidate with wrong arity isn't silently accepted). Verify this before applying.

Impact on fib.ndc

In fib(n) (no type annotations → n: Any), every arithmetic operator has exactly one binary registration:

  • +, - (binary), *, / — registered as (Number, Number) → Number
  • <, >, <=, >= — registered as (Any, Any) → Bool

This means the optimisation fires for all three operators in fib (-, -, +, <), eliminating the overload resolution overhead on every call.

Trade-off

A type mismatch will produce an error from inside the native function rather than a "no matching overload" error at the call site. The error message is slightly less precise, but still meaningful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions