A Rust library for SAT solving and automated theorem proving. This package is a work in progress, but the following are supported:
- Datatypes/parsing/printing operations
- Eval
- Standard DNF/CNF algorithms,
- Definitional CNF (preserving equisatisfiability) for propositional logic.
- The classic DP and DPLL algorithms for testing satisfiability
- Basic Iterative DPLL as well as Backjumping/Conflict clause learning.
- Prenex normal form
- Skolemization
- A semi-decision procedure for first-order validity checking (Gilmore and Davis-Putnam)
To run the Jupyter notebook README.ipynb, you will need both Jupyter
https://jupyter-notebook.readthedocs.io/en/stable/
and a Jupyter Rust kernel, e.g. https://github.com/evcxr/evcxr/blob/main/evcxr_jupyter/README.md.
README.md is generated from README.ipynb by running
jupyter nbconvert --execute --to markdown README.ipynb
Acknowlegement: This library was highly informed by John Harrison's text on Automated Theorem Proving (which uses Ocaml).
(Harrison, J. (2009). Handbook of Practical Logic and Automated Reasoning. Cambridge: Cambridge University Press)
:dep harrison_rust = {path = "."}
use std::collections::{HashMap, HashSet}; use std::io::stdout; use std::path::Path; use harrison_rust::first_order_logic::{FOValuation, Interpretation, Language, Pred}; use harrison_rust::formula::{DPLISolver, Formula}; use harrison_rust::propositional_logic::Prop; use harrison_rust::sudoku::{get_board_formulas, parse_sudoku_dataset, Board}; use harrison_rust::utils::run_and_time;
Example 1: Simple formula
let formula = Formula::<Prop>::parse("C \\/ D <=> (~A /\\ B)").unwrap(); formula.pprint(); println!("{}", formula.get_truthtable()); let cnf = Formula::to_cnf(&formula); cnf.pprint(); println!("Is satisfiable?: {}", formula.dpll_sat()); println!("Is tautology?: {}", formula.dpll_taut());
C \/ D <=> ~A /\ B
A B C D | formula
---------------------------------
true true true true | false
true true true false | false
true true false true | false
true true false false | true
true false true true | false
true false true false | false
true false false true | false
true false false false | true
false true true true | true
false true true false | true
false true false true | true
false true false false | false
false false true true | false
false false true false | false
false false false true | false
false false false false | true
---------------------------------
((((((A \/ C) \/ D) \/ ~B) /\ (B \/ ~C)) /\ (B \/ ~D)) /\ (~A \/ ~C)) /\ (~A \/ ~D)
Is satisfiable?: true
Is tautology?: false
Example 2: A Tautology
let formula = Formula::<Prop>::parse("A \\/ ~A").unwrap(); formula.pprint(); println!("{}", formula.get_truthtable()); println!("Is satisfiable?: {}", formula.dpll_sat()); println!("Is tautology?: {}", formula.dpll_taut());
A \/ ~A
A | formula
---------------
true | true
false | true
---------------
Is satisfiable?: true
Is tautology?: true
Example 3: A Contradiction
let formula = Formula::<Prop>::parse("~A /\\ A").unwrap(); formula.pprint(); println!("{}", formula.get_truthtable()); println!("Is satisfiable?: {}", formula.dpll_sat()); println!("Is tautology?: {}", formula.dpll_taut()); println!("Is contradiction?: {}", Formula::not(&formula).dpll_taut());
~A /\ A
A | formula
---------------
true | false
false | false
---------------
Is satisfiable?: false
Is tautology?: false
Is contradiction?: true
Example 4: Formula simplification
let formula = Formula::<Prop>::parse("((true ==> (x <=> false)) ==> ~(y \\/ (false /\\ z)))").unwrap(); formula.pprint(); println!("...simplifies to..."); let simplified = formula.simplify(); simplified.pprint(); let formula = Formula::<Pred>::parse( "forall x. ((true ==> (R(x) <=> false)) ==> exists z. exists y. ~(K(y) \\/ false))", ).unwrap(); formula.pprint(); println!("...simplifies to..."); let simplified = formula.simplify(); simplified.pprint();
(true ==> (x <=> false)) ==> ~(y \/ false /\ z)
...simplifies to...
~x ==> ~y
forall x. ((true ==> (R(x) <=> false)) ==> (exists z y. ~(K(y) \/ false)))
...simplifies to...
forall x. (~R(x) ==> (exists y. ~K(y)))
Example 5: Solve a hard sudoku board
let path_str: &str = "./data/sudoku.txt"; let path: &Path = Path::new(path_str); let boards: Vec<Board> = parse_sudoku_dataset(path, Some(1)); let clauses = get_board_formulas(&boards, 9, 3)[0].clone(); let mut solver = DPLISolver::new(&clauses); let num_props = solver.num_props(); println!("(Sukoku sentence has {num_props} propositional variables)"); let is_sat = run_and_time(|| solver.dplb_solve()); println!("Is satisfiable?: {is_sat}"); let formula = Formula::formulaset_to_cnf_formula(clauses); let check = formula.eval(&solver.get_valuation().unwrap()); println!("Check: Solution satisfies original constraints?: {check}");
(Sukoku sentence has 729 propositional variables)
Run time is 5.664883334s.
Is satisfiable?: true
Check: Solution satisfies original constraints?: true
Example 6: Arithmetic mod n (n >= 2)
fn integers_mod_n(n: u32) -> Interpretation<u32> { assert!(n > 1); type FuncType = dyn Fn(&[u32]) -> u32; type RelType = dyn Fn(&[u32]) -> bool; let lang = Language { func: HashMap::from([ ("+".to_string(), 2), ("*".to_string(), 2), ("0".to_string(), 0), ("1".to_string(), 0), ]), rel: HashMap::from([("=".to_string(), 2)]), }; let domain: HashSet<u32> = HashSet::from_iter(0..n); let addition = move |inputs: &[u32]| -> u32 { (inputs[0] + inputs[1]) % n }; let multiplication = move |inputs: &[u32]| -> u32 { (inputs[0] * inputs[1]) % n }; let zero = |_inputs: &[u32]| -> u32 { 0 }; let one = |_inputs: &[u32]| -> u32 { 1 }; fn equality(inputs: &[u32]) -> bool { inputs[0] == inputs[1] } let funcs: HashMap<String, Box<FuncType>> = HashMap::from([ ("+".to_string(), Box::new(addition) as Box<FuncType>), ("*".to_string(), Box::new(multiplication) as Box<FuncType>), ("0".to_string(), Box::new(zero) as Box<FuncType>), ("1".to_string(), Box::new(one) as Box<FuncType>), ]); let rels: HashMap<String, Box<RelType>> = HashMap::from([("=".to_string(), Box::new(equality) as Box<RelType>)]); Interpretation::new(&lang, domain, funcs, rels) } // Let's verify (for n < 20) that the integers mod n form a field // (have multiplicative inverses) if and only if n is prime. let mult_inverse = "forall x. (~(x = 0) ==> exists y. x * y = 1)"; let mult_inverse_formula = Formula::<Pred>::parse(mult_inverse).unwrap(); println!("Definition of multiplicative inverses:"); mult_inverse_formula.pprint(); let empty_valuation = FOValuation::new(); println!("Model: | Is a field?"); for n in 2..20 { let interpretation = integers_mod_n(n); let sat = mult_inverse_formula.eval(&interpretation, &empty_valuation); println!("Integers mod {n}: {sat}"); }
Definition of multiplicative inverses:
forall x. (~x = 0 ==> (exists y. x * y = 1))
Model: | Is a field?
Integers mod 2: true
Integers mod 3: true
Integers mod 4: false
Integers mod 5: true
Integers mod 6: false
Integers mod 7: true
Integers mod 8: false
Integers mod 9: false
Integers mod 10: false
Integers mod 11: true
Integers mod 12: false
Integers mod 13: true
Integers mod 14: false
Integers mod 15: false
Integers mod 16: false
Integers mod 17: true
Integers mod 18: false
Integers mod 19: true
Example 7: Prenex Normal Form
let formula = Formula::<Pred>::parse("(exists x. F(x, z)) ==> (exists w. forall z. ~G(z, x))").unwrap(); formula.pprint(); println!("In prenex normal form:"); let result = formula.pnf(); result.pprint();
(exists x. F(x, z)) ==> (exists w. (forall z. ~G(z, x)))
In prenex normal form:
forall x' z'. (~F(x', z) \/ ~G(z', x))
Example 8: Skolemization
let formula = Formula::<Pred>::parse( "R(F(y)) \\/ (exists x. P(f_w(x))) /\\ exists n. forall r. forall y. exists w. M(G(y, w)) \\/ exists z. ~M(F(z, w))", ) .unwrap(); formula.pprint(); println!("Skolemized:"); let result = formula.skolemize(); result.pprint();
R(F(y)) \/ (exists x. P(f_w(x))) /\ (exists n. (forall r y. (exists w. M(G(y, w))))) \/ (exists z. ~M(F(z, w)))
Skolemized:
R(F(y)) \/ P(f_w(c_x)) /\ M(G(y', f_w'(y'))) \/ ~M(F(f_z(w), w))
Example 9: Test a first order formula for validity (valid formula)
let string = "(forall x y. exists z. forall w. (P(x) /\\ Q(y) ==> R(z) /\\ U(w))) ==> (exists x y. (P(x) /\\ Q(y))) ==> (exists z. R(z))"; let formula = Formula::<Pred>::parse(string).unwrap(); let compute_unsat_core = true; let max_depth = 10; Formula::davis_putnam(&formula, compute_unsat_core, max_depth);
Skolemized negation is ((~P(x) \/ ~Q(y)) \/ R(f_z(x, y)) /\ U(w)) /\ (P(c_x) /\ Q(c_y)) /\ ~R(x)
Generating tuples for next level 0
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(c_x, c_x)) \\/ ~P(c_x)) \\/ ~Q(c_x))) /\\ ((U(c_x) \\/ ~P(c_x)) \\/ ~Q(c_x))) /\\ ~R(c_x)"
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(c_x, c_y)) \\/ ~P(c_x)) \\/ ~Q(c_y))) /\\ ((U(c_x) \\/ ~P(c_x)) \\/ ~Q(c_y))) /\\ ~R(c_x)"
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(c_y, c_x)) \\/ ~P(c_y)) \\/ ~Q(c_x))) /\\ ((U(c_x) \\/ ~P(c_y)) \\/ ~Q(c_x))) /\\ ~R(c_y)"
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(c_y, c_y)) \\/ ~P(c_y)) \\/ ~Q(c_y))) /\\ ((U(c_x) \\/ ~P(c_y)) \\/ ~Q(c_y))) /\\ ~R(c_y)"
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(c_x, c_x)) \\/ ~P(c_x)) \\/ ~Q(c_x))) /\\ ((U(c_y) \\/ ~P(c_x)) \\/ ~Q(c_x))) /\\ ~R(c_x)"
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(c_x, c_y)) \\/ ~P(c_x)) \\/ ~Q(c_y))) /\\ ((U(c_y) \\/ ~P(c_x)) \\/ ~Q(c_y))) /\\ ~R(c_x)"
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(c_y, c_x)) \\/ ~P(c_y)) \\/ ~Q(c_x))) /\\ ((U(c_y) \\/ ~P(c_y)) \\/ ~Q(c_x))) /\\ ~R(c_y)"
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(c_y, c_y)) \\/ ~P(c_y)) \\/ ~Q(c_y))) /\\ ((U(c_y) \\/ ~P(c_y)) \\/ ~Q(c_y))) /\\ ~R(c_y)"
Generating tuples for next level 1
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(c_x, f_z(c_x, c_x))) \\/ ~P(c_x)) \\/ ~Q(f_z(c_x, c_x)))) /\\ ((U(c_x) \\/ ~P(c_x)) \\/ ~Q(f_z(c_x, c_x)))) /\\ ~R(c_x)"
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(c_x, f_z(c_x, c_y))) \\/ ~P(c_x)) \\/ ~Q(f_z(c_x, c_y)))) /\\ ((U(c_x) \\/ ~P(c_x)) \\/ ~Q(f_z(c_x, c_y)))) /\\ ~R(c_x)"
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(c_x, f_z(c_y, c_x))) \\/ ~P(c_x)) \\/ ~Q(f_z(c_y, c_x)))) /\\ ((U(c_x) \\/ ~P(c_x)) \\/ ~Q(f_z(c_y, c_x)))) /\\ ~R(c_x)"
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(c_x, f_z(c_y, c_y))) \\/ ~P(c_x)) \\/ ~Q(f_z(c_y, c_y)))) /\\ ((U(c_x) \\/ ~P(c_x)) \\/ ~Q(f_z(c_y, c_y)))) /\\ ~R(c_x)"
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(c_y, f_z(c_x, c_x))) \\/ ~P(c_y)) \\/ ~Q(f_z(c_x, c_x)))) /\\ ((U(c_x) \\/ ~P(c_y)) \\/ ~Q(f_z(c_x, c_x)))) /\\ ~R(c_y)"
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(c_y, f_z(c_x, c_y))) \\/ ~P(c_y)) \\/ ~Q(f_z(c_x, c_y)))) /\\ ((U(c_x) \\/ ~P(c_y)) \\/ ~Q(f_z(c_x, c_y)))) /\\ ~R(c_y)"
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(c_y, f_z(c_y, c_x))) \\/ ~P(c_y)) \\/ ~Q(f_z(c_y, c_x)))) /\\ ((U(c_x) \\/ ~P(c_y)) \\/ ~Q(f_z(c_y, c_x)))) /\\ ~R(c_y)"
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(c_y, f_z(c_y, c_y))) \\/ ~P(c_y)) \\/ ~Q(f_z(c_y, c_y)))) /\\ ((U(c_x) \\/ ~P(c_y)) \\/ ~Q(f_z(c_y, c_y)))) /\\ ~R(c_y)"
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(f_z(c_x, c_x), c_x)) \\/ ~P(f_z(c_x, c_x))) \\/ ~Q(c_x))) /\\ ((U(c_x) \\/ ~P(f_z(c_x, c_x))) \\/ ~Q(c_x))) /\\ ~R(f_z(c_x, c_x))"
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(f_z(c_x, c_x), c_y)) \\/ ~P(f_z(c_x, c_x))) \\/ ~Q(c_y))) /\\ ((U(c_x) \\/ ~P(f_z(c_x, c_x))) \\/ ~Q(c_y))) /\\ ~R(f_z(c_x, c_x))"
Adding new formula to set: "(((P(c_x) /\\ Q(c_y)) /\\ ((R(f_z(f_z(c_x, c_y), c_x)) \\/ ~P(f_z(c_x, c_y))) \\/ ~Q(c_x))) /\\ ((U(c_x) \\/ ~P(f_z(c_x, c_y))) \\/ ~Q(c_x))) /\\ ~R(f_z(c_x, c_y))"
Found 2 inconsistent ground instances of skolemized negation:
(((P(c_x) /\ Q(c_y)) /\ ((R(f_z(f_z(c_x, c_y), c_x)) \/ ~P(f_z(c_x, c_y))) \/ ~Q(c_x))) /\ ((U(c_x) \/ ~P(f_z(c_x, c_y))) \/ ~Q(c_x))) /\ ~R(f_z(c_x, c_y))
(((P(c_x) /\ Q(c_y)) /\ ((R(f_z(c_x, c_y)) \/ ~P(c_x)) \/ ~Q(c_y))) /\ ((U(c_y) \/ ~P(c_x)) \/ ~Q(c_y))) /\ ~R(c_x)
Formula is valid.
Example 10: Test a first order formula for validity (invalid formula)
let string = "forall boy. exists girl. (Loves(girl, friend(boy)))"; let formula = Formula::<Pred>::parse(string).unwrap(); let compute_unsat_core = true; let max_depth = 20; let result = Formula::davis_putnam(&formula, compute_unsat_core, max_depth); println!("{:?}", result);
Skolemized negation is ~Loves(girl, friend(c_boy))
Generating tuples for next level 0
Adding new formula to set: "~Loves(c_boy, friend(c_boy))"
Generating tuples for next level 1
Adding new formula to set: "~Loves(friend(c_boy), friend(c_boy))"
Generating tuples for next level 2
Adding new formula to set: "~Loves(friend(friend(c_boy)), friend(c_boy))"
Generating tuples for next level 3
Adding new formula to set: "~Loves(friend(friend(friend(c_boy))), friend(c_boy))"
Generating tuples for next level 4
Adding new formula to set: "~Loves(friend(friend(friend(friend(c_boy)))), friend(c_boy))"
Generating tuples for next level 5
Adding new formula to set: "~Loves(friend(friend(friend(friend(friend(c_boy))))), friend(c_boy))"
Generating tuples for next level 6
Adding new formula to set: "~Loves(friend(friend(friend(friend(friend(friend(c_boy)))))), friend(c_boy))"
Generating tuples for next level 7
Adding new formula to set: "~Loves(friend(friend(friend(friend(friend(friend(friend(c_boy))))))), friend(c_boy))"
Generating tuples for next level 8
Adding new formula to set: "~Loves(friend(friend(friend(friend(friend(friend(friend(friend(c_boy)))))))), friend(c_boy))"
Generating tuples for next level 9
Adding new formula to set: "~Loves(friend(friend(friend(friend(friend(friend(friend(friend(friend(c_boy))))))))), friend(c_boy))"
Generating tuples for next level 10
Adding new formula to set: "~Loves(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(c_boy)))))))))), friend(c_boy))"
Generating tuples for next level 11
Adding new formula to set: "~Loves(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(c_boy))))))))))), friend(c_boy))"
Generating tuples for next level 12
Adding new formula to set: "~Loves(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(c_boy)))))))))))), friend(c_boy))"
Generating tuples for next level 13
Adding new formula to set: "~Loves(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(c_boy))))))))))))), friend(c_boy))"
Generating tuples for next level 14
Adding new formula to set: "~Loves(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(c_boy)))))))))))))), friend(c_boy))"
Generating tuples for next level 15
Adding new formula to set: "~Loves(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(c_boy))))))))))))))), friend(c_boy))"
Generating tuples for next level 16
Adding new formula to set: "~Loves(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(c_boy)))))))))))))))), friend(c_boy))"
Generating tuples for next level 17
Adding new formula to set: "~Loves(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(c_boy))))))))))))))))), friend(c_boy))"
Generating tuples for next level 18
Adding new formula to set: "~Loves(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(c_boy)))))))))))))))))), friend(c_boy))"
Generating tuples for next level 19
Adding new formula to set: "~Loves(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(c_boy))))))))))))))))))), friend(c_boy))"
Generating tuples for next level 20
Adding new formula to set: "~Loves(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(friend(c_boy)))))))))))))))))))), friend(c_boy))"
After searching to bound depth 20, set of ground instances (of negation) is still satisfiable. Giving up.
BoundReached(20)