diff --git a/Cargo.lock b/Cargo.lock index 7375530a..453eaa3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,6 +104,18 @@ dependencies = [ "typenum", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -118,6 +130,7 @@ name = "bounded" version = "0.1.0" dependencies = [ "biodivine-lib-bdd", + "bitvec", "cadical", "fly", "fxhash", @@ -460,6 +473,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "fxhash" version = "0.2.1" @@ -895,6 +914,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -1156,6 +1181,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "temporal-verifier" version = "0.1.0" @@ -1568,6 +1599,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/bounded/Cargo.toml b/bounded/Cargo.toml index ae42d4fa..7410b0a8 100644 --- a/bounded/Cargo.toml +++ b/bounded/Cargo.toml @@ -12,3 +12,4 @@ thiserror = "1.0.40" biodivine-lib-bdd = "0.5.1" cadical = "0.1.14" fxhash = "0.2.1" +bitvec = "1.0.1" diff --git a/bounded/src/set.rs b/bounded/src/set.rs index 2fb79e0d..da45341e 100644 --- a/bounded/src/set.rs +++ b/bounded/src/set.rs @@ -4,6 +4,7 @@ //! A bounded model checker for flyvy programs. Use `translate` to turn a flyvy `Module` //! into a `BoundedProgram`, then use `interpret` to evaluate it. +use bitvec::prelude::*; use fly::{ouritertools::OurItertools, semantics::*, syntax::*, transitions::*}; use itertools::Itertools; use std::collections::VecDeque; @@ -72,7 +73,7 @@ pub fn check( print_timing: bool, ) -> Result { let (program, context) = translate(module, universe, print_timing)?; - match interpret(&program, depth, compress_traces, print_timing) { + match interpret(&program, depth, compress_traces, print_timing, &context) { InterpreterResult::Unknown => Ok(CheckerAnswer::Unknown), InterpreterResult::Convergence => Ok(CheckerAnswer::Convergence), InterpreterResult::Counterexample(trace) => { @@ -157,38 +158,39 @@ impl Context<'_> { /// Compile-time upper bound on the bounded universe size. const STATE_LEN: usize = 128; -type StateData = u8; -const STATE_DATA_BITS: usize = std::mem::size_of::() * 8; /// A state in the bounded system. Conceptually, this is an interpretation of the signature on the /// bounded universe. We represent states concretely as a bitvector, where each bit represents the /// presence of a tuple in a relation. The order of the bits is determined by [Context]. -#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd)] -struct BoundedState([StateData; STATE_LEN / STATE_DATA_BITS]); +#[derive(Clone, Copy, Eq, PartialOrd)] +struct BoundedState(BitArr!(for STATE_LEN)); + +// Go word by word instead of bit by bit. +impl std::hash::Hash for BoundedState { + fn hash(&self, h: &mut H) + where + H: std::hash::Hasher, + { + self.0.as_raw_slice().hash(h) + } +} +impl PartialEq for BoundedState { + fn eq(&self, other: &BoundedState) -> bool { + self.0.as_raw_slice().eq(other.0.as_raw_slice()) + } +} impl BoundedState { - const ZERO: BoundedState = BoundedState([0; STATE_LEN / STATE_DATA_BITS]); + const ZERO: BoundedState = BoundedState(BitArray::ZERO); fn get(&self, index: usize) -> bool { assert!(index < STATE_LEN, "STATE_LEN is too small"); - let idx = index / STATE_DATA_BITS; - // `STATE_DATA_BITS - 1 - ` fixes ordering - let bit = STATE_DATA_BITS - 1 - (index % STATE_DATA_BITS); - - ((self.0[idx] >> bit) & 1) != 0 + self.0[index] } fn set(&mut self, index: usize, value: bool) { assert!(index < STATE_LEN, "STATE_LEN is too small"); - let idx = index / STATE_DATA_BITS; - // `STATE_DATA_BITS - 1 - ` fixes ordering - let bit = STATE_DATA_BITS - 1 - (index % STATE_DATA_BITS); - - if value { - self.0[idx] |= 1 << bit; - } else { - self.0[idx] &= !(1 << bit); - } + self.0.set(index, value); } } @@ -1067,18 +1069,19 @@ fn interpret( max_depth: Option, compress_traces: TraceCompression, print_timing: bool, + context: &Context, ) -> InterpreterResult { // States we have seen so far. - let mut seen: HashSet = program.inits.iter().cloned().collect(); - + let mut seen = IsoStateSet::new(context); // The BFS queue, i.e., states on the frontier that need to be explored. // The queue is always a subset of seen. - let mut queue: VecDeque = program - .inits - .iter() - .cloned() - .map(|state| Trace::new(state, compress_traces)) - .collect(); + let mut queue: VecDeque = VecDeque::new(); + + for init in &program.inits { + if seen.insert(init) { + queue.push_back(Trace::new(*init, compress_traces)); + } + } let mut transitions = Transitions::new(); for tr in &program.trs { @@ -1102,7 +1105,7 @@ fn interpret( println!( "considering new depth: {current_depth}. \ queue length is {}. seen {} unique states.", - queue.len(), + queue.len() + 1, // include current state seen.len() ); } @@ -1120,17 +1123,7 @@ fn interpret( tr.updates .iter() .for_each(|update| next.set(update.index, update.formula.evaluate(state))); - if !seen.contains(&next) { - seen.insert(next); - if seen.len() % 1_000_000 == 0 { - let elapsed = start_time.elapsed().as_secs_f64(); - println!( - "progress report: ({elapsed:0.1}s since start) considering depth {current_depth}. \ - queue length is {}. visited {} states.", - queue.len(), - seen.len() - ); - } + if seen.insert(&next) { let mut trace = trace.clone(); trace.push(next); queue.push_back(trace); @@ -1208,6 +1201,242 @@ impl<'a> Transitions<'a> { } } +/// Can answer the question "have I seen a state that is isomorphic to this one before"? +struct IsoStateSet<'a> { + context: &'a Context<'a>, + data: HashMap>, +} + +impl<'a> IsoStateSet<'a> { + fn new(context: &'a Context) -> IsoStateSet<'a> { + IsoStateSet { + context, + data: Default::default(), + } + } + + fn len(&self) -> usize { + self.data.values().map(Vec::len).sum() + } + + // true if insertion was successful + // false if element was already present + fn insert(&mut self, x: &BoundedState) -> bool { + let (fingerprint, xp) = self.fingerprint(x); + if let Some(vec) = self.data.get_mut(&fingerprint) { + if vec + .iter() + .any(|(y, yp)| is_isomorphic(self.context, x, &xp, y, yp)) + { + false + } else { + vec.push((*x, xp)); + true + } + } else { + self.data.insert(fingerprint.clone(), vec![(*x, xp)]); + true + } + } + + fn fingerprint(&self, state: &BoundedState) -> (Fingerprint, Partition) { + let mut previous: HashMap<(&str, usize), usize> = self + .context + .signature + .sorts + .iter() + .enumerate() + .flat_map(|(i, s)| (0..self.context.universe[s]).map(move |e| ((s.as_str(), e), i))) + .collect(); + + for _ in 0..self.context.universe.values().sum() { + use std::collections::BTreeMap; + let mut current: HashMap<(&str, usize), BTreeMap<(&str, usize, Vec