feat(types): add concept registry for type checking
ConceptRegistry tracks all concepts and sub_concepts, providing lookup and validation for sub-concept variant/field references. Supports both enum and record sub-concept kinds.
This commit is contained in:
@@ -3,9 +3,20 @@
|
|||||||
//! These types are similar to AST types but represent fully resolved,
|
//! These types are similar to AST types but represent fully resolved,
|
||||||
//! validated entities with all cross-references resolved.
|
//! validated entities with all cross-references resolved.
|
||||||
|
|
||||||
use crate::syntax::ast::{
|
use std::collections::HashMap;
|
||||||
Field,
|
|
||||||
Span,
|
use crate::{
|
||||||
|
resolve::{
|
||||||
|
ResolveError,
|
||||||
|
Result,
|
||||||
|
},
|
||||||
|
syntax::ast::{
|
||||||
|
Declaration,
|
||||||
|
Field,
|
||||||
|
File,
|
||||||
|
Span,
|
||||||
|
SubConceptKind,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A fully resolved file with all cross-references resolved
|
/// A fully resolved file with all cross-references resolved
|
||||||
@@ -137,3 +148,350 @@ pub struct ResolvedEnum {
|
|||||||
pub span: Span,
|
pub span: Span,
|
||||||
pub qualified_path: Vec<String>,
|
pub qualified_path: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Concept Registry =====
|
||||||
|
|
||||||
|
/// Information about a registered concept
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ConceptInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub sub_concepts: HashMap<String, SubConceptInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<String, ConceptInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<Vec<_>>()
|
||||||
|
.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::<Vec<_>>()
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user