Proving termination of BFS with Dafny

元气小坏坏 提交于 2021-01-28 21:16:20

问题


  • I'm trying to prove some properties of BFS with dafny, but so far I can't even prove termination.
  • The progression of the algorithm is guaranteed by the fact that once a node is colored false (visited) it will not be colored true again.
  • Still, I am having a hard time translating this fact to a formal dafny decreases <something>:
class Graph {
    var adjList : seq<seq<int>>;
}
method BFS(G : Graph, s : int) returns (d : array<int>)
    requires 0 <= s < |G.adjList|
    requires forall u :: 0 <= u < |G.adjList| ==> forall v   :: 0 <= v <     |G.adjList[u]| ==> 0 <= G.adjList[u][v] < |G.adjList|
    requires forall u :: 0 <= u < |G.adjList| ==> forall v,w :: 0 <= v < w < |G.adjList[u]| ==> G.adjList[u][v] != G.adjList[u][w] 
{
    var i := 0;
    var j := 0;
    var u : int;
    var Q : seq<int>;
    var iterations := G.adjList[0];
    var n := |G.adjList|;
    var color : array<bool>;

    color := new bool[n];
    d     := new int [n];

    i := 0; while (i < n)
    {
        color[i] := true;
        d[i] := -1;
        i := i + 1;
    }

    Q := [s];
    while (Q != [])
    {        
        // u <- Dequeue(Q)
        u := Q[0]; Q := Q[1..];
        
        // foreach v in adjList[u]
        i := 0; while (i < |G.adjList[u]|)
        {
            var v := G.adjList[u][i];
            if (color[v])
            {
                color[v] := false;
                d[v]     := d[u] + 1;
                Q        := Q + [v];
            }
            i := i + 1;
        }
    }
}

The error message I get is:

cannot prove termination; try supplying a decreases clause for the loop

回答1:


Here is one way to do it.

The key is to introduce the concept of "the set of indices that have not yet been colored false". For this concept, I use a function TrueIndices.

function TrueIndices(a: array<bool>): set<int>
  reads a
{
  set i | 0 <= i < a.Length && a[i] 
}

The beginning of the BFS method remains unchanged:

method BFS(G : Graph, s : int) returns (d : array<int>)
    requires 0 <= s < |G.adjList|
    requires forall u :: 0 <= u < |G.adjList| ==>
        forall v :: 0 <= v < |G.adjList[u]| ==> 0 <= G.adjList[u][v] < |G.adjList|
    requires forall u :: 0 <= u < |G.adjList| ==>
        forall v,w :: 0 <= v < w < |G.adjList[u]| ==> G.adjList[u][v] != G.adjList[u][w] 
{
    var i := 0;
    var j := 0;
    var u : int;
    var Q : seq<int>;
    var iterations := G.adjList[0];
    var n := |G.adjList|;
    var color : array<bool>;

    color := new bool[n];
    d     := new int [n];

    i := 0; while (i < n)
    {
        color[i] := true;
        d[i] := -1;
        i := i + 1;
    }

The implementation uses a worklist Q. In this case, the algorithm pops an element of the worklist, and then marks all unvisited neighbors as visited. If there are no unvisited neighbors, then the size of the worklist decreases. If there are unvisited neighbors, then they get marked visited, so the total number of unvisited nodes decreases.

To sum up, either the number of unvisited nodes decreases (in which case the worklist might get longer), or the number of unvisited nodes remains the same, but the length of the worklist decreases. We can formalize this reasoning by saying that the loop decreases the tuple (number of unvisited nodes, length of Q) in the lexicographic ordering.

This is exactly what is encoded in the decreases clause below.

    Q := [s];
    while (Q != [])
      decreases TrueIndices(color), |Q|
      invariant forall x | x in Q :: 0 <= x < |G.adjList|  // invariant (1)
    {        
        ghost var top_of_loop_indices := TrueIndices(color);
        ghost var top_of_loop_Q := Q;

        // u <- Dequeue(Q)
        u := Q[0]; Q := Q[1..];
        assert u in top_of_loop_Q;  // trigger invariant (1) for u

        // help Dafny see that dequeueing is ok
        assert forall x | x in Q :: x in top_of_loop_Q;

        // foreach v in adjList[u]
        i := 0; while i < |G.adjList[u]|
          invariant forall x | x in Q :: 0 <= x < |G.adjList|  // invariant (2)
          invariant  // invariant (3)
            || TrueIndices(color) < top_of_loop_indices
            || (TrueIndices(color) == top_of_loop_indices && |Q| < |top_of_loop_Q|)
        {
            var v := G.adjList[u][i];
            if (color[v])
            {
                // help Dafny see that v was newly colored false
                assert v in TrueIndices(color);
                color[v] := false;
                d[v]     := d[u] + 1;
                Q        := Q + [v];
            }
            i := i + 1;
        }
    }
}

The code also contains several invariants and assertions that are necessary to prove the decreases clause. You might like to stop at this point and try to figure them out for yourself, starting only from the decreases clause. Or you can read the narrative below to see how I figured it out.


If you just add the decreases clause, you will get two errors. First, Dafny will say that it cannot prove that the decreases clause decreases. Let's come back to that. The second error is an "index out of range" error on the expression G.adjList[u] in the loop condition of the inner loop. Basically, it cannot prove that u is in bounds here. Which kind of makes sense, because u is just some arbitrary element of Q, and we haven't given any loop invariants about Q yet.

To fix this, we need to say that every element of Q is a valid index into G.adjList. This is stated by the line marked // invariant (1) above.

Unfortunately, adding just that line does not immediately fix the problem. And, we get an additional error that the new loop invariant we just added might not be maintained by the loop. Why didn't this invariant fix the error? The problem is that it's actually still not obvious to Dafny that u is an element of Q, even though u is defined to be Q[0]. We can fix this by adding the assertion marked // trigger invariant (1) for u.

Now let's try to prove that invariant (1) is preserved by the loop. The problem is that there is an inner loop with no loop invariants yet. So Dafny makes worst case assumptions about what the inner loop might do to Q. We can fix this by copy-pasting the same invariant to the inner loop, which I marked as // invariant (2) above.

That fixes the outer loop's "might not preserve invariant (1)" error, but now we get a new error saying that invariant (2) might not hold on entry to the inner loop. What gives? All we've done since the top of the outer loop is dequeue an element of Q. We can help Dafny see that all elements after dequeueing were also elements of the original Q at the top of the loop. We do this using the assertion marked // help Dafny see that dequeueing is ok above.

Ok, that completes the proof of invariant (1). Now let's fix the remaining error saying that the decreases clause might not decrease.

Again, the problem is the inner loop. In the absence of invariants, Dafny makes worst case assumptions about what might happen to color and Q. Basically, we need to find a way to guarantee that, after the inner loop terminates, the tuple (TrueIndices(color), |Q|) has lexicographically decreased compared to its value at the top of the outer loop. We do this by spelling out what lexicographic ordering means here: either TrueIndices(color) gets strictly smaller, or it stays the same while |Q| gets smaller. This is stated as // invariant (3) above. (Note that, unfortunately, ordering on tuples (a, b) < (c, d) does not appear to do the right thing here. I looked under the covers, and what it actually does is pretty weird. Not really sure why it is this way, but so be it. I filed an issue about this here.)

Adding invariant (3) causes the error about the decreases clause not decreasing to go away, but we get one last error, which is that invariant (3) might not be preserved by the inner loop. The problem here is basically that inside the true branch of the if, we need to help Dafny realize that v is the index that is going to be removed from TrueIndices we do this with the assertion marked // help Dafny see that v was newly colored false.

This completes the proof of termination!

Note that, as is often the case in sophisticated termination arguments, we had to prove several other invariants along the way. This might be somewhat surprising to you at first, since it might seem like termination and correctness would be independent. But in fact, this is quite common.

Of course, actually proving functional correctness of BFS will require yet more invariants. I haven't tried it, but I hope you will!



来源:https://stackoverflow.com/questions/63202757/proving-termination-of-bfs-with-dafny

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!