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:
2026-02-14 09:28:20 +00:00
parent 6e3b35e68f
commit 25d59d6107
30 changed files with 8639 additions and 6536 deletions

View File

@@ -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]

View File

@@ -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]

View File

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

View File

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

View File

@@ -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

View File

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

View File

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