Academia.eduAcademia.edu

A syntactic approach to program transformations

1991, ACM SIGPLAN Notices

Kid, a language for expressing compiler optimizations for functional languages is introduced. The language is -calculus based but treats let-blocks as rst class objects. Let-blocks and associated rewrite rules provide the basis to capture the sharing of subexpressions precisely. The language goes beyond -calculus by including I-structures which are essential to express e cient translations of list and array comprehensions. A calculus and a parallel interpreter for Kid are developed. Many commonly known program transformations are also presented. A partial evaluator for Kid is developed and a notion of correctness of Kid transformations based on the syntactic structure of terms and printable answers is presented.

A Syntactic Approach to Program Transformations Zena M. Ariola Aiken Computational Laboratory Harvard University Abstract: Kid, a language for expressing compiler opti- mizations for functional languages is introduced. The language is -calculus based but treats let-blocks as rst class objects. Let-blocks and associated rewrite rules provide the basis to capture the sharing of subexpressions precisely. The language goes beyond -calculus by including I-structures which are essential to express ecient translations of list and array comprehensions. A calculus and a parallel interpreter for Kid are developed. Many commonly known program transformations are also presented. A partial evaluator for Kid is developed and a notion of correctness of Kid transformations based on the syntactic structure of terms and printable answers is presented. Keywords and phrases: -calculus, Term Rewriting Systems, Contextual Rewriting Systems, Con uence, Optimizations, Correctness, Intermediate Language, Non-strictness. 1 Introduction For a number of years at MIT we have been working with an implicit parallel language called Id. Id is a high level functional language [16] augmented with a novel data structuring facility known as I-structures [5]. Id, like most modern functional languages, is non-strict, and has higher-order functions and a Milner-style type system. It also has a fairly large syntax to express currying, loops, list and array comprehensions, and pattern matching for all algebraic types. We have found it dicult to give direct operational semantics of Id because of its syntactic complexities. Kid, a kernel language for Id, has been developed to give precise (though indirect) operational semantics of Id. Kid has also proved to be an extremely useful intermediate language for the Id compiler [3]. Within the compiler, all machine in- Arvind Laboratory for Computer Science Massachusetts Institute of Technology dependent optimizations are expressed as source-to-source program transformations in Kid. Kid is a much more rened version of the language P-TAC presented earlier by the authors [2]. Kid's operational semantics and associated calculus are innovative, and given in terms of a Contextual Rewriting System (CRS) [4]. Since Kid, relative to the -calculus, is a large language, we introduce it in steps, introducing a new feature at each step. No prior knowledge of Id or CRS's is assumed on the part of the reader. However, we assume a good understanding of non-strict functional languages and some knowledge of Term Rewriting Systems (TRS). Experience of many researchers has shown that the calculus with constants is not adequate as an intermediate language for compilers of functional languages. One of its primary de ciency is the inability to capture the sharing of subexpressions. Avoiding repeated evaluation of an expression is a central concern in implementations of non-strict functional languages. Not surprisingly, graph reduction [21], [19], [13], has been one of the popular ways of implementing functional languages, but it is only recently that people have investigated suitable calculii for graph rewriting [7], [8], and [9]. Contextual Rewriting Systems, which we introduce in Section 2, represent another formalism to capture the sharing of subexpressions. The idea of having a calculus that re ects what happens operationally in a sequential implementation goes back to the work of Plotkin [18]. The core of Kid consists of the -calculus with constants and letrec or block expressions (Section 3). A block in Kid is not treated as syntactic sugar for applications; it is central to expressing the sharing of subexpressions. Kid also embodies the novel idea of multiple values (Section 4). In Section 5, we enrich our language by introducing I-structures [5], which takes us beyond the realm of pure functional languages. Kid without loops is presented in Section 6, where we also introduce the notion of printable value and answer associated with a Kid term. Informally, the answer is the maximal information that a term can produce and plays a crucial role in de ning the correctness of optimizations. We will consider an optimization to be correct if for any two programs M and N , where N is the optimized version of M , the answer of N is the same or more de ned than the answer of M . Our approach shows that term models are good enough to formulate many interesting questions about program equivalences. In Section 7 a parallel interpreter for Kid is developed. In Section 8 optimizations for Kid programs are introduced and a partial evaluator for Kid is presented. Finally loops are introduced in Kid in Section 9 and our thoughts on future work are given in Section 10. 2 Contextual Rewriting Systems (CRS) Consider the term (S f g (K x y)) from combinatory logic. It reduces to ((f x) (g x)) using the S-rule and K-rule. The redex (K x y) will be evaluated once or twice depending upon whether graph or string reduction is used. A CRS prohibits this duplication of work by assigning a unique name to each subterm and by allowing the substitution only when the subterm becomes a value. 2.1 Syntax of CRS Terms A natural way to represent a graph textually is to associate an identi er to each node, and then write all the interconnections as a recursive let-block. The terms of a CRS with signature F are described by the grammar of Figure 1 where Term is the start symbol. Superscript on a function symbol indicates its arity, and the constants are assumed to be function symbols of arity 0. The textual order of bindings in a block does not matter, that is, bi ; bj  bj ; bi . A binding in a CRS block may be viewed as an instruction because its right-hand-side (rhs) consists of an operator followed by variables and constants. When functions have arity 2, this format recalls the three-address-code used as an intermediate language by conventional compilers [1]. 2.2 A CRS for Combinatory Logic The signature F for combinatory logic contains two constants S and K, and one function symbol Apply of arity two. S and K rules for combinatory logic which are normally written as Apply(Apply(Apply(S;X );Y );Z ) ??! Apply(Apply(X;Z ); Apply(Y; Z )) Apply(Apply(K;X );Y ) ?! X 2 2 Fi 2 Constant 2 SE E Simple Expression Expression F F ::= V ariable j Constant ::= SE j Fn (SE1 ;   ;SEn ) j Block Block ::= f [Binding; ] In SE g Binding ::= V ariable = E Term ::= Block SE E Figure 1: Syntax of terms of a CRS with signature F will be written as follows in the CRS notation: T1 = Apply (S; X ) j T2 = Apply (T1 ; Y ) Apply (T2; Z ) ??! f t1 = Apply (X; Z ); t2 = Apply (Y; Z ); t = Apply (t1 ; t2 ); In tg T = Apply (K; X ) Apply (T; Y ) ??! X The separator (j) between the two bindings in the precondition of the S-rule is used to indicate that the textual order is irrelevant. All the variables that appear on the lefthand-side (lhs) of the rules or in the preconditions are metavariables that range over appropriate syntactic categories. By convention, we use capital letters for meta-variables and small letters for CRS variables. All variables that appear on the rhs of the rules are either meta-variables or \new" CRS variables, which will be usually represented by ti . Throughout the paper we will make use of the following convention regarding meta-variables: Xi ; Zi ; Yi ; Fi 2 V ariable and Constant C 2 Constant Si ; SSi ; S 0 2 Statement Ei 2 Expression where Statement is a generalization of Binding, as will be discussed later. Consider the CRS reduction of the term (S f g (K x y)) discussed earlier. As shown below, the CRS version of the S-rule guarantees that (K x y) will be evaluated only once, regardless of the reduction strategy. Apply (S; f ); f ww21 == Apply (w1 ; g); w3 = Apply (K; x); w4 = Apply (w3 ; y); w5 = Apply ({z w2 ; w4 );} | In w5 g  In the above term the two bindings in the box match the precondition of the S-rule, and the subterm () matches the lhs of the S-rule. We call  a CRS redex. The matching gives rise to the following substitution for the meta-variables of the S-rule: T1 = w1 ; T2 = w2 ; X = f ; Y = g; Z = w4 Thus, the above term rewrites to: f w1 = Apply (S; f ); w2 = Apply (w1 ; g); w3 = Apply (K; x); w4 = Apply (w3 ; y); w5 = f t01 = Apply (f; w4 ); t02 = Apply (g; w4 ); t0 = Apply (t01 ; t02 ); In t0 g In w5 g Notice that each application of the rule introduces fresh copies of the bound variables of the rhs of the rule. 2.3 Basic Rules of Contextual Rewriting System's (RCRS ) All CRS's share the following two rules, named RCRS . De nition 2.2 (Canonical form) Let N be the normal form of a term M with respect to RCRS . M , the canonical form of M, is the term obtained by deleting from N all bindings of the form x = y (where x and y are distinct variables) or x = c ( where c is a constant). De nition 2.3 ( -equivalence) Two terms M and N are said to be -equivalent, if M and N can be transformed into each other by a consistent renaming of bound variables. Intuitively the canonical form of a term corresponds to the graph associated to the term. The reader may refer to [4] for a more rigorous de nition of term graphs and alphaequivalence as rooted graph isomorphism. Lemma 2.4 Each term has a unique canonical form. 3 B -calculus The same term sharing ideas can be applied to the -calculus; for which we introduce the B -calculus, given in Figure 2. SE E Substitution rules X=Y X ?! Y X=C X ?! C where X and Y stand for distinct variables, and metavariable C stands for a constant. Block Flattening rule fX f SS1 ; SS2 ;   fX = Y ; In Y g ??! SS1 ; SS2 ;    S1 ;    Sn S1 ;    Sn In Z g In Z g = Lemma 2.1 RCRS is Strongly Normalizing and Con uent. Proof: See [2]. 2.4 Canonical Forms of terms in a CRS The following two terms have apparently di erent syntactic structure. fx = 8; t0 = fy = x; t = x + y In tg In t0 g and fx = 8; y = x; t = x + y In tg However, we consider the di erence between the above two terms merely \syntactic noise". Eliminating this syntactic noise is the motivation behind the following de nitions. Block Binding Term ::= ::= j j j j ::= ::= ::= V ariable j Constant SE F n (SE1 ;   ; SEn ) Block  V ariable : E Ap (SE1 ;SE2 ) f [Binding; ] In SE g V ariable = E Block Figure 2: Syntax of terms of B -calculus The rule in the B -calculus is expressed as follows: F =Z :E Ap (F; X ) ??! (RB[[E ]]) [X=Z ] where the notation E [X=Z ] means the substitution of X for Z in E , and the RB function is needed to rename the bound variables of E. After the discussion of function RB, given below, it will become clear that E [X=Z ] stands for naive substitution, that is, substitution where no danger of free-variable-capture exists. In the -calculus one has to deal with the problem of free-variable capture. The problem is usually solved either by making some variable convention, as for example, assuming that all free variables are di erent from the bound variables, or by adopting a variable-free notation such as that of DeBruijn [11]. According to the variable convention given in [6], the term (x:xx)(x:xx) is a legal term, while the term (y:(x: +(x;y))x is not a legal term, because the variable x appears both free and bound. The convention allows one to express application in terms of a naive form of substitution, which does not require -renaming at execution time. In a system with let-blocks, in order to avoid the free-variable capture as well as to allow naive substitution, we have to adopt an even more stringent convention: all bound variables in an expression must be unique and di erent from variables free in the whole program. In order to maintain this invariant we will occasionally rename all bound variables of a term to completely new variables explicitly, by applying the function RB to the term. For example, 0 0 RB [ fx = + (a; 1) In xg] = fx = + (a; 1) In x g At the implementation level the e ect of function RB is to make a copy of the expression. The most important thing to notice is that the -rule in the B -calculus allows variables to be substitutable, which in turn may be bound to unevaluated expressions. Thus, sharing aspects of CRS's have not ruled out non-strict implementations. However, the use of the function RB automatically rules out \context sharing", which is needed for optimal interpreters of the -calculus [14]. The S combinator can be expressed as follows in the B calculus: f:g:x:ft1 = Ap(f; x); t2 = Ap(g; x); t = Ap(t1 ; t2 ); In tg Notice, however, that Ap(S; w) produces a -term while Apply(S; w) in the SK-CRS is not reducible any further. This discrepacy can be removed by extending the B -calculus with multiple values. Arity Detection rule F = Z:E Apply (F; X ) ??! Ap (F; X ) F = ! n Zn :E Apply (F; X ) ??! Apply1 (F; n; X ) where n stands for numeral n. ! Fi = Applyi (F; n; Xi ) n>1 ! i < (n ? 1) Apply (Fi ; Xi+1 ) ??! Applyi+1 (F; n; Xi+1 ) ! Fi = Applyi (F; n; Xi ) i = (n ? 1) ! Apply (Fi ; Xi+1 ) ??! Apn (F; Xn ) Thus, Apply (S; w) will produce Apply1 (S; 3; w) and not a -term. Multiple variables play an important role in avoid- In the above de nition 3 indicates that a multiple value of arity 3 is expected as argument, and (f;g; x) is a multiple variable of arity 3. The following rule captures the application in the presence of multiple values. ing the construction of unnecessary closures. For example, without multiple values, a term like (S e1 e2 e3 ) will involve copying the body of S twice. While according to the multiple variable de nition of S the values of e1 , e2 , and e3 will be substituted directly into a single copy of S. Another important use of multiple values is in elimination of tuples which are used just to \package" several values. Consider the following binding (x; y) = f a =    ; b =    ; In (a; b)g where (a; b) on the rhs indicates a 2-tuple and (x; y) on the lhs indicates destructuring via pattern matching. Such tuples are ubiquitous and put unnecessary demands on the storage manager. It is possible to transform this binding into a multiple value binding as follows: x; y = f2 a =    ; b =    ; In a; bg where \x; y" indicates multiple variables, and the 2 after the curly brace indicates that two values are to be returned by this block expression. Thus, a binding has the form MVm = Em , where MVm stands for m multiple variables, and the expression Em on the rhs must return m values. We add the following basic destructuring rule to RCRS involving multiple values: Application rule Multivariable rule 4 Multiple Values Consider the following de nition of S using multiple values: 3 (f; g; x):f t1 = Apply(f; x); t2 = Apply(g; x); t = Apply (t1; t2 ); In tg ! ! F = !  n Zn : E ! ! ! Apn (F; Xn ) ??! (RB[[E ]]) [Xn = Zn ] where X n stands for multiple variables (X1 ;    ; Xn ), and ! ! E [Yn = Xn ] stands for E [Y1 =X1 ;   ; Yn =Xn ], which is the same as (  ((E [Y1 =X1 ]) [Y2 =X2 ])  ) [Yn =Xn ]). The following rules relate Apply to Apn using another function Applyi which behaves like a closure. ! ??! Xn = Yn (X = 1 Y1 ;   Xn = Yn ) Notice we need to generalize function Apn to be able to deal with functions that return multiple values as follows: F ! = ! n;m Zn : E ! ! Apn;m (F; Xn ) ??! (RB[[E ]]) [Xn = Zn ] However, we insist that the general Apply function always return one value. 5 I-structures Another major short coming of -calculus, or any other purely functional language, is the inability to express \efcient" translations of list and array comprehensions. The usual translations [17] are unnecessarily sequential and elude to other implementation tricks to avoid construction of intermediate data structures. We believe I-structures [5] are essential to express ecient translation of many constructs in a language such as Haskell [12] and Miranda [20]. I-structures are \write-once" structures and di er from functional data structures in the sense that an I-structure can be de ned incrementally. It is possible to de ne an array by giving its bounds only, i.e., I-array(xb ). At some other stage in the program, the de nition for an element of an array, say the ith element of array x, can be given by writing the store command P-store(x; i; z ). If two de nitions for the same element are given then some subexpression in the program may go into inconsistent state, represented by >. These aspects of I-structures are captured by the following rules. Array rules X = I array (Xb ) Bounds (X ) ??! Xb P store (X; I ; Z ) P select (X; I ) ??! Z P store (X; I ; Z ) P store (X; I ; Z 0 ) ?! >s P-store and P-select do not require bounds checking; it is as- sumed that code for bounds checking is inserted explicitly during the translation from Id to Kid. An important fact to realize is that a functional language augmented with I-structures does not preserve \referential transparency" [5]. For example: f x = I array(xb); In (x; x)g 6 (I array( b) I array( b)) x ; x It is, therefore, essential to specify rules for sharing of subexpressions to give the semantics of I-structures. CRS have the necessary expressive power for this purpose. We also need to specify how >s propagates through an expression. If this propagation is not done carefully then the addition of I-structures can destroy the con uence property of the language. For example, the expression f P store( x; i; z1 ); P store(x; i; z2 ); w = P select(x; i); In wg has four possibilities for the next reduction. These are fP store( 1 ); >s ; = P select( f>s; P store( 2 ); = P select( fP store( 1 ); P store( 2 ); fP store( 1 ); P store( 2 ); x; i; z In wg In wg w = z1 ; In wg w = z2 ; In wg x; i); w x; i; z x; i); w x; i; z x; i; z x; i; z x; i; z If we want to preserve con uence then we must guarantee that the eventual result is the same in all these cases. The following rules for propagating > were motivated by these concerns and will eventually produce > in the above example. Propagation of > ! fm >s ; 1 ;   n In m g S fm X = ??! >   n In m g ??! > S >; S1 ; Z S ! Z These rules for propagating > were motivated by a discussion with Vinod Kathail. It should be noted that the > propagation rule for a block implies that the rhs of all bindings in a block must be evaluated. This idea is in con ict with lazy evaluation of a block. However, I-structures have no impact on the non-strict semantics of a functional language. The idea of I-structures is generalizable to all functional data structures such as tuples and other algebraic types. As an example, the rules for the I-structure version of lists, the so-called open lists, are given below List rules Cons (X; Y ) ??! f t = Open cons (); Cons store 1 (t; X ); Cons store 2 (t; Y ) In tg Cons store 1 (X; Y ) Cons 1 (X ) ?! Y Cons store 2 (X; Y ) Cons 2 (X ) ?! Y Cons store 1 (X; Y ) Cons store 1 (X; Y 0 ) ??! >s Cons store 2 (X; Y ) Cons store 2 (X; Y 0 ) ??! >s We brie y describe the use of I-structures in the translation of list comprehensions. A typical translation of a listcomprehension is given in terms of nested map-list operations followed by a list attening operation [17]. In Id, we make use of \open lists" , a type of I-structure, to generate a tail recursive program. The translation of the Id list comprehension f: e || x <- xs ; y <- ysg may be given as follows: PFim and m outputs Ap Em 2 Applicative Expression with m outputs Case Em 2 Case Expression with m outputs Lambda E 2 Lambda Expression f h1 = Open cons(); hn = fFor x <? xs do Next h1 = Case ys of j Nil = h1 j y : yss = fh = Open cons(); h1 :Cons 2 = h:Cons 2; In fFor y <? ys do t = Open cons(); t:Cons 1 = e; h:Cons 2 = t; Next h = t; Finally hgg; Finally h1 g; hn :Cons 2 = Nil; In h1 :Cons 2g V ariable MV m Constant SE SE m PF 11 6.1 Figure 3: The Grammar of Kid  rules + (m; n) ??! +(m; n)  .. . Equal? (n; n) ??! True  Equal? (m; n) ??! False  Case rules Kid Rewrite Rules We now present a set of rewrite rules, RKid , which give an intuitive understanding of how Kid terms may get evaluated. We assume that a primitive function is applied only to arguments of appropriate types, i.e., the type checking has been done statically. RKid , in addition to the rules given below, contains RCRS , the Application, the Arity detection, the Array, the List, and the Propagation rules, introduced in the previous sections. j Open cons j Cons 1 j Cons 2 ::= Detuplem ::= + j    j Apply j Cons j Make tuple2 j P select ::= Applyn-2 j Make tuplen ::= Apn;m (SEn+1 ) ::= Bool casem(SE; Em ; Em ) j List casem(SE;Em ;Em ) Lambda E ::= n;m MVn:Em E1 ::= SE 1 j PF 11 (SE ) j PF 21 (SE2 ) j PFN1 (SEn ) j Lambda E j Ap E1 j Case E1 j Block1 Em ::= SE m j PF 1m (SE ) j Ap Em j Case Em j Blockm Blockm ::= fm [Statement; ] In SEm g Statement ::= Binding j Command Binding ::= MVm = Em Command ::= P store(SE3 ) j Cons store 1 (SE2 ) j Cons store 2 (SE2 ) j Store error j >s Program ::= Block1 Kid : The Kernel Id Language Kid is a CRS containing B -calculus, multiple values and I-structures. Kid also contains functions WLoop and FLoop to capture those aspects of tail recursion that are very important for optimizations. However, we postpone a discussion of loops until Section 9. The syntax of Kid is given in Figure 3. Note that the function symbol Apply appears as a PF 21 in the grammar because we assume that all user-de ned procedures return only one result. We also use subscripted function symbols to express a family of functions. For example, Make tuplen stands for Make tuple2 , Make tuple3 , etc. Subscripts in a function symbol do not necessarily represent the number of values to be returned by the application of the function. By convention, we drop the subscript when its value is one. (Exception Apply1 6 Apply). A procedure to translate Id into Kid is given in [3]. ::= x j y j z j    j a j b j    j f j   j x1 j   ::= V ariable1 ;   ; V ariablem ::= Integer j Boolean j Nil j Error j >    ::= V ariable j Constant ::= SE1 ;   ; SEm j SE;SEm?1 j   ::= Negate j Not j Bounds j I array PF 1m PF 21 PFN1 Ap Em Case Em In the above program, an open list (signi ed by h in the inner loop) is generated for each element of xs and then these open lists are \glued" together in the outer loop. 6 2 Primitive Function with i arguments Tuple rule (if m 6= n) Bool casem (True; E1 ; E2 ) ??! E1 Bool casem (False; E1 ; E2 ) ??! E2 ??! E1 List casem (Nil; E1 ; E2 ) X = Open cons () List casem (X; E1 ; E2 ) ??! E2 ! X = Make tuplen (Xn ) ! Detuplen (X ) ??! Xn Theorem 6.1 Kid is con uent upto -renaming on canon- P ical terms. h ::= Integers Booleans Error \Function" ::= List PV List Nil ::= 2 Tuple PV PV 3 Tuple PV PV PV ::= n Array Tuple PV PV ::= Atoms List Tuple Array j h j j j i j h i j h  h j j i j  i j j > Figure 4: Grammar of Printable Values The constant stands for no information. The following procedure, P , produces the printable value associated with a term.  is the list of bindings that have as rhs either a -expression or an allocator (i.e., Make tuple, I array, Open cons), and  is the list of store commands (i.e., the Istructure store). To simplify the notation, we will use x:i to refer to the ith component of any type of data structure in the store. x:i may be thought of as a location. We will also represent a block as Ss ; As ; Bs In X , where Ss represent all the store commands, As all the allocators bindings and Bs all other bindings in the block, respectively. The procedure L is used to lookup the value of a variable or a location in  and , respectively. Given a program, i.e., a closed term, M , the P rint procedure is invoked as follows: f g P rint (M ) = P [ M ] Nil Nil where M represents the canonical form of M and P is: [ Ss; As ; Bs In X ]   = [ X ] (As : ) (Ss : ) [ n]   = n [ True]   = True [ False]   = False [ ]= [ Nil]   = Nil P f P P P > P  P i g > h P h h  P i L 6.2 Printable Values and Answer of a Kid Term We now de ne the printable information associated with a term. The grammar for printable values for Kid is given in Figure 4. A precise notion of printable values is essential to develop an interpreter for Kid as well as to discuss the correctness of optimizations (see Sections 7 and 8.4, respectively). P P L Proof: See [2]. Atoms List Tuple Array PV [ X ] 8 = n Tuple ( [ X1]  ) ( [ Xn]  ) > > if (X, ) = Make tuplen (X1 ; ; Xn ) > > List ( [ X:1]]  ) ( [ X:2]]  ) > > if (X, ) = Open cons() > > < Array 2 Tuple l u ( [ X:l]  ) ( [ X:u]  ) if (X, ) = I array(Y ) and (Y, ) = Make tuple2 (l; u) > > \Function" > ! > > if (X, ) =  Xn :E or > ! > if (X, ) = Applyi (F; n; Xi ) > :  Otherwise [ Y ]   if (X.i, ) = Y [ X:i]   = if (X.i, ) = not found P i P  P i L L L L P P L L Notice that the printable value of a nite term can be an in nite tree. For example, the P rint of : x = Open cons (); Cons store 1 (x; 1); Cons store 2 (x; x); In x is List 1 List 1 , that is, an in nite list of 1's. Intuitively, the answer associated with a term is the maximum printable information that can be extracted from the term by repeatedly rewriting it. We need to de ne an order on printable values to give a precise de nition of answer. f g h h h  i   i i De nition 6.2 (Partial Order on Printable Values) Let a and b be printable values; a v b i (i) a = ; or (ii) b = >; or (iii) both a and b are Atoms and a = b; or (iv) a = hn Tuple a1  an i and b = hn Tuple b1   bn i and ai v bi 8 1  i  n; or (v) a = hList a1 a2 i and b = hList b1 b2 i and a1 v b1 and a2 v b2 ; or (vi) a = hn Array abounds a1  an i and b = hn Array bbounds b1  bn i and abounds = bbounds and ai v bi 8 1  i  n. Theorem 6.3 PV is a complete partial order with respect to v. Proof: [10]. Lemma 6.4 Given a term M, M ?! N =) P rint (M ) v P rint (N ): Pictorially: ?! M1 ?! M2 M  P rint(M ) v P rint(M1 ) v P rint(M2 )  Thus, the answer is the limit of this chain. De nition 6.5 (Reduction Graph) Given a term M, the reduction graph of M, G (M ), is de ned as G (M )  fN j M ?! ! N g: De nition 6.6 (Printable Reduction Graph) Given a term M, the printable reduction graph of M, PG (M ), is dened as PG (M )  fP rint (N ) j M ?! ! N g: De nition 6.7 (Answer) Given a term M, the answer of M, P rint (M ), is de ned as P rint (M )  t PG (M ): Notice that this notion is independent of any interpreter, i.e., the method for computing it. In order for the above de nition to make sense we need to show that the limit of the printable reduction graph exists and is unique. Theorem 6.8 8 terms M, PG (M) is directed. Proof: Follows trivially from the con uence of Kid [22]. Since PG (M)  P V is directed by theorem 6.3 it follows that the answer of a term exists and is unique. 7 A Parallel Interpreter to Compute the Answer The set of rewrite rules given in Section 6.1 per se do not de ne the operational semantics of Kid; rather the rules de ne a calculus to prove properties of programs. From a computational point of view we need to specify a strategy to apply these rules. Furthermore, the reduction strategy should be normalizing with respect to the de nition of the answer. Intuitively the interpreter should stop when it is known that no more printable information can be gotten by further reductions. The interpreter E keeps track of store commands it encounters in an I-structure store, . It also keeps track of -bindings and allocator bindings encountered in . At the start of the evaluation, both  and  are empty. Given a program M , the E interpreter is invoked as follows: E val [ M ] = E[[M ] Nil Nil The strategy presented here consists of evaluating the redexes from outside-in. In case of a block all rhs redexes are evaluated in parallel using the function ERHS described below: ERHS[[fSs; As; X1 = E1 ;   Xn = En In Xi g]]   = fSs; As; X1 = E[[E1]  ;    Xn = E[[En]   In Xi g After the application of ERHS , we substitute variables and constants and eliminate the corresponding binding from the top block. We also atten the top-level block by merging the block expressions on the rhs of bindings with the toplevel block. Since any substitution or block attening can create new redexes on the rhs of bindings in a block, the top-level block is evaluated again, if necessary. The function F &S (for atten and substitute) accomplishes this, and in addition to the new block, it returns a ag showing if any substitutions or attening was done. Thus, F&S[[fX = 1; Y = fP store (W; I; Z ); Z = X + 3 In Z g In Y g]] = fP store (W; I; Z ); Z = 1 + 3; In Z g; True The only remaining complication in evaluating a block, is the propagation of >. When a block is evaluated, all the store commands, Ss, in the block are merged with . The function CC is used to check for inconsistencies, that is, multiple writes into the same location. Thus, CC ( [ Ss) returns either a new consistent store or >s. According to the > propagation rules, if a binding of the form x = > exists in a block then that block goes to >. We assume that the F &S function returns > if it encounters such a binding. The Interpreter: E[[+ (m; n)]]   = +(m; n) E[[Equal? (n; n)]]   = True E[[Equal? (n; m)]]   = False E[[Bool casem (True; E1 ; E2 )]]   = E[[E1 ]   E[[Bool casem (False; E1 ; E2 )]]   = E[[E2]   E[[List casem (Nil; E1 ; E2 )]]   = E[[E1]   E[[List casem (X; E1 ; E2 )]]   = E[[E2 ]   if L(X , ) = Open cons () E8 [ Apply (F; X )]]   = >> E[[Ap (F; X )]]   if L(F , ) = Z :!E >< Apply1 (F; n; X ) !if L(F , ) = n Zn :E ^ n > 1 ! Apply (F; n; Xi+1 ) if L(F , ) = Applyi (F; n; Xi ) ^ > i+1 ! i < (n?1) >> E[[Apn (F; Xn)]]   if L(F , ) = Applyi (F; n; X!i) ^ : i = (n ?1) ! ! ! E[[Apn;m (F; Xn )]]   = E[[(RB [ E ] ) [Xn = Zn ]]]   ! where L(F , ) = n;m Zn :E ! ! E [ Detuplen (X )]]   =Xn if L(X , ) = Make tuple (Xn ) E [ Cons(X;Y )]]   = E[[f t = Open cons(); Cons store 1(t;X ); Cons store 2(t; Y ) In tg]]   E [ Cons 1 (X )]]   = Y if L(X , ) = Cons store 1 (X; Y ) E [ Cons 2 (X )]]   = Y if L(X , ) = Cons store 2 (X; Y ) E[[Bounds (X )]]   = Xb if L(X , ) = I array (Xb ) E[[P select (X; Y )]]   = Z if L(X , ) = P store (X; Y; Z ) E[[fSs; As; Bs In X g]]   = e where e is obtained by the execution of the following program written in pseudo-Id f blk; ? = F&S[[fSs; As; Bs In X g]]; flag = True; In If blk = > then > else fWhile flag do Suppose blk is fSs; As; Bs In X g is = CC( [ Ss); fs =  [ As; next blk; next flag = If is = >s then (>; False) else F&S[[ERHS[[blk] is fs] nally blkgg If none of the above clauses apply, then E[[E ]   = E Theorem 7.1 (Soundness) Given a term M, E val[ M ] = N =) P rint(M ) = P rint(N ): 8 Optimizations of Kid Programs Any of the Kid rewrite rules given in Section 6.1 can also be applied to an open term (that is, at compile time) and thus, viewed as an optimization. In addition, we can give some new rewrite rules which do not necessarily make sense as part of the Kid interpreter, because of their complexity and interference with other rules. Care needs to be exercised in applying some of the Kid rules at compile time, because they may cause non-termination. For example, if the applicationrule is invoked on a recursive function at compile time, the optimizer will keep producing a bigger and bigger expression and not terminate. We solve this problem by requiring some annotations by the user. This additional information is reminiscent of a mechanism, called underlining in term rewriting systems, which has the e ect of turning a set of rules into an equivalent strongly-normalizing set of rules [15]. There are also cases where we want to postpone the application of a certain Kid rule because it can obscure and, possibly, prohibit the applicability of some other optimizations because of loss of information. The cons-rule in Kid is a good example to illustrate this point. Once the cons data structure is expressed in terms of I-structures the optimizer will not be able to recognize that the cons is a functional data structure, and is, therefore, amenable for common subexpression elimination or loop code hoisting. Optimizations should be performed after type checking and after all bound variables have been assigned unique names. Applicability of certain rules requires some semantic check such as \n > 1". We write such semantic predicates above the line but following an \&". We have divided the optimizations in two categories, the ones that are applied from outside in and the one that are applied from inside out. 8.1 Outside-in Optimizations rules As discussed earlier all Kid rules except the cons-rule and the application-rule can be applied at compile time. Since the application-rule is too important an optimization, Id provides the Defsubst annotation for the user to indicate that the function is substitutable at compile time. In the translation from Id to Kid such s are underlined. Since the cons-rule is eliminated from the optimizations, we need the following additional rules to restore the fetchelimination possibilities. Similar ideas are applicable to all functional data structures, particularly the make-array primitive. Fetch Elimination X = Cons (X1 ; X2 ) Cons 1 (X ) ??! X1 X = Cons (X1 ; X2 ) Cons 2 (X ) ??! X2 X = Cons (X1 ; X2 ) List case (X; E1 ; E2 ) ?! E2 The following algebraic identities in conjunction with other normal optimizations often lead to dramatically more ecient programs. Algebraic Identities We have classi ed the algebraic identities into three groups because they have slightly di erent properties. Only the rst group preserve total correctness. Alg1 And (True; X ) ??! X Or (False; X ) ??! X + (X; 0)  (X; 1) .. . ??! X ??! X Alg2 And (False; X ) ?! False ?! True Or (True; X ) ?! 0  (X; 0) Equal? (X; X ) ?! True .. . Alg3 & m>0 X = + (X1 ; m) Less (X1 ; X ) ??! True & m>0 X = + (X1 ; m) Equal? (X1 ; X ) ??! False .. . We have shown in [2] that Alg3 rules cause the optimization rules to be non-con uent. However, this is not a serious drawback because the cases where the con uence is lost are the ones where the unoptimized program would have produced no information. Specialization Applysp is a user annotated version of Apply to indicate that a new (specialized)  de nition be generated corresponding to this closure. The rational for this annotation is that we want to avoid the generation of too many specialized functions. The user has to exercise some caution with this annotation as well because this rule can also cause non-termination in a recursive de nition. A more sophisticated strategy, that we have not explored yet, would try to avoid non-termination by computing some xpoint in case of such an annotation. ! F = n;m Zn : E & n > 1 Apply sp(F; X ) ??! ! ff = n?1;m zn!?1 :(RB[[E ] ) [zn!?1 = Z2;n ; X=Z1 ] In f g ! Thus, PE is derived from E 0 by adding the following boolean-case: PE[[Bool casem (X; E1 ; E2 )]]   = Bool casem (X; (PE[[E1 ]  ); (PE[[E2 ]]]  )) and replacing the block evaluation by the following program: PE[[fSs; As; Bs In X g]]   = e where e is obtained by the execution of the following program written in pseudo-Id f blk; ? = F&S[[fSs; As; Bs In X g]]; flag = True; If blk = > then > else f blk0 = f While flag do Suppose blk is fSs; As; Bs In X g is = CC( [ Ss); fs =  [ As; next blk; next flag = If is = >s then (>; False) else F&S[[E 0 RHS[[blk] is fs] nally blkg Suppose blk0 is fSs; As; Bs In X g is = CC( [ Ss0 ); fs =  [ As0 ; blk00 = If is = >s then (>; False) else (PERHS[[blk0 ] is fs) 00 In blk gg ! Fi = Applyi (F; n; Xi ) j F = n;m Zn : E & i < (n ? 1) Applysp (Fi ; Xi+1 ) ??! ff = n?i?1;m zn?!i?1 : ! ! ! (RB [ E ] ) [zn?!i?1 = Zi+2;n ; Xi+1 = Zi+1 ] In f g 8.2 A Partial Evaluator for Kid Applying the rules given in Section 6.1 to a term is like partial evaluation. In the following, we present an ecient partial evaluator, PE , which is based on extending the interpreter, E , given in Section 7. Let E 0 be an interpreter derived from E by disallowing the cons-rule and by restricting the application rule to underlined s. Furthermore, E 0 contains cases corresponding to all optimizations given in Section 8. Notice that to write E 0 we will also need E 0 RHS function, which is analogous to the ERHS function. The main di erence between PE and E 0 shows up in expressions that encapsulate other expressions, e.g., caseexpressions and blocks. PE , unlike E 0 , will eventually cause the evaluation of both arms of a conditional. We call a block stable when it does not contain any redexes at the top-level. Notice that when E 0 is applied to a block, it terminates when the block is stable, while PE evaluates all the rhs in the top-level block after it becomes stable, using the function PERHS : PERHS[[fSs; As; X1 = E1 ;   Xn = En In Xi g]]   = fSs; As; X1 = PE 0[ E1 ]  ;   Xn = PE 0 [ En]   In Xi g Let R be the set of rules used by the interpreter E 0 . Theorem 8.1 (Normalization Theorem) Given terms M and N, M ?! ! R N =) PE [ M ] Nil Nil = N: 8.3 Inside-out Optimization rules Common subexpression elimination is one of the best known compiler optimizations. Though it can be expressed as a rewrite rule, it requires determining if an expression is sidee ect-free (sef) as de ned below: De nition 8.2 (Side e ect free (sef)) (1) Variables and constants are sef; ! (2) all PFn;m (Xn ) , except I array, Open cons, and Apply are sef; (3) Bool Casem (X; E1 ; E2 ) is sef, if E1 and E2 are sef; similarly for List Case. (4) n;m X:E is sef, if E is sef; (5) Apn;m (F; X ) is sef, if F is bound to a sef -abstraction; Common Subexpression Elimination ! ! ! Ym = E & E is sef ^ Ym 6Xm ! ! ! ?! Xm =Ym Xm = E ? For correctness, observational equivalence is not enough. It has to be shown that no context can distinguish between optimized and unoptimized term. De nition 8.4 (Observational Congruence) Given two Lift Free Expressions Let FE (e; e0 ) return true if the expression e is free in e0 . ! ! & FE (E; n;m Zn :fm Y = E ; S In Xm g) ^ E is sef ! ! ?! n;m Zn : fm Y = E ; S In Xm g ? fm t1 = E ; ! ! t = n;m Zn : fm Y = t1 ; S In Xm g In t g This rule in conjunction with the loop rules, (see Section 9) will cause loop invariants, that is, expressions that do not depend on the nexti ed variables, to be lifted from loops. Hoisting Code out of a Conditional fn Y = E ; S In Xn!g; fn Y 0 = E ; S 0 In Xn0 g))) ! ! Bool casen(X; fn Y = E ; S In Xn g; fn Y 0 = E ; S 0 In Xn0 g) ??! fn t1 = E ; ! tn = Bool casen (X; ! fn Y = t1 ; S In X!n g; fn Y 0 = t1 ; S 0 In Xn0 g) ! In tn g & FE (E; (Bool casen(X; ! All the optimizations discussed in this section need to be applied \inside-out", because one wants to nd the largest common subexpression or the largest expression to be lifted. It is dicult to give a precise algorithm for applying these optimizations because it requires choosing a data structure for Kid programs. However, an overall strategy for optimizations is given below: (1) apply PE given in Section 8.2; (2) apply the inside out rules; (3) if there is any change in the expression in step (2) go to step (1), else stop. Step (3) is needed because step (2) can enable new outsidein optimizations. For example, the cse-rule may trigger the algebraic equality rule. We believe that the above strategy is normalizing. 8.4 Correctness of Optimizations De nition 8.3 (Observational Equivalence) Given two terms M and N, M and N are said to be Observationally Equivalent i P rint (M )  P rint (N ): terms M and N, M and N are said to be Observationally Congruent i 8 C [2]; P rint (C [M ])  P rint (C [N ]) : If we let (A;R) be a CRS, where A represents the set of terms, and R a set of rules, then correctness can be formulated as follows: De nition 8.5 (Correctness) An optimizer (A,Ro) for (A,R) is correct i 8 M 2 A; M ?! ! N =) Ro  (i) P rint (M ) v P rint (N ) and (ii) P rint (N ) = > =) P rint (M ) = >: Notice that condition (ii) is needed because > is higher than all printable values and we do not want the optimizer to take all programs to >. It is believed that all optimizations presented in Section 8 preserve correctness, though this has been proven for only a small subset of them so far [2]. In general, correctness of an optimization is dicult to prove. However, some optimizations can be proven correct easily because they are derived rules. De nition 8.6 (Derived rule) A rule r is said to be de- rived in R i ! M2 ^ M1 ?! ! M2 M ?r! M1 =) 9 M2 ; M ?! R R Corollary 8.7 Given an optimizer (A,Ro) of (A,R), 8 r 2 Ro ; r is derived =) (A; Ro ) is correct : Lemma 8.8 All RKid rules, Inline Substitution and Fetch Elimination are derived rules, and hence correct. Unfortunately, it is not always possible to mimic inside RKid what an optimizations does. Therefore, we introduce a notion of tree equivalence, which is useful for proving the correctness of cse-like rules. The tree associated to a term is obtained intuitively by repeatetly substituting the rhs of a binding for each occurrence of the lhs. Let F l be the function that given a term produces the corresponding unravelled term [4]. De nition 8.9 (Tree-equivalence t ) Two terms M and N are said to be tree-equivalent, if F l(M )  F l(N ): Lemma 8.10 Given an optimizer (A,Ro) of (A,R), 8 M; N 2 A; [M ?! N =) M t N ] =) (A;Ro ) is correct Ro Corollary 8.11 Let R be the set fCommon subexpression elimination rule, lift free expressions rule, Hoisting code out of a conditional rule g, M ?! N =) M t N: R Hence cse -like rules are correct. 9 Loops Even though iteration can be expressed in terms of recursion, it is generally accepted that iterative forms can be supported more eciently on all architectures. Moreover, explicit loop expressions make it easy to express additional optimizations. We add two new function symbols to the syntax of Kid, given if Figure 3, as follows: Em ::= WLoopm (SEm+3 ) j FLoopm (SEm+4 ) j   During the translation phase from Id to Kid the predicate and loop body are transformed into -expressions. The WLoop combinator has as parameters the predicate identier (P ), the loop body identi er (B ), the nexti ed variables ! (Xn ), and the loop predicate variable. The rewrite rules for WLoop are given below: ! WLoopn (P; B; Xn ; True) ??! ! ! fn tn = Apn;n (B; Xn ); ! tp = Apn (P; tn ); !0 ! tn = WLoopn (P; B; tn ; tp) ! In t0n g ! ! WLoopn (P; B; Xn ; False) ??!Xn For the FLoop, the predicate P of the WLoop is replaced by an index variable (by convention X1 ), and an upper bound (U ), and the delta D by which the index variable is to be incremented in each iteration. The rewrite rules for FLoop are given below: ! FLoopn (U; D; B; Xn ; True) ??! ! ! fn t2;n = Apn;n?1 (B; Xn ); t1 = + (X1 ; D); tp = < (t1; U ); ! ! t0n = FLoopn (U; D; B; tn ; tp ) !0 In tn g ! ! FLoopn (U; D; B; Xn ; False) ??!Xn Loop Optimizations Peeling the Loop ! FLoopn (U; D; B; Xn ; X ) ??! ! ! Bool casen (X; fn t2;n = Apn;n?1 (B; Xn ); t1 = + (X1 ; D); tp = < (t1; U ); ! ! t0n = FLoopn (U; D; B; tn ; tp) !0 In tn g; ! Xn ) Notice that the above rule is again applicable to the loop expression generated on the rhs. To avoid unbounded number of applications of this rule the user will have to indicate how many times the loop peeling should be performed. Loop Body Unrolling (K times) & remainder ((U ? X1 )=D;k) = 0 ! FLoopn (U; D; B; Xn ; Xp ) ??! ! fn b = n;n?1 x!n : fn?1 t12;n = Apn;n?1 (B; x!n ); t11 = +(X1 ;D); ! ! t22;n = Apn;n?1 (B; t1n ); t21 = +(t11; D); .. . ! ! k t2;n = Apn;n?1 (B; tkn?1 ) ! In tk2;n g ; ! ! tn = FLoopn (U;D; b; Xn ;Xp ); ! In tn g In the above rule we suppose r = remainder((U ? X1 )=D;k), and r is not zero. We can still apply the above transformation by rst peeling the loop r times. Notice, k has to be supplied by the user. When a loop in Id is annotated to be unfolded or peeled, the translation from Id to Kid generates underlined 's for P and B . Eliminating Circulating Variables Suppose in the loop body of an Id program there exists an expression like \Next x = x " , then the variable x can be made into a free variable of the loop and its circulation can be avoided. Without loss of generality, we assume that the nexti ed variable to be eliminated is the last one. ! j P =  n Xn : E !0 ! 0 B = n;n Xn : fn S In Zn?1 ; Xn g ! WLoopn (P; B; Yn ; Yp ) ??! ! f p = n?1 xn!?1 :RB[[E ] [xn!?1 = Xn?1 ; Yn =Xn ]; ! b = n?1;n?1 x0n?1 : ! ! ! RB[[fn?1 S In Zn?1 g]] [x0n?1 = Xn?1 ; Yn =Xn0 ]; ! ! tn?1 = WLoopn?1 (p; b; Yn?1 ; Yp ) ! In tn?1 ; Yn g A similar optimization applies to for-loops. Eliminating Circulating Constants Suppose in the loop body there exists an expression like \Next x = t ", where the variable t is a free variable of the loop body, then its circulation can be avoided. Such situations may arise as a consequence of lifting invariants from a loop. For example, f While (p x y) do Next x = t; Next y = f x y; Finally yg =) f If (p x y) then fy1 = f x y; In f While (p t y1 ) do Next y1 = f t y1 ; Finally y1 gg else yg Notice that it is only after the rst iteration that the value of the variable x is t. Thus, to avoid the circulation of the nexti ed variable x, the loop has to be peeled once. This rule can be expressed as follows. ! j P = n Xn :E ! ! B = n;n Xn0 :fn S In Zn g & F E (Zn ; ) {z } | !  WLoopn (P; B; Yn ; Yp ) ??! Bool casen(Yp ; ! fn p = n?1 xn!?!1 :RB[[E ] [xn!?1 = Xn?1 ; tn =Xn ]; b = n?1;n x0n?1 : ! ! ! RB[[fn?1 S In Zn?1 g]] [x0n?1 = Xn0 ?1 ; tn =Xn0 ]; ! ! tn = Apn;n (B; Yn ); ! tp = Apn?1 (p; tn?1 ); ! ! t0n?1 = WLoopn?1 (p; b; tn?1 ; tp ); ! In t0 ; tn g; ! n?1 Yn ) Please note that we could have also written Zn instead of tn on the rhs. 10 Conclusions Kid goes a long way towards expressing many machine independent implementation concerns. Since Kid has a proper calculus associated with it, correctness issues can be handled at an abstract level. The major de ciency of Kid is its inability to express storage reuse. We believe a language like Kid with a proper calculus which does not obscure parallelism and which can express storage reuse would be extremely useful in practical compilers for functional and other declarative languages. In fact, Kid is central to the current restructuring of the Id compiler which is already being used by 4 groups to generate code for their machines by just changing back end of the compiler. Some of the important optimizations that we currently perform in the Id compiler but have not discussed are dead code elimination, loop variable induction to hoist array bound checking, and array subscript analyses. These optimizations do not t in the CRS model very well. A better formalization of these optimizations would also be very useful. Acknowledgements Funding for this work has been provided in part by the Advanced Research Projects Agency of the Department of Defense under the Oce of Naval Research contract N0001484-K-0099 (MIT) and N0039-88-C-0163 (Harvard). Many thanks to Paul Barth, Shail Gupta, Jamey Hicks, Yuli Zhou and Bob Muller for reading the current draft of the paper and for providing insightful comments. References [1] A. Aho, J. Ullman, and R. Sethi. Compilers: Principles, Techniques, Tools. London, Addison-Wesley, 1986. [2] Z. M. Ariola and Arvind. P-TAC: A Parallel Intermediate Language. In Proc. ACM Conference on Functional Programming Languages and Computer Architecture, London, 1989. [3] Z. M. Ariola and Arvind. Compilation of Id? : a Subset of Id. Technical Report CSG Memo 315, MIT Laboratory for Computer Science, July 1990. [4] Z. M. Ariola and Arvind. Contextual Rewriting. Technical Report CSG Memo 323, MIT Laboratory for Computer Science, 1991. [5] Arvind, R. S. Nikhil, and K. K. Pingali. I-Structures: Data Structures for Parallel Computing. ACM Transactions on Programming Languages and Systems, 11(4), October 1989. [6] H. P. Barendregt. The Lambda Calculus: Its Syntax and Semantics. North-Holland, Amsterdam, 1984. [7] H. P. Barendregt, T. H. Brus, M. C. J. D. van Eekelen, J. R. W. Glauert, J. R. Kennaway, M. O. van Leer, M. J. Plasmeijer, and M. R. Sleep. Towards an Intermediate Language based on Graph Rewriting. In Proceedings of the PARLE Conference, Eindhoven, The Netherlands, Springer-Verlag LNCS 259, June 1987. [8] H. P. Barendregt, M. C. J. D. van Eekelen, J. R. W. Glauert, J. R. Kennaway, M. J. Plasmeijer, and M. R. Sleep. Term Graph Rewriting. In Proceedings of the PARLE Conference, Eindhoven, The Netherlands, Springer-Verlag LNCS 259, pages 141{158, June 1987. [9] T. Brus, M. van Eekelen, M. vam Leer, and M. Plasmeijer. Clean - A Language for Functional Graph Rewriting. In Proc. ACM Conference on Functional Programming Languages and Computer Architecture, Portland, Oregon,Springer-Verlag LNCS 274, 1987. [10] B. Courcelle. Fundamentals Properties of In nite Trees. D. Reidel Publishing Company, 1982. [11] N. de Bruijn. Lambda Calculus Notation with Nameless Dummies: A Tool for Automatic Formula Manipulation, with Application to the Church-Rosser Theorem. In Koninkijke Nederlandse Akademie van Wetenschappen, Series A, Mathematical Sciences, 1972. [12] P. Hudak and P. Wadler. Report on the Programming Language Haskell, A Non-strict Purely Functional Language (Version 1.0). Technical Report YALEU/DCS/RR777, Yale University, Department of Computer Science, April 1990. [13] T. Johnsson. Ecient Compilation of Lazy Evaluation. In Proceedings of the ACM SIGPLAN '84 Symposium on Compiler Construction, pages 58{69, June 1984. [14] V. Kathail. Optimal Interpreters for Lambda-calculus Based Funtional Languages. May 1990. Ph.D. thesis, Dept. of Electrical Engineering and Computer Science, MIT. [15] J. Klop. Term Rewriting Systems. 1s t Autumn workshop on Reduction Machines, Italy, September 1985. [16] R. S. Nikhil. Id (Version 90.0) Reference Manual. Technical Report CSG Memo 284-a, MIT Laboratory for Computer Science, July 1990. [17] S. L. Peyton Jones. The Implementation of Functional Programming Languages. Prentice-Hall International, Englewood Cli s, N.J., 1987. [18] G. Plotkin. Call-by-name, Call-by-value and the Lambda Calculus. Theoretical Computer Science, 1:125{159, 1975. [19] D. A. Turner. A New Implemetation Technique for Applicative Languages. In Software - Practice and Experience, volume 9, pages 31{49, 1979. [20] D. A. Turner. Miranda: a non-strict functional language with polymorphic types. In IFIP Int'l Conf. on Functional Programming and Computer Architecture, Nancy, France, Springer-Verlag LNCS 201, 1985. [21] C. Wadsworth. Semantics And Pragmatics Of The Lambda-Calculus. Ph.D. thesis, University of Oxford, Semtember 1971. [22] C. Wadsworth. Approximate Reduction and Lambda Calculus Models. Theoretical Computer Science, 7, 1978.