diff --git a/doc/conf.py b/doc/conf.py index 72569cc9d4925ab6f2d75e52a3259a6f7ec85bd9..9f132805dd8822fcf0b19338e1087fed67f96a83 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -15,7 +15,7 @@ import sys import os -docauthorlist = u'Régis Blanc, Samuel Gruetter, Etienne Kneuss, Manos Koukoutos, Viktor Kuncak, Philippe Suter' +docauthorlist = u'Marco Antognini, Régis Blanc, Samuel Gruetter, Etienne Kneuss, Manos Koukoutos, Viktor Kuncak, Sandro Stucki, Philippe Suter' # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the diff --git a/doc/index.rst b/doc/index.rst index 4615689355ed9cc9b51d08476026a832196365b7..0dc9d673eb7ce2d812675dcec6294d6ab227c8ad 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -19,6 +19,7 @@ Contents: library xlang verification + neon limitations synthesis repair diff --git a/doc/neon.rst b/doc/neon.rst new file mode 100644 index 0000000000000000000000000000000000000000..f830531ce911d4be9d50c33c2c29cd9da8954f98 --- /dev/null +++ b/doc/neon.rst @@ -0,0 +1,1004 @@ +.. _neon: + +Proving Theorems +================ + +Verifying the contract of a function is really proving a mathematical +theorem. Leon can be seen as a (mostly) automated theorem prover. It is +automated in the sense that once the property stated, Leon will proceed with searching +for a proof without any user interaction. In practice however, many theorems will be fairly +difficult to prove, and it is possible for the user to provide hints to Leon. + +Hints typically take the form of simpler properties that combine in order to prove +more complicated ones. In the remaining subsections we provide code patterns and introduce +simple domain-specific language operators that can help in constructing complex Leon proofs. + +A practical introduction to proofs +---------------------------------- + +When writing logical propositions such as preconditions or +postconditions in Leon, one is basically writing Boolean +predicates. They can be as simple as testing whether a list is empty +or not, to more complex combinations of properties. Lemmas or +theorems are simply logical tautologies, that is, propositions that +always hold. They can be expressed using Boolean-valued methods that +return ``true`` for all their inputs. + +To make this more concrete, let's take a simple lemma as an +example [#example-dir]_. Here we want to prove that the append operation (``++``) on +lists preserves the content of the two lists being concatenated. This +proposition is relatively straightforward and Leon is able to verify +that it always holds. + +.. code-block:: scala + + import leon.collection._ // for List + import leon.lang._ // for holds + + object Example { + def appendContent[A](l1: List[A], l2: List[A]): Boolean = { + l1.content ++ l2.content == (l1 ++ l2).content + }.holds + } + +Here we wrote ``.holds`` which is a method implicitly available on ``Boolean`` +that ensure the returned value is ``true``. It is equivalent to writing +``ensuring { res => res }``. + +Now let's look at another example that looks trivial but for which Leon +actually needs some help with the proof: we want to prove that adding ``Nil`` +at the end of a list has no effect. + +.. code-block:: scala + + import leon.collection._ // for List + import leon.lang._ // for holds + + object Example { + def rightUnitAppend[T](l1: List[T]): Boolean = { + l1 ++ Nil() == l1 + }.holds + } + +If you try to verify this last example you'll face a delicate +situation: Leon runs indeterminately until it is either killed or +times out. But why does this happen? The proposition doesn't seems +more complicated than ``appendContent``. Perhaps even more +surprisingly, Leon *is* able to verify the following: + +.. code-block:: scala + + def leftUnitAppend[T](l1: List[T]): Boolean = { + Nil() ++ l1 == l1 + }.holds + +How is this possible? The two propositions are completely symmetric! +The problem is that Leon doesn't know anything about lists, a priori. +It can only reason about lists thanks to their definition in terms of +the case classes ``Cons`` and ``Nil`` and associated methods such as +``++``. In particular, Leon doesn't know that ``Nil`` represents the +empty list, and hence that appending it to some other list is a no-op. +What then, is the difference between the two examples above? To +answer this question, we need to have a look at the definition of the +``++`` method: + +.. code-block:: scala + + def ++(that: List[T]): List[T] = (this match { + case Nil() => that + case Cons(x, xs) => Cons(x, xs ++ that) + }) ensuring { res => /* ... */ } + +Note that the implementation of ``++`` is recursive in its first +argument ``this`` but *not* in its second argument ``that``. This is +why Leon was able to verify ``leftUnitAppend`` easily: it is true *by +definition*, i.e. ``Nil() ++ l1`` is actually defined to be ``l1``. +What about the symmetric case? How is ``l1 ++ Nil()`` defined? Well, +it depends on whether ``l1`` is the empty list or not. So in order to +prove ``rightUnitAppend``, we need to proceed by case analysis. The +resulting proof has a recursive (i.e. inductive) structure reminiscent +of the definition of ``++``: + +.. code-block:: scala + + import leon.collection._ // for List + import leon.lang._ // for holds + import leon.proof._ // for because + + object Example { + def rightUnitAppend[T](l1: List[T]): Boolean = { + (l1 ++ Nil() == l1) because { + l1 match { + case Nil() => true + case Cons(x, xs) => rightUnitAppend(xs) + } + } + }.holds + } + +With this new implementation of the ``rightUnitAppend`` lemma, Leon is capable +of verifying that it holds. If you look closely at it, you can distinguish three +parts: + +1. the claim we want to prove ``l1 ++ Nil() == l1``; +2. ``because``, which is just some syntactic sugar for conjunction -- remember, + every proposition is a Boolean formula; +3. and a recursion on ``l1`` that serves as a hint for Leon to perform + induction. + +The recursion is based here on pattern matching, which Leon will also check for +exhaustiveness. It has essentially the same structure as +the implementation of ``++``: the base case is when ``l1`` is the empty list +and the inductive case is performed on ``Cons`` objects. + + +Techniques for proving non-trivial propositions +----------------------------------------------- + +In the previous section we saw that "proof hints" can improve the odds +of Leon successfully verifying a given proposition. In this section +we will have a closer look at what constitutes such a proof and +discuss a few techniques for writing them. + +As mentioned earlier, propositions are represented by Boolean +expressions in Leon. But how are proofs represented? They are just +Boolean expressions as well [#props-not-types]_. The difference +between propositions and proofs is not their representation, but how +they are used by Leon. Intuitively, a proof ``p: Boolean`` for a +proposition ``x: Boolean`` is an expression such that + + 1. Leon is able to verify ``p``, and + 2. Leon is able to verify that ``p`` implies ``x``. + +This is what we mean when we say that proofs are "hints". Typically, +a proof ``p`` of a proposition ``x`` is a more complex-looking but +equivalent version of ``x``, i.e. an expression such that ``p == x``. +This might seem a bit counter-intuitive: why should it be easier for +Leon to verify an equivalent but more complex expression? The answer +is that the more complex version may consist of sub-expressions that +more closely resemble the definitions of functions used in ``x``. We +have already seen an example of this principle in the previous +section: let's have another look at the proof of ``rightUnitAppend``: + +.. code-block:: scala + + def rightUnitAppend[T](l1: List[T]): Boolean = { + val x = l1 ++ Nil() == l1 + val p = l1 match { + case Nil() => true + case Cons(x, xs) => rightUnitAppend(xs) + } + x because p + }.holds + +Here, we have rewritten the example to make the distinction between +the proposition ``x`` and its proof ``p`` more explicit. It's easy to +check that indeed ``x == p``, and hence the overall result of +``rightUnitAppend`` is equivalent to ``x`` (recall that ``because`` is +just an alias of ``&&``, so ``(x because p) == (x && x) == x``). +However, the proof term ``p`` closely resembles the definition of +``++`` and its sub-expressions are easier to verify for Leon than +``x`` itself. The only non-trivial expression is the recursive call +to ``rightUnitAppend(xs)``, which serves as the inductive hypothesis. +We will discuss induction in more detail in Section +":ref:`induction`". + + +Divide and Conquer +****************** + +Before we delve into the details of particular proof techniques, it is +worth revisiting a guiding principle for writing proofs -- whether it +be in Leon, by hand, or using some other form of mechanized proof +checker -- namely to *modularize* proofs, i.e. to split the proofs of +complex propositions into manageable *sub-goals*. This can be +achieved in various ways. + + * Use *helper lemmas* -- these are propositions that are lemmas on + their own, i.e. they state and prove simple but self-contained + propositions that can be reused elsewhere. As such, they play a + role akin to helper methods in normal programming, and indeed, are + implemented in the same way, except that they carry a ``.holds`` + suffix. + + * Use *case analysis* to split complex propositions into simpler + sub-cases. This is especially helpful in the presence of + recursion, where it leads to inductive proofs (see Section + ":ref:`induction`"). + + * Use *relational reasoning* to split complex relationships into + conjunctions of elementary ones. This often requires one to make + use of relational properties such as transitivity (e.g. to break a + single equation ``a == b`` into a chain of equations ``a == x1 && + x1 == x2 && ... && xN == b``), symmetry (e.g. to use a previously + proven inequality ``a <= b`` where ``b >= a`` is expected), + anti-symmetry (to unify variables), and so on (see Section + ":ref:`rel-reasoning`"). + + * Separate specification form implementation. It is sometimes easier + to prove the fact that a given function fulfills its specification + as a separate lemma (although the proof techniques are roughly the + same, see Section ":ref:`post-cond`"). + + * Generalize (or specialize) propositions. Sometimes, propositions + are more easily proved in a stronger (or weaker) form and + subsequently instantiated (or combined with other propositions) to + yield a proof of the original proposition. + +While it is good practice to factor common propositions into helper +lemmas, one sometimes wants to verify simple, specific sub-goals in a +proof without going through the trouble of introducing an additional +method. This is especially true while one is exploring the branches +of a case analysis or wants to quickly check whether Leon is able to +prove a seemingly trivial statement automatically (we will see +examples of such situations in the coming sections). For such cases, +one can use the ``check`` function from ``leon.proof``. The ``check`` +function behaves as the identity function on Booleans but additionally +assumes its argument in its precondition. As a result, Leon will +check that ``x`` holds while verifying the call to ``check(x)``. +For example, when verifying the following function: + +.. code-block:: scala + + import leon.proof.check + + def foo(x: BigInt): Boolean = { + check(x >= 0 || x < 0) && check(x + 0 == 0) + } + +Leon will check (separately) that ``x >= 0 || x < 0`` and ``x + 0 == +0`` hold for all ``x``, even though the function ``foo`` does not +specify any pre or postconditions, and report a counter example for +the second case:: + + [ Info ] - Now considering 'precond. (call check(x >= 0 || x < 0))' VC for foo @40:5... + [ Info ] => VALID + [ Info ] - Now considering 'precond. (call check(x + 0 == 0))' VC for foo @40:31... + [ Error ] => INVALID + [ Error ] Found counter-example: + [ Error ] x -> 1 + +This is especially helpful when "debugging" proofs. + + +.. _induction: + +Induction +********* + +The vast majority of functional programs are written as functions over +:ref:`adts` (ADTs), and consequently, Leon comes with some special +support for verifying properties of ADTs. Among other things, Leon +provides an annotation ``@induct``, which can be used to automatically +prove postconditions of recursive functions defined on ADTs by way of +*structural induction*. We have already seen an example of such an +inductive property, namely ``rightUnitAppend``. In fact, using +``@induct``, Leon is able to prove ``rightUnitAppend`` directly: + +.. code-block:: scala + + import leon.annotation._ // for @induct + + @induct + def rightUnitAppend[T](l1: List[T]): Boolean = { + l1 ++ Nil() == l1 + }.holds + +This is possible because the inductive step follows (more or less) +directly from the inductive hypothesis and Leon can verify the base +case automatically. However, Leon may fail to verify more complex +functions with non-trivial base cases or inductive steps. In such +cases, one may still try to provide proof hints by performing *manual +case analysis*. Consider the following lemma about list reversal: + +.. code-block:: scala + + import leon.collection._ // for List + import leon.lang._ // for holds + + object Example { + def reverseReverse[T](l: List[T]): Boolean = { + l.reverse.reverse == l + }.holds + } + +Leon is unable to verify ``reverseReverse`` even using ``@induct``. +So let's try and prove the lemma using manual case analysis. We start +by adding an "unrolled" version of the proposition and inserting calls +to ``check`` in each branch of the resulting pattern match: + +.. code-block:: scala + + def reverseReverse[T](l: List[T]): Boolean = { + l.reverse.reverse == l because { + l match { + case Nil() => check { Nil[T]().reverse.reverse == Nil[T]() } + case Cons(x, xs) => check { (x :: xs).reverse.reverse == (x :: xs) } + } + } + }.holds + +Clearly, the two versions of the lemma are equivalent: all we did was +expand the proposition using a pattern match and add some calls to +``check`` (remember ``check`` acts as the identity function on its +argument). Let's see what output Leon produces for the expanded +version:: + + [ Info ] - Now considering 'postcondition' VC for reverseReverse @615:5... + [Warning ] => UNKNOWN + [ Info ] - Now considering 'precond. (call check(List[T]().reverse().reverse() ...)' VC for reverseReverse @617:28... + [ Info ] => VALID + [ Info ] - Now considering 'precond. (call check({val x$27 = l.h; ...)' VC for reverseReverse @618:28... + [Warning ] => UNKNOWN + [ Info ] - Now considering 'match exhaustiveness' VC for reverseReverse @616:7... + [ Info ] => VALID + +As expected, Leon failed to verify the expanded version. However, we +get some additional information due to the extra pattern match and the +calls to ``check``. In particular, Leon tells us that the match is +exhaustive, which means we covered all the cases in our case analysis +-- that's certainly a good start. Leon was also able to automatically +verify the base case, so we can either leave the call to ``check`` as +is, or replace it by ``trivial``. Unfortunately, Leon wasn't able to +verify the inductive step, something is missing. Let's try to +manually reduce the inductive case and see where we get. + +.. code-block:: scala + + def reverseReverse[T](l: List[T]): Boolean = { + l.reverse.reverse == l because { + l match { + case Nil() => trivial + case Cons(x, xs) => check { (xs.reverse :+ x).reverse == (x :: xs) } + } + } + }.holds + +And now we're stuck. We can't apply the inductive hypothesis here, +nor can we reduce the inductive case further, unless we perform +case analysis on ``xs``, which would grow the term further without +changing its shape. To make any headway, we need to use an additional +property of ``reverse``, given by the following lemma (which Leon *is* +able to prove using ``@induct``): + +.. code-block:: scala + + @induct + def snocReverse[T](l: List[T], t: T): Boolean = { + (l :+ t).reverse == t :: l.reverse + }.holds + +The lemma states that appending an element ``t`` to a list ``l`` and +reversing it is equivalent to first reversing ``l`` and then +prepending ``t``. Using this lemma, we can write the proof of +``reverseReverse`` as + +.. code-block:: scala + + def reverseReverse[T](l: List[T]): Boolean = { + l.reverse.reverse == l because { + l match { + case Nil() => trivial + case Cons(x, xs) => check { + (xs.reverse :+ x).reverse == x :: xs.reverse.reverse && + x :: xs.reverse.reverse == (x :: xs) because + snocReverse(xs.reverse, x) && reverseReverse(xs) + } + } + } + }.holds + +Leon is able to verify this version of the lemma. Note that Leon +doesn't actually require us to include the two equations as they are +equivalent to the applications ``snocReverse(xs.reverse, x)`` and +``reverseReverse(xs)``. Similarly, the call to ``check`` is somewhat +redundant now that Leon is able to verify the entire proof. We could +thus "simplify" the above to + +.. code-block:: scala + + def reverseReverse[T](l: List[T]): Boolean = { + l.reverse.reverse == l because { + l match { + case Nil() => trivial + case Cons(x, xs) => snocReverse(xs.reverse, x) && reverseReverse(xs) + } + } + }.holds + +However, the previous version is arguably more readable for a human +being, and therefore preferable. In Section ":ref:`rel-reasoning`" we +will see how readability can be improved even further through the use +of a DSL for equational reasoning. + +So far, we have only considered structurally inductive proofs. +However, Leon is also able to verify proofs using *natural induction* +-- the form of induction that is perhaps more familiar to most +readers. Consider the following definition of the exponential +function :math:`exp(x, y) = x^y` over integers: + +.. code-block:: scala + + def exp(x: BigInt, y: BigInt): BigInt = { + require(y >= 0) + if (x == 0) 0 + else if (y == 0) 1 + else x * exp(x, y - 1) + } + +The function ``exp`` is again defined recursively, but this time using +``if`` statements rather than pattern matching. Let's try and prove +some properties of this function using natural induction. One such +property is that for any pair of positive numbers :math:`x, y \geq 0`, +the exponential :math:`x^y` is again a positive number. + +.. code-block:: scala + + def positive(x: BigInt, y: BigInt): Boolean = { + require(y >= 0 && x >= 0) + exp(x, y) >= 0 + } + +Since Leon doesn't know anything about exponentials, it isn't able to +verify the lemma without hints. As with the previous example, we +start writing our inductive proof by expanding the top-level ``if`` +statement in the definition of ``exp``. + +.. code-block:: scala + + def positive(x: BigInt, y: BigInt): Boolean = { + require(y >= 0 && x >= 0) + exp(x, y) >= 0 because { + if (x == 0) check { exp(x, y) >= 0 } // <-- valid + else if (y == 0) check { exp(x, y) >= 0 } // <-- valid + else check { exp(x, y) >= 0 } // <-- unknown + } + }.holds + +Leon was able to verify the first two (base) cases, but not the +inductive step, so let's continue unfolding ``exp`` for that case. + +.. code-block:: scala + + def positive(x: BigInt, y: BigInt): Boolean = { + require(y >= 0 && x >= 0) + exp(x, y) >= 0 because { + if (x == 0) trivial + else if (y == 0) trivial + else check { x * exp(x, y - 1) >= 0 } + } + }.holds + +Although Leon still isn't able to verify the lemma, we now see a way +to prove the inductive step: ``x`` is positive (by the second +precondition) and so is ``exp(x, y - 1)`` (by the inductive +hypothesis). Hence the product ``x * exp(x, y - 1)`` is again +positive. + +.. code-block:: scala + + def positive(x: BigInt, y: BigInt): Boolean = { + require(y >= 0 && x >= 0) + exp(x, y) >= 0 because { + if (x == 0) trivial + else if (y == 0) trivial + else check { + x >= 0 && exp(x, y - 1) >= 0 because positive(x, y - 1) + } + } + }.holds + +With these hints, Leon is able to verify the proof. Again, we could +shorten the proof by omitting inequalities that Leon can infer +directly, albeit at the expense of readability. + +.. code-block:: scala + + def positiveShort(x: BigInt, y: BigInt): Boolean = { + require(y >= 0 && x > 0) + exp(x, y) >= 0 because { + if (x == 0) trivial + else if (y == 0) trivial + else positiveShort(x, y - 1) + } + }.holds + +We conclude the section with the inductive proof of another, somewhat +more interesting property of the exponential function, namely that +:math:`(x y)^z = x^z y^z`. + +.. code-block:: scala + + def expMultCommute(x: BigInt, y: BigInt, z: BigInt): Boolean = { + require(z >= 0) + exp(x * y, z) == exp(x, z) * exp(y, z) because { + if (x == 0) trivial + else if (y == 0) trivial + else if (z == 0) trivial + else check { + x * y * exp(x * y, z - 1) == + x * exp(x, z - 1) * y * exp(y, z - 1) because + expMultCommute(x, y, z - 1) + } + } + }.holds + +.. _rel-reasoning: + +Relational reasoning +******************** + +The majority of the example propositions we have seen so far related +some expression (e.g. ``l.reverse ++ Nil()`` or ``exp(x, y)``) to some +other expression (e.g. ``... == l1`` or ``... >= 0``). This is +certainly a common case among the sorts of propositions about +functions and data structures one might wish to prove. The proofs of +such propositions typically involve some form of *relational +reasoning*, i.e. reasoning involving properties (such as transitivity, +reflexivity or symmetry) of the relations in question. Leon knows +about these properties for built-in relations such as ``==`` or orders +on numbers. For user-defined relations, they first need to be +established as lemmas. In this section, we discuss how to make +effective use of built-in relations, but the general principles extend +to their user-defined counterparts. + +When working with simple structural equality, we can rely on the default ``==`` +operator and Leon will happily understand when the reflexivity, symmetry and +transitivity properties apply and use them to conclude bigger proofs. Similarly, +when working on ``BigInt``, Leon knows about reflexivity, antisymmetry and +transitivity over ``>=`` or ``<=``, and also antireflexivity, antisymmetry +and transitivity of ``>`` and ``<``. + +However, even for relatively simple proofs about ADTs, Leon needs +hints when combining, say equality, with user-defined operations, such +as ``++`` or ``reverse`` on lists. For example, Leon is not able to +verify that the following holds for arbitrary pairs of lists ``l1`` +and ``l2``: + +.. code-block:: scala + + (l1 ++ l2).reverse == l2.reverse ++ l1.reverse + +The hard part of giving hints to Leon is often to find them in the +first place. Here we can apply a general principle on top of +structural induction (as discussed in the previous section): we start +from the left-hand side of an equation and build a chain of +intermediate equations to the right-hand side. Using ``check`` +statements we can identify where Leon times out and hence potentially +needs hints. + +.. code-block:: scala + + def reverseAppend[T](l1: List[T], l2: List[T]): Boolean = { + ( (l1 ++ l2).reverse == l2.reverse ++ l1.reverse ) because { + l1 match { + case Nil() => + /* 1 */ check { (Nil() ++ l2).reverse == l2.reverse } && + /* 2 */ check { l2.reverse == l2.reverse ++ Nil() } && + /* 3 */ check { l2.reverse ++ Nil() == l2.reverse ++ Nil().reverse } + case Cons(x, xs) => + /* 4 */ check { ((x :: xs) ++ l2).reverse == (x :: (xs ++ l2)).reverse } && + /* 5 */ check { (x :: (xs ++ l2)).reverse == (xs ++ l2).reverse :+ x } && + /* 6 */ check { (xs ++ l2).reverse :+ x == (l2.reverse ++ xs.reverse) :+ x } && + /* 7 */ check { (l2.reverse ++ xs.reverse) :+ x == l2.reverse ++ (xs.reverse :+ x) } && + /* 8 */ check { l2.reverse ++ (xs.reverse :+ x) == l2.reverse ++ (x :: xs).reverse } + } + } + }.holds + +If we run the above code with a decent timeout, Leon reports four +*UNKNOWN* cases: the postcondition of the ``reverseAppend`` function itself and +checks number 2, 6 and 7. + + * Check #2 fails because, as we saw earlier, Leon is not capable of + guessing the ``rightUnitAppend`` lemma by itself. We fix this case + by simply instantiating the lemma, i.e. by appending ``&& + rightUnitAppend(l2.reverse)`` to the base case. + + * Check #6 fails because, at this point, we need to inject the + induction hypothesis on ``xs`` and ``l2`` by adding ``&& + reverseAppend(xs, l2)``. + + * Finally, check #7 fails for a similar reason as check #2: we need + an additional "associativity" lemma to prove that ``(l1 ++ l2) :+ t + == l1 ++ (l2 :+ t)`` holds for any ``l1``, ``l2`` and ``t``. We + call this lemma ``snocAfterAppend`` and leave it as an exercise for + the reader. + +Once we have a valid proof, we can try to optimize it for readability. +As it stands, the resulting code is rather verbose because both sides +of most equations are duplicated. One option is to completely remove +the equations (they are implied by the instantiations of the lemmas) +and simply write + +.. code-block:: scala + + def reverseAppend[T](l1: List[T], l2: List[T]): Boolean = { + ( (l1 ++ l2).reverse == l2.reverse ++ l1.reverse ) because { + l1 match { + case Nil() => + rightUnitAppend(l2.reverse) + case Cons(x, xs) => + reverseAppend(xs, l2) && snocAfterAppend(l2.reverse, xs.reverse, x) + } + } + }.holds + +Or we can employ the equational reasoning DSL provided by the +``leon.proofs`` package to remove the duplicate expressions and +interleave the equations with their associated proofs. This has the +advantage of not losing information that is still useful for a human +being reading the proof later on: + +.. code-block:: scala + + def reverseAppend[T](l1: List[T], l2: List[T]): Boolean = { + ( (l1 ++ l2).reverse == l2.reverse ++ l1.reverse ) because { + l1 match { + case Nil() => { + (Nil() ++ l2).reverse ==| trivial | + l2.reverse ==| rightUnitAppend(l2.reverse) | + l2.reverse ++ Nil() ==| trivial | + l2.reverse ++ Nil().reverse + }.qed + case Cons(x, xs) => { + ((x :: xs) ++ l2).reverse ==| trivial | + (x :: (xs ++ l2)).reverse ==| trivial | + (xs ++ l2).reverse :+ x ==| reverseAppend(xs, l2) | + (l2.reverse ++ xs.reverse) :+ x ==| + snocAfterAppend(l2.reverse, xs.reverse, x) | + l2.reverse ++ (xs.reverse :+ x) ==| trivial | + l2.reverse ++ (x :: xs).reverse + }.qed + } + } + }.holds + +The idea is to group statements in a block +(``{ }``) and call ``qed`` on it. Then, instead of writing ``a == b && b == c +&& hint1 && hint2`` we write ``a ==| hint1 | b ==| hint2 | c``. And when no +additional hint is required, we can use ``trivial`` which simply stands for +``true``. + +Additionally, by using this DSL, we get the same feedback granularity from Leon +as if we had used ``check`` statements. This way we can construct proofs based +on equality more easily and directly identify where hints are vital. + +One shortcoming of the relational reasoning DSL is that it relies on +Leon's knowledge of the relational properties of the built-in +relations, and in particular those of equality. Consequently is works +badly (if at all) with user-defined relations. However, since the DSL +is defined as a library (in ``library/proof/package.scala``), it can +in principle be extended and modified to include specific user-defined +relations on a case-by-case basis. + +.. TODO add a word about requirement in ctor (e.g. Rational) + +.. TODO Footnote linking to Etienne's answer on SO. + + +Limits of the approach: HOFs, quantifiers and termination +********************************************************* + +While the techniques discussed in this section are useful in general, +their applicability has of course its limitations in practice. These +limitations are mostly due to Leon's limited support for certain +language constructs, such as higher-order functions (HOFs) or +quantifiers (which in turn is due, mostly, to the limited support of +the corresponding theories in the underlying SMT solvers). + +Still, even using these "experimental" features, one manages to prove +some interesting propositions. Here is another list example, which +relates the ``foldLeft``, ``foldRight`` and ``reverse`` operations +defined on lists and makes crucial use of HOFs: + +.. code-block:: scala + + import leon.collection._ + import leon.lang._ + import leon.proof._ + + def folds[A, B](xs: List[A], z: B, f: (B, A) => B): Boolean = { + val f2 = (x: A, z: B) => f(z, x) + xs.foldLeft(z)(f) == xs.reverse.foldRight(z)(f2) because { + xs match { + case Nil() => true + case Cons(x, xs) => { + (x :: xs).foldLeft(z)(f) ==| trivial | + xs.foldLeft(f(z, x))(f) ==| folds(xs, f(z, x), f) | + xs.reverse.foldRight(f(z, x))(f2) ==| trivial | + xs.reverse.foldRight(f2(x, z))(f2) ==| + snocFoldRight(xs.reverse, x, z, f2) | + (xs.reverse :+ x).foldRight(z)(f2) ==| trivial | + (x :: xs).reverse.foldRight(z)(f2) + }.qed + } + } + }.holds + +A rather different, more general issue that arises when proving +propositions using Leon is related to *termination checking*. When +verifying inductive proofs (and more generally the postconditions of +recursive methods), Leon assumes that the corresponding proofs are +*well-founded*, or equivalently, that the corresponding recursive +methods terminate on all inputs. Yet Leon does not -- by default -- +check that this is the case. It is thus possible -- and indeed rather +easy -- to write bogus proofs (intentionally or accidentally) which +Leon recognizes as valid, but which are not well-founded. Consider +the following lemma, which apparently establishes that all lists are +empty: + +.. code-block:: scala + + import leon.collection._ + import leon.lang._ + import leon.proof._ + + object NotWellFounded { + + // This proof is not well-founded. Since Leon doesn't run the + // termination checker by default, it will accept the proof as + // valid. + def allListsAreEmpty[T](xs: List[T]): Boolean = { + xs.isEmpty because { + xs match { + case Nil() => trivial + case Cons(x, xs) => allListsAreEmpty(x :: xs) + } + } + }.holds + } + +Leon has (experimental) support for termination checking, which can be +enabled using the ``--termination`` command line option to minimize +the risk of accidentally writing bogus proofs such as the one above. + +.. TODO example: folds + future work (alt. version of folds) + +.. _post-cond: + +Techniques for proving non-trivial postconditions +------------------------------------------------- + +When proving a mathematical lemma, the return type of the +corresponding function is most of +the time, if not always, ``Boolean``. For those proofs it is rather easy to +write a postcondition: using ``holds`` is generally enough. + +But when it comes to writing postconditions for more general functions, such as +the addition on rational numbers, we are no longer dealing with ``Boolean`` so +we need a strategy to properly write ``ensuring`` statements. + + +Rationals: a simple example +*************************** + +Let's take rational numbers as an example: we define them as a case class with +two attributes, `n` for the numerator and `d` for the denominator. We also +define three simple properties on them: ``isRational``, ``isNonZero`` and +``isPositive``. + +.. code-block:: scala + + case class Rational(n: BigInt, d: BigInt) { + def isRational = d != 0 + def isPositive = isRational && (n * d >= 0) + def isNonZero = isRational && (n != 0) + + // ... + } + +And on top of that we want to support addition on ``Rational`` in a way that +the rationality and positiveness properties are correctly preserved: + +.. code-block:: scala + + def +(that: Rational): Rational = { + require(isRational && that.isRational) + Rational(n * that.d + that.n * d, d * that.d) + } ensuring { res => + res.isRational && + (this.isPositive == that.isPositive ==> res.isPositive == this.isPositive) + } + +In this simple case, things work nicely and we can write the +multiplication in a similar fashion: + +.. code-block:: scala + + def *(that: Rational): Rational = { + require(isRational && that.isRational) + Rational(n * that.n, d * that.d) + } ensuring { res => + res.isRational && + (res.isNonZero == (this.isNonZero && that.isNonZero)) && + (res.isPositive == (!res.isNonZero || this.isPositive == that.isPositive)) + } + + +Measures: a slightly more complex example +***************************************** + +Now let's look at a slightly more complex example: measures on +discrete probability spaces. We represent such measures using a +``List``-like recursive data structure: a generic abstract class +``Meas[A]`` that has two subclasses, ``Empty[A]`` and ``Cons[A]``. +The constructor of the class ``Empty[A]`` takes no arguments; it +represents an "empty" measure that evaluates to 0 when applied to any +set of values of type ``A``. The constructor of ``Cons[A]``, on the +other hand, takes three parameters: a value ``x``, its associated +weight ``w`` expressed as a ``Rational`` (since Leon doesn't quite yet +support real numbers out of the box), and another measure ``m`` on +``A``. The value ``Cons(x, w, m)`` represents the measure obtained by +adding to ``m`` the "single-point" measure that evaluates to ``w`` at +``x`` and to 0 everywhere else. We also define an ``isMeasure`` +property -- similar to the ``isRational`` property presented above -- +which recursively checks that all the weights in a measure are +positive rationals (note that all our measures have finite support). + +.. code-block:: scala + + /** Measures on discrete probability spaces. */ + sealed abstract class Meas[A] { + + /** All weights must be positive. */ + def isMeasure: Boolean = this match { + case Empty() => true + case Cons(x, w, m) => w.isPositive && m.isMeasure + } + + // ... + } + + /** The empty measure maps every subset of the space A to 0. */ + case class Empty[A]() extends Meas[A] + + /** + * The 'Cons' measure adjoins an additional element 'x' of type 'A' + * to an existing measure 'm' over 'A'. Note that 'x' might already + * be present in 'm'. + */ + case class Cons[A](x: A, w: Rational, m: Meas[A]) extends Meas[A] + + +The defining operation on a measure ``m`` is its evaluation ``m(xs)`` +(or equivalently ``m.apply(xs)``) on some set ``xs: Set[A]``, i.e. on a +subset of the "space" ``A``. The value of ``m`` should be a positive +rational for any such set ``xs``, provided ``m.isMeasure`` holds. +This suggests ``_.isPositive`` as the postcondition for ``apply``, +but simply claiming that the result is positive is not enough for Leon +to verify this postcondition. + +We can provide the necessary hint to Leon by performing structural +induction on ``this`` inside the postcondition as follows: + +.. code-block:: scala + + /** Compute the value of this measure on a subset of the space 'A'. */ + def apply(xs: Set[A]): Rational = { + require (isMeasure) + this match { + case Empty() => Rational(0, 1) + case Cons(x, w, m) => if (xs contains x) w + m(xs) else m(xs) + } + } ensuring { res => + res.isPositive because { + this match { + case Empty() => trivial + case Cons(x, w, m) => m(xs).isPositive + } + } + } + +Notice the similarity between the pattern match in the body of the +``apply`` function and that in the postcondition. With this hint, +Leon is able to verify the postcondition. + + +A complex example: additivity of measures +----------------------------------------- + +Using the principles and techniques discussed so far, one can prove +quite advanced propositions using Leon. Returning to the +measure-theoretic example from the previous section, we would like to +prove that our implementation of measures is properly *additive*. +Formally, a measure :math:`\mu \colon A \to \mathbb{R}` on a countable +set :math:`A` must fulfill the following additivity property +[#dicrete-meas]_: + +.. math:: + + \forall A_1, A_2 \subseteq A . A_1 \cap A_2 = \emptyset \Rightarrow + \mu(A_1 \cup A_2) = \mu(A_1) + \mu(A_2) + +which we can express in Leon as + +.. code-block:: scala + + def additivity[A](m: Meas[A], xs: Set[A], ys: Set[A]): Boolean = { + require(m.isMeasure && (xs & ys).isEmpty) + m(xs ++ ys) == m(xs) + m(ys) + }.holds + +We can prove this property using structural induction on the parameter +``m``, case analysis on the parameters ``xs`` and ``ys``, equational +reasoning, and properties of rational numbers (in the form of +user-defined lemmas) as well as sets (using Leon's built-in support). + +.. code-block:: scala + + def additivity[A](m: Meas[A], xs: Set[A], ys: Set[A]): Boolean = { + require(m.isMeasure && (xs & ys).isEmpty) + m(xs ++ ys) == m(xs) + m(ys) because { + m match { + case Empty() => trivial + case Cons(x, w, n) => if (xs contains x) { + w + n(xs ++ ys) ==| additivity(n, xs, ys) | + w + (n(xs) + n(ys)) ==| plusAssoc(w, n(xs), n(ys)) | + (w + n(xs)) + n(ys) ==| !(ys contains x) | + m(xs) + m(ys) + }.qed else if (ys contains x) { + w + n(xs ++ ys) ==| additivity(n, xs, ys) | + w + (n(xs) + n(ys)) ==| plusComm(w, (n(xs) + n(ys))) | + (n(xs) + n(ys)) + w ==| plusAssoc(n(xs), n(ys), w) | + n(xs) + (n(ys) + w) ==| plusComm(n(ys), w) | + n(xs) + (w + n(ys)) ==| !(xs contains x) | + m(xs) + m(ys) + }.qed else { + n(xs ++ ys) ==| additivity(n, xs, ys) | + n(xs) + n(ys) + }.qed + } + } + }.holds + +The full proof (including the proofs of all helper lemmas) as well as +its generalization to *sub-additivity* can be found in the +``testcases/verification/proof/measure/`` directory of the Leon +distribution [#example-dir]_. + + +Quick Recap +----------- + +Let's summarize what we've learned here. To write proofs efficiently, +it's good to keep the following in mind: + +1. Always use a proper timeout and ask Leon for more information about + what he tries to verify, e.g. ``--timeout=5 --debug=verification``. + +2. Use ``@induct`` when working on structurally inductive proofs to + get a more precise feedback from Leon: this will decompose the + proof into a base case and an inductive case for the first argument + of the function under consideration. + + If Leon isn't able to verify the proof using ``@induct``, try + performing manual case analysis. + +3. Modularize your proofs and verify *sub-goals*! + + - use plenty of helper lemmas; + - use ``check`` abundantly; + - if possible use the relational reasoning DSL presented above. + + This is especially handy when you can connect the two sides of a relational + claim with sub-statements. + + +.. rubric:: Footnotes + +.. [#example-dir] The source code of this example and all other in + this chapter are included in the Leon distribution. Examples about + lists can be found in ``library/collection/List.scala``, the other + examples are located in the ``testcases/verification/proof/`` + directory. + +.. [#props-not-types] Perhaps surprisingly, propositions and proofs + live in the same universe in Leon. This is contrary to + e.g. type-theoretic proof assistants where propositions are + represented by types and proofs are terms inhabiting such types. + +.. [#dicrete-meas] To be precise, we are assuming here the underlying + measurable space :math:`(A, \mathcal{P}(A))`, where :math:`A` is + countable and :math:`\mathcal{P}(A)` denotes its discrete σ-algebra + (i.e. the power set of :math:`A`). diff --git a/doc/verification.rst b/doc/verification.rst index e98758d8c27c6bda781899243f65ec9713923800..10b27b6f73b1b96922d28a052414a3f77c12087e 100644 --- a/doc/verification.rst +++ b/doc/verification.rst @@ -213,1013 +213,5 @@ As an example, the following code should issue a warning with Scala: } } -But Leon will actually prove that the pattern matching is actually exhaustive, +But Leon will prove that the pattern matching is actually exhaustive, relying on the given precondition. - -Proving mathematical theorems with Leon ---------------------------------------- - -As we have seen, verifying the contract of a function is really proving a mathematical -theorem. In some sense, Leon can be seen as a (mostly) automated theorem prover. It is -automated in the sense that once the property stated, Leon will proceed with searching -for a proof without any user interaction. In practice however, many theorems will be fairly -difficult to prove, and it is possible for the user to provide hints to Leon. - -Hints typically take the form of simpler properties that combine in order to prove -more complicated ones. - -Neon -==== - -.. TODO decide how previous § & what follows should be integrated together (or - not) - -A practical introduction to proofs ----------------------------------- - -When writing logical propositions such as preconditions or -postconditions in Leon, one is basically writing Boolean -predicates. They can be as simple as testing whether a list is empty -or not, to more complex combinations of properties. Lemmas or -theorems are simply logical tautologies, that is, propositions that -always hold. They can be expressed using Boolean-valued methods that -return ``true`` for all their inputs. - -To make this more concrete, let's take a simple lemma as an -example [#example-dir]_. Here we want to prove that the append operation (``++``) on -lists preserves the content of the two lists being concatenated. This -proposition is relatively straightforward and Leon is able to verify -that it always holds. - -.. code-block:: scala - - import leon.collection._ // for List - import leon.lang._ // for holds - - object Example { - def appendContent[A](l1: List[A], l2: List[A]): Boolean = { - l1.content ++ l2.content == (l1 ++ l2).content - }.holds - } - -Here we wrote ``.holds`` which is a method implicitly available on ``Boolean`` -that ensure the returned value is ``true``. It is equivalent to writing -``ensuring { res => res }``. - -Now let's look at another example that looks trivial but for which Leon -actually needs some help with the proof: we want to prove that adding ``Nil`` -at the end of a list has no effect. - -.. code-block:: scala - - import leon.collection._ // for List - import leon.lang._ // for holds - - object Example { - def rightUnitAppend[T](l1: List[T]): Boolean = { - l1 ++ Nil() == l1 - }.holds - } - -If you try to verify this last example you'll face a delicate -situation: Leon runs indeterminately until it is either killed or -times out. But why does this happen? The proposition doesn't seems -more complicated than ``appendContent``. Perhaps even more -surprisingly, Leon *is* able to verify the following: - -.. code-block:: scala - - def leftUnitAppend[T](l1: List[T]): Boolean = { - Nil() ++ l1 == l1 - }.holds - -How is this possible? The two propositions are completely symmetric! -The problem is that Leon doesn't know anything about lists, a priori. -It can only reason about lists thanks to their definition in terms of -the case classes ``Cons`` and ``Nil`` and associated methods such as -``++``. In particular, Leon doesn't know that ``Nil`` represents the -empty list, and hence that appending it to some other list is a no-op. -What then, is the difference between the two examples above? To -answer this question, we need to have a look at the definition of the -``++`` method: - -.. code-block:: scala - - def ++(that: List[T]): List[T] = (this match { - case Nil() => that - case Cons(x, xs) => Cons(x, xs ++ that) - }) ensuring { res => /* ... */ } - -Note that the implementation of ``++`` is recursive in its first -argument ``this`` but *not* in its second argument ``that``. This is -why Leon was able to verify ``leftUnitAppend`` easily: it is true *by -definition*, i.e. ``Nil() ++ l1`` is actually defined to be ``l1``. -What about the symmetric case? How is ``l1 ++ Nil()`` defined? Well, -it depends on whether ``l1`` is the empty list or not. So in order to -prove ``rightUnitAppend``, we need to proceed by case analysis. The -resulting proof has a recursive (i.e. inductive) structure reminiscent -of the definition of ``++``: - -.. code-block:: scala - - import leon.collection._ // for List - import leon.lang._ // for holds - import leon.proof._ // for because - - object Example { - def rightUnitAppend[T](l1: List[T]): Boolean = { - (l1 ++ Nil() == l1) because { - l1 match { - case Nil() => true - case Cons(x, xs) => rightUnitAppend(xs) - } - } - }.holds - } - -With this new implementation of the ``rightUnitAppend`` lemma, Leon is capable -of verifying that it holds. If you look closely at it, you can distinguish three -parts: - -1. the claim we want to prove ``l1 ++ Nil() == l1``; -2. ``because``, which is just some syntactic sugar for conjunction -- remember, - every proposition is a Boolean formula; -3. and a recursion on ``l1`` that serves as a hint for Leon to perform - induction. - -The recursion is based here on pattern matching, which Leon will also check for -exhaustiveness. It has essentially the same structure as -the implementation of ``++``: the base case is when ``l1`` is the empty list -and the inductive case is performed on ``Cons`` objects. - - -Techniques for proving non-trivial propositions ------------------------------------------------ - -In the previous section we saw that "proof hints" can improve the odds -of Leon successfully verifying a given proposition. In this section -we will have a closer look at what constitutes such a proof and -discuss a few techniques for writing them. - -As mentioned earlier, propositions are represented by Boolean -expressions in Leon. But how are proofs represented? They are just -Boolean expressions as well [#props-not-types]_. The difference -between propositions and proofs is not their representation, but how -they are used by Leon. Intuitively, a proof ``p: Boolean`` for a -proposition ``x: Boolean`` is an expression such that - - 1. Leon is able to verify ``p``, and - 2. Leon is able to verify that ``p`` implies ``x``. - -This is what we mean when we say that proofs are "hints". Typically, -a proof ``p`` of a proposition ``x`` is a more complex-looking but -equivalent version of ``x``, i.e. an expression such that ``p == x``. -This might seem a bit counter-intuitive: why should it be easier for -Leon to verify an equivalent but more complex expression? The answer -is that the more complex version may consist of sub-expressions that -more closely resemble the definitions of functions used in ``x``. We -have already seen an example of this principle in the previous -section: let's have another look at the proof of ``rightUnitAppend``: - -.. code-block:: scala - - def rightUnitAppend[T](l1: List[T]): Boolean = { - val x = l1 ++ Nil() == l1 - val p = l1 match { - case Nil() => true - case Cons(x, xs) => rightUnitAppend(xs) - } - x because p - }.holds - -Here, we have rewritten the example to make the distinction between -the proposition ``x`` and its proof ``p`` more explicit. It's easy to -check that indeed ``x == p``, and hence the overall result of -``rightUnitAppend`` is equivalent to ``x`` (recall that ``because`` is -just an alias of ``&&``, so ``(x because p) == (x && x) == x``). -However, the proof term ``p`` closely resembles the definition of -``++`` and its sub-expressions are easier to verify for Leon than -``x`` itself. The only non-trivial expression is the recursive call -to ``rightUnitAppend(xs)``, which serves as the inductive hypothesis. -We will discuss induction in more detail in Section -":ref:`induction`". - - -Divide and Conquer -****************** - -Before we delve into the details of particular proof techniques, it is -worth revisiting a guiding principle for writing proofs -- whether it -be in Leon, by hand, or using some other form of mechanized proof -checker -- namely to *modularize* proofs, i.e. to split the proofs of -complex propositions into manageable *sub-goals*. This can be -achieved in various ways. - - * Use *helper lemmas* -- these are propositions that are lemmas on - their own, i.e. they state and prove simple but self-contained - propositions that can be reused elsewhere. As such, they play a - role akin to helper methods in normal programming, and indeed, are - implemented in the same way, except that they carry a ``.holds`` - suffix. - - * Use *case analysis* to split complex propositions into simpler - sub-cases. This is especially helpful in the presence of - recursion, where it leads to inductive proofs (see Section - ":ref:`induction`"). - - * Use *relational reasoning* to split complex relationships into - conjunctions of elementary ones. This often requires one to make - use of relational properties such as transitivity (e.g. to break a - single equation ``a == b`` into a chain of equations ``a == x1 && - x1 == x2 && ... && xN == b``), symmetry (e.g. to use a previously - proven inequality ``a <= b`` where ``b >= a`` is expected), - anti-symmetry (to unify variables), and so on (see Section - ":ref:`rel-reasoning`"). - - * Separate specification form implementation. It is sometimes easier - to prove the fact that a given function fulfills its specification - as a separate lemma (although the proof techniques are roughly the - same, see Section ":ref:`post-cond`"). - - * Generalize (or specialize) propositions. Sometimes, propositions - are more easily proved in a stronger (or weaker) form and - subsequently instantiated (or combined with other propositions) to - yield a proof of the original proposition. - -While it is good practice to factor common propositions into helper -lemmas, one sometimes wants to verify simple, specific sub-goals in a -proof without going through the trouble of introducing an additional -method. This is especially true while one is exploring the branches -of a case analysis or wants to quickly check whether Leon is able to -prove a seemingly trivial statement automatically (we will see -examples of such situations in the coming sections). For such cases, -one can use the ``check`` function from ``leon.proof``. The ``check`` -function behaves as the identity function on Booleans but additionally -assumes its argument in its precondition. As a result, Leon will -check that ``x`` holds while verifying the call to ``check(x)``. -For example, when verifying the following function: - -.. code-block:: scala - - import leon.proof.check - - def foo(x: BigInt): Boolean = { - check(x >= 0 || x < 0) && check(x + 0 == 0) - } - -Leon will check (separately) that ``x >= 0 || x < 0`` and ``x + 0 == -0`` hold for all ``x``, even though the function ``foo`` does not -specify any pre or postconditions, and report a counter example for -the second case:: - - [ Info ] - Now considering 'precond. (call check(x >= 0 || x < 0))' VC for foo @40:5... - [ Info ] => VALID - [ Info ] - Now considering 'precond. (call check(x + 0 == 0))' VC for foo @40:31... - [ Error ] => INVALID - [ Error ] Found counter-example: - [ Error ] x -> 1 - -This is especially helpful when "debugging" proofs. - - -.. _induction: - -Induction -********* - -The vast majority of functional programs are written as functions over -:ref:`adts` (ADTs), and consequently, Leon comes with some special -support for verifying properties of ADTs. Among other things, Leon -provides an annotation ``@induct``, which can be used to automatically -prove postconditions of recursive functions defined on ADTs by way of -*structural induction*. We have already seen an example of such an -inductive property, namely ``rightUnitAppend``. In fact, using -``@induct``, Leon is able to prove ``rightUnitAppend`` directly: - -.. code-block:: scala - - import leon.annotation._ // for @induct - - @induct - def rightUnitAppend[T](l1: List[T]): Boolean = { - l1 ++ Nil() == l1 - }.holds - -This is possible because the inductive step follows (more or less) -directly from the inductive hypothesis and Leon can verify the base -case automatically. However, Leon may fail to verify more complex -functions with non-trivial base cases or inductive steps. In such -cases, one may still try to provide proof hints by performing *manual -case analysis*. Consider the following lemma about list reversal: - -.. code-block:: scala - - import leon.collection._ // for List - import leon.lang._ // for holds - - object Example { - def reverseReverse[T](l: List[T]): Boolean = { - l.reverse.reverse == l - }.holds - } - -Leon is unable to verify ``reverseReverse`` even using ``@induct``. -So let's try and prove the lemma using manual case analysis. We start -by adding an "unrolled" version of the proposition and inserting calls -to ``check`` in each branch of the resulting pattern match: - -.. code-block:: scala - - def reverseReverse[T](l: List[T]): Boolean = { - l.reverse.reverse == l because { - l match { - case Nil() => check { Nil[T]().reverse.reverse == Nil[T]() } - case Cons(x, xs) => check { (x :: xs).reverse.reverse == (x :: xs) } - } - } - }.holds - -Clearly, the two versions of the lemma are equivalent: all we did was -expand the proposition using a pattern match and add some calls to -``check`` (remember ``check`` acts as the identity function on its -argument). Let's see what output Leon produces for the expanded -version:: - - [ Info ] - Now considering 'postcondition' VC for reverseReverse @615:5... - [Warning ] => UNKNOWN - [ Info ] - Now considering 'precond. (call check(List[T]().reverse().reverse() ...)' VC for reverseReverse @617:28... - [ Info ] => VALID - [ Info ] - Now considering 'precond. (call check({val x$27 = l.h; ...)' VC for reverseReverse @618:28... - [Warning ] => UNKNOWN - [ Info ] - Now considering 'match exhaustiveness' VC for reverseReverse @616:7... - [ Info ] => VALID - -As expected, Leon failed to verify the expanded version. However, we -get some additional information due to the extra pattern match and the -calls to ``check``. In particular, Leon tells us that the match is -exhaustive, which means we covered all the cases in our case analysis --- that's certainly a good start. Leon was also able to automatically -verify the base case, so we can either leave the call to ``check`` as -is, or replace it by ``trivial``. Unfortunately, Leon wasn't able to -verify the inductive step, something is missing. Let's try to -manually reduce the inductive case and see where we get. - -.. code-block:: scala - - def reverseReverse[T](l: List[T]): Boolean = { - l.reverse.reverse == l because { - l match { - case Nil() => trivial - case Cons(x, xs) => check { (xs.reverse :+ x).reverse == (x :: xs) } - } - } - }.holds - -And now we're stuck. We can't apply the inductive hypothesis here, -nor can we reduce the inductive case further, unless we perform -case analysis on ``xs``, which would grow the term further without -changing its shape. To make any headway, we need to use an additional -property of ``reverse``, given by the following lemma (which Leon *is* -able to prove using ``@induct``): - -.. code-block:: scala - - @induct - def snocReverse[T](l: List[T], t: T): Boolean = { - (l :+ t).reverse == t :: l.reverse - }.holds - -The lemma states that appending an element ``t`` to a list ``l`` and -reversing it is equivalent to first reversing ``l`` and then -prepending ``t``. Using this lemma, we can write the proof of -``reverseReverse`` as - -.. code-block:: scala - - def reverseReverse[T](l: List[T]): Boolean = { - l.reverse.reverse == l because { - l match { - case Nil() => trivial - case Cons(x, xs) => check { - (xs.reverse :+ x).reverse == x :: xs.reverse.reverse && - x :: xs.reverse.reverse == (x :: xs) because - snocReverse(xs.reverse, x) && reverseReverse(xs) - } - } - } - }.holds - -Leon is able to verify this version of the lemma. Note that Leon -doesn't actually require us to include the two equations as they are -equivalent to the applications ``snocReverse(xs.reverse, x)`` and -``reverseReverse(xs)``. Similarly, the call to ``check`` is somewhat -redundant now that Leon is able to verify the entire proof. We could -thus "simplify" the above to - -.. code-block:: scala - - def reverseReverse[T](l: List[T]): Boolean = { - l.reverse.reverse == l because { - l match { - case Nil() => trivial - case Cons(x, xs) => snocReverse(xs.reverse, x) && reverseReverse(xs) - } - } - }.holds - -However, the previous version is arguably more readable for a human -being, and therefore preferable. In Section ":ref:`rel-reasoning`" we -will see how readability can be improved even further through the use -of a DSL for equational reasoning. - -So far, we have only considered structurally inductive proofs. -However, Leon is also able to verify proofs using *natural induction* --- the form of induction that is perhaps more familiar to most -readers. Consider the following definition of the exponential -function :math:`exp(x, y) = x^y` over integers: - -.. code-block:: scala - - def exp(x: BigInt, y: BigInt): BigInt = { - require(y >= 0) - if (x == 0) 0 - else if (y == 0) 1 - else x * exp(x, y - 1) - } - -The function ``exp`` is again defined recursively, but this time using -``if`` statements rather than pattern matching. Let's try and prove -some properties of this function using natural induction. One such -property is that for any pair of positive numbers :math:`x, y \geq 0`, -the exponential :math:`x^y` is again a positive number. - -.. code-block:: scala - - def positive(x: BigInt, y: BigInt): Boolean = { - require(y >= 0 && x >= 0) - exp(x, y) >= 0 - } - -Since Leon doesn't know anything about exponentials, it isn't able to -verify the lemma without hints. As with the previous example, we -start writing our inductive proof by expanding the top-level ``if`` -statement in the definition of ``exp``. - -.. code-block:: scala - - def positive(x: BigInt, y: BigInt): Boolean = { - require(y >= 0 && x >= 0) - exp(x, y) >= 0 because { - if (x == 0) check { exp(x, y) >= 0 } // <-- valid - else if (y == 0) check { exp(x, y) >= 0 } // <-- valid - else check { exp(x, y) >= 0 } // <-- unknown - } - }.holds - -Leon was able to verify the first two (base) cases, but not the -inductive step, so let's continue unfolding ``exp`` for that case. - -.. code-block:: scala - - def positive(x: BigInt, y: BigInt): Boolean = { - require(y >= 0 && x >= 0) - exp(x, y) >= 0 because { - if (x == 0) trivial - else if (y == 0) trivial - else check { x * exp(x, y - 1) >= 0 } - } - }.holds - -Although Leon still isn't able to verify the lemma, we now see a way -to prove the inductive step: ``x`` is positive (by the second -precondition) and so is ``exp(x, y - 1)`` (by the inductive -hypothesis). Hence the product ``x * exp(x, y - 1)`` is again -positive. - -.. code-block:: scala - - def positive(x: BigInt, y: BigInt): Boolean = { - require(y >= 0 && x >= 0) - exp(x, y) >= 0 because { - if (x == 0) trivial - else if (y == 0) trivial - else check { - x >= 0 && exp(x, y - 1) >= 0 because positive(x, y - 1) - } - } - }.holds - -With these hints, Leon is able to verify the proof. Again, we could -shorten the proof by omitting inequalities that Leon can infer -directly, albeit at the expense of readability. - -.. code-block:: scala - - def positiveShort(x: BigInt, y: BigInt): Boolean = { - require(y >= 0 && x > 0) - exp(x, y) >= 0 because { - if (x == 0) trivial - else if (y == 0) trivial - else positiveShort(x, y - 1) - } - }.holds - -We conclude the section with the inductive proof of another, somewhat -more interesting property of the exponential function, namely that -:math:`(x y)^z = x^z y^z`. - -.. code-block:: scala - - def expMultCommute(x: BigInt, y: BigInt, z: BigInt): Boolean = { - require(z >= 0) - exp(x * y, z) == exp(x, z) * exp(y, z) because { - if (x == 0) trivial - else if (y == 0) trivial - else if (z == 0) trivial - else check { - x * y * exp(x * y, z - 1) == - x * exp(x, z - 1) * y * exp(y, z - 1) because - expMultCommute(x, y, z - 1) - } - } - }.holds - -.. _rel-reasoning: - -Relational reasoning -******************** - -The majority of the example propositions we have seen so far related -some expression (e.g. ``l.reverse ++ Nil()`` or ``exp(x, y)``) to some -other expression (e.g. ``... == l1`` or ``... >= 0``). This is -certainly a common case among the sorts of propositions about -functions and data structures one might wish to prove. The proofs of -such propositions typically involve some form of *relational -reasoning*, i.e. reasoning involving properties (such as transitivity, -reflexivity or symmetry) of the relations in question. Leon knows -about these properties for built-in relations such as ``==`` or orders -on numbers. For user-defined relations, they first need to be -established as lemmas. In this section, we discuss how to make -effective use of built-in relations, but the general principles extend -to their user-defined counterparts. - -When working with simple structural equality, we can rely on the default ``==`` -operator and Leon will happily understand when the reflexivity, symmetry and -transitivity properties apply and use them to conclude bigger proofs. Similarly, -when working on ``BigInt``, Leon knows about reflexivity, antisymmetry and -transitivity over ``>=`` or ``<=``, and also antireflexivity, antisymmetry -and transitivity of ``>`` and ``<``. - -However, even for relatively simple proofs about ADTs, Leon needs -hints when combining, say equality, with user-defined operations, such -as ``++`` or ``reverse`` on lists. For example, Leon is not able to -verify that the following holds for arbitrary pairs of lists ``l1`` -and ``l2``: - -.. code-block:: scala - - (l1 ++ l2).reverse == l2.reverse ++ l1.reverse - -The hard part of giving hints to Leon is often to find them in the -first place. Here we can apply a general principle on top of -structural induction (as discussed in the previous section): we start -from the left-hand side of an equation and build a chain of -intermediate equations to the right-hand side. Using ``check`` -statements we can identify where Leon times out and hence potentially -needs hints. - -.. code-block:: scala - - def reverseAppend[T](l1: List[T], l2: List[T]): Boolean = { - ( (l1 ++ l2).reverse == l2.reverse ++ l1.reverse ) because { - l1 match { - case Nil() => - /* 1 */ check { (Nil() ++ l2).reverse == l2.reverse } && - /* 2 */ check { l2.reverse == l2.reverse ++ Nil() } && - /* 3 */ check { l2.reverse ++ Nil() == l2.reverse ++ Nil().reverse } - case Cons(x, xs) => - /* 4 */ check { ((x :: xs) ++ l2).reverse == (x :: (xs ++ l2)).reverse } && - /* 5 */ check { (x :: (xs ++ l2)).reverse == (xs ++ l2).reverse :+ x } && - /* 6 */ check { (xs ++ l2).reverse :+ x == (l2.reverse ++ xs.reverse) :+ x } && - /* 7 */ check { (l2.reverse ++ xs.reverse) :+ x == l2.reverse ++ (xs.reverse :+ x) } && - /* 8 */ check { l2.reverse ++ (xs.reverse :+ x) == l2.reverse ++ (x :: xs).reverse } - } - } - }.holds - -If we run the above code with a decent timeout, Leon reports four -*UNKNOWN* cases: the postcondition of the ``reverseAppend`` function itself and -checks number 2, 6 and 7. - - * Check #2 fails because, as we saw earlier, Leon is not capable of - guessing the ``rightUnitAppend`` lemma by itself. We fix this case - by simply instantiating the lemma, i.e. by appending ``&& - rightUnitAppend(l2.reverse)`` to the base case. - - * Check #6 fails because, at this point, we need to inject the - induction hypothesis on ``xs`` and ``l2`` by adding ``&& - reverseAppend(xs, l2)``. - - * Finally, check #7 fails for a similar reason as check #2: we need - an additional "associativity" lemma to prove that ``(l1 ++ l2) :+ t - == l1 ++ (l2 :+ t)`` holds for any ``l1``, ``l2`` and ``t``. We - call this lemma ``snocAfterAppend`` and leave it as an exercise for - the reader. - -Once we have a valid proof, we can try to optimize it for readability. -As it stands, the resulting code is rather verbose because both sides -of most equations are duplicated. One option is to completely remove -the equations (they are implied by the instantiations of the lemmas) -and simply write - -.. code-block:: scala - - def reverseAppend[T](l1: List[T], l2: List[T]): Boolean = { - ( (l1 ++ l2).reverse == l2.reverse ++ l1.reverse ) because { - l1 match { - case Nil() => - rightUnitAppend(l2.reverse) - case Cons(x, xs) => - reverseAppend(xs, l2) && snocAfterAppend(l2.reverse, xs.reverse, x) - } - } - }.holds - -Or we can employ the equational reasoning DSL provided by the -``leon.proofs`` package to remove the duplicate expressions and -interleave the equations with their associated proofs. This has the -advantage of not losing information that is still useful for a human -being reading the proof later on: - -.. code-block:: scala - - def reverseAppend[T](l1: List[T], l2: List[T]): Boolean = { - ( (l1 ++ l2).reverse == l2.reverse ++ l1.reverse ) because { - l1 match { - case Nil() => { - (Nil() ++ l2).reverse ==| trivial | - l2.reverse ==| rightUnitAppend(l2.reverse) | - l2.reverse ++ Nil() ==| trivial | - l2.reverse ++ Nil().reverse - }.qed - case Cons(x, xs) => { - ((x :: xs) ++ l2).reverse ==| trivial | - (x :: (xs ++ l2)).reverse ==| trivial | - (xs ++ l2).reverse :+ x ==| reverseAppend(xs, l2) | - (l2.reverse ++ xs.reverse) :+ x ==| - snocAfterAppend(l2.reverse, xs.reverse, x) | - l2.reverse ++ (xs.reverse :+ x) ==| trivial | - l2.reverse ++ (x :: xs).reverse - }.qed - } - } - }.holds - -The idea is to group statements in a block -(``{ }``) and call ``qed`` on it. Then, instead of writing ``a == b && b == c -&& hint1 && hint2`` we write ``a ==| hint1 | b ==| hint2 | c``. And when no -additional hint is required, we can use ``trivial`` which simply stands for -``true``. - -Additionally, by using this DSL, we get the same feedback granularity from Leon -as if we had used ``check`` statements. This way we can construct proofs based -on equality more easily and directly identify where hints are vital. - -One shortcoming of the relational reasoning DSL is that it relies on -Leon's knowledge of the relational properties of the built-in -relations, and in particular those of equality. Consequently is works -badly (if at all) with user-defined relations. However, since the DSL -is defined as a library (in ``library/proof/package.scala``), it can -in principle be extended and modified to include specific user-defined -relations on a case-by-case basis. - -.. TODO add a word about requirement in ctor (e.g. Rational) - -.. TODO Footnote linking to Etienne's answer on SO. - - -Limits of the approach: HOFs, quantifiers and termination -********************************************************* - -While the techniques discussed in this section are useful in general, -their applicability has of course its limitations in practice. These -limitations are mostly due to Leon's limited support for certain -language constructs, such as higher-order functions (HOFs) or -quantifiers (which in turn is due, mostly, to the limited support of -the corresponding theories in the underlying SMT solvers). - -Still, even using these "experimental" features, one manages to prove -some interesting propositions. Here is another list example, which -relates the ``foldLeft``, ``foldRight`` and ``reverse`` operations -defined on lists and makes crucial use of HOFs: - -.. code-block:: scala - - import leon.collection._ - import leon.lang._ - import leon.proof._ - - def folds[A, B](xs: List[A], z: B, f: (B, A) => B): Boolean = { - val f2 = (x: A, z: B) => f(z, x) - xs.foldLeft(z)(f) == xs.reverse.foldRight(z)(f2) because { - xs match { - case Nil() => true - case Cons(x, xs) => { - (x :: xs).foldLeft(z)(f) ==| trivial | - xs.foldLeft(f(z, x))(f) ==| folds(xs, f(z, x), f) | - xs.reverse.foldRight(f(z, x))(f2) ==| trivial | - xs.reverse.foldRight(f2(x, z))(f2) ==| - snocFoldRight(xs.reverse, x, z, f2) | - (xs.reverse :+ x).foldRight(z)(f2) ==| trivial | - (x :: xs).reverse.foldRight(z)(f2) - }.qed - } - } - }.holds - -A rather different, more general issue that arises when proving -propositions using Leon is related to *termination checking*. When -verifying inductive proofs (and more generally the postconditions of -recursive methods), Leon assumes that the corresponding proofs are -*well-founded*, or equivalently, that the corresponding recursive -methods terminate on all inputs. Yet Leon does not -- by default -- -check that this is the case. It is thus possible -- and indeed rather -easy -- to write bogus proofs (intentionally or accidentally) which -Leon recognizes as valid, but which are not well-founded. Consider -the following lemma, which apparently establishes that all lists are -empty: - -.. code-block:: scala - - import leon.collection._ - import leon.lang._ - import leon.proof._ - - object NotWellFounded { - - // This proof is not well-founded. Since Leon doesn't run the - // termination checker by default, it will accept the proof as - // valid. - def allListsAreEmpty[T](xs: List[T]): Boolean = { - xs.isEmpty because { - xs match { - case Nil() => trivial - case Cons(x, xs) => allListsAreEmpty(x :: xs) - } - } - }.holds - } - -Leon has (experimental) support for termination checking, which can be -enabled using the ``--termination`` command line option to minimize -the risk of accidentally writing bogus proofs such as the one above. - -.. TODO example: folds + future work (alt. version of folds) - -.. _post-cond: - -Techniques for proving non-trivial postconditions -------------------------------------------------- - -When proving a mathematical lemma, the return type of the -corresponding function is most of -the time, if not always, ``Boolean``. For those proofs it is rather easy to -write a postcondition: using ``holds`` is generally enough. - -But when it comes to writing postconditions for more general functions, such as -the addition on rational numbers, we are no longer dealing with ``Boolean`` so -we need a strategy to properly write ``ensuring`` statements. - - -Rationals: a simple example -*************************** - -Let's take rational numbers as an example: we define them as a case class with -two attributes, `n` for the numerator and `d` for the denominator. We also -define three simple properties on them: ``isRational``, ``isNonZero`` and -``isPositive``. - -.. code-block:: scala - - case class Rational(n: BigInt, d: BigInt) { - def isRational = d != 0 - def isPositive = isRational && (n * d >= 0) - def isNonZero = isRational && (n != 0) - - // ... - } - -And on top of that we want to support addition on ``Rational`` in a way that -the rationality and positiveness properties are correctly preserved: - -.. code-block:: scala - - def +(that: Rational): Rational = { - require(isRational && that.isRational) - Rational(n * that.d + that.n * d, d * that.d) - } ensuring { res => - res.isRational && - (this.isPositive == that.isPositive ==> res.isPositive == this.isPositive) - } - -In this simple case, things work nicely and we can write the -multiplication in a similar fashion: - -.. code-block:: scala - - def *(that: Rational): Rational = { - require(isRational && that.isRational) - Rational(n * that.n, d * that.d) - } ensuring { res => - res.isRational && - (res.isNonZero == (this.isNonZero && that.isNonZero)) && - (res.isPositive == (!res.isNonZero || this.isPositive == that.isPositive)) - } - - -Measures: a slightly more complex example -***************************************** - -Now let's look at a slightly more complex example: measures on -discrete probability spaces. We represent such measures using a -``List``-like recursive data structure: a generic abstract class -``Meas[A]`` that has two subclasses, ``Empty[A]`` and ``Cons[A]``. -The constructor of the class ``Empty[A]`` takes no arguments; it -represents an "empty" measure that evaluates to 0 when applied to any -set of values of type ``A``. The constructor of ``Cons[A]``, on the -other hand, takes three parameters: a value ``x``, its associated -weight ``w`` expressed as a ``Rational`` (since Leon doesn't quite yet -support real numbers out of the box), and another measure ``m`` on -``A``. The value ``Cons(x, w, m)`` represents the measure obtained by -adding to ``m`` the "single-point" measure that evaluates to ``w`` at -``x`` and to 0 everywhere else. We also define an ``isMeasure`` -property -- similar to the ``isRational`` property presented above -- -which recursively checks that all the weights in a measure are -positive rationals (note that all our measures have finite support). - -.. code-block:: scala - - /** Measures on discrete probability spaces. */ - sealed abstract class Meas[A] { - - /** All weights must be positive. */ - def isMeasure: Boolean = this match { - case Empty() => true - case Cons(x, w, m) => w.isPositive && m.isMeasure - } - - // ... - } - - /** The empty measure maps every subset of the space A to 0. */ - case class Empty[A]() extends Meas[A] - - /** - * The 'Cons' measure adjoins an additional element 'x' of type 'A' - * to an existing measure 'm' over 'A'. Note that 'x' might already - * be present in 'm'. - */ - case class Cons[A](x: A, w: Rational, m: Meas[A]) extends Meas[A] - - -The defining operation on a measure ``m`` is its evaluation ``m(xs)`` -(or equivalently ``m.apply(xs)``) on some set ``xs: Set[A]``, i.e. on a -subset of the "space" ``A``. The value of ``m`` should be a positive -rational for any such set ``xs``, provided ``m.isMeasure`` holds. -This suggests ``_.isPositive`` as the postcondition for ``apply``, -but simply claiming that the result is positive is not enough for Leon -to verify this postcondition. - -We can provide the necessary hint to Leon by performing structural -induction on ``this`` inside the postcondition as follows: - -.. code-block:: scala - - /** Compute the value of this measure on a subset of the space 'A'. */ - def apply(xs: Set[A]): Rational = { - require (isMeasure) - this match { - case Empty() => Rational(0, 1) - case Cons(x, w, m) => if (xs contains x) w + m(xs) else m(xs) - } - } ensuring { res => - res.isPositive because { - this match { - case Empty() => trivial - case Cons(x, w, m) => m(xs).isPositive - } - } - } - -Notice the similarity between the pattern match in the body of the -``apply`` function and that in the postcondition. With this hint, -Leon is able to verify the postcondition. - - -A complex example: additivity of measures ------------------------------------------ - -Using the principles and techniques discussed so far, one can prove -quite advanced propositions using Leon. Returning to the -measure-theoretic example from the previous section, we would like to -prove that our implementation of measures is properly *additive*. -Formally, a measure :math:`\mu \colon A \to \mathbb{R}` on a countable -set :math:`A` must fulfill the following additivity property -[#dicrete-meas]_: - -.. math:: - - \forall A_1, A_2 \subseteq A . A_1 \cap A_2 = \emptyset \Rightarrow - \mu(A_1 \cup A_2) = \mu(A_1) + \mu(A_2) - -which we can express in Leon as - -.. code-block:: scala - - def additivity[A](m: Meas[A], xs: Set[A], ys: Set[A]): Boolean = { - require(m.isMeasure && (xs & ys).isEmpty) - m(xs ++ ys) == m(xs) + m(ys) - }.holds - -We can prove this property using structural induction on the parameter -``m``, case analysis on the parameters ``xs`` and ``ys``, equational -reasoning, and properties of rational numbers (in the form of -user-defined lemmas) as well as sets (using Leon's built-in support). - -.. code-block:: scala - - def additivity[A](m: Meas[A], xs: Set[A], ys: Set[A]): Boolean = { - require(m.isMeasure && (xs & ys).isEmpty) - m(xs ++ ys) == m(xs) + m(ys) because { - m match { - case Empty() => trivial - case Cons(x, w, n) => if (xs contains x) { - w + n(xs ++ ys) ==| additivity(n, xs, ys) | - w + (n(xs) + n(ys)) ==| plusAssoc(w, n(xs), n(ys)) | - (w + n(xs)) + n(ys) ==| !(ys contains x) | - m(xs) + m(ys) - }.qed else if (ys contains x) { - w + n(xs ++ ys) ==| additivity(n, xs, ys) | - w + (n(xs) + n(ys)) ==| plusComm(w, (n(xs) + n(ys))) | - (n(xs) + n(ys)) + w ==| plusAssoc(n(xs), n(ys), w) | - n(xs) + (n(ys) + w) ==| plusComm(n(ys), w) | - n(xs) + (w + n(ys)) ==| !(xs contains x) | - m(xs) + m(ys) - }.qed else { - n(xs ++ ys) ==| additivity(n, xs, ys) | - n(xs) + n(ys) - }.qed - } - } - }.holds - -The full proof (including the proofs of all helper lemmas) as well as -its generalization to *sub-additivity* can be found in the -``testcases/verification/proof/measure/`` directory of the Leon -distribution [#example-dir]_. - - -Quick Recap ------------ - -Let's summarize what we've learned here. To write proofs efficiently, -it's good to keep the following in mind: - -1. Always use a proper timeout and ask Leon for more information about - what he tries to verify, e.g. ``--timeout=5 --debug=verification``. - -2. Use ``@induct`` when working on structurally inductive proofs to - get a more precise feedback from Leon: this will decompose the - proof into a base case and an inductive case for the first argument - of the function under consideration. - - If Leon isn't able to verify the proof using ``@induct``, try - performing manual case analysis. - -3. Modularize your proofs and verify *sub-goals*! - - - use plenty of helper lemmas; - - use ``check`` abundantly; - - if possible use the relational reasoning DSL presented above. - - This is especially handy when you can connect the two sides of a relational - claim with sub-statements. - - -.. rubric:: Footnotes - -.. [#example-dir] The source code of this example and all other in - this chapter are included in the Leon distribution. Examples about - lists can be found in ``library/collection/List.scala``, the other - examples are located in the ``testcases/verification/proof/`` - directory. - -.. [#props-not-types] Perhaps surprisingly, propositions and proofs - live in the same universe in Leon. This is contrary to - e.g. type-theoretic proof assistants where propositions are - represented by types and proofs are terms inhabiting such types. - -.. [#dicrete-meas] To be precise, we are assuming here the underlying - measurable space :math:`(A, \mathcal{P}(A))`, where :math:`A` is - countable and :math:`\mathcal{P}(A)` denotes its discrete σ-algebra - (i.e. the power set of :math:`A`).