1use crate::engine::rule::Rule;
2use std::collections::{HashMap, HashSet};
3
4#[derive(Debug, Clone)]
6pub struct DependencyAnalyzer {
7 readers: HashMap<String, Vec<String>>, writers: HashMap<String, Vec<String>>, dependencies: HashMap<String, HashSet<String>>,
13}
14
15impl Default for DependencyAnalyzer {
16 fn default() -> Self {
17 Self::new()
18 }
19}
20
21impl DependencyAnalyzer {
22 pub fn new() -> Self {
24 Self {
25 readers: HashMap::new(),
26 writers: HashMap::new(),
27 dependencies: HashMap::new(),
28 }
29 }
30
31 pub fn analyze(&mut self, rules: &[Rule]) -> DependencyAnalysisResult {
33 self.clear();
34
35 for rule in rules {
37 self.analyze_rule_io(rule);
38 }
39
40 self.build_dependency_graph();
42
43 let conflicts = self.find_conflicts(rules);
45
46 let execution_groups = self.create_execution_groups(rules);
48 let conflicts_len = conflicts.len();
49
50 DependencyAnalysisResult {
51 total_rules: rules.len(),
52 conflicts: conflicts_len,
53 conflict_details: conflicts,
54 execution_groups,
55 can_parallelize_safely: conflicts_len == 0,
56 }
57 }
58
59 fn clear(&mut self) {
61 self.readers.clear();
62 self.writers.clear();
63 self.dependencies.clear();
64 }
65
66 fn analyze_rule_io(&mut self, rule: &Rule) {
68 let condition_reads = self.extract_condition_reads(rule);
70 for field in condition_reads {
71 self.readers
72 .entry(field)
73 .or_default()
74 .push(rule.name.clone());
75 }
76
77 let action_writes = self.extract_action_writes(rule);
79 for field in action_writes {
80 self.writers
81 .entry(field)
82 .or_default()
83 .push(rule.name.clone());
84 }
85 }
86
87 fn extract_condition_reads(&self, rule: &Rule) -> Vec<String> {
89 let mut reads = Vec::new();
90
91 Self::extract_fields_from_condition_group(&rule.conditions, &mut reads);
93
94 reads
95 }
96
97 fn extract_fields_from_condition_group(
99 condition_group: &crate::engine::rule::ConditionGroup,
100 reads: &mut Vec<String>,
101 ) {
102 match condition_group {
103 crate::engine::rule::ConditionGroup::Single(condition) => {
104 reads.push(condition.field.clone());
105 }
106 crate::engine::rule::ConditionGroup::Compound { left, right, .. } => {
107 Self::extract_fields_from_condition_group(left, reads);
108 Self::extract_fields_from_condition_group(right, reads);
109 }
110 crate::engine::rule::ConditionGroup::Not(inner) => {
111 Self::extract_fields_from_condition_group(inner, reads);
112 }
113 crate::engine::rule::ConditionGroup::Exists(inner) => {
114 Self::extract_fields_from_condition_group(inner, reads);
116 }
117 crate::engine::rule::ConditionGroup::Forall(inner) => {
118 Self::extract_fields_from_condition_group(inner, reads);
120 }
121 crate::engine::rule::ConditionGroup::Accumulate { source_pattern, extract_field, .. } => {
122 reads.push(format!("{}.{}", source_pattern, extract_field));
124 }
125 }
126 }
127
128 fn extract_action_writes(&self, rule: &Rule) -> Vec<String> {
130 let mut writes = Vec::new();
131
132 for action in &rule.actions {
134 match action {
135 crate::types::ActionType::Set { field, .. } => {
136 writes.push(field.clone());
137 }
138 crate::types::ActionType::Retract { object } => {
139 writes.push(format!("_retracted_{}", object));
141 }
142 crate::types::ActionType::MethodCall { object, method, .. } => {
143 writes.push(object.clone());
145
146 if method.contains("set")
148 || method.contains("update")
149 || method.contains("modify")
150 || method.contains("change")
151 {
152 writes.push(format!("{}.{}", object, method));
153 }
154 }
155 crate::types::ActionType::Call { function, .. } => {
156 writes.extend(self.analyze_function_side_effects(function));
158 }
159 crate::types::ActionType::Custom {
160 action_type,
161 params,
162 } => {
163 if let Some(crate::types::Value::String(field)) = params.get("target_field") {
165 writes.push(field.clone());
166 }
167
168 writes.extend(self.analyze_custom_action_side_effects(action_type, params));
170 }
171 crate::types::ActionType::Log { .. } => {}
173 crate::types::ActionType::ActivateAgendaGroup { .. } => {}
175 crate::types::ActionType::ScheduleRule { .. } => {}
176 crate::types::ActionType::CompleteWorkflow { .. } => {}
177 crate::types::ActionType::SetWorkflowData { .. } => {}
178 }
179 }
180
181 writes
182 }
183
184 fn analyze_function_side_effects(&self, function_name: &str) -> Vec<String> {
186 let mut side_effects = Vec::new();
187
188 if function_name.starts_with("set") || function_name.starts_with("update") {
190 if let Some(field) = self.extract_field_from_function_name(function_name) {
192 side_effects.push(field);
193 }
194 } else if function_name.starts_with("calculate") || function_name.starts_with("compute") {
195 if let Some(field) = self.extract_field_from_function_name(function_name) {
197 side_effects.push(field);
198 }
199 } else if function_name.contains("modify") || function_name.contains("change") {
200 if let Some(field) = self.extract_field_from_function_name(function_name) {
202 side_effects.push(field);
203 }
204 }
205
206 side_effects
207 }
208
209 fn analyze_custom_action_side_effects(
211 &self,
212 action_type: &str,
213 params: &std::collections::HashMap<String, crate::types::Value>,
214 ) -> Vec<String> {
215 let mut side_effects = Vec::new();
216
217 for (key, value) in params {
219 if key == "field" || key == "target" || key == "output_field" {
220 if let crate::types::Value::String(field_name) = value {
221 side_effects.push(field_name.clone());
222 }
223 }
224 }
225
226 if action_type.contains("set")
228 || action_type.contains("update")
229 || action_type.contains("modify")
230 || action_type.contains("calculate")
231 {
232 if let Some(field) = self.extract_field_from_function_name(action_type) {
234 side_effects.push(field);
235 }
236 }
237
238 side_effects
239 }
240
241 fn extract_field_from_function_name(&self, name: &str) -> Option<String> {
243 let name = name
249 .trim_start_matches("set")
250 .trim_start_matches("update")
251 .trim_start_matches("calculate")
252 .trim_start_matches("compute")
253 .trim_start_matches("modify")
254 .trim_start_matches("change");
255
256 if name.contains("User") && name.contains("Score") {
258 Some("User.Score".to_string())
259 } else if name.contains("User") && name.contains("VIP") {
260 Some("User.IsVIP".to_string())
261 } else if name.contains("Order") && name.contains("Total") {
262 Some("Order.Total".to_string())
263 } else if name.contains("Order") && name.contains("Amount") {
264 Some("Order.Amount".to_string())
265 } else if name.contains("Discount") {
266 Some("Order.DiscountRate".to_string())
267 } else {
268 self.convert_camel_case_to_field(name)
270 }
271 }
272
273 fn convert_camel_case_to_field(&self, name: &str) -> Option<String> {
275 if name.is_empty() {
276 return None;
277 }
278
279 let mut result = String::new();
280 let chars = name.chars().peekable();
281
282 for c in chars {
283 if c.is_uppercase() && !result.is_empty() {
284 result.push('.');
285 }
286 result.push(c);
287 }
288
289 if result.contains('.') {
290 Some(result)
291 } else {
292 None
293 }
294 }
295
296 fn build_dependency_graph(&mut self) {
298 for (field, readers) in &self.readers {
299 if let Some(writers) = self.writers.get(field) {
300 for reader in readers {
303 for writer in writers {
304 if reader != writer {
305 self.dependencies
306 .entry(reader.clone())
307 .or_default()
308 .insert(writer.clone());
309 }
310 }
311 }
312 }
313 }
314 }
315
316 fn find_conflicts(&self, rules: &[Rule]) -> Vec<DependencyConflict> {
318 let mut conflicts = Vec::new();
319
320 let mut salience_groups: HashMap<i32, Vec<&Rule>> = HashMap::new();
322 for rule in rules {
323 salience_groups.entry(rule.salience).or_default().push(rule);
324 }
325
326 for (salience, group_rules) in salience_groups {
328 if group_rules.len() <= 1 {
329 continue; }
331
332 let mut field_writers: HashMap<String, Vec<String>> = HashMap::new();
334 for rule in &group_rules {
335 let writes = self.extract_action_writes(rule);
336 for field in writes {
337 field_writers
338 .entry(field)
339 .or_default()
340 .push(rule.name.clone());
341 }
342 }
343
344 for (field, writers) in field_writers {
345 if writers.len() > 1 {
346 conflicts.push(DependencyConflict {
347 conflict_type: ConflictType::WriteWrite,
348 field: field.clone(),
349 rules: writers,
350 salience,
351 description: format!("Multiple rules write to {}", field),
352 });
353 }
354 }
355
356 for rule in &group_rules {
358 let reads = self.extract_condition_reads(rule);
359 for field in &reads {
360 if let Some(writers) = self.writers.get(field) {
361 let conflicting_writers: Vec<String> = writers
362 .iter()
363 .filter(|writer| {
364 group_rules
365 .iter()
366 .any(|r| r.name == **writer && r.name != rule.name)
367 })
368 .cloned()
369 .collect();
370
371 if !conflicting_writers.is_empty() {
372 let mut involved_rules = conflicting_writers.clone();
373 involved_rules.push(rule.name.clone());
374
375 conflicts.push(DependencyConflict {
376 conflict_type: ConflictType::ReadWrite,
377 field: field.clone(),
378 rules: involved_rules,
379 salience,
380 description: format!(
381 "Rule {} reads {} while others write to it",
382 rule.name, field
383 ),
384 });
385 }
386 }
387 }
388 }
389 }
390
391 conflicts
392 }
393
394 fn create_execution_groups(&self, rules: &[Rule]) -> Vec<ExecutionGroup> {
396 let mut groups = Vec::new();
397
398 let mut salience_groups: HashMap<i32, Vec<Rule>> = HashMap::new();
400 for rule in rules {
401 salience_groups
402 .entry(rule.salience)
403 .or_default()
404 .push(rule.clone());
405 }
406
407 let mut salience_levels: Vec<_> = salience_groups.keys().copied().collect();
409 salience_levels.sort_by(|a, b| b.cmp(a)); for salience in salience_levels {
412 let rules_at_level = &salience_groups[&salience];
413
414 if rules_at_level.len() == 1 {
415 groups.push(ExecutionGroup {
417 rules: rules_at_level.clone(),
418 execution_mode: ExecutionMode::Sequential,
419 salience,
420 can_parallelize: false,
421 conflicts: Vec::new(),
422 });
423 } else {
424 let conflicts = self.find_conflicts(rules_at_level);
426 let can_parallelize = conflicts.is_empty();
427
428 groups.push(ExecutionGroup {
429 rules: rules_at_level.clone(),
430 execution_mode: if can_parallelize {
431 ExecutionMode::Parallel
432 } else {
433 ExecutionMode::Sequential
434 },
435 salience,
436 can_parallelize,
437 conflicts,
438 });
439 }
440 }
441
442 groups
443 }
444}
445
446#[derive(Debug, Clone)]
448pub struct DependencyAnalysisResult {
449 pub total_rules: usize,
451 pub conflicts: usize,
453 pub conflict_details: Vec<DependencyConflict>,
455 pub execution_groups: Vec<ExecutionGroup>,
457 pub can_parallelize_safely: bool,
459}
460
461#[derive(Debug, Clone)]
463pub struct DependencyConflict {
464 pub conflict_type: ConflictType,
466 pub field: String,
468 pub rules: Vec<String>,
470 pub salience: i32,
472 pub description: String,
474}
475
476#[derive(Debug, Clone, PartialEq)]
478pub enum ConflictType {
479 WriteWrite,
481 ReadWrite,
483 Circular,
485}
486
487#[derive(Debug, Clone)]
489pub struct ExecutionGroup {
490 pub rules: Vec<Rule>,
492 pub execution_mode: ExecutionMode,
494 pub salience: i32,
496 pub can_parallelize: bool,
498 pub conflicts: Vec<DependencyConflict>,
500}
501
502#[derive(Debug, Clone, PartialEq)]
504pub enum ExecutionMode {
505 Parallel,
507 Sequential,
509}
510
511#[derive(Debug, Clone, PartialEq)]
513pub enum ExecutionStrategy {
514 FullSequential,
516 FullParallel,
518 Hybrid,
520 ForcedSequential,
522}
523
524impl DependencyAnalysisResult {
525 pub fn get_summary(&self) -> String {
527 format!(
528 "š Dependency Analysis Summary:\n Total rules: {}\n Conflicts found: {}\n Safe for parallel: {}\n Execution groups: {}",
529 self.total_rules,
530 self.conflicts,
531 if self.can_parallelize_safely { "ā
Yes" } else { "ā No" },
532 self.execution_groups.len()
533 )
534 }
535
536 pub fn get_detailed_report(&self) -> String {
538 let mut report = self.get_summary();
539 report.push_str("\n\nš Detailed Analysis:");
540
541 for (i, group) in self.execution_groups.iter().enumerate() {
542 report.push_str(&format!(
543 "\n\nš Group {} (Salience {}):",
544 i + 1,
545 group.salience
546 ));
547 report.push_str(&format!(
548 "\n Mode: {:?} | Can parallelize: {}",
549 group.execution_mode,
550 if group.can_parallelize { "ā
" } else { "ā" }
551 ));
552 report.push_str(&format!(
553 "\n Rules: {}",
554 group
555 .rules
556 .iter()
557 .map(|r| r.name.as_str())
558 .collect::<Vec<_>>()
559 .join(", ")
560 ));
561
562 if !group.conflicts.is_empty() {
563 report.push_str("\n šØ Conflicts:");
564 for conflict in &group.conflicts {
565 report.push_str(&format!(
566 "\n - {}: {} (rules: {})",
567 match conflict.conflict_type {
568 ConflictType::WriteWrite => "Write-Write",
569 ConflictType::ReadWrite => "Read-Write",
570 ConflictType::Circular => "Circular",
571 },
572 conflict.field,
573 conflict.rules.join(", ")
574 ));
575 }
576 }
577 }
578
579 report
580 }
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586 use crate::engine::rule::{Condition, ConditionGroup};
587
588 #[test]
589 fn test_dependency_analyzer_creation() {
590 let analyzer = DependencyAnalyzer::new();
591 assert!(analyzer.readers.is_empty());
592 assert!(analyzer.writers.is_empty());
593 assert!(analyzer.dependencies.is_empty());
594 }
595
596 #[test]
597 fn test_safe_rules_analysis() {
598 let mut analyzer = DependencyAnalyzer::new();
599
600 let rules = vec![
601 Rule::new(
602 "AgeValidation".to_string(),
603 ConditionGroup::Single(Condition::new(
604 "User.Age".to_string(),
605 crate::types::Operator::GreaterThan,
606 crate::types::Value::Integer(18),
607 )),
608 vec![],
609 ),
610 Rule::new(
611 "CountryCheck".to_string(),
612 ConditionGroup::Single(Condition::new(
613 "User.Country".to_string(),
614 crate::types::Operator::Equal,
615 crate::types::Value::String("US".to_string()),
616 )),
617 vec![],
618 ),
619 ];
620
621 let result = analyzer.analyze(&rules);
622 assert_eq!(result.total_rules, 2);
623 assert_eq!(result.conflicts, 0);
624 assert!(result.can_parallelize_safely);
625 }
626
627 #[test]
628 fn test_conflicting_rules_analysis() {
629 let mut analyzer = DependencyAnalyzer::new();
630
631 let rules = vec![
632 Rule::new(
633 "CalculateScore".to_string(),
634 ConditionGroup::Single(Condition::new(
635 "User.Data".to_string(),
636 crate::types::Operator::Equal,
637 crate::types::Value::String("valid".to_string()),
638 )),
639 vec![crate::types::ActionType::Set {
640 field: "User.Score".to_string(),
641 value: crate::types::Value::Integer(85),
642 }],
643 ),
644 Rule::new(
645 "CheckVIPStatus".to_string(),
646 ConditionGroup::Single(Condition::new(
647 "User.Score".to_string(),
648 crate::types::Operator::GreaterThan,
649 crate::types::Value::Integer(80),
650 )),
651 vec![crate::types::ActionType::Set {
652 field: "User.IsVIP".to_string(),
653 value: crate::types::Value::Boolean(true),
654 }],
655 ),
656 ];
657
658 let result = analyzer.analyze(&rules);
659 assert_eq!(result.total_rules, 2);
660 assert!(!result.can_parallelize_safely);
662 }
663}