diff --git a/docs/plans/mq.3.md b/docs/plans/mq.3.md new file mode 100644 index 0000000..dac2310 --- /dev/null +++ b/docs/plans/mq.3.md @@ -0,0 +1,1473 @@ +# Iter mq.3 — Retire MethodNameCollision + multi-class E2E + DESIGN.md sync — Implementation Plan + +> **Parent spec:** `docs/specs/2026-05-13-module-qualified-class-names.md` +> +> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` to run +> this plan. Steps use `- [ ]` checkboxes for tracking. + +**Goal:** Retire the workspace-global `MethodNameCollision` workaround +that mq.1 + mq.2 made redundant; exercise the type-driven dispatch +end-to-end via three new multi-class fixtures; sync DESIGN.md and +the roadmap to reflect the post-retirement architecture. + +**Architecture:** Three out-of-band corrections precede the retirement. +(1) `ModuleGlobals.class_methods` + `Env.class_methods` re-key to +`BTreeMap<(QualifiedClassName, MethodName), ClassMethodEntry>` so the +post-retirement multi-candidate world has O(1) `(class, method)` +lookup. (2) `Env.active_declared_constraints: Vec` field +plumbed through `check_fn` pre-synth so `resolve_method_dispatch`'s +constraint-driven filter can fire at synth time for the rigid-var +fallback. (3) `synth(...)` signature extended with `warnings: &mut +Vec` out-parameter mirroring `residuals` / +`free_fn_calls`; new structured warning `class-method-shadowed-by-fn` +fires when both a fn and a class-method candidate exist for the same +name. With those in place: delete `MethodNameCollision` variant + +pre-pass + Origin enum + Display arm; relocate the two pin tests +to `crates/ailang-check/tests/method_collision_pin.rs` (cross-crate +for Env access) and invert their assertions; ship three new positive +E2E fixtures exercising the multi-candidate dispatch path (ambiguous, +explicit-qualifier, class-vs-fn-shadow); sync DESIGN.md (strike +`MethodNameCollision`, add `AmbiguousMethodResolution` + +class-fn-shadow paragraph, new §"Method dispatch" subsection); +update `docs/roadmap.md` P2 entry to `[x]` and clear milestone 24's +`depends on:` line. + +**Tech Stack:** `ailang-core` (`workspace.rs`, `DESIGN.md`), +`ailang-check` (`lib.rs`, `mono.rs`, `diagnostic.rs`, +`tests/method_collision_pin.rs` — new), `ail` CLI (`main.rs` Display +arm strike), `docs/roadmap.md`. + +**Pre-flight notes (Boss decisions from recon):** +- `class_methods` re-key: **Option (a) tuple key** `(QualifiedClassName, + MethodName)`, NOT `Vec` per method — O(1) post- + dispatch lookup wins over linear scan; mono's existing presence + check switches to `method_to_candidate_classes.contains_key(name)` + which has the right shape natively. +- Warning channel: **Option (a) `synth(...) -> warnings out-parameter`** — + symmetric to `residuals` / `free_fn_calls`; alternative (b) at + check_fn boundary has no call-site context. +- `Env.active_declared_constraints` field: stores POST-superclass-expansion + constraints (sounder for the filter; expansion already runs in check_fn + per mq.2 journal). Cleared at workspace entry, re-populated per fn body. +- Repurposed pin tests: **relocate to `crates/ailang-check/tests/method_collision_pin.rs`** — cross-crate access to `Env.method_to_candidate_classes` is required. +- §"Diagnostic categories" `AmbiguousInstance` paragraph: REWORD, don't delete (per-class coherence stays — `DuplicateInstance` still fires; cross-class method ambiguity is the new diagnostic). +- Operator-=/`<` corner case: out of scope; prelude `ne`/`lt`/etc. are + free fns not class methods, so the new warning does not fire on them + even if a user adds a `class` declaring same-named methods. + +--- + +## Task 1: Plumb `Env.active_declared_constraints` field + +**Files:** +- Modify: `crates/ailang-check/src/lib.rs:3311-3391` (Env struct — add field) +- Modify: `crates/ailang-check/src/lib.rs:1622-1716` (check_fn — populate field pre-synth) + +- [ ] **Step 1: Write a RED test for the new field** + +Append in `crates/ailang-check/src/lib.rs` `mod tests`: + +```rust +/// mq.3.1: `Env` carries an `active_declared_constraints` field +/// holding the active fn's POST-superclass-expansion declared +/// constraints. Empty at workspace entry; populated by `check_fn` +/// before invoking `synth` on the fn body. Consumed by `synth`'s +/// Var-arm class-method branch when calling `resolve_method_dispatch` +/// (constraint-driven filter path). +#[test] +fn mq3_env_active_declared_constraints_field_exists() { + let env = Env::default(); + assert_eq!(env.active_declared_constraints.len(), 0, + "active_declared_constraints empty at default-construction"); +} +``` + +- [ ] **Step 2: Run test to confirm RED** + +Run: `cargo test --workspace -p ailang-check mq3_env_active_declared_constraints_field_exists` + +Expected: compile error — `active_declared_constraints` field does not exist on `Env`. + +- [ ] **Step 3: Add the field to `Env`** + +At `crates/ailang-check/src/lib.rs:3311-3391`, find the `Env` struct. +Add a field immediately after `class_methods` (Env field at lib.rs:3366): + +```rust +/// mq.3: post-superclass-expansion declared constraints of the +/// active fn body being checked. Populated by `check_fn` before +/// invoking `synth` on the body; cleared at workspace entry. +/// Consumed by `synth`'s Var-arm class-method branch when calling +/// `resolve_method_dispatch` — the constraint-driven filter +/// disambiguates multi-candidate residuals at the rigid-var +/// fallback (e.g. inside a polymorphic fn body where the arg +/// type is still a rigid type variable). +pub active_declared_constraints: Vec, +``` + +Update `Default` impl if applicable (the field defaults to empty +`Vec` via `#[derive(Default)]` if `Constraint: Default`; otherwise +add explicit init in the Default impl). + +- [ ] **Step 4: Populate the field in `check_fn` pre-synth** + +In `crates/ailang-check/src/lib.rs:1622-1716` (`check_fn`), find the +existing `let expanded = expand_declared_constraints(...)` call (per +mq.2 journal: post-superclass-expansion happens here). The expansion +result is what we stash on Env. + +Locate the env-clone-and-shadow pattern at lib.rs:1654 (mirror of +`env.rigid_vars.insert(...)`). Add: + +```rust +let mut env = env.clone(); +env.rigid_vars.insert(/* existing insert call */); +// mq.3: stash declared constraints for synth-time access by the +// class-method dispatch resolver's constraint-driven filter. +env.active_declared_constraints = expanded.clone(); +``` + +(The exact placement is after the existing `expanded` computation and +before the `synth(...)` call at lib.rs:1680. Implementer adjusts the +insertion line.) + +- [ ] **Step 5: Update `resolve_method_dispatch` call site in synth Var-arm to use `env.active_declared_constraints`** + +At `crates/ailang-check/src/lib.rs:2462-2469` (per recon, the existing +`resolve_method_dispatch` call site passes `&[]` for +`declared_constraints`). Replace `&[]` with +`&env.active_declared_constraints`: + +```rust +let outcome = resolve_method_dispatch( + method_name, + qualifier_prefix.as_deref(), + candidates, + /*concrete_arg_type*/ None, + /*declared_constraints*/ &env.active_declared_constraints, + /*registry*/ ®istry_unit, +); +``` + +- [ ] **Step 6: Run the mq.3.1 test to confirm GREEN** + +Run: `cargo test --workspace -p ailang-check mq3_env_active_declared_constraints_field_exists` + +Expected: PASS. + +- [ ] **Step 7: Run full ailang-check test suite** + +Run: `cargo test --workspace -p ailang-check` + +Expected: green. With `MethodNameCollision` still active, the +constraint-driven filter is unreachable (singleton candidate set short- +circuits before reaching the filter); existing tests carry the same +behaviour. + +--- + +## Task 2: Re-key `class_methods` to `(QualifiedClassName, MethodName)` + +**Files:** +- Modify: `crates/ailang-check/src/lib.rs:1129` (ModuleGlobals.class_methods type) +- Modify: `crates/ailang-check/src/lib.rs:1184-1240` (build_module_globals insert site) +- Modify: `crates/ailang-check/src/lib.rs:1086-1102` (ModuleGlobals accessor methods) +- Modify: `crates/ailang-check/src/lib.rs:3366` (Env.class_methods type) +- Modify: `crates/ailang-check/src/lib.rs:1343-1354` (build_check_env merge loop) +- Modify: `crates/ailang-check/src/lib.rs:2497-2500` (synth Var-arm class_methods lookup) +- Modify: `crates/ailang-check/src/mono.rs:967-998, 1268-1284` (mono presence-check sites switch to method_to_candidate_classes) + +- [ ] **Step 1: Write a RED test for the new key shape** + +Append in `crates/ailang-check/src/lib.rs` `mod tests`: + +```rust +/// mq.3.2: `Env.class_methods` is keyed by `(QualifiedClassName, +/// MethodName)` tuple — post-mq.3 the workspace can carry multiple +/// classes sharing a method name, so the key must disambiguate. +/// O(1) lookup post-dispatch via `(resolved_class, method)`. +#[test] +fn mq3_env_class_methods_tuple_keyed() { + let mut modules = BTreeMap::new(); + modules.insert( + "m".to_string(), + Module { + name: "m".to_string(), + imports: vec![], + defs: vec![Def::Class(ClassDef { + name: "MyCls".to_string(), + param: "a".to_string(), + superclass: None, + methods: vec![ClassMethod { + name: "mymethod".to_string(), + ty: Type::Fn { + params: vec![Type::Var { name: "a".to_string() }], + param_modes: vec![ParamMode::Borrow], + ret: Box::new(Type::Con { name: "Str".to_string(), args: vec![] }), + effects: vec![], + }, + default: None, + }], + doc: None, + })], + }, + ); + let ws = build_workspace_for_test(modules); + let env = build_check_env(&ws); + let key = ("m.MyCls".to_string(), "mymethod".to_string()); + assert!(env.class_methods.contains_key(&key), + "env.class_methods must contain {key:?}"); +} +``` + +- [ ] **Step 2: Run test to confirm RED** + +Run: `cargo test --workspace -p ailang-check mq3_env_class_methods_tuple_keyed` + +Expected: compile error or runtime panic — today's key is `String`, +not `(String, String)`. + +- [ ] **Step 3: Re-key `ModuleGlobals.class_methods`** + +At `crates/ailang-check/src/lib.rs:1129`, find the field: + +```rust +pub class_methods: IndexMap, +``` + +Replace with: + +```rust +/// mq.3: re-keyed by `(qualified-class-name, method-name)` tuple — +/// post-retirement of `MethodNameCollision`, two classes can declare +/// same-named methods within the same workspace. The tuple key +/// disambiguates without losing the per-module ordering guarantee +/// (`IndexMap` preserves insertion order). Lookups by method-name- +/// only consult `method_to_candidate_classes` (Env-level) to get +/// the candidate-class set, then index into `class_methods` with +/// `(resolved_class, method)`. +pub class_methods: IndexMap<(String, String), ClassMethodEntry>, +``` + +Update the accessor methods at lib.rs:1086-1102: + +```rust +impl ModuleGlobals { + pub fn has_class_method(&self, class: &str, name: &str) -> bool { + self.class_methods.contains_key(&(class.to_string(), name.to_string())) + } + + pub fn class_method_class(&self, class: &str, name: &str) -> Option<&str> { + self.class_methods + .get(&(class.to_string(), name.to_string())) + .map(|e| e.class_name.as_str()) + } + + pub fn class_method(&self, class: &str, name: &str) -> Option<&ClassMethodEntry> { + self.class_methods.get(&(class.to_string(), name.to_string())) + } + + /// mq.3: enumerate all `(class, method) → entry` pairs whose + /// method name matches `name`. Replaces the pre-mq.3 `get` + /// signature for callers that want all candidates for a method + /// name (today: zero or one entry; post-mq.3: zero or more). + pub fn class_method_candidates<'a>( + &'a self, + name: &str, + ) -> impl Iterator { + self.class_methods + .iter() + .filter_map(move |((c, m), e)| if m == name { Some((c, e)) } else { None }) + } +} +``` + +Existing callers of `has_class_method(name)` / `class_method_class(name)` / +`class_method(name)` need updating to pass the class — recon notes the +two call sites in `crates/ail/tests/typeclass_22b2.rs:42` etc. Update +those to pass the qualified class explicitly (e.g. +`mod_globals.class_method_class("test_22b2_class_method_lookup.Show", "show")`). + +- [ ] **Step 4: Update `build_module_globals` insert site** + +At `crates/ailang-check/src/lib.rs:1184-1240`, find the insert +inside the per-method loop in the `Def::Class` arm. Today's shape (per +mq.1): + +```rust +let qualified_class = format!("{mname}.{}", cd.name); +class_methods.insert( + method.name.clone(), + ClassMethodEntry { + class_name: qualified_class.clone(), + // ... + }, +); +``` + +Replace with: + +```rust +let qualified_class = format!("{mname}.{}", cd.name); +class_methods.insert( + (qualified_class.clone(), method.name.clone()), + ClassMethodEntry { + class_name: qualified_class, + // ... + }, +); +``` + +- [ ] **Step 5: Re-key `Env.class_methods`** + +At `crates/ailang-check/src/lib.rs:3366`, find the field: + +```rust +pub class_methods: BTreeMap, +``` + +Replace with: + +```rust +/// mq.3: workspace-flat aggregate of every module's +/// `ModuleGlobals.class_methods`, re-keyed to +/// `(qualified-class-name, method-name)` so post-retirement of +/// `MethodNameCollision` two classes can share a method name. +/// Consumed by `synth`'s Var-arm via `resolve_method_dispatch`'s +/// resolved class plus the method name. +pub class_methods: BTreeMap<(String, String), ClassMethodEntry>, +``` + +- [ ] **Step 6: Update `build_check_env` merge loop** + +At `crates/ailang-check/src/lib.rs:1343-1354`, the merge loop today +inserts via `env.class_methods.insert(n.clone(), e.clone())` per +method name. Update to use the tuple key: + +```rust +for ((qualified_class, method_name), entry) in &mg.class_methods { + env.class_methods.insert( + (qualified_class.clone(), method_name.clone()), + entry.clone(), + ); +} +``` + +(The exact iteration pattern matches the new `IndexMap<(String, String), ...>` +shape.) + +- [ ] **Step 7: Update synth Var-arm consumer at lib.rs:2497-2500** + +At the existing synth Var-arm class-method branch (per recon, the mq.2 +rewrite at lib.rs:2497-2500 does +`env.class_methods.get(method_name).expect("...")`): + +Replace with: + +```rust +let cm = env + .class_methods + .get(&(residual_class.clone(), method_name.to_string())) + .expect( + "method_to_candidate_classes invariant: \ + (resolved class, method) present implies class_methods entry", + ); +``` + +(Where `residual_class` is the `MethodDispatchOutcome::Resolved` / +`MethodDispatchOutcome::Multi.candidates.first()` tentative class +already computed by the outer match — see the mq.2 plan Task 6 Step 4 +code for context.) + +- [ ] **Step 8: Update mono presence checks at mono.rs:972, 1314** + +At `crates/ailang-check/src/mono.rs:972` and `mono.rs:1314`, today's +shape (per mq.2): + +```rust +let is_class_method = class_methods.contains_key(name); +``` + +`class_methods` here is `&BTreeMap` passed +in as a parameter. Post-T2 it's `&BTreeMap<(String, String), ClassMethodEntry>`, +so the bare `name`-keyed lookup no longer compiles. + +Switch the presence check to `method_to_candidate_classes`, which is +method-keyed natively. The mono caller already has access to `env`; +plumb `method_to_candidate_classes` through `rewrite_mono_calls` and +`interleave_slots` (mirroring how `class_methods` is plumbed): + +```rust +fn rewrite_mono_calls( + body: &mut Term, + class_methods: &BTreeMap<(String, String), ClassMethodEntry>, + method_to_candidate_classes: &BTreeMap>, + /* ...other existing params... */ +) { + // ... + let is_class_method = method_to_candidate_classes.contains_key(name); + // ... +} +``` + +Update the two call sites at `mono.rs:207, 218, 1283` to thread the +new parameter (existing `&env.class_methods` argument becomes a pair +`&env.class_methods, &env.method_to_candidate_classes`). + +- [ ] **Step 9: Run the mq.3.2 test + full ailang-check tests** + +Run: `cargo test --workspace -p ailang-check mq3_env_class_methods_tuple_keyed` +Run: `cargo test --workspace -p ailang-check` + +Expected: both green. With `MethodNameCollision` still active, every +workspace produces singleton-per-method entries; the new tuple key +adds disambiguation capacity but no behavioural change. + +- [ ] **Step 10: Run full cargo workspace test suite** + +Run: `cargo test --workspace` + +Expected: green. Downstream consumers (ail CLI, integration tests in +`crates/ail/tests/`) see qualified class names in residuals and mono +targets; tuple-keyed class_methods is internal to ailang-check. + +Note: the test-assertion update at `crates/ail/tests/typeclass_22b2.rs:42` +follows the accessor-signature change in Step 3. If that test breaks +on Step 9 with a "wrong number of arguments to class_method_class", +update its call site to pass the qualified class explicitly. + +--- + +## Task 3: `synth(...)` warnings channel + class-method-shadowed-by-fn warning + +**Files:** +- Modify: `crates/ailang-check/src/lib.rs:2351-2361` (synth signature) +- Modify: `crates/ailang-check/src/lib.rs:2402-2587` (synth Var-arm — reorder + emit warning) +- Modify: `crates/ailang-check/src/diagnostic.rs:64-79` (new diagnostic code) +- Modify: `crates/ailang-check/src/lib.rs:1622-1716` (check_fn — thread Vec + collect) +- Modify: `crates/ailang-check/src/lib.rs:840-967` (check_workspace — surface warnings) + +- [ ] **Step 1: Write a RED test for the new warning** + +Append in `crates/ailang-check/src/lib.rs` `mod tests`: + +```rust +/// mq.3.3: when synth's Var-arm sees a name that matches BOTH a +/// free fn (per lookup precedence: locals → caller-module-fn → +/// imported-fn) AND a class method (via `method_to_candidate_classes`), +/// the fn wins per spec §"Class-fn collisions" + a structured +/// warning `class-method-shadowed-by-fn` is emitted via the +/// warnings out-parameter. +#[test] +fn mq3_class_method_shadowed_by_fn_warning_fires() { + // Synthesise: a workspace with `class C { foo }` in module A + // and `fn foo : (Int) -> Int` in module B; a call site in + // module C imports both A and B and writes `foo 3`. Expected: + // typecheck passes (resolves to B.foo) with one warning carrying + // the structured code `class-method-shadowed-by-fn`. + let ws = build_class_vs_fn_workspace_for_test(); // helper builds the three-module workspace + let env = build_check_env(&ws); + let mut residuals = Vec::new(); + let mut free_fn_calls = Vec::new(); + let mut warnings: Vec = Vec::new(); + let mut counter: u64 = 0; + let mut locals = BTreeMap::new(); + let _ty = synth_term_for_test( + &Term::Var { name: "foo".to_string() }, + &env, + &mut residuals, + &mut free_fn_calls, + &mut warnings, + &mut counter, + &mut locals, + ); + assert!( + warnings.iter().any(|d| d.code == "class-method-shadowed-by-fn"), + "expected at least one class-method-shadowed-by-fn warning, got: {warnings:?}", + ); +} +``` + +(`build_class_vs_fn_workspace_for_test` is a test-only helper the +implementer adds inline; `synth_term_for_test` likewise. Pattern: +mirror existing helpers used by the mq.2 in-test synth invocations.) + +- [ ] **Step 2: Run test to confirm RED** + +Run: `cargo test --workspace -p ailang-check mq3_class_method_shadowed_by_fn_warning_fires` + +Expected: compile error — `synth`'s signature does not yet carry +`warnings: &mut Vec`. + +- [ ] **Step 3: Extend `synth(...)` signature with warnings out-parameter** + +At `crates/ailang-check/src/lib.rs:2351-2361`, find the synth function +signature. Add the new out-parameter (symmetric to the existing +`residuals` and `free_fn_calls`): + +```rust +fn synth( + term: &Term, + env: &Env, + residuals: &mut Vec, + free_fn_calls: &mut Vec, + warnings: &mut Vec, // mq.3: out-parameter for synth-time warnings + counter: &mut u64, + locals: &mut BTreeMap, +) -> Result { + // ...existing body... +} +``` + +Update every recursive call site of `synth(...)` inside the function body +to pass `warnings` through unchanged. + +- [ ] **Step 4: Update the two existing `synth` callers** + +`check_fn` at `crates/ailang-check/src/lib.rs:1680` and the in-test +synth-invocation helper at lib.rs:5919 (per recon) both call `synth`. +Add a `warnings: &mut Vec` local at each caller and pass +through. In `check_fn`, the warnings vector lives at fn-body-scope and +gets returned alongside the existing `Result<...>`. + +In `check_fn`: + +```rust +let mut warnings: Vec = Vec::new(); +let body_ty = synth( + &f.body, + &env, + &mut residuals, + &mut free_fn_calls, + &mut warnings, + &mut counter, + &mut locals, +)?; +// ...after synth returns, the warnings are surfaced via the +// check_fn signature extension below. +``` + +`check_fn`'s signature needs to return warnings. Existing signature: + +```rust +fn check_fn(/* ... */) -> Result +``` + +Extend (per recon `lib.rs:1622`): + +```rust +fn check_fn(/* ... */) -> Result<(CheckedFn, Vec), CheckError> +``` + +Update every caller of `check_fn` (recon names `check_workspace` at +`lib.rs:840-967`) to destructure the new tuple. + +- [ ] **Step 5: Surface warnings in `check_workspace`** + +At `crates/ailang-check/src/lib.rs:840-967`, `check_workspace` already +returns a `Vec` per the existing linearity-pass pattern +(`linearity.rs:194-208`). Append per-fn warnings into that collection. + +```rust +let mut all_warnings: Vec = Vec::new(); +for fn_def in /* ... */ { + let (checked, fn_warnings) = check_fn(/* ... */)?; + all_warnings.extend(fn_warnings); + // ...existing checked-fn handling... +} +// Return all_warnings alongside the existing check_workspace return. +``` + +(Existing diagnostic surface in `check_workspace` — implementer reads +the lines and threads the new warnings list into the existing return +shape verbatim.) + +- [ ] **Step 6: Reorder the synth Var-arm class-method branch (fn wins)** + +At `crates/ailang-check/src/lib.rs:2402-2587` (the resolution ladder +per recon), today's order is: + +``` +2402 locals +2404 same-module globals (fn / builtin) +2415-2518 class-method via method_to_candidate_classes +2519-2549 implicit-import free fn +2550-2584 dot-qualified explicit cross-module +2586 UnknownIdent +``` + +Move the implicit-import free-fn branch BEFORE the class-method branch. +The new order: + +``` +2402 locals +2404 same-module globals (fn / builtin) +2519-2549 implicit-import free fn ← moved up +2415-2518 class-method via method_to_candidate_classes ← moved down +2550-2584 dot-qualified explicit cross-module +2586 UnknownIdent +``` + +This implements "fn wins" per spec §"Class-fn collisions". + +- [ ] **Step 7: Emit the warning at the class-method branch when an upstream-fn match shadows** + +When the class-method branch is reached, check if the same `name` +ALSO had a match in the upstream fn branches (locals, same-module +fn, implicit-import fn). If yes, the class method has been +shadowed — but the upstream `return Ok(...)` already fired, so the +class-method branch never executes for shadowed-by-fn names. + +Reformulate: the warning should fire ABOVE the early-return in the +implicit-import-fn branch, conditional on `method_to_candidate_classes` +containing `parse_method_qualifier(name).0`. Concretely, at the +implicit-import-fn branch (post-Step-6 reorder, now upstream of the +class-method branch): + +```rust +} else if let Some((owner_module, raw_ty)) = /* implicit-import-fn match */ { + // ...existing qualification logic... + + // mq.3: if this name ALSO has class-method candidates, the fn is + // shadowing them. Emit a structured warning so the LLM-author can + // disambiguate if the shadow was unintentional. + let (method_name, _) = parse_method_qualifier(name); + if env.method_to_candidate_classes.contains_key(method_name) { + let candidates = env.method_to_candidate_classes.get(method_name).unwrap(); + let mut candidate_list: Vec = candidates.iter().cloned().collect(); + candidate_list.sort(); + warnings.push(Diagnostic::warning( + "class-method-shadowed-by-fn", + format!( + "free fn `{name}` in module `{owner_module}` shadows class method `{method_name}` \ + declared in classes {candidate_list:?}. \ + To call the class method instead, write `.{method_name} ...`.", + ), + )); + } + + return Ok(qualified); +} +``` + +(The exact `Diagnostic::warning` API is per `diagnostic.rs:186`; the +implementer matches the signature.) + +Symmetric warning placements at the locals branch + same-module-fn +branch (where the fn-wins precedence is already in place pre-mq.3): +the implementer adds the same shadow-check at those branches' early- +return sites. + +- [ ] **Step 8: Document the new diagnostic code in `diagnostic.rs`** + +In `crates/ailang-check/src/diagnostic.rs:64-79`, append: + +```rust +//! - `class-method-shadowed-by-fn` — mq.3: a `Term::Var.name` +//! resolved to a free fn (locals / caller-module-fn / +//! imported-fn), but the same method name has class-method +//! candidates in the workspace. Warning, not error — the fn +//! resolution proceeds; the warning surfaces the shadow so the +//! LLM-author can disambiguate. +``` + +- [ ] **Step 9: Run the mq.3.3 test to confirm GREEN** + +Run: `cargo test --workspace -p ailang-check mq3_class_method_shadowed_by_fn_warning_fires` + +Expected: PASS. + +- [ ] **Step 10: Run full ailang-check test suite** + +Run: `cargo test --workspace -p ailang-check` + +Expected: green. With `MethodNameCollision` still gating real +workspaces in `MethodNameCollision { kind: "class-fn" }` mode, no +real workspace currently has both a class method and a free fn of +the same name. The warning only fires post-Task-4 retirement. + +- [ ] **Step 11: Run full cargo workspace test suite** + +Run: `cargo test --workspace` + +Expected: green. + +--- + +## Task 4: Delete `MethodNameCollision` + +**Files:** +- Modify: `crates/ailang-core/src/workspace.rs:297-308` (delete variant) +- Modify: `crates/ailang-core/src/workspace.rs:609-694` (delete pre-pass + Origin enum) +- Modify: `crates/ail/src/main.rs:1201-1219` (delete Display arm) +- Modify: `crates/ailang-core/src/workspace.rs:2130-2200` (delete in-workspace.rs pin tests; relocation happens in Task 5) + +- [ ] **Step 1: Delete the `MethodNameCollision` variant** + +At `crates/ailang-core/src/workspace.rs:297-308`, remove the entire +variant: + +```rust +/// Iter 22b.2: a class-method name collides with another +/// class-method or with a top-level fn. `kind` is +/// `"class-class"` or `"class-fn"`. +#[error( + "method name `{method}` collides ({kind}): defined in `{first_origin}` and `{second_origin}`" +)] +MethodNameCollision { + method: String, + kind: &'static str, + first_origin: String, + second_origin: String, +}, +``` + +- [ ] **Step 2: Delete the pre-pass + `Origin` enum** + +At `crates/ailang-core/src/workspace.rs:609-694`, remove the entire +method-name-collision pre-pass — from the iter-22b.2 comment block at +609 down through the close of the per-def iteration loop at 694 +inclusive. This includes the `Origin` enum + `Origin::format` impl +defined inside. + +The pass-1 class scan (587-607) and pass-2 instance registry build +(696+) stay; they are independent. + +- [ ] **Step 3: Delete the Display arm in `main.rs`** + +At `crates/ail/src/main.rs:1201-1219`, remove the +`W::MethodNameCollision { ... } => ...` arm in +`workspace_error_to_diagnostic`. + +After the delete, verify the match expression compiles (no missing +arms — `WorkspaceLoadError` post-deletion no longer carries the +`MethodNameCollision` variant, so the match is complete without it). + +- [ ] **Step 4: Delete the in-workspace.rs pin tests** + +At `crates/ailang-core/src/workspace.rs:2130-2200`, remove the two +pin tests `class_class_method_name_collision_fires` and +`class_fn_method_name_collision_fires`. The relocated + inverted +versions land in Task 5; in this task they go out (and the on-disk +fixtures stay — they will be exercised by the relocated tests). + +- [ ] **Step 5: Run full cargo workspace test suite** + +Run: `cargo test --workspace` + +Expected: green. The two on-disk fixtures +`examples/test_22b2_method_name_collision_class_class.ail.json` and +`examples/test_22b2_method_name_collision_class_fn.ail.json` now load +cleanly (no diagnostic). No test asserts on them at this point — +Task 5 adds the new repurposed tests. + +If `cargo test` reports failures, they are tests that depended on the +`MethodNameCollision` shape; reading the failure output identifies the +exact stale assertion. Common case: a test asserts +`WorkspaceLoadError::MethodNameCollision { ... }` matches the load +result; post-deletion the test panics with "expected Err but got Ok". + +--- + +## Task 5: Relocate + invert the pin tests to `crates/ailang-check/tests/method_collision_pin.rs` + +**Files:** +- Create: `crates/ailang-check/tests/method_collision_pin.rs` + +- [ ] **Step 1: Create the new test file with the two repurposed pin tests** + +Create `crates/ailang-check/tests/method_collision_pin.rs`: + +```rust +//! mq.3.5: repurposed pin tests for the post-retirement workspace +//! load path. The two on-disk fixtures that fired +//! `MethodNameCollision` pre-mq.3 now load cleanly; the assertion +//! migrates from "expect collision diagnostic" to "load +//! successful + `method_to_candidate_classes` contains the +//! expected multi-entry set". + +use ailang_check::build_check_env; +use ailang_core::workspace::load_workspace_from_paths; +use std::path::Path; + +/// mq.3.5: `examples/test_22b2_method_name_collision_class_class.ail.json` +/// declares two classes in the same module each declaring a same- +/// named method (the pre-mq.3 invariant violation). Post-mq.3 the +/// workspace loads cleanly and `env.method_to_candidate_classes` +/// carries both classes as candidates for the method name. +#[test] +fn mq3_class_class_collision_loads_clean_and_populates_candidates() { + let ws = load_workspace_from_paths(&[ + Path::new("examples/test_22b2_method_name_collision_class_class.ail.json"), + ]) + .expect("post-mq.3: workspace loads without collision diagnostic"); + + let env = build_check_env(&ws); + // The fixture's method name is determined by reading the file — + // implementer substitutes the actual method name here. From the + // fixture: classes `A` and `B` each declare method `foo`. + let candidates = env + .method_to_candidate_classes + .get("foo") + .expect("foo must be in method_to_candidate_classes"); + let module_name = ws + .modules + .keys() + .find(|n| *n != "prelude") + .expect("non-prelude module") + .clone(); + assert!(candidates.contains(&format!("{module_name}.A")), + "candidates: {candidates:?}"); + assert!(candidates.contains(&format!("{module_name}.B")), + "candidates: {candidates:?}"); + assert_eq!(candidates.len(), 2); +} + +/// mq.3.5: `examples/test_22b2_method_name_collision_class_fn.ail.json` +/// declares `class Greet { greet }` and `fn greet`. Post-mq.3 the +/// workspace loads cleanly; the method-vs-fn coexistence is now +/// legal (warning fires at call sites, not at load time). +#[test] +fn mq3_class_fn_collision_loads_clean() { + load_workspace_from_paths(&[ + Path::new("examples/test_22b2_method_name_collision_class_fn.ail.json"), + ]) + .expect("post-mq.3: workspace loads — class-vs-fn name overlap is no longer a load error"); +} +``` + +(`load_workspace_from_paths` is the existing loader entry — implementer +matches the actual API name if it differs in `ailang-core`.) + +- [ ] **Step 2: Run the new tests** + +Run: `cargo test --workspace --test method_collision_pin` + +Expected: both PASS. + +- [ ] **Step 3: Run full cargo workspace test suite** + +Run: `cargo test --workspace` + +Expected: green. + +--- + +## Task 6: Three new positive E2E fixtures + integration tests + +**Files:** +- Create: `examples/mq3_two_show_ambiguous_a.ail.json` +- Create: `examples/mq3_two_show_ambiguous_b.ail.json` +- Create: `examples/mq3_two_show_ambiguous.ail.json` (entry; bare `show 42` → AmbiguousMethodResolution) +- Create: `examples/mq3_two_show_qualified.ail.json` (entry; `modA.Show.show 42` → resolves cleanly) +- Create: `examples/mq3_class_eq_vs_fn_eq_classmod.ail.json` +- Create: `examples/mq3_class_eq_vs_fn_eq_fnmod.ail.json` +- Create: `examples/mq3_class_eq_vs_fn_eq.ail.json` (entry; `eq x y` → fn wins + warning) +- Create: `crates/ail/tests/mq3_multi_class_e2e.rs` + +- [ ] **Step 1: Create fixture (a) — two Show classes + Show Int each** + +Create `examples/mq3_two_show_ambiguous_a.ail.json`: + +```json +{ + "schema": "ailang/v0", + "name": "mq3_two_show_ambiguous_a", + "imports": [], + "defs": [ + { "kind": "class", + "name": "Show", + "param": "a", + "methods": [ + { "name": "show", + "type": { + "k": "fn", + "params": [{ "k": "var", "name": "a" }], + "param_modes": ["borrow"], + "ret": { "k": "con", "name": "Str" }, + "effects": [] + } + } + ] + }, + { "kind": "instance", + "class": "Show", + "type": { "k": "con", "name": "Int" }, + "methods": [ + { "name": "show", + "body": { "t": "app", "fn": { "t": "var", "name": "int_to_str" }, + "args": [{ "t": "var", "name": "x" }] } + } + ] + } + ] +} +``` + +Create `examples/mq3_two_show_ambiguous_b.ail.json` (identical +structure, different module name): + +```json +{ + "schema": "ailang/v0", + "name": "mq3_two_show_ambiguous_b", + "imports": [], + "defs": [ + { "kind": "class", + "name": "Show", + "param": "a", + "methods": [ + { "name": "show", + "type": { + "k": "fn", + "params": [{ "k": "var", "name": "a" }], + "param_modes": ["borrow"], + "ret": { "k": "con", "name": "Str" }, + "effects": [] + } + } + ] + }, + { "kind": "instance", + "class": "Show", + "type": { "k": "con", "name": "Int" }, + "methods": [ + { "name": "show", + "body": { "t": "app", "fn": { "t": "var", "name": "int_to_str" }, + "args": [{ "t": "var", "name": "x" }] } + } + ] + } + ] +} +``` + +(The `body` shape for the instance methods follows the pattern from +existing `examples/mq1_xmod_constraint_class_dep.ail.json`; implementer +matches the actual instance-method-body schema if it differs. Schema: +each instance method has a `body` field carrying a `Term`; the body +for `show` is `\x -> int_to_str x` — but JSON-side it's emitted as +just the `Term::App` without the outer lambda because the instance- +method body's lambda is implicit. Implementer adjusts to match the +exact shape used by `examples/prelude.ail.json`'s Eq/Ord instances.) + +- [ ] **Step 2: Create fixture (a) entry module — bare `show 42`** + +Create `examples/mq3_two_show_ambiguous.ail.json`: + +```json +{ + "schema": "ailang/v0", + "name": "mq3_two_show_ambiguous", + "imports": ["mq3_two_show_ambiguous_a", "mq3_two_show_ambiguous_b"], + "defs": [ + { "kind": "fn", + "name": "main", + "type": { + "k": "fn", + "params": [], + "param_modes": [], + "ret": { "k": "con", "name": "Unit" }, + "effects": ["IO"] + }, + "params": [], + "body": { + "t": "do", + "op": "io/print_str", + "args": [{ "t": "app", + "fn": { "t": "var", "name": "show" }, + "args": [{ "t": "lit", "k": "int", "v": 42 }] }] + } + } + ] +} +``` + +- [ ] **Step 3: Create fixture (b) entry module — explicit qualifier** + +Create `examples/mq3_two_show_qualified.ail.json`: + +```json +{ + "schema": "ailang/v0", + "name": "mq3_two_show_qualified", + "imports": ["mq3_two_show_ambiguous_a", "mq3_two_show_ambiguous_b"], + "defs": [ + { "kind": "fn", + "name": "main", + "type": { + "k": "fn", + "params": [], + "param_modes": [], + "ret": { "k": "con", "name": "Unit" }, + "effects": ["IO"] + }, + "params": [], + "body": { + "t": "do", + "op": "io/print_str", + "args": [{ "t": "app", + "fn": { "t": "var", "name": "mq3_two_show_ambiguous_a.Show.show" }, + "args": [{ "t": "lit", "k": "int", "v": 42 }] }] + } + } + ] +} +``` + +- [ ] **Step 4: Create fixture (c) — class Eq + fn eq + entry** + +Create `examples/mq3_class_eq_vs_fn_eq_classmod.ail.json`: + +```json +{ + "schema": "ailang/v0", + "name": "mq3_class_eq_vs_fn_eq_classmod", + "imports": [], + "defs": [ + { "kind": "class", + "name": "MyEq", + "param": "a", + "methods": [ + { "name": "myeq", + "type": { + "k": "fn", + "params": [{ "k": "var", "name": "a" }, { "k": "var", "name": "a" }], + "param_modes": ["borrow", "borrow"], + "ret": { "k": "con", "name": "Bool" }, + "effects": [] + } + } + ] + }, + { "kind": "instance", + "class": "MyEq", + "type": { "k": "con", "name": "Int" }, + "methods": [ + { "name": "myeq", + "body": { "t": "var", "name": "true" } + } + ] + } + ] +} +``` + +Create `examples/mq3_class_eq_vs_fn_eq_fnmod.ail.json`: + +```json +{ + "schema": "ailang/v0", + "name": "mq3_class_eq_vs_fn_eq_fnmod", + "imports": [], + "defs": [ + { "kind": "fn", + "name": "myeq", + "type": { + "k": "fn", + "params": [{ "k": "con", "name": "Int" }, { "k": "con", "name": "Int" }], + "param_modes": ["borrow", "borrow"], + "ret": { "k": "con", "name": "Bool" }, + "effects": [] + }, + "params": ["x", "y"], + "body": { "t": "var", "name": "false" } + } + ] +} +``` + +Create `examples/mq3_class_eq_vs_fn_eq.ail.json` (entry): + +```json +{ + "schema": "ailang/v0", + "name": "mq3_class_eq_vs_fn_eq", + "imports": ["mq3_class_eq_vs_fn_eq_classmod", "mq3_class_eq_vs_fn_eq_fnmod"], + "defs": [ + { "kind": "fn", + "name": "main", + "type": { + "k": "fn", + "params": [], + "param_modes": [], + "ret": { "k": "con", "name": "Unit" }, + "effects": ["IO"] + }, + "params": [], + "body": { + "t": "do", + "op": "io/print_bool", + "args": [{ "t": "app", + "fn": { "t": "var", "name": "myeq" }, + "args": [{ "t": "lit", "k": "int", "v": 1 }, + { "t": "lit", "k": "int", "v": 2 }] }] + } + } + ] +} +``` + +(Naming choice: `MyEq`/`myeq` rather than reusing the prelude's +`Eq`/`eq` to avoid an additional prelude-vs-fn shadow that would +confuse the warning assertion. Implementer adjusts the +`io/print_bool` op call if the prelude builtin's exact name differs.) + +- [ ] **Step 5: Create the integration-test file** + +Create `crates/ail/tests/mq3_multi_class_e2e.rs`: + +```rust +//! mq.3.6: end-to-end coverage of the post-mq.3 multi-candidate +//! dispatch path. Three positive fixtures exercise the three +//! trajectories from spec §"Data flow": +//! - ambiguous (Trajectory C): `AmbiguousMethodResolution`. +//! - explicit qualifier (Trajectory E): clean resolve. +//! - class-fn shadow: fn wins + warning. + +use std::path::Path; +use std::process::Command; + +fn run_ail_check(entry: &str, deps: &[&str]) -> std::process::Output { + let mut cmd = Command::new(env!("CARGO_BIN_EXE_ail")); + cmd.arg("check").arg("--json").arg(entry); + for dep in deps { + cmd.arg(dep); + } + cmd.output().expect("ail check must run") +} + +/// mq.3.6: bare `show 42` in a workspace with two Show classes both +/// shipping Show Int fires `AmbiguousMethodResolution` with both +/// candidate classes named. +#[test] +fn mq3_two_show_ambiguous_fires_ambiguous_method_resolution() { + let out = run_ail_check( + "examples/mq3_two_show_ambiguous.ail.json", + &[ + "examples/mq3_two_show_ambiguous_a.ail.json", + "examples/mq3_two_show_ambiguous_b.ail.json", + ], + ); + assert!(!out.status.success(), + "expected check to reject ambiguous dispatch"); + let stdout = String::from_utf8_lossy(&out.stdout); + assert!(stdout.contains("ambiguous-method-resolution"), + "expected ambiguous-method-resolution diagnostic, got: {stdout}"); + assert!(stdout.contains("mq3_two_show_ambiguous_a.Show"), + "expected candidate A named, got: {stdout}"); + assert!(stdout.contains("mq3_two_show_ambiguous_b.Show"), + "expected candidate B named, got: {stdout}"); +} + +/// mq.3.6: explicit qualifier `modA.Show.show 42` resolves to +/// modA's class cleanly. +#[test] +fn mq3_two_show_qualified_resolves_clean() { + let out = run_ail_check( + "examples/mq3_two_show_qualified.ail.json", + &[ + "examples/mq3_two_show_ambiguous_a.ail.json", + "examples/mq3_two_show_ambiguous_b.ail.json", + ], + ); + assert!(out.status.success(), + "expected check to succeed with explicit qualifier, got: {}", + String::from_utf8_lossy(&out.stdout)); +} + +/// mq.3.6: `eq x y` in a workspace with `class MyEq` + `fn myeq` +/// resolves to the fn per lookup precedence; structured warning +/// `class-method-shadowed-by-fn` fires. +#[test] +fn mq3_class_eq_vs_fn_eq_fn_wins_with_warning() { + let out = run_ail_check( + "examples/mq3_class_eq_vs_fn_eq.ail.json", + &[ + "examples/mq3_class_eq_vs_fn_eq_classmod.ail.json", + "examples/mq3_class_eq_vs_fn_eq_fnmod.ail.json", + ], + ); + assert!(out.status.success(), + "expected check to succeed (fn wins): {}", + String::from_utf8_lossy(&out.stdout)); + let stdout = String::from_utf8_lossy(&out.stdout); + assert!(stdout.contains("class-method-shadowed-by-fn"), + "expected shadow warning, got: {stdout}"); +} +``` + +(The `run_ail_check` helper builds an `ail check` invocation. The exact +CLI flag for emitting structured-JSON diagnostics is `--json` per the +existing CLI convention; implementer verifies the flag name from the +existing CLI test files at `crates/ail/tests/ct1_check_cli.rs`.) + +- [ ] **Step 6: Run the new E2E tests** + +Run: `cargo test --workspace --test mq3_multi_class_e2e` + +Expected: all three PASS. + +- [ ] **Step 7: Run full cargo workspace test suite** + +Run: `cargo test --workspace` + +Expected: green. + +--- + +## Task 7: DESIGN.md sync + +**Files:** +- Modify: `docs/DESIGN.md:1156-1160` (rewrite class-names-bare paragraph) +- Modify: `docs/DESIGN.md:1717-1755` (Diagnostic categories — strike MethodNameCollision, add new diagnostics, reword AmbiguousInstance paragraph) +- Modify: `docs/DESIGN.md` (new subsection "Method dispatch") + +- [ ] **Step 1: Rewrite the `class names stay bare` paragraph** + +At `docs/DESIGN.md:1156-1160`, replace the paragraph that today +references `MethodNameCollision`: + +```markdown +Class names follow the canonical-form rule (mq.1): bare for same- +module references, `.` for cross-module references — +symmetric to `Type::Con.name`'s rule from ct.1. Three schema fields +carry class references in this form: `InstanceDef.class`, +`Constraint.class`, and `SuperclassRef.class`. `ClassDef.name` +itself stays bare (defining-site context, like `TypeDef.name`). + +Method dispatch is type-driven post-mq.3 (see §"Method dispatch"): +synth resolves a `Term::Var { name: "show" }` by consulting the +workspace's method-to-candidate-class index, filtering by argument +type (concrete) or by declared constraint (rigid-var), and routing +the residual through the registry at fn-body-end discharge. +Method-name collisions across classes are now structurally legal — +they resolve at the call site via type-driven dispatch with explicit +qualifier (`..`) as the LLM-author's +disambiguation tool. +``` + +- [ ] **Step 2: Strike `MethodNameCollision` from Diagnostic categories** + +At `docs/DESIGN.md:1730-1731` (per recon, under "Workspace-load +(registry-build) diagnostics"), remove the `MethodNameCollision` +bullet entirely. + +- [ ] **Step 3: Add new typecheck diagnostics** + +At `docs/DESIGN.md:1747-1755` (per recon, under "Typecheck +diagnostics"), append: + +```markdown +- `AmbiguousMethodResolution` — a monomorphic `Term::Var` call site + has multiple candidate classes after type-driven and constraint- + driven filters. LLM-author writes the explicit qualifier form + `..` to disambiguate. +- `UnknownClass` — an explicit class qualifier in `Term::Var.name` + names a qualified class that is not in the workspace registry. +- `class-method-shadowed-by-fn` (warning) — a `Term::Var` resolved + via fn lookup precedence (locals → caller-module-fn → + imported-fn) while a class method of the same name also exists + in the workspace. Fn resolution proceeds; the warning surfaces + the shadow so the LLM-author can disambiguate via explicit + class-qualified call if the shadow was unintentional. +- `NoInstance.candidate_classes` — when the bare-method dispatch + path fails because none of the candidate classes have an instance + for the concrete argument type, the diagnostic surfaces the + candidate list so the LLM-author knows which class needs an + instance. +``` + +- [ ] **Step 4: Reword the `AmbiguousInstance` paragraph** + +At `docs/DESIGN.md:1753-1755`, replace the existing wording with: + +```markdown +There is no `AmbiguousInstance` diagnostic at the registry level — +coherence (`DuplicateInstance` at registry build) makes per-`(class, +type)` ambiguity structurally impossible. Cross-class method +ambiguity is a separate concern resolved at the call site via +`AmbiguousMethodResolution` (see §"Method dispatch"). +``` + +- [ ] **Step 5: Add the `## Method dispatch` subsection** + +Append a new top-level subsection at the end of the diagnostic- +categories block (after `docs/DESIGN.md:1755`). Implementer adjusts +the heading depth to match surrounding structure (`##` if the +diagnostic block is `##`, `###` otherwise). + +```markdown +## Method dispatch + +Post-mq.3, dispatch is two-mode: + +**Polymorphic call sites** (inside a fn body with `forall` + +constraint set): the constraint names the class via the qualified +`Constraint.class` field (canonical-form per mq.1). Synth's +residual carries the class name directly; constraint-discharge at +fn-body-end matches against the workspace registry by +`(class, type_hash)` key. + +**Monomorphic call sites**: synth consults the workspace-flat +`Env.method_to_candidate_classes: BTreeMap>` index, then runs the 5-step +dispatch rule: + +1. Parse the `Term::Var.name` for an optional class qualifier + (last-dot-segment is the method name; everything before is the + qualified class). +2. If `method_to_candidate_classes` has no entry for the method + name, fall through to the existing Var-arm branches (free fn + lookup, dot-qualified cross-module). +3. Qualifier present: filter candidates to the named class. + Empty result fires `UnknownClass`. Singleton survivor proceeds. +4. Qualifier empty (bare-method form): singleton candidate proceeds + directly; multiple candidates yield a multi-candidate residual + for discharge-time refinement. +5. At discharge, refinement runs: concrete `type_` filters + candidates via the workspace registry; rigid-var `type_` filters + via the active fn's declared constraints. Single survivor + discharges; multiple survivors fire `AmbiguousMethodResolution` + (concrete) or `MissingConstraint` (rigid-var); zero survivors + fire `NoInstance` (concrete) or `MissingConstraint` (rigid-var). + +The `method_to_candidate_classes` index is the load-bearing data +structure for this routing — its construction in `build_check_env` +inverts the per-module `class_methods` maps to a workspace-flat +method-name-to-class-set map. + +Class-fn collisions resolve at the call site, not at workspace load +time: the fn lookup precedence (locals → caller-module-fn → +imported-fn) runs ahead of the class-method branch. When both +sides have a match, the fn wins and a `class-method-shadowed-by-fn` +warning surfaces the shadow. +``` + +- [ ] **Step 6: Run the DESIGN.md drift test** + +Run: `cargo test --workspace -p ailang-check design_schema_drift` + +(If the test exists with that exact name; otherwise check +`crates/ailang-check/src/lib.rs` `mod tests` for the test that scans +DESIGN.md for required anchors.) + +Expected: green. If the test fails on the modified DESIGN.md +sections, update the test's anchor list to match the new wording. + +- [ ] **Step 7: Run full cargo workspace test suite** + +Run: `cargo test --workspace` + +Expected: green. + +--- + +## Task 8: Roadmap update + +**Files:** +- Modify: `docs/roadmap.md:126-143` (P2 entry — mark `[x]`) +- Modify: `docs/roadmap.md:64-86` (P1 entry — strike `depends on:` line + annotate) + +- [ ] **Step 1: Mark the P2 milestone as done** + +At `docs/roadmap.md:126`, find the entry `- [ ] **[milestone]** +Module-qualified class names + type-driven method dispatch — ...`. +Flip the checkbox to `[x]`: + +```markdown +- [x] **[milestone]** Module-qualified class names + type-driven method dispatch — retire the `MethodNameCollision` workaround [...] +``` + +(Optionally trim the body once closed; convention is to leave the +description in place until the entry is purged on the next roadmap +sweep.) + +- [ ] **Step 2: Clear milestone 24's `depends on:` line + annotate** + +At `docs/roadmap.md:64-86`, the P1 entry "Post-22 Prelude — Show + +print rewire" carries (per recon) a `depends on:` line at 83-85: + +```markdown + - depends on: P2 milestone "Module-qualified class names + + type-driven method dispatch" (retires the `MethodNameCollision` + workaround that drives the collision). +``` + +Strike that line. Then add an annotation under the entry: + +```markdown + - ready for re-brainstorm — the `MethodNameCollision` workaround + that blocked the original spec retired in mq.3 (2026-05-13). + Fresh brainstorm re-derives the spec against the post-retirement + architecture (type-driven dispatch, class-ref canonical form). +``` + +- [ ] **Step 3: Run `cargo test --workspace`** + +Run: `cargo test --workspace` + +Expected: green. Roadmap edits are doc-only. + +--- + +## Task 9: Integration verification — full cargo + bench + roundtrip + +**Files:** none modified; verification only. + +- [ ] **Step 1: Full `cargo test --workspace`** + +Run: `cargo test --workspace` + +Expected: green. The new and repurposed tests pass; existing tests +unchanged. + +- [ ] **Step 2: Spot-check the three new E2E fixtures via `ail check`** + +Run: +``` +cargo run -p ail -- check examples/mq3_two_show_ambiguous.ail.json +``` +Expected: exit non-zero with `ambiguous-method-resolution` diagnostic. + +Run: +``` +cargo run -p ail -- check examples/mq3_two_show_qualified.ail.json +``` +Expected: exit 0. + +Run: +``` +cargo run -p ail -- check examples/mq3_class_eq_vs_fn_eq.ail.json +``` +Expected: exit 0 with `class-method-shadowed-by-fn` warning surfaced. + +- [ ] **Step 3: Verify zero diff on prelude** + +Run: `git diff -- examples/prelude.ail.json` + +Expected: no output (prelude unchanged). + +- [ ] **Step 4: Bench scripts** + +Run: `python3 bench/check.py` +Expected: exit 0 OR audit-ratified. + +Run: `python3 bench/compile_check.py` +Expected: exit 0 OR audit-ratified. + +Run: `python3 bench/cross_lang.py` +Expected: exit 0 OR audit-ratified. + +- [ ] **Step 5: Confirm the per-iter journal placeholder is staged** + +The implementer leaves `docs/journals/2026-05-13-iter-mq.3.md` (or the +date the iter actually finishes) in the working tree with a draft +entry covering: tasks ran, deviations from plan (if any), the +`class_methods` re-key consumer-rewrite scope, the synth warnings- +channel plumbing, the three E2E fixtures, DESIGN.md sync, roadmap +update, test counts, and bench outcomes. Append a pointer line to +`docs/journals/INDEX.md`. The Boss commits. + +Note for milestone-close: WhatsNew.md is shipped at milestone-close- +audit (separate iter), NOT in mq.3 itself.