On abstracting the procedural behaviour of logic programs - Core

0 downloads 0 Views 1MB Size Report
Supported by the project RFO-AI-02 : "Logic as a basis for ~ificial intelligence ... of the concrete execution of programs involves two levels of abstraction. .... optimizations, in particular safe specializations of PROLOG predicates [21]. ... version of the predicate definition if the difference is substantial enough for the compiler.
On abstracting the procedural behaviour of logic programs.

G. Janssens , M. Bruynooghe Department of Computer Science, K.U.Leuven Celestijnenlaan 200A B-3001 Heverlee Belgium

ABSTRACT

Abstract interpretation is a widely applied method for doing static analysis of logic programs. A diversity of formalisms and applications have appeared in the literature. This paper describes at a rather informal level our formalism based on AND-OR-graphs and compares it with the approach based on denotational semantics.

* Supported by the project RFO-AI-02 : "Logic as a basis for ~ificial intelligence : conlxol and efficiency of deduclive inference - parallelism " ** Supported by the Belgian National Fund for Scientific Research.

241

Introduction

In general abstract interpretation aims at computing at compile time certain features of the concrete execution of a program. Applying abstract interpretation to extract features of the concrete execution of programs involves two levels of abstraction. The first one consists of the development of a framework, a parametrized construction for the analysis of programs in a given language, together with theorems that ensure the soundness and termination of the analysis provided that the domains and operations - the parameters supplied to complete the construction obey certain safety requirements. During this process the actual application does not have to be taken into account - it determines the second level of abstraction. Frameworks can be formalized for particular languages or language classes. Our framework, for example, is developed for logic programs [2]. Such a framework offers a backbone for the development of a new application, which is concerned with the second level of abstraction. We have been using abstract interpretation of logic programs for deriving information which can be used during code generation, especially for specializing versions of procedures. [3, 4, 15]. The desired code optimizations determine the required expressive power of the latter abstraction : it must offer a formalism to describe sets of concrete substitutions. Such a description is called an abstract substitution and the set of abstract substitutions an abstract domain. This paper informally discusses both levels of abstractions. We explain what the rationale is behind abstract interpretation of logic programs and in particular behind our approach, such that the reader gets an idea of what abstract interpretation is about and what it can offer him. For a more formal and precise specification of our framework, one is referred to [2-4, 15]. In section 1 we briefly sketch the general idea of abstract interpretation. Section 2 points out what kind of information about the execution behaviour is useful for code generation and thus should be derivable by abstract interpretation. The next two sections deal with the two levels of abstraction. The issues raised in section 2 allow us to fommlate some basic requirements and characteristics of a framework, as is done in section 3. We illustrate these arguments by referring to our framework and to frameworks based on the denotational semantics. In section 4, we discuss some important issues of the design of an abstract domain a.o. a property of abstract domains which makes that the abstract interpretation can be implemented efficiendy. The conclusion is found in section 5. 1. Basic principles of abstract interpretation Abstract interpretation as considered here is intended to compute a description of the execution behaviour. It is important to be aware of the need to use approximations and of the implied loss of precision. Consider the deterministic finite automaton in figure 1.1. The concrete behaviour of such an automaton can be characterized by its language, namely the set L I of strings accepted or recognized by the automaton : L1 = {0, 10, 110,..., 111...1110, --. } Note that this set is infinite and thus impractical as a characterization. The accepted strings can be partitioned according to the characters appearing in the string : the strings

242

made out of O's, denoted by the class , the strings made out of l's, denoted by the class , and the strings made out of O's and 1 's, denoted by . Suppose that we only want to distinguish between these classes.1 Then sets built from these classes can be used as a description space or abstract domain and the set ALl = {, } is the most precise description - called the abstraction - of L 1. The abstraction function cz1 maps the set of the accepted strings into the minimal set of classes needed to describe it : a l (L1) = ALl. The abstraction provides us with a finite and compact description of the language, but in general implies some loss of information. The concretization fffnction T1 maps the set ALl to the set of languages {L1,L2, 9 9 - } described by it. ALI does not only characterizes L1, but infinitely many other languages such as e.g. I-,2 = {0, 00,..., 00...0, 010, 01010,..., 010I...010}. This is illustrated in figure 1.2. The intended use of the derived information strongly affects the final degree of abstraction that is stU1 acceptable. Suppose we only want to know which characters appear in the accepted strings. Now the abstract domain consists of sets built from 0 and 1 and AL2 = {0, 1} is the best abstraction of L1 and also of L2, although even more precision is lost : A L 2 also describes L3 = {0, 00, 000,..., 000...000,...,1, 11, 111,..., 111...111,...}, while ALl did not. The requirement that the abstraction function yields the "best" description closely ties it with the concretization function. In [7] rather natural conditions have been imposed on their relationship. Before giving them, we introduce some terminology : C, the concrete domain 2 A, the abstract domain 3 a : 2 c ~ A, the abstraction function where 2 s denotes the powerset of the set S "~: A ---> 2 c, the concretization function The functions c~ and y must have the following properties : 1.

cz and 3/are monotonic.

2.

c~ and y are adjoint, i.e. a. Vc ~ C : c _ ?(cz(c)) b. Va e A : a = a(T(a))

These properties impose a structural similarity between the concrete and the abstract domain. However, this is not essential. The framework in [2] introduces only a 1. It is well known that one can develop a finite description, which is an exact characterization for the language of such an automaton, namely the automaton itself. 2. in this example, the language of the automaton (its set of accepted strings) 3. in this example, the set of classes, or the set of characters

243 concretization function that provides the elements of the abstract domain with a meaning (a semantics). The application of the absl~act interpretation technique to computer programs results in a tool for global (or interprocedural) analysis. The term program point is used in the literature to name a position in the program source at which we wish to infer properties of the program state when control reaches that point. An example of such properties could be the values of the program variables. During the execution of a program, such program points are visited in a certain order. Thus a sequence of program points is determined. Program points can be reached more than once, but the property of interest does not necessarily has the same value at each visit. So, one has to determine the property of the set of values. The properties of interest are modeled as points in the abstract domain. Abstract interpretation mimics the execution of the program by replacing concrete operations by their abstract counterparts. Suppose i is the concrete property at some program point and after the execution of a sequence P of instructions, property j holds. Let ia be ~(i), Pa the abstract counterpart of the sequence P and assume that Pa computes Ja from ia. The abstract interpretation Pa is correct i f j E 7(Ja) holds. Cousot and Cousot [7] have formalized the abstract interpretation for procedural languages. Their theory is based on denotational semantics. They define program points as points in the flowcharts and they approximate the value assignments that exist each time the execution reaches that program point. The abstract interpretation paradigm has for a long time been recognized as a powerftfi global analysis tool for procedural languages and has recently been adapted for the analysis of logic programs. 2. The motivation for deriving the information The information gathered by abstract interpretation is intended to allow code optimizations, in particular safe specializations of PROLOG predicates [21]. Consider the well-known append/3 predicate. Depending on the actual query, it tests wheflaer a specific list is the concatenation of two given lists : ?- append([1,2], [3], [1,2,3]). -

-

it constructs a list that is the concatenation of two given lists : ?- append([1,2], [3], L).

- - it splits a given list into two : ?- append(L1, L2, [1,2,3]). The code for append/3 must be general enough to deal with these classes of queries and many more; it must have the power of general unification. Nevertheless, a typical characteristic of a WAM-based PROLOG compiler [29] is that it seldom uses the general unification algorithm for unifying a call and a head, but performs a case analysis. Special instructions are generated which are adapted to the arguments appearing in calls and clause heads. Further improvements of the code are possible if more information about the arguments of a query or a call is available. It is well known that mode declarations aUow for a substantial speed-up [28]. In the same vein, information about the possible

244

values for the actual arguments is very useful [15,21]. Methods for gathering this additional information at compile time are based on abstract interpretation : they compute safe descriptions of all possible call patterns of the predicates. Indeed, the same predicate can be called in different ways and thus have distinct call patterns. Therefore, the context of a call is relevant. Each call pattem can be linked to a specific version of the predicate definition if the difference is substantial enough for the compiler. Consider the following example. P(...) :- ..., { 0i } append(X, Y, Z), .... Q(...) :-..., { 02 } append(X, Y, Z), ....

{construct a list} {splits a list }

append(...). append(...) :- ..., { 03 } append(U, V, W). Here 01, 0 z and 03 are the concrete substitutions describing the values of the variables just before the call of append/3 in the body of P, just before the call of append/3 in the body of Q and just before the recursive call of append/3 in its recursive clause. Suppose that the properties of 01 and 02 are substantially different. One approach could be to have a single version of append/3 that can deal with both call patterns. This would lead towards the general case. The other approach is to have two different versions of append/3 : one corresponding to the 01 call pattern and the other to the 02 call pattern. The former one will be used if the initial call to append/3 has the same properties as 01 and the latter if its properties are the same as 07_, as the properties of 03 depend on the initial call to append/3. The latter approach leads to the idea of multiple specialization [31]. Summarizing, we can say we need all possible call patterns for the predicates and also information about their context. 3. The first level of abstraction : the framework 3.1 Some requirements The aim of abstract interpretation is to obtain valid descriptions of the procedural behaviour o5 logic programs. The basic idea is to start from a formal description of the procedural semantics and derive an abstraction from it, which is finite, compact and informative. There are some important differences with the approach based on flow-charts which has been used for procedural languages. Firstly, the dataflow is bi-dixectional as unification is more powerful than the parameter binding mechanisms of procedural languages. Secondly, we must be able to deal with nondeterminism : the call of a predicate can be solved by more than one of its defining clauses. The backtracking mechanism assures that all clauses are tried. Moreover, once we use descriptions of sets of concrete substitutions, it is even more plausible that more than one clause is applicable. Thirdly, most of the data structures in logic programming languages are recursive. Due to the logic programming paradigm, simple operations on them, which in the procedural languages would be implemented by loops (iterations), now are defined recursively.

245

Each (recursive) call to a predicate uses fleshly renamed versions of the defining clauses. During the execution of a recursively defined predicate the set of variables increases with each call. In other words, each call has a different context. So, it is difficult to detect similar or identical calls. This is a crucial operation in the course of the abstract interpretation, which allows to construct a finite description (more details are found in section 3.2). Whereas, the code in the loops of procedural languages has always access to the same set of variables, namely those in its scope. Let us try to identify the aspects of the procedural behaviour that have to be described by the abstract interpretation method. The execution of a program P for a given query Q 0,t results in a (possibly empty) set of solutions { 0 ql s . . . . . 0% s }, which are enumerated during backtracking : C

Iq I~ I

ql)

-

.

.

c

~ l

)

$

where 0q is the concrete call-substitution of Q and 0~ a concrete success-substitution of

Q. For code generation we want this kind of information about all called predicates and even about all possible ways a predicate is called in. This is called the extended execution [23]. 0q

/0'

0;,

10;,

c

ql'

{@,,

" " "

01) , o;'o, I

9 "

9 ' 0 P~2 '~

}

It is important to notice that it is sufficient to have the values of the variables appearing in the call, so e.g. the domain of 0; 1 and of 0;1 can be limited to var(P). 4

The code will be specialized for a class of similar queries Q {0 c l , . . . , 0 ck }, rather than for just one query Q 0 c~. The design of an abstract domain is aimed at finding an adequate description 13 of a set of concrete substitutions : {0 r . . . . . 0 ck } is described by the abstract call-substitution 13c with N[3~) _~ { 0 ~ ' , . . . , 0 c~ }. It is not always possible to find an abstract substitution that describes exactly a specific set. It is the responsibility of the designer to balance the expressive power (granularity) of the abstract domain and its complexity. For each 13c the corresponding abstract success-substitution ~s will be _ {0~1. . . . , 0 m sl , , . . . , 0 ~ k . . . . . 0 m~}" ~, computed such that "1,(13s)~ We lose information about which success-substitution corresponds to which call-substitution, but this is 4. For any syntactic object o, mr(o) denotes the set of variables occurring in o.

246

reasonable as similar queries will have similar results. The outcome of an abstract execution can be characterized by a table of the following form :

P

For specializing versions this linear table is not appropriate in the sense that it does not say which versions of the predicates appearing in the body o f a clause correspond to a specific call of a predicate. The information about the calling hierarchy is indispensable (it can be obtained by an extra pass through the program). 3.2 Our framework We will sketch how our method of abstract interpretation satisfies the above requirements in a natural way. We start from the procedural semantics defined in terms of the SLDderivation [19]. The concrete execution of a program for a given query is completely described by the corresponding SLD-tree. Traditionally, a step in the SLD-derivation is expressed as follows (assuming Prolog's left to right computation rule) : Given the goal ~ (A1, A 2 . . . . , A n)0 and the clause B ~-- B 1 , . . . ,Bm. Let (r be the mgu of A10 and B. Then the next goal in the derivation is ~---(B1. . . . , B I n , A 2 , . . . ,An)00. Applying the same procedure for a number of additional derivation steps leads to the goal ~-- ( A 2 , . . . , An)0'. During the derivation the subsequent goals and the corresponding substitutions involve an increasing set of variables, which is undesirable in the case of abstract interpretation. Therefore, we introduce the idea of subderivation : Given the goal ~ (A1, A2, 9 9 9 An)0 and the clause B ~-- B 1, 9 9 -, Bm. Let (y be the mgu of A10 and B, restricted to var(B). Then a subderivation can be started for the goal ~-- (B I, 9 9 9 Bin)OSuppose its answer substitution is "c= (rp. Let }x be the mgu of A10 and B'~, restricted to var(Al 0). Then the next goal is ~-- (A2 . . . . . An)0~t. In [15] we show that ~ ( A 2 , . . . , An)0" and ~-- ( A e , . . . ,An)0~x are identical. Notice that a single call now requires two unification based operations, one corresponding to the call, as in standard SLD-resolution, and one corresponding to r e t u r n which updates the callers environment.

247

For us this approach has the advantage that -

-

-

-

activations of the same clause that are actually renamings, can be easily detected as they appear as subderivations that are identical upto renaming. (This is crucial to guarantee termination of the analysis). an alternative representation for the SLD-tree emerges by treating the subderivations separately : the AND-OR-graph. The alternative clauses of a predicate are assembled in an OR-node. For each clause, an AND-node contains the information concerning its subderivation. The derivation sketched above gives rise to the partial AND-ORgraph of figure 3.1.

The abstract execution can be described in the same way : Given the goal