E05230: dispatcher has ambiguous handler resolution for reachable type

Category: E05xxx Explicit type symbol definition · Reported in phase: PRE_IR_CHECKS · Classification: DISPATCHER_HANDLER_AMBIGUITY · All error codes

What went wrong

A dispatcher has two or more handlers that match a reachable type with equal cost, making it impossible to determine which handler should be called. The dispatcher entry point (marked 'as dispatcher') acts as the fallback of last resort -- it only handles types that no specific handler matches. Specific handlers are preferred over the entry point.

When two handlers match at equal cost, the compiler cannot determine which should win. This is a declaration-time error: the ambiguity exists in the type hierarchy regardless of what values are actually dispatched.

Dispatchers work best with tree hierarchies (class inheritance) where every type has exactly one path to the base. Traits create DAG hierarchies (multiple inheritance paths), which is where ambiguity becomes structural. Choose the resolution strategy that best fits the scale and ownership of the type hierarchy.

Worked examples

Compilable examples demonstrating this error and the correct fix:

  • ek9 -q 870 (How does adding a concrete type handler resolve dispatche...)

[RESOLUTION STRATEGY 1: Explicit Handler] Add a specific handler for each ambiguous concrete type: a handler for Duck itself is an exact match (cost 0.0) and beats both trait handlers (0.10). Best when: few ambiguous types, dispatcher owns all types (good.ek9 in the [EXAMPLE] section below).

Limitation: does not scale. Adding a new multi-trait type (Pelican, Platypus) breaks the dispatcher again. Each new type needs a handler.

[RESOLUTION STRATEGY 2: Bridge Trait] Create a combining trait that encompasses both competing traits. Ambiguous types implement the bridge trait instead of individual traits. Best when: you control the type hierarchy and can refactor it (good_bridge.ek9 below: Duck has only 'AmphibiousFlyer', which has Flyable and Swimmable, and the dispatcher has a handler for AmphibiousFlyer).

Why it works: Duck -> AmphibiousFlyer is one trait hop (cost 0.10). Duck -> Flyable goes Duck -> AmphibiousFlyer -> Flyable (cost 0.20). The bridge handler wins because it is closer in the trait hierarchy.

CRITICAL: the concrete type must implement ONLY the bridge trait, not the individual traits directly. If Duck implements all three (Flyable, Swimmable, AmphibiousFlyer), all are at cost 0.10 and the ambiguity remains.

[RESOLUTION STRATEGY 3: Decomposed Dispatchers] Split into multiple single-axis dispatchers, each handling one trait dimension. The caller chooses which dimension to dispatch on. Best when: you cannot modify the type hierarchy, or traits represent genuinely independent concerns. A SerializationDispatcher whose 'serialize()' dispatches over Entity (with a handler for UserProfile), beside a RenderDispatcher whose 'render()' dispatches over Entity (with a handler for Dashboard), each handles one behavioural axis. A type implementing both Serializable and Renderable is dispatched by the appropriate dispatcher for the operation being performed. No ambiguity because each dispatcher has a single concern.

Choosing a strategy

  • Few ambiguous types, simple hierarchy -> Strategy 1 (explicit handler)
  • You own the trait hierarchy and can refactor -> Strategy 2 (bridge trait)
  • Traits represent independent concerns -> Strategy 3 (decomposed dispatchers)
  • Large hierarchy with many multi-trait types -> Strategy 3 (always scales)

Common causes

  • A class implements two traits and both traits have handlers
  • Two classes at the same hierarchy depth both have handlers
  • Adding a trait handler without considering which types implement it
  • Growing a type hierarchy without re-evaluating dispatcher coverage

See also

  • E05210: dispatcher handler type not in hierarchy
  • E05220: dispatcher handler return type covariance
  • E06140: method ambiguous (call-site ambiguity)

Examples

Every example is compiled by the EK9 compiler: the first fails exactly as captioned, the others fix it.

Fails with E05230: dispatcher has ambiguous handler resolution for reachable type

#!ek9
defines module error.e05230.bad

  defines trait

    Flyable
      fly()
        Stdout().println("Flying")

    Swimmable
      swim()
        Stdout().println("Swimming")

  defines class

    Animal as abstract
      speak() as abstract

    Dog is Animal
      override speak()
        Stdout().println("Woof")

    Cat is Animal
      override speak()
        Stdout().println("Meow")

    <?-
      Duck implements both Flyable and Swimmable.
      This means handlers for Flyable and Swimmable both match Duck at equal cost.
    -?>
    Duck is Animal with trait of Flyable, Swimmable
      override speak()
        Stdout().println("Quack")

    <?-
      AnimalProcessor has handlers for Flyable and Swimmable but no handler
      for Duck. Since Duck implements both traits at equal cost (0.10),
      the dispatcher has ambiguous resolution for Duck.
    -?>
    AnimalProcessor

      process() as dispatcher
        -> a as Animal
        a.speak()

      process()
        -> a as Dog
        a.speak()

      process()
        -> a as Flyable
        a.fly()

      process()
        -> a as Swimmable
        a.swim()

Fixed: Add a handler for the type that matches two handlers equally - here Duck

#!ek9
defines module error.e05230.good

  defines trait

    Flyable
      fly()
        Stdout().println("Flying")

    Swimmable
      swim()
        Stdout().println("Swimming")

  defines class

    Animal as abstract
      speak() as abstract

    Dog is Animal
      override speak()
        Stdout().println("Woof")

    Duck is Animal with trait of Flyable, Swimmable
      override speak()
        Stdout().println("Quack")

    AnimalProcessor

      process() as dispatcher
        -> animal as Animal
        animal.speak()

      process()
        -> animal as Dog
        animal.speak()

      process()
        -> animal as Duck
        animal.fly()
        animal.swim()

      process()
        -> animal as Flyable
        animal.fly()

      process()
        -> animal as Swimmable
        animal.swim()

      //Duck is both Flyable and Swimmable: this handler says which wins

Fixed: A bridge trait: the multi-trait type takes ONLY the trait that combines the others

#!ek9
defines module error.e05230.good.bridge

  defines trait
    Flyable
      fly()
        Stdout().println("Flying")
    Swimmable
      swim()
        Stdout().println("Swimming")
    //The bridge: inherits fly() and swim()
    AmphibiousFlyer with trait of Flyable, Swimmable

  defines class
    Animal as abstract
      speak() as abstract

    //Duck takes ONLY the bridge trait, never Flyable or Swimmable directly
    Duck is Animal with trait of AmphibiousFlyer
      override speak()
        Stdout().println("Quack")

    AnimalProcessor
      process() as dispatcher
        -> animal as Animal
        animal.speak()
      process()
        -> animal as Flyable
        animal.fly()
      process()
        -> animal as Swimmable
        animal.swim()
      //One trait hop from Duck: closer than Flyable or Swimmable (two hops)
      process()
        -> animal as AmphibiousFlyer
        animal.fly()
        animal.swim()

Questions and answers

These Q&A entries show E05230 and its fix: