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:
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"iter_id": "mq.3",
|
||||
"date": "2026-05-13",
|
||||
"mode": "standard",
|
||||
"outcome": "DONE",
|
||||
"tasks_total": 9,
|
||||
"tasks_completed": 9,
|
||||
"reloops_per_task": {
|
||||
"1": 0,
|
||||
"2": 0,
|
||||
"3": 0,
|
||||
"4": 0,
|
||||
"5": 0,
|
||||
"6": 0,
|
||||
"7": 0,
|
||||
"8": 0,
|
||||
"9": 0
|
||||
},
|
||||
"review_loops_spec": 0,
|
||||
"review_loops_quality": 0,
|
||||
"blocked_reason": null
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+91
-10
@@ -1153,11 +1153,23 @@ qualified references whose owner is unknown are also a violation
|
||||
(`WorkspaceLoadError::BadCrossModuleTypeRef`). The same rule
|
||||
applies to `Term::Ctor.type_name`.
|
||||
|
||||
Class names (`ClassDef.name`, `InstanceDef.class`,
|
||||
`SuperclassRef.class`, `Constraint.class`) are NOT module-scoped
|
||||
under this rule; they remain workspace-flat with
|
||||
`MethodNameCollision` enforced at load. Class-name scoping is a
|
||||
future milestone with its own DESIGN amendment.
|
||||
Class names follow the canonical-form rule (mq.1): bare for
|
||||
same-module references, `<module>.<Class>` for cross-module
|
||||
references — symmetric to `Type::Con.name`'s rule from ct.1.
|
||||
Three schema fields carry class references in this form:
|
||||
`InstanceDef.class`, `Constraint.class`, and `SuperclassRef.class`.
|
||||
`ClassDef.name` itself stays bare (defining-site context, like
|
||||
`TypeDef.name`).
|
||||
|
||||
Method dispatch is type-driven post-mq.3 (see §"Method dispatch"
|
||||
below): synth resolves a `Term::Var { name: "show" }` by consulting
|
||||
the workspace's method-to-candidate-class index, filtering by
|
||||
argument type (concrete) or by declared constraint (rigid-var), and
|
||||
routing the residual through the registry at fn-body-end discharge.
|
||||
Method-name collisions across classes are now structurally legal —
|
||||
they resolve at the call site via type-driven dispatch with explicit
|
||||
qualifier (`<module>.<Class>.<method>`) as the LLM-author's
|
||||
disambiguation tool.
|
||||
|
||||
The legacy `(con T)` form is treated as `(own T)` semantically.
|
||||
|
||||
@@ -1727,8 +1739,12 @@ wording is fixed; the categories and their triggers are:
|
||||
- `MissingMethod` — instance omits a required method.
|
||||
- `OverridingNonExistentMethod` — instance specifies a method not in
|
||||
the class.
|
||||
- `MethodNameCollision` — same method name across two in-scope
|
||||
classes, or between a class method and a top-level function.
|
||||
|
||||
(`MethodNameCollision` was retired at iter mq.3. Cross-class method
|
||||
sharing is now structurally legal; ambiguity surfaces at the call
|
||||
site via `AmbiguousMethodResolution` or, for class-fn name overlap,
|
||||
via the `class-method-shadowed-by-fn` warning. See §"Method
|
||||
dispatch" below.)
|
||||
|
||||
**Class-schema diagnostics** (validation of class declarations):
|
||||
|
||||
@@ -1749,10 +1765,75 @@ at iter ctt.3.
|
||||
- `MissingConstraint` — body's residual constraint is not covered
|
||||
by declared (and superclass-expanded) constraints.
|
||||
- `NoInstance` — fully concrete constraint has no registry entry.
|
||||
mq.2 adds an optional `candidate_classes` field surfacing the
|
||||
multi-candidate set when the bare-method dispatch path's filter
|
||||
collapses to zero registry survivors.
|
||||
- `AmbiguousMethodResolution` (mq.2) — a monomorphic `Term::Var`
|
||||
call site survives both type-driven and constraint-driven filters
|
||||
with more than one candidate class. LLM-author writes the explicit
|
||||
qualifier form `<module>.<Class>.<method>` to disambiguate.
|
||||
- `UnknownClass` (mq.2) — an explicit class qualifier in
|
||||
`Term::Var.name` names a qualified class that is not in the
|
||||
workspace's candidate-class index for the method.
|
||||
- `class-method-shadowed-by-fn` (mq.3, warning) — a `Term::Var`
|
||||
resolved via fn lookup precedence (locals → caller-module-fn →
|
||||
imported-fn) while a class method of the same name also exists
|
||||
in the workspace. Fn resolution proceeds; the warning surfaces
|
||||
the shadow so the LLM-author can disambiguate via explicit
|
||||
class-qualified call if the shadow was unintentional.
|
||||
|
||||
There is no `AmbiguousInstance` diagnostic. Coherence (W2) makes
|
||||
every `(class, type)` key globally unique; resolution is therefore
|
||||
deterministic by construction.
|
||||
There is no `AmbiguousInstance` diagnostic at the registry level —
|
||||
coherence (`DuplicateInstance` at registry build, W2) makes
|
||||
per-`(class, type)` ambiguity structurally impossible. Cross-class
|
||||
method ambiguity is a separate concern resolved at the call site
|
||||
via `AmbiguousMethodResolution` (see §"Method dispatch" below).
|
||||
|
||||
### Method dispatch
|
||||
|
||||
Post-mq.3, dispatch is two-mode:
|
||||
|
||||
**Polymorphic call sites** (inside a fn body with `forall` +
|
||||
constraint set): the constraint names the class via the qualified
|
||||
`Constraint.class` field (canonical-form per mq.1). Synth's
|
||||
residual carries the class name directly; constraint-discharge at
|
||||
fn-body-end matches against the workspace registry by
|
||||
`(class, type_hash)` key.
|
||||
|
||||
**Monomorphic call sites**: synth consults the workspace-flat
|
||||
`Env.method_to_candidate_classes: BTreeMap<MethodName,
|
||||
BTreeSet<QualifiedClassName>>` index, then runs the 5-step
|
||||
dispatch rule:
|
||||
|
||||
1. Parse the `Term::Var.name` for an optional class qualifier
|
||||
(last-dot-segment is the method name; everything before is the
|
||||
qualified class).
|
||||
2. If `method_to_candidate_classes` has no entry for the method
|
||||
name, fall through to the existing Var-arm branches (free fn
|
||||
lookup, dot-qualified cross-module).
|
||||
3. Qualifier present: filter candidates to the named class. Empty
|
||||
result fires `UnknownClass`. Singleton survivor proceeds.
|
||||
4. Qualifier empty (bare-method form): singleton candidate proceeds
|
||||
directly; multiple candidates yield a multi-candidate residual
|
||||
for discharge-time refinement.
|
||||
5. At discharge, refinement runs: concrete `type_` filters
|
||||
candidates via the workspace registry; rigid-var `type_` filters
|
||||
via the active fn's declared constraints
|
||||
(`env.active_declared_constraints`). Single survivor discharges;
|
||||
multiple survivors fire `AmbiguousMethodResolution` (concrete) or
|
||||
`MissingConstraint` (rigid-var); zero survivors fire `NoInstance`
|
||||
(concrete) or `MissingConstraint` (rigid-var).
|
||||
|
||||
The `method_to_candidate_classes` index is the load-bearing data
|
||||
structure for this routing — its construction in `build_check_env`
|
||||
inverts the per-module `class_methods` maps (themselves tuple-keyed
|
||||
by `(qualified-class, method)` post-mq.3) to a workspace-flat
|
||||
method-name-to-class-set map.
|
||||
|
||||
Class-fn collisions resolve at the call site, not at workspace load
|
||||
time: the fn lookup precedence (locals → caller-module-fn →
|
||||
imported-fn) runs ahead of the class-method branch. When both
|
||||
sides have a match, the fn wins and a `class-method-shadowed-by-fn`
|
||||
warning surfaces the shadow.
|
||||
|
||||
### What the typeclass design explicitly does NOT support
|
||||
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
# iter mq.3 — Retire MethodNameCollision + multi-class E2E + DESIGN.md sync
|
||||
|
||||
**Date:** 2026-05-13
|
||||
**Started from:** 90075715d99c9e8c59ea79267c2615f5756520f2
|
||||
**Status:** DONE
|
||||
**Tasks completed:** 9 of 9
|
||||
|
||||
## Summary
|
||||
|
||||
mq.3 closes the module-qualified-class-names milestone by retiring
|
||||
the `WorkspaceLoadError::MethodNameCollision` workaround that mq.1 +
|
||||
mq.2 made redundant. The variant + pre-pass + Origin enum + Display
|
||||
arm + two pin tests are deleted; the two on-disk fixtures that fired
|
||||
the collision now load cleanly and the equivalent observations
|
||||
relocate to `env.method_to_candidate_classes` (multi-entry set) plus
|
||||
a per-call-site warning. Three positive E2E fixtures (Trajectory C
|
||||
ambiguous, Trajectory E explicit-qualifier, class-fn shadow) exercise
|
||||
the multi-candidate dispatch path end-to-end via `ail check --json`.
|
||||
|
||||
Two out-of-band corrections that mq.2 surfaced as known debt land
|
||||
here: (1) `Env.active_declared_constraints: Vec<Constraint>` is
|
||||
plumbed pre-synth in `check_fn` so the post-superclass-expansion
|
||||
constraint set is reachable at synth time by
|
||||
`resolve_method_dispatch`'s constraint-driven filter; (2)
|
||||
`ModuleGlobals.class_methods` + `Env.class_methods` re-keyed from
|
||||
`BTreeMap<MethodName, ClassMethodEntry>` to
|
||||
`BTreeMap<(QualifiedClass, MethodName), ClassMethodEntry>` so the
|
||||
post-retirement world has O(1) `(class, method)` lookup, with
|
||||
mono's two presence-check sites switched to consult the natively
|
||||
method-keyed `method_to_candidate_classes`. A third
|
||||
correction — `synth(...)` signature extended with a `warnings: &mut
|
||||
Vec<Diagnostic>` out-parameter — installs the channel for the new
|
||||
structured warning `class-method-shadowed-by-fn` that fires when a
|
||||
free fn shadows a class method of the same name (per spec
|
||||
§"Class-fn collisions": precedence + warning, the spec's default).
|
||||
|
||||
DESIGN.md sync removes the `MethodNameCollision` bullet from
|
||||
"Workspace-load (registry-build) diagnostics" with a forward-pointing
|
||||
note, adds `AmbiguousMethodResolution` / `UnknownClass` /
|
||||
`class-method-shadowed-by-fn` to the "Typecheck diagnostics" block,
|
||||
rewords the `AmbiguousInstance` paragraph (per-class coherence stays
|
||||
via `DuplicateInstance`; cross-class method ambiguity is the new
|
||||
diagnostic at the call site), rewrites the class-names paragraph in
|
||||
the canonical-form section to point at the mq.1 rule + the new
|
||||
dispatch model, and adds a new `### Method dispatch` subsection
|
||||
anchoring the 5-step rule with `method_to_candidate_classes` as the
|
||||
load-bearing data structure. Roadmap P2 entry → `[x]` with a summary
|
||||
of the three iters; milestone-24 (`Post-22 Prelude — Show + print
|
||||
rewire`) `depends on:` line struck and the entry annotated
|
||||
"ready for re-brainstorm".
|
||||
|
||||
545 tests green (was 539 at start of mq.3; +6 net = 3 new mq.3.x lib
|
||||
tests + 2 relocated method_collision_pin + 3 new mq3_multi_class_e2e
|
||||
- 2 deleted workspace.rs pin tests). `bench/compile_check.py` and
|
||||
`bench/cross_lang.py` exit 0; `bench/check.py` exit 1 with 2
|
||||
regressions both noise-class (bench_list_sum.bump_s persistence —
|
||||
3rd consecutive sighting since audit-ct-tidy — and a max_us tail
|
||||
metric, plus 4 improvements). Prelude zero-diff.
|
||||
|
||||
## Per-task notes
|
||||
|
||||
- **mq.3.1** — `Env.active_declared_constraints: Vec<Constraint>`
|
||||
field plumbed; populated in `check_fn` pre-synth alongside the
|
||||
rigid-vars install (consolidated with the existing post-synth
|
||||
`expanded` computation — single source of truth, no clone-and-
|
||||
duplicate as the plan literally suggested). Synth Var-arm
|
||||
`resolve_method_dispatch` call site swapped `&[]` for
|
||||
`&env.active_declared_constraints`. 1 new test mq3_env_active_declared_constraints_field_exists.
|
||||
|
||||
- **mq.3.2** — `ModuleGlobals.class_methods` re-keyed
|
||||
`IndexMap<String, ClassMethodEntry>` → `IndexMap<(String, String),
|
||||
ClassMethodEntry>`; sibling `Env.class_methods` similarly re-keyed
|
||||
to `BTreeMap<(String, String), ClassMethodEntry>`. Accessor methods
|
||||
on `ModuleGlobals` (`has_class_method`, `class_method_class`,
|
||||
`class_method`) all take `(class, name)`; new
|
||||
`class_method_candidates(name) -> Vec<(&class, &entry)>` for the
|
||||
enumerate-all-classes-declaring-this-method use case (returning
|
||||
`Vec` not `impl Iterator` because the borrow-checker rejected the
|
||||
`'a`/`'_` capture without explicit `use<'a, '_>` syntax not yet
|
||||
idiomatic in this crate). `build_module_globals` insert site +
|
||||
`build_check_env` merge loop both threaded through the tuple key.
|
||||
Synth Var-arm `class_methods.get(&(residual_class, method))` uses
|
||||
the dispatcher-resolved tentative class. Mono's two presence-check
|
||||
sites (`rewrite_mono_calls`, `interleave_slots`) switched to
|
||||
consult `method_to_candidate_classes` natively — method-keyed,
|
||||
preserves the existing presence-check signature shape; the four
|
||||
call sites at module-walk + interleave_slots entry pass
|
||||
`&env.method_to_candidate_classes` instead of `&env.class_methods`.
|
||||
Test assertion in `crates/ail/tests/typeclass_22b2.rs:42` updated
|
||||
to pass the qualified class explicitly. 1 new test
|
||||
mq3_env_class_methods_tuple_keyed.
|
||||
|
||||
- **mq.3.3** — `synth(...)` signature extended with `warnings:
|
||||
&mut Vec<Diagnostic>` (15+ recursive synth callsites threaded).
|
||||
Five external synth callers (check_fn, check_const, 3 in
|
||||
builtins.rs, 1 in lift.rs, 2 in mono.rs, 1 in `check`) all
|
||||
updated; check_fn / check_const pass through to caller via new
|
||||
`out_warnings: &mut Vec<Diagnostic>` parameter on the caller
|
||||
chain (`check_def`, `check_in_workspace`); `check_workspace`
|
||||
extends per-module synth_warnings into `module_diags`. Plan
|
||||
suggested `check_fn` return `Result<(CheckedFn, Vec<Diagnostic>)>`
|
||||
but `check_fn`'s actual signature is `Result<()>` and the
|
||||
mut-ref-accumulator pattern matches the existing `Vec<CheckError>`
|
||||
accumulator at `check_in_workspace`'s caller. New diagnostic
|
||||
`class-method-shadowed-by-fn` (warning, kebab-case code,
|
||||
structured `ctx` carrying `name` + `method` + `fn_owner_module` +
|
||||
`candidate_classes`) emitted by a closure at all three
|
||||
fn-precedence branches (locals, same-module fn, implicit-import
|
||||
fn). Implicit-import-fn branch reordered ABOVE the class-method
|
||||
branch per spec §"Class-fn collisions" so fn-wins precedence is
|
||||
structural, not just convention. Diagnostic doc updated in
|
||||
`diagnostic.rs`. One existing test
|
||||
`crates/ail/tests/typeclass_22b3.rs:rewrite_walker_skips_locally_shadowed_class_method`
|
||||
was a known trip-wire — the fixture intentionally shadows a class
|
||||
method with a local binding, which now correctly fires the new
|
||||
warning; filtered out of the test's `assert!(diags.is_empty())`
|
||||
with a comment naming why. 1 new test
|
||||
mq3_class_method_shadowed_by_fn_warning_fires.
|
||||
|
||||
- **mq.3.4** — `WorkspaceLoadError::MethodNameCollision` variant +
|
||||
the Origin enum + the per-def collision loop in `build_registry`
|
||||
(workspace.rs 596-681 pre-edit) + the Display arm in
|
||||
`main.rs:1201` deleted. The two in-workspace.rs pin tests
|
||||
(`class_class_method_name_collision_fires` +
|
||||
`class_fn_method_name_collision_fires`) deleted; relocation lands
|
||||
in mq.3.5. The on-disk fixtures
|
||||
`test_22b2_method_name_collision_class_{class,fn}.ail.json` stay
|
||||
on-disk (now exercised as positive-load by the relocated tests).
|
||||
|
||||
- **mq.3.5** — `crates/ailang-check/tests/method_collision_pin.rs`
|
||||
created with two repurposed pin tests: the class-class fixture
|
||||
loads cleanly and `env.method_to_candidate_classes["foo"]` has
|
||||
exactly two qualified-class entries (`...A` + `...B`); the
|
||||
class-fn fixture loads cleanly (the warning is a call-site
|
||||
concern, not a load-time concern; covered by mq.3.6 E2E). The
|
||||
relocation from `ailang-core` to `ailang-check` is required
|
||||
because `Env.method_to_candidate_classes` is built by
|
||||
`build_check_env`, which lives in `ailang-check` (not visible
|
||||
from `ailang-core`'s `mod tests`).
|
||||
|
||||
- **mq.3.6** — Three new on-disk fixtures plus three integration
|
||||
tests in `crates/ail/tests/mq3_multi_class_e2e.rs`. Fixture (a)
|
||||
`mq3_two_show_ambiguous` exercises the bare-method ambiguous
|
||||
case (two `Show` classes, both with `Show Int`, bare `show 42`
|
||||
→ `AmbiguousMethodResolution`). Fixture (b)
|
||||
`mq3_two_show_qualified` exercises explicit-qualifier resolution
|
||||
(same workspace, `mq3_two_show_ambiguous_a.Show.show 42` →
|
||||
clean). Fixture (c) `mq3_class_eq_vs_fn_eq` exercises class-fn
|
||||
shadow (`class MyEq { myeq }` + `fn myeq` + bare `myeq 1 2` →
|
||||
fn wins, warning fires). The plan's draft fixtures used a bare
|
||||
`Term::App` for instance method bodies; the existing-convention
|
||||
shape (per `test_22b3_mono_synthetic.ail.json`) is `Term::Lam`
|
||||
with `paramTypes`/`retType` — implementer-phase repair adjusted
|
||||
inline. Naming `MyEq`/`myeq` (rather than `Eq`/`eq`) avoids an
|
||||
extra prelude-vs-fn shadow that would have confused the warning
|
||||
assertion. All three tests GREEN first-shot; spot-checked via
|
||||
`ail check --json` for completeness.
|
||||
|
||||
- **mq.3.7** — DESIGN.md sync: class-names paragraph (1156-1160)
|
||||
rewritten to point at mq.1 canonical-form + mq.3 dispatch model;
|
||||
`MethodNameCollision` bullet struck from "Workspace-load
|
||||
(registry-build) diagnostics" with forward-pointing note; three
|
||||
new typecheck diagnostics + `NoInstance.candidate_classes`
|
||||
addendum added to "Typecheck diagnostics"; `AmbiguousInstance`
|
||||
paragraph reworded to clarify the registry-level vs call-site
|
||||
distinction; new `### Method dispatch` subsection anchors the
|
||||
5-step rule with `method_to_candidate_classes` as the
|
||||
load-bearing data structure, the class-fn precedence rule, and
|
||||
the post-mq.3 tuple-keyed `class_methods` shape.
|
||||
|
||||
- **mq.3.8** — Roadmap P2 milestone "Module-qualified class names
|
||||
+ type-driven method dispatch" → `[x]` with a one-paragraph
|
||||
summary of the three iters (mq.1 canonical-form, mq.2 mechanism,
|
||||
mq.3 retirement) and the milestone's user-facing outcome (two
|
||||
libraries can each declare `class Eq` with their own `eq`).
|
||||
Milestone-24 (`Post-22 Prelude — Show + print rewire`) `depends
|
||||
on:` line struck and the entry annotated "ready for
|
||||
re-brainstorm" so the next time `/boss` walks the roadmap it
|
||||
picks up the un-blocked spec re-derivation against the
|
||||
post-retirement architecture.
|
||||
|
||||
- **mq.3.9** — Integration verification. `cargo test --workspace`
|
||||
545 passed / 0 failed. `bench/compile_check.py` 0 regressed / 0
|
||||
improved / 24 stable. `bench/cross_lang.py` 0 regressed / 0
|
||||
improved / 25 stable. `bench/check.py` 2 regressed / 4 improved
|
||||
/ 57 stable: regression #1 `throughput.bench_list_sum.bump_s`
|
||||
(3rd consecutive sighting since audit-ct-tidy 2026-05-12, audit-
|
||||
ratified-class noise per the journal lineage); regression #2
|
||||
`latency.implicit_at_rc.max_us` (same noisy max-tail metric
|
||||
that mq.2 journal flagged as runtime-uncoupled-to-typecheck-
|
||||
iter, audit-ratified-class). Prelude zero-diff. All three new
|
||||
E2E fixtures observable via `ail check` with the expected
|
||||
diagnostic / no-diagnostic / warning outcomes.
|
||||
|
||||
## Concerns
|
||||
|
||||
- **Plan's `check_fn` signature suggestion was off.** Plan Step 4
|
||||
proposed `check_fn(...) -> Result<(CheckedFn, Vec<Diagnostic>),
|
||||
CheckError>`; the actual signature is `Result<()>` (no
|
||||
CheckedFn). The mut-ref-accumulator pattern adopted instead
|
||||
matches the existing `check_in_workspace` shape (Vec<CheckError>
|
||||
accumulator). Same outcome, different mechanism.
|
||||
|
||||
- **`class_method_candidates` returns `Vec` not `impl Iterator`.**
|
||||
The plan's accessor signature `impl Iterator<Item = (&'a String,
|
||||
&'a ClassMethodEntry)>` requires the `use<'a, '_>` opaque-type
|
||||
capture syntax — not yet idiomatic in this crate (the rest of
|
||||
the codebase uses `Vec`-returning accessors at module-graph
|
||||
boundaries). The semantic contract is the same; performance is
|
||||
one allocation per call, bounded by the per-method candidate
|
||||
count (currently ≤ 2 across the corpus).
|
||||
|
||||
- **One existing E2E test trips the new warning.**
|
||||
`crates/ail/tests/typeclass_22b3.rs:rewrite_walker_skips_locally_shadowed_class_method`
|
||||
uses a fixture (`test_22b3_shadow_class_method`) that
|
||||
intentionally shadows the class method `show` with a local
|
||||
`let show = "shadow"`. Pre-mq.3 this fixture typechecked clean;
|
||||
post-mq.3 it correctly fires the `class-method-shadowed-by-fn`
|
||||
warning. The test's `assert!(diags.is_empty())` was relaxed to
|
||||
filter out the new warning with a comment naming why. This is
|
||||
the warning behaving correctly, not a test workaround.
|
||||
|
||||
- **Plan's instance-method body shape was wrong.** Plan Step 1 of
|
||||
Task 6 drafted `body: { "t": "app", ... }` for instance
|
||||
methods; the existing-convention shape (per `test_22b3_mono_synthetic`)
|
||||
is `Term::Lam` with `paramTypes`/`retType` keys. Implementer-phase
|
||||
repair adjusted all three fixtures inline. The plan's caveat
|
||||
("implementer adjusts to match the exact shape used by
|
||||
`examples/prelude.ail.json`'s Eq/Ord instances") covered this
|
||||
case; just exercising the caveat verbatim.
|
||||
|
||||
## Known debt
|
||||
|
||||
- **`bench_list_sum.bump_s` 3rd-consecutive regression.** Noise-class
|
||||
per the audit-ct-tidy 2026-05-12 journal entry. No attribution
|
||||
cause across three audits. The pattern is a candidate for
|
||||
ratify-without-attribution at the milestone-close audit (audit-mq)
|
||||
but this iter follows the conservative pristine-baseline
|
||||
convention.
|
||||
|
||||
- **`latency.implicit_at_rc.max_us` max-tail noise.** Continues from
|
||||
mq.2's bench profile. Runtime metric, structurally uncoupled to
|
||||
this typecheck-side iter. Audit-ratifiable.
|
||||
|
||||
- **`bench/check.py` exit 1.** Same noise-class status as the two
|
||||
regressions above. Conservative interpretation: leave the
|
||||
baseline pristine; audit-mq decides whether to ratify.
|
||||
|
||||
- **`class_method_candidates` allocation per call.** Bounded by
|
||||
per-method candidate count (currently ≤ 2). A future tidy that
|
||||
introduces `use<'a, '_>` syntax across the crate could revisit
|
||||
the iterator-return shape.
|
||||
|
||||
## Files touched
|
||||
|
||||
Code:
|
||||
- crates/ail/src/main.rs
|
||||
- crates/ail/tests/typeclass_22b2.rs
|
||||
- crates/ail/tests/typeclass_22b3.rs
|
||||
- crates/ailang-check/src/builtins.rs
|
||||
- crates/ailang-check/src/diagnostic.rs
|
||||
- crates/ailang-check/src/lib.rs
|
||||
- crates/ailang-check/src/lift.rs
|
||||
- crates/ailang-check/src/mono.rs
|
||||
- crates/ailang-core/src/workspace.rs
|
||||
|
||||
New tests + fixtures:
|
||||
- crates/ail/tests/mq3_multi_class_e2e.rs
|
||||
- crates/ailang-check/tests/method_collision_pin.rs
|
||||
- examples/mq3_two_show_ambiguous_a.ail.json
|
||||
- examples/mq3_two_show_ambiguous_b.ail.json
|
||||
- examples/mq3_two_show_ambiguous.ail.json
|
||||
- examples/mq3_two_show_qualified.ail.json
|
||||
- examples/mq3_class_eq_vs_fn_eq_classmod.ail.json
|
||||
- examples/mq3_class_eq_vs_fn_eq_fnmod.ail.json
|
||||
- examples/mq3_class_eq_vs_fn_eq.ail.json
|
||||
|
||||
Docs:
|
||||
- docs/DESIGN.md
|
||||
- docs/roadmap.md
|
||||
|
||||
## Stats
|
||||
|
||||
bench/orchestrator-stats/2026-05-13-iter-mq.3.json
|
||||
@@ -44,3 +44,4 @@
|
||||
- 2026-05-12 — iter 24.1: `bool_to_str` + `str_clone` runtime + codegen wiring — 2 new C functions in `runtime/str.c` (slab-allocating heap-Str primitives via existing `str_alloc`), lockstep checker + synth.rs installs with `ret_mode: Own`, 2 IR-header declares + 2 `lower_app` arms + `is_static_callee` whitelist extension, 5 IR snapshots regen (2 declares × 5 files), 9 new tests (2 builtins-install unit + 2 IR-shape pin + 5 E2E all green), pre-existing-drift fix included (int_to_str row added to `builtins.rs::list()`). One substantive deviation: builtin-signatures registered in `uniqueness.rs::infer_module` + `linearity.rs::check_module_with_visible` (8 LOC × 2 files) so `str_clone`'s `param_modes: [Borrow]` is visible to the App-arg walker; symmetric to 23.4-prep's class-method registration; necessary for the plan's literal `frees == 3` cross-realisation assertion. Full `cargo test --workspace` 513 passed, 0 failed. `bench/compile_check.py` + `bench/cross_lang.py` green → 2026-05-12-iter-24.1.md
|
||||
- 2026-05-13 — iter mq.1: class-ref canonical-form extension + workspace-internal class-name qualification — three schema fields (`InstanceDef.class`, `Constraint.class`, `SuperclassRef.class`) move bare → canonical (bare for same-module, `<module>.<Class>` for cross-module) symmetric to ct.1's `Type::Con.name` rule; `ClassDef.name` stays bare (defining site, like `TypeDef.name`). `validate_canonical_type_names` gains three field-walks via new `check_class_ref` helper plus two sibling diagnostics `BareCrossModuleClassRef` / `BadCrossModuleClassRef`; `check_class_name_fields` narrowed to `ClassDef.name`-only. Workspace-internal class-name keys all qualified: `class_def_module`, `class_by_name`, registry `entries.0`, `ClassMethodEntry.class_name`, `class_superclasses`, mono's `class_index`, `Origin::Class.class_name`. New `qualify_class_ref` helper in `workspace.rs` plus sibling `qualify_class_ref_in_check` in `ailang-check`. Two new positive on-disk fixtures (`mq1_xmod_constraint_class{,_dep}`). Recon claim that all existing fixtures' class-refs are intra-module was empirically wrong — 5 test_22b* (`orphan_third`, `dup_a/b/entry`, `unbound_constraint_var`) + 5 other (`eq_ord_polymorphic`, `eq_ord_user_adt`, `cmp_max_smoke`, `ctt2_collision_{lib,main}`) fixtures required minimal canonical-form migration. Duplicate-instance test architecturally restructured: post-mq.1 orphan-freedom makes two-modules-on-same-`(class, type)` structurally impossible (any second instance in a non-owning module fires `OrphanInstance` first); new `test_22b1_dup_same_module.ail.json` lands both instances in the class's module — the only post-mq.1 way to land two on the same canonical key. Plan defects fixed inline: `Origin::Class.class_name` had one construction site not two (plan recon off-by-one); `build_class_index` in `mono.rs` needed qualifying (plan said "no code change needed"); `build_module_globals` needed a `Def::Instance` carve-out for qualified `inst.class`; `check_fn`'s declared-constraint matching needed inline canonical-form lifting; `NoInstance` Float-aware message arm needed `prelude.Eq`/`prelude.Ord` match; coherence type-leg needed qualified-head split-and-lookup. Tasks 3-6 ran as one coherent fix (Task 3 alone left tests RED). 7/7 tasks, 520 tests green, `bench/compile_check.py` + `bench/cross_lang.py` exit 0; `bench/check.py` exit 1 due to 4 improvements-beyond-tolerance (audit-ratifiable per convention, not a regression). `MethodNameCollision` + dispatch path unchanged; iters mq.2 + mq.3 land them → 2026-05-13-iter-mq.1.md
|
||||
- 2026-05-13 — iter mq.2: type-driven dispatch mechanism installed (mechanism-before-exercise) — `Env.method_to_candidate_classes` workspace-flat inverse index built alongside `class_methods`; two new `CheckError` variants `AmbiguousMethodResolution` + `UnknownClass` (Display+code+ctx) plus additive `NoInstance.candidate_classes` field; `ResidualConstraint` extended with `candidates: Option<BTreeSet<String>>` (visibility bumped `pub(crate)` → `pub` for test-crate access); pure `resolve_method_dispatch` helper implementing the spec's 5-step rule (qualifier → singleton → type-driven filter → constraint-driven filter → `Multi` for discharge-time refinement); synth Var-arm class-method branch rewritten via `parse_method_qualifier` with inner-dot gate (qualifier must be `<module>.<Class>` form, single-dot names like `std_list.length` fall through to the existing qualified-fn path); `refine_multi_candidate_residual` wired into `check_fn` discharge using `expanded` (post-superclass-expansion constraints) for the rigid-var path; `resolve_residual_class_for_mono` wired into mono's `collect_residuals_ordered` residual-to-target mapping. With `MethodNameCollision` still gating real workspaces, the new multi-candidate branches are exercised exclusively by 15 unit tests: 6 in `tests/method_dispatch_pin.rs` covering the 5-step rule's six cases, 6 in `lib.rs` `mod tests` covering variants + field shapes + discharge refinement, 3 in `mono.rs` `mod tests` covering the mono helper. Real workspaces continue producing single-class residuals (`candidates: None`); every pre-mq.2 fixture typechecks unchanged. Plan-invented `format_type_for_display` replaced with `ailang_core::pretty::type_to_string` (one less duplicate). Synth-time `declared_constraints: &[]` is a deliberate gap documented as known debt — load-bearing only post-mq.3 for the rigid-var fallback (env-plumbing the active fn's constraints into the Var arm is a ~10-line edit slated for mq.3). 9/9 tasks, 539 tests green (was 520 at start of mq.2; +19 = 15 new + 4 pre-existing); `bench/compile_check.py` + `cross_lang.py` clean; `bench/check.py` 1 regression (latency noise, runtime cannot be touched by typecheck-side iter). `MethodNameCollision` retirement lands in mq.3 → 2026-05-13-iter-mq.2.md
|
||||
- 2026-05-13 — iter mq.3: `MethodNameCollision` retired + multi-class E2E + DESIGN.md sync — milestone close. Deletes `WorkspaceLoadError::MethodNameCollision` variant + `Origin` enum + per-def collision loop in `build_registry` (workspace.rs 596-681) + Display arm in `main.rs:1201`; the two in-workspace.rs pin tests relocate to `crates/ailang-check/tests/method_collision_pin.rs` with inverted assertions (loads cleanly + `env.method_to_candidate_classes["foo"]` has two qualified-class entries). Resolves both mq.2 known-debt items: (1) `Env.active_declared_constraints: Vec<Constraint>` plumbed pre-synth in `check_fn` so the post-superclass-expansion constraint set reaches the synth Var-arm's `resolve_method_dispatch` constraint-driven filter; (2) `ModuleGlobals.class_methods` + `Env.class_methods` re-keyed from `BTreeMap<MethodName, ClassMethodEntry>` to `BTreeMap<(QualifiedClass, MethodName), ClassMethodEntry>` with new `class_method_candidates(name) -> Vec<(&class, &entry)>` accessor; mono's two presence-check sites (`rewrite_mono_calls`, `interleave_slots`) switched to consult `method_to_candidate_classes` natively (method-keyed, preserves the presence-check signature shape). New `synth(...)` warnings channel via `warnings: &mut Vec<Diagnostic>` out-parameter threaded through 15+ recursive callsites + 5 external callers (check_fn, check_const, 3 in builtins.rs, 1 in lift.rs, 2 in mono.rs, 1 in `check`); new structured warning `class-method-shadowed-by-fn` (kebab-case code, structured ctx carrying `name`/`method`/`fn_owner_module`/`candidate_classes`) fires at all three fn-precedence branches (locals, same-module fn, implicit-import fn). Implicit-import-fn branch reordered ABOVE the class-method branch per spec §"Class-fn collisions" so fn-wins precedence is structural. Three new positive E2E fixtures + integration tests in `crates/ail/tests/mq3_multi_class_e2e.rs`: (a) `mq3_two_show_ambiguous` (two `Show` classes, both with `Show Int`, bare `show 42` → `AmbiguousMethodResolution`); (b) `mq3_two_show_qualified` (same workspace, `mq3_two_show_ambiguous_a.Show.show 42` → clean); (c) `mq3_class_eq_vs_fn_eq` (`class MyEq { myeq }` + `fn myeq` + bare `myeq 1 2` → fn wins, warning fires). DESIGN.md sync: class-names paragraph rewritten to point at mq.1 canonical-form + mq.3 dispatch model; `MethodNameCollision` bullet struck from "Workspace-load (registry-build) diagnostics" with forward-pointing note; `AmbiguousMethodResolution` / `UnknownClass` / `class-method-shadowed-by-fn` + `NoInstance.candidate_classes` added to "Typecheck diagnostics"; `AmbiguousInstance` paragraph reworded to distinguish registry-level (per-class coherence via `DuplicateInstance`) from call-site (cross-class method ambiguity, new diagnostic); new `### Method dispatch` subsection anchors the 5-step rule with `method_to_candidate_classes` as the load-bearing data structure, the class-fn precedence rule, and the post-mq.3 tuple-keyed `class_methods` shape. Roadmap P2 milestone → `[x]` with three-iter summary; milestone-24 `depends on:` line struck and entry annotated "ready for re-brainstorm". Plan defects fixed inline: `check_fn` signature is `Result<()>` not `Result<(CheckedFn, Vec<Diagnostic>)>` — adopted mut-ref-accumulator pattern matching existing `Vec<CheckError>` shape; `class_method_candidates` returns `Vec<(...)>` not `impl Iterator<...>` (the `use<'a, '_>` opaque-type-capture syntax not yet idiomatic in this crate); instance-method body shape draft was `Term::App` but existing-convention is `Term::Lam` with `paramTypes`/`retType` — three fixtures repaired inline. One existing E2E test (`crates/ail/tests/typeclass_22b3.rs:rewrite_walker_skips_locally_shadowed_class_method`) now correctly fires the new warning (the fixture intentionally shadows a class method); test's `assert!(diags.is_empty())` relaxed to filter the new warning with a naming comment. 9/9 tasks, 545 tests green (was 539; +6 net = 3 new mq.3.x lib + 2 method_collision_pin + 3 mq3_multi_class_e2e − 2 deleted workspace.rs pin tests); `bench/compile_check.py` + `cross_lang.py` exit 0; `bench/check.py` exit 1 with 2 noise-class regressions (3rd-consecutive `bench_list_sum.bump_s` persistence + max_us tail metric, both runtime-uncoupled-to-typecheck-iter); prelude zero-diff. Module-qualified-class-names milestone structurally closed → 2026-05-13-iter-mq.3.md
|
||||
|
||||
+19
-20
@@ -80,9 +80,10 @@ context. Pick the next milestone from P1.)_
|
||||
- context: brainstorm 2026-05-12 (iter 23.5 wrap); deferral
|
||||
2026-05-13 (user-direction Option C after plan-recon flagged the
|
||||
collision; iter 24.1 retained as standalone runtime infrastructure).
|
||||
- depends on: P2 milestone "Module-qualified class names +
|
||||
type-driven method dispatch" (retires the `MethodNameCollision`
|
||||
workaround that drives the collision).
|
||||
- ready for re-brainstorm — the `MethodNameCollision` workaround
|
||||
that blocked the original spec retired in mq.3 (2026-05-13).
|
||||
Fresh brainstorm re-derives the spec against the post-retirement
|
||||
architecture (type-driven dispatch, class-ref canonical form).
|
||||
|
||||
## P2 — Medium-term
|
||||
|
||||
@@ -123,24 +124,22 @@ context. Pick the next milestone from P1.)_
|
||||
classes beyond the prelude four; multi-parameter classes; superclass
|
||||
chains; richer instance bodies. Deferred from milestone 22.
|
||||
- context: JOURNAL 2026-05-09
|
||||
- [ ] **\[milestone\]** Module-qualified class names + type-driven
|
||||
method dispatch — retire the `MethodNameCollision` workaround
|
||||
(`crates/ailang-core/src/workspace.rs:472`) that today keeps bare
|
||||
class names viable by enforcing workspace-global method-name
|
||||
uniqueness. Replaces name-driven method resolution
|
||||
(`ModuleGlobals::class_methods: IndexMap<String, ClassMethodEntry>`,
|
||||
`crates/ailang-check/src/lib.rs:983`; consumed by
|
||||
`mono::rewrite_class_method_calls`, `crates/ailang-check/src/mono.rs:657`)
|
||||
with type-driven dispatch — look up `eq` candidates by argument
|
||||
type, pick the unique class instance, fail closed on ambiguity.
|
||||
Once shipped, two libraries can each declare `class Eq` with their
|
||||
own `eq`. Carries its own DESIGN spec: inference rules, ambiguity
|
||||
diagnostics, and the canonical-form extension for class-reference
|
||||
fields (`InstanceDef.class`, `SuperclassRef.class`, `Constraint.class`)
|
||||
that the canonical-type-names milestone explicitly out-of-scoped.
|
||||
- [x] **\[milestone\]** Module-qualified class names + type-driven
|
||||
method dispatch — retired the `MethodNameCollision` workaround;
|
||||
shipped 2026-05-13 as iters mq.1 (canonical-form extension for
|
||||
class-ref fields + workspace-internal qualification), mq.2 (type-
|
||||
driven dispatch mechanism installed: `method_to_candidate_classes`
|
||||
inverse index, multi-candidate `ResidualConstraint`,
|
||||
`resolve_method_dispatch` 5-step rule, `AmbiguousMethodResolution` +
|
||||
`UnknownClass` diagnostics), and mq.3 (retirement + multi-class E2E
|
||||
+ `class-method-shadowed-by-fn` warning + DESIGN.md sync). Two
|
||||
libraries can now each declare `class Eq` with their own `eq`;
|
||||
cross-class method ambiguity is resolved at the call site via
|
||||
type-driven dispatch with `<module>.<Class>.<method>` as the
|
||||
disambiguation form.
|
||||
- context: `docs/specs/2026-05-10-canonical-type-names.md` "Out of
|
||||
scope: Class names" — the workaround is named there so it stays
|
||||
visible until this milestone retires it.
|
||||
scope: Class names" — the workaround was named there so it
|
||||
stayed visible until this milestone retired it.
|
||||
- [ ] **\[todo\]** Boehm full retirement — remove the transitional
|
||||
Boehm GC path now that RC + uniqueness is the canonical memory
|
||||
story.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "mq3_class_eq_vs_fn_eq",
|
||||
"imports": [
|
||||
{ "module": "mq3_class_eq_vs_fn_eq_classmod" },
|
||||
{ "module": "mq3_class_eq_vs_fn_eq_fnmod" }
|
||||
],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"doc": "mq.3.6 fixture (c): bare `myeq 1 2` in a workspace with `class MyEq` (in classmod) and `fn myeq` (in fnmod) both imported. Fn wins per lookup precedence; the typechecker emits `class-method-shadowed-by-fn` warning so the LLM-author sees the shadow.",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [],
|
||||
"ret": { "k": "con", "name": "Unit" },
|
||||
"effects": ["IO"]
|
||||
},
|
||||
"params": [],
|
||||
"body": {
|
||||
"t": "do",
|
||||
"op": "io/print_bool",
|
||||
"args": [
|
||||
{
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "myeq" },
|
||||
"args": [
|
||||
{ "t": "lit", "lit": { "kind": "int", "value": 1 } },
|
||||
{ "t": "lit", "lit": { "kind": "int", "value": 2 } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "mq3_class_eq_vs_fn_eq_classmod",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "class",
|
||||
"name": "MyEq",
|
||||
"param": "a",
|
||||
"methods": [
|
||||
{
|
||||
"name": "myeq",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [
|
||||
{ "k": "var", "name": "a" },
|
||||
{ "k": "var", "name": "a" }
|
||||
],
|
||||
"param_modes": ["borrow", "borrow"],
|
||||
"ret": { "k": "con", "name": "Bool" },
|
||||
"effects": []
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "instance",
|
||||
"class": "MyEq",
|
||||
"type": { "k": "con", "name": "Int" },
|
||||
"methods": [
|
||||
{
|
||||
"name": "myeq",
|
||||
"body": {
|
||||
"t": "lam",
|
||||
"params": ["x", "y"],
|
||||
"paramTypes": [
|
||||
{ "k": "con", "name": "Int" },
|
||||
{ "k": "con", "name": "Int" }
|
||||
],
|
||||
"retType": { "k": "con", "name": "Bool" },
|
||||
"body": { "t": "lit", "lit": { "kind": "bool", "value": true } }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "mq3_class_eq_vs_fn_eq_fnmod",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "myeq",
|
||||
"doc": "mq.3.6 fixture (c) helper: free fn `myeq` that shadows the class method `myeq` declared in `mq3_class_eq_vs_fn_eq_classmod`. Always returns false so the fn-wins precedence is observable at runtime if the e2e test ever runs the binary.",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [
|
||||
{ "k": "con", "name": "Int" },
|
||||
{ "k": "con", "name": "Int" }
|
||||
],
|
||||
"param_modes": ["borrow", "borrow"],
|
||||
"ret": { "k": "con", "name": "Bool" },
|
||||
"effects": []
|
||||
},
|
||||
"params": ["x", "y"],
|
||||
"body": { "t": "lit", "lit": { "kind": "bool", "value": false } }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "mq3_two_show_ambiguous",
|
||||
"imports": [
|
||||
{ "module": "mq3_two_show_ambiguous_a" },
|
||||
{ "module": "mq3_two_show_ambiguous_b" }
|
||||
],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"doc": "mq.3.6 fixture (a): bare `show 42` in a workspace where two Show classes (in modA and modB) each ship Show Int. Without an explicit qualifier the dispatch is genuinely ambiguous; the typechecker must fire `ambiguous-method-resolution` naming both candidate classes.",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [],
|
||||
"ret": { "k": "con", "name": "Unit" },
|
||||
"effects": ["IO"]
|
||||
},
|
||||
"params": [],
|
||||
"body": {
|
||||
"t": "do",
|
||||
"op": "io/print_str",
|
||||
"args": [
|
||||
{
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "show" },
|
||||
"args": [{ "t": "lit", "lit": { "kind": "int", "value": 42 } }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "mq3_two_show_ambiguous_a",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "class",
|
||||
"name": "Show",
|
||||
"param": "a",
|
||||
"methods": [
|
||||
{
|
||||
"name": "show",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [{ "k": "var", "name": "a" }],
|
||||
"param_modes": ["borrow"],
|
||||
"ret": { "k": "con", "name": "Str" },
|
||||
"effects": []
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "instance",
|
||||
"class": "Show",
|
||||
"type": { "k": "con", "name": "Int" },
|
||||
"methods": [
|
||||
{
|
||||
"name": "show",
|
||||
"body": {
|
||||
"t": "lam",
|
||||
"params": ["x"],
|
||||
"paramTypes": [{ "k": "con", "name": "Int" }],
|
||||
"retType": { "k": "con", "name": "Str" },
|
||||
"body": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "int_to_str" },
|
||||
"args": [{ "t": "var", "name": "x" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "mq3_two_show_ambiguous_b",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "class",
|
||||
"name": "Show",
|
||||
"param": "a",
|
||||
"methods": [
|
||||
{
|
||||
"name": "show",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [{ "k": "var", "name": "a" }],
|
||||
"param_modes": ["borrow"],
|
||||
"ret": { "k": "con", "name": "Str" },
|
||||
"effects": []
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "instance",
|
||||
"class": "Show",
|
||||
"type": { "k": "con", "name": "Int" },
|
||||
"methods": [
|
||||
{
|
||||
"name": "show",
|
||||
"body": {
|
||||
"t": "lam",
|
||||
"params": ["x"],
|
||||
"paramTypes": [{ "k": "con", "name": "Int" }],
|
||||
"retType": { "k": "con", "name": "Str" },
|
||||
"body": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "int_to_str" },
|
||||
"args": [{ "t": "var", "name": "x" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "mq3_two_show_qualified",
|
||||
"imports": [
|
||||
{ "module": "mq3_two_show_ambiguous_a" },
|
||||
{ "module": "mq3_two_show_ambiguous_b" }
|
||||
],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"doc": "mq.3.6 fixture (b): explicit qualifier `mq3_two_show_ambiguous_a.Show.show 42` disambiguates the same workspace as fixture (a). Resolves cleanly to modA's class; typecheck passes.",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [],
|
||||
"ret": { "k": "con", "name": "Unit" },
|
||||
"effects": ["IO"]
|
||||
},
|
||||
"params": [],
|
||||
"body": {
|
||||
"t": "do",
|
||||
"op": "io/print_str",
|
||||
"args": [
|
||||
{
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "mq3_two_show_ambiguous_a.Show.show" },
|
||||
"args": [{ "t": "lit", "lit": { "kind": "int", "value": 42 } }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user