Skip to main content

tensorlogic_ir/resolution/
clause.rs

1//! # Clauses for Resolution
2//!
3//! A [`Clause`] is a disjunction of literals. This module implements
4//! construction, tautology/Horn detection, substitution, renaming, and
5//! theta-subsumption used by the resolution prover.
6
7use crate::expr::TLExpr;
8use crate::term::Term;
9use crate::unification::{rename_vars, Substitution};
10use serde::{Deserialize, Serialize};
11use std::collections::HashSet;
12
13use super::literal::Literal;
14
15/// A clause is a disjunction of literals: `L₁ ∨ L₂ ∨ ... ∨ Lₙ`.
16///
17/// Special cases:
18/// - Empty clause (∅): contradiction, no literals
19/// - Unit clause: single literal
20/// - Horn clause: at most one positive literal
21#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
22pub struct Clause {
23    /// The literals in this clause (disjunction)
24    pub literals: Vec<Literal>,
25}
26
27impl Clause {
28    /// Create a new clause from a list of literals.
29    pub fn from_literals(literals: Vec<Literal>) -> Self {
30        // Remove duplicates and sort for consistency
31        let mut unique_lits: Vec<Literal> = literals.into_iter().collect();
32        unique_lits.sort_by(|a, b| {
33            let a_str = format!("{:?}", a);
34            let b_str = format!("{:?}", b);
35            a_str.cmp(&b_str)
36        });
37        unique_lits.dedup();
38
39        Clause {
40            literals: unique_lits,
41        }
42    }
43
44    /// Create an empty clause (contradiction).
45    pub fn empty() -> Self {
46        Clause { literals: vec![] }
47    }
48
49    /// Create a unit clause (single literal).
50    pub fn unit(literal: Literal) -> Self {
51        Clause {
52            literals: vec![literal],
53        }
54    }
55
56    /// Check if this is the empty clause (contradiction).
57    pub fn is_empty(&self) -> bool {
58        self.literals.is_empty()
59    }
60
61    /// Check if this is a unit clause (single literal).
62    pub fn is_unit(&self) -> bool {
63        self.literals.len() == 1
64    }
65
66    /// Check if this is a Horn clause (at most one positive literal).
67    pub fn is_horn(&self) -> bool {
68        self.literals.iter().filter(|l| l.is_positive()).count() <= 1
69    }
70
71    /// Get the number of literals in this clause.
72    pub fn len(&self) -> usize {
73        self.literals.len()
74    }
75
76    /// Check if clause is empty (different from is_empty which checks for contradiction).
77    pub fn is_len_zero(&self) -> bool {
78        self.literals.is_empty()
79    }
80
81    /// Get all free variables in this clause.
82    pub fn free_vars(&self) -> HashSet<String> {
83        self.literals
84            .iter()
85            .flat_map(|lit| lit.free_vars())
86            .collect()
87    }
88
89    /// Check if this clause subsumes another (is more general).
90    ///
91    /// **Theta-Subsumption**: Clause C subsumes D (C ⪯ D) if there exists a
92    /// substitution θ such that Cθ ⊆ D.
93    ///
94    /// This means C is more general than D. For example:
95    /// - `{P(x)}` subsumes `{P(a), Q(a)}` with θ = {x/a}
96    /// - `{P(x), Q(x)}` subsumes `{P(a), Q(a), R(a)}` with θ = {x/a}
97    ///
98    /// # Implementation
99    ///
100    /// We try to find a substitution by:
101    /// 1. For each literal in C, try to unify it with some literal in D
102    /// 2. Check if all substitutions are consistent
103    /// 3. If successful, C subsumes D
104    ///
105    /// # Examples
106    ///
107    /// ```rust
108    /// use tensorlogic_ir::{TLExpr, Term, Literal, Clause};
109    ///
110    /// // {P(x)} subsumes {P(a)}
111    /// let c = Clause::unit(Literal::positive(TLExpr::pred("P", vec![Term::var("x")])));
112    /// let d = Clause::unit(Literal::positive(TLExpr::pred("P", vec![Term::constant("a")])));
113    /// assert!(c.subsumes(&d));
114    ///
115    /// // {P(a)} does not subsume {P(x)} (x is more general than a)
116    /// assert!(!d.subsumes(&c));
117    /// ```
118    pub fn subsumes(&self, other: &Clause) -> bool {
119        // Empty clause subsumes nothing (except itself)
120        if self.is_empty() {
121            return other.is_empty();
122        }
123
124        // Can't subsume if C has more literals than D
125        if self.literals.len() > other.literals.len() {
126            return false;
127        }
128
129        // Try to find a substitution that makes all of C's literals match some literal in D
130        self.try_subsumption_matching(other).is_some()
131    }
132
133    /// Attempt to find a substitution θ such that Cθ ⊆ D.
134    ///
135    /// Uses a backtracking search to find consistent literal matchings.
136    fn try_subsumption_matching(&self, other: &Clause) -> Option<Substitution> {
137        // Rename variables in self to avoid conflicts
138        static mut SUBSUME_COUNTER: usize = 0;
139        let counter = unsafe {
140            SUBSUME_COUNTER += 1;
141            SUBSUME_COUNTER
142        };
143
144        let renamed_self = self.rename_variables(&format!("_s{}", counter));
145        let renamed_vars: HashSet<String> = renamed_self.free_vars();
146
147        // Try to match each literal in renamed_self with literals in other
148        let mut subst = Substitution::empty();
149
150        for self_lit in &renamed_self.literals {
151            // Try to find a matching literal in other
152            let mut found_match = false;
153
154            for other_lit in &other.literals {
155                // Literals must have the same polarity
156                if self_lit.polarity != other_lit.polarity {
157                    continue;
158                }
159
160                // Try one-way matching: only variables from self can be bound
161                if let Some(lit_mgu) = self_lit.try_one_way_match(&other_lit.atom, &renamed_vars) {
162                    // Check if this unifier is consistent with existing substitution
163                    if let Ok(()) = subst.try_extend(&lit_mgu) {
164                        found_match = true;
165                        break;
166                    }
167                }
168            }
169
170            if !found_match {
171                return None; // Failed to match this literal
172            }
173        }
174
175        Some(subst)
176    }
177
178    /// Check if this clause is tautology (contains complementary literals).
179    pub fn is_tautology(&self) -> bool {
180        for i in 0..self.literals.len() {
181            for j in (i + 1)..self.literals.len() {
182                if self.literals[i].is_complementary(&self.literals[j]) {
183                    return true;
184                }
185            }
186        }
187        false
188    }
189
190    /// Apply a substitution to this clause.
191    ///
192    /// This creates a new clause with the substitution applied to all literals.
193    pub fn apply_substitution(&self, subst: &Substitution) -> Clause {
194        let new_literals = self
195            .literals
196            .iter()
197            .map(|lit| lit.apply_substitution(subst))
198            .collect();
199        Clause::from_literals(new_literals)
200    }
201
202    /// Rename all variables in this clause with a suffix.
203    ///
204    /// This is used for standardizing apart clauses before resolution
205    /// to avoid variable name conflicts.
206    ///
207    /// # Example
208    ///
209    /// ```rust
210    /// use tensorlogic_ir::{TLExpr, Term, Literal, Clause};
211    ///
212    /// // P(x) ∨ Q(x)
213    /// let p_x = Literal::positive(TLExpr::pred("P", vec![Term::var("x")]));
214    /// let q_x = Literal::positive(TLExpr::pred("Q", vec![Term::var("x")]));
215    /// let clause = Clause::from_literals(vec![p_x, q_x]);
216    ///
217    /// // Rename to P(x_1) ∨ Q(x_1)
218    /// let renamed = clause.rename_variables("1");
219    /// ```
220    pub fn rename_variables(&self, suffix: &str) -> Clause {
221        let renamed_literals = self
222            .literals
223            .iter()
224            .map(|lit| self.rename_literal(lit, suffix))
225            .collect();
226        Clause::from_literals(renamed_literals)
227    }
228
229    /// Rename variables in a literal (helper for rename_variables).
230    fn rename_literal(&self, lit: &Literal, suffix: &str) -> Literal {
231        match &lit.atom {
232            TLExpr::Pred { name, args } => {
233                let renamed_args: Vec<Term> =
234                    args.iter().map(|term| rename_vars(term, suffix)).collect();
235                Literal {
236                    atom: TLExpr::Pred {
237                        name: name.clone(),
238                        args: renamed_args,
239                    },
240                    polarity: lit.polarity,
241                }
242            }
243            _ => lit.clone(),
244        }
245    }
246}