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,
|
||||
//! 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<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