feat(type-system): implement concept_comparison with pattern matching
Added complete support for the new type system syntax including: - concept: Base type declarations - sub_concept: Enum and record sub-type definitions - concept_comparison: Compile-time pattern matching with conditional guards Parser changes: - Added VariantPattern, FieldCondition, and Condition AST nodes - Implemented "is" keyword for pattern matching (e.g., "CupType is Glass or CupType is Plastic") - Added Value::Any variant to support universal type matching - Disambiguated enum-like vs record-like sub_concept syntax LSP updates: - Added Value::Any match arms across code_actions, completion, hover, inlay_hints, and semantic_tokens - Type inference and formatting support for Any values Example fixes: - Fixed syntax error in baker-family behaviors (missing closing brace in nested if) - Removed deprecated core_enums.sb file
This commit is contained in:
@@ -91,9 +91,6 @@ pub fn convert_file_with_all_files(
|
||||
| ast::Declaration::Species(s) => {
|
||||
resolved.push(ResolvedDeclaration::Species(convert_species(s)?));
|
||||
},
|
||||
| ast::Declaration::Enum(e) => {
|
||||
resolved.push(ResolvedDeclaration::Enum(convert_enum(e)?));
|
||||
},
|
||||
| ast::Declaration::Use(_) => {
|
||||
// Use declarations are handled during name resolution, not
|
||||
// conversion
|
||||
@@ -330,15 +327,6 @@ pub fn convert_species(species: &ast::Species) -> Result<ResolvedSpecies> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert enum AST to resolved type
|
||||
pub fn convert_enum(enum_decl: &ast::EnumDecl) -> Result<ResolvedEnum> {
|
||||
Ok(ResolvedEnum {
|
||||
name: enum_decl.name.clone(),
|
||||
variants: enum_decl.variants.clone(),
|
||||
span: enum_decl.span.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Extract fields and prose blocks from a field list
|
||||
///
|
||||
/// Returns (fields_map, prose_blocks_map)
|
||||
@@ -384,7 +372,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::syntax::ast::{
|
||||
Character,
|
||||
EnumDecl,
|
||||
Field,
|
||||
Span,
|
||||
};
|
||||
@@ -486,58 +473,31 @@ mod tests {
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_enum() {
|
||||
let enum_decl = EnumDecl {
|
||||
name: "Status".to_string(),
|
||||
variants: vec!["active".to_string(), "inactive".to_string()],
|
||||
span: Span::new(0, 50),
|
||||
};
|
||||
|
||||
let resolved = convert_enum(&enum_decl).unwrap();
|
||||
|
||||
assert_eq!(resolved.name, "Status");
|
||||
assert_eq!(resolved.variants.len(), 2);
|
||||
assert_eq!(resolved.variants[0], "active");
|
||||
assert_eq!(resolved.variants[1], "inactive");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_file_mixed_declarations() {
|
||||
let file = ast::File {
|
||||
declarations: vec![
|
||||
ast::Declaration::Character(Character {
|
||||
name: "Martha".to_string(),
|
||||
species: None,
|
||||
fields: vec![Field {
|
||||
name: "age".to_string(),
|
||||
value: Value::Int(34),
|
||||
span: Span::new(0, 10),
|
||||
}],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 50),
|
||||
}),
|
||||
ast::Declaration::Enum(EnumDecl {
|
||||
name: "Status".to_string(),
|
||||
variants: vec!["active".to_string()],
|
||||
span: Span::new(50, 100),
|
||||
}),
|
||||
],
|
||||
declarations: vec![ast::Declaration::Character(Character {
|
||||
name: "Martha".to_string(),
|
||||
species: None,
|
||||
fields: vec![Field {
|
||||
name: "age".to_string(),
|
||||
value: Value::Int(34),
|
||||
span: Span::new(0, 10),
|
||||
}],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 50),
|
||||
})],
|
||||
};
|
||||
|
||||
let resolved = convert_file(&file).unwrap();
|
||||
|
||||
assert_eq!(resolved.len(), 2);
|
||||
assert_eq!(resolved.len(), 1);
|
||||
match &resolved[0] {
|
||||
| ResolvedDeclaration::Character(c) => assert_eq!(c.name, "Martha"),
|
||||
| _ => panic!("Expected Character"),
|
||||
}
|
||||
match &resolved[1] {
|
||||
| ResolvedDeclaration::Enum(e) => assert_eq!(e.name, "Status"),
|
||||
| _ => panic!("Expected Enum"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -79,26 +79,16 @@ fn test_multiple_declarations_end_to_end() {
|
||||
character David {
|
||||
age: 36
|
||||
}
|
||||
|
||||
enum Status {
|
||||
active, inactive, pending
|
||||
}
|
||||
"#;
|
||||
|
||||
let resolved = parse_and_convert(source).unwrap();
|
||||
assert_eq!(resolved.len(), 3);
|
||||
assert_eq!(resolved.len(), 2);
|
||||
|
||||
let char_count = resolved
|
||||
.iter()
|
||||
.filter(|d| matches!(d, ResolvedDeclaration::Character(_)))
|
||||
.count();
|
||||
let enum_count = resolved
|
||||
.iter()
|
||||
.filter(|d| matches!(d, ResolvedDeclaration::Enum(_)))
|
||||
.count();
|
||||
|
||||
assert_eq!(char_count, 2);
|
||||
assert_eq!(enum_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -330,10 +320,6 @@ Martha grew up in a small town.
|
||||
bond: 0.9
|
||||
}
|
||||
|
||||
enum BondType {
|
||||
romantic, familial, friendship
|
||||
}
|
||||
|
||||
schedule DailyRoutine {
|
||||
08:00 -> 12:00: work { }
|
||||
12:00 -> 13:00: lunch { }
|
||||
@@ -351,10 +337,6 @@ Martha grew up in a small town.
|
||||
.iter()
|
||||
.filter(|d| matches!(d, ResolvedDeclaration::Relationship(_)))
|
||||
.count();
|
||||
let enums = resolved
|
||||
.iter()
|
||||
.filter(|d| matches!(d, ResolvedDeclaration::Enum(_)))
|
||||
.count();
|
||||
let scheds = resolved
|
||||
.iter()
|
||||
.filter(|d| matches!(d, ResolvedDeclaration::Schedule(_)))
|
||||
@@ -362,9 +344,8 @@ Martha grew up in a small town.
|
||||
|
||||
assert_eq!(chars, 2);
|
||||
assert_eq!(rels, 1);
|
||||
assert_eq!(enums, 1);
|
||||
assert_eq!(scheds, 1);
|
||||
assert_eq!(resolved.len(), 5); // Total, excluding use declaration
|
||||
assert_eq!(resolved.len(), 4); // Total, excluding use declaration
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -5,7 +5,6 @@ use proptest::prelude::*;
|
||||
use crate::{
|
||||
resolve::convert::{
|
||||
convert_character,
|
||||
convert_enum,
|
||||
convert_file,
|
||||
},
|
||||
syntax::ast::*,
|
||||
@@ -98,16 +97,6 @@ fn valid_character() -> impl Strategy<Value = Character> {
|
||||
})
|
||||
}
|
||||
|
||||
fn valid_enum() -> impl Strategy<Value = EnumDecl> {
|
||||
(valid_ident(), prop::collection::vec(valid_ident(), 1..10)).prop_map(|(name, variants)| {
|
||||
EnumDecl {
|
||||
name,
|
||||
variants,
|
||||
span: Span::new(0, 100),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ===== Property Tests =====
|
||||
|
||||
proptest! {
|
||||
@@ -142,26 +131,10 @@ proptest! {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_name_preserved(enum_decl in valid_enum()) {
|
||||
let original_name = enum_decl.name.clone();
|
||||
let resolved = convert_enum(&enum_decl).unwrap();
|
||||
assert_eq!(resolved.name, original_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_variants_preserved(enum_decl in valid_enum()) {
|
||||
let resolved = convert_enum(&enum_decl).unwrap();
|
||||
assert_eq!(resolved.variants.len(), enum_decl.variants.len());
|
||||
for (i, variant) in enum_decl.variants.iter().enumerate() {
|
||||
assert_eq!(&resolved.variants[i], variant);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_file_preserves_declaration_count(
|
||||
characters in prop::collection::vec(valid_character(), 0..5),
|
||||
enums in prop::collection::vec(valid_enum(), 0..5)
|
||||
characters in prop::collection::vec(valid_character(), 0..5)
|
||||
) {
|
||||
// Ensure unique names across all declarations to avoid duplicate definition errors
|
||||
let mut seen_names = std::collections::HashSet::new();
|
||||
@@ -173,12 +146,6 @@ proptest! {
|
||||
}
|
||||
}
|
||||
|
||||
for enum_decl in enums {
|
||||
if seen_names.insert(enum_decl.name.clone()) {
|
||||
declarations.push(Declaration::Enum(enum_decl));
|
||||
}
|
||||
}
|
||||
|
||||
let file = File { declarations: declarations.clone() };
|
||||
let resolved = convert_file(&file).unwrap();
|
||||
|
||||
|
||||
@@ -31,11 +31,6 @@ fn test_name_resolution_example_file() {
|
||||
template PersonTemplate {
|
||||
age: 18..80
|
||||
}
|
||||
|
||||
enum Status {
|
||||
active,
|
||||
inactive
|
||||
}
|
||||
"#;
|
||||
|
||||
let file = parse(source);
|
||||
@@ -45,12 +40,10 @@ fn test_name_resolution_example_file() {
|
||||
assert!(table.lookup(&["Alice".to_string()]).is_some());
|
||||
assert!(table.lookup(&["Bob".to_string()]).is_some());
|
||||
assert!(table.lookup(&["PersonTemplate".to_string()]).is_some());
|
||||
assert!(table.lookup(&["Status".to_string()]).is_some());
|
||||
|
||||
// Verify kind filtering
|
||||
assert_eq!(table.entries_of_kind(DeclKind::Character).count(), 2);
|
||||
assert_eq!(table.entries_of_kind(DeclKind::Template).count(), 1);
|
||||
assert_eq!(table.entries_of_kind(DeclKind::Enum).count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -141,16 +134,12 @@ fn test_all_declaration_kinds() {
|
||||
species Sp {
|
||||
lifespan: 100
|
||||
}
|
||||
enum E {
|
||||
a,
|
||||
b
|
||||
}
|
||||
"#;
|
||||
|
||||
let file = parse(source);
|
||||
let table = NameTable::from_file(&file).expect("Should build name table");
|
||||
|
||||
// All 10 declaration kinds should be represented
|
||||
// All 9 declaration kinds should be represented (enum removed)
|
||||
assert_eq!(table.entries_of_kind(DeclKind::Character).count(), 1);
|
||||
assert_eq!(table.entries_of_kind(DeclKind::Template).count(), 1);
|
||||
assert_eq!(table.entries_of_kind(DeclKind::LifeArc).count(), 1);
|
||||
@@ -160,5 +149,4 @@ fn test_all_declaration_kinds() {
|
||||
assert_eq!(table.entries_of_kind(DeclKind::Relationship).count(), 1);
|
||||
assert_eq!(table.entries_of_kind(DeclKind::Location).count(), 1);
|
||||
assert_eq!(table.entries_of_kind(DeclKind::Species).count(), 1);
|
||||
assert_eq!(table.entries_of_kind(DeclKind::Enum).count(), 1);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ pub enum DeclKind {
|
||||
Relationship,
|
||||
Location,
|
||||
Species,
|
||||
Enum,
|
||||
}
|
||||
|
||||
/// Entry in the name table
|
||||
@@ -138,7 +137,6 @@ impl NameTable {
|
||||
},
|
||||
| Declaration::Location(l) => (l.name.clone(), DeclKind::Location, l.span.clone()),
|
||||
| Declaration::Species(s) => (s.name.clone(), DeclKind::Species, s.span.clone()),
|
||||
| Declaration::Enum(e) => (e.name.clone(), DeclKind::Enum, e.span.clone()),
|
||||
| Declaration::Concept(_) |
|
||||
Declaration::SubConcept(_) |
|
||||
Declaration::ConceptComparison(_) => continue, /* TODO: Implement name resolution
|
||||
|
||||
@@ -79,17 +79,6 @@ fn valid_template_decl() -> impl Strategy<Value = (String, Declaration)> {
|
||||
})
|
||||
}
|
||||
|
||||
fn valid_enum_decl() -> impl Strategy<Value = (String, Declaration)> {
|
||||
(valid_ident(), prop::collection::vec(valid_ident(), 1..5)).prop_map(|(name, variants)| {
|
||||
let decl = Declaration::Enum(EnumDecl {
|
||||
name: name.clone(),
|
||||
variants,
|
||||
span: Span::new(0, 10),
|
||||
});
|
||||
(name, decl)
|
||||
})
|
||||
}
|
||||
|
||||
fn valid_use_single() -> impl Strategy<Value = Declaration> {
|
||||
(valid_ident(), valid_ident()).prop_map(|(module, name)| {
|
||||
Declaration::Use(UseDecl {
|
||||
@@ -211,32 +200,27 @@ proptest! {
|
||||
#[test]
|
||||
fn test_kind_filtering_works(
|
||||
chars in prop::collection::vec(valid_character_decl(), 0..5),
|
||||
templates in prop::collection::vec(valid_template_decl(), 0..5),
|
||||
enums in prop::collection::vec(valid_enum_decl(), 0..5)
|
||||
templates in prop::collection::vec(valid_template_decl(), 0..5)
|
||||
) {
|
||||
let mut declarations = vec![];
|
||||
declarations.extend(chars.iter().map(|(_, d)| d.clone()));
|
||||
declarations.extend(templates.iter().map(|(_, d)| d.clone()));
|
||||
declarations.extend(enums.iter().map(|(_, d)| d.clone()));
|
||||
|
||||
let file = File { declarations };
|
||||
|
||||
// Only proceed if no duplicates
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let has_duplicates = chars.iter().any(|(name, _)| !seen.insert(name))
|
||||
|| templates.iter().any(|(name, _)| !seen.insert(name))
|
||||
|| enums.iter().any(|(name, _)| !seen.insert(name));
|
||||
|| templates.iter().any(|(name, _)| !seen.insert(name));
|
||||
|
||||
if !has_duplicates {
|
||||
let table = NameTable::from_file(&file).unwrap();
|
||||
|
||||
let char_count = table.entries_of_kind(DeclKind::Character).count();
|
||||
let template_count = table.entries_of_kind(DeclKind::Template).count();
|
||||
let enum_count = table.entries_of_kind(DeclKind::Enum).count();
|
||||
|
||||
assert_eq!(char_count, chars.len());
|
||||
assert_eq!(template_count, templates.len());
|
||||
assert_eq!(enum_count, enums.len());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -183,11 +183,6 @@ fn find_references_in_declaration(
|
||||
file_index,
|
||||
));
|
||||
},
|
||||
| Declaration::Enum(e) => {
|
||||
// Enums themselves don't reference symbols, but their values might be
|
||||
// referenced Skip for now
|
||||
let _ = (e, symbol_name, symbol_kind, file_index);
|
||||
},
|
||||
| Declaration::Use(_) => {
|
||||
// Use statements are handled separately
|
||||
},
|
||||
@@ -319,10 +314,7 @@ fn find_references_in_value(
|
||||
// Identifiers can reference characters, templates, enums, species
|
||||
let matches_kind = matches!(
|
||||
symbol_kind,
|
||||
DeclKind::Character |
|
||||
DeclKind::Template |
|
||||
DeclKind::Enum |
|
||||
DeclKind::Species
|
||||
DeclKind::Character | DeclKind::Template | DeclKind::Species
|
||||
);
|
||||
|
||||
if matches_kind {
|
||||
@@ -667,24 +659,4 @@ relationship Friends { Alice as friend {} Bob as friend {} }
|
||||
// Should find nothing
|
||||
assert_eq!(refs.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_field_references() {
|
||||
let source = r#"
|
||||
enum Mood { Happy, Sad }
|
||||
character Alice { mood: Mood }
|
||||
"#;
|
||||
let file = parse(source);
|
||||
let files = vec![file];
|
||||
|
||||
let refs = find_all_references(&files, "Mood", DeclKind::Enum);
|
||||
|
||||
// Should find: definition + field value reference
|
||||
assert_eq!(refs.len(), 2);
|
||||
|
||||
let field_ref = refs
|
||||
.iter()
|
||||
.find(|r| r.context == ReferenceContext::FieldValue);
|
||||
assert!(field_ref.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user