From 0dae4308417910b14429859d0063964d7f047c76 Mon Sep 17 00:00:00 2001 From: Sienna Meridian Satterwhite Date: Sat, 14 Feb 2026 14:07:39 +0000 Subject: [PATCH] feat(validate): check parent concept exists for sub_concepts Added validation that sub_concept declarations reference an existing parent concept. Produces clear error message with guidance to add the missing concept declaration. --- src/resolve/validate.rs | 139 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/src/resolve/validate.rs b/src/resolve/validate.rs index 8e88e8b..b22992f 100644 --- a/src/resolve/validate.rs +++ b/src/resolve/validate.rs @@ -410,6 +410,40 @@ pub fn validate_schedule_composition(schedule: &Schedule, collector: &mut ErrorC } } +/// Validate that all sub_concepts reference existing parent concepts +pub fn validate_sub_concept_parents(file: &File, collector: &mut ErrorCollector) { + // Collect all concept names + let concept_names: HashSet = file + .declarations + .iter() + .filter_map(|decl| { + if let Declaration::Concept(c) = decl { + Some(c.name.clone()) + } else { + None + } + }) + .collect(); + + // Check each sub_concept references an existing parent + for decl in &file.declarations { + if let Declaration::SubConcept(sc) = decl { + if !concept_names.contains(&sc.parent_concept) { + collector.add(ResolveError::ValidationError { + message: format!( + "Parent concept '{}' not found for sub_concept '{}.{}'", + sc.parent_concept, sc.parent_concept, sc.name + ), + help: Some(format!( + "Add 'concept {}' before defining sub_concepts for it.", + sc.parent_concept + )), + }); + } + } + } +} + /// Validate an entire file /// /// Collects all validation errors and returns them together instead of failing @@ -417,6 +451,9 @@ pub fn validate_schedule_composition(schedule: &Schedule, collector: &mut ErrorC pub fn validate_file(file: &File, action_registry: &HashSet) -> Result<()> { let mut collector = ErrorCollector::new(); + // Type system validation + validate_sub_concept_parents(file, &mut collector); + for decl in &file.declarations { match decl { | Declaration::Character(c) => { @@ -652,4 +689,106 @@ mod tests { validate_behavior_tree_actions(&tree, ®istry, &mut collector); assert!(collector.has_errors()); } + + #[test] + fn test_sub_concept_with_valid_parent() { + let 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), + }), + ], + }; + + let mut collector = ErrorCollector::new(); + validate_sub_concept_parents(&file, &mut collector); + assert!(!collector.has_errors()); + } + + #[test] + fn test_sub_concept_with_missing_parent() { + let file = File { + declarations: vec![Declaration::SubConcept(SubConceptDecl { + name: "Type".to_string(), + parent_concept: "Cup".to_string(), + kind: SubConceptKind::Enum { + variants: vec!["Small".to_string()], + }, + span: Span::new(0, 40), + })], + }; + + let mut collector = ErrorCollector::new(); + validate_sub_concept_parents(&file, &mut collector); + assert!(collector.has_errors()); + } + + #[test] + fn test_multiple_sub_concepts_same_valid_parent() { + let 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!["Mug".to_string()], + }, + span: Span::new(20, 40), + }), + Declaration::SubConcept(SubConceptDecl { + name: "Material".to_string(), + parent_concept: "Cup".to_string(), + kind: SubConceptKind::Enum { + variants: vec!["Glass".to_string()], + }, + span: Span::new(50, 70), + }), + ], + }; + + let mut collector = ErrorCollector::new(); + validate_sub_concept_parents(&file, &mut collector); + assert!(!collector.has_errors()); + } + + #[test] + fn test_sub_concept_wrong_parent_name() { + let file = File { + declarations: vec![ + Declaration::Concept(ConceptDecl { + name: "Plate".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()], + }, + span: Span::new(20, 40), + }), + ], + }; + + let mut collector = ErrorCollector::new(); + validate_sub_concept_parents(&file, &mut collector); + assert!(collector.has_errors()); + } }