stateright 0.29.0

A model checker for implementing distributed systems.
Documentation
//! Private module for selective re-export.

mod bfs;
mod dfs;
mod explorer;
mod path;
mod representative;
mod rewrite;
mod rewrite_plan;
mod visitor;

use crate::{Expectation, Model};
use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::Hash;
use std::num::NonZeroUsize;
use std::time::Instant;

pub use rewrite::*;
pub use path::*;
pub use representative::*;
pub use rewrite_plan::*;
pub use visitor::*;

/// A [`Model`] [`Checker`] builder. Instantiable via the [`Model::checker`] method.
///
/// # Example
///
/// ```
/// # use stateright::*; let model = ();
/// model.checker().threads(4).spawn_dfs().join().assert_properties();
/// ```
#[must_use = "This code constructs a builder, not a checker. \
              Consider calling spawn_bfs() or spawn_dfs()."]
pub struct CheckerBuilder<M: Model> {
    model: M,
    #[allow(clippy::type_complexity)]
    symmetry: Option<fn(&M::State) -> M::State>,
    target_state_count: Option<NonZeroUsize>,
    thread_count: usize,
    visitor: Option<Box<dyn CheckerVisitor<M> + Send + Sync>>,
}
impl<M: Model> CheckerBuilder<M> {
    pub(crate) fn new(model: M) -> Self {
        Self {
            model,
            target_state_count: None,
            symmetry: None,
            thread_count: 1,
            visitor: None,
        }
    }

    /// Starts a web service for interactively exploring a model ([demo](http://demo.stateright.rs:3000/)).
    ///
    /// ![Stateright Explorer screenshot](https://raw.githubusercontent.com/stateright/stateright/master/explorer.png)
    ///
    /// # Example
    ///
    /// ```no_run
    /// use stateright::{Checker, Model};
    ///
    /// #[derive(Clone, Debug, Hash)]
    /// enum FizzBuzzAction { Fizz, Buzz, FizzBuzz }
    /// #[derive(Clone)]
    /// struct FizzBuzzModel { max: usize }
    ///
    /// impl Model for FizzBuzzModel {
    ///     type State = Vec<(usize, Option<FizzBuzzAction>)>;
    ///     type Action = Option<FizzBuzzAction>;
    ///     fn init_states(&self) -> Vec<Self::State> {
    ///         vec![Vec::new()]
    ///     }
    ///     fn actions(&self, state: &Self::State, actions: &mut Vec<Self::Action>) {
    ///         actions.push(
    ///             if state.len() % 15 == 0 {
    ///                 Some(FizzBuzzAction::FizzBuzz)
    ///             } else if state.len() % 5 == 0 {
    ///                 Some(FizzBuzzAction::Buzz)
    ///             } else if state.len() % 3 == 0 {
    ///                 Some(FizzBuzzAction::Fizz)
    ///             } else {
    ///                 None
    ///             });
    ///     }
    ///     fn next_state(&self, state: &Self::State, action: Self::Action) -> Option<Self::State> {
    ///         let mut state = state.clone();
    ///         state.push((state.len(), action));
    ///         Some(state)
    ///     }
    ///     fn within_boundary(&self, state: &Self::State) -> bool {
    ///         state.len() <= self.max
    ///     }
    /// }
    ///
    /// let _ = FizzBuzzModel { max: 30 }.checker().serve("localhost:3000");
    /// ```
    ///
    /// # API
    ///
    /// - `GET /` returns a web browser UI as HTML.
    /// - `GET /.status` returns information about the model checker status.
    /// - `GET /.states` returns available initial states and fingerprints.
    /// - `GET /.states/{fingerprint1}/{fingerprint2}/...` follows the specified
    ///    path of fingerprints and returns available actions with resulting
    ///    states and fingerprints.
    /// - `GET /.states/.../{invalid-fingerprint}` returns 404.
    pub fn serve(self, addresses: impl std::net::ToSocketAddrs) -> std::sync::Arc<impl Checker<M>>
    where M: 'static + Model + Send + Sync,
          M::Action: Debug + Send + Sync,
          M::State: Debug + Hash + Send + Sync,
    {
        explorer::serve(self, addresses)
    }

    /// Spawns a breadth-first search model checker. This traversal strategy uses more memory than
    /// [`CheckerBuilder::spawn_dfs`] but will find the shortest [`Path`] to each discovery if
    /// checking is single threadeded (the default behavior, which [`CheckerBuilder::threads`]
    /// overrides).
    ///
    /// This call does not block the current thread. Call [`Checker::join`] to block until checking
    /// completes.
    #[must_use = "Checkers run on background threads. \
                  Consider calling join() or report(...), for example."]
    pub fn spawn_bfs(self) -> impl Checker<M>
    where M: Model + Send + Sync + 'static,
          M::State: Hash + Send + Sync + 'static,
    {
        bfs::BfsChecker::spawn(self)
    }

    /// Spawns a depth-first search model checker. This traversal strategy uses dramatically less
    /// memory than [`CheckerBuilder::spawn_bfs`] at the cost of not finding the shortest [`Path`]
    /// to each discovery.
    ///
    /// This call does not block the current thread. Call [`Checker::join`] to block until
    /// checking completes.
    #[must_use = "Checkers run on background threads. \
                  Consider calling join() or report(...), for example."]
    pub fn spawn_dfs(self) -> impl Checker<M>
    where M: Model + Send + Sync + 'static,
          M::State: Hash + Send + Sync + 'static,
    {
        dfs::DfsChecker::spawn(self)
    }

    /// Enables symmetry reduction. Requires the [model state] to implement [`Representative`].
    ///
    /// [model state]: crate::Model::State
    pub fn symmetry(self) -> Self
    where M::State: Representative,
    {
        self.symmetry_fn(Representative::representative)
    }

    /// Enables symmetry reduction based on a representative function.
    ///
    /// [model state]: crate::Model::State
    pub fn symmetry_fn(self, representative: fn(&M::State) -> M::State) -> Self {
        Self { symmetry: Some(representative), .. self }
    }

    /// Sets the number of states that the checker should aim to generate. For performance reasons
    /// the checker may exceed this number, but it will never generate fewer states if more exist.
    pub fn target_state_count(self, count: usize) -> Self {
        Self { target_state_count: NonZeroUsize::new(count), .. self }
    }

    /// Sets the number of threads available for model checking. For maximum performance this
    /// should match the number of cores.
    pub fn threads(self, thread_count: usize) -> Self {
        Self { thread_count, .. self }
    }

    /// Indicates a function to be run on each evaluated state.
    pub fn visitor(self, visitor: impl CheckerVisitor<M> + Send + Sync + 'static) -> Self {
        Self { visitor: Some(Box::new(visitor)), .. self }
    }
}

/// Implementations perform [`Model`] checking.
///
/// Call [`Model::checker`] to instantiate a [`CheckerBuilder`]. Then call
/// [`CheckerBuilder::spawn_dfs`] or [`CheckerBuilder::spawn_bfs`].
pub trait Checker<M: Model> {
    /// Returns a reference to this checker's [`Model`].
    fn model(&self) -> &M;

    /// Indicate how many states have been generated including repeats. Always greater than or
    /// equal to [`Checker::unique_state_count`].
    fn state_count(&self) -> usize;

    /// Indicates how many unique states have been generated. Always less than or equal to
    /// [`Checker::state_count`].
    fn unique_state_count(&self) -> usize;

    /// Returns a map from property name to corresponding "discovery" (indicated
    /// by a [`Path`]).
    fn discoveries(&self) -> HashMap<&'static str, Path<M::State, M::Action>>;

    /// Blocks the current thread until checking [`is_done`] or each thread evaluates
    /// a specified maximum number of states.
    ///
    /// [`is_done`]: Self::is_done
    fn join(self) -> Self;

    /// Indicates that either all properties have associated discoveries or all reachable states
    /// have been visited.
    fn is_done(&self) -> bool;

    /// Looks up a discovery by property name. Panics if the property does not exist.
    fn discovery(&self, name: &'static str) -> Option<Path<M::State, M::Action>> {
        self.discoveries().remove(name)
    }

    /// Periodically emits a status message.
    fn report(self, w: &mut impl std::io::Write) -> Self
    where M::Action: Debug,
          M::State: Debug,
          Self: Sized,
    {
        // Start with the checking status.
        let method_start = Instant::now();
        while !self.is_done() {
            let _ = writeln!(w, "Checking. states={}, unique={}",
                             self.state_count(),
                             self.unique_state_count());
            std::thread::sleep(std::time::Duration::from_millis(1_000));
        }
        let _ = writeln!(w, "Done. states={}, unique={}, sec={}",
                         self.state_count(),
                         self.unique_state_count(),
                         method_start.elapsed().as_secs());

        // Finish with a discovery summary.
        for (name, path) in self.discoveries() {
            let _ = write!(w, "Discovered \"{}\" {} {}",
                           name, self.discovery_classification(name), path);
        }

        self
    }

    /// Indicates whether a discovery is an `"example"` or `"counterexample"`.
    fn discovery_classification(&self, name: &str) -> &'static str {
        let properties = self.model().properties();
        let property = properties.iter().find(|p| p.name == name).unwrap();
        match property.expectation {
            Expectation::Always | Expectation::Eventually => "counterexample",
            Expectation::Sometimes => "example",
        }
    }

    /// A helper that verifies examples exist for all `sometimes` properties and no counterexamples
    /// exist for any `always`/`eventually` properties.
    fn assert_properties(&self)
    where M::Action: Debug,
          M::State: Debug,
    {
        for p in self.model().properties() {
            match p.expectation {
                Expectation::Always => self.assert_no_discovery(p.name),
                Expectation::Eventually => self.assert_no_discovery(p.name),
                Expectation::Sometimes => { self.assert_any_discovery(p.name); },
            }
        }
    }

    /// Panics if a particular discovery is not found.
    fn assert_any_discovery(&self, name: &'static str) -> Path<M::State, M::Action> {
        if let Some(found) = self.discovery(name) { return found }
        assert!(self.is_done(),
                "Discovery for \"{}\" not found, but model checking is incomplete.", name);
        panic!("Discovery for \"{}\" not found.", name);
    }

    /// Panics if a particular discovery is found.
    fn assert_no_discovery(&self, name: &'static str)
    where M::Action: Debug,
          M::State: Debug,
    {
        if let Some(found) = self.discovery(name) {
            panic!("Unexpected \"{}\" {} {}Last state: {:?}\n",
                   name, self.discovery_classification(name), found, found.last_state());
        }
        assert!(self.is_done(),
                "Discovery for \"{}\" not found, but model checking is incomplete.",
                name);
    }

    /// Panics if the specified actions do not result in a discovery for the specified property
    /// name.
    fn assert_discovery(&self, name: &'static str, actions: Vec<M::Action>)
    where M::State: Debug + PartialEq,
          M::Action: Debug + PartialEq,
    {
        let mut additional_info: Vec<&'static str> = Vec::new();

        let found = self.assert_any_discovery(name);
        for init_state in self.model().init_states() {
            if let Some(path) = Path::from_actions(self.model(), init_state, &actions) {
                let property = self.model().property(name);
                match property.expectation {
                    Expectation::Always => {
                        if !(property.condition)(self.model(), path.last_state()) { return }
                    }
                    Expectation::Eventually => {
                        let states = path.into_states();
                        let is_liveness_satisfied = states.iter().any(|s| {
                            (property.condition)(self.model(), s)
                        });
                        let is_path_terminal = {
                            let mut actions = Vec::new();
                            self.model().actions(states.last().unwrap(), &mut actions);
                            actions.is_empty()
                        };
                        if !is_liveness_satisfied && is_path_terminal { return }
                        if is_liveness_satisfied {
                            additional_info.push("incorrect counterexample satisfies eventually property");
                        }
                        if !is_path_terminal {
                            additional_info.push("incorrect counterexample is nonterminal");
                        }
                    }
                    Expectation::Sometimes => {
                        if (property.condition)(self.model(), path.last_state()) { return }
                    }
                }
            }
        }
        let additional_info = if additional_info.is_empty() {
            "".to_string()
        } else {
            format!(" ({})", additional_info.join("; "))
        };
        panic!("Invalid discovery for \"{}\"{}, but a valid one was found. found={:?}",
               name, additional_info, found.into_actions());
    }
}

// EventuallyBits tracks one bit per 'eventually' property being checked. Properties are assigned
// bit-numbers just by counting the 'eventually' properties up from 0 in the properties list. If a
// bit is present in a bitset, the property has _not_ been found on this path yet. Bits are removed
// from the propagating bitset when we find a state satisfying an `eventually` property; these
// states are not considered discoveries. Only if we hit the "end" of a path (i.e. return to a known
// state / no further state) with any of these bits still 1, the path is considered a discovery,
// a counterexample to the property.
type EventuallyBits = id_set::IdSet;

#[cfg(test)]
mod test_eventually_property_checker {
    use crate::{Checker, Property};
    use crate::test_util::dgraph::DGraph;

    fn eventually_odd() -> Property<DGraph> {
        Property::eventually("odd", |_, s| s % 2 == 1)
    }

    #[test]
    fn can_validate() {
        DGraph::with_property(eventually_odd())
            .with_path(vec![1])        // satisfied at terminal init
            .with_path(vec![2, 3])     // satisfied at nonterminal init
            .with_path(vec![2, 6, 7])  // satisfied at terminal next
            .with_path(vec![4, 9, 10]) // satisfied at nonterminal next
            .check().assert_properties();
        // Repeat with distinct state spaces since stateful checking skips visited states (which we
        // don't expect here, but this is defense in depth).
        DGraph::with_property(eventually_odd())
            .with_path(vec![1]).check().assert_properties();
        DGraph::with_property(eventually_odd())
            .with_path(vec![2, 3]).check().assert_properties();
        DGraph::with_property(eventually_odd())
            .with_path(vec![2, 6, 7]).check().assert_properties();
        DGraph::with_property(eventually_odd())
            .with_path(vec![4, 9, 10]).check().assert_properties();
    }

    #[test]
    fn can_discover_counterexample() { // i.e. can falsify
        assert_eq!(
            DGraph::with_property(eventually_odd())
                .with_path(vec![0, 1])
                .with_path(vec![0, 2])
                .check().discovery("odd").unwrap().into_states(),
            vec![0, 2]);
        assert_eq!(
            DGraph::with_property(eventually_odd())
                .with_path(vec![0, 1])
                .with_path(vec![2, 4])
                .check().discovery("odd").unwrap().into_states(),
            vec![2, 4]);
        assert_eq!(
            DGraph::with_property(eventually_odd())
                .with_path(vec![0, 1, 4, 6])
                .with_path(vec![2, 4, 8])
                .check().discovery("odd").unwrap().into_states(),
            vec![2, 4, 6]);
    }

    #[test]
    fn fixme_can_miss_counterexample_when_revisiting_a_state() { // i.e. incorrectly verify
        assert_eq!(
            DGraph::with_property(eventually_odd())
                .with_path(vec![0, 2, 4, 2]) // cycle
                .check().discovery("odd"),
            None); // FIXME: `unwrap().into_states()` should be [0, 2, 4, 2]
        assert_eq!(
            DGraph::with_property(eventually_odd())
                .with_path(vec![0, 2, 4])
                .with_path(vec![1, 4, 6]) // revisiting 4
                .check().discovery("odd"),
            None); // FIXME: `unwrap().into_states()` should be [0, 2, 4, 6]
    }
}

#[cfg(test)]
mod test_path {
    use super::*;
    use crate::fingerprint;
    use crate::test_util::linear_equation_solver::LinearEquation;
    use std::collections::VecDeque;

    #[test]
    fn can_build_path_from_fingerprints() {
        let fp = |a: u8, b: u8| fingerprint(&(a, b));
        let model = LinearEquation { a: 2, b: 10, c: 14 };
        let fingerprints = VecDeque::from(vec![
            fp(0, 0),
            fp(0, 1),
            fp(1, 1),
            fp(2, 1), // final state
        ]);
        let path = Path::from_fingerprints(&model, fingerprints.clone());
        assert_eq!(
            path.last_state(),
            &(2,1));
        assert_eq!(
            path.last_state(),
            &Path::final_state(&model, fingerprints).unwrap());
    }
}

#[cfg(test)]
mod test_report {
    use super::*;
    use crate::test_util::linear_equation_solver::LinearEquation;

    #[test]
    fn report_includes_property_names_and_paths() {
        // The assertions use `starts_with` to omit timing since it varies.

        // BFS
        let mut written: Vec<u8> = Vec::new();
        LinearEquation { a: 2, b: 10, c: 14 }.checker()
            .spawn_bfs().report(&mut written);
        let output = String::from_utf8(written).unwrap();
        assert!(
            output.starts_with("\
                Checking. states=1, unique=1\n\
                Done. states=15, unique=12, sec="),
            "Output did not start as expected (see test). output={:?}`", output);
        assert!(
            output.ends_with("\
                Discovered \"solvable\" example Path[3]:\n\
                - IncreaseX\n\
                - IncreaseX\n\
                - IncreaseY\n"),
            "Output did not end as expected (see test). output={:?}`", output);

        // DFS
        let mut written: Vec<u8> = Vec::new();
        LinearEquation { a: 2, b: 10, c: 14 }.checker()
            .spawn_dfs().report(&mut written);
        let output = String::from_utf8(written).unwrap();
        assert!(
            output.starts_with("\
                Checking. states=1, unique=1\n\
                Done. states=55, unique=55, sec="),
            "Output did not start as expected (see test). output={:?}`", output);
        assert!(
            output.ends_with("\
                Discovered \"solvable\" example Path[27]:\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n\
                - IncreaseY\n"),
            "Output did not end as expected (see test). output={:?}`", output);
    }
}