A method that copies the negative elements of an array of integers into another array has the property that the set of elements in the result is a subset of the elements in the original array, which stays the same during the copy.
The problem in the code below is that, as soon as we write something in the result array, Dafny somehow forgets that the original set is unchanged. How to fix this?
method copy_neg (a: array<int>, b: array<int>)
requires a != null && b != null && a != b
requires a.Length == b.Length
modifies b
{
var i := 0;
var r := 0;
ghost var sa := set j | 0 <= j < a.Length :: a[j];
while i < a.Length
invariant 0 <= r <= i <= a.Length
invariant sa == set j | 0 <= j < a.Length :: a[j]
{
if a[i] < 0 {
assert sa == set j | 0 <= j < a.Length :: a[j]; // OK
b[r] := a[i];
assert sa == set j | 0 <= j < a.Length :: a[j]; // KO!
r := r + 1;
}
i := i + 1;
}
}
Edit
Following James Wilcox's answer, replacing inclusions of sets with predicates on sequences is what works the best.
Here is the complete specification (for an array with distinct elements). The post-condition has to be detailed a bit in the loop invariant and a dumb assert remains in the middle of the loop, but all ghost variables are gone, which is great.
method copy_neg (a: array<int>, b: array<int>)
returns (r: nat)
requires a != null && b != null && a != b
requires a.Length <= b.Length
modifies b
ensures 0 <= r <= a.Length
ensures forall x | x in a[..] :: x < 0 <==> x in b[..r]
{
r := 0;
var i := 0;
while i < a.Length
invariant 0 <= r <= i <= a.Length
invariant forall x | x in b[..r] :: x < 0
invariant forall x | x in a[..i] && x < 0 :: x in b[..r]
{
if a[i] < 0 {
b[r] := a[i];
assert forall x | x in b[..r] :: x < 0;
r := r + 1;
}
i := i + 1;
}
}
This is indeed confusing. I will explain why Dafny has trouble proving this below, but first let me give a few ways to make it go through.
First workaround
One way to make the proof go through is to insert the following
forall
statement after the lineb[r] := a[i];
.The
forall
statement is a proof thatsa <= set j | 0 <= j < a.Length :: a[j]
. I will come back to why this works below.Second workaround
In general, when reasoning about arrays in Dafny, it is best to use the
a[..]
syntax to convert the array to a mathematical sequence, and then work with that sequence. If you really need to work with the set of elements, you can useset x | x in a[..]
, and you will have a better time than if you useset j | 0 <= j < a.Length :: a[j]
.Systematically replacing
set j | 0 <= j < a.Length :: a[j]
withset x | x in a[..]
causes your program to verify.Third solution
Popping up a level to specifying your method, it seems like you don't actually need to mention the set of all elements. Instead, you can get away with saying something like "every element of
b
is an element ofa
". Or, more formallyforall x | x in b[..] :: x in a[..]
. This is not quite a valid postcondition for your method, because your method may not fill out all ofb
. Since I'm not sure what your other constraints are, I'll leave that to you.Explanations
Dafny's sets with elements of type
A
are translated to Boogie maps[A]Bool
, where an element maps to true iff it is in the set. Comprehensions such asset j | 0 <= j < a.Length :: a[j]
are translated to Boogie maps whose definition involves an existential quantifier. This particular comprehension translates to a map that mapsx
towhere the
read
expression is the Boogie translation ofa[j]
, which, in particular, makes the heap explicit.So, to prove that an element is in the set defined by the comprehension, Z3 needs to prove an existential quantifier, which is hard. Z3 uses triggers to prove such quantifiers, and Dafny tells Z3 to use the trigger
read($Heap, a, IndexField(j))
when trying to prove this quantifier. This turns out to not be a great trigger choice, because it mentions the current value of the heap. Thus, when the heap changes (ie, after updatingb[r]
), the trigger may not fire, and you get a failing proof.Dafny lets you customize the trigger it uses for set comprehensions using a
{:trigger}
attribute. Unfortunately, there is no great choice of trigger at the Dafny level. However, a reasonable trigger for this program at the Boogie/Z3 level would be justIndexField(j)
(though this is likely a bad trigger for such expressions in general, since it is overly general). Z3 itself will infer this trigger if Dafny doesn't tell it otherwise. You can Dafny to get out of the way by saying{:autotriggers false}
, like thisThis solution is unsatisfying and requires detailed knowledge of Dafny's internals. But now that we've understood it, we can also understand the other workarounds I proposed.
For the first workaround, the proof goes through because the
forall
statement mentionsa[j]
, which is the trigger. This causes Z3 to successfully prove the existential.For the second workaround, we have simplified the set comprehension expression so that it no longer introduces an existential quantifier. Instead the comprehension
set x | x in a[..]
, is translated to a map that mapsx
to(ignoring details of how
a[..]
is translated). This means that Z3 never has to prove an existential, so the otherwise very similar proof goes through.The third solution works for similar reasons, since it uses no comprehensions and thus no problematic existential quantifiers/