This subtle issue due to the order in which heap updates are processed. First, let's simplify the example, because it's not specific to anything about positive and negative numbers.
type T
predicate P(t: T)
method ThisFails(t0: T, yes: T, no: T) requires P(yes) && !P(no) {
var arr := new T[2](_ => t0);
arr[0] := yes;
arr[1] := no;
assert exists k :: 0 <= k < arr.Length && P(arr[k]); // FAILS [:(]
}
Flipping the two array modifications fixes the issue:
type T
predicate P(t: T)
method ThisFails(t0: T, yes: T, no: T) requires P(yes) && !P(no) {
var arr := new T[2](_ => t0);
arr[1] := no; // Different order
arr[0] := yes; // Different order
assert exists k :: 0 <= k < arr.Length && P(arr[k]); // SUCCEEDS [?!]
}
Why do these examples behave differently?
The key to understand what's going on is to think of how arr[
idx
] :=
value
]
affects the verification state. When you assign into an array, Dafny learns that:
- The array has changed
- Position
idx
maps to value
in the new array
- Other positions are unchanged between the old and the new array
So after two assignments in the first (broken) code example above, we have
- The array has changed
- Position
0
maps to yes
in the intermediate array
- Position
1
maps to no
in the final array
In contrast, after two assignments in the second (working) code example above, we have:
- The array has changed
- Position
1
maps to no
in the intermediate array
- Position
0
maps to yes
in the final array
Note which fact talks about the intermediate array and which fact talks about the final array. In the broken example we know arr[0] == yes
in the intermediate array. In the working example we know arr[0] == yes
in the final array.
Of course, we can prove that the final array has arr[0] == yes
, but we don't know it by default in the second (broken) broken example.
Why does this matter?
The proof of the quantifier goes this way:
- Negate the assertion: it becomes
forall k | 0 <= k < arr.Length :: !P(arr[k])
- Look for a contradiction by instantiating the quantifier.
If you hover over the quantifier you will see Selected triggers: {arr[k]}
This means that Dafny will "learn" !P(arr[k])
any time it already knows something about arr[
k
]
for some k
.
Importantly, the quantifier refers to the final state of the array! So, facts about the intermediate state of the array are not used.
In the broken example above, we learn the following about the final array, which is not enough to derive a contradiction and complete the proof.
arr[1] == no
(from the last assignment, hence !P(arr[1])
)
!P(arr[1])
(from the quantifier)
In the working example above, we learn the following about the final array, which is enough to derive a contradiction and complete the proof.
arr[0] == yes
(from the last assignment, hence P(arr[0])
)
!P(arr[0])
(from the quantifier)
In the broken case, the problem can be fixed by asserting assert arr[0] == yes;
, which is easily provable and gives us the fact that we needed about the final array.
Final remarks
We can make intermediate states explicit using labels:
method ThisFails(t0: T, yes: T, no: T) requires P(yes) && !P(no) {
var arr := new T[2](_ => t0);
label initial:
arr[0] := yes;
label intermediate:
arr[1] := no;
label final:
assert true;
assert exists k :: 0 <= k < arr.Length && P(arr[k]); // FAILS
}
Then, it's enough to add the assertion assert old@intermediate(arr[0]) == old@final(arr[0]);
to confirm that this part of the array has not been modified.
Another way to see the issue is to reproduce the problem with sequences:
method ThisFails(t0: T, yes: T, no: T) requires P(yes) && !P(no) {
var sq := [t0, t0];
var sq0 := sq[0 := yes];
var sq1 := sq0[1 := no];
// We know sq0[0] == yes, but not sq1[0] == yes
// assert sq1[0] == yes; // Adding this fixes the issue
assert exists k :: 0 <= k < |sq1| && P(sq1[k]); // FAILS
}