-
Notifications
You must be signed in to change notification settings - Fork 39
Help My Verification Fails
Step one is always to make sure that your annotations are actually correct, and that the verifier has enough information to continue. Use some proof debugging to get insight into why your verification fails. Some steps require extra assertions or verification lemmas before a proof can be completed.
Sometimes, your annotations are actually correct, but the proof fails (non-deterministically). This is called proof brittleness; see Proof Brittleness and Countermeasures for more details.
Often, you will not write a specification that immediately proves your code correct. In this section, we suggest some general proof debugging steps to help your progress. We suggest using the --watch flag (see VerCors usage tips).
Dafny, a similar verifier (but not based on separation logic), also has a useful section on proof debugging. Most tips are also applicable to VerCors.
Sometimes an obvious fact to you may not be obvious to VerCors. For example, the following fails verification.
requires |a| == 1 && |b| == 1 && a <= b;
void same_elems(set<int> a, set<int> b) {
int x = \choose(a); // Random element from a
int y = \choose(b); // Random element from b
assert x == y;
}
Often it is useful to keep adding assertions that you assume to be true, to see exactly which step is failing. Sometimes adding these will make verification go through, as in the following example.
requires |a| == 1 && |b| == 1 && a <= b;
void same_elems(set<int> a, set<int> b) {
int x = \choose(a);
int y = \choose(b);
assert x in a;
assert x in b;
assert |a - {x}| == 0;
assert |b - {x}| == 0;
assert x == y;
}
Apparently the line assert |b - {x}| == 0; was essential for VerCors to prove this. This has to do with how sets are encoded in this case. In general, adding assertions helps you better understand verification failures.
Suppose you want to verify code with the following structure.
requires A;
ensures B;
void f(...) {
if(...){
...
} else if(...) {
...
} else {
...
}
}
And the ensures annotation fails, but you do not know for which branch. This can be discovered as follows.
requires A;
ensures B;
void f(...) {
if(...){
...
assert B; // First branch passes
} else if(...) {
...
assert B; // Second branch passes
} else {
...
assert B; // Throws a verification error, so apparently the error was here.
}
}
After an assume false is placed, all verification conditions afterward, within the same path, pass vacuously. Typically, this allows verification to be faster when building up a proof. For example:
requires A;
ensures B;
void f(...) {
if(...){
assume false; // This branch is skipped
...
} else if(...) {
assume false; // Just like this branch
...
} else {
... // This branch is still verified.
}
}
Sometimes, it might be unclear whether code is actually reached. It could be that the path conditions are formed in such a way that this is impossible. Placing assert false will only throw an assertion error if this point is reached.
requires A;
ensures B;
void f(...) {
if(...){
...
} else if(...) {
assert false; // If this doesn't raise an assertion error, apparently this code is not reached.
...
} else {
...
}
}
Sometimes a proof fails deep inside nested statements. It can then be easier to model the program using inhales and exhales to see what is going on, with less complex verification conditions. For example, in the following we build up an arbitrary array, which we can then modify.
void test(){
int[] A;
assume A != null && A.length == 32;
inhale (\forall* int i; 0<=i && i<32; Perm({:A[i]:}, write));
assume (\forall int i; 0<=i && i<32; {:A[i]:} > 0);
loop_invariant... // For loop which modifies the array, you want to check
for (int i=0; i<32;i++) {
....
}
}
A verification lemma is a ghost/proof method whose main purpose is to establish a logical property used by other code.
A concrete example from this repository is the reachability proof in reachability.pvl:
// If pathExists(start, v) and u is a direct successor of v, then pathExists(start, u).
requires ValidGraph(N, transitions);
requires 0 <= start && start < N;
requires 0 <= v && v < N;
requires 0 <= u && u < N;
requires pathExists(N, transitions, start, v);
requires u in transitions[v];
ensures pathExists(N, transitions, start, u);
decreases;
void ExtendPath(int N, seq<set<int>> transitions, int start, int v, int u)
{
if(start == u) {
} else if(start == v) {
seq<int> path = [v, u];
assert isAPath(N, transitions, path);
} else {
seq<int> path :| isAPath(N, transitions, path) && path[0] == start && path[|path| - 1] == v;
seq<int> newPath = path + [u];
assert newPath[|path|] == u;
assert newPath[|newPath| - 1] == u;
assert isAPath(N, transitions, newPath);
}
}
This lemma is then used by another lemma (ExtendPathPrefix) to lift the argument to all successors, and eventually by the algorithm proof.
Without these intermediate lemmas, it is often impossible to prove the intended properties.
Tutorial
- Introduction
- Installing and Running VerCors
- Prototypal Verification Language
- Specification Syntax
- Permissions
- Termination
- Axiomatic Data Types
- Arrays and Pointers
- Parallel Blocks
- GPGPU Verification
- Atomics and Locks
- Predicates
- Inheritance
- Exceptions & Goto
- VeyMont
- Platform-Dependent Verification
- Advanced Concepts
- Help My Verification Fails
- Proof Brittleness and Countermeasures
- Unsupported Features
- Annex
- Case Studies
Developing for VerCors