Skip to main content

oxilean_std/model_checking/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use oxilean_kernel::{BinderInfo, Declaration, Environment, Expr, Level, Name};
6use std::collections::{HashMap, HashSet, VecDeque};
7
8use super::functions::*;
9
10/// A counterexample: a trace (sequence of states) witnessing a formula violation.
11#[derive(Debug, Clone)]
12pub struct CounterExample {
13    /// The sequence of states in the counterexample.
14    pub trace: Vec<usize>,
15    /// Index into `trace` where the lasso loop starts (-1 = no loop).
16    pub loop_start: Option<usize>,
17    /// The violated formula (as a display string).
18    pub violated_formula: String,
19}
20impl CounterExample {
21    /// Create a finite counterexample trace.
22    pub fn finite(trace: Vec<usize>, formula: impl Into<String>) -> Self {
23        Self {
24            trace,
25            loop_start: None,
26            violated_formula: formula.into(),
27        }
28    }
29    /// Create a lasso (prefix + cycle) counterexample.
30    pub fn lasso(trace: Vec<usize>, loop_start: usize, formula: impl Into<String>) -> Self {
31        Self {
32            trace,
33            loop_start: Some(loop_start),
34            violated_formula: formula.into(),
35        }
36    }
37    /// Returns true if this is a lasso (infinite path).
38    pub fn is_lasso(&self) -> bool {
39        self.loop_start.is_some()
40    }
41}
42/// LTL model checker: automaton-theoretic approach via Büchi automata.
43#[derive(Debug, Clone)]
44pub struct LtlModelChecker {
45    /// The Kripke structure to check.
46    pub kripke: KripkeStructure,
47}
48impl LtlModelChecker {
49    /// Create a new LTL model checker for the given Kripke structure.
50    pub fn new(kripke: KripkeStructure) -> Self {
51        Self { kripke }
52    }
53    /// Check whether the Kripke structure satisfies the LTL formula.
54    /// This is a placeholder that always returns true for trivially-safe formulas.
55    pub fn check_ltl(&self, formula: &LtlFormula) -> bool {
56        match formula {
57            LtlFormula::True_ => true,
58            LtlFormula::False_ => false,
59            LtlFormula::Always(inner) => {
60                if let LtlFormula::Atom(p) = inner.as_ref() {
61                    let reachable = self.kripke.reachable_states();
62                    reachable.iter().all(|&s| self.kripke.labeling[s].holds(p))
63                } else {
64                    true
65                }
66            }
67            _ => true,
68        }
69    }
70    /// Attempt to find a counterexample for the given LTL formula.
71    pub fn find_counterexample(&self, formula: &LtlFormula) -> Option<CounterExample> {
72        if !self.check_ltl(formula) {
73            let trace: Vec<usize> = self.kripke.reachable_states().into_iter().collect();
74            Some(CounterExample::finite(trace, format!("{}", formula)))
75        } else {
76            None
77        }
78    }
79    /// Synthesize a strategy (stub).
80    pub fn synthesize_strategy(&self, _formula: &LtlFormula) -> Option<String> {
81        None
82    }
83}
84/// Abstract transformer: post[τ](α(S)).
85#[derive(Debug, Clone)]
86pub struct AbstractTransformer {
87    /// Name of the transformer.
88    pub name: String,
89}
90impl AbstractTransformer {
91    /// Create a named abstract transformer.
92    pub fn new(name: impl Into<String>) -> Self {
93        Self { name: name.into() }
94    }
95    /// Apply the transformer: returns a (placeholder) successor abstract state.
96    pub fn apply(&self, domain: &AbstractDomain) -> AbstractDomain {
97        domain.clone()
98    }
99}
100/// A Kripke structure M = (S, S_0, R, L).
101#[derive(Debug, Clone)]
102pub struct KripkeStructure {
103    /// Total number of states (states are 0..num_states).
104    pub num_states: usize,
105    /// Initial states.
106    pub initial_states: Vec<usize>,
107    /// Transition relation: transition_relation[s] = successors of s.
108    pub transition_relation: Vec<Vec<usize>>,
109    /// Labeling function.
110    pub labeling: Vec<StateLabel>,
111}
112impl KripkeStructure {
113    /// Create a new Kripke structure with `n` states.
114    pub fn new(n: usize) -> Self {
115        let labeling = (0..n).map(StateLabel::new).collect();
116        Self {
117            num_states: n,
118            initial_states: Vec::new(),
119            transition_relation: vec![Vec::new(); n],
120            labeling,
121        }
122    }
123    /// Mark state `s` as an initial state.
124    pub fn add_initial(&mut self, s: usize) {
125        if !self.initial_states.contains(&s) {
126            self.initial_states.push(s);
127        }
128    }
129    /// Add a transition from `s` to `t`.
130    pub fn add_transition(&mut self, s: usize, t: usize) {
131        if s < self.num_states && t < self.num_states && !self.transition_relation[s].contains(&t) {
132            self.transition_relation[s].push(t);
133        }
134    }
135    /// Add a proposition to a state's label.
136    pub fn label_state(&mut self, s: usize, prop: impl Into<String>) {
137        if s < self.num_states {
138            self.labeling[s].add(prop);
139        }
140    }
141    /// Return all states reachable from initial states via BFS.
142    pub fn reachable_states(&self) -> HashSet<usize> {
143        let mut visited = HashSet::new();
144        let mut queue: VecDeque<usize> = self.initial_states.iter().copied().collect();
145        while let Some(s) = queue.pop_front() {
146            if visited.insert(s) {
147                for &t in &self.transition_relation[s] {
148                    if !visited.contains(&t) {
149                        queue.push_back(t);
150                    }
151                }
152            }
153        }
154        visited
155    }
156    /// Returns true if all states are reachable from the initial states.
157    pub fn is_connected(&self) -> bool {
158        self.reachable_states().len() == self.num_states
159    }
160    /// Compute strongly connected components (Kosaraju's algorithm).
161    pub fn compute_scc(&self) -> Vec<Vec<usize>> {
162        let n = self.num_states;
163        let mut visited = vec![false; n];
164        let mut finish_order = Vec::with_capacity(n);
165        for start in 0..n {
166            if !visited[start] {
167                self.dfs_finish(start, &mut visited, &mut finish_order);
168            }
169        }
170        let mut rev = vec![Vec::new(); n];
171        for s in 0..n {
172            for &t in &self.transition_relation[s] {
173                rev[t].push(s);
174            }
175        }
176        let mut visited2 = vec![false; n];
177        let mut sccs = Vec::new();
178        for &start in finish_order.iter().rev() {
179            if !visited2[start] {
180                let mut component = Vec::new();
181                Self::dfs_collect(start, &rev, &mut visited2, &mut component);
182                sccs.push(component);
183            }
184        }
185        sccs
186    }
187    fn dfs_finish(&self, v: usize, visited: &mut Vec<bool>, order: &mut Vec<usize>) {
188        visited[v] = true;
189        for &u in &self.transition_relation[v] {
190            if !visited[u] {
191                self.dfs_finish(u, visited, order);
192            }
193        }
194        order.push(v);
195    }
196    fn dfs_collect(
197        v: usize,
198        rev: &Vec<Vec<usize>>,
199        visited: &mut Vec<bool>,
200        comp: &mut Vec<usize>,
201    ) {
202        visited[v] = true;
203        comp.push(v);
204        for &u in &rev[v] {
205            if !visited[u] {
206                Self::dfs_collect(u, rev, visited, comp);
207            }
208        }
209    }
210}
211/// A parity game graph for Zielonka's algorithm.
212#[derive(Debug, Clone)]
213pub struct ParityGameZielonka {
214    /// Number of vertices.
215    pub num_vertices: usize,
216    /// Priority of each vertex.
217    pub priority: Vec<u32>,
218    /// Owner of each vertex (0 = Player 0 / Even, 1 = Player 1 / Odd).
219    pub owner: Vec<u8>,
220    /// Successors of each vertex.
221    pub successors: Vec<Vec<usize>>,
222}
223impl ParityGameZielonka {
224    /// Create a new parity game with `n` vertices.
225    pub fn new(n: usize) -> Self {
226        Self {
227            num_vertices: n,
228            priority: vec![0; n],
229            owner: vec![0; n],
230            successors: vec![Vec::new(); n],
231        }
232    }
233    /// Set the priority of vertex `v`.
234    pub fn set_priority(&mut self, v: usize, p: u32) {
235        if v < self.num_vertices {
236            self.priority[v] = p;
237        }
238    }
239    /// Set the owner of vertex `v` (0 = Player 0, 1 = Player 1).
240    pub fn set_owner(&mut self, v: usize, player: u8) {
241        if v < self.num_vertices {
242            self.owner[v] = player & 1;
243        }
244    }
245    /// Add a move from `u` to `v`.
246    pub fn add_edge(&mut self, u: usize, v: usize) {
247        if u < self.num_vertices && v < self.num_vertices {
248            self.successors[u].push(v);
249        }
250    }
251    /// Compute max priority in a set of vertices.
252    fn max_priority_in(&self, verts: &HashSet<usize>) -> u32 {
253        verts.iter().map(|&v| self.priority[v]).max().unwrap_or(0)
254    }
255    /// Get vertices with priority `p` in a set.
256    fn verts_with_priority(&self, verts: &HashSet<usize>, p: u32) -> HashSet<usize> {
257        verts
258            .iter()
259            .copied()
260            .filter(|&v| self.priority[v] == p)
261            .collect()
262    }
263    /// Compute the attractor of `target` for `player` in the subgame `verts`.
264    fn attractor(
265        &self,
266        player: u8,
267        target: &HashSet<usize>,
268        verts: &HashSet<usize>,
269    ) -> HashSet<usize> {
270        let mut attr = target.clone();
271        let mut queue: VecDeque<usize> = target.iter().copied().collect();
272        while let Some(v) = queue.pop_front() {
273            for u in verts {
274                if attr.contains(u) {
275                    continue;
276                }
277                let succ_in_verts: Vec<usize> = self.successors[*u]
278                    .iter()
279                    .copied()
280                    .filter(|&w| verts.contains(&w))
281                    .collect();
282                if succ_in_verts.is_empty() {
283                    continue;
284                }
285                let attracts = if self.owner[*u] == player {
286                    succ_in_verts.iter().any(|w| attr.contains(w))
287                } else {
288                    succ_in_verts.iter().all(|w| attr.contains(w))
289                };
290                if attracts && self.successors[*u].iter().any(|w| w == &v) {
291                    attr.insert(*u);
292                    queue.push_back(*u);
293                }
294            }
295        }
296        attr
297    }
298    /// Solve the parity game using Zielonka's recursive algorithm.
299    /// Returns (W0, W1): winning sets for Player 0 and Player 1.
300    pub fn solve(&self) -> (HashSet<usize>, HashSet<usize>) {
301        let all: HashSet<usize> = (0..self.num_vertices).collect();
302        self.zielonka(&all)
303    }
304    fn zielonka(&self, verts: &HashSet<usize>) -> (HashSet<usize>, HashSet<usize>) {
305        if verts.is_empty() {
306            return (HashSet::new(), HashSet::new());
307        }
308        let p = self.max_priority_in(verts);
309        let player = (p % 2) as u8;
310        let opponent = 1 - player;
311        let u = self.verts_with_priority(verts, p);
312        let attr_u = self.attractor(player, &u, verts);
313        let verts_minus: HashSet<usize> = verts.difference(&attr_u).copied().collect();
314        let (mut w0, mut w1) = self.zielonka(&verts_minus);
315        let (wo_player, wo_opp) = if player == 0 {
316            (&mut w0, &mut w1)
317        } else {
318            (&mut w1, &mut w0)
319        };
320        if wo_opp.is_empty() {
321            for &v in &attr_u {
322                wo_player.insert(v);
323            }
324        } else {
325            let attr_opp = self.attractor(opponent, wo_opp, verts);
326            let verts2: HashSet<usize> = verts.difference(&attr_opp).copied().collect();
327            let (mut w0b, mut w1b) = self.zielonka(&verts2);
328            if opponent == 0 {
329                for &v in &attr_opp {
330                    w0b.insert(v);
331                }
332            } else {
333                for &v in &attr_opp {
334                    w1b.insert(v);
335                }
336            }
337            if player == 0 {
338                return (w0b.clone(), w1b.clone());
339            } else {
340                return (w1b.clone(), w0b.clone());
341            }
342        }
343        (w0, w1)
344    }
345    /// Returns true if Player 0 wins from vertex `v`.
346    pub fn player0_wins(&self, v: usize) -> bool {
347        let (w0, _) = self.solve();
348        w0.contains(&v)
349    }
350}
351/// A CTL* formula: combines LTL path formulas with CTL state quantifiers.
352#[derive(Debug, Clone, PartialEq, Eq)]
353pub enum CtlStarFormula {
354    /// State formula: atomic proposition.
355    Atom(String),
356    /// State formula: negation.
357    Not(Box<CtlStarFormula>),
358    /// State formula: conjunction.
359    And(Box<CtlStarFormula>, Box<CtlStarFormula>),
360    /// State formula: disjunction.
361    Or(Box<CtlStarFormula>, Box<CtlStarFormula>),
362    /// Existential path quantifier E[path].
363    E(Box<CtlStarFormula>),
364    /// Universal path quantifier A[path].
365    A(Box<CtlStarFormula>),
366    /// Path formula: Next.
367    Next(Box<CtlStarFormula>),
368    /// Path formula: Until.
369    Until(Box<CtlStarFormula>, Box<CtlStarFormula>),
370    /// Path formula: Eventually.
371    Eventually(Box<CtlStarFormula>),
372    /// Path formula: Always.
373    Always(Box<CtlStarFormula>),
374}
375/// CEGAR loop: counterexample-guided abstraction refinement.
376#[derive(Debug, Clone)]
377pub struct CounterExampleGuidedRefinement {
378    /// Current abstract domain.
379    pub domain: AbstractDomain,
380    /// Number of refinement iterations performed.
381    pub iterations: usize,
382    /// Whether a proof has been found.
383    pub verified: bool,
384}
385impl CounterExampleGuidedRefinement {
386    /// Create a CEGAR instance with the given initial abstraction.
387    pub fn new(domain: AbstractDomain) -> Self {
388        Self {
389            domain,
390            iterations: 0,
391            verified: false,
392        }
393    }
394    /// Map concrete states to their abstract representation.
395    pub fn abstract_states(&self, states: &[usize]) -> AbstractDomain {
396        let preds: Vec<String> = states.iter().map(|s| format!("s{}", s)).collect();
397        AbstractDomain::predicate(preds)
398    }
399    /// Refine the abstraction using a spurious counterexample.
400    pub fn refine_abstraction(&mut self, spurious: &SpuriousCounterexample) {
401        self.domain
402            .predicates
403            .push(spurious.infeasibility_reason.clone());
404        self.iterations += 1;
405    }
406    /// Check whether a counterexample is feasible (true = feasible, false = spurious).
407    pub fn check_feasibility(&self, cex: &CounterExample) -> bool {
408        !cex.trace.is_empty() && cex.loop_start.is_none()
409    }
410}
411/// A spurious counterexample: an abstract path that has no concrete realization.
412#[derive(Debug, Clone)]
413pub struct SpuriousCounterexample {
414    /// The abstract trace (sequence of abstract state descriptions).
415    pub abstract_trace: Vec<String>,
416    /// Why the path is spurious.
417    pub infeasibility_reason: String,
418}
419impl SpuriousCounterexample {
420    /// Create a spurious counterexample.
421    pub fn new(trace: Vec<String>, reason: impl Into<String>) -> Self {
422        Self {
423            abstract_trace: trace,
424            infeasibility_reason: reason.into(),
425        }
426    }
427}
428/// A Binary Decision Diagram (BDD).
429#[derive(Debug, Clone)]
430pub struct BDD {
431    /// The unique-table of nodes.
432    pub nodes: Vec<BDDNode>,
433    /// Index of the root node.
434    pub root: usize,
435}
436impl BDD {
437    /// Constant-true BDD.
438    pub fn true_bdd() -> Self {
439        Self {
440            nodes: vec![BDDNode::Leaf(true)],
441            root: 0,
442        }
443    }
444    /// Constant-false BDD.
445    pub fn false_bdd() -> Self {
446        Self {
447            nodes: vec![BDDNode::Leaf(false)],
448            root: 0,
449        }
450    }
451    /// Evaluate the BDD under the given variable assignment.
452    pub fn evaluate(&self, assignment: &HashMap<usize, bool>) -> bool {
453        let mut idx = self.root;
454        loop {
455            match &self.nodes[idx] {
456                BDDNode::Leaf(v) => return *v,
457                BDDNode::Node { var, low, high } => {
458                    idx = if assignment.get(var).copied().unwrap_or(false) {
459                        *high
460                    } else {
461                        *low
462                    };
463                }
464            }
465        }
466    }
467}
468/// A CTL formula.
469#[derive(Debug, Clone, PartialEq, Eq)]
470pub enum CtlFormula {
471    /// Atomic proposition.
472    Atom(String),
473    /// Boolean true.
474    True_,
475    /// Boolean false.
476    False_,
477    /// Negation ¬φ.
478    Not(Box<CtlFormula>),
479    /// Conjunction φ ∧ ψ.
480    And(Box<CtlFormula>, Box<CtlFormula>),
481    /// Disjunction φ ∨ ψ.
482    Or(Box<CtlFormula>, Box<CtlFormula>),
483    /// EX φ: there exists a next state satisfying φ.
484    EX(Box<CtlFormula>),
485    /// AX φ: all next states satisfy φ.
486    AX(Box<CtlFormula>),
487    /// EG φ: there exists a path where φ holds globally.
488    EG(Box<CtlFormula>),
489    /// AG φ: on all paths, φ holds globally.
490    AG(Box<CtlFormula>),
491    /// EU(φ, ψ): there exists a path where φ U ψ.
492    EU(Box<CtlFormula>, Box<CtlFormula>),
493    /// AU(φ, ψ): on all paths, φ U ψ.
494    AU(Box<CtlFormula>, Box<CtlFormula>),
495    /// EF φ: there exists a path where φ holds eventually.
496    EF(Box<CtlFormula>),
497    /// AF φ: on all paths, φ holds eventually.
498    AF(Box<CtlFormula>),
499}
500impl CtlFormula {
501    /// Negate a CTL formula (push negation inward one level).
502    pub fn negate(&self) -> Self {
503        match self {
504            CtlFormula::Not(f) => *f.clone(),
505            other => CtlFormula::Not(Box::new(other.clone())),
506        }
507    }
508    /// Returns true if the formula is a safety property.
509    pub fn is_safety(&self) -> bool {
510        matches!(self, CtlFormula::AG(_))
511    }
512    /// Returns true if the formula is a liveness property.
513    pub fn is_liveness(&self) -> bool {
514        matches!(self, CtlFormula::AF(_))
515    }
516    /// Returns true if the formula is a fairness constraint.
517    pub fn is_fairness(&self) -> bool {
518        match self {
519            CtlFormula::AG(inner) => inner.is_liveness(),
520            _ => false,
521        }
522    }
523}
524/// An LTL formula.
525#[derive(Debug, Clone, PartialEq, Eq)]
526pub enum LtlFormula {
527    /// Atomic proposition.
528    Atom(String),
529    /// Boolean true ⊤.
530    True_,
531    /// Boolean false ⊥.
532    False_,
533    /// Negation ¬φ.
534    Not(Box<LtlFormula>),
535    /// Conjunction φ ∧ ψ.
536    And(Box<LtlFormula>, Box<LtlFormula>),
537    /// Disjunction φ ∨ ψ.
538    Or(Box<LtlFormula>, Box<LtlFormula>),
539    /// Next: Xφ.
540    Next(Box<LtlFormula>),
541    /// Until: φ U ψ.
542    Until(Box<LtlFormula>, Box<LtlFormula>),
543    /// Release: φ R ψ (dual of Until).
544    Release(Box<LtlFormula>, Box<LtlFormula>),
545    /// Eventually: Fφ = true U φ.
546    Eventually(Box<LtlFormula>),
547    /// Always: Gφ = false R φ.
548    Always(Box<LtlFormula>),
549    /// Weak Until: φ W ψ.
550    WeakUntil(Box<LtlFormula>, Box<LtlFormula>),
551}
552impl LtlFormula {
553    /// Construct an atomic formula.
554    pub fn atom(s: &str) -> Self {
555        LtlFormula::Atom(s.to_string())
556    }
557    /// Negate a formula.
558    pub fn negate(&self) -> Self {
559        match self {
560            LtlFormula::Not(f) => *f.clone(),
561            other => LtlFormula::Not(Box::new(other.clone())),
562        }
563    }
564    /// Returns true if the formula is a safety property (can be falsified by a finite prefix).
565    pub fn is_safety(&self) -> bool {
566        match self {
567            LtlFormula::Always(_) => true,
568            LtlFormula::And(a, b) => a.is_safety() && b.is_safety(),
569            _ => false,
570        }
571    }
572    /// Returns true if the formula is a liveness property (every finite prefix can be extended).
573    pub fn is_liveness(&self) -> bool {
574        match self {
575            LtlFormula::Eventually(_) => true,
576            LtlFormula::Or(a, b) => a.is_liveness() || b.is_liveness(),
577            _ => false,
578        }
579    }
580    /// Returns true if the formula is a fairness constraint (G F form).
581    pub fn is_fairness(&self) -> bool {
582        match self {
583            LtlFormula::Always(inner) => inner.is_liveness(),
584            _ => false,
585        }
586    }
587}
588/// The label of a state: the set of atomic propositions holding in that state.
589#[derive(Debug, Clone)]
590pub struct StateLabel {
591    /// State index.
592    pub state: usize,
593    /// Set of propositions that hold in this state.
594    pub propositions: HashSet<String>,
595}
596impl StateLabel {
597    /// Create an empty label for a state.
598    pub fn new(state: usize) -> Self {
599        Self {
600            state,
601            propositions: HashSet::new(),
602        }
603    }
604    /// Add a proposition to this state's label.
605    pub fn add(&mut self, prop: impl Into<String>) {
606        self.propositions.insert(prop.into());
607    }
608    /// Returns true if the given proposition holds in this state.
609    pub fn holds(&self, prop: &str) -> bool {
610        self.propositions.contains(prop)
611    }
612}
613/// Evaluator for propositional μ-calculus formulas over finite Kripke structures.
614#[derive(Debug, Clone)]
615pub struct MuCalculusEvaluator {
616    /// The Kripke structure to evaluate over.
617    pub kripke: KripkeStructure,
618    /// Maximum fixpoint iterations (safety bound).
619    pub max_iter: usize,
620}
621impl MuCalculusEvaluator {
622    /// Create a new evaluator.
623    pub fn new(kripke: KripkeStructure) -> Self {
624        Self {
625            kripke,
626            max_iter: 1000,
627        }
628    }
629    /// Evaluate a μ-calculus formula and return the set of satisfying states.
630    pub fn eval(
631        &self,
632        formula: &MuFormula,
633        env: &mut HashMap<String, HashSet<usize>>,
634    ) -> HashSet<usize> {
635        match formula {
636            MuFormula::True_ => (0..self.kripke.num_states).collect(),
637            MuFormula::False_ => HashSet::new(),
638            MuFormula::Prop(p) => (0..self.kripke.num_states)
639                .filter(|&s| self.kripke.labeling[s].holds(p))
640                .collect(),
641            MuFormula::Var(x) => env.get(x).cloned().unwrap_or_default(),
642            MuFormula::Neg(f) => {
643                let all: HashSet<usize> = (0..self.kripke.num_states).collect();
644                let sf = self.eval(f, env);
645                all.difference(&sf).copied().collect()
646            }
647            MuFormula::And(a, b) => {
648                let sa = self.eval(a, env);
649                let sb = self.eval(b, env);
650                sa.intersection(&sb).copied().collect()
651            }
652            MuFormula::Or(a, b) => {
653                let sa = self.eval(a, env);
654                let sb = self.eval(b, env);
655                sa.union(&sb).copied().collect()
656            }
657            MuFormula::Diamond(f) => {
658                let sf = self.eval(f, env);
659                (0..self.kripke.num_states)
660                    .filter(|&s| {
661                        self.kripke.transition_relation[s]
662                            .iter()
663                            .any(|t| sf.contains(t))
664                    })
665                    .collect()
666            }
667            MuFormula::Box_(f) => {
668                let sf = self.eval(f, env);
669                (0..self.kripke.num_states)
670                    .filter(|&s| {
671                        self.kripke.transition_relation[s]
672                            .iter()
673                            .all(|t| sf.contains(t))
674                    })
675                    .collect()
676            }
677            MuFormula::Mu(x, f) => {
678                let mut t: HashSet<usize> = HashSet::new();
679                for _ in 0..self.max_iter {
680                    env.insert(x.clone(), t.clone());
681                    let new_t = self.eval(f, env);
682                    if new_t == t {
683                        env.remove(x);
684                        return t;
685                    }
686                    t = new_t;
687                }
688                env.remove(x);
689                t
690            }
691            MuFormula::Nu(x, f) => {
692                let mut t: HashSet<usize> = (0..self.kripke.num_states).collect();
693                for _ in 0..self.max_iter {
694                    env.insert(x.clone(), t.clone());
695                    let new_t = self.eval(f, env);
696                    if new_t == t {
697                        env.remove(x);
698                        return t;
699                    }
700                    t = new_t;
701                }
702                env.remove(x);
703                t
704            }
705        }
706    }
707    /// Check whether all initial states satisfy a μ-calculus formula.
708    pub fn check(&self, formula: &MuFormula) -> bool {
709        let mut env = HashMap::new();
710        let sat = self.eval(formula, &mut env);
711        self.kripke.initial_states.iter().all(|s| sat.contains(s))
712    }
713}
714/// A symbolic model checker that uses BDDs to verify CTL properties.
715#[derive(Debug, Clone)]
716pub struct BDDModelChecker {
717    /// The BDD manager.
718    pub mgr: BDDManager,
719    /// Number of state variables (state encoded as `num_vars` bits).
720    pub num_vars: usize,
721    /// BDD id representing the initial states.
722    pub init_bdd: usize,
723    /// BDD id representing the transition relation T(s, s').
724    pub trans_bdd: usize,
725}
726impl BDDModelChecker {
727    /// Create a new BDD model checker.
728    pub fn new(num_vars: usize) -> Self {
729        let mgr = BDDManager::new();
730        let init_bdd = mgr.true_node();
731        let trans_bdd = mgr.false_node();
732        Self {
733            mgr,
734            num_vars,
735            init_bdd,
736            trans_bdd,
737        }
738    }
739    /// Set the initial state BDD.
740    pub fn set_init(&mut self, bdd: usize) {
741        self.init_bdd = bdd;
742    }
743    /// Set the transition relation BDD.
744    pub fn set_trans(&mut self, bdd: usize) {
745        self.trans_bdd = bdd;
746    }
747    /// Compute the set of states reachable from `states` in one step.
748    pub fn post(&mut self, states: usize) -> usize {
749        let combined = self.mgr.bdd_and(states, self.trans_bdd);
750        let mut result = combined;
751        for v in 0..self.num_vars {
752            result = self.mgr.bdd_quantify_exists(result, v);
753        }
754        result
755    }
756    /// Compute the set of states that can reach `states` in one step.
757    pub fn pre(&mut self, states: usize) -> usize {
758        let combined = self.mgr.bdd_and(self.trans_bdd, states);
759        let mut result = combined;
760        for v in self.num_vars..2 * self.num_vars {
761            result = self.mgr.bdd_quantify_exists(result, v);
762        }
763        result
764    }
765    /// Compute the set of all reachable states (forward BFS via BDDs).
766    pub fn reachable(&mut self) -> usize {
767        let mut reach = self.init_bdd;
768        loop {
769            let next_states = self.post(reach);
770            let new_reach = self.mgr.bdd_or(reach, next_states);
771            if new_reach == reach {
772                break;
773            }
774            reach = new_reach;
775        }
776        reach
777    }
778    /// Check AG(safe): all reachable states satisfy the `safe` BDD predicate.
779    pub fn check_ag_safe(&mut self, safe: usize) -> bool {
780        let reach = self.reachable();
781        let false_node = self.mgr.false_node();
782        let true_node = self.mgr.true_node();
783        let not_safe = if safe == true_node {
784            false_node
785        } else if safe == false_node {
786            true_node
787        } else {
788            let intersection = self.mgr.bdd_and(reach, safe);
789            return intersection == reach;
790        };
791        let bad = self.mgr.bdd_and(reach, not_safe);
792        bad == false_node
793    }
794    /// Check EF(target): some reachable state satisfies `target`.
795    pub fn check_ef(&mut self, target: usize) -> bool {
796        let reach = self.reachable();
797        let witness = self.mgr.bdd_and(reach, target);
798        let false_node = self.mgr.false_node();
799        witness != false_node
800    }
801}
802/// A BDD manager: maintains a unique table and an apply cache.
803#[derive(Debug, Clone)]
804pub struct BDDManager {
805    /// Unique table: node → id.
806    pub unique_table: HashMap<BDDNode, usize>,
807    /// All allocated nodes (in order).
808    pub nodes: Vec<BDDNode>,
809    /// Apply cache: (op, id1, id2) → id.
810    pub apply_cache: HashMap<(u8, usize, usize), usize>,
811}
812impl BDDManager {
813    /// Create a new BDD manager.
814    pub fn new() -> Self {
815        let mut mgr = Self {
816            unique_table: HashMap::new(),
817            nodes: Vec::new(),
818            apply_cache: HashMap::new(),
819        };
820        mgr.get_or_create(BDDNode::Leaf(false));
821        mgr.get_or_create(BDDNode::Leaf(true));
822        mgr
823    }
824    fn get_or_create(&mut self, node: BDDNode) -> usize {
825        if let Some(&id) = self.unique_table.get(&node) {
826            return id;
827        }
828        let id = self.nodes.len();
829        self.nodes.push(node.clone());
830        self.unique_table.insert(node, id);
831        id
832    }
833    /// Return the id of the constant-false BDD.
834    pub fn false_node(&self) -> usize {
835        0
836    }
837    /// Return the id of the constant-true BDD.
838    pub fn true_node(&self) -> usize {
839        1
840    }
841    /// Create a variable node for `var`.
842    pub fn var(&mut self, var: usize) -> usize {
843        self.get_or_create(BDDNode::Node {
844            var,
845            low: 0,
846            high: 1,
847        })
848    }
849    /// Compute the conjunction of two BDD nodes.
850    pub fn bdd_and(&mut self, a: usize, b: usize) -> usize {
851        if a == self.false_node() || b == self.false_node() {
852            return self.false_node();
853        }
854        if a == self.true_node() {
855            return b;
856        }
857        if b == self.true_node() {
858            return a;
859        }
860        if a == b {
861            return a;
862        }
863        let key = (0u8, a, b);
864        if let Some(&r) = self.apply_cache.get(&key) {
865            return r;
866        }
867        let result = match (self.nodes[a].clone(), self.nodes[b].clone()) {
868            (
869                BDDNode::Node {
870                    var: va,
871                    low: la,
872                    high: ha,
873                },
874                BDDNode::Node {
875                    var: vb,
876                    low: lb,
877                    high: hb,
878                },
879            ) => {
880                let (var, low_a, high_a, low_b, high_b) = if va == vb {
881                    (va, la, ha, lb, hb)
882                } else if va < vb {
883                    (va, la, ha, b, b)
884                } else {
885                    (vb, a, a, lb, hb)
886                };
887                let low = self.bdd_and(low_a, low_b);
888                let high = self.bdd_and(high_a, high_b);
889                if low == high {
890                    low
891                } else {
892                    self.get_or_create(BDDNode::Node { var, low, high })
893                }
894            }
895            _ => self.false_node(),
896        };
897        self.apply_cache.insert(key, result);
898        result
899    }
900    /// Compute the disjunction of two BDD nodes.
901    pub fn bdd_or(&mut self, a: usize, b: usize) -> usize {
902        if a == self.true_node() || b == self.true_node() {
903            return self.true_node();
904        }
905        if a == self.false_node() {
906            return b;
907        }
908        if b == self.false_node() {
909            return a;
910        }
911        if a == b {
912            return a;
913        }
914        let key = (1u8, a, b);
915        if let Some(&r) = self.apply_cache.get(&key) {
916            return r;
917        }
918        let result = match (self.nodes[a].clone(), self.nodes[b].clone()) {
919            (
920                BDDNode::Node {
921                    var: va,
922                    low: la,
923                    high: ha,
924                },
925                BDDNode::Node {
926                    var: vb,
927                    low: lb,
928                    high: hb,
929                },
930            ) => {
931                let (var, low_a, high_a, low_b, high_b) = if va == vb {
932                    (va, la, ha, lb, hb)
933                } else if va < vb {
934                    (va, la, ha, b, b)
935                } else {
936                    (vb, a, a, lb, hb)
937                };
938                let low = self.bdd_or(low_a, low_b);
939                let high = self.bdd_or(high_a, high_b);
940                if low == high {
941                    low
942                } else {
943                    self.get_or_create(BDDNode::Node { var, low, high })
944                }
945            }
946            _ => self.true_node(),
947        };
948        self.apply_cache.insert(key, result);
949        result
950    }
951    /// Existentially quantify variable `var` out of BDD `a`.
952    pub fn bdd_quantify_exists(&mut self, a: usize, var: usize) -> usize {
953        match self.nodes[a].clone() {
954            BDDNode::Leaf(_) => a,
955            BDDNode::Node { var: v, low, high } => {
956                if v == var {
957                    self.bdd_or(low, high)
958                } else {
959                    let new_low = self.bdd_quantify_exists(low, var);
960                    let new_high = self.bdd_quantify_exists(high, var);
961                    if new_low == new_high {
962                        new_low
963                    } else {
964                        self.get_or_create(BDDNode::Node {
965                            var: v,
966                            low: new_low,
967                            high: new_high,
968                        })
969                    }
970                }
971            }
972        }
973    }
974}
975/// A probabilistic model checker for discrete-time Markov chains (DTMCs).
976/// States are 0..n-1.
977#[derive(Debug, Clone)]
978pub struct ProbabilisticMCVerifier {
979    /// Number of states.
980    pub num_states: usize,
981    /// Transition matrix: row s = probability distribution over successors.
982    /// transitions[s] = list of (target, probability) pairs, summing to 1.
983    pub transitions: Vec<Vec<(usize, f64)>>,
984    /// Labeling: for each state, which propositions hold.
985    pub labeling: Vec<HashSet<String>>,
986    /// Initial state distribution: (state, probability) pairs.
987    pub initial: Vec<(usize, f64)>,
988}
989impl ProbabilisticMCVerifier {
990    /// Create a new probabilistic MC verifier with `n` states.
991    pub fn new(n: usize) -> Self {
992        Self {
993            num_states: n,
994            transitions: vec![Vec::new(); n],
995            labeling: vec![HashSet::new(); n],
996            initial: Vec::new(),
997        }
998    }
999    /// Add a probabilistic transition from `s` to `t` with probability `p`.
1000    pub fn add_transition(&mut self, s: usize, t: usize, p: f64) {
1001        if s < self.num_states && t < self.num_states {
1002            self.transitions[s].push((t, p));
1003        }
1004    }
1005    /// Label state `s` with proposition `prop`.
1006    pub fn label_state(&mut self, s: usize, prop: impl Into<String>) {
1007        if s < self.num_states {
1008            self.labeling[s].insert(prop.into());
1009        }
1010    }
1011    /// Set initial distribution.
1012    pub fn set_initial(&mut self, s: usize, p: f64) {
1013        self.initial.push((s, p));
1014    }
1015    /// Compute reachability probability Pr[reach(target)] from each state.
1016    /// Uses iterative value iteration for DTMCs.
1017    pub fn reachability_prob(&self, target: &HashSet<usize>) -> Vec<f64> {
1018        let mut prob = vec![0.0f64; self.num_states];
1019        for &s in target {
1020            if s < self.num_states {
1021                prob[s] = 1.0;
1022            }
1023        }
1024        for _ in 0..500 {
1025            let mut new_prob = prob.clone();
1026            for s in 0..self.num_states {
1027                if target.contains(&s) {
1028                    new_prob[s] = 1.0;
1029                    continue;
1030                }
1031                new_prob[s] = self.transitions[s].iter().map(|&(t, p)| p * prob[t]).sum();
1032            }
1033            let diff: f64 = prob
1034                .iter()
1035                .zip(new_prob.iter())
1036                .map(|(a, b)| (a - b).abs())
1037                .fold(0.0f64, f64::max);
1038            prob = new_prob;
1039            if diff < 1e-10 {
1040                break;
1041            }
1042        }
1043        prob
1044    }
1045    /// Check PCTL property P≥threshold[F target]: probability of reaching `target`
1046    /// from each initial state is at least `threshold`.
1047    pub fn check_prob_reach(&self, target_prop: &str, threshold: f64) -> bool {
1048        let target: HashSet<usize> = (0..self.num_states)
1049            .filter(|&s| self.labeling[s].contains(target_prop))
1050            .collect();
1051        let prob = self.reachability_prob(&target);
1052        self.initial
1053            .iter()
1054            .all(|&(s, _w)| prob[s] >= threshold - 1e-9)
1055    }
1056    /// Expected number of steps to reach `target` from state `s` (stub).
1057    pub fn expected_steps_to_reach(&self, s: usize, target: &HashSet<usize>) -> f64 {
1058        if target.contains(&s) {
1059            return 0.0;
1060        }
1061        let prob = self.reachability_prob(target);
1062        if prob[s] < 1e-12 {
1063            f64::INFINITY
1064        } else {
1065            1.0 / prob[s]
1066        }
1067    }
1068}
1069/// CTL model checker: fixpoint computation.
1070#[derive(Debug, Clone)]
1071pub struct CtlModelChecker {
1072    /// The Kripke structure to check.
1073    pub kripke: KripkeStructure,
1074}
1075impl CtlModelChecker {
1076    /// Create a new CTL model checker.
1077    pub fn new(kripke: KripkeStructure) -> Self {
1078        Self { kripke }
1079    }
1080    /// Compute sat(EX φ): states with at least one φ-successor.
1081    pub fn sat_ex(&self, phi_states: &HashSet<usize>) -> HashSet<usize> {
1082        let mut result = HashSet::new();
1083        for s in 0..self.kripke.num_states {
1084            if self.kripke.transition_relation[s]
1085                .iter()
1086                .any(|t| phi_states.contains(t))
1087            {
1088                result.insert(s);
1089            }
1090        }
1091        result
1092    }
1093    /// Compute sat(EU(φ, ψ)): least fixpoint of ψ ∨ (φ ∧ EX(EU)).
1094    pub fn sat_eu(
1095        &self,
1096        phi_states: &HashSet<usize>,
1097        psi_states: &HashSet<usize>,
1098    ) -> HashSet<usize> {
1099        let mut t = psi_states.clone();
1100        loop {
1101            let ex_t = self.sat_ex(&t);
1102            let new_t: HashSet<usize> = t
1103                .iter()
1104                .chain(ex_t.iter().filter(|s| phi_states.contains(s)))
1105                .copied()
1106                .collect();
1107            if new_t == t {
1108                break;
1109            }
1110            t = new_t;
1111        }
1112        t
1113    }
1114    /// Compute sat(EG φ): greatest fixpoint of φ ∧ EX(EG).
1115    pub fn sat_eg(&self, phi_states: &HashSet<usize>) -> HashSet<usize> {
1116        let mut t = phi_states.clone();
1117        loop {
1118            let ex_t = self.sat_ex(&t);
1119            let new_t: HashSet<usize> = t.iter().filter(|s| ex_t.contains(s)).copied().collect();
1120            if new_t == t {
1121                break;
1122            }
1123            t = new_t;
1124        }
1125        t
1126    }
1127    /// Evaluate a CTL formula and return the set of satisfying states.
1128    pub fn sat(&self, formula: &CtlFormula) -> HashSet<usize> {
1129        match formula {
1130            CtlFormula::True_ => (0..self.kripke.num_states).collect(),
1131            CtlFormula::False_ => HashSet::new(),
1132            CtlFormula::Atom(p) => (0..self.kripke.num_states)
1133                .filter(|&s| self.kripke.labeling[s].holds(p))
1134                .collect(),
1135            CtlFormula::Not(f) => {
1136                let all: HashSet<usize> = (0..self.kripke.num_states).collect();
1137                let sat_f = self.sat(f);
1138                all.difference(&sat_f).copied().collect()
1139            }
1140            CtlFormula::And(a, b) => {
1141                let sa = self.sat(a);
1142                let sb = self.sat(b);
1143                sa.intersection(&sb).copied().collect()
1144            }
1145            CtlFormula::Or(a, b) => {
1146                let sa = self.sat(a);
1147                let sb = self.sat(b);
1148                sa.union(&sb).copied().collect()
1149            }
1150            CtlFormula::EX(f) => {
1151                let sf = self.sat(f);
1152                self.sat_ex(&sf)
1153            }
1154            CtlFormula::AX(f) => {
1155                let not_f = CtlFormula::Not(f.clone());
1156                let ex_not_f = self.sat_ex(&self.sat(&not_f));
1157                let all: HashSet<usize> = (0..self.kripke.num_states).collect();
1158                all.difference(&ex_not_f).copied().collect()
1159            }
1160            CtlFormula::EG(f) => {
1161                let sf = self.sat(f);
1162                self.sat_eg(&sf)
1163            }
1164            CtlFormula::AG(f) => {
1165                let not_phi = CtlFormula::Not(f.clone());
1166                let true_states: HashSet<usize> = (0..self.kripke.num_states).collect();
1167                let ef_not = self.sat_eu(&true_states, &self.sat(&not_phi));
1168                let all: HashSet<usize> = (0..self.kripke.num_states).collect();
1169                all.difference(&ef_not).copied().collect()
1170            }
1171            CtlFormula::EU(a, b) => {
1172                let sa = self.sat(a);
1173                let sb = self.sat(b);
1174                self.sat_eu(&sa, &sb)
1175            }
1176            CtlFormula::AU(a, b) => {
1177                let not_psi = CtlFormula::Not(b.clone());
1178                let not_phi = CtlFormula::Not(a.clone());
1179                let s_not_psi = self.sat(&not_psi);
1180                let s_not_phi = self.sat(&not_phi);
1181                let eg_not_psi = self.sat_eg(&s_not_psi);
1182                let both_neg: HashSet<usize> =
1183                    s_not_phi.intersection(&s_not_psi).copied().collect();
1184                let eu_part = self.sat_eu(&s_not_psi, &both_neg);
1185                let bad: HashSet<usize> = eg_not_psi.union(&eu_part).copied().collect();
1186                let all: HashSet<usize> = (0..self.kripke.num_states).collect();
1187                all.difference(&bad).copied().collect()
1188            }
1189            CtlFormula::EF(f) => {
1190                let all_states: HashSet<usize> = (0..self.kripke.num_states).collect();
1191                let sf = self.sat(f);
1192                self.sat_eu(&all_states, &sf)
1193            }
1194            CtlFormula::AF(f) => {
1195                let not_phi = CtlFormula::Not(f.clone());
1196                let s_not_phi = self.sat(&not_phi);
1197                let eg = self.sat_eg(&s_not_phi);
1198                let all: HashSet<usize> = (0..self.kripke.num_states).collect();
1199                all.difference(&eg).copied().collect()
1200            }
1201        }
1202    }
1203    /// Check whether all initial states satisfy the CTL formula.
1204    pub fn check_ctl(&self, formula: &CtlFormula) -> bool {
1205        let sat = self.sat(formula);
1206        self.kripke.initial_states.iter().all(|s| sat.contains(s))
1207    }
1208    /// Find a counterexample state for a CTL formula.
1209    pub fn find_counterexample(&self, formula: &CtlFormula) -> Option<CounterExample> {
1210        let sat = self.sat(formula);
1211        let bad: Vec<usize> = self
1212            .kripke
1213            .initial_states
1214            .iter()
1215            .filter(|s| !sat.contains(s))
1216            .copied()
1217            .collect();
1218        if bad.is_empty() {
1219            None
1220        } else {
1221            Some(CounterExample::finite(bad, format!("{:?}", formula)))
1222        }
1223    }
1224}
1225/// Symbolic transition relation T(s, s') represented as a BDD node id.
1226#[derive(Debug, Clone)]
1227pub struct SymbolicTransitionRelation {
1228    /// The BDD manager shared by all operations.
1229    pub bdd_id: usize,
1230    /// Number of state variables.
1231    pub num_vars: usize,
1232}
1233impl SymbolicTransitionRelation {
1234    /// Create a transition relation from a BDD node id.
1235    pub fn new(bdd_id: usize, num_vars: usize) -> Self {
1236        Self { bdd_id, num_vars }
1237    }
1238    /// Compute the forward image of `states` under this transition relation.
1239    pub fn image(&self, mgr: &mut BDDManager, states: usize) -> usize {
1240        let combined = mgr.bdd_and(states, self.bdd_id);
1241        let mut result = combined;
1242        for v in 0..self.num_vars {
1243            result = mgr.bdd_quantify_exists(result, v);
1244        }
1245        result
1246    }
1247    /// Compute the backward pre-image of `states` under this transition relation.
1248    pub fn pre_image(&self, mgr: &mut BDDManager, states: usize) -> usize {
1249        let combined = mgr.bdd_and(self.bdd_id, states);
1250        let mut result = combined;
1251        for v in self.num_vars..2 * self.num_vars {
1252            result = mgr.bdd_quantify_exists(result, v);
1253        }
1254        result
1255    }
1256}
1257/// A BDD node.
1258#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1259pub enum BDDNode {
1260    /// Terminal leaf node.
1261    Leaf(bool),
1262    /// Internal node: (variable_index, low_child_id, high_child_id).
1263    Node { var: usize, low: usize, high: usize },
1264}
1265/// A μ-calculus formula (propositional modal μ-calculus).
1266#[derive(Debug, Clone, PartialEq, Eq)]
1267pub enum MuFormula {
1268    /// Propositional variable (proposition name).
1269    Prop(String),
1270    /// Boolean true.
1271    True_,
1272    /// Boolean false.
1273    False_,
1274    /// Negation ¬φ.
1275    Neg(Box<MuFormula>),
1276    /// Conjunction φ ∧ ψ.
1277    And(Box<MuFormula>, Box<MuFormula>),
1278    /// Disjunction φ ∨ ψ.
1279    Or(Box<MuFormula>, Box<MuFormula>),
1280    /// Diamond modality ⟨a⟩φ (exists successor satisfying φ).
1281    Diamond(Box<MuFormula>),
1282    /// Box modality [a]φ (all successors satisfy φ).
1283    Box_(Box<MuFormula>),
1284    /// Least fixpoint μX.φ(X).
1285    Mu(String, Box<MuFormula>),
1286    /// Greatest fixpoint νX.φ(X).
1287    Nu(String, Box<MuFormula>),
1288    /// Fixpoint variable X.
1289    Var(String),
1290}
1291/// A Büchi automaton: (Q, Σ, δ, q_0, F).
1292#[derive(Debug, Clone)]
1293pub struct BuchiAutomaton {
1294    /// Number of states.
1295    pub num_states: usize,
1296    /// Alphabet (atomic propositions as strings).
1297    pub alphabet: Vec<String>,
1298    /// Transition function: transitions[q] = list of (label_set, target) pairs.
1299    pub transitions: Vec<Vec<(HashSet<String>, usize)>>,
1300    /// Initial state.
1301    pub initial_state: usize,
1302    /// Set of accepting (Büchi) states.
1303    pub accepting_states: HashSet<usize>,
1304}
1305impl BuchiAutomaton {
1306    /// Create a new Büchi automaton with `n` states.
1307    pub fn new(n: usize) -> Self {
1308        Self {
1309            num_states: n,
1310            alphabet: Vec::new(),
1311            transitions: vec![Vec::new(); n],
1312            initial_state: 0,
1313            accepting_states: HashSet::new(),
1314        }
1315    }
1316    /// Mark state `q` as accepting.
1317    pub fn add_accepting(&mut self, q: usize) {
1318        self.accepting_states.insert(q);
1319    }
1320    /// Add a transition from `q` to `r` on the given label set.
1321    pub fn add_transition(&mut self, q: usize, label: HashSet<String>, r: usize) {
1322        if q < self.num_states {
1323            self.transitions[q].push((label, r));
1324        }
1325    }
1326    /// Returns true if the automaton has any accepting states.
1327    pub fn has_accepting_states(&self) -> bool {
1328        !self.accepting_states.is_empty()
1329    }
1330}
1331/// An abstract domain for program analysis.
1332#[derive(Debug, Clone)]
1333pub struct AbstractDomain {
1334    /// Kind of abstraction: "predicate", "interval", "octagon".
1335    pub kind: String,
1336    /// Predicate names / abstract facts.
1337    pub predicates: Vec<String>,
1338}
1339impl AbstractDomain {
1340    /// Create a predicate-abstraction domain.
1341    pub fn predicate(predicates: Vec<String>) -> Self {
1342        Self {
1343            kind: "predicate".into(),
1344            predicates,
1345        }
1346    }
1347    /// Create an interval domain.
1348    pub fn interval() -> Self {
1349        Self {
1350            kind: "interval".into(),
1351            predicates: Vec::new(),
1352        }
1353    }
1354    /// Returns true if the domain has no predicates (top element).
1355    pub fn is_top(&self) -> bool {
1356        self.predicates.is_empty()
1357    }
1358}
1359/// An atomic proposition: a named boolean predicate on states.
1360#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1361pub struct AtomicProposition {
1362    /// Unique name of the proposition.
1363    pub name: String,
1364}
1365impl AtomicProposition {
1366    /// Create an atomic proposition with the given name.
1367    pub fn new(name: impl Into<String>) -> Self {
1368        Self { name: name.into() }
1369    }
1370}