4.3 The Verification
4.3.3 Symbolic Execution Rules
This section defines the symbolic execution rules used in the first step of the con- straint abstraction generation. If the program contains recursive calls to itself, the postcondition will be in a recursive (open) form.
The type of our symbolic execution is defined as
|[e]| =df AllSpec → (PSH× Int) → (PSH× Int)
where AllSpec contains all the specifications of all methods (extracted from the program Prog). The integer (label) in both input and output is used to record a
4.3. The Verification
program location where abduction is needed. If the integer remains zero after the symbolic execution of e, then the output state denotes the post-state of e. However, a positive number indicates that an abduction must have occurred and the resulting state (the abduction result) will be propagated back to the the method’s precondition by our verification, so that the next round of symbolic execution should succeed in the same location.
The foundation of the symbolic execution is the basic transition functions from a conjunctive abstract state to a conjunctive or disjunctive abstract state below:
unfold(x) =dfSH → PSH[x] Unfolding
exec(d[x]) =dfAllSpec → (SH[x] × Int) → (SH × Int)Heap-sensitive execution
exec(d) =dfAllSpec → (SH × Int) → (SH × Int) Heap-insensitive execution
where SH[x] denotes the set of conjunctive abstract states in which each element has x exposed as the head of an object node (x::chvi), and PSH[x] contains all the
(disjunctive) abstract states, each of which is composed by such conjunctive states. Here unfold(x) unfolds the symbolic heap so that the cell referred to by x is ex- posed for access by heap sensitive commands d[x] via the second transition function exec(d[x]). The third function defined for other (heap insensitive) commands d does not require such exposure of x.
For the unfolding operation unfold(x), there are two possible scenarios. If x refers to an object node in the current state σ, no unfolding is required and the exec operation can proceed directly. However, if x refers to a (user-defined) shape predicate, then unfold(x) will unfold the current state σ according to the definition of the predicate in order to expose the object node referred to by x:
isobj(c) σ ` x::chvi ∗ σ0
unfold(x)σ Ã σ
isspred(c) σ ` x::chui ∗ σ0
root::chvi≡Φ
unfold(x)σ Ã σ0∗ [x/root, u/v]Φ
4.3. The Verification
only if c is a shape predicate.
The symbolic execution of heap-sensitive commands d[x] (i.e. x.f, x.f := w, or free(x)) assumes that the unfolding unfold(x) has been done prior to the execution. The first three rules below are for normal symbolic execution where the current state is sufficiently strong for safe execution. The last two rules handle the cases where the symbolic execution fails and abductive reasoning can be used to discover missing pure information.
isobj(c) σ ` x::chv1, .., vni ∗ σ0
exec(x.fi)(T )(σ, 0) Ã (σ0∗ x::chv1, .., vni ∧ res=vi, 0)
isobj(c) σ ` x::chv1, .., vni ∗ σ0
exec(x.fi := w)(T )(σ, 0) Ã (σ0∗ x::chv1, .., vi−1, w, vi+1, .., vni, 0)
isobj(c) σ ` x::chui ∗ σ0
exec(free(x))(T )(σ, 0) Ã (σ0, 0)
isobj(c) σ 0 x::chui∗true σ∗[σ0] ¤ x::chui∗true
exec(d[x])(T )(σ, 0) Ã (σ0, lbl(d[x]))
isobj(c) σ 0 x::chui∗true σ∗[σ0] 7 x::chui∗true
exec(d[x])(T )(σ, 0) Ã (false, lbl(d[x]))
Note that the second to last rule uses an abductive reasoning (via Sleek) to dis- cover the missing numerical information σ0. Here we use a mapping lbl(−) to map
any instruction in the program being analysed to a unique positive integer label (namely the aforementioned program location). The rule changes the second ele- ment of the result to lbl(d[x]) which will be used by the verification to record the instruction causing an abduction, quits the current execution, propagates the dis- covered information back to the precondition of the current method, and restarts the symbolic execution with the strengthened precondition. The last rule covers the scenario in which the abduction fails. Then the execution cannot continue and returns (false, lbl(d[x])).
4.3. The Verification
The symbolic execution rules for heap-insensitive commands are as follows: exec(k)(T )(σ, 0) Ã (σ ∧ res=k, 0) exec(v)(T )(σ, 0) Ã (σ ∧ res=v, 0)
isobj(c)
exec(new c(v))(T )(σ, 0) Ã (σ ∗ res::chvi, 0)
t mn ((ti ui)mi=1; (ti vi)ni=1) requires Φpr ensures Φpo ∈ T ρ = [x0
i/ui]mi=1◦ [y0i/vi]ni=1 σ ` ρΦpr∗ σ0
ρo = [ri/vi]ni=1◦ [x0i/u0i]mi=1◦ [y0i/vi0]ni=1 ρl = [ri/y0i]ni=1 fresh logical ri
exec(mn(x1..xm; y1..yn))(T )(σ, 0) Ã ((ρlσ0) ∗ (ρoΦpo), 0)
t mn ((ti ui)mi=1; (ti vi)ni=1) requires Φpr ensures Φpo ∈ T ρ = [x0
i/ui]mi=1◦ [y0i/vi]ni=1 σ ` ρΦpr∗ σ0
ρo = [ri/vi]ni=1◦ [x0i/u0i]mi=1◦ [y0i/vi0]ni=1 ρl = [ri/y0i]ni=1 fresh logical ri
exec(mn(x1..xm; y1..yn))(T )(σ, 0) Ã ((ρlσ0) ∗ (ρo(Φpo∧P(u, v))), 0)
t mn...∈T ρ = [x0
i/ui]mi=1◦ [yi0/vi]ni=1 σ 0 ρΦpr∗ true σ ∧ [σ0] ¤ ρΦpr∗ true
exec(mn(x1..xm; y1..yn))(T )(σ, 0) Ã (σ0, lbl(mn(...)))
t mn...∈T ρ = [x0
i/ui]mi=1◦ [yi0/vi]ni=1 σ 0 ρΦpr∗ true σ ∧ [σ0] 7 ρΦpr∗ true
exec(mn(x1..xm; y1..yn))(T )(σ, 0) Ã (false, lbl(mn(...)))
Note that the first three rules deal with constant (k), variable (v) and object node creation (new c(v)), respectively, while the remaining rules handle method invoca- tion. The fourth rule is used for the invocation of another method mn which has already been annotated, and the call site meets the precondition of mn, as checked by the entailment σ ` ρΦpr∗ σ0. In this case, the execution succeeds and moves
on. The fifth rule is for a recursive call to the current method, similar as above except that a constraint abstraction is in place as postcondition. The last two rules are for the cases where the call site cannot establish the precondition of the callee method and where abductive reasoning is employed. In both cases, the execution discontinues. The sixth rule returns the abduction result σ0, which is a pure formula
4.3. The Verification
and will be propagated back by the verification to strengthen the caller method’s precondition. The last rule captures the scenario in which the abduction fails. Note that the operator ◦ is used to compose two substitutions: the substitution ρ2◦ρ1
works by first applying ρ1 and then ρ2.
To keep presentation simple, we assume there are no mutual recursions in the pro- grams to analyse; therefore each method to be analysed should only call itself re- cursively. This assumption does not lose generality, as we can always transform mutual recursion into single recursion (Rubio-S´anchez et al.,2008) to have only one constraint abstraction Q in our verification for one method.
The following rule for all commands signifies that when starting from a configuration in which the second element is positive (i.e. a faulty state), the execution will not change the state. This rule is used to skip all remaining instructions when abductive reasoning is used as a new round of symbolic execution with strengthened precondition should be started instead:
l > 0
exec(−)(T )(σ, l) Ã (σ, l)
We can now lift unfold’s domain to PSH using the following operation unfold†:
unfold†(x)_σi =df
_
(unfold(x)σi)
and similarly for exec:
exec†(d)(T )(Wσ
i, l) =df (
W
σ0
i, max{li}) where (σ0i, li)=exec(d)(T )(σi, l)
The symbolic execution rules for program constructors e can now be defined using the lifted transition functions above. Firstly, no change will be made if starting from a faulty state, as the first rule shows. In all other cases, the symbolic execution
4.3. The Verification
transforms one abstract state to another w.r.t. the program instruction:
|[−]|T(∆, l) =df (∆, l), where l > 0
|[d[x]]|T(∆, 0) =df exec†(d[x])(T )(unfold†(x)∆, 0) |[d]|T(∆, 0) =df exec†(d)(T )(∆, 0)
|[e1; e2]|T(∆, 0) =df |[e2]|T ◦ |[e1]|T(∆, 0)
|[v := e]|T(∆, 0) =df [v1/v0, r1/res](|[e]|T(∆, 0)) ∧ v0=r1, fresh v1, r1
(∆0
1, l1) = |[e1]|T(v∧∆, 0) (∆02, l2) = |[e2]|T(¬v∧∆, 0) |[if (v) e1 else e2]|T(∆, 0) =df (∆01∨ ∆02, max{l1, l2})