Lecture Notes in Computer Science 675 Edited by G. Goos and J. Hartmanis Advisory Board: W. Brauer D. Gries J. Stoer Anne Mulkers Live Data Structures in Logic Programs Derivation by Means of Abstract Interpretation Springer-Verlag Berlin Heidelberg NewYork London Paris Tokyo Hong Kong Barcelona Budapest Series Editors Gerhard Goos Juris Hartmanis Universit~it Karlsruhe Cornell University Postfach 69 80 Department of Computer Science Vincenz-Priessnitz-Stra6e 1 4130 Upson Hall W-7500 Karlsruhe, FRG Ithaca, NY 14853, USA Author Anne Mulkers Department of Computer Science, K.U. Leuven Celestijnenlaan 200 A, B-3001 Heverlee, Belgium CR Subject Classification (1991): F.3.1, D.3.4, 1.2.2-3 ISBN 3-540-56694-5 Springer-Verlag Berlin Heidelberg New York ISBN 0-387-56694-5 Springer-Verlag New York Berlin Heidelberg This work is subject to copyright. All rights are reserved, whether the whole or part of the material is concerned, specifically the rights of translation, reprinting, re-use of illustrations, recitation, broadcasting, reproduction on microfilms or in any other way, and storage in data banks. Duplication of this publication or parts thereof is permitted only under the provisions of the German Copyright Law of September 9, 1965, in its current version, and permission for use must always be obtained from Springer-Verlag. Violations are liable for prosecution under the German Copyright Law. �9 Springer-Verlag Berlin Heidelberg 1993 Printed in Germany Typesetting: Camera ready by author/editor 45/3140-543210 - Printed on acid-free paper P r e f a c e Abstract interpretation is a general approach for program analysis to discover at compile time properties of the run-time behavior of programs, as a basis to perform sophisticated compiler optimizations. Several frameworks of abstract interpretation for logic programs have been presented 11, 25, 27, 43, 48, 49, 51, 55, 57, 65, 81, 82. A framework is a parameterized construction for the static analysis of programs, together with theorems that ensure the soundness and termination of the analysis. To complete the construction, an application specific domain and primitive operations satisfying certain safety conditions must be provided. This book elaborates on an application for such a generic framework. The framework used 11 belongs to the class of top-down abstract interpretation methods and collects the information derived in an abstract AND-OR-graph that represents the set of concrete proof trees that can possibly occur when executing the source program. The starting point of the present work is the previously developed application of integrated type and mode analysis 38. The purpose of that application was to guide the compiler, based on a characterization of the entry uses of the program, to generate code that is more specific for the calls that can occur at run time. In an a t tempt to give further guidance to the compiler, we address the prob- lem of compile-time garbage collection, the purpose of which is to (partially) shift run-time storage reclamation overhead to compile time. In applicative program- ming languages, the programmer has no direct control over storage utilization, and run-time garbage collection is necessary. Garbage collection involves a pe- riodic disruption of the program execution, during which usually a marking and compaction algorithm is employed. Such schemes are expensive in time. Our research shows that at compile time useful and detailed information about the liveness of term substructures can be deduced which the compiler can use to improve the allocation of run-time structures. In fact, it provides a technique to automatically introduce destructive assignments into logic languages in a safe and transparent way, thereby reducing the rate at which garbage cells are cre- ated. The resulting system gets near to the methods of storage allocation used in imperative programming languages. The global flow analysis to be performed on Prolog source programs in order to derive the liveness of data structures is constructed in three layers. The vI first layer, consisting of the type and mode analysis, basically supplies the logical terms to which variables can be bound. The two subsequent layers of the analysis heavily rely on these descriptions of term values. The sharing analysis derives how the representation of logical terms as structures in memory can be shared, and the liveness analysis uses the sharing information to determine when a term structure in memory can be live. Acknowledgments This book is based on my Ph.D. dissertation 59 conducted at the Department of Computer Science of the K.U.Leuven, Belgium. The research presented has been carried out as part of the RFO/AI/02 project of the Diensten voor de programmable van he~ wetenschapsbeleid, which started in November 1987 and was aimed at the study of implementation aspects of logic programming: 'Logic as a basis for artificial intelligence: control and efficiency of deductive inferencing and parallelism'. I am indebted to Professor Maurice Bruynooghe, my supervisor, for giving me the opportunity to work on the project and introducing me to the domain of abstract interpretation, for sharing his experience in logic programming~ his invaluable insights and guidance. I wish to thank Will Winsborough for many helpful discussions, for his advice on the design of the abstract domain and safety proofs and his generous support; Gerda Janssens for her encouragement and support, and for allowing the use of the prototype for type analysis as the starting point for implementing the liveness analysis; Professors Yves Willems and Bart Demoen, for managing the RFO/AI/02 project and providing me with optimal working facilities; Professor Marc Gobin, my second supervisor, and Professors Baudouin Le Charlier and Danny De Schreye, for their interest and helpful comments, and for serving on my Ph.D. thesis committee. I also want to thank my family, friends and colleagues for their support and companionship. Leuven, March 1993 Anne Mulkers C o n t e n t s In t r o d u c t i o n 1 Abstract I n t e r p r e t a t i o n 5 2.1 Basic Concepts . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.2 Abst rac t In te rp re ta t ion Framework . . . . . . . . . . . . . . . 7 2.2.1 Overview of the Framework . . . . . . . . . . . . . . . . . . . . 8 2.2.2 Concrete and Abs t rac t Domains of Subs t i tu t ions . . . . . . . . 10 2.2.3 Pr imi t ive Opera t ions . . . . . . . . . . . . . . . . . . . . . . . 11 2.2.4 Abst rac t In te rp re ta t ion Procedure . . . . . . . . . . . . . . . . 14 2.3 Example: In tegra ted Type and Mode Inference . . . . . . . . . 16 2.3.1 Rigid and In tegra ted Type Graphs . . . . . . . . . . . . . . . . 16 2.3.2 Type-graph Env i ronmen t s . . . . . . . . . . . . . . . . . . . . . 23 2.3.3 Pr imi t ive Opera t ions for Type-graph Env i ronmen t s . . . . . . 25 3 R e l a t e d W o r k 31 3.1 Aliasing and Pointer Analys is . . . . . . . . . . . . . . . . . . . 31 3.2 Reference Coun t ing and Liveness Analysis . . . . . . . . . . . . 38 3.3 Code Op t imiza t ion . . . . . . . . . . . . . . . . . . . . . . . . . 41 4 Sharing A n a l y s i s 47 4.1 Sharing Env i ronmen t s . . . . . . . . . . . . . . . . . . . . . . . 47 4.1.1 Concrete Representa t ion of Shared St ructure . . . . . . . . . . 48 4.1.2 Abs t rac t Representa t ion of Shared St ructure . . . . . . . . . . 55 4.1.3 The Concrete and Abs t rac t Domains . . . . . . . . . . . . . . . 62 4.1.4 Order Rela t ion and U p p e r b o u n d Opera t ion . . . . . . . . . . . 66 4.2 Pr imi t ive Opera t ions . . . . . . . . . . . . . . . . . . . . . . . 68 4.2.1 Unif icat ion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 4.2.1.1 X i : X j . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 4.2.1.2 X i : f ( X i l , . . . , X i j ) . . . . . . . . . . . . . . . . . . . . . . . 85 4.2.2 Procedure En t ry . . . . . . . . . . . . . . . . . . . . . . . . . . 93 4.2.3 Procedure Exit . . . . . . . . . . . . . . . . . . . . . . . . . . . 98 4.3 Eva lua t ion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 4.3.1 Example: i n s e r t / 3 . . . . . . . . . . . . . . . . . . . . . . . . 111 4.3.2 Relevance of Sharing Edges . . . . . . . . . . . . . . . . . . . . 114 v lll C O N T E N T S 4.3.3 Imprecis ion in the Sharing Analysis . . . . . . . . . . . . . . . 117 4.3.4 Efficiency of the Shar ing Analysis . . . . . . . . . . . . . . . . 123 L i v e n e s s A n a l y s i s 127 5.1 Liveness Env i ronmen t s . . . . . . . . . . . . . . . . . . . . . . 127 5.1.1 Concrete Representa t ion of Liveness In fo rmat ion . . . . . . . . 128 5.1.2 Abst rac t Representa t ion of Liveness In fo rmat ion . . . . . . . . 133 5.1.3 The Concrete and Abs t rac t Domains . . . . . . . . . . . . . . . 141 5.1.4 Order Rela t ion and Uppe rbound Opera t ion . . . . . . . . . . . 145 5.2 Pr imi t ive Opera t ions . . . . . . . . . . . . . . . . . . . . . . . 147 5.2.1 Unificat ion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147 5.2.1.1 Xi = Xj . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147 5.2.1.2 X i : f ( X i , , . . . , X i j ) . . . . . . . . . . . . . . . . . . . . . . . 153 5.2.2 Procedure Ent ry . . . . . . . . . . . . . . . . . . . . . . . . . . 154 5.2.3 Procedure Exit . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 5.3 Eva lua t ion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165 5.3.1 Example: q s o r t / 3 . . . . . . . . . . . . . . . . . . . . . . . . . 165 5.3.2 Precision of the Liveness Analysis . . . . . . . . . . . . . . . . 168 5.3.3 The Pract ical Usefulness of Liveness In fo rmat ion . . . . . . . . 171 6 C o n c l u s i o n 179 A p p e n d i x ; D e t a i l e d E x a m p l e s 183 A.1 List of Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184 A.2 a p p e n d / 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 A.3 n rev /2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188 A.4 bu i ld t ree /2 and inse r t /3 . . . . . . . . . . . . . . . . . . . . . . 193 A.5 p e r m u t a t i o n / 2 and select/3 . . . . . . . . . . . . . . . . . . . . 196 A.6 sp l i t /3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199 A.7 qsor t /2 and p a r t i t i o n / 4 . . . . . . . . . . . . . . . . . . . . . . 202 A.8 sameleaves/2 and profile/2 . . . . . . . . . . . . . . . . . . . . 205 A.9 sif t /2 and remove/3 . . . . . . . . . . . . . . . . . . . . . . . . 209 B i b l i o g r a p h y 213 Chapter 1 I n t r o d u c t i o n In conventional languages, such as C or Pascal, the programmer explicitly con- trols the utilization of memory by means of declarations and destructive assign- ments. For example, when reversing a linear list L, the list cells of the original list can be reused to construct the reversed list in the case that the original list is no longer needed for further computat ions. It is up to the programmer to decide whether he needs to preserve the old list intact and construct a reversed list which has only the list elernertLs in common with the list L (e.g. Rev_L1 in Figure 1.1), rather than reuse the list-constructor cells of L as well (e.g. Rev_L2). _1 I -J I _1 -, , I , - , , I , -~ , I i Rev_L2 / \ " / \ " -, RevL 1 eJ / Figure 1.1: Reversing a linear list. Applicative languages, in their pure form, do not have destructive assign- ments. Also type declarations are often absent. The declarative nature of these languages is often cited as an impor tant advantage, which allows programmers to focus on the logic of the problems they have to solve, rather than on more technical aspects such as search control and efficient memory usage. Unfortu- nately, the performance of current implementat ions of applicative languages does not compare well with procedural languages yet. To achieve better utilization of memory, global flow analysis techniques are being developed that are concerned with determining the type and liveness of da ta structures that are dynamical ly 2 C H A P T E R 1. I N T R O D U C T I O N append (nil, _Y, _Y). append(_E I _UJ,_Y,_E I _W) :- append(_U,_Y,_W). nrev(nil, nil). nrev(_E I _U, _Y) :-nrev(_U, _KU), append(_KU, ~, ~). Program 1.h n r e v / 2 (Naive reverse) created during program execution. Knowledge about the lifetime of da ta struc- tures guides the compiler in the generation of target code to reuse heap storage tha t is no longer accessible from program variables, i.e. to introduce destructive operations and avoid the copying of data structures that have no subsequent references. In this book, we address the problem of liveness analysis for the class of pure Horn clause logic programs. The language considered has a countable set of variables (Vats), and countable sets of function and predicate symbols. A term is a variable, a constant, or a compound term f ( Q , . . . , t , ~ ) where f is a n-ary function symbol and the t~ are terms. An atom has the form p ( Q , . . . , tin) where p is a m-ary predicate symbol and the ti are terms. A body is a (possibly empty) finite conjunction of atoms, written A1, . . . , A,~. A clause consists of an a tom (its head) and a body and is written A : - B. A program consists of a finite number of clauses. A query or goalconsists of a body only, written 7- B. We assume that the reader is acquainted with the basic terminology of logic programming and the execution mechanism of Prolog which is based on unification and backtracking. Features such as assert and retract are not considered, i.e. we assume that any source code for the predicates that can be executed at run t ime is available to the compiler. The handling of da ta structures is very flexible in Prolog. Data manipulat ion (record allocation as well as record access and parameter passing) is achieved entirely via unification. An optimizing compiler can translate general unification to more conventional memory manipulat ion operations if information is available about the mode of use of the predicates. When at run t ime a compound term becomes accessible for the first time, we can say the term is being constructed. When a pat tern is matched against a compound te rm that is already accessible, we can say the components of the term are being selected. Integrated type and mode analysis in many cases allows to predict at compile t ime whether a unifi- cation is a selection rather than a construction operation. Selection statements in particular are good candidates to check for the possible creation of garbage cells, i.e. cells that have no further references. Consider the Prolog Program 1.1 for naive list reversal. We use the conven- tion that variable names start with an underscore. If we assume that queries to n r e v / 2 are restricted to have as first argument a list that is no longer referenced after the call, and as second argument a free variable to return the output, then it is possible to generate target code for this program that allocates no new list-constructor cells, but rather reuses the list cells of the first argument. In- deed, under the assumption, the integrated type and mode analysis will infer tha t each call to the recursive clause of n r e v / 2 has as its first a rgument a list, and as second argument a free variable. The unification of the call with the clause head selects the head and tail of the first argument list. The principal list-constructor cell of this list on the contrary has no subsequent references in the clause following the unification of the call with the clause head. This means that the compiler can recognize the principal list cell as garbage and generate target code tha t reuses it. For instance, consider the caI1 to append/3 made by the same clause. A single element list _E needs to be constructed. Instead of allocating a new cell, the compiler can reuse the garbage cell that was detected. Note that the problem is more complex if there may be multiple references to the cells of the input list. Most implementat ions of unification unify a variable and a compound structure by making the variable a reference to the structure - not a copy of the structure. The representations in memory of the logical terms to which variables can be bound typically share some of their structure: while the denoted terms make up a forest of trees, their representations form a more general directed acyclic graph. This is why in general the sharing analysis plays a crucial part in the liveness analysis. In the above example, we can also infer that , the first two arguments in each call to append/3 will be lists and that the third argument will be a free variable. Again, it is possible to detect that, after invocation of the recursive clause of append/3 , the principal cell of the first argument is garbage and can be reused to construct the value of the third (output) argument. Thus, all list con- structions in this example can reuse garbage list ceils, eliminating all allocation operations. Since the reused cells would otherwise be garbage, we have elim- inated the garbage-collection overhead associated with the n r e v / 2 procedure. Moreover, a compiler can detect that the element field of each reused list cell already contains the value desired in the cells new use. The operations filling in these car fields can be eliminated from the generated target code. The resulting code closely resembles how a programmer using an imperative language would solve the problem of reversing a linear list of linked records. In the present work, we propose an abstract domain and operations to ana- lyze the liveness of da ta structures within a framework of abstract interpretation. Chapter 2 presents the principles of abstract interpretation for logic programs, and the application of type and mode analysis on which the domain for liveness analysis is based. In Chapter 3, we discuss work related to the application of compile-t ime garbage collection in the context of both logic and functional pro- gramming languages. In Chapter 4, we formalize an abstract interpretat ion for analyzing how the terms to which program variables are bound at run time, can share substructure in storage. We also augment the usual concrete semantics with information about sharing of te rm structures and discuss whether any im- plementat ion commitments are implied. As argued above, the sharing analysis constitutes a prerequisite for the liveness analysis. The lat ter is presented in Chapter 5. In both Chapter 4 and 5, the emphasis is mainly on the precision