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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
1708
src/syntax/parser.rs
1708
src/syntax/parser.rs
File diff suppressed because it is too large
Load Diff
184
src/syntax/type_system_tests.rs
Normal file
184
src/syntax/type_system_tests.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user