diff --git a/src/resolve/types.rs b/src/resolve/types.rs index 5232f2e..8b9e278 100644 --- a/src/resolve/types.rs +++ b/src/resolve/types.rs @@ -3,9 +3,20 @@ //! These types are similar to AST types but represent fully resolved, //! validated entities with all cross-references resolved. -use crate::syntax::ast::{ - Field, - Span, +use std::collections::HashMap; + +use crate::{ + resolve::{ + ResolveError, + Result, + }, + syntax::ast::{ + Declaration, + Field, + File, + Span, + SubConceptKind, + }, }; /// A fully resolved file with all cross-references resolved @@ -137,3 +148,350 @@ pub struct ResolvedEnum { pub span: Span, pub qualified_path: Vec, } + +// ===== Concept Registry ===== + +/// Information about a registered concept +#[derive(Debug, Clone)] +pub struct ConceptInfo { + pub name: String, + pub sub_concepts: HashMap, +} + +/// Information about a registered sub-concept +#[derive(Debug, Clone)] +pub struct SubConceptInfo { + pub name: String, + pub parent: String, + pub kind: SubConceptKind, +} + +/// Registry tracking all concepts and sub_concepts for type checking +#[derive(Debug, Clone, Default)] +pub struct ConceptRegistry { + concepts: HashMap, +} + +impl ConceptRegistry { + pub fn new() -> Self { + Self { + concepts: HashMap::new(), + } + } + + /// Build a registry from a parsed file + pub fn from_file(file: &File) -> Self { + let mut registry = Self::new(); + + for decl in &file.declarations { + match decl { + | Declaration::Concept(c) => { + registry.register_concept(c.name.clone()); + }, + | Declaration::SubConcept(sc) => { + registry.register_sub_concept( + sc.parent_concept.clone(), + sc.name.clone(), + sc.kind.clone(), + ); + }, + | _ => {}, + } + } + + registry + } + + /// Register a new concept + pub fn register_concept(&mut self, name: String) { + self.concepts + .entry(name.clone()) + .or_insert_with(|| ConceptInfo { + name, + sub_concepts: HashMap::new(), + }); + } + + /// Register a sub-concept under its parent + pub fn register_sub_concept(&mut self, parent: String, name: String, kind: SubConceptKind) { + let concept = self + .concepts + .entry(parent.clone()) + .or_insert_with(|| ConceptInfo { + name: parent.clone(), + sub_concepts: HashMap::new(), + }); + + concept + .sub_concepts + .insert(name.clone(), SubConceptInfo { name, parent, kind }); + } + + /// Look up a concept by name + pub fn lookup_concept(&self, name: &str) -> Option<&ConceptInfo> { + self.concepts.get(name) + } + + /// Validate that a sub-concept variant reference is valid + /// + /// For enum sub-concepts, checks that the variant exists. + /// For record sub-concepts, checks that the field exists. + pub fn validate_sub_concept_reference( + &self, + parent: &str, + sub_concept: &str, + variant: &str, + ) -> Result<()> { + let concept = self + .concepts + .get(parent) + .ok_or_else(|| ResolveError::ValidationError { + message: format!("Concept '{}' not found", parent), + help: Some(format!( + "Add 'concept {}' to define this concept before referencing it.", + parent + )), + })?; + + let sc = concept.sub_concepts.get(sub_concept).ok_or_else(|| { + let available = concept + .sub_concepts + .keys() + .map(|k| k.as_str()) + .collect::>() + .join(", "); + ResolveError::ValidationError { + message: format!("Sub-concept '{}.{}' not found", parent, sub_concept), + help: Some(format!( + "Available sub-concepts for '{}': {}", + parent, + if available.is_empty() { + "(none)".to_string() + } else { + available + } + )), + } + })?; + + match &sc.kind { + | SubConceptKind::Enum { variants } => { + if !variants.contains(&variant.to_string()) { + let available = variants.join(", "); + return Err(ResolveError::ValidationError { + message: format!( + "Variant '{}' not found in sub-concept '{}.{}'", + variant, parent, sub_concept + ), + help: Some(format!("Available variants: {}", available)), + }); + } + }, + | SubConceptKind::Record { fields } => { + if !fields.iter().any(|f| f.name == variant) { + let available = fields + .iter() + .map(|f| f.name.as_str()) + .collect::>() + .join(", "); + return Err(ResolveError::ValidationError { + message: format!( + "Field '{}' not found in record sub-concept '{}.{}'", + variant, parent, sub_concept + ), + help: Some(format!("Available fields: {}", available)), + }); + } + }, + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_register_and_lookup_concept() { + let mut registry = ConceptRegistry::new(); + registry.register_concept("Cup".to_string()); + + let concept = registry.lookup_concept("Cup"); + assert!(concept.is_some()); + assert_eq!(concept.unwrap().name, "Cup"); + } + + #[test] + fn test_lookup_missing_concept() { + let registry = ConceptRegistry::new(); + assert!(registry.lookup_concept("Cup").is_none()); + } + + #[test] + fn test_register_sub_concept_enum() { + let mut registry = ConceptRegistry::new(); + registry.register_concept("Cup".to_string()); + registry.register_sub_concept( + "Cup".to_string(), + "Type".to_string(), + SubConceptKind::Enum { + variants: vec![ + "Small".to_string(), + "Medium".to_string(), + "Large".to_string(), + ], + }, + ); + + let concept = registry.lookup_concept("Cup").unwrap(); + assert_eq!(concept.sub_concepts.len(), 1); + assert!(concept.sub_concepts.contains_key("Type")); + } + + #[test] + fn test_register_sub_concept_record() { + let mut registry = ConceptRegistry::new(); + registry.register_concept("Cup".to_string()); + registry.register_sub_concept( + "Cup".to_string(), + "Material".to_string(), + SubConceptKind::Record { + fields: vec![Field { + name: "weight".to_string(), + value: crate::syntax::ast::Value::Number(100), + span: Span::new(0, 10), + }], + }, + ); + + let concept = registry.lookup_concept("Cup").unwrap(); + assert!(concept.sub_concepts.contains_key("Material")); + } + + #[test] + fn test_validate_valid_variant_reference() { + let mut registry = ConceptRegistry::new(); + registry.register_concept("Cup".to_string()); + registry.register_sub_concept( + "Cup".to_string(), + "Type".to_string(), + SubConceptKind::Enum { + variants: vec!["Small".to_string(), "Large".to_string()], + }, + ); + + assert!(registry + .validate_sub_concept_reference("Cup", "Type", "Small") + .is_ok()); + } + + #[test] + fn test_validate_invalid_variant_reference() { + let mut registry = ConceptRegistry::new(); + registry.register_concept("Cup".to_string()); + registry.register_sub_concept( + "Cup".to_string(), + "Type".to_string(), + SubConceptKind::Enum { + variants: vec!["Small".to_string(), "Large".to_string()], + }, + ); + + let result = registry.validate_sub_concept_reference("Cup", "Type", "Huge"); + assert!(result.is_err()); + } + + #[test] + fn test_validate_missing_concept() { + let registry = ConceptRegistry::new(); + let result = registry.validate_sub_concept_reference("Cup", "Type", "Small"); + assert!(result.is_err()); + } + + #[test] + fn test_validate_missing_sub_concept() { + let mut registry = ConceptRegistry::new(); + registry.register_concept("Cup".to_string()); + + let result = registry.validate_sub_concept_reference("Cup", "Type", "Small"); + assert!(result.is_err()); + } + + #[test] + fn test_validate_record_field_reference() { + let mut registry = ConceptRegistry::new(); + registry.register_concept("Cup".to_string()); + registry.register_sub_concept( + "Cup".to_string(), + "Size".to_string(), + SubConceptKind::Record { + fields: vec![ + Field { + name: "height".to_string(), + value: crate::syntax::ast::Value::Number(10), + span: Span::new(0, 10), + }, + Field { + name: "width".to_string(), + value: crate::syntax::ast::Value::Number(5), + span: Span::new(0, 10), + }, + ], + }, + ); + + assert!(registry + .validate_sub_concept_reference("Cup", "Size", "height") + .is_ok()); + assert!(registry + .validate_sub_concept_reference("Cup", "Size", "depth") + .is_err()); + } + + #[test] + fn test_from_file() { + let file = File { + declarations: vec![ + Declaration::Concept(crate::syntax::ast::ConceptDecl { + name: "Cup".to_string(), + span: Span::new(0, 10), + }), + Declaration::SubConcept(crate::syntax::ast::SubConceptDecl { + name: "Type".to_string(), + parent_concept: "Cup".to_string(), + kind: SubConceptKind::Enum { + variants: vec!["Small".to_string(), "Large".to_string()], + }, + span: Span::new(20, 60), + }), + ], + }; + + let registry = ConceptRegistry::from_file(&file); + let concept = registry.lookup_concept("Cup").unwrap(); + assert_eq!(concept.sub_concepts.len(), 1); + assert!(registry + .validate_sub_concept_reference("Cup", "Type", "Small") + .is_ok()); + } + + #[test] + fn test_sub_concept_auto_creates_parent() { + let mut registry = ConceptRegistry::new(); + // Register sub-concept without explicitly registering parent + registry.register_sub_concept( + "Cup".to_string(), + "Type".to_string(), + SubConceptKind::Enum { + variants: vec!["Small".to_string()], + }, + ); + + // Parent should be auto-created + let concept = registry.lookup_concept("Cup"); + assert!(concept.is_some()); + assert_eq!(concept.unwrap().sub_concepts.len(), 1); + } +}