Files
AILang/docs/plans/0054-mq.3.md
T
Brummel 832375f2ac convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
All 176 files in the four accumulating directories now use a
zero-padded 4-digit counter prefix that reflects creation order
(`NNNN-slug.md`). The counter is assigned per directory in strict
git-log creation order; ties broken alphabetically by original name.
The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is
dropped — the date is recoverable from git log and the counter
carries the ordering.

A file's counter is stable for the life of the file: never reassigned,
never reused, never compacted. Deleted files retire their counter;
subsequent files do not fill the gap. This is the property that lets
cross-references stay literal — refs use the full filename including
the counter (`design/contracts/0007-honesty-rule.md`) so they grep
cleanly and resolve directly without a glob step.

313 cross-references updated across .md/.rs/.toml/.c/.json files
(test pins, include_str! paths, design-INDEX entries, baseline notes,
runtime C comments, inter-contract markdown links incl. bare basename
and `../models/foo.md` forms).

CLAUDE.md gets a new "File-naming convention" section spelling out
the rule and rationale. skills/brainstorm/SKILL.md and
skills/planner/SKILL.md updated so new spec/plan creation produces
counter-prefixed names from the start.

The full test suite (cargo test --workspace) passes.
2026-05-28 13:31:31 +02:00

51 KiB

Iter mq.3 — Retire MethodNameCollision + multi-class E2E + DESIGN.md sync — Implementation Plan

Parent spec: docs/specs/0023-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<Constraint> 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<Diagnostic> 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<ClassMethodEntry> 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:

/// 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):

/// 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<Constraint>,

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:

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:

let outcome = resolve_method_dispatch(
    method_name,
    qualifier_prefix.as_deref(),
    candidates,
    /*concrete_arg_type*/ None,
    /*declared_constraints*/ &env.active_declared_constraints,
    /*registry*/ &registry_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:

/// 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:

pub class_methods: IndexMap<String, ClassMethodEntry>,

Replace with:

/// 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:

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<Item = (&'a String, &'a ClassMethodEntry)> {
        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):

let qualified_class = format!("{mname}.{}", cd.name);
class_methods.insert(
    method.name.clone(),
    ClassMethodEntry {
        class_name: qualified_class.clone(),
        // ...
    },
);

Replace with:

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:

pub class_methods: BTreeMap<String, ClassMethodEntry>,

Replace with:

/// 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:

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:

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):

let is_class_method = class_methods.contains_key(name);

class_methods here is &BTreeMap<String, ClassMethodEntry> 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):

fn rewrite_mono_calls(
    body: &mut Term,
    class_methods: &BTreeMap<(String, String), ClassMethodEntry>,
    method_to_candidate_classes: &BTreeMap<String, BTreeSet<String>>,
    /* ...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:

/// 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<Diagnostic> = 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<Diagnostic>.

  • 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):

fn synth(
    term: &Term,
    env: &Env,
    residuals: &mut Vec<ResidualConstraint>,
    free_fn_calls: &mut Vec<FreeFnCall>,
    warnings: &mut Vec<Diagnostic>,  // mq.3: out-parameter for synth-time warnings
    counter: &mut u64,
    locals: &mut BTreeMap<String, Type>,
) -> Result<Type, CheckError> {
    // ...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<Diagnostic> 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:

let mut warnings: Vec<Diagnostic> = 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:

fn check_fn(/* ... */) -> Result<CheckedFn, CheckError>

Extend (per recon lib.rs:1622):

fn check_fn(/* ... */) -> Result<(CheckedFn, Vec<Diagnostic>), 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<Diagnostic> per the existing linearity-pass pattern (linearity.rs:194-208). Append per-fn warnings into that collection.

let mut all_warnings: Vec<Diagnostic> = 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):

} 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<String> = 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 `<ClassQualifier>.{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:

//! - `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:

/// 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:

//! 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:

{
  "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):

{
  "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:

{
  "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:

{
  "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:

{
  "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:

{
  "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):

{
  "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:

//! 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:

Class names follow the canonical-form rule (mq.1): bare for same-
module references, `<module>.<Class>` 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 (`<module>.<Class>.<method>`) 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:

- `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
  `<module>.<Class>.<method>` 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:

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).

## 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<MethodName,
BTreeSet<QualifiedClassName>>` 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]:

- [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:

  - 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:

  - 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.