Dr. David J. Pearce

Verifying the Whiley Standard Library


For sometime now, its been possible to use Boogie / Z3 as a backend for verifying Whiley programs. Initially that was pretty sketchy, but it’s really starting to ramp up now. If you haven’t heard of it before, Boogie is an Intermediate Verification Language (IVL) that provides a human-readable interface sitting on top of an SMT solver. In this case, we use Z3 as the SMT solver — but it is possible to use others, such as CVC4, etc. Boogie was initially developed at Microsoft, but is now maintained by a consortium that appears to include Facebook and Amazon, amongst others.

For example, here’s a simple Whiley program:

function abs(int x) -> (int r)
ensures r >= 0
ensures (r == x) || (r == -x):
   if x >= 0:
      return x
   else:
      return -x

The Whiley2Boogie backend is responsible for translating the above program into Boogie. Here’s a simplified version of how that looks:

procedure abs(x : int) returns (r : int)
ensures r >= 0;
ensures (r == x) || (r == -x);
{
   if(x >= 0) {
      r := x;
      return;
   } else {
      r := -x;
      return;
   }
}

Here we can see there are a few differences from the Whiley code but, in this example at least, the two look quite similar. In fact, Boogie implements a form of Dijkstra’s Guarded Command Language which has been extended with syntax to bring it closer to a programming language. However, in some cases, it can end up looking quite different.

STD.wy

The Whiley standard library, STD.wy, presents an interesting target for verification, as it is currently the largest single body of code written in Whiley. My goal is to verify the standard library and, from thereafter, ensure it is verified on every commit (using a GitHub Action). The library is still pretty small, but contains some of the things you would expect, such as collections (e.g. std::vector, std::hash_map), ASCII support (std::ascii), math functions (std::math) and various array manipulation functions (std::array). Yes, it is very much a work in progress … and now is the time to get verification ingrained as part of the build process.

As an example, here’s a function from std::array:

// find first index after a given start point in list which matches character.
// If no match, then return null.
function first_index_of<T>(T[] items, T item, uint start) -> (uint|null index)
// Starting point cannot be beyond array
requires start <= |items|
// If int returned, element at this position matches item
ensures index is uint ==> items[index] == item
// If int returned, element at this position is first match
ensures index is uint ==> !contains(items,item,start,index)
// If null returned, no element in items matches item
ensures index is null ==> !contains(items,item,start,|items|):
    //
    for i in start .. |items|
    // No element seen so far matches item
    where !contains(items,item,start,i):
        if items[i] == item:
            return (uint) i
    //
    return null

This searches forward in items from a given start index and returns either the first index matching item or (if none exists) returns null. The specification is reasonably involved, but essentially says this in logical form. To keep things a bit more intuitive we’ve used contains() instead of raw quantifiers. Here, constrains() is a property defined in std::array as follows:

property contains<T>(T[] lhs, T item, int start, int end)
// Some index in given range contains item
where some { i in start..end | lhs[i] == item }

The module std::array contains several predefined properties like this which are helpful in specifying array manipulating functions. The great thing about first_index_of() above is that we can now statically verify that its implementation meets its specification. No need to write lots of extensive tests checking all the edge cases!! This also means first_index_of() is guaranteed not to perform an out-of-bounds access or exhibit other undefined behaviour. That is something really quite powerful, and Boogie / Z3 is key to making it work.

Challenges

At this point, the goal was fairly straightforward: go through all the library functions adding specifications so they will now statically verify. This was quite a laborious task since, initially, large chunks of the library had not been specified. Having largely completed that now, we can proceed incrementally by requiring that all new code must be fully specified and pass verification.

Still, there are some challenges. Most notably, we have at least one function which currently cannot be verified (due to limitations with Boogie / Z3). This is the following function:

unsafe function copy<T>(T[] src, uint srcStart, T[] dest, uint destStart, uint length) -> (T[] result)
// Source array must contain enough elements to be copied
requires (srcStart + length) <= |src|
// Destination array must have enough space for copied elements
requires (destStart + length) <= |dest|
// Result is same size as dest
ensures |result| == |dest|
// All elements before copied region are same
ensures equals(dest,0,result,0,destStart)
// All elements in copied region match src
ensures equals(src,srcStart,result,destStart,length)
// All elements above copied region are same
ensures all { i in (destStart+length) .. |dest| | dest[i] == result[i] }:
    T[] _dest = dest // ghost
    //
    for i in 0 .. length
    // Size of dest unchanged
    where |dest| == |_dest|
    // Everything below destStart unchanged
    where equals(_dest,0,dest,0,destStart)
    // Everything copied so far is equal
    where equals(src, srcStart, dest, destStart, i)
    // Everything above j is unchanged
    where equals(_dest,dest,i+destStart,|dest|):
        dest[i+destStart] = src[i+srcStart]
    //
    return dest

This is roughly equivalent to System.arraycopy() in Java, and its not very complicated. Unfortunately, Boogie / Z3 cannot verify this without additional help. Whilst this is potentially something we can fix in the Whiley2Boogie backend, for now we simply mark the function as unsafe. This means it will be ignored during verification. However, its specification is still used when verifying other functions not marked unsafe. Whilst this is not ideal, it is a pragmatic compromise for now.

Conclusion

Using Boogie / Z3 though the Whiley2Boogie backend has significantly improved our ability to verify non-trivial Whiley programs. Work is ongoing here, and you can find a number of interesting benchmarks we are currently working through.