Why can't Dafny verify certain easy set cardinality and relational propositions?

。_饼干妹妹 提交于 2021-02-10 20:16:15

问题


Here's a simple Dafny program: two line of code and three assertions.

method Main()
{
    var S := set s: int | 0 <= s < 50 :: 2 * s;
    var T := set t | t in S && t < 25;
    assert |S| == 50;                    // does not verify  
    assert T <= S;                       // does verify
    assert T < S;                        // does not verify
}

The cardinality of S is 50, but Dafny can't verify this claim, as written. Similarly, T is obviously a subset of S, and Dafny can verify this claim; but T is also a proper subset of S, and Dafny cannot verify this claim.

What is going on "under the hood" that accounts for these difficulties, and might people who are learning and using Dafny anticipate and avoid, or deal with, such difficulties?


回答1:


Dafny cannot verify the first assertion because Dafny has no built-in model for reasoning about the cardinality of set comprehensions. So it will never be able to prove equations like |S| == 50 on its own. It can, however, reason about cardinalities of some other set operations, such as union and singleton sets. You can use this to convince Dafny that |S| == 50 in a rather roundabout way, which I'll show below.

Dafny cannot prove the last assertion because it isn't smart enough to come up with an element of S that is not in T. We can help it out by mentioning such an element. More on that below.

Setting up appropriate triggers

But first, there is a warning about the set comprehension, saying that Dafny "cannot find any terms to trigger on". This has to do with how Dafny translates set comprehensions into first-order logic, using quantifiers. Dafny instructs Z3 to use certain syntactic patterns (called triggers) to reason about quantifiers, but this warning says that no such pattern could be found. This problem is not just abstract, Dafny has serious trouble reasoning about this comprehension: it cannot even prove 0 in S.

A fairly generic way of fixing such problems is to introduce a new function to abbreviate some portion of the formula that is useful as a trigger. Concretely, define

function method Double(n: int): int {
    2 * n
}

and then rewrite the comprehension as

var S := set s: int | 0 <= s < 50 :: Double(s);

so that Dafny can use the trigger Double(s). Now, whenever you want to convince Dafny that something is in S, you can mention it as Double(x) for some x, and Dafny will be able to prove it.

For example, to show 0 in S under the new definition, we can say

assert Double(0) in S;  // remove this to see the one below fail
assert 0 in S;

Proving T < S

So far all we've done is massage the program a little bit so that Dafny can better reason about S. This is already enough to fix the proof of your final assertion T < S, so let's look at that next.

Dafny understands T < S as (essentially) T <= S && T != S. You have already seen that it is able to show T <= S, so it remains to prove T != S. The easiest way to do that is to exhibit an element of one set that is not in the other, in this case, an element of S that is not in T. 26 is such an element, and we can fix the assertion by adding the line

assert Double(13) in S;  // mention the trigger; this proves 26 in S
assert T < S;            // now verifies

Proving |S| == 50

Finally, let's go back to your first failing assertion, that |S| == 50. This one is harder to fix. There might be a shorter proof, but I'll show you one way to do it that should also help you see how to fix similar problems on your own.

Our approach will be to first show that S can be expressed in terms of two simpler functions on sets, and then prove lemmas about how these two functions affect cardinalities.

Dafny is able to show

assert S == MapSet(Double, RangeSet(0, 50));

where RangeSet(a,b) is a function that constructs the set of integers between a and b, and MapSet is a function that "maps a function over a set". These functions are defined as follows:

function method RangeSet(a: int, b: int): set<int> {
    set x: int | a <= x < b
}

function method MapSet<A,B>(f: A -> B, S: set<A>): set<B>
{
    set x | x in S :: f(x)
}

So in order to prove something about |S|, it suffices to reason about the cardinality of RangeSet and to show that (under suitable conditions) MapSet preserves cardinality. Here are two lemmas to this effect

lemma CardRangeSet(a: int, b: int)
    requires a <= b
    decreases b - a
    ensures |RangeSet(a, b)| == b - a

lemma CardMapSetInj<A, B>(f: A -> B, S: set<A>)
    requires Injective(f)
    decreases S
    ensures |MapSet(f, S)| == |S|

CardRangeSet says that the cardinality of the set of numbers between a and b is b - a, which should be fairly intuitive, remembering that RangeSet is inclusive of a but exclusive of b.

The lemma CardMapSetInj says that injective functions preserve cardinality when mapped over a set. Note that Double is injective (Dafny thinks this is obvious -- it requires no proof). Also note that the injectivity assumption is necessary for the lemma to hold, otherwise mapping could decrease the cardinality if the function sends two elements of the input set to the same output element. We can define Injective as follows:

predicate Injective<A, B>(f: A -> B)
{
    forall x, y :: x != y ==> f(x) != f(y)
}

I will prove the two lemmas below, but first let's see how to use them to finish showing |S| == 50.

assert S == MapSet(Double, RangeSet(0, 50));
CardMapSetInj(Double, RangeSet(0, 50));
assert |S| == |RangeSet(0, 50)|;
CardRangeSet(0, 50);
assert |S| == 50;                    // now verifies

Not too bad! We first call CardMapSet with Double to show that |S| == |RangeSet(0, 50)|. Then we call CardRangeSet to show |RangeSet(0, 50)| == 50, which finishes the proof.

The two lemmas are both proved by induction.

lemma CardRangeSet(a: int, b: int)
    requires a <= b
    decreases b - a
    ensures |RangeSet(a, b)| == b - a
{
    if a != b {
        calc {
            |RangeSet(a, b)|;
            { assert RangeSet(a, b) == {a} + RangeSet(a + 1, b); }
            |{a} + RangeSet(a + 1, b)|;
            1 + |RangeSet(a + 1, b)|;
            { CardRangeSet(a + 1, b); }
            1 + (b - (a + 1));
            b - a;
        }
    }
}

CardRangeSet is proved by induction on b - a. Dafny finds the base case trivial, so we consider only the induction step when a != b. The proof works by unrolling RangeSet to pull out the a, and then calling the induction hypothesis on (a + 1, b) and cleaning up.

(The proof is written in a calculational style. If you're not familiar with calc, it allows you to write a sequence of equations very succinctly and readbly. The lines surrounded in braces are proof annotations showing why the next line is equal to the previous. If no annotation is required, it can be omitted. Consult the Dafny manual for more details on calc.)

lemma CardMapSetInj<A, B>(f: A -> B, S: set<A>)
    requires Injective(f)
    decreases S
    ensures |MapSet(f, S)| == |S|
{
    if S != {} {
        var x :| x in S;
        calc {
            |MapSet(f, S)|;
            { assert MapSet(f, S) == MapSet(f, {x} + (S - {x})) == {f(x)} + MapSet(f, S - {x}); }
            |{f(x)} + MapSet(f, S - {x})|;
            1 + |MapSet(f, S - {x})|;
            { CardMapSetInj(f, S - {x}); }
            |S|;
        }
    }
}

CardMapSetInj is proved by induction on the argument set S. Again the base case is trivial and omitted. In the inductive step where S is nonempty, we select an arbitrary element x in S (such an x exists since S is nonempty). Then we can unroll the definition of MapSet on S and call the induction hypothesis on S - {x}.

(If you're not familiar with the syntax var x :| x in S, this is Dafny's "let such-that" statement. It is sort of similar to an assignment statement, except instead of assigning a particular value to x, it instead selects an arbitrary value that satisfies the predicate on the right-hand side of the :|. During verification, Dafny verifies that there exists such an element. The rest of the program knows nothing about the value of x except that it satisfies the predicate. See the Dafny reference for more details.)

Wrapping up

There are basically two ideas here:

  1. Introduce new functions to serve as triggers
  2. Comprehensions are sometimes hard to reason about; use functions instead.

There is also probably a way to prove |S| == 50 more directly, by introducing a function f(n) that generalizes the comprehension defining S by returning set s | 0 <= s < n :: Double(s). One could then proceed directly by induction on n to prove that |f(n)| == n, and then note that S == f(50).

That would be a perfectly fine alternative proof, and I encourage you to try to develop it yourself. One advantage of the proof I've given here is that it is more generic (MapSet and RangeSet are likely to be generally useful if you are frequently manipulating sets).



来源:https://stackoverflow.com/questions/48963978/why-cant-dafny-verify-certain-easy-set-cardinality-and-relational-propositions

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