• No results found

Specification Synthesis for Auxiliary Methods

4.4 Related Work

5.4.2 Specification Synthesis for Auxiliary Methods

For auxiliary procedures, as we do not expect the user to provide specification annotations, we conduct a pre-analysis (Figure 5.6 andFigure 5.7) to synthesise the pre- and post-shapes before invoking the constraint abstraction generation algorithm (Figure 4.4). Loops are dealt with by analysing their tail-recursive versions in the same way.

The pre-shape synthesis algorithm SynPre (Figure 5.6) takes in as input the set of shape predicates (S), the auxiliary method name (f ), its formal parameters (u, v), the current symbolic state in which f is called (σ), and the corresponding actual parameters (x, y) of the invocation. The algorithm first obtains possible shape

5.4. The Verification Algorithm SynPre(S, f, u, v, σ, x, y) 1 C := ShpCand(S, u, v) 2 for σC ∈ C do 3 if σ 0 [x/u, y/v]σC 4 then C := C\{σC} 5 end if 6 end for 7 return C end Algorithm

Figure 5.6: Precondition synthesis algorithm.

candidates from the parameters u, v with ShpCand (line 1), tests for each shape whether it is a sound abstraction for the method’s pre-shape with entailment (line 3), then picks up a sound abstraction for the method’s pre-shape with entailment, and filter out the ones which fail (line 4). Finally the pre-shape abstraction is returned. While we use an enumeration strategy here, the number of possible shape candidates per type is small as it is strictly limited by what the user provides in the primary methods, and further filtered and prioritised by our system.

To synthesise post-shapes (SynPost inFigure 5.7), we also assign C as possible shape candidates (line 1). We unroll f ’s body e once (i.e. replace recursive calls to f in e with a substituted e) and symbolically execute it with the algorithm in Figure 4.5 (line 3), assuming f has a specification requires Φpr ensures false (line 2). The

postcondition false is used to ensure that the execution only considers the effect of the program branches with no recursive calls (to f itself). We then use ∆ to find out appropriate abstraction of post-shape (line 5), which is paired with Φprand returned

5.4. The Verification

Algorithm SynPost (T , S, f, e, Φpr, u, v)

1 C := ShpCand(S, u, v)

2 T0 := T ∪ {f (u, v) requires Φ

pr ensures false {e}}

3 ∆ := Symb Exec(T0, f, syn unroll(f, e), Φ pr)

4 for σC ∈ C do

5 if ∆∧[σ] 7 σC then C := C\{σC} end if

6 end for

7 return pair spec list(Φpr, C)

end Algorithm

Figure 5.7: Postcondition synthesis algorithm.

as result. The function pair spec list forms an ordered list of pre-/post-shape pairs, each of which has Φpr as pre-shape and a Φpo in C as post-shape.

As can be seen, the generation of possible shape candidates plays an important role in our synthesis. Its implementation is a recursive algorithm, each recursion of which decides whether or not to include a given variable and its shape in the produced separation conjunction as result. The algorithm is listed inFigure 5.8.

This algorithm is the foundation of ShpCand. It invokes length to obtain the length of a list, and isCompatible(v, S) is a type checker to test whether variable v’s type is consistent with the shape predicate S, namely, whether the recursive branches of

S’s definition look like root::chvi ∗ . . . where c is the type of v. Therefore, based

on ShpCandRec, the definition of ShpCand is straightforward as follows:

ShpCand(S, u, v) =df ShpCandRec(S, concat(u, v), 1)

5.4. The Verification Algorithm ShpCandRec (S, v, d) 1 R := ∅ 2 if d = length(v) then 3 for S ∈ S do 4 if isCompatible(vd, S) then

5 R := R∪{vd::Shui}, where u are fresh logical variables

6 end if 7 R := R∪{emp} 8 end for 9 else 10 R0 := ShpCandRec(S, v, d+1) 11 for σ ∈ R0 do 12 for S ∈ S do 13 if isCompatible(vd, S) then

14 R := R∪{vd::Shui ∗ σ}, where u are fresh logical variables

15 end if 16 end for 17 R := R∪{σ} 18 end for 19 end if 20 return R end Algorithm

5.4. The Verification

with type Node, and the user has defined two shape predicates llB and sllB with Node, then the list of all possible shape candidates for the two variables (C) will be [x::sllBhSi ∗ y::sllBhTi, x::llBhSi ∗ y::sllBhTi, x::sllBhSi ∗ y::llBhTi, x::llBhSi ∗ y::llBhTi, x::sllBhSi, y::sllBhSi, x::llBhSi, y::llBhSi, emp]. Then elements of this list will be checked against appropriate abstract states (line 3 inFigure 5.6and line 5 inFigure 5.7) where most elements should be reduced because they are not sound abstractions. For example, in the previous list, only x::llBhSi ∗ y::llBhTi remains in the list and participates in further verification. Meanwhile, for any candidate variable, ShpCand only picks up compatible shape predicates from S, which reduces more shape candidates. For instance, if the data structure manipulated by the method is of type Node, then ShpCand rules out shape predicates specifying other types of data structures, for example doubly-linked lists and trees, etc.

Our shape synthesis generally keeps only highly relevant abstractions. For the while loop inSection 5.2.2, we filtered out 24 (of 26) abstractions. Generally, in case that there are several abstractions as candidate specifications, we employ some other mechanisms to reduce them further. Firstly, we prioritise post-shapes with same (or stronger) predicates as in precondition since it is more likely that the output will have the same or similar shape predicates as the input, e.g. x is expected to remain as sllB (or stronger) if it points to sllB as input. Secondly, we employ a lazy scheme when refining the synthesised pre/post-shapes (to complete specifications). We retrieve (and remove) the pre/post-shape pair from the head of the list, (1) use the refinement algorithm (Figure 4.4) to obtain a specification for the auxiliary method, and (2) continue the analysis for the primary method. If the analysis for the primary method succeeds, we will ignore all other synthesised pre/post-shapes from the list. If either (1) or (2) fails, we will try the next one from the list. Note that our synthesis of shape specification could only cater to one predicate per parameter/result. In cases where more complex shape specifications are needed, we allow users to specify them directly for the respective auxiliary method. These

5.4. The Verification

mechanisms help to keep attempts over candidate specifications at a minimum level.

A final note on this synthesis approach is on the fact that it carries on with primary procedure’s verification until the call site of an auxiliary procedure, and then verifies the auxiliary procedure before completing the verification for the primary procedure. Therefore, it is at the expense of loss of modularity, if we consider both primary and auxiliary procedures as having equivalent status (both are procedures of the program). However, from a pragmatic perspective, we tend to regard this approach as semi-modular, since if we take every primary procedure and its affiliated auxiliary procedures as an integrated part, then this approach is modular in this sense. As long as such “integrated parts” in programs are not oversized, it will not affect the scalability of our approach.