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).
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
# 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
|
||||
@@ -43,3 +43,4 @@
|
||||
- 2026-05-12 — audit-ct-tidy: milestone close (ct-tidy) — architect drift fixed inline as `ctt.tidy` (3 doc-side edits: DESIGN.md §"Class-schema diagnostics" drops the retired `KindMismatch` bullet + adds successor paragraph naming `BareCrossModuleTypeRef`; DESIGN.md §"Higher-kinded class params" rewritten to name `BareCrossModuleTypeRef` from canonical-form validation instead; roadmap.md two P2 todos struck `[x]` with forward-reference to ctt.3 and ctt.2). Bench mixed (check.py exit 1: bump_s persistence on `bench_list_sum` second consecutive sighting at +12.23% → +12.60%; two new first-sighting rc-cohort max_us latency regressions classified as noise pending next audit; the recurring 3-audit `latency.explicit_at_rc` improvement cluster narrowed to within tolerance this audit, withdrawing the ratify-pending plan from audit-eob — the meaningful shrinkage is itself attribution evidence that the cluster is noise-class not signal-class), baseline pristine for the 4th consecutive audit → 2026-05-12-audit-ct-tidy.md
|
||||
- 2026-05-12 — iter 24.1: `bool_to_str` + `str_clone` runtime + codegen wiring — 2 new C functions in `runtime/str.c` (slab-allocating heap-Str primitives via existing `str_alloc`), lockstep checker + synth.rs installs with `ret_mode: Own`, 2 IR-header declares + 2 `lower_app` arms + `is_static_callee` whitelist extension, 5 IR snapshots regen (2 declares × 5 files), 9 new tests (2 builtins-install unit + 2 IR-shape pin + 5 E2E all green), pre-existing-drift fix included (int_to_str row added to `builtins.rs::list()`). One substantive deviation: builtin-signatures registered in `uniqueness.rs::infer_module` + `linearity.rs::check_module_with_visible` (8 LOC × 2 files) so `str_clone`'s `param_modes: [Borrow]` is visible to the App-arg walker; symmetric to 23.4-prep's class-method registration; necessary for the plan's literal `frees == 3` cross-realisation assertion. Full `cargo test --workspace` 513 passed, 0 failed. `bench/compile_check.py` + `bench/cross_lang.py` green → 2026-05-12-iter-24.1.md
|
||||
- 2026-05-13 — iter mq.1: class-ref canonical-form extension + workspace-internal class-name qualification — three schema fields (`InstanceDef.class`, `Constraint.class`, `SuperclassRef.class`) move bare → canonical (bare for same-module, `<module>.<Class>` for cross-module) symmetric to ct.1's `Type::Con.name` rule; `ClassDef.name` stays bare (defining site, like `TypeDef.name`). `validate_canonical_type_names` gains three field-walks via new `check_class_ref` helper plus two sibling diagnostics `BareCrossModuleClassRef` / `BadCrossModuleClassRef`; `check_class_name_fields` narrowed to `ClassDef.name`-only. Workspace-internal class-name keys all qualified: `class_def_module`, `class_by_name`, registry `entries.0`, `ClassMethodEntry.class_name`, `class_superclasses`, mono's `class_index`, `Origin::Class.class_name`. New `qualify_class_ref` helper in `workspace.rs` plus sibling `qualify_class_ref_in_check` in `ailang-check`. Two new positive on-disk fixtures (`mq1_xmod_constraint_class{,_dep}`). Recon claim that all existing fixtures' class-refs are intra-module was empirically wrong — 5 test_22b* (`orphan_third`, `dup_a/b/entry`, `unbound_constraint_var`) + 5 other (`eq_ord_polymorphic`, `eq_ord_user_adt`, `cmp_max_smoke`, `ctt2_collision_{lib,main}`) fixtures required minimal canonical-form migration. Duplicate-instance test architecturally restructured: post-mq.1 orphan-freedom makes two-modules-on-same-`(class, type)` structurally impossible (any second instance in a non-owning module fires `OrphanInstance` first); new `test_22b1_dup_same_module.ail.json` lands both instances in the class's module — the only post-mq.1 way to land two on the same canonical key. Plan defects fixed inline: `Origin::Class.class_name` had one construction site not two (plan recon off-by-one); `build_class_index` in `mono.rs` needed qualifying (plan said "no code change needed"); `build_module_globals` needed a `Def::Instance` carve-out for qualified `inst.class`; `check_fn`'s declared-constraint matching needed inline canonical-form lifting; `NoInstance` Float-aware message arm needed `prelude.Eq`/`prelude.Ord` match; coherence type-leg needed qualified-head split-and-lookup. Tasks 3-6 ran as one coherent fix (Task 3 alone left tests RED). 7/7 tasks, 520 tests green, `bench/compile_check.py` + `bench/cross_lang.py` exit 0; `bench/check.py` exit 1 due to 4 improvements-beyond-tolerance (audit-ratifiable per convention, not a regression). `MethodNameCollision` + dispatch path unchanged; iters mq.2 + mq.3 land them → 2026-05-13-iter-mq.1.md
|
||||
- 2026-05-13 — iter mq.2: type-driven dispatch mechanism installed (mechanism-before-exercise) — `Env.method_to_candidate_classes` workspace-flat inverse index built alongside `class_methods`; two new `CheckError` variants `AmbiguousMethodResolution` + `UnknownClass` (Display+code+ctx) plus additive `NoInstance.candidate_classes` field; `ResidualConstraint` extended with `candidates: Option<BTreeSet<String>>` (visibility bumped `pub(crate)` → `pub` for test-crate access); 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); synth Var-arm class-method branch rewritten via `parse_method_qualifier` with inner-dot gate (qualifier must be `<module>.<Class>` form, single-dot names like `std_list.length` fall through to the existing qualified-fn path); `refine_multi_candidate_residual` wired into `check_fn` discharge using `expanded` (post-superclass-expansion constraints) for the rigid-var path; `resolve_residual_class_for_mono` wired into mono's `collect_residuals_ordered` residual-to-target mapping. With `MethodNameCollision` still gating real workspaces, the new multi-candidate branches are exercised exclusively by 15 unit tests: 6 in `tests/method_dispatch_pin.rs` covering the 5-step rule's six cases, 6 in `lib.rs` `mod tests` covering variants + field shapes + discharge refinement, 3 in `mono.rs` `mod tests` covering the mono helper. Real workspaces continue producing single-class residuals (`candidates: None`); every pre-mq.2 fixture typechecks unchanged. Plan-invented `format_type_for_display` replaced with `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 at start of mq.2; +19 = 15 new + 4 pre-existing); `bench/compile_check.py` + `cross_lang.py` clean; `bench/check.py` 1 regression (latency noise, runtime cannot be touched by typecheck-side iter). `MethodNameCollision` retirement lands in mq.3 → 2026-05-13-iter-mq.2.md
|
||||
|
||||
Reference in New Issue
Block a user