EMA: Implementing the Rewriting Computational ... - Semantic Scholar

1 downloads 0 Views 1MB Size Report
Nov 13, 1998 - constructive feedback has helped to improve this work. I am thankful to my parents Helga and Hans-Georg Eder for their love, understanding.
EMA: Implementing the Rewriting Computational Model of Escher Kerstin Inge Eder

A thesis submitted to the University of Bristol in accordance with the requirements of the degree of Doctor of Philosophy in the Faculty of Engineering, Department of Computer Science. November 1998

Abstract Escher is a new functional logic programming language which was designed to combine the best ideas of existing single-paradigm languages. The computational model of Escher is based on rewriting with residuation. The Escher systems module Booleans contains a number of well-chosen rewrite rules which de ne the basics of logic programming in Escher. Due to the novel integration approach of functional and logic features that is used in Escher, four years ago it was still an unresolved issue whether Escher could be implemented eciently. This thesis is devoted to nding such an ecient implementation for Escher. A comprehensive study of the Escher language and its computational model, especially the functions in the Booleans module, has been carried out. Based on the results of this study, the Brisk machine (which supports the purely functional language Haskell) has been identi ed as an abstract machine to serve as the basis for an Escher implementation. It is a simpli ed version of the Spineless Tagless G-machine, a machine which is considered to provide the fastest implementation of a lazy functional language. The Brisk machine has a basic graph reduction architecture. This thesis discusses how to extend and modify this machine by introducing a number of new components which support the functions in the Escher Booleans module. The resulting Escher machine (EMA) implements the pure rewriting computational model of Escher; it supports logic variables, residuation, pattern matching on function symbols and set processing in the Escher style. A new approach to utilise di erent forms of sharing in the presence of variables in function calls has been successfully integrated into the machine model. The language of the abstract machine resembles the STG language but is even simpler. A number of built-in functions are provided: one set deals with complicated controlrelated issues like evaluation, application and partial application; and the other supports the system functions in the Escher Booleans module. Quanti cation and set expressions can easily by integrated into the machine language by using specialised lifting techniques. The thesis contains the compilation route from Escher into the language of the abstract machine, and also a description of the machine's operational semantics, which can be given in the form of a state transition system. To demonstrate the feasibility of the proposed extensions and modi cations, the Escher compiler and the abstract machine have been implemented on the basis of the Brisk machine. The system is still in its early stages and does not employ any of the optimisations possible for an STG-like machine. The results obtained from the implementation show that more research is necessary to increase the performance of the machine. However, a rst step towards an ecient implementation has now been made.

Acknowledgements I am particularly indebted to my supervisor John W. Lloyd for his support, guidance and encouragement during the production of this work. His knowledge, endless patience and optimism have provided a very inspiring atmosphere. This work would not have been possible without him. I am also grateful to Ian Holyer and Tony Bowers. They have made it possible for me to implement the ideas outlined in this thesis (in time). Their input was invaluable. Special thanks are due to Rob Thomas for his immediate help when things seemed to go wrong (and needed recovering). For productive discussions I would like to thank Ian Holyer, Tony Bowers and Henk Muller. Thanks to Ilesh Dattani for his companionship and many cheerful chats. I would also like to thank Silke Kuball for having the time to listen and for giving constructive advice. She has become a good friend. Furthermore, I want to thank John Lloyd, John Gallagher, Ian Holyer, Giovanni Dallara, Tony Bowers and Sean Richards for proof reading all or parts of this thesis. Their constructive feedback has helped to improve this work. I am thankful to my parents Helga and Hans-Georg Eder for their love, understanding and support while I was working on this thesis (and not only). My most sincere thanks go to Sean Richards. He helped me in a variety of ways. Directly by listening to my ideas and proof reading parts of this thesis. But more importantly, he has been a true friend and caring partner who supported me in the ups and downs which he has gone through with me during the last three years. For inspiration, encouragement and other reasons I would also like to thank my sister Antje, Kathrin and James, and also Sabine. I would like to thank my examiners, Michael Hanus and Ian Holyer for a most interesting discussion and their detailed and sharp comments on my work. Finally I want to express my thanks to the Department of Computer Science for the scholarship that provided the nancial foundation for the last three years. Bristol, 13 November 1998/27 January 1999

Kerstin I. Eder

Fur Helga, Hans-Georg und Antje, Sean und Oma Emma

Declaration

The work in this thesis is the independent and original work of the author, except where explicit reference to the contrary has been made. No portion of this work has previously been submitted in support of an application for a degree of this or any other university.

Kerstin I. Eder

Contents 1 Introduction

1

2 Preliminary Considerations for an Implementation

5

1.1 What, why and how? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Thesis Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1 The Escher Language . . . . . . . . . . . . . . . . . . . . . . . . 2.1.1 Features of Escher . . . . . . . . . . . . . . . . . . . . . . 2.1.2 Escher Programs . . . . . . . . . . . . . . . . . . . . . . . 2.1.3 Haskell Syntax Extensions . . . . . . . . . . . . . . . . . . 2.1.4 Escher as a Term Rewriting System . . . . . . . . . . . . 2.1.5 Operational Semantics of Escher . . . . . . . . . . . . . . 2.1.6 Resume: How does Escher extend and modify Haskell? . . 2.2 Review of Existing Implementation Approaches . . . . . . . . . . 2.2.1 The Foundation of Functional Language Implementation . 2.2.2 Implementation of Functional Languages . . . . . . . . . 2.2.3 Implementation of Logic Languages . . . . . . . . . . . . 2.2.4 Implementation of Functional Logic Languages . . . . . . 2.3 Implementing Escher on an Abstract Machine . . . . . . . . . . . 2.3.1 Selecting the Base Machine . . . . . . . . . . . . . . . . . 2.3.2 The Brisk Machine . . . . . . . . . . . . . . . . . . . . . . 2.3.3 Extensions and Modi cations to support Escher on Brisk

3 Compilation of Escher

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . .

1 3

5 6 6 7 10 11 14 16 16 20 22 24 29 29 30 34

39

3.1 The Escher Kernel Language . . . . . . . . . . . . . . . . . . . . . . . . . . 40 3.2 Translating Core Escher into Escher Kernel Language . . . . . . . . . . . . 42 3.2.1 Built-in Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42

xii

CONTENTS 3.2.2 Sharing . . . . . . . . . . . . 3.2.3 Lifting . . . . . . . . . . . . . 3.2.4 The General Transformation 3.3 Summary . . . . . . . . . . . . . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

4 Implementation of the Escher System Functions

4.1 Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1.1 Initial Case Study . . . . . . . . . . . . . . . . . . . . 4.1.2 Implementing Pattern Matching on Function Symbols 4.1.3 Renaming . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Existential Quanti cation . . . . . . . . . . . . . . . . . . . . 4.3 Universal Quanti cation . . . . . . . . . . . . . . . . . . . . . 4.4 Equality . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.4.1 Binding Directly Existentially Quanti ed Variables . . 4.4.2 Binding Directly Universally Quanti ed Variables . . . 4.4.3 Binding Variables which are Not Directly Quanti ed . 4.4.4 Summary: Evaluating a Binding . . . . . . . . . . . . 4.4.5 Implementation of the Side Conditions . . . . . . . . . 4.4.6 Equality of Sets . . . . . . . . . . . . . . . . . . . . . . 4.5 Handling of Expressions which cannot be further evaluated . 4.6 Conjunction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.7 Disjunction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.8 Negation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.9 Supporting the Remaining Functions in Booleans . . . . . . 4.10 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

5 The Abstract Machine

5.1 Machine State . . . . . . . . . . . . . . . . . . . . . . . . . . 5.1.1 General Notation . . . . . . . . . . . . . . . . . . . . 5.1.2 Forms of the Code Component in the Machine State 5.1.3 Nodes on the Heap . . . . . . . . . . . . . . . . . . . 5.1.4 Environments . . . . . . . . . . . . . . . . . . . . . . 5.1.5 Return Nodes on the Stack . . . . . . . . . . . . . . 5.1.6 Variable Flag . . . . . . . . . . . . . . . . . . . . . . 5.1.7 Initial State . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

54 56 59 61

63 63 63 68 70 71 71 72 74 74 75 76 77 78 78 80 82 82 83 83

85 85 86 87 88 90 92 93 95

CONTENTS

xiii

5.2 Basic Transition Rules . . . . . . . . . . . . . . . . . . . 5.2.1 Entering a Node . . . . . . . . . . . . . . . . . . 5.2.2 Execution of the Evaluation Code of a Function 5.2.3 Evaluation of EKL Code . . . . . . . . . . . . . . 5.2.4 Returning from a Subcomputation . . . . . . . . 5.3 Variables and Deferred Computations . . . . . . . . . . 5.3.1 Variables . . . . . . . . . . . . . . . . . . . . . . 5.3.2 Deferred Computations . . . . . . . . . . . . . . 5.3.3 Activation of Deferred Computations . . . . . . . 5.4 General Control-related Built-in Functions . . . . . . . . 5.4.1 Evaluation . . . . . . . . . . . . . . . . . . . . . 5.4.2 Partial Application . . . . . . . . . . . . . . . . . 5.4.3 Application . . . . . . . . . . . . . . . . . . . . . 5.5 Escher System Built-in Functions . . . . . . . . . . . . . 5.5.1 Existential Quanti cation . . . . . . . . . . . . . 5.5.2 Universal Quanti cation . . . . . . . . . . . . . . 5.5.3 Equality . . . . . . . . . . . . . . . . . . . . . . . 5.5.4 Conjunction . . . . . . . . . . . . . . . . . . . . . 5.5.5 Disjunction . . . . . . . . . . . . . . . . . . . . . 5.5.6 Negation . . . . . . . . . . . . . . . . . . . . . . 5.6 Set Processing . . . . . . . . . . . . . . . . . . . . . . . 5.6.1 Representation of Sets on the Machine Level . . 5.6.2 The Evaluation of an Argument to a Set . . . . . 5.7 Summary . . . . . . . . . . . . . . . . . . . . . . . . . .

6 Results and Future Research

6.1 Implementation . . . . . . . . . . . . . . . . . . . . . 6.2 Performance . . . . . . . . . . . . . . . . . . . . . . . 6.2.1 Purely Functional Programs . . . . . . . . . . 6.2.2 Purely Logic Programs . . . . . . . . . . . . . 6.2.3 Comparison of Di erent Programming Styles 6.3 Set Processing . . . . . . . . . . . . . . . . . . . . . 6.4 Contribution . . . . . . . . . . . . . . . . . . . . . .

7 Conclusion

. . . . . . .

. . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. 97 . 97 . 97 . 98 . 101 . 102 . 102 . 105 . 116 . 122 . 122 . 125 . 126 . 129 . 129 . 131 . 139 . 152 . 159 . 165 . 170 . 170 . 171 . 179

183

. 183 . 184 . 184 . 186 . 194 . 196 . 197

199

xiv

CONTENTS 7.1 7.2 7.3 7.4

Understanding the Computational Model of Escher . . . . . . . . Classifying EMA with Criteria from Or-parallel Implementations Programming in Escher . . . . . . . . . . . . . . . . . . . . . . . Outlook on Application Areas for Escher . . . . . . . . . . . . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. 199 . 200 . 201 . 202

A Declarative Semantics of Escher

203

B

207

Booleans

Module

C Test Programs

C.1 Mergesort Program . . . . . . . . . . . . . . . . . . . . . . . . . . C.1.1 Mergesort Algorithm: Purely functional coding . . . . . . C.1.2 Mergesort Algorithm: Purely logic coding . . . . . . . . . C.1.3 Mergesort Algorithm: Functional logic programming style C.2 Permutationsort Program . . . . . . . . . . . . . . . . . . . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

215

. 215 . 215 . 216 . 218 . 219

Bibliography

221

Index

229

List of Figures 2.1 2.2 2.3 2.4 2.5 2.6 2.7

Representation of the application (f 4 2) using xed size nodes . . Representation of the application (f 4 2) using variable size nodes A heap node as a function call . . . . . . . . . . . . . . . . . . . . A heap node as a uniform data structure . . . . . . . . . . . . . . . A heap node as an active object . . . . . . . . . . . . . . . . . . . The basic principle of the Brisk machine stack . . . . . . . . . . . The organisation of the Brisk machine stack . . . . . . . . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

17 18 31 31 32 33 33

3.1 Syntax of EKL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 3.2 Reduction of a non-ground expression . . . . . . . . . . . . . . . . . . . . . 55 4.1 A conjunction as an indirect argument of another conjunction . . . . . . . . 79 4.2 Example for a hierarchy of conjunction levels . . . . . . . . . . . . . . . . . 81 5.1 Notation for representing nodes on the heap . . . . . . . . . . . . . . . . . . 89 6.1 Computation with distributed existentially quanti ed variables . . . . . . . 190 6.2 Comparison of di erent programming styles . . . . . . . . . . . . . . . . . . 195

List of Tables 3.1 Translating sets into core Escher . . . . . . . . . . . . . . . . . . . . . . . . 39 3.2 Representing sets with the Set constructor . . . . . . . . . . . . . . . . . . 48 5.1 Determining the next state of the variable ag . . . . . . . . . . . . . . . . 95 5.2 Uniform representation of function nodes on the heap . . . . . . . . . . . . 96 5.3 Evaluation of equalities through dedicated built-in functions . . . . . . . . . 152 6.1 6.2 6.3 6.4

Performance comparison of EMA and Brisk . . . . . . . . . . Performance comparison of EMA and Sicstus Prolog . . . . . Performance of sorting a list using permutation sort on EMA Di erent programming styles and the number of reductions .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. 184 . 187 . 191 . 194

A.1 Mapping symbols and expressions from type theory into Escher syntax . . . 205

Chapter 1

Introduction 1.1 What, why and how? In this section the objective of this thesis, the motivation and a brief outline of the method to achieve the objective are given. Declarative programming languages provide a means of de ning what it is that needs to be computed, with little or no mention of how to compute it; this is typically left to the compiler writers and machine architects, i.e. the language implementors. Because declarative programming languages are based on well founded semantics, it is easy to reason about a declarative program, to analyse it systematically, to apply program transformation techniques and use declarative debugging methods. Other practical advantages of declarative programming, such as freeing programmers from low-level implementation issues which increases the productiveness of programming and the provision of declarative meta-programming features, are well known. With the integration of the two approaches to declarative programming, viz. the functional and logic paradigms, the best features of both functional and logic programming, like the use of higher-order functions and currying, lazy evaluation, partially instantiated data structures, and the search for multiple solutions, are provided (and can be used) in a single declarative language. The combination of these features results in highly expressive and powerful languages which typically go well beyond their single-paradigm predecessors. The early research e orts in this direction date back more than a decade; rst results have been published in the mid 80's in [DL86]. Regarding the operational foundation of integrated languages, the trend was to use narrowing , which was (for a long time) regarded to be the operational model underlying functional logic languages [Loc93]. In the last few years, the development of functional logic languages has progressed suciently far that e orts are now being made to unite the various approaches resulting from di erent groups, with the aim to provide a common platform [Han98]. A language which notably in uenced the uni cation e ort is Escher [Llo98a]. It is a strongly-typed, higher-order, lazy functional logic programming language which does not use narrowing as operational foundation. Instead, Escher works on the basis of term rewriting with residuation, which provides a simple computational model. An Escher

2

Introduction program consists of a number of equations, called program statements. A computation can be regarded as the simpli cation of an initial term, called goal, to a nal term called answer, via a series of intermediate terms each of which is obtained from its preceding term by function application. The answer term is logically equivalent to the goal term with respect to the program, and can not be simpli ed any further. Such a computation consists solely of one path, because multiple solutions in Escher are captured by explicit disjunctions (in the bodies of statements). From the beginning, the major unresolved issue concerning Escher was the question of whether it could be implemented eciently. This thesis aims to answer this question. The objective of the work is to nd an implementation for Escher which has the potential of eciency and supports the pure rewriting computational model (with residuation). Clearly, amalgamated languages are only attractive to programmers if they can be implemented eciently. It is most desirable that a purely functional (resp. logic) program, written in a functional logic language, executes equally eciently as the same program written in a purely functional (resp. logic) language. A straightforward (but certainly not trivial) approach to obtaining an implementation for a functional logic language is to extend the scope of an abstract machine for either a functional or a logic language, to incorporate the features of the other paradigm; alternatively a completely new machine could be designed. This is true irrespective of the concrete operational model of the language. Research into the implementation of narrowing-based integrated languages started roughly 10 years ago [JD89, Muc92, BCM89, KLMNRA90a, Loo91]. Most of these developments concentrated on the implementation of one particular language. A general framework for implementing functional logic languages with a narrowing operational model was proposed in 1993 [Loc93]. However, there is currently no machine which supports the pure rewriting model of Escher. Since Escher can be seen as an extension and modi cation of the purely functional language Haskell, it is obvious to base an implementation of Escher on a machine which supports Haskell, and extend (resp. modify) this machine to also support the extra features of Escher. However, even though both languages share many basic concepts and are therefore very closely related, the design of Escher goes well beyond the typical functional logic languages, some of which are reviewed in [Han94]. Escher does not just integrate logic features into a purely functional language like Haskell, but also aims to support a more

exible programming paradigm by providing a more general term rewriting mechanism than functional languages, which are usually constructor-based. As a consequence, in spite of the availability of sophisticated machines that support Haskell [PJ93, HS97], the ecient implementation of Escher remained a considerable challenge. The last three years have seen the development of the Escher MAchine, or EMA for short, which provides the rst step towards a sophisticated and ecient Escher implementation.

1.2 Thesis Structure

1.2 Thesis Structure The thesis is structured as follows:

Chapter 2: Preliminary Considerations for an Implementation. This chapter

introduces the features of the programming language Escher and presents Escher's computational model. It contains a review of existing approaches towards the implementation of functional, logic, and functional logic programming languages. Based on this background, the kind of machine features that are needed to support Escher's computational model are speci ed. In the nal section a base machine (which provides the most important concepts) is identi ed and the changes that need to be made to implement Escher on this machine are discussed.

Chapter 3: Compilation of Escher. Here the machine language of the Escher ma-

chine is introduced. The chapter describes the transformation of Escher programs into the language of the abstract machine and gives an informal outline of the operational behaviour of the built-in functions which are used to control a computation.

Chapter 4: Implementation of the Escher System Functions. Some of the func-

tions in the system module Booleans are not translated into the machine language. Instead, their functionality is supported by system built-in functions on the machine level. In this chapter the de nitions of the respective system functions are mapped to the corresponding built-in functions. The rst section contains a case study on a representative example. The analysis is followed by a discussion of issues such as pattern matching on function symbols, renaming and the handling of expressions which cannot be further evaluated.

Chapter 5: The Abstract Machine. This chapter presents the architecture of the

Escher machine. It describes in detail all machine components and gives the machine's operational semantics by means of a state transition system. Starting from the initial state of the machine, the transition rules are divided into a set of simple rules which de ne the basic reduction mechanism, and a series of more complicated rules. These specify the support for the basic logic features of Escher, the functionality of the controlrelated built-in functions, the operation of the Escher system built-in functions, and the support for set processing.

Chapter 6: Results and Future Research. The outcome of the Escher implemen-

tation is analysed in terms of performance and its practical value. The chapter contains execution examples and discussions; it looks back at the objectives of this work and summarises the contribution made. Suggestions for future research are given together with the respective issue under discussion.

3

4

Introduction Chapter 7: Conclusion. A re ection on the lessons learned while performing this work

and a more general view on the features of EMA and Escher, and their future, conclude the thesis.

Appendix A: Declarative Semantics of Escher. This part of the appendix provides an overview of type theory.

Appendix B: Booleans Module. The second part of the appendix contains the Escher system module Booleans.

Appendix C: Test Programs. In the nal part of the appendix the test programs, which are used for the performance tests on the current EMA implementation, are given. The appendix is followed by a bibliography and an index.

Chapter 2

Preliminary Considerations for an Implementation This chapter rst introduces Escher by comparing Escher to Haskell and addresses the di erences between the two languages. It then reviews existing implementation techniques for functional, logic, and functional logic languages and identi es fundamental operational concepts. The last section is devoted to selecting a suitable base machine for the Escher implementation and pointing out the required extensions.

2.1 The Escher Language In this section the programming language Escher will be discussed by comparing it to Haskell. The discussion is concentrated only on the di erences between the two languages; for this reason the reader is assumed to have a basic knowledge of Haskell. An introduction to Haskell can be found in [HF97]; a de nition of the Haskell language is contained in the Haskell Report [PH97]. In the following sections, rstly the important features of Escher as a functional logic language are given. This is followed by an overview of the structure of Escher programs, including the extensions made to cover quanti cation and set processing. The computational model of Escher is then introduced and it is explained how expressions are intended to be evaluated. This section concludes with a summary on how Escher extends and modi es the purely functional language Haskell.

Notation: Within this thesis typewriter

is used for example programs in Escher and Haskell in running text. \Holes" in program fragments representing arbitrary pieces of code are written in italics (for example: case e of alts ). In general the names in italics are mnemonics, such as e for expression. When talking about the logic underlying the programming language, italics are used throughout. font

6

Preliminary Considerations for an Implementation

2.1.1 Features of Escher Escher [Llo98a] has been designed to integrate the best features of existing functional and logic programming languages like Godel [HL94] and Haskell [PH97]. It is a strongly-typed, higher-order, functional logic programming language which provides a module system, meta-programming facilities and declarative input/output. In Escher, logic computations are embedded in functional computations. Escher can be seen as an extension and modi cation of the purely functional language Haskell. It is more general than Haskell in two aspects. First, it integrates logic features and hence allows function calls containing variables to be evaluated. The language also supports existential and universal quanti cation. Second, it admits function symbols to appear in patterns which, amongst other things, allows a more exible handling of the connectives, and makes it possible to support set processing in an elegant way. In addition, the languages di er in their semantic foundations. The logic underlying Escher is an extension of Church's simple theory of types [Chu40], also called type theory . Type theory provides an appropriate formal basis for integrated functional logic languages, since it does not distinguish between functions and relations. It forms a logical theory based on simply typed lambda terms and allows both predicates over lambda terms as well as abstractions over predicates. Type theory has an elegant model theory. The model theory is based on Henkin's general models [Hen50]. These are a generalisation of rst-order interpretations and are appropriate to capture the intended interpretation of Escher programs. Hence, an Escher program is to be understood as an equational theory; an interpretation for the program means an interpretation for the theory associated with the program. Escher is in this respect di erent from Haskell, which is based on the lambda calculus and a denotational semantics. A brief overview of type theory and Henkin models is given in Appendix A. A formal de nition of the declarative semantics of Escher is not given here, because the focus of this thesis is on the operational behaviour of Escher programs. Instead, in a rather informal style, an intuitive meaning is given to new functions and language extensions by stating their corresponding lambda expressions. A table which relates various symbols and expressions of type theory to their equivalent in Escher syntax can be found in Appendix A.

2.1.2 Escher Programs Escher terms are the terms of the typed -calculus.1 They are formed by abstraction and application from a given set of functions and a set of variables (see Appendix A). An Escher program consists of a number of function de nitions . Predicates are considered to be boolean functions. Boolean expressions can also be called formulae. A de nition consists of one or more equations, called statements , of the form h = b. The head h is a term of the form f t1 : : : tn where f is a function and each ti is a term, the body b is a term. All variables appearing in the body of a statement, but not the head, must be explicitly quanti ed. Such variables are referred to as local variables . In contrast to Haskell, where overlapping function de nitions, provided they pass the compiler, are interpreted as sequences (i.e. they are tried from top to bottom in the order they are given In the context of Escher (and therefore in this thesis), there is no distinction between terms and expressions; both are used synonymously. 1

2.1 The Escher Language in the program), Escher does not impose an order on the statements belonging to a function de nition. Hence, an Escher statement can be given a meaning without considering the whole function de nition. Moreover, Escher generally allows function de nitions to contain overlapping statements. It is the responsibility of the programmer to ensure that if the heads of two (or more) statements overlap then the corresponding statement bodies are semantically equivalent. Escher provides system modules containing de nitions for Boolean functions, set and list processing and input/output. The most important of Escher's system modules is the module Booleans (see Appendix B). It de nes the Boolean connectives, quanti ers and set-processing facilities of Escher, and is, by default, imported into any Escher program. There is a subtle distinction between certain statements in the system modules which contain variables and those which do not. Variables in these statements are regarded to be syntactical variables , and the statement is in fact a statement schema . One can think of a statement schema as an expression in a meta-language which speci es a (potentially in nite) collection of statements, each obtained as an instance of the statement schema. This treatment of variables is a result of the way statements are de ned. Even though it does not apply to every statement, for simplicity all statements are treated uniformly. In the context of rewriting systems, in particular in the area of equational logic programming [Hol89], the use of syntactical variables and statement schemas is very common, see [O'D85] for an example. Escher's computational model is based on rewriting, which will be addressed in Section 2.1.4. In statement schemas, identi ers beginning with a lower-case letter (and not having a signature) are syntactic variables ranging over appropriately typed (object) terms. An instance of a statement schema is an (object) formula, called a statement, which is obtained by instantiating syntactic variables by (object) terms. The following informally stated conventions are de ned for the use of syntactical variables. They have been taken from [Llo98a], where some detailed explanation regarding their meaning can be found as well. 1. Syntactical variables appearing immediately after a lambda are restricted to range over (object) variables. 2. If a syntactical variable u occurs both inside the scope of some x and also outside the scope of all x's, then u cannot be instantiated by an (object) term containing as a free variable the (object) variable instantiating x. 3. The set ft1 ; : : : ; tn g means fx j (x = t1 ) _ : : : _ (x = tn )g, for which x cannot be free in the ti 's. In particular, fg means fx j Falseg and ftg means fx j x = tg. For brevity, statement schemas will be called statements in the following sections.

2.1.3 Haskell Syntax Extensions Escher conforms to the Haskell syntax conventions [PH97]. The top-level equality symbol is denoted by (=). Conjunction, disjunction, ordinary equality and negation are denoted

7

8

Preliminary Considerations for an Implementation by the function symbols (&&), (||), (==) and not. There is a special notation for lambda abstractions: the abstraction x:x+1 is written in Haskell as \x -> x + 1. The extensions made to support existential and universal quanti cation, and set processing are informally introduced below. In the system module Booleans given in Appendix B a kind of metalanguage is used to state system function de nitions. This meta-language will be discussed in Chapter 4. It is not available to Escher users.

2.1.3.1 Quanti ers Escher provides existential and universal quanti cation. The quanti ers exists and forall are declared in the Escher system module Booleans as boolean functions. exists :: (a -> Bool) -> Bool forall :: (a -> Bool) -> Bool

The statements for both quanti ers are given in Appendix B. The formula 9x:even x is expressed in Escher as exists \x -> even x. The function exists is Church's (generalised) existential quanti er [Chu40] in the sense that 9x:t stands for (x:t). The syntax has been chosen to re ect this connection. It is important to note that the notation exists \x y z -> e, which will be used frequently in Escher statements, is an abbreviation for: exists \x -> (exists \y -> (exists \z ->

e))

The universal quanti er forall has a similar syntax to exists. The meaning of the function forall is based on Church's universal quanti er . The formula 8x:t stands for (x:t). Escher supports only restricted universal quanti cation. As de ned in [BHI97], a restricted quanti cation is a formula of the form 8x:(V ! W ). This is a natural way to use a universal quanti er. The antecedent V restricts the domain of the quanti ed variables and the consequent W gives the condition that must be satis ed by all the values in the restricted domain. Hence, a typical use of the universal quanti er would be forall \x flies x ==> bird x, where ==> is the function symbol for implication. This assumes an appropriate de nition of data constructors and the predicates flies and bird. For example, flies can be de ned as: flies :: Bird -> Bool flies x = (x==Sparrow) || (x==Eagle) || (x==Pigeon)

Notice that flies in fact de nes a set of birds, as will be seen in the next section.

2.1.3.2 Sets The meaning of some set expressions has already been given informally at the end of Section 2.1.2. Here the syntax of sets and how sets are intended to be used in programs will be discussed.

2.1 The Escher Language The higher-order logic underlying Escher makes it possible to identify a set with a predicate. To be more speci c, a set is identi ed with the predicate which maps an element of the domain to true, if and only if, the element is a member of the set. For instance the set {1,2,3} can be identi ed with a predicate of the form: p :: Int -> Bool p = \x -> (x==1) || (x==2) || (x==3)

Set-processing functions are simply higher-order functions whose arguments are predicates. In Escher, the conventional set brackets { and } are used as syntax for sets. The following set patterns are allowed to be used in statement heads, and in the alternatives of case expressions in statement bodies:

 {} to represent the empty set;  {t} to represent the singleton set containing t where t is a syntactic variable; and  {x|(u||v)} where x, u and v are syntactic variables. According to point 1 in

Section 2.1.2, the syntactic variable x ranges only over (object) variables. This notation represents the set of all x so that u or v is true.

Only the above named three set patterns can be used for matching on sets. In a statement body, except for the alternatives of case expressions (i.e. not for pattern matching), sets of the form {t1 ; : : : ; tn }, where the ti 's are terms, and {x|u}, where x is a syntactic variable ranging over (object) variables and u is a formula, can be used in addition. Here, x is called set variable , and the boolean expression behind the bar is the set body . It is important to point out that a set pattern like {t} is intended to match any singleton set, this means t needs to be a (syntactical) variable. Hence, the pattern {1} is not a valid set pattern, and is rejected during parsing. The same applies to the pattern {x|(u||v)}. Typically, the set variable x is a free variable in both u and v. For this reason, u and v are only allowed to appear in set patterns containing x as a set variable. This ensures that point 2 in Section 2.1.2 is satis ed. Both u and v can only be used outside a set expression, once the set variable does not occur free in them. This can, for instance, be achieved through a set application of the form {x|u} y, which will bind x to y in u. Sets are functions or, more precisely, predicates. There is a lambda abstraction corresponding to every set expression. For instance, the lambda abstraction \x -> False corresponds to {}, the singleton set {t} is \x -> (x==t) and the set {t1 ; : : : ; tn } can be represented as \x -> ((x==t1 )|| : : : ||(x==tn )). Set processing can be provided by higher-order functions which take predicates as arguments and/or use set expressions in statement bodies. Pattern matching on sets means using the above named patterns in statement heads. The higher-order function emptyset is an example of a set-processing function: emptyset emptyset emptyset emptyset

:: (a -> Bool) -> Bool {} = True {t} = False {x|(u||v)} = emptyset {x|u} && emptyset {x|v}

9

10

Preliminary Considerations for an Implementation

It is important to point out that set patterns can semantically overlap, even though they are syntactically di erent. This is due to the fact that a value can be represented in di erent forms, and hence syntactically unrelated expressions may represent the same value. In the de nition of emptyset the set pattern of the last statement overlaps with both the empty set and the singleton set. It is the responsibility of the programmer to ensure that functions produce the same result for the overlapping cases.2 The above de nition of emptyset can be used to give some further explanation on how syntactical variables are used, and what the consequences are. One of the conventions on syntactical variables (point 1 in Section 2.1.2) is that those variables appearing immediately after a lambda, are restricted to range over (object) variables. This applies to the variable x in the last statement in the de nition of emptyset above. It appears rst in the head of the statement in the set pattern {x|(u||v)}, which is syntactic sugar for \x -> (u||v). In the body, it is used in the sets {x|u} and {x|v}. An appropriate instantiation of the syntactic variables would bind x to the same object variable in all three sets. The concept of syntactic variables facilitates the breaking up of sets by pattern matching on them in statement heads, and the construction of sets with the same set variable in the statement body; this is a very powerful style of parameter passing. It needs to be stressed, though, that sets which occur in function bodies, and which contain set variables that also appear in the head of the statement, can only be given the intended meaning in the context of the statement. Otherwise the link between the set variables would be lost. In particular, the lambda abstractions corresponding to these sets must not be taken out of their context (e.g. through lambda lifting). This is an important point when it comes to the compilation of set-processing functions.

2.1.4 Escher as a Term Rewriting System Escher works on the basis of term rewriting , which provides a simple computational model. This distinguishes Escher from most logic programming languages, which are typically based on a \theorem proving" computational model. The key idea of term rewriting is to put a directionality on the use of equations, which are also called (rewrite) rules . A rule over a set of terms T is an ordered pair < h; b > which can be written h ! b to indicate the direction. These rules are used to replace instances of h by corresponding instances of b. Essentially, Escher can be seen as a term rewrite system which consists of a nite set of rules R (the program statements) over a set of terms T (terms satisfying the Escher syntax). Rewriting in such a system is de ned in [DJ90] as follows: De nition: For a given rewrite system R, a term s in T rewrites to a term t in T , R written s ,! t, if sjp = h and t = s[b]p , for some rule h ! b in R, position p in s, and substitution . A subterm sjp, at which a rewrite can take place, is called a redex. Functional languages can be seen as a restricted form of rewriting systems. They have a clear separation of data constructors and functions, which is used to distinguish between values and reducible expressions. Pattern matching is limited to constructor terms. The Instead of enforcing explicit restrictions, like only allowing constructor-based rewrite rules, Escher o ers more freedom to the programmer. As a consequence, it is the duty of the programmer to write meaningful programs, i.e. programs that comply with the declarative semantics of Escher (see Appendix A). 2

2.1 The Escher Language

11

evaluation of an expression consists of a number of reductions, and returns the value of the expression in the given program. Rewriting systems are more general than functional languages. They admit function symbols in patterns, hence pattern matching is no longer restricted to constructor terms. Escher allows the use of function symbols in patterns, however, user-de ned functions are restricted to constructor-based de nitions, except for set-processing functions. The following de nition of the system function not is an example which illustrates how both constructors and de ned functions are used in the statement heads. not not not not not not

:: Bool -> Bool False = True True = False (not x) = x (x || y) = (not x) && (not y) (x && y) = (not x) || (not y)

Also, functional languages typically allow total functions only. A function is total if it is de ned on all argument values, and partial if it is unde ned for some argument values. An example is the de nition of the function head: head :: [a] -> a head (x:xs) = x

The value of head [] is unde ned. In functional languages failure is produced in the case of unde nedness. This, in fact, makes partial functions total even if they are not de ned as total functions. The result is that the evaluation of an expression like head [] creates an error. In the logic underlying Escher (see appendix of [Llo98a]) all functions are de ned as total functions; there are no partial functions. However, even though functions are de ned on all argument values, the value of a function for some elements of the domain might not be known, e.g. the value of the expression head [] is not known and hence the expression does not reduce. Typically, rewriting systems do allow partial functions without failure semantics. A rst version of Escher followed this approach. It was, however, later revised because such behaviour is clearly undesirable in a practical programming language. This is why Escher now adopts the strategy of Haskell and produces failure instead of a nonreducible expression. This is in contrast to general term rewriting systems, which allow partial functions without a failure semantics. Having described Escher as a term rewriting system, we can now consider the operational semantics of Escher.

2.1.5 Operational Semantics of Escher A computation from a term t in Escher is a sequence fti gni=1 of terms so that t = t1 and ti+1 is obtained from ti by a computation step, for i = 1; : : : ; n , 1. It can be regarded as the simpli cation of the initial term t, called the goal , to a nal term tn , called the

12

Preliminary Considerations for an Implementation

answer , via a series of intermediate terms each of which is obtained from its preceding term by a computation step. Since rewriting is the only simpli cation method in Escher, a computation step can also be called a rewrite step . Such a rewrite step is a function call which is performed as follows. A redex r of a term s is a subterm of s, which is identical to the head of some instance of a statement schema. Escher selects the redex in the current term, on which to make a call, according to a speci ed selection rule. Let L be the set of terms constructed from the alphabet of a program and let DSL be the set of subterms of terms in L distinguished by their position. A selection rule S is a function from L to the power set of DSL satisfying the following condition: if t is a term in L, then S (t) is a subset of the set of outermost subterms of t, each of which is a redex. Typical selection rules are the parallel-outermost selection rule for which all outermost redexes are selected, and the leftmost selection rule in which the leftmost outermost redex is selected. An implementation of Escher can employ any appropriate selection rule. For example, an implementation of Escher which aims to encompass Haskell must employ the Haskell selection rule on the Haskell subset of the language. A statement h = b matches a redex r, if there is a substitution , so that h is identical to r. If so, the redex r, in the current term s, is replaced by b to give the next term in the computation. The leftmost redex is the redex whose function symbol3 is textually to the left of all other redexes within the expression to be reduced. The outermost redex is the redex which is not contained within any other redex. Leftmost outermost reduction is also known as normal-order reduction [FH88]. A term s is obtained from a term t by a rewrite step if the following conditions are satis ed:

1. S (t) is a non-empty set, for example fr g. 2. For each , the redex r is identical to the head h , of some instance h = b , of a statement schema. 3. s is the term obtained from t by replacing, for each , the redex r by b . An important property of a rewriting system is its con uence . Informally, this property states that whenever a term s reduces to two di erent terms, s1 and s2 , then there exists another term t, to which both s1 and s2 can be reduced. The con uence property guarantees the uniqueness of normal forms. At present, the extent to which Escher is con uent is unclear. Several of the rewrite rules in Booleans overlap. Due to the fact that Escher allows matching on function symbols, an expression (representing one value) can syntactically be represented in di erent ways, and might, therefore, be rewritten to (syntactically) di erent results depending on its representation. A thorough study of Escher, wrt. its properties as a higher-order rewriting system, is still necessary. However, Escher computations are sound in the sense that: if s is the goal term and t is the answer term of a computation, then s == t is a logical consequence of the program, i.e. in the intended interpretation, s and t have the same value. The term t cannot be simpli ed any further, and is said to be in normal form . 3

Assuming pre x notation.

2.1 The Escher Language

13

Typically, an answer contains some constructor such as (:) or []. It may, however, also contain some functions, such as (==), (&&), (||) or not, which are de ned in system modules. These system functions normally appear as top-level functions in an answer, which distinguishes Escher from Haskell. In Haskell, the value of a program is computed; values are represented by data constructors. Because Escher also supports the logic programming style, the answer to an Escher program can be either a value (for purely functional programs) or a substitution for the free variables in the goal term, which is, in this case, a formula. The normal form of a formula is a disjunction of conjunctions of (possibly negated) equalities of the form (x==e), where x is a variable, e is an arbitrary expression, and the variables on the left-hand side of the equations in each conjunction are distinct. The relation of such a formula to traditional logic programming languages is straightforward; it simply represents a substitution, or, in the case of a disjunction of conjunctions, it represents several substitutions, one for each branch of the search space. An Escher computation consists of just one path, because multiple solutions are represented by explicit disjunctions in the bodies of statements. Hence, Escher computations for logic programming examples return \all answers". Set-processing functions can be used to lazily evaluate computations with a potentially in nite number of answers. It is also worth noting that in Escher, the equivalent of a failure in a conventional logic programming language is to return the answer False. In Escher, logic computations are embedded in functional computations. Escher does not have uni cation directly built into the computational model. Instead, matching is used during a function call, and the remainder of uni cation is handled by explicit equalities which appear in the bodies of statements. The statements of the equality function (==), de ned in the module Booleans, have a close connection to uni cation. However, they do not cover the binding of variables. This is handled explicitly together with substitution application through some of the statements de ning conjunction, existential and universal quanti cation. Whereas variables in functional languages are simply names for values, variables in (functional) logic languages are initially unbound, and later instantiated. Once bound, they keep their value within one branch of the search space of a computation. However, the same variable might have di erent values corresponding to di erent branches of the search space. For example the answer to a goal containing x as a free variable, such as p x, might be the formula (x==1) || (x==2) || (x==3). As a consequence, expressions containing variables can potentially represent a variety of values, depending on the bindings of the variables. For example, using the intuitive meaning of even and odd, the expression even x is equivalent to True for x being bound to 2, but if x is bound to 1, even x evaluates to False. A ground expression is an expression which does not contain any variables. The expression under evaluation in a functional language, such as Haskell, is ground; this allows the evaluation of Haskell programs to be implemented as ground term reduction . Reduction is a technique which is successfully employed to implement the operational model of functional languages. Graph reduction introduces sharing to the representation of expressions on the level of implementation. Thus, in graph reduction, an expression is represented as a graph in which the same occurrence of a subexpression is, if occurring elsewhere in

14

Preliminary Considerations for an Implementation

the expression, represented as the same branch of the graph. The foundations of graph reduction are investigated in Section 2.2.1. Escher introduces the ability to reduce expressions which contain variables. This requires a more sophisticated evaluation strategy than those used by functional languages because, due to the presence of variables in expressions, a function call may have an unbound variable at an argument position, where a pattern is demanded by the heads of the statements de ning the corresponding function. The meaning of a pattern is extended, in Escher, to include terms which have either a function or a data constructor at the top level. A pattern can be demanded by an argument position in the head of a statement, if the head has a pattern at this position. In such a situation, there are two ways to proceed:

 Defer the evaluation of this function call until the variable is suciently instantiated.

This approach is commonly referred to as residuation, and is used in Escher, and other languages such as Le Fun [AKLN87], Life [AK90] and Oz [Smo95]. Residuation forces deterministic function reduction.  Alternatively, instantiate the unbound variable to the di erent values demanded by the heads of the statements, and perform non-deterministic function reduction. This approach is called narrowing, and is used in languages such as ALF [Han90] and BABEL [MNRA92]. Languages based on narrowing typically use uni cation4 as their parameter passing mechanism.

There is an ongoing discussion about the bene ts and disadvantages of narrowing compared to residuation. Curry [Han98], a new integrated functional logic language supports both strategies. A review of various narrowing strategies can be found in [Han94]. The computational model of Escher can be characterised by rewriting with residuation. The following section summarises how Escher extends and modi es Haskell.

2.1.6 Resume: How does Escher extend and modify Haskell? Escher aims to provide a more exible programming paradigm than typical functional or logic languages. It is easiest to understand Escher as an extension and modi cation, respectively, of the purely functional language Haskell. The fundamental di erences between Escher and Haskell are listed and discussed below: 1. Escher gives up the ground redex assumption. It allows function calls containing variables to be reduced. The presence of variables gives rise to a number of other di erences:

 Escher does not have uni cation directly integrated into the computational

model. Instead, matching is used when a function call is made. The remainder of uni cation is handled by explicit equalities in the statement bodies. Several

Narrowing can be seen as a generalisation of rewriting, where pattern matching is replaced by uni cation. 4

2.1 The Escher Language

15

statements in the system module Booleans, which involve the equality function de ne how variables are bound, and how substitutions are applied to expressions. The de nition of (==) has a close connection with uni cation.  Answers to Escher programs can contain function symbols; such answers typically represent an expression which can be interpreted as a substitution for the free variables in the goal term.  Escher uses residuation to handle function calls with insuciently instantiated arguments; the call is deferred until the corresponding variable is instantiated. Function calls are, however, reduced deterministically, as in functional languages. (==)

2. Escher admits functions to appear in patterns. It gives up on the strong separation between functions and data constructors which is a feature of Haskell. This is, however, currently restricted to the functions de ned in system modules, like Booleans, and to set pattern matching. As a consequence, set processing, and a more exible de nition of functions such as (&&), (||), (==) and not, are made possible, and hence extra power is given to the programmer. However, since patterns containing function symbols can overlap, i.e. an expression can be represented in di erent ways and hence can match di erent patterns even though it represents only one value, programmers need to ensure that the functions they write actually have a meaning. For instance, it is the responsibility of the programmer to ensure that functions produce the same result where patterns overlap. Due to the fact that expressions can be represented in various ways, it is not yet clear to what extent Escher is con uent. In practice, it computes sensible answers (in most cases); however, more research is clearly necessary. 3. Escher provides functions for existential and universal quanti cation, and for set processing. The Haskell syntax has been extended accordingly. 4. Escher does not impose an order on statements when trying to pattern match. This allows the meaning of single statements to be inferred without having to consider the whole function de nition and hence supports a declarative reading of Escher programs. In contrast, overlapping Haskell statements are understood as a sequence (rather than equations). In an implementation, they are tried in the order they are given in the program, which leads to a more operational reading of programs. 5. Escher is based on type theory, and Henkin interpretations are used to formalise the intended interpretation of Escher programs. Escher is thus closer to the modeltheoretic approach of logic programming languages. In contrast, Haskell is based on the lambda calculus. Denotational semantics is used to give meaning to a Haskell programs. The rst three points are clearly extensions to Haskell. Point 2 brings Escher closer to general term rewriting systems, whereas point 1 and point 3 add logic programming features to Escher. The last two points, however, are modi cations. The above points will be used in Section 2.3, which examines how a Haskell implementation needs to be extended and modi ed to support Escher.

16

Preliminary Considerations for an Implementation

2.2 Review of Existing Implementation Approaches Typically, the presentation of an evaluation model for declarative programming languages is done by de ning an abstract machine which executes a stream of instructions, called abstract machine code . A set of compilation rules de nes how to transform a program into abstract machine code. The abstract machine forms a concrete operational semantics for a given programming language in the sense that it directly describes how to compute a value of an expression in this language. Abstract machines are commonly used as a stepping stone between a source language, and a particular concrete machine code. According to [Kog91], a good abstract machine can be characterised by two properties. First, it can be easily translated into any concrete machine code, and second, it is easy to generate the abstract machine code from the source code. A general introduction to abstract machines can be found in [Kog91]. An abstract machine for a functional logic language can be based either on the design of an abstract machine for a logic programming language, or on the design of an abstract machine for a functional language, or on a completely new machine design. In the rst two cases the abstract machine needs to be modi ed in order to also capture the features of functional or logic programming respectively. This section reviews a selection of abstract machines, and discusses existing implementation techniques for functional, logic, and functional logic languages. A comprehensive taxonomy of existing approaches towards functional logic language implementation can be found in [Loc93]; a survey on implementing functional logic languages is contained in [Han94]; and a tutorial on language features and corresponding implementation techniques is given in [MN94].

2.2.1 The Foundation of Functional Language Implementation Before coming to concrete implementations of functional languages, it is helpful to examine graph reduction a little closer. Graph reduction is the basis of the computational model underlying functional languages. In order to execute a functional program, it is translated into the lambda calculus or an enriched form of it. Executing such a program means reducing the corresponding lambda expression to normal form. For this purpose, the expression is represented by a graph. Evaluation consists of a sequence of reduction steps. Each reduction step replaces a reducible expression (also called redex ) in the graph by its reduced form. The evaluation is nished when the expression contains no more redexes. It is then said to be in normal form . If the expression under evaluation contains more than one redex, one of them can be chosen. The following sections investigate the foundations of graph reduction, with a view on implementation. They aim to answer the questions: how to represent expressions on the machine level; which redex to select, and how to detect it; and how to perform a reduction.

2.2 Review of Existing Implementation Approaches

17

2.2.1.1 Representation of Expressions The pure lambda calculus needs to be extended for the purpose of functional language implementation. An expression in the extended lambda calculus can be an application, an abstraction, a variable, or a built-in function. The selection of built-in functions can vary; it typically includes arithmetic and logical functions, conditionals, and constants. The latter can also be called data objects, such as integers, booleans or data constructors. Such an expression can easily be represented by a graph. For the purpose of implementation, the graph can be represented by a collection of connected nodes on the heap of an abstract machine. Each node in the graph corresponds to a heap cell. In early implementations, such as the G-machine,5 heap nodes had a xed size. Applications of functions to several arguments were represented in curried 6 form. Graph representation:

Concrete representation of the graph:

@

@

f

@

@

2

4

&

f

I

I

2

4 Tags: @ application & built-in function I integer

Figure 2.1: Representation of the application (f 4 2) using xed size nodes Figure 2.1 shows how the application of a built-in function f to two arguments 4 and 2 could be represented using xed size nodes. The 0 @0 is a tag which identi es a node as an application node. The left-branching chain of application nodes is called the spine of the expression. Tagging is used to distinguish di erent types of heap nodes, e.g. applications, abstractions, constants and built-in functions. A much more ecient representation can be achieved by using variable-sized heap nodes. Figure 2.2 shows how the application (f 4 2) would be represented with this approach. G-machine stands for graph reduction machine. Functions of several arguments are obtained by iteration of application, e.g. (f 4 2) is written as ((f 4) 2). Currying is named after the mathematician Haskell Curry. 5 6

18

Preliminary Considerations for an Implementation Graph representation:

Concrete representation of the graph:

@

f

4

@

2

&

f

I

4

I

2

Tags: @ application & built-in function I integer

Figure 2.2: Representation of the application (f 4 2) using variable size nodes

2.2.1.2 Which Redex to select, and how to detect it Before a reduction step can be performed, the next redex needs to be selected. There are several selection strategies known. The most important ones being:

 Select the leftmost outermost redex which implements a call-by-need semantics, since

it postpones the evaluation of function arguments until their value is actually required, which allows the handling of in nite data structures.  Select the leftmost innermost redex which implements a call-by-value semantics, i.e. arguments to a function are evaluated before the function is called.

In the context of functional languages, call-by-need is often referred to as lazy evaluation ; whereas call-by-value is called eager evaluation . In an implementation of a lazy evaluation strategy, arguments to functions should only be evaluated when their value is needed. This is the case for strict functions, which require the value of their argument to be computed in order to compute their result. A function, say f , is strict if and only if f ? = ?. The symbol ? is called bottom ; it comes from domain theory, and denotes the value of an expression without a normal form. The de nition of strictness can be extended easily to functions of several arguments, which can be strict at particular argument positions. Lazy evaluation can be supported by selecting the leftmost outermost redex, a strategy which is also known as normal order reduction . It has a useful property in that, if an expression is reducible to normal form, there exists a normal order reduction sequence to nd it. This property is the content of the second Church-Rosser-Theorem. Escher is a lazy functional logic language, and for this reason the discussion will be focussed on the implementation of lazy functional languages. The best method to implementing lazy evaluation is to pursue normal order reductions, but stop when the top-level expression is no longer a redex; it is said to be in weak head normal form . An expression is in weak head normal form if and only if it is of the form f e1 : : : en where n  0 and either f is a variable or a data object, or f is a lambda abstraction or built-in function, and f e1 : : : em is not a redex for any m  n. Notice that the expression in weak head normal form may

2.2 Review of Existing Implementation Approaches

19

still contain inner redexes. Reduction to weak head normal form avoids the reduction of these redexes, thus supporting lazy evaluation. The question is now, how is the normal form of an expression computed? This is, in fact, quite simple, but requires a slightly higher-level view of the evaluation process. In the implementation of lazy functional languages, it is common to assume a simple toplevel evaluate/print loop which drives the evaluation of a functional program. It reduces the initial expression representing the functional program until it has reached weak head normal form. If the top-level expression is a constant, such as an integer or a boolean value, it is printed and evaluation is nished. If the top-level expression is, however, a data constructor, the evaluation of the components of the constructor is invoked. The evaluate/print loop is repeatedly applied to all components of a data constructor. It prints out the results as it proceeds in the evaluation. This concept is supported by the fact that data constructors do not evaluate their arguments; they are called lazy constructors . Thus, reduction to normal form is achieved by reducing the top-level redexes to weak head normal form using normal order reductions, and, subsequently, applying normal order reductions to the inner redexes on demand. One can think of the evaluate/print loop as a strict function, which takes the expression representing the program and returns a string representing the value of the program; the latter can then be printed. The function is strict; it forces the evaluation of its argument (the functional program) to weak head normal form and, is subsequently applied to the components of data constructors. In the same way as the print function, all strict (built-in) functions force the evaluation of their argument to weak head normal form. Let us now turn to the issue of how to nd the next top-level redex. The expression to reduce can only be of the form f e1 : : : en where n  0 and f is a data object, a built-in function, or a lambda abstraction. The arguments are arbitrarily complex expressions. If f is a data object (this is a constant, or a data constructor) the expression is in weak head normal form and thus not a redex. If f is an m-ary built-in function and, if m  n, then f e1 : : : em is the outermost redex, otherwise the expression is in weak head normal form. If f is a lambda abstraction which is applied to at least one argument n  1, the next redex is f e1 . If no arguments are available (n = 0) then the expression is in weak head normal form. To nd f in a variable size node representation is straightforward. In a xed node size representation one has to go \down" the left branch of each application node from the root of the expression under evaluation. This process is called unwinding the spine, and f is called the tip of the spine. Having identi ed f , one goes back \up" the spine to the root of the next redex, depending on how many arguments f is applied to. A spine stack is used to support this in an implementation. It contains pointers to the application nodes of the expression. When arguments of strict functions are evaluated, a new stack is needed. This stack can be built on top of the existing stack because the existing stack does not change until the argument evaluation is completed, at which stage the new stack can be discarded. It is important to save sucient information to be able to restore the old stack after an argument evaluation. In an implementation a dump stack is typically used for this purpose.

20

Preliminary Considerations for an Implementation

2.2.1.3 How to Perform a Reduction Once the desired redex (according to the selection rule) has been detected, a reduction needs to be performed. A redex is a subgraph which has either a built-in function or a lambda abstraction at the tip of its spine. If the redex is an application of a built-in function to the right number of arguments, the arguments rst need to be evaluated to weak head normal form if the built-in function is strict. The built-in function is then executed and the root node of the redex is overwritten with the evaluation result. If the redex is a lambda abstraction (e.g. applied to one argument) the reduction step consists of applying beta reduction to the subgraph. This means that a new instance of the body of the lambda abstraction needs to be constructed (on the heap), where the argument is substituted for the free occurrences of the formal parameter (or lambda variable). In practice, pointers to the argument are used to replace the formal parameter. Thus, if a formal parameter occurs more than once in the lambda body, the pointers point to the same argument expression, which is then said to be shared . Sharing is important for supporting lazy evaluation. It provides the base for evaluating each expression at most once, by avoiding the (physical) duplication of arguments, which are typically passed in unevaluated form. Subsequent references to the same expression should use the result of the rst evaluation. This can be achieved by physically overwriting the root of a redex with its evaluation result. During the process of reduction, new graphs are constructed on the heap frequently. To support this, a graph reduction machine needs the support of a storage management system which allocates new heap cells when new nodes need to be created. On the other hand, subgraphs can become detached from the part of the graph that is currently modi ed. They might, however, be shared by other nodes, and can therefore not be recovered immediately. If they are completely unreferenced they are called garbage . The storage management system invokes a garbage collector when the free heap area is exhausted. The garbage collector compacts the graph by collecting the garbage and thus increases the available free heap space. A discussion of various garbage collection techniques is given in [PJ87]. Having now reviewed the fundamental aspects of graph reduction, the next section looks at some concrete examples of abstract machines which support functional languages.

2.2.2 Implementation of Functional Languages The SECD machine was the standard machine for implementing functional languages in the mid '80s. It is based on an automaton designed by Landin [Lan64] for mechanically evaluating mathematical expressions. The original SECD machine uses interpretation, and an eager evaluation strategy, to evaluate expressions of an applied lambda calculus, which provides the formal basis for function application and function de nition. The SECD machine comprises of an argument stack (S), environments (E) which hold the values of the free variables in expressions, a control stack (C) which is used to translate the expression under evaluation into a graph, and a dump stack (D) which stores copies of the machine state during the evaluation of subfunctions. The nodes in the graph are called closures . Such a closure consists of the body of a lambda abstraction, the bound variable

2.2 Review of Existing Implementation Approaches

21

and an environment, which ensures that the free variables are associated with the correct values when the abstraction is evaluated. If the translation and execution phases which interleave in the original SECD machine are separated, the basic principles of the SECD machine can provide a rather ecient machine model. There are several derivates of the SECD machine [Per91] which use a more conventional machine architecture by integrating the stacks S, E and D into one, and employ more sophisticated code generation techniques, which results in increased performance. The rst compiler-based implementation of a functional language was the G-machine, which was developed by Augustsson [Aug87] and Johnsson [Joh87]. A functional program is compiled into a supercombinator program through lambda lifting [Joh85]. A supercombinator [Hug82] is a lambda abstraction which neither contains further lambda abstractions, nor free variables. The G-machine implements graph reduction on supercombinator de nitions. It uses a xed size node representation. The basic reduction mechanism consists of repeated spine unwinding and updating until normal form is reached. The G-machine is a nite state machine. It consists of an argument stack, the heap, the G-code sequence to be executed7 and the dump. The heap stores the program graph; it is represented by a mapping of addresses to graph nodes. The argument stack is used during unwinding and function execution; it holds the addresses of the spine nodes. The dump is a stack of pairs, which are also called closures . They consist of a code sequence and an argument stack. The dump stores function calls which are suspended until an argument is reduced. Before running a program, each supercombinator body is translated to a sequence of machine instructions. When these are executed by the abstract machine, they rst construct an instance of the supercombinator body as a graph on the heap, re-using the arguments on the stack. The root of the supercombinator application is then updated with the supercombinator body. Afterwards the computation returns to another unwinding of the resulting graph. The G-machine provides a basic graph reduction model which can still be found in todays functional language implementations. Another compiled graph reduction machine is the Three Instruction Machine (TIM). The TIM [FW87] also evaluates supercombinators. Its key idea is to pack the arguments of a supercombinator into tuples. These tuples are then augmented by code pointers, forming closures which are kept in environments (called frames). Such a closure represents an application node, in the sense that the code de nes a function that is being applied to the arguments in the tuple. This is more advanced than closures in the SECD machine, where they were used to couple parts of the graph, rather than code, with an environment. A result of this approach, is that spines are no longer represented on the heap. Instead, the stack is used, which consists of a sequence of closures representing the spine; making the TIM a de facto spineless machine. This has an e ect on how sharing is supported in the TIM. In contrast to the G-machine, which performs updates after every reduction, the TIM only performs an update when the evaluation of a closure is complete. This can easily be recognised by emptying the stack onto a dump stack before a closure is entered. The evaluation of the closure proceeds normally, until weak head normal form is reached. At this stage the computation returns to an empty stack, which triggers updating. The TIM is a simple, yet e ective, graph reduction machine, which shows a better performance 7

The abstract machine code of the G-machine is called G-code.

22

Preliminary Considerations for an Implementation

than the G-machine. The Spineless Tagless Graph reduction machine (STGM) [PJ93] combines features of the TIM and the G-machine. It uses the spinelessness and the update mechanism of the TIM, and keeps pointers to heap objects on the stack, which is similar to the approach taken in the G-machine. The abstract machine language of the STGM is, itself, a very basic functional language. It has the usual denotational semantics which forms the declarative semantics of functional languages. However, each language construct can also be given a direct operational meaning. The operational semantics of the STG language (the code of the STGM) can be given in the form of a state transition system. The STG language is an unusual machine code because, in conventional abstract machines, the machine code consists of a sequence of machine instructions; each of which has a precise operational semantics, but is typically not covered by the declarative semantics of the source language. The STGM uses closures to achieve a uniform representation of data objects on the heap. Each closure consists of a code pointer together with an environment containing the data on which the code operates. The machine uses a tagless representation; this means, instead of using tags to distinguish between the di erent kinds of heap objects, code pointers are used. The di erent treatment of di erent kinds of objects is encapsulated in the code of the heap object, which can be specialised towards the objects kind. Checking tags can therefore be avoided; instead, a jump is made to the code pointed to by the objects closure. The STGM is used in the Glasgow Haskell compiler. It is a highly optimised machine which has shown to be very ecient; it is considered to be the fastest implementation of a lazy functional language.

2.2.3 Implementation of Logic Languages The Warren Abstract Machine (WAM) has become widely accepted as standard for the implementation of the logic language Prolog.8 It was rst developed by Warren in 1983 [War83], and introduced two new methods: the compilation of uni cation into a set of uni cation instructions; and the ecient implementation of backtracking through a choice point technique. A mathematical analysis of the WAM, and a proof of its correctness, is given in [BR92]. A complete tutorial on the WAM can be found in [AK91]. The WAM features a stack-based architecture extended by a tagged term representation and a choice point technique. The machine components are: a frame stack, a choice point stack, a trail stack, argument registers, a heap and a push-down list. The frame stack is used to implement (recursive) procedure calls. It contains a number of procedure activation frames, also called environment frames. Each of these holds the information needed for the correct execution of what remains to be done after returning from a procedure call. This comprises of the address of the code area of the next instruction on (successful) return (i.e. the return address), the stack address of the previous environment to restore upon return, and the values of the local variables of the procedure. The A Prolog program consists of a nite set of clauses; [SS86] gives an introduction to programming in Prolog. For execution on the WAM, each clause is translated into a sequence of uni cation instructions for the clause head, followed by a sequence of calls to predicates in the clause body. The code sequences of multiple clauses de ning one predicate are combined and form one procedure. 8

2.2 Review of Existing Implementation Approaches

23

arguments to a procedure call are passed in dedicated machine registers, called argument registers. Backtracking is a method to manage a sequential ordered search through a space of alternatives. It implements don't-know non-determinism, and has become the basic concept used, to support multiple solutions, in sequential implementations of logic programming languages. In the WAM, the selection of alternative branches within a procedure is controlled by backtrackable conditional statements. The conditions are uni cation instructions. When a branch has been selected, a choice point is created. A choice point is represented by a record (called choice point frame) from which a correct state of computation can be restored to allow another alternative to be tried, should the selected alternative fail. Choice points are organised as a stack (the choice point stack) to re ect that each of them spawns potentially more alternatives to try in sequence. An interesting view on the two stacks is to call the frame stack the AND-stack and the choice point stack the OR-stack. These names emphasise that on a successful execution of a procedure call, the remaining part of the computation is recovered from the AND-stack. Entries on the AND stack can therefore be seen as success continuations. In contrast, on a failed procedure call, the next alternative is taken from the OR-stack, which contains failure continuations. Parallels can easily be drawn to the de nitions of the two logical connectives. Section 2.2.4.2 returns to this concept, by using it to support the functionality of conjunction and disjunction on the machine level. In practice, the frame stack and the choice point stack are implemented in one stack, called the local stack. A choice point stores the state of the computation, in particular the value of the argument registers, the top of the frame stack, the address of the code area of the next instruction, the previous choice point, the next alternative, and pointers to the trail stack and heap. When uni cation fails, the WAM has to backtrack to the most recent choice point which provides other alternatives to be tried. The machine state held in the latest choice point is restored. To be able to undo all e ects of the failed computation, the trail stack keeps a record of those variables which need to be reset to unbound upon backtracking. The heap (sometimes referred to as the global stack) of the WAM stores variables and structures representing terms.9 Explicit tags are used to discriminate between the two kinds of heap objects. A variable is identi ed by a reference pointer, and is represented using a single heap cell. An unbound variable is represented by a tagged pointer to itself. When a variable is bound, this pointer is exchanged by a pointer to the binding object. Dereferencing is performed before an object is accessed; this avoids (amongst other things) binding chains to be created when variables are bound to unbound variables. Thus, variables are only bound to unbound variables or non-variable objects. The e ect of dereferencing is simply the composition of variable substitutions. The push-down list (also known as the uni cation stack) is a stack which supports the recursion in the uni cation operation of the WAM. The representation of variables in the WAM allows fast, constanttime access to variables; creating a binding involves writing it to the respective heap cell, and trailing it in the push-down list; accessing a binding simply means reading a value In the context of logic programming ( rst-order) terms are either variables, constants or a saturated application of a function symbol to terms; a constant can be seen as a special case of an application. 9

24

Preliminary Considerations for an Implementation

from a heap cell. Improved versions of the WAM are the Berkeley Abstract Machine (BAMr) [Roy90] and the Vienna Abstract Machine (VAM) [KN90]. The BAMr is based on a slightly modi ed machine architecture compared to the WAM. It uses a completely new, lower level instruction set, which allows code to be optimised further. The BAMr is used for the Aquarius Prolog Compiler. The VAM has an optimised uni cation procedure, which delays the construction of terms. There are di erent versions of the VAM which are specialised for di erent purposes like interpretation, native code compilation and fast abstract interpretation. Another interesting specialisation of the WAM was developed by Tarau [Tar96] to support BinProlog. Programs are transformed into binary programs (each predicate has only one atom in its body) before execution. With this representation, a continuation passing style can be adapted, which allows quite elegant optimisations, and also space for inlining. Instead of using the frame stack, a continuation (which is encoded in the last argument of each clause in the binary program) is put on the heap. This increases the heap consumption of the program, but Tarau has used partial evaluation techniques to minimise this problem.

2.2.4 Implementation of Functional Logic Languages A variety of approaches has been taken to implement functional logic languages. In general, two groups can be distinguished: extensions of the WAM e.g. the K-WAM [BCM89], the AWAM [Han90], and extensions of reduction machines such as the LBAM [KLMNRA90b], the LANM [Loo91] and CAMEL [Muc92]. These machines are developed to support speci c languages. In contrast, the JUMP machine [Loc93] aims to provide an orthogonal combination of the WAM and the STGM, by systematically integrating the operational concepts required to support both paradigms. It can be used as a generic basis to support a whole class of languages in a uniform way. All these machines can be classi ed as narrowing machines, since they support narrowing in one form or another as a basic operational model. The HOLM [Cha95] is an extension of the STGM which re-uses concepts of the JUMP machine, but is based on rewriting with residuation. The major representatives of the narrowing machines, and also the HOLM, will now be reviewed below.

2.2.4.1 Narrowing Machines Let us rst turn to WAM-based narrowing machines. In order to implement a functional logic language, the WAM needs to be extended by a function reduction mechanism, and support narrowing. It has been shown [BGM89] that leftmost innermost basic narrowing can be supported by SLD resolution if the program has been transformed to a at program. As a consequence, the WAM can be used to directly support this form of narrowing on at programs. The K-WAM10 was developed by Bosco et al. [BCM89] as a machine to support the language Kernel LEAF [GLMP91]. It is a WAM extension which implements an outermost resolution strategy. The K-WAM supports lazy evaluation, and has a suspension/reactivation mechanism, so that function calls are only activated when they 10

The K in K-WAM stands for the language supported by the K-WAM, which is called K-Leaf.

2.2 Review of Existing Implementation Approaches

25

are needed. Closures are used in the same way as in functional language implementations, viz. to encapsulate unevaluated expressions. The A-WAM [Han91] is another WAM extension. It implements the language ALF, which is an Algebraic Logic Functional language [Han90]. The operational semantics of ALF is based on SLD resolution for predicates, normalising innermost basic narrowing for functions and rewriting. The A-WAM uses a new stack, called an occurrence stack, to select the next redex. The occurrence stack holds the basic positions11 in the term under evaluation in leftmost innermost order. The next narrowing position can be accessed on the occurrence stack in constant time. Extra machine instructions are used to manipulate the occurrence stack. One can think of the occurrence stack as an instance of a spine stack in functional language implementations. The A-WAM also has new instructions, which are used to replace terms on the heap with new terms, thus supporting function reduction. The manipulation of terms on the heap is trailed, so that it can be undone on backtracking. A useful feature of the A-WAM is its modularity. The new instructions and the occurrence stack are only used when a program contains function de nitions. These can be written as conditional equations in ALF. The code, generated for the A-WAM, shows that functions are compiled similarly to predicates. Purely functional programs are reported to run very eciently due to the fact that the A-WAM prefers deterministic rewrite steps to non-deterministic narrowing or resolution steps, which are only performed if no further rewriting is possible. The code, generated for purely logic programs, corresponds to the code which would be used for an execution of the program on the WAM. The review of WAM-based machines ends here. This section now continues with an examination of abstract machines, for functional logic languages, which are based on machines that support the graph reduction model of functional languages. To implement integrated languages (that have a computational model based on narrowing) on a functional machine, one needs to additionally support (logic) variables, uni cation and the ability to provide multiple solutions (which is commonly supported by backtracking). The machine for the functional logic language immediately bene ts from the graph-based representation inherent in machines which support functional languages. This allows sharing to be exploited. Loogen [Loo91, Loo93] has extended a graph reduction machine to support a subset of the functional logic language BABEL [MNRA92]. The major machine concepts are a stack and a heap to store the graph structure. Terms and variables are represented by tagged nodes in the graph. As usual, the evaluation is controlled by the stack. The stack integrates a dump, an environment stack and a choice point stack. The basic graph reduction machine was rst extended by an innermost narrowing strategy. Later, new heap nodes were introduced to represent unevaluated expressions and function calls, and the instruction set was extended by new instructions which generate, evaluate, and update these nodes. The resulting narrowing machine was capable of supporting a lazy evaluation strategy; it is sometimes referred to as the Lazy Narrowing Machine or LANM. The machine structure resembles the one of the WAM; it has, however, an explicit stack to In basic narrowing, a narrowing step is only performed at a subterm which is not part of a substitution; it must belong to a program statement or the initial goal. The position of such a subterm, relative to the term in which it occurs, is called basic position. Basic positions can be computed at compile time. 11

26

Preliminary Considerations for an Implementation

pass arguments to function calls. Backtracking is implemented through choice points, which are placed on the stack, and a trail stack, to keep track of what needs to be undone on backtracking. The code generated for the machine is similar to WAM code for attened functional logic programs. Even though choice points may be created for purely functional computations, no goal variables are bound during function application because purely functional computations are ground. Moreover, purely functional computations can be detected at run time, and can therefore run deterministically. The BAM12 is another graph-based reduction machine with WAM-like extensions. It supports the functional logic language BABEL [MNRA92]. The rst BAM implementation [KLMNRA90a] supports innermost narrowing. The main machine component is a graph which contains task nodes for each evaluation of a function call. The BAM is di erent from the machine described in the previous paragraph, in that it relies purely on a graph representation, and it is not stack-based like the LANM. It supports a \stackless" choice point technique, where the backtracking information is stored in the graph nodes directly. This makes BAM code particularly suitable for a parallel execution. A parallel implementation of the BAM on a shared memory multiprocessor is described in [KMNH92]. It supports independent AND-parallelism; i.e. the subexpressions of an expression can run in parallel. There is also an extension of the BAM, the LBAM, which supports a lazy evaluation strategy [KLMNRA90b]. The components of the LBAM are the program store, which holds the machine code; the graph, which contains constructor, variable and task nodes; and the active task pointer, which points at the task node that represents the function call currently under evaluation. The evaluation is controlled by the task nodes in the graph. These correspond to activation records of function calls, but contain some more information to support backtracking. Muck has extended the Categorical Abstract Machine [CCM85] to support functional logic programming languages with the aim to validate the correctness of the implementation. The base machine supports functional programming. It is very simple, consisting of a code area, a value stack and a graph of values. The CAMEL13 [Muc92] supports a backtracking mechanism via choice points, which are kept in the value stack, and has uni cation instructions. Variables are implemented by an additional data structure to represent them in the graph. Muck uses partial evaluation techniques to translate a functional logic program into machine code. The CAMEL supports an eager evaluation strategy. Muck points out, however, that the same techniques which apply to the CAM can be used to turn the CAMEL into a lazy machine. The JUMP machine has been developed by Lock [Loc93] and Chakravarty [CL94]. They claim to provide a generic basis for the implementation of functional logic languages which have higher-order term rewriting systems as syntactic foundation and extended narrowing with rst-order uni cation as their operational semantics. The machine language is based on an enriched lambda calculus, containing for example operations for choice, uni cation, and control annotations for evaluation strategies and sharing. The language of the JUMP machine resembles the STG language, which is used in the STGM. The JUMP machine is a narrowing machine which represents an orthogonal combination of the WAM and the 12 13

BAM stands for BABEL Abstract Machine. CAMEL stands for Categorical Abstract Machine with Logic Extensions.

2.2 Review of Existing Implementation Approaches

27

STGM. It was designed by systematically analysing existing approaches, identifying the underlying operational concepts, and integrating the best of them to support both the functional and logic programming paradigm. In particular, the JUMP machine incorporates a conventional stack-based architecture with a choice point technique, and closures for the uniform representation of all kinds of functional and logic machine objects; a combination which almost guarantees very ecient expression evaluation. A closure is formed by a pair consisting of a code pointer, and an environment which stores the values of the variables that are accessed by the closures code. The value of a closure is obtained by jumping to the code address of the closure. As in the STGM, the overhead of tag-testing is thereby eliminated. Such closures can be regarded as active objects since they are not passively manipulated by the machine, but participate actively in the process of evaluation. A parallel to the object-oriented paradigm can easily be drawn if the code of a closure is called method and the environment is regarded to hold the arguments of the object. The use of closures as active objects gives rise to a variety of optimisations, one of them being a new way to implement uni cation, called threaded uni cation. Instead of the usual uni cation procedure which recursively traverses the terms that need to be uni ed, additional methods are given to uni able objects, such as variables and data constructors. The uni cation is, thus, driven by the code of the objects, and can therefore be specialised towards each object's kind. This results in a more powerful and ecient implementation of uni cation. The main machine components of the JUMP machine are: a stack, for activation records of functions and choice points; a heap, which stores environments and closures representing data, unevaluated expressions and (logic) variables; and a trail, which stores the modi cations to the machine state which need to be undone in the case of backtracking. If no variables occur during a computation, no choice points are created and the code is executed as a purely functional program. The design of the JUMP machine represents, to the author's knowledge, the most comprehensive study of integrating functional logic languages on the implementation level. The approach has been used to develop a compiler for the language Guarded Term ML (GTML); an unoptimised version of the compiler has been shown to be reasonably ecient. An extension of the JUMP machine is described in [Cha94]. It explicitly supports parallel evaluation, and uses the closure concept to integrate mechanisms for communication and distribution. The major representatives of compiled narrowing machines have now been reviewed. Most of them, with the exception of CAMEL and the JUMP machine, use some form of WAMlike code as instruction set. One possible reason for this might be that these machines were designed by people who come from the \logic programming community", and who, therefore, base an implementation of an integrated functional logic language on the principles they are familiar with. Lock points out, however, that WAM code can actually block optimisations, especially regarding uni cation, which are possible in the JUMP machine. However, the design, and the basic concepts, used in the existing machines are similar. The machine core has typically a stack-based architecture; the structure of the stack controls the execution of the program. A heap is used to store variables, data objects and unevaluated expressions; some machines use closures to represent objects on the heap. To support multiple solutions, backtracking is implemented via a choice point technique and a trail stack. These machine components and techniques will play an important role in Section 2.3, where an appropriate abstract machine design for Escher is discussed.

28

Preliminary Considerations for an Implementation

2.2.4.2 The Higher-Order Logic Machine The Higher-Order Logic Machine (HOLM) was developed as an abstract machine for the ecient implementation of Escher-like languages by Chakravarty [Cha95]. It is based on concepts of the STGM and the JUMP machine, and aims to support a subset of Escher, called Escher, . The machine language is a minimal, explicitly typed, functional language. The language is based on the simple theory of types, like Escher. It can be given both a declarative semantics, using general models, and a direct operational meaning. The constructs of the language are typed lambda terms. A special syntax is provided for the logical connectives; this allows them to be recognised and evaluated in a special way. The machine includes the functional core present in the STGM and the JUMP machine, consisting of a code area, a heap, a global environment which de nes the heap locations of the globally de ned functions, and the three stacks: argument stack, return stack, and update stack. The argument stack contains the arguments passed to heap objects, which are represented by closures. The return stack maintains return continuations to support recursive function evaluation, and the update stack supports sharing. The HOLM uses vectored returns [PJ93], an optimisation introduced for the STGM. The return stack contains a vector of alternative continuations. When an active object returns, the appropriate continuation is selected depending on the kind of the returning object. It is argued that the use of return vectors leads to a very ecient treatment of returned data, in particular pattern matching. The extensions to support the logic features of Escher are two stacks: one for success continuations, and the other for failure continuations. They support conjunction and disjunction, which push their second argument as success or failure continuation respectively, and then evaluate the rst. As described for the WAM in Section 2.2.3, the success continuation stack can be compared to the frame stack, and the failure continuation stack to the choice point stack. A trail stack is used to keep track of variable bindings and updates of closures through sharing, which need to be undone when a failure continuation is selected. To support residuation, an extra machine component holds a number of waiting lists. There is one list for each unbound variable, containing the computations which are currently deferred, \waiting" for this variable to be bound. When the variable becomes bound, these computations are reactivated. A set of transition rules de nes the operational semantics of the HOLM. From the above machine description, it becomes clear that the HOLM does not implement Escher's pure rewriting computational model, but instead uses the traditional backtracking approach to search for multiple solutions in a goal. Moreover, pattern matching on functions, as is allowed in Escher, is not supported. As a consequence, the rules de ned in the Escher Booleans module, especially those for negation and set processing, are not supported. Also, in HOLM, functional computations are embedded in logic computations. This restricts pattern matching on booleans, which is enforced by distinguishing between the types of formulae and the conventional boolean data type which de nes the two constructors True and False. As a consequence, formulae contained in arguments of functions cannot be evaluated unless they have reached the \top-level" of a computation, which Chakravarty calls the \logic" level. This restricts the scope of Escher computations signi cantly. In real Escher, logic computations are intended to be embedded in functional

2.3 Implementing Escher on an Abstract Machine

29

computations, thus several \logic" levels might be established during a computation. The HOLM provides a machine model which is much closer to supporting Escher than any of the narrowing machines discussed in the previous section. The Escher implementation has been based on some of the concepts used in the HOLM (especially the closure representation and the machine instructions), but started with a less specialised base machine. As pointed out by Chakravarty, the most challenging feature of Escher to support eciently, is the pure rewriting model, which only uses simplifying rewrites to compute alternative solutions, rather than backtracking. The operational semantics of the Escher Machine, which supports pure rewriting, is de ned in Chapter 5.

2.3 Implementing Escher on an Abstract Machine The computational model of Escher, and the aspects in which Escher di ers from Haskell have been introduced and discussed. Also, the preceding sections have reviewed existing abstract machines supporting functional logic languages, and discussed the implementation concepts which are used to support the features that are typical for a functional logic language. This section will now apply this knowledge to nd an appropriate starting point for an Escher implementation and systematically de ne the extensions necessary to support Escher.

2.3.1 Selecting the Base Machine It is necessary to start with a machine that supports the basic rewriting computational model of Escher, in particular redex selection and detection, and the reduction of function calls. A kind of graph reduction machine would be most suitable. The selection rule of the Escher machine shall be xed (for this particular machine) as leftmost outermost. An important aspect is also the ecient representation of expressions on the machine level. The previous section showed that closures are used in di erent forms for this purpose. They have developed from a primitive form of coupling graphs with environments in the SECD machine [Lan64], to active objects in more recent implementations such as the STGM [PJ93], the JUMP[CL95] machine, and the HOLM [Cha95]. To recapitulate quickly, a closure consists of a code pointer and an environment which provides the values of all variables that are accessed by the closure's code. A closure is evaluated by executing its code. Closures are a powerful concept, since they provide a simple and uniform representation for various kinds of objects, like unevaluated expressions, functions, constructors etc. They have been identi ed by Lock [Loc93] as a facility for the smooth integration of functional and logic features, especially unevaluated expressions and variables. Hence, the base machine for the Escher implementation should use some kind of closure representation. Finally, for obvious reasons, the machine needs to be ecient. Escher can be seen as an extension and modi cation of the purely functional language Haskell. A very ecient abstract machine which supports Haskell is the STGM. However, the STGM is a highly optimised abstract machine which appears to be complicated to

30

Preliminary Considerations for an Implementation

modify and extend. The Brisk14 machine [HS97] is a simpli ed version of the STGM, having had most of the optimisations removed. In fact, the Brisk machine makes the internal representation of expressions match the original graph reduction model very closely, because many of the highly specialised execution details are not incorporated directly into the machine. The pure graph reduction model can be easily adapted towards various programming paradigms, yet it is exible enough for later optimisation. This makes the Brisk machine an excellent starting point for an abstract machine that supports Escher. The main components of the Brisk machine are reviewed in the next section.

2.3.2 The Brisk Machine The language of the Brisk machine strongly resembles the STG language [PJ92] which is used for the same purpose in the Glasgow Haskell compiler (ghc), but is even more simpli ed. A collection of built-in functions is provided to encapsulate complex issues like evaluation, partial application, and application. Calls to the built-in functions are introduced during the translation into the machine language. Before coming to the machine state of the Brisk machine, the way in which programs are represented on the machine level will be examined. This is described in detail in [HS97]. The representation is based strongly on that used for the STGM. However, it is simpli ed, and made more uniform, so that the link with conventional graph reduction is clearer. This makes it easy to adapt the machine to alternative uses. At the same time, enough

exibility is retained to allow most of the usual optimisations to be included in some form.

2.3.2.1 Representing Programs on the Brisk-Machine: The Heap In Brisk, a Haskell program is represented as a graph. The program graph is held as a collection of nodes on a continuous block of memory, the heap. Every node in the heap can be thought of as a function call. A spineless representation is used; nodes vary in size, with the number of arguments being determined by the arity of the function. For example, an expression f x y z is represented as a 4-word node in which the rst word points to a node representing f, and the other words point to nodes representing x, y and z; a node of this form is a call node . There is a single root node representing the current state of a computation, and all active nodes are accessible from it. Free space is obtained at the end of the heap, and a copying or compacting garbage collector is used to reclaim dead nodes. In general, functions may have both unboxed and boxed arguments, with the unboxed ones preceding the boxed ones, and the arity of a function re ects how many of each. A node may thus, in general, consist of a function pointer followed by a number of raw words, followed by a number of pointer words. Constructors with unboxed arguments can be used to represent raw data. A node representing a function contains information about the arity of the function, the code for evaluating the function, code for other purposes (such as garbage collection or 14

Brisk stands for Bristol Haskell compiler.

2.3 Implementing Escher on an Abstract Machine node:

function

info node:

function ... constructor

31

arguments

arity code

...

references ...

...

Figure 2.3: A heap node as a function call debugging), and all references to other nodes needed by the evaluation code (such as global functions). The arity information allows the function node to be used as an info or descriptor node , i.e. a node which describes the size and layout of call nodes. To ensure that the size and layout information is always available, the function pointer in a call node must always refer to an evaluated function node, and not to an unevaluated expression which may later evaluate to a function. During the compilation into the machine language, built-in functions for application and partial application are introduced to guarantee that this is the case. This uniform representation of nodes means that any heap node can be viewed in three ways. First, a node represents an expression in the form of a function call, as in Figure 2.3. Second, a node can be treated as a data structure which can be manipulated, e.g. by the garbage collector or by debugging tools, in a uniform way, as shown in Figure 2.4. node:

info node:

arguments

info

...

...

l

n ... ...

...

size and shape of node e.g. node length (l), number of arguments (n)

Figure 2.4: A heap node as a uniform data structure Third, a node can be regarded as an active object, responsible for its own execution, with methods for evaluation and other purposes available via its info pointer, as in Figure 2.5. Function nodes follow the same pattern as call nodes. The function pointer at the beginning of a function node can be regarded as a (function) constructor. Thus functions are represented using a normal data type which is hidden from the programmer by an abstraction. All functions, including global ones, are represented as heap nodes which contain references to each other. New function nodes can be created dynamically to cope with special situations. The Brisk machine supports both statically compiled code and bytecode, which can be dynamically loaded into the heap. The interested reader is referred to [HS97]

32

Preliminary Considerations for an Implementation object:

desc

class descriptor:

desc

arguments

...

... ...

c

g

t

...

evaluation code garbage collection support debugging support

Figure 2.5: A heap node as an active object for more details of this machine feature. This reference also describes how raw (i.e. unboxed) data is handled in the Brisk machine. This issue is not addressed in detail here, since the focus of this work is much broader, and the Escher machine simply adapts the same strategy as the Brisk machine in this case. In fact, the issue will be ignored as much as possible, to concentrate on the main aspects of the Escher implementation. The same applies to the handling of constant applicative forms, and the (optimised) representation of constructors on the machine level. To summarise, the structure of nodes on the heap of the Brisk machine provides a simple and uniform closure-like representation, which is exible enough to support the features of Escher.

2.3.2.2 The Stack of the Brisk Machine The stack of the Brisk machine keeps track of the current evaluation point. It represents the path from the root node down to the current node. The stack pointer points to the top stack node. Each stack entry consists of a pointer to a node in the heap representing a function call, which waits for an argument to be evaluated and, in some form, the position within that node of an argument which needs to be evaluated before execution of the call can continue. Figure 2.6 gives a rough sketch of the principle. However, in the implementation, the stack is actually organised as a linked list of stack nodes which is held on the heap. Each stack entry has a slot containing a pointer to the stack frame previously pushed onto the stack. The stack base has a null pointer at this place. Stack nodes follow the same layout as other heap nodes, i.e. they have a stack node descriptor, followed by some arguments. Figure 2.7 shows the organisation of the stack as a linked list of heap nodes. A stack entry can, thus, be thought of as a stack frame. The descriptor represents a continuation. Its code speci es what to do next when the graph \below" has been evaluated. When the current node cannot be evaluated any further, a return is performed. This invokes the continuation function on the stack. It rst plugs the current node into the previous node (which is pointed to by the stack pointer) at the speci ed argument position, then makes the previous node the new current node, and pops the stack (by setting the stack pinter to point to the previous stack node).

2.3 Implementing Escher on an Abstract Machine 1

2

Stack:

Stack Pointer:

3 1 4

x’ 1

y’

33 3 x

4

1 y

2

2

3

4 z

z’

Current Point of Evaluation:

Figure 2.6: The basic principle of the Brisk machine stack Stack: (base) stk 3

1

2

4

1 y

2

2

3

NULL

x’ stk 1 y’ Stack Pointer:

3 x

1

stk 4

Current Point of Evaluation:

4 z

z’

(stack node descriptor)

Figure 2.7: The organisation of the Brisk machine stack

2.3.2.3 The Architecture of the Brisk Machine The machine state of the Brisk machine consists of a code component, the current node pointer, a heap and a stack. Unlike in the STGM, there are no argument or update stacks. The current node is a pointer into the heap, pointing to the node that represents the (sub)expression currently under evaluation. The heap contains a collection of nodes which hold the program graph. Depending on the kind of object, the evaluation code in the corresponding heap node is either: for compiled functions, a machine language expression; for constructors, a simple return; or for built-in functions, the entry point for a procedure which implements the built-in function. The stack corresponds to the return stack of the STGM. It keeps track of the current evaluation point. The code component can take one of four instructions which are used to enter nodes, execute the evaluation code of a node, evaluate a machine language expression, or simply return to the continuation on top of the stack. An evaluation step consists of calling the evaluation code for the current node. If the

34

Preliminary Considerations for an Implementation

current node cannot be evaluated any further, the code causes an immediate return. Otherwise, the code carries out some processing, usually ending with a tail call . A tail call consists of building a new node in the heap and making it current. The operational semantics of the Brisk machine can be given in the form of a state transition system. There are two kinds of transition rules. One speci es the basics of a reduction, like entering nodes on the heap, executing the code of a function, evaluating machine language expressions and returning from subcomputations. These are quite simple transitions. The more complicated issues, like argument evaluation, partial application and application, are encapsulated in built-in functions. A number of specialised transition rules de nes the execution of the evaluation code for each built-in function. The Brisk machine provides the basic leftmost outermost graph reduction mechanism on which an Escher implementation can be built. It o ers a exible representation of expressions on the machine level; the built-in functions can be modi ed easily and the machine model is open to the addition of new built-in functions.

2.3.3 Extensions and Modi cations to support Escher on Brisk Having introduced the Brisk machine, it is now possible to identify what extensions and modi cations are necessary to support Escher. This will be done using the points from the resume in Section 2.1.6 (which discussed how Escher di ers from Haskell) as orientation. In the following sections, a brief overview of each extension or modi cation is given. References are provided to guide the reader to those sections (elsewhere in this thesis) containing a more detailed discussion of a particular extension or modi cation.

2.3.3.1 Integrating Quanti cation and Set Processing Both quanti ers and set expressions need to be integrated into the language of the abstract machine. In the case of the quanti ers, this can be achieved by introducing two new builtin functions (one for each quanti er). The compilation of quanti ers is called quanti er lifting , and is described in Section 3.2.3.2. On the machine level, existentially quanti ed variables are kept in a dedicated machine register, the Existentially Quanti ed Variables Register (EQVR). There is no need for an explicit representation of the function exists in the graph representing the program, since the rewrite rules for exists can be supported by other built-in functions as discussed in Section 4.2. The universally quanti ed variables are also kept in a dedicated machine register, the Universally Quanti ed Variables Register (UQVR). However, a series of built-in functions is used to support the evaluation of restricted universal quanti cation in several stages; details are given in Section 4.3. The integration of sets requires that a set can be represented in a exible way, which allows it to be used both, for pattern matching (i.e. as data), and in an application (i.e. as a function). For this purpose, a new meta-constructor is introduced into the machine language. Set patterns in statement heads can be treated similar to other (nonset) patterns, i.e. to force the evaluation of an argument. However, a special evaluation function evalSet is introduced to evaluate an argument to set (pattern) representation on the machine level. The representation and evaluation of sets is described in detail in

2.3 Implementing Escher on an Abstract Machine

35

Section 3.2.1.2.

2.3.3.2 Reduction of non-ground Redexes The Brisk machine only supports the reduction of ground function calls. Escher adds logic features to functional programming. This allows computing with non-ground expressions; residuation is used to deal with insuciently instantiated function calls. In order to support this aspect of Escher, we rst need to be able to represent variables as objects on the heap, and then introduce mechanisms to handle the issues related to computing with variables (like the evaluation of variables, residuation, substitution application and sharing in the presence of variables). To represent variables on the machine level, a new info node is introduced. Each variable is then represented by a 1-word node which points to this info node. If a variable occurs more than once in an expression, all occurrences refer to one variable node, i.e. the variable is shared. Variables are active objects in the sense that the info node contains code which is executed when a variable is evaluated. The evaluation of a variable node has di erent results depending on whether the variable is already instantiated or not. A variable is said to be instantiated when it is bound to a non-variable expression. When an instantiated variable is evaluated, the expression that the variable is bound to is returned. This way, the variable is substituted by its binding. It is important to realize that updating the variable node with a reference to the binding (e.g. as used in the WAM) is not a valid solution. This is due to the fact that the aim is to implement Escher with a pure rewriting model, i.e. without using backtracking to search for multiple solutions. Because the node representing a variable might be shared, even across the branches of a disjunction, the variable can be bound to di erent expressions in each branch of a computation. In general, di erent bindings might apply to the same variable, if it occurs in di erent conjunctions. This leads to the fundamental question underlying or-parallel implementations, which is: How to represent di erent bindings of the same variable corresponding to di erent branches of the search space? Several techniques to solve this problem have been proposed; a survey is given in [War87a]. Here, an environment technique is used, where each expression is evaluated in a speci c environment. The environment is a data structure which holds a binding for each variable contained in an expression. Environments also have a status which indicates whether the environment is read-only (status READ) or not (status WRITE). Every function call is evaluated in a particular environment. Especially, di erent branches of a search space are evaluated in di erent environments, which allows the same variable to be bound to di erent expressions in each branch. Environments can be propagated via the built-in functions that handle evaluation. In Section 4.6 the role of conjunctions for environment propagation is discussed. Let us now turn to the evaluation of variables which are not yet instantiated (in the given environment). In this case, the function call, which demanded the evaluation of the variable, needs to be deferred until the variable is instantiated. A defer mechanism is used to support residuation. A deferred computation is encapsulated in so-called defer nodes . These defer nodes contain activation conditions which allow the deferred function call to

36

Preliminary Considerations for an Implementation

be re-evaluated when the respective variable is instantiated. To integrate residuation into the evaluation model of the Brisk machine, the functionality of the built-in functions that control evaluation needs to be extended. This is described in detail in Section 3.2.1.1. Defer nodes are active heap objects as well. If the activation condition is not ful lled, they simply return, otherwise they activate the deferred function call which can then be re-evaluated. Through the encapsulation, deferred computations remain part of the graph which represents the expression under evaluation. If they do not become active during the computation, deferred function calls can become part of an answer to an Escher program. In Section 2.2.1.3, sharing was discussed, and was shown to be an essential concept to support lazy evaluation. In the Brisk machine, sharing can be implemented through the evaluation built-in functions. They use updating to replace an unevaluated expression with its evaluation result; hence ensuring that each expression is evaluated at most once. This technique cannot, in general, be used when non-ground expressions are evaluated. This is because an expression containing variables can, depending on the binding of the variables, reduce to di erent results. In Section 3.2.2 the possibility of sharing in the presence of variables in function calls is examined.

2.3.3.3 Matching on Functions The vision of Escher is to overcome the strong separation of data constructors and functions that is typically found in functional languages such as Haskell. Escher allows function symbols to be used for pattern matching in the de nition of system functions. Examples include the laws for distributing (&&) over (||), which are contained in the de nition of (&&); or De Morgan's laws, which are part of the de nition of not. Even though the statements used to bind variables (e.g. in the de nitions of (&&) and exists) also contain several de ned functions in the statement heads, these statements are not implemented via matching on function symbols. Instead, they, and also some other statements for system functions, are supported by di erent built-in functions and machine registers. This is explained with an example in Section 4.1.1. In addition, user-de ned functions can match on the three set patterns introduced earlier in Section 2.1.3.2. Clearly, the pattern matching mechanism of Haskell, which is constructorbased, needs to be extended to support pattern matching on function symbols and sets. A fundamental requirement is that in each function call, whether it is a call to a de ned function, a built-in function or a constructor (function), the function can be identi ed to allow pattern matching on it. The function (or info) nodes used in the Brisk machine already provide a uniform representation of all objects on the heap. These nodes can be extended so that they hold an identi er for each heap object in addition to the standard layout information, evaluation code, etc. Section 5.1.7 describes the extended node structure used in the Escher machine. In addition to being able to identify all functions, a mechanism is needed to stop the evaluation of an argument when it has reached a form on which the function, that demanded the evaluation, can match. This mechanism is called look-above , and is introduced in Section 4.1.2. It is based on the idea that a function, say f , which is matched on by another function, say g, can, before it does any rewriting itself, check the identi er of the function

2.3 Implementing Escher on an Abstract Machine

37

in the call node on the stack. Remember, the call node on top of the stack is the node representing the call to the function that forced the argument evaluation. If the identi er of the function in the call node on top of the stack is the identi er of g, the function f behaves like a constructor and simply returns. The pattern matching, usually done by the \outer" function (in this case g), is triggered by the \inner" function, and thus outermost reduction can be preserved.

2.3.3.4 Statement Order Since the order of statements in an Escher program does not matter, a concrete implementation is free to choose some order. To allow a high degree of compatibility with existing Haskell code, the implementation on EMA will execute statements in the order in which they are given in the program.

2.3.3.5 Di erent Declarative Semantics Haskell is based on the lambda calculus with the typical denotational semantics used for functional languages. The logic underlying Escher is type theory, and the declarative semantics is based on Henkin's general models. Both the denotational semantics, and the model-theoretic approach used in Escher, assign values to expressions, and thus de ne the declarative meaning of programs. However, there are subtle di erences between the two approaches, e.g. concerning the treatment of types, which are typically ignored in the denotational approach. Up to now, the relationship between the two views on declarative semantics has not been studied in detail. This issue is out of the scope of this work, but certainly an interesting area for further research, which will sooner or later need to be addressed in order to provide an \integrated" semantics for integrated languages. Despite the di erences on the declarative semantics, it turns out that the basic computational model of Escher (i.e. rewriting with residuation), is an extension and modi cation of the pure rewriting computational model of lazy functional languages such as Haskell. Based on this fact, an implementation of Escher can be achieved by making the appropriate changes to an abstract machine that supports Haskell.

Chapter 3

Compilation of Escher The main focus in this thesis is on the design of the Escher Machine, which forms the back end of the compiler. However, the compilation from Escher into the language of the abstract machine is closely related. For this reason, a short overview describing the complete compilation route is given here. The compilation of Escher programs involves the following steps: 1. The primary source language is Escher, a strongly-typed, higher-order, non-strict, functional logic language. 2. Escher is compiled to a small subset of Escher, called core Escher. Type checking is performed, and overloading is resolved. Sets which occur in function bodies, except for those in the alternatives of case statements, are translated into lambda abstractions according to Table 3.1. The variable x, in the rst three rows of this table, is a new variable which is introduced with the lambda abstraction; it does not occur in t, nor in any of the ti 's. Escher

core Escher

{} { } { 1 { | }

\ \ \ \

t t ; : : : ; tn } xu

x x x x

-> False -> ( == ) -> ( == 1 || ->

x t x t u

: : : || x==tn)

Table 3.1: Translating sets into core Escher Set expressions of the form {x|u} occurring in statement bodies are not translated into lambda abstractions when the set variable x also occurs in the head of the statement. If the latter is not the case the translation can be performed. This distinction ensures that sets, which occur in statement bodies, and which contain a set variable that also appears in the head of the statement, remain in the context of the statement. As a consequence, the link between the (set) variables in the statement head and those in the set pattern(s) in the statement body will not be

40

Compilation of Escher broken through lambda lifting. In the following, those sets which are translated into lambda abstractions, will be referred to as liftable sets . 3. Next, program analysis and a variety of transformations can be applied to the program in core Escher. This is a topic for further research. 4. The program in core Escher is translated into Escher Kernel Language (EKL).

The Escher Machine works on the level of EKL. As a further step of compilation, a code generator could translate an EKL program for instance into C, or directly into machine code. This compilation stage is still open and a topic for further research. However, it should be noticed that a code generator exists which translates from the Brisk Kernel Language into BAM1 code [HS97], a code much like the bytecode instructions used by the original G-machine [Aug87, Joh87]. A similar approach is expected to be suitable for Escher. This chapter consists of two parts. The rst section outlines the structure of EKL programs and the features of EKL. The second section gives details of the translation from core Escher to EKL. It contains compilation steps which are also used in the Brisk compiler for compiling Haskell; in addition it describes new steps which translate the logic features of Escher into EKL.

3.1 The Escher Kernel Language The Escher Kernel Language has been derived from the Brisk Kernel Language (BKL), which is described in detail in [HS97]. BKL has been developed as an intermediate language for the compilation of Haskell, with the aim to produce very simple and pure code. EKL extends the scope of BKL by some extra primitives which allow the compilation of Escher at the cost of some purity. EKL is a low-level functional logic language which can be given a formal operational semantics expressed as a state transition system. Both BKL and EKL strongly resemble the STG language [PJ92], which is used for the same purpose in the Glasgow Haskell compiler (ghc), but are even more simpli ed. A grammar for EKL is given in Figure 3.1. This grammar is simpli ed in the sense that various issues such as modules, types, data statements and literals are omitted in order to concentrate on the essential features. EKL inherits its exibility from BKL. When comparing EKL code to STG code, it becomes clear that issues like evaluation and sharing have not been built into EKL. Instead, they are encapsulated in a collection of built-in functions, which are introduced by the compiler during the translation into EKL. This simpli es the abstract machine signi cantly, bringing it closer to a pure graph reduction machine. Also, the built-in functions can be replaced and modi ed easily to support a variety of approaches (e.g. di erent evaluation techniques). Moreover, new built-in functions can be added without diculty. This allows the implementation of several computational models, one of these is the functional logic programming style of Escher. 1

BAM stands for Brisk Abstract Machine.

3.1 The Escher Kernel Language pgm ! decl1 ; . . . ; decln decl ! fun arg1 . . . argn = exp exp ! let bdg1 ; . . . ; bdgn in exp j case var of alt1 ; . . . ; altn j appl bdg ! var = appl alt ! cstr arg1 . . . argn -> exp j var -> exp appl ! fun arg1 . . . argn fun ! var cstr ! var arg ! var

41 n1 n  0 (Global function) n  1 (Let binding) n  1 (Case expression) n0

(Default alternative)

n  0 (Saturated application)

Where var is an identi er. An identi er pre xed with prim indicates a built-in function. Figure 3.1: Syntax of EKL Because EKL has been designed as an abstract machine language, some of the language features are discussed, here, in relationship to the abstract machine and with the representation of EKL programs on machine level in mind. The machine components will be introduced in Chapter 5. In the following, the main characteristics of EKL are listed. 1. All function arguments are simple identi ers. This corresponds to the operational reality that heap nodes need to be built for function arguments prior to the call. Hence, when translating into EKL, let bindings need to be added for non-trivial arguments. 2. Function applications must be saturated. Every function has a known arity determined from its de nition. In every call to it, it is applied to the right number of arguments. 3. In every application , whether it appears in an expression or on the right hand side of a local de nition, the function must be in evaluated form. This allows a heap node to be built, in which the node representing the function acts as an info node for an application. In particular, partial applications are handled explicitly by the compiler by introducing built-in functions during the translation into EKL. Thus, the abstract machine does not need to check whether the right number of arguments has been provided for function calls, nor does it need to build partial applications implicitly at run time. A uniform representation of expressions and functions on the heap of the abstract machine can be achieved, because in EKL all function applications are saturated, and functions need to be in evaluated form before being called. Every expression and function call can be represented as a node on the heap, where the rst word points to a function node . The

42

Compilation of Escher

function node describes the call node; it provides layout information (e.g. the arity of the call node) and information on how to evaluate the call. For this reason, function nodes are sometimes referred to as info nodes or descriptors . All function nodes can be represented uniformly. In fact, they follow the same layout as call nodes, being built from (function) constructors as if they were data. 4. A case expression represents an immediate switch. It does not handle the evaluation of its argument. The argument is assumed to represent a node, which is guaranteed to be already in evaluated form. In EKL, evaluation of expressions is not assumed to be implicitly caused by the use of case expressions or primitive functions, as it is in the STG language. Instead, the evaluation of the argument of case expressions or arguments to strict functions is expressed explicitly using calls to built-in functions such as the free family for Escher, which is discussed in Section 3.2.1.1. 5. The patterns in case expressions are simple one-level patterns. More complex forms of pattern matching can easily be translated into this form [Wad87]. 6. Sets in alternatives of case expressions, and sets which have not been translated into lambda abstractions during the translation into core Escher, are represented using the Set constructor. All other sets have been translated into lambda abstractions (during the translation into core Escher) and are later lifted to the top-level. 7. Local functions are lifted out, so that local let de nitions de ne simple identi ers, not functions. Lifting is described in Section 3.2.3. An EKL program is a collection of declarations. The value of an EKL program is the value of the function main. Notice, that main can have arguments. These arguments are variables, called global variables which typically appear in the answer to an Escher program.

3.2 Translating Core Escher into Escher Kernel Language The Escher compiler rst transforms a source program into core Escher and then translates it into an EKL program. To make this thesis self-contained, the translation from core Escher into EKL is explained in this section. Important aspects of the translation into EKL are the introduction of built-in functions and lifting. Both are explained in the following sections. In the last section the whole translation is outlined.

3.2.1 Built-in Functions The aim in developing BKL and the Brisk machine was to make the abstract machine match the underlying graph reduction very closely. In order to achieve this, many of the

3.2 Translating Core Escher into Escher Kernel Language

43

highly specialised execution details used in the STG machine have not been incorporated into Brisk. Instead, these control features are handled by a collection of built-in functions. The resulting machine is simple and exible. EMA inherits this exibility from Brisk. There is a variety of built-in functions used by the Escher compiler. Some of them are taken directly from Brisk, like those used for partial application. Others are modi ed versions of Brisk built-in functions, examples are the built-ins for evaluation and application. This section contains an informal description of the operational behaviour of all control-related built-ins (irrespective of their origin), which are added to programs by the Escher compiler during the translation into EKL. The description contained in the following sections refers in some places to components of the abstract machine and to program representation on machine level. In particular, evaluation is stack-based and the expression to be evaluated is represented as a graph which is kept as a collection of nodes on the heap ; the current node is the node on the heap which represents the expression that is currently under evaluation. These references are kept to a minimum. However, the Escher machine is based on the architecture of the Brisk machine which was described in Section 2.3.2. The Escher machine will be presented in Chapter 5 together with a formal description of the operational semantics of the built-in functions. There are, in addition, other built-in functions used in EMA. The Escher system functions (&&), (||), (==) and not for instance. Their de nitions are not translated into EKL, instead they are provided as built-in functions, sometimes called built-in system functions because of their origin. In Chapter 4 it is described how the built-in system functions relate to the de nitions given in the Escher Booleans module. In addition, there are the two quanti ers exists and forall which are compiled into the built-in (system) functions sigma and pi respectively. These are described together with the compilation of the quanti ers in Section 3.2.3.2. Finally, built-in functions can have a number of auxiliary purposes, one of these is the encapsulation of deferred function calls. Because auxiliary built-ins do not appear in an EKL program initially, they will not be described here; a formal description of them is contained in Chapter 5.

Notation: Programs are written in typewriter font. The arrow --> is used to indicate the translation from core Escher to EKL. The pre x prim is omitted when it is clear that the discussion is about a built-in function.

3.2.1.1 Evaluation This part of the section is heavily based on the description of the strict family in [HS97] which is used for evaluation in Brisk. It does not cover the evaluation of arguments to match set patterns. This is described separately in Section 3.2.1.2. In EKL, various subexpressions such as the argument to the case construct are assumed to be already in evaluated form. To achieve this the compiler uses built-in functions which force the evaluation of subexpressions. Moreover, one of the features of EKL is that in any function application such as f x y, the function f is assumed to be in an evaluated form before being called, and not to be represented as a suspension node. The expression f x y can then be represented in the

44

Compilation of Escher

heap as a 3-word node, with the rst word pointing to the function node which represents f. The function node provides layout information for the application node, as well as information on how to evaluate it. This information can only be supplied when f is in evaluated form. While the application is handled by one of the apply built-in functions (see Section 3.2.1.4), this section explains how evaluation is performed. To force explicit evaluation of expressions the built-in family of evaluation functions free is provided. There is one family member for every number of arguments. To simplify evaluation, and to allow for di erent evaluation functions to be used to evaluate di erent arguments, each free function evaluates only the rst argument out of a number of arguments. For example, free2 forces the evaluation of the rst argument out of two arguments. This aspect will be picked up again later in this section. A simple example which illustrates the use of the family of free functions is the de nition of length, for which the Escher de nition is given rst: length :: [a] -> Int length [] = 0 length (x:xs) = 1 + length xs

The length function is strict in its ( rst) argument. The compiler translates the above function de nition into EKL and produces the following code: length = primfree1 free0 length0 length0 x = case x of [] -> 0 (:) y ys -> let v1 = length ys in (+) 1 v1 v -> error

The function free0 is used as the continuation of the evaluation function free1. It is a built-in function which is called when the evaluation of the argument returns. In fact, all free functions use the same continuation function. The function length0 is introduced during compilation. It is the continuation of the function length which is applied (instead of length) after the argument has been evaluated. It uses a case statement to switch to one of the alternatives depending on the outcome of the evaluation. The de nition of length can be seen in two ways: 1. It can be understood as de ning length to be an application of the built-in function free1 to the continuation functions free0 and length0 . When this application is evaluated for the rst time, a function node is created for length, and this node is used to update the original application. Hence, in any further calls to length the function is in evaluated form. The corresponding function node contains the code that forces argument evaluation.

3.2 Translating Core Escher into Escher Kernel Language

45

2. When heap nodes are created to represent the top-level functions of an EKL program, the pre x prim is recognised and a function node is built for length directly, instead of an application. In this function node the arguments free0 and length0 are used as references. The code part of the function node is a routine which implements the functionality of free1. This ensures that the function length is in evaluated form, i.e. represented as a function node rather than an application, when a call to length is created. The second approach is used in the Escher compiler. It is more optimised than the rst approach because the compiler generates code for the strict function directly, inlining the call to free1, i.e. the evaluation of the right hand side of the length de nition is performed at compile time. To explain how the free functions work, take the call length t as an example. Here, t is meant to be an arbitrary term used as an argument to length, it is not intended to be a variable, even though it could be one. The node which represents such a call on the heap is sometimes referred to as call node . The function length uses free1 to evaluate its argument. Operationally, free1 places a pointer to a copy2 of the original expression length t on the return stack (see Section 2.3.2.2), and makes t the current node. The continuation free0 is used once an argument has been evaluated. In Escher, the result of evaluating the argument t of a constructor-based function3 to, say t0 , can be: 1. a weak head normal form, i.e. an expression with a constructor on top level, or 2. an expression which cannot be evaluated any further at the current state of the computation because one of its arguments, or in fact the expression itself, is a variable which is not suciently instantiated to allow a reduction. Such expressions are called deferred . In the rst case, the expression length t is popped o the sack and updated in place by which becomes the current node. In the second case, the expression length t is popped o the stack and t is updated with the evaluation result t0 . However, the call cannot at the moment be reduced any further and is therefore deferred as well. Evaluation returns to the computation which demanded the evaluation of the original expression. Deferred expressions behave like constructors until the variable which caused the computation to defer gets instantiated. The expression can then be re-evaluated. The defer mechanism implements residuation. The details of the defer mechanism are not described here, because deferred computations only occur at run time. Section 5.3.2 outlines how deferring is realized on the machine level. As mentioned earlier, each member of the free family evaluates only the rst argument out of a number of arguments. However, a function can be strict in more than one argument. length0 t0

A copy of the original expression is used here to simplify the description of evaluation and to separate evaluation from sharing. Section 3.2.2 gives an informal description of how sharing can be supported in EMA. 3 User-de ned functions in Escher are constructor based, except for those matching on sets. Only system functions are allowed to match on de ned functions. The free family is used to evaluate constructor-based functions. 2

46

Compilation of Escher

Below is the de nition of the function intMerge which can be used to merge two lists of integers, e.g. in a mergesort algorithm. intMerge :: [Int] -> [Int] -> [Int] intMerge [] ys = ys intMerge (x:xs) [] = (x:xs) intMerge (x:xs) (y:ys) = if (x > y) then (y:(intMerge (x:xs) ys)) else (x:(intMerge xs (y:ys)))

There are unfolding techniques which can be used during the compilation of pattern matching [Aug85, Wad87] or before, to transform the de nitions of functions which are strict in more than one argument into a series of functions which are strict in the rst argument only. The function intMerge for example can be translated into the following EKL code. intMerge = primfree2 free0 intMerge c1 intMerge c1 x y = case x of [] -> y (:) x1 xs -> intMerge c2 y x1 xs v -> error intMerge c2 = primfree3 free0 intMerge c3 intMerge c3 y x1 xs = case y of [] -> (:) x1 xs (:) y1 ys -> let v1 = (>) x1 y1 v2 = (:) x1 xs v3 = intMerge v2 ys v4 = (:) y v3 v5 = intMerge xs y v6 = (:) x v5 in ifte v1 v4 v6 v -> error

Arguments are evaluated from left to right. Notice that as soon as an argument returns deferred, evaluation is interrupted at that stage. The expression on the stack is popped and subsequently deferred as well. No further arguments are evaluated. The author is aware of the fact that the repeated use of the evaluation functions, to evaluate the arguments of functions which are strict in more than one argument, causes

3.2 Translating Core Escher into Escher Kernel Language

47

new evaluation nodes to be created each time and is therefore a source for ineciency. One way to optimise this is to use an approach similar to Brisk, where each free function is annotated with a bit pattern, for instance free101 evaluates the rst and third argument of a given 3-ary function call. The 0s and 1s attached to the name free refer to each of the arguments which are supplied to a strict function call; a 1 indicates that the respective item needs to be evaluated before the function continuation is called, and 0 means that no evaluation is required. There is one member of the family for each arity and each possible combination of requirements. The position of the rst argument to be evaluated can then be determined by interpreting the pattern of 0s and 1s in the free function used for evaluation. After evaluating an argument, the corresponding 1 needs to be changed to a 0. The evaluation of the second and any further strict arguments proceeds in the same way. As soon as one of the arguments returns deferred, the expression on the stack is popped and deferred as well. No further arguments are evaluated. In general, by using this approach the node on the stack can be re-used and no new call nodes need to be created. However, the Escher compiler uses another family of evaluation functions; the evalSet family which evaluates arguments to match on set patterns. In the presence of two evaluation functions the approach outlined above needs to be modi ed so that the \bit" pattern also indicates which evaluation function to use. This is left for further research. There is in addition space for various optimisations of the compiler, such as unboxing and more inlining. Some of these are described in [HS97] for the evaluation functions used in Brisk. It is expected that similar techniques can be used for the Escher compiler; this, as well, is an area of further research.

3.2.1.2 Set Evaluation After the translation into core Escher the liftable sets are represented uniformly as lambda abstractions. These lambda abstractions are later lifted to the top level. Lifting is described in Section 3.2.3. To clarify the above points, below is an Escher program containing set processing. A suitable declaration of the data types Person and Sport is assumed. main :: Bool main = emptyset { s | likes (Fred,s) } likes :: (Person,Sport) -> Bool likes = { (Mary,Dancing), (Bill,Tennis) }

The result of compiling the above program is the following EKL program: main = emptyset sportset sportset :: Sport -> Bool sportset s = let v1 = (Fred,s) in likes v1

48

Compilation of Escher likes x = let v1 = v2 = v3 = v4 = in (||)

(Mary,Dancing) (==) x v1 (Bill,Tennis) (==) x v3 v2 v4

where sportset is a new function. The non-pattern-matching set-processing functions in the Escher Booleans module can be translated in the same way. The compilation on the function inters, which implements set intersection, is demonstrated here. Below is the Escher de nition of inters: inters :: (a -> Bool) -> (a -> Bool) -> (a -> Bool) s `inters` t = { x | (x `in` s) && (x `in` t) }

The result of the transformation is the following EKL declaration: inters s t let v1 = v2 = in (&&)

x = in x s in x t v1 v2

Set-processing functions treat sets as data when pattern matching on the set patterns {}, {t} and {x|(u||v )}. However, sets are also functions. Hence, a exible representation of sets is required, which allows sets to be used as functions e.g. in applications and as data e.g. in pattern matching. EKL provides the built-in constructor Set which takes three arguments. It expects a variable in its rst argument, a boolean formula in its second argument and holds a data structure called environment in its third argument. Environments are used in EMA to store bindings for variables. Section 5.1.4 gives a detailed description of the structure and use of environments; for the purpose of this section, environments are simply regarded as data structures. As shown in Table 3.2, set patterns can easily be represented using the set constructor. In the table, the variable x in the rst two rows of the table is a new variable, which is introduced during compilation; it does not occur in t. Pattern {} { } { |( || )}

t x u v

Representation with Set constructor Set x False env Set x ((==) x t) env Set x ((||) u v ) env

Table 3.2: Representing sets with the Set constructor

3.2 Translating Core Escher into Escher Kernel Language

49

In addition, non-liftable sets in function bodies, which are typically of the form {x|u}, can be represented with the set constructor as Set x u env . Evaluation of arguments to match on set patterns is handled by a family of specialised evaluation functions, the evalSet family. Like with the free family, there is one member for every number of arguments. A call to evalSet is used to explicitly force the evaluation of an expression when a set pattern occurs in a function head. Each evalSet function evaluates only the rst argument out of a number of arguments. The basics of the compilation for set pattern-matching functions into EKL are similar to what is described in Section 3.2.1.1; instead of calls to free, calls to evalSet are inserted here. Again, it is assumed that the de nitions of functions, which are strict in more than one argument, are translated into a form in which the resulting de nitions only contain statements with one pattern argument in the head, so that the pattern argument is the rst of the arguments in a statement head. Notice that functions can have set patterns and other patterns in the heads of their statements. The compiled versions of such functions use both free and evalSet for argument evaluation. An example of such a transformation is given in Section 3.2.1.1. To illustrate how calls to evalSet are added during compilation the EKL de nition of the emptyset function, whose Escher de nition is given in Section 2.1.3.2, is shown below: emptyset :: (a -> Bool) -> Bool emptyset = primevalSet1 evalSet0 evalToSet0 emptyset0 emptyset0 s = case s of Set x b env -> case b of False -> True (==) x t -> False (||) d1 d2 -> let v1 = Set x d1 env v2 = Set x d2 env v3 = emptyset v1 v4 = emptyset v2 in (&&) v3 v4 w -> error v -> error

The function evalSet1 has two continuations evalSet0 and evalToSet0. They will be described later in this section. The function emptyset0 is introduced by the compiler. It is the continuation of the function emptyset which is applied instead of emptyset after the argument has been evaluated. In a similar way as for the free functions, the compiler recognises that evalSet1 is a built-in function and creates a function node for emptyset using the arguments evalSet0, evalToSet0 and emptyset0 as references. The code part of this function node is a routine which implements the functionality of evalSet1. Hence emptyset is a function node rather than an application when a call to it is created.

50

Compilation of Escher

The above statement for the continuation function emptyset0 uses the functions (==) and (||) as patterns in the alternatives of the case expression. These functions are declared as built-in system functions on the machine level. In order to support this kind of matching, built-in functions are given an identi er as if they were constructors. However, these functions also implement some of the rewrite rules de ned in the Booleans module. Hence, the function node representing a built-in system function also contains evaluation code to support these rewrites. Chapter 4 describes in detail how the statements in the Booleans module are supported by built-in system functions. To explain the operation of the evalSet functions, take the call emptyset t from the examples above. The term t is an argument to emptyset, it is not intended to be a variable, even though it could be one. The function emptyset uses evalSet1 to evaluate its argument. An argument which needs to be evaluated to match a set pattern is either represented by a function node or by a set constructor. Operationally, evalSet1 places a pointer to a copy4 of the original expression emptyset t on the return stack. It makes the argument t the current node if it is not yet represented by a set constructor, otherwise it makes the set body (which is contained in the set constructor) the current node. Depending on how the argument was represented, the continuation evalSet0 is used if it was a set constructor, or evalToSet0 is used otherwise. The set body is a boolean expression. Evaluation of expressions returns naturally when a constructor, in this case either of the boolean constructors, is reached or an expression which cannot be further evaluated at the moment because one of its arguments or the expression itself is a variable which is not suciently instantiated to allow a reduction; i.e. the set body evaluated to a deferred expression. The evaluation of the set body needs to be stopped explicitly, and control needs to return, when the expression evaluates either to an equality involving the set variable, or to a disjunction, which are the top-level functions in the set body of the remaining two set patterns (the rst one is the constructor False). For the sake of simplicity, it is assumed, here, that evaluation stops when either of these forms is reached. A more detailed description of how evaluation can be interrupted to match on de ned functions is given in Chapter 5. Assuming in our example that the set t is in set constructor representation. If the set body returns deferred, the expression emptyset t is popped of the stack, it gets deferred subsequently and evaluation returns to the computation which demanded the call to emptyset. Otherwise, the expression emptyset t is popped of the stack and the set body in t (which is represented by a set constructor) is updated with the evaluation result. The function node is updated with the continuation emptyset0 and made the new current node. If the set body evaluated to True the default alternative of the case statement in the de nition of emptyset0 matches, causing a match error. If the argument t was not in set constructor representation it can return as a set constructor, a deferred computation or a function. Remember that functions are represented using function constructors which behave like ordinary constructors when evaluated. A set in constructor representation could be the outcome of evaluating a set-processing function A copy of the original expression is used here to simplify the description of evaluation and to separate evaluation from sharing. Section 3.2.2 gives an informal description of how sharing can be supported in EMA. 4

3.2 Translating Core Escher into Escher Kernel Language

51

which returns a set. A function can be returned if the original argument is one of the sets which have been lifted by the compiler, like sportset in the example above. We now assume that the set t has not been in set constructor representation before evaluation. If it returns as a set constructor, the set body still needs evaluation before matching can be performed. The call emptyset t is popped of the stack, the argument is updated with the evaluation result and the call to emptyset is made the current node. This means it will be re-evaluated, but this time the set body will be entered. If the argument returns deferred, the call to emptyset t is popped of the stack and the argument t is updated with the evaluation result. The call cannot be reduced any further and is therefore deferred. Evaluation returns to the function which demanded the evaluation of the original expression. Finally, if the argument evaluates to a function, this function represents a set and needs to be transformed to allow the evaluation of the set body. To do this an expression is created representing the application of the function to a (logical) variable; this is the set body. In the case of sportset the expression would be for instance sportset x where x is a fresh variable. In addition an expression needs to be created which represents the set constructor that is used from now on instead of the original function node. This set constructor takes the variable and the set body just created and a new environment (the details of which will not be given here). The expression emptyset t is popped of the stack and t is updated with the newly created set constructor. It is then made the current node and re-entered. Because both the representation as a function and the one using the set constructor are functionally equivalent, the node representing the original argument can be updated with the set constructor if sharing can be used. Section 3.2.2 explains why sharing is not always possible.

3.2.1.3 Partial Application The built-in function for partial application has the same functionality in EMA and Brisk. Parts of this section correspond to the description in [HS97]. Every function in EKL has an arity indicating how many arguments it expects. Every call must be saturated, which means that the correct number of arguments must always be supplied, according to the arity. In order to achieve this the compiler must introduce explicit partial applications where necessary. For example, given that (+) has arity two, then the expression (+) x is translated into EKL as a partial application, using the built-in function pap1of2: (+) x

-->

primpap1of2 (+) x

The function pap1of2 takes a function of arity two and its rst argument and returns a function of arity one which expects the second argument. When the second argument is supplied a 3-word node is created in which the original function is applied to both arguments.

52

Compilation of Escher

The function pap1of2 is one of a family dealing with each possible number of arguments supplied and expected respectively, where the number of supplied arguments is less than the number of expected arguments. In practice the compiler recognises that pap1of2 is a built-in function and creates a function node on the heap rather than an expression node. This function node uses the arguments supplied to the partial application function as references. In fact, it already represents the function which is returned by pap1of2. Hence, part of the work can be performed at compile-time. The arity restriction applies to higher-order functions as well. For example, if the compiler determines that the function map has arity two, and that its rst argument is a function of arity one, then for the call map (+) xs a transformation of the following form is needed: map (+) xs

-->

let f x = primpap1of2 (+) x in map f xs

The compiler also needs to deal with over-applications. For example, if head, which has arity one, is applied to a list fs of functions to extract the rst, and the result applied to two more arguments x and y, then the over-application head fs x y would need to be translated. One possible way is shown below : head fs x y

-->

let f = head fs v0 = primapply3 v1 = primfree3 free0 v0 in v1 f x y

Since f is an unevaluated expression, a member of the free family of functions is used to evaluate it before applying it. The application is performed by a member of the apply family. This family of built-in functions is described in the next section. The described arity restrictions mean, that functions are evaluated by calling and returning. Other compilers include optimisations which avoid this, implementing a call to a possibly unevaluated function simply as a jump (see [PJ92, CL94]). However, the Brisk approach has compensating advantages. It allows for greater uniformity and simplicity in the abstract machine. Every application can be represented in a direct way, without the need for argument stacks or for testing to see whether enough arguments are available. The argument testing is normally needed to check for partial applications. Here, all partial application issues are dealt with at compile-time.

3.2.1.4 Application The behaviour of the built-in function for application used in Brisk has been extended by set application for EMA. Parts of this section describe the functionality which is shared between both machines, a similar description can also be found in [HS97]. When a function, say f, which was passed as an argument to another function, say g, is applied in the body of g to an argument, e.g. f x y, the application is represented using

3.2 Translating Core Escher into Escher Kernel Language

53

the built-in functions free and apply. This is because f is a dynamically passed function which may not be in evaluated form. Here is an example: g f x y = let z = f x y in h z

-->

g f x y = let v0 = primapply3 v1 = primfree3 free0 v0 z = v1 f x y in h z

The built-in function free3 is used to evaluate the function f. After the evaluation a call to apply3 is made. It is the responsibility of the built-in function apply3 to nd the arity of f and deal with any mismatch between this arity and the number of arguments f is being applied to. The function apply3 takes an evaluated function and two more arguments. It is a member of a family of apply functions, each one deals with a di erent number of arguments. The rst argument is always an expression which is expected to evaluate to a function. Again, the compiler recognises that both free3 and apply3 are built-in functions and creates function nodes for them directly, using the arguments as references. To explain the operation of the apply functions in more detail, take the call f x y, which applies the function f to some arguments, as an example. The compiler creates a call to free3 f x y to evaluate the function f. The result of the evaluation, say f0 , should be a function node (represented by a function constructor) or an evaluated set (represented by a set constructor). In this case the function apply3 is used as continuation, creating a call of the form apply3 f0 x y. Any other result is dealt with by the evaluation continuation free0 . This includes deferring the original call in the case that the computation of f defers. Hence, when apply is called, it is certain that the rst argument of the application node is a function. There are then two cases to distinguish:

 If f0 is a set in constructor representation, then exactly one argument, say t, should

be supplied. (This assumes that the program is type correct.) The set variable, say x, is bound to the argument, the substitution {x/t} is applied to the set body which is made the new current node.  Otherwise f0 must be a function node. The built-in function apply3 needs to check the arity of this function (which is contained in the function node) and compare with the number of arguments available. The comparison can have three results: { If too few arguments are supplied, a partial application of f0 to these arguments is created. { If the right number of arguments is supplied, an expression node that applies the function f0 to all arguments is created. In the above example the node is of the form f0 x y. { If too many arguments are supplied, an expression node is created representing the application of f0 to the right number of arguments. This node is used as the function in a new application of free and apply to the remaining arguments.

54

Compilation of Escher The node created is made the new current node.

3.2.2 Sharing Sharing requires that the expression to be evaluated is represented as a graph. In particular, common expressions are shared in the sense that, if an expression contains a subexpression more than once, the graph corresponding to the expression contains only one node that represents the subexpression; but a number of links to this node, one for each occurrence of the subexpression. Sharing ensures that an expression is evaluated at most once (see Section 2.2.1.3 for a general introduction). As a consequence, a maximum degree of sharing is essential for an ecient implementation. In contrast to Brisk, the Escher implementation cannot, in general, use sharing when evaluating functional logic programs. This part of the section describes when sharing can be used safely and what alternative approach can be taken when sharing is not safe. First of all, a brief investigation into the basics of sharing in Brisk is made. In Brisk, sharing can be supported in two ways. First by using the built-in function share, which is described in [HS97]. Alternatively, it can be built into the return mechanism of evaluation. Instead of keeping a copy of the original expression on the stack, the expression itself is kept on the stack. Every time the result of an argument evaluation is di erent from the original argument expression, the original argument expression is overwritten with an indirection. At the end of an argument evaluation, the function in the original call node is overwritten with its continuation. Unlike in the Brisk machine, which evaluates Haskell programs, sharing needs to be used very carefully when evaluating Escher programs. This is because Escher allows the evaluation of expressions containing variables. As explained in Section 2.1 these variables are initially unbound and later instantiated to potentially di erent values corresponding to di erent branches of the computation. An expression which contains variables can therefore represent several values, depending on the binding of its variables. In contrast to this, expressions in Haskell are always ground and hence the same expression always evaluates to the same value. It becomes clear that there are two aspects in sharing: 1. Sharing the representation of (sub)expressions which provides the foundation. 2. Sharing the evaluation result of expressions by updating them in place (with their evaluation result) after evaluation. This is only e ective if the representations of (sub)expressions are shared. In an Escher computation, (sub)expressions can be shared without restrictions. In particular, each variable is represented by a unique node in the graph; di erent occurrences of one variable share this node. However, due to the presence of variables in expressions, evaluation results cannot in general be shared. For example, assume that in the expression: (x==[] && (emptylist x)) || (x==[1] && (emptylist x))

3.2 Translating Core Escher into Escher Kernel Language

55

the subexpression emptylist x is shared between the two branches of the disjunction. This can for instance happen after in an expression of the form: (x==[] || x==[1]) && (emptylist x)

the

(&&) is distributed over (||). emptylist x will evaluate to True

In the rst branch of the disjunction the call to once x is bound to []. It would be wrong to update the original node representing the call to emptylist x with this evaluation result, because this makes True appear instead of emptylist x in the second branch of the disjunction as well. Figure 3.2 depicts the initial expression with the underlined redex and the result of the reduction, it also shows the corresponding graph before and after the redex evaluation. Shared nodes, the links to them and the evaluation result are shown in a di erent colour. Hence, when an expression contains variables, updating it with its Expression: (x==Nil && (emptylist x)) || (x==[1] && (emptylist x))

(x==Nil && True) || (x==[1] && (emptylist x)) Graph: ||

|| &&

&& == Nil x

== ...

emptylist

&& ==

True == Nil ...

&& emptylist

x

Figure 3.2: Reduction of a non-ground expression evaluation result cannot be used. The evaluation result is represented by a new node in the example above. For the sake of simplicity not all details of the evaluation (e.g. how the evaluation is caused and so on) are shown here. In general, the call node cannot be updated with the result of an argument evaluation when a variable is involved during the computation. Instead it is copied and the copy is updated at the argument position. Copying will ensure that the original node remains unchanged. However, through sharing multiple evaluation of expressions is avoided, which turns out to be the key to ecient implementations of functional languages. Taking this into consideration, a strategy which switches between updating and copying is necessary to achieve the best performance for evaluating Escher programs. The method used to support sharing in EMA is called switching and is built into the return mechanism of evaluation. In the following paragraph it is explained how switching can be realized: The basic requirement that subexpressions are shared, or more precisely the nodes representing subexpressions on the heap, is met. When a function forces the evaluation of an

56

Compilation of Escher

argument, instead of keeping a copy of the node which represents the original expression (i.e. the call node) on the stack, the expression itself is kept on the stack, like in Brisk. A ag (called variable ag in what follows) is set when a variable is evaluated during the evaluation of an argument expression. Before the argument is evaluated, the ag is set to o . If the result of the argument evaluation is di erent from the original argument expression and the ag is still o , the original argument expression is overwritten with its evaluation result5 and the call node can be updated in place with the result of the evaluation at the respective argument position. Thus EMA supports sharing in the same way as the Brisk machine for ground expressions. On the other hand, if the result of the argument evaluation is di erent from the original argument expression and the ag is on, the original expression is copied and the copy is updated with the evaluation result. This ensures that the original expression remains unchanged when the evaluation of an argument depended on the value of a variable. In Section 5.1.6 details about the implementation of the variable ag are given, together with an outline of a possibility for more sharing, which can only be described in conjunction with the representation of bindings for variables on the machine level. In Section 5.4.1 it is shown how switching is embedded into the functionality of the free family of evaluation built-in functions.

3.2.3 Lifting Lifting is an important technique because there are no local function de nitions in EKL; all local de nitions represent values instead of functions. The Escher compiler uses several forms of lifting: lambda lifting, lifting of quanti ers and case lifting. This section explains the basic aims of each form of lifting and how the translation is performed.

3.2.3.1 Lambda Lifting Lambda lifting is a transformation in which all local function de nitions are lifted to the top level, by turning their free variables into extra arguments. In this context, variable is used to refer to an identi er (see the syntax of EKL in Figure 3.1). Such a variable is called free if: 1. it occurs in the body of the local de nition, and 2. it is not bound by the lambda abstraction (which de nes the local de nition), and 3. it is not a top-level function in the program. A free variable in the context of lifting must not be mixed up with a free variable in the logic programming sense, where it is used to refer to a variable which is not bound by a In practice, overwriting the original argument expression is realized by converting it into an indirection node which points to the node that represents the evaluation result. 5

3.2 Translating Core Escher into Escher Kernel Language

57

quanti er. The terminology is based on the lambda calculus where the \identi er" x in x:e is also called a variable. It was decided not to use \identi er" instead of variable in this thesis, to keep the terminology uniform with functional language implementations. To avoid ambiguity, it will be explicitly pointed out in what sense a variable is understood when the context is not clear enough. The Escher compiler uses the traditional way of lambda lifting [Joh85]. The free variables of local functions are added as extra arguments, so that the function can be lifted to the top level. The function is then replaced by a partial application of the lifted version. For example, a subexpression of the form:

x1 : : : xk -> e

\

is replaced by a new subexpression:

f v1 : : : vn where f is a new function of arity n + k and v1 : : : vn are all the free variables in the lambda abstraction \x1 : : : xk -> e. At a later stage in compilation the arity transformation takes care of the partial application f v1 : : : vn by introducing a partial application built-in function. A new top-level de nition of the form:

f v1 : : : vn x1 : : : xk = e0 is added where e0 is the expression resulting from lifting the body e of the original lambda abstraction. Note that lambda lifting also brings liftable sets in function bodies to the top level. This is because sets are transformed into lambda abstractions during the translation from Escher into core Escher. (See the beginning of Section 3.2.) Hence no special set lifting is needed. A slightly di erent, but more optimised, approach to lifting is taken in the Glasgow Haskell compiler; it is described in [PJL91]. Brisk can support both approaches. However, the Escher compiler currently uses the simplest way of lifting; optimisation is left as a task for further research.

3.2.3.2 Quanti er Lifting The lifting of quanti ers is a new transformation. Both the existential quanti er exists and the universal quanti er forall are lifted during the compilation from Escher to EKL. It can be assumed that at least one variable is quanti ed.

Existential Quanti cation: The aim of lifting existential quanti ers is to convert them into applications of the built-in function sigma to a lifted lambda abstraction. A subexpression of the form:

x1 : : : xn -> e

exists \

58

Compilation of Escher

is replaced by a new expression: let

v

f v1 : : : vk v

= in sigma

where sigma is a built-in function, v is a new variable, f is a new function of arity k + n, and v1 : : : vk are the free variables (with respect to lifting) contained in e. At a later stage in the compilation, the arity transformation will introduce a partial application built-in function to take care of the partial application f v1 : : : vk . A new top-level function of the form:

f v1 : : : vk x1 : : : xn = e0 is added where e0 is the expression resulting from lifting the quanti ed formula e. Notice that, in this approach, the function sigma does not have a type. It is a generalisation which is used instead of a family of sigma functions. The function sigma is a built-in function; it determines the number of quanti ed variables from the arity of the function it is applied to. Alternatively, the Brisk philosophy can be followed, introducing an in nite family of sigma functions: sigma1, sigma2, : : :, sigman, where n indicates the number of quanti ed variables in each application. This is similar to the partial application family papnofk . Each sigman function can then be given the appropriate type, for instance: sigma2 :: (a -> b -> Bool) -> Bool

Universal Quanti cation: The aim of lifting universal quanti ers is to convert them

into applications of the built-in function pi to a lifted lambda abstraction. The transformation is similar to the one for existential quanti cation. The only di erence is that it introduces the built-in function pi instead of sigma. In the same way as sigma, the function pi is a generalisation of a family of universal quanti cation functions pi1, pi2, . . . , pin.

3.2.3.3 Case Lifting As part of the process of compilation to EKL, case lifting adds execution control information to the program to force the evaluation of expressions before they are used for pattern matching. The Escher compiler uses two families of evaluation functions, the free family and the evalSet family. The general idea of case lifting, taking the insertion of calls to the free functions as an example, is given below. Matching on sets is restricted to matching on the three de ned set patterns. Section 3.2.1.2 shows how calls to the evalSet functions are introduced. The case lifting transformation originated from a similar transformation that is used by the Brisk compiler for adding calls to evaluation built-in functions during the translation from Haskell into BKL [HS97]. It is assumed that there are no more compound subexpressions; they have been unpacked at a previous stage in compilation. In particular, in each statement of the form:

3.2 Translating Core Escher into Escher Kernel Language case

59

v of alts

the v is a variable. In EKL, a case expression represents an immediate switch. Hence, a subexpression of the form: case

v of { p1 -> e1 ; : : : ; pn -> en ;

}

requires that the expression v is evaluated before the case expression is evaluated. To achieve this, the above subexpression is replaced by:

f v v1 : : : vk where f is a new function and v1 : : : vk are the free variables (with respect to lifting) in p1 -> e1 ; : : : ; pn -> en, together with new top-level equations:

f

f0

= primfree1 free0 1 k = case

x v :::v

f0

x

of {

p1

->

e01 ; : : : ; pn

->

e0n ;

}

where f 0 is a new function, x is a variable that does not occur free in p1 -> e01 ; : : : ; pn -> e0n , and e01 ; : : : ; e0n are the case-lifted forms of e1 ; : : : ; en .

3.2.4 The General Transformation This section summarises the general transformation of an Escher program into EKL, which takes the following steps: 1. Unpack compound expressions. Name every non-atomic function argument and every lambda abstraction by introducing a let expression. It is assumed here that the initial equations do not contain local de nitions. This can be achieved by prior

attening of nested let bindings which brings all de nitions to the same level. It is left for a future version of the compiler to allow local de nitions. Afterwards, all applications are simple. All case and binding expressions are at the top-level of the function bodies or the body of a local de nition. All local de nitions de ne single variables only. This transformation also pattern-compiles any lambda abstractions with pattern bindings, and replaces these with new lambda abstractions in which all the arguments are variables. 2. Pattern compile and case lift. Saturate the alternatives of case expressions by adding a default alternative which rewrites to a match error. Afterwards, all de nitions consist of a single equation, with a body consisting of the application of a function to zero or more variables. All case expressions are lifted, and control annotations (calls to members of the free family and the evalSet family) are added. The process of case lifting has been described in Section 3.2.3.3. It introduces new equations.

60

Compilation of Escher 3. Lift binders by lifting quanti ers and applying lambda lifting. Afterwards, no binder expressions, that is no lambda abstractions and no quanti ed expressions containing exists or forall, remain. New equations are added, existential and universal quanti cations have been converted into calls to the built-in functions sigma and pi, respectively. 4. Arity transformation. At this stage, an application is one of:  Application of a variable, which needs evaluation via one of the apply built-in functions. Let v t1 : : : tn be an application, where v is a variable (and thus has no discernible arity). The transformation gives: let

v1 = primapply(n + 1) v2 = primfree(n + 1) free(n + 1)0 v1 in v2 v t1 : : : tn where v1 and v2 are new variables and the free function is used to evaluate v.

 Application of a named function to too few arguments, which results in a partial application. Let f t1 : : : tn be an application, where f has arity k, k > n > 1. The transformation gives the following partial application:

n k f t1 : : : tn

pap of

 Application of a named function to the correct number of arguments, which needs no transformation.  Application of a named function to too many arguments. This leads to: (a) the application of the named function to the correct number of arguments, producing a new function as result; (b) the application of the new function from step (a) to the remaining arguments. Let f t1 : : : tn be an application, where f has arity k, 0  k < n. If k 6= 0, the transformation gives: let

v1 = f t1 : : : tk v2 = primapply(n , k + 1) v3 = free(n , k + 1) free(n , k + 1)0 v2 in v3 v1 t(k+1) : : : tn where v1 , v2 and v3 are new variables. If k = 0, the transformation gives: let

v1 = primapply(n + 1) v2 = primfree(n + 1) free(n + 1)0 v1 in v2 f t1 : : : tn where v1 and v2 are new variables.

3.3 Summary

61

Afterwards, all function applications consist of the application of a fully evaluated function to the correct number of arguments. This transformation adds calls to the built-in functions apply and free and the partial application built-in functions papnofk .

3.3 Summary In this chapter the compilation from Escher into the abstract machine language of the Escher machine was described. Because the focus of this thesis is on the abstract machine, the complete transformation route was only sketched brie y. However, the more important aspects, which are the abstract machine language and the functionality of the controlrelated built-in functions, have been introduced in sucient detail. The machine language is very closely related to the language of the Brisk machine, but supports in addition a representation for sets. The translation into the machine language introduces several built-in functions, which are used to encapsulate control-related issues like evaluation, sharing, application and partial application. These built-in functions are, originally, part of the Brisk machine. In this chapter the necessary extensions and modi cations to the Brisk built-in functions have been described. Special attention was given to the builtin functions which control evaluation; they need to be extended to support residuation. Also, a new built-in function for set evaluation was introduced. One way to support sharing in the Brisk machine is to integrate it into the return mechanism of evaluation. This method can be transferred to the Escher machine. However, sharing cannot always be used in the presence of variables in a computation. To allow the evaluation result of ground computations to be shared, the variable ag has been introduced. Purely functional computations, thus, bene t from the same degree of sharing, as when they are executed on a machine that supports functional languages only. The compilation route shows that Escher programs can be translated into the Escher Kernel Language using standard compilation techniques and some, not very complicated, new transformations. The encapsulation of evaluation, and also the other control issues, is a major bene t which the Escher implementation inherits from the Brisk machine. The extensions and modi cations, that support Escher computations, can be integrated easily into the machine model. However, it needs to be stressed that not all parts of Escher can be translated into the machine language. In particular, no method has been found to compile the de nitions of the system functions that are contained in the module Booleans. It was, therefore, decided to support the functionality of these functions through dedicated system built-in functions. The next chapter will focus on the respective Escher system functions and investigate possibilities for their implementation.

Chapter 4

Implementation of the Escher System Functions In Escher, pattern matching is not restricted to constructor terms. At present, the use of function symbols in patterns is, however, restricted to statements in system modules and functions which match on set patterns. The compilation of set pattern matching into EKL is described in Section 3.2.1.2. In this chapter, the focus is on the de nitions of the functions exists, forall, (==), (&&), (||) and not, which are contained in the Escher system module Booleans. These de nitions are not translated into the language of the abstract machine. Instead, a set of built-in system functions is provided, which supports their functionality. These built-in system functions will be introduced in the following sections.

4.1 Overview To give a motivation for the sections to follow, it is rst demonstrated on an example that some of the more complicated rules in the module Booleans (given in Appendix B), can in fact be supported by distributing their functionality over a number of built-in system functions, which act on the state of the abstract machine. This will also help to understand the general idea of how the rewrite rules contained in the Booleans module are supported on the machine level. Afterwards the focus is turned to the implementation of pattern matching on function symbols, which is used frequently in the de nitions contained in the Booleans module. As a nal part of the overview section, the issue of renaming is addressed.

4.1.1 Initial Case Study The system module Booleans is by default imported into any Escher program. It contains the de nitions of functions like (==), (&&), (||) and not, which are essential for functional logic programming in the Escher style. Here, the rules are analysed from the view of an implementation. The rules are given in a meta-language, which allows for instance to

64

Implementation of the Escher System Functions

express the result of applying the substitution {x/u} to an expression y as y{x/u}. Close examination reveals that some rules are quite unusual in that the head of the rule is given in a rather unconventional way. An example is the following rule: x && (exists \x1 ... xn -> v) && y = exists \x1 ... xn -> x && v && y --- where no xi is free in x or y; and x or y may be absent.

It de nes how the scope of an existential quanti er is extended when the quanti er is an argument of a conjunction. The rule can be understood as an abbreviation for the following two rules: x && (exists \x1 ... xn -> v) --- where no xi is free in x.

=

exists \x1 ... xn -> x && v

(exists \x1 ... xn -> v) && y --- where no xi is free in y.

=

exists \x1 ... xn -> v && y

The second side condition can then be dropped, and what remains is to ensure that no name clashes occur between quanti ed and free variables when extending the scope of the quanti er. The topic of name clashes will be discussed further in Section 4.4.5. The intention with giving just one rule for extending the scope of an existential quanti er is to apply the rule to the \maximum" context; in this case to a \whole" conjunction, which can have an arbitrarily deep structure, like: (((p u) && ((q u) && (exists \w -> (r w)))) && (s u))

The conjunction above, where p, q, r and s are predicates and intended to be rewritten in one reduction step to:

u

is a free variable, is

exists \w -> (((p u) && ((q u) && (r w))) && (s u))

To achieve this, the two rules suggested above would have to be applied incrementally several times. The essential trick to match on the original rule is to ignore all the bracketing within a conjunction, which gives the expression: (p u) && (q u) && (exists \w -> (r w)) && (s u)

It is easy to see that this expression matches the original rule. The syntactical variables and x1 to xn in the rule head, can be instantiated with the appropriate object terms. For example, x is instantiated with ((p u) && (q u)). The rewrite can then be performed. x, v, y

4.1 Overview

65

In a style similar to what was discussed so far, some rewrite rules in Booleans are intended to match on equalities in certain contexts. The corresponding rule bodies contain substitutions and various side conditions need to be ful lled before an expression can be reduced. An example is the following rule contained in the de nition of exists: exists \x1 ... xn -> x && (xi == u) && y = exists \x1 ... xi-1 xi+1 ... xn -> x{xi/u} && y{xi/u} --- where xi is not free in u; u is free for xi in x and y; -- and x or y (or both) may be absent. -- If n=1, then the RHS is x{x1/u} && y{x1/u}. -- If both x and y are absent, then the RHS is True.

Before discussing the rule, it needs to be pointed out that the traditional de nition of a binding in logic programming [Llo87] is extended in the context of this thesis. The term binding is used, on one hand, to refer to an equality which involves a variable as argument, for instance x == e, where x is a variable and e is an expression, and, on the other hand, sometimes just to refer to the expression e. Now, the discussion can focus on the rule given above. The top-level function of the rule head is exists. The rule can be understood as an abbreviation for a number of rules of a similar form, each with a di erent number of quanti ed variables and various forms of quanti ed formulas. The intention, here, is that the rule matches on an existential quanti cation, where the quanti ed formula is either an equality involving one of the quanti ed variables (i.e. a binding for a quanti ed variable), or the quanti ed formula is a conjunction (with an arbitrary structure) containing such a binding as an argument. If the conjunction contains more than one such equality, the leftmost equality is chosen rst. The bracketing of the quanti ed formula is deliberately left open. Even though (&&) is by default right associative, the intention is to match on a conjunction of any structure (as long as it contains an equality), which in fact means ignoring the bracketing as it was done in the previous example. For instance, the expression: exists \v -> ((p v) && (((q v) && v==1) && ((r v) && (s v))))

where p, q, r and s are predicates, is intended to match the rule given above. Finding a redex of this form is laborious and expensive, considering that it involves the detection of a binding, which can be an argument of an arbitrarily structured conjunction. An alternative way of looking at the rule, would be to break it down into \smaller", more conventional, rewrite rules, each of which de nes in the rule head the exact structure to match on. One of these rules would apply to an existentially quanti ed formula, which is either a binding for one of the quanti ed variables, or an application of (&&) containing such a binding in its left argument. Such a rule is then used (instead of the original rule) to bind a quanti ed variable. It could have the following form: exists \x1 ... xn -> (xi == u) && x = exists \x1 ... xi-1 xi+1 ... xn -> x{xi/u} --

66

Implementation of the Escher System Functions ---

where xi is not free in u; u is free for xi in x. If n=1, then the RHS is x{x1/u}.

This is still a meta-rule but, compared to the original rule, it can be matched on literally. Other rewrite rules are needed to de ne how a binding, occurring in an arbitrary position in a conjunction, can be made the left argument of the top-level (&&), for example: y && ((x == u) && z) y && (z && (x == u))

= (x == u) && (y && z) = (x == u) && (y && z) ...

However, suitable side conditions would need to be de ned to avoid bindings for unquanti ed variables interfering. Since bindings for unquanti ed variables remain part of the conjunction, even after their evaluation, they can potentially \block" the position at which a binding for a quanti ed variable could be reduced. This aspect shall be ignored for the moment. By breaking down the original rule in this way, all rule heads can be matched on literally. It is therefore easier to see whether an expression matches one of the rules. The binding would be brought into the right position before the variable is bound. Several rewrite steps would have the same e ect as one application of the original rule. However, not only does the number of rules increase, the structural change is obviously expensive from a computational point of view and should therefore be avoided. Moreover, the interference of bindings for quanti ed and unquanti ed variables turns out to be the reason why the approach can not be realized. From our point of view, the way to implement the rule is to support its functionality on the machine level, instead of literally matching on it. The aim is to detect, with as little e ort as possible, when an expression matches the rule (head). Searching for a binding within a conjunction is obviously not appropriate, and structural changes to bring the binding into a prede ned position are not feasible. Both equality and conjunction, together with a suitable representation of existential quanti cation, can be identi ed as the main components of the rewrite rule. The corresponding built-in functions and machine components should support the functionality of the rule on the machine level. First of all, it is necessary to establish how a potential redex would be represented. It needs to be possible to identify a variable, as existentially quanti ed variable, from any depth within the quanti ed conjunction. To allow this, the quanti ed variables are held in a dedicated register, called Existentially Quanti ed Variables Register (EQVR). The register is initialised by the built-in function sigma. It is valid for the evaluation of all direct1 arguments of the conjunction in which the call to sigma occurred. For the evaluation of an argument of an argument of a conjunction (i.e. the evaluation of a call that is not directly an argument of a conjunction), the EQVR is reset to empty. To realize this, the evaluation built-in functions can be specialised to handle the evaluation of arguments of the function (&&) di erently from the evaluation of arguments of other If (p x) && (q x) && (r (f x)) is a conjunction, then p x , q x and the call to r (f x) are regarded to be calls to direct arguments of conjunctions, whereas the call to f x is not directly an argument of a conjunction. 1

4.1 Overview

67

functions. The quanti ed formula is represented by a graph of nodes on the heap of the abstract machine. The rule is triggered when the current node is an application of the equality function, such as xi == u. Equality is implemented by a built-in system function. It rst checks whether the variable xi is contained in the EQVR. If so, it is taken o the EQVR. On the machine level, variable bindings are kept in so-called environments. The Current Environment Register (CER) points to the environment which contains a binding record for each variable in the expression currently under evaluation. The variable xi is bound to the other argument u of the equality, by inserting (a pointer to) u into the binding record of the variable x in the environment in the CER. Notice that u is not evaluated. A carefully chosen representation of variables on the heap will make it unnecessary to check most of the side conditions. This issue will be discussed again later. According to the rewrite rule, the binding either rewrites to True if it (itself) is the quanti ed formula, or it is no longer part of the quanti ed formula, if the quanti ed formula is a conjunction. To achieve this e ect on the machine level, the binding always rewrites to the constructor True. When True is evaluated, it simply returns. If it is an argument of a conjunction it will be projected out by the rules for conjunction, which are supported by the built-in system function (&&). What is left to do, is to apply the substitution to the remainder of the conjunction. This can be achieved by propagating the environment into the evaluation of all arguments of the conjunction, which is the task of the evaluation built-in functions. The substitution is applied by evaluating the variable xi in the current environment; a bound variable simply rewrites to its binding u. It is now clear that two built-in system functions, viz. conjunction and equality, contribute to the implementation of the rewrite rule under discussion. The machine state, in particular the registers EQVR and CER and the heap, represents the expression to be reduced before the binding is evaluated; it is modi ed to re ect the e ects of the rewrite rule. Through evaluating a conjunction in a leftmost outermost order, no search for the equality was necessary. However, the original rewrite rule is not applied in a leftmost outermost way. Strictly speaking, any evaluation of function calls \to the left" of the binding (this is inside x in the original rule) should not be done before the binding is evaluated. The implementation breaks the leftmost outermost reduction order in this case and for similar context-dependent rules. This can, however, be regarded as an optimisation, since the search, which would be necessary to apply such a rule literally, is avoided without the loss of the functionality of the rule. The example shows also that there is not necessarily a one-to-one correspondence between the rewrite rules de ned for a particular system function in the Booleans module and the built-in system functions provided on the machine level. This is true for a number of other rewrite rules, which will be discussed later. However, except for the quanti ers exists and forall, the application of a system function in an Escher program is not a ected by compilation. This does not apply to the quanti ers because they are replaced during compilation by the built-in functions sigma and pi. On the machine level, the system functions occurring in EKL expressions are simply mapped to the corresponding built-in system functions.

68

Implementation of the Escher System Functions

4.1.2 Implementing Pattern Matching on Function Symbols The built-in system functions have specialised evaluation code that implements demand driven, leftmost outermost reduction. An important issue is the support of patterns containing function symbols. The more complex rules, like those similar to the one discussed in the previous section, are supported by a number of built-in system functions on the machine level; they are not matched on literally. The rules, which are considered in this section, are rules such as the laws for distributing (&&) over (||) which are contained in the de nition of (&&), or De Morgan's laws which are part of the de nition of not. The key technique to support matching on function symbols is the so called look-above . Each function symbol which is used in a pattern, and each function symbol which matches on a pattern containing a function symbol, is assigned a unique identi er. When a function, say f , which is used in a pattern, is evaluated, it rst checks the identi er of the function in the call node on the stack. Remember, when an argument of a function is evaluated, the node representing the call to the function which forces the evaluation, i.e. the call node, is kept on the stack during the argument evaluation. If the identi er of the function in the call node is the identi er of a function which can match on f , the function f behaves like a constructor and simply returns, without performing any rewrite or evaluation itself. The computation returns to the function on the stack, which in turn scrutinises the identi er of its argument and performs the corresponding rewrite. This is a very exible way of supporting pattern matching on function symbols. It allows to stop the evaluation of an expression when it has reached a form on which some function can match. In [Loc93] Lock describes a way to bring matching on function symbols to the constructor level, by using the concepts of lifting and re ection . His main aim is to make all pattern matching constructor-based for the sake of a simpler implementation. Through lifting each function symbol, which is used in a pattern, is mapped to a unique data constructor. Re ection adds to each function de nition (of functions which are used like patterns) an alternative that maps an application to its lifted form, which is the corresponding data constructor. The re ected form of an application can then be matched by a corresponding lifted pattern. This is particularly useful when partially de ned functions, i.e. functions that are unde ned for some argument values, are allowed. An unde ned function application is resolved through turning it into an application of the corresponding lifted form. Assuming that the function (&&) is assigned the data constructor C(&&), the function (||) is assigned the data constructor C(||) and the function not is assigned the data constructor Cnot, the application of lifting and re ection to the de nition of the function not would be translated into an EKL declaration like the following:

4.1 Overview

69

not = primfree1 free0 not0 not0 x = case x of True -> False False -> True Cnot y -> y C(&&) y z -> let v1 = not y v2 = not z in (||) v1 v2 C(||) y z -> let v1 = not y v2 = not z in (&&) v1 v2 v -> Cnot x

The lifted form can be placed as a nal alternative (innermost re ection), which means that argument evaluation has taken place already. Alternatively, the lifted form can be placed before the argument evaluation (outermost re ection), allowing to match on a function symbol without argument evaluation. The right position needs to be chosen depending on the intended semantics. Obviously, the position in uences the termination properties of the evaluation; outermost re ection allows to match a function symbol before argument evaluation, which can lead to non-termination according to Lock. There are obvious parallels between the look-above approach and the combination of lifting and re ection; one of them is the assignment of an identi er or a data constructor to function symbols. The advantage of the approach outlined above is, that a translation scheme similar to the one suggested in [Loc93] can be de ned. This allows the respective function de nitions to be translated into EKL by the compiler. Currently, the translation of functions which pattern match on function symbols is not handled by the Escher compiler. A promising idea is to use a pre-parse over a source program to determine the look-above dependencies, i.e. the function symbols on which a look-above is needed from each function which occurs in a pattern. This information can then be used, when the code for the function that performs the look-above is created. The investigation of a compilation scheme to support the look-above mechanism is a topic for future research. However, the lifting and re ection approach does not cover the intended semantics of Escher. As an example, take a call to not p where p is de ned as follows p = not q. The expression p is evaluated to not q. Afterwards, the expression q would be evaluated. The only way to reach the constructor Cnot is for q to evaluate to some expression q0 which does not match any of the constructors in the case statement in the de nition of not0 . By re ection, not q0 rewrites to Cnot q0 and returns. Hence, the expression not p evaluates to not (not q) which then evaluates to not (Cnot q0 ), before the outer call to not reduces the double negation. What is intended in Escher is that as soon as p rewrites

70

Implementation of the Escher System Functions

to not q, the double negation is reduced without the evaluation of q. Lifting function symbols to the constructor level is simply not exible enough. The vision of Escher is to overcome the strong separation of data constructors and functions imposed by functional languages like Haskell. No distinction is made between de ned functions and constructor functions. This is also re ected by the uniform representation of function nodes on the machine level (see Section 5.1.7).

4.1.3 Renaming In general, renaming is used to distinguish syntactically equal symbols, which are used to denote di erent values. Renaming plays an important role when implementing some of the rewrite rules de ned in the Escher system module Booleans. The statement, which de nes how universal quanti cation matches on a disjunction, is a good example. It is given below. forall \x1 ... xn -> (u || v) ==> w = (forall \x1 ... xn -> u ==> w) && (forall \x1 ... xn -> v ==> w)

Two new universally quanti ed formulas are built as arguments of a conjunction. Even though the quanti ed variables in both arguments of the conjunction are represented by syntactically equal symbols, they denote distinct variables because they are bound by two separate quanti ers. This allows the quanti ed variables to be bound to di erent values in each of the new quanti cations. On the machine level, however, a variable is identi ed by the address of its heap node. This address is used to identify the binding record of the particular variable in an environment; which allows access to the binding2 of the variable. Clashes of identi ers may lead to undesired behaviour and also to incorrectness. This can be avoided, if it is required that a new identi er is used for each new variable that is introduced during the computation. Technically, this can be realized by representing new variables as new nodes on the heap. Since the node is new, it is unique during the whole computation. It needs to be emphasised here that, in an Escher program, new variables can only be introduced through the use of one of the quanti ers exists or forall, or in set patterns. As pointed out previously, the rewrite rule given above is one which introduces new quanti ed variables. It is somehow special because the new variables are actually created from existing variables at run time. To maintain the property that di erent variables have different identi ers, it is necessary to make the variables physically distinct from each other on the machine level. This is achieved by renaming the graph that represents the result of the rule application, which is held on the heap at run time. Renaming involves creating n new nodes representing variables on the heap, copying one of the new quanti cations and overwriting each occurrence of an old universally quanti ed variable by its corresponding new variable in the copy. It is also necessary to rename the environment in which the renamed part of the graph is re-evaluated later. Renaming is obviously an expensive The term binding is used here to refer to the expression a variable is bound to, not to the equality which caused the variable to be bound. 2

4.2 Existential Quanti cation

71

operation. However, the author is not aware of a suitable way to transform the above statement (and similar statements) so that renaming would not be necessary.

4.2 Existential Quanti cation Occurrences of the function exists in an Escher program are translated by the compiler into applications of the built-in system function sigma. The translation is described in Section 3.2.3.2. The task of sigma is to provide a suitable representation of the quanti ed variables and to indicate that a formula is existentially quanti ed. On the machine level, the Existentially Quanti ed Variables Register (EQVR) is used for this purpose. It is described in Section 5.1. A formal description of the operation of sigma is given in Section 5.5.1. The rewrite rules for exists are contained in Appendix B. The rst two rewrite rules in the de nition are handled implicitly by the evaluation built-in functions, which set the EQVR accordingly. The next rewrite rule is implemented by the built-in system function (||). The last rule, which speci es how an existentially quanti ed variable is bound, is supported by the built-in system functions (==) and (&&), together with several machine components and the evaluation built-in functions. This rule has been discussed in Section 4.1.1. It is not applied in a leftmost outermost order.

4.3 Universal Quanti cation The Escher Booleans module contains statements for the universal quanti er forall which implements restricted universal quanti cation. In Section 2.1.3.1 a brief motivation for supporting this kind of universal quanti cation in Escher was given. The compilation of applications of universal quanti cation, in particular lifting of forall is described in Section 3.2.3.2. As a result, all occurrences of the universal quanti er forall in an Escher program are translated into applications of the built-in system function pi. On the machine level, the functionality of the forall rewrite rules is supported by evaluating universally quanti ed formulas in several stages. The task of pi is to provide a representation of the quanti ed variables and to initiate the evaluation of the quanti ed formula. In a rst instance, the quanti ed formula is evaluated with the aim to obtain an implication as top-level function. There are no rewrite rules for implication. Hence the function symbol ==> behaves like a constructor. Once the quanti ed formula has evaluated to an implication, the antecedent of this implication is evaluated next. This is the second stage of evaluation. If the quanti ed formula does not evaluate to an implication or a deferred computation, then none of the rewrite rules for forall matches. An error has occurred and the computation is stopped. The rewrite rules de ned for forall match on di erent forms of the antecedent of the quanti ed implication. The second rule in the function de nition is a meta-rule similar to the example discussed in Section 4.1.1; it is given below.

72

Implementation of the Escher System Functions forall \x1 ... xn -> x && (xi == u) && y ==> v = forall \x1 ... xi-1 xi+1 ... xn -> x{xi/u} && y{xi/u} ==> v{xi/u} --- where xi is not free in u; u is free for xi in x, y and v; -- x or y (or both) may be absent; and, if n=1, then both x -- and y are absent. -- If n>1 and both x and y are absent, then the RHS is -- forall \x1 ... xi-1 xi+1 ... xn -> True ==> v{xi/u}. -- If n=1, then the RHS is v{x1/u}.

The rule de nes the e ects of binding a universally quanti ed variable. This rule is supported in two stages. To evaluate the antecedent, the machine uses the built-in system functions (==) and (&&), together with the UQVR and the substitution application mechanism. The nal step, which is matching on the evaluated antecedent, is supported by a specialised built-in system functions for the evaluation of universal quanti cation. A special case in the de nition of forall is the binding of the last quanti ed variable. It causes the universally quanti ed implication to be rewritten to its consequent. In the corresponding rewrite rule, this case is singled out with the comment if n=1 then both x and y are absent, which means that the antecedent must be an equality that binds the last universally quanti ed variable, for the rewrite to be applied. On the machine level, where evaluation is performed from left to right, the binding for the last quanti ed variable might be in a conjunction with some other computation. Instead of keeping this binding so that it remains part of the antecedent, the bindings for directly universally quanti ed variables are rewritten to True, and the variable is removed from the UQVR. If the antecedent reduces to True and the UQVR is empty, then all universally quanti ed variables have been bound, and the universally quanti ed implication can be rewritten to its consequent. On the other hand, if the antecedent reduces to True and the UQVR still contains some quanti ed variables, none of the rules for forall matches and an error has occurred. The built-in system function (||), which supports disjunction, does a look-above and returns when it is evaluated as an argument of a universally quanti ed implication. When the second evaluation stage (i.e. the evaluation of the antecedent) returns, the rules de ned for forall can be applied. If the antecedent did not evaluate to either False, True with an empty UQVR, a disjunction, or a deferred computation, then none of the rules matches and the computation is stopped with a match error. The statement, which de nes how to match on a disjunction, has been discussed in Section 4.1.3.

4.4 Equality According to [Llo98a] equality in Escher is to be understood as the identity relation. The aim of the following discussion is to clarify the evaluation behaviour (lazy or strict) of equality in Escher. The equality function (==) is the built-in system function which implements ( rst-order)

4.4 Equality

73

uni cation. It supports the rewrite rules which involve the Escher function (==) in the Escher Booleans module. The de nition of the function (==) is shown below. (==) :: a -> a -> Bool f x1 ... xn == f y1 ... yn = (x1 == y1) && ... && (xn == yn) --- where n >= 0; and f is a data constructor. -- (If n=0, then the RHS is True.) f x1 ... xn == g y1 ... ym = False --- where n and m >= 0; f and g are data constructors; -- and f is distinct from g. y == x = x == y --- where x is a variable; and y is not a variable. (x1,...,xn) == (y1,...,yn)

=

(x1 == y1) && ... && (xn == yn)

{x | u} == {y | v} = ({x | u} `subset` {y | v}) && ({y | v} `subset` {x | u})

In particular, the built-in function (==) implements the uni cation of constructors ( rst two rules). The third rule is not supported on the machine level. It does not seem to have an impact on the uni cation process; the machine is capable of processing bindings irrespective of the position of the variable. Tuples may be implemented as constructors, which covers the fourth rule in the equality de nition. The equality function does not provide the functionality of the general higher-order uni cation algorithm. However, the de nition of (==) covers the equality of certain terms which involve lambda abstractions, e.g. sets. The rule for set equality is implemented by resolving the overloading of the equality function in the case when both arguments are sets. This is described brie y in Section 4.4.6. How variables are bound is obviously missing from the de nition of (==) given above. Instead, binding variables appears in statements for (&&), exists and forall in the Booleans module. In this section, these statements are examined, to understand their e ects. Before doing so, some terminology will be introduced. As it will be seen later, a di erent rewrite rule applies depending on what kind of variable is involved. To distinguish between the variables, the following is de ned: An occurrence of a variable is directly existentially quanti ed if the variable is bound by an existential quanti er and it occurs in the quanti ed formula either as an argument of the top-level function, or as an argument of a function call in a conjunction if the quanti ed formula is a conjunction. An occurrence of a variable is directly universally quanti ed if the variable is bound by

74

Implementation of the Escher System Functions

a universal quanti er, the quanti ed formula is an implication and the variable occurs in the antecedent of the quanti ed formula either as an argument of the top-level function, or as an argument of a function call in a conjunction if the antecedent is a conjunction. When it is clear from the context that a particular occurrence of a variable is discussed, this occurrence of the variable will be referred to as directly existentially or universally quanti ed variable or not directly quanti ed variable. A directly quanti ed variable is either directly existentially quanti ed or directly universally quanti ed, depending on the context.

4.4.1 Binding Directly Existentially Quanti ed Variables An existentially quanti ed variable is bound according to the following rule which is part of the de nition of exists. exists \x1 ... xn -> x && (xi == u) && y = exists \x1 ... xi-1 xi+1 ... xn -> x{xi/u} && y{xi/u} --- where xi is not free in u; u is free for xi in x and y; -- and x or y (or both) may be absent. -- If n=1, then the RHS is x{x1/u} && y{x1/u}. -- If both x and y are absent, then the RHS is True.

The variable xi is bound to the other argument u of the equality. The equality xi == u is no longer part of the quanti ed formula. The substitution {xi/u} is applied to the remainder of the quanti ed formula. Afterwards the variable xi is no longer quanti ed. A number of side conditions constrain the selection of the rule. The condition xi is not free in u is in fact an occurs check , ensuring that u does not contain any free occurrences of xi. The condition u is free for xi in x and y ensures that no variable which was free before, can become bound by a quanti er after the substitution is applied. In addition, some special forms of the resulting expression are de ned in the last two lines above.

4.4.2 Binding Directly Universally Quanti ed Variables A universally quanti ed variable can only be bound according to the following rule. forall \x1 ... xn -> x && (xi == u) && y ==> v = forall \x1 ... xi-1 xi+1 ... xn -> x{xi/u} && y{xi/u} ==> v{xi/u} --- where xi is not free in u; u is free for xi in x, y and v; -- x or y (or both) may be absent; and, if n=1, then both x -- and y are absent. -- If n>1 and both x and y are absent, then the RHS is -- forall \x1 ... xi-1 xi+1 ... xn -> True ==> v{xi/u}. -- If n=1, then the RHS is v{x1/u}.

4.4 Equality

75

The quanti ed formula must be an implication. The variable xi is bound to the other argument u of the equality. The equality x1 == u is no longer part of the antecedent. The substitution {xi/u} is applied to the antecedent and the consequent of the quanti ed implication. Afterwards, the variable xi is no longer quanti ed. The rst three side conditions are the same as for existential quanti cation. In addition, the condition if n=1, then both x and y are absent restricts the rule to be selected for a single universally quanti ed variable only when the antecedent has rewritten to a binding for this particular variable. The next condition speci es that, if the antecedent rewrites to a binding for a quanti ed variable and there are more variables quanti ed, the respective variable is bound and the antecedent is rewritten to True. When this happens, no other rule for forall matches. When the last quanti ed variable is bound, the quanti cation rewrites to the consequent of the quanti ed implication.

4.4.3 Binding Variables which are Not Directly Quanti ed Escher has a (leftmost) outermost reduction order. If the previous two rules for binding variables do not apply, then the following rule matches. y && (x == u) && z = y{x/u} && (x == u) && z{x/u} --- where x is a variable; x is not free in u; -- x is free in y or z; u is free for x in y and z; -- and y or z may be absent.

The order of the rules results from the fact that:

 An existential quanti er is always scoped over the whole conjunction in which it

occurred; there is a rewrite rule that ensures this, see Section 4.6. Hence, the rule which binds directly existentially quanti ed variables is the outermost rule when it comes to binding variables in an existentially quanti ed conjunction.  Even though a universal quanti cation is more rigidly scoped, the rule which binds directly universally quanti ed variables is the outermost rule when variables in a conjunction that is the antecedent of a universally quanti ed implication are to be bound.

As a consequence, any equalities involving variables, which are not directly quanti ed by or forall, are evaluated according the rule given above. This rule binds a not directly quanti ed variable in a conjunction and applies the substitution to the remainder of the conjunction. The condition x is free in y or z ensures that the equality is not selected more than once. It is important to note that, in contrast to the previous rules where the equality does not appear on the right hand side of the rule, the equality here remains in the conjunction. Also, the substitution {x/u} is not applied to the equality. Such an equality becomes part of the answer to an Escher program, unless the same variable is bound \outside" the conjunction. In this case, a new substitution would be applied to the whole conjunction; this time including the (variable exists

76

Implementation of the Escher System Functions

in the previously evaluated) equality, which can then be rewritten further. This issue will be discussed in more detail in Section 4.5.

4.4.4 Summary: Evaluating a Binding Rewriting an equality which involves a variable can have di erent results depending on what kind of variable it is. As a consequence of the three rules for binding variables in Escher, and provided that the side conditions of the respective rules allow a rule to be applied, an equality is evaluated in several stages on the machine level: 1. If one argument of the equality is an unbound directly quanti ed variable (either existentially quanti ed or universally quanti ed), it is simply bound to the other argument of the equality and the binding is rewritten to True. The other argument of the equality is not evaluated. 2. Otherwise, the left argument of the equality is evaluated. If it evaluates to a directly quanti ed variable, it is bound in the same way as described in point 1. Otherwise, the right argument is evaluated. If this results in a directly quanti ed variable, this variable is bound to the evaluated left argument of the equality as described in point 1. 3. At this stage, both arguments have been evaluated and neither of the two arguments was, or evaluated to, a directly quanti ed variable. What remains is to deal with the evaluation results. The conclusion is: if an equality binds a directly quanti ed variable, it is evaluated lazily, otherwise strictly. If both arguments evaluate to constructors, the uni cation of these constructors is performed as de ned in the rst two rules of (==) in the Booleans module. Directly quanti ed variables can be bound to either an unevaluated or an evaluated expression. This is different for not directly quanti ed variables. These are only bound to constructors or other not directly quanti ed variables. Any other combination of arguments causes the equality to defer. The reason for this is that an unevaluated expression or a deferred computation can, when evaluated, or re-evaluated respectively, rewrite to a directly quanti ed variable. If this is the case, the directly quanti ed variable is to be bound and the equality rewritten to True. Thus binding a directly quanti ed variable is preferred to binding a not directly quanti ed variable. It follows that the order of the three binding rules discussed in Sections 4.4.1{4.4.3 is maintained, even though these rules are not directly supported, and hence are not applied in a leftmost outermost order on the machine level. They are not directly supported in the sense that they are not implemented literally as atomic, i.e. indivisible, units. Rather, they are regarded as a de ning how variable bindings are to be evaluated in Escher, and to what expressions the resulting substitution is applied. On the machine level, a conjunction, which might be an existentially quanti ed formula or the antecedent of a universally quanti ed implication, is evaluated in a leftmost outermost order. An equality (in such a conjunction or on its own) is evaluated in di erent ways depending on its arguments and

4.4 Equality

77

the context (the status of the registers EQVR and UQVR). When a variable is bound, its binding is inserted into the current environment. If the binding was an argument of a conjunction, the environment (and thus the substitution) is propagated into the whole conjunction by specialised evaluation functions for arguments of conjunctions.

4.4.5 Implementation of the Side Conditions In this section, the implementation of the side conditions, which are given in the rewrite rules for binding variables, is clari ed. These side conditions restrict the applicability of a rule. Their meaning has been explained in the previous sections, where each rule that binds variables was discussed separately. The side conditions are concerned mainly with two issues. The rst one is rather subtle. When substituting a variable with an expression, free variables contained in the expression may enter the scope of a quanti er. This happens for instance when free variables are syntactically equal to quanti ed variables. Name clashes can be avoided, if it is required that each variable identi er is used in a particular computation only once. This can be achieved by renaming variables so that a new symbol is used for each new variable; thus variable names are di erent and name clashes are ruled out from the start. On the machine level, each variable is represented by a unique node on the heap. This node is created either at the beginning of the computation for global variables, by the built-in system functions sigma or pi for quanti ed variables, or by a member of the set evaluation family evalSet for set variables. Several occurrences of the same variable share this node. As a consequence, no variable can occur both free and bound (this is bound by either of the quanti ers or a set expression) and hence it can be guaranteed, that applying a substitution will not result in binding or quantifying a variable, which was free before. This property is maintained through explicit renaming (see Section 4.1.3) when necessary. The second issue is the occurs check (see Section 4.4.1) which is part of the standard uni cation algorithm [Llo87]. As a side condition it plays an important role, since it prevents a rewrite rule from being chosen for a reduction step. The appropriate function which should perform the occurs check is a built-in function that supports the binding of variables on the machine level. This means, before a variable is bound, the occurs check should be performed. The binding is then only made when the occurs check is satis ed; otherwise none of the three variable-binding rewrite rules applies and therefore the equality remains unchanged. However, the occurs check is known to be computationally very expensive [Llo87]. Also, in practice it is not often required (i.e. it is not often violated). For these reasons the occurs check is not supported in the abstract machine. As in most Prolog implementations, the omission of the occurs check in EMA can cause incorrectness. For example, a directly quanti ed variable can be bound to a term that actually contains this variable, as in exists \x -> x == f x. As a consequence, the binding is reduced to True (irrespective of the de nition of f since the right-hand side is not evaluated) and the variable is no longer quanti ed. Hence, the whole expression reduces to True, which is not logically equivalent to the original expression. This type of error cannot be recognised during a computation (since the value of x is not used again). If the value of a variable in a circular binding is demanded, the computation goes into an in nite loop, which indicates

78

Implementation of the Escher System Functions

that an error has occurred. More research is needed to nd a neat way of supporting the occurs check in EMA. It is expected that the occurs check can be integrated into the functionality of the built-in functions which implement equality on the machine level.

4.4.6 Equality of Sets The equality function is heavily overloaded. The type checker needs to resolve overloading. In connection with this the rule for set equality can be simpli ed. The rule given in the Booleans module is: {x | u} == {y | v} = ({x | u} `subset` {y | v}) && ({y | v} `subset` {x | u})

It suggests that both arguments of the equality need to be evaluated to sets before the rewrite can be applied. However, under the condition that both arguments are sets, no prior evaluation is necessary and the rewrite can be simpli ed to the following rule. s1 == s2 = (s1 `subset` s2) && (s2 `subset` s1) -- where s1 and s2 are sets

The simpli ed version can be encoded as a set equality function setEqual with the following EKL de nition: setEqual :: (a -> Bool) -> (a -> Bool) -> Bool setEqual s1 s2 = let e1 = subset s1 s2 e2 = subset s2 s1 in (&&) e1 e2

When overloading is resolved the function setEqual is used instead of (==).

4.5 Handling of Expressions which cannot be further evaluated Residuation is the technique used in Escher to deal with insuciently instantiated function calls. A call, which cannot be reduced because it contains a variable in a position where a pattern is demanded by the corresponding function de nition, is deferred until the variable is instantiated. This topic has been discussed brie y in Section 2.1.5. In order to indicate that an expression can, at the moment, not be evaluated any further, on the machine level the node, which represents this expression on the heap, is encapsulated in a defer node. The evaluation built-in functions, which are used to evaluate strict functions, handle deferred arguments by subsequently deferring the function call which demanded

4.5 Handling of Expressions which cannot be further evaluated

79

the argument evaluation (see Section 3.2.1.1 for details). Such a deferred function call can be part of the answer to an Escher computation. In Section 2.1.5, it was mentioned that applications of system functions such as (==), not, (&&) and (||), can also be contained in an answer, in addition to the usual data constructors found in the computed values returned from Haskell programs. The typical answer to a logic computation is a disjunction of conjunctions of equalities, where the equalities hold bindings for the variables which are the arguments of the function main. These variables are the free variables in the body of the de nition of main; they are also called global variables. The body of the function main can be regarded as the goal for the logic computation. This is one important point where Escher as an integrated language extends the scope of functional languages. Whereas the emphasis in functional programming is in computing the value of a ground expression, logic computations are focused on nding bindings for the free variables in a goal with respect to a given program. As a consequence, the answer to a logic program and a speci c goal is a substitution for the free variables in the goal. In Escher, the substitution is returned explicitly as a boolean expression. The bindings for the free variables in the goal are typically returned as equalities of the form x==e, where x is a variable that is an argument of the call to main and e is an arbitrary expression. In Section 4.4.3, it was shown how bindings for not directly quanti ed variables are evaluated, and that they afterwards remain part of the expression under evaluation and might later appear in an answer. Coming back to deferred computations, such bindings can, in fact, be viewed as function calls which cannot, at the moment, be reduced further. However, when would deferred bindings be re-evaluated? The answer is simple. Let x==e be a binding where x is a variable. When a substitution, which instantiates the variable x, is applied to x==e, the equality needs re-evaluation. Such a substitution can be applied as the result of nding a (not necessarily di erent) binding for x outside the conjunction in which the binding x==e was evaluated. Let us clarify what is meant by outside . In Escher, logic computations are embedded in functional computations. A conjunction can be contained as an argument of a function call which itself is an argument of a conjunction, i.e. the conjunction occurs as a subexpression which is not itself an argument of a conjunction, but of some other function. Figure 4.1 shows an example, which will be used for a discussion. Expression:

(g (x==0 && x Bool y `in` s = s y

The compiler treats it as an application of the set s to y. It simply adds calls to the free and the apply built-ins as described in Section 3.2.1.4.

4.10 Summary This chapter has linked the de nitions of the Escher system functions in the module Booleans to the built-in system functions, which are used to support the functionality of the Booleans module on the machine level. A comprehensive study of the respective functions was necessary to understand their operation and the resulting e ects on a computation; it formed the basis for establishing the connection. The analysis has shown that features, such as the binding of variables, are distributed between di erent Escher functions. It is therefore important to analyse the de nitions in Booleans as a whole, so that these interconnections can be identi ed, and hence brought together in one system built-in function on the machine level. Even though the rewrite rules of the functions discussed in this chapter are quite complex, it is possible (due to the exible machine architecture) to support them using dedicated built-in system functions. This is one aspect in which the Escher compiler di ers from the conventional approach. Traditionally, all functions, including system functions, are translated into the language of the abstract machine. In the case of Escher, a well chosen set of rewrite rules needs to be implemented by special purpose techniques. This approach can be justi ed, since no compilation techniques that would support the translation of the function de nitions, discussed in this chapter, into a lower-level language, are known to the author.

Chapter 5

The Abstract Machine In this chapter the operational semantics of EKL, and the EMA built-in functions will be discussed. Built-in functions are used for a variety of purposes. Some deal with control issues like argument evaluation, sharing and partial application; others support the Escher extensions. EKL can be given a direct operational semantics in the form of a state transition system. A set of dedicated transitions speci es the semantics of the built-in functions. A state transition system is de ned by an initial state and a series of state transition rules. Each rule speci es a set of source states and the corresponding target states after the transition has taken place. The set of source states is speci ed implicitly using patternmatching and guard conditions. If a state is in the source set of a given transition rule, the rule matches the state. One transition rule should match any given state, and if no rule matches, the machine halts. The chapter consists of seven subsections. In the rst one, the machine state is introduced, the di erent parts of it are discussed, and the initial machine state is given. Section 5.2 contains a collection of basic transition rules which are also used in the Brisk machine. In Section 5.3 the handling of variables and deferred computations is described. This is followed by a section containing the transition rules that de ne evaluation, partial application and application. Section 5.5 gives the implementation of the built-in system functions which have been discussed in Chapter 4. The support for set processing on the machine level is described in Section 5.6. The nal section contains a summary. Because this chapter contains the technical details of the Escher implementation, it is written in a style that allows it to be read as a reference manual, rather than strictly sequential.

5.1 Machine State The EMA state has nine components. They can be subdivided into those that are also used by the Brisk machine, which runs purely functional programs, and those necessary to support the Escher extensions. The machine state consists of:

86

The Abstract Machine 1. The Code component which speci es the next operation of the machine. It can take several forms; they are described in Section 5.1.2. 2. The Current Node (NR) which is a pointer to a node in the heap and represents the node currently under evaluation. 3. The Stack (SR) which is a pointer into the heap. It points to a linked list of return nodes; these are described in Section 5.1.5. 4. The Heap (H) which contains a collection of nodes. It is represented by a mapping from a set of named pointers to nodes. These nodes are either part of the graph that represents the expression under evaluation, or they hold other parts of the machine state. The structure of heap nodes is described in Section 5.1.3. 5. The Variable Flag (VF) which is described in Section 5.1.6. 6. The Existentially Quanti ed Variables Register (EQVR) and 7. the Universally Quanti ed Variables Register (UQVR) which are pointers into the heap, pointing to linked lists of variables. 8. The Current Environment Register (CER) which is a pointer to an environment record on the heap. The structure of environment records is described in Section 5.1.4. 9. The Saved Environment Register (SER) which holds a pointer to an environment record on the heap.

The rst four components: Code, Current Node, Stack and Heap are shared between the Brisk machine and EMA.

5.1.1 General Notation Sequences are used frequently in the transition rules; for instance to represent arguments in function calls. The form x1 : : : xn denotes a sequence of n elements. Let xs be a sequence, then length (xs ) is the number of elements in xs . For brevity, a sequence with a known number of elements can be written as a vector xn , where n denotes the number of elements of the sequence. For linked lists, like the lists of quanti ed variables in EQVR and UQVR, list notation is used, instead of pointers into the heap. The form [v1 ; : : : ; vn ] denotes a linked list where v1 ; : : : ; vn are pointers to variables in the heap. The empty list is denoted by []. Let vs and ws be two lists, then vs ++ws concatenates them, v : vs adds v to the front of the list vs and v in vs is true if v is an element of vs , otherwise it is false. Further, maps are used e.g. to represent the local environment  and the heap. They are denoted by [x1 7! y1 ; : : : ; xn 7! yn]. Let xs be a map, then dom (xs ) is the domain of xs. The notation [xs 7! ys ] denotes a map obtained from two sequences xs and ys of equal length. For a map m, the notation m x denotes the value that x is mapped to. This

5.1 Machine State

87

also extends to sequences, i.e. m xs denotes the sequence of values ys to which the xs are mapped. For the heap, the notation h[p 7! nd ] extends the heap h with the mapping of pointer p to the node nd if pointer p did not exist in h, otherwise it overwrites the heap node that is pointed to by p, with nd . Let p be a pointer to an expression node f xn on the heap. The arguments or elds of the expression can be accessed using the notation p [i ] for 1  i  n. For expression nodes which are used very frequently, names can be introduced to refer to speci c argument positions. A machine state is written as a horizontal row of its components. Guard conditions can be speci ed to further constrain a transition rule; they are given in a provided clause. Such a clause can contain more than one condition. Conditions can be connected via and and or , or negated using not , these have the intuitive meaning; local de nitions in conditions can be introduced through a let clause. Auxiliary de nitions that apply to the whole transition rule can be introduced with where clauses. When the same sequence of changes to the machine state is used in di erent transition rules, an operation can be de ned which performs these changes. A typical example is the binding of a variable or the creation of a defer node on the heap. Operations have access to all components of the machine state. In addition, some operations take arguments. An operation can either modify the machine state directly or return data which can be used to modify the machine state afterwards. Finally, to save space in some transition rules, components of the machine state, which are not changed by the transition, can be omitted. However, the rst four components are always present. An underscore ( ) can be used for an arbitrary syntactic form.

5.1.2 Forms of the Code Component in the Machine State The code component in the machine state can take one of the following instructions: Enter nd Make the node, pointed to by the heap pointer nd , the current node. Exec cd Execute the code cd. Return Return to the continuation on top of the stack. Eval (e ) Evaluate the EKL expression e in the local environment .

Local environment: The local environment  is a mapping from names to heap point-

ers. It associates names with their corresponding nodes on the heap. A name can be a top-level declaration in an EKL program, a formal parameter in an application or an identi er on the left-hand side of a let binding. In order to guarantee sound substitution, it is assumed that all identi ers for function declarations, formal parameters in declarations and patterns, and the left-hand sides of let bindings are completely distinct. For the local environment , the notation  [v 7! p] extends the map  with a mapping of the name v to the pointer p.

88

The Abstract Machine

5.1.3 Nodes on the Heap The heap contains a collection of nodes. A node on the heap representing an unevaluated expression is written f xn . It can be thought of as a function call consisting of a sequence of pointers; rst the pointer to the node representing the function f , then the pointers to the nodes representing the actual arguments xn . A node representing a function can also be called function , info or descriptor node . Function nodes are built in the same way as the expression nodes; the function pointer, which points to a function constructor, is followed by a number of arguments. These arguments are: an identi er id for pattern matching, the arity n of the function and the evaluation code cd for the function, together with a sequence of references rs which are pointers to any of the global functions referred to in the evaluation code. Such a node can be written as hid=n; cd; rs i, where =n can be omitted for 0-ary functions. The identi er id is used for pattern matching either on constructors or on built-in functions. In practice, numbers are used as identi ers. To facilitate the representation, strings in typewriter font are used here. Even though, in practice, compiled functions do not have identi ers, it is assumed here (to achieve a uniform representation), that they have a default identi er, id say, which can also be given a superscript. Identi ers of built-in functions are mnemonics like var, which identi es variable nodes, or names like (&&), which is used to identify conjunctions. When the connection between a function and its identi er is clear from the context, nodes representing expressions like f xn , where f is a pointer to hidf =n; cd; rs i, can be written as idf xn . The code part cd in a function node can be of the form: \xn ->e The code of a compiled function where xn are the formal parameters and e is an EKL expression. By analogy with the STGM, this form of code is called lambda form . &ret The code of a constructor. &bltn The code of built-in functions. It represents the entry point of a procedure with name bltn. Typically, bltn is a mnemonic for one of the built-in functions; e.g. &free is the code for the free family of evaluation functions, &var is the code for variables, and &and is the code for conjunction. The notation for function nodes can be specialised if the function represents a constructor. Constructors do not have any references and their evaluation code is &ret. When the context allows, constructors can be represented as hid=n; &reti where id is the identi er of a data constructor. Nodes representing evaluated data or stack frames may contain raw data or pointers or both. In most cases, raw data can be ignored, since it is only accessed by built-in functions. To indicate that an argument is raw, a box is put round it; by default other values are assumed to be heap pointers. Whenever possible, the name of a heap pointer to a function node is chosen to be the mnemonic, used for the identi er of this function node. For example, the pointer var is used to point to the function node for variables, which has identi er var. If an argument of an expression node on the heap points to a linked list, the

5.1 Machine State

89

pointer can be used to represent the list, e.g. in a list concatenation. Moreover, instead of representing each node of the linked list on the heap, linked lists are abbreviated, and represented on the heap in list notation. A complete description of the notation used to represent heap nodes is given in Figure 5.1. node ! fun arg1 . . . argn j h d=arity, code, ptr1 . . . ptrni fun ! d j ptr arg ! value j ptr j list code ! cid j \xn -> e j &cid list ! [bdg1 ,. . . ,bdgn] j [ptr1 ,. . . ,ptrn] j [] bdg ! (ptr,ptr,fun) value ! int

j

READ

arity ! nat

n  0 (Expression node) n  0 (Function node) (Raw argument) (Lambda form) (Built-in code) n  0 (Binding list) n0 (Binding record)

j WRITE j ON j ENV j OFF j . . .

Where d, cid and ptr are identi ers, and d and cid are written in typewriter font. Figure 5.1: Notation for representing nodes on the heap To simplify the handling of nodes on the heap, some auxiliary operations are de ned in the following. Let nd be a pointer to an expression node f xn on the heap, then getInfo (nd ) returns the pointer f which points to a function node. Let f be a pointer to a function node hid=n; cd; rs i on the heap, then getId (f ) returns the identi er id contained in the function node and getArity (f ) returns the arity n. The operation getExpId (nd ) can be used to abbreviate the combination getId (getInfo (nd )); similarly, the operation getExpArity (nd ) abbreviates getArity (getInfo (nd )). In addition, the operation copy can be used; it takes a heap node as argument and returns a copy of this node. For the sake of simplicity, it shall be assumed that heap changes, caused by modi cations of the registers EQVR, UQVR, CER and SER, and by the operation copy are performed implicitly, i.e. these changes do not need be shown explicitly on the heap component of the machine in a transition rule. The following are conventions concerning heap pointers in transition rules. The right hand side of a transition rule can contain expression nodes which have a descriptor (either denoted by a pointer or a function identi er) that does not appear on the left hand side of the rule. Such descriptors are assumed to be contained in the references of the function node which contains the code part that is currently executed. This function node is either

90

The Abstract Machine

the descriptor of the current node or the descriptor of the top stack frame. Any other pointers, which appear on the right hand side of a transition rule but not on the left, are assumed to be new pointers.

5.1.4 Environments An environment is a data structure on the heap which holds the bindings for the variables that are contained in an expression. In particular, the CER holds the environment which contains the bindings for the variables in the expression pointed to by the current node (NR). Environments are represented on the heap by environment records, which are nodes of the form: env es bdl

where env points to a node on the heap of the form: env 7! hEnv=2; &reti

which is the info node of the environment constructor. The rst argument, es , in an environment record is the environment status and the second, bdl , is a pointer to a linked list of binding records1 on the heap, also called binding list. Let ce be the pointer to an environment record on the heap. In what follows, the elds of ce are referred to as ce [es ] and ce [bdl ]. The environment status is a raw word. It can take one of two values: READ or WRITE. New bindings can be written into environments with status WRITE (these environments are writable), but not into environments with status READ (these are only readable). Each binding record is a node on the heap, it contains: 1. a pointer to a variable, which is used as the index of the record; 2. a pointer to the value to which that variable is bound, which is also called the binding of the variable; or a pointer to the variable itself if it is unbound (similar to unbound variables in the WAM [AK91]); 3. a stamp , uniquely associated with a particular binding of the variable; and A linked list representation was chosen initially for simplicity, so that the discussion can be focussed on the more important machine features. Using a dynamic array instead will allow more ecient access to environment entries, but also introduces unnecessary complexity. Alternatively, a higher level of abstraction could have been chosen, where environments are simply viewed as mappings from variables to binding records. Although this is a valid approach and indeed the machine description can be rewritten using such a representation, a programmer will inevitably encounter that implementing ecient and practical support for the functionality of environments is not trivial. The list representation demonstrates one way of supporting the functionality of environments. 1

5.1 Machine State

91

4. a pointer to the next binding record in the list, or to a constructor which indicates that this is the last record. A stamp (also called binding stamp ) is represented by the address of a node on the heap. There is one stamp associated with each binding. To initialise binding stamps, a special node, here called Unbound, is used; this node is abbreviated to Ub in the transition rules. Rather than using pointers into the heap and showing the respective nodes in the heap component of the machine in each transition rule, the notation is simpli ed, whenever the simpli cation gives sucient detail. This involves representing the linked list of binding records, pointed to by bdl , as a list of the form [b1 ; : : : ; bn ]. Each binding record bi is simpli ed to a tuple of the form (v ; bdg ; s ) where v is a variable, bdg is the binding of this variable and s is the corresponding binding stamp. To simplify the handling of environments in the transition rules, the following operations are introduced.

5.1.4.1 Dereferencing a Variable in an Environment The operation fullDereference dereferences a term v (which is intended to be a variable) in a binding list bdl . fullDereference (v ; bdl ) Return the result of dereferencing the term v repeatedly in the binding list bdl until an unbound variable or non-variable expression is found. If v is not contained in bdl (e.g. because v is not a variable), then v is returned directly.

5.1.4.2 Binding a Variable The operation bindVar binds a variable in an environment. It takes a pointer to a variable v, a pointer bdg to the node to which the variable is to be bound and an environment env . bindVar (v ; bdg ; env ) Bind the variable v in the environment env . If the status of env is READ, copy env and change the status of the copy to WRITE. The copy is used instead of the original in what follows. A node s, representing a new binding stamp, is created on the heap. The variable v is bound by updating its binding record in the binding list in the environment with the pointer bdg and the new binding stamp s. A tuple, consisting of the updated environment env 0 and the new binding stamp s is returned. If the environment does not contain a binding record for v, an error has occurred and the computation is stopped.

5.1.4.3 Refreshing the Binding of a Variable A variable, v say, can be bound to an unevaluated expression, e say. A binding can be refreshed (in the current environment) if any of the variables involved in the evaluation of e are dereferenced in the same environment in which the variable v was dereferenced.

92

The Abstract Machine

Refreshing saves computing the binding value of a variable more than once; without refreshing, the value would be computed each time the variable is evaluated. It is a form of sharing evaluation results. Note that refreshing only changes the binding of the variable in the current environment. The node representing the original binding remains unchanged, because it can, potentially, be pointed to from another environment. refresh (v ; newbdg ; bdl ) Refresh the binding of variable v by updating the corresponding binding record in bdl with the binding newbdg and return the modi ed binding list bdl 0 .

5.1.4.4 Merging two Environments Every function call in a program is evaluated in a particular environment. During a computation it can become necessary to interrupt the evaluation of an expression and resume it later, e.g. when a call gets deferred. In this case, the environment needs to be stored together with the expression. This ensures that bindings, which were found before the interruption, are still available when the evaluation can be continued. When evaluation is resumed, the stored environment needs to be merged with the environment in the CER, so that both the new bindings found after the interrupt and the old (local) bindings are available. The operation which merges environments is described next. merge (local ; global ) Merge the bindings contained in the binding list of environment global into the binding list of environment local . Return a new environment record which is writable and points to a list of binding records composed of the binding records which are present in global but not in local and the updated binding records from local . The binding records of the merged environment are obtained from the binding records of local and global as follows:  For variables that exist in both environments global and local : If the variable is bound in global , its binding record in local is updated with the binding and stamp from global . If the variable is unbound in global , its binding record in local remains unchanged.  The binding records of variables which only exist in local remain unchanged.  The binding records of variables which only exist in global are copied and added to the beginning of the binding list contained in local . Because the records in local are updated, local is assumed to be writable, i.e. its state needs to be WRITE before merging.

5.1.5 Return Nodes on the Stack The stack pointer (SR) points to a linked list of return nodes. Return nodes are in fact stack frames. They are used to hold parts of the state which the machine is in before an

5.1 Machine State

93

argument evaluation. The uniform representation allows to see return nodes as functions of the form cont es vf eqv uqv env ps fn cn

where cont is a pointer to a function node representing the continuation of a built-in evaluation function; es and vf are raw arguments representing the environment status and the variable ag; eqv , uqv , env , ps are the pointers in EQVR, UQVR, CER and SR before the argument evaluation; fn is a pointer to a function node which is used as the continuation of a strict function or built-in function, and cn is a pointer to the original call node. Let st be a pointer to a stack frame. In what follows, the notation st [es ], st [vf ], st [eqv ], st [uqv ], st [env ], st [ps ], st [fn ] and st [cn ] is used to refer to the arguments of a stack frame. All stack frames, except the stack base, have the same structure no matter what the continuation function is. The stack base frame represents a return node which terminates execution when evaluation returns to it. Its structure is similar to those stack frames which are created by evaluation built-in functions. The only di erence is that the stack base does not have the last two words which are present in other stack frames. The rst word in the stack base points to a function node which contains the code for termination. The eld for the pointer to the previous stack frame is a self-reference. It is important to point out that transition rules which refer to stack frame elds in their condition part (e.g. the rules which support a look-above), do not match the current machine state if the top node on the stack does not have the particular eld that is referred to.

5.1.6 Variable Flag The variable ag was already introduced in Section 3.2.2. There, the basics of switching, one technique used in EMA to support sharing, were discussed brie y. Here, on the machine level, another way of sharing can be employed; it is called refreshing , and is used to share the evaluation result of the bindings of variables. In more detail, the evaluation of an instantiated variable, say v, causes the evaluation of the expression to which this variable is bound in the current environment. If this expression is ground, the original expression is updated, and thus the evaluation result can be shared. However, if all variables involved in the computation of the evaluation result (of the binding) have been substituted by bindings which are also contained in the environment in which v was dereferenced, the binding record of v in the current environment can be \refreshed" to point to the evaluation result of the original binding. This way, when v is dereferenced again, no re-evaluation of its binding is necessary, and the result of the previous evaluation can be shared. To determine whether refreshing can be employed, it is necessary to nd out whether the variables, that were involved in the computation of the binding of v, have been substituted by bindings which are also contained in the environment in which v was dereferenced. One way of doing so is to compare the pointer to the environment record used for the call node (where v is an argument), with the pointer to the environment record that is in the CER when the evaluated binding of v returns. If both pointers are the same, then the variables involved

94

The Abstract Machine

in the computation have been dereferenced in the same environment, and refreshing can be used. This method is, obviously, an approximation, but has shown to be suciently e ective. It is left for further research to nd a more accurate measure. In the following section, the variable ag and its use are described. The variable ag can take either of three values: ON, ENV or OFF. It indicates whether an expression contains variables and which environment these variables were evaluated in. This information is needed to support switching and refreshing of variable bindings, both of which are forms of sharing evaluation results in the presence of variables. Initially, the variable ag is OFF. Each time an argument of a function is evaluated by one of the evaluation built-in functions, e.g. a call to a free function, the current state of the variable ag is put into a stack frame, and the variable ag is set to OFF for the argument evaluation. If no variable is evaluated during the computation, the variable ag remains OFF. In this case, the original expression node can be updated with an indirection to its evaluation result (if this di ers from the original expression) just like in functional language implementations. When a variable gets evaluated, the variable ag is set from OFF to ENV by the evaluation code of the variable. This indicates that a variable was evaluated in the environment currently pointed to by the CER. When a computation cannot proceed further it returns. If the evaluation result does not di er from the original expression, the expression remains unchanged. If the variable ag is not OFF, updating cannot be used due to the presence of at least one variable and the possibility that this variable is bound to an expression which evaluates to a di erent result in a di erent environment. Instead, the call node is copied and the copy is updated at the respective position with the evaluation result of the argument. In addition, if the variable ag is ENV, the environment has not changed, i.e. the CER points to the same environment as the environment eld in the stack frame (which is the environment of the call node), and the original argument was a variable, the binding of this variable can be refreshed to point to the node that was just evaluated. Section 5.1.4.3 contains the operation which is used for this purpose. After returning from an argument evaluation, the variable ag is set to ON if it was ENV before and the environment of the argument under evaluation di ers from the environment on the stack. The new variable ag is determined from the variable ag on the stack and the current contents of the variable ag according to Table 5.1. It is put into the variable ag component (VF) in the machine state. The operation setVF works on the machine state and returns the new value of the variable

ag according to Table 5.1.

5.1 Machine State

95

SR[vf ] VF new VF OFF

ON

ENV

OFF ENV ON OFF ENV ON OFF ENV ON

OFF

if (CER = SR[env ]) then ENV else ON ON ON ON ON ENV

if (CER = SR[env ]) then ENV else ON ON

Table 5.1: Determining the next state of the variable ag

5.1.7 Initial State The initial state of EMA is: Code NR SR H VF EQVR UQVR CER SER Exec cd nd st hinit OFF [] [] ce NULL where 2 nd 7! main vn 6 main 7! hid=n; cd; rs i 6 1 6 6 v1 ! 7 var 6

hinit =

6 6 6 v 6 n 6 6 var 6 6 ce 6 6 4 st

:::

7! var 7 hvar; &var; rs2 i ! 7 Env READ [(v ; v ; Ub); : : : ; (vn ; vn ; Ub)] ! 7! base READ OFF [] [] ce st 1

:::

1

3 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 5

A computation starts with the execution of the evaluation code of the EKL function main. The function main is applied to a number of global variables v1 ; : : : ; vn . The initial heap contains nodes representing these variables; they are pointed to by v1 ; : : : ; vn . The pointer var points to a function node that represents variables. The pointer nd points to an (n + 1)-word node representing the call to the function main with the global variables as arguments. The stack points to the stack base node which is held on the heap. It represents a return node which terminates execution when evaluation returns to it. The pointer base points to a function node describing the stack base node; it contains the code for termination. The environment pointed to by the CER is initially readable and contains binding records for the global variables which are arguments to the function main. These variables are initially unbound; if none are present, the list of binding records of the initial environment is empty. The SER is initialised with the NULL pointer.

96

The Abstract Machine

On the heap, all functions are represented in a uniform way. There is no distinction between di erent kinds of functions such as de ned functions, constructor functions and primitive functions. For every top-level declaration in an EKL program, a function node is created on the heap. Functions de ned in terms of built-in functions in an EKL program are translated directly into function nodes. An example is the de nition of length given in Section 3.2.1.1: length = primfree1 free0 length0

The function node corresponding to this de nition is:

hfree=1; &free; free 0 length 0 i For every top-level declaration, in an EKL program, of the form:

f x1 : : : xn = e a corresponding function node of the form:

hid=n; \xn->e; rs i where rs are references to the global functions contained in e, is created on the heap to represent this function. Function nodes are provided for all built-in functions, e.g. the function node representing variables is hvar; &var; rs i, where rs contains just the reference to the function used to encapsulate deferred variables. The Escher system functions are provided as built-in system functions, for instance conjunction is represented by the function node h(&&)=2; ∧ rs i, where rs are references to all function nodes used by the evaluation code &and. A function node is also provided for each constructor; these nodes have the form hid=n; &reti, where id is a unique identi er and n is the arity of the constructor. Table 5.2 shows how to generate function nodes from various kinds of function de nitions in an EKL program. EKL declaration

f f f f f

Function node on heap xn = e hid=n; \xn->e; rs i where rs are references to the global functions contained in e = primfreen rs hfree=n; &free; rs i = primevalSetn rs hevalSet=n; &evalSet; rs i = primapplyn rs happly=n; &apply; rs i = primpapnofk rs hpapnofk=m; &pap; rs i where m = k , n

Table 5.2: Uniform representation of function nodes on the heap The initial heap hinit contains nodes representing all the global functions. These nodes refer to each other as necessary, hence a global environment is not needed.

5.2 Basic Transition Rules

97

5.2 Basic Transition Rules The transition rules de ned on the machine state of EMA can be divided into two sets. The rst eight rules specify the basics of a computation, such as entering nodes on the heap, executing the code of a function, evaluating EKL expressions and returning from subcomputations. These are quite simple transitions. In fact, their semantics can be given using just the rst four components of the machine state (viz. the code component, the current node, the stack and the heap); the other components remain unchanged. However, the more complicated issues like argument evaluation, sharing, partial application, the functionality of the non-constructor based Escher system functions and other Escher features like variables, residuation and set processing are not covered by the rst set of transition rules. They are encapsulated in dedicated built-in functions. A collection of specialised transition rules de nes the actions of these functions, forming the second set of rules which act on the machine state of EMA. In contrast to the rst set of transition rules, these rules modify not just the rst four components of the machine state, but also change some or all of the other components. The second set of transition rules is entered, when the code of a built-in function is executed. Built-in functions have a special form of evaluation code, which does not match any of the rst eight transition rules. Typically, a built-in function results in a return or in entering a new node, which brings us back from the second set of rules into the rst. In this section, the rst set of transition rules is given. The rules of the second set are given in the remaining sections.

5.2.1 Entering a Node Entering a node means making it the current node. The code of the new node is executed next. Code NR SR Enter new nd st =) Exec cd new st where 2 new 7! 6 7! h = 4f

:::

H

h h

f xn hid=n; cd; rs i

3 7 5

5.2.2 Execution of the Evaluation Code of a Function The evaluation code of functions can have the three forms described in Section 5.1.3. The code of compiled functions is a lambda form which is executed according to the following

98

The Abstract Machine

rule. Code NR SR H Exec \xn ->e nd st h =) Eval e  nd st h where 2 nd 7! f yn 6 h = 4 f 7! hid=n; \xn ->e; rs i

:::  = [xn 7! yn; vs 7! rs ]

3 7 5

vs are the names of the global functions contained in e A local environment is created which maps the formal parameters xn to the corresponding heap objects yn, and the free variables vs in the expression e to the pointers rs on the heap. The EKL expression e is evaluated next. The code &ret is the evaluation code of constructors. Its execution is speci ed in the next transition rule.

Code NR SR H Exec &ret nd st h =) Return nd st h Executing &ret simply results in changing the code to Return which is the next instruction of the machine. Notice that the above two rules do not match on the code of built-in functions, which is denoted by &bltn. As mentioned earlier, the execution of such code forms is described in separate sections.

5.2.3 Evaluation of EKL Code The code part of compiled functions is a lambda form \xn ->e, where e is an EKL expression. The syntax of EKL was given in Figure 3.1. The instruction Eval evaluates EKL expressions according to the following four transition rules.

5.2.3.1 Function Application The rule for function application is given rst. Code NR SR H Eval (g tm ) nd st h h =) Enter nd 0 nd st h nd 0 7! g0 t0m

i

5.2 Basic Transition Rules

99

where g0

= g m =  tm The rule represents a tail call . The names g and tm are dereferenced in the local environment  to nd the pointers to the corresponding objects g0 (which points to a function node) and t0m on the heap. A new node is created to represent the call. It is entered next.

t0

5.2.3.2 Local Bindings Local bindings are introduced by follows.

let

constructs. These are evaluated in two stages as

Code NR SR H Eval (let v1 = as 1 ; v2 = as 2 ; : : : ; vn = as n in e) nd st h provided vi 2= dom (); 1  i  n =) Eval (let v1 = as 1 ; v2 = as 2 ; : : : ; vn = as n in e)0 nd st h0 where 0 =  [v1 7! v10 ; v2 7! v20 ; : : : ; vn 7! vn0 ] 3 2 0 v1 7! ds 1 7 6 0 h0 = h 664 v2 :7!: : ds 2 775 vn0 7! ds n length (ds i ) = length (as i ); 1  i  n The rule above simply extends the local environment with entries for the new names de ned by the left-hand sides of the equalities in the let binding. These names are mapped in the new local environment 0 to pointers for new heap nodes, but the nodes are not yet \ lled in". Instead, dummies are used to allocate adequate space. This is necessary to obtain a correct representation for mutually recursive de nitions. In the second stage the heap nodes representing the expressions on the right-hand side of the let bindings are actually built, thus lling the space that has been allocated through the application of the previous rule. The remainder of the code is allowed to refer to them

100

The Abstract Machine

via the local environment. Code Eval (let

v1 = as 1 v2 = as 2 ::: vn = as n in e) provided vi 2 dom (); 1  i  n =)

Eval e 

where as 01 as 0 2

=  as 1 =  as 2

NR SR H nd st h

nd

st

2 0 v 6 v10 h 664 2

v0

n

7! as 0 7 as 0 !

1

:::

2

7! as 0n

3 7 7 7 5

:::

as 0n =  as n Notice that the as i are saturated function applications which have the form f x1 : : : xn . References to the nodes representing the function f and the arguments xn are in , either because they have already been in the local environment before the evaluation of the let binding was started (i.e. before the rst evaluation stage), or because they have been added during the rst evaluation stage. The let in EKL corresponds to letrec in the STGM [PJ92]. An optimisation for let is not provided here.

5.2.3.3 Selection A case expression is only evaluated when its variable is already known to be in evaluated form. In EMA, case is implemented as a simple switch in the code. The selection of alternatives is described in the following two rules. First, the default alternative is used because the constructor c does not match any of the

5.2 Basic Transition Rules

101

constructors cn in the alternatives. Code Eval (case x of c1 xs 1

->

e1

::: cn xs n -> en v -> e) provided p ys = h ( x) and getId (p ) 6= ci ; (1  i  n) =) Eval e 0 where 0 =  [v 7! ( x)]

NR SR H nd st h

nd

st

h

Evaluation continues with the expression given as default alternative, the variable v is bound to the node corresponding to the name x in the local environment . Next, the case in which one of the non-default alternatives matches, is shown. Code Eval (case x of c1 xs 1

->

:::

e1

NR SR H nd st h

cn xs n -> en v -> e) provided p ys = h ( x) and getId (p) = ci , for some i (1  i  n) =) Eval ei 0 nd st 0 where  =  [xs i 7! ys ]

h

Any one of the alternatives which matches the evaluated argument of the case expression can in theory be chosen. In the current EMA implementation the alternatives are considered from top to bottom. This allows better compatibility with existing Haskell code, since Haskell tries matching on statements in the order in which they are given in a program. Once an alternative has been selected, evaluation continues with the expression corresponding to the matching constructor in the environment 0 . The new environment 0 is obtained from  by adding the mappings of the constructor arguments xs i to the corresponding heap nodes ys .

5.2.4 Returning from a Subcomputation On the heap, all functions are represented in a uniform way. This also applies to constructors. When the current node is a constructor, the code contained in its function node is evaluated; this is a simple return. It indicates that the current node cannot be further

102

The Abstract Machine

evaluated. Some of the Escher system functions are not constructor-based. To enable outermost reduction, the evaluation of an argument needs to be stopped when the function, which demanded the subcomputation, can match on the top-level function of the argument; even when the argument is not yet fully evaluated. To support such behaviour, the functions, which are used like patterns in the heads of statements, need to check whether the call node on the stack is a call to one of the functions that matches on them. If this is the case, the current node must not be evaluated any further; the next reduction is performed by the function on the stack. The check is called a look-above in what follows. When the current node cannot or must not be evaluated any further, the computation returns. The return causes the code of the top stack frame to be evaluated next. This is either the continuation of a built-in evaluation function or the base of the stack in which case the computation is terminated. Code NR SR H Return nd st h =) Enter st nd st h

5.3 Variables and Deferred Computations 5.3.1 Variables EMA only evaluates a node when some function demands the value of this particular node to be computed; this applies to all nodes including those representing variables. This section describes what happens, when a variable is evaluated. A variable is represented by a 1-word node on the heap. Its descriptor is a built-in function node which has identi er var and is used as info nodes for all variables. Each variable can be identi ed individually by the address of the node that represents the variable on the heap. There are global variables and bound variables (i.e. bound by a binder such as a quanti er). Global variables are arguments of the function main; they are created when the machine is initialised. A variable can be bound by a quanti er or a set expression. Quanti ed variables are created during the computation by the built-in quanti cation functions sigma, which creates existentially quanti ed variables, and pi, which creates universally quanti ed variables. Set variables are created by set evaluation functions. EMA is based on the assumption that quanti ed variables are bound by exactly one quanti er. As a consequence, the heap address of a variable node is a unique identi er for this variable. Variables are active objects. The function node representing variables is of the form:

hvar; &var; dLeaf i where the references contain a pointer to the function node dLeaf which is used for defer leaf nodes. When a variable is evaluated, the code &var performs the following actions:

5.3 Variables and Deferred Computations

103

1. Set the variable ag to ENV if it is OFF to indicate that a variable has been evaluated. Otherwise the variable ag remains unchanged. 2. Fully dereference the variable in the environment pointed to by the CER; this yields the current binding of the variable. 3. If the result of step 2. is a variable then create a defer leaf node for this variable, make the defer leaf node the current node and return. Otherwise enter the binding. The actions of a variable support two fundamental mechanisms used in EMA: substitution application and residuation. In the following sections, the implementation of both these mechanisms is described.

5.3.1.1 Substitution Application In this thesis, substitution application means the replacement (or substitution) of a variable by its binding. In EMA, bindings for variables are kept in environments, where the variable, more speci cally its address, is used as a key for a binding record. When a variable is evaluated, the binding of this variable is looked up in the environment. The binding is used from then on instead of the variable. Looking up a binding for a variable in an environment involves fully dereferencing this variable. This means dereferencing the variable in the current environment, which yields a binding, and then repeatedly dereferencing the binding until either an unbound variable is reached or a non-variable expression. This way, binding chains of variables are followed up. The approach has two advantages. On one hand it saves substituting a variable by another variable, and thus prevents subsequent evaluation of variables. On the other hand it avoids deferring computations on variables which are bound but not instantiated; this would result in a lot of wasteful activations immediately followed by re-deferring (on another variable). Notice the di erence between bound and instantiated . A variable is instantiated when it is bound to a non-variable expression. This method is also used in the WAM to cut out reference chains when dereferencing variables. In [Car87] a similar approach is described to freeze Prolog goals with not yet instantiated arguments. Substitution application is always involved when a variable is evaluated, whether the variable is instantiated or not. In the following transition rule, the evaluation of an instantiated variable is shown. The current node pointer points to a variable. Code NR SR H VF CER Exec &var v st h vf ce provided bdg = fullDereference (v ; ce [bdl ]) and getExpId (bdg ) 6= var =) Enter bdg v st h vf 0 ce

104 where

The Abstract Machine (

if vf = OFF otherwise The variable is simply substituted by its binding which is entered next. vf 0 =

ENV

vf

5.3.1.2 Evaluation of Unbound Variables Residuation is the approach used in Escher to deal with insuciently instantiated arguments of function calls. Strict functions require their arguments to be in evaluated form before the function is called. If an argument is a variable, which is not instantiated, the evaluation of the function call is deferred until the variable is instantiated; the function call is said to be deferred on this variable. On the machine level, deferred computations are encapsulated in so-called defer nodes. The defer leaf node, which is created by &var, indicates that an uninstantiated variable has been evaluated. Typically, a deferred variable is the beginning of a defer tree, hence the name defer leaf node. The computation returns to the evaluation function on the stack, which will defer the function call that caused the evaluation of the variable argument. Defer nodes are built-in functions. They are used to encapsulate expressions which cannot be evaluated further and thus support residuation. Section 5.3.2 describes the format and functionality of defer nodes. The next transition rule shows the evaluation of an unbound variable. The current node pointer points to a variable.

Code NR SR H VF EQVR CER Exec &var v st h vf eqv ce provided bdg = fullDereference (v ; ce [bdl ]) and getExpId (bdg ) = var =) Return nd 0 st h0 vf 0 eqv ce where (nd 0 ; h 0 ) = makeDeferLeafNode (dLeaf ; bdg ) ( ENV if vf = OFF 0 vf = vf otherwise The operation makeDeferLeafNode is described in detail in Section 5.3.2.2. It creates a defer leaf node and returns a tuple consisting of a pointer to the new defer node and the modi ed heap. The defer leaf node stores the context in which the variable was evaluated, so that it can be restored when the defer leaf node is activated. A defer leaf node, which was created from a not instantiated variable, is active when the variable on which it is deferred is instantiated.

5.3 Variables and Deferred Computations

105

5.3.2 Deferred Computations Function calls which cannot at the moment be evaluated any further, because an argument is a variable that is not suciently instantiated, are encapsulated in defer nodes. Defer nodes represent control annotations; semantically a defer node is equivalent to the deferred function call (which is contained as an argument in the defer node). When the respective variable is instantiated, the defer node can be activated and the deferred function call can be resumed. In order to be able to continue the computation from the state in which the function call was deferred, defer nodes have to store the context of the deferred computation. In particular, the environment in which the function call was evaluated and the existential quanti er directly preceding the function call, if the function call is existentially quanti ed, must be stored. Because the evaluation of an uninstantiated variable is involved when a computation is deferred, copying is typically used by the evaluation built-in function (instead of sharing). For this reason, deferred computations are initially not shared. However, defer nodes can become shared subexpressions in a computation through the distribution of conjunction over disjunction. To indicate that a defer node is shared, defer nodes have a eld for a share ag, which is set at the time when they become shared. The share ag can be set to either of two values: NS indicates that a node is not shared, S is used for shared nodes. There are ve kinds of defer nodes: defer leaf nodes encapsulate variables, defer reference nodes encapsulate function calls which are deferred on one variable (e.g. strict functions which are evaluated by the built-in free functions), and the three binary defer nodes for conjunction, disjunction and equality. In the following sections, the structure of the di erent defer nodes is outlined. Rather than giving transition rules, it is more appropriate here to explain in words how the machine state is changed when a defer node is evaluated. This allows a clearer presentation.

5.3.2.1 Preliminary De nitions To access the elds in a defer node names are de ned, which are used in any defer node (no matter what kind of defer node it is), provided the kind contains the particular eld. Let dn be a defer node, then the following mapping is de ned. dn [sf ] dn [eqv ] dn [es ] dn [env ]

share ag list of existentially quanti ed variables environment status environment

In the remaining sections, frequent use is made of the following operations.

Restoring existential quanti cation restoreExists The operation restoreExists is used to restore an existential quanti er before a deferred computation is re-evaluated. It results in a modi cation of the

106

The Abstract Machine machine state of EMA. The current node pointer NR points to a defer node, say dn , when the operation is called. If dn is shared (i.e. dn [sf ] = S) then the list of quanti ed variables in the defer node dn [eqv ] needs to be copied rst and the copy is used instead of dn [eqv ] in what follows. Let vs be the current contents of EQVR. The quanti er in the defer node is restored by setting EQVR to the concatenation dn [eqv ]++vs .

Updating existential quanti cation updateExists The operation updateExists is used to update existential quanti cation in inactive defer nodes. The operation modi es the machine state of EMA. When the operation is called, the current node pointer NR points to a defer node, say dn . If dn is shared (i.e. dn [sf ] = S) then the node and the list of quanti ed variables in the defer node dn [eqv ] need to be copied rst. The share ag in the copy of dn is set to NS and the current node pointer is set to point to the copy. The copies are used in what follows. Using updateExists can have two outcomes. On one hand an existential quanti er, which is kept in a defer node, needs to be lifted into a conjunction if the defer node is evaluated as an argument of a conjunction. This is done as follows. Let vs be the current contents of EQVR. The quanti er in the defer node is lifted by setting EQVR to the concatenation dn [eqv ]++vs and by emptying the eld for the existential quanti er in the defer node (i.e. dn [eqv ] = []). On the other hand, an existential quanti er needs to be saved in a defer node if the defer node is not an argument of a conjunction. This happens as follows. Let vs be the existentially quanti ed variables in EQVR. The new contents of dn [eqv ] is the concatenation dn [eqv ]++vs . The EQVR is emptied (i.e. EQVR = []). The defer node dn is an argument of a conjunction if the identi er of the call node in the top stack frame, which is pointed to by SR, is (&&).

Restoring an environment restoreEnv The operation restoreEnv is used to restore the environment before a deferred computation is re-evaluated. It results in a modi cation of the machine state of EMA. The current node pointer NR is pointing to a defer node, say dn , when the operation is called. The environment in which the deferred call is to be evaluated, is the environment currently in the CER if the environment in the defer node is readable (i.e. dn [es ] = READ), or if the CER and the environment in dn [env ] point to the same environment record. In these cases no change to the CER is necessary. Otherwise, the CER needs to be set to the

5.3 Variables and Deferred Computations

107

result of merging the environment in dn [env ] with the current contents of CER. Environment merging is described in Section 5.1.4.4. If the environment status dn [es ] in the defer reference node is COPY then a copy needs to be taken of the environment in dn [env ] before merging. The status COPY is an environment status which only occurs in defer and activate nodes. It is set when defer nodes, which store writable environments, become shared.

Updating an environment updateEnv The operation updateEnv is used to update the environment in inactive defer nodes. It results in a modi cation of the machine state of EMA. The current node pointer NR is pointing to a defer node, say dn , when the operation is called. If dn is shared (i.e. dn [sf ] = S) then the node needs to be copied rst. The share ag in the copy is set to NS and the current node pointer is set to point to the copy. The copy is used instead of dn in what follows. The environment status eld and the environment eld in dn are updated as follows. If dn [es ] = READ then dn [es ] = CER[es ] and dn [env ] = CER. If dn [es ] = WRITE then the environment dn [env ] is merged with the environment currently pointed to by the CER. Environment merging is described in Section 5.1.4.4. The eld dn [env ] in the defer node is updated with the result of the merge operation. If dn [es ] = COPY then the environment in dn [env ] needs to be copied; the environment status of the copy and the status in dn [es ] are set to WRITE. Afterwards, the copied environment is merged with the environment currently pointed to by the CER. The eld dn [env ] in the defer node is then updated with the result of the merge operation. The environment status COPY only occurs in defer and activate nodes. It is set when defer nodes, which store writable environments, become shared.

5.3.2.2 Defer Leaf Nodes Defer leaf nodes are built-in functions with identi er dL. They are created when a not instantiated variable is evaluated. Typically, the function which demanded the evaluation is deferred subsequently; whole branches of computations can be deferred like this, thus creating defer trees. Deferred variables being the leafs of these trees. Defer leaf nodes have the following structure: dLeaf sf eqv v s

where dLeaf points to an info node in the heap of the form: dLeaf 7! hdL=4; &dL; aLi

In addition to the share ag sf and the existential quanti er, which is kept in eqv , defer leaf nodes contain a pointer to the deferred variable v and its binding stamp s at defer time.

108

The Abstract Machine

Environments are not kept in defer leaf nodes. The pointer aL, contained in the references, points to the function node which is used for active leaf nodes; these are described further in Section 5.3.3.1. Let dn be a pointer in the heap to a defer leaf node. In what follows the elds of the defer leaf node, pointed to by dn are referred to as dn [sf ], dn [eqv ], dn [v ] and dn [s ]. In addition, the operation getVar (dn ) is de ned to return the pointer in dn [v ].

Creating defer leaf nodes. The operation makeDeferLeafNode is used to build new defer leaf nodes. It takes a pointer to the descriptor for defer leaf nodes dLeaf and the unbound variable var for which a defer leaf node needs to be created. It creates a defer leaf node on the heap and returns a tuple consisting of a pointer to the new defer node and the modi ed heap. Let new be the pointer to a new defer leaf node. The elds of the defer leaf node pointed to by new are lled as follows. new [sf ]

=

(

[] if the variable var is an argument of a conjunction EQVR otherwise = var = Ub

new [eqv ] = new [v ] new [s ]

NS

Initially, the defer leaf is not shared. The eld new [eqv ] is an empty list if the variable var is an argument of a conjunction, otherwise new [eqv ] takes the contents of EQVR. The variable is an argument of a conjunction if the identi er of the call node in the top stack frame, which is pointed to by SR, is (&&). The initial stamp for a defer leaf is the default stamp for unbound variables.

Evaluating a defer leaf node. Let dn be the pointer to a defer leaf node which is held in NR. When this node is evaluated, the code &dL performs the following actions: Check whether dn is active. Defer leaf nodes are active if the variable dn [v ] is instantiated; more precisely, the variable needs to be instantiated with a binding di erent to one at defer time. By testing the binding stamp of the variable in the current environment it can be determined, whether the variable got bound since the defer leaf was created. If this stamp is not the stamp for unbound variables, and it is di erent from the stamp dn [s ] which is kept in the defer leaf node, the variable has a new binding. If this new binding is not a variable, the defer leaf node is active.

 If dn is active then:

5.3 Variables and Deferred Computations

109

The contents of the existentially quanti ed variables register EQVR is modi ed through a call to the operation restoreExists if the defer leaf node contained an existential quanti er. Let bdg be the binding of dn [v ] dereferenced in the CER. The next instruction is Enter bdg .  else (dn is inactive): Inactive defer nodes remain deferred and behave like constructors; when evaluated they return. However, the context stored in the defer node needs to be updated before, to re ect the current machine state. In the case of defer leaf nodes this means that the quanti er in the defer node might need updating. This is achieved through a call to updateExists . The next instruction is Return .

5.3.2.3 Defer Reference Nodes Defer reference nodes are built-in functions with identi er dR. They are created by evaluation built-in functions like free, when an argument under evaluation returns deferred. The function which demanded the evaluation of this argument is encapsulated in a defer reference node. Function calls encapsulated by defer reference nodes have exactly one argument which is a reference to another defer node, hence the name defer reference . Typically, chains of defer reference nodes are built by the continuations of the evaluation built-in functions until either evaluation returns to a built-in system function like (&&) or (||), where the computation can continue in the other argument, or evaluation returns to the top level in which case the computation is terminated. Defer reference nodes have the following structure: dRef sf p es env eqv fc

where dRef points to an info node on the heap: dRef 7! hdR=6; &dR; aR i

Defer reference nodes contain a share ag sf , the position p of the deferred argument in the deferred call, the environment state es and the environment env , which were valid for the deferred call at defer time, an existential quanti er in the form of eqv and a pointer fc to the function call which is deferred (also called defer(red) call ). The pointer aR , contained in the references, points to the function node which is used for active reference nodes; these are described further in Section 5.3.3.2. Let dn be a pointer in the heap to a defer reference node. In what follows, the elds of the defer reference node, pointed to by dn , will be referred to as dn [sf ], dn [p ], dn [es ], dn [env ], dn [eqv ] and dn [fc ].

Creating defer reference nodes. The operation makeDeferRefNode is used by the continuation of evaluation functions (i.e. the stack frame descriptors) to build new defer reference nodes when an argument of a

110

The Abstract Machine

strict function returns deferred. It takes the descriptor for defer reference nodes dRef , a pointer to the function call which needs to be deferred dc , the position i of the argument on which the defer reference is created and a heap, which amongst others, contains the deferred call. The operation creates a defer reference node on the heap and returns a tuple consisting of a pointer to the new defer node and the modi ed heap. Let new be the pointer to a new defer reference node. The elds of the defer reference node pointed to by new are lled as follows. new [sf ] new [p ] new [es ] new [env ]

= NS = i = SR[es ] = SR[ env ] ( [] if dc is an argument of a conjunction new [eqv ] = SR[eqv ] otherwise new [fc ] = dc Initially, defer reference nodes are not shared. The environment state and the environment in the defer reference node are those used for the function call before any argument evaluation; these are kept in the stack frame at the time the function call is deferred. The eld for the existential quanti er new [eqv ] is an empty list if the function call is an argument of a conjunction, otherwise new [eqv ] is lled with the existential quanti er that was valid for the function call before the argument evaluation; it is kept in the stack frame similar to the environment status and the environment. The function call is an argument of a conjunction if the identi er of the call node in the previous stack frame SR[ps ] is (&&).

Evaluating a defer reference node. Let dn be the pointer to a defer reference node which is held in NR, i be the value in dn [p ] and dc be the pointer in dn [fc ]. When dn is evaluated, the code &dR performs the following actions: Check whether dn is active. A defer reference node is active if the argument at position i in the deferred call dc is active. The argument can be any kind of defer node. The activation check is thus simply passed to the deferred argument.

 If dn is active then:

For the activated computation an environment needs to be provided that holds both the bindings in the environment in dn [env ] and the new bindings established after the computation was deferred; these are kept in the environment currently in the CER. The operation restoreEnv is used to produce the new environment. Let ms be the machine state which results from the call to restoreEnv . The operation restoreExists is used on ms to modify the contents of the existentially quanti ed variables register EQVR if dn contains an existential quanti er.

5.3 Variables and Deferred Computations

111

If dn is shared, the deferred call dc is copied and the copy is used instead of dc in what follows. The deferred call dc needs to be updated at position i with a pointer to the activated argument. The node at fc [i ] is currently a defer node. To avoid subsequent tests for activation on arguments of active defer nodes, the operation activate is applied to an active defer node. It is described in more detail in Section 5.3.3. The operation activate creates activate nodes from active defer nodes, thus marking a branch of a defer tree as active. Using activate , the deferred call dc is updated like this dc [i ] = activate (dc [i ]). The next instruction is Enter dc . This has the e ect that the evaluation of the arguments of the deferred call is resumed at position i.  else (dn is inactive): Inactive defer nodes remain deferred and behave like constructors; when evaluated they return. However, the context stored in the defer node needs to be updated before, to re ect the current machine state. In the case of defer reference nodes this means that the environment and the quanti er in the defer node might need updating. This is achieved through a call to updateEnv followed by a call to updateExists . The next instruction is Return . Notice that before modifying a shared defer reference node a copy needs to be taken, so that the original node remains unchanged.

5.3.2.4 Binary Defer Nodes Binary defer nodes are created for calls to (&&) and (||) which cannot be reduced further at the current state of evaluation and in some cases for calls to (==) as well. They are called binary because they contain both arguments of the conjunction, disjunction or equality and hence join two branches of a defer tree. A binary defer node is active if one of its arguments is active.

5.3.2.5 Defer Conjunction and Defer Equality Nodes Because defer conjunction nodes and defer equality nodes have a similar structure and functionality, both of them are described in one section. Defer conjunction nodes are built-in functions with identi er d(&&). They are created only by the code of the function (&&) when both arguments of a conjunction are inactive defer nodes. Defer conjunction nodes have the following structure: dAnd sf es env eqv x1 x2

where dAnd points to an info node on the heap: dAnd 7! hd(&&)=6; &dBin; aAndL aAndR evalAndL evalAndR i

112

The Abstract Machine

The structure of defer conjunction nodes is similar to defer reference nodes. However, they do not need to hold the argument position, and instead of the pointer to the deferred call, they hold pointers to the arguments x1 and x2 of the deferred conjunction. The references in the info node contain two pointers aAndL and aAndR to the info nodes used for activated conjunction nodes (these are described further in Section 5.3.3.4) and pointers to the evaluation functions evalAndL and evalAndR (which are described in Section 5.5.4.2). Defer equality nodes are built-in functions with identi er d(==). They are created by the built-in evaluation function continuation eqLD0 when both arguments of an equality are defer nodes, but not defer leafs. The structure of defer equality nodes is the same as for defer conjunction nodes. However, instead of the descriptor dAnd they use the descriptor dEq which points to an info node on the heap of the form: dEq 7! hd(==)=6; &dBin; aEqL aEqR evalEqL eqLD i

The references in dEq contain two pointers aEqL and aEqR which point to info nodes used for activated equality nodes (these are described further in Section 5.3.3.4) and pointers to the evaluation functions evalEqL and eqLD (which are described in Section 5.5.3.1). Let dn be a pointer in the heap to a defer conjunction or a defer equality node. In what follows, the elds of the defer node, pointed to by dn , are referred to as dn [sf ], dn [es ], dn [env ], dn [eqv ], dn [x1 ] and dn [x2 ]. The last two elds are also called the left or right branch of the defer node.

Creating defer conjunction nodes. The operation makeDeferAndNode is used by the code of conjunctions to create defer conjunction nodes. It takes the descriptor for defer conjunction nodes dAnd and pointers to two defer nodes d1 and d2 which are the arguments of a conjunction. The operation creates a defer conjunction node on the heap and returns a tuple consisting of a pointer to the new defer node and the modi ed heap. The current node pointer points to the conjunction which needs to be deferred. Let new be the pointer to a new defer conjunction node. The elds of the node pointed to by new are lled as follows. new [sf ] = NS new [es ] = CER[es ] new [env ] = CER ( [] if the current node is an argument of a conjunction new [eqv ] = EQVR otherwise new [x1 ] = d1 new [x2 ] = d2

Initially, defer conjunction nodes are not shared. The environment state and the environment in the defer conjunction node are taken from the current environment pointed to by the CER. The eld for the existential quanti er new [eqv ] is an empty list if the current

5.3 Variables and Deferred Computations

113

node (which is the conjunction that is deferred) is itself an argument of a conjunction, otherwise new [eqv ] is lled with the pointer held in EQVR. The current node is an argument of a conjunction if the identi er of the call node in the top stack frame is (&&).

Creating defer equality nodes. The operation makeDeferEqNode is used by the code of the evaluation continuation function eqLD0 to defer equality nodes. It takes the descriptor for defer equality nodes dEq and pointers to two defer nodes d1 and d2 . The operation creates a defer equality node on the heap and returns a tuple consisting of a pointer to the new defer node and the modi ed heap. Let new be the pointer to a new defer equality node. The elds of the node pointed to by new are lled as follows. new [sf ] = NS new [es ] = SR[es ] new [env ] = SR[ env ] ( [] if the call node is an argument of a conjunction new [eqv ] = SR[eqv ] otherwise new [x1 ] = d1 new [x2 ] = d2

Initially, defer equality nodes are not shared. The environment state and the environment in the defer equality node are taken from the top stack frame. The eld for the existential quanti er new [eqv ] is an empty list if the call node, which is an equality, is an argument of a conjunction, otherwise new [eqv ] is lled with the existential quanti er that was valid for the equality before the argument evaluation; it is kept in the stack frame similar to the environment status and the environment. The equality is an argument of a conjunction if the identi er of the call node in the previous stack frame SR[ps ] is (&&).

Evaluating a defer conjunction or a defer equality node. Let dn be a pointer to a defer conjunction node (or a defer equality node respectively) which is held in NR. When dn is evaluated the code &dBin performs the following actions: Check whether the left branch dn [x1 ] is active. If the left branch is not active, check the right branch dn [x2 ].  If none of the branches is active: Inactive defer nodes remain deferred and behave like constructors; when evaluated they return. However, the context stored in the defer node needs to be updated before, to re ect the current machine state. In the case of defer conjunction nodes (or defer equality nodes respectively) this means that the environment and the quanti er in the defer node might need updating. This is achieved through a call to updateEnv followed by a call to updateExists . The next instruction is Return .

114

The Abstract Machine  else (at least one branch is active):

For the activated conjunction (or equality respectively) an environment needs to be provided that holds both the bindings in the environment in dn [env ] and the new bindings established after the conjunction (or equality respectively) was deferred; these are kept in the CER. Furthermore, if the deferred conjunction (or equality respectively) was existentially quanti ed, the contents of EQVR needs to be restored before the conjunction (or equality respectively) is re-evaluated. Both can be achieved through subsequent calls to the operations restoreEnv and restoreExists . To re-evaluate the conjunction (or equality respectively), a new 3-word node, say nd 0 , is created on the heap. Let dn [xi ] be the active branch and dn [xj ] the respective other branch of the defer node, for some i; j 2 f1; 2g. Depending on which branch is active, this node represents a call to either evalAndL (if the left branch is active) or evalAndR (if the right branch is active) in the case of a conjunction (or a call to either evalEqL or eqLD in the case of an equality respectively) with activate (dn [xi ]) and dn [xj ] as the respective arguments. The next instruction is Enter nd 0 . The operation activate is described in Section 5.3.3.

5.3.2.6 Defer Disjunction Nodes Defer disjunction nodes are built-in functions with identi er d(||). They are created only by the code of the function (||) when both arguments to a disjunction are deferred. Defer disjunction nodes have the following structure: dOr sf es env x1 x2

where dOr points to an info node on the heap: dOr 7! hd(||)=5; &dOr; aOrL aOrR evalOrL evalOrR i

Compared to the other kinds of defer nodes, defer disjunction nodes do not hold existential quanti ers. Apart from that, they have a structure similar to defer conjunction nodes. In fact, existential quanti ers which directly preceded a disjunction are rewritten according to the following statement: exists \x1 ... xn -> (x || y) = (exists \x1 ... xn -> x) || (exists \x1 ... xn -> y)

Disjunction is implemented in a way that it can only defer after it has distributed the preceding existential quanti er, which means that at defer time a call to (||) cannot be the top-level function of an existentially quanti ed formula. The references in the info node for defer disjunction nodes contain two pointers aOrL and aOrR to the info nodes used for activated disjunction nodes (these are described further in Section 5.3.3.5) and pointers to the evaluation functions evalOrL and evalOrR (which are described in Section 5.5.5.2).

5.3 Variables and Deferred Computations

115

Let dn be a pointer in the heap to a defer disjunction node. In what follows, the elds of the defer disjunction node, pointed to by dn , are referred to as dn [sf ], dn [es ], dn [env ], dn [x1 ] and dn [x2 ]. The last two elds are also called the left or right branch of the defer disjunction node.

Creating defer disjunction nodes. The operation makeDeferOrNode is used by the code of disjunctions to create defer disjunction nodes. It takes the descriptor for defer disjunction nodes dOr and pointers to two defer nodes d1 and d2 which are the arguments of a disjunction. The operation creates a defer disjunction node on the heap and returns a tuple consisting of a pointer to the new defer node and the modi ed heap. The current node pointer points to the disjunction which needs to be deferred. Let new be the pointer to a new defer disjunction node. The elds of the node pointed to by new are lled as follows. new [sf ] new [es ] new [env ] new [x1 ] new [x2 ]

= = = = =

NS

CER[es ] CER

d1 d2

Initially, defer disjunction nodes are not shared. The environment state and the environment in the defer disjunction node are taken from the current environment pointed to by the CER.

Evaluating a defer disjunction node. Let dn be a pointer to a defer disjunction node which is held in NR. When dn is evaluated, the code &dOr performs the following actions: Check whether the left branch dn [x1 ] is active. If the left branch is not active, check the right branch dn [x2 ].  If none of the branches is active: Inactive defer nodes remain deferred and behave like constructors; when evaluated they return. However, the context stored in the defer node needs to be updated before, to re ect the current machine state. In the case of defer disjunction nodes this means that the environment in the defer node might need updating. This is achieved through a call to updateEnv . The next instruction is Return .  else (at least one branch is active): Before the disjunction can be re-evaluated, an environment needs to be provided which holds both the bindings in the environment in dn [env ] and the new bindings established after the disjunction was deferred; these are kept in the CER. The environment is restored through a call to restoreEnv .

116

The Abstract Machine To re-evaluate the disjunction, a new 3-word node, say nd 0 , is created on the heap. Let dn [xi ] be the active branch and dn [xj ] the respective other branch of the defer node, for some i; j 2 f1; 2g. Depending on which branch is active, this node represents a call to either evalOrL (if the left branch is active) or evalOrR (if the right branch is active) with activate (dn [xi ]) and dn [xj ] as the respective arguments. The next instruction is Enter nd 0 . The operation activate is described in Section 5.3.3.

5.3.3 Activation of Deferred Computations When a defer node is evaluated, it rst checks whether it is active. An activation check involves traversing a tree of defer nodes and checking whether the leafs are active. When an active leaf is found, the root node is also active, and so are all defer nodes between the root and the active leaf. The predicate isActive can be used to determine whether a defer node is active. It takes a pointer to a defer node and returns true if the node is an active defer node, otherwise it returns false. To mark an active branch in a defer tree, activate nodes can be created from defer nodes using the operation activate . This operation takes a pointer to the root of an active defer tree and returns a pointer to a partially copied tree, in which the active defer nodes have been replaced by activate nodes. In practice, both isActive and activate can be combined so that the defer tree is only traversed once. When an activate node is evaluated, it simply restores the context of the previously deferred computation and afterwards enters the active function call, which is then reevaluated. Building activate nodes avoids subsequent activation checks in active branches of defer trees. There is an activate node corresponding to defer leaf nodes and one for defer reference nodes. A binary defer node is active when one of its arguments is active. To indicate which argument is the active one, there are two kinds of activate nodes for each binary defer node. How activate nodes are built and evaluated, is described in the following sections. As a convention, elds with the same name refer to the same contents in defer nodes and their corresponding activate nodes.

5.3.3.1 Active Leaf Nodes Active leaf nodes are built-in functions with identi er aL. They are created by the operation activate from active defer leaf nodes. Active leaf nodes have the following structure: aLeaf eqv bdg

where aLeaf points to an info node on the heap: aLeaf 7! haL=2; &aL; rs i

Instead of the deferred variable and its binding stamp, active leaf nodes just contain the binding of the previously deferred variable. The references rs are empty.

5.3 Variables and Deferred Computations

117

Let an be a pointer to an active leaf node. In what follows, the elds of an are referred to as an [eqv ] and an [bdg ].

Creating active leaf nodes. The operation activate returns an active leaf node when applied to an active defer leaf node. Let dn be the pointer to an active defer leaf node and let new be a pointer to a new active leaf node. The elds of the active leaf node pointed to by new are lled as follows. (

dn [eqv ] if dn [sf ] = NS copy (dn [eqv ]) otherwise new [bdg ] = fullDereference (dn [v ]; CER[bdl ]) new [eqv ] =

The descriptor for the active leaf node is obtained from the references in the defer leaf info node.

Evaluating an active leaf node. The following transition rule describes the evaluation of an active leaf node, which is pointed to by NR. Code NR SR H EQVR Exec &aL nd st h qv =) Enter nd [bdg ] nd st h nd [eqv ]++qv

5.3.3.2 Active Reference Nodes Active reference nodes are built-in functions with identi er aR. They are created by the operation activate from active defer reference nodes. Active reference nodes have the following structure: aRef es env eqv fc

where aRef points to an info node on the heap: aRef 7! haR=4; &aR; rs i

The last reference fc in an active reference node points to the activated function call. The references rs are empty. Let an be a pointer to an active reference node. In what follows, the elds of the node pointed to by an , are referred to as an [es ], an [env ], an [eqv ] and an [fc ].

118

The Abstract Machine

Creating active reference nodes. The operation activate returns an active reference node when applied to an active defer reference node. Let dn be a pointer to an active defer reference node, let dc be the deferred call in dn [fc ], let i be the value in dn [p ]. Let an be the active node obtained from the call activate (dc [i ]), let dc 0 be either the pointer to a copy of dc , updated at position i with an if dn [sf ] = S, or a pointer to dc updated in the same way otherwise, and let new be the pointer to a new active reference node. The elds of the node pointed to by new are lled as follows: (

if dn [sf ] = S and dn [es ] = WRITE dn [es ] otherwise new [env ] = dn [env ] ( dn [eqv ] if dn [sf ] = NS new [eqv ] = copy (dn [eqv ]) otherwise 0 new [fc ] = dc

new [es ]

=

COPY

The descriptor for the active reference node is obtained from the references in the defer reference info node.

Evaluating an active reference node. The following transition rule describes the evaluation of an active reference node, which is pointed to by NR. Code NR SR H EQVR CER Exec &aR nd st h qv ce =) Enter nd [fc ] nd st h nd [eqv ]++qv ce 0 where 8 > if nd [es ] = READ or nd [env ] = ce < ce 0 ce = > merge (nd [env ]; ce ) if nd [es ] = WRITE : merge (copy (nd [env ]); ce ) if nd [es ] = COPY

5.3.3.3 Binary Active Nodes Binary activate nodes are created when the operation activate is applied to a binary defer node. Notice that only one branch of a binary defer node needs to be active to activate a binary defer node. The activate node indicates which argument of an active binary defer node is active.

5.3.3.4 Active Conjunction Nodes and Active Equality Nodes Because active conjunction nodes and active equality nodes have a similar structure and functionality, both of them are described in one section.

5.3 Variables and Deferred Computations

119

Active conjunction (and respectively active equality) nodes are built-in functions with identi er a(&&)L (a(==)L) or a(&&)R (a(==)R) indicating that either the left or right argument of a conjunction (or an equality respectively) is active. They are created by the operation activate from active conjunction (or respectively active equality) nodes. The structure of a left-active conjunction node is as follows: aAndL es env eqv x1 x2

where aAndL points to an info node on the heap: aAndL 7! ha(&&)L=5; &aBin; evalAndLi

The activated argument is x1 . The references contain a pointer to the evaluation function evalAndL which is described in Section 5.5.4.2. Left-active equality nodes only di er in the descriptor, which points to the following info node on the heap: aEqL 7! ha(==)L=5; &aBin; evalEqLi

The evaluation function evalEqL is described in Section 5.5.3.1. The structure of right-active conjunction (or respectively equality) nodes is the same, except that the active argument is x2 and the descriptor is di erent. The following node on the heap is used for right-active conjunction nodes: aAndR 7! ha(&&)R=5; &aBin; evalAndR i

The evaluation function evalAndR is described in Section 5.5.4.2. Right-active equality nodes have the following info node on the heap: aEqR 7! ha(==)R=5; &aBin; eqLD i

The evaluation function eqLD is described in Section 5.5.3.1. Let an be a pointer to an active conjunction (equality respectively) node. In what follows, the elds of the node pointed to by an , are referred to as an [es ], an [env ], an [eqv ], an [x1 ] and an [x2 ].

Creating left- and right-active conjunction and equality nodes. The operation activate returns either a left-active or a right-active conjunction (or equality respectively) node when applied to a defer conjunction (or respectively equality) node which is active on the left or right argument. Let dn be a pointer to a defer conjunction (equality respectively) node which is active on its left argument. Let new be the pointer to a new left-active conjunction (equality respectively) node. The elds of the node pointed to by new are lled as follows:

120

The Abstract Machine (

if dn [sf ] = S and dn [es ] = WRITE dn [es ] otherwise new [env ] = dn [env ] ( dn [eqv ] if dn [sf ] = NS new [eqv ] = copy (dn [eqv ]) otherwise new [x1 ] = activate dn [x1 ] new [x2 ] = dn [x2 ] The descriptor of the active node is obtained from the references contained in the descriptor of the defer node dn . When activate is applied to a defer node which is active on the right argument, then a call to activate is made on dn [x2 ] instead of dn [x1 ]. new [es ]

=

COPY

Evaluating an active conjunction or equality node. The following transition rule describes the evaluation of an active conjunction or active equality node, which is pointed to by NR. Code NR SR Exec &aBin nd st =) Enter nd 0 nd st where " nd 7! h = aBin 7!

h0

h

H EQVR CER h qv ce h0 eqv ++qv ce 0 aBin es env eqv x1 x2 h =5; &aBin; eval i

#

i

= h nd 0 7! eval x1 x2 8 > if es = READ or env = ce < ce ce 0 = > merge (env ; ce ) if es = WRITE : merge (copy (env ); ce ) if es = COPY A new node representing a call to an evaluation function with both arguments of the conjunction (or equality respectively) is created and entered next. The pointer eval is either evalAndL or evalAndR in the case of an active conjunction depending on whether getExpId (nd ) returns a(&&)L or a(&&)R. For an active equality the pointer eval points either to evalEqL or eqLD depending on whether getExpId (nd ) returns a(==)L or a(==)R.

5.3.3.5 Active Disjunction Nodes Active disjunction nodes are built-in functions with identi er a(||)L or a(||)R indicating that either the left or right argument of a disjunction is active. They are created by the operation activate from active disjunction nodes. The structure of a left-active disjunction node is as follows: aOrL es env x1 x2

5.3 Variables and Deferred Computations

121

where aOrL points to an info node on the heap: aOrL 7! ha(||)L=4; &aOr; evalOrLi

The reference evalOrL is a pointer to one of the evaluation functions for disjunction (see Section 5.5.5.2 for a description). The structure of right-active disjunction nodes is similar, except that the descriptor is a pointer to the following heap node: aOrR 7! ha(||)R=4; &aOr; evalOrR i

The reference evalOrR is a pointer to one of the evaluation function for the arguments of disjunctions (see Section 5.5.5.2 for a description). Let an be a pointer to an active disjunction node. In what follows, the elds of the node pointed to by an , are referred to as an [es ], an [env ], an [x1 ] and an [x2 ].

Creating active disjunction nodes. The operation activate returns either a left-active or a right-active disjunction node when applied to a defer disjunction node which is active on the left or right argument. Let dn be a pointer to a defer disjunction node which is active on its left argument. Let new be the pointer to a new left-active disjunction node. The elds of the node pointed to by new are lled as follows: (

if dn [sf ] = S and dn [es ] = WRITE dn [es ] otherwise new [env ] = dn [env ] new [x1 ] = activate dn [x1 ] new [x2 ] = dn [x2 ] new [es ]

=

COPY

The descriptor of the active node is obtained from the references contained in the descriptor of the defer node dn . When activate is applied to a defer node which is active on the right argument then a call to activate is made on dn [x2 ] instead of dn [x1 ].

Evaluating an active disjunction node. The following transition rule describes the evaluation of an active disjunction node, which is pointed to by NR. Code NR SR H CER Exec &aOr nd st h ce =) Enter nd 0 nd st h0 ce 0

122

The Abstract Machine

where

h

=

"

nd 7! aOr es env x1 x2 aOr 7! h =4; &aOr; eval i

h

#

i

= h nd 0 7! eval x1 x2 8 > if es = READ or env = ce < ce 0 if es = WRITE ce = > merge (env ; ce ) : merge (copy (env ); ce ) if es = COPY A new node representing a call to an evaluation function with both arguments of the disjunction is created and entered next. The pointer eval points either to evalOrL or evalOrR depending on whether getExpId (nd ) returns a(||)L or a(||)R.

h0

5.4 General Control-related Built-in Functions 5.4.1 Evaluation This section describes how arguments of compiled strict functions are evaluated on the machine level. Set-processing functions will not be considered here. Section 5.6.2 describes how the arguments of strict set-processing functions are evaluated. The evaluation of the arguments of built-in system functions is specialised for each particular system function. The specialised evaluation functions are introduced together with the respective system function in separate sections. In Section 3.2 it was explained that the pattern compilation also includes an unfolding step. The result of compiling a function, which is strict in a number of arguments, is a sequence of functions, each of which is strict in the rst argument only. To force the evaluation of this argument, calls to members of the built-in evaluation family free are inserted during the compilation. The functionality of the free functions has been described brie y in Section 3.2.1.1.

5.4.1.1 Before the Evaluation of an Argument Before an argument is evaluated, the function pointer f of the current node (also known as call node) points to the evaluation built-in function free which contains the code &free. The references of this function node are a pointer to the continuation free0 , which is used as a stack frame descriptor, and a pointer to the function f 0 , which is used as the continuation function for the call node, when the argument evaluation is nished. The function node representing the evaluation function is shown below:

f 7! hfree=n; &free; free 0 f 0 i

5.4 General Control-related Built-in Functions

123

The following transition rule formalises what is done before an argument of a strict function gets evaluated. Code NR SR H VF EQVR UQVR CER Exec &free nd st h vf eqv uqv ce =) Enter x1 nd st 0 h0 OFF [] [] ce where 2 3 nd 7! f x1 xn,1 6 7 6 f ! 7 h free=n; &free; free 0 f 0 i 7 6 7 h = 6 7 4 ce 7! Env es bdl 5 "

:::

#

st 0 7! free0 es vf eqv uqv ce st f 0 nd h ce 7! Env READ bdl A new stack frame is built in which the environment status es, the contents of VF, EQVR, UQVR, CER and SR are saved, and also pointers to the function continuation f 0 and to the call node nd. This frame is put on top of the stack. For the evaluation of the argument, the variable ag is OFF, the EQVR and UQVR are empty and the environment status is set to READ. The argument x1 of the current node is entered next. The stack frame is itself an expression node. The rst word points to a stack frame descriptor which is a function node. It contains layout information of stack frames which have eight arguments. The code of the stack frame descriptor, here &free0, is called to continue the computation after the evaluation of the argument returns, this is why stack frame descriptors can be called continuation functions as well. In fact, what was described as the functionality of the evaluation function free in Section 3.2.1.1 is now split into two parts: rst setting up the evaluation of an argument which is done by free and later the continuation free0 is called, once the argument has been evaluated.

h0 =

5.4.1.2 After the Evaluation of an Argument An argument which is evaluated by the function free returns either when it is evaluated to a constructor, or it is deferred. In both cases, the instruction Return is used to return to the continuation built-in function on the stack, which is then evaluated. The function node representing the continuation of the function free on the heap is: free 0 7! hfree0 =8; &free0; dRef ind i

The references of an evaluation continuation contain a pointer to the function node for defer reference nodes and one to the indirection function ind, which is used when an evaluation result becomes shared. The continuation scrutinises the evaluation result and defers the call node if the argument returned deferred. Otherwise, it passes the result to the continuation of the function which demanded the evaluation.

124

The Abstract Machine

Sharing is built into the return mechanism of evaluation. The method used is described in Section 3.2.2. In the rst of the following transition rules it is shown how sharing, in the form of switching and refreshing, can be implemented. In the interest of clarity, and to help focus on di erent aspects of the machine, other transition rules (for continuations of evaluation built-in functions) will be simpli ed in the sense that they do not include sharing, but instead using copying as default. The strategy for sharing shown below can, however, easily be integrated into the other transition rules at the cost of increasing their complexity. When the continuation free0 is called, two cases can be distinguished. They will be discussed in the following.

A defer node returns. A node is a defer node if its identi er is either dL, dR, d(&&), d(==) or d(||). If an argument returns deferred, no more arguments are evaluated and the function which demanded the argument evaluation is deferred subsequently. Code NR SR H VF EQVR UQVR CER 0 Exec &free nd st h vf eqv uqv ce provided getExpId (nd ) 2 fdL; dR; d(&&); d(==); d(||)g =) Return nd 0 ps h0 vf 0 eqvst uqvst envst where 2 st 7! free 0 esst vfst eqvst uqvst envst ps f 0 cn 6 free 0 7! hfree0 =8; &free0 ; rs i 6 6 h = 66 envst 7! Env es bdl 6 4 cn 7! f x1 xn,1 "

:::

#

3 7 7 7 7 7 7 5

dn 7! f nd xn,1 h1 = h envst 7! Env esst bdl (nd 0 ; h 0 ) = makeDeferRefNode (dRef ; dn ; 1; h1 ) vf 0 = setVF The operation makeDeferRefNode is described in detail in Section 5.3.2.3. It creates a defer reference node on the heap and returns a tuple consisting of a pointer to the new defer node and the modi ed heap. The defer reference node contains the parts of the machine state that are relevant to restore this state at a later stage in the computation. The deferred call dn is a copy of the original call node updated at position 1 with a pointer to the deferred argument. The computation returns to the next continuation on the stack.

5.4 General Control-related Built-in Functions

125

A constructor returns. In this case, the evaluation continues with the continuation of the function that demanded the argument evaluation. To demonstrate how sharing is incorporated it is assumed, here, that the original argument is at least a 2-word node, and that the evaluation result di ers from the original argument. The operation refresh was described in Section 5.1.4.3. Code NR SR H VF EQVR UQVR CER Exec &free0 nd st h vf eqv uqv ce provided getExpId (nd ) 2= fdL; dR; d(&&); d(==); d(||)g =) Enter cn 0 nd ps h0 vf 0 eqvst uqvst envst where 2 st 7! free0 esst vfst eqvst uqvst envst ps f 0 cn 6 6 envst 7! Env es bdl 6 h = 66 cn 7! f x1 xn,1 6 4 x1 7! g zs

cn 0 vf 0

:::

7 7 7 7 7 7 5

vf = ENV and envst = ce and refresh (x1 ; nd ; bdl ) ifgetExpId (x1 ) = var : bdl otherwise 8 2 0 nd xn,1 3 > cn ! 7 f > > > 7 if vf = OFF > 7! ind nd h6 x > 5 > < 4 1 es env ! 7 Env bdl st st = > " # > > 0 nd xn,1 > cp ! 7 f > > otherwise > : h envst 7! Env esst bdl 0 ( cn if vf = OFF = cp otherwise = setVF

bdl 0 =

h0

8
n > 1. Calls to members of the partial application family are inserted by the compiler during arity transformation which is described in Section 3.2.4. The function papnofk takes a function, say f , of arity k and its rst n arguments and returns a function of arity m = k , n which expects the remaining m arguments. When these are supplied, the code &pap creates a

126

The Abstract Machine

node representing the application of f to all arguments in the following way: Code NR SR Exec &pap nd st =) Enter nd 0 nd st where 2 nd 7! 6 6 h = 6 pap 7! 6 4

f

H

hh h nd 0 7! f x1 : : : xn y1 : : : ym pap y1 : : : ym

i

3

hpapnofk=m; &pap; f x1 : : : xn i 777 7 7! hid=k; cd; rs i 5

:::

The node representing the call to f is entered next.

5.4.3 Application Calls to the apply family of built-in functions are added during compilation, for example when a function, say f, which was passed as an argument to another function, is applied to an argument, say x. Such an application is represented in an EKL program as apply2 f x. The functionality of the apply functions is described in Section 3.2.1.4. The compiler uses a family of apply functions, each member of this family is used for a di erent arity of the application node. An application node takes a function and a number of arguments. It has the form: apply f xn

where apply points to the following function node: apply 7! happly=(n + 1); &apply; rs i

The references rs of an application function node contain the function constructor which is used to build partial applications and new apply nodes. When apply is called, its rst argument has already been evaluated to a function node. This is done through a call to one of the free evaluation built-in functions, which is added during the compilation of applications. In an application, there are four cases to consider. The function f can be applied to:

 the right number of arguments,  too few arguments,  too many arguments. In the presence of set processing, the function f can also be a set in set constructor representation. This is the fourth case of application. Section 5.6.1 describes how sets are represented on the machine level.

5.4 General Control-related Built-in Functions

127

The following rules specify the execution of &apply in each of the above cases. In the rst three transition rules it is assumed that the function, in the rst argument of the apply node, is not a set in set constructor representation.

5.4.3.1 Application to the Right Number of Arguments Code NR SR H Exec &apply nd st h provided getExpArity (nd ) , 1 = getExpArity (nd [1]) =) Enter nd 0 nd st h0 where 2 nd 7! apply f xn 6 6 h = 66 apply 7! happly=(n + 1); &apply; rs i 4 f 7! hid=n; cd; rsf i h

:::

h0 = h nd 0 7! f xn

3 7 7 7 7 5

i

An expression node is created in which the function in the rst argument of the apply node is applied to the arguments supplied to the application. This expression node is entered.

5.4.3.2 Application to Too Few Arguments Code NR SR H Exec &apply nd st h provided getExpArity (nd ) , 1 < getExpArity (nd [1]) =) Return nd 0 st h0 where 2 nd 7! apply f xn 6 6 h = 66 apply 7! happly=(n + 1); &apply; rs i 4 f 7! hid=k; cd; rsf i h

:::

3 7 7 7 7 5

h0 = h nd 0 7! hpapnofk=(k , n); &pap; f x n i

i

A partial application function node is created which expects the remaining arguments. This node is made the current node and the next instruction is a return.

128

The Abstract Machine

5.4.3.3 Application to Too Many Arguments Code NR SR H Exec &apply nd st h provided getExpArity (nd ) , 1 > getExpArity (nd [1]) =) Enter nd 0 nd st h0 where 2 nd 7! apply1 f xk yn 6 6 h = 66 apply1 7! happly=(k + n + 1); &apply; rs i 4 f 7! hid=k; cd; rsf i 2

:::

nd 0

7! 6 6 apply 7! h0 = h 66 4 apply 0 7! fun 7! 2 2

3 7 7 7 7 5

apply2 fun yn hfree=(n + 1); &free; free 0 apply20 i happly=(n + 1); &apply; rs i

f xk

3 7 7 7 7 5

A node representing the application of a new apply node to the remaining arguments is built on the heap and entered.

5.4.3.4 Application of a Set Code NR SR H VF CER Exec &apply nd st h vf ce provided getExpId (nd [1]) = Set =) Enter sb nd st h vf 0 ce 0 where 2 nd 7! apply sn x 6 6 h = 66 apply 7! happly=2; &apply; rs i 4 sn 7! Set sv sb senv (

:::

3 7 7 7 7 5

merge (senv ; ce ) if senv [es ] = WRITE merge (copy (senv ); ce ) otherwise (ce 0 ; ) = bindVar (sv ; x; ce 1 ) vf 0 = setVF The set environment is merged with the environment in CER, the result is put into CER. The set variable is bound in the merged environment to the argument in the application node. The set body is entered. Notice that the set variable is bound to the argument no matter whether it was bound or unbound before. Any evaluated equalities which occur in the set body and involve the set variable were deferred after evaluation. Through the new binding they are activated and will be re-evaluated when the set body is entered. Because ce 1

=

5.5 Escher System Built-in Functions

129

this rule binds a variable it is necessary to set the variable ag which will disallow sharing the result expression.

5.5 Escher System Built-in Functions 5.5.1 Existential Quanti cation In Section 3.2.3.2 it was explained how calls to the function exists are transformed into calls to the function sigma. The built-in function sigma is represented on the heap by the following info node: sigma 7! hsigma=1; σ var i

The references contain a pointer var which points to the info node that represents variables. The function sigma is applied to an m-ary function, say f , which represents a formula with n quanti ed variables, where 1  n  m. The number of existentially quanti ed variables n is the di erence between the actual arguments supplied to the function f and its arity. Typically, f is a partial application, which has been introduced by the compiler during arity transformation. When sigma is evaluated, it performs the following actions. 1. Create nodes for n new variables on the heap. 2. Add n new binding records for these variables to the front of the binding list in the environment currently pointed to by the CER. 3. Create a new environment record which has the same environment status as the current environment, but points to the binding list which results from step 2. 4. Add the new quanti ed variables to the front of the list pointed to by the EQVR and put a pointer to the amended list into EQVR. This way, subsequent existential quanti ers and existential quanti ers in conjunctions are joined; only one quanti er remains. This quanti er binds all existentially quanti ed variables. 5. Set the variable ag to ENV if VF=OFF. By doing so, copying is forced to be used, rather than sharing. The e ect is, if the call to sigma is shared, the computation, which shares it, does not share the evaluation result, and thus needs to call sigma again. This creates new quanti ed variables, and hence makes the variables physically di erent to those created before. 6. Create an expression node which applies the function f to the new variables; this represents the quanti ed formula. 7. Enter the node created in step 6.

130

The Abstract Machine

The functionality of sigma is summarised in the following transition rule. Code NR SR Exec &sigma nd st =) Enter nd 0 nd st where 2 nd 7! 6 sigma 7! 6 7! h = 666 f 4 ce 7!

:::

2

h0

x1 ! 7 6 ::: 6 = h 666 xn 7! 4

(

nd 0 ce 0

H VF EQVR CER h vf eqv ce h0 vf 0 [x1 ; : : : ; xn ]++eqv ce 0 3

sigma f

hsigma=1; σ var i 777 hid=n; cd; rs i 7 7 Env

es bdl

5 3

var var

7! f x : : : xn 7 Env es [(x ; x ; Ub); : : : ; (xn; xn ; Ub)]++bdl ! 1

1

7 7 7 7 7 5

1

if vf = OFF otherwise Notice that it is not necessary to copy the environment when a call to sigma is made. The new binding records for the quanti ed variables are simply added to the front of the binding list in the current environment. A new environment record is created; it points to the new binding list and has the same status as the record pointed to by the CER. If the environment status of the new environment is READ and writing becomes necessary, the environment is copied by the function which requires to write. This copies the new binding records as well. If the status is WRITE (only conjunctions can pass environments into the evaluation of their arguments without changing the status to READ) then the new records are writable from the beginning. Bindings for the new quanti ed variables can thus be propagated into a conjunction easily. Also, adding the new binding records to the front of the binding list and creating a new environment record is important. It ensures that nodes which store a pointer to the original environment (e.g. defer nodes) are not a ected by adding the new records. Hence the quanti ed variables only occur within the environment of the quanti ed formula. If this is a conjunction, the environment will later be propagated into all arguments of the conjunction. However, the old environment record is still pointing to the original front of the binding list. After evaluating sigma, the existential quanti er is represented by the state of EQVR. Thus, a node for which EQVR is not empty, is either directly preceded by an existential quanti er, or is an atom in a conjunction which is directly preceded by an existential quanti er. The rewrite rules de ned in the Escher Booleans module for existential quanti cation are supported by the code of other built-in system functions, like (==), (&&) and (||). vf 0 =

ENV

vf

5.5 Escher System Built-in Functions

131

5.5.2 Universal Quanti cation The quanti er forall implements restricted universal quanti cation. A brief motivation for supporting this kind of universal quanti cation in Escher is given in Section 2.1.3.1. Occurrences of the quanti er forall in an Escher program are translated by the compiler into applications of the built-in system function pi. The translation is outlined in Section 3.2.3.2. In Section 4.3 an informal description was given, stating how the rewrite rules for forall, which are contained in the Escher module Booleans, are supported by built-in system functions on the machine level. In this section, the operational details of the implementation of universal quanti cation are presented. EMA supports the functionality of forall by evaluating universally quanti ed formulas in several stages. First pi creates the variables and the quanti ed formula and passes these to an initial evaluation function. This function takes two arguments: a pointer to the list of quanti ed variables and one to the quanti ed formula. The rst evaluation stage is used to evaluate the quanti ed formula to an implication. Afterwards the quanti ed implication is split into its antecedent and consequent. These are then passed as second and third argument to another evaluation function; the rst argument is the list of quanti ed variables. This function evaluates only the antecedent. When the evaluation of the antecedent returns, the continuation function on the stack matches on the top-level function of the evaluation result. In the following sections, each stage of the evaluation of universal quanti cation is explained, together with the corresponding transition rules.

5.5.2.1 Creating the Variables and the Quanti ed Formula Section 3.2.3.2 contains the transformation, which translates calls to the function forall into calls to the function pi. The built-in function pi is represented on the heap by the following function node: pi 7! hpi=1; π var evalVInit i

The references in the function node contain a pointer to the info node for variables and a pointer to the evaluation function evalVInit. The function pi is applied to an m-ary function, say f , which represents a formula with n quanti ed variables, where 1  n  m. The number of universally quanti ed variables n is the di erence between the actual arguments supplied to the function and its arity. Typically, f is a partial application, which has been introduced by the compiler during arity transformation. When pi is evaluated, it performs the following actions. 1. Create nodes for n new variables on the heap. 2. Create a list of the quanti ed variables. 3. Create an expression node which applies the function f to the new variables; this represents the quanti ed formula.

132

The Abstract Machine 4. Create a node representing a call to the evaluation function evalVInit with a pointer to the list of quanti ed variables from step 2. as rst argument and a pointer the quanti ed formula created in step 3. as second argument. 5. Set the variable ag to ENV if VF=OFF. By doing so, copying is used rather than sharing. The e ect is that, if the call to pi is shared, the computation which shares it, does not share the evaluation result, and thus needs to call pi again. This creates new quanti ed variables, and hence makes the variables physically di erent to those created before. 6. Enter the node created in step 4.

The evaluation function evalVInit is a variant of the built-in function evalV which sets up the environment for the quanti ed formula before evaluating it. Both functions are described in Section 5.5.2.2. The functionality of pi is summarised in the following transition rule. Code NR SR Exec &pi nd st =) Enter nd 0 nd st where 2 nd 7! h = 64 pi 7!

H VF h vf h0 vf 0

hpi=1; π rs i 75

::: uqv = [x1 ; : : : ; xn ] 2 x1 7! 6 ::: 6 h0 = h 666 xn 7! 4

(

nd 0 qf

3

pi f

var var evalVInit

7! uqv qf 7 f x : : : xn !

3 7 7 7 7 7 5

1

if vf = OFF vf otherwise Notice that pi does not put records for the new variables into the environment. This is done at a later stage by evalVInit. Also, the UQVR remains unchanged. vf 0

=

ENV

5.5.2.2 Evaluating the Quanti ed Formula to an Implication In the Escher Booleans module the rewrite rules for forall only match when the quanti ed formula is an implication. Hence, in the rst evaluation stage the quanti ed formula is evaluated with the aim to obtain an implication as top-level function. Implication is de ned as a constructor; its info node is:

h(==>)=2; &reti

5.5 Escher System Built-in Functions

133

The functions evalVInit and evalV arrange for the quanti ed formula to be evaluated. They are represented on the heap by the following two info nodes: evalVInit 7! hevalVInit=2; &evalVInit; evalV 0 evalVAnt evalV i evalV 7! hevalV=2; &evalV; evalV 0 evalVAnt i

The references in the above function nodes point to the stack frame descriptor evalV0, the evaluation function evalVAnt which is used to evaluate the antecedent of a universally quanti ed implication. The function evalVInit has an additional reference to the function evalV. The functions evalVInit and evalV work similar to the evaluation function free, but evaluate the second argument instead of the rst. In addition, evalVInit sets up an environment for the universally quanti ed variables before evaluation starts. The extended environment is thus only accessible in the scope of the quanti er (i.e. within the quanti ed formula). Once the environment is set up, the call node is copied and the function pointer of the copy is updated with a pointer to evalV. Here is the transition rule for evalVInit. Code NR SR H VF EQVR UQVR CER Exec &evalVInit nd st h vf eqv uqv ce =) Enter qf nd st 0 h0 OFF [] [] ce 0 where 3 2 nd 7! evalVInit uqv qf 6 evalVInit 7! hevalVInit=2; &evalVInit; rs i 7 7 h = 664 7 5 ce 7! Env es bdl 2

:::

3 st 0 7! evalV0 es vf eqv uqv ce st evalVAnt cp 7 h0 = h 64 cp 7! evalV uqv qf 5 ce 0 7! Env WRITE [(x1 ; x1 ; ub); : : : ; (xn ; xn ; ub)]++copy (bdl ) The execution of the code &evalV is similar to the execution of &evalVInit, except that the environment status is simply set to READ without modifying the environment record or binding list.

Code NR SR H VF EQVR UQVR CER Exec &evalV nd st h vf eqv uqv ce =) Enter qf nd st 0 h0 OFF [] [] ce

134

The Abstract Machine

where

2

h = h0 =

6 6 6 4

nd 7 evalV uqv qf ! evalV ! 7 hevalV=2; &evalV; rs i ce 7 Env es bdl ! "

:::

3 7 7 7 5

st 0 ! 7 evalV 0 es vf eqv uqv ce st evalVAnt nd h ce ! 7 Env READ bdl

#

5.5.2.3 Returning from the First Evaluation Stage When the evaluation of the quanti ed formula returns, the code of the continuation function evalV0, which is the descriptor of the top stack frame, is executed. The pointer evalV 0 points to the following function node on the heap: evalV 0 7! hevalV0=8; &evalV0; evalVAnt 0 evalV dRef i

The references in the above function node are pointers to the stack frame descriptor evalVAnt0, the evaluation function evalV and dRef points to the descriptor for defer reference nodes. The function evalV0 is specialised towards the evaluation of universal quanti cation. If the quanti ed formula evaluates to an implication, it builds a node representing a call to the evaluation function evalVAnt with the list of quanti ed variables, the antecedent and consequent of the returned implication as arguments. The pointer to evalVAnt is contained in the stack frame at the usual position for function continuations. A pointer to the list of quanti ed variables is put into UQVR. The antecedent is entered next. The stack frame stays the same, except for the descriptor, which is updated to evalVAnt0, and the pointer to the call node, which updated to point to the newly created call to evalVAnt. Note that the antecedent is evaluated without calling evalVAnt. Evaluation simply continues after setting up the UQVR with entering the antecedent. The following transition rule shows the execution of &evalV0 when an implication is returned. In order to focus on the evaluation of the quanti er, the following rule is simpli ed; it does not use sharing. Sharing can be incorporated in the same way as shown in Section 5.4.1 for the function free. Notice, however, that variable bindings cannot be refreshed, because substitutions, found in the quanti ed formula, can only be applied within the scope of the quanti er. This is ensured by the function evalVInit which creates a new environment record for the environment of the quanti ed formula. As a consequence, the CER and the environment pointer held on the stack frame always point to di erent environment records, and thus one of the current preconditions for the application of refreshing cannot be satis ed. The pointer to the evaluation function evalVAnt is abbreviated to eA in the following rule. Code NR SR H VF EQVR UQVR CER 0 Exec &evalV nd st h vf [] [] ce =) Enter ant nd st h0 OFF [] uqv ce

5.5 Escher System Built-in Functions where

2

h

=

nd st evalV 0 cn

6 6 6 6 6 6 4

"

135

7! (==>) ant con 7 evalV 0 esst vfst eqvst uqvst envst ps eA cn ! 7 hevalV0=8; &evalV0; rs i ! 7 evalV uqv qf !

:::

3 7 7 7 7 7 7 5 #

0 7! evalVAnt uqv ant con = h cn st 7! evalVAnt 0 esst vf 0 eqvst uqvst envst ps eA cn 0 vf 0 = setVF If the quanti ed formula returns deferred, the call node (which is a evalV node) gets deferred in the same way as described for the continuation of the function free in Section 5.4.1. For this reason, the rule for the defer case is not given here. It is an error if the quanti ed formula does not return either with an implication as toplevel function or as a deferred computation. Notice that an implication for which EQVR is not empty represents an existentially quanti ed implication, and hence does not match the rule given above, but is an error case.

h0

5.5.2.4 Evaluating the Antecedent of the Quanti ed Implication The evaluation function evalVAnt is used to evaluate the antecedent of a universally quanti ed formula. Its info node on the heap is:

hevalVAnt=3; &evalVAnt; evalVAnt 0 evalVAnt i The references point to the stack frame descriptor evalVAnt0 and the continuation function evalVAnt which is a self-reference. The evaluation of &evalVAnt is shown in the next transition rule. Code NR SR H VF EQVR UQVR CER Exec &evalVAnt nd st h vf eqv uqv ce =) Enter ant nd st 0 h0 OFF [] qv ce 0 where 2 3 nd 7! evalVAnt qv ant con 6 evalVAnt 7! hevalVAnt=3; &evalVAnt; rs i 7 7 h = 664 7 5 ce 7! Env es bdl "

:::

#

st 0 7! evalVAnt 0 es vf eqv uqv ce st evalVAnt nd h0 = h 0 ce 7! Env READ bdl When the quanti ed formula evaluates to an implication, a new call to evalVAnt is created and the evaluation of the antecedent is immediately invoked. In fact, the call to evalVAnt is skipped, going straight into the evaluation of the antecedent without executing &evalVAnt.

136

The Abstract Machine

The new call node is kept on the stack. The old stack frame is re-used; see the previous Section 5.5.2.3 for details. If the antecedent returns deferred, the call node, which is a evalVAnt node, is subsequently deferred with a defer reference. When this node is activated, the antecedent is re-evaluated through the execution of the code &evalVAnt. In contrast to the evaluation built-in function free, the function evalVAnt does not have a function continuation. Instead, the matching is done by the specialised stack descriptor evalVAnt0. For uniformity, the eld for the continuation function is therefore lled with a self-reference.

5.5.2.5 Matching on the Evaluated Antecedent The antecedent is evaluated until either a constructor returns, the top-level function is deferred or the de ned function (||) is on top level and returned through a look-above . A return invokes the function evalVAnt0 which is on the stack. It has the following function node on the heap: evalVAnt 0 7! hevalVAnt0=8; &evalVAnt0; true and var evalVEnv dRef i

The function evalVAnt0 deals with the di erent evaluation results; these are discussed in the remainder of this section.

The antecedent evaluates to a constructor. Because the antecedent is a boolean formula, it can evaluate to either of the boolean constructors True or False. If True is returned and the UQVR is not empty, none of the Escher rules for forall matches. Hence, a match error has occurred and the computation is stopped. On the machine level, no transition rule matches and the machine halts. If True is returned and the UQVR is empty, then all universally quanti ed variables have been bound and the universally quanti ed implication can be rewritten to its consequent. The environment of the antecedent is left in the CER; this propagates the bindings for the universally quanti ed variables into the consequent. The following transition rule demonstrates the actions performed by &evalVAnt0 when the antecedent reduces to True and all universally quanti ed variables have been bound. Code NR SR H VF EQVR UQVR CER Exec &evalVAnt0 nd st h vf [] [] ce provided getExpId (nd) = True =) Enter con nd ps h vf 0 eqvst uqvst ce

5.5 Escher System Built-in Functions where

2

h

=

6 4

137

st 7! evalVAnt0 esst vfst eqvst uqvst envst ps evalVAnt cn cn 7! evalVAnt uqv ant con

:::

3 7 5

vf 0 = setVF If False is returned, then the whole universal quanti cation evaluated to True according to the following Escher statement: forall \x1 ... xn -> False ==> u

=

True

The next transition rule shows what happens on the machine level when False is returned. Here, again, sharing is not incorporated. The pointer to the evaluation function evalVAnt is abbreviated to eA in the following rule. Code NR SR H VF EQVR UQVR Exec &evalVAnt0 nd st h vf eqv uqv provided getExpId (nd) = False =) Enter true nd ps h0 vf 0 eqvst uqvst where 2 st 7! evalVAnt 0 esst vfst eqvst uqvst 6 0 0 0 6 h = 66 evalVAnt 7! hevalVAnt =8; &evalVAnt ; rs i 4 envst 7! Env es bdl

h0

h

= h envst 7! vf 0 = setVF

:::

Env

esst bdl

CER ce envst envst ps eA cn

3 7 7 7 7 5

i

The antecedent returns deferred. If the antecedent evaluated to a defer node, the call node, which is a evalVAnt node, is deferred with a defer reference node in the same way as the function free (or more precisely its continuation free0) defers function calls. The registers are restored from the stack, the new variable ag is determined and put into VF. The environment which is restored is the one that was valid before the evaluation of the quanti er. A transition rule is not given here. Notice the encapsulation of the environment which contains the quanti ed variables. It is stored in the deferred antecedent, so no bindings made in the quanti ed formula are propagated outside the scope of the quanti er.

The antecedent evaluates to a disjunction. In Section 4.1.3 this case was discussed in detail, especially that the variables in one of the new quanti cations created need to be renamed to obtain a consistent representation

138

The Abstract Machine

of the expression under evaluation on the heap. The next transition rule implements the case when the antecedent evaluates to a disjunction. To focus on the handling of the quanti cation, sharing is left out of the transition. In the following rule, the pointer to the evaluation function evalVAnt is abbreviated to eVA, and the pointer to the continuation evalVAnt0 is abbreviated to eVA0 . Code NR SR H VF EQVR UQVR Exec &evalVAnt0 nd st h vf [] uqv provided getExpId (nd) = (||) =) Enter nd 0 nd ps h0 vf 0 eqvst uqvst where 2 nd 7! (||) x1 x2 6 6 st 7! eVA0 esst vfst eqvst uqvst envst 6 6 0 0 0 h = 666 eVA 7! hevalVAnt =8; &evalVAnt ; rs i 7! evalVAnt uqv ant con 6 cn 6 4 ce 7! Env es bdl

:::

= [v1 ; : : : ; vn ]

uqv

2

7! 6 env ! 7 6 6 6 y 7 ! 6 6 = h 6 env ! 7 6 0 6 v 7 ! 6 y1

2

1

7 7 7 7 7 7 7 7 5

var

:::

vn0

x

evalVEnv uqv 2 con env 2 Env WRITE copy (bdl )

2

4

3

7! var (y0 ; h ) = rename (y ; fv =v0 ; : : : ; vn =vi n0 g; h ) h h0 = h nd 0 7! (&&) y y0 2

vf 0

2

2

2

envst 3

ps eVA cn

7 7 7 7 7 7 7 7 7 5

evalVEnv uqv 1 con env 1 7 Env WRITE copy (bdl ) 7

1

h1

x

CER ce

1

1

1

1

2

= setVF The function evalVEnv is a variant of the function evalVAnt which merges the environment in its fourth argument with the environment in the CER before evaluating the antecedent. This ensures that the bindings in the environment of the antecedent are still available when the evaluation of the quanti er is continued. The function evalVEnv is described in more detail in Section 5.5.2.6. The operation rename takes a pointer p to a node in the heap which represents an expression, a substitution  = fv1 =v10 ; : : : ; vn =vn0 g where each vi , vi0 is a pointer to a variable on the heap (1  i  n) and a heap. It copies the whole expression starting with p and replaces every occurrence of vi in the copy with vi0 . It returns a pointer to the renamed expression together with the modi ed heap. A suitable algorithm to implement renaming and copying is the copying garbage collector algorithm described in [FH88]. The operation rename is also used when an existential quanti er is distributed through disjunction (see Section 5.5.5.1).

5.5 Escher System Built-in Functions

139

5.5.2.6 Re-evaluating the Antecedent after a Distribution The function evalVEnv is a variant of the evaluation function evalVAnt which is described in Section 5.5.2.4. The info node for evalVEnv on the heap is: evalVEnv 7! hevalVEnv=4; &evalVEnv; evalVAnt 0 evalVAnt i

The references point to the stack frame descriptor evalVAnt0 and the continuation function evalVAnt. Calls to the evaluation function evalVEnv are created when the antecedent of a universally quanti ed implication evaluated to a disjunction. Compared to calls to evalVAnt, a call to evalVEnv has one more argument which is a pointer to an environment. The environment in the CER is merged with the environment in the call node before the antecedent is re-evaluated. The evaluation of &evalVEnv is shown in the next transition rule. Code NR SR H VF EQVR UQVR CER Exec &evalVEnv nd st h vf eqv uqv ce =) Enter ant nd st 0 h0 OFF [] qv ce 0 where 2 3 nd ! 7 evalVEnv qv ant con env h = 64 evalVEnv 7! hevalVEnv=4; &evalVEnv; rs i 75 "

:::

st 0 7! evalVAnt 0 es vf eqv uqv ce st evalVAnt nd 0 h0 = h nd 0 7! evalVAnt qv ant con ce 0 = merge (env ; ce )

#

5.5.3 Equality In Section 4.4, the functionality of the built-in system function (==) has been discussed. It was explained there, that equalities are evaluated in several stages at the machine level. In addition, the de nition of directly (existentially or universally) quanti ed variable was introduced. This section gives the operational details of how equalities are evaluated by EMA.

5.5.3.1 Evaluating an Equality The rst stage of evaluating an equality is handled by the code &equal which is the code part of the following info node on the heap: equal 7! h(==)=2; &equal; true evalEqLi

The references contain a pointer to the constructor that evaluates the left argument of an equality.

True

and a pointer to the function

140

The Abstract Machine

Directly quanti ed variables are bound and the equality is rewritten to True. If none of the arguments is a directly quanti ed variable the evaluation of the left argument is caused.

Binding directly quanti ed variables without argument evaluation. The rst transition rule speci es how an existentially quanti ed variable is bound. Code NR SR H VF EQVR CER Exec &equal nd st h vf [v1 ; : : : ; vi ; : : : ; vn ] ce provided nd [j ] = vi , for some j 2 f1; 2g =) Enter true nd st h vf 0 [v1 ; : : : ; vi,1 ; vi+1 ; : : : ; vn ] ce 0 where 2 3 nd ! 7 equal x1 x2 h = 64 equal 7! h(==)=2; &equal; rs i 75

::: (ce 0 ; ) = bindVar (vi ; xk ; ce ) where k = if j = 1 then 2 else 1 (

vf 0

=

ENV

vf

if vf = OFF otherwise

The operation bindVar (which was introduced in Section 5.1.4) is used to bind the variable vi in the current environment. The transition rule for binding a universally quanti ed variable is the same as the rule above, except that the variable is in UQVR instead of EQVR, and therefore UQVR needs to be changed accordingly.

Forcing the evaluation of the left argument of an equality. If neither of the arguments are unbound directly quanti ed variables, the evaluation of the left argument is forced by creating a call with descriptor evalEqL and entering it. Code NR SR H EQVR UQVR Exec &equal nd st h eqv uqv provided nd [i ] not in eqv and nd [i ] not in uqv , for i = 1; 2 =) Enter nd 0 nd st h0 eqv uqv where 2 3 nd ! 7 equal x1 x2 h = 64 equal 7! h(==)=2; &equal; rs i 75 h

:::

h0 = h nd 0 7! evalEqL x1 x2

i

5.5 Escher System Built-in Functions

141

Evaluation of the left argument of an equality. The function evalEqL works exactly like free. It is represented on the heap by the function node: evalEqL 7! h(==)=2; &free; evalEqL0 equal i

Notice that the identi er in the function node is (==). This allows matching on the equality when it is the top-level function of a set body. A stack frame with descriptor evalEqL0 is put onto the stack and the reference equal , pointing to the equality function, is used as a dummy continuation function. The transition rule for executing &free is given in Section 5.4.1. The stack frame descriptor evalEqL0 has references to the equality continuations eqLC , eqLD and eqLVar . It is represented by the following function node on the heap: evalEqL0 7! hevalEqL0=8; &evalEqL0; eqLC eqLD eqLVar i

When the evaluation of the left argument returns, the code of the stack frame descriptor evalEqL0 is executed. It behaves almost like free0 , except that instead of using the function continuation from the stack, it uses the continuation eqLC when the left argument returned as a constructor, eqLVar when the left argument returned as a defer leaf or eqLD when the left argument returned as a di erent defer node. The call node is updated with the respective continuation and entered next. Because of the similarity with the execution of &free0 , the corresponding transition rule is not given here. The result of evaluating an argument of an equality can be either a constructor or a deferred computation; an unbound variable (directly quanti ed or not) returns as a defer leaf node. If the left argument evaluated to a defer leaf, the function eqLVar checks whether the variable in the defer leaf is a directly quanti ed variable. If so, it binds the variable to the right argument of the equality in the current environment and rewrites to True. Otherwise it updates the descriptor of the current node with a pointer to the function eqLV . Hence, at the time when eqLV is called, the variable in the defer leaf node is a not directly quanti ed variable. The function eqLVar is represented on the heap by the following node: eqLVar 7! heqLVar=2; &eqLVar; true eqLV i

The transition rules for the execution of &eqLVar are very similar to some of the rules given for &equal, and are therefore not given here.

Forcing the evaluation of the right argument of an equality. The functions eqLC , eqLD and eqLV force the evaluation of the right argument of an equality using the code &evalEqR. They are de ned as follows:

142

The Abstract Machine eqLC 7! h(==)=2; &evalEqR; evalEqR 0 eqLC 0 i eqLD 7! h(==)=2; &evalEqR; evalEqR 0 eqLD 0i eqLV 7! h(==)=2; &evalEqR; evalEqR 0 eqLV 0 i

The references contain a pointer to the stack frame descriptor evalEqR0 and the respective continuation function. Notice that the identi er of all three evaluation functions is (==). This allows matching on the equality when it is the top-level function of a set body. The transition rule for executing &evalEqR is given below. To avoid repeating the same rule for each of the evaluation functions, eqL? is used instead of eqLC , eqLD or eqLV . Code NR SR H VF EQVR UQVR CER Exec &evalEqR nd st h vf eqv uqv ce 0 0 =) Enter x2 nd st h OFF [] [] ce where 3 2 nd 7! eval x1 x2 6 eval 7! h(==)=2; &evalEqR; rs i 7 7 h = 664 7 5 ce 7! Env es bdl

h0 =

"

st 0

:::

7 evalEqR0 es vf eqv uqv ce st eqL?0 nd ! h ce 7! Env READ bdl

#

Returning from the evaluation of the right argument of an equality. When the right argument returns, the function evalEqR0 is called. It is the descriptor for the top stack frame which is represented on the heap by the following node: evalEqR 0 7! hevalEqR0=8; &evalEqR0; rs i

The references rs are empty. The transition rule for executing &evalEqR0 is shown below. Code NR SR H VF EQVR UQVR Exec &evalEqR0 nd st h vf eqv uqv =) Enter cn nd ps h0 vf 0 eqvst uqvst where 2 st 7! evalEqR0 esst vfst eqvst uqvst 6 6 h = 66 envst 7! Env es bdl 7! evalEqR x1 x2 4 cn "

:::

#

CER ce envst envst ps eqL?0 cn

3 7 7 7 7 5

7! eqL?0 x1 nd = h cn envst 7! Env esst bdl vf 0 = setVF The evaluation continues with eqLC0, eqLD0 or eqLV0. These three functions are described h0

5.5 Escher System Built-in Functions

143

in the following sections.

5.5.3.2 Continuation eqLC0 The function continuation eqLC 0 is represented by the following node on the heap: eqLC 0 7! heqLC0 =2; &eqLC0; true false equal and eqLC dRef i

The references are pointers to the constructors True and False, the built-in system functions (==) and (&&), the evaluation function eqLC and the descriptor for defer reference nodes. Both arguments of the equality have now been evaluated. The left argument evaluated to a constructor. Depending on the evaluation result of the right argument, three cases can be distinguished: the right argument can return either as a constructor, a defer leaf node, or another deferred computation. In the rst case both constructors are uni ed. A returned defer leaf node represents an unbound variable. This variable is bound to the constructor in the left argument of the equality. The equality is then either rewritten to True or deferred, depending on whether the defer leaf node encapsulates a directly quanti ed variable or a variable which is not directly quanti ed. Remember that bindings for variables which are not directly quanti ed remain in the expression (see Section 4.5). If the right argument returns as another defer node (i.e. di erent from a defer leaf node) then the equality is encapsulated in a defer reference node. All three cases are described in detail in the following.

The right argument evaluated to a constructor. Both arguments are uni ed by comparing the constructor identi ers and arity. The corresponding transition rules are given below. If the constructors di er, False is entered.

144

The Abstract Machine

Code NR SR H Exec &eqLC0 nd st h provided getExpId (nd [2]) 2= fdL; dR; d(&&); d(==); d(||)g and getExpId (nd [1]) 6= getExpId (nd [2]) =) Enter false nd st h If the constructors in both arguments are equal 0-ary constructors then True is entered. Code NR SR H Exec &eqLC0 nd st h provided getExpId (nd [2]) 2= fdL; dR; d(&&); d(==); d(||)g and getExpId (nd [1]) = getExpId (nd [2]) and getExpArity (nd [i ]) = 0 =) Enter true nd st h If the constructors in both arguments are equal n-ary constructors for n > 0 then a conjunction is built consisting of one equality for each pair of arguments. The conjunction is entered next. If n = 1 the conjunction reduces to a single equality which is entered instead. Code NR SR H Exec &eqLC0 nd st h provided getExpId (nd [2]) 2= fdL; dR; d(&&); d(==); d(||)g and getExpId (nd [1]) = getExpId (nd [2]) and getExpArity (nd [1]) > 0 =) Enter nd 0 nd st h0 where 2 3 nd 7! eqLC 0 x1 x2 6 eqLC 0 7! heqLC0 =2; &eqLC0 ; rs i 7 6 7 7 h = 666 x1 7! c ys 7 7 4 x2 5 7! c zs

:::

(nd 0 ; h0 ) = makeConjunction (ys ; zs ; equal ; and ) The operation makeConjunction creates a conjunction of the form y1 = z1 ^ : : :^ yn = zn on the heap using equal and and which are pointers to the built-in system functions (==) and (&&). It returns the amended heap and a pointer to the top-level node in the conjunction.

The right argument returned as a defer leaf. The variable in the defer leaf is bound to the constructor (left argument) in the current environment. If the variable is a directly quanti ed variable the equality is rewritten to True. Otherwise a defer reference node is built and the equality is deferred with a reference to the defer leaf in the right argument. The corresponding transition rules are given below.

5.5 Escher System Built-in Functions

145

The case when the variable in the defer leaf is existentially quanti ed is shown rst. Code NR SR H EQVR CER Exec &eqLC0 nd st h [v1 ; : : : ; vi ; : : : ; vn ] ce provided getExpId (nd [2]) = dL and getVar (nd [2]) = vi =) Enter true nd st h [v1 ; : : : ; vi,1 ; vi+1 ; : : : ; vn ] ce 0 where 2 3 nd ! 7 eqLC 0 dc dn h = 64 eqLC 0 7! heqLC0=2; &eqLC0; rs i 75

::: (ce 0 ; ) = bindVar (vi ; dc ; ce )

The transition rule which handles the case when the variable in the defer leaf is universally quanti ed is not shown here. It is the same as the previous transition rule, except that the variable is in UQVR instead of EQVR, and hence the UQVR needs to be changed accordingly. If the variable in the defer leaf is not directly quanti ed it is bound to the constructor in the left argument of the equality. The function node is changed to the evaluation function eqLC (ready for re-evaluation) and the equality is deferred on the bound variable. Code NR SR H EQVR UQVR CER 0 Exec &eqLC nd st h eqv uqv ce provided getExpId (nd [2]) = dL and getVar (nd [2]) not in eqv and getVar (nd [2]) not in uqv =) Return nd 0 st h0 eqv uqv ce 0 where 2 3 nd ! 7 eqLC 0 dc dn h = 64 eqLC 0 7! heqLC0 =2; &eqLC0; rs i 75 h

:::

i

= h nd 7! eqLC dc dn (nd 0 ; h0 ) = makeDeferRefNode (dRef ; nd ; 2; h1 ) (ce 0 ; stp ) = bindVar (dn [v ]; dc ; ce ) dn [s ] = stp It is important to update the binding stamp in the defer leaf node which encapsulates the bound variable with the stamp that is used in the environment record. This ensures that both stamps are identical at the time the binding is made. The equality is only re-evaluated when the stamp in the environment is di erent from the stamp in the defer leaf node. The stamp in the environment in which the deferred equality is evaluated, is propagated into the conjunction containing the deferred equality. However, outside the conjunction the binding and stamp are not known. Should the same variable be bound outside, a new binding stamp will be used; it is unique and therefore di erent from any

h1

146

The Abstract Machine

binding stamps used before. This causes the activation check to succeed for deferred bindings. The new binding (and stamp) are propagated into the activated computations through an environment merge.

The right argument returned as a defer node but not a defer leaf. In this case the equality is deferred with a reference to the right argument. Code NR SR H Exec &eqLC0 nd st h provided getExpId (nd [2]) 2 fdR; d(&&); d(==); d(||)g =) Enter nd 0 2 nd st h0 nd 7! eqLC 0 dc dn 6 eqLC 0 7! heqLC0 =2; &eqLC0 ; rs i where h = 4 h

:::

3 7 5

i

= h nd 7! eqLC dc dn (nd 0 ; h0 ) = makeDeferRefNode (dRef ; nd ; 2; h1 )

h1

5.5.3.3 Continuation eqLD0 The function continuation eqLD 0 is represented by the following node on the heap: eqLD 0 7! heqLD0 =2; &eqLD0; true eqLC evalEqL dRef dEq i

The references are pointers to the constructor True, the evaluation functions eqLC and evalEqL and the descriptors for defer reference and defer equality nodes. Both arguments of the equality have now been evaluated. The left argument returned as a deferred computation which is not a defer leaf node. Depending on the evaluation result of the right argument, three cases can be distinguished: the right argument can return either as a constructor, a defer leaf node, or another deferred computation. In the rst case the equality is encapsulated in a defer reference node. A returned defer leaf node represents an unbound variable. If this variable is directly quanti ed, the variable is bound to the deferred computation in the left argument and the equality is rewritten to True. If the variable is not directly quanti ed, the equality is deferred with a defer reference on the deferred left argument. The variable is left unbound because the deferred computation might later evaluate to a directly quanti ed variable, in which case binding the directly quanti ed variable is preferred. If the right argument returns as another defer node (i.e. di erent from a defer leaf node) then the equality is encapsulated in a binary defer equality node. All three cases are described in detail in the following.

5.5 Escher System Built-in Functions

147

The right argument evaluated to a constructor. In this case, the function pointer of the equality node (in NR) is updated with a pointer to the evaluation function eqLC and swap the arguments, so that the constructor is the left argument. The updated equality is deferred with a reference to the defer node in the right argument. Code NR SR H Exec &eqLD0 nd st h provided getExpId (nd [2]) 2= fdL; dR; d(&&); d(==); d(||)g =) Enter nd 0 nd st h0 where 2 3 nd ! 7 eqLD 0 dn dc h = 64 eqLD 0 7! heqLD0=2; &eqLD0; rs i 75 h

:::

i

= h nd 7! eqLC dc dn (nd 0 ; h0 ) = makeDeferRefNode (dRef ; nd ; 2; h1 )

h1

The right argument returns as a defer leaf. If the variable in the defer node is a directly quanti ed variable it is bound to the deferred computation (left argument) in the current environment. The equality is rewritten to True. Otherwise the variable remains unbound and the equality is deferred with a reference to the deferred computation in the left argument. The corresponding transition rules are given below. The case when the variable in the defer leaf is existentially quanti ed is shown rst. Code NR SR H EQVR CER Exec &eqLD0 nd st h [v1 ; : : : ; vi ; : : : ; vn ] ce provided getExpId (nd [2]) = dL and getVar (nd [2]) = vi =) Enter true nd st h [v1 ; : : : ; vi,1 ; vi+1 ; : : : ; vn ] ce 0 where 2 3 nd ! 7 eqLD 0 dn 1 dn 2 h = 64 eqLD 0 7! heqLD0 =2; &eqLD0; rs i 75

::: (ce 0 ; ) = bindVar (vi ; dn 1 ; ce )

The case when the variable in the defer leaf is universally quanti ed is not shown here. The transition rule is the same as the previous transition rule, except that the variable is in UQVR instead of EQVR, and hence the UQVR needs to be changed accordingly. If the variable in the defer leaf is not directly quanti ed it is not bound and the equality

148

The Abstract Machine

is deferred with a reference to the deferred computation in the left argument. Code NR SR H EQVR UQVR CER Exec &eqLD0 nd st h eqv uqv ce provided getExpId (nd [2]) = dL and getVar (nd [2]) not in eqv and getVar (nd [2]) not in uqv =) Return nd 0 st h0 eqv uqv ce 0 where 2 3 nd ! 7 eqLD 0 dn 1 dn 2 h = 64 eqLD 0 7! heqLD0 =2; &eqLD0; rs i 75 h

:::

i

= h nd 7! evalEqL dn 1 dn 2 (nd 0 ; h0 ) = makeDeferRefNode (dRef ; nd ; 1; h1 )

h1

The right argument returns as a defer node but not a defer leaf. Both arguments of the equality are deferred. A defer equality node is created and the computation returns. Code NR SR H Exec &eqLD0 nd st h provided getExpId (nd [2]) 2 fdR; d(&&); d(==); d(||)g =) Return nd 0 st h0 where 2 nd 7! eqLD 0 dn 1 dn 2 6 0 h = 4 eqLD 7! heqLD0 =2; &eqLD0; rs i

:::

3 7 5

(nd 0 ; h0 ) = makeDeferEqNode (dEq ; dn 1 ; dn 2 ; h

5.5.3.4 Continuation eqLV0 The function continuation eqLV 0 is represented by the following node on the heap: eqLV 0 7! heqLV0 =2; &eqLV0; true eqLC evalEqL dRef i

The references are pointers to the constructor True, the evaluation functions eqLC , eqLV and evalEqL and the descriptor for defer reference nodes. Both arguments of the equality have now been evaluated. The left argument returned as a defer leaf node. The variable in the defer leaf is not directly quanti ed; directly quanti ed variables have been eliminated through eqLVar , which was called right after the evaluation of the left argument (see page 141). Depending on the evaluation result of

5.5 Escher System Built-in Functions

149

the right argument, three cases can be distinguished: the right argument can return either as a constructor, a defer leaf node, or another deferred computation. In the rst case the variable in the left argument of the equality is bound to the constructor and the equality is encapsulated in a defer reference node. A returned defer leaf node represents an unbound variable in the right argument of the equality. If this variable is directly quanti ed, it is bound to the variable in the left argument of the equality and the equality is rewritten to True. If this variable (i.e. the one in the right argument of the equality) is not a directly quanti ed variable, the variable in the left argument is bound to it. The equality is then deferred with a defer reference on the bound variable. If the right argument returns as another defer node (i.e. di erent from a defer leaf node) then the equality is encapsulated in a defer reference node, with reference to the deferred computation in the right argument. All three cases are described in detail in the following.

The right argument evaluated to a constructor. The variable in the defer leaf node in the left argument is bound to the constructor in the current environment. The function pointer of the equality node in NR is updated with a pointer to the evaluation function eqLC and the arguments are swapped, so that the constructor is the left argument. The modi ed equality node is deferred on the bound variable. Code NR SR H CER Exec &eqLV0 nd st h ce provided getExpId (nd [2]) 2= fdL; dR; d(&&); d(==); d(||)g =) Return nd 0 st h0 ce 0 where 2 nd 7! eqLV 0 dn dc 6 h = 4 eqLV 0 7! heqLV0 =2; &eqLV0; rs i h

:::

3 7 5

i

= h nd 7! eqLC dc dn (nd 0 ; h0 ) = makeDeferRefNode (dRef ; nd ; 2; h1 ) (ce 0 ; stp ) = bindVar (dn [v ]; dc ; ce ) dn [s ] = stp With the last transition rule in Section 5.5.3.2 an explanation was given, why the binding stamp in the defer leaf node, which represents the bound variable, needs to be updated with the stamp that is used in the environment record.

h1

The right argument returns as a defer leaf. Bind the variable in the defer leaf in the right argument in the current environment to the variable in the defer leaf in the left argument. If the bound variable is a directly quanti ed variable the equality is rewritten to True. Otherwise a defer reference node is

150

The Abstract Machine

built and the equality is deferred with a reference to the defer leaf in the right argument. The corresponding transition rules are given below. The case when the variable in the right defer leaf is existentially quanti ed is given rst. Code NR SR H EQVR CER Exec &eqLV0 nd st h [v1 ; : : : ; vi ; : : : ; vn ] ce provided getExpId (nd [2]) = dL and getVar (nd [2]) = vi =) Enter true2 nd st h [v1 ; : : : ; vi,1 ; vi+13; : : : ; vn ] ce 0 nd 7! eqLV 0 dl 1 dl 2 6 0 where h = 4 eqLV 7! heqLV0 =2; &eqLV0; rs i 75

::: (ce 0 ; ) = bindVar (vi ; dl 1 [v ]; ce )

The case when the variable in the right defer leaf is universally quanti ed is not shown. The corresponding transition rule is the same as the transition rule above, except that the variable is in UQVR instead of EQVR, and hence the UQVR needs to be changed accordingly. If the variable in the right defer leaf is not directly quanti ed it is bound to the variable in the left defer leaf (which is also a not directly quanti ed variable) of the equality. The equality is deferred on the bound variable, since it is only worth re-evaluating the equality, when a new binding is found \outside" (see Section 4.5) for the variable that was just bound. Code NR SR H EQVR UQVR CER Exec &eqLV0 nd st h eqv uqv ce provided getExpId (nd [2]) = dL and getVar (nd [2]) not in eqv and getVar (nd [2]) not in uqv 0 0 =) Return h0 eqv uqv ce 2 nd st 3 nd 7! eqLV 0 dl 1 dl 2 6 0 where h = 4 eqLV 7! heqLV0 =2; &eqLV0; rs i 75 h

:::

i

= h nd 7! evalEqL dl 1 dl 2 (nd 0 ; h0 ) = makeDeferRefNode (dRef ; nd ; 1; h1 ) (ce 0 ; stp ) = bindVar (dl 1 [v ]; dl 2 [v ]; ce ) dl 2 [s ] = stp With the last transition rule given in Section 5.5.3.2 it was explained, why the binding stamp in the defer leaf node, which represents the bound variable, needs to be updated with the stamp that is used in the environment record.

h1

5.5 Escher System Built-in Functions

151

The right argument returns as a deferred computation. The right argument can return as a deferred computation, which is not a defer leaf node. The corresponding transition rule is given next. Code NR SR H CER Exec &eqLV0 nd st h ce provided getExpId (nd [2]) 2 fdR; d(&&); d(==); d(||)g =) Return nd 0 st h0 ce where 2 nd 7! eqLV 0 dl dn 6 0 h = 4 eqLV 7! heqLV0 =2; &eqLV0; rs i h

:::

3 7 5

i

= h nd 7! evalEqL dl dn (nd 0 ; h0 ) = makeDeferRefNode (dRef ; nd ; 2; h1 ) In this case, the variable in the defer leaf in the left argument remains unbound. The function pointer of the current node is updated to point to the evaluation function evalEqL (ready for re-evaluation). The updated equality is deferred with a reference to the deferred computation in the right argument.

h1

5.5.3.5 Summary on the Evaluation of Equalities The handling of equalities on the machine level has been described in the previous sections. The built-in functions which implement equality, do not just support the Escher rewrite rules for the function (==), but also the rewrite rules which bind variables in di erent contexts. The initial call to the function (==) is the entry point to a series of specialised built-in functions. Equalities are evaluated in di erent stages; the necessity for this has already been pointed out in Section 4.4. From the transition rules that were presented in this section, it can be seen that the arguments of equalities are only evaluated on demand. As soon as one argument can be identi ed as a directly quanti ed variable, no more evaluation takes place. As a side e ect of the need to scrutinise the arguments, purpose-built continuation functions have been introduced to enable faster processing. The three continuations eqLC0, eqLD0 and eqLV0 , together with the function eqLVar cover all possible cases of argument evaluation results. To give an overview, Table 5.3 summarises the possible outcome of an argument evaluation, and shows how these results are processed by the di erent continuation functions. In the table, the binding of a variable, say v, to a term, say t, is symbolised by v 7! t. If the equality is deferred after evaluation, the table shows the kind of defer node created, using the identi ers dR for a defer reference node and d(==) for a defer equality node. In the case of a defer reference node, the argument xi of the equality on which the defer reference is made is indicated as dR on xi . From Table 5.3 it can also be seen that some results of argument evaluations are treated

152

The Abstract Machine left argument (x1 ) constructor directly quanti ed variable not directly quanti ed variable deferred computation

right argument (x2 ) variable directly not directly deferred built-in constructor quanti ed quanti ed computation function unify True dR on x2 dR on x2 eqLC0 x1 with x2 x2 7! x1 x2 7! x1 True

x1 7! x2 dR

on x1

x1 7! x2 dR on x1

True

x1 7! x2 True

x2 7! x1 True

x2 7! x1

True

x1 7! x2 dR

on x1

x1 7! x2 dR on x1

True

x1 7! x2 dR

on x2

d(==)

eqLVar eqLV0 eqLD0

Table 5.3: Evaluation of equalities through dedicated built-in functions uniformly by di erent built-in functions. If the table is divided into two halves along a diagonal (drawn from the top-left corner to the bottom-right corner) the corresponding cases can be found. Finally, it is also important to point out that deferred bindings are created, by the built-in functions that support the evaluation of equalities, for variables which become bound but are not directly quanti ed. The bindings are \time stamped" with a binding stamp in both the environment and the corresponding defer reference node that encapsulates the binding. The stamping method can be compared to similar approaches that are used in or-parallel implementations of Prolog [GJ90]. It helps distinguishing di erent bindings for the same variable and hence can be used as an indicator that shows whether a previously deferred binding for a variable can be re-evaluated. In this sense, all bindings made for variables which are not directly quanti ed can be seen as conditional bindings (subject to re-evaluation). These bindings remain in the expression under evaluation. They can become part of the answer to an Escher program, unless the same variable is bound \outside" (see Section 4.5), which causes the deferred binding to be re-evaluated.

5.5.4 Conjunction The functionality of conjunction on the machine level has been discussed in Section 4.6. Conjunction is a built-in system function with identi er (&&). It is represented on the heap with the following function node: and 7! h(&&)=2; ∧ or evalAndL evalAndR dAnd i

The references point to the function node for disjunction, the evaluation functions evalAndL and evalAndR and the last reference dAnd points to the descriptor for defer con-

5.5 Escher System Built-in Functions

153

junction nodes.

5.5.4.1 Evaluation of Conjunctions The evaluation of conjunctions can be broken down into a sequence of eight steps. There is a number of transition rules de ned for each step. It is assumed that the rules are tried in the sequence in which they are given in this section; the rst rule that matches is chosen.

Look-above. The following rule de nes when a conjunction returns without further evaluation. If the EQVR is empty, look at the top stack frame and return if the conjunction is an argument of a negation. Notice that a non-empty EQVR means that the conjunction is directly preceded by an existential quanti er. Code NR SR H EQVR CER SER Exec &and nd st h [] ce provided getExpId (st [cn ]) = not =) Return nd st h [] ce ce

Simpli cation of the Boolean constructors.  If the left or right argument of the conjunction in the current node is True then

rewrite to the respective other argument.  If the left or right argument of the conjunction in the current node is False then rewrite to False.

Notice that in contrast to Haskell, where arguments are rst evaluated and afterwards scrutinised, in Escher arguments are already scrutinised before evaluation. There are two transition rules. The rst covers the case when one of the arguments is True. Code NR SR H Exec &and nd st h provided getExpId (nd [i ]) = True, for some i 2 f1; 2g =) Enter nd [j ] nd st h where ( 2 if i = 1 j = 1 otherwise The next transition rule speci es what happens when one of the arguments is False.

154

The Abstract Machine

Code NR SR H Exec &and nd st h provided getExpId (nd [i ]) = False, for some i 2 f1; 2g =) Enter nd [i ] nd st h

Distribution of conjunction over disjunction. The laws for distributing conjunction over disjunction are contained in the statements for (&&). To distribute, the function (&&) matches on the de ned function (||). If one of the arguments of a conjunction is a disjunction, the conjunction is distributed over the disjunction. First, a disjunction in the left argument is distributed. Code NR SR H Exec &and nd st h provided getExpId (nd [1]) = (||) =) Enter nd 0 nd st h0 where 2 nd 7! (&&) x1 x2 6 h = 4 x1 7! (||) y1 y2

:::

3 7 5

(nd0 ; h0 ) = distributeLeft (y1 ; y2 ; setShareFlag (x2 )) The operation setShareFlag takes a pointer to a node on the heap. If this node is a defer node, it traverses the underlying defer tree and sets the share ags in all the defer nodes to S, indicating that the defer tree is now shared. Otherwise it leaves the node unchanged. The operation distributeLeft takes pointers to three nodes, say x, y and z . It creates a disjunction on the heap which represents the formula (x ^ z ) _ (y ^ z ) and returns a pointer to the top-level disjunction together with the modi ed heap. The next transition rule shows how a disjunction in the right argument is distributed. Code NR SR H Exec &and nd st h provided getExpId (nd [2]) = (||) =) Enter nd 0 nd st h0 where 2 nd 7! (&&) x1 x2 h = 64 x2 7! (||) y1 y2

:::

3 7 5

(nd0 ; h0 ) = distributeRight (setShareFlag (x1 ); y1 ; y2 )

5.5 Escher System Built-in Functions

155

The operation distributeRight takes pointers to three nodes, say x, y and z . It creates a disjunction on the heap which represents the formula (x ^ y) _ (x ^ z ) and returns a pointer to the top-level disjunction together with the modi ed heap.

Evaluation of the left argument. If the left argument is not deferred, evaluate it. Code NR SR H Exec &and nd st h provided getExpId (nd [1]) 2= fdL; dR; d(&&); d(==); d(||)g =) Enter nd 0 nd st h0 where 2 3 nd 7! and x1 x2 h = 64 and 7! h(&&)=2; ∧ rs i 75 h

:::

h0 = h nd 0 7! evalAndL x1 x2

i

A call to the evaluation function evalAndL is created and entered next.

Re-evaluation of a deferred left argument. If the left argument is an active defer node, re-evaluate it. Code NR SR H Exec &and nd st h provided isActive (nd [1]) =) Enter nd 0 nd st h0 where 2 nd 7! and x1 x2 h = 64 and 7! h(&&)=2; ∧ rs i h

:::

3 7 5

h0 = h nd 0 7! evalAndL activate (x1 ) x2

i

The predicate isActive and the operation activate are described in Section 5.3.3. The left argument x1 of the conjunction is activated. A call to the evaluation function evalAndL is created for the activated argument and the right argument x2 . This call is entered next.

156

The Abstract Machine

Evaluation of the right argument. If the right argument is not deferred, evaluate it. Code NR SR H Exec &and nd st h provided getExpId (nd [2]) 2= fdL; dR; d(&&); d(==); d(||)g =) Enter nd 0 nd st h0 where 2 3 nd 7! and x1 x2 h = 64 and 7! h(&&)=2; ∧ rs i 75 h

:::

h0 = h nd 0 7! evalAndR x1 x2

i

A call to the evaluation function evalAndR is created and entered next.

Re-evaluation of a deferred right argument. If the right argument is an active defer node, re-evaluate it. Code NR SR H Exec &and nd st h provided isActive (nd [2]) =) Enter nd 0 nd st h0 where 2 nd 7! and x1 x2 h = 64 and 7! h(&&)=2; ∧ rs i h

:::

3 7 5 i

h0 = h nd 0 7! evalAndR x1 activate (x2 ) The right argument x2 of the conjunction is activated. A call to the evaluation function evalAndR is created for the left argument x1 and the activated argument. This call is entered next.

5.5 Escher System Built-in Functions

157

Defer the conjunction. At this stage in the matching process on the transition rules, both arguments of the conjunction must be deferred. Hence, the whole conjunction is deferred and control returns. Code NR SR H VF Exec &and nd st h vf provided getExpId (nd [i ]) 2 fdL; dR; d(&&); d(==); d(||)g, for i = 1; 2 =) Return nd 0 st h0 vf 0 where 2 3 nd 7! and x1 x2 h = 64 and 7! h(&&)=2; ∧ rs i 75

:::

(nd0 ; h0 ) = makeDeferAndNode (dAnd ; x1 ; x2 ) ( ENV if vf = OFF 0 vf = vf otherwise A binary defer node is created for the conjunction and the computation returns. The operation makeDeferAndNode is described in Section 5.3.2.5.

5.5.4.2 Evaluation of the Arguments of Conjunctions Only one argument of a conjunction is evaluated at a time. Two specialised evaluation built-in functions have been de ned for this purpose. These functions are represented on the heap by the following info nodes: evalAndL 7! h(&&)=2; &evalAndL; evalAndL0 and i evalAndR 7! h(&&)=2; &evalAndR; evalAndR 0 and i

The rst reference in the above nodes points to the stack frame descriptor with identi er evalAndL0 or evalAndR0 respectively. The second reference points to the function node of conjunctions, which is used as the continuation function after an argument has been evaluated. Notice that the identi er of both evaluation functions is (&&). This allows matching on the call node in the stack frame. The transition rule for the execution of &evalAndL is given below; the evaluation of the right argument of a conjunction is similar. Both code parts are specialised, &evalAndL evaluates the left and &evalAndR evaluates the right argument. Code NR SR H VF EQVR UQVR CER Exec &evalAndL nd st h vf eqv uqv ce =) Enter x1 nd st 0 h0 OFF eqv uqv ce

158

The Abstract Machine

where

2

h =

6 4

nd 7 evalAndL x1 x2 ! evalAndL ! 7 h(&&)=2; &evalAndL; rs i h

h0 = h st 0 7!

:::

evalAndL0

3 7 5

es vf eqv uqv ce st and nd

i

When arguments of a conjunction are evaluated, the state of the EQVR and UQVR remains unchanged. Also, unlike in other evaluation functions, the environment status is left unchanged. The transition rule(s) thus implement one part of the special role of conjunctions regarding the propagation of variable bindings, which was discussed in Section 4.6. The other part is supported by the respective continuation functions. When returning from the evaluation of an argument of a conjunction, one of the continuation functions evalAndL0 or evalAndR0 on the stack is invoked. The transition rule for evalAndL0 is given below; returning from the evaluation of the right argument is similar. Code NR SR H VF Exec &evalAndL0 nd st h vf =) Enter cn nd ps h0 vf 0 where 2 nd 7! f ys 6 0 6 h = 66 st 7! evalAndL esst 4 cn 7! evalAndL x1 x2

h0

h

:::

i

EQVR UQVR CER eqv uqv ce eqv uqv ce 3

vfst eqvst uqvst envst ps and cn

7 7 7 7 5

= h cn 7! and nd x2 vf 0 = setVF The call node is updated with a pointer to the evaluated left argument and turned into a conjunction node which is re-entered next. It is safe to update since (&&) creates a new call node before an argument is evaluated. Unlike free0 which deals with deferred computations and only passes constructors to the continuation of the calling function, the evaluation functions for conjunctions pass any evaluation result to the conjunction. Notice that the EQVR contents from the argument evaluation remains. This has the e ect that, when an argument is a call to the built-in function sigma, the new existential quanti er is lifted to the top level of the conjunction, which extends its scope. The corresponding rewrite rule was discussed in Section 4.6. Also, unlike other evaluation functions which restore the environment from the stack to avoid bindings being propagated from arguments into the call level, both evalAndL0 and evalAndR0 leave the environment of the argument unchanged, except for refreshing variable bindings. Hence, equalities occurring as arguments of conjunctions write bindings into an environment which is propagated through the evaluation continuation functions into the conjunction; making bindings accessible in the whole conjunction level. The evaluation of equalities is described in Section 5.5.3.

5.5 Escher System Built-in Functions

159

5.5.5 Disjunction Disjunction is a built-in function with identi er (||). It is represented on the heap with the following function node: or 7! h(||)=2; ∨ exq evalOrL evalOrR dOr i

The references point to the descriptor of the auxiliary function exq (which is further described in Section 5.5.5.1), the evaluation functions evalOrL and evalOrR and the last pointer dOr points to the descriptor for defer disjunction nodes.

5.5.5.1 Evaluation of Disjunctions The evaluation of disjunctions can be broken down into a sequence of 6 steps. There is a number of transition rules de ned for each step. It is assumed that the rules are tried in the sequence in which they are given in this section; the rst rule that matches is chosen.

Look-above. The following rules de ne when a disjunction returns without further evaluation.

 Return if the disjunction is an argument of a conjunction. Code NR SR H Exec &or nd st h provided getExpId (st [cn ]) = (&&) =) Return nd st h  Return if the EQVR is empty and the disjunction is an argument of a negation. Code NR SR H EQVR CER SER Exec &or nd st h [] ce provided getExpId (st [cn ]) = not =) Return nd st h [] ce ce The EQVR needs to be empty to ensure that there is no existential quanti er between the negation on the stack and the disjunction in the current node. A pointer to the environment of the disjunction is placed into the SER.  Return if the EQVR is empty and the disjunction is the top-level function of a set body. The representation of sets is described in Section 5.6.1. In order to evaluate sets the built-in function evalSet is used. It can be determined, whether the current node is the top-level function of a set body, by checking, whether the call node, on

160

The Abstract Machine the top stack frame, is a call to evalSet. Code NR SR H EQVR Exec &or nd st h [] provided getExpId (st [cn ]) = evalSet =) Return nd st h [] The EQVR needs to be empty to ensure that there is no existential quanti er between the set evaluation on the stack and the disjunction in the current node.  Return if the EQVR is empty and the disjunction is the antecedent of a universally quanti ed implication. Universal quanti cation is evaluated in several stages; see Section 5.5.2 for details. The nal stage is the evaluation of the antecedent of the quanti ed implication. The evaluation function evalVAnt is used for this purpose. Code NR SR H EQVR Exec &or nd st h [] provided getExpId (st [cn ]) = evalVAnt =) Return nd st h [] The EQVR needs to be empty to ensure that the antecedent is not existentially quanti ed.

Simpli cation of the Boolean constructors.  If the left or right argument of the disjunction in the current node is True then

rewrite to True.  If the left or right argument of the disjunction in the current node is False then rewrite to the respective other argument.

Again, notice that in contrast to Haskell, where arguments are rst evaluated and afterwards scrutinised, in Escher arguments are already scrutinised before evaluation. There are two transition rules. The rst covers the case when one of the arguments is True. Code NR SR H Exec &or nd st h provided getExpId (nd [i ]) = True, for some i 2 f1; 2g =) Enter nd [i ] nd st h

5.5 Escher System Built-in Functions

161

The next transition rule speci es what happens when one of the arguments is False. Code NR SR H Exec &or nd st h provided getExpId (nd [i ]) = False, for some i 2 f1; 2g =) Enter nd [j ] nd st h where ( 2 if i = 1 j = 1 otherwise

Distribution of existential quanti cation. In Section 4.7 the rewrite rule which distributes existential quanti cation has been discussed. It was made clear there, that renaming is essential to maintain a consistent representation of variables on the machine level. In EMA, a disjunction is directly preceded by an existential quanti er if the EQVR is not empty (i.e. EQVR 6= []) at the time when the disjunction is evaluated. The following transition rule gives the technical realization of the rewrite rule. Code NR SR Exec &or nd st =) Enter nd 0 nd st where 2 nd 6 or h = 664 st 2

H EQVR CER h [v1; : : : ; vn ] ce h0 [] ce 0

7! or x x 7 h(||)=2; ∨ rs i ! 7 cont esst vfst eqvst uqvst envst ps fn cn ! 1

:::

2

y1 ! 7 exq [v1 ; : : : ; vn ] ce x1 6 y ! exq [v10 ; : : : ; vn0 ] ce x2 6 2 7 6 h1 = h 66 v10 ! 7 var 4 ::: vn0 7! var (y20 ; h2 ) = rename (y2 ; fv1 =v10 ; : : : ; vn =vn0 g; h1 ) h i h0 = h2 nd 0 7! (||) y1 y20 ce 0

=

8 > > < > > :

3 7 7 7 5

3 7 7 7 7 7 5

envset if getExpId (cn ) = evalSet where envset is the environment of the set constructor whose body is currently evaluated envst otherwise; envst [es ] needs to be set to READ

The function exq is used to encapsulate an existentially quanti ed computation which has been distributed. A call to exq contains pointers to the list of existentially quan-

162

The Abstract Machine

ti ed variables, the quanti ed formula and the environment in which the quanti cation was evaluated before. When exq is evaluated it restores the context and rewrites to the quanti ed formula. This is described in more detail in the following Section 5.5.5.3. The operation rename has been described in detail in Section 5.5.2.5 on page 138.

Evaluation of the left argument. If the left argument is not deferred, evaluate it. Code NR SR H Exec &or nd st h provided getExpId (nd [1]) 2= fdL; dR; d(&&); d(==); d(||)g =) Enter nd 0 nd st h0 where 2 3 nd 7! or x1 x2 h = 64 or 7! h(||)=2; ∨ rs i 75 h

:::

h0 = h nd 0 7! evalOrL x1 x2

i

A call to the evaluation function evalOrL is created and entered next.

Evaluation of the right argument. If the right argument is not deferred, evaluate it. Code NR SR H Exec &or nd st h provided getExpId (nd [2]) 2= fdL; dR; d(&&); d(==); d(||)g =) Enter nd 0 nd st h0 where 2 3 nd 7! or x1 x2 h = 64 or 7! h(||)=2; ∨ rs i 75 h

:::

h0 = h nd 0 7! evalOrR x1 x2

i

A call to the evaluation function evalOrR is created and entered next.

5.5 Escher System Built-in Functions

163

Defer the disjunction. At this stage in the matching process on the transition rules, both arguments of the disjunction must be deferred. The whole disjunction is deferred and control returns. Code NR SR H VF Exec &or nd st h vf provided getExpId (nd [i ]) 2 fdL; dR; d(&&); d(==); d(||)g, for i = 1; 2 =) Return nd st h0 vf 0 where 2 3 nd 7! or x1 x2 h = 64 or 7! h(||)=2; ∨ rs i 75

:::

(nd0 ; h0 ) = makeDeferOrNode (dOr ; x1 ; x2 ) ( ENV if vf = OFF 0 vf = vf otherwise A binary defer node is created for the disjunction and the computation returns. The operation makeDeferOrNode is described in Section 5.3.2.6.

5.5.5.2 Evaluation of the Arguments of Disjunctions For the evaluation of the arguments of disjunctions two specialised evaluation built-in functions have been de ned. They work similar to the evaluation built-in function free. They evaluate one argument at a time, but pass the evaluation result directly to the disjunction without changing it. The nodes representing the evaluation functions on the heap are as follows: evalOrL 7! h(||)=2; &evalOrL; evalOrL0 or i evalOrR 7! h(||)=2; &evalOrR; evalOrR 0 or i

The rst reference in the above nodes points to the stack frame descriptor with identi er evalOrL0 or evalOrR0 respectively. The second reference points to the function node of disjunctions, which is used as the continuation function after an argument has been evaluated. Notice that the identi er of both evaluation functions is (||). This allows matching on the call node in the stack frame. The transition rule for the execution of &evalOrL is given below; the evaluation of the right argument of a disjunction is similar. Both code parts are specialised, &evalOrL evaluates the left and &evalOrR evaluates the right argument. Code NR SR H VF EQVR UQVR CER Exec &evalOrL nd st h vf eqv uqv ce =) Enter x1 nd st 0 h0 OFF [] [] ce

164

The Abstract Machine

where

2

h =

6 6 6 4

nd 7 evalOrL x1 x2 ! evalOrL ! 7 h(||)=2; &evalOrL; rs i ce 7 Env es bdl ! "

:::

3 7 7 7 5 #

st 0 7! evalOrL0 es vf eqv uqv ce st or nd h0 = h ce 7! Env READ bdl The stack frame and registers are set up for the argument evaluation in the same way as &free does. When returning to one of the continuation functions evalOrL0 or evalOrR0 the state of the previous computation is restored in the same way as the continuation of the function free does. As an example, a return to evalOrL0 is shown below.

Code NR SR H VF EQVR UQVR 0 Exec &evalOrL nd st h vf eqv uqv 0 0 =) Enter cn nd ps h vf eqvst uqvst where 2 st 7! evalOrL0 esst vfst eqvst uqvst 6 6 h = 66 envst 7! Env es bdl 7! evalOrL x1 x2 4 cn "

:::

CER ce envst envst ps or cn

3 7 7 7 7 5

#

7! or nd x2 = h cn envst 7! Env esst bdl vf 0 = setVF The call node is updated with a pointer to the evaluated argument and turned into a disjunction node which is re-entered. It is safe to update since (||) creates a new call node before an argument is evaluated. A return to evalOrR0 is similar. h0

5.5.5.3 Re-evaluation of Distributed Existential Quanti cation The function exq is an auxiliary function which is used to encapsulate an existentially quanti ed computation that has been distributed. Calls to exq are created by disjunctions when an existential quanti er is distributed. A exq node has the following structure: exq qv env qf where the pointer exq points to the following function node on the heap: exq 7! hexq=3; &exq; rs i The three arguments are: a list of existentially quanti ed variables, the local environment, which contains the binding records for the quanti ed variables, and the quanti ed formula.

5.5 Escher System Built-in Functions

165

The code &exq performs the following actions: 1. Copy the list of quanti ed variables and afterwards add the copied list to the front of the list pointed to by the EQVR. Put a pointer to the amended list into EQVR. 2. Copy the local environment and merge the writable copy with the environment pointed to by the CER. 3. Enter the quanti ed formula. The following transition rule summarises the functionality of exq. Code NR SR H EQVR CER Exec &exq nd st h eqv ce 0 =) Enter qf nd st h qv ++eqv ce 0 where " # nd ! 7 exq qv env qf h = ::: ce 0 = merge (copy (env ); ce )

5.5.6 Negation Negation is a built-in function with identi er not. It implements the statements de ned for negation in the Escher Booleans module. Negation is represented by the following function node on the heap: not 7! hnot=1; ¬ true false or and dRef var evalNot ctx i

The references contain pointers to the constructors True and False, the function nodes for disjunction and conjunction, the descriptor node for defer references, variables and the evaluation function for negation. The pointer ctx points to a context-restoring indirection node which is used when a conjunction or disjunction is negated.

5.5.6.1 Evaluation of Negation The evaluation of negation can be broken down into a sequence of 7 steps. There is a number of transition rules de ned for each step. It is assumed that the rules are tried in the sequence they are given below; the rst rule that matches is chosen.

Look-above. If there are no existentially quanti ed variables, look at the top stack frame and return if it is a negation.

166

The Abstract Machine

Code NR SR H EQVR CER SER Exec ¬ nd st h [] ce se provided getExpId (st [cn ]) = not =) Return nd st h [] ce ce

Simpli cation of the Boolean constructors.  If the argument of the negation is True then rewrite to False.  If the argument of the negation is False then rewrite to True. Here again, it is worth to notice that in Escher arguments are already scrutinised before evaluation takes place. There are two transition rules. The rst covers the case when the argument is True. Code NR SR H Exec ¬ nd st h provided getExpId (nd [1]) = True =) Enter false nd st h The next transition rule speci es the resulting machine state when the argument of a negation is False. Code NR SR H Exec ¬ nd st h provided getExpId (nd [1]) = False =) Enter true nd st h

Rewrite double negation. The Escher statement not

(not x) = x

is implemented in the following transition rule.

Code NR SR H CER SER Exec ¬ nd st h ce se provided getExpId (nd [1]) = not =) Enter y nd st h ce 0 NULL

5.5 Escher System Built-in Functions where

2

h

=

ce 0 =

6 6 6 4

nd ! 7 not x x 7 not y ! not ! 7 hnot=1; ¬ rs i

(

:::

167 3 7 7 7 5

se if se 6= NULL ce otherwise

Negate conjunctions. When a conjunction is negated the following transition rule matches. Code NR SR H CER SER Exec ¬ nd st h ce se provided getExpId (nd [1]) = (&&) =) Enter nd 0 nd st h0 ce 0 NULL where 2 nd 7! not x 6 x (&&) y z 6 h = 64 not 7! 7! hnot=1; ¬ rs i (

:::

3 7 7 7 5

se if se 6= NULL ce otherwise (nd0 ; h0 ) = negateConjunction (y ; z ; or ; ctx ; h ) The operation negateConjunction takes pointers to two expression nodes, say y and z , two pointers to function nodes, one for disjunction the other for a context indirection, and a heap. It creates a disjunction on the heap which represents the formula (not y) _ (not z ) and returns a pointer to the disjunction together with the modi ed heap. If the SER is not empty, the environment of the negated conjunction needs to be propagated into both negated arguments. This is achieved by indirecting the nodes y and z via the function ctx; i.e. in the above formula y is replaced by (ctx se y ) and z is replaced by (ctx se z ) where se is the environment pointer in SER. The function ctx is semantically speaking an identity function. It takes an environment (called the local environment) and a node as arguments and works similar to an indirection. In addition, it merges a copy of the local environment with the environment in the CER. The resulting environment is writable; it is put into the CER before the node in the second argument is entered. ce 0

=

168

The Abstract Machine

Negate disjunctions. Disjunction nodes are negated according to the following rule. Code NR SR H CER SER Exec ¬ nd st h ce se provided getExpId (nd [1]) = (||) =) Enter nd 0 nd st h0 ce 0 NULL where 2 nd 7! not x 6 x (||) y z h = 664 not 7! 7! hnot=1; ¬ rs i (

:::

3 7 7 7 5

se if se 6= NULL ce otherwise (nd0 ; h0 ) = negateDisjunction (y ; z ; and ; ctx ; h ) The operation negateDisjunction takes pointers to two expression nodes, say y and z , two pointers to function nodes, one for conjunction and one to the context indirection ctx, and a heap. It creates a conjunction on the heap which represents the formula (not y) ^ (not z ) and returns a pointer to the disjunction together with the modi ed heap. If the SER is not empty, the environment of the negated conjunction needs to be propagated into both negated arguments. This is achieved by indirecting the nodes y and z via the function ctx; i.e. in the above formula y is replaced by (ctx se y ) and z is replaced by (ctx se z ) where se is the environment pointer in SER. The function ctx was described with the previous transition rule where conjunction was negated. ce 0

=

Evaluate the argument of the negation. None of the previous transition rules can be applied. If the argument is not a deferred computation then evaluate it. Code NR SR H Exec ¬ nd st h provided getExpId (nd [1]) 2= fdL; dR; d(&&); d(==); d(||)g =) Enter nd 0 nd st h0 where 2 3 nd 7! not x h = 64 or 7! hnot=1; ¬ rs i 75 h

:::

h0 = h nd 0 7! evalNot x

i

5.5 Escher System Built-in Functions

169

A call to the evaluation function evalNot is created and entered next.

Defer the negation. A defer node is created for the negation with a reference to the deferred argument. Code NR SR H VF Exec ¬ nd st h0 vf provided getExpId (nd [1]) 2 fdL; dR; d(&&); d(==); d(||)g =) Return nd 0 st h0 vf 0 where 2 3 nd 7! not x h = 64 not 7! hnot=1; ¬ rs i 75

:::

(nd 0 ; h 0 ) = makeDeferRefNode (dRef ; nd ; 1; h) ( ENV if vf = OFF 0 vf = vf otherwise

5.5.6.2 Evaluation of a Negated Expression For the evaluation of the argument of a negation the specialised evaluation built-in function has been de ned. It works similar to free, but after the evaluation it passes the evaluation result directly to the negation without changing it. The node representing the evaluation function on the heap is: evalNot 7! hnot=1; &evalNot; evalNot 0 not i

The rst reference in the above node points to the stack frame descriptor with identi er evalNot0. The second reference points to the function node of negation, which is used as the continuation function after an argument has been evaluated. Notice that the identi er of the evaluation function is not. This allows matching on the call node in the stack frame. The transition rule for the execution of &evalNot is shown below. Code NR SR H VF EQVR UQVR CER Exec &evalNot nd st h vf eqv uqv ce 0 0 =) Enter x nd st h OFF [] [] ce

170

The Abstract Machine

where

2

h =

6 6 6 4

nd 7 evalNot x ! evalNot ! 7 hnot=1; &evalNot; rs i ce 7 Env es bdl !

:::

"

3 7 7 7 5 #

st 0 7! evalNot 0 es vf eqv uqv ce st not nd h0 = h ce 7! Env READ bdl The stack frame and registers are set up for the argument evaluation in the same way as free does. When returning to the continuation function evalNot0 the state of the previous computation is restored. The call node is updated with the evaluation result and re-entered.

Code NR SR H VF EQVR UQVR Exec &evalNot0 nd st h vf eqv uqv =) Enter cn nd ps h0 vf 0 eqvst uqvst where 2 st 7! evalNot0 esst vfst eqvst uqvst 6 6 h = 66 envst 7! Env es bdl 7! evalNot x 4 cn "

:::

CER ce envst envst ps not cn

3 7 7 7 7 5

#

7! not nd h0 = h cn envst 7! Env esst bdl vf 0 = setVF It is safe to update the call node because the negation which forced the argument evaluation created a new node before evaluation was started.

5.6 Set Processing Escher provides set-processing facilities by means of higher-order functions which match on set patterns. In Section 3 it was explained, how set-processing functions can be compiled. The family of evalSet built-in functions, which is used to evaluate the arguments of set-processing functions, was also introduced there. In this section, an implementation of evalSet, and its continuation functions evalSet0 and evalToSet0, is outlined. First, however, it needs to be made clear how sets are represented on the machine level. Set application is described in Section 5.4.3.4, where function application, using the apply family of built-in functions, is explained.

5.6.1 Representation of Sets on the Machine Level Sets are functions of type (a -> Bool). Escher provides set patterns which can be used in function heads to match on sets and in function bodies to construct sets. Sets in statement

5.6 Set Processing

171

heads are purely used for pattern matching. A function, say f , with a set pattern in the head of its statements uses evalSet to evaluate the corresponding argument to a set. The matching on a set pattern is based on matching on the top-level function of a set body. When the argument returns, the function continuation of f scrutinises the evaluation result using a case expression. To allow matching on the set body, evaluated sets are represented using the constructor Set, which was introduced brie y in Section 3.2.1.2. On the machine level an evaluated set is represented by the following node: set v b env

where set points to the following descriptor node: set 7! hSet=3; &reti

The Set constructor is in fact a meta-constructor. It takes a variable (called the set variable), a boolean expression (called the set body) in which the set variable occurs free and an environment (called the set environment). Let p be the pointer to a node representing an evaluated set on the heap. In what follows, the elds of the node pointed to by p, are referred to as p [v ], p [b ] and p [env ]. The Set constructor provides a exible representation of sets. It allows matching on the set body once the set has been evaluated. Moreover, evaluated sets can still be used like functions in applications. Applying a set which is represented by a Set constructor to an argument simply means binding the set variable in the set environment to the argument, and rewriting to the set body (see Section 5.4.3.4 for details). The set constructor is also used by the compiler to represent not liftable sets in function bodies.

5.6.2 The Evaluation of an Argument to a Set This section describes how arguments of compiled strict set-processing functions are evaluated. The compilation of strict functions which contain set patterns in the heads of their statements results in the addition of calls to the evalSet evaluation functions instead of calls to free. The functionality of evalSet has been described brie y in Section 3.2.1.2.

5.6.2.1 Before the Evaluation of an Argument Let f be an n-ary set processing function which is strict in its rst argument. Before an argument is evaluated to a set, the function pointer f of the current node points to an evaluation function evalSet which contains the code &evalSet. The references of this function node are a pointer to the continuations evalSet0 and evalToSet0, which are used as stack frame descriptors, and a pointer to the function f 0 , which is used as the continuation function for the call node when the argument evaluation is nished. The function node representing the evaluation function is shown below:

f 7! hevalSet=n; &evalSet; evalSet 0 evalToSet 0 f 0i

172

The Abstract Machine

The evaluation function evalSet is used to evaluate arguments so that set pattern matching can be performed on them. When evalSet is called to evaluate an argument which is already in set constructor representation then evalSet merges the set environment with the current environment and puts the merged environment into the CER. The call to evalSet (i.e. the call node) is put into the stack frame and the set body is entered. If the argument is not yet in set constructor representation it is simply entered. The transition rules for both cases are given below.

Evaluation of a set in constructor representation. The case, when the argument is already in set constructor representation, is shown rst. Code NR SR H VF EQVR UQVR CER Exec &evalSet nd st h vf eqv uqv ce provided getExpId (nd [1]) = Set =) Enter sb nd st 0 h0 OFF [] [] ce 0 where 2 nd 7! f x1 xn,1 6 x1 7! Set sv sb senv h = 664 f 7! hevalSet=n; &evalSet; evalSet 0 evalToSet 0 f 0 i h

:::

= h st 0 7! evalSet0 es vf eqv uqv ce st f 0 nd ( merge (copy (senv ); ce ) if senv [es ] = READ ce 0 = merge (senv ; ce ) otherwise

h0

3 7 7 7 5

i

The stack frame descriptor evalSet0 is used to indicate that the argument was already in set constructor representation before its evaluation was forced and thus the node under evaluation is the set body.

Evaluation of a set which is not in set constructor representation. The next rule is used when the argument to be evaluated in not yet in set constructor representation. Code NR SR H VF EQVR UQVR CER Exec &evalSet nd st h vf eqv uqv ce provided getExpId (nd [1]) 6= Set =) Enter nd [1 ] nd st 0 h0 OFF [] [] ce

5.6 Set Processing where

2

h =

6 6 6 6 4

173 3

nd 7! f x1 xn,1

7! hevalSet=n; &evalSet; evalSet 0 evalToSet 0 f 0i 777 7 ce 7! Env es bdl 5

f "

:::

#

st 0 7! evalToSet0 es vf eqv uqv ce st f 0 nd h ce 7! Env READ bdl The stack frame descriptor evalToSet0 indicates that the argument was not yet in set constructor representation and therefore the actual argument is currently evaluated.

h0 =

5.6.2.2 Returning to the Continuation evalSet0 The continuation evalSet0 is put on the stack when evalSet is used to evaluate an argument which is already in set constructor representation. It is represented on the heap by the following function node: evalSet 0 7! hevalSet0=8; &evalSet0; dRef equal i

The references contain pointers to the descriptor for defer reference nodes and the equality function. The node under evaluation is a set body. Set bodies are boolean formulas; they can evaluate to either of the constructors True or False or to a deferred computation. In addition, disjunctions return without further evaluation when the call node on the top stack frame is an evalSet node. Hence, a set body can also return as a disjunction. Pattern matching is performed in a cascade of case expressions contained in the continuation of the function that forced evaluation. Pattern matching on sets means matching on the top-level function of the set body. Table 3.2 in Section 3.2.1.2 shows each pattern and the corresponding set constructor representation. To match one of the patterns, the top-level function of the set body needs to be False, an equality involving the set variable or a disjunction. Hence, if the set body evaluates to True an error has occurred. The case expression which implements set matching in the function continuation matches True against the default alternative which triggers a match error; the computation is stopped. The set variable is a free variable in the set body. Equalities for not directly quanti ed variables are rewritten to defer nodes. Hence, if the set body returns as a deferred computation, it is necessary to check, whether the deferred call is an equality containing the set variable. If not, the set evaluation is deferred subsequently. The following transition rules show the execution of &evalSet0 for the above named cases.

174

The Abstract Machine

The set body evaluates to a constructor or a disjunction. The case when the set body evaluates to True, False or a disjunction is shown rst. Code NR SR H VF EQVR UQVR CER 0 Exec &evalSet nd st h vf eqv uqv ce provided getExpId (nd ) 2 fTrue; False; (||)g =) Enter cn 0 nd ps h0 vf 0 eqvst uqvst envst where 2 st 7! evalSet0 esst vfst eqvst uqvst envst ps f 0 cn 6 h = 664 cn 7! f x1 xn,1 x1 7! Set sv sb senv

h0

"

:::

0 7 Set sv nd ce = h xcn1 0 ! 7 f 0 x10 xn,1 !

3 7 7 7 5

#

vf 0 = setVF The set body is simply put into the set constructor which is used to represent the set. Pattern matching is then performed by the continuation of the calling function. It is important to restate here, that in the set pattern {x|(u||v)}, x, u and v must be syntactic variables. Hence, if the set body evaluated to a disjunction, pattern matching is only performed on the top-level disjunction, and not on its arguments. The restriction disallows patterns like {x|(x==1||x==2)} to be used for matching. To achieve the intended e ect (in the previous example), set-processing functions can be de ned recursively. This means, the recursive case uses an instance of the more general pattern where the arguments of the disjunction are represented by syntactical variables. Provided the de nition of such a recursive set-processing function contains the two base cases viz. matching on an empty and singleton set, matching on the more general pattern can be used to recursively apply a set-processing function to the sets {x|u} and {x|v}. Speci c elements of a set can then be processed in the body of the statement that matches on singleton sets.

The set under evaluation is a singleton set. A set variable is not recognised as a directly quanti ed variable during the evaluation of the set body. It is therefore treated by the functions which implement equality as a variable which is not directly quanti ed. Hence, if the set under evaluation is a singleton set, the set body returns as an equality, encapsulated in a defer reference node. This equality involves the set variable in one argument. Notice that the defer reference (i.e. the position of the deferred argument in the deferred equality) is not necessarily on the set variable; see Section 5.5.3 for details. However, the set pattern which is used to match on a singleton set requires the set variable to appear as the rst argument in an equality node. Hence, to enable the continuation of a set-processing function to match on a singleton set, the arguments of the equality in the defer node might need to be swapped, before the equality can be used for matching. In the transition rule which follows, a new equality node which

5.6 Set Processing

175

has the set variable as rst argument is created for this purpose. This equality can be used as the set body, in the set constructor which represents the set that was evaluated. Set processing can be used to integrate the functional and logic programming style in Escher. Set-processing functions provide the interface between purely functional computations and logic computations, in the sense that the set-processing function itself can be a non-predicate function, but the evaluation of its argument(s) to match on set patterns involves logic computations. Matching on a singleton set is one of the base cases in the de nition of a recursive set-processing function. In fact, matching on a singleton set often means extracting a term (which represents the sole element of the set) from the set (which is a predicate). This term can then be used for further processing, e.g. as an argument of another (non-predicate) function. It is important to note that such a term is not necessarily ground. However, all local variables (i.e. quanti ed variables) that were involved in the logic computation which produced the singleton set, (must) have been bound during the set evaluation. On the machine level, such a term is represented by a graph of heap nodes and the corresponding environment which holds the bindings for the local variables. When the term is \pulled out" from the set context, the connection between the local variables and their bindings must be preserved to ensure a correct representation. There are di erent ways of realizing this; three of them are examined below.

 The rst is a \brute force" solution, where the local variables are explicitly sub-

stituted with their bindings. It can be implemented as part of the functionality of &evalSet0. The method is based on the assumption that after substituting the local variables, no more local variables occur in the term; the term can therefore be handed to a purely functional computation.  The second solution is to encapsulate the term into a context indirection function (similar to ctx in Section 5.5.6.1), which restores the local environment when the value of the term is demanded. However, this solution only works when the local environment of the term is further propagated by the respective evaluation function. Remember that on return from an argument evaluation the local environment is discarded and replaced with the environment of the calling function; except when the calling function is a conjunction. Hence, unless the term is processed as an argument of a conjunction, the suggested approach does not work.  Would the locally quanti ed variables be updated in place when they are bound, the environment would not be necessary. Thus the graph that represents the term on the heap would no longer contain local variables at the time when the set pattern matching is performed. This looks, at rst sight, like a reasonable solution. It is based on the assumption that the local variables can be kept physically distinct during a computation (i.e. renaming is used when new variables are introduced at run time and the results of calls to built-in functions like sigma and pi, which introduce new variables, are not shared). However, for several reasons, updating in place cannot be used as a substitution application mechanism in EMA. What goes wrong here is that updating interferes with sharing, or better \not" sharing. For example, take an original expression that contains some existentially quanti ed variables. During a computation, new existentially quanti ed variables can be introduced. But the

176

The Abstract Machine result of a call to the built-in function sigma cannot be shared. If one of the \old" existentially quanti ed variables is bound to (and, hence, updated with) an expression which contains one of the \new" variables, the \new" variable is introduced into the original expression. In the case that the original expression cannot be updated with its evaluation result (because sharing cannot be used), it might be re-evaluated at a later stage in the computation. Then, the \new" variable in the expression cannot be recognised.

It has been decided to use the \brute force" method as an initial way of preserving a correct term representation on the machine level. As pointed out before, matching on a singleton set is one of the base cases for recursive set-processing functions. Explicit substitution is, hence, necessary for each element in a set; this means it does potentially happen very frequently in set-processing computations. More work is therefore necessary to nd a more ecient solution. In the next transition rule, the operation substitute is used to perform an explicit substitution. It takes a term (say t), an environment (say env ) and the heap. The operation reconstructs t on the heap, thereby substituting all occurrences of variables in t with the corresponding binding of these variables in env . The resulting term and the modi ed heap are returned in form of a tuple. In the transition rule below, the pointer sv points to the set variable of the set whose body is under evaluation. The operation getPosition determines the argument position of the set variable in the equality that was returned. Code NR SR H VF EQVR UQVR CER Exec &evalSet0 nd st h vf eqv uqv ce provided getExpId (nd ) = dR and let dc = nd [fc ] in getExpId (dc ) = (==) and getExpId (dc [i ]) = dL and getVar (dc [i ]) = sv , for some i 2 f1; 2g =) Enter cn 0 nd ps h0 vf 0 eqvst uqvst envst where 2 st 7! evalSet0 esst vfst eqvst uqvst envst ps f 0 cn 6 h = 664 cn 7! f x1 xn,1 x1 7! Set sv sb senv

i

:::

3 7 7 7 5

= getPosition (nd [fc ]; sv ) (t; h1 ) = substitute ((nd [fc ])[i]; ce ; h) 2 3 nd 0 7! equal sv t h0 = h1 64 x01 7! Set sv nd 0 ce 75 cn 0 7! f 0 x10 xn,1 vf 0 = setVF The equality with the set variable as rst argument and the explicitly substituted second argument is put into the set constructor and the function continuation is called on the evaluated set.

5.6 Set Processing

177

The set body returns as a deferred computation. Finally, if the set body returns deferred and the previous rule does not apply, the call to evalSet is deferred with a defer reference node. A transition rule is not given here, because the transition is very similar to the one for the deferred evaluation result given for &free0 in Section 5.4.1.2.

5.6.2.3 Returning to the Continuation evalToSet0 The continuation evalToSet0 is put on the stack when evalSet is used to evaluate an argument which is not in set constructor representation. It is represented on the heap by the following function node: evalToSet 0 7! hevalToSet0=8; &evalToSet0; dRef var set i

The references contain pointers to the descriptor of defer reference nodes, the variable descriptor and a pointer to the constructor Set. The node under evaluation represents a set. Depending on what the result of the evaluation is, di erent cases can be distinguished. The node can either return in the form of a set constructor, as a function node, or as a deferred computation. All three cases are described in detail in the following.

A set constructor returns. This occurs e.g. when the original argument is either a call to a set-processing function that returns a set, or a variable that is bound to a set constructor. The fact that a set constructor returned does not mean that the set body has been evaluated. To force the evaluation of the set body the set constructor is put into a copy of the call node on the stack. The call node, which is a call to evalSet, is then re-entered. In practice, one can combine the following rule with the rule that de nes the re-evaluation to obtain more ecient behaviour. Code NR SR H VF EQVR UQVR CER 0 Exec &evalToSet nd st h vf [] [] ce provided getExpId (nd ) = Set =) Enter cn 0 nd ps h0 vf 0 eqvst uqvst envst

178

The Abstract Machine

where

2

h

=

6 6 6 6 6 6 4

nd st envst cn "

7! Set sv sb senv 7 evalToSet0 esst vfst eqvst uqvst envst ps f 0 cn ! 7! Env es bdl 7 f x xn, !

:::

1

1

cn 0 7! f nd xn,1 = h envst 7! Env esst bdl vf 0 = setVF

h0

3 7 7 7 7 7 7 5

#

A function node returns. Sets are functions. During compilation all liftable sets in function bodies are translated into lambda expressions, which are then lifted to the top-level through lambda lifting. The original argument can therefore be a function node. The evaluation of a function node causes the execution of the code &ret contained in the function constructor. The computation returns immediately. Since the program has been type-checked during compilation, it can be assumed that the function, which is pointed to by the current node, represents a set. To perform set pattern matching on this set, its body needs to be evaluated. To do this, the set is transformed into constructor representation according to the following steps. 1. Create a node for one new variable on the heap. This is the set variable. 2. Create an expression node applying the function in the current node (this is the set) to the variable created before. This represents the body of the set. 3. Create a binding record for the new variable. 4. Create a writable environment with a binding list containing just the binding record for the set variable. This is the initial set environment. 5. Create a set constructor with the set variable, body and environment. 6. Copy the original call node and update it with a pointer to the set constructor at the respective argument position. 7. Re-enter the updated call node. The following transition rule shows how the transformation is performed by &evalToSet0. Code NR SR H VF EQVR UQVR CER 0 Exec &evalToSet nd st h vf [] [] ce provided getExpId (nd ) 6= Set =) Enter cn 0 nd ps h0 vf 0 eqvst uqvst envst

5.7 Summary where

179 2

h

=

6 6 6 6 6 6 4

nd st envst cn 2

7! hid=1; cd; rs i 7 evalToSet0 esst vfst eqvst uqvst envst ps f 0 cn ! 7! Env es bdl 7! f x xn,

:::

1

1

sv 7 var ! 6 sb 7 nd sv ! 6 6 6 senv ! 7 Env WRITE [(sv ; sv ; Ub)] h0 = h 66 x0 ! 7 Set sv sb senv 6 10 6 cn 7! f x01 xn,1 4 envst 7! Env esst bdl vf 0 = setVF

3 7 7 7 7 7 7 5

3 7 7 7 7 7 7 7 7 5

A deferred computation returns. In this case the call node is deferred with a defer reference node. A transition rule is not given here because the transition is very similar to the one for the deferred evaluation result, given for &free0 in Section 5.4.1.2.

5.7 Summary This chapter describes the features of the abstract machine that supports Escher computations. It outlines the machine architecture, which is based on the Brisk machine and has ve new machine components which support the logic programming features of Escher. The operational behaviour of the machine is given in form of a state transition system. The transition rules can be divided into a set of rules which support the basics of a computation, and a series of more complicated transitions which handle the more sophisticated features. The rst set of transition rules de nes entering nodes on the heap, executing the code of a function, evaluating EKL expressions and returning from subcomputations. The rules are very simple; they modify only the rst four machine components (viz. the code component, the current node, the stack and the heap). The other components remain unchanged. To keep the structure of the chapter clear, the second set of transition rules has again been divided into several parts. The rst of them describes how the foundations of logic programming, such as the evaluation of variables and the handling of deferred computations, is implemented. In the next part, the control-related built-in functions which support evaluation, partial application and application are described. A major part is taken by the built-in functions which support the Escher system functions (i.e. those functions in the Escher systems module Booleans which are not translated into EKL). The nal part presents the support for set processing on the machine level. The machine architecture of EMA clearly resembles the Brisk machine. Especially, the encapsulation of the more complicated issues, like evaluation, sharing, partial application and application, has been a straightforward extension and modi cation of the correspond-

180

The Abstract Machine

ing built-in functions used in Brisk. In addition, the new built-in functions which support the logic features of Escher can be integrated well into the underlying machine model. In fact, the simple graph reduction mechanism that is supported in the Brisk machine greatly facilitated the implementation of the more sophisticated Escher system functions in Booleans. It has been addressed before (see Section 4.10) that at present no techniques which could be used to compile the de nitions of these functions into a lower-level language are available. For this reason, the built-in functions that implement the Escher system functions on the machine level, are tailored to support the current set of system rewrite rules in Booleans. Due to the fact that some of the Escher system functions are supported by special-purpose built-in functions, rather than by compiled code, the abstract machine is very much specialised towards Escher. Due to the fact that Escher is potentially not con uent, several normal forms might exist for a particular expression. The Escher machine is incomplete in the sense that the computed answer represents only one of these normal forms. The Escher machine supports a leftmost outermost reduction strategy, except for the implementation of the rewrite rules which bind variables. The functionality of these rules is supported by a number of built-in system functions (see Section 4.4 for a discussion). An important feature of the machine is that it allows sharing. Depending on whether the evaluation of a variable was involved during a computation, the evaluation built-in functions switch between copying and sharing. To share the evaluation results of variable bindings, refreshing has been introduced. The di erent forms of sharing ensure that an expression is evaluated at most once, provided that its representation on the heap of the abstract machine is shared and it is \safe" to share the evaluation result. EMA has been developed to support the pure rewriting model of Escher. Instead of using the traditional backtracking search procedure that is employed by sequential implementations of logic programming languages, an attempt is made to support a single computation path model. The environment technique allows the representation of di erent bindings for the same variable that correspond to di erent branches in the computation. On the machine level, expressions are represented by graphs of nodes on the heap, together with an environment which holds the bindings for the variables in the expression. To achieve a correct representation, explicit operations like renaming (e.g. when an existential quanti cation is distributed by a disjunction, see page 161) and substituting (when matching on a singleton set, see page 174) are necessary. These operations are computationally expensive and more research is needed to eliminate their use. In close connection with this, the implementation of an occurs check (see Section 4.4.5) should be investigated. Finally, in Section 2.2 two properties have been mentioned to evaluate the design of an abstract machine. First, a good abstract machine can be easily translated into any concrete machine code. Second, it must be easy to generate the abstract machine code from the source code. Even though these criteria are rather informal (and do not make any reference to the performance of the abstract machine), it is still worth discussing these aspects in EMA brie y. Starting with the second point, Escher programs can be translated into EKL (the abstract machine language) using the compilation route described in Chapter 3. The Escher compiler utilises well understood compilation techniques. From this point of view the translation into machine language can be regarded as simple. As discussed earlier, some of the Escher system functions in Booleans cannot be translated into the language

5.7 Summary

181

of the abstract machine. This does, however, not complicate the translation, since the issue can be hidden (and supported by built-in functions) due to the exibility of the underlying machine model. The rst out of the two points given above, concerns the implementation of an abstract machine on a concrete machine. The challenging part regarding EMA is the implementation of the built-in functions, especially those which support the logic features of Escher. Otherwise, the machine-oriented presentation of the transition rules, which de ne to operation of the abstract machine, facilitates a straightforward implementation. The Escher machine has been implemented and tested using a variety of example programs. The next chapter gives a discussion of the performance results obtained from this implementation, together with suggestions for further research to improve these results.

Chapter 6

Results and Future Research In this chapter the results of an implementation of EMA are presented and analysed. Areas for future research are suggested, followed by a discussion of the contribution that this thesis makes to the eld of declarative language implementation.

6.1 Implementation The transition rules given in the previous chapter, together with the machine architecture, formally capture the behaviour of compiled Escher programs, i.e. EKL programs. Compared to, for example the STGM or the JUMP machine, the description of EMA was presented from the point of view of an implementation, on a relatively low level. Due to the fact that the behaviour of all built-in functions has been exposed, EMA can be straightforwardly implemented in a series of extensions to the Brisk machine. To start with, function identi ers need to be introduced into information nodes; they can be integrated easily due to the exible and uniform representation of heap nodes in Brisk. Then, the extra components of the machine state are added to the core Brisk machine components. Finally, the Brisk built-in functions have to be extended and modi ed to support the evaluation strategy of Escher, and new built-in functions need to be added to implement the functionality of the Escher system functions in Booleans. Currently, a substantial part of EMA has been implemented on the basis of the Brisk run-time system. The implementation is, however, at quite an early stage and will bene t from further improvement and optimisation, especially the new machine components. For instance, arguments of functions are currently evaluated one at a time, which means pushing and popping similar stack frames until all arguments are evaluated. The provision of multiple-argument evaluation built-in functions for both ordinary and set-processing functions, as described in Section 3.2.1.1 is expected to speed up the process of evaluation signi cantly. Nevertheless, the basic machine architecture has proved to support the rewriting computational model of Escher. The next section contains a report on the performance results of the current implementation.

184

Results and Future Research

6.2 Performance The EMA implementation has been tested with a series of programs. In this section, some representative and interesting examples are selected for discussion of several aspects related to the performance of EMA.

6.2.1 Purely Functional Programs The question in which a functional programmer will be most interested when deciding whether to use an integrated language is: Will the purely functional programs run with the same eciency as when they are executed on a machine that supports (only) purely functional languages? In the case of the Escher implementation, the answer is that these programs are executed on EMA with an overhead of typically less than 15% for substantial computations. This is the result of a series of tests, conducted using a number of purely functional programs which were executed on both EMA and the Brisk machine. The table below shows, for example, the execution time for the computation of the Fibonacci numbers, and for sorting a list using the mergesort algorithm. EMA Brisk overhead b 10 0.00s 0.00s b 20 0.97s 0.91s 7% b 25 10.58s 9.34s 13% b 30 120.25s 108.06s 11% msort 700 1.39s 1.20s 15% msort 900 1.82s 1.59s 14% Table 6.1: Performance comparison of EMA and Brisk The Brisk machine was chosen for a comparison because it has the same basic architecture as EMA and it uses the same run-time system. Therefore, the experiment will just show the overhead of the extensions and modi cations made to Brisk in order to obtain EMA. Any performance loss, due to the simpli ed model that is used in the Brisk machine compared to the STGM, is not taken into account. So far, this comparison is fair; it is however anticipated that the comparison is not suciently convincing, seeing that real implementations (i.e. unsimpli ed non-research vehicle implementations) of functional languages do not use the Brisk machine, but instead use the STGM (like the Glasgow Haskell compiler for example). It is a topic for further research to nd out which of the optimisations, that have been simpli ed away, can be brought back into the machine model in the context of functional logic computations. Based on such research, a new comparison of an optimised EMA, with an optimised functional machine, should be conducted.

6.2 Performance

185

Analysis of the results from the experiment. It was found that the overhead incurred in EMA is composed of an initial cost for setting up the new machine components, and the ongoing cost of tag testing during a computation. Starting with the rst aspect, the variable ag is initially set to OFF, both quanti ed variables registers are empty, and so is the environment. The tests (e.g. b10 in Table 6.1 above) show the cost of initialisation is so small that it is considered to be negligible. During a purely functional computation, none of the new machine components is modi ed; only the original Brisk machine components are necessary. The variable ag is, however, tested after the evaluation of an argument returns; but, since the entire computation is ground, it remains in its initial state. Hence, full sharing is supported when a purely functional program is executed, and the computation bene ts from all the well known advantages of sharing. The second aspect of the overhead incurred is more serious. The most expensive aspect during a computation is the repeated tag testing, which is done in EMA to distinguish between di erent kinds of evaluation results. In a purely functional setting, in the Brisk machine for example, the result of an argument evaluation is always an expression with a top-level constructor. This can be a data constructor, or one of the built-in function constructors; either way, all of them are treated uniformly. In EMA, a computation can return deferred, which subsequently causes the function that demanded the evaluation to defer as well. When a computation returns, the identi er of the result's function node is tested to determine whether to defer or carry on in the evaluation. In addition, matching on function symbols, which is handled through the look-above strategy, introduces extra testing of function identi ers. The tag testing has been identi ed as a major source of ineciency, which obviously in uences not only the execution of purely functional programs, but the performance of EMA in general. The same problem was present in early implementations of functional languages. There, it was overcome by using the tagless approach introduced, rst, in the STGM. Instead of identifying an object by its tag and processing it accordingly, the code of an object is specialised towards the object's kind. However, in EMA heap nodes already are active objects with specialised code parts. In addition to the taglessness, the STGM uses vectored returns (which were described together with the HOLM in Section 2.2.4.2); where alternative continuations are provided for returning computations, typically on a return stack. It has been reported in several publications [PJ93, Loc93, Cha95] that return vectors are a very ecient way of dealing with di erent kinds of returning computations. The conclusion for improving the EMA performance is that both further specialisation of the code part in heap nodes, and an investigation into the possibilities of employing vectored returns, are necessary in the future. When discussing the behaviour of purely functional programs on EMA, it is worth pointing out that no extra abstract machine instructions are performed due to the architecture of EMA. However, a computation does behave di erently, compared to Haskell for example, when one of those functions, having a di erent de nition in both languages, is used. For instance, boolean expressions, as found in the condition of if,then,else statements, might use boolean functions like (&&), (||) or not. Due to the fact that the de nitions of these functions di er, the computation of an expression like (test a) && (test b || test c)

186

Results and Future Research

is di erent. In Escher, the distribution of conjunction over disjunction would occur before any of the arguments of the boolean functions are evaluated. Afterwards, the evaluation of the resulting disjunction is performed from left to right. In Haskell, which uses the conventional (strict) de nition of conjunction, the evaluation starts with (test a). This di erence in the computation of expressions (which does not a ect the evaluation result) is, however, a feature of the Escher language, and not an e ect of the implementation.

6.2.2 Purely Logic Programs An important question to consider, especially for a logic programmer, is whether purely logic programs are executed as eciently on EMA as on a machine designed for executing a logic language. The answer is: no; at present, they run considerably slower (depending on the kind of program). In this section, the reasons for the ineciencies in EMA are investigated.

Advice on the logic programming style of Escher. First of all, it needs to be pointed out that a program written in a traditional logic language like Prolog has to be transformed slightly before it can be run as an Escher program. This transformation does not just apply to the syntactical di erences, but has more fundamental reasons, based on the fact that Escher uses equality at the top level of a statement where Prolog uses implication. Also, parameters are passed by pattern matching, and the remainder of uni cation is handled by explicit equalities in the body of statements. Particular attention needs to be paid to the fact that, if a function demands a pattern in an argument position in a statement head, residuation will be used during a computation, if the corresponding argument in the call to the function is not suciently instantiated. In the most general case, a predicate in Escher is represented by the completed de nition [Llo87] of the corresponding predicate in Prolog. As an example, the Prolog version of the predicate length is shown below on the left, and the corresponding Escher version is given on the right. length([], 0). length([H|T], L) :length T L1, L is L1 + 1.

length x l = (x == [] && l == 0) || (exists \h t l1 -> x == (h:t) && length t l1 && l == l1 + 1)

However, most of the time, a programmer can exploit the fact that a predicate is used in a certain way, i.e. the form of one of the arguments is known, when the predicate is called. Then, instead of using the completed de nition, a more specialised version can be written. For instance, if it is known that the predicate length will only be called when its rst argument is instantiated to a list, the following de nition is more suitable. length [] l = l == 0 length (h:t) l = exists \l1 -> length t l1 && l == l1 + 1

6.2 Performance

187

It can be seen that the last de nition is very close to the Prolog predicate given above. The key to logic programming in Escher is to take advantage of all information that is available about function calls, so that function de nitions can be written as deterministically as possible.

Conducting an experiment with a purely logic program. Having addressed the logic programming style of Escher, the focus is now turned to the performance of Escher programs on EMA. More detailed information about the form of logic programming in Escher can be found in [Llo98a]. One of the programs, used to test EMA, is the purely logical version of sorting a list with the mergesort algorithm. The results of the experiment are given in Table 6.2. length EMA Sicstus Prolog of list compiled interpreted 100 0.79s 0.01s 0.08s 200 2.80s 0.01s 0.19s 300 6.41s 0.02s 0.32s 500 20.14s 0.04s 0.74s 700 43.03s 0.06s 1.04s 900 73.70s 0.08s 1.29s Table 6.2: Performance comparison of EMA and Sicstus Prolog It is obvious that the execution times measured on EMA are considerably higher than the times taken when the same algorithm, coded in Prolog, is run under Sicstus Prolog. When EMA performs logic computations, operations like renaming the variables in a complete branch of a disjunction, copying and merging environments, and also substituting the local variables when the element of a singleton set is pulled out, are very expensive. All these operations are necessary to ensure the correct representation of the expression under evaluation on the machine level. There are several aspects which create diculties; they will be discussed in the following sections.

Analysis of the results from the experiment. To start with, the expression under evaluation might contain disjunctions, and the same variable might have di erent bindings in each branch of a disjunction. Even though backtracking is, in a sequential implementation, still the most ecient way to traverse a search space, EMA was designed with the aim to support the single computation path model. Backtracking was, therefore, excluded from the beginning. Consequently, (ecient) techniques, like binding a variable through overwriting and trailing the change (as done in the WAM [War83]), could not be used. Instead, as in to or-parallel implementations, it was necessary to represent di erent bindings for the same variable corresponding to di erent

188

Results and Future Research

branches of the search space. Based on a variety of approaches [War87a] which address this aspect of or-parallel implementations, including the binding arrays used in the SRI model [War87b], the environment technique was introduced. It is a rst, and simple, attempt to solve the problem concerning variable bindings for Escher. Leaving aside disjunction for a moment, there is another reason why the environment technique was needed. An Escher computation can have several logic levels (see Section 4.6). The result is that the substitution for a variable only applies to those occurrences of the variable that are within the level in which the respective variable binding was found. As a consequence, it is not obvious which binding is applicable when a variable is accessed. Extra \bookkeeping" (in form of environments and the operations on them) is required to associate bindings to the right logic level. At present, user-de ned functions are restricted1 to constructor-based de nitions. Due to this restriction, user-de ned functions must match on True or False in the boolean case. This means that the (logic) level corresponding to an argument that is expected to reduce to a boolean constructor, either does reduce to a constructor (i.e. it \collapses") or the argument computation defers. In the case of a deferred argument, the missing variable binding is then expected to be found in one of the levels preceding the level of the deferred function call. Through the defer and activation mechanism the deferred computation will be activated when the respective variable is instantiated. Propagating new variable bindings into the local environment of the deferred call is an expensive operation; it is however necessary to distinguish the substitutions that apply on di erent (logic) levels. The techniques used in or-parallel Prolog implementations could not be transferred directly to EMA, because both languages, Escher and Prolog, di er considerably in their functionality, especially concerning the handling of boolean expressions. In Prolog, conjunction is not distributed over disjunction, and negation does not turn a disjunction into a conjunction (of negated arguments). This means for instance, that when a disjunction is encountered, the choice point (which represents a disjunction in the computation tree) is stable; i.e. it does not move \upwards" nor \downwards". Also, disjunctive branches are never united through negation, since negation is typically implemented by negation as failure. Escher is far more powerful and expressive than Prolog. The de nition of the Escher system function (&&) for instance, causes disjunction to move \upwards" (in the graph representing the expression under evaluation) during a computation while distributing conjunctions. A consequence of the leftmost outermost reduction strategy is that arguments are only evaluated once the disjunctions have \bubbled to the top" of the current expression; this is under the assumption that no pattern matching (on a disjunction) takes place there. In addition, the function not pattern matches on disjunctions, producing a conjunction with negated arguments, thus joining previously disjunctive branches of a computation in a conjunction. Likewise, not matches on conjunctions; thus, negation is pushed \downwards", i.e. into the arguments of the resulting disjunction. Needless to say, there is a price to pay for the extra functionality in some of the Escher system functions. To support the extra features of Escher, the bindings for variables, which were found before a distribution point (i.e. before a disjunction is matched on by a conjunction), need to be kept local to the computation in which they occurred. Also, due to the repeated 1

except for set-processing user-de ned functions

6.2 Performance

189

moving \down" and \up" of the control in the expression under evaluation, new bindings found \on the way down" need to be introduced into the environments of the local computations. This is realized through an environment merge of the current environment with the respective local environment. Merging environments is a computationally expensive operation. Environments are implemented as linked lists of binding records; thus, merging is performed with quadratic complexity on the number of variables in the current environment (in the worst case). More research is needed to re ne the environment technique; a data structure with better access complexity is necessary to speed up the operations on environments. Moreover, care needs to be taken concerning quanti ed variables and negated disjunctions. When an existentially quanti ed variable is created, it is represented by a new heap node as discussed in Section 4.1.3. A disjunction can distribute an existential quanti cation, thus pushing the quanti cation inside its arguments. The result is that the quanti ed variables in each branch are logically di erent. To represent this fact on the machine level, the variables in one argument of the disjunction need to be represented by physically di erent heap nodes. Renaming is also an expensive operation because all parts of the argument that refer to such a variable need to be reconstructed using a newly created node representing a variable. Fortunately, only system functions are allowed to match on function symbols, user-de ned functions need to be constructor-based. This restriction localises the problem of renaming to a collection of clearly identi able system functions, where the problem can be treated directly. Renaming would not be necessary if the branches in a disjunction could not be joined together in a conjunction later. The reason is that bindings for locally quanti ed variables are not propagated outside the scope of the quanti er (in this case outside each argument of a disjunction). However, Escher allows the negation of disjunctions;2 thus, previously separated branches can come together in a conjunction, where it is then vital for the logically distinct existentially quanti ed variables to be represented by distinct heap nodes, since this enables them to be bound to di erent values. Figure 6.1 shows an example computation which demonstrates the need for renaming. The computation steps are numbered, and the redex, which is evaluated to produce the next stage, is underlined. Rather than showing a trace of the EMA state (which would consist of all machine reduction steps), Figure 6.1 only shows the function calls \visible" from the Escher level. The environment in the CER is given for the last two computation steps. It is assumed that y is a global variable. The computation in Figure 6.1 shows that without renaming the variable x0 would not have an extra environment entry; it would be mistaken with the variable x due to a name clash.

Possibilities for optimisations. With the environment technique, all aspects of Escher computations can be supported. Two optimisations regarding the treatment of variables and environments have been introduced. One is the refreshing of variable bindings as explained in Section 5.1.4.3. In 2 Remember that the desired form of an answer to a logic computation in Escher is a disjunction of conjunctions of possibly negated equalities that represent the bindings of the variables in the goal term (see Section 2.1.5). To achieve this form, disjunctions have to be pushed to the top level of an answer term, whereas negation needs to be pushed \inside".

190

Results and Future Research (1)

not (exists \x -> p x && not (y==C x))

(2)

not (exists \x -> (r x || s x) && not (y==C x))

(3)

not (exists \x -> (r x && not (y==C x)) || (s x && not (y==C x)))

(4)

not (exists \x -> r x && not (y==C x)) || (exists \x’ -> s x’ && not (y==C x’)) not (exists \x -> r x && not (y==C x)) && not (exists \x’ -> s x’ && not (y==C x’))

(5) (6)

not (exists \x -> x==1 && not (y==C x)) && not (exists \x’ -> s x’ && not (y==C x’))

(7)

not (not (y==C x)) && not (exists \x’ -> s x’ && not (y==C x’))

(8)

y==C x && not (exists \x’ -> s x’ && not (y==C x’)) CER: x/1, y/(C x)

(9)

y==C x && not (exists \x’ -> x’==2 && not (y==C x’)) CER: x/1, y/(C x), x’/...

Figure 6.1: Computation with distributed existentially quanti ed variables short, when a variable, say v, is bound to a computation which only uses variables that are dereferenced in the same environment as v, then the binding record of v can be updated with the evaluation result of the computation v is bound to. Thus, no re-evaluation is necessary, even if the value of v is demanded again later in the computation. As an example, consider the evaluation of the following expression: exists \x -> x == y + 2 && y == 0 && x < 3

Here, the variable x is rst bound to the expression y+2, and y is then bound to 0. When the value of x is demanded for the comparison, the addition is performed and it is safe to rebind x in the current environment to 2. Refreshing is supported through the evaluation built-in functions and by the variable ag as described in Sections 5.4.1.2 and 5.1.6 in Chapter 5. Refreshing is a way of sharing evaluation results of non-ground computations. It can reduce the number of reductions in a computation considerably. For instance, in the case of the mergesort example, by more than half. It is important to point out that it is not safe to update the expression y+2 with its evaluation result, since the computation depended on the value of the variable y. The issue of sharing will be discussed further later on in this section. The second optimisation targets the fact that as long as a disjunction, which previously distributed an existential quanti er, is not negated, the existentially quanti ed variables in both branches of the disjunction do not need renaming. This is because even though these variables are logically distinct, on the machine level no distinction is needed as long as the variables of both branches do not occur in one conjunction. Thus, instead of renaming each time a disjunction distributes an existential quanti er, the renaming is postponed until such a disjunction is negated. The biggest improvement of this optimisation can

6.2 Performance

191

be seen in computations which do not use negation at all. An interesting example is the permutation sort algorithm. The performance gures of sorting a list (worst case) with permutation sort are contained in Table 6.3. length number of execution time of list reductions postponed renaming instant renaming 1 61 0.00s 0.00s 2 203 0.00s 0.00s 3 677 0.01s 0.01s 4 2,709 0.01s 0.07s 5 13,412 0.08s 0.33s 6 79,617 0.50s 2.12s 7 554,099 3.06s 15.80s 8 4,413,743 24.30s 131.37s Table 6.3: Performance of sorting a list using permutation sort on EMA To support late renaming in EMA, a new disjunction node needs to be introduced. This node has to store the variables which need renaming. For the experiment, the new disjunction node was simply given one more argument, which is a pointer to the list of variables contained in the EQVR. Thus, instead of renaming when a disjunction distributes an existential quanti er, one of the new disjunction nodes is created. These behave exactly like ordinary disjunction nodes. The only di erence is that when one of them is negated, the negation has to ensure that renaming is performed on one of the branches. Disjunctions are matched on by universal quanti cation and in set patterns. When 3-ary disjunction nodes are used, the renaming needs to be performed by the continuation functions evalVAnt0 and evalSet0, respectively. Regarding universal quanti cation, the renaming of the remaining universally quanti ed variables, when the antecedent returns as a disjunction, is necessary in any case, and can, therefore, be combined with renaming the existentially quanti ed variables if the disjunction is 3-ary. The renaming in sets is necessary to avoid the known name clashes should both branches of the set body later be combined in one conjunction. Concerning the permutation sort example, there is another point worth noting. Table 6.3 shows the length of the list to be sorted. It was stated that the gures are for the worst case, meaning all branches of the search space need to be traversed in order to nd the solution. In Escher, the goal sort [3,2,7,1] xs, where sort is a predicate which is true when the second argument is the sorted version of its rst argument, returns the equation xs==[1,2,3,7] as answer. Because Escher always returns all solutions, the underlying computation traverses the entire search space. The corresponding Prolog goal is therefore bagof(Xs, sort([3,2,7,1],Xs),L), rather than sort([3,2,7,1], Xs). What forces the evaluation of the entire search space in Escher is the fact that xs is a global variable, and bindings of global variables remain in the computation as deferred equalities (see Section 4.5). In contrast, if the goal was exists \xs -> sort [3,2,7,1] xs, the binding for xs would reduce to True. This, in turn, reduces one of the branches in the search space

192

Results and Future Research

to True, which makes the entire disjunction reduce to True. No answer substitution is returned in this case. Coming back to the performance of logic programs on EMA, there is another aspect which can be improved by introducing a further optimisation. This concerns the implementation of uni cation. At present, two constructors are uni ed by comparing their identi ers; if these are equal, a conjunction of equalities is created for each pair of corresponding arguments. Using the observation that the evaluation of such a conjunction will never encounter a disjunction in an argument (which would then be matched on by a conjunction and distribute), one could encapsulate the respective conjunctions, and use a specialised (stack-based) uni cation procedure on them. This avoids the creation of conjunctions and, also, the repeated calls to the (&&) function. Moreover, such an encapsulated conjunction could be distributed in its \compact" form rather than one argument at a time; if the uni cation fails, the whole encapsulated expression can be reduced to False, which would normally be done stepwise (i.e. for each call to (&&) separately).

Optimisations through more sharing. A strategy which has been successfully used to speed up the evaluation of functional computations is sharing. The basics of sharing have been described in Section 3.2.2. Whereas sharing is well understood in the context of functional programming, it is less often used in the implementation of functional logic programming languages. The JUMP machine integrates sharing via explicit share annotations, which are introduced during compilation. When an object is updated with its evaluation result, the change is trailed to be undone on backtracking. In EMA, the evaluation built-in functions and the variable ag are used to determine whether sharing can be employed safely. If not, copying is used instead of updating, with the e ect that the original expression remains unchanged and needs to be re-evaluated if its value is demanded later. The criteria to decide between sharing or copying during evaluation is whether the value of the expression under evaluation depends on the binding of a variable. If so, copying is needed; otherwise the original expression is ground, and therefore sharing can be used. The approach implemented in EMA resembles, in some aspects, the sharing method developed in [Liu93], which is aimed at resolutionbased functional logic languages, and has its foundations in techniques for AND/OR tree rewriting. To ensure that locally quanti ed variables are represented by distinct heap nodes on the machine level, the nodes created by the built-in functions which introduce new variables are, by default, not shared. This applies, for instance, to the calls to sigma and pi. Hence, the representation of the quanti ed formula in an existential quanti cation, for example, is built each time the call to the corresponding sigma node is evaluated, and thus the purely functional computations within the quanti ed formula cannot be shared. The current approach to sharing in EMA is based on the fact that in functional languages sharing typically is an all-or-nothing choice, meaning that either all parts of an expression are shared or none. In [MS94] a technique, called quasi-sharing, has been introduced which allows parts of an expression to be shared. The technique is applied to narrowing-based functional logic languages and adapts an optimal reduction strategy for lambda terms based on [Lam90]. Quasi-sharing can be integrated into the architecture

6.2 Performance

193

of the G-machine. Instead of using backtracking to rebind variables, alternative choices are explicitly represented in the graph. Quasi-sharing allows to \fan-in" from several different expressions to a shared subexpression (which is usual for sharing), and in addition to \fan-out" from a shared expression to several di erent subexpressions. In this way the duplication of expressions can be avoided (reducing the space complexity of the computation) and sharing is extended over di erent branches in a search space. Unfortunately, no performance gures for quasi-sharing have been published. Due to the fact that Escher is based on rewriting with residuation, it is unclear to which extent quasi-sharing, as described in [MS94], can be employed in EMA. However, EMA would clearly bene t from the integration of such a technique in two ways. First, more of logic computations could be shared with the usual bene ts of sharing. Second, quasi-sharing is an alternative to backtracking and, from this point of view, its integration has the potential to improve the handling of logic computations in EMA. Therefore, a study of how quasi-sharing could be used in EMA, in particular whether the extended functionality given to the boolean system functions in Escher (see Chapter 4 for a discussion) in uences the usefulness of quasi-sharing, is necessary in the future.

Resume on the support for logic programming in EMA. Before summarising this section, it is also important to stress that typical applications of declarative languages require in general more (non-predicate) functions than predicates. This means that some of the predicates which appear in a purely logic program can be much better and more naturally implemented as functions. Comparing the performance results of functional (see Section 6.2.1) with logic computations (both run on EMA), it is clear that non-predicate functions also run far more eciently. A consequence is that a hybrid style of programming is best supported by EMA, since it bene ts from both the speed of functional computations and the extra power of logic programming. To summarise, logic computations are not executed on EMA with the same speed as if they were coded and run in a purely logic programming system like Prolog. Several reasons for this were given in this section. A number of performance improving optimisations were suggested, some of them have been implemented and/or tested. More work is clearly necessary to improve the performance of logic computations. However, it was also pointed out that Escher is far more powerful and expressive than conventional logic programming languages, o ering for example (higher-order) functions, lazy evaluation and set processing in a purely declarative framework. The design of Escher should be seen as an experiment that investigates a novel approach of integrating functional and logic programming aspects by maintaining the simple rewriting computational model of functional languages and extending it with explicit rewrite rules that support logic computations. Due to this rather unconventional style of integration, Escher also has rather unconventional (or new) features, especially when dealing with boolean expressions; one of these being matching on function symbols, another one is joining together branches of disjunctions through negation. To support the full rewriting computational model, it is only to be expected that the extra features of the language require a more sophisticated implementation. In addition, Escher is a relatively young language and the e orts to implement Escher's computational model on an abstract machine reach back three years, rather than twenty- ve for Prolog;

194

Results and Future Research

also there is a considerable di erence in the man power that has been invested. The facts have been brought up here, to bring the current performance results into relation with the amount of e ort spent on the respective implementation. Now that a rst step has been made and the rewriting computational model of Escher is implemented in EMA, further research should be devoted to optimisations and architectural improvements of the machine.

6.2.3 Comparison of Di erent Programming Styles Having looked at both purely functional and purely logic programs, this section is used to compare the two styles of programming regarding their performance. Experiments have been made comparing the mergesort algorithm coded in a purely functional, a purely logic and a hybrid variation. The corresponding programs can be found in Appendix C. Both the logic and the functional logic (i.e. hybrid) style programs return an equality where one argument is a sorted list of integers. To allow a fair comparison, the functional program has been forced to fully evaluate the output list as well. Table 6.4 shows the number of reductions performed on EMA for each programming style on lists with increasing length. The diagram in Figure 6.2 gives a graphical picture of the gures in Table 6.4. length number of reductions in programming style: of list functional logic hybrid 1 42 45 35 10 3,044 5,131 3,262 20 7,436 12,649 7,980 40 17,892 30,132 19,469 50 23,769 40,132 25,911 60 30,108 50,275 32,908 80 41,828 69,906 45,491 100 55,482 92,361 60,614 200 125,633 208,920 137,777 300 201,069 333,958 220,771 500 366,061 606,116 (20.14s) 402,787 700 531,954 882,507 585,620 900 711,649 (1.82s) 1,178,691 (73.70s) 784,100 (4.8s) Table 6.4: Di erent programming styles and the number of reductions The gures in Table 6.4 clearly show that the purely functional programming style is executed most eciently. This result is to be expected since EMA has been developed from a machine architecture that was designed to support the execution of purely functional programs. The logic program uses considerably more reductions compared to the functional program. This is due to the fact that pattern matching is used in statement heads and the remainder of uni cation is done by explicit equalities in statement bodies. As a consequence, additional function calls need to be made to the equality function, and

6.2 Performance

195

to conjunctions and disjunctions which are typically used to connect the equalities. The increase in the number of reductions is therefore a feature of the logic programming style of Escher and is correctly re ected on the machine level.

1400000

Number of reductions

1200000

(73.7s)

1000000 (4.8s)

800000

(1.82s)

600000 400000

logic hybrid functional

200000 0 0

200

400 600 Listlength

800

1000

Figure 6.2: Comparison of di erent programming styles However, leaving aside the di erence in the total number of reductions needed to solve the same problem, Table 6.4 also shows that logic computations take far longer than functional computations. For example, sorting the 900 element list takes 1.82 s and 711,649 reductions, giving an average time of 2.56 ms per 1000 reductions. The logic computation, on the other hand, needs 73.7 s to perform 1,178,691 reductions, which gives an average time of 62.53 ms per 1000 reductions on solving a problem of the same size. The explanation for the di erence lies in the fact that expensive operations, like rebuilding whole branches of a disjunction via renaming, are currently necessary to support logic computations correctly. In Section 6.2.2 other time consuming operations such as environment merging were discussed. It was also pointed out that, in order to make programming in Escher more attractive to the logic programming community, more research is needed to speed up the execution of logic computations on EMA. In this section the demand to increase the eciency of logic computations needs to be emphasised again, this time from a di erent angle. The point is that the current performance results produced by EMA are not uniform, a property that a user should be able to expect from an implementation. It is undesirable that the time taken to perform a certain number of reductions depends on how a problem is coded. Uniformity is the basis for predictability, this means knowing

196

Results and Future Research

that problems of similar size will be solved using similar resources, one of them being time. From this point of view, it is absolutely essential that more work is done to improve the execution of logic programs on EMA. Within the scope of this thesis, most importance has been placed on the timing aspects of EMA. It is realized that other factors, like heap space consumption, are also worth studying (and improving). This is an issue which has been left for further research into the optimisation of EMA. The review of EMA's performance results nishes with this section. The comparison of all three programming styles has shown that functional computations are best supported by EMA, and that more work is needed to increase the performance of logic computations. This will also improve the overall predictability of EMA. Furthermore, the study shows that programs, which use the functional and the logic features of Escher (i.e. hybrid programs) can greatly bene t from the speed of functional computations (see Figure 6.2). This suggests that programmers who use predicates carefully can exploit the best properties of Escher and can expect their programs to be executed eciently on EMA. In fact, Escher has been designed to embed logic computations in functional computations; a strategy which is feasible seeing that many practical applications are much more naturally modelled by (non-predicate) functions than by predicates.

6.3 Set Processing Set processing is an important feature of Escher. In fact, it builds the interface between functional and logic computations as described in [Llo98a]. Set processing is therefore a major ingredient on which the integration of the functional and logic paradigms is based in Escher. However, a well known fact is that in functional languages, lists (and list comprehensions) are typically used to simulate sets. It needs to be stressed, though, that lists and sets have di erent operations and properties; they are, really, di erent data types. The author does not want to engage in the discussion on which one is better, but prefers to see the set-processing facilities of Escher as an alternative approach, which at the same time provides a neat interface between two programming paradigms. In this thesis, a strategy has been developed to translate sets, which are essentially predicates, and set-processing functions into the machine language of EMA. In particular, pattern matching on set patterns (see Sections 3.2.1.2 and 5.6.1) is supported by a cascade of case statements. The basis of computing with set expressions is formed by a

exible representation of sets on the machine level, which allows a set to be used both as data (for pattern matching) and as a function (for application). The built-in function evalSet has been introduced to support the evaluation of arguments which are demanded by set patterns in statement heads. In Escher sets are predicates. It was therefore straightforward to integrate set processing into EMA, once the machine architecture supported the basics of logic programming, which are the non-set-processing rewrite rules in the Escher system module Booleans. As it is based on logic computations, set processing is another feature of Escher which would bene t directly from performance-increasing optimisations for logic computations. It is expected that multisets and multiset-processing functions, which have been introduced into Escher recently [Llo98b], can be supported by EMA in a similar way to ordinary set

6.4 Contribution

197

processing.

6.4 Contribution The rst paragraph of this section is devoted to assessing the practicability of EMA. The remaining paragraphs summarise the contribution made by this thesis towards the implementation techniques for integrated declarative programming languages. One criteria for judging the practical value of an implementation of an integrated language was already pointed out in Chapter 1, namely that it is most desirable for a purely functional (resp. logic) program, written in a functional logic language, to execute with the same eciency as the same program written in a purely functional (resp. logic) language, and run on a single-paradigm implementation. The performance of EMA has been discussed in detail in Section 6.2. As expected, purely functional computations run with a relatively small overhead on EMA. On the other hand, purely logic programs are at present executed considerably slower than comparable programs run on a machine designed for logic computations. To make Escher attractive to both functional and logic programmers, it is essential to have an ecient implementation. EMA should be seen as a rst step towards this. The machine provides a foundation on which Escher programs can be executed. It supports the pure rewriting computational model of Escher. The main objective of this thesis was to nd an ecient implementation of the pure rewriting computational model of Escher. In order to achieve this, the features of Escher were analysed. In fact, a substantial part of the research undertaken was spent on studying rewriting as a computational model, and the e ects of the rewrite rules in the system module Booleans which add logic programming components to a functional rewriting system. Since Escher is considered to be an extension and modi cation of the purely functional language Haskell; a comparison between the two languages has also been conducted to clarify exactly in which aspects they di er (see Section 2.1.6). Based on the analysis results, the Brisk machine was identi ed as a suitable foundation on which to develop an Escher implementation. The Brisk machine was then gradually modi ed and extended to support the integrated functional logic programming style of Escher, resulting in the Escher machine. The study of the rewriting model of Escher and the design of EMA form the main contribution of this thesis. This includes the analysis of the functions in the system module Booleans (contained in Chapter 4), the lifting strategies which integrate quanti cation and set processing into the machine language (see the beginning of Chapter 3 and Section 3.2.3.2), and the representation of sets on the machine level (see Section 3.2.1.2). The modi ed and extended control-related built-in functions, like the free and evalSet families of evaluation functions (see Section 3.2.1), and the new built-in functions which support the rewrite rules in the systems module Booleans (see Section 5.5), form a major part of the EMA design. The representation of variables as active objects together with the defer mechanism, which is also supported by active heap nodes, are new machine features (see Section 5.3). Further, the look-above (see Section 4.1.2), a new technique to implement matching on

198

Results and Future Research

function symbols, has been introduced. It is used to support several rewrite rules for boolean system functions and, also, the matching on set patterns. In Escher, logic computations are embedded in functional computations; all computations have a single computation path. EMA implements this by supporting several \logic" levels in one computation. It uses the environment technique to handle the problem of di erent bindings for the same variable, corresponding to di erent branches of the search space. Finally, switching (see Section 3.2.2) and refreshing (see Section 5.1.6), two new concepts of sharing, have been incorporated successfully into the abstract machine model. A substantial part of EMA has been implemented and tested. Even though the machine needs improvements regarding its performance, the fundamental mechanisms de ned by the rewrite rules, especially those supporting the logic features of Escher, are supported on the machine level. Because EMA is based on a very exible and simple architecture, the machine o ers the opportunity to integrate optimisations used in other functional/logic language implementations, such as the STGM and the JUMP machine. Several other starting points for optimisations (e.g. postponed renaming) and further research (e.g. an investigation of more sophisticated sharing strategies, like quasi-sharing) have been made in the previous sections.

Chapter 7

Conclusion This nal chapter aims to give an overview of what has been learned during the time in which EMA has been developed.

7.1 Understanding the Computational Model of Escher Escher has been designed as a general-purpose, declarative programming language, which in a natural way combines the best features of both functional and logic programming. Escher was introduced in [Llo94] approximately four years ago. It attracted attention because of its novel computational model, rewriting with residuation, which made it stand out in a eld of narrowing-based integrated languages. For a long time, narrowing has been regarded as the (only) operational model of functional logic languages. With Escher, a di erent approach was taken and thus a \competitor entered the market". As a consequence, the main development on the narrowing-based languages, viz. the functional logic language Curry, has been in uenced by the existence of an alternative approach; and so has Escher. Curry is intended to set a standard for integrated languages; it is designed to be a conservative extension of Haskell. The existence of the two languages has triggered numerous comparisons and has thus helped both sides to understand their di erences and common grounds better. The study of the foundations of Escher's computational model has shown that rewriting is indeed a simple computational model, which can be understood by programmers without diculty. By using a rewriting computational model, the functional programming aspects of Escher are naturally integrated, since computing in functional languages is based on rewriting. To add logic programming features, a set of dedicated rewrite rules (i.e. the systems module Booleans) is used in Escher. These rules de ne the behaviour of boolean functions and are therefore the essential ingredient which allows logic computations to be embedded in functional computations. It is at present not absolutely clear how the rewrite rules of Escher relate to narrowing. However, since both computational models solve the same class of problems, they must have common principles in their foundations. When Escher was designed, little attention was given to the restrictions (like constructorbased rewriting) with which traditional functional (logic) programming languages are built.

200

Conclusion

The idea was to experiment with a new approach, rather than combine what is already well known and understood. The outcome is a highly expressive and powerful integrated language with lots of novel features, one of those is the elegant way Escher handles set processing. From the beginning, it was an unresolved issue whether Escher could be implemented eciently. This uncertainty was due to the fact that implementation techniques to support the new features of Escher were not available at that time. A comprehensive study of the rules in the Booleans module was necessary to understand how the various rewrite rules work together in supporting logic computations. The fact that most of the functions in this module are not constructor-based turned out to be a major challenge for an (ecient) implementation. Several pitfalls were encountered along the way. For instance, there are various rules for binding variables. Depending on whether (and how) a variable is quanti ed, these rules treat an equality in di erent ways and consequently produce di erent outcomes. Also, due to the outermost reduction strategy and the rule which propagates existential quanti cation to the \top" of a conjunction, variables which are not directly quanti ed are never bound to directly quanti ed variables; the binding is always made the other way round. This is quite a subtle and important distinction, which underlines that it is important to analyse the rewrite rules in Booleans as a whole, not in isolation. A demanding feature of Escher is that several logic levels can exists at the same time. This alone, even without disjunctions, rules out updating as a means of substitution application, which was traditionally used in the WAM and in implementations of functional logic languages like the JUMP machine as well. A binding for a variable applies just to the occurrences of that variable in the conjunction in which the binding was found. For the same reason, deferred computations cannot be attached (as done in the HOLM) to the (unbound) variable which caused them to defer. An issue, which has caused a lot of problems on the implementation level, is that negation in Escher can turn disjunctions into conjunctions, thus joining previously disjoined branches of a search space. The e ects of this issue on the performance of EMA were discussed in detail in Section 6.2.2. All the points mentioned up to now show that, despite the basic computational model being very simple, the rules which de ne the handling of logic computations are rather sophisticated; in particular the interplay between some of the rules is quite subtle. The ecient implementation of such a system is clearly not a trivial task.

7.2 Classifying EMA with Criteria from Or-parallel Implementations Considering that three years ago it was not clear whether Escher could be implemented eciently, the current state of the abstract machine is a big step forward. Even though problems remain to be solved and improvements plus further research are necessary to increase the eciency of EMA, this work shows that it is possible to support the novel features of Escher on an abstract machine which has the potential for eciency. In an attempt to classify EMA using the standard criteria for or-parallel implementations [GJ90] in logic programming, it was found that EMA has features in common with machines that are classi ed under constant task creation. Such a classi cation of EMA is possible because

7.3 Programming in Escher

201

a sequential implementation (like EMA) can be seen as a special case of an or-parallel implementation. The other criteria for the classi cation are the cost of variable access and task switching. Constant time means here that the time needed for an operation is independent from the number of nodes in the or-parallel tree and the size of the terms in the tree. The cost of task creation is the time taken to construct the environment for a new branch in the search tree. It is indeed the case that for the evaluation of arguments (of disjunctions), the current environment is simply propagated into the argument level. Task switching can be compared to re-evaluating a previously deferred computation; due to the cost of repeated environment merging until the leaf of a deferred branch is reached, task switching is a non-constant time operation. Even though, variable access does not depend on where in the graph under evaluation the value of a variable is needed, to bind a variable in a readable environment, the environment needs to be copied, which is a nonconstant time operation. A new challenge is to transfer the ideas developed for EMA to a concurrent or truly parallel implementation. The Brisk machine now has an extension that supports di erent computation threads; it could be used as a starting point. Discussing the future of EMA, it is clear that the machine architecture, especially the operations of the built-in functions, is tailored to support the Escher system rewrite rules. This limits the applicability of the abstract machine to executing Escher programs. It is hoped that some of the more general ideas, such as matching on function symbols, will be useful in other contexts.

7.3 Programming in Escher In a sense, programming in Escher is straightforward once the basic ideas have been understood. The system rewrite rules have been tested on a variety of examples and have shown to work satisfactorily in practical applications. However, in some cases Escher does not return the answer a user would expect. Take for example the following predicate de nition: isin :: Town -> County -> Bool isin x y = (x==Salisbury && y==Wiltshire) || (x==Bristol && y==Avon)

Appropriate data declarations for Town and County are assumed. Supposing the question: \Is the set of all cities which are not in Wiltshire an empty set?" needs to be answered. Using the de nition of the function emptyset which was given in Section 2.1.3.2, an unexperienced programmer might use the following goal: emptyset {x | not (isin x Wiltshire)}

This goal does not reduce to the expected answer False. Rather, due to the fact that the negation (eagerly) rewrites disjunctions into conjunctions and the other way round (which causes disjunctions to reach the top-level of the set body where they are matched on by the de nition of emptyset) the computation returns the expression:

202

Conclusion emptyset {x | not (x==Salisbury) && not (x==Bristol)} && emptyset {x | not (x==Salisbury)}

Experienced programmers would use a generate and test approach to solve the problem. If the query is asked in the following alternative way: emptyset {x | exists \y -> isin x y && y /= Wiltshire}

the system returns False as expected. What can be seen from this example is that even though in a large number of application programs Escher does compute reasonable answers, in some cases care needs to be taken when a problem is formulated. Clearly, it is necessary to educate programmers and give advice on good programming style in Escher. However, a discussion of this aspect is out of the scope of this work.

7.4 Outlook on Application Areas for Escher Escher, being designed as a general purpose programming language, has a variety of application areas. As an extension and modi cation of Haskell, the problem classes which are solved using a functional language, can also be solved using Escher. Interesting new application areas come from the ability of Escher to support logic programming. On one side, Escher o ers an attractive approach to meta-programming, based a ground representation similar to that of Godel [HL94]. The design of the Escher meta-programming facilities has not yet started, but a proposal has been made in [Bow98]. It is anticipated that meta-programming in an integrated language like Escher is more powerful due to the uniform and simple structure of Escher programs. A challenging application area for an integrated language is machine learning. Traditionally, rst-order logic languages are used as representation languages in this eld. As a consequence, what an algorithm can \learn" is restricted to the rst-order case and predicates. Using a functional logic language like Escher instead, introduces (non-predicate) functions into machine learning; an aspect which has not been (much) considered in the traditional approach. On the other hand, the higher-order features of Escher, especially the way set processing is handled, make it natural to use higher-order functions when problems are represented. This in turn is expected to enable higher-order learning, which will add a new dimension to the area of machine learning.

Appendix A

Declarative Semantics of Escher This appendix contains an overview of the declarative semantics of Escher, it is based on [Llo98a]. The basic logic of Escher is an extension of Church's simple theory of types [Chu40]. In what follows, Church's logic is referred to as type theory . There are several accessible accounts of type theory. The original work by Church can be found in [Chu40]. A more comprehensive account of higher-order logic is contained in [And86], a more recent account, including a discussion of higher-order uni cation, in [Wol93], or the summaries in [Nad87] and [NM94]. A more detailed account of (the extension of) type theory underlying Escher is contained in [Llo95]. For the purposes of this thesis, a brief outline of the main concepts of (extended) type theory is given here. If more detail is required, the reader is referred to the above accounts. It is assumed that a set of type constructors C of various arities is given. Included in C are the type constructors 1 and o both of arity 0. The domain corresponding to 1 is some canonical singleton set and the domain corresponding to o is the set containing just True and False . The main purpose of having 1 is so that constants can be given types in a uniform way as for functions. The type o is the type of propositions. The types of the logic are built up in the standard way from the set of type constructors and a set of type variables, using the symbol ! (for function types) and  (for product types). Note that the logic is polymorphic, an extension not considered by Church. The terms of type theory are the terms of the typed -calculus, which are formed in the usual way by abstraction and application from a given set of functions F having types of the form ! and a set of variables V . Constants are regarded to be 0-ary functions. The set S of simply typed lambda terms is de ned as follows:

M ::= f j x j (x :M ) ! j (M1 ! M2 ) where and are types, f is a function from F and x is a variable from V . The type annotations may be dropped if they are not important or can be inferred. A term of type o is called a formula . In type theory, one can introduce the usual connectives and quanti ers as functions of appropriate types. Thus the connectives conjunction, ^, and disjunction, _, are functions of type o ! o ! o and the (generalized) existential quanti er, , and

204

Declarative Semantics of Escher

universal quanti er, , have type ( ! o) ! o. (The ! is right associative.) Terms of the form (x:t) are written as 9x:t and terms of the form (x:t) are written as 8x:t. In addition, if t is of type o, the abstraction x:t is written fx j tg to emphasize its intended meaning as a set. The notation fg means fx j Falseg. A set abstraction of the form fx j (x = t1 ) _ : : : _ (x = tn )g is abbreviated to ft1 ; : : : ; tn g, where x is not free in any ti . There is also a tuple-forming notation < : : : >. Thus, if t1 ; : : : ; tn are terms of type 1 ; : : : ; n , respectively, then < t1 ; : : : ; tn > is a term of type 1  : : :  n. The term f (< t1; : : : ; tn >) is abbreviated to f (t1; : : : ; tn ), where f is a function. Thus, although all functions are unary, one can e ectively use the more common syntax of n-ary functions and refer to the \arguments" of a function (rather than the argument). Functions mapping from the domain of type 1 have their argument omitted. Type theory has an elegant and useful model theory. The key idea, introduced by Henkin in his paper [Hen50] which proved the completeness of type theory, is that of a general model . General models are a natural generalization of rst-order interpretations. Very brie y, leaving aside the extension to handle polymorphism, the model theory of type theory is as follows. The domain for a nullary type constructor in C is some set, the domain for a type of the form ! is a set of functions mapping from the domain of type to the domain of type , and the domain for a type of the form  is the cartesian product of the domains of type and . A function of type  is assigned some element of the domain of type  and the meaning of the connectives and quanti ers is what one would expect. From this, the notions of general model, satisfaction, validity, model of a set of formulas, and so on, can be given in a rather straightforward way. (See, for example, [And86], [Hen50], [Llo95], or [Wol93] for the details.) Henkin's concept of a general model is proposed as appropriate for capturing the intended interpretation of an application. Finally, Table A.1 shows the correspondence between various symbols and expressions of type theory in the left column and their equivalent in the syntax of Haskell and Escher in the right column.

205

1

o !  =

: ^ _ !

x:t  

9x:t 8x:t fx j tg 2

< s; t >

() Bool Sigma -> Tau (Sigma, Tau) == not && || ==> \x -> t exists forall exists \x -> t forall \x -> t {x | t} in (s, t)

Table A.1: Mapping symbols and expressions from type theory into Escher syntax

Appendix B Booleans

Module

module Booleans where { data Bool = True | False; data (a -> Bool) = Inc a (a -> Bool); --- Inc is used in the extensional representation of sets. -- Inc x s = {x} union s. (==) :: a -> a -> Bool; f x1 ... xn == f y1 ... yn = (x1 == y1) && ... && (xn == yn); --- where n >= 0; and f is a data constructor. -- (If n=0, then the RHS is True.) f x1 ... xn == g y1 ... ym = False; --- where n and m >= 0; f and g are data constructors; -- and f is distinct from g. y == x = x == y; --- where x is a variable; and y is not a variable. (x1,...,xn) == (y1,...,yn)

=

(x1 == y1) && ... && (xn == yn);

{x | u} == {y | v} = ({x | u} `subset` {y | v}) && ({y | v} `subset` {x | u});

208

Booleans

(/=) :: a -> a -> Bool; x /= y

=

not (x == y);

(&&) :: Bool -> Bool -> Bool; True && x

=

x;

x && True

=

x;

False && x

=

False;

x && False

=

False;

(x || y) && z

=

(x && z) || (y && z);

x && (y || z)

=

(x && y) || (x && z);

x && (exists \x1 ... xn -> v) && y = exists \x1 ... xn -> x && v && y; --- where no xi is free in x or y; and x or y may be absent. y && (x == u) && z = y{x/u} && (x == u) && z{x/u}; --- where x is a variable; x is not free in u; -- x is free in y or z; u is free for x in y and z; -- and y or z may be absent.

(||) :: Bool -> Bool -> Bool; True || x

=

True;

x || True

=

True;

False || x

=

x;

x || False

=

x;

--

Rewrites for the conditional syntactic sugar.

if exists \x1 ... xn -> True then x else y exists \x1 ... xn -> x;

=

Module

209 if exists \x1 ... xn -> False then x else y

=

y;

if exists \x1 ... xn -> x && (xi == u) && y then z else v = if exists \x1 ... xi-1 xi+1 ... xn -> x{xi/u} && y{xi/u} then z{xi/u} else v; --- where xi is not free in u; u is free for xi in x, y and z; -- and x or y (or both) may be absent. if exists \x1 ... xn -> x || (xi == u) || y then z else v exists \x1 ... xn -> (x || (xi == u) || y) && z; --- where x or y (or both) may be absent.

=

not :: Bool -> Bool; not False not True

= =

not (not x)

True; False; =

x;

not (x || y)

=

(not x) && (not y);

not (x && y)

=

(not x) || (not y);

exists :: (a -> Bool) -> Bool; exists \x1 ... xn -> True exists \x1 ... xn -> False

= =

True; False;

exists \x1 ... xn -> x || y = (exists \x1 ... xn -> x) || (exists \x1 ... xn -> y); exists \x1 ... xn -> x && (xi == u) && y = exists \x1 ... xi-1 xi+1 ... xn -> x{xi/u} && y{xi/u}; --- where xi is not free in u; u is free for xi in x and y; -- and x or y (or both) may be absent. -- If n=1, then the RHS is x{x1/u} && y{x1/u}. -- If both x and y are absent, then the RHS is True.

210

Booleans

forall :: (a -> Bool) -> Bool; forall \x1 ... xn -> False ==> u

=

True;

forall \x1 ... xn -> x && (xi == u) && y ==> v = forall \x1 ... xi-1 xi+1 ... xn -> x{xi/u} && y{xi/u} ==> v{xi/u}; --- where xi is not free in u; u is free for xi in x, y and v; -- x or y (or both) may be absent; and, if n=1, then both x -- and y are absent. -- If n>1 and both x and y are absent, then the RHS is -- forall \x1 ... xi-1 xi+1 ... xn -> True ==> v{xi/u}. -- If n=1, then the RHS is v{x1/u}. forall \x1 ... xn -> (u || v) ==> w = (forall \x1 ... xn -> u ==> w) && (forall \x1 ... xn -> v ==> w);

union :: (a -> Bool) -> (a -> Bool) -> (a -> Bool); s `union` t

=

{x | (x `in` s) || (x `in` t)};

inters :: (a -> Bool) -> (a -> Bool) -> (a -> Bool); s `inters` t

=

{x | (x `in` s) && (x `in` t)};

minus :: (a -> Bool) -> (a -> Bool) -> (a -> Bool); s `minus` t

=

{x | (x `in` s) && (not (x `in` t))};

subset :: (a -> Bool) -> (a -> Bool) -> Bool; {} `subset` s {u} `subset` s

=

True; =

u `in` s;

{x | u || v} `subset` s = ({x | u} `subset` s) && ({x | v} `subset` s);

Module

211 superset :: (a -> Bool) -> (a -> Bool) -> Bool; s `superset` {}

=

s `superset` {u}

=

True; u `in` s;

s `superset` {x | u || v} = (s `superset` {x | u}) && (s `superset` {x | v});

power :: (a -> Bool) -> (a -> Bool) -> Bool; --- Power set function. power {} power {u}

= =

{{}}; {{}, {u}};

power {x | u || v} = {s | exists \l r -> l `in` (power {x | u}) && r `in` (power {x | v}) && s == l `union` r};

mapset :: (a -> b) -> (a -> Bool) -> (b -> Bool); --- Analogue of map for set processing. mapset f {} mapset f {u}

= =

{}; {f u};

mapset f {x | u || v} = (mapset f {x | u}) `union` (mapset f {x | v});

in :: a -> (a -> Bool) -> Bool; --- Set membership. -- y `in` {x | u} = u{x/y}; --- where y is free for x in u. y `in` s

=

s y;

212

Booleans

linearise :: (a -> Bool) -> (a -> Bool); --- Convert from standard to extensional representation -- of a set. linearise {}

=

linearise {x}

{}; =

Inc x {};

linearise {x | u || v} = combine (linearise {x | u}) (linearise {x | v});

combine :: (a -> Bool) -> (a -> Bool) -> (a -> Bool); --- Union of sets (in extensional representation). combine {} s

=

s;

combine (Inc x s) t

=

Inc x (combine s t);

delinearise :: (a -> Bool) -> (a -> Bool); --- Convert from extensional to standard representation -- of a set. delinearise {}

=

{};

delinearise (Inc x s)

=

{x} `union` (delinearise s);

remove :: a -> (a -> Bool) -> (a -> Bool); --- Delete an element from a set. remove x {}

=

{};

remove x (Inc y s) = if x == y then remove x s else Inc y (remove x s);

deletedup :: (a -> Bool) -> (a -> Bool); --- Delete duplicates from an extensional representation -- of a set.

Module

213 deletedup {}

=

{};

deletedup (Inc x s) }

=

Inc x (deletedup (remove x s));

Appendix C

Test Programs C.1 Mergesort Program C.1.1 Mergesort Algorithm: Purely functional coding module Main where import PreludeEscher () import List(head, tail, length, take, drop)

main :: [Int] main = force (sort intMerge [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15])

force :: [Int] -> [Int] force l = if (ok l) then l else [] ok :: [Int] -> Bool ok [] = True ok (x:xs) = ok xs

sort :: ([a] -> [a] -> [a]) -> [a] -> [a] sort merge xs = if (length xs) < 2 then xs else merge (sort merge (firsthalf xs)) (sort merge (secondhalf xs))

216 intMerge :: [Int] -> [Int] -> [Int] intMerge [] ys = ys intMerge (x:xs) [] = (x:xs) intMerge (x:xs) (y:ys) = if (x > y) then (y:(intMerge (x:xs) ys)) else (x:(intMerge xs (y:ys)))

firsthalf :: [a] -> [a] firsthalf xs = take (length xs `div` 2) xs

secondhalf :: [a] -> [a] secondhalf xs = drop (length xs `div` 2) xs

C.1.2 Mergesort Algorithm: Purely logic coding module Main where import PreludeEscher ()

main :: [Int] -> Bool main xs = sort [1,2,3,4,5,6,7,8,9,11,12,13] xs

sort :: ([a] -> [a] -> [a] -> Bool) -> [a] -> [a] -> Bool sort merge xs ys = exists \n -> length xs n && if n < 2 then ys == xs else exists \us vs ws zs -> firsthalf xs ws && secondhalf xs zs && hosort merge ws us && hosort merge zs vs && merge us vs ys

-- Version without higher-order features.

Test Programs

C.1 Mergesort Program -- Used for comparison with Prolog. sort' :: [a] -> [a] -> Bool sort' xs ys = exists \n -> length xs n && if n < 2 then ys == xs else exists \us vs ws zs -> firsthalf xs ws && secondhalf xs zs && sort' ws us && sort' zs vs && intMerge us vs ys

intMerge :: [Int] -> [Int] -> [Int] -> Bool intMerge [] ys zs = zs == ys intMerge (x:xs) [] zs = zs == (x:xs) intMerge (x:xs) (y:ys) zs = if (x > y) then exists \us -> intMerge (x:xs) ys us && zs == (y:us) else exists \vs -> intMerge xs (y:ys) vs && zs == (x:vs)

firsthalf :: [a] -> [a] -> Bool firsthalf xs ys = exists \n n1-> length xs n && n1 == (n `div` 2) && take n1 xs ys

secondhalf :: [a] -> [a] -> Bool secondhalf xs ys = exists \n n1 -> length xs n && n1 == (n `div` 2) && drop n1 xs ys

take :: Int -> [a] -> [a] -> Bool take n [] ys = ys == [] take n (x:xs) ys = if n > 0 then exists \zs -> ys == x:zs &&

217

218

Test Programs exists \n1 -> n1 == n - 1 && take n1 xs zs else ys == []

drop :: Int -> [a] -> [a] -> Bool drop n [] ys = ys == [] drop n (x:xs) ys = if n > 0 then exists \n1 -> n1 == (n - 1) && drop n1 xs ys else ys == (x:xs)

length :: [a] -> Int -> Bool length [] n = n == 0 length (x:xs) n = exists \n1 -> length xs n1 && (n == (n1 + 1))

C.1.3 Mergesort Algorithm: Functional logic programming style module Main where import PreludeEscher () import List(head, tail, length, take, drop)

main :: [Int] -> Bool main xs = sort intMerge [1,2,3,4,5,6,7,8,9,10,11,12,13] xs

sort :: ([a] -> [a] -> [a] -> Bool) -> [a] -> [a] -> Bool sort merge xs ys = if length xs < 2 then ys == xs else exists \us vs -> sort merge (firsthalf xs) us && sort merge (secondhalf xs) vs && merge us vs ys

intMerge :: [Int] -> [Int] -> [Int] -> Bool intMerge [] ys zs = zs == ys intMerge (x:xs) [] zs = zs == (x:xs) intMerge (x:xs) (y:ys) zs = if (x > y) then exists \us -> intMerge (x:xs) ys us && zs == y:us else

C.2 Permutationsort Program exists \vs -> intMerge xs (y:ys) vs && zs == x:vs

firsthalf :: [a] -> [a] firsthalf xs = take (length xs `div` 2) xs

secondhalf :: [a] -> [a] secondhalf xs = drop (length xs `div` 2) xs

C.2 Permutationsort Program module Main where import PreludeEscher () main :: [Int] -> Bool main xs = sort [3,2,4,7,1,8,5,6] xs -- main = exists \xs -> sort [3,1,4,2] xs -- alternative goal sort :: [Int] -> [Int] -> Bool sort xs ys = perm xs ys && sorted ys

sorted sorted sorted sorted

:: [Int] -> Bool [] = True [x] = True (x:y:ys) = x < y && sorted (y:ys)

perm :: [Int] -> [Int] -> Bool perm [] l = l == [] perm (h:t) l = exists \u v r -> perm t r && split r u v && l == concat u (h:v)

concat :: [Int] -> [Int] -> [Int] concat [] y = y concat (h:t) y = h:concat t y

split :: [Int] -> [Int] -> [Int] -> Bool

219

220

Test Programs

split [] y z = y == [] && z == [] split (h:t) y z = y == [] && z == h:t || exists \w -> y == h:w && split t w z

Bibliography [AK90]

[AK91] [AKLN87] [And86] [Aug85]

[Aug87] [BCM89]

[BGM89] [BHI97] [Bow98]

Hassan Ait-Kaci. An Overview of LIFE. In J. W. Schmidt and A. A. Stogny, editors, Proceedings of the International Workshop on Next Generation Information System Technology, Lecture Notes in Computer Science 504, pages 42{58. Springer-Verlag, 1990. Hassan Ait-Kaci. Warren's Abstract Machine: A Tuturial Reconstruction. The MIT Press, 1991. H. Ait-Kaci, P. Lincoln, and R. Nasr. Le Fun: Logic, Equations, and Functions. In 4th IEEE International Symposium on Logic Programming, pages 17{23, San Francisco, 1987. Peter B. Andrews. An Introduction to Mathematical Logic and Type Theory: To Truth through Proof. Academic Press, 1986. Lennart Augustsson. Compiling Pattern Matching. In G. Goos and J. Hartmanis, editors, Functional Programming Languages and Computer Architecture, Lecture Notes in Computer Science 201, pages 368{381. Springer-Verlag, September 1985. L. Augustsson. Compiling lazy functional languages, part II. PhD thesis, Department of Computer Science, Chalmers University, Goteborg, Sweden, 1987. P. G. Bosco, C. Cecchi, and C. Moiso. An extension of WAM for K-LEAF: A WAM-based Compilation of Conditional Narrowing. In Proceedings of the 6th International Conference on Logic Programming (ICLP '89), pages 318{336, Lisbon, Portugal, June 1989. MIT Press. P. G. Bosco, E. Giovanetti, and C. Moiso. Narrowing vs. SLD-resolution. Journal of Theoretical Computer Science, 59:3{23, 1989. A.F. Bowers, P.M. Hill, and F. Iba~nez. Resolution for logic programming with universal quanti ers. In PLILP 97. Springer-Verlag, 1997. Antony F. Bowers. E ective Meta-programming in Declarative Languages. PhD thesis, University of Bristol, Department of Computer Science, January 1998.

222 [BR92] [Car87] [CCM85]

[Cha94]

[Cha95] [Chu40] [CL94] [CL95] [DJ90] [DL86] [FH88] [FW87]

[GJ90]

BIBLIOGRAPHY Egon Borger and Dean Rosenzweig. The WAM - De nition and Compiler Correctness. Technical Report TR-14/92, University Pisa, Department of Computer Science, June 1992. Mats Carlsson. Freeze, Indexing, and Other Implementation Issues in the WAM. In Fourth International Conference on Logic Programming, pages 40{58. University of Melbourne, MIT Press, May 1987. G. Cousineau, P. L. Curien, and M. Mauny. The Categorical Abstract Machine. In Jean-Pierre Jouannaud, editor, Functional Programming Languages and Computer Architecture, volume 201 of Lecture Notes in Computer Science 201, pages 50{64. Springer-Verlag, September 1985. Manuel M. T. Chakravarty. A self-scheduling, non-blocking parallel Abstract Machine for non-strict Functional Languages. In Proceedings of the International Workshop on the Implementation of Functional Languages, University of East Anglia, 1994. Manuel M.T. Chakravarty. Higher-Order Logic as a Basis for Abstract Machines Implementing Functional Logic Languages. Forschungsgruppe Softwaretechnik, Technische Universitat Berlin, April 1995. Alonzo Church. A Formulation of the Simple Theory of Types. Journal of Symbolic Logic, 5:56{68, 1940. Manuel M. T. Chakravarty and Hendrik C. R. Lock. The JUMP-machine: A Generic Basis for the Integration of Declarative Paradigms. In Proceedings of the Post-ICLP'94 Workshop, 1994. Manuel M.T. Chakravarty and Hendrik C. R. Lock. Towards the Uniform Implementation of Declarative Languages. Forschungsgruppe Softwaretechnik, Technische Universitat Berlin, June 1995. Nachum Dershowitz and Jean-Pierre Jouannaud. Rewrite Systems. In Jan van Leeuwen, editor, Handbook of Theoretical Computer Science, volume B, pages 243{320. Elsevier, 1990. D. DeGroot and G. Lindstrom, editors. Logic Programming: Relations, Functions and Equations. Prentice Hall, 1986. Anthony J. Field and Peter G. Harrison. Functional Programming. Addison-Wesley, 1988. Jon Fairbairn and Stuart Wray. Tim - A simple lazy Abstract Machine to Execute Supercombinators. In Conference on Functional Programming Languages and Computer Architecture, Lecture Notes in Computer Science 274. Springer-Verlag, 1987. Gopal Gupta and Bharat Jayaraman. On Criteria of Or-Parallel Execution Models of Logic Programs. In S. Debray and M. Hermanegildo,

BIBLIOGRAPHY

[GLMP91] [Han90]

[Han91]

[Han94] [Han98] [Hen50] [HF97] [HL94] [Hol89] [HS97] [Hug82] [JD89] [Joh85]

223

editors, Proceedings of the 1990 North American Conference on Logic Programming, pages 737{756. MIT Press, 1990. E. Giovanetti, G. Levi, C. Moiso, and C. Palamidessi. Kernel LEAF: A Logic plus Functional Language. Journal of Computer and System Sciences, 42(2):139{185, 1991. Michael Hanus. Compiling Logic Programs with Equality. In P. Deransart and J. Maluszynski, editors, Proceedings of Programming Language Implementation and Logic Programming, Lecture Notes in Computer Science 456, pages 387{401. Springer-Verlag, August 1990. Michael Hanus. Ecient Implementation of Narrowing and Rewriting. In Proceedings of the International Workshop on Processing Declarative Knowledge, Lecture Notes in Arti cial Intelligence 567, pages 344{365. Springer-Verlag, 1991. Michael Hanus. The Integration of Functions into Logic Programming: From Theory to Practice. Journal of Logic Programming, 19&20:583{628, 1994. Michael Hanus. Curry: An Integrated Functional Logic Language. Curry Report Draft, June 1998. Leon Henkin. Completeness in the Theory of Types. Journal of Symbolic Logic, 15(2):81{91, 1950. Paul Hudak and Joseph H. Fasel. A Gentle Introduction to Haskell, Version 1.4. Available at http://www.haskell.org/tutorial/, March 1997. Patricia M. Hill and John W. Lloyd. The Godel Programming Language. Logic Programming Series. MIT Press, 1994. Ste en Holldobler. Foundations of Equational Logic Programming. Lecture Notes in Arti cial Intelligence 353. Springer-Verlag, 1989. Ian Holyer and Eleni Spiliopoulou. The Brisk Machine: A Simpli ed STG Machine. University of Bristol, Department of Computer Science, September 1997. John Hughes. Super Combinators - A new Implementation Technique for applicative Languages. In ACM Conference on LISP and Functional Programming, pages 1{10, Pittsburgh, 1982. Alan Josephson and Nachum Dershowitz. An Implementation of Narrowing. Jounal of Logic Programming, 6(1 & 2):57{77, 1989. Thomas Johnsson. Lambda lifting: Transforming programs to recursive equations. In Jean-Pierre Jouannaud, editor, Proceedings IFIP Conference on Functional Programming and Computer Architecture, Lecture Notes in Computer Science 201, pages 190{205. Springer-Verlag, 1985.

224 [Joh87]

BIBLIOGRAPHY

Thomas Johnsson. Compiling lazy functional languages. PhD thesis, Department of Computer Science, Chalmers University, Goteborg, Sweden, 1987. [KLMNRA90a] Herbert Kuchen, Rita Loogen, Juan Jose Moreno-Navarro, and Mario Rodrguez-Artalejo. Graph-based Implementation of a Functional Logic Language. In Proceedings of the 3rd European Symposium on Programming, Lecture Notes in Computer Science 432, pages 271{290. SpringerVerlag, May 1990. [KLMNRA90b] Herbert Kuchen, Rita Loogen, Juan Jose Moreno-Navarro, and Mario Rodrguez-Artalejo. Lazy Narrowing in a Graph Machine. Technical Report 90-11, Technical University of Aachen (RWTH Aachen), 1990. Published by Springer-Verlag in Lecture Notes in Computer Science 463. [KMNH92] Herbert Kuchen, Juan Jose Moreno-Navarro, and Manuel V. Hermenegildo. Independent AND-parallel Implementation of Narrowing. In Proceedings of the 4th International Symposium on Programming Language Implementation and Logic Programming, Lecture Notes in Computer Science 631, pages 24{38. Springer-Verlag, 1992. [KN90] Andreas Krall and Ulrich Neumerkel. The Vienna Abstract Machine. In Programming Language Implementation and Logic Programming, Lecture Notes in Computer Science 456, pages 121{136. Springer-Verlag, 1990. [Kog91] Peter M. Kogge. The Architecture of Symbolic Computers. McGraw-Hill, 1991. [Lam90] John Lamping. An algorithm for optimal lambda calculus reduction. In Conference Record, ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages. ACM Press, January 1990. [Lan64] Peter J. Landin. The mechanical Evaluation of Expressions. Computer Journal, 6:308{320, 1964. [Liu93] Feixiong Liu. Towards Lazy Evaluation, Sharing and Non-determinism in Resolution Based Functional Logic Languages. In Proceedings of the Conference on Functional Programming Languages and Computer Architecture, pages 201{209, New York, NY, USA, June 1993. ACM Press. Also available at FB Informatik, Universitat Oldenburg, Germany. [Llo87] John W. Lloyd. Foundations of Logic Programming. Springer-Verlag, second edition, 1987. [Llo94] John W. Lloyd. Combining functional and logic programming languages. In Proceedings of the 1994 International Logic Programming Symposium ILPS'94, pages 43{57. MIT Press, 1994. [Llo95] John W. Lloyd. Declarative Programming in Escher. Technical Report CSTR-95-013, University of Bristol, Department of Computer Science, June 1995. Available at http://www.cs.bris.ac.uk.

BIBLIOGRAPHY [Llo98a] [Llo98b] [Loc93] [Loo91]

[Loo93] [MN94] [MNRA92] [MS94]

[Muc92]

[Nad87] [NM94]

[O'D85]

225

John W. Lloyd. Programming in an Integrated Functional Logic Language. To appear in Journal of Functional and Logic Programming, 1998. John W. Lloyd. Programming with Multisets. Working Draft, September 1998. Hendrik C. R. Lock. The Implementation of Functional Logic Programming Languages. Number 208 in GMD-Berichte. Oldenbourg Verlag, 1993. Rita Loogen. From Reduction Machines to Narrowing Machines. In Colloquium on Combining Paradigms for Software Development | CCPSD, TAPSOFT, Lecture Notes in Computer Science 494, pages 438{454. Springer-Verlag, 1991. Rita Loogen. Relating the Implementation Techniques of Functional and Functional Logic Languages. New Generation Computing, 11:179{215, 1993. Juan Jose Moreno-Navarro. Expressivity of Functional Logic Languages and their Implementation. In Tutorials: Joint Conference, GULPPRODE'94, pages 11{42, Spain, September 1994. Juan Jose Moreno-Navarro and Mario Rodrguez-Artalejo. Logic Programming with Functions and Predicates: The Language BABEL. Journal of Logic Programming, 12:191{223, 1992. John Maraist and Frank S. K. Silbermann. A Graph Reduction Technique and an Extension to the G-Machine for Pure, Lazy Functional-Logic Languages. In 6th International Symposium, Programming Language Implementation and Logic Programming, pages 355{369. Springer Verlag, September 1994. Andy Muck. CAMEL: an Extension of the Categorical Abstract Machine to Compile Functional/Logic Programs. In M. Bruynooghe and M. Wirsing, editors, Programming Language Implementation and Logic Programming: Proceedings of the 4th International Symposium, PLILP '92, Leuven, BE, pages 341{354, Berlin, DE, 1992. Springer-Verlag. Gopalan Nadathur. A Higher-Order Logic as the Basis for Logic Programming. PhD thesis, University of Pennsylvania, 1987. Gopalan Nadathur and D. A. Millner. Higher-Order Logic Programming. Technical Report CS-1994-38, Department of Computer Science, Duke University, 1994. To appear in The Handbook of Logic in Arti cial Intelligence and Logic Programming, D. Gabbay, C. Hogger, and J.A. Robinson (eds.), Oxford University Press. Michael J. O'Donnell. Equational Logic as a Programming Language. Series in the foundations of computing. MIT Press, 1985.

226

BIBLIOGRAPHY

[Per91]

Nigel Perry. The Implementation of Practical Functional Programming Languages. PhD thesis, Imperial College, University of London, Great Britain, April 1991.

[PH97]

John Peterson and Kevin Hammond, editors. Report on the Programming Language Haskell, A Non-strict Purely Functional Language (Version 1.4). Available at http://haskell.org/, April 1997.

[PJ87]

Simon L. Peyton-Jones. The Implementation of Functional Programming Languages. Prentice Hall, 1987. Simon L. Peyton-Jones. Implementing lazy Functional Languages on Stock Hardware: The Spineless Tagless G-machine. Journal of Functional Programming, 2(2), 1992. Simon L. Peyton-Jones. Implementing lazy Functional Languages on Stock Hardware: The Spineless Tagless G-machine. Technical report, University of Glasgow, Department of Computing Science, March 1993. Version 2.5. Simon L. Peyton-Jones and David R. Lester. A modular fully-lazy lambda lifter in Haskell. Sortware - Practice and Experience, 21, May 1991. Peter L. Van Roy. Can Logic Programming Execute as Fast as Imperative Programming. PhD thesis, University of California, Computer Science Division (EESC), Berkeley, California 94720, December 1990. Gert Smolka. The Oz Programming Model. In J. van Leeuwen, editor, Computer Science Today: Recent Trends and Developments, Lecture Notes in Computer Science 1000, pages 324{343. Springer-Verlag, 1995. Leon Sterling and Ehud Shapiro. The Art of Prolog. Series in logic programming. MIT Press, 1986. Paul Tarau. WAM-optimizations in BinProlog: towards a realistic Continuation Passing Prolog Engine. Technical report, Universite de Moncton, Canada, September 1996. Philip Wadler. Ecient Compilation of Pattern Matching. In Simon Peyton-Jones, editor, The Implementation of Functional Programming Languages, pages 78{103. Prentice Hall, 1987. David H. D. Warren. An Abstract Prolog Instruction Set. Technical Note 309, SRI International, 1983. David H. D. Warren. Or-parallel Execution Models of Prolog. In TAPSOFT 87, Joint Conference on Theory and Practice of Software Development, Pisa, March 1987. DCS, University of Manchester.

[PJ92] [PJ93]

[PJL91] [Roy90] [Smo95] [SS86] [Tar96] [Wad87] [War83] [War87a]

BIBLIOGRAPHY [War87b]

[Wol93]

227

David H. D. Warren. The SRI Model for Or-Parallel Execution of Prolog. Abstract Design and Implementation Isues. In The IEEE Computer Society Press, editor, Proceedings | 1987 Symposium on Logic Programming, pages 46{53. IEEE, September 1987. D. A. Wolfram. The Clausal Theory of Types. Cambridge University Press, 1993.

Index abstract machine, 16 code, 16 activate node, 116 active object, 27 answer, 12 arity transformation, 60 auxiliary operation activate, 116 bindVar, 91 copy, 89 distributeLeft, 154 distributeRight, 155 fullDereference, 91 getArity, 89 getExpArity, 89 getExpId, 89 getId, 89 getInfo, 89 getPosition, 176 getVar, 108 isActive, 116 makeConjunction, 144 makeDeferAndNode, 112 makeDeferEqNode, 113 makeDeferLeafNode, 108 makeDeferOrNode, 115 makeDeferRefNode, 109 merge, 92 negateConjunction, 167 negateDisjunction, 168 refresh, 91 rename, 138 restoreEnv, 106 restoreExists, 105 setShareFlag, 154 setVF, 94 substitute, 176 updateEnv, 107

updateExists, 106

backtracking, 23 binary defer node, 111 binder, 60 binding, 65 binding stamp, 90 body, 6 bottom (?), 18 bound, 103 Brisk machine, 30 built-in function, 43 built-in system function, 43, 50 call node, 30, 42, 45 call-by-need, 18 call-by-value, 18 CER, 86 closure, 20{22, 27 code component, 86 computation, 11 step, 12 con uence, 12 conjunction level, 81 context indirection ctx, 167 current node, 86 currying, 17 defer call, 109 leaf, 104, 107 node, 35 reference, 109 deferring, 14 dereferencing, 91 descriptor node, 88 directly existentially quanti ed, 73 directly universally quanti ed, 74

230 eager evaluation, 18 EQVR, 86 evaluation eager, 18 lazy, 18 existential quanti cation, 8 formula, 6 free variable, 57 function, 6 call, 12 de nition, 6 node, 31, 88 function node, 42 G-machine, 21 garbage, 20 collector, 20 ghc, 30 global variable, 42 goal, 12, 79 graph reduction, 13, 16 ground, 13 H, 86 head, 6 heap, 86 HOLM, 28 implication, 132 indirection, 125 info node, 31, 42, 88 instantiated, 35, 103 JUMP machine, 26 lambda form, 88 lazy constructor, 19 evaluation, 18 leftmost, 18 redex, 12 leftmost innermost, 18 liftable set, 40 lifting, 57 local variable, 6 logic level, 81 look-above, 68, 102

INDEX merging environments, 92 name, 87 narrowing, 14 machine, 24 node deferred, 105 normal form, 16 normal order reduction, 18 NR, 86 occurs check, 74, 77 outermost redex, 12 outside (a conjunction), 80 partial application, 51 partial function, 11 pattern, 14 predicate, 6 redex, 10, 12, 16 leftmost, 12 reduction, 13 refreshing, 91, 93 rename, 138, 162 renaming, 70 residuation, 14, 104 rewrite step, 12 root node, 30 SECD machine, 20 selection rule, 12 SER, 86 set application, 128 body, 9, 171 environment, 171 liftable, 40 pattern, 9 matching on, 49 variable, 9, 171 set-processing function, 9 share ag, 105 sharing, 13, 20, 54, 93 soundness, 12 spine, 17 spineless, 21

INDEX SR, 86 stack, 86 base, 93 frame, 92, 123 stamp, 91 state transition system, 85 statement, 6 STGM, 22 strict, 18 substitution application, 103 supercombinator, 21 switching, 55, 93 syntactical variable, 7 tagless, 22 tail call, 99 term rewriting, 10 termination, 93 TIM, 21 tip, 19 total function, 11 transition rule, 85 type theory, 6 universal quanti cation, 8 unwinding the spine, 19 updating, 54 UQVR, 86 variable directly existentially quanti ed, 73 directly universally quanti ed, 74

ag, 56, 93 global, 42 node, 102 VF, 86 WAM, 22 weak head normal form, 18

231

Suggest Documents