diff --git a/crates/ailang-core/src/workspace.rs b/crates/ailang-core/src/workspace.rs index eac0fe6..27cc41f 100644 --- a/crates/ailang-core/src/workspace.rs +++ b/crates/ailang-core/src/workspace.rs @@ -824,6 +824,7 @@ pub(crate) fn validate_canonical_type_names( type_name, mod_name, &local_types, &import_names, ) })?; + check_class_name_fields(def, mod_name)?; } } @@ -1146,6 +1147,67 @@ where } } +/// ct.1: reject any `.` in the four class-reference fields. +/// `ClassDef.name`, `InstanceDef.class`, `SuperclassRef.class`, +/// `Constraint.class`. Per spec ยง"Out of scope: Class names": class +/// names are NOT module-qualified in this milestone, so any +/// qualified form indicates a half-migrated file. +fn check_class_name_fields( + def: &Def, + owning_module: &str, +) -> Result<(), WorkspaceLoadError> { + fn fire( + owning_module: &str, + name: &str, + field: &'static str, + ) -> WorkspaceLoadError { + WorkspaceLoadError::QualifiedClassName { + module: owning_module.to_string(), + name: name.to_string(), + field, + } + } + match def { + Def::Class(cd) => { + if cd.name.contains('.') { + return Err(fire(owning_module, &cd.name, "ClassDef.name")); + } + if let Some(sc) = &cd.superclass { + if sc.class.contains('.') { + return Err(fire(owning_module, &sc.class, "SuperclassRef.class")); + } + } + for m in &cd.methods { + if let Type::Forall { constraints, .. } = &m.ty { + for c in constraints { + if c.class.contains('.') { + return Err(fire(owning_module, &c.class, "Constraint.class")); + } + } + } + } + Ok(()) + } + Def::Instance(id) => { + if id.class.contains('.') { + return Err(fire(owning_module, &id.class, "InstanceDef.class")); + } + Ok(()) + } + Def::Fn(fd) => { + if let Type::Forall { constraints, .. } = &fd.ty { + for c in constraints { + if c.class.contains('.') { + return Err(fire(owning_module, &c.class, "Constraint.class")); + } + } + } + Ok(()) + } + Def::Const(_) | Def::Type(_) => Ok(()), + } +} + /// Iter 22b.1: extract the head-constructor name of a type, for the /// "where is this type defined" lookup and for diagnostic-message /// rendering. @@ -2044,4 +2106,129 @@ mod tests { other => panic!("expected BareCrossModuleTypeRef, got {other:?}"), } } + + /// ct.1: a qualified `ClassDef.name` (e.g. `other.Eq`) must fire + /// `QualifiedClassName`. Class names stay bare in this milestone. + #[test] + fn ct1_validator_rejects_qualified_classdef_name() { + let m: Module = serde_json::from_value(serde_json::json!({ + "schema": crate::SCHEMA, + "name": "m", + "imports": [], + "defs": [{ + "kind": "class", + "name": "other.MyEq", + "param": "a", + "methods": [] + }], + })).unwrap(); + let mut modules = BTreeMap::new(); + modules.insert("m".to_string(), m); + let err = validate_canonical_type_names(&modules) + .expect_err("qualified ClassDef.name must be rejected"); + match err { + WorkspaceLoadError::QualifiedClassName { module, name, field } => { + assert_eq!(module, "m"); + assert_eq!(name, "other.MyEq"); + assert_eq!(field, "ClassDef.name"); + } + other => panic!("expected QualifiedClassName, got {other:?}"), + } + } + + /// ct.1: a qualified `InstanceDef.class` must fire + /// `QualifiedClassName`. + #[test] + fn ct1_validator_rejects_qualified_instancedef_class() { + let m: Module = serde_json::from_value(serde_json::json!({ + "schema": crate::SCHEMA, + "name": "m", + "imports": [], + "defs": [{ + "kind": "instance", + "class": "other.MyEq", + "type": { "k": "con", "name": "Int" }, + "methods": [] + }], + })).unwrap(); + let mut modules = BTreeMap::new(); + modules.insert("m".to_string(), m); + let err = validate_canonical_type_names(&modules) + .expect_err("qualified InstanceDef.class must be rejected"); + match err { + WorkspaceLoadError::QualifiedClassName { name, field, .. } => { + assert_eq!(name, "other.MyEq"); + assert_eq!(field, "InstanceDef.class"); + } + other => panic!("expected QualifiedClassName, got {other:?}"), + } + } + + /// ct.1: a qualified `SuperclassRef.class` must fire + /// `QualifiedClassName`. + #[test] + fn ct1_validator_rejects_qualified_superclassref_class() { + let m: Module = serde_json::from_value(serde_json::json!({ + "schema": crate::SCHEMA, + "name": "m", + "imports": [], + "defs": [{ + "kind": "class", + "name": "MyOrd", + "param": "a", + "superclass": { "class": "other.MyEq", "type": "a" }, + "methods": [] + }], + })).unwrap(); + let mut modules = BTreeMap::new(); + modules.insert("m".to_string(), m); + let err = validate_canonical_type_names(&modules) + .expect_err("qualified SuperclassRef.class must be rejected"); + match err { + WorkspaceLoadError::QualifiedClassName { name, field, .. } => { + assert_eq!(name, "other.MyEq"); + assert_eq!(field, "SuperclassRef.class"); + } + other => panic!("expected QualifiedClassName, got {other:?}"), + } + } + + /// ct.1: a qualified `Constraint.class` (inside a `Type::Forall`) + /// must fire `QualifiedClassName`. + #[test] + fn ct1_validator_rejects_qualified_constraint_class() { + let m: Module = serde_json::from_value(serde_json::json!({ + "schema": crate::SCHEMA, + "name": "m", + "imports": [], + "defs": [{ + "kind": "fn", + "name": "f", + "type": { + "k": "forall", + "vars": ["a"], + "constraints": [ + { "class": "other.MyEq", "type": { "k": "var", "name": "a" } } + ], + "body": { + "k": "fn", "params": [{ "k": "var", "name": "a" }], + "ret": { "k": "con", "name": "Unit" }, "effects": [] + } + }, + "params": ["x"], + "body": { "t": "lit", "lit": { "kind": "unit" } } + }], + })).unwrap(); + let mut modules = BTreeMap::new(); + modules.insert("m".to_string(), m); + let err = validate_canonical_type_names(&modules) + .expect_err("qualified Constraint.class must be rejected"); + match err { + WorkspaceLoadError::QualifiedClassName { name, field, .. } => { + assert_eq!(name, "other.MyEq"); + assert_eq!(field, "Constraint.class"); + } + other => panic!("expected QualifiedClassName, got {other:?}"), + } + } }