2

This may be an extremely stupid question, but here goes:

Why can Dafny very this:

var arr := new int[2];
arr[0], arr[1] := -1, -2;
assert exists k :: 0 <= k < arr.Length && arr[k] < 0;

but not this:

var arr := new int[2];
arr[0], arr[1] := -1, 2;
assert exists k :: 0 <= k < arr.Length && arr[k] < 0;

I've traced an error in my bigger program back to this. I'm sure it's something minor that I overlooked, but I'd appreciate the help!

James Wilcox
  • 5,307
  • 16
  • 25
arn
  • 83
  • 6

2 Answers2

4

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:

  1. Negate the assertion: it becomes forall k | 0 <= k < arr.Length :: !P(arr[k])
  2. 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
}
Clément
  • 12,299
  • 15
  • 75
  • 115
2

Interesting question. I'm not sure! Maybe someone else can chime in with a deeper investigation.

Let me just mention that this issue is related to triggers. Any time you ask Dafny to prove an exists, you have to understand that it (and the underlying solver Z3) uses a syntactic heuristic. It looks at the body of the quantifier and tries to find a "trigger" or pattern. After it selects the trigger, it will only guess values of k that match the trigger.

In your particular example, the trigger is arr[k]. So Dafny will only attempt to guess values of k where arr[k] is already mentioned elsewhere in the program.

It's also important to understand that arrays are heap-allocated, and the "mentioned elsewhere in the program" clause mostly applies to the current heap. The program mentions arr[0] and arr[1], but it mentions those in a previous heap, before the assignment statement on line 2.

All of that to say, I'm actually more surprised that Dafny can prove the assertion in your first example, than I am that it can't prove the second.

Finally, let me note that once you understand that triggers are the way Dafny understands quantifiers, it is easy to manually coax Dafny to prove the second assertion: simply mention arr[k] for the value of k you know to be correct. In other words, insert this line in your program before the existing assertion:

assert arr[0] < 0;

Note that it's not actually important that we assert arr[0] is less than 0. What matters is that we mention arr[0] at all. We could say something silly about it instead, as long as we mention it.

James Wilcox
  • 5,307
  • 16
  • 25
  • That's very interesting! I also noticed that it works with asserts already. The thing is: I'm doing this for a uni assignment where I am not allowed to add assertions (a different example) :D So I'm gonna have to think about that a bit more. Thanks! – arn Nov 21 '22 at 06:49
  • +1. I started to write an edit to your answer but it was getting too long, so I made a separate answer. The problem is that the second heap update hides the first one (so we have a ground term and hence an instantiation only for `arr[1]`, not `arr[0]`). – Clément Nov 29 '22 at 18:19