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:
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
55
src/lib.rs
55
src/lib.rs
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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, ¶ms);
|
||||
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#"
|
||||
|
||||
@@ -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};"),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;";
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -29,8 +29,6 @@ pub enum Token {
|
||||
Location,
|
||||
#[token("species")]
|
||||
Species,
|
||||
#[token("enum")]
|
||||
Enum,
|
||||
#[token("concept")]
|
||||
Concept,
|
||||
#[token("sub_concept")]
|
||||
|
||||
@@ -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,
|
||||
|
||||
14284
src/syntax/parser.rs
14284
src/syntax/parser.rs
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
17
src/types.rs
17
src/types.rs
@@ -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)
|
||||
|
||||
@@ -836,8 +836,8 @@ character David {
|
||||
}
|
||||
|
||||
relationship Spousal {
|
||||
Martha
|
||||
David
|
||||
Martha {}
|
||||
David {}
|
||||
|
||||
---bond_description
|
||||
Martha and David have been married for 8 years.
|
||||
|
||||
Reference in New Issue
Block a user