Counterexample-Guided Focus

Counterexample-Guided Focus
Andreas Podelski
Thomas Wies
University of Freiburg
[email protected]
EPFL and University of Freiburg
[email protected]
Abstract
The automated inference of quantified invariants is considered one
of the next challenges in software verification. The question of
the right precision-efficiency tradeoff for the corresponding program analyses here boils down to the question of the right treatment of disjunction below and above the universal quantifier. In
the closely related setting of shape analysis one uses the focus operator in order to adapt the treatment of disjunction (and thus the
efficiency-precision tradeoff) to the individual program statement.
One promising research direction is to design parameterized versions of the focus operator which allow the user to fine-tune the focus operator not only to the individual program statements but also
to the specific verification task. We carry this research direction one
step further. We fine-tune the focus operator to each individual step
of the analysis (for a specific verification task). This fine-tuning
must be done automatically. Our idea is to use counterexamples
for this purpose. We realize this idea in a tool that automatically
infers quantified invariants for the verification of a variety of heapmanipulating programs.
Categories and Subject Descriptors D.2.4 [Software Engineering]: Software/Program Verification; F.3.1 [Logics and Meaning
of Programs]: Specifying and Verifying and Reasoning about Programs; F.3.2 [Logics and Meaning of Programs]: Semantics of
Programming Languages–Program Analysis
General Terms
Algorithms, Languages, Reliability, Verification
Keywords Data Structures, Quantified Invariants, Predicate Abstraction, Abstraction Refinement, Shape Analysis
1. Introduction
There is a considerable interest in the automated verification of
correctness properties of programs that implement or use heapallocated data structures [6, 10, 19–21, 23, 24, 27, 29, 31, 34, 42,
43, 45, 48–50, 56, 57]. The correctness properties of such programs
typically include quantified assertions, e.g., assertions that describe
the expected shape of data structures such as “the reference count
for each internal object of the data structure is 1” and assertions
that describe the effect of data structure operations such as “all
objects stored in the data structure are properly initialized”. The
automated inference of quantified invariants for the verification of
quantified assertions is considered one of the next challenges in
software verification.
Permission to make digital or hard copies of all or part of this work for personal or
classroom use is granted without fee provided that copies are not made or distributed
for profit or commercial advantage and that copies bear this notice and the full citation
on the first page. To copy otherwise, to republish, to post on servers or to redistribute
to lists, requires prior specific permission and/or a fee.
POPL’10, January 17–23, 2010, Madrid, Spain.
c 2010 ACM 978-1-60558-479-9/10/01. . . $10.00
Copyright The eternal quest for the right precision/efficiency tradeoff in
the corresponding program analyses here boils down to the question of the right treatment of disjunction below and above the universal quantifier (specifically in the construction of the abstract
transformer). To achieve a reasonable average tradeoff between
precision and efficiency, existing program analyses follow a common recipe (see, e.g., [19, 42, 48]). The designer of the program
analysis starts off with a coarse but efficient generic abstract transformer and then manually adapts this abstract transformer to the
individual kinds of program statements such that disjunctions are
introduced prudently. In the closely related setting of shape analysis this adaptation is referred to as the focus operation [48]. If one
has to adapt the abstract transformer to individual program statements, uniformly for all possible uses of the analysis, the designer
of the program analysis is obliged to be very conservative with regard to the precision of the focus operator. As a consequence, the
analysis is often too inefficient.
It is therefore desirable to fine-tune the focus operator to a
specific use of the analysis. A promising research direction is to
design parameterized focus operators that allow the user of the
analysis to (manually) do this fine-tuning herself, for each new
verification task; the resulting gain of scalability is encouraging;
new classes of heap-manipulating programs, even concurrent ones,
have been successfully analyzed [39–41].
In this paper, we carry this research direction one step further
(and perhaps to its logical extreme). We propose to fine-tune the
focus operator not only to each individual problem instance, but
to each individual step of the analysis, i.e., each application of the
abstract transformer. This fine-tuning must be done automatically.
Our idea is to use counterexamples for this purpose. The contribution of this paper is to conceptually and practically realize the
idea and to demonstrate its interesting potential. We present a new
method and tool that automatically infers quantified invariants for
the verification of a variety of heap-manipulating programs.
The general idea of using counterexamples for refining an
abstraction stems from the classical scheme of counterexampleguided abstraction refinement (CEGAR) [3, 14, 25]. A number of
software verification tools based on the scheme, e.g., [3, 12, 26, 44],
are able to synthesize expressive invariants (though, not quantified
ones, so far). A spurious counterexample is an error trace that is
possible in the abstract but not in the concrete. Adding predicates
extracted from a spurious counterexample refines the abstract domain; in a subsequent step, the scheme re-defines the abstract transformer (as a function on the new abstract domain). In contrast, the
focus operator refines the effect of the abstract transformer without
changing the abstract domain and without re-defining the function.
We propose to use spurious counterexamples for both: for refining
the abstract domain, and for fine-tuning a focus operator that adapts
the effect of the abstract transformer.
More precisely, we transcend the idea of lazy abstraction [25],
a CEGAR scheme which adapts the abstract domain exactly to the
point of the execution of the analyzed program. We devise a nested
lazy abstraction refinement loop that adapts both, the abstract domain and the abstract transformer exactly to the point. The refinement loop consists of two nested refinement steps. The first step
refines the abstract domain by extracting new predicates from the
spurious error trace. If a spurious error trace is not eliminated by
merely refining the abstract domain then the second step uses the
spurious error trace to construct a focus operator that adapts the effect of the abstract transformer on the current abstract domain. This
adaptation can thus be performed locally and lazily, i.e., anew for
each single application of the abstract transformer.
In passing, let us note that our development of the counterexample-guided focus was originally motivated by the goal to establish the so-called progress property [25]. The property means
that every spurious counterexample encountered during the analysis is eventually eliminated by a refinement step. The property
is of foremost theoretical interest (a priori, it need not improve the
chances of convergence in practice since there may be always a new
spurious counterexample). In the setting of quantified invariants,
however, the theoretical interest of counterexample-guided focus is
in line with its practical relevance. The progress property holds (and
only holds) in the presence of counterexample-guided focus. The
practical verification does succeed with counterexample-guided focus and does not without; the reason is apparently that in many
benchmarks, a uniformly precise abstract transformer with feasible
cost comes with a too low precision.
Summary. To summarize, our work leverages the research on
the CEGAR scheme in software verification [3, 12, 14, 25] and
the research on the focus operator in shape analysis [39–41, 48].
Our contribution is to show that the techniques developed in these
two research directions can be fruitfully integrated to enhance one
another for the inference of quantified invariants:
• the focus operator can be made effective in a CEGAR setting
because it can be fine-tuned lazily and its locality can be driven
to the extreme,
• the CEGAR scheme can be made effective for quantified in-
variants because adding an inner refinement loop for the focus
operator provides the progress property and the precision required on practical examples (without the otherwise prohibitive
cost for a precise abstract transformer on an abstract domain for
quantified assertions).
2. Motivating Example
In this section, we will use an example to motivate the counterexample-guided focus and explain it in more detail. The program
I NIT shown in Fig. 1 initializes all entries in a singly-linked list.
The assert statement at location ℓ2 checks that all entries in the list
are indeed initialized after termination of the while loop. We would
like to automatically compute an inductive invariant for the loop
cutpoint at location ℓ1 that implies the safety of this assertion.
In the remainder of this section, we will first present the abstract domain, then the most precise abstract transformer (which
is too costly to implement), then a less costly abstract transformer
(which is too coarse), and finally the transformer obtained by the
counterexample-guided focus.
Abstract Domain: Boolean heaps. We use a variation of predicate abstraction [22] which we call Boolean heap abstraction [45].
Here we do not use state predicates (which can be defined by closed
first-order formulas as, e.g., in the assert statement at location ℓ2 ).
Instead, we use predicates that range over objects in the heap (and
which can be defined by formulas with free first-order variables).
These predicates are reminiscent of the predicates used in threevalued shape analysis [48] and indexed predicate abstraction [31].
Figure 2 shows three such predicates for Program I NIT . The predi-
ℓ0 :
ℓ1 :
ℓ2 :
y:= x
while y6=null do
y.data:= 0
y:= y.next
assert (∀v. (x, v) ∈ next ∗ ∧ v6=null → v.data=0)
Figure 1. Program I NIT
Cont = { v | (x, v) ∈ next ∗ ∧ v 6= null }
Iter = { v | (y, v) ∈ next ∗ ∧ v 6= null }
Init = { v | v.data = 0 }
Figure 2. Predicates for Program I NIT denoting sets of objects
cates range over objects in the heap. For notational convenience we
write predicates as sets. For instance, the predicate Cont denotes
the content of the list pointed to by x, i.e., the set of all non-null
objects that are reachable from x by following next pointers in the
heap. Data structure fields such as next and data are modeled as
function symbols. The binary relation next ∗ denotes the reflexive
transitive closure of the function next . Given a program state (i.e.,
a valuation of next and data as functions), the finite set of predicates induces a partition of the heap into finitely many equivalence
classes of heap objects.
The abstract domain of Boolean heaps consists of formulas that
describe such partitions. Each formula in the abstract domain is
a disjunction of universally quantified Boolean combinations of
the given predicates. The abstract domain is finite. We call the
outer disjuncts in these formulas abstract states. For instance, the
formula F given by
F ≡ ∀v. (v ∈ Iter → v ∈ Cont ) ∧
(v ∈ Cont → v ∈ Iter ∨ v ∈ Init)
is an abstract state for the predicates in Fig. 2. An abstract state is
a special case of an element of the abstract domain (a disjunction
with only one disjunct).
We use elements of the abstract domain to express inductive
invariants. For example, the formula F is an inductive invariant for
location ℓ1 in program I NIT and implies that the assert statement at
location ℓ2 does not fail.
Most Precise Abstract Transformer. For the purpose of this exposition, we represent the most precise abstract transformer using an
abstract program over abstract states; see Fig. 3. The abstraction
of the concrete transformer for each basic block in the concrete
program is translated to a statement in the abstract program. The
abstract program has a program variable for each of the given predicates (carrying the same name, e.g., Iter , Cont ). The program
variables in the abstract program range over sets of objects.
Figure 3 shows the Boolean heap abstraction of program I NIT
for the predicates given in Fig. 2. Here “*” stands for the nondeterministic choice of a Boolean value. The statement havoc x
stands for the nondeterministic assignment of program variable x.
In the example, for the sake of simplicity, we implicitly assume
that all lists are acyclic. We express the updates in the abstract program through logical formulas over unprimed and primed variables
(which, as usual, model the pre and the post value for the update).
For instance, the abstract transformer for the loop body of program
I NIT is given by
∀v. (v
(v
(v
(v
∈ Cont ↔ v ∈ Cont ′ ) ∧
∈ Init v ∈ Init ′ ) ∧
∈ Iter ′ v ∈ Iter ) ∧
∈ Init ′ ∧ v ∈
/ Init ↔ v ∈ Iter ∧ v ∈
/ Iter ′ ).
ℓ0 :
ℓ1 :
ℓ2 :
havoc Iter ′
assume Iter ′ = Cont
Iter := Iter ′
while ∗ do
havoc Iter ′ , Init ′
assume Init ⊆ Init ′
assume Iter ′ ⊆ Iter
assume Init ′ − Init = Iter − Iter ′
(Iter , Init):= (Iter ′ , Init ′ )
assume Iter =∅
assert Cont ⊆ Init
Figure 3. Program A BS I NIT : Boolean heap abstraction of Program I NIT
ℓ0 :
ℓ1 :
ℓ2 :
havoc Iter ′
assume Iter ′ = Cont
Iter := Iter ′
while ∗ do
havoc Iter ′ , Init ′
assume Init ⊆ Init ′
assume Iter ′ ⊆ Iter
(Iter , Init):= (Iter ′ , Init ′ )
assume Iter =∅
assert Cont ⊆ Init
ℓ0 :
ℓ1 :
ℓ2 :
havoc Iter ′
assume Iter ′ = Cont
Iter := Iter ′
while ∗ do
havoc Iter ′ , Init ′ , Y
assume Init ′ = Init ∪ Y
assume Iter ′ = Iter − Y
(Iter , Init ):= (Iter ′ , Init ′ )
assume Iter =∅
assert Cont ⊆ Init
Figure 4. Program C ARTA BS I NIT : Carte- Figure 5. Counterexample-guided focus
sian abstraction of Program A BS I NIT
applied to Program C ARTA BS I NIT
For readability, in the abstract program in Fig. 3, we decompose the
abstract transformers into several assume statements and represent
them as set constraints (instead of universally quantified formulas).
Also, we omit some redundant information.
The successor abstract state under the execution of a basic block
in the abstract program is obtained as one expects. First the abstract state is conjoined with the logical formula (in unprimed and
primed variables) that represents the abstract transformer of the basic block. Then the unprimed variables are projected and the primed
variables renamed by their unprimed version. The (finite) set of
reachable abstract states for the abstract program A BS I NIT represents an (inductive) invariant of the program I NIT . Projected to location ℓ1 , this invariant implies the formula F . I.e., the corresponding analysis (which computes the set of reachable abstract states of
A BS I NIT ) succeeds to prove the correctness of the program I NIT .
Program A BS I NIT represents the most precise abstract transformer with respect to the abstract domain induced by the given
predicates. This abstraction is in general too expensive. First, the
construction of the abstract program requires exponentially many
theorem prover calls in the number of predicates. Second, the most
precise abstraction often keeps track of more information than is
necessary for proving a specific property and causes the analysis to
explore unnecessarily large parts of the abstract domain.
Abstract Transformer with Cartesian Abstraction. In order to
obtain an abstract transformer with feasible cost, one can apply
the Cartesian abstraction [2, 16] on top of the Boolean heap abstraction. Cartesian abstraction is originally defined on abstract domains that are power sets of vectors (in our case bitvectors). Its
name stems from the fact that it abstracts a set of vectors S by the
smallest Cartesian product (of component sets) that contains S. It
applies to our setting because we can represent an abstract state
canonically as a set of bitvectors where each bitvector corresponds
to an inner disjunct of the abstract state.
The Cartesian abstraction of the most precise abstract post (with
respect to the Boolean heap abstraction) can be constructed effectively from the concrete program (in practice using only a polynomial number of theorem prover calls) [45]. Furthermore, Cartesian
abstraction avoids an explosion of the size and number of abstract
states that are explored in the fixed point computation by restricting
the disjuncts that can appear below and above the universal quantifier in abstract states. The resulting analysis is efficient in practice.
In general, however, it is too imprecise. This is demonstrated by
our example program.
Figure 4 shows the Cartesian abstraction of Program A BS I NIT .
Program C ARTA BS I NIT loses the correlation between the predicates Iter and Init in the abstract transformer of the loop body (the
correlation is expressed by the statement assume Init ′ − Init =
Iter − Iter ′ in the program A BS I NIT in Fig. 3). As a consequence,
the invariant F of Program A BS I NIT is not an invariant of Program C ARTA BS I NIT . I.e., the corresponding analysis (which computes the set of reachable abstract states of C ARTA BS I NIT ) does
not succeed to prove the correctness of the program I NIT .
Counterexample-Guided Focus. The analysis of the abstract program C ARTA BS I NIT produces a spurious error trace that witnesses
a violation of the assert statement at location ℓ2 . It seems tempting to try to use the spurious error trace to refine the abstraction as
done in the CEGAR scheme. As mentioned above, in the existing
CEGAR scheme, the abstract domain is refined; the desired effect
is to eliminate the spurious error trace. The underlying assumption
which guarantees this effect is that the refined analysis is based
on the most precise abstract transformer. In our case, however, the
spurious error trace results from an imprecise abstract transformer,
rather than an imprecise abstract domain. In fact, the abstract domain is already able to express an inductive invariant that is sufficiently strong to rule out all spurious error traces. An attempt to
extract new predicates from the proof of spuriousness of the counterexample is therefore pointless. It makes more sense to use the
spurious error trace for refining the abstract transformer, rather than
the abstract domain.
To do so, we use the above-mentioned focus operator that is inspired by shape analysis. It refines the image of the abstract transformer by changing the presentation of its pre-image. We do not
change the definition of the abstract transformer (with the Cartesian abstraction). However, when applied to the new presentation,
the Cartesian abstraction does not lose as much precision as before.
The novelty of our approach is that we devise a focus operator that
is constructed on-demand and locally from the spurious error trace.
Figure 5 illustrates the effect of our counterexample-guided focus operator on Program C ARTA BS I NIT . It only affects the body
of the while loop. The focus operator uses an additional predicate
Y = { v | y = v } in order to split disjuncts below the universal
quantifier into multiple disjuncts before the application of the abstract transformer for the loop body. As a consequence, the correlation between variables Iter and Init is not lost in spite of the
Cartesian abstraction of the abstract transformer. Note the different, but equivalent representation of the correlation by two assume
statements. Our method and tool automatically infers the split predicate Y and the corresponding focus operator from a spurious counterexample of the abstract program C ARTA BS I NIT . The inductive
invariant obtained by the fixed point of the resulting abstract transformer implies the assertion at location ℓ2 . I.e., the analysis (which
computes the set of reachable abstract states of the abstract program in Fig. 5) does succeed to prove the correctness of Program
I NIT .
3. Preliminaries
• If c is a sequential composition c1 ; c2 then there must exist a
We now formalize the notion of formulas and programs used in this
paper and formally introduce Boolean heap abstraction.
• If c is an assume command assume F , we require that s |= F
3.1 Logics and Programs.
• If c is an update of the program counter or a program variable,
Logical formulas and structures. For reasoning about programs
we consider formulas in a sorted logic L. We require that L provides the sorts obj (heap objects) and loc (program locations). Furthermore, we require that L provides logical constructs for equality
over both sorts, Boolean connectives, and first-order quantification
over variables of sort obj. Formulas are expressed over a signature
Σ where Σ consists of the following constant symbols of sort loc:
ℓ ∈ Locs (control locations), pc (the program counter), ℓ0 (the initial location), and ℓE (the error location), as well as the following
symbols of sort obj: unary function symbols f ∈ Flds (data structure fields) and constant symbols x ∈ Vars (program variables).
We leave out sort annotations in formulas whenever this causes no
confusion. In our running examples, the logic L is given by firstorder logic with transitive closure.
We fix non-empty disjoints sets O and L for the interpretations
of sort obj and loc. A Σ-structure A is a first-order interpretation
that interprets each symbol in Σ by a function over, respectively, an
element in the sets interpreting the associated sorts. We write A(f )
for the interpretation of symbol f ∈ Σ in structure A. We further
write A(t) for the denotation of a Σ-term t in structure A. Hereby,
A also provides interpretations for the free variables occurring in
t. We use standard logical notation for satisfiability, validity, and
entailment. Finally, for a formula F we write [[F ]] to denote the set
of all structures that satisfy F .
Programs. The set of commands Coms is defined by the following
grammar where F is a formula in L, x, y ∈ Vars denote program
variables, f ∈ Flds a pointer field, and ℓ ∈ Locs ∪ {ℓ0 , ℓE } a
control location:
c ::= c ; c | assume F | pc:= ℓ | x:= y | x:= y.f | x.f := y
A program P is a finite set of commands. Consecutiveness of commands in a program is achieved by composing assume statements
on the value of pc, updates of pc, and the actual commands using
the sequential composition operator.
Program states. A program state is a Σ-structure. We denote by
States the set of all program states. We call a state s initial state
iff it satisfies s |= pc=ℓ0 and we call it error state iff it satisfies
s |= pc=ℓE . Let init be the formula pc=ℓ0 denoting all initial
states and let safe be the formula pc6=ℓE denoting all non-error
states.
Null pointers and allocation. Note that we interpret data structure
fields f as total functions. We treat null as a program variable that
can neither be assigned nor dereferenced. We assume that for all
fields f ∈ Flds the equality null.f = null holds in all program
states. In order to ensure absence of null dereferences, every command c that contains a dereference of the form x.f is guarded by a
command assume (x=null); pc:= ℓE that directs control to the error location if x is not defined. Allocation of fresh heap objects can
be modelled by introducing a predicate symbol that keeps track of
the current set of allocated objects. However, this requires the inclusion of havoc commands that nondeterministically update the
value of a program variable. The techniques presented in this paper
carry over to programs extended with havoc commands. For details
see [51].
Transition relations. Each command c represents a relation [[c]] on
pairs of states (s, s′ ) that is defined recursively on the structure of
commands as follows:
state s′′ such that (s, s′′ ) ∈ [[c1 ]] and (s′′ , s′ ) ∈ [[c2 ]].
and s′ = s.
we have s′ = s[pc7→s(ℓ)] for c = (pc:= ℓ), s′ = s[x 7→ s(y)]
for c = (x:= y) and s′ = s[x 7→ s(f (y))] for c = (x:= f.y).
• Finally, if c is a field update of the form f.x:= y, we have
s′ = s[f 7→ s(f )[s(x)7→s(y)]].
Computations and traces. A program computation of a program
c1
c0
. . . of states
s1 →
P is a (possibly infinite) sequence σ = s0 →
and commands such that s0 is an initial state and for each pair of
consecutive states si and si+1 we have (si , si+1 ) ∈ [[ci ]] for some
command ci ∈ P . A trace is a sequence of commands and we
c1
c0
. . . to the
s1 →
call the projection of a computation σ = s0 →
sequence of commands c0 c1 . . . the trace of that computation. A
trace is called error trace if it is the trace of some computation that
reaches an error state. A program is safe if it has no error traces.
Predicate transformers. Given a set of states S and a binary
relation R on states, we define strongest postcondition post and
weakest (liberal) precondition wlp as usual:
¯
def ˘
post(R)(S) = s′ | ∃s. (s, s′ ) ∈ R ∧ s ∈ S
¯
def ˘
wlp(R)(S) = s | ∀s′ . (s, s′ ) ∈ R ⇒ s′ ∈ S .
We further introduce symbolic weakest preconditions on formulas.
For any command c and formula F the formula wlp(c)(F ) is a formula such that we have wlp([[c]])([[F ]]) = [[wlp(c)(F )]]. Note that
we do not require F to be closed. We extend symbolic weakest preconditions from commands to sequences of commands as expected.
3.2 Boolean Heap Abstraction
We formalize the Boolean heap abstraction in terms of an abstract
interpretation [15]. The concrete domain is given by sets of states
ordered by set inclusion. We represent elements of the concrete domain by closed formulas in L. The concrete fixed point functional
is the operator post for the transition relation of the concrete program (i.e., the union of the transition relations of all its commands)
and the initial states init. The abstract domain is a finite set of formulas that forms a sublattice of the concrete domain. The analysis
is to compute the least fixed point of an abstraction of post that is
defined on the abstract domain. The computed least fixed point is
an inductive invariant of the concrete program.
Abstract domain. The abstract domain is parameterized by a finite
set of predicates that denote sets on heap objects in a given state.
In the following, we fix a particular finite set of predicates P. We
consider P to be given by a set of (closed) lambda terms of the form
λv. G where G is a formula in L, i.e., each predicate p ∈ P denotes
a set of objects in a given state. If the formula G is itself closed
we call the corresponding predicate state predicate. We denote by
P(v) the set of formulas obtained by beta reduction of all formulas
p(v) for p ∈ P. For notational convenience we assume that P
is such that for all v the set P(v) is closed under negation. The
following definitions are implicitly parameterized by the set P.
The abstract domain AbsDom over P consists of all formulas
of the form
mi
n
_
_
∀v.
Dji (v)
i=1
ji =1
where each Dji (v) is a conjunction of formulas in P(v). We
call the outer disjuncts of these formulas abstract states and the
inner disjuncts abstract objects. We identify formulas up to logical
equivalence. The partial order on the abstract domain is given by
Abstraction function. The abstraction function α that maps a set of
states represented by a closed formula F to a formula in the abstract
domain is defined as follows
o
^n #
def
α(F ) =
F ∈ AbsDom | F |= F #
.
x, y
pc=ℓ1 s
0
data
next
Cont
Iter
Init c
7
next
null
x
Cont c
Iter c
Cont
Iter c
Init
pc=ℓ1 s′
data
7
data
3
data
the logical entailment relation “|=”. Note that AbsDom is finite
(modulo logical equivalence) and closed under both conjunction
and disjunction. Thus, it forms a complete lattice. The abstract
domain can be easily generalized to formulas with quantification
over more than one variable and predicates that denote relations on
objects rather than just sets [51].
next
y
Cont
Iter
Init c
next
null
Cont c
Iter c
Figure 6. A reachable program state s of Program I NIT and its
successor state s′ that is obtained after execution of the loop body.
complement of S. Formally the state s satisfies the abstract state
The function α is the lower adjoint of a Galois connection (α, γ)
between the concrete and abstract domain, with γ being the identity
function.
F # = ∀v. v ∈ Cont ∧v ∈ Iter ∧v ∈
/ Init ∨v ∈
/ Cont ∧v ∈
/ Iter
Abstract post operator. The most precise abstract post operator on
the abstract domain of Boolean heaps post#
BH and a command c is
given by composition of the concrete post operator for c with the
Galois connection (α, γ). The actual abstract post operator that we
use in the fixed point computation of the analysis is an abstraction
#
of post#
BH . We denote this operator by postC·BH and call it the
Cartesian abstract post operator. Formally, the operator post#
C·BH
is defined as a Cartesian abstraction of the operator post#
BH . In the
following, we only show how post#
C·BH is computed. For further
details see [45, 51].
We allow abstract states in the pre and post-images of operator
post#
C·BH to range over different sets of predicates P1 , respectively,
P2 . Let c be a command and F # an abstract state over predicates
P1 of the form
m
_
F # = ∀v.
Dj (v)
v ∈ Cont ∧ v ∈ Iter ∧ v ∈
/ Init
j=1
where the disjuncts Dj are monomials, i.e., each predicate in
P1 occurs either positive or negative in each Dj . The operator
#
post#
to a single abstract state F ′# by mapping each
C·BH maps F
#
disjunct Dj in F to a single disjunct Dj′ in F ′# . The mapping
guarantees that if s ∈ States is a concrete state that satisfies F #
and o ∈ O an object that satisfies disjunct Dj in s then for every
c-successor s′ of s, o satisfies Dj′ in s′ . Since this property holds
for all objects o, every c-successor s′ of s satisfies F ′# . Formally,
the image of F # under the abstract post operator post#
C·BH for
command c is given by:
#
post#
C·BH [P1 , P2 ](c)(F ) =
Wm V ˘
∀v. j=1
p(v) ∈ P2 (v)
| F # ∧ Dj (v) |= wlp(c)(p(v))
¯
Thus, the image of the Cartesian abstract post is computed by
checking entailments between conjunctions of predicates and
weakest preconditions of predicates. The quantified formula F # in
the antecedent of these entailments can be replaced by any weaker
formula, e.g., a conjunction of finitely many instantiations of F # .
The operator post#
C·BH is extended to disjunctions of abstract states
as expected.
4. Counterexample-Guided Focus
Before we formally define the counterexample-guided focus operator, it is instructive to fully understand the nature of the loss of
precision that is induced by Cartesian abstraction.
Recall Program Init from Section 2. The left part of Figure 6
shows a program state s that may occur at location ℓ1 during
execution of Program I NIT . The boxes represent abstract objects
over the sets Cont , Init, and Iter . Hereby S c stands for the set
The right part of Figure 6 shows the post-state s′ of s that is
obtained at location ℓ1 after execution of the loop body. The boxes
indicate again the abstract objects associated with the concrete
objects. The abstract state consisting of the disjunction of these
abstract objects is the image of the most precise abstract post
#
′#
post#
be this abstract postBH of the abstract state F . Let F
state. Note that the concrete objects in s that are represented by
the abstract object
end up in two disjoint abstract objects in F ′# . The Cartesian abstract post operator merges these two disjuncts into a single conjunction that only contains the predicates on which both disjuncts
agree, namely, v ∈ Cont . The correlations between predicates Init
and Iter in the two inner disjuncts of F ′# are lost.
If we want to adapt the precision of the Cartesian abstract post
we need to prevent it from merging disjuncts in the post-image of
the most precise abstract post. This adaptation is performed by our
focus operator. The focus operator adapts the precision of the Cartesian abstract post indirectly. Namely, it refines the abstract domain
of the pre-image and splits disjuncts (i.e., both abstract states and
abstract objects in abstract states) in the pre-image into more finegrained disjunctions. The splitting ensures that individual disjuncts
in the refined pre-image are mapped to individual disjuncts in the
post-image under the most precise abstract post. This effectively
prevents Cartesian abstraction from losing precision. Both the refinement of the abstract domain and the splitting of disjuncts are
guided by spurious error traces that are produced by the analysis.
Counterexample-guided focus. We now formally define the
counterexample-guided focus operator. In the following we fix a
program P and a set of predicates P. An abstract computation σ #
is a sequence
op
op
op n−1
0
1
F0# −→
F1# −→
. . . −→ Fn#
where the Fi# are elements of the abstract domain AbsDom[P]
and the op i are abstract transformers AbsDom[P] → AbsDom[P].
Moreover, the following two conditions hold: (1) F0# = α[P](init)
#
and (2) for all i between 0 and n−1, Fi+1
= op i (Fi# ). We say that
the abstract computation ends in an error state if Fn# 6|= safe. We
say that σ # is generated by a trace π = c0 . . . cn−1 and an operator
op ∈ Coms → AbsDom → AbsDom if for all i, op i = op(ci ).
We say that the generated abstract computation is sound if for all i
#
between 0 and n − 1, Fi+1
is an over-approximation of the set of
states that are reachable from an initial state by following the trace
c0 . . . ci . Finally, we say that a trace π is a spurious error trace for
op, if the abstract computation generated by π and op ends in an
error state, yet, π is not an error trace of program P .
The counterexample-guided focus operator is used to eliminate
spurious error traces for the Cartesian abstract post that are not
spurious error traces for the most precise abstract post. Let π0 =
c0 . . . cn−1 be such a trace.
Note that concrete error traces can be characterized in terms of
symbolic weakest preconditions.
L EMMA 1. A trace π is an error trace iff init 6|= wlp(π)(safe).
Since π0 is not a concrete error trace, we know that π0 satisfies
init |= wlp(π0 )(safe)
(1)
Let pref i (π0 ) be the prefix of π0 up to command ci−1 , respectively,
suff i (π0 ) its suffix starting from command ci . From (1) and the
properties of predicate transformers post and wlp follows that for
all i between 0 and n − 1 we have
post([[pref i (π0 )]])([[init]]) ⊆ wlp([[suff i (π0 )]])([[safe]])
(2)
In other words, for each i the formula wlp(suff i (π0 ))(safe) is
satisfied in all states that are reachable from an initial state by
following the trace pref i (π0 ). The idea of our counterexampleguided focus operator is to use the formulas wlp(suff i (π0 ))(safe)
to guide the splitting of disjuncts in the pre-images of the Cartesian
post operator.
The counterexample-guided focus operator focus takes a sequence of commands π (the suffix of a spurious error trace π0 )
and an element of the abstract domain AbsDom[P] as arguments
and maps the latter to an element of a refined abstract domain
AbsDom[preds(π)] with P ⊆ preds (π). The operator focus is
defined as follows
def
focus(π)(F # ) = α[preds(π)](wlp(π)(safe)) ∧ F #
The set of predicates preds(π) is the union of the predicates P and
predicates that are extracted from the weakest precondition of safe
with respect to π. More precisely, if π = cπ ′ then preds extracts
all atoms from the formula
wlp(c)(α[P](wlp(π ′ )(safe)))
(3)
post#
f·C·BH (π)
The adapted Cartesian abstract post operator
for the
suffix π of some spurious error trace is obtained by composition of
the Cartesian post operator (for the refined pre-image domain) with
the focus operator:
def
#
post#
f·C·BH (π) = λc. postC·BH [preds(π), P](c) ◦ focus(π)
Let σ # be the abstract computation generated from path π0 and
the sequence of operators [post#
f·C·BH (suff i (π))(ci )]0≤i<n .
P ROPOSITION 2 (Soundness of Focus). The abstract computation
σ # is sound.
The proof of Proposition 2 follows from Property (2) and the
fact that post#
C·BH is a sound approximation of the most precise
abstract post operator post#
BH .
P ROPOSITION 3 (Progress of Focus). The abstract computation
σ # does not reach an error state.
The proof of Proposition 3 relies on the fact that the trace πo
of computation σ # is not an error trace and not spurious for the
most precise abstract post. One can then show that focus performs
sufficient splitting of disjuncts in abstract states of σ # before each
application of the Cartesian abstract post. This splitting ensures
that Cartesian abstraction causes no loss of information that is
crucial for proving that πo is safe. Note, however, that the focused
Cartesian abstract post is not guranteed to compute the most precise
abstract post for the given trace. Its precision lies between the plain
Cartesian abstract post and the most precise abstract post.
Practical considerations. In our actual analysis the focus operator is always applied in a very specific situation, namely, when
the abstract domain for the post states of the refined abstract transformer already contains all the predicates that can be extracted from
the spurious error trace used for the focus. Therefore the abstraction function α in (3) can be replaced by the identity function. The
resulting focus operator is then polynomial in the number of extracted predicates and the size of the representation of the focused
abstract states.
5. Additional Examples
We now illustrate how the counterexample-guided focus adapts
the abstract transformers for the abstractions of three example
programs. In all of these examples, the analysis without counterexample-guided focus would not be able to infer a sufficiently
strong invariant that proves the correctness of the program.
Program I NIT. We first revisit Program I NIT from Section 2.
We explained the nature of the loss of precision under Cartesian
abstraction for this example program in the previous section. This
loss of precision causes that the analysis of Program I NIT produces
spurious error traces even though the abstract domain can express
a sufficiently strong inductive invariant. The shortest such spurious
error trace is the trace that starts with the commands at location ℓ0
executes the while loop once and then goes to the error location via
the failing assert statement at location ℓ2 . The weakest precondition
wlp(π)(safe) for the suffix π of this spurious error trace that starts
in location ℓ1 is given by the formula
y.next =null ∧ y6=null →
(∀v. (x, v) ∈ next ∗ ∧ v6=null → v.data=0 ∨ y=v)
Using this formula, the focus operator refines the pre-image of the
abstract transformer for the loop body by adding, among other
predicates, the predicate Y = { v | y=v }. In this particular example, simply adding predicate Y to the pre-image domain is sufficient to rule out the spurious error trace. Recall that the image of
the Cartesian abstract post operator is computed by considering a
normal form of the pre-image where in each inner disjunct every
predicate occurs either positively or negatively. Thus, refining the
abstract domain by adding predicate Y already enables the Cartesian abstract post to perform the necessary splitting of disjuncts in
the original pre-image. However, this splitting is purely syntactic.
Some of the split disjuncts might be unsatisfiable in all represented
pre-states but might be mapped to satisfiable disjuncts in the postimage and, thus, cause imprecision. Also, without proper focus the
image of the Cartesian abstract post will always be a single abstract
state and never a proper disjunction. Our second example shows
that, in general, the local refinement of the abstract domain of the
pre-image alone, does not suffice to eliminate a spurious error trace
for the Cartesian abstract post.
Program L IST R EVERSE. Consider program L IST R EVERSE in
Figure 7 that performs an in-place reversal of a singly-linked list.
The list is pointed to by program variable r. Assume the heap
is sharing-free before execution of the program (the first assume
statement at location ℓ0 ) and assume that r points to the actual root
node of the list (the second assume statement at location ℓ0 ). We
would like to verify that under these assumptions r points again to
the root node of the reversed list after termination of the program.
One part of the inductive invariant for location ℓ1 that is needed
to verify the assertion at location ℓ2 is given by the formula
e=null ∨ (∀v. v.next 6=e)
This formula expresses that e always points to the root of the part
of the original list that has yet to be reversed. This formula can be
ℓ0 : assume (∀u v w. v.next=w ∧ u.next=w ∧ v6=u → w=null)
assume (r=null ∨ (∀v. v.next6=r))
e:= r ; r := null
ℓ1 : while e6=null do
t:= e; e:= e.next
t.next:= r
ℓ2 : assert (r=null ∨ (∀v. v.next6=r))
Figure 7. Program L IST R EVERSE
r
next
s2 pc=ℓ1
next
t
nex
s1 pc=ℓ1
r
nex
t
e
null
e
next
next
Figure 9. Program DL IST E RASE
nex
t
t
nex
ℓ0 : assume (∀v w. v.prev 6=null ∧ w.next6=v ∨ v.prev .next = v)
assume (∀v. v ∈ Cont 0 ↔ (r, v) ∈ next ∗ ∧ v6=null)
assume l ∈ Cont 0
assume l.next=null
ℓ1 : while l6=null do
l.next:= null
t:= l; l:= l.prev
t.prev := null
ℓ2 : assert (∀v. v ∈ Cont 0 → v.next=null ∧ v.prev =null)
null
Figure 8. Two reachable states of Program L IST R EVERSE
expressed by the disjunction of the following two abstract states
F1# : ∀v. e=v ↔ null=v
F2# : ∀v. v.next 6=e
Figure 8 shows two states that may occur at location ℓ1 during
execution of the while loop. Both states satisfy the abstract state
F2# . Note that after execution of the loop body, the state s1 satisfies only abstract state F1# while the second state satisfies only
abstract state F2# . The Cartesian abstract post operator will always
merge the post states of F2# that result from execution of the loop
body into a single abstract state. An analysis based on this operator therefore cannot infer a sufficiently strong inductive invariant.
The counterexample-guided focus causes F2# to be split into two
abstract states, one whose post states are covered by F1# and one
whose post states are covered by F2# . Thus, the Cartesian abstract
post will not lose precision on the focused pre-image. Refining the
abstract domain by adding additional predicates will not make up
for the loss of precision on the unfocused pre-image, unless one
adds a state predicate that expresses one of the outer disjuncts in
terms of an inner disjunct of the other (e.g., the predicate e=null in
the given example). In general, the state predicates that one needs
to add to prevent loss of precision under Cartesian abstraction can
be arbitrarily complex quantified formulas.
Program DL IST E RASE. The splitting of disjuncts that is performed by our counterexample-guided focus operator is closely related to materialization in shape analysis. Materialization refers to
an intermediate step in the computation of the abstract post where
a concrete object is extracted from an abstraction of a collection of
objects. For instance, given an abstraction of a list, one needs to
extract the head of the list in order to compute a precise abstract
post-state for a command that iterates over the list. Our third example demonstrates that counterexample-guided focus performs materialization automatically, even in cases that require data-structurespecific manual adaptation of the abstract transformers in many existing shape analyses.
Consider Program DL IST E RASE shown in Figure 9. This program erases all entries in an acyclic doubly-linked list. The list is
pointed to by program variable r. The loop that erases the entries in
the list iterates backwards over the data structure starting from the
last entry l. In each iteration both the forward pointer next and the
backward pointer prev of the current iterate are set to null. The task
is to verify that the prev and next fields of all original list entries
have indeed been set to null after the loop terminates. This property is expressed by the assert statement at location ℓ2 . The assume
statements at location ℓ0 express the precondition of the program.
The first assume statement expresses that field prev is the inverse
of field next which implies that the list r is doubly-linked. The second assume statements defines the set Cond 0 , a ghost variable that
denotes the set of elements that are originally stored in the list. The
third and fourth assume statement together ensure that l points to
the last entry in the list.
The important part of the inductive invariant at location ℓ1 that
is strong enough for proving the assertion at location ℓ2 is given by
the following formula
∀v. v ∈ Cont 0 ∧ (l, v) ∈
/ prev ∗ → v.next =null ∧ v.prev =null
In order to infer this formula, the abstract transformer for the loop
body needs to split some of the inner disjuncts that contain positive
occurrences of the predicate (l, v) ∈ prev ∗ in order to keep precise information about the object pointed to by program variable l
in each iteration. This splitting corresponds to materialization from
the back of the doubly-linked list. Many shape analyses, e.g., those
based on separation logic [13, 19, 38], need special hand-crafted
rules to perform materialization for specific data structures. With
counterexample-guided focus, the abstract transformer is automatically adapted to perform materialization. The adaptation mechanism is independent of the data structures that the analyzed program manipulates and is only applied when the extra precision is
needed to prove a particular property.
6. Lazy Nested Abstraction Refinement
We now present our lazy nested abstraction refinement loop
that integrates lazy counterexample-guided refinement of the abstract domain and lazy adaptation of the abstract transformer via
counterexample-guided focus.
The refinement loop is shown in Figure 10. The procedure
LazyNestedRefine takes a program P as input and constructs
an abstract reachability tree (ART) in the spirit of lazy abstraction [25]. An ART is a tree where each node r is labeled by a set of
predicates r.preds and abstract states r.states in AbsDom[r.preds].
The root node r0 of the ART is labeled by an abstract state denoting the set of all initial states. Each edge in the ART is labeled by
a command c in program P and an abstract transformer op for the
c,op
command c. We write r −→ r ′ to denote that there is an edge in
the ART from node r to node r ′ which is labeled by c and op.
π
Furthermore, we write r → ∗ r ′ to indicate that there is a (possibly
empty) path from r to r ′ in the ART that is labeled by the trace π.
Each path in the ART starting from the root node corresponds to an
abstract computation that is generated from the trace labelling the
path.
The lazy nested abstraction refinement algorithm iteratively extends and refines the ART until either a fixed point is reached, i.e.,
the disjunction of the abstract states contained in all ART nodes is
an inductive invariant of program P , or until an error trace has been
constructed. If a spurious error trace is encountered during the fixed
point computation then this trace is used to refine the abstraction.
We now describe the algorithm in detail.
proc LazyNestedRefine(P : program)
begin
let r0 = hpreds : {init} , states : init, covered : falsei
let succ(r) =
let Succ = ∅
for all c ∈ T do
let r ′ = hpreds : ∅, states : false, covered : falsei
let op = post#
C·BH (c)
c,op
add edge r −→ r ′
Succ:= Succ ∪ {(r, op, r ′ )}
return Succ
let U = succ(r0 )
while U 6= ∅ do
choose and remove (r1 , op, r2 ) ∈ U
r2 .states:= op[r1 .preds , r2 .preds](r1 .states)
W
if r2 .states |= r { r.states | r 6= r2 } then
r.covered := true
else if r2 .states |= safe then U := U ∪ succ(r ′ )
π
else let rs , π such that π is maximal trace with rs → ∗ r2 ∧
rs .states 6|= wlp(π)(safe)
if rs = r0 then return counterexample (π)
c,op
else let rp , c, op such that rp −→ rs
let Pπ = preds(wlp(π)(safe))
let op ′ = if Pπ 6⊆ rs .preds then
rs .preds := rs .preds ∪ Pπ
op
else op ◦ focus(cπ)
remove subtrees starting from rs
for all r2 such that
r2 .covered ∧ r2 .states 6|= false ∧
rs .states older than r2 .states do
c,op
let r1 , c, op such that r1 −→ r2
r2 .covered := false
r2 .states:= false
U := U ∪ {(r1 , op, r2 )}
rs .states:= false
rs .covered:= false
c,op ′
update edge rp −→ rs
U := U ∪ {(rp , op ′ , rs )}
return ”program is safe”
end
Figure 10. Lazy nested abstraction refinement algorithm
The algorithm maintains a work set of unprocessed ART edges
U . In each iteration one unprocessed ART edge (r1 , op, r2 ) is
selected. Then the image of the abstract states in r1 and the abstract
transformer op is computed and the resulting abstract states are
stored in r2 .states . If the computed abstract states are already
subsumed by other ART nodes then the node r2 is marked as
covered. Otherwise if r2 .states contains no error states then the
ART is extended with new nodes that are the successors of r2 for all
the commands in P . The edges to the successor nodes are labeled
by the commands c and the initial abstract transformer given by the
Cartesian abstract post for the command c. Then the new edges are
inserted to the set of unprocessed edges.
If r2 contains error states then the trace labelling the path from
r0 to r2 is a potential error trace. The analysis now determines
whether this trace is a spurious error trace. For this purpose, it
performs a symbolic backward analysis of the error trace. This
backward analysis finds the oldest ancestor node rs of r2 with
π
rs → ∗ r2 such that rs .states represents some concrete state that
can reach an error state by executing the trace π, i.e., formally rs is
the oldest node on the path that still satisfies
rs .states 6|= wlp(π)(safe) .
If rs is the root node of the ART then π is a concrete error trace and
the procedure returns the counterexample. If, however, rs is not the
root node then the trace is a spurious error trace. In this case we call
rs the spurious node of the trace. The algorithm then determines
the immediate predecessor node rp of the spurious node. We call
rp the pivot node of the spurious error trace. The pivot node is the
youngest node on the given path in the ART that does not represent
any concrete states that can reach an error state by following the
commands in the trace. Depending on the refinement phase either
the abstract domain of rs is refined or the abstract transformer that
labels the edge between rs and rp is adapted.
The refinement works as follows. The spurious part of the error
trace starts from the spurious node rs . Our abstraction refinement
procedure first attempts to refine the abstract domain of node rs
by adding new predicates Pπ that are extracted from the spurious
part π of the spurious error trace. The predicate extraction function
preds guarantees that the weakest precondition wlp(π)(safe) of the
path is expressible in the abstract domain of the refined node rs ,
i.e., formally the following entailment holds
α[Pπ ](wlp(π)(false)) |= wlp(π)(false) .
Suppose the analysis was to compute the most precise abstract post
operator for each command. Then we were guaranteed that after
refining the predicate set of node rs and reprocessing the ART
edge between rp and rs , the node rs would no longer be spurious
for this spurious error trace. This would ensure that the spurious
error trace would eventually be eliminated. However, our analysis
uses the Cartesian abstract post operator. Thus, the same spurious
error trace might be reproduced after the refinement of the abstract
domain. The refinement procedure would then fail to derive new
predicates for node rs . In this case, the second refinement phase
refines the current abstract transformer op for command c that
labels the edge between rp and rs . The abstract transformer op is
refined by composition with the counterexample-guided focus for
the spurious part cπ of the trace.
After the abstraction has been refined, the spurious subtrees below rs are removed from the ART. In order to ensure soundness,
ART nodes that have potentially been marked as covered due to
subsumption by nodes in the removed subtrees are uncovered and
reinserted into the work set. Finally, the spurious ART edge between rp and rs is updated and also reinserted into the work set.
If the set of unprocessed ART edges becomes empty then all
outgoing edges of inner ART nodes have been processed and all
leaf nodes are covered, i.e., an inductive invariant that proves the
absence of reachable error states has been computed. Thus, the
procedure returns “program is safe”.
T HEOREM 4 (Soundness). Procedure LazyNestedRefine is sound,
i.e., for any program P if LazyNestedRefine(P ) terminates with
“program is safe” then program P is safe.
The proof of Theorem 4 relies on Proposition 2 and follows
the argumentation in [25]. The next theorem states that procedure
LazyNestedRefine has the progress property. In the setting of lazy
abstraction, progress means that procedure LazyNestedRefine cannot diverge because it gets stuck on refining a finite set of spurious
error traces over and over again.
T HEOREM 5 (Progress). A run ∆ of procedure LazyNestedRefine
terminates, unless the set of spurious error traces encountered in ∆
is infinite.
benchmark
List.traverse
List.init
List.create
List.getLast
List.contains
List.insertBefore
List.append
List.filter
List.partition
List.reverse
DList.addLast
DList.erase
SortedList.add
SkipList.add
Tree.add
ParentTree.add
ThreadedTree.add
Client.move
Client.createMove
Client.partition
checked properties
AC, SF
AC, SF, PC
AC, SF
AC, SF, PC
AC, SF, PC
AC, SF
AC, SF
AC, SF
AC, SF
AC, SF
AC, SF, DL, PC
AC, SF, DL, PC
AC, SF, SO, PC
AC, SF, PC
AC, SF, PC
AC, SF, PL, PC
AC, SF, TH, SO, PC
CS
CS, PC
CS, FC, PC
DP
time (in s) CGF
MONA
0.11 no
MONA
0.69 no
MONA
0.78 yes
MONA
0.53 no
MONA
0.53 no
MONA
2.48 yes
MONA
8.95 no
MONA
5.31 yes
MONA
149.16 yes
MONA
5.52 yes
MONA
2.05 yes
MONA
17.98 yes
MONA, Z3
9.88 no
MONA
10.82 yes
MONA
18.51 no
MONA
20.48 no
MONA, Z3
445.93 no
Z3
3.11 no
Z3
41.07 yes
Z3
108.15 no
Properties: CS = call safety, AC = acyclic, SF = sharing free, DL = doubly linked, PL =
parent linked, TH = threaded, SO = sorted, FC = frame condition, PC = post condition
Table 1. Summary of experiments. Column DP lists the used decision procedures. Column CGF indicates whether counterexampleguided focus was required to successfully verify the corresponding
program.
7. Implementation and Case Studies
We implemented our analysis in our tool Bohne. Bohne is implemented in Objective Caml and distributed as part of the Jahob system [30, 51, 56, 57]. The input to Bohne are Java programs annotated with special comments that specify procedure contracts and
representation invariants of data structures. We represent elements
of the abstract domain as sets of BDDs [11] and use the weaker
order of propositional implication for the fixed point test in the
abstraction refinement loop. Abstract transformers are also represented as BDDs to enable efficient post-image computation. We
represent the program counter explicitely and not implicitely in abstract states. Our tool uses a few simple heuristics to guess an initial set of predicates from the input program and its specification.
All additional predicates are inferred in the nested abstraction refinement procedure. Since we syntactically extract new predicates
from weakest preconditions of finite traces in the program, we cannot infer reachability predicates (i.e., predicates with transitive closure operators) if they do not already occur in the specification.
We therefore use a widening technique to infer new reachability
predicates from the predicates that are extracted from weakest preconditions.
Case studies. We applied Bohne to verify operations on a diverse
set of data structures implementations, checking a variety of properties. No manual adaptation of the abstract domain or the abstract
transformers was required for the successful verification of these
example programs. In particular, we were able to verify preservation of data structure invariants for operations on threaded binary
trees (including sortedness and the in-order traversal invariant). We
are not aware of any other analysis that can verify these properties with a comparable degree of automation. Further experiments
cover data structures such as (sorted) singly-linked lists, doublylinked lists, two-level skip lists, trees, and trees with parent pointers. The verified properties include: absence of runtime errors such
as null dereferences complex data structure consistency properties,
such as preservation of the tree structure and sortedness. Finally,
we verified procedure contracts expressing functional correctness
benchmark
#appl. of #ref.
final ART
#predicates
abs. post steps size depth st./loc. total avrg. max.
List.traverse
3
0
4
4 1.00
4 2.8
3
List.init
5
1
5
5 2.00
7 5.4
7
List.create
11
6
6
6 1.67 11 6.7
9
List.getLast
7
1
6
6 2.00
7 6.0
7
List.contains
5
1
5
5 2.00
6 5.2
6
List.insertBefore
8
2
5
5 13.50 10 7.4
8
List.append
5
1
4
4 1.50 13 8.2
11
List.filter
31
5 14
5 2.50 12 7.1
10
List.partition
62
21 40
7 3.50 15 10.8
12
List.reverse
9
3
5
5 2.00 11 7.0
9
DList.addLast
7
3
5
5 1.50
8 7.2
8
DList.erase
23
6 10
5 5.00 10 7.6
8
SortedList.add
21
3 13
5 1.33
9 6.2
9
Skiplist.add
19
4 16
6 3.67 12 9.6
11
Tree.add
11
0 12
5 3.00 11 10.5
11
ParentTree.add
11
0 12
5 3.00 11 10.5
11
ThreadedTree.add
151
4 82
6 4.33 17 7.8
17
Client.move
8
0
9
9 1.00 16 8.4
11
Client.createMove
46
6 21
18 1.00 33 10.1
14
Client.partition
118
18 24
19 1.00 32 11.9
15
Table 2. Analysis details for experiments. The columns list the
number of applications of the abstract post, the number of refinement steps, the size and depth of the final ART that represents the
computed fixed point, the average number of abstract states per location in the fixed point, the total number of predicates, and the
average and maximal number of predicates in a single ART node.
properties, e.g., how the set of elements stored in a data structure is
affected by the procedure.
We further performed modular verification of data structure
clients that use the interface for sets with iterators from the
java.util library. For this purpose, we annotated procedure contracts for all set operations and then used our tool to infer invariants
for the client. These invariants ensure that all preconditions of the
set operations are satisfied at call sites in the client. Furthermore
we verified functional correctness properties of the client code.
Table 1 shows a summary for a collection of benchmarks running on a 2.66 GHz Intel Core2 with 3 GB memory using one core.
Each program satisfied all of the checked properties listed for the
respective program and for each program all of the checked properties have been verified in a single run of the analysis. For reasoning about transitive closure of fields in tree-like data structures we
used MONA [28] in combination with our field constraint analysis
technique [52] for reasoning about non-tree fields. We further used
the SMT solver Z3 [18] for verifying sortedness properties. For the
the data structure clients we used only Z3. The last column in Table 1 indicates that for many of our benchmarks the verification
would not succeed without the use of counterexample-guided focus, i.e., without counterexample-guided focus the analysis would
not be able to rule out some spurious error trace and get stuck.
Note that our examples are not limited to stand-alone programs
that build and then traverse their own data structures. Instead, our
examples verify procedures with non-trivial preconditions, postconditions and representation invariants that can be part of arbitrarily large code.
Further details of the benchmarks are given in Tables 2 and
3. Table 3 gives details on the calls to the validity checker and
its underlying decision procedures. One immediately observes that
the calls to the validity checker are the main bottleneck of the
analysis. On average, 98% of the total running time is spent in
the validity checker. The reasons for the high running times are
diverse. First, communication with decision procedures is currently
implemented via files which is slower than passing data directly.
Second, we use expensive decision procedures such as MONA. In
benchmark
total
#VC calls
DP cache
rel. time spent in VC
time/DP call
total
abstr. refine. avrg. max.
List.traverse
41
20 51.22% 92.59% 92.59% 0.00% 0.005 0.012
List.init
132
69 47.73% 95.93% 72.67% 23.26% 0.010 0.024
List.create
189
68 64.02% 95.36% 57.22% 38.14% 0.011 0.016
List.getLast
158
56 64.56% 97.74% 54.89% 42.86% 0.009 0.028
List.contains
114
52 54.39% 95.45% 55.30% 40.15% 0.010 0.028
List.insertBefore
246 143 41.87% 97.25% 80.61% 16.64% 0.017 0.052
List.append
311 254 18.33% 99.46% 97.10% 2.37% 0.035 0.080
List.filter
820 273 66.71% 97.36% 87.20% 10.17% 0.019 0.060
List.partition
7650 3027 60.43% 99.17% 95.63% 3.54% 0.049 0.088
List.reverse
615 312 49.27% 98.55% 89.05% 9.50% 0.017 0.048
DList.addLast
161
89 44.72% 97.86% 62.57% 35.28% 0.023 0.040
DList.erase
749 484 35.38% 98.15% 81.42% 16.73% 0.036 0.224
SortedList.add
470 190 59.57% 97.89% 65.65% 32.24% 0.051 0.120
Skiplist.add
679 241 64.51% 97.52% 43.84% 53.68% 0.044 0.076
Tree.add
390 124 68.21% 99.52% 67.59% 31.94% 0.149 0.624
ParentTree.add
428 141 67.06% 99.36% 63.41% 35.94% 0.144 0.596
ThreadedTree.add 2882 619 78.52% 99.65% 91.80% 7.85% 0.720 3.816
Client.move
111
82 26.13% 97.17% 85.48% 11.70% 0.037 0.136
Client.createMove 662 393 40.63% 96.35% 33.35% 63.00% 0.101 5.428
Client.partition
2138 896 58.09% 94.92% 27.13% 67.79% 0.115 5.540
Table 3. Statistics for validity checker (VC) calls. The columns
list the total number of calls to the VC, the number of actual calls
to decision procedures and the corresponding cache hit ration, the
time spent in the VC relative to the total running time, and the
average and maximal time spent for a single VC call.
some of the examples individual calls to these decision procedures
can take up to several seconds. Running times can be improved
by incorporating more efficient decision procedures for reasoning
about specific data structures [7, 33, 54].
Limitations. The set of data structures that our implementation can
handle is limited by the decision procedures that we have currently
incorporated into our system. The use of monadic second-order
logic over trees as our main logic for reasoning about transitive
closure makes it more difficult to use our tool for verifying data
structures that admit cycles or sharing. Furthermore, our widening
technique for inferring new reachability predicates only works for
flat tree-like structures. It is not appropriate for handling nested
data structures such as lists of lists which may require the analysis
to infer nested reachability predicates.
Costs and gains of automation. In order to estimate the costs and
gains of an increased degree of automation, we compared Bohne
to TVLA [35], the implementation of three-valued shape analysis
[48]. We used TVLA version 3.0 alpha [8] for our comparison.
We ran both tools on a set of singly-linked list benchmarks. For
each example program we used the same precondition in both tools:
heaps that form a forest of acyclic, sharing free lists. For TVLA we
provided preconditions in the form of sets of three-valued logical
structures. Bohne automatically computed the abstraction of preconditions given as logical formulas. We did not use finite differencing [46] to automatically compute predicate updates in TVLA.
With finite differencing TVLA was unable to prove preservation
of acyclicity of lists in some of the examples. We therefore used
the standard abstract domain and abstract transformers for singlylinked lists that are shipped with TVLA (with standard focus as
described in [48]). This abstract domain provides high precision
for analyzing list-manipulating programs. We checked for properties that require such high precision, in order to get a meaningful
comparison. We checked for absence of null dereferences as well
as preservation of acyclicity and absence of sharing. All properties
where checked in a single run of each analysis. Both tools were
able to verify these properties for all our benchmarks.
The results of our experiments are summarized in Table 4.
The running times for Bohne are between one and two orders of
magnitude higher than for TVLA. Observe that almost all time is
benchmark
running time (in s)
avrg. #abs. states #predicates
Bohne w/o VC TVLA Bohne
TVLA Bohne TVLA
traverse
0.11 0.008 0.179
1.0
8
4
12
create
0.78 0.036 0.133
1.7
6
11
12
getLast
0.53 0.012 0.214
2.0
10
7
14
insertBefore
2.48 0.068 0.503 13.5
15
10
18
append
8.95 0.048 0.462
1.5
23
13
18
filter
5.31 0.140 0.600
2.5
19
12
18
partition
149.16 1.238 1.508
3.5
72
15
18
reverse
5.52 0.080 0.331
2.0
12
11
14
Table 4. Comparison between Bohne and TVLA. The columns list
total running times, average number of abstract states per location
in the fixed point, and total number of predicates (we refer to
the total number of unary predicates used by TVLA.). The third
column shows the running time of Bohne without the time spent in
the validity checker, i.e., this would be the total running time if we
had an oracle for checking validity of formulae that would always
return instantaneously.
spent in the decision procedure. Thus, the increase in running time
is the price that we pay for automation.
More important in the context of this work is the fact that the
space consumption of Bohne (measured in number of explored abstract states) is smaller than TVLA’s, in some examples significantly. TVLA’s focus operator eagerly splits abstract states (and
summary nodes) during fixed point computation in order to retain
high precision. This potentially leads to an explosion in the number of explored abstract states. Instead, our counterexample-guided
focus splits abstract states and abstract objects on demand, only if
the additional precision is required to rule out some spurious error
trace. We believe that this is the main reason for the smaller space
consumption of Bohne. This believe is supported by our experience
with a uniform focus operator similar to TVLA’s that we used in an
earlier implementation.
However, there are other factors that play a role, such as the fact
that Bohne’s abstraction refinement loop infers a smaller number
of predicates, compared to the fixed set of predicates that TVLA
uses. A smaller predicate set results in a smaller abstract domain.
Furthermore, the abstract domains of the two analyses are very
similar, but not equivalent. In particular, abstract objects in our
abstract states can be empty. This is in contrast to summary nodes in
TVLA’s three-valued structures which are bound to be non-empty.
The presence of empty abstract objects can result in more compact
abstractions. Hence, a more sturdy conclusion as to why the space
consumption of Bohne is smaller requires further experiments.
8. Further Related Work
Shape analysis. Most shape analyses infer quantified invariants of
heap programs, either explicitly or implicitly. We discuss some of
these techniques in the following.
Our analysis shares many ideas with three-valued shape analysis [48] which inspired our idea of the counterexample-guided
focus operator. In the previous section we presented an experimental comparison of both analyses. We now discuss additional
related work. Recent approaches enable the automatic computation
of transfer functions [36, 46] for three-valued shape analysis. Some
of these approaches are using decision procedures [55]. A method
for automated generation of predicates using inductive learning has
been presented in [37], but is not based on counterexamples. A
recent direction is the development of parameterized focus operators that are fined-tuned to specific verification tasks [40, 41, 47].
Our counterexample-guided focus is not only fine-tuned to a specific verification task but also to the individual steps of the analysis
of a specific verification task. Furthermore, this fine-tuning is performed automatically. However, the above techniques are explored
in the more challenging setting of the verification of concurrent
heap programs. Here, user-provided domain-specific knowledge is
often invaluable for obtaining an efficient analysis.
Shape analyses based on separation logic such as [13, 19, 38]
are typically tailored towards specific data structures and properties. This makes them scale to programs of impressive size [53],
but also limits their application domain. Recent techniques introduce some degree of parametricity and allow the analysis to automatically adapt to specific data structure classes [4, 24]. All of these
techniques still require manual adaptation of abstract transformers
to perform materialization for different classes of data structures.
Our focus operator performs this adaptation automatically.
Shape analysis based on abstract regular tree model checking
[10] encodes heap programs into tree transducers which can be
analyzed using automata-based program analysis techniques. The
encoding into tree transducers loses precision if the structures observed in the heap program do not exhibit some regularity. This
shape analysis can take advantage of abstraction refinement techniques that have been developed for abstract regular tree model
checking [9]. In particular, there is an automata-based version of
predicate abstraction that can be combined with abstraction refinement and provide progress guarantees. However, these refinement
techniques cannot prevent any loss of precision that is caused by the
initial encoding of a heap program into tree transducers. Also, this
approach focuses on shape invariants of data structures and does
not apply to properties such as sortedness.
Predicate abstraction.
Classical predicate abstraction [22]
can be seen as an instance of Boolean heap abstraction where
all abstraction predicates are closed formulas. Our technique of
counterexample-guided focus therefore carries over to classical
predicate abstraction.
The advantages of combining predicate abstraction with shape
analysis are clearly demonstrated in lazy shape analysis [6]. Lazy
shape analysis performs independent runs of a shape analysis algorithm, whose results are then used to improve the precision of predicate abstraction. The combined analysis implicitly infers quantified
invariants. In contrast, our analysis transcends the lazy abstraction
technique to the point where it itself becomes effective as a shape
analysis. Thus, our analysis offers the benefits of lazy abstraction
(i.e., a high degree of automation and targeted precision) also for
the heap-aware analysis.
Indexed predicate abstraction [31] uses predicates with free
variables to infer quantified invariants and is similar to our analysis.
However, indexed predicate abstraction has not yet been used for
the analysis of heap programs. Heuristics for automatic discovery
of indexed predicates are described in [32]. The abstract domain of
our analysis is more general than the indexed predicate abstraction
domain, because it contains disjunctions of universally quantified
statements. The presence of disjunctions avoids loss of precision
at join points of the control flow graph. This is important in the
context of abstraction refinement because it enables the analysis to
precisely identify spurious error traces in the abstract system.
The SLAM tool [3] uses Cartesian abstraction [2] on top of
predicate abstraction. The loss of precision under Cartesian abstraction is not a decisive factor in standard predicate abstraction [1].
However, there are other approximations of abstract transformers
that are used for performance reasons in actual implementations.
A side-effect of such approximations is that spurious error traces
may not be eliminated by sole refinement of the abstract domain.
SLAM incorporates an algorithm developed by Das and Dill [17]
as a countermeasure. This algorithm gradually refines the abstract
transformer towards the most precise abstract post for a fixed predicate abstraction. In SLAM this algorithm is applied whenever no
new predicates can be extracted from a spurious error trace. The
refinement of the abstract transformer guarantees that the spurious
error trace is eventually eliminated. However, this algorithm is not
appropriate in the context of an abstract domain of quantified assertions. It relies on a greedy elimination of spurious abstract transitions in the abstract transformer. In predicate abstraction these abstract transitions are simple conjunctions of predicates. In our setting they are quantified Boolean combinations of predicates. The
enumeration of abstract transitions is therefore infeasible.
Quantified invariants over arrays. For programs over arrays the
problem of how to treat disjunctions in the inference of quantified
invariants is not as accentuated as in the context of heap programs.
Techniques for inferring quantified invariants that only apply to
programs over arrays include [20, 27, 29, 49]. Noticeable exceptions are [23, 50] which also apply to heap programs. Here the user
specifies predicates and templates for the quantified invariants that
partially fix the Boolean structure of the inferred invariants. The
analysis then automatically instantiates the template parameters.
The templates can significantly reduce the search-space. However,
finding the right predicates and the right templates is non-trivial in
the context of heap programs.
Extracting predicates from counterexamples. The problem of
extracting good predicates from counterexamples for a domain of
quantified assertions is orthogonal to the contribution of this paper. In the setting of quantified invariant inference, the question of
how to infer predicates from a finite set of spurious counterexamples that rule out infinitely many similar spurious counterexamples
(e.g., resulting from the traversal of a recursive data structure) is
open. In our implementation we followed a practical approach to
solve this problem. Techniques for the inference of quantified interpolants [43] offer a promising alternative. Such techniques could be
integrated in our approach following the line of interpolation-based
abstraction refinement [5, 27].
9. Conclusion
In this paper, we have addressed the automated inference of quantified invariants for software verification. The fine-tuning of the focus operator (from the related area of shape analysis) is central to
finding the right efficiency-precision tradeoff in the underlying program analysis. We have put forward the idea to use counterexamples to guide the fine-tuning of the focus operator. We have shown
how this idea can be realized in a method and tool; preliminary
experiments indicate its practical potential.
An interesting line of future research is the extension of the presented work to the verification of concurrent programs. It seems
that here one should seek the integration of counterexample-guided
focus with existing mechanisms for manually fine-tuning parameterized versions of the focus operator[39]. This would allow the
user to incorporate domain-specific knowledge (e.g., about synchronization) into the analysis.
References
[1] T. Ball, B. Cook, S. Das, and S. K. Rajamani. Refining approximations
in software predicate abstraction. In TACAS’04, pages 388–403, 2004.
[2] T. Ball, A. Podelski, and S. K. Rajamani. Boolean and Cartesian
abstraction for model checking C programs. In TACAS’01, pages 268–
283, 2001.
[3] T. Ball and S. K. Rajamani. The SLAM project: debugging system
software via static analysis. In POPL’02, pages 1–3, 2002.
[4] J. Berdine, C. Calcagno, B. Cook, D. Distefano, P. O’Hearn, T. Wies,
and H. Yang. Shape analysis for composite data structures. In CAV’07,
pages 178–192, 2007.
[5] D. Beyer, T. A. Henzinger, R. Majumdar, and A. Rybalchenko. Path
invariants. In PLDI, pages 300–309, 2007.
[6] D. Beyer, T. A. Henzinger, and G. Théoduloz. Lazy shape analysis. In
CAV’06, pages 532–546, 2006.
[7] J. D. Bingham and Z. Rakamaric. A logic and decision procedure for
predicate abstraction of heap-manipulating programs. In VMCAI’06,
pages 207–221, 2006.
[8] I. Bogudlov, T. Lev-Ami, T. W. Reps, and M. Sagiv. Revamping
TVLA: Making Parametric Shape Analysis Competitive. In CAV’07,
pages 221–225, 2007.
[9] A. Bouajjani, P. Habermehl, A. Rogalewicz, and T. Vojnar. Abstract
regular tree model checking. ENTCS, 149(1):37–48, 2006.
[10] A. Bouajjani, P. Habermehl, A. Rogalewicz, and T. Vojnar. Abstract
regular tree model checking of complex dynamic data structures. In
SAS’06, pages 52–70, 2006.
[11] R. E. Bryant. Graph-Based Algorithms for Boolean Function
Manipulation. TC, 35(10):677–691, 1986.
[12] S. Chaki, E. M. Clarke, A. Groce, S. Jha, and H. Veith. Modular
verification of software components in C. In ICSE’03, pages 385–395,
2003.
[13] B.-Y. E. Chang, X. Rival, and G. C. Necula. Shape analysis with
structural invariant checkers. In SAS’07, pages 384–401, 2007.
[14] E. M. Clarke, O. Grumberg, S. Jha, Y. Lu, and H. Veith.
Counterexample-Guided Abstraction Refinement. In CAV’00, pages
154–169, 2000.
[15] P. Cousot and R. Cousot. Abstract interpretation: a unified lattice
model for static analysis of programs by construction or approximation
of fixpoints. In POPL’77, pages 238–252, 1977.
[16] P. Cousot and R. Cousot. Formal language, grammar and setconstraint-based program analysis by abstract interpretation. In
FPCA’95, pages 170–181, 1995.
[17] S. Das and D. L. Dill. Successive approximation of abstract transition
relations. In LICS’01, pages 51–60, 2001.
[18] L. de Moura and N. Bjørner. Z3: An Efficient SMT Solver. In
TACAS’08, 2008.
[19] D. Distefano, P. O’Hearn, and H. Yang. A local shape analysis based
on separation logic. In TACAS’06, pages 287–302, 2006.
[20] C. Flanagan and S. Qadeer. Predicate abstraction for software
verification. In POPL’02, pages 191–202, 2002.
[21] D. Gopan, T. W. Reps, and S. Sagiv. A framework for numeric analysis
of array operations. In POPL’05, pages 338–350, 2005.
[22] S. Graf and H. Saı̈di. Construction of Abstract State Graphs with
PVS. In CAV’97, pages 72–83, 1997.
[23] S. Gulwani, B. McCloskey, and A. Tiwari. Lifting abstract interpreters
to quantified logical domains. In POPL’08, pages 235–246, 2008.
[24] B. Guo, N. Vachharajani, and D. I. August. Shape analysis with
inductive recursion synthesis. In PLDI’07, pages 256–265, 2007.
[25] T. A. Henzinger, R. Jhala, R. Majumdar, and G. Sutre. Lazy
Abstraction. In POPL’02, pages 58–70, 2002.
[26] T. A. Henzinger, R. Jhala, R. Majumdar, and G. Sutre. Software
verification with B LAST. In SPIN 03: Model Checking of Software,
pages 235–239. 2003.
[27] R. Jhala and K. L. McMillan. Array abstractions from proofs. In
CAV’07, pages 193–206, 2007.
[28] N. Klarlund and A. Møller. MONA Version 1.4 User Manual. BRICS
Notes Series NS-01-1, University of Aarhus, January 2001.
[29] L. Kovacs and A. Voronkov. Finding loop invariants for programs
over arrays using a theorem prover. In FASE’09, pages 470–485, 2009.
[30] V. Kuncak. Modular Data Structure Verification. PhD thesis, EECS
Department, Massachusetts Institute of Technology, February 2007.
[31] S. K. Lahiri and R. E. Bryant. Constructing quantified invariants via
predicate abstraction. In VMCAI’04, pages 267–281, 2004.
[32] S. K. Lahiri and R. E. Bryant. Indexed predicate discovery for
unbounded system verification. In CAV’04, 2004.
[33] S. K. Lahiri and S. Qadeer. Back to the future: revisiting precise
program verification using SMT solvers. In POPL’08, pages 171–182,
2008.
[34] P. Lam, V. Kuncak, and M. Rinard. Hob: A tool for verifying data
structure consistency. In CC’05, 2005.
[35] T. Lev-Ami. TVLA: A Framework for Kleene Based Logic Static
Analyses. Master’s thesis, Tel-Aviv University, Israel, 2000.
[36] T. Lev-Ami, N. Immerman, and M. Sagiv. Abstraction for shape
analysis with fast and precise transformers. In CAV’06, pages 533–546,
2006.
[37] A. Loginov, T. Reps, and M. Sagiv. Abstraction refinement via
inductive learning. In CAV’05, 2005.
[38] S. Magill, J. Berdine, E. M. Clarke, and B. Cook. Arithmetic
strengthening for shape analysis. In SAS’07, pages 419–436, 2007.
[39] R. Manevich. Partially Disjunctive Shape Analysis. PhD thesis,
Tel-Aviv University, Israel, 2009.
[40] R. Manevich, J. Berdine, B. Cook, G. Ramalingam, and M. Sagiv.
Shape analysis by graph decomposition. In TACAS’07, pages 3–18,
2007.
[41] R. Manevich, M. Sagiv, G. Ramalingam, and J. Field. Partially
disjunctive heap abstraction. In SAS’04, pages 265–279, 2004.
[42] M. Marron, D. Kapur, D. Stefanovic, and M. V. Hermenegildo. A
static heap analysis for shape and connectivity. In LCPC, pages 345–
363, 2006.
[43] K. L. McMillan. Quantified invariant generation using an interpolating
saturation prover. In TACAS’08, volume 4963, pages 413–427, 2008.
[44] A. Podelski and A. Rybalchenko. ARMC: the logical choice for
software model checking with abstraction refinement. In PADL’07,
pages 245–259, 2007.
[45] A. Podelski and T. Wies. Boolean Heaps. In SAS’05, pages 267–282,
2005.
[46] T. Reps, M. Sagiv, and A. Loginov. Finite differencing of logical
formulas for static analysis. In ESOP’03, pages 380–398, 2003.
[47] N. Rinetzky, A. Poetzsch-Heffter, G. Ramalingam, M. Sagiv, and
E. Yahav. Modular shape analysis for dynamically encapsulated
programs. In ESOP’07, pages 220–236, 2007.
[48] M. Sagiv, T. Reps, and R. Wilhelm. Parametric shape analysis via
3-valued logic. TOPLAS, 24(3):217–298, 2002.
[49] M. N. Seghir, A. Podelski, and T. Wies. Abstraction refinement for
quantified array assertions. In SAS’09, 2009.
[50] S. Srivastava and S. Gulwani. Program verification using templates
over predicate abstraction. In PLDI’09, 2009.
[51] T. Wies. Symbolic Shape Analysis. PhD thesis, University of Freiburg,
Freiburg, Germany, 2009.
[52] T. Wies, V. Kuncak, P. Lam, A. Podelski, and M. Rinard. Field
Constraint Analysis. In VMCAI’06, 2006.
[53] H. Yang, O. Lee, J. Berdine, C. Calcagno, B. Cook, D. Distefano, and
P. W. O’Hearn. Scalable shape analysis for systems code. In CAV’08,
pages 385–398, 2008.
[54] G. Yorsh, A. M. Rabinovich, M. Sagiv, A. Meyer, and A. Bouajjani. A
Logic of Reachable Patterns in Linked Data-Structures. In FOSSACS’06,
pages 94–110, 2006.
[55] G. Yorsh, T. Reps, and M. Sagiv. Symbolically computing mostprecise abstract operations for shape analysis. In TACAS’04, 2004.
[56] K. Zee, V. Kuncak, and M. Rinard. Full Functional Verification for
Linked Data Structures. In PLDI’08, pages 349–361, 2008.
[57] K. Zee, V. Kuncak, and M. C. Rinard. An integrated proof language
for imperative programs. In PLDI’09, pages 338–351, 2009.