//! Minimal exhaustive DFS model checker. //! //! Provides the same core API surface as `stateright` — [`Model`] trait, //! [`Property`] (always/sometimes), and a [`Checker`] with DFS exploration — //! without requiring an external crate dependency. //! //! This keeps Cargo.lock unchanged while still providing genuine exhaustive //! state-space exploration for the runtime guarantee proofs. use std::collections::HashSet; use std::fmt::Debug; use std::hash::Hash; use std::marker::PhantomData; // ── Model trait ────────────────────────────────────────────────────────────── /// A finite-state model suitable for exhaustive exploration. pub trait Model: Sized { type State: Clone + Debug + Hash + Eq; type Action: Clone + Debug + Hash + Eq; /// Initial states to begin exploration from. fn init_states(&self) -> Vec; /// Enumerate all enabled actions in `state`, pushing them into `actions`. fn actions(&self, state: &Self::State, actions: &mut Vec); /// Compute the successor state after applying `action`. Return `None` if /// the action is a no-op (state unchanged). fn next_state(&self, state: &Self::State, action: Self::Action) -> Option; /// Properties to verify across all reachable states. fn properties(&self) -> Vec>; /// Create a checker for this model. fn checker(&self) -> Checker<'_, Self> { Checker { model: self } } } // ── Property ───────────────────────────────────────────────────────────────── /// The kind of temporal property. enum PropertyKind { /// Must hold in every reachable state. Always, /// Must hold in at least one reachable state (liveness canary). Sometimes, } /// A named property checked during model exploration. pub struct Property { name: String, kind: PropertyKind, checker_fn: Box bool>, } impl Property { /// Safety invariant: must hold in every reachable state. pub fn always(name: &str, f: impl Fn(&M, &M::State) -> bool + 'static) -> Self { Self { name: name.to_string(), kind: PropertyKind::Always, checker_fn: Box::new(f), } } /// Liveness canary: must hold in at least one reachable state. pub fn sometimes(name: &str, f: impl Fn(&M, &M::State) -> bool + 'static) -> Self { Self { name: name.to_string(), kind: PropertyKind::Sometimes, checker_fn: Box::new(f), } } } // ── Checker & DFS ──────────────────────────────────────────────────────────── /// Builder that holds a reference to the model. pub struct Checker<'a, M: Model> { model: &'a M, } impl<'a, M: Model> Checker<'a, M> { /// Run exhaustive DFS. Named `spawn_dfs` for API compatibility, but runs /// synchronously (no threads needed for our bounded models). pub fn spawn_dfs(self) -> DfsHandle { let properties = self.model.properties(); let mut visited: HashSet = HashSet::new(); let mut stack: Vec<(M::State, usize)> = Vec::new(); // (state, depth) let mut max_depth: usize = 0; let mut actions_buf: Vec = Vec::new(); // Track property results let mut always_violated: Vec> = properties .iter() .map(|_| None) .collect(); let mut sometimes_satisfied: Vec = properties .iter() .map(|_| false) .collect(); // Seed with init states for s in self.model.init_states() { if visited.insert(s.clone()) { stack.push((s, 0)); } } while let Some((state, depth)) = stack.pop() { if depth > max_depth { max_depth = depth; } // Check all properties against this state for (i, prop) in properties.iter().enumerate() { let holds = (prop.checker_fn)(self.model, &state); match prop.kind { PropertyKind::Always => { if !holds && always_violated[i].is_none() { always_violated[i] = Some(format!( "ALWAYS property {:?} violated in state: {:?}", prop.name, state )); } } PropertyKind::Sometimes => { if holds { sometimes_satisfied[i] = true; } } } } // Expand successors actions_buf.clear(); self.model.actions(&state, &mut actions_buf); for action in actions_buf.drain(..) { if let Some(next) = self.model.next_state(&state, action) { if visited.insert(next.clone()) { stack.push((next, depth + 1)); } } } } // Build failures list let mut failures = Vec::new(); for (i, prop) in properties.iter().enumerate() { match prop.kind { PropertyKind::Always => { if let Some(msg) = &always_violated[i] { failures.push(msg.clone()); } } PropertyKind::Sometimes => { if !sometimes_satisfied[i] { failures.push(format!( "SOMETIMES property {:?} was never satisfied across {} states", prop.name, visited.len() )); } } } } DfsHandle { result: CheckResult { unique_states: visited.len(), max_depth, failures, }, _phantom: PhantomData, } } } /// Handle returned by `spawn_dfs`. Call `.join()` to get the result. pub struct DfsHandle { result: CheckResult, _phantom: PhantomData, } // Suppress unused type parameter warning impl DfsHandle { /// Consume the handle and return the exploration result. pub fn join(self) -> CheckResult { self.result } } /// Result of an exhaustive DFS exploration. pub struct CheckResult { unique_states: usize, max_depth: usize, failures: Vec, } impl CheckResult { /// Number of unique states explored. pub fn unique_state_count(&self) -> usize { self.unique_states } /// Maximum DFS depth reached. pub fn max_depth(&self) -> usize { self.max_depth } /// Panic if any property was violated. pub fn assert_properties(&self) { if !self.failures.is_empty() { let msg = self.failures.join("\n"); panic!("Property violations:\n{msg}"); } } }