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:
2026-02-14 14:21:49 +00:00
parent eded129438
commit e0f5a78b69

View File

@@ -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);
}
}