Skip to main content

oxilean_std/prop/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::*;
6/// A simple truth table checker for propositional formulas over a fixed number of variables.
7///
8/// Checks if a formula is a tautology, satisfiable, or contradictory by exhaustive
9/// enumeration of truth assignments.
10#[allow(dead_code)]
11pub struct TruthTableChecker {
12    /// Number of propositional variables.
13    pub num_vars: usize,
14}
15#[allow(dead_code)]
16impl TruthTableChecker {
17    /// Create a checker for `n` variables.
18    pub fn new(n: usize) -> Self {
19        Self { num_vars: n }
20    }
21    /// Enumerate all truth assignments.
22    pub fn all_assignments(&self) -> Vec<Vec<bool>> {
23        let n = 1usize << self.num_vars;
24        (0..n)
25            .map(|mask| (0..self.num_vars).map(|i| (mask >> i) & 1 == 1).collect())
26            .collect()
27    }
28    /// Check if a formula (given as evaluator) is a tautology.
29    pub fn is_tautology(&self, formula: &NnfFormula) -> bool {
30        self.all_assignments().iter().all(|a| formula.eval(a))
31    }
32    /// Check if a formula is satisfiable.
33    pub fn is_satisfiable(&self, formula: &NnfFormula) -> bool {
34        self.all_assignments().iter().any(|a| formula.eval(a))
35    }
36    /// Check if a formula is a contradiction.
37    pub fn is_contradiction(&self, formula: &NnfFormula) -> bool {
38        !self.is_satisfiable(formula)
39    }
40    /// Find a satisfying assignment, if one exists.
41    pub fn find_satisfying_assignment(&self, formula: &NnfFormula) -> Option<Vec<bool>> {
42        self.all_assignments().into_iter().find(|a| formula.eval(a))
43    }
44    /// Count the number of satisfying assignments.
45    pub fn count_satisfying(&self, formula: &NnfFormula) -> usize {
46        self.all_assignments()
47            .iter()
48            .filter(|a| formula.eval(a))
49            .count()
50    }
51}
52/// A propositional formula in NNF (Negation Normal Form).
53///
54/// In NNF, negations appear only at atoms (literals). This simplifies
55/// many proof-theoretic manipulations.
56#[allow(dead_code)]
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub enum NnfFormula {
59    /// A positive atom (propositional variable index).
60    Atom(usize),
61    /// A negated atom.
62    NegAtom(usize),
63    /// Conjunction.
64    And(Box<NnfFormula>, Box<NnfFormula>),
65    /// Disjunction.
66    Or(Box<NnfFormula>, Box<NnfFormula>),
67    /// The constant True.
68    Top,
69    /// The constant False.
70    Bot,
71}
72#[allow(dead_code)]
73impl NnfFormula {
74    /// Check if the formula is a literal (atom or negated atom).
75    pub fn is_literal(&self) -> bool {
76        matches!(self, NnfFormula::Atom(_) | NnfFormula::NegAtom(_))
77    }
78    /// Count the number of atoms in the formula.
79    pub fn atom_count(&self) -> usize {
80        match self {
81            NnfFormula::Atom(_) | NnfFormula::NegAtom(_) => 1,
82            NnfFormula::And(a, b) | NnfFormula::Or(a, b) => a.atom_count() + b.atom_count(),
83            NnfFormula::Top | NnfFormula::Bot => 0,
84        }
85    }
86    /// Count the number of connectives in the formula.
87    pub fn connective_count(&self) -> usize {
88        match self {
89            NnfFormula::Atom(_) | NnfFormula::NegAtom(_) | NnfFormula::Top | NnfFormula::Bot => 0,
90            NnfFormula::And(a, b) | NnfFormula::Or(a, b) => {
91                1 + a.connective_count() + b.connective_count()
92            }
93        }
94    }
95    /// Collect all variable indices appearing in the formula.
96    pub fn variables(&self) -> Vec<usize> {
97        let mut vars = Vec::new();
98        self.collect_vars(&mut vars);
99        vars.sort();
100        vars.dedup();
101        vars
102    }
103    fn collect_vars(&self, acc: &mut Vec<usize>) {
104        match self {
105            NnfFormula::Atom(i) | NnfFormula::NegAtom(i) => acc.push(*i),
106            NnfFormula::And(a, b) | NnfFormula::Or(a, b) => {
107                a.collect_vars(acc);
108                b.collect_vars(acc);
109            }
110            _ => {}
111        }
112    }
113    /// Evaluate the formula under a truth assignment (variable index → bool).
114    pub fn eval(&self, assignment: &[bool]) -> bool {
115        match self {
116            NnfFormula::Atom(i) => assignment.get(*i).copied().unwrap_or(false),
117            NnfFormula::NegAtom(i) => !assignment.get(*i).copied().unwrap_or(false),
118            NnfFormula::And(a, b) => a.eval(assignment) && b.eval(assignment),
119            NnfFormula::Or(a, b) => a.eval(assignment) || b.eval(assignment),
120            NnfFormula::Top => true,
121            NnfFormula::Bot => false,
122        }
123    }
124    /// Simplify by eliminating Top/Bot.
125    pub fn simplify(self) -> NnfFormula {
126        match self {
127            NnfFormula::And(a, b) => {
128                let a = a.simplify();
129                let b = b.simplify();
130                match (&a, &b) {
131                    (NnfFormula::Bot, _) | (_, NnfFormula::Bot) => NnfFormula::Bot,
132                    (NnfFormula::Top, _) => b,
133                    (_, NnfFormula::Top) => a,
134                    _ => NnfFormula::And(Box::new(a), Box::new(b)),
135                }
136            }
137            NnfFormula::Or(a, b) => {
138                let a = a.simplify();
139                let b = b.simplify();
140                match (&a, &b) {
141                    (NnfFormula::Top, _) | (_, NnfFormula::Top) => NnfFormula::Top,
142                    (NnfFormula::Bot, _) => b,
143                    (_, NnfFormula::Bot) => a,
144                    _ => NnfFormula::Or(Box::new(a), Box::new(b)),
145                }
146            }
147            other => other,
148        }
149    }
150}
151/// A sequent in propositional sequent calculus (LK/LJ).
152///
153/// A sequent `Γ ⊢ Δ` consists of a context (antecedent) and a conclusion (succedent).
154/// For LJ (intuitionistic), `Δ` has at most one formula.
155#[allow(dead_code)]
156#[derive(Clone, Debug)]
157pub struct Sequent {
158    /// Antecedent: list of assumption formulas.
159    pub antecedent: Vec<NnfFormula>,
160    /// Succedent: list of conclusion formulas (at most one for intuitionistic).
161    pub succedent: Vec<NnfFormula>,
162}
163#[allow(dead_code)]
164impl Sequent {
165    /// Create an empty sequent ⊢ .
166    pub fn empty() -> Self {
167        Self {
168            antecedent: vec![],
169            succedent: vec![],
170        }
171    }
172    /// Create a sequent from antecedent and succedent.
173    pub fn new(antecedent: Vec<NnfFormula>, succedent: Vec<NnfFormula>) -> Self {
174        Self {
175            antecedent,
176            succedent,
177        }
178    }
179    /// Create an axiom sequent: p ⊢ p.
180    pub fn axiom(p: NnfFormula) -> Self {
181        Self {
182            antecedent: vec![p.clone()],
183            succedent: vec![p],
184        }
185    }
186    /// Check if this is an initial sequent (antecedent and succedent share a formula).
187    pub fn is_initial(&self) -> bool {
188        for a in &self.antecedent {
189            if self.succedent.contains(a) {
190                return true;
191            }
192        }
193        false
194    }
195    /// Check if this sequent is classically valid (all atoms satisfied under any assignment).
196    pub fn is_classically_valid(&self, num_vars: usize) -> bool {
197        let checker = TruthTableChecker::new(num_vars);
198        checker.all_assignments().iter().all(|assign| {
199            let ant_false = self.antecedent.iter().any(|f| !f.eval(assign));
200            let suc_true = self.succedent.iter().any(|f| f.eval(assign));
201            ant_false || suc_true
202        })
203    }
204    /// Number of formulas in the sequent.
205    pub fn size(&self) -> usize {
206        self.antecedent.len() + self.succedent.len()
207    }
208    /// Add a formula to the antecedent.
209    pub fn with_assumption(mut self, f: NnfFormula) -> Self {
210        self.antecedent.push(f);
211        self
212    }
213    /// Add a formula to the succedent.
214    pub fn with_conclusion(mut self, f: NnfFormula) -> Self {
215        self.succedent.push(f);
216        self
217    }
218    /// Check if the antecedent contains Bot (False), making the sequent trivially valid.
219    pub fn has_false_in_antecedent(&self) -> bool {
220        self.antecedent.contains(&NnfFormula::Bot)
221    }
222    /// Check if the succedent contains Top (True), making the sequent trivially valid.
223    pub fn has_true_in_succedent(&self) -> bool {
224        self.succedent.contains(&NnfFormula::Top)
225    }
226}
227/// A Kripke model for modal propositional logic.
228///
229/// A Kripke model M = (W, R, V) consists of:
230/// - W: a set of worlds (indexed 0..n)
231/// - R: an accessibility relation (as adjacency matrix)
232/// - V: a valuation assigning truth values to atoms at each world
233#[allow(dead_code)]
234pub struct KripkeModel {
235    /// Number of worlds.
236    pub num_worlds: usize,
237    /// Number of propositional variables.
238    pub num_vars: usize,
239    /// Accessibility relation: access[i][j] = world i can see world j.
240    pub access: Vec<Vec<bool>>,
241    /// Valuation: val[w][p] = atom p is true at world w.
242    pub val: Vec<Vec<bool>>,
243}
244#[allow(dead_code)]
245impl KripkeModel {
246    /// Create a Kripke model with reflexive accessibility (for T axiom).
247    pub fn reflexive(num_worlds: usize, num_vars: usize) -> Self {
248        let mut access = vec![vec![false; num_worlds]; num_worlds];
249        for i in 0..num_worlds {
250            access[i][i] = true;
251        }
252        let val = vec![vec![false; num_vars]; num_worlds];
253        Self {
254            num_worlds,
255            num_vars,
256            access,
257            val,
258        }
259    }
260    /// Set the valuation of variable `var` at world `world`.
261    pub fn set_val(&mut self, world: usize, var: usize, truth: bool) {
262        if world < self.num_worlds && var < self.num_vars {
263            self.val[world][var] = truth;
264        }
265    }
266    /// Set the accessibility from world `from` to world `to`.
267    pub fn set_access(&mut self, from: usize, to: usize) {
268        if from < self.num_worlds && to < self.num_worlds {
269            self.access[from][to] = true;
270        }
271    }
272    /// Evaluate an NnfFormula at a given world (treating Box as necessity over accessible worlds).
273    pub fn satisfies_at(&self, formula: &NnfFormula, world: usize) -> bool {
274        formula.eval(&self.val[world])
275    }
276    /// Check if the model is reflexive (for T axiom validity).
277    pub fn is_reflexive(&self) -> bool {
278        (0..self.num_worlds).all(|i| self.access[i][i])
279    }
280    /// Check if the model is transitive (for S4 axiom validity).
281    pub fn is_transitive(&self) -> bool {
282        for i in 0..self.num_worlds {
283            for j in 0..self.num_worlds {
284                for k in 0..self.num_worlds {
285                    if self.access[i][j] && self.access[j][k] && !self.access[i][k] {
286                        return false;
287                    }
288                }
289            }
290        }
291        true
292    }
293    /// Check if the model is symmetric (for B axiom validity).
294    pub fn is_symmetric(&self) -> bool {
295        for i in 0..self.num_worlds {
296            for j in 0..self.num_worlds {
297                if self.access[i][j] && !self.access[j][i] {
298                    return false;
299                }
300            }
301        }
302        true
303    }
304    /// Check if the model is Euclidean (for S5 axiom validity).
305    pub fn is_euclidean(&self) -> bool {
306        for i in 0..self.num_worlds {
307            for j in 0..self.num_worlds {
308                for k in 0..self.num_worlds {
309                    if self.access[i][j] && self.access[i][k] && !self.access[j][k] {
310                        return false;
311                    }
312                }
313            }
314        }
315        true
316    }
317    /// Number of worlds accessible from a given world.
318    pub fn accessible_count(&self, world: usize) -> usize {
319        if world < self.num_worlds {
320            self.access[world].iter().filter(|&&b| b).count()
321        } else {
322            0
323        }
324    }
325}