Induction: Mikino and Step Cex-s
BMC is not the only thing mikino can do. It also has an induction mode which is activated with the
check
sub-command.
Pro tip: remember the
--smt_log <DIR>
flag from mikino'sbmc
mode? It specifies a directory where mikino will create one SMT-LIB 2 file per solver it uses, and log all the commands issued to that solver. Mikino'scheck
mode has it too:> mikino check --smt_log smt_traces test.mkn > tree smt_traces/ smt_traces ├── base.smt2 └── step.smt2
This can be useful to inspect the exact checks mikino is performing and modify/relaunch them.
Let's make sure mikino is able to prove that cnt ≥ 0
on the stopwatch system. I am using the
version of the system that starts at 0
#![allow(unused)] fn main() { /// Initial predicate. init { is_counting = start_stop, cnt = 0, } }
with a cnt ≥ 0
candidate as well as a few falsifiable other candidates:
#![allow(unused)] fn main() { /// Candidate invariants, or "Proof Objectives" (`po_s`). candidates { "cnt is positive": cnt ≥ 0, "cnt ≤ 2": cnt ≤ 2, "cnt ≤ 4": cnt ≤ 4, } }
Let's run mikino in check
mode on this system, full code in the Full Code section.
> mikino check test.mkn
checking base case...
success: all candidate(s) hold in the base state
checking step case...
failed: the following candidate(s) are not inductive:
- `cnt ≤ 2` = (<= cnt 2)
|=| Step k
| cnt = 2
| is_counting = false
| reset = false
| start_stop = false
|=| Step k + 1
| cnt = 3
| is_counting = true
| reset = false
| start_stop = true
|=|
- `cnt ≤ 4` = (<= cnt 4)
|=| Step k
| cnt = 4
| is_counting = true
| reset = false
| start_stop = false
|=| Step k + 1
| cnt = 5
| is_counting = true
| reset = false
| start_stop = false
|=|
|===| Induction attempt result
| - all candidates hold in the initial state(s)
|
| - the following candidate(s) are not inductive (not preserved by the transition relation)
| `cnt ≤ 2`
| `cnt ≤ 4`
|
| - system might be unsafe, some candidate(s) are not inductive
|
| - the following candidate(s) hold in the initial state(s) and are inductive
| and thus hold in all reachable states of the system:
| `cnt is positive`
|===|
Looking just at the final result, mikino is telling us that all candidates hold in the initial state(s). So, the base part of the induction proof holds for all candidates. Next, mikino reports that two of the candidates are not preserved by the transition relation. Since the check part of the induction proof does not hold for these candidates, mikino does not know whether they hold and tells us that the system might be unsafe. Based on the checks mikino performed, it cannot decide whether they are invariants or not.
Last, mikino reports that cnt is positive
is actually preserved by the transition relation
meaning step, in addition to base, holds for this candidate. Mikino can thus conclude that it
is an invariant for the system.
If we take a closer look at mikino's output during the step checks, we see that mikino is doing
more that just saying some step checks failed. When they do, mikino also outputs a step
counterexample (cex). Remember what a step check is: for any state k
- assuming state
k
verifiescandidate
, - assuming state
k+1
is a successor of statek
, - is it possible that state
k+1
falsifiescandidate
?
If it is possible, it means that there exists a state k
verifying candidate
which has a
successor state k+1
falsifying candidate
. Under the hood, this means the solver answered sat
to mikino's step check-sat
query, meaning mikino can invoke get-model
for step cex: a pair
of succeeding states showing that candidate
is not necessarily preserved by the transition
relation.
For candidate cnt ≥ 2
for instance, mikino lets us know that from a state where cnt = 2
which
verifies the candidate, it is possible in one transition to reach a state where cnt = 3
which
falsifies the candidate. Mikino gives us the values of all state variables so that we know how
precisely the bad state can be reached. There might be more than one step cex, as is the case for
candidate cnt ≥ 2
: mikino (the SMT solver, really) decides to start counting in step k+1
in the
step cex, but we could get the same result if we were already counting in step k
.
This notion of step cex is important because, on real systems, candidates tend not to be inductive. We will discuss this in the next chapter, but a candidate can be an invariant without being inductive. There are various ways we can go about proving that a non-inductive candidate is an invariant, but most of them (including the one we will discuss soon) involve manually or automatically looking at step cex-s to understand why the candidate is not inductive and "act" accordingly. (The meaning of "act" depends on the actual technique used to get around non-inductiveness.)
Run BMC, Again
We can change the mikino command we used to instruct mikino to run BMC on the candidates it failed
to prove. For instance, mikino check --bmc test.mkn
will run BMC after the proof attempts in an
attempt to falsify remaining candidates. More precisely, this command will run BMC without a
maximum unrolling depth.
On this simple example, we already know the remaining candidates are falsifiable in a small number
of transitions. When we do not have this information, it can be a good idea to specify a maximum
unrolling depth with --bmc_max <int>
just like we did in bmc
mode. Note that using the
--bmc_max
flag automatically activates BMC, so we don't need the --bmc
flag anymore.
On our example
#![allow(unused)] fn main() { /// Candidate invariants, or "Proof Objectives" (`po_s`). candidates { "cnt is positive": cnt ≥ 0, "cnt ≤ 2": cnt ≤ 2, "cnt ≤ 4": cnt ≤ 4, } }
the last two candidates should be falsifiable at depth 3
and 5
respectively. Let's make sure
this is the case.
> mikino check --bmc_max 5 test.mkn
checking base case...
success: all candidate(s) hold in the base state
checking step case...
failed: the following candidate(s) are not inductive:
- `cnt ≤ 2` = (<= cnt 2)
|=| Step k
| cnt = 2
| is_counting = false
| reset = false
| start_stop = false
|=| Step k + 1
| cnt = 3
| is_counting = true
| reset = false
| start_stop = true
|=|
- `cnt ≤ 4` = (<= cnt 4)
|=| Step k
| cnt = 4
| is_counting = true
| reset = false
| start_stop = false
|=| Step k + 1
| cnt = 5
| is_counting = true
| reset = false
| start_stop = false
|=|
|===| Induction attempt result
| - all candidates hold in the initial state(s)
|
| - the following candidate(s) are not inductive (not preserved by the transition relation)
| `cnt ≤ 2`
| `cnt ≤ 4`
|
| - system might be unsafe, some candidate(s) are not inductive
|
| - the following candidate(s) hold in the initial state(s) and are inductive
| and thus hold in all reachable states of the system:
| `cnt is positive`
|===|
running BMC, looking for falsifications for 2 candidate(s)...
checking for falsifications at depth 0
checking for falsifications at depth 1
checking for falsifications at depth 2
checking for falsifications at depth 3
found a falsification at depth 3:
- `cnt ≤ 2` = (<= cnt 2)
|=| Step 0
| cnt = 0
| is_counting = true
| reset = false
| start_stop = true
|=| Step 1
| cnt = 1
| is_counting = true
| reset = false
| start_stop = false
|=| Step 2
| cnt = 2
| is_counting = true
| reset = false
| start_stop = false
|=| Step 3
| cnt = 3
| is_counting = true
| reset = false
| start_stop = false
|=|
checking for falsifications at depth 4
checking for falsifications at depth 5
found a falsification at depth 5:
- `cnt ≤ 4` = (<= cnt 4)
|=| Step 0
| cnt = 0
| is_counting = true
| reset = false
| start_stop = true
|=| Step 1
| cnt = 1
| is_counting = true
| reset = false
| start_stop = false
|=| Step 2
| cnt = 2
| is_counting = true
| reset = false
| start_stop = false
|=| Step 3
| cnt = 3
| is_counting = true
| reset = false
| start_stop = false
|=| Step 4
| cnt = 4
| is_counting = true
| reset = false
| start_stop = false
|=| Step 5
| cnt = 5
| is_counting = true
| reset = false
| start_stop = false
|=|
|===| Bmc result
| - found a falsification for the following candidate(s)
| `cnt ≤ 2`
| `cnt ≤ 4`
|
| - system is unsafe
|===|
Full Code
Expand this for the full code.
#![allow(unused)] fn main() { /// State variables. svars { /// Inputs of the system. start_stop reset: bool, /// Keeps track of whether we are counting. is_counting: bool, /// Counts time, output of the system. cnt: int, } /// Initial predicate. init { is_counting = start_stop, cnt = 0, } /// Transition relation. trans { // The *next* value of `is_counting` is equal to... 'is_counting = if 'start_stop { // ...if `'start_stop` is true, then the negation of // its *previous* value... ¬is_counting } else { // ...otherwise, it is equal to its previous value. is_counting }, 'cnt = if 'reset { 0 } else if 'is_counting { cnt + 1 } else { cnt }, } /// Candidate invariants, or "Proof Objectives" (`po_s`). candidates { "cnt is positive": cnt ≥ 0, "cnt ≤ 2": cnt ≤ 2, "cnt ≤ 4": cnt ≤ 4, } }
Output:
> mikino bmc --bmc_max 10 test.mkn
checking base case...
success: all candidate(s) hold in the base state
checking step case...
failed: the following candidate(s) are not inductive:
- `cnt ≤ 2` = (<= cnt 2)
|=| Step k
| cnt = 2
| is_counting = false
| reset = false
| start_stop = false
|=| Step k + 1
| cnt = 3
| is_counting = true
| reset = false
| start_stop = true
|=|
- `cnt ≤ 4` = (<= cnt 4)
|=| Step k
| cnt = 4
| is_counting = true
| reset = false
| start_stop = false
|=| Step k + 1
| cnt = 5
| is_counting = true
| reset = false
| start_stop = false
|=|
|===| Induction attempt result
| - all candidates hold in the initial state(s)
|
| - the following candidate(s) are not inductive (not preserved by the transition relation)
| `cnt ≤ 2`
| `cnt ≤ 4`
|
| - system might be unsafe, some candidate(s) are not inductive
|
| - the following candidate(s) hold in the initial state(s) and are inductive
| and thus hold in all reachable states of the system:
| `cnt is positive`
|===|