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}