Introduction & Foundations

Preconditions

Files:     [Slides]     [Code skeletons]

We will work with various tools, including the Viper verification infrastructure. Please bring a device on which you can run Viper (requires Java and VS Code) to the lecture. A guide for installing Viper is found here.

Furthermore, we will expect you to read and write simple formulas in (mostly quantifier-free) predicate logic. Suitable literature to (re-)gain the necessary background is found in the preliminaries. The same page also briefly introduces our notation for these formulas.

Postconditions

Files:     [Slides with solutions]     [Full code examples]

What you should have learned after completing chapter 1:

  • Program verification means proving that a program meets its specification.
  • How automated program verifcation compares, on a high level, to other techniques that aim to increase our confidence in the correctness of software.
  • What is the look and feel of automated verifiers that follow the principle of verification like compilation, including modular reasoning with contracts, incremental construction of verified code, and the notion of a trusted codebase.
  • Program verification relies on rigorous mathematical definitions. We formalize the verification problem using Floyd-Hoare triples. There are different notions of validity or correctness for such triples; the strongest one is total correctness.
  • To check whether a Floyd-Hoare triple is valid, we attempt to construct a verification condition, a logical predicate, which we can then check for validity, that is, it must be true for all possible program states.
  • There are two systematic approaches for constructing verification conditions: start at the precondition and construct a suitable predicate by moving forward through the program or start at the postcondition and move backward through the program. The resulting predicate transformers compute the strongest postcondition and the weakest precondition, respectively.
  • How weakest preconditions compare to strongest postconditions.

Homework

Reading assignment

The course page contains additional material that you should be able to read after attending the lecture; it might also help you with the homework tasks. In summary:

  • Chapter 4.1 describes the fragment of the Viper language used in the lecture.
  • Chapter 4.2 formally defines the syntax and semantics of the PL0 language.
  • Chapter 4.3 formally defines Floyd-Hoare triples for the PL0 language.
  • Chapter 4.4 formally defines weakest preconditions for the PL0 language.
  • Chapter 4.5 formally defines strongest postconditions for the PL0 language.

Most of these pages give more precise definitions than those we used on the lecture slides, but nothing is conceptually new. Go over the above chapters and check for yourself if you can follow the formalization. If something is unclear, add questions to your submission in a file QUESTIONS.md or ask them during question time.

Furthermore, remember to prepare for the next lecture.

Tasks

Submission deadline: Thursday, September 8, 12:59 (right before the lecture).

Please submit your solutions by pushing all relevant files to the GitLab repository assigned to your group. If you have not been assigned a repository yet, contact the teacher.

Task 1 (5 points): Forward reasoning with Viper

For each of the Viper programs below, replace TODO by the strongest predicate such that the contract verifies; try to find a predicate that is as simple as possible.

Hint: Use the Viper verifier to check whether the program verifies for your your proposed predicates.

(a)

method a(x: Int, y: Int) returns (z: Int)
    requires 0 <= x && x <= y && y < 100
    ensures TODO
{
    z := y - x
}

(b)

method b()
{
    var x: Int 
    assume 0 <= x && x < 100 
    x := 2 * x 
    assert TODO
}

(c)

method c() {
    var x: Int 
    var y: Int 

    assume x > 0 && x < y 

    x := x + 23
    y := y - 3 * x 

    assert TODO
}

(d)

method d() {
   var x: Int       
   var y: Bool

   assume x > 0
   
   x := x + 1
   if (y) {
     var z: Int
     x := x + z
   } else {
     x := 42
   }

   assert TODO
}

Task 2 (5 points): Backward reasoning with Viper

For each of the Viper programs below, replace TODO by the weakest predicate such that the contract verifies; try to find a predicate that is as simple as possible.

Hint: Use the Viper verifier to check whether the program verifies for your your proposed predicates.

(a)

method a(x: Int, y: Int) returns (X: Int, Y: Int)
    requires TODO
    ensures X == y && Y == x
{
    X := y - x
    Y := y - X
    X := Y + X
}

(b)

method b() {

    var x: Int 
    var y: Int 

    assume TODO

    x := x + y 
    y := x * y

    assert x > y
}

(c)

method c() {
    var x: Int 
    var y: Int 

    assume TODO

    if (y > 5) {
        y := x - y
    } else {
        x := y - x
    }

    assert x > 7
}

(d)

method d(x: Int) returns (y: Int) 
    requires TODO
    ensures y % 2 == 0
{
    if (x < 17) {
        if (x > 3) {
            y := 1
        } else {
            y := 2
        }
    } else {
        y := 6
    }
}

Task 3 (7 points): Encoding conditionals

The language PL0 is simplistic and does not support basic statements (see full definition), such as conditionals if (b) { S1 } else { S2 }. Such a statement evaluates the Boolean expression b in the current program state; if the result is true, we execute S1; otherwise, we executes S2.

In terms of our formal operational semantics, the semantics of conditionals is given by the following inference rules:

(a) Define the weakest precondition of conditionals, that is, WP(if (b) { S1 } else { S2 }, Q).

(b) Define the strongest postcondition of conditionals, that is, SP(P, if (b) { S1 } else { S2 }).

(c) Encode the conditional if (b) { S1 } else { S2 } as a PL0 program. That is, write a PL0 program ENC(if (b) { S1 } else { S2 }) that models the effect of a conditional.

Hint: You may write ENC(S1) and ENC(S2) do denote statements the S1 and S2 to which the encoding of conditionals has already been applied.

(d) Prove that your encoding in (c) is correct in the sense that it yields verification conditions that are logically equivalent to the direct definitions of WP and SP from (a) and (b). That is, show that

  • WP(if (b) { S1 } else { S2 }, Q) <==> WP(ENC(if (b) { S1 } else { S2 }), Q) and
  • SP(P, if (b) { S1 } else { S2 }) <==> SP(P, ENC(if (b) { S1 } else { S2 })) are valid, where <==> denotes logical equivalence.

Task 4 (8 points): Forward reasoning about total correctness

In the lecture, we noticed a difference between weakest preconditions and strongest postconditions: for strongest, the computed verification condition is valid even if the program encounters a runtime error. This corresponds to the notion of partial correctness. By contrast, the verification conditions obtained from computing weakest preconditions consider total correctness, that is, they enforce safety (i.e., the absence of runtime errors) and termination.

To enable forward reasoning about total correctness, we need to compute a second verification condition that enforces safety.

(a) Give a recursive definition of a predicate transformer SAFE: Pred --> PL0 --> Pred such that the predicate SAFE(P, S) is the verification condition that needs to be checked to enforce that starting S on any state in precondition P does not lead to a runtime error. As for strongest postconditions, SAFE(P, S) should reason about statements in forward direction.

Hint: It might help to first express the same safety property in terms of weakest preconditions.

(b) Using your definition in (a), prove that, for all PL0 statements S and all predicates P, Q, we have

P ==> WP(S, Q) valid
if and only if
SAFE(P, S) valid and SP(P, S) ==> Q valid

Hint: Proceed by structural induction on the syntax of statements S.