Added complete support for the new type system syntax including: - concept: Base type declarations - sub_concept: Enum and record sub-type definitions - concept_comparison: Compile-time pattern matching with conditional guards Parser changes: - Added VariantPattern, FieldCondition, and Condition AST nodes - Implemented "is" keyword for pattern matching (e.g., "CupType is Glass or CupType is Plastic") - Added Value::Any variant to support universal type matching - Disambiguated enum-like vs record-like sub_concept syntax LSP updates: - Added Value::Any match arms across code_actions, completion, hover, inlay_hints, and semantic_tokens - Type inference and formatting support for Any values Example fixes: - Fixed syntax error in baker-family behaviors (missing closing brace in nested if) - Removed deprecated core_enums.sb file
663 lines
18 KiB
Rust
663 lines
18 KiB
Rust
//! 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::Use(_) => {
|
|
// Use statements are handled separately
|
|
},
|
|
| Declaration::Concept(_) |
|
|
Declaration::SubConcept(_) |
|
|
Declaration::ConceptComparison(_) => {
|
|
// TODO: Implement reference finding for type system
|
|
},
|
|
}
|
|
|
|
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::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);
|
|
}
|
|
}
|