832375f2ac
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.
1474 lines
51 KiB
Markdown
1474 lines
51 KiB
Markdown
# 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`:
|
|
|
|
```rust
|
|
/// mq.3.1: `Env` carries an `active_declared_constraints` field
|
|
/// holding the active fn's POST-superclass-expansion declared
|
|
/// constraints. Empty at workspace entry; populated by `check_fn`
|
|
/// before invoking `synth` on the fn body. Consumed by `synth`'s
|
|
/// Var-arm class-method branch when calling `resolve_method_dispatch`
|
|
/// (constraint-driven filter path).
|
|
#[test]
|
|
fn mq3_env_active_declared_constraints_field_exists() {
|
|
let env = Env::default();
|
|
assert_eq!(env.active_declared_constraints.len(), 0,
|
|
"active_declared_constraints empty at default-construction");
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to confirm RED**
|
|
|
|
Run: `cargo test --workspace -p ailang-check mq3_env_active_declared_constraints_field_exists`
|
|
|
|
Expected: compile error — `active_declared_constraints` field does not exist on `Env`.
|
|
|
|
- [ ] **Step 3: Add the field to `Env`**
|
|
|
|
At `crates/ailang-check/src/lib.rs:3311-3391`, find the `Env` struct.
|
|
Add a field immediately after `class_methods` (Env field at lib.rs:3366):
|
|
|
|
```rust
|
|
/// mq.3: post-superclass-expansion declared constraints of the
|
|
/// active fn body being checked. Populated by `check_fn` before
|
|
/// invoking `synth` on the body; cleared at workspace entry.
|
|
/// Consumed by `synth`'s Var-arm class-method branch when calling
|
|
/// `resolve_method_dispatch` — the constraint-driven filter
|
|
/// disambiguates multi-candidate residuals at the rigid-var
|
|
/// fallback (e.g. inside a polymorphic fn body where the arg
|
|
/// type is still a rigid type variable).
|
|
pub active_declared_constraints: Vec<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:
|
|
|
|
```rust
|
|
let mut env = env.clone();
|
|
env.rigid_vars.insert(/* existing insert call */);
|
|
// mq.3: stash declared constraints for synth-time access by the
|
|
// class-method dispatch resolver's constraint-driven filter.
|
|
env.active_declared_constraints = expanded.clone();
|
|
```
|
|
|
|
(The exact placement is after the existing `expanded` computation and
|
|
before the `synth(...)` call at lib.rs:1680. Implementer adjusts the
|
|
insertion line.)
|
|
|
|
- [ ] **Step 5: Update `resolve_method_dispatch` call site in synth Var-arm to use `env.active_declared_constraints`**
|
|
|
|
At `crates/ailang-check/src/lib.rs:2462-2469` (per recon, the existing
|
|
`resolve_method_dispatch` call site passes `&[]` for
|
|
`declared_constraints`). Replace `&[]` with
|
|
`&env.active_declared_constraints`:
|
|
|
|
```rust
|
|
let outcome = resolve_method_dispatch(
|
|
method_name,
|
|
qualifier_prefix.as_deref(),
|
|
candidates,
|
|
/*concrete_arg_type*/ None,
|
|
/*declared_constraints*/ &env.active_declared_constraints,
|
|
/*registry*/ ®istry_unit,
|
|
);
|
|
```
|
|
|
|
- [ ] **Step 6: Run the mq.3.1 test to confirm GREEN**
|
|
|
|
Run: `cargo test --workspace -p ailang-check mq3_env_active_declared_constraints_field_exists`
|
|
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 7: Run full ailang-check test suite**
|
|
|
|
Run: `cargo test --workspace -p ailang-check`
|
|
|
|
Expected: green. With `MethodNameCollision` still active, the
|
|
constraint-driven filter is unreachable (singleton candidate set short-
|
|
circuits before reaching the filter); existing tests carry the same
|
|
behaviour.
|
|
|
|
---
|
|
|
|
## Task 2: Re-key `class_methods` to `(QualifiedClassName, MethodName)`
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-check/src/lib.rs:1129` (ModuleGlobals.class_methods type)
|
|
- Modify: `crates/ailang-check/src/lib.rs:1184-1240` (build_module_globals insert site)
|
|
- Modify: `crates/ailang-check/src/lib.rs:1086-1102` (ModuleGlobals accessor methods)
|
|
- Modify: `crates/ailang-check/src/lib.rs:3366` (Env.class_methods type)
|
|
- Modify: `crates/ailang-check/src/lib.rs:1343-1354` (build_check_env merge loop)
|
|
- Modify: `crates/ailang-check/src/lib.rs:2497-2500` (synth Var-arm class_methods lookup)
|
|
- Modify: `crates/ailang-check/src/mono.rs:967-998, 1268-1284` (mono presence-check sites switch to method_to_candidate_classes)
|
|
|
|
- [ ] **Step 1: Write a RED test for the new key shape**
|
|
|
|
Append in `crates/ailang-check/src/lib.rs` `mod tests`:
|
|
|
|
```rust
|
|
/// mq.3.2: `Env.class_methods` is keyed by `(QualifiedClassName,
|
|
/// MethodName)` tuple — post-mq.3 the workspace can carry multiple
|
|
/// classes sharing a method name, so the key must disambiguate.
|
|
/// O(1) lookup post-dispatch via `(resolved_class, method)`.
|
|
#[test]
|
|
fn mq3_env_class_methods_tuple_keyed() {
|
|
let mut modules = BTreeMap::new();
|
|
modules.insert(
|
|
"m".to_string(),
|
|
Module {
|
|
name: "m".to_string(),
|
|
imports: vec![],
|
|
defs: vec![Def::Class(ClassDef {
|
|
name: "MyCls".to_string(),
|
|
param: "a".to_string(),
|
|
superclass: None,
|
|
methods: vec![ClassMethod {
|
|
name: "mymethod".to_string(),
|
|
ty: Type::Fn {
|
|
params: vec![Type::Var { name: "a".to_string() }],
|
|
param_modes: vec![ParamMode::Borrow],
|
|
ret: Box::new(Type::Con { name: "Str".to_string(), args: vec![] }),
|
|
effects: vec![],
|
|
},
|
|
default: None,
|
|
}],
|
|
doc: None,
|
|
})],
|
|
},
|
|
);
|
|
let ws = build_workspace_for_test(modules);
|
|
let env = build_check_env(&ws);
|
|
let key = ("m.MyCls".to_string(), "mymethod".to_string());
|
|
assert!(env.class_methods.contains_key(&key),
|
|
"env.class_methods must contain {key:?}");
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to confirm RED**
|
|
|
|
Run: `cargo test --workspace -p ailang-check mq3_env_class_methods_tuple_keyed`
|
|
|
|
Expected: compile error or runtime panic — today's key is `String`,
|
|
not `(String, String)`.
|
|
|
|
- [ ] **Step 3: Re-key `ModuleGlobals.class_methods`**
|
|
|
|
At `crates/ailang-check/src/lib.rs:1129`, find the field:
|
|
|
|
```rust
|
|
pub class_methods: IndexMap<String, ClassMethodEntry>,
|
|
```
|
|
|
|
Replace with:
|
|
|
|
```rust
|
|
/// mq.3: re-keyed by `(qualified-class-name, method-name)` tuple —
|
|
/// post-retirement of `MethodNameCollision`, two classes can declare
|
|
/// same-named methods within the same workspace. The tuple key
|
|
/// disambiguates without losing the per-module ordering guarantee
|
|
/// (`IndexMap` preserves insertion order). Lookups by method-name-
|
|
/// only consult `method_to_candidate_classes` (Env-level) to get
|
|
/// the candidate-class set, then index into `class_methods` with
|
|
/// `(resolved_class, method)`.
|
|
pub class_methods: IndexMap<(String, String), ClassMethodEntry>,
|
|
```
|
|
|
|
Update the accessor methods at lib.rs:1086-1102:
|
|
|
|
```rust
|
|
impl ModuleGlobals {
|
|
pub fn has_class_method(&self, class: &str, name: &str) -> bool {
|
|
self.class_methods.contains_key(&(class.to_string(), name.to_string()))
|
|
}
|
|
|
|
pub fn class_method_class(&self, class: &str, name: &str) -> Option<&str> {
|
|
self.class_methods
|
|
.get(&(class.to_string(), name.to_string()))
|
|
.map(|e| e.class_name.as_str())
|
|
}
|
|
|
|
pub fn class_method(&self, class: &str, name: &str) -> Option<&ClassMethodEntry> {
|
|
self.class_methods.get(&(class.to_string(), name.to_string()))
|
|
}
|
|
|
|
/// mq.3: enumerate all `(class, method) → entry` pairs whose
|
|
/// method name matches `name`. Replaces the pre-mq.3 `get`
|
|
/// signature for callers that want all candidates for a method
|
|
/// name (today: zero or one entry; post-mq.3: zero or more).
|
|
pub fn class_method_candidates<'a>(
|
|
&'a self,
|
|
name: &str,
|
|
) -> impl Iterator<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):
|
|
|
|
```rust
|
|
let qualified_class = format!("{mname}.{}", cd.name);
|
|
class_methods.insert(
|
|
method.name.clone(),
|
|
ClassMethodEntry {
|
|
class_name: qualified_class.clone(),
|
|
// ...
|
|
},
|
|
);
|
|
```
|
|
|
|
Replace with:
|
|
|
|
```rust
|
|
let qualified_class = format!("{mname}.{}", cd.name);
|
|
class_methods.insert(
|
|
(qualified_class.clone(), method.name.clone()),
|
|
ClassMethodEntry {
|
|
class_name: qualified_class,
|
|
// ...
|
|
},
|
|
);
|
|
```
|
|
|
|
- [ ] **Step 5: Re-key `Env.class_methods`**
|
|
|
|
At `crates/ailang-check/src/lib.rs:3366`, find the field:
|
|
|
|
```rust
|
|
pub class_methods: BTreeMap<String, ClassMethodEntry>,
|
|
```
|
|
|
|
Replace with:
|
|
|
|
```rust
|
|
/// mq.3: workspace-flat aggregate of every module's
|
|
/// `ModuleGlobals.class_methods`, re-keyed to
|
|
/// `(qualified-class-name, method-name)` so post-retirement of
|
|
/// `MethodNameCollision` two classes can share a method name.
|
|
/// Consumed by `synth`'s Var-arm via `resolve_method_dispatch`'s
|
|
/// resolved class plus the method name.
|
|
pub class_methods: BTreeMap<(String, String), ClassMethodEntry>,
|
|
```
|
|
|
|
- [ ] **Step 6: Update `build_check_env` merge loop**
|
|
|
|
At `crates/ailang-check/src/lib.rs:1343-1354`, the merge loop today
|
|
inserts via `env.class_methods.insert(n.clone(), e.clone())` per
|
|
method name. Update to use the tuple key:
|
|
|
|
```rust
|
|
for ((qualified_class, method_name), entry) in &mg.class_methods {
|
|
env.class_methods.insert(
|
|
(qualified_class.clone(), method_name.clone()),
|
|
entry.clone(),
|
|
);
|
|
}
|
|
```
|
|
|
|
(The exact iteration pattern matches the new `IndexMap<(String, String), ...>`
|
|
shape.)
|
|
|
|
- [ ] **Step 7: Update synth Var-arm consumer at lib.rs:2497-2500**
|
|
|
|
At the existing synth Var-arm class-method branch (per recon, the mq.2
|
|
rewrite at lib.rs:2497-2500 does
|
|
`env.class_methods.get(method_name).expect("...")`):
|
|
|
|
Replace with:
|
|
|
|
```rust
|
|
let cm = env
|
|
.class_methods
|
|
.get(&(residual_class.clone(), method_name.to_string()))
|
|
.expect(
|
|
"method_to_candidate_classes invariant: \
|
|
(resolved class, method) present implies class_methods entry",
|
|
);
|
|
```
|
|
|
|
(Where `residual_class` is the `MethodDispatchOutcome::Resolved` /
|
|
`MethodDispatchOutcome::Multi.candidates.first()` tentative class
|
|
already computed by the outer match — see the mq.2 plan Task 6 Step 4
|
|
code for context.)
|
|
|
|
- [ ] **Step 8: Update mono presence checks at mono.rs:972, 1314**
|
|
|
|
At `crates/ailang-check/src/mono.rs:972` and `mono.rs:1314`, today's
|
|
shape (per mq.2):
|
|
|
|
```rust
|
|
let is_class_method = class_methods.contains_key(name);
|
|
```
|
|
|
|
`class_methods` here is `&BTreeMap<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):
|
|
|
|
```rust
|
|
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<Diagnostic> + collect)
|
|
- Modify: `crates/ailang-check/src/lib.rs:840-967` (check_workspace — surface warnings)
|
|
|
|
- [ ] **Step 1: Write a RED test for the new warning**
|
|
|
|
Append in `crates/ailang-check/src/lib.rs` `mod tests`:
|
|
|
|
```rust
|
|
/// mq.3.3: when synth's Var-arm sees a name that matches BOTH a
|
|
/// free fn (per lookup precedence: locals → caller-module-fn →
|
|
/// imported-fn) AND a class method (via `method_to_candidate_classes`),
|
|
/// the fn wins per spec §"Class-fn collisions" + a structured
|
|
/// warning `class-method-shadowed-by-fn` is emitted via the
|
|
/// warnings out-parameter.
|
|
#[test]
|
|
fn mq3_class_method_shadowed_by_fn_warning_fires() {
|
|
// Synthesise: a workspace with `class C { foo }` in module A
|
|
// and `fn foo : (Int) -> Int` in module B; a call site in
|
|
// module C imports both A and B and writes `foo 3`. Expected:
|
|
// typecheck passes (resolves to B.foo) with one warning carrying
|
|
// the structured code `class-method-shadowed-by-fn`.
|
|
let ws = build_class_vs_fn_workspace_for_test(); // helper builds the three-module workspace
|
|
let env = build_check_env(&ws);
|
|
let mut residuals = Vec::new();
|
|
let mut free_fn_calls = Vec::new();
|
|
let mut warnings: Vec<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`):
|
|
|
|
```rust
|
|
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`:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
fn check_fn(/* ... */) -> Result<CheckedFn, CheckError>
|
|
```
|
|
|
|
Extend (per recon `lib.rs:1622`):
|
|
|
|
```rust
|
|
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.
|
|
|
|
```rust
|
|
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):
|
|
|
|
```rust
|
|
} else if let Some((owner_module, raw_ty)) = /* implicit-import-fn match */ {
|
|
// ...existing qualification logic...
|
|
|
|
// mq.3: if this name ALSO has class-method candidates, the fn is
|
|
// shadowing them. Emit a structured warning so the LLM-author can
|
|
// disambiguate if the shadow was unintentional.
|
|
let (method_name, _) = parse_method_qualifier(name);
|
|
if env.method_to_candidate_classes.contains_key(method_name) {
|
|
let candidates = env.method_to_candidate_classes.get(method_name).unwrap();
|
|
let mut candidate_list: Vec<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:
|
|
|
|
```rust
|
|
//! - `class-method-shadowed-by-fn` — mq.3: a `Term::Var.name`
|
|
//! resolved to a free fn (locals / caller-module-fn /
|
|
//! imported-fn), but the same method name has class-method
|
|
//! candidates in the workspace. Warning, not error — the fn
|
|
//! resolution proceeds; the warning surfaces the shadow so the
|
|
//! LLM-author can disambiguate.
|
|
```
|
|
|
|
- [ ] **Step 9: Run the mq.3.3 test to confirm GREEN**
|
|
|
|
Run: `cargo test --workspace -p ailang-check mq3_class_method_shadowed_by_fn_warning_fires`
|
|
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 10: Run full ailang-check test suite**
|
|
|
|
Run: `cargo test --workspace -p ailang-check`
|
|
|
|
Expected: green. With `MethodNameCollision` still gating real
|
|
workspaces in `MethodNameCollision { kind: "class-fn" }` mode, no
|
|
real workspace currently has both a class method and a free fn of
|
|
the same name. The warning only fires post-Task-4 retirement.
|
|
|
|
- [ ] **Step 11: Run full cargo workspace test suite**
|
|
|
|
Run: `cargo test --workspace`
|
|
|
|
Expected: green.
|
|
|
|
---
|
|
|
|
## Task 4: Delete `MethodNameCollision`
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-core/src/workspace.rs:297-308` (delete variant)
|
|
- Modify: `crates/ailang-core/src/workspace.rs:609-694` (delete pre-pass + Origin enum)
|
|
- Modify: `crates/ail/src/main.rs:1201-1219` (delete Display arm)
|
|
- Modify: `crates/ailang-core/src/workspace.rs:2130-2200` (delete in-workspace.rs pin tests; relocation happens in Task 5)
|
|
|
|
- [ ] **Step 1: Delete the `MethodNameCollision` variant**
|
|
|
|
At `crates/ailang-core/src/workspace.rs:297-308`, remove the entire
|
|
variant:
|
|
|
|
```rust
|
|
/// Iter 22b.2: a class-method name collides with another
|
|
/// class-method or with a top-level fn. `kind` is
|
|
/// `"class-class"` or `"class-fn"`.
|
|
#[error(
|
|
"method name `{method}` collides ({kind}): defined in `{first_origin}` and `{second_origin}`"
|
|
)]
|
|
MethodNameCollision {
|
|
method: String,
|
|
kind: &'static str,
|
|
first_origin: String,
|
|
second_origin: String,
|
|
},
|
|
```
|
|
|
|
- [ ] **Step 2: Delete the pre-pass + `Origin` enum**
|
|
|
|
At `crates/ailang-core/src/workspace.rs:609-694`, remove the entire
|
|
method-name-collision pre-pass — from the iter-22b.2 comment block at
|
|
609 down through the close of the per-def iteration loop at 694
|
|
inclusive. This includes the `Origin` enum + `Origin::format` impl
|
|
defined inside.
|
|
|
|
The pass-1 class scan (587-607) and pass-2 instance registry build
|
|
(696+) stay; they are independent.
|
|
|
|
- [ ] **Step 3: Delete the Display arm in `main.rs`**
|
|
|
|
At `crates/ail/src/main.rs:1201-1219`, remove the
|
|
`W::MethodNameCollision { ... } => ...` arm in
|
|
`workspace_error_to_diagnostic`.
|
|
|
|
After the delete, verify the match expression compiles (no missing
|
|
arms — `WorkspaceLoadError` post-deletion no longer carries the
|
|
`MethodNameCollision` variant, so the match is complete without it).
|
|
|
|
- [ ] **Step 4: Delete the in-workspace.rs pin tests**
|
|
|
|
At `crates/ailang-core/src/workspace.rs:2130-2200`, remove the two
|
|
pin tests `class_class_method_name_collision_fires` and
|
|
`class_fn_method_name_collision_fires`. The relocated + inverted
|
|
versions land in Task 5; in this task they go out (and the on-disk
|
|
fixtures stay — they will be exercised by the relocated tests).
|
|
|
|
- [ ] **Step 5: Run full cargo workspace test suite**
|
|
|
|
Run: `cargo test --workspace`
|
|
|
|
Expected: green. The two on-disk fixtures
|
|
`examples/test_22b2_method_name_collision_class_class.ail.json` and
|
|
`examples/test_22b2_method_name_collision_class_fn.ail.json` now load
|
|
cleanly (no diagnostic). No test asserts on them at this point —
|
|
Task 5 adds the new repurposed tests.
|
|
|
|
If `cargo test` reports failures, they are tests that depended on the
|
|
`MethodNameCollision` shape; reading the failure output identifies the
|
|
exact stale assertion. Common case: a test asserts
|
|
`WorkspaceLoadError::MethodNameCollision { ... }` matches the load
|
|
result; post-deletion the test panics with "expected Err but got Ok".
|
|
|
|
---
|
|
|
|
## Task 5: Relocate + invert the pin tests to `crates/ailang-check/tests/method_collision_pin.rs`
|
|
|
|
**Files:**
|
|
- Create: `crates/ailang-check/tests/method_collision_pin.rs`
|
|
|
|
- [ ] **Step 1: Create the new test file with the two repurposed pin tests**
|
|
|
|
Create `crates/ailang-check/tests/method_collision_pin.rs`:
|
|
|
|
```rust
|
|
//! mq.3.5: repurposed pin tests for the post-retirement workspace
|
|
//! load path. The two on-disk fixtures that fired
|
|
//! `MethodNameCollision` pre-mq.3 now load cleanly; the assertion
|
|
//! migrates from "expect collision diagnostic" to "load
|
|
//! successful + `method_to_candidate_classes` contains the
|
|
//! expected multi-entry set".
|
|
|
|
use ailang_check::build_check_env;
|
|
use ailang_core::workspace::load_workspace_from_paths;
|
|
use std::path::Path;
|
|
|
|
/// mq.3.5: `examples/test_22b2_method_name_collision_class_class.ail.json`
|
|
/// declares two classes in the same module each declaring a same-
|
|
/// named method (the pre-mq.3 invariant violation). Post-mq.3 the
|
|
/// workspace loads cleanly and `env.method_to_candidate_classes`
|
|
/// carries both classes as candidates for the method name.
|
|
#[test]
|
|
fn mq3_class_class_collision_loads_clean_and_populates_candidates() {
|
|
let ws = load_workspace_from_paths(&[
|
|
Path::new("examples/test_22b2_method_name_collision_class_class.ail.json"),
|
|
])
|
|
.expect("post-mq.3: workspace loads without collision diagnostic");
|
|
|
|
let env = build_check_env(&ws);
|
|
// The fixture's method name is determined by reading the file —
|
|
// implementer substitutes the actual method name here. From the
|
|
// fixture: classes `A` and `B` each declare method `foo`.
|
|
let candidates = env
|
|
.method_to_candidate_classes
|
|
.get("foo")
|
|
.expect("foo must be in method_to_candidate_classes");
|
|
let module_name = ws
|
|
.modules
|
|
.keys()
|
|
.find(|n| *n != "prelude")
|
|
.expect("non-prelude module")
|
|
.clone();
|
|
assert!(candidates.contains(&format!("{module_name}.A")),
|
|
"candidates: {candidates:?}");
|
|
assert!(candidates.contains(&format!("{module_name}.B")),
|
|
"candidates: {candidates:?}");
|
|
assert_eq!(candidates.len(), 2);
|
|
}
|
|
|
|
/// mq.3.5: `examples/test_22b2_method_name_collision_class_fn.ail.json`
|
|
/// declares `class Greet { greet }` and `fn greet`. Post-mq.3 the
|
|
/// workspace loads cleanly; the method-vs-fn coexistence is now
|
|
/// legal (warning fires at call sites, not at load time).
|
|
#[test]
|
|
fn mq3_class_fn_collision_loads_clean() {
|
|
load_workspace_from_paths(&[
|
|
Path::new("examples/test_22b2_method_name_collision_class_fn.ail.json"),
|
|
])
|
|
.expect("post-mq.3: workspace loads — class-vs-fn name overlap is no longer a load error");
|
|
}
|
|
```
|
|
|
|
(`load_workspace_from_paths` is the existing loader entry — implementer
|
|
matches the actual API name if it differs in `ailang-core`.)
|
|
|
|
- [ ] **Step 2: Run the new tests**
|
|
|
|
Run: `cargo test --workspace --test method_collision_pin`
|
|
|
|
Expected: both PASS.
|
|
|
|
- [ ] **Step 3: Run full cargo workspace test suite**
|
|
|
|
Run: `cargo test --workspace`
|
|
|
|
Expected: green.
|
|
|
|
---
|
|
|
|
## Task 6: Three new positive E2E fixtures + integration tests
|
|
|
|
**Files:**
|
|
- Create: `examples/mq3_two_show_ambiguous_a.ail.json`
|
|
- Create: `examples/mq3_two_show_ambiguous_b.ail.json`
|
|
- Create: `examples/mq3_two_show_ambiguous.ail.json` (entry; bare `show 42` → AmbiguousMethodResolution)
|
|
- Create: `examples/mq3_two_show_qualified.ail.json` (entry; `modA.Show.show 42` → resolves cleanly)
|
|
- Create: `examples/mq3_class_eq_vs_fn_eq_classmod.ail.json`
|
|
- Create: `examples/mq3_class_eq_vs_fn_eq_fnmod.ail.json`
|
|
- Create: `examples/mq3_class_eq_vs_fn_eq.ail.json` (entry; `eq x y` → fn wins + warning)
|
|
- Create: `crates/ail/tests/mq3_multi_class_e2e.rs`
|
|
|
|
- [ ] **Step 1: Create fixture (a) — two Show classes + Show Int each**
|
|
|
|
Create `examples/mq3_two_show_ambiguous_a.ail.json`:
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "mq3_two_show_ambiguous_a",
|
|
"imports": [],
|
|
"defs": [
|
|
{ "kind": "class",
|
|
"name": "Show",
|
|
"param": "a",
|
|
"methods": [
|
|
{ "name": "show",
|
|
"type": {
|
|
"k": "fn",
|
|
"params": [{ "k": "var", "name": "a" }],
|
|
"param_modes": ["borrow"],
|
|
"ret": { "k": "con", "name": "Str" },
|
|
"effects": []
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{ "kind": "instance",
|
|
"class": "Show",
|
|
"type": { "k": "con", "name": "Int" },
|
|
"methods": [
|
|
{ "name": "show",
|
|
"body": { "t": "app", "fn": { "t": "var", "name": "int_to_str" },
|
|
"args": [{ "t": "var", "name": "x" }] }
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Create `examples/mq3_two_show_ambiguous_b.ail.json` (identical
|
|
structure, different module name):
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "mq3_two_show_ambiguous_b",
|
|
"imports": [],
|
|
"defs": [
|
|
{ "kind": "class",
|
|
"name": "Show",
|
|
"param": "a",
|
|
"methods": [
|
|
{ "name": "show",
|
|
"type": {
|
|
"k": "fn",
|
|
"params": [{ "k": "var", "name": "a" }],
|
|
"param_modes": ["borrow"],
|
|
"ret": { "k": "con", "name": "Str" },
|
|
"effects": []
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{ "kind": "instance",
|
|
"class": "Show",
|
|
"type": { "k": "con", "name": "Int" },
|
|
"methods": [
|
|
{ "name": "show",
|
|
"body": { "t": "app", "fn": { "t": "var", "name": "int_to_str" },
|
|
"args": [{ "t": "var", "name": "x" }] }
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
(The `body` shape for the instance methods follows the pattern from
|
|
existing `examples/mq1_xmod_constraint_class_dep.ail.json`; implementer
|
|
matches the actual instance-method-body schema if it differs. Schema:
|
|
each instance method has a `body` field carrying a `Term`; the body
|
|
for `show` is `\x -> int_to_str x` — but JSON-side it's emitted as
|
|
just the `Term::App` without the outer lambda because the instance-
|
|
method body's lambda is implicit. Implementer adjusts to match the
|
|
exact shape used by `examples/prelude.ail.json`'s Eq/Ord instances.)
|
|
|
|
- [ ] **Step 2: Create fixture (a) entry module — bare `show 42`**
|
|
|
|
Create `examples/mq3_two_show_ambiguous.ail.json`:
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "mq3_two_show_ambiguous",
|
|
"imports": ["mq3_two_show_ambiguous_a", "mq3_two_show_ambiguous_b"],
|
|
"defs": [
|
|
{ "kind": "fn",
|
|
"name": "main",
|
|
"type": {
|
|
"k": "fn",
|
|
"params": [],
|
|
"param_modes": [],
|
|
"ret": { "k": "con", "name": "Unit" },
|
|
"effects": ["IO"]
|
|
},
|
|
"params": [],
|
|
"body": {
|
|
"t": "do",
|
|
"op": "io/print_str",
|
|
"args": [{ "t": "app",
|
|
"fn": { "t": "var", "name": "show" },
|
|
"args": [{ "t": "lit", "k": "int", "v": 42 }] }]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Create fixture (b) entry module — explicit qualifier**
|
|
|
|
Create `examples/mq3_two_show_qualified.ail.json`:
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "mq3_two_show_qualified",
|
|
"imports": ["mq3_two_show_ambiguous_a", "mq3_two_show_ambiguous_b"],
|
|
"defs": [
|
|
{ "kind": "fn",
|
|
"name": "main",
|
|
"type": {
|
|
"k": "fn",
|
|
"params": [],
|
|
"param_modes": [],
|
|
"ret": { "k": "con", "name": "Unit" },
|
|
"effects": ["IO"]
|
|
},
|
|
"params": [],
|
|
"body": {
|
|
"t": "do",
|
|
"op": "io/print_str",
|
|
"args": [{ "t": "app",
|
|
"fn": { "t": "var", "name": "mq3_two_show_ambiguous_a.Show.show" },
|
|
"args": [{ "t": "lit", "k": "int", "v": 42 }] }]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Create fixture (c) — class Eq + fn eq + entry**
|
|
|
|
Create `examples/mq3_class_eq_vs_fn_eq_classmod.ail.json`:
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "mq3_class_eq_vs_fn_eq_classmod",
|
|
"imports": [],
|
|
"defs": [
|
|
{ "kind": "class",
|
|
"name": "MyEq",
|
|
"param": "a",
|
|
"methods": [
|
|
{ "name": "myeq",
|
|
"type": {
|
|
"k": "fn",
|
|
"params": [{ "k": "var", "name": "a" }, { "k": "var", "name": "a" }],
|
|
"param_modes": ["borrow", "borrow"],
|
|
"ret": { "k": "con", "name": "Bool" },
|
|
"effects": []
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{ "kind": "instance",
|
|
"class": "MyEq",
|
|
"type": { "k": "con", "name": "Int" },
|
|
"methods": [
|
|
{ "name": "myeq",
|
|
"body": { "t": "var", "name": "true" }
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Create `examples/mq3_class_eq_vs_fn_eq_fnmod.ail.json`:
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "mq3_class_eq_vs_fn_eq_fnmod",
|
|
"imports": [],
|
|
"defs": [
|
|
{ "kind": "fn",
|
|
"name": "myeq",
|
|
"type": {
|
|
"k": "fn",
|
|
"params": [{ "k": "con", "name": "Int" }, { "k": "con", "name": "Int" }],
|
|
"param_modes": ["borrow", "borrow"],
|
|
"ret": { "k": "con", "name": "Bool" },
|
|
"effects": []
|
|
},
|
|
"params": ["x", "y"],
|
|
"body": { "t": "var", "name": "false" }
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Create `examples/mq3_class_eq_vs_fn_eq.ail.json` (entry):
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "mq3_class_eq_vs_fn_eq",
|
|
"imports": ["mq3_class_eq_vs_fn_eq_classmod", "mq3_class_eq_vs_fn_eq_fnmod"],
|
|
"defs": [
|
|
{ "kind": "fn",
|
|
"name": "main",
|
|
"type": {
|
|
"k": "fn",
|
|
"params": [],
|
|
"param_modes": [],
|
|
"ret": { "k": "con", "name": "Unit" },
|
|
"effects": ["IO"]
|
|
},
|
|
"params": [],
|
|
"body": {
|
|
"t": "do",
|
|
"op": "io/print_bool",
|
|
"args": [{ "t": "app",
|
|
"fn": { "t": "var", "name": "myeq" },
|
|
"args": [{ "t": "lit", "k": "int", "v": 1 },
|
|
{ "t": "lit", "k": "int", "v": 2 }] }]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
(Naming choice: `MyEq`/`myeq` rather than reusing the prelude's
|
|
`Eq`/`eq` to avoid an additional prelude-vs-fn shadow that would
|
|
confuse the warning assertion. Implementer adjusts the
|
|
`io/print_bool` op call if the prelude builtin's exact name differs.)
|
|
|
|
- [ ] **Step 5: Create the integration-test file**
|
|
|
|
Create `crates/ail/tests/mq3_multi_class_e2e.rs`:
|
|
|
|
```rust
|
|
//! mq.3.6: end-to-end coverage of the post-mq.3 multi-candidate
|
|
//! dispatch path. Three positive fixtures exercise the three
|
|
//! trajectories from spec §"Data flow":
|
|
//! - ambiguous (Trajectory C): `AmbiguousMethodResolution`.
|
|
//! - explicit qualifier (Trajectory E): clean resolve.
|
|
//! - class-fn shadow: fn wins + warning.
|
|
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
fn run_ail_check(entry: &str, deps: &[&str]) -> std::process::Output {
|
|
let mut cmd = Command::new(env!("CARGO_BIN_EXE_ail"));
|
|
cmd.arg("check").arg("--json").arg(entry);
|
|
for dep in deps {
|
|
cmd.arg(dep);
|
|
}
|
|
cmd.output().expect("ail check must run")
|
|
}
|
|
|
|
/// mq.3.6: bare `show 42` in a workspace with two Show classes both
|
|
/// shipping Show Int fires `AmbiguousMethodResolution` with both
|
|
/// candidate classes named.
|
|
#[test]
|
|
fn mq3_two_show_ambiguous_fires_ambiguous_method_resolution() {
|
|
let out = run_ail_check(
|
|
"examples/mq3_two_show_ambiguous.ail.json",
|
|
&[
|
|
"examples/mq3_two_show_ambiguous_a.ail.json",
|
|
"examples/mq3_two_show_ambiguous_b.ail.json",
|
|
],
|
|
);
|
|
assert!(!out.status.success(),
|
|
"expected check to reject ambiguous dispatch");
|
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
assert!(stdout.contains("ambiguous-method-resolution"),
|
|
"expected ambiguous-method-resolution diagnostic, got: {stdout}");
|
|
assert!(stdout.contains("mq3_two_show_ambiguous_a.Show"),
|
|
"expected candidate A named, got: {stdout}");
|
|
assert!(stdout.contains("mq3_two_show_ambiguous_b.Show"),
|
|
"expected candidate B named, got: {stdout}");
|
|
}
|
|
|
|
/// mq.3.6: explicit qualifier `modA.Show.show 42` resolves to
|
|
/// modA's class cleanly.
|
|
#[test]
|
|
fn mq3_two_show_qualified_resolves_clean() {
|
|
let out = run_ail_check(
|
|
"examples/mq3_two_show_qualified.ail.json",
|
|
&[
|
|
"examples/mq3_two_show_ambiguous_a.ail.json",
|
|
"examples/mq3_two_show_ambiguous_b.ail.json",
|
|
],
|
|
);
|
|
assert!(out.status.success(),
|
|
"expected check to succeed with explicit qualifier, got: {}",
|
|
String::from_utf8_lossy(&out.stdout));
|
|
}
|
|
|
|
/// mq.3.6: `eq x y` in a workspace with `class MyEq` + `fn myeq`
|
|
/// resolves to the fn per lookup precedence; structured warning
|
|
/// `class-method-shadowed-by-fn` fires.
|
|
#[test]
|
|
fn mq3_class_eq_vs_fn_eq_fn_wins_with_warning() {
|
|
let out = run_ail_check(
|
|
"examples/mq3_class_eq_vs_fn_eq.ail.json",
|
|
&[
|
|
"examples/mq3_class_eq_vs_fn_eq_classmod.ail.json",
|
|
"examples/mq3_class_eq_vs_fn_eq_fnmod.ail.json",
|
|
],
|
|
);
|
|
assert!(out.status.success(),
|
|
"expected check to succeed (fn wins): {}",
|
|
String::from_utf8_lossy(&out.stdout));
|
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
assert!(stdout.contains("class-method-shadowed-by-fn"),
|
|
"expected shadow warning, got: {stdout}");
|
|
}
|
|
```
|
|
|
|
(The `run_ail_check` helper builds an `ail check` invocation. The exact
|
|
CLI flag for emitting structured-JSON diagnostics is `--json` per the
|
|
existing CLI convention; implementer verifies the flag name from the
|
|
existing CLI test files at `crates/ail/tests/ct1_check_cli.rs`.)
|
|
|
|
- [ ] **Step 6: Run the new E2E tests**
|
|
|
|
Run: `cargo test --workspace --test mq3_multi_class_e2e`
|
|
|
|
Expected: all three PASS.
|
|
|
|
- [ ] **Step 7: Run full cargo workspace test suite**
|
|
|
|
Run: `cargo test --workspace`
|
|
|
|
Expected: green.
|
|
|
|
---
|
|
|
|
## Task 7: DESIGN.md sync
|
|
|
|
**Files:**
|
|
- Modify: `docs/DESIGN.md:1156-1160` (rewrite class-names-bare paragraph)
|
|
- Modify: `docs/DESIGN.md:1717-1755` (Diagnostic categories — strike MethodNameCollision, add new diagnostics, reword AmbiguousInstance paragraph)
|
|
- Modify: `docs/DESIGN.md` (new subsection "Method dispatch")
|
|
|
|
- [ ] **Step 1: Rewrite the `class names stay bare` paragraph**
|
|
|
|
At `docs/DESIGN.md:1156-1160`, replace the paragraph that today
|
|
references `MethodNameCollision`:
|
|
|
|
```markdown
|
|
Class names follow the canonical-form rule (mq.1): bare for same-
|
|
module references, `<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:
|
|
|
|
```markdown
|
|
- `AmbiguousMethodResolution` — a monomorphic `Term::Var` call site
|
|
has multiple candidate classes after type-driven and constraint-
|
|
driven filters. LLM-author writes the explicit qualifier form
|
|
`<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:
|
|
|
|
```markdown
|
|
There is no `AmbiguousInstance` diagnostic at the registry level —
|
|
coherence (`DuplicateInstance` at registry build) makes per-`(class,
|
|
type)` ambiguity structurally impossible. Cross-class method
|
|
ambiguity is a separate concern resolved at the call site via
|
|
`AmbiguousMethodResolution` (see §"Method dispatch").
|
|
```
|
|
|
|
- [ ] **Step 5: Add the `## Method dispatch` subsection**
|
|
|
|
Append a new top-level subsection at the end of the diagnostic-
|
|
categories block (after `docs/DESIGN.md:1755`). Implementer adjusts
|
|
the heading depth to match surrounding structure (`##` if the
|
|
diagnostic block is `##`, `###` otherwise).
|
|
|
|
```markdown
|
|
## Method dispatch
|
|
|
|
Post-mq.3, dispatch is two-mode:
|
|
|
|
**Polymorphic call sites** (inside a fn body with `forall` +
|
|
constraint set): the constraint names the class via the qualified
|
|
`Constraint.class` field (canonical-form per mq.1). Synth's
|
|
residual carries the class name directly; constraint-discharge at
|
|
fn-body-end matches against the workspace registry by
|
|
`(class, type_hash)` key.
|
|
|
|
**Monomorphic call sites**: synth consults the workspace-flat
|
|
`Env.method_to_candidate_classes: BTreeMap<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]`:
|
|
|
|
```markdown
|
|
- [x] **[milestone]** Module-qualified class names + type-driven method dispatch — retire the `MethodNameCollision` workaround [...]
|
|
```
|
|
|
|
(Optionally trim the body once closed; convention is to leave the
|
|
description in place until the entry is purged on the next roadmap
|
|
sweep.)
|
|
|
|
- [ ] **Step 2: Clear milestone 24's `depends on:` line + annotate**
|
|
|
|
At `docs/roadmap.md:64-86`, the P1 entry "Post-22 Prelude — Show +
|
|
print rewire" carries (per recon) a `depends on:` line at 83-85:
|
|
|
|
```markdown
|
|
- depends on: P2 milestone "Module-qualified class names +
|
|
type-driven method dispatch" (retires the `MethodNameCollision`
|
|
workaround that drives the collision).
|
|
```
|
|
|
|
Strike that line. Then add an annotation under the entry:
|
|
|
|
```markdown
|
|
- ready for re-brainstorm — the `MethodNameCollision` workaround
|
|
that blocked the original spec retired in mq.3 (2026-05-13).
|
|
Fresh brainstorm re-derives the spec against the post-retirement
|
|
architecture (type-driven dispatch, class-ref canonical form).
|
|
```
|
|
|
|
- [ ] **Step 3: Run `cargo test --workspace`**
|
|
|
|
Run: `cargo test --workspace`
|
|
|
|
Expected: green. Roadmap edits are doc-only.
|
|
|
|
---
|
|
|
|
## Task 9: Integration verification — full cargo + bench + roundtrip
|
|
|
|
**Files:** none modified; verification only.
|
|
|
|
- [ ] **Step 1: Full `cargo test --workspace`**
|
|
|
|
Run: `cargo test --workspace`
|
|
|
|
Expected: green. The new and repurposed tests pass; existing tests
|
|
unchanged.
|
|
|
|
- [ ] **Step 2: Spot-check the three new E2E fixtures via `ail check`**
|
|
|
|
Run:
|
|
```
|
|
cargo run -p ail -- check examples/mq3_two_show_ambiguous.ail.json
|
|
```
|
|
Expected: exit non-zero with `ambiguous-method-resolution` diagnostic.
|
|
|
|
Run:
|
|
```
|
|
cargo run -p ail -- check examples/mq3_two_show_qualified.ail.json
|
|
```
|
|
Expected: exit 0.
|
|
|
|
Run:
|
|
```
|
|
cargo run -p ail -- check examples/mq3_class_eq_vs_fn_eq.ail.json
|
|
```
|
|
Expected: exit 0 with `class-method-shadowed-by-fn` warning surfaced.
|
|
|
|
- [ ] **Step 3: Verify zero diff on prelude**
|
|
|
|
Run: `git diff -- examples/prelude.ail.json`
|
|
|
|
Expected: no output (prelude unchanged).
|
|
|
|
- [ ] **Step 4: Bench scripts**
|
|
|
|
Run: `python3 bench/check.py`
|
|
Expected: exit 0 OR audit-ratified.
|
|
|
|
Run: `python3 bench/compile_check.py`
|
|
Expected: exit 0 OR audit-ratified.
|
|
|
|
Run: `python3 bench/cross_lang.py`
|
|
Expected: exit 0 OR audit-ratified.
|
|
|
|
- [ ] **Step 5: Confirm the per-iter journal placeholder is staged**
|
|
|
|
The implementer leaves `docs/journals/2026-05-13-iter-mq.3.md` (or the
|
|
date the iter actually finishes) in the working tree with a draft
|
|
entry covering: tasks ran, deviations from plan (if any), the
|
|
`class_methods` re-key consumer-rewrite scope, the synth warnings-
|
|
channel plumbing, the three E2E fixtures, DESIGN.md sync, roadmap
|
|
update, test counts, and bench outcomes. Append a pointer line to
|
|
`docs/journals/INDEX.md`. The Boss commits.
|
|
|
|
Note for milestone-close: WhatsNew.md is shipped at milestone-close-
|
|
audit (separate iter), NOT in mq.3 itself.
|