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
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)
andSP(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
.