diff --git a/crates/ail/src/main.rs b/crates/ail/src/main.rs index d00a4c0..3c7f30c 100644 --- a/crates/ail/src/main.rs +++ b/crates/ail/src/main.rs @@ -1029,6 +1029,23 @@ fn workspace_error_to_diagnostic( "method": method, })), ), + W::OverridingNonExistentMethod { + class, + type_repr, + method, + } => Some( + ailang_check::Diagnostic::error( + "overriding-non-existent-method", + format!( + "instance `{class} {type_repr}` provides body for method `{method}`, but class `{class}` does not declare it" + ), + ) + .with_ctx(serde_json::json!({ + "class": class, + "type": type_repr, + "method": method, + })), + ), W::KindMismatch { class, param, diff --git a/crates/ailang-core/src/workspace.rs b/crates/ailang-core/src/workspace.rs index 4fef528..443654f 100644 --- a/crates/ailang-core/src/workspace.rs +++ b/crates/ailang-core/src/workspace.rs @@ -200,6 +200,18 @@ pub enum WorkspaceLoadError { method: String, }, + /// Iter 22b.2: an instance specifies a body for a method name + /// that the corresponding class does not declare. Symmetric to + /// `MissingMethod` but in the opposite direction. + #[error( + "instance `{class} {type_repr}` provides body for method `{method}`, but class `{class}` does not declare it" + )] + OverridingNonExistentMethod { + class: String, + type_repr: String, + method: String, + }, + /// Iter 22b.2: class-schema validation. The class parameter /// appears in applied position (e.g. as the head of a /// `Type::Con { name == param, args.len() > 0 }`) inside a method @@ -420,6 +432,23 @@ fn build_registry( }); } } + + // Symmetric to MissingMethod: an instance must + // not specify a body for a method name the class + // never declared. Decision 11 forbids ad-hoc + // additions to a class's method set at the + // instance site. + let declared: BTreeSet<&str> = + class_def.methods.iter().map(|m| m.name.as_str()).collect(); + for inst_method in &inst.methods { + if !declared.contains(inst_method.name.as_str()) { + return Err(WorkspaceLoadError::OverridingNonExistentMethod { + class: inst.class.clone(), + type_repr: type_repr.clone(), + method: inst_method.name.clone(), + }); + } + } } entries.insert( @@ -890,6 +919,33 @@ mod tests { } } + /// Iter 22b.2: an instance that specifies a body for a method + /// name the class never declared must fire + /// `OverridingNonExistentMethod`. Symmetric counterpart to + /// `MissingMethod`: the latter fires when the class declares a + /// non-default method that the instance omits; this one fires + /// when the instance provides a body the class did not ask for. + /// Decision 11 forbids ad-hoc additions to a class's method set + /// at the instance site. + #[test] + fn instance_overriding_nonexistent_method_fires() { + let entry = std::path::PathBuf::from( + "../../examples/test_22b2_overriding_nonexistent.ail.json", + ); + let err = load_workspace(&entry) + .expect_err("must fire overriding-non-existent-method"); + match err { + WorkspaceLoadError::OverridingNonExistentMethod { + class, type_repr, method, + } => { + assert_eq!(class, "Eq"); + assert_eq!(type_repr, "Int"); + assert_eq!(method, "ne"); + } + other => panic!("expected OverridingNonExistentMethod, got {other:?}"), + } + } + /// Iter 22b.2: a class method's `Type::Forall` whose `constraints` /// reference a type variable that is neither bound by `Forall.vars` /// nor equal to the class's `param` must fire diff --git a/examples/test_22b2_overriding_nonexistent.ail.json b/examples/test_22b2_overriding_nonexistent.ail.json new file mode 100644 index 0000000..2a6dd5f --- /dev/null +++ b/examples/test_22b2_overriding_nonexistent.ail.json @@ -0,0 +1,38 @@ +{ + "schema": "ailang/v0", + "name": "test_22b2_overriding_nonexistent", + "imports": [], + "defs": [ + { + "kind": "class", + "name": "Eq", + "param": "a", + "methods": [ + { + "name": "eq", + "type": { + "k": "fn", + "params": [{ "k": "var", "name": "a" }, { "k": "var", "name": "a" }], + "ret": { "k": "con", "name": "Bool" }, + "effects": [] + } + } + ] + }, + { + "kind": "instance", + "class": "Eq", + "type": { "k": "con", "name": "Int" }, + "methods": [ + { + "name": "eq", + "body": { "t": "lit", "lit": { "kind": "bool", "value": true } } + }, + { + "name": "ne", + "body": { "t": "lit", "lit": { "kind": "bool", "value": false } } + } + ] + } + ] +}