Renamed AST value types for v0.3 naming convention: - Value::Int -> Value::Number - Value::Float -> Value::Decimal - Value::String -> Value::Text - Value::Bool -> Value::Boolean - Expr::IntLit -> Expr::NumberLit - Expr::FloatLit -> Expr::DecimalLit - Expr::StringLit -> Expr::TextLit - Expr::BoolLit -> Expr::BooleanLit Updated all references across parser, resolver, validator, LSP, query engine, tests, and editor.
300 lines
9.4 KiB
Rust
300 lines
9.4 KiB
Rust
//! Bidirectional relationship resolution
|
|
//!
|
|
//! Handles relationships that can be declared from either participant's
|
|
//! perspective, merging self/other blocks and validating consistency.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use crate::{
|
|
resolve::{
|
|
ResolveError,
|
|
Result,
|
|
},
|
|
syntax::ast::{
|
|
Declaration,
|
|
Field,
|
|
File,
|
|
Participant,
|
|
Relationship,
|
|
},
|
|
};
|
|
|
|
/// A relationship key that's order-independent
|
|
/// (Martha, David) and (David, Martha) map to the same key
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
struct RelationshipKey {
|
|
participants: Vec<String>,
|
|
name: String,
|
|
}
|
|
|
|
impl RelationshipKey {
|
|
fn new(mut participants: Vec<String>, name: String) -> Self {
|
|
// Sort participants to make key order-independent
|
|
participants.sort();
|
|
Self { participants, name }
|
|
}
|
|
}
|
|
|
|
/// Information about a relationship declaration
|
|
#[derive(Debug, Clone)]
|
|
struct RelationshipDecl {
|
|
relationship: Relationship,
|
|
}
|
|
|
|
/// Resolved bidirectional relationship
|
|
#[derive(Debug, Clone)]
|
|
pub struct ResolvedRelationship {
|
|
pub name: String,
|
|
pub participants: Vec<Participant>,
|
|
pub fields: Vec<Field>,
|
|
/// Merged self/other blocks for each participant
|
|
pub participant_fields: Vec<ParticipantFields>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ParticipantFields {
|
|
pub participant_name: Vec<String>,
|
|
pub role: Option<String>,
|
|
/// Fields from this participant's block
|
|
pub fields: Vec<Field>,
|
|
}
|
|
|
|
/// Resolve bidirectional relationships in a file
|
|
pub fn resolve_relationships(file: &File) -> Result<Vec<ResolvedRelationship>> {
|
|
// Group relationships by key
|
|
let mut relationship_groups: HashMap<RelationshipKey, Vec<RelationshipDecl>> = HashMap::new();
|
|
|
|
for decl in &file.declarations {
|
|
if let Declaration::Relationship(rel) = decl {
|
|
// Extract participant names
|
|
let participant_names: Vec<String> =
|
|
rel.participants.iter().map(|p| p.name.join("::")).collect();
|
|
|
|
let key = RelationshipKey::new(participant_names, rel.name.clone());
|
|
|
|
relationship_groups
|
|
.entry(key)
|
|
.or_default()
|
|
.push(RelationshipDecl {
|
|
relationship: rel.clone(),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Merge grouped relationships
|
|
let mut resolved = Vec::new();
|
|
for (key, decls) in relationship_groups {
|
|
let merged = merge_relationship_declarations(&key, decls)?;
|
|
resolved.push(merged);
|
|
}
|
|
|
|
Ok(resolved)
|
|
}
|
|
|
|
/// Merge multiple declarations of the same relationship
|
|
fn merge_relationship_declarations(
|
|
key: &RelationshipKey,
|
|
decls: Vec<RelationshipDecl>,
|
|
) -> Result<ResolvedRelationship> {
|
|
if decls.is_empty() {
|
|
return Err(ResolveError::ValidationError {
|
|
message: "Empty relationship group".to_string(),
|
|
help: Some("This is an internal error - relationship groups should never be empty. Please report this as a bug.".to_string()),
|
|
});
|
|
}
|
|
|
|
// Start with the first declaration
|
|
let base = &decls[0].relationship;
|
|
let mut participant_fields: Vec<ParticipantFields> = base
|
|
.participants
|
|
.iter()
|
|
.map(|p| ParticipantFields {
|
|
participant_name: p.name.clone(),
|
|
role: p.role.clone(),
|
|
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) {
|
|
// Merge participant fields
|
|
for participant in &decl.relationship.participants {
|
|
// Find this participant in our merged list
|
|
if let Some(pf_idx) = participant_fields
|
|
.iter()
|
|
.position(|pf| pf.participant_name == participant.name)
|
|
{
|
|
// Merge fields for this participant
|
|
merge_fields(
|
|
&mut participant_fields[pf_idx].fields,
|
|
participant.fields.clone(),
|
|
)?;
|
|
}
|
|
}
|
|
|
|
// Merge shared relationship fields
|
|
merge_fields(&mut merged_fields, decl.relationship.fields.clone())?;
|
|
}
|
|
|
|
Ok(ResolvedRelationship {
|
|
name: key.name.clone(),
|
|
participants: base.participants.clone(),
|
|
fields: merged_fields,
|
|
participant_fields,
|
|
})
|
|
}
|
|
|
|
/// Merge field lists, detecting conflicts
|
|
fn merge_fields(target: &mut Vec<Field>, source: Vec<Field>) -> Result<()> {
|
|
for new_field in source {
|
|
// Check if field already exists
|
|
if let Some(existing) = target.iter().find(|f| f.name == new_field.name) {
|
|
// Fields must have the same value
|
|
if existing.value != new_field.value {
|
|
return Err(ResolveError::ValidationError {
|
|
message: format!(
|
|
"Conflicting values for field '{}' in relationship",
|
|
new_field.name
|
|
),
|
|
help: Some(format!(
|
|
"The field '{}' has different values in different declarations of the same relationship. Make sure all declarations of this relationship use the same value for shared fields.",
|
|
new_field.name
|
|
)),
|
|
});
|
|
}
|
|
// Same value, no need to add again
|
|
} else {
|
|
// New field, add it
|
|
target.push(new_field);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::syntax::ast::{
|
|
Span,
|
|
Value,
|
|
};
|
|
|
|
fn make_participant(name: &str, role: Option<&str>) -> Participant {
|
|
Participant {
|
|
name: vec![name.to_string()],
|
|
role: role.map(|s| s.to_string()),
|
|
fields: vec![],
|
|
span: Span::new(0, 10),
|
|
}
|
|
}
|
|
|
|
fn make_field(name: &str, value: i64) -> Field {
|
|
Field {
|
|
name: name.to_string(),
|
|
value: Value::Number(value),
|
|
span: Span::new(0, 10),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_relationship_key_order_independent() {
|
|
let key1 = RelationshipKey::new(
|
|
vec!["Martha".to_string(), "David".to_string()],
|
|
"Marriage".to_string(),
|
|
);
|
|
let key2 = RelationshipKey::new(
|
|
vec!["David".to_string(), "Martha".to_string()],
|
|
"Marriage".to_string(),
|
|
);
|
|
assert_eq!(key1, key2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_single_relationship_declaration() {
|
|
let file = File {
|
|
declarations: vec![Declaration::Relationship(Relationship {
|
|
name: "Friendship".to_string(),
|
|
participants: vec![
|
|
make_participant("Alice", None),
|
|
make_participant("Bob", None),
|
|
],
|
|
fields: vec![make_field("bond", 80)],
|
|
span: Span::new(0, 10),
|
|
})],
|
|
};
|
|
|
|
let resolved = resolve_relationships(&file).unwrap();
|
|
assert_eq!(resolved.len(), 1);
|
|
assert_eq!(resolved[0].name, "Friendship");
|
|
assert_eq!(resolved[0].participants.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_relationship_merge() {
|
|
let mut martha_participant = make_participant("Martha", Some("spouse"));
|
|
martha_participant.fields = vec![make_field("commitment", 90)];
|
|
|
|
let mut david_participant = make_participant("David", Some("spouse"));
|
|
david_participant.fields = vec![make_field("trust", 85)];
|
|
|
|
let file = File {
|
|
declarations: vec![
|
|
Declaration::Relationship(Relationship {
|
|
name: "Marriage".to_string(),
|
|
participants: vec![
|
|
martha_participant.clone(),
|
|
make_participant("David", Some("spouse")),
|
|
],
|
|
fields: vec![],
|
|
span: Span::new(0, 10),
|
|
}),
|
|
Declaration::Relationship(Relationship {
|
|
name: "Marriage".to_string(),
|
|
participants: vec![
|
|
david_participant.clone(),
|
|
make_participant("Martha", Some("spouse")),
|
|
],
|
|
fields: vec![],
|
|
span: Span::new(20, 30),
|
|
}),
|
|
],
|
|
};
|
|
|
|
let resolved = resolve_relationships(&file).unwrap();
|
|
assert_eq!(resolved.len(), 1);
|
|
assert_eq!(resolved[0].name, "Marriage");
|
|
}
|
|
|
|
#[test]
|
|
fn test_conflicting_field_values() {
|
|
let mut p1 = make_participant("Alice", None);
|
|
p1.fields = vec![make_field("bond", 80)];
|
|
|
|
let mut p2 = make_participant("Alice", None);
|
|
p2.fields = vec![make_field("bond", 90)]; // Different value
|
|
|
|
let file = File {
|
|
declarations: vec![
|
|
Declaration::Relationship(Relationship {
|
|
name: "Test".to_string(),
|
|
participants: vec![p1, make_participant("Bob", None)],
|
|
fields: vec![],
|
|
span: Span::new(0, 10),
|
|
}),
|
|
Declaration::Relationship(Relationship {
|
|
name: "Test".to_string(),
|
|
participants: vec![p2, make_participant("Bob", None)],
|
|
fields: vec![],
|
|
span: Span::new(20, 30),
|
|
}),
|
|
],
|
|
};
|
|
|
|
let result = resolve_relationships(&file);
|
|
assert!(result.is_err());
|
|
}
|
|
}
|