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::Retract { object } => {
139                    // Retract removes a fact, mark it as a write
140                    writes.push(format!("_retracted_{}", object));
141                }
142                crate::types::ActionType::MethodCall { object, method, .. } => {
143                    // Method calls might modify the object
144                    writes.push(object.clone());
145
146                    // Some methods have predictable side effects
147                    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                    // Analyze function calls for side effects
157                    writes.extend(self.analyze_function_side_effects(function));
158                }
159                crate::types::ActionType::Custom {
160                    action_type,
161                    params,
162                } => {
163                    // Check if custom action has a target field parameter
164                    if let Some(crate::types::Value::String(field)) = params.get("target_field") {
165                        writes.push(field.clone());
166                    }
167
168                    // Analyze custom action type for side effects
169                    writes.extend(self.analyze_custom_action_side_effects(action_type, params));
170                }
171                // Log doesn't modify fields
172                crate::types::ActionType::Log { .. } => {}
173                // Workflow actions don't modify facts directly
174                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    /// Analyze function calls for potential field writes
185    fn analyze_function_side_effects(&self, function_name: &str) -> Vec<String> {
186        let mut side_effects = Vec::new();
187
188        // Pattern matching for common function naming conventions
189        if function_name.starts_with("set") || function_name.starts_with("update") {
190            // setUserScore, updateOrderTotal, etc.
191            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            // calculateScore, computeTotal, etc.
196            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            // modifyUser, changeStatus, etc.
201            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    /// Analyze custom actions for potential field writes
210    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        // Check for common parameter names that indicate field modification
218        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        // Pattern matching on action type
227        if action_type.contains("set")
228            || action_type.contains("update")
229            || action_type.contains("modify")
230            || action_type.contains("calculate")
231        {
232            // Extract potential field from action type name
233            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    /// Extract field name from function/action name using common patterns
242    fn extract_field_from_function_name(&self, name: &str) -> Option<String> {
243        // Convert camelCase/PascalCase to dot notation
244        // setUserScore -> User.Score
245        // calculateOrderTotal -> Order.Total
246        // updateVIPStatus -> VIP.Status
247
248        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        // Simple pattern matching for common field patterns
257        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            // Generic field extraction from camelCase
269            self.convert_camel_case_to_field(name)
270        }
271    }
272
273    /// Convert camelCase to potential field name
274    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    /// Build dependency graph based on read/write analysis
297    fn build_dependency_graph(&mut self) {
298        for (field, readers) in &self.readers {
299            if let Some(writers) = self.writers.get(field) {
300                // If rule A writes to field X and rule B reads from field X,
301                // then rule B depends on rule A
302                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    /// Find rules that have conflicts (read/write or write/write to same field)
317    fn find_conflicts(&self, rules: &[Rule]) -> Vec<DependencyConflict> {
318        let mut conflicts = Vec::new();
319
320        // Group rules by salience
321        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        // Check for conflicts within each salience group
327        for (salience, group_rules) in salience_groups {
328            if group_rules.len() <= 1 {
329                continue; // No conflicts possible with single rule
330            }
331
332            // Check for write-write conflicts
333            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            // Check for read-write conflicts
357            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    /// Create execution groups for safe parallel execution
395    fn create_execution_groups(&self, rules: &[Rule]) -> Vec<ExecutionGroup> {
396        let mut groups = Vec::new();
397
398        // Group by salience first
399        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        // Process each salience level
408        let mut salience_levels: Vec<_> = salience_groups.keys().copied().collect();
409        salience_levels.sort_by(|a, b| b.cmp(a)); // Descending order
410
411        for salience in salience_levels {
412            let rules_at_level = &salience_groups[&salience];
413
414            if rules_at_level.len() == 1 {
415                // Single rule - always safe
416                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                // Multiple rules - check for conflicts
425                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/// Result of dependency analysis
447#[derive(Debug, Clone)]
448pub struct DependencyAnalysisResult {
449    /// Total number of rules analyzed
450    pub total_rules: usize,
451    /// Number of conflicts found
452    pub conflicts: usize,
453    /// Detailed conflict information
454    pub conflict_details: Vec<DependencyConflict>,
455    /// Recommended execution groups
456    pub execution_groups: Vec<ExecutionGroup>,
457    /// Whether rules can be safely parallelized
458    pub can_parallelize_safely: bool,
459}
460
461/// A conflict between rules
462#[derive(Debug, Clone)]
463pub struct DependencyConflict {
464    /// Type of conflict
465    pub conflict_type: ConflictType,
466    /// Field that causes the conflict
467    pub field: String,
468    /// Rules involved in the conflict
469    pub rules: Vec<String>,
470    /// Salience level where conflict occurs
471    pub salience: i32,
472    /// Human-readable description
473    pub description: String,
474}
475
476/// Type of dependency conflict
477#[derive(Debug, Clone, PartialEq)]
478pub enum ConflictType {
479    /// Multiple rules write to the same field
480    WriteWrite,
481    /// One rule reads while another writes to the same field
482    ReadWrite,
483    /// Circular dependency
484    Circular,
485}
486
487/// Execution group with parallelization recommendation
488#[derive(Debug, Clone)]
489pub struct ExecutionGroup {
490    /// Rules in this group
491    pub rules: Vec<Rule>,
492    /// Recommended execution mode
493    pub execution_mode: ExecutionMode,
494    /// Salience level
495    pub salience: i32,
496    /// Whether this group can be safely parallelized
497    pub can_parallelize: bool,
498    /// Conflicts preventing parallelization
499    pub conflicts: Vec<DependencyConflict>,
500}
501
502/// Execution mode recommendation
503#[derive(Debug, Clone, PartialEq)]
504pub enum ExecutionMode {
505    /// Safe to run in parallel
506    Parallel,
507    /// Must run sequentially due to dependencies
508    Sequential,
509}
510
511/// Strategy used for execution
512#[derive(Debug, Clone, PartialEq)]
513pub enum ExecutionStrategy {
514    /// All rules executed sequentially (due to dependencies)
515    FullSequential,
516    /// All rules executed in parallel (no dependencies)
517    FullParallel,
518    /// Mixed execution (some parallel, some sequential)
519    Hybrid,
520    /// Forced sequential due to configuration
521    ForcedSequential,
522}
523
524impl DependencyAnalysisResult {
525    /// Get a summary report
526    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    /// Get detailed report
537    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        // Should detect conflicts between score calculation and VIP check
661        assert!(!result.can_parallelize_safely);
662    }
663}