This page documents the KIV solutions of the VerifyThis Competition held at ETAPS 2015 in London.
The version of KIV used can be downloaded at the Eclipse update site https://swt.informatik.uni-augsburg.de/kiv/v6/verifythis2015/site.
The solutions submitted during the competition are archived below. The submissions were recognized with the best student team award. The screen recordings are completely unedited.
Contact: Gidon Ernst & Jörg Pfähler, {ernst,pfaehler} (at) isse.de
Project overview relaxed-prefix, the property that was to be shown is this one. The partial proof from the competition has been completed afterwards, extending the invariant slightly and adding three additional helper-lemmas.
All definitions can be found in this specification.
The program has been modified slightly to accommodate for KIV's lack of return statement, namely by a variable break
. Note that we do not increase the index i
then, which unifies some cases of the loop invariant.
The relaxed prefix property is given by the predicate relaxed-prefix?
by two cases. Sequence pat
is a relaxed prefix of x
iff one of the following holds:
pat ⊑ x
, i.e., it is a proper prefix (where ⊑
is defined in the library.pat = pat0 + a + pat1
such that pat0 + pat1 ⊑ x
The invariant (predicate inv
) states that we have checked the relaxed prefix property up to the current index i
. If the shift is 0, then the first i
elements of pat
, written as firstn(i, pat)
must be a proper prefix of x
. Otherwise, there is the corresponding split and we know that the intermediate element a
mismatches the list x
at that position. If the loop is aborted with break = true
we track the second mismatch as well.
The proof is unfortunately not so straight forward. The following equivalences are exploited (omitting any size constraints here):
firstn(n + 1, x) = firstn(n, x) + x[n]
x + a ⊑ y ↔ x ⊑ y ∧ y[# x] = a
The (technically) complicated part is showing that returning false
can be justified: we have found two mismatches (rightmost part of the proof). The reason is that the invariant gives one arbitrary split, whereas the negated relaxed-prefix?
gives another one. These have to be matched up properly.
Project overview parallel-gcd-rg, developed after the competition. Compositional verification of partial correctness using rely guarantee reasoning.
The implementation runs procedure GCD_loop
interleaved to itself symmetrically. Access is at the level of reads/writes, modeled by two local variables X
and Y
. Each assignment and test of conditional is a separate step. Note that the Done
flag could be avoided with a do { ... } while
loop (which is not supported by KIV).
GCD_loop(; N, M) {
let Done = false, X = 0, Y = 0
in while ¬ Done do {
X := N;
Y := M;
if X = Y
then Done := True
else if X < Y
then M := Y - X
}
}
The process modifying N
provides the following guarantee that is in turn the rely for the process modifying M
. Such a step from N, M
to N', M'
satisfies
M
remains the same: M' = M
N
can only decrease N' ≤ N
N ≤ M → N' = N
These properties are encoded in a predicate rely
.
The invariant of the loop is simply that for initial values n,m
of N,M
the GCD is still the same, i.e.
gcd(n,m) = gcd(N,M)
Two theorems are proved
[: X | rely, guar, inv, proc(; X), post)
states that procedure proc
runs in a rely
-compatible environment, providing guar
and maintaining the state-invariant inv
. If proc
terminates, then post
holds (no termination guarantee). Variables X
denote the state shared with other processes.See also: G. Schellhorn, B. Tofan, G. Ernst, J. Pfähler, and W. Reif:
RGITL: A Temporal Logic Framework for Compositional Reasoning about Interleaved Programs
Project overview dancing-links, the property that was to be shown is this one. This is the exact solution from the competition.
Specification cell defines heap cells with pointers left
and right
; and data val
of generic type elem
. Specifications reflist cell-heap instantiate lists with references resp. heaps with this cell type defined above (i.e., just some boiler-plate).
Specification dllist defines the separation logic abstraction recursively given a list of values (third parameter), and proves some lemmas, notably to unfold the abstraction for the last or some intermediate list cell.
The abstraction is a heap-predicate
dls(head, prev, x, rx, next, tail)
where head
/tail
are a pointer to the first/last cell. Prev
/next
are the left/right pointer of the first/last cell (this can be used to express cyclic lists for example). The list x
collects all the values (could be computed from rx
) and the list rx
collects the references to the cells between and including head
and tail
.
Slightly simplifying, there is one case for empty lists:
dls(head, prev, [], [], next, tail)
= emp ∧ head = next ∧ tail = prev
where emp
denotes an empty remaining heap part. In KIV, lifting of pure formulas p
of type bool
to heap assertions heap → bool
is written ⌜p⌝
. The recursive case unfolds one heap cell at head
:
dls(head, prev, a+x, r0+rx, next, tail)
= r0 = head
∧ ∃ r. head ↦ mkcell(prev, r, a)
* dls(r, head, x, rx, next, tail)
In KIV, the points-to assertion ↦
is written =>
and the existential quantifier is written as a higher-order operator _∃
taking a λ
-abstraction as parameter. Note that the actual definition destructs rx
with selectors .first
and .rest
instead of pattern matching so that dls
is specified for all cases.
The operations remove
and its converse insert
("unremove") are defined here. Since the formatting of the web-export is not so nice, we repeat the definitions here. Parameter H
is the heap (after the semicolon) that is explicit in our programs. It is passed by reference, so that assignments to the variable H
in the program are propagated to the caller. Note that H[r].left := ...
is just an abbreviation for the assignment H := H[r, ...]
where H[r, ce]
denotes (algebraic) modification of the heap (you can see this in the web-export linked above).
remove(r; H) {
let prev = H[r].left,
next = H[r].right
in {
H[next].left := prev;
H[prev].right := next;
}
};
insert(r; H) {
let prev = H[r].left,
next = H[r].right
in {
H[next].left := r;
H[prev].right := r;
}
};
The two main proofs are a contract for remove and one for insert.
Removing r
from a doubly linked list has as part of its precondition
dls(head, prev, x0+a+x1, rx0+r+rx1, next, tail)
i.e., there is a split such that r
is in the list of references; x0
and rx0
must have same length, the prefix/postfix of the split x0
resp. x1
is assumed to be nonempty. The postcondition pulls r
out of the list:
r ↦ mkcell(rx0.last, rx1.first, a)
* dls(head, prev, x0+x1, rx0+rx1, next, tail)
In KIV, there is an additional part of the heap P: heap → bool
(a higher-order variable), providing explicit framing. Whenever this lemma is used in some context, P
can be instantiated to whatever (disjoint) heap assertion needs to be preserved over the call (the frame rule of separation logic is not generally valid in KIV, because the heap H
is explicit and the program can do non-local actions).
We proved that reinsertion works in the main correctness theorem.
To facilitate this, we have reformulated the two contracts as follows: remove, insert.
Finally, we proved that removing two elements and reinserting these works as well.
We'd also like to share our submission during the collection of challenge proposals, namely a simple, partial implementation of Cuckoo Hashing.
Cuckoo hash tables are a variant of hash tables that allow constant-time lookups. The basic idea is to have two tables, T1
and T2
and two corresponding hash functions h1
and h2
. Here we consider an implementation of sets using arrays to represent the two tables.
An item x
is in the data structure iff T1[h1(x)] = x ∨ T2[h2(x)] = x
, i.e., x
is found in table T1
or T2
using the respective hash function.
Insertion of an element x
first checks if the element is already present. If not, x
is inserted into T1
at position h1(x)
. If this slot is already occupied, i.e., T1[h1(x)] = y
, this element y
is moved to table T2
using the same strategy.
Elements are moved between T1
and T2
until an empty slot is found. To prevent looping forever, after a bounded number of tries the tables are resized, rehashed and insertion is restarted with the current pending element. Resizing of the table shall not be part of the challenge.
The following code is a straight forward translation from R. Pagh and F.F. Rodler: Cuckoo Hashing, J. Algorithms, 51(2):122-144, 2004. It has not been tested or compiled.
class CuckooHashSet<Elem> {
Elem[] T1,T2;
int size;
CuckooHashSet(int size) {
T1 = new Elem[size];
T2 = new Elem[size];
this.size = size;
}
int h1(x: Elem);
int h2(x: Elem);
boolean lookup(x: Elem) {
return T1[h1(x)] == x || T2[h2(x)] == x;
}
void insert(x: Elem) {
if(lookup(x))
return;
while(true) {
Elem y;
if(T1[h1(x)] == null) { T1[h1(x)] = x; return; }
y = T1[h1(x)];
T1[h1(x)] = x;
x = y;
if(T2[h2(x)] == null) { T2[h2(x)] = x; return; }
y = T2[h2(x)];
T2[h2(x)] = x;
x = y;
}
}
}
Specify and verify the partial correctness operations lookup and insert wrt. the semantics of sets. You can make the following assumptions
h1
and h2
return positions in the arrays: hi(x) < Ti.length
. Other than that, nothing should be assumed about the hash values. In particular it is unspecified whether h1(x) = h2(x)
.A recursive version is shorter and easier to verify. It passes the hash functions as parameters (in order to swap them), and assumes that the element is not already in the data structure.
void insert_rec(Elem x, Elem[] T1, Elem[] T2,
Function<Elem,int> h1, Function<Elem, int> h2)
{
Elem y = T1[h1(x)];
T1[h1(x)] = x;
if(y != null)
insert_rec(y, T2, T1, h2, h1);
}
Project overview cuckoo. The implementation is here, the invariants and abstraction function are here Correctness proofs: lookup, insert, recursive insert. Since there is no built-in null
in KIV, we define a variant slot for array elements, that can either be empty or full with a given entry.
Invariants of the hash table (we prove that they are preserved by insert):
Ti[n] = x
implies hi[x] = n
.