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