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