Unrolling and BMC
Here we finally start doing things. We will perform a kind of analysis called BMC. While not a proof technique in general, it is a very useful falsification technique and paves the way towards induction.
In the previous chapter, we played with our running example using Z3 by
- defining the transition relation as a
define-fun
,
Expand for a refresher on this definition.
(define-fun trans
(
; "Previous" state.
(|s.start_stop| Bool)
(|s.reset| Bool)
(|s.is_counting| Bool)
(|s.cnt| Int)
; "Next" state.
(|s'.start_stop| Bool)
(|s'.reset| Bool)
(|s'.is_counting| Bool)
(|s'.cnt| Int)
)
Bool
(and
(=> |s'.start_stop|
(= |s'.is_counting| (not |s.is_counting|))
)
(=> (not |s'.start_stop|)
(= |s'.is_counting| |s.is_counting|)
)
(= |s'.cnt|
(ite |s'.reset|
0
(ite |s'.is_counting|
(+ |s.cnt| 1)
|s.cnt|
)
)
)
)
)
- declaring two states
0
and1
,
Expand for a refresher on these declarations.
; "Previous" state.
(declare-const start_stop_0 Bool)
(declare-const reset_0 Bool)
(declare-const is_counting_0 Bool)
(declare-const cnt_0 Int)
; "Next" state.
(declare-const start_stop_1 Bool)
(declare-const reset_1 Bool)
(declare-const is_counting_1 Bool)
(declare-const cnt_1 Int)
- asserting the transition relation between state
0
and state1
, and
Expand for a refresher on this assertion.
(assert (trans
start_stop_0 reset_0 is_counting_0 cnt_0
start_stop_1 reset_1 is_counting_1 cnt_1
))
- querying Z3 by constraining state
0
and/or state1
to inspect the transition relation and prove some basic properties over it.
Expand for the last assertion and the queries.
(assert (and
; constraints over state 0
(> cnt_0 7)
(not is_counting_0)
; constraints over state 1's inputs
start_stop_1
(not reset_1)
))
(check-sat)
(get-model)
Unrolling
Now, this process of asserting the transition relation between two states 0
and 1
effectively
enforces the constraint that state 1
must be a legal successor of state 0
. This process is
called unrolling the transition relation, or unrolling the system, or just unrolling.
So far, we have only unrolled once to relate state 0
and state 1
. We can unroll more than once,
simply by declaring more states and relate 0
to 1
, 1
to 2
, etc. by asserting the
transition relation over the appropriate state variables.
Expand for an example of unrolling the system thrice.
; ANCHOR: all
; ANCHOR: trans_def
(define-fun trans
(
; "Previous" state.
(|s.start_stop| Bool)
(|s.reset| Bool)
(|s.is_counting| Bool)
(|s.cnt| Int)
; "Next" state.
(|s'.start_stop| Bool)
(|s'.reset| Bool)
(|s'.is_counting| Bool)
(|s'.cnt| Int)
)
Bool
(and
(=> |s'.start_stop|
(= |s'.is_counting| (not |s.is_counting|))
)
(=> (not |s'.start_stop|)
(= |s'.is_counting| |s.is_counting|)
)
(= |s'.cnt|
(ite |s'.reset|
0
(ite |s'.is_counting|
(+ |s.cnt| 1)
|s.cnt|
)
)
)
)
)
; ANCHOR_END: trans_def
; ANCHOR: states_def
; State 0.
(declare-const start_stop_0 Bool)
(declare-const reset_0 Bool)
(declare-const is_counting_0 Bool)
(declare-const cnt_0 Int)
; State 1.
(declare-const start_stop_1 Bool)
(declare-const reset_1 Bool)
(declare-const is_counting_1 Bool)
(declare-const cnt_1 Int)
; State 2.
(declare-const start_stop_2 Bool)
(declare-const reset_2 Bool)
(declare-const is_counting_2 Bool)
(declare-const cnt_2 Int)
; State 3.
(declare-const start_stop_3 Bool)
(declare-const reset_3 Bool)
(declare-const is_counting_3 Bool)
(declare-const cnt_3 Int)
; ANCHOR_END: states_def
; ANCHOR: unroll_1
(assert (trans
start_stop_0 reset_0 is_counting_0 cnt_0
start_stop_1 reset_1 is_counting_1 cnt_1
))
(assert (trans
start_stop_1 reset_1 is_counting_1 cnt_1
start_stop_2 reset_2 is_counting_2 cnt_2
))
(assert (trans
start_stop_2 reset_2 is_counting_2 cnt_2
start_stop_3 reset_3 is_counting_3 cnt_3
))
; ANCHOR_END: unroll_1
; ANCHOR: state_constraints
(assert (and
; starting from `cnt > 7`,
(> cnt_0 7)
; can we reach a state where `cnt = 5` in three transitions?
(= cnt_3 5)
))
(check-sat)
; ANCHOR_END: state_constraints
; ANCHOR_END: all
Output:
> z3 test.smt2
unsat
Reachability
Before we actually get to BMC, let's define a few notions. Remember that transition systems have
state variables, and that a state is a valuation of all these state variables. Let's say a state
s
is reachable if the system, starting from one of its initial states, can reach s
by
applying its transition relation n
times, where n
is an arbitrary natural number.
Note that
n
can be0
, i.e. the initial states of a system are reachable, naturally.
This notion of reachability is usually extended to apply to state predicates. For instance, in
the stopwatch system, we can say that the state predicate cnt > 5
is reachable: just start from
0
and keep counting.
Using this notion, we can talk about the set of all reachable states for a given system, called its reachable state space. This set can be infinite, and even when it's not, actually constructing this set for realistic systems tends to be impractical. A tool that tries to construct this set is called explicit-state. SMT-based approaches are almost always implicit-state, as SMT solvers reason at theory-level about booleans, integers... directly to assess whether a given formula is satisfiable. The only time actual values (and thus states) are produced is when the formula is satisfiable and we ask for a model.
While generally inefficient, tools such as TLA+ do manage to scale the explicit-state approach to impressively large systems. Also, it should be noted that when such a tool manages to terminate, i.e. compute the entire reachable state space, they are able to (dis)prove properties that are much more complex than anything we will do here. TLA+ for instance can reason about linear temporal logic formulas which are far beyond the scope of this book.
Let's introduce one more notion, that of invariants. Given some transition system, we can write
down a state predicate (such as cnt ≥ 0
) and wonder whether it holds (evaluates to true
) on
all reachable states. If it does, then that state predicate is an invariant of the system, or
holds for the system.
From now on, we will be interested in transition systems with some candidate invariants. That is, we will have "properties" and we will have to (dis)prove that they are invariants for the system. We will say a system is safe if all the candidate invariants are actually invariants. Otherwise, the system is unsafe.
Fantastic Counterexamples and How to Find Them
When a candidate invariant I
is not an actual invariant, it means there is at least one
reachable state s
that falsifies I
, i.e. I(s)
evaluates to false
. Since s
is
a reachable state, it means that there exists a trace of states s_0
, s_1
, ... s_n
such that
s_0
is an initial state, meaninginit(s_0)
istrue
,∀ i ∈ [0, n[
,s_i+1
is a successor ofs_i
, meaningtrans(s_i, s_i+1)
istrue
, ands_n = s
, meaningI(s_n)
isfalse
.
Assuming we are reporting to someone/something, we want to do better than just say unsafe
when a
candidate invariant does not hold for the system. Ideally, we should produce a witness of the
candidate's falsification: a counterexample, which would be a trace of succeeding states leading
to a state falsifying the candidate.
Thankfully, we have an SMT solver to do this. Can we actually explore the reachable state space and look for a falsification given what we have seen so far?
A first approach to doing this would be to write a bunch of assertions that are satisfiable if and
only if there exists such a counterexample trace of some arbitrary length. Unfortunately, we cannot
really do this as unrolling is a manual process: we declare state i
, then assert the relation
between state i-1
and state i
. We cannot write a finite number of assertions that encode an
arbitrary number of unrollings for the SMT solver to reason about. You can go ahead and try it, but
it will not work. At least not without quantifiers (∀
, ∃
), which would not scale well at all.
Now that we are frustrated by this dead-end approach, let's forget about our ideal goal and try tackle something simpler: can we look for a falsification of the candidate invariants in the initial states?
Let's get back to our running stopwatch example, and add a candidate invariant: cnt ≥ 0
. Recall
that the stopwatch's initial predicate is
init(s) ≜ (s.reset ⇒ s.cnt = 0) ∧ (s.is_counting = s.start_stop)
Now, we need to design some assertions for the SMT solver such that they are satisfiable if and
only if there exists an initial state that falsifies our candidate. This is quite similar to the
unrolling we did in the previous chapter, where we wrote assertions that were satisfiable if and
only if state 1
was a successor of state 0
, and cnt_1
was something else than 0
, cnt_0
or
cnt_0 + 1
.
Following the same approach, we first need to define-fun
our initial state predicate.
(define-fun init
(
(|s.start_stop| Bool)
(|s.reset| Bool)
(|s.is_counting| Bool)
(|s.cnt| Int)
)
Bool
(and
(= |s.is_counting| |s.start_stop|)
(=> |s.reset| (= |s.cnt| 0))
)
)
That was easy. Next up, declare a state so that we can assert init
on it.
(declare-const start_stop_0 Bool)
(declare-const reset_0 Bool)
(declare-const is_counting_0 Bool)
(declare-const cnt_0 Int)
Then we write the init
assertion.
(assert
(init start_stop_0 reset_0 is_counting_0 cnt_0)
)
So easy. We want the SMT solver to look for a falsification of our candidate cnt ≥ 0
, so we just
assert that.
(assert
(not (>= cnt_0 0))
)
(check-sat)
(get-model)
If Z3 answers sat
to these constraints, it means it found some values for the state variables
such that they
- represent an actual initial state, and
- falsify our candidate.
Full code in the Version 1 section below.
> z3 test.smt2
sat
(
(define-fun reset_0 () Bool
false)
(define-fun cnt_0 () Int
(- 1))
(define-fun start_stop_0 () Bool
false)
(define-fun is_counting_0 () Bool
false)
)
Did it work? Let's look at our initial predicate again:
init(s) ≜ (s.reset ⇒ s.cnt = 0) ∧ (s.is_counting = s.start_stop)
The specification of the system did not really say anything about the initial value cnt
should
have. At least not besides the fact that it should be 0
when reset
is true
. Hence, when
reset
is false
, cnt
is not constrained in any way. In a real program, one could see this as a
use-before-init problem where we declare (but not initialize) a cnt
variable, set it to 0
if
reset
is true
, but do nothing otherwise: cnt
could be anything (including an invalid integer).
So our system is unsafe: the candidate does not hold. The specification was not precise enough.
Let's say from now on that the specification tells us cnt
is initially an arbitrary positive
integer. Let's fix our initial predicate.
(define-fun init
(
(|s.start_stop| Bool)
(|s.reset| Bool)
(|s.is_counting| Bool)
(|s.cnt| Int)
)
Bool
(and
(= |s.is_counting| |s.start_stop|)
(=> |s.reset| (= |s.cnt| 0))
(>= |s.cnt| 0)
; ^^^^^^^^^^^^^^~~~~ fixed the specification
)
)
We do not need to change anything else, we can just run the same check on the updated initial predicate.
Full code in the Version 2 section below.
> z3 test.smt2
unsat
Perfect: by answering unsat
, Z3 is telling us that it proved that no valuation of s_0
is such
that it is an initial state and it falsifies the candidate invariant. We just proved something: no
falsification of the candidate is reachable in 0
transitions. Nice.
Can we keep going? Is the candidate falsifiable in 1
transitions, or 7
, or more? Of course we
can, thanks to unrolling. We previously used unrolling to start from some arbitrary state s_0
and
perform checks about its potential successor(s). If we just force s_0
to be initial, like we just
did, we can then unroll once to ask Z3 whether a successor s_1
of s_0
can falsify the
candidate. If not, we can unroll once more, and so on until we find a falsification.
Congratulations to us, we just invented BMC.
The stopwatch system has an infinite reachable state space since
cnt
can reach any integer value (just keep incrementing it). Start actually proving candidates in the next chapter, this one focuses solely on finding counterexamples.
BMC
Bounded Model-Checking (BMC) is a forward exploration of the reachable state space of a transition system. The term forward exploration refers to the fact that BMC starts from the initial states and explores the state space by unrolling the transition relation incrementally.
As a technique BMC is explicitly bounded: it explores the reachable state space up to some depth (number of unrollings). That is, as long as the reachable state space is infinite, BMC cannot prove anything because it will never finish exploring the state space. All it can do is disprove candidates.
By this point, I expect most readers to be able to write SMT-LIB code that corresponds to a BMC check for a given number of unrollings. Just to make sure, the BMC with Unrolling section showcases a BMC check where the transition relation is unrolled twice.
Warning: slightly (more) technical and practical discussion ahead. What follows is not mandatory to understand the upcoming chapters, but I think it is quite interesting and definitely mandatory for writing your own (efficient) verification engine.
Let's discuss a few practical points regarding BMC. Mainly, the fact that it is incremental and
that what we do at a given step depends on the solver's previous answer. For instance, say we have
several candidate invariants and we are checking whether we can falsify some of them by unrolling
k
times. Say also the solver answers yes (sat
) and gives us a model. Then what we want to do is
check which candidates are falsified (using the model), and keep BMC-ing the candidates that were
not falsified. We got a counterexample for some of the candidates, but there might be a different
counterexample at the same depth falsifying other candidates. If not (unsat
), we move on and
check the remaining candidates at k+1
and so on. Maybe they can be falsified by unrolling more
than k
times.
So, this whole process is interactive: what BMC does depends on what the solver previously said. In
the next chapter, I will introduce mikino which is a tool that will perform BMC (and more) for us.
The way mikino conducts BMC is by launching Z3 as a separate process and feed it definitions,
declarations, assertions, check-sat
s and get-model
s on its standard input. Z3 produces answers
on its standard output, which mikino looks at to decide if/how to continue and what to stream to
the input of the Z3 process.
Based on what we have done so far, this would require restarting the solver each time. Say Z3
answers unsat
when we unroll k
times. It means that we have an assertion of the negation of the
candidate(s) for s_k
which made our whole SMT-LIB instance (all the assertions together) unsat
:
(assert (not (candidate s_k)))
. Now, this assertion is only there for our check at depth k
, to
ask for a falsification of the candidate(s). Still, we cannot move on and unroll at k+1
: this
assertion will still be there, and the instance will remain unsat
.
So, instead of restarting Z3, we can make the assertion of the negation of the candidates conditional. That is, we can give ourselves a boolean flag that activates this assertion. Such a flag is called an activation literal, or actlit.
Say we are performing a BMC check at depth k
. First, we need to declare this "boolean flag".
(declare-const actlit_k Bool)
Next, we write our assertion of the negation of the candidate(s) in such a way that actlit_k
needs to be true for the assertion to be active.
(assert
(=> actlit_k
(not (and (candidate_1 s_k) (candidate_2 s_k) ...))
)
)
Notice that if actlit_k
is false
, then this assertion is trivially true
and thus does not
contribute to whether other assertions are satisfiable. This is because false ⇒ P
is always
true
, regardless of what P
is. In particular, false ⇒ false
is true.
The last thing we need now is to perform a check-sat
assuming actlit_k
is true. We could
(assert actlit_k)
but that would not solve our problem: we cannot go back and undo this
assertion, hence the negation of the candidates is active, hence the whole instance is unsat
.
Instead, we can use the check-sat-assuming
SMT-LIB command. This command takes a list of boolean
variables and forces to check-sat
our assertions assuming these variables are true
, but
without actually asserting them. Meaning that, after the check-sat, these variables are still
unconstrained.
In particular, it means we can perform a check-sat-assuming
on actlit_k
, and then just assert
actlit_k
to be false to effectively deactivate the assertion of the negation of the candidates.
This is all a bit abstract, let's see it in action on the stopwatch system. The BMC with
Unrolling section shows an unrolling of the system at depth 2
with the
corresponding check for candidate cnt ≥ 0
, and just that check. Let's modify it so that it uses
activation literals to perform all checks up to depth 2
.
The first check to perform is at depth 0
, so all we asserted so far is that state 0
is initial.
; State 0 is initial.
(assert
(init start_stop_0 reset_0 is_counting_0 cnt_0)
)
Next we declare our activation literal for the upcoming check.
; Activation literal for the first check.
(declare-const actlit_0 Bool)
Then we conditionally assert the negation of the candidate.
; Conditional assertion of the negation of the candidate at 0.
(assert (=> actlit_0
(not (>= cnt_0 0))
))
Then comes the check itself, using the brand new check-sat-assuming
command we just discussed.
; Check-sat, assuming `actlit_0` is true.
(echo "; info: falsification at depth 0?")
(check-sat-assuming ( actlit_0 ))
Notice the use of the echo
command which takes a string and causes the solver to output said
string to its standard output. Remember that we will perform more than one check, so we use echo
to keep track of what question the sat
/unsat
answers are for.
As we already saw, this check will produce unsat
: there is no falsification of this candidate at
depth 0
(or at any depth, but we have not proved that yet).
; Solver answers `unsat`, we want to unroll the system some more.
; Before we do that, we deactivate the actlit.
(assert (not actlit_0))
; At this point, the solver realized the conditional assertion
; of the negation of the candidate is trivially `true`, and will
; ignore it from now on.
As a sanity check, we can check-sat
right after we deactivated actlit_0
. We just got an unsat
because we assumed actlit_0
, so if the deactivation of the negation of the candidate failed then
a regular check-sat
would also yield unsat
. If deactivation worked, we should get sat
because
(init s_0)
, our only active assertion, is sat
.
(echo "; info: making sure assertion at 0 is not active anymore, expecting `sat`")
(check-sat)
We can now keep on unrolling and check-sat-assuming
, since the assertion of the negation of the
candidate should now be deactivated.
(assert
(trans
start_stop_0 reset_0 is_counting_0 cnt_0
start_stop_1 reset_1 is_counting_1 cnt_1
)
)
(declare-const actlit_1 Bool)
(assert (=> actlit_1
(not (>= cnt_1 0))
))
(echo "; info: falsification at depth 1?")
(check-sat-assuming ( actlit_1 ))
(assert (not actlit_1))
(echo "; info: making sure assertion at 1 is not active anymore, expecting `sat`")
(check-sat)
We can keep going like that for as long as we want. Running Z3 on SMT-LIB code enforcing this
methodology up to depth 2
produces the following output (see the BMC with
Actlits section below for the full code).
; info: falsification at depth 0?
unsat
; info: making sure assertion at 0 is not active anymore, expecting `sat`
sat
; info: falsification at depth 1?
unsat
; info: making sure assertion at 1 is not active anymore, expecting `sat`
sat
; info: falsification at depth 2?
unsat
; info: making sure assertion at 2 is not active anymore, expecting `sat`
sat
Pretty nice 😸. Let's never do this again manually to preserve our own sanity, and move on to the next chapter where we will introduce mikino to do all of this automatically.
Full Code For All Examples
Version 1
Expand this for the full code.
(define-fun init
(
(|s.start_stop| Bool)
(|s.reset| Bool)
(|s.is_counting| Bool)
(|s.cnt| Int)
)
Bool
(and
(= |s.is_counting| |s.start_stop|)
(=> |s.reset| (= |s.cnt| 0))
)
)
(declare-const start_stop_0 Bool)
(declare-const reset_0 Bool)
(declare-const is_counting_0 Bool)
(declare-const cnt_0 Int)
(assert
(init start_stop_0 reset_0 is_counting_0 cnt_0)
)
(assert
(not (>= cnt_0 0))
)
(check-sat)
(get-model)
Output:
> z3 test.smt2
sat
(
(define-fun reset_0 () Bool
false)
(define-fun cnt_0 () Int
(- 1))
(define-fun start_stop_0 () Bool
false)
(define-fun is_counting_0 () Bool
false)
)
Version 2
Expand this for the full code.
(define-fun init
(
(|s.start_stop| Bool)
(|s.reset| Bool)
(|s.is_counting| Bool)
(|s.cnt| Int)
)
Bool
(and
(= |s.is_counting| |s.start_stop|)
(=> |s.reset| (= |s.cnt| 0))
(>= |s.cnt| 0)
; ^^^^^^^^^^^^^^~~~~ fixed the specification
)
)
(declare-const start_stop_0 Bool)
(declare-const reset_0 Bool)
(declare-const is_counting_0 Bool)
(declare-const cnt_0 Int)
(assert
(init start_stop_0 reset_0 is_counting_0 cnt_0)
)
(assert
(not (>= cnt_0 0))
)
(check-sat)
Output:
> z3 test.smt2
unsat
BMC with Unrolling
Expand this for the full code.
; Initial predicate.
(define-fun init
(
(|s.start_stop| Bool)
(|s.reset| Bool)
(|s.is_counting| Bool)
(|s.cnt| Int)
)
Bool
(and
(= |s.is_counting| |s.start_stop|)
(=> |s.reset| (= |s.cnt| 0))
(>= |s.cnt| 0)
)
)
; Transition relation.
(define-fun trans
(
; "Previous" state.
(|s.start_stop| Bool)
(|s.reset| Bool)
(|s.is_counting| Bool)
(|s.cnt| Int)
; "Next" state.
(|s'.start_stop| Bool)
(|s'.reset| Bool)
(|s'.is_counting| Bool)
(|s'.cnt| Int)
)
Bool
(and
(=> |s'.start_stop|
(= |s'.is_counting| (not |s.is_counting|))
)
(=> (not |s'.start_stop|)
(= |s'.is_counting| |s.is_counting|)
)
(= |s'.cnt|
(ite |s'.reset|
0
(ite |s'.is_counting|
(+ |s.cnt| 1)
|s.cnt|
)
)
)
)
)
; State 0.
(declare-const start_stop_0 Bool)
(declare-const reset_0 Bool)
(declare-const is_counting_0 Bool)
(declare-const cnt_0 Int)
; State 1.
(declare-const start_stop_1 Bool)
(declare-const reset_1 Bool)
(declare-const is_counting_1 Bool)
(declare-const cnt_1 Int)
; State 2.
(declare-const start_stop_2 Bool)
(declare-const reset_2 Bool)
(declare-const is_counting_2 Bool)
(declare-const cnt_2 Int)
; State 0 is initial.
(assert
(init start_stop_0 reset_0 is_counting_0 cnt_0)
)
; State 1 is a successor of state 0.
(assert
(trans
start_stop_0 reset_0 is_counting_0 cnt_0
start_stop_1 reset_1 is_counting_1 cnt_1
)
)
; State 2 is a successor of state 1.
(assert
(trans
start_stop_1 reset_1 is_counting_1 cnt_1
start_stop_2 reset_2 is_counting_2 cnt_2
)
)
; State 2 must falsify the candidate.
(assert
(not (>= cnt_2 0))
)
; Is this possible?
(check-sat)
Output:
> z3 test.smt2
unsat
BMC with Actlits
Expand this for the full code.
; Initial predicate.
(define-fun init
(
(|s.start_stop| Bool)
(|s.reset| Bool)
(|s.is_counting| Bool)
(|s.cnt| Int)
)
Bool
(and
(= |s.is_counting| |s.start_stop|)
(=> |s.reset| (= |s.cnt| 0))
(>= |s.cnt| 0)
)
)
; Transition relation.
(define-fun trans
(
; "Previous" state.
(|s.start_stop| Bool)
(|s.reset| Bool)
(|s.is_counting| Bool)
(|s.cnt| Int)
; "Next" state.
(|s'.start_stop| Bool)
(|s'.reset| Bool)
(|s'.is_counting| Bool)
(|s'.cnt| Int)
)
Bool
(and
(=> |s'.start_stop|
(= |s'.is_counting| (not |s.is_counting|))
)
(=> (not |s'.start_stop|)
(= |s'.is_counting| |s.is_counting|)
)
(= |s'.cnt|
(ite |s'.reset|
0
(ite |s'.is_counting|
(+ |s.cnt| 1)
|s.cnt|
)
)
)
)
)
; State 0.
(declare-const start_stop_0 Bool)
(declare-const reset_0 Bool)
(declare-const is_counting_0 Bool)
(declare-const cnt_0 Int)
; State 1.
(declare-const start_stop_1 Bool)
(declare-const reset_1 Bool)
(declare-const is_counting_1 Bool)
(declare-const cnt_1 Int)
; State 2.
(declare-const start_stop_2 Bool)
(declare-const reset_2 Bool)
(declare-const is_counting_2 Bool)
(declare-const cnt_2 Int)
; State 0 is initial.
(assert
(init start_stop_0 reset_0 is_counting_0 cnt_0)
)
; Activation literal for the first check.
(declare-const actlit_0 Bool)
; Conditional assertion of the negation of the candidate at 0.
(assert (=> actlit_0
(not (>= cnt_0 0))
))
; Check-sat, assuming `actlit_0` is true.
(echo "; info: falsification at depth 0?")
(check-sat-assuming ( actlit_0 ))
; Solver answers `unsat`, we want to unroll the system some more.
; Before we do that, we deactivate the actlit.
(assert (not actlit_0))
; At this point, the solver realized the conditional assertion
; of the negation of the candidate is trivially `true`, and will
; ignore it from now on.
(echo "; info: making sure assertion at 0 is not active anymore, expecting `sat`")
(check-sat)
(assert
(trans
start_stop_0 reset_0 is_counting_0 cnt_0
start_stop_1 reset_1 is_counting_1 cnt_1
)
)
(declare-const actlit_1 Bool)
(assert (=> actlit_1
(not (>= cnt_1 0))
))
(echo "; info: falsification at depth 1?")
(check-sat-assuming ( actlit_1 ))
(assert (not actlit_1))
(echo "; info: making sure assertion at 1 is not active anymore, expecting `sat`")
(check-sat)
(assert
(trans
start_stop_1 reset_1 is_counting_1 cnt_1
start_stop_2 reset_2 is_counting_2 cnt_2
)
)
(declare-const actlit_2 Bool)
(assert (=> actlit_2
(not (>= cnt_2 0))
))
(echo "; info: falsification at depth 2?")
(check-sat-assuming ( actlit_2 ))
(assert (not actlit_2))
(echo "; info: making sure assertion at 2 is not active anymore, expecting `sat`")
(check-sat)
Output:
> z3 test.smt2
; info: falsification at depth 0?
unsat
; info: making sure assertion at 0 is not active anymore, expecting `sat`
sat
; info: falsification at depth 1?
unsat
; info: making sure assertion at 1 is not active anymore, expecting `sat`
sat
; info: falsification at depth 2?
unsat
; info: making sure assertion at 2 is not active anymore, expecting `sat`
sat