Skip to main content

ipfrs_tensorlogic/
recursive_reasoning.rs

1//! Recursive Query Support with Tabling
2//!
3//! This module implements advanced recursive query handling including:
4//! - Tabling/tabulation for efficient recursive queries
5//! - Stratified evaluation
6//! - Support for left-recursive rules
7//! - Fixpoint computation
8//!
9//! # Tabling
10//!
11//! Tabling (also called tabled resolution or SLG resolution) is a technique
12//! for evaluating logic programs that improves on standard SLD resolution
13//! by memoizing intermediate results and detecting loops.
14//!
15//! # Example
16//!
17//! ```
18//! use ipfrs_tensorlogic::{TabledInferenceEngine, KnowledgeBase, Predicate, Rule, Term, Constant};
19//!
20//! let mut kb = KnowledgeBase::new();
21//!
22//! // Define ancestor relation: ancestor(X, Y) :- parent(X, Y).
23//! // ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).
24//! // This is recursive and benefits from tabling
25//!
26//! // Add parent facts
27//! kb.add_fact(Predicate::new("parent".to_string(), vec![
28//!     Term::Const(Constant::String("alice".to_string())),
29//!     Term::Const(Constant::String("bob".to_string())),
30//! ]));
31//! kb.add_fact(Predicate::new("parent".to_string(), vec![
32//!     Term::Const(Constant::String("bob".to_string())),
33//!     Term::Const(Constant::String("charlie".to_string())),
34//! ]));
35//!
36//! // Add base rule: ancestor(X, Y) :- parent(X, Y)
37//! kb.add_rule(Rule::new(
38//!     Predicate::new("ancestor".to_string(), vec![
39//!         Term::Var("X".to_string()),
40//!         Term::Var("Y".to_string()),
41//!     ]),
42//!     vec![Predicate::new("parent".to_string(), vec![
43//!         Term::Var("X".to_string()),
44//!         Term::Var("Y".to_string()),
45//!     ])],
46//! ));
47//!
48//! // Add recursive rule: ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z)
49//! kb.add_rule(Rule::new(
50//!     Predicate::new("ancestor".to_string(), vec![
51//!         Term::Var("X".to_string()),
52//!         Term::Var("Z".to_string()),
53//!     ]),
54//!     vec![
55//!         Predicate::new("parent".to_string(), vec![
56//!             Term::Var("X".to_string()),
57//!             Term::Var("Y".to_string()),
58//!         ]),
59//!         Predicate::new("ancestor".to_string(), vec![
60//!             Term::Var("Y".to_string()),
61//!             Term::Var("Z".to_string()),
62//!         ]),
63//!     ],
64//! ));
65//!
66//! // Create tabled engine
67//! let engine = TabledInferenceEngine::new();
68//!
69//! // Query for all ancestors of alice
70//! let goal = Predicate::new("ancestor".to_string(), vec![
71//!     Term::Const(Constant::String("alice".to_string())),
72//!     Term::Var("Z".to_string()),
73//! ]);
74//!
75//! let solutions = engine.query(&goal, &kb).expect("example: should succeed in docs");
76//! // Should find at least bob as an ancestor
77//! assert!(!solutions.is_empty());
78//! ```
79
80use crate::ir::{KnowledgeBase, Predicate, Rule};
81use crate::reasoning::{apply_subst_predicate, unify_predicates, Substitution};
82use ipfrs_core::error::Result;
83use std::collections::{HashMap, HashSet};
84
85/// Table entry for memoized subgoals
86#[derive(Debug, Clone)]
87struct TableEntry {
88    /// The subgoal being solved
89    #[allow(dead_code)]
90    goal: Predicate,
91    /// Solutions found so far
92    solutions: Vec<Substitution>,
93    /// Whether this entry is complete
94    complete: bool,
95    /// Depth at which this was tabled
96    #[allow(dead_code)]
97    depth: usize,
98}
99
100/// Tabled inference engine using SLG resolution
101pub struct TabledInferenceEngine {
102    /// Table for memoizing subgoals
103    table: HashMap<String, TableEntry>,
104    /// Maximum depth
105    max_depth: usize,
106    /// Maximum solutions per subgoal
107    max_solutions: usize,
108}
109
110impl TabledInferenceEngine {
111    /// Create a new tabled inference engine
112    pub fn new() -> Self {
113        Self {
114            table: HashMap::new(),
115            max_depth: 100,
116            max_solutions: 1000,
117        }
118    }
119
120    /// Create with custom limits
121    pub fn with_limits(max_depth: usize, max_solutions: usize) -> Self {
122        Self {
123            table: HashMap::new(),
124            max_depth,
125            max_solutions,
126        }
127    }
128
129    /// Query with tabling
130    pub fn query(&self, goal: &Predicate, kb: &KnowledgeBase) -> Result<Vec<Substitution>> {
131        let mut engine = Self {
132            table: HashMap::new(),
133            max_depth: self.max_depth,
134            max_solutions: self.max_solutions,
135        };
136
137        engine.solve_tabled(goal, &Substitution::new(), kb, 0)
138    }
139
140    /// Solve a goal with tabling
141    fn solve_tabled(
142        &mut self,
143        goal: &Predicate,
144        subst: &Substitution,
145        kb: &KnowledgeBase,
146        depth: usize,
147    ) -> Result<Vec<Substitution>> {
148        // Check depth limit
149        if depth > self.max_depth {
150            return Ok(Vec::new());
151        }
152
153        // Apply substitution to goal
154        let goal = apply_subst_predicate(goal, subst);
155
156        // Create table key
157        let key = self.goal_key(&goal);
158
159        // Check if goal is already tabled
160        if let Some(entry) = self.table.get(&key) {
161            // If complete, return cached solutions
162            if entry.complete {
163                return Ok(entry.solutions.clone());
164            }
165            // If incomplete, we have a loop - return empty for now
166            return Ok(Vec::new());
167        }
168
169        // Create new table entry
170        let mut entry = TableEntry {
171            goal: goal.clone(),
172            solutions: Vec::new(),
173            complete: false,
174            depth,
175        };
176
177        // Insert incomplete entry to detect loops
178        self.table.insert(key.clone(), entry.clone());
179
180        // Solve using standard backward chaining
181        let mut solutions = Vec::new();
182
183        // Try facts
184        for fact in kb.get_predicates(&goal.name) {
185            if let Some(new_subst) = unify_predicates(&goal, fact, &Substitution::new()) {
186                solutions.push(new_subst);
187                if solutions.len() >= self.max_solutions {
188                    break;
189                }
190            }
191        }
192
193        // Try rules
194        for rule in kb.get_rules(&goal.name) {
195            if solutions.len() >= self.max_solutions {
196                break;
197            }
198
199            // Rename variables in rule
200            let renamed_rule = self.rename_rule(rule, depth);
201
202            // Try to unify with rule head
203            if let Some(new_subst) =
204                unify_predicates(&goal, &renamed_rule.head, &Substitution::new())
205            {
206                // Solve rule body
207                let body_solutions =
208                    self.solve_conjunction(&renamed_rule.body, &new_subst, kb, depth + 1)?;
209                solutions.extend(body_solutions);
210            }
211        }
212
213        // Mark entry as complete and update solutions
214        entry.solutions = solutions.clone();
215        entry.complete = true;
216        self.table.insert(key, entry);
217
218        Ok(solutions)
219    }
220
221    /// Solve a conjunction of goals
222    fn solve_conjunction(
223        &mut self,
224        goals: &[Predicate],
225        subst: &Substitution,
226        kb: &KnowledgeBase,
227        depth: usize,
228    ) -> Result<Vec<Substitution>> {
229        if goals.is_empty() {
230            return Ok(vec![subst.clone()]);
231        }
232
233        let first = &goals[0];
234        let rest = &goals[1..];
235
236        let first_solutions = self.solve_tabled(first, subst, kb, depth)?;
237
238        let mut all_solutions = Vec::new();
239        for first_subst in first_solutions {
240            let rest_solutions = self.solve_conjunction(rest, &first_subst, kb, depth)?;
241            all_solutions.extend(rest_solutions);
242
243            if all_solutions.len() >= self.max_solutions {
244                break;
245            }
246        }
247
248        Ok(all_solutions)
249    }
250
251    /// Generate a unique key for a goal
252    fn goal_key(&self, goal: &Predicate) -> String {
253        format!("{}({})", goal.name, goal.args.len())
254    }
255
256    /// Rename variables in a rule
257    fn rename_rule(&self, rule: &Rule, suffix: usize) -> Rule {
258        let var_map: HashMap<String, String> = rule
259            .variables()
260            .into_iter()
261            .map(|v| (v.clone(), format!("{}_{}", v, suffix)))
262            .collect();
263
264        let rename_subst: Substitution = var_map
265            .into_iter()
266            .map(|(old, new)| (old, crate::ir::Term::Var(new)))
267            .collect();
268
269        Rule {
270            head: apply_subst_predicate(&rule.head, &rename_subst),
271            body: rule
272                .body
273                .iter()
274                .map(|p| apply_subst_predicate(p, &rename_subst))
275                .collect(),
276        }
277    }
278
279    /// Get table statistics
280    pub fn table_stats(&self) -> TableStats {
281        TableStats {
282            entries: self.table.len(),
283            complete_entries: self.table.values().filter(|e| e.complete).count(),
284            total_solutions: self.table.values().map(|e| e.solutions.len()).sum(),
285        }
286    }
287
288    /// Clear the table
289    pub fn clear_table(&mut self) {
290        self.table.clear();
291    }
292}
293
294impl Default for TabledInferenceEngine {
295    fn default() -> Self {
296        Self::new()
297    }
298}
299
300/// Statistics about the tabling system
301#[derive(Debug, Clone)]
302pub struct TableStats {
303    /// Number of table entries
304    pub entries: usize,
305    /// Number of complete entries
306    pub complete_entries: usize,
307    /// Total solutions across all entries
308    pub total_solutions: usize,
309}
310
311/// Fixpoint computation for stratified programs
312pub struct FixpointEngine {
313    /// Maximum iterations for fixpoint
314    max_iterations: usize,
315}
316
317impl FixpointEngine {
318    /// Create a new fixpoint engine
319    pub fn new() -> Self {
320        Self {
321            max_iterations: 100,
322        }
323    }
324
325    /// Create with custom iteration limit
326    pub fn with_max_iterations(max_iterations: usize) -> Self {
327        Self { max_iterations }
328    }
329
330    /// Compute fixpoint for a set of rules
331    pub fn compute_fixpoint(&self, kb: &KnowledgeBase) -> Result<KnowledgeBase> {
332        let mut current_kb = kb.clone();
333        let mut iteration = 0;
334
335        loop {
336            iteration += 1;
337            if iteration > self.max_iterations {
338                break;
339            }
340
341            let mut new_facts = Vec::new();
342            let mut changed = false;
343
344            // Apply all rules to derive new facts
345            // Collect unique predicate names from rules
346            let predicate_names: std::collections::HashSet<String> = current_kb
347                .rules
348                .iter()
349                .map(|r| r.head.name.clone())
350                .collect();
351
352            for predicate_name in predicate_names {
353                for rule in current_kb.get_rules(&predicate_name) {
354                    let derived = self.derive_facts_from_rule(rule, &current_kb)?;
355                    for fact in derived {
356                        // Check if fact already exists
357                        if !current_kb.facts.contains(&fact) {
358                            new_facts.push(fact);
359                            changed = true;
360                        }
361                    }
362                }
363            }
364
365            // Add new facts to KB
366            for fact in new_facts {
367                current_kb.add_fact(fact);
368            }
369
370            // If no new facts, we've reached fixpoint
371            if !changed {
372                break;
373            }
374        }
375
376        Ok(current_kb)
377    }
378
379    /// Derive all ground facts entailed by a single rule given the current KB.
380    ///
381    /// Uses backward-chaining via `solve_body` to collect every complete
382    /// substitution that satisfies the rule body, then applies each
383    /// substitution to the rule head to produce a new ground fact.
384    fn derive_facts_from_rule(&self, rule: &Rule, kb: &KnowledgeBase) -> Result<Vec<Predicate>> {
385        // Collect all substitutions that satisfy the entire body.
386        let body_solutions = self.solve_body(&rule.body, &Substitution::new(), kb, 0)?;
387
388        let mut derived = Vec::new();
389        for subst in body_solutions {
390            let grounded_head = apply_subst_predicate(&rule.head, &subst);
391            // Only emit fully-ground facts (no residual variables).
392            if !self.has_variables(&grounded_head) {
393                derived.push(grounded_head);
394            }
395        }
396        Ok(derived)
397    }
398
399    /// Solve a conjunction of body goals and return all satisfying substitutions.
400    fn solve_body(
401        &self,
402        goals: &[Predicate],
403        subst: &Substitution,
404        kb: &KnowledgeBase,
405        depth: usize,
406    ) -> Result<Vec<Substitution>> {
407        if depth > self.max_iterations {
408            return Ok(Vec::new());
409        }
410        if goals.is_empty() {
411            return Ok(vec![subst.clone()]);
412        }
413
414        let current_goal = apply_subst_predicate(&goals[0], subst);
415        let rest = &goals[1..];
416
417        let mut all_solutions: Vec<Substitution> = Vec::new();
418
419        // Match against ground facts in the KB.
420        for fact in kb.get_predicates(&current_goal.name) {
421            if let Some(new_subst) = unify_predicates(&current_goal, fact, subst) {
422                let tail_solutions = self.solve_body(rest, &new_subst, kb, depth + 1)?;
423                all_solutions.extend(tail_solutions);
424            }
425        }
426
427        // Match against rule heads (backward chaining into rules).
428        for rule in kb.get_rules(&current_goal.name) {
429            // Rename variables to avoid collisions.
430            let suffix = depth * 1000 + all_solutions.len();
431            let renamed = self.rename_rule_fixpoint(rule, suffix);
432            if let Some(new_subst) = unify_predicates(&current_goal, &renamed.head, subst) {
433                // Prepend the rule body in front of the remaining goals.
434                let mut combined: Vec<Predicate> = renamed.body.clone();
435                combined.extend_from_slice(rest);
436                let tail_solutions = self.solve_body(&combined, &new_subst, kb, depth + 1)?;
437                all_solutions.extend(tail_solutions);
438            }
439        }
440
441        Ok(all_solutions)
442    }
443
444    /// Return `true` if any argument of `pred` is still an unbound variable.
445    fn has_variables(&self, pred: &Predicate) -> bool {
446        pred.args
447            .iter()
448            .any(|t| matches!(t, crate::ir::Term::Var(_)))
449    }
450
451    /// Rename variables in a rule using a numeric suffix (fixpoint variant).
452    fn rename_rule_fixpoint(&self, rule: &Rule, suffix: usize) -> Rule {
453        let var_map: HashMap<String, String> = rule
454            .variables()
455            .into_iter()
456            .map(|v| (v.clone(), format!("{}__fp{}", v, suffix)))
457            .collect();
458
459        let rename_subst: Substitution = var_map
460            .into_iter()
461            .map(|(old, new)| (old, crate::ir::Term::Var(new)))
462            .collect();
463
464        Rule {
465            head: apply_subst_predicate(&rule.head, &rename_subst),
466            body: rule
467                .body
468                .iter()
469                .map(|p| apply_subst_predicate(p, &rename_subst))
470                .collect(),
471        }
472    }
473}
474
475impl Default for FixpointEngine {
476    fn default() -> Self {
477        Self::new()
478    }
479}
480
481/// Stratification analysis for logic programs
482pub struct StratificationAnalyzer {
483    /// Dependency graph between predicates
484    dependencies: HashMap<String, HashSet<String>>,
485}
486
487impl StratificationAnalyzer {
488    /// Create a new stratification analyzer
489    pub fn new() -> Self {
490        Self {
491            dependencies: HashMap::new(),
492        }
493    }
494
495    /// Analyze a knowledge base for stratification
496    pub fn analyze(&mut self, kb: &KnowledgeBase) -> StratificationResult {
497        self.build_dependency_graph(kb);
498
499        // Check for cycles
500        if self.has_cycles() {
501            StratificationResult::NonStratifiable
502        } else {
503            // Compute stratification levels
504            let strata = self.compute_strata();
505            StratificationResult::Stratifiable(strata)
506        }
507    }
508
509    /// Build dependency graph from KB
510    fn build_dependency_graph(&mut self, kb: &KnowledgeBase) {
511        // Collect unique predicate names from rules
512        let predicate_names: HashSet<String> =
513            kb.rules.iter().map(|r| r.head.name.clone()).collect();
514
515        for predicate_name in predicate_names {
516            for rule in kb.get_rules(&predicate_name) {
517                let head = &rule.head.name;
518                let deps: HashSet<String> = rule.body.iter().map(|p| p.name.clone()).collect();
519
520                self.dependencies
521                    .entry(head.clone())
522                    .or_default()
523                    .extend(deps);
524            }
525        }
526    }
527
528    /// Check if dependency graph has cycles
529    fn has_cycles(&self) -> bool {
530        let mut visited = HashSet::new();
531        let mut rec_stack = HashSet::new();
532
533        for node in self.dependencies.keys() {
534            if self.has_cycle_util(node, &mut visited, &mut rec_stack) {
535                return true;
536            }
537        }
538
539        false
540    }
541
542    /// Utility for cycle detection (DFS)
543    fn has_cycle_util(
544        &self,
545        node: &str,
546        visited: &mut HashSet<String>,
547        rec_stack: &mut HashSet<String>,
548    ) -> bool {
549        if rec_stack.contains(node) {
550            return true;
551        }
552
553        if visited.contains(node) {
554            return false;
555        }
556
557        visited.insert(node.to_string());
558        rec_stack.insert(node.to_string());
559
560        if let Some(neighbors) = self.dependencies.get(node) {
561            for neighbor in neighbors {
562                if self.has_cycle_util(neighbor, visited, rec_stack) {
563                    return true;
564                }
565            }
566        }
567
568        rec_stack.remove(node);
569        false
570    }
571
572    /// Compute stratification levels
573    fn compute_strata(&self) -> Vec<Vec<String>> {
574        let mut strata = Vec::new();
575        let mut remaining: HashSet<String> = self.dependencies.keys().cloned().collect();
576
577        while !remaining.is_empty() {
578            // Find predicates with no dependencies on remaining predicates
579            let mut current_stratum = Vec::new();
580
581            for pred in &remaining {
582                let has_remaining_deps = self
583                    .dependencies
584                    .get(pred)
585                    .map(|deps| deps.iter().any(|d| remaining.contains(d)))
586                    .unwrap_or(false);
587
588                if !has_remaining_deps {
589                    current_stratum.push(pred.clone());
590                }
591            }
592
593            if current_stratum.is_empty() {
594                // Shouldn't happen if no cycles, but break to avoid infinite loop
595                break;
596            }
597
598            for pred in &current_stratum {
599                remaining.remove(pred);
600            }
601
602            strata.push(current_stratum);
603        }
604
605        strata
606    }
607}
608
609impl Default for StratificationAnalyzer {
610    fn default() -> Self {
611        Self::new()
612    }
613}
614
615/// Result of stratification analysis
616#[derive(Debug, Clone)]
617pub enum StratificationResult {
618    /// Program is stratifiable with given strata
619    Stratifiable(Vec<Vec<String>>),
620    /// Program contains unstratifiable recursion
621    NonStratifiable,
622}
623
624#[cfg(test)]
625mod tests {
626    use super::*;
627    use crate::ir::{Constant, Term};
628
629    #[test]
630    fn test_tabled_inference_basic() {
631        let mut kb = KnowledgeBase::new();
632
633        // Add facts
634        kb.add_fact(Predicate::new(
635            "parent".to_string(),
636            vec![
637                Term::Const(Constant::String("alice".to_string())),
638                Term::Const(Constant::String("bob".to_string())),
639            ],
640        ));
641        kb.add_fact(Predicate::new(
642            "parent".to_string(),
643            vec![
644                Term::Const(Constant::String("bob".to_string())),
645                Term::Const(Constant::String("charlie".to_string())),
646            ],
647        ));
648
649        // Add recursive rule: ancestor(X, Y) :- parent(X, Y)
650        kb.add_rule(Rule::new(
651            Predicate::new(
652                "ancestor".to_string(),
653                vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
654            ),
655            vec![Predicate::new(
656                "parent".to_string(),
657                vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
658            )],
659        ));
660
661        // Add recursive rule: ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z)
662        kb.add_rule(Rule::new(
663            Predicate::new(
664                "ancestor".to_string(),
665                vec![Term::Var("X".to_string()), Term::Var("Z".to_string())],
666            ),
667            vec![
668                Predicate::new(
669                    "parent".to_string(),
670                    vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
671                ),
672                Predicate::new(
673                    "ancestor".to_string(),
674                    vec![Term::Var("Y".to_string()), Term::Var("Z".to_string())],
675                ),
676            ],
677        ));
678
679        let engine = TabledInferenceEngine::new();
680
681        let goal = Predicate::new(
682            "ancestor".to_string(),
683            vec![
684                Term::Const(Constant::String("alice".to_string())),
685                Term::Var("Z".to_string()),
686            ],
687        );
688
689        let solutions = engine.query(&goal, &kb).expect("test: should succeed");
690        assert!(!solutions.is_empty());
691    }
692
693    #[test]
694    fn test_table_stats() {
695        let engine = TabledInferenceEngine::new();
696        let stats = engine.table_stats();
697        assert_eq!(stats.entries, 0);
698        assert_eq!(stats.complete_entries, 0);
699    }
700
701    #[test]
702    fn test_stratification_no_cycles() {
703        let mut kb = KnowledgeBase::new();
704
705        // Add non-recursive rule: grandparent(X, Z) :- parent(X, Y), parent(Y, Z)
706        kb.add_rule(Rule::new(
707            Predicate::new(
708                "grandparent".to_string(),
709                vec![Term::Var("X".to_string()), Term::Var("Z".to_string())],
710            ),
711            vec![
712                Predicate::new(
713                    "parent".to_string(),
714                    vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
715                ),
716                Predicate::new(
717                    "parent".to_string(),
718                    vec![Term::Var("Y".to_string()), Term::Var("Z".to_string())],
719                ),
720            ],
721        ));
722
723        let mut analyzer = StratificationAnalyzer::new();
724        let result = analyzer.analyze(&kb);
725
726        match result {
727            StratificationResult::Stratifiable(strata) => {
728                assert!(!strata.is_empty());
729            }
730            StratificationResult::NonStratifiable => {
731                // Should be stratifiable
732                panic!("Expected stratifiable result");
733            }
734        }
735    }
736
737    #[test]
738    fn test_fixpoint_engine() {
739        let engine = FixpointEngine::new();
740        let kb = KnowledgeBase::new();
741
742        // Compute fixpoint (should return same KB for empty KB)
743        let result = engine.compute_fixpoint(&kb).expect("test: should succeed");
744        assert_eq!(result.facts.len(), kb.facts.len());
745    }
746}