iter mq.3: retire MethodNameCollision + multi-class E2E + DESIGN.md sync
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).
This commit is contained in:
+91
-10
@@ -1153,11 +1153,23 @@ qualified references whose owner is unknown are also a violation
|
||||
(`WorkspaceLoadError::BadCrossModuleTypeRef`). The same rule
|
||||
applies to `Term::Ctor.type_name`.
|
||||
|
||||
Class names (`ClassDef.name`, `InstanceDef.class`,
|
||||
`SuperclassRef.class`, `Constraint.class`) are NOT module-scoped
|
||||
under this rule; they remain workspace-flat with
|
||||
`MethodNameCollision` enforced at load. Class-name scoping is a
|
||||
future milestone with its own DESIGN amendment.
|
||||
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"
|
||||
below): 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.
|
||||
|
||||
The legacy `(con T)` form is treated as `(own T)` semantically.
|
||||
|
||||
@@ -1727,8 +1739,12 @@ wording is fixed; the categories and their triggers are:
|
||||
- `MissingMethod` — instance omits a required method.
|
||||
- `OverridingNonExistentMethod` — instance specifies a method not in
|
||||
the class.
|
||||
- `MethodNameCollision` — same method name across two in-scope
|
||||
classes, or between a class method and a top-level function.
|
||||
|
||||
(`MethodNameCollision` was retired at iter mq.3. Cross-class method
|
||||
sharing is now structurally legal; ambiguity surfaces at the call
|
||||
site via `AmbiguousMethodResolution` or, for class-fn name overlap,
|
||||
via the `class-method-shadowed-by-fn` warning. See §"Method
|
||||
dispatch" below.)
|
||||
|
||||
**Class-schema diagnostics** (validation of class declarations):
|
||||
|
||||
@@ -1749,10 +1765,75 @@ at iter ctt.3.
|
||||
- `MissingConstraint` — body's residual constraint is not covered
|
||||
by declared (and superclass-expanded) constraints.
|
||||
- `NoInstance` — fully concrete constraint has no registry entry.
|
||||
mq.2 adds an optional `candidate_classes` field surfacing the
|
||||
multi-candidate set when the bare-method dispatch path's filter
|
||||
collapses to zero registry survivors.
|
||||
- `AmbiguousMethodResolution` (mq.2) — a monomorphic `Term::Var`
|
||||
call site survives both type-driven and constraint-driven filters
|
||||
with more than one candidate class. LLM-author writes the explicit
|
||||
qualifier form `<module>.<Class>.<method>` to disambiguate.
|
||||
- `UnknownClass` (mq.2) — an explicit class qualifier in
|
||||
`Term::Var.name` names a qualified class that is not in the
|
||||
workspace's candidate-class index for the method.
|
||||
- `class-method-shadowed-by-fn` (mq.3, 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.
|
||||
|
||||
There is no `AmbiguousInstance` diagnostic. Coherence (W2) makes
|
||||
every `(class, type)` key globally unique; resolution is therefore
|
||||
deterministic by construction.
|
||||
There is no `AmbiguousInstance` diagnostic at the registry level —
|
||||
coherence (`DuplicateInstance` at registry build, W2) 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" below).
|
||||
|
||||
### 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
|
||||
(`env.active_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 (themselves tuple-keyed
|
||||
by `(qualified-class, method)` post-mq.3) 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.
|
||||
|
||||
### What the typeclass design explicitly does NOT support
|
||||
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
# 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
|
||||
@@ -44,3 +44,4 @@
|
||||
- 2026-05-12 — iter 24.1: `bool_to_str` + `str_clone` runtime + codegen wiring — 2 new C functions in `runtime/str.c` (slab-allocating heap-Str primitives via existing `str_alloc`), lockstep checker + synth.rs installs with `ret_mode: Own`, 2 IR-header declares + 2 `lower_app` arms + `is_static_callee` whitelist extension, 5 IR snapshots regen (2 declares × 5 files), 9 new tests (2 builtins-install unit + 2 IR-shape pin + 5 E2E all green), pre-existing-drift fix included (int_to_str row added to `builtins.rs::list()`). One substantive deviation: builtin-signatures registered in `uniqueness.rs::infer_module` + `linearity.rs::check_module_with_visible` (8 LOC × 2 files) so `str_clone`'s `param_modes: [Borrow]` is visible to the App-arg walker; symmetric to 23.4-prep's class-method registration; necessary for the plan's literal `frees == 3` cross-realisation assertion. Full `cargo test --workspace` 513 passed, 0 failed. `bench/compile_check.py` + `bench/cross_lang.py` green → 2026-05-12-iter-24.1.md
|
||||
- 2026-05-13 — iter mq.1: class-ref canonical-form extension + workspace-internal class-name qualification — three schema fields (`InstanceDef.class`, `Constraint.class`, `SuperclassRef.class`) move bare → canonical (bare for same-module, `<module>.<Class>` for cross-module) symmetric to ct.1's `Type::Con.name` rule; `ClassDef.name` stays bare (defining site, like `TypeDef.name`). `validate_canonical_type_names` gains three field-walks via new `check_class_ref` helper plus two sibling diagnostics `BareCrossModuleClassRef` / `BadCrossModuleClassRef`; `check_class_name_fields` narrowed to `ClassDef.name`-only. Workspace-internal class-name keys all qualified: `class_def_module`, `class_by_name`, registry `entries.0`, `ClassMethodEntry.class_name`, `class_superclasses`, mono's `class_index`, `Origin::Class.class_name`. New `qualify_class_ref` helper in `workspace.rs` plus sibling `qualify_class_ref_in_check` in `ailang-check`. Two new positive on-disk fixtures (`mq1_xmod_constraint_class{,_dep}`). Recon claim that all existing fixtures' class-refs are intra-module was empirically wrong — 5 test_22b* (`orphan_third`, `dup_a/b/entry`, `unbound_constraint_var`) + 5 other (`eq_ord_polymorphic`, `eq_ord_user_adt`, `cmp_max_smoke`, `ctt2_collision_{lib,main}`) fixtures required minimal canonical-form migration. Duplicate-instance test architecturally restructured: post-mq.1 orphan-freedom makes two-modules-on-same-`(class, type)` structurally impossible (any second instance in a non-owning module fires `OrphanInstance` first); new `test_22b1_dup_same_module.ail.json` lands both instances in the class's module — the only post-mq.1 way to land two on the same canonical key. Plan defects fixed inline: `Origin::Class.class_name` had one construction site not two (plan recon off-by-one); `build_class_index` in `mono.rs` needed qualifying (plan said "no code change needed"); `build_module_globals` needed a `Def::Instance` carve-out for qualified `inst.class`; `check_fn`'s declared-constraint matching needed inline canonical-form lifting; `NoInstance` Float-aware message arm needed `prelude.Eq`/`prelude.Ord` match; coherence type-leg needed qualified-head split-and-lookup. Tasks 3-6 ran as one coherent fix (Task 3 alone left tests RED). 7/7 tasks, 520 tests green, `bench/compile_check.py` + `bench/cross_lang.py` exit 0; `bench/check.py` exit 1 due to 4 improvements-beyond-tolerance (audit-ratifiable per convention, not a regression). `MethodNameCollision` + dispatch path unchanged; iters mq.2 + mq.3 land them → 2026-05-13-iter-mq.1.md
|
||||
- 2026-05-13 — iter mq.2: type-driven dispatch mechanism installed (mechanism-before-exercise) — `Env.method_to_candidate_classes` workspace-flat inverse index built alongside `class_methods`; two new `CheckError` variants `AmbiguousMethodResolution` + `UnknownClass` (Display+code+ctx) plus additive `NoInstance.candidate_classes` field; `ResidualConstraint` extended with `candidates: Option<BTreeSet<String>>` (visibility bumped `pub(crate)` → `pub` for test-crate access); pure `resolve_method_dispatch` helper implementing the spec's 5-step rule (qualifier → singleton → type-driven filter → constraint-driven filter → `Multi` for discharge-time refinement); synth Var-arm class-method branch rewritten via `parse_method_qualifier` with inner-dot gate (qualifier must be `<module>.<Class>` form, single-dot names like `std_list.length` fall through to the existing qualified-fn path); `refine_multi_candidate_residual` wired into `check_fn` discharge using `expanded` (post-superclass-expansion constraints) for the rigid-var path; `resolve_residual_class_for_mono` wired into mono's `collect_residuals_ordered` residual-to-target mapping. With `MethodNameCollision` still gating real workspaces, the new multi-candidate branches are exercised exclusively by 15 unit tests: 6 in `tests/method_dispatch_pin.rs` covering the 5-step rule's six cases, 6 in `lib.rs` `mod tests` covering variants + field shapes + discharge refinement, 3 in `mono.rs` `mod tests` covering the mono helper. Real workspaces continue producing single-class residuals (`candidates: None`); every pre-mq.2 fixture typechecks unchanged. Plan-invented `format_type_for_display` replaced with `ailang_core::pretty::type_to_string` (one less duplicate). Synth-time `declared_constraints: &[]` is a deliberate gap documented as known debt — load-bearing only post-mq.3 for the rigid-var fallback (env-plumbing the active fn's constraints into the Var arm is a ~10-line edit slated for mq.3). 9/9 tasks, 539 tests green (was 520 at start of mq.2; +19 = 15 new + 4 pre-existing); `bench/compile_check.py` + `cross_lang.py` clean; `bench/check.py` 1 regression (latency noise, runtime cannot be touched by typecheck-side iter). `MethodNameCollision` retirement lands in mq.3 → 2026-05-13-iter-mq.2.md
|
||||
- 2026-05-13 — iter mq.3: `MethodNameCollision` retired + multi-class E2E + DESIGN.md sync — milestone close. Deletes `WorkspaceLoadError::MethodNameCollision` variant + `Origin` enum + per-def collision loop in `build_registry` (workspace.rs 596-681) + Display arm in `main.rs:1201`; the two in-workspace.rs pin tests relocate to `crates/ailang-check/tests/method_collision_pin.rs` with inverted assertions (loads cleanly + `env.method_to_candidate_classes["foo"]` has two qualified-class entries). Resolves both mq.2 known-debt items: (1) `Env.active_declared_constraints: Vec<Constraint>` plumbed pre-synth in `check_fn` so the post-superclass-expansion constraint set reaches the synth Var-arm's `resolve_method_dispatch` constraint-driven filter; (2) `ModuleGlobals.class_methods` + `Env.class_methods` re-keyed from `BTreeMap<MethodName, ClassMethodEntry>` to `BTreeMap<(QualifiedClass, MethodName), ClassMethodEntry>` with new `class_method_candidates(name) -> Vec<(&class, &entry)>` accessor; mono's two presence-check sites (`rewrite_mono_calls`, `interleave_slots`) switched to consult `method_to_candidate_classes` natively (method-keyed, preserves the presence-check signature shape). New `synth(...)` warnings channel via `warnings: &mut Vec<Diagnostic>` out-parameter threaded through 15+ recursive callsites + 5 external callers (check_fn, check_const, 3 in builtins.rs, 1 in lift.rs, 2 in mono.rs, 1 in `check`); new structured warning `class-method-shadowed-by-fn` (kebab-case code, structured ctx carrying `name`/`method`/`fn_owner_module`/`candidate_classes`) fires 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. Three new positive E2E fixtures + integration tests in `crates/ail/tests/mq3_multi_class_e2e.rs`: (a) `mq3_two_show_ambiguous` (two `Show` classes, both with `Show Int`, bare `show 42` → `AmbiguousMethodResolution`); (b) `mq3_two_show_qualified` (same workspace, `mq3_two_show_ambiguous_a.Show.show 42` → clean); (c) `mq3_class_eq_vs_fn_eq` (`class MyEq { myeq }` + `fn myeq` + bare `myeq 1 2` → fn wins, warning fires). DESIGN.md sync: class-names paragraph 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; `AmbiguousMethodResolution` / `UnknownClass` / `class-method-shadowed-by-fn` + `NoInstance.candidate_classes` added to "Typecheck diagnostics"; `AmbiguousInstance` paragraph reworded to distinguish registry-level (per-class coherence via `DuplicateInstance`) from call-site (cross-class method ambiguity, new diagnostic); 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. Roadmap P2 milestone → `[x]` with three-iter summary; milestone-24 `depends on:` line struck and entry annotated "ready for re-brainstorm". Plan defects fixed inline: `check_fn` signature is `Result<()>` not `Result<(CheckedFn, Vec<Diagnostic>)>` — adopted mut-ref-accumulator pattern matching existing `Vec<CheckError>` shape; `class_method_candidates` returns `Vec<(...)>` not `impl Iterator<...>` (the `use<'a, '_>` opaque-type-capture syntax not yet idiomatic in this crate); instance-method body shape draft was `Term::App` but existing-convention is `Term::Lam` with `paramTypes`/`retType` — three fixtures repaired inline. One existing E2E test (`crates/ail/tests/typeclass_22b3.rs:rewrite_walker_skips_locally_shadowed_class_method`) now correctly fires the new warning (the fixture intentionally shadows a class method); test's `assert!(diags.is_empty())` relaxed to filter the new warning with a naming comment. 9/9 tasks, 545 tests green (was 539; +6 net = 3 new mq.3.x lib + 2 method_collision_pin + 3 mq3_multi_class_e2e − 2 deleted workspace.rs pin tests); `bench/compile_check.py` + `cross_lang.py` exit 0; `bench/check.py` exit 1 with 2 noise-class regressions (3rd-consecutive `bench_list_sum.bump_s` persistence + max_us tail metric, both runtime-uncoupled-to-typecheck-iter); prelude zero-diff. Module-qualified-class-names milestone structurally closed → 2026-05-13-iter-mq.3.md
|
||||
|
||||
+19
-20
@@ -80,9 +80,10 @@ context. Pick the next milestone from P1.)_
|
||||
- context: brainstorm 2026-05-12 (iter 23.5 wrap); deferral
|
||||
2026-05-13 (user-direction Option C after plan-recon flagged the
|
||||
collision; iter 24.1 retained as standalone runtime infrastructure).
|
||||
- depends on: P2 milestone "Module-qualified class names +
|
||||
type-driven method dispatch" (retires the `MethodNameCollision`
|
||||
workaround that drives the collision).
|
||||
- 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).
|
||||
|
||||
## P2 — Medium-term
|
||||
|
||||
@@ -123,24 +124,22 @@ context. Pick the next milestone from P1.)_
|
||||
classes beyond the prelude four; multi-parameter classes; superclass
|
||||
chains; richer instance bodies. Deferred from milestone 22.
|
||||
- context: JOURNAL 2026-05-09
|
||||
- [ ] **\[milestone\]** Module-qualified class names + type-driven
|
||||
method dispatch — retire the `MethodNameCollision` workaround
|
||||
(`crates/ailang-core/src/workspace.rs:472`) that today keeps bare
|
||||
class names viable by enforcing workspace-global method-name
|
||||
uniqueness. Replaces name-driven method resolution
|
||||
(`ModuleGlobals::class_methods: IndexMap<String, ClassMethodEntry>`,
|
||||
`crates/ailang-check/src/lib.rs:983`; consumed by
|
||||
`mono::rewrite_class_method_calls`, `crates/ailang-check/src/mono.rs:657`)
|
||||
with type-driven dispatch — look up `eq` candidates by argument
|
||||
type, pick the unique class instance, fail closed on ambiguity.
|
||||
Once shipped, two libraries can each declare `class Eq` with their
|
||||
own `eq`. Carries its own DESIGN spec: inference rules, ambiguity
|
||||
diagnostics, and the canonical-form extension for class-reference
|
||||
fields (`InstanceDef.class`, `SuperclassRef.class`, `Constraint.class`)
|
||||
that the canonical-type-names milestone explicitly out-of-scoped.
|
||||
- [x] **\[milestone\]** Module-qualified class names + type-driven
|
||||
method dispatch — retired the `MethodNameCollision` workaround;
|
||||
shipped 2026-05-13 as iters mq.1 (canonical-form extension for
|
||||
class-ref fields + workspace-internal qualification), mq.2 (type-
|
||||
driven dispatch mechanism installed: `method_to_candidate_classes`
|
||||
inverse index, multi-candidate `ResidualConstraint`,
|
||||
`resolve_method_dispatch` 5-step rule, `AmbiguousMethodResolution` +
|
||||
`UnknownClass` diagnostics), and mq.3 (retirement + multi-class E2E
|
||||
+ `class-method-shadowed-by-fn` warning + DESIGN.md sync). Two
|
||||
libraries can now each declare `class Eq` with their own `eq`;
|
||||
cross-class method ambiguity is resolved at the call site via
|
||||
type-driven dispatch with `<module>.<Class>.<method>` as the
|
||||
disambiguation form.
|
||||
- context: `docs/specs/2026-05-10-canonical-type-names.md` "Out of
|
||||
scope: Class names" — the workaround is named there so it stays
|
||||
visible until this milestone retires it.
|
||||
scope: Class names" — the workaround was named there so it
|
||||
stayed visible until this milestone retired it.
|
||||
- [ ] **\[todo\]** Boehm full retirement — remove the transitional
|
||||
Boehm GC path now that RC + uniqueness is the canonical memory
|
||||
story.
|
||||
|
||||
Reference in New Issue
Block a user