feat(validate): validate concept_comparison patterns

Checks that concept_comparison variant names exist in the referenced
concept's enum sub-concepts and that all variants are covered
(exhaustiveness checking).
This commit is contained in:
2026-02-14 14:24:05 +00:00
parent e0f5a78b69
commit 4ce325e4ac

View File

@@ -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<String> = 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<String> = 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::<Vec<_>>()
.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::<Vec<_>>().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::<Vec<_>>()
.join(", ")
),
help: Some(format!(
"Add patterns for all variants of concept '{}'. Missing: {}",
comp.name,
sorted
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.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<String>) -> 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, &registry, &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, &registry, &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, &registry, &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, &registry, &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, &registry, &mut collector);
assert!(!collector.has_errors());
}
}