rust_rule_engine/rete/
propagation.rs

1//! Incremental Propagation Engine (P3 Feature - Advanced)
2//!
3//! This module implements incremental updates similar to Drools:
4//! - Only propagate changed facts through the network
5//! - Track affected rules and activations
6//! - Efficient re-evaluation after updates
7
8use std::collections::{HashMap, HashSet};
9use super::working_memory::{WorkingMemory, FactHandle};
10use super::network::{ReteUlNode, TypedReteUlRule};
11use super::facts::TypedFacts;
12use super::agenda::{AdvancedAgenda, Activation};
13use super::template::TemplateRegistry;
14use super::globals::GlobalsRegistry;
15
16/// Track which rules are affected by which fact types
17#[derive(Debug)]
18pub struct RuleDependencyGraph {
19    /// Map: fact_type -> set of rule indices that depend on it
20    fact_type_to_rules: HashMap<String, HashSet<usize>>,
21    /// Map: rule index -> set of fact types it depends on
22    rule_to_fact_types: HashMap<usize, HashSet<String>>,
23}
24
25impl RuleDependencyGraph {
26    /// Create new dependency graph
27    pub fn new() -> Self {
28        Self {
29            fact_type_to_rules: HashMap::new(),
30            rule_to_fact_types: HashMap::new(),
31        }
32    }
33
34    /// Add dependency: rule depends on fact type
35    pub fn add_dependency(&mut self, rule_idx: usize, fact_type: String) {
36        self.fact_type_to_rules
37            .entry(fact_type.clone())
38            .or_insert_with(HashSet::new)
39            .insert(rule_idx);
40
41        self.rule_to_fact_types
42            .entry(rule_idx)
43            .or_insert_with(HashSet::new)
44            .insert(fact_type);
45    }
46
47    /// Get rules affected by a fact type change
48    pub fn get_affected_rules(&self, fact_type: &str) -> HashSet<usize> {
49        self.fact_type_to_rules
50            .get(fact_type)
51            .cloned()
52            .unwrap_or_else(HashSet::new)
53    }
54
55    /// Get fact types that a rule depends on
56    pub fn get_rule_dependencies(&self, rule_idx: usize) -> HashSet<String> {
57        self.rule_to_fact_types
58            .get(&rule_idx)
59            .cloned()
60            .unwrap_or_else(HashSet::new)
61    }
62}
63
64impl Default for RuleDependencyGraph {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70/// Incremental Propagation Engine
71/// Only re-evaluates rules affected by changed facts
72pub struct IncrementalEngine {
73    /// Working memory
74    working_memory: WorkingMemory,
75    /// Rules
76    rules: Vec<TypedReteUlRule>,
77    /// Dependency graph
78    dependencies: RuleDependencyGraph,
79    /// Advanced agenda
80    agenda: AdvancedAgenda,
81    /// Track which facts each rule last matched
82    rule_matched_facts: HashMap<usize, HashSet<FactHandle>>,
83    /// Template registry for type-safe facts
84    templates: TemplateRegistry,
85    /// Global variables registry
86    globals: GlobalsRegistry,
87}
88
89impl IncrementalEngine {
90    /// Create new incremental engine
91    pub fn new() -> Self {
92        Self {
93            working_memory: WorkingMemory::new(),
94            rules: Vec::new(),
95            dependencies: RuleDependencyGraph::new(),
96            agenda: AdvancedAgenda::new(),
97            rule_matched_facts: HashMap::new(),
98            templates: TemplateRegistry::new(),
99            globals: GlobalsRegistry::new(),
100        }
101    }
102
103    /// Add rule and register its dependencies
104    pub fn add_rule(&mut self, rule: TypedReteUlRule, depends_on: Vec<String>) {
105        let rule_idx = self.rules.len();
106
107        // Register dependencies
108        for fact_type in depends_on {
109            self.dependencies.add_dependency(rule_idx, fact_type);
110        }
111
112        self.rules.push(rule);
113    }
114
115    /// Insert fact into working memory
116    pub fn insert(&mut self, fact_type: String, data: TypedFacts) -> FactHandle {
117        let handle = self.working_memory.insert(fact_type.clone(), data);
118
119        // Trigger incremental propagation for this fact type
120        self.propagate_changes_for_type(&fact_type);
121
122        handle
123    }
124
125    /// Update fact in working memory
126    pub fn update(&mut self, handle: FactHandle, data: TypedFacts) -> Result<(), String> {
127        // Get fact type before update
128        let fact_type = self.working_memory
129            .get(&handle)
130            .map(|f| f.fact_type.clone())
131            .ok_or_else(|| format!("FactHandle {} not found", handle))?;
132
133        self.working_memory.update(handle, data)?;
134
135        // Trigger incremental propagation for this fact type
136        self.propagate_changes_for_type(&fact_type);
137
138        Ok(())
139    }
140
141    /// Retract fact from working memory
142    pub fn retract(&mut self, handle: FactHandle) -> Result<(), String> {
143        // Get fact type before retract
144        let fact_type = self.working_memory
145            .get(&handle)
146            .map(|f| f.fact_type.clone())
147            .ok_or_else(|| format!("FactHandle {} not found", handle))?;
148
149        self.working_memory.retract(handle)?;
150
151        // Trigger incremental propagation for this fact type
152        self.propagate_changes_for_type(&fact_type);
153
154        Ok(())
155    }
156
157    /// Propagate changes for a specific fact type (incremental!)
158    fn propagate_changes_for_type(&mut self, fact_type: &str) {
159        // Get affected rules
160        let affected_rules = self.dependencies.get_affected_rules(fact_type);
161
162        if affected_rules.is_empty() {
163            return; // No rules depend on this fact type
164        }
165
166        // Flatten working memory to TypedFacts for evaluation
167        let facts = self.working_memory.to_typed_facts();
168
169        // Re-evaluate only affected rules
170        for &rule_idx in &affected_rules {
171            let rule = &self.rules[rule_idx];
172
173            // Evaluate rule condition
174            let matches = super::network::evaluate_rete_ul_node_typed(&rule.node, &facts);
175
176            if matches {
177                // Create activation
178                let activation = Activation::new(rule.name.clone(), rule.priority)
179                    .with_no_loop(rule.no_loop);
180
181                self.agenda.add_activation(activation);
182            }
183        }
184    }
185
186    /// Fire all pending activations
187    pub fn fire_all(&mut self) -> Vec<String> {
188        let mut fired_rules = Vec::new();
189
190        while let Some(activation) = self.agenda.get_next_activation() {
191            // Find rule
192            if let Some((idx, rule)) = self.rules
193                .iter_mut()
194                .enumerate()
195                .find(|(_, r)| r.name == activation.rule_name)
196            {
197                // Execute action
198                let mut facts = self.working_memory.to_typed_facts();
199                (rule.action)(&mut facts);
200
201                // Track fired rule
202                fired_rules.push(activation.rule_name.clone());
203                self.agenda.mark_rule_fired(&activation);
204
205                // TODO: Update working memory with changed facts
206                // This is complex and would require tracking what changed
207            }
208        }
209
210        fired_rules
211    }
212
213    /// Get working memory
214    pub fn working_memory(&self) -> &WorkingMemory {
215        &self.working_memory
216    }
217
218    /// Get mutable working memory
219    pub fn working_memory_mut(&mut self) -> &mut WorkingMemory {
220        &mut self.working_memory
221    }
222
223    /// Get agenda
224    pub fn agenda(&self) -> &AdvancedAgenda {
225        &self.agenda
226    }
227
228    /// Get mutable agenda
229    pub fn agenda_mut(&mut self) -> &mut AdvancedAgenda {
230        &mut self.agenda
231    }
232
233    /// Get statistics
234    pub fn stats(&self) -> IncrementalEngineStats {
235        IncrementalEngineStats {
236            rules: self.rules.len(),
237            working_memory: self.working_memory.stats(),
238            agenda: self.agenda.stats(),
239            dependencies: self.dependencies.fact_type_to_rules.len(),
240        }
241    }
242
243    /// Clear fired flags and reset agenda
244    pub fn reset(&mut self) {
245        self.agenda.reset_fired_flags();
246    }
247
248    /// Get template registry
249    pub fn templates(&self) -> &TemplateRegistry {
250        &self.templates
251    }
252
253    /// Get mutable template registry
254    pub fn templates_mut(&mut self) -> &mut TemplateRegistry {
255        &mut self.templates
256    }
257
258    /// Get global variables registry
259    pub fn globals(&self) -> &GlobalsRegistry {
260        &self.globals
261    }
262
263    /// Get mutable global variables registry
264    pub fn globals_mut(&mut self) -> &mut GlobalsRegistry {
265        &mut self.globals
266    }
267
268    /// Insert a typed fact with template validation
269    pub fn insert_with_template(
270        &mut self,
271        template_name: &str,
272        data: TypedFacts,
273    ) -> crate::errors::Result<FactHandle> {
274        // Validate against template
275        self.templates.validate(template_name, &data)?;
276
277        // Insert into working memory
278        Ok(self.insert(template_name.to_string(), data))
279    }
280}
281
282impl Default for IncrementalEngine {
283    fn default() -> Self {
284        Self::new()
285    }
286}
287
288/// Engine statistics
289#[derive(Debug)]
290pub struct IncrementalEngineStats {
291    pub rules: usize,
292    pub working_memory: super::working_memory::WorkingMemoryStats,
293    pub agenda: super::agenda::AgendaStats,
294    pub dependencies: usize,
295}
296
297impl std::fmt::Display for IncrementalEngineStats {
298    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299        write!(
300            f,
301            "Engine Stats: {} rules, {} fact types tracked\nWM: {}\nAgenda: {}",
302            self.rules,
303            self.dependencies,
304            self.working_memory,
305            self.agenda
306        )
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use crate::rete::network::ReteUlNode;
314    use crate::rete::alpha::AlphaNode;
315
316    #[test]
317    fn test_dependency_graph() {
318        let mut graph = RuleDependencyGraph::new();
319
320        graph.add_dependency(0, "Person".to_string());
321        graph.add_dependency(1, "Person".to_string());
322        graph.add_dependency(1, "Order".to_string());
323
324        let affected = graph.get_affected_rules("Person");
325        assert_eq!(affected.len(), 2);
326        assert!(affected.contains(&0));
327        assert!(affected.contains(&1));
328
329        let deps = graph.get_rule_dependencies(1);
330        assert_eq!(deps.len(), 2);
331        assert!(deps.contains("Person"));
332        assert!(deps.contains("Order"));
333    }
334
335    #[test]
336    fn test_incremental_propagation() {
337        let mut engine = IncrementalEngine::new();
338
339        // Add rule that depends on "Person" type
340        let node = ReteUlNode::UlAlpha(AlphaNode {
341            field: "Person.age".to_string(),
342            operator: ">".to_string(),
343            value: "18".to_string(),
344        });
345
346        let rule = TypedReteUlRule {
347            name: "IsAdult".to_string(),
348            node,
349            priority: 0,
350            no_loop: true,
351            action: Box::new(|_| {}),
352        };
353
354        engine.add_rule(rule, vec!["Person".to_string()]);
355
356        // Insert Person fact
357        let mut person = TypedFacts::new();
358        person.set("age", 25i64);
359        let handle = engine.insert("Person".to_string(), person);
360
361        // Check that rule was activated
362        let stats = engine.stats();
363        assert!(stats.agenda.total_activations > 0);
364
365        // Update person
366        let mut updated = TypedFacts::new();
367        updated.set("age", 15i64); // Now under 18
368        engine.update(handle, updated).unwrap();
369
370        // Rule should be re-evaluated (incrementally)
371    }
372}