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::Custom {
156 action_type,
157 params,
158 } => {
159 if let Some(crate::types::Value::String(field)) = params.get("target_field") {
161 writes.push(field.clone());
162 }
163
164 writes.extend(self.analyze_custom_action_side_effects(action_type, params));
166 }
167 crate::types::ActionType::Log { .. } => {}
169 crate::types::ActionType::ActivateAgendaGroup { .. } => {}
171 crate::types::ActionType::ScheduleRule { .. } => {}
172 crate::types::ActionType::CompleteWorkflow { .. } => {}
173 crate::types::ActionType::SetWorkflowData { .. } => {}
174 }
175 }
176
177 writes
178 }
179
180 fn analyze_function_side_effects(&self, function_name: &str) -> Vec<String> {
182 let mut side_effects = Vec::new();
183
184 if function_name.starts_with("set") || function_name.starts_with("update") {
186 if let Some(field) = self.extract_field_from_function_name(function_name) {
188 side_effects.push(field);
189 }
190 } else if function_name.starts_with("calculate") || function_name.starts_with("compute") {
191 if let Some(field) = self.extract_field_from_function_name(function_name) {
193 side_effects.push(field);
194 }
195 } else if function_name.contains("modify") || function_name.contains("change") {
196 if let Some(field) = self.extract_field_from_function_name(function_name) {
198 side_effects.push(field);
199 }
200 }
201
202 side_effects
203 }
204
205 fn analyze_custom_action_side_effects(
207 &self,
208 action_type: &str,
209 params: &std::collections::HashMap<String, crate::types::Value>,
210 ) -> Vec<String> {
211 let mut side_effects = Vec::new();
212
213 for (key, value) in params {
215 if key == "field" || key == "target" || key == "output_field" {
216 if let crate::types::Value::String(field_name) = value {
217 side_effects.push(field_name.clone());
218 }
219 }
220 }
221
222 if action_type.contains("set")
224 || action_type.contains("update")
225 || action_type.contains("modify")
226 || action_type.contains("calculate")
227 {
228 if let Some(field) = self.extract_field_from_function_name(action_type) {
230 side_effects.push(field);
231 }
232 }
233
234 side_effects
235 }
236
237 fn extract_field_from_function_name(&self, name: &str) -> Option<String> {
239 let name = name
245 .trim_start_matches("set")
246 .trim_start_matches("update")
247 .trim_start_matches("calculate")
248 .trim_start_matches("compute")
249 .trim_start_matches("modify")
250 .trim_start_matches("change");
251
252 if name.contains("User") && name.contains("Score") {
254 Some("User.Score".to_string())
255 } else if name.contains("User") && name.contains("VIP") {
256 Some("User.IsVIP".to_string())
257 } else if name.contains("Order") && name.contains("Total") {
258 Some("Order.Total".to_string())
259 } else if name.contains("Order") && name.contains("Amount") {
260 Some("Order.Amount".to_string())
261 } else if name.contains("Discount") {
262 Some("Order.DiscountRate".to_string())
263 } else {
264 self.convert_camel_case_to_field(name)
266 }
267 }
268
269 fn convert_camel_case_to_field(&self, name: &str) -> Option<String> {
271 if name.is_empty() {
272 return None;
273 }
274
275 let mut result = String::new();
276 let chars = name.chars().peekable();
277
278 for c in chars {
279 if c.is_uppercase() && !result.is_empty() {
280 result.push('.');
281 }
282 result.push(c);
283 }
284
285 if result.contains('.') {
286 Some(result)
287 } else {
288 None
289 }
290 }
291
292 fn build_dependency_graph(&mut self) {
294 for (field, readers) in &self.readers {
295 if let Some(writers) = self.writers.get(field) {
296 for reader in readers {
299 for writer in writers {
300 if reader != writer {
301 self.dependencies
302 .entry(reader.clone())
303 .or_default()
304 .insert(writer.clone());
305 }
306 }
307 }
308 }
309 }
310 }
311
312 fn find_conflicts(&self, rules: &[Rule]) -> Vec<DependencyConflict> {
314 let mut conflicts = Vec::new();
315
316 let mut salience_groups: HashMap<i32, Vec<&Rule>> = HashMap::new();
318 for rule in rules {
319 salience_groups.entry(rule.salience).or_default().push(rule);
320 }
321
322 for (salience, group_rules) in salience_groups {
324 if group_rules.len() <= 1 {
325 continue; }
327
328 let mut field_writers: HashMap<String, Vec<String>> = HashMap::new();
330 for rule in &group_rules {
331 let writes = self.extract_action_writes(rule);
332 for field in writes {
333 field_writers
334 .entry(field)
335 .or_default()
336 .push(rule.name.clone());
337 }
338 }
339
340 for (field, writers) in field_writers {
341 if writers.len() > 1 {
342 conflicts.push(DependencyConflict {
343 conflict_type: ConflictType::WriteWrite,
344 field: field.clone(),
345 rules: writers,
346 salience,
347 description: format!("Multiple rules write to {}", field),
348 });
349 }
350 }
351
352 for rule in &group_rules {
354 let reads = self.extract_condition_reads(rule);
355 for field in &reads {
356 if let Some(writers) = self.writers.get(field) {
357 let conflicting_writers: Vec<String> = writers
358 .iter()
359 .filter(|writer| {
360 group_rules
361 .iter()
362 .any(|r| r.name == **writer && r.name != rule.name)
363 })
364 .cloned()
365 .collect();
366
367 if !conflicting_writers.is_empty() {
368 let mut involved_rules = conflicting_writers.clone();
369 involved_rules.push(rule.name.clone());
370
371 conflicts.push(DependencyConflict {
372 conflict_type: ConflictType::ReadWrite,
373 field: field.clone(),
374 rules: involved_rules,
375 salience,
376 description: format!(
377 "Rule {} reads {} while others write to it",
378 rule.name, field
379 ),
380 });
381 }
382 }
383 }
384 }
385 }
386
387 conflicts
388 }
389
390 fn create_execution_groups(&self, rules: &[Rule]) -> Vec<ExecutionGroup> {
392 let mut groups = Vec::new();
393
394 let mut salience_groups: HashMap<i32, Vec<Rule>> = HashMap::new();
396 for rule in rules {
397 salience_groups
398 .entry(rule.salience)
399 .or_default()
400 .push(rule.clone());
401 }
402
403 let mut salience_levels: Vec<_> = salience_groups.keys().copied().collect();
405 salience_levels.sort_by(|a, b| b.cmp(a)); for salience in salience_levels {
408 let rules_at_level = &salience_groups[&salience];
409
410 if rules_at_level.len() == 1 {
411 groups.push(ExecutionGroup {
413 rules: rules_at_level.clone(),
414 execution_mode: ExecutionMode::Sequential,
415 salience,
416 can_parallelize: false,
417 conflicts: Vec::new(),
418 });
419 } else {
420 let conflicts = self.find_conflicts(rules_at_level);
422 let can_parallelize = conflicts.is_empty();
423
424 groups.push(ExecutionGroup {
425 rules: rules_at_level.clone(),
426 execution_mode: if can_parallelize {
427 ExecutionMode::Parallel
428 } else {
429 ExecutionMode::Sequential
430 },
431 salience,
432 can_parallelize,
433 conflicts,
434 });
435 }
436 }
437
438 groups
439 }
440}
441
442#[derive(Debug, Clone)]
444pub struct DependencyAnalysisResult {
445 pub total_rules: usize,
447 pub conflicts: usize,
449 pub conflict_details: Vec<DependencyConflict>,
451 pub execution_groups: Vec<ExecutionGroup>,
453 pub can_parallelize_safely: bool,
455}
456
457#[derive(Debug, Clone)]
459pub struct DependencyConflict {
460 pub conflict_type: ConflictType,
462 pub field: String,
464 pub rules: Vec<String>,
466 pub salience: i32,
468 pub description: String,
470}
471
472#[derive(Debug, Clone, PartialEq)]
474pub enum ConflictType {
475 WriteWrite,
477 ReadWrite,
479 Circular,
481}
482
483#[derive(Debug, Clone)]
485pub struct ExecutionGroup {
486 pub rules: Vec<Rule>,
488 pub execution_mode: ExecutionMode,
490 pub salience: i32,
492 pub can_parallelize: bool,
494 pub conflicts: Vec<DependencyConflict>,
496}
497
498#[derive(Debug, Clone, PartialEq)]
500pub enum ExecutionMode {
501 Parallel,
503 Sequential,
505}
506
507#[derive(Debug, Clone, PartialEq)]
509pub enum ExecutionStrategy {
510 FullSequential,
512 FullParallel,
514 Hybrid,
516 ForcedSequential,
518}
519
520impl DependencyAnalysisResult {
521 pub fn get_summary(&self) -> String {
523 format!(
524 "š Dependency Analysis Summary:\n Total rules: {}\n Conflicts found: {}\n Safe for parallel: {}\n Execution groups: {}",
525 self.total_rules,
526 self.conflicts,
527 if self.can_parallelize_safely { "ā
Yes" } else { "ā No" },
528 self.execution_groups.len()
529 )
530 }
531
532 pub fn get_detailed_report(&self) -> String {
534 let mut report = self.get_summary();
535 report.push_str("\n\nš Detailed Analysis:");
536
537 for (i, group) in self.execution_groups.iter().enumerate() {
538 report.push_str(&format!(
539 "\n\nš Group {} (Salience {}):",
540 i + 1,
541 group.salience
542 ));
543 report.push_str(&format!(
544 "\n Mode: {:?} | Can parallelize: {}",
545 group.execution_mode,
546 if group.can_parallelize { "ā
" } else { "ā" }
547 ));
548 report.push_str(&format!(
549 "\n Rules: {}",
550 group
551 .rules
552 .iter()
553 .map(|r| r.name.as_str())
554 .collect::<Vec<_>>()
555 .join(", ")
556 ));
557
558 if !group.conflicts.is_empty() {
559 report.push_str("\n šØ Conflicts:");
560 for conflict in &group.conflicts {
561 report.push_str(&format!(
562 "\n - {}: {} (rules: {})",
563 match conflict.conflict_type {
564 ConflictType::WriteWrite => "Write-Write",
565 ConflictType::ReadWrite => "Read-Write",
566 ConflictType::Circular => "Circular",
567 },
568 conflict.field,
569 conflict.rules.join(", ")
570 ));
571 }
572 }
573 }
574
575 report
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582 use crate::engine::rule::{Condition, ConditionGroup};
583
584 #[test]
585 fn test_dependency_analyzer_creation() {
586 let analyzer = DependencyAnalyzer::new();
587 assert!(analyzer.readers.is_empty());
588 assert!(analyzer.writers.is_empty());
589 assert!(analyzer.dependencies.is_empty());
590 }
591
592 #[test]
593 fn test_safe_rules_analysis() {
594 let mut analyzer = DependencyAnalyzer::new();
595
596 let rules = vec![
597 Rule::new(
598 "AgeValidation".to_string(),
599 ConditionGroup::Single(Condition::new(
600 "User.Age".to_string(),
601 crate::types::Operator::GreaterThan,
602 crate::types::Value::Integer(18),
603 )),
604 vec![],
605 ),
606 Rule::new(
607 "CountryCheck".to_string(),
608 ConditionGroup::Single(Condition::new(
609 "User.Country".to_string(),
610 crate::types::Operator::Equal,
611 crate::types::Value::String("US".to_string()),
612 )),
613 vec![],
614 ),
615 ];
616
617 let result = analyzer.analyze(&rules);
618 assert_eq!(result.total_rules, 2);
619 assert_eq!(result.conflicts, 0);
620 assert!(result.can_parallelize_safely);
621 }
622
623 #[test]
624 fn test_conflicting_rules_analysis() {
625 let mut analyzer = DependencyAnalyzer::new();
626
627 let rules = vec![
628 Rule::new(
629 "CalculateScore".to_string(),
630 ConditionGroup::Single(Condition::new(
631 "User.Data".to_string(),
632 crate::types::Operator::Equal,
633 crate::types::Value::String("valid".to_string()),
634 )),
635 vec![crate::types::ActionType::Set {
636 field: "User.Score".to_string(),
637 value: crate::types::Value::Integer(85),
638 }],
639 ),
640 Rule::new(
641 "CheckVIPStatus".to_string(),
642 ConditionGroup::Single(Condition::new(
643 "User.Score".to_string(),
644 crate::types::Operator::GreaterThan,
645 crate::types::Value::Integer(80),
646 )),
647 vec![crate::types::ActionType::Set {
648 field: "User.IsVIP".to_string(),
649 value: crate::types::Value::Boolean(true),
650 }],
651 ),
652 ];
653
654 let result = analyzer.analyze(&rules);
655 assert_eq!(result.total_rules, 2);
656 assert!(!result.can_parallelize_safely);
658 }
659}