Induction
Let's get a little bit abstract and meta. The incremental BMC approach we have seen in previous chapters can itself be seen as a transition system. To do this formally, we would need a whole bunch of notions from logics which are far beyond this book's scope. No matter, we can discuss BMC as a transition system informally, with words. Writing counterexample constantly is quite tedious, we will use the much shorter cex (plural cex-s) contraction instead. Also, assume there is only one candidate for simplicity.
We can see BMC as having a state: the unrolling depth k
from the initial state at which it
currently is. We can say it produces a boolean output that's true
if a cex for the candidate
exists at depth k
. BMC's starting point is "check the initial states for a cex (of the
candidate)", which we can rephrase as
init
: unroll the system at depth0
from the initial state and check for cex.
A BMC transition amounts to unrolling the transition relation one step further (depth k
), and
check for falsification at k
. Rephrased to echo init
's formulation, we get
trans(k-1, k)
: from an unrolling atk-1
from the initial states with no cex, unroll tok
and check for a cex (withk > 0
).
So now we have a representation of BMC as a transition system with its init
predicate and trans
relation.
It would be interesting to find a candidate invariant for BMC and try to prove it over the
transition system representation of BMC we just introduced. As some readers have probably intuited,
BMC actually has the property that it always finds the shortest cex possible in terms of depth
(number of unrollings). More precisely, it finds one of them: if BMC produces a cex (trace of k
states) of length k
, there might be other cex-s of length k
(and of length i > k
). However,
there cannot exist a shorter cex (of length i < k
). More generally:
candidate(k)
: when BMC is unrolled atk
from the initial states, then no cex of length0 ≤ i < k
exists.
To be clear, our goal is to find a way to prove that candidate
is an invariant for BMC, which
is represented as a transition system by the init
predicate and trans
relation above.
The question now is "How can we prove that BMC verifies candidate
?", or "What would such a
proof look like?". Back when we discussed transition system, we managed to prove a few properties
regarding the stopwatch system's transition relation. For instance, we previously proved that
assuming cnt_0 > 7
, then we cannot have cnt_1 = 0
without reset_1
being true
if state 1
is a successor of state 0
.
So we could start building a proof for candidate
by checking if it is a consequence of trans
:
is it true that trans(k-1, k) ⇒ candidate(k)
?. Let's assume we have an unrolling at depth k-1
from the initial states with no cex; by trans
, the next BMC step is to unroll to k
and check
for a cex at k
. Looking at candidate
now, is it the case that no cex of length i < k
can
exist?
Well no, looking only at the assumptions we made we cannot draw that conclusion: trans
tells us
there is no cex at k-1
, but that's it. We know nothing of potential cex-s at i < k - 1
. So,
candidate
is not a consequence of trans
. All hope is not lost: our tiny proofs over the
stopwatch system actually made assumptions about the previous state. "Assuming cnt_0 > 7
,
then we cannot have cnt_1 = 0
without ..."
Instead of checking if candidate
is a consequence of trans
(which it is not as we just saw) we
could check trans
preserves candidate
. Since trans
relates succeeding states k-1
and k
,
we say that trans
preserves candidate
if candidate(k-1) ∧ trans(k-1, k) ⇒ candidate(k)
. That
is, "states verifying candidate
cannot, in one application of trans
, reach a state falsifying
candidate
".
Is it the case though? We take the same assumptions as above, when we looked at a transition from
k-1
to k
, and also assume candidate
at k-1
: "no cex of length i < k-1
exists". By
trans
, we unroll to k
and check for a cex at k
. Can there be cex of length i < k
? No:
trans
tells us there was no cex at k-1
, and our new candidate
-assumption in the previous
state tells us no cex of length i < kh-1
exists.
Sweet, trans
preserves candidate
: from a state verifying candidate
, we can only reach states
that also verify candidate
. Hence, a state verifying candidate
cannot lead to a state
falsifying it by repeated applications of trans
. Is this enough to decide candidate
to be an
invariant for BMC? No, unless we show all our initial states verify candidate
.
Imagine for instance we got BMC wrong and started at depth 1
instead of 0
, with exactly the
same trans
. Then, since we never checked for a cex at 0
, candidate
has no reason to be true
at 1
. More generally, if our initial states do not all verify candidate
, then the fact that
trans
preserves candidate
is not helpful because it says nothing about transitions from a
state that does not verify candidate
.
Do(es) BMC's initial state(s) verify candidate
then? That is, when BMC is unrolled at 0
from
the initial states, is it possible that a cex of length i
with 0 ≤ i < 0
exists? Well, since
there is no i
such that 0 ≤ i < 0
, then no.
Putting both results together we have
- BMC's starting point verifies
candidate
, i.e.init ⇒ candidate(0)
, and - BMC's notion of transition preserves
candidate
, i.e.candidate(k-1) ∧ trans(k-1, k) ⇒ candidate(k)
.
Then, by induction, executions of BMC cannot ever falsify candidate
.
One Last Short SMT Dive
To sum up, given a transition system 𝕋
with state variables s
and specified by (init(s), trans(s, s'))
, we can invoke induction to prove that candidate(s)
is an invariant for 𝕋
if
we have shown both base and step:
- base: for all
s
,init(s) ⇒ candidate(s)
; - step: for all
s
ands'
,candidate(s) ∧ trans(s, s') ⇒ candidate(s')
.
We can rephrase these two in terms of SMT solving and formula (un)satisfiability:
- base:
init(s) ∧ ¬candidate(s)
is unsatisfiable; - step:
candidate(s) ∧ trans(s, s') ∧ ¬candidate(s')
is unsatisfiable.
We all know what this means: it is finally time to achieve our dream of proving that the
stopwatch's counter is always positive. I will use the version that starts from cnt = 0
, not the
one where it can take an arbitrary positive value. Both versions work for this proof, but the
former is better for obtaining non-trivial (length 0
) counterexamples if we need to.
Readers eager to learn by doing can start from any of the previous SMT examples involving BMC and try to conduct the proof themselves. The SMT examples for BMC have definitions for both
init
andtrans
ready to use. The easy version consists in having an SMT-LIB script for the base case, and another one for the step case. Solution in the Stopwatch Base and Stopwatch Step sections.Bonus points if you can do both with just one script, using activation literals. Perfect score if you can do both checks using only one activation literal, solely for the purpose of (de)activating
init
.Perfect score solution in the Stopwatch Actlit section.
Base
SMT examples for BMC from previous chapters already have definitions for the stopwatch's init
and
trans
. For convenience and readability, we define a candidate
predicate over the system's
counter:
(define-fun candidate ((|s.cnt| Int)) Bool
(>= |s.cnt| 0)
)
In the base check, we are only concerned with the initial states. All we need to do
- is declare our state variables,
- assert that they are a legal initial state,
- assert that they falsify our candidate, and
- ask whether this is satisfiable.
(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 (candidate cnt_0)))
; Is there a state that's initial but does not verify `candidate`?
(check-sat)
Z3 easily answers our question.
Full code in the Stopwatch Base section below.
> z3 test.smt2
unsat
Done: we proved the base case, step is next, let's move on.
Step
For the step check, which we are doing separately (in a different file) from base, we first
need to declare two states since we need to check that trans
preserves candidate
.
; 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)
Next, we want to ask whether it is possible to have
- state
0
verifycandidate
, - state
1
be a successor of state0
, and - state
0
falsifycandidate
.
(assert (candidate cnt_0))
(assert (trans
start_stop_0 reset_0 is_counting_0 cnt_0
start_stop_1 reset_1 is_counting_1 cnt_1
))
(assert (not (candidate cnt_1)))
; Is there a state verifying `candidate` that can
; reach a state falsifying it in one transition?
(check-sat)
And that's it. Unless Z3 decides to answer sat
for some reason, we are done.
Full code in the Stopwatch Step section below.
> play drumroll.wav
> z3 test.smt2
unsat
Done and done. Both base and step hold, and thus we can finally invoke induction and conclude
that cnt ≥ 0
is an invariant for the stopwatch system.
Before we move on to mikino's induction mode, some readers might want to check out Section Stopwatch Actlit below. In it, we conduct base and step in succession in a single file using an activation literal. That's not all we do however, there is a clever trick that we use for the checks. The trick is discussed in details in the comments.
Full Code For All Examples
Stopwatch Base
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)
)
)
(define-fun candidate ((|s.cnt| Int)) Bool
(>= |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 (candidate cnt_0)))
; Is there a state that's initial but does not verify `candidate`?
(check-sat)
Output:
> z3 test.smt2
unsat
Stopwatch Step
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)
)
)
(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|
)
)
)
)
)
(define-fun candidate ((|s.cnt| Int)) Bool
(>= |s.cnt| 0)
)
; 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)
(assert (candidate cnt_0))
(assert (trans
start_stop_0 reset_0 is_counting_0 cnt_0
start_stop_1 reset_1 is_counting_1 cnt_1
))
(assert (not (candidate cnt_1)))
; Is there a state verifying `candidate` that can
; reach a state falsifying it in one transition?
(check-sat)
Output:
> z3 test.smt2
unsat
Stopwatch Actlit
Expand this for the full code.
Until now, we performed the step check of an induction proof by giving ourselves a state 0
and
a state 1
such that
trans(s_0, s_1)
: state1
is state0
's successor,candidate(s_0)
: state0
verifies the candidate,¬candidate(s_1)
: state1
falsifies the candidate.
Now, the example below swaps the state indices 0
and 1
. That is:
trans(s_1, s_0)
: state0
is state1
's successor,candidate(s_1)
: state1
verifies the candidate,¬candidate(s_0)
: state0
falsifies the candidate.
This does not really change anything by itself, the check is same except that the indices have changed. We do need to be careful to extract the counterexample correctly though.
The reason we present this version is that this reverse-unrolling version lets us keep state 0
as the last state of the trace, i.e. the state on which the falsification occurs. In normal
unrolling, if we wanted to unroll the transition relation more, we would need to introduce s_2
,
deactivate ¬candidate(s_1)
, assert candidate(s_1)
, and conditionally assert ¬candidate(s_2)
.
With reverse-unrolling we can just say s_2
is s_1
's previous state, and assert
candidate(s_2)
. We are not unrolling more than once here (a process called k
-induction), but
this reverse-unrolling trick is still convenient w.r.t. the base check. The base check asserts
¬candidate(s_0)
, which step also needs.
; 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)
)
)
(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|
)
)
)
)
)
(define-fun candidate ((|s.cnt| Int)) Bool
(>= |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)
; Wait, shouldn't this be under an activation literal?
(assert (not (candidate cnt_0)))
; No because we're being very clever. More on that in the
; step check below.
(echo "base check, expecting `unsat`")
(declare-const base_actlit Bool)
(assert (=> base_actlit
(init start_stop_0 reset_0 is_counting_0 cnt_0)
))
(check-sat-assuming (base_actlit))
(declare-const start_stop_1 Bool)
(declare-const reset_1 Bool)
(declare-const is_counting_1 Bool)
(declare-const cnt_1 Int)
(echo "step check, expecting `unsat`")
(assert
(trans
; State `1` is first?
start_stop_1 reset_1 is_counting_1 cnt_1
; State `0` is its successor?
start_stop_0 reset_0 is_counting_0 cnt_0
)
)
(assert (candidate cnt_1))
; Most unhorthodox. We are reverse-unrolling™.
; When we checked for base above, we asserted `(not (candidate cnt_0))`
; **unconditionnaly**. So at this point, we have
; - candidate(s_1)
; - trans(s_1, s_0)
; - ¬candidate(s_0)
;
; By unrolling backwards, with `s_0` the *last* state, we can reverse-unroll
; without changing anything, just adding a state `s_2` and asserting
; `trans(s_2, s_1)`.
;
; This is useful when doing "k-induction", which requires unrolling more than
; once in step (and base), and because SMT-solvers work by **learning** facts
; about the SMT instance they work on. These facts are *kept between checks*.
; So, Z3 will learn facts from the three assertions above in the `check-sat`
; below. If we reverse-unroll once more by adding an `s_2` as the predecessor
; of `s_1`, then Z3 will be able to re-use facts it learnt since we only added
; to the instance, but did not change anything that was there.
(check-sat)
(echo "induction proof complete")
Output:
> z3 test.smt2
base check, expecting `unsat`
unsat
step check, expecting `unsat`
unsat
induction proof complete