ArticlePDF Available

Verifying Whiley Programs with Boogie

Authors:

Abstract and Figures

The quest to develop increasingly sophisticated verification systems continues unabated. Tools such as Dafny, Spec#, ESC/Java, SPARK Ada and Whiley attempt to seamlessly integrate specification and verification into a programming language, in a similar way to type checking. A common integration approach is to generate verification conditions that are handed off to an automated theorem prover. This provides a nice separation of concerns and allows different theorem provers to be used interchangeably. However, generating verification conditions is still a difficult undertaking and the use of more “high-level” intermediate verification languages has become commonplace. In particular, Boogie provides a widely used and understood intermediate verification language. A common difficulty is the potential for an impedance mismatch between the source language and the intermediate verification language. In this paper, we explore the use of Boogie as an intermediate verification language for verifying programs in Whiley. This is noteworthy because the Whiley language has (amongst other things) a rich type system with considerable potential for an impedance mismatch. We provide a comprehensive account of translating Whiley to Boogie which demonstrates that it is possible to model most aspects of the Whiley language. Key challenges posed by the Whiley language included: the encoding of Whiley’s expressive type system and support for flow typing and generics; the implicit assumption that expressions in specifications are well defined; the ability to invoke methods from within expressions; the ability to return multiple values from a function or method; the presence of unrestricted lambda functions; and the limited syntax for framing. We demonstrate that the resulting verification tool can verify significantly more programs than the native Whiley verifier which was custom-built for Whiley verification. Furthermore, our work provides evidence that Boogie is (for the most part) sufficiently general to act as an intermediate language for a wide range of source languages.
This content is subject to copyright. Terms and conditions apply.
Journal of Automated Reasoning (2022) 66:747–803
https://doi.org/10.1007/s10817-022-09619-1
Verifying Whiley Programs with Boogie
David J. Pearce1·Mark Utting2·Lindsay Groves1
Received: 22 April 2020 / Accepted: 14 January 2022 / Published online: 20 March 2022
© The Author(s) 2022
Abstract
The quest to develop increasingly sophisticated verification systems continues unabated.
Tools such as Dafny, Spec#, ESC/Java, SPARK Ada and Whiley attempt to seamlessly inte-
grate specification and verification into a programming language, in a similar way to type
checking. A common integration approach is to generate verification conditions that are
handed off to an automated theorem prover. This provides a nice separation of concerns and
allows different theorem provers to be used interchangeably. However, generating verification
conditions is still a difficult undertaking and the use of more “high-level” intermediate veri-
fication languages has become commonplace. In particular, Boogie provides a widely used
and understood intermediate verification language. A common difficulty is the potential for
an impedance mismatch between the source language and the intermediate verification lan-
guage. In this paper, we explore the use of Boogie as an intermediate verification language for
verifying programs in Whiley. This is noteworthy because the Whiley language has (amongst
other things) a rich type system with considerable potential for an impedance mismatch. We
provide a comprehensive account of translating Whiley to Boogie which demonstrates that
it is possible to model most aspects of the Whiley language. Key challenges posed by the
Whiley language included: the encoding of Whiley’s expressive type system and support
for flow typing and generics; the implicit assumption that expressions in specifications are
well defined; the ability to invoke methods from within expressions; the ability to return
multiple values from a function or method; the presence of unrestricted lambda functions;
and the limited syntax for framing. We demonstrate that the resulting verification tool can
verify significantly more programs than the native Whiley verifier which was custom-built
for Whiley verification. Furthermore, our work provides evidence that Boogie is (for the
most part) sufficiently general to act as an intermediate language for a wide range of source
languages.
BMark Utting
m.utting@uq.edu.au
David J. Pearce
david.pearce@ecs.vuw.ac.nz
Lindsay Groves
lindsay@ecs.vuw.ac.nz
1Victoria University of Wellington, Wellington, New Zealand
2The University of Queensland, Brisbane, Australia
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
748 D. J. Pearce et al.
Keywords Whiley ·Boogie ·Verifying compiler ·Intermediate verification language ·
Semantic translation ·Impedance mismatch ·Flow typing ·Verification conditions
1 Introduction
The idea of verifying that a program meets a given specification for all possible inputs has
been studied for a long time. Part of the appeal of software verification is that it can ensure
theoretical correctness of a software module for all possible usages. This is complementary
to testing which, by acting at a more concrete level, may detect resource or hardware errors
that are typically outside the scope of software verification [43].
According to Hoare’s vision, a verifying compiler uses automated mathematical and
logical reasoning to check the correctness of the programs that it compiles”[80]. A vari-
ety of tools have blossomed in this space, including Spec# [16], Dafny [104], Why3 [65],
OpenJML [46], ESC/Java [68], VeriFast [85], SPARK/Ada [121], AutoProof for Eiffel [163],
Frama-C [52], KeY [2], SPARK/Ada [13,40] and Whiley [139,159]. Automated theorem
provers are integral to such tools and are responsible for discharging proof obligations [16,45,
68,85]. Various satisfiability modulo theory (SMT) solvers are typically used for this, such
as Z3 [53], CVC4 [19,20], Yices2 [61], Alt-Ergo [49], Vampire [82,95] or Simplify [55].
These provide handcrafted implementations of important decision procedures, e.g. for linear
and nonlinear arithmetic [23,44,62,144], congruence [126,128] and quantifier instantia-
tion [54,71,145,146]. Different solvers are appropriate for different tasks, so the ability to
utilise multiple solvers can improve the chances of successful verification.
Verifying compilers often target an intermediate verification language, such as Boo-
gie [14], WhyML [28,65]orViper[125], as these provide a nice separation of concerns
and allow different theorem provers to be used interchangeably. SMT-LIB [21] provides
another standard readily accepted by modern automated theorem provers, although it is often
considered rather low level [28]. One issue faced by intermediate verification languages is
the potential for an impedance mismatch [139] (see Sect. 5). This arises when constructs in
the source language cannot be easily translated into those of the intermediate verification
language (and vice versa).
Whiley is a programming language with first-class support for software specifications that
is designed to simplify verification [43,134,135,137140,159,168,169]. An important
goal was to develop a system which is as accessible as possible and which one could imag-
ine being used in a day-to-day setting. As such, Whiley superficially resembles a modern
imperative language and employs flow typing [77,133,160] to eliminate unnecessary casts
(which also aids specification). The ultimate aim is that all programs written in Whiley will
be verified at compile time to ensure their specifications hold which, for example, has obvi-
ous application in safety-critical systems [40,134]. In this paper, we explore Boogie as an
intermediate verification language for Whiley. Our motivation is the desire to improve the
verification capability of Whiley by leveraging the significant resources already invested in
the development of Boogie (and Z3). A particular concern is the potential for an impedance
mismatch arising, such as from Whiley’s type system (e.g. which supports union types and
flow typing).
The contributions of this paper include:
(Translation) A comprehensive account of our encoding of Whiley programs into Boogie
for the purpose of verification. Whilst in many cases the translation is straightforward, a
number of challenges had to be overcome arising from Whiley’s design, including: the
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 749
encoding of Whiley’s expressive type system and support for flow typing and generics;
Whiley’s implicit assumption that expressions in specifications are well defined; the
ability to invoke methods from within expressions; the ability to return multiple values
from a function or method; the presence of unrestricted lambda functions; and Whiley’s
limited syntax for framing.
(Evaluation) An empirical comparison between Boogie/Z3 and the native Whiley verifier
using the existing suite of 1100+tests provided for the Whiley compiler. The results
confirm that Boogie/Z3 significantly outperforms the Whiley native verifier in terms of
the number of tests passing.
(Case Studies) A report into the use of Boogie/Z3 to verify a number of larger Whiley
programs, including a web-based implementation of Conway’s Game of Life and a num-
ber of challenges from the VerifyThis 2019 competition [59]. From these case studies
we identify several areas in which the Whiley language or libraries could be improved
to better exploit Boogie.
We note also that our work provides further evidence of Boogie’s utility as a general-
purpose intermediate verification language. In particular, compared with Dafny or Spec#,
Whiley was developed entirely independently from Boogie and includes various design
choices that are not necessarily a natural fit. As such, it was unclear from the outset of this
project whether or not Boogie would be sufficiently general for this task. Finally, compared
with our earlier paper [164], this paper represents a significant evolution and improvement
of our translation. We also provide a much more detailed account which covers almost the
entire language, including generics, lambdas, references and the handling of various sound-
ness issues. Our evaluation now includes a number of larger case studies, and we have
expanded the related work discussion.
Organisation. The remainder of this paper is organised as follows: Sect. 2provides an intro-
duction to Whiley and Boogie; Sect. 3provides a detailed description of our Whiley-to-Boogie
translator and discusses the various challenges encountered; Sect. 4presents our evaluation
using the existing Whiley compiler test suite and various case studies; Sect. 5examines the
related work; and finally, Sect. 6concludes. Finally, for reference, “Appendix A illustrates
our verified version of Conway’s Game of Life.
2 Background
We begin with an overview of Whiley and then a brief discussion of Boogie.
2.1 Whiley
The Whiley programming language has been developed to enable compile time verification of
programs and, furthermore, to make this accessible to everyday programmers [139,159]. The
Whiley Compiler (WyC) attempts to ensure that all functions and methods in a program meet
their specifications. When this succeeds, we know that: (i) all function/method postconditions
are met (assuming their preconditions held on entry); (ii) all invocations meet the respective
function or method precondition; (iii) runtime errors such as divide-by-zero, out-of-bounds
accesses and null-pointer dereferences cannot occur. Notwithstanding, such programs may
still loop indefinitely and/or exhaust available resources (e.g. stack or heap).
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
750 D. J. Pearce et al.
2.1.1 Primitive Types
Whiley provides a small number of primitive types, including: null ,bool ,byte
and int (for unbound integers). Likewise, types can be composed into records (e.g.
{int x, int y} ), arrays (e.g. int[] )andunions (e.g. null|int ). Here, the
latter represents a type which is either null or an int . Records can be constructed
using literals (e.g. {x:1,y:2} , whilst arrays can be constructed using either literals (e.g.
[1,2,3] )orgenerators (e.g. [0;3] which gives [0,0,0] ). The length of an array
can also be queried dynamically (e.g. |xs| ). As expected, user-defined types are supported
and can be declared as follows:
type Point is {int x, int y}
Whiley also supports type polymorphism (i.e. generics) and recursive types (which are
similar to algebraic data types) as follows:
type Node<T> is { T data, List<T> next }
type List<T> is null | Node<T>
The type {T data, List<T> next} indicates a record with two fields, data
and next . Thus, a List<T> is either null or a record with the given structure. For
completeness, we note that subtyping of generic types follows an (implicit) definition-site
variance protocol [3]. Furthermore, user-defined types in Whiley offer greater flexibility than
typically found with implementations of algebraic data types (e.g. in Haskell). For example:
type IntList is null |{int data, IntList next }
function id(IntList l) -> (List<int> r):
return l
The above illustrates how one recursive type ( IntList ) can implicitly subtype another
(List<int> ). This highlights a key advantage of typing in Whiley over, for example,
algebraic data types. The approach to typing taken in Whiley is, in fact, closer to structural
typing [36,60,72,117,118] with certain caveats to ensure safe treatment of type invariants
(see below).
2.1.2 Flow Typing
An unusual feature of Whiley is the use of a flow typing system [77,132,133,160] coupled
with union types [12,84]. Union types support runtime type tests to discriminate their cases,
as the following illustrates (recall List<T> from above):
function length<T>(List<T> list) -> int:
if list is null:
return 0
else:
return 1 + length(list.next)
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 751
This counts the number of nodes in a list. Here, we see flow typing in action as list is
automatically retyped to Node<T> on the false branch [132,133]. Flow typing turns out
to be particularly useful when specifying programs. Specifically, in (x is T) ==> e it
follows that xhas type Twithin the expression e. This helps, for example, when writing
postconditions (as we’ll see shortly).
2.1.3 Value Semantics
The semantics of Whiley divergefrom many mainstream languages (e.g. Java) in the treatment
of compound data types, such as arrays. Specifically, arrays and records in Whiley have value
semantics. This means they are passed and returned by value (as in Pascal, MATLAB [98]
or most functional languages). But, unlike functional languages (and like Pascal), values of
compound types can be updated in place [129,151]. This latter point serves to give Whiley
the appearance of an imperative language when, in fact, Whiley has a functional core. The
following illustrates:
function fill<T>(T[] items, int n, T v) -> (T[] nitems):
for iin 0..n:
items[i] = v
return items
Despite appearances, the above is a pure function which has no side effects. This contrasts
with languages like Java, where arrays are references and updating them has unavoidable
side effects. The following attempts to clarify this further:
int[] xs = [1,2,3]
int[] ys = xs
ys[0] = 0
assert xs[0] == 1
assert ys[0] == 0
In a language like Java, the assertion xs[0] == 1 would fail because xs and ys
would alias each other. However, since this is not the case in Whiley, the above verifies without
problem. We can think of arrays and records in Whiley as being immutable, so that updating
them effectively means cloning them. The reason this semantics is adopted in Whiley is to
facilitate their use in specification. Indeed, without a fundamental immutable collection type,
verification is inherently challenging [99].
2.1.4 Side Effects
Afunction in Whiley is pure and cannot have side effects. In contrast, a method
is impure and may have side effects, such as mutating the global heap or performing I/O.
Whiley provides reference types which are allocated from a single global heap. For example,
&int is a reference to an integer variable. The following illustrates the syntax:
&int p=new 1
&int q=p
*p=2
assert *p == *q
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
752 D. J. Pearce et al.
Here, the assignment through paffects q(because they are aliases), and hence, the
final assertion holds. We note that, at the time of writing, Whiley supports allocation but not
deallocation (and, hence, currently relies on garbage collection).
Statements which mutate the heap must appear within the body of a method and, for
example, are not permitted within a function . To illustrate a more complete example,
here is the classical algorithm for reversing a linked list [6]:
type LinkedList<T> is null | &{T data, LinkedList<T> next}
method reverse<T>(LinkedList<T> v) -> (LinkedList<T> r):
//
LinkedList<T> w = null
//
while !(v is null):
LinkedList<T> t = v->next
v->next = w
w=v
v=t
//
return w
We note that the above is not yet fully specified, and this would be necessary before its
behaviour could be fully verified (more on this later).
2.1.5 Packaging
Whiley currently supports a relatively limited form of packages and package management.
For example, the standard library, STD.wy, can be added as a dependency and compiled
against. The following illustrates a simple example:
import std::ascii
import append from std::array
function to_string(int[] items) -> (ascii::string str):
ascii::string r = "["
// Convert each element to an ascii string
for iin 0..|items|:
// Add comma (when necessary)
if i!=0:
r = append(r,",")
// Add element as string
r = append(r,ascii::to_string(items[i]))
return append(r,"]")
The above illustrates a simple function for converting an integer array into a string. This
employs standard library functions from the modules std::ascii and std::array .
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 753
Fig. 1 Implementation of indexOf() in Whiley, returning the least index in items which matches
item ,or null if no match exists
2.1.6 Specification and Verification
We now consider those features of Whiley provided for specifying and verifying programs.
Figure 1provides an initial example to illustrate the salient features:
Properties are used to specify things of interest, particularly to help with verification.
They are interpreted meaning that, during verification, they can be expanded/unrolled as
necessary. To facilitate this, they have a restricted form allowing them to be substituted
in place for their body. In contrast, functions are uninterpreted which helps ensure
verification remains (mostly) modular [78]. This means that, during verification, their
actual implementation is ignored at call sites (more on this below).
Preconditions are given by requires clauses and postconditions by ensures
clauses. Multiple clauses are simply conjoined together. We have found that allowing
multiple requires and/or ensures clauses can help readability, and note that
JML [48], Spec# [16]andDafny[104] also permit this.
Loop invariants are given by where clauses. Figure 1illustrates an inductive loop
invariant covering indices from zero to i(exclusive). Similarly, type invariants arise
from where clauses. For example, type nat hasaninvariantandisusedforvariable
ito avoid the need for a loop invariant of the form i>=0
. We consider good use
of type invariants as critical to improving the readability of function specifications.
Assertions must be statically checked during verification, thus providing a useful debug-
ging tool. For example, if during verification we are struggling to understand why a given
postcondition is not met, assertions can be added to check our beliefs at a given point.
In contrast, assumptions are not statically checked and, instead, are simply assumed to
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
754 D. J. Pearce et al.
hold during verification. As such, they are a useful tool for overriding the verifier in cases
where it cannot establish something we know to be true.
Flow typing simplifies postconditions (amongst other things) by ensuring that casts need
not be given. For example, without flow typing, the first ensures clause from Figure 1
would require a cast for ron the right-hand side.
Being uninterpreted means a function’s implementation can change arbitrarily without
affecting callers provided it still meets its specification. However, it also means that functions
need to be properly specified before they can be used, which is sometimes problematic (e.g.
when several functions are developed in tandem). For example, consider the following:
function max(int x, int y) -> (int r):
if x>=y:
return x
else:
return y
Whilst the above function is implemented correctly, it has yet to be specified. Perhaps this
has arisen because it is, in fact, part of a larger function being developed:
function max(int[] items, int i) -> (int r)
// At least one item must remain
requires 0 <= i && i < |items|
// Return greater than all remaining items
ensures all {kin i .. |items| | items[k] <= r }:
if (i+1) == |items|:
return items[i]
else:
return max(items[i], max(items,i+1))
At this moment, max(int[],int) cannot be statically verified because the speci-
fication for max(int,int) (or lack thereof) yields insufficient information at the call
site.
Framing. A related aspect of static verification is the need for clarity around side effects and
framing [9,91,92,130,131,153]. A key issue is the ability to distinguish the value of state
before a method call from that after it. Languages such as Dafny, JML and Boogie support
this by allowing one to refer to the “old” state of a location (i.e. the value it held on entry).
For example, in JML writing \old(*p) < *p in a method’s postcondition indicates
the value stored in *p is increased by the method. Whiley supports similar syntax as the
following illustrates:
method swap(&int p, &int q)
ensures *p == old(*q) && *q == old(*p):
int tmp=*p
*p=*q
*q = tmp
This simple method swaps the values referred to by pand q, and to specify it, we had
to use the old() syntax. With the above specification for swap(&int,&int) we can
verify, for example, the following snippet:
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 755
...
&int x=new 2
&int y=new 123
&int z=new 234
swap(x,y)
// Check expected outcome
assert (*x == 123) && (*y == 2)
// Check z unchanged
assert (*z == 234)
Here, the first assert follows from the specification of swap(&int,&int) .In
contrast, the second follows because the state referred to by zis not reachable from any
parameter passed to swap(&int,&int) and, hence, could not be modified by it.
2.2 Boogie
Boogie [14] is an intermediate verification language developed by Microsoft Research as part
of the Spec# project [16]. Boogie is intended as a back end for other programming language
and verification systems [106], and has found use in various tools, such as Dafny [104],
VCC [45], and others (e.g. [25]). Boogie is both a specification language (which shares
some similarity with Dijkstra’s language of guarded commands [57]) and a tool for checking
that Boogie “programs” are correct. The original Boogie language was “somewhat like a
high-level assembly language in that the control flow is unstructured but the notions of stat-
ically scoped locals and procedural abstraction are retained” [14]. However, later versions
support structured if and while statements to improve readability. Nevertheless, a non-
deterministic goto statement is retained for encoding arbitrary control flow, which permits
multiple target labels with non-deterministic choice. Boogie provides various primitive types
including bool ,int and map types, which can be used to model arrays and records.
Concepts such as a “program heap” can also be modelled using a map from references to
values.
Boogie supports function and procedure declarations which have an impor-
tant distinction. In general, functions are pure and can be used within the Boogie logic,
such as in axioms and specifications. In contrast, procedures are potentially impure and are
intended to model methods in the source language. A procedure can be given a specifica-
tion composed of requires and ensures clauses, and also a modifies clause
indicating non-local state that can be modified. Most importantly, a procedure can be given
an implementation , and the tool will attempt to ensure this implementation meets the
given specification. The requires and ensures for procedures demarcate proof obli-
gations, for which Boogie emits verification conditions in first-order logic to be discharged
by Z3. In addition, the implementation of a procedure may include assert and assume
statements. The former lead to proof obligations, whilst the latter give properties which the
underlying theorem prover can exploit.
To illustrate Boogie, Figure 2provides an example encoding of the indexOf() function
into Boogie. Note that the example encodings used in this section are a little different to the
more sophisticated encoding used later in the paper. At first glance, it is perhaps surprising
how close to an actual programming language Boogie has become. Various features of the
language are demonstrated with this example. Firstly, an array length operator is encoded
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
756 D. J. Pearce et al.
Fig. 2 Simple Boogie program encoding an implementation of the indexOf() function, making extensive
use of the structured syntax provided in later versions of Boogie
using an uninterpreted function len() , and accompanying axiom . Secondly, the input
array is modelled using the map [int]int , which is a total mapping from arbitrary
integers to arbitrary integers. For example, xs[-1] identifies a valid element of the map
despite -1 not normally being a valid array index (e.g. in Whiley). We can refine this to
something closer to an array through additional constraints, as shown in the next section.
Whilst the structured form of Boogie is preferred, where possible, it is also useful to
consider the unstructured form, which we use for a few Whiley constructs such as switch
(Sect. 3.4.1). Figure 3provides an unstructured encoding of the indexOf() function from
Figure 2. In this version, the while loop is decomposed using a non-deterministic goto
statement—the goto LOOP_BODY, LOOP_EXIT statement allows flow of control to
jump to either label, but the assume statements after those labels block progress if their
condition is false. Likewise, in this unstructured encoding, the loop condition and invariant are
explicitly assumed (lines 8,9,12) and asserted (lines 15,16), rather than being done implicitly
by the tool (as in Figure 2). The havoc statement“assigns an arbitrary value to each
indicated variable” [14], so is used here to indicate that variable icontains an arbitrary
integer value at this point.
Finally, we note that Boogie allows one to designate preconditions, postconditions and
loop invariants as free . This allows Boogie to assume these conditions hold without
checking them—thereby (potentially) reducing overall verification time [103].
3 Modelling Whiley in Boogie
Our goal is to model as much of the Whiley language as possible in Boogie, so that we can
utilise Boogie for verifying Whiley programs. Indeed, the motivation for this project was the
hope that Boogie would offer significantly better verification capability than the existing (and
relatively ad hoc) native verifier used in Whiley (and, as Sect. 4shows, this is the case). At
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 757
Fig. 3 Unstructured encoding of the example from Figure 2—the pre-/postconditions are omitted as they are
unchanged from above, and likewise for len()
a superficial level, Whiley’s native verifier is not so different from Boogie/Z3. In particular,
it employs an intermediate assertion language in which verification conditions are encoded
and then discharged using a purpose-built SMT solver [139]. A key advantage is that the
generated verification conditions resemble the Whiley source language much more closely.
Nevertheless, whilst this toolchain has potential, it remains relatively immature compared
with Boogie/Z3 and the considerable resources invested in their development [16]. However,
this transition is not without challenges as, despite their obvious similarities, there remain
significant differences between Whiley and Boogie:
Typ es. Whiley has a relatively rich (structural) type system which includes: union,record,
array,reference and lambda types. Furthermore, there is support for type polymorphism
through generics.
Flow Typing. Whiley’s support for flow typing is also problematic, as a given variable
may have different types at different program points and there is a need to support runtime
type tests [133].
Functions. Whiley functions are defined via code bodies, whereas the body of a Boogie
function can contain only a single expression.
Methods. Whiley methods correspond quite well with procedures in Boogie, but may be
invoked from within expressions in Whiley.
Definedness. Whiley implicitly assumes specification elements (e.g. pre-/postconditions
and invariants) are well defined. This differs from other tools (e.g. Dafny) which require
programmers to explicitly ensure that specification elements are well defined.
To understand the definedness issue, consider a precondition that contains an array ref-
erence, like requires a[i] == 0 . In a language like Dafny, one would additionally
need to explicitly specify i>=0&&i<|a|to avoid the verifier reporting an out-of-
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
758 D. J. Pearce et al.
bounds error. Such preconditions are implicit in Whiley, so must be (automatically) extracted
by our translator and made explicit in the generated Boogie.
We now present the main contribution of this paper, namely a mechanism for translat-
ing Whiley programs into Boogie, which is implemented in our translator program, called
Wy2B.1
3.1 Types
Finding an appropriate representation of Whiley types is a challenge. We begin by considering
the straightforward (i.e. naive) shallow translation of Whiley types into Boogie, and highlight
why this fails. Then, we present a more sophisticated approach which corresponds more
closely with a deep embedding of types.
Shallow Embedding. The simple and obvious translation of Whiley types into Boogie would
be a direct translation to the built-in types of Boogie. Here, an int in Whiley is translated
into a Boogie int , which is appropriate since both languages support unbounded integers.
AWhileyarray(e.g. int[] ) then translates to a Boogie map (e.g. [int]int , with
appropriate constraints), and Whiley records can also be translated using Boogie’s map type.
However, by itself, this is not sufficient to model all Whiley types. For example, the type
int|null has no obvious corresponding representation in Boogie. Likewise, a Whiley
type test such as xisintrequires additional machinery. So this shallow embedding
where Whiley types are directly translated into Boogie types is insufficient.
Deep Embedding. To support the more complex types found in Whiley such as unions, we
provide a deep embedding of all types into Boogie.2Specifically, we model all Whiley values
as disjoint members of a single set, Any , and model the various Whiley types as subsets of
this:
type Any;
// The set of all Whiley values.
For each Whiley type T, we define a membership predicate T#is(Any) that holds for
values in T, an extraction function T#unbox(Any) that maps Any to a Boogie type, and
an injection function T#box(T) which does the reverse. We axiomatise these two functions
to define a partial bijection between a type’s representation and its corresponding subset of
Any . We also add Boogie axioms to ensure the subtypes of Any which correspond to each
built-in Whiley type ( int ,bool ,T[] , etc.) are mutually disjoint. This embedding has
several advantages. Firstly, it is easy to model a Whiley user-defined subtype Sby defining
a predicate S#is(v) as (T#is(v) && ...) . Secondly, union types simply map to
disjunctions of these type predicates. Thirdly, Boogie can prove equality of two Any values
only if they are constructed using the same T#box() injection function from values that
are equal.
Finally, to aid with the translation of compound types in Whiley (such as arrays—see
Sect. 3.1.2 below) a special constant, Void ,isused:
const unique Void : Any;
1See https://github.com/Whiley/Whiley2Boogie.
2An alternative (though untested) approach would be to utilise Boogie’s support for algebraic data types.
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 759
Observe that, since this value has (by design) no counterpart in Whiley, we must ensure
it remains disjoint from all other Whiley values.
3.1.1 Primitives
Integers. The mapping functions for the Whiley int type of unbounded integers are as
follows (recall int is also the Boogie name for integers).
function Int#is(Any v) returns (bool) {
(exists i:int :: Int#box(i) == v)
}
function Int#unbox(Any) returns (int);
function Int#box(int) returns (Any);
axiom (forall i:int :: Int#unbox(Int#box(i)) == i);
axiom (forall i:int :: Int#box(i) != Void);
Bits and Bytes. Whiley includes a native byte type which supports the usual plethora
of bitwise operators, including left- and right shifts. For this, Boogie provides a family of
bitvector types (e.g. bv8 ,bv16 , etc.) of which bv8 provides a suitable match. To use
this, however, we must exploit various internal functions to implement bitwise operators as
follows:
function Byte#box(bv8) returns (Any);
function Byte#unbox(Any) returns (bv8);
function Byte#is(v : Any) returns (bool) {
(exists b:bv8 :: Byte#box(b) == v)
}
function {:bvbuiltin "bv2int"} Byte#toInt(bv8)
returns (int);
function {:bvbuiltin "(_ int2bv 8)"} Byte#fromInt(int)
returns (bv8);
function {:bvbuiltin "bvnot"} Byte#Not(bv8)
returns (bv8);
function {:bvbuiltin "bvand"} Byte#And(bv8, bv8)
returns (bv8);
...
axiom (forall b:bv8 :: Byte#unbox(Byte#box(b)) == b);
axiom (forall b:bv8 :: Byte#box(b) != Void);
Coercions. In order to utilise our deep embedding, values must be coerced to / from primitive
Boogie types. Consider an assignment x=0 where xhas type int|null .Since
union types in Whiley are encoded as type Any in Boogie, we must coerce the value
0(of Boogie type int ) into its embedded form via Int#box() . Such an assign-
ment is thus translated as x := Int#Box(0); . In general, our translation attempts to
minimise the amount of boxing/unboxing. For example, generated expressions of the form
Int#Unbox(Int#Box(x)) are automatically reduced to x, etc. Amongst other things,
this helps to simplify debugging!
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
760 D. J. Pearce et al.
3.1.2 Arrays
Whiley arrays are fixed-length sequences of values whose length can be queried at runtime
(recall from Sect. 2.1.3 they have value semantics). We model Whiley arrays using: (1) a
Boogie map [int]Any from integers to Any values; and (2) an uninterpreted function
returning the length. The embedding requires a number of additional axioms, as follows. As
before, we provide extraction/injection functions as follows:
function Array#box([int]Any) returns (Any);
function Array#unbox(Any) returns ([int]Any);
function Array#is(v : Any) returns (bool) {
(exists a:[int]Any :: Array#box(a) == v)
}
axiom (forall i:[int]Any :: Array#unbox(Array#box(i))==i);
axiom (forall a:[int]Any :: Array#box(a) != Void);
// Helper constraining index to be in-bounds
function Array#in(a : [int]Any, i : int) returns (bool) {
(i >= 0) && (i < Array#Length(a))
}
A key aspect of our embedding is the treatment of indices which are out-of-bounds.The
primary issue is that Boogie maps (e.g. [int]Any ) are infinite structures with no concept
of bounds. Elements which have not been explicitly defined always exist with some arbitrary
value. This presents a problem for equality of arrays, as illustrated in Figure 4.Toresolve
this we fix all out-of-bounds indices to the special Void value, and enforce this throughout
the axioms that follow.
Array Length. We employ the following function for extracting the length of an array:
// Extraction for array length
function Array#Length([int]Any) returns (int);
// Length of an array is non-negative
axiom (forall a:[int]Any :: 0 <= Array#Length(a));
// Updates don’t affect array length
axiom (forall a:[int]Any,i:int,v:Any ::
(v != Void && Array#in(a,i))
==> (Array#Length(a) == Array#Length(a[i:=v])));
In the above, we take steps to ensure the axioms remain consistent. To understand this,
consider the last axiom above which holds the array length invariant across an update. The
value vbeing assigned cannot be Void as, otherwise, we could artificially reduce an
array’s length (e.g. by assigning Void to the last element). Finally, whilst our encoding
of arrays here may appear somewhat elaborate, it does allow us to exploit Boogie’s internal
notion of equality. An alternative, however, would be to define a bespoke equality operator
for arrays (though this is complicated by the presence of unions and recursive types).
Array Initialisers. Array values in Whiley can be constructed using the array literal syntax
(e.g. [0,4,3] , etc.). This creates an array containing the given values (zero-indexed). To
translate this we employ a constructor, Array#Empty(int) , as follows:
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 761
Fig. 4 A pictorial illustration of four arrays embedded using (infinite) Boogie maps, where undefined (i.e.
out-of-bounds) values are shown as “?”. We might expect the first and fourth arrays to be equal (i.e. since they
have the same length and values within bounds), but this depends also on whether the out-of-bounds values
are also equal. To ensure these two arrays are indeed equal, we fix these undefined values to some known
constant ( Void )
// Construct (empty) array literal of size n
function Array#Empty(int) returns ([int]Any);
// Fix out-of-bounds indices for array literal
axiom (forall l:int,i:int ::
(i<0||l<=i)==>(Array#Empty(l)[i] == Void));
// Fix in-bounds indices for array literal
axiom (forall l:int,i:int ::
(0 <= i && i < l) ==> (Array#Empty(l)[i] != Void));
// Array length must match length of array literal
axiom (forall a:[int]Any,l:int ::
(0 <= l && Array#Empty(l)==a) ==> (Array#Length(a)==l));
The intuition is that Array#Empty(n) constructs an uninitialised array of size n,
whose elements must then be initialised individually. For example, the array literal [6,3]
is translated into Array#Empty(2)[0:=Int#box(6)][1:=Int#box(3)] .
Array Generators. Array values can also be constructed using the array generator syntax,
[v;n] (recall Sect. 2.1.1). The constructor, Array#Generator(Any,int) ,isused
for translating these as follows:
function Array#Generator(Any, int) returns ([int]Any);
// Every element of array generator matches given value
axiom (forall v:Any,l:int,i:int ::
(0<=i && i<l && v!=Void) ==> Array#Generator(v,l)[i]==v);
// Fix out-of-bounds indices for array generator
axiom (forall v:Any,l:int,i:int ::
(i < 0 || l <= i) ==> (Array#Generator(v,l)[i] == Void));
// Array length must match length of array generator
axiom (forall a:[int]Any,v:Any,l:int ::
(0<=l && Array#Generator(v,l)==a) ==> Array#Length(a)==l);
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
762 D. J. Pearce et al.
3.1.3 Records
Records are encoded using maps, [Field]Any ,where Field characterises field names.
For every field name used within the program, a unique constant is created. For example, if
the type {int x, int y} is used then the following constants are generated:
type Field;
// Set of all field names
const unique $x : Field;
const unique $y : Field;
These constants are then used as indices for the map encoding of the record (and any other
record type containing a field xor y). The constants are marked unique to ensure they
are disjoint. Thus, the number of constants generated depends on exactly what types are used
within the target program. As for arrays, care must be taken when encoding a given record to
ensure that all other fields are mapped to Void . Again, various functions and axioms are
provided to allow records to be embedded within other compound types:
function Record#box([Field]Any) returns (Any);
function Record#unbox(Any) returns ([Field]Any);
function Record#is(v : Any) returns (bool) {
(exists r:[Field]Any :: Record#box(r) == v)
}
axiom (forall i:[Field]Any ::
Record#unbox(Record#box(i)) == i);
axiom (forall r:[Field]Any :: Record#box(r) != Void);
Like arrays, all fields not in a given record should hold Void . This cannot be enforced
with an axiom as it depends upon the record type in question (i.e. what fields it has). Instead,
this is enforced using constraints on parameters, returns and local variables as necessary.
Record Literals. As for arrays, a simple constructor is used for translating record literals:
const unique Record#Empty : [Field]Any;
// Every field in an empty record holds Void
axiom (forall f:Field :: Record#Empty[f] == Void);
As an example, the record literal {x:1,y:2} would be translated into Boogie as
Record#Empty[$x:=Int#box(1)][$y:=Int#box(2)] .
3.1.4 Generics
Type polymorphism in Whiley presents a number of challenges when translating to Boogie.
Roughly speaking, we translate generic types (e.g. T) into Boogie’s Any type. We will
return to discuss this in more detail later (see §3.4).
3.1.5 Lambdas
The ability to pass around first-class functions and methods as lambdas also presents some
challenges, since lambdas in Boogie are relatively restricted. We return to discuss this in more
detail later (see §3.4.1), but for now it suffices to introduce the following which represents
the set of all lambda values:
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 763
type Lambda;
// Set of all lambda values
function Lambda#box(Lambda) returns (Any);
function Lambda#unbox(Any) returns (Lambda);
function Lambda#is(v : Any) returns (bool) {
(exists l:Lambda :: Lambda#box(l) == v)
}
axiom (forall l:Lambda :: Lambda#unbox(Lambda#box(l))==l);
axiom (forall l:Lambda :: Lambda#box(l) != Void);
3.1.6 References
References in Whiley are modelled in a relative standard fashion as indexes into a heap
represented as a map of the form [Ref]Any [103]. Again, we return to discuss this in
more detail later (see Sect. 3.5), and for now, we simply introduce the Ref type:
type Ref;
// Set of all Whiley references
function Ref#box(Ref) returns (Any);
function Ref#unbox(Any) returns (Ref);
function Ref#is(v : Any) returns (bool) {
(exists r:Ref :: Ref#box(r) == v)
}
axiom (forall r:Ref :: Ref#unbox(Ref#box(r)) == r);
axiom (forall r:Ref :: Ref#box(r) != Void)
In addition, the following constant is provided for describing an arbitrary heap:
const unique Ref#Empty : [Ref]Any;
The above is useful in various situations where there is no logical heap (more on this
later). In particular, since it does not provide any guarantee about its contents, it cannot be
relied upon at all.
3.1.7 User-Defined Types
Our treatment of user-defined types follows naturally from our embedding of types discussed
above. Roughly speaking, we can consider that every user-defined type in Whiley consists of
two parts: firstly, its base or underlying type; secondly, its invariants (if any). For example,
consider the following Whiley declaration:
type nat is (int x) where x>=0
The underlying type of nat is int , and it enforces a single invariant x>=0
.In
our translation to Boogie, this declaration would produce the following:3
3In practice, name mangling is applied to ensure uniqueness across modules and packages in Whiley.
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
764 D. J. Pearce et al.
type nat = int;
function nat#inv(x : int, HEAP : [Ref]Any) returns (bool) {
x>=0
}
function nat#is(x : Any, HEAP : [Ref]Any) returns (bool) {
Int#is(x) && nat#inv(Int#unbox(x),HEAP)
}
This allows for several different use cases. For example, if we have a variable of type
nat and wish to assume or assert its invariant, then nat#inv() can be applied directly.
Alternatively, if we are reading such a variable from a boxed position (e.g. out of an array or
record), then nat#is() can be applied. Observe also that, for uniformity, such methods
always accept a HEAP parameter even if (as in this case) this is not used. This parameter
is necessary for user-defined types which are, or contain, references. For example, consider
this declaration which builds upon the definition of nat :
type pNat is (&nat p)
This describes the type of references to integer values which enforce the nat constraint.
This would be translated as follows:
type pNat = Ref;
function pNat#inv(p:Ref,HEAP:[Ref]Any) returns (bool){true}
function pNat#is(p:Any, HEAP:[Ref]Any) returns (bool) {
Ref#is(p) && nat#is(HEAP[Ref#unbox(p)],HEAP)
&& pNat#inv(Ref#unbox(p),HEAP)
}
Here, pNat#is() enforces nat#is() upon the element in HEAP referred to by p.
Thus it becomes clear that the embedding of a reference type only makes sense in the context
of a given HEAP .
3.2 Constants
Global constants in Whiley require care to ensure a safe translation. A well-known issue
with Boogie arises when specifications written by the user (i.e. in Whiley) are translated
into unguarded Boogie axioms. In such cases, the user can be considered as maliciously
injecting problematic (though rarely useful) code.4For example, a user can (perhaps acci-
dentally) insert axiom false; (or some equivalent thereof) into the generated Boogie
file. Unfortunately, the presence of such a declaration allows Boogie to immediately verify
all assertions in the file (i.e. regardless of whether they are correct or not) [102]. More impor-
tantly, Boogie does not report this as an error, and hence, it happens silently without the user
being made aware. To see how this applies to constants in Whiley, consider the following
(recall definition of nat from page 19):
4This is perhaps somewhat reminiscent of SQL injection attack, whereby a user submits arbitrary SQL (e.g.
through a form) which is executed on the server (e.g. because an input string was not escaped properly).
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 765
final natx=0
The challenge here is to ensure the value being assigned adheres to any type invariant(s)
required of x. One approach is to generate a typing axiom, such as axiom nat#is(x) ,
for this. Whilst this is sufficient for the above example, a problem arises if the value assigned
was -1 instead of 0. In such case, the translation leads to the following:
const x : int;
axiom x == -1;
axiom nat#is(x);
Unfortunately, these axioms conflict as they imply both x==-1
and x>=0
(which
is equivalent to axiom false ). To protected against this, we stratify our translation into
two levels: the first establishes global constants are correctly initialised, and the second ver-
ifies functions and methods assuming they are correctly initialised. Following the approach
takeninDafny[104], this is done using a special constant Context#Level . The follow-
ing illustrates the translation of our example above:
const Context#Level:int;
const x : nat;
axiom x==1;
axiom (Context#Level > 1) ==> nat#is(x);
procedure x#check()
requires Context#Level == 1;
{
assert nat#is(x);
}
The above verifies without trouble. However, were xto be initialised with -1 , Boo-
gie would now correctly report a failed proof obligation inside the x#check() method.
Furthermore, note that all procedure bodies generated from functions or methods in Whiley
require Context#Level > 1 to ensure access to x’s invariant (see Figure 6below).
3.3 Properties
Properties in Whiley are straightforward as they can be translated directly as Boogie functions.
For example, consider the following property in Whiley:
property above(int[] xs, int n)
where all {iin 0..|xs||n<xs[i] }
This is translated directly as follows (again name mangling would be applied in practice):
function above(HEAP : [Ref]Any, xs : [int]Any, n : int)
returns (bool) { (forall i:int ::
Array#in(xs,i) ==> n < Int#unbox(xs[i])
)}
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
766 D. J. Pearce et al.
Fig. 5 Illustrating a simple function in Whiley which, for brevity, has not been fully specified
As for types, properties always accept a HEAP parameter for uniformity even when
not needed. A key observation is that Boogie functions are strictly more expressive than
properties in Whiley, and we will return later to consider the impact of this (see Sect. 4.4).
3.4 Functions
Recall that functions in Whiley are pure, have bodies comprised of statement blocks and may
have multiple return values. This differs from functions in Boogie, whose bodies are made up
of a single expression and can only return a single value. This presents challenges: firstly, the
body of a Whiley function corresponds more closely with a Boogie procedure; but, secondly,
functions in Whiley can be called from specification elements (e.g. pre-/postconditions)
whereas Boogie procedures cannot. As such we provide a two-pronged translation (similar
to that found in Dafny [102]) comprising: a prototype implemented as a Boogie function
which can be invoked from a specification element; and a body, implemented as a Boogie
procedure, which can be invoked directly from the body of other functions or methods.
Figure 5illustrates a simple function written in Whiley which we adopt as a running
example, whilst the generated Boogie for this is shown in Figure 6. We will endeavour
to fully clarify all aspects of this figure over the coming pages, but for now we focus on
the procedure’s specification. Here, additional requires clauses are included to enforce
the type of xs and, likewise, additional ensures clauses for the type of rs . Whilst
the soundness assumption for constants was discussed above, we will return to discuss the
purpose of the function prototype and linkage later. We note also that, whilst functions in
Whiley cannot modify the heap, they can manipulate references as simple values (though
cannot mutate through them).
Generics. Since Whiley supports type polymorphism, we might like to upgrade our fill()
function as follows:
function fill<T>(T[] xs, T x) -> (T[] rs)
...
As discussed in Sect. 3.1.4, we translate the Whiley type Tas Boogie type Any .In
terms of verifying the above function in isolation, this presents no problems. However, in
most cases, call sites of this function would expect to receive an array of the same type they
put in. For example, consider this:
function zero(int[] xs) -> (int[] ys):
return fill<int>(xs,0)
In this case, Boogie must be able to determine that the return from fill() is an array
of integers. Thus, a mechanism is required to enable our translation to state meta properties
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 767
Fig. 6 Illustrating the generated Boogie code for the fill() example. Note that this is somewhat simplified
as various details related to name mangling and parameter shadowing are omitted
about the relationships between variable types (e.g. that they are the same). To do this, we
introduce meta types as follows:
type Type;
// Meta type test
function Type#is([Ref]Any, Type, Any) returns (bool);
Here, the Boogie type Type represents the set of all meta types, whilst Type#is()
performs a similar function as, for example, Int#is() (but for an arbitrary meta type).
In this way, we extend the generated procedure for fill<T>() as follows:
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
768 D. J. Pearce et al.
procedure fill(T:Type, xs:[int]Any, x:Any)
returns (rs:[int]Any);
// Elements in xs have type T
requires (forall i:int :: Array#in(xs,i)
==> Type#is(HEAP,T,xs[i]));
...
// Elements in rs have type T
ensures (forall i:int :: Array#in(rs,i)
==> Type#is(HEAP,T,rs[i]));
...
Here, we see the generic type Tis now passed as an argument to procedure fill()
and using this we can, for example, make statements about the return value. For example,
the postcondition now tells us at a given call site that all elements in the returned array have
the same type as those elements in the input array. To make this work, we still need one
additional piece. Specifically, for every type which can be used to instantiate a type variable
(e.g. int in fill<int>() ) we construct a unique meta type constant. For example,
the meta type constant for int is declared as follows:
// Int meta type
const unique Type#I : Type;
// Int meta axiom
axiom (forall HEAP:[Ref]Any,v:Any ::
Type#is(HEAP,Type#I,v) <==> Int#is(v));
Finally, we note that user-defined types must be extended to use meta types as well. For
example, consider the following:
type List<T> is (null | { List<T> next, T data } l)
The various Boogie support functions generated for this type (recall Sect. 3.1.7)mustnow
accept a meta type parameter. For example, List#is() is defined as:
function List#is(T:Type, l:Any, HEAP:[Ref]Any)
returns (bool) { ... }
Overloading and Parameters. Overloading on parameter types is supported in Whiley, but
not in Boogie. To resolve this, we employ name mangling for every property, function,
method and type. The latter is necessary because mangling also includes package and module
information. Likewise, parameters for Boogie procedures are immutable, whereas parameters
to functions or methods can be mutated in Whiley. To resolve this, our translator generates a
shadow variable for each parameter which is assigned the parameter’s value on entry.
Function Linkage. Since functions in Whiley can be called from specification elements, a
key question arises as to how such calls are encoded. Consider the following partial imple-
mentation of a stack:
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 769
type Stack is {int[] items, nat len} where len < |items|
function size(Stack b) -> (nat r):
return b.len
function top(Stack s) -> (int v) requires size(s) > 0:
return s.items[s.len]
Here, the ... postcondition of top() uses other publicly visible functions to hide the
implementation of Stack .5Our translation of top() looks roughly as follows:
procedure top(s : Stack) returns (v : int);
...
requires size(HEAP,s) > 0;
...
The key here is that size(HEAP,s) refers to the function prototype of size() ,
rather than its procedure. Furthermore, since size(HEAP,s) == r isensuredinthe
postcondition of procedure size() , we can verify statements such as the following:
Stacks=...
if size(s) > 0:
return top(s)
Partial Correctness. An important limitation of Whiley is that it cannot ensure termination.
For example, there is no equivalent syntax to decreasing as found in Dafny. As a
result, non-terminating recursive functions can be verified with almost any postcondition.
The following illustrates such an example:
function inc(int x) -> (int y) ensures y>x:
return inc(x)
Observe that the above function will never violate its postcondition and, hence, is correct
up to non-termination. In the future, we expect Whiley to be extended with support for variant
expressions such that a well-founded ordering over recursive calls can be specified to ensure
termination.
3.4.1 Statements and Expressions
Translating most Whiley statements and expressions into Boogie is straightforward (see the
similarities between Figures 1and 2). Here, we describe only the interesting cases that present
specific challenges.
Variable Scoping.
Boogie requires all local variables to be declared at the start of a procedure body where,
like most modern languages, Whiley allows variables to be declared with block scopes.
Whilst, in most cases, this is relatively trivial to manage there are cases where name clashes
arise. The following illustrates:
5We note that Whiley supports a range of visibility modifiers for statically enforcing information hiding,
though these are beyond the scope of this paper.
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
770 D. J. Pearce et al.
type imsg_t is {int kind, int data}
type bmsg_t is {int kind, bool data}
function read(imsg_t|bmsg_t m) -> (int r):
if mis imsg_t:
int tmp = m.data
...
else:
bool tmp = m.data
...
In this case, the same variable is declared twice with different types. This is a problem
because they have incompatible types, and hence, we cannot declare a single Boogie variable
to cover both. Instead, we apply name mangling to ensure variables in different scopes have
unique names.
Well-Definedness. As highlighted already, Whiley’s treatment of expressions (especially
when used in specification elements such as pre-/postconditions) differs from other com-
parable systems (e.g. Dafny). In fact, handling this is straightforward and has been covered
reasonably extensively elsewhere [102]. Essentially, when translating a Whiley expression,
care must be taken to insert checks as necessary to ensure expressions are well defined. The
following illustrates a simple example:
...
int x = xs[i]
...
Here, there is an implicit assumption that i>=0
and i < |xs| . Of course, this may
not actually be the case and we employ assert statements to check such preconditions.
As such, the above is translated roughly as follows:
...
assert 0<=i;
assert i < Array#Length(xs);
x := Int#unbox(xs[i]);
...
Whilst, in many cases, the extraction of such checks is straightforward there are some
challenges. For example, we employed window inference [148] here. To understand this,
consider the following:
...
if i < |xs| && xs[i] == w:
...
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 771
For this example, the following translation is not sufficient:
...
assert (0 <= i);
assert (i < Array#Length(xs));
if(i < Array#Length(xs) && Int#unbox(xs[i]) == w) {
...
This translation is invalid because the second assert may not hold. This arises because
this definedness check is for part of the condition in a given context. Instead, for every check
extracted, we must additionally extract facts which havebecome known within the expression.
Thus our translation, in fact, is as follows:
...
assert (i < Array#Length(xs)) ==> (0 <= i);
assert (i < Array#Length(xs)) ==> (i < Array#Length(xs));
if(i < Array#Length(xs) && Int#unbox(xs[i]) == w) {
...
Another aspect of this issue is the well-definedness of specification elements, such as
pre-/postconditions and loop invariants. Consider the following (albeit contrived) example:
function read(int i, int[] map) -> (int r)
requires map[i] >= 0:
...
Since the precondition for this function requires facts about map[i] , it follows (implic-
itly) that map[i] must be well defined (i.e. that iis within bounds). Thus, our translator
extracts such additional requirements as necessary, as the following illustrates:
procedure read(i : int, map : [int]Any) returns (r : int);
...
requires (0 <= i) && (i < Array#Length(map));
requires Int#unbox(map[i]) >= 0;
...
A similar approach is taken to handling loop invariants and, perhaps surprisingly, also for
postconditions. For example, consider the following (albeit also contrived) example:
function create(int n) -> (int[] xs)
ensures xs[0] == n:
...
In this case, it follows from the postcondition that |xs| > 0 holds and, hence, is
translated as follows:
procedure create(n : int) returns (xs : [int]Any);
...
ensures (0 <= 0) && (0 < Array#Length(xs));
ensures Int#unbox(xs[0]) == n;
...
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
772 D. J. Pearce et al.
Finally, we note that care must be taken in a number of contexts when extracting well-
definedness conditions, such as for expressions nested within quantifiers.
Type Invariants. Our translation must ensure type invariants are properly preserved at all
points. For example, consider the following (recall definition of nat from page 19):
...
natx=1
...
x=y+1
In this case, we must establish that x>=0holds after xis initialised, and also after
it is subsequently reassigned. To do this, the above is translated as follows:
...
var x : nat;
x:=1;
assert nat#is(Int#box(x),HEAP);
...
x:=y+1;
assert nat#is(Int#box(x),HEAP);
Here the Boogie function nat#is() encapsulates the invariant for nat and is gen-
erated where translating the type declaration (recall Sect. 3.1.7).
Looking at Figure 6provides further insight into this process. No assertion for invariant
preservation is generated for i:=i+1 because the type of variable iis uncon-
strained. In other words, since the check would correspond to assert true; we simply
optimise it away. However, such optimisation remains relatively simplistic, as checks are still
produced unnecessarily for the xs := xs[i:=Int#box(x)] assignment.
Invocation. Translating function invocations into Boogie presents something of a challenge,
since functions can be invoked from arbitrary expressions (including specification elements
discussed previously in §3.4). However, Boogie does not permit procedure invocations
from within an expression, and provides only a simple statement form for calling procedures
(e.g. call x := f(y); ).6In short, this means function invocations must be extracted
from expressions. Consider the following snippet in Whiley:
int y = f(x) + 1
The above is translated into the following Boogie sequence:
call f#114 := f(x);
y := f#114 + 1;
Here a temporary variable, f#144 , is introduced to hold the value returned from f(x) .
Thus, the order of evaluation for expressions is exposed by the order in which the calls
are made prior to the final expression. In general, this approach works fine, but there are
challenges. Short-circuit semantics presents the first challenge. For example, consider the
following:
6Presumably, this is because Boogie wants to remain agnostic regarding execution order of expressions.
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 773
if (x < 0) || (f(x) > 0):
...
In this case, we cannot just extract the function invocation and execute it before the
if statement. Such a translation would model f(x) being executed every time the if
statement is executed, which is not the case. Instead, we must carefully preserve short circuit
semantics using unstructured branching as necessary. For example, we can translate the above
as follows:
if(x<0){goto trueLab; }
call f#114 := f(x);
if(f#114 <= 0) { goto falseLab; }
trueLab:
...
falseLab:
We can see that, whilst this gives a faithful rendition of the original program, it is quite
low level and harder to comprehend. This issue is further compounded with loops, whose
unstructured representation is far more verbose (recall Figure 2versus Figure 3).
Finally, we note our approach above is reminiscent of that used for Spec# [113] but differs
from Dafny (because Dafny does not permit method calls within expressions).
Assignments. Boogie supports assignments to variables (e.g. x:=y;
)andmapele-
ments (e.g. xs[0] := 0; ). Unfortunately, our choice to represent arrays uniformly with
Boogie type [int]Any presents some minor challenges. For a Whiley variable xs of
type int[] , we could translate xs[i] = 0 directly as xs[i] := Int#box(0); .
However, for a Whiley variable ys of type int[][] a direct translation fails because
the Boogie type for ys is still [int]Any (i.e. not [int][int]Any as needed for
a direct translation). For simplicity, we translate array assignments uniformly regardless of
the nesting level. For example, consider the following:
...
xs[i] = 0
As seen in Figure 6, the above is translated using Boogie’s m[e->v] operator as follows:
xs := xs[i:=Int#box(0)];
A similar approach is needed for assignments to records and to the heap via references.
A slightly more challenging issue arises from multiple assignments in Whiley. These have
interesting semantics from a verification perspective. Consider this example:
type Point is {int x, int y}where x<y||x>y
function swap(Point p) -> (Point r):
p.x,p.y = p.y,p.x
return p
The semantics of multiple assignments mean that the type invariant of pmust hold
after the assignment (hence the above correctly preserves its invariant). Observe, however,
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
774 D. J. Pearce et al.
that attempting to assign each field individually would give a verification error, as the type
invariant for pwould be temporarily broken. Thus, our translation of the above would be:
...
t#0 := Int#unbox(p$97[$y]);
t#1 := Int#unbox(p$97[$x]);
p := p[$x:=Int#box(t#0)];
p := p[$y:=Int#box(t#1)];
assert Point#is(Record#box(p),HEAP);
...
Notice that the values of p.y and p.x are first stored in temporary variables to avoid
interference between the left- and right-hand sides.
Another important aspect of multiple assignments is the semantics for conflicting assign-
ments [75,76]. The following illustrates:
xs[i],xs[j] = 0,1
They key question is what value is assigned to xs[i] when i==j . We follow Gries
by resolving this based on the order of the right-hand side. Thus, when i==j above,
xs[i] == 1 holds after the assignment since xs[i] is first assigned 0then 1.This
differs from Dafny where the above would be rejected unless i!=j was known.
Switches. Like many languages, Whiley supports multi-way branching via switch state-
ments. Although Boogie has no switch statement, it does support non-deterministic goto .
Hence, rather than using a sequence of if–else statements, we exploit this with appropriate
constraints. The following illustrates:
switch c:
case 0,1:
...
default:
...
...
goto l1,l2;
l1:
assume (c == 0) || (c == 1);
...
goto l3;
l2:
assume (c != 0) && (c != 1);
...
l3:
Here, l1 corresponds with case 0,1 whilst l2 corresponds with the default case.
Note also that cases do not fall through by default in Whiley. Furthermore, if there are nested
break /continue statements these are translated into goto saswell.
Loops. Loops are also relatively easy to translate. Since Boogie supports only while loops,
all other looping forms found in Whiley must be translated using this. Furthermore, since
Boogie has no break or continue statement, we translate these using goto sasfor
switch statements. We note also that, for a do–while loop in Whiley, the loop invariant
need not hold before the first iteration (which makes some proofs easier). Furthermore (if
desired) one can always check the invariant on entry using an explicit assert statement.
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 775
One challenge faced in translating loops is the handling of types for variables which
are modified in a loop. For example, in our translation of fill() our translator inserted
additional loop invariants to preserve the type of variable xs (recall Figure 6). This is
necessary because the postcondition for fill() restates that rs is an array of integers
and this is not expressed explicitly in the type [int]Any . Indeed, this is stated for xs
in the function’s precondition but, since xs is modified in the loop, this information is lost
within and after the loop (because Boogie sends its value to havoc). To resolve this, we must
reassert this type information as a loop invariant. Furthermore, this is done for any variables
modified in the loop.
A related issue, which our translator does not currently address, is that of preserving
immutable properties of variables. Consider again the fill() example from Figure 5.In
fact, this example does not verify as is with our translator! Again, key information about xs
is lost within and after the loop. In this case, the information that needs to be preserved is
that the length of xs is unchanged by the loop. In principle, our translator could be extended
with a static analysis to infer this and add it implicitly as a loop invariant (but this remains
future work). We note that this extends to records, as the following illustrates:
type Buffer is {nat len, int[] items} where len < |items|
function clean(Buffer b) -> (Buffer r)
// Buffer is emptied!
ensures (r.len == 0):
b.len = 0
for iin 0..|b.items|:
b.items[i] = 0
return b
Perhaps surprisingly, this also does not verify because the property b.len == 0 is
not preserved across the loop. This can be fixed by performing the assignment to b.len
after the loop. Or, we could add a loop invariant to ensure b.len == 0 is preserved.
Lambdas. Boogie provides syntax (e.g. (lambda y:int :: y + 1) ) for lambdas
(with map type [int]int in this case). They are comparable with Boogie functions
and cannot, for example, call procedures, etc. As such, they are insufficient for representing
lambdas in Whiley which can have side effects. Instead, we translate them into named Boogie
procedures. Mostly this is straightforward, but a few challenges arise with captured variables.
For example, consider the following:
type Pred<T> is function(T)->(bool)
function isBelow(int n) -> Pred<int>:
return &(int v->v<n)
Translating the lambda into a standalone procedure requires identifying captured
variables ( nin this case) and adding them as parameters. The following illustrates:
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
776 D. J. Pearce et al.
procedure lambda#131(HEAP : [Ref]Any, v : int, n : int)
returns (r : bool);
{
...
}
const unique lambda#131 : Lambda;
Here, the procedure contains the body of the lambda, which will include any necessary
checks on the lambda itself. Likewise, the constant lambda#131 is generated to represent
this particular lambda. When translating an indirect invocation, we automatically generate a
suitable prototype to invoke. For example:
Pred<int> fn = ...
bool b = fn(10)
The above Whiley snippet is then translated (roughly speaking) as follows:
fn := ...;
assert Pred#is(Type#L,Lambda#box(fn),HEAP);
b := Bool#unbox(f_apply(fn,Int#box(10)));
Here, the function f_apply() is generated (in practice, with a suitable mangling) to
represent the anonymous function being invoked. It accepts the lambda as a parameter, thus
allowing one to exploit the fact that the same lambda returns the same value(s) when given the
same parameter. Finally, we note that work remains to improve our translation of lambdas.
In particular, information known about captured variables is not currently transferred to the
generated procedure . Thus, the following fails to verify:
function isBelow(int[] xs, int i) -> Pred<int>
// index i within bounds
requires i >= 0 && i < xs[i]:
// Return lambda
return &(int v -> v < xs[i])
the generated procedure accepts the captured variables iand xs , but does not
include a corresponding precondition. Whilst, in this case, it would be relatively easy to fix,
in other cases it is more challenging (e.g. when a parameter has been modified prior to being
captured). One approach, for example, would be to apply the Weakest Precondition trans-
former [17,57,101] to the body of the lambda (which should be relatively straightforward
since this is just an expression).
3.5 Methods and Framing
Recall from Sect. 2.1.4 that methods in Whiley are permitted to have side effects and, for
example, manipulate heap-allocated data through references. As such, Whiley methods cor-
respond closely with procedures in Boogie. However, methods in Whiley can be called
from expressions used in statements (though not from specification elements, such as pre-
/postconditions or loop invariants). In many ways, the translation of methods follows that for
functions, but with some important differences which we now consider.
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 777
Framing. Whilst the Whiley language provides relatively limited support for describing the
effect a method has on the heap, a lot of machinery is nevertheless required to manage what
can be expressed. As highlighted before, we adopt a relatively standard approach to modelling
the heap. Specifically, a global variable HEAP of type [Ref]Any is provided to model
this. For example, consider the following Whiley method:
method swap(&int p, &int q)
ensures *p == old(*q) && *q == old(*p):
...
Our translation produces both a procedure prototype and implementation in Boogie. The
prototype for the above method looks roughly as follows:
procedure swap(p : Ref, q : Ref);
// Heap may be modified
modifies HEAP;
// Incoming typing constraints
requires Int#is(HEAP[p]) && Int#is(HEAP[q]);
// Postcondition
ensures Int#unbox(HEAP[p]) == old(Int#unbox(HEAP[q]))
&& Int#unbox(HEAP[q]) == old(Int#unbox(HEAP[p]));
// Outgoing typing guarantees
ensures Int#is(HEAP[p]) && Int#is(HEAP[q]);
// Frame condition (i)
free ensures ...
// Frame condition (ii)
free ensures ...
Observe that the modifies clause is provided as we must conservatively assume
methods may modify the heap. Note also that old() in Whiley is translated directly using
Boogie’s old() syntax. There are two essential issues here: typing and framing.The
former simply makes explicit guarantees on the shape of the heap provided by Whiley’s type
system. For example, that for an integer reference pthere is indeed an integer value at
HEAP[p] , etc. The latter aspect of framing is perhaps more interesting. We divide framing
into two separate conditions (both of which are marked free since Whiley’s type system
guarantees them). These conditions rely on a simple predicate for determining whether a
reference is reachable from—or within—the frame of a given variable (see Figure 7).
The first frame condition enforces self-framing [92] by ensuring that only locations within
the method’s frame can be modified:
...
// Frame Condition I
free ensures (forall r:Ref ::
Ref#within(HEAP,r,p) || Ref#within(HEAP,r,q)
// (1)
|| (old(HEAP[r]) == HEAP[r])
// (2)
|| (old(HEAP[r]) == Void));
// (3)
...
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
778 D. J. Pearce et al.
Fig. 7 Illustrating the Boogie definition of a predicate for determining whether a reference pis within the
footprint of given variable q. More specifically, it searches the contents of q(whatever that might be)
looking for p, whilst traversing references as necessary
There are three parts of the condition as follows:
1. (Mutable) This identifies which locations could be modified by the method and, for these,
does not provide a connection between the heap beforehand with that after.
2. (Immutable) For locations which could not be modified by the method, an explicit con-
nection is made to ensure this between the heap beforehand and that after.
3. (Allocated) As a special case, heap locations which did not exist prior to the method (i.e.
were mapped to Void ) can have arbitrary values afterwards.
In essence, the footprint of a method (i.e. those locations it could write) is conservatively
tied with its frame (i.e. those locations it could read). This provides a straightforward and
extensible basis for reasoning about how methods modify the heap. For example, if syntax
for describing the old heap in postconditions was added to Whiley, this would easily layer on
top. The key is that, in the absence of more expressive syntax for restricting the locations a
method may modify, we must adopt a worst-case assumption that any reachable location
could be modified.
The second frame condition (known as the swinging pivots restriction [92]) prevents
unreachable locations from “migrating” into the frame:
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 779
...
// Frame Condition II (Swinging Pivots)
free ensures (forall r:Ref ::
// any reference r in postframe
(Ref#within(HEAP,r,p) || Ref#within(HEAP,r,q)) ==>
// (1) was either freshtly allocated, or
(old(HEAP[r]) == Void ||
// (2) was reachable from the preframe
Ref#within(old(HEAP),r,p) || Ref#within(old(HEAP),r,q)));
...
In essence, this ensures that any reference reachable from parameters por qafter
the method was either freshly allocated, or was reachable from them beforehand. Note that,
whilst for this particular method, these conditions are trivial they are required in general (e.g.
for handling linked structures).
As a further example to illustrate the challenges addressed by the frame conditions, con-
sider the following:
type LinkedList is null |&{int data, LinkedList next }
method clear(LinkedList l):
l->data = 0
method main():
LinkedList l1 = new {data:1, next:null}
LinkedList l2 = new {data:2, next:null}
// Clear first node of l1 twice!
clear(l1)
clear(l1)
// Check l2 unaffected
assert l2->data == 2
Establishing that l2 is not modified by the calls to clear(l1) above requires both
frame conditions (something which is not immediately obvious at first glance). It is clear that
the first frame condition (self-framing) allows us to establish that l2 is not modified by the
first call. One might then conclude the first condition is sufficient to establish this across both
calls—but that is not the case! The challenge is that clear(l1) ensures l2 is not mod-
ified, but allows l1 to be modified. Without the second frame condition, the verifier might
then consider that l2 was within l1 after the first call (e.g. that l1->next == l2 ).
And, in such case, it would then rightly conclude that l2 could be modified by the second
call. As such, we see how the second frame condition helps to ensure that disjoint frames
remain disjoint.
Finally, we note that our encoding makes heavy use of a recursive predicate (see Figure 7)
which (as we have observed) can lead to the butterfly effect [111]. That is, where the verifier
loops indefinitely unrolling predicates fruitlessly. In our experience, this typically happens
when the condition being checked is invalid, and hence, the verifier cannot quickly find a
proof by contradiction.
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
780 D. J. Pearce et al.
Allocation. Since data can be allocated on the heap in Whiley methods using the new
operator, a translation of this operator is required. To this end, we employ the following:
procedure Ref#new(val : Any) returns (ref : Ref);
modifies HEAP;
// Location not previously allocated
ensures old(HEAP[ref]) == Void;
// Location now holds given value
ensures HEAP[ref] == val;
// Everything else untouched
ensures (forall r:Ref :: ref==r || old(HEAP[r])==HEAP[r]);
This simply returns an arbitrary location which was not previously allocated, and ensures
it now holds the requested value. Recall that, at the time of writing, Whiley does not support
explicit memory deallocation, and hence, no counterpart for this is required. Finally we note
that, since allocations result in calls to Ref#new , they must be extracted from expressions
as for method invocations above.
4 Experimental Results
In this section, we compare our Wy2B translator against the Whiley native verifier using the
existing compiler test suite which consists of 1100+(mostly) small Whiley programs. In
particular, we are concerned with the number of tests that Wy2B can pass correctly, and note
that the existing Whiley native verifier does not pass all the tests (e.g. because of outstanding
bugs, etc.). In addition, we discuss our experiences using the new Wy2B toolchain on several
larger case studies.
4.1 Micro-test Statistics
The Whiley compiler system includes a comprehensive suite of “micro”-test programs, which
are small Whiley programs intended to methodically test all Whiley language features, includ-
ing the Whiley native verifier. At the time of this evaluation (May 2021), this test suite included
731 “valid” micro-test case programs that should be verifiable, as well as 461 “invalid” micro-
test case programs that should generate compiler errors or verification failures (to ensure that
the compiler correctly catches them). Our first step in evaluating the correctness and useful-
ness of our new verifier is to apply it to this test suite. We use Boogie v2.8.26 and Z3 v4.8.10
for these evaluations.
When we applied our new Wy2B verifier to the invalid programs, ignoring 7 programs
that are marked as IGNORE due to current limitations of the compiler front end, we found
that all 454 of the remaining programs failed as expected. This confirms that the Boogie back
end is correctly detecting verification issues in programs that should not be verifiable. For
completeness, we illustrate one such example:
function f(int[] xs) -> int[]:
xs[0] = 1
return xs
The above “invalid” program is used to test that the verifier correctly reports a potential
out-of-bounds access on line 2. Both the native verifier and our Wy2B verifier pass this test.
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 781
Fig. 8 Stacked bar chart of the Whiley native verifier and Boogie-based verifier results on the “valid” test
programs. Green (left and middle bars) indicates percentage of programs fully verified, and red (right) indicates
percentage where one or more proofs failed or timed out
The valid micro-test programs are small Whiley programs (ranging from 3 to 250 lines of
code with an average length of 18 lines) that each contain several (2.2 on average) function
and method definitions, some with specifications and some without. Around one third of
the programs have functions or methods with requires/ensures specifications, one third use
arrays (which generate array bound proof obligations), and 21% have loops with invariants.
On average, our Wy2B translator generates 6.0 explicit proof obligations per micro-test
program (to check array bounds, function call preconditions, etc.). This is on top of any
explicit assert statements in the Whiley program and also in addition to the main proof
obligations of Boogie, which are that each function or method body correctly implements its
specification, and that every loop invariant is correctly preserved. Again, for completeness
we illustrate one such example:
function f(int[] xs) -> (int r)
requires xs[0] >= 0
ensures r>=0:
return xs[0]
public export method test():
int[] xs = [1,2,3]
int x = f(xs)
assert x>=0
The above “valid” program is expected to pass verification without raising any errors.
This means that, amongst other things, the verifier must prove that the body of fsatisfies
its specification, and within test must establish the precondition for the call f(xs) and
that the final assert holds. Again, both the native verifier and our Wy2B verifier pass this
test.
Figure 8compares the percentages of these “valid” micro-tests that the native Whiley
verifier and the Wy2B Boogie-based verifier can verify respectively. The leftmost bar on each
row corresponds to the programs that both verifiers can verify (604 programs, or 82.6%).
The middle bars show that the Whiley native verifier can verify an extra 7 programs (1.0%),
whereas the Boogie verifier can verify an additional 102 programs (14.0%). So in total, the
Whiley native verifier can verify 83.6% of the programs, whilst the Boogie verifier can verify
a total of 96.6%.
We investigated the 7 programs that the Whiley native verifier could verify but Boogie
could not, and found that 4 of them are verifiable by a later version of Boogie (v2.9.6.0)
and Z3 (v4.8.12). The remaining three are due to outstanding issues with the translation to
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
782 D. J. Pearce et al.
Boogie related to lambda functions that return union types (Issue #59 in the Whiley2Boogie
repository) and to proving the type invariants of cyclic data structures (Issue #61).
The larger number of programs that are verifiable by Boogie but not by the Whiley native
verifier are largely because there are several Whiley language features that are not supported
by the Whiley native verifier, such as:
heap updates;
reasoning about the results of calls to lambda functions;
some kinds of generic types.
The Wy2B+Boogie toolchain takes 15:30 minutes (930 seconds) to translate and verify
just the 706 test programs that it can verify, on a Dell Precision 5520 laptop with an Intel
i7-7820HQ CPU @ 2.90GHz and 32Gb RAM, and a 60 second timeout for Boogie. This is
1.3 seconds on average for each small valid test program, which is acceptable performance
for real-world usage. When run on all 731 programs with a timeout of 60 seconds, the whole
test run takes around 17:55 minutes, because some of the more difficult programs hit the 60
second timeout and fail. This is around 1.5 seconds average for each test, with a maximum
of 60 seconds for those that time out, which is still reasonable.
Another interesting performance issue is that we run Boogie with the -useArrayTheory
flag by default—this uses the built-in SMT theory of arrays within Z3, which handles large
arrays better, usually gives better performance, and enables more programs to be verified
(without this flag, Boogie can verify only 665/731 = 91% of the valid test suite). However,
there are a few programs (e.g. While_Valid_71.whiley) where performance becomes dra-
matically worse with this flag—it takes 4.5 minutes to report 5 unverifiable proof obligations
with the flag, but less than one second to finish and report 7 unverifiable proof obligations
without the flag.
The Whiley native verifier takes only four minutes to process the 600+ test programs
that it can verify (around 2.5 programs/sec), which is significantly faster than the Boogie
verifier, but takes 18:32 minutes to process all the 731 valid tests (around 1.5 secs/test on
average). However, it is difficult to compare the actual proof times, because the Whiley
verifier runs within a single Java JVM process, whereas the Wy2B+Boogie toolchain creates
several separate processes and intermediate files for each test program.
4.2 Case Study: Conway Game of Life
The first case study we discuss is an interactive web page for playing the Game of Life by
Conway [70]. This consists of a small index.html file to load the game, plus three Whiley
modules:
model.whiley (141 lines): defines the 2D board and the logic of the game;
view.whiley (26 lines): defines how to draw the board onto an HTML canvas;
main.whiley (87 lines): defines mouse event handlers and other controller methods.
The Whiley compiler compiles these three modules and generates JavaScript as output, which
can then run in a standard web browser (see Figure 9). We focussed on verifying just the
model component, since the others are just the view and controller components whose correct
functioning is generally obvious by the visual updates of the canvas. We aimed to specify
and verify as much of the functional behaviour of the model as possible, to try to explore the
limits of the Boogie verification path. Figure 10 shows the main data structure that represents
the board, plus a Whiley function that counts the number of neighbouring cells that are alive.
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
Verifying Whiley Programs with Boogie 783
Fig. 9 Illustrating the web-based implementation of Conway’s Game of Life developed in Whiley
Appendix A gives the full listing of model.whiley plus links to the corresponding
output Boogie code.
In addition to adding specifications to model.whiley, we made some small changes
to the code to make specification or verification easier:
The original board init function took width and height inputs as arbitrary
pixel sizes, but we changed these to be cell counts rather than pixels (since the size in
pixels is just a GUI display issue) and required them to be greater than zero to avoid
empty board cases that are not interesting in practice;
We moved the cell-update code out of a doubly nested loop into a separate function, for
better modularity and easier specification;
Whiley currently supports only one-dimensional arrays, so the code implemented the
2D board as a one-dimensional array, where each (x,y)location was translated into
an index x + y*state.width . We respected this data representation choice,7but
initially had some difficulty with Boogie struggling to verify in-range assertions about
these indexes, due to the nonlinear multiplication ( state.width is initialised at the
start of each game, so is not a static constant). Frequently, Boogie would go into an infinite
loop trying to prove these assertions (or terminate with a timeout error if we set a time
limit). Eventually we found that upgrading Z3 from version 4.8.9 to 4.8.10 solved most
of these problems, and Boogie was then able to prove most of the required assertions,
or give a quick failure result for those it could not prove. Even then, we found that it
was sometimes necessary to try several different ways of specifying indexes and bounds
before finding one that Boogie could verify. For example, it was much easier to verify the
count_living(...) function when it took a single index parameter rather than
separate xand yparameters—this is why in our final version the count_living
function re-derives the xand ycoordinates from the index. This meant that only
one variable needed to be quantified in the update loop invariant, instead of both the
7An alternative would be to use an array of arrays, but we did not explore this option as we wanted to leave
the code relatively unchanged.
123
Content courtesy of Springer Nature, terms of use apply. Rights reserved.
784 D. J. Pearce et al.
Fig. 10 Snippets from the Game of Life case study: the State data structure with its invariants, and the
count_living function that counts how many neighbouring cells are alive. As explained in the text, the index
input of count_living is given a cell location (x,y)as x+y*width . Note that uint is defined in the
standard library and has the same definition as nat (recall Figure 1)
xand ycoordinates. Typically, we found that cases where Boogie did not terminate
were due to array accesses that it couldn’t prove were within bounds, and that adding
redundant constraints to the specification to make it clear that they were in bounds would
fix that problem. This process was rather frustrating, but reflects