Closes the module-qualified-class-names milestone. Deletes the workspace-load workaround; the two-libraries case now resolves via type-driven dispatch at the call site with explicit qualifier as the LLM-author's disambiguation tool. Resolves both mq.2 known-debt items (active_declared_constraints plumbing, (class,method) re-key). New synth warnings channel emits class-method-shadowed-by-fn at all three fn-precedence branches per spec section Class-fn collisions. Three new E2E fixtures + integration tests cover the ambiguous, qualified, and class-fn-shadow trajectories. DESIGN.md class-names paragraph rewritten, new section Method dispatch added. Roadmap P2 marked done, milestone-24 unblocked for re-brainstorm. 9/9 tasks. 545 tests green. bench/compile_check.py + cross_lang.py exit 0; bench/check.py exit 1 (2 noise-class regressions, runtime uncoupled to typecheck iter).
14 KiB
iter mq.3 — Retire MethodNameCollision + multi-class E2E + DESIGN.md sync
Date: 2026-05-13
Started from: 90075715d9
Status: DONE
Tasks completed: 9 of 9
Summary
mq.3 closes the module-qualified-class-names milestone by retiring
the WorkspaceLoadError::MethodNameCollision workaround that mq.1 +
mq.2 made redundant. The variant + pre-pass + Origin enum + Display
arm + two pin tests are deleted; the two on-disk fixtures that fired
the collision now load cleanly and the equivalent observations
relocate to env.method_to_candidate_classes (multi-entry set) plus
a per-call-site warning. Three positive E2E fixtures (Trajectory C
ambiguous, Trajectory E explicit-qualifier, class-fn shadow) exercise
the multi-candidate dispatch path end-to-end via ail check --json.
Two out-of-band corrections that mq.2 surfaced as known debt land
here: (1) Env.active_declared_constraints: Vec<Constraint> is
plumbed pre-synth in check_fn so the post-superclass-expansion
constraint set is reachable at synth time by
resolve_method_dispatch's constraint-driven filter; (2)
ModuleGlobals.class_methods + Env.class_methods re-keyed from
BTreeMap<MethodName, ClassMethodEntry> to
BTreeMap<(QualifiedClass, MethodName), ClassMethodEntry> so the
post-retirement world has O(1) (class, method) lookup, with
mono's two presence-check sites switched to consult the natively
method-keyed method_to_candidate_classes. A third
correction — synth(...) signature extended with a warnings: &mut Vec<Diagnostic> out-parameter — installs the channel for the new
structured warning class-method-shadowed-by-fn that fires when a
free fn shadows a class method of the same name (per spec
§"Class-fn collisions": precedence + warning, the spec's default).
DESIGN.md sync removes the MethodNameCollision bullet from
"Workspace-load (registry-build) diagnostics" with a forward-pointing
note, adds AmbiguousMethodResolution / UnknownClass /
class-method-shadowed-by-fn to the "Typecheck diagnostics" block,
rewords the AmbiguousInstance paragraph (per-class coherence stays
via DuplicateInstance; cross-class method ambiguity is the new
diagnostic at the call site), rewrites the class-names paragraph in
the canonical-form section to point at the mq.1 rule + the new
dispatch model, and adds a new ### Method dispatch subsection
anchoring the 5-step rule with method_to_candidate_classes as the
load-bearing data structure. Roadmap P2 entry → [x] with a summary
of the three iters; milestone-24 (Post-22 Prelude — Show + print rewire) depends on: line struck and the entry annotated
"ready for re-brainstorm".
545 tests green (was 539 at start of mq.3; +6 net = 3 new mq.3.x lib tests + 2 relocated method_collision_pin + 3 new mq3_multi_class_e2e
- 2 deleted workspace.rs pin tests).
bench/compile_check.pyandbench/cross_lang.pyexit 0;bench/check.pyexit 1 with 2 regressions both noise-class (bench_list_sum.bump_s persistence — 3rd consecutive sighting since audit-ct-tidy — and a max_us tail metric, plus 4 improvements). Prelude zero-diff.
Per-task notes
-
mq.3.1 —
Env.active_declared_constraints: Vec<Constraint>field plumbed; populated incheck_fnpre-synth alongside the rigid-vars install (consolidated with the existing post-synthexpandedcomputation — single source of truth, no clone-and- duplicate as the plan literally suggested). Synth Var-armresolve_method_dispatchcall site swapped&[]for&env.active_declared_constraints. 1 new test mq3_env_active_declared_constraints_field_exists. -
mq.3.2 —
ModuleGlobals.class_methodsre-keyedIndexMap<String, ClassMethodEntry>→IndexMap<(String, String), ClassMethodEntry>; siblingEnv.class_methodssimilarly re-keyed toBTreeMap<(String, String), ClassMethodEntry>. Accessor methods onModuleGlobals(has_class_method,class_method_class,class_method) all take(class, name); newclass_method_candidates(name) -> Vec<(&class, &entry)>for the enumerate-all-classes-declaring-this-method use case (returningVecnotimpl Iteratorbecause the borrow-checker rejected the'a/'_capture without explicituse<'a, '_>syntax not yet idiomatic in this crate).build_module_globalsinsert site +build_check_envmerge loop both threaded through the tuple key. Synth Var-armclass_methods.get(&(residual_class, method))uses the dispatcher-resolved tentative class. Mono's two presence-check sites (rewrite_mono_calls,interleave_slots) switched to consultmethod_to_candidate_classesnatively — method-keyed, preserves the existing presence-check signature shape; the four call sites at module-walk + interleave_slots entry pass&env.method_to_candidate_classesinstead of&env.class_methods. Test assertion incrates/ail/tests/typeclass_22b2.rs:42updated to pass the qualified class explicitly. 1 new test mq3_env_class_methods_tuple_keyed. -
mq.3.3 —
synth(...)signature extended withwarnings: &mut Vec<Diagnostic>(15+ recursive synth callsites threaded). Five external synth callers (check_fn, check_const, 3 in builtins.rs, 1 in lift.rs, 2 in mono.rs, 1 incheck) all updated; check_fn / check_const pass through to caller via newout_warnings: &mut Vec<Diagnostic>parameter on the caller chain (check_def,check_in_workspace);check_workspaceextends per-module synth_warnings intomodule_diags. Plan suggestedcheck_fnreturnResult<(CheckedFn, Vec<Diagnostic>)>butcheck_fn's actual signature isResult<()>and the mut-ref-accumulator pattern matches the existingVec<CheckError>accumulator atcheck_in_workspace's caller. New diagnosticclass-method-shadowed-by-fn(warning, kebab-case code, structuredctxcarryingname+method+fn_owner_module+candidate_classes) emitted by a closure at all three fn-precedence branches (locals, same-module fn, implicit-import fn). Implicit-import-fn branch reordered ABOVE the class-method branch per spec §"Class-fn collisions" so fn-wins precedence is structural, not just convention. Diagnostic doc updated indiagnostic.rs. One existing testcrates/ail/tests/typeclass_22b3.rs:rewrite_walker_skips_locally_shadowed_class_methodwas a known trip-wire — the fixture intentionally shadows a class method with a local binding, which now correctly fires the new warning; filtered out of the test'sassert!(diags.is_empty())with a comment naming why. 1 new test mq3_class_method_shadowed_by_fn_warning_fires. -
mq.3.4 —
WorkspaceLoadError::MethodNameCollisionvariant + the Origin enum + the per-def collision loop inbuild_registry(workspace.rs 596-681 pre-edit) + the Display arm inmain.rs:1201deleted. The two in-workspace.rs pin tests (class_class_method_name_collision_fires+class_fn_method_name_collision_fires) deleted; relocation lands in mq.3.5. The on-disk fixturestest_22b2_method_name_collision_class_{class,fn}.ail.jsonstay on-disk (now exercised as positive-load by the relocated tests). -
mq.3.5 —
crates/ailang-check/tests/method_collision_pin.rscreated with two repurposed pin tests: the class-class fixture loads cleanly andenv.method_to_candidate_classes["foo"]has exactly two qualified-class entries (...A+...B); the class-fn fixture loads cleanly (the warning is a call-site concern, not a load-time concern; covered by mq.3.6 E2E). The relocation fromailang-coretoailang-checkis required becauseEnv.method_to_candidate_classesis built bybuild_check_env, which lives inailang-check(not visible fromailang-core'smod tests). -
mq.3.6 — Three new on-disk fixtures plus three integration tests in
crates/ail/tests/mq3_multi_class_e2e.rs. Fixture (a)mq3_two_show_ambiguousexercises the bare-method ambiguous case (twoShowclasses, both withShow Int, bareshow 42→AmbiguousMethodResolution). Fixture (b)mq3_two_show_qualifiedexercises explicit-qualifier resolution (same workspace,mq3_two_show_ambiguous_a.Show.show 42→ clean). Fixture (c)mq3_class_eq_vs_fn_eqexercises class-fn shadow (class MyEq { myeq }+fn myeq+ baremyeq 1 2→ fn wins, warning fires). The plan's draft fixtures used a bareTerm::Appfor instance method bodies; the existing-convention shape (pertest_22b3_mono_synthetic.ail.json) isTerm::LamwithparamTypes/retType— implementer-phase repair adjusted inline. NamingMyEq/myeq(rather thanEq/eq) avoids an extra prelude-vs-fn shadow that would have confused the warning assertion. All three tests GREEN first-shot; spot-checked viaail check --jsonfor completeness. -
mq.3.7 — DESIGN.md sync: class-names paragraph (1156-1160) rewritten to point at mq.1 canonical-form + mq.3 dispatch model;
MethodNameCollisionbullet struck from "Workspace-load (registry-build) diagnostics" with forward-pointing note; three new typecheck diagnostics +NoInstance.candidate_classesaddendum added to "Typecheck diagnostics";AmbiguousInstanceparagraph reworded to clarify the registry-level vs call-site distinction; new### Method dispatchsubsection anchors the 5-step rule withmethod_to_candidate_classesas the load-bearing data structure, the class-fn precedence rule, and the post-mq.3 tuple-keyedclass_methodsshape. -
mq.3.8 — Roadmap P2 milestone "Module-qualified class names
- type-driven method dispatch" →
[x]with a one-paragraph summary of the three iters (mq.1 canonical-form, mq.2 mechanism, mq.3 retirement) and the milestone's user-facing outcome (two libraries can each declareclass Eqwith their owneq). Milestone-24 (Post-22 Prelude — Show + print rewire)depends on:line struck and the entry annotated "ready for re-brainstorm" so the next time/bosswalks the roadmap it picks up the un-blocked spec re-derivation against the post-retirement architecture.
- type-driven method dispatch" →
-
mq.3.9 — Integration verification.
cargo test --workspace545 passed / 0 failed.bench/compile_check.py0 regressed / 0 improved / 24 stable.bench/cross_lang.py0 regressed / 0 improved / 25 stable.bench/check.py2 regressed / 4 improved / 57 stable: regression #1throughput.bench_list_sum.bump_s(3rd consecutive sighting since audit-ct-tidy 2026-05-12, audit- ratified-class noise per the journal lineage); regression #2latency.implicit_at_rc.max_us(same noisy max-tail metric that mq.2 journal flagged as runtime-uncoupled-to-typecheck- iter, audit-ratified-class). Prelude zero-diff. All three new E2E fixtures observable viaail checkwith the expected diagnostic / no-diagnostic / warning outcomes.
Concerns
-
Plan's
check_fnsignature suggestion was off. Plan Step 4 proposedcheck_fn(...) -> Result<(CheckedFn, Vec<Diagnostic>), CheckError>; the actual signature isResult<()>(no CheckedFn). The mut-ref-accumulator pattern adopted instead matches the existingcheck_in_workspaceshape (Vec accumulator). Same outcome, different mechanism. -
class_method_candidatesreturnsVecnotimpl Iterator. The plan's accessor signatureimpl Iterator<Item = (&'a String, &'a ClassMethodEntry)>requires theuse<'a, '_>opaque-type capture syntax — not yet idiomatic in this crate (the rest of the codebase usesVec-returning accessors at module-graph boundaries). The semantic contract is the same; performance is one allocation per call, bounded by the per-method candidate count (currently ≤ 2 across the corpus). -
One existing E2E test trips the new warning.
crates/ail/tests/typeclass_22b3.rs:rewrite_walker_skips_locally_shadowed_class_methoduses a fixture (test_22b3_shadow_class_method) that intentionally shadows the class methodshowwith a locallet show = "shadow". Pre-mq.3 this fixture typechecked clean; post-mq.3 it correctly fires theclass-method-shadowed-by-fnwarning. The test'sassert!(diags.is_empty())was relaxed to filter out the new warning with a comment naming why. This is the warning behaving correctly, not a test workaround. -
Plan's instance-method body shape was wrong. Plan Step 1 of Task 6 drafted
body: { "t": "app", ... }for instance methods; the existing-convention shape (pertest_22b3_mono_synthetic) isTerm::LamwithparamTypes/retTypekeys. Implementer-phase repair adjusted all three fixtures inline. The plan's caveat ("implementer adjusts to match the exact shape used byexamples/prelude.ail.json's Eq/Ord instances") covered this case; just exercising the caveat verbatim.
Known debt
-
bench_list_sum.bump_s3rd-consecutive regression. Noise-class per the audit-ct-tidy 2026-05-12 journal entry. No attribution cause across three audits. The pattern is a candidate for ratify-without-attribution at the milestone-close audit (audit-mq) but this iter follows the conservative pristine-baseline convention. -
latency.implicit_at_rc.max_usmax-tail noise. Continues from mq.2's bench profile. Runtime metric, structurally uncoupled to this typecheck-side iter. Audit-ratifiable. -
bench/check.pyexit 1. Same noise-class status as the two regressions above. Conservative interpretation: leave the baseline pristine; audit-mq decides whether to ratify. -
class_method_candidatesallocation per call. Bounded by per-method candidate count (currently ≤ 2). A future tidy that introducesuse<'a, '_>syntax across the crate could revisit the iterator-return shape.
Files touched
Code:
- crates/ail/src/main.rs
- crates/ail/tests/typeclass_22b2.rs
- crates/ail/tests/typeclass_22b3.rs
- crates/ailang-check/src/builtins.rs
- crates/ailang-check/src/diagnostic.rs
- crates/ailang-check/src/lib.rs
- crates/ailang-check/src/lift.rs
- crates/ailang-check/src/mono.rs
- crates/ailang-core/src/workspace.rs
New tests + fixtures:
- crates/ail/tests/mq3_multi_class_e2e.rs
- crates/ailang-check/tests/method_collision_pin.rs
- examples/mq3_two_show_ambiguous_a.ail.json
- examples/mq3_two_show_ambiguous_b.ail.json
- examples/mq3_two_show_ambiguous.ail.json
- examples/mq3_two_show_qualified.ail.json
- examples/mq3_class_eq_vs_fn_eq_classmod.ail.json
- examples/mq3_class_eq_vs_fn_eq_fnmod.ail.json
- examples/mq3_class_eq_vs_fn_eq.ail.json
Docs:
- docs/DESIGN.md
- docs/roadmap.md
Stats
bench/orchestrator-stats/2026-05-13-iter-mq.3.json