rust_rule_engine/engine/
dependency.rs

1use crate::engine::rule::Rule;
2use std::collections::{HashMap, HashSet};
3
4/// Dependency analysis for safe parallel execution
5#[derive(Debug, Clone)]
6pub struct DependencyAnalyzer {
7    /// Rules that read from specific fields
8    readers: HashMap<String, Vec<String>>, // field -> rule_names
9    /// Rules that write to specific fields  
10    writers: HashMap<String, Vec<String>>, // field -> rule_names
11    /// Dependency graph: rule -> rules it depends on
12    dependencies: HashMap<String, HashSet<String>>,
13}
14
15impl Default for DependencyAnalyzer {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21impl DependencyAnalyzer {
22    /// Create new dependency analyzer
23    pub fn new() -> Self {
24        Self {
25            readers: HashMap::new(),
26            writers: HashMap::new(),
27            dependencies: HashMap::new(),
28        }
29    }
30
31    /// Analyze dependencies in a set of rules
32    pub fn analyze(&mut self, rules: &[Rule]) -> DependencyAnalysisResult {
33        self.clear();
34
35        // First pass: identify all reads and writes
36        for rule in rules {
37            self.analyze_rule_io(rule);
38        }
39
40        // Second pass: build dependency graph
41        self.build_dependency_graph();
42
43        // Third pass: identify conflicts
44        let conflicts = self.find_conflicts(rules);
45
46        // Fourth pass: group rules for safe parallel execution
47        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    /// Clear previous analysis
60    fn clear(&mut self) {
61        self.readers.clear();
62        self.writers.clear();
63        self.dependencies.clear();
64    }
65
66    /// Analyze what fields a rule reads from and writes to
67    fn analyze_rule_io(&mut self, rule: &Rule) {
68        // Analyze condition reads
69        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        // Analyze action writes
78        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    /// Extract field reads from rule conditions (proper implementation)
88    fn extract_condition_reads(&self, rule: &Rule) -> Vec<String> {
89        let mut reads = Vec::new();
90
91        // Extract from actual condition structure
92        Self::extract_fields_from_condition_group(&rule.conditions, &mut reads);
93
94        reads
95    }
96
97    /// Recursively extract fields from condition groups
98    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                // For EXISTS, we're reading the fields to check existence
115                Self::extract_fields_from_condition_group(inner, reads);
116            }
117            crate::engine::rule::ConditionGroup::Forall(inner) => {
118                // For FORALL, we're reading the fields to check all match
119                Self::extract_fields_from_condition_group(inner, reads);
120            }
121            crate::engine::rule::ConditionGroup::Accumulate { source_pattern, extract_field, .. } => {
122                // For ACCUMULATE, we're reading the source pattern and extract field
123                reads.push(format!("{}.{}", source_pattern, extract_field));
124            }
125        }
126    }
127
128    /// Extract field writes from rule actions (proper implementation)
129    fn extract_action_writes(&self, rule: &Rule) -> Vec<String> {
130        let mut writes = Vec::new();
131
132        // Analyze actual actions to find field writes
133        for action in &rule.actions {
134            match action {
135                crate::types::ActionType::Set { field, .. } => {
136                    writes.push(field.clone());
137                }
138                crate::types::ActionType::Update { object } => {
139                    writes.push(object.clone());
140                }
141                crate::types::ActionType::MethodCall { object, method, .. } => {
142                    // Method calls might modify the object
143                    writes.push(object.clone());
144
145                    // Some methods have predictable side effects
146                    if method.contains("set")
147                        || method.contains("update")
148                        || method.contains("modify")
149                        || method.contains("change")
150                    {
151                        writes.push(format!("{}.{}", object, method));
152                    }
153                }
154                crate::types::ActionType::Call { function, .. } => {
155                    // Analyze function calls for side effects
156                    writes.extend(self.analyze_function_side_effects(function));
157                }
158                crate::types::ActionType::Custom {
159                    action_type,
160                    params,
161                } => {
162                    // Check if custom action has a target field parameter
163                    if let Some(crate::types::Value::String(field)) = params.get("target_field") {
164                        writes.push(field.clone());
165                    }
166
167                    // Analyze custom action type for side effects
168                    writes.extend(self.analyze_custom_action_side_effects(action_type, params));
169                }
170                // Log doesn't modify fields
171                crate::types::ActionType::Log { .. } => {}
172                // Workflow actions don't modify facts directly
173                crate::types::ActionType::ActivateAgendaGroup { .. } => {}
174                crate::types::ActionType::ScheduleRule { .. } => {}
175                crate::types::ActionType::CompleteWorkflow { .. } => {}
176                crate::types::ActionType::SetWorkflowData { .. } => {}
177            }
178        }
179
180        writes
181    }
182
183    /// Analyze function calls for potential field writes
184    fn analyze_function_side_effects(&self, function_name: &str) -> Vec<String> {
185        let mut side_effects = Vec::new();
186
187        // Pattern matching for common function naming conventions
188        if function_name.starts_with("set") || function_name.starts_with("update") {
189            // setUserScore, updateOrderTotal, etc.
190            if let Some(field) = self.extract_field_from_function_name(function_name) {
191                side_effects.push(field);
192            }
193        } else if function_name.starts_with("calculate") || function_name.starts_with("compute") {
194            // calculateScore, computeTotal, etc.
195            if let Some(field) = self.extract_field_from_function_name(function_name) {
196                side_effects.push(field);
197            }
198        } else if function_name.contains("modify") || function_name.contains("change") {
199            // modifyUser, changeStatus, etc.
200            if let Some(field) = self.extract_field_from_function_name(function_name) {
201                side_effects.push(field);
202            }
203        }
204
205        side_effects
206    }
207
208    /// Analyze custom actions for potential field writes
209    fn analyze_custom_action_side_effects(
210        &self,
211        action_type: &str,
212        params: &std::collections::HashMap<String, crate::types::Value>,
213    ) -> Vec<String> {
214        let mut side_effects = Vec::new();
215
216        // Check for common parameter names that indicate field modification
217        for (key, value) in params {
218            if key == "field" || key == "target" || key == "output_field" {
219                if let crate::types::Value::String(field_name) = value {
220                    side_effects.push(field_name.clone());
221                }
222            }
223        }
224
225        // Pattern matching on action type
226        if action_type.contains("set")
227            || action_type.contains("update")
228            || action_type.contains("modify")
229            || action_type.contains("calculate")
230        {
231            // Extract potential field from action type name
232            if let Some(field) = self.extract_field_from_function_name(action_type) {
233                side_effects.push(field);
234            }
235        }
236
237        side_effects
238    }
239
240    /// Extract field name from function/action name using common patterns
241    fn extract_field_from_function_name(&self, name: &str) -> Option<String> {
242        // Convert camelCase/PascalCase to dot notation
243        // setUserScore -> User.Score
244        // calculateOrderTotal -> Order.Total
245        // updateVIPStatus -> VIP.Status
246
247        let name = name
248            .trim_start_matches("set")
249            .trim_start_matches("update")
250            .trim_start_matches("calculate")
251            .trim_start_matches("compute")
252            .trim_start_matches("modify")
253            .trim_start_matches("change");
254
255        // Simple pattern matching for common field patterns
256        if name.contains("User") && name.contains("Score") {
257            Some("User.Score".to_string())
258        } else if name.contains("User") && name.contains("VIP") {
259            Some("User.IsVIP".to_string())
260        } else if name.contains("Order") && name.contains("Total") {
261            Some("Order.Total".to_string())
262        } else if name.contains("Order") && name.contains("Amount") {
263            Some("Order.Amount".to_string())
264        } else if name.contains("Discount") {
265            Some("Order.DiscountRate".to_string())
266        } else {
267            // Generic field extraction from camelCase
268            self.convert_camel_case_to_field(name)
269        }
270    }
271
272    /// Convert camelCase to potential field name
273    fn convert_camel_case_to_field(&self, name: &str) -> Option<String> {
274        if name.is_empty() {
275            return None;
276        }
277
278        let mut result = String::new();
279        let chars = name.chars().peekable();
280
281        for c in chars {
282            if c.is_uppercase() && !result.is_empty() {
283                result.push('.');
284            }
285            result.push(c);
286        }
287
288        if result.contains('.') {
289            Some(result)
290        } else {
291            None
292        }
293    }
294
295    /// Build dependency graph based on read/write analysis
296    fn build_dependency_graph(&mut self) {
297        for (field, readers) in &self.readers {
298            if let Some(writers) = self.writers.get(field) {
299                // If rule A writes to field X and rule B reads from field X,
300                // then rule B depends on rule A
301                for reader in readers {
302                    for writer in writers {
303                        if reader != writer {
304                            self.dependencies
305                                .entry(reader.clone())
306                                .or_default()
307                                .insert(writer.clone());
308                        }
309                    }
310                }
311            }
312        }
313    }
314
315    /// Find rules that have conflicts (read/write or write/write to same field)
316    fn find_conflicts(&self, rules: &[Rule]) -> Vec<DependencyConflict> {
317        let mut conflicts = Vec::new();
318
319        // Group rules by salience
320        let mut salience_groups: HashMap<i32, Vec<&Rule>> = HashMap::new();
321        for rule in rules {
322            salience_groups.entry(rule.salience).or_default().push(rule);
323        }
324
325        // Check for conflicts within each salience group
326        for (salience, group_rules) in salience_groups {
327            if group_rules.len() <= 1 {
328                continue; // No conflicts possible with single rule
329            }
330
331            // Check for write-write conflicts
332            let mut field_writers: HashMap<String, Vec<String>> = HashMap::new();
333            for rule in &group_rules {
334                let writes = self.extract_action_writes(rule);
335                for field in writes {
336                    field_writers
337                        .entry(field)
338                        .or_default()
339                        .push(rule.name.clone());
340                }
341            }
342
343            for (field, writers) in field_writers {
344                if writers.len() > 1 {
345                    conflicts.push(DependencyConflict {
346                        conflict_type: ConflictType::WriteWrite,
347                        field: field.clone(),
348                        rules: writers,
349                        salience,
350                        description: format!("Multiple rules write to {}", field),
351                    });
352                }
353            }
354
355            // Check for read-write conflicts
356            for rule in &group_rules {
357                let reads = self.extract_condition_reads(rule);
358                for field in &reads {
359                    if let Some(writers) = self.writers.get(field) {
360                        let conflicting_writers: Vec<String> = writers
361                            .iter()
362                            .filter(|writer| {
363                                group_rules
364                                    .iter()
365                                    .any(|r| r.name == **writer && r.name != rule.name)
366                            })
367                            .cloned()
368                            .collect();
369
370                        if !conflicting_writers.is_empty() {
371                            let mut involved_rules = conflicting_writers.clone();
372                            involved_rules.push(rule.name.clone());
373
374                            conflicts.push(DependencyConflict {
375                                conflict_type: ConflictType::ReadWrite,
376                                field: field.clone(),
377                                rules: involved_rules,
378                                salience,
379                                description: format!(
380                                    "Rule {} reads {} while others write to it",
381                                    rule.name, field
382                                ),
383                            });
384                        }
385                    }
386                }
387            }
388        }
389
390        conflicts
391    }
392
393    /// Create execution groups for safe parallel execution
394    fn create_execution_groups(&self, rules: &[Rule]) -> Vec<ExecutionGroup> {
395        let mut groups = Vec::new();
396
397        // Group by salience first
398        let mut salience_groups: HashMap<i32, Vec<Rule>> = HashMap::new();
399        for rule in rules {
400            salience_groups
401                .entry(rule.salience)
402                .or_default()
403                .push(rule.clone());
404        }
405
406        // Process each salience level
407        let mut salience_levels: Vec<_> = salience_groups.keys().copied().collect();
408        salience_levels.sort_by(|a, b| b.cmp(a)); // Descending order
409
410        for salience in salience_levels {
411            let rules_at_level = &salience_groups[&salience];
412
413            if rules_at_level.len() == 1 {
414                // Single rule - always safe
415                groups.push(ExecutionGroup {
416                    rules: rules_at_level.clone(),
417                    execution_mode: ExecutionMode::Sequential,
418                    salience,
419                    can_parallelize: false,
420                    conflicts: Vec::new(),
421                });
422            } else {
423                // Multiple rules - check for conflicts
424                let conflicts = self.find_conflicts(rules_at_level);
425                let can_parallelize = conflicts.is_empty();
426
427                groups.push(ExecutionGroup {
428                    rules: rules_at_level.clone(),
429                    execution_mode: if can_parallelize {
430                        ExecutionMode::Parallel
431                    } else {
432                        ExecutionMode::Sequential
433                    },
434                    salience,
435                    can_parallelize,
436                    conflicts,
437                });
438            }
439        }
440
441        groups
442    }
443}
444
445/// Result of dependency analysis
446#[derive(Debug, Clone)]
447pub struct DependencyAnalysisResult {
448    /// Total number of rules analyzed
449    pub total_rules: usize,
450    /// Number of conflicts found
451    pub conflicts: usize,
452    /// Detailed conflict information
453    pub conflict_details: Vec<DependencyConflict>,
454    /// Recommended execution groups
455    pub execution_groups: Vec<ExecutionGroup>,
456    /// Whether rules can be safely parallelized
457    pub can_parallelize_safely: bool,
458}
459
460/// A conflict between rules
461#[derive(Debug, Clone)]
462pub struct DependencyConflict {
463    /// Type of conflict
464    pub conflict_type: ConflictType,
465    /// Field that causes the conflict
466    pub field: String,
467    /// Rules involved in the conflict
468    pub rules: Vec<String>,
469    /// Salience level where conflict occurs
470    pub salience: i32,
471    /// Human-readable description
472    pub description: String,
473}
474
475/// Type of dependency conflict
476#[derive(Debug, Clone, PartialEq)]
477pub enum ConflictType {
478    /// Multiple rules write to the same field
479    WriteWrite,
480    /// One rule reads while another writes to the same field
481    ReadWrite,
482    /// Circular dependency
483    Circular,
484}
485
486/// Execution group with parallelization recommendation
487#[derive(Debug, Clone)]
488pub struct ExecutionGroup {
489    /// Rules in this group
490    pub rules: Vec<Rule>,
491    /// Recommended execution mode
492    pub execution_mode: ExecutionMode,
493    /// Salience level
494    pub salience: i32,
495    /// Whether this group can be safely parallelized
496    pub can_parallelize: bool,
497    /// Conflicts preventing parallelization
498    pub conflicts: Vec<DependencyConflict>,
499}
500
501/// Execution mode recommendation
502#[derive(Debug, Clone, PartialEq)]
503pub enum ExecutionMode {
504    /// Safe to run in parallel
505    Parallel,
506    /// Must run sequentially due to dependencies
507    Sequential,
508}
509
510/// Strategy used for execution
511#[derive(Debug, Clone, PartialEq)]
512pub enum ExecutionStrategy {
513    /// All rules executed sequentially (due to dependencies)
514    FullSequential,
515    /// All rules executed in parallel (no dependencies)
516    FullParallel,
517    /// Mixed execution (some parallel, some sequential)
518    Hybrid,
519    /// Forced sequential due to configuration
520    ForcedSequential,
521}
522
523impl DependencyAnalysisResult {
524    /// Get a summary report
525    pub fn get_summary(&self) -> String {
526        format!(
527            "šŸ“Š Dependency Analysis Summary:\n   Total rules: {}\n   Conflicts found: {}\n   Safe for parallel: {}\n   Execution groups: {}",
528            self.total_rules,
529            self.conflicts,
530            if self.can_parallelize_safely { "āœ… Yes" } else { "āŒ No" },
531            self.execution_groups.len()
532        )
533    }
534
535    /// Get detailed report
536    pub fn get_detailed_report(&self) -> String {
537        let mut report = self.get_summary();
538        report.push_str("\n\nšŸ” Detailed Analysis:");
539
540        for (i, group) in self.execution_groups.iter().enumerate() {
541            report.push_str(&format!(
542                "\n\nšŸ“‹ Group {} (Salience {}):",
543                i + 1,
544                group.salience
545            ));
546            report.push_str(&format!(
547                "\n   Mode: {:?} | Can parallelize: {}",
548                group.execution_mode,
549                if group.can_parallelize { "āœ…" } else { "āŒ" }
550            ));
551            report.push_str(&format!(
552                "\n   Rules: {}",
553                group
554                    .rules
555                    .iter()
556                    .map(|r| r.name.as_str())
557                    .collect::<Vec<_>>()
558                    .join(", ")
559            ));
560
561            if !group.conflicts.is_empty() {
562                report.push_str("\n   🚨 Conflicts:");
563                for conflict in &group.conflicts {
564                    report.push_str(&format!(
565                        "\n      - {}: {} (rules: {})",
566                        match conflict.conflict_type {
567                            ConflictType::WriteWrite => "Write-Write",
568                            ConflictType::ReadWrite => "Read-Write",
569                            ConflictType::Circular => "Circular",
570                        },
571                        conflict.field,
572                        conflict.rules.join(", ")
573                    ));
574                }
575            }
576        }
577
578        report
579    }
580}
581
582#[cfg(test)]
583mod tests {
584    use super::*;
585    use crate::engine::rule::{Condition, ConditionGroup};
586
587    #[test]
588    fn test_dependency_analyzer_creation() {
589        let analyzer = DependencyAnalyzer::new();
590        assert!(analyzer.readers.is_empty());
591        assert!(analyzer.writers.is_empty());
592        assert!(analyzer.dependencies.is_empty());
593    }
594
595    #[test]
596    fn test_safe_rules_analysis() {
597        let mut analyzer = DependencyAnalyzer::new();
598
599        let rules = vec![
600            Rule::new(
601                "AgeValidation".to_string(),
602                ConditionGroup::Single(Condition::new(
603                    "User.Age".to_string(),
604                    crate::types::Operator::GreaterThan,
605                    crate::types::Value::Integer(18),
606                )),
607                vec![],
608            ),
609            Rule::new(
610                "CountryCheck".to_string(),
611                ConditionGroup::Single(Condition::new(
612                    "User.Country".to_string(),
613                    crate::types::Operator::Equal,
614                    crate::types::Value::String("US".to_string()),
615                )),
616                vec![],
617            ),
618        ];
619
620        let result = analyzer.analyze(&rules);
621        assert_eq!(result.total_rules, 2);
622        assert_eq!(result.conflicts, 0);
623        assert!(result.can_parallelize_safely);
624    }
625
626    #[test]
627    fn test_conflicting_rules_analysis() {
628        let mut analyzer = DependencyAnalyzer::new();
629
630        let rules = vec![
631            Rule::new(
632                "CalculateScore".to_string(),
633                ConditionGroup::Single(Condition::new(
634                    "User.Data".to_string(),
635                    crate::types::Operator::Equal,
636                    crate::types::Value::String("valid".to_string()),
637                )),
638                vec![crate::types::ActionType::Set {
639                    field: "User.Score".to_string(),
640                    value: crate::types::Value::Integer(85),
641                }],
642            ),
643            Rule::new(
644                "CheckVIPStatus".to_string(),
645                ConditionGroup::Single(Condition::new(
646                    "User.Score".to_string(),
647                    crate::types::Operator::GreaterThan,
648                    crate::types::Value::Integer(80),
649                )),
650                vec![crate::types::ActionType::Set {
651                    field: "User.IsVIP".to_string(),
652                    value: crate::types::Value::Boolean(true),
653                }],
654            ),
655        ];
656
657        let result = analyzer.analyze(&rules);
658        assert_eq!(result.total_rules, 2);
659        // Should detect conflicts between score calculation and VIP check
660        assert!(!result.can_parallelize_safely);
661    }
662}