release: Storybook v0.2.0 - Major syntax and features update
BREAKING CHANGES: - Relationship syntax now requires blocks for all participants - Removed self/other perspective blocks from relationships - Replaced 'guard' keyword with 'if' for behavior tree decorators Language Features: - Add tree-sitter grammar with improved if/condition disambiguation - Add comprehensive tutorial and reference documentation - Add SBIR v0.2.0 binary format specification - Add resource linking system for behaviors and schedules - Add year-long schedule patterns (day, season, recurrence) - Add behavior tree enhancements (named nodes, decorators) Documentation: - Complete tutorial series (9 chapters) with baker family examples - Complete reference documentation for all language features - SBIR v0.2.0 specification with binary format details - Added locations and institutions documentation Examples: - Convert all examples to baker family scenario - Add comprehensive working examples Tooling: - Zed extension with LSP integration - Tree-sitter grammar for syntax highlighting - Build scripts and development tools Version Updates: - Main package: 0.1.0 → 0.2.0 - Tree-sitter grammar: 0.1.0 → 0.2.0 - Zed extension: 0.1.0 → 0.2.0 - Storybook editor: 0.1.0 → 0.2.0
This commit is contained in:
@@ -113,6 +113,8 @@ pub fn convert_character(character: &ast::Character) -> Result<ResolvedCharacter
|
||||
species: None,
|
||||
fields,
|
||||
prose_blocks,
|
||||
uses_behaviors: character.uses_behaviors.clone(),
|
||||
uses_schedule: character.uses_schedule.clone(),
|
||||
span: character.span.clone(),
|
||||
})
|
||||
}
|
||||
@@ -124,6 +126,7 @@ pub fn convert_character(character: &ast::Character) -> Result<ResolvedCharacter
|
||||
/// 2. Recursively resolving template includes
|
||||
/// 3. Validating strict mode requirements
|
||||
/// 4. Applying character's own fields on top
|
||||
/// 5. Merging behavior and schedule links from templates
|
||||
pub fn convert_character_with_templates(
|
||||
character: &ast::Character,
|
||||
all_files: &[ast::File],
|
||||
@@ -136,6 +139,45 @@ pub fn convert_character_with_templates(
|
||||
character.fields.clone()
|
||||
};
|
||||
|
||||
// Collect behavior and schedule links from templates
|
||||
let (merged_behaviors, merged_schedules) = if let Some(template_names) = &character.template {
|
||||
let template_behaviors = Vec::new();
|
||||
let template_schedules = Vec::new();
|
||||
|
||||
for template_name in template_names {
|
||||
// Look up template
|
||||
let entry = name_table
|
||||
.lookup(std::slice::from_ref(template_name))
|
||||
.ok_or_else(|| ResolveError::NameNotFound {
|
||||
name: template_name.clone(),
|
||||
suggestion: name_table.find_suggestion(template_name),
|
||||
})?;
|
||||
|
||||
// Get template declaration
|
||||
if let ast::Declaration::Template(_template) =
|
||||
&all_files[entry.file_index].declarations[entry.decl_index]
|
||||
{
|
||||
// Templates don't have uses_behaviors/uses_schedule yet, but
|
||||
// they will For now, just pass empty vecs
|
||||
// TODO: Add template resource linking support
|
||||
}
|
||||
}
|
||||
|
||||
// Merge using merge functions from merge.rs
|
||||
let merged_b =
|
||||
merge::merge_behavior_links(character.uses_behaviors.clone(), template_behaviors);
|
||||
let merged_s =
|
||||
merge::merge_schedule_links(character.uses_schedule.clone(), template_schedules);
|
||||
|
||||
(merged_b, merged_s)
|
||||
} else {
|
||||
// No templates, just use character's own links
|
||||
(
|
||||
character.uses_behaviors.clone(),
|
||||
character.uses_schedule.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
// Extract fields and prose blocks from merged result
|
||||
let (fields, prose_blocks) = extract_fields_and_prose(&merged_fields)?;
|
||||
|
||||
@@ -144,6 +186,8 @@ pub fn convert_character_with_templates(
|
||||
species: None,
|
||||
fields,
|
||||
prose_blocks,
|
||||
uses_behaviors: merged_behaviors,
|
||||
uses_schedule: merged_schedules,
|
||||
span: character.span.clone(),
|
||||
})
|
||||
}
|
||||
@@ -358,6 +402,8 @@ mod tests {
|
||||
},
|
||||
],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 50),
|
||||
};
|
||||
|
||||
@@ -394,6 +440,8 @@ mod tests {
|
||||
},
|
||||
],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 100),
|
||||
};
|
||||
|
||||
@@ -424,6 +472,8 @@ mod tests {
|
||||
},
|
||||
],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 50),
|
||||
};
|
||||
|
||||
@@ -460,6 +510,8 @@ mod tests {
|
||||
span: Span::new(0, 10),
|
||||
}],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 50),
|
||||
}),
|
||||
ast::Declaration::Enum(EnumDecl {
|
||||
@@ -497,6 +549,8 @@ mod tests {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(10, 50),
|
||||
}),
|
||||
@@ -571,6 +625,8 @@ mod tests {
|
||||
}],
|
||||
strict: false,
|
||||
includes: vec![],
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 50),
|
||||
};
|
||||
|
||||
@@ -583,6 +639,8 @@ mod tests {
|
||||
span: Span::new(0, 10),
|
||||
}],
|
||||
template: Some(vec!["Person".to_string()]),
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 100),
|
||||
};
|
||||
|
||||
@@ -621,6 +679,8 @@ mod tests {
|
||||
}],
|
||||
strict: false,
|
||||
includes: vec![],
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 50),
|
||||
};
|
||||
|
||||
@@ -633,6 +693,8 @@ mod tests {
|
||||
}],
|
||||
strict: false,
|
||||
includes: vec![],
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 50),
|
||||
};
|
||||
|
||||
@@ -652,6 +714,8 @@ mod tests {
|
||||
},
|
||||
],
|
||||
template: Some(vec!["Physical".to_string(), "Mental".to_string()]),
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 100),
|
||||
};
|
||||
|
||||
@@ -688,6 +752,8 @@ mod tests {
|
||||
}],
|
||||
strict: false,
|
||||
includes: vec![],
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 50),
|
||||
};
|
||||
|
||||
@@ -700,6 +766,8 @@ mod tests {
|
||||
}],
|
||||
strict: false,
|
||||
includes: vec!["Human".to_string()],
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 50),
|
||||
};
|
||||
|
||||
@@ -736,6 +804,8 @@ mod tests {
|
||||
span: Span::new(0, 10),
|
||||
}],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 50),
|
||||
};
|
||||
|
||||
@@ -763,6 +833,8 @@ mod tests {
|
||||
}],
|
||||
strict: true,
|
||||
includes: vec![],
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 50),
|
||||
};
|
||||
|
||||
@@ -771,6 +843,8 @@ mod tests {
|
||||
species: None,
|
||||
fields: vec![], // No fields - inherits range from template
|
||||
template: Some(vec!["Person".to_string()]),
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 100),
|
||||
};
|
||||
|
||||
|
||||
@@ -105,8 +105,8 @@ fn test_multiple_declarations_end_to_end() {
|
||||
fn test_relationship_end_to_end() {
|
||||
let source = r#"
|
||||
relationship Spousal {
|
||||
Martha
|
||||
David
|
||||
Martha { }
|
||||
David { }
|
||||
bond: 0.9
|
||||
}
|
||||
"#;
|
||||
@@ -157,7 +157,7 @@ fn test_life_arc_end_to_end() {
|
||||
fn test_behavior_tree_end_to_end() {
|
||||
let source = r#"
|
||||
behavior WorkAtBakery {
|
||||
> {
|
||||
then {
|
||||
walk
|
||||
work(duration: 8h)
|
||||
rest
|
||||
@@ -172,7 +172,7 @@ fn test_behavior_tree_end_to_end() {
|
||||
| ResolvedDeclaration::Behavior(b) => {
|
||||
assert_eq!(b.name, "WorkAtBakery");
|
||||
// Root should be a Sequence node
|
||||
assert!(matches!(b.root, BehaviorNode::Sequence(_)));
|
||||
assert!(matches!(b.root, BehaviorNode::Sequence { .. }));
|
||||
},
|
||||
| _ => panic!("Expected Behavior"),
|
||||
}
|
||||
@@ -325,8 +325,8 @@ Martha grew up in a small town.
|
||||
}
|
||||
|
||||
relationship Spousal {
|
||||
Martha
|
||||
David
|
||||
Martha { }
|
||||
David { }
|
||||
bond: 0.9
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,8 @@ fn valid_character() -> impl Strategy<Value = Character> {
|
||||
species: None,
|
||||
fields,
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 100),
|
||||
})
|
||||
}
|
||||
@@ -207,6 +209,8 @@ proptest! {
|
||||
},
|
||||
],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 50),
|
||||
};
|
||||
|
||||
@@ -243,6 +247,8 @@ proptest! {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(0, 10),
|
||||
};
|
||||
@@ -291,6 +297,8 @@ proptest! {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 100),
|
||||
}));
|
||||
}
|
||||
@@ -341,6 +349,8 @@ mod edge_cases {
|
||||
},
|
||||
],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 50),
|
||||
};
|
||||
|
||||
@@ -368,6 +378,8 @@ mod edge_cases {
|
||||
span: Span::new(0, 10),
|
||||
}],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 50),
|
||||
};
|
||||
|
||||
|
||||
@@ -132,8 +132,8 @@ fn test_all_declaration_kinds() {
|
||||
name: "Test"
|
||||
}
|
||||
relationship R {
|
||||
C
|
||||
C
|
||||
C { }
|
||||
C { }
|
||||
}
|
||||
location Loc {
|
||||
name: "Place"
|
||||
|
||||
@@ -39,8 +39,6 @@ impl RelationshipKey {
|
||||
#[derive(Debug, Clone)]
|
||||
struct RelationshipDecl {
|
||||
relationship: Relationship,
|
||||
/// Which participant is "self" (index into participants)
|
||||
self_index: Option<usize>,
|
||||
}
|
||||
|
||||
/// Resolved bidirectional relationship
|
||||
@@ -57,10 +55,8 @@ pub struct ResolvedRelationship {
|
||||
pub struct ParticipantFields {
|
||||
pub participant_name: Vec<String>,
|
||||
pub role: Option<String>,
|
||||
/// Fields from this participant's "self" block
|
||||
pub self_fields: Vec<Field>,
|
||||
/// Fields from this participant's "other" block (about other participants)
|
||||
pub other_fields: Vec<Field>,
|
||||
/// Fields from this participant's block
|
||||
pub fields: Vec<Field>,
|
||||
}
|
||||
|
||||
/// Resolve bidirectional relationships in a file
|
||||
@@ -76,18 +72,11 @@ pub fn resolve_relationships(file: &File) -> Result<Vec<ResolvedRelationship>> {
|
||||
|
||||
let key = RelationshipKey::new(participant_names, rel.name.clone());
|
||||
|
||||
// Determine which participant is "self" based on self/other blocks
|
||||
let self_index = rel
|
||||
.participants
|
||||
.iter()
|
||||
.position(|p| p.self_block.is_some() || p.other_block.is_some());
|
||||
|
||||
relationship_groups
|
||||
.entry(key)
|
||||
.or_default()
|
||||
.push(RelationshipDecl {
|
||||
relationship: rel.clone(),
|
||||
self_index,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -122,43 +111,31 @@ fn merge_relationship_declarations(
|
||||
.map(|p| ParticipantFields {
|
||||
participant_name: p.name.clone(),
|
||||
role: p.role.clone(),
|
||||
self_fields: p.self_block.clone().unwrap_or_default(),
|
||||
other_fields: p.other_block.clone().unwrap_or_default(),
|
||||
fields: p.fields.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Merge shared fields (fields outside participant blocks)
|
||||
let mut merged_fields = base.fields.clone();
|
||||
|
||||
// Merge additional declarations
|
||||
for decl in decls.iter().skip(1) {
|
||||
// If this declaration specifies a different participant as "self",
|
||||
// merge their self/other blocks appropriately
|
||||
if let Some(self_idx) = decl.self_index {
|
||||
let participant_name = &decl.relationship.participants[self_idx].name;
|
||||
|
||||
// Merge participant fields
|
||||
for participant in &decl.relationship.participants {
|
||||
// Find this participant in our merged list
|
||||
if let Some(idx) = participant_fields
|
||||
if let Some(pf_idx) = participant_fields
|
||||
.iter()
|
||||
.position(|pf| &pf.participant_name == participant_name)
|
||||
.position(|pf| pf.participant_name == participant.name)
|
||||
{
|
||||
// Merge self blocks
|
||||
let self_block = decl.relationship.participants[self_idx]
|
||||
.self_block
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
merge_fields(&mut participant_fields[idx].self_fields, self_block)?;
|
||||
|
||||
// Merge other blocks
|
||||
let other_block = decl.relationship.participants[self_idx]
|
||||
.other_block
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
merge_fields(&mut participant_fields[idx].other_fields, other_block)?;
|
||||
// Merge fields for this participant
|
||||
merge_fields(
|
||||
&mut participant_fields[pf_idx].fields,
|
||||
participant.fields.clone(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge shared fields (fields outside self/other blocks)
|
||||
let mut merged_fields = base.fields.clone();
|
||||
for decl in decls.iter().skip(1) {
|
||||
// Merge shared relationship fields
|
||||
merge_fields(&mut merged_fields, decl.relationship.fields.clone())?;
|
||||
}
|
||||
|
||||
@@ -209,8 +186,7 @@ mod tests {
|
||||
Participant {
|
||||
name: vec![name.to_string()],
|
||||
role: role.map(|s| s.to_string()),
|
||||
self_block: None,
|
||||
other_block: None,
|
||||
fields: vec![],
|
||||
span: Span::new(0, 10),
|
||||
}
|
||||
}
|
||||
@@ -257,14 +233,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bidirectional_relationship_merge() {
|
||||
fn test_relationship_merge() {
|
||||
let mut martha_participant = make_participant("Martha", Some("spouse"));
|
||||
martha_participant.self_block = Some(vec![make_field("bond", 90)]);
|
||||
martha_participant.other_block = Some(vec![make_field("trust", 85)]);
|
||||
martha_participant.fields = vec![make_field("commitment", 90)];
|
||||
|
||||
let mut david_participant = make_participant("David", Some("spouse"));
|
||||
david_participant.self_block = Some(vec![make_field("bond", 90)]);
|
||||
david_participant.other_block = Some(vec![make_field("trust", 85)]);
|
||||
david_participant.fields = vec![make_field("trust", 85)];
|
||||
|
||||
let file = File {
|
||||
declarations: vec![
|
||||
@@ -297,10 +271,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_conflicting_field_values() {
|
||||
let mut p1 = make_participant("Alice", None);
|
||||
p1.self_block = Some(vec![make_field("bond", 80)]);
|
||||
p1.fields = vec![make_field("bond", 80)];
|
||||
|
||||
let mut p2 = make_participant("Alice", None);
|
||||
p2.self_block = Some(vec![make_field("bond", 90)]); // Different value
|
||||
p2.fields = vec![make_field("bond", 90)]; // Different value
|
||||
|
||||
let file = File {
|
||||
declarations: vec![
|
||||
|
||||
@@ -73,26 +73,21 @@ fn valid_participant(name: String) -> impl Strategy<Value = Participant> {
|
||||
prop::option::of(valid_ident()).prop_map(move |role| Participant {
|
||||
name: vec![name.clone()],
|
||||
role,
|
||||
self_block: None,
|
||||
other_block: None,
|
||||
fields: vec![],
|
||||
span: Span::new(0, 10),
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn valid_participant_with_blocks(name: String) -> impl Strategy<Value = Participant> {
|
||||
(
|
||||
prop::option::of(valid_ident()),
|
||||
prop::option::of(valid_field_list()),
|
||||
prop::option::of(valid_field_list()),
|
||||
)
|
||||
.prop_map(move |(role, self_block, other_block)| Participant {
|
||||
(prop::option::of(valid_ident()), valid_field_list()).prop_map(move |(role, fields)| {
|
||||
Participant {
|
||||
name: vec![name.clone()],
|
||||
role,
|
||||
self_block,
|
||||
other_block,
|
||||
fields,
|
||||
span: Span::new(0, 10),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn valid_relationship() -> impl Strategy<Value = Relationship> {
|
||||
@@ -142,15 +137,13 @@ fn valid_bidirectional_relationship() -> impl Strategy<Value = (Relationship, Re
|
||||
let p1 = Participant {
|
||||
name: vec![p1_name.clone()],
|
||||
role: None,
|
||||
self_block: Some(p1_self),
|
||||
other_block: None,
|
||||
fields: p1_self,
|
||||
span: Span::new(0, 10),
|
||||
};
|
||||
let p2_in_p1_rel = Participant {
|
||||
name: vec![p2_name.clone()],
|
||||
role: None,
|
||||
self_block: None,
|
||||
other_block: None,
|
||||
fields: vec![],
|
||||
span: Span::new(0, 10),
|
||||
};
|
||||
|
||||
@@ -165,15 +158,13 @@ fn valid_bidirectional_relationship() -> impl Strategy<Value = (Relationship, Re
|
||||
let p2 = Participant {
|
||||
name: vec![p2_name],
|
||||
role: None,
|
||||
self_block: Some(p2_self),
|
||||
other_block: None,
|
||||
fields: p2_self,
|
||||
span: Span::new(20, 30),
|
||||
};
|
||||
let p1_in_p2_rel = Participant {
|
||||
name: vec![p1_name],
|
||||
role: None,
|
||||
self_block: None,
|
||||
other_block: None,
|
||||
fields: vec![],
|
||||
span: Span::new(20, 30),
|
||||
};
|
||||
|
||||
@@ -257,15 +248,13 @@ proptest! {
|
||||
Participant {
|
||||
name: vec![p1.clone()],
|
||||
role: None,
|
||||
self_block: None,
|
||||
other_block: None,
|
||||
fields: vec![],
|
||||
span: Span::new(0, 10),
|
||||
},
|
||||
Participant {
|
||||
name: vec![p2.clone()],
|
||||
role: None,
|
||||
self_block: None,
|
||||
other_block: None,
|
||||
fields: vec![],
|
||||
span: Span::new(0, 10),
|
||||
},
|
||||
],
|
||||
@@ -279,15 +268,13 @@ proptest! {
|
||||
Participant {
|
||||
name: vec![p2.clone()],
|
||||
role: None,
|
||||
self_block: None,
|
||||
other_block: None,
|
||||
fields: vec![],
|
||||
span: Span::new(20, 30),
|
||||
},
|
||||
Participant {
|
||||
name: vec![p1.clone()],
|
||||
role: None,
|
||||
self_block: None,
|
||||
other_block: None,
|
||||
fields: vec![],
|
||||
span: Span::new(20, 30),
|
||||
},
|
||||
],
|
||||
@@ -327,15 +314,13 @@ proptest! {
|
||||
Participant {
|
||||
name: vec![p1.clone()],
|
||||
role: None,
|
||||
self_block: None,
|
||||
other_block: None,
|
||||
fields: vec![],
|
||||
span: Span::new(0, 10),
|
||||
},
|
||||
Participant {
|
||||
name: vec![p2.clone()],
|
||||
role: None,
|
||||
self_block: None,
|
||||
other_block: None,
|
||||
fields: vec![],
|
||||
span: Span::new(0, 10),
|
||||
},
|
||||
],
|
||||
@@ -349,15 +334,13 @@ proptest! {
|
||||
Participant {
|
||||
name: vec![p1],
|
||||
role: None,
|
||||
self_block: None,
|
||||
other_block: None,
|
||||
fields: vec![],
|
||||
span: Span::new(20, 30),
|
||||
},
|
||||
Participant {
|
||||
name: vec![p2],
|
||||
role: None,
|
||||
self_block: None,
|
||||
other_block: None,
|
||||
fields: vec![],
|
||||
span: Span::new(20, 30),
|
||||
},
|
||||
],
|
||||
@@ -391,16 +374,14 @@ proptest! {
|
||||
let participant1 = Participant {
|
||||
name: vec![p1.clone()],
|
||||
role: None,
|
||||
self_block: Some(fields1),
|
||||
other_block: None,
|
||||
fields: fields1,
|
||||
span: Span::new(0, 10),
|
||||
};
|
||||
|
||||
let participant1_again = Participant {
|
||||
name: vec![p1.clone()],
|
||||
role: None,
|
||||
self_block: Some(fields2),
|
||||
other_block: None,
|
||||
fields: fields2,
|
||||
span: Span::new(20, 30),
|
||||
};
|
||||
|
||||
@@ -411,8 +392,7 @@ proptest! {
|
||||
Participant {
|
||||
name: vec![p2.clone()],
|
||||
role: None,
|
||||
self_block: None,
|
||||
other_block: None,
|
||||
fields: vec![],
|
||||
span: Span::new(0, 10),
|
||||
},
|
||||
],
|
||||
@@ -427,8 +407,7 @@ proptest! {
|
||||
Participant {
|
||||
name: vec![p2],
|
||||
role: None,
|
||||
self_block: None,
|
||||
other_block: None,
|
||||
fields: vec![],
|
||||
span: Span::new(20, 30),
|
||||
},
|
||||
],
|
||||
@@ -461,6 +440,8 @@ proptest! {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 10),
|
||||
})),
|
||||
valid_ident().prop_map(|name| Declaration::Template(Template {
|
||||
@@ -468,6 +449,8 @@ proptest! {
|
||||
fields: vec![],
|
||||
strict: false,
|
||||
includes: vec![],
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 10),
|
||||
})),
|
||||
],
|
||||
|
||||
@@ -103,6 +103,84 @@ pub fn resolve_template_includes(
|
||||
Ok(merged_fields)
|
||||
}
|
||||
|
||||
// ===== Resource Linking Merge =====
|
||||
|
||||
/// Merge behavior links from templates into character
|
||||
///
|
||||
/// Algorithm (override-by-name semantics from design doc):
|
||||
/// 1. Start with character's own behavior links (highest priority)
|
||||
/// 2. For each template (in order), add its behavior links if not already
|
||||
/// present by tree name
|
||||
/// 3. Return concatenated list with character links first
|
||||
///
|
||||
/// This implements the merge semantics from
|
||||
/// resource-linking-checkpoint2-addendum.md
|
||||
pub fn merge_behavior_links(
|
||||
character_links: Option<Vec<crate::syntax::ast::BehaviorLink>>,
|
||||
template_links: Vec<Vec<crate::syntax::ast::BehaviorLink>>,
|
||||
) -> Option<Vec<crate::syntax::ast::BehaviorLink>> {
|
||||
use crate::syntax::ast::BehaviorLink;
|
||||
|
||||
// Start with character's own links
|
||||
let mut result: Vec<BehaviorLink> = character_links.unwrap_or_default();
|
||||
|
||||
// Track which behavior trees are already linked (by tree path)
|
||||
let mut seen_trees: HashSet<Vec<String>> =
|
||||
result.iter().map(|link| link.tree.clone()).collect();
|
||||
|
||||
// Merge template links (in order)
|
||||
for template_link_set in template_links {
|
||||
for link in template_link_set {
|
||||
// Only add if not already present (character overrides templates)
|
||||
if !seen_trees.contains(&link.tree) {
|
||||
seen_trees.insert(link.tree.clone());
|
||||
result.push(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if result.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge schedule links from templates into character
|
||||
///
|
||||
/// Algorithm:
|
||||
/// 1. Start with character's own schedule links
|
||||
/// 2. For each template (in order), add its schedule links if not already
|
||||
/// present
|
||||
/// 3. Return concatenated list with character schedules first
|
||||
pub fn merge_schedule_links(
|
||||
character_schedules: Option<Vec<String>>,
|
||||
template_schedules: Vec<Vec<String>>,
|
||||
) -> Option<Vec<String>> {
|
||||
// Start with character's own schedules
|
||||
let mut result: Vec<String> = character_schedules.unwrap_or_default();
|
||||
|
||||
// Track which schedules are already linked
|
||||
let mut seen_schedules: HashSet<String> = result.iter().cloned().collect();
|
||||
|
||||
// Merge template schedules (in order)
|
||||
for template_schedule_set in template_schedules {
|
||||
for schedule in template_schedule_set {
|
||||
// Only add if not already present (character overrides templates)
|
||||
if !seen_schedules.contains(&schedule) {
|
||||
seen_schedules.insert(schedule.clone());
|
||||
result.push(schedule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if result.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge character templates into character fields
|
||||
///
|
||||
/// Algorithm:
|
||||
@@ -499,6 +577,8 @@ mod tests {
|
||||
fields,
|
||||
includes: includes.iter().map(|s| s.to_string()).collect(),
|
||||
strict,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 10),
|
||||
}
|
||||
}
|
||||
@@ -513,6 +593,8 @@ mod tests {
|
||||
} else {
|
||||
Some(templates.iter().map(|s| s.to_string()).collect())
|
||||
},
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 10),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ pub mod convert;
|
||||
pub mod links;
|
||||
pub mod merge;
|
||||
pub mod names;
|
||||
pub mod references;
|
||||
pub mod types;
|
||||
pub mod validate;
|
||||
|
||||
@@ -43,9 +44,15 @@ mod convert_integration_tests;
|
||||
|
||||
use miette::Diagnostic;
|
||||
pub use names::{
|
||||
DeclKind,
|
||||
NameTable,
|
||||
QualifiedPath,
|
||||
};
|
||||
pub use references::{
|
||||
find_all_references,
|
||||
Reference,
|
||||
ReferenceContext,
|
||||
};
|
||||
use thiserror::Error;
|
||||
pub use types::ResolvedFile;
|
||||
|
||||
|
||||
@@ -322,6 +322,8 @@ mod tests {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(0, 10),
|
||||
}),
|
||||
@@ -330,6 +332,8 @@ mod tests {
|
||||
fields: vec![],
|
||||
strict: false,
|
||||
includes: vec![],
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(20, 30),
|
||||
}),
|
||||
],
|
||||
@@ -351,6 +355,8 @@ mod tests {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(0, 10),
|
||||
}),
|
||||
@@ -359,6 +365,8 @@ mod tests {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(20, 30),
|
||||
}),
|
||||
@@ -377,6 +385,8 @@ mod tests {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(0, 10),
|
||||
})],
|
||||
@@ -408,6 +418,8 @@ mod tests {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(20, 30),
|
||||
}),
|
||||
@@ -483,6 +495,8 @@ mod tests {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(0, 10),
|
||||
})],
|
||||
@@ -494,6 +508,8 @@ mod tests {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(0, 10),
|
||||
})],
|
||||
@@ -517,6 +533,8 @@ mod tests {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(0, 10),
|
||||
})],
|
||||
@@ -528,6 +546,8 @@ mod tests {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(20, 30),
|
||||
})],
|
||||
@@ -549,6 +569,8 @@ mod tests {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(0, 10),
|
||||
})],
|
||||
@@ -559,6 +581,8 @@ mod tests {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(0, 10),
|
||||
})],
|
||||
@@ -580,6 +604,8 @@ mod tests {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(0, 10),
|
||||
})],
|
||||
@@ -590,6 +616,8 @@ mod tests {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(20, 30),
|
||||
})],
|
||||
|
||||
@@ -55,6 +55,8 @@ fn valid_character_decl() -> impl Strategy<Value = (String, Declaration)> {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(0, 10),
|
||||
});
|
||||
@@ -69,6 +71,8 @@ fn valid_template_decl() -> impl Strategy<Value = (String, Declaration)> {
|
||||
fields: vec![],
|
||||
strict: false,
|
||||
includes: vec![],
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
span: Span::new(0, 10),
|
||||
});
|
||||
(name, decl)
|
||||
@@ -161,6 +165,8 @@ proptest! {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(i * 10, i * 10 + 10),
|
||||
})
|
||||
@@ -182,6 +188,8 @@ proptest! {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(0, 10),
|
||||
}),
|
||||
@@ -260,6 +268,8 @@ proptest! {
|
||||
species: None,
|
||||
fields: vec![],
|
||||
template: None,
|
||||
uses_behaviors: None,
|
||||
uses_schedule: None,
|
||||
|
||||
span: Span::new(0, 10),
|
||||
}),
|
||||
|
||||
685
src/resolve/references.rs
Normal file
685
src/resolve/references.rs
Normal file
@@ -0,0 +1,685 @@
|
||||
//! Semantic reference tracking and resolution
|
||||
//!
|
||||
//! This module provides semantic analysis to find all references to symbols,
|
||||
//! enabling features like rename refactoring and find-all-references.
|
||||
|
||||
use super::names::{
|
||||
DeclKind,
|
||||
NameTable,
|
||||
};
|
||||
use crate::syntax::ast::{
|
||||
Behavior,
|
||||
Character,
|
||||
Declaration,
|
||||
Field,
|
||||
File,
|
||||
Institution,
|
||||
LifeArc,
|
||||
Location,
|
||||
Participant,
|
||||
Relationship,
|
||||
Schedule,
|
||||
Span,
|
||||
Species,
|
||||
Template,
|
||||
Value,
|
||||
};
|
||||
|
||||
/// A reference to a symbol in the code
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Reference {
|
||||
/// The name being referenced
|
||||
pub name: String,
|
||||
/// Kind of symbol being referenced
|
||||
pub kind: DeclKind,
|
||||
/// Location of the reference (just the identifier)
|
||||
pub span: Span,
|
||||
/// Index of the file containing this reference
|
||||
pub file_index: usize,
|
||||
/// Context of the reference
|
||||
pub context: ReferenceContext,
|
||||
}
|
||||
|
||||
/// Context describing where and how a symbol is referenced
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ReferenceContext {
|
||||
/// Symbol definition/declaration
|
||||
Definition,
|
||||
/// Used as a type annotation (e.g., `character Alice: Person`)
|
||||
TypeAnnotation,
|
||||
/// Used in a field value (e.g., `friend: Alice`)
|
||||
FieldValue,
|
||||
/// Referenced in a behavior tree (e.g., `@WorkAtBakery`)
|
||||
BehaviorReference,
|
||||
/// Used in a template include
|
||||
TemplateInclude,
|
||||
/// Used in a relationship participant
|
||||
RelationshipParticipant,
|
||||
/// Other/unknown context
|
||||
Other,
|
||||
}
|
||||
|
||||
/// Find all references to a specific symbol across all files
|
||||
pub fn find_all_references(
|
||||
files: &[File],
|
||||
symbol_name: &str,
|
||||
symbol_kind: DeclKind,
|
||||
) -> Vec<Reference> {
|
||||
let mut references = Vec::new();
|
||||
|
||||
for (file_index, file) in files.iter().enumerate() {
|
||||
// Build name table to validate symbols exist
|
||||
let name_table = match NameTable::from_file(file) {
|
||||
| Ok(table) => table,
|
||||
| Err(_) => continue, // Skip files with errors
|
||||
};
|
||||
|
||||
// Find definition if it exists in this file
|
||||
if let Some(entry) = name_table.resolve_name(symbol_name) {
|
||||
if entry.kind == symbol_kind {
|
||||
references.push(Reference {
|
||||
name: symbol_name.to_string(),
|
||||
kind: symbol_kind,
|
||||
span: entry.span.clone(),
|
||||
file_index,
|
||||
context: ReferenceContext::Definition,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Walk AST to find all semantic references
|
||||
for decl in &file.declarations {
|
||||
references.extend(find_references_in_declaration(
|
||||
decl,
|
||||
symbol_name,
|
||||
symbol_kind,
|
||||
file_index,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
references
|
||||
}
|
||||
|
||||
/// Find references within a single declaration
|
||||
fn find_references_in_declaration(
|
||||
decl: &Declaration,
|
||||
symbol_name: &str,
|
||||
symbol_kind: DeclKind,
|
||||
file_index: usize,
|
||||
) -> Vec<Reference> {
|
||||
let mut refs = Vec::new();
|
||||
|
||||
match decl {
|
||||
| Declaration::Character(c) => {
|
||||
refs.extend(find_references_in_character(
|
||||
c,
|
||||
symbol_name,
|
||||
symbol_kind,
|
||||
file_index,
|
||||
));
|
||||
},
|
||||
| Declaration::Template(t) => {
|
||||
refs.extend(find_references_in_template(
|
||||
t,
|
||||
symbol_name,
|
||||
symbol_kind,
|
||||
file_index,
|
||||
));
|
||||
},
|
||||
| Declaration::LifeArc(l) => {
|
||||
refs.extend(find_references_in_life_arc(
|
||||
l,
|
||||
symbol_name,
|
||||
symbol_kind,
|
||||
file_index,
|
||||
));
|
||||
},
|
||||
| Declaration::Schedule(s) => {
|
||||
refs.extend(find_references_in_schedule(
|
||||
s,
|
||||
symbol_name,
|
||||
symbol_kind,
|
||||
file_index,
|
||||
));
|
||||
},
|
||||
| Declaration::Behavior(b) => {
|
||||
refs.extend(find_references_in_behavior(
|
||||
b,
|
||||
symbol_name,
|
||||
symbol_kind,
|
||||
file_index,
|
||||
));
|
||||
},
|
||||
| Declaration::Institution(i) => {
|
||||
refs.extend(find_references_in_institution(
|
||||
i,
|
||||
symbol_name,
|
||||
symbol_kind,
|
||||
file_index,
|
||||
));
|
||||
},
|
||||
| Declaration::Relationship(r) => {
|
||||
refs.extend(find_references_in_relationship(
|
||||
r,
|
||||
symbol_name,
|
||||
symbol_kind,
|
||||
file_index,
|
||||
));
|
||||
},
|
||||
| Declaration::Location(l) => {
|
||||
refs.extend(find_references_in_location(
|
||||
l,
|
||||
symbol_name,
|
||||
symbol_kind,
|
||||
file_index,
|
||||
));
|
||||
},
|
||||
| Declaration::Species(s) => {
|
||||
refs.extend(find_references_in_species(
|
||||
s,
|
||||
symbol_name,
|
||||
symbol_kind,
|
||||
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
|
||||
},
|
||||
}
|
||||
|
||||
refs
|
||||
}
|
||||
|
||||
fn find_references_in_character(
|
||||
c: &Character,
|
||||
symbol_name: &str,
|
||||
symbol_kind: DeclKind,
|
||||
file_index: usize,
|
||||
) -> Vec<Reference> {
|
||||
let mut refs = Vec::new();
|
||||
|
||||
// Check species annotation
|
||||
if let Some(ref species) = c.species {
|
||||
if species == symbol_name && symbol_kind == DeclKind::Species {
|
||||
refs.push(Reference {
|
||||
name: symbol_name.to_string(),
|
||||
kind: symbol_kind,
|
||||
span: c.span.clone(),
|
||||
file_index,
|
||||
context: ReferenceContext::TypeAnnotation,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check templates (character can have multiple)
|
||||
if let Some(ref templates) = c.template {
|
||||
for template in templates {
|
||||
if template == symbol_name && symbol_kind == DeclKind::Template {
|
||||
refs.push(Reference {
|
||||
name: symbol_name.to_string(),
|
||||
kind: symbol_kind,
|
||||
span: c.span.clone(),
|
||||
file_index,
|
||||
context: ReferenceContext::TypeAnnotation,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check fields for identifier references
|
||||
refs.extend(find_references_in_fields(
|
||||
&c.fields,
|
||||
symbol_name,
|
||||
symbol_kind,
|
||||
file_index,
|
||||
));
|
||||
|
||||
refs
|
||||
}
|
||||
|
||||
fn find_references_in_template(
|
||||
t: &Template,
|
||||
symbol_name: &str,
|
||||
symbol_kind: DeclKind,
|
||||
file_index: usize,
|
||||
) -> Vec<Reference> {
|
||||
let mut refs = Vec::new();
|
||||
|
||||
// Check includes
|
||||
for include in &t.includes {
|
||||
if include == symbol_name && symbol_kind == DeclKind::Template {
|
||||
refs.push(Reference {
|
||||
name: symbol_name.to_string(),
|
||||
kind: symbol_kind,
|
||||
span: t.span.clone(),
|
||||
file_index,
|
||||
context: ReferenceContext::TemplateInclude,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check fields
|
||||
refs.extend(find_references_in_fields(
|
||||
&t.fields,
|
||||
symbol_name,
|
||||
symbol_kind,
|
||||
file_index,
|
||||
));
|
||||
|
||||
refs
|
||||
}
|
||||
|
||||
fn find_references_in_fields(
|
||||
fields: &[Field],
|
||||
symbol_name: &str,
|
||||
symbol_kind: DeclKind,
|
||||
file_index: usize,
|
||||
) -> Vec<Reference> {
|
||||
let mut refs = Vec::new();
|
||||
|
||||
for field in fields {
|
||||
// Check if field value is an identifier that references the symbol
|
||||
refs.extend(find_references_in_value(
|
||||
&field.value,
|
||||
symbol_name,
|
||||
symbol_kind,
|
||||
file_index,
|
||||
field.span.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
refs
|
||||
}
|
||||
|
||||
fn find_references_in_value(
|
||||
value: &Value,
|
||||
symbol_name: &str,
|
||||
symbol_kind: DeclKind,
|
||||
file_index: usize,
|
||||
span: Span,
|
||||
) -> Vec<Reference> {
|
||||
let mut refs = Vec::new();
|
||||
|
||||
match value {
|
||||
| Value::Identifier(path) => {
|
||||
// Check if this identifier references our symbol
|
||||
if let Some(name) = path.last() {
|
||||
if name == symbol_name {
|
||||
// Identifiers can reference characters, templates, enums, species
|
||||
let matches_kind = matches!(
|
||||
symbol_kind,
|
||||
DeclKind::Character |
|
||||
DeclKind::Template |
|
||||
DeclKind::Enum |
|
||||
DeclKind::Species
|
||||
);
|
||||
|
||||
if matches_kind {
|
||||
refs.push(Reference {
|
||||
name: symbol_name.to_string(),
|
||||
kind: symbol_kind,
|
||||
span,
|
||||
file_index,
|
||||
context: ReferenceContext::FieldValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
| Value::List(values) => {
|
||||
// Recursively check list values
|
||||
for v in values {
|
||||
refs.extend(find_references_in_value(
|
||||
v,
|
||||
symbol_name,
|
||||
symbol_kind,
|
||||
file_index,
|
||||
span.clone(),
|
||||
));
|
||||
}
|
||||
},
|
||||
| Value::Object(fields) => {
|
||||
// Recursively check object fields
|
||||
refs.extend(find_references_in_fields(
|
||||
fields,
|
||||
symbol_name,
|
||||
symbol_kind,
|
||||
file_index,
|
||||
));
|
||||
},
|
||||
| Value::Override(override_val) => {
|
||||
// Check the base template reference
|
||||
if let Some(base_name) = override_val.base.last() {
|
||||
if base_name == symbol_name && symbol_kind == DeclKind::Template {
|
||||
refs.push(Reference {
|
||||
name: symbol_name.to_string(),
|
||||
kind: symbol_kind,
|
||||
span: override_val.span.clone(),
|
||||
file_index,
|
||||
context: ReferenceContext::FieldValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
| _ => {
|
||||
// Other value types don't contain references
|
||||
},
|
||||
}
|
||||
|
||||
refs
|
||||
}
|
||||
|
||||
fn find_references_in_life_arc(
|
||||
_l: &LifeArc,
|
||||
_symbol_name: &str,
|
||||
_symbol_kind: DeclKind,
|
||||
_file_index: usize,
|
||||
) -> Vec<Reference> {
|
||||
// Life arcs don't typically reference other symbols
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn find_references_in_schedule(
|
||||
_s: &Schedule,
|
||||
_symbol_name: &str,
|
||||
_symbol_kind: DeclKind,
|
||||
_file_index: usize,
|
||||
) -> Vec<Reference> {
|
||||
// Schedules don't typically reference other symbols
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn find_references_in_behavior(
|
||||
_b: &Behavior,
|
||||
_symbol_name: &str,
|
||||
_symbol_kind: DeclKind,
|
||||
_file_index: usize,
|
||||
) -> Vec<Reference> {
|
||||
// TODO: Parse behavior tree nodes to find @BehaviorName references
|
||||
// This requires walking the BehaviorNode tree
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn find_references_in_institution(
|
||||
i: &Institution,
|
||||
symbol_name: &str,
|
||||
symbol_kind: DeclKind,
|
||||
file_index: usize,
|
||||
) -> Vec<Reference> {
|
||||
find_references_in_fields(&i.fields, symbol_name, symbol_kind, file_index)
|
||||
}
|
||||
|
||||
fn find_references_in_relationship(
|
||||
r: &Relationship,
|
||||
symbol_name: &str,
|
||||
symbol_kind: DeclKind,
|
||||
file_index: usize,
|
||||
) -> Vec<Reference> {
|
||||
let mut refs = Vec::new();
|
||||
|
||||
// Check participant references
|
||||
for participant in &r.participants {
|
||||
refs.extend(find_references_in_participant(
|
||||
participant,
|
||||
symbol_name,
|
||||
symbol_kind,
|
||||
file_index,
|
||||
));
|
||||
}
|
||||
|
||||
refs
|
||||
}
|
||||
|
||||
fn find_references_in_participant(
|
||||
p: &Participant,
|
||||
symbol_name: &str,
|
||||
symbol_kind: DeclKind,
|
||||
file_index: usize,
|
||||
) -> Vec<Reference> {
|
||||
let mut refs = Vec::new();
|
||||
|
||||
// Check if participant name references the symbol
|
||||
if let Some(participant_name) = p.name.last() {
|
||||
if participant_name == symbol_name && symbol_kind == DeclKind::Character {
|
||||
refs.push(Reference {
|
||||
name: symbol_name.to_string(),
|
||||
kind: symbol_kind,
|
||||
span: p.span.clone(),
|
||||
file_index,
|
||||
context: ReferenceContext::RelationshipParticipant,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check participant fields
|
||||
refs.extend(find_references_in_fields(
|
||||
&p.fields,
|
||||
symbol_name,
|
||||
symbol_kind,
|
||||
file_index,
|
||||
));
|
||||
|
||||
refs
|
||||
}
|
||||
|
||||
fn find_references_in_location(
|
||||
l: &Location,
|
||||
symbol_name: &str,
|
||||
symbol_kind: DeclKind,
|
||||
file_index: usize,
|
||||
) -> Vec<Reference> {
|
||||
find_references_in_fields(&l.fields, symbol_name, symbol_kind, file_index)
|
||||
}
|
||||
|
||||
fn find_references_in_species(
|
||||
s: &Species,
|
||||
symbol_name: &str,
|
||||
symbol_kind: DeclKind,
|
||||
file_index: usize,
|
||||
) -> Vec<Reference> {
|
||||
find_references_in_fields(&s.fields, symbol_name, symbol_kind, file_index)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::syntax::{
|
||||
lexer::Lexer,
|
||||
FileParser,
|
||||
};
|
||||
|
||||
fn parse(source: &str) -> File {
|
||||
let lexer = Lexer::new(source);
|
||||
FileParser::new().parse(lexer).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_character_references_in_field() {
|
||||
let source = r#"
|
||||
character Alice {}
|
||||
character Bob { friend: Alice }
|
||||
"#;
|
||||
let file = parse(source);
|
||||
let files = vec![file];
|
||||
|
||||
let refs = find_all_references(&files, "Alice", DeclKind::Character);
|
||||
|
||||
// Should find: definition + field reference
|
||||
assert_eq!(refs.len(), 2);
|
||||
|
||||
let definition = refs
|
||||
.iter()
|
||||
.find(|r| r.context == ReferenceContext::Definition);
|
||||
assert!(definition.is_some());
|
||||
|
||||
let field_ref = refs
|
||||
.iter()
|
||||
.find(|r| r.context == ReferenceContext::FieldValue);
|
||||
assert!(field_ref.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_template_references() {
|
||||
let source = r#"
|
||||
template Person {}
|
||||
character Alice from Person {}
|
||||
"#;
|
||||
let file = parse(source);
|
||||
let files = vec![file];
|
||||
|
||||
let refs = find_all_references(&files, "Person", DeclKind::Template);
|
||||
|
||||
// Should find: definition + type annotation
|
||||
assert_eq!(refs.len(), 2);
|
||||
|
||||
let type_ref = refs
|
||||
.iter()
|
||||
.find(|r| r.context == ReferenceContext::TypeAnnotation);
|
||||
assert!(type_ref.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_species_references() {
|
||||
let source = r#"
|
||||
species Human {}
|
||||
character Alice: Human {}
|
||||
"#;
|
||||
let file = parse(source);
|
||||
let files = vec![file];
|
||||
|
||||
let refs = find_all_references(&files, "Human", DeclKind::Species);
|
||||
|
||||
// Should find: definition + species annotation
|
||||
assert_eq!(refs.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_references_across_multiple_files() {
|
||||
let file1 = parse("character Alice {}");
|
||||
let file2 = parse("character Bob { friend: Alice }");
|
||||
let file3 = parse("character Charlie { mentor: Alice }");
|
||||
|
||||
let files = vec![file1, file2, file3];
|
||||
|
||||
let refs = find_all_references(&files, "Alice", DeclKind::Character);
|
||||
|
||||
// Should find: 1 definition + 2 references
|
||||
assert_eq!(refs.len(), 3);
|
||||
|
||||
let def = refs
|
||||
.iter()
|
||||
.filter(|r| r.context == ReferenceContext::Definition)
|
||||
.count();
|
||||
assert_eq!(def, 1);
|
||||
|
||||
let field_refs = refs
|
||||
.iter()
|
||||
.filter(|r| r.context == ReferenceContext::FieldValue)
|
||||
.count();
|
||||
assert_eq!(field_refs, 2);
|
||||
|
||||
// Check file indices
|
||||
assert_eq!(refs.iter().filter(|r| r.file_index == 0).count(), 1);
|
||||
assert_eq!(refs.iter().filter(|r| r.file_index == 1).count(), 1);
|
||||
assert_eq!(refs.iter().filter(|r| r.file_index == 2).count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_respects_symbol_kind() {
|
||||
let source = r#"
|
||||
character Alice {}
|
||||
template Person {}
|
||||
character Bob { friend: Alice }
|
||||
character Charlie from Person {}
|
||||
"#;
|
||||
let file = parse(source);
|
||||
let files = vec![file];
|
||||
|
||||
// Find character Alice
|
||||
let char_refs = find_all_references(&files, "Alice", DeclKind::Character);
|
||||
// Should find: character definition + field reference
|
||||
assert_eq!(char_refs.len(), 2);
|
||||
|
||||
// Find template Person
|
||||
let template_refs = find_all_references(&files, "Person", DeclKind::Template);
|
||||
// Should find: template definition + from reference
|
||||
assert_eq!(template_refs.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_includes() {
|
||||
let source = r#"
|
||||
template Base {}
|
||||
template Extended { include Base }
|
||||
"#;
|
||||
let file = parse(source);
|
||||
let files = vec![file];
|
||||
|
||||
let refs = find_all_references(&files, "Base", DeclKind::Template);
|
||||
|
||||
// Should find: definition + include reference
|
||||
assert_eq!(refs.len(), 2);
|
||||
|
||||
let include_ref = refs
|
||||
.iter()
|
||||
.find(|r| r.context == ReferenceContext::TemplateInclude);
|
||||
assert!(include_ref.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relationship_participants() {
|
||||
let source = r#"
|
||||
character Alice {}
|
||||
character Bob {}
|
||||
relationship Friends { Alice as friend {} Bob as friend {} }
|
||||
"#;
|
||||
let file = parse(source);
|
||||
let files = vec![file];
|
||||
|
||||
let alice_refs = find_all_references(&files, "Alice", DeclKind::Character);
|
||||
let bob_refs = find_all_references(&files, "Bob", DeclKind::Character);
|
||||
|
||||
// Each should have: definition + relationship participant
|
||||
assert_eq!(alice_refs.len(), 2);
|
||||
assert_eq!(bob_refs.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_references_found() {
|
||||
let source = "character Alice {}";
|
||||
let file = parse(source);
|
||||
let files = vec![file];
|
||||
|
||||
// Look for non-existent symbol
|
||||
let refs = find_all_references(&files, "Bob", DeclKind::Character);
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
@@ -133,14 +133,9 @@ pub fn validate_relationship_bonds(relationships: &[Relationship], collector: &m
|
||||
}
|
||||
}
|
||||
|
||||
// Validate self/other blocks if present
|
||||
// Validate participant fields
|
||||
for participant in &rel.participants {
|
||||
if let Some(ref self_fields) = participant.self_block {
|
||||
validate_trait_ranges(self_fields, collector);
|
||||
}
|
||||
if let Some(ref other_fields) = participant.other_block {
|
||||
validate_trait_ranges(other_fields, collector);
|
||||
}
|
||||
validate_trait_ranges(&participant.fields, collector);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,7 +160,7 @@ pub fn validate_schedule_overlaps(schedule: &Schedule, collector: &mut ErrorColl
|
||||
collector.add(ResolveError::ScheduleOverlap {
|
||||
block1: format!(
|
||||
"{} ({}:{:02}-{}:{:02})",
|
||||
block1.activity,
|
||||
block1.name.as_ref().unwrap_or(&block1.activity),
|
||||
block1.start.hour,
|
||||
block1.start.minute,
|
||||
block1.end.hour,
|
||||
@@ -173,7 +168,7 @@ pub fn validate_schedule_overlaps(schedule: &Schedule, collector: &mut ErrorColl
|
||||
),
|
||||
block2: format!(
|
||||
"{} ({}:{:02}-{}:{:02})",
|
||||
block2.activity,
|
||||
block2.name.as_ref().unwrap_or(&block2.activity),
|
||||
block2.start.hour,
|
||||
block2.start.minute,
|
||||
block2.end.hour,
|
||||
@@ -242,7 +237,7 @@ fn validate_tree_node_actions(
|
||||
collector: &mut ErrorCollector,
|
||||
) {
|
||||
match node {
|
||||
| BehaviorNode::Sequence(children) | BehaviorNode::Selector(children) => {
|
||||
| BehaviorNode::Sequence { children, .. } | BehaviorNode::Selector { children, .. } => {
|
||||
for child in children {
|
||||
validate_tree_node_actions(child, action_registry, tree_name, collector);
|
||||
}
|
||||
@@ -258,7 +253,7 @@ fn validate_tree_node_actions(
|
||||
| BehaviorNode::Condition(_) => {
|
||||
// Conditions are validated separately via expression validation
|
||||
},
|
||||
| BehaviorNode::Decorator(_name, child) => {
|
||||
| BehaviorNode::Decorator { child, .. } => {
|
||||
validate_tree_node_actions(child, action_registry, tree_name, collector);
|
||||
},
|
||||
| BehaviorNode::SubTree(_path) => {
|
||||
@@ -267,6 +262,154 @@ fn validate_tree_node_actions(
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate character resource linking
|
||||
///
|
||||
/// Checks:
|
||||
/// 1. No duplicate behavior tree references
|
||||
/// 2. Priority values are valid (handled by type system)
|
||||
pub fn validate_character_resource_links(character: &Character, collector: &mut ErrorCollector) {
|
||||
// Check for duplicate behavior tree references
|
||||
if let Some(ref behavior_links) = character.uses_behaviors {
|
||||
let mut seen_trees: HashSet<String> = HashSet::new();
|
||||
|
||||
for link in behavior_links {
|
||||
let tree_name = link.tree.join("::");
|
||||
|
||||
if seen_trees.contains(&tree_name) {
|
||||
collector.add(ResolveError::ValidationError {
|
||||
message: format!(
|
||||
"Character '{}' has duplicate behavior tree reference: {}",
|
||||
character.name,
|
||||
tree_name
|
||||
),
|
||||
help: Some(format!(
|
||||
"The behavior tree '{}' is referenced multiple times in the uses behaviors list. Each behavior tree should only be referenced once. If you want different conditions or priorities, combine them into a single entry.",
|
||||
tree_name
|
||||
)),
|
||||
});
|
||||
}
|
||||
|
||||
seen_trees.insert(tree_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicate schedule references
|
||||
if let Some(ref schedules) = character.uses_schedule {
|
||||
let mut seen_schedules: HashSet<String> = HashSet::new();
|
||||
|
||||
for schedule in schedules {
|
||||
if seen_schedules.contains(schedule) {
|
||||
collector.add(ResolveError::ValidationError {
|
||||
message: format!(
|
||||
"Character '{}' has duplicate schedule reference: {}",
|
||||
character.name,
|
||||
schedule
|
||||
),
|
||||
help: Some(format!(
|
||||
"The schedule '{}' is referenced multiple times. Each schedule should only be referenced once.",
|
||||
schedule
|
||||
)),
|
||||
});
|
||||
}
|
||||
|
||||
seen_schedules.insert(schedule.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate institution resource linking
|
||||
pub fn validate_institution_resource_links(
|
||||
institution: &Institution,
|
||||
collector: &mut ErrorCollector,
|
||||
) {
|
||||
// Check for duplicate behavior tree references
|
||||
if let Some(ref behavior_links) = institution.uses_behaviors {
|
||||
let mut seen_trees: HashSet<String> = HashSet::new();
|
||||
|
||||
for link in behavior_links {
|
||||
let tree_name = link.tree.join("::");
|
||||
|
||||
if seen_trees.contains(&tree_name) {
|
||||
collector.add(ResolveError::ValidationError {
|
||||
message: format!(
|
||||
"Institution '{}' has duplicate behavior tree reference: {}",
|
||||
institution.name,
|
||||
tree_name
|
||||
),
|
||||
help: Some(format!(
|
||||
"The behavior tree '{}' is referenced multiple times. Each behavior tree should only be referenced once.",
|
||||
tree_name
|
||||
)),
|
||||
});
|
||||
}
|
||||
|
||||
seen_trees.insert(tree_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicate schedule references
|
||||
if let Some(ref schedules) = institution.uses_schedule {
|
||||
let mut seen_schedules: HashSet<String> = HashSet::new();
|
||||
|
||||
for schedule in schedules {
|
||||
if seen_schedules.contains(schedule) {
|
||||
collector.add(ResolveError::ValidationError {
|
||||
message: format!(
|
||||
"Institution '{}' has duplicate schedule reference: {}",
|
||||
institution.name, schedule
|
||||
),
|
||||
help: Some(format!(
|
||||
"The schedule '{}' is referenced multiple times.",
|
||||
schedule
|
||||
)),
|
||||
});
|
||||
}
|
||||
|
||||
seen_schedules.insert(schedule.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate schedule composition requirements
|
||||
///
|
||||
/// Checks:
|
||||
/// 1. All blocks in extended schedules have names
|
||||
/// 2. Override blocks reference valid block names (requires name resolution)
|
||||
pub fn validate_schedule_composition(schedule: &Schedule, collector: &mut ErrorCollector) {
|
||||
// If schedule extends another, all blocks must have names for override system
|
||||
if schedule.extends.is_some() {
|
||||
for block in &schedule.blocks {
|
||||
if block.name.is_none() && !block.activity.is_empty() {
|
||||
collector.add(ResolveError::ValidationError {
|
||||
message: format!(
|
||||
"Schedule '{}' extends another schedule but has unnamed blocks",
|
||||
schedule.name
|
||||
),
|
||||
help: Some(
|
||||
"When a schedule extends another, all blocks must have names to support the override system. Use 'block name { ... }' syntax instead of 'time -> time : activity { ... }'.".to_string()
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that new-style blocks have action references
|
||||
for block in &schedule.blocks {
|
||||
if block.name.is_some() && block.action.is_none() && block.activity.is_empty() {
|
||||
collector.add(ResolveError::ValidationError {
|
||||
message: format!(
|
||||
"Schedule '{}' block '{}' missing action reference",
|
||||
schedule.name,
|
||||
block.name.as_ref().unwrap()
|
||||
),
|
||||
help: Some(
|
||||
"Named blocks should specify a behavior using 'action: BehaviorName'. Example: 'block work { 9:00 -> 17:00, action: WorkBehavior }'".to_string()
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate an entire file
|
||||
///
|
||||
/// Collects all validation errors and returns them together instead of failing
|
||||
@@ -278,12 +421,17 @@ pub fn validate_file(file: &File, action_registry: &HashSet<String>) -> Result<(
|
||||
match decl {
|
||||
| Declaration::Character(c) => {
|
||||
validate_trait_ranges(&c.fields, &mut collector);
|
||||
validate_character_resource_links(c, &mut collector);
|
||||
},
|
||||
| Declaration::Institution(i) => {
|
||||
validate_institution_resource_links(i, &mut collector);
|
||||
},
|
||||
| Declaration::Relationship(r) => {
|
||||
validate_relationship_bonds(std::slice::from_ref(r), &mut collector);
|
||||
},
|
||||
| Declaration::Schedule(s) => {
|
||||
validate_schedule_overlaps(s, &mut collector);
|
||||
validate_schedule_composition(s, &mut collector);
|
||||
},
|
||||
| Declaration::LifeArc(la) => {
|
||||
validate_life_arc_transitions(la, &mut collector);
|
||||
@@ -472,10 +620,13 @@ mod tests {
|
||||
|
||||
let tree = Behavior {
|
||||
name: "Test".to_string(),
|
||||
root: BehaviorNode::Sequence(vec![
|
||||
BehaviorNode::Action("walk".to_string(), vec![]),
|
||||
BehaviorNode::Action("eat".to_string(), vec![]),
|
||||
]),
|
||||
root: BehaviorNode::Sequence {
|
||||
label: None,
|
||||
children: vec![
|
||||
BehaviorNode::Action("walk".to_string(), vec![]),
|
||||
BehaviorNode::Action("eat".to_string(), vec![]),
|
||||
],
|
||||
},
|
||||
span: Span::new(0, 100),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user