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