Lecture Notes in Computer Science Commenced Publication in 1973 Founding and Former Series Editors: Gerhard Goos, Juris Hartmanis, and Jan van Leeuwen
Editorial Board David Hutchison Lancaster University, UK Takeo Kanade Carnegie Mellon University, Pittsburgh, PA, USA Josef Kittler University of Surrey, Guildford, UK Jon M. Kleinberg Cornell University, Ithaca, NY, USA Alfred Kobsa University of California, Irvine, CA, USA Friedemann Mattern ETH Zurich, Switzerland John C. Mitchell Stanford University, CA, USA Moni Naor Weizmann Institute of Science, Rehovot, Israel Oscar Nierstrasz University of Bern, Switzerland C. Pandu Rangan Indian Institute of Technology, Madras, India Bernhard Steffen TU Dortmund University, Germany Madhu Sudan Microsoft Research, Cambridge, MA, USA Demetri Terzopoulos University of California, Los Angeles, CA, USA Doug Tygar University of California, Berkeley, CA, USA Gerhard Weikum Max-Planck Institute of Computer Science, Saarbruecken, Germany
6009
Matthias Blume Naoki Kobayashi Germán Vidal (Eds.)
Functional and Logic Programming 10th International Symposium, FLOPS 2010 Sendai, Japan, April 19-21, 2010 Proceedings
13
Volume Editors Matthias Blume Google 20 West Kinzie Street, Chicago, IL 60610, USA E-mail:
[email protected] Naoki Kobayashi Tohoku University, Graduate School of Information Sciences 6-3-9 Aoba, Aramaki, Aoba-ku, Sendai-shi, Miyagi 980-8579, Japan E-mail:
[email protected] Germán Vidal Universidad Politécnica de Valencia, DSIC, MiST Camino de Vera, S/N, 46022 Valencia, Spain E-mail:
[email protected]
Library of Congress Control Number: 2010923409 CR Subject Classification (1998): F.3, D.2, D.3, D.2.4, F.4.1, D.1 LNCS Sublibrary: SL 1 – Theoretical Computer Science and General Issues ISSN ISBN-10 ISBN-13
0302-9743 3-642-12250-7 Springer Berlin Heidelberg New York 978-3-642-12250-7 Springer Berlin Heidelberg New York
This work is subject to copyright. All rights are reserved, whether the whole or part of the material is concerned, specifically the rights of translation, reprinting, re-use of illustrations, recitation, broadcasting, reproduction on microfilms or in any other way, and storage in data banks. Duplication of this publication or parts thereof is permitted only under the provisions of the German Copyright Law of September 9, 1965, in its current version, and permission for use must always be obtained from Springer. Violations are liable to prosecution under the German Copyright Law. springer.com © Springer-Verlag Berlin Heidelberg 2010 Printed in Germany Typesetting: Camera-ready by author, data conversion by Scientific Publishing Services, Chennai, India Printed on acid-free paper 06/3180
Preface
This volume contains the proceedings of the 10th International Symposium on Functional and Logic Programming (FLOPS 2010), held in Sendai, Japan, April 19–21, 2010 at Aoba Memorial Hall, Tohoku University. FLOPS is a forum for research on all issues concerning declarative programming, including functional programming and logic programming, and aims to promote cross-fertilization and integration between the two paradigms. The previous FLOPS meetings were held in Fuji Susono (1995), Shonan Village (1996), Kyoto (1998), Tsukuba (1999), Tokyo (2001), Aizu (2002), Nara (2004), Fuji Susono (2006), and Ise (2008). Since its 1999 edition, FLOPS proceedings have been published by Springer in its Lecture Notes in Computer Science series, as volumes 1722, 2024, 2441, 2998, 3945, and 4989, respectively. In response to the call for papers, 49 papers were submitted. Each paper was reviewed by at least three Program Committee members, with the help of expert external reviewers. The Program Committee meeting was conducted electronically, for a period of two weeks, in December 2009. After careful and thorough discussion, the Program Committee selected 21 papers for presentation at the conference. In addition to the 21 contributed papers, the symposium included talks by three invited speakers: Brigitte Pientka (McGill University, Canada), Kostis Sagonas (National Technical University of Athens, Greece), and Naoyuki Tamura (Kobe University, Japan). On behalf of the Program Committee, we would like to thank the invited speakers, who agreed to give talks and contribute papers, and all those who submitted papers to FLOPS 2010. As Program Committee Chairs, we would like to sincerely thank all the members of the FLOPS 2010 Program Committee for their excellent job, and all the external reviewers for their invaluable contribution. We are also grateful to Andrei Voronkov for making EasyChair available to us. The support of our sponsors is acknowledged. We are indebted to the Japan Society for Software Science and Technology (JSSST) SIG-PPL, the CERIES Global COE Program (Tohoku University Electro-Related Departments), the Graduate School of Information Sciences (Tohoku University), the International Information Science Foundation, the Asian Association for Foundation of Software (AAFS), the Association for Computing Machinery (ACM) SIGPLAN, and the Association for Logic Programming (ALP). Finally, we would like to thank Naoki Kobayashi (Symposium Chair), Eijiro Sumii (Local Chair) and all the members of the Local Arrangements Committee for their invaluable support throughout the preparation and organization of the symposium. February 2010
Matthias Blume Germ´an Vidal
Symposium Organization
Program Chairs Matthias Blume Germ´an Vidal
Google, Chicago, USA Universidad Polit´ecnica de Valencia, Spain
Symposium Chair Naoki Kobayashi
Tohoku University, Sendai, Japan
Program Committee Nick Benton Manuel Chakravarty Michael Codish Bart Demoen Agostino Dovier John P. Gallagher Maria Garcia de la Banda Michael Hanus Atsushi Igarashi Patricia Johann Shin-ya Katsumata Michael Leuschel Francisco L´opez-Fraguas Paqui Lucio Yasuhiko Minamide Frank Pfenning Francois Pottier Tom Schrijvers Chung-chieh “Ken” Shan Zhong Shao Jan-Georg Smaus Nobuko Yoshida
Microsoft Research, Cambridge, UK University of New South Wales, Australia Ben-Gurion University of the Negev, Israel Katholieke Universiteit Leuven, Belgium University of Udine, Italy Roskilde University, Denmark Monash University, Australia University of Kiel, Germany Kyoto University, Japan Rutgers University, USA Kyoto University, Japan University of D¨ usseldorf, Germany Complutense University of Madrid, Spain University of the Basque Country, Spain University of Tsukuba, Japan Carnegie Mellon University, USA INRIA, France Katholieke Universiteit Leuven, Belgium Rutgers University, USA Yale University, USA University of Freiburg, Germany Imperial College London, UK
Local Chair Eijiro Sumii
Tohoku University, Sendai, Japan
VIII
Symposium Organization
External Reviewers Andreas Abel Kenichi Asai Dariusz Biernacki Francisco Bueno James Cheney Michael Elhadad Sebastian Fischer Jacques Garrigue Neil Ghani Mayer Goldberg Kevin Hammond Montserrat Hermo Andrew Kennedy Barbara K¨ onig Roman Leshchinskiy Michael Maher Koji Nakazawa Monica Nesi Dominic Orchard Carla Piazza Morten Rhiger Claudio Russo Pietro Sala Peter Schneider-Kamp Christian Sternagel Peter Stuckey Alexander Summers Akihiko Tozawa Mark Wallace Toshiyuki Yamada
´ Javier Alvez Demis Ballis Bernd Braßel Dario Campagna Markus Degen Andrzej Filinski Marc Fontaine Raffaela Gentilini Silvia Ghilezan Clemens Grelck Hugo Herbelin Petra Hofstedt Neelakantan Krishnaswami Sean Lee Pedro L´opez-Garc´ıa Mircea Marin Marisa Navarro Christopher Okasaki Matthieu Petit Benjamin Pierce Adri´ an Riesco Fernando S´ aenz-P´erez Alan Schmitt Antonis Stampoulis Don Stewart Eijiro Sumii Jaime S´ anchez-Hern´andez Janis Voigtl¨ ander Hongwei Xi Noam Zeilberger
Table of Contents
Invited Talks Beluga: Programming with Dependent Types, Contextual Data, and Contexts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Brigitte Pientka
1
Using Static Analysis to Detect Type Errors and Concurrency Defects in Erlang Programs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Konstantinos Sagonas
13
Solving Constraint Satisfaction Problems with SAT Technology . . . . . . . . Naoyuki Tamura, Tomoya Tanjo, and Mutsunori Banbara
19
Refereed Papers Types A Church-Style Intermediate Language for MLF . . . . . . . . . . . . . . . . . . . . . Didier R´emy and Boris Yakobowski
24
ΠΣ: Dependent Types without the Sugar . . . . . . . . . . . . . . . . . . . . . . . . . . . Thorsten Altenkirch, Nils Anders Danielsson, Andres L¨ oh, and Nicolas Oury
40
Haskell Type Constraints Unleashed . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dominic Orchard and Tom Schrijvers
56
Program Analysis and Transformation A Functional Framework for Result Checking . . . . . . . . . . . . . . . . . . . . . . . . Gilles Barthe, Pablo Buiras, and C´esar Kunz Tag-Free Combinators for Binding-Time Polymorphic Program Generation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Peter Thiemann and Martin Sulzmann Code Generation via Higher-Order Rewrite Systems . . . . . . . . . . . . . . . . . . Florian Haftmann and Tobias Nipkow
72
87 103
Foundations A Complete Axiomatization of Strict Equality . . . . . . . . . . . . . . . . . . . . . . . ´ Javier Alvez and Francisco J. L´ opez-Fraguas
118
X
Table of Contents
Standardization and B¨ ohm Trees for Λμ-Calculus . . . . . . . . . . . . . . . . . . . . Alexis Saurin
134
An Integrated Distance for Atoms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vicent Estruch, C´esar Ferri, Jos´e Hern´ andez-Orallo, and M. Jos´e Ram´ırez-Quintana
150
Logic Programming A Pearl on SAT Solving in Prolog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Jacob M. Howe and Andy King
165
Automatically Generating Counterexamples to Naive Free Theorems . . . Daniel Seidel and Janis Voigtl¨ ander
175
Applying Constraint Logic Programming to SQL Test Case Generation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Rafael Caballero, Yolanda Garc´ıa-Ruiz, and Fernando S´ aenz-P´erez
191
Evaluation and Normalization Internal Normalization, Compilation and Decompilation for System Fβη . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Stefano Berardi and Makoto Tatsuta
207
Towards Normalization by Evaluation for the βη-Calculus of Constructions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Andreas Abel
224
Defunctionalized Interpreters for Call-by-Need Evaluation . . . . . . . . . . . . . Olivier Danvy, Kevin Millikin, Johan Munk, and Ian Zerny
240
Term Rewriting Complexity Analysis by Graph Rewriting . . . . . . . . . . . . . . . . . . . . . . . . . . . Martin Avanzini and Georg Moser
257
Least Upper Bounds on the Size of Church-Rosser Diagrams in Term Rewriting and λ-Calculus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Jeroen Ketema and Jakob Grue Simonsen
272
Proving Injectivity of Functions via Program Inversion in Term Rewriting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Naoki Nishida and Masahiko Sakai
288
Table of Contents
XI
Parallelism and Control Delimited Control in OCaml, Abstractly and Concretely: System Description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Oleg Kiselyov
304
Automatic Parallelization of Recursive Functions Using Quantifier Elimination . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Akimasa Morihata and Kiminori Matsuzaki
321
A Skeleton for Distributed Work Pools in Eden . . . . . . . . . . . . . . . . . . . . . . Mischa Dieterle, Jost Berthold, and Rita Loogen
337
Author Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
355
Beluga: Programming with Dependent Types, Contextual Data, and Contexts Brigitte Pientka McGill University, Montreal, Canada
[email protected]
Abstract. The logical framework LF provides an elegant foundation for specifying formal systems and proofs and it is used successfully in a wide range of applications such as certifying code and mechanizing metatheory of programming languages. However, incorporating LF technology into functional programming to allow programmers to specify and reason about formal guarantees of their programs from within the programming language itself has been a major challenge. In this paper, we present an overview of Beluga, a framework for programming and reasoning with formal systems. It supports specifying formal systems in LF and it also provides a dependently typed functional language that supports analyzing and manipulating LF data via pattern matching. A distinct feature of Beluga is its direct support for reasoning with contexts and contextual objects. Taken together these features lead to a powerful language which supports writing compact and elegant proofs.
1
Introduction
Formal systems given via axioms and inference rules play a central role in describing and verifying guarantees about the runtime behavior of programs. While we have made a lot of progress in statically checking a variety of formal guarantees such as type or memory safety, programmers typically cannot define their own safety policy and reason about it within the programming language itself. This paper presents an overview of a novel programming and reasoning framework, called Beluga [Pie08, PD08]. Beluga uses a two-level approach: on the datalevel, it supports specifications of formal systems within the logical framework LF [HHP93]. The strength and elegance of LF comes from supporting encodings based on higher-order abstract syntax (HOAS), in which binders in the object language are represented as binders in LF’s meta-language. As a consequence, users can avoid implementing common and tricky routines dealing with variables, such as capture-avoiding substitution, renaming and fresh name generation. Because of this, one can think of HOAS encodings as the most advanced technology for specifying and prototyping formal systems which leads to very concise and elegant encodings and provides the most support for such an endeavor. On top of LF, we provide a dependently typed functional language that supports analyzing and manipulating LF data via pattern matching. A distinct M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 1–12, 2010. c Springer-Verlag Berlin Heidelberg 2010
2
B. Pientka
feature of Beluga is its explicit support for contexts to keep track of hypothesis, and contextual objects to describe objects which may depend on them. Contextual objects are characterized by contextual types. For example, A[Ψ ] describes a contextual object Ψ.M where M has type A in the context Ψ and hence may refer to the variables declared in the context Ψ . These contextual objects are analyzed and manipulated naturally by pattern matching. Furthermore, Beluga supports context variables which allow us to write generic functions that abstract over contexts. As types classify terms, context schemas classify contexts. Contexts whose schemas are superficially incompatible can be reasoned with via context weakening and context subsumption. The main application of Beluga at the moment is to prototype formal systems together with their meta-theory. Formal systems given via axioms and inference rules are common in the design and implementation of programming languages, type systems, authorization and security logics, etc. Contextual objects concisely characterize hypothetical and parametric derivations. Inductive proofs about a given formal system can be implemented as recursive functions that case-analyze some given (possibly hypothetical) derivation. Hence, Beluga serves as a proof checking framework. At the same time, Beluga provides an experimental framework for programming with proof objects. Due to its powerful type system, the programmer can not only enforce strong invariants about programs statically, but also to create, manipulate, and analyze certificates (=proofs) which guarantee that a program satisfies a user-defined safety property. Therefore, Beluga is ideally suited for applications such as certified programming and proof-carrying code [Nec97]. Beluga is an implementation in OCaml based on our earlier work [Pie08, PD08]. It provides an re-implementation of LF [HHP93] including type reconstruction, constraint-based higher-order unification and type checking. On top of LF, we designed and implemented a dependently typed functional language that supports explicit contexts and pattern matching over contextual objects. To support reasoning with contexts, we support context weakening and subsumptions. A key step towards a palatable, practical source-level language was the design and implementation of a bidirectional type reconstruction algorithm for dependently typed Beluga functions. While type reconstruction for LF and Beluga is in general undecidable, in practice, the performance is competitive. Beluga also provides an interpreter to execute programs using an environment-based semantics. Our test suite includes many specifications from the Twelf repository [PS99]. We also implemented a broad range of proofs as recursive Beluga functions, including proofs of the Church-Rosser theorem, proofs about compiler transformations, subject reduction, and a translation from natural deduction to Hilbert style proofs. To illustrate the expressive power of Beluga, our test suite also includes simple theorems about structural relationships between expressions and proofs about the paths in expressions. These latter theorems are interesting since they require nested quantifiers and implications, placing them outside the fragment of propositions expressible in systems such as Twelf. The Beluga system,
Beluga
3
including source code, examples, and a tutorial discussing key features of Beluga, is available from http://complogic.cs.mcgill.ca/beluga/. Overview. To provide an intuition for what Beluga accomplishes and how it is used, we concentrate on implementing normalization for the simply-typed lambda-calculus where lambda-terms are indexed with their types (Section 2). In Section 3, we discuss the implementation of Beluga and focus in particular on issues surrounding type reconstruction. Finally, in Section 4 we compare Beluga to related systems with similar ambition and outline future work in Section 5.
2
Example: Normalizing Lambda-Terms
To illustrate the core ideas behind Beluga, we show how to implement a normalizer for the simply-typed lambda-calculus. We begin by introducing the simplytyped lambda calculus. We will write T for types which consist of the base type nat and function types T1 → T2 . For lambda-terms, we use M and N . We begin by introducing it. Types T ::= nat | T1 → T2 Term M, N ::= y | lam x . M | app M1 M2 Next, we will define normalization of simply-typed lambda-terms using the judgment Γ M −→ N which states that given the term M we can compute its normalform N in the context Γ . The context Γ here is simply keeping track of the list of bound variables in M and N and can be defined as follows: Context Γ ::= · | Γ, x We now define a normalization algorithm for the lambda-calculus where we impose a call-by-name strategy. x∈Γ Γ x −→ x
Γ, x M −→ N Γ lam x . M −→ lam x . N
Γ M1 −→ lam x . M Γ [M2 /x]M −→ N Γ app M1 M2 −→ N Γ M1 −→ N1 Γ M2 −→ N2 N1 = lam x . M Γ app M1 M2 −→ app N1 N2 Finally, we show how to represent terms and types in the logical framework LF, and implement the normalization algorithm as a recursive program in Beluga.
4
B. Pientka
Representation of simply-typed lambda-terms in LF. We will represent types and terms in such a way in the logical framework LF that we will only characterize well-typed terms. The definition for types in LF is straightforward and since several excellent tutorials and notes exist already [Pfe97, Twe09], we will keep this short. We introduce an LF type tp and define type constructors nat and arr. Next, we represent terms with the goal to only characterize well-typed lamda-terms. We will achieve this by indexing the type of expressions with their type using a dependent type. In addition, we will employ higher-order abstract syntax to encode the binder in the object-language by binders in the meta-language, namely LF. Hence, the constructor lam takes in a meta-level abstraction of type (exp T1 → exp T2). To illustrate, consider the object-level lambda-term lam x . lam y . x y. It is represented as lam λx. lam λy. app x y in LF. tp: type . nat: tp. arr: tp → tp → tp.
exp: tp → type . lam : (exp T1 → exp T2) → exp (arr T1 T2). app : exp (arr T2 T) → exp T2 → exp (arr T2 T).
Implementation of normalization in Beluga. The specification of simply-typed lambda-terms in LF is standard up to this point. We now concentrate on implementing the normalization algorithm described by the judgement Γ M −→ N . Intuitively, we will implement a function which when given a lambda-term M in a context Γ , it produces a lambda-term N in the same context Γ which will be in normal form. The statement relies on a generic context Γ since the context of variables which we will encounter when we traverse a lambda-abstraction grows. Defining contexts using context schemas. We begin by defining the shape of contexts using context schemas in Beluga as follows: schema ctx =
exp T;
Schemas classify contexts just as types classify terms. The schema ctx describes a context which contains assumptions x:exp T for some type T. In other words, all declarations occurring in a context of schema ctx are instances of exp T for some T. Defining a recursive function for normalizing lambda-terms. Next, we will represent the judgment Γ M −→ N as a computation-level type in Beluga. Since we index expressions with their types, our statement will naturally enforce that types are preserved. The type will state that “for all contexts Γ , given an expression M of type T in the context Γ , we return an expression N of type T in the context Γ ”. In Beluga, this is written as follows: {g:(ctx)*} (exp T)[g] → (exp T)[g]
Writing {g:(ctx)*} in concrete syntax corresponds to quantifying over the context variable g which has schema ctx. We annotate the schema name ctx with * to indicate that declarations matching the given schema may be repeated. The contextual type (exp T)[g] directly describes an expression M with type
Beluga
5
T in the context g. The element inhabiting the computation-level type (exp T) [g] is called a contextual object, since they may refer to variables listed in the context g and hence only make sense within the context g. For example, the contextual object [x:exp] lam λy. app x y has type exp [x:exp] and describes an expression which may refer to the bound variable x. The variable T which is free in the specified computation-level type is implicitly quantified at the outside and has type tp[ ] denoting a closed object of type tp. Type reconstruction will infer the type of T and abstract over it.
We will now show the recursive function which implements the normalization algorithm given earlier. The function proceeds by pattern matching on elements of type (exp T)[g] and every inference rule will correspond to one branch in the case-expression. rec norm : {g:(ctx)*} (exp T)[g] → (exp T)[g] = Λ g ⇒ fn e ⇒ case e of | [g] #p ... ⇒ [g] #p ...
% Variable
| [g] lam (λx. M ... x) ⇒ % Abstraction let [g,x:exp _ ] N ... x = norm [g, x:exp _ ] ([g,x] M ... x) in [g] lam λx. N ... x | [g] app (M1 ... ) (M2 ... ) ⇒ (case norm [g] ([g] M1 ... ) of [g] lam (λx. M’ ... x) ⇒ norm [g] ([g] M’ ... (M2 ... ) ) | [g] N1 ... ⇒ let [g] N2 ... = norm [g] ([g] M2 ... ) in [g] app (N1 ... ) (N2 ... ) )
% Application
;
The Beluga syntax follows ideas from ML-like languages with a few extension. For example, Λg ⇒... introduces abstraction over the context variable g corresponding to quantification over the context variable g in the type of norm. We then split on the object e which has contextual type (exp T)[g]. As in the definition we gave earlier, there are three cases to consider for e: either it is a variable from the context, it is a lambda-abstraction, or it is an application. Each pattern is written as a contextual object, i.e. the object itself together with its context. For the variable case, we use a parameter variable, written as #p ... and write [g] #p ... . Operationally, it will match any declaration from the context g once g is concrete. The parameter variable #p is associated with the identity substitution (written in concrete syntax with ... ) to explicitly state its dependency on the context g. The pattern [g] lam λx. M... x describes the case where the object e is a lambdaabstraction. We write M... x for the body of the lambda-abstraction which may refer to all the variables from the context g (written as ... ) and the variable x. Technically, ... x describes the identity substitution which maps all the variables from g, x:exp T to themselves. We now recursively normalize the contextual object [g,x] M... x. To accomplish this, we pass to the recursive call the extended context g, x:exp _ together with the contextual object [g,x] M... x. We write an underscore for the type of x in the context g, x:exp _ and let type reconstruction determine
6
B. Pientka
it. Note, that we cannot write x:exp T1 since T1 would be free. Hence, supporting holes is crucial to be able to write the program compactly and avoid unnecessary type annotations. The result of the recursive call is a contextual object [g,x] N... x which we will use to assemble the result. In the case for applications, we recursively normalize the contextual object [g] M1... and then pattern match on its result. If it returned a lambda-abstraction lam λx. M’... x, we simply replace x with M2... . Substitution is inherently supported in Beluga and... (M2... ) describes the substitution which maps all variables in g to themselves (written as ... ) and x is mapped to M2... . In the case where normalizing [g] M1... does not return a lambda-abstraction, we continue normalizing [g] M2... and reassemble the final result. In conclusion, our implementation yields a natural, elegant, and very direct encoding of the formal description of normalization. 2.1
Summary of the Main Ideas
Beluga supports a two-level approach for programming with and reasoning about HOAS encodings. The data-level supports specifications of formal systems in the logical framework LF. On top of it, we provide an expressive computation language which supports dependent types and recursion over HOAS encodings. A key challenge is that we must traverse a λ-abstractions and manipulate objects which may contain bound variables. In Beluga, we solve this problem by using contextual types which characterize contextual objects and by introducing context variables to abstract over concrete contexts and parameterize computation with them. By design, variables occurring in contextual objects can never escape their scope, a problem which often arises in other approaches. While in our previous example, all contextual types and objects shared the same context variable, our framework allows the introduction of different context variables, if we wish to do so. Beluga’s theoretical basis is contextual modal type theory which has been described in [NPP08]. We later extended contextual modal type theory with context variables which allow us to abstract over concrete contexts and parameter variables which allow us to talk abstractly about elements of contexts. The foundation for programming with contextual data objects and contexts was first described in [Pie08] and subsequently extended with dependent types in [PD08].
3
Implementation
The Beluga system is implemented in OCaml. It consists of a re-implementation of the logical framework LF [HHP93] which supports in general specifying formal systems given via axioms and inference rules. Similar to the core of the Twelf system [PS99], we support type reconstruction for LF signatures based on higherorder pattern unification with constraints. On top of the logical framework LF, we provide a dependently-typed functional language which allows the user to declare context schemas, write recursive functions using pattern matching on contextual data and abstract over concrete
Beluga
7
contexts using context variables and context abstraction. Designing a palatable, usable source language for Beluga has been challenging. Subsequently, we will list some of the challenges we addressed: 3.1
Higher-Order Unification with Constraints
Higher-order unification is in general undecidable [Gol81], however a decidable fragment, called higher-order patterns [Mil91, Pfe91b] exist. A higher-order pattern is a meta-variable which is applied to distinct bound variables. In our implementation, we associate meta-variables with explicit substitutions which represents the distinct bound variables which may occur in the instantiation of the meta-variable and employ de Bruijn indices [DHKP96]. Our implementation of higher-order pattern unification is based on the development in [Pie03]. Since concentrating on higher-order patterns only is too restrictive, we adopt ideas from the Twelf system and solve higher-order pattern problems eagerly and delay non-pattern cases using constraints which are periodically revisited. Beluga also supports parameter variables which can be instantiated with either bound variables or other parameter variables and we extended unification to account for them. The main difficulty in handling parameter variables lies in the fact that their type may be a Σ-type (dependent product). In general, higherorder unification for Σ-types is undecidable. Fortunately, if we restrict Σ-types to only parameter variables or bound variables, then unique solution exists. 3.2
Type Reconstruction for Beluga
Dependently typed systems can be extremely verbose since dependently typed objects must carry type information. For this reason, languages supporting dependent types such as Twelf [PS99], Delphin [PS08], Coq [BC04] , Agda [Nor07], or Epigram [MM04] all support some form of type reconstruction. However, there are hardly any concise formal description on how this is accomplished, what issues arise in practice, and what requirements the user-level syntax should satisfy. Formal foundations and correctness guarantees are even harder to find. This is a major obstacle if we want this technology to spread and dependent types are to reach mainstream programmers and implementors of programming languages. In the setting of Beluga, we must consider two forms of type reconstruction: (1) type reconstruction for the logical framework LF, which allows us to specify formal systems, and (2) type reconstruction for the computation language which support writing recursive programs using pattern matching on LF objects. Type reconstruction for LF. We illustrate briefly the problem. Consider the expression lam x . lam y . app x y which is represented as lam λx. lam λy. app x y in LF. However, since expressions are indexed by types to ensure that we only represent well-typed expressions, constructors lam and app take in also two index arguments describing the type of the expression. Type reconstruction needs to infer them. The reconstructed, fully explicit representation is lam (arr T S) (arr T S) λx. lam S T λy. app T S x y
8
B. Pientka
We adapted the general principle also found in [Pfe91a]: We may omit an index argument when a constant is used, if it was implicit when the type of the constant was declared. An argument is implicit to a type if it either occurs as a free variable in the type or it is an index argument in the type. Following the ideas in the Twelf system, we do not allow the user to supply an implicit argument explicitly. Type reconstruction is, in general, undecidable for the LF. Our algorithm similar to its implementation in the Twelf system reports a principal type, a type error, or that the source term needs more type information. Type reconstruction for dependently typed recursive functions. Type reconstruction for Beluga functions begins by reconstructing their specified computationlevel type. For example, the type of norm was declared as {g:(ctx)*} (exp T)[g] →(exp T)[g] where the variable T is free. Type reconstruction will infer its type as tp[ ] and yield {g:(ctx)*}{T::tp[ ]}(exp T)[g] →(exp T)[g]. Note, that we do not attempt to infer the schema of the context variable at this point. This could only be done by inspecting the actual program and performing multiple passes over it. Since the type of inferred variables may depend on the context variable g, we insert their abstractions just after the context variable has been introduced. Beluga functions, as the function norm, may be dependently typed and we apply the same principle as before. An index argument is implicit to a computationlevel type if it either occurs as a free meta-variable in the computation-level type or it is an index argument in the computation-level type. Hence, we may omit passing an index argument to a Beluga function, if it was implicit when the type of the function was declared. Considering the program norm again this means whenever we call norm recursively we may omit passing the concrete type for T. Let us describe reconstruction of recursive function step-by-step. Reconstruction of the recursive function norm is guided by its type which is now fully known. For example, we know that after introducing the context with Λg ⇒... , we must introduce the meta-variable T and the beginning of the norm function will be: Λg ⇒mlam T ⇒fn e ⇒case e of .... Reconstruction of the function body may refer to T and cannot leave any free meta-variables. It must accomplish three tasks: (1) We must insert missing arguments to recursive calls to norm. For example in the application case, we have norm [g] ([g] M1 ... ), but norm must in fact also take as a second input the actual type of (M1 ... ). (2) We must infer the type of meta-variables and parameter variables occurring in patterns. (3) We must infer the overall type of the pattern and since patterns may refine dependent types, we must compute a refinement. For example, in the case for abstractions, the type of the scrutinee is (exp T)[g], but the type of the pattern is (exp (arr T1 T2))[g]. Hence, we must infer the refinement which state T = (arr T1 T2). One major challenge is that omitted arguments, which we need to infer, may depend on context variables, bound variables, and other meta-variables. To accomplish this we extended our unification algorithm to support meta2 variables which allow us to track dependencies on contexts and other bound meta-variables.
Beluga
9
Type reconstruction for the computation level is undecidable. For our computation language, we check functions against a given type and either succeed, report a type error, or fail by asking for more type information if no ground instantiation can be found for an omitted argument or if we cannot infer the type of meta-variables occurring in patterns. It is always possible to make typing unambiguous by adding more annotations.
4
Related Work
In our discussion on related work, we will concentrate on programming languages supporting HOAS specifications and reasoning about them. Most closely related to our work is the Twelf system [PS99], a proof checking environment based on the logical framework LF. Its design has strongly influenced the design of Beluga. While both Twelf and Beluga support specifying formal systems using HOAS in LF, Twelf supports implementing proofs as relations. To verify that the relation indeed constitutes a proof, one needs to prove separately that it is a total function. Twelf is a mature system providing termination checking as well as an implementation of coverage checking. Both features are under development for Beluga. One main difference between Twelf and Beluga lies in the treatment of contexts. In Twelf, the actual context of hypotheses remains implicit. As a consequence, instead of a generic base case, base cases in proofs are handled whenever an assumption is introduced. This may lead to scattering of base cases and adds some redundancy. World declarations, similar to context schema declarations, check that assumptions introduced are of the expected form and that appropriate base cases are indeed present. Because worlds in the Twelf system also carry information about base cases, manual weakening is required more often when assembling larger proofs using lemmas. Explicit contexts in Beluga, make the meta-theoretic reasoning about contexts, which is hidden in Twelf, explicit. We give a systematic comparison and discuss the trade-offs of this decision together with illustrative examples in [FP10]. Another important difference between Twelf and Beluga is its expressive power. To illustrate, consider the following simple statement about lambdaterms: If for all N , N is a subterm of K implies that N is a subterm of M , then K must be a subterm of M . Because this statement requires nested quantification and implication, especially in a negative position, it is outside Twelf’s meta-logic which is used to verify that a given relation constitutes a proof. While this has been known, we hope that this simple theorem illustrates this point vividly. More recently, we see a push towards incorporating logical framework technology into mainstream programming languages to support the tight integration of specifying program properties with proofs that these properties hold. The Delphin language [PS08] is most closely related to Beluga. Both support writing recursive functions over LF specifications, but differ in the theoretical foundation. In particular, contexts to keep track of assumptions are implicit in Delphin. It hence lacks the ability to distinguish between closed objects and objects depending on bound variables on the type-level. Delphin’s implementation utilizes as much of the Twelf infrastructure as possible.
10
B. Pientka
Licata and Harper [LH09] developed a logical framework supporting datatypes that mix binding and computation, implemented in the programming language Agda [Nor07, Agd09]. Their system does not explicitly support context variables and abstraction over them, but interprets binders as pronouns which refer to a designated binding site. Structural properties such as weakening, contraction, and substitution are not directly supported by the underlying theoretical foundation, but implemented in a datatype-generic manner. Finally, the current implementation does not support dependent types. A different more pragmatic approach to allow manipulation of binding structures is pursued in nominal type systems which serve as a foundation of FreshML [SPG03]. In this approach names and α-renaming are supported but implementing substitution is left to the user. Generation of a new name and binding names are separate operations which means it is possible to generate data which contains accidentally unbound names since fresh name generation is an observable side effect. To address this problem, Pottier [Pot07] describes pure FreshML where we can reason about the set of names occurring in an expression via a Hoare-style proof system. These approaches however lack dependent typing and hence are not suitable for programming with proofs.
5
Conclusion and Future Work
Over the past year, we designed an implemented Beluga based on our typetheoretic foundation described in [Pie08, PD08]. Our current prototype has been tested on a wide variety of examples, including proofs of the Church-Rosser theorem, proofs about compiler transformations, subject reduction, and translation from natural deduction to Hilbert style proofs. We also used Beluga to implement proofs for theorems about structural relationships between expressions and proofs about the paths in expressions. Both of these statements require nested quantifiers and implications, placing them outside the fragment of propositions expressible in systems such as Twelf. In the future, we plan to concentrate on the following two aspects: Guaranteeing totality of functions. While type-checking guarantees local consistency and partial correctness, it does not guarantee that the implemented function is total. Thus, while we can implement, partially verify, and execute the functions, at present Beluga cannot guarantee that these functions are total and that their implementation constitutes a valid proof. The two missing pieces are coverage and termination. In previous work [DP09], we have described an algorithm to ensure that all cases are covered and we are planning an implementation of coverage during the next few months. Verifying termination of a recursive function will essentially follow similar ideas from the Twelf system [RP96, Pie05] to ensure that arguments passed to the recursive call are indeed smaller. Automating induction proofs. Our framework currently supports the implementation of induction proofs as recursive functions. It however lacks automation. In
Beluga
11
the future, we plan to explore how to connect our framework to a theorem prover which can fill in parts of the function (= proof) automatically and where the user can interactively develop functions in collaboration with a theorem prover.
References [Agd09] [BC04]
[DHKP96]
[DP09]
[FP10]
[Gol81] [HHP93] [LH09]
[Mil91]
[MM04] [Nec97]
[Nor07]
[NPP08] [PD08]
Agda wiki (2009), http://wiki.portal.chalmers.se/agda/ Bertot, Y., Cast´eran, P.: Interactive Theorem Proving and Program Development. In: Coq’Art: The Calculus of Inductive Constructions. Springer, Heidelberg (2004) Dowek, G., Hardin, T., Kirchner, C., Pfenning, F.: Unification via explicit substitutions: The case of higher-order patterns. In: Maher, M. (ed.) Proceedings of the Joint International Conference and Symposium on Logic Programming, Bonn, Germany, pp. 259–273. MIT Press, Cambridge (1996) Dunfield, J., Pientka, B.: Case analysis of higher-order data. In: International Workshop on Logical Frameworks and Meta-Languages: Theory and Practice (LFMTP 2008). ENTCS, vol. 228, pp. 69–84. Elsevier, Amsterdam (2009) Felty, A.P., Pientka, B.: Reasoning with higher-order abstract syntax and contexts: A comparison. Technical report, School of Computer Science, McGill University (January 2010) Goldfarb, W.D.: The undecidability of the second-order unification problem. Theoretical Computer Science 13, 225–230 (1981) Harper, R., Honsell, F., Plotkin, G.: A framework for defining logics. Journal of the ACM 40(1), 143–184 (1993) Licata, D.R., Harper, R.: A universe of binding and computation. In: Hutton, G., Tolmach, A.P. (eds.) 14th ACM SIGPLAN International Conference on Functional Programming, pp. 123–134. ACM Press, New York (2009) Miller, D.: Unification of simply typed lambda-terms as logic programming. In: Eighth International Logic Programming Conference, Paris, France, pp. 255–269. MIT Press, Cambridge (1991) McBride, C., McKinna, J.: The view from the left. Journal of Functional Programming 14(1), 69–111 (2004) Necula, G.C.: Proof-carrying code. In: 24th Annual Symposium on Principles of Programming Languages (POPL 1997), pp. 106–119. ACM Press, New York (1997) Norell, U.: Towards a practical programming language based on dependent type theory. PhD thesis, Department of Computer Science and Engineering, Chalmers University of Technology, Technical Report 33D (September 2007) Nanevski, A., Pfenning, F., Pientka, B.: Contextual modal type theory. ACM Transactions on Computational Logic 9(3), 1–49 (2008) Pientka, B., Dunfield, J.: Programming with proofs and explicit contexts. In: ACM SIGPLAN Symposium on Principles and Practice of Declarative Programming (PPDP 2008), pp. 163–173. ACM Press, New York (2008)
12 [Pfe91a]
[Pfe91b]
[Pfe97] [Pie03]
[Pie05]
[Pie08]
[Pot07]
[PS99]
[PS08]
[RP96]
[SPG03]
[Twe09]
B. Pientka Pfenning, F.: Logic programming in the LF logical framework. In: Huet, G., Plotkin, G. (eds.) Logical Frameworks, pp. 149–181. Cambridge University Press, Cambridge (1991) Pfenning, F.: Unification and anti-unification in the Calculus of Constructions. In: Sixth Annual IEEE Symposium on Logic in Computer Science, Amsterdam, The Netherlands, July 1991, pp. 74–85 (1991) Pfenning, F.: Computation and deduction (1997) Pientka, B.: Tabled higher-order logic programming. PhD thesis, Department of Computer Science, Carnegie Mellon University, CMU-CS03-185 (2003) Pientka, B.: Verifying termination and reduction properties about higher-order logic programs. Journal of Automated Reasoning 34(2), 179–207 (2005) Pientka, B.: A type-theoretic foundation for programming with higherorder abstract syntax and first-class substitutions. In: 35th Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL 2008), pp. 371–382. ACM Press, New York (2008) Pottier, F.: Static name control for FreshML. In: 22nd IEEE Symposium on Logic in Computer Science (LICS 2007), pp. 356–365. IEEE Computer Society, Los Alamitos (2007) Pfenning, F., Sch¨ urmann, C.: System description: Twelf — a metalogical framework for deductive systems. In: Ganzinger, H. (ed.) CADE 1999. LNCS (LNAI), vol. 1632, pp. 202–206. Springer, Heidelberg (1999) Poswolsky, A.B., Sch¨ urmann, C.: Practical programming with higherorder encodings and dependent types. In: Drossopoulou, S. (ed.) ESOP 2008. LNCS, vol. 4960, pp. 93–107. Springer, Heidelberg (2008) Rohwedder, E., Pfenning, F.: Mode and termination checking for higherorder logic programs. In: Riis Nielson, H. (ed.) ESOP 1996. LNCS, vol. 1058, pp. 296–310. Springer, Heidelberg (1996) Shinwell, M.R., Pitts, A.M., Gabbay, M.J.: FreshML: programming with binders made simple. In: 8th International Conference on Functional Programming (ICFP 2003), pp. 263–274. ACM Press, New York (2003) Twelf wiki (2009), http://twelf.plparty.org/wiki/Main_Page
Using Static Analysis to Detect Type Errors and Concurrency Defects in Erlang Programs Konstantinos Sagonas
[email protected]
Abstract. This invited talk will present the key ideas in the design and implementation of Dialyzer, a static analysis tool for Erlang programs. Dialyzer started as a defect detection tool using a rather ad hoc dataflow analysis to detect type errors in Erlang programs, but relatively early in its development it adopted a more disciplined approach to detecting definite type clashes in dynamically typed languages. Namely, an approach based on using a constraint-based analysis to infer success typings which are also enhanced with optional contracts supplied by the programmer. In the first part of the talk, we will describe this constraint-based approach to type inference and explain how it differs with past and recent attempts to type check programs written in dynamic languages. In the second part of the talk, we will present important recent additions to Dialyzer, namely analyses that detect concurrency defects (such as race conditions) in Erlang programs. For a number of years now, Dialyzer has been part of the Erlang/OTP system and has been actively used by its community. Based on this experience, we will also critically examine Dialyzer’s design choices, show interesting cases of Dialyzer’s use, and distill the main lessons learned from using static analysis in open source as well as commercial code bases of significant size.
1
Introduction
Erlang [1] is a strict, dynamically typed functional programming language that comes with built-in support for message-passing concurrency, interprocess communication, distribution, and fault-tolerance. Although the syntax of Erlang is heavily influenced by logic programming languages such as Prolog, its core is similar to those of modern functional programming languages such as ML and Haskell. For example, Erlang features single-assignment variables, pattern matching extended with guards, function closures, list comprehensions, etc. The Erlang/OTP (Open Telecom Platform) system from Ericsson is its standard implementation and has been released as open source since 1998. Erlang’s primary application area has been that of large-scale embedded control systems such as those developed by the telecommunications industry, though recent years have witnessed an increased interest in the language. In particular, Erlang has been successfully used to develop a variety of applications which require concurrency and/or fault-tolerance (e.g., distributed fault-tolerant instant messaging servers, high performance web servers, event-driven web frameworks, and distributed, M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 13–18, 2010. c Springer-Verlag Berlin Heidelberg 2010
14
K. Sagonas
fault-tolerant and schema-free document-oriented databases). Nowadays, applications written in Erlang are significant, both in number and code size, making Erlang one of the most industrially relevant functional programming languages. Program development needs tools tailored to the characteristics of the language that detect common programmer pitfalls and errors (so called “bugs”) in programs. Erlang is no exception in this respect. For example, in sequential programs, the absence of static type checking makes Erlang programs susceptible to type errors which remain undetected by the compiler. Things get worse when concurrency enters the picture, as the number of possible interleavings between processes introduces possibilities for subtle errors which are hard for the programmer to reason about and reproduce during testing. To ameliorate the situation, we have created a static analysis tool, called Dialyzer (DIscrepancy AnaLYZer for ERlang programs), that automatically detects and reports to its user various kinds of software defects in large applications written in Erlang. Defects that are detected by Dialyzer range from identifying code points that will definitely raise an exception due to a type clash, code that has become unreachable due to some logical error, code that has a data race if executed concurrently by more than one processes, function definitions which do not respect the APIs that are specified in the published documentation of Erlang/OTP, etc. Since 2007, Dialyzer is part of the Erlang/OTP distribution. Nowadays, Dialyzer is used extensively in the Erlang programming community and is often integrated in the build environment of many applications. A survey of tools for developing and testing Erlang programs [2], conducted in the spring of 2008, reported that Dialyzer was by a wide margin the software tool most widely known (70%) and used (47%) by Erlang developers. This invited talk will present the techniques used in Dialyzer. The next two sections of this paper review these techniques but do so very briefly. For more information, the reader is referred to the relevant publications that describe these techniques in detail [3,4,5] and to a paper which reports early experiences from developing and using Dialyzer [6]. All these papers are accessible from Dialyzer’s homepage: http://www.it.uu.se/research/group/hipe/dialyzer.
2
Techniques to Detect Type Errors in Erlang
As Erlang is a dynamically typed functional language, the first kind of errors that Dialyzer aimed to detect were definite type errors, i.e., source code points that would always raise a runtime exception due to a type mismatch if the code in that particular program point was ever executed. It should be pointed out that this was not the first attempt to detect type errors in Erlang programs. Among published such attempts, Marlow and Wadler had previously proposed a subtyping system for Erlang programs [7] that was never adopted, mainly due to the fact that it failed to type and unnecessarily rejected valid programs, while Nystr¨ om had proposed a soft typing system for Erlang [8] that never matured to the point of handling the complete language and being practical. The approach taken by Dialyzer differs significantly from
Using Static Analysis to Detect Type Errors and Concurrency Defects
15
these works, both in the techniques that were used but more importantly in the fact that Dialyzer aimed to detect definite type errors instead of possible ones. To detect this kind of errors, Dialyzer initially used a relatively ad hoc pathinsensitive dataflow analysis [3]. Even though this approach was quite weak in principle, it turned out surprisingly effective in practice. Dialyzer managed to detect a significant number of type errors in heavily used libraries and applications, errors which remained hidden during many years of testing and use of the code. The analysis was fast and scalable allowing it to be used in programs of significant size (hundreds of thousand lines of code). More importantly, the analysis was sound for defect detection: it modelled the operational semantics of Erlang programs accurately and never reported a false alarm. We believe this was a key factor in Dialyzer’s adoption by the Erlang community, even before the tool was included in the Erlang/OTP distribution. Early experiences using this analysis were reported in Bugs’05 [6]. Despite Dialyzer’s success, there was a clear limit to the kind of type errors that could be detected using a purely dataflow-based analysis. To ameliorate the situation, Dialyzer’s analysis was redesigned, pretty much from scratch. We opted for a more disciplined and considerably more powerful analysis that infers success typings of functions using a constraint-based inference algorithm [4]. Informally, a success typing is a type signature that over-approximates the function’s dynamic semantics. More concretely, it over-approximates the set of terms for which the function can evaluate to a value. The domain of the type signature includes a type-level representation of all possible terms that the function could accept as parameters, and its range includes all possible return values for this domain. In effect, success typings approach the type inference problem from a direction opposite to that of type systems for statically typed languages. For example, while most type systems have to restrict the set of terms that a function can accept in order to prove type safety, success typings, which are intended to locate definite type clashes, only need to be careful not to exclude some term for which the function can be used without raising some runtime exception. The analogy can be taken further. The well-known slogan that “well-typed programs never go wrong” has its analogue “ill-typed programs always fail”, meaning that use of a function in a way which is incompatible with its success typing will surely raise a runtime exception if encountered. Slogans aside, success typings allow for compositional, bottom-up, constraintbased type inference which appears to scale well in practice. Moreover, by taking control and data flow into account and by exploiting properties of the language such as its module system, which allows for specializing success typings of module-local functions based on their actual instead of their possible uses, success typings can be refined using a top-down algorithm [4]. This refinement process often makes success typings as accurate as the types inferred by statically typed functional languages. Given these so called refined success typings, Dialyzer employs a function-local dataflow analysis that locates type clashes and other errors (e.g., case clauses that can never match, guards that will always fail, etc.) in programs.
16
K. Sagonas
Once there was a solid basis for detecting discrepancies between allowed and actual uses of functions, the obvious next step was to design a specification language for Erlang. This language is nowadays a reality and allows Erlang programmers to express their intentions about how certain functions are to be used, thereby serving both as important documentation for the source code and providing additional constraints to the analysis in the form of type contracts [9]. These contracts are either success typings which are automatically generated and inserted into the source code or user-specified refinements of the inferred refined success typings. In many respects, this approach of adding contracts is similar to that pioneered by other dynamically typed functional languages such as PLT Scheme [10]. Nowadays, many of Erlang/OTP’s libraries as well as open source applications written in Erlang come with type contracts for functions; especially in those which are part of a module’s API. The presence of such information has allowed Dialyzer to detect even more type errors and subtle interface abuses in key functions of Erlang/OTP. A paper describing in detail the approach we advocate and experiences from applying it in one non-trivial case study was published in the 2008 Erlang workshop [11].
3
Techniques to Detect Concurrency Defects in Erlang
Relatively recently, Dialyzer was enhanced with a precise and scalable analysis that automatically detects data races. In pure Erlang code, data races are impossible: the language does not provide any constructs for processes to create and modify shared memory. However, the Erlang/OTP implementation comes with key libraries and built-ins, implemented in C as part of the runtime system, that do create and manipulate data structures which are shared between processes. Unrestricted uses of these built-ins in code run by different processes may lead to data races. To detect such situations, we have designed a static analysis that detects some of the most common kinds of data races in Erlang programs: races in the process registry, in the Erlang Term Storage (ETS) facility, and in the mnesia database management system. This analysis integrates smoothly with the rest of Dialyzer’s analyses targetting the sequential part of the language. The analysis is non-trivial as the built-ins accessing shared data structures may be spatially far apart, they may even be located in the code of different modules, or even worse, hidden in the code of higher-order functions. For this reason, the analysis takes control flow into account. Also, it has to be able to reason about data flow: if at some program point the analysis locates a call to a built-in reading a shared data structure and from that point on control reaches a program point where a call to a built-in writing the same data structure appears, the analysis needs to determine whether the two calls may possibly be performed on the same data item or not. If they may, it has detected a race condition, otherwise there is none. Because data races are subtle and difficult to locate, Dialyzer departs from the “report only definite errors” principle: for the first time its user can opt for an analysis that is sound either for defect detection or for correctness. The former
Using Static Analysis to Detect Type Errors and Concurrency Defects
17
analysis completely avoids false alarms by ignoring any unknown higher-order calls it may encounter, while the latter finds all data races with the risk of also producing some false alarms depending on the precision of the information about these calls. A recently published paper [5] describes in detail the steps of the analysis, the optimizations that it employs, and measurements evaluating its effectiveness and performance on a suite of widely used industrial and open source programs of considerable size. As reported there, the analysis was able to detect a significant number of previously unknown race conditions in these programs. More importantly, since November 2009 it is part of Erlang/OTP, it has already been put to test by its users, and has helped application programmers discover subtle data races in their code bases.
4
Concluding Remarks
Although many of the ideas of Dialyzer can be traced as far back as 2003, this line of research is active and far from complete. We are currently working on various extensions and additions to the core of Dialyzer’s analysis that will enable it to detect many more kinds of errors in programs. Chief among them are those related to detecting defects in concurrent programs that use messagepassing for concurrency, which arguably is Erlang’s most salient feature. No matter how many analyses one designs and employs, programmers somehow seem to be continuously stepping upon interesting new cases of bugs which are beyond the reach of these analyses. Although it is clearly impossible to catch all software bugs, it’s certainly fun to try!
Acknowledgements This research has been supported in part by grant #621-2006-4669 from the Swedish Research Council. Tobias Lindahl and Maria Christakis have contributed enormously to Dialyzer; both to the design of the analyses employed by the tool, by fine-tuning their actual implementation and making Dialyzer not only effective in discovering bugs, but also efficient and scalable. The author wishes to thank both the Program Committee of FLOPS 2010 for their invitation and the program chairs of the Symposium for their patience in receiving answers to their e-mails and the camera-ready version of this paper.
References 1. Armstrong, J.: Programming Erlang: Software for a Concurrent World. The Pragmatic Bookshelf, Raleigh (2007) 2. Nagy, T., Nagyn´e V´ıg, A.: Erlang testing and tools survey. In: Proceedings of the 7th ACM SIGPLAN Workshop on Erlang, pp. 21–28. ACM, New York (2008)
18
K. Sagonas
3. Lindahl, T., Sagonas, K.: Detecting software defects in telecom applications through lightweight static analysis: A war story. In: Wei-Ngan, C. (ed.) APLAS 2004. LNCS, vol. 3302, pp. 91–106. Springer, Heidelberg (2004) 4. Lindahl, T., Sagonas, K.: Practical type inference based on success typings. In: Proceedings of the 8th ACM SIGPLAN International Conference on Principles and Practice of Declarative Programming, pp. 167–178. ACM, New York (2006) 5. Christakis, M., Sagonas, K.: Static detection of race conditions in Erlang. In: Carro, M., Pe˜ na, R. (eds.) PADL 2010. LNCS, vol. 5937, pp. 119–133. Springer, Heidelberg (2010) 6. Sagonas, K.: Experience from developing the Dialyzer: A static analysis tool detecting defects in Erlang applications. In: Proceedings of the ACM SIGPLAN Workshop on the Evaluation of Software Defect Detection Tools (2005) 7. Marlow, S., Wadler, P.: A practical subtyping system for Erlang. In: Proceedings of the ACM SIGPLAN International Conference on Functional Programming, pp. 136–149. ACM, New York (1997) 8. Nystr¨ om, S.O.: A soft-typing system for Erlang. In: Proceedings of ACM SIGPLAN Erlang Workshop, pp. 56–71. ACM, New York (2003) 9. Jimenez, M., Lindahl, T., Sagonas, K.: A language for specifying type contracts in Erlang and its interaction with success typings. In: Proceedings of the 6th ACM SIGPLAN Workshop on Erlang, pp. 11–17. ACM, New York (2007) 10. Findler, R.B., Clements, J., Flanagan, C., Flatt, M., Krishnamurthi, S., Steckler, P., Felleisen, M.: DrScheme: A programming environment for Scheme. Journal of Functional Programming 12(2), 159–182 (2002) 11. Sagonas, K., Luna, D.: Gradual typing of Erlang programs: A Wrangler experience. In: Proceedings of the 7th ACM SIGPLAN Workshop on Erlang, pp. 73–82. ACM, New York (2008)
Solving Constraint Satisfaction Problems with SAT Technology Naoyuki Tamura1 , Tomoya Tanjo2 , and Mutsunori Banbara1 1
Information Science and Technology Center, Kobe University, Japan {tamura,banbara}@kobe-u.ac.jp 2 Graduate School of Engineering, Kobe University, Japan
Abstract. A Boolean Satisfiability Testing Problem (SAT) is a combinatorial problem to find a Boolean variable assignment which satisfies all given Boolean formulas. Recent performance improvement of SAT technologies makes SAT-based approaches applicable for solving hard and practical combinatorial problems, such as planning, scheduling, hardware/software verification, and constraint satisfaction. Sugar is a SAT-based constraint solver based on a new encoding method called order encoding which was first used to encode job-shop scheduling problems by Crawford and Baker. In the order encoding, a comparison x ≤ a is encoded by a different Boolean variable for each integer variable x and integer value a. The Sugar solver shows a good performance for a wide variety of problems, and became the winner of the GLOBAL categories in 2008 and 2009 CSP solver competitions. The talk will provide an introduction to modern SAT solvers, SAT encodings, implementation techniques of the Sugar solver, and its performance evaluation.
1
SAT and SAT Encodings
A Boolean Satisfiability Testing Problem (SAT) is a combinatorial problem to find a Boolean variable assignment which satisfies all given Boolean formulas [1]. Recent performance improvement of SAT technologies makes SAT-based approaches applicable for solving hard and practical combinatorial problems, such as planning, scheduling, hardware/software verification, and constraint satisfaction. A (finite) Constraint Satisfaction Problem (CSP) is a combinatorial problem to find an integer variable assignment which satisfies all given constraints on integers. A SAT-based constraint solver is a program which solves constraint satisfaction problems (CSP) by encoding them to SAT and searching solutions by a SAT solver. There have been several proposed methods to encode CSP into SAT. Direct encoding is the most widely used one in which a Boolean variable p(x = i) is defined as true if and only if the CSP variable x has the domain value i [2,3]. A constraint is encoded by enumerating its conflict points as SAT clauses. Support encoding also uses a Boolean variable p(x = i) as in the direct encoding [4,5]. While a constraint is encoded by considering its support points. M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 19–23, 2010. c Springer-Verlag Berlin Heidelberg 2010
20
N. Tamura, T. Tanjo, and M. Banbara
Log encoding uses a Boolean variable p(x(i) ) for each i-th bit of each CSP variable x [6,7]. A constraint is encoded by enumerating its conflict points as SAT clauses as in the direct encoding. Log-support encoding also uses a Boolean variable p(x(i) ) for each i-th bit of each CSP variable x as in the log encoding [8]. A constraint is encoded by considering its support points. Order encoding uses a Boolean variable p(x ≤ i) for each integer variable x and domain value i where p(x ≤ i) is defined as true if and only if the CSP variable x is less than or equal to i [9]. This encoding was first used to encode job-shop scheduling problems by Crawford and Baker [10] and studied by Inoue et al. [11,12]. This encoding shows a good performance for a wide variety of problems, It succeeded to solve previously undecided problems, such as openshop scheduling problems [9] and two-dimensional strip packing problems [13]. 1.1
Order Encoding
Let x be an integer variable whose domain is {l..u}. Boolean variables p(x ≤ l), p(x ≤ l + 1), . . . , p(x ≤ u − 1) and the following SAT clauses are introduced to encode each integer variable x. Please note that p(x ≤ u) is unnecessary because x ≤ u is always true. ¬p(x ≤ i − 1) ∨ p(x ≤ i) (l < i < u) Constraints are encoded by enumerating conflict regions. When all points (x1 , . . . , xk ) in the region i1 < x1 ≤ j1 , . . . , ik < xk ≤ jk violate the constraint, the following SAT clause is added. p(x1 ≤ i1 ) ∨ ¬p(x1 ≤ j1 ) ∨ · · · ∨ p(xk ≤ ik ) ∨ ¬p(xk ≤ jk ) The following is an example of encoding of x+ y ≤ 7 when x, y ∈ {2, 3, 4, 5, 6}.
y 7
6
5
4
3
2
1
0
1
2
3
4
5
6
7
x
Fig. 1. Order encoding of x + y ≤ 7
Solving Constraint Satisfaction Problems with SAT Technology
21
The following six SAT clauses are used to encode the variables x and y. ¬p(x ≤ 2) ∨ p(x ≤ 3) ¬p(y ≤ 2) ∨ p(y ≤ 3)
¬p(x ≤ 3) ∨ p(x ≤ 4) ¬p(y ≤ 3) ∨ p(y ≤ 4)
¬p(x ≤ 4) ∨ p(x ≤ 5) ¬p(y ≤ 4) ∨ p(y ≤ 5)
The following five SAT clauses are used to encode the constraint x + y ≤ 7 (Fig. 1) p(y ≤ 5)
2
p(x ≤ 2) ∨ p(y ≤ 4)
p(x ≤ 3) ∨ p(y ≤ 3)
p(x ≤ 4) ∨ p(y ≤ 2)
p(x ≤ 5)
SAT-Based Constraint Solver Sugar
Sugar1 is a SAT-based constraint solver based on the order encoding [14,15,16]. It can solve CSP, COP (Constraint Optimization Problem), and Max-CSP by translating the problem instance to a SAT instance and then solving the translated SAT instance with an external efficient SAT solver such as MiniSat [17] and PicoSAT [18]. Table 1. Comparison of CSP solvers for 556 global category instances Series BIBD Costas Array Latin Square Magic Square NengFa Orthogonal Latin Square Perfect Square Packing Pigeons Quasigroup Existence Pseudo-Boolean BQWH Cumulative Job-Shop RCPSP Cabinet Timetabling Total
( 83) ( 11) ( 10) ( 18) ( 3) ( 9) ( 74) ( 19) ( 35) (100) ( 20) ( 10) ( 78) ( 40) ( 46) (556)
Sugar Sugar Mistral Choco bpsolver +MiniSat +PicoSAT 76 77 76 58 35 8 8 9 9 9 10 9 5 5 5 8 8 13 15 11 3 3 3 3 3 3 3 3 2 3 54 53 40 47 36 19 19 19 19 19 30 29 29 28 30 68 75 59 53 70 20 20 20 20 20 4 4 2 1 0 78 78 78 77 75 40 40 40 40 40 25 42 39 14 1 446 468 435 391 357
Sugar showed a good performance in the recent International CSP Solver Competitions held in 2009 2 . It became the winner in 3 categories out of 7 categories. These 3 categories are the widest ones which can contain all of global, intensional, and extensional constraints. Table 1 shows the number of solved 1 2
http://bach.istc.kobe-u.ac.jp/sugar/ http://www.cril.univ-artois.fr/CPAI09/
22
N. Tamura, T. Tanjo, and M. Banbara
instances in these categories by Sugar and other top ranked solvers Mistral3 , Choco4 , and bpsolver5 . Through those results, it is shown that a SAT-based solver can have a competitive performance with other state-of-the-art solvers in difficult CSP benchmark instances. We hope the order encoding used in Sugar is becoming popular in other SAT-based systems. This research was partially supported by JSPS (Japan Society for the Promotion of Science), Grant-in-Aid for Scientific Research (A), 2008–2011, 20240003.
References 1. Biere, A., Heule, M., van Maaren, H., Walsh, T. (eds.): Handbook of Satisfiability. Frontiers in Artificial Intelligence and Applications (FAIA), vol. 185. IOS Press, Amsterdam (2009) 2. de Kleer, J.: A comparison of ATMS and CSP techniques. In: Proceedings of the 11th International Joint Conference on Artificial Intelligence (IJCAI 1989), pp. 290–296 (1989) 3. Walsh, T.: SAT v CSP. In: Dechter, R. (ed.) CP 2000. LNCS, vol. 1894, pp. 441– 456. Springer, Heidelberg (2000) 4. Kasif, S.: On the parallel complexity of discrete relaxation in constraint satisfaction networks. Artificial Intelligence 45, 275–286 (1990) 5. Gent, I.P.: Arc consistency in SAT. In: Proceedings of the 15th European Conference on Artificial Intelligence (ECAI 2002), pp. 121–125 (2002) 6. Iwama, K., Miyazaki, S.: SAT-variable complexity of hard combinatorial problems. In: Proceedings of the IFIP 13th World Computer Congress, pp. 253–258 (1994) 7. Gelder, A.V.: Another look at graph coloring via propositional satisfiability. Discrete Applied Mathematics 156, 230–243 (2008) 8. Gavanelli, M.: The log-support encoding of CSP into SAT. In: Bessi`ere, C. (ed.) CP 2007. LNCS, vol. 4741, pp. 815–822. Springer, Heidelberg (2007) 9. Tamura, N., Taga, A., Kitagawa, S., Banbara, M.: Compiling finite linear CSP into SAT. Constraints 14, 254–272 (2009) 10. Crawford, J.M., Baker, A.B.: Experimental results on the application of satisfiability algorithms to scheduling problems. In: Proceedings of the 12th National Conference on Artificial Intelligence (AAAI 1994), pp. 1092–1097 (1994) 11. Inoue, K., Soh, T., Ueda, S., Sasaura, Y., Banbara, M., Tamura, N.: A competitive and cooperative approach to propositional satisfiability. Discrete Applied Mathematics 154, 2291–2306 (2006) 12. Nabeshima, H., Soh, T., Inoue, K., Iwanuma, K.: Lemma reusing for SAT based planning and scheduling. In: Proceedings of the International Conference on Automated Planning and Scheduling 2006 (ICAPS 2006), pp. 103–112 (2006) 13. Soh, T., Inoue, K., Tamura, N., Banbara, M., Nabeshima, H.: A SAT-based method for solving the two-dimensional strip packing problem. Journal of Algorithms in Cognition, Informatics and Logic (2009) (to appear) 14. Tamura, N., Banbara, M.: Sugar: a CSP to SAT translator based on order encoding. In: Proceedings of the 2nd International CSP Solver Competition, pp. 65–69 (2008) 3 4 5
http://www.4c.ucc.ie/~ehebrard/Software.html http://choco.emn.fr http://www.probp.com
Solving Constraint Satisfaction Problems with SAT Technology
23
15. Tamura, N., Tanjo, T., Banbara, M.: System description of a SAT-based CSP solver Sugar. In: Proceedings of the 3rd International CSP Solver Competition, pp. 71–75 (2008) 16. Tanjo, T., Tamura, N., Banbara, M.: Sugar++: a SAT-based Max-CSP/COP solver. In: Proceedings of the 3rd International CSP Solver Competition, pp. 77–82 (2008) 17. E´en, N., S¨ orensson, N.: An extensible SAT-solver. In: Giunchiglia, E., Tacchella, A. (eds.) SAT 2003. LNCS, vol. 2919, pp. 502–518. Springer, Heidelberg (2004) 18. Biere, A.: PicoSAT essentials. Journal on Satisfiability, Boolean Modeling and Computation 4, 75–97 (2008)
A Church-Style Intermediate Language for MLF Didier R´emy1 and Boris Yakobowski2 1
2
INRIA Paris - Rocquencourt CEA, LIST, Laboratoire Suret´e des Logiciels, Boˆıte 94, 91191 Gif-sur-Yvette Cedex, France
Abstract. MLF is a type system that seamlessly merges ML-style implicit but second-class polymorphism with System-F explicit first-class polymorphism. We present x MLF, a Church-style version of MLF with full type information that can easily be maintained during reduction. All parameters of functions are explicitly typed and both type abstraction and type instantiation are explicit. However, type instantiation in x MLF is more general than type application in System F. We equip x MLF with a small-step reduction semantics that allows reduction in any context and show that this relation is confluent and type preserving. We also show that both subject reduction and progress hold for weak-reduction strategies, including call-by-value with the value-restriction.
Introduction MLF (Le Botlan and R´emy 2003, 2007; R´emy and Yakobowski 2008b) is a type system that seamlessly merges ML-style implicit but second-class polymorphism with System-F explicit first-class polymorphism. This is done by enriching System-F types. Indeed, System F is not well-suited for partial type inference, as illustrated by the following example. Assume that a function, say choice, of type ∀ (α) α → α → α and the identity function id, of type ∀ (β) β → β, have been defined. How can the application choice to id be typed in System F? Should choice be applied to the type ∀ (β) β → β of the identity that is itself kept polymorphic? Or should it be applied to the monomorphic type γ → γ, with the identity being applied to γ (where γ is bound in a type abstraction in front of the application)? Unfortunately, these alternatives have incompatible types, respectively (∀ (α) α → α) → (∀ (α) α → α) and ∀ (γ) (γ → γ) → (γ → γ): none is an instance of the other. Hence, in System F, one is forced to irreversibly choose between one of the two explicitly typed terms. However, a type inference system cannot choose between the two, as this would sacrifice completeness and be somehow arbitrary. This is why MLF enriches types with instance-bounded polymorphism, which allows to write more expressive types that factor out in a single type all typechecking alternatives in such cases as the example of choice. Now, the type ∀ (α τ ) α → α, which should be read “α → α where α is any instance of τ ”, can be assigned to choice id, and the two previous alternatives can be recovered a posteriori by choosing different instances for α. M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 24–39, 2010. c Springer-Verlag Berlin Heidelberg 2010
A Church-Style Intermediate Language for MLF
25
Currently, the language MLF comes with a Curry-style version iMLF where no type information is needed and a type-inference version eMLF that requires partial type information (Le Botlan and R´emy 2007). However, eMLF is not quite in Church’s style, since a large amount of type information is still implicit and partial type information cannot be easily maintained during reduction. Hence, while eMLF is a good surface language, it is not a good candidate for use as an internal language during the compilation process, where some program transformations, and perhaps some reduction steps, are being performed. This has been a problem for the adoption of MLF in the Haskell community (Peyton Jones 2003), as the Haskell compilation chain uses an explicitly-typed internal language. This is also an obstacle to proving subject reduction, which does not hold in eMLF. In a way, this is unavoidable in a language with non-trivial partial type inference. Indeed, type annotations cannot be completely dropped, but must at least be transformed and reorganized during reduction. Still, one could expect that eMLF be equipped with reduction rules for type annotations. This has actually been considered in the original presentation of MLF, but only with limited success. The reduction kept track of annotation sites during reduction; this showed, in particular, that no new annotation site needs to be introduced during reduction. Unfortunately, the exact form of annotations could not be maintained during reduction, by lack of an appropriate language to describe their computation. As a result, it has only been shown that some type derivation can be rebuilt after the reduction of a well-typed program, but without exhibiting an algorithm to compute them during reduction. Independently, R´emy and Yakobowski (2008b) have introduced graphic constraints, both to simplify the presentation of MLF and to improve its type inference algorithm. This also lead to a simpler, more expressive definition of MLF. In this paper, we present x MLF, a Church-style version of MLF that contains full type information. In fact, type checking becomes a simple and local verification process—by contrast with type inference in eMLF, which is based on unification. In x MLF, type abstraction, type instantiation, and all parameters of functions are explicit, as in System F. However, type instantiation is more general and more atomic than type application in System F: we use explicit type instantiation expressions that are proof evidences for the type instance relations. In addition to the usual β-reduction, we give a series of reduction rules for simplifying type instantiations. These rules are confluent when allowed in any context. Moreover, reduction preserves typings, and is sufficient to reduce all typable expressions to a value when used in either a call-by-value or call-by-name setting. This establishes the soundness of MLF for a call-by-name semantics for the first time. Notably, x MLF is a conservative extension of System F. The paper is organized as follows. We present x MLF, its syntax and its static and dynamic semantics in §1. We study its main properties, including type soundness for different evaluations strategies in §2. We discuss possible variations, as well as related and future works in §3. All proofs are omitted, but can be found in (Yakobowski 2008, Chapters 14 & 15).
26
D. R´emy and B. Yakobowski
α, β, γ, δ τ ::= | | | | φ
::= | | | | | | | |
α τ →τ ∀ (α τ ) τ ⊥ τ !α ∀ ( φ) ∀ (α ) φ φ; φ
½
Type variable Type Type variable Arrow type Quantification Bottom type Instantiation Bottom Abstract Inside Under ∀-elimination ∀-introduction Composition Identity
x, y, z a ::= | | | | | |
x λ(x : τ ) a aa Λ(α τ ) a aφ let x = a in a
Γ ::= | ∅ | Γ, α τ | Γ, x : τ
Term variable Term Variable Function Application Type function Instantiation Let-binding Environment Empty Type variable Term variable
Fig. 1. Grammar of types, instantiations, and terms
1
The Calculus
Types, instantiations, terms, and typing environments. All the syntactic definitions of x MLF can be found in Figure 1. We assume given a countable collection of variables ranged over by letters α, β, γ, and δ. As usual, types include type variables and arrow types. Other type constructors will be added later—straightforwardly, as the arrow constructor receives no special treatment. Types also include a bottom type ⊥ that corresponds to the System-F type ∀α.α. Finally, a type may also be a form of bounded quantification ∀ (α τ ) τ , called flexible quantification, that generalizes the ∀α.τ form of System F and, intuitively, restricts the variable α to range only over instances of τ . The variable α is bound in τ but not in τ . (We may write ∀ (α) τ when the bound τ is ⊥.) In Church-style System F, type instantiation inside terms is simply type application, of the form a τ . By contrast, type instantiation a φ in x MLF details every intermediate instantiation step, so that it can be checked locally. Intuitively, the instantiation φ transforms a type τ into another type τ that is an instance of τ . In a way, φ is a witness for the instance relation that holds between τ and τ . It is therefore easier to understand instantiations altogether with their static semantics, which will be explained in the next section. Terms of x MLF are those of the λ-calculus enriched with let constructs, with two small differences. Type instantiation a φ generalizes System-F type application. Type abstractions are extended with an instance bound τ and written Λ(α τ ) a. The type variable α is bound in a, but not in τ . We abbreviate Λ(α ⊥) a as Λ(α) a, which simulates the type abstraction Λα. a of System F. We write ftv(τ ) and ftv(a) the set of type variables that appear free in τ and a. We identify types, instantiations, and terms up to the renaming of bound variables. The capture-avoiding substitution of a variable v inside an expression s by an expression s is written s{v ← s }.
A Church-Style Intermediate Language for MLF
27
Inst-Bot
Inst-Under Γ, α τ φ : τ1 ≤ τ2
Inst-Abstr ατ ∈Γ
Γ τ :⊥≤τ
Γ ∀ (α ) φ : ∀ (α τ ) τ1 ≤ ∀ (α τ ) τ2
Γ !α : τ ≤ α
Inst-Inside
Inst-Intro α∈ / ftv(τ ) Γ : τ ≤ ∀ (α ⊥) τ
Γ φ : τ1 ≤ τ2 Γ ∀ ( φ) : ∀ (α τ1 ) τ ≤ ∀ (α τ2 ) τ Inst-Comp Γ φ 1 : τ1 ≤ τ2 Γ φ2 : τ2 ≤ τ3 Γ φ 1 ; φ 2 : τ1 ≤ τ3
Inst-Elim
Inst-Id
Γ : ∀ (α τ ) τ ≤ τ {α ← τ }
Γ ½:τ ≤τ
Fig. 2. Type instance
As usual, type environments assign types to program variables. However, instead of just listing type variables, as is the case in System F, type variables are also assigned a bound in a binding of the form α τ . We write dom(Γ ) for the set of all terms and type variables that are bound by Γ . We also assume that typing environments are well-formed, i.e. they do not bind twice the same variable and free type variables appearing in a type of the environment Γ must be bound earlier in Γ . Formally, the empty environment is well-formed and, given a (well-formed) environment Γ , the relations α ∈ dom(Γ ) and ftv(τ ) ⊆ dom(Γ ) must hold to form environments Γ, α τ and Γ, x : τ . Instantiations. Instantiations φ are defined in Figure 1. Their typing, described in Figure 2, are type instance judgments of the form Γ φ : τ ≤ τ , stating that in environment Γ , the instantiation φ transforms the type τ into the type τ . The bottom instantiation τ expresses that (any) type τ is an instance of the bottom type. The abstract instantiation !α, which assumes that the hypothesis ατ is in the environment, abstracts the bound τ of α as the type variable α. The inside instantiation ∀ ( φ) applies φ to the bound τ of a flexible quantification ∀ (α τ ) τ . Conversely, the under instantiation ∀ (α ) φ applies φ to the type τ under the quantification. The type variable α is bound in φ; the environment in the premise of the rule Inst-Under is increased accordingly. The quantifier introduction 1 introduces a fresh trivial quantification ∀ (α⊥). Conversely, the quantifier elimination eliminates the bound of a type of the form ∀ (ατ ) τ by substituting τ for α in τ . This amounts to definitely choosing the present bound τ for α, while the bound before the application could be further instantiated by some inside instantiation. The composition φ; φ witnesses the transitivity of type instance, while the identity instantiation ½ witnesses reflexivity.
1
The choice of is only by symmetry with the elimination form described next, and has no connection at all with linear logic.
28
D. R´emy and B. Yakobowski
τ (!α) ⊥ τ τ ½ τ (φ1 ; φ2 )
= = = =
α τ τ (τ φ1 ) φ2
τ (∀ (α τ ) τ ) (∀ (α τ ) τ ) (∀ (α τ ) τ )
(∀ ( φ)) (∀ (α ) φ)
= = = =
∀ (α ⊥) τ α∈ / ftv(τ ) τ {α ← τ } ∀ (α τ φ) τ ∀ (α τ ) (τ φ)
Fig. 3. Type instantiation (on types)
Example. Let τmin , τcmp , and τand be the types of the parametric minimum and comparison functions and of the conjunction of boolean formulas: τmin ∀ (α ⊥) α → α → α τcmp ∀ (α ⊥) α → α → bool τand bool → bool → bool Let φ be the instantiation ∀ ( bool); . Then, φ : τmin ≤ τand and φ : τcmp ≤ τand hold. Let τK be the type ∀ (α ⊥) ∀ (β ⊥) α → β → α (e.g. of the λ-term λ(x) λ(y) x) and φ be the instantiation2 ∀ (α ) (∀ ( α); ). Then, φ : τK ≤ τmin . Type application. As above, we often instantiate a quantification over ⊥ and immediately substitute the result. Moreover, this pattern corresponds to the System-F unique instantiation form. Therefore, we define τ as syntactic sugar for (∀ ( τ ); ). The instantiations φ and φ can then be abbreviated as bool and ∀ (α ) α . More generally, we write φ for the computation (∀ ( φ); ). Properties of instantiations. Since instantiations make all steps in the instance relation explicit, their typing is deterministic. Lemma 1. If Γ φ : τ ≤ τ1 and Γ φ : τ ≤ τ2 , then τ1 = τ2 . The use of Γ instead of Γ may be surprising. However, Γ does not contribute to the instance relation, except in the side condition of rule Inst-Abstr. Hence, the type instance relation defines a partial function, called type instantiation 3 that, given an instantiation φ and a type τ , returns (if it exists) the unique type τ φ such that φ : τ ≤ τ φ. An inductive definition of this function is given in Figure 3. Type instantiation is complete for type instance: Lemma 2. If Γ φ : τ ≤ τ , then τ φ = τ . However, the fact that τ φ may be defined and equal to τ does not imply that Γ φ : τ ≤ τ holds for some Γ . Indeed, type instantiation does not check the premise of rule Inst-Abstr. This is intentional, as it avoids parametrizing type instantiation over the type environment. This means that type instantiation is not sound in general. This is never a problem, however, since we only use type instantiation originating from well-typed terms for which there always exists some context Γ such that Γ φ : τ ≤ τ . 2 3
The occurrence of α in the inside instantiation is bound by the under instantiation. There should never be any ambiguity with the operation a φ on expressions; moreover, both operations have strong similarities.
A Church-Style Intermediate Language for MLF Var x:τ ∈Γ Γ x:τ Abs
Let Γ a:τ
Γ, x : τ a : τ
App Γ a1 : τ2 → τ 1
Γ let x = a in a : τ
Γ, x : τ a : τ Γ λ(x : τ ) a : τ → τ
TAbs Γ, α τ a : τ α∈ / ftv(Γ ) Γ Λ(α τ ) a : ∀ (α τ ) τ
29
Γ a 2 : τ2
Γ a 1 a 2 : τ1 TApp Γ a:τ Γ φ : τ ≤ τ Γ a φ : τ
Fig. 4. Typing rules for x MLF
We say that types τ and τ are equivalent in Γ if there exist φ and φ such that Γ φ : τ ≤ τ and Γ φ : τ ≤ τ . Although types of x MLF are syntactically the same as the types of iMLF—the Curry-style version of MLF (Le Botlan and R´emy 2007)—they are richer, because type equivalence in x MLF is finer than type equivalence in iMLF, as will be explained in §3. Typing rules for x MLF. Typing rules are defined in Figure 4. Compared with System F, the novelties are type abstraction and type instantiation, unsurprisingly. The typing of a type abstraction Λ(α τ ) a extends the typing environment with the type variable α bound by τ . The typing of a type instantiation a φ resembles the typing of a coercion, as it just requires the instantiation φ to transform the type of a into the type of the result. Of course, it has the full power of the type application rule of System F. For example, the type instantiation a τ has type τ {α ← τ } provided the term a has type ∀ (α) τ . As in System F, a well-typed closed term has a unique type—in fact, a unique typing derivation. A let-binding let x = a1 in a2 cannot entirely be treated as an abstraction for an immediate application (λ(x : τ1 ) a2 ) a1 because the former does not require a type annotation on x whereas the latter does. This is nothing new, and the same as in System F extended with let-bindings. (Notice however that τ1 , which is the type of a1 , is fully determined by a1 and could be synthesized by a typechecker.) Example. Let id stand for the identity Λ(α ⊥) λ(x : α) x and τid for the type ∀ (α ⊥) α → α. We have id : τid . The function choice mentioned in the introduction, may be defined as Λ(β ⊥) λ(x : β) λ(y : β) x. It has type ∀ (β ⊥) β → β → β. The application of choice to id, which we refer to below as choice id, may be defined as Λ(β τid ) choice β (id (!β)) and has type ∀ (β τid ) β → β. The term choice id may also be given weaker types by type instantiation. For example, choice id has type (∀ (α ⊥) α → α) → (∀ (α ⊥) α → α) as in System F, while choice id (; ∀ (γ ) (∀ ( γ ); )) has the ML type ∀ (γ ⊥) (γ → γ) → γ → γ. Reduction. The semantics of the calculus is given by a small-step reduction semantics. We let reduction occur in any context, including under abstractions. That is, the evaluation contexts are single-hole contexts, given by the grammar: E ::= [ · ] | E φ | λ(x : τ ) E | Λ(α τ ) E | E a | a E | let x = E in a | let x = a in E
30
D. R´emy and B. Yakobowski (λ(x : τ ) a1 ) a2 let x = a2 in a1
−→ a1 {x ← a2 } −→ a1 {x ← a2 }
a½ −→ a a (φ; φ ) −→ a φ (φ ) a −→ Λ(α ⊥) a α∈ / ftv(a) (Λ(α τ ) a) −→ a{!α ← ½}{α ← τ } (Λ(α τ ) a) (∀ (α ) φ) −→ Λ(α τ ) (a φ) (Λ(α τ ) a) (∀ ( φ)) −→ Λ(α τ φ) a{!α ← φ; !α} E[a] −→ E[a ]
if a −→ a
(β) (βlet ) (ι-Id) (ι-Seq) (ι-Intro) (ι-Elim) (ι-Under) (ι-Inside) (Context)
Fig. 5. Reduction rules
The reduction rules are described in Figure 5. As usual, basic reduction steps contain β-reduction, with the two variants (β) and (βlet ). Other basic reduction rules, related to the reduction of type instantiations and called ι-steps, are described below. The one-step reduction is closed under the context rule. We write −→β and −→ι for the two subrelations of −→ that contains only Context and β-steps or ι-step, respectively. Finally, the reduction is the reflexive and transitive closure −→ → of the one-step reduction relation. Reduction of type instantiation. Type instantiation redexes are all of the form a φ. The first three rules do not constrain the form of a. The identity type instantiation is just dropped (Rule ι-Id). A type instantiation composition is replaced by the successive corresponding type instantiations (Rule ι-Seq). Rule ι-Intro introduces a new type abstraction in front of a; we assume that the bound variable α is fresh in a. The other three rules require the type instantiation to be applied to a type abstraction Λ(α τ ) a. Rule ι-Under propagates the type instantiation under the bound, inside the body a. By contrast, Rule ιInside propagates the type instantiation φ inside the bound, replacing τ by τ φ. However, as the bound of α has changed, the domain of the type instantiations !α is no more τ , but τ φ. Hence, in order to maintain well-typedness, all the occurrences of the instantiation !α in a must be simultaneously replaced by the instantiation (φ; !α). Here, the instantiation !α is seen as atomic, i.e. all occurrences of !α are substituted, but other occurrences of α are left unchanged (see the appendix for the formal definition). For instance, if a is the term Λ(α τ ) λ(x : α → α) λ(y : ⊥) y (α → α) (z (!α)) then, the type instantiation a (∀ ( φ)) reduces to: Λ(α τ φ) λ(x : α → α) λ(y : ⊥) y (α → α) (z (φ; !α)) Rule ι-Elim eliminates the type abstraction, replacing all the occurrences of α inside a by the bound τ . All the occurrences of !α inside τ (used to instantiate τ into α) become vacuous and must be replaced by the identity instantiation. For example, reusing the term a above, a reduces to λ(x : τ → τ ) λ(y : ⊥) y (τ → τ ) (z ½). Notice that type instantiations a τ and a (!α) are irreducible.
A Church-Style Intermediate Language for MLF
31
Examples of reduction. Let us reuse the term choice id defined in §1 as Λ(β τid ) choice β (id (!β)). Remember that τ stands for the System-F type application τ and expands to (∀ ( τ ); ). Therefore, the type instantiation choice β reduces to the term λ(x : β) λ(y : β) x by ι-Seq, ι-Inside and ι-Elim. Hence, the term choice id reduces by these rules, Context, and (β) to the expression Λ(β τid ) λ(y : β) id (!β). Below are three specialized versions of choice id (remember that ∀ (α) τ and Λ(α) a are abbreviations for ∀ (α ⊥) τ and Λ(α ⊥) a). Here, all type instantiations are eliminated by reduction, but this is not always possible in general. choice id int choice id choice id (; ∀ (γ ) (∀ ( γ ); ))
: (int → int) → (int → int) −→ → λ(y : int → int) (λ(x : int) x) : (∀ (α) α → α) → (∀ (α) α → α) −→ → λ(y : ∀ (α) α → α) (Λ(α) λ(x : α) x) : ∀ (γ) (γ → γ) → (γ → γ) −→ → Λ(γ) λ(y : γ → γ) (λ(x : γ) x)
System F as a subsystem of x MLF. System F can be seen as a subset of x MLF, using the following syntactic restrictions: all quantifications are of the form ∀ (α) τ and ⊥ is not a valid type anymore (however, as in System F, ∀ (α) α is); all type abstractions are of the form Λ(α) a; and all type instantiations are of the form a τ . The derived typing rule for Λ(α) a and a τ are exactly the System-F typing rules for type abstraction and type application. Hence, typechecking in this restriction of x MLF corresponds to typechecking in System F. Moreover, the reduction in this restriction also corresponds to reduction in System F. Indeed, a reducible type application is necessarily of the form (Λ(α) a) τ and can always be reduced to a{α ← τ } as follows: (Λ(α) a) τ = (Λ(α ⊥) a) (∀ ( τ ); ) −→ (Λ(α ⊥) a) (∀ ( τ )) () −→ (Λ(α ⊥τ ) a{!α ← τ ; !α}) () = (Λ(α τ ) a) () −→ a{!α ← ½}{α ← τ } = a{α ← τ }
(1) (2) (3) (4)
Step (1) is by definition; step (2) is by ι-Seq; step (3) is by ι-Inside, step (4) is by ι-Elim and equality steps (3) and (4) are by type instantiation and by assumption as a is a term of System F, thus in which !α does not appear.
2
Properties of Reduction
The reduction has been defined so that the type erasure of a reduction sequence in x MLF is a reduction sequence in the untyped λ-calculus. Formally, the type erasure of a term a of x MLF is the untyped λ-term a defined inductively by
x = x
a φ = a
a1 a2 = a1 a2
let x = a1 in a2 = let x = a1 in a2
λ(x : τ ) a = λ(x) a
Λ(α τ ) a = a
32
D. R´emy and B. Yakobowski
It is immediate to verify that two terms related by ι-reduction have the same type erasure. Moreover, if a β-reduces to a , then the type erasure of a β-reduces to the type erasure of a in one step in the untyped λ-calculus. 2.1
Subject Reduction
Reduction of x MLF, which can occur in any context, preserves typings. This relies on weakening and substitution lemmas for both instance and typing judgments. Lemma 3 (Weakening). Let Γ, Γ , Γ be a well-formed environment. If Γ, Γ φ : τ1 ≤ τ2 , then Γ, Γ , Γ φ : τ1 ≤ τ2 . If Γ, Γ a : τ , then Γ, Γ , Γ a : τ . Lemma 4 (Term substitution). Assume that Γ a : τ holds. If Γ, x : τ , Γ φ : τ1 ≤ τ2 then Γ, Γ φ : τ1 ≤ τ2 . If Γ, x : τ , Γ a : τ , then Γ, Γ a{x ← a } : τ The next lemma, which expresses that we can substitute an instance bound inside judgments, ensures the correctness of Rule ι-Elim. Lemma 5 (Bound substitution). Let ϕ and θ be respectively the substitutions {α ← τ } and {!α ← ½}{α ← τ }. If Γ, α τ, Γ φ : τ1 ≤ τ2 then Γ, Γ ϕ φθ : τ1 ϕ ≤ τ2 ϕ. If Γ, α τ , Γ a : τ then Γ, Γ ϕ aθ : τ ϕ. Finally, the following lemma ensures that an instance bound can be instantiated, proving in turn the correctness of the rule ι-Inside. Lemma 6 (Narrowing). Assume Γ φ : τ ≤ τ . Let θ be {!α ← φ; !α}. If Γ, α τ , Γ φ : τ1 ≤ τ2 then Γ, α τ , Γ φ θ : τ1 ≤ τ2 . If Γ, α τ, Γ a : τ then Γ, α τ , Γ aθ : τ Subject reduction is an easy consequence of all these results. Theorem 1 (Subject reduction). If Γ a : τ and a −→ a then, Γ a : τ . 2.2
Confluence
Theorem 2. The relation −→β is confluent. The relations −→ι and −→ are confluent on the terms well-typed in some context. This result is proved using the standard technique of parallel reductions (Barendregt 1984). Thus β-reduction and ι-reduction are independent; this allows for instance to perform ι-reductions under λ-abstractions as far as possible while keeping a weak evaluation strategy for β-reduction. The restriction to well-typed terms for the confluence of ι-reduction is due to two things. First, the rule ι-Inside is not applicable to ill-typed terms in which τ φ cannot be computed (for example (Λ(α int) a) (∀ ( ))). Second, τ φ can
A Church-Style Intermediate Language for MLF
33
sometimes be computed, even though Γ φ : τ ≤ τ never holds (for example if φ is !α and τ is not the bound of α in Γ ). Hence, type errors may be either revealed or silently reduced and perhaps eliminated, depending on the reduction path. As an example, let a be the term Λ(α ∀ (γ) γ) (Λ(β int) x) (∀ ( !α)) (∀ ( )) We have both a −→ Λ(α ⊥) (Λ(β int) x) (∀ ( ; !α)) −→, and a −→ Λ(α ∀ (γ) γ) Λ(β α) x (∀ ( )) −→ Λ(α ⊥) Λ(β α) x −→. The fact that ill-typed terms may not be confluent is not new: for instance, this is already the case with η-reduction in System F. We believe this is not a serious issue. In practice, this means that typechecking should be performed before any program simplification, which is usually the case anyway. 2.3
Strong Normalization
We conjecture, but have not proved, that all reduction sequences are finite. 2.4
Accommodating Weak Reduction Strategies and Constants
In order to show that the calculus may also be used as the core of a programming language, we now introduce constants and restricts the semantics to a weak evaluation strategy. We let the letter c range over constants. Each constant comes with its arity |c|. The dynamic semantics of constants must be provided by primitive reduction rules, called δ-rules. However, these are usually of a certain form. To characterize δ-rules (and values), we partition constants into constructors and primitives, ranged over by letters C and f , respectively. The difference between the two lies in their semantics: primitives (such as +) are reduced when fully applied, while constructors (such as cons) are irreducible and typically eliminated when passed as argument to primitives. In order to classify constructed values, we assume given a collection of type constructors κ, together with their arities |κ|. We extend types with constructed types κ (τ1 , . . . τ|κ| ). We write α for a sequence of variables α1 , . . . αk and ∀ (α) τ for the type ∀ (α1 ) . . . ∀ (αk ) τ . The static semantics of constants is given by an initial typing environment Γ0 that assigns to every constant c a type τ of the form ∀ (α) τ1 → . . . τn → τ0 , where τ0 is a constructed type whenever the constant c is a constructor. We distinguish a subset of terms, called values and written v. Values are term abstractions, type abstractions, full or partial applications of constructors, or partial applications of primitives. We use an auxiliary letter w to characterize the arguments of functions, which differ for call-by-value and call-by-name strategies. In values, an application of a constant c can involve a series of type instantiations, but only evaluated ones and placed before all other arguments. Moreover, the application may only be partial whenever c is a primitive. Evaluated instantiations θ may be quantifier eliminations or either inside or under
34
D. R´emy and B. Yakobowski
(general) instantiations. In particular, a τ and a (!α) are never values. The grammar for values and evaluated instantiations is as follows: v ::= λ(x : τ ) a | Λ(α : τ ) a n ≤ |C| | C θ1 . . . θk w1 . . . wn | f θ1 . . . θk w1 . . . wn n < |f | θ ::= ∀ ( φ) | ∀ (α ) φ | Finally, we assume that δ-rules are of the form f θ1 . . . θk w1 . . . w|f | −→f a (that is, δ-rules may only reduce fully applied primitives). In addition to this general setting, we make further assumptions to relate the static and dynamic semantics of constants. Subject reduction: δ-reduction preserves typings, i.e., for any typing context Γ such that Γ a : τ and a −→f a , the judgment Γ a : τ holds. Progress: Well-typed, full applications of primitives can be reduced, i.e., for any term a of the form f θ1 . . . θk w1 . . . wn verifying Γ0 a : τ , there exists a term a such that a −→f a . Call-by-value reduction. We now specialize the previous framework to a call-by-value semantics. In this case, arguments of applications in values are themselves restricted to values, i.e. w is taken equal to v. Rules (β) and (βlet ) are limited to the substitution of values, that is, to reductions of the form (λ(x : τ ) a) v −→ a{x ← v} and let x = v in a −→ a{x ← v}. Rules ι-Id, ι-Comp and ι-Intro are also restricted so that they only apply to values (e.g. a is textually replaced by v in each of these rules). Finally, we restrict rule Context to callby-value contexts, which are of the form Ev ::= [ · ] | Ev a | v Ev | Ev φ | let x = Ev in a We write −→ →v the resulting reduction relation. It follows from the above restrictions that the reduction is deterministic. Moreover, since δ-reduction preserves typings, by asumption, the relation −→ →v also preserves typings by Theorem 1. Progress holds for call-by-value. In combination with subject-reduction, this ensures that the evaluation of well-typed terms “cannot go wrong”. →v a for some a . Theorem 3. If Γ0 a : τ , then either a is a value or a −→ Call-by-value reduction and the value restriction. The value-restriction is the most standard way to add side effects in a call-by-value language. It is thus important to verify that it can be transposed to x MLF. Typically, the value restriction amounts to restricting type generalization to non-expansive expressions, which contain at least value-forms, i.e. values and term variables, as well as their type-instantiations. Hence, we obtain the following revised grammar for expansive expressions b and for non-expansive expressions u. b ::= u | b b | let x = u in b u ::= x | λ(x : τ ) b | Λ(α : τ ) u | u φ | C θ1 . . . θk u 1 . . . u n | f θ 1 . . . θk u 1 . . . u n
n ≤ |C| n < |f |
A Church-Style Intermediate Language for MLF
35
As usual, we restrict let-bound expressions to be non-expansive, since they implicitly contain a type generalization. Notice that, although type instantiations are restricted to non-expansive expressions, this is not a limitation: b φ can always be written as (λ(x : τ ) x φ) b, where τ is the type of a, and similarly for applications of constants to expansive expressions. Theorem 4. Expansive and non-expansive expressions are closed by call-byvalue reduction. Corollary 1. Subject reduction holds with the value restriction. It is then routine work to extend the semantics with a global store to model side effects and verify type soundness for this extension. Call-by-name reduction. For call-by-name reduction semantics, we can actually increase the set of values, which may contain applications of constants to arbitrary expressions; that is, we take a for w. The ι-reduction is restricted as for call-by-value. However, evaluation contexts are now En ::= [ · ] | En a | En φ. We write −→ →n the resulting reduction relation. As for call-by-value, it is deterministic by definition and preserves typings. It may also always progress. Theorem 5. If Γ0 a : τ , then either a is a value or a −→ →n a for some a .
3
Discussion
Elaboration of graphical eMLF into x MLF. To verify that, as expected, x MLF can be used as an internal language for eMLF, we have exhibited a typepreserving type-erasure-preserving translation from eMLF to x MLF. Technically, this translation is based on the presolutions of type inference problems in the graphic constraint framework of MLF. An important corollary is the type soundness of eMLF—in its most expressive4 version (R´emy and Yakobowski 2008b). By lack of space, this translation is however not presented in this paper, but can be found in (R´emy and Yakobowski 2008a). We also expect that x MLF could be used as an internal language for HML, another less expressive but simpler surface language for iMLF that has been recently proposed (Leijen 2009). Expressiveness of x MLF. The translation of eMLF into x MLF shows that x MLF is at least as expressive as eMLF. However, and perhaps surprisingly, the converse is not true. That is, there exist programs of x MLF that cannot be typed in MLF. While, this is mostly irrelevant when using MLF as an internal language for eMLF, the question is still interesting from a theoretical point of view, as understanding x MLF on its own, i.e. independently of the type inference constraints of eMLF, could perhaps suggest other useful extensions of x MLF. 4
So far, type-soundness has only been proved for the original, but slightly weaker variant of MLF (Le Botlan 2004) and for the shallow, recast version of MLF (Le Botlan and R´emy 2007).
36
D. R´emy and B. Yakobowski
For the sake of simplicity, we explain the difference between x MLF and iMLF, the Curry-style version of MLF (which has the same expressiveness as eMLF). Although syntactically identical, the types of x MLF and of syntactic iMLF differ in their interpretation of alias bounds, i.e. quantifications of the form ∀ (β α) τ . Consider, for example, the two types τ0 and τid defined as ∀ (ατ ) ∀ (β α) β → α and ∀ (α τ ) α → α. In iMLF, alias bounds can be expanded and τ0 and τid are equivalent. Roughly, the set of their instances (stripped of toplevel quantifiers) is {τ → τ | τ ≤ τ }. In contrast, the set of instances of τ0 is larger in x MLF and at least a superset of {τ → τ | τ ≤ τ ≤ τ }. This level of generality cannot be expressed in iMLF. The current treatment of alias bounds in x MLF is quite natural in a Churchstyle presentation. Surprisingly, it is also simpler than treating them as in eMLF. A restriction of x MLF without alias bounds that is closed under reduction and in closer correspondence with iMLF can still be defined a posteriori, by constraining the formation of terms, but the definition is contrived and unnatural. Instead of restricting x MLF to match the expressiveness of iMLF, a question worth further investigation is whether the treatment of alias bounds could be enhanced in iMLF and eMLF to match the one in x MLF without compromising type inference. Related works. A strong difference between eMLF and x MLF is the use of explicit coercions to trace the derivation of type instantiation judgments. A similar approach has already been used in a language with subtyping and intersection types, proposed as a target for the compilation of bounded polymorphism (Crary 2000). In both cases, coercions are used to make typechecking a trivial process. In our case, they are also exploited to make subject reduction easy—by introducing the language to describe how type instance derivations must be transformed during reduction. (We believe that the use of explicit coercions for simplifying subject-reduction proofs has been neglected.) In both approaches, reduction is split into a standard notion of β-reduction and a new form of reduction (which we call ι-reduction) that only deals with coercions, preserves type-erasures, and is (conjectured to be) strongly normalizing. There are also important differences. While both coercion languages have common forms, our coercions intendedly keep the instance-bounded polymorphism form ∀ (α τ ) τ . On the opposite, coercions are used to eliminate the subtype-bounded polymorphism form ∀ (α ≤ τ ) τ in (Crary 2000), using intersection types and contravariant arrow coercions instead, which we do not need. It would be worth checking whether union types, which are propsoed as an extension in (Crary 2000), could be used to encode away our instance-bounded polymorphism form. Besides this work and the several papers that describe variants of MLF, there are actually few other related works. Both Leijen and L¨oh (2005) and Leijen (2007) have studied the extension of MLF with qualified types, and as a subcase, the translation of MLF without qualified types into System F. However, in order to handle type instantiations, a term a of type ∀ (α τ ) τ is elaborated as a function of type ∀ (α) (τ → α) → τ , where τ is a runtime representation of τ . The first argument is a runtime coercion, which bears strong similarities with our instantiations. However, an important difference is that their coercions are at
A Church-Style Intermediate Language for MLF
37
the level of terms, while our instantiations are at the level of types. In particular, although coercion functions should not change the semantics, this critical result has not been proved so far, while in our settings the type-erasure semantics comes for free by construction. The impact of coercion functions in a call-byvalue language with side effects is also unclear. Perhaps, a closer connection between their coercion functions and our instantiations could be established and used to actually prove that their coercions do not alter the semantics. However, even if such a result could be proved, coercions should preferably remain at the type level, as in our setting, than be intermixed with terms, as in their proposal. Future works. The demand for an internal language for MLF was first made in the context of using the eMLF type system for the Haskell language. We expect x MLF to better accommodate qualified types than eMLF since no evidence function would be needed for flexible polymorphism, but it remains to be verified. Type instantiation, which changes the type of an expression without changing its meaning, goes far beyond type application in System F and resembles retyping functions in System Fη —the closure of F by η-conversion (Mitchell 1988). Those functions can be seen either at the level of terms, as expressions of System F that βη-reduces to the identity, or at the level of types as a type conversion. Some loose parallel can be made between the encoding of MLF in System F by Leijen and L¨ oh (2005) which uses term-level coercions, and x MLF which uses type-level instantiations. Additionally, perhaps Fη could be extended with a form of abstraction over retyping functions, much as type abstraction ∀ (α τ ) in x MLF amounts to abstracting over the instantiation !α of type τ → α. (Or perhaps, as suggested by the work of Crary (2000), intersection and union types could be added to Fη to avoid the need for abstracting over coercion functions.) Regarding type soundness, it is also worth noticing that the proof of subject reduction in x MLF does not subsume, but complements, the one in the original presentation of MLF. The latter does not explain how to transform type annotations, but shows that annotation sites need not be introduced (only transformed) during reduction. Because x MLF has full type information, it cannot say anything about type information that could be left implicit and inferred. Given a term in x MLF, can we rebuild a term in iMLF with minimal type annotations? While this should be easy if we require that corresponding subterms have identical types in x MLF and iMLF, the answer is unclear if we allow subterms to have different types. The semantics of x MLF allows reduction (and elimination) of type instantiations a φ through ι-reduction but does not operate reduction (and simplification) of instantiations φ alone. It would be possible to define a notion of reduction on instantiations φ −→ φ (such that, for instance, ∀ ( φ1 ; φ2 ) −→ ∀ ( φ1 ); ∀ ( φ2 ), or conversely?) and extend the reduction of terms with a context rule a φ −→ a φ whenever φ −→ φ . This might be interesting for more economical representations of instantiation. However, it is unclear whether there exists an interesting form of reduction that is both Church-Rosser and large enough for optimization purposes. Perhaps, one should rather consider instantiation transformations that preserve observational equivalence, which would leave more freedom in the way one instantiation could be replaced by another.
38
D. R´emy and B. Yakobowski
Extending x MLF to allow higher-order polymorphism is another interesting research direction for the future. Such an extension is already under investigation for the type inference version eMLF (Herms 2009). Conclusion. We have completed the MLF trilogy by introducing the Churchstyle version x MLF, that was still desperately missing for type-aware compilation and from a theoretical point of view. The original type-inference version eMLF, which requires partial type annotations but does not tell how to track them during reduction, now lies between the Curry-style presentation iMLF that ignores all type information and x MLF that maintains it during reduction. We have shown that x MLF is well-behaved: reduction preserves well-typedness, and the calculus is sound for both call-by-value and call-by-name semantics. Hence, x MLF can be used as an internal language for MLF, with either call-byvalue or call-by-name semantics, and also for the many restrictions of MLF that have been proposed, including HML. Indeed, the translation of partially typed eMLF programs into fully typed x MLF ones, presented in (R´emy and Yakobowski 2008a), preserves well-typedness and the type erasure of terms, and therefore ensures the type soundness of eMLF. Hopefully, this will help the adoption of MLF and maintain a powerful form of type inference in modern programming languages that will necessarily feature first-class polymorphism. Independently, the idea of enriching type applications to richer forms of type transformations might also be useful in other contexts.
References Barendregt, H.P.: The Lambda Calculus: Its Syntax and Semantics. North-Holland, Amsterdam (1984), ISBN: 0-444-86748-1 Crary, K.: Typed compilation of inclusive subtyping. In: ICFP 2000: Proceedings of the fifth ACM SIGPLAN international conference on Functional programming, pp. 68–81. ACM, New York (2000) Herms, P.: Partial Type Inference with Higher-Order Types. Master’s thesis, University of Pisa and INRIA (2009) (to appear) Le Botlan, D.: MLF: An extension of ML with second-order polymorphism and implicit instantiation. PhD thesis, Ecole Polytechnique (June 2004) (english version) Le Botlan, D., R´emy, D.: MLF: Raising ML to the power of System-F. In: Proceedings of the Eighth ACM SIGPLAN International Conference on Functional Programming, August 2003, pp. 27–38 (2003) Le Botlan, D., R´emy, D.: Recasting MLF. Research Report 6228, INRIA, Rocquencourt, BP 105, 78 153 Le Chesnay Cedex, France (June 2007) Leijen, D.: A type directed translation of MLF to System F. In: The International Conference on Functional Programming (ICFP 2007). ACM Press, New York (2007) Leijen, D.: Flexible types: robust type inference for first-class polymorphism. In: Proceedings of the 36th annual ACM Symposium on Principles of Programming Languages (POPL 2009), pp. 66–77. ACM, New York (2009) Leijen, D., L¨ oh, A.: Qualified types for MLF. In: ICFP 2005: Proceedings of the tenth ACM SIGPLAN international conference on Functional programming, pp. 144–155. ACM Press, New York (2005)
A Church-Style Intermediate Language for MLF
39
Mitchell, J.C.: Polymorphic type inference and containment. Information and Computation 2/3(76), 211–249 (1988) Jones, S.P.: Haskell 98 Language and Libraries: The Revised Report. Cambridge University Press, Cambridge (2003), ISBN: 0521826144 R´emy, D., Yakobowski, B.: A church-style intermediate language for MLF (extended version) (September 2008a), http://gallium.inria.fr/~ remy/mlf/xmlf.pdf R´emy, D., Yakobowski, B.: From ML to MLF: Graphic type constraints with efficient type inference. In: The 13th ACM SIGPLAN International Conference on Functional Programming (ICFP 2008), Victoria, BC, Canada, September 2008, pp. 63–74 (2008b) Yakobowski, B.: Graphical types and constraints: second-order polymorphism and inference. PhD thesis, University of Paris 7 (December 2008)
A
Appendix: Definition of a{!α0 ← φ0 }
Formally, a θ where θ is {!α0 ← φ0 } is defined recursively as follows (we assume α = α0 ). The interesting lines are the two first ones of the second column; other lines are just lifting the substitution from the leaves to types, type instantiations, and terms in the usual way. Types τθ=τ Terms xθ (a1 a1 ) θ (a φ) θ (λ(x : τ ) a) θ (Λ(α τ ) a) θ
=x = (a1 θ) (a1 θ) = a (φ θ) = λ(x : τ θ) a θ = Λ(α : τ θ) a θ
Type instantiations !α θ = !α !α0 θ = φ0 (τ ) θ = (τ θ) (∀ ( φ)) θ = ∀ ( φ θ) (∀ (α ) φ) θ = ∀ (α ) (φ θ) (φ; φ ) θ = (φ θ); (φ θ) θ = θ = ½θ = ½
ΠΣ: Dependent Types without the Sugar Thorsten Altenkirch1 , Nils Anders Danielsson1 , Andres L¨ oh2 , and Nicolas Oury3 2
1 School of Computer Science, University of Nottingham Institute of Information and Computing Sciences, Utrecht University 3 Division of Informatics, University of Edinburgh
Abstract. The recent success of languages like Agda and Coq demonstrates the potential of using dependent types for programming. These systems rely on many high-level features like datatype definitions, pattern matching and implicit arguments to facilitate the use of the languages. However, these features complicate the metatheoretical study and are a potential source of bugs. To address these issues we introduce ΠΣ, a dependently typed core language. It is small enough for metatheoretical study and the type checker is small enough to be formally verified. In this language there is only one mechanism for recursion—used for types, functions and infinite objects—and an explicit mechanism to control unfolding, based on lifted types. Furthermore structural equality is used consistently for values and types; this is achieved by a new notion of α-equality for recursive definitions. We show, by translating several high-level constructions, that ΠΣ is suitable as a core language for dependently typed programming.
1
Introduction
Dependent types offer programmers a flexible path towards formally verified programs and, at the same time, opportunities for increased productivity through new ways of structuring programs (Altenkirch et al. 2005). Dependently typed programming languages like Agda (Norell 2007) are gaining in popularity, and dependently typed programming is also becoming more popular in the Coq community (Coq Development Team 2009), for instance through the use of some recent extensions (Sozeau 2008). An alternative to moving to full-blown dependent types as present in Agda and Coq is to add dependently typed features without giving up a traditional view of the distinction between values and types. This is exemplified by the presence of GADTs in Haskell, and by more experimental systems like Ωmega (Sheard 2005), ATS (Cui et al. 2005), and the Strathclyde Haskell Enhancement (McBride 2009). Dependently typed languages tend to offer a number of high-level features for reducing the complexity of programming in such a rich type discipline, and at the same time improve the readability of the code. These features include: Datatype definitions. A convenient syntax for defining dependently typed families inductively and/or coinductively. M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 40–55, 2010. c Springer-Verlag Berlin Heidelberg 2010
ΠΣ: Dependent Types without the Sugar
41
Pattern matching. Agda offers a very powerful mechanism for dependently typed pattern matching. To some degree this can be emulated in Coq by using Sozeau’s new tactic Program (2008). Hidden parameters. In dependently typed programs datatypes are often indexed. The indices can often be inferred using unification, which means that the programmer does not have to read or write them. This provides an alternative to polymorphic type inference a` la Hindley-Milner. These features, while important for the usability of dependently typed languages, complicate the metatheoretic study and can be the source of subtle bugs in the type checker. To address such problems, we can use a core language which is small enough to allow metatheoretic study. A verified type checker for the core language can also provide a trusted core in the implementation of a full language. Coq makes use of a core language, the Calculus of (Co)Inductive Constructions (CCIC, Gim´enez 1996). However, this calculus is quite complex: it includes the schemes for strictly positive datatype definitions and the accompanying recursion principles. Furthermore it is unclear whether some of the advanced features of Agda, such as dependently typed pattern matching, the flexible use of mixed induction/coinduction, and induction-recursion, can be easily translated into CCIC or a similar calculus. (One can argue that a core language is less useful if the translation from the full language is difficult to understand.) In the present paper we suggest a different approach: we propose a core language that is designed in such a way that we can easily translate the high-level features mentioned above; on the other hand, we postpone the question of totality. Totality is important for dependently typed programs, partly because non-terminating proofs are not very useful, and partly for reasons of efficiency: if a certain type has at most one total value, then total code of that type does not need to be run at all. However, we believe that it can be beneficial to separate the verification of totality from the functional specification of the code. A future version of our core language may have support for independent certificates of totality (and the related notions of positivity and stratification); such certificates could be produced manually, or through the use of a termination checker. The core language proposed in this paper is called ΠΣ and is based on a small collection of basic features:1 – – – –
Dependent function types (Π-types) and dependent product types (Σ-types). A (very) impredicative universe of types with Type : Type. Finite sets (enumerations) using reusable and scopeless labels. A general mechanism for mutual recursion, allowing the encoding of advanced concepts such as induction-recursion. – Lifted types, which are used to control recursion. These types offer a convenient way to represent mixed inductive/coinductive definitions. – A definitional equality which is structural for all definitions, whether types or programs, enabled by a novel definition of α-equality for recursive definitions.
1
The present version is a simplification of a previous implementation, described in an unpublished draft (Altenkirch and Oury 2008).
42
T. Altenkirch et al.
We have implemented an interactive type checker/interpreter for ΠΣ in Haskell.2 Before we continue, note that we have not yet developed the metatheory of ΠΣ formally, so we would not be surprised if there were some problems with the presentation given here. We plan to establish important metatheoretic properties such as soundness (well-typed programs do not get stuck) for a suitably modified version of ΠΣ in a later paper. The main purpose of this paper is to introduce the general ideas underlying the language, and to start a discussion about them. Outline of the Paper We introduce ΠΣ by giving examples showing how high-level dependently typed programming constructs can be represented (Sect. 2). We then specify the operational semantics (Sect. 3), develop the equational theory (Sect. 4), and present the type system (Sect. 5). Finally we conclude and discuss further work (Sect. 6). Related Work We have already mentioned the core language CCIC (Gim´enez 1996). Another dependently typed core language, that of Epigram (Chapman et al. 2006), is basically a framework for implementing a total type theory, based on elimination combinators. Augustsson’s influential language Cayenne (1998) is like ΠΣ a partial language, but is not a core language. The idea to use a partial core language was recently and independently suggested by Coquand et al. (2009), who propose a language called Mini-TT, which is also related to Coquand’s Calculus of Definitions (2008). Mini-TT uses a nominal equality, unlike ΠΣ’s structural equality, and unfolding of recursive definitions is not controlled explicitly by using lifted types, but by not unfolding inside patterns and sum types. The core language FC (Sulzmann et al. 2007) provides support for GADTs, among other things. The use of lifted types is closely related to the use of suspensions to encode non-strictness in strict languages (Wadler et al. 1998).
2
ΠΣ by Example
In this section we first briefly introduce the syntax of ΠΣ (Sect. 2.1). The rest of the section demonstrates how to encode a number of high-level features from dependently typed programming languages in ΠΣ: (co)datatypes (Sects. 2.2 and 2.3), equality (Sect. 2.4), families of datatypes (Sect. 2.5), and finally induction-recursion (Sect. 2.6). 2.1
Syntax Overview
The syntax of ΠΣ is defined as follows: 2
The package pisigma is available from Hackage (http://hackage.haskell.org).
ΠΣ: Dependent Types without the Sugar Terms
43
t, u, σ, τ ::= let Γ in t | x | Type | (x : σ) → τ | λx → t | t u | (x : σ) ∗ τ | (t, u) | split t with (x , y) → t | {l } | l | case t of {l → u | } | ↑σ | [t ] | !t | Rec σ | fold t | unfold t as x → u
Contexts Γ, Δ
::= ε | Γ ; x : σ | Γ ; x = t
While there is no syntactic difference between terms and types, we use the metavariables σ and τ to highlight positions where terms play the role of types. We write a s for an s-separated sequence of as. The language supports the following concepts: Type. The type of types is Type. Since ΠΣ is partial anyway due to the use of general recursion, we also assume Type : Type. Dependent functions. We use the same notation as Agda, writing (x : σ) → τ for dependent function types. Dependent products. We write (x :σ)∗τ for dependent products. Elements are constructed using tuple notation: (t , u). The eliminator split t with (x , y) → u deconstructs the scrutinee t , binding its components to x and y, and then evaluates u. Enumerations. Enumerations are finite sets, written {l }. The labels l do not interfere with identifiers, can be reused and have no scope. To disambiguate them from identifiers we use l to construct an element of an enumeration. The eliminator case t of {l → u | } analyzes the scrutinee t and chooses the matching branch. Lifting. A lifted type ↑σ contains boxed terms [t ]. Definitions are not unfolded inside boxes. If a box is forced using !, then evaluation can continue. To enable the definition of recursive types we introduce a type former Rec which turns a suspended type (i.e., an inhabitant of ↑Type) into a type. Rec comes together with a constructor fold and an eliminator unfold. Let. A let expression’s first argument Γ is a context, i.e., a sequence of declarations x : σ and (possibly recursive) definitions x = t . Definitions and declarations may occur in any order in a let context, subject to the following constraints: – Before a variable can be defined, it must first be declared. – Every declared variable must be defined exactly once in the same context (subject to shadowing, i.e., x : σ; x = t ; x : τ ; x = u is fine). – Every declaration and definition has to type check with respect to the previous declarations and definitions. Note that the order matters and that we cannot always shift all declarations before all definitions, because type checking a declaration may depend on the definition of a mutually defined variable (see Sect. 2.6). To simplify the presentation, ΠΣ—despite being a core language—allows a modicum of syntactic sugar: A non-dependent function type can be written as σ → τ , and a non-dependent product as σ ∗ τ (both → and ∗ associate to the right). Several variables may be bound at once in λ abstractions (λx1 x2 . . . xn → t ),
44
T. Altenkirch et al.
function types ((x1 x2 . . . xn :σ) → τ ), and product types ((x1 x2 . . . xn :σ)∗τ ). We can also combine a declaration and a subsequent definition: instead of x :σ; x = t we write x : σ = t . Finally we write unfold t as a shorthand for unfold t as x → x . 2.2
Datatypes
ΠΣ does not have a builtin mechanism to define datatypes. Instead, we rely on its more primitive features—finite types, Σ-types, recursion and lifting—to model datatypes. As a simple example, consider the declaration of (Peano) natural numbers and addition. We represent Nat as a recursively defined Σ-type whose first component is a tag (zero or suc), indicating which constructor we are using, and whose second component gives the type of the constructor arguments: Nat : Type = (l : {zero suc }) ∗ case l of {zero → Unit | suc → Rec [Nat ]};
In the case of zero we use a one element type Unit which is defined by Unit : Type = {unit }. The recursive occurrence of Nat is placed inside a box (i.e., [Nat ] rather than Nat). Boxing prevents infinite unfolding during evaluation. Evaluation is performed while testing type equality, and boxing is essential to keep the type checker from diverging. Note also that we need to use Rec because [Nat ] has type ↑Type but we expect an element of Type here. Using the above representation we can derive the constructors zero : Nat = ( zero, unit) and suc :Nat → Nat = λi → ( suc, fold i). (We use fold to construct an element of Rec [Nat ].) Addition can then be defined as follows: add : Nat → Nat → Nat ; add = λm n → split m with (ml , mr ) → ! case ml of { zero → [n ] | suc → [suc (add (unfold mr ) n)]};
Here we use dependent elimination, i.e., the typing rules for split and case exploit the constraint that the scrutinized term is equal to the corresponding pattern. In the zero branch mr has type Unit, and in the suc branch it has type Rec [Nat ]. In the latter case we use unfold to get a term of type Nat . Note that, yet again, we use boxing to stop the infinite unfolding of the recursive call (type checking a dependently typed program can involve evaluation under binders). We have to box both branches of the case to satisfy the type checker—they both have type ↑Nat . Once the variable ml gets instantiated with a concrete label the case reduces and the box in the matching case branch gets forced by the !. As a consequence, computations like 2 + 1, i.e., add (suc (suc zero)) (suc zero), evaluate correctly—in this case to ( suc, fold ( suc, fold ( suc, fold ( zero, unit)))). 2.3
Codata
The type Nat is an eager datatype, corresponding to an inductive definition. In particular, it does not make sense to write the following definition:
ΠΣ: Dependent Types without the Sugar
45
omega : Nat = ( suc, fold omega);
Here the recursive occurrence is not guarded by a box and hence omega will simply diverge if evaluation to normal form is attempted. To define a lazy or coinductive type like the type of streams we have to use lifting (↑ . . . ) explicitly in the type definition: Stream : Type → Type = λA → A ∗ Rec [↑(Stream A)];
We can now define programs by corecursion. As an example we define from, a function that creates streams of increasing numbers: from : Nat → Stream Nat ; from = λn → (n, fold [from (suc n)]);
The type system forces us to protect the recursive occurrence with a box. Evaluation of from zero terminates with (zero, let n : Nat = zero in fold [from (suc n)]). The use of lifting to indicate corecursive occurrences allows a large flexibility in defining datatypes. In particular, it facilitates the definition of mixed inductive/coinductive types such as the type of stream processors (Hancock et al. 2009), a concrete representation of functions on streams: SP : Type → Type → Type; SP = λA B → (l : {get put }) ∗ case l of { get → A → Rec [SP A B ] | put → B ∗ Rec [↑(SP A B )]};
The basic idea of stream processors is that we can only perform a finite number of get s before issuing the next of infinitely many puts. As an example we define the identity stream processor corecursively: idsp : (A : Type) → SP A A; idsp = λA → ( get , (λa → fold ( put , (a, fold [idsp A]))));
We can use mixed recursion/corecursion to define the semantics of stream processors in terms of functions on streams: eval : (A B : Type) → SP A B → Stream A → Stream B ; eval = λA B sp aas → split sp with (sp l , sp r ) → !case sp l of { get → split aas with (a, aas ) → [(eval A B (unfold (sp r a)) (!(unfold aas )))] | put → split sp r with (b, sp ) → [(b, fold [eval A B (!(unfold sp )) aas ])]};
Inspired by ΠΣ the latest version of Agda also supports definitions using mixed induction and coinduction, using basically the same mechanism (but in a total setting). Applications of such mixed definitions are explored in more detail by Danielsson and Altenkirch (2009). High-level languages such as Agda and Coq control the evaluation of recursive definitions using the evaluation context, with different mechanisms for defining datatypes and values. In contrast, ΠΣ handles recursion uniformly, at the cost of additional annotations stating where to lift, box and force. For a core language this seems to be a price worth paying, though. We can still recover syntactical conveniences as part of the translation of high-level features.
46
T. Altenkirch et al.
2.4
Equality
ΠΣ does not come with a propositional equality like the one provided by inductive families in Agda, and currently such an equality cannot, in general, be defined. This omission is intentional. In the future we plan to implement an extensional equality, similar to that described by Altenkirch et al. (2007), by recursion over the structure of types. However, for types with decidable equality a (substitutive) equality can be defined in ΠΣ as it is. As an example, we consider natural numbers again (cf. Sect. 2.2); see also Sect. 2.6 for a generic approach. Using Bool : Type = {true false }, we first implement a decision procedure for equality of natural numbers (omitted here due to lack of space): eqNat : Nat → Nat → Bool ;
We can lift a Bool ean to the type level using the truth predicate T , and then use that to define equality: Empty : Type = { }; T : Bool → Type = λb → case b of {true → Unit | false → Empty }; EqNat : Nat → Nat → Type = λm n → T (eqNat m n);
Using recursion we now implement a proof 3 of reflexivity: reflNat : (n : Nat) → EqNat n n; reflNat = λn → split n with (nl , nr ) → !case nl of { zero → [ unit ] | suc → [reflNat (unfold nr )]};
Note the use of dependent elimination to encode dependent pattern matching here. Currently dependent elimination is only permitted if the scrutinee is a variable (as is the case for n and nl here; see Sect. 5), but the current design lacks subject reduction for open terms (see Sect. 6), so we may reconsider this design choice. To complete the definition of equality we have to show that EqNat is substitutive. This can also be done by recursion over the natural numbers (we omit the definition for reasons of space): substNat : (P : Nat → Type) → (m n : Nat ) → EqNat m n → P m → P n;
Using substNat and reflNat it is straightforward to show that EqNat is a congruence. For instance, transitivity can be proved as follows: transNat : (i j k : Nat ) → EqNat i j → EqNat j k → EqNat i k ; transNat = λi j k p q → substNat (λx → EqNat i x ) j k q p;
The approach outlined above is limited to types with decidable equality. While we can define a non-boolean equality eqStreamNat : Stream Nat → Stream Nat → Type (corresponding to the extensional equality of streams, or bisimulation), we cannot derive a substitution principle. The same applies to function types, where we can define an extensional equality but not prove it to be substitutive. 3
Because termination is not checked, reflNat is not a formal proof. However, in this case it easy to see that the definition is total.
ΠΣ: Dependent Types without the Sugar
2.5
47
Families
Dependent datatypes, or families, are the workhorse of dependently typed languages like Agda. As an example, consider the definition of vectors (lists indexed by their length) in Agda: data Vec (A : Set ) : N → Set where [ ] : Vec A zero :: : {n : N} → A → Vec A n → Vec A (suc n)
Using another family, the family of finite sets, we can define a total lookup function for vectors. This function, unlike its counterpart for lists, will never raise a runtime error: data Fin : N → Set where zero : {n : N} → Fin (suc n) suc : {n : N} → Fin n → Fin (suc n) lookup : ∀ {A n } → Fin n → Vec A n → A lookup zero (x :: xs) = x lookup (suc i) (x :: xs) = lookup i xs
How can we encode these families and the total lookup function in ΠΣ? One possibility is to use recursion over the natural numbers: Vec : Type → Nat → Type; Vec = λA n → split n with (nl , nr ) → ! case nl of { zero → [Unit ] | suc → [A ∗ Vec A (unfold nr )]}; Fin : Nat → Type; Fin = λn → split n with (nl , nr ) → case nl of { zero → { } | suc → (l : {zero suc }) ∗ ! case l of { zero → [Unit ] | suc → [Fin (unfold nr )]}};
Given these types it is straightforward to define lookup by recursion over the natural numbers: lookup : (A : Type) → (n : Nat ) → Fin n → Vec A n → A;
However, these recursive encodings do not reflect the nature of the definitions in Agda, which are not using recursion over the indices. There are types which cannot easily be encoded this way, for example the simply typed λ-terms indexed by contexts and types. To define Agda-style families we can use explicit equalities instead: Vec : Type → Nat → Type; Vec = λA n → (l : {nil cons }) ∗ case l of { nil → EqNat zero n | cons → (n : Nat) ∗ A ∗ (Rec [Vec A n ]) ∗ EqNat (suc n ) n }; Fin : Nat → Type; Fin = λn → (l : {zero suc }) ∗ case l of { zero → (n : Nat) ∗ EqNat (suc n ) n | suc → (n : Nat) ∗ (Rec [Fin n ]) ∗ EqNat (suc n ) n };
48
T. Altenkirch et al.
Terms corresponding to the Agda constructors, for instance :: , are definable: cons : (A : Type) → (n : Nat ) → A → Vec A n → Vec A (suc n); cons = λA n a v → ( cons, (n, (a, (fold v , reflNat (suc n)))));
For reasons of space we omit the implementation of lookup based on these definitions—it has to make the equational reasoning which is behind the Agda pattern matching explicit (Goguen et al. 2006). 2.6
Universes
In Sect. 2.4 we remarked that we can define equality for types with a decidable equality on a case by case basis. However, we can do better, using the fact that datatype-generic programming via reflection can be represented within a language like ΠΣ. Which types have a decidable equality? Clearly, if we only use enumerations, dependent products and (well-behaved) recursion, then equality is decidable. We can reflect the syntax and semantics of this subset of ΠΣ’s type system as a type. We exploit the fact that ΠΣ allows very flexible mutually recursive definitions to encode induction-recursion (Dybjer and Setzer 2006). We define a universe U of type codes together with a decoding function El . We start by declaring both: U : Type; El : U → Type;
We can then define U using the fact that we know the type (but not the definition) of El : U = (l : {enum sigma box }) ∗ case l of {enum → Nat | sigma → Rec [(a : U ) ∗ (El a → U )] | box → Rec [↑U ]};
Note that we define enumerations up to isomorphism—we only keep track of the number of constructors. El is defined by exploiting that we know the types of U and El , and also the definition of U : El = λu → split u with (ul , ur ) → ! case ul of { enum → [Fin ur ] | sigma → [unfold ur as ur → split ur with (b, c) → (x : El b) ∗ El (c x )] | box → [unfold ur as ur → Rec [El (!ur )]]};
Note that, unlike in a simply typed framework, we cannot arbitrarily change the order of the definitions above—the definition of U is required to type check El . ΠΣ supports any kind of mutually recursive definition, with declarations and definitions appearing in any order (subject to the requirement that there is exactly one definition per declaration), as long as each item type checks with respect to the previous items.
ΠΣ: Dependent Types without the Sugar
49
It is straightforward to translate type definitions into elements of U . For instance, Nat can be represented as follows: nat : U = ( sigma, fold (( enum, suc (suc zero)), (λi → split i with (il , ir ) → ! case il of { zero → [( enum, suc zero)] | suc → [( box , fold [nat ])]})));
We can now define a generic decidable equality eq :(a :U ) → El a → El a → Bool by recursion over U (omitted here). Note that the encoding can also be used for families with decidable equality; Fin can for instance be encoded as an element of El nat → U .
3
β-Reduction
This section gives an operational semantics for ΠΣ by inductively defining the notion of (weak) β-reduction. Instead of defining substitution we use local definitions; in this sense ΠΣ is an explicit substitution calculus. We start by defining Δ x = t , which is derivable if the definition x = t is visible in Δ. The rules are straightforward: Δ; x = t x = t
Δx =t x ≡y
Δx =t x ≡y
Δ; y : σ x = t
Δ; y = u x = t
Values (v ), weak head normal forms (w) and neutral values (n) are defined as follows: v ::= w | n w ::= Type | (x : σ) → τ | λx → t | (x : σ) ∗ τ | (t, u) | {l } | l | ↑σ | [t ] | Rec σ | fold t n ::= x | n t | split n with (x , y) → t | case n of {l → t | } | !n | unfold n as x → t
We specify β-reduction using a big-step semantics; Δ t v means that t β-reduces to v in the context Δ. To avoid cluttering the rules with renamings we give simplified rules in which we assume that variables are suitably fresh; x # Δ means that x is not declared in Δ. Reduction and equality do not keep track of all type information, so we allow declarations without a type signature, denoted by x :, and use the abbreviation x := t for x :; x = t . We also overload ; for concatenation of contexts: Δ t (t0 , t1 ) Δw w Δx =t
x, y # Δ
Δ split t with (x , y) → u v
Δt v
Δ t λx → t
Δx v Δ ui v
Δ case t of {li → ui | } v x #Δ
x #Δ
Δ let x := u in t v
Δt u v
Δ t li Δ t fold t
Δ let x := t0 ; y := t1 in u v
Δ t [u ]
Δu v
Δ !t v
Δ let x := t in u v Δ; Γ t v
Δ unfold t as x → u v
Δ let Γ in v → v
Δ let Γ in t v
50
T. Altenkirch et al.
The let rule uses the auxiliary relation Δ let Γ in v → v , which pushes lets inside constructors. We only give a representative selection of the rules for this relation. Note that it maps neutral terms to neutral terms: x # Δ; Γ Δ let Γ in λx → t → λx → let Γ in t
Δ let Γ in [t ] → [let Γ in t ]
Δ; Γ x =t
Δ let Γ in n → n
Δ let Γ in x →x
Δ let Γ in n t → n (let Γ in t)
Δ let Γ in n → n
x # Δ; Γ
Δ let Γ in unfold n as x → t → unfold n as x → let Γ in t
Finally we give some of the rules for computations which are stuck:
4
Δ x =t
Δt n
Δt n
Δx x
Δt un u
Δ unfold t as x → u unfold n as x → u
α- and β-Equality
As mentioned earlier, ΠΣ uses a structural equality for recursive definitions. This makes it necessary to define a novel notion of α-equality. Let us look at some examples. We have already discussed the use of boxes to stop infinite unfolding of recursive definitions. This is achieved by specifying that inside a box we are only using α-equality. For instance, the following terms are not β-equal, because this would require looking up a definition inside a box:4 let x : Bool = true in [x ] ≡β [ true ].
However, we still want the ordinary α-equality let x : Bool = true in [x ] ≡α let y : Bool = true in [y ]
to hold, because we can get from one side to the other by consistently renaming bound variables. This means that we have to compare the definitions of variables which we want to identify—while being careful not to expand recursive definitions indefinitely—because clearly let x : Bool = true in [x ] ≡β let y : Bool = y in [y ].
We also want to allow weakening: let x : Bool = true; y : Bool = false in [x ] ≡α let z : Bool = true in [z ].
We achieve the goals outlined above by specifying that two expressions of the form let Γ in t and let Γ in t are α-equivalent if there is a partial bijection (a bijection on a subset) between the variables defined in Γ and Γ so that t and t are α-equal up to this identification of variables; the identified variables, 4
In this particular example the definition is not actually recursive, but this is irrelevant, because we do not distinguish between recursive and non-recursive definitions.
ΠΣ: Dependent Types without the Sugar
51
in turn, are required to have β-equal definitions (up to some relevant partial bijection). In the implementation we construct this identification lazily: if we are forced to identify two let-bound variables, we replace the definitions of these variables with a single, fresh (undefined) variable and check whether the original definitions are equal. This way we do not unfold recursive definitions more than once. We specify partial bijections using ϕ ::= ε | ϕ; (ι, o), where ι, o ::= x | −. Here (x , −) is used when the variable x is ignored, i.e., not a member of the partial bijection. Lookup is specified as follows: ϕ x ∼y ϕ; (x , y) x ∼ y
x ≡ι
y ≡o
ϕ; (ι, o) x ∼ y
We specify α- and β-equality at the same time, indexing the rules on the metavariable κ ∈ {α, β}, because all but one rule is shared between the two equalities. The judgement ϕ : Δ ∼ Δ t ≡κ t means that, given a partial bijection ϕ for the contexts Δ and Δ , the terms t and t are κ-equivalent. The difference between α- and β-equality is that the latter is closed under βreduction: Δt v
Δ t v
ϕ : Δ ∼ Δ v ≡ β v
ϕ : Δ ∼ Δ t ≡β t
The remaining rules apply to both equalities. Variables are equal if identified by the partial bijection: ϕ x ∼y ϕ : Δ ∼ Δ x ≡κ y
A congruence rule is included for each term former. For reasons of space we omit most of these rules—the following are typical examples: ϕ : Δ ∼ Δ t ≡ κ t ϕ : Δ ∼ Δ u ≡ κ u
ϕ; (x , x ) : (Δ; x :) ∼ (Δ ; x :) t ≡κ t
ϕ : Δ ∼ Δ t u ≡κ t u
ϕ : Δ ∼ Δ λx → t ≡κ λx → t
As noted above, the congruence rule for boxes only allows α-equality in the premise: ϕ : Δ ∼ Δ t ≡α t ϕ : Δ ∼ Δ [t ] ≡κ [t ]
Finally we have some rules for let expressions. Empty lets can be added, and contexts can be merged (we omit the symmetric cases): ϕ : Δ ∼ Δ let ε in t ≡κ t
ϕ : Δ ∼ Δ let Γ ; Γ in t ≡κ t
ϕ : Δ ∼ Δ t ≡κ t
ϕ : Δ ∼ Δ let Γ in let Γ in t ≡κ t
There is also a congruence rule. This rule uses an auxiliary judgement ϕ:Δ ∼ Δ ψ : Γ ∼ Γ which extends a partial bijection over a pair of contexts (ψ can be seen as the rule’s “output”). Note that ; is overloaded for concatenation: ϕ : Δ ∼ Δ ψ : Γ ∼ Γ
ϕ; ψ : (Δ; Γ ) ∼ (Δ ; Γ ) t ≡κ t
ϕ : Δ ∼ Δ let Γ in t ≡κ let Γ in t
52
T. Altenkirch et al.
The rules for ϕ : Δ ∼ Δ ψ : Γ ∼ Γ allow us to choose which variables get identified and which are ignored. The base case is ϕ : Δ ∼ Δ ε : ε ∼ ε. We can extend a partial bijection by identifying two variables, under the condition that the associated types are β-equal: ϕ : Δ ∼ Δ ψ : Γ ∼ Γ
ϕ; ψ : (Δ; Γ ) ∼ (Δ ; Γ ) σ ≡β σ
ϕ : Δ ∼ Δ (ψ; (x , x )) : (Γ ; x : σ) ∼ (Γ ; x : σ )
Alternatively, we can ignore a declaration: ϕ : Δ ∼ Δ ψ : Γ ∼ Γ
ϕ : Δ ∼ Δ ψ : Γ ∼ Γ
ϕ : Δ ∼ Δ (ψ; (x , −)) : (Γ ; x : σ) ∼ Γ
ϕ : Δ ∼ Δ (ψ; (−, x )) : Γ ∼ (Γ ; x : σ )
To check whether two definitions are equal we compare the terms using β-equality. Note that this takes place before the definitions are added to the context: ϕ : Δ ∼ Δ ψ : Γ ∼ Γ
ϕ; ψ x ∼ x
ϕ; ψ : (Δ; Γ ) ∼ (Δ ; Γ ) t ≡β t
ϕ : Δ ∼ Δ ψ : (Γ ; x = t) ∼ (Γ ; x = t )
The definition of an ignored variable has to be ignored as well: ϕ : Δ ∼ Δ ψ : Γ ∼ Γ
ϕ; ψ x ∼ −
ϕ : Δ ∼ Δ ψ : Γ ∼ Γ
ϕ : Δ ∼ Δ ψ : (Γ ; x = t) ∼ Γ
ϕ; ψ − ∼ x
ϕ : Δ ∼ Δ ψ : Γ ∼ (Γ ; x = t )
Definitions and declarations can also be reordered if they do not depend on each other. For reasons of space we omit the details. In the typing rules in Sect. 5 we only use κ-equality with respect to the identity partial bijection, so we define Δ t ≡κ u to mean idΔ : Δ ∼ Δ t ≡κ u.
5
The Type System
We define a bidirectional type system. There are three main, mutually inductive judgements: Γ Δ, which means that Δ is well-formed with respect to Γ ; Γ t ⇐ σ, which means that we can check that t has type σ; and Γ t ⇒ σ, which means that we can infer that t has type σ. We also use Γ x : σ, which means that x : σ is visible in Γ : Γ;x : σ x : σ
Γ x :σ x ≡y
Γ x :σ
Γ;y : τ x : σ
Γ;y = t x : σ
Context formation is specified as follows: Γ Δ Γ ε
Γ ; Δ σ ⇐ Type
Γ Δ
Γ Δ; x : σ
Γ;Δ x ⇒ σ
Γ;Δ t ⇐ σ
Γ Δ; x = t
There is one type checking rule which applies to all terms: the conversion rule. This rule changes the direction: if we want to check whether a term has type τ , we can first infer the type σ and then verify that σ and τ are convertible: Γ τ ⇐ Type
Γ t ⇒σ Γ t ⇐τ
Γ σ ≡β τ
ΠΣ: Dependent Types without the Sugar
53
The remaining rules apply to specific term formers. We have chosen to simplify some of the rules to avoid the use of renamings. This means that the type system, as given, is not closed under α-equality. The rules below use two new metavariables: , which stands for the quantifiers → and ∗; and ⇔, which can be instantiated with either ⇒ or ⇐. We also use the notation Γ t ⇒β σ, which stands for Γ t ⇒ σ and Γ σ σ. This is used when we match against a particular type constructor: Γ Δ Γ ρ ⇐ Type Γ ρ ≡α let Δ in σ Γ ; Δ t ⇐ σ
Γ Δ
Γ let Δ in t ⇐ ρ εΓ
Γ;Δ t ⇒ σ
Γ let Δ in t ⇒ let Δ in σ
Γ x :σ
εΓ
Γ x ⇒σ
Γ Type ⇔ Type
Γ σ ⇐ Type Γ , x : σ τ ⇐ Type
Γ ρ ⇐ Type Γ ρ (x : σ) → τ Γ,x : σ t ⇐ τ
Γ t ⇒β (x : σ) → τ Γ u ⇐ σ x #Γ
Γ (x : σ) τ ⇒ Type
Γ λx → t ⇐ ρ
Γ t u ⇒ let x : σ = u in τ
Γ ρ ⇐ Type Γ ρ (x : σ) ∗ τ Γ t ⇐ σ x #Γ Γ u ⇐ let x : σ = t in τ
Γ ρ ⇐ Type x , y # Γ Γ t ⇒β (x : σ) ∗ τ Γ t z Γ ; x : σ; y : τ ; z = (x , y) u ⇐ ρ
Γ (t, u) ⇐ ρ
Γ split t with (x , y) → u ⇐ ρ
εΓ
Γ ρ ⇐ Type Γ ρ {l } m ∈ l
Γ ρ ⇐ Type Γ t ⇒β {l } Γ t x (Γ, x = li ui ⇐ ρ)i
Γ {l } ⇒ Type
Γ m ⇐ ρ
Γ case t of {l → u } ⇐ ρ
Γ σ ⇐ Type
Γ ρ ⇐ Type Γ ρ ↑σ Γ t ⇐σ
Γ t ⇒σ
Γ t ⇒β ↑σ
Γ t ⇐ ↑σ
Γ ↑σ ⇒ Type
Γ [t ] ⇐ ρ
Γ [t ] ⇒ ↑σ
Γ !t ⇒ σ
Γ !t ⇐ σ
Γ σ ⇐ ↑Type
Γ ρ ⇐ Type Γ ρ Rec σ Γ t ⇐ !σ
Γ t ⇒σ
Γ ρ ⇐ Type Γ t ⇒β Rec σ Γ t y x #Γ Γ ; x :!σ; y = fold x u ⇐ ρ
Γ Rec σ ⇒ Type Γ fold t ⇐ ρ Γ fold t ⇒ Rec [σ ] Γ unfold t as x → u ⇐ ρ
The let rules have a side-condition: the context Δ must contain exactly one definition for every declaration, as specified in Sect. 2.1. The case rule’s indexed premise must hold for each of the branches, and there must be exactly one branch for every label in {l } (recall that {l } stands for a set of labels). Above we have listed the dependent elimination rules for products, labels and Rec (the rules for split, case and unfold). There are also non-dependent variants, which do not require the scrutinee to reduce to a variable, but whose premises do not get the benefit of equality constraints.
54
6
T. Altenkirch et al.
Conclusions
The definition of ΠΣ uses several innovations to meet the challenge of providing a concise core language for dependently typed programming. We are able to use one recursion mechanism for the definition of both types and (recursive or corecursive) programs, relying essentially on lifted types and boxes. We also permit arbitrary mutually recursive definitions where the only condition is that, at any point in the program, the current definition or declaration has to type check with respect to the current context—this captures inductive-recursive definitions. Furthermore all programs and types can be used locally in let expressions; the top-level does not have special status. To facilitate this flexible use of let expressions we have introduced a novel notion of α-equality for recursive definitions. As a bonus the use of local definitions makes it unnecessary to define substitution as an operation on terms. Much remains to be done. We need to demonstrate the usefulness of ΠΣ by using it as a core language for an Agda-like language. As we have seen in Sect. 2 this seems possible, provided we restrict indexing to types with a decidable equality. We plan to go further and realize an extensional equality for higher types based on previous work (Altenkirch et al. 2007). The current type system is less complicated than that described in a previous draft paper (Altenkirch and Oury 2008). We have simplified the system by restricting dependent elimination to the case where the scrutinee reduces to a variable. Unfortunately subject reduction for open terms does not hold in the current design, because a variable may get replaced by a neutral term during reduction. We may allow dependent elimination for arbitrary terms again in a later version of ΠΣ. Having a small language makes complete reflection feasible, opening the door for generic programming. Another goal is to develop ΠΣ’s metatheory formally. The distance between the specification and the implementation seems small enough that we plan to develop a verified version of the type checker in Agda (using the partiality monad). This type checker can then be translated into ΠΣ itself. Using this implementation we hope to be able to formally verify central aspects of (some version of) the language, most importantly type-soundness: β-reduction does not get stuck for closed, well-typed programs.
Acknowledgements We would like to thank Andreas Abel, Thierry Coquand, Ulf Norell, Simon Peyton Jones and Stephanie Weirich for discussions related to the ΠΣ project. We would also like to thank Darin Morrison, who has contributed to the implementation of ΠΣ, and members of the Functional Programming Laboratory in Nottingham, who have given feedback on our work.
ΠΣ: Dependent Types without the Sugar
55
References Altenkirch, T., Oury, N.: ΠΣ: A core language for dependently typed programming. Draft (2008) Altenkirch, T., McBride, C., McKinna, J.: Why dependent types matter. Draft (2005) Altenkirch, T., McBride, C., Swierstra, W.: Observational equality, now! In: Proceedings of the 2007 workshop on Programming languages meets program verification, pp. 57–68 (2007) Augustsson, L.: Cayenne — a language with dependent types. In: Proceedings of the third ACM SIGPLAN international conference on Functional programming, pp. 239– 250 (1998) Chapman, J., Altenkirch, T., McBride, C.: Epigram reloaded: A standalone typechecker for ETT. In: Trends in Functional Programming, vol. 6. Intellect (2006) The Coq Development Team. The Coq Proof Assistant Reference Manual, Version 8.2 (2009) Coquand, T.: A calculus of definitions, Draft (2008), http://www.cs.chalmers.se/~ coquand/def.pdf Coquand, T., Kinoshita, Y., Nordstr¨ om, B., Takeyama, M.: A simple type-theoretic language: Mini-TT. In: From Semantics to Computer Science; Essays in Honour of Gilles Kahn, pp. 139–164. Cambridge University Press, Cambridge (2009) Cui, S., Donnelly, K., Xi, H.: ATS: A language that combines programming with theorem proving. In: Gramlich, B. (ed.) FroCos 2005. LNCS (LNAI), vol. 3717, pp. 310–320. Springer, Heidelberg (2005) Danielsson, N.A., Altenkirch, T.: Mixing induction and coinduction. Draft (2009) Dybjer, P., Setzer, A.: Indexed induction-recursion. Journal of Logic and Algebraic Programming 66(1), 1–49 (2006) Gim´enez, E.: Un Calcul de Constructions Infinies et son Application ` a la V´erification de Syst`emes Communicants. PhD thesis, Ecole Normale Sup´erieure de Lyon (1996) Goguen, H., McBride, C., McKinna, J.: Eliminating dependent pattern matching. In: Futatsugi, K., Jouannaud, J.-P., Meseguer, J. (eds.) Algebra, Meaning, and Computation. LNCS, vol. 4060, pp. 521–540. Springer, Heidelberg (2006) Hancock, P., Pattinson, D., Ghani, N.: Representations of stream processors using nested fixed points. Logical Methods in Computer Science 5(3, 9) (2009) McBride, C.: The Strathclyde Haskell Enhancement (2009), http://personal.cis.strath.ac.uk/~ conor/pub/she/ Norell, U.: Towards a practical programming language based on dependent type theory. PhD thesis, Chalmers University of Technology and G¨ oteborg University (2007) Sheard, T.: Putting Curry-Howard to work. In: Proceedings of the 2005 ACM SIGPLAN workshop on Haskell, pp. 74–85 (2005) Sozeau, M.: Un environnement pour la programmation avec types d´ependants. PhD thesis, Universit´e Paris 11 (2008) Sulzmann, M., Chakravarty, M.M.T., Jones, S.P., Donnelly, K.: System F with type equality coercions. In: Proceedings of the 2007 ACM SIGPLAN International Workshop on Types in Languages Design and Implementation, pp. 53–66 (2007) Wadler, P., Taha, W., MacQueen, D.: How to add laziness to a strict language, without even being odd. In: The 1998 ACM SIGPLAN Workshop on ML (1998)
Haskell Type Constraints Unleashed Dominic Orchard1 and Tom Schrijvers2, 1
Computer Laboratory, University of Cambridge
[email protected] 2 Katholieke Universiteit Leuven
[email protected]
Abstract. The popular Glasgow Haskell Compiler extends the Haskell 98 type system with several powerful features, leading to an expressive language of type terms. In contrast, constraints over types have received much less attention, creating an imbalance in the expressivity of the type system. In this paper, we rectify the imbalance, transferring familiar type-level constructs, synonyms and families, to the language of constraints, providing a symmetrical set of features at the type-level and constraint-level. We introduce constraint synonyms and constraint families, and illustrate their increased expressivity for improving the utility of polymorphic EDSLs in Haskell, amongst other examples. We provide a discussion of the semantics of the new features relative to existing type system features and similar proposals, including details of termination.
1
Introduction
The functional programming language Haskell has a rich set of type system features, as described by the Haskell 98 standard [1], which the Glasgow Haskell Compiler (GHC) has extended considerably. Types in Haskell consist of two parts, the type term τ and constraint term C, forming a qualified type C ⇒ τ . The syntactic position left of ⇒ is known as a type’s context and may be empty. The majority of GHC’s type system features extend the language of type terms τ . The type term language includes, from the Haskell 98 standard: algebraic data types and type synonyms, and added by GHC: generalised algebraic data types (GADTs), type synonym families [2], and data type families [3]. In contrast, the language of constraints C has received little attention, consisting of only type classes (from Haskell 98) and GHC’s equality constraints [4]. Recently, Haskell has been recognised as a good host for polymorphic embedded domain-specific languages (EDSLs) [5]. However, limitations of the relatively inexpressive constraint language have become even more apparent with this recent wave of EDSLs, that exploit the possibilities of the rich type-term language, but find the constraint language lacking (see example problems in Section 2). We rectify this imbalance, expanding Haskell’s capacity for expressing EDSLs.
Post-doctoral researcher of the Fund for Scientific Research - Flanders.
M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 56–71, 2010. c Springer-Verlag Berlin Heidelberg 2010
Haskell Type Constraints Unleashed
57
We introduce two new type system features, constraint synonyms (Section 4) and constraint synonym families (sometimes abbreviated to constraint families) (Section 5) which tackle some of the problems faced with the inflexible constraint language. We discuss the syntax and semantics of the new features, paying particular attention to the termination conditions of constraint families. Our new features are a natural extrapolation of Haskell’s1 type-term constructions to the constraint-term language i.e. the new features are derived by analogy, as opposed to being added in an ad-hoc manner (see Section 3). The new features do not extend the power of Haskell, but can be encoded in the existing language. An encoding schema and implementation is discussed in Section 6. Section 7 provides a discussion of existing proposals for solutions to the inflexible constraint language, such as class aliases [6,7] and class families [8].
2
Problems
We see two kinds of problem with Haskell’s current constraint-term language: the first related to the naming of constraints, the second related to type-indexing, or type-dependency, of constraints. 2.1
Naming Problems
Haskell does not provide a renaming feature for constraints or conjunctions of multiple constraints. There are several undesirable situations which arise due to the lack of such renaming features. Tiresome Repetition of Long Constraints. Generalised frameworks and libraries factor out instance-specific functionality and algorithms as (higher-order) parameters. Type classes provide considerable convenience in making these parameters implicit. Related functionality can be grouped in a single type class, but in the case of orthogonal design choices, multiple type classes may be involved. Consequently, highly flexible functions accumulate a large number of constraints in their signature, yielding unwieldy, large, and hard to understand constraint terms. Consider, for instance, the signature of the eval function in the Monadic Constraint Programming framework [9]: eval :: (Solver s, Queue q, Transformer t, Elem q ~ (Label s, Tree s a, TreeState t), ForSolver t ~ s) => ...
where the first three type class constraints capture different implicit parameters and the last two equality constraints (involving type families Elem, Label, TreeState and ForSolver) enforce consistency. Such a long constraint term is cumbersome to read and write for the programmer. 1
From now on, reference to Haskell means the Haskell language as accepted by GHC 6.12.1, December 2009.
58
D. Orchard and T. Schrijvers
Cumbersome Refactoring. Type classes can modularise the design of libraries, where independently useful classes provide more generalised behaviour than their subclasses. An existing type class might be refactored by decomposition into several smaller, more general, independent superclasses. For example, the pervasive Num class could be divided into Additive, Multiplicative, and FromInteger classes. Unfortunately, this decomposition means any program with explicit Num constraints and/or type instances of the Num class must be rewritten, refactoring instances and rewriting explicit constraints. 2.2
Uniformity Problems
Type classes impose a uniform structure on all instances; all methods have type signatures of a fixed shape. However, associated type families allow variation in the shape of signatures by making them (partly) dependent on the instance type. No such flexibility is available with constraint terms, thus type classes impose uniform constraints on all instances, considerably restricting possible instances. Constrained Functors. The Functor type class is a good example of a larger class of constructor type classes, including also monads. class Functor f where fmap :: (a -> b) -> f a -> f b
Members of Functor implement the fmap function, which lifts a function to operate over a parameterised data type. The context of the fmap function is empty – there are no constraints – which is sufficient for many data types, such as lists. Other data types may impose constraints on the parameter types a, b. For instance, the Set data type from Haskell’s standard library provides a map function over Set with type: Set.map :: (Ord a, Ord b) ⇒ (a → b) → Set a → Set b which almost matches the signature of fmap, except its non-empty context (Ord a, Ord b). To generalise the Functor type class, we would like the context of fmap to depend on the type f. Polymorphic Final EDSLs. A similar issue has recently appeared2 in the development of final tagless EDSLs [10] of a polymorphic nature. Consider the following EDSL of simple polymorphic expressions of constants and addition: class Expr sem where constant :: a -> sem a add :: sem a -> sem a -> sem a
where add (constant 1) (constant 2) would denote the integer expression 1 + 2 while add (constant 1.0) (constant 2.0) would denote the real expression 1.0 + 2.0. We would like to implement an evaluation semantics E: 2
http://www.haskell.org/pipermail/haskell-cafe/2009-September/ 066670.html
Haskell Type Constraints Unleashed
59
data E a = E {eval :: a} instance Expr E where constant c = E c add e1 e2 = E $ eval e1 + eval e2
However, this code fails to type check because addition (+) requires the constraint Num a. We could solve this problem by adding the Num a constraint to the method signature of add in the class declaration. However, other useful semantics for this polymorphic EDSL may require their own constraints, e.g. Show for printing, Read for parsing, etc. Adding more and more constraints to the methods of Expr prevents modular reuse and narrows the number of possible monomorphic instances of the polymorphic EDSL. In other words, the polymorphic nature of the EDSL becomes almost pointless. Polymorphic HOAS-based EDSL. A similar problem arises in the polymorphic EDSL for constraint models Tree s a of the Monadic Constraint Programming framework [9]. This data type has a constrained (i.e. GADT) higher-order abstract syntax (HOAS) constructor: data Tree s a where NewVar :: Term s t => (t -> Tree s a) -> Tree s a ...
where Term s t expresses that t is one of (possibly) several term types supported by the constraint solver of type s. The Term class provides one method: class (Solver solver) => Term solver term where newvar :: solver term
However, for specific solvers, we would like to capture that additional methods are available. Unfortunately, we cannot refine the NewVar constructor’s constraint based on the particular solver type s.
3
Our Approach
We introduce two new constraint term features, constraint synonyms and constraint synonym families, analogous to existing type term features. We classify GHC’s existing type system features into two groups: type terms and constraint terms. Within these groups there are two further levels of subdivision. The second division differentiates between family terms (indexed-sets) and constant terms (nullary families). The third division differentiates whether families or constants are synonym definitions or generative definitions – that is, definitions that generate unique, non-substitutable constructors. These three levels of division essentially give a three-dimensional design space which is partially inhabited by GHC’s existing features. Fig.1 summarises existing GHC features and names the missing features in the design space (in bold). In this paper we consider constraint synonyms (Section 4)
60
D. Orchard and T. Schrijvers types generative
constants synonym
families
constraints 4
Data types data T a ¯ where . . . Type synonyms type T a ¯ = τ
Data type families generative data family T a ¯ data instance T τ¯ = . . . Type synonym families synonym type family T a ¯ type instance T τ¯ = τ
Classes class K a ¯ where . . . Constraint synonyms constraint K a ¯ = C Class families not considered Constraint synonym families constraint family K a ¯ constraint instance K τ¯ = C
Fig. 1. Type-level features extrapolated to the sparse constraint-level
and constraint synonym families (Section 5). The third feature missing from the constraint language, class families, has been informally discussed in the Haskell community (see Section 7), but we do not know of any problems which cannot be solved more elegantly with constraint synonym families. Fig.1 also introduces the syntax of constraint synonyms and constraint families, derived systematically from the corresponding type-side syntax. 3.1
Preliminaries
We adopt the following syntax for constraint terms: C, D ::= () | (C1 , . . . , Cn ) | τ1 ∼ τ2 | K τ¯ The first three of these constraint forms are built into the language: – True - The empty constraint () denotes truth. Existing Haskell syntax does not provide an explicit notation for this concept; the whole constraint context C ⇒ is simply omitted. For both new features that we introduce, the explicit notation becomes necessary. – Conjunction - The tuple (C1 , . . . , Cn ) denotes the n-way conjunction n C . Conjunction is associative and commutative, but the tuple notation i i=1 is not. – Equality - The notation τ1 ∼ τ2 denotes equality of the types τ1 and τ2 . The fourth form of constraints, K τ¯, are user-defined, where K is the name of a constraint constructor and τ¯ its type parameters. A user-defined constructor of arity n must be fully applied to exactly n type arguments. Currently, Haskell provides only one form of user-defined constraints: type class constraints. Our new features add two new forms for constraint synonyms and constraint families. 2
The term “data types” refers to GADTs, of which ADTs are a special case.
Haskell Type Constraints Unleashed
61
a). Existing Constraint Features: (True) (Given)
(Conj)
C |= ()
C |= D1 · · · C |= Dn C |= (D1 , . . . , Dn )
(Decomp)
C |= C
instance D => K τ¯ C |= θ(D) (Inst) C |= θ(K τ¯)
Ci |= D (. . . , Ci , . . .) |= D
class C => K a ¯ θ(C) |= D (Super) θ(K a ¯) |= D
b). Constraint Synonyms: constraint K a ¯ = D C |= θ(D) (W-Syn) C |= θ(K a ¯)
constraint K a ¯ = D θ(D) |= C (G-Syn) θ(K a ¯) |= C
c). Constraint Synonym Families: (constraint keyword omitted for compactness) family K a ¯ instance K τ¯ = D C |= θ(D) (W-Syn-Fam) C |= θ(K τ¯)
family K a ¯ instance K τ¯ = D θ(D) |= C (G-Syn-Fam) θ(K τ¯) |= C
Fig. 2. Constraint Implication
Constraints appear in the static semantics of Haskell essentially in the (Var) rule, for typing a variable expression x: (Var)
(x : ∀¯ a.D ⇒ τ ) ∈ Γ θ = [¯ τ /¯ a] C; Γ x : θ(τ )
C |= θ(D)
which states that expression x has type θ(τ ) (where θ is a substitution of type variables for types) with respect to environment Γ and given constraints C, if: 1. x has a more general type ∀¯ a.D ⇒ τ in the environment, and 2. constraint θ(D) is entailed by given constraints C, denoted C |= θ(D). The entailment relation |= defines the static meaning of constraints. Fig.2a defines |= for the built-in constraint constructors, ignoring equality constraints. We refer to [4, Fig. 3] for the semantics of type equalities, which involve minor adjustments to the form of the judgments that are irrelevant to our purposes.
4
Constraint Synonyms
Haskell’s type synonyms provide a simple mechanism for specifying renamings of types which can be rewritten (desugarded) prior to type checking. Rewriting
62
D. Orchard and T. Schrijvers
does not change the meaning of a program. Constraint synonyms provide the same functionality as type synonyms but on constraint terms, with syntax: constraint K a ¯=C where ftv (C) is the set of (free) type variables in C and ftv (C) ⊆ a ¯. The two rules of Fig.2b extend constraint entailment to constraint synonyms in the obvious way. The (W-Syn) rule defines entailment of a wanted synonym constraint K a ¯, that must be satisfied by existing, given constraints C; the (GSyn) rule defines entailment of a wanted constraint C, which is satisfied by a given constraint synonym K a ¯. 4.1
Examples
Revisiting the problems of Section 2.1, we see the advantage of constraint synonyms. Firstly, a large constraint term can be conveniently abbreviated: constraint Eval s q t a = (Solver s, Queue q, Transformer t, Elem q ~ (Label s, Tree s a, TreeState t), ForSolver t ~ s) eval :: Eval s q t a => ...
Secondly, for decomposition of monolithic classes, such as Num, into a number of smaller, independent, more general classes, the original constraint constructor can be preserved as the synonym of a conjunction of its superclasses e.g. constraint Num a = (Additive a, Multiplicative a, FromInteger a)
Thus, existing explicit Num constraints do not need rewriting. However, Num instances must be rewritten as instances of the Additive, Multiplicative, etc. superclasses. Proposed class aliases [6] do not have this problem (see Section 7). 4.2
Termination
Without restrictions, ill-founded constraint synonyms may arise, such as: constraint K1 a = K1 a constraint K2 a = (K2 [a], Eq a)
Neither of these is a synonym of any well-founded synonym-free constraint term. Hence, we enforce that constraint synonym definitions form a terminating rewrite system, when interpreted as left-to-right rewrite rules, in the same way as the Haskell 98 rule for type synonyms. Therefore the call graph of synonym definitions must be acyclic: a synonym may not appear in the right-hand side of its definition, or transitively in the right-hand side of any synonym mentioned.
Haskell Type Constraints Unleashed
5
63
Constraint Synonym Families
Type families allow types to depend upon, or be indexed by, other types [2,3,4]. Constraint synonym families extend this capability to the constraint terms of a type, giving type-indexed constraints. A new constraint family K is defined by: constraint family K a ¯ The definition of families is open, allowing instances to be defined in other modules that are separately compiled. An instance is defined by: constraint instance K τ¯ = C where the arity of instances must match the arity of the family declaration, and ftv (C) ⊆ ftv (¯ τ ). Families can also be associated to a type class by declaration inside a class body. This nesting requires that all n parameters of the class are repeated as the first n parameters of the associated family, and that all additional parameters are distinct free type variables. This ensures that there is exactly one constraint family instance for every host type class instance. Note, that this differs from associated type families and data families, whose parameters must be exactly those of the parent class. Possible additional named parameters, further to the class parameters, are required as constraints on the right-hand side of a family instance must be fully applied, thus a point-free style cannot be admitted. The family and instance keywords are elided when a family is associated with a class: class K a ¯ where constraint KF a ¯ ¯b
instance K τ¯ where constraint KF τ¯ ¯b = C
Constraint entailment is extended to constraint synonym families where entailment of instances proceeds in the same way as constraint synonym entailment (see Fig.2c, rules (W-syn-fam) and (G-syn-fam)). 5.1
Examples
The following examples show how the problems of Section 2.2 can be solved with constraint synonym families. They also address two additional aspects: default values and the lack of need for explicit implication constraints. Constrained Functors. The Functor type class is generalised to impose a constraint, Inv, indexed by the functor type. class Functor f where constraint Inv f e fmap :: (Inv f a, Inv f b) => (a -> b) -> f a -> f b
64
D. Orchard and T. Schrijvers
Both lists and sets can be instances of this generalised Functor class. instance Functor [] where constraint Inv [] e = () fmap = ...
instance Functor Set where constraint Inv Set e = Ord e fmap = ...
A default can be provided in a class declaration for an associated constraint family, similar to default method implementations. Any instance that does not explicitly provide an associated constraint opts for the default. For Functor we can use the () constraint:3 class Functor f where constraint Inv f e = ()
This is particularly convenient because it allows reuse of the existing Functor instances without modification. In contrast, work-arounds such as Restricted Monads [11] require rewriting existing instances. Final Polymorphic EDSL. The final polymorphic EDSL becomes much more useful with constraint families: class Expr sem where constraint Pre sem a constant :: Pre sem a => a -> sem a add :: Pre sem a => sem a -> sem a -> sem a data E a = E {eval :: a} instance Expr E where constraint Pre E a = Num a constant c = E c add e1 e2 = E $ eval e1 + eval e2
Semantics for the EDSL, provided by the sem type, are free to choose their own constraints with an instance of the Pre constraint family, thus opening up a much wider range of applications. HOAS-based EDSL. Due to type synonym families, Haskell programs no longer have principal types. As a remedy, explicit equality constraints τ1 ~ τ2 were added to recover principal typing. For instance, consider: class Coll c where type Elem c insert :: c -> Elem c -> c
addx c = insert c ‘x‘
The principal type of addx is (Coll c, Elem c ~ Char) => c -> c, which cannot be expressed without the explicit equality constraint. We may wonder whether, similarly, an explicit implication constraint, say C |= D, between constraints C and D is necessary for principality or expressivity reasons. For instance, in order to generalise the HOAS-based EDSL, we may want to write: 3
Recall that () => τ is equivalent to the unqualified type τ .
Haskell Type Constraints Unleashed
65
constraint family TermF s t data Tree s where NewVar :: (TermF s t, TermF s t |= Term s t) => (t -> Tree s) -> Tree s
which expresses 1) that the constraint relating s and t is indexed by the solver s and term type t, and 2) that the constraint implies Term s t. Hence, this code is strictly more general than the previous version in Section 2.2. Yet, the only way to use (or eliminate) such an explicit implication is through a modus ponens rule: (TermF s t, TermF s t |= Term s t) |= Term s t
This suggests a simpler solution that does not involve an explicit implication |=, but directly requires the right-hand side of the explicit implication: constraint family TermF s t data Tree s where NewVar :: (TermF s t, Term s t) => (t -> Tree s) -> Tree s
This solution expresses the indexing property 1) and that Term s t holds, without enforcing a relationship between TermF s t and Term s t. In summary, we argue that adding constraint synonym families to the type system does not destroy principal types. In particular, no explicit implication constraints need to be added to recover principal types. 5.2
Well-Defined Families
For constraint families to be well-defined, we must consider confluence and termination. The former ensures that family reductions are unambiguous, the latter ensures that they are well-founded. Confluence. A constraint family application which reduces to two distinct constraints is ambiguous and may ultimately lead to program crashes. For instance, in a dictionary-based implementation, ambiguity of constraints results in ambiguity of dictionary types, which may cause crashes at runtime if we lookup a non-existent method or method of an unexpected type. Hence, we enforce confluence by requiring non-overlapping instances, in the same way as type families [2]. This means that at most one instance matches a family application. Consequently, type family applications are not allowed as parameters of constraint family instances; due to their openness, overlap cannot be ruled out. Termination. The termination of synonym family reductions is more complicated than that of synonyms because family definitions are open. Even if a family’s call graph is acyclic, further modules may define cycle-forming instances, possibly causing infinite reductions. However, not all cyclic call graphs are non-terminating; for instance, the following constraint family is terminating: constraint constraint constraint constraint
family K instance instance instance
(m :: * -> *) a K [] a = () K Set a = Eq a K (States s m) a = K m a
66
D. Orchard and T. Schrijvers
Note: synonyms may hide family constructors, thus all (non-family instance) synonyms should be substituted prior to well-definedness checking of families. Because termination checking is generally undecidable, GHC imposes conservative conditions on type synonym families, some of which are discussed in recent work [4]. These conservative conditions can be applied to constraint families to ensure termination, which we present below as the strong termination condition. However, due to the nature of constraints, it is possible to relax this condition, allowing greater expressivity whilst still ensuring termination. We first define the strong termination condition and, motivated by examples, go on to successively weaken the condition. Definition 1 (Strong Termination Condition). For each constraint family instance K τ¯ = C, 1. either C contains no constraint family application, or 2. C is a constraint family application of the form K τ¯ , and (a) |¯ τ | > |¯ τ |, (b) the RHS has no more occurrences of any type variables than the LHS, (c) the RHS does not contain any type family applications The size measure | · | is defined as: |a| = 1 |T | = 1
|(τ1 τ2 )| = |τ 1 | + |τ2 | |¯ τ | = τ ∈¯τ |τ |
The previous example satisfies the strong termination condition and is rightly accepted as terminating. The following terminating instance is also accepted as it contains no constraint family applications, satisfying case 1 (although it does apply a type family in an equality constraint): constraint instance K (State s) = (Eq [s], s ~ F (s,s)) type family F s
The following non-terminating instances are on the other hand rejected:
} constraint instance K (Baz [x] m) a = K (Baz a m) a } constraint instance K Foo a = K Bar a constraint instance K Bar a = K Foo a
constraint instance K (Foz (Foz m)) a = K (F m) a type family F (m :: * -> *) type instance F Set = Foz (Foz Set)
violates 2(a) violates 2(b)
} violates 2(c)
where all of Foo, Bar, Baz and Foz are data type constructors. In the second case, non-termination occurs when a is of the form [x]. In the third case, K (Foz (Foz Set)) a is non-terminating; the type family F reduces such that the left-hand side and right-hand side of the constraint instance are equal, hence forming a non-terminating family reduction. The strong termination condition is however too conservative for our purposes, disallowing many common, terminating constraint families. For example, the following contains more than one family occurrence in the right-hand side:
Haskell Type Constraints Unleashed constraint family K a constraint instance K (a,b)
67
= (K a, K b)
yet this instance is terminating. Contrast this with a type family instance of a similar form: type family TF a type instance TF (a,b)
= (TF a, TF b)
where the constraint α ~ TF (α,Int) would lead to an infinite type checker derivation. The problem is that the only solution for the unknown type α is an infinite type term (((..., TF Int), TF Int), TF Int), which is built up gradually by instantiating α. Such a problem does not arise in the constraint family setting for two reasons: 1) an unknown type α is never bound to a constraint constructor (,), and 2) there are no equality constraints between constraints. Thus, we can impose a less severe termination condition for constraint families than for type families. Definition 2 (Weak Termination Condition). For each constraint family instance K τ¯ = C, for each constraint synonym family application K τ¯ in C: 1. |¯ τ | > |¯ τ |, 2. τ has no more occurrences of any type variables than the left-hand side, 3. τ does not contain any type family applications. Finally, we consider the interaction of constraint families with class instances, which also forms a derivation system. Allowing constraint families in contexts of class instances permits non-termination via mutual recursion. Consider: class K a instance KF a => K [a]
constraint family KF a constraint instance KF a = K [a]
which exhibits looping derivation K [a] → KF a → K [a] → . . .. We see two ways to avoid this form of non-termination. The first solution is to disallow constraint synonym families altogether in instance contexts; i.e. ruling out the above type class instance. The second solution is to strengthen the termination condition for family instances; i.e. ruling out the above family instance. Definition 3 (Class Instance Compatible Termination Condition). In addition to satisfying the Weak Termination Condition, we have for each constraint family instance K τ¯ = C, that for each type class application K τ¯ in C the following three conditions are met: 1. |¯ τ | ≥ |¯ τ |, 2. τ has no more occurrences of any type variables than the left-hand side, 3. τ does not contain any type family applications. Note that the first condition requires a non-strict size decrease, rather than a strict one. The reason is that the Paterson conditions for termination of type class instances [12, Def. 11] already require a strict size decrease of type terms from the instance head to individual constraints in the instance context. As a consequence, a strict decrease is still realised for mutual recursion between family and class instances.
68
D. Orchard and T. Schrijvers
6
Encoding and Implementation
As with type classes, both constraint synonyms and constraint families do not extend the power of Haskell but can be encoded using existing language features. Such encodings are much less convenient to write by hand than our proposed extensions. In this section, we present an encoding employed by a prototype preprocessor accompanying this paper.4 An alternate encoding and a more direct implementation in System Fc are included in a companion technical report [13]. 6.1
Constraint Synonym Encoding
A lightweight encoding for constraint synonyms can be given using a class and a single catch-all instance, e.g. class (Additive a, Multiplicative a, FromInteger a) => Num a where ... instance (Additive a, Multiplicative a, FromInteger a) => Num a where ...
However, this approach requires GHC’s undecidable instances extension, removing conservative termination conditions on type class instances. This extension is globally applied, thus type-checking decidability can no longer be guaranteed – an unnecessary, undesirable side-effect of this encoding. An alternative to using undecidable instances is to supply individual instances for each required type. This is tedious and even more inelegant than the above auxiliary class definition. 6.2
Constraint Synonym Family Encoding
The encoding of constraint synonym families relies on two ingredients: 1) type synonym families (below Pre) for capturing the synonym family aspect, and 2) GADTs (below NumDict) reifying constraints in a type-level construct. Applied to the final polymorphic EDSL we obtain the following solution: type family Pre (sem :: * -> *)
a
class Expr sem where constant :: Pre sem a -> a -> sem a add :: Pre sem a -> sem a -> sem a -> sem a data E a = E {eval :: a} type instance Pre E a = NumDict a data NumDict a where ND :: Num a => NumDict a instance Expr E where constant _ c = E c add ND e1 e2 = E $ eval e1 + eval e2
This encoding is much more cluttered than the original due to the value-level passing of reified constraints and the releasing of constraints by GADT pattern matching. Moreover, uses of add and constant must be updated, e.g. 4
http://github.com/dorchard/constraintTermExtensions
Haskell Type Constraints Unleashed
69
expr :: (Expr sem, Pre sem Int) => sem Int expr = add (constant 1) (constant 2) three :: Int three = eval expr
becomes: expr :: Expr sem => Pre sem Int -> sem Int expr d = add d (constant d 1) (constant d 2) three :: Int three = eval (expr ND)
7
Related Work
Diatchki [14] proposed a form of type synonym that combines a type term with some constraints, and relies on functional dependencies [15] for the encoding. In combination with type synonym families, this may provide some of the functionality of constraint synonym families, but under the strong termination condition of type families and not the weaker termination conditions we have provided. Proposed class aliases (or context aliases) [6,7] can define constraint synonyms, but have extra features to ease refactoring. Class aliases define a new class from a conjunction of existing classes, whose methods are at least those of the aliased classes. Additional methods may also be defined in the class alias. Class alias instances implement all methods of the class alias, e.g. class alias Num a = (Additive a, Multiplicative a, FromInteger a) where (-) :: a -> a -> a instance Num Integer where x + y = ... ...
Existing instances of Num do not have to be rewritten as individual instances of Additive, Multiplicative, unlike an equivalent constraint synonym. However, some class aliases are potentially problematic: class alias Eq’ a b = (Eq a, Eq b)
Instances of Eq’ must implement two equality operations, although the type to which each belongs may be indistinguishable for some instances without some form of explicit differentiation. Another issue arises if class instances overlap class alias instances e.g. if both Additive Int and Num Int instances are defined, which implementation is chosen when applying the + operation at type Int? Constraint synonyms are more general than class aliases, simply extending type synonyms to the constraint-level. A class alias-like extension to constraint synonyms may be considered, allowing class instances of constraint synonyms. Restricted data types [16] were proposed to address the problem of writing polymorphic classes whose instance parameters may have constraints. Restricted
70
D. Orchard and T. Schrijvers
data types allow constraints on the parameters of a data type to be given with a data type definition; constraints on a type are implicit. Such an approach is less flexible than our own, not permitting arbitrary type-indexing of constraints, or more specialised constraints under certain type parameters. An encoding for restricted monads [11] binds all the type parameters of class methods in the head of the class declaration such that instance-specific constraints can be given. Such an approach is not as practical as constraint families, as it requires all existing instances to be rewritten and cumbersome class declarations with many parameters and potentially many subclasses to differentiate methods. The RMonad library [17] gives restricted alternatives to functor and monad classes on which instance-specific constraints can be given, using a similar manual encoding to Section 6.2. Class families have been discussed in online discussions [8], although we currently know of no problems that class families solve that constraint synonym families do not solve more elegantly with less machinery. Class families are discussed further in a companion technical report [13]. Recently, constraint families have been informally proposed online in a form that is similar to our own presentation [18]. Design issues (such as default values, extra non-class parameters of associated families, redundancy of explicit implications) and semantical aspects (static semantics, termination) have not been considered thoroughly until now.
8
Conclusion and Further Work
The current imbalance in the Haskell/GHC type system, at the disadvantage of type-level constraints, imposes a rather unfortunate barrier for building larger and more flexible systems, including polymorphic EDSLs, in Haskell. The balance is restored here by transferring the type-term constructs of synonyms and families to the constraint language. This symmetrising approach provides a reference syntax and semantics from which to derive the new constraint-level features, such that their design is not “from scratch”. We categorised existing type system features using a three-dimensional design space to elucidate the imbalance between types and constraints. It would be interesting to see if such a framework could not only categorise features, but provide a systematic approach to defining syntax, semantics, properties, and even implementations of type system features in terms of of disjoint, composable abstract units. There are certainly further interesting axes of the design space, such as the choice between open vs. closed definitions. As programming pervades science, engineering, and business, and as new (parallel) hardware architectures emerge, the utility of DSLs is becoming increasingly apparent. Building DSLs within a well-established language provides inexpensive application or implementation specific expressivity and optimisation. A good EDSL-host language must be proficient at handling abstract structures, with high levels of parametricity. Our extensions increase Haskell’s ability to host EDSLs, further boosting its potential.
Haskell Type Constraints Unleashed
71
Acknowledgements. We are grateful to Manuel Chakravarty and Simon Peyton Jones for discussions and explaining details of GHC’s associated type families. Also thanks to Max Bolingbroke, Oleg Kiselyov, Martin Sulzmann and Marko van Dooren for their insightful feedback. Finally, thank you to the anonymous reviewers for their useful comments and feedback.
References 1. Peyton Jones, S., et al.: Haskell 98 Language and Libraries: The Revised Report. Cambridge University Press, Cambridge (2003) 2. Chakravarty, M.M.T., Keller, G., Jones, S.P.: Associated type synonyms. In: ICFP 2005: Proceedings of the tenth ACM SIGPLAN international conference on Functional programming, pp. 241–253. ACM, New York (2005) 3. Chakravarty, M.M.T., Keller, G., Jones, S.P., Marlow, S.: Associated types with class. SIGPLAN Not. 40(1), 1–13 (2005) 4. Schrijvers, T., Jones, S.P., Chakravarty, M., Sulzmann, M.: Type checking with open type functions. SIGPLAN Not. 43(9), 51–62 (2008) 5. Stewart, D.: Domain Specific Languages for Domain Specific Problems. In: Workshop on Non-Traditional Programming Models for High-Performance Computing, LACSS (2009) 6. Meacham, J.: Class Alias Proposal for Haskell, http://repetae.net/recent/out/classalias.html (last visited August 2009) 7. Jeltsch, W., van Dijk, B., van Dijk, R.: HaskellWiki: Context alias entry, http://www.haskell.org/haskellwiki/Context_alias (last visited August 2009) 8. Chakravarty, M., Peyton Jones, S., Sulzmann, M., Schrijvers, T.: GHC developer wiki: Class families entry, http://hackage.haskell.org/trac/ghc/wiki/TypeFunctions/ClassFamilies (last visited August 2009) 9. Schrijvers, T., Stuckey, P., Wadler, P.: Monadic Constraint Programming. J. Func. Prog. 19(6), 663–697 (2009) 10. Carette, J., Kiselyov, O., Shan, C.: Finally Tagless, Partially Evaluated. In: Shao, Z. (ed.) APLAS 2007. LNCS, vol. 4807, pp. 222–238. Springer, Heidelberg (2007) 11. Kiselyov, O.: Restricted Data Types Now (February 2006), http://okmij.org/ftp/Haskell/RestrictedMonad.lhs 12. Sulzmann, M., Duck, G.J., Peyton-Jones, S., Stuckey, P.J.: Understanding functional dependencies via constraint handling rules. J. Func. Prog. 17, 83–129 (2007) 13. Orchard, D., Schrijvers, T.: Haskell Type Constraints Unleashed: Companion Report. Report CW 574, Dept. of Computer Science, K.U. Leuven, Belgium (January 2010) 14. Diatchki, I.S.: High-Level Abstractions for Low-Level Programming. PhD thesis, OGI School of Science & Engineering at Oregon Health & Science University (May 2007) 15. Jones, M.P.: Type classes with functional dependencies. In: Smolka, G. (ed.) ESOP 2000. LNCS, vol. 1782, p. 230. Springer, Heidelberg (2000) 16. Hughes, J.: Restricted Data Types in Haskell. In: Proceedings of the 1999 Haskell Workshop. Technical Report UU-CS-1999-28, Utrecht (1999) 17. Sittampalam, G., Gavin, P.: Rmonad: Restricted monad library (2008), http://hackage.haskell.org/package/rmonad 18. Bolingbroke, M.: Constraint families (2009), http://blog.omega-prime.co.uk/?p=61
A Functional Framework for Result Checking Gilles Barthe1 , Pablo Buiras1,2 , and C´esar Kunz1 2
1 IMDEA Software, Spain FCEIA, Universidad Nacional de Rosario, Argentina
Abstract. Result checking is a general methodology for ensuring that untrusted computations are valid. Its essence lies in defining efficient checking procedures to verify that a result satisfies some expected property. Result checking often relies on certificates to make the verification process efficient, and thus involves two strongly connected tasks: the generation of certificates and the implementation of a checking procedure. Several ad-hoc solutions exist, but they differ significantly on the kind of properties involved and thus on the validation procedure. The lack of common methodologies has been an obstacle to the applicability of result checking to a more comprehensive set of algorithms. We propose the first framework for building result checking infrastructures for a large class of properties, and illustrate its generality through several examples. The framework has been implemented in Haskell.
1
Introduction
Computer programs are error-prone, making it a challenge to assure the validity of computations. Errors arise from many sources: programming mistakes, rounding-off errors in floating-point computations, defects in the underlying hardware, or simply because part of a computation has been delegated to some untrusted party. A general methodology for ascertaining the correctness of the computations performed by a program F is to rely on an independent result checker V , which guarantees the correctness of the computation performed by F . A simple example of result checker is a boolean-valued predicate between inputs and outputs that only returns true on pairs (x, y) such that F (x) = y, where F is a program with a single input x and single output y. For example, a result checker for the program F computing the square root y of x is the program V that returns the boolean expression (y 2 x)&&(x < y 2 + 2y + 1). However, result checkers may in general rely on additional inputs, called certificates, that guarantee efficient execution. A typical example of result checker which relies on certificates is the checker for greatest common divisor (gcd), which takes as arguments, in addition to a and b for which the gcd must be computed, two additional values u and v (which constitute the certificate), and the candidate gcd d, and returns the boolean value d = ua + vb ∧ d | a ∧ d | b.
Partially funded by the EU project HATS and Spanish project Desafios-10 and Community of Madrid project Comprometidos.
M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 72–86, 2010. c Springer-Verlag Berlin Heidelberg 2010
A Functional Framework for Result Checking
73
While result checking offers a general methodology to guarantee the correctness of computations, and thus is potentially applicable to many domains, its applications have been circumscribed to a few and rather specific settings; see Section 2. The main challenge in broadening the scope of result checking is finding a systematic means of building, for a large class of properties, a result checking framework that provides (a) for every property P in the class, a type witP of certificates; (b) a means of generating certificates1 ; and (c) a checker checkP : A → witP → bool (where A is the carrier of P ), such that for all a : A and w : witP we have (checkP a w = true) ⇒ P a. The purpose of this article is to provide a framework for building and verifying certified results for a large class of algorithms. The framework is implemented on top of the Haskell [9,10] programming language, and provides: – certifying combinators, which extend the usual combinators of functional programming with facilities for turning certificates of the combinators’ inputs into certificates of the combinators’ outputs. Certifying combinators can be combined to produce certified results; – a generic checker function, that takes a representation of a property and behaves like a checker for it. The combination of certifying combinators and the generic checker allows us to obtain a result checking framework for a large class of properties, including sorting and searching algorithms, or primality testing. Although our results are developed in the setting of a sequential language, our primary motivation is to provide a certificate-based infrastructure for guaranteeing the correctness of large distributed computations among untrusted hosts [1]. The results of this paper can be embedded in the framework of [16] for this purpose. Outline. In Section 3, we define a generic checking function for a large class of predicates that includes inductively defined ones. In Section 4, we propose two methods for the generation of certificates. Both are defined as an extension of the original producer algorithm. The first one is based on the recursion pattern defining the producer algorithm. The second one is a general approach for the certification of nondeterministic computations.
2
Related Work
Blum and Kannan [2] were among the first to recognise the importance of result checking as a general method for discovering bugs in programs, and to advocate its superiority over testing, which can be unreliable, or program verification, 1
Note that no property of the certificate generation mechanism is needed for checked results to be correct, i.e., it is not necessary to trust the certificate generators.
74
G. Barthe, P. Buiras, and C. Kunz
which can be costly. Many of the checkers considered in [2] are probabilistic programs, whose soundness is expressed in probabilistic terms. Blum subsequently reflects on the usefulness of result checking for managing hardware errors, as in the Pentium bug, or round-off errors arising in floating-point computations. The pioneering ideas of Blum and Kannan were further developed in the context of certification trails for common data structures, and certifying algorithms for mathematical software: – a certifying algorithm is one which computes, along with the intended result, a certificate that allows a checker program to verify independently the correctness of the result (independent verification means that it should not be necessary for the checker program to trust the certifying algorithm in any way: results and certificates should speak for themselves). Certifying algorithms are implemented notably in the LEDA and CGAL platforms for computational geometry; the role of the checkers is to increase the reliability of the platform, through a checking phase introduced at the end of each geometric algorithm. – a certification trail [3,14] is a record of selected intermediate results during a program computation. A second algorithm executes more efficiently by using the execution trail of the first program to compute the same result. Then, the original result and the result of the second algorithm are compared. The technique is applicable to algorithms that manipulate data structures, e.g. priority queues. Result checking is also commonly considered—although not always explicitly so—in formal verification. For example, result checking is a natural approach to connecting proof assistants with external tools, e.g. computer algebra systems or mathematical packages whose results are untrusted. One prominent application of result checking in proof assistants is Gr´egoire, Th´ery and Werner’s work on primality checking [7], using Pocklington’s criterion, and optimizing the checker in order to check large prime numbers. Another example is the work of Harrison [8], based on a sums of squares representation to certify positive semidefinite polynomials. The experiments have been developed by combining the HOL Light machinery with a semidefinite programming package. Result checking is also used as a proof technique for simplifying the task of program verification; the basic idea is to cut-off program verification tasks by isolating subroutines for which appropriate checkers exist, and then verifying these checkers instead of the aforementioned subroutines. This process is used e.g. in the CompCert project [12], where a formal proof of compiler correctness uses result certification for graph colorings. Applications of result checking to guarantee trust in distributed environments are relatively recent. One of the most prominent ones is Proof Carrying Code (PCC) [13], which offers trust in a mobile application (the value to be checked) through a formal proof (the certificate) that the application respects a given security or safety policy. However, PCC cannot be used to establish the integrity of distributed computations among untrusted hosts, because one cannot make assumptions on the code executed by remote hosts.
A Functional Framework for Result Checking
3
75
Construction of Result Checkers
This section presents a general framework for result checking. Given a predicate P , and assuming that the certificate types and checkers for the atomic predicates that compose P are known in advance, we infer the type of certificates of P , and we can check P using a generic checker. The generic checker, which we present in Section 3.2, is generator-agnostic, and works regardless of the way in which certificates are generated. The main difficulty in this section is handling inductively presented predicates, i.e. predicates that are defined by inference rules of the form:2 Qa [Base case] P a
P r1 . . . P rn P x
R r x
[Inductive case]
where Q is a predicate describing a base case and R is a predicate for the induction step, or equivalently by the equation: P x = Q x ∨ (∃r. (∀i ∈ bounds(r). P ri ) ∧ R r x) where bounds(v) = {i | 1 i |v|}. Pocklington’s criterion is an instance of an inductively presented predicate that has been used for checking primality efficiently. Example 1. A number N is prime if it satisfies Prime N where . Prime N = (N = 2) ∨ (∃ p. (∀i ∈ bounds( p). Prime pi√ ) ∧ Pock p N ) . αk αk 1 1 Pock p N = ∃ αa. | α| = | p| ∧ pα · · · p | (N − 1) ∧ N < pα 1 1 · · · pk ∧ k aN −1 mod N = 1 ∧ ∀i ∈ bounds( p). coprime (a where k = | p|
N −1 pi
− 1) N
Inductively presented predicates arise in many contexts, and it is thus desirable for a result checking framework to support them. 3.1
Properties and Certificates
The starting point of the framework is the definition of formulae and predicates in Figure 1. Instead of relying on the usual formalisation of these as vanilla data types, we take advantage of Haskell’s support for Generalised Algebraic Data Types (GADTs) [11] and index the type of formulae and predicates with a type of certificates: thus, Form w is the type of formulae whose correctness is certificated by terms of type w , and Pred w a is the type of predicates over a with certificate type w . The definition of formulae includes constructors for the true and false values (TT and FF ), logical connectives (∧ and ∨), existential quantification (E ), 2
We stress that inductively presented predicates do not require the ri s to be smaller than x with respect to some well-founded order; in particular every predicate P is equivalent to an inductively presented one by taking Q to be false and n = 1 and R r x to be x = r.
76
G. Barthe, P. Buiras, and C. Kunz
data Form w where TT :: Form () FF :: Form () (∧) :: Form w1 → Form w2 → Form (w1 , w2 ) (∨) :: Form w1 → Form w2 → Form (Either w1 w2 ) E :: Pred w a → Form (a, w ) (@) :: Pred w a → a → Form w Forall :: Pred w a → [a ] → Form [w ] data Pred Atom Abs RecPred
w a where :: (w → a → Bool ) → Pred w a :: (a → Form w ) → Pred w a :: Pred w1 a → Pred w2 ([a ], a) → Pred (RecWit w1 a w2 ) a
Fig. 1. Definition of formulae (Form) and predicates (Pred )
universal quantification over lists (Forall ), and @ for predicate application to a value. For TT and FF , the certificate type is the unit type. For conjunction and disjunction, the certificate types are respectively the product and sum of the certificate types of the two conjuncts. For existential quantification over a predicate P over the type a, a certificate is a pair consisting of an element of a and a certificate for P . Finally, universal quantification requires a list of certificates, and @ expects a certificate for the predicate. We omit treating negation in our framework, since it would be impossible to derive checkers for the negation of an existential. The definition of predicates includes the constructor Abs, which uses higherorder abstract syntax to turn a formulae into a predicate. Atomic predicates, for which we assume that a checker is given, are modeled by encapsulating the corresponding checker with the constructor Atom. The constructor RecPred models inductively presented predicates. It takes the predicate Q as first parameter, and R as the second parameter. The data type constructor RecWit is introduced to define certificate types for recursive predicates: data RecWit w1 a w2 = Base w1 | Rec [a ] [RecWit w1 a w2 ] w2 The parameters for this type are: w1 a w2
the type of certificates for the base case, Q the input type for the predicate the type of certificates for predicate R
A certificate of the form Base w corresponds to the certificate of the validity of the base case Q, as certificated by w . A certificate of the form Rec as ws w proves the existential part, where as is the list of values for which the predicate
A Functional Framework for Result Checking
77
recursively holds, ws is the list of certificates for the values in as, and w is the certificate for R. Example 2. Consider the certification of the primality testing algorithm introduced in Example 1. The predicate Prime derived previously from Pocklington’s criterion matches the form of inductively presented predicates shown above. Therefore, we can encode the predicate Prime as an expression of type Pred , by using the data type constructor RecPred : predicate Q corresponds simply to the condition n ≡ 2; predicate R corresponds to Pock. We also include the definitions of some auxiliary predicates that we need to express the criterion. divides = Atom (λ() (n, m) → m ‘mod ‘ n ≡ 0) mult ps as = product (zipWith (ˆ) ps as) coprimes = Abs (λ(a, n, ps ) → Forall (Abs (λp → coprime@(aˆ((n − 1) ‘div ‘ p) − 1, n))) ps) coprime = Atom (λ() (n, m) → gcd n m ≡ 1) pock ps n = E (Abs $ λ(a, αs) → Atom (λ() (xs, ys) → length xs ≡ length ys)@(ps, αs) ∧ divides @(mult ps αs, n − 1) ∧ Atom (λ() √ (x , y) → x ˆ(y − 1) ‘mod ‘ y ≡ 1)@(a, n) ∧ ordP @( n, mult ps αs) ∧ coprimes@(a, n, ps)) prime = RecPred (Atom (λ() n → n ≡ 2)) (Abs $ λ(ns, n) → pock ns n) The type of prime is prime :: Pred (RecWit () Int ((Int , [Int ]), T )) Int where T = ((((), ()), ()), [()]) is the (trivial) certificate type for Pock. 3.2
Generic Checker
This section describes a generic checker for formulae and predicates. The checker is defined by mutual recursion, and consists of functions check and checkPred , defined in Figure 2. Most equations are straightforward. Atomic predicates are represented by their own checkers, so in checkPred we just call the checker function with the appropriate parameters. Predicate abstractions are also easy to deal with: we just compute the formula with the encapsulated function, and then delegate the work of checking this formula to the function check , taking care to deliver it the right certificate. Finally, we must deal with the recursive predicate case, where terms are of the form RecPred q r . Recursive certificates either certify base cases (predicate q here), or recursive cases (the existential part of the disjunction). If we have a certificate for the base case, we just check predicate q with the supplied certificate. The recursive case is the trickiest one. On the one hand, we must recursively
78
G. Barthe, P. Buiras, and C. Kunz
check check check check check check check check check
:: Form w → w → Bool TT = True = False FF (x ∧ y) (w1 , w2 ) = check x w1 ∧ check y w2 (x ∨ y) (Left w1 ) = check x w1 (x ∨ y) (Right w2 ) = check y w2 (E p) (x , w ) = checkPred p x w (Forall p xs) ws = all id (zipWith (checkPred p) xs ws) (p@x ) w = checkPred p x w
checkPred :: Pred w t → t → w → Bool checkPred (Atom f ) x w =f w x checkPred (Abs f ) x w = check (f x ) w checkPred (RecPred q r ) x (Base w ) = checkPred q x w checkPred (RecPred q r ) x (Rec as rs w2 ) = check (Forall (RecPred q r ) as) rs ∧ checkPred r (as, x ) w2
Fig. 2. The definition of check and checkPred
check the predicate for the list of existentially quantified values. On the other hand, we must check that predicate r holds, which we do by calling checkPred with the appropriate parameters. Example 3. Consider again the example of Pocklington’s Criterion to verify the primality of an integer n. An implementation of a checker for this criterion, requires a partial decomposition of n−1 into prime factors, so we must recursively invoke the checker to verify the primality of these numbers as well. An instance of a Pocklington certificate is the term Rec [2, 3] [Base (), Rec [2] [Base ()] ((2, [1]), t )] ((11, [4, 2]), t ) which proves that the number 1009 is prime, where t = ((((), ()), ()), [()]) is the trivial certificate for the side conditions in Pock. Note that there is unnecessary redundancy in this certificate. There are two instances of the certificate for 2 (i.e. Base ()), which have to be checked separately by the checker. This is inefficient not only in terms of space, but also in terms of execution time. The solution to this problem consists in the introduction of sharing in the certificate data structure. The idea is to avoid repeated subcertificates by having only one instance of each, and allowing it to be shared in several parts of the structure. Using sharing, the certificate for 1009 is written as let w2 = Base () in Rec [2, 3] [w2 , Rec [2] [w2 ] ((2, [1]), t )] ((11, [4, 2]), t ) Although requiring a more sophisticated certificate generation mechanism, sharing forces memoisation of checker results, thus allowing the proof of the primality
A Functional Framework for Result Checking
79
of 2 to be performed only once for this certificate. We would like to stress that no changes in the checker are needed for this to work; it is just a consequence of lazy graph reduction. Our implementation of Pocklington’s criterion is relatively easy to improve by combining the use of clever algebraic properties of the modulo and exponentiation operators, and having certificates for gcd in the coprime predicate. Since these optimisations can be thought of as more sophisticated checkers for atomic predicates, it is straightforward to incorporate them into our framework. Other implementations of Pocklington’s criterion [7,5] using these techniques are able to cope with huge primes, with approximately 10000 digits. 3.3
Generic Checker Properties
Assuming a standard interpretation function [[·]] that maps formulae and predicates to values in some semantic domain suitable for first-order logic, we say that a checker checkPred φ is sound if checkPred φ x w ≡ True ⇒ [[φ]] (x ). It is possible to state a soundness property for generic checkers: all checkers of the form checkPred φ are sound, provided that φ only has atomic predicates with sound checkers. We define a certifying variant of a function of type A → B as a function of type A → (B , W ), where W is the type of the witness. A specification for such a certifying function is a pair (φ,ψ) where φ is a precondition with trivial witness, i.e. of type Pred () A; and ψ is a postcondition of type Pred W (A, B ). The generic checker can be used at runtime to ensure the partial correctness of certifying functions. One can define a function wrap that lifts a certifying computation into the Maybe monad: wrap :: (Pred () a, Pred w (a, b)) → (a → (b, w )) → a → Maybe b wrap (pre, post ) f x | checkPred pre x () ≡ False = Nothing | otherwise = let (y, w ) = f x in if checkPred post (x , y) w then Just y else Nothing The function wrap (φ, ψ) f checks that φ holds on the input, then computes f , and finally checks that ψ holds on the output, returning Nothing if any check fails. It has the useful property of turning a certifying (not necessarily correct) function into a correct one: Theorem. Let f be a certifying function with specification (φ, ψ). Then, wrap (φ, ψ) f is partially correct with respect to the specification (φ, ψ ), where ψ (x , y) = y ≡ Nothing ∨ (y ≡ Just z ∧ ψ (x , z )).
4
Certificate Generation
Certificate generation is a much harder problem than certificate checking. One cannot program a generic certificate generator that automatically builds
80
G. Barthe, P. Buiras, and C. Kunz
certificates of complex properties, even when having certificate generators for atomic predicates. The problem arises from existentials: recall that a certificate for ∃x : T. φ(x) is a pair (t, p) where t is an element of T and p is a certificate of φ(t). In general, it is unfeasible to find t. The problem also appears in the case of inductively presented predicates. In the general case, one cannot find the witnesses that are used to infer that an element verifies the predicates. It should be pointed however, that if we require that the witnesses r are smaller than x w.r.t. a well-founded order, then one can write a generic certificate generator. Of course, such a generic certificate may be hopelessly inefficient. In this section, we present two different approaches to certificate generation for particular classes of problems. They share the underlying notion of certifying higher-order recursion operators, which abstract away common patterns of certificate generation. In particular, we focus on structural recursion and generation patterns, otherwise known as folds and unfolds in the functional programming literature, because it is possible to express all general recursive functions as compositions of these combinators. In both cases, the point is to generate results and their certificates in tandem, instead of doing it in a separate post-processing step. 4.1
Certificates as Monoids
It is possible to define certifying versions of fold and unfold , namely cfold and cunfold , which build certificates incrementally: at each node of the structure, a local certificate is generated; then, all local certificates merge to form a certificate for the whole computation. For this merging to be well-defined, we require that certificates form a monoid. Let us consider the certification of the output of sorting algorithms. In a pure formulation of the problem, a sorting algorithm is fed with an input list L, and produces an output list L such that i. The list L is in nondecreasing order (according to some total order on the elements of L); ii. The list L is a permutation of L. A certifying sorting algorithm would then have to prove that these conditions are met by its output. A checker would need no hints to prove the first condition, since it is possible to efficiently check the sortedness of a list. Therefore, certification in this case boils down to checking the second condition, i.e. that L has the same elements as L. The certificate we require is just a mapping that describes the sorting performed by the algorithm. Consider the Quicksort algorithm, which can be visualised as building a tree of elements such that, when flattened, yields a sorted permutation of the input list. We can make this intermediate structure explicit, and write the tree-generation step as an unfold, and the flattening step as a fold. data Tree a = E | N (Tree a) a (Tree a) unfoldT :: (s → Maybe (s, a, s)) → s → Tree a unfoldT f s = case f s of
A Functional Framework for Result Checking
81
Nothing →E Just (s1 , x , s2 ) → N (unfoldT f s1 ) x (unfoldT f s2 ) foldT :: (b → a → b → b) → b → Tree a → b foldT f z E =z foldT f z (N lt x rt ) = f (foldT f z lt) x (foldT f z rt) qsort = foldT (λls x rs → ls + + x : rs) [ ] ◦ unfoldT build where build [ ] = Nothing build (x : xs) = let (l , r ) = partition (<x ) xs in Just (l , x , r ) By expressing the algorithm in terms of higher-order recursion operators, we can produce a certificate for the result in a compositional way which is directed by the structure of the recursion. For Quicksort, since we are building a tree of elements, we would only be required to show a certificate for every node of the tree, and the higher-order operator could combine them into a certificate for the whole tree. Certificates for Quicksort must be permutations. We shall represent mappings as lists of pairs, where the mapping (x , y) maps the element in position x to position y in the list. newtype Map = Map [(Int, Int)] We can make our Map data type an instance of Monoid in this way: instance Monoid Map where = Map [ ] (Map xs) ⊕ (Map ys) = Map (norm (xs + + ys)) The identity is defined as the empty mapping (represented by [ ]), and the merging operator is the concatenation + + (taking care to normalise the list resulting from the append operation with the auxiliary function norm). This function norm removes pairs coinciding on the first element to make sure that the list still represents a valid function. We can now write certifying versions of fold and unfold for binary trees. cunfoldT :: (Monoid w ) ⇒ (s → Maybe (s, (a, w ), s)) → s → Tree (a, w ) cunfoldT f s = case f s of →E Nothing Just (s1 , (x , w ), s2 ) → N (cunfoldT f s1 ) (x , w ) (cunfoldT f s2 ) cfoldT :: (Monoid w ) ⇒ (b → a → b → (b, w )) → b → Tree (a, w ) → (b, w ) cfoldT f z E = (z , ) cfoldT f z (N lt (x , w ) rt ) = let (v1 , w1 ) = cfoldT f z lt (v2 , w2 ) = cfoldT f z rt (v , fw ) = f v1 x v2 in (v , fw ⊕ w1 ⊕ w ⊕ w2 )
82
G. Barthe, P. Buiras, and C. Kunz
As can be seen, the modified intermediate tree contains nodes of pairs (τ, w ), where certificates of type w are generated by the coalgebra of the function cunfoldT . All certificates in the intermediate tree are combined together by the ⊕ operator, when the tree is flattened by the function cfoldT . We use these combinators to write a certifying version of Quicksort: qs :: (Ord a) ⇒ [(a, Int )] → ([a ], Map) qs = cfoldT (λxs (x , ) ys → (xs + + x : ys, )) [ ] ◦ cunfoldT build build [ ] = Nothing build (x @( , from) : xs) = let (l , r ) = partition (<x ) xs in Just (l , (x , Map (zip (map snd l ) [1 . .] + + [(from, length l + 1)] + + zip (map snd r ) [(length l + 2) . .])), r ) We assume that the input for qs has been preprocessed into a form where every element is paired with its inital position in the list. We omit this transformation since it is trivial and can be done in linear time, so it will not dominate the time complexity of the algorithm. Note that, essentially, all we have to do is to specify the permutation induced by each call to partition (in the function build ). The certifying combinators serve to abstract away the combination of small, locally generated certificates into large, global certificates for the whole computation. Example 4 (Insertion sort). Another sorting algorithm where certifying fold can be used is Insertion sort. This algorithm is traditionally written as follows: isort :: Ord α ⇒ [α] → [α] isort = foldr insert [ ] where insert x [ ] = [x ] insert x (y : ys) | x y = x : y : ys | otherwise = y : insert x ys The certifying version is a little bit more involved. The basic idea is the same as with Quicksort, i.e. to produce an appropriate certificate for each step of the computation, and have the certifying combinator merge them together. We shall present the final version of the algorithm, and invite the reader to work out the details. isort :: [((Int , Int), [(Int , Int)])] → ([Int ], [(Int, Int)]) isort = cfoldL insert [ ] where insert (x , from) ys = let ins x [ ] n = ([x ], n) ins x (y : ys) n |x y = (x : y : ys, n) | otherwise = let (zs, n ) = ins x ys n
A Functional Framework for Result Checking
83
in (y : zs, n + 1) (zs, to) = ins x ys 1 in (zs, [(to + from − 1, from)]) 4.2
Certificates for Nondeterministic Computations
In this section we consider the application of certifying higher-order recursion operators to derive certificate generators for nondeterministic algorithms. Nondeterministic programming is a design methodology that consists in searching for solutions to a constraint satisfaction problem within a given space of potential solutions, which is generally expressed as a tree. Candidate solutions are evaluated in some fixed order, typically depth-first. Although usually requiring some form of backtracking to manage control flow, this type of search is more efficient in terms of program complexity than brute force enumeration of all the candidates, and also easier to express in a functional setting than breadth-first traversal. In general, there is a notion of position or index for traversable container types, i.e. they are isomorphic to the type P → A, where P is the type of positions and A the type of contained elements. Lists, for instance, have integers as positions, since we can regard them as functions from integers into values (of the list element type). Nondeterministic algorithms can be extended to record the path in the tree leading to a valid solution as a certificate, allowing the checker to reproduce the search strategy. This effect can be captured using monads [15], with which we assume the reader is familiar. To support nondeterministic programming, a monad must provide two additional operations: mzero and mplus. The former denotes a failing computation, returning no results. The latter makes a nondeterministic choice between two computations. The generality and expressiveness of Haskell’s type system allows us to write nondeterministic computations that are parameterised by an arbitrary monad, providing it supports these operations. We encode the fact that a monad supports nondeterminism with instances of the following classes: class Monad m ⇒ MonadZero m where mzero :: m α class MonadZero m ⇒ MonadPlus m where mplus :: m α → m α → m α Using these operators, let us write a function that searches a tree for an element satisfying a given predicate: find :: MonadPlus m ⇒ (α → Bool ) → Tree α → m α find p = foldT mzero test where test flt x frt | p x = return x | otherwise = flt ‘mplus‘ frt
84
G. Barthe, P. Buiras, and C. Kunz
We are using the same Tree type and foldT operator as in the previous section. Now, let us propose a certifying version of find , namely cfind . The function cfind behaves exactly like find, but it additionally returns the position of the element in the structure. For this purpose, we introduce yet another certifying version of foldT , and only make minor changes in find . Our certifying operator does most of the work, basically keeping track of the path that we take for each alternative. First of all, we need a data type for describing positions. data Choice = L | R type Position = [Choice ] At every node in the tree, we can either go left or right (denoted by L and R, respectively). Any node in the tree can be identified by the sequence of choices we have to make so as to get there from the root of the tree, so we define the position to be a list of choices. To keep a record of the current path in the tree, we need the monad to hold a trace of type Position . So as to keep the monad type abstract, we will resort to the standard MonadWriter class. Monads in this class support the following operation (among others): tell :: MonadWriter p m ⇒ p → m () Type p is the type of positions, which does not have to be fixed in general. Every time we want to add a position p to the trace, we perform tell p. The type of positions should be a Monoid , and every tell operation appends its parameter to the trace using (⊕). In the running example, positions are lists, which are already defined as monoids in the Haskell module Data.Monoid . We can now give a definition for our certifying fold : cfoldT :: (MonadWriter Position m) ⇒ m b → (m b → t → m b → m b) → Tree t → m b cfoldT z f Empty =z cfoldT z f (Node lt x rt) = f (tell [L] > > cfoldT z f lt) x (tell [R ] > > cfoldT z f rt) The function cfoldT is a just like its non-certifying counterpart, but it records every choice of branching in the trace monad: if we examine the left branch, we append L to the trace; if we examine the right branch, we append R. However, we would like cfind to work in an arbitrary nondeterminism monad. By using a standard monad transformer, called WriterT , we can add tracing capabilities to any monad. Thus, for every monad m, the monad WriterT Position m encapsulates the same effects as m, plus a Position trace. This monad provides an operation runWriterT :: WriterT w m a → m (a, w )
A Functional Framework for Result Checking
85
which performs all tracing effects, accumulating the resulting trace in the second component of the result. This operation, along with cfoldT , allows us to define cfind as follows: cfind :: MonadPlus m ⇒ (a → Bool ) → Tree a → m (a, Position ) cfind p = runWriterT ◦ cfoldT mzero test where test flt x frt | p x = return x | otherwise = flt ‘mplus‘ frt The function cfind captures the essence of a nondeterministic depth-first search in a binary tree. It takes a predicate p and a tree t , and it finds the first element x in t such that p x . In addition, it outputs the path leading to x in the tree as certificate, so that a checker can efficiently locate the element. It is interesting to note that cfind works independently of the way in which we implement nondeterminism: the monad m encapsulates these details.
5
Conclusion
We have provided a general framework for building and checking certificates for a large class of programs and properties. The framework relies on certifying combinators, that carry certificates throughout computations and eventually to results, and checking combinators, that construct checkers for complex properties from checkers for simpler properties. The usefulness of the framework has been demonstrated through a set of examples. It could be argued that the main challenge with result checking is to find ad hoc certifying algorithms, and is thus more central to algorithmics than programming languages. Yet, providing good support for result checking in programming languages is essential to promoting its generalisation. In this sense, we hope that the framework presented in the paper will contribute to increase the number of applications that make beneficial use of result checking. Improving the efficiency of the generic checker seems necessary. One goal would be to check primality using Pocklington’s criterion, achieving performances on a par with [7]. A further goal would be to apply our framework to known certifying algorithms; in particular, considering certifying algorithms for graphs, based on previous work on functional graph algorithms [6,4]. Finally, it would be appealing to implement a result checking infrastructure in the context of a distributed functional language; the implementation should address several interesting issues, including setting up protocols for building and transmitting certificates (certificates, like results, may be built by several cooperating parties), guaranteeing the correctness of result checkers (checkers can be downloaded from untrusted third parties), and providing access to a database of mathematical theorems that can be used to check result checkers (so that the proof of the result checker based on Pocklington’s criterion does not need to contain a proof of the criterion itself).
86
G. Barthe, P. Buiras, and C. Kunz
References 1. Barthe, G., Cr´egut, P., Gr´egoire, B., Jensen, T.P., Pichardie, D.: The mobius proof carrying code infrastructure. In: de Boer, F.S., Bonsangue, M.M., Graf, S., de Roever, W.-P. (eds.) FMCO 2007. LNCS, vol. 5382, pp. 1–24. Springer, Heidelberg (2008) 2. Blum, M., Kannan, S.: Designing programs that check their work. J. ACM 42(1), 269–291 (1995) 3. Bright, J.D.: Checking and Certifying Computational Results. PhD thesis (1994) 4. Brunn, T., Moller, B., Russling, M.: Layered graph traversals and hamiltonian path problems-an algebraic approach. In: Jeuring, J. (ed.) MPC 1998. LNCS, vol. 1422, pp. 96–121. Springer, Heidelberg (1998) 5. Caprotti, O., Oostdijk, M.: Formal and efficient primality proofs by use of computer algebra oracles. J. Symb. Comput. 32(1/2), 55–70 (2001) 6. Erwig, M.: Functional programming with graphs. In: Proceedings of the second ACM SIGPLAN international conference on Functional programming, pp. 52–65. ACM, New York (1997) 7. Gr´egoire, B., Th´ery, L., Werner, B.: A computational approach to pocklington certificates in type theory. In: Hagiya, M., Wadler, P. (eds.) FLOPS 2006. LNCS, vol. 3945, pp. 97–113. Springer, Heidelberg (2006) 8. Harrison, J.: Verifying nonlinear real formulas via sums of squares. In: Schneider, K., Brandt, J. (eds.) TPHOLs 2007. LNCS, vol. 4732, pp. 102–118. Springer, Heidelberg (2007) 9. Hudak, P., Peyton Jones, S.L., Wadler, P., Boutel, B., Fairbairn, J., Fasel, J.H., Guzm´ an, M.M., Hammond, K., Hughes, J., Johnsson, T., Kieburtz, R.B., Nikhil, R.S., Partain, W., Peterson, J.: Report on the Programming Language Haskell, A Non-strict, Purely Functional Language. SIGPLAN Notices 27(5), R1–R164 (1992) 10. Hudak, P., Peterson, J., Fasel, J.: A gentle introduction to Haskell 98 (1999), http://www.haskell.org/tutorial/ 11. Jones, S.P., Vytiniotis, D., Weirich, S., Washburn, G.: Simple unification-based type inference for GADTs. In: Proceedings of the eleventh ACM SIGPLAN international conference on Functional programming, pp. 50–61. ACM, New York (2006) 12. Leroy, X.: Formal certification of a compiler back-end or: programming a compiler with a proof assistant. In: Morrisett, J.G., Peyton Jones, S.L. (eds.) POPL, pp. 42–54. ACM, New York (2006) 13. Necula, G.C.: Proof-carrying code. In: POPL, pp. 106–119 (1997) 14. Sullivan, G.F., Masson, G.M.: Using certification trails to achieve software fault tolerance. In: 20th International Symposium on Fault-Tolerant Computing, FTCS20. Digest of Papers, June 1990, pp. 423–431 (1990) 15. Wadler, P.: Monads for functional programming. In: Jeuring, J., Meijer, E. (eds.) AFP 1995. LNCS, vol. 925, pp. 24–52. Springer, Heidelberg (1995) 16. Zipitr´ıa, F.: Towards secure distributed computations. Master’s thesis, Universidad de la Rep´ ublica, Uruguay (2008)
Tag-Free Combinators for Binding-Time Polymorphic Program Generation Peter Thiemann1 and Martin Sulzmann2 1
Albert-Ludwigs-Universit¨ at Freiburg, Germany
[email protected] 2 Informatik Consulting Systems AG
[email protected]
Abstract. Binding-time polymorphism enables a highly flexible binding-time analysis for offline partial evaluation. This work provides the tools to translate this flexibility into efficient program specialization in the context of a polymorphic language. Following the cogen-combinator approach, a set of combinators is defined in Haskell that enables the straightforward transcription of a binding-time polymorphic annotated program into the corresponding program generator. The typing of the combinators mimics the constraints of the binding-time analysis. The resulting program generator is safe, tag-free, and it has no interpretive overhead.
1
Introduction
A polymorphic binding-time analysis empowers an offline partial evaluator to obtain specialization results on par with those of an online partial evaluator. However, implemented specializers for polymorphic binding-time analysis so far do not exploit the efficiency potential of offline partial evaluation. They are interpreter-based, they pass and interpret binding-time descriptions at specialization time, and they use tagging to distinguish ordinary static values from dynamic values (generated code). For monomorphic binding-time analysis, there is a well-known approach to obtain compiled, tag-free program generators that perform offline partial evaluation. The cogen approach to partial evaluation [14] explains the direct construction of a program generator from a binding-time annotated program. For typed languages, this direct generation step is more efficient than going via the Futamura projections, which can lead to multiple levels of data encoding [12]. For example, the binding-time annotated power function power xD nS = if n =S 0 then lift 1 else x ∗D power x (n −S 1) uses the superscripts S and D to indicate static and dynamic operations that happen at specialization time and at run time, respectively. The lift expression avoids a binding-time mismatch by converting the static constant 1 into the M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 87–102, 2010. c Springer-Verlag Berlin Heidelberg 2010
88
P. Thiemann and M. Sulzmann
required dynamic code at that point. The translation to a program generator can be done in a compositional way, by specifying a translation for each annotated syntactic construct: The constructs annotated with S are translated to themselves, the constructs annotated with D are translated to expressions that generate the respective expression tree, and lift maps to the appropriate syntax constructor. A translation to Haskell would look like this: data Exp = Const Int | Mul Exp Exp -- and so on power :: Exp -> Int -> Exp power x n = if n==0 then Const 1 else Mul x (power x (n-1))
This simple example already demonstrates that the static data is neither encoded nor tagged and that, consequently, the static expressions execute efficiently. The methods used so far for translating binding-time annotated programs to program generators are only suitable for monovariant annotation schemes [1,20, 2, 22]. They do not cover annotations created by the more precise polyvariant binding-time analyses [10, 5, 6, 9, 8]. A polyvariant binding-time analysis enables abstraction over concrete binding times. To continue the example, the power function would receive three additional binding-time parameters that express the binding times of the arguments x and n and of the result of power, which must be more dynamic than either argument: S
S
(power :: Int → Int → Int) : ∀βγδ.(β ≤ δ, γ ≤ δ) ⇒ β →γ →δ power β γ δ x n = if n =γ lift S,γ 0 then lift S,δ 1 else lift β,δ x ∗δ power β γ δ x (n −γ lift S,γ 1) Evidently, the lift expression must be generalized to liftβ,δ which converts a base-type value of binding time β to binding time δ. This conversion requires β ≤ δ where the ordering is the least partial order such that S ≤ D. The other constraint, γ ≤ δ, arises from the conditional. The binding time γ of the condition is a lower bound of the binding time δ of the result. The translation of this annotated program to a satisfactory program generator becomes more tricky. Fig. 1 shows the naive approach, which is hardly satisfactory. First, binding times have to be passed and tested explicitly in the generator. Second, the generator relies on run-time tags to identify static and dynamic values in the Value datatype as evident from the implementations of pIf and pOp2: An untagged generator could omit stripping off (and reapplying) the Bool and Exp tags. Indeed, the pIf combinator could be implemented such that it does not examine the BT argument.1 Third, the generator is not self-checking. Its type does not incorporate the constraints from the binding-time annotation, thus it can give rise to run-time errors because of binding-time mismatches. For 1
Use of the combinator still requires a preceding binding-time analysis because it expects its e1 and e2 arguments to carry the Exp tag if the condition carries an Exp tag. Unlike the staged interpreters of Carette and coworkers [3], this pIf combinator would not be suitable for online partial evaluation because the dynamic version of the conditional does not convert static values in the branches to dynamic values.
Tag-Free Combinators for Binding-Time Polymorphic Program Generation
89
data Value = Int Int | Bool Bool | Exp Exp data BT = S | D powergen :: BT -> BT -> BT -> Value -> Value -> Value powergen b g d x n = pIf g (pOp2 g opEqu n (pLift S g (Int 0))) (pLift S d (Int 1)) (pOp2 d opMul (pLift b d x) (powergen b g d x (pOp2 g opSub n (pLift S g 1)))) -- lifting values of base type pLift :: BT -> BT -> Value -> Value pLift S S (Int i) = Int i pLift S D (Int i) = Exp (Const i) pLift D D (Exp e) = Exp e -- conditional pIf :: BT -> Value -> Value -> Value -> Value pIf S (Bool x) v1 v2 = if x then v1 else v2 pIf D (Exp e) (Exp e1) (Exp e2) = Exp (If e e1 e2) -- binary operator pOp2 :: BT -> Op (Int -> Int -> Int) -> Value -> Value -> Value pOp2 S op (Int x) (Int y) = Int (opvalue op x y) pOp2 D op (Exp x) (Exp y) = Exp (opctor op [x, y]) -- operators data Op t = Op { opvalue :: t, opctor :: [Exp] -> Exp } opMul = Op (*) (\[x,y] -> Mul x y) :: Op (Int -> Int -> Int) opSub = Op (-) (\[x,y] -> Sub x y) :: Op (Int -> Int -> Int) opEqu = Op (==) (\[x,y] -> Equ x y) :: Op (Int -> Int -> Bool) Fig. 1. Naive generator with binding-time polymorphism
example, an invocation (powergen D D S) can result in a run-time error when attempting to execute (pLift D S x). This particular generator has a further problem not addressed in this work. The invocation (powergen D D D x n) does not terminate, for any x and n, because the recursive call to powergen is implicitly static, that is, it is always performed at specialization time. The present work is the first to address the construction of efficient program generators with polymorphic binding times. Because it applies to languages with ML-style polymorphism, it paves the way for efficient program specialization for Haskell. It addresses the three main shortcomings of the naive generator. 1. No interpretive overhead. Binding-time descriptions are passed at run time but they are never tested. Due to laziness they have virtually no cost. 2. Tag-free. The generator requires no tagging, neither type tags nor tags to distinguish static from dynamic values. 3. Safety. The typing of the generator ensures that binding-time inconsistencies in the input of the generator are caught by the type checker before starting the specialization.
90
P. Thiemann and M. Sulzmann
The main contribution is a set of combinators that enables the construction of tag-free program generators via a simple type-directed translation from a polymorphic binding-time type derivation to a Haskell program using these combinators. The starting point is the polyvariant binding-time analysis for ML-style polymorphic languages by Glynn and coworkers [6]. The implementation is in Haskell [16] with various extensions (e.g., type functions [18], multi parameter type classes, rank-2 types [17], GADTs) as implemented by GHC. For lack of space, we assume familiarity with the language and the extensions. The code is available at http://proglang.informatik. uni-freiburg.de/projects/polyspec/.
2
Tag-Free Polymorphic Program Generation
Figure 2 contains the tag free variant of the polymorphic generator for the power function shown in Fig. 1. Before delving into a detailed explanation of the combinators, let’s introduce some preliminaries and run the generator on examples. Like the previous generator, the new generator receives three binding-time parameters and two value parameters. Binding times are represented by polymorphic functions that construct binding-time descriptions (bt descriptions), which are passed to the combinators. A bt description has the same structure as the underlying type but alternates binding times with regular type constructors. Binding times are represented by two data types, St and Dy. newtype St a = St a newtype Dy a = Dy a
-- static annotation -- dynamic annotation
For example, (St int :: St Int) describes a static integer and (St (St Int -> Dy Bool)) is the type of a description of a static function with static input and dynamic output. Descriptions are reified type arguments, which are never evaluated at run time. They are computed at compile time and give rise to the dictionaries for the generator combinators. In principle, the compiler could apply
powergen :: (...) => (forall a. a -> dx a) -> (forall -> (forall a. a -> dz a) -> R Int (dx Int) -> R Int (dn Int) -> R Int powergen dx dn dz x n = cIf (dn bool) (dz int) (cOp2 sEqInt oEqInt n (cSub (St int) (dn int) (cSub (St int) (dz int) (R 1)) (cOp2 sMult oMult (cSub (dx int) (dz int) x) (powergen dx dn dz x (cOp2 sMinus oMinus n (cSub (St int) where { sEqInt = bop2 dn int int bool; sMinus = bop2 sMult = bop2 dz int int int }
a. a -> dn a) (dz Int)
(R 0)))
(dn int) (R 1))))) dn int int int;
Fig. 2. Tag-free generator for specializations of power. The type signature is truncated to save space.
Tag-Free Combinators for Binding-Time Polymorphic Program Generation
91
> -- an all static run computes the power function > unR (powergen St St St (R 2) (R 5)) 32 > -- a run with dynamic basis performs specialization > toString $ unR $ powergen Dy St Dy (R (EVar "x")) (R 5) "EOp2 (*) (EVar x) (EOp2 (*) (EVar x) (EOp2 (*) (EVar x) (EOp2 (*) (EVar x) (EOp2 (*) (EVar x) (EInt 1)))))" > -- nonterminating specialization > toString $ unR $ powergen Dy Dy Dy (R (EVar"x")) (R (EVar"n")) "EIf (EOp2 (==) (EVar n) (EInt 0)) (EInt 1) (EOp2 (*) (EVar x) (EIf (EOp2 (==) (EOp2 (-) (EVar n) (EInt 1)) (EInt 0)) (EInt 1) (EOp2 (*) (EVar x) (EIf (EOp2 (==) Interrupted. > -- binding-time mismatch > powergen Dy Dy St No instances for (CIF Dy St, CSUB (Dy Int) (St Int) Int) Fig. 3. Running powergen
dictionary specialization [11] to eliminate all dictionary handling at run time. In practice, the compiled program still contains some dictionary manipulation. Depending on the instantiation of the binding time parameters, powergen exhibits dramatically different behaviors as shown and labeled in Fig. 3. The nontermination of the third example is the expected behavior because the recursion in powergen is always static. The error message for the last example accurately reflects the failing constraints of the binding-time analysis (§2.1, §2.3). The computation of the generator happens in terms of a representation type R t btd, which depends on the underlying type t and its bt description btd. Any value that is passed into (out of) the generator must first be wrapped (unwrapped). As R is an isomorphism, its use does not amount to tagging.2 -- representation type newtype R t btd = R { unR :: ImpT t btd } -- implementation type type family ImpT t btd -type instance ImpT Int (St Int) type instance ImpT Bool (St Bool) type instance ImpT [a] (St [ba]) type instance ImpT (a -> b) (St (ba -> bb)) -type instance ImpT a (Dy aa)
= = = =
Int Bool [ImpT a ba] ImpT a ba -> ImpT b bb
= Exp a
The argument to the R constructor must have the implementation type, computed by the type function ImpT. For type constructors with static bt descriptions, ImpT rebuilds the type constructors and translates components of the type recursively. 2
The reader may wonder why R t btd is needed as it is isomorphic to ImpT t btd. However, when type inference equates R t1 b1 = R t2 b2 it can deduce that t1 = t2 and b1 = b2. It cannot deduce these equalities from ImpT t1 b1 = ImpT t2 b2.
92
P. Thiemann and M. Sulzmann
This strategy implies that static computations are implemented by themselves. If the translation hits a dynamic annotation, then well-formedness dictates that further components of the type carry a dynamic annotation, too. Hence, any value of dynamic type a is implemented as an expression of type Exp a. The latter type is a GADT with the usual definition. 2.1 Basic Combinators Continuing the analysis of the code in Fig. 2, the cIf combinator takes two bt descriptions, one (dn bool) describing the binding time of the condition and one (dz int) fixing the binding time of the result of the conditional. The remaining arguments stand for the condition, the true-branch, and the false-branch, where the two branches have the same representation type. cIf is overloaded such that there are instances for either static dn and arbitrary dz or for dynamic dn and dz. The type checker rejects any other combination of binding times (via unresolved instance), thus enforcing the constraints of the binding-time analysis. The definition shows that the bt descriptions are not touched. class CIF bb bt where cIf :: bb Bool -> bt shp -> R Bool (bb Bool) -> R t (bt shp) -> R t (bt shp) -> R t (bt shp) instance CIF St bt where cIf _ _ b x y = if (unR b) then x else y instance CIF Dy Dy where cIf _ _ b x y = R (EIf (unR b) (unR x) (unR y))
The combinator cOp2 for binary primitive operations takes a bt description for the type of the operation, the operation itself, and its two arguments. It is implemented in terms of a more general operator cConst, which injects constants into a generator, and function application cApp. Again, the overloading of these combinators enables the dual use of static and dynamic operations. data Op a = Op { opname :: String, opvalue :: a } cOp2 btd op x y = cApp undefined (cApp btd (cConst btd op) x) y
Instead of examining the unwieldy type of cOp2, it is simpler and more general to look at the cConst operator. To safely embed a constant of arbitrary type in a generator requires that the constant’s bt description is uniform, that is, it is either completely static or completely dynamic [21]. This requirement is stronger than the usual well-formedness (see §3.4), which can be enforced locally. Uniformity is asserted with a two-parameter type class Uniform. class Uniform t btd => CONST t btd where cConst :: btd -> Op t -> R t btd instance (AllStatic t aa) => CONST t (St aa) where cConst btd op = R (toImpT btd (opvalue op)) instance (AllDynamic aa) => CONST t (Dy aa) where cConst _ op = R (EConst op)
The dynamic case is straightforward, but the static case has a slight complication. Because of the recursive definition of ImpT for static bt descriptions, the type checker needs a proof that t is equal to ImpT t btd if btd is fully static. The class Uniform defines identities providing this proof in the usual way.
Tag-Free Combinators for Binding-Time Polymorphic Program Generation
93
class Uniform t btd where toImpT :: btd -> t -> ImpT t btd fromImpT :: btd -> ImpT t btd -> t
2.2
Function Combinators
The encoding of functions follows the ideas of higher-order abstract syntax as in previous work [20, 22, 3]. Thus, the generator represents bound variables by metavariables so that the cLam combinator for abstraction takes a function from representation type to representation type as an argument. class CLAM bf bs bt where cLam :: bf (bs as -> bt at) -> (R s (bs as) -> R t (bt at)) -> R (s -> t) (bf (bs as -> bt at)) instance CLAM St bs bt where cLam _ f = R $ (unR . f . R) instance CLAM Dy Dy Dy where cLam _ f = R $ ELam (unR . f . R)
The two instances for the overloaded cLam combinator reflect exactly the bindingtime constraints: a static function does not restrict the binding time of its argument and result, whereas a dynamic function requires dynamic argument and result. The function ELam is the constructor for the typed (higher-order) abstract syntax. The definitions of the combinators cApp for function application and cFix for the fixpoint follow a similar scheme. 2.3
Coercive Subtyping
Subtyping is the final important ingredient of binding-time analysis and expresses conversions (coercions) between binding times. The earlier combinators such as cIf already contain an implicit subtype relation. For example, the instances of type class CIF bb bt imply that bb is a subtype of bt. Explicit coercive subtyping is necessary, for example, to convert a static integer into one of unknown binding time by the coercion (cSub (St int) (dz int) (R 1)) from Fig. 2. In general, a coercion (cSub bfrom bto v) takes two bt descriptions and converts value v from binding time bfrom to binding time bto. The function cSub is defined in type class CSUB. class CSUB b1 b2 t where cSub :: b1 -> b2 -> R t b1 -> R t b2
The instances of this class follow the inductive definition of the subtyping relation in the binding-time analysis (see §3.4). For base types, it corresponds to the wellknown lifting operation. -- reflexivity instance CSUB a a t where cSub _ _ = id -- base
94
P. Thiemann and M. Sulzmann
instance cSub _ instance cSub _
CSUB (St Int) (Dy Int) Int where _ = R . EInt . unR CSUB (St Bool) (Dy Bool) Bool where _ = R . EBool . unR
For function types, the code for the instances also follows the inductive definition, but it requires extra type annotations for technical reasons. 2.4
List Operations
Using the same principles as for functions, it is straightforward to develop combinators that support partially static operations on recursive data types. The signature for list-processing combinators serves as an example. class L d da where -- d : bt of list, da : bt of elements cNil :: d [da s] -> R [a] (d [da s]) cCons :: d [da s] -> R a (da s) -> R [a] cHead :: d [da s] -> R [a] (d [da s]) -> cTail :: d [da s] -> R [a] (d [da s]) -> cNull :: d [da s] -> R [a] (d [da s]) ->
2.5
(d [da s]) -> R [a] (d [da s]) R a [da s] R [a] [da s] R Bool (d Bool)
Further Examples
This section specifies tag-free program generators for the sum and the twice functions. In both cases, the program text is instrumented with the appropriate binding-time descriptions. The comments restate these settings in a more readable, isomorphic way. -- sum :: [Int] -> Int -bt <= ba => [ba]^bt -> ba sumBT bt0 ba0 = -- type isomorphisms: let bt = mbt bt0 -- bt = bt0 ba = mbt ba0 -- ba = ba0 sfix = (bt (Con2 ssum ssum)) -- sfix = ssum -bt-> ssum ssum = (bt (Con2 sarg sres)) -- ssum = sarg -bt-> sres sarg = (bt (Con1 (ba int))) -- sarg = [int^ba]^bt sres = (ba int) -- sres = int^ba in cFix sfix (cLam sfix (\s -> cLam ssum (\l -> cIf (bt bool) sres (cNull sarg l) (cSub (St int) sres (R (0::Int))) (cOp2 (bop2 ba int int int) oPlus (cHead sarg l) (cApp ssum s (cTail sarg l))))))
For the twice function, it would be sufficient to provide b1, b2, and ba and to construct the other descriptions from those.
Tag-Free Combinators for Binding-Time Polymorphic Program Generation
95
-- twice :: (a -> a) -> a -> a -b1 <= b2 => (b2 -ba-> b1) -ba-> b2 -ba-> b1 -- bt_f = b2 -ba-> b1 -- bt_twice = bt_f -bt-> bt_f twiceBT bt_twice bt_f b1 b2 = cLam bt_twice (\f -> cLam bt_f (\x -> (cApp bt_f f (cSub b1 b2 (cApp bt_f f x)))))
3
From Binding-Time Analysis to Tag-Free Program Generators
This section defines a translation that maps a polymorphic binding-time type derivation generated by the polymorphic binding-time analysis of Glynn and coworkers [6] into a valid Haskell program that uses the combinators from §2. We do not claim a formal correctness argument, but we argue informally that the Haskell types express the binding-time constraints and, thus, that Haskell’s type soundness guarantees specialization soundness. Furthermore, our automatic translation scheme relieves the programmer from the cumbersome task of writing tag-free program generators by hand. Before we formalize the type-directed translation scheme, we recapitulate the essentials of Glynn’s and coworkers polymorphic binding-time analysis and establish connections to our set of combinators. 3.1
Underlying Type System
We consider the translation of an ML-style let-polymorphic typed language with base types Int and Bool. For brevity, the formalization omits structured data types, but the implementation supports them (§2.4). Types t ::= α | Int | Bool | t → t Type Schemes σ ::= t | ∀¯ α.t Expressions e ::= x | λx.e | e e | let x = e in e The vector notation α ¯ represents a sequence α1 , . . . αn of type variables. Constructors for numerals and Boolean values are recorded in some initial type environment. The treatment of Haskell’s advanced language feature such as type classes is possible but postponed to future work. For instance, Glynn’s and coworkers polymorphic binding-time analysis is performed on GHC’s internal System F style type language CORE where type classes have already been ’removed’ via the dictionary-passing translation. Hence, we would require combinators operating on GHC’s CORE language directly to properly deal with type classes.
96
P. Thiemann and M. Sulzmann
Δ b : Int
Δ b : Bool
(β : α) ∈ Δ Δ β:α
Δ1 τ 1 : t 1
Δ2 τ 2 : t 2 b
Δ1 ∪ Δ2 τ 1 → τ2 : t1 → t2
Fig. 4. Shape Rules
3.2
Binding-Time Descriptions
On top of the underlying type structure we impose a binding-time (type) description structure which reflects the structure of the underlying type system. S For instance, S → D describes a static function that takes a static value of base type as an argument and returns a dynamic value of base type. Annotations
b ::= δ | S | D
b
τ ::= β | b | τ →τ ¯ ¯ Binding-Time Type Schemes η ::= τ | ∀β, δ.C ⇒ τ Constraints C ::= (τ ≤ τ ) | wft(τ ) | C ∧ C Binding-Time Descriptions
The grammar distinguishes annotation variables δ, which may only be instantiated to S or D, from binding-time type variables β, which may be instantiated to any τ , including δ. Constraints are described in §3.4. 3.3
Shapes
The binding-time description of an expression must generally have the same ’shape’ as its underlying type, in particular in the presence of polymorphism. For this purpose, a shape environment Δ maps a polymorphic binding-time description variable to its corresponding underlying polymorphic type variable. The judgment Δ τ : t states that under shape environment Δ the bindingtime description τ has shape t. A judgment Δ η : σ is valid if it can be derived by the shape rules in Figure 4. For brevity, we omit the straightforward rule for quantified types. The combinator system in §2 detects ill-shaped types via unresolved instances. For example, the (ill-shaped) type R (a -> b) (St Int) yields the unresolved type function application ImpT (a -> b) (St Int). 3.4
Binding-Time Constraints
A subtype constraint (x ≤ y) is read as “y is at least as dynamic as x”. It comes in various flavors: an ordering on annotations (· ≤a ·), a structural ordering (· ≤s ·) on bt descriptions, and an auxiliary ordering (· ≤f ·), which is used in combination with the ’well-formed’ constraint wft() to rule out ’ill-formed’ D constraints such as S → S. Figure 5 summarizes the constraint rules. Our combinator system detects ill-formed constraints via unresolved instances. For example, the irreducible constraint CLAM Dy St St corresponds to the ill-formed D description S → S. A binding-time description is only well-formed if a dynamic
Tag-Free Combinators for Binding-Time Polymorphic Program Generation (Sta) C (S ≤a b)
97
(Dyn) C (b ≤a D)
(Hyp) C1 , (b1 ≤a b2 ), C2 (b1 ≤a b2 ) (Refl) C (b ≤a b)
(Basw ) C wft(b)
(Trans)
C (b1 ≤a b2 )
C (b2 ≤a b3 )
C (b1 ≤a b3 )
C (b3 ≤f τ1 ) (Arroww ) C (b3 ≤f τ2 )
C wft(τ1 ) C wft(τ2 ) b
C wft(τ1 → 3 τ2 ) (Basf )
(Bass )
C (b1 ≤a b2 ) C (b1 ≤f b2 )
C (b1 ≤a b2 ) C (b1 ≤s b2 )
(Arrowf )
C (b1 ≤a b2 ) b
C (b1 ≤f τ1 → 2 τ3 )
C (b2 ≤a b5 ) (Arrows ) C (τ4 ≤s τ1 ) C (τ3 ≤s τ6 ) b
b
C (τ1 → 2 τ3 ≤s τ4 → 5 τ6 )
Fig. 5. Binding-Time Constraint Rules
annotation at the top of a binding-time description implies that all its components are dynamic, too, because nothing can be known about them. Hence, the above constraint is ill-formed. The remaining binding-time subtype relations are expressed via the type class CSUB and its instances (§2.3). The instance bodies construct the necessary coercions among binding-time values. To be honest, the Haskell encoding leads to a slightly inferior system for the following reasons. First, the transitivity rule (Trans) cannot be easily expressed because the straightforward encoding in Haskell instance (CSUB a b t, CSUB b c t) => CSUB a c t
requires guessing the intermediate type b during type class instance resolution. A second short-coming of the Haskell encoding is that out of CSUB (St (a -> b)) (St (a -> c)) (Int -> Int) we cannot extract the proof term (a.k.a. dictionary) connected to CSUB b c Int. The reverse direction is of course possible. Hence, if a program text requires CSUB (St (a -> b)) (St (a -> c)) (Int -> Int) but the surrounding context only provides CSUB b c Int, Haskell’s type inference will fail. A simple workaround for both problems is to provide additional constraints which either mimic application of the transitivity rule or supply the necessary proof terms. 3.5
Type-Directed Translation from BTA to Program Generators
Now everything is in place to describe the automatic construction of program generators based on our combinators out of Glynn and coworkers binding-time
98
P. Thiemann and M. Sulzmann
analysis. The construction is achieved via a type-directed translation scheme and relies on judgments of the form C, Γ (e :: t) : τ (eH CH ) where C is a binding-time constraint, Γ a binding-time environment, e an expression welltyped in the underlying system with type t, τ is a binding-time description, eH is the Haskell expression derived from e instrumented with program generator combinators and CH is a Haskell constraint which contains all the requested combinator instances including subtype (coercion) constraints. Figure 6 contains the (non-syntax directed) translation rules. It is an easy exercise to make them syntax directed, following Glynn and coworkers [6]. In rule (Sub), C CH denotes the translation of binding-time subtype constraints (τ1 ≤ τ2 ) to Haskell type class constraints CSUB τ1 τ2 t for some appropriate t.3 Ill-formed binding-time descriptions are caught via unresolved instances. Hence, the translation simply drops the well-formed constraint wft(τ ). The translation of the judgment C (τ2 ≤s τ1 ) to the Haskell setting may not hold any more, unless C contains redundant constraints as discussed in §3.4. Hence, we assume from now on that such redundant constraints are present in C. In the resulting (Haskell) program text, the expression eH is coerced to the expected bt description τ1 by inserting the combinator call cSub τ2 τ1 . Descriptions such as τ1 occurring in expressions are short-hands for undefined :: τ1 where variables appearing in τ1 are bound by lexically scoped type annotations.4 Recall that binding-time descriptions passed at run-time are never inspected. Thanks to laziness they have virtually no cost. Rule (Abs) and (App) are straightforward and do not contain any surprises. The rule (Let) additionally abstract over the binding-time descriptions β¯ and δ¯ which then will be supplied with arguments at the instantiation site (see rule (∀E)). The function inst computes the corresponding binding-time description instances for each underlying type instance. Let Δ be a shape environment, t¯ a sequence of underlying types, and α ¯ a sequence of underlying type variables. Let inst(Δ, t¯, α ¯ ) = τ¯ where τ¯ are fresh binding-time types of appropriate shape: Each element of τ¯ is related to the corresponding element of β¯ by the shape environment Δ. That is, Δ, ti τij where Δ βij : αi . The last rule (Fix) deals with polymorphic recursion (in the binding-time descriptions). A fixpoint iteration is required to compute the set of combinator instances CIF etc. The constraints resulting from (e :: t) are split into those, which are not connected to δ¯ (C1H ), and those which constrain δ¯ (C2H ). The fixpoint operator F starts with C2 H plus the Haskell equivalent C2 H of the subtype constraints in C2 and iterates until a fixpoint C3 H is found. The exact definition of F is as follows: ¯ 2 H ⇒ τ }, e :: t) F (Γ ∪ {x : ∀δ.C ¯ 2H ∧ C3H ⇒ τ }, e :: t) = F (Γ ∪ {x : ∀δ.C = C2 H
if C2H =set C3H otherwise
¯ 2H ⇒ τ } (e :: t) : τ (eH C3H ). where Γ ∪ {x : ∀δ.C 3 4
In a syntax-directed inference system the program text determines the type t. The alternative is to build an explicit term of type τ1 as in Fig. 2.
Tag-Free Combinators for Binding-Time Polymorphic Program Generation
(Var)
(Sub)
(x : η) ∈ Γ
Δ η:σ
C CH
C, Γ (x :: σ) : η (x CH ) C, Γ (e :: t) : τ2 (eH CH )
C (τ2 ≤s τ1 )
C wft(τ1 )
C, Γ (e :: t) : τ1 (cSub τ2 τ1 eH CH ∧ CSUB τ2 τ1 t) C, Γ ∪ {x : τ1 } (e :: t2 ) : τ2 (eH CH ) C wft(τ1 ) Δ τ1 : t1
(Abs)
S
C, Γ (λx.e :: t1 → t2 ) : τ1 → τ2 (cLam ST(τ1 → τ2 ) (λx.eH ) CH ∧ CLAM ST τ1 τ2 }) b
C1 , Γ (e1 :: t1 → t2 ) : (τ1 → τ2 ) (e1H C1H ) C2 , Γ (e2 :: t1 ) : τ1 (e2H C2H ) (App)
(Let)
C1 ∧ C2 , Γ (e1 e2 :: t2 ) : τ2 (cApp (b (τ1 → τ2 )) e1H e2H C1H ∧ C2H ∧ CAPP b τ1 τ2 ) C1 , Γ (e1 :: t1 ) : τ1 (e1H C1H ) ¯ δ¯ ⊆ fv(C1 , τ1 )\fv(Γ ) Δ τ1 : t1 β, where Δ βij : αi ¯ 1 ⇒ τ1 } (e2 :: t2 ) : τ2 (e2H C2H ) C2 , Γ ∪ {x : ∀β¯δ.C C2 , Γ (let x = (e1 :: ∀α.t ¯ 1 ) in e2 :: t2 ) : τ2 ¯ δ.e ¯ 1H :: ∀α ¯ 1H ⇒ R t1 τ1 ) in e2H C2H ) (let x = (λβ.λ ¯ β¯δ.C
(∀E)
(Fix)
¯ δ.D ¯ ⇒ τ (eH CH ) C, Γ (e :: ∀α.t) ¯ : ∀β, Δ τ : t inst(Δ, t¯, α ¯ ) = τ¯ ¯ ¯b/δ]D ¯ C [¯ τ /β, ¯ ¯b/δ]τ ¯ (eH τ¯ ¯b CH ) C, Γ (e :: [t¯/α]t) ¯ : [¯ τ /β, ¯ 2 ⇒ τ C2 C2 H η = ∀δ.C C1 ∧ C2 , Γ ∪ {x : η} (e :: t) : τ (eH C1H ∧ C2H ) C1 ∧ C2 wft(τ ) Δ η : t fv(C1H ) ∩ δ¯ = ∅ fv(C2H ) ⊆ δ¯ ¯ 2 H ∧ C2H ⇒ τ }, e :: t) = C3H F(Γ ∪ {x : ∀δ.C C1 , Γ ((fix x :: t in e) :: t) : η ¯ H :: ∀δ.C ¯ 3H ⇒ R t τ ) in x C1H ) (let x = (λδ.e Fig. 6. Type-direction translation rules
99
100
P. Thiemann and M. Sulzmann
The following example serves to illustrate the fixpoint iteration. f x y = if x == 0 then 1 else f y (x-1)
Function f’s most general binding time description is S
S
∀bx , by , b.(bx ≤ b) ∧ (by ≤ b) ⇒ bx → by →b Binding-time polymorphism is required in the subexpression f y (x-1) for S S building the instance by → bx → b. The first step of the translation yields the following (Haskell) constraints from the program text: SUB bx b Int, SUB by b Int, CIF bx b, CLAM St by b, CLAM St bx (by->b), CAPP St b x b, CAPP St by (bx->b)
These constraints are not sufficient for the resulting program to type check. For example, at the instantiation site f y (x-1) the constraint CIF by b is needed but there is only CIF bx b. Another iteration starting with the above constraints leads to the fixpoint:5 SUB bx b Int, CIF bx b, CIF CLAM St by b, CAPP St bx b,
SUB by b Int, by b, CLAM St bx (by->b), CLAM St bx b, CLAM St by (bx->b), CAPP St by (bx->b), CAPP St by b, CAPP St bx (by->b)
The fixpoint iteration must terminate because it only iterates over annotations whose shape is fixed/bound by the underlying type. Hence, the number of instances arising is finite. An alternative translation scheme could employ the cFix combinator also provided by the library. It corresponds to a monomorphic (Fix) rule, which requires no fixpoint iteration. In summary, the type-directed translation scheme builds a tight correspondence between the typing of the combinators and the typing rules of the bindingtime analysis. It might be stated as a slogan in the following way. Proposition 1. Let True, ∅ (e :: t) : τ (eH CH ). Then, the resulting expression eH is well-typed in Haskell with type R t τ under constraints CH . We have no proof for this proposition, although it is easy in many cases to match the typing of a single combinator with its corresponding typing rule. An attempt to prove it would have to overcome the shortcomings discussed in the preceding text and it would have to draw on a formalization of a large subset of Haskell’s type system. Both tasks are out of scope of the present work. 5
The fixpoint iteration requires a variant of the (∀E) rule which also infers the required instantiation constraints, rather than simply checking if the provided constraints imply the instantiation constraints. For reasons of space, we omit the straightforward details.
Tag-Free Combinators for Binding-Time Polymorphic Program Generation
4
101
Related Work
Among the large body of related work on partial evaluation (see the respective overviews [12, 7]), there are only few works which consider offline partial evaluation based on a polymorphic binding-time analysis for polymorphic languages [6, 8, 9]. None of them consider the direct construction of program generators. Only Heldal and Hughes [8] deal with the pragmatics of constructing the specializer. Other works that consider polymorphism concentrate either on polymorphic binding-time analysis for monomorphic languages [10, 5] or monomorphic analysis for polymorphic languages [15, 13, 4]. Closely related are previous constructions of combinators that perform specialization by the first author [20, 22] as well as combinators by Carette and coworkers [3] that can be statically configured (either via overloading or via the ML module language) to perform evaluation, compilation, or (online) partial evaluation. Two main differences to the latter work are (1) that our combinators are geared towards offline partial evaluation and require a preceding binding-time analysis and (2) that our combinators are dynamically configured by type passing.
5
Conclusion
The present work complements the earlier work of Glynn and coworkers [6] and puts it into practice. Our combinators solve the open question of obtaining safe and efficient (tag-free) program generators for ML-style languages based on a polymorphic binding-time analysis. Our proof-of-concept implementation relies on GHC’s advanced (source) typing features and allows us to experiment with smaller examples. There are many opportunities for future work. We doubt that there an analogous set of combinators that can be implemented in ML, but it is an interesting question to consider. We believe that the approach is extensible to typing features of Haskell beyond ML. We further believe that the approach can be extended to cater for typical partial evaluation features like program point specialization, multi-level specialization, or continuation-based specialization. Acknowledgments We thank the reviewers for their feedback on the submitted version of this paper.
References 1. Birkedal, L., Welinder, M.: Hand-writing program generator generators. In: Hermenegildo, M.V., Penjam, J. (eds.) PLILP 1994. LNCS, vol. 844, pp. 198–214. Springer, Heidelberg (1994) 2. Bondorf, A., Dussart, D.: Improving CPS-based partial evaluation: Writing cogen by hand. In: Sestoft, P., Søndergaard, H. (eds.) [19], Technical Report 94/9, Department of Computer Science, pp. 1–10 3. Carette, J., Kiselyov, O., Chieh Shan, C.: Finally tagless, partially evaluated: Tagless staged interpreters for simpler typed languages. J. Funct. Program. 19(5), 509–543 (2009)
102
P. Thiemann and M. Sulzmann
4. De Niel, A., Bevers, E., De Vlaminck, K.: Partial evaluation of polymorphically typed functional languages: the representation problem. In: JTASPEFT/WSA 1991, pp. 90–97 (1991) 5. Dussart, D., Henglein, F., Mossin, C.: Polymorphic recursion and subtype qualifications: Polymorphic binding-time analysis in polynomial time. In: Mycroft, A. (ed.) SAS 1995. LNCS, vol. 983, pp. 118–136. Springer, Heidelberg (1995) 6. Glynn, K., Stuckey, P., Sulzmann, M., Søndergaard, H.: Boolean constraints for binding-time analysis. In: Danvy, O., Filinski, A. (eds.) PADO 2001. LNCS, vol. 2053, pp. 39–62. Springer, Heidelberg (2001) 7. Hatcliff, J., Mogensen, T.Æ., Thiemann, P. (eds.): DIKU 1998. LNCS, vol. 1706. Springer, Heidelberg (1999) 8. Heldal, R., Hughes, J.: Binding-time analysis for polymorphic types. In: Bjørner, D., Broy, M., Zamulin, A.V. (eds.) PSI 2001. LNCS, vol. 2244, pp. 191–204. Springer, Heidelberg (2001) 9. Helsen, S., Thiemann, P.: Polymorphic specialization for ML. ACM TOPLAS 26(4), 1–50 (2004) 10. Henglein, F., Mossin, C.: Polymorphic binding-time analysis. In: Sannella, D. (ed.) ESOP 1994. LNCS, vol. 788, pp. 287–301. Springer, Heidelberg (1994) 11. Jones, M.P.: Partial evaluation for dictionary-free overloading. In: Sestoft, P., Søndergaard, H. (eds.) [19], pp. 107–118. Technical Report 94/9, Department of Computer Science 12. Jones, N., Gomard, C., Sestoft, P.: Partial Evaluation and Automatic Program Generation. Prentice-Hall, Englewood Cliffs (1993) 13. Launchbury, J.: A strongly-typed self-applicable partial evaluator. In: Hughes, J. (ed.) FPCA 1991. LNCS, vol. 523, pp. 145–164. Springer, Heidelberg (1991) 14. Launchbury, J., Holst, C.K.: Handwriting cogen to avoid problems with static typing. In: Draft Proceedings, Fourth Annual Glasgow Workshop on Functional Programming, Skye, Scotland, pp. 210–218. Glasgow University (1991) 15. Mogensen, T.Æ.: Binding time analysis for polymorphically typed higher order languages. In: D´ıaz, J., Orejas, F. (eds.) CAAP 1989 and TAPSOFT 1989. LNCS, vol. 351, pp. 298–312. Springer, Heidelberg (1989) 16. Peyton Jones, S. (ed.): Haskell 98 Language and Libraries, The Revised Report. Cambridge University Press, Cambridge (2003) 17. Peyton Jones, S., Vytiniotis, D., Weirich, S., Shields, M.: Practical type inference for arbitrary-rank types. J. Funct. Program. 17(1), 1–82 (2007) 18. Schrijvers, T., Peyton Jones, S.L., Chakravarty, M.M.T., Sulzmann, M.: Type checking with open type functions. In: Thiemann, P. (ed.) Proc. ICFP 2008, Victoria, BC, Canada, October 2008, pp. 51–62. ACM Press, New York (2008) 19. Sestoft, P., Søndergaard, H. (eds.): Proc. 1994 ACM Workshop Partial Evaluation and Semantics-Based Program Manipulation, Orlando, Fla (June 1994); University of Melbourne, Australia. Technical Report 94/9, Department of Computer Science 20. Thiemann, P.: Cogen in six lines. In: Dybvig, K. (ed.) Proc. 1996 ICFP, Philadelphia, PA, pp. 180–189. ACM Press, New York (1996) 21. Thiemann, P.: Aspects of the PGG system: Specialization for standard Scheme. In: Hatcliff, et al. (eds.) [7], pp. 412–432 22. Thiemann, P.: Combinators for program generation. J. Funct. Program. 9(5), 483– 525 (1999)
Code Generation via Higher-Order Rewrite Systems Florian Haftmann and Tobias Nipkow Technische Universit¨ at M¨ unchen, Institut f¨ ur Informatik http://www.in.tum.de/~ haftmann/ http://www.in.tum.de/~ nipkow/ Abstract. We present the meta-theory behind the code generation facilities of Isabelle/HOL. To bridge the gap between the source (higherorder logic with type classes) and the many possible targets (functional programming languages), we introduce an intermediate language, MiniHaskell. To relate the source and the intermediate language, both are given a semantics in terms of higher-order rewrite systems (HRSs). In a second step, type classes are removed from Mini-Haskell programs by means of a dictionary translation; we prove the correctness of this step. Building on equational logic also directly supports a simple but powerful algorithm and data refinement concept.
1
Introduction and Related Work
Like many theorem provers, Isabelle/HOL can generate functional programs from recursive functions specified in the logic. Many applications have taken advantage of this feature, e.g. the certified termination analysis tool CeTA [19] or the Quickcheck counterexample search [3]. The initial code generator [2] has since been replaced by a new design [6] that supports a) type classes and b) multiple target languages (currently: SML, OCaml and Haskell). This paper describes the meta-theory underlying this new design. The theoretical contributions can be summarized as follows: – The formalization of the various stages of the translation between HOL and a functional programming language by means of an intermediate language, Mini-Haskell, with an equational semantics intermediate language, Mini-Haskell, with an equational semantics in terms of higher-order rewrite systems. The equational semantics has two advantages: • Correctness of the translation is established in a purely proof theoretic way by relating rewrite systems. • Instead of a fixed programming language we cover all functional languages where reduction of pure terms (no side effects, no exceptions, etc) can be viewed as equational deduction. This requirement is met by languages like SML, OCaml and Haskell, and we only generate pure programs. We are also largely independent of the precise nature of the source logic because we focus on its equational sublanguage.
Supported by DFG project NI 491/10-1.
M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 103–117, 2010. c Springer-Verlag Berlin Heidelberg 2010
104
F. Haftmann and T. Nipkow
– A non-trivial correctness proof for the replacement of type classes by dictionaries. In contrast to Haskell, where the meaning of type classes is defined by such a translation, our starting point is a language with type classes which already has a semantics. Thus we need to show that this translation preserves the semantics. On a practical level we show how the code generator supports stepwise refinement of both algorithms and data by means of code lemmas that replace less efficient functions and data by more efficient ones in a uniform and logically sound way. Related work. Many theorem provers support code generation by translating an internal functional language to an external one: – Coq can generate OCaml, Haskell and Scheme both from constructive proofs and explicitly defined recursive functions [11]. – The language of the theorem prover ACL2 is (almost) a subset of Common Lisp, i.e. the translation is (almost) the identity function [5]. – PVS allows evaluation of ground terms by translation to Common Lisp [4]. The gap between the functional language of the theorem prover and the target programming language varies from system to system and needs to be bridged with care if it is less trivial than in the case of ACL2. We follow common practice (e.g. [10]) and show the correctness of the key part of our translation by a standard mathematical proof. The outline of the paper is as follows: First we introduce the types and terms of Isabelle/HOL and describe its internal functional language (2). Then we describe how code generation works in principle and introduce the intermediate language to abstract from the details of specific target languages (3). The technical core of the paper is 4, where we prove correctness of a key component of our code generator, the dictionary translation that eliminates Isabelle’s type classes from the intermediate language. Finally we describe how the code generator naturally supports algorithm and data refinement (5).
2
Isabelle/HOL
Isabelle/HOL [14] is an interactive proof assistant for higher-order logic (HOL). Isabelle’s HOL is a typed λ-calculus with polymorphism and type classes. It is based on the following syntactic entities, where en denotes the tuple or sequence e1 , . . . , en , where the index can be omitted for brevity. – – – – – – –
classes: c with a subclass relation ⊆ sorts: s ::= c1 ∩ . . . ∩ cn type constructors: κ with fixed arities types: τ ::= κ τ | α::s instances: κ :: s → c constants: f with most general type scheme ∀ α::s. τ terms: t ::= f [τ ] | x ::τ | λx ::τ . t | t1 t2
Code Generation via Higher-Order Rewrite Systems
105
Classes correspond to Haskell type classes in their classical formulation [7]. Notationally we treat them as sets of types rather than predicates on types. Sorts are an auxiliary notion that describes (possibly empty) intersections of classes. Types are built up in the usual fashion from (sorted) type variables and type constructors. They form an order-sorted algebra [18]. The type-in-class and type-in-sort judgments τ :: c and τ :: s induced by subclasses and instances are defined in 4. Terms are built up from polymorphic constants, variables, abstractions and applications. Constants are polymorphic and may appear at different types. If f has type scheme ∀ α::sn . τ , where αn must be the set of all type variables in τ , any occurrence of f in a term must be of the form f [τ n ], where type argument τ i instantiates type parameter αi . Well-typedness requires τ i :: si (i = 1, . . . , n), in which case f [τ n ] :: τ [τ 1 /α1 ,. . . ,τ n /αn ]. The remaining typing rules for t :: τ are standard. We assume that type/term variables are consistently tagged with their sorts/types. Isabelle/HOL identifies terms up to αβη conversion. Terms of the distinguished type prop are called propositions; the most interesting propositions in our case are equations built from equality = with type scheme ∀ α. α ⇒ α ⇒ prop,1 where ⇒ is the function space type constructor. It is important to realize that types are an integral part of the term language and that substitutions can affect both type and term variables. For example, we can have the equations zero [nat ] = 0 and zero [set α] = ∅. The presence of types ensures that at most one of these two equations is applicable to a given term: we have zero [set nat ] = ∅ (by instantiation) but not zero [set nat ] = 0. Isabelle/HOL provides theories as containers of logical (and extra-logical) data. Internally, a theory is incrementally enriched with primitive definitions and theorems. Theorems can only be proved by a fixed set of inference rules. It is this notion of theorems as an abstract type that leads to a small trusted (and trustworthy) kernel. To make the kernel accessible to humans, high-level specification and automated proof tools are provided, to which Isabelle’s specification and proof language Isar provides a coherent interface: Isar theory text consists of a series of statements, each of which produces new definitions and/or theorems. For example, this is a specification of queues in Isar:2 datatype α queue = Queue (α list) definition empty :: α queue where empty = Queue [] fun enqueue :: α ⇒ α queue ⇒ α queue where enqueue x (Queue xs) = Queue (xs @ [x ]) fun dequeue :: α queue ⇒ α option × α queue where dequeue (Queue []) = (None, Queue []) | dequeue (Queue (x # xs)) = (Some x , Queue xs) 1 2
For Isabelle experts: for our purpose we can and have identified ≡ and =. In concrete Isabelle syntax, types are written postfix: (τ ) κ rather than κ τ . Lists have explicit enumeration syntax [. . . ]; cons is written as # and append as @.
106
F. Haftmann and T. Nipkow
This illustrates datatype and function definitions. Statements for type class specification and instantiation complete Isabelle/HOL’s functional programming language. Here is an example of a lemma with a simple proof (by . . . ): lemma dequeue-enqueue-empty: dequeue (enqueue x empty) = (Some x , empty) by (simp add : empty-def )
3
Code Generation
The Haskell code generated from the queue specification contains no surprises:3 newtype Queue a = Queue [a]; empty :: forall a. Queue a; empty = Queue []; dequeue :: forall a. Queue a -> (Maybe a, Queue a); dequeue (Queue []) = (Nothing, Queue []); dequeue (Queue (x : xs)) = (Just x, Queue xs); enqueue :: forall a. a -> Queue a -> Queue a; enqueue x (Queue xs) = Queue (xs ++ [x]);
Superficially this appears like a trivial syntactic transformation of Isar text, but this is misleading: the source of code generation is not the Isar text as typed by the user, but equational theorems proved in the theory. Typically these result from the Isar statements above, but they may also have been proved by the user, which leads to a powerful method for program refinement (see 5). Thus code generation is the translation of a system of equations in the logic to a corresponding program text which implements the same system. A suitable abstract framework to describe these equations are higher-order rewrite systems (HRSs) [13], i.e. rewrite systems on typed λ-terms. Because types are really part of the term language (see the discussion above), we do not need to extend the HRS framework to cover our application. HRSs can serve as the uniform basis for both the source logic and the target programming language. If the code generator preserves the equations from the logic when turning them into programs, partial correctness of the generated programs w.r.t. the original equational theorems is guaranteed. No claims are stated for aspects which have no explicit representation in the logic, in particular termination or runtime complexity. This scenario assumes that our target languages cover the simply-typed λcalculus, and functions can be specified by equations with pattern matching, which is the case for our targets SML, OCaml and Haskell. Note that code generation addresses only the pure part of those languages: no side effects or exceptions. Hence an equational semantics is justified. 3
Isabelle’s type option is translated to Haskell’s isomorphic type Maybe, and similarly for lists.
Code Generation via Higher-Order Rewrite Systems
3.1
107
Intermediate Language
There remains one substantial difference between equational theorems and a concrete target language program: a program cannot specify an arbitrary HRS, but imposes syntactic restrictions on the equations. Therefore one task of the code generator is to arrange equational theorems in a fashion such that translation to a target language becomes feasible. This is conveniently shared between all target languages by introducing an intermediate language “Mini-Haskell” with four kinds of statements: data κ αk = f1 of τ 1 | · · · | fn of τ n fun f :: ∀ α::sk . τ where f [α::sk ] t1 = t1 | ... | f [α::sk ] tn = tn class c ⊆ c1 ∩ · · · ∩ cm where g1 :: ∀ α::c. τ 1 , . . . , gn :: ∀ α::c. τ n inst κ α::sk :: c where g1 [κ α::sk ] = t1 , . . . , gn [κ α::sk ] = tn The data and fun statements should be clear. The class statement introduces a new class c with superclasses c1 , . . . , cm and class methods g1 , . . . , gn . The inst statement instantiates class c with type constructor κ, assuming that the arguments of κ are of the sorts sk . Dropping the type variables we can write κ :: s → c instead of κ α::s :: c. Terms occurring as arguments on left-hand sides of equations in fun statements are required to be left-linear constructor patterns, where constructors are constants introduced by data statements. The class and instance hierarchy must be coregular [16]: for each instance κ :: si → c and each superclass d of c, there must be exactly one instance κ :: z i → d and each sj must be a subsort of zj , i.e. each class in sj must be a subclass (in the transitive reflexive sense) of some class in zj . Among other things, this guarantees principal types. These and further standard well-formedness requirements are discussed elsewhere [6]. The equational semantics of a Mini-Haskell program is given by the set of equations in its fun and inst statements, restricted to well-typed terms. Therefore the translation from a HOL theory T to Mini-Haskell is straightforward: take some (user specified) subset of equational theorems from T , turn them into fun and inst statements, and enrich that with suitable data and class statements to form a type correct Mini-Haskell program. The semantic essence, the equations, are not modified, only the syntax is adjusted. However, a translation to SML or OCaml requires a further step to eliminate type classes via dictionaries:
108
F. Haftmann and T. Nipkow
intermediate program
dictionary intermediate program w/o type classes translation
HOL theory
⊇ HRS
∼
HRS w/o type classes
The upper level of the diagram is the actual translation process, the dashed arrows are the projections to the equations, the lower level are the resulting HRSs. The dictionary translation process is explained in 4. It alters the HRS considerably and we show that its semantics is preserved. The transformation of an intermediate program to a program in a full-blown SML or Haskell-like target language is again a mere syntactic adjustment and does not change the equational semantics. Note that in this last step we restrict ourselves to partial correctness: if evaluation of a term t in the target language terminates with value v, then t = v is derivable in the equational semantics of the intermediate program. Therefore we are independent of the evaluation strategy of the target language.
4
Dictionary Translation
In Isabelle/HOL, types are part of the term language via f [τ ] and for class methods g these types help to determine if a particular equation for g applies or not. We remove these types and classes by the well-known dictionary translation (e.g. [7], which we loosely follow) and show that the semantics is preserved. The dictionary translation is always applied to a whole program. In the following we avoid carrying around an explicit context but refer implicitly to the declarations in that program: typing of constants f :: ∀ α::s. τ (in fun, class and data statements), instances κ :: s → c, and classes c ⊆ c1 ∩ · · · ∩ cm . Table 1 describes how dictionary translation operates on intermediate language statements. The central idea is that a statement class c . . . translates to a record-like datatype δ c α, a dictionary type, which contains fields for all class methods of c. The class methods gi are defined as projections of the appropriate fields from a dictionary of type δ c α. Correspondingly a statement inst κ α::sk :: c . . . translates to a dictionary of type δ c (κ α::sk ) containing methods defined in this instance. Superclasses are dealt with by extending dictionary types with additional fields for superclass dictionaries and by defining corresponding projections π d→c . Note that the inst translation only works because of coregularity (see above): otherwise the required dictionaries for the superclasses might not be well-defined. Both fun and inst statements are translated by means of three auxiliary functions (|·|) on type schemes, terms and type-in-sort judgments:
Code Generation via Higher-Order Rewrite Systems
109
Table 1. Dictionary translation for program statements statement
statement(s) with dictionaries
data κ αk = f1 of τ 1 | · · · | fn of τ n
data κ αk = f1 of τ 1 | · · · | fn of τ n
fun f :: ∀ α::sk . τ where f [α::sk ] t1 = t1 | ... | f [α::sk ] tn = tn
fun f :: (|∀ α::sk . τ |) where (|f [α::sk ] t1 |) = (|t1 |) | ... | (|f [α::sk ] tn |) = (|tn |)
class c ⊆ c1 ∩ · · · ∩ cm where g1 :: ∀ α::c. τ 1 , ..., gn :: ∀ α::c. τ n
data δ c α = Δc of (δ c1 α) · · · (δ cm α) τ 1 · · · τ n fun π c→c1 :: ∀ α. δ c α ⇒ δ c1 α where π c→c1 (Δc xc1 · · · xcm xg1 · · · xgn ) = xc1 ... fun π c→cm :: ∀ α. δ c α ⇒ δ cm α where π c→cm (Δc xc1 · · · xcm xg1 · · · xgn ) = xcm fun g1 :: ∀ α. δ c α ⇒ τ 1 where g1 (Δc xc1 · · · xcm xg1 · · · xgn ) = xg1 ... fun gn :: ∀ α. δ c α ⇒ τ n where gn (Δc xc1 · · · xcm xg1 · · · xgn ) = xgn
inst κ α::sk :: c where g1 [κ α::sk ] = t1 , ..., gn [κ α::sk ] = tn
fun cκ :: (|∀ α::sk . δ c (κ α::sk )|) where (|κ α::sk :: c|) = Δc (|κ α::sk :: c1 |) · · · (|κ α::sk :: cn |) (|t1 |) · · · (|tn |) if c ⊆ c1 ∩ . . . ∩ cn
Translation of type schemes: (|∀ α::s. τ |) turns the sorts s into additional dictionary type parameters: (|∀ α1 :: (c1,1 ∩ · · · ∩ c1,k1 ) · · · αn :: (cn,1 ∩ · · · ∩ cn,kn ). τ |) = ∀ α1 · · · αn . δ c1,1 α1 ⇒ · · · ⇒ δ c1,k1 α1 ⇒ · · · ⇒ δ cn,1 αn ⇒ · · · ⇒ δ cn,kn αn ⇒ τ Translation of terms: (|t |) replaces type arguments by dictionaries: f :: ∀ α1 ::s1 · · · αn ::sn . τ (|f [τ 1 , . . . , τ n ]|) = f (|τ 1 :: s1 |) · · · (|τ n :: sn |) (|x ::τ |) = x ::τ
(|λx ::τ . t |) = λx ::τ . (|t |)
(|t1 t2 |) = (|t1 |) (|t2 |)
Translation of type-in-sort judgments: The translation of a type-in-class judgment τ :: c amounts to the construction of a dictionary D for type τ . We combine both into one judgment τ :: c D :
110
F. Haftmann and T. Nipkow
κ :: sn → c τ 1 :: s1 D1 . . . τ n :: sn Dn κ τ 1 · · · τ n :: c cκ D1 · · · Dn α::(c1 ∩ · · · ∩ cj ∩ · · · ∩ cn ) :: cj αj τ :: d D d ⊆ . . . ∩ c ∩ . . . τ :: c π d→c D τ :: c1 D1 . . . τ :: cn Dn τ :: c1 ∩ · · · ∩ cn D1 · · · Dn The first two rules create dictionaries from cκ s and dictionary variables. By convention we translate a type variable α::s where s = c1 ∩ · · · ∩ cn (and where the ci are in some canonical order) into dictionary variables α1 , . . . , αn such that each αi represents a dictionary for class ci . The third rule projects superclass dictionaries. The last rule reduces type-in-sort to type-in-class. It produces a sequence of dictionaries, one for each class ci in the sort. Now we define (|τ :: c|) = D if τ :: c D is derivable (and similarly for (|τ :: s|) = D and τ :: s D). There can be multiple derivations of τ :: c with different D s, in which case we pick an arbitrary canonical representative of the possible D s when defining (|τ :: c|). Although our system is coherent in the sense of [9], a proof is beyond the scope of this paper. For an example of the complete dictionary translation see Table 2. An interesting alternative to the classic dictionary translation formalized above is Wehr’s representation of dictionaries as ML modules [21]. This avoids polymorphic recursion which may otherwise arise in the translation (although this is rare in practice). Our intermediate language allows polymorphic recursion but the resulting ML code would be rejected by the compiler. 4.1
Correctness
Below we show that dictionary translation preserves reduction semantics. For reasons of space we do not argue preservation of well-typedness: in the worst case we end up with an ill-typed program that the target language compiler will reject. Well-typedness is frequently dealt with in the type class literature (e.g. [20]) and we concentrate on semantic arguments. First some preliminaries: Subclasses. We follow [15] and eliminate subclasses: classes no longer inherit and each occurrence of a class c in a type or term is replaced by the intersection c ∩ c1 ∩ · · · ∩ cn with all its (transitive) superclasses c1 , . . . , cn . To simplify the presentation below, we assume that subclassing has been eliminated. Constructor terms. We call a term r a constructor term if it only consists of fully applied constants introduced by data statements. Since data statements do not constrain the type variables (i.e. constrain them implicitly by the empty sort) we have (|f |) = f for all data constructors, and hence (|r |) = r.
Code Generation via Higher-Order Rewrite Systems
111
Table 2. Dictionary translation example (for succintness some type arguments [τ ] are not printed explicitly) statement
statement(s) with dictionaries
data N = Zero | Suc of N data Inf α = Fin of α | ∞ data List α = Nil | Cons of α (List α)
data N = Zero | Suc of N data Inf α = Fin of α | ∞ data List α = Nil | Cons of α (List α)
class monoid where pls :: ∀ α::monoid. α ⇒ α ⇒ α, zero :: ∀ α::monoid. α
data monoid α = Monoid of (α ⇒ α ⇒ α) α fun pls :: ∀ α. monoid α ⇒ α ⇒ α ⇒ α where pls (Monoid x y) = x fun zero :: ∀ α. monoid α ⇒ α where zero (Monoid x y) = y
fun plsN :: N ⇒ N ⇒ N where plsN Zero n = n | plsN (Suc m) n = Suc (plsN m n)
fun plsN :: N ⇒ N ⇒ N where plsN Zero n = n | plsN (Suc m) n = Suc (plsN m n)
fun plsInf :: ∀ α::monoid. Inf α ⇒ Inf α ⇒ Inf α where plsInf [α::monoid] (Fin a) (Fin b) = Fin (pls [α::monoid] a b) | plsInf [α::monoid] ∞ b = ∞ | plsInf [α::monoid] a ∞ = ∞
fun plsInf :: ∀ α. monoid α ⇒ Inf α ⇒ Inf α ⇒ Inf α where plsInf α (Fin a) (Fin b) = Fin (pls α a b) | plsInf α ∞ b = ∞ | plsInf α a ∞ = ∞
inst N :: monoid where pls [N] = plsN , zero [N] = Zero
fun monoidN :: monoid N monoidN = Monoid plsN Zero
fun monoidInf :: ∀ α. monoid α ⇒ inst Inf (α::monoid) :: monoid where pls [Inf (α::monoid)] = plsInf [α::monoid], monoid (Inf α) where monoidInf α = zero [Inf (α::monoid)] = Monoid (plsInf α) (Fin (zero α)) Fin (zero [α::monoid]) fun sum :: ∀ α::monoid. List α ⇒ α where fun sum :: ∀ α. monoid α ⇒ sum [α::monoid] Nil = zero [α::monoid] List α ⇒ α where | sum [α::monoid] (Cons x xs) = sum α Nil = zero α pls [α::monoid] x (sum [α::monoid] xs) | sum α (Cons x xs) = pls α x (sum α xs) fun example :: Inf N where example = sum [Inf N] (Cons (Fin Zero) (Cons ∞ Nil))
fun example :: Inf N where example = sum (monoidInf monoidN ) (Cons (Fin Zero) (Cons ∞ Nil))
112
F. Haftmann and T. Nipkow
Terms and substitutions. We make use of the notation C [t ] for terms where the context C is a term with a “hole” that is filled with a subterm t. Because (|·|) is a homomorphism on terms we have (|C [t ]|) = (|C |)[(|t |)]. Given a substitution σ we define (|σ|) to be the substitution σ such that σ (x ) = (|σ(x )|) for all x. By induction on term t we obtain (|σ(t )|) = (|σ|)((|t |)). Rewriting. An HRS E is a set of rewrite rules l = r where l and r are λ-terms of the same type. The rewrite relation E t −→ t holds iff t = C [σ(l )] and t = C [σ(r )] for suitable C, σ and l = r in E [13]. In the proof below we have to argue about the order in which equations are applied. These arguments become particularly transparent if we appeal to a well-known strategy, lazy evaluation as in Haskell. This is admissible for the following reasons. We focus our attention on the target languages SML, OCaml, Haskell. They impose a sequentialization of our rewrite systems at the end of the translation chain: overlapping equations are disambiguated by the order in which they occur. For example, f (T rue) = e1 , f (x) = e2 is equivalent to f (T rue) = e1 , f (F alse) = e2 in the target language. Thus we may as well assume that all function definitions in a program are non-overlapping to start with. Therefore the notion of lazy evaluation is well-defined, for example as given by the Haskell semantics. Now observe that in the theorem below we consider only reductions to normal forms. Hence Haskell subsumes SML or OCaml: if SML or OCaml evaluation finds a normal form, so does Haskell. In the following we are given a fixed program P and its dictionary translation PΔ . Let E and EΔ be the the set of equations contained in fun and inst statements of P and PΔ . We will now study the reduction behavior of E and EΔ , i.e. view them as HRSs. Theorem 1 (Correctness). If all functions in P are defined by non-overlapping sets of equations, t is well-typed w.r.t. P , and r is a constructor term, then E t −→∗ r iff EΔ (|t |) −→∗ r. Proof. We start by comparing the structure of equations in both systems: equations E
equations EΔ
f f [α::s] t = t f α (|t |) = (|t |) g g [κ β::s] = t
fΔ
cκ β = Δc . . . (|t |) . . . ΔI g (Δc x) = x ΔE
Throughout this proof f will always represent a constant introduced by a fun statement and g a class method. Equations in E can be partitioned into those defining f s and those defining gs. In EΔ , equations of kind fΔ correspond to equations of kind f ; equations of kind g have no direct counterpart, but are split into equations of kind ΔI producing a particular Δc and equations of kind ΔE consuming a particular Δc . Our proof will work in two steps: first, we establish an intermediate system which joins the ΔE / ΔI equations of EΔ ; then we show that this intermediate system behaves like E.
Code Generation via Higher-Order Rewrite Systems
113
Because r is a constructor term, it is in normal form. Hence we may restrict our attention to reductions following a lazy evaluation strategy. First we show that in a lazy reduction sequence EΔ (|t |) −→∗ r, each ΔI step is immediately followed by its corresponding ΔE step. We note that in (|t |), constants cκ can only occur in subterms of the form h . . . (cκ . . . ), where h is a constant, and that this is preserved in each reduction step: the right-hand side of each reduction rule is either a single variable of non-dictionary type (ΔE rules) or (|t |) (fΔ rules) or Δc (|t1 |) . . . (|tn |) (ΔI rules, remember we have no superclasses). Looking at the rules of EΔ we find that fΔ and ΔI rules do not require their dictionary arguments to be evaluated. Hence lazy evaluation will unfold f and cκ before unfolding their dictionary arguments. Finally we consider evaluation of a redex cκ . . . inside h . . . (cκ . . . ). As we just argued (by laziness) the h cannot be an f or another (not necessarily different) c κ . Hence it must be a g, whose only dictionary parameter is the cκ . . . . Thus we now have a new redex g (Δc . . . ) which lazy evaluation will reduce by the corresponding ΔE rule g (Δc x) = x. We have shown that lazy evaluation automatically ensures that ΔI and ΔE steps always occur pairwise. Thus it is legitimate to treat those pairs as fixed singleton steps. Let (|E |) (a suggestive name!) be the system which results from EΔ by merging the corresponding ΔI / ΔE equations into equations of a new kind gΔ (see below). By construction we have: (|E |) (|t |) −→∗ r iff EΔ (|t |) −→∗ r The relationship between E and (|E |) is very close and justifies the name (|E |) because we have (l = r ) ∈ E iff ((|l |) = (|r |)) ∈ (|E |): equations E
equations (|E |)
f f [α::s] t = t f α (|t |) = (|t |) fΔ g g [κ β::s] = t g (cκ β) = (|t |) gΔ The remainder of the proof shows E t −→n r iff (|E |) (|t |) −→n r by induction on n. The case n = 0 is trivial. The induction step works according to the following picture: E
u
(|E |) (|u|)
t
(|t |)
n
n
r
r
The right part (solid lines) is the induction hypothesis. For the induction step it remains to prove the following implications:
114
F. Haftmann and T. Nipkow
1. E u −→ t implies (|E |) (|u|) −→ (|t |) 2. (|E |) (|u|) −→ v implies ∃ t . (|t |) = v ∧ E u −→ t Proof of 1. The rewrite step E u −→ t takes place at a certain redex in u which is a substitution instance σ(l ) of the left-hand side l of an equation l = r in E. Hence u = C [σ(l )] and t = C [σ(r )]. Therefore (|u|) = (|C [σ(l )]|) = (|C |)[(|σ(l )|)] = (|C |)[(|σ|)((|l |))] and (|t |) = (|C [σ(r )]|) = (|C |)[(|σ(r )|)] = (|C |)[(|σ|)((|r |))]. Thus (|E |) (|u|) −→ (|t |) using equation (|l |) = (|r |) in (|E |). Proof of 2. The rewrite step (|E |) (|u|) −→ v implies (|u|) = C [σ ((|l |))] and v = C [σ ((|r |))] for suitable C , σ and (|l |) = (|r |) in (|E |). From C and σ we obtain C and σ by reconstructing type arguments from dictionaries. This reconstruction is the inverse of function (|τ :: s|). Essentially it turns cκ back into κ and αj into α. Then we have u = C [σ(l )], (|u|) = (|C |)[(|σ(l )|)] and v = (|C |)[(|σ(r )|)]. Defining t = C [σ(r )] we obtain the desired E u −→ t (using equation l = r in E ) and (|t |) = (|C [σ(r )]|) = (|C |)[(|σ(r )|)] = v. Although this proof restricts to non-overlapping equations, we believe that this theorem also holds without the restriction.
5
Program and Data Refinement
Program refinement is the replacement of less efficient algorithms and data structures by more efficient ones. We show how the code generator supports both activities with surprising ease because we can generate code from arbitrary equational theorems, not just definitions. Replacing one algorithm by another is in fact trivial. For example, implementing the standard recursive definition of list reversal rev (which takes quadratic time and space) by a linear, tail recursive one itrev of type α list ⇒ α list ⇒ α list simply requires a proof of the lemma rev xs = itrev xs []. Notifying the code generator of this lemma (which needs to be done explicitly) has the effect that from then on (for code generation) the original equations for rev are dropped and rev xs = itrev xs [] is used instead. More interesting is a change of data structures, also known as data refinement [8]. The key is the insight that data statements of our intermediate language do not contribute to a program’s equational semantics, by definition. Hence we can replace one datatype by another as long as we can still express our functions by pattern matching over the new rather than the old type. Our approach to data refinement is best explained by an example. The queues presented in 2 are the natural abstract specifications that one can reason about in a straightforward manner. However, the generated code is suboptimal; a more efficient implementation would use amortized queues [17], which are pairs of lists. The queue corresponding to such a pair is obtained by reversing the first list and appending it to the second:
Code Generation via Higher-Order Rewrite Systems
115
definition AQueue :: α list ⇒ α list ⇒ α queue where AQueue xs ys = Queue (ys @ rev xs) This is a classic case of data refinement and AQueue is the abstraction function: Queue [a, b, c]
representation
[c, b], [a]
α queue
e ueu AQ
AQ ueu e
abstraction
[c], [a, b]
α list, α list
For the primitive queue operations we can now prove alternative equations which perform pattern matching on AQueue rather than Queue: empty = AQueue [] [] enqueue x (AQueue xs ys) = AQueue (x # xs) ys dequeue (AQueue xs []) = (if null xs then (None, AQueue [] []) else dequeue (AQueue [] (rev xs))) dequeue (AQueue xs (y # ys)) = (Some y, AQueue xs ys) We instruct the code generator to view AQueue as a constructor. Now it produces the following Haskell program: data Queue a = AQueue [a] [a]; empty :: forall a. Queue a; empty = AQueue [] []; dequeue :: forall a. Queue a -> (Maybe a, Queue a); dequeue (AQueue xs (y : ys)) = (Just y, AQueue xs ys); dequeue (AQueue xs []) = (if null xs then (Nothing, AQueue [] []) else dequeue (AQueue [] (reverse xs))); enqueue :: forall a. a -> Queue a -> Queue a; enqueue x (AQueue xs ys) = AQueue (x : xs) ys;
Clients of the abstract type α queue can continue to use the primitive operations empty, enqueue and dequeue and reason in terms of the abstract constructor Queue. Upon code generation, the primitive operations will now be implemented in terms of the concrete constructor AQueue. If a client has broken the abstraction and has used Queue for pattern matching in some function f, code generation for f will fail because Queue is no longer a constructor. Isabelle already objects, but even if it did not, Haskell would. For example, code generation for this perfectly good function definition fails:
116
F. Haftmann and T. Nipkow
fun peek :: α queue ⇒ α option where peek (Queue []) = None | peek (Queue (x # xs)) = Some x Of course we can view peek as another primitive operation on queues and prove the following executable equation in terms of AQueue: lemma peek-AQueue [code]: peek (AQueue xs ys) = (if null ys then (if null xs then None else Some (last xs)) else Some (hd ys)) A considerably larger example are Lochbihler’s finite functions and their refinement to executable code [12]. Related work. ACL2 allows replacement of subterms at code generation time with other provably equal subterms [5]. Coq also allows replacement of one function by another at code generation time but this is completely unchecked. Neither system supports data refinement in the way we showed in our queue example.
6
Conclusion
We have presented the essentials behind Isabelle/HOL’s code generator: it transforms a system of equations into a program in an intermediate language capturing the essence of functional programming languages. Type classes are supported and we proved that dictionary translation preserves their semantics. Program development in the form of algorithm and data refinement is supported by the underlying equational logic. Recently the scope of the code generator has been extended towards logic programming [1]. Inductive predicates are translated to recursive functions and the equivalence is proved automatically within HOL. The code generator itself is left untouched. Acknowledgement. We sincerely thank Alex Krauss and the referees for their many comments and suggestions.
References 1. Berghofer, S., Bulwahn, L., Haftmann, F.: Turning inductive into equational specifications. In: Urban, C. (ed.) TPHOLs 2009. LNCS, vol. 5674, pp. 131–146. Springer, Heidelberg (2009) 2. Berghofer, S., Nipkow, T.: Executing higher order logic. In: Callaghan, P., Luo, Z., McKinna, J., Pollack, R. (eds.) TYPES 2000. LNCS, vol. 2277, pp. 24–40. Springer, Heidelberg (2002)
Code Generation via Higher-Order Rewrite Systems
117
3. Berghofer, S., Nipkow, T.: Random testing in Isabelle/HOL. In: Second International Conference on SEFM 2004: Proc. of the Software Engineering and Formal Methods. IEEE Computer Society, Los Alamitos (2004) 4. Crow, J., Owre, S., Rushby, J., Shankar, N., Stringer-Calvert, D.: Evaluating, testing, and animating PVS specifications. Tech. rep., Computer Science Laboratory, SRI International (2001) 5. Greve, D.A., Kaufmann, M., Manolios, P., Moore, J.S., Ray, S., Ruiz-Reina, J.L., Sumners, R., Vroon, D., Wilding, M.: Efficient execution in an automated reasoning environment. Journal of Functional Programming 18(1), 15–46 (2007) 6. Haftmann, F.: Code generation from specifications in higher order logic. Ph.D. thesis, Technische Universit¨ at M¨ unchen (2009) 7. Hall, C., Hammond, K., Peyton Jones, S., Wadler, P.: Type classes in Haskell. ACM Transactions on Programming Languages and Systems 18(2) (1996) 8. Jones, C.B.: Systematic Software Development using VDM, 2nd edn. Prentice Hall International, Englewood Cliffs (1990) 9. Jones, M.P.: Qualified types: Theory and practice. Ph.D. thesis, University of Nottingham (1994) 10. Letouzey, P.: Programmation fonctionnelle certifi´ee – l’extraction de programmes dans l’assistant Coq. Ph.D. thesis, Universit´e Paris-Sud (2004) 11. Letouzey, P.: Coq Extraction, an Overview. In: Beckmann, A., Dimitracopoulos, C., L¨ owe, B. (eds.) CiE 2008. LNCS, vol. 5028, pp. 359–369. Springer, Heidelberg (2008) 12. Lochbihler, A.: Formalising FinFuns - generating code for functions as data from Isabelle/HOL. In: Urban, C. (ed.) TPHOLs 2009. LNCS, vol. 5674, pp. 310–326. Springer, Heidelberg (2009) 13. Mayr, R., Nipkow, T.: Higher-order rewrite systems and their confluence. Theor. Comput. Sci. 192, 3–29 (1998) 14. Nipkow, T., Paulson, L.C., Wenzel, M.: Isabelle/HOL. LNCS, vol. 2283. Springer, Heidelberg (2002) 15. Nipkow, T., Prehofer, C.: Type checking type classes. In: Proc. 20th ACM Symp. Principles of Programming Languages. ACM Press, New York (1993) 16. Nipkow, T., Prehofer, C.: Type reconstruction for type classes. J. Functional Programming 5(2), 201–224 (1995) 17. Okasaki, C.: Catenable double-ended queues. In: Proc. Int. Conf. Functional Programming (ICFP 1997). ACM Press, New York (1997) 18. Schmidt-Schauß, M.: Computational aspects of an order-sorted logic with term declarations. LNAI 395. Springer (1989) 19. Thiemann, R., Sternagel, C.: Certification of termination proofs using CeTA. In: Urban, C. (ed.) TPHOLs 2009. LNCS, vol. 5674, pp. 452–468. Springer, Heidelberg (2009) 20. Wehr, S.: ML modules and Haskell type classes: A constructive comparison. Master’s thesis, Albert-Ludwigs-Universit¨ at, Freiburg (2005) 21. Wehr, S., Chakravarty, M.M.T.: ML modules and Haskell type classes: A constructive comparison. In: Ramalingam, G. (ed.) APLAS 2008. LNCS, vol. 5356, pp. 188–204. Springer, Heidelberg (2008)
A Complete Axiomatization of Strict Equality ´ Javier Alvez and Francisco J. L´ opez-Fraguas Universidad Complutense de Madrid
[email protected],
[email protected] Universidad del Pa´ıs Vasco
[email protected]
Abstract. Computing with data values that are some kind of trees — finite, infinite, rational— is at the core of declarative programming, either logic, functional, or functional-logic. Understanding the logic of trees is therefore a fundamental question with impact in different aspects, like language design, including constraint systems or constructive negation, or obtaining methods for verifying and reasoning about programs. The theory of true equality over finite or infinite trees is quite well-known. In particular, a seminal paper by Maher proved its decidability and gave a complete axiomatization of the theory. However, the sensible notion of equality for functional and functional-logic languages with a lazy evaluation regime is strict equality, a computable approximation of true equality for possibly infinite and partial trees. In this paper, we investigate the first-order theory of strict equality, arriving to remarkable and not obvious results: the theory is again decidable and admits a complete axiomatization, not requiring predicate symbols other than strict equality itself. Besides, the results stem from an effective —taking into account the intrinsic complexity of the problem— decision procedure that can be seen as a constraint solver for general strict equality constraints. As a side product of our results, we obtain that the theories of strict equality over finite and infinite partial trees, respectively, are elementarily equivalent.
1
Introduction
Computing with data values that are —or can be interpreted as— some kind of trees is at the core of declarative programming, either logic, functional or functional-logic programming. The family of trees may vary from finite trees, for the case of standard logic programming, infinite rational trees, for the case of Prolog-II and variants, or infinite trees (that correspond to data values in constructor data-types) for the case of functional or functional-logic programming that allow non-terminating programs by following a lazy evaluation regime. Understanding trees, in particular the logical principles governing tree equality, is a fundamental question with impact in different aspects of declarative
This work has been partially supported by the Spanish projects TIN2008-06622-C03-01, S-0505/TIC/0407, S2009TIC-1465, UCM-BSCH-GR58/ 08-910502, TIN2007-66523 and GIU07/35.
M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 118–133, 2010. c Springer-Verlag Berlin Heidelberg 2010
A Complete Axiomatization of Strict Equality
119
programming languages. For instance, handling constructive negation in a logic language requires solving complex Herbrand constraints over finite trees. The theory of true equality ≈ over finite or infinite trees is quite well known (by true equality we mean t1 ≈ t2 iff t1 and t2 are the same tree). In a seminal paper [14], Maher proved its decidability and gave a complete axiomatization for the cases of finite and infinite trees, and finite and infinite signatures. In another influential paper [5], the authors provided more effective decision procedures, based on reduction to solved forms by quantifier elimination. In functional or functional-logic languages like Haskell, Curry or Toy [15,8,11], the universe of interest is that of (possibly) infinite partial trees, because nonterminating programs handled with lazy evaluation lead to that kind of trees as denotations of expressions. Partial means that some of the tree components may be undefined. For instance, with the definitions loop = loop and repeat(x) = [x|repeat(x)], repeat(0) denotes the infinite list [0, 0, 0, . . .], repeat(loop) denotes the infinite partial list [⊥, ⊥, ⊥, . . .], and [0|loop] denotes the finite partial list [0|⊥] (⊥ represents the undefined value). In those languages, true equality is not the sensible notion to consider, because true equality over partial trees is not Scott continuous (hence not computable). It is then replaced (see e.g. [8]) by strict equality ==, defined as the restriction of ≈ to finite and total trees. It is easy to prove [9] that == is the largest continuous approximation to ≈. The theories of equality and of strict equality for infinite partial trees are far from being the same: for instance, the formula ∀x x ≈ x hold (≈ is reflexive) while ∀x x == x does not (⊥ == ⊥ is not true); similarly ∃x x ≈ s(x) holds (there is exactly one infinite regular tree verifying x ≈ s(x)), while ∃x x == s(x) does not. As far as we know, a comprehensive study of the full first-order theory of strict equality has not been done before. Certainly, strict equality is incorporated as primitive in the aforementioned languages, and there are several works incorporating various Herbrand constraint systems —and corresponding solving procedures— to functional-logic languages [2,10,3]. But in all cases, the considered class of formulae over == is only a subset of general first-order formulae. Moreover, the works that study true equality cannot be easily extended to handle strict equality. For example, in [7] the authors propose to extend the theory of true equality with the predicate f inite/1 , that only holds for finite trees. Coping with strict equality would require an additional predicate total/1 (to characterize those trees not having ⊥ as component). Comparing that hypothetical approach with our proposal of directly considering the first-order theory of ==, we see several disadvantages: first, the axiomatization of ≈ +f inite+ total would be larger than ours, leading also to larger proofs of decidability and completeness; second, the theory would be less directly connected to the mentioned languages (Haskell, Curry, Toy) because programs in those languages use == but not f inite or total. Our aim is precisely to investigate the full first-order theory of strict equality over the algebra IT of possibly infinite partial trees. Note that decidability and existence of complete axiomatization for ≈ says nothing about the same problems for ==, even although == is a strict subset of ≈ (i.e., ∀x∀y ( x == y → x ≈ y ) is valid in IT ). These are indeed the main questions tackled in this paper:
120
´ J. Alvez and F.J. L´ opez-Fraguas
– Does the theory of == over IT admit a complete recursive axiomatization? – In the affirmative case, is it possible that the axioms use only the symbol ==? We cannot discard a priori the possibility of explicitly connecting == to ≈, although the resulting set of axioms and transformations rules would be more complicated since we would need to add connection axioms (like the formula stated above) to the axiomatization of ≈ in [14]. – A complete recursive axiomatization of a theory implies its decidability (at least a brute force decision procedure exists). Can we give a more practical decision procedure, in the style of [5]? As a matter of fact, such a procedure —if existing— will be itself a proof of completeness for the theory. We obtain affirmative answers to all these questions, both in the cases of infinite and finite signatures. Our paper does not look for immediate applications, keeping in a theoretical realm and trying to achieve fundamental and not obvious results about strict equality that could be a basis for potential applications: the design of constraint systems more expressive than existing ones or the development of reasoning frameworks for functional-logic programs with built-in equality. Outline of the paper. In the next section, we provide preliminary definitions and notation. In Section 3, we give an axiomatization for strict equality. Next, in Section 4, we first introduce some transformation rules and then provide decision methods for strict equality, distinguishing the cases of infinite and finite signatures. Finally, in Section 5, we discuss complexity issues and future work. For the sake of space, most of the proofs have been omitted or sketched; more detailed proofs can be found in [1].
2
Preliminaries
Let V be a countable set of variables and Σ = FΣ ∪{==} a signature of function symbols f ∈ FΣ , each with associated arity n (written as s/n ), and the strict equality predicate symbol ==/2 . For technical convenience, we assume that FΣ contains at least a 0-ary function symbol (constant), an n-ary function symbol with n > 0 and a distinguished 0-ary function symbol ⊥ known as bottom. If Σ contains a finite number of function symbols, then Σ is said to be finite. Otherwise, Σ is infinite. By using the name function, we follow the tradition of first-order logic, but note that the notion of function corresponds to the notion of free constructor in functional/functional-logic programming and not to userdefined functions, which play no role in this paper. We consider the classical definitions of finite and infinite ground trees. The interested reader is referred to [4] for an exhaustive definition. A tree is said to be partial if it contains ⊥ at some node. Otherwise, the tree is total. The algebra of finite and infinite trees are denoted by F T and IT , respectively. Besides, we also refer to [5] for the definitions that do not appear in this paper. A term (or constructor term) is either a variable v ∈ V or an expression f (t1 , . . . , tn ) where f/n ∈ FΣ and t1 , . . . , tn are terms. An expression of the form
A Complete Axiomatization of Strict Equality
121
t[s] denotes a term in which s occurs as subterm. For any n > 0, an n-tuple of terms is denoted by t1 , . . . , tn and abbreviated by t. When convenient, we also treat t as the set of its components. As for the case of trees, a term t is said to be partial if t = s[⊥], and t is total otherwise. We denote by Var(t) the set of variables occurring in t. If Var(t) = ∅, t is said to be ground. The size of a term t is the number of occurrences of function symbols in t. A sentence (or constraint ) φ is an arbitrary first-order formula built with Σ. In our case, the only predicate symbol is ==. Thus, atomic formulas are t rue, f alse, strict equations t1 == t2 or negated equations ¬t1 == t2 . If r = r1 , . . . , rn and s = s1 , . . . , sn , then r == s (resp. ¬r == s) abbreviates r1 == s1 ∧ . . . ∧ rn == sn (resp. ¬r1 == s1 ∨ . . . ∨ ¬rn == sn ). Sentences may use propositional connectives (¬, ∧, ∨, →, ↔) and quantifiers (∃, ∀). Q stands for both kinds of quantifiers. Free(φ) denotes the set of free variables of φ. If Free(φ) = ∅, then φ is closed. φQ denotes the Q-closure of φ and φQ\w denotes Qv φ, where v = Free(φ) \ w. Now we recall some semantics of first-order logic. An interpretation A is a carrier set A = ∅ together with interpretations f M , pM for the symbols in Σ. Given A, an assignment σ maps variables to values in A; if φσ is t rue (according to standard rules for truth-valuation) in A, we say that σ is a solution (in A) of φ. A models φ, written A |= φ, if all assignments are solutions in A of φ. Notice that, for given A, φ and σ, σ must be a solution in A of either φ or ¬φ; moreover, if φ is closed, either A |= φ or A |= ¬φ (the latter being equivalent to A |= φ). A theory T is a set of closed sentences. A is a model of T , written A |= T , if A |= φ for each φ ∈ T . A formula φ is a logical consequence of T , written T |= φ, if A |= φ whenever A |= T . This notation extends naturally to sets Φ of formulas. A sentence φ is satisfiable (or solvable) in T , if T |= φ∃ . Two sentences φ1 and φ2 are (logically) equivalent in T , denoted by φ1 ≡ φ2 , if T |= φ1 ↔ φ2 . A theory T is complete iff for any closed sentence φ either T |= φ or T |= ¬φ holds. The theory TA of A is the set of all closed φ such that A |= φ. Note that TA is always complete. A1 and A2 are elementarily equivalent if TA1 = TA2 . A complete axiomatization of A is a theory S ⊆ TA such that S |= TA (or, equivalently, S is a complete theory and A |= S). Usually one is interested in recursive axiomatizations where the property ‘φ ∈ S’ is decidable. Given two sentences φ1 and φ2 , a transformation rule φ1 → φ2 replaces any occurrence of φ1 in a formula (modulo variable renaming) with φ2 . The application of a transformation rule R to φ1 yielding φ2 is denoted by φ1 ;R φ2 . A transformation rule R is said to be correct in a theory T iff for any two formulas φ1 and φ2 such that φ1 ;R φ2 we have that φ1 ≡ φ2 .
3
An Axiomatization of Strict Equality
Strict equality is a particular case of classical equality where, besides being syntactically equal, two terms have to be finite and total to be strictly equal.
122
´ J. Alvez and F.J. L´ opez-Fraguas
(A1 ) For every f/n ∈ FΣ such that f =⊥ ∀x ∀y ( f (x) == f (y) ↔ x == y ) (A2 ) For every f/n , g/m ∈ FΣ such that f =g ∀x ∀y ¬f (x) == g(y) (A3 ) For every term t[x] except x such that y = Var(t[x]) \ {x} ∀x ∀y ¬x == t[x] (A4 ) Bottom: (A5 ) Symmetry:
∀x ¬x == ⊥ ∀x ∀y ( x == y → y == x )
(A6 ) Transitivity: ∀x ∀y ∀z ( [ x == y ∧ y == z ] → x == z ) (A7 ) Domain Closure Axiom or DCA: % only for finite signatures ∀x ( x == x → f/n ∈FΣ ∃w x == f (w) ) Fig. 1. Axiomatization of Infinite Trees with Strict Equality
Definition 1 (Strict equality). Two trees t1 and t2 are strictly equal, denoted by t1 == t2 , iff t1 and t2 are the same finite and total tree. Strict equality allows us to characterize the subset of IT consisting of finite and total trees: x is a finite and total tree ⇐⇒ x == x. In Figure 1, we propose an axiomatization of infinite trees with strict equality, which is similar, but not equal, to the one of finite trees with equality given in [14]. The main difference comes from the fact that strict equality is not reflexive: because of A3 and A4 , non-finite/non-total trees are not strictly equal to themselves. Due to this property, ⊥ and the remaining functions in FΣ have a different treatment. We distinguish two cases, depending on whether Σ is either finite or infinite. In the case of infinite signatures, the axiomatization of strict equality over IT consists of A1 − A6 1 and is denoted by Einf . For finite signatures, the axiomatization also includes A7 and is denoted by Ef in . Axiom A7 is an adaptation of the Domain Closure Axiom introduced in [16] to the case of ==, which prevents the existence of isolated finite and total trees in the algebra. Note that A7 does not provide any information about non-finite/non-total trees. To simplify statements and reasonings, we will frequently use E to refer indistinctly to Einf and Ef in for the respective cases of infinite and finite signatures. We will also abuse of notation IT to refer either to the set of infinite trees or the interpretation with IT as carrier and symbols in Σ interpreted in the natural way (symbols in FΣ as free constructors and == as strict equality). 1
To be more precise, A1 − A3 are axiom schemes where A3 embodies an infinite number of instances (also A1 and A2 in the case of infinite signatures). To simplify notation, A4 − A7 can be also taken as axiom schemes with a single instance.
A Complete Axiomatization of Strict Equality
123
Three basic questions about E arise: Are the axioms of E correct for IT ? Are there enough axioms as to characterize ==? Are there too many? The first and third questions are addressed in the next proposition. The second one concerns completeness of E, and is far from being a trivial question. It will be proved by means of a decision procedure based on some equivalences under E used as transformation rules for quantifier elimination. Theorem 1 (Correctness and minimality of E). (i) IT |= E. (ii) E \ Ai is not a complete theory for any (axiom scheme) Ai in E.
Remark. IT |= E is proved by direct inspection. In particular, A3 is correct since, by definition, infinite trees are not strictly equal. Regarding minimality, we cannot replace (ii) by the stronger result “no stricter subset of E is a complete theory”. The reason is that some instances of A3 can be skipped from E without losing completeness. For example, as discussed also in [13] for true equality, the formula ∀x ¬x == f (x) follows from ∀x ¬x == f (f (x)) (and A1 , A6 ). Finally, we show that == satisfies the following weak version of reflexivity. Proposition 1.
4
E |= ∀x ( x == x ↔ ∃y x == y )
A Decision Method for Strict Equality
In this section, we prove that the theory of strict equality is decidable by providing an algorithm that transforms any initial constraint into an equivalent disjunction of formulas in solved form, i.e., in a distinguished simplified syntactic form that ensures satisfiability of the formula. This algorithm is based on the well-known technique of quantifier elimination, as the algorithms proposed in [5,14] for the equality theory. As in the above cited works, we distinguish two cases depending on whether the signature is finite or infinite. In the next subsections, we first provide a decision algorithm for the case of infinite signatures and then adapt that algorithm for finite ones. Those decision methods use the transformation rules introduced in Figure 2. Note that some conditions in rules, like those of R, are not necessary for correctness. Instead, they serve to discard the application of some rules when there exist more suitable ones. Some other basic transformations that are trivially correct in first-order logic, such as De Morgan’s laws or double negation elimination, are also implicitly used in the decision methods. Next, we state that the transformation rules in Figure 2 are correct. Theorem 2. The transformation rules in Figure 2 are correct in E.
´ J. Alvez and F.J. L´ opez-Fraguas
124
Bottom
(B1 ) x == t[⊥] →
f alse (B2 ) ¬x == t[⊥]
→ true
Non-finite trees (NFT1 ) ¬x == x ∧ ¬r == s[x] →
¬x == x (NFT2 ) ¬x == x ∧ r == s[x]
→ f alse (NFT3 ) ∀y ¬x == y → ¬x == x
Finite trees (FT) x == x ∧ r == s[x]
→ r == s[x] Decomposition (D1 ) f (r1 , . . . , rn ) == f (s1 , . . . , sn ) →
r1 == s1 ∧ . . . ∧ rn == sn (D2 ) ¬f (r1 , . . . , rn ) == f (s1 , . . . , sn )
→ ¬r1 == s1 ∨ . . . ∨ ¬rn == sn Clash
(C1 ) f (r1 , . . . , rm ) == g(s1 , . . . , sn ) →
f alse if f = g (C2 ) ¬f (r1 , . . . , rm ) == g(s1 , . . . , sn )
→ true if f =g
Occur-check
(O1 ) x == t[x] →
f alse if x = t[x] (O2 ) ¬x == t[x]
→ true if x = t[x]
Replacement (R) x == t ∧ ϕ[x]
→ x == t ∧ ϕ[x ← t] if t is total and x ∈ Var(t) Existential quantification elimination (EE1 ) ∃w ( w == w ∧ ϕ ) →
ϕ if w ∈ Var(ϕ) (EE2 ) ∃w ( w == t ∧ ϕ )
→ x == x ∧ ϕ if t is total, x = Var(t) and w ∈ Var(t) ∪ Var(ϕ) (EE3 ) ∃w ( ¬w == w ∧ ϕ )
→ ϕ if w ∈ Var(ϕ) Existential quantification introduction (EI) r == s[x]
→ ∃w ( x == w ∧ r == s[x ← w] ) Universal quantification elimination (UE) ∀y (¬y == t ∨ ϕ)
→ ¬x == x ∨ ϕ[y ← t] if t is total, x = Var(t) and y ∈ Var(t) Tautology (T) ϕ
→ ϕ ∧ ( x == x ∨ ¬x == x ) Split (S) ¬∃w∃z ( x == t[w] ∧ ϕ[w · z] )
→ ¬∃w ( x == t[w] ) ∨ ∃w ( x == t[w] ∧ ¬∃z ϕ[w · z] ) Fig. 2. Transformation Rules
A Complete Axiomatization of Strict Equality
4.1
125
Infinite Signatures
In order to provide a decision algorithm, we first introduce the class of basic formulas, which for the case of infinite signatures are formulas in solved form. Then, we show that conjunction and negation can be performed on basic formulas. And, finally, we describe the decision algorithm. Definition 2. A basic formula for the variables x is either t rue, f alse (closed basic formulas) or a constraint ∃w c(x, w) such that c(x, w) =
x1 ∈x1
¬x1 == x1 ∧ x2 == t ∧
ni
(¬wi == sij )∀\w
wi ∈w j=1
where − x = x1 ∪ x2 and x1 ∩ x2 = ∅, − w = Var(t) and x ∩ w = ∅, − if sij is a variable, then sij ∈ w, otherwise sij is total, wi ∈ Var(sij ) and Var(sij ) ∩ x = ∅ for every wi ∈ w and 1 ≤ j ≤ ni . A formula is in basic normal form (or BNF) if it is of the form Qy ϕ[x · y] where ϕ is a disjunction of basic formulas for x · y. Example 1. Let {a/0 , g/1 , f/2 } ⊂ FΣ and x = {x1 , x2 , x3 } ⊂ V. The sentences ∃w1 ∃w2 ( ¬x1 == x1 ∧ x2 = g(w1 ) ∧ x3 == g(w2 ) ∧ ¬w1 == w2 ∧ ∀v ¬w2 == f (a, v) ), ( ¬x1 == x1 ∧ ¬x2 == x2 ∧ ¬x3 == x3 ) and t rue are basic formulas for x. First, we will show that basic formulas are in solved form. Theorem 3. Any basic formula different from f alse is satisfiable in Einf .
Second, we describe the transformation of any universally quantified disjunction of negated equations into an equivalent disjunction of basic formulas. Proposition 2. Any universally quantified disjunction of negated equations ∀v ( ¬w1 == t1 ∨ ¬w2 == t2 ∨ . . . ∨ ¬wn == tn ) where wi ∈ v for each 1 ≤ i ≤ n can be transformed into an equivalent disjunction of basic formulas for the variables x = Var(t1 , . . . , tn ) \ v. Then, we describe the basic Boolean operations on basic formulas. Proposition 3. A conjunction of disjunctions of basic formulas for x can be transformed into an equivalent disjunction of basic formulas for x. In the next example, we show the transformation of a conjunction of two basic formulas into an equivalent disjunction of basic formulas for the same variables.
126
´ J. Alvez and F.J. L´ opez-Fraguas
Example 2. Let {a/0 , g/1 , f/2 } ⊂ FΣ and x = {x1 , x2 , x3 } ⊂ V. The conjunction of basic formulas for x ϕ1 = ∃w1 c1 (x, w1 ) ∧ ∃w2 c2 (x, w2 ) where c1 (x, w1 ) = ¬x1 == x1 ∧ x2 == w11 ∧ x3 == g(w21 ) ∧ ∀v ¬w11 == f (a, v) c2 (x, w2 ) = ¬x1 == x1 ∧ ¬x2 == x2 ∧ x3 == f (w12 , w22 ) ∧ ¬w12 == w22 is unsatisfiable since x2 == w11 ∧ ¬x2 == x2 is reduced to f alse by NFT2 . On the contrary, the conjunction ϕ2 = ∃w 1 c1 (x, w 1 ) ∧ ∃w 3 c3 (x, w 3 ) where c3 (x, w3 ) = ¬x1 == x1 ∧ x2 == f (w13 , w23 ) ∧ x3 == w13 ∧ ∀v ¬w23 == g(v) is transformed in the following way. Since σ = mgu( w11 , g(w21 ) , f (w13 , w23 ), w13 ) = {w11 ← f (g(w21 ), w23 ), w13 ← g(w21 )}, ϕ2 is transformed into ∃w21 ∃w23 ( ¬x1 == x1 ∧ x2 == f (g(w21 ), w23 ) ∧ x3 == g(w21 ) ∧ ∀v ¬f (g(w21 ), w23 ) == f (a, v) ∧ ∀v ¬w23 == g(v) ). Then, the negated equation ∀v ¬f (g(w21 ), w23 ) == f (a, v) is reduced to t rue using rules D2 and C2 . Thus, the resulting basic formula is ∃w21 ∃w23 (¬x1 == x1 ∧ x2 == f (g(w21 ), w23 ) ∧ x3 == g(w21 ) ∧ ∀v ¬w23 == g(v)). Proposition 4. A negated disjunction of basic formulas for the variables x can be transformed into an equivalent disjunction of basic formulas for x. Example 3. Let {a/0 , f/2 } ⊂ FΣ and x = {x1 , x2 } ⊂ V. The negated basic formula for the variables x ϕ = ¬∃w ( ¬x1 == x1 ∧ x2 == f (w, a) ∧ ∀v ¬w == f (a, v) ) is transformed as follows. First, ϕ is trivially equivalent to ( x1 == x1 ) ∨ ¬∃w ( x2 == f (w, a) ∧ ∀v ¬w == f (a, v) ), where ( x1 == x1 ) is transformed into the following basic formulas for x ∃w1 ( ¬x2 == x2 ∧ x1 == w1 ) ∨ ∃w2 · w3 ( x1 == w2 ∧ x2 == w3 ) using T, EI, R and FT. Further, the remaining subformula is transformed into ¬∃w ( x2 == f (w, a) ) ∨ ∃w ( x2 == f (w, a) ∧ ¬∀v ¬w == f (a, v) ) using the rule S. The constraint ¬∃w ( x2 == f (w, a) ) is transformed into ( ¬x1 == x1 ∧ ¬x2 == x2 ) ∨ ∃w4 ( ¬x2 == x2 ∧ x1 == w4 ) ∨ ∃w5 ( ¬x1 == x1 ∧ x2 == w5 ∧ ∀v ¬w5 == f (v, a) ) ∨ ∃w6 ∃w7 ( x1 == w6 ∧ x2 == w7 ∧ ∀v ¬w7 == f (v, a) ) using T, EI, R and NFT1 . Finally, ∃w ( x2 == f (w, a)∧¬∀v ¬w == f (a, v) ) ≡ ∃w ( x2 == f (w, a) ∧ ∃v w == f (a, v) ) is transformed into ∃w8 ( ¬x1 == x1 ∧ x2 == f (f (a, w8 ), a) ) ∨ ∃w9 ∃w10 ( x1 == w9 ∧ x2 == f (f (a, w10 ), a) ) using rules R, EE2 and FT on w, and T and EI on x1 .
A Complete Axiomatization of Strict Equality
(EE4 ) ∃w ( v == v ∧
n
127
(¬si == ti )∀\w ∧ ϕ )
→ ϕ
i=1
if w ∩ Var(ϕ) = ∅, v ⊆ w, si = ti , w ∩ Var(si , ti ) = ∅ and either si (resp. ti ) is not a variable or si ∈ w (resp. ti ∈ w) for each 1 ≤ i ≤ n Fig. 3. Existential Quantification Elimination: Infinite Signatures
Next, we show that the elimination of the innermost block of quantifiers is correct in Einf when it is existential. For this purpose, we introduce the transformation rule EE4 (see Figure 3), which allows to eliminate existential variables only occurring in a conjunction of (universally quantified) negated equations. Proposition 5. The transformation rule EE4 is correct in Einf .
Finally, the elimination of the innermost block of existential quantifiers is used in the decision algorithm given in Figure 4. Theorem 4. Let ∃w a(x · y, w · z) be a basic formula for x · y of the form ∃w∃z ( ¬x1 == x1 ∧ ¬y1 == y1 ∧ x2 == t ∧ y 2 == r ∧ ϕ ∧ ψ ) x1 ∈x1
y1 ∈y 1
where − w = Var(t) and z = Var(r) \ w, − ϕ is a finite conjunction of negated equations such that Free(ϕ) ⊆ w, n − ψ = i=1 (¬vi == si )∀\w·z and (vi ∪ Var(si )) ∩ z = ∅ for 1 ≤ i ≤ n. The formulas ∃y [ ∃w a(x · y, w · z) ] and ∃w ( x1 ∈x1 ¬x1 == x1 ∧ x2 == t ∧ ϕ ) are equivalent in Einf . Proof. It follows from rules EE1 , EE2 , EE3 and EE4 .
Example 4. Let {a/0 , g/1 , f/2 } ⊂ FΣ . The formulas ∃y [ ∃w1 ∃w2 ( x == g(w1 ) ∧ y == f (w2 , a) ∧ ¬w1 == a ∧ ¬w1 == w2 ∧ ∀v ¬w2 == f (a, v) ) ] and ∃w1 ( x == g(w1 ) ∧ ¬w1 == a ) are equivalent in Einf .
The algorithm described in Figure 4 is illustrated in the next example. Roughly speaking, we first transform the input constraint ϕ into an equivalent formula in basic normal form. Then, we proceed to iteratively eliminate the innermost block of quantifiers Qi xi . By Theorem 4, the elimination of Qi xi is trivial when Qi is existential. However, when Qi is universal, we have to use double negation to turn Qi into existential. This process requires to negate the matrix of the formula and to transform it into an equivalent disjunction of basic formulas for the same
128
´ J. Alvez and F.J. L´ opez-Fraguas
Given any constraint ϕ0 with free variables x0 : (Step 1) Transform ϕ0 into a prenex DNF formula ϕ1 = Q1 x1 . . . Qn xn m i=1 ψi (Step 2) For each 1 ≤ i ≤ m, transform ψi into a disjunction of basic formulas for the variables x = x0 · x1 · . . . · xn as follows: (a) Apply rules B1 , B2 , NFT1 , NFT2 , NFT3 , FT, D1 , D2 , C1 , C2 , O1 and O2 . When none of the previousrules applies, it remains a disjunction o2 o1 of constraints of the form ψi = v == r ∧ j j j=1 j=o1 +1 ¬vj == rj where vi is a variable, rj is total and vj ∈ Var(rj ) for each 1 ≤ j ≤ o2 . (b) For each conjunct ψi that results from (a) and each variable x ∈ x: • If x = vj for some 1 ≤ j ≤ o1 , then apply R on x. • If x = vk for every 1 ≤ j ≤ o1 and x ∈ Var(rk ) for some 1 ≤ k ≤ o1 , then apply EI and R on x. • Otherwise, apply T on x and goto (a). i i The resulting formula ϕ2 = Q1 x1 . . . Qn xn m i=1 ∃w ai (x, w ) is in BNF (Step 3) Iteratively eliminate the innermost block of consecutive existential/universal quantifiers Qn xn in ϕ2 : (i) If Qn = ∃, by Theorem 4 (Theorem 7 for finite signatures) the formula i i ϕ2 is equivalent to Q1 x1 . . . Qn−1 xn−1 m i=1 ∃w ai (x , w ) (ii) If Qn = ∀, then apply (i) using double negation as follows i i Q1 x1 . . . ¬∃xn ¬ m i=1 ∃w ai (x, w ). Negation on basic formulas is therefore used before and after applying (i). Fig. 4. A Decision Method for Strict Equality
variables before and after the elimination of Qi . In both cases, the length of the block of consecutive quantifiers strictly decreases at each elimination step because no new variable is introduced in the prefix. Hence, since the length of the prefix is finite, the algorithm always terminates and transforms ϕ into an equivalent disjunction of basic formulas for its free variables. Example 5. Let {a/0 , g/1 , f/2 } ⊂ FΣ and x = {x1 , x2 } ⊂ V. The constraint ∀x [( f (x1 , a) == f (g(x2 ), x2 )∧¬g(x2 ) == g(g(x1 )) ) ∨ f (x1 , x2 ) == f (x2 , x1 )] is already in prenex disjunctive normal form, thus Step 1 is not applicable. In Step 2, the formula is first transformed into ∀x [ ( x1 == g(x2 ) ∧ x2 == a ∧ ¬x2 == g(x1 ) ) ∨ x1 == x2 ] using D1 and D2 . Next, the formula is transformed into basic normal form ∀x [ ( x1 == g(a) ∧ x2 == a ) ∨ ∃w ( x1 == w ∧ x2 == w ) ] using R, C2 and EI. Next, in Step 3, we proceed to eliminate ∀x (case (ii)). Using double negation, we obtain ¬∃x [ ¬( x1 == g(a) ∧ x2 == a ) ∧ ¬∃w ( x1 == w ∧ x2 == w ) ]
A Complete Axiomatization of Strict Equality
129
that is transformed into ¬∃x [ ( ¬x1 == x1 ∧ ¬x2 == x2 ) ∨ ∃w ( ¬x1 == x1 ∧ x2 == w ) ∨ ∃w ( ¬x2 == x2 ∧ x1 == w ∧ ¬w == g(a) ) ∨ ∃w ( x1 == w1 ∧ x2 == w2 ∧ ¬w1 == g(a) ∧ ¬w1 == w2 ) ∨ ( ¬x2 == x2 ∧ x1 == g(a) ) ∨ ∃w ( x1 == g(a) ∧ x2 == w ∧ ¬w == a ∧ ¬w == g(a) ) ] by negation and conjunction of basic formulas. Then, ∃x can be eliminated and we obtain ¬[t rue], which is trivially equivalent to f alse. As an easy but important consequence of having a decision method, we obtain the completeness of our axiomatization Einf . Theorem 5 (Completeness of Einf ). Einf is a complete theory. Proof. Consider any closed formula φ. By using the decision method of Fig. 4 we obtain a formula ψ in BNF such that Einf |= φ ↔ ψ. Now, ψ must be also closed (since the transformation rules do not introduce new free variables in a formula), and therefore ψ is a disjunction made of the atoms t rue or f alse (which are the only closed basic formulas). Hence, ψ is equivalent to t rue or f alse. In the first case, we have Einf |= φ ↔ t rue, which implies Einf |= φ. In the second case, we have Einf |= φ ↔ f alse, and then Einf |= ¬φ. Therefore, Einf is complete. 4.2
Finite Signatures
In the case of finite signatures, the normal form provided in Definition 2 is not solved. This arises from the fact that a finite conjunction of universally quantified negated equations on a variable w may be unsatisfiable if only finite and total trees can be assigned to w. For example, if FΣ = {a/0 , g/1 }, the constraint ∃w ( x == w ∧ ¬w == a ∧ ∀v ¬w == g(v) ) is unsatisfiable although ∃w ( ¬w == a ∧ ∀v ¬w == g(v) ) is satisfiable. In general, for any finite signature FΣ one can write a formula that is in normal form according to Definition 2 but is unsatisfiable. Roughly speaking, the reason is that all the function symbols of the signature can be used in a constraint. Next, we show that Ef in is a decidable theory. For this purpose, we adapt all the definitions and results in Subsection 4.1 to the case of finite signatures. Besides, we add two new transformation rules E and EE5 (see Figure 5). Rule Eallows for the elimination of universal quantification whereas EE5 , which is the adaptation of EE4 to the case of finite signatures, makes it possible to eliminate the innermost block of existential quantifiers. Proposition 6. The transformation rules EE5 and E are correct in Ef in .
The use of E for eliminating universal quantification is necessary because our notion of solved form for finite signatures is free of universal variables.
130
´ J. Alvez and F.J. L´ opez-Fraguas
Existential Quantification Elimination n (EE5 ) ∃w ( v == v ∧ ¬si == ti ∧ ϕ )
→ ϕ i=1
if w ∩ Var(ϕ) = ∅, v ⊆ w, si = ti and w ∩ Var(si , ti ) = ∅ for 1 ≤ i ≤ n Explosion (E) ϕ[x]
→ ϕ[x] ∧ [ ¬x == x ∨
∃w x == f (w) ]
f ∈FΣ
Fig. 5. Transformation Rules: Finite Signatures
Definition 3. A basic formula for the variables x is either t rue, f alse (closed basic formulas) or a constraint ∃w c(x, w) such that c(x, w) =
x1 ∈x1
¬x1 == x1 ∧ x2 == t ∧
ni
¬wi == sij
wi ∈w j=1
where − x = x1 ∪ x2 and x1 ∩ x2 = ∅, − w = Var(t) and x ∩ w = ∅, − if sij is a variable, then sij = wi , otherwise sij is total, Var(sij ) ⊆ w − and wi ∈ Var(sij ) for every wi ∈ w and 1 ≤ j ≤ ni . A formula is in basic normal form (or BNF) if it is of the form Qy ϕ[x · y] where ϕ is a disjunction of basic formulas for x · y. With this definition, basic formulas for finite signatures are also in solved form. Theorem 6. Any basic formula different from f alse is satisfiable in Ef in .
Note that the syntactical form provided in Def. 3 is a particular case of the one in Def. 2. The only difference is that universal quantification is not allowed in the case of finite signatures. Further, there exists a very simple transformation using E from formulas as defined in Def. 2 into formulas as defined above. ni Proposition 7. Any constraint of the form ϕ = wi ∈w j=1 (¬wi == sij )∀\w can be transformed into an equivalent disjunction of basic formulas for w. Being ∃w c(x, w) a formula as described in Definition 2, the conjunction of negated equations in c(x, w) is transformed into a disjunction of basic formulas w as shown in Proposition 7. Then, the whole formula is transformed into an equivalent disjunction of basic formulas for x using R, EE2 and FT. This result allows us to easily adapt Propositions 3 and 4 to the case of finite signatures. Example 6. Let FΣ = {a/0 , g/1 , f/2 } and x = {x1 , x2 } ⊂ V. The constraint ∃w ( ¬x1 == x1 ∧ x2 == f (w, a) ∧ ∀v ¬w == f (a, v) )
A Complete Axiomatization of Strict Equality
131
is transformed into a disjunction of basic formulas for x as follows. First, we transform ∀v ¬w == f (a, v) into a disjunction of basic formulas for w using E: ∀v ¬w == f (a, v) ∧ [ ¬w == w ∨ w == a ∨ ∃z w == g(z) ∨ ∃z w == f (z1 , z2 ) ] ≡ ¬w == w ∨ w == a ∨ ∃z w == g(z) ∨
(1)
∃z ( w == f (z1 , z2 ) ∧ ∀v ¬w == f (a, v) ) The first three subformulas are already basic formulas for w. Regarding the last one, it is transformed using rules R and D2 as follows ∃z ( w == f (z1 , z2 ) ∧ [ ¬z1 == a ∨ ∀v ¬z2 == v ] ) ≡ ∃z (w == f (z1 , z2 ) ∧ ¬z1 == a) ∨ ∃z (w == f (z1 , z2 ) ∧ ∀v ¬z2 == v) (2) where the second subformula is equivalent to f alse by rules NFT3 and NFT2 . Thus, ∀v ¬w == f (a, v) has been transformed into the disjunction of basic formulas for w in (1, 2). Finally, the conjunction of the above disjunction and ¬x1 == x1 ∧ x2 == f (w, a) is transformed into ( ¬x1 == x1 ∧ x2 == f (a, a) ) ∨ ∃z ( ¬x1 == x1 ∧ x2 == f (g(z), a) ) ∨ ∃z ( ¬x1 == x1 ∧ x2 == f (f (z1 , z2 ), a) ∧ ¬z1 == a ) using rules R, EE1 and NFT2 .
Next, in order to be able to apply the algorithm in Figure 4 to the case of finite signatures, we adapt the result in Theorem 4. Theorem 7. Let ∃w a(x · y, w · z) be a basic formula for x · y of the form ∃w∃z ( ¬x1 == x1 ∧ ¬y1 == y1 ∧ x2 == t ∧ y 2 == r ∧ ϕ ∧ ψ ) x1 ∈x1
y1 ∈y 1
where − w = Var(t) and z = Var(r) \ w, − ϕ is a finite conjunction of negated equations such that Var(ϕ) ⊆ w, − ψ = ni=1 ¬vi == si and (vi ∪ Var(si )) ∩ z = ∅ for each 1 ≤ i ≤ n. The formulas ∃y [ ∃w a(x · y, w · z) ] and ∃w ( x1 ∈x1 ¬x1 == x1 ∧ x2 == t ∧ ϕ ) are equivalent in Ef in . Proof. It follows from rules EE1 , EE2 , EE3 and EE5 .
Finally, and similarly to the case of infinite signatures (Th. 5), we obtain: Theorem 8 (Completeness of Ef in ). Ef in is a complete theory.
5
Conclusions and Future Work
We have given an axiomatization E of the theory of strict equality over IT , the algebra of possibly infinite and partial trees, both for the cases of infinite
132
´ J. Alvez and F.J. L´ opez-Fraguas
and finite signatures. The notion of strict equality over that kind of trees is of particular interest for functional and functional-logic programming. In addition, we have provided a decision algorithm —which proves that the axiomatization is complete— based on the use of solved forms and quantifier elimination. Further, it is easy to see that the problem of deciding first-order equality constraints of finite trees can be reduced to the decision problem of the theory of infinite trees with strict equality: it suffices to restrict the value of every variable x in any formula to be a finite and total tree by assertions of the form x == x. Thus, it follows from the results in [6,17] that the decision problem of the theory of infinite trees with strict equality is non-elementary (as lower bound). Intuitively, in computational complexity a problem is said to have non-elementary time complexity iff for any algorithm that solves it, we always can find some input of size n so that the running time performed by the algorithm over this input is greater than a tower of n powers of 2. In this paper, we have focused on the algebra IT of possibly infinite and partial trees. However, as a side product of our results, we can derive interesting consequences also for the algebra F T of finite and possibly partial trees. In particular, it is easy to see that F T |= E. Since E is a complete theory, it follows that E is also a complete axiomatization of == over F T and, therefore, we conclude that IT and F T are elementarily equivalent (when the language of == is considered). We remark that this does not happen for infinite and finite trees when true equality (≈) is considered. Although direct applications of our results have been left out of the focus of the paper, we foresee some potential uses that will be subject of future work: Herbrand constraint solvers present in existing functional-logic languages, essentially corresponding to existential constraints, could be enhanced to deal with more general formulas. Constructive failure [12,10], the natural counterpart of constructive negation in the functional logic field, could also profit from our methods, specially for the case of programs with extra variables, not considered in those papers. For these envisaged continuations of our work it could be convenient to extend the theory and methods of this paper by adding two additional predicate symbols: strict disequality (a computable approximation of negation of strict equality) and true equality.
References ´ 1. Alvez, J., L´ opez-Fraguas, F.J.: A complete axiomatization of strict equality over infinite trees. Technical Report SIC-3-09, UCM, Madrid (2009), http://gpd.sip.ucm.es/fraguas/papers/TR_SIC_3_09.pdf 2. Arenas-S´ anchez, P., Gil-Luezas, A., L´ opez-Fraguas, F.J.: Combining lazy narrowing with disequality constraints. In: Hermenegildo, M.V., Penjam, J. (eds.) PLILP 1994. LNCS, vol. 844, pp. 385–399. Springer, Heidelberg (1994) 3. Arias, E.J.G., Mari˜ no-Carballo, J., Poza, J.M.R.: A proposal for disequality constraints in Curry. Electr. Notes Theor. Comput. Sci. 177, 269–285 (2007) 4. Colmerauer, A.: Equations and inequations on finite and infinite trees. In: Clark, K.L., T¨ arnlund, S.A. (eds.) FGCS 1984, pp. 85–99 (1984)
A Complete Axiomatization of Strict Equality
133
5. Comon, H., Lescanne, P.: Equational problems and disunification. Journal of Symbolic Computation 7(3/4), 371–425 (1989) 6. Compton, K.J., Henson, C.W.: A uniform method for proving lower bounds on the computational complexity of logical theories. Annals of Pure and Applied Logic 48(1), 1–79 (1990) 7. Djelloul, K., Dao, T.-B.-H., Fr¨ uhwirth, T.W.: Theory of finite or infinite trees revisited. TPLP 8(4), 431–489 (2008) 8. Hanus, M. (ed.): Curry: An integrated functional logic language (March 2006), http://www.informatik.uni-kiel.de/~ curry/report.html 9. L´ opez-Fraguas, F.J.: Programaci´ on Funcional y L´ ogica con Restricciones. PhD thesis, Univ. Complutense Madrid (1994) (in Spanish) 10. L´ opez-Fraguas, F.J., S´ anchez-Hern´ andez, J.: Failure and equality in functional logic programming. Electr. Notes Theor. Comput. Sci. 86(3) (2003) 11. L´ opez-Fraguas, F., S´ anchez-Hern´ andez, J.: T OY: A multiparadigm declarative system. In: Narendran, P., Rusinowitch, M. (eds.) RTA 1999. LNCS, vol. 1631, pp. 244–247. Springer, Heidelberg (1999) 12. L´ opez-Fraguas, F., S´ anchez-Hern´ andez, J.: A proof theoretic approach to failure in functional logic programming. TPLP 4(1, 2), 41–74 (2004) 13. Maher, M.: Complete axiomatizations of the algebras of finite, rational and infinite trees. Technical report, IBM T.J. Watson Research Center (1988), http://www.cse.unsw.edu.au/~ mmaher/pubs/trees/axiomatizations.pdf 14. Maher, M.J.: Complete axiomatizations of the algebras of finite, rational and infinite trees. In: LICS 1988, pp. 348–357. IEEE Computer Society, Los Alamitos (1988) 15. Peyton Jones, S.L. (ed.): Haskell 98 Language and Libraries. The Revised Report. Cambridge Univ. Press, Cambridge (2003) 16. Reiter, R.: On closed world data bases. In: Logic and Data Bases, pp. 55–76 (1978) 17. Vorobyov, S.G.: An improved lower bound for the elementary theories of trees. In: McRobbie, M.A., Slaney, J.K. (eds.) CADE 1996. LNCS, vol. 1104, pp. 275–287. Springer, Heidelberg (1996)
Standardization and Böhm Trees for Λμ-Calculus Alexis Saurin PPS & INRIA πr 2
[email protected]
Abstract. Λμ-calculus is an extension of Parigot’s λμ-calculus which (i) satisfies Separation theorem: it is Böhm-complete, (ii) corresponds to CBN delimited control and (iii) is provided with a stream interpretation. In the present paper, we study solvability and investigate Böhm trees for Λμ-calculus. Moreover, we make clear the connections between Λμcalculus and infinitary λ-calculi. After establishing a standardization theorem for Λμ-calculus, we characterize solvability. Then, we study infinite Λμ-Böhm trees, which are Böhm-like trees for Λμ-calculus; this allows to strengthen the separation results that we established previously for Λμcalculus and to shed a new light on the failure of separation in Parigot’s original λμ-calculus. Our construction clarifies Λμ-calculus both as an infinitary calculus and as a core language for dealing with streams as primitive objects.
1
Introduction
From λμ-calculus to Λμ-calculus. Curry-Howard correspondence [15] originated as a correspondence between intuitionistic natural deduction (NJ) and simply typed λ-calculus. The extension of the correspondence to classical logic resulted in strong connections with control operators in functional languages as first noticed [13] by Griffin in his analysis of the logical interpretation of Felleisen’s C operator [10]. Shortly after Griffin, Parigot [26] introduced λμcalculus as an extension of λ-calculus corresponding to minimal classical natural deduction [25,1] in which one can encode usual control operators. λμ-calculus became one of the most widely studied classical λ-calculi, both in the typed and untyped setting. In particular, it extends λ-calculus (while retaining most of its standard properties) and intuitionistic natural deduction in a natural way. However, a fundamental property of pure λ-calculus, known as separation property (or Böhm theorem [5]), does not hold for λμ-calculus [29,8]. In a previous work, we introduced Λμ-calculus, an extension of λμ-calculus, for which we proved that separation holds [30]. Since our result on separation theorem for Λμcalculus, several authors investigated this calculus. While we studied confluence and type systems for Λμ-calculus [31,32,33], Lassen [21] introduced bisimulations for Λμ-calculus and introduced a CPS translation for Λμ-calculus and Herbelin and Ghilezan [14] showed that Λμ-calculus is a CBN calculus with delimited control. M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 134–149, 2010. c Springer-Verlag Berlin Heidelberg 2010
Standardization and Böhm Trees for Λμ-Calculus
135
Delimited Control. Herbelin and Ghilezan [14] evidenced that Λμ-calculus is indeed a call-by-name calculus with delimited continuations (in the spirit of Danvy and Filinski shift/reset operators [7]) using λμtp-calculus as a mediator between Danvy-Filinski CBV calculi and Λμ-calculus. Delimited control refers to a class of control operators which are much more expressive than non-delimited control operators (like call/cc for instance): they allow to simulate various sideeffects [11], the monadic side-effects. In their seminal paper on shift/reset [7], Danvy and Filinski defined shift/reset operators by their CPS semantics. They also introduced a hierarchy of such control operators, (shifti /reseti )i∈ω , which are obtained by iterating CPS translations and that is known as the CPS hierarchy. Delimited control and the CPS hierarchy found applications in linguistics, normalization by evaluation, partial evaluation or concurrency. While the emphasis was traditionally given to the delimited-control languages in call-by-value, recent works [14,18] have advocated the study CBN delimited control. In a recent work [34], we introduced a hierarchy of calculi generalizing both λ-calculus and Λμ-calculus, the Stream Hierarchy, that we proved to be a call-by-name analogous to the CPS hierarchy. Streams and Infinitary λ-calculi. Another viewpoint on the Separation property in Λμ-calculus is that the continuation variables in Λμ-calculus can be seen as abstracting streams of Λμ-terms. This provides the Λμ-calculus with an operational intuition of a stream calculus where one has the ability to abstract both on terms and streams. A weak form of this had already been noticed by Parigot who considered that “the operator μ looks like a λ having potentially infinite number of arguments” [26]. The understanding of calculi of the family of λμ as infinitary calculi is straightforward in Λμ-calculus: μα is considered as an α abstraction over streams of terms (i.e. λxα 1 . . . xn . . . .t) while (t)α can be seen α as the application of a function t to a stream of inputs (i.e. (t)xα 1 . . . xn . . . ). Infinitary λ-calculi have been considered in the literature [3,16,17,4,9] both to study infinite structures arising from lazy functional languages or to study consistency problems in the standard λ-calculus. Though, infinitary λ-calculi have been designed in a much different way from the infinitary calculus underlying Λμ-calculus: whereas in those frameworks, a reduction sequence may have transfinite length, terms have a (possibly infinite) depth which is bounded by ω. On the contrary, the infinitary correspondent to μα.μβ.λx.x would be the transfinite term λx0 , x1 . . . xω , xω+1 . . . xω2 .xω2 . Structure of the Paper. The meta-theory of Λμ-calculus is already quite developed (Böhm and Church-Rosser theorems, simply typed calculus with strong normalization and subject reduction, complete CPS translations, abstract machines) as well as the connections with delimited control calculi. However, there is no standardization theorem known for Λμ-calculus. In the same line of ideas, one may wonder how to construct Böhm trees for Λμ-calculus. The answers to these questions would help understanding the operational theory of Λμ-calculus as well as its model theory (by developing, for instance, a Böhm model for Λμ).
136
A. Saurin
We shall precisely provide Λμ-calculus meta-theory with a standardization theorem and an analysis of Böhm trees in the present paper. In section 2, we shall recall some background on Λμ-calculus. We shall then define Λμ-head normal forms in Section 3, prove a standardization theorem and characterize solvability. In section 4, Böhm-like trees for Λμ-calculus will be introduced. A study of a Böhm tree semantics for Λμ-calculus is postponed to future work. Notations. In the following, we shall use Krivine’s notation [20] for λ-terms (as well as Λμ-terms or other λ-like calculi...): λ-application shall be written (t)u instead of the notation (M N ) and we consider, as usual, λ-application to be left-associative, that is (t)u1 . . . uk−1 uk shall be read as (. . . ((t)u1 ) . . . uk−1 )uk . Moreover we use an alternative notation for Λμ-terms, writing (t)α instead of the more common [α]t from Parigot (see [30,32] for explanations). For instance, we shall write μα.(t)uβ for μα.((t)u)β (which would be written μα.[β](t u) in [26]).
2
Background and Notations on Λμ-Calculus
Parigot’s Original Calculus: λμ. In 1992, Parigot introduced λμ, an extension of λ-calculus providing “an algorithmic interpretation of classical natural deduction” [26] by allowing for a proof-program correspondence à la Curry-Howard [15] between λμ-calculus and classical natural deduction [25,26]. Moreover, λμ satisfies standard properties of λ-calculus such as confluence [26,29,8,2], subject reduction [26] and strong normalization [27,28]. However, there is no such thing as a Böhm theorem for λμ-calculus. Indeed, David & Py proved that separation fails in λμ [29,8] by finding a counter-example (for details, see [8,30,32]). A λμ-calculus Satisfying Böhm Theorem: Λμ-calculus. The failure of separation in λμ-calculus may be understood as the fact that some separating contexts are missing in λμ, making it impossible to observe (or access by Böhm out) the sub-parts of the terms that contain the difference. This led us to define, in a previous work [30], Λμ-calculus, an extension to λμ for which we proved Böhm theorem. In Λμ-calculus, the validity of separation may be understood as the fact that the new contexts made available by the new syntax are sufficient to realize a Böhm Out (in the new syntax). Definition 1. Λμ-terms (t, u, v · · · ∈ ΣΛμ ) are defined by the following syntax: ΣΛμ
t, u ::= x | λx.t | (t)u | μα.t | (t)α
with x ∈ Vt , α ∈ Vs
Vt (term variables) and Vs (stream variables) are infinite and disjoint. F V (t) denotes the set of free term and stream variables of t. Λμ-terms with no free c stream variable (that is F V (t) ⊂ Vt ) will be denoted by ΣΛμ and called μ-closed. Remark 1. Since α ∈ ΣΛμ , notations (t)α and (t)u are not ambiguous.
Standardization and Böhm Trees for Λμ-Calculus
137
Definition 2. Λμ-reduction, written −→Λμ , is induced by the following rules: (λx.t)u −→βT λx.(t)x −→ηT (μα.t)β −→βS μα.(t)α −→ηS μα.t −→fst
t {u/x} t if x ∈ F V (t) t {β/α} t if α ∈ F V (t) λx.μα.t {(v)xα/(v)α} if x ∈ F V (t)
Remark 2. Notice that μ-reduction (also called structural reduction, or R2 in Parigot’s papers, see [26]) is not part of Λμ-calculus reduction system. It can indeed be simulated by a fst -reduction followed by a βT -reduction: (μα.t)u −→fst (λx.μα.t {(v)xα/(v)α})u −→βT μα.t {(v)uα/(v)α} and can be added for free. Remark 3. Names for reductions in Λμ-calculus come from the stream interpretation of Λμ: μα is seen as an abstraction over streams of terms while (t)α is a construction passing a stream as an argument to term t. In particular, μ can be viewed as an infinitary λ-abstraction. Under this interpretation, fst instantiates the first elements of a stream: μα.t −→fst λx1 . . . λxn .μα.t {(v)x1 . . . xn α/(v)α} Definition 3 (β, η). We consider the following subsystems of −→Λμ : – – – –
β is the subsystem made of reductions βT and βS ; η is the subsystem made of reductions ηT and ηS ; βfst is the subsystem βT βS fst and βηfst is the full Λμ-reduction system; ∼Λμ is the equivalence associated with −→Λμ .
The separation theorem for Λμ-calculus is stated with respect to a set of canonical normal forms (corresponding, in λ-calculus, to βη-normal forms): Definition 4. A Λμ-term t is in canonical normal form (CNF) if it is βηnormal and if it contains no subterm of the form (λx.u)α nor (μα.u)v. Remark 4. A closed canonical normal form is thus a βη-normal form such that no fst -reduction creates a βT -redex. Definition 5. Λμ-contexts are defined by the following syntax: C ::= [] | λx.C | (t)C | (C)t | μα.C | (C)α. Theorem 1 (Böhm theorem for Λμ-calculus [30]). If t and t are two non Λμ-equivalent closed canonical normal forms, there exists a context C such that:
C[t] −→Λμ λx.λy.x
and
C[t ] −→Λμ λx.λy.y.
138
A. Saurin
Confluence holds in Λμ-calculus [31,33] for μ-closed terms: c c Theorem 2. For any t, t , t ∈ ΣΛμ , there exists u ∈ ΣΛμ such that if t −→Λμ t , t then t , t −→Λμ u.
Λμ-calculus, a CBN calculus of delimited control. Separation theorem for Λμ-calculus can be understood as the fact that Λμ-calculus admits more contexts than Parigot’s original calculus allowing for a more powerful exploration of terms than in λμ-calculus. Typical contexts used in the separation proofs are []u1 . . . um βu v1 . . . vn βv . This exploits the fact that a context of the form []u1 . . . um βu delimits the part of the environment that can be passed through the left-most μ-abstracted variable (i.e. α) when term μα.μα .t is placed in the hole. As a result, one can access to the second μ-abstracted variable α thanks to the second portion of the context, v1 . . . vn βv . Based on this fact, Herbelin and Ghilezan [14] evidenced strong connections between Λμ-calculus and calculi with delimited continuations in the spirit of Danvy and Filinski shift/reset (where tp is dynamically bound, see [14]): operators [7] using the calculus λμtp Σλμtp
t, u ::= x | λx.t | (t)u | μq.c
c ::= [q]t
q ::= α | tp
is equivalent to Danvy-Filinski’s shift/reset In its call-by-value version, λμtp operators while in its call-by-name version the calculus is equationally correspondent to Λμ. This led Herbelin & Ghilezan to assert that Λμ is a CBN calculus of delimited control, providing an additional evidence of the striking difference between this calculus and λμ due to the slight change of syntax operated in [30]. Delimited control operators are much more expressive than non-delimited control operators (like call/cc for instance) in that they allow to simulate various side-effects [11]. Delimited control found several applications in linguistics, normalization by evaluation, partial evaluation or concurrency.
3
Standardization Theorem for Λμ-Calculus
In this section, we shall prove a standardization theorem for Λμ-calculus. 3.1
Λμ-Head Normal Forms
c Definition 6 (Pre-redex). Let v ∈ ΣΛμ . A pre-redex (or p.r.) of v is a subterm of one of the following forms: (λx.t)u, (λx.t)α, (μα.t)u or (μα.t)β. The four types of pre-redex are respectively denoted as (T )T , (T )S, (S)T and (S)S.
Lemma 1. Considering βfst, a p.r. t ∈ ΣΛμ can be reduced in different ways: 1. a (T )T p.r. is a βT -redex: it can be reduced by exactly one instance of βT ; 2. a (S)T p.r. is not a redex, but can be turned into a (T )T p.r. (i.e. βT -redex) thanks to an instance of fst; 3. a (T )S p.r., (λx.u)α, can be treated as in case 2, provided α is bound in t;
Standardization and Böhm Trees for Λμ-Calculus
139
4. a (S)S p.r. (μα.u)β is a βS -redex and can thus be βS -reduced. Moreover, and contrarily to any other type of pre-redex, two other rules can affect this p.r.: one can either apply a fst-reduction on μα creating a (T )S p.r., or apply a fst-reduction to the μ-abstraction which binds β and create a (S)T p.r. Definition 7. – t is an application term if it is of form (u)v or (u)α; – t is an abstraction term if it is of form λx.u or μα.u. A Λμ-term t is either a variable, or an application term, or an abstraction term. →.μα . . . . λ− −−− → An abstraction term t is of the form λ− x x→ 0 0 n .μαn .λxn+1 .t0 where t0 is → − not an abstraction term (vectors xi may be empty). An application term t is of → − − → −−−→ the form (t0 ) t1 α1 . . . tm αm tm+1 where t0 is not an application term. Lemma 2. Any Λμ-term t has the following form: → − − → −−−→ →.μα . . . . λ− −−−→ λ− x x→ 0 0 n .μαn .λxn+1 .(t0 ) t1 β1 . . . tm βm tm+1
( )
where t0 is either a variable x or a pre-redex of t. c Definition 8 (Head normal forms). t ∈ ΣΛμ is in head normal form (hnf ) if t0 is a variable x in representation ( ); in this case x is the head variable of t. Otherwise, t0 is a pre-redex and is called the head pre-redex (or hpr) of t. Λμ-HNF denotes the set of hnf. A term t is said to have a hnf if t −→Λμ t where t is in hnf. Hnf are also the μ-closed terms given by the following grammar:
h ::= g | λx.h | μα.h
g ::= x | (g)t | (g)α
Definition 9 (Head reduction). Head reduction, denoted −→h , is the subreduction of βfst which reduces the hpr if there is one. A head reduction path (or hrp) for a term t is a sequence t0 , t1 , . . . such that t = t0 −→h t1 −→h . . . If tn is in hnf for some n, then the hrp for t terminates in tn , otherwise t has an infinite head reduction. Notice that, contrarily to λβ-calculus, the hrp of t is not necessarily unique. Example 1. For instance if t = μα.(μβ.x)α and u = λy.μα.x, the following two reduction sequences are head reduction paths from t to u: t = −→fst −→fst −→βT −→βS 3.2
μα.(μβ.x)α λy.μα.(μβ.x)yα λy.μα.(λz.μβ.x)yα λy.μα.(μβ.x)α λy.μα.x
t = −→fst −→fst −→βT −→βS
μα.(μβ.x)α μα.(λz.μβ.x)α λy.μα.(λz.μβ.x)yα λy.μα.(μβ.x)α λy.μα.x
Standard Reductions
We shall now define standard reductions for Λμ-calculus and prove standardization. The notion of standard reduction is more complex than in λ-calculus
140
A. Saurin
because of the structure of fst -reduction. Indeed, fst -reduction is a non-local rule which acts both at the place of the μ-abstracted sub-term μα.t but also at the occurrences of abstracted variable: (t)α. This is reminiscent of a proof-net analysis of Λμ-calculus pursued together with Pagani [24] in which the proofnet counter-part of fst -reduction could be activated either at the level of the abstraction or at the level of the variables. In our analysis of standardization, the appropriate notion will thus be that of p.r.: to each βfst-reduction on a given Λμ-term t, we shall associate at most one p.r. That p.r. will be used to determine whether a given reduction step extends a standard reduction sequence into a larger reduction sequence. Definition 10 (p.r. associated with a reduction step, fste , fsts ). Let ρ c t, u ∈ ΣΛμ such that t − →βfst u. Depending on the reduction step ρ and the structure of t, we shall associate zero or one p.r. of t which will be called the pre-redex associated with (t, ρ). ρ
→βT u. Let t = Cρ [tρ ] with tρ = (λx.v)w and u = Cρ [v {w/x}]. Then, tρ – If t − is the p.r. associated with (t, ρ). ρ →βS u. Let t = Cρ [tρ ] with tρ = (μα.v)β and u = Cρ [v {β/α}]. Then , tρ – If t − is the p.r. associated with (t, ρ). ρ →fst u. Let t = Cρ [tρ ] with tρ = μα.v and u = Cρ [λx.μα.v {(w)xα/(w)α}]. – If t − We consider three cases: • either Cρ = Cρ [([])w]. In this case, then (tρ )w is a p.r. and is the preredex associated with (t, ρ) and ρ is an essential fst-reduction step; • otherwise, if there is a subterm r of v which is a p.r. of the form (λx.w)α or (μβ.w)α where α is free in v, then the pre-redex associated with (t, ρ) is the left-most such r and ρ an essential fst-reduction step; • otherwise, there is no such (λx.w)α nor (μβ.w)α (i.e. either α ∈ F V (v) or all free occurrences of α are of the form (x)α, ((w )w )α or ((w )β)α). Then ρ is said superfluous and no p.r. is associated with (t, ρ). We write fste to denote an essential fst-reduction step and fsts to denote a superfluous fst-reduction step. Definition 11 (Active symbol). A symbol λ or μ is active if it is the first symbol of a pre-redex. c and r be a p.r. of t. The degree d(r) Definition 12 (Degree). Let t ∈ ΣΛμ of r is the number of λ or μ which are both active in t and to the left of r in t. The degree of a reduction step ρ, d(ρ) is defined as follows:
– if ρ is a β-reduction or a fste -reduction, then d(ρ) is the degree of the preredex associated with (t, ρ); – if ρ is a fsts -reduction and n is the number of μ-abstraction to the left of the μ-abstraction corresponding to ρ, then d(ρ) = ω + n. Definition 13 (Standard reduction). A (possibly infinite) reduction sequence ρ1 ρ2 t = t0 −→ t1 −→ . . . is standard if d(ρi ) ≤ d(ρj ) as soon as i ≤ j.
Standardization and Böhm Trees for Λμ-Calculus
141
A reduction sequence is said essentially standard if it is standard and all the degrees involved in the sequence are finite. We write t −→s u if there exists a standard reduction from t to u. Example 2. let t, u be closed Λμ-terms and consider the following reduction sequences (where wα = w {(v)xα/(v)α}, wβ = w {(v)yβ/(v)β}, wαβ = (wα )β and wαββ = wα {(v)yzβ/(v)β}): σ : −→fst −→fst −→βT −→fst −→βT
μα.(μβ.t)uα λx.μα.(μβ.tα )uα xα λx.μα.(λy.μβ.tαβ )uα xα λx.μα.(μβ.tαβ {uα /y})xα λx.μα.(λz.μβ.tαββ {uα /y})xα λx.μα.(μβ.tαββ {uα /y} {x/z})α
τ : −→fst −→βT −→fst −→fst −→βT
μα.(μβ.t)uα μα.(λy.μβ.tβ )uα μα.(μβ.tβ {u/y})α λx.μα.(μβ.tαβ {uα /y})xα λx.μα.(λz.μβ.tαββ {uα /y})xα λx.μα.(μβ.tαββ {uα /y} {x/z})α
Then σ is not standard since the first reduction step has degree ω while the second has degree 0. τ is standard: the degrees are all equal to 0 in τ . The following lemma explains the terminology superfluous for fsts -reductions: Lemma 3. In a standard reduction from a (closed) Λμ-term t to a Λμ-term u containing no μ-abstraction (that is, a λ-term), there is no fsts -reduction. We can now state the standardization theorem: Theorem 3 (Standardization). Standard reduction sequences always exist: if t −→βfst u then t −→s u. However, standard reductions are not unique. Indeed, there may exist several reduction sequences which are standard, equivalent (in some sense to be defined) though not equal. For instance reductions shown in example 1 are both standard and equivalent (and there is moreover reduction σ0 : μα.(μβ.x)α −→βS μα.x −→fst λy.μα.x which is also standard though not essentially standard). 3.3
Strong Standardization
As a consequence, following Klop [19], we shall introduce a notion of strong standardization which shall ensure that there is a unique standard reduction sequence in a class of equivalent reductions. But first, we shall informally introduce the notion of equivalence on reductions that we shall consider and which is inspired from Lévy strong equivalence on reductions: using a straightforward indexing technique à la Lévy, adapted to Λμ-calculus, one can define residuals of a set of pre-redexes by a reduction sequence, F /σ, and the residual of a reduction sequence by another reduction sequence, σ/τ . Then, two reduction sequences σ, τ from t to u are equivalent if σ/τ = τ /σ = ∅. Due to the more complex structure of Λμ-calculus, the description of strongly standard reductions is quite involved. We shall show a standardization procedure which transforms a reduction sequence into a strongly standard reduction
142
A. Saurin
sequence and use this to define strongly standard reductions. For this purpose, the following definition introduces the leftmost pre-redex associated with a reduction sequence, lmp(σ), and the leftmost contracted pre-redex in a reduction sequence, lmc(σ). Definition 14 (lmp(σ), lmc(σ)). Let σ = (σi ) be a reduction from t to u. – If some p.r. of t has a residual r associated with σn for some n, then lmp(σ) is the leftmost such p.r. Otherwise, σ is a fsts -reduction sequence and lmp(σ) is the leftmost fst-redex which undergoes a fst-reduction. – lmc(σ) is defined as follows: • if lmp(σ) is a (T )T , (T )S or (S)T p.r., then lmc(σ) is the only reduction step that can be associated to this p.r.; • if lmp(σ) = (μα.t)β, then βS , fst on the binder of β or fst on μα.t can be associated with this p.r. Thus we do a case analysis: 1. if lmp(σ) has no residual by σ, then it means that a βS is applied to some residual of lmp(σ) and lmc(σ) is this occurrence of βS ; 2. otherwise, if there is in σ a residual of lmp(σ) of type (T )T or (S)T , then lmc(σ) is this occurrence of fst applied to the binder of β; 3. otherwise, lmc(σ) is the occurrence of fst applied to μα.t; • if lmp(σ) not a pre-redex, then it is a μ-abstracted term and lmc(σ) is the occurrence of fst applied to this μ-abstraction. – p(σ) = σ/{lmc(σ)} is the residual of σ after contracting lmc(σ). Definition 15 (Standardization process). Given a reduction sequence σ : σ
σ
σ
σ
lmc(σ)
1 2 3 n t1 −→ t2 −→ t3 . . . −−→ tn . One considers σs defined as: σs : t0 −−−−→ t = t0 −→
lmc(p(σ))
lmc(p(p(σ)))
t1 −−−−−−→ t2 −−−−−−−−→ t3 . . . (σs stops when p(p(. . . p(σ))) = ∅). A strongly standard reduction σ is a reduction which is invariant by the standardization process: σs = σ. Theorem 4 (Strong Standardization). Let σ be a finite reduction sequence. – σs is finite, equivalent to σ and strongly standard; – if τ is equivalent to σ, then σs = τs . Other Approaches to Standardization in λμ. Py studied standardization for λμ-calculus in his PhD thesis. However, his approaches cannot be generalized and therefore our approach to standardization differs quite radically from his. In particular, Py proves that it is possible to postpone applications of βS while this does not hold in the more general framework of Λμ-calculus: for instance, in μβ(μα.λx.(x)x)βλx.(x)x, the (Δ)Δ cycling structure can appear only once a βS reduction has been applied. As a consequence, our standard reductions have a very different structure from that of Py. Another important difference between the two approaches is that in our approach, the important notion is that of pre-redex rather than Λμ-redexes themselves. In our opinion, this makes more sense operationally in particular if one views Λμ as a calculus computing on streams.
Standardization and Böhm Trees for Λμ-Calculus
3.4
143
Solvability
We shall now characterize solvability in Λμ-calculus. Definition 16 (Stream applicative context). Stream applicative contexts are of the form []t11 . . . t1n1 α1 . . . tk1 . . . tknk αk . They are defined by: S ::= [] | (S)t | (S)α Definition 17. t ∈ ΣΛμ closed is solvable if there exists a stream applicative context S such that S[t] −→βfst λx.x; t ∈ ΣΛμ is solvable if its closure is solvable. The following theorem will be useful for characterizing solvability: c Theorem 5. Let t ∈ ΣΛμ . There exists a terminating head reduction path of t iff t has a head normal form.
Proof. ⇒ is trivial. For ⇐, let us suppose t has a hnf h and consider a standard reduction sequence σ = (ti )0≤i≤n to h: t −→s h. Since σ is standard, σ begins possibly with some head reduction steps and as soon as ti −→ ti+1 is an internal reduction all remaining reductions are internal, so that t −→h u −→i h. But since h is in hnf, u shall also be in hnf (otherwise h would contain a head pre redex) and t −→h u is the (finite) head reduction path for t. Lemma 4. Let t, u ∈ ΣΛμ , α ∈ VS , x ∈ VT . – t has a hnf iff λx.t has a hnf; c – t ∈ ΣΛμ is solvable iff there exists a family (tx )x∈F VT (t) in ΣΛμ and a family → − of vectors of closed Λμ-terms ( tα )α∈F VS (t) and a closed stream applicative → − context S such that: S t {tx /x} (v) tα α/(v)α −→ λx.x; – t is solvable iff λx.t is solvable iff μα.t is solvable. – If t is unsolvable then (t)u, (t)α, λx.t, μα.t, t {u/x} and t {(v)uα/(v)α} are also unsolvable. c is solvable if, and only if, it has a hnf. Theorem 6 (Solvability). t ∈ ΣΛμ
Proof. ⇒ Suppose that t is solvable and S is a stream applicative context such that u = S[t ] −→ λx.x for t = λx1 . . . λxn .t the closure of t. Thus u has a hnf and t as well by lemma 4. → − → j − →.μα . . . . μα 1 k 1 l x ⇐ Suppose t −→ λ− 0 0 n−1 .λxn . . . xn .(xi ) t1 β1 . . . tm βm t . . . t . j − → 1 k Then if S = []u0 α0 . . . αn−1 un . . . un with ui = μγ1 . . . γm .λz1 . . . zl .λx.x, one gets S[t] −→ λx.x.
4
Böhm-like Trees for Λμ
In this section, we introduce Böhm-like trees for Λμ-calculus. By doing so, we aim at making clearer the connections between Λμ-calculus and transfinitary λ-calculi. Moreover, those Böhm trees (and their corresponding Nakajima Trees, Λn -NT) are promising in at least two directions:
144
A. Saurin
– getting more precise characterizations of separability for non-normalizing terms in the spirit of Barendregt-Dezani-Ronchi della Rocca results, semiseparability being characterized as compatibility of Nakajima trees; – developing a Böhm model for Λμ-calculus based on these Böhm trees. Moreover, we believe that these Böhm trees can be helpful in characterizing differences between languages by analyzing their characteristic ordinal. This might be a starting point for classifying the expressivity of those calculi by means of infinitary calculi (and to study the frontier between Λμ-calculus and λμ-calculus, that is between delimited and non-delimited control in CBN). 4.1
Stable Part of a Λμ-hnf
Definition 18. A Λμ-term t will be said in Stream head normal form, shnf, → − − → →.μα . . . λ− x→ when it is of the form h = λ− x 0 0 n .μαn .(y) t0 β0 . . . tm βm . Remark 5. Every Λμ-hnf t is ηS -equivalent to a shnf u. More precisely, if t is a hnf, there exists a shnf u such that u −→= ηS t (ie 0 or 1 reduction step). An important property of hnf in λ-calculus is the following: if t = λx1 . . . λxn .(y)t1 . . . tm , then m, n and y will remain identical on any βreduction sequence of term t: if t −→β u, then there exist u1 . . . um such that ti −→β ui , for any 1 ≤ i ≤ m and u = λx1 . . . λxn .(y)u1 . . . um . Such a property cannot be directly transfered to Λμ-calculus. Indeed, because → of fst -reduction, the size of vectors of variables − xi is not constant along βfst→ − reduction sequences from a Λμ-hnf h (actually the size of vectors ti is not constant either). The following example makes this clear: t = λx.μα.λy.(x)αyα −→fst λx.λx1 . . . xn .μα.λy.(x)x1 . . . xn αyx1 . . . xn α. However, one can find a corresponding property which is stated in the following proposition: c . Suppose that t −→βfst h with Proposition 1. Let t ∈ ΣΛμ → − − → h = λx0 .μα0 . . . μαn .λx1n+1 . . . xkn+1 .(y) t0 β0 . . . βm t1m+1 . . . tlm+1 , then n, m, k, l and y are characteristic of the head normal forms of t: if t −→βfst h with − → 1 k →β . . . β u1 h = λx μα . . . μα λx . . . x .(z)− u . . . ul then m = 0
0
n
n +1
n +1
0 0
m m +1
m +1
m , n = n , k = k , l = l and y = z. Moreover, if h −→βfst h , then h is
→ − − → −→ 1 →.λx − − →.μα . . . λ− −−→ 1 k −→ − l λ− x x→ 0 α0 n .λxαn .μαn λxn+1 . . . xn+1 .(y) t0 xβ0 β0 . . . tm xβm βm t m+1 . . . t m+1 0
→ → − l − l 1 with tj = t1j . . . tjj , tj = t j . . . t jj , for 0 ≤ j ≤ m and tkj {(u)− x→ αi αi /(u)αi , i ≤ n} k
−→βfst t j for 0 ≤ j ≤ m, 1 ≤ k ≤ lj or j = m + 1 and 1 ≤ k ≤ l. 4.2
Λμ-Böhm Trees
The previous property characterizes the stable information in a Λμ-hnf: up to possible fst -reductions, Λμ-head normal forms have essentially the same properties as λ-calculus hnf. In particular, to construct a Böhm-like tree structure for Λμ-calculus, one shall have an object that:
Standardization and Böhm Trees for Λμ-Calculus
145
– records the relevant information on Λμ-hnf: (n, k) for the head abstractions, the head variable y, and the pair (m, l) for the sons of the head; – that is invariant by fst -reduction. The following three observations lead us to the definition of the class of trees that will be called Λμ-Böhm trees: – applying a fst -reduction to a hnf does not modify the five characteristic elements of the hnf mentioned in proposition 1; – an arbitrary (finite) number of fst -reductions can be applied to any term containing a μ-abstraction so that in the head abstraction of a head normal form, a μ can be preceded by an arbitrary number of λ and in the argument branching, a stream application construction can be preceded by an arbitrary number of term applications; – any ordinal λ ∈ ω 2 is exactly characterized by a pair of natural numbers (n, k) such that λ = ω.n + k. Definition 19 (Λμ-BT). Böhm trees for Λμ-calculus (B ∈ Λμ-BT) are (coinductively) defined as follows: B ::= Ω | Λ(xi )i∈μ∈ω2 .(y)(Bj )j∈λ∈ω2 One can now associate a Λμ-BT to any (closed) Λμ-term thanks to the following definition (we assume that for each β ∈ VS we have (xiβ )i∈ω )): Definition 20. We define BTΛμ : ΣΛμ → Λμ-BT as: – BTΛμ (t) := Ω if t is unsolvable; – BTΛμ (t) := Λ(zi )i∈μ .(y)(BTΛμ (uj ))j∈λ → − −−− → →μα . . . μα λ− −− → if t −→ λ− x 1 1 n xn+1 .(y) t1 β1 . . . βm tm+1 → l → = x1 . . . xkp if 1 ≤ p ≤ n + 1, and − with − x tp = t1p . . . tpp if 1 ≤ p ≤ m + 1 p p p and with μ = ω.n + kn+1 and λ = ω.m + lm+1 , xj+1 if 0 ≤ p ≤ n, 0 ≤ j < kp+1 p+1 • zω.p+j = j−kp+1 xαp+1 if 0 ≤ p < n, kp+1 ≤ j < ω j+1 tp+1 if 0 ≤ p ≤ m, 0 ≤ j < lp+1 • uω.p+j = j−l xβp+1p+1 if 0 ≤ p < m, lp+1 ≤ j < ω Remark 6. The Böhm tree of a Λμ-term can also be obtained using direct approximants and completely (that is, infinitely) developing the μ-abstractions thanks to fst. Example 3. Let t = μα.λx.μβ.λy.((x)y ((Δ)Δ)β) β. BTΛμ (t) = Λ(zi )i∈ω.2+1 .(zω )(Bj )j∈ω with B0 = zω.2 , B1 = Ω and Bj+1 = zω+j for 1 ≤ j < ω.
146
4.3
A. Saurin
Nakajima Trees for Λμ-Calculus
We give a brief account of Nakajima Trees for Λμ-calculus. Nakajima Trees are infinite η-expanded Böhm trees. One can associate to any closed Λμ-term an infinite tree called a Nakajima tree: Definition 21 (Λμ-NT). Nakajima trees for Λμ-calculus (N ∈ Λμ-NT) are (coinductively) defined as follows: N ::= Ω | Λ(xi )i∈ω2 .(y)(Nj )j∈ω2 . One can associate a Λμ-NT to a Λμ-term can considering its stream head normal forms (see definition 18) and the infinite ηS -expansions of this shnf, associating an element of Λμ-NT in much the same way as in 4.2. Two Nakajima trees N1 and N2 are said compatible if given any path p in the trees (that is a finite word over the alphabet ω 2 ), either the subtrees at p have the same head variable y or there exists a prefix p of p such that one of the trees is equal to Ω at p . We have: Theorem 7. Given two closed Λμ-terms u, v, there exists a stream applicative context S such that S[u] −→Λμ λx.λy.x and S[v] −→Λμ λx.λy.y if, and only if, NTΛμ (u) and NTΛμ (v) are not compatible. 4.4
A New Look at the Failure of Separation in λμ-Calculus
Parigot’s λμ-calculus is a subset of Λμ-terms defined by the following grammar: t, u ::= x | λx.t | (t)u | μα.(t)β It is known that separation property fails in λμ-calculus. Λμ-Böhm trees provide a new look at this fact. Indeed, Λμ-BT for λμ-terms have (up to finitely many ηS -expansions, which corresponds to replacing hnf by shnf in the definition of the Böhm trees) the following particularly constrained shape: Bλμ ::= Ω | x | Λ(xi )i∈ω .(y)(Bλμ j )j∈ω One can observe that there is no freedom on the arity in these Böhm trees: the index is always ω and the arities are thus forced to match. This is, in our opinion, the deep reason for the failure of separation in λμ-calculus. Below, we show a classification of several calculi (or logical formalisms) depending on – whether they have an arity matching constraint1 , – whether they are linear or not2 and – whether they have separation or not. 1 2
By arity matching, we mean the fact that a function of arity n is always fed with the expected number of arguments, examples other than Bλμ are ABT or Ludics. Linearity denotes here the constraint to have only a single occurrences of each variables and the impossibility to reuse variables multiple times. Ludics is an example of a linear framework.
Standardization and Böhm Trees for Λμ-Calculus
147
ABT denotes Curien’s Abstract Böhm Trees [6], CP S∞ denotes an infinitary language studied by Streicher and Loew [22] and Λn the languages of the Stream hierarchy [34]. Interestingly one observes that Separation fails when arities match and the calculus is non-linear. calculus λ-calculus λμ-calculus Λμ-calculus ABT CPS∞ Ludics (Λn )n∈ω
5
arity matching linear separation (ref) no no yes [5] yes no no [8] no no yes [30] yes no no [23] yes no no [22] yes yes yes [12] no no yes [34]
Conclusion, Perspectives and Future Works
Contributions of the Paper. In the present paper, we introduced a notion of standard reductions for Λμ-calculus for which a standardization theorem (as well as a strong standardization theorem) holds. We then characterized solvability in Λμ-calculus thanks to the results on standardization. This allowed us to introduce a class of Λμ-Böhm trees as transfinitary wide generalization of usual Böhm trees for λ-calculus. Those Böhm trees shed an interesting perspective on separation and non-separation in λμ/Λμ as well as in other calculi. Those contributions are important in at least three directions: – they make clear our understanding of head normal forms in Λμ-calculus and allow for a more precise analysis of reductions in Λμ; – they clarify the connections between Λμ and infinitary λ-calculi; – they open new perspectives for model-theoretic investigations of Λμ-calculus. Perspectives and Future Works. The present work is connected with several other works: – Lassen [21] provided a study of bisimulation relations in Λμ-calculus in which he proposed head normal forms for Λμ-calculus as well as a characterization of solvability using this bisimilarity. Lassen’s hnf are slightly different from the hnf we discussed here but, since solvability is characterized in terms of these hnf, they must be related to ours. We shall investigate this point; – Loew [22] studied an infinitary version of SP CF , SP CF∞ , as well as an infinitary target language, CP S∞ . In particular, separation fails in CP S∞ . Interestingly, infinite normal forms of CP S∞ are precisely λμ-BT. Several other directions for future are exciting: – We shall build a model of Λμ-calculus based on Λμ-BT; – Not all Λμ-BT is the Böhm tree of a Λμ-term (a phenomenon which is already observed with Nakajima trees for λ-calculus). We shall characterize those Λμ-Böhm trees which are the image of a Λμ-term;
148
A. Saurin
– Λμ-BT can be generalized to Böhm trees for the Stream hierarchy [34]: Bn ::= Ω | Λ(xi )i∈μ∈ωn+1 .(y)(Bnj )j∈λ∈ωn+1 (this extends the definition for Λμ-Böhm trees (n = 1) as well as for λ-Böhm trees (n = 0).) – The geometry of the Böhm trees introduced in the present paper seems to say a lot about the relationships between various languages. In particular, we plan to investigate the frontier between delimited and non-delimited control thanks to these tools. Acknowledgments. The author wishes to thank Simona Ronchi della Rocca as well as Kazushige Terui for helpful discussions on the material of this paper. Many thanks to FLOPS referees for their very precise and detailed comments.
References 1. Ariola, Z., Herbelin, H.: Minimal classical logic and control operators. In: Baeten, J.C.M., Lenstra, J.K., Parrow, J., Woeginger, G.J. (eds.) ICALP 2003. LNCS, vol. 2719, pp. 871–885. Springer, Heidelberg (2003) 2. Baba, K., Hirokawa, S., etsu Fujita, K.: Parallel reduction in type free lambda/mucalculus. Electronic Notes in Theoretical Computer Science 42 (2001) 3. Berarducci, A.: Infinite lambda-calculus and non-sensible models. In: Ursini, A., Aglianò, P. (eds.) Logic and Algebra (Pontignano 1994). Lecture Notes in Pure and Applied Mathematics Series, vol. 180, pp. 339–378. Marcel Dekker Inc., New York (1996) 4. Berarducci, A., Dezani, M.: Infinite lambda-calculus and types. TCS 212(1-2), 29– 75 (1999) 5. Böhm, C.: Alcune proprietà delle forme βη-normali nel λK-calcolo. Publicazioni dell’Istituto per le Applicazioni del Calcolo, 696 (1968) 6. Curien, P.-L., Herbelin, H.: Computing with abstract Böhm trees. In: Third Fuji International Symposium on Functional and Logic Programming, Kyoto. World Scientific, Singapore (1998) 7. Danvy, O., Filinski, A.: Abstracting control. In: LISP and Functional Programming, pp. 151–160 (1990) 8. David, R., Py, W.: λμ-calculus and Böhm’s theorem. Journal of Symbolic Logic (2001) 9. Dezani, M., Severi, P., de Vries, F.-J.: Infinitary lambda calculus and discrimination of Berarducci trees. TCS 2(298), 275–302 (2003) 10. Felleisen, M., Friedman, D.P., Kohlbecker, E.E., Duba, B.F.: A syntactic theory of sequential control. TCS 52, 205–237 (1987) 11. Filinski, A.: Representing monads. In: Conf. Record 21st ACM SIGPLAN-SIGACT Symp. on Principles of Programming Languages, POPL 1994, Portland, OR, USA, January 17-21, pp. 446–457. Association for Computing Machinery (1994) 12. Girard, J.-Y.: Locus solum. Mathematical Structures in Computer Science 11, 301– 506 (2001) 13. Griffin, T.: A formulae-as-types notion of control. In: Principles of Programming Languages. IEEE Computer Society Press, Los Alamitos (1990)
Standardization and Böhm Trees for Λμ-Calculus
149
14. Herbelin, H., Ghilezan, S.: An approach to call-by-name delimited continuations. In: Principles of Programming Languages, ACM Sigplan (January 2008) 15. Howard, W.A.: The formulae-as-type notion of construction. In: Seldin, J.P., Hindley, R. (eds.) To H. B. Curry: Essays in Combinatory Logic, Lambda Calculus, and Formalism, pp. 479–490. Academic Press, New York (1980) 16. Kennaway, R., Klop, J.W., Sleep, M.R., de Vries, F.-J.: Infinitary lambda calculi and böhm models. In: Hsiang, J. (ed.) RTA 1995. LNCS, vol. 914, pp. 257–270. Springer, Heidelberg (1995) 17. Kennaway, R., Klop, J.W., Sleep, M.R., de Vries, F.-J.: Infinitary lambda calculus. TCS 175(1), 93–125 (1997) 18. Kiselyov, O.: Call-by-name linguistic side effects. In: ESSLLI 2008 Workshop on Symmetric calculi and Ludics for the semantic interpretation (2008) 19. Klop, J.W.: Combinatory Reduction Systems. Ph.D. thesis, State University of Utrecht (1980) 20. Krivine, J.-L.: Lambda-calculus, Types and Models. Ellis Horwood (1993) 21. Lassen, S.: Head normal form bisimulation for pairs and the λμ-calculus. In: Logic In Computer Science. IEEE Computer Society Press, Los Alamitos (2006) 22. Loew, T.: Locally Boolean Domains and Universal Models for Infinitary Sequential Languages. PhD thesis, Darmstadt University (2006) 23. Maurel, F.: Un cadre quantitatif pour la Ludique. PhD thesis, Université Paris VII (2004) 24. Pagani, M., Saurin, A.: Stream associative nets and Λμ-calculus. Technical Report 6431, INRIA (January 2008) 25. Parigot, M.: Free deduction: An analysis of “computations" in classical logic. In: Voronkov, A. (ed.) RCLP 1990 and RCLP 1991. LNCS, vol. 592, pp. 361–380. Springer, Heidelberg (1992) 26. Parigot, M.: λμ-calculus: an algorithmic interpretation of classical natural deduction. In: Voronkov, A. (ed.) LPAR 1992. LNCS, vol. 624, pp. 190–201. Springer, Heidelberg (1992) 27. Parigot, M.: Strong normalization for second order classical natural deduction. In: Vardi, M. (ed.) Eighth Annual Symposium on Logic in Computer Science, pp. 39–46. IEEE, Los Alamitos (1993) 28. Parigot, M.: Proofs of strong normalisation for second order classical natural deduction. Journal of Symbolic Logic 62(4), 1461–1479 (1997) 29. Py, W.: Confluence en λμ-calcul. PhD thesis, Université de Savoie (1998) 30. Saurin, A.: Separation with streams in the Λμ-calculus. In: Logic In Computer Science, Chicago, pp. 356–365. IEEE Computer Society Press, Los Alamitos (2005) 31. Saurin, A.: On the relations between the syntactic theories of λμ-calculi. In: Kaminski, M., Martini, S. (eds.) CSL 2008. LNCS, vol. 5213, pp. 154–168. Springer, Heidelberg (2008) 32. Saurin, A.: Une étude logique du contrôle, appliquée à la programmation fonctionnelle et logique. PhD thesis, École Polytechnique (September 2008) 33. Saurin, A.: Typing streams in the Λμ-calculus. ACM Transactions on Computational Logic (2009) (to appear) 34. Saurin, A.: A hierarchy for delimited continuations in call-by-name. In: Ong, L. (ed.) FOSSACS 2010. LNCS, vol. 6014, pp. 374–388. Springer, Heidelberg (2010)
An Integrated Distance for Atoms Vicent Estruch, C´esar Ferri, Jos´e Hern´andez-Orallo, and M. Jos´e Ram´ırez-Quintana DSIC, Univ. Polit`ecnica de Val`encia Cam´ı de Vera s/n, 46020 Val`encia, Spain {vestruch,cferri,jorallo,mramirez}@dsic.upv.es
Abstract. In this work, we introduce a new distance function for data representations based on first-order logic (atoms, to be more precise) which integrates the main advantages of the distances that have been previously presented in the literature. Basically, our distance simultaneously takes into account some relevant aspects, concerning atom-based presentations, such as the position where the differences between two atoms occur (context sensitivity), their complexity (size of these differences) and how many times each difference occur (the number of repetitions). Although the distance is defined for first-order atoms, it is valid for any programming language with the underlying notion of unification. Consequently, many functional and logic programming languages can also use this distance. Keywords: First-order logic, distance functions, similarity, knowledge representation.
1
Introduction
Distances (also called metrics) pervade computer science as a theoretical and practical tool to evaluate similarity between objects. The definition of a distance over a set of objects allows us to consider this set as a metric space. This gives us a repertoire of tools and methods to work and analyse the objects therein. Hence, there has been a considerable effort to define distances for any kind of object, including complex or highly structured ones, such as tuples, sets, lists, trees, graphs, images, sounds, web pages, ontologies, XML documents, etc. The notion of distance between objects allow us to reason about the amount of transformations needed to go from one object to another (and viceversa). A distance is also a frequent formalisation of the notion of error: it is not the same to output 3.3 instead of 3.4 than to output 3.3 instead of 15.2. In the area of programming languages the notion of distance is still an awkward concept, since objects which are (syntactically) different are just that, different objects, and there is not much interest in measuring how much different they are. However, potential applications of the use of distances exist in the areas of debugging (as a measure of the magnitude of the error), termination (to find similar traces or similar rewriting terms), program analysis (to find similar parts in the code M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 150–164, 2010. c Springer-Verlag Berlin Heidelberg 2010
An Integrated Distance for Atoms
151
that could be generalised), and program transformation (to approximate the distance between two terms). At a meta-level, the use of functional languages to implement distances has been vindicated by [1]. Functional and logic programming languages, additionally, have many important applications as languages for object (knowledge) representation. Logic, and logic programming in particular, is one of the most common formalisms to represent (relational) knowledge. Likewise, functional programming is becoming more and more usual too as a knowledge representation formalism, especially with the use of XML documents and related functional-alike structures [4]. The use of distances in the area of knowledge representation is commonplace. Some of the areas where functional and logic programming have profusely been used as a knowledge representation formalism are machine learning and program synthesis, in intersecting areas known as Inductive Logic Programming [17][13], Inductive Functional Logic Programming [9][10][6] or, more generally, Inductive Programming [8]. Given that Inductive Programming overlaps machine learning, there has been a remarkable interest in upgrading learning techniques to deal with programbased representations. Among them, we find the so-called instance or distancebased methods. The great advantage of these methods is that the same algorithm or technique can be applied to different sorts of data, as long as a similarity function has previously been defined over them [16]. It is widely-known that the performance of these methods depends to a great extent on the similarity function employed. Thus, it is convenient that the similarity functions satisfy some well-defined properties such as positive definiteness or triangular inequality in order to ensure consistent results [20]. Overall, this makes distance functions a very appropriate tool to express (dis)similarity. There has been a considerable effort to derive distances for any datatype, including complex or structured datatypes. Hence, we find distances for sets, lists, trees, graphs, etc. One challenging case in machine learning, but more especially in the area of functional and logic programming, is the distance between first-order atoms and terms. Although atoms and terms can be used to represent many of the previous datatypes (and consequently, a distance between atoms/terms virtually becomes a distance for any complex/structured data), they are specially suited for term-based or tree-based representations. In this way, distances between atoms are not only useful in the area of inductive logic programming (ILP) [17] (e.g. first-order clustering [3]), but also in other areas where structured (hierarchical) information is involved such as learning from ontologies or XML documents. For instance, if an XML document represents a set of cars, or houses, or customers, we may be interested in obtaining the similarities between the objects, or to cluster them according to their distances. However, it is important to remark that a distance between atoms or terms is not the same as a distance between trees (such as many other introduced in the literature, see, e.g., the Bille’s survey [2]), since two subterms are just different when the topmost element of their tree representation is different, while this is not generally the case for trees.
152
V. Estruch et al.
In this work, we introduce a new distance between ground terms and atoms, which integrates the advantages that some of the existing distances between atoms have separately. In particular, we recover the context sensitivity of Nienhuys-Cheng’s distance [18], which implies that the distance between two atoms depends not only on their syntactic differences but also on the positions where these differences take place. This becomes crucial for many applications where atoms represent hierarchical information (e.g. an XML document). Additionally, and like Nienhuys-Cheng’s distance, our distance is also a normalised function, which is an interesting property to reduce the effect caused in the distance by noisy or irrelevant information [15], and can easily be composed with other distances in order to define metrics for more complex representations. However, Nienhuys-Cheng’s proposal shows some disadvantages in that it does not properly deal with repeated differences between atoms, which is indeed a common property when handling this datatype, and also ignores the syntactic complexity of these differences. This is considered by J. Ramon et al. distance [20] but at the expense of disregarding context-sensitivity, normalisation and composability. Our approach does consider repetitions and complexity as J. Ramon et al. do, but in a different way which allows us to preserve context-sensitivity, normalisation and composability. This is so because we do not need to rely on the least general generalisation operator (lgg) [19] in order to manage repeated differences. This paper is organised as follows. Section 2 introduces the notation and some previous definitions that will be used in the following sections. Section 3 reviews and analyses these two previous distances proposed in the literature which are related to our proposal. Our distance between ground terms/atoms is formally defined in Section 4. An illustrative example is presented in Section 5 in order to compare how our distance works in practice wrt. the two aforementioned distances. Section 6 concludes the paper and relates to future work. Finally, note that an important part of our work deals with proving that our proposal agrees all the axioms a distance function is supposed to satisfy. Although the main result is included in this work, the proofs of the main theorem and auxiliary results can be found in [5].
2
Preliminaries
Let L be a first order language defined over the signature Σ = C, F , Π where C is a set of constants, and F (respectively Π) is a family indexed on N (non negative integers) being Fn (Πn ) a set of n−adic function (predicate) symbols. Atoms and terms are constructed from the Σ as usual. An expression is either a term or an atom. The root symbol and the arity of an expression t is given by the functions Root(t) and Arity(t), respectively. Thus, letting t = p(a, f (b)), Root(t) = p and Arity(t) = 2. By considering the usual representation of t as a labelled tree, the occurrences are finite sequences of positive numbers (separated by dots) representing an access path in t. We assume that every occurrence
An Integrated Distance for Atoms
153
is always headed by a (implicit) special symbol λ, which denotes the empty occurrence. The set of all the occurrences of t is denoted by O(t). In our case, O(t) = {λ, 1, 2, 2.1}. We use the (indexed) lowercase letters o , o, o1 , o2 , . . . to represent occurrences. The length of an occurrence o, Length(o), is the number of items in o (λ excluded). For instance, Length(2.1) = 2, Length(2) = 1 and Length(λ) = 0. Additionally, if o ∈ O(t) then t|o represents the subterm of t at the occurrence o. In our example, t|1 = a, t|2 = f (b), t|2.1 = b. In any case, we always have that t|λ = t. By P re(o), we denote the set of all prefix occurrences of o different from o. For instance, P re(2.1) = {λ, 2}, P re(2) = {λ} and P re(λ) = ∅. Two expressions s and t are compatible (denoted by the Boolean function Compatible(s, t)) iff Root(s) = Root(t) and Arity(s) = Arity(t). Otherwise, we say that s and t are incompatible (¬Compatible(s, t)).
3
Related Work
As mentioned in the introduction, although distances for atoms can be used for many applications, only two relevant proposals have been generally used to compute distances between atoms. In [18], Nienhuys-Cheng introduces a bounded distance for ground terms/atoms which takes the depth of the symbol occurrences into account in such a way that differences occurring close to the root symbols count more. Given two ground terms/atoms s = s0 (s1 , . . . , sn ) and t = t0 (t1 , . . . , tn ), this distance (denoted by dN ) is recursively defined as follows. ⎧ if s = t ⎨ 0, if ¬Compatible(s, t) dN (s, t) = 1, ⎩ 1 n d(s , t ), otherwise i i i=1 2n For instance, if s = p(a, b) and t = p(c, d) then dN (s, t) = 1/4·(d(a, c)+d(b, d)) = 1/4(1 + 1) = 1/2. A different approach is presented by J. Ramon et al. in [21]. Following [11], the authors define a distance between (non-)ground terms/atoms based on the syntactic differences wrt. their lgg. An auxiliary function, the so-called Size(t) = (F, V ), is required to compute this distance. Roughly speaking, F counts the number of predicate and function symbols occurring in t and V is the sum of the squared frequency of appearance of each variable in t. Finally, this distance (denoted by dR ) is formulated as follows. Given two terms/atoms s and t, dR (s, t) = [Size(s) − Size(lgg(s, t))] + [Size(t) − Size(lgg(s, t))] Thus, one of its particularities is that dR returns an ordered pair of integer values (F, V ) instead of a single value, that expresses how different two atoms are in terms of function and variable symbols, respectively. For instance, if s = p(a, b) and t = p(c, d) and knowing that lgg(s, t) = p(X, Y ), we have Size(s) = (3, 0) Size(t) = (3, 0) Size(lgg(s, t)) = (1, 2) dR (s, t) = [(3, 0) − (1, 2)] + [(3, 0) − (1, 2)] = (2, −2) + (2, −2) = (4, −4)
154
V. Estruch et al.
With regard to these distances for atoms, some interesting properties are analysed next. 1. Context Sensitivity: it is the possibility of taking into consideration where the differences between two terms/atoms occur. Intuitively, it means that the distance between two atoms such as p(a) and p(b) should be greater than the distance between p(f (a)) and p(f (b)), as these latter atoms have more symbols (information) in common than the two previous ones. Or equivalently, symbolic differences occurring at deeper positions count less since they provide less information. The Nienhuys-Cheng’s distance does not always satisfy this property. Note that, by definition, the distance between atoms decreases as the differences occur at deeper positions. For instance, in our example: dN (p(a), p(b)) = 1/2 dN (p(f (a)), p(f (b))) = 1/4 Nevertheless, when differences occur at the same depth but in all of the arguments of two terms/atoms (that is, the number of differences coincides with the arity of the outermost symbol) then the distance behaves as if there was only one difference. For instance, given the atoms e1 = p(f (a1 , . . . , an ), g(a)), e2 = p(f (b1 , . . . , bn ), g(a)) and e3 = p(f (a1 , . . . , an ), g(b)) it would be expected that the distance between e1 and e2 were greater than the distance between e1 and e3 , since there are n differences between e1 and e2 whereas there is only one difference between e1 and e3 . However, dN (e1 , e2 ) = 1/8 dN (e1 , e3 ) = 1/8 As we have mentioned, the first component F of the J. Ramon et al.’s distance counts the differences between the functors of the two atoms. This component can be context-sensitive by giving different importance to components in different positions. The authors do that by associating a set of (m + 1) positive weights with each functor f ∈ Fm and a set of (n + 1) positive weights with each predicate p ∈ Πn . In this way, the definition of the F -component is parametrised by these weights (see Definition 4 in [21]). Thus, J. Ramon et al.’s distance is not always context-sensitive (depending on the weights used). 2. Normalisation: sometimes, it is useful to work with normalised distances. In this sense, a distance function d which returns (non-negative) real numbers can be easily normalised, for instance, using the expression d/(1 + d), which is known that results in an equivalent distance [12]. However, a distance like that of J. Ramon et al. is very difficult (or at least, not intuitive) to be normalised since it returns a pair of integer numbers. 3. Repeated differences: this concerns the fact of handling repeated differences between terms/atoms properly. Suppose that the atoms r = p(a, a), s = p(b, b) and t = p(c, d) are given. Intuitively, it is reasonable to expect that the atoms r and s come nearer than the atoms r and t (or s and t), since r and s share that their (sub)terms (a and b, respectively) occur twice whereas no (sub)term is repeated in t.
An Integrated Distance for Atoms
155
Only J. Ramon et al.’s distance can handle repetitions since this distance is defined via the lgg operator, which takes repetitions into consideration. However, this possibility is lost when the Nienhuys-Cheng’s distance is used. In our example, dN (r, s) = 1/2 dN (r, t) = 1/2 dR (r, s) = (2, −2) dR (r, t) = (3, −4) 4. Size of the differences: another interesting question to be treated is the complexity (the size) of the differences occurring when two terms/atoms are compared. Logically, it is expected that as the size of two terms/atoms increases, its distance will become greater. This is so regarding the J. Ramon et al.’s proposal, since it explicitly introduces a size function. However, NienhuysCheng’s disregards this important fact. For instance, given the atoms p(a), p(b) and p(f (c)) then, dN (p(a), p(b)) = 1/2 dN (p(a), p(f (c))) = 1/2 dR (p(a), p(b)) = (2, −2) dR (p(a), p(f (c))) = (3, −2) 5. Handling variables: variables become a useful tool when part of the structure of an object is missing. J. Ramon et. al’s proposal handles both constant and variable symbols indistinctly in a very elegant way. As seen, NienhuysCheng’s distance is defined over ground terms/atoms and for this reason needs some non-integrated extra concepts (least Herbrand model and Hausdorff distance) in order to deal with variable symbols. 6. Composability: A tuple is a widely used structure for knowledge representation in real applications, since examples are usually represented as tuples of values of different data types (nominal, numerical, atoms, graphs, . . .). For example, a molecule can be described as a tuple composed by its breaking temperature (a real number) and its description (expressed, for instance, as a list of symbols). The property of composability allows us to define distance functions for tuples by combining the distance functions defined over the basic types from which the tuple is constructed. Typically, the combination is made as a linear combination of the underlying distances, which is well-known to be a distance. Therefore, composability requires that the computed distances are expressed as real values in order to combine them. Obviously, the Nienhuys-Cheng’s distance holds this condition. However, the J. Ramon et. al’s distance computes a pair of numbers, so it is necessary to first transform it into a real number before composing it. And, as we have mentioned, converting J.Ramon et al’s distance into a single number seems difficult. 7. Weights: in some cases, it may be convenient to give higher or lower weights to some constants or function symbols, in such a way that the distance between f (a) and f (b) could greater than the distance between f (c) and f (d). In some other occasions it may be interesting to give more or less weight to specific positions in a term over others. This latter case is more general than the first one. Nienhuys-Cheng’s distance does not allow weights while
156
V. Estruch et al.
Table 1. Advantages and drawbacks of several distances between terms/atoms
Context N ormalisation Repetitions Size V ariables Composability W eights
Nienhuys-Cheng Not always Yes No No Indirectly Yes No
J. Ramon et al. Our distance Not always Yes (depending on the weights used) Not easy Yes Yes Yes Yes Yes Yes Indirectly Difficult Yes Yes Indirectly
J.Ramon et al.’s does. Our proposal allows this possibility in an indirect way, by the use of dummy function symbols. For instance, the distance between f (a) and f (b) can be increased if we just rewrite them into f (d1 (d2 (a))) and f (d1 (d2 (b))). The weight depends on the number of dummy symbols which have been introduced. This analysis about Nienhuys-Cheng’s and J. Ramon et al.’s distances suggests to integrate both in a new distance that inherits the best of them. Table 1 compares these three distances in terms of the properties above: that is, contextsensitivity (Context), ease of normalisation (N ormalisation), handling repeated differences (Repetitions), complexity of the differences (Size), handling variable symbols (V ariables) and the ability to be combined with other distances (Composability). In the table, we have also included which of these properties are satisfied and which are not by the distance we will define in the next section. In the following sections we will show how these issues are accomplished in a novel way, by: i) understanding terms/atoms as rooted acyclic directed graphs, ii) introducing a new size function which is iii) weighted depending on the context and the number of times the differences occur.
4
Distance between Atoms
As said, the distance function we present in this work takes into consideration three fundamental issues concerning first-order atoms: namely, the complexity of the syntactic differences between the atoms, the number of times each syntactic difference occurs and finally, the position (or context) where each difference takes place. This all is formalised next. First, we precisely define what we mean by syntactical differences between expressions. Definition 1. (Syntactical differences between expressions) Let s and t be two expressions, the set of their syntactic differences, denoted by O (s, t), is defined as: O (s, t) = {o ∈ O(s) ∩ O(t) : ¬Compatible(s|o , t|o ) and Compatible(s|o , t|o ), ∀o ∈ P re(o)}
An Integrated Distance for Atoms
157
For instance, with s = p(f (a), h(b), b) and t = p(g(c), h(d), d), then O (s, t) = {1, 2.1, 3}. Additionally, observe that if ¬Compatible(s, t) then O (s, t) = {λ}. The complexity of the syntactic differences between s and t is calculated on the number of symbols the subterms (in s and t) at the occurrences o ∈ O (s, t) are composed of. For this purpose, we introduce a special function called Size which is defined next. Definition 2. (Size of an expression) Given an expression t = t0 (t1 , . . . , tn ), we define the function Size (t) = 14 Size(t) where, 1, n =0 n Size(t0 (t1 , . . . , tn )) = Size(ti ) 1 + i=1 ,n>0 2(n+1) For instance, considering s = f (f (a), h(b), b), then Size(a) = Size(b) = 1, Size(f (a)) = Size(h(b)) = 1+1/4 = 5/4, Size(s) = 1+(5/4+5/4+1)/8 = 23/16 and finally, Size(s) = 23/64. The rationale for the denominator in definition 2 is that we expect to have that Size(p(a)) < Size(p(a, b, c)). Consequently, the denominator has to be greater than 2n in order to avoid a cancellation with the size of the arguments, as happens with Nienhuys-Cheng’s distance. The next step is devoted to find out repeated differences between atoms. To do this, we define an equivalence relation (∼) on the set O (s, t), as follows: ∀oi , oj ∈ O (s, t), oi ∼ oj ⇔ s|oi = s|oj and t|oi = t|oj Consequently, there exists a non-overlapping partition of O (s, t) into equivalence classes, that is, O (s, t) = ∪i∈I Oi (s, t). Related to this, we also introduce the auxiliary function π : O (s, t) → I which just returns the index of the equivalence class one occurrence belongs to. Back to our example, we can see that there only exist two equivalence classes: namely, O1 (s, t) = {1} and O2 (s, t) = {2.1, 3}. Hence, π(1) = 1, π(2.1) = π(3) = 2. Finally, we need to set the context of every syntactic difference. This is formalised as follows: Definition 3. (Context value of an occurrence) Let t be an expression. Given an occurrence o ∈ O(t), the context value of o in t, denoted by C(o; t), is defined as 1, o = λ C(o; t) = 2Length(o) · ∀o ∈P re(o) (Arity(t|o ) + 1), otherwise For instance, C(λ; t) = 1, C(1; t) = 2 · (3 + 1) = 8 and C(2.1; t) = 22 · (1 + 1) · (3 + 1) = 32. Therefore, the context value tells us about the relationship between t|o and t in the sense that, a high value of C(o; t) corresponds to a deep position of t|o in t or the existence of superterms of t|o with a large number of arguments. As we will see later, this information will be employed to conveniently weight the syntactic differences between atoms. The context value of an occurrence satisfies the following property.
158
V. Estruch et al.
Proposition 1. Given two expressions s and t, if o ∈ O (s, t) then C(o; s) = C(o; t) Proof. It directly comes from the definition of O (s, t). If o ∈ O (s, t) then, for any o ∈ P re(o), Compatible(s|o , t|o ), hence Arity(s|o ) = Arity(t|o ). When no doubts arise from omitting t, the short form C(o) will be used instead. Definition 3 allows us to set an order relation (≤) in every equivalence class Oi (s, t). That is, ∀oj , ok ∈ Oi (s, t), oj ≤ ok ⇔ C(oj ) ≤ C(ok ) Note that the relation order ≤ makes sense on the grounds of Proposition 1. Additionally, for every ordered equivalence class, (Oi , ≤), we define the function fi : (Oi , ≤) → N+ that simply returns the position an occurrence o ∈ Oi has according to ≤. In the case of C(oi ) = C(oj ), we can rank first either oi or oj , since as we will see, this decision will not affect the computation of the proposed distance. We still require, previously to introduce our distance, another additional function. Again, given two expressions s and t, we define the function w as: w : O (s, t) → R+ o → w(o) =
3fi (o)+1 4fi (o) ,
where i = π(o)
Note that the function w simply associates weights to occurrences in such a way that the greater C(o), the lower the weight o is assigned, i.e., the less meaningful the syntactical difference referred by o is. For instance, if we consider (O2 (s, t), ≤) = {3, 2.1} then w(3) = 1 and w(2.1) = 7/8. By wO (o), we will denote the restriction of the function w(·) to a subset O ⊂ O (s, t). Realise that if o ∈ O ⊂ O (s, t), then wO (o) ≥ w(o). Finally, the distance between atoms we propose in this work is defined as: Definition 4. (Distance between atoms) Let s and t be two expressions, the distance between s and t is, d(s, t) =
o∈O (s,t)
w(o)
Size(s|o ) + Size (t|o ) C(o)
Theorem 1. The ordered pair (L,d) is a bounded metric space. Concretely, 0 ≤ d ≤ 1. Proof. For any expressions r, s and t in L, the function d satisfies: 1. (identity): d(r, t) = 0 ⇔ r = t. If d(r, t) = 0 then O (r, t) = ∅ which necessarily means that r = t. As for the other implication, if r = t then O (r, t) = ∅ and d(r, t) = 0. 2. (symmetry): d(r, t) = d(t, r). Simply, note that O (r, t) = O (t, r) 3. (triangular inequality): d(r, t) ≤ d(r, s) + d(s, t). See [7]. 4. (bounded distance): 0 ≤ d(r, t) ≤ 1. See [7].
An Integrated Distance for Atoms
159
Next, we provide short artificial examples illustrating how our distance works. Example 1. s = f (a) and t = a. We have that O (s, t) = {λ}. Next, C(λ) = 1 The sizes of the subterms involved in the computation of the distance are: Size (f (a)) = 5/16 and Size (a) = 1/4 Obviously, w(λ) = 1 Finally, d(s, t) =
5 1
1 Size(s) + Size (t)) = + 1 16 4
Example 2. s = p(a, a) and t = p(f (b), f (b)). We have that O (s, t) = {1, 2}. Next, C(1) = C(2) = 2 · (2 + 1) = 6 The sizes of the subterms involved in the computation of the distance are: Size (a) = 1/4 and Size(f (b)) = 5/16 There is only one equivalence class O = O1 (s, t). Assume that the occurrence 1 is ranked first, w(1) = 1 and w(2) = 7/8 Finally, d(s, t) =
1 1 5 7 1 5 + + + 6 4 16 48 4 16
Example 3. s = p(a, a, f (c)) and t = p(b, b, f (b)). We already know that O (s, t) = {1, 2, 3.1}, The contexts respective differences are, C(1) = C(2) = 21 · (3 + 1) = 8 and C(3.1) = 22 · (1 + 1) · (3 + 1) = 32 The sizes of the subterms involved in the computation of the distance are: Size (a) = Size(b) = Size (c) = 1/4 Additionally, we have seen that (O1 (s, t), ≤) = {1}, (O2 (s, t), ≤) = {2, 3.1}. Consequently, w(1) = 1, w(3.1) = 1 and w(2) = 7/8 Finally, d(s, t) =
1 1 1 7 1 1 1 1 1 + + + + + 8 4 4 64 4 4 32 4 4
160
5
V. Estruch et al.
Discussion
Next, we present a simple but illustrative comparison on how our distance performs wrt. those distances reported in Section 3. Basically, we aim to analyse the notion of similarity shaped by the different distances. For this purpose, we will use a toy XML dataset containing several car descriptions (see Table 2) and we will see how similar these descriptions are depending on the distance employed. Our XML dataset contains structured information about 8 different cars. More concretely, for every car, we know the company, the model, a list of certifications that some organisations have granted to the car, the engine and several other features. As we can see, every description can directly be represented as an atom, except from some attributes: the photo which cannot be properly represented, and two numerical values (the power and the baseprice) which can be represented inside an atom, but that any of the atom distances is not going to handle appropriately (directly). Table 3 shows a term-based representation of the whole dataset. Ignoring the photo and the two numerical values for the moment, we see that if we focus on cars 1, 2 and 3, we intuitively see that the car 1 looks more similar to the car 2 than 1 to 3, although, both pairs of cars (1, 2) and (1, 3) have an identical number of differences. Namely, the difference between the cars 1 and 2 relies on the engine traits (occurrences 4.3.1 and 4.3.2) whereas cars 1 and 3 differ both in company and model (occurrences 1 and 2). Therefore, the reason Table 2. A representative extract from the XML dataset
Chevrolet <model> Corvette E3 D52 RAC red abs <power> 250 full mid <engine> diesel yes 60,000 ChevCorv.jpg ...
An Integrated Distance for Atoms
161
Table 3. An equivalent term-based representation of the XML dataset 1 2 3 4 5 6 7 8
car(Ford,Ka,cert([E3]),feats(75, red,abs,ab(full,mid),mt(gas,no)), 9000, ChevKaG.jpg) car(Ford,Ka,cert([E3]),feats(80, red,abs,ab(full,mid),mt(diesel,yes)), 10000, ChevKaD.jpg) car(Chev,Corv,cert([E3]),feats(250,red,abs,ab(full,mid),mt(gas,no)), 60000, ChevCorv.jpg) car(Ford,Ka,cert([E3]),feats(100, blue,abs,ab(mid,mid),mt(diesel,yes)), 10000, ChevKaD2.jpg) car(Ford,Ka,cert([E3]),feats(125, blue,abs,ab(full,full),mt(diesel,yes)), 10500, ChevKa3.jpg) car(Ford,Ka,cert([E3]),feats(125, blue,abs,ab(extra,no),mt(diesel,yes)), 11000, ChevKaD4.jpg) car(Chev,Xen,cert([D52, RAC, H5]),feats(300, red,abs,ab(full,mid),mt(gas,no)), 70000, CX.jpg) car(Chev,Prot,cert([RAC]),feats(300, red,abs,ab(full,mid),mt(gas,no)), 60000, ChevProt.jpg)
why car 2 comes nearer to 1 than car 3 is due to a qualitative criterion rather than quantitative, in that company and model results in a more meaningful difference than the engine traits. In general, it makes sense to assume that differences at top positions in the atoms are more important than differences at inner positions. In our case, as well as in Nienhuys-Cheng’s proposal, the position of the differences, the so-called context, between atoms is taken into account when computing the distance. Note that, this aspect is disregarded by the unweighted J. Ramon et al.’s distance. For this distance, cars 2 and 3 are equally similar to car 1. Furthermore, note that a context-sensitive distance allows us to indirectly use the position in the atom/term in order to set different levels of importance for every trait of the car. For instance, moving the trait colour to a higher position in the atom implies that differences involving this attribute become more meaningful. In this line, we could also endow our representation language with artificial constructors, namely art(·), which allow us to reduce the importance of a trait. For instance, a nested expression such as art(art(art(F ord))) would decrease the importance of the trait company. Additionally, the size of the differences is also taken into account. If we observe the differences between cars 3, 7 and 8, our intuition gives more similarity to 3 and 8 because they have only one certification while 7 has three. NienhuysCheng’s distance disregards this and gives that the three cars are at the same distance to each other. Our distance and J. Ramon et al.’s distance place cars 3 and 8 closer than any of them with 7. Finally, let us consider the remaining group of cars. We can see that cars 4, 5 and 6 differ in the airbag description (occurrences 4.4.1 and 4.4.2) in such a way that 4 and 5 have an homogeneous airbag equipment but not 6. According to this observation, we can affirm that cars 4 and 5 are more similar than 5 and 6. Here, the rationale is that those differences occurring repeatedly are less significant. Our distance as well as J. Ramon et al.’s distance are capable of coping with this (repeated differences) and hence, the computed distances are in agreement with this fact. Nevertheless, Nienhuys-Cheng’s proposal ignores repeated differences, and for that reason, cars 4 and 5 are at the same distance that cars 5 and 6. If now we consider the photo and the two numerical values, we see that J. Ramon et al.’s distance is not able to handle them. If we exclude these three values and compute J. Ramon et al.’s distance with the rest, we have as a result a pair such as (n, m). If, next, we compute the distances for the photo and the numerical values, we get three scalar values d1 , d2 and d3 . We do not know how
162
V. Estruch et al.
these four results can be combined and integrated into a single value. In contrast, Nienhuys-Cheng’s distance and ours can handle the whole XML description. In both cases, one simple way to compose atom with non-atom representations (such as the picture) is to construct a tuple, taking out all the non-term-based representations, such as pictures and numerical values. The resulting tuple-based representation is shown in Table 4: Table 4. An equivalent tuple-based representation of the atom representation 1 2 3 4 5 6 7 8
75, 9000, ChevKaG.jpg, car(Ford,Ka,cert([E3]),feats(red,abs,ab(full,mid),mt(gas,no))) 80, 10000, ChevKaD.jpg, car(Ford,Ka,cert([E3]),feats(red,abs,ab(full,mid),mt(diesel,yes))) 250, 60000, ChevCorv.jpg, car(Chev,Corv,cert([E3]),feats(red,abs,ab(full,mid),mt(gas,no))) 100, 10000, ChevKaD2.jpg, car(Ford,Ka,cert([E3]),feats(blue,abs,ab(mid,mid),mt(diesel,yes))) 125, 10500, ChevKa3.jpg, car(Ford,Ka,cert([E3]),feats(blue,abs,ab(full,full),mt(diesel,yes))) 125, 11000, ChevKaD4.jpg, car(Ford,Ka,cert([E3]),feats(blue,abs,ab(extra,no),mt(diesel,yes))) 300, 70000, CX.jpg, car(Chev,Xen,cert([D52, RAC, H5]),feats(red,abs,ab(full,mid),mt(gas,no))) 300, 60000, ChevProt.jpg, car(Chev,Prot,cert([RAC]),feats(red,abs,ab(full,mid),mt(gas,no)))
The two first attributes use distances for real numbers (e.g. the absolute difference), the third attribute can use any distance for images (e.g. the Earth Mover’s Distance, Mallows Distance or Kantorovich distance [14]) and the fourth attribute use a distance for atoms. Using a proper weighting of the four attributes in the tuple (by normalising them and then using their original depth as a way to determine their weight), we can now compute the distances between the atoms and then aggregate the four distance values into a single distance. This shows that our distance allows the composability, an important requirement when trying to integrate data which is represented not only as atoms, but also using other data representations.
6
Conclusions and Future Work
In this paper we have presented a new distance for ground terms/atoms which integrates the most remarkable traits in Nienhuys-Cheng’s and J. Ramon et al.’s proposals. That is, context-sensitivity in the former case and complexity and repeated differences in the latter without losing the general and convenient feature of returning a single number, instead of two such as J.Ramon et al.’s distance. This can directly be seen from the formulation of the distance where the function Size, which takes the complexity of the differences into account, w(o) is weighted by the quotient C(o) where the numerator controls the frequency of the repeated difference and the denominator the context where this difference takes place. Apart from the direct application of a distance between atoms in areas such as machine learning and inductive programming (very especially in inductive logic programming), the distance can also be used when atoms are employed to represent other structures, as we have illustrated for XML documents. Additionally, we hope that a proper measure for terms could foster the application
An Integrated Distance for Atoms
163
in different areas inside the logic and functional programming communities. In order to show this, we need to implement the distance to conduct experiments in these areas of application. In this proposal, there is an easy way to assign weights at different positions, by using dummy function symbols. Relating this problem with the limitation of not handling variables directly, as future work, we are working on an extension to consider weights directly and to handle variables directly as J. Ramon et al.’s distance does. In fact, this extension can be done in two different ways. First, upgrading the function Size as well as the definition of syntactical differences between terms/atoms (O ) in order to take variable symbols into account. For instance, the size of a variable could be weighted half of the value assigned to a constant symbol. We could also give different weights to different constants or function symbols or to different positions. Second, following J. Ramon et al.’s approach, we could seek to integrate a syntactic difference search guided by the lgg into our setting. With this extension (especially with the first approach), our distance would include all the positive features (the desiderata) that we showed on Table 1. Additionally, we are studying how the ideas of size and context-sensitive could be adapted in order to improve other distances for nested data types (e.g. sequences of sets, or lists of lists, etc.).
Acknowledgments The authors thank the funding from the Spanish Ministerio de Educaci´ on y Ciencia (MEC) for projects CONSOLIDER-INGENIO 26706 and TIN 200768093-C02, and GVA project PROMETEO/2008/051.
References 1. Aleksovski, D., Erwig, M., Dzeroski, S.: A functional programming approach to distance-based machine learning. In: Conference on Data Mining and Data Warehouses (SiKDD 2008), Jozef Stefan Institute (2008) 2. Bille, P.: A survey on tree edit distance and related problems. Theoretical computer science 337(1-3), 217–239 (2005) 3. Blockeel, H., De Raedt, L., Ramon, J.: Top-down induction of clustering trees. In: Proc. of the 15th International Conference on Machine Learning (ICML 1998), pp. 55–63. Morgan Kaufmann, San Francisco (1998) 4. Cheney, J.: Flux: functional updates for XML. In: Proceeding of the 13th ACM SIGPLAN international conference on Functional programming, ICFP, pp. 3–14. ACM, New York (2008) 5. Estruch, V., Ferri, C., Hern´ andez-Orallo, J., Ram´ırez-Quintana, M.J.: A new context-sensitive and composable distance for first-order terms. Technical report, Departament de Sistemes Informatics i Computacio, Universitat Politecnica de Valencia (2009), http://users.dsic.upv.es/~ flip 6. Ferri, C., Hern´ andez-Orallo, J., Ram´ırez-Quintana, M.J.: Incremental learning of functional logic programs. In: Kuchen, H., Ueda, K. (eds.) FLOPS 2001. LNCS, vol. 2024, pp. 233–247. Springer, Heidelberg (2001)
164
V. Estruch et al.
7. Ferri, C., Hern´ andez-Orallo, J., Ram´ırez-Quintana, M.J.: Learning MDL-guided decision trees for conctructor based languages. In: Codognet, P. (ed.) ILP 2005. LNCS, vol. 3625, pp. 87–102. Springer, Heidelberg (2001) 8. Flener, P., Schmid, U.: An introduction to inductive programming. Artificial Intelligence Review 29(1), 45–62 (2008) 9. Hern´ andez, J., Ram´ırez, M.J.: Inverse narrowing for the induction of functional logic programs. In: Proceedings of the Joint Conference on Declarative Programming, Univ. de la Coru˜ na (1998) 10. Hern´ andez-Orallo, J., Ram´ırez-Quintana, M.J.: A strong complete schema for inductive functional logic programming. In: Dˇzeroski, S., Flach, P.A. (eds.) ILP 1999. LNCS (LNAI), vol. 1634, pp. 116–127. Springer, Heidelberg (1999) 11. Hutchinson, A.: Metrics on terms and clauses. In: van Someren, M., Widmer, G. (eds.) ECML 1997. LNCS, vol. 1224, pp. 138–145. Springer, Heidelberg (1997) 12. Nagata, J., Hart, K.P., Vaughan, J.E.: Encyclopedia of General Topology. Elsevier, Amsterdam (2003) 13. Lavraˇc, N., Dˇzeroski, S.: Inductive Logic Programming: Techniques and Applications. Ellis Horwood (1994) 14. Levina, E., Bickel, P.J.: The earth mover’s distance is the mallows distance: Some insights from statistics. In: 8th International Conference on Computer Vision, pp. 251–256. IEEE, Los Alamitos (2001) 15. Marzal, A., Vidal, E.: Computation of normalized edit distance and applications. IEEE Transactions on Pattern Analysis and Machine Learning Intelligence 15(9), 915–925 (1993) 16. Mitchell, T.M.: Machine Learning. McGraw-Hill, New York (1997) 17. Muggleton, S.: Inductive Logic Programming. New Generation Computing 8(4), 295–318 (1991) 18. Nienhuys-Cheng, S.H., de Wolf, R.: Foundations of Inductive Logic Programming. LNCS (LNAI), vol. 1228. Springer, Heidelberg (1997) 19. Plotkin, G.: A note on inductive generalisation. Machine Intelligence 5, 153–163 (1970) 20. Ramon, J., Bruynooghe, M.: A framework for defining distances between firstorder logic objects. In: Page, D.L. (ed.) ILP 1998. LNCS, vol. 1446, pp. 271–280. Springer, Heidelberg (1998) 21. Ramon, J., Bruynooghe, M., Van Laer, W.: Distance measures between atoms. In: CompulogNet Area Meeting on Computational Logic and Machine Learing, pp. 35–41. University of Manchester, UK (1998)
A Pearl on SAT Solving in Prolog Jacob M. Howe1 and Andy King2 1
Department of Computing, City University London, EC1V 0HB, UK 2 School of Computing, University of Kent, CT2 7NF, UK
Abstract. A succinct SAT solver is presented that exploits the control provided by delay declarations to implement watched literals and unit propagation. Despite its brevity the solver is surprisingly powerful and its elegant use of Prolog constructs is presented as a programming pearl.
1
Introduction
The Boolean satisfiability problem, SAT, is of continuing interest because a variety of problems are naturally expressible as a SAT instance. Much effort has been expended in the development of algorithms for, and implementations of, efficient SAT solvers. This has borne fruit with a number of solvers that are either for specialised applications or are general purpose [5]. Recently, it has been demonstrated how a dedicated external SAT solver coded in C can be integrated with Prolog [2] and this has been utilised for a number of applications. This work was published as a pearl owing to its elegant use of Prolog to transform logical formulae to Conjunctive Normal Form (CNF). This work begs the question of the suitability of Prolog as a medium for coding a SAT solver. In this short paper it is argued that a SAT solver can not only be coded in Prolog, but that this solver is a so-called natural pearl. That is, the key concepts of efficient SAT solving can be formulated in a logic program using a combination of logic and control features [11] that lie at the heart of the paradigm. This pearl was discovered when implementing an efficient groundness analyser [8], naturally emerging from the representation of Boolean functions using logical variables; the solver has not been previously described. The logic and control features exemplified in this pearl are the use of logical variables, backtracking and the suspension and resumption of execution via delay declarations [15]. A delay declaration is a control mechanism that provides a way to delay the selection of an atom in a goal until some condition is satisfied. They provide a way to handle, for example, negated goals and non-linear constraints. Delay declarations are now an integral part of Prolog systems, though their centrality in the paradigm has only recently been formally established [10]. This paper demonstrates just how good the match between Prolog and SAT is, when implementing the Davis, Putnam, Logemann, Loveland (DPLL) algorithm [3] with watched literals [14]. Watched literals are one of the most powerful features in speeding up SAT solvers. The resulting solver is elegant and concise, coded in twenty lines of Prolog, it is self-contained and it will be argued that it is M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 165–174, 2010. c Springer-Verlag Berlin Heidelberg 2010
166
J.M. Howe and A. King
efficient enough for solving some interesting, albeit modest, SAT instances [8,9]. The solver can be further developed in a number of ways, a few of which are discussed here, and provides an easy entry into SAT solving for the Prolog programmer. The rest of the paper contains a short summary of relevant background on SAT solving, gives the code for the solver and comments upon it, presents a short empirical evaluation to demonstrate its power, discusses extensions to the solver and concludes with a discussion of the limitations of the solver and its approach.
2
Background
This section briefly outlines the SAT problem and the DPLL algorithm with watched literals [14] that the solver implements. The Boolean satisfiability problem is the problem of determining whether or not, for a given Boolean formula, there is a truth assignment to the variables under which the formula evaluates to true. Most recent Boolean satisfiability solvers have been been based on the Davis, Putnam, Logemann, Loveland (DPLL) algorithm [3]. Figure 1 presents a recursive formulation of the algorithm adapted from that given in [16]. The first argument of the function DPLL is a formula, f , defined over a set of propositional variables X. As usual f is assumed to be in CNF. The second argument, θ, is a partial (truth) function over X → {true, f alse}. The call DPLL(f , ∅) decides the satisfiability of f where ∅ denotes the empty truth function. If the call returns the special symbol ⊥ then f is unsatisfiable, otherwise the call returns a truth function θ that satisfies f .
(1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) (13) (14) (15) (16)
function DPLL(f : CNF formula, θ : truth assignment) begin θ1 := θ ∪ unit-propagation(f , θ); if (is-satisfied(f , θ1 )) then return θ1 ; else if (is-conflicting(f , θ1 )) then return ⊥; endif x := choose-free-variable(f , θ1 ); → true}); θ2 := DPLL(f , θ1 ∪ {x if (θ2 = ⊥) then return θ2 ; else → f alse}); return DPLL(f , θ1 ∪ {x endif end Fig. 1. Recursive formulation of the DPLL algorithm
A Pearl on SAT Solving in Prolog
2.1
167
Unit Propagation
At line (3) the function extends the truth assignment θ to θ1 by applying socalled unit propagation on f and θ. For instance, suppose f = (¬x ∨ z) ∧ (u ∨ ¬v ∨ w) ∧ (¬w ∨ y ∨ ¬z) so that X = {u, v, w, x, y, z} and θ is the partial function θ = {x → true, y → f alse}. Unit propagation examines each clause in f to deduce a truth assignment θ1 that extends θ and necessarily holds for f to be satisfiable. For example, for the clause (¬x ∨ z) to be satisfiable, and hence f as a whole, it is necessary that z → true. Moreover, for (¬w ∨ y ∨ ¬z) to be satisfiable, it follows that w → f alse. The satisfability of (u∨¬v ∨w) depends on two unknowns, u and v, hence no further information can be deduced from this clause. The function unit-propagation(f, θ) encapsulates this reasoning returning the bindings {w → f alse, z → true}. Extending θ with these necessary bindings gives θ1 . 2.2
Watched Literals
Information can only be derived from a clause if it does not contain two unknowns. This is the observation behind watched literals [14], which is an implementation technique for realising unit propagation. The idea is to keep watch on a clause by monitoring only two of its unknowns. Returning to the previous example, before any variable assignment is made suitable monitors for the clause (u ∨ ¬v ∨ w) are the unknowns u and v, suitable monitors for (¬w ∨ y ∨ ¬z) are w and z and (¬x ∨ z) must have monitors x and z. When the initial empty θ is augmented with x → true, a new monitor for the third clause is not available and unit propagation immediately applies to infer z → true. The new binding on z is detected by the monitors on the second clause, which are then updated to be w and y. If θ is further augmented with y → f alse, the change in y is again detected by the monitors on (¬w ∨ y ∨ ¬z). This time there are no remaining unbound variables to monitor and unit propagation applies, giving the binding w → f alse. Now notice that the first clause, (u ∨ ¬v ∨ w), is not monitoring w, hence no action is taken in response to the binding on w. Therefore, watched literals provide a mechanism for controlling propagation without inspecting clauses unnecessarily. 2.3
Termination and the Base Cases
Once unit propagation has been completely applied, it remains to detect whether sufficient variables have be bound for f to be satisfiable. This is the role of the predicate is-satisfied(f, θ). This predicate returns true if every clause of f contains at least one literal that is satisfied. For example, is-satisfied(f, θ1 ) = false since (u ∨ ¬v ∨ w) is not satisfied under θ1 because u and v are unknown whereas w is bound to false. If is-satisfied(f, θ1 ) were satisfied, then θ1 could be returned to demonstrate the existence of a satisfying assignment. Conversely, a conflict can be observed when inspecting f and θ1 , from which it follows that f is unsatisfiable. To illustrate, suppose f = (¬x) ∧ (x ∨ y) ∧ (¬y)
168
J.M. Howe and A. King
sat(Clauses, Vars) :problem_setup(Clauses), elim_var(Vars). elim_var([]). elim_var([Var | Vars]) :elim_var(Vars), (Var = true; Var = false). problem_setup([]). problem_setup([Clause | Clauses]) :clause_setup(Clause), problem_setup(Clauses). clause_setup([Pol-Var | Pairs]) :- set_watch(Pairs, Var, Pol). set_watch([], Var, Pol) :- Var = Pol. set_watch([Pol2-Var2 | Pairs], Var1, Pol1):watch(Var1, Pol1, Var2, Pol2, Pairs). :- block watch(-, ?, -, ?, ?). watch(Var1, Pol1, Var2, Pol2, Pairs) :nonvar(Var1) -> update_watch(Var1, Pol1, Var2, Pol2, Pairs); update_watch(Var2, Pol2, Var1, Pol1, Pairs). update_watch(Var1, Pol1, Var2, Pol2, Pairs) :Var1 == Pol1 -> true; set_watch(Pairs, Var2, Pol2). Fig. 2. Code for SAT solver
and θ = ∅. From the first and third clauses it follows that θ1 = {x → f alse, y → f alse}. The predicate is-conflicting(f, θ) detects whether f contains a clause in which every literal is unsatisfiable. The clause (x ∨ y) satisfies this criteria under θ1 , therefore it follows that f is unsatisfiable, which is indicated by returning ⊥. 2.4
Search and the Recursive Cases
If neither satisfiability nor unsatisfiability have been detected thus far, a variable x is selected for labelling. The DPLL algorithm is then invoked with θ1 augmented with the new binding x → true. If satisfability cannot be detected with → f alse. this choice, DPLL is subsequently invoked with θ1 augmented with x Termination is assured because the number of unassigned variables strictly reduces on each recursive call.
3
The SAT Solver
The code for the solver is give in Figure 2. It consists of just twenty lines of Prolog. Since a declarative description of assignment and propagation can be
A Pearl on SAT Solving in Prolog
169
fully expressed in Prolog, execution can deal with all aspects of controlling the search, leading to the succinct code given in the figure. 3.1
Invoking the Solver
The solver is called with two arguments. The first represents a formula in CNF as a list of lists, each constituent list representing a clause. The literals of a clause are represented as pairs, Pol-Var, where Var is a logical variable and Pol is true or false, indicating that the literal has positive or negative polarity. The formula ¬x ∨ (y ∧ ¬z) would thus be represented in CNF as (¬x ∨ y) ∧ (¬x ∨ ¬z) and presented to the solver as the list L = [[false-X, true-Y], [false-X, false-Z]] where X, Y and Z are logical variables. The second argument is a list of the variables occurring in the problem. Thus the query sat(L, [X, Y, Z]) will succeed and bind the variables to a solution, for example, X = false, Y = true, Z = true. As a by-product, L will be instantiated to [[false-false, true-true], [false-false, false-true]]. This illustrates that the interpretation of true and false in L depends on whether they are left or right of the - operator: to the left they denote polarity; to the right they denote truth values. If L is unsatisfiable then sat(L, Vars) will fail. If necessary, the solver can be called under a double negation to check for satisfiability, whilst leaving the variables unbound. 3.2
Watched Literals
The solver is based on launching a watch goal for each clause that monitors two literals of that clause. Since the polarity of the literals is known, this amounts to blocking execution until one of the two uninstantiated variables occurring in the clause is bound. The watch predicate thus blocks on its first and third arguments until one of them is instantiated to a truth value. In SICStus Prolog, this requirement is stated by the declaration :- block watch(-, ?, -, ?, ?). If the first argument is bound, then update watch will diagnose what action, if any, to perform based on the polarity of the bound variable and its binding. If the polarity is positive, and the variable is bound to true, then the clause has been satisfied and no further action is required. Likewise, the clause is satisfied if the variable is false and the polarity is negative. Otherwise, the satisfiability of the clause depends on those variables of the clause which have not yet been inspected. They are considered in the subsequent call to set watch. 3.3
Unit Propagation
The first clause of set watch handles the case when there are no further variables to watch. If the remaining variable is not bound, then unit propagation occurs, assigning the variable a value that satisfies the clause. If the polarity of the variable is positive, then the variable is assigned true. Conversely, if the polarity is negative, then the variable is assigned false. A single unification is sufficient
170
J.M. Howe and A. King
to handle both cases. If Var and Pol are not unifiable, then the bindings to Vars do not satisfy the clause, hence do not satisfy the whole CNF formula. Once problem setup(Clauses) has launched a process for each clause in the list Clauses, elim var(Vars) is invoked to bind each variable of Vars to a truth value. Control switches to a watch goal as soon as its first or third argument is bound. In effect, the (Var = true; Var = false) sub-goals of elim vars(Vars) coroutine with the watch sub-goals of problem setup(Clauses). Thus, for instance, elim var(Vars) can bind a variable which transfers control to a watch goal that is waiting on that variable. This goal can, in turn, call update watch and thus invoke set watch, the first clause of which is responsible for unit propagation. Unit propagation can instantiate another variable, so that control is passed to another watch goal, thus leading to a sequence of bindings that eminate from a single binding in elim vars(Vars). Control will only return to elim var(Vars) when unit propagation has been maximally applied. 3.4
Search
In addition to supporting coroutining, Prolog permits a conflicting binding to be undone through backtracking. Suppose a single binding in elim var(Vars) triggers a sequence of bindings to be made by the watch goals and, in doing so, the watch goals encounter a conflict: the unification Var = Pol in set watch fails. Then backtracking will undo the original binding made in elim var(Vars), as well as the subsequent bindings made by the watch goals. The watch goals themselves are also rewound to their point of execution immediately prior to when the original binding was made in elim var(Vars). The goal elim var(Vars) will then instantiate Vars to the next combination of truth values, which may itself cause a watch goal to be resumed, and another sequence of bindings to be made. Thus monitoring, propagation and search are seamlessly interwoven. Note that backtracking can enumerate all the satisfying assignments, unlike most SAT solvers (therefore also [2]). For example, the query sat(L, [X, Y, Z]) will give the solutions: X = false, Y = true, Z = true; X = false, Y = false, Z = true; X = true, Y = true, Z = false; X = false, Y = true, Z = false; and X = false, Y = false, Z = false.
4
Extensions
The development of SAT solvers over the last decade has resulted in numerous heuristics that dramatically improve the performance of general purpose solvers. This section outlines how a number of these refinements might be incorporated into the solver presented above. However, discussion of the popular learning heuristic [13] is left until section 6 as its integration into the solver is more problematic.
A Pearl on SAT Solving in Prolog
171
– The first and simplest heuristic is to use a static variable ordering. Variables are ordered by frequency of occurrence in the input problem, with the most frequently occurring assigned first. This wins in two ways: the problem size is quickly reduced by satisfying clauses and the amount of propagation achieved is greater. Both reduce the number of assignments required to reach a satisfying assignment or a conflict. This tactic, of course, can be straightforwardly implemented in Prolog. – Static variable ordering is the simplest preprocessing tactic aimed at discovering and exploiting structure in a problem. As well as analysing the structure of a problem, another popular tactic is to change the problem by restructuring it using limited applications of resolution steps [4]. Again, these preprocessing steps can clearly be achieved satisfactorily in Prolog. – Many SAT solvers use non-chronological backtracking, or backjumping, in order to avoid exploration of fruitless branches of the search tree [13]. Backjumping for depth-first search algorithms in Prolog has been explored in [1] and this approach carries over to the solver presented in this paper. – Dynamically ordering variables during search [14] has also been widely incorporated in SAT solvers. This too can be incorporated into the solver presented in this paper. The approach has some similarities to the backjumping of [1] using the blackboard to hold conflict information which is then used after backtracking to select the next variable for assignment.
5
Experimental Results
In order to illustrate the problems that the solver can tackle, empirical results for a small benchmark suite are included and are tabulated below. In the table, benchmark names the SAT instance, whilst vars and clauses give the number of variables and clauses respectively, satisfiable indicates whether or not the instance is satisfiable, time gives the time taken to find a first satisfying assignment, or to establish that no such assignment exist, mini gives the time for the benchmark using MiniSat and assigns gives the total number of variable assignments made in elim var. The MiniSat results have been included for reference and are unsurprisingly considerably faster, owing to its C implementation and use of many heuristics. Also note that the timing granularity of SICStus is different to MiniSat. The implementation is in SICStus 4.0.4 and these experiments were run on a single core of a MacBook with a 2.4GHz Intel Core 2 Duo processor and 4GB of memory. Timeout was set at one minute. Finally, note that these results utilise a static variable ordering as described in section 4 and that this significantly speeds up the solver of these benchmarks. The first six chat 80 benchmarks are a selection of the largest SAT instances solved in the Pos-based analysis of [8]. It is worth pointing out that these are encodings of entailment checks for stability in fixpoint calculations, therefore are not themselves necessarily positive Boolean formulae (which would be satisfiable by definition). The remaining benchmarks come from [7]. The uf* and uuf* benchmarks are random 3SAT instances at the phase transition boundary and
172
J.M. Howe and A. King benchmark chat 80 1.cnf chat 80 2.cnf chat 80 3.cnf chat 80 4.cnf chat 80 5.cnf chat 80 6.cnf uf20-0903.cnf uf50-0429.cnf uf100-0658.cnf uf150-046.cnf uf250-091.cnf uuf50-0168.cnf uuf100-0592.cnf uuf150-089.cnf uuf250-016.cnf 2bitcomp 5.cnf flat200-90.cnf
vars 13 12 8 7 7 8 20 50 100 150 250 50 100 150 250 125 600
clauses 31 30 14 16 16 14 91 218 430 645 1065 218 430 645 1065 310 2237
satisfiable true true true true true true true true true true true false false false false true true
time (ms) 0 0 0 0 0 0 0 10 20 290 2850 0 50 770 t/o 130 380
mini (ms) 1 1 1 1 1 1 1 1 1 15 171 1 6 18 1970 1 12
assigns 9 5 7 3 4 6 8 89 176 3002 13920 79 535 8394 7617 1811
Fig. 3. Experimental evaluation of the SAT solver
are included to illustrate behaviour on problems likely to involve large amounts of search; the individual problems were chosen at random from larger suites. The remaining problems were chosen to illustrate behaviour on structured problems. Observe that the problems arising from the context where this solver was discovered are all solved quickly, requiring very few variable assignments. On these problems, where there can be thousands of calls to the solver in a single run of the analysis [8], the time to solve the larger SAT instances are beneath the granularity of the clock, thus the solver is clearly fast enough. As expected, on the phase transition problems the amount of search grows sharply with the size of the problem. However, instances with hundreds of clauses are still solved, and this observation is confirmed by the results for the structured problems.
6
Concluding Discussion
Thus far this paper has highlighted the ways in which Prolog provides an easy entry point into SAT solving. This section begins by highlighting the limitations of the approach taken, before concluding with a discussion of the strengths of this implementation technique. The challenge of SAT solving grows with the size of the problem. This can manifest itself in two ways: the storage of the SAT instance and the growth of the search space. The first of these is perhaps the greatest obstacle to solving really large problems in Prolog – the programmer does not have the fine-grained memory control required to store and access hundreds of thousands of clauses. To address the second issue search heuristics, such as those outlined in section 4, are needed. One popular kind of heuristic is learning in which clauses are added
A Pearl on SAT Solving in Prolog
173
to the problem that express regions of the search space that do not contain a solution [13]. Unfortunately, it is not clear how to achieve this cleanly in this Prolog solver, as calls to the learnt clauses would be lost on backtracking. One approach would be to store a learnt clause on a blackboard and then add it to the problem at an appropriate point on backtracking, but the approach is both restrictive and unattractive (although it does fit well with random restarts that are used in some solvers). A final point to note is in the implementation of watched literals. The literals being watched change during search and changes made during propagation are undone on backtracking. This makes maintenance of the clauses easy, but loses one advantage that watched literals potentially have, namely that the literals being watched do not need to be changed on backtracking [6]. Owing to the drawbacks outlined above, the solver presented in this paper is not going to be competitive on the large, difficult problems set as challenges presented in the international SAT competitions [12]. However, the solver does provide a declarative description of SAT solving with watched literals in a succinct and self-contained manner, and one which can be extended in a number of ways. In addition it performs well enough to be of use for small and mediumsize problems, an example being detecting stability in fixpoint calculations in Pos-based program analysis [8]. In this context, a SAT engine coded in Prolog itself is attractive since it avoids using a foreign language interface (note that [2] hides this interface from the user), simplifies distribution issues, and avoids the overhead of converting a Prolog representation of a SAT instance to the internal C representation used by the external SAT solver. Finally, the solver is available at www.soi.city.ac.uk/~jacob/solver/. Acknowledgements. This work was supported by EPSRC-funded projects EP/E033105/1 and EP/E034519/1. The authors would like to thank the Royal Society and the University of St Andrews for their generous support. They would also like to thank the anonymous referees for their helpful comments.
References 1. Bruynooghe, M.: Enhancing a Search Algorithm to Perform Intelligent Backtracking. Theory and Practice of Logic Programming 4(3), 371–380 (2004) 2. Codish, M., Lagoon, V., Stuckey, P.J.: Logic Programming with Satisfiability. Theory and Practice of Logic Programming 8(1), 121–128 (2008) 3. Davis, M., Logemann, G., Loveland, D.: A Machine Program for Theorem Proving. Communications of the ACM 5(7), 394–397 (1962) 4. E´en, N., Biere, A.: Effective preprocessing in SAT through variable and clause elimination. In: Bacchus, F., Walsh, T. (eds.) SAT 2005. LNCS, vol. 3569, pp. 61–75. Springer, Heidelberg (2005) 5. E´en, N., S¨ orensson, N.: An Extensible SAT-solver. In: Giunchiglia, E., Tacchella, A. (eds.) SAT 2003. LNCS, vol. 2919, pp. 502–518. Springer, Heidelberg (2004) 6. Gent, I.P., Jefferson, C., Miguel, I.: Watched Literals for Constraint Propagation in Minion. In: Benhamou, F. (ed.) CP 2006. LNCS, vol. 4204, pp. 182–197. Springer, Heidelberg (2006)
174
J.M. Howe and A. King
7. Hoos, H.H., St¨ utzle, T.: SATLIB: An Online Resource for Research on SAT. In: SAT 2000, pp. 283–292. IOS Press, Amsterdam (2000) 8. Howe, J.M., King, A.: Positive Boolean Functions as Multiheaded Clauses. In: Codognet, P. (ed.) ICLP 2001. LNCS, vol. 2237, pp. 120–134. Springer, Heidelberg (2001) 9. Howe, J.M., King, A.: Efficient Groundness Analysis in Prolog. Theory and Practice of Logic Programming 3(1), 95–124 (2003) 10. King, A., Martin, J.C.: Control Generation by Program Transformation. Fundamenta Informaticae 69(1-2), 179–218 (2006) 11. Kowalski, R.A.: Algorithm = Logic + Control. Communication of the ACM 22(7), 424–436 (1979) 12. Le Berre, D., Roussel, O., Simon, L.: The International SAT Competitions Webpage (2009), http://www.satcompetition.org/ 13. Marques-Silva, J.P., Sakallah, K.A.: GRASP – a New Search Algorithm for Satisfiability. In: International Conference on Computer-Aided Design, pp. 220–227. ACM and IEEE Computer Society (1996) 14. Moskewicz, M.W., Madigan, C.F., Zhao, Y., Zhang, L., Malik, S.: Chaff: Engineering an Efficient SAT Solver. In: Design Automation Conference, pp. 530–535. ACM Press, New York (2001) 15. Naish, L.: Negation and Control in Logic Programs. Springer, Heidelberg (1986) 16. Zhang, L., Malik, S.: The Quest for Efficient Boolean Satisfiability Solvers. In: Brinksma, E., Larsen, K.G. (eds.) CAV 2002. LNCS, vol. 2404, pp. 17–36. Springer, Heidelberg (2002)
Automatically Generating Counterexamples to Naive Free Theorems Daniel Seidel and Janis Voigtl¨ ander Rheinische Friedrich-Wilhelms-Universit¨ at Bonn Institut f¨ ur Informatik R¨ omerstraße 164 53117 Bonn, Germany {ds,jv}@iai.uni-bonn.de
Abstract. Disproof can be as important as proof in studying programs and programming languages. In particular, side conditions in a statement about program behavior are sometimes best understood and explored by trying to exhibit a falsifying example in the absence of a condition in question. Automation is as desirable for such falsification as it is for verification. We develop formal and implemented tools for counterexample generation in the context of free theorems, i.e., statements derived from polymorphic types a ` la relational parametricity. The machinery we use is rooted in constraining the type system and in intuitionistic proof search.
1
Introduction
Free theorems [19] as derived from relational parametricity [12] are an important source for useful statements about the semantics of programs in typed functional languages. But often, free theorems are derived in a “naive” setting, pretending that the language under consideration were conceptually as simple as the pure polymorphic lambda calculus [11] (maybe with algebraic data types added in) for which relational parametricity was originally conceived. For example, such a naive version claims that for every function, in Haskell syntax (no explicit “∀α.”), f :: [α] → [α]
(1)
it holds that for every choice of types τ1 , τ2 , g :: τ1 → τ2 , and x :: [τ1 ], map g (f x) = f (map g x)
(2)
where map :: (α → β) → [α] → [β] is the standard function. But equivalence (2) does not actually hold in Haskell. A counterexample is obtained by setting f = λx → [fix id ],
τ1 = τ2 = Int,
g = λy → 17,
x = []
(3)
where fix :: (α → α) → α is a fixpoint combinator and id :: α → α the identity function. Indeed, now map g (f x) = [17] but f (map g x) = [⊥], where ⊥ corresponds to nontermination. (Note that Haskell is lazy, so [⊥] = ⊥.)
This author was supported by the DFG under grant VO 1512/1-1.
M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 175–190, 2010. c Springer-Verlag Berlin Heidelberg 2010
176
D. Seidel and J. Voigtl¨ ander
Trying to be less naive, we can take into account that the presence of fixpoint recursion enforces certain strictness conditions when deriving free theorems [19, Section 7] and thus obtain (2) only for g with g ⊥ = ⊥. Generally, only sufficient, not always necessary, conditions are obtained. For example, the naive free theorem that for every function h :: [α] → Int it holds that h x = h (map g x) becomes encumbered with the condition that, in the potential presence of fix , g should be strict. But here it is actually impossible to give a counterexample like (3) above. No matter how we complicate h by involving general recursion, in a pure lazy setting the naive free theorem holds steady, even for nonstrict g. So it is natural to ask when it is the case that naive free theorems actually break in more realistic settings, and how to find corresponding counterexamples. Answering these questions will improve the understanding of free theorems and allow us to more systematically study corner cases in their applications. Indeed, concrete counterexamples were very important in studying semantic aspects of type-based program transformations in [8] and informed the invention of new transformations overcoming some of the encountered semantic problems [17]. (For a discussion of the general importance of counterexamples, see [9].) And also for other applications [16,18] it seems relevant to consider the potential negative impact of language extensions on free theorems. To date, counterexamples of interest have been literally manufactured, i.e., produced by hand in an ad-hoc fashion. With the work reported here we move to automatic generation instead, which is both an interesting problem and practically useful. Summarily, the aim is as follows. Given a type, two versions of a free theorem can be produced: the naive one that ignores advanced language features and a more cautious one that comes with certain additional conditions. To explain whether, and if so why, a certain additional condition is really necessary, we want to provide a separating counterexample (or assert that there is none): a term of the given type such that the naive version of the free theorem fails for it precisely due to missing that condition. Even when considering only the impact of general recursion and ⊥ for now (as opposed to seq [7] or imprecise error semantics [15]), the set task is quite challenging; indeed much more so than the example of (2) vs. (3) suggests. Take, for example, the following type: f :: ((Int → [α]) → Either Int Bool) → [Int]
(4)
The “fix - and ⊥-aware” free theorem generated for it is that for every choice of types τ1 , τ2 , strict function g :: τ1 → τ2 , and arbitrary functions p :: (Int → [τ1 ]) → Either Int Bool and q :: (Int → [τ2 ]) → Either Int Bool, ∀r :: Int → [τ1 ]. p r = q (λx → map g (r x))
(5)
f p=f q
(6)
implies while the naive version would drop the strictness condition on g. The reader is invited to devise a concrete function term f of type (4) and concrete instantiations for τ1 , τ2 , (nonstrict) g, and p and q such that (5) holds but (6)
Automatically Generating Counterexamples to Naive Free Theorems
177
fails. We have developed a system (and implemented it, and made it available online: http://www-ps.iai.uni-bonn.de/cgi-bin/exfind.cgi) which meets this challenge. A screenshot solving above puzzle is given below:
Since a counterexample consists of several terms that need to fit together in a very specific way, also guaranteeing extra conditions like the relationship (5) between p, q, and g in the case just considered, a random or unguided exhaustive search approach would be unsuitable here. That is, successfully using a tool in the spirit of QuickCheck [3] to refute naive free theorems by finding counterexamples would require extremely smart and elaborate generators to prevent a “needle in a haystack search”. Indeed, we contend that the required generators would have to be so complex and ad-hoc, and extra effort would have to go into enforcing a suitable search strategy, that there would be no benefit left from using a generic testing framework. We should hence instead directly go for a formal algorithm. (We did, however, use QuickCheck to validate our implemented generator.) The approach we do follow is based on first capturing what is not a counterexample. Even in a language including general recursion and ⊥ there will be
178
D. Seidel and J. Voigtl¨ ander
terms that do not involve either of them or that do so but in a way not affecting a given naive free theorem. It is possible to develop a refined type system that keeps track of uses of fix (and thus ⊥) and admits a refined notion of relational parametricity in which fewer strictness conditions are imposed, depending on the recorded information. This refinement allows us to describe a subset of the terms of a given (original) type for which a certain strictness condition in the “fix -aware” free theorem can actually be dropped. This idea was pioneered in [10], and we provide an equivalent formalization tailored to our purposes. Knowing how to describe what is not a counterexample, we can then systematically search for a term that is not not a counterexample. That is, we look for a term that has the original type but not the refined type (in the refined type system). For the term search we take inspiration from a decision procedure for intuitionistic propositional logic in [5]. This procedure can be turned into a fix free term generator for polymorphic types, and was indeed put to use so [1]. Our twist is that we do allow the generation of terms containing fix , but not arbitrarily. Allowing arbitrary occurrences would lead to a trivial generator, because fix id can be given any type. So instead we design the search in such a way that at least one use of fix is ensured in a position where it enforces a restriction in the refined type system, and truly separates between “harmful/harmless encounter of ⊥”. Thus we really find a term (if one exists) which is in the difference of the set of all terms and the subset containing only the “not a counterexample” terms. At this point we have, in the terminology of [9], found a local counterexample: a term for which the refined type system and its refined notion of relational parametricity cannot prove that a certain strictness condition can be dropped. What we really want is a global counterexample: a term for which there cannot be any proof that the strictness condition in question can be dropped. Turning a local counterexample into a global one requires additional work, in particular to come up with appropriate instantiations like for τ1 , τ2 , g, p, and q in the challenge regarding (4) above. We return to this issue, also based on example (1), in Section 6. It turns out that not all our local counterexamples can be turned into global ones using our proposed construction, but where that construction succeeds we have a correctness statement (proved in [13]); correctness meaning that any counterexample we offer really contradicts the naive free theorem in question. Moreover, we claim completeness; meaning that if our method finds no local counterexample, then there is no global counterexample.
2
The Calculus and Standard Parametricity
We set out from a standard denotational semantics for a polymorphic lambda calculus, called PolyFix, that corresponds to a core of Haskell (without seq, without type classes, without special treatment of errors or exceptions, . . . ). Types are formed according to the following grammar, where α ranges over type variables: τ ::= α | τ → τ | [τ ] | (τ, τ ) | Either τ τ | (). We include lists, pairs, and a disjoint sum type as representatives for algebraic data types. Additionally, we include a unit type () to be used later on. (And our implementation
Automatically Generating Counterexamples to Naive Free Theorems
179
additionally deals with Int, Bool, Maybe.) Note that there is no case ∀α.τ in the grammar, because we will only consider rank-1 polymorphism (as in Haskell 98). Dealing with higher-rank polymorphism, and thus local quantification over type variables, would complicate our technique immensely. Terms are formed according to the grammar t ::= x | λx :: τ.t | t t | [ ]τ | t : t | (t, t) | Leftτ t | Rightτ t | () | fix t | case t of {· · · }, where x ranges over term variables. There are versions of case t of {· · · } for lists, pairs, disjoint sum types, and the unit type (each with the obvious configuration of exhaustive pattern match branches). Terms are typed according to standard typing rules. We give only some examples in Fig. 1. Type and term variable contexts Γ and Σ are unordered sets of the forms α1 , . . . , αk and x1 :: τ1 , . . . , xl :: τl . Γ ; Σ, x :: τ x :: τ (Var)
Γ ; Σ [ ]τ :: [τ ] (Nil)
Γ ; Σ, x :: τ1 t :: τ2 (Abs) Γ ; Σ (λx :: τ1 .t) :: τ1 → τ2
Γ ; Σ () :: () (Unit)
Γ ; Σ t1 :: τ1 → τ2 Γ ; Σ t2 :: τ1 (App) Γ ; Σ (t1 t2 ) :: τ2
Γ ; Σ t1 :: τ1 Γ ; Σ t2 :: τ2 (Pair) Γ ; Σ (t1 , t2 ) :: (τ1 , τ2 )
Γ ; Σ t :: τ1 (Left) Γ ; Σ (Leftτ2 t) :: Either τ1 τ2
Γ ; Σ t :: τ → τ (Fix) Γ ; Σ (fix t) :: τ Γ ; Σ t :: [τ1 ] Γ ; Σ t1 :: τ Γ ; Σ, x1 :: τ1 , x2 :: [τ1 ] t2 :: τ (Case) Γ ; Σ (case t of {[ ] → t1 ; x1 : x2 → t2 }) :: τ Fig. 1. Some of the Typing Rules for PolyFix
The denotational semantics interprets types as pointed complete partial orders (least element always denoted ⊥), functions as monotonic and continuous, but not necessarily strict, maps (ordered point-wise), and is entirely standard. Of note is that the interpretations of pair and disjoint sum types are noncoalesced: [[(τ1 , τ2 )]]θ = lift⊥ {(a, b) | a ∈ [[τ1 ]]θ , b ∈ [[τ2 ]]θ } [[Either τ1 τ2 ]]θ = lift⊥ ({Left a | a ∈ [[τ1 ]]θ } ∪ {Right a | a ∈ [[τ2 ]]θ }) The operation lift⊥ takes a complete partial order, adds a new element ⊥ to the carrier set, and defines this new ⊥ to be below every other element. For the semantics of terms we also show just a few example cases: [[x]]σ = σ(x),
[[λx :: τ.t]]σ a = [[t]]σ{x→a} , [[case t of {() → t1 }]]σ
[[t1 t2 ]]σ = [[t1 ]]σ [[t2 ]]σ , [[t1 ]]σ if [[t]]σ = () = ⊥ if [[t]]σ = ⊥
[[()]]σ = (),
Altogether, we have that if Γ ; Σ t :: τ and σ(x) ∈ [[τ ]]θ for every x :: τ occurring in Σ, then [[t]]σ ∈ [[τ ]]θ .
180
D. Seidel and J. Voigtl¨ ander
The key to parametricity results is the definition of a family of relations by induction on a calculus’ type structure. The appropriate such logical relation for our current setting is defined as follows, assuming ρ to be a mapping from type variables to binary relations between complete partial orders: Δα,ρ Δτ1 →τ2 ,ρ Δ[τ ],ρ Δ(τ1 ,τ2 ),ρ ΔEither τ1 τ2 ,ρ Δ(),ρ
= ρ(α) = {(f, g) | ∀(a, b) ∈ Δτ1 ,ρ . (f a, g b) ∈ Δτ2 ,ρ } = lfp (λS.{(⊥, ⊥), ([ ], [ ])} ∪ {(a : b, c : d) | (a, c) ∈ Δτ,ρ , (b, d) ∈ S}) = {(⊥, ⊥)} ∪ {((a, b), (c, d)) | (a, c) ∈ Δτ1 ,ρ , (b, d) ∈ Δτ2 ,ρ } = {(⊥, ⊥)} ∪ {(Left a, Left b) | (a, b) ∈ Δτ1 ,ρ } ∪ {· · · } = id {⊥,()}
For two pointed complete partial orders D1 and D2 , let Rel ⊥ (D1 , D2 ) collect all relations between them that are strict (i.e., contain the pair (⊥, ⊥)) and continuous (i.e., are closed under suprema). Also, let Rel ⊥ be the union of all Rel ⊥ (D1 , D2 ). The following parametricity theorem is standard [12,19]. Theorem 1. If Γ ; Σ t :: τ , then ([[t]]σ1 , [[t]]σ2 ) ∈ Δτ,ρ for every θ1 , θ2 , ρ, σ1 , and σ2 such that for every α occurring in Γ , ρ(α) ∈ Rel ⊥ (θ1 (α), θ2 (α)), and for every x :: τ occurring in Σ, (σ1 (x), σ2 (x)) ∈ Δτ ,ρ . An important special case in practice is when ρ maps only to functions.
3
Refining the Type System to Put Fix under Control
The requirement ρ(α) ∈ Rel ⊥ in Theorem 1 is responsible for strictness conditions like those on the g in the examples in the introduction. It is required because due to fix some parts of the theorem’s proof really depend on the strictness of relational interpretations of (certain) types. Launchbury and Paterson [10] proposed to explicitly keep track, in the type of a term, of uses of general recursion and/or ⊥. One of their aims was to provide less restrictive free theorems for situations where fix is known not to be used in a harmful way. While they formulated their ideas using Haskell’s type classes, a direct formalization in typing rules is possible as well. In a type variable context Γ we now distinguish between variables on which general recursion is not allowed and those on which it is. We simply annotate the latter type variables by ∗ . The idea is that a derivation step Γ ; Σ t :: α → α Γ ; Σ (fix t) :: α is only allowed if α is thus annotated in Γ . Since the actual rule (Fix) mentions an arbitrary τ , rather than more specifically a type variable, we need propagation rules describing when a type supports the use of fix. This need is addressed by defining a predicate Pointed on types. The base and the sole propagation rule are: α∗ ∈ Γ Γ α ∈ Pointed
Γ τ2 ∈ Pointed Γ (τ1 → τ2 ) ∈ Pointed
Automatically Generating Counterexamples to Naive Free Theorems
181
In addition, axioms assert that algebraic data types support fix: Γ () ∈ Pointed, Γ [τ ] ∈ Pointed, Γ (τ1 , τ2 ) ∈ Pointed, Γ (Either τ1 τ2 ) ∈ Pointed. To the typing rules (Fix) and (Case) from Fig. 1 we can now add the premise Γ τ ∈ Pointed, and similarly to the typing rules for the versions of case t of {· · · } for pairs, disjoint sum types, and the unit type. The resulting system is called PolyFix*. Note that the syntax of types and terms is the same in PolyFix and in PolyFix*. But depending on which type variables are ∗ -annotated in Γ , the latter may have fewer typable terms at a given type. The denotational semantics remains as before, except that for Γ ; Σ t :: τ we can choose to map non-∗ type variables in Γ to just complete partial orders (rather than necessarily to pointed ones) in the type environment θ. The benefit of recording at which types fix may be used is that we can now give a parametricity theorem with relaxed preconditions. For two complete partial orders D1 and D2 , let Rel(D1 , D2 ) collect all relations between them that are continuous (but not necessarily strict). For the very same logical relation Δ as in Section 2 we then have the following variant of Theorem 1, proved in [13]. Theorem 2. If Γ ; Σ t :: τ in PolyFix*, then ([[t]]σ1 , [[t]]σ2 ) ∈ Δτ,ρ for every θ1 , θ2 , ρ, σ1 , and σ2 such that for every α occurring in Γ , ρ(α) ∈ Rel(θ1 (α), θ2 (α)), for every α∗ occurring in Γ , ρ(α) ∈ Rel ⊥ (θ1 (α), θ2 (α)), and for every x :: τ occurring in Σ, (σ1 (x), σ2 (x)) ∈ Δτ ,ρ . This brings us on a par with [10]. While Launchbury and Paterson stopped at this point, being able to determine for a specific term that its use (or non-use) of fix and ⊥ does not require certain strictness conditions that would have to be imposed when just knowing the term’s original type, we instead use this result as a stepping stone. Our aim is somewhat inverse to theirs: we will start from just a type and try to find a specific term using fix in such a way that a certain side condition is required.
4
Term Search, Motivation
Theorem 2 tells us that sometimes strictness conditions are not required in free theorems. For example, we now know that for every function f typable as α f :: [α] → [α] (i.e., with non-∗ α) in PolyFix* strictness of g is not required for (2) from the introduction to hold. Correspondingly, the function f = λx → [fix id ] (or rather, f = λx :: [α].(fix (λy :: α.y)) : [ ]α ) used for the counterexample in (3) is not so typable. It is only typable as α∗ f :: [α] → [α] in PolyFix*. This observation agrees with our general strategy for finding counterexamples as already outlined in the introduction. Given a polymorphic type signature containing one or more type variables, and a free theorem derived from it that contains one or more strictness conditions due to the ρ(α) ∈ Rel ⊥ , ρ(β) ∈ Rel ⊥ , . . . from Theorem 1, assume that we want to explain (or refute) the necessity of one particular among these strictness conditions, say of the one originating from ρ(α) ∈ Rel ⊥ , and that we want to do so by investigating the existence of a counterexample to the more naive
182
D. Seidel and J. Voigtl¨ ander
free theorem obtained by dropping that particular strictness condition. This investigation can now be done by searching a term that is typable with the given signature in PolyFix* under context α∗ , β ∗ , . . ., but not under α, β ∗ , . . .. Unfortunately, this term search cannot use the typing rules of PolyFix and/or PolyFix* themselves, because in proof theory terminology they lack the subformula property. For example, rule (App) from Fig. 1 with terms (and Γ ) omitted corresponds to modus ponens. Reading it upwards we have to invent τ1 without any guidance being given by τ2 . Rule (Case) is similarly problematic. In proof search for intuitionistic propositional logic this problem is solved by designing rule systems that have the same proof power but additionally do enjoy the subformula property in the sense that one can work out what formulas can appear in a proof of a particular goal sequent. One such system is that of [5]. It can be extended to inductive constructions and then turned into a term generator for “PolyFix \ {fix}”. This extension serves as starting point for our search of PolyFix* terms typable under α∗ but not under just α, in the next section. Specifically, terms typable to a given type in “PolyFix \ {fix}” can be generated based on a system extending that of [5] by rules for inductive definitions (to deal with lists, pairs, . . . ) a` la those of [4]. The resulting system retains some of the rules from PolyFix (e.g., (Var), (Nil), (Unit), (Abs), (Pair), and (Left) from Fig. 1), naturally drops (Fix), replaces (App) by1 Γ ; Σ, x :: τ1 , y :: τ2 t :: τ (App’) Γ ; Σ, f :: τ1 → τ2 , x :: τ1 [y → f x]t :: τ adds the rule (Arrow→): Γ ; Σ, x :: τ1 , g :: τ2 → τ3 t1 :: τ2 Γ ; Σ, y :: τ3 t2 :: τ Γ ; Σ, f :: (τ1 → τ2 ) → τ3 [y → f (λx :: τ1 .[g → λz :: τ2 .f (λu :: τ1 .z)]t1 )]t2 :: τ and further rules for dealing with data types. We do not go into more detail here (but see Section 4 of [13]), because we will anyway discuss the full rule system for counterexample term search in PolyFix* in the next section.
5
Term Search, the Algorithm
The prime way to provoke a term to be typable in PolyFix* under context α∗ , . . . but not under context α, . . . is to use fix at a type whose membership in Pointed depends on α being ∗ -annotated.2 So a natural first rule for our new system TermFind is the following one: Γ τ ∈ / Pointed (Bottom) Γ ; Σ ⊥τ :: τ 1 2
We use the notation [y → · · · ]t for substituting y in t by “· · · ”. The typing rules for case t of {· · · } in PolyFix* also each have a “∈ Pointed” precondition, but those terms do not introduce any ⊥ values by themselves. Only if the evaluation of t is already ⊥, the overall term would evaluate to ⊥. Thus, every occurrence of ⊥ (to which all trouble with naive free theorems in our setting eventually boils down) originates from a use of fix.
Automatically Generating Counterexamples to Naive Free Theorems
183
where we use the syntactic abbreviation ⊥τ = fix (λx :: τ.x). Note the new symbol , instead of , for term level rules. The rules defining the predicate Pointed on types are kept unchanged, and Γ τ ∈ / Pointed simply means that Γ τ ∈ Pointed is not derivable. In a judgment of the form Γ ; Σ t :: τ , the type and term variable contexts Γ and Σ, as well as the type τ , are considered as “inputs”, while the term t is taken to be the produced “output”. If α, . . . τ ∈ / Pointed but α∗ , . . . τ ∈ Pointed, then with an α, . . . ; Σ ⊥τ :: τ obtained according to the above rule we have really found a term in the intended difference set. Of course, we will not always be so lucky that the type for which we originally want to find a counterexample term is itself one whose “pointedness” depends on an α of interest. So in general we have to do a real search for an opportunity to “inject” a harmful ⊥. For example, if the type for which we are searching for a counterexample is Either τ1 τ2 , we could try to find a counterexample term for τ1 and then build the one for Either τ1 τ2 from it via Leftτ2 . That is, TermFind takes over (modulo replacing by ) rule (Left) from Fig. 1 and also the corresponding rule (Right). At list types, we can search at the element type level, then wrap the result in a singleton list: Γ ; Σ t :: τ (Wrap) Γ ; Σ (t : [ ]τ ) :: [τ ] For pair types we have a choice similarly to (Left) vs. (Right) above. The other pair component in each case is simply filled with an appropriate ⊥-term:3 Γ ; Σ t :: τ1 (Pair1 ) Γ ; Σ (t, ⊥τ2 ) :: (τ1 , τ2 )
Γ ; Σ t :: τ2 (Pair2 ) Γ ; Σ (⊥τ1 , t) :: (τ1 , τ2 )
For the unit type, there is no hope of introducing an “α is ∗ -annotated”-enforcing ⊥ directly, i.e., without using material from the term variable context Σ. For function types we can bring material into the term variable context by using the rule (Abs), with instead of , from Fig. 1. Material that once is in the context may or may not be useful for eventually constructing a counterexample. But in certain cases we can simplify it without danger of missing out on some possible counterexample. For example, if we have a pair (variable) in the context, then we can be sure that if a counterexample term can be found using it, the same would be true if we replaced the pair by its components. After all, from any such counterexample using the components one could also produce a counterexample based on the pair itself, involving simple projections. Hence: Γ ; Σ, x :: τ1 , y :: τ2 t :: τ (Proj) Γ ; Σ, p :: (τ1 , τ2 ) [x → fst p, y → snd p]t :: τ where we use abbreviations fst p and snd p, e.g., fst p = case p of {(x, y) → x}. 3
Intuitively, once we have found a counterexample term t :: τ1 , we can as well add ⊥ in other places. Note that using, say, ⊥τ2 does not require us to put a Γ τ2 ∈ Pointed constraint in. After all, we will require typability of the resulting term in PolyFix* only under the context with all type variables ∗ -annotated anyway.
184
D. Seidel and J. Voigtl¨ ander
Similarly, we obtain the following rule: Γ ; Σ, h :: τ1 t :: τ (Head) Γ ; Σ, l :: [τ1 ] [h → headτ1 l]t :: τ where we use the abbreviation headτ1 l = case l of {[ ] → ⊥τ1 ; x : y → x}. (As with rule (Wrap), we only ever use lists via their first elements.) For a type Either τ1 τ2 in the context, since we do not know up front whether we will have more success constructing a counterexample when replacing it with τ1 or with τ2 , we get two rules that are in competition with each other. One of them (the other one, (Dist2 ), is analogous) looks as follows: Γ ; Σ, x :: τ1 t :: τ (Dist1 ) Γ ; Σ, e :: Either τ1 τ2 [x → fromLeftτ1 e]t :: τ where we abbreviate fromLeftτ1 e = case e of {Left x → x ; Right x → ⊥τ1 }. Unit and unpointed types in the context are of no use for counterexample generation, because no relevant α-affecting ⊥ can be “hidden” in them. The following two rules reflecting this observation do not really contribute to the discovery of a solution term, but can shorten the search process by removing material that then needs not to be considered anymore. Γ ; Σ t :: τ (Drop1 ) Γ ; Σ, x :: () t :: τ
Γ τ1 ∈ / Pointed Γ ; Σ t :: τ (Drop2 ) Γ ; Σ, x :: τ1 t :: τ
What remains to be done is to deal with (pointed) function types in the context. We distinguish several cases according to what is on the input side of those function types. The intuitive idea is that an input of an input corresponds to an output (akin to a logical double negation translation). Thus, for example, the following rule corresponds to the rule (Wrap) seen earlier: Γ τ2 ∈ Pointed Γ ; Σ, g :: τ1 → τ2 t :: τ (Wrap→) Γ ; Σ, f :: [τ1 ] → τ2 [g → λx :: τ1 .f (x : [ ]τ1 )]t :: τ For pairs we introduce a rule corresponding to currying: Γ τ3 ∈ Pointed Γ ; Σ, g :: τ1 → τ2 → τ3 t :: τ (Pair→) Γ ; Σ, f :: (τ1 , τ2 ) → τ3 [g → λx :: τ1 .λy :: τ2 .f (x, y)]t :: τ Similarly, we introduce the following rule (Either→): Γ τ3 ∈ Pointed Γ ; Σ, g :: τ1 → τ3 , h :: τ2 → τ3 t :: τ Γ ; Σ, f :: Either τ1 τ2 → τ3 [g → λx :: τ1 .f (Leftτ2 x), h → λx :: τ2 .f (Rightτ1 x)]t :: τ Independently of the input side of a function type in the context we can always simplify such a type by providing a dummy ⊥-term: Γ ; Σ, x :: τ2 t :: τ (Bottom→) Γ ; Σ, f :: τ1 → τ2 [x → f ⊥τ1 ]t :: τ
Automatically Generating Counterexamples to Naive Free Theorems
185
This ⊥ by itself will not generally contribute to the overall constructed term being a counterexample, but can enable important progress in the search. A variant of (Bottom→) that would be more directly promising for injecting a harmful ⊥ would be if we demanded Γ τ1 ∈ / Pointed as a precondition. The resulting rule would correspond to rule (Bottom) from the beginning of this section. However, here additional effort is needed in order to ensure that a t of type τ generated from context Γ ; Σ, x :: τ2 can really lead to the term [x → f ⊥τ1 ]t being a counterexample (of type τ , in context Γ ; Σ, f :: τ1 → τ2 ). Namely, we need to ensure that x really occurs in t, and is actually used in a somehow “essential” way, because otherwise the ⊥τ1 we inject would be for naught, and could not provoke a breach of the naive free theorem under consideration. This notion of “essential use” (related to relevance typing [6,20]) will be formalized by a separate rule system ◦ below. Using it, our rule variant becomes: Γ τ1 ∈ / Pointed Γ ; Σ, x◦ :: τ2 ◦ t :: τ (Bottom→’) Γ ; Σ, f :: τ1 → τ2 [x → f ⊥τ1 ]t :: τ Of the rule (Arrow→) mentioned at the end of Section 4, we also give a variant, called (Arrow→◦ ), that employs ◦ to enforce an essential use. It is: Γ τ2 , τ3 ∈ Pointed Γ ; Σ, x :: τ1 , g :: τ2 → τ3 t1 :: τ2 Γ ; Σ, y ◦ :: τ3 ◦ t2 :: τ Γ ; Σ, f :: (τ1 → τ2 ) → τ3 [y → f (λx :: τ1 .[g → λz :: τ2 .f (λu :: τ1 .z)]t1 )]t2 :: τ If Γ τ2 ∈ / Pointed, then the use of (Bottom→’) will promise more immediate success, by requiring only (an equivalent of) the last precondition. The purpose of rule system ◦ is to produce, given a pair of type and term variable contexts in which some term variables may be ◦ -annotated, a term of a given type such that an evaluation of that term is not possible without accessing the value of at least one of the term variables that are ◦ -annotated in the context. The simplest rule naturally is as follows: Γ ; Σ, x◦ :: τ ◦ x :: τ (Var◦ ) Other rules are by analyzing the type of some ◦ -annotated term variable in the context. For example, if we find a so annotated variable of a pair type, then an essential use of that variable can be enforced by enforcing the use of either of its components. This observation leads to the following variant of rule (Proj): Γ ; Σ, x◦ :: τ1 , y ◦ :: τ2 ◦ t :: τ (Proj◦ ) Γ ; Σ, p :: (τ1 , τ2 ) ◦ [x → fst p, y → snd p]t :: τ ◦
Analogously, ◦ -variants of the rules (Head), (Dist1 ), and (Dist2 ) are obtained. The only way to enforce the use of a variable of function type is to apply it to some argument and enforce the use of the result. The argument itself is irrelevant for doing so, hence we get the following variant of rule (Bottom→): Γ ; Σ, x◦ :: τ2 ◦ t :: τ (Bottom→◦ ) Γ ; Σ, f ◦ :: τ1 → τ2 ◦ [x → f ⊥τ1 ]t :: τ
186
D. Seidel and J. Voigtl¨ ander
Another possibility to use a ◦ -annotated term variable is to provide it as argument to another term variable that has a function type with matching input side. That term variable of function type needs not itself be ◦ -annotated. In fact, if it were, it could already have been eliminated by (Bottom→◦ ). But it is essential to enforce that the function result is used in the overall term if the argument is not already used via other means. Thus, we get as variant of (App’): Γ τ2 ∈ Pointed Γ ; Σ, x◦ :: τ1 , y ◦ :: τ2 ◦ t :: τ (App’◦ ) Γ ; Σ, f :: τ1 → τ2 , x◦ :: τ1 ◦ [y → f x]t :: τ Finally, we can try to use a ◦ -annotated term variable as scrutinee for case. If we have a ◦ -annotated term variable of unit type, we need axioms of the form Γ ; Σ, x◦ :: () ◦ (case x of {() → t}) :: τ where we just have to guarantee that t has type τ in context Γ ; Σ and that it does not evaluate to ⊥. Since the “first phase” has already done most of the work of deconstructing types, the following axioms suffice: Γ ; Σ, y :: τ, x◦ :: () ◦ (case x of {() → y}) :: τ (Unit◦ –Var’) Γ ; Σ, x◦ :: () ◦ (case x of {() → ()}) :: () (Unit◦ –Unit’) Γ ; Σ, x◦ :: () ◦ (case x of {() → [⊥τ ]}) :: [τ ] (Unit◦ –List’) plus similar (Unit◦ –Pair’) and (Unit◦ –Either’). Note that in (Unit◦ –Var’), using y is okay, since we can find environments in which its evaluation is non-⊥. Axioms analogous to the ones just considered are added for every other possible type of x supporting case, e.g., Γ ; Σ, y :: τ, x◦ :: [τ1 ] ◦ (case x of {[z] → y}) :: τ (List◦ –Var’) So far we have not set up an explicit order in which the rules of and ◦ should be applied. In fact, they could be used in arbitrary order, and using an appropriate measure (based on the structure and nesting levels of types) we have shown that even full backtracking search terminates [13]. That is, starting with a triple of Γ (potentially containing ∗ -annotated type variables), Σ, and τ , either a term t with Γ ; Σ t :: τ is produced, or it is guaranteed that there is no such term. In the implementation, we actually use a safely reduced amount of backtracking and an optimized order, both based on ideas from [4]. The full rule systems, incorporating order and backtracking information, are given in [13]. Example. For “α; t :: ([α] → ()) → ()” as target judgement, the term t = λf :: [α] → ().(λx :: α.f (x : [ ]α )) ⊥α is found as follows: αα∈ / Pointed α; x◦ :: () ◦ x :: () (Var◦ ) (Bottom→’) α () ∈ Pointed α; g :: α → () (g ⊥α ) :: () (Wrap→) α; f :: [α] → () ((λx :: α.f (x : [ ]α )) ⊥α ) :: () (Abs) α; (λf :: [α] → ().(λx :: α.f (x : [ ]α )) ⊥α ) :: ([α] → ()) → ()
Automatically Generating Counterexamples to Naive Free Theorems
187
This t is such that α∗ ; t :: ([α] → ()) → () holds in PolyFix*, while α; t :: ([α] → ()) → () does not. Indeed, for Γ = α∗ we have: Γ ; f :: [α] → (), x :: α x :: α (Var) (Abs) Γ ; f :: [α] → () (λx :: α.x) :: α → α .. (Fix) . Γ ; f :: [α] → () ⊥α :: α (App) Γ ; f :: [α] → () ((λx :: α.f (x : [ ]α )) ⊥α ) :: () (Abs) Γ ; (λf :: [α] → ().(λx :: α.f (x : [ ]α )) ⊥α ) :: ([α] → ()) → () α∗ ∈ Γ Γ α ∈ Pointed
while for Γ = α there would be no successful typing. In general, we get: Theorem 3. Let (Γ, Σ, τ ) be an input for the term search. Let Γ ∗ be as Γ , but all type variables ∗ -annotated. If term search returns some t, i.e., if Γ ; Σ t :: τ , then in PolyFix* Γ ; Σ t :: τ does not hold, but Γ ∗ ; Σ t :: τ does. An important completeness claim (we have not formally proved, and indeed have no clear idea of how a proof would go), based on the strategy of injecting harmful ⊥ whenever possible, is that if TermFind finds no term, then the naive free theorem in question, i.e., the one omitting all strictness conditions corresponding to non-∗ type variables in Γ , actually holds.
6
Producing Full Counterexamples
In the introduction we proclaimed the construction of complete counterexamples to naive free theorems. Thus, ideally, if α, β ∗ , . . . ; Σ t :: τ , then we would want this t to really be a counterexample to the statement given by Theorem 2 for α, β ∗ , . . . ; Σ t :: τ in PolyFix*. That is, we would want to establish the negation of: “([[t]]σ1 , [[t]]σ2 ) ∈ Δτ,ρ for every θ1 , θ2 , ρ, σ1 , and σ2 such that ρ(α) ∈ Rel(θ1 (α), θ2 (α)), ρ(β) ∈ Rel ⊥ (θ1 (β), θ2 (β)), . . . , and for every x :: τ occurring in Σ, (σ1 (x), σ2 (x)) ∈ Δτ ,ρ .” Clearly, Theorem 3 alone does not suffice to do so. Instead, establishing the intended negation requires providing specific environments θ1 , θ2 , ρ, σ1 , and σ2 that do fulfill all preconditions mentioned above, but not ([[t]]σ1 , [[t]]σ2 ) ∈ Δτ,ρ . One thing we know for sure is that ρ(α) should be nonstrict, i.e., in Rel \ Rel ⊥ , because for every ρ(α) ∈ Rel ⊥ (θ1 (α), θ2 (α)) the above-quoted statement (and not its negation) would be true by Theorem 1. Regarding the type environments, it suffices for our purposes to let θ1 and θ2 map each type variable to the simplest type interpretation that admits both strict and nonstrict functions, namely to the interpretation of the unit type, {⊥, ()}. This predetermines ρ(α) ∈ Rel \ Rel ⊥ to the value of λx :: ().(), while we choose identity functions for ρ(β) ∈ Rel ⊥ . What remains to be done is to specify σ1 and σ2 . Here more complex requirements must be met. For example, for every instance of the rule (Bottom) in
188
D. Seidel and J. Voigtl¨ ander
TermFind we need to provide σ1 and σ2 such that for every x :: τ occurring in Σ, (σ1 (x), σ2 (x)) ∈ Δτ ,ρ , but that not ([[⊥τ ]]σ1 , [[⊥τ ]]σ2 ) ∈ Δτ,ρ . The latter, (⊥, ⊥) ∈ / Δτ,ρ , will be guaranteed by Γ τ ∈ / Pointed and our choices for ρ. But for the former we need to be able to produce for every type τ concrete values related by Δτ ,ρ with our fixed ρ. This production can be achieved based on the structure of τ , but we omit details here. (They are contained in [13].) For the remaining rules in TermFind we mainly need to either propagate already found values unchanged, or manipulate and/or combine them appropriately. For usefully handling the rule (Abs) the introduction of an additional mechanism is required, explained as follows. For this rule we can assume that we have already found some σ1 and σ2 with, in particular, (σ1 (x), σ2 (x)) ∈ Δτ1 ,ρ and ([[t]]σ1 , [[t]]σ2 ) ∈ / Δτ2 ,ρ . What we need is to provide σ1 and σ2 with ([[t ]]σ1 , [[t ]]σ2 ) ∈ / Δτ1 →τ2 ,ρ for t = (λx :: τ1 .t). This task can be solved by choos ing σ1 and σ2 to simply be σ1 and σ2 without the bindings for x.4 But then the assignments of values to term variables from the context would not suffice to gather and keep all the information required to establish, and eventually demonstrate to the user of our system, that the overall found term is a counterexample. Hence, we introduce an additional construct, called disrelator, which keeps track of how to manifest, based on the current term, a conflict to the parametricity theorem that was provoked somewhere above in the derivation tree. In the case of the rule (Abs) we precisely record that, in order to “navigate” towards the conflict, the term in the conclusion needs to be applied to the values produced for the additional context term variable in the premise. We do not go into further details as presented in [13] here, about the treatment of other rules, both due to a lack of space and because these are not needed for the full example run shown in the following figure: αα∈ / Pointed (Bottom)
α; x :: [α] t2 :: α
t2 = ⊥α
σ1 = {x → [()]}, σ2 = {x → [()]}
= (α, (id, id))
t1 = ⊥α : [ ]α
σ1 = {x → [()]}, σ2 = {x → [()]}
= (α, (head, head))
t0 = λx :: [α].⊥α : [ ]α
σ1 = {}, σ2 = {}
= (α, (head ◦ (λt.t [()]), head ◦ (λt.t [()])))
(Wrap)
α; x :: [α] t1 :: [α] (Abs)
α t0 :: [α] → [α]
The first two columns illustrate the term search process by applying rules and assembling the result term. The third column shows the environments σ1 and σ2 at each stage, and the fourth one shows the disrelators as compositions of single “navigation steps” as obtained from each rule. Additionally, the disrelators remember the specific (subpart of the original) type at which the conflict was provoked, i.e., to which the navigation leads. Consequently, in each row of the figure we see the description of a complete counterexample to the naivified parametricity theorem. In particular, the last row tells us that for the term λx :: [α].⊥α : [ ]α of type [α] → [α] evaluation (in empty environments) leads to values v1 and v2 which are unrelated because head (v1 [()]) and head (v2 [()]) 4
Then, [[t]]σ1 = ([[t ]]σ1 σ1 (x)) and [[t]]σ2 = ([[t ]]σ2 σ2 (x)), and thus ([[t ]]σ1 , [[t ]]σ2 ) ∈ Δτ1 →τ2 ,ρ would contradict (σ1 (x), σ2 (x)) ∈ Δτ1 ,ρ and ([[t]]σ1 , [[t]]σ2 ) ∈ / Δτ2 ,ρ .
Automatically Generating Counterexamples to Naive Free Theorems
189
are not related by ρ(α). Indeed, both head (v1 [()]) and head (v2 [()]) will be ⊥, and (⊥, ⊥) is not in the relation graph of the function λx :: ().() which we chose for ρ(α). The result is a counterexample to the naive free theorem (2) from the introduction in very much the same spirit as the counterexample (3) discussed there, but now obtained automatically. Things can become more complicated and terms found by TermFind are not always suitable for full counterexample generation. The reason is the rule (Arrow→◦ ). It splits the derivation tree into two term search branches, and the same term variables may be used in the two branches in different ways. As a consequence, the (σ1 , σ2 ) pairs obtained for these two branches separately may disagree on some term variables. To deal with these situations, we keep track of a creation history for values in the σi , recording which choices were essential. Whenever the recorded information prevents a merging at the rule (Arrow→◦ ) we abort. Another problem arises from the double use of f in the conclusion of (Arrow→◦ ). Here f might (and sometimes does) fulfill different roles and it is not always possible to provide a single pair of values (to be assigned to f in σ1 and σ2 ) that meets all requirements. Our solution is to switch to a simplified version of (Arrow→◦ ) that omits g and thus the double use of f : Γ τ2 , τ3 ∈ Pointed Γ ; Σ, x :: τ1 t1 :: τ2 Γ ; Σ, y ◦ :: τ3 ◦ t2 :: τ Γ ; Σ, f :: (τ1 → τ2 ) → τ3 [y → f (λx :: τ1 .t1 )]t2 :: τ The algorithm with this changed rule, history tracking, and value and disrelator construction is what we implemented. It lacks the completeness claim of TermFind, but in return we proved correctness in the sense that the counterexamples it produces really contradict the naive free theorems in question. That proof, as well as the proofs of other mentioned results, can be found in [13].
7
Related and Future Work
We have discussed the relation of our approach and results here to [1] and [10] in the introduction and at the end of Section 3. Another related work is [14], where we perform the parametricity-related programme of [10] for the Haskell primitive seq, taking the lessons of [7] into account. This provides the basis for a future extension of the work presented here to a more complex language, in particular for producing counterexamples that demonstrate when and why seq-imposed side conditions in free theorems are really necessary for a given type. With [14] the part of the development which would be required for that setting up to and including Section 3 of the present paper is already done. Another interesting direction for future work would be to investigate counterexample generation for free theorems in more exotic settings, like the one where nondeterminism and free variables complicate the situation [2].
190
D. Seidel and J. Voigtl¨ ander
References 1. Augustsson, L.: Putting Curry-Howard to work (Invited talk). At Approaches and Applications of Inductive Programming (2009) 2. Christiansen, J., Seidel, D., Voigtl¨ ander, J.: Free theorems for functional logic programs. In: Proceedings of Programming Languages meets Program Verification, pp. 39–48. ACM Press, New York (2010) 3. Claessen, K., Hughes, R.J.M.: QuickCheck: A lightweight tool for random testing of Haskell programs. In: Proceedings of International Conference on Functional Programming, pp. 268–279. ACM Press, New York (2000) 4. Corbineau, P.: First-order reasoning in the calculus of inductive constructions. In: Berardi, S., Coppo, M., Damiani, F. (eds.) TYPES 2003. LNCS, vol. 3085, pp. 162–177. Springer, Heidelberg (2004) 5. Dyckhoff, R.: Contraction-free sequent calculi for intuitionistic logic. Journal of Symbolic Logic 57(3), 795–807 (1992) 6. Holdermans, S., Hage, J.: Making “stricterness” more relevant. In: Proceedings of Partial Evaluation and Program Manipulation, pp. 121–130. ACM Press, New York (2010) 7. Johann, P., Voigtl¨ ander, J.: Free theorems in the presence of seq. In: Proceedings of Principles of Programming Languages, pp. 99–110. ACM Press, New York (2004) 8. Johann, P., Voigtl¨ ander, J.: The impact of seq on free theorems-based program transformations. Fundamenta Informaticae 69(1–2), 63–102 (2006) 9. Lakatos, I.: Proofs and Refutations: The Logic of Mathematical Discovery. Cambridge University Press, Cambridge (1976) 10. Launchbury, J., Paterson, R.: Parametricity and unboxing with unpointed types. In: Riis Nielson, H. (ed.) ESOP 1996. LNCS, vol. 1058, pp. 204–218. Springer, Heidelberg (1996) 11. Reynolds, J.C.: Towards a theory of type structure. In: Robinet, B. (ed.) Programming Symposium. LNCS, vol. 19, pp. 408–423. Springer, Heidelberg (1974) 12. Reynolds, J.C.: Types, abstraction and parametric polymorphism. In: Proceedings of Information Processing, pp. 513–523. Elsevier, Amsterdam (1983) 13. Seidel, D., Voigtl¨ ander, J.: Automatically generating counterexamples to naive free theorems. Technical Report TUD-FI09-05, Technische Universit¨ at Dresden (2009), http://www.iai.uni-bonn.de/~ jv/TUD-FI09-05.pdf 14. Seidel, D., Voigtl¨ ander, J.: Taming selective strictness. In: Proceedings of Arbeitstagung Programmiersprachen. LNI, vol. 154, pp. 2916–2930. GI (2009) 15. Stenger, F., Voigtl¨ ander, J.: Parametricity for Haskell with imprecise error semantics. In: Curien, P.-L. (ed.) TLCA 2009. LNCS, vol. 5608, pp. 294–308. Springer, Heidelberg (2009) 16. Voigtl¨ ander, J.: Much ado about two: A pearl on parallel prefix computation. In: Proceedings of Principles of Programming Languages, pp. 29–35. ACM Press, New York (2008) 17. Voigtl¨ ander, J.: Semantics and pragmatics of new shortcut fusion rules. In: Garrigue, J., Hermenegildo, M.V. (eds.) FLOPS 2008. LNCS, vol. 4989, pp. 163–179. Springer, Heidelberg (2008) 18. Voigtl¨ ander, J.: Bidirectionalization for free! In: Proceedings of Principles of Programming Languages, pp. 165–176. ACM Press, New York (2009) 19. Wadler, P.: Theorems for free! In: Proceedings of Functional Programming Languages and Computer Architecture, pp. 347–359. ACM Press, New York (1989) 20. Wright, D.A.: A new technique for strictness analysis. In: Abramsky, S., Maibaum, T. (eds.) TAPSOFT 1991. LNCS, vol. 494, pp. 235–258. Springer, Heidelberg (1991)
Applying Constraint Logic Programming to SQL Test Case Generation Rafael Caballero, Yolanda Garc´ıa-Ruiz, and Fernando S´ aenz-P´erez Departamento de Sistemas Inform´ aticos y Computaci´ on Universidad Complutense de Madrid, Spain {rafa,fernan}@sip.ucm.es,
[email protected]
Abstract. We present a general framework for generating SQL query test cases using Constraint Logic Programming. Given a database schema and a SQL view defined in terms of other views and schema tables, our technique generates automatically a set of finite domain constraints whose solutions constitute the test database instances. The soundness and correctness of the technique w.r.t. the semantics of Extended Relational Algebra is proved. Our setting has been implemented in an available tool covering a wide range of SQL queries, including views, subqueries, aggregates and set operations.
1
Introduction
Checking the correctness of a piece of software is generally a labor-intensive and time-consuming work. In the case of the declarative relational database language SQL [17] this task becomes especially painful due to the size of actual databases; it is usual to find select queries involving thousands of database rows, and reducing the size of the databases for testing is not a trivial task. The situation becomes worse when we consider correlated views. Thus, generating test database instances to show the possible presence of faults during unit testing has become an important task. Much effort has been devoted to studying and improving the different possible coverage criteria for SQL queries (see [21,1] for a general discussion, [3,18] for the particular case of SQL). However, the common situation of queries defined through correlated views had not yet been considered. In this work we address the problem of generating test cases for checking correlated SQL queries. A set of related views is transformed into a constraint satisfiability problem whose solution provides an instance of the database which will constitute a test case. This technique is known as constraint-based test data generation [7], and has already been applied to SQL basic queries [20]. Other recent works [2] use RQP (Reverse Query Processing) to generate different database instances for a given query and a result of that query. In [6] the problem of generating database test cases in the context of Java programs interacting with relational databases, focusing on the relation between SQL queries and
This work has been partially supported by the Spanish projects TIN2008-06622C03-01, S-0505/TIC/0407, S2009TIC-1465 and UCM-BSCH-GR58/08-910502
M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 191–206, 2010. c Springer-Verlag Berlin Heidelberg 2010
192
R. Caballero, Y. Garc´ıa-Ruiz, and F. S´ aenz-P´erez
program values. The contributions of our work w.r.t. previous related proposals are twofold: First, as mentioned above, the previous works focus on a single SQL query instead of considering the more usual case of a set of correlated views. Observe that the problem of test case generation for views cannot be reduced to solving the problem for each query separately. For instance, consider the two simple views, that assume the existence of a table t with one integer attribute a: create view v2(c) as select v1.b from v1 where v1.b>5; create view v1(b) as select t.a from t where t.a>8; A positive test case for v2 considering its query as a non-correlated query could consist of a single row for v1 containing for instance v1.b = 6, since 6 > 5 and therefore this row fulfils the v2 condition. However, 6 is not a possible value for v1.b because v1.b can contain only numbers greater than 8. Therefore the connection between the two views must be taken into account (a valid positive test case would be for instance a single row in t with t.a = 9). Second, we present a formal definition of the algorithm for defining the constraints. This definition allows us to prove the soundness and (weak) completeness of the technique with respect to the Extended Relational Algebra [9]. The next section presents the basis of our SQL setting. Section 3 introduces the concept of positive and negative test cases, while Section 4 defines the constraints whose solution will constitute our test cases. This section also introduces the main theoretical result, which is proven in Appendix A. Section 5 discusses the prototype implementation and Section 6 presents the conclusions.
2
Representing SQL Queries
The first formal semantics for relational databases were based on the concept of set (e.g. relational algebra, tuple calculus [4]). However these formalisms are incomplete with respect to the treatment of non-relational features such as repeated rows and aggregates, which are part of practical languages such as SQL. Therefore, other semantics based on multisets [5], also known in this context as bags, have been proposed. In this paper we adopt the point of view of the Extended Relational Algebra [12,9]. We start by defining the concepts of database schemas and instances but with a Logic Programming flavor. In particular the database instance rows will be considered logic substitutions of attributes by values. 2.1
Relational Database Schemas and Instances
A table schema is of the form T (A1 , . . . , An ), with T the table name and Ai attribute names for i = 1 . . . n. We will refer to a particular attribute A by using the notation T.A. Each attribute A has an associated type (integer, string, . . . ) represented by type(T.A). An instance of a table schema T (A1 , . . . , An ) will be represented as a finite multiset of functions (called rows) {|μ1 , μ2 , ..., μm |}
Applying Constraint Logic Programming to SQL Test Case Generation
193
such that dom(μi ) = {T.A1 , . . . , T.An }, and μi (T.Aj ) ∈ type(T.Aj ) for every i = 1, . . . , m, j = 1, . . . , n. Observe that we qualify the attribute names in the domain by table names. This is done because in general we will be interested in rows that combine attributes from different tables, usually as result of cartesian products. In the following, it will be useful to consider each attribute T.Ai in dom(μ) as a logic variable, and μ as a logic substitution. The concatenation of two rows μ1 , μ2 with disjoint domain is defined as the union of both functions represented as μ1 μ2 . Given a row μ and an expression e we use the notation eμ to represent the value obtained applying the substitution μ to e. Analogously, let S be a multiset of rows {|μ1 , . . . , μn |} and let e be an expression. Then (e)S represents the result of replacing each attribute T.A occurring in an aggregate subexpression of e by the multiset {|μ1 (T.A), . . . , μn (T.A)|}. The attributes T.B not occurring in aggregate subexpressions of e must take the same value for every μi ∈ S, and are replaced by such value. For instance, let → 2, T.B → 5}, μ2 = e = sum(T.A)+T.B and S = {|μ1 , μ2 , μ3 |} with μ1 = {T.A {T.A → 3, T.B → 5}, μ3 = {T.A → 4, T.B → 5}. Then (e)S = sum({|2, 3, 4|}) + 5. If dom(μ) = {T.A1 , . . . , T.An } and ν = {U.A1 → T.A1 , . . . , U.An → T.An } (i.e., ν is a table renaming) we will use the notation μU to represent the substitution composition ν◦μ. The previous concepts for substitutions can be extended to multisets of rows in a natural way. For instance, given the multiset of rows S and the row μ, Sμ represents the application of μ to each member of the multiset. A database schema D is a tuple (T , C, V), where T is a finite set of tables, C a finite set of database constrains and V a finite set of views (defined below). In this paper we consider only primary key and foreign key constraints, defined as traditionally in relational databases (see Subsection 4.1). A database instance d of a database schema is a set of table instances, one for each table in T verifying C (thus we only consider valid instances). To represent the instance of a table T in d we will use the notation d(T ). A symbolic database instance ds is a database instance whose rows can contain logical variables. We say that ds is satisfied by a substitution μ when (ds μ) is a database instance. μ must substitute all the logic variables in ds by domain values. 2.2
Extended Relational Algebra and SQL Queries
Next we present the basics of Extended Relational Algebra (ERA from now on) [12,9] which will be used as semantics of our framework. There are other approaches for defining SQL semantics such as [14], but we have chosen ERA because it provides an operational semantics very suitable for proving the correctness of our technique. Let R and S be multisets. Let μ be any row occurring n times in R and m times in S. Then ERA consists of the following operations: – Unions and intersections. The union of R and S, is a multiset R ∪ S in which the row μ occurs n + m times. Analogously R ∩ S, the intersection of R and S, is a multiset in which the row μ occurs min(n, m) times.
194
R. Caballero, Y. Garc´ıa-Ruiz, and F. S´ aenz-P´erez
– Projection. The expression πe1 →A1 ,...,en →An (R) produces a new relation producing for each row μ ∈ R a new row {A1 → e1 μ, . . . , An → en μ}. The resulting multiset has the same number of rows as R. – Selection. Denoted by σC (R), where C is the condition that must be satisfied for all rows in the result. The selection operator on multisets applies the selection condition to each row occurring in the multiset independently. – Cartesian products. Denoted as R × S, each row in the first relation is paired with each row in the second relation. – Renaming. The expression ρS (R) changes the name of the relation R to S and the expression ρA/B (R) changes the name of the attribute A of R to B. – Aggregate operators. These operators are used to aggregate the values in one column of a relation. Here we consider sum, avg, min, max and count. – Grouping operator. Denoted by γ, this operator allows us to consider the rows of a relation in groups, corresponding to the value of one or more attributes and to aggregate only within each group. This operation is denoted by γL (R), where L is a list of elements, each one either a grouping attribute, that is, an attribute of the relation R to which the γ is applied, or an aggregate operator applied to an attribute of the relation. To provide a name for the attribute corresponding to this aggregate in the result, an arrow and a new name are appended to the aggregate. It is worth observing that γL (R) will contain one row for each maximal group, i.e., for each group not strictly contained in a larger group. A relational database can be consulted by using queries and views defined over other views and queries. Queries are select SQL statements. In our setting we allow three kind of queries: – Basic queries of the form: Q = select e1 E1 , . . . , en En from R1 B1 , . . . , Rm Bm where Cw ; with Rj tables or views for j = 1 . . . m, ei , i = 1 . . . n expressions involving constants, predefined functions and attributes of the form Bj .A, 1 ≤ j ≤ m, and A an attribute of Rj . The meaning of any query Q in ERA is denoted
Q. In the case of basic queries is
Q = Πe1 →E1 ,...,en →En (σCw (R)) where R = ρB1 (R1 ) × . . . × ρBm (Rm ). – Aggregate queries, including group by and having clauses: Q = select e1 E1 , . . . , en En from R1 B1 , . . . , Rm Bm where Cw group by A1 , . . . , Ak having Ch ; In this case, the equivalent ERA expression is the following:
Q = Πe1 →E1 ,...,en →En (σCh (γL (σCw (R)))) → U1 , . . . , ul → Ul }, R defined as in the previous where L = {A1 , . . . , Ak , u1 case, ui , 1 ≤ i ≤ l the aggregate expressions occurring either in the select or in the having clauses, Ui new attribute names, ej , j = 1 . . . n the result of replacing each occurrence of ui by Ui in ej and analogously for Ch .
Applying Constraint Logic Programming to SQL Test Case Generation
195
– Set queries of the form Q = V1 {union, intersection} V2 ; with V1 , V2 views (defined below) with the same attribute names. The meaning of set queries in ERA is represented by ∪ and ∩ multiset operators for union and intersection, respectively: Q = V1 {∪, ∩} V2 In order to simplify our framework we assume queries such that: – Without loss of generality we assume that the where and having clauses only contain existential subqueries of the form exists Q (or not exists Q). It has been shown that other subqueries of the form ... in Q, ... any Q or ... all Q can be translated into equivalent subqueries with exists and not exists (see for instance [10]). Analogously, subqueries occurring in arithmetic expressions can be transformed into exists subqueries. – The from clause does not contain subqueries. This is not a limitation since all the subqueries in the from clause can be replaced by views. – We also do not allow the use of the distinct operator in the select clause. It is well-known that queries using this operator can be replaced by equivalent aggregate queries without distinct. In the language of ERA, this means that the operator δ for eliminating duplicates –not used here– is a particular case of the aggregate operator γ (see [9]). – Our setting does not allow: recursive queries, the minus operator, join operations, and null values. All these features, excepting the recursive queries, can be integrated in our setting, although they have not been considered here for simplicity. We also need to consider the concept of views, which can be thought of as new tables created dynamically from existing ones by using a query and allowing the renaming of attributes. The general form of a view is: create view V(A1 , . . . , An ) as Q, with Q a query and V.A1 , . . . V.An the name of the view attributes. Its meaning is defined as: V = ΠE1 →V.A1 ,...,En →V.An Q, with E1 , . . . , En the attribute names of the select clause in Q. In general, we will use the name relation to refer to either a table or a view. The semantics of a table T in a given instance d is defined simply as its rows: T = d(T ). A view query can depend on the tables of the schema and also on previously defined views (no recursion between views is allowed). Thus, the dependency tree of any view V in the schema is a tree with V labeling the root, and its children the dependency trees of the relations occurring in the from clause of its query. This concept can be easily extended to queries by assuming some arbitrary name labeling the root node, and to tables, where the dependency tree will be a single node labeled by the table name.
3
SQL Test Cases
In the previous section we have defined an operational semantics for SQL. Now we are ready for defining the concept of test case for SQL. We distinguish between positive and negative test cases:
196
R. Caballero, Y. Garc´ıa-Ruiz, and F. S´ aenz-P´erez
Definition 1. We say that a non-empty database instance d is a positive test case (PTC) for a view V when V = ∅. Observe that our definition excludes implicitly the empty instances, which will be considered as neither positive nor negative test cases. We require that the (positive or negative) test case contains at least one row that will act as witness of the possible error in the view definition. The overall idea is that we consider d a PTC for a view when the corresponding query answer is not empty. In a basic query this means that at least one tuple in the query domain satisfies the where condition. In the case of aggregate queries, a PTC will require finding a valid aggregate verifying the having condition, which in turn implies that all its rows verify the where condition. If the query is a set query, then the ranges are handled according to the set operation involved. The negative test cases (NTC) are defined by modifying the initial queries and then applying the concept of positive test case. With this purpose we use the notation QCw and Q(Cw ,Ch ) to indicate that Cw is the where condition in Q and Ch is the having condition in Q (when Q is an aggregate query). If QCw is of the form select e1 E1 , . . . , en En from R1 B1 , . . . , Rm Bm where Cw ; then the notation Qnot(Cw ) represents select e1 E1 , . . . , en En from R1 B1 , . . . , Rm Bm where not(Cw ); and analogously for Q(Cw ,Ch ) and Q(not(Cw ),Ch ) , Q(Cw ,not(Ch )) , and Q(not(Cw ),not(Ch )) . For instance, in the case of basic query, we expect that a NTC will contain some row in the domain of the view not verifying the where condition: Definition 2. We say that a database instance d is a NTC for a view V with associated basic query QCw when d is a PTC for Qnot(Cw ) . In queries containing aggregate functions, the negative case corresponds either to a tuple that does not satisfy the where condition, or to an aggregate not satisfying the having condition: Definition 3. We say that a database instance d is a NTC for a view V with associated aggregate query Q(Cw ,Ch ) if it is a PTC for either Q(not(Cw ),Ch ) , Q(Cw ,not(Ch )) , or Q(not(Cw ),not(Ch )) . Next is the definition of negative test cases for set queries: Definition 4. We say that a database instance d is a NTC for a view with query defined by: – A query union of Q1 , Q2 , if d is a NTC for both Q1 and Q2 . – A query intersection of Q1 , Q2 , if d is a NTC for either Q1 or Q2 . The main advantage of defining NTCs in terms of PTCs is that only a positive test case generator must be implemented. The previous definitions are somehow arbitrary depending on the coverage. For instance the NTCs for views with aggregate queries Q(Cw ,Ch ) could be defined simply as the PTCs for Q(not(Cw ),not(Ch )) .
Applying Constraint Logic Programming to SQL Test Case Generation
197
It is possible to obtain a test case which is both positive and negative at the same time thus achieving predicate coverage with respect to the where and having conditions (in the sense of [1]). We will call these tests PNTCs. For instance, for the query select A from T where A=5; with T a table with a single attribute A, → 5}, μ2 = {T.A → X}, X the test case d s.t. d(T ) = {|μ1 , μ2 |} with μ1 = {T.A any value different from 5, is a PNTC. However, this is not always possible. For instance, the query select R1 .A from T R1 where R1 .A=5 and not exists (select R2 .A from T R2 where R2 .A<>5); allows both PTCs and NTCs but no PNTC. Our tool will try to generate a PNTC for a view first, but if it is not possible it will try to obtain a PTC and a NTC separately.
4
Generating Constraints
The main goal of this paper is to use Constraint Logic Programming for generating test cases for SQL views. The process can be summarized as follows: 1. First, create a symbolic database instance. Each table will contain an arbitrary number of rows, and each attribute value in each row will correspond to a fresh logic variable with its associated domain integrity constraints. 2. Establish the constraints corresponding to the integrity of the database schema: primary and foreign keys. 3. Represent the problem of obtaining a test case as a constraint satisfaction problem. Next, we explain in detail phases 2 and 3. 4.1
Primary and Foreign Keys
Given a relation R with primary key pk(R) = {A1 , . . . , Am } and a symbolic instance d such that d(R) = {|μ1 , . . . , μn |}, we check that d satisfies pk(R) by establishing the following constraint: n i=1
(
n
j=i+1
(
m
(μi (R.Ak ) = μj (R.Ak )) ) )
k=1
that is, different rows must contain different values for the primary key. Given two relations R1 , R2 and an instance d such that d(R1 ) = {|μ1 , . . . , μn1 |}, d(R2 ) = {|ν1 , . . . , νn2 |} a foreign key from R1 referencing R2 , denoted by f k(R1 , R2 ) = {(A1 , . . . , Am ), (B1 , . . . , Bm )}, indicates that for each row μ in R1 there is a row ν in R2 such that (A1 μ, . . . , Am μ) = (B1 ν, . . . , Bm ν). Foreign keys are represented with the following constraints: n1 n2 m ( ( ( μi (R1 .Ak ) = νj (R2 .Bk ) ) ) ) i=1
j=1
k=1
198
4.2
R. Caballero, Y. Garc´ıa-Ruiz, and F. S´ aenz-P´erez
SQL Test Cases as a Constraint Satisfaction Problem
Now we are ready for describing the technique supporting our implementation. First we introduce the two following auxiliary operations over multisets: Definition 5. Let A = {|(a1 , b1 ), . . . , (an , bn )|}. Then we define the operations Π1 and Π2 as follows: Π1 (A) = {|a1 , . . . , an |}, Π2 (A) = {|b1 , . . . , bn |}. The following definition will associate a first order formula to every possible row of a relation. The idea is that the row will be in the relation instance iff the formula is satisfied. Definition 6. Let D be a database schema and d a database instance. We define θ(R) for every relation R in D as a multiset of pairs (ψ, u) with ψ a first order formula, and u a row. This multiset is defined as follows: 1. For every table T in D such that d(T ) = {|μ1 , . . . , μn |}: θ(T ) = {|(true, μ1 ), . . . , (true, μn )|} 2. For every view V = create view V(A1 , . . . , An ) as Q, θ(V ) = θ(Q){V.A1 → E1 , . . . , V.An → En } with E1 , . . . , En the attribute names in the select clause of Q. 3. If Q is a basic query of the form: select e1 E1 , . . . , en En from R1 B1 , . . . , Rm Bm where Cw ; Then: θ(Q) = {|(ψ1 ∧ . . . ∧ ψm ∧ ϕ(Cw , μ), sQ (μ)) | (ψ1 , ν1 ) ∈ θ(R1 ), . . . , (ψm , νm ) ∈ θ(Rm ), μ = ν1 B1 · · · νm Bm |} → (e1 μ), . . . , En → (en μ)}, and the first order formula with sQ (μ) = {E1 ϕ(C, μ) is defined as – if C does not contain subqueries, ϕ(C, μ) = C μ, with C obtained from C by replacing every occurrence of and by ∧, or by ∨, and not by ¬. – if C does contain subqueries, let Q= (exists QE ) be an outermost existential subquery in C, with θ(QE ) = {|(ψ1 , μ1 ), . . . (ψn , μn )|}. Let C be the result of replacing Q by true in C. Then ϕ(C, μ) = (∨ni=1 ψi ) ∧ ϕ(C , μ). 4. For set queries: – θ(V1 union V2 ) = θ(V1 ) ∪ θ(V2 ) with ∪ the multiset union. – (ψ, μ) ∈ θ(V1 intersection V2 ) with cardinality k iff (ψ1 , μ) ∈ θ(V1 ) with cardinality k1 , (ψ2 , μ) ∈ θ(V2 ) with cardinality k2 , k = min(k1 , k2 ) and ψ = ψ1 ∧ ψ2 .
Applying Constraint Logic Programming to SQL Test Case Generation
199
5. If Q includes aggregates, then it is of the form: select e1 E1 , . . . , en En from R1 B1 , . . . , Rm Bm where Cw group by e1 , . . . , ek having Ch Then we define: {|(ψ, μ) | (ψ1 , ν1 ), . . . , (ψm , νm ) ∈ (θ(R1 ) × . . . × θ(Rm )) ψ = ψ1 ∧ . . . ∧ ψm , μ = ν1 B1 · · · νm Bm |} θ(Q) = {|( (Π1 (A)) ∧ aggregate(Q, A), sQ (Π2 (A))) | A ⊆ P |}
P =
aggregate(Q, A) = group(Q,Π2 (A)) ∧ maximal(Q,A) ∧ ϕ(Ch , Π2 (A)) group(Q, S) = ( {|ϕ(Cw , μ) | μ ∈ S|} ) ∧ ( {| ((e1 )ν1 = (e1 )ν2 ∧ . . . ∧ (ek )ν1 = (ek )ν2 ) | ν1 , ν2 ∈ S|} ) maximal(Q, A) = {| (¬ψ ∨ ¬group(Q, Π2 (A) ∪ {|μ|}) | (ψ, μ) ∈ (P − A) |} Observe that the notation sQ (x) with Q a query is a shorthand for the row μ with domain {E1 , . . . , En } such that (Ei )x = (ei )x, with i = 1 . . . n, with select e1 E1 , . . . , en En the select clause of Q. If Ei ’s are omitted in the query, it is assumed that Ei = ei . Example 1. Let V1 , V2 , V3 and V4 be four SQL views defined as: create view V1 (A1 , A2 ) as select T1 .A E1 , T1 .B E2 from T1 T1 where T1 .A ≥ 10
create view V2 (A) as select T2 .C E1 from V1 V1 , T2 T2 where V1 .A1 + T2 .C = 0
create view V3 (A) as select(V1 .A1 ) E from V1 V1 where exists (select T2 .C E1 from T2 T2 where T2 .C = V1 .A1 )
create view V4 (A) as select V1 .A2 E from V1 V1 where V1 .A2 = “a” group by V1 .A2 having sum(V1 .A1 ) > 100;
Suppose table T1 has the attributes A, B while table T2 has only one attribute C. Consider the following symbolic database instances d(T1 ) = {|μ1 , μ2 |} → x1 , T1 .B → y1 }, μ2 = {T1 .A → and d(T2 ) = {|μ3 , μ4 |} with: μ1 = {T1 .A x2 , T1 .B → y2 } and μ3 = {T2 .C → z1 }, μ4 = {T2 .C → z2 }. Then: θ(T1 ) = {|(true, μ1 ), (true, μ2 )|}, θ(V1 ) = {| (x1 (x2 θ(V2 ) = {| (x1 (x1 (x2 (x2
θ(T2 ) = {|(true, μ3 ), (true, μ4 )|}
≥ 10, {V1 .A1 → x1 , V1 .A2 → y1 }) , ≥ 10, {V1 .A1 → x2 , V1 .A2 → y2 }) |} ≥ 10 ∧ x1 + z1 = 0, {V2 .A → z1 }), ≥ 10 ∧ x1 + z2 = 0, {V2 .A → z2 }), ≥ 10 ∧ x2 + z1 = 0, {V2 .A → z1 }), ≥ 10 ∧ x2 + z2 = 0, {V2 .A → z2 }) |}
200
R. Caballero, Y. Garc´ıa-Ruiz, and F. S´ aenz-P´erez
θ(V3 ) = {| (x1 ≥ 10 ∧ ((z1 = x1 ) ∨ (z2 = x1 )), {V3 .A → x1 }), (x2 ≥ 10 ∧ ((z1 = x2 ) ∨ (z2 = x2 )), {V3 .A → x2 }) |} → y1 }), (ψ2 , {V4 .A → y1 }), (ψ3 , {V4 .A → y2 })|} θ(V4 ) = {|(ψ1 , {V4 .A ψ1 = (x1 ≥ 10 ∧ x2 ≥ 10) ∧ (y1 = “a” ∧ y2 = “a” ∧ y1 = y2 )∧ (x1 + x2 > 100) ψ2 = (x1 ≥ 10) ∧ (y1 = “a”) ∧ (¬(x2 ≥ 10) ∨ ¬(y1 = “a” ∧ y2 = “a” ∧ y1 = y2 )) ∧ (x1 > 100) ψ3 = (x2 ≥ 10) ∧ (y2 > “a”) ∧ (¬(x1 ≥ 10) ∨ ¬(y1 = “a” ∧ y2 = “a” ∧ y1 = y2 )) ∧ (x2 > 100) For instance observe that V4 has an aggregate query with a group by over V1 . Since θ(V1 ) contains 2 tuples, θ(V4 ) contains three possible tuples, one for each possible group in V1 : the first group containing the two rows in V1 , the second corresponding only to the first row, and the third possibility a group containing only the second row in V1 . The following result and its corollary represent the main result of this paper, stating the soundness and completeness of our proposal: Theorem 1. Let D be a database schema and d a database instance. Assume that the views and queries in D do not include subqueries. Let R be a relation in D. Then μ ∈ R with cardinality k iff (μ, true) ∈ θ(R) with cardinality k. Proof. See Appendix A. The restriction to queries without subqueries is due to the limitations of ERA. The following corollary contains the idea for generating constraints that will yield the PTCs: Corollary 1. Let D be a database schema and ds a symbolic database instance. Assume that the views and queries in D do not include subqueries. Let R be a relation in D such that θ(R) = {|(ψ1 , μ1 ), . . . (ψn , μn )|}, and η a substitution satisfying ds . Then ds η is a PTC for R iff ( ni=1 ψi )η = true. Proof. Straightforward from Theorem 1: ( ni=1 ψi )η = true iff there is some ψi with 1 ≤ i ≤ n such that ψi η = true iff (μi η) ∈ R iff R = ∅.
5
Implementation and Prototype
In this section, we comment on some aspects of our implementation and show a system session with actual results of the test case generator. Our test case generator is bundled as a component of the Datalog deductive database system DES [15]. The input of the tool consists of: – A database schema D defined by means of SQL language, i.e., a finite set of tables T , constraints C and views V, as well as the integrity constraints for the columns (primary and foreign keys). – A SQL view V for which the test case is to be generated.
Applying Constraint Logic Programming to SQL Test Case Generation
201
DES [15] is implemented in Prolog and includes a SQL parser for queries and views, and a type inference system for SQL views. In this way we benefit from the DES facilities for dealing with SQL and at the same time we can exploit the constraint solving features available in current Prolog implementations. As a first step, we have chosen SICStus Prolog as a suitable platform for our development (although others will be handled in a near future). As explained in Section 4, we do need constraints that include a mix of conjunctions and disjunctions. We use reification to achieve an efficient implementation of these connectives. Thus, we reify every atomic constraint and transform conjunctions and disjunctions of constraints into finite domain constraints of the form B1 ∗ . . . ∗ Bk ≥ B0 , and B1 + . . . + Bk ≥ B0 , respectively. B0 allows a compact form to state the truth or falsity of these constraints. Apart from the constraints indicated in Section 4 we also need to consider domain integrity constraints, the constraints that restrict the given set of values a table attribute can take. These values are represented by a built-in datatype, e.g., string, integer, and float. On the one hand, types in SQL are declared in create table statements. In addition, further domain constraints can be declared, which can be seen as subtype declarations, as the column constraint A > 0, where A is a table attribute with numeric type. On the other hand, types are inferred for views. Up to now, we support integer and string datatypes by using the finite domain (F D) constraint system available in SICStus Prolog. Although with a few changes this can also be easily mapped to Ciao Prolog, GNU Prolog and SWIProlog. Posting our constraints over integers to the underlying F D constraint solver is straightforward. In the case of string constraints we map each different string constant in the SQL statements to a unique integer, allowing equality and disequality (F D) constraints. This mapping is stored in a dictionary before posting constraints for generating the test cases. Then, string constants are replaced by integer keys in the involved views. Generation and solving of constraints describing the test cases in the integer domain follows. Before displaying the instanced result involving only integers, the string constants are recovered back by looking for these integer keys in the dictionary. If some key values are not in the dictionary they must correspond to new strings. The tool generates new string constants for these values. Our treatment is only valid for equalities and disequalities, and it does not cover other common string operations such as the concatenation or the LIKE operator which will require a string constraint solver (see [8] for a discussion on solving string constraints involving the LIKE operator). Our tool allows the user to choose the type of test case to be generated, either PTC, or NTC or both PNTC for any view V previously defined in D. The output is a database instance d of a database schema D such that d is a test case for the given view V with as few entries as possible.
202
R. Caballero, Y. Garc´ıa-Ruiz, and F. S´ aenz-P´erez
For instance, consider the following session: DES-SQL> CREATE OR REPLACE TABLE t(a INT PRIMARY KEY, b INT); DES-SQL> CREATE OR REPLACE VIEW u(a1, a2) AS SELECT a, b FROM t WHERE a >= 10; DES-SQL> CREATE OR REPLACE VIEW v(a) AS SELECT a2 FROM u WHERE a2 = 88 GROUP BY a2 HAVING SUM(a1) > 0; Then, test cases (both positive and negative) for the view v can be obtained via the following command: DES-SQL> /test_case v Info: Test Case over integers: [t([[1000,88],[999,1000]])] Here, we get the PNTC [t([[1000,88],[999,1000]])]. If it is not possible to find a PNTC, the tool would try to generate a PTC and a NTC separately. Observe that in practice our system cannot reach completeness, but only weak completeness module the size of the tables of the instance. That is, our system will find a PTC if it is possible to construct it with all the tables containing a number of rows less than an arbitrary number. By default the system starts trying to define PTCs with the number of rows limited to 2. If it is not possible, the number of rows is increased. The process is repeated stopping either when a PTC is found or when an upper bound is reached (by default 10). Both the lower and the upper limits are user configurable.
6
Conclusions and Future Work
We have presented a novel technique for generating finite domain constraints whose solutions correspond to test cases for SQL relations. Similar ideas have been suggested in other works but, to the best of our knowledge, not for views, which corresponds to more realistic applications. We have formally defined the algorithm for producing the constraints, and have proved the soundness and weak completeness of the approach with respect to the operational semantics of Extended Relational Algebra. Another novelty of our approach is that we allow the use of string values in the query definitions. Although constraint systems over other domains, as reals or rationals, are available, we have not used them in our current work. However, they can be straightforwardly implemented. In addition, enumerated types (available in object-oriented SQL extensions) could also be included, following a similar approach to the one taken for strings. Our setting includes primary and foreign keys, existential subqueries, unions, intersections, and aggregate queries, and can be extended to cover other SQL features not included in this paper. For instance, null values can be considered by defining an extra null table Tnull containing the logic variables that are null, and taking into account this table when evaluating expressions. For instance, a / Tnull ) ∧ condition T.A = T .B will be translated into (T.A = T .B) ∧ (T.A ∈ (T .B ∈ / Tnull ).
Applying Constraint Logic Programming to SQL Test Case Generation
203
Dealing with recursive queries is more involved. One possibility could be translating the SQL views into a logic language like Prolog, and then use a technique for generating test cases for this language [11]. However, aggregate queries are not easily transformed into Prolog queries, and thus this approach will only be useful for non-aggregate queries. It is well-known that the problem of finding complete sets of test cases is in general undecidable [1]. Different coverage criteria have been defined (see [1] for a survey) in order to define test cases that are complete at least w.r.t. some desired property. In this work, we have considered a simple criterion for SQL queries, namely the predicate coverage criterium. However, it has been shown [19] that other coverage criteria can be reduced to predicate coverage by using suitable query transformations. For instance, if we look for a set of test cases covering every atomic condition in the where clause of a query Q, we could apply our tool to a set of queries, each one containing a where clause containing only one of the atomic conditions occurring in Q. A SICStus Prolog prototype implementing these ideas has been reported in this paper, which can be downloaded and tested (binaries provided for both Windows and Linux OSs) from http://gpd.sip.ucm.es/yolanda/research.htm. To allow performance comparisons and make the sources for different Prolog platforms available, an immediate work is the port to Ciao, GNU Prolog and SWI-Prolog. Although test case generation is a time consuming problem, the efficiency of our prototype is reasonable, finding in a few seconds TCs for views with dependence trees of about ten nodes and with a number of rows limited to seven for every table. The main efficiency problem comes from aggregate queries, where the combinatorial problem of selecting the aggregates can be too complex for the solver. To improve this point, even when efficiency of the SICStus constraint solver is acknowledged, there are more powerful solvers in the market. In particular, we plan to test the industrial, more efficient F D and R IBM ILOG solvers [13], which allow to handle bigger problems at a faster rate than SICStus solvers. Also, another striking state-of-the-art, free, and open-source F D solver library to be tested is Gecode [16].
References 1. Ammann, P., Offutt, J.: Introduction to Software Testing. Cambridge University Press, Cambridge (2008) 2. Binnig, C., Kossmann, D., Lo, E.: Towards automatic test database generation. IEEE Data Eng. Bull. 31(1), 28–35 (2008) 3. Cabal, M.J.S., Tuya, J.: Using an SQL coverage measurement for testing database applications. In: Taylor, R.N., Dwyer, M.B. (eds.) SIGSOFT FSE, pp. 253–262. ACM, New York (2004) 4. Codd, E.: Relational Completeness of Data Base Sublanguages. In: Rustin, R. (ed.) Data base Systems. Courant Computer Science Symposia Series 6, Prentice-Hall, Englewood Cliffs (1972)
204
R. Caballero, Y. Garc´ıa-Ruiz, and F. S´ aenz-P´erez
5. Dayal, U., Goodman, N., Katz, R.H.: An extended relational algebra with control over duplicate elimination. In: PODS 1982: Proceedings of the 1st ACM SIGACTSIGMOD symposium on Principles of database systems, pp. 117–123. ACM, New York (1982) 6. Degrave, F., Schrijvers, T., Vanhoof, W.: Automatic generation of test inputs for mercury, pp. 71–86 (2009) 7. DeMillo, R.A., Offutt, A.J.: Constraint-based automatic test data generation. IEEE Transactions on Software Engineering 17(9), 900–910 (1991) 8. Emmi, M., Majumdar, R., Sen, K.: Dynamic test input generation for database applications. In: ISSTA 2007: Proceedings of the 2007 international symposium on Software testing and analysis, pp. 151–162. ACM, New York (2007) 9. Garcia-Molina, H., Ullman, J.D., Widom, J.: Database Systems: The Complete Book. Prentice Hall PTR, Upper Saddle River (2008) 10. Gogolla, M.: A note on the translation of SQL to tuple calculus. SIGMOD Record 19(1), 18–22 (1990) 11. G´ omez-Zamalloa, M., Albert, E., Puebla, G.: On the generation of test data for prolog by partial evaluation. CoRR, abs/0903.2199 (2009) 12. Grefen, P.W.P.J., de By, R.A.: A multi-set extended relational algebra: a formal approach to a practical issue. In: 10th International Conference on Data Engineering, pp. 80–88. IEEE, Los Alamitos (1994) 13. ILOG CP 1.4, http://www.ilog.com/products/cp/ 14. Negri, M., Pelagatti, G., Sbattella, L.: Formal semantics of SQL queries. ACM Trans. Database Syst. 16(3), 513–534 (1991) 15. S´ aenz-P´erez, F.: Datalog educational system. user’s manual version 1.7.0. Technical report, Faculty of Computer Science, UCM (November 2009), http://des.sourceforge.net/ 16. Schulte, C., Lagerkvist, M.Z., Tack, G.: Gecode, http://www.gecode.org/ 17. SQL, ISO/IEC 9075:1992, third edn. (1992) 18. Su´ arez-Cabal, M., Tuya, J.: Structural coverage criteria for testing SQL queries. Journal of Universal Computer Science 15(3), 584–619 (2009) 19. Tuya, J., Su´ arez-Cabal, M.J., de la Riva, C.: Full predicate coverage for testing SQL database queries. Software Testing, Verification and Reliability (2009) (to be published) 20. Zhang, J., Xu, C., Cheung, S.C.: Automatic generation of database instances for white-box testing. In: COMPSAC, pp. 161–165. IEEE Computer Society, Los Alamitos (2001) 21. Zhu, H., Hall, P.A.V., May, J.H.R.: Software unit test coverage and adequacy. ACM Computing Surveys 29, 366–427 (1997)
A
Proof of Theorem 1
In this Appendix we include the proof of our main theoretical result. The theorem establishes a bijective mapping between the rows obtained by applying the ERA semantics to a relation R defined on an schema with instance d and the tuples (true, σ) in θ(R) (see Definition 6). Before proving the result we introduce an auxiliary Lemma: Lemma 1. Let D be a database schema and d a database instance, R1 , . . . , Rm relations verifying Theorem 1, B1 , . . . , Bm attribute names, and R an expression
Applying Constraint Logic Programming to SQL Test Case Generation
205
in ERA defined as R = ρB1 (R1 ) × . . . × ρBm (Rm ). Let P be a multiset defined as P = {|(ψ, μ) | (ψ1 , ν1 ), . . . , (ψm , νm ) ∈ (θ(R1 ) × . . . × θ(Rm )) ψ = ψ1 ∧ . . . ∧ ψm , μ = ν1 B1 · · · νm Bm |} Then μ ∈ R with cardinality k iff (true, μ) ∈ P with cardinality k. Proof. (true, μ) ∈ P with cardinality k iff there are pairs (ψi , νi ) ∈ θ(Ri ) with cardinality ci for i = 1 . . . m such that k = c1 ×. . .×cm and μ = ν1 B1 · · ·νm Bm . From the conditions of P we have that ψ = true iff ψi = true for i = 1 . . . m. By hypothesis (true, νi ) ∈ θ(Ri ) with cardinality ci iff νi ∈ Ri with cardinality ci for i = 1 . . . m, iff (ν1 B1 · · · νm Bm ) ∈ (ρB1 (R1 ) × . . . × ρBm (Rm )) with cardinality c1 × . . . × cm , i.e., μ ∈ R with cardinality k. Next we prove the Theorem by induction on the number of nodes of the dependence tree for R. If this number is 1 (basis) then R is a table T , T = d(T ), and the result is an easy consequence of Definition 6 item 1. If the dependence tree contains at least two nodes (inductive case) R cannot be a table. We distinguish cases depending on the form of R: - R aggregate query. Then Q is of the form Q = select e1 E1 , . . . , en En from R1 B1 , . . . , Rm Bm where Cw group by A1 , . . . , Ak having Ch Then Q = Πe1 →E1 ,...,en →En (σCh (γL (σCw (R)))), with R = ρB1 (R1 ) × . . . × → U1 , . . . , u l → Ul }, ui the aggregate expressions ρBm (Rm ), L = {A1 , . . . , Ak , u1 occurring either in the select or in the having clauses for i = 1 . . . l, Ui new attribute names for i = 1 . . . l, ej the result of replacing each occurrence of ui in ej , 1 ≤ j ≤ n by Ui and analogously for Ch . From Definition 6, item 5 we have θ(Q) = {|( (Π1 (A)) ∧ aggregate(Q, A), sQ (Π2 (A))) | A ⊆ P |} Let μ ∈ Q with cardinality k. Then there are rows ν1 , . . . , νr such that μ is of the form μ = (νi ){E1 → e1 , . . . , En → en }, 1 ≤ i ≤ r with νi ∈ (σCh (γL (σCw (R))), with cardinality ci for i = 1 . . . r and k = c1 + . . . + cr . From the definition of γ we have that the ci occurrences of νi for i = 1 . . . r correspond to the existence of ci maximal aggregates Sij ⊆ σCw (R), j = 1 . . . ci . Observe for every η ∈ Sij we have that the cardinality of η in Sij and in R is the same because Sij is maximal. Then from Lemma 1 we have that the set Aji = {|(true, η) | η ∈ Sij |} verifies Aji ⊆ P for i = 1 . . . r, j = 1 . . . ci . Then it is immediate that (Π1 (Aji )) = true and that sQ (Π2 (Aji )) = sQ (Si ) = {E1 → j j → ((en )Si )} = (νi ){E1 → e1 , . . . , En → en } = μ. Then we ((e1 )Si ), . . . , En have that (true ∧ aggregate(Q, Aji ), μ) ∈ θ(Q) for i = 1 . . . r, j = 1 . . . ci . It remains to check that aggregate(Q, Aji ) = true, i.e., that – group(Q, Π2 (Aji )) = true. Π2 (Aji ) = Sij and the definition of group requires that all the rows in Sij verify the where condition and that every row takes
206
R. Caballero, Y. Garc´ıa-Ruiz, and F. S´ aenz-P´erez
the same values for the grouping attributes. The first requirement is a consequence of Sij ⊆ σCw (R), while the second one holds because we are assuming that the multiset Sij was selected has a valid group by the operator γ. – maximal(Q, Aji ) = true. The auxiliary definition maximal indicates that no other element of the form (true, μ ) from P can be included in Sij verifying that we still have the same values for the grouping attributes and μ verifying the where condition. This is true because if there were such (true, μ ) ∈ P − Aji , then by Lemma 1 μ will be in R and Sij will not be maximal in σCw (R) as required by γ. – ϕ(Ch , Π2 (Aji )) = true. Observe that ϕ(Ch , Π2 (Aji )) = ϕ(Ch , Sij ), and that in the absence of subqueries ϕ only checks that the Sij verify the having condition Ch , which is true because νi verifies Ch . Then we have (true, μ) ∈ θ(Q) for i = 1 . . . r, j = 1 . . . ci and thus (true, μ) ∈ θ(Q) with cardinality k . The converse result, i.e., assuming (true, μ) ∈ θ(Q) with cardinality k and proving that then μ ∈∈ Q with cardinality k, is analogous. - R basic query. Similar to the previous case. - R = V1 union V2 . Then R = V1 ∪ V2 , θ(R) = θ(V1 ) ∪ θ(V2 ) and the result follows by induction hypothesis since V1 , V2 are children of R in its dependence tree. - R = V1 intersection V2 . Then
R = V1 ∩ V2 θ(R) = {|(ψ1 ∧ ψ2 ∧ ν1 = ν2 , ν1 ) | (ψ1 , ν1 ) ∈ θ(V1 ), (ψ2 , ν2 ) ∈ θ(V2 )|} Then μ ∈ R with cardinality k iff μ ∈ V1 and μ ∈ V2 with cardinalities k1 , k2 respectively and k = min(k1 , k2 ). By the induction hypothesis (true, μ) ∈ θ(V1 ) with cardinality k1 , (true, μ) ∈ θ(V2 ) with cardinality k2 and this happens iff (true, μ) ∈ θ(R). - R is a view V with associated query Q. Then V = ΠE1 →V.A1 ,...,En →V.An Q and θ(V ) = θ(Q){V.A1 → E1 , . . . , V.An → En } with E1 , . . . , En the attribute names of the select clause in Q. We have proved above that μ ∈ Q iff (true, μ) ∈ θ(Q) with the same cardinality. Now observe that for every μ ∈ Q applying the projection ΠE1 →A1 ,...,En →An produces a renaming of its domain E1 , . . . , En to A1 , . . . An , and that this is the same as μ{V.A1 → E1 , . . . , V.An → En }.
Internal Normalization, Compilation and Decompilation for System F βη
Stefano Berardi1 and Makoto Tatsuta2 1 2
C.S. Dept., University of Turin, Italy National Institute of Informatics, Japan
Abstract. This paper defines a family of terms of System F which is a decompiler-normalizer for an image of System F by some injective interpretation in System F. We clarify the relationship among these terms, normalization by evaluation, and beta-eta-complete models of F.
1
Introduction
Let Fβη denote the second order λ-calculus with βη-equality. For a definition of Fβη and of a model of Fβη we refer to [11] and to Section 4. We write j : Fβη → Fβη for an embedding of Fβη into some λ-calculus Fβη such that j is compatible an “extension” of Fβη . with typing, substitution and βη-reductions. We call Fβη For instance, we may have j = the identity map, and Fβη = Fβη , Fω , CC, all with βη-equality. j(Fβη ) ⊆ Fβη is an image of Fβη inside Fβη . This paper is about three interrelated problems, namely: 1. Normalization by evaluation. To find a normalization algorithm for Fβη , writ ten in some extension Fβη of Fβη . 2. Compilation and Decompilation. To find compilation and decompilation al of Fβη , for the image j(Fβη ) ⊆ Fβη gorithms, written in some extension Fβη of Fβη . 3. βη-completeness. To find a class of βη-complete models for Fβη . Abel ([1]) defined a normalizer for Fβη inside ML. Pfenning and Lee [14] defined a compiler and a decompiler for Fβη inside an extension F3 of Fβη . The first non-trivial example of βη-complete model of Fβη is the BB-model ([4]), proved βη-complete in [5] and generalized in [6]. Problems 1, 2, 3 are not related a priori, but they may be solved together. Problem 1 requires to find a normalizer (see , indexed over the types A of Section 5), i.e., some family evA of terms in Fβη Fβη , and computing the code of the normal form, given the code of a term of type A. Problem 2 requires to find two families fA and gA of terms of Fβη , indexed over the types A of Fβη . The terms fA represent a decompiler (see Sections 5 and 6), and compute the “source code” u of a term t of type A in Fβη , from its “executable version” j(t) in j(Fβη ) ⊆ Fβη . In the case fA computes the “source code” of the normal form of t, we say that fA is a decompilernormalizer. The action of fA is also called a “reification”: it makes a program, an abstract concept, into a concrete datum available to manipulation. The terms gA M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 207–223, 2010. c Springer-Verlag Berlin Heidelberg 2010
208
S. Berardi and M. Tatsuta
represent a compiler (see Sections 5 and 6), and compute an “executable term” j(t) of j(Fβη ) ⊆ Fβη from the “source code” u of the term t. The action of gA is also called a “reflection”: it is a process by which a program may define a new program. Problem 3, instead, requires to find a class of models of Fβη , models defined as mathematical structures whose equational theory is exactly βη. All these problems have independent reasons for interest. 1. Problem 1, normalization for the image j(Fβη ) of Fβη inside Fβη , has a potential interest from a programming viewpoint, because if the language can evaluate a subset j(Fβη ) ⊆ Fβη of itself, it may also design extensions Fβη of this subset. An example taken from real programming is the language Scheme with its primitive eval. 2. Problem 2, compilation and decompilation of the image j(Fβη ) ⊆ Fβη of Fβη inside Fβη , has also a potential interest from a programming viewpoint, be cause if a language Fβη can decompile a subset j(Fβη ) of itself, then Fβη may manipulate the source code of its programs, in order to optimize them. An example of a language having this feature in the real world is again Scheme, in which there are primitives quote, unquote, for “freezing” and “unfreezing” the execution of any expression of Scheme itself, and for manipulating the syntactical tree of a Scheme expression. 3. The interest of Problem 3, completeness, lies in the fact that a βη-complete model of Fβη describes the equality =βη of the calculus Fβη in the language of mathematical structures, and explains the mathematical principles which are hidden behind the syntax of Fβη .
After Pfenning and Lee’s result, a natural additional request for the problems 1 and 2 is that Fβη should be as close to Fβη as possible. In this paper we address the following version of the problems 1 and 2: whether we may define = Fβη , that is, if we a compiler, a decompiler or a normalizer if we require Fβη require having an embedding j : Fβη → Fβη from Fβη to itself, or, in other words, having an image j(Fβη ) ⊆ Fβη of Fβη inside Fβη itself. In this case the compiler, decompiler, or normalizer for j(Fβη ) is defined within Fβη itself. We call them internal when a compiler, decompiler, or normalizer for j(Fβη ) ⊆ Fβη is written in Fβη . In this paper, we first prove that there is no normalizer for the source code of Fβη written in Fβη itself. Then we consider the existence of internal compilers and decompilers in the case j = id. We prove that there is no compiler nor decompiler for Fβη written in Fβη itself. However, we can define an embedding j = (.)∗ : Fβη → Fβη of Fβη into itself, and a decompiler-normalizer for the ∗ ⊆ Fβη of Fβη , written inside Fβη . This positive answer terms of the image Fβη is surprising, because for real-world languages decompilation is a hard problem, and because we just showed that no internal normalizer exists. Besides, a similar result does not hold for compilation: we prove that for j(.) = (.)∗ (and indeed for any “reasonable” choice of j : Fβη → Fβη ) we cannot define a compiler in Fβη for the terms of j(Fβη ). The best result for compilation is still Pfenning and Lee’s compilation of Fβη inside F3 . We interpret these results as follows. Normalization and compilation for j(Fβη ) require reduction rules essentially stronger than those
Internal Normalization, Compilation and Decompilation for System Fβη
209
∗ available in Fβη , like those in F3 . Decompiling Fβη , instead, means deducing the structure of the normal forms from their observable behavior, and this can be done inside Fβη . Summing up, Fβη may manipulate its own programs, at some extend, even if the last step, compiling, must be done in F3 . Instead Problem 3 requires to define a class of βη-complete models for Fβη , a problem which is a priori unrelated with problems 1 and 2. However, we claim that within any model M of Fβη , we may use the map (.)∗ in order to define some βη-complete sub-model M∗ of M. We did not yet succeed in proving this claim, but at the end of this paper we prove a promising result, and we think we are close to this goal. In Section 2 we sum up what is known about the corresponding of the problems 1, 2, 3 for simply typed lambda calculus λ→. In the rest of the paper we try to adapt the solutions we have in the case of λ→ to Fβη . In Section 3 we introduce the definition of Fβη and a type Tmc of Fβη , coding all untyped λ-terms in Fβη . In Section 4 we define models of Fβη and βη-completeness. In Section 5 we prove that there is no normalizer, compiler, nor decompiler for Fβη in Fβη . In Section 6 we define an interpretation (.)∗ of Fβη in Fβη , and in Section 7 we prove there ∗ . is a decompiler-normalizer (but no compiler) written in Fβη for the terms of Fβη In Section 8 we discuss how to define a class of βη-complete models of Fβη using (.)∗ , and we prove a partial result towards this goal. The proofs we omit may be found in the full version [2] of the paper.
2
βη-Completeness, Internal Decompilation and Normalization for Simply Typed λ-Calculus
In this section we sum up what is known about the corresponding of the problems 1, 2, 3 for simply typed lambda calculus λ→. In the rest of the paper, we try to adapt the solutions we have for λ→ to Fβη . We refer to [7] for a definition of λ→. Friedman considered the problem of defining βη-complete models for λ→. He considered all set-theoretical models of λ→. In these models all atomic types o of λ→ are interpreted by some set, and the type A → B is interpreted as the set of all maps from the interpretation of A to the interpretation of B. Friedman has proved the following theorem ([7]): “a set-theoretical model of λ→ is βη-complete if and only if all atomic types of λ→ are interpreted by infinite sets.” The proof of Friedman is highly abstract, using Classical Logic, uncountable sets and Choice Axiom. However, if we unwind Friedman’s proof, we discover the following elementary and constructive argument, hidden in it. First, in any infinite set-theoretical model there is a decompiler-normalizer for λ→. Indeed, there is some atomic type Tm representing λ-terms (see Section 3). There is some family fA : A → Tm of maps of the model, for each simple type A, such that if t : A is a term of λ→, then fA (t) = the code in Tm of the normal form t of t. To be more accurate, fA (t) returns the code of the untyped λ-term, which is the erasure of the βη-long normal of the term t (as defined in Section 3). However, this amounts to the same, because the typing information required for a normal term of λ→ may be recovered
210
S. Berardi and M. Tatsuta
from the erasure of the term. The argument in Friedman’s proof continues as follows: if t, u are definable in λ→ and t = u in the set-theoretical model, then fA (t) = fA (u) : Tm in the same model, therefore t, u have the same (βη-long) normal form, and hence t =βη u. Surprisingly, fA turns out to be definable in λ→. The definition of fA requires two free variables ap : Tm → Tm → Tm and lam : (Tm → Tm) → Tm to represent application and lambda abstraction. Thiery Joly ([8]) explicitly defined this decompiler-normalizer fA : A → Tm for λ→, though he only studied it as an example of a family of injections definable in λ→, from all types to a single type. Berger ([3]) and Werner ([9]) studied this internal decompiler-normalizer for λ→, and then defined a compiler and a normalizer for λ→ in G¨ odel’s system T , an extension of λ→. These results are solutions for the problems 1, 2, 3 for λ→ which are probably optimal: there are a family of βη-complete models, an internal decompiler-normalizer for λ→, and compilers and normalizers written in some extensions T of λ→. We will try to adapt the definition of a decompilernormalizer from λ→ to Fβη . The problem is that the definition of a decompilernormalizer in λ→ heavily relies on two properties of λ→, the fact that we may recover a normal form from its erasure, and the existence of a set-theoretical model, which do not hold for Fβη .
3
The System Fβη and the Type Tmc of Untyped λ-Terms
In this section we introduce Fβη , and a type Tmc in which we may represent an untyped λ-terms t by some (βη-long) normal form [t] ∈ Fβη . Representation is up to α-rule. If t, u are two untyped λ-terms which are not α-convertible, then t, u have two representations in Fβη by some (βη-long) normal forms which are not α-convertible, and therefore not βη-convertible. By Λ we denote the untyped λ-calculus. We assume variables x, y, a, b, . . .. The syntax of Λ is t ::= x|tt|λx.t. We use “typewriter” letters t, u, v, . . . to denote untyped λ-terms. By Fβη we denote the second order λ-calculus, as defined in [11]. We assume type variables Tm, Var, dB, α, β, . . .. The syntax of Fβη is A ::= α|A → A|∀αA for types. We write both A → B → C and A, B → C for A → (B → C). We write ∀α.A → B for ∀α.(A → B). A context is a set Γ = {x1 : A1 , . . . , xm : Am } where xi = xj for i = j. We write x, y : A for x : A, y : A. We assume variables ap, lam, 0, S, Var, x, y, z, a1 , a2 , f1 , f2 , g1 , g2 , . . .. The syntax for pseudoterms is t ::= x|λx : A.t|tt|λα.t|tA. We write t(t1 ) and t(t1 , t2 ) for tt1 and (tt1 )t2 respectively. We define λ : A.t as λx : A.t for some fresh variable x. FV(t) will denote the set of the free variables in t. There are introduction and elimination rules for → and ∀, assigning types to some pseudo-terms. We write Γ t : A for “t has type A in Fβη and FV(t) ⊆ Γ ”. The degree deg(A) of A is defined by deg(α) = 0, deg(A → B) = max(deg(A) + 1, deg(B)) and deg(∀α.A) = deg(A) + 1. We write =α for the α-convertibility relation: equality up to variable renaming. The reduction rules of Fβη are β and η reductions. We write =βη for the convertibility relation up to β and η rules. By “βη-long normal
Internal Normalization, Compilation and Decompilation for System Fβη
211
form” of a term we mean the longest β-normal form which is βη-convertible to the term. The βη-long normal form exists by Girard’s Normalization Theorem of Fβη , and it is unique by the Church-Rosser confluence property of Fβη . For every type A of Fβη , we write idA for the identity λx : A.x on the type A, Id for the type ∀α.α → α, and id for the polymorphic identity λα.λx : α.x : Id. tn u is defined as (t(. . . (tu) . . .)) (n times of t) for a natural number n. For every term of Fβη , we write |t| ∈ Λ for the untyped λ-term obtained by stripping all type information from t, and replacing the variable xi of index i of Fβη with itself (to be accurate: with the variable xi of index i of Λ). We recursively define |xi | = xi , |λx : A.t| = λx.|t|, |tu| = |t||u|, |λα.t| = |t|, |tA| = |t|. By |βη| we denote the equational theory on Fβη such that, for all terms t, u of Fβη , by t =|βη| u if and only if |t| =βη |u| in Λ. We have βη ⊂ |βη|: the equational theory |βη| for Fβη is larger than the equational theory βη, because βη-long normal form of Fβη cannot be uniquely recovered from its erasure. An example is given as follows. Let Void = ∀α.α and t = λx : Void.x and u = λx : Void.x(Void). Then t, u : Void → Void and t =βη u (t, u are different βη-long normal forms), while t =|βη| u, because |t| = λx.x = |u|. We want to represent all untyped λ-terms by the elements of some type Tmc of Fβη . We first explain how to internalize the notion of boolean, natural numbers and trees, and, in general, data types in Fβη . Definition 1. (Data Types) We introduce boolean, natural numbers, binary trees and data types in Fβη , as follows. 1. A data type is any closed type of the form ∀α1 , . . . , αn .A, with no connective ∀ in A, and deg(A) ≤ 2. 2. Bool = ∀α.α → α → α, and True = λα.λx, y : α.x, False = λα.λx, y : α.y. 3. Nat = ∀α.(α → α) → (α → α) and n = λα.λf : (α → α).λa : α.f n (a) for any natural number n ∈ N . Sl , Sr : Nat → Nat are defined in the context {x : Nat, α, f : α → α, a : α} by: Sl (x, α, f, a) = x(α, f, f (a)) and Sr (x, α, f, a) = f (x(α, f, a)). 4. Tree = ∀α.α, (α → α → α) → α and: (a) In the context x : α, y : α → α → α we set leaf(α, x, y) = x : α (b) In the context a, b : Tree, x : α, y : (α → α → α) we set mktree(a, b, α, x, y) = y(a(α, x, y), b(α, x, y)) : α. Bool, Nat are data types of Fβη , whose βη-long normal forms are True, False, 0, 1, 2 . . ., in bijection with boolean, natural numbers. Sl , Sr are called the leftand right-successor. By definition we have Sl (n) =βη 1 + n =βη n + 1 =βη Sr (n) for all n ∈ N . We internalize the notion of untyped λ-term inside Fβη , first by an open type, then by a closed type, using a technique called Higher-order abstract syntax [13], in which binders in the object-language are represented via binders in the meta-language. Fix a type variable Tm. We fix a context ΓTm = {lam : ((Tm → Tm) → Tm), ap : (Tm → Tm → Tm)}. Then the elements of Tm represent
212
S. Berardi and M. Tatsuta
the syntax trees of untyped λ-terms in Fβη . The variables lam and ap codify λabstraction and application: if f : Tm → Tm then lam(λx : Tm.f (x)) : Tm codifies the λ-abstraction of f , and if x, y : Tm then ap(x, y) : Tm codifies the application of x to y. Codes in Tm are βη-long normal forms: we do not have reduction rules for them. If x, y : Tm are variables, then the term ap(lam(λx : Tm.x), y) : Tm of Fβη codifies the untyped λ-term (λx.x)(y) ∈ Λ, but (λx.x)(y) =βη y, while ap(lam(λx.x), y) =βη y, because the terms ap(lam(λx.x), y) : Tm and y : Tm of Fβη are different βη-long normal forms. The free variables Tm, lam, ap of ΓTm define a generic coding for Λ in Fβη . In Definition 8 we introduce a triple DB, Lam, Ap replacing these variables with an example of concrete coding, de Bruijn coding. We formally define the interpretations [.] of Λ and [[.]] of Fβη into the open type Tm and in the context ΓTm . The map [.] applied to any closed t ∈ Λ interprets abstractions and applications of t by lam and ap. The map [[.]] applied to a term of Fβη forgets abstractions and applications over types, and translates abstractions and applications over terms by lam and ap. Definition 2. (Interpreting Λ in Fβη ) Assume t ∈ Λ be an untyped λ-term with FV(t) = {x1 , . . . , xm }. Let Γ = {x1 : Am , . . . , xm : Am } and u be a term of Fβη such that Γ u : A. Let σ be the variable renaming a1 /x1 , . . . , am /xm , with pairwise distinct variables a1 , . . . , am . We recursively define [t]σ : Tm and [[u]]σ : Tm in the context ΓTm ∪ {a1 : Tm, . . . , am : Tm} by: 1. (a) (variable) [xi ]σ = ai . (b) (abstraction) [λx.t]σ = lam(λa : Tm.[t]σ,a/x ) with a fresh variable a. (c) (application) [t1 t2 ]σ = ap([t1 ]σ , [t1 ]σ ). When m = 0, we abbreviate [t]σ with [t]. 2. [[u]]σ = [|u|]σ and [[u]] = [|u|]. We interpret a term of Fβη by first stripping off its type information. We may avoid the use of free variables lam, ap and define a closed type Tmc = ∀Tm.(Tm → Tm → Tm) → ((Tm → Tm) → Tm) → Tm of Fβη representing all untyped λ-terms. Define λΓTm as λTm.λlam : (Tm → Tm) → Tm.λap : (Tm, Tm → Tm). c
The closed interpretation of t ∈ Λ in Tmc is [t] = λΓTm .[t] : Tmc , and the closed interpretation of u ∈ Fβη in Tmc is [[u]]c = λΓTm .[[u]] : Tmc . Closed terms of type Tmc are, up to βη-rule, exactly the closures of the terms of type Tm in the context ΓTm . Tmc is not a data type because Tmc = ∀α.A for some A such that deg(A) = 3. We might have defined a closed term apc : c c c Tmc → Tmc → Tmc such that apc ([[a]] , [[b]] ) =βη [[ab]] , by setting apc (x, y) = λΓTm .ap(x(Tm, lam, ap), y(Tm, lam, ap)) in the context {x : Tmc , y : Tmc }. There is no way of defining a corresponding lamc : ((Tmc → Tmc ) → Tmc ) of lam, though. We prove that Tm is a “faithful” representation of Λ inside Fβη . By this we mean: all closed terms of Tm are βη-convertible to the interpretation of some closed untyped λ-term, and these interpretations are βη-equal in Fβη if and only if the two original untyped λ-terms are α-convertible.
Internal Normalization, Compilation and Decompilation for System Fβη
213
Lemma 1. (Faithfulness of the Interpretation of Λ in Tm) 1. If ΓTm c : Tm, then c =βη [u] for a some closed u ∈ Λ. 2. If u1 , u2 ∈ Λ, then u1 =α u2 if and only if [u1 ] =βη [u2 ]. c
c
The same properties hold if we replace [.] by [[.]] and we consider closed c : Tmc .
4
Models of Fβη and βη-Completeness
In this section we introduce the notion of βη-completeness for types and models of Fβη . The definition of models is taken from [11]. Definition 3. (Fβη -Models ) A frame for Fβη is a tuple M = T, |.|, P, ⇒, Q : T is a set of elements called “the types” of M. For all θ ∈ T, |θ| is a set of elements called “the terms of type θ” of M. P ⊆ (T → T) is a set of maps over types of M called “the predicates” of M. ⇒ : T → T → T is a map on types of M called “arrow”, and for all θ, ρ ∈ T we have a canonical injection from |⇒(θ, ρ)| to (|θ| → |ρ|) 5. Q : P → T is a map from predicates of M to types of M called “quantifier”, and for all φ ∈ P we have a canonical injection from |Q(φ)| to Πθ∈T |φ(θ)|.
1. 2. 3. 4.
From now on, we identify each element of |⇒(θ, ρ)| with some element of |ρ| → |θ| and each element of Q(φ) with some element of Πθ∈T |φ(θ)|. A model of Fβη is a frame for Fβη equipped with two interpretation maps [[A]]σ and [[t]]σ,τ as follows. If Γ = {α1 , . . . , αn } is a set of type variables of Fβη , and σ : Γ → T, and A is a type of Fβη with FV(A) ⊆ Γ , then [[A]]σ ∈ T and: 1. [[αi ]]σ = σ(αi ) for i = 1, . . . , n. 2. [[A → B]]σ = ⇒([[A]]σ , [[B]]σ ). 3. [[∀α.A]]σ = Q(φ) for some φ ∈ P such that φ(θ) = [[A]]σ,θ/α for all θ ∈ T. If Δ = {x1 : A1 , . . . , xm : Am } is a set of term variables of Fβη , and τ (xi ) ∈ |[[Ai ]]σ | for j = 1, . . . , m, FV(A) ⊆ Γ , and Δ t : A in Fβη , then [[t]]σ,τ ∈ |[[A]]σ |, and: 1. 2. 3. 4. 5.
[[xj ]]σ,τ = τ (xj ) for j = 1, . . . , m. [[t(u)]]σ,τ = [[t]]σ,τ ([[u]]σ,τ ). [[λx : A.t]]σ,τ (a) = [[t]]σ,(τ,a/x) for all a ∈ [[A]]σ . [[t(A)]]σ,τ = [[t]]σ,τ ([[A]]σ ). [[λα.t]]σ,τ (θ) = [[t]](σ,θ/α),τ for all θ ∈ T.
If A is a closed type and t, u : A are closed terms of Fβη , and M is a model, we write t =M u if [[t]]M = [[u]]M . The equivalence relation =M defines an equational theory for Fβη including βη. A model M of Fβη is inconsistent if any two elements of the same type of M are equal. A model is consistent if it is not inconsistent. Among the consistent models of Fβη we quote: the term model, consisting of all open types and terms of Fβη , the observational model, consisting of all closed types and terms of Fβη modulo the largest consistent equality [12], and Longo-Moggi PER models [10]. We may now formally define the notion of βη-completeness.
214
S. Berardi and M. Tatsuta
Definition 4. (βη-completeness for types and models of Fβη ) Assume A, B are closed types of Fβη . 1. eqA is an internal equality for A if eqA : A → A → Bool is a closed term of Fβη and for all closed terms t, u : A of Fβη we have t =βη u if and only if eqA (t, u) =βη True. 2. f : A → B is an internal injection if f is a closed term of Fβη and for all closed terms t, u : A of Fβη : if f (t) =βη f (u) then t =βη u. 3. A closed type A of Fβη is βη-complete in a model M for Fβη if for all closed terms t, u : A of Fβη : t =M u if and only if t =βη u. 4. A closed type A of Fβη is βη-complete if A is βη-complete in all consistent models of Fβη . 5. A model M is βη-complete if all closed types of Fβη are βη-complete in M. All data types (like Bool, Nat, Tree, . . .) of Fβη are βη-complete (that is, βηcomplete in all consistent models of Fβη ). Lemma 2. Assume A, B are closed types of Fβη and D is some data type of Fβη . Let M be any consistent model of Fβη . 1. 2. 3. 4. 5. 6.
D has some internal equality eqD in Fβη . True =M False. (Statman’s Lemma) Bool is βη-complete. If A has some internal equality eqA , then A is βη-complete. Any data type D of Fβη is βη-complete. If B is βη-complete and f : A → B is an internal injection, then A is βη-complete.
There are consistent models which are βη-complete (with respect to all types, and not only with respect to data types). The term model is trivially βηcomplete, and there are also non-trivial examples ([6]). The observational model and most PER-models are consistent but not βη-complete: in these models, the type Nat → Nat is not βη-complete. Lemma 3. Assume O is the observational model of Fβη and Sl , Sr : Nat → Nat are the left- and right-successor (Definition 1). =|βη| Sr and Sl =O Sr . 1. Sl 2. O is not βη-complete for the type Nat → Nat.
5
There Is No Normalizer, Compiler, Nor Decompiler for Fβη Inside Fβη
In this section, by adapting Turing’s diagonalization argument, we prove that there is no normalizer nor compiler for all terms in Fβη written in Fβη itself. By defining an internal equality for Tmc we also prove that there is no decompiler for all terms in Fβη written in Fβη itself. We first formally define normalizers, compilers, and decompilers for Fβη inside Fβη .
Internal Normalization, Compilation and Decompilation for System Fβη
215
Definition 5. 1. A normalizer for Fβη in Fβη is a family of closed terms evA : Tmc → Tmc of Fβη , for each closed type A of Fβη , such that for all closed c terms t : A of Fβη , with its βη-long normal form t : A, we have evA ([[t]] ) =βη c [[t ]] . 2. A compiler for Fβη in Fβη is a family gA : Tmc → A of closed terms of Fβη , for each closed type A of Fβη , such that for all closed terms t : A of Fβη we c have gA ([[t]] ) =βη t. 3. A decompiler for Fβη in Fβη is a family fA : A → Tmc of closed terms of Fβη , for each closed type A of Fβη , such that for all closed terms t : A of c Fβη we have fA (t) =βη [[t ]] for some t =βη t. Lemma 4. (Normalizer and Compiler for Fβη in Fβη ) 1. There is no normalizer for Fβη in Fβη . 2. There is no compiler for Fβη in Fβη . Proof. In both cases we use some kind of diagonalization argument. 1. Assume evA is a normalizer of Fβη in Fβη . Set A = Tmc . We use the following Claim: there is some closed term K : Tmc → Tmc of Fβη such that c c c K([[t]] ) =βη [[[[t]] ]] for all closed terms t of Fβη . For a proof of the Claim we refer to [2]. Using the Claim we define a closed term f : Tmc → Tmc of Fβη by f (x) = evA (apc (x, K(x))) : Tmc , where x : Tmc . Assume c g : Tmc is the βη-long normal form of f ([[f ]] ). Then by definition we have: c c c g =βη f ([[f ]] ) =βη evA (apc ([[f ]] , K([[f ]] ))) =βη evA (apc ([[f ]]c , [[[[f ]]c ]]c )) =βη c c c c evA ([[f ([[f ]] )]] ) =βη [[g]] . The terms g, [[g]] are βη-long normal, therefore c g =α [[g]] . By induction over a βη-long normal t we can prove that if ΓTm , x1 : Tm, . . . , xn : Tm t : Tm and t is not a variable, then t is a shorter term than [[t]]. Therefore all closed terms t : Tmc are shorter than c [[t]] , which is a contradiction. 2. Assume gA : Tmc → A is a compiler for Fβη , written inside Fβη . Let A = (Tm → Nat) and Sr be the right-successor (Definition 1). Define a closed term h : A of Fβη by h(x) = Sr (gA (x)(x)) : Nat, where x : Tmc . Then h : A. c c c c By definition we have h([[h]] ) =βη Sr (gA ([[h]] )([[h]] )) =βη Sr (h([[h]] )). The c βη-long normal form of h([[h]] ) is n in Nat for some n ∈ N , while the βη-long normal form of Sr (h([[h]]c )) is n + 1, which is a contradiction. In order to prove that there is no decompiler for Fβη in Fβη , we define an internal equality eqTmc , deciding βη-equality for Tmc (hence α-equality for the coding of untyped λ-terms). We cannot use the internal equality for a data type, because Tmc is not a data type. As a preliminary step, we translate the elements of Tmc into some suitable data type dBc by some internal injection of Fβη , and then we use the internal equality of dBc . dBc internalizes de Bruijn coding of untyped λ-terms in a type dB of Fβη . In dBc we explicitly represent variable names with a type Var isomorphic to Nat, something we do not have in Tm. We fix two type variables Var, dB and a context ΓdB with the variables of de Bruijn coding: 0 : Var, S : Var → Var, var : Var → dB, ap : dB → dB → dB, lam : Var → dB → dB.
216
S. Berardi and M. Tatsuta
Definition 6. We write λΓdB as an abbreviation for the λ-abstractions: λVar.λdB.λ0 : Var.λS : Var → Var.λvar : (Var → dB).λap : (dB → dB → dB).λlam : (Var → dB → dB) and x(ΓdB ) for x(Var, dB, 0, S, var, ap, lam). Then we set: 1. dBc = ∀Var.∀dB.Var → (Var → Var) → (Var → dB) → (dB → dB → dB) → (Var → dB → dB) → dB. 2. We define the closed terms of Fβη representing the constructors of dBc : (a) varc : Nat → dBc by varc (x) = λΓdB .var(x(V ar, 0, S)). (b) apc : dBc → dBc → dBc by apc (x, y) = λΓdB .ap(x(ΓdB ), y(ΓdB )). (c) lamc : (Nat → dBc → dBc ) by lamc (n, y) = λΓdB .lam(n(V ar, 0, S), y(ΓdB )). If we take A such that dBc = ∀Var.∀dB.A, there is no ∀ in A and deg(A) = 2, and therefore dBc is a data type. The code of a untyped λ-term in dB (or in dBc ) is uniquely given for an untyped context (a list of untyped variables) for the λ-term. Assume that u is an untyped λ-term with a context a0 , . . . , an−1 , and lam(a, t) is a subterm of u, within nested m abstractions of u. Then we require that a = an+m , that is, that the variable bound by lam is the variable number n + m. Definition 7. Let n ∈ N , and v(n) = S n (0) in the context ΓdB . The coding tn of a term t ∈ Λ in ΓdB is recursively defined as follows: 1. xi n = var(v(i)). 2. λa.tn = lam(var(v(n)), t[xn /a]n+1 ). 3. tun = ap(tn , un ). c
c
We define tn = λΓdB .tn : dBc . If t is closed then t0 is closed. Assume A, B are closed types of Fβη . We prove that de’ Bruijn coding of t ∈ Λ is an injection up to α-conversion, and that there is an internal injection from Tmc to dBc , sending a representation [t] of any closed t ∈ Λ to a representation tc0 of the same t in dBc . An untyped λ-term t is in fact represented by a family of de Bruijn codes, depending on a parameter n ∈ Nat, and representing the code of t in the context a0 , . . . , an−1 . Implicitly, the type of the de Bruijn coding of t should be Nat → dBc . We define a canonical substitution over the context ΓTm , replacing a generic coding of Λ by a concrete coding, de Bruijn coding. Definition 8. (The de Bruijn Substitution) We set DB = Nat → dBc . We define Lam : (DB → DB) → DB and Ap : DB → DB → DB with arguments a, b : DB, n : Nat, f : DB → DB by: 1. Lam(f )(n) = lamc (n, f (λ : Nat.varc (n))(n + 1)) : dBc . 2. Ap(a, b)(n) = apc (a(n), b(n)) : dBc . We call δ = [DB/Tm, Lam/lam, Ap/ap] the de Bruijn substitution. We prove that the de Bruijn substitution δ sends the coding of untyped λ-terms in Tm to their de Bruijn coding in DB.
Internal Normalization, Compilation and Decompilation for System Fβη
217
Lemma 5. (de Bruijn’s coding and βη-completeness of Tmc ) 1. If t, u ∈ Λ have free variables in the context a0 , . . . , an−1 , then t =α u if and only if tn =βη un . 2. There is a term db : Tmc → dBc of Fβη , such that for all closed terms t of Λ we have db([t]c ) = tc0 . 3. db is an internal injection from Tmc to dBc . 4. The type Tmc is βη-complete. Theorem 1. There is no decompiler for Fβη in Fβη . Proof. We Claim: any family of decompilers fA : A → Tmc is a family of internal injections. The Claim is proved as follows. Assume fA (t) =βη fA (u). By definition c c of decompiler we have fA (t) =βη [[t ]] for some t =βη t and fA (u) =βη [[u ]] for c c some u =βη u. We deduce [[t ]] =βη [[u ]] , then t =α u (by Lemma 1), and eventually t =βη u, as wished. From the βη-completeness of Tmc (Lemma 5.4) and the existence of internal injection from any closed type A of Fβη to Tmc we deduce (by Lemma 2.6) the βη-completeness of all closed types of Fβη in all models of Fβη , contradicting the fact that the observational model O is not βη-complete (Lemma 3.2).
6
An Interpretation (.)∗ of Fβη into Itself Whose Image Is Decompilable
In this section we define an interpretation (.)∗ of Fβη inside Fβη and a decompiler∗ normalizer, written in Fβη , for the terms of Fβη . In addition we prove that there ∗ . In Lemma 4 we already proved is no compiler written in Fβη for the terms of Fβη that there is no normalizer in Fβη for the codes of terms of Fβη . ∗ We formally define the notion of decompiler-normalizer and compiler for Fβη ∗ in Fβη , for any interpretation (.) of Fβη in Fβη . Definition 9. Assume (.)∗ is any interpretation of Fβη in Fβη . ∗ 1. A compiler for Fβη in Fβη is any family gA : Tmc → A∗ of closed terms of Fβη , for each closed type A of Fβη , such that for all closed terms t : A of c Fβη we have gA ([[t]] ) =βη t∗ . ∗ in Fβη is any family fA : A∗ → Tmc of 2. A decompiler-normalizer for Fβη closed terms of Fβη , for each closed type A of Fβη , such that for all closed c terms t : A of Fβη we have fA (t∗ ) =βη [[t ]] for the βη-long normal form t of t.
The goal of this section is to define some interpretation (.)∗ of Fβη into Fβη , and ∗ a decompiler-normalizer in Fβη for the terms of Fβη . We first define A∗ by an induction external to Fβη , over types A of Fβη . Let A, B be two types of Fβη . A connection of A, B is a pair (f, g) of terms f : A → B and g : B → A of Fβη in some context Γ . Two types are connected if they have a connection. For instance, (idTm , idTm ) is a connection between Tm
218
S. Berardi and M. Tatsuta
and Tm in the context ΓTm , while (lam, ap) is a connection between Tm → Tm ∗ and Tm, again in the context ΓTm . In Fβη , all type quantifications (∀α.A)∗ will be bounded over the types α which are connected with Tm, i.e., for which two maps fα : α → Tm and gα : Tm → α are given in the context in which A∗ lives. The restriction in the quantification is reminiscent of what happens in the normalization proof for Fβη : quantification has to be restricted to the set of candidates in order to interpret ∀α.A as a candidate. We define a family of connections (fA , gA ) between A∗ and Tm, and then we prove that if all type variables in FV(A) are connected with Tm, then fA is a decompiler. The idea of the pair (f, g) is taken from Friedman’s proof for λ→, but in the case of λ→ there is the additional requirement that (f, g) is an embedding-retraction pair, i.e., that g ◦f = idα . This is something we do not ask for Fβη . Definition 10. For any type A of Fβη we define A∗ by induction on A. 1. α∗ = α, if α is a type variable. 2. (B → C)∗ = B ∗ → C ∗ . 3. (∀α.B)∗ = ∀α.(α → Tm) → (Tm → α) → B ∗ . If Γ = x1 : A1 , . . . , xm : Am is any context of Fβη then Γ ∗ = x1 : A∗1 , . . . , xm : A∗m . For all types A∗ , we now define some connection between A∗ and Tm in a suitable context. We explain the idea first. 1. For any type variable α we assume some connection (fα , gα ) between α and Tm. 2. For arrow types we follow Thiery Joly [8]. We lift one connection between B ∗ , Tm and another connection between C ∗ , Tm to a connection between B ∗ → C ∗ and Tm → Tm, and eventually we compose the connection (lam, ap) between Tm → Tm and Tm, obtaining a connection between A∗ = (B ∗ → C ∗ ) and Tm. 3. The case of (∀α.A)∗ is the main original idea of this paper. Assume some connection between A∗ and Tm is given. We may lift the map : Tm → A∗ to a map : Tm → (∀α.A)∗ by λ-abstraction. Conversely, we take any term : (∀α.A)∗ , and we assign α to Tm, and the connection between α and Tm to the connection (idTm , idTm ) between Tm and Tm. We obtain a term in A[Tm/α]∗ and we apply some suitable instance of the map : A∗ → Tm. Definition 11. (The Connection fA , gA between A∗ and Tm). For each type A of Fβη with FV(A) ⊆ {α1 , ..., αn , Tm}, by induction on A, we define two terms fA : A∗ → Tm and gA : Tm → A∗ of Fβη , in the context {f1 : α1 → Tm, . . . , fn : αn → Tm, g1 : Tm → α1 , . . . , gn : Tm → αn } ∪ ΓTm . Assume y is a fresh variable. 1. fαi = fi and fTm = idTm . gαi = gi and gTm = idTm . 2. fB→C = λy : (B → C)∗ .lam(fC ◦y ◦gB ). gB→C = λy : Tm.gC ◦ap(y)◦fB .
Internal Normalization, Compilation and Decompilation for System Fβη
219
3. f∀α.B = λy : (∀α.B)∗ .fB[Tm/α] (y(Tm, idTm , idTm )). g∀α.B = λy : Tm.λα.λfα : α → Tm.λgα : Tm → α.gB (y). For any closed type A, we define fAc = λx : A∗ .λΓTm .fA (x) : A∗ → Tmc . ∗ is a triple (A∗ , fA , gA ), of some type A∗ Implicitly, the interpretation of A in Fβη ∗ and a connection between A and Tm. In the definition of g∀α.B , the term gB has three more free variables: α, fα : α → Tm, and gα : Tm → α. For example, we assume A = Id (the type of polymorphic identity id = λα.λx : α.x) and we unfold the definition of fA . fA first applies the clause for ∀α.α → α, and maps id∗ to id∗ (Tm) = λf : Tm → Tm.λg : Tm → Tm.λx : Tm.x. Then fA applies the clause for Tm → Tm, sending f, g and λx : Tm.x to lam(λx : Tm.x). This latter is the coding of the untyped λ-term λx.x, and therefore it is equal to [λx.x], that is, to [|id|], or [[id]]. We conclude fA (id∗ ) =βη [[id]], as expected: fA is a decompiler at least when applied to id∗ . We define the interpretation (.)∗ over terms.
Definition 12. Assume Γ t : A in Fβη , with Γ = {x1 : A1 , ..., xm : Am }. We define t∗ : A∗ in the context Γ ∗ ∪ {f1 : α1 → Tm, . . . , fn : αn → Tm, g1 : Tm → α1 , . . . , gn : Tm → αn } ∪ ΓTm . 1. 2. 3. 4. 5.
x∗ = x. (tu)∗ = t∗ u∗ . (λx : A.t)∗ = λx : A∗ .t∗ . (λα.t)∗ = λα.λfα : α → Tm.λgα : Tm → α.t∗ . (t(T ))∗ = t∗ (T ∗ , fT , gT ).
In the definition of (λx : A.t)∗ , the term t∗ has three more free variables: α, fα : (α → Tm), gα : (Tm → α). The main theorem of the paper is: ∗ in Fβη ). Assume u, v Theorem 2. (There is a decompiler-normalizer for Fβη are terms of Fβη . Assume ∅ t : A in Fβη , and that t is the βη-long normal form of t.
1. (.)∗ is an interpretation: if u =βη v, then u∗ =βη v ∗ . 2. fA is a decompiler-normalizer: fA (t∗ ) =βη [[t ]] : Tm. ∗ 3. There is no compiler for Fβη in Fβη . We postpone the proof of Theorem 2 to the next section.
7
Proof of the Main Theorem
In this section we prove Theorem 2. During the proof, we extend the notion of compilable (decompilable) term t to terms with free variables (both types and term variables), by asking that the “canonical substitution” of t is compilable (decompilable). The “canonical substitution” replaces any type variable α with Tm, any connection (fα , gα ) with (fTm , gTm ).
220
S. Berardi and M. Tatsuta
Definition 13. Assume t : A has the context {x1 : A1 , . . . , xm : Am }. Fix any list {f1 : α1 → Tm, . . . , fn : αn → Tm, g1 : Tm → α1 , . . . , gn : Tm → αn } of 2n fresh variables, and any list {a1 : Tm, . . . , am : Tm} of m fresh variables of type Tm. 1. σ, τ are the canonical substitutions for t if σ(αi ) = Tm, σ(fi ) = σ(gi ) = idTm , and σ(xj ) = σ(gAj (aj )), and τ (xj ) = aj , for all i = 1, . . . , n and all j = 1, . . . , m. 2. t is decompilable if σ(fA (t∗ )) =βη [[t]]τ for some σ, τ canonical for t. 3. t is compilable if σ(t∗ ) =βη σ(gA ([[t]]τ )) for some σ, τ canonical for t. In the definition of “compilable” and “decompilable” we do not ask that we recover the open untyped λ-term underlying the term, but only that we may compile (decompile) the canonical instance of the term. If A = αi for some i, then σ(fA ) = σ(fα ) = idTm = σ(gα ) = σ(gA ), therefore both “decompilable” and “compilable” unfold to σ(t∗ ) = [[t]]τ . “compilable” and “decompilable” are equivalent in this case, and they both say that the canonical substitution σ maps t into the code [[t]]τ for t. We prove that (.)∗ , fA , gA commute with substitutions. Lemma 6. (Substitution Lemma) 1. A[T /α]∗ = A∗ [T ∗ /α] 2. t[u/x]∗ = t∗ [u∗ /x] 3. fA[T /α] = (fA )[T ∗ /α, fT /fα , gT /gα ] gA[T /α] = (gA )[T ∗ /α, fT /fα , gT /gα ] 4. t[T /α]∗ = t∗ [T ∗ /α, fT /fα , gT /gα ] 5. g∀α.A (y)(T ∗ , fT , gT ) =βη gA[T /α] (y) As a consequence of Lemma 6, we can prove that (.)∗ is sound w.r.t. β. The soundness of (.)∗ w.r.t. η may be proved directly, by using η itself. Lemma 7. (Soundness of (.)∗ ) 1. 2. 3. 4. 5.
((λx : A.t)(u))∗ =βη t[u/x]∗ (λα.t)(T )∗ =βη t[T /α]∗ (λx : A.t(x))∗ =βη t∗ , if x ∈ FV(t) ∈ FV(t) (λα.t(α))∗ =βη t∗ , if α If t =βη u then t∗ =βη u∗
In the next lemma we prove that βη-long normal terms in Fβη are decompilable. Lemma 8. (Decompilation of βη-Long Normal Forms) Assume that t1 , . . . , tn , t, u are any terms, A, B, T are any types, and α is any type variable of Fβη . 1. All term variables x are compilable. 2. If t : A → B is compilable and u : A is decompilable, then tu : B is compilable. 3. If t : ∀α.A is compilable then t(T ) : A[T /α] is compilable.
Internal Normalization, Compilation and Decompilation for System Fβη
221
4. If t : α is compilable, then t is decompilable. 5. If t = x(t1 ) . . . (tn ) : α and each ti is either decompilable or is a type, then t is compilable and decompilable. 6. If t is decompilable then λx : A.t is decompilable. 7. If t is decompilable then λα.t is decompilable. 8. If t is βη-long normal then t is decompilable. ∗ Theorem 2 (there is a decompiler-normalizer for Fβη in Fβη ) is an immediate ∗ consequence of Lemma 8 Point 8. There is no compiler for Fβη in Fβη because the composition of a decompiler-normalizer and a compiler defines a normalizer (this is the key idea in normalization by evaluation).
Proof of Theorem 2 1. (.)∗ is compatible with βη by Lemma 7 Clause 5. 2. Assume t is a closed term, A is a closed type, and t : A. Let t =βη t be the βη-long normal form of t. Then by Lemma 8 Point 8 we have fA (t∗ ) =βη [[t ]] (the substitutions σ, τ are empty because t, A are closed). By the Clause 1 above we have t∗ =βη t∗ . We conclude fA (t∗ ) =βη fA (t∗ ) =βη [[t ]]. Thus, c fAc (t∗ ) =βη λΓTm .[[t ]] = [[t ]] . The family fAc of closed terms of Fβη is a decompiler. 3. Assume for contradiction that there is some family GA : Tmc → A∗ of closed terms of Fβη , for A closed type of Fβη , which is a compiler. Define evA = fAc ◦GA : Tmc → Tmc . Let t : A be a closed term of Fβη and t =βη t c c be the βη-long normal form of t. Then we have evA ([[t]] ) = fAc ◦GA ([[t]] ) =βη c ∗ c fA (t ) =βη [[t ]] . Thus, the family evA is a normalizer, which contradicts Lemma 4 Clause 1.
8
What We Can Prove about Fβη and βη-Completeness
In this section we claim that we may use the map (.)∗ in order to define a class of βη-complete models. We cannot prove our goal yet, but we may prove a result which is close to our goal: inside every consistent model M of Fβη we may internally define some model M∗ whose equational theory includes βη and is included in |βη|. Let δ = [DB/Tm, Lam/lam, Ap/ap] be de Bruijn substitution (Definition 8). A type in M∗ is a triple of a type of M and a connection pair between the type and the interpretation of dBc in M. The interpretation of a type or a term in M∗ is obtained by composing the interpretation (.)∗ of Fβη into Fβη with the interpretation of Fβη in M, and applying δ. Definition 14. Assume M is any model of Fβη . Then we define a model M∗ of Fβη as follows. Let Θ = [[DB]]M . The set TM∗ of types of M∗ consists of all triples (θ, φ, ψ), with θ ∈ TM and φ ∈ |θ⇒M Θ|M , ψ ∈ |Θ⇒M θ|M , two maps connecting θ and Θ. The set of terms of (θ, φ, ψ) is |(θ, φ, ψ)|M∗ = |θ|M . Let σ be an assignment of M∗ , with σ(αi ) = (θi , φi , ψi ) and (φi , ψi ) connecting θi and Θ, for i = 1, . . . , n. Define σ by σ (αi ) = θi , σ (fαi ) = φi , and σ (gαi ) = ψi .
222
S. Berardi and M. Tatsuta
1. For any type A of Fβη in the context Δ = α1 , . . . , αn , and any assignment σ with σ(αi ) = (θi , φi , ψi ) for i = 1, . . . , n, we set [[A]]M∗ ,σ = ([[δ(A∗ )]]M,σ , [[δ(fA )]]M,σ , [[δ(gA )]]M,σ ). 2. The set PM∗ of predicates of M ∗ is the set of all maps φ : TM∗ → TM∗ for which there is a type A of Fβη and a substitution σ of M∗ such that for all (θ, φ, ψ) ∈ T we have φ(θ, φ, ψ) = [[A]]M∗ ,(σ,(θ,φ,ψ)/α). 3. For any term t of Fβη in the context Δ, Γ , with Γ = x1 : A1 , . . . , xn : An , any σ as above, and any substitution τ over Γ such that τ (xi ) ∈ |[[Ai ]]M∗ ,σ |M∗ for i = 1, . . . , n: [[t]]M∗ ,σ,τ = [[δ(t∗ )]]M,σ ,τ . We can prove that there exists a unique ⇒M∗ : TM∗ → TM∗ → TM∗ and a unique QM∗ : TM∗ → TM∗ such that M∗ = TM∗ , |.|M∗ , PM∗ , ⇒M∗ , QM∗ is a model of Fβη . The proof requires Lemma 7 (the soundness of (.)∗ w.r.t. βη and substitution). We cannot prove that M∗ is βη-complete. However, by applying ∗ the interpretation of the decompiler for Fβη in M∗ to the elements of M∗ we can prove: Theorem 3. If M is consistent, then the equational theory =M∗ of M∗ includes βη and is included in |βη|. We would like to prove a stronger result: “in every model M of Fβη we can define some model M∗ whose equational theory is exactly βη.” We conjecture that this can be done if in the proof of the previous theorem we change the type Tm representing all untyped λ-terms of Λ in Fβη to some suitable type of Fβη representing all pseudo-terms of Fβη .
References 1. Abel, A.: Weak beta-Normalization and Normalization by Evaluation for System F. In: Cervesato, I., Veith, H., Voronkov, A. (eds.) LPAR 2008. LNCS (LNAI), vol. 5330, pp. 497–511. Springer, Heidelberg (2008) 2. Berardi, S., Tatsuta, M.: Internal Normalization, Compilation and Decompilation for System Fβη (full paper). Draft, http://www.di.unito.it/~ stefano/CompletenessF.pdf 3. Berger, U., Eberl, M., Schwichtenberg, H.: Normalisation by Evaluation. In: Prospects for Hardware Foundations, pp. 117–137 (1998) 4. Barbanera, F., Berardi, S.: A full continuous model of polymorphism. Theor. Comput. Sci. 290(1), 407–428 (2003) 5. Berardi, S., Berline, C.: βη-Complete Models for System F. Mathematical Structures in Computer Science 12(6), 823–874 (2002) 6. Berardi, S., Berline, C.: Building continuous webbed models for system F. Theor. Comput. Sci. 315(1), 3–34 (2004)
Internal Normalization, Compilation and Decompilation for System Fβη
223
7. Friedman, H.: Classically and Intuitionistically Provably Recursive Functions. In: Scott, D.S., Muller, G.H. (eds.) Higher Set Theory. LNM, vol. 699, pp. 21–28. Springer, Heidelberg (1978) 8. Joly, T.: Codage, Separabilite et Representation, These de doctorat, Universite de Paris VII (2000), http://www.cs.ru.nl/~ joly/these.ps.gz 9. Garillot, F., Werner, B.: Simple Types in Type Theory: Deep and Shallow Encodings. In: Schneider, K., Brandt, J. (eds.) TPHOLs 2007. LNCS, vol. 4732, pp. 368–382. Springer, Heidelberg (2007) 10. Longo, G., Moggi, E.: Constructive Natural Deduction and its ‘Omega-Set’ Interpretation. MSCS 1(2), 215–254 (1991) 11. Mitchell, J.C.: Semantic Models For Second-Order Lambda Calculus. In: 25th Annual Symposium on Foundations of Computer Science, pp. 289–299 (1984) 12. Moggi, E., Statman, R.: The Maximum Consistent Theory of Second Order Lambda Calculus. e-mail message to the “Types” net (July 24, 1986), http://www.di.unito.it/~ stefano/MoggiStatman1986.zip 13. Pfenning, F., Elliott, C.: Higher-order abstract syntax. In: Wexelblat, R.L. (ed.) Proceedings of the ACM SIGPLAN 1988 PLDI. SIGPLAN Notices, vol. 23(7), pp. 199–208. ACM Press, New York (1988) 14. Pfenning, F., Lee, P.: LEAP: A language with eval and polymorphism. In: D´ıaz, J., Orejas, F. (eds.) TAPSOFT 1989 and CCIPL 1989. LNCS, vol. 352, pp. 345–359. Springer, Heidelberg (1989)
Towards Normalization by Evaluation for the βη-Calculus of Constructions Andreas Abel Project PI.R2, INRIA Rocquencourt and PPS, Paris
[email protected]
Abstract. We consider the Calculus of Constructions with typed beta-eta equality and an algorithm which computes long normal forms. The normalization algorithm evaluates terms into a semantic domain, and reifies the values back to terms in normal form. To show termination, we interpret types as partial equivalence relations between values and type constructors as operators on PERs. This models also yields consistency of the beta-eta-Calculus of Constructions. The model construction can be carried out directly in impredicative type theory, enabling a formalization in Coq.
1 Introduction The proof assistant Coq [INR08] based on intensional type theory is used for large verification projects in mathematics [Gon04] and computer science [Ler06]. However, to this day no complete meta theory of its logical core, the Calculus of Inductive Constructions (CIC) exists. The CIC is a dependent type theory with at least one impredicative base universe (Set or Prop or both) and an infinite cumulative hierarchy of predicative universes (Typei ) above this base. Inductive types with large (aka strong) eliminations exist at every level. The CIC is formulated with untyped equality, leading to complications in model constructions [MW03] and in the treatment of η-equality. As η-reduction, the subject reduction property requires contravariant subtyping, which is especially hard to model (I am only aware of Miquel’s coherence space model [Miq00]). And it cannot be formulated as η-expansion in an untyped setting. The lack of η-equality in Coq is an annoyance both for its implementers and its users. Recently, formulations of CIC with typed equality, aka judgemental equality, have been considered since they admit simple set-theoretical models [Bar09]. Judgemental equality also integrates η-equality nicely. On the downside, injectivity of the function space constructor Π, crucial for the implementation of type checking, is notoriously difficult to establish. Goguen [Gog94] has obtained injectivity of Π in the Extended Calculus of Constructions via his Typed Operational Semantics, a typed Kripke term model with standardizing reduction. In predicative Martin-L¨of Type Theory, it is the byproduct of a PER model construction which also yields Normalization by Evaluation (NbE) [ACD07]. In this article, we investigate NbE for the Calculus of Constructions (CoC), a fragment of the CIC with just one impredicative and one predicative universe, with typed βη-equality. By constructing a PER model, we obtain termination and completeness for M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 224–239, 2010. c Springer-Verlag Berlin Heidelberg 2010
Towards Normalization by Evaluation for the βη-Calculus of Constructions
225
NbE, the latter meaning that all judgmentally equal terms normalize to the same expression. As a consequence of the model, we obtain logical consistency of the βη-CoC. The missing property of soundness of NbE, meaning that each term is judgmentally equal to its computed normal form, is implied by injectivity of Π and vice versa. Decidability of typing also hinges on injectivity. This leaves two options to complete this work and obtain a sound and complete type checker for the CoC with η: Prove soundness of NbE by Kripke logical relations between syntax and semantics as in [ACP09], or obtain injectivity by syntactical means. Adams [Ada06] obtained injectivity for functional pure type systems with judgemental β-equality; his proof might extend to βη. Overview. This article is organized as follows: In Section 2 we introduce CoC with typed equality and explicit substitutions. In Section 3 we define normalization by evaluation for CoC using partial applicative structures, and we specify a type inference algorithm. In Section 4 we recapitulate a simple method how to classify CoC expressions into terms, types, and kinds, a device which helps us to bootstrap the PER model construction in Section 5. Section 6 proves the rules of CoC sound wrt. our model, and as a corollary we obtain termination and completeness of NbE and consistency of CoC. Loose ends are listed in the conclusions (Section 7).
2 Syntax We present the Calculus of Constructions (CoC) as a pure type system (PTS) with annotated λ-abstraction, typed equality and explicit substitutions. 1. Annotated λ-abstraction (as in λ M N ) enables us to compute the type of a term from the type of its free variables and its semantics from the semantics of its free variables (see Section 6). Thus, a term makes already sense in a context alone, it does not need an ascribed type. 2. Typed equality is the natural choice in the presence of η since untyped η-reduction is badly behaved in set-theoretical models and type-theoretical models without subtyping —this includes the semantics we are constructing. (Untyped η-expansion cannot be defined.) 3. Lambda calculi with explicit substitutions have more models than lambda calculi with substitution implemented as an operation. In particular, the model of closures in weak head normal form we will use in Section 3. Recent meta theoretic studies involving explicit substitutions include [Dan07, Cha09, Gra09, ACP09]. In the presence of explicit substitutions, variables are most naturally represented as de Bruijn indices [ACCL91]. 2.1 Expressions and Typing The CoC is a dependently typed lambda calculus with expressions on three levels: terms t, u, the data structures and programs of the language; the types T, U of terms, generalized to a lambda-calculus of type constructors1; and the kinds κ, ι, the types of types. 1
E.g., List is a type constructor which produces a type of homogeneous lists List T for each element type T .
226
A. Abel
In the PTS-style presentation, there is just one language of expressions M, N for all three levels, and the classification of expressions into terms, type constructors, and kinds is a byproduct of typing, using the two sorts s ::= ∗, . The inhabitants of sort are kinds, one of which is ∗, and the inhabitants of sort ∗ are types, whose inhabitants in turn are terms. From our perspective, the CoC is a dependent version of System Fω . In terms of the Calculus of Inductive Constructions, the core language of Coq [INR08], the sort ∗ is the impredicative Set, and the sort the predicative Type0 . Alternatively [Wer92], one could identify ∗ with the impredicative Prop and refer to the levels as proof terms, predicates, and kinds instead. Syntax of expressions, substitutions, and contexts. We represent bound variables by de Bruijn indices; the 0th variable is represented by the expression v0 , the ith variable by the i-fold application of the lifting substitution to the expression v0 . Consequently, we use the expression vi as a shorthand for the expression v0 i . Also, we abbreviate (id, N ) by [N ]. Sort s ::= ∗ | Exp M, N, t, u, T, U, κ, ι ::= s | v0 | λ M N | M N | Π M N | M σ Subst σ, τ ::= | id | σ τ | (σ, M ) Cxt Γ, Δ ::= () | Γ, M We denote the length of context Γ by Γ . We use ≡ for literal identity of expressions, and the dot notations Π U. T and λ U. M to save parentheses. Normal forms are those expressions that do not contain substitutions (except lifting of an index) or β-redexes. Normal forms starting with a variable are called neutral. Norm v, w, V, W ::= s | λV w | Π V W | n Neut n, N ::= vi | n v
β-normal form neutral normal form
Typing. The judgements Γ “Γ is a well-formed context” and Γ M : N “M has type N in context Γ ” are given inductively by the following rules. Γ () Γ Γ ∗: Γ T :s Γ, T v0 : T
Γ T :s Γ, T
Γ U :s Γ, U T : s Γ Π U T : s
Γ U :s
Γ M :ΠUT Γ N :U Γ M N : T [N ]
Γ, U T : s Γ, U M : T Γ λU M : Π U T Γ M :T Γ T = T : s Γ M : T
Towards Normalization by Evaluation for the βη-Calculus of Constructions
227
We come to the judgement for type equality, Γ T = T : s, later. The typing rules for substitutions are: Γ σ:Δ Δ M :T Γ Mσ:Tσ Γ Γ id : Γ
Γ σ:Δ
Δ T :s Γ M :Tσ Γ (σ, M ) : Δ, T
Γ1 τ : Γ2 Γ2 σ : Γ3 Γ1 σ τ : Γ3
Γ T :s Γ, T : Γ
For i < Γ , we define context look-up Γ (i) by (Γ, T )(0) = T and (Γ, T )(i + 1) = Γ (i) . It is easy to see that the general variable rule is derivable by induction on i: Γ Γ vi : Γ (i) Using this rule, we can understand typing of expressions from the first set of rules alone, under an abstract view on substitution and equality. 2.2 Typed Equality We formalize βησ-equality by the judgements Γ M = M : T and Γ σ = σ : Δ. Equality holds only between well-formed objects of syntax, thus the rules have to be formulated such that they entail Γ M : T (and likewise for M , σ, and σ ). For instance, the η-rule reads: Γ M :ΠUT Γ M = λ U. (M ) v0 : Π U T We will not spell out the rules will types and typing assumptions. Instead, we will just write down axioms in the form M = M and σ = σ , the typing can be reconstructed. Also we will skip all congruence rules expressing that equality is an equivalence relation and that it is closed under all syntactic constructions. We have taken a similar approach before [ACP09] which is justified by Cartmell’s work on generalized algebraic theories [Car86]. Computation: β, resolution of substitutions, pushing substitution under constructions. (λU M ) N = M [N ] v0 (σ, M ) = M (σ, M ) = σ M id sσ
=M =s
(M N ) σ (λ U M ) σ (Π U T ) σ (M σ) τ (σ, M ) τ
= (M σ) (N σ) = λ U σ. M (σ , v0 ) = Π U σ. T (σ , v0 ) = M (σ τ ) = (σ τ, M τ )
Non-computational rules: Extensionality and rules of the category of substitutions. M = λ U. (M ) v0 id = (, v0 )
id σ =σ σ id =σ (σ1 σ2 ) σ3 = σ1 (σ2 σ3 )
228
A. Abel
3 Normalization by Evaluation We conceive normalization by evaluation as the composition of a standard interpreter : Exp → D mapping expressions into a semantics D and a reifier which computes a long normal form from a value in D. Coquand [ACP09] observed that the η-expansion part of reification can be carried out entirely within the semantics which splits reification into an η-expansion phase ↓ : D → Dnf and a read-back phase Rnf : Dnf → Norm ⊆ Exp.
Exp o
⊇
Norm o
Rnf
/D aDD DD ↑ DD ↓ DD D Dnf Dne
η-expansion ↓ is mutually defined with reflection ↑ : Dne → D which injects variables (de Bruijn levels), and more generally neutral values e ∈ Dne , into the semantics in η-expanded form. 3.1 Weak Head Evaluation While having used a Scott domain to represent values in previous work [ACP09], we now change to partial applicative structures which subsume Scott domains (and, indeed, all λ-models and -algebras). The following representation of values by closures is such a structure and it can be directly formalized in type theory which is not the case for any effective total applicative structure. Values are defined in terms of closures (λt)η and de Bruijn levels. Level xj represents the jth free variable. In effect, we are using a locally nameless presentation of values [Pol94] where bound variables are represented as relative references (de Bruijn indices vi ) and free variables as absolute references, i. e., names (de Bruijn levels xj ). The delayed η-expansions ↑Π A F e and ↓Π A F f are the defunctionalization of reflection and reification in the Scott domain (as closures are the defunctionalization of evaluation). D a, b, f, A, B, F, G, K, L ::= s | Π A F | (λt)η | ↑Π A F e | e DNe e, E ::= xj (j ∈ N) | e d DNf d, D ::= ↓Π A F f | a Env η ::= id | (η, a)
value neutral value normal value environment
We let ↑B e = e and ↓B a = a if B is not a Π-type. Evaluation and application. We introduce the judgements M η a “in environment η, expression M evaluates to a”, ση η “in environment η, substitution σ evaluates to environment η ”, and f ·a b “value f applied to value a evaluates to b” inductively by the following rules.
Towards Normalization by Evaluation for the βη-Calculus of Constructions
sη s
v0 (η,a) a
tη f
(η,a) η
idη η
U η A Π U T η Π A (λT )η
λ U tη (λt)η
uη a t uη b
f ·a b
ση η tη a t ση a
ση η tη a (σ, t)η (η , a)
t(η,a) b (λt)η · a b
229
τ η η ση η σ τ η η
F ·a B (↑
Π AF
e) · a ↑B (e ↓A a)
These three relations are deterministic, thus, they can be turned into partial functions : Exp × Env D, : Subst × Env Env, and · : D × D D. 3.2 Read-Back (aka Reification) We introduce two judgements m d v “at level m, normal value d reifies to normal form v”, and m ne e n “at level m, neutral value reifies to neutral normal form n” inductively by the following rules. The natural number m corresponds to the de Bruijn level of the next fresh variable. m A V m ∗ ∗ m A V
F · ↑A xm B
F · ↑A xm B m+1 B W m Π AF Π V W f · ↑A xm b
m + 1 ↓B b w
m ↓Π A F f λ V w m ne e n m e n
m ne xj vm−(j+1)
m ne e n m d v ne m ed nv
In the but last rule, we use the “monus” function on N where m − m = 0 if m ≥ m. Read-back is deterministic, so we introduce two partial functions by Rnf m d = v iff ne m d v and Rne e = n iff m e n. These correspond to the read-back m function by Gregoire and Leroy [GL02] and own previous work [ACP09]. 3.3 Type Inference In the following we adopt bidirectional value-based type checking [Coq96, ACD08] to de Bruijn style. Since we have typed abstraction, the type of every well-typed term is inferable. We specify the type inference algorithm by a deterministic inductive judgement Δ t⇒A
230
A. Abel
meaning that in context Δ, the principal type of t is A. We keep context Δ and type A in evaluated form. The algorithm is very similar to Huet’s constructive engine [Hue89] as refined by Pollack [Pol06]. By induction on a context of values Δ, we define the η-expanded environment idΔ which maps de Bruijn indices to their corresponding de Bruijn levels. We write xΔ for xΔ . id() = id
idΔ,A = (idΔ , ↑A xΔ )
Type inference is given inductively by the following rules. Note that only type checked terms are evaluated, and only values enter the contexts or are returned. Δ U ⇒s Δ, U idΔ T ⇒ s Δ Π U T ⇒ s
Δ ∗⇒ Δ U ⇒s
U idΔ A Δ, A t ⇒ B Δ λU t ⇒ Π AF
Δ vi ⇒ Δ(i) λ
Δ, A B −→ F
Δ A∼ =B
Δ t ⇒ Π AF Δ u⇒B Δ t u ⇒ F · uidΔ
In the last two rules we have used two auxiliary judgements. In the application rule, we check the ascribed type A and the inferred type B for βη-equality using Δ A ∼ = B. In the application rule, we need to turn the type value B of the function body t into a λ
function over the last variable xΔ , which is of type A. We write Δ, A B −→ F for this abstraction operation. Δ A∼ =B
⇐⇒ Δ A V and Δ B V
λ
Δ, A B −→ F ⇐⇒ Δ, A B V and F ≡ (λV )id Type values A and B are equal if they reify to the same normal form V . Due to our λ
locally nameless style values, abstraction Δ, A B −→ F is a bit cumbersome. We implement it by first reifying value B in context Δ, A to term V and then building the closure F ≡ (λV )id. 3.4 Normalization During type inference, values are reified to normal forms to test equality. We can also compose evaluation and read-back to obtain a normalization function for terms. Let Γ be evaluation of contexts partially defined by () = ()
Γ, U = Γ , U idΓ
Using the partially defined identity environment ηΓ := idΓ , the partial normalization function is now obtained as T ηΓ nbeTΓ (t) = Rnf tηΓ ) Γ (↓
NbeΓ (T ) = nbe Γ (T ).
Towards Normalization by Evaluation for the βη-Calculus of Constructions
231
The goal of this work is to show its correctness on well-formed expressions, i. e.: 1. Soundness: if Γ t : T then Γ t = nbeTΓ (t) : T . 2. Completeness: if Γ t = t : T then nbeTΓ (t) ≡ nbeTΓ (t ). 3. Termination: if Γ t : T then nbeTΓ (t) is defined. The termination property is a consequence of soundness and also of completeness, since judgemental equality and expression equality presuppose definedness. Remarks on soundness can be found in the long version of this paper [Abe10]. In the remainder of the paper, we will focus on completeness, which will be established by a PER model construction.
4 Classification of Expressions If Γ κ : , then κ is a called a kind. If Γ T : κ for a kind κ, then T is called a type constructor. In particular, if Γ T : ∗, then T is called a type. If Γ t : T for a type T , then t is called a term. We obtain three syntactic subclasses Kind, Ty, Tm of Exp. There are no kind variables, only term variables and type (constructor) variables. Kind κ, ι ::= ∗ | Π κ κ | Π U κ Ty T, U ::= Π U T | Π κ T | X | λκT | T U | λU T | T u Tm t, u ::= x | λ U t | t u | λ κ t | t U It is well-known that all dependencies in pure CoC can be erased such that one ends up with the terms, type constructors, and kinds of System Fω . This way, one can inherit normalization of CoC from Fω [GN91]. However, since we want to add inductive types in ∗ with large eliminations into ∗ (just as Werner [Wer92]), we cannot pursue this path; CoC with natural numbers has types defined by recursion on a number, so it can express types of functions with varying arity, like (X : ∗) → (n : Nat) → X · · → X → X → · n times which have no counterpart in Fω . However, without large eliminations into , so no kinds defined by recursion, the structure of kinds is still simple and dependencies can be erased on the kind level. This observation by Coquand and Gallier [CG90] and Werner [Wer92] has been exploited by Barras and Werner [BW97] to completely formalize strong normalization of pure CoC in Coq. Following their lead, we will use erased kinds to bootstrap our model construction in Section 5. Simple kinds. We enrich the kinds of Fω by a base kind of terms. SKi k ::= | l SKiP l ::= ∗ | k → l SCxt γ, δ ::= () | γ, k
simple kind proper simple kind simple kinding context
232
A. Abel
The simple kind → k is the erasure of the indexed kind Π T κ. The grammar forbids k → , the kind of functions from constructors of simple kind k to terms, which is a subset of the terms, so we set k → := . We define the judgements γ M ÷ k “in context γ, expression M has simple kind . k”, γ σ ÷ δ “in context γ, substitution σ has simple kinding δ, and γ M = k “in context γ, expression M has skeleton k” inductively by the rules to follow. These rules are basically an erasure of the typing rules. Kinds κ are related to their skeleton k by . γ κ = k, where Types T : ∗ are assigned skeleton . Type constructors T : κ are be related to the skeleton k of κ by judgement γ T ÷ k, and terms to skeleton . . . γ U =k γ, k T = k . γ Π U T = k → k
. γ ∗=∗
γ, k v0 ÷ k
. γ U =k γ, k M ÷ k γ λ U M ÷ k → k
γ M ÷ k → k γ N ÷k γ M N ÷ k γ, k ÷ γ
γ T ÷∗ . γ T =
γ σ÷δ δ M k . ∈ {÷, =} γ Mσk
γ id ÷ γ
γ σ÷δ γ M ÷k γ (σ, M ) ÷ δ, k
γ1 τ ÷ γ2 γ2 σ ÷ γ3 γ1 σ τ ÷ γ3 Shape computation. We now define two (total) functions, |M |÷ γ “the kind of M in . = simple context γ”, and |M |γ “the skeleton of M in simple context γ”, by means of a . general shape function |M |γ which returns a pair (, k) with ∈ {=, ÷}: .
|M |= γ
=
. k if |M |γ = (=, k) otherwise
|M |÷ γ
=
k if |M |γ = (÷, k) ∗ otherwise
The shape function is defined mutually with the function |σ|÷ γ , written |σ|γ , which computes the kinds of the expressions in σ. We also define the skeleton |Γ | of a context. . |∗|γ = (=, ∗) . . . = .) |Π U T |γ = (=, |U |= γ → |T |γ,|U|= γ
|v0 |γ,k = (÷, k) |v0 |() = (÷, ) . ÷ .) |λ U M |γ = (÷, |U |= γ → |M |γ,|U|= γ ÷ (÷, k ) if |M |γ = k → k |M N |γ = (÷, ) otherwise |M σ|γ = |M ||σ| γ
||γ,k ||() |id|γ |(σ, M )|γ |σ τ |γ
=γ = () =γ = |σ|γ , |M |÷ γ = |σ||τ |
|()| |Γ, T |
= () . = |Γ |, |T |= |Γ |
γ
Towards Normalization by Evaluation for the βη-Calculus of Constructions
233
Lemma 1 (Soundness of shape computation). For L ::= M | σ and R ::= k | γ and γ L R we have R = |L|γ . . . = Lemma 2 (Kind skeleton independence). |M |= γ = |M |γ for all γ, γ . . Therefore, we may suppress γ and just write |M |= . Theorem 1 (Shapes of wellformed expressions). Let γ = |Γ |. . . 1. If Γ κ : then γ κ = |κ|= γ. . . = 2. If Γ κ = κ : then |κ|= γ = |κ |γ . . . 3. If Γ T : ∗ then γ T = |T |= γ = . . = 4. If Γ M : T ≡ then γ M ÷ |M |÷ γ = |T |γ . 5. If Γ σ : Δ then γ σ ÷ |σ|γ = |Δ|. Proof. Simultaneously by induction on the typing/equality derivation. Note that Γ ÷ ≡ implies |M |÷ M = M : T γ = |M |γ since the kinds of M and M equal the skeleton of T .
5 A Model for the βη-CoC with Large Eliminations In this section, we present a PER model of CoC. Each expression M is modeled by a pair (F, F ) where F : D is simply the value of M and F is the semantic role of M . Terms t ÷ have no semantic role, they are modeled by a pair (a, ()). Types T ÷ ∗ are modeled by a pair (A, A) where A is a partial equivalence relation (PER) between semantic terms. The objects (a, ()) and (a , ()) are related by A iff, intuitively, a and a are βη-equal values of type A. Formally that means that ↓A a and ↓A a must read back as the same expression; this connection between A and A is written A A and pronounced “A realizes A”. Type constructors T ÷ k → k are modeled as (F, F ) where F is a higher-order operator on PERs, it maps constructors (G, G) of kind k to constructors F (G, G) of kind k . Note that unlike in System Fω or erased versions of the CoC [BW97, Geu94], F also depends on a value G (see [Wer92, SG96]). Finally . kinds κ = k are modeled as (K, K) where the PER K relates constructors (F, F ) and (F , F ) of kind k if F and F are extensionally equal operators and ↓K F and ↓K F have the same normal form (thus, K realizes K). To avoid duplication we have modeled types and kinds uniformly in the formal presentation of the semantics, probably at the cost of readability; may this informal exposition serve as an Ariadne thread in the maze to follow. Meta language. We use an impredicative type-theoretic meta language, i. e., we will not speak in terms of sets, but in terms of types and predicates. However, we will use some set-theoretic notation with care. For a type α, the type P(α) contains the predicates over α, and for P : P(α) and a : α we write a ∈ P if P (a) holds. The subset type {a : α | P (a)} is the type of pairs (a, p) such that p is a proof of P (a). Usually, we suppress the proof and write just a ∈ {a : α | P (a)}. The value f (a, p) of a function f : {a : α | P (a)} → β may not depend on the form of the proof p.
234
A. Abel
A setoid is a pair of a type α and an equivalence relation =α : P(α × α). We write α for the setoid. A function f : α → β is a (setoid) morphism, f ∈ α → β, if it respects setoid equality, i. e., a =α a implies f (a) =β f (a ). Two morphisms f, f are equal, f =α→β f iff a =α a implies f (a) =β f (a). This makes (α → β, =α→β ) a setoid in turn. A partial equivalence relation A : Per(α) is a binary relation over type α which is symmetric and transitive. We write a = a ∈ A for a, a : α with (a, a ) ∈ A, and a ∈ A for a = a ∈ A. Equality A = A of PERs holds extensionally if for all a, a : α, a = a ∈ A iff a = a ∈ A . Each PER A can be coerced into an associated setoid {a : α | a ∈ A} with setoid equality A. This defines the notion of morphism F ∈ A → β from PER A to setoid β. We define Ne : Per(DNe) and Nf : Per(DNf) by e = e ∈ Ne ⇐⇒ ∀m : N. ∃n : Neut. m ne e n and m ne e n d = d ∈ Nf ⇐⇒ ∀m : N. ∃v : Norm. m d v and m d v. Note that transitivity follows from determinism of read-back. A partial function H : α β is a pair (dom(H) : P(α), apply(H) : {a : α | a ∈ dom H} → β) where dom(H) and apply(H) respect the setoid equalities associated to α and β. We will write H(a) “H(a) is defined” if there is a proof p that a ∈ dom(H), and then H(a) stands for apply(H)(a, p). Two partial functions H, H are equal H =αβ H if they have equal domains and coincide pointwise (wrt. to setoid equalities); this makes α β a setoid. Raw interpretation of kinds. Let () denote the unit type with single inhabitant (). We define the candidate space k for simple kind k and an inhabitant ⊥k : k by recursion on k: = () ∗ = Per(D × ()) k → k = (D × k) k
⊥ = () ⊥∗ = Ne() ⊥k→k (G : D, G : k) = ⊥k
where Ne() = Ne × () = {((e, ()), (e , ())) | e = e ∈ Ne}. Extensional setoid equality F = k F is defined along the way. We set k = Per(D × k). Note that = ∗. Sort interpretation. For K : D, K : k we define K, K : k and K K, “K realizes K”, by (F, F ) = (F , F ) ∈ K ⇐⇒ ↓K F = ↓K F ∈ Nf (↑ E, ⊥k ) = (↑K E , ⊥k ) ∈ K ⇐⇒ E = E ∈ Ne K K ⇐⇒ K ⊆ K ⊆ K K
In words, code K realizes PER K iff equal inhabitants F of K reify to the same normal form, where K directs the amount of η-expansion during reification; and neutrals E can be reflected at code K into K. Equivalently, we could say K K iff ↓K ◦ π1 ∈ K → Nf, “↓K after the first projection is a PER morphism from K to Nf”, and (E : DNe → (↑K E, ⊥k )) ∈ Ne → K, “↑K paired with ⊥k is a PER morphism from Ne to K”. Lemma 3 (Least PER is a candidate). For all E ∈ Ne, E ⊥∗ : .
Towards Normalization by Evaluation for the βη-Calculus of Constructions
235
Equality of candidates (K, K) = (K , K ) ∈ k shall hold iff 1. K K and K K . 2. K =
k K and ↓ K = ↓ K ∈ Nf. 3. ↑K = ↑K ∈ Ne × {⊥k } → K and ↓K = ↓K ∈ π1 (K) → Nf. This states that K and K are extensionally equal PERs, plus the associated codes K and K are reifiable, plus they are indistinguishable with respect to their own normal form and their directive behaviour during reification of inhabitants of K and reflection of neutrals into K. Now sort ∗ is interpreted by and (informally) by k = k. Lemma 4. For all k, k is a PER over D × k. If K K then (K, K) ∈ k. Lemma 5 (Interpretation of ∗). We have ∗ : ∗ and (∗, ) = (∗, ) ∈ ∗. Function space construction. For simple kinds k, l : SKi we define k,l k,l
: (K ∈ k) → (K → l) → k → l K L = {((F, F ), (F , F )) : (D × k → l)2 | for all (G, G) = (G , G ) ∈ K, F · G , F · G , F (G, G) , F (G , G ) , and (F · G, F (G, G)) = (F · G , F (G , G )) ∈ L(G, G)}.
In case l = both F , F : k → l = () are trivial and we let the application F (G, G) be defined as (). Otherwise, F , F : D × k l. k,l K L is indeed a PER, the proof is standard. The definition above is a bit abstract, k,l the following table conveys some intuition about . k l not not not not
description dependent function space universal quantification indexed kind formation function kind formation
PTS rule (∗, ∗, ∗) (, ∗, ∗) (∗, , ) (, , )
Where is the impredicativity? For k = ∗ and l = we should recover the impredicative ∗, quantification of System F. Let us look at the definition of this instance : (K ∈ Per(D × ∗)) → (K → ∗) → ∗. Remember that ∗ = Per(D × ()), thus, modulo the isomorphism D × () ∼ = D we get ∗, K L = {(F, F ) : D2 | ∀G, G : D, G, G : ∗. (G, G) = (G , G ) ∈ K =⇒ F · G = F · G ∈ L(G, G)}. Hence, to obtain a new element K L : ∗ we quantify over all G, G : ∗ — there is System F impredicativity. Lemma 6 (Function type formation, introduction, and elimination). Let K, K : k, L, L ∈ K → l, and F , F ∈ K → l. The following inferences are valid in the model.
236
A. Abel
(K, K) = (K , K ) ∈ k (L · G, L(G, G)) = (L · G , L (G , G )) ∈ l for all (G, G) = (G , G ) ∈ K (Π K L, K L) = (Π K L , K L ) ∈ k → l (F · G, F (G, G)) = (F · G , F (G , G )) ∈ L(G, G) for all (G, G) = (G , G ) ∈ K (F, F ) = (F , F ) ∈ K L (F, F ) = (F , F ) ∈ K L (G, G) = (G , G ) ∈ K (F · G, F (G, G)) = (F · G , F (G , G )) ∈ L(G, G) Proof. Introduction and elimination follow directly from the definition of K L. The formation rule follows from properties of reflection and reification at function types [ACD07].
6 Soundness of the Model We extend the raw semantic interpretation to shapes by setting ÷, k = k and . =, k = k, and to simple kinding contexts via () = () and γ, k = γ × k. Interpretation. By induction on M/σ we simultaneously define the partial functions [[M ]]η;γ; : (ρ : γ) |M |γ and [[σ]]η;γ; : (ρ : γ) |σ|γ . [[∗]]η;γ;ρ = : ∗ |U|=. ,|T |=. [[Π U T ]]η;γ;ρ = [[U ]]η;γ;ρ ((G, G) ∈ [[U ]]η;γ;ρ → [[T ]]η,G;γ,|U|=. ;ρ,G ) [[v0 ]]η;γ,k;ρ,G = G : k [[λ U M ]]η;γ;ρ = ((G, G) ∈ [[U ]]η;γ;ρ → [[M ]]η,G;γ,|U|=. ;ρ,G ) [[M N ]]η;γ;ρ = [[M ]]η;γ;ρ (N η , [[N ]]η;γ;ρ ) [[M σ]]η;γ;ρ = [[M ]]ση ;|σ|γ ;[[σ]] η;γ;ρ
[[]]η;γ,k;ρ,o [[id]]η;γ;ρ [[(σ, M )]]η;γ;ρ [[σ τ ]]η;γ;ρ
= ρ : γ = ρ : γ = [[σ]]η;γ;ρ , [[M ]]η;γ;ρ : |σ|γ , |M |÷ γ = [[σ]]τ η ;|τ |γ ;[[τ ]] : |σ||τ | η;γ;ρ
γ
The prime source of partiality is the potential undefinedness of N η in the interpretation of the application M N . Contrast this to Barras and Werner [BW97] where N η is gone and with it the large eliminations. The precise conditions for definedness can be extracted mechanically from this definition, for instance, [[Π U T ]]η;γ;ρ is defined iff [[U ]]η;γ;ρ is defined and for all (G, G) ∈ [[U ]]η;γ;ρ , [[T ]]η,G;γ,|U|=. ;η;G is defined.
Towards Normalization by Evaluation for the βη-Calculus of Constructions
237
Validity. We define the relation (η; ρ) = (η ; ρ ) ∈ [[Γ ]] for η, η : Env and ρ, ρ : |Γ | inductively by the rules (η; ρ) = (η ; ρ ) ∈ [[Γ ]]
(G, G) = (G , G ) ∈ [[T ]]η;|Γ |;ρ
(η, G; ρ, G) = (η, G ; ρ, G ) ∈ [[Γ, T ]]
((); ()) = ((); ()) ∈ [[()]]
There is an implicit premise [[T ]]η;|Γ |;ρ in the second rule. By induction on Γ we simultaneously define the following propositions: () |= Γ, T |=
:⇐⇒ true :⇐⇒ Γ |= T :
Γ |= κ = κ :
:⇐⇒ Γ |= and for all (η, ρ) = (η , ρ ) ∈ [[Γ ]],
. (κη , [[κ]]η;|Γ |;ρ ) = (κ η , [[κ ]]η ;|Γ |;ρ ) ∈ |κ|=
Γ |= M = M : T ≡ :⇐⇒ Γ |= T : and for all (η, ρ) = (η , ρ ) ∈ [[Γ ]], (M η , [[M ]]η;|Γ |;ρ ) = (M η , [[M ]]η ;|Γ |;ρ ) ∈ [[T ]]η;|Γ |;ρ Γ |= M : T
:⇐⇒ Γ |= M = M : T
Γ |= σ = σ : Δ
:⇐⇒ Γ |= and Δ |= and for all (η, ρ) = (η , ρ ) ∈ [[Γ ]], (ση , [[σ]]η;|Γ |;ρ ) = (σ η , [[σ ]]η ;|Γ |;ρ ) ∈ [[Δ]]
Γ |= σ : Δ
:⇐⇒ Γ |= σ = σ : Δ
Lemma 7. If Γ |= then = ∈ [[Γ ]] is a PER. Theorem 2 (Fundamental theorem). If Γ J then Γ |= J. Proof. Simultaneously by for all judgements J by induction on Γ J. Theorem 3 (Completeness of NbE). If Γ M = M : T then nbeTΓ M ≡ nbeTΓ M . . = Proof. Define ρΓ by the clauses ρ() = () and ρΓ,U = ρΓ , ⊥|U| and prove that (ηΓ , ρΓ ) ∈ [[Γ ]] by induction on Γ . Let (F, F ) = (M ηΓ , [[M ]]ηΓ ;|Γ |;ρΓ ) and (F , F ) analogously. If T ≡ then with (K, K) = (T ηΓ , [[T ]]ηΓ ;|Γ |;ρΓ ) we obtain (F, F ) =
(F , F ) ∈ K and K K by the fundamental theorem. Hence, ↓K F = ↓K F ∈ Nf, K K nf so in particular Rnf Γ (↓ F ) ≡ RΓ (↓ F ). If T ≡ then the fundamental theorem . yields (F, F ) = (F , F ) ∈ k for k = |M |= which also implies ↓ F = ↓ F ∈ Nf. 6.1 On Consistency From our PER model we can prove the consistency of CoC as follows. Add a new type constant ∅, the empty type, with rules Γ ∅ : ∗ and Γ ∅ = ∅ : ∗ and semantics [[∅]]η;γ;ρ = Ne() . Theorem 4 (Consistency). t : ∅. Proof. Let a = tid and observe that a cannot mention a de Bruijn level xj . Since a ∈ Ne by the fundamental theorem (Thm .2), we have 0 ne a n for some n. But this means that a is of the shape xj d for some j, contradiction!
238
A. Abel
7 Conclusion We have built a model for the Calculus of Constructions with typed βη-equality which proves termination and completeness of normalization by evaluation. The model supports extensions of the CoC by small inductive types (“small” meaning “in ∗”) and large (aka strong) eliminations into ∗, i. e., types defined by recursion [Wer92]. The model is formalizable directly in impredicative dependent type theories with inductive definitions, e. g., in the Calculus of Inductive Constructions (CIC) [INR08]. This work is a first step towards a metatheory of CIC with typed βη-equality using normalization by evaluation. The long term goal is a correctness proof for the Coq type checker in the presence of η. However, a number of things need to be done: – Construct a Kripke logical relation between expressions and values that proves the soundness of NbE without external reference to injectivity. As a consequence, we obtain soundness and completeness of the type checker of Section 3. – Extend the calculus to more universes and to large inductive types. This will break simple kinding, requiring a new way to bootstrap the model. The usual technique are inaccessible cardinals [Luo89, Gog94, Miq00], however, we seek a more direct representation in type theory. Acknowledgments. The author acknowledges the support of INRIA through a guest researcher fellowship. He would like to thank Hugo Herbelin and Pierre-Louis Curien for the invitation, and Bruno Barras and Benjamin Werner for discussions on the topic of this work. Their [Wer92, BW97] and Stefanova and Geuver’s work [SG96] were a major help in understanding models for the CoC. Thanks also to the anonymous referees for their comments which helped to improve the quality of this paper.
References [Abe10]
Abel, A.: Towards Normalization by Evaluation for the Calculus of Constructions (Extended Version). Available on the author’s homepage (2010) [ACCL91] Abadi, M., Cardelli, L., Curien, P.-L., L´evy, J.-J.: Explicit substitutions. JFP 1(4), 375–416 (1991) [ACD07] Abel, A., Coquand, T., Dybjer, P.: Normalization by evaluation for Martin-L¨of Type Theory with typed equality judgements. In: LICS 2007, pp. 3–12. IEEE CS Press, Los Alamitos (2007) [ACD08] Abel, A., Coquand, T., Dybjer, P.: Verifying a semantic βη-conversion test for MartinL¨of type theory. In: Audebaud, P., Paulin-Mohring, C. (eds.) MPC 2008. LNCS, vol. 5133, pp. 29–56. Springer, Heidelberg (2008) [ACP09] Abel, A., Coquand, T., Pagano, M.: A modular type-checking algorithm for type theory with singleton types and proof irrelevance. In: Curien, P.-L. (ed.) TLCA 2009. LNCS, vol. 5608, pp. 5–19. Springer, Heidelberg (2009) [Ada06] Adams, R.: Pure type systems with judgemental equality. JFP 16(2), 219–246 (2006) [Bar09] Barras, B.: Sets in Coq, Coq in sets. In: The 1st Coq Workshop, Proceedings, Technische Universit¨at M¨unchen (2009) [BW97] Barras, B., Werner, B.: Coq in Coq. Available on the WWW (1997) [Car86] Cartmell, J.: Generalised algebraic theories and contextual categories. In: APAL, pp. 32–209 (1986)
Towards Normalization by Evaluation for the βη-Calculus of Constructions [CG90]
239
Coquand, T., Gallier, J.: A proof of strong normalization for the theory of constructions using a kripke-like interpretation. In: Proceedings of the First Workshop on Logical Frameworks (1990) [Cha09] Chapman, J.: Type Checking and Normalization. PhD thesis, School of Computer Science, University of Nottingham (2009) [Coq96] Coquand, T.: An algorithm for type-checking dependent types. In: M¨oller, B. (ed.) MPC 1995. LNCS, vol. 947, pp. 167–177. Springer, Heidelberg (1995) [Dan07] Danielsson, N.A.: A formalisation of a dependently typed language as an inductiverecursive family. In: Altenkirch, T., McBride, C. (eds.) TYPES 2006. LNCS, vol. 4502, pp. 93–109. Springer, Heidelberg (2007) [Geu94] Geuvers, H.: A short and flexible proof of strong normalization for the Calculus of Constructions. In: Smith, J., Dybjer, P., Nordstr¨om, B. (eds.) TYPES 1994. LNCS, vol. 996, pp. 14–38. Springer, Heidelberg (1995) [GL02] Gr´egoire, B., Leroy, X.: A compiled implementation of strong reduction. In: ICFP 2002. SIGPLAN Notices, vol. 37, pp. 235–246. ACM, New York (2002) [GN91] Geuvers, H., Nederhof, M.-J.: Modular proof of strong normalization for the calculus of constructions. JFP 1(2), 155–189 (1991) [Gog94] Goguen, H.: A Typed Operational Semantics for Type Theory. PhD thesis, University of Edinburgh. Available as LFCS Report ECS-LFCS-94-304 (1994) [Gon04] Gonthier, G.: A computer-checked proof of the four colour theorem. Technical report, Microsoft Research (2004), http://research.microsoft.com/˜gonthier/ [Gra09] Granstr¨om, J.: Reference and Computation in Intuitionistic Type Theory. PhD thesis, Mathematical Logic, Uppsala University (2009) [Hue89] Huet, G.: The constructive engine. In: Narasimhan, R. (ed.) 2nd European Symposium on Programming, Nancy, March 1988. Final version in anniversary volume Theoretical Computer Science in memory of Gift Siromoney. World Scientific Publishing, Singapore (1989) [INR08] INRIA. The Coq Proof Assistant Reference Manual. INRIA, version 8.2 edition (2008), http://coq.inria.fr/ [Ler06] Leroy, X.: Formal certification of a compiler back-end or: programming a compiler with a proof assistant. In: POPL 2006, pp. 42–54. ACM, New York (2006) [Luo89] Luo, Z.: ECC, an Extended Calculus of Constructions. In: LICS 1989, pp. 386–395. IEEE CS Press, Los Alamitos (1989) [Miq00] Miquel, A.: A model for impredicative type systems, universes, intersection types and subtyping. In: LICS, pp. 18–29 (2000) [MW03] Miquel, A., Werner, B.: The not so simple proof-irrelevant model of CC. In: Geuvers, H., Wiedijk, F. (eds.) TYPES 2002. LNCS, vol. 2646, pp. 240–258. Springer, Heidelberg (2003) [Pol94] Pollack, R.: Closure under alpha-conversion. In: Barendregt, H., Nipkow, T. (eds.) TYPES 1993. LNCS, vol. 806, pp. 313–332. Springer, Heidelberg (1994) [Pol06] Pollack, R.: The constructive engine. Talk presented at the TYPES Workshop CurryHoward Implementation Techniques - Connecting Humans And Theorem provers, CHIT-CHAT 2006, Radboud University, Nijmegen, The Netherlands (2006) [SG96] Stefanova, M., Geuvers, H.: A simple model construction for the calculus of constructions. In: Berardi, S., Coppo, M. (eds.) TYPES 1995. LNCS, vol. 1158, pp. 249–264. Springer, Heidelberg (1996) [Wer92] Werner, B.: A normalization proof for an impredicative type system with large eliminations over integers. In: TYPES 1992, pp. 341–357 (1992)
Defunctionalized Interpreters for Call-by-Need Evaluation Olivier Danvy1 , Kevin Millikin2 , Johan Munk3 , and Ian Zerny1 1
Department of Computer Science, Aarhus University Aabogade 34, 8200 Aarhus N, Denmark {danvy,zerny}@cs.au.dk 2 Google Aabogade 15, 8200 Aarhus N, Denmark
[email protected] 3 Arctic Lake Systems Aabogade 15, 8200 Aarhus N, Denmark
[email protected]
Abstract. Starting from the standard call-by-need reduction for the λ-calculus that is common to Ariola, Felleisen, Maraist, Odersky, and Wadler, we inter-derive a series of hygienic semantic artifacts: a reductionfree stateless abstract machine, a continuation-passing evaluation function, and what appears to be the first heapless natural semantics for call-by-need evaluation. Furthermore we observe that a data structure and a judgment in this natural semantics are in defunctionalized form. The refunctionalized counterpart of this evaluation function is an extended direct semantics in the sense of Cartwright and Felleisen. Overall, the semantic artifacts presented here are simpler than many other such artifacts that have been independently worked out, and which require ingenuity, skill, and independent soundness proofs on a caseby-case basis. They are also simpler to inter-derive because the interderivational tools (e.g., refocusing and defunctionalization) already exist.
1
Introduction
A famous functional programmer once was asked to give an overview talk. He began with “This talk is about lazy functional programming and call by need.” and paused. Then, quizzically looking at the audience, he quipped: “Are there any questions?” There were some, and so he continued: “Now listen very carefully, I shall say this only once.” This apocryphal story illustrates demand-driven computation and memoization of intermediate results, two key features that have elicited a fascinating variety of semantic specifications and implementation techniques over the years, ranging from purely syntactic treatments to mutable state, and featuring smallstep operational semantics [2, 25], a range of abstract machines [16, 18, 32], bigstep operational semantics [1, 23], as well as evaluation functions [19, 21]. In this article, we extract the computational content of the standard call-byneed reduction for the λ-calculus that is common to both Ariola and Felleisen [2] M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 240–256, 2010. c Springer-Verlag Berlin Heidelberg 2010
Defunctionalized Interpreters for Call-by-Need Evaluation
241
and Maraist, Odersky, and Wadler [25]. This computational content takes the forms of a one-step reduction function, an abstract machine, and a natural semantics that are mutually compatible and all abide by Barendregt’s variable convention [3, page 26]. Rather than handcrafting each of these semantic artifacts from scratch and then proving a series of soundness theorems, we successively inter-derive them from small steps to big steps using a series of fully correct transformations. To this end, we follow the programme outlined in the first author’s invited talk at ICFP 2008 [8]. To this programme we add one new refunctionalization step that is specific to call by need: 1. we modify one axiom in the standard call-by-need reduction to make the associated one-step reduction function preserve Barendregt’s variable convention; 2. iterating this hygienic one-step reduction function yields a reduction-based evaluation function, which we refocus to obtain a reduction-free evaluation function with the same built-in hygiene; this evaluation function takes the form of an abstract machine and is correct by construction; 3. we simplify this hygienic abstract machine by compressing its corridor transitions, and we refunctionalize this simplified hygienic abstract machine into a continuation-passing evaluation function, which we write back to direct style, obtaining a functional program that is correct by construction and that implements a heapless natural semantics with the same built-in hygiene; 4. in addition, we observe that a data structure and a judgment in this hygienic natural semantics are in defunctionalized form, and we present the corresponding higher-order evaluation function. The ML code of the entire derivation is available from the authors. Prerequisites: We assume the reader to know the formats of a reduction semantics, an abstract machine, and a natural semantics as can be gathered, e.g., in the first author’s lecture notes at AFP 2008 [9].
2
The Standard Call-by-Name Reduction for the λ-Calculus
The standard call-by-name reduction for the λlet -calculus is a simplification of Ariola et al.’s call-by-need formulation presented in Section 3. This call-by-name formulation reads as follows: Definition 1 (call-by-name λlet -calculus) Syntax:
Terms Values Answers Evaluation Contexts
T V A E
::= ::= ::= ::=
x | λx.T | T T | let x be T in T λx.T V | let x be T in A [ ] | E T | let x be T in E
242
O. Danvy et al.
Axioms (i.e., contraction rules):1 (I) (λx.T ) T1 → let x be T1 in T (N ) let x be T in E[x] → let x be T in E[T ] (C) (let x be T1 in A) T2 → let x be T1 in A T2 In words: Terms are pure λ-terms with let expressions. Values are λ-abstractions. Answers are let expressions nested around a value. Evaluation contexts are terms with a hole and are constructed according to the call-by-name reduction strategy. As for the axioms, Rule (I) introduces a let binding from an application; Rule (N ) hygienically substitutes a term for the occurrence of a let-bound variable arising in an evaluation context; and Rule (C) allows let bindings to commute with applications, hygienically, i.e., renaming what needs to be renamed so that no free variable is captured. The following reduction sequence illustrates the demand-driven aspect of call by name as well as the duplication of work it entails, noting one-step reduction with →name and annotating each reduction step with the corresponding axiom: (λz.z z) ((λy.y) (λx.x)) →name let z be (λy.y) (λx.x) in z z →name let z be (λy.y) (λx.x) in ((λy.y) (λx.x)) z →name let z be (λy.y) (λx.x) in (let y be λx.x in y) z →name let z be (λy.y) (λx.x) in let y be λx.x in y z →name let z be (λy.y) (λx.x) in let y be λx.x in (λx.x) z →name let z be (λy.y) (λx.x) in let y be λx.x in let x be z in x →name let z be (λy.y) (λx.x) in let y be λx.x in let x be z in z →name let z be (λy.y) (λx.x) in let y be λx.x in let x be z in (λy.y) (λx.x) →name ...
(I) (N ) (I) (C) (N ) (I) (N ) (N ) (I)
We have shaded the occurrences of the variables whose value is needed in the course of the reduction. Each of the two shaded occurrences of z triggers the reduction of (λy.y) (λx.x). The result of this demand-driven reduction is not memoized.
3
The Standard Call-by-Need Reduction for the λ-Calculus
Our starting point is the standard call-by-need reduction for the λlet -calculus that is common to Ariola, Felleisen, Maraist, Odersky, and Wadler’s articles [2, 25], renaming non-terminals for notational uniformity, and assuming that initial terms are closed: 1
The unusual notation “E[x]” in Rule (N ) stands for a term that uniquely decomposes into an evaluation context E and a variable x. Naturally, there may be more than one occurrence of x in the term E[x].
Defunctionalized Interpreters for Call-by-Need Evaluation
243
Definition 2 (call-by-need λlet -calculus) Syntax:
Terms Values Answers Evaluation Contexts
T V A E
::= ::= ::= ::=
x | λx.T | T T | let x be T in T λx.T V | let x be T in A [ ] | E T | let x be T in E | let x be E in E[x]
Axioms (i.e., contraction rules): (I) (λx.T ) T1 (V ) let x be V in E[x] (C) (let x be T1 in A) T2 (A) let x be let y be T1 in A in E[x]
→ → → →
let x be T1 in T let x be V in E[V ] let x be T1 in A T2 let y be T1 in let x be A in E[x]
In words: Terms are pure λ-terms with let expressions. Values are λ-abstractions. Answers are let expressions nested around a value. Evaluation contexts are terms with a hole and are constructed according to the call-by-need reduction strategy. As for the axioms, Rule (I) introduces a let binding from an application; Rule (V ) substitutes a value for the occurrence of a let-bound variable arising in an evaluation context; Rule (C) allows let bindings to commute with applications; and Rule (A) re-associates let bindings. Where call by name uses Axiom (N ), call by need uses (V ), ensuring that only values are duplicated. The reduction strategy thus also differs, so that the definiens of a needed variable is first reduced and this variable is henceforth declared to denote this reduct. The following reduction sequence illustrates the demand-driven aspect of call by need as well as the memoization of intermediate results it enables, noting one-step reduction with →need (and specifying it precisely in Section 4.7): (λz.z z) ((λy.y) (λx.x)) →need let z be (λy.y) (λx.x) in z z →need let z be (let y be λx.x in y) in z z →need let z be (let y be λx.x in λx.x) in z z →need let y be λx.x in let z be λx.x in z z →need let y be λx.x in let z be λx.x in (λx.x) z →need let y be λx.x in let z be λx.x in let x be z in x →need let y be λx.x in let z be λx.x in let x be λx.x in x →need let y be λx.x in let z be λx.x in let x be λx.x in λx.x
(I) (I) (V ) (A) (V ) (I) (V ) (V )
As in Section 2, we have annotated each reduction step with the name of the corresponding axiom and we have shaded the occurrences of the variables whose values are needed in the course of the reduction. Only the first shaded occurrence of z triggers the reduction of (λy.y) (λx.x). The result of this demand-driven reduction is memoized in the let expression that declares z and thus the two other shaded occurrences of z trigger the (V ) rule. This let expression is needed as long as z occurs free in its body; thereafter it can be garbage-collected [5].
244
4
O. Danvy et al.
Some Exegesis
Definition 2 packs a lot of information. Let us methodically spell it out: – The axioms are a mouthful, and so in Section 4.1, we identify their underlying structure by stating a grammar for potential redexes. – In reduction semantics, evaluation is defined as iterated one-step reduction. However, one-step reduction assumes Barendregt’s variable convention, i.e., that all bound variables are distinct, but not all the axioms preserve this convention: naive iteration is thus unsound. Rather than subsequently ensuring hygiene as in Garcia et al.’s construction of a lazy abstract machine [18], we restate one axiom in Section 4.2 to make naive iteration hygienic upfront. – The evaluation contexts are unusual in that they involve terms that are uniquely decomposable into a delimited evaluation context and an identifier. In Section 4.3, we restate their definition to clearly distinguish between ordinary evaluation contexts and delimited evaluation contexts. – The one-step reduction of a reduction semantics is implicitly structured in three parts: given a non-answer term, (1, decomposition): locate the next potential redex according to the reduction strategy; (2, contraction): if the potential redex is an actual one, i.e., if the non-answer term is not stuck, contract this actual redex as specified by the axioms; and (3, recomposition): fill the surrounding context with the contractum to construct the next term in the reduction sequence. Based on Sections 4.1, 4.2, and 4.3, we specify decomposition, contraction, and recomposition in Sections 4.4, 4.5, and 4.6. We then formalize one-step reduction in Section 4.7 and evaluation as iterated one-step reduction in Section 4.8. 4.1
Potential Redexes
To bring out the underlying structure of the axioms, let us state a grammar for potential redexes: Pot Redexes R ::= A T | let x be A in E[x] where E[x] stands for a non-answer term. The two forms of answers – value and let expression – give rise to an axiom for each production in the grammar of potential redexes: (I) arises from the application of a value to a term; (C) arises from the application of a let expression to a term; and likewise, (V ) and (A) arise from the binding of an answer to a variable whose value is needed. In the present case of the pure λlet -calculus, all potential redexes are actual ones. 4.2
Barendregt’s Variable Convention
The definition of evaluation as iterated one-step reduction assumes Barendregt’s variable convention, i.e., that all bound variables are distinct. Indeed the rules
Defunctionalized Interpreters for Call-by-Need Evaluation
245
(V ), (C) and (A) assume the variable convention when they move a term in the scope of a binding. A reduction step involving (V ), however, yields a term where the variable convention does not hold, since V is duplicated and it may contain λ-abstractions and therefore bound variables. There are many ways to ensure hygiene, if not the variable convention, at all times. We choose to allow λ-bound (not let-bound) variables to overlap, and since no reduction can take place inside a λ-abstraction prior to its application, in Rule (I), we lazily rename λ-bound variables as the need arises, using an auxiliary function to rename the formal parameter of a λ-abstraction with a globally fresh name: (I) (λx.T ) T1 → let x be T1 in T [x /x] where x is fresh This modification preserves Barendregt’s variable convention for let-bound variables. We thus assume that any initial term satisfies the convention for let-bound variables (otherwise we appropriately rename it). Other alternatives exist for ensuring hygiene. We have explored several of them, and in our experience they lead to semantic artifacts that are about as simple and understandable as the ones presented here. The alternative we chose here, i.e., the modification of Rule (I), corresponds to, and is derived into the same renaming side condition as in Maraist, Odersky, and Wadler’s natural semantics [25, Figure 11]. 4.3
The Evaluation Contexts
The grammar of contexts for call by need, given in Definition 2, is unusual compared to the one for call by name given in Definition 1. Call-by-need evaluation contexts have an additional constructor involving the term “E[x]” for which there exists an identifier x in the eye of a delimited context E. Spelling out the decomposition function (see Section 4.5 and Figure 3) shows that these delimited contexts are constructed outside in whereas all the others are constructed inside out. Let us make it explicit which are which by adopting an isomorphic representation of contexts as a list of frames: Context Frames F ::= T | let x be in Eoi [x] | let x be T in Outside-in Contexts Eoi ::= ε | F ◦ Eoi Inside-out Contexts Eio ::= ε | F ◦ Eio Here ε is the empty list, ◦ is the list constructor, and is the hole in a context frame. For example, the context E = ([ ] T1 ) T2 is isomorphic to Eio = ( T1 ) ◦ ( T2 ) ◦ ε which is equivalent to Eoi = ( T2 ) ◦ ( T1 ) ◦ ε in the sense that, as defined in Section 4.4, Eio , T io ⇑rec T and Eoi , T oi ⇑rec T for all T . NB. As pointed out in Footnote 1 page 242, in this BNF of context frames, the notation “Eoi [x]” is meant to represent a term that uniquely decomposes into an outside-in evaluation context Eoi and a variable x. From Section 5.1 onwards, we take notational advantage of this paired representation to short-cut the subsequent decomposition of this term into Eoi and x towards the potential redex contracted in Rule (V ) in Section 4.6.
246
O. Danvy et al.
Eoi , T oi ⇑rec T1 Eoi , xoi ⇑rec T2 (let x be in Eoi [x]) ◦ Eoi , T oi ⇑rec let x be T1 in T2
ε, T oi ⇑rec T
Eoi , T oi ⇑rec T0 ( T1 ) ◦ Eoi , T oi ⇑rec T0 T1
Eoi , T oi ⇑rec T2 (let x be T1 in ) ◦ Eoi , T oi ⇑rec let x be T1 in T2
Fig. 1. Recomposition of outside-in contexts
ε, T io ⇑rec T
Eoi , xoi ⇑rec T Eio , let x be T1 in T io ⇑rec T2 (let x be in Eoi [x]) ◦ Eio , T1 io ⇑rec T2
Eio , T0 T1 io ⇑rec T2 ( T1 ) ◦ Eio , T0 io ⇑rec T2
Eio , let x be T1 in T io ⇑rec T2 (let x be T1 in ) ◦ Eio , T io ⇑rec T2
Fig. 2. Recomposition of inside-out contexts
4.4
Recomposition
Outside-in contexts and inside-out contexts are recomposed (or again are ‘plugged’ or ‘filled’) as follows: Definition 3 (recomposition of outside-in contexts). An outside-in context Eoi is recomposed around a term T into a term T whenever Eoi , T oi ⇑rec T holds. (See Figure 1.) Definition 4 (recomposition of inside-out contexts). An inside-out context Eio is recomposed around a term T into a term T whenever Eio , T io ⇑rec T holds. (See Figure 2.) The alert reader will have noticed that each of these recomposition functions, together with the data type of contexts, is in defunctionalized form [12,13,29,30], indicating a large and friendly degree of freedom for implementing a one-step reduction function in a functional programming language. 4.5
Decomposition
Decomposing a non-answer term into a potential redex and its evaluation context is at the heart of a reduction semantics, but outside of the authors’ publications, it seems never to be spelled out. Let us do so. There are many ways to specify decomposition. In our experience, the simplest one is the abstract machine displayed in Figure 3. This machine starts in the configuration T, εterm , for a given term T . It halts in an answer state if the given term contains no potential redex, and in a decomposition state
Defunctionalized Interpreters for Call-by-Need Evaluation
x, λx.T , T0 T1 , let x be T1 in T ,
Eio term Eio term Eio term Eio term
↓dec ↓dec ↓dec ↓dec
Eio , (ε, x)reroot Eio , λx.T context T0 , ( T1 ) ◦ Eio term T, (let x be T1 in ) ◦ Eio term
ε, ( T1 ) ◦ Eio , (let x be in Eoi [x]) ◦ Eio , (let x be T1 in ) ◦ Eio ,
Acontext Acontext Acontext Acontext
↓dec ↓dec ↓dec ↓dec
Aanswer A T1 , Eio decomposition let x be A in Eoi [x], Eio decomposition Eio , let x be T1 in Acontext
247
(let x be T1 in ) ◦ Eio , (Eoi , x)reroot ↓dec T1 , (let x be in Eoi [x]) ◦ Eio term F ◦ Eio , (Eoi , x)reroot ↓dec Eio , (F ◦ Eoi , x)reroot where F = let x be T in
Fig. 3. Decomposition of an answer term into itself and of a non-answer term into a potential redex and its context
R, Eio decomposition otherwise, where R denotes the first potential redex in T and Eio its evaluation context according to the reduction strategy specified by the grammar of evaluation contexts. The term and context transitions are traditional: one dispatches on a term and the other on the top context frame. The reroot transitions locate the letbinder for a variable while maintaining the outside-in context from the binder to its occurrence, zipper-style [20].1 In effect, the transitions reverse the prefix of an inside-out context into an outside-in context. 4.6
The Axioms
In accordance with the new BNF of contexts, the hygienic axioms of Definition 2 are restated as follows: (I) (λx.T ) T1 (V ) let x be V in Eoi [x] (C) (let x be T1 in A) T2 (A) let x be let y be T1 in A in Eoi [x] 4.7
→ → → →
let x be T1 in T [x /x] let x be V in T let x be T1 in A T2 let y be T1 in let x be A in Eoi [x]
where x is fresh where Eoi , V oi ⇑rec T
One-Step Reduction
The partial function of performing one contraction in a non-answer term is defined as (1) locating a redex and its context through a number of decomposition 1
Decomposition could be stuck if the initial term contained free variables, but we assume it to be closed.
248
O. Danvy et al.
steps, (2) contracting this redex, and (3) recomposing the resulting contractum into the context: Definition 5 (one-step reduction). For any T , ⎧ ∗ ⎪ ⎨ T, εterm ↓dec R, Eio decomposition T →need T if (R, T ) ∈ (I) ∪ (V ) ∪ (C) ∪ (A) ⎪ ⎩ Eio , T io ⇑rec T One-step reduction is a partial function because the given term may already be an answer. 4.8
Reduction-Based Evaluation
Reduction-based evaluation is defined as the iteration of the one-step reduction function. It thus proceeds by enumerating the reduction sequence of any given term: Definition 6 (reduction-based evaluation). For any T , T → ∗need A Evaluation is a partial function because it may diverge. 4.9
Conclusion and Perspectives
As illustrated here, there is substantially more than meets the eye in a reduction semantics. In addition, extensional properties such as unique decomposition, standardization, and hygiene do not only ensure the existence of a deterministic evaluator extensionally, but it is our thesis that they also provide precious intensional guidelines. Indeed, after exegetically spelling out what does not readily meet the eye, things become compellingly simple: refocusing the reduction-based evaluation function immediately gives a reduction-free small-step abstract machine (Section 5.1); fusing its iteration function with its transition functions yields a big-step abstract machine (Section 5.2); compressing the corridor transitions of this abstract machine improves the efficiency of its execution (Section 5.3); refunctionalizing this improved abstract machine with respect to the contexts gives a reduction-free evaluation function in continuation-passing style (Section 5.4); and mapping this evaluation function back to direct style gives a functional implementation of a natural semantics (Section 5.5). All of these semantic artifacts are correct by construction, and their operational behaviors rigorously mirror each other. And should one be tempted to fine-tune one of these semantic artifacts, one is in position to adjust the others to keep their operational behaviors in line, or to understand why this alignment is not possible and where coherence got lost in the fine-tuning [9].
5
Derivation: From Small-Step Reduction Semantics to Big-Step Evaluation Function
This section implements the programme outlined in Section 4.9.
Defunctionalized Interpreters for Call-by-Need Evaluation
5.1
249
Refocusing: From Reduction Semantics to Small-Step Abstract Machine
By recomposing and then immediately decomposing, a reduction-based evaluator takes a detour from a redex site, up to the top of the term, and back down again to the next redex site. Many of the steps that make up this detour can be eliminated by refocusing [14]. Refocusing the reduction-based evaluation function of a reduction semantics yields a reduction-free evaluation function in the form of an abstract machine that directly navigates in a term from redex site to redex site without any detour via the top of the term. Refocusing replaces successive recompositions and decompositions by a call to a ‘refocus’ function that maps a contractum and its associated (inside-out) evaluation context into a value or a decomposition consisting of the next potential redex and associated evaluation context. Surprisingly, optimal refocusing consists of simply continuing with decomposition from the contractum and its associated evaluation context. This is another reason why we place such store in the decomposition function of a reduction semantics. The hygienic reduction semantics of Section 4 satisfies the requirements for refocusing [14] and so its reduction-based evaluation function can be mechanically replaced by a reduction-free evaluation function that short-cuts the successive terms in the reduction sequence. 5.2
Lightweight Fusion: From Small-Step Abstract Machine to Big-Step Abstract Machine
The refocused specification of a reduction semantics implements a small-step abstract machine. Small-step abstract machines are characterized by a singlestep state-transition function which maps a machine configuration to the next and is iterated toward a final state if any. In contrast, big-step abstract machines are characterized by a collection of mutually tail-recursive transition functions mapping a configuration to a final state, if any. Fusing the composition of a small-step abstract machine’s iteration function with its transition functions yields a big-step abstract machine [11]. To this end, we use Ohori and Sasano’s lightweight fusion by fixed-point promotion [27]. The difference between the two styles of abstract machines is not typically apparent in the abstract-machine specifications found in programming-language semantics. A machine specification is normally presented as a small-step abstract machine given by reading the transition arrow as the definition of a single-step transition function to be iterated and with the configuration labels as passive components of the configurations. However, the same specification can equally be seen as a big-step abstract machine if the transition labels are interpreted as tail recursive functions, with the transition arrow connecting left- and right-hand sides of their definitions. Ohori and Sasano’s correctness-preserving fusion of the small-step machine’s transition and iteration functions justifies this dual view. The difference between the two styles is relevant when we consider the transformation of an abstract machine semantics into an evaluator implementing a
250
O. Danvy et al.
x, λx.T , T0 T1 , let x be T1 in T ,
Eio term Eio term Eio term Eio term
→run →run →run →run
Eio , (ε, x)reroot Eio , λx.T context T0 , ( T1 ) ◦ Eio term T, (let x be T1 in ) ◦ Eio term
ε, Acontext →run Aanswer ( T1 ) ◦ Eio , λx.T context →run T [x /x], (let x be T1 in ) ◦ Eio term where x is fresh ( T2 ) ◦ Eio , let x be T1 in Acontext →run ( T2 ) ◦ (let x be T1 in ) ◦ Eio , Acontext (let x be in Eoi [x]) ◦ Eio , V context →run T, (let x be V in ) ◦ Eio term where Eoi , V«oi„⇑rec T „ „ „ « « « let x be let y be T1 let x be let y be T1 ◦ Eio , context →run ◦ ◦ Eio , Acontext in Eoi [x] in Eoi [x] in A in (let x be T1 in ) ◦ Eio , Acontext →run Eio , let x be T1 in Acontext (let x be T1 in ) ◦ Eio , (Eoi , x)reroot →run T1 , (let x be in Eoi [x]) ◦ Eio term F ◦ Eio , (Eoi , x)reroot →run Eio , (F ◦ Eoi , x)reroot where F = let x be T in
Fig. 4. Abstract machine after transition compression
natural semantics. Such evaluators are big-step ones, and it is for this reason that we transform small-step machines into big-step machines. 5.3
Transition Compression: From Big-Step Abstract Machine to Big-Step Abstract Machine
Some of the transitions of the abstract machine of Section 5.2 are to intermediate configurations such that the next transition is statically known. These so-called “corridor transitions” can be hereditarily compressed so that the original configuration transitions directly to the final one, skipping all intermediate ones. The resulting abstract machine is displayed in Figure 4. Proposition 1 (full correctness). For any T , T → ∗need A ⇔ T, εterm →∗run Aanswer . 5.4
Refunctionalization: From Abstract Machine to Continuation-Passing Interpreter
Reynolds introduced defunctionalization [13, 29] to derive first-order evaluators from higher-order ones. Defunctionalization turns a function type into a sum type, and function application into the application of an apply function dispatching on the sum type. Its left inverse, refunctionalization [12], can transform first-order abstract machines into higher-order evaluators. It specifically works on programs that are in defunctionalized form, i.e., in the image of Reynolds’s defunctionalization.
Defunctionalized Interpreters for Call-by-Need Evaluation
251
The big-step abstract machine of Section 5.3 is not in defunctionalized form with respect to the inside-out reduction contexts. Indeed these contexts are consumed by the two transition functions corresponding to Eio , Acontext and Eio , (Eoi , x)reroot rather than by the single apply function demanded for refunctionalization. This mismatch can be fixed by introducing a sum type discriminating between the (non-context) arguments to the two transition functions and combining them into a single transition function [12]. The left summand (tagged “ans”) contains an answer, and the right summand (tagged “ide”) contains a pair of an identifier whose value is needed and an incrementally-constructed outside-in context used to get back to the place in the term where the value was needed. Three of the context constructors occur on the right-hand sides of their own apply function clauses. When refunctionalized, these correspond to recursive functions and therefore show up as named functions. The refunctionalized abstract machine is an interpreter for lazy evaluation in continuation-passing style, with the functional representation of the inside-out contexts serving as continuations. 5.5
Back to Direct Style: From Continuation-Passing Interpreter to Natural Semantics
It is a simple matter to transform the continuation-passing interpreter described in Section 5.4 into direct style [7]. The continuations do not represent any control effect other than non-tail calls, so the resulting direct-style interpreter does not require first-class control operators [10]. This interpreter implements a natural semantics (i.e., a big-step operational semantics) for lazy evaluation. This semantics is displayed in Figure 5. Proposition 2 (full correctness). For any T , T, εterm →∗run Aanswer ⇔ T ⇓eval ans(A). 5.6
Refunctionalization: From Natural Semantics to Higher-Order Evaluation Function
The natural semantics implementation of Section 5.5 is already in defunctionalized form with respect to the first-order outside-in contexts. Indeed, as already mentioned in Section 4.4, the recomposition function of Definition 3 and Figure 1 is the corresponding apply function. An outside-in context acts as an accumulator recording the path from a variable whose value is needed to its binding site. The recomposition function turns this accumulator inside-out again when the variable’s value is found. The refunctionalized outside-in contexts are functional representations of these accumulators. The resulting refunctionalized evaluation function is displayed in Figure 6. Notationally, higher-order functions are introduced with λ and eliminated with @, which is infix.
252
O. Danvy et al.
x ⇓eval ide(ε, x)
λx.T ⇓eval ans(λx.T )
T ⇓eval r (x, T1 , r) ⇓bind r let x be T1 in T ⇓eval r
r T1 ⇓apply r T0 ⇓eval r T0 T1 ⇓eval r
(ans(A)) T2 ⇓apply r (x, T1 , r) ⇓bind r (ans(let x be T1 in A)) T2 ⇓apply r
T [x /x] ⇓eval r (x , T1 , r) ⇓bind r where x is fresh (ans(λx.T )) T1 ⇓apply r (ide(Eoi , x)) T1 ⇓apply ide(( T1 ) ◦ Eoi , x) (x, T1 , ans(A)) ⇓bind ans(let x be T1 in A)
(x, Eoi , r) ⇓force r T1 ⇓eval r (x, T1 , ide(Eoi , x)) ⇓bind r
(x, T1 , ide(Eoi , y)) ⇓bind ide((let x be T1 in ) ◦ Eoi , y)
where x =y
T ⇓eval r (x, V, r) ⇓bind r Eoi , V oi ⇑rec T (x, Eoi , ans(V )) ⇓force r (x, Eoi , ans(A)) ⇓force r (y, T1 , r) ⇓bind r (x, Eoi , ans(let y be T1 in A)) ⇓force r (x, Eoi , ide(Eoi , y)) ⇓force ide((let x be in Eoi [x]) ◦ Eoi , y)
Fig. 5. Natural semantics
eval(x) eval(λx.T ) eval(T0 T1 ) eval(let x be T1 in T )
= ide(λr.r, x) = ans(λx.T ) = apply(eval(T0 ), T1 ) = bind(x, T1 , eval(T ))
where x is fresh apply(ans(λx.T ), T1 ) = bind(x , T1 , eval(T [x /x])) apply(ans(let x be T1 in A), T2 ) = bind(x, T1 , apply(ans(A), T2 )) apply(ide(h, x), T1 ) = ide(λr.apply(h @ r, T1 ), x) bind(x, T1 , ans(A)) = ans(let x be T1 in A) bind(x, T1 , ide(h, x)) = force(x, h, eval(T1 )) bind(x, T1 , ide(h, y)) = ide(λr.bind(x, T1 , h @ r), y)
where x =y
force(x, h, ans(V )) = bind(x, V, h @ (ans(V ))) force(x, h, ans(let y be T1 in A)) = bind(y, T1 , force(x, h, ans(A))) force(x, h, ide(h , y)) = ide(λr.force(x, h, h @ r), y)
Fig. 6. Higher-order evaluation function
Defunctionalized Interpreters for Call-by-Need Evaluation
253
Proposition 3 (full correctness). For any T , T ⇓eval ans(A) ⇔ eval(T ) = ans(A). This higher-order evaluation function exhibits a computational pattern that we find striking because it also occurs in Cartwright and Felleisen’s work on extensible denotational language specifications [6]: each valuation function yields either a (left-injected with “ans”) value or a (right-injected with “ide”) higherorder function. For each call, this higher-order function may yield another rightinjected higher-order function that, when applied, restores this current call. This computational pattern is typical of delimited control: the left inject stands for an expected result, while the right inject acts as an exceptional return that incrementally captures the current continuation. At any rate, this pattern was subsequently re-invented by F¨ unfrocken to implement process migration [17,31,33], and then put to use to implement first-class continuations [24,28]. In the present case, this pattern embodies two distinct computational aspects—one intensional and the other extensional: How: The computational pattern is one of delimited control, from the point of use of a let-bound identifier to its point of declaration. What: The computational effect is one of a write-once state since once the delimited context is captured, it is restored with the value of the let-bound identifier. These two aspects were instrumental in Cartwright and Felleisen’s design of extensible denotational semantics for (undelimited) Control Scheme and for State Scheme [6]. For call by need, this control aspect was recently re-discovered by Garcia, Lumsdaine and Sabry [18], and this store aspect was originally envisioned by Landin [22]. These observations put us in position to write the evaluation function in direct style, either with delimited control operators (one control delimiter for each let declaration, and one control abstraction for each occurrence of a let-declared identifier whose value is needed), or with a state monad. We elaborate this point further in the extended version of this article.
6
Conclusion Semantics should be call by need. – Rod Burstall
Over the years, the two key features of lazy evaluation – demand-driven computation and memoization of intermediate results – have elicited a fascinating variety of semantic artifacts, each with its own originality and elegance. It is our overarching thesis that spelling out the methodical search for the next potential redex that is implicit in a reduction semantics paves the way towards other semantic artifacts that not only are uniformly inter-derivable and sound by construction but also correspond to what one crafts by hand. Elsewhere, we have already shown that refocusing, etc. do not merely apply to purely syntactic theories such as, e.g., Felleisen and Hieb’s syntactic theories of sequential control
254
O. Danvy et al.
and state [15,26]: the methodology also applies to call by need with a global heap of memo-thunks [1, 4], and to graph reduction, connecting term graph rewriting systems `a la Barendregt et al. and graph reduction machines a` la Turner [34]. Here, we have shown that the methodology also applies to Ariola et al.’s purely syntactic account of call-by-need. Acknowledgments. Thanks are due to the anonymous reviewers. We are also grateful to Zena Ariola, Kenichi Asai, Ronald Garcia, Oleg Kiselyov, Kristoffer Rose and Chung-chieh Shan for discussions and comments.
References 1. Ager, M.S., Danvy, O., Midtgaard, J.: A functional correspondence between call-byneed evaluators and lazy abstract machines. Information Processing Letters 90(5), 223–232 (2004) 2. Ariola, Z.M., Felleisen, M.: The call-by-need lambda calculus. Journal of Functional Programming 7(3), 265–301 (1997) 3. Barendregt, H.: The Lambda Calculus: Its Syntax and Semantics. In: Studies in Logic and the Foundation of Mathematics, revised edn., vol. 103, North-Holland, Amsterdam (1984) 4. Biernacka, M., Danvy, O.: A syntactic correspondence between context-sensitive calculi and abstract machines. Theoretical Computer Science 375(1-3), 76–108 (2007) 5. Bloo, R., Rose, K.H.: Preservation of strong normalisation in named lambda calculi with explicit substitution and garbage collection. In: CSN 1995: Computer Science in the Netherlands, pp. 62–72 (1995) 6. Cartwright, R., Felleisen, M.: Extensible denotational language specifications. In: Hagiya, M., Mitchell, J.C. (eds.) TACS 1994. LNCS, vol. 789, pp. 244–272. Springer, Heidelberg (1994) 7. Danvy, O.: Back to direct style. Science of Computer Programming 22(3), 183–195 (1994) 8. Danvy, O.: Defunctionalized interpreters for programming languages. In: Thiemann, P. (ed.) Proceedings of the 2008 ACM SIGPLAN International Conference on Functional Programming (ICFP 2008), SIGPLAN Notices, Victoria, British Columbia, vol. 43(9), pp. 131–142. ACM Press, New York (2008) (invited talk) 9. Danvy, O.: From reduction-based to reduction-free normalization. In: Koopman, P., Plasmeijer, R., Swierstra, D. (eds.) AFP 2008. LNCS, vol. 5832, pp. 66–164. Springer, Heidelberg (2009) 10. Danvy, O., Lawall, J.L.: Back to direct style II: First-class continuations. In: Clinger, W. (ed.) Proceedings of the 1992 ACM Conference on Lisp and Functional Programming, LISP Pointers, San Francisco, California, vol. V(1), pp. 299–310. ACM Press, New York (1992) 11. Danvy, O., Millikin, K.: On the equivalence between small-step and big-step abstract machines: a simple application of lightweight fusion. Information Processing Letters 106(3), 100–109 (2008) 12. Danvy, O., Millikin, K.: Refunctionalization at work. Science of Computer Programming 74(8), 534–549 (2009)
Defunctionalized Interpreters for Call-by-Need Evaluation
255
13. Danvy, O., Nielsen, L.R.: Defunctionalization at work. In: Søndergaard, H. (ed.) Proceedings of the Third International ACM SIGPLAN Conference on Principles and Practice of Declarative Programming (PPDP 2001), Firenze, Italy, pp. 162– 174. ACM Press, New York (2001) 14. Danvy, O., Nielsen, L.R.: Refocusing in reduction semantics. Research Report BRICS RS-04-26, Department of Computer Science, Aarhus University, Aarhus, Denmark (November 2004); A preliminary version appeared in the informal proceedings of the Second International Workshop on Rule-Based Programming (RULE 2001). Electronic Notes in Theoretical Computer Science, vol. 59.4 (2001) 15. Felleisen, M., Hieb, R.: The revised report on the syntactic theories of sequential control and state. Theoretical Computer Science 103(2), 235–271 (1992) 16. Friedman, D.P., Ghuloum, A., Siek, J.G., Winebarger, L.: Improving the lazy Krivine machine. Higher-Order and Symbolic Computation 20(3), 271–293 (2007) 17. F¨ unfrocken, S.: Transparent migration of Java-based mobile agents. In: Rothermel, K., Hohl, F. (eds.) MA 1998. LNCS, vol. 1477, pp. 26–37. Springer, Heidelberg (1998) 18. Garcia, R., Lumsdaine, A., Sabry, A.: Lazy evaluation and delimited control. In: Pierce, B.C. (ed.) Proceedings of the Thirty-Sixth Annual ACM Symposium on Principles of Programming Languages, SIGPLAN Notices, Savannah, GA, vol. 44(1), pp. 153–164. ACM Press, New York (2009) 19. Henderson, P., Morris Jr., J.H.: A lazy evaluator. In: Graham, S.L. (ed.) Proceedings of the Third Annual ACM Symposium on Principles of Programming Languages, pp. 95–103. ACM Press, New York (1976) 20. Huet, G.: The zipper. Journal of Functional Programming 7(5), 549–554 (1997) 21. Josephs, M.B.: The semantics of lazy functional languages. Theoretical Computer Science 68, 105–111 (1989) 22. Landin, P.J.: The mechanical evaluation of expressions. The Computer Journal 6(4), 308–320 (1964) 23. Launchbury, J.: A natural semantics for lazy evaluation. In: Graham, S.L. (ed.) Proceedings of the Twentieth Annual ACM Symposium on Principles of Programming Languages, Charleston, South Carolina, pp. 144–154. ACM Press, New York (1993) 24. Loitsch, F.: Scheme to JavaScript Compilation. PhD thesis, Universit´e de Nice, Nice, France (March 2009) 25. Maraist, J., Odersky, M., Wadler, P.: The call-by-need lambda calculus. Journal of Functional Programming 8(3), 275–317 (1998) 26. Munk, J.: A study of syntactic and semantic artifacts and its application to lambda definability, strong normalization, and weak normalization in the presence of state. Master’s thesis, Department of Computer Science, Aarhus University, Aarhus, Denmark. BRICS research report RS-08-3 (May 2007) 27. Ohori, A., Sasano, I.: Lightweight fusion by fixed point promotion. In: Felleisen, M. (ed.) Proceedings of the Thirty-Fourth Annual ACM Symposium on Principles of Programming Languages, SIGPLAN Notices, Nice, France, vol. 42(1), pp. 143–154. ACM Press, New York (2007) 28. Pettyjohn, G., Clements, J., Marshall, J., Krishnamurthi, S., Felleisen, M.: Continuations from generalized stack inspection. In: Pierce, B. (ed.) Proceedings of the 2005 ACM SIGPLAN International Conference on Functional Programming (ICFP 2005), SIGPLAN Notices, Tallinn, Estonia, vol. 40(9), pp. 216–227. ACM Press, New York (2005)
256
O. Danvy et al.
29. Reynolds, J.C.: Definitional interpreters for higher-order programming languages. In: Proceedings of 25th ACM National Conference, Boston, Massachusetts, pp. 717–740 (1972); Reprinted in Higher-Order and Symbolic Computation 11(4), 363– 397 (1998), with a foreword [30] 30. Reynolds, J.C.: Definitional interpreters revisited. Higher-Order and Symbolic Computation 11(4), 355–361 (1998) 31. Sekiguchi, T., Masuhara, H., Yonezawa, A.: A simple extension of Java language for controllable transparent migration and its portable implementation. In: Ciancarini, P., Wolf, A.L. (eds.) COORDINATION 1999. LNCS, vol. 1594, pp. 211–226. Springer, Heidelberg (1999) 32. Sestoft, P.: Deriving a lazy abstract machine. Journal of Functional Programming 7(3), 231–264 (1997) 33. Tao, W.: A portable mechanism for thread persistence and migration. PhD thesis, University of Utah, Salt Lake City, Utah (2001) 34. Zerny, I.: On graph rewriting, reduction and evaluation. In: Horv´ ath, Z., Zs´ ok, V., Achten, P., Koopman, P. (eds.) Trends in Functional Programming, Kom´ arno, Slovakia, June 2009, vol. 10. Intellect Books (2009) (to appear)
Complexity Analysis by Graph Rewriting Martin Avanzini and Georg Moser Institute of Computer Science, University of Innsbruck, Austria {martin.avanzini,georg.moser}@uibk.ac.at
Abstract. Recently, many techniques have been introduced that allow the (automated) classification of the runtime complexity of term rewrite systems (TRSs for short). In this paper we show that polynomial (innermost) runtime complexity of TRSs induces polytime computability of the functions defined. In this way we show a tight correspondence between the number of steps performed in a given rewrite system and the computational complexity of an implementation of rewriting. The result uses graph rewriting as a first step towards the implementation of term rewriting. In particular, we prove the adequacy of (innermost) graph rewriting for (innermost) term rewriting.
1
Introduction
We study techniques to analyse the complexity of programs automatically. Instead of studying programs of a particular programming language directly, we focus on the complexity analysis of term rewrite systems instead. The reason is that term rewriting is a very simple formalism underlying many programming languages. Recently, many techniques to automatically assess the runtime complexity of term rewrite systems (TRSs for short) have been introduced. For example in [1,3] we introduced the polynomial path order POP∗ and extensions of it. POP∗ is a restriction of the multiset path order [13] and whenever compatibility of a TRS R with POP∗ can be shown then the runtime complexity of R is polynomially bounded. Here the runtime complexity of a TRS measures the maximal number of rewrite steps as a function in the size of the initial term. We have successfully implemented this technique.1 Thus we can automatically verify for a given TRS R that it admits at most polynomial runtime complexity. This opens the way to automatically verify for a given (functional) program P that its runtime complexity is polynomial (in the input size). The only restrictions in the applicability of the result are that (i) the program P is transformable into a term rewrite system R and (ii) a feasible (i.e., polynomial) runtime complexity with respect to R gives rise to a feasible runtime complexity of P. In short the transformation has to be non-termination and complexity preserving. 1
This research is supported by FWF (Austrian Science Fund) projects P20133. The here mentioned polynomial path orders are one complexity technique implemented in the Tyrolean Complexity Tool, a complexity tool to analyse the runtime complexity of TRSs. The program is open-source and freely available at http://cl-informatik.uibk.ac.at/software/tct/.
M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 257–271, 2010. © Springer-Verlag Berlin Heidelberg 2010
258
M. Avanzini and G. Moser
As an example of the technique consider the following rendering of insert sort as TRS R1 given in [11]. Observe that for a list xs build from constructors cs and nil, sort(xs, len(xs)) returns xs sorted. if(true, x, y) → x if(false, x, y) → y len(nil) → 0 len(cs(x, xs)) → s(len(xs)) x < 0 → false s(x) < s(y) → x < y
0 < s(x) → true ins(0, x, ys) → cs(x, ys) ins(s(n), x, cs(y, ys)) → if(x < y, cs(x, cs(y, ys)), cs(y, ins(n, x, ys))) sort(nil, 0) → nil sort(cs(x, xs), s(n)) → ins(n, x, sort(xs, n))
It is not difficult to see that the innermost runtime complexity of the TRS R1 is polynomially bounded. Moreover this fact can be verified automatically by showing that R1 ⊆ >pop∗ for a suitable instance >pop∗ of POP∗ , cf. [1]. Once we have established a bound on the (innermost) runtime complexity of a TRS, it is natural to direct our attention to the computational complexity of the functions defined by the TRS. In particular with respect to TRS R1 we also want to verify (automatically if possible) that insertion sort is polytime computable. Due to the restrictive definition of >pop∗ this is not difficult as the signature of R1 is simple. (See [2] for a proof.) Here a simple signature [11] essentially means that the size of any constructor term depends polynomially on its depth. Unfortunately the restriction to a simple signature is rather restrictive. Consider the following TRS R2 . (This is Example 8 in [9].) D(c) → 0 D(t) → 1
D(x + y) → D(x) + D(y) D(x − y) → D(x) − D(y)
D(x × y) → y × D(x) + x × D(y) Employing runtime complexity techniques developed in [10], we can automatically verify that the runtime complexity of R2 is polynomially bounded. However the signature of R2 is not simple and thus we cannot directly conclude polytime computability of the function computed. The main obstacle here is that due to the last rule, a single step may copy arbitrary large subterms. In this paper we show that polynomial runtime complexity of TRSs induces polytime computability of the functions defined. The only restriction is that we consider an eager evaluation strategy, i.e., we base our study on an innermost rewriting strategy. We show the precise correspondence between the number of steps performed in a given rewrite system and the computational complexity of an implementation of rewriting. In order to overcome the problem of copying we use graph rewriting—here copying is replaced by sharing—as a first step towards the implementation of term rewriting. We re-prove the adequacy of innermost graph rewriting for innermost term rewriting, compare for example [12,13]. A new proof becomes necessary as we need to control the resources needed in the simulation. This
Complexity Analysis by Graph Rewriting
259
pedantry is then used to establish the tight correspondence between the complexity of a given TRS R and the intrinsic computational complexity of the functions computed by R. The rest of the paper is organised as follows. In Section 2 we present basic notions and recall (briefly) the central concepts of graph rewriting. The adequacy theorem is provided in Section 3 and in Section 4 we show how innermost graph rewriting can be encoded efficiently. Finally we conclude in Section 5, where we also relate our work to recent work. Due to space limitations, we omit some proofs in the presentation. Those are available in the technical report [4].
2
Preliminaries
We assume familiarity with term rewriting [5,13], but no familiarity with graph rewriting (see [13]) is assumed. The purpose of this section is to fix the term rewriting notions and introduce a formulation of graph rewriting that is sufficient for our purposes. Let V denote a countably infinite set of variables and F a finite signature. The set of terms over F and V is denoted by T . The arity of a function symbol f is denoted as ar(f ). Var(t) denotes the set of variables occurring in a term t. The size of a term t, i.e., the number of symbols appearing in t, is denoted as |t|. We write t|p for the subterm of t at position p. A term rewrite system (TRS for short) R (over a signature F ) is a finite set of rewrite rules l → r, such that l ∈ / V and Var(l) ⊇ Var(r). The root symbols of left-hand sides of rewrite rules are called defined, while all other function symbols are called constructors. Constructor symbols are collected in C ⊆ F. The smallest rewrite relation, i.e., closed under contexts and substitutions, that contains R →∗R for the transitive and reflexive closure of − →R . is denoted by − →R . We write − Let s and t be terms. If exactly n steps are performed to contract s to t we write s− →nR t. A term s ∈ T is called a normal form if there is no t ∈ T such that s− →R t. With NF(R) we denote the set of all normal forms of a term rewrite system R. We write s − →!R t if s − →∗R t and t ∈ NF(R). A term t is called argument normalised (with respect to R) if every proper subterm t|p ∈ NF(R). Let 2 be a fresh constant. Terms over F ∪ {2} and V are called contexts. The empty context is denoted as 2. For a context C with n holes, we write C[t1 , . . . , tn ] for the term obtained by replacing the holes from left to right in i → C with the terms t1 , . . . , tn . The innermost rewrite relation − R of a TRS R is i defined on terms as follows: s − →R t if there exists a rewrite rule l → r ∈ R, a context C, and a substitution σ such that s = C[lσ] and t = C[rσ] for lσ argument normalised. →∗R t1 and s − →∗R t2 A TRS is called confluent if for all s, t1 , t2 ∈ T with s − ∗ ∗ there exists a term t3 such that t1 − →R t3 and t2 − → R t3 . In the sequel we introduce the central concepts of term graph rewriting or graph rewriting for short. We concentrate on standard approaches in the context of rewriting, compare also [12,13]. A graph G = (VG , succG , LG ) over the set L is a structure such that VG is a ∗ finite set, the nodes or vertexes, succG : VG → VG is a mapping that associates a
260
M. Avanzini and G. Moser
node n with an (ordered) sequence of nodes, called the successors of n. Finally LG : VG → L is a mapping that associates each node n with its label LG (n). Often we drop the reference to the graph G from VG , succG , and LG , i.e., we write G = (V, succ, L). If possible, we will omit references to the constituents of the structure G; in particular we often write n ∈ G instead of n ∈ V. Note that the sequence of successors of n may be empty: succ(n) = []. Typically the set of labels L is clear from context and not explicitly mentioned. Definition 1. Let G = (V, succ, L) be a graph and let n ∈ V. Consider succ(n) = i [n1 , . . . , nk ]. We call ni the i-th successor of n (denoted as n ni ). If there exists i i such that n m, then we simply write n m. A node m is called reachable ∗ ∗ from n if n m, where denotes the reflexive and transitive closure of . + ∗ We write for · . +
A graph G is acyclic if n m implies n = m and G is rooted if there exists a unique node n such that every other node in G is reachable from n. The node n is called the root rt(G) of G. The size of G, i.e. the number of nodes, is denoted as |G|. We write Gn for the subgraph of G reachable from n. Definition 2. A term graph (with respect to F and V) is an acyclic and rooted graph G = (V, succ, L) over F ∪V. Let n ∈ V and suppose L(n) = f ∈ F such that f is k-ary (k 0). Then succ(n) = [n1 , . . . , nk ]. On the other hand if L(n) ∈ V, then succ(n) = []. We demand that any variable node is shared, i.e., for n ∈ V with L(n) ∈ V, if L(n) = L(m) for m ∈ V then n = m. We set Var(G) := {n | n ∈ G, L(n) ∈ V} to denote the set of variable nodes in G. Let R be a TRS over a signature F . We keep R and F fixed for the remainder of this paper. We write Graph for the set of all term graphs with respect to F and V. Example 3. Consider the graph G = ({1, 2}, succ, L) where succ(1) = [2, 2] and L(1) = f ∈ F and L(2) = x ∈ V. Then G is a term graph. Intuitively G represents the term f(x, x) such that the variable x is shared. We define the term t := term(G) represented by G as follows: x if L(rt(G)) = x ∈ V t := f (t1 , . . . , tk ) if L(rt(G)) = f ∈ F and succ(rt(G)) = [n1 , . . . , nk ] . Here ti := term(Gni ) for i = 1, . . . , k. We write Term(G) for the set of subterms of term(G). Let G ∈ Graph. A position in G is a finite sequence of positive integers. The position of rt(G) is the empty sequence (denoted as ε). For positions p and q we write pq for their concatenation. The set of positions PosG (n) of n ∈ G is defined i1 ik ... n}. as PosG (n) := {ε} if n = rt(G) and PosG (n) := {i1 . . . ik | rt(G) Note that for any node n: PosG (n) = ∅. It is easy to see that for p ∈ PosG (n), term(Gn) = term(G)|p .
Complexity Analysis by Graph Rewriting
261
We say that n ∈ G is shared, if n represents more than one subterm of term(G). Note that n is shared if the set of positions PosG (n) of n is not a singleton. If PosG (n) is a singleton, then n is unshared. Definition 4. A node n is minimally shared if it is a variable node or unshared. We say n is maximally shared if for all m ∈ G such that term(Gm) = term(Gn) implies n = m. A term graph G is normal form sharing (with respect to R) if for all nodes n ∈ G, if term(G n) ∈ NF(R) then n is maximally shared and minimally shared otherwise. Let t be a term. We write (t) (♦(t)) for the set of minimally (normal form) sharing term graphs that represent t. Example 5. Reconsider the TRS R2 from the introduction. Let t = D(x + x) × D(x + x), represented by the term graphs Tmin , Tnf and Tmax depicted as follows: ×
×
D
D
+
+ x
D
× D
D
+ Tmin
x
+ Tnf
x
Tmax
Then Tmin ∈ (t) as only variable nodes are shared. Further Tnf ∈ ♦(t) since the subterms {x + x, x} = Term(Tnf ) ∩ NF(R) are represented by unique nodes whereas all other nodes are unshared. Tmax contains only maximally shared nodes. Let G and H be two graphs, possibly sharing nodes. We write G ∪ H for their union. We say that G and H are properly sharing if n ∈ G ∩ H implies LG (n) = LH (n) and succG (n) = succH (n). For two properly sharing graphs G and H, G ∪ H is again a graph (but possibly not a term graph). Definition 6. Let G be a term graph and H a rooted graph over F ∪ V. We denote by G[H]n the graph obtained by replacing the subgraph at node n in G by H: G[H]n := (G[n ← − rt(H)] ∪ H) m where m := rt(H) if n = rt(G) and m := rt(G) otherwise. Here G[n ← − m] denotes the redirection of n to m in G: Set VG := (VG ∪ {m}) \ {n} and for all p ∈ VG , succG (p) := r∗ (succG (p)). Here r replaces n by m: r(n) = m and r(p) = p for all p ∈ VG . Further r∗ denotes the standard extension of r to sequences. Finally, set G[n ← − m] := G = (VG , succG , LG ). Observe that for properly sharing term graphs G and H such that n ∈ H and H acyclic, G[H]n is again a term graph. Let G and H be two term graphs. A morphism (denoted m : G → H) is a function m : VG → VH such that m(rt(G)) = rt(H), and for all n with LG (n) ∈ F, LG (n) = LH (m(n)) and m∗ (succG (n)) = succH (m(n)). The next lemma follows directly from the definition.
262
M. Avanzini and G. Moser
Lemma 7. Suppose m : G → H and m : G → H. 1) For any n ∈ G we have m : Gn → Hm(n). 2) For any n ∈ G we have m(n) = m (n). We write G m H (or G H for short) if there exists a surjective morphism m : G → H that preservers labels from V. In this case we have for all n ∈ VG : LG (n) = LH (m(n)). When the graph morphism m is non-injective we write G >m H (or G > H for short). If m is injective and surjective then m is an isomorphism. Conclusively G and H are called isomorphic (denoted as G ∼ = H). The next lemma follows by a simple inductive argument. Lemma 8. For all term graph G and H, G m H implies term(G) = term(H). Definition 9. Let L, R be two properly sharing term graphs. Suppose Var(R) ⊆ Var(L), L(rt(L)) ∈ F and rt(L) ∈ R. Then the graph L ∪ R is called a graph rewrite rule ( rule for short). A rule is denoted as L → R, where L, R denotes the left-hand, right-hand side of L → R, respectively. A graph rewrite system (GRS) G is a set of graph rewrite rules. Note that for a rule L → R, the graphs L and R share at least all variable nodes. A rule L → R is called a renaming of L → R with respect to S if L → R ∼ = L → R and VS ∩ VL →R = ∅. Let G be a GRS, let S ∈ Graph and let L → R be a rule. A redex of S with L → R is a node u ∈ S such that there exists a renaming L → R of (L → R) ∈ G with respect to S such that m : L → Su is a morphism and T = S[m(R )]u . Here m(R ) denotes the structure obtained by replacing in R every node v ∈ L ∩ R by m(v) ∈ S, where the labels of m(v) ∈ m(R ) are the labels of m(v) ∈ S. From m : L → Sv we obtain that T is a term graph. Definition 10. We say S that n ∈ S is a redex with ⇒G,n,L→R T S= ⇒G T if S = is called the graph rewrite
rewrites to T if there exists a rule (L → R) ∈ G such this rule. This is denoted as S = ⇒G,n,L→R T . We set for some n ∈ S and (L → R) ∈ G. The relation = ⇒G relation induced by G.
We denote the set of normal forms with respect to ⇒ = G as NF(G). The innermost i graph rewrite relation = ⇒ is the restriction of ⇒ = G G where arguments need to be i ⇒G,n,L→R T if S = ⇒G,n,L→R T and for all m ∈ succS (n), in normal form, i.e. S = Sm ∈ NF(G).
3
Adequacy of Graph Rewriting for Term Rewriting
In this section we show that graph rewriting is adequate for term rewriting if we restrict our attention to innermost (graph) rewriting. This holds without further restrictions on the studied TRS R. (However, we assume that R is finite.) The here presented adequacy theorem (see Theorem 19) is not essentially new. Related results can be found in the extensive literature, see for example [13]. In
Complexity Analysis by Graph Rewriting
263
particular in [12] the adequacy theorem is even generalised to full rewriting. As this approach involves copying, it is currently not clear whether it applies in our context. Still our treatment of innermost rewriting for unrestricted TRSs is new. (See [8] for strongly related work on orthogonal constructor TRSs.) Furthermore the detailed analysis given in this section is a necessary foundation for the precise characterisation of the implementation of graph rewriting, presented in Section 4. Definition 11. The simulating graph rewrite system G(R) of R contains for each rule l → r ∈ R some rule L → R such that L ∈ (l) and R ∈ (r) have only variable-nodes in common, i.e. VL ∩ VR = Var(R). Below L, R, S and T denote term graphs. Suppose m : L → S is a morphism. Then m induces a substitution σm : V → Term(S): For any n ∈ S such that L(n) = x ∈ V we set σm (x) := term(Sm(n)). Lemma 12. Let L and S be term graphs. Suppose m : L → S for some morphism m. Then for each node n ∈ L, term(Ln)σm = term(Sm(n)). In particular, term(L)σm = term(S). Proof. We prove the lemma by induction on l := term(Ln). We write σ instead of σm . If l ∈ V, then by definition Ln consists of a single (variable-)node and lσ = term(S m(n)) follows by the definition of the induced substitution σ. If l = f (l1 , . . . , lk ), then we have succL (n) = [n1 , . . . , nk ] for some n1 , . . . , nk ∈ L. As m : L → S holds, Lemma 7 yields m : Lni → Sm(ni ) for all i = 1, . . . , k. And induction hypothesis becomes applicable so that li σ = term(Sm(ni )). Thus lσ = f (l1 σ, . . . , lk σ) = f (term(Sm(n1 )), . . . , term(Sm(nk ))) . By definition of m, LS (m(n)) = LL (n) = f and succS (m(n)) = m∗ (succL (n)) = [m(n1 ), . . . , m(nk )]. Hence f (term(S m(n1 )), . . . , term(S m(nk ))) = term(S m(n)). Finally, in order to conclude term(L)σ = term(S), it suffices to observe that by definition of m: m(rt(L)) = rt(S) holds. Lemma 13. Let L → R be a graph rewrite rule and let S be a term graph such that S ∩R = ∅ and m : L → S for some morphism m. Let m denote the standard extension of m to all nodes in R, i.e., m (n) := m(n) if n is in the domain of m and m (n) := n otherwise. Set T := (m (R) ∪ S)rt(m (R)). Then for each n ∈ R, term(Rn)σm = term(Tm (n)). In particular, term(R)σm = term(T ). Proof. We write σ instead of σm . Suppose n ∈ R ∩ L. Then Rn = Ln as L, R are properly shared. By definition of the morphism m: m(n) ∈ S. Thus by definition of T we have term(T m (n)) = term(Sm(n)). Moreover, employing Lemma 12, we have term(Rn)σ = term(Ln)σ = term(Sm(n)). From this the assertion follows. Thus suppose n ∈ R \ L. This subcase we prove by induction on r = term(R n). The base case r ∈ V follows as variables are shared in L → R. For the
264
M. Avanzini and G. Moser
inductive step, let r = f (r1 , . . . , rk ) with succR (n) = [n1 , . . . , nk ]. The induction hypothesis yields ri σ = term(T m (ni )) for i = 1, . . . , k. By definition of m : m (n) = n ∈ m (R) ⊆ T . Hence succT (m (n)) = succm (R) (n) = ∗ m (succR (n)) = [m (n1 ), . . . , m (nk )]. Moreover LT (n) = Lm (R) (n) = f by definition. We conclude rσ = f (r1 σ, . . . , rk σ) = f (term(T m (n1 )), . . . , term(T m (nk ))) = term(Tm (n)). This concludes the inductive argument. Finally, in order to conclude term(R)σm = term(T ) observe that m(rt(T )) = rt(R) holds. Below we also write 2 for the unique (up-to isomorphism) graph representing the constant 2. Lemma 14. Let S and T be two properly sharing term graphs, let n ∈ S \ T . Then term(S[T ]n ) = C[term(T ), . . . , term(T )] where C = term(S[2]n ) and the number of occurrences of the term term(T ) equals |PosS (n)|. Proof. We proceed by induction on the size of S. In the base case S consists of a single node n. Hence the context C is empty and the lemma follows trivially. For the induction step we can assume without loss of generality that n = rt(S). We assume LS (rt(S)) = f ∈ F and succS (rt(S)) = [m1 , . . . , mk ]. For all i (1 i k) such that mi = n set Ci = 2 and for all i such that mi = n but (S[T ]n )mi = (Smi )[T ]n we set Ci = term((Smi )[2]n ). In the latter sub-case induction hypothesis is applicable to conclude term S[T ]n mi = Ci [term(T ), . . . , term(T )] . Finally we set C := f (C1, . . . , Ck ) and obtain C= term(S[2]n ). In sum we have term(S[T ]n ) = f S[T ]n m1 , . . . , S[T ]n mk = C[term(T )], where term(T ) denotes the sequences of terms term(T ) of the required length |PosS (n)|. Lemma 15. Let R be a TRS, l be a term and let σ : V → NF(R) be a substitution such that s = lσ is argument normalised with respect to R. If L ∈ (l) and S ∈ ♦(s), then there exists a morphism m : L → S such that for all variable nodes n ∈ Var(L) with LL (n) = x, xσ = term(Sm(n)). Proof. We prove the lemma by induction on l. It suffices to consider the induction step. Let l = f (l1 , . . . , lk ) and s = f (l1 σ, . . . , lk σ). Let succL (rt(L)) = [p1 , . . . , pk ], let succS (rt(S)) = [q1 , . . . , qk ], and let i ∈ {1, . . . , k}. By induction hypothesis there exist morphisms mi : Lpi → Sqi of the required form. Either mi (n) = mj (n) or n ∈ (dom(mi ) ∩ dom(mj )). Otherwise suppose n ∈ (dom(mi )∩dom(mj )). We show mi (n) = mj (n). Since L ∈ (l), only variable nodes are shared, hence n needs to be a variable node. Suppose LL (n) = x. Then term(Smi (n)) = xσ = term(Smj (n)). As S ∈ ♦(s) and xσ ∈ NF(R), mi (n) = mj (n) has to hold. Define a function m : VL → VS as follows. Set m(rt(L)) = rt(S) and for p = rt(L) define m(p) = mi (p) if p ∈ dom(mi ). By the above observation, m is well-defined and a morphism.
Complexity Analysis by Graph Rewriting
265
Lemma 16. Let t be a term and let T be a normal form sharing term graph such that t = term(T ). Then t ∈ NF(R) if and only if T ∈ NF(G(R)). Let T be a term graph; a node n ∈ T is an R-redex if term(T n) ∈ NF(R). We call T redex-unsharing if none of its R-redexes are shared. Lemma 17. Suppose T is redex-unsharing. Then T is normal form sharing if and only if it is not possible to find distinct nodes p, q ∈ T such that (i) term(T p) ∈ NF(R), (ii) L(p) = L(q) and (iii) succ(p) = succ(q). Proof. It suffices to consider the direction from right to left as the other is trivial. Thus suppose T is not normal form shared. We show that there exist nodes p, q fulfilling the properties stated. We pick some node p ∈ T with term(Tp) ∈ NF(R) and there exists a distinct node q ∈ T with term(Tp) = term(Tq). For that we assume that p is -minimal + in the sense that there is no node p with p p such that p would fulfil the above properties. The node p exists as T is not normal form shared. By definition property (i) holds and property (ii) follows from term(Tp) = term(Tq). To show property (iii) we assume succ(p) = [p1 , . . . , pl ] and succ(q) = [q1 , . . . , ql ] where for at least for one i ∈ {1, . . . , l}, pi = qi . But this contradicts the minimality of p. We conclude succ(p) = succ(q) as desired. The next lemma follows from the above using a simple inductive argument. Lemma 18. Let S be redex-unsharing. Then for all T ∈ ♦(term(S)), S T . Theorem 19 (Adequacy). Let R be a TRS and let G(R) denote the simulating i i GRS. Suppose s is a term and S ∈ ♦(s). Then s − → ⇒ R t if and only if S = G(R) · T , where T ∈ ♦(t). i Proof. First we consider the direction from right to left. Suppose S = ⇒ G(R),p T such that (L → R) ∈ G(R), p ∈ S, m : L → Sp and T = S[m(R)]p . Moreover, Sn ∈ NF(G) for all n ∈ succ(p). Define the context C = term(S[2]p ), we write σ for the induced substitution σm and prove i term(S) = C[term(L)σ] − → R C[term(R)σ] = term(T ) .
Observe that S = S[Sp]p . By Lemma 12, term(S p) = term(L)σ. Due to Lemma 14 we see term(S) = term(S[Sp]p ) = C[term(Sp)] = C[term(L)σ] . By definition T = S[m(R)]p = S[(m(R) ∪ S)rt(m(R))]p . Due to Lemma 13 we i have term(T ) = C[term(R)σ]. In order to show term(S) − → R term(T ) it suffices to verify that term(L)σ = term(Sp) is argument normalised with respect to R. Note that lσ ∈ NF(R) and S is a normal form sharing graph. Hence the context C contains only one hole. Let u be a proper subterm of term(S p). As for all n ∈ succS (p), Sn ∈ NF(G), and Sp is argument normalised, u ∈ NF(R) follows from Lemma 16. Hence term(l)σ is argument normalised.
266
M. Avanzini and G. Moser
i Finally, we prove the direction from left to right. Suppose s = C[lσ] − → R C[rσ] = t with l → r ∈ R and lσ argument normalised. Let p ∈ S be the node corresponding to lσ. As lσ is not a normal form, p is unique. Clearly S p ∈ ♦(lσ). Let (L → R) ∈ G(R). According to Lemma 15, there exists a morphism m : L → Sp such that xσ = term(Sm(n)) where LS (n) = x ∈ V. As i ⇒ lσ is argument normalised, S = G(R) S[m(R)]p follows from Lemma 16. It remains to prove that T := S[m(R)]p T ∈ ♦(t). We claim term(T ) = C[rσ] = t. For this, let U := (m(R) ∪ S)rt(m(R)) and observe T = S[m(R)]p = S[U ]p . Due to Lemma 13, term(U ) = rσ. Moreover, it is easy to see that C = term(S[2]p ). Hence by Lemma 14, term(S[U ]p ) = C[term(U )] and the claim follows. Let T ∈ ♦(t). We want to apply Lemma 18 to conclude T T . For that we have to prove that T is redex-unshared. Suppose there exists q ∈ T such that u := term(T q) is reducible with respect to R. We show that PosT (q) is a singleton set. We distinguish two cases: Either q is reachable from rt(m(R)) in T or it is not reachable from rt(m(R)). If q is reachable, then we can even conclude that q ∈ m(R). For this, suppose there exists a variable node n ∈ R such that + ∗ rt(m(R)) m(n) q. Then term(T m(n)) = xσ for some variable x ∈ Var(r). But xσ ∈ NF(R) as lσ is argument normalised. This contradicts the assumption that term(T q) is reducible. Hence suppose q ∈ m(R). By Definition 9 in L, R only variables are shared and in S only normal forms are shared. Hence q can only be shared in m(R) if it represents a normal form, contrary to our assumption. Hence it is unshared. Finally consider the case that q is not reachable from rt(m(R)) in T . This implies that q ∈ S and thus q is unshared in T if q is unshared in S. Suppose u = term(Sq), i.e., the term represented by node q is unchanged by the (graph) rewrite step. Then q is unshared as by assumption u ∗ is reducible and S ∈ ♦(s). Otherwise if u = term(Sq), then q p for the redex p ∈ S. Hence term(Sq) is reducible and again not shared in S.
Sometimes it is convenient to combine graph rewrite and collapse steps into one ≥ i relation. Thus we define S = ⇒G T if and only if S = ⇒ G · T . Employing this i notion we can rephrase the conclusion of the theorem as: s − → R t if and only if ≥ S= ⇒G T , whenever the conditions of the theorem are fulfilled.
4
Complexity Considerations
We now prove a polynomial relationship between the number of rewrite steps admitted by R and the computational complexity of the functions defined. We give semantics to R in the most natural way. Let Val, the set of values, denote the set of terms generated from constructors C and variables V. Further, suppose R is a confluent and terminating TRS. An n-ary partial function f : Valn → Val is computable by R if there exists a defined function symbol f ∈ F such that for all s1 , . . . , sn , t ∈ Val: f(s1 , . . . , sn ) − →!R t ⇐⇒ f (s1 , . . . , sn ) = t ,
Complexity Analysis by Graph Rewriting
267
where f is defined. Note that this is well defined as R is confluent and terminating. We say that R computes f , if the function f : Valn → Val is defined by the above equation. We define the derivation length of a term s with respect to a terminating TRS R and rewrite relation →: dl(s, →) = max{n | ∃t s →n t}. The (innermost) runtime complexity (with respect to R) is defined as follows: i rciR (n) = max{dl(s, − → R ) | s = f (s1 , . . . , sn ), s1 , . . . , sn ∈ Val and |s| n} .
Suppose R admits polynomial runtime complexity, i.e. rciR is bounded polynomially. To conclude polytime computability of a function f computed by R, we implement f using graph rewriting. More precisely, to evaluate s := f(s1 , . . . , sn ) ≥ for values s1 , . . . , sn , we normalise the graph S corresponding to s under = ⇒G(R) . This is admissible by the Adequacy Theorem (Theorem 19). At most polynomially many reduction steps (in |s|) need to be performed. Below we show that each such intermediate step can be performed in time polynomial in the size of s. ≥
⇒G T Lemma 20. Let G denote a GRS, set c := max{|R| | (L → R) ∈ G}. If S = then |T | |S| + c. Hence, sizes of intermediate graphs are polynomially related to the size of S and s. (Note that |S| |s|). From this we derive that polynomial innermost runtime complexity induces polytime computability. In the following, we fix the set of nodes V := N. We represent S ∈ Graph as a pair rt(S), spec where spec is a sequence containing for each node n ∈ S the triple n, LS (n), succS (n). Following [6], we call such triples node specifications. We say that a term graph S is normalised if VS = {1, . . . , |S|}. Representing normalised term graphs S requires space O(log(|S|) ∗ |S|) on any reasonable model of computation: Each node n ∈ S requires space at most log(|S|). Consequently, each node specification n, LS (n), succS (n) is representable in space O(log(|S|)). Here we employ that for a fixed signature F , arities are bounded by some constant, moreover each label LS (n) requires just constant space. As we have to store at most |S| specifications (and the root node), the assertion follows. We define S := O(log(|S|) ∗ |S|). Below we also employ the notation · for the space required to represent different data-types like nodes or lists. In the following, we tacitly assume that term graphs are normalised. This is justified as normalisation introduces negligible overhead as can be seen in the next lemma. Lemma 21. Let S be a term graph. The function that maps S to an isomorphic and normalised term graph is computable in time O(S2 ). Proof. We traverse over S and replace each encountered node n ∈ S by the node m(n), where m is a graph morphism normalising S to be constructed. To obtain m, we start the overall procedure using a counter c := 1 and initialise m as the morphism that is undefined everywhere. At each call m(n) we check whether m is defined on n. If so, we return m(n), otherwise we set m(n) := c, c := c + 1 and return m(n) afterwards. When the procedure stops, m will be an injective graph morphism, and m(S) a normalised term graph isomorphic to S.
268
M. Avanzini and G. Moser
While traversing S in time O(S), we replace the root node, and for each node specification encountered we replace at most a constant number of nodes. I.e., in total O(|S|) calls m(n) have to be performed. We can represent m in space m = O(S). From this we obtain that m(n) is computable in time O(S). Overall, the procedure finished after at most O(S2 ) steps. The implementation of a graph rewrite step S = ⇒G T is developed stepwise in the following. The function match : (Graph × Graph× V) → (V → V) ∪ {⊥} computes morphisms between term graphs. Here ⊥ is a designator indicating that no morphism can be found. More precisely, for m : L → Sn we set match(L, S, n) := m; otherwise match(L, S, n) := ⊥. The definition of match is well-formed as m : L → S is unique, cf. Lemma 7. Second, we use the function apply : (Graph × V × Graph × (V → V)) → Graph defined by apply(S, n, R, m) := S[m(R)]n . Here we suppose m : L → Sn for some graph rewrite rule L → R. Let Rule denote the set of (normalised) graph rewrite rules over labels F ∪ V. A step S = ⇒G,n,L→R T is thus computed by the function step : (Graph × V × Rule) → Graph ∪ {⊥} given by step(S, n, L → R) := apply(S, n, R , match(L , S, n)) where L → R = rename(S, L → R) . Here we suppose step(S, n, L → R) = ⊥ if match(L , S, n) = ⊥. The function rename : (Graph × Rule) → Rule is defined such that rename(S, L → R) is a renaming of the rule L → R with respect to the term graph S. We give bounds on the computational complexity of match, rename and apply. Here we essentially translate the definition into programs and prove that those programs operate under the desired bounds, c.f. [4]. We implement match(L, S, n) by recursion on the term representation of L, which explains why match(L, S, n) operates in time exponential in |L|. For R fixed, 2O(L) is constant and thus harmless for our concerns. Lemma 22. Let S be a term graph and L → R a graph rewrite rule. Then 1) match(L, S, n) is computable in time 2O(L) ∗ S2 , and 2) rename(S, L → R) is computable in time O(S + L → R), and 3) apply(S, n, R, m) is computable in time O((S + L → R)3 ). Lemma 23. Let S be a term graph, L → R a graph rewrite rule and n ∈ L be a node. Then step(S, n, L → R) is computable in time O(2O(L→R) ∗ S3 ). Proof. Employing L < L → R, the lemma follows from Lemma 22.
Remark 24. Note that for G fixed and (L → R) ∈ G, the bound O(2O(L→R) ∗ S3 ) reduces to O(S3 ). ≥
Let S and T be term graphs such that S = ⇒G T . To show that T is efficiently computable from S, it remains to verify that collapsing to normal form sharing graphs is feasible. We introduce the function share : Graph → Graph that maps (redex-unsharing) term graphs S to their normal form sharing counterparts. To be more precise, S share(S) with share(S) normal form sharing.
Complexity Analysis by Graph Rewriting
269
Lemma 25. Let S be redex-unsharing and let p ∈ S be some -minimal node such that there exists a distinct node q ∈ S with (i) L(p) = L(q) and (ii) succ(p) = succ(q). Then Sp is normal form sharing. Proof. For a proof by contradiction, suppose Sp is not normal form sharing. By Lemma 17, there exist distinct nodes p , q ∈ Sp such that labels and successor ∗ ∗ function coincide in Sp for p and q . By definition p p and p q such that in at least one case the path considered in Sp has non-zero length. Suppose we + have p p . Then the node p is a counter example to minimality of p. Lemma 26. Let S be redex-unsharing. share(S) is computable in time O(S4 ). Proof. We compute share(S) by computing term graphs S1 , . . . , Sn with S = S1 > · · · > Sn = S such that for all i = 1, . . . , n, the graph Si is redexunsharing. Lemma 18 guarantees that the above sequence is well-defined. Since Si > Si+1 implies |Si | > |Si+1 |, the number of iterations is bound by |S|. It suffices to show one iteration. We show that the computation of Si+1 from Si , can be performed in time O(S3 ). Including normalisation (operating in time O(S2 ), cf. Lemma 21), we obtain that share(S) is computable in time |S| ∗ O(S3 ) + O(S2 ) O(S4 ). Suppose we have constructed the redex-unsharing graph Si . To obtain Si+1 from Si , we search for two distinct nodes p and q such that succSi (p) = succSi (q), LSi (p) = LSi (q) and term(Si p) ∈ NF(R). By Lemma 17, nodes p and q exist if and only if Si is normal form shared. If p and q exist we obtain Si+1 by identifying nodes p and q in Si . Otherwise Si is normal form shared, we set Si+1 := Si and the procedure stops. Set R := {p | ∃q. p = q ∧ succSi (p) = succSi (q) ∧ LSi (p) = LSi (q)}. Thus, reformulating the above approach, we search for p ∈ R with term(Sip) ∈ NF(R). To this extend, let Rmin ⊆ R be the restriction of R to -minimal nodes; + + Rmin := {p | p ∈ R∧¬∃p ∈ R. p p}. Clearly p p and term(Sip ) ∈ NF(R) implies term(Si p) ∈ NF(R). It is thus sufficient to search for some p ∈ Rmin with term(Si p) ∈ NF(R), or equivalently Si p ∈ NF(G). The latter observation results from Lemma 16, since by Lemma 25 the subgraph Si p is normal form sharing due to the definition of Rmin . Suppose p ∈ Rmin with term(Si p) ∈ NF(R). By definition of Rmin there exists a distinct node q ∈ Si such that term(Si p) = term(Si q). To obtain Si+1 from Si , we identify p and q, i.e. we set Si+1 := m(Si ) where the graph morphism m : Si → Si+1 is defined by m(q) = p and m(n) = n for n ∈ VSi \ {q}. Then Si+1 is redex-unsharing, moreover Si >m Si+1 as desired. One easily verifies that R is computable in time O(Si 2 ) O(S2 ). The cardinality of R is bound by |Si | |S|, as S is normalised we see that R O(S). + Using a quadratic reachability-algorithm to check p p, Rmin is computable 2 3 from R in time R ∗ O(Si ) O(S ). As Rmin ⊆ R we have to check Sip ∈ NF(G) at most |Si | |S| times. For this, we check match(L, Si , p) = ⊥ for all rules (L → R) ∈ G(R). The latter can be done in time O(Si 2 ) O(S2 ) (cf. Lemma 22). We conclude that p ∈ Rmin with term(Si p) ∈ NF(R) can be
270
M. Avanzini and G. Moser
found in time O(S2 ) + O(S3 ) + |S| ∗ O(S2 ) O(S3 ). Searching the corresponding node q ∈ Si and applying the morphism m can be done in time O(S). Overall, the runtime for computing Si+1 from Si is bound by O(S3 ) as desired. Theorem 27. Let R be a confluent and terminating TRS, moreover suppose rciR (n) = O(nk ) for all n ∈ N and some k ∈ N. The functions computed by R are computable in time O(n5∗(k+1) ). Proof. To compute the functions defined by R, we encode terms as graphs and perform graph rewriting using the simulating GRS G := G(R). Let f be a function computed by R, let f ∈ F be the associated function symbol. Fix values s1 , . . . , sn such that f (s1 , . . . , sn ) = t is defined. Let s = f(s1 , . . . , sn ) and i ! S ∈ ♦(s). By definition and confluence of R, we obtain s − → R t. We show that 5∗(k+1) . For this, consider some derivation f (s1 , . . . , sn ) is computable in time |s| ≥
≥
⇒G . . . = ⇒G Tl = T S = T0 =
(†)
with T ∈ NF(G). By Theorem 19 we conclude term(T ) = t. ≥ We first analyse a single step Ti = ⇒G Ti+1 from the derivation (†). In order to compute Ti+1 from Ti , we identify the corresponding redex in Ti . Define ∈ NF(G) ∧ ∀nj ∈ succTi (n). Ti nj ∈ NF(G). Then the node redex(n) := Ti n n ∈ Ti is an innermost redex if and only if redex(n) holds. In order to check Tiq ∈ NF(G) for node q ∈ Ti , as before we apply the function match a constant number of times. Due to Lemma 22 we see that redex(n) is computable in time O(Ti 2 ) overall. We find the desired redex in Ti in time O(Ti 3 ): we traverse Ti and return the first n ∈ Ti encountered satisfying redex(n). Without loss of generality, ≥ we can assume n is the redex in Ti = ⇒G Ti+1 (otherwise, we adapt the derivation (†) appropriately). Let Ti+1 = step(s, n, L → R) for the first applicable rule (L → R) ∈ G. By Lemma 23, since we have to check only a constant number of is computable from Ti in time O(Ti 3 ). As observed in the rules L → R, Ti+1 proof of Theorem 19, the graph Ti+1 is redex-unsharing. Using Lemma 26, we finally obtain Ti+1 = share(Ti+1 ) in time O(Ti+1 4 ). Summing all up, employing Ti+1 Ti + c for some fixed c ∈ N (cf. Lemma 20), Ti+1 is computable from Ti in time O(Ti 4 ). ≥ We return to the derivation (†). Note s ∈ B and further Ti = ⇒G Ti+1 implies i k term(Ti ) − → R term(Ti+1 ). Thus we conclude l = O(|s| ) for some k ∈ N. Let j ∈ {0, . . . , l − 1}. Lemma 20 implies |Tj | |S| + j ∗ c O(|s|k ) for some fixed c ∈ N. Here we employ that |S| |s|. Recall Tj = O(log(|Tj |) ∗ |Tj |). ≥ ⇒G Tj+1 can be Thus Tj O(log(|s|k )∗|s|k ) O(|s|k+1 ). And so each step Tj = performed in time O(Tj 4 ) O(|s|4∗(k+1) ) using the above observation. In total, ≥ ⇒G -normalised in time O(|s|k ) ∗ O(|s|4∗(k+1) ) we obtain that S ∈ ♦(s) can be = 5∗(k+1) ). We conclude the theorem. O(|s|
Complexity Analysis by Graph Rewriting
5
271
Conclusion
Recently, many techniques have been introduced that allow the (automated) classification of the runtime complexity of TRS. In this paper we show that polynomial innermost runtime complexity of TRSs induces polytime computability of the functions defined. As a side-result we present a simulation between (innermost) term rewriting and (innermost) graph rewriting. The latter result is related to implicit computational complexity and in particular to a recent result by Dal Lago and Martini. In [8] Dal Lago and Martini establish that orthogonal constructor TRSs and lambda calculus with weak call-by-value reduction simulate each other with linear overhead. The proof of this [8] (compare also [7]) provides a variant of Theorem 19 for the restricted case that the TRS in question is constructor and orthogonal. By augmenting the innermost graph rewrite relation by collapse steps, our result prevail also in the general case.
References 1. Avanzini, M., Moser, G.: Complexity Analysis by Rewriting. In: Garrigue, J., Hermenegildo, M.V. (eds.) FLOPS 2008. LNCS, vol. 4989, pp. 130–146. Springer, Heidelberg (2008) 2. Avanzini, M., Moser, G.: Complexity Analysis by Rewriting. Technical report, Computational Logic (November 2008), http://cl-informatik.uibk.ac.at/ zini/publications/ FLOPS08 techreport.pdf 3. Avanzini, M., Moser, G.: Dependency Pairs and Polynomial Path Orders. In: Treinen, R. (ed.) RTA 2009. LNCS, vol. 5595, pp. 48–62. Springer, Heidelberg (2009) 4. Avanzini, M., Moser, G.: Complexity Analysis by Graph Rewriting. Technical report, Computational Logic (December 2010), http://cl-informatik.uibk.ac.at/ zini/publications/FLOPS10 techreport.pdf 5. Baader, F., Nipkow, T.: Term Rewriting and All That. Cambridge University Press, Cambridge (1998) 6. Barendregt, H., Eekelen, M.v., Glauert, J., Kennaway, R., Plasmeijer, M., Sleep, R.: Towards an Intermediate Language based on Graph Rewriting. In: de Bakker, J.W., Nijman, A.J., Treleaven, P.C. (eds.) PARLE 1987. LNCS, vol. 259, pp. 159– 175. Springer, Heidelberg (1987) 7. Dal Lago, U., Martini, S.: Derivational complexity is an invariant cost model. In: Proc. 1st FOPARA (2009) 8. Dal Lago, U., Martini, S.: On Constructor Rewrite Systems and the LambdaCalculus. In: Albers, S., Marchetti-Spaccamela, A., Matias, Y., Nikoletseas, S., Thomas, W. (eds.) ICALP 2009. LNCS, vol. 5556, pp. 163–174. Springer, Heidelberg (2009) 9. Dershowitz, N.: 33 Examples of Termination. In: Comon, H., Jouannaud, J.-P. (eds.) TCS School 1993. LNCS, vol. 909, pp. 16–26. Springer, Heidelberg (1995) 10. Hirokawa, N., Moser, G.: Automated Complexity Analysis Based on the Dependency Pair Method. In: Armando, A., Baumgartner, P., Dowek, G. (eds.) IJCAR 2008. LNCS (LNAI), vol. 5195, pp. 364–379. Springer, Heidelberg (2008) 11. Marion, J.-Y.: Analysing the Implicit Complexity of Programs. IC 183, 2–18 (2003) 12. Plump, D.: Essentials of Term Graph Rewriting. ENTCS 51 (2001) 13. TeReSe: Term Rewriting Systems. Cambridge Tracks in Theoretical Computer Science, vol. 55. Cambridge University Press, Cambridge (2003)
Least Upper Bounds on the Size of Church-Rosser Diagrams in Term Rewriting and λ-Calculus Jeroen Ketema1 and Jakob Grue Simonsen2
2
1 Faculty EEMCS, University of Twente P.O. Box 217, 7500 AE Enschede, The Netherlands
[email protected] Department of Computer Science, University of Copenhagen (DIKU) Universitetsparken 1, 2100 Copenhagen Ø, Denmark
[email protected]
Abstract. We study the Church-Rosser property—which is also known as confluence—in term rewriting and λ-calculus. Given a system R and a peak t ∗ ← s →∗ t in R, we are interested in the length of the reductions in the smallest corresponding valley t →∗ s ∗ ← t as a function vsR (m, n) of the size m of s and the maximum length n of the reductions in the peak. For confluent term rewriting systems (TRSs), we prove the (expected) result that vsR (m, n) is a computable function. Conversely, for every total computable function ϕ(n) there is a TRS with a single term s such that vsR (|s|, n) ≥ ϕ(n) for all n. In contrast, for orthogonal term rewriting systems R we prove that there is a constant k such that vsR (m, n) is bounded from above by a function exponential in k and independent of the size of s. For λ-calculus, we show that vsR (m, n) is bounded from above by a function contained in the fourth level of the Grzegorczyk hierarchy.
1
Introduction
The Church-Rosser property—also called confluence—is a property of rewriting systems which states that any peak t ∗ ← s →∗ t has a corresponding valley t →∗ s ∗ ← t . The valley and the term s are said to complete the diagram. In functional programming, the Church-Rosser property ensures that different ways of evaluating a program will always yield the same end result (modulo non-termination): The outcome will be independent of the evaluation order or reduction strategy. In logic, if a deductive system has the Church-Rosser property, the system will be consistent: No statement can both hold and not hold. While the Church-Rosser property has been shown to hold for a wide variety of rewrite systems, there has, to our knowledge, never been an investigation into the number of reduction steps in a valley that completes the diagram of a peak of a given size (see Figure 1). Succinctly: The question “How large is the valley as a function of the peak?” has apparently never been asked. M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 272–287, 2010. c Springer-Verlag Berlin Heidelberg 2010
Least Upper Bounds on the Size of Church-Rosser Diagrams s@ @@@ ≤n @@ @@
273
≤n
t
≤l
s
t
≤l
Fig. 1. The Church-Rosser property for a rewriting system R with bounds on the lengths of reductions. This paper is concerned with finding least upper bounds of l as a function of n. Succinctly: The peak being the upper half of the diagram, the valley being the lower half, how large is the valley as a function of the peak?
We believe the above question to be intrinsically interesting from a theoretical point of view, as Church-Rosser-type results are ubiquitous. We also believe the practical implications in mainstream functional programming to be limited: Standard functional languages like ML and Haskell employ a fixed evaluation strategy such as call-by-value or call-by-need, and there seems to be little interest in performing optimisations by switching strategies (modulo non-termination). However, for more specialised languages, like declarative DSLs where the evaluation order may not be fixed, there may be practical implications: If, for small peaks, the size of the smallest corresponding valley is so large that a term completing the Church-Rosser diagram cannot be computed using realistic resources, then it matters very much what kind of reduction strategy is used: Choosing the ‘wrong’ evaluation strategy (say, call-by-value) and performing just a few steps of computation could result in a very long reduction before a result is reached— better to backtrack to the original term and try another strategy. Apparently, there is no prior research concerning this problem in the foundational basis of declarative programming—λ-calculus and term rewriting. There does exist some literature on length of shortest and longest reductions to normal form for certain classes of systems [14,8,16], but the Church-Rosser theorem does not concern normal forms: It also applies to systems where some (or all) terms may fail to have normal forms. In this paper, we perform the first fundamental study of the size of peaks and valleys for systems having the Church-Rosser property; specifically we study how the size of a peak affects the valley size of the smallest corresponding valley. We consider three very general settings: That of (arbitrary) first-order term rewriting systems, of orthogonal term rewriting systems (roughly corresponding to firstorder functional programs that have no fixed evaluation order), and untyped λ-calculus. We believe that these three areas cover most of the non-specialised areas where the Church-Rosser property occurs; the most significant omission is the case of general higher-order rewrite systems (including general higher-order functional programs and logics with bound variables)—we expect general upper bounds in that case to be difficult to derive (and, likely, to be astronomical), as is foreshadowed by our treatment of λ-calculus in Section 5.
274
J. Ketema and J.G. Simonsen
The remainder of this paper proceeds as follows: Section 2 reviews preliminary notions. Section 3 formally introduces valley sizes and shows, respectively, that for term rewriting systems the valley size will always be a computable function and that every computable function can be majorized by a valley size of a specific family of peaks in a term rewriting system. Section 4 gives an exponential upper bound for valley sizes in orthogonal term rewriting systems and Section 5 shows that valley sizes in λ-calculus are bounded from above by a function in the fourth level of the Grzegorczyk hierarchy. Section 6 concludes.
2
Preliminaries
We presuppose a working knowledge of Turing machines and a basic familiarity with term rewriting and λ-calculus. We give brief definitions below. The basic references for term rewriting are [1,15,9]; for λ-calculus we refer the reader to [2,15,5]. For Turing machines, almost any introductory textbook on computability will do, e.g. [11,7,13,5]. Section 5 of the paper uses the Grzegorczyk hierarchy; we refer the reader to [6,10] for definitions. 2.1
Abstract Rewriting and the Church-Rosser Property
We introduce some basic notions related to abstract rewriting and confluence. Definition 2.1. An abstract rewriting system (ARS) is a pair (A, R) with A a set of objects and R a binary relation over A where we write (a, b) ∈ R as a → b. We write →∗ for the reflexive, transitive closure of R. A reduction or rewrite sequence is a finite sequence a1 , a2 , . . . , an with ai → ai+1 for all i < n, which we usually write as a1 → a2 → · · · → an−1 → an , or even as a1 →∗ an . Definition 2.2. A peak is a pair of reductions (s → s1 → · · · → si−1 → si , s → s1 → · · · → sj−1 → sj ) both starting from s. A valley is a pair of reductions (si → t1 → · · · → tk−1 → t, sj → t1 → · · · → tl−1 → t) both ending in t. We usually write a peak as si i ← s →j sj and a valley as si →k t l ← sj , occasionally replacing reduction lengths with the Kleene star ∗ when the lengths are unimportant. Definition 2.3. An ARS (A, R) has the Church-Rosser property or is confluent iff for every peak t ∗ ← s →∗ t there exists a valley t →∗ s ∗ ← t . Definition 2.4. Let (A, R) be an ARS and let a ∈ A. Define the reduction graph of a, denoted G(a), to be the graph (Va , Ea ) inductively defined by {a} if n = 0 Va,n = {b : ∃a ∈ Va,n−1 .a → b} if n > 0
Least Upper Bounds on the Size of Church-Rosser Diagrams
and
∅ {(a , b) : a ∈ Va,n−1 , b ∈ Va,n . a → b} And G(a) = (Va , Ea ) = ( n≥0 Va,n , n≥0 Ea,n ). Ea,n =
275
if n = 0 if n > 0
Thus, Va,n is the set of objects b such that a →n b. 2.2
Term Rewriting Systems
We define term rewriting systems. Throughout, we assume a fixed, finite signature Σ with each function symbol of non-negative integer arity and a denumerable, infinite set of variables V . The set of terms over Σ and V , denoted Ter(Σ, V ), is defined by induction, as usual. We assume the following. Definition 2.5. Let s be a term. – The term s is ground if no variables occur in s. – The set of positions of s, denoted pos(s) is the subset of N∗ inductively n defined by pos(x) = {} and pos(f (s1 , . . . , sn )) = {} ∪ ( i=1 i · pos(si )). – The set of variables of s, denoted vars(s), is the finitesubset of V inductively n defined by vars(x) = {x} and vars(f (s1 , . . . , sn )) = i=1 vars(si ). – The size of s, denoted |s|, is defined inductively as: • |x| = 1; • |f (s1 , . . . , sn )| = 1 + |s1 | + · · · + |sn |. Positions are equipped with a partial (strict) order ≺ such that p ≺ q if p is a proper prefix of q. Moreover, we write s|p for the subterm of a term s that occurs at position p ∈ pos(s). Substitutions, written θ : V −→ Ter(Σ, V ), are defined as usual. Contexts are terms over Σ {}, written as C[], where we say that that a context C[] is a k-hole context if there are exactly k occurrences of in C[]. Definition 2.6. A rule over Σ is a pair (l, r), invariably written l → r, where l and r are terms over Σ such that l ∈ / V and vars(r) ⊆ vars(l). A term s rewrites to a term t by l → r if there is a one-hole context C[] and a substitution θ such that s = C[θ(l)] and t = C[θ(r)]. A term rewriting system (TRS) is a pair (Σ, R) with Σ a signature and R a finite set of rules over Σ. We usually suppress explicit mention of the signature Σ and refer to the TRS (Σ, R) as R. Every TRS R gives rise to an ARS (A, R ) in the obvious fashion: The elements of A are the terms and R is the above rewrite relation. Definition 2.7. A rule is left-linear if every variable of occurs at most once in l. A TRS R is left-linear if all of its rules are. A rule l1 → r1 is said to overlap a rule l2 → r2 at position p ∈ pos(l2 ) if l2 |p ∈ / V and there are two substitutions σ, θ such that θ(l1 ) = σ(l2 |p ). A TRS (Σ, R) is said to be orthogonal if R is left-linear, and the only overlaps of rules in R are those where a rule overlaps itself at position . Two TRSs (Σ0 , R0 ) and (Σ1 , R1 ) are said to be mutually orthogonal if they are left-linear, and no rule of R0 overlaps with a rule of R1 , and vice versa.
276
2.3
J. Ketema and J.G. Simonsen
λ-Calculus
The (untyped) λ-calculus is the ARS (Λ, →β ) with Λ the set of objects M defined inductively by M ::= x | λx.M | M M where x ∈ V is a variable and with →β the rewrite relation induced by the β-rule: (λx.M ) N →β M {N/x} where M {N/x} equals M with N substituted for every free occurrence of x in M . Contexts for λ-calculus are defined as for TRSs. We assume the following. Definition 2.8. Let M be a λ-term. – The set of positions of M , denoted pos(M ), is the subset of N∗ inductively defined by pos(x) = {}, pos(λx.M ) = {}∪0·pos(M ), and pos(M1 M2 ) = {} ∪ 0 · pos(M1 ) ∪ 1 · pos(M2 ). – The size of M , denoted |M |, is defined by inductively as: • |x| = 1; • |λx.M | = 1 + |M |; • |M N | = |M | + |N |. Positions are again equipped with a partial (strict) order ≺ such that p ≺ q if p is a proper prefix of q. The notion of a residual of a β-redex across reduction, i.e. the formalisation of “what happens” to a redex across a reduction, is defined as usual [2]. Recall that a development of a set of redexes U of a λ-term M is a reduction starting from M contracting a residual of a redex in U in each step. Moreover, a development is complete if the set of residuals of redex in U across the development is empty. We have the following. Theorem 2.9 (Finite Developments Theorem). Let M be a λ-term and U a set of redexes of M . All developments of U are finite and there is a unique λ-term N that is the final term of all complete developments of U .
3
Valley Sizes in ARSs and TRSs
We now define the main object of study in this paper: The function vsR . Definition 3.1. Let (A, R) be an ARS that has the Church-Rosser property and let |·| : A −→ N be a function (‘size’) such that for each m ∈ N, the set {a ∈ A : |a| ≤ m} is finite. The valley size vsR : N2 −→ N is defined as vsR (m, n) = l where l is the least number such that for every object a with |a| ≤ m and every peak starting from a with reductions of length at most n there is a corresponding valley with reductions of length at most l.
Least Upper Bounds on the Size of Church-Rosser Diagrams
277
Observe that vsR is well-defined as {a ∈ A : |a| ≤ m} is finite and (A, R) has the Church-Rosser property. The ‘size’ function |·| will depend on the class of ARSs considered. In this paper, we are concerned solely with term rewriting systems and λ-calculus where we consider terms modulo the renaming of (free) variables to ensure {a ∈ A : |a| ≤ m} is finite. We employ |a| ≤ m, and not |a| = m, in the above the definition to ensure that vsR is monotone. Replacing |a| ≤ m by |a| = m gives us a less well-behaved function; The example we give below demonstrates this: vsR (2, 1) would be equal to 1 instead of being equal to vsR (1, 1) = 2. In an ARS with the Church-Rosser property, there will usually be several (or even infinitely many) different valleys that complete the diagram of a specific peak. If the ARS is both Church-Rosser and terminating, a valley can always be found by reducing to normal form (but this may yield a valley with longer reductions than necessary); if the ARS has cycles, there may be an infinite number of possible valleys. The function vsR (m, n) picks the smallest valley for each specific peak, but has to take into account all peaks with a starting term of size (at most) m and reductions of size (at most) n; thus, vsR (m, n) may be larger than what is needed for ‘most’ peaks—it gives the least valley size that will surely work for all terms and peaks limited by m and n. We illustrate the workings of vsR by computing vsR (2, 1) for a small TRS in the following example. Example 3.2. Let R be the TRS ⎧ ⎪ ⎨a → b a→c ⎪ ⎩ a→e
with rules b→d c→a d→a
⎫ d→e ⎪ ⎬ g(x) → h(a) ⎪ ⎭ h(x) → e
This TRS is confluent1 (and normalising, but not terminating2 ). Consider the peak g(b) ← g(a) → g(c). Some valleys completing the diagram are: (i) g(b) → h(a) ← g(c), (ii) g(b) → g(d) → g(a) → g(c), (iii) g(b) → h(a) → e ← h(a) ← g(c), (iv) g(b) → g(d) → h(a) ← g(c), and so on. Observe there are an infinite number of valleys of the form g(b) → g(d) →∗ g(a) → h(a) ← g(a) ∗ ← g(c) and that there is no largest valley completing the diagram. The smallest possible valley is the first of the above: Both reductions have length 1. Note that this valley does not involve normal forms, and that any valley with reductions to normal form involves strictly longer reductions. By definition of the size of terms (Def. 2.5), the term g(a) has size 2, and by inspection, we find that for any peak with reductions of length at most 1 starting from a term of size 2, there is a corresponding valley where each reduction has length at most 1. However, for terms of size 1, there is the peak b ← a → c whose 1 2
The system has the unique normal form property and is weakly confluent (see [15] for details and definitions). Normalisation and termination are also called, respectively, weak normalisation and strong normalisation.
278
J. Ketema and J.G. Simonsen
smallest valleys involve reductions of length 2, e.g. b → d → a ← c. Thus, for peaks involving terms of size at most 2 and reductions of length at most 1, the smallest corresponding valleys involve reductions of length at most 2, and there is a peak that needs a valley with reductions of length 2. Hence, vsR (2, 1) = 2. In the above example, R was a non-orthogonal TRS. We shall see in Sect. 4 that for orthogonal TRSs the term size does not matter ; thus, the first argument of vsR can be dropped in that case. Remark 3.3. The function vsR need not be computable for an ARS: Let h : N −→ N be any non-computable total function, let A = N ∪ N2 , and let |i| = i and |(i, j)| = i + j for all i ∈ N and (i, j) ∈ N2 . Define, for every m ≥ 1 and n > 1: m → (m, 1), m → (m, h(m) + 1), and (m, n) → (m, n − 1). Then, (A, R) has the Church-Rosser property by the last rule, but vsR (|m|, 1) = h(m), whence vsR (m, n) is not computable. 3.1
The Valley Size Is a Computable Function for TRSs
We now show that vsR is computable for arbitrary term rewriting systems R; in fact it is uniformly so: There is a program that, given an encoding of a confluent TRS, returns another program that computes vsR . We give a formal account in the following. Recall that we consider only TRSs with a finite signature and a finite number of rules. As terms are inductively defined, it is clear that every such TRS (Σ, R) can be recursively encoded and decoded as an integer j(Σ,R) . In the remainder of the paper we assume a fixed such encoding and decoding. Theorem 3.4. There is a (partial) computable function g : N3 −→ N such that if j(Σ,R) encodes a TRS (Σ, R) with the Church-Rosser property, then vs(Σ,R) (m, n) = g(j(Σ,R) , m, n) for all m, n. Proof. Let P be a program that does the following: On input (j(Σ,R) , m, n), P decodes j(Σ,R) , builds all terms t1 , . . . , tl (modulo the renaming of variables), of size at most m over Σ, and stores them in memory. Using R and the fact each term has a finite number of one-step reducts, for each ti ∈ {t1 , . . . , tl }, P brute-force applies all rules of R to obtain, after a finite number of steps, every terms, P uses term ti such that ti →≤n ti . Next, for every pair (si , si ) of such R to simultaneously build increasingly larger parts ( V , s ,k i 0≤k≤j 0≤k≤j Esi ,k ) and ( 0≤k≤j Vsi ,k , 0≤k≤j Esi ,k ) of the reduction graphs of si and si . If (Σ, R) has the Church-Rosser a j is reached such that a term ri property, eventually exists that is both in 0≤k≤j Vsi ,k and 0≤k≤j Vsi ,k . The program P stores the least such j for (si , si ). Clearly, the least such j is equal to the number of steps in the longest reduction of the smallest valley of si and si . After iterating over every pair (si , si ), P takes the maximum of the stored lengths and returns it. This value is clearly vs(Σ,R) (m, n). Thus, P computes a function g(j(Σ,R) , m, n) as desired.
Least Upper Bounds on the Size of Church-Rosser Diagrams
279
Theorem 3.5. If (Σ, R) is a TRS having the Church-Rosser property, then vs(Σ,R) is a total computable function. Proof. By Theorem 3.4, we have vs(Σ,R) (m, n) = g(j(Σ,R) , m, n) for all m, n. That vs(Σ,R) is a partial computable function follows immediately by the s-m-n Theorem [12]. That the function vs(Σ,R) is total follows by the fact that vs(Σ,R) is well-defined by the comments below Definition 3.1. 3.2
All Computable Functions Can Be Majorized by Valleys in TRSs
Above we showed that for every TRS (Σ, R) the size vsR is computable; colloquially, we have a very tight computable upper bound on valley sizes. Na¨ıvely, one might conjecture that an even tighter bound is obtainable—e.g. that vs(Σ,R) is always primitive recursive. We now proceed to show this is not possible in a very strong sense: For every computable function ϕ : N −→ N, there is a TRS and a single term of some size m such that vs(Σ,R) (m, n) ≥ ϕ(n) for all n ≥ 2. Encoding Turing Machines. We shall use the following (inconsequential) constraints on the Turing machines we encode: Definition 3.6. All Turing machines are one-head, one-tape machines with no auxiliary input or output tapes. There are no transitions to the initial state qs , nor are there any transitions from the halting state qh . The input and tape alphabets of all Turing machines are {0, 1, } where is ‘blank’ as usual. All inputs are assumed to be given in unary; hence, n ∈ N is encoded as 0n . The initial configuration of a Turing machine will always be in the initial state with the input starting in the tape cell immediately to the right of the read/write head. The machine is assumed never to be stuck on a legal configuration; for every state q ∈ Q \ {qh } and every element b ∈ {0, 1, }, the transition δ(q, b) is defined. We give the standard encoding of [15]. The tape alphabet is modelled by unary function symbols 0, 1 and , respectively. Both tape ends are modelled by the nullary symbol . The representation of the string 011 enclosed on the right by a tape end will thus be 0(1((1()))); the left tape end and position of the read/write head of the machine will be encoded in the TRS rules representing the Turing machine transitions. For each state q ∈ Q of the machine, we assume a binary function symbol q. The TRS induced by the transitions of a Turing machine M is given in Figure 2. For our purposes, we augment Δ(M ) with a constant symbol T and a binary function symbol r. In addition, we augment the rewrite rules of Δ(M ) with the rule set from Figure 3, which extends the rule set from [4, Sect. 5] with the rules r(x, 0y) → r(x, 00y) and r(x, 00y) → r(x, 0y). To prove confluence of ΔC (M ) in the case where M halts on all inputs, we first give a general lemma concerning mutually orthogonal systems. For i ∈ {0, 1} we define i = (i + 1) mod 2.
280
J. Ketema and J.G. Simonsen Rewrite rules induced by transition rules of the Turing machine M (ΔN (M )): (L/R)-move rewrite rules (for each q ∈ Q, a ∈ {0, 1, }) δ(q, b) = (q , b , R) q(x, by) → q (b x, y) δ(q, b) = (q , b , L) q(ax, by) → q (x, ab y) Extra rules (ΔE (M )): (L/R)-move extra rewrite rules (for each q ∈ Q, a ∈ {0, 1, }) δ(q, ) = (q , b , R) q(x, ) → q (b x, ) δ(q, b) = (q , b , L) q(, by) → q (, b y) q(ax, ) → q (x, ab ) δ(q, ) = (q , b , L) q(, ) → q (, b ) Δ(M ) = ΔN (M ) ∪ ΔE (M ) Fig. 2. Basic encoding Δ(M ) of a Turing machine M
Rule for transitioning to T when the halting state has been reached (†): qh (x, y) → T Rules for non-deterministic choice of n ∈ N (Δndt (M )): r(x, ) → T r(, y) → qs (, y) r(x, 0y) → r(0x, y) r(0x, y) → r(x, 0y) r(x, 0y) → r(x, 00y) r(x, 00y) → r(x, 0y) ΔC (M ) = Δ(M ) ∪ {†} ∪ Δndt (M ) Fig. 3. Extra rules for non-deterministic choice and confluence
Lemma 3.7. Let R0 and R1 be mutually orthogonal systems such that for each i ∈ {0, 1} and for each peak t ∗i ← s →∗i t , there either exists a corresponding valley t →∗i s ∗i ← t , or a corresponding valley t →∗i s ∗i ← t . Then, R0 ∪ R1 has the Church-Rosser property. Proof (Sketch). Straightforward tiling of peaks.
Proposition 3.8. If M halts on all inputs, the two systems R0 = Δ(M ) ∪ {(†)} and R1 = Δndt (M ) satisfy the conditions of Lemma 3.7. Proof. Both systems are left-linear and clearly no left-hand side of a rule of R0 overlaps with a left-hand side of a rule of R1 and vice versa, whence the two systems are mutually orthogonal. Also, R0 is orthogonal, hence has the ChurchRosser property. Furthermore, observe that two rules from of Δndt (M ) can only overlap at the root. As there are no collapsing rules in Δndt (M ), we thus obtain confluence if every peak t ∗ ← r(s, s ) →∗ t has a corresponding valley. By
Least Upper Bounds on the Size of Church-Rosser Diagrams
281
inspection of the rules of Δndt (M ), it is seen that if r(s, s ) →∗1 r(t, t ), then r(t, t ) →∗1 r(s, s ). Thus, the only peaks of R1 that do not have corresponding valleys in R1 are the ones on the form T ∗1 ← r(s, s ) →∗1 qs (, t) By inspection of the rules of Δndt (M ), we see that such a peak is only possible if t = 0n . As M halts on all configurations, we obtain qs (, t) →∗0 qh (t , t) →0 T , concluding the proof. Corollary 3.9. If M halts on all inputs, then ΔC (M ) has the Church-Rosser property. We have the following lemma: Lemma 3.10. Let ϕM : N −→ N be a total computable function. Then there is a Turing machine M that (i) halts on all inputs, and (ii) on input 0n halts in at least ϕM (n) steps. Proof. Let M be the Turing machine containing an inlined copy of M and, on input 0n , computes k = ϕM (n), then performs k “idle steps” before halting. As M halts on all inputs, so does M , and by construction M runs for at least ϕM (n) steps before halting. Majorizing a Computable Function with Valleys in a TRS. We now show that for every computable function ϕM : N −→ N, there exists a TRS R having the Church-Rosser property and a term s such that there is a peak of size n with smallest corresponding valley of size ϕM (n). Thus, vs(Σ,R) (m, n) ≥ ϕM (n) for all m ≥ |s|. Theorem 3.11. For every total computable function ϕM : N −→ N, there exists a TRS R having the Church-Rosser property, a ground term s, and a ground normal form s0 of R such that, for every natural number n, there is a term sn with (i) s0 2 ← s →n sn , (ii) sn →∗ s0 , and (iii) every reduction sn →∗ s0 has length at least ϕM (n). s 2
s0
n
/ sn ≥ϕM (n)
s0
Proof. Let M be the Turing machine obtained by applying Lemma 3.10 to ϕM . Then, M halts on all inputs and halts in at least ϕM (n) steps on input 0n for all n ∈ N. We set R = ΔC (M ), s = r(, 0), s0 = T , and sn = qs (, 0n ). For all n ∈ N, we then have s → r(0, ) → T and s →n sn . Observe that R has the Church-Rosser property by Corollary 3.9, and that s is ground. By the fact that each step of Δ(M ) simulates exactly one step of M , we obtain
282
J. Ketema and J.G. Simonsen
that qs (, 0n ) →m qh (t, t ) (for terms t, t ) where m ≥ ϕM (n). As M is deterministic, this is the only possible reduction from qs (, 0n ) to qh (t, t ). Finally, we use rule (†) to obtain qh (t, t ) → T = s0 . Hence, sn →∗ s0 and all such reductions are of length at least ϕM (n). We hence have: Theorem 3.12. For every total computable function ϕM : N −→ N, there is an explicitly constructible TRS (Σ, R) that has the Church-Rosser property and an explicitly constructible ground term s of R such that for all m ≥ |s| vs(Σ,R) (m, n) ≥ ϕM (n).
4
Bounds on Valley Sizes in Orthogonal TRSs
For orthogonal TRSs, much better bounds can be obtained than those presented in the previous section. We shall prove existence, for every TRS R, of a constant μR such that vs(Σ,R) (m, n) ≤ n · (μR )n . Definition 4.1. Let R be a TRS. The parallel rewrite relation ⇒ is defined as follows: s ⇒k t if there is a k-hole context such that (i) s = C[s1 , . . . , sk ], (ii) t = C[t1 , . . . , tk ], and (iii) for all 1 ≤ i ≤ k, we have si → ti . Definition 4.2. The multiplicity of a finite TRS R, denoted μR , is defined as: max
max (1, number of occurrences of x in r)
l→r∈R x∈vars(l)
Thus, the multiplicity of a system is simply the maximum number of times that a variable can occur in a right-hand side of a rule of R. Example 4.3. Let R = {f (x, y) → g(x, x, y), g(x, y, z) → f (x, z)} Then μR = 2 as x occurs twice in the right-hand side of the rule f (x, y) → g(x, x, y), and no variable occurs more often in a right-hand side. Lemma 4.4 (Parallel Moves Lemma with reduction lengths). Let R be an orthogonal TRS and let s be a term. If t m ⇐ s ⇒n t is a peak, then there is a valley t ⇒≤n·μR s ≤m·μR ⇐ t . Proof. Existence of a valley follows by the standard Parallel Moves Lemma [1]. The reduction in t ⇒ s consists of a parallel contraction of the residuals of the redexes contracted in s ⇒n t across contraction of the m redexes in s ⇒m t, and vice versa. The step s ⇒m t consists of m separate →-steps, each contracting a single redex parallel to the other m − 1 redexes. By the definition of the rewrite relation →, every single step using a rule l → r may copy each of its subterms by as many times a variable occurs in r. Each of the n parallel redexes contracted in s ⇒n t may, or may not, occur inside one of the subterms copied by a redex in s ⇒m t. The total number of copies that occur in t is hence bounded from above by n times the maximum number of times that a single variable can occur in the right-hand side of a rule, hence n·μR . The situation with m·μR is symmetrical.
Least Upper Bounds on the Size of Church-Rosser Diagrams
283
Theorem 4.5. Let the TRS R be orthogonal and let s be a term in R with a i j peak t j ← s →i t . Then there is a valley t →≤j·(μR ) s ≤i·(μR ) ← t . Hence, vs(Σ,R) (m, n) ≤ n · (μR )n . Proof. As every →-reduction is also a ⇒-reduction and as ⇒∗ =→∗ , repeated application of Lemma 4.4 allows us to erect the tiling diagram in Figure 4. The result now follows by tallying the number of steps on the right-most and bottommost sides of the diagram. s0,0 = s 1
+3 s0,1
1
1
+3 s1,1
≤1·µR
s2,0 1
+3 s2,1
≤µR ·µR
≤µR ·µR
+3 s2,2 ·
+3 sj−1,1
≤(µR )j−1
sj,0 = t
+3 sj,1
≤(µR )j−1 ·µR
+3
s2,i−1
+3
≤(µR )i−1 ·µR
+3 s2,i
≤µR ·µR
≤(µR )i−1
sj−1,i−1
sj,2 · ≤(µR )j−1 ·µR ≤(µR )j−1 ·µR
+3 s1,i
≤(µR )i−1
+3 ·
≤µR ·µR
≤(µR )i−1 ·µR
≤1·µR
·
≤(µR )j−1
≤1·µR
s1,i−1
·
+3 sj−1,2
≤(µR )j−1
+3 ·
≤µR ·µR
+3 s0,i = t
≤(µR )i−1
≤µR ·µR
·
+3 ·
≤1·µR
1
s0,i−1
≤µR ·µR
≤1·µR
sj−1,0
+3 s1,2
≤1·µR
+3 ·
1
≤µR ·µR
≤1·µR
·
1
≤1·µR
s1,0
+3 s0,2
1
≤(µR )i−1 ·µR
·
+3 sj−1,i
≤(µR )j−1
≤(µR )i−1
sj,i−1
+3
sj,i ≤(µR )j−1 ·µR
≤(µR )i−1 ·µR
=t
Fig. 4. Tiling diagram annotated with reduction lengths for the proof of Theorem 4.5
Remark 4.6. The bounds of the above theorem are tight for non-erasing TRSs (Σ, R) in the following sense: There is an infinite number of terms s such that vs(Σ,R) (|s|, n) = n · (μR )n . Let l → r be a rule such that there is a variable x in l that occurs μR times in r. For j ≥ 0 let sj be the term defined inductively by s0 = l and sj+1 = l[sj ]px where px is the (unique, by left-linearity) position of the variable x in l. For every n ≥ 1, consider the term s2n and the peak obtained by performing (a) a complete development of the n outermost redexes, and (b) the n innermost redexes; observe that both of these reductions are of length precisely n. The (a)-reduction copies the ‘inner’ term sn a total of (μR )n times ending in some term t. The (b)-reduction leaves exactly one copy of each of the top n redexes, ending in some term t . To complete the Church-Rosser diagram, one needs to reach the term obtained by a complete development of all redexes in s2n . From term t , a total of n steps is required to reach this step. From term t, reaching the final term requires the contraction of n redexes in (μR )n parallel subterms, for a total of n · (μR )n steps.
284
5
J. Ketema and J.G. Simonsen
A Bound on Valley Sizes in λ-Calculus
In λ-calculus we cannot expect the valley size vsΛ (m, n) to be independent of m as in Theorem 4.5: In λ-calculus, the growth rate of terms across β-steps depends on the number of bound variables in the original term. Hence, as the size of the valleys is determined by the number of copies of redexes, vsΛ (m, n) must thus depend on m. Of the many available proofs of the Church-Rosser property for λ-calculus, the one most amenable to analysis of reduction lengths consists of “tiling a peak” with commuting squares of so-called complete developments of sets of redexes in a single term; the construction is essentially the same as the one depicted by figure in the proof of Theorem 4.5 (indeed, the figure is often called a tiling diagram [15]), except that for λ-calculus, the “parallel reduction” relation used in each square is a complete development of a set of redexes in a single term. An analysis of this proof reveals vsΛ (m, n) to be bounded from above by a function in the fourth level, E4 , of the Grzegorczyk hierarchy, roughly corresponding to limited recursion on iterated exponentiation, also called tetration—a typical function ···2 is n → 22 (2 taken to the power of itself n times). Indeed, considering the special case of the so-called “Strip Lemma” where one reduction in the peak has length 1 and the other length k (see Lemma 5.3), na¨ıve analysis yields a k 2·|Mi,0 |2 +k
for the length of the reduction Mi+1,0 →∗ Mi+1,k . We bound |Mi,0 |2 give a somewhat better bound in the present section; this bound is still in E4 , 2k
but much less than the bound obtained by na¨ıve analysis: |Mi,0 |2 +k for the Strip Lemma. Upper bounds on the length of developments [3] and standard reductions [16] have been investigated in the literature, as have lower bounds for normalising reductions in typed systems [14]; the present paper is the first study of the size of Church-Rosser diagrams in λ-calculus. Proposition 5.1. Let M0 → M1 → · · · Mn−1 → Mn be a reduction of length n ≥ 0, and let u be a redex in M0 . For each position p ∈ pos(Mn ), at most 2n residuals of u occur in Mn at prefix positions of p. Proof (Sketch). By induction on n.
Lemma 5.2. Let M be a term and U a set of redexes in M . Suppose for each p ∈ pos(M ) that at most i ≥ 0 other redexes from U occur at prefix positions of 2·i p. Then contracting all redexes in U yields a term of at most size |M |2 . Proof (Sketch). By induction on i. 5.1
Bounds for the Strip Lemma
Lemma 5.3 (Strip Lemma with term sizes and reduction lengths). Let k ≥ 1 and consider the peak Mi+1,0 β ← Mi,0 →β Mi,1 →β Mi,2 →β · · · Mi,k−1 →β Mi,k .
Least Upper Bounds on the Size of Church-Rosser Diagrams
285
Then we may obtain a valley by tiling the peak using the Finite Developments Theorem in the following way: Mi,0 1
Mi+1,0
/ Mi,1
1
∗ ∗
/ Mi,2
1
∗
∗
/ Mi+1,1
1
∗
/ Mi+1,2
/ Mi,k
1
Mi,k−1
∗
/ Mi+1,k
∗
Mi+1,k−1
where the following holds for 1 ≤ j ≤ k: 2j+1 +j
j
1. |Mi,j | ≤ |Mi,0 |2 and |Mi+1,j | ≤ |Mi,0 |2 2. The reduction
Mi+1,j−1 →∗β
, and 2j +j−1
Mi+1,j has length at most |Mi,0 |2
Moreover, the reduction Mi+1,0 →∗β Mi+1,k has length at most |Mi,0 |2
.
2k +k
.
Proof. If P →β Q, then |Q| ≤ |P |2 . Hence, straightforward induction shows k that |Mi,k | ≤ |Mi,0 |2 . Let u be the redex contracted in Mi,0 →β Mi+1,0 . By Proposition 5.1, the number of residuals of u along any path from the root to a leaf of Mi,k is at most 2k . Observe that the reduction Mi,k →∗β Mi+1,k is a complete development of U = u/(Mi,0→∗β Mi,k ). Then, Lemma 5.2 and the first part of the lemma yield k
2·2k
|Mi+1,k | ≤ (|Mi,0 |2 )2
= |Mi,0 |2
k
k+1
·22
= |Mi,0 |2
2k+1 +k
.
The reduction Mi+1,j−1 →∗β Mi+1,j is a complete development of a set of residuals of the single redex contracted in Mi,j−1 →β Mi,j , and an innermost development has length bounded from above by the size of Mi+1,j−1 ; by the 2j +j−1
previous item of the lemma, that size is at most |Mi,0 |2 . By the previous parts of the lemma, the length of the entire bottom reduction Mi+1,0 →∗β Mi+1,k is then bounded from above by k
|Mi,0 |2
2j +j−1
2k +k−1
≤ 2 · |Mi,0 |2
2k +k
≤ |Mi,0 |2
,
j=1
completing the proof. 5.2
Valley Sizes in λ-Calculus Are in E4
Lemma 5.4. Consider the following family of peaks (for l, k ≥ 0): Ml,0 β ← · · · β ← M1,0 β ← M0,0 →β M0,1 →β · · · →β M0,k and write m = |M0,0 |. Then, in the tiling of the peak with complete developments, the length, bl (l, k, m) of the bottom side of the tiling diagram satisfies the following recursion inequality
286
J. Ketema and J.G. Simonsen
bl (l, k, m) ≤
k m
if l = 0 bl(l−1,k,m) +bl(l−1,k,m)+l
22
if l > 0
Proof. The tiling diagram may be viewed as m versions of the Strip Lemma (horizontal tiling) stacked on top of each other. The result now follows by a simple induction using Lemma 5.3 (observing for 1 ≤ i < l that the upper left i−1 term in the ith copy of the Strip Lemma has size |Mi,0 | ≤ m2 ). Theorem 5.5. There is a function g(w, n) : N2 −→ N in the fourth level, E4 , of the Grzegorczyk hierarchy such that vsΛ (w, n) ≤ g(w, n). Proof. The right-hand side of the recurrence equation of Lemma 5.4 involves composition of addition, multiplication and exponentiation, applied to limited recursion on the function bl (m, k, w) being defined. As addition, multiplication and exponentiation are at the first, second, and third levels of the Grzegorczyk hierarchy, hence a fortiori in E3 , the function g(w, n) = bl (n, n, m) is in E4 . We are currently unable to exhibit a λ-term with a peak of size n such that the corresponding valley size is more than singly exponential. The reader should note that while performing projections of the reductions in a peak across each other may yield reductions of extreme length, the projections will usually be equivalent to much shorter reductions and will only give rise to ‘small’ values of vsΛ (m, n).
6
Conclusion and Conjectures
We have performed the first fundamental study of the size of Church-Rosser diagrams in TRSs and λ-calculus. For orthogonal TRSs, bounds on valleys turn out to be exponential in a constant dependent on the rewrite system, and thus potentially tractable; for non-orthogonal systems, we showed that for every computable total function, there are TRSs with valley sizes majorizing the function. For λ-calculus, we gave an upper bound on valley sizes. Our inability to construct terms that saturate the upper bounds derived in Section 5 suggests that vsΛ (m, n) may be in E3 . We conjecture that the dependence on term size |s| in the bound given for arbitrary TRSs in Section 3.1 can be removed; we are currently unable to do so. Finally, the question of valley sizes for higher-order rewriting systems must be investigated; bounds for such systems will automatically lead to bounds for deduction systems in first- and higher order logics, as well as for higher-order functional programs. Acknowledgements. We would like to thank Richard Statman and Andrzej Filinski for providing valuable answers to some of our questions.
Least Upper Bounds on the Size of Church-Rosser Diagrams
287
References 1. Baader, F., Nipkow, T.: Term Rewriting and All That. Cambridge University Press, Cambridge (1998) 2. Barendregt, H.P.: The Lambda Calculus: Its Syntax and Semantics. In: Studies in Logic and the Foundations of Mathematics, rev. edn., vol. 103. North-Holland, Amsterdam (1985) 3. de Vrijer, R.: A direct proof of the finite developments theorem. Journal of Symbolic Logic 50(2), 339–343 (1985) 4. Endrullis, J., Geuvers, H., Zantema, H.: Degrees of undecidability in term rewriting. In: Gr¨ adel, E., Kahle, R. (eds.) CSL 2009. LNCS, vol. 5771, pp. 255–270. Springer, Heidelberg (2009) 5. Fernandez, M.: Models of Computation: An Introduction to Computability Theory. Undergraduate topics in computer science. Springer, Heidelberg (2009) 6. Grzegorczyk, A.: Some classes of recursive functions. Rozpr. Mat. 4, 1–45 (1953) 7. Jones, N.D.: Computability and Complexity from a Programming Perspective. MIT Press, Cambridge (1997) 8. Khasidashvili, Z.: The longest perpetual reductions in orthogonal expression reduction systems. In: Matiyasevich, Y.V., Nerode, A. (eds.) LFCS 1994. LNCS, vol. 813, pp. 191–203. Springer, Heidelberg (1994) 9. Klop, J.W.: Term rewriting systems. In: Abramsky, S., Gabbay, D., Maibaum, T. (eds.) Handbook of Logic in Computer Science, vol. 2, pp. 1–116. Oxford University Press, Oxford (1992) 10. Odifreddi, P.: Classical Recursion Theory. Studies in Logic and the Foundations of Mathematics, vol. II, 143. North-Holland, Amsterdam (1999) 11. Papadimitriou, C.: Computational Complexity. Addison-Wesley, Reading (1994) 12. Rogers Jr., H.: Theory of Recursive Functions and Effective Computability. MIT Press, Cambridge (1987) 13. Sipser, M.: Introduction to the Theory of Computation, 2nd edn. Thomson Course Technology (2006) 14. Statman, R.: The typed lambda calculus is not elementary recursive. Theoretical Computer Science 9, 73–81 (1979) 15. Terese: Term Rewriting Systems. In: Cambridge Tracts in Theoretical Computer Science, vol. 55. Cambridge University Press, Cambridge (2003) 16. Xi, H.: Upper bounds for standardizations and an application. Journal of Symbolic Logic 64(1), 291–303 (1999)
Proving Injectivity of Functions via Program Inversion in Term Rewriting Naoki Nishida and Masahiko Sakai Graduate School of Information Science, Nagoya University Furo-cho, Chikusa-ku, Nagoya 464-8603, Japan {nishida,sakai}@is.nagoya-u.ac.jp
Abstract. Injectivity is one of the important properties for functions while it is undecidable in general and decidable for linear treeless functions. In this paper, we show new sufficient conditions for injectivity of functions in term rewriting, which are based on program inversion. More precisely, we show that functions defined by non-erasing, convergent and sufficiently complete constructor rewrite systems are injective if the corresponding inverse-computation rewrite systems generated by the inversion framework are innermost-confluent. By using this property, we also show a syntactic sufficient-condition of injectivity.
1
Introduction
Injectivity is one of the important properties for functions. However, injectivity is undecidable in general [5] but known to be decidable for functions defined in linear treeless forms [12]: a function definition f (u1 , . . . , un ) = t is treeless if every subterm of t rooted by a defined function has only variables as its arguments, e.g., t has no nest of defined functions. The class of linear treeless functions is not so practical because any nested function call is not allowed. The approach of this paper follows from the observation that injectivity of target functions is guaranteed by functionality of the corresponding inverse relations, i.e., the existence of functions defining the inverse computation of the target functions. The inverse computation of an n-ary function f is, given an output v, the calculation of the possible input v1 , . . . , vn such that f (v1 , . . . , vn ) = v. Two approaches to inverse computation are distinguished [1]: inverse interpreters [4,1] that perform inverse computation, and inversion compilers [14,26,10,21,22,8,9,15,16,2,13] that perform program inversion. For example, given the definition of the above f , inversion compilers generate a definition of g such that g(v) = (v1 , . . . , vn ). Especially, program inversion is investigated for injective functions in order to generate deterministic or confluent systems as inverse-computation programs [10,21,8,2,13]. In this paper, we show a general sufficient condition for injectivity of functions via program inversion in term rewriting, i.e., non-erasing and convergent functions are injective if there exist the corresponding inverse functions defined by innermost-confluent rewrite systems. We also show an instance of the sufficient M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 288–303, 2010. c Springer-Verlag Berlin Heidelberg 2010
Proving Injectivity of Functions via Program Inversion in Term Rewriting
289
condition via the inversion framework proposed in [21,22], and show a decidable sufficient condition for injectivity, that is based on a syntactic property of given rewrite systems. Moreover, we show that the restricted completion procedure proposed in [18] is sometimes useful in transforming non-confluent inverse systems into confluent ones, i.e., the procedure can help to prove injectivity. Methods to verify injectivity are helpful in the area of inverse computation because injectivity is often assumed to target functions in program inversion. One may feel that the approach in this paper is a vicious circle because inversion methods for injective functions are used to prove the injectivity. However, this is not true because at least the inversion method used in this paper does not require injectivity of input functions in its mechanism. Moreover, as far as we know, there is no inversion method that succeeds in generating a confluent (or deterministic) inverse program for every injective function. Thus, methods to prove injectivity are useful in program inversion itself and its development even though the methods depend on the inversion techniques. This paper is organized as follows. Section 2 prepares notations of term rewriting. Section 3 gives the definition of inverse computation in term rewriting. In Section 4, we discuss general conditions for injectivity. In Section 5, we review the inversion framework for constructor rewrite systems. In Section 6, we show a decidable sufficient condition for injectivity, that is based on program inversion in Section 5. Section 7 concludes this paper. Throughout this paper, we assume that given rewrite systems have the sufficiently complete property that is often assumed to practical functions. The assumption is done for the sake of simplicity and hence it is not essential. In addition, this paper does not consider sorts. However, the results in this paper can be extended to many-sorted systems as usual.
2
Preliminaries
Here, we will review the following basic notations of term rewriting [3,24]. Throughout this paper, we use V as a countably infinite set of variables. The set of all terms over a signature F and V is denoted by T (F , V). The set of all variables appearing in terms t1 , . . . , tn is represented by Var(t1 , . . . , tn ). The identity of terms s and t is denoted by s ≡ t. For a term t and a position p of t, the notation t|p represents the subterm of t at p and we denote the subterm relation between t and t|p by t t|p . Moreover, we write t t|p if p = ε. The root function symbol of t is denoted by root(t). The notation C[t1 , . . . , tn ]p1 ,...,pn represents the term obtained by replacing each at position pi of an n-hole context C[ ] with term ti for 1 ≤ i ≤ n. The application σ(t) of a substitution σ to t is abbreviated to tσ. The composition σθ of substitutions σ and θ is defined as σθ(x) = θ(σ(x)). Given terms s and t, we write s ∼ t if there is some substitution θ such that s tθ. An (oriented) conditional rewrite rule over F is a triple (l, r, c), denoted by l → r ⇐ c, such that l is a non-variable term in T (F , V), r is a term in T (F , V), and c is of the form s1 → t1 ∧ · · · ∧ sn → tn (n ≥ 0) where terms si and
290
N. Nishida and M. Sakai
ti are in T (F , V). In particular, the conditional rewrite rule is said to be an (unconditional) rewrite rule if n = 0, and we may abbreviate it to l → r. We sometimes attach a unique label ρ to a rule l → r ⇐ c by denoting ρ : l → r ⇐ c, and we use the label to refer to the rule. An (oriented) conditional rewriting system (CTRS, for short) R over a signature F is a finite set of conditional rewrite rules over F . Note that R is a TRS if every rule in R is unconditional. The rewrite relation of R is denoted by − →R . A conditional rewrite rule ρ : l → r ⇐ s1 → t1 ∧· · ·∧sk → tk is called deterministic if Var(r) ⊆ Var(l, t1 , . . . , tk ) and Var(si ) ⊆ Var(l, t1 , . . . , ti−1 ) for 1 ≤ i ≤ k. A conditional rewrite rule l → r ⇐ c is called linear if the both-hand sides l and r are linear. An unconditional rewrite rule l → r is called non-erasing if Var(l) = Var(r). We say that a CTRS has a property P if all of its rules have the property P . We call a deterministic CTRS a DCTRS for short. Roughly speaking, DCTRSs corresponds to functions defined by first-order functional languages having if-then-else and let structures. Let R be a DCTRS. The binary relation − →⊆ − →R is said to be confluent if ∗ ∗ ∗ ∗ ← −·− → ⊆ − →·← −. R is said to be confluent if − →R is confluent. R is said to be terminating if there is no infinite reduction sequence t1 − →R t2 − →R · · ·. A notion of operational termination of DCTRSs is defined via the absence of infinite wellformed proof trees in some inference system [11]: a CTRS R is operationally terminating if for any terms s and t, any proof tree attempting to prove that s ∗ − →R t cannot be infinite. Let → be a reduction over terms in T (F , V). Then, the ∗ set of normal forms w.r.t. → is denoted by NF → (F , V). The binary relation − →! ∗ is defined as { (s, t) | s − → t, t ∈ NF → (F , V) }. Let R be a DCTRS over a signature F . The sets DR and CR of defined symbols and constructors of R are defined as DR = {root(l) | l → r ⇐ c ∈ R} and CR = F \ DR , respectively. Terms in T (CR , V) are called constructor terms of R. The DCTRS R is called a constructor system if every rule f (t1 , . . . , tn ) → r ⇐ c in R satisfies {t1 , . . . , tn } ⊆ T (CR , V). Roughly speaking, constructor systems correspond to first-order functional programs with pattern matching. Terminating and confluent TRSs are called convergent . A TRS R over a signature F is said to be sufficiently complete if every ground term s can be reduced ∗ to a constructor term, i.e., s − →R t ∈ T (CR ). Note that NF − →R (F ) = T (CR ) if R is sufficiently complete. R is said to be quasi-reducible if every term f (t1 , . . . , tn ) with f ∈ DR and t1 , . . . , tn ∈ T (CR ) is a redex of R. It is known that every terminating and quasi-reducible constructor TRS is sufficiently complete, and quasi-reducibility is decidable. Let li → ri (i = 1, 2) be two rules whose variables have been renamed such that Var(l1 , r1 ) ∩ Var(l2 , r2 ) = ∅. Let p be a position in l1 such that l1 |p is not a variable and let θ be a most general unifier of l1 |p and l2 . Then, the pair r1 θ, (C[r2 ]p )θ is called a critical pair where l1 ≡ C[l1 |p ]p . If the two rules are obtained by renaming the same rewrite rule, we do not consider the case p = ε. If p = ε, then the critical pair is called an overlay. A critical pair s, t is called trivial if s ≡ t. If two rules give rise to a critical pair, we say that they overlap. We denote the set of critical pairs constructed by rules in a TRS R by
Proving Injectivity of Functions via Program Inversion in Term Rewriting
291
CP (R). We also denote the set of critical pairs between rules in TRSs R and R by CP (R, R ). Moreover, CP ε (R) denotes the set of overlays of R. R is called non-overlay (or root-non-overlapping) if R has no overlay. A TRS R is called non-overlapping if R has no overlap. Let R and R be CTRSs such that their normal forms are well-defined, and T be a set of terms. Roughly speaking, R is computationally equivalent to R w.r.t. T [28] if, whenever R terminates on a term s ∈ T admitting a unique normal form t, there exist mappings φ and ψ such that – R also terminates on φ(s), and – for any of its normal forms t , we have ψ(t ) = t. In this paper, we assume that φ and ψ are the identity mapping. Let − and − be two binary relations on terms, and T and T be sets of → → 1 2 = − in T × T (− ⊇ − in T × T , respectively) if terms. We say that − → → → → 1 2 1 2 ∩ (T × T ) = − ∩ (T × T ) (− ∩ (T × T ) ⊇ − ∩ (T × T ), respectively). − → → → → 1 2 1 2 = − in T (and − ⊇ − in T ) if T = T . Especially, we say that − → → → → 1 2 1 2 An equation over a signature F is a pair (s, t), denoted by s ≈ t, such that s and t are terms in T (F , V). We write s t for representing s ≈ t or t ≈ s. The equational relation w.r.t. a set E of equations is defined as ↔E = {(C[sσ], C[tσ]) | s t ∈ E }.
3
Inverse Computation in Term Rewriting
In this section, we recall inverse computation in term rewriting [22]. We prepare special constructors tp0 , tp1 , · · · to denote the tuple (t1 , . . . , tn ) of terms t1 , . . . , tn by tpn (t1 , . . . , tn ). For the sake of simplicity, we may abbreviate tpn (t1 , . . . , tn ) to (t1 , . . . , tn ). The reason why such symbols are introduced is that inverses of n-ary functions return tuples of some terms. We only consider the innermost reduction that corresponds to call-by-value evaluation. Let R be an operationally terminating DCTRS. The n-level operationally innermost reduction −−−→R is defined as follows [21,18]: (n),i
– −−−→R = ∅, and (0),i
– −−−−−→R = −−−→R ∪ { (C[lσ], C[rσ]) | l → r ⇐ s1 → t1 ∧ · · · sk → tk ∈ (n+1),i
(n),i
∗
! R, ∀u lσ. u ∈ NF − →R (F , V), ∀i. si σ −−−→R ti σ }. (n),i
The operationally innermost reduction → −i R of R is defined as j≥0 −−− → . (j),i R is equivalent to the ordinary innermost Note that if R is a TRS then − → i R reduction. Note that the ordinary definition of innermost reduction is not welldefined for every CTRS [7]. However, both the ordinary and operationally innermost reductions of operationally terminating CTRSs are well-defined. Roughly speaking, the operationally innermost reduction corresponds to the call-by-value evaluation of functional programs having let structures. R is called innermostis confluent. confluent if − → i R
292
N. Nishida and M. Sakai
Let R be a DCTRS over a signature F . The binary relation between input and output of an n-ary defined symbol f ∈ DR w.r.t. − → ⊆ − →R over T ⊆ T (F , V), denoted by Φ − → (f, T ), is defined by {(tpn (t1 , . . . , tn ), tpm (u1 , . . . , um )) | ∗ t1 , . . . , tn , u1 , . . . , um ∈ T, f (t1 , . . . , tn ) − → tpm (u1 , . . . , um ) }. Note that the out∗ put of f is of the tuple with m objects. For m = 1, f (t1 , . . . , tn ) − → tp1 (u1 ) may ∗ be replaced by f (t1 , . . . , tn ) − → u1 , i.e., Φ − → (f, T ) = { (tpn (t1 , . . . , tn ), tp1 (u1 )) | ∗ t1 , . . . , tn , u1 ∈ T, f (t1 , . . . , tn ) − → u1 }. Here we give the definition of inverses w.r.t. the innermost reduction. Definition 1 (inverse system). Let R and S be DCTRSs over a signature F and G, respectively, such that CR ⊆ CS . Let f and g be defined symbols of R and S, respectively. Then, g is an inverse of f (w.r.t. the innermost reduction) if Φ − →S (g, T (CR )) is the inverse relation of Φ − →R (f, T (CR )) (i.e., for all terms i
∗
i
t1 , . . . , tn , u1 , . . . , um ∈ T (CR ), f (t1 , . . . , tn ) − tpm (u1 , . . . , um ) 1 if and only → i R ∗ 2 if g(u1 , . . . , um ) − tpn (t1 , . . . , tn ) ). Moreover, the DCTRS S is called an → i S inverse system of R w.r.t. f , and simply called an inverse system of R if it is an inverse system of R w.r.t. all defined symbols of R. Definition 1 is a simple extension of the definition in [22]. Example 2. Consider the following convergent constructor TRSs over the signature {0, s, twice, half}: twice(0) → 0, half(0) → 0, R1 = R2 = twice(s(x)) → s(s(twice(x))) half(s(s(x))) → s(half(x)) R2 is an inverse TRS of R1 w.r.t. twice.
4
General Conditions of Injectivity via Inversion
In this section, we first show a necessary and sufficient condition for injectivity of functions defined by TRSs, even though it is not practical. After that, we then discuss practical sufficient conditions for injectivity. We define injectivity of functions in term rewriting as follows. Definition 3 (injectivity). Let R be a convergent and sufficiently complete constructor TRS over a signature F . A defined symbol f of R is called injective (w.r.t. ground constructor terms) if the binary relation Φ − (f, T (CR )) is → i R injective. Note that injectivity in this paper is not defined for semantics but defined for given function definitions. Injectivity of functions coincides with functionality of their inverse relations. Here, we call a binary relation functional if the relation is a mapping that may not be surjective. 1 2
∗
∗
For m = 1, f (t1 , . . . , tn ) − tpm (u1 ) may be replaced by f (t1 , . . . , tn ) − u1 . → → i R i R ∗ ∗ For n = 1, g(u1 , . . . , um ) − tp (t ) may be replaced by g(u , . . . , u ) t1 . → − → 1 1 m S S n i i
Proving Injectivity of Functions via Program Inversion in Term Rewriting
293
Proposition 4. Let R be a DCTRS over a signature F , respectively. Then, f is injective (i.e., Φ − (f, T (CR )) is injective) if and only if the inverse relation → i R (f, T (C )) is a mapping. of Φ − R →R i
If the corresponding inverse systems are defined, then the inverse relation of (f, T (CR )) coincides with the relation Φ − (g, T (CR )) between input and Φ− → → i R i S output of the inverse g of f . Then, injectivity is equivalent to functionality of the inverses. Proposition 5. Let R and S be DCTRSs over signatures F and G, respectively. Let f and g be defined symbols of R and S, respectively, such that g is an inverse of f . Then, f is injective if and only if Φ − →S (g, T (CR )) is a mapping. i
Proposition 5 can be transformed into the following sufficient condition for injectivity, that is based on the existence of inverse systems. Corollary 6. Let R be a DCTRS over a signature F . If there exists a DCTRS S that defines an inverse g of f such that Φ − (g, T (CR )) is a mapping, then a → i S defined symbol f of R is injective. The converse of Corollary 6 for non-erasing constructor TRSs because the corresponding inverse DCTRSs can be generated by the inversion framework in [22]. In order to show injectivity of f , it is necessary to show functionality of g over T (CR ), that can be guaranteed by the following condition. Proposition 7. Let S be a DCTRS over a signature G, and T ⊆ NF − →S (G). If S is innermost-confluent, then Φ − →S (g, T ) is a mapping.
i
i
Proof. Suppose that Φ − →S (g, T ) is not a mapping and S is innermost-confluent. i
Then, there exist terms t1 , . . . , tm in T such that {tpn (u1 , . . . , un ) | u1 , . . . , un ∈ ∗ T, g(t1 , . . . , tm ) − tpn (u1 , . . . , un )} is not a singleton. Thus, g(t1 , . . . , tm ) has → i S at least two different normal forms in T . This contradicts confluence of S.
From Corollary 6 and Proposition 7, we obtain the following sufficient condition for injectivity. Theorem 8. Let R be a DCTRS over a signature F . If there is an innermostconfluent DCTRS S that defines an inverse g of f , then a defined symbol f of R is injective. (G) because CR ⊆ CS from the Proof. It holds that T (CR ) ⊆ T (CS ) ⊆ NF − → i S definition of inverse systems. Thus, this theorem follows from Corollary 6 and Proposition 7. Note that it is not known whether the converse of Theorem 8 holds or not. Example 9. Consider the TRS R1 in Example 2 again. The function twice defined by R1 is injective because the inverse TRS R2 of R1 w.r.t. twice is non-overlay (i.e., innermost-confluent [25]). From the above discussion, program inversion techniques are useful in proving injectivity of functions (see Section 6).
294
N. Nishida and M. Sakai
constructor TRS R
-
inversion Inv
- unraveling U
U(Inv(R)) terminates?
H - H H y- completion H
convergent inverse TRS
- R
n
- U(Inv(R))
inversion compiler [21]
non-terminating inverse TRS
Fig. 1. Overview of the inversion framework [21,22,18]
5
Program Inversion of Constructor TRSs
In this section, we review the inversion framework for constructor TRSs [21,22,18] (Fig. 1). Given a constructor TRS, we first generate a DCTRS that is an inverse system of the given TRS. We then transform the DCTRS into a TRS, using Ohlebusch’s unraveling transformation. Finally, if the unraveled TRS is terminating but not innermost-confluent, then we apply the restricted completion procedure to the unraveled TRS in order to obtain an innermost-confluent inverse TRS of the given TRS. We assume that given TRSs are non-erasing, convergent and sufficientlycomplete constructor systems. The non-erasingness is necessary to obtain TRSs without extra variables (variables in the right-hand sides but not in the lefthand side). We call non-erasing and sufficiently complete TRSs NE-SC-TRSs. Consider the TRS { f(x) → g(x, 0), g(x, y) → x }. The erasingness of g w.r.t. the second argument has no effect on computation of f. By assuming non-erasingness, we can exclude TRSs having such a rule from given TRSs. For the sake of simplicity, we prefer the inversion method in [22] to the method for partial inversion in [21] because the definition in [22] is much simpler than the definition in [21]. The results of the methods in [21] and [22] are equivalent if for every rule l → r ∈ R and for every subterm g(t1 , . . . , tn ) of r with g ∈ DR , every ti contains a variable. 5.1
Inversion of Constructor TRSs into DCTRSs
In this subsection, we describe the first stage of the inversion framework proposed in [22]. For a defined symbol f , we use a function symbol f for representing an inverse of f . We first provide an auxiliary definition that is necessary to show the first stage of the inversion framework. Definition 10 ([22]). Let F (= C D) be a signature. The procedure T , which outputs a pair of a term and a condition part from an input term, is inductively defined as follows: 1. T (x) = x, where x is a variable, 2. T (c(t1 , . . . , tn )) = c(u1 , . . . , un ), Cond1 ∧ · · · ∧ Condn where c ∈ C and T (ti ) = ui , Condi for 1 ≤ i ≤ n,
Proving Injectivity of Functions via Program Inversion in Term Rewriting
295
3. T (f (t1 , . . . , tn )) = y, f (y) → tpn (u1 , . . . , un ) ∧ Cond1 ∧ · · · ∧ Condn where f ∈ D, y is a ‘fresh’ 3 variable, and T (ti ) = ui , Condi for 1 ≤ i ≤ n where we write to represent the empty sequence of conditions. Clearly, the above procedure T always terminates. Note that u ∈ T (C, V) for T (t) = u, Cond . We finally define the inversion of constructor TRSs into DCTRSs. Definition 11 (inversion Inv [22]). Let R be a sufficiently complete constructor TRS over a signature F . For a rewrite rule ρ : f (t1 , . . . , tn ) → r ∈ R, its corresponding conditional rewrite rule InvRule(ρ) of f is defined as follows: InvRule( f (t1 , . . . , tn ) → r ) = f (u) → tpn (t1 , . . . , tn ) ⇐ Cond where T ( r ) = u, Cond and (Var(u, Cond) \ Var(r)) ∩ Var(t1 , . . . , tn ) = ∅. The inverse CTRS Inv(R) of R is defined as follows 4 : Inv(R) = { InvRule(ρ) | ρ : f (u1 , . . . , un ) → r ∈ R } The extended signature F is defined as F = F {f | f ∈ DR } {tpi | 0 ≤ i ≤ k} where k is the maximum arity of defined symbols in DR . It is clear that InvRule(ρ) is exactly a deterministic conditional rewrite rule and then Inv(R) is exactly a DCTRS over the signature F . Example 12. Consider the following TRS: ⎧ ⎫ rev(nil) → nil, ⎪ ⎪ ⎪ ⎪ ⎨ ⎬ rev(cons(x, xs)) → snoc(rev(xs), x)), R3 = snoc(nil, y) → cons(y, nil), ⎪ ⎪ ⎪ ⎪ ⎩ ⎭ snoc(cons(x, xs), y) → cons(x, snoc(xs, y)) rev(xs) produces the reverse list of the list xs using snoc, and snoc(xs, y) produces the list obtained from the list xs by adding y as the last element. This TRS is inverted by Inv as follows: Inv(R ⎧ 3) = ⎪ ⎪ ⎨
⎫ rev(nil) → (nil), ⎪ ⎪ ⎬ rev(ys) → (cons(x, xs)) ⇐ snoc(ys) → (zs, x) ∧ rev(zs) → (xs), ⎪ ⎪ ⎪ snoc(cons(y, nil)) → (nil, y), ⎪ ⎩ ⎭ snoc(cons(x, ys)) → (cons(x, xs), y) ⇐ snoc(ys) → (xs, y)
3
4
This means that y ∈ n i=1 Var(ti , ui , Condi ), and in the both cases 2 and 3, variables introduced in each T (ti ) = ui , Condi are disjoint, i.e., (Var(ui , Condi ) \ Var(ti )) ∩ Var(tj , uj , Condj ) = ∅ for i = j. Special rules of the form f (f (x1 , . . . , xn )) → tpn (x1 , . . . , xn ) [22] are not necessary for inverse computation of sufficiently complete TRSs R because they are necessary for normal forms of R rooted by f and there are no such normal forms.
296
N. Nishida and M. Sakai
In the process of constructing Inv(R), we do not allow to abbreviate tp1 (t) into t. In some cases, such an abbreviation does not preserve termination. Here, we call a DCTRS S strongly non-erasing if every rule l → r ⇐ s1 → t1 ∧ · · · ∧ sk → tk ∈ S satisfies all of the following conditions [20,21]: – Var(l) ⊆ Var(r, s1 , t1 , . . . , sk , tk ), and – Var(ti ) ⊆ Var(r, si+1 , ti+1 , . . . , sk , tk ) for 1 ≤ i ≤ k. Proposition 13 ([22]). Let R be a convergent constructor NE-SC-TRS over a signature F . Then, Inv(R) is a strongly non-erasing constructor DCTRS such that CR ⊆ CInv(R) and NF − →R (F ) ⊆ T (CInv(R) ) ⊆ NF − →Inv(R) (F ). The following theorem shows that the DCTRS obtained by Inv from a given constructor TRS can perform the inverse computation of innermost derivations of the TRS. Theorem 14 ([22]). Let R be a convergent constructor NE-SC-TRS over a signature F , f ∈ DR and t, t1 , . . . , tn be terms in T (CR ). Then, f (t1 , . . . , tn ) ∗ t if and only if f (t) − tpn (t1 , . . . , tn ). − → → i R i Inv(R) Thus, Inv(R) is exactly an inverse system of R in the sense of Definition 1. Analysis of DCTRS properties is more difficult than that of TRS properties. For this reason, in the next subsection, we transform inverse DCTRSs into TRSs by using Ohlebusch’s unraveling transformation. 5.2
Unraveling of Inverse DCTRSs into Inverse TRSs
In this subsection, we introduce Ohlebusch’s unraveling transformation of DCTRSs into TRSs, and we also show that the transformation is simulation-sound for inverse DCTRSs generated by Inv. We first give the definition of Ohlebusch’s unraveling [23]. Given a finite set X → − of variables, we denote by X the sequence of variables in X without repetitions (in some fixed order). Definition 15 ([23,24]). Let S be a DCTRS over a signature G. For every conditional rewrite rule ρ : l → r ⇐ s1 → t1 ∧ · · · ∧ sk → tk ∈ S, let |ρ| denote the number k of conditions in ρ. For every conditional rule ρ ∈ S, we prepare k ‘fresh’ function symbols U1ρ , . . . , Ukρ not in G, called U symbols, in the transformation. We transform ρ into a set U(ρ) of k + 1 unconditional rewrite rules as follows:
− → − → − → −→ U(ρ) = l → U1ρ (s1 , X1 ), U1ρ (t1 , X1 ) → U2ρ (s2 , X2 ), · · · , Ukρ (tk , Xk ) → r where Xi = Var(l, t1 , . . . , ti−1 ). The system U(S) = ρ∈R U(ρ) is a TRS over ρ the extended signature GU = G {Ui | ρ ∈ R, 1 ≤ i ≤ |ρ|}.
Proving Injectivity of Functions via Program Inversion in Term Rewriting
297
Example 16. The DCTRS Inv(R3 ) in Example 12 is unraveled as follows: ⎧ ⎫ rev(nil) → (nil), ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ rev(ys) → U (snoc(ys), ys), ⎪ ⎪ 1 ⎪ ⎪ ⎪ ⎪ ⎪ U1 ((zs, x), ys) → U2 (rev(zs), ys, zs, x), ⎪ ⎨ ⎬ U(Inv(R3 )) = U2 ((xs), ys, zs, x) → (cons(x, xs)), ⎪ ⎪ ⎪ ⎪ snoc(cons(y, nil)) → (nil, y), ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ snoc(cons(x, ys)) → U (snoc(ys), x, ys), ⎪ ⎪ 3 ⎪ ⎪ ⎩ ⎭ U3 ((xs, y)) → (cons(x, xs), y) An unraveling U is simulation-sound (simulation-preserving and simulation∗ ∗ ∗ complete, respectively) for a DCTRS S over G if − →S ⊆ − →U(S) in T (G, V) ( − →S ∗ ∗ ∗ ⊇− →U(S) and − →S = − →U(S) in T (G, V), respectively). Note that the simulationpreserving property is sometimes called simulation-completeness in some papers, and it is a necessary condition of being unravelings. In the case of the innermost reduction, the simulation-preserving property is not trivial. Theorem 17. Let S be an operationally terminating constructor DCTRS over a signature G such that for every rule l → r ⇐ s1 → t1 ∧ · · · sk → tk ∈ S, ∗ ! t1 , . . . , tk are in T (CS , V). Let s and t be terms in T (G, V). If s − t, then s → i S ∗ ! ∗ ! u for some u in T (G , V) such that t u. − → − → U i U(S) i U(S) Proof (Sketch). Consider a normal form s of S and a rule f (l1 , . . . , ln ) → r ⇐ s1 → t1 ∧ · · · ∧ sk → tk ∈ S such that s matches either of patterns l1 , . . . , ln , t1 , . . ., or tk . From the discussion in [18], it is sufficient to show that a normal form of s w.r.t. S matches the same pattern. Since the pattern is a constructor term of S and since we have CS ⊆ CU(S) by definition, the pattern is a constructor term of U(S). Thus, let s ≡ C[s1 , . . . , sm ] with C[, . . . , ] ∈ T (CS ∪ {}, V), then we have C[, . . . , ] ∈ T (CU(S) ∪ {}, V) and any normal form of s w.r.t. U(S) is of the form C[s1 , . . . , sm ] such that si is a normal form of si w.r.t. U(S). The unraveling U is not simulation-sound for every DCTRS [24]. To avoid this difficulty of non-‘simulation-soundness’ of U, a restriction to the rewrite relations of the unraveled TRSs is shown in [27], which is done by the contextsensitive condition. The non-erasing property of S is another sufficient condition for simulation-soundness. Theorem 18 ([18]). Let S be a strongly non-erasing and operationally termi∗ ! nating DCTRS over a signature G, and s and t be terms in T (G, V). If s − → i U(S) ∗ ! t, then s − t. → i S Proposition 13, Theorem 14, 17 and 18 provide the following corollary. Corollary 19. Let R be a convergent constructor NE-SC-TRS over a signature F , and f ∈ DR . Then, U(Inv(R)) is an inverse system of R. Unfortunately, U does not preserve (innermost-)confluence of every DCTRS S, i.e., U(S) is not (innermost-)confluent for every (innermost-)confluent DCTRS S because NF − ⊇ NF − →S (G, V) →U(S) (GU , V) in general. The completion procedure shown in the next subsection can sometimes transform such a TRS into confluent one, preserving computational equivalence.
298
5.3
N. Nishida and M. Sakai
Completion of Unraveled TRSs
In this subsection, we review the simplified modification of the ordinary KnuthBendix completion procedure for the unraveled TRSs of convergent DCTRSs [18]. The simplified procedure transforms the unraveled TRSs into convergent TRSs that are computationally equivalent to the DCTRSs. Definition 20 ([18]). Let S be an operationally terminating DCTRS over a signature G, and be a reduction order such that U(S) ⊆ . Let S = { l → r ∈ U(S) | ∃l → r ∈ U(S), l ≡ t}, S(0) ∼ l }, E(0) = {s ≈ t | s ≈ t ∈ CP ε (U(S)), s = { l → r ∈ U(S) \ S | ∃l → r ∈ U(S) \ S , l ∼ l }, and i = 0, then we apply the following steps: 1. select s t ∈ E(i) such that s t, root(s) is a U symbol, and CP ({s → t}, S(i) ∪ {s → t}) = ∅; 2. S(i+1) := {s → t} ∪ S(i) , and E(i+1) := E(i) \ {s t}; 3. if E(i+1) = ∅ then i := i + 1 and go to step 1, otherwise output S(i+1) . We call this procedure the simplified completion procedure. It is clear that E(i) ⊃ E(i+1) for every i ≥ 0. Therefore, the simplified completion procedure always halts. Note that the simplified procedure does not succeed for all input. The restricted completion procedure preserves computational equivalence between the input unraveled TRSs and the resulting TRSs. Theorem 21 ([18]). Let S be a strongly non-erasing and operationally terminating DCTRS over F , such that for every rule l → r ⇐ s1 → t1 ∧ · · · ∧ sk → tk ∈ S, t1 , . . . , tk are in T (CS , V). Let be a reduction order such that U(S) ⊆ . Let S be a TRS obtained by the simplified completion procedure from S and . ∗ ! ∗ ! Then, S is innermost-convergent and − = − → → in T (G, V). i U(S) i S Note that innermost-confluence of S is necessary for the simplified completion procedure to halt ‘successfully’. Therefore, the simplified procedure is a method to show innermost-confluence of S. Proposition 13, Corollary 19, and Theorem 21 provide the following corollary. Corollary 22. Let R be a convergent constructor NE-SC-TRS over a signature F , and f ∈ DR , and be a reduction order such that U(Inv(R)) ⊆ . Let S be a TRS obtained by the simplified completion procedure from U(Inv(R)) and . Then, S is an innermost-confluent and terminating inverse system of R. To make the procedure powerful, termination provers are often employed instead of reduction orders [29]. In such cases, termination of the restricted procedure depends on the employed termination provers, nevertheless the number of steps in the procedure is always finite. Example 23. Consider the terminating but non-confluent TRS U(Inv(R3 )) in Example 16. We obtain the following innermost-confluent and terminating inverse TRS of R3 by the restricted completion procedure with the termination prover AProVE 1.2 [6] (by 2 cycles):
Proving Injectivity of Functions via Program Inversion in Term Rewriting
299
⎧ ⎫ rev(ys) → U1 (snoc(ys), ys), ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ U1 ((zs, x), ys) → U2 (rev(zs), ys, zs, x), ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎨ U2 ((xs), ys, zs, x) → (cons(x, xs)), ⎬ snoc(cons(x, ys)) → U3 (snoc(ys), x, ys), R4 = ⎪ ⎪ ⎪ ⎪ U3 ((xs, y)) → (cons(x, xs), y), ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ U1 (snoc(nil), nil) → (nil), ⎪ ⎪ ⎪ ⎩ ⎭ U3 (snoc(nil), y, nil) → (nil, y)
6
Decidable Sufficient Condition via Inversion
In this section, we show a sufficient condition for injectivity via the inversion framework shown in Section 5. We also show a decidable sufficient condition for injectivity, that is based on a syntactic property of given functions. Due to Corollary 19, Theorem 8 is adapted to the inversion framework U(Inv(· · ·)) as follows. Theorem 24. Let R be a convergent constructor NE-SC-TRS over a signature F . Then, all defined symbols of R are injective if U(Inv(R)) is innermostconfluent. It is known that non-overlay TRSs are innermost-confluent [25]. Corollary 25. Let R be a convergent constructor NE-SC-TRS over a signature F . If U(Inv(R)) is non-overlay, then all defined symbols of R are injective. In [15], overlappingness of inverted programs has been discussed in the viewpoint of the way to make overlapping equations non-overlapping. However, the discussion does not make reference to the injectivity of target programs. Next, we show a syntactic condition of R that guarantees the non-overlay property of U(Inv(R)). Given a term t, CAP(t) results from replacing all the subterms of t that have a defined root symbol by different fresh variables [24]. Proposition 26. Let R be a convergent constructor NE-SC-TRS over a signature F . U(Inv(R)) is non-overlapping 5 if for any two different rules l1 → r1 and l2 → r2 in R such that root(l1 ) = root(l2 ) and they are renamed to have no common variables, CAP(r1 ) and CAP(r2 ) are not unifiable. Proof. For a rewrite rule l → r ∈ R with root(l) = f , it follows from the construction of Inv(R) that the left-hand side of the corresponding rule in Inv(R) is of the form f (CAP(r)). Consider two different rules l1 → r1 and l2 → r2 in R. If root(l1 ) = root(l2 ), then the corresponding rules in Inv(R) have no overlay. Otherwise, the left-hand sides of the corresponding rules are of the form f (CAP(r1 )) and f (CAP(r2 )), respectively, where root(l1 ) = f . It follows from the assumption that f (CAP(r1 )) and f (CAP(r2 )) are not unifiable, and , and hence the corresponding rules have no overlay. Thus, every two different rules in Inv(R) have no overlay. It follows 5
Note that non-overlapping TRSs are non-overlay.
300
N. Nishida and M. Sakai
from Proposition 13 that Inv(R) is a constructor system. Since non-overlay constructor systems are non-overlapping, Inv(R) is non-overlapping. By definition, U preserves non-overlappingness. Therefore, U(Inv(R)) is nonoverlapping. Then, we obtain the following decidable sufficient condition for injectivity. Corollary 27. Let R be a convergent constructor NE-SC-TRS over a signature F . Suppose that for any two different rules l1 → r1 and l2 → r2 in R such that root(l1 ) = root(l2 ) and they are renamed to have no common variables, CAP(r1 ) and CAP(r2 ) are not unifiable. Then, all defined symbols of R are injective. Example 28. Consider the following convergent constructor NE-SC-TRS over {leaf, node, cpy, mirror, cpymrr}: ⎧ ⎫ cpymrr(x) → cpy(mirror(x)), ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ cpy(leaf) → node(leaf, leaf), ⎨ ⎬ R5 = cpy(node(x, y)) → node(node(cpy(x), cpy(y)), node(cpy(x), cpy(y))), ⎪ ⎪ ⎪ ⎪ mirror(leaf) → leaf, ⎪ ⎪ ⎪ ⎪ ⎩ ⎭ mirror(node(x, y)) → node(mirror(y), mirror(x)) The constructors leaf and node provide tree-structure data, cpy copies nodes inductively, mirror transforms data into mirrored ones, and cpymrr is the composition of cpy and mirror. The function cpymrr is injective since R5 satisfies all the condition in Corollary 27. Note that this TRS R5 is neither linear nor treeless. The conditions in Corollary 27 may be conceived in the course of nature. However, there exists another way to prove innermost-confluence of U(Inv(R)). When U(Inv(R)) is not innermost-confluent, the restricted completion procedure can be used as a method to prove injectivity of f in R if U(Inv(R)) is terminating. Theorem 29. Let R be a convergent constructor NE-SC-TRS over a signature F . If U(Inv(R)) is terminating 6 and if the restricted completion procedure succeeds for U(Inv(R)), then all defined symbols of R are injective. Theorem 29 has been described briefly in [18] but its correctness has not been given yet, although the correctness is a direct consequence of the previous result as shown in this paper. Some successful examples shown in [18] are not in the treeless forms. One of them is R3 in Example 12. Example 30. Consider the following convergent constructor NE-SC-TRS over {cons, nil, frev, frevsub}: ⎧ ⎫ frev(xs) → frevsub(xs, nil), ⎨ ⎬ R6 = frevsub(nil, ys) → ys, ⎩ ⎭ frevsub(cons(x, xs), ys) → frevsub(xs, cons(x, ys)) 6
A syntactic sufficient-condition of R for termination of U(Inv(R)) can be found in [19].
Proving Injectivity of Functions via Program Inversion in Term Rewriting
301
The function frev is known to compute efficiently the reverse of lists. Unfortunately, R6 does not satisfy the conditions in Corollary 27. In fact, U(Inv(R6 )) is not innermost confluent. Furthermore, the restricted completion cannot be applied to U(Inv(R6 )) because U(Inv(R6 )) is not terminating. Thus, the injectivity of R6 cannot be proved by the methods in this paper. However, the inversion method in [8] succeeds in generating a deterministic (i.e., innermost confluent) inverse program of R6 . Therefore, R6 is proved to be injective via the inversion method in [8].
7
Conclusion
In this paper, we have shown that functions defined by convergent constructor NE-SC-TRSs are injective if the corresponding inverse TRSs obtained by the inversion framework in [21,22] are innermost-confluent. Moreover, we have shown the cases that the restricted completion procedure in [18] is useful to prove injectivity even if the inverse TRSs resulted by the inversion framework are not innermost-confluent. The method in this paper to prove injectivity is applicable to functions written in other functional languages if the functions can be expressed in constructor NE-SC-TRSs. As shown in Example 30, however, the inversion framework in citeNishida05rta,Nishida05ieice-J88-D1 cannot generate a terminating inverse TRS of R6 although the way to invert rewrite rules is naive. For this reason, it is worth of improving the inversion framework [21,22]. The results in this paper hold for TRSs that are not sufficiently complete because all the results in Section 5 and Section 6 holds even if we replace T (CR ) with NF − →R (F ) (see the original ones in [22,18]). The other assumption that given TRSs are non-erasing can be also relaxed by employing the inversion method in [21] instead. For such a relaxation, we will have to force the inverse systems to be TRSs, i.e., to have no extra variables. However, we conjecture that given TRSs are not injective if the resulting inverse systems are not TRSs. Proving this conjecture is one of our future works. The discussion in this paper is impossible for inverse interpreters because the interpreters do not produce any inverse programs. On the other hand, as shown in Example 30, the discussion on proving injectivity via program inversion, is possible for other inversion compilers. However, as far as we know, there is no research for the syntactic relationship between given systems and the corresponding inverse systems, except for the framework in [21,22]. In [9], it is shown that the disjoint simple post conditions inferred from given post condition of functions guarantee that the inverses generated by its inverse technique are deterministic, i.e., that the target functions are injective. In [13], it is shown that a non-erasing program is injective if its derived regular tree grammar is unambiguous, where the unambiguity is decidable. Furthermore, [13] states that the unambiguity is equivalent to the disjointness of the simple post conditions. The sufficient condition for injectivity in [13], the unambiguity of the derived regular tree grammars, is properly weaker than the sufficient condition in Theorem 27 because the condition in Theorem 27 clearly implies the
302
N. Nishida and M. Sakai
unambiguity. On the other hand, the sufficient conditions in [13] and Theorem 29 are incomparable because each of them has a successful example for which the other fails.
Acknowledgements This work is partly supported by Kayamori Foundation of Informational Science Advancement, and MEXT. KAKENHI #18500011 and #21700011. We thank Kazutaka Matsuda for his useful comments on related works.
References 1. Abramov, S., Gl¨ uck, R.: The universal resolving algorithm and its correctness: Inverse computation in a functional language. Science of Computer Programming 43(2-3), 193–229 (2002) 2. Almendros-Jim´enez, J.M., Vidal, G.: Automatic partial inversion of inductively sequential functions. In: Horv´ ath, Z., Zs´ ok, V., Butterfield, A. (eds.) IFL 2006. LNCS, vol. 4449, pp. 253–270. Springer, Heidelberg (2007) 3. Baader, F., Nipkow, T.: Term Rewriting and All That. Cambridge University Press, United Kingdom (1998) 4. Dershowitz, N., Mitra, S.: Jeopardy. In: Narendran, P., Rusinowitch, M. (eds.) RTA 1999. LNCS, vol. 1631, pp. 16–29. Springer, Heidelberg (1999) 5. F¨ ul¨ op, Z.: Undecidable properties of deterministic top-down tree transducers. Theoretical Computer Science 134(2), 311–328 (1994) 6. Giesl, J., Schneider-Kamp, P., Thiemann, R.: Automatic termination proofs in the dependency pair framework. In: Furbach, U., Shankar, N. (eds.) IJCAR 2006. LNCS (LNAI), vol. 4130, pp. 281–286. Springer, Heidelberg (2006) 7. Gramlich, B.: On the (non-)existence of least fixed points in conditional equational logic and conditional rewriting. In: Proc. 2nd Int. Workshop on Fixed Points in Computer Science (FICS 2000) – Extended Abstracts, pp. 38–40 (2000) 8. Gl¨ uck, R., Kawabe, M.: A method for automatic program inversion based on LR(0) parsing. Fundam. Inform. 66(4), 367–395 (2005) 9. Gl¨ uck, R., Kawabe, M.: Revisiting an automatic program inverter for Lisp. SIGPLAN Notices 40(5), 8–17 (2005) 10. Kawabe, M., Futamura, Y.: Case studies with an automatic program inversion system. In: Proc. of the 21st Conference of Japan Society for Software Science and Technology, No. 6C-3, 5 pages (2004) 11. Lucas, S., March´e, C., Meseguer, J.: Operational termination of conditional term rewriting systems. Information Processing Letters 95(4), 446–453 (2005) 12. Matsuda, K., Hu, Z., Nakano, K., Hamana, M., Takeichi, M.: Bidirectionalization transformation based on automatic derivation of view complement functions. In: Proc. of the 12th ACM SIGPLAN International Conference on Functional Programming, pp. 47–58 (2007) 13. Matsuda, K., Mu, S.C., Hu, Z., Takeichi, M.: A grammar-based approach to invertible programs. In: Proc. of the 19th European Symposium on Programming (2010) (to appear) 14. McCarthy, J.: The inversion of functions defined by Turing machines. In: Automata Studies, pp. 177–181. Princeton University Press, Princeton (1956)
Proving Injectivity of Functions via Program Inversion in Term Rewriting
303
15. Mogensen, T.Æ.: Semi-inversion of guarded equations. In: Gl¨ uck, R., Lowry, M. (eds.) GPCE 2005. LNCS, vol. 3676, pp. 189–204. Springer, Heidelberg (2005) 16. Mogensen, T.Æ.: Semi-inversion of functional parameters. In: Proc. of the 2008 ACM SIGPLAN Symposium on Partial Evaluation and Semantics-Based Program Manipulation, pp. 21–29. ACM Press, New York (2008) 17. Nishida, N.: Transformational Approach to Inverse Computation in Term Rewriting. Doctor thesis, Nagoya University, Nagoya, Japan (2004), http://www.trs.cm.is.nagoya-u.ac.jp/~ nishida/papers/ 18. Nishida, N., Sakai, M.: Completion after program inversion of injective functions. Postproceedings of the 8th International Workshop on Reduction Strategies in Rewriting and Programming. ENTCS, vol. 237, pp. 39–56 (2009) 19. Nishida, N., Sakai, M., Kato, T.: Convergent term rewriting systems for inverse computation of injective functions. In: Proc. of the 9th International Workshop on Termination (WST 2007), pp. 77–81 (2007), http://www.trs.cm.is.nagoya-u.ac.jp/~ nishida/papers/ 20. Nishida, N., Sakai, M., Sakabe, T.: On simulation-completeness of unraveling for conditional term rewriting systems. IEICE Technical Report SS2004-18, the Institute of Electronics, Information and Communication Engineers (IEICE) 104(243), 25–30 (2004) 21. Nishida, N., Sakai, M., Sakabe, T.: Partial inversion of constructor term rewriting systems. In: Giesl, J. (ed.) RTA 2005. LNCS, vol. 3467, pp. 264–278. Springer, Heidelberg (2005) 22. Nishida, N., Sakai, M., Sakabe, T.: Generation of inverse computation programs of constructor term rewriting systems. The IEICE Transactions on Information and Systems J88-D-I(8), 1171–1183 (2005); in Japanese, see [17] 23. Ohlebusch, E.: Termination of logic programs: Transformational methods revisited. Applicable Algebra in Engineering, Communication and Computing 12(1-2), 73– 116 (2001) 24. Ohlebusch, E.: Advanced Topics in Term Rewriting. Springer, Heidelberg (2002) 25. Plaisted, D.A.: Equational reasoning and term rewriting systems. In: Handbook of Logic in Artificial Intelligence and Logic Programming, vol. 1, pp. 273–364. Oxford University Press, Oxford (1993) 26. Romanenko, A.: Inversion and metacomputation. In: Proc. of the Symposium on Partial Evaluation and Semantics-Based Program Manipulation. SIGPLAN Notices, vol. 26, pp. 12–22. ACM Press, New York (1991) 27. Schernhammer, F., Gramlich, B.: On proving and characterizing operational termination of deterministic conditional rewrite systems. In: Proc. of the 9th International Workshop on Termination (WST 2007), pp. 82–85 (2007) 28. S ¸ erb˘ anut¸a ˘, T.F., Ro¸su, G.: Computationally equivalent elimination of conditions. In: Pfenning, F. (ed.) RTA 2006. LNCS, vol. 4098, pp. 19–34. Springer, Heidelberg (2006) 29. Wehrman, I., Stump, A., Westbrook, E.M.: Slothrop: Knuth-Bendix completion with a modern termination checker. In: Pfenning, F. (ed.) RTA 2006. LNCS, vol. 4098, pp. 287–296. Springer, Heidelberg (2006)
Delimited Control in OCaml, Abstractly and Concretely: System Description Oleg Kiselyov FNMOC
[email protected]
Abstract. We describe the first implementation of multi-prompt delimited control operators in OCaml that is direct in that it captures only the needed part of the control stack. The implementation is a library that requires no changes to the OCaml compiler or run-time, so it is perfectly compatible with existing OCaml source code and byte-code. The library has been in fruitful practical use for four years. We present the library as an implementation of an abstract machine derived by elaborating the definitional machine. The abstract view lets us distill a minimalistic API, scAPI, sufficient for implementing multiprompt delimited control. We argue that a language system that supports exception and stack-overflow handling supports scAPI. Our library illustrates how to use scAPI to implement multi-prompt delimited control in a typed language. The approach is general and can be used to add multi-prompt delimited control to other existing language systems.
1
Introduction
The library delimcc of delimited control for byte-code OCaml was released at the beginning of 2006 [1] and has been used for implementing (delimited) dynamic binding [2], a very shallow embedding of a probabilistic domain-specific language [3, 4], CGI programming with nested transactions [5], efficient and comprehensible direct-style code generators [6], normalization of MapReduce-loop bodies by evaluation [7], and automatic bundling of RPC requests [8]. The delimcc library was the first direct implementation of delimited control in a typed, mainstream, mature language – it captures only the needed prefix of the current continuation, requires no code transformations, and integrates with native-language exceptions. Captured delimited continuations can be serialized, stored, or migrated, then resumed in a different process. The delimcc library is an OCaml library rather than a fork or a patch of the OCaml system. Like the num library of arbitrary-precision numbers, delimcc gives OCaml programmers new datatypes and operations, some backed by C code. The delimcc library does not modify the OCaml compiler or run-time in any way, so it ensures perfect binary compatibility with existing OCaml code and other libraries. This library shows that delimited control can be implemented efficiently
M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 304–320, 2010. c Springer-Verlag Berlin Heidelberg 2010
Delimited Control in OCaml, Abstractly and Concretely: System Description
305
(without copying the whole stack) and non-invasively in a typed language that was not designed with delimited control in mind and that offers no compiler plug-ins or run-time extensions beyond a basic foreign-function interface. Our goal in this paper is to describe the implementation of delimcc with enough detail and generality so that it can be replicated in other language systems. The delimcc library implements the so-called multi-prompt delimited control operators that were first proposed by Gunter, R´emy, and Riecke [9] and further developed by Dybvig, Peyton Jones, and Sabry [10]. The multi-prompt operators turn out indispensable for normalization-by-evaluation for strong sums [11]. Further applications of specifically multi-prompt operators include the implementation of delimited dynamic binding [2] and the normalization of loop bodies by evaluation [7]. The delimcc library turns out suitably fast, useful, and working in practice. In this paper, we show that it also works in theory. We describe the implementation and argue for its generality and correctness. The correctness argument cannot be formal: after all, there is no formal specification of OCaml, with or without delimited control. We informally relate the byte-code OCaml interpreter to an abstract machine, which we rigorously relate to abstract machines for delimited control. The main insight is the discovery that OCaml byte-code already has the facilities needed to implement delimited control efficiently. In fact, any language system accommodating exception handling and recovery from control-stack overflow likely offers these facilities. Languages that use recursion extensively typically deal with stack overflow [12]. Our contributions are as follows. 1. We state the semantics of multi-prompt delimited control in a form that guides the implementer, in §3. We derive a minimalistic API, scAPI, sufficient for implementing delimited control. For generality, we describe scAPI in terms of an abstract state machine, which focuses on activation frame manipulation while eliding idiosyncratic details of concrete language systems. Our scAPI includes the creation of ‘stable-point’ frames, completely describing the machine state including the contents of non-scratch registers. We should be able to identify the recent stable point frame and copy a part of the stack between two stable points. We do not require marking of arbitrary frames, adding new types of frames, or even knowing the format of the stack. 2. On the concrete example of the OCaml byte-code and delimcc, we demonstrate in §4 using the scAPI to implement multi-prompt delimited control.1 OCaml happens to support scAPI, §4.2. 3. The implementation of delimcc poses challenging typing problems, which previously [10, 13] were handled using unsafe coerce. We use reference cells to derive in §4.1 a safe solution, free from any undefined behavior. 4. The experience with the delimcc library called for an extension of the simple interface [10], to avoid a memory leak in multi-prompt shift, appendix B of the full paper.2 The new primitive push_delim_subcont reinstates the captured continuation along with its delimiter. 1 2
The Scheme implementation, mentioned on the delimcc web page, is another concrete example of using scAPI, attesting to the generality of the approach. Available at http://okmij.org/ftp/Computation/caml-shift.pdf
306
O. Kiselyov
5. We describe serialization of captured delimited continuations so to make them persistent. We show why serialized delimited continuations must refer to some reachable data by name rather than incorporate everything by value. Serialized delimited continuations should be, so to speak, twice delimited.3 We review the related work in §5 and then conclude. The performance of the library proved adequate, see [4]. In particular, aborting part of the computation with delimcc is just as fast as raising an OCaml exception. We start by introducing the multi-prompt delimited control and the delimcc library in §2. The delimcc library source along with validation tests and sample code is freely available from http://okmij.org/ftp/Computation/ Continuations.html#caml-shift
2
Multi-prompt Delimited Control
Before discussing the implementation of delimcc, we introduce the library on sample code, informally describing multi-prompt delimited control. The basic delimcc interface, taken from [10], defines two abstract types and four functions: type ’a prompt type (’a,’b) subcont val val val val
new_prompt push_prompt take_subcont push_subcont
: : : :
unit -> ’a prompt ’a prompt -> (unit -> ’a) -> ’a ’b prompt -> ((’a,’b) subcont -> unit -> ’b) -> ’a (’a,’b) subcont -> (unit -> ’a) -> ’b
whose semantics is formally discussed in §3. Intuitively, a value of the type ’a prompt is an exception object, with operations to pack and extract a thunk of the type unit -> ’a. The expression new prompt () produces a fresh exception object; take subcont p (fun () -> e) packs fun () -> e into the exception object denoted by the prompt p, and raises the exception. The expression push prompt p (fun () -> e) is akin to OCaml’s try e with ... form, evaluating e and returning its result. Should e raise an exception p, it is caught, the contained thunk is extracted, and the result of its evaluation is returned. All other exceptions are re-raised. As an example, let us left fold over a file, reading the file line-by-line and reducing using the given function f: (* val fold_file: (’a -> string -> ’a) -> ’a -> in_channel -> ’a *) let fold_file f z file = let ex = new_prompt () in let rec loop z = let inp = try input_line file with End_of_file -> take_subcont ex (fun _ ()-> z) in loop (f z inp) in push_prompt ex (fun () -> loop z);; 3
Due to the lack of space, we refer the reader to the long title comments in the file delimcc.ml for the explanation of the serialization.
Delimited Control in OCaml, Abstractly and Concretely: System Description
307
For example, fold file (fun z s -> z + 1) 0 cin returns the line count in the input channel cin. The code for fold_file is exactly equivalent to let fold_file f z file : ’a = let exception Ex of ’a in let rec loop z = let inp = try input_line file with End_of_file -> raise (Ex z) in loop (f z inp) in try loop z with Ex z -> z
if OCaml had local exception declaration such as those in SML. OCaml however lacks such exception declarations.4 The delimcc library thus fills this omission. The exceptions thrown by take_subcont are restartable: take subcont p (fun sk () -> e) would bind sk to a ‘restart object’ before raising the exception p; e may return the object as part of its result. Given the restart object, push_subcont restarts the exception, continuing the execution from the point of take_subcont p till push_prompt p, returning the result of the latter. The following should make it concrete. First we introduce shift0 that captures a frequently occurring pattern (* val shift0: ’a prompt -> ((’b -> ’a) -> ’a) -> ’b *) let shift0 p f = take_subcont p (fun sk () -> f (fun c -> push_prompt p (fun () -> push_subcont sk (fun () -> c))))
which is used as follows: type ’a res = Value of ’a | Exc of ’a * (unit -> ’a res) let accum p z str = if str = "" then shift0 p (fun k -> Exc (z,fun () -> k z)) else z + String.length str
We may view shift0 in this code as raising the exception p, with k bound to the restart function. When k is applied to a value z, the execution continues as if the entire shift0 expression had been replaced by z. Since the computation, after restart, may raise the exception again, we have to be able to handle it, hence the call to push_prompt. The function accum is meant to be a reducer function passed to a fold: let sum_arr arr = let p = new_prompt () in push_prompt p (fun () -> Value (Array.fold_left (accum p) 0 arr));; let t2 → val let t3 → val
= sum_arr [| "FLOPS";"";"2010"|];; t2 : int res = Exc (5,
) = match t2 with Exc (_,resume) -> resume ();; t3 : int res = Value 9
The function sum_arr sums the lengths of all strings in a string array. Encountering an empty string throws an exception. The function sum_arr then returns Exc (z,resume) reporting the length so far. Evaluating resume () restarts 4
Placing exception declarations into an OCaml local module does not fully implement SML local exceptions. In SML, a local exception declaration may refer to a bound type variable. A type variable in OCaml cannot bind into a local structure.
308
O. Kiselyov
the exception and resumes the accumulation, returning either the final result Value z or another exception. The same exception can be restarted more than once, which is particularly useful for probabilistic programming [3]. The functions accum and sum_arr have demonstrated the application of delimited control to ‘invert’ an enumerator, that is, to convert the enumerator to a stream [14, 15]. We can use accum with fold_file defined earlier, to sum the lengths of the strings read from the file, stopping at empty strings. Although fold_file itself uses delimited control, the two take_subcont use different prompts and so act unaware of each other. The formal, small-step semantics of these delimited control operators was specified in [9] (push_prompt was called set and take_subcont was called cupto) – as a set of re-writing rules. The rules, which operate essentially on the source code, greatly help a programmer to predict the evaluation result of an expression. Alas, the rules offer little guidance for the implementer since typical language systems are stateful machines, whose behavior is difficult to correlate with pure source-code re-writing.
3
Abstract Machine for Multi-prompt Delimited Control
More useful for the implementer is semantics expressed in terms of an abstract machine, whose components and steps can, hopefully, be related to an implementation of a concrete machine at hand. By abstracting away implementation details, abstract state machines let us discern generally applicable lessons. Our first lesson is the identification of a small scAPI for manipulating the control stack. We further learn that any language system supporting exception handling already implements a half of scAPI. We start with the definitional machine introduced in [10, Figure 1] as a formal specification of multi-prompt delimited control. We reproduce the definition in appendix A for reference. The machine contains features that are recognizable by implementers, such as ‘context’ – which is a sequence of activation frames, commonly known as ‘(control) stack.’ The machine however contains an extra component, a list of contexts. It is not immediately clear what it may correspond to in concrete machines, raising doubts if delimited control can be added to an existing machine such as the OCaml byte-code without re-designing it. These worries are unfounded. The machine of [10] can be converted into the equivalent machine described below, which has no extra components such as lists of control stacks. We prove the equivalence in appendix A. Our machine Mdc , Figure 1, is bare-bone: it has no environment, arithmetic and many other practically useful features, which are orthogonal and can be easily added. It abstracts away all details except for control stack. The machine can be viewed as a generalization of the environment-less version of the machine of [16]. The program for the machine is call-by-value λ-calculus, augmented with integral-valued prompts and delimited control operators. The operators here are syntactic forms rather than constants: for example, newP evaluates each time to a new prompt. In delimcc, we eschew extending the syntax of OCaml. Therefore,
Delimited Control in OCaml, Abstractly and Concretely: System Description Variables
x, y, . . .
Prompts
309
p, q ∈ N
Expressions e ::= v | e e | newP | pushP e e | takeSC e e | pushSC e e Values Contexts
v ::= x | λx. e | p | D D ::= | De | vD | pushP D e | pushSC D e | takeSC D e | takeSC p D | pushP p D Transitions between configurations (e, D, q)
→ (e, D[e ], q) (ee , D, q)
e non-value
(ve, D, q) → (e, D[v], q)
e non-value
→ (e, D[pushP e ], q) e non-value (pushP ee , D, q) (takeSC ee , D, q) → (e, D[takeSC e ], q) e non-value (takeSC pe, D, q) → (e, D[takeSC p], q) e non-value → (e, D[pushSC e ], q) e non-value (pushSC ee , D, q) ((λx. e)v, D, q) → (e[v/x], D, q) (newP, D, q) → (q, D, q + 1) (pushP pe, D, q) → (e, D[pushP p], q) (takeSC pv, D, q) → (vD1 , D2 , q)
D2 [pushP pD1 ] = D, pushP pD ∈ D1
(pushSC D e, D, q) → (e, D[D ], q) (v, D[D1 ], q) → (D1 [v], D, q)
D1 =
(pushP pv, D, q) → (v, D, q) Fig. 1. Abstract machine Mdc for multi-prompt delimited control
we represent newP as a function application new_prompt (). Likewise, pushP p e takes the form push_prompt p (fun () -> e) in delimcc. The operation D[u] replaces the hole in context D with u, which may be either an expression or another context; e[v/x] stands for a capture-avoiding substitution of v for variable x in expression e. Prompts p and contexts D may not appear in source programs. The machine operates on configurations (e, D, q) of the current expression e, ‘stack’ D and the counter for generating fresh prompt names. The initial configuration is (e, , 0); the machine stops when it reaches (v, , q). The machine exhibits familiar to the implementers features: D is a sequence of activation frames, the ‘stack’; the first six transitions look like a function call, pushing a new activation frame onto the stack; the last-but-one transition is akin to the function return, popping the frame. (For generality, we only require the sequence of the popped frames D1 to be non-empty.) The machine also exhibits non-standard stack-manipulation operations: D[D ] in the pushSC transition pushes several frames D at once onto the stack; the takeSC transition involves locating a particular frame pushP pD1 and splitting the stack at that frame. The removed prefix D1 is passed as a value to the argument of takeSC; in a real machine, the stack prefix D1 would be copied onto heap, the ordinary place of storing composite values. These non-standard stack operations thus
310
O. Kiselyov
Variables
x, y, . . .
Exceptions
p, . . .
Expressions e ::= v | e e | raisep e | tryp e e v ::= x | λx. e
Values Contexts
D ::= | De | vD | raisep D | tryp D e
Transitions between configurations (e, D)
(ee , D) → (e, D[e ])
e non-value
(ve, D) → (e, D[v])
e non-value
(raisep e, D) → (e, D[raisep ]) e non-value ((λx. e)v, D) → (e[v/x], D) → (e, D[tryp e ]) (tryp ee , D)
→ (e v, D2 ) (raisep v, D)
(v, D[D1 ]) → (D1 [v], D)
D2 [tryp D1 e ] = D, tryp D e ∈ D1 D1 =
→ (v, D) (tryp ve , D) Fig. 2. Abstract machine Mex for exception handling
constitute an API, which we call scAPI, for implementing multi-prompt delimited control. To see how scAPI may be supported, we relate scAPI with exception handling, a widely supported feature. As a specification of exception handling we take an abstract machine Mex , Figure 2. The program for Mex is too call-by-value λ-calculus, extended with the operations to raise and catch exceptions. These operations are indexed by exception types. A source programmer has an unlimited supply of exception types to choose from. Exception types, however, are not values and cannot be created at run-time. The comparison of Figures 1 and 2 shows many similarities. For example, we observe that the expression pushP p v reduces to v in any evaluation context; likewise, tryp v e reduces to v for any D. One may also notice a similarity between raising an exception and takeSC that disregards the captured continuation. On the other hand, takeSC uses prompts whose new values can be created at run-time; the set of exceptions is fixed during the program execution. To dispel doubts, we state the equivalence result precisely, even more so as we rely on it in the implementation. First, we have to extend Mex with integers serving as prompts, which can be compared for equality using ==. Prompts cannot appear in source programs but are generated by an operator newP, evaluating each time to a fresh value. We add unit (), pairs (e, e) and pair projections fst and snd, and the conditional. We call the extended machine Mex . Let Mdc be Mdc with a restriction on source programs: no pushSC, all takeSC expressions must be of the form takeSC e (λx. e ) where x is not free in e . Therefore, contexts D are not values of Mdc . We define the
Delimited Control in OCaml, Abstractly and Concretely: System Description
311
translation · of Mdc expressions to the expressions of Mex as follows (where p0 is a dedicated exception type): takeSC p (λx. e) = raisep0 (λx. e, p) pushP p e = tryp0 e (λy. if p == snd y then fst y () else raisep0 y) It is homomorphism in the other cases. The intuition comes from mail-relay systems. The exception is an envelope, the prompt p is an address, the exception handler is a relay station, which matches the address on the envelope with its own. If the address matches, the station opens the envelope; otherwise, it forwards the message to the next relay. More formally, we state: for all Mdc source programs e, the machine reaches the terminal configuration iff Mex does so for the source program e. The proof is straightforward bi-simulation. We conclude that Mex effectively provides the operation to locate a particular stack frame and split the stack at the frame, disregarding the prefix. That particular stack frame, tryp D e is quite like the frame pushP pD that has to be located in Mdc . Thus any real machine that supports exception handling implements a part of scAPI. To see how the stack-copying part of scAPI could be implemented, we turn to stack overflow. Any language system that supports and encourages recursion has to face stack overflow and should be able to recover from it [12]. Recovery typically involves either copying the stack into a larger allocated area, or adjoining a new stack fragment. In the latter case, the implementation needs to handle stack underflow, to switch to the previous stack fragment. In the extreme case, each ‘stack’ fragment is one-frame long and so all frames are heap-allocated. In every case, the language system has to copy, or adjoin and remove stack fragments. These are exactly the operations of scAPI. The deep analogy between handling stack overflow and underflow on one hand and capturing and reinstating continuations on the other hand has been noted in [12]. We now introduce an equivalent variant of Mdc ensuring that a captured continuation is delimited by pushP frames on both ends. These frames are stable points. Real machines use the control stack as a scratch allocation area and for register spill-over. The state of real machines also contains more components (such as CPU registers), used as a fast cache for various frame data [17]. When capturing continuation, we have to make sure that all these caches are flushed so that the captured activation frames contain the complete state for resuming the computation. As we rely on exception handling for support of a part of scAPI, we identify pushP frames with exception handling frames. To our knowledge, the points of exception handling correspond to stable points of concrete machines. We define the variant Midc of Mdc by changing two transitions to: (takeSC pv, D, q) → (vD1 , D2 , q) ∈ D1 D2 [pushP pD1 ] = D[pushP p ], p fresh, pushP pD (pushSC D e, D, q) → (e, D[pushP p D ], q) p fresh Strictly speaking, we ought to have introduced an auxiliary counter q in the configuration to generate fresh auxiliary prompts p and p . We can prove the equivalence of the modified Mdc to the original one, using bi-simulation similar to
312
O. Kiselyov
the one in appendix A. The key fact is that the auxiliary prompts are fresh, are not passed as values and so there cannot be any takeSC operations referring to these prompts. Any continuation captured by Midc is delimited by pushP p at one end and pushP p at the other: the continuation is captured between two stable points, as desired. The re-instated continuation is too sandwiched between two pushP frames: pushP p is part of the captured continuation, the other frame is inserted by pushSC. The presence of pushP on both ends also helps in making delimcc well-typed, as we see next.
4
Implementation in OCaml
In the previous section, we have introduced the deliberately general and minimalistic scAPI that is sufficient to implement delimited control, and shown that a concrete language system supporting handling of exceptions and of stack overflow is likely to implement scAPI. We now demonstrate both points on the concrete example of OCaml: that is, we describe the implementation of delimcc. In §4.2 we show how exactly OCaml, which supports exceptions and handles stack overflow, implements scAPI. In fact, the OCaml byte-code interpreter is an instance of Mex extended with the operations for copying parts of stack. §4.3 then explains the implementation of delimcc in terms of scAPI, closely following the ‘abstract implementation’ in §3. The OCaml byte-code interpreter is written in C; our delimcc code is in OCaml (using thin C wrappers for scAPI), giving us more confidence in the correctness due to the expressive language and the use of types. OCaml is a typed language; the delimcc interface is also typed. Having avoided types so far we confront them now. 4.1
Implementing Typed Prompts
We describe the challenges of implementing delimited control in a typed language on a simpler example, of realizing the Mdc machine, with the restricted form of takeSC, in terms of exception handling. Earlier, in §3, we explained the implementation on abstract machines. The version of that code in OCaml: let take_subcont p thunk = raise (P0 (thunk,p)) let push_prompt p thunk = try thunk () with (P0 (v,p’)) as y -> if p = p’ then v () else raise y
is ill-typed for two reasons. First, the type of a prompt in delimcc, §2 (whose interface is based on [9, 10]) is parametrized by the so-called answer-type, the type of values yielded by the push_prompt that pushed it. The prompts p and p’ in the above code are generally pushed by different push_prompts and hence may have different types. In OCaml, we can only compare values of the same type. To solve the problem, we implement prompts as records with an int component, called ‘mark’, making new_prompt produce a unique value for that field. We can then compare prompts by comparing their marks. (The overhead of marks proved negligible.) A deeper problem is that the typing of try e1 with ex -> e2 in OCaml requires e1 and e2 be of the same type. Hence thunk and v in our
Delimited Control in OCaml, Abstractly and Concretely: System Description
313
code must have the same type. However, thunk produces the value to return by push_prompt p and v is ‘thrown to’ push_prompt p’. Generally, p and p’, and so thunk and v, have different types. It is only when the marks of p and p’ have the same value that v and thunk have the same type. Dependent types, or at least recursive and existential types [18] seem necessary. The post-office intuition helps us again: we usually do not communicate with a mailman directly; rather, we use a shared mailbox. The correspondence between take_subcont and push_prompt is established through a common prompt, a shared value. This prompt is well-suited for the role of the mailbox. A reference cell of the type ’a option ref may act as a mailbox to exchange values of the type ’a; the empty mailbox contains None. Since in our code take_subcont sends to push_subcont a thunk, it is fitting to rather use (unit -> ’a) ref as the mailbox type. type ’a prompt = {mbox: (unit -> ’a) ref; mark: unit ref} let mbox_empty () = failwith "Empty mbox" let mbox_receive p = (* val mbox_receive : ’a prompt -> ’a *) let k = !(p.mbox) in p.mbox := mbox_empty; k () let new_prompt () = {mbox = ref mbox_empty; mark = ref ()};;
The mark field of the prompt should uniquely identify the prompt. Since we already use reference cells, and since OCaml has the physical equality ==, it behooves us to take a unit ref as prompt’s mark. We rely on the fact that each evaluation of ref () gives a unique value, which is == only to itself. If physical equality is not provided, we can always emulate it via equi-mutability. To send a thunk to a push_prompt, the operation take_subcont deposits the thunk into the shared mailbox and ‘alerts’ the receiver, by sending the exception containing the mark of the mailbox. Since the type of the mark is always unit ref regardless of the type of the thunk, we no longer have any typing problems. exception P0 of unit ref let take_subcont p thunk = p.mbox := thunk; raise (P0 p.mark) let push_prompt p thunk = try thunk () with (P0 mark’) as y -> if p.mark == mark’ then mbox_receive p else raise y;;
Anticipating the continuation capture in §4.3, we make the code more uniform: let push_prompt p thunk = try let res = thunk () in p.mbox := (fun () -> res); raise (P0 p.mark) with (P0 mark’) as y -> if p.mark == mark’ then mbox_receive p else raise y;;
The inferred type is ’a prompt -> (unit -> ’a) -> ’a, befitting delimcc. The value produced by push_prompt is in every case the value received from the mailbox. Our earlier typing problems are clearly eliminated.
314
4.2
O. Kiselyov
scAPI in OCaml
We now precisely specify scAPI and describe how the OCaml byte-code implements it. We formulate scAPI as the interface module EK : sig type ek type ekfragment val get_ek : unit -> ek val add_ek : ek -> ek -> ek val sub_ek : ek -> ek -> ek val pop_stack_fragment : ek -> ek -> ekfragment val push_stack_fragment : ekfragment -> unit end
with two abstract types, ek and ekfragment. The former identifies an exception frame; get_ek () returns the identity of the latest exception frame. There are no operations to scan the stack looking for a particular frame. A stack fragment between two exception frames is represented by ekfragment. Given the stack of the form D2 [tryek1 [D1 [tryek2 D ]]], pop stack fragment ek1 ek2 transforms the stack to D2 [tryek1 D ] returning the removed part D1 [tryek2 ] as ekfragment. One of the exception frames is captured as part of ekfragment. The operation push_stack_fragment ekfragment splices such an ekfragment in at the point of the latest exception frame, turning the stack from D2 [tryek D ] to D2 [tryek [D1 [tryek2 D ]]]. These stack operations clearly correspond to the transitions of Midc in §3. We never capture the top stack fragment D and never copy onto the top of the stack D because D contains ephemeral local data [17]. When the captured ekfragment is pushed back onto the stack, the identities of the exception frames captured in the fragment may change. If we obtained the identities of the captured frames before, we should adjust our ek values; hence the operations add_ek and sub_ek. The OCaml byte-code interpreter [19], an elaboration of the abstract machine ZAM [17], supports exceptions, pairs, conditionals, comparison, state to generate unique identifiers – and is thus an instance of Mex . Exception frames are linked together; the dedicated register trapsp of the interpreter keeps the pointer to the latest exception frame. Therefore, we can identify exception frames by their pointers; ek is such a pointer, relative to the beginning of the stack caml_stack_high, in units of value. Evaluating try e with ... creates a new exception frame before evaluating e. Reading trapsp in e by executing get_ek () gives us the identity of the created exception frame. Since the relative pointer is just an integer, add_ek and sub_ek are integer addition and subtraction. OCaml handles stack overflow by copying the stack into a larger allocated memory block. That implies that either there are no absolute pointers to stack values stored in data structures, or there is a way to adjust them. In fact, the only absolute pointers into stack are the link pointers in exception frames. The OCaml byte-code has a procedure to adjust such pointers after copying the stack. The operations pop_stack_fragment and push_stack_fragment are the variants of interpreter’s stack-copying procedure. These operations along with get_ek can be invoked from OCaml code via the foreign-function interface.
Delimited Control in OCaml, Abstractly and Concretely: System Description
4.3
315
Implementing delimcc in Terms of scAPI
In this section we show how to use scAPI to implement the delimcc interface, presented in §2. One may view this section as an example of transcribing the abstract implementation, Midc in §3, into OCaml, keeping the code well-typed. The transcription is mostly straightforward, after we remove the final obstacle that we now explain. Recall that Midc requires locating on the stack a pushP p frame with a particular prompt value p and copying parts of stack between two pushP frames. OCaml, via scAPI, supports copying parts of stack between exception frames. We can also obtain the identity of the latest exception frame. However, scAPI gives us no way to scan the stack looking for a frame with a particular identity. §4.1 showed how to relate a push_prompt frame to an exception frame and how to locate on stack a push_prompt p frame with a particular prompt value p – alas, flushing the stack up to that point. We have to find a way to identify a pushP frame without disturbing the stack. The solution is easy: push_prompt should maintain its own stack of its invocations, called ‘parallel stack’ or pstack. The pstack is a mutable list of pframes, which we can easily scan. A pframe on pstack corresponds to a push_prompt on the real stack and contains the identity of push_prompt’s exception frame and the mark of the prompt (see §4.1) ‘pushed’ at that point: exception DelimCCE type pframe = {pfr_mark : unit ref; pfr_ek : ek} type pstack = pframe list ref let ptop : pstack = ref []
DelimCCE is the dedicated exception type, called p0 in Mex and P0 in §4.1. Unlike the latter, the exception no longer carries the prompt’s identity since we obtain this identity from pstack, accessed via the global variable ptop. Essentially, pstack maintains the association between the ‘pushed’ prompts and the corresponding push_prompt’s frames on the real stack – precisely what we need for implementing Midc . From now on, the transcription from Midc to OCaml is straightforward. First we implement the pushP pe and pushP pv transitions of Mdc (inherited by Midc ): let push_prompt_aux (p : ’a prompt) (body : unit -> ’a) : ’a = let pframe = {pfr_mark = p.mark; pfr_ek = get_ek ()} in let () = ptop := pframe :: (!ptop) in let res = body () in p.mbox := fun () -> res; raise DelimCCE let push_prompt (p : ’a prompt) (body : unit -> ’a) : ’a = try push_prompt_aux p body with | DelimCCE -> (match !ptop with h::t -> assert (h.pfr_mark == p.mark); ptop := t; mbox_receive p) | e -> match !ptop with h::t -> assert(h.pfr_mark==p.mark); ptop:=t; raise e
316
O. Kiselyov
The try-block establishes an exception frame, on the top of which we build the call frame for the evaluation of the body – or, of the wrapper push_prompt_aux. That call frame will be at the very bottom of ekfragment when the continuation is captured. The wrapper pushes a new pframe onto pstack, which push_prompt removes upon normal or exceptional exit. The assert expresses the invariant: every exception frame created by push_prompt corresponds to a pframe. That pframe is on the top of pstack iff push_prompt’s exception frame is the latest exception frame. The body may finish normally, returning a value. It may also invoke take_subcont capturing and removing the part of the stack up to push_prompt, thus sending the value to push_prompt ‘directly’. We use a mailbox for such communication, see §4.1. In fact, the above code is an elaboration of the code in §4.1, using prompt, mbox_receive defined in that section. The code for take_subcont is too an elaboration of the code in §4.1; now it has to capture the continuation rather than simply disregarding it. In Midc , we capture the continuation between two pushP frames, that is, between two exception frames. The captured continuation: type (’a,’b) subcont = {subcont_ek : ekfragment; subcont_ps : pframe list; subcont_bs : ek; subcont_pa : ’a prompt; subcont_pb : ’b prompt}
includes two mailboxes (to receive a value when the continuation is reinstated and to send the result), the copy of the OCaml stack ekfragment, and the corresponding copy of the parallel stack. The latter is a list of pframes in reverse order. We note in subcont_bs the base of the ekfragment, the identity of the exception frame left on the stack after the ekfragment is removed. We need the base to adjust pfr_ek fields of pframes when the continuation is reinstated. The transition takeSC of Midc requires locating the latest frame pushP p with the given prompt p and splitting the stack at that point. This job is now done by unwind, which scans the pstack returning h, the pframe corresponding to a given prompt (identified by its mark). let rec unwind acc mark = function | [] -> failwith "No prompt was set" | h::t as s -> if h.pfr_mark == mark then (h,s,acc) else unwind (h::acc) mark t
The function also splits pstack at h, returning the part up to but not including h as acc, in reverse frame order. The function take_subcont straightforwardly implements the takeSC transition of Midc , removing the fragments from the real and parallel stack, packaging them into a subcont structure. First, however, take_subcont must push the frame pushP p with a fresh prompt p . That prompt will never be referred to in any take_subcont function, see §3; therefore, we should not register the pushP p frame in pstack. We use push_prompt_simple to push such an ‘ephemeral’ prompt, used only as a mailbox.
Delimited Control in OCaml, Abstractly and Concretely: System Description
317
let push_prompt_simple (p: ’a prompt) (body: unit -> unit) : ’a = try body (); raise DelimCCE with DelimCCE -> mbox_receive p let take_subcont (p: ’b prompt) (f: (’a,’b) subcont -> unit->’b) : ’a = let pa = new_prompt () in push_prompt_simple pa (fun () -> let (h,s,subcontchain) = unwind [] p.mark !ptop in let () = ptop := s in let ek = h.pfr_ek in let sk = get_ek () in let ekfrag = pop_stack_fragment ek sk in p.mbox := f {subcont_ek = ekfrag; subcont_pa = pa; subcont_pb = p; subcont_ps = subcontchain; subcont_bs = ek})
The function push_subcont is the transcription of Midc ’s transition pushSC. let push_subcont (sk : (’a,’b) subcont) (m : unit -> ’a) : ’b = let pb = sk.subcont_pb in push_prompt_simple pb (fun () -> let base = sk.subcont_bs in let ek = get_ek () in List.iter (fun pf -> ptop := {pf with pfr_ek = add_ek ek (sub_ek pf.pfr_ek base)} :: !ptop) sk.subcont_ps; sk.subcont_pa.mbox := m; push_stack_fragment sk.subcont_ek)
When we push the ekfragment onto the stack, the identities of the exception frames therein may change. We have to ‘re-base’ pfk_ek fields of pframes in the parallel stack fragment to restore the correspondence.
5
Related Work
The paper [9] that introduced multi-prompt delimited control presented its implementation in SML/NJ, relying on local exceptions and call/cc. Later the same authors offered an OCaml implementation [13], using “a very naive experimental brute-force version of callcc that copies the stack”, along with Obj.magic, or unsafe coerce. Not only copying of the entire control stack to and from the heap on each use of control operators that is problematic. Since now delimited continuations capture (much more) of the stack than needed, the values referred from the unneeded part cannot be garbage-collected: The implementation has a memory leak. Furthermore, the correctness of the OCaml call/cc implementation [20] is not obvious as it copies the stack regardless of whether the byte-code interpreter is at a stable point or not. Perhaps for that reason the users of call/cc are warned that its “Use in production code is not advised”[20]. Multi-prompt delimited control was further developed and formalized in [10], who also presented indirect implementations in Scheme and Haskell. The Scheme implementation used call/cc, and the Haskell used the continuation monad along with unsafeCoerce. A direct and efficient implementation of single-prompt delimited control (shift/reset) was first described in [21], specifically for Scheme48. The implementation relied on the hybrid stack/heap strategy for activation frames, particular to Scheme48 and a few other Scheme systems. The implementation required several
318
O. Kiselyov
modifications of the Scheme48 run-time. On many benchmarks, the paper [21] showed the impressive performance of the direct implementation of shift/reset compared to the call/cc emulation. The implementation, alas, has not been available as part of Scheme48. The paper specifically left to future work relating the implementation to the specification of shift/reset. Recently there has been interest in direct implementations (as compared to the call/cc-based one [22] in SML/NJ) of the single prompt shift/reset in the typed setting [23, 24]. Supporting delimited control required modifying the compiler or the run-time, or both. Many efficient implementations of undelimited continuations have been described in Scheme literature, e.g. [12]. Clinger et al. [25] is a comprehensive survey. Their lessons hold for delimited control as well. Sekiguchi et al. [26] use exceptions to implement multi-prompt delimited control in Java and C++. Their method relies on source- or byte-code translation, changing method signatures and preventing mixing the translated code with untranslated libraries. The run-time overhead is especially notable for the control-operator–free portions of the code. A similar, more explicit transformation technique for source Scheme programs is described in [27], with proofs of correctness. The approach, alas, targets undelimited continuations, which brings unnecessary complications. The translation is untyped, deals only with a subset of Scheme and too has difficulties interfacing third-party libraries.
6
Conclusions
We have presented abstract and concrete implementations of multi-prompt delimited control. The concrete implementation is the delimcc OCaml library, which has been fruitfully used for over four years. The abstract implementation has related delimited control to exception handling and distilled scAPI, a minimalistic API, sufficient for the implementation of delimited control. A language system accommodating exception handling and stack-overflow recovery is likely to support scAPI. The OCaml byte-code does support scAPI, and thus permits, as it is, the implementation of delimited control. We described the implementation of delimcc as an example of using scAPI in a typed language. OCaml exceptions and delimited control integrate and benefit each other. OCaml exception frames naturally implement stable points of scAPI. Exception handlers may be captured in delimited continuations, and re-instated along with the captured continuation; exceptions remove the prompts. Conversely, delimcc effectively provides local exception declarations, hitherto missing in OCaml. In the future, we would like to incorporate the lessons learned in efficient implementations of undelimited continuations, in particular, stack segmentation of [12]. Determining if the native-code OCaml compiler can support scAPI efficiently requires further investigation.5 We also want to apply the scAPI-based 5
The main difficulty is the natively compiled code’s using the C stack, which may contain unboxed values. The naive copying of such stack fragments to and from the heap requires many movements and GC root registrations.
Delimited Control in OCaml, Abstractly and Concretely: System Description
319
approach to implementing delimited control in other language systems. The formal part of the paper can be extended further by adding state and stack-copying primitives to Mex and relating the result to Midc . Acknowledgements. I thank Paul Snively for inspiration and encouragement. I am immensely grateful to Chung-chieh Shan for numerous helpful discussions and advice that improved the content and the presentation. Many helpful suggestions by anonymous reviewers and Kenichi Asai are greatly appreciated.
References [1] Kiselyov, O.: Native delimited continuations in (byte-code) OCaml (2006), http://okmij.org/ftp/Computation/Continuations.html#caml-shift [2] Kiselyov, O., Shan, C.-c., Sabry, A.: Delimited dynamic binding. In: ICFP, pp. 26–37 (2006) [3] Kiselyov, O., Shan, C.-c.: Embedded probabilistic programming. In: Taha, W.M. (ed.) Domain-Specific Languages. LNCS, vol. 5658, pp. 360–384. Springer, Heidelberg (2009) [4] Kiselyov, O., Shan, C.-c.: Monolingual probabilistic programming using generalized coroutines. In: Uncertainty in Artificial Intelligence (2009) [5] Kiselyov, O.: Persistent delimited continuations for CGI programming with nested transactions. Continuation Fest 2008 (2008), http://okmij.org/ftp/Computation/Continuations.html#shift-cgi [6] Kameyama, Y., Kiselyov, O., Shan, C.-c.: Shifting the stage: Staging with delimited control. In: PEPM, pp. 111–120 (2009) [7] Kiselyov, O., Shan, C.-c.: Lifted inference: Normalizing loops by evaluation. In: Proc. 2009 Workshop on Normalization by Evaluation, BRICS (2009) [8] Kiselyov, O.: Ask-by-need: On-demand evaluation with effects (2007), http://okmij.org/ftp/Computation/Continuations.html#ask-by-need [9] Gunter, C.A., R´emy, D., Riecke, J.G.: A generalization of exceptions and control in ML-like languages. In: Functional Programming Languages and Computer Architecture, pp. 12–23 (1995) [10] Dybvig, R.K., Peyton Jones, S.L., Sabry, A.: A monadic framework for delimited continuations. J. Functional Progr. 17, 687–730 (2007) [11] Balat, V., Di Cosmo, R., Fiore, M.P.: Extensional normalisation and type-directed partial evaluation for typed lambda calculus with sums. In: POPL 2004, pp. 64–76 (2004) [12] Hieb, R., Dybvig, R.K., Bruggeman, C.: Representing control in the presence of first-class continuations. In: PLDI 1990, pp. 66–77 (1990) [13] Gunter, C.A., R´emy, D., Riecke, J.G.: Return types for functional continuations (1998), http://pauillac.inria.fr/~ remy/work/cupto/ [14] Kiselyov, O.: Zipper in Scheme (2004), comp.lang.scheme, http://okmij.org/ftp/Scheme/zipper-in-scheme.txt [15] Kiselyov, O.: Zipper as a delimited continuation. Message to the Haskell mailing list (2005), http://okmij.org/ftp/Haskell/Zipper1.lhs [16] Felleisen, M.: The theory and practice of first-class prompts. In: POPL, pp. 180– 190 (1988) [17] Leroy, X.: The ZINC experiment: An economical implementation of the ML language. Technical Report 117, INRIA (1990)
320
O. Kiselyov
[18] Glew, N.: Type dispatch for named hierarchical types. In: ICFP, pp. 172–182 (1999) [19] Leroy, X.: The bytecode interpreter. version 1.96, in OCaml distribution (2006), byterun/interp.c [20] Leroy, X.: Ocaml-callcc: call/cc for ocaml (2005), http://pauillac.inria.fr/~ xleroy/software.html#callcc [21] Gasbichler, M., Sperber, M.: Final shift for call/cc: Direct implementation of shift and reset. In: ICFP, pp. 271–282 (2002) [22] Filinski, A.: Representing monads. In: POPL, pp. 446–457 (1994) [23] Masuko, M., Asai, K.: Direct implementation of shift and reset in the MinCaml compiler. In: ACM SIGPLAN Workshop on ML (2009) [24] Rompf, T., Maier, I., Odersky, M.: Implementing first-class polymorphic delimited continuations by a type-directed selective CPS-transform. In: ICFP, pp. 317–328 (2009) [25] Clinger, W.D., Hartheimer, A.H., Ost, E.M.: Implementation strategies for firstclass continuations. Higher-Order and Symbolic Computation 12, 7–45 (1999) [26] Sekiguchi, T., Sakamoto, T., Yonezawa, A.: Portable implementation of continuation operators in imperative languages by exception handling. In: Romanovsky, A., Dony, C., Knudsen, J.L., Tripathi, A.R. (eds.) ECOOP-WS 2000. LNCS, vol. 2022, pp. 217–233. Springer, Heidelberg (2001) [27] Pettyjohn, G., Clements, J., Marshall, J., Krishnamurthi, S., Felleisen, M.: Continuations from generalized stack inspection. In: ICFP, pp. 216–227 (2005)
Automatic Parallelization of Recursive Functions Using Quantifier Elimination Akimasa Morihata1 and Kiminori Matsuzaki2 1
JSPS research fellow, University of Tokyo 2 Kochi University of Technology
Abstract. Although the recent popularity of parallel-computing environments has called for parallel programs, it is difficult for nonspecialists to develop those that are efficient. What is required are parallelization methods that can automatically generate efficient parallel programs from sequential ones. In this paper, we propose an automatic method of parallelization for recursive functions. The key is a quantifier-eliminationbased derivation of an operator that shrinks function closures representing partial computations. Once we obtain such an operator, we can split the input structure and perform computation on each part in parallel. Our method has several features: it does not require any human help, it guarantees computational efficiency of generated programs, and it deals with complicated recursive functions such as those that are nonlinear recursive, non-self recursive, and accumulative.
1
Introduction
Parallel-computing environments have recently become popular. Personal computers are commonly equipped with more than one core. Yet, we have not sufficiently experienced the effectiveness of parallel-computing environments because of the difficulty of developing parallel programs. To develop efficient parallel programs, we need to manage several matters that sequential programs do not have, such as task distributions and processor communications. We hope to automatically obtain efficient parallel programs from sequential ones. Automatic methods of parallelization have been researched in depth [1,2,3,4,5,6], and now even standard textbooks on compilers devotes a great deal of space to parallelization [7]. However, as far as we know, there have been no automatic methods of parallelization that can derive efficient parallel programs from following sequential program height , which computes the height of a binary tree. height Leaf =0 height (Node l v r) = 1 + (if height l > height r then height l else height r) The automatic parallelization of height poses two major difficulties. The first is the efficiency of the derived program. We can think of height as a divide-andconquer parallel program that computes the height of each independent subtree in parallel. However, such a naive parallel program is not generally efficient. M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 321–336, 2010. c Springer-Verlag Berlin Heidelberg 2010
322
A. Morihata and K. Matsuzaki
Consider a very slender tree. Since there are few independent subtrees of appropriate size, we cannot take advantage of parallel speedups. When we intend to derive a more efficient parallel program, we encounter the second difficulty, i.e., its rather complicated recursion structure. The function height computes its result by combining multiple recursive calls and conditional expressions. If our target of parallelization is a simple computation such as summation, we could consider horizontally cutting slender trees in the middle. However, in the case of height , the computations appear to have no parallelism other than the naive one. In this paper, we propose an automatic method of parallelization. We consider parallel programs that divide the input structure into chunks, compute small function closures each of which represents partial computation on a chunk, and then calculate the result by composing the function closures. The key is the way to shrink function closures. We propose deriving an operator that shrinks function closures based on quantifier elimination [8]. Our method has three main features. – Suitable for automatic implementation: Our method requires neither background knowledge, directives, nor human interactions. We describe our automatic parallelization procedure in Sect. 3. – Guaranteed efficiency: Our method yields efficient parallel programs in the sense that they show linear speedups with respect to the number of processors (Theorem 2). – Capability for dealing with involved recursive functions: Our method allows us to use conditional expressions. Moreover, it can be naturally extended so that involved recursive functions can be dealt with, such as those that are nonlinear recursive, non-self recursive, and accumulative. We discuss these extensions in Sect. 4. We also report the experiments we carried out with our prototype implementation in Sect. 5.
2 2.1
Preliminary Basic Notions
We borrow notations from Haskell [9] to describe programs. We omit parentheses for function applications, which precede operator applications; thus, a + f x is equivalent to a + (f x). An operator (◦) denotes a composition of two functions and its definition is (f ◦ g) x = f (g x). We mainly consider list-manipulating functions. Lists are constructed from the empty list [ ] and the left-extension operator (:). (+ +) denotes an operator that concatenates two lists. We use the standard functions in Fig. 1. 2.2
Quantifier Elimination
We consider the first-order predicate logic on real numbers. A formula is said to be quantifier-free if it does not contain any quantifiers. A variable in a formula
Automatic Parallelization of Recursive Functions
323
foldr f e [ ] =e foldr f e (a : x) = f a (foldr f e x) scanl f e [ ] = [e] scanl f e (a : x) = e : scanl f (f e a) x scanr f e [ ] = [e] scanr f e (a : x) = foldr f e (a : x) : (scanr f e x) Fig. 1. Definitions of Standard Functions
is said to be free if it is not quantified. Given variable x and terms t1 and t2 , t1 [t2 /x] denotes a term that is obtained by substituting all free occurrences of x in t1 by t2 . Quantifier elimination [8] is a method of transforming a formula into an equivalent quantifier-free formula. Since quantifier-free formulae are easy to reason, quantifier elimination is useful for deduction and many quantifier-elimination procedures have been developed. Above all, quantifier-elimination procedures called “quantifier elimination by virtual substitutions” are known to be suitable for practical implementation. The idea of the virtual-substitution-based methods is to use a finite set of test cases, called an elimination set. Definition 1 (elimination set [10]). Given quantifier-free formula ψ and variable x, finite set S of terms is said to be an elimination set of ∃x. ψ if ∃x. ψ ⇐⇒ ψ[t/x] t∈S
holds. The elimination sets of ∀x. ψ are those of ∃x. ¬ψ. An elimination set of ∃x. ψ contains at least one instance of x satisfying ψ if one exists. Therefore, we can eliminate the quantifier by checking each element of an elimination set. For example, consider formula ∃x. ax2 + 2x + b < 0 where a = 0. One of its elimination set is {−∞, −1/a, ∞}, i.e., both ends and the extremum in the middle. Substituting each element into the formula, we obtain (a(−∞)2 +2(−∞)+b < 0)∨(a(−1/a)2 +2(−1/a)+b < 0)∨(a(∞)2 +2(∞)+b < 0). The formula is simplified into a < 0 ∨ ab < 1, which is quantifier-free and equivalent to the original formula. 2.3
Basis of Parallelization
We consider the small first-order fragment of Haskell shown in Fig. 2 as the target of parallelization. A program consists of a set of function declarations. Each function is real-valued, and defined in a structural-recursive manner. Expressions consist of constant values, variables, function calls, additions, and conditional expressions. Conditions are specified by logical combinations of equations and inequalities. We assume that all functions are total, and do not consider undefined values or non-terminating computations.
324
A. Morihata and K. Matsuzaki
prog decl e p
::= ::= ::= ::=
decl · · · decl f (C x · · · x) y · · · y = e n | y | f x e · · · e | e + e | if p then e else e p ∧ p | p ∨ p | ¬p | e ≥ e | e > e | e ≤ e | e < e | e = e | e = e
{ Program } { Declaration } { Expression } { Predicate }
Fig. 2. Target Language: x and y denote variables, and f , C, and n respectively denote a function, a constructor, and a real-valued constant
A recursive function is said to be non-accumulative if its arity is one, and accumulative otherwise. A recursive function is said to be self-recursive if it does not call functions other than itself, and non-self-recursive otherwise. We consider exclusive-read exclusive-write parallel random-access machines as the parallel computation environments. Unless otherwise stated, n and p are respectively the size of the input structure and the number of processors.
3
Parallelization by Composition Evaluators
We first study on parallelizing non-accumulative self-recursive list-operating functions. Since such functions can be recognized as foldr , we consider parallelizing foldr . We will consider more involved ones in the next section. 3.1
Overview
Here, we overview our method of parallelization. Let us consider the following maximum-extracting function. maximum [ ] =0 maximum (a : x) = if a > maximum x then a else maximum x Intuitively, maximum can be easily parallelized. It is sufficient to consider the maximum of each portion of the list, and we can calculate the maximum of each portion in parallel. We would like to mechanically reveal this intuition. First, note that maximum is an instance of foldr : maximum = foldr max 0 where max a r = if a > r then a else r. Therefore, given list x = [a0 , a1 , . . . , an−1 ], maximum x is equivalent to ((max a0 ) ◦ (max a1 ) ◦ · · · ◦ (max an−1 )) 0. Next, consider shrinking the sequences of function compositions in parallel: the first processor tries to shrink (max a0 ) ◦ · · · ◦ (max ak−1 ), the second takes charge of (max ak ) ◦ · · · ◦ (max a2k−1 ), and so on. This shrinking may appear to be too difficult. In fact, our parallelization procedure automatically reveals that we can shrink (max a) ◦ (max b) to max c where c is the maximum of a and b. Then, by shrinking the sequences of compositions in parallel, we finally obtain closure max a∗ , and max a∗ 0 is the result of maximum x. 3.2
Context Preservation Theorem
Our parallelization is based on the context preservation theorem [11].
Automatic Parallelization of Recursive Functions
325
Input: list [a0 , . . . , an−1 ], program foldr f e, set F , and composition evaluator g do parallel (0 ≤ i < n) let bi be the index such that f ai = fbi ∈ F. do parallel (0 ≤ j < p) c0,j := bjn/p ⊕g bjn/p+1 ⊕g · · · ⊕g b(j+1)n/p−1 ; for (1 ≤ i ≤ log p) do parallel (0 ≤ j < p/2i ) ci,j := ci−1,2j ⊕g ci−1,2j+1 ; Output: fclog p,0 e, which is equivalent to foldr f e [a0 , . . . , an−1 ] Fig. 3. Pseudo-code of Parallel Programs Obtained from Theorem 1: for simplicity, we assume n is divisible by p and p = 2k holds for some k; ⊕g is defined as a ⊕g b = g a b and is associative
Definition 2. Set of indexed functions F is said to be closed under composition if for any indexes a and b (fa , fb ∈ F ), there exists an index c such that fa ◦ fb = fc ∈ F holds. Definition 3 (composition evaluator). Given set of indexed functions F that is closed under composition, function g is said to be a composition evaluator of F if (g a b = c) ⇒ (fa ◦ fb = fc ) where fa , fb , fc ∈ F . Theorem 1 (context preservation theorem for foldr [11]). foldr f e can be evaluated in O(n/p + log p) time, if there is a set of indexed functions F such that (i) each element of F is a constant-time computation, (ii) for any a of the appropriate type, we can find index i satisfying f a = fi ∈ F in constant time, and (iii) F is closed under composition and its composition evaluator is a constant-time computation. Figure 3 shows a pseudo-code of parallel programs obtained from Theorem 1. The main point is to calculate the indexes of functions using the composition evaluator. 3.3
Deriving Composition Evaluator from Elimination Set
Now let us consider deriving a composition evaluator. Given two functions fa and fb , we hope to obtain c such that fa ◦fb = fc holds. The key is an elimination set. Consider formula ∃c. fa ◦ fb = fc . If such c exists, each elimination set of the formula contains an instance of such c. Thus, we can construct a composition evaluator by examining which we should select for c from the elimination set. Lemma 1. Given formula ∃x. ψ, let S = {s1 , s2 , . . . , sk } be its elimination set and y1 , . . . , yn be free variables in ψ other than x. Then, ∀y1 , . . . , yn . ((∃x. ψ) ⇔ ψ[(g y1 · · · yn )/x]) holds, where g is a function defined as follows. g y1 · · · yn = if ψ[s1 /x] then s1 else if ψ[s2 /x] then s2 .. . else if ψ[sk−1 /x] then sk−1 else sk
326
A. Morihata and K. Matsuzaki
Proof. It is evident that ∀y1 , . . . , yn . ((∃x. ψ) ⇐ ψ[(g y1 · · · yn )/x]) holds. The following calculation proves the opposite direction. ∀y1 , . . . , yn . ((∃x. ψ) ⇒ ψ[(g y1 · · · yn )/x]) ⇐⇒ { eliminationset } ∀y1 , . . . , yn . (( s∈S ψ[s/x]) ⇒ ψ[(g y1 · · · yn )/x]) ⇐⇒ { logical implication } ∀y1 , . . . , yn . (( s∈S ¬ψ[s/x]) ∨ ψ[(g y1 · · · yn )/x]) ⇐⇒ { distributivity } ∀y1 , . . . , yn . s∈S (¬ψ[s/x] ∨ ψ[(g y1 · · · yn )/x]) ⇐⇒ { definition of g } ∀y1 , . . . , yn . s∈S (ψ[s/x] ⇒ ψ[s/x]) Note that free variables y1 , . . . , yn are quantified at the outermost position in ∀y1 , . . . , yn . ((∃x. ψ) ⇔ ψ[(g y1 · · · yn )/x]). This means that the substitution by g preserves the value of the formula. Hence, Lemma 1 is different from classic Skolemization that only preserves satisfiability. Lemma 1 enables us to construct a composition evaluator. Lemma 2. Given set of indexed functions F that is closed under composition, let ψ be a formula that is quantifier-free and equivalent to ∀r. (fa (fb r) = fc r) where fa , fb , fc ∈ F . Then, function g constructed from ∃c. ψ as described in Lemma 1 is a composition evaluator of F . Proof. ∀a, b. ψ[(g a b)/c] follows from Lemma 1 because F is closed under composition; thus, since g a b does not concern r, ∀a, b. (∀r. (fa (fb r) = fc r)[(g a b)/c]) holds. Lemma 2 shows that Lemma 1 leads to a composition evaluator. It is worth noting that we can also deal with multi-indexed functions. For example, consider a set of two-indexed functions F and let ψ be a quantifier-free equivalent of ∀r. fa,b (fc,d r) = fp,q r where fa,b , fc,d , fp,q ∈ F . Based on Lemma 1, we construct two functions g1 and g2 respectively from ∃p. ψ and ∃q. ψ[(g1 a b c d q)/p]. Then, function g (a, b) (c, d) = (g1 a b c d (g2 a b c d), (g2 a b c d)) is a composition evaluator of F if F is closed under composition. Now let us introduce our parallelization procedure. Given non-accumulative self-recursive function h, we parallelize it as follows. Procedure 1. 1. Recognize h to be foldr and let foldr f e = h. 2. Let F be set {fa | fa = f a ∧ a ∈ IR}. 3. Calculate quantifier-free formula ψ that is equivalent to ∀r. fa (fb r) = fc r where fa , fb , fc ∈ F . 4. Check whether ∀a, b. ∃c. ψ holds. If not, we fail in parallelizing h. 5. Construct function g from ∃c. ψ as described in Lemma 1. 6. Generate a parallel program following Theorem 1. Since h is a self-recursive non-accumulative list-operating function, Step 1 is easily achieved by specifying computation applied to recursive calls. We can use
Automatic Parallelization of Recursive Functions
327
known methods [12,13] for dealing with more involved functions. For implementing Steps 3–5, we adopt the method by Loos and Weispfenning [10] that yields an elimination set for any first-order formula on linear equations and inequalities; then, we can both eliminate quantifiers and construct composition evaluators. Note that because of the restrictions of our language, f should be described by combinations of additions and conditionals; hence, equation fa (fb r) = fc r can be straightforwardly rewritten into an equivalent formula that consists of linear equations and inequalities. Procedure 1 certainly yields efficient parallel programs; moreover, it has a kind of completeness property. Both are direct consequences of Lemma 2. Theorem 2. The parallel program obtained from Procedure 1 computes the same result as h in O(n/p + log p) time. Theorem 3. Procedure 1 succeeds in parallelizing a program if F given in the procedure is closed under composition. Example: maximum Let us parallelize function maximum. First, consider formula ∀r. f a (f b r) = f c r where f a r = if a > r then a else r. The formula is equivalent to ∀r. (b > r ∧ a > b ∧ c > r ∧ a = c) ∨ (b > r ∧ a > b ∧ c ≤ r ∧ a = r) ∨ (b > r ∧ a ≤ b ∧ c > r ∧ b = c) ∨ (b > r ∧ a ≤ b ∧ c ≤ r ∧ b = r) ∨ (b ≤ r ∧ a > r ∧ c > r ∧ a = c) ∨ (b ≤ r ∧ a > r ∧ c ≤ r ∧ a = r) ∨ (b ≤ r ∧ a ≤ r ∧ c > r ∧ r = c) ∨ (b ≤ r ∧ a ≤ r ∧ c ≤ r ∧ r = r) and can be simplified into following quantifier-free formula ψ (Step 3). ψ = (b ≥ c ∨ a ≥ c) ∧ (b ≥ a ∨ c ≥ a) ∧ (c ≥ b ∨ a ≥ b) ∀a, b. ∃c. ψ holds (Step 4). {a, b} is an elimination set of ∃c. ψ, and thus, a composition evaluator is g a b = if a ≥ b then a else b (Step 5). Then, we obtain a parallel program. Deriving Efficient Composition Evaluator. For deriving an efficient composition evaluator, it is important to use a small elimination set in Lemma 1 because the size of the elimination set determines the number of conditional branches in the obtained composition evaluator. The following lemma is useful to find a smaller elimination set. Note that we can mechanically check the premise by using quantifier elimination. Lemma 3. Let S be an elimination set of ∃x.ψ. Then, S = S \ {s} is an elimination set of ∃x. ψ, provided that ψ[s/x] ⇒ t∈S ψ[t/x] holds. Proof. ∃x. ψ ⇐⇒ t∈S ψ[t/x] ⇐⇒ ψ[s/x] ∨ ( t∈S ψ[t/x]) ⇐⇒ t∈S ψ[t/x]
328
3.4
A. Morihata and K. Matsuzaki
Heuristics for Obtaining Sets Closed under Composition
Procedure 1 finds a composition evaluator if one exists (Theorem 3). Yet, even simple functions often lead to a set that is not closed under composition. We borrow two heuristics from Fisher and Ghuloum [1] for such cases. Function length is a typical example that cannot be directly parallelized. length [ ] =0 length (a : x) = 1 + length x Let f a r = 1 + r; then length = foldr f 0. We want to calculate c satisfying f a (f b r) = f c. However, the left-hand-side yields 2 + r that cannot match the right-hand-side. In such cases, we generalize constant-valued expressions to variables and parallelize the following function length instead of length. length [ ] =0 length (a : x) = α + length x Apparently, parallelizing length suffices for our purposes. Now consider the set of functions that are indexed by both a and α, as fa,α r = α + r. Then, the set is closed under composition: fa,α (fb,β r) = fc,α+β r where c can be any value. Therefore, we can obtain a parallel program. There are several strategies for introducing variables. In our implementation, we generalize each constant value except for 0 by a fresh variable at Step 2 of Procedure 1. Since 0 is the unit of (+), generalization of 0 might introduce many unnecessary variables and thus we avoid generalizing it. The other heuristics is to expand the set of functions by considering their compositions. Consider the maximum prefix sum problem [5,14]: given a list, find its prefix of the maximum total sum and return the sum. For example, the maximum prefix sum of [−4, 3, −2, 4, −1, −5, 2] is 1 because [−4, 3, −2, 4] is the prefix of the maximum sum. The following function mps calculates the result. mps [ ] =0 mps (a : x) = if a + mps x > 0 then a + mps x else 0 Even though mps = foldr f 0 where f a r = if a + r > 0 then a + r else 0, F = {fa | fa = f a} is not closed under composition: there is no c satisfying ∀r. f2 (f−1 r) = fc r because the right-hand-side results in c + r or 0 while the left-hand-side results in 1 + r (if r > 1) or 2 (if r ≤ 1). Here, we consider F 2 = F ∪ {fa,b | fa,b = fa ◦ fb where fa , fb ∈ F } instead of F as a set of indexed functions. Then, F 2 is closed under composition, and we can parallelize mps by deriving a composition evaluator of F 2 . More concretely, we modify Step 4 of Procedure 1. If ∀a, b. ∃c. ψ does not holds, we let F 2 be the new set of indexed functions, i.e., F := F 2 , and retry the procedure from Step 3. It is worth noting that these techniques will introduce functions having several indexes. Then, since a composition evaluator should compute several values, the obtained parallel program may not be p times as fast as the sequential one. Nevertheless, the obtained parallel programs are scalable with respect to the number of processors.
Automatic Parallelization of Recursive Functions
4
329
Parallelizing Complex Recursive Functions
This section demonstrates that our method can naturally cooperate with known techniques in parallelizing rather involved functions, namely those that are nonlinear recursive (Sect. 4.1), non-self-recursive, and accumulative (Sect. 4.2). In addition, we discuss the applicability and limitations of our method in Sect. 4.3. 4.1
Parallelizing Non-linear Recursive Functions
Let us consider tree-operating functions rather than list-operating ones. We use parallel tree-contraction algorithms [15,16], which enable us to develop parallel programs that are efficient even for slender trees. The following is a context preservation theorem for evaluating tree-operating functions based on parallel tree contraction. Theorem 4 ([17]). Given set H of functions, consider an expression that consists of functions in H and primitive values. The expression can be evaluated in O(n/p + log p) time, if there is set of indexed functions F such that (i) each element of F is a constant-time computation, (ii) given k-ary function h ∈ H, number 1 ≤ i ≤ k, and values a1 , . . . , ai−1 , ai+1 , . . . , ak of the appropriate types, we can find function fv ∈ F satisfying fv w = h a1 · · · ai−1 w ai+1 · · · ak in constant time, and (iii) F is closed under composition and its composition evaluator is a constant-time computation. For example, consider height discussed in the introduction. Given a tree t, the computation of height t can be regarded as evaluating an expression whose shape is the same as t, each of whose internal nodes is function h lv rv = 1 + (if lv > rv then lv else rv ), and each of whose leaves is value 0. Then, Theorem 4 shows a way of obtaining a parallel program. First, since operation h contains constant 1, we generalize it and consider functions hα lv rv = α + (if lv > rv then lv else rv ). Next, we construct set of indexed functions F by fixing one of the arguments of hα , i.e., F = {fL,l,α | fL,l,α x = hα l x} ∪ {fR,r,α | fR,r,α x = hα x r} where L and R are distinguishable values. Though F is not closed under composition, expanded set F 2 is closed under composition. Then, we can obtain a parallel program by deriving a composition evaluator of F 2 . 4.2
Parallelizing Complex Recursions and Accumulations
Diffusion [18,19], which decomposes a complicated recursive function into compositions of simple functions, is effective for dealing with non-self-recursive and accumulative functions. Here, we just glance at the diffusion strategy. Refer to the original paper [18] for details including formal statements and mechanization.
330
A. Morihata and K. Matsuzaki
For example, consider mts that computes the results for the maximum tailsegment sum problem, which is the dual of the maximum prefix sum problem. mts [ ] =0 mts (a : x) = if a + sum x > mts x then a + sum x else mts x sum [ ] =0 sum (a : x) = a + sum x Because mts uses the result of sum, while sum does not call mts, we can evaluate sum first and mts afterward. Therefore, we decompose mts and introduce scanr (+) 0 that computes the values of sum in advance.1 mts x = let (b : x ) = scanr (+) 0 x mts [ ] =0 mts ((a, s) : y) = if a + s > mts y then a + s else mts y in mts (zip x x ) zip is the standard zipping function and is naturally parallelizable. mts is selfrecursive and can be dealt with by our method. Moreover, we can parallelize scanr using the same method as foldr . Theorem 5 ([11]). scanr f e can be evaluated in O(n/p + log p) time if the same condition as Theorem 1 holds. In summary, we can parallelize mts with our method and diffusion. The case of accumulative functions is similar. Consider another program mps for calculating the maximum prefix sum, where the initial value of the accumulative parameter s is 0. mps [ ] s = if s > 0 then s else 0 mps (a : x) s = if s > mps x (s + a) then s else mps x (s + a) Since accumulative computations are the same for all recursive calls, we can decompose mps as follows. mps x = let (x + + [b]) = scanl (+) 0 x mps [ ] = if b > 0 then b else 0 mps ((a, s) : y) = if s > mps y then s else mps y in mps (zip x x ) Because the dual of Theorem 5 also holds [11], our method can deal with both scanl (+) 0 and mps . Then, we can parallelize mps . It is worth noting that Theorem 5 is useful if we do not consider diffusion. scanr and scanl are important computation pattern and frequently appears [20]. To apply diffusion to nonlinear recursions, we should prepare tree-versions of scanr and scanl . Gibbons et al. [21] introduced binary-tree versions of scanr and scanl , called an upward accumulation and a downward accumulation, together 1
Here we consider (−, −) : − as a constructor.
Automatic Parallelization of Recursive Functions
331
with their efficient parallel implementations. We can extend them to non-binary trees based on our parallel tree-contraction algorithm [17]; then, under a similar condition to Theorem 4, we can compute upward/downward accumulations in O(n/p + log p) time. Therefore, diffusion can be applied to nonlinear recursive functions. Nevertheless, the current diffusion techniques cannot deal with functions that iterate on trees neither upward nor downward manners, such as prefix/postfix/infix traversals. Parallelization of these computation patterns is a topic of further study. Even when several functions depend on one another, we can eliminate the dependencies using tupling transformation [22,23]. Tupling transformation converts a set of functions into a function that simultaneously computes all values. Refer to preceding studies [5,24,25] for applications of tupling to parallelization. 4.3
Applicability and Limitation
By virtue of its combination of known techniques, our method can parallelize many programs, including several problems that have been considered in the literature on the parallelization of functional programs. Even without diffusion or tupling, our method can parallelize various programs, such as those that calculate the total sum, count positive numbers, extract the maximum/minimum element, calculate the maximum prefix sum, and accomplish reductions by Boolean operations. With diffusion and tupling, our method can parallelize programs such as those for the maximum tail-segment sum, the maximum segment sum [1,5], the maximum non-adjoining-elements sum, and the line-of-sight problem [5]. We can also parallelize tree-operating programs such as those for calculating the height, the diameter, the maximum path weight [26], the party-planning problem [4], and tree versions of the list-operations previously mentioned. Although our method is powerful, it has its limitations. For example, it cannot parallelize function pow2 . pow2 [ ] =1 pow2 (a : x) = pow2 x + pow2 x Even though pow2 is an instance of foldr , i.e., pow2 = foldr f 1 where f a r = r + r, the set F = {fa | f a} is not closed under composition: fa (fb r) = r+r+r+r = fc r. Moreover, the heuristics in Sect. 3.4 cannot resolve this problem. An appropriate set of indexed functions is {fv,a | fv,a r = v × r}, which satisfies the requirement of Theorem 1. However, our method cannot construct any operations that cannot be specified by combinations of linear inequalities. Another example is the following slightly strange maximum-extracting function. =0 max [ ] max (a : x) = if a > maximum x then a else max x Since max calls maximum, which is the usual maximum-extracting function, we apply diffusion and try to parallelize max . =0 max [ ] max ((a, m) : x) = if a > m then a else max x
332
A. Morihata and K. Matsuzaki
Then we consider set F = {fa,m | fa,m r = if a > m then a else r}. However, F is not closed under composition. fa,m (fa ,m r) can result in either a, a , or r, while the three possibilities cannot be expressed by an element of F . The heuristics in Sect. 3.4 is helpless. The problem is that our method cannot notice maximum x = max x and regards maximum x as yielding any value. In this way, our method may fail when some constraints or dependencies are imposed on the results of recursive functions.
5
Experiments
We have developed a prototype implementation of our method. Our prototype is written in Haskell. We implemented the core part shown in Sect. 3 and used the method by Loos and Weispfenning [10] to obtain elimination sets. Other extensions have not yet been completed. Simplifying the formulae significantly improves the efficiency of the parallelization procedure, and we implemented the smart simplification by Dolzmann and Sturm [27]. The environment for our experiments consisted of dual quad-core Xeon X5550 2.66-GHz CPUs, six DDR3-1333 2-GB memories, Linux 2.6.31 (Ubuntu 9.10), ICC 11.1, and GHC 6.10.4. We measured the running times of the parallelizer and generated programs. The times did not include those for I/O. First, we attempted to derive composition evaluators for four examples: counting positive numbers (pcount ), extracting the maximum (maximum), calculating exclusive-or (xor ), and calculating the maximum prefix sum (mps). maximum and mps are the ones we have already discussed, and the others are shown in Fig. 4. Table 1 summarizes the results. For all examples, our prototype system succeeded in deriving a composition evaluator. The former two examples were finished immediately. The latter two took longer because it was necessary to expand the set of indexed functions. The expansion made formulae larger, and quantifier elimination therefore took much longer. We found that the derived composition evaluators for xor and mps are not sufficiently efficient. Our implementation failed in simplifying formulae in conditional expressions. For the case of xor , one reason for the failure is that our implementation did not note the fact that xor only results in either 0 or 1. The use of such information is a future work. Next, we tested the efficiency of the generated parallel programs. We adopted maximum and mps as our test cases. For both cases, we prepared two C++ parallel programs using OpenMP [28]: one is the program obtained from Theorem 1 pcount [ ] =0 pcount (a : x) = if a > 0 then 1 + pcount x else pcount x xor [ ] =0 xor (a : x) = if a = 0 then (if xor x = 0 then 0 else a) else a Fig. 4. Programs of pcount and xor
Automatic Parallelization of Recursive Functions
333
Table 1. Times for Deriving Composition Evaluators (units: seconds) pcount maximum 0.01 0.01
xor 0.09
mps 0.20
Table 2. Running Times of Parallel Programs (units: milliseconds) Sequential maximum (handwritten) maximum (obtained) mps (handwritten) mps (obtained)
249 351
p=2 128 126 211 235
p=4 98 98 113 129
p=6 76 76 86 95
p=8 72 72 79 80
and the generated composition evaluator, and the other is the implementation of a known efficient parallel algorithm [5,14]. In addition, we prepared sequential programs for comparison. The inputs was a list that consisted of uniformly generated 228 64-bit integers from −231 + 1 to 231 . Table 2 shows the running times for parallel programs. Roughly speaking, the programs based on our method are as fast as the handwritten ones. For the case of mps, the generated program was slightly slower because of the inefficiency of the mechanically obtained composition operator. Nevertheless, by virtue of its good scalability, it caught up with the handwritten one when more processors were available. Regrettably, the performances of the programs did not agree with theoretical speedups. We surmised that the program reached the maximum speedups limited by the memory bandwidth. More complicated programs should show better speedups; however, parallelization of complicated programs will take extremely long.
6
Related Works
The heart of our parallelization method is the construction of composition evaluators. It is a folk observation that sets of functions that are closed under composition lead to parallel computations. Abrahamson et al. [15] showed a variant of Theorem 4, which is applicable for expressions that consist of binary operations. Callahan [29] demonstrated that recurrence computations can be parallelized based on the closure property. Gibbons et al. [21] and Matsuzaki et al. [19] discussed developing efficient binary-tree operations on the closure property. Nishimura and Ohori [30] discussed the use of sets of equations, which represent closures, in functional parallel programs. However, fewer attempts have been made at obtaining the closure property without human assistance. Chin et al. [25] briefly mentioned that automatic deduction techniques would be useful to obtain the closure property, but no concrete procedure has been proposed. Xu et al. [2] and Matsuzaki et al. [4] demonstrated that algebraic properties such as
334
A. Morihata and K. Matsuzaki
associativity and distributivity are useful for obtaining the closure property. Yet, they required users to specify algebraic properties. A notable exception is the study by Fisher and Ghuloum [1]. They considered several heuristic rewriting on the bodies of functions so as to obtain the closure property and composition evaluators. We aimed at restructuring their method and removing heuristics as much as possible. Theorem 3 is a benefit of our restructuring. Another benefit is extensibility. Since we have clarified that the key is to obtain elimination sets, our method can deal with other primitive types rather than real values, because there have been studies on obtaining elimination sets on several settings [10,31,32]. The key technique we proposed is Lemma 1. It is well known that for a formula ∃x. ψ, quantifier elimination by virtual substitution can find an instance of x that satisfies ψ if one exists. However, as far as we know, no one has explicitly noted a way of constructing a function that can compute an instance of x from free variables. Jiang [33] recently studied quantifier elimination by substituting functions rather than terms, and proposed a way of obtaining such functions on propositional logic. Yet, he did not provide a way on predicate logic. Most of the current studies on automatic parallelization have considered linearrecursive structures or nested linear recursions [1,2,3,5,6,11], and paid less attention to nonlinear, tree-like recursions. As discussed in the introduction, parallelization by using subtree structures does not generally yield efficient parallel programs. For example, recognizing tree structures as nested linear structures [34] is not helpful in resolving inefficiency for slender trees. We recently clarified a correspondence between divide-and-conquer list operations and tree-contraction-based parallel programs [26], which gave the conjecture that automatic methods of parallelizing linear recursions are also applicable to nonlinear-recursive computations. This conjecture is an underlying view of our study.
7
Conclusion
We proposed an automatic methods of parallelization for recursive functions. We demonstrated the importance of sets of functions that are closed under compositions, and proposed a quantifier-elimination-based method of constructing an operator that enabled us to shrink function closures. We proposed a way of obtaining composition evaluators. Yet, it is still unclear how we can obtain a set that is closed under composition. Another issue is the efficiency of the parallelization procedure. A significant research direction is to construct a practical system of automatic parallelization; however, we expect that quantifier elimination will be too costly to deal with large programs. We are looking for program analyses that will make our method more practical. Acknowledgments. The authors are grateful to the anonymous referees who made valuable comments. The first author is supported by the Grant-in-Aid for JSPS research fellows 20 · 2411.
Automatic Parallelization of Recursive Functions
335
References 1. Fisher, A.L., Ghuloum, A.M.: Parallelizing complex scans and reductions. In: Proceedings of the ACM SIGPLAN 1994 Conference on Programming Language Design and Implementation, pp. 135–146. ACM, New York (1994) 2. Xu, D.N., Khoo, S.C., Hu, Z.: Ptype system: A featherweight parallelizability detector. In: Chin, W.-N. (ed.) APLAS 2004. LNCS, vol. 3302, pp. 197–212. Springer, Heidelberg (2004) 3. Gr¨ oßlinger, A., Griebl, M., Lengauer, C.: Quantifier elimination in automatic loop parallelization. Journal of Symbolic Computation 41(11), 1206–1221 (2006) 4. Matsuzaki, K., Hu, Z., Takeichi, M.: Towards automatic parallelization of tree reductions in dynamic programming. In: SPAA 2006: Proceedings of the 18th Annual ACM Symposium on Parallel Algorithms and Architectures, pp. 39–48. ACM, New York (2006) 5. Morita, K., Morihata, A., Matsuzaki, K., Hu, Z., Takeichi, M.: Automatic inversion generates divide-and-conquer parallel programs. In: Proceedings of the ACM SIGPLAN 2007 Conference on Programming Language Design and Implementation, pp. 146–155. ACM, New York (2007) 6. Tournavitis, G., Wang, Z., Franke, B., O’Boyle, M.F.P.: Towards a holistic approach to auto-parallelization: integrating profile-driven parallelism detection and machine-learning based mapping. In: Proceedings of the 2009 ACM SIGPLAN Conference on Programming Language Design and Implementation, pp. 177–187. ACM, New York (2009) 7. Aho, A.V., Lam, M.S., Sethi, R., Ullman, J.D.: Compilers: Principles, Techniques, and Tools, 2nd edn. Addison-Wesley, Reading (2006) 8. Caviness, B.F., Johnson, J.R. (eds.): Quantifier Elimination and Cylindrical Algebraic Decomposition. Springer, Heidelberg (1998) 9. Peyton Jones, S. (ed.): Haskell 1998 Language and Libraries: The Revised Report. Cambridge University Press, Cambridge (2003) 10. Loos, R., Weispfenning, V.: Applying linear quantifier elimination. Comput. J. 36(5), 450–462 (1993) 11. Chin, W.N., Takano, A., Hu, Z.: Parallelization via context preservation. In: Proceedings of the 1998 International Conference on Computer Languages, pp. 153– 162. IEEE Computer Society, Los Alamitos (1998) 12. Launchbury, J., Sheard, T.: Warm fusion: Deriving build-cata’s from recursive definitions. In: Conference Record of FPCA 1995 SIGPLAN-SIGARCH-WG2.8 Conference on Functional Programming Languages and Computer Architecture, pp. 314–323. ACM, New York (1995) 13. Hu, Z., Iwasaki, H., Takeichi, M.: Deriving structural hylomorphisms from recursive definitions. In: Proceedings of the 1996 ACM SIGPLAN International Conference on Functional Programming, pp. 73–82. ACM, New York (1996) 14. Cole, M.: Parallel programming, list homomorphisms and the maximum segment sum problem. In: Parallel Computing: Trends and Applications, PARCO 1993, pp. 489–492. Elsevier, Amsterdam (1994) 15. Abrahamson, K.R., Dadoun, N., Kirkpatrick, D.G., Przytycka, T.M.: A simple parallel tree contraction algorithm. J. Algorithms 10(2), 287–302 (1989) 16. Reif, J.H. (ed.): Synthesis of Parallel Algorithms. Morgan Kaufmann Publishers, San Francisco (1993) 17. Morihata, A., Matsuzaki, K.: A tree contraction algorithm on non-binary trees. Technical Report METR 2008-27, Department of Mathematical Informatics, University of Tokyo (2008)
336
A. Morihata and K. Matsuzaki
18. Hu, Z., Takeichi, M., Iwasaki, H.: Diffusion: Calculating efficient parallel programs. In: Proceedings of the 1999 ACM SIGPLAN Workshop on Partial Evaluation and Semantics-Based Program Manipulation, pp. 85–94. ACM, New York (1999) 19. Matsuzaki, K., Hu, Z., Takeichi, M.: Parallelization with tree skeletons. In: Kosch, H., B¨ osz¨ orm´enyi, L., Hellwagner, H. (eds.) Euro-Par 2003. LNCS, vol. 2790, pp. 789–798. Springer, Heidelberg (2003) 20. Blelloch, G.E.: Scans as primitive parallel operations. IEEE Trans. Computers 38(11), 1526–1538 (1989) 21. Gibbons, J., Cai, W., Skillicorn, D.B.: Efficient parallel algorithms for tree accumulations. Science of Computer Programming 23(1), 1–18 (1994) 22. Hu, Z., Iwasaki, H., Takeichi, M., Takano, A.: Tupling calculation eliminates multiple data traversals. In: Proceedings of the 2nd ACM SIGPLAN International Conference on Functional Programming, pp. 164–175. ACM, New York (1997) 23. Chin, W.N., Khoo, S.C., Jones, N.: Redundant call elimination via tupling. Fundam. Inform. 69(1-2), 1–37 (2006) 24. Hu, Z., Iwasaki, H., Takechi, M.: Formal derivation of efficient parallel programs by construction of list homomorphisms. ACM Trans. Program. Lang. Syst. 19(3), 444–461 (1997) 25. Chin, W.N., Khoo, S.C., Hu, Z., Takeichi, M.: Deriving parallel codes via invariants. In: Palsberg, J. (ed.) SAS 2000. LNCS, vol. 1824, pp. 75–94. Springer, Heidelberg (2000) 26. Morihata, A., Matsuzaki, K., Hu, Z., Takeichi, M.: The third homomorphism theorem on trees: Downward & upward lead to divide-and-conquer. In: Proceedings of the 36th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, pp. 177–185. ACM, New York (2009) 27. Dolzmann, A., Sturm, T.: Simplification of quantifier-free formulae over ordered fields. J. Symb. Comput. 24(2), 209–231 (1997) 28. Chapman, B., Jost, G., der Pas, R.V.: Using OpenMP: Portable shared memory parallel programming. MIT Press, Cambridge (2007) 29. Callahan, D.: Recognizing and parallelizing bounded recurrences. In: Banerjee, U., Nicolau, A., Gelernter, D., Padua, D.A. (eds.) LCPC 1991. LNCS, vol. 589, pp. 169–185. Springer, Heidelberg (1992) 30. Nishimura, S., Ohori, A.: Parallel functional programming on recursively defined data via data-parallel recursion. J. Funct. Program. 9(4), 427–462 (1999) 31. Weispfenning, V.: Mixed real-integer linear quantifier elimination. In: Proceedings of the 1999 International Symposium on Symbolic and Algebraic Computation, pp. 129–136. ACM, New York (1999) 32. Sturm, T., Weispfenning, V.: Quantifier elimination in term algebras: The case of finite languages. In: Proceedings of the Fifth International Workshop on Computer Algebra in Scientific Computing, pp. 285–300. Technische Universit¨ at M¨ unchen (2002) 33. Jiang, J.H.R.: Quantifier elimination via functional composition. In: Bouajjani, A., Maler, O. (eds.) CAV 2009. LNCS, vol. 5643, pp. 383–397. Springer, Heidelberg (2009) 34. Keller, G., Chakravarty, M.M.T.: Flattening trees. In: Pritchard, D., Reeve, J.S. (eds.) Euro-Par 1998. LNCS, vol. 1470, pp. 709–719. Springer, Heidelberg (1998)
A Skeleton for Distributed Work Pools in Eden Mischa Dieterle1 , Jost Berthold2 , and Rita Loogen1 1
Philipps-Universit¨ at Marburg, Fachbereich Mathematik und Informatik Hans Meerwein Straße, D-35032 Marburg, Germany {dieterle,loogen}@informatik.uni-marburg.de 2 Datalogisk Institut, University of Copenhagen, Denmark [email protected]
Abstract. We present a flexible skeleton for implementing distributed work pools in our parallel functional language Eden. The skeleton manages a pool of tasks (work pool) in a distributed manner using a demanddriven work stealing approach for load balancing. All coordination is done locally within the worker processes. The latter are arranged in a ring topology and exchange additional channels to shortcut communication paths. The skeleton is suited for different types of algorithms, namely simple data parallel ones and standard tree search algorithms like backtracking, and using a global state as needed for branch-and-bound. Runtime experiments reveal a stable runtime behaviour for the different algorithm classes as illustrated by activity profiles (timeline diagrams). Acceptable speedups can be achieved with low effort.
1
Introduction
Parallel evaluation of a large and dynamically evolving pool of tasks (a work pool ) is a classical parallelisation problem [Fos95]. The common approach is a system with one master process managing the work pool, and a set of worker processes which process the tasks. The master distributes tasks to the workers and collects both the results and possibly created tasks produced and sent by the workers. With a big number of workers, such a masterworker setup quickly leads to a bottleneck in the master process. Consequently, more sophisticated work pool schemes have been proposed, with a focus on optimizing the task-scheduling strategy [Fos95, GGKK03, Qui03]. In Fig. 1, we classify such task-scheduling approaches according to their work allocation policy, the organisation of Fig. 1. Classification of Task the work pool and the task distribution strategy. Scheduling Approaches Work can be allocated statically or dynamically. While a static scheme certainly reduces the communication overhead, it may lead to load imbalance in the presence of highly irregular tasks or differences in worker performance. For this reason, a dynamic work allocation strategy is generally favourable. In contrast to the classical centralised master-worker scheme, M. Blume, N. Kobayashi, and G. Vidal (Eds.): FLOPS 2010, LNCS 6009, pp. 337–353, 2010. c Springer-Verlag Berlin Heidelberg 2010
338
M. Dieterle, J. Berthold, and R. Loogen
a completely distributed task pool avoids the single hot spot in the system (but requires more sophisticated work distribution mechanisms and termination detection) [Qui03]. The master process’ role reduces to setting up the system and collecting the results. In such a distributed work pool, a basic distinction can be made between task pushing and stealing approaches [Qui03]. A work pushing strategy means to speculatively forward surplus tasks to random peers when the amount of local tasks exceeds a given threshold. In a demand-driven work stealing strategy, workers send work request messages to peers when idle. New tasks created by workers can be kept locally until work requests from other workers arrive. In the following, we present a sophisticated functional implementation of a work pool skeleton where the work pool is managed in a distributed manner, and a demand-driven work stealing approach is used for load balancing. All coordination takes place between the worker processes, the master only collects the results. As in [PK06], the worker processes are arranged in a ring topology. This provides an easy way to traverse the whole setup for termination detection, and is also an acceptably fast interconnect for propagating global information. Additional channels are used at runtime to directly pass tasks and requests to peer workers without using the ring. Our paper shows that complex coordination structures can efficiently be implemented in a functional setting yielding a flexible base for a low-effort parallelisation of various algorithm classes. The skeleton is especially useful for solving combinatorial optimisation problems with backtracking or branch-and-bound algorithms. Experiments show stable runtime behaviour for several algorithm classes as illustrated by activity profiles. Our traces show well-balanced workloads. Runtime measurements reveal a good scalability with respect to the number of processors. After a short introduction to Eden in Section 2, the skeleton is described in Section 3. First we explain the skeleton interface and how to adapt and apply it to typical algorithm classes. Then the functional implementation of the distributed work pool skeleton is presented. Section 4 shows experimental results for two case studies. Related work is discussed in Section 5. The paper ends with conclusions.
2
Eden in a Nutshell
The distributed work pool skeleton has been implemented in the parallel Haskell dialect Eden [LOMP05], which extends Haskell with an explicit notion of processes (function applications evaluated remotely in parallel). The programmer has direct control over evaluation site, process granularity, data distribution and communication topology, but does not have to manage synchronisation and data exchange between processes. The latter are performed by the parallel runtime system through implicit communication channels, transparent to the programmer. The essential two coordination constructs of Eden are process abstraction and instantiation: process :: (Trans a, Trans b) => (a -> b) -> Process a b ( # ) :: (Trans a, Trans b) => Process a b -> a -> b
A Skeleton for Distributed Work Pools in Eden
339
The function process embeds functions of type (a -> b) into process abstractions of type Process a b where the context (Trans a, Trans b) states that both a and b must be types belonging to the Trans class of transmissible values. Evaluation of an expression (process funct) # arg leads to the creation of a new process for evaluating the application of the function funct to the argument arg. The type class Trans provides overloaded communication functions for lists, which are transmitted as streams, i.e. element by element, and for tuples, which are evaluated componentwise by concurrent threads in the same process. An Eden process can thus contain a variable number of threads during its lifetime. Two additional non-functional features of Eden are essential for performance optimisations: nondeterministic stream merging and explicit communication. Eden’s non-deterministic function merge :: Trans a => [[a]] -> [a] merges a list of streams into a single stream. It simplifies the specification of control and coordination. Communication channels may be created implicitly during process creation - in this case we call them static channels - or explicitly during process evaluation. In the latter case we call them dynamic channels. The following functions provide the interface to create and use dynamic channels: new :: Trans a => (ChanName a -> a -> b) -> b parfill :: Trans a => ChanName a -> a -> b -> b
Evaluating new (\ name val -> e), a process creates a dynamic channel name of type ChanName a in order to receive a value val of type a. After creation, the channel should be passed to another process (just like normal data) inside the expression result e, which will as well use the eventually received value val. Because of Haskell’s lazy evaluation, the execution will not block on val until that value is actually needed. Evaluating (parfill name e1 e2) in the other process has the side-effect that a new thread is forked to concurrently evaluate and send the value e1 via the channel. The overall result of the expression is e2. In the skeleton, dynamic channels are used to create the ring connections between the processes, as well as shortcut connections between ring processes. The latter are used to bypass (previously) idle workers when sending a new work request and when returning tasks to a requesting worker process. Eden’s nondeterministic merge function is heavily used to ensure that incoming data can be processed as soon as it is available.
3
Skeleton Definition
The distributed work pool skeleton uses a set of workers to solve a list of initial tasks received from the caller. Each worker holds a local task pool, and maybe a local state. New tasks may be created and added while solving the initial task set. Load balancing is achieved by a demand-driven exchange of surplus tasks. 3.1
Skeleton Interface and Application
Fig. 2 shows the interface of the skeleton, which allows to customise its functionality by a large set of parameter functions. While the last two parameters
340
M. Dieterle, J. Berthold, and R. Loogen
mwRing :: (Trans t, Trans r, Trans s, NFData r’) => Int -> -- no of processes -- task processing and result post processing ([(t,s)] -> [(Maybe (r’,s),[t])]) -> -- worker function wf ([Maybe (r’,s)] -> s -> [r]) -> -- result transform function resTf ([[r]] -> [r]) -> -- result merge function -- work pool transformation ([t] -> [t] -> s -> [t]) -> -- attach function ttAf ([t] -> s -> ([t],[t])) -> -- split function ttSplitf ([t] -> s -> ([t],Maybe (t,s))) -> -- detach function ttDf -- state comparison function (s -> s -> Bool) -> -- compare function cpSf -- initialisation s -> [t] -> -- initial state initS/tasks initTs [r] -- results Fig. 2. Interface of the General Distributed Work Pool Skeleton
provide the initial state and task list, the first parameter specifies the number of processes to be created. The skeleton creates a ring of worker processes together with a hierarchy of collector processes. The latter is used to speed-up result post-processing. Three functions determine task processing and result post processing, i.e. the proper worker functionality. The work pool is manipulated with the following three parameter functions of the general skeleton: the task pool transformation and attach function ttAf is used to extend the work pool with newly created tasks, the function ttSplitf is used to split the work pool when an external work request arrives, and the function ttDf detaches a single task for local evaluation. Different selection strategies can be used for serving oneself via ttDf and other workers via ttSplitf. Finally, the state comparison function is used for branch-and-bound algorithms to select the optimal solution (state). The following table illustrates how the skeleton functionality is reduced for specific algorithm classes. Algorithm class
task pool poststate task pool size processing structure transformation fixed at start no no queue
parallel (map) transformation and re- fixed at start duction (map-reduce) backtracking (tree search) dynamic branch-and-bound dynamic (optimum search)
yes
no
queue
maybe yes
no yes
queue or stack priority queue
To show how to parallelise a variety of common data processing patterns, we exemplarily discuss the simplest and the most involved instantiation.
A Skeleton for Distributed Work Pools in Eden
341
Data-Parallel Transformation. The most simple and very common application of work pool skeletons is the case of a big set of data items processed by a common transformation (using a functionality like the well-known higher-order function map::(a->b)->[a]->[b]). In our general distributed work pool skeleton, the worker function simplifies to a transformation (t -> r), since it does not create new tasks, nor does it depend on a system state or environment. We embed such a simple worker function into the type needed by our work pool skeleton using the function staticWF and extract the results with the result transformation function idResTf before they are returned to the master: staticWF :: (t->r) -> [(t,())] -> [(Maybe (r,()),[t])] staticWF wf ts = [ (Just (wf t,()),[]) | (t,_) <- ts ] idResTf :: [Maybe (r,())] -> () -> [r] idResTf rss _ = [ r | (Just (r,())) <- l ]
The data set can easily be divided and processed in parallel. However, it may have extremely varying complexity for different input data, and thereby needs dynamic load balancing between the working parallel processes. Every worker in the ring receives a subset of the (usually numerous) tasks. Load balancing becomes relevant in the end phase of the computation, when some workers might already be idle, while others still work on the remaining tasks. Transformation and Reduction. As an immediate extension, a reduction operation (commutative and associative) can be applied to the results, yielding a map-reduce skeleton. The reduction is easily realised by the result transform function resTf: workers can pre-combine all their results after processing and send only one single result back to the caller. The latter then reduces only few pre-results: mwRingMapReduce :: (Trans t, Trans r) => (t->r) -> ([r]->[r]) -> [t] -> [r] mwRingMapReduce wf redF ts = mwRing (noPe-1) (staticWF wf) (\ rs _-> redF(idResTf rs ())) (redF . merge) (\ ts _ _->ts) halfTTSplit topTTD (\_ _->False) () ts
Parameters are the worker function wf, the reduce function redF and the task list ts. Note that the type of the reduce function redF :: [r]->[r] allows any list transformation including identity or sorting. The constant noPe :: Int determines the number of processing elements. The task pool transform and split strategy halfTTSplit passes over half of the tasks within the task pool and the taskpool transform and detach function topTTD selects the first task to be processed next by the local worker function (both not shown). Data transformation/reduction and exhaustive tree search (not discussed here) can also be implemented using a hierarchical master-worker systems (as shown and analysed in [BDLP08]). Our distributed work pool skeleton is tailored to the more interesting case of tree search problems which look for an optimal solution.
342
M. Dieterle, J. Berthold, and R. Loogen
Note, that ordinary master-worker skeletons are in general not able to handle such optimisation problems. Tree Search for Optimal Results (Branch-and-Bound). Branch-and-bound algorithms require an internal state (best result yet), and the comparison function of the skeleton interface to decide which branches of the decision tree should be searched further, and which can be discarded because of already known better results. The best result which has previously been found forms the global system state. This system state is included as a parameter and as a result in the worker, yielding the general type: [(t,s)] -> [(Maybe (r,s),[t])]. Each time a new (better) result has been found, the new state is propagated through the ring to all worker processes. Delays in this state update mechanism may lead to unnecessary evaluations of suboptimal results and thus should be avoided. It is essential that the ring communication remains responsive under all circumstances. Branch-and-bound algorithms can use a best-first search strategy, where the task pool is implemented as a priority queue, or a depth-first search strategy, with a stack implementation of the task pool [CP96]. Our skeleton can implement both strategies using appropriate instantiations of the parameter functions ttDf, ttSplitf, and ttAf. In our experiments, we observed a better performance of the depth first search in most cases. The general interface of the skeleton (shown in Fig. 2) must be used for branch-and-bound algorithms. 3.2
Skeleton Implementation
In the following, we describe the full implementation of the skeleton and explain more details of the skeleton parameters on the way. Global Functionality. In the beginning, all initial tasks are evenly distributed to the worker processes, and the workers work on their local work pools. Newly generated tasks are put into this local pool. When the first worker becomes idle, the demand-driven task exchange starts, following a local round robin strategy [GGKK03]. Fig. 3 shows an exemplary request cycle to illustrate the functionality. Workers with an empty task pool are depicted in white, working processes appear with a coloured (dark) center. The first idle worker sends a work request to its neighbour through the ring. The request of type Req t with data Req t =
...
Other ChanName ([t], ChanName (Req t))
...
contains a dynamic return channel which the receiving process can use to send part of its tasks to the demanding worker (Fig. 3 (a)). The split strategy defined by the parameter function ttSplitf determines which tasks will be sent to the requesting process. Together with the task list of type [t], a dynamic request channel for further work requests (type ChanName (Req t)) is passed to the requesting worker. If the served worker runs out of work again, this request channel is used to send another work request directly to the process which answered the previous request (Fig. 3 (b)). The worker immediately forwards the request to its
A Skeleton for Distributed Work Pools in Eden
(a) Return tasks after first request
(b) Further request
(c) Idle processes forward request
(d) Further return
343
Fig. 3. Snapshots of Local Round Robin Strategy for Task Distribution
successor in the ring. The request is further passed through the ring (Fig. 3 (c)) until it reaches a worker with spare tasks which again will directly send further work (Fig. 3 (d)) and a new request channel via the return channel included in the request. Note our notational distinction between request and return channels, which are technically the same. Request channels are the channels which transport work requests together with a return channel. The return channel is then used by a busy worker process to hand over some of its local tasks to the requesting process. In addition to the tasks, a request channel is supplied, which will be used by the requesting process to send the next work request directly to the process which answered its previous request. Thus, the ring structure is often bypassed via dynamic channels. Nevertheless, the ring is essential when systematically visiting all workers for termination detection.
344
M. Dieterle, J. Berthold, and R. Loogen
Worker Functionality. The behaviour of the worker processes is determined by two functions: the task processing worker function wf and a control function which is the heart of the work pool management. Fig. 4 illustrates the internal flow of information and the code of the worker administration function workerAdminS which essentially maps a stream ringIn of incoming requests with type ReqS t s to a stream ringOut of outgoing requests and a results stream to the parent. The ring output is passed via a dynamic channel ringOutChan while the output for the parent is simply returned as the function result. The request type ReqS t s (see Fig. 4) is the type of information passed through the ring, now extended with state information. It covers external and internal requests for task lists of type t as well as update information for the state (type s). External work requests are identified by the OtherS constructor. These include a Tag that is needed for distributed termination detection. Requests of the local task processing function are identified by the TasksNME constructor which additionally includes a list of newly generated tasks, or by the ME constructor which indicates a pure request for new work. State update information identified by the NewState constructor will be broadcasted using the ring topology. The worker function wf :: [(t,s)] -> [(Maybe (r,s),[t])] processes a list of task/state pairs and outputs a list of pairs. In the first component a result/state pair may be returned. The second component is a list of newly created tasks. The Maybe type allows allows to indicate that no result or no better solution than the already known solution has been found. The output stream of the worker function is split into two streams: a stream of results which is transformed using the parameter function resTf and returned to the parent process, and another stream containing the new states and new tasks that have been produced. If existent, the new state s is forwarded as NewState s, the task list is embedded in a local work request TaskNMe newTs. The stream of local work request and new state information is merged with the ring input stream of external work requests and passed to the function control (see Fig. 4). Initially, the request stream to a control function contains a single ME request. Local Worker Coordination. The central worker function control distinguishes between two different modes handled by the functions distribWork and passWhileReceive. The function distribWork is active as long as the local work pool is nonempty, i.e. work requests can be answered with tasks. It is the initial mode of the control function. control .. requests initTasks initState isFirst = distribWork .. requests initTasks initState Nothing .. distribWork :: Trans t => ... -> -- passed parameter functions [ReqS t s] -> -- requests [t] -> s -> -- work pool/state Maybe(ChanName (ReqS t s)) -> -- return channel if available ... -> -- book keeping parameters ([t],s,[ReqS t s]) -- new work pool/state, -outgoing requests/state infos
A Skeleton for Distributed Work Pools in Eden
data ReqS t s
= | | |
345
ME OtherS (Tag, ChanName([t],Maybe(ChanName(ReqS t s)))) TasksNME [t] NewState s -- carry state inside and between workers
data Tag = Black | White (Int,Int,Int,Int) | None -- Tag White carries four counters of incoming/outgoing messages -- for two subsequent tours through the ring workerAdminS :: (Trans t, Trans r, Trans s, NFData r’) => ... -> -- passed parameters of general interface ChanName [ReqS t s] -> -- outgoing ring channel ringOutChan [ReqS t s] -> -- ring input ringIn Bool -> -- first worker? isFirst [r] -- results to parent workerAdminS wf resTf ttAf ttSplitf ttDf cpSf initTs initS ringOutChan ringIn isFirst = parfill ringOutChan ringOut results where -- central control: manage local work pool and requests (ts’, ringOut) = control ttAf ttSplitf ttDf cpSf initTs initS isFirst reqL reqL = ME : mergeS rnf [ringIn,localReqs] -- task processing and final result transformation (ress, localReqs) = split (wf ts’) results = resTf ress finalState NewState finalState = last ringOut Fig. 4. Implementation Scheme of Ring-Connected Worker Processes
346
M. Dieterle, J. Berthold, and R. Loogen
The function passWhileReceive is called when the local task pool is empty and a Me request occurs, i.e. the worker itself runs out of work. The first time this situation occurs, an OtherS work request is sent into the ring, tagged Black and containing a newly created return channel. Later requests will be sent on the previously received request channel. Incoming work requests are passed to the next ring process and incoming state information is handled as follows: New states are compared with the current state, and either accepted and passed on to the next ring process or discarded. When new tasks are received via the return channel, control is passed back to the distribWork function. Termination Detection. We have implemented a combination of two standard algorithms: Mattern’s ”four counter method” [Mat87] and Dijkstra’s token algorithm [ED83]. The four counter method counts the number of outgoing and incoming messages per process with a control message circulated twice through the whole ring. We use our work requests for that purpose. Termination is initiated when a work request completes the second ring tour with the same balanced number of sent and received messages as in the first tour. While counting incoming and outgoing messages helps to detect ongoing communications, the tag colour is used to check whether there are still busy workers that may produce additional work. The main difficulties in the implementation of the distributed work pool skeleton have been to add additional evaluation demand, to ensure liveness of the whole system, and to appropriately merge input data received via many different channels. An example for additional demand is the mergeS variant used in the function workerAdminS (see Fig. 4). This variant uses a function rnf (reduce to normal form strategy) to force the evaluation of stream elements before they are written into the result stream. An additional optimisation of merging will be discussed in the following section. We will not go further into the details of the skeleton implementation. The complete code can be found in [Die07].
4
Experimental Results
In this section, we present experimental results for typical case studies. We visualise the runtime behaviour using activity profiles and show runtime measurements and speedup figures for the most general case of a branch-and-bound problem. NAS EP Benchmark. We have compared the skeleton with a simple masterworker-skeleton [LOMP05] using an exemplary transformation problem, the NAS parallel benchmark EP (Embarrassingly Parallel) [BBB+ 94]. In this benchmark, two-dimensional statistics are accumulated from a large number of Gaussian pseudo-random numbers. Very little communication is required. Fig. 5 visualises the process activities over time for both computations with 3 million numbers on an inhomogeneous local network of 9 Linux workstations. Active phases of the processes are shown in cyan (middle gray), blocked phases in red (dark), and
A Skeleton for Distributed Work Pools in Eden
(a) Distributed Work Pool Skeleton
347
(b) Simple Master-Worker-Skeleton
Fig. 5. Activity Profiles of NAS EP Benchmark, Input Size 3M, 9 PEs
runnable phases in yellow (light). Communication is overlayed, i.e. messages are shown as black arrows from the sending to the receiving process. In data-parallel transformation problems, the entire task pool is known in advance. With the distributed work pool skeleton, workers are assigned a fixed task subset initially. The activity profile shows that workers are active most of the time. Communication takes place only in the beginning and at the end of the computations. The computation of the simple master-worker-skeleton is very communication-intensive, because the master continuously distributes tasks to the workers. The more sophisticated distributed work pool skeleton has almost no runtime overhead. Load is well balanced in both cases. Graph Partitioning Problem. The graph partitioning problem is a typical branch-and-bound problem. A graph has to be partitioned into two sub-graphs with an (almost) equal number of nodes where the weight sum of the edges connecting the two subgraphs — the truncation cost — is minimal. The partitioning is incrementally built up by traversing the list of graph nodes and defining subproblems where each node is assigned to one of the two possible sub-graphs. The actual truncation costs of partial solutions are used to compute a lower bound on the truncation costs of corresponding complete solutions. The following runtime experiments were carried out on a Beowulf cluster at Heriot-Watt-University, Edinburgh which consists of 32 Intel P4-SMP-processors running at 3 GHz with 512 MB RAM and a Fast Ethernet interconnection. Fig. 6 shows the activity profile when evaluating the graph partitioning problem for a graph with 30 nodes on 31 PEs. On the left hand side, the whole trace is shown. On the right hand side, we see a zoom of the end phase of the same trace. Again, most communication takes place in the beginning to establish the topology and during the final phase, when idle workers send work requests to other workers. The request-reply cycles are clearly visible in the zoomed view. The whole system is well-balanced and all workers are equally loaded. Note that a comparison with the simple master-worker skeleton is not possible, because the latter cannot be used for the implementation of branch-and-bound algorithms.
348
M. Dieterle, J. Berthold, and R. Loogen
zoomed area Fig. 6. Graph Partitioning (30 Nodes), Entire Run (Left) and Zoomed End Phase (Right), 31 PEs, Runtime: 2,68 sec
Two parameters have major impact on the runtime of the parallel program: The cutoff depth (explained below) and the merge function. We have examined this impact with experiments focussing on a depth-first branch-and-bound implementation of the graph partitioning problem with graphs consisting of 32 nodes. The cutoff Parameter. Tasks evaluating nodes near the root of the search tree usually have a higher complexity than tasks whose nodes are deeper in the tree, i.e. tasks are irregular in their potential to generate new subtasks. Load balancing strategies should take this fact into account, and preferably give away tasks which have a higher “potential”. Usually, the tree depth in the decision tree is known, or can be estimated cheaply. Thus, a load balancing strategy may retain tasks when their distance from the root is bigger than a cutoff. These small tasks will be solved locally: Passing them to other workers would be more expensive than local evaluation. Although this behaviour is related to task distribution, it is more easily encoded in the worker function. The runtime of the work pool can benefit in two ways from a properly adjusted cutoff parameter. Tasks are only sent to other workers if they have the potential to produce enough work, depending on the remaining subtree depth, thereby reducing communication overhead. In addition, evaluation of tree levels beyond the cutoff depth is done by a simple recursive function, bypassing the control function. The traces in Fig. 6 have been obtained with the (experimentally determined) best cutoff depth. We have tested the impact of the cutoff depth with two different program versions. One is based on the skeleton described in the previous section, i.e. using a ring of worker processes and implementing a local round robin strategy for task stealing. The second one implements an all-to-all communication topology among the workers and a random strategy for task stealing, i.e. the processes to be asked for tasks are randomly chosen. We made runtime comparisons using
A Skeleton for Distributed Work Pools in Eden
349
these skeleton versions on up to 31 PEs with the cutoff depth ranging from 5 (early cut) to 29 (late cut). The results presented in Fig. 7(a) show an optimal cutoff with 12, 13, 14, or 15 for both program versions. The two versions perform similar in the area of the optimal cutoff values. With higher cutoff values the local round robin version is faster than the random version. Improving merge. At first, our example programs showed a steadily increasing number of active threads per process. The thread activities of a single process (number 21) in a program run with cutoff value 26 and the original “old” merge are illustrated in Fig. 7(b). Each horizontal line represents the life time of a thread. The picture shows many long-living threads within a single process which are blocked most of the time. This is due to the fact that Eden’s merge which is implemented using the Concurrent Haskell nmergeIO: [[a]] -> IO [a] forks one additional thread per input list for concurrently passing through this list; the untouched list elements are written into a single output list. A thread terminates as soon as it reaches the end of its list. Even the final thread will transmit its input list element-by-element into the output list. This approach is acceptable for stream merging. However, each time a finite list is merged with a stream, the number of threads increases by one. In our skeleton, the values sent on dynamic channels (e.g. tasks or work requests) are always merged with the request stream scanned by the worker’s control function. Thus, the original merge implementation causes the number of running threads to increase with the number of requests and replies. We have modified the Concurrent Haskell nmergeIO, so that the merger threads can detect this situation and terminate earlier. This implementation dramatically reduces the life time and number of merge threads in the above scenario, as we can see in Fig. 7(c). The overall runtime is reduced because messages are no longer passed through several intermediate threads. Speedup. If the search tree is immediately cut with cutoff value 0, the task pool contains only the initial node, and the whole branch-and-bound problem is evaluated sequentially. This eliminates most of the overhead of the work pool
Random Stealing vs Local Round Robin
(a) Runtimes for varying cutoff par.
(b) Threads view: old merge
Fig. 7. GPP 32 Test Runs on 31 PEs
(c) new
merge
350
M. Dieterle, J. Berthold, and R. Loogen
skeleton compared to a sequential implementation, so we used this cutoff 0 version of the graph partitioning problem to approximate the behaviour and runtime of the sequential algorithm. Fig. 8 shows the almost linear speedup of a series of program runs with the new merge version, the cutoff value 13 and the number of processors ranging from 1 up to 29 in comparison with the pseudo-sequential version. The runtime on 1 machine with cutoff 13 and the pseudo-sequential version (cutoff value 0) are very close, they differ less then 1%. Efficiency slightly drops when the number of processors is increased, but it stays above 88%. Summary. The parallel runtime behaviour of the skeleton has been visualised for the NAS EP benchmark which implements a data-parallel transformation problem and for the graph partitioning problem which is a typical branch-andbound algorithm. Both profiles show that communication concentrates on the start-up and the final computation phase when the worker processes run out of work. Load is well-balanced in both cases. For the NAS EP benchmark, the activity profile has been compared with a profile of a simple master-worker skeleton which shows a higher communication overhead due to the continuous task distribution by the master process. For the graph partitioning problem, it has been shown that the cutoff parameter has a great impact on the runtime. Using a random instead of a local round robin strategy for task stealing makes only a difference for sub-optimal cutoff values, where the random strategy leads to higher runtimes. By improving the implementation of the merge function in the case that a stream is merged with a finite list a substantial reduction of the number and the lifetime of threads and, consequently, of the overall runtime could be achieved. Finally, this led to an almost linear speedup when using an optimal cutoff value and the new merge function.
Fig. 8. Speedup for the GPP 32 Problem
A Skeleton for Distributed Work Pools in Eden
5
351
Related Work
The master-worker paradigm has been extensively investigated and many implementations exist. We focus here on other pattern or skeleton approaches and especially on distributed work pool implementations. In the context of the grid computing environment Condor, the MW (Master-Worker) library has been developed [GL06]. MW is tailored for branch-and-bound applications. It implements the basic master-worker system with a central master managing the task pool and a set of worker processes. A special feature that is not supported by our skeleton is the addition and removal of workers during runtime which is especially important in a grid environment. This feature cannot easily be implemented in Eden, because dynamic channels cannot simply be re-directed. Most skeleton libraries like [Kuc, Ben, Dan] provide master-worker skeletons. The MPI-based skeleton library Muesli [Kuc], e.g. offers a farm, a search and a branch-and-bound skeleton. The farm implements a master-worker system with a dynamic task distribution. The search and branch-and-bound skeletons are especially tailored for the corresponding problem classes. Our distributed work pool skeleton is more general, because it supports all three problem classes. Moreover, it allows a hierarchical result collection. In [PK06], Kuchen and Poldner present a distributed branch-and-bound skeleton based on a distributed work pool. The workers are also arranged in a ring. Two task distribution policies are supported: a supply-driven scheme where workers send their second-best problem to their ring neighbour from time to time, and a demand-driven scheme where work is only distributed if an idle worker requests it. Hippold and R¨ unger describe task pool teams [HR06], a programming environment for SMP clusters that is explicitly tailored towards irregular problems with strong inter-task dependences. The scheme comprises a set of task pools, each running on its own SMP node, and interacting via explicit message passing. Dynamic task creation by workers, task migration, and distributed task pools with a task stealing mechanism are possible. Locality can be exploited to hold global data on the SMP nodes, while communication between nodes is used for task migration, remote data access, and global synchronisation. Dorta et al. present a master-worker skeleton implementation in C plus MPI which is tailored for branch-and-bound problems [DLR06]. A master process is used to coordinate the interaction between the worker processes and to keep the information about the currently best solution. A task pushing approach is implemented where the master process determines to which workers a worker should send its newly created tasks. No cutoff parameter is used to improve the task granularity. Distributing the whole search tree leads to a high imbalance of task sizes. The profiles show that the workers spend most of the time waiting for new tasks. Moreover, a lack of scalability was observed. In a previous paper [BDLP08], we have investigated declarative techniques for hierarchically nesting master-worker instances. The workers are divided into several groups managed by a hierarchy of sub-masters. The work pool is divided into several sub-pools within the sub-masters. Now we have followed a more radical approach and completely distributed the work pool within the worker processes.
352
6
M. Dieterle, J. Berthold, and R. Loogen
Conclusions
A distributed work pool skeleton has been implemented in the parallel functional language Eden. Eden’s specific features like lazy stream processing, dynamic reply channels, nondeterministic merge are very supportive for the efficient implementation of the complex coordination structure of the skeleton. The skeleton is very general, highly parameterised, and thus applicable to a range of problem classes. Experiments show a stable runtime behaviour, well-balanced work loads and worker activities. Communication overhead mainly occurs in the end phase of executions. Acknowledgements. We thank the anonymous referees for their helpful comments on a previous version of this paper.
References [BBB+ 94]
[BDLP08]
[Ben] [CP96]
[Dan] [Die07]
[DLR06]
[ED83]
[Fos95] [GGKK03] [GL06]
[HR06]
Bailey, D., Baszcz, E., Barton, J., Browning, D., Carter, R., Dagum, I., et al.: The NAS Parallel Benchmarks. Technical Report RNR-94-007, NASA (1994) Berthold, J., Dieterle, M., Loogen, R., Priebe, S.: Hierarchical MasterWorker Skeletons. In: Hudak, P., Warren, D.S. (eds.) PADL 2008. LNCS, vol. 4902, pp. 248–264. Springer, Heidelberg (2008) Benoit, A.: ESkel — The Edinburgh Skeleton Library. Univ. of Edinburgh (2007), http://homepages.inf.ed.ac.uk/abenoit1/eSkel/ Clausen, J., Perregaard, M.: On the best search strategy in parallel branch-and-bound - best-first-search vs. lazy depth-first-search. Technical Report 16, University of Copenhagen (1996) Danelutto, M.: The parallel programming library Muskel. Universita di Pisa (2007), http://www.di.unipi.it/∼marcod/Muskel/Home.html Dieterle, M.: Parallel functional implementation of master worker skeletons. Diploma Thesis, Philipps-Universit¨ at Marburg (October 2007) (in German) Dorta, I., L´eon, C., Rodr´ıguez, C.: Performance Analysis of Branch-andBound Skeletons. In: 14th Euromicro Conf. on Parallel, Distributed, and Network-Based Processing (PDP 2006). IEEE, Los Alamitos (2006) van Gasteren, A.J.M., Dijkstra, E.W., Feijen, W.H.J.: Derivation of a termination detection algorithm for distributed computations. Inform. Process. Lett. 16(5), 217–219 (1983) Foster, I.: Designing and Building Parallel Programs. Addison-Wesley, Reading (1995) Grama, A., Gupta, A., Karypis, G., Kumar, V.: Introduction to Parallel Computing. Pearson Education, London (2003) Glankwamdee, W., Linderoth, J.T.: MW: A Software Framework for Combinatorial Optimization on Computational Grids. In: Talbi, E. (ed.) Parallel Combinatorial Optimization, pp. 239–262. Wiley, Chichester (2006) Hippold, J., R¨ unger, G.: Task Pool Teams: A Hybrid Programming Environment for Irregular Algorithms on SMP Clusters. Concurrency and Computation: Practice and Experience 18, 1575–1594 (2006)
A Skeleton for Distributed Work Pools in Eden [Kuc] [LOMP05]
[Mat87] [PK06] [Qui03]
353
Kuchen, H.: The M¨ unster Skeleton Library Muesli. Univ. M¨ unster (2007), http://www.wi.uni-muenster.de/PI/forschung/Skeletons/index.php Loogen, R., Ortega-Mall´en, Y., Pe˜ na-Mar´ı, R.: Parallel Functional Programming in Eden. Journal of Functional Programming 15(3), 431–475 (2005) Mattern, F.: Algorithms for distributed termination detection. Distributed Computing 2, 161–175 (1987) Poldner, M., Kuchen, H.: Algorithmic skeletons for branch & bound. In: ICSOFT (1), pp. 291–300. INSTICC Press (2006) Quinn, M.: Parallel Programming in C with MPI and OpenMP. McGraw Hill, New York (2003)
Author Index
Abel, Andreas, 224 Altenkirch, Thorsten, 40 ´ Alvez, Javier, 118 Avanzini, Martin, 257 Banbara, Mutsunori, 19 Barthe, Gilles, 72 Berardi, Stefano, 207 Berthold, Jost, 337 Buiras, Pablo, 72 Caballero, Rafael, 191 Danielsson, Nils Anders, 40 Danvy, Olivier, 240 Dieterle, Mischa, 337 Estruch, Vicent, 150 Ferri, C´esar, 150 Garc´ıa-Ruiz, Yolanda, 191 Haftmann, Florian, 103 Hern´ andez-Orallo, Jos´e, 150 Howe, Jacob M., 165 Ketema, Jeroen, 272 King, Andy, 165 Kiselyov, Oleg, 304 Kunz, C´esar, 72 L¨ oh, Andres, 40 Loogen, Rita, 337 L´ opez-Fraguas, Francisco J., 118
Matsuzaki, Kiminori, 321 Millikin, Kevin, 240 Morihata, Akimasa, 321 Moser, Georg, 257 Munk, Johan, 240 Nipkow, Tobias, 103 Nishida, Naoki, 288 Orchard, Dominic, 56 Oury, Nicolas, 40 Pientka, Brigitte, 1 Ram´ırez-Quintana, M. Jos´e, 150 R´emy, Didier, 24 S´ aenz-P´erez, Fernando, 191 Sagonas, Konstantinos, 13 Sakai, Masahiko, 288 Saurin, Alexis, 134 Schrijvers, Tom, 56 Seidel, Daniel, 175 Simonsen, Jakob Grue, 272 Sulzmann, Martin, 87 Tamura, Naoyuki, 19 Tanjo, Tomoya, 19 Tatsuta, Makoto, 207 Thiemann, Peter, 87 Voigtl¨ ander, Janis, 175 Yakobowski, Boris, 24 Zerny, Ian, 240