iter 22b.2.6: method-name-collision diagnostic

This commit is contained in:
2026-05-09 18:07:52 +02:00
parent 1f6d2322c3
commit a9aa248910
4 changed files with 186 additions and 0 deletions
+19
View File
@@ -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,
})),
),
}
}
+114
View File
@@ -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<String, String> = 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:?}"),
}
}
}
@@ -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": []
}
}]
}
]
}
@@ -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" } }
}
]
}