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:
@@ -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,
|
||||
|
||||
@@ -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}",
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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
@@ -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,
|
||||
®istry_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*/ ®istry_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(), <y, 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:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ¶ms_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 ¶ms_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");
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user