Files
AILang/docs/journals/2026-05-13-iter-mq.2.md
T
Brummel 2e6a4ca200 iter mq.2: type-driven dispatch mechanism installed (mechanism-before-exercise)
Installs the dispatch infrastructure for type-driven method
resolution without retiring MethodNameCollision yet. The new
multi-candidate path is exercised exclusively by 15 unit tests;
real workspaces continue producing single-class residuals
(candidates: None) and every pre-mq.2 fixture typechecks unchanged.

Three new CheckError variants:
- AmbiguousMethodResolution (multi-candidate after type-driven filter)
- UnknownClass (explicit qualifier names a class not in registry)
- NoInstance gains additive candidate_classes field (Vec<String>)

Schema/Env additions:
- Env.method_to_candidate_classes: BTreeMap<String, BTreeSet<String>>
  workspace-flat inverse of class_methods, built in build_check_env.
- ResidualConstraint.candidates: Option<BTreeSet<String>> — None
  preserves pre-mq.2 single-class semantics; Some(set) carries the
  multi-candidate residual that discharge refines.
- ResidualConstraint visibility bumped pub(crate) → pub for
  unit-test crate access.

New helpers:
- MethodDispatchOutcome enum + pure resolve_method_dispatch
  implementing the spec's 5-step rule (qualifier → singleton →
  type-driven filter → constraint-driven filter → Multi for
  discharge-time refinement).
- parse_method_qualifier splits Term::Var.name into
  (method_name, optional_qualifier_prefix) at the last dot.
- RefineOutcome enum + refine_multi_candidate_residual for
  discharge-time refinement.
- resolve_residual_class_for_mono wires the refinement into mono's
  collect_residuals_ordered residual-to-target mapping.

Synth Var-arm class-method branch rewritten via parse_method_qualifier
with inner-dot gate (qualifier must be <module>.<Class>; single-dot
names fall through to the existing qualified-fn path).

Constraint-discharge in check_fn uses expanded (post-superclass-
expansion) constraints for the rigid-var path — sounder than raw
declared_constraints.

Plan-invented format_type_for_display replaced with the existing
ailang_core::pretty::type_to_string (one less duplicate).

Synth-time declared_constraints: &[] is a deliberate gap documented
as known debt — load-bearing only post-mq.3 for the rigid-var
fallback (env-plumbing the active fn's constraints into the Var
arm is a ~10-line edit slated for mq.3).

9/9 tasks, 539 tests green (was 520 pre-mq.2; +15 mq.2 unit tests +
4 pre-existing). bench/compile_check.py + cross_lang.py clean;
bench/check.py 1 regression (latency noise — runtime cannot be
touched by a typecheck-side iter).
2026-05-13 01:40:42 +02:00

8.5 KiB

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

Date: 2026-05-13 Started from: e9e45c77af 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