# iter mq.2 — Type-driven dispatch mechanism (installed, not yet exercised)

**Date:** 2026-05-13
**Started from:** e9e45c77affa635652505cc937b77b9349d18c8e
**Status:** DONE
**Tasks completed:** 9 of 9

## Summary

mq.2 installs the type-driven dispatch infrastructure that mq.3 will
exercise end-to-end once `MethodNameCollision` retires. Three new
`CheckError` variants (`AmbiguousMethodResolution`, `UnknownClass`,
plus an additive `candidate_classes` field on `NoInstance`); a new
workspace-flat `method_to_candidate_classes` index on `Env` (inverse
of `class_methods`); an extended `ResidualConstraint` carrying an
optional `candidates: Option<BTreeSet<String>>` for the multi-
candidate dispatch path; a pure `resolve_method_dispatch` helper
implementing the spec's 5-step rule (qualifier → singleton →
type-driven filter → constraint-driven filter → `Multi` for discharge-
time refinement); a `refine_multi_candidate_residual` helper for
discharge / mono refinement; and a `resolve_residual_class_for_mono`
helper that wires the refinement into mono's residual-to-target
mapping. The synth `Term::Var` arm class-method branch is rewritten
to consult the new index via `parse_method_qualifier`, with a gate
that prevents capturing the existing cross-module-fn path
(`<module>.<fn>`) by requiring class-qualifiers to carry an inner
dot (per mq.1's canonical-form rule, class qualifiers are always
`<module>.<Class>`).

With `MethodNameCollision` still active, the new multi-candidate
branch is exercised exclusively by 15 unit tests: 6 in
`tests/method_dispatch_pin.rs` (the 5-step rule's six cases), 6 in
`lib.rs` `mod tests` (the new `CheckError` variants + the
`ResidualConstraint`/`Env` field shapes + the discharge-side
refinement), and 3 in `mono.rs` `mod tests` (the mono-side helper).
Real workspaces continue producing single-class residuals
(`candidates: None`), and every pre-mq.2 fixture typechecks
unchanged.

## Per-task notes

- iter mq.2.1: Two new `CheckError` variants
  `AmbiguousMethodResolution` and `UnknownClass`, added as siblings
  after `NoInstance`. `code()` + `ctx()` table entries; module-doc
  list in `diagnostic.rs` extended. 2 RED-then-GREEN smoke tests.
- iter mq.2.2: `NoInstance` extended with an additive
  `candidate_classes: Vec<String>` field. `ctx()` emits the field
  only when non-empty (preserves pre-mq.2 JSON shape). 2 tests:
  with + without populated candidates.
- iter mq.2.3: `ResidualConstraint` extended with `candidates:
  Option<BTreeSet<String>>`. Struct bumped to `pub` (was
  `pub(crate)`) for unit-test access from the dispatch-pin / Task 7
  + 8 tests. Single existing construction site updated with
  `candidates: None`. 1 smoke test.
- iter mq.2.4: `Env.method_to_candidate_classes:
  BTreeMap<String, BTreeSet<String>>` built workspace-flat in
  `build_check_env` alongside the existing `class_methods` insert
  (coalesced into one loop). 1 in-test fixture round-trip.
- iter mq.2.5: `MethodDispatchOutcome` enum + pure
  `resolve_method_dispatch` helper. 6 unit tests cover unique
  candidate, qualifier-match, qualifier-no-match (UnknownClass),
  type-driven narrow-to-one, constraint-driven narrow-to-one
  (rigid-var fallback), and true ambiguity. Helper uses
  `ailang_core::pretty::type_to_string` for the at_type rendering
  rather than the plan's invented `format_type_for_display` (one
  fewer duplicate; the canonical pretty-printer is already in use
  across other diagnostic surfaces).
- iter mq.2.6: Synth `Term::Var` arm class-method branch rewritten
  to consult `method_to_candidate_classes` via
  `parse_method_qualifier`. Branch gate: qualifier must be either
  absent (bare form `"show"`) or contain an inner dot
  (`<module>.<Class>` per mq.1's canonical-form rule). 1-dot names
  like `std_list.length` fall through to the existing qualified-fn
  path. Plan's `active_declared_constraints` plumbing intentionally
  skipped at synth time — synth's Var arm does not carry the body's
  declared constraints; the constraint-driven filter runs at
  discharge time (Task 7) where `expanded` is in scope. With
  `MethodNameCollision` still active, the candidate set is always
  singleton at this branch, so the `&[]` synth-time filter is
  load-bearing only post-mq.3 and only for the rigid-var fallback;
  on the post-mq.3 multi-candidate fully-concrete path discharge
  will refine the residual on `r_ty` against the workspace registry.
- iter mq.2.7: `RefineOutcome` enum + `refine_multi_candidate_residual`
  helper. Wired into `check_fn`'s residual loop BEFORE the existing
  single-class discharge — multi-candidate residuals refine first,
  `Resolved` falls through to `continue`, error variants return.
  Used `expanded` (already-superclass-expanded declared constraints)
  for the rigid-var path, sounder than `declared_constraints` direct.
  3 unit tests for the discharge path.
- iter mq.2.8: `resolve_residual_class_for_mono` helper in mono.rs
  + 3 unit tests in a new `mod tests` block. Mono's residual-to-
  target mapping (`collect_residuals_ordered`) now calls the helper
  before constructing the registry key; resolved-class overrides
  the residual's tentative `r.class` in `MonoTarget::ClassMethod`.
  Single-class residuals flow through unchanged.
- iter mq.2.9: full `cargo test --workspace` 539 passed / 0 failed
  (was 520 at start of mq.2; +19 = 15 new mq.2 tests + 4 pre-
  existing mq.1 follow-on additions in cross-language test files
  already shipped). All pre-mq.2 fixtures (prelude, mq1_xmod,
  eq_ord_polymorphic) typecheck unchanged. Bench results:
  `compile_check.py` 0 regressed / 0 improved / 24 stable;
  `cross_lang.py` 0 regressed / 0 improved / 25 stable;
  `check.py` 1 regressed / 2-4 improved / 58-60 stable (latency
  noise — different metric regressed between two runs, runtime
  benches cannot be touched by a typecheck-side iter). Prelude
  roundtrip clean.

## Concerns

- **Helper-cost: `parse_method_qualifier` does an `rfind('.')` on
  every `Term::Var` lookup that reaches the class-method branch.**
  Today this is bounded by the per-Var-arm cost (already O(name-
  length) due to the dot-counting and unification ladder), so it's
  noise. If profiling ever shows the Var arm hot, the right fix is
  to cache `(method_name, qualifier_opt)` on the Var node rather
  than recompute — but that's a Term struct change and out of
  scope for mq.2.

- **`pub` bump on `ResidualConstraint`.** The unit-test crate
  (`tests/method_dispatch_pin.rs`) needs to construct
  `ResidualConstraint` for Task 5; Task 7's in-lib tests also
  reference it. Bumping from `pub(crate)` to `pub` is the minimum
  edit; the struct semantically belongs at the workspace boundary
  anyway (the spec's published shape for what the dispatcher
  produces). No risk: no external crate depends on `ailang-check`
  outside this workspace.

- **`MissingConstraint.class` field for multi-candidate rigid-var
  case is best-effort.** When `refine_multi_candidate_residual`
  emits `MissingConstraint`, the surfaced `class` is the first
  candidate (BTreeSet order). The diagnostic ctx still names the
  method, the type, and (via the diagnostic Display) the full
  candidate set is reachable; the `class` is just a hint. Real
  workspaces never reach this branch pre-mq.3.

## Known debt

- **mq.3 retires `MethodNameCollision` and exercises the
  multi-candidate path end-to-end.** The unit tests in this iter
  are the only coverage of the new branches until then. mq.3 will
  add cross-class-collision fixtures (e.g. two `Show` classes in
  separate modules) and the existing tests should continue to pass
  unchanged.

- **`class_methods` keyed by method name only.** Pre-mq.3 invariant
  guarantees `env.class_methods.get(method_name)` is unambiguous;
  post-mq.3 this lookup needs to be reworked (likely keyed by
  `(class, method)` or by `method` returning a list). The synth
  branch's `expect("invariant")` documents the dependency. Not
  load-bearing in mq.2.

- **Synth-time `declared_constraints: &[]` in the Var arm rewrite.**
  This is correct for pre-mq.3 (singleton candidate set never
  needs filtering) but means the post-mq.3 constraint-driven
  filter cannot fire at synth time. The branch will need
  `env.active_declared_constraints` plumbed through (Env field
  + `check_fn` initialization) before mq.3's first multi-candidate
  workspace with declared constraints. Cost: ~10-line edit.

## Files touched

- crates/ailang-check/src/diagnostic.rs
- crates/ailang-check/src/lib.rs
- crates/ailang-check/src/mono.rs
- crates/ailang-check/tests/method_dispatch_pin.rs (new)

## Stats

bench/orchestrator-stats/2026-05-13-iter-mq.2.json
