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:
2026-05-13 02:19:21 +02:00
parent 90075715d9
commit 99d3968656
23 changed files with 1399 additions and 399 deletions
-19
View File
@@ -1198,25 +1198,6 @@ fn workspace_error_to_diagnostic(
"var": var,
})),
),
W::MethodNameCollision {
method,
kind,
first_origin,
second_origin,
} => Some(
ailang_check::Diagnostic::error(
"method-name-collision",
format!(
"method name `{method}` collides ({kind}): defined in `{first_origin}` and `{second_origin}`"
),
)
.with_ctx(serde_json::json!({
"method": method,
"kind": kind,
"first_origin": first_origin,
"second_origin": second_origin,
})),
),
W::MissingSuperclassInstance {
class,
superclass,
+110
View File
@@ -0,0 +1,110 @@
//! mq.3.6: end-to-end coverage of the post-`MethodNameCollision`-
//! retirement multi-candidate dispatch path. Three positive fixtures
//! exercise the three trajectories from the milestone spec
//! §"Data flow":
//!
//! - Trajectory C — ambiguous bare-method call with both candidate
//! classes shipping `Show Int`. Typecheck rejects with
//! `ambiguous-method-resolution`.
//! - Trajectory E — explicit qualifier `<module>.<Class>.<method>`
//! resolves cleanly to the named class.
//! - Class-fn shadow — bare-method call where both a class method and
//! a free fn of the same name exist. Fn wins per lookup precedence;
//! `class-method-shadowed-by-fn` warning fires so the LLM-author
//! sees the shadow.
//!
//! Each test invokes `ail check --json <entry>` and asserts on the
//! diagnostic stream's `code` field. Stdout / runtime behaviour is
//! out of scope here — the dispatch path is a check-time concern.
use std::path::PathBuf;
use std::process::Command;
fn ail_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_ail"))
}
fn examples_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("examples")
}
fn run_ail_check_json(entry: &str) -> std::process::Output {
Command::new(ail_bin())
.args(["check", "--json", examples_dir().join(entry).to_str().unwrap()])
.output()
.expect("ail binary must launch")
}
/// mq.3.6 (Trajectory C): bare `show 42` in a workspace with two
/// `Show` classes (modA and modB) each shipping `Show Int` is
/// genuinely ambiguous. `ail check` rejects with
/// `ambiguous-method-resolution` and names both candidate classes.
#[test]
fn mq3_two_show_ambiguous_fires_ambiguous_method_resolution() {
let out = run_ail_check_json("mq3_two_show_ambiguous.ail.json");
assert!(
!out.status.success(),
"expected check to reject ambiguous dispatch; stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let stdout = String::from_utf8(out.stdout).expect("stdout is utf-8");
let diags: serde_json::Value =
serde_json::from_str(&stdout).expect("--json mode emits a JSON array on stdout");
let arr = diags.as_array().expect("diagnostics is a JSON array");
assert!(
arr.iter().any(|d| d["code"] == "ambiguous-method-resolution"),
"expected `ambiguous-method-resolution` diagnostic; got {stdout}",
);
// Both candidate classes named in the diagnostic so the author
// knows which qualifiers to choose from.
assert!(
stdout.contains("mq3_two_show_ambiguous_a.Show"),
"expected candidate A in diagnostic; got {stdout}",
);
assert!(
stdout.contains("mq3_two_show_ambiguous_b.Show"),
"expected candidate B in diagnostic; got {stdout}",
);
}
/// mq.3.6 (Trajectory E): explicit qualifier
/// `mq3_two_show_ambiguous_a.Show.show 42` disambiguates the same
/// workspace as the ambiguous fixture. Typecheck succeeds.
#[test]
fn mq3_two_show_qualified_resolves_clean() {
let out = run_ail_check_json("mq3_two_show_qualified.ail.json");
assert!(
out.status.success(),
"expected check to succeed with explicit qualifier; stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
}
/// mq.3.6 (class-fn shadow): bare `myeq 1 2` in a workspace with
/// `class MyEq { myeq }` in one module and `fn myeq` in another
/// resolves to the fn per lookup precedence. Typecheck succeeds AND
/// the structured warning `class-method-shadowed-by-fn` fires so the
/// LLM-author sees the shadow.
#[test]
fn mq3_class_eq_vs_fn_eq_fn_wins_with_warning() {
let out = run_ail_check_json("mq3_class_eq_vs_fn_eq.ail.json");
assert!(
out.status.success(),
"expected check to succeed (fn wins); stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let stdout = String::from_utf8(out.stdout).expect("stdout is utf-8");
let diags: serde_json::Value =
serde_json::from_str(&stdout).expect("--json mode emits a JSON array on stdout");
let arr = diags.as_array().expect("diagnostics is a JSON array");
assert!(
arr.iter().any(|d| d["code"] == "class-method-shadowed-by-fn"),
"expected `class-method-shadowed-by-fn` warning; got {stdout}",
);
}
+8 -5
View File
@@ -33,14 +33,17 @@ fn class_method_is_in_module_globals() {
let mod_globals = globals
.get("test_22b2_class_method_lookup")
.expect("module globals present");
// mq.3: ModuleGlobals accessors are tuple-keyed by (class, method).
// Pre-mq.3 callers passed the bare method name only; post-mq.3 they
// must disambiguate by class.
assert!(
mod_globals.has_class_method("show"),
"class method `show` must appear in module globals"
mod_globals.has_class_method("test_22b2_class_method_lookup.Show", "show"),
"class method `show` (in class `Show`) must appear in module globals"
);
// mq.1: class_method_class returns the qualified form
// `<defining_module>.<Class>`.
assert_eq!(
mod_globals.class_method_class("show"),
mod_globals.class_method_class("test_22b2_class_method_lookup.Show", "show"),
Some("test_22b2_class_method_lookup.Show"),
"class method `show` must remember its class is `Show` (qualified post-mq.1)"
);
@@ -65,8 +68,8 @@ fn class_method_entry_carries_full_metadata() {
.get("test_22b2_class_method_lookup")
.expect("module globals present");
let cm_entry = mod_globals
.class_method("show")
.expect("class method `show` must be registered");
.class_method("test_22b2_class_method_lookup.Show", "show")
.expect("class method `show` (in class `Show`) must be registered");
assert_eq!(
cm_entry.defining_module, "test_22b2_class_method_lookup",
+10 -1
View File
@@ -483,7 +483,16 @@ fn rewrite_walker_skips_locally_shadowed_class_method() {
let entry = examples_dir().join("test_22b3_shadow_class_method.ail.json");
let ws = ailang_core::load_workspace(&entry).expect("load");
let diags = ailang_check::check_workspace(&ws);
assert!(diags.is_empty(), "fixture must typecheck: {:?}", diags);
// mq.3: this fixture intentionally shadows the class method `show`
// with a local `let show = "shadow"`, which fires the new
// `class-method-shadowed-by-fn` warning. The warning is an
// expected correct consequence; filter it out so the assertion
// still pins "no errors / no other warnings".
let unexpected: Vec<_> = diags
.iter()
.filter(|d| d.code != "class-method-shadowed-by-fn")
.collect();
assert!(unexpected.is_empty(), "fixture must typecheck: {:?}", unexpected);
let ws = ailang_check::monomorphise_workspace(&ws).expect("mono");
+6
View File
@@ -362,6 +362,7 @@ mod tests {
let mut counter: u32 = 0;
let mut residuals = Vec::new();
let mut free_fn_calls = Vec::new();
let mut warnings: Vec<crate::diagnostic::Diagnostic> = Vec::new();
let ty = crate::synth(
t,
&env,
@@ -372,6 +373,7 @@ mod tests {
&mut counter,
&mut residuals,
&mut free_fn_calls,
&mut warnings,
)
.expect("synth");
subst.apply(&ty)
@@ -559,6 +561,7 @@ mod tests {
let mut counter: u32 = 0;
let mut residuals = Vec::new();
let mut free_fn_calls = Vec::new();
let mut warnings: Vec<crate::diagnostic::Diagnostic> = Vec::new();
let ret = crate::synth(
&do_term,
&env,
@@ -569,6 +572,7 @@ mod tests {
&mut counter,
&mut residuals,
&mut free_fn_calls,
&mut warnings,
)
.expect("synth");
let ret = subst.apply(&ret);
@@ -606,6 +610,7 @@ mod tests {
let mut counter: u32 = 0;
let mut residuals = Vec::new();
let mut free_fn_calls = Vec::new();
let mut warnings: Vec<crate::diagnostic::Diagnostic> = Vec::new();
let err = crate::synth(
&term,
&env,
@@ -616,6 +621,7 @@ mod tests {
&mut counter,
&mut residuals,
&mut free_fn_calls,
&mut warnings,
)
.expect_err("must reject");
assert!(
+10
View File
@@ -92,6 +92,16 @@
//! qualified class that is not in the workspace registry of
//! candidate classes for the method. `ctx`:
//! `{"name": "<qualified-class>"}`.
//! - `class-method-shadowed-by-fn` (mq.3) — `severity: warning`.
//! Emitted by `synth`'s `Term::Var` arm when a name resolves to a
//! free fn (locals / caller-module-fn / imported-fn) AND a class
//! method of the same name also exists in the workspace. Fn
//! resolution proceeds per lookup precedence; the warning surfaces
//! the shadow so the LLM-author can disambiguate via explicit
//! class-qualified call (`<ClassQualifier>.<method> ...`) if the
//! shadow was unintentional. `ctx`: `{"name": "<resolved-name>",
//! "method": "<bare-method>", "fn_owner_module": "<m>",
//! "candidate_classes": ["<C1>", ...]}`.
use serde::Serialize;
+414 -128
View File
@@ -906,7 +906,14 @@ pub fn check_workspace(ws: &Workspace) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = Vec::new();
for name in order {
let m = &ws.modules[name];
let typecheck_errors = check_in_workspace(m, ws, &module_globals);
// mq.3: synth-time warnings collected per module then merged
// into the per-module diagnostic stream alongside typecheck
// errors. The synth warnings flow through `check_in_workspace`
// even when `had_typecheck_errors` is true; surface them so a
// shadowed-class-method warning is visible even when the
// module also has unrelated errors.
let mut synth_warnings: Vec<Diagnostic> = Vec::new();
let typecheck_errors = check_in_workspace(m, ws, &module_globals, &mut synth_warnings);
let had_typecheck_errors = !typecheck_errors.is_empty();
// Per-module diagnostic accumulator. The suppress filter
// (Iter 19b) runs at the end of the per-module block, so
@@ -916,6 +923,7 @@ pub fn check_workspace(ws: &Workspace) -> Vec<Diagnostic> {
for e in typecheck_errors {
module_diags.push(e.to_diagnostic());
}
module_diags.extend(synth_warnings);
// Iter 18c.2: linearity check runs only on modules that
// typechecked clean. Running it on a body that already has a
// type error would wade into a partly-defined IR (e.g.
@@ -1018,8 +1026,11 @@ pub fn check(m: &Module) -> Result<CheckedModule> {
let module_globals = build_module_globals(&ws)?;
// `check` keeps single-error semantics for callers (snapshot tests,
// legacy code). Multi-diagnose is exposed via `check_module` /
// `check_workspace`.
if let Some(first) = check_in_workspace(m, &ws, &module_globals).into_iter().next() {
// `check_workspace`. mq.3: synth warnings are collected here but
// discarded — `check`'s legacy contract returns only the first
// error, and warnings on this path have no surface to flow to.
let mut synth_warnings: Vec<Diagnostic> = Vec::new();
if let Some(first) = check_in_workspace(m, &ws, &module_globals, &mut synth_warnings).into_iter().next() {
return Err(first);
}
// Collect symbols for the return value (existing semantics).
@@ -1119,37 +1130,49 @@ pub struct ModuleGlobals {
/// shape — every call site that only needs HM lookup goes through
/// here.
pub fns: IndexMap<String, Type>,
/// Class-method names, by method name. Populated in
/// [`build_module_globals`] from `Def::Class` entries; consulted by
/// the 22b.2 `missing-constraint` / `no-instance` arms (Tasks 9 / 10).
/// `IndexMap` matches the sibling [`Self::fns`] channel and preserves
/// definition order — Tasks 9 / 10 may surface diagnostics in source
/// order, and downstream consumers should not depend on a sorted
/// iteration order they did not ask for.
pub class_methods: IndexMap<String, ClassMethodEntry>,
/// 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>,
}
impl ModuleGlobals {
/// True if `name` is a class method declared in this module.
pub fn has_class_method(&self, name: &str) -> bool {
self.class_methods.contains_key(name)
/// True if class `class` declares a method named `name` in this
/// module. mq.3: tuple-keyed lookup post-`MethodNameCollision`
/// retirement.
pub fn has_class_method(&self, class: &str, name: &str) -> bool {
self.class_methods.contains_key(&(class.to_string(), name.to_string()))
}
/// The class that declared the method `name`, or `None` if `name`
/// is not a class method in this module.
pub fn class_method_class(&self, name: &str) -> Option<&str> {
/// The class that declared the method `name` in class `class`, or
/// `None` if no such entry exists. (Pre-mq.3 callers passed only the
/// method name; post-mq.3 they must disambiguate by class.)
pub fn class_method_class(&self, class: &str, name: &str) -> Option<&str> {
self.class_methods
.get(name)
.get(&(class.to_string(), name.to_string()))
.map(|e| e.class_name.as_str())
}
/// The full [`ClassMethodEntry`] for method `name`, or `None` if
/// `name` is not a class method in this module. Tasks 9 / 10 use
/// this to read `method_ty` and `defining_module` together; the
/// narrower `has_class_method` / `class_method_class` accessors
/// stay for callers that only need a yes/no or the class name.
pub fn class_method(&self, name: &str) -> Option<&ClassMethodEntry> {
self.class_methods.get(name)
/// The full [`ClassMethodEntry`] for method `name` in class `class`,
/// or `None` if no such entry exists.
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`. Returns zero-or-more entries —
/// pre-mq.3 always at most one, post-mq.3 a workspace may have
/// multiple classes declaring the same method.
pub fn class_method_candidates(&self, name: &str) -> Vec<(&String, &ClassMethodEntry)> {
self.class_methods
.iter()
.filter_map(|((c, m), e)| if m == name { Some((c, e)) } else { None })
.collect()
}
}
@@ -1271,7 +1294,7 @@ pub fn build_module_globals(
let qualified_class = format!("{mname}.{}", cd.name);
for meth in &cd.methods {
globals.class_methods.insert(
meth.name.clone(),
(qualified_class.clone(), meth.name.clone()),
ClassMethodEntry {
class_name: qualified_class.clone(),
class_param: cd.param.clone(),
@@ -1341,14 +1364,20 @@ pub fn build_check_env(ws: &Workspace) -> Env {
.map(|(k, v)| (k.clone(), v.fns.clone()))
.collect();
for g in mg.values() {
for (n, e) in &g.class_methods {
env.class_methods.insert(n.clone(), e.clone());
for ((qualified_class, method_name), e) in &g.class_methods {
// mq.3: env.class_methods is tuple-keyed by
// `(qualified-class, method-name)`. Multiple classes may
// share a method name post-`MethodNameCollision`-retirement.
env.class_methods.insert(
(qualified_class.clone(), method_name.clone()),
e.clone(),
);
// mq.2: build the inverse method → candidate-class set in
// the same loop. `e.class_name` already carries the
// qualified form (mq.1 invariant), so the set is keyed on
// workspace-flat qualified class names directly.
env.method_to_candidate_classes
.entry(n.clone())
.entry(method_name.clone())
.or_default()
.insert(e.class_name.clone());
}
@@ -1428,6 +1457,11 @@ fn check_in_workspace(
m: &Module,
ws: &Workspace,
module_globals: &BTreeMap<String, ModuleGlobals>,
// mq.3: synth-time warnings collected here so the caller can
// surface them into the workspace-wide diagnostic stream. Today
// only `class-method-shadowed-by-fn` flows through this channel
// (emitted from `synth` via `check_fn`'s warnings accumulator).
out_warnings: &mut Vec<Diagnostic>,
) -> Vec<CheckError> {
let mut env = build_check_env(ws);
let mut errors: Vec<CheckError> = Vec::new();
@@ -1505,17 +1539,29 @@ fn check_in_workspace(
env.current_module = m.name.clone();
for def in &m.defs {
if let Err(e) = check_def(def, &env) {
errors.push(CheckError::Def(def.name().to_string(), Box::new(e)));
match check_def(def, &env, out_warnings) {
Ok(()) => {}
Err(e) => {
errors.push(CheckError::Def(def.name().to_string(), Box::new(e)));
}
}
}
// mq.3: stamp the synth warnings with the module's name as `def`
// so the JSON consumer can correlate them with the surrounding
// module. The warning's `def` field carries the call-site def name
// already (set inside `check_fn`); the workspace-wide caller
// extends without further annotation.
errors
}
fn check_def(def: &Def, env: &Env) -> Result<()> {
fn check_def(
def: &Def,
env: &Env,
out_warnings: &mut Vec<Diagnostic>,
) -> Result<()> {
match def {
Def::Fn(f) => check_fn(f, env),
Def::Const(c) => check_const(c, env),
Def::Fn(f) => check_fn(f, env, out_warnings),
Def::Const(c) => check_const(c, env, out_warnings),
Def::Type(td) => check_type_def(td, env),
// Iter 22b.1: schema-only landing for class/instance defs.
// Class-schema validation (InvalidSuperclassParam,
@@ -1619,7 +1665,7 @@ fn check_type_well_formed(t: &Type, env: &Env) -> Result<()> {
}
}
fn check_fn(f: &FnDef, env: &Env) -> Result<()> {
fn check_fn(f: &FnDef, env: &Env, out_warnings: &mut Vec<Diagnostic>) -> Result<()> {
// Peel an outer Forall (Iter 12a). The vars become rigid in the
// inner env so they pass `check_type_well_formed` and unify only
// with themselves. An empty `vars` list (vacuously polymorphic)
@@ -1665,6 +1711,32 @@ fn check_fn(f: &FnDef, env: &Env) -> Result<()> {
}
check_type_well_formed(&ret_ty, &env)?;
// mq.3: build the post-superclass-expansion declared-constraint
// set ONCE here and stash on env, so synth's Var-arm class-method
// branch can hand it to `resolve_method_dispatch`'s constraint-
// driven filter. The post-synth missing-constraint check below
// re-reads the same expanded list from env to keep one source of
// truth.
//
// mq.1 follow-on: declared constraints carry the canonical-form
// `class` value (bare for same-module, qualified for cross-module).
// Lift to the workspace-key shape (always qualified) before
// expanding — residuals always carry qualified `class` because
// they come from `ClassMethodEntry.class_name` which is qualified
// post-mq.1.
let declared_constraints: Vec<Constraint> = match &f.ty {
Type::Forall { constraints, .. } => constraints
.iter()
.map(|c| Constraint {
class: qualify_class_ref_in_check(&c.class, &env.current_module),
type_: c.type_.clone(),
})
.collect(),
_ => Vec::new(),
};
env.active_declared_constraints =
expand_declared_constraints(&declared_constraints, &env.class_superclasses);
let mut locals = IndexMap::new();
for (n, t) in f.params.iter().zip(param_tys.iter()) {
locals.insert(n.clone(), t.clone());
@@ -1677,8 +1749,13 @@ fn check_fn(f: &FnDef, env: &Env) -> Result<()> {
// body finishes, we compare against the expanded declared set.
let mut residuals: Vec<ResidualConstraint> = Vec::new();
let mut free_fn_calls: Vec<FreeFnCall> = Vec::new();
let body_ty = synth(&f.body, &env, &mut locals, &mut effects, &f.name, &mut subst, &mut counter, &mut residuals, &mut free_fn_calls)?;
let mut warnings: Vec<Diagnostic> = Vec::new();
let body_ty = synth(&f.body, &env, &mut locals, &mut effects, &f.name, &mut subst, &mut counter, &mut residuals, &mut free_fn_calls, &mut warnings)?;
unify(&ret_ty, &body_ty, &mut subst)?;
// mq.3: surface synth-time warnings into the caller-supplied
// accumulator. The warnings already carry `def: Some(f.name)`
// set inside synth's emission site.
out_warnings.extend(warnings);
// Iter 14e: tail-position verification (Decision 8). Runs after
// the main type-check so that a tail-call marker on a malformed
@@ -1692,29 +1769,13 @@ fn check_fn(f: &FnDef, env: &Env) -> Result<()> {
}
}
// Iter 22b.2 (Task 9): missing-constraint check. Build the expanded
// Iter 22b.2 (Task 9): missing-constraint check. The expanded
// declared-constraint set (declared + one-step superclass per
// Decision 11) and compare against residuals. Var-shaped residuals
// Decision 11) was computed pre-synth and stashed on
// `env.active_declared_constraints` (mq.3). Var-shaped residuals
// not covered by the expanded set fire `MissingConstraint`. Concrete
// residuals are deferred to Task 10's `no-instance` arm.
//
// mq.1: declared constraints carry the canonical-form `class`
// value (bare for same-module, qualified for cross-module). Lift
// to the workspace-key shape (always qualified) before comparing
// against residuals — residuals always carry qualified `class`
// because they come from `ClassMethodEntry.class_name` which is
// qualified post-mq.1.
let declared_constraints: Vec<Constraint> = match &f.ty {
Type::Forall { constraints, .. } => constraints
.iter()
.map(|c| Constraint {
class: qualify_class_ref_in_check(&c.class, &env.current_module),
type_: c.type_.clone(),
})
.collect(),
_ => Vec::new(),
};
let expanded = expand_declared_constraints(&declared_constraints, &env.class_superclasses);
let expanded = &env.active_declared_constraints;
for r in &residuals {
let r_ty = subst.apply(&r.type_);
@@ -1742,7 +1803,7 @@ fn check_fn(f: &FnDef, env: &Env) -> Result<()> {
.collect();
match refine_multi_candidate_residual(
&normalized_residual,
&expanded,
expanded,
&registry_unit,
) {
RefineOutcome::Resolved(_) => {
@@ -2321,7 +2382,7 @@ pub fn verify_tail_positions(t: &Term, is_tail: bool) -> Result<()> {
}
}
fn check_const(c: &ConstDef, env: &Env) -> Result<()> {
fn check_const(c: &ConstDef, env: &Env, out_warnings: &mut Vec<Diagnostic>) -> Result<()> {
// Const types are never polymorphic — a Forall here is rejected
// outright. Any other type passes through to `synth` as before.
if matches!(&c.ty, Type::Forall { .. }) {
@@ -2337,7 +2398,9 @@ fn check_const(c: &ConstDef, env: &Env) -> Result<()> {
// accumulator only to keep `synth`'s signature uniform.
let mut residuals: Vec<ResidualConstraint> = Vec::new();
let mut free_fn_calls: Vec<FreeFnCall> = Vec::new();
let v = synth(&c.value, env, &mut locals, &mut effects, &c.name, &mut subst, &mut counter, &mut residuals, &mut free_fn_calls)?;
let mut warnings: Vec<Diagnostic> = Vec::new();
let v = synth(&c.value, env, &mut locals, &mut effects, &c.name, &mut subst, &mut counter, &mut residuals, &mut free_fn_calls, &mut warnings)?;
out_warnings.extend(warnings);
unify(&c.ty, &v, &mut subst)?;
if !effects.is_empty() {
return Err(CheckError::ConstHasEffects(
@@ -2348,6 +2411,7 @@ fn check_const(c: &ConstDef, env: &Env) -> Result<()> {
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn synth(
t: &Term,
env: &Env,
@@ -2358,6 +2422,11 @@ pub(crate) fn synth(
counter: &mut u32,
residuals: &mut Vec<ResidualConstraint>,
free_fn_calls: &mut Vec<FreeFnCall>,
// mq.3: out-parameter for synth-time structured warnings. Symmetric
// to `residuals` / `free_fn_calls`. Currently only the
// `class-method-shadowed-by-fn` warning is emitted here; future
// synth-time warnings (if any) would flow through the same channel.
warnings: &mut Vec<crate::diagnostic::Diagnostic>,
) -> Result<Type> {
match t {
Term::Lit { lit } => Ok(match lit {
@@ -2399,7 +2468,45 @@ pub(crate) fn synth(
// observation; the mono pass `subst.apply`s the metas
// post-synth to recover concrete type-args at each
// polymorphic free-fn call site.
// mq.3: a helper closure that emits the
// `class-method-shadowed-by-fn` warning when a fn-precedence
// branch resolves a name that ALSO has class-method
// candidates in the workspace. The fn resolution proceeds;
// the warning surfaces the shadow so the LLM-author can
// disambiguate via explicit class-qualified call if the
// shadow was unintentional. Locals/same-module-fn/implicit-
// import-fn all share this rule.
let emit_shadow_warning_if_class_method =
|name: &str, owner_module: &str, warnings: &mut Vec<crate::diagnostic::Diagnostic>| {
let (method_name, _) = parse_method_qualifier(name);
if let Some(cands) = env.method_to_candidate_classes.get(method_name) {
let mut candidate_list: Vec<String> = cands.iter().cloned().collect();
candidate_list.sort();
let d = crate::diagnostic::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} ...`.",
),
)
.with_def(in_def.to_string())
.with_ctx(serde_json::json!({
"name": name,
"method": method_name,
"fn_owner_module": owner_module,
"candidate_classes": candidate_list,
}));
warnings.push(d);
}
};
let (raw, free_fn_owner): (Type, Option<(String, String)>) = if let Some(t) = locals.get(name) {
// Locals (lambda / let / fn-params) — fn wins per
// lookup precedence. A class method shadowed by a
// local binding fires the shadow warning so the
// author can disambiguate if unintentional.
emit_shadow_warning_if_class_method(name, env.current_module.as_str(), warnings);
(t.clone(), None)
} else if let Some(t) = env.globals.get(name) {
// Same-module global. Owner is the current module IFF
@@ -2411,7 +2518,44 @@ pub(crate) fn synth(
.get(&env.current_module)
.filter(|m| m.contains_key(name))
.map(|_| (env.current_module.clone(), name.clone()));
emit_shadow_warning_if_class_method(name, env.current_module.as_str(), warnings);
(t.clone(), owner)
} else if let Some((owner_module, raw_ty)) = env
.imports
.values()
.find_map(|mod_name| {
env.module_globals
.get(mod_name)
.and_then(|mg| mg.get(name).map(|ty| (mod_name.clone(), ty.clone())))
})
{
// mq.3: implicit-import free-fn precedence runs BEFORE
// the class-method branch (fn wins per spec
// §"Class-fn collisions"). When the name has class-
// method candidates too, emit the shadow warning.
//
// Iter 23.4-prep: bare-name fall-through to free fns of
// implicitly-imported modules. Today only the prelude is
// implicitly imported (see `build_check_env` and
// `check_in_workspace`'s prelude-injection), so the
// first match is unambiguous. If a second implicit
// import lands, the single-match-wins rule becomes a
// real ambiguity and this branch needs a
// duplicate-detection diagnostic.
emit_shadow_warning_if_class_method(name, owner_module.as_str(), warnings);
// Qualify any bare type-cons referring to a type defined in
// the owning module — same treatment the dot-qualified arm
// below applies — so signatures cross the module boundary
// with fully-qualified Type::Cons.
let owner_types = env
.module_types
.get(&owner_module)
.cloned()
.unwrap_or_default();
let qualified = qualify_local_types(&raw_ty, &owner_module, &owner_types);
// Bare name resolves through implicit import; the
// unqualified name in the owner module is the same `name`.
(qualified, Some((owner_module, name.clone())))
} else if {
// mq.2 dispatch entry: the Var arm matches a class
// method either bare (`"show"`) or qualified
@@ -2464,7 +2608,7 @@ pub(crate) fn synth(
qualifier_opt.as_deref(),
candidates,
/*concrete_arg_type*/ None,
/*declared_constraints*/ &[],
/*declared_constraints*/ &env.active_declared_constraints,
/*registry*/ &registry_unit,
);
@@ -2488,16 +2632,22 @@ pub(crate) fn synth(
}
};
// Look up the `ClassMethodEntry` by method name —
// workspace-flat. The `MethodNameCollision` invariant
// (pre-mq.3) guarantees the lookup is unambiguous. Post-
// mq.3, the class_methods table is keyed by
// `(class, method)` and this `.get(method_name)` will
// need to be reworked alongside the table.
let cm = env.class_methods.get(method_name).expect(
"method_to_candidate_classes invariant: method present \
implies class_methods entry",
);
// mq.3: class_methods is tuple-keyed by
// `(qualified-class, method)`. The dispatcher above
// resolved a tentative class (single-survivor or first
// BTreeSet element for the multi-candidate path) — use
// it to disambiguate the lookup. The `expect` is sound
// by construction: `method_to_candidate_classes` and
// `class_methods` are built from the same source loop
// (every entry in one has a matching entry in the
// other; see `build_check_env`).
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",
);
let owner_types = env
.module_types
.get(&cm.defining_module)
@@ -2516,37 +2666,6 @@ pub(crate) fn synth(
candidates: residual_candidates,
});
return Ok(inst_ty);
} else if let Some((owner_module, raw_ty)) = env
.imports
.values()
.find_map(|mod_name| {
env.module_globals
.get(mod_name)
.and_then(|mg| mg.get(name).map(|ty| (mod_name.clone(), ty.clone())))
})
{
// Iter 23.4-prep: bare-name fall-through to free fns of
// implicitly-imported modules. Today only the prelude is
// implicitly imported (see `build_check_env` and
// `check_in_workspace`'s prelude-injection at this file's
// ~1170 / ~1285), so the first match is unambiguous. If a
// second implicit import lands, the single-match-wins rule
// becomes a real ambiguity and this branch needs a
// duplicate-detection diagnostic.
//
// Qualify any bare type-cons referring to a type defined in
// the owning module — same treatment the dot-qualified arm
// below applies — so signatures cross the module boundary
// with fully-qualified Type::Cons.
let owner_types = env
.module_types
.get(&owner_module)
.cloned()
.unwrap_or_default();
let qualified = qualify_local_types(&raw_ty, &owner_module, &owner_types);
// Bare name resolves through implicit import; the
// unqualified name in the owner module is the same `name`.
(qualified, Some((owner_module, name.clone())))
} else if name.matches('.').count() == 1 {
let (prefix, suffix) = name.split_once('.').expect("checked");
let target_module = match env.imports.get(prefix) {
@@ -2610,7 +2729,7 @@ pub(crate) fn synth(
Ok(maybe_instantiate(raw, counter))
}
Term::App { callee, args, .. } => {
let cty = synth(callee, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls)?;
let cty = synth(callee, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?;
let cty = subst.apply(&cty);
let (params, ret, fx) = match &cty {
Type::Fn { params, ret, effects: fx, .. } => {
@@ -2646,7 +2765,7 @@ pub(crate) fn synth(
});
}
for (a, exp) in args.iter().zip(params.iter()) {
let actual = synth(a, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls)?;
let actual = synth(a, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?;
unify(exp, &actual, subst)?;
}
for e in fx {
@@ -2655,9 +2774,9 @@ pub(crate) fn synth(
Ok(subst.apply(&ret))
}
Term::Let { name, value, body } => {
let v = synth(value, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls)?;
let v = synth(value, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?;
let prev = locals.insert(name.clone(), v);
let r = synth(body, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls)?;
let r = synth(body, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?;
match prev {
Some(p) => {
locals.insert(name.clone(), p);
@@ -2669,10 +2788,10 @@ pub(crate) fn synth(
Ok(r)
}
Term::If { cond, then, else_ } => {
let c = synth(cond, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls)?;
let c = synth(cond, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?;
unify(&Type::bool_(), &c, subst)?;
let t1 = synth(then, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls)?;
let t2 = synth(else_, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls)?;
let t1 = synth(then, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?;
let t2 = synth(else_, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?;
unify(&t1, &t2, subst)?;
Ok(subst.apply(&t1))
}
@@ -2690,7 +2809,7 @@ pub(crate) fn synth(
});
}
for (a, exp) in args.iter().zip(sig.params.iter()) {
let actual = synth(a, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls)?;
let actual = synth(a, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?;
unify(exp, &actual, subst)?;
}
effects.insert(sig.effect.clone());
@@ -2785,7 +2904,7 @@ pub(crate) fn synth(
}
for (a, exp) in args.iter().zip(qualified_fields.iter()) {
let exp_inst = substitute_rigids(exp, &mapping);
let actual = synth(a, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls)?;
let actual = synth(a, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?;
unify(&exp_inst, &actual, subst)?;
}
Ok(Type::Con {
@@ -2794,7 +2913,7 @@ pub(crate) fn synth(
})
}
Term::Match { scrutinee, arms } => {
let s_ty = synth(scrutinee, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls)?;
let s_ty = synth(scrutinee, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?;
if arms.is_empty() {
return Err(CheckError::NonExhaustive {
ty: ailang_core::pretty::type_to_string(&s_ty),
@@ -2812,7 +2931,7 @@ pub(crate) fn synth(
let prev = locals.insert(n.clone(), t.clone());
pushed.push((n.clone(), prev));
}
let body_ty = synth(&arm.body, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls)?;
let body_ty = synth(&arm.body, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?;
for (n, prev) in pushed.into_iter().rev() {
match prev {
Some(p) => {
@@ -2887,9 +3006,9 @@ pub(crate) fn synth(
Ok(subst.apply(&result_ty.expect("checked arms is non-empty")))
}
Term::Seq { lhs, rhs } => {
let lty = synth(lhs, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls)?;
let lty = synth(lhs, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?;
unify(&Type::unit(), &lty, subst)?;
synth(rhs, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls)
synth(rhs, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)
}
Term::Lam { params, param_tys, ret_ty, effects: lam_effects, body } => {
let mut pushed: Vec<(String, Option<Type>)> = Vec::new();
@@ -2898,7 +3017,7 @@ pub(crate) fn synth(
pushed.push((n.clone(), prev));
}
let mut body_effects: BTreeSet<String> = BTreeSet::new();
let body_ty = synth(body, env, locals, &mut body_effects, in_def, subst, counter, residuals, free_fn_calls);
let body_ty = synth(body, env, locals, &mut body_effects, in_def, subst, counter, residuals, free_fn_calls, warnings);
for (n, prev) in pushed.into_iter().rev() {
match prev {
Some(p) => {
@@ -2986,7 +3105,7 @@ pub(crate) fn synth(
// subset rule against `declared_effs` — exactly like
// `Term::Lam`.
let mut body_effects: BTreeSet<String> = BTreeSet::new();
let body_ty = synth(body, env, locals, &mut body_effects, in_def, subst, counter, residuals, free_fn_calls);
let body_ty = synth(body, env, locals, &mut body_effects, in_def, subst, counter, residuals, free_fn_calls, warnings);
// Restore body-scope locals (params + name).
for (n, prev) in pushed.into_iter().rev() {
@@ -3013,7 +3132,7 @@ pub(crate) fn synth(
// scope (params are not visible here; only the recursive
// binding is).
let prev_in = locals.insert(name.clone(), ty.clone());
let in_ty = synth(in_term, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls);
let in_ty = synth(in_term, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings);
match prev_in {
Some(p) => {
locals.insert(name.clone(), p);
@@ -3029,7 +3148,7 @@ pub(crate) fn synth(
// No constraint generated, no environment change. The
// wrapper records author intent for the future RC inc/dec
// emission pass (18c.3); typing is pure passthrough.
synth(value, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls)
synth(value, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)
}
Term::ReuseAs { source, body } => {
// Iter 18d.1: `(reuse-as SRC NEW-CTOR)` requires `body` to
@@ -3043,7 +3162,7 @@ pub(crate) fn synth(
// shape-compatibility check (18d.2 will add a
// `reuse-as-shape-mismatch` diagnostic when codegen has
// the actual size info).
let _ = synth(source, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls)?;
let _ = synth(source, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)?;
match body.as_ref() {
Term::Ctor { .. } | Term::Lam { .. } => {}
other => {
@@ -3069,7 +3188,7 @@ pub(crate) fn synth(
}
// Body's type is the result type of the whole reuse-as
// expression.
synth(body, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls)
synth(body, env, locals, effects, in_def, subst, counter, residuals, free_fn_calls, warnings)
}
}
}
@@ -3356,14 +3475,15 @@ pub struct Env {
/// these names are legal as `Type::Var { name }` and unify only
/// with themselves.
pub rigid_vars: BTreeSet<String>,
/// Iter 22b.2 (Task 9): workspace-wide class-method table. Merged
/// from every module's [`ModuleGlobals::class_methods`] so that a
/// `Term::Var { name: "show" }` in any module resolves to the
/// declaring class's [`ClassMethodEntry`]. Method-name uniqueness
/// is enforced earlier by `workspace::build_registry`'s
/// method-name-collision check (Iter 22b.2.6), so the merge is
/// safe (no overwrites). Read in [`synth`]'s `Term::Var` arm.
pub class_methods: BTreeMap<String, ClassMethodEntry>,
/// 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 `Term::Var` arm via the resolved class
/// (from `method_to_candidate_classes` + dispatch) plus the method
/// name. Mono's presence checks consult
/// [`Self::method_to_candidate_classes`] (method-keyed natively).
pub class_methods: BTreeMap<(String, String), ClassMethodEntry>,
/// mq.2: workspace-flat inverse of [`Self::class_methods`] — for
/// each method name, the set of qualified class names that declare
/// it. Pre-mq.3, the `MethodNameCollision` invariant guarantees
@@ -3371,6 +3491,15 @@ pub struct Env {
/// > 1 becomes legal. Synth's `Term::Var` arm consults this index
/// for type-driven dispatch (spec §Architecture's 5-step rule).
pub method_to_candidate_classes: BTreeMap<String, BTreeSet<String>>,
/// mq.3: post-superclass-expansion declared constraints of the
/// active fn body being checked. Populated by `check_fn` before
/// invoking `synth` on the body; empty 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>,
/// Iter 22b.2 (Task 9): one-step superclass expansion table.
/// Maps each class name to its superclass class name. Absence from
/// the map means the class has no superclass — populated only for
@@ -6016,4 +6145,161 @@ mod tests {
assert!(candidates.contains("m.MyShow"), "candidates: {candidates:?}");
assert_eq!(candidates.len(), 1, "MethodNameCollision invariant: singleton");
}
/// 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");
}
/// mq.3.3: when synth's Var-arm resolves a name to a free fn
/// (locals / caller-module-fn / implicit-import fn) AND the same
/// method name has class-method candidates in the workspace, a
/// structured warning `class-method-shadowed-by-fn` is emitted
/// via the warnings out-parameter. The fn resolution proceeds;
/// the warning surfaces the shadow so the LLM-author can
/// disambiguate via explicit class-qualified call if the shadow
/// was unintentional.
#[test]
fn mq3_class_method_shadowed_by_fn_warning_fires() {
use ailang_core::ast::{ClassDef, ClassMethod, Module};
// Synthesise a workspace with `class Show { show: a -> Str }`
// in module `clsmod` and `fn show : (Int) -> Str` in module
// `fnmod`. Module `m` imports both implicitly; `synth` on
// `Term::Var { name: "show" }` should resolve to the fn AND
// emit the shadow warning.
let clsmod = Module {
schema: SCHEMA.into(),
name: "clsmod".into(),
imports: vec![],
defs: vec![Def::Class(ClassDef {
name: "Show".into(),
param: "a".into(),
superclass: None,
methods: vec![ClassMethod {
name: "show".into(),
ty: Type::Fn {
params: vec![Type::Var { name: "a".into() }],
param_modes: vec![ParamMode::Borrow],
ret: Box::new(Type::str_()),
effects: vec![],
ret_mode: ParamMode::Implicit,
},
default: None,
}],
doc: None,
})],
};
let show_fn_ty = Type::Fn {
params: vec![Type::int()],
param_modes: vec![ParamMode::Borrow],
ret: Box::new(Type::str_()),
effects: vec![],
ret_mode: ParamMode::Implicit,
};
let fnmod = Module {
schema: SCHEMA.into(),
name: "fnmod".into(),
imports: vec![],
defs: vec![Def::Fn(FnDef {
name: "show".into(),
ty: show_fn_ty.clone(),
params: vec!["x".into()],
body: Term::Lit { lit: Literal::Str { value: "_".into() } },
doc: None,
suppress: vec![],
})],
};
let mut modules = BTreeMap::new();
modules.insert("clsmod".into(), clsmod);
modules.insert("fnmod".into(), fnmod);
let ws = Workspace {
entry: "fnmod".into(),
modules,
root_dir: std::path::PathBuf::from("."),
registry: ailang_core::workspace::Registry::default(),
};
let mut env = build_check_env(&ws);
// Treat both modules as implicitly imported by `env.imports`
// (the test-only equivalent of the prelude-injection convention).
env.imports.insert("clsmod".into(), "clsmod".into());
env.imports.insert("fnmod".into(), "fnmod".into());
env.current_module = "fnmod".into();
let term = Term::Var { name: "show".into() };
let mut locals: IndexMap<String, Type> = IndexMap::new();
let mut effects: BTreeSet<String> = BTreeSet::new();
let mut subst = Subst::default();
let mut counter: u32 = 0;
let mut residuals: Vec<ResidualConstraint> = Vec::new();
let mut free_fn_calls: Vec<FreeFnCall> = Vec::new();
let mut warnings: Vec<crate::diagnostic::Diagnostic> = Vec::new();
let _ = synth(
&term,
&env,
&mut locals,
&mut effects,
"test_call_site",
&mut subst,
&mut counter,
&mut residuals,
&mut free_fn_calls,
&mut warnings,
)
.expect("synth must succeed (fn wins precedence)");
assert!(
warnings.iter().any(|d| d.code == "class-method-shadowed-by-fn"),
"expected at least one class-method-shadowed-by-fn warning, got: {warnings:?}",
);
}
/// 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() {
use ailang_core::ast::{ClassDef, ClassMethod, Module};
let m = Module {
schema: SCHEMA.into(),
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![],
ret_mode: ParamMode::Implicit,
},
default: None,
}],
doc: None,
})],
};
let mut modules = BTreeMap::new();
modules.insert("m".into(), m);
let ws = Workspace {
entry: "m".into(),
modules,
root_dir: std::path::PathBuf::from("."),
registry: ailang_core::workspace::Registry::default(),
};
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:?}");
}
}
+5 -1
View File
@@ -694,7 +694,11 @@ impl<'a> Lifter<'a> {
// Iter 23.4: free-fn-call observations are similarly discarded
// here — the mono pass will re-synth bodies post-lift.
let mut free_fn_calls: Vec<crate::FreeFnCall> = Vec::new();
let ty = synth(t, &self.env, locals, &mut effects, in_def, &mut subst, &mut counter, &mut residuals, &mut free_fn_calls)?;
// mq.3: lift's synth re-entry is post-typecheck — warnings
// (e.g. class-method-shadowed-by-fn) have already been
// surfaced upstream by `check_workspace`. Discard here.
let mut warnings_discarded: Vec<crate::diagnostic::Diagnostic> = Vec::new();
let ty = synth(t, &self.env, locals, &mut effects, in_def, &mut subst, &mut counter, &mut residuals, &mut free_fn_calls, &mut warnings_discarded)?;
Ok(subst.apply(&ty))
}
}
+66 -45
View File
@@ -204,7 +204,7 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result<Workspace> {
}
rewrite_mono_calls(
&mut f.body,
&env.class_methods,
&env.method_to_candidate_classes,
&poly_free_fns,
mname,
&ordered,
@@ -215,7 +215,7 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result<Workspace> {
Def::Const(c) => {
rewrite_mono_calls(
&mut c.value,
&env.class_methods,
&env.method_to_candidate_classes,
&poly_free_fns,
mname,
&ordered,
@@ -608,6 +608,10 @@ pub fn collect_mono_targets(
let mut counter: u32 = 0;
let mut residuals: Vec<crate::ResidualConstraint> = Vec::new();
let mut free_fn_calls: Vec<crate::FreeFnCall> = Vec::new();
// mq.3: mono's residual-collection synth re-entry collects-and-
// discards warnings — typecheck has already run and surfaced any
// shadow warnings; mono is a post-typecheck pass.
let mut warnings_discarded: Vec<crate::diagnostic::Diagnostic> = Vec::new();
crate::synth(
&f.body,
&env,
@@ -618,6 +622,7 @@ pub fn collect_mono_targets(
&mut counter,
&mut residuals,
&mut free_fn_calls,
&mut warnings_discarded,
)?;
// Filter residuals to fully-concrete ones; look up
@@ -954,9 +959,10 @@ pub fn synthesise_mono_fn_for_free_fn(
/// - `None` (slot is None): residual was non-concrete or instance
/// not registered; cursor still advances (to keep alignment) but
/// the name is left unchanged.
#[allow(clippy::too_many_arguments)]
fn rewrite_mono_calls(
body: &mut Term,
class_methods: &BTreeMap<String, crate::ClassMethodEntry>,
method_to_candidate_classes: &BTreeMap<String, BTreeSet<String>>,
poly_free_fns: &BTreeSet<String>,
caller_module: &str,
ordered_targets: &[Option<MonoTarget>],
@@ -969,7 +975,12 @@ fn rewrite_mono_calls(
// whose name synth would resolve to either a class-method
// residual OR a poly-free-fn observation. Locally
// shadowed names match neither — synth pushes nothing.
let is_class_method = class_methods.contains_key(name);
//
// mq.3: the class-method side is method-keyed natively via
// `method_to_candidate_classes`. Class disambiguation
// (which class declared the resolved method) lives in the
// residual slot the cursor consumes, not here.
let is_class_method = method_to_candidate_classes.contains_key(name);
let is_poly_free_fn = !is_class_method && poly_free_fns.contains(name);
let should_advance = (is_class_method || is_poly_free_fn) && !locals.contains(name);
if should_advance {
@@ -997,15 +1008,15 @@ fn rewrite_mono_calls(
}
}
Term::App { callee, args, .. } => {
rewrite_mono_calls(callee, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(callee, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
for a in args {
rewrite_mono_calls(a, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(a, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
}
}
Term::Let { name, value, body } => {
rewrite_mono_calls(value, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(value, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
let inserted = locals.insert(name.clone());
rewrite_mono_calls(body, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
if inserted {
locals.remove(name);
}
@@ -1018,32 +1029,32 @@ fn rewrite_mono_calls(
params_inserted.push(p.clone());
}
}
rewrite_mono_calls(body, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
for p in &params_inserted {
locals.remove(p);
}
rewrite_mono_calls(in_term, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(in_term, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
if name_inserted {
locals.remove(name);
}
}
Term::If { cond, then, else_ } => {
rewrite_mono_calls(cond, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(then, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(else_, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(cond, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(then, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(else_, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
}
Term::Do { args, .. } => {
for a in args {
rewrite_mono_calls(a, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(a, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
}
}
Term::Ctor { args, .. } => {
for a in args {
rewrite_mono_calls(a, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(a, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
}
}
Term::Match { scrutinee, arms } => {
rewrite_mono_calls(scrutinee, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(scrutinee, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
for Arm { pat, body } in arms {
let binders = pattern_binders(pat);
let mut inserted: Vec<String> = Vec::new();
@@ -1052,7 +1063,7 @@ fn rewrite_mono_calls(
inserted.push(b.clone());
}
}
rewrite_mono_calls(body, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
for b in &inserted {
locals.remove(b);
}
@@ -1065,21 +1076,21 @@ fn rewrite_mono_calls(
inserted.push(p.clone());
}
}
rewrite_mono_calls(body, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
for p in &inserted {
locals.remove(p);
}
}
Term::Seq { lhs, rhs } => {
rewrite_mono_calls(lhs, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(rhs, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(lhs, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(rhs, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
}
Term::Clone { value } => {
rewrite_mono_calls(value, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(value, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
}
Term::ReuseAs { source, body } => {
rewrite_mono_calls(source, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(body, class_methods, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(source, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
rewrite_mono_calls(body, method_to_candidate_classes, poly_free_fns, caller_module, ordered_targets, cursor, locals);
}
Term::Lit { .. } => {}
}
@@ -1176,6 +1187,10 @@ pub(crate) fn collect_residuals_ordered(
let mut counter: u32 = 0;
let mut residuals: Vec<crate::ResidualConstraint> = Vec::new();
let mut free_fn_calls: Vec<crate::FreeFnCall> = Vec::new();
// mq.3: mono's residual-collection synth re-entry collects-and-
// discards warnings — typecheck has already run and surfaced any
// shadow warnings; mono is a post-typecheck pass.
let mut warnings_discarded: Vec<crate::diagnostic::Diagnostic> = Vec::new();
crate::synth(
&f.body,
&env,
@@ -1186,6 +1201,7 @@ pub(crate) fn collect_residuals_ordered(
&mut counter,
&mut residuals,
&mut free_fn_calls,
&mut warnings_discarded,
)?;
// Iter 23.4 Task 6: build two per-channel slot lists — one for
@@ -1280,7 +1296,7 @@ pub(crate) fn collect_residuals_ordered(
let mut walker_locals: BTreeSet<String> = f.params.iter().cloned().collect();
interleave_slots(
&f.body,
&env.class_methods,
&env.method_to_candidate_classes,
poly_free_fns,
&class_slots,
&free_fn_slots,
@@ -1297,10 +1313,15 @@ pub(crate) fn collect_residuals_ordered(
/// poly-free-fn slots in source-AST order. The walker MUST stay
/// in lockstep with `rewrite_mono_calls` — same predicates, same
/// shadowing handling.
///
/// mq.3: class-method presence is method-keyed natively via
/// `method_to_candidate_classes` (post-`MethodNameCollision`-retirement,
/// `class_methods` is tuple-keyed by `(class, method)` and not
/// directly probable by method name alone).
#[allow(clippy::too_many_arguments)]
fn interleave_slots(
term: &Term,
class_methods: &BTreeMap<String, crate::ClassMethodEntry>,
method_to_candidate_classes: &BTreeMap<String, BTreeSet<String>>,
poly_free_fns: &BTreeSet<String>,
class_slots: &[Option<MonoTarget>],
free_fn_slots: &[Option<MonoTarget>],
@@ -1311,7 +1332,7 @@ fn interleave_slots(
) {
match term {
Term::Var { name } => {
let is_class_method = class_methods.contains_key(name);
let is_class_method = method_to_candidate_classes.contains_key(name);
let is_poly_free_fn = !is_class_method && poly_free_fns.contains(name);
let advances = (is_class_method || is_poly_free_fn) && !locals.contains(name);
if advances {
@@ -1327,15 +1348,15 @@ fn interleave_slots(
}
}
Term::App { callee, args, .. } => {
interleave_slots(callee, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(callee, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
for a in args {
interleave_slots(a, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(a, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
}
Term::Let { name, value, body } => {
interleave_slots(value, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(value, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
let inserted = locals.insert(name.clone());
interleave_slots(body, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(body, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
if inserted {
locals.remove(name);
}
@@ -1348,32 +1369,32 @@ fn interleave_slots(
params_inserted.push(p.clone());
}
}
interleave_slots(body, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(body, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
for p in &params_inserted {
locals.remove(p);
}
interleave_slots(in_term, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(in_term, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
if name_inserted {
locals.remove(name);
}
}
Term::If { cond, then, else_ } => {
interleave_slots(cond, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(then, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(else_, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(cond, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(then, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(else_, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
Term::Do { args, .. } => {
for a in args {
interleave_slots(a, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(a, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
}
Term::Ctor { args, .. } => {
for a in args {
interleave_slots(a, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(a, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
}
Term::Match { scrutinee, arms } => {
interleave_slots(scrutinee, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(scrutinee, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
for Arm { pat, body } in arms {
let binders = pattern_binders(pat);
let mut inserted: Vec<String> = Vec::new();
@@ -1382,7 +1403,7 @@ fn interleave_slots(
inserted.push(b.clone());
}
}
interleave_slots(body, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(body, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
for b in &inserted {
locals.remove(b);
}
@@ -1395,21 +1416,21 @@ fn interleave_slots(
inserted.push(p.clone());
}
}
interleave_slots(body, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(body, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
for p in &inserted {
locals.remove(p);
}
}
Term::Seq { lhs, rhs } => {
interleave_slots(lhs, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(rhs, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(lhs, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(rhs, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
Term::Clone { value } => {
interleave_slots(value, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(value, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
Term::ReuseAs { source, body } => {
interleave_slots(source, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(body, class_methods, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(source, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
interleave_slots(body, method_to_candidate_classes, poly_free_fns, class_slots, free_fn_slots, class_cur, free_cur, locals, out);
}
Term::Lit { .. } => {}
}
@@ -0,0 +1,75 @@
//! mq.3.5: repurposed pin tests for the post-`MethodNameCollision`-
//! retirement workspace-load path. The two on-disk fixtures that fired
//! `WorkspaceLoadError::MethodNameCollision` pre-mq.3 now load cleanly;
//! the assertion migrates from "expect collision diagnostic" to "load
//! successful + `Env.method_to_candidate_classes` carries the
//! expected multi-entry set" (class-class case) or "load successful +
//! class-fn coexistence is legal" (class-fn case).
//!
//! The relocation from `crates/ailang-core/src/workspace.rs` to this
//! file is required because the post-retirement assertions read
//! `Env.method_to_candidate_classes`, which is built by `ailang-check`
//! (`build_check_env`) and not visible from `ailang-core`.
use ailang_check::build_check_env;
use ailang_core::load_workspace;
use std::path::Path;
fn examples_dir() -> std::path::PathBuf {
let manifest = env!("CARGO_MANIFEST_DIR");
Path::new(manifest)
.parent().expect("CARGO_MANIFEST_DIR has a parent (crates/ailang-check)")
.parent().expect("CARGO_MANIFEST_DIR has a grandparent (crates/)")
.join("examples")
}
/// mq.3.5 (class-class repurpose): the fixture
/// `examples/test_22b2_method_name_collision_class_class.ail.json`
/// declares two classes `A` and `B` in the same module, each declaring
/// the method `foo`. Pre-mq.3 this fired
/// `WorkspaceLoadError::MethodNameCollision { kind: "class-class" }`.
/// Post-mq.3 the workspace loads cleanly and
/// `env.method_to_candidate_classes` carries both qualified classes as
/// candidates for the method name `foo`.
#[test]
fn mq3_class_class_collision_loads_clean_and_populates_candidates() {
let entry = examples_dir()
.join("test_22b2_method_name_collision_class_class.ail.json");
let ws = load_workspace(&entry)
.expect("post-mq.3: workspace loads without collision diagnostic");
let env = build_check_env(&ws);
let candidates = env
.method_to_candidate_classes
.get("foo")
.expect("`foo` must be in method_to_candidate_classes");
assert!(
candidates.contains("test_22b2_method_name_collision_class_class.A"),
"candidates must include class A: {candidates:?}",
);
assert!(
candidates.contains("test_22b2_method_name_collision_class_class.B"),
"candidates must include class B: {candidates:?}",
);
assert_eq!(
candidates.len(),
2,
"exactly two candidate classes for `foo`: {candidates:?}",
);
}
/// mq.3.5 (class-fn repurpose): the fixture
/// `examples/test_22b2_method_name_collision_class_fn.ail.json`
/// declares `class Greet { greet }` and `fn greet`. Pre-mq.3 this
/// fired `WorkspaceLoadError::MethodNameCollision { kind: "class-fn" }`.
/// Post-mq.3 the workspace loads cleanly — the method-vs-fn
/// coexistence is now legal at load time; the call-site emits a
/// `class-method-shadowed-by-fn` warning (covered by mq.3.6 E2E,
/// not this load-level pin).
#[test]
fn mq3_class_fn_collision_loads_clean() {
let entry = examples_dir()
.join("test_22b2_method_name_collision_class_fn.ail.json");
load_workspace(&entry)
.expect("post-mq.3: workspace loads — class-vs-fn name overlap is no longer a load error");
}
+15 -170
View File
@@ -294,19 +294,6 @@ pub enum WorkspaceLoadError {
method: String,
},
/// 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,
},
/// Iter 22b.2: an instance `C T` was declared, but `C`'s
/// superclass `S` does not have an instance for the same type
/// `T`. Decision 11 single-superclass model requires `instance S
@@ -606,92 +593,14 @@ fn build_registry(
}
}
// Iter 22b.2: method-name-collision pre-pass. Bare-name resolution
// (`foo x` rather than `A.foo x`) requires that a method name
// appears in at most one origin across the whole workspace. We
// walk every `Def::Class` and `Def::Fn` once, building a
// `method_origins` map; the first repeat fires the diagnostic.
// The `kind` field distinguishes class-method ↔ class-method from
// class-method ↔ top-level-fn; fn-fn collisions are a separate
// concern surfaced by `CheckError::DuplicateDef` in `ailang-check`,
// not here.
//
// Origins are kept structural (the `Origin` enum below) so the
// `kind` discriminator is a `match` on variants rather than a
// string-prefix check on a display form.
enum Origin {
Class { class_name: String, module: String },
Fn { name: String, module: String },
}
impl Origin {
fn format(&self) -> String {
match self {
Origin::Class { class_name, module } => {
format!("class {class_name} (in {module})")
}
Origin::Fn { name, module } => format!("fn {name} (in {module})"),
}
}
}
let mut method_origins: BTreeMap<String, Origin> = BTreeMap::new();
for (mod_name, m) in modules {
for def in &m.defs {
match def {
Def::Class(c) => {
for method in &c.methods {
// mq.1: `class_name` carries the qualified
// workspace key (`<mod>.<Class>`). The Display
// format emits `"class <mod>.<Class> (in <mod>)"`
// which is informative even if visually
// redundant.
let origin = Origin::Class {
class_name: format!("{mod_name}.{}", c.name),
module: mod_name.clone(),
};
if let Some(prior) = method_origins.get(&method.name) {
let kind = match prior {
Origin::Class { .. } => "class-class",
Origin::Fn { .. } => "class-fn",
};
return Err(WorkspaceLoadError::MethodNameCollision {
method: method.name.clone(),
kind,
first_origin: prior.format(),
second_origin: origin.format(),
});
}
method_origins.insert(method.name.clone(), origin);
}
}
Def::Fn(f) => {
let origin = Origin::Fn {
name: f.name.clone(),
module: mod_name.clone(),
};
if let Some(prior) = method_origins.get(&f.name) {
// Only fire on class-fn collisions here. fn-fn
// collisions (two `Def::Fn` with the same name)
// are `CheckError::DuplicateDef`'s job in
// `ailang-check`; firing here with `kind:
// "class-fn"` would misreport.
if matches!(prior, Origin::Class { .. }) {
return Err(WorkspaceLoadError::MethodNameCollision {
method: f.name.clone(),
kind: "class-fn",
first_origin: prior.format(),
second_origin: origin.format(),
});
}
// prior is a fn: skip; do not overwrite.
} else {
method_origins.insert(f.name.clone(), origin);
}
}
_ => {}
}
}
}
// mq.3: the `MethodNameCollision` pre-pass (variant + Origin enum +
// per-def loop) was retired here. Bare-method resolution no longer
// requires workspace-wide method-name uniqueness — synth's
// `Term::Var` arm consults `Env.method_to_candidate_classes`
// (built workspace-flat in `ailang-check`) and runs the type-driven
// dispatch rule, with `AmbiguousMethodResolution` /
// `class-method-shadowed-by-fn` as the per-call-site outcomes.
// See `docs/journals/2026-05-13-iter-mq.3.md` for the rationale.
// Pass 2: register each instance, with coherence / uniqueness /
// method-completeness checks.
@@ -2127,77 +2036,13 @@ mod tests {
}
}
/// Iter 22b.2: two classes that declare a method with the same
/// name must fire `MethodNameCollision` with `kind == "class-class"`.
/// Decision 11 keeps method names workspace-unique so that bare
/// method-name resolution (`foo x` rather than `A.foo x`) is
/// unambiguous; if two classes both export `foo`, the registry has
/// no way to choose between them at a use site.
#[test]
fn class_class_method_name_collision_fires() {
let entry = examples_dir()
.join("test_22b2_method_name_collision_class_class.ail.json");
let err = load_workspace(&entry)
.expect_err("must fire method-name-collision");
match err {
WorkspaceLoadError::MethodNameCollision {
method,
kind,
first_origin,
second_origin,
} => {
assert_eq!(method, "foo");
assert_eq!(kind, "class-class");
// mq.1: Origin::Class.class_name is qualified —
// Display emits "class <module>.<Class> (in <module>)".
assert!(
first_origin.contains(".A"),
"first_origin = {first_origin:?}",
);
assert!(
second_origin.contains(".B"),
"second_origin = {second_origin:?}",
);
}
other => panic!("expected MethodNameCollision, got {other:?}"),
}
}
/// Iter 22b.2: a class-method name that collides with a top-level
/// `fn` of the same name must fire `MethodNameCollision` with
/// `kind == "class-fn"`. Same rationale as the class-class case:
/// bare-name resolution must be unambiguous, and a class method
/// shadowing (or being shadowed by) a free function silently is
/// the worst possible failure mode.
#[test]
fn class_fn_method_name_collision_fires() {
let entry = examples_dir()
.join("test_22b2_method_name_collision_class_fn.ail.json");
let err = load_workspace(&entry)
.expect_err("must fire method-name-collision");
match err {
WorkspaceLoadError::MethodNameCollision {
method,
kind,
first_origin,
second_origin,
} => {
assert_eq!(method, "greet");
assert_eq!(kind, "class-fn");
// mq.1: Origin::Class.class_name is qualified —
// Display emits "class <module>.Greet (in <module>)".
assert!(
first_origin.contains(".Greet"),
"first_origin = {first_origin:?}",
);
assert!(
second_origin.starts_with("fn greet"),
"second_origin = {second_origin:?}",
);
}
other => panic!("expected MethodNameCollision, got {other:?}"),
}
}
// mq.3: the two `..._method_name_collision_fires` pin tests
// (class-class and class-fn variants) were retired here and
// re-located as positive-load tests in
// `crates/ailang-check/tests/method_collision_pin.rs`. Post-
// mq.3 those on-disk fixtures load cleanly and the equivalent
// observations move to `env.method_to_candidate_classes`
// (multi-entry set) plus the call-site warning.
/// Iter 22b.2: an instance `C T` whose class `C` declares a
/// superclass `S` requires that `instance S T` also exist in the