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