libprop_sat_solver/tableaux_solver/
mod.rs

1//! Propositional formula satisfiability solver using the Propositional Tableaux method.
2
3use crate::formula::PropositionalFormula;
4
5pub mod tableau;
6pub mod theory;
7pub use tableau::Tableau;
8pub use theory::Theory;
9
10use log::debug;
11
12/// Result of expansion using various rules.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ExpansionKind {
15    /// The alpha (α) rule is applicable to the forms:
16    ///
17    /// | Form                   | α1           | α2       |
18    /// | ---------------------- | ------------ | -------- |
19    /// | `(A^B)`                | `A`          | `B`      |
20    /// | `(A<->B)`              | `(A->B)`     | `(B->A)` |
21    /// | <code>(-(A\|B))</code> | `(-A)`       | `(-B)`   |
22    /// | `(-(A->B))`            | `A`          | `(-B)`   |
23    /// | `(-(-A))`              | `A`          | -        |
24    Alpha(Box<PropositionalFormula>, Option<Box<PropositionalFormula>>),
25    /// The beta (β) rule is applicable to the forms:
26    ///
27    /// | Form                  | β1         | β2         |
28    /// | --------------------- | ---------- | ---------- |
29    /// | <code>(A\|B)</code >  | `A`        | `B`        |
30    /// | <code>(-(A^B))</code> | `(-A)`     | `(-B)`     |
31    /// | `(A->B)`              | `(-A)`     | `B`        |
32    /// | `-(A<->B)`            | `(A^(-B))` | `(B^(-A))` |
33    Beta(Box<PropositionalFormula>, Box<PropositionalFormula>),
34}
35
36/// Checks if the given propositional formula is _satisfiable_.
37///
38/// # Propositional Tableaux Algorithm
39///
40/// ## Key Terminology
41///
42/// | Term        | Meaning                                  |
43/// | ----------- | ---------------------------------------- |
44/// | `Theory`    | A _set_ of propositional **formulas**.   |
45/// | `Tableau`   | A _queue_ of _alternative_ **theories**. |
46///
47/// ## Core Algorithm
48///
49/// The original algorithm is given by the pseudocode:
50///
51/// ```text
52/// begin procedure Satisfiable(phi: PropositionalFormula) -> bool
53///     // Initialize `phi` as the single element of the single set contained
54///     // within the theory queue.
55///     Tableau <- [{ phi }]
56///
57///     while !IsEmpty(Tab) do
58///         Theory <- Dequeue(Tableau)
59///
60///         if FullyExpanded(Theory) && !ContainsContradictions(Theory) then
61///             return true
62///         else
63///             NonLiteral <- PickNonLiteral(Theory)
64///
65///             ExpansionType <- DetermineExpansionType(NonLiteral)
66///
67///             match ExpansionType
68///                 case ALPHA => {
69///                     Theory = Theory[alpha <- {alpha_1, alpha_2}]
70///                     if !ContainsContradictions(Theory) && !(Theory in Tableau) then
71///                         Enqueue(Theory)
72///                     end if
73///                 }
74///                 case BETA => {
75///                     Theory_1 = Theory[beta <- beta_1]
76///                     Theory_2 = Theory[beta <- beta_2]
77///
78///                     if !(Theory_1 in Tableau) && !ContainsContradictions(Theory_1) then
79///                         Enqueue(Theory_1)
80///                     end if
81///
82///                     if !(Theory_2 in Tableau) && !ContainsContradictions(Theory_2) then
83///                         Enqueue(Theory_2)
84///                     end if
85///                 }
86///             end match
87///         end if
88///     end while
89///
90///     assert IsEmpty(Tableau)
91///     return false
92/// end procedure
93/// ```
94///
95/// Notice that the algorithm performs an optimization for early return by fusing the contradiction
96/// checking logic (i.e. determining if a branch closes) with the branch construction logic.
97pub fn is_satisfiable(propositional_formula: &PropositionalFormula) -> bool {
98    let mut tableau = Tableau::from_starting_propositional_formula(propositional_formula.clone());
99    debug!("starting with tableau:\n{:#?}", &tableau);
100
101    while !tableau.is_empty() {
102        // PANIC: Cannot panic because a `Theory` always exists if the `Tableau` is non-empty.
103        let mut theory = tableau.pop_theory().unwrap();
104        debug!("current_theory:\n{:#?}", &theory);
105
106        if theory.is_fully_expanded() && !theory.has_contradictions() {
107            // If the theory is:
108            //
109            // 1. fully expanded (contains only literals); and
110            // 2. contains no contradictions; then
111            //
112            // The branch represented by the theory remains open, and so the tableau remains open
113            // too because at least one branch (this branch) remains open, hence the
114            // propositional formula is indeed satisfiable.
115            return true;
116        } else {
117            // PANIC: should never panic because we already check that the theory is _not_ fully
118            // expanded, hence it must contain _non-literals_.
119            let non_literal_formula = theory.get_non_literal_formula().unwrap();
120            debug!("current non_literal: {:#?}", &non_literal_formula);
121
122            // PANIC: should never panic because we exhaustively apply expansion rules and ensure
123            // that we pass in a _non-literal_ formula.
124            match expand_non_literal_formula(&non_literal_formula).unwrap() {
125                ExpansionKind::Alpha(literal_1, optional_literal_2) => {
126                    debug!(
127                        "apply alpha expansion: [LEFT = {:#?}], [RIGHT = {:#?}]",
128                        &literal_1, &optional_literal_2
129                    );
130
131                    debug!("theory before expansion: {:#?}", &theory);
132                    // FIXME: this `clone()` does not behave as the intended deep copy?
133                    let mut new_theory = theory.clone();
134
135                    debug!(
136                        "new_theory before expansion:\n{:#?}",
137                        &new_theory.formulas().collect::<Vec<_>>()
138                    );
139
140                    if let Some(literal_2) = optional_literal_2 {
141                        new_theory.swap_formula2(&non_literal_formula, (*literal_1, *literal_2));
142                    } else {
143                        new_theory.swap_formula(&non_literal_formula, *literal_1);
144                    }
145
146                    debug!(
147                        "new_theory after expansion:\n{:#?}",
148                        &new_theory.formulas().collect::<Vec<_>>()
149                    );
150
151                    if !tableau.contains(&new_theory) && !new_theory.has_contradictions() {
152                        tableau.push_theory(new_theory);
153                    }
154                }
155                ExpansionKind::Beta(literal_1, literal_2) => {
156                    let mut new_theory_1 = theory.clone();
157                    let mut new_theory_2 = theory.clone();
158
159                    new_theory_1.swap_formula(&non_literal_formula, *literal_1);
160                    new_theory_2.swap_formula(&non_literal_formula, *literal_2);
161
162                    if !tableau.contains(&new_theory_1) && !new_theory_1.has_contradictions() {
163                        tableau.push_theory(new_theory_1);
164                    }
165
166                    if !tableau.contains(&new_theory_2) && !new_theory_2.has_contradictions() {
167                        tableau.push_theory(new_theory_2);
168                    }
169                }
170            }
171        }
172    }
173
174    // An empty tableau means the propositional formula is unsatisfiable, because we fully expanded
175    // the propositional formula to construct all possible branches, and all branches close, hence
176    // the entire tableau closes.
177    false
178}
179
180fn expand_non_literal_formula(non_literal: &PropositionalFormula) -> Option<ExpansionKind> {
181    match non_literal {
182        // (A <op> B) cases:
183        //
184        // 1. (A^B) => Alpha(A, Some(B)).
185        // 2. (A<->B) => Alpha((A->B), Some((B->A))).
186        // 3. (A|B) => Beta(A, B).
187        // 4. (A->B) => Beta((-A), B).
188        PropositionalFormula::Conjunction(Some(a), Some(b)) => {
189            return Some(ExpansionKind::Alpha(a.clone(), Some(b.clone())));
190        }
191        PropositionalFormula::Biimplication(Some(a), Some(b)) => {
192            let alpha_1 = PropositionalFormula::implication(a.clone(), b.clone());
193            let alpha_2 = PropositionalFormula::implication(a.clone(), b.clone());
194            return Some(ExpansionKind::Alpha(
195                Box::new(alpha_1),
196                Some(Box::new(alpha_2)),
197            ));
198        }
199        PropositionalFormula::Disjunction(Some(a), Some(b)) => {
200            return Some(ExpansionKind::Beta(a.clone(), b.clone()));
201        }
202        PropositionalFormula::Implication(Some(a), Some(b)) => {
203            let beta_1 = PropositionalFormula::negated(a.clone());
204            return Some(ExpansionKind::Beta(Box::new(beta_1), b.clone()));
205        }
206
207        // (-(-A)) case:
208        //
209        // 1. (-(-A)) => Alpha(A, None).
210        //
211        // (-(A <op> B)) cases:
212        //
213        // 1. (-(A|B)) => Alpha((-A), Some((-B))).
214        // 2. (-(A^B)) => Beta((-A), (-B)).
215        // 3. (-(A->B)) => Alpha(A, Some((-B))).
216        // 4. (-(A<->B)) => Beta((A^(-B)), (B^(-A))).
217        PropositionalFormula::Negation(Some(f)) => match &**f {
218            PropositionalFormula::Negation(Some(a)) => {
219                return Some(ExpansionKind::Alpha(a.clone(), None));
220            }
221            PropositionalFormula::Disjunction(Some(a), Some(b)) => {
222                let alpha_1 = PropositionalFormula::negated(a.clone());
223                let alpha_2 = PropositionalFormula::negated(b.clone());
224                return Some(ExpansionKind::Alpha(
225                    Box::new(alpha_1),
226                    Some(Box::new(alpha_2)),
227                ));
228            }
229            PropositionalFormula::Conjunction(Some(a), Some(b)) => {
230                let beta_1 = PropositionalFormula::negated(a.clone());
231                let beta_2 = PropositionalFormula::negated(b.clone());
232                return Some(ExpansionKind::Beta(Box::new(beta_1), Box::new(beta_2)));
233            }
234            PropositionalFormula::Implication(Some(a), Some(b)) => {
235                let alpha_2 = PropositionalFormula::negated(b.clone());
236                return Some(ExpansionKind::Alpha(a.clone(), Some(Box::new(alpha_2))));
237            }
238            PropositionalFormula::Biimplication(Some(a), Some(b)) => {
239                let beta_1 = PropositionalFormula::conjunction(
240                    a.clone(),
241                    Box::new(PropositionalFormula::negated(b.clone())),
242                );
243                let beta_2 = PropositionalFormula::conjunction(
244                    b.clone(),
245                    Box::new(PropositionalFormula::negated(a.clone())),
246                );
247
248                return Some(ExpansionKind::Beta(Box::new(beta_1), Box::new(beta_2)));
249            }
250            _ => {
251                return None;
252            }
253        },
254        _ => {
255            return None;
256        }
257    }
258}
259
260/// Checks if a given propositional formula is _valid_.
261///
262/// This is done by checking that the contrapositive statement: "is `-<formula>` unsatisfiable?"
263pub fn is_valid(formula: &PropositionalFormula) -> bool {
264    let negated_formula = PropositionalFormula::negated(Box::new(formula.clone()));
265    !is_satisfiable(&negated_formula)
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use crate::formula::Variable;
272    use assert2::check;
273
274    #[test]
275    fn test_propositional_variable() {
276        // a
277        let formula = PropositionalFormula::variable(Variable::new("a"));
278
279        check!(is_satisfiable(&formula));
280        check!(!is_valid(&formula));
281    }
282
283    #[test]
284    fn test_conjunction_same_variable() {
285        // (a^a)
286        let formula = PropositionalFormula::conjunction(
287            Box::new(PropositionalFormula::variable(Variable::new("a"))),
288            Box::new(PropositionalFormula::variable(Variable::new("a"))),
289        );
290
291        check!(is_satisfiable(&formula));
292        check!(!is_valid(&formula));
293    }
294
295    #[test]
296    fn test_conjunction_different_variables() {
297        // (a^b)
298        let formula = PropositionalFormula::conjunction(
299            Box::new(PropositionalFormula::variable(Variable::new("a"))),
300            Box::new(PropositionalFormula::variable(Variable::new("b"))),
301        );
302
303        check!(is_satisfiable(&formula));
304        check!(!is_valid(&formula));
305    }
306
307    #[test]
308    fn test_disjunction_same_variable() {
309        // (a|a)
310        let formula = PropositionalFormula::disjunction(
311            Box::new(PropositionalFormula::variable(Variable::new("a"))),
312            Box::new(PropositionalFormula::variable(Variable::new("a"))),
313        );
314
315        check!(is_satisfiable(&formula));
316        check!(!is_valid(&formula));
317    }
318
319    #[test]
320    fn test_disjunction_different_variables() {
321        // (a|b)
322        let formula = PropositionalFormula::disjunction(
323            Box::new(PropositionalFormula::variable(Variable::new("a"))),
324            Box::new(PropositionalFormula::variable(Variable::new("b"))),
325        );
326
327        check!(is_satisfiable(&formula));
328        check!(!is_valid(&formula));
329    }
330
331    #[test]
332    fn test_implication_different_variables() {
333        // (a->b)
334        let formula = PropositionalFormula::implication(
335            Box::new(PropositionalFormula::variable(Variable::new("a"))),
336            Box::new(PropositionalFormula::variable(Variable::new("b"))),
337        );
338
339        check!(is_satisfiable(&formula));
340        check!(!is_valid(&formula));
341    }
342
343    #[test]
344    fn test_biimplication_different_variables() {
345        // (a<->b)
346        let formula = PropositionalFormula::biimplication(
347            Box::new(PropositionalFormula::variable(Variable::new("a"))),
348            Box::new(PropositionalFormula::variable(Variable::new("b"))),
349        );
350
351        check!(is_satisfiable(&formula));
352        check!(!is_valid(&formula));
353    }
354
355    #[test]
356    fn test_contradiction() {
357        // (a^-a)
358        let formula = PropositionalFormula::conjunction(
359            Box::new(PropositionalFormula::variable(Variable::new("a"))),
360            Box::new(PropositionalFormula::negated(Box::new(
361                PropositionalFormula::variable(Variable::new("a")),
362            ))),
363        );
364
365        check!(!is_satisfiable(&formula));
366        check!(!is_valid(&formula));
367    }
368
369    #[test]
370    fn test_tautology_disjunction() {
371        // (a|(-a))
372        let formula = PropositionalFormula::disjunction(
373            Box::new(PropositionalFormula::variable(Variable::new("a"))),
374            Box::new(PropositionalFormula::negated(Box::new(
375                PropositionalFormula::variable(Variable::new("a")),
376            ))),
377        );
378
379        check!(is_satisfiable(&formula));
380        check!(is_valid(&formula));
381    }
382
383    #[test]
384    fn test_tautology_disjunction_nested_negation() {
385        // ((-a)|(-(-a)))
386        let formula = PropositionalFormula::disjunction(
387            Box::new(PropositionalFormula::negated(Box::new(
388                PropositionalFormula::variable(Variable::new("a")),
389            ))),
390            Box::new(PropositionalFormula::negated(Box::new(
391                PropositionalFormula::negated(Box::new(PropositionalFormula::variable(
392                    Variable::new("a"),
393                ))),
394            ))),
395        );
396
397        check!(is_satisfiable(&formula));
398        check!(is_valid(&formula));
399    }
400
401    #[test]
402    fn test_tautology_implication_literal() {
403        // (a->a)
404        let formula = PropositionalFormula::implication(
405            Box::new(PropositionalFormula::variable(Variable::new("a"))),
406            Box::new(PropositionalFormula::variable(Variable::new("a"))),
407        );
408
409        check!(is_satisfiable(&formula));
410        check!(is_valid(&formula));
411    }
412
413    #[test]
414    fn test_tautology_implication_negated_literal() {
415        // ((-a)->(-a))
416        let formula = PropositionalFormula::implication(
417            Box::new(PropositionalFormula::negated(Box::new(
418                PropositionalFormula::variable(Variable::new("a")),
419            ))),
420            Box::new(PropositionalFormula::negated(Box::new(
421                PropositionalFormula::variable(Variable::new("a")),
422            ))),
423        );
424
425        check!(is_satisfiable(&formula));
426        check!(is_valid(&formula));
427    }
428
429    #[test]
430    fn test_tautology_biimplication_literal() {
431        // (a<->a)
432        let formula = PropositionalFormula::biimplication(
433            Box::new(PropositionalFormula::variable(Variable::new("a"))),
434            Box::new(PropositionalFormula::variable(Variable::new("a"))),
435        );
436
437        check!(is_satisfiable(&formula));
438        check!(is_valid(&formula));
439    }
440
441    #[test]
442    fn test_tautology_biimplication_negated_literal() {
443        // ((-a)<->(-a))
444        let formula = PropositionalFormula::biimplication(
445            Box::new(PropositionalFormula::negated(Box::new(
446                PropositionalFormula::variable(Variable::new("a")),
447            ))),
448            Box::new(PropositionalFormula::negated(Box::new(
449                PropositionalFormula::variable(Variable::new("a")),
450            ))),
451        );
452
453        check!(is_satisfiable(&formula));
454        check!(is_valid(&formula));
455    }
456}