iter 22b.2.6: method-name-collision diagnostic
This commit is contained in:
@@ -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,
|
||||
})),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user