diff --git a/src/resolve/validate.rs b/src/resolve/validate.rs index 750c624..388a341 100644 --- a/src/resolve/validate.rs +++ b/src/resolve/validate.rs @@ -542,6 +542,133 @@ pub fn validate_species_type_invariance(file: &File, collector: &mut ErrorCollec } } +/// Validate concept_comparison patterns against the concept registry +/// +/// Checks: +/// 1. The concept_comparison name references an existing concept +/// 2. All variant names in patterns exist in the concept's enum sub-concepts +/// 3. Patterns are exhaustive (all variants are covered) +pub fn validate_concept_comparison_patterns( + file: &File, + registry: &crate::resolve::types::ConceptRegistry, + collector: &mut ErrorCollector, +) { + for decl in &file.declarations { + if let Declaration::ConceptComparison(comp) = decl { + // Check that the concept exists + let concept = match registry.lookup_concept(&comp.name) { + | Some(c) => c, + | None => { + collector.add(ResolveError::ValidationError { + message: format!( + "Concept '{}' not found for concept_comparison", + comp.name + ), + help: Some(format!( + "Add 'concept {}' before defining a concept_comparison for it.", + comp.name + )), + }); + continue; + }, + }; + + // Collect all enum variants from all enum sub-concepts + let mut all_variants: HashSet = HashSet::new(); + for sc_info in concept.sub_concepts.values() { + if let SubConceptKind::Enum { variants } = &sc_info.kind { + for variant in variants { + all_variants.insert(variant.clone()); + } + } + } + + // Check each variant pattern references valid variants + let mut covered_variants: HashSet = HashSet::new(); + for pattern in &comp.variants { + // Check that the variant name is a known variant + if !all_variants.contains(&pattern.name) { + collector.add(ResolveError::ValidationError { + message: format!( + "Variant '{}' not found in concept '{}'", + pattern.name, comp.name + ), + help: Some(format!( + "Available variants for '{}': {}", + comp.name, + if all_variants.is_empty() { + "(none - add sub_concept with enum variants)".to_string() + } else { + let mut sorted: Vec<_> = all_variants.iter().collect(); + sorted.sort(); + sorted + .iter() + .map(|s| s.as_str()) + .collect::>() + .join(", ") + } + )), + }); + } else { + covered_variants.insert(pattern.name.clone()); + } + + // Check field conditions reference valid fields in the Is values + for condition in &pattern.conditions { + if let Condition::Is(values) = &condition.condition { + for value in values { + // Check that the value references a valid variant + if !all_variants.contains(value) { + collector.add(ResolveError::ValidationError { + message: format!( + "Value '{}' in condition for variant '{}' not found in concept '{}'", + value, pattern.name, comp.name + ), + help: Some(format!( + "Available variants: {}", + { + let mut sorted: Vec<_> = all_variants.iter().collect(); + sorted.sort(); + sorted.iter().map(|s| s.as_str()).collect::>().join(", ") + } + )), + }); + } + } + } + } + } + + // Check exhaustiveness - all variants should be covered + let uncovered: Vec<_> = all_variants.difference(&covered_variants).collect(); + if !uncovered.is_empty() { + let mut sorted: Vec<_> = uncovered.into_iter().collect(); + sorted.sort(); + collector.add(ResolveError::ValidationError { + message: format!( + "concept_comparison '{}' is not exhaustive: missing variants {}", + comp.name, + sorted + .iter() + .map(|s| format!("'{}'", s)) + .collect::>() + .join(", ") + ), + help: Some(format!( + "Add patterns for all variants of concept '{}'. Missing: {}", + comp.name, + sorted + .iter() + .map(|s| s.as_str()) + .collect::>() + .join(", ") + )), + }); + } + } + } +} + /// Validate an entire file /// /// Collects all validation errors and returns them together instead of failing @@ -552,6 +679,8 @@ pub fn validate_file(file: &File, action_registry: &HashSet) -> Result<( // Type system validation validate_sub_concept_parents(file, &mut collector); validate_species_type_invariance(file, &mut collector); + let concept_registry = crate::resolve::types::ConceptRegistry::from_file(file); + validate_concept_comparison_patterns(file, &concept_registry, &mut collector); for decl in &file.declarations { match decl { @@ -1053,4 +1182,179 @@ mod tests { validate_species_type_invariance(&file, &mut collector); assert!(!collector.has_errors()); } + + // ===== Concept Comparison Validation Tests ===== + + use crate::resolve::types::ConceptRegistry; + + fn make_concept_comparison_file() -> File { + File { + declarations: vec![ + Declaration::Concept(ConceptDecl { + name: "Cup".to_string(), + span: Span::new(0, 10), + }), + Declaration::SubConcept(SubConceptDecl { + name: "Type".to_string(), + parent_concept: "Cup".to_string(), + kind: SubConceptKind::Enum { + variants: vec![ + "Small".to_string(), + "Medium".to_string(), + "Large".to_string(), + ], + }, + span: Span::new(20, 60), + }), + ], + } + } + + #[test] + fn test_concept_comparison_valid_exhaustive() { + let mut file = make_concept_comparison_file(); + file.declarations + .push(Declaration::ConceptComparison(ConceptComparisonDecl { + name: "Cup".to_string(), + variants: vec![ + VariantPattern { + name: "Small".to_string(), + conditions: vec![], + span: Span::new(0, 10), + }, + VariantPattern { + name: "Medium".to_string(), + conditions: vec![], + span: Span::new(0, 10), + }, + VariantPattern { + name: "Large".to_string(), + conditions: vec![], + span: Span::new(0, 10), + }, + ], + span: Span::new(0, 100), + })); + + let registry = ConceptRegistry::from_file(&file); + let mut collector = ErrorCollector::new(); + validate_concept_comparison_patterns(&file, ®istry, &mut collector); + assert!(!collector.has_errors()); + } + + #[test] + fn test_concept_comparison_not_exhaustive() { + let mut file = make_concept_comparison_file(); + file.declarations + .push(Declaration::ConceptComparison(ConceptComparisonDecl { + name: "Cup".to_string(), + variants: vec![ + VariantPattern { + name: "Small".to_string(), + conditions: vec![], + span: Span::new(0, 10), + }, + // Missing Medium and Large + ], + span: Span::new(0, 100), + })); + + let registry = ConceptRegistry::from_file(&file); + let mut collector = ErrorCollector::new(); + validate_concept_comparison_patterns(&file, ®istry, &mut collector); + assert!(collector.has_errors()); + } + + #[test] + fn test_concept_comparison_unknown_variant() { + let mut file = make_concept_comparison_file(); + file.declarations + .push(Declaration::ConceptComparison(ConceptComparisonDecl { + name: "Cup".to_string(), + variants: vec![ + VariantPattern { + name: "Small".to_string(), + conditions: vec![], + span: Span::new(0, 10), + }, + VariantPattern { + name: "Medium".to_string(), + conditions: vec![], + span: Span::new(0, 10), + }, + VariantPattern { + name: "Large".to_string(), + conditions: vec![], + span: Span::new(0, 10), + }, + VariantPattern { + name: "Huge".to_string(), // Not a valid variant + conditions: vec![], + span: Span::new(0, 10), + }, + ], + span: Span::new(0, 100), + })); + + let registry = ConceptRegistry::from_file(&file); + let mut collector = ErrorCollector::new(); + validate_concept_comparison_patterns(&file, ®istry, &mut collector); + assert!(collector.has_errors()); + } + + #[test] + fn test_concept_comparison_missing_concept() { + let file = File { + declarations: vec![Declaration::ConceptComparison(ConceptComparisonDecl { + name: "NonExistent".to_string(), + variants: vec![], + span: Span::new(0, 100), + })], + }; + + let registry = ConceptRegistry::from_file(&file); + let mut collector = ErrorCollector::new(); + validate_concept_comparison_patterns(&file, ®istry, &mut collector); + assert!(collector.has_errors()); + } + + #[test] + fn test_concept_comparison_with_field_conditions() { + let mut file = make_concept_comparison_file(); + file.declarations + .push(Declaration::ConceptComparison(ConceptComparisonDecl { + name: "Cup".to_string(), + variants: vec![ + VariantPattern { + name: "Small".to_string(), + conditions: vec![FieldCondition { + field_name: "material".to_string(), + condition: Condition::Is(vec!["Medium".to_string()]), // Valid variant ref + span: Span::new(0, 10), + }], + span: Span::new(0, 10), + }, + VariantPattern { + name: "Medium".to_string(), + conditions: vec![FieldCondition { + field_name: "material".to_string(), + condition: Condition::Any, + span: Span::new(0, 10), + }], + span: Span::new(0, 10), + }, + VariantPattern { + name: "Large".to_string(), + conditions: vec![], + span: Span::new(0, 10), + }, + ], + span: Span::new(0, 100), + })); + + let registry = ConceptRegistry::from_file(&file); + let mut collector = ErrorCollector::new(); + validate_concept_comparison_patterns(&file, ®istry, &mut collector); + assert!(!collector.has_errors()); + } }