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:
2026-02-13 21:52:03 +00:00
parent 80332971b8
commit 16deb5d237
290 changed files with 90316 additions and 5827 deletions

View File

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

View File

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

View File

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

View File

@@ -132,8 +132,8 @@ fn test_all_declaration_kinds() {
name: "Test"
}
relationship R {
C
C
C { }
C { }
}
location Loc {
name: "Place"

View File

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

View File

@@ -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),
})),
],

View File

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

View File

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

View File

@@ -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),
})],

View File

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

View File

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