Skip to main content

Lightweight Static Dispatch

DependencyAtlas does not try to become a whole-program Julia type inferencer. The da_source dispatch layer is intentionally smaller and more explicit than that.

Its job is to improve call resolution for common, local, and explainable cases without turning the base graph into a speculative compiler pass.

Goals

The lightweight dispatch layer exists to answer a narrow question:

  • given a callsite already extracted from source
  • and a set of local candidate methods already known in the graph
  • can we safely narrow that set using source-visible information?

In practice that means it focuses on:

  • method signature metadata already present on nodes
  • local call-shape facts from syntax/lowering
  • owner parameter annotations
  • simple type-hierarchy reasoning
  • one-step local value propagation

It does not try to perform:

  • global dataflow
  • interprocedural type inference
  • arbitrary evaluation of Julia code
  • package-specific dispatch heuristics

Main Components

The dispatch stack is split into three internal layers.

1. Type Hierarchy

type_hierarchy.jl maintains a small compatibility model for:

  • project-defined supertype chains
  • builtin fallback chains such as String -> AbstractString
  • family-tag expansion used by narrowing

This layer answers questions like:

  • does MyDerived satisfy DerivedStrategy?
  • what family tags should be attached to BitVector?
  • how should short type refs be normalized in the current owner module?

2. Dispatch Types

dispatch_types.jl converts raw node metadata and call payloads into typed internal records.

This is where DependencyAtlas normalizes:

  • signature parameters
  • call arguments
  • owner parameter environments
  • local value environments
  • return summaries

This layer keeps the orchestration code from passing around loosely-typed Dict{String,Any} blobs.

3. Dispatch Orchestration

dispatch.jl does only the narrowing workflow:

  • arity filtering
  • argument-shape scoring
  • candidate truncation
  • final evidence payload assembly

The orchestrator should stay small. If type reasoning or shape normalization starts growing there, it usually belongs back in dispatch_types.jl or type_hierarchy.jl.

What the Layer Uses

The current narrowing pass can consume several kinds of local information.

Signature Metadata

Method nodes carry normalized dispatch metadata such as:

  • signature summary/text
  • typed parameter payloads
  • arity bounds
  • vararg flags

That metadata is attached during lowering/build, then reused during call resolution.

Owner Parameter Annotations

If a call argument is just an identifier like method, the dispatch layer can inherit type information from the enclosing method signature.

This is what lets a call like:

reduce_derived(method::MyDerived, idx) = route_value(method, idx)

narrow toward overloads that accept MyDerived or its supertypes.

Local Value Flow

The layer also uses one-step local value propagation for simple assignments:

expanded = make_pairs(vals)
pick_default(expanded)

This does not try to evaluate make_pairs. Instead it carries a compact shape summary forward so later narrowing is not forced to treat expanded as a completely opaque identifier.

Macro-Generated Runtime Methods

Macro-generated functions are represented as generated runtime method nodes, not as calls attributed back to macro owners.

That means dispatch narrowing runs against the generated method body the same way it would for ordinary source-defined methods.

What “Safe” Means Here

The dispatch layer is intentionally conservative.

Examples of current safe wins:

  • unique Type{T} matches
  • project subtype-to-supertype narrowing
  • builtin family narrowing such as String vs AbstractString
  • constructor-adjacent helper dispatch after one local assignment
  • bare external short-name qualification when a single visible using Module export matches

Examples of cases it deliberately leaves unresolved:

  • ambiguous short names across multiple visible modules
  • dynamic export generation such as Core.eval(Expr(:export, ...))
  • broad interprocedural type recovery
  • macro-definition dependency edges that are not runtime method truth

Why This Layer Exists Separately

Keeping static dispatch lightweight gives DependencyAtlas a few practical advantages:

  • the base graph remains fast enough to run on large projects
  • evidence remains explainable to humans
  • behavior stays portable across codebases instead of becoming Clapeyron-specific
  • on-demand JET views can still add richer evidence without fighting hidden heuristics in the base layer

In other words, da_source should resolve the calls it can justify from source structure and local facts, then stop.