From a9aa248910bec45b4e5286771ab52d89f2cfc96d Mon Sep 17 00:00:00 2001 From: Brummel Date: Sat, 9 May 2026 18:07:52 +0200 Subject: [PATCH] iter 22b.2.6: method-name-collision diagnostic --- crates/ail/src/main.rs | 19 +++ crates/ailang-core/src/workspace.rs | 114 ++++++++++++++++++ ...method_name_collision_class_class.ail.json | 27 +++++ ...b2_method_name_collision_class_fn.ail.json | 26 ++++ 4 files changed, 186 insertions(+) create mode 100644 examples/test_22b2_method_name_collision_class_class.ail.json create mode 100644 examples/test_22b2_method_name_collision_class_fn.ail.json diff --git a/crates/ail/src/main.rs b/crates/ail/src/main.rs index 3c7f30c..26385f2 100644 --- a/crates/ail/src/main.rs +++ b/crates/ail/src/main.rs @@ -1104,6 +1104,25 @@ 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, + })), + ), } } diff --git a/crates/ailang-core/src/workspace.rs b/crates/ailang-core/src/workspace.rs index bef7614..0219566 100644 --- a/crates/ailang-core/src/workspace.rs +++ b/crates/ailang-core/src/workspace.rs @@ -256,6 +256,20 @@ pub enum WorkspaceLoadError { type_repr: String, method: String, }, + + /// Iter 22b.2: a class-method name collides with another + /// class-method or with a top-level fn. The Prelude reserves + /// names `show`, `eq`, `ne`, `lt`, `le`, `gt`, `ge` once 22b.4 + /// lands. `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, + }, } /// Hash over the canonical bytes of a complete module. @@ -363,6 +377,62 @@ 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. + let mut method_origins: BTreeMap = BTreeMap::new(); + for (mod_name, m) in modules { + for def in &m.defs { + match def { + Def::Class(c) => { + for method in &c.methods { + let origin = format!("class {} (in {mod_name})", c.name); + if let Some(prior) = method_origins.get(&method.name) { + return Err(WorkspaceLoadError::MethodNameCollision { + method: method.name.clone(), + kind: if prior.starts_with("class ") { + "class-class" + } else { + "class-fn" + }, + first_origin: prior.clone(), + second_origin: origin, + }); + } + method_origins.insert(method.name.clone(), origin); + } + } + Def::Fn(f) => { + // fn-fn collisions (two `Def::Fn` with the same + // name) are a separate concern: `ailang-check` + // surfaces them as `CheckError::DuplicateDef`. The + // `kind` literal here is therefore `"class-fn"` + // unconditionally — by construction, the only + // collision this pass cares about is a class + // method shadowing (or being shadowed by) a free + // fn. + let origin = format!("fn {} (in {mod_name})", f.name); + if let Some(prior) = method_origins.get(&f.name) { + return Err(WorkspaceLoadError::MethodNameCollision { + method: f.name.clone(), + kind: "class-fn", + first_origin: prior.clone(), + second_origin: origin, + }); + } + method_origins.insert(f.name.clone(), origin); + } + _ => {} + } + } + } + // Pass 2: register each instance, with coherence / uniqueness / // method-completeness checks. let mut entries: BTreeMap<(String, String), RegistryEntry> = BTreeMap::new(); @@ -971,4 +1041,48 @@ mod tests { other => panic!("expected UnboundConstraintTypeVar, got {other:?}"), } } + + /// 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 = std::path::PathBuf::from( + "../../examples/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, .. } => { + assert_eq!(method, "foo"); + assert_eq!(kind, "class-class"); + } + 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 = std::path::PathBuf::from( + "../../examples/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, .. } => { + assert_eq!(method, "greet"); + assert_eq!(kind, "class-fn"); + } + other => panic!("expected MethodNameCollision, got {other:?}"), + } + } } diff --git a/examples/test_22b2_method_name_collision_class_class.ail.json b/examples/test_22b2_method_name_collision_class_class.ail.json new file mode 100644 index 0000000..92b49da --- /dev/null +++ b/examples/test_22b2_method_name_collision_class_class.ail.json @@ -0,0 +1,27 @@ +{ + "schema": "ailang/v0", + "name": "test_22b2_method_name_collision_class_class", + "imports": [], + "defs": [ + { + "kind": "class", "name": "A", "param": "a", + "methods": [{ + "name": "foo", + "type": { + "k": "fn", "params": [{ "k": "var", "name": "a" }], + "ret": { "k": "con", "name": "Unit" }, "effects": [] + } + }] + }, + { + "kind": "class", "name": "B", "param": "b", + "methods": [{ + "name": "foo", + "type": { + "k": "fn", "params": [{ "k": "var", "name": "b" }], + "ret": { "k": "con", "name": "Unit" }, "effects": [] + } + }] + } + ] +} diff --git a/examples/test_22b2_method_name_collision_class_fn.ail.json b/examples/test_22b2_method_name_collision_class_fn.ail.json new file mode 100644 index 0000000..6cc92fa --- /dev/null +++ b/examples/test_22b2_method_name_collision_class_fn.ail.json @@ -0,0 +1,26 @@ +{ + "schema": "ailang/v0", + "name": "test_22b2_method_name_collision_class_fn", + "imports": [], + "defs": [ + { + "kind": "class", "name": "Greet", "param": "a", + "methods": [{ + "name": "greet", + "type": { + "k": "fn", "params": [{ "k": "var", "name": "a" }], + "ret": { "k": "con", "name": "Str" }, "effects": [] + } + }] + }, + { + "kind": "fn", "name": "greet", + "type": { + "k": "fn", "params": [{ "k": "con", "name": "Int" }], + "ret": { "k": "con", "name": "Str" }, "effects": [] + }, + "params": ["x"], + "body": { "t": "lit", "lit": { "kind": "str", "value": "hi" } } + } + ] +}