use crate::*;
use dashmap::DashMap;
use dashmap::mapref::entry::Entry;
use id_set::IdSet;
use nohash_hasher::NoHashHasher;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::hash::{BuildHasher, BuildHasherDefault};
use std::sync::Arc;
#[derive(Clone, Debug, PartialEq)]
pub struct Path<State, Action>(pub Vec<(State, Option<Action>)>);
impl<State, Action> Path<State, Action> {
pub fn last_state(&self) -> &State {
&self.0.last().unwrap().0
}
pub fn into_actions(self) -> Vec<Action> {
self.0.into_iter().filter_map(|(_s, a)| a).collect()
}
pub fn into_vec(self) -> Vec<(State, Option<Action>)> {
self.into()
}
pub fn name(&self) -> PathName where State: Hash {
self.0.iter()
.map(|(s, _a)| format!("{}", fingerprint(s)))
.collect::<Vec<String>>()
.join("/")
}
}
impl<State, Action> Into<Vec<(State, Option<Action>)>> for Path<State, Action> {
fn into(self) -> Vec<(State, Option<Action>)> { self.0 }
}
pub type PathName = String;
type EventuallyBits = IdSet;
pub struct Checker<M: Model> {
pub(crate) thread_count: usize,
pub(crate) model: M,
pub(crate) pending: VecDeque<(Fingerprint, EventuallyBits, M::State)>,
pub(crate) sources: DashMap<Fingerprint, Option<Fingerprint>, BuildHasherDefault<NoHashHasher<u64>>>,
pub(crate) discoveries: DashMap<&'static str, Fingerprint>,
}
impl<M: Model> Checker<M>
where
M: Sync,
M::State: Debug + Hash + Send + Sync,
M::Action: Debug,
{
pub fn check(&mut self, max_count: usize) -> &mut Self {
let Checker { thread_count, model, pending, sources, discoveries } = self;
let thread_count = *thread_count;
if sources.is_empty() {
let mut init_ebits = IdSet::new();
for (i, p) in model.properties().iter().enumerate() {
if let Property { expectation: Expectation::Eventually, .. } = p {
init_ebits.insert(i);
}
}
for init_state in model.init_states() {
let init_digest = fingerprint(&init_state);
if let Entry::Vacant(init_source) = sources.entry(init_digest) {
init_source.insert(None);
pending.push_front((init_digest, init_ebits.clone(), init_state));
}
}
}
let sources = Arc::new(sources);
let discoveries = Arc::new(discoveries);
if pending.len() < 10_000 {
Self::check_block(max_count, model, pending, sources, discoveries);
return self;
}
let results = crossbeam_utils::thread::scope(|scope| {
let mut threads = Vec::new();
let count = std::cmp::min(max_count, (pending.len() + thread_count - 1) / thread_count);
for _thread_id in 0..thread_count {
let model = &model;
let sources = Arc::clone(&sources);
let discoveries = Arc::clone(&discoveries);
let count = std::cmp::min(pending.len(), count);
let mut pending = pending.split_off(pending.len() - count);
threads.push(scope.spawn(move |_| {
Self::check_block(max_count/thread_count, model, &mut pending, sources, discoveries);
pending
}));
}
let mut results = Vec::new();
for thread in threads { results.push(thread.join().unwrap()); }
results
}).unwrap();
for mut sub_pending in results { pending.append(&mut sub_pending); }
self
}
fn check_properties(model: &M,
state: &M::State,
digest: Fingerprint,
props: &[Property<M>],
ebits: &mut EventuallyBits,
discoveries: &DashMap<&'static str, Fingerprint>,
update_props: &mut bool)
{
for (i, property) in props.iter().enumerate() {
match property {
Property { expectation: Expectation::Always, name, condition: always } => {
if !always(model, &state) {
discoveries.insert(name, digest);
*update_props = true;
}
},
Property { expectation: Expectation::Sometimes, name, condition: sometimes } => {
if sometimes(model, &state) {
discoveries.insert(name, digest);
*update_props = true;
}
},
Property { expectation: Expectation::Eventually, condition, .. } => {
if ebits.contains(i) && condition(model, &state) {
ebits.remove(i);
*update_props = true;
}
}
}
}
}
fn note_terminal_state(digest: Fingerprint,
props: &[Property<M>],
ebits: &EventuallyBits,
discoveries: &DashMap<&'static str, Fingerprint>,
update_props: &mut bool)
{
for (i, property) in props.iter().enumerate() {
if ebits.contains(i) {
discoveries.insert(property.name, digest);
*update_props = true;
}
}
}
fn check_block<S: Clone + BuildHasher>(
max_count: usize,
model: &M,
pending: &mut VecDeque<(Fingerprint, EventuallyBits, M::State)>,
sources: Arc<&mut DashMap<Fingerprint, Option<Fingerprint>, S>>,
discoveries: Arc<&mut DashMap<&'static str, Fingerprint>>) {
let mut remaining = max_count;
let mut next_actions = Vec::new();
let mut properties: Vec<_> = Self::remaining_properties(
model.properties(), &discoveries);
if properties.is_empty() { return }
while let Some((digest, mut ebits, state)) = pending.pop_back() {
let mut update_props = false;
Self::check_properties(model, &state, digest, &properties,
&mut ebits, &discoveries, &mut update_props);
model.actions(&state, &mut next_actions);
if next_actions.is_empty() {
Self::note_terminal_state(digest, &properties, &ebits,
&discoveries, &mut update_props);
}
for next_action in next_actions.drain(0..) {
if let Some(next_state) = model.next_state(&state, next_action) {
if !model.within_boundary(&next_state) {
Self::note_terminal_state(digest, &properties, &ebits,
&discoveries, &mut update_props);
continue
}
let next_digest = fingerprint(&next_state);
if let Entry::Vacant(next_entry) = sources.entry(next_digest) {
next_entry.insert(Some(digest));
pending.push_front((next_digest, ebits.clone(), next_state));
} else {
}
} else {
Self::note_terminal_state(digest, &properties, &ebits,
&discoveries, &mut update_props);
}
}
if update_props {
properties = properties.into_iter().filter(|p| !discoveries.contains_key(p.name)).collect();
}
remaining -= 1;
if remaining == 0 { return }
}
}
pub fn example(&self, name: &'static str) -> Option<Path<M::State, M::Action>> {
if let Some(p) = self.model.properties().into_iter().find(|p| p.name == name) {
if p.expectation != Expectation::Sometimes {
panic!("Please use `counterexample(\"{}\")` for this `always` or `eventually` property.", name);
}
self.discoveries.get(name).map(|mapref| self.path(*mapref.value()))
} else {
let available: Vec<_> = self.model.properties().iter().map(|p| p.name).collect();
panic!("Unknown property. requested={:?}, available={:?}", name, available);
}
}
pub fn counterexample(&self, name: &'static str) -> Option<Path<M::State, M::Action>> {
if let Some(p) = self.model.properties().iter().find(|p| p.name == name) {
if p.expectation == Expectation::Sometimes {
panic!("Please use `example(\"{}\")` for this `sometimes` property.", name);
}
self.discoveries.get(name).map(|mapref| self.path(*mapref.value()))
} else {
let available: Vec<_> = self.model.properties().iter().map(|p| p.name).collect();
panic!("Unknown property. requested={}, available={:?}", name, available);
}
}
pub fn path(&self, fp: Fingerprint) -> Path<M::State, M::Action> {
let model = &self.model;
let sources = &self.sources;
let mut digests = Vec::new();
let mut next_digest = fp;
while let Some(source) = sources.get(&next_digest) {
match *source {
Some(prev_digest) => {
digests.push(next_digest);
next_digest = prev_digest;
},
None => {
digests.push(next_digest);
break;
},
}
}
let init_states = model.init_states();
let mut last_state = init_states.into_iter()
.find(|s| fingerprint(&s) == digests.pop().unwrap())
.unwrap();
let mut output = Vec::new();
while let Some(next_digest) = digests.pop() {
let mut actions = Vec::new();
model.actions(
&last_state,
&mut actions);
let (action, next_state) = model
.next_steps(&last_state).into_iter()
.find_map(|(a,s)| {
if fingerprint(&s) == next_digest {
Some((a, s))
} else {
None
}
}).expect("state matching recorded digest");
output.push((last_state, Some(action)));
last_state = next_state;
}
output.push((last_state, None));
Path(output)
}
pub fn check_and_report(&mut self, w: &mut impl std::io::Write) {
use std::cmp::max;
use std::time::Instant;
let method_start = Instant::now();
let mut block_size = 32_768;
loop {
let block_start = Instant::now();
if self.check(block_size).is_done() {
let elapsed = method_start.elapsed().as_secs();
for mapref in self.discoveries.iter() {
let (name, fp) = mapref.pair();
writeln!(w, "== {} ==", name).unwrap();
for action in self.path(*fp).into_actions() {
writeln!(w, "ACTION: {:?}", action).unwrap();
}
}
writeln!(w, "Complete. generated={}, pending={}, sec={}",
self.sources.len(),
self.pending.len(),
elapsed
).unwrap();
return;
}
let block_elapsed = block_start.elapsed().as_secs();
if block_elapsed > 0 {
println!("{} states pending after {} sec. Continuing.",
self.pending.len(),
method_start.elapsed().as_secs());
}
if block_elapsed < 2 { block_size = 3 * block_size / 2; }
else if block_elapsed > 10 { block_size = max(1, block_size / 2); }
}
}
pub fn is_done(&self) -> bool {
let remaining_properties = Self::remaining_properties(self.model.properties(), &self.discoveries);
remaining_properties.is_empty() || self.pending.is_empty()
}
fn remaining_properties(
properties: Vec<Property<M>>,
discoveries: &DashMap<&str, Fingerprint>
) -> Vec<Property<M>> {
properties.into_iter().filter(|p| !discoveries.contains_key(p.name)).collect()
}
pub fn generated_count(&self) -> usize {
self.sources.len()
}
pub fn generated_fingerprints(&self) -> HashSet<Fingerprint> {
self.sources.iter().map(|rm| *rm.key()).collect()
}
pub fn assert_properties(&self) -> &Self {
for p in self.model.properties() {
match p.expectation {
Expectation::Always => self.assert_no_counterexample(p.name),
Expectation::Eventually => self.assert_no_counterexample(p.name),
Expectation::Sometimes => { self.assert_example(p.name); },
}
}
self
}
pub fn assert_example(&self, name: &'static str) -> Path<M::State, M::Action> {
if let Some(path) = self.example(name) {
return path;
}
assert!(self.is_done(),
"Example for '{}' not found, but model checking is incomplete.", name);
panic!("Example for '{}' not found. `stateright::explorer` may be useful for debugging.", name);
}
pub fn assert_counterexample(&self, name: &'static str) -> Path<M::State, M::Action> {
if let Some(path) = self.counterexample(name) {
return path;
}
assert!(self.is_done(),
"Counterexample for '{}' not found, but model checking is incomplete.", name);
panic!("Counterexample for '{}' not found. `stateright::explorer` may be useful for debugging.", name);
}
pub fn assert_no_example(&self, name: &'static str) {
if let Some(example) = self.example(name) {
let last_state = format!("{:#?}", example.last_state());
let actions = example.into_actions()
.iter()
.map(|a| format!("{:?}", a))
.collect::<Vec<_>>()
.join("\n");
panic!("Example for '{}' found.\n\n== ACTIONS ==\n{}\n\n== LAST STATE ==\n{}",
name, actions, last_state);
}
assert!(self.is_done(),
"Example for '{}' not found, but model checking is incomplete.",
name);
}
pub fn assert_no_counterexample(&self, name: &'static str) {
if let Some(counterexample) = self.counterexample(name) {
let last_state = format!("{:#?}", counterexample.last_state());
let actions = counterexample.into_actions()
.iter()
.map(|a| format!("{:?}", a))
.collect::<Vec<_>>()
.join("\n");
panic!("Counterexample for '{}' found.\n\n== ACTIONS ==\n{}\n\n== LAST STATE ==\n{}",
name, actions, last_state);
}
assert!(self.is_done(),
"Counterexample for '{}' not found, but model checking is incomplete.",
name);
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::test_util::linear_equation_solver::*;
#[test]
fn records_states() {
let h = |a: u8, b: u8| fingerprint(&(a, b));
let state_space = LinearEquation { a: 2, b: 10, c: 14 }.checker()
.check(100).generated_fingerprints();
assert!(state_space.contains(&h(0, 0)));
assert!(state_space.contains(&h(1, 0)));
assert!(state_space.contains(&h(0, 1)));
assert!(state_space.contains(&h(2, 0)));
assert!(state_space.contains(&h(1, 1)));
assert!(state_space.contains(&h(0, 2)));
assert!(state_space.contains(&h(3, 0)));
assert!(state_space.contains(&h(2, 1)));
assert_eq!(state_space.len(), 115);
}
#[test]
fn can_complete_by_enumerating_all_states() {
let mut checker = LinearEquation { a: 2, b: 4, c: 7 }.checker();
assert_eq!(checker.check(10).example("solvable"), None);
assert_eq!(checker.is_done(), false);
assert_eq!(checker.pending.len(), 5);
assert_eq!(checker.sources.len(), 15);
assert_eq!(checker.check(100_000).is_done(), true);
checker.assert_no_example("solvable");
assert_eq!(checker.sources.len(), 256 * 256);
}
#[test]
fn can_complete_by_eliminating_properties() {
let mut checker = LinearEquation { a: 1, b: 2, c: 3 }.checker();
assert_eq!(
checker.check(100).assert_example("solvable"),
Path(vec![
((0, 0), Some(Guess::IncreaseX)),
((1, 0), Some(Guess::IncreaseY)),
((1, 1), None),
]));
assert_eq!(checker.pending.len(), 15);
assert_eq!(checker.sources.len(), 115);
checker.check(100);
assert_eq!(checker.pending.len(), 15);
assert_eq!(checker.sources.len(), 115);
}
#[test]
fn report_includes_property_names_and_paths() {
let mut written: Vec<u8> = Vec::new();
LinearEquation { a: 2, b: 10, c: 14 }.checker().check_and_report(&mut written);
let output = String::from_utf8(written).unwrap();
assert!(
output.starts_with("\
== solvable ==\n\
ACTION: IncreaseX\n\
ACTION: IncreaseX\n\
ACTION: IncreaseY\n\
Complete. generated=33024, pending=256, sec="),
"Output did not start as expected (see test). output={:?}`", output);
}
}