Skip to content

war on Any: unannotated function parameters default to Any #148

@timfennis

Description

@timfennis

Problem

The single largest source of Any in the analyzer. When a function is declared without parameter type annotations, every parameter is given type TypeBinding::Inferred(StaticType::Any) (ndc_analyser/src/analyser.rs:700–705). This cascades through the entire function body: arithmetic on Any-typed parameters dispatches through the all-by-name fallback, operations return Any, and the inferred return type ends up as Any too.

fn add(x, y) { x + y }
// x: Any, y: Any → x + y dispatches dynamically → return type: Any
// caller: let z = add(1, 2) → z: Any

Since most user-defined functions are unannotated, this is the root cause of most Any in practice.

Root cause

resolve_parameters_declarative in ndc_analyser/src/analyser.rs checks param.type_name != StaticType::Any to detect explicit annotations. Parameters without annotations come from the parser with type_name = StaticType::Any. No inference is attempted — the Any stands for the duration of analysis.

Impact

Severity: Critical. Every operation inside an unannotated function body produces Any. Callers of those functions also get Any. In practice this means the analyzer gives no useful type information for the majority of user code.

Approaches

Partial fix (low effort, high payoff)

Register all stdlib functions with explicit, narrow parameter types where possible. This won't help user-defined functions but immediately improves let z = add(1, 2) where add is a stdlib function.

Medium fix (medium effort)

After the function body is fully analyzed, re-examine all call sites within the same translation unit and narrow parameter types from the LUB of argument types at all call sites. This is a single backward pass over the call graph.

Full fix (high effort)

Implement proper bidirectional / Hindley-Milner–style inference with constraint generation and unification. This would handle mutual recursion, higher-order functions, and cross-module calls. This is a significant architectural change.

Notes

  • The parser represents unannotated params as type_name = StaticType::Any; this conflates "annotated as Any" with "unannotated." A first step would be distinguishing these two states (e.g., Option<StaticType> where None means unannotated).
  • Fixing this class eliminates the downstream Any in for-loop variables, map comprehensions, and any expression depending on a call to a user-defined function.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions