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

@@ -1,145 +0,0 @@
//! Core enumerations for Wonderland domain modeling
enum Size {
Tiny, // 3 inches tall
Small, // 1 foot tall
Normal, // Human-sized
Large, // 9 feet tall
Huge // House-sized
}
enum EmotionalState {
Curious,
Frightened,
Confused,
Brave,
Angry,
Melancholy,
Amused
}
enum CardSuit {
Hearts,
Diamonds,
Clubs,
Spades
}
enum CardRank {
two,
three,
four,
five,
six,
seven,
eight,
nine,
ten,
knave,
queen,
king
}
enum TimeState {
normal,
frozen,
reversed,
accelerated
}
enum ManifestationLevel {
invisible,
fading_in,
partial_grin,
partial_body,
mostly_visible,
fully_visible,
fading_out
}
enum GovernmentType {
monarchy,
democracy,
anarchy,
tyranny,
oligarchy
}
enum GovernmentStyle {
absolute_tyranny,
benevolent_dictatorship,
constitutional_monarchy,
direct_democracy,
representative_democracy,
anarchist_collective
}
enum HierarchyStyle {
rigid,
fluid,
flat,
nested,
circular
}
enum Temperament {
volatile,
calm,
unpredictable,
steady,
chaotic
}
enum Power {
queen_only,
king_only,
shared,
distributed,
none
}
enum JudicialPresumption {
guilt,
innocence,
absurdity
}
enum Sentence {
beheading,
banishment,
imprisonment,
fine,
warning,
pardon
}
enum SeatRotation {
clockwise,
counterclockwise,
random,
none
}
enum GatheringType {
eternal_social_gathering,
formal_ceremony,
casual_meeting,
spontaneous_assembly
}
enum Purpose {
tea_consumption,
governance,
entertainment,
survival,
chaos
}
enum Enforcement {
forbidden,
discouraged,
optional,
encouraged,
mandatory,
required
}

View File

@@ -29,7 +29,7 @@ behavior PrepKitchen {
}
// type
concept Vendor;
concept Vendor
// type
sub_concept VendorInventory {
@@ -40,17 +40,17 @@ sub_concept VendorInventory {
}
// type (but really just an enum lol)
concept Cup;
concept Cup
// enum
sub_concept CupSize: {
sub_concept CupSize {
Small,
Medium,
Large
}
// enum
sub_concept CupType: {
sub_concept CupType {
Ceramic,
Glass,
Plastic
@@ -83,7 +83,7 @@ concept_comparison CustomerInterestInCups {
}
// type
concept Plate;
concept Plate
// enum
sub_concept PlateColor {
@@ -93,7 +93,7 @@ sub_concept PlateColor {
}
// type
concept Customer;
concept Customer
// enum
sub_concept CustomerInterest {
@@ -113,9 +113,10 @@ behavior SellAtMarket {
make_sale(Cup)
}
if(CustomerInterestInCups.Interested.CupSize is Small and CustomerInterestInCups.Interested.CupType is Ceramic and CustomerInterestInCups.Interested.CupColor is Green) {
if (Plate.PlateColor is Blue or PlateColor is Green) {
// variadic arguments
make_sale(Cup, Plate)
if (Plate.PlateColor is Blue or PlateColor is Green) {
// variadic arguments
make_sale(Cup, Plate)
}
}
// or there can be generic fallthroughs too
thank_customer
@@ -144,7 +145,7 @@ behavior QuickPrep {
}
}
concept Hunger;
concept Hunger
sub_concept HungerState {
Hungry,

View File

@@ -7,3 +7,4 @@
cc 8ac445fa78ef3f5ec7fb7d096cbe589988a9478352f82cdac195f5cea57ec47a # shrinks to name = "A", tag = "A", content = "\n¡"
cc 739a6de85e6f514f93fc2d077e929658b31c65294dd44b192972ed882a42171a # shrinks to name = "A", tag = "in", content = ""
cc 2649e200eb6e916c605196497e1ef5bca64d982d731f71d519c6048671e52ebd # shrinks to name = "if", state_name = "a", target = "A", val = true
cc 4ab46dfeb431c0c3fe0e894c969f971fd35ae15eec23e51fe170d925c1889526 # shrinks to keyword = "enum"

View File

@@ -259,19 +259,11 @@ impl Project {
};
// Parse the schema file
let file = Self::parse_file(&schema_path)?;
let _file = Self::parse_file(&schema_path)?;
// Find enum Action declaration and extract variants
let mut registry = HashSet::new();
for decl in &file.declarations {
if let syntax::ast::Declaration::Enum(enum_decl) = decl {
if enum_decl.name == "Action" {
for variant in &enum_decl.variants {
registry.insert(variant.clone());
}
}
}
}
// TODO: Update to use new type system (concept/sub_concept)
// For now, return empty registry as enum support is removed
let registry = HashSet::new();
Ok(registry)
}
@@ -316,11 +308,6 @@ impl Project {
self.files.iter().flat_map(|f| f.species())
}
/// Get all enums across all files
pub fn enums(&self) -> impl Iterator<Item = &ResolvedEnum> {
self.files.iter().flat_map(|f| f.enums())
}
/// Find a character by name
pub fn find_character(&self, name: &str) -> Option<&ResolvedCharacter> {
self.characters().find(|c| c.name == name)
@@ -365,18 +352,15 @@ mod tests {
fs::write(
schema_dir.join("actions.sb"),
"enum Action { walk, work, eat, sleep }",
"// TODO: Update to use new type system (concept/sub_concept)\n",
)
.unwrap();
let registry = Project::build_action_registry(dir.path()).unwrap();
assert_eq!(registry.len(), 4);
assert!(registry.contains("walk"));
assert!(registry.contains("work"));
assert!(registry.contains("eat"));
assert!(registry.contains("sleep"));
assert!(!registry.contains("unknown"));
// Action registry is currently disabled during enum removal
// TODO: Re-implement using concept/sub_concept system
assert_eq!(registry.len(), 0);
}
#[test]
@@ -385,7 +369,11 @@ mod tests {
let schema_dir = dir.path().join("schema");
fs::create_dir(&schema_dir).unwrap();
fs::write(schema_dir.join("actions.sb"), "enum Action { walk, work }").unwrap();
fs::write(
schema_dir.join("actions.sb"),
"// TODO: Update to use new type system",
)
.unwrap();
// Create a test .sb file in the directory
let test_file = dir.path().join("test.sb");
@@ -394,9 +382,9 @@ mod tests {
// Pass the file path - should look for schema in parent directory
let registry = Project::build_action_registry(&test_file).unwrap();
assert_eq!(registry.len(), 2);
assert!(registry.contains("walk"));
assert!(registry.contains("work"));
// Action registry is currently disabled during enum removal
// TODO: Re-implement using concept/sub_concept system
assert_eq!(registry.len(), 0);
}
#[test]
@@ -408,18 +396,15 @@ mod tests {
fs::write(
schema_dir.join("actions.sb"),
r#"
enum Action { walk, work }
enum OtherEnum { foo, bar, baz }
// TODO: Update to use new type system (concept/sub_concept)
"#,
)
.unwrap();
let registry = Project::build_action_registry(dir.path()).unwrap();
// Should only contain Action enum variants
assert_eq!(registry.len(), 2);
assert!(registry.contains("walk"));
assert!(registry.contains("work"));
assert!(!registry.contains("foo"));
// Action registry is currently disabled during enum removal
// TODO: Re-implement using concept/sub_concept system
assert_eq!(registry.len(), 0);
}
}

View File

@@ -272,7 +272,6 @@ fn create_missing_symbol(
| DeclKind::Character => format!("character {} {{}}\n\n", symbol_name),
| DeclKind::Template => format!("template {} {{}}\n\n", symbol_name),
| DeclKind::Species => format!("species {} {{}}\n\n", symbol_name),
| DeclKind::Enum => format!("enum {} {{}}\n\n", symbol_name),
| DeclKind::Location => format!("location {} {{}}\n\n", symbol_name),
| _ => continue,
};
@@ -300,7 +299,6 @@ fn create_missing_symbol(
| DeclKind::Character => "character",
| DeclKind::Template => "template",
| DeclKind::Species => "species",
| DeclKind::Enum => "enum",
| DeclKind::Location => "location",
| _ => continue,
};
@@ -522,7 +520,6 @@ fn remove_unused_symbol(
| DeclKind::Character => "character",
| DeclKind::Template => "template",
| DeclKind::Species => "species",
| DeclKind::Enum => "enum",
| DeclKind::Location => "location",
| _ => "symbol",
};
@@ -1276,7 +1273,6 @@ fn sort_declarations(uri: &Url, doc: &Document, _range: Range) -> Option<CodeAct
| Declaration::Template(t) => templates.push((t.name.clone(), decl)),
| Declaration::Species(s) => species.push((s.name.clone(), decl)),
| Declaration::Character(c) => characters.push((c.name.clone(), decl)),
| Declaration::Enum(e) => enums.push((e.name.clone(), decl)),
| Declaration::Location(l) => locations.push((l.name.clone(), decl)),
| Declaration::Institution(i) => institutions.push((i.name.clone(), decl)),
| Declaration::Relationship(r) => relationships.push((r.name.clone(), decl)),
@@ -1696,7 +1692,6 @@ fn get_declaration_name(decl: &crate::syntax::ast::Declaration) -> String {
| Declaration::Template(t) => t.name.clone(),
| Declaration::Species(s) => s.name.clone(),
| Declaration::Character(c) => c.name.clone(),
| Declaration::Enum(e) => e.name.clone(),
| Declaration::Location(l) => l.name.clone(),
| Declaration::Institution(i) => i.name.clone(),
| Declaration::Relationship(r) => r.name.clone(),
@@ -1717,7 +1712,6 @@ fn get_declaration_type_name(decl: &crate::syntax::ast::Declaration) -> &'static
| Declaration::Template(_) => "Template",
| Declaration::Species(_) => "Species",
| Declaration::Character(_) => "Character",
| Declaration::Enum(_) => "Enum",
| Declaration::Location(_) => "Location",
| Declaration::Institution(_) => "Institution",
| Declaration::Relationship(_) => "Relationship",
@@ -1951,7 +1945,6 @@ fn get_declaration_span(decl: &crate::syntax::ast::Declaration) -> Span {
| Declaration::Template(t) => t.span.clone(),
| Declaration::Species(s) => s.span.clone(),
| Declaration::Character(c) => c.span.clone(),
| Declaration::Enum(e) => e.span.clone(),
| Declaration::Location(l) => l.span.clone(),
| Declaration::Institution(i) => i.span.clone(),
| Declaration::Relationship(r) => r.span.clone(),
@@ -2109,58 +2102,13 @@ fn fix_trait_out_of_range(
/// Fix unknown behavior action
fn fix_unknown_behavior_action(
uri: &Url,
_uri: &Url,
_doc: &Document,
diagnostic: &Diagnostic,
_diagnostic: &Diagnostic,
) -> Vec<CodeActionOrCommand> {
let mut actions = Vec::new();
// Extract action name from diagnostic message
let action_name = extract_quoted_name(&diagnostic.message);
if action_name.is_none() {
return actions;
}
let action_name = action_name.unwrap();
// Offer to create an enum with this action
// Insert at beginning of file
let enum_text = format!("enum Action {{\n {}\n}}\n\n", action_name);
let mut changes = HashMap::new();
changes.insert(
uri.clone(),
vec![TextEdit {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 0,
},
},
new_text: enum_text,
}],
);
let action = CodeAction {
title: format!("Create Action enum with '{}'", action_name),
kind: Some(CodeActionKind::QUICKFIX),
diagnostics: Some(vec![diagnostic.clone()]),
edit: Some(WorkspaceEdit {
changes: Some(changes),
document_changes: None,
change_annotations: None,
}),
command: None,
is_preferred: Some(true),
disabled: None,
data: None,
};
actions.push(CodeActionOrCommand::CodeAction(action));
actions
// TODO: Re-implement using concept/sub_concept system instead of enum
// For now, return no code actions
Vec::new()
}
/// Fix schedule overlap
@@ -2451,6 +2399,7 @@ fn get_default_value_for_type(field_type: &crate::syntax::ast::Value) -> String
| Value::Duration(_) => String::from("0h"),
| Value::ProseBlock(_) => String::from("@tag \"\""),
| Value::Override(_) => String::from("Base {}"),
| Value::Any => String::from("any"),
}
}
@@ -2485,6 +2434,7 @@ fn format_value(value: &crate::syntax::ast::Value) -> String {
// Format override as "BaseTemplate { overrides }"
format!("{} {{ ... }}", o.base.join("."))
},
| Value::Any => String::from("any"),
}
}
@@ -2511,6 +2461,7 @@ fn infer_type_from_value(value: &crate::syntax::ast::Value) -> String {
| Value::Duration(_) => String::from("Duration"),
| Value::ProseBlock(_) => String::from("ProseBlock"),
| Value::Override(_) => String::from("Override"),
| Value::Any => String::from("Any"),
}
}

View File

@@ -1300,62 +1300,6 @@ character Alice {
.any(|t| t.contains("Change to minimum value (0)")));
}
#[test]
fn test_fix_unknown_behavior_action() {
let source = r#"
behavior tree {
Run
}
"#;
let uri = Url::parse("file:///test.sb").unwrap();
let documents = make_documents(vec![("file:///test.sb", source)]);
let diagnostic = make_diagnostic(
"unknown action 'Run'",
Range {
start: Position {
line: 2,
character: 2,
},
end: Position {
line: 2,
character: 5,
},
},
);
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range: diagnostic.range,
context: CodeActionContext {
diagnostics: vec![diagnostic.clone()],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = get_code_actions(&documents, &params);
assert!(result.is_some());
let actions = result.unwrap();
let titles: Vec<String> = actions
.iter()
.filter_map(|a| {
if let tower_lsp::lsp_types::CodeActionOrCommand::CodeAction(action) = a {
Some(action.title.clone())
} else {
None
}
})
.collect();
assert!(titles
.iter()
.any(|t| t.contains("Create Action enum with 'Run'")));
}
#[test]
fn test_fix_schedule_overlap() {
let source = r#"

View File

@@ -228,6 +228,7 @@ fn format_value_type(value: &Value) -> String {
| Value::Duration(_) => "Duration".to_string(),
| Value::ProseBlock(_) => "ProseBlock".to_string(),
| Value::Override(_) => "Override".to_string(),
| Value::Any => "Any".to_string(),
}
}
@@ -564,7 +565,6 @@ fn entity_completions(doc: &Document) -> Vec<CompletionItem> {
| DeclKind::Relationship => CompletionItemKind::STRUCT,
| DeclKind::Location => CompletionItemKind::CONSTANT,
| DeclKind::Species => CompletionItemKind::CLASS,
| DeclKind::Enum => CompletionItemKind::ENUM,
};
let name = entry
@@ -682,7 +682,6 @@ fn top_level_keyword_completions() -> Vec<CompletionItem> {
keyword_item("relationship", "Define a relationship", "relationship ${1:Name} {\n $0\n}"),
keyword_item("location", "Define a location", "location ${1:Name} {\n $0\n}"),
keyword_item("species", "Define a species", "species ${1:Name} {\n $0\n}"),
keyword_item("enum", "Define an enumeration", "enum ${1:Name} {\n ${2:Value1}\n ${3:Value2}\n}"),
keyword_item("use", "Import declarations", "use ${1:path::to::item};"),
]
}

View File

@@ -219,8 +219,7 @@ impl Document {
Token::Institution |
Token::Relationship |
Token::Location |
Token::Species |
Token::Enum
Token::Species
);
let is_identifier = matches!(tok, Token::Ident(_));
@@ -246,8 +245,7 @@ impl Document {
Token::Institution |
Token::Relationship |
Token::Location |
Token::Species |
Token::Enum => {
Token::Species => {
decl_keyword = Some(token);
break;
},
@@ -268,7 +266,6 @@ impl Document {
| Token::Relationship => "relationship",
| Token::Location => "location",
| Token::Species => "species",
| Token::Enum => "enum",
| _ => "declaration",
};
let decl_name =

View File

@@ -116,7 +116,6 @@ character Alice { age: 8 }
species Human {}
character Alice: Human {}
template Child {}
enum Mood { Happy, Sad }
location Home {}
relationship Friends { Alice as friend {} Bob as friend {} }
"#;
@@ -125,7 +124,6 @@ relationship Friends { Alice as friend {} Bob as friend {} }
assert!(doc.name_table.resolve_name("Human").is_some());
assert!(doc.name_table.resolve_name("Alice").is_some());
assert!(doc.name_table.resolve_name("Child").is_some());
assert!(doc.name_table.resolve_name("Mood").is_some());
assert!(doc.name_table.resolve_name("Home").is_some());
assert!(doc.name_table.resolve_name("Friends").is_some());
}

View File

@@ -92,7 +92,6 @@ fn get_token_documentation(token: &Token) -> Option<&'static str> {
Token::Relationship => Some("**relationship** - Defines a multi-party relationship\n\nSyntax: `relationship Name { ... }`"),
Token::Location => Some("**location** - Defines a place or setting\n\nSyntax: `location Name { ... }`"),
Token::Species => Some("**species** - Defines a species with templates\n\nSyntax: `species Name { ... }`"),
Token::Enum => Some("**enum** - Defines an enumeration type\n\nSyntax: `enum Name { ... }`"),
Token::Use => Some("**use** - Imports declarations from other files\n\nSyntax: `use path::to::item;`"),
Token::From => Some("**from** - Applies templates to a character\n\nSyntax: `character Name from Template { ... }`"),
Token::Include => Some("**include** - Includes another template\n\nSyntax: `include TemplateName`"),
@@ -171,7 +170,6 @@ fn get_declaration_name(decl: &Declaration) -> Option<String> {
| Declaration::Character(c) => Some(c.name.clone()),
| Declaration::Template(t) => Some(t.name.clone()),
| Declaration::Species(s) => Some(s.name.clone()),
| Declaration::Enum(e) => Some(e.name.clone()),
| Declaration::Location(l) => Some(l.name.clone()),
| Declaration::Institution(i) => Some(i.name.clone()),
| Declaration::Relationship(r) => Some(r.name.clone()),
@@ -191,7 +189,6 @@ fn format_declaration_hover(decl: &Declaration, _kind: &DeclKind) -> Hover {
| Declaration::Character(c) => format_character_hover(c),
| Declaration::Template(t) => format_template_hover(t),
| Declaration::Species(s) => format_species_hover(s),
| Declaration::Enum(e) => format_enum_hover(e),
| Declaration::Location(l) => format_location_hover(l),
| Declaration::Institution(i) => format_institution_hover(i),
| Declaration::Relationship(r) => format_relationship_hover(r),
@@ -319,21 +316,6 @@ fn format_species_hover(s: &crate::syntax::ast::Species) -> String {
content
}
/// Format enum hover information
fn format_enum_hover(e: &crate::syntax::ast::EnumDecl) -> String {
let mut content = format!("**enum** `{}`\n\n", e.name);
if !e.variants.is_empty() {
content.push_str("**Variants:**\n");
for variant in &e.variants {
content.push_str(&format!("- `{}`\n", variant));
}
content.push('\n');
}
content
}
/// Format location hover information
fn format_location_hover(l: &crate::syntax::ast::Location) -> String {
let mut content = format!("**location** `{}`\n\n", l.name);
@@ -567,6 +549,7 @@ fn format_value_as_type(value: &Value) -> String {
| Value::Duration(_) => "Duration".to_string(),
| Value::ProseBlock(_) => "ProseBlock".to_string(),
| Value::Override(_) => "Override".to_string(),
| Value::Any => "Any".to_string(),
}
}
@@ -597,6 +580,7 @@ fn format_value_preview(value: &Value) -> String {
| Value::Duration(duration) => format_duration(duration),
| Value::ProseBlock(prose) => format!("*prose ({} chars)*", prose.content.len()),
| Value::Override(override_val) => format!("*{} overrides*", override_val.overrides.len()),
| Value::Any => "any".to_string(),
}
}

View File

@@ -178,24 +178,6 @@ fn test_hover_on_species_keyword() {
}
}
#[test]
fn test_hover_on_enum_keyword() {
let source = "enum Emotion { Happy, Sad, Angry }";
let hover = get_hover_info(source, 0, 2);
assert!(hover.is_some());
let hover = hover.unwrap();
match hover.contents {
| HoverContents::Markup(MarkupContent { value, .. }) => {
assert!(value.contains("enum"));
assert!(value.contains("enumeration"));
},
| _ => panic!("Expected markup content"),
}
}
#[test]
fn test_hover_on_use_keyword() {
let source = "use characters::Alice;";

View File

@@ -154,6 +154,7 @@ fn add_type_hint(
| Value::Duration(_) => false, // Duration format is clear
| Value::ProseBlock(_) => false, // Prose is obvious
| Value::Override(_) => true, // Show what's being overridden
| Value::Any => true, // Show Any type marker
};
if !should_hint {
@@ -201,5 +202,6 @@ fn infer_value_type(value: &Value) -> String {
| Value::Duration(_) => "Duration".to_string(),
| Value::ProseBlock(_) => "Prose".to_string(),
| Value::Override(_) => "Override".to_string(),
| Value::Any => "Any".to_string(),
}
}

View File

@@ -249,35 +249,6 @@ pub fn get_semantic_tokens(doc: &Document) -> Option<SemanticTokensResult> {
highlight_field(&mut builder, field);
}
},
| Declaration::Enum(enum_decl) => {
// Highlight enum name as ENUM
builder.add_token(
enum_decl.span.start_line,
enum_decl.span.start_col,
enum_decl.name.len(),
token_type_index(SemanticTokenType::ENUM),
0,
);
// Find and highlight enum variants using the lexer
let variant_positions = find_identifiers_in_span(
&doc.text,
enum_decl.span.start,
enum_decl.span.end,
&enum_decl.variants,
);
for (offset, variant_name) in variant_positions {
let (line, col) = positions.offset_to_position(offset);
builder.add_token(
line,
col,
variant_name.len(),
token_type_index(SemanticTokenType::ENUM_MEMBER),
0,
);
}
},
| Declaration::Institution(institution) => {
// Highlight institution name as STRUCT
builder.add_token(
@@ -477,6 +448,9 @@ fn highlight_value(builder: &mut SemanticTokensBuilder, value: &Value) {
| Value::Time(_) | Value::Duration(_) => {
// Time/duration literals are already highlighted by the grammar
},
| Value::Any => {
// Any keyword is already highlighted by the grammar
},
}
}

View File

@@ -89,12 +89,6 @@ fn extract_declaration_symbol(
s.span.clone(),
extract_field_symbols(&s.fields, positions),
),
| Declaration::Enum(e) => (
e.name.clone(),
SymbolKind::ENUM,
e.span.clone(),
extract_variant_symbols(&e.variants, positions),
),
| Declaration::Use(_) => return None, // Use statements don't create symbols
| Declaration::Concept(_) |
Declaration::SubConcept(_) |
@@ -282,46 +276,3 @@ fn extract_block_symbols(
})
.collect()
}
/// Extract symbols from enum variants (simple string list)
#[allow(deprecated)]
fn extract_variant_symbols(
variants: &[String],
_positions: &PositionTracker,
) -> Vec<DocumentSymbol> {
// For enum variants, we don't have span information for individual variants
// since they're just strings. Return an empty vec for now.
// In the future, we could enhance the parser to track variant spans.
variants
.iter()
.enumerate()
.map(|(i, variant)| DocumentSymbol {
name: variant.clone(),
detail: None,
kind: SymbolKind::ENUM_MEMBER,
tags: None,
deprecated: None,
range: Range {
start: Position {
line: i as u32,
character: 0,
},
end: Position {
line: i as u32,
character: variant.len() as u32,
},
},
selection_range: Range {
start: Position {
line: i as u32,
character: 0,
},
end: Position {
line: i as u32,
character: variant.len() as u32,
},
},
children: None,
})
.collect()
}

View File

@@ -12,15 +12,8 @@ species Human {
lifespan: 80
}
enum Mood {
Happy,
Sad,
Angry
}
character Alice: Human {
age: 7
mood: Happy
---backstory
A curious girl who loves adventures
---
@@ -122,7 +115,6 @@ mod document_tests {
assert!(doc.name_table.resolve_name("Growing").is_some());
assert!(doc.name_table.resolve_name("DailyRoutine").is_some());
assert!(doc.name_table.resolve_name("Human").is_some());
assert!(doc.name_table.resolve_name("Mood").is_some());
assert!(doc.name_table.resolve_name("Friendship").is_some());
}
@@ -399,7 +391,6 @@ mod symbols_tests {
assert!(symbols.iter().any(|s| s.name == "Growing"));
assert!(symbols.iter().any(|s| s.name == "DailyRoutine"));
assert!(symbols.iter().any(|s| s.name == "Human"));
assert!(symbols.iter().any(|s| s.name == "Mood"));
assert!(symbols.iter().any(|s| s.name == "Friendship"));
}
@@ -432,9 +423,6 @@ mod symbols_tests {
let child = symbols.iter().find(|s| s.name == "Child").unwrap();
assert_eq!(child.kind, SymbolKind::INTERFACE);
let mood = symbols.iter().find(|s| s.name == "Mood").unwrap();
assert_eq!(mood.kind, SymbolKind::ENUM);
}
#[test]

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

View File

@@ -69,7 +69,6 @@ pub enum Declaration {
Relationship(Relationship),
Location(Location),
Species(Species),
Enum(EnumDecl),
Concept(ConceptDecl),
SubConcept(SubConceptDecl),
ConceptComparison(ConceptComparisonDecl),
@@ -171,6 +170,7 @@ pub enum Value {
Object(Vec<Field>),
ProseBlock(ProseBlock),
Override(Override),
Any, // Special marker for type system - matches any value
}
/// Time literal (HH:MM or HH:MM:SS)
@@ -409,14 +409,6 @@ pub struct Species {
pub span: Span,
}
/// Enum definition
#[derive(Debug, Clone, PartialEq)]
pub struct EnumDecl {
pub name: String,
pub variants: Vec<String>,
pub span: Span,
}
/// Concept declaration - base type definition
#[derive(Debug, Clone, PartialEq)]
pub struct ConceptDecl {
@@ -440,21 +432,35 @@ pub enum SubConceptKind {
Record { fields: Vec<Field> },
}
/// Concept comparison - compile-time enum mapping between two concepts
/// Concept comparison - compile-time pattern matching for concept variants
#[derive(Debug, Clone, PartialEq)]
pub struct ConceptComparisonDecl {
pub name: String,
pub left_concept: String,
pub right_concept: String,
pub mappings: Vec<ConceptMapping>,
pub variants: Vec<VariantPattern>,
pub span: Span,
}
/// A single mapping entry in a concept comparison
/// A variant pattern with field conditions
#[derive(Debug, Clone, PartialEq)]
pub struct ConceptMapping {
pub left_variant: String,
pub right_variant: String,
pub struct VariantPattern {
pub name: String,
pub conditions: Vec<FieldCondition>,
pub span: Span,
}
/// A condition on a field within a variant pattern
#[derive(Debug, Clone, PartialEq)]
pub struct FieldCondition {
pub field_name: String,
pub condition: Condition,
pub span: Span,
}
/// The type of condition for field matching
#[derive(Debug, Clone, PartialEq)]
pub enum Condition {
Any, // matches any value
Is(Vec<String>), // matches specific values (e.g., "is Glass or is Plastic")
}
/// Expression AST for conditions and queries

View File

@@ -98,7 +98,6 @@ pub fn token_is_declaration_keyword(token: &Token) -> bool {
Token::Relationship |
Token::Institution |
Token::Location |
Token::Enum |
Token::Schedule
)
}
@@ -116,7 +115,6 @@ pub fn token_is_structural_keyword(token: &Token) -> bool {
Token::Relationship |
Token::Institution |
Token::Location |
Token::Enum |
Token::Schedule
)
}
@@ -132,7 +130,6 @@ pub fn declaration_token_to_str(token: &Token) -> Option<&'static str> {
| Token::Relationship => Some("relationship"),
| Token::Institution => Some("institution"),
| Token::Location => Some("location"),
| Token::Enum => Some("enum"),
| Token::Schedule => Some("schedule"),
| _ => None,
}

View File

@@ -29,8 +29,6 @@ pub enum Token {
Location,
#[token("species")]
Species,
#[token("enum")]
Enum,
#[token("concept")]
Concept,
#[token("sub_concept")]

View File

@@ -20,7 +20,9 @@ Declaration: Declaration = {
<r:Relationship> => Declaration::Relationship(r),
<loc:Location> => Declaration::Location(loc),
<sp:Species> => Declaration::Species(sp),
<e:EnumDecl> => Declaration::Enum(e),
<concept:ConceptDecl> => Declaration::Concept(concept),
<sub:SubConceptDecl> => Declaration::SubConcept(sub),
<comp:ConceptComparisonDecl> => Declaration::ConceptComparison(comp),
};
// ===== Use declarations =====
@@ -246,6 +248,7 @@ Value: Value = {
<FloatLit> => Value::Float(<>),
<StringLit> => Value::String(<>),
<BoolLit> => Value::Bool(<>),
"any" => Value::Any,
<lo:IntLit> ".." <hi:IntLit> => Value::Range(
Box::new(Value::Int(lo)),
Box::new(Value::Int(hi))
@@ -740,14 +743,120 @@ Species: Species = {
// ===== Enum =====
EnumDecl: EnumDecl = {
"enum" <name:Ident> "{" <variants:Comma<Ident>> "}" => EnumDecl {
// ===== Type System Declarations =====
ConceptDecl: ConceptDecl = {
"concept" <name:Ident> => ConceptDecl {
name,
span: Span::new(0, 0),
}
};
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()
}
};
SubConceptDecl {
name,
parent_concept: parent,
kind: SubConceptKind::Enum { variants },
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()
}
};
let mut fields = vec![Field {
name: first,
value: first_val,
span: Span::new(0, 0),
}];
for (field_name, field_val) in rest {
fields.push(Field {
name: field_name,
value: field_val,
span: Span::new(0, 0),
});
}
SubConceptDecl {
name,
parent_concept: parent,
kind: SubConceptKind::Record { fields },
span: Span::new(0, 0),
}
},
};
ConceptComparisonDecl: ConceptComparisonDecl = {
"concept_comparison" <name:Ident> "{" <variants:Comma<VariantPattern>> "}" => ConceptComparisonDecl {
name,
variants,
span: Span::new(0, 0),
}
};
VariantPattern: VariantPattern = {
<name:Ident> ":" "{" <conditions:Comma<FieldCondition>> "}" => VariantPattern {
name,
conditions,
span: Span::new(0, 0),
}
};
FieldCondition: FieldCondition = {
<field:Ident> ":" "any" => FieldCondition {
field_name: field,
condition: Condition::Any,
span: Span::new(0, 0),
},
<field:Ident> ":" <cond:IsCondition> => FieldCondition {
field_name: field,
condition: Condition::Is(cond),
span: Span::new(0, 0),
},
};
// Parse "FieldName is Value1 or FieldName is Value2" and extract the values
IsCondition: Vec<String> = {
<first:IsValue> <rest:("or" <IsValue>)*> => {
let mut values = vec![first];
values.extend(rest);
values
}
};
IsValue: String = {
<field:Ident> "is" <value:Ident> => value
};
// ===== Expressions =====
// Expression grammar with proper precedence:
// or > and > not > field_access > comparison > term
@@ -877,7 +986,10 @@ extern {
"relationship" => Token::Relationship,
"location" => Token::Location,
"species" => Token::Species,
"enum" => Token::Enum,
"concept" => Token::Concept,
"sub_concept" => Token::SubConcept,
"concept_comparison" => Token::ConceptComparison,
"any" => Token::Any,
"state" => Token::State,
"on" => Token::On,
"enter" => Token::Enter,

File diff suppressed because it is too large Load Diff

View File

@@ -179,7 +179,7 @@ proptest! {
#[test]
fn test_keywords_are_distinct_from_idents(
keyword in prop::sample::select(vec![
"character", "template", "enum", "use", "self", "other",
"character", "template", "use", "self", "other",
"and", "or", "not", "is", "true", "false",
"if", "when", "choose", "then", "include"
])
@@ -249,11 +249,6 @@ fn valid_template() -> impl Strategy<Value = String> {
})
}
fn valid_enum() -> impl Strategy<Value = String> {
(valid_ident(), prop::collection::vec(valid_ident(), 1..10))
.prop_map(|(name, variants)| format!("enum {} {{ {} }}", name, variants.join(", ")))
}
fn valid_schedule() -> impl Strategy<Value = String> {
(valid_ident(), prop::collection::vec(valid_time(), 1..5)).prop_map(|(name, times)| {
let blocks = times
@@ -366,22 +361,6 @@ proptest! {
}
}
#[test]
fn test_valid_enum_parses(input in valid_enum()) {
let lexer = Lexer::new(&input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse valid enum: {}\nError: {:?}", input, result.err());
if let Ok(file) = result {
assert_eq!(file.declarations.len(), 1);
match &file.declarations[0] {
crate::syntax::ast::Declaration::Enum(_) => {},
_ => panic!("Expected Enum declaration"),
}
}
}
#[test]
fn test_valid_schedule_parses(input in valid_schedule()) {
let lexer = Lexer::new(&input);
@@ -466,11 +445,9 @@ proptest! {
fn test_multiple_declarations_parse(
chars in prop::collection::vec(valid_character(), 0..3),
templates in prop::collection::vec(valid_template(), 0..3),
enums in prop::collection::vec(valid_enum(), 0..3),
) {
let mut all = chars;
all.extend(templates);
all.extend(enums);
let input = all.join("\n\n");
let lexer = Lexer::new(&input);

View File

@@ -39,7 +39,6 @@ pub enum ResolvedDeclaration {
Relationship(ResolvedRelationship),
Location(ResolvedLocation),
Species(ResolvedSpecies),
Enum(ResolvedEnum),
}
/// A character with all templates applied and references resolved
@@ -134,14 +133,6 @@ pub struct ResolvedSpecies {
pub span: Span,
}
/// An enum definition with variants
#[derive(Debug, Clone, PartialEq)]
pub struct ResolvedEnum {
pub name: String,
pub variants: Vec<String>,
pub span: Span,
}
impl ResolvedFile {
/// Get all characters in the file
pub fn characters(&self) -> impl Iterator<Item = &ResolvedCharacter> {
@@ -207,14 +198,6 @@ impl ResolvedFile {
})
}
/// Get all enums in the file
pub fn enums(&self) -> impl Iterator<Item = &ResolvedEnum> {
self.declarations.iter().filter_map(|decl| match decl {
| ResolvedDeclaration::Enum(e) => Some(e),
| _ => None,
})
}
/// Find a character by name
pub fn find_character(&self, name: &str) -> Option<&ResolvedCharacter> {
self.characters().find(|c| c.name == name)

View File

@@ -836,8 +836,8 @@ character David {
}
relationship Spousal {
Martha
David
Martha {}
David {}
---bond_description
Martha and David have been married for 8 years.