iter 22b.2.6: fix — structural Origin enum, fn-fn collision delegation, test path helper

Address quality review of a9aa248:

- Replace string-prefix discriminator (`prior.starts_with("class ")`)
  with a structural `Origin { Class | Fn }` enum; `kind` is now a
  `match` on variants, formatting is reserved for error construction.
- Fix fn-fn collision misreport: in the `Def::Fn` arm, only fire
  `MethodNameCollision` when the prior origin is a class. Two
  `Def::Fn` with the same name are `CheckError::DuplicateDef`'s job
  in `ailang-check`, not this pre-pass.
- Tests use `examples_dir()` like the rest of the module; path
  construction is no longer ad-hoc.
- Tests assert `first_origin` / `second_origin` so an origin-format
  regression cannot silently break the `kind` discriminator.
- Drop forward-looking 22b.4 prelude reservation note from the
  variant doc — keep only what's load-bearing for 22b.2.
This commit is contained in:
2026-05-09 18:13:49 +02:00
parent a9aa248910
commit b252f837af
+85 -36
View File
@@ -258,9 +258,8 @@ pub enum WorkspaceLoadError {
},
/// 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"`.
/// 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}`"
)]
@@ -386,47 +385,73 @@ fn build_registry(
// 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();
//
// 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 {
let origin = format!("class {} (in {mod_name})", c.name);
let origin = Origin::Class {
class_name: c.name.clone(),
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: if prior.starts_with("class ") {
"class-class"
} else {
"class-fn"
},
first_origin: prior.clone(),
second_origin: origin,
kind,
first_origin: prior.format(),
second_origin: origin.format(),
});
}
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);
let origin = Origin::Fn {
name: f.name.clone(),
module: mod_name.clone(),
};
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,
});
// 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);
}
method_origins.insert(f.name.clone(), origin);
}
_ => {}
}
@@ -1050,15 +1075,27 @@ mod tests {
/// 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 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, .. } => {
WorkspaceLoadError::MethodNameCollision {
method,
kind,
first_origin,
second_origin,
} => {
assert_eq!(method, "foo");
assert_eq!(kind, "class-class");
assert!(
first_origin.starts_with("class A"),
"first_origin = {first_origin:?}",
);
assert!(
second_origin.starts_with("class B"),
"second_origin = {second_origin:?}",
);
}
other => panic!("expected MethodNameCollision, got {other:?}"),
}
@@ -1072,15 +1109,27 @@ mod tests {
/// 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 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, .. } => {
WorkspaceLoadError::MethodNameCollision {
method,
kind,
first_origin,
second_origin,
} => {
assert_eq!(method, "greet");
assert_eq!(kind, "class-fn");
assert!(
first_origin.starts_with("class Greet"),
"first_origin = {first_origin:?}",
);
assert!(
second_origin.starts_with("fn greet"),
"second_origin = {second_origin:?}",
);
}
other => panic!("expected MethodNameCollision, got {other:?}"),
}