rust_formal_verification/algorithms/proof/
ic3_stateful_solver.rs

1//! This algorithm is an almost exact implementation of what is described in "SAT-Based Model Checking without Unrolling".
2//!
3//! Bradley, A.R. (2011). SAT-Based Model Checking without Unrolling.
4//! In: Jhala, R., Schmidt, D. (eds) Verification, Model Checking, and Abstract Interpretation.
5//! VMCAI 2011. Lecture Notes in Computer Science, vol 6538. Springer, Berlin,
6//! Heidelberg. https://doi.org/10.1007/978-3-642-18275-4_7
7//!
8//! Abstract: A new form of SAT-based symbolic model checking is described.
9//! Instead of unrolling the transition relation, it incrementally generates clauses that are
10//! inductive relative to (and augment) stepwise approximate reachability information.
11//! In this way, the algorithm gradually refines the property, eventually producing either an
12//! inductive strengthening of the property or a counterexample trace. Our experimental studies
13//! show that induction is a powerful tool for generalizing the unreachability of given error
14//! states: it can refine away many states at once, and it is effective at focusing the proof
15//! search on aspects of the transition system relevant to the property. Furthermore, the
16//! incremental structure of the algorithm lends itself to a parallel implementation.
17//!
18//! This implementation uses stateful sat solvers to gain speedup.
19
20// ************************************************************************************************
21// use
22// ************************************************************************************************
23
24use super::{FiniteStateTransitionSystemProver, ProofResult};
25use crate::{
26    formulas::{literal::VariableType, Clause, Cube, Literal, CNF},
27    models::FiniteStateTransitionSystem,
28    solvers::sat::{
29        stateful::{StatefulSatSolver, StatefulSatSolverHint},
30        SatResponse,
31    },
32};
33use priority_queue::PriorityQueue;
34use rand::rngs::ThreadRng;
35use rand::seq::SliceRandom;
36use rand::thread_rng;
37use std::cmp::{max, Reverse};
38use std::time;
39
40// ************************************************************************************************
41// Enum
42// ************************************************************************************************
43
44enum StrengthenResult {
45    Success,
46    Failure { _depth: VariableType },
47}
48
49enum InductivelyGeneralizeResult {
50    Success { n: usize },
51    Failure,
52}
53
54enum PushGeneralizeResult {
55    Success,
56    Failure,
57}
58
59enum SolverVariant {
60    Initial,
61    FiAndT(usize),
62    FiAndTAndNotPTag(usize),
63    Custom(CNF),
64}
65
66// ************************************************************************************************
67// struct
68// ************************************************************************************************
69
70pub struct IC3Stateful<T> {
71    // for the algorithm
72    clauses: Vec<CNF>,
73    fin_state: FiniteStateTransitionSystem,
74    rng: ThreadRng,
75
76    // stateful sat solvers for speedup
77    // Reminder: F0 == initial
78    fi_and_t_solvers: Vec<T>, // for each index i the solver holds Fi ^ T
79    initial_solver: T,        // houses just F0
80    fi_and_t_and_not_p_tag_solvers: Vec<T>, // Fi ^ T ^ !P'
81
82    // caching for speedup
83    initial: CNF,
84    transition: CNF,
85    p0: CNF,
86    not_p0: CNF,
87    not_p1: CNF,
88
89    // for printing
90    verbose: bool,
91    number_of_sat_calls: u32,
92    time_in_sat_calls: time::Duration,
93    start_time: time::Instant,
94}
95
96// ************************************************************************************************
97// impl
98// ************************************************************************************************
99
100impl<T: StatefulSatSolver> IC3Stateful<T> {
101    // ********************************************************************************************
102    // clauses
103    // ********************************************************************************************
104
105    fn add_clause_to_clauses(&mut self, index: usize, clause: &Clause) {
106        debug_assert_eq!(self.clauses.len(), self.fi_and_t_solvers.len());
107        debug_assert_eq!(
108            self.clauses.len(),
109            self.fi_and_t_and_not_p_tag_solvers.len()
110        );
111
112        self.clauses[index].add_clause(clause);
113        self.fi_and_t_solvers[index].add_cnf(&clause.to_cnf());
114        self.fi_and_t_and_not_p_tag_solvers[index].add_cnf(&clause.to_cnf());
115    }
116
117    fn get_clause_from_clauses(&self, index: usize) -> CNF {
118        self.clauses[index].to_owned()
119    }
120
121    fn push_extra_frame_to_clauses(&mut self) {
122        debug_assert_eq!(self.clauses.len(), self.fi_and_t_solvers.len());
123        debug_assert_eq!(
124            self.clauses.len(),
125            self.fi_and_t_and_not_p_tag_solvers.len()
126        );
127
128        self.clauses.push(CNF::new());
129
130        // update solvers
131        let mut fi_and_t = T::new(StatefulSatSolverHint::None);
132        fi_and_t.add_cnf(&self.transition);
133
134        let mut fi_and_t_and_not_p_tag = T::new(StatefulSatSolverHint::None);
135        fi_and_t_and_not_p_tag.add_cnf(&self.transition);
136        fi_and_t_and_not_p_tag.add_cnf(&self.not_p1);
137
138        if self.fi_and_t_solvers.is_empty() {
139            fi_and_t.add_cnf(&self.initial);
140            fi_and_t_and_not_p_tag.add_cnf(&self.initial);
141        } else {
142            fi_and_t.add_cnf(&self.p0);
143            fi_and_t_and_not_p_tag.add_cnf(&self.p0);
144        }
145
146        self.fi_and_t_solvers.push(fi_and_t);
147        self.fi_and_t_and_not_p_tag_solvers
148            .push(fi_and_t_and_not_p_tag);
149    }
150
151    // ********************************************************************************************
152    // sat calls
153    // ********************************************************************************************
154
155    fn sat_call(
156        &mut self,
157        solver_index: SolverVariant,
158        cube_assumptions: Option<&Cube>,
159        clause_assumptions: Option<&Clause>,
160    ) -> SatResponse {
161        self.number_of_sat_calls += 1;
162        let start_time = time::Instant::now();
163
164        // find solver
165        let result = match solver_index {
166            SolverVariant::Initial => self
167                .initial_solver
168                .solve(cube_assumptions, clause_assumptions),
169            SolverVariant::FiAndT(j) => {
170                self.fi_and_t_solvers[j].solve(cube_assumptions, clause_assumptions)
171            }
172            SolverVariant::FiAndTAndNotPTag(j) => {
173                self.fi_and_t_and_not_p_tag_solvers[j].solve(cube_assumptions, clause_assumptions)
174            }
175            SolverVariant::Custom(cnf) => {
176                let mut current_solver = T::new(StatefulSatSolverHint::None);
177                current_solver.add_cnf(&cnf);
178                current_solver.solve(cube_assumptions, clause_assumptions)
179            }
180        };
181
182        self.time_in_sat_calls += start_time.elapsed();
183        result
184    }
185
186    fn is_bad_reached_in_0_steps(&mut self) -> SatResponse {
187        // I ^ !P
188        let mut cnf = CNF::new();
189        cnf.append(&self.initial);
190        cnf.append(&self.not_p0);
191        self.sat_call(SolverVariant::Custom(cnf), None, None)
192    }
193
194    fn is_bad_reached_in_1_steps(&mut self) -> SatResponse {
195        // I ^ T ^ !P'
196        let mut cnf = CNF::new();
197        cnf.append(&self.initial);
198        cnf.append(&self.transition);
199        cnf.append(&self.not_p1);
200        self.sat_call(SolverVariant::Custom(cnf), None, None)
201    }
202
203    fn is_cube_reachable_in_1_step_from_fi(&mut self, i: usize, cube: &Cube) -> SatResponse {
204        // Fi ^ T ^ c'
205        let cube_tag = self.fin_state.add_tags_to_cube(cube, 1);
206        self.sat_call(SolverVariant::FiAndT(i), Some(&cube_tag), None)
207    }
208
209    fn is_bad_reachable_in_1_step_from_fi(&mut self, i: usize) -> SatResponse {
210        // Fi ^ T ^ !P'
211        self.sat_call(SolverVariant::FiAndTAndNotPTag(i), None, None)
212    }
213
214    fn is_fi_and_t_and_not_s_and_s_tag_sat(&mut self, i: usize, s: &Cube) -> bool {
215        // Fi ^ T ^ !s ^ s'
216        let s_tag = self.fin_state.add_tags_to_cube(s, 1);
217        let not_s = !(s.to_owned());
218
219        match self.sat_call(SolverVariant::FiAndT(i), Some(&s_tag), Some(&not_s)) {
220            SatResponse::UnSat => false,
221            SatResponse::Sat { assignment: _ } => true,
222        }
223    }
224
225    fn is_fi_and_t_and_clause_and_not_clause_tag_sat(&mut self, i: usize, d: &Clause) -> bool {
226        // Fi ^ T ^ d ^ !d’
227        let not_d_tag = self.fin_state.add_tags_to_cube(&(!(d.to_owned())), 1);
228
229        match self.sat_call(SolverVariant::FiAndT(i), Some(&not_d_tag), Some(d)) {
230            SatResponse::UnSat => false,
231            SatResponse::Sat { assignment: _ } => true,
232        }
233    }
234
235    fn does_cube_intersect_with_initial(&mut self, cube: &Cube) -> bool {
236        match self.sat_call(SolverVariant::Initial, Some(cube), None) {
237            SatResponse::UnSat => false,
238            SatResponse::Sat { assignment: _ } => true,
239        }
240    }
241
242    // ********************************************************************************************
243    // helper functions
244    // ********************************************************************************************
245
246    fn get_fk(&self, k: usize) -> CNF {
247        let mut clauses_fk = self.get_clause_from_clauses(k);
248        if k == 0 {
249            clauses_fk.append(&self.initial);
250        } else {
251            clauses_fk.append(&self.p0);
252        }
253        clauses_fk
254    }
255
256    fn propagate_clauses(&mut self, k: usize) {
257        for i in 1..(k + 1) {
258            let clauses_fi = self.get_clause_from_clauses(i);
259            for c in clauses_fi.iter() {
260                let check = self.is_cube_reachable_in_1_step_from_fi(i, &(!(c.to_owned())));
261                match check {
262                    SatResponse::UnSat => {
263                        // can propagate this property :)
264                        self.add_clause_to_clauses(i + 1, c);
265                    }
266                    SatResponse::Sat { assignment: _ } => {
267                        // can't propagate this clause :(
268                    }
269                }
270            }
271        }
272    }
273
274    fn is_clause_inductive_relative_to_fi(&mut self, d: &Clause, i: usize) -> bool {
275        // return !(Init ∧ ¬d) && !((Fi ∧ d)∧ Tr ∧ ¬d’)
276        if self.fin_state.is_cube_initial(&(!(d.to_owned()))) {
277            debug_assert!(self.does_cube_intersect_with_initial(&(!(d.to_owned()))));
278            return false;
279        } else {
280            debug_assert!(!self.does_cube_intersect_with_initial(&(!(d.to_owned()))));
281        }
282
283        !self.is_fi_and_t_and_clause_and_not_clause_tag_sat(i, d)
284    }
285
286    fn get_subclause_of_not_s_that_is_inductive_relative_to_fi(
287        &mut self,
288        s: &Cube,
289        i: usize,
290    ) -> Clause {
291        let c = !(s.to_owned());
292        let mut c_literals: Vec<Literal> = c.iter().map(|l| l.to_owned()).collect();
293        c_literals.shuffle(&mut self.rng);
294        let mut j = 0;
295        while j < c_literals.len() {
296            let removed = c_literals.swap_remove(j);
297            let d = Clause::new(&c_literals);
298            if self.is_clause_inductive_relative_to_fi(&d, i) {
299                // remove successful, j should remain the same
300            } else {
301                // undo remove
302                c_literals.push(removed);
303                let last_index = c_literals.len() - 1;
304                c_literals.swap(j, last_index);
305                // move on to next literal
306                j += 1;
307            }
308        }
309        Clause::new(&c_literals)
310    }
311
312    fn generate_clause(&mut self, s: &Cube, i: usize, _k: usize) {
313        let c = self.get_subclause_of_not_s_that_is_inductive_relative_to_fi(s, i);
314        for j in 1..(i + 2) {
315            self.add_clause_to_clauses(j, &c);
316        }
317    }
318
319    fn inductively_generalize(
320        &mut self,
321        s: &Cube,
322        min: isize,
323        k: usize,
324    ) -> InductivelyGeneralizeResult {
325        if min < 0 && self.is_fi_and_t_and_not_s_and_s_tag_sat(0, s) {
326            return InductivelyGeneralizeResult::Failure;
327        }
328
329        for i in max(1, min + 1).try_into().unwrap()..(k + 1) {
330            if self.is_fi_and_t_and_not_s_and_s_tag_sat(i, s) {
331                self.generate_clause(s, i - 1, k);
332                return InductivelyGeneralizeResult::Success { n: i - 1 };
333            }
334        }
335        self.generate_clause(s, k, k);
336        InductivelyGeneralizeResult::Success { n: k }
337    }
338
339    fn push_generalization(
340        &mut self,
341        states: &PriorityQueue<Cube, Reverse<usize>>,
342        k: usize,
343    ) -> PushGeneralizeResult {
344        let mut states = states.to_owned();
345        loop {
346            let (s, reversed_n) = states.pop().unwrap();
347            let n = reversed_n.0;
348            if n > k {
349                return PushGeneralizeResult::Success;
350            }
351            match self.is_cube_reachable_in_1_step_from_fi(n, &s) {
352                SatResponse::Sat { assignment } => {
353                    // we have to block p in order to block n.
354                    let p = self.fin_state.extract_state_from_assignment(&assignment);
355                    // println!("Should block p = {} from F{}", p, n - 1);
356                    match self.inductively_generalize(
357                        &p,
358                        <usize as TryInto<isize>>::try_into(n).unwrap() - 2,
359                        k,
360                    ) {
361                        InductivelyGeneralizeResult::Failure => {
362                            return PushGeneralizeResult::Failure;
363                        }
364                        InductivelyGeneralizeResult::Success { n: m } => {
365                            states.push(s, reversed_n);
366                            states.push(p, Reverse(m + 1));
367                        }
368                    }
369                }
370                SatResponse::UnSat => {
371                    // n can be blocked
372                    match self.inductively_generalize(&s, n.try_into().unwrap(), k) {
373                        InductivelyGeneralizeResult::Failure => {
374                            return PushGeneralizeResult::Failure;
375                        }
376                        InductivelyGeneralizeResult::Success { n: m } => {
377                            states.push(s.to_owned(), Reverse(m + 1));
378                        }
379                    }
380                }
381            }
382        }
383    }
384
385    fn print_progress_if_verbose(&self, k: usize) {
386        if self.verbose {
387            let clauses = self
388                .clauses
389                .iter()
390                .map(|c| c.len())
391                .rev()
392                .take(10)
393                .collect::<Vec<usize>>();
394            println!("IC3 is on k = {}, clauses lengths = {:?}", k, clauses);
395            println!("Number of SAT calls = {}", self.number_of_sat_calls);
396            println!(
397                "Time since start = {}",
398                self.start_time.elapsed().as_secs_f32()
399            );
400            println!(
401                "Time in SAT calls = {}",
402                self.time_in_sat_calls.as_secs_f32()
403            );
404        }
405    }
406
407    fn strengthen(&mut self, k: usize) -> StrengthenResult {
408        loop {
409            match self.is_bad_reachable_in_1_step_from_fi(k) {
410                SatResponse::UnSat => {
411                    break;
412                }
413                SatResponse::Sat { assignment } => {
414                    let s = self.fin_state.extract_state_from_assignment(&assignment);
415                    // println!("Should block s = {} from F{}", s, k - 1);
416                    match self.inductively_generalize(
417                        &s,
418                        <usize as TryInto<isize>>::try_into(k).unwrap() - 2,
419                        k,
420                    ) {
421                        InductivelyGeneralizeResult::Failure => {
422                            return StrengthenResult::Failure {
423                                _depth: k.try_into().unwrap(),
424                            };
425                        }
426                        InductivelyGeneralizeResult::Success { n } => {
427                            let mut queue = PriorityQueue::<Cube, Reverse<usize>>::new();
428                            queue.push(s, Reverse(n + 1));
429                            match self.push_generalization(&queue, k) {
430                                PushGeneralizeResult::Failure => {
431                                    return StrengthenResult::Failure {
432                                        _depth: k.try_into().unwrap(),
433                                    };
434                                }
435                                PushGeneralizeResult::Success => {}
436                            };
437                        }
438                    };
439                }
440            }
441        }
442
443        StrengthenResult::Success
444    }
445
446    // ********************************************************************************************
447    // API functions
448    // ********************************************************************************************
449
450    pub fn new(fin_state: &FiniteStateTransitionSystem, verbose: bool) -> Self {
451        let mut p0 = fin_state.get_state_to_safety_translation();
452        p0.append(&fin_state.get_safety_property().to_cnf());
453
454        let mut not_p0 = fin_state.get_state_to_safety_translation();
455        not_p0.append(&fin_state.get_unsafety_property().to_cnf());
456
457        let mut initial_solver = T::new(StatefulSatSolverHint::None);
458        initial_solver.add_cnf(&fin_state.get_initial_relation().to_cnf());
459
460        Self {
461            clauses: Vec::new(),
462            fin_state: fin_state.to_owned(),
463            fi_and_t_solvers: Vec::new(),
464            initial_solver,
465            fi_and_t_and_not_p_tag_solvers: Vec::new(),
466            rng: thread_rng(),
467            initial: fin_state.get_initial_relation().to_cnf(),
468            transition: fin_state.get_transition_relation(),
469            p0,
470            not_p0: not_p0.to_owned(),
471            not_p1: fin_state.add_tags_to_relation(&not_p0, 1),
472            verbose,
473            number_of_sat_calls: 0,
474            time_in_sat_calls: time::Duration::from_secs(0),
475            start_time: time::Instant::now(),
476        }
477    }
478
479    pub fn prove(&mut self) -> ProofResult {
480        // update start time.
481        self.start_time = time::Instant::now();
482
483        let init_and_not_p = self.is_bad_reached_in_0_steps();
484        match init_and_not_p {
485            SatResponse::Sat { assignment: _ } => return ProofResult::CTX { depth: 0 },
486            SatResponse::UnSat => (),
487        }
488        // debug_assert!(does_a_imply_b::<T>(&self.initial, &self.p0));
489
490        let init_and_tr_and_not_p_tag = self.is_bad_reached_in_1_steps();
491        match init_and_tr_and_not_p_tag {
492            SatResponse::Sat { assignment: _ } => return ProofResult::CTX { depth: 1 },
493            SatResponse::UnSat => (),
494        }
495
496        self.push_extra_frame_to_clauses();
497        self.push_extra_frame_to_clauses();
498        for k in 1.. {
499            self.push_extra_frame_to_clauses();
500            // debug_assert!(self.does_a_hold(k), "Bug in algorithm implementation found!!");
501            self.print_progress_if_verbose(k);
502            debug_assert_eq!(self.clauses.len(), (k + 2));
503            debug_assert_eq!(self.get_clause_from_clauses(0).len(), 0);
504            match self.strengthen(k) {
505                StrengthenResult::Success => {}
506                StrengthenResult::Failure { _depth: _ } => {
507                    return ProofResult::CTX {
508                        depth: k.try_into().unwrap(),
509                    };
510                }
511            };
512            self.propagate_clauses(k);
513            for i in 1..(k + 1) {
514                // all clauses in i+1 should be in i.
515                debug_assert!(self
516                    .get_clause_from_clauses(i + 1)
517                    .iter()
518                    .all(|c| self.get_clause_from_clauses(i).contains(c)));
519                if self.get_clause_from_clauses(i).len()
520                    == self.get_clause_from_clauses(i + 1).len()
521                {
522                    // todo: compare just the lengths
523                    self.print_progress_if_verbose(k);
524                    return ProofResult::Proof {
525                        invariant: self.get_fk(i),
526                    };
527                }
528            }
529        }
530        unreachable!();
531    }
532}
533
534// ************************************************************************************************
535// impl trait
536// ************************************************************************************************
537
538impl<T: StatefulSatSolver> FiniteStateTransitionSystemProver for IC3Stateful<T> {
539    fn new(fin_state: &FiniteStateTransitionSystem) -> Self {
540        IC3Stateful::new(fin_state, true)
541    }
542
543    fn prove(&mut self) -> ProofResult {
544        self.prove()
545    }
546}