Candidate Strengthening
Candidate strengthening is about making non-inductive candidates inductive (when possible). It is somewhat difficult to come up with an example that is
- interesting for strengthening,
- not too artificial, i.e. is relatively realistic for a developer, and
- is accessible without too much explanation.
While not extremely complex, the central example of this chapter and the manipulations in general are going to be more involved than what we have discussed so far.
This chapter also touches on the very high complexity and potential undecidability of verification.
This chapter is quite technical, and a bit theoretical. It is recommended that you understand all the concepts introduced so far before diving into this chapter. Some understanding of Rust could help in places, although it is by no means mandatory.
We have seen two kinds of candidate up to this point:
- inductive candidates, meaning induction can prove they are invariants, and
- falsifiable candidates, which can be falsified by running BMC for long enough.
Not all invariants are inductive. Meaning it is possible to get a step counterexample, i.e. the transition relation does not preserve the candidate, even though the candidate is an invariant and no legal trace of states can falsify it.
Array Value Grouping
Let's switch gears and look at an actual piece of (Rust) code. Assume we have an array arr
of
statically-known size len
of elements of type T
(arr
has type [T; len]
. Say we also have an
integer (usize
) value grouping
. What we want to do is fold over successive groups
(sub-arrays) of length grouping
of the elements of arr
.
In practice, we want to have a type Wrap
that stores arr
and grouping
, and exposes a fold
function over values of type &[T]
which is guaranteed to store exactly grouping
elements.
Fold implements an sliding, non-overlapping window of length grouping
over arr
. Here is what
using it would look like.
fn main() { // `len` ~~~~~~v let arr: [f64; 6] = [0., 1., 2., 3., 4., 5.]; // `grouping` ~~~~~~~~~~~~v let wrap = Wrap::new(arr, 3); let value_1 = wrap.fold(0., |acc, arr| { println!( "len: {} | arr[0]: {}, arr[1]: {}, arr[2]: {}", arr.len(), arr[0], arr[1], arr[2] ); assert_eq!(arr.len(), 3); let new_acc = acc + arr[0] * arr[1] * arr[2]; println!("acc: {}, new_acc: {}", acc, new_acc); new_acc }); println!("fold result: {}", value_1); println!("\n---------------------\n"); // `len` ~~~~~~v let arr: [f64; 6] = [0., 1., 2., 3., 4., 5.]; // `grouping` ~~~~~~~~~~~~v let wrap = Wrap::new(arr, 2); let value_2 = wrap.fold(0., |acc, arr| { println!( "len: {} | arr[0]: {}, arr[1]: {}", arr.len(), arr[0], arr[1] ); assert_eq!(arr.len(), 2); let new_acc = acc + arr[0] * arr[1]; println!("acc: {}, new_acc: {}", acc, new_acc); new_acc }); println!("fold result: {}", value_2); }
Which produces
len: 3 | arr[0]: 0, arr[1]: 1, arr[2]: 2
acc: 0, new_acc: 0
len: 3 | arr[0]: 3, arr[1]: 4, arr[2]: 5
acc: 0, new_acc: 60
fold result: 60
---------------------
len: 2 | arr[0]: 0, arr[1]: 1
acc: 0, new_acc: 0
len: 2 | arr[0]: 2, arr[1]: 3
acc: 0, new_acc: 6
len: 2 | arr[0]: 4, arr[1]: 5
acc: 6, new_acc: 26
fold result: 26
Full code available below. For now let's turn to how the fold
function would be implemented. Say
init
and next
are fold
's two arguments (initial value and fold function respectively). Then
the body of fold
could look something like
#![allow(unused)] fn main() { let mut acc = init; let mut i = 0; while i < len { let next_i = i + grouping; acc = next(acc, &arr[i..next_i]); i = next_i } acc }
- local variable
acc
is initiallyinit
, and the loop updates its value with the result of the call tonext
; - the
while
loop increments ani
index until it goes overlen
(length ofarr
); - at the end of each iteration, the new value of
i
isi + grouping
; - the call to
next
gives it the current value of the accumulator, and a slice fromi
toi + grouping
---remember thatn..m
is the interval[n, m[
:m
is not included in the interval; - when we exit the loop, we return the final value of
acc
.
Full code for this example, requires some Rust knowledge.
#![allow(non_upper_case_globals)] pub struct Wrap<T, const len: usize> { arr: [T; len], grouping: usize, } impl<T, const len: usize> Wrap<T, len> { /// Constructor. pub fn new(arr: [T; len], grouping: usize) -> Self { if len % grouping != 0 { panic!( "grouping value {} is not a multiple of array length ({})", grouping, len, ); } if grouping < 1 { panic!("illegal grouping value {}, must be > 0", grouping) } Self { arr, grouping } } /// Grouped fold. pub fn fold<Acc>(&self, init: Acc, mut next: impl FnMut(Acc, &[T]) -> Acc) -> Acc { let (arr, grouping) = (&self.arr, self.grouping); let mut acc = init; let mut i = 0; while i < len { let next_i = i + grouping; acc = next(acc, &arr[i..next_i]); i = next_i } acc } } fn main() { // `len` ~~~~~~v let arr: [f64; 6] = [0., 1., 2., 3., 4., 5.]; // `grouping` ~~~~~~~~~~~~v let wrap = Wrap::new(arr, 3); let value_1 = wrap.fold(0., |acc, arr| { println!( "len: {} | arr[0]: {}, arr[1]: {}, arr[2]: {}", arr.len(), arr[0], arr[1], arr[2] ); assert_eq!(arr.len(), 3); let new_acc = acc + arr[0] * arr[1] * arr[2]; println!("acc: {}, new_acc: {}", acc, new_acc); new_acc }); println!("fold result: {}", value_1); println!("\n---------------------\n"); // `len` ~~~~~~v let arr: [f64; 6] = [0., 1., 2., 3., 4., 5.]; // `grouping` ~~~~~~~~~~~~v let wrap = Wrap::new(arr, 2); let value_2 = wrap.fold(0., |acc, arr| { println!( "len: {} | arr[0]: {}, arr[1]: {}", arr.len(), arr[0], arr[1] ); assert_eq!(arr.len(), 2); let new_acc = acc + arr[0] * arr[1]; println!("acc: {}, new_acc: {}", acc, new_acc); new_acc }); println!("fold result: {}", value_2); }
Now let's think about this a little bit. The value &arr[i..next_i]
, where next_i
is i + grouping
, does not always make sense: i + grouping
might strictly superior to len
(arr
's
length). This cannot happen if len
is a multiple of grouping
however, and actually Wrap
's
constructor checks for this.
#![allow(unused)] fn main() { /// Constructor. pub fn new(arr: [T; len], grouping: usize) -> Self { if len % grouping != 0 { panic!( "grouping value {} is not a multiple of array length ({})", grouping, len, ); } if grouping < 1 { panic!("illegal grouping value {}, must be > 0", grouping) } }
There is also a check that grouping > 0
, otherwise fold
would not terminate when len > 0
. So
everything should be fine, right? Humans do not make very good proof engines, let's just prove it
to make sure.
Encoding
You are encouraged to think about how you would encode this verification problem so that mikino can handle it. You will probably not going to succeed unless you are already very familiar with this kind of encoding. May read a paragraph below or two, think about it some more, read some more, etc.
Mikino does not accept Rust code, we need to encode what we want to check. Writing an encoder for (a subset of) of programming language is a very challenging task. It requires an exact understanding of the semantics of the language and the ability to design encoding rules that are guaranteed to preserve it, or at least preserve the part relevant for the analysis. So let's not do that and encode it by hand.
#![allow(unused)] fn main() { let mut acc = init; let mut i = 0; while i < len { let next_i = i + grouping; acc = next(acc, &arr[i..next_i]); i = next_i } acc }
What do we want to check, i.e. what does it mean for this code to make sense? It's really a
matter of how i
is related to len
. Each iteration makes sense if the interval we use on arr
is legal, meaning i < len
and next_i = i + grouping ≤ len
(interval is exclusive on its upper
bound). We actually don't care about what's in arr
, just that its length is len
. Our encoding
can safely ignore arr
completely. Same for acc
, its value is not relevant for fold
to make
sense in how it accesses arr
. The only real state variable of interest in this loop is thus i
.
There a whole bunch of additional properties we might want to prove. For instance, that
arr
slices do not overlap. The point is that, whatever we are interested in proving, we should remove everything that is not necessary as we encode the program. However, the encoding should at least be sound: proving an invariant in the encoding must entail it is an invariant for the original program.Here, we are doing the encoding by hand and we can pick and choose what is encoded or left out. In an actual verification tool, the encoding process would be automatic. Such an automatic encoder might encode the whole semantics of the program (meaning dealing with arrays at SMT-level here) and hope for the best, or be clever enough to realize it can excise array-related things for this particular verification challenge. Writing a clever, automatic encoder is difficult and challenging, and can void all results if it is not sound.
Note that we have a few constants here: len
and grouping
do not change as the loop runs.
Unfortunately mikino does not have a notion of constant yet (contributions are welcome), so we
have to encode them as state variables along with i
.
#![allow(unused)] fn main() { svars { len grouping: int, i: int, done: bool, } }
The surprise state variable done
is a flag we are going to use to indicate whether the loop is
done or not. This allows our system to never end, which is a useful property for technical reasons
beyond the scope of this chapter.
We initialize the transition system encoding fold
's loop, as follows.
#![allow(unused)] fn main() { init { len ≥ 0, grouping ≥ 1, i = 0, done = ¬(i < len), len % grouping = 0, } }
The first two simply constrain the values for len
(usize
, thus ≥ 0
) and grouping
(checked
to be ≥ 1
by the constructor). Then i
is constrained to be equal to its initial value, 0
. The
done
flag is initially true if and only if i ≥ len
, which basically means len
is 0
---arr
is empty, the loop never starts.
Last, we have the assumption we need for the whole thing to work: len
is a multiple of
grouping
, which Wrap
's constructor checks.
The transition relation needs to do a few things. Our constants (encoded as state variables) need
to keep their value, since if we left them unconstrained they could take any value. We also need to
update the value of done
, and finally that of i
.
#![allow(unused)] fn main() { // Encodes iteration `(pre i)`, NOT iteration `i`. That is, `trans` encodes the // iteration where we access `arr` with the interval `(pre i)..((pre i) + grouping)`. trans { 'grouping = grouping, 'len = len, // NEW value of the `done` flag, if `true` there will be no further iteration. 'done = ¬('i < 'len), if done { // just maintain the value; 'i = i } else { // otherwise, `i` is incremented by `grouping`. 'i = i + grouping }, } }
Testing the Encoding
Before we do anything else, let's think of what happens if len = 8
and grouping = 4
. Initially
i = 0
, then it should increase to i = 4
, then to i = 8
at which point done = i ≥ len
becomes true
and the system stutters (its state variables stay the same forever).
We should make sure our encoding behaves like this. So, what we should ask mikino is "with len = 8
and grouping = 4
, is it possible to reach a state such that i = len
?". We can do this with
BMC: since we want to reach len = 8 ∧ grouping = 4 ∧ i = len
, we can just need ask BMC to try to
falsify ¬(len = 8 ∧ grouping = 4 ∧ i = len)
.
#![allow(unused)] fn main() { candidates { "candidate": ¬(len = 8 ⋀ grouping = 4 ⋀ i = len), } }
Mikino yields
> mikino bmc test.mkn
running BMC, looking for falsifications for 1 candidate(s)...
checking for falsifications at depth 0
checking for falsifications at depth 1
checking for falsifications at depth 2
found a falsification at depth 2:
- `candidate` = (not (and (= len 8) (= grouping 4) (= i len)))
|=| Step 0
| done = false
| grouping = 4
| i = 0
| len = 8
|=| Step 1
| done = false
| grouping = 4
| i = 4
| len = 8
|=| Step 2
| done = true
| grouping = 4
| i = 8
| len = 8
|=| Z3 produced the following unexpected values
| div0 ((x!0 int) (x!1 int)) int = 2
| mod0 ((x!0 int) (x!1 int)) int = 0
|=|
|===| Bmc result
| - found a falsification for the following candidate(s)
| `candidate`
|
| - system is unsafe
|===|
We got exactly what we expected, that's encouraging. Also, remember that we proved that BMC produces counterexamples of minimal length. Hence, no shorter trace of states can lead the system to falsify our property.
Full code available in the Version 1 section. You are encouraged to mess around with the example to get a better understanding of the encoding.
Mikino reports that Z3 produced unexpected values. This is not relevant for our discussion, but these functions are part of the model. Z3 is letting us know how it decided to handle division (modulo) by
0
. I won't go into deeper details here for space constraints, but this is a consequence of the fact that our system uses non-linear arithmetic, and non-linear division in particular. A (system made of) formula(s) uses non-linear arithmetic if it uses multiplication/division involving more than one variable (x * y
,x / y
). As opposed to linear arithmetic which only allows one variable to be used (3 * x
,x / 7
,0 / x
).SMT is not decidable when non-linear arithmetic is involved, but we will have more to say on that shortly as we will run into this exact problem.
Diving Head First
We start with trying to prove that the lower bound of the interval i..(i + grouping)
used on
arr
is legal, meaning to prove that i < len
. More precisely, ¬done ⇒ i < len
since on exit
(when done
is true
) we actually should have i = len
; let's prove done ⇒ i = len
as well.
#![allow(unused)] fn main() { candidates { "not done then i < len": ¬done ⇒ i < len, "done then i = len": done ⇒ i = len, } }
Full code in the Version 2 section. Running mikino in check
mode, we get:
> 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:
- `done then i = len` = (=> done (= i len))
|=| Step k
| done = false
| grouping = 2
| i = (- 1)
| len = 0
|=| Step k + 1
| done = true
| grouping = 2
| i = 1
| len = 0
|=|
|===| Induction attempt result
| - all candidates hold in the initial state(s)
|
| - the following candidate(s) are not inductive (not preserved by the transition relation)
| `done then i = len`
|
| - 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:
| `not done then i < len`
|===|
Well, the good news is that the lower bound of our interval is always legal: mikino proved it. We
get a counterexample (cex) for the second candidate though. A stupid cex too, as i
is obviously
positive. At least, it is obviously positive if you take into account the initial states (i
= 0),
which the step check of the induction proof does not do by definition. As far as the transition
relation is concerned, there is no reason for i
to be positive at some step k
.
Your first reflex might be to add this constraint to the transition relation ((pre i) ≥ 0
), which
is a terrible idea. That's the encoding of the original code, you should not touch it as this would
actively mask cex-s. Spurious cex-s in this case, but if you make any mistake when augmenting
trans
then you might block actual cex-s and safety proofs you might get would not say anything
about the original system.
The reason why, in this case, it would be safe to add (pre i) ≥ 0
(don't do it though) is
that it is an invariant, i.e. a consequence of the system's behavior. So there's no reason to
force it on trans
, we can just ask mikino to prove it instead.
#![allow(unused)] fn main() { candidates { "i ≥ 0": i ≥ 0, "not done then i < len": ¬done ⇒ i < len, "done then i = len": done ⇒ i = len, } }
Full code in the Version 3 section. Mikino tells us
> 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:
- `done then i = len` = (=> done (= i len))
|=| Step k
| done = false
| grouping = 2
| i = 0
| len = 1
|=| Step k + 1
| done = true
| grouping = 2
| i = 2
| len = 1
|=|
- `i ≥ 0` = (>= i 0)
|=| Step k
| done = false
| grouping = (- 1)
| i = 0
| len = 1
|=| Step k + 1
| done = false
| grouping = (- 1)
| i = (- 1)
| len = 1
|=|
|===| Induction attempt result
| - all candidates hold in the initial state(s)
|
| - the following candidate(s) are not inductive (not preserved by the transition relation)
| `done then i = len`
| `i ≥ 0`
|
| - 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:
| `not done then i < len`
|===|
Okay, now grouping
is negative. There is a pattern emerging: mikino (induction, really) is
stupid and needs to be told everything.
Strengthening 💪
When we add i ≥ 0
as a candidate, we are strengthening candidate done ⇒ i = len
. That is,
this candidate is not inductive on the raw system (made of init
and trans
). If we hope to
prove it by induction, we have to try to prove a stronger version (done ⇒ i = len) ∧ lemma
, for
example with lemma ≝ i ≥ 0
in the previous example. We added the lemma as a separate candidate,
but morally lemma
was not really what we wanted to prove, it was just a lemma we added to help
prove the main candidate (which failed). It did not work, but if we can find a lemma
for which
the proof goes through, then we won since the fact that (done ⇒ i = len) ∧ lemma
is an invariant
implies that done ⇒ i = len
is an invariant.
We are about to strengthen this candidate manually, but maybe you can see why many induction-based proof engine have some form of invariant discovery. Such techniques aim at discovering potentially complex lemmas about the system that strengthen the candidates when they are not inductive.
We could keep on asking mikino for step cex-s and come up with a candidate that will hopefully block them. But we have been doing this whole thing wrong.
The first thing we should do is write everything we expect to be an invariant. For instance we know
that len ≥ 0
, and that grouping ≥ 1
and len % grouping = 0
(both checked in the constructor).
Our first move should have been to write these candidates down (in addition to i ≥ 0
) and make
sure they are actually (inductive) invariants. Besides hopefully strengthening our original
candidates (done ⇒ i = len
and ¬done ⇒ i < len
), we must check that they are invariants since
if they are not something is very wrong ---the system, our encoding, our overall understanding, or
all of the above.
Note that, if the encoding process was automatic, these candidates could have been generated
automatically: len: usize
implies len ≥ 0
, same for i
, and the checks in the constructor
guarantee grouping ≥ 1
and len % grouping = 0
since neither len
nor grouping
ever change.
As we will see, this is not enough, and we will need to add another (candidate) invariant that is
less trivial to strengthen our real candidate successfully.
For now let's just add these new candidates
#![allow(unused)] fn main() { candidates { "not done then i < len": ¬done ⇒ i < len, "done then i = len": done ⇒ i = len, "i ≥ 0": i ≥ 0, // new "len ≥ 0": len ≥ 0, "grouping > 0": grouping > 0, "len grouping related": len % grouping = 0, } }
and run mikino. Full code in the Version 4 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:
- `done then i = len` = (=> done (= i len))
|=| Step k
| done = false
| grouping = 2
| i = 3
| len = 4
|=| Step k + 1
| done = true
| grouping = 2
| i = 5
| len = 4
|=| Z3 produced the following unexpected values
| div0 ((x!0 int) (x!1 int)) int = 2
| mod0 ((x!0 int) (x!1 int)) int = 0
|=|
|===| Induction attempt result
| - all candidates hold in the initial state(s)
|
| - the following candidate(s) are not inductive (not preserved by the transition relation)
| `done then i = len`
|
| - 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:
| `grouping > 0`
| `i ≥ 0`
| `len grouping related`
| `len ≥ 0`
| `not done then i < len`
|===|
At least the new candidates are actual (inductive) invariants. It was not enough to strengthen our
real candidate, but the cex we got is interesting. With len = 4
and grouping = 2
, the step
check is able to go from i = 3
to i = 5
and falsify the main candidate.
The problem here, as you might have guessed, is currently i
can really take any value as long as
its positive: no (candidate) invariant constrain it besides i ≥ 0
. Looking at the code for the
loop again
#![allow(unused)] fn main() { let mut acc = init; let mut i = 0; while i < len { let next_i = i + grouping; acc = next(acc, &arr[i..next_i]); i = next_i } acc }
we see that i
can only be a multiple of grouping
. But we cannot know until we try to prove it.
Also, will it strengthen our main candidate enough to make it inductive?
#![allow(unused)] fn main() { candidates { "not done then i < len": ¬done ⇒ i < len, "done then i = len": done ⇒ i = len, "i ≥ 0": i ≥ 0, "len ≥ 0": len ≥ 0, "grouping > 0": grouping > 0, "len grouping related": len % grouping = 0, // new "i % grouping = 0": i % grouping = 0, } }
We run mikino but, inconspicuously, we change the command used to run Z3 slightly (change z3
in
the string to whatever the path to your Z3 binary is). Full code in the Version 5
section.
> mikino --z3_cmd "z3 -T:5" check test.mkn
checking base case...
success: all candidate(s) hold in the base state
checking step case...
|===| Error
| smt-level error:
| - smt solver reported `timeout`
| - during step check
|===|
Adding -T:5
to the Z3 command tells Z3 to timeout after five seconds. Feel free to increase the
timeout, unless you are from the future and using a version Z3 that performs better on non-linear
arithmetic, the step check will never end. Because of...
Undecidability
This small aside illustrates the importance of delimiting precisely the logical fragment in which your encoding takes place. That is, what the formulas mention: strings, arrays, non-linear arithmetic?
Verification is expensive; very expensive. SMT checks themselves are at least exp-time, meaning
that if n
is "the size of the problem", the complexity of SMT checks is at least 2ⁿ
. Most
SMT-based verification techniques end up being at least k
-exp-time, also informally called tower
of exponential, which is even worse than it sounds:
2^(2^(2^(2^(.....2ⁿ))))
^^^^^^^^^^^^^^^^^^
`k` times
where k
is some arbitrarily large constant specific to a given problem.
So that's not great. As it turns out, the hardware is now so efficient and the tools so carefully
implemented and optimized that despite being k
-exp-time, we can actually verify very interesting
and complex systems. Still, given the complexity, we can only go so far.
One way around this high complexity is to analyze complex systems compositionally, which is akin
to type-checking. When proving function f
, which calls g
, we can abstract g
away if we have
some specification for it, effectively replacing g
's implementation by what the specification
says it does. Then we prove g
respects its specification, recursively.
This assumes we have a specification for sub-functions (g
), and not just a single specification
at the top-most level (f
). Refining a top-level specification to generate specifications for
the sub-functions is very difficult to do automatically (also manually).
To sum up, verification has at best a very high complexity. Still, in theory, if you had enough time and memory you could just wait for an arbitrary long time and actually get an answer. Unless you are in an undecidable logic fragment: you cannot know in advance if you will get an answer, even if you wait for an infinite amount of time and memory.
That's not great either, and in fact this is (probably) what happened to us on
#![allow(unused)] fn main() { candidates { "not done then i < len": ¬done ⇒ i < len, "done then i = len": done ⇒ i = len, "i ≥ 0": i ≥ 0, "len ≥ 0": len ≥ 0, "grouping > 0": grouping > 0, "len grouping related": len % grouping = 0, // new "i % grouping = 0": i % grouping = 0, } }
when we got
> mikino --z3_cmd "z3 -T:5" check test.mkn
checking base case...
success: all candidate(s) hold in the base state
checking step case...
|===| Error
| smt-level error:
| - smt solver reported `timeout`
| - during step check
|===|
We have been in an undecidable fragment all along (since we use non-linear arithmetic). But so far,
we managed to only send Z3 checks it could handle. The last candidate we added, i % grouping = 0
,
apparently made our checks escape Z3's abilities.
And that's pretty much it, we will not prove done ⇒ i = len
and probably not other interesting
properties such as "the upper bound of the interval i..(i + grouping)
must be ≤ len
(interval
exclusive on the upper bound)" (or simply i + grouping ≤ len
) either.
Or maybe what we could do is...
Settling For The Next Best Thing
Our current system is very loose on what len
and grouping
could be. We cannot say much on
grouping
as it is given to the code at runtime. It can be anything (module the constructor's
checks) and we have no control over it at compile-time (verification-time). But len
is different.
It is actually not unknown at compile-time since it is a const
, i.e. a value that can be
computed when compiling ---at least when compiling a binary using this code, but not necessarily
when compiling a library. See section Rust System for the full Rust code of the
system. More precisely, different version of our case-study might be used: with len = 3
, len = 7
... But the point is that, due to how Rust works (monomorphization in particular), there is a
finite number of actual len
values and we know all of them. (Again, at least when compiling a
binary.)
So, maybe, instead the loose constraint len ≥ 0
and trying to conduct a proof for any value of
len
(which does not work, as we have seen), we could check each possible value of len
separately. Notice that len
is used in a non-linear mod
application, len % grouping
:
#![allow(unused)] fn main() { candidates { "not done then i < len": ¬done ⇒ i < len, "done then i = len": done ⇒ i = len, "i ≥ 0": i ≥ 0, "len ≥ 0": len ≥ 0, "grouping > 0": grouping > 0, "len grouping related": len % grouping = 0, // new "i % grouping = 0": i % grouping = 0, } }
Checking for the concrete possible values of len
instead would remove this non-linear mod
and,
maybe, simplify our checks enough that Z3 can handle them. Let's try with len = 8
; we could
remove the len
state variable altogether, but we can try simply forcing it to be 8
. To do this,
first we need to change init
slightly:
#![allow(unused)] fn main() { init { len = 8, // new grouping ≥ 1, i = 0, done = ¬(i < len), len % grouping = 0, } }
Nothing to do on trans
, it does not mention len
anyway. The candidate len ≥ 0
changes to
become more precise though:
#![allow(unused)] fn main() { candidates { "not done then i < len": ¬done ⇒ i < len, "done then i = len": done ⇒ i = len, "i ≥ 0": i ≥ 0, "len is 8": len = 8, // changed "grouping > 0": grouping > 0, "len grouping related": len % grouping = 0, "i % grouping = 0": i % grouping = 0, } }
While we still have the non-linear len % grouping
, the (candidate) invariant len = 8
might
actually be enough for Z3 to handle the checks on this system. We shall see.
Full code in the Version 6 section. Crossing our fingers, we nervously run mikino:
> mikino --z3_cmd "z3 -T:5" check test.mkn
checking base case...
success: all candidate(s) hold in the base state
checking step case...
success: all candidate(s) are inductive
|===| Induction attempt result
| - all candidates hold in the initial state(s)
|
| - all candidates are inductive (preserved by the transition relation)
|
| - system is safe, all reachable states verify the candidate(s)
|===|
🎊 🎉 👏 🎊
Wrapping Up
We proved the lower bound of the interval in &arr[i..(i + grouping)]
is legal 🎉. But that's not
enough for the program to be safe, since we have not proved that the upper bound is legal. That is,
prove that i + grouping ≤ len
(interval still exclusive on the upper bound 😔).
By now you might be familiar enough with induction's step checks to be able to anticipate whether this candidate is going to be inductive or not.
#![allow(unused)] fn main() { candidates { "not done then i < len": ¬done ⇒ i < len, "not done then i + grouping ≤ len": // new ¬done ⇒ i + grouping ≤ len, "done then i = len": done ⇒ i = len, "i ≥ 0": i ≥ 0, "len is 8": len = 8, // changed "grouping > 0": grouping > 0, "len grouping related": len % grouping = 0, "i % grouping = 0": i % grouping = 0, } }
Full code in the Version 7 section.
🥁🥁🥁🥁🥁🥁
> mikino --z3_cmd "z3 -T:5" check test.mkn
checking base case...
success: all candidate(s) hold in the base state
checking step case...
success: all candidate(s) are inductive
|===| Induction attempt result
| - all candidates hold in the initial state(s)
|
| - all candidates are inductive (preserved by the transition relation)
|
| - system is safe, all reachable states verify the candidate(s)
|===|
🎉
It might be disappointing that we had to choose a value for the length, i.e. that we did not
prove that fold
's array accesses are legal when len
is 8
. Unfortunately, in its current
state, Z3 cannot handle to have len
as an unknown value because of the non-linear applications of
modulo.
This means that, assuming we are verifying a binary, we can conduct the same proof for all len
values actually used by the program. It would be much better to verify fold
for any length, but
this illustrates the kind of trade-offs that are often necessary in a verification process due to
the very high complexity (and sometimes undecidability) of the checks intrinsic to the verification
approach.
Full Code for All Examples
Rust System
Expand this for the full code.
#![allow(non_upper_case_globals)] pub struct Wrap<T, const len: usize> { arr: [T; len], grouping: usize, } impl<T, const len: usize> Wrap<T, len> { /// Constructor. pub fn new(arr: [T; len], grouping: usize) -> Self { if len % grouping != 0 { panic!( "grouping value {} is not a multiple of array length ({})", grouping, len, ); } if grouping < 1 { panic!("illegal grouping value {}, must be > 0", grouping) } Self { arr, grouping } } /// Grouped fold. pub fn fold<Acc>(&self, init: Acc, mut next: impl FnMut(Acc, &[T]) -> Acc) -> Acc { let (arr, grouping) = (&self.arr, self.grouping); let mut acc = init; let mut i = 0; while i < len { let next_i = i + grouping; acc = next(acc, &arr[i..next_i]); i = next_i } acc } } fn main() { // `len` ~~~~~~v let arr: [f64; 6] = [0., 1., 2., 3., 4., 5.]; // `grouping` ~~~~~~~~~~~~v let wrap = Wrap::new(arr, 3); let value_1 = wrap.fold(0., |acc, arr| { println!( "len: {} | arr[0]: {}, arr[1]: {}, arr[2]: {}", arr.len(), arr[0], arr[1], arr[2] ); assert_eq!(arr.len(), 3); let new_acc = acc + arr[0] * arr[1] * arr[2]; println!("acc: {}, new_acc: {}", acc, new_acc); new_acc }); println!("fold result: {}", value_1); println!("\n---------------------\n"); // `len` ~~~~~~v let arr: [f64; 6] = [0., 1., 2., 3., 4., 5.]; // `grouping` ~~~~~~~~~~~~v let wrap = Wrap::new(arr, 2); let value_2 = wrap.fold(0., |acc, arr| { println!( "len: {} | arr[0]: {}, arr[1]: {}", arr.len(), arr[0], arr[1] ); assert_eq!(arr.len(), 2); let new_acc = acc + arr[0] * arr[1]; println!("acc: {}, new_acc: {}", acc, new_acc); new_acc }); println!("fold result: {}", value_2); }
Output:
len: 3 | arr[0]: 0, arr[1]: 1, arr[2]: 2
acc: 0, new_acc: 0
len: 3 | arr[0]: 3, arr[1]: 4, arr[2]: 5
acc: 0, new_acc: 60
fold result: 60
---------------------
len: 2 | arr[0]: 0, arr[1]: 1
acc: 0, new_acc: 0
len: 2 | arr[0]: 2, arr[1]: 3
acc: 0, new_acc: 6
len: 2 | arr[0]: 4, arr[1]: 5
acc: 6, new_acc: 26
fold result: 26
Version 1
Expand this for the full code.
#![allow(unused)] fn main() { svars { len grouping: int, i: int, done: bool, } init { len ≥ 0, grouping ≥ 1, i = 0, done = ¬(i < len), len % grouping = 0, } // Encodes iteration `(pre i)`, NOT iteration `i`. That is, `trans` encodes the // iteration where we access `arr` with the interval `(pre i)..((pre i) + grouping)`. trans { 'grouping = grouping, 'len = len, // NEW value of the `done` flag, if `true` there will be no further iteration. 'done = ¬('i < 'len), if done { // just maintain the value; 'i = i } else { // otherwise, `i` is incremented by `grouping`. 'i = i + grouping }, } candidates { "candidate": ¬(len = 8 ⋀ grouping = 4 ⋀ i = len), } }
Output:
> mikino check test.mkn
running BMC, looking for falsifications for 1 candidate(s)...
checking for falsifications at depth 0
checking for falsifications at depth 1
checking for falsifications at depth 2
found a falsification at depth 2:
- `candidate` = (not (and (= len 8) (= grouping 4) (= i len)))
|=| Step 0
| done = false
| grouping = 4
| i = 0
| len = 8
|=| Step 1
| done = false
| grouping = 4
| i = 4
| len = 8
|=| Step 2
| done = true
| grouping = 4
| i = 8
| len = 8
|=| Z3 produced the following unexpected values
| div0 ((x!0 int) (x!1 int)) int = 2
| mod0 ((x!0 int) (x!1 int)) int = 0
|=|
|===| Bmc result
| - found a falsification for the following candidate(s)
| `candidate`
|
| - system is unsafe
|===|
Version 2
Expand this for the full code.
#![allow(unused)] fn main() { svars { len grouping: int, i: int, done: bool, } init { len ≥ 0, grouping ≥ 1, i = 0, done = ¬(i < len), len % grouping = 0, } // Encodes iteration `(pre i)`, NOT iteration `i`. That is, `trans` encodes the // iteration where we access `arr` with the interval `(pre i)..((pre i) + grouping)`. trans { 'grouping = grouping, 'len = len, // NEW value of the `done` flag, if `true` there will be no further iteration. 'done = ¬('i < 'len), if done { // just maintain the value; 'i = i } else { // otherwise, `i` is incremented by `grouping`. 'i = i + grouping }, } candidates { "not done then i < len": ¬done ⇒ i < len, "done then i = len": done ⇒ i = len, } }
Output:
> 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:
- `done then i = len` = (=> done (= i len))
|=| Step k
| done = false
| grouping = 2
| i = (- 1)
| len = 0
|=| Step k + 1
| done = true
| grouping = 2
| i = 1
| len = 0
|=|
|===| Induction attempt result
| - all candidates hold in the initial state(s)
|
| - the following candidate(s) are not inductive (not preserved by the transition relation)
| `done then i = len`
|
| - 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:
| `not done then i < len`
|===|
Version 3
Expand this for the full code.
#![allow(unused)] fn main() { svars { len grouping: int, i: int, done: bool, } init { len ≥ 0, grouping ≥ 1, i = 0, done = ¬(i < len), len % grouping = 0, } // Encodes iteration `(pre i)`, NOT iteration `i`. That is, `trans` encodes the // iteration where we access `arr` with the interval `(pre i)..((pre i) + grouping)`. trans { 'grouping = grouping, 'len = len, // NEW value of the `done` flag, if `true` there will be no further iteration. 'done = ¬('i < 'len), if done { // just maintain the value; 'i = i } else { // otherwise, `i` is incremented by `grouping`. 'i = i + grouping }, } candidates { "i ≥ 0": i ≥ 0, "not done then i < len": ¬done ⇒ i < len, "done then i = len": done ⇒ i = len, } }
Output:
> 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:
- `done then i = len` = (=> done (= i len))
|=| Step k
| done = false
| grouping = 2
| i = 0
| len = 1
|=| Step k + 1
| done = true
| grouping = 2
| i = 2
| len = 1
|=|
- `i ≥ 0` = (>= i 0)
|=| Step k
| done = false
| grouping = (- 1)
| i = 0
| len = 1
|=| Step k + 1
| done = false
| grouping = (- 1)
| i = (- 1)
| len = 1
|=|
|===| Induction attempt result
| - all candidates hold in the initial state(s)
|
| - the following candidate(s) are not inductive (not preserved by the transition relation)
| `done then i = len`
| `i ≥ 0`
|
| - 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:
| `not done then i < len`
|===|
Version 4
Expand this for the full code.
#![allow(unused)] fn main() { svars { len grouping: int, i: int, done: bool, } init { len ≥ 0, grouping ≥ 1, i = 0, done = ¬(i < len), len % grouping = 0, } // Encodes iteration `(pre i)`, NOT iteration `i`. That is, `trans` encodes the // iteration where we access `arr` with the interval `(pre i)..((pre i) + grouping)`. trans { 'grouping = grouping, 'len = len, // NEW value of the `done` flag, if `true` there will be no further iteration. 'done = ¬('i < 'len), if done { // just maintain the value; 'i = i } else { // otherwise, `i` is incremented by `grouping`. 'i = i + grouping }, } candidates { "not done then i < len": ¬done ⇒ i < len, "done then i = len": done ⇒ i = len, "i ≥ 0": i ≥ 0, // new "len ≥ 0": len ≥ 0, "grouping > 0": grouping > 0, "len grouping related": len % grouping = 0, } }
Output:
> 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:
- `done then i = len` = (=> done (= i len))
|=| Step k
| done = false
| grouping = 2
| i = 3
| len = 4
|=| Step k + 1
| done = true
| grouping = 2
| i = 5
| len = 4
|=| Z3 produced the following unexpected values
| div0 ((x!0 int) (x!1 int)) int = 2
| mod0 ((x!0 int) (x!1 int)) int = 0
|=|
|===| Induction attempt result
| - all candidates hold in the initial state(s)
|
| - the following candidate(s) are not inductive (not preserved by the transition relation)
| `done then i = len`
|
| - 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:
| `grouping > 0`
| `i ≥ 0`
| `len grouping related`
| `len ≥ 0`
| `not done then i < len`
|===|
Version 5
Expand this for the full code.
#![allow(unused)] fn main() { svars { len grouping: int, i: int, done: bool, } init { len ≥ 0, grouping ≥ 1, i = 0, done = ¬(i < len), len % grouping = 0, } // Encodes iteration `(pre i)`, NOT iteration `i`. That is, `trans` encodes the // iteration where we access `arr` with the interval `(pre i)..((pre i) + grouping)`. trans { 'grouping = grouping, 'len = len, // NEW value of the `done` flag, if `true` there will be no further iteration. 'done = ¬('i < 'len), if done { // just maintain the value; 'i = i } else { // otherwise, `i` is incremented by `grouping`. 'i = i + grouping }, } candidates { "not done then i < len": ¬done ⇒ i < len, "done then i = len": done ⇒ i = len, "i ≥ 0": i ≥ 0, "len ≥ 0": len ≥ 0, "grouping > 0": grouping > 0, "len grouping related": len % grouping = 0, // new "i % grouping = 0": i % grouping = 0, } }
Output:
> mikino --z3_cmd "z3 -T:5" check test.mkn
checking base case...
success: all candidate(s) hold in the base state
checking step case...
|===| Error
| smt-level error:
| - smt solver reported `timeout`
| - during step check
|===|
Version 6
Expand this for the full code.
#![allow(unused)] fn main() { svars { len grouping: int, i: int, done: bool, } init { len = 8, // new grouping ≥ 1, i = 0, done = ¬(i < len), len % grouping = 0, } // Encodes iteration `(pre i)`, NOT iteration `i`. That is, `trans` encodes the // iteration where we access `arr` with the interval `(pre i)..((pre i) + grouping)`. trans { 'grouping = grouping, 'len = len, // NEW value of the `done` flag, if `true` there will be no further iteration. 'done = ¬('i < 'len), if done { // just maintain the value; 'i = i } else { // otherwise, `i` is incremented by `grouping`. 'i = i + grouping }, } candidates { "not done then i < len": ¬done ⇒ i < len, "done then i = len": done ⇒ i = len, "i ≥ 0": i ≥ 0, "len is 8": len = 8, // changed "grouping > 0": grouping > 0, "len grouping related": len % grouping = 0, "i % grouping = 0": i % grouping = 0, } }
Output:
> mikino check test.mkn
checking base case...
success: all candidate(s) hold in the base state
checking step case...
success: all candidate(s) are inductive
|===| Induction attempt result
| - all candidates hold in the initial state(s)
|
| - all candidates are inductive (preserved by the transition relation)
|
| - system is safe, all reachable states verify the candidate(s)
|===|
Version 7
Expand this for the full code.
#![allow(unused)] fn main() { svars { len grouping: int, i: int, done: bool, } init { len = 8, // new grouping ≥ 1, i = 0, done = ¬(i < len), len % grouping = 0, } // Encodes iteration `(pre i)`, NOT iteration `i`. That is, `trans` encodes the // iteration where we access `arr` with the interval `(pre i)..((pre i) + grouping)`. trans { 'grouping = grouping, 'len = len, // NEW value of the `done` flag, if `true` there will be no further iteration. 'done = ¬('i < 'len), if done { // just maintain the value; 'i = i } else { // otherwise, `i` is incremented by `grouping`. 'i = i + grouping }, } candidates { "not done then i < len": ¬done ⇒ i < len, "not done then i + grouping ≤ len": // new ¬done ⇒ i + grouping ≤ len, "done then i = len": done ⇒ i = len, "i ≥ 0": i ≥ 0, "len is 8": len = 8, // changed "grouping > 0": grouping > 0, "len grouping related": len % grouping = 0, "i % grouping = 0": i % grouping = 0, } }
Output:
> mikino check test.mkn
checking base case...
success: all candidate(s) hold in the base state
checking step case...
success: all candidate(s) are inductive
|===| Induction attempt result
| - all candidates hold in the initial state(s)
|
| - all candidates are inductive (preserved by the transition relation)
|
| - system is safe, all reachable states verify the candidate(s)
|===|