feat(parser): add dot notation for sub_concept declarations

Changed sub_concept syntax from heuristic parent extraction to
explicit dot notation: `sub_concept Parent.Name { ... }`

The old syntax inferred the parent concept from the name using
uppercase letter heuristics. The new syntax makes the parent
concept explicit and unambiguous.

Added type_system_tests module with comprehensive tests.
This commit is contained in:
2026-02-14 14:05:47 +00:00
parent 8e4bdd3942
commit 0cd00a9e73
4 changed files with 1048 additions and 883 deletions

View File

@@ -20,6 +20,9 @@ mod resource_linking_tests;
#[cfg(test)]
mod schedule_composition_tests;
#[cfg(test)]
mod type_system_tests;
use miette::Diagnostic;
use thiserror::Error;

View File

@@ -753,22 +753,8 @@ ConceptDecl: ConceptDecl = {
};
SubConceptDecl: SubConceptDecl = {
// Enum-like sub_concept: sub_concept PlateColor { Red, Blue, Green }
"sub_concept" <name:Ident> "{" <variants:Comma<Ident>> "}" => {
let parent = {
let mut last_cap = 0;
for (i, ch) in name.char_indices().skip(1) {
if ch.is_uppercase() {
last_cap = i;
}
}
if last_cap > 0 {
name[..last_cap].to_string()
} else {
name.clone()
}
};
// Enum-like sub_concept: sub_concept Cup.Type { Small, Medium, Large }
"sub_concept" <parent:Ident> "." <name:Ident> "{" <variants:Comma<Ident>> "}" => {
SubConceptDecl {
name,
parent_concept: parent,
@@ -776,22 +762,8 @@ SubConceptDecl: SubConceptDecl = {
span: Span::new(0, 0),
}
},
// Record-like sub_concept with at least one field
"sub_concept" <name:Ident> "{" <first:Ident> ":" <first_val:Value> <rest:("," <Ident> ":" <Value>)*> ","? "}" => {
let parent = {
let mut last_cap = 0;
for (i, ch) in name.char_indices().skip(1) {
if ch.is_uppercase() {
last_cap = i;
}
}
if last_cap > 0 {
name[..last_cap].to_string()
} else {
name.clone()
}
};
// Record-like sub_concept with at least one field: sub_concept Cup.Material { weight: 100 }
"sub_concept" <parent:Ident> "." <name:Ident> "{" <first:Ident> ":" <first_val:Value> <rest:("," <Ident> ":" <Value>)*> ","? "}" => {
let mut fields = vec![Field {
name: first,
value: first_val,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,184 @@
use crate::syntax::{
ast::*,
lexer::Lexer,
FileParser,
};
fn parse(input: &str) -> File {
let lexer = Lexer::new(input);
let parser = FileParser::new();
parser.parse(lexer).unwrap()
}
// ===== Sub-concept dot notation tests =====
#[test]
fn test_sub_concept_enum_dot_notation() {
let input = "sub_concept Cup.Type { Small, Medium, Large }";
let file = parse(input);
assert_eq!(file.declarations.len(), 1);
match &file.declarations[0] {
| Declaration::SubConcept(sc) => {
assert_eq!(sc.name, "Type");
assert_eq!(sc.parent_concept, "Cup");
match &sc.kind {
| SubConceptKind::Enum { variants } => {
assert_eq!(variants, &["Small", "Medium", "Large"]);
},
| _ => panic!("Expected Enum sub-concept"),
}
},
| _ => panic!("Expected SubConcept declaration"),
}
}
#[test]
fn test_sub_concept_record_dot_notation() {
let input = "sub_concept Cup.Material { weight: 100, fragile: true }";
let file = parse(input);
assert_eq!(file.declarations.len(), 1);
match &file.declarations[0] {
| Declaration::SubConcept(sc) => {
assert_eq!(sc.name, "Material");
assert_eq!(sc.parent_concept, "Cup");
match &sc.kind {
| SubConceptKind::Record { fields } => {
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].name, "weight");
assert_eq!(fields[0].value, Value::Number(100));
assert_eq!(fields[1].name, "fragile");
assert_eq!(fields[1].value, Value::Boolean(true));
},
| _ => panic!("Expected Record sub-concept"),
}
},
| _ => panic!("Expected SubConcept declaration"),
}
}
#[test]
fn test_sub_concept_single_variant() {
let input = "sub_concept Animal.Sound { Bark }";
let file = parse(input);
assert_eq!(file.declarations.len(), 1);
match &file.declarations[0] {
| Declaration::SubConcept(sc) => {
assert_eq!(sc.name, "Sound");
assert_eq!(sc.parent_concept, "Animal");
match &sc.kind {
| SubConceptKind::Enum { variants } => {
assert_eq!(variants, &["Bark"]);
},
| _ => panic!("Expected Enum sub-concept"),
}
},
| _ => panic!("Expected SubConcept declaration"),
}
}
#[test]
fn test_sub_concept_record_single_field() {
let input = "sub_concept Drink.Temperature { celsius: 20 }";
let file = parse(input);
assert_eq!(file.declarations.len(), 1);
match &file.declarations[0] {
| Declaration::SubConcept(sc) => {
assert_eq!(sc.name, "Temperature");
assert_eq!(sc.parent_concept, "Drink");
match &sc.kind {
| SubConceptKind::Record { fields } => {
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].name, "celsius");
assert_eq!(fields[0].value, Value::Number(20));
},
| _ => panic!("Expected Record sub-concept"),
}
},
| _ => panic!("Expected SubConcept declaration"),
}
}
#[test]
fn test_sub_concept_with_trailing_comma() {
let input = "sub_concept Cup.Size { height: 10, width: 5, }";
let file = parse(input);
assert_eq!(file.declarations.len(), 1);
match &file.declarations[0] {
| Declaration::SubConcept(sc) => {
assert_eq!(sc.name, "Size");
assert_eq!(sc.parent_concept, "Cup");
match &sc.kind {
| SubConceptKind::Record { fields } => {
assert_eq!(fields.len(), 2);
},
| _ => panic!("Expected Record sub-concept"),
}
},
| _ => panic!("Expected SubConcept declaration"),
}
}
#[test]
fn test_multiple_sub_concepts_same_parent() {
let input = r#"
sub_concept Cup.Type { Mug, Teacup, Goblet }
sub_concept Cup.Material { Glass, Ceramic, Metal }
"#;
let file = parse(input);
assert_eq!(file.declarations.len(), 2);
match &file.declarations[0] {
| Declaration::SubConcept(sc) => {
assert_eq!(sc.parent_concept, "Cup");
assert_eq!(sc.name, "Type");
},
| _ => panic!("Expected SubConcept"),
}
match &file.declarations[1] {
| Declaration::SubConcept(sc) => {
assert_eq!(sc.parent_concept, "Cup");
assert_eq!(sc.name, "Material");
},
| _ => panic!("Expected SubConcept"),
}
}
#[test]
fn test_concept_with_sub_concepts() {
let input = r#"
concept Cup
sub_concept Cup.Type { Small, Medium, Large }
sub_concept Cup.Material { Glass, Ceramic, Metal }
"#;
let file = parse(input);
assert_eq!(file.declarations.len(), 3);
match &file.declarations[0] {
| Declaration::Concept(c) => assert_eq!(c.name, "Cup"),
| _ => panic!("Expected Concept"),
}
match &file.declarations[1] {
| Declaration::SubConcept(sc) => {
assert_eq!(sc.parent_concept, "Cup");
assert_eq!(sc.name, "Type");
},
| _ => panic!("Expected SubConcept"),
}
match &file.declarations[2] {
| Declaration::SubConcept(sc) => {
assert_eq!(sc.parent_concept, "Cup");
assert_eq!(sc.name, "Material");
},
| _ => panic!("Expected SubConcept"),
}
}