The delimited checkpoint interface serves as the technical foundation for this ...... as well as the magic wand operator, by treating them as contract functions: ...... Westley Weimer, ThanhVu Nguyen, Claire Le Goues, and Stephanie Forrest.
Safe and Effective Contracts
A dissertation presented by Avraham Ever Shinnar to The School of Engineering and Applied Science in partial fulfillment of the requirements for the degree of Doctor of Philosophy in the subject of Computer Science Harvard University Cambridge, Massachusetts May 2011
c �2011  Avraham Ever Shinnar All rights reserved.
Dissertation advisor
Author
Greg Morrisett
Avraham Ever Shinnar
Safe and Effective Contracts
Abstract This dissertation introduces a framework enabling the dynamic verification of expressive specifications. Inspired by formal verification methods, this framework supports assertion, framing, and separation contracts. Assertion contracts specify what code should do, whereas framing contracts specify what code must not do. Separation contracts, inspired by separation logic, combine an explicit assertion contract with an implicit framing contract. In addition to supporting these expressive contracts, this framework also enables assertions to call existing code with side effects while ensuring that successful assertions do not affect the rest of the program. Contracts are guaranteed safe while remaining easy to write. This dissertation introduces a single interface, the delimited checkpoint, that supports all of the contracts listed above. Similar to previous work on equipping a programming language with first class stores, checkpoints represent a state in time. Computations can be run with memory restored to a checkpoint state. Checkpoints augment existing work with a novel family of difference operations that compare two checkpoints, revealing how the intervening computation interacted with memory. Additionally, checkpoints are delimited: they can only be used within a limited scope. This interface suffices to build assertion contracts that support time travel, framing contracts, and separation contracts. Additionally, it supports a novel suppression contract,
iii
Abstract
iv
allowing assertions to safely run existing code by suppressing proscribed effects. A formal operational semantics precisely defines the checkpoint interface, enabling formal reasoning about derived contracts. In particular, the derived contracts provably satisfy key properties. For example, the defining property of separation logic, the frame rule, provably holds for the derived separation contract. Additionally, this dissertation notes the utility of restricting checkpoints to a given (dynamic) scope, giving rise to delimited checkpoints. Contracts do not need the full power of checkpoints, using checkpoints in only a delimited manner. This restriction is useful as it enables delimited checkpoints to reuse the infrastructure needed to support Software Transactional Memory systems. This dissertation presents a prototype implementation of delimited checkpoints based on this observation. Extending the GHC compiler for Haskell, the implementation supports all of the contracts previously discussed.
Contents Title Page . . . . . Abstract . . . . . . Table of Contents . List of Figures . . . List of Tables . . . Acknowledgments . Dedication . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. i . iii . viii . ix . xi . xii . xiii
1
Introduction 1 1.1 Contracts: Usability vs Safety . . . . . . . . . . . . . . . . . . . . . . . . 8 1.2 Delimited Checkpoints . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 1.3 Contributions and Structure . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2
Background 2.1 Contracts . . . . . . . . . . . . 2.1.1 Enforcement . . . . . . 2.1.2 Erasability . . . . . . . 2.2 Haskell . . . . . . . . . . . . . 2.2.1 Syntax . . . . . . . . . 2.2.2 Types . . . . . . . . . . 2.2.3 Monads . . . . . . . . . 2.3 Software Transactional Memory 2.3.1 STM Haskell . . . . . .
3
Delimited Checkpoints 3.1 Delimited Difference . . . . . . . . . . . . . . . 3.2 Discussion . . . . . . . . . . . . . . . . . . . . . 3.2.1 Delimited Checkpoints . . . . . . . . . . 3.2.2 The Difference of Unordered Checkpoints 3.2.3 Untyped Delta Sets . . . . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
v
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
16 16 18 20 21 22 23 27 29 31
. . . . . . . . . . . . . . . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
38 41 43 44 45 48
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
Contents
vi
4
Contracts 4.1 Assertions . . . . . . . . . . . . . . . . . . . . . . 4.1.1 Implementing with Delimited Checkpoints 4.1.2 Detecting Effects: the noEffect Contract 4.1.3 Benign Writes . . . . . . . . . . . . . . . 4.2 Framing . . . . . . . . . . . . . . . . . . . . . . . 4.2.1 Implementing with Delimited Checkpoints 4.3 Suppression . . . . . . . . . . . . . . . . . . . . . 4.3.1 Implementing with Delimited Checkpoints 4.4 IntraContract Local State: Ghost State . . . . . . . 4.4.1 Implementing with Delimited Checkpoints 4.4.2 Enforcing Noninterference . . . . . . . . 4.5 The Java Modeling Language . . . . . . . . . . . . 4.6 Separation Contracts . . . . . . . . . . . . . . . . 4.6.1 Implementing with Delimited Checkpoints
. . . . . . . . . . . . . .
49 51 54 57 59 62 63 66 66 70 71 72 74 77 82
5
Formal Semantics 5.1 Semantics of STM Haskell . . . . . . . . . . . . . . . 5.2 Semantics of DC Haskell . . . . . . . . . . . . . . . . 5.2.1 DC Haskell Properties . . . . . . . . . . . . . 5.2.2 Delimited Difference: DCd Haskell . . . . . . 5.3 Conservative Extension . . . . . . . . . . . . . . . . . 5.4 Taking Out the Garbage . . . . . . . . . . . . . . . . . 5.4.1 The Freshness of References and Checkpoints . 5.4.2 Garbage in the Input Structures . . . . . . . . 5.5 Erasure . . . . . . . . . . . . . . . . . . . . . . . . . 5.5.1 Definition . . . . . . . . . . . . . . . . . . . . 5.5.2 Erasing Contracts . . . . . . . . . . . . . . . . 5.6 Frame Rule . . . . . . . . . . . . . . . . . . . . . . . 5.7 Undelimited Checkpoints . . . . . . . . . . . . . . . .
. . . . . . . . . . . . .
88 89 96 110 113 115 116 117 119 125 128 130 132 135
6
Implementation 6.1 Implementation in GHC . . . . . . . . . 6.1.1 Adding Delimited Checkpoints . 6.1.2 Difference Functions . . . . . . . 6.2 Evaluation . . . . . . . . . . . . . . . . . 6.2.1 Correctness . . . . . . . . . . . . 6.2.2 Benchmarking Setup . . . . . . . 6.2.3 Delimited Checkpoint Operations 6.2.4 Contracts . . . . . . . . . . . . . 6.2.5 Overhead of STM Modifications . 6.2.6 What It All Means . . . . . . . .
. . . . . . . . . .
138 140 141 143 146 147 149 152 154 158 159
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Contents 6.3 6.4
vii Working with Other STM Implementations . . . . . 6.3.1 In Place Updates (Undo Logging) . . . . . . 6.3.2 MultiVersion Concurrency Control (MVCC) Implementing Undelimited Checkpoints . . . . . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
159 160 161 162
7
Related Work 7.1 First Class Stores . . . . . . . . . . . . . . . . . 7.1.1 Delimited Control and Delimited Binding 7.2 Transactional Memory . . . . . . . . . . . . . . 7.2.1 Checkpoints and Continuations . . . . . 7.2.2 Abort on Exception . . . . . . . . . . . . 7.2.3 Transactions and Concurrency . . . . . . 7.3 Contracts for Functional Languages . . . . . . . 7.3.1 Static Contract Checking for Haskell . . 7.4 Runtime Verification . . . . . . . . . . . . . . . 7.4.1 JML Runtime Assertion Checker . . . . . 7.4.2 Implementing old with Snapshots . . . . 7.4.3 Runtime Checking for Separation Logic . 7.4.4 Transactional Consistency . . . . . . . . 7.4.5 GC Assertions . . . . . . . . . . . . . . 7.5 Contract Properties . . . . . . . . . . . . . . . . 7.5.1 Erasure . . . . . . . . . . . . . . . . . . 7.5.2 Blame . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . .
165 166 167 168 168 168 169 171 172 173 173 174 175 176 177 178 178 179
8
Conclusions and Future Work 8.1 Motivation for Expressive Contracts . . . . . . . . . . 8.2 Adoption of Expressive Contracts . . . . . . . . . . . 8.3 Future Work . . . . . . . . . . . . . . . . . . . . . . . 8.3.1 Integration with Static Verification . . . . . . . 8.3.2 Delimited Checkpoints and Delimited Control 8.3.3 Contracts . . . . . . . . . . . . . . . . . . . . 8.3.4 Implementation . . . . . . . . . . . . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
181 182 184 185 186 186 187 191
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
194 194 195 201 204 214 215 221 221
A Additional Proofs A.1 Semantics of DC Haskell . . . . . . . . . . A.2 DC Haskell Properties . . . . . . . . . . . A.2.1 Delimited Difference: DCd Haskell A.3 Conservative Extension . . . . . . . . . . . A.4 Taking Out the Garbage . . . . . . . . . . . A.4.1 Garbage in the Input Structures . . A.5 Erasure . . . . . . . . . . . . . . . . . . . A.5.1 Definition . . . . . . . . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . .
. . . . . . . . . . . . . . . . .
. . . . . . . .
. . . . . . . .
Contents
viii
A.5.2 Erasing Contracts . . . . . . . . . . . . . . . . . . . . . . . . . . . 223 A.6 Frame Rule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226 Bibliography
232
List of Figures 1.1 1.2 1.3 1.4
Possible specification of deleteMin . . . . . . . . . . . . . . . . . . . The well_formed_tree and getLocations helpers for deleteMin Accidentally changing errno inside of an assertion . . . . . . . . . . . Masking a buffer overflow with an assertion . . . . . . . . . . . . . . .
. . . .
. 4 . 6 . 9 . 10
2.1 2.2 2.3 2.4 2.5 2.6
Possible Eiffel Specification of a Bank Transfer Function . . . Possible JML Specification of a Bank Transfer Function . . . Implementing a bank transfer function with an atomic block . STM monad interface from STM Haskell (Harris et al. 2005) . Implementing a bank transfer function in STM Haskell . . . . The transfer transaction preserves the sum of the accounts
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
17 18 31 33 35 36
3.1 3.2 3.3 3.4
Haskell Interface to Delimited Checkpoints . . . . . . . . . . . . . Derived Delimited Difference Operations . . . . . . . . . . . . . . Example that creates unordered checkpoints . . . . . . . . . . . . . View of memory with unordered checkpoints (created in Figure 3.3)
. . . .
. . . .
. . . .
. . . .
39 42 46 47
4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9 4.10 4.11 4.12 4.13 4.14
An interface for monadic assertions in Haskell . . . . . . . . . . . . . . Building a hoare contract . . . . . . . . . . . . . . . . . . . . . . . . Types for Delimited Checkpoint Implementation of Assertion Interface . Building assertion contracts with delimited checkpoints . . . . . . . . . The noEffect helper function for detecting effects in assertions . . . . The noModify contract . . . . . . . . . . . . . . . . . . . . . . . . . . The writesOnly contract for limiting effects . . . . . . . . . . . . . . The modifiesOnly contract for limiting effects . . . . . . . . . . . . Suppressing writes with delimited checkpoints . . . . . . . . . . . . . . Suppressing all writes with delimited checkpoints . . . . . . . . . . . . The Contract Monad: Restricting Access to Delimited Checkpoints . . . The Contract Monad: Supporting Ghost State . . . . . . . . . . . . . . Separation Logic: The Frame Rule . . . . . . . . . . . . . . . . . . . . Implementing Separation Logic: pointsto . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
54 55 55 56 58 60 64 65 67 69 73 74 80 83
ix
. . . . . .
. . . . . .
List of Figures
x
4.15 Implementing Separation Logic: deltaFootprintCheckPoint . . . . . . . . . 83 4.16 Implementing Separation Logic: Separating Conjunction ( P * Q) . . . . . 84 4.17 Implementing Separation Logic Contract with Delimited Checkpoints . . . 85 5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 5.9 5.10 5.11 5.12 5.13
STM Haskell: The syntax of values and terms . . . . . . . . . . . . . . . STM Haskell: The program state and evaluation contexts . . . . . . . . . Operational semantics of STM Haskell: I/O and administrative transitions Operational semantics of STM Haskell: STM transitions . . . . . . . . . DC Haskell: The syntax of values and terms . . . . . . . . . . . . . . . . DC Haskell: The program state and evaluation contexts . . . . . . . . . . Operational semantics of DC Haskell: I/O and administrative transitions . Operational semantics of DC Haskell: STM transitions . . . . . . . . . . Operational semantics of DC Haskell checkpoints: Checkpoint transitions Operational semantics of DC Haskell checkpoints: helper definitions . . . Operational semantics of DC Haskell checkpoints: difference function . Looking at the input trace with deltaRCP . . . . . . . . . . . . . . . . . Operational semantics of undelimited (first class) checkpoints . . . . . .
. . . . . . . . . . . . .
90 93 94 95 98 99 105 106 107 108 109 123 137
6.1 6.2 6.3
The findLCA function . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145 The countAncestors helper function . . . . . . . . . . . . . . . . . . . 146 A test case for the suppressWritesOnly suppression contract . . . . . . 148
7.1
Implementing the tryall construct introduced by Shinnar et al. (2004) . . 169
List of Tables 6.1 6.2 6.3
Overhead of Transaction and Delimited Checkpoint Operations . . . . . . . 152 assertM Overhead . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 Breakdown of Contract Overheads . . . . . . . . . . . . . . . . . . . . . . 156
xi
Acknowledgments This dissertation represents the culmination of many years of growth. There are many people who have helped me on this journey, both academically and personally. First and foremost, I must thank my parents, who have always prioritized education. In college, Professors Aho and Edwards took me under their wing, and introduced me to both programming languages and research. During my internships at Microsoft Research, Dave Tarditi helped me gain a better understanding for the research process. At Harvard, I have been privileged to interact with many extraordinary people. Foremost is my adviser, Greg Morrisett, who taught me both programming language theory and how to perform research. Margo Seltzer has an unerring ability to find and help address both methodological and grammatical flaws. The generality and simplicity of the checkpoint interface is a direct result of frequent conversations with Stephen Chong. Many others at Harvard also contributed to my experience. Norman Ramsey, my first year field adviser, taught me functional programming. My officemates provided many interesting discussions and collaborations. Finally, I want to thank Uri Braun, a staunch friend and colleague, for our many collaborations and for his assistance with this thesis. I thank Marco Pistoia and Anindya Banerjee for our energizing collaboration at IBM. I would also like to call out some people who have helped out with the nonacademic aspects of my years in graduate school. My family, have supported me in countless ways, particularly my parents and my sister Ora Sheinson. Finally, I turn to the people who enrich my life beyond measure: my wife and children. My wife supported and encouraged me throughout the ups and downs of graduate school. Despite being in medical school, she found time to bear and take care of our two children. My children always keep me on my toes and provide therapeutic hugs and giggles. xii
Dedicated to my amazing wife who supported me throughout graduate school and enabled me to finish this dissertation.
xiii
Chapter 1 Introduction This dissertation introduces a framework for dynamically verifying expressive specifications inspired by formal verification techniques. In an ideal world, systems would automatically statically verify expressive specifications. In practice, it is impossible to obtain all three of these goals: static assurance, automation, and expressive specifications. As a result, systems need to decide which goals to prioritize. Research on formal methods emphasizes static verification. As a result, this body of research reveals a tension between automation and expressiveness. At one end of the spectrum are weak specifications that are automatically decidable. These include type systems for mainstream programming languages. At the other end are systems that support extremely expressive specifications, but require interactive theorem proving to verify the specifications. An example is the Ynot library for verifying imperative programs (Chlipala et al. 2009). In between these two extremes are languages such as the Java Modeling Language (Poll et al. 2006) and Spec# (Barnett et al. 2008). Tools for these language use SMT (Satisfiability Modulo Theories) solvers to attempt automatic verification of first or1
Chapter 1: Introduction
2
der logic based specifications. These languages attempt to find a useful balance between expressiveness and automation. Unlike formal methods, contract systems enforce specifications dynamically, foregoing verification for validation testing. Dynamic validation ensure that a given run of the program satisfies its specifications. By giving up on static assurance, contract systems should ease the tension between our other two goals: automated verification and expressive specifications. Indeed, dynamic techniques are usually fully automated. However, existing contract systems do not allow specifications as expressive as those allowed by static methodologies. This dissertation takes steps to ameliorate some of this deficiency, enabling the dynamic validation of more expressive contracts. The most common form of dynamic validation, found in every major programming language (Chalin 2005) is the assertion. As typified by the C programming language, assertions are commonly supported by an assert construct. As a simple example, code can verify that a given parameter, size, is sufficiently small by calling: assert(isSmall(size));
This might be important to ensure that a subsequent copy operation does not overflow a buffer. If the isSmall helper function returns false, then the runtime will signal an assertion violation. Inspired by static verification techniques based on Hoare logic, a few languages include more expressive forms of assertions such as preconditions and postconditions. Preconditions specify a method’s initial assumptions, and postconditions specify a method’s final guarantees. Eiffel, for example, promotes the Design By Contract methodology, which is predicated on the pervasive specification of preconditions, postconditions, and invari
Chapter 1: Introduction
3
ants (Meyer 1997). Eiffel also supports an advanced feature of postconditions: they can evaluate expressions in the old state, with memory as it was before the contracted function was run. Pushing expressiveness in a different direction, Findler and Felleisen (2002) generalize assertions to work for higher order contracts. This allows a higher order function, such as map, to impose a contract on its argument function. This is critical for supporting rich specifications in a higher order language. In particular, this work underlies the contract mechanism used in Racket (formerly PLT Scheme) (Flatt and PLT 2010). A contract for a restricted map function can be encoded as follows in Racket: (→ (→ positive? positive?) list? list?)
This contract specifies that the map function accepts a function and a list, and returns a list. The argument function must both accept and return only positive numbers. These generalizations of the basic assertion are useful, but are still limited compared to the wealth of techniques developed for static verification. Selected examples include frame conditions (March´e and PaulinMohring 2005), ownership types (Clarke et al. 2001), support for temporal properties, separation logic (Ishtiaq and O’Hearn 2001), and rely/guarantee (Jones 1983) reasoning. This dissertation provides a framework for enabling dynamic validation of selected expressive specifications. We concentrate on state related contracts for a functional language, and do not address effects such as I/O or concurrency. Specifically, we demonstrate how to support assertions with time travel (for postconditions), frame conditions, and separation logic. Additionally, we provide a basis for safely supporting temporal properties. By enabling these specifications, we allow programmers to clearly express and verify more
Chapter 1: Introduction
4
void deleteMin(Tree *t) requires well_formed_tree(t) ensures well_formed_tree(t) && ! find(t, \old(getMin(t))) modifies getLocations(t)
Figure 1.1: Possible specification of deleteMin properties of a program, and pave the way for integration with static techniques. More detailed descriptions of these contracts will be presented in Section 2.1 and Chapter 4, but we provide a high level introduction here. Assertion contracts specify what a function should do: they assert that some property holds of the current state. Framing contracts, on the other hand, restrict the effects a function can have. Separation contracts combine an explicit assertion contract with an implicit framing contract, using a form of access control. Assertions used in a separation contract delineate what parts of memory are relevant to a computation. The separation contract then restricts the computation, only allowing it to access the relevant parts of memory. To motivate these different types of contracts, consider a deleteMin function that deletes the smallest number in a binary search tree. A possible interface for deleteMin is presented in Figure 1.1. This interface specifies a precondition for deleteMin: it requires the input tree to be wellformed. A postcondition ensures that the tree is still well formed after deleteMin completes. Additionally, it guarantees that the smallest element of the original tree was deleted by the function. To check this, the postcondition runs the getMin function in the old state of memory, obtaining the minimum value stored in the original tree passed to the function. The postcondition then passes this value to find, run against
Chapter 1: Introduction
5
the tree as it is after the deleteMin function has run. Note that this specification of deleteMin is not a complete specification. For example, it does not guarantee that deleteMin preserves the rest of the tree: a function which deletes everything in the tree would satisfy this interface. This highlights a feature of contract systems: it is simple to write partial specifications. Contract writers can write contracts as needed, without needing to fully specify an interface. The interface in Figure 1.1 also includes a framing contract that bounds the locations deleteMin modifies. This contract restricts deleteMin to writing only locations in the
tree. Frame conditions are commonly used for static verification, and are crucial for modular reasoning. Consider the following code, which manipulates two trees, tree1 and tree2: int min = getMin(tree1); deleteMin(tree1); deleteMin(tree2); assert(min < getMin(tree1));
The final assertion in this code should always hold: the postcondition on the first call to deleteMin ensures that the minimum element, min, is no longer in the tree. Assuming a
reasonable specification for getMin, this implies that the assertion holds. However, there is one detail left: the second call to deleteMin. Assuming that tree1 is not an alias of tree2, we can simply ignore the effect of that call, since it only modifies locations in tree2, and so does not affect our reasoning about tree1. In larger programs, there are
frequently many such irrelevant functions called in between updates of the data structure in question. Framing allows the effects of these functions to be discounted. Since the
Chapter 1: Introduction
bool well_formed_tree(Tree *t) {
6
LocSet *getLocations(Tree *t) {
if(t == null) {
if(t == null) {
return true;
return null;
} else {
}
return wft_l(t→left, t→elem)
return add(t, union(
&& wft_r(t→right, t→elem);
getLocations(t→left),
}
getLocations(t→right)));
}
}
bool wft_l(Tree *t, int elem) {
bool wft_r(Tree *t, int elem) {
if(t == null) {
if(t == null) {
return true;
return true;
} else {
} else {
return t→elem < elem
} }
return elem < t→elem
&& wft_l(t→left, t→elem)
&& wft_l(t→left, t→elem)
&& wft_r(t→right, t→elem);
&& wft_r(t→right, t→elem); } }
Figure 1.2: The well_formed_tree and getLocations helpers for deleteMin
Chapter 1: Introduction
7
functions do not modify the relevant portion of memory, they cannot affect the assertion in question. Having introduced assertion and framing contracts, we now motivate separation contracts. Looking again at the interface for deleteMin given in Figure 1.1, notice that the precondition uses a helper function well_formed_tree and the frame condition uses a helper function getLocations. Figure 1.2 presents a possible implementation of these helper functions. The well_formed_tree function calls two mutually recursive helper functions, wft_l (wellformedtreeleft) and wft_r (wellformedtreeright), that recurse down the tree and ensure it satisfies the binary search tree invariant. All elements in the left subtree must be less than the current node’s element, and all elements in the right subtree must be greater than the current node’s element. The getLocations function also recurses over the tree, but instead of checking its contents, it gathers up all the node’s locations into a set. This set can then be used as the framing condition for the Tree’s methods. Separation contracts, inspired by separation logic (Ishtiaq and O’Hearn 2001), provide a way to avoid the redundant tree traversals performed by both well_formed_tree and getLocations. Observing that well_formed_tree needs to traverse the entire tree in
order to ensure that is satisfies the binary search tree invariant, a separation contract can use the footprint of well_formed_tree as the frame condition for the deleteMin function. Only the locations accessed by well_formed_tree will be accessible by deleteMin. This and other features of separation contracts (including a computational variant of the separating conjunction from separation logic) are discussed in more detail in Section 4.6.
Chapter 1: Introduction
1.1
8
Contracts: Usability vs Safety
As just introduced, this dissertation focuses on dynamically checking expressive contracts inspired by static verification. In addition to the differences previously discussed, there is another distinction between specification languages commonly used for dynamic checking and those used for formal verification. Contract languages, intended for dynamic verification, are generally specified using the host programming language. The most common contract, the assertion, usually runs any valid boolean expression in the host language. This allows programmers to employ a familiar language when writing specifications. Additionally, it allows for code reuse: contracts can reuse existing program code. For example, the update function introduced in the previous section called the lookup function in its precondition. Finally, reusing the host language drastically simplifies the implementation. For example, the assert construct in C simply runs the provided code. If the code returns false, the assert construct signals a contract violation. In contrast, specifications intended for formal verification are generally written in a different language from the program. Common examples are first order logics and higher order logics. This ensures that the specifications themselves have a well understood meaning that is easy to reason about. However, it also hinders adoption, as it forces the programmer to learn a new language. Additionally, portions of the program need to be rewritten in the specification language and then connected to the program’s code. Another way to view this divide between dynamic and static specification languages is through an important safety property: erasability. A specification is erasable if it only affects the behavior of the program when the specification fails to hold. Languages de
Chapter 1: Introduction
9
int getSize(file) { size = getSize(file); ... fstat ... assert(isSmall(size)); } if(errno) {
bool isSmall(size) {
... return log(size) < THRESHOLD; } }
Code with an assertion
Helper functions: both can set errno
Figure 1.3: Accidentally changing errno inside of an assertion signed for static verification naturally satisfy this property, as the specification is not run as part of the program and does not change its semantics. Dynamic contracts that allow host language expressions to be run, however, may allow a contract to affect the behavior of the program. In fact, the assertion mechanism in most languages allows this problematic behavior. For example, the Rationale for the ANSI C Programming Language warns: “Note that defining the macro NDEBUG to disable assertions may change the behavior of a program with no failing assertion if any argument expression to assert has sideeffects, because the expression is no longer evaluated.” (American National Standards Institute 1990) Allowing a specification to alter the behavior of a program can be disastrous. For example, Figure 1.3 presents an assertion that accidentally modifies the global errno variable, used for reporting errors in C standard library functions. The program first obtains the size of a file using the getSize helper, which internally calls fstat. If the resulting system call fails, it will set errno to report the problem. After validating the size of the file, the code checks if errno was set, indicating that the file size could not be obtained. Unfor
Chapter 1: Introduction
10
tunately, the assertion itself may have set errno, altering the final check of errno. In particular, if an empty file is queried then the logarithm of zero will be requested, which is undefined. To report the error, the log function sets the global errno variable. void copier(char *input) {
bool alldigits(char *str,
int size = 8;
int *size) {
char buffer1[8];
while(*size != 0
char buffer2[8];
&& str[*size] != ’\0’) { if(isdigit(str[*size])) { return false;
strncpy(buffer1, input, size); assert(alldigits(input, &size));
} (*size)++; return true;
strcpy(buffer2, buffer1); }
}
A method with a buffer overflow
The alldigits helper function
Figure 1.4: Masking a buffer overflow with an assertion
Figure 1.4 presents a subtler and more pernicious problem, where running an assertion masks a serious bug. The copier function copies the first eight characters of an input string into a local buffer. It then calls the alldigits helper function to ensure that the copied characters are valid. Finally, it uses strcpy to copy the string from buffer1 into buffer2. The copier function has a potentially serious bug: if the input string is at least
eight characters long, then only the first eight will be copied into buffer1. In particular, this means that the final null character (which is just the integer 0 interpreted as a character) signaling the end of the string will not be copied. The call to strcpy used to copy the string
Chapter 1: Introduction
11
from buffer1 into buffer2 will then overflow. It will continue copying everything on the stack until it happens to find a 0 somewhere on the stack. If assertions are disabled and the call to assert is elided, then this bug is likely to corrupt the stack. However, if assertions are enabled, the call to alldigits may actually mask the bug. A side effect of calling alldigits is that the size integer stored on copier’s stack frame is overwritten with 0. If memory is laid out so that size is stored
right after buffer1. This will serve to null terminate the buffer, limiting the damage done by the bug. Thus, the bug is likely to only manifest when assertions are turned off. This is very troubling, as assertions are important for debugging, and so are likely to be enabled in an attempt at finding the bug. As just illustrated, there is a conflict between usability and safety. To increase their usability, contracts should be allowed to use the host language. To ensure their safety, we need to ensure that contracts do not have any untoward sideeffects. One solution to this problem is proposed by the Java Modeling Language (JML). Assertions in JML are written in the host language, Java. However, assertions may not have sideeffects. Any methods called must be marked as [pure], indicating that they do not have any sideeffects and only call other pure methods. Unfortunately, only allowing pure code does not completely solve the problem. This restriction suffices to ensure safety: JML specifications do not affect the behavior of the program. By disallowing benign side effects, this restriction limits the usability of the language. JML specifications cannot call existing code that has not been marked pure. This is not a simple problem to fix: adding the pure annotation to a method limits all implementations of that method from using benign side effects. For example, JML contracts may not
Chapter 1: Introduction
12
call object.equals, as this method may not be marked as pure. Some objects practice lazy initialization, where the object’s data is only initialized when the first method is called on it. If that method happens to be the object’s implementation of the equals method, then that method will need to have side effects. In practice, this type of problem leads to a lot of code duplication: the relevant parts of the program need to be rewritten in a purely functional style so they can be called inside of a contract. More generally, it can be very useful to call code that may have sideeffects. The lookup function for a splay tree provides a good example. A splay tree is a binary search
tree that implements all of its operations by adjusting (splaying) the tree to move the desired element to the root of the tree (Sleator and Tarjan 1985). In particular, this means that lookup operations have the side effect of modifying the tree so that the requested node is the new root node. Splay trees present a realistic example of a benign side effect: the lookup operation modifies the structure of the tree while preserving its contents. Given a data structure (a set for example) implemented with a splay tree, it should be possible to call the lookup function inside of a contract. Note that it does not suffice to simply declare certain effects as benign and allow them. This would abrogate our desire for safety, especially as it is easy to make a mistake and label a sideeffect benign when it actually can affect the program. The splay tree again provides a nice example. While splaying is a benign operation with respect to the other splay tree operations (lookup, insert, delete, . . . ), if the program does a depth first traversal of the tree it will get different results before and after a splay. Recapping these points, usability requires that contracts be able to call code that may have (assumed to be benign) side effects, while safety requires that all such side effects
Chapter 1: Introduction
13
not affect the behavior of the program. This dissertation introduces a novel approach to mediating the tension between usability and safety. Section 4.3 introduces a suppression contract, which allows an expression with side effects to run, but then suppresses those effects to ensure that they do not influence the rest of the program. If the lookup function is called on a splay tree inside of a suppression contract, the tree will be splayed as needed so that the result can be calculated and the desired assertion can be checked. Then, the suppression contract will revert the tree back to its original structure, restoring memory to its previous state, and thereby conservatively ensuring that the contract does not affect the rest of the program.
1.2
Delimited Checkpoints
Rather than introduce adhoc mechanisms for supporting the different contracts outlined above, we introduce a simple interface, the delimited checkpoint which is capable of supporting all of them. Checkpoints generalize the idea of a memory snapshot, representing a point in time during a program’s execution. Computations can be run with memory as it was when a checkpoint was taken, allowing a form of time travel. This is akin to first class stores (Johnson and Duggan 1988), which allow the store to be reified as a first class object1 . Crucially, however, checkpoints allow another novel operations: difference. Checkpoints can be compared, revealing how a computation interacted with memory in between the two checkpoints. As will be demonstrated in Chapter 4, this interface suffices to implement usable assertion, framing, and separation contracts in a safe manner. Additionally, our focus on de1
Section 7.1 discusses related work on first class stores in more detail.
Chapter 1: Introduction
14
limited checkpoints that can only be used within a given static scope allows for a straightforward implementation. Chapter 6 demonstrates how existing machinery for Software Transactional Memory systems (STM is introduced in Section 2.3) can be reused to efficiently support the delimited checkpoint interface.
1.3
Contributions and Structure
The delimited checkpoint interface serves as the technical foundation for this thesis. Chapter 2 presents technical background important for understanding the rest of this thesis. First, we present more information about contracts, providing additional examples, a discussion of enforcement techniques, and desirable properties. Next, we introduce the Haskell programming language, which we use for our implementation. Finally, we review Software Transactional Memory, focusing on the interface provided by Haskell. Chapter 3 introduces and describes delimited checkpoints, the interface that forms the basis of the contracts supported by this work. Chapter 4 demonstrates that delimited checkpoints provide a solid foundation for building contracts, implementing three different types of contracts: assertion, framing, and separation contracts. Additionally, Section 4.3 introduces a new variant of assertion contracts which safely allow assertions to have a class of benign side effects, and Section 4.4 adds support for memory that is shared amongst multiple contracts, enabling contracts to enforce temporal constraints. Next, Chapter 5 presents a formal operational semantics for delimited checkpoints, precisely defining the interface given in Chapter 3. Using this semantics, the contracts described in Chapter 4 are proven to satisfy appropriate theorems. For example, Section 5.5
Chapter 1: Introduction
15
proves that successful assertions do not affect the behavior of the program, and Section 5.6 proves that the separation contract implemented in Section 4.6 obeys the frame rule, a crucial property of separation logic. For readability, Chapter 5 omits the details of many proofs, instead presenting their intuition. For completeness, Appendix A supplies all the missing details. Following this development of the theory underlying delimited checkpoints, Chapter 6 discusses their implementation. Restricting checkpoints to a constrained scope allows the implementation to reuse the same infrastructure used to support software transactional memory. Section 6.1 presents a prototype implementation built on top of the GHC Haskell compiler. Finally, Chapter 7 discusses how this thesis fits in with and extends prior work, and Chapter 8 recaps the results in this thesis and discusses the intended benefits of this work, as well as interesting avenues for future exploration.
Chapter 2 Background In this chapter, we introduce important concepts that recur throughout this thesis. First we provide a brief introduction to interesting types of contracts. We next provide a brief introduction to the Haskell programming language, which will be used for both exposition and implementation purposes. Finally, we introduce Software Transactional Memory (STM), with a special focus on the STM interface provided by Haskell. For reasons that we explain in Section 2.3, STM implementations rely upon functionality similar to that needed by delimited checkpoints. Section 6 exploits this, explaining how to implement delimited checkpoints on top of existing STM implementations.
2.1
Contracts
A contract specifies how a part of a program should behave. Commonly used contracts include assertions, preconditions, invariants, and postconditions, all of which are computable predicates over a program’s state. Other contracts limit a computation’s effects.
16
Chapter 2: Background
17
These framing contracts enable local reasoning about the program. The simplest and most widelyused contracts are assertions of the form assert(e) where e is a computable booleanvalued expression that halts or throws an exception when e returns false. Many programming languages support these simple assertions. For exam
ple, the following is a typical check for a null pointer in C: assert(ptr != NULL);
To enable dynamic analysis, assertions must be computable. The simplest way to ensure that is to write them in the host language (in this example, the C programming language). An increasing number of programming languages also support more structured assertions, including preconditions, postconditions, and invariants. These are heavily used in formal specifications, but they are also gaining support in programming languages such as Eiffel (Design by Contract) (Meyer 1997) and C# (Code Contracts) (F¨ahndrich et al. 2010). As an example, an Eiffel interface for a function to transfer money between two bank accounts might look like: transfer (a: ACCOUNT; b: ACCOUNT; amount: INTEGER) is require amount ≤ a.bal ensure a.bal + b.bal = old (a.bal + b.bal) end
Figure 2.1: Possible Eiffel Specification of a Bank Transfer Function The require clause verifies that there are sufficient funds in the source bank account
Chapter 2: Background
18
a and the ensure clause verifies that the final total balance is the same as the original total
balance. In addition to assertions, contracts can specify frame properties, restricting what effects a computation can have. Interesting effects include throwing an exception, interacting with memory, failing to terminate, spawning a thread, or performing I/O. These framing contracts are supported by specification languages such as the Java Modeling Language (JML), a specification language for Java. Using the Java Modeling Language (JML), an interface for the previous bank transfer example can include a frame property (modifies) that limits the memory the function is allowed to modify. Here, the transfer function can modify only the accounts passed in as void transfer(account a, account b, int amount) requires amount ≤ a.bal ensures a.bal + b.bal == \old(a.bal + b.bal) modifies {a, b}
Figure 2.2: Possible JML Specification of a Bank Transfer Function arguments to the function. Specifically, the JML modifies clause allows the transfer function to only change the fields of the a and b objects
2.1.1
Enforcement
Contracts are useful for gaining confidence in the correctness of a program, and can be statically verified or dynamically enforced. This choice may depend on the language and environment. Languages inspired by formal reasoning may support full static verification,
Chapter 2: Background
19
whereas dynamic languages, such as Scheme, may opt for dynamic enforcement. In languages such as Haskell (introduced in Section 2.2), it is natural to encode static guarantees in the type system. However, more complicated contracts that cannot be encoded in the type system are more convenient to verify with dynamic enforcement, obviating the need for programmer guided theorem proving or code restructuring. Verifying an assertion or a precondition is simple: just run it and see if it runs successfully. Enforcing framing contracts, which restrict a program’s effects, is more complicated. Static enforcement of framing contracts is greatly simplified if the language is restricted to simplify analysis. For example, the Java type system forces every method to declare the set of (checked) exceptions that may be propagated. Forcing the programmer to annotate every method simplifies the enforcement of these annotations. As will be discussed in Section 2.2.3, Haskell similarly uses the type system to sequester code that may perform I/O.1 In contrast, statically verifying a modifies clause requires a form of alias analysis. This could be done using an ownership discipline as done by Clarke et al. (2001), separation logic (introduced in Section 4.6) or a standard alias analysis (points to analysis) as is done by Cheng and Hwu (2000). The first two require additional annotations (throughout the code) and may necessitate restructuring the code to enforce the required invariants, while the latter is overly conservative. When static analysis detects a problem, the programmer is informed and can fix the problem. When dynamic enforcement detects a contract violation, deciding what to do is more complicated. Possible options include failing, suppressing the error, or trying to All code that may have side effects runs in the IO monad. As is usual, we ignore the FFI, unsafePerformIO, and similar unsafe operations which break this assumption. 1
Chapter 2: Background
20
correct the error. Failure options include throwing an exception, rolling back to a previous good state, and terminating the program. A final option is to try to correct the error. There has been prior work on automatically fixing programs to meet their specifications. Demsky and Rinard (2003) focus on the special case of data structure invariants. When a data structure is left in an illegal state, they attempt to repair the data structure by making small changes until all the invariants are satisfied. More recently, Weimer et al. (2009) demonstrate how genetic programming can be used to automatically patch erroneous code to meet its specification, and Samimi et al. (2010) use a constraint solver to execute specifications as a backup implementation when the provided implementation fails. This thesis will not explore general corrective contracts, instead focusing on the detection and possible suppression of errors. Suppression can be seen as a specific case of corrective contracts, which can be automatically applied without domain specific knowledge. In general, suppressing contract violations is dangerous, potentially masking bugs. However, as we we will discuss in Section 4.3, these suppression techniques can be useful when applied to contracts themselves.
2.1.2
Erasability
Contracts are supposed to verify a property of a program, not affect the behavior of the program. More formally, contracts should be erasable: they should either signal a contract violation or have no effect on the program. This justifies the common practice of erasing contracts for performance reasons. If a contract is statically verified (or, in practice, strongly believed) to always hold, it can be safely elided.
Chapter 2: Background
21
If an assertion can be statically verified to have no observable effects, it is simple to guarantee its erasability. That is, erasing a successfully terminating assertion will not change the behavior of the program. However, most languages do not statically differentiate between pure and impure code. Languages that do so differentiate, such as Haskell, may be overly conservative, and needlessly restrict harmless effects. For example, in many settings, reading from memory is harmless (and useful for assertions). However, in concurrent contexts, reading from shared memory may be treated as an effect, and disallowed, in order to ensure that transient values (written by another thread) are not observed. This can make static approaches unwieldy or overly conservative. An alternative is to dynamically enforce sufficient conditions to guarantee that an assertion is erasable. In other words, we specify erasability as a contract on assertions. Section 4.1 will describe how to implement such a contract using delimited checkpoints. Specifically, we will use the Haskell type system to rule out a large class of effects, and use delimited checkpoints to eliminate the rest. The one effect that we choose to ignore is divergence.
2.2
Haskell
This thesis uses Haskell as its programming language of choice. For the benefit of readers unfamiliar with Haskell, this section provides a basic introduction to the features of Haskell needed to understand this thesis. This section is pragmatic in nature, and merely renders this thesis comprehensible.2 For those interested in learning more about the lanNote to the Haskellliterate: To make this section simpler, this thesis eschews many idiomatic Haskell patterns, attempting to limit the additional material those less Haskellliterate need to learn. Please forgive Haskell “code smells”, such as nests of parenthesis instead of the more idiomatic dollar sign, or the use of do 2
Chapter 2: Background
22
guage, there are many tutorials available3 , a common choice being “A Gentle Introduction to Haskell” (Hudak et al. 1997).
2.2.1
Syntax
Haskell is a layoutsensitive functional language. Functions are generally defined by cases (using pattern matching). As an example, the following defines a list length function: length [] = 0 length (_:xs) = 1 + length xs
Lists in Haskell are created out of the empty list, denoted [], and cons cells, denoted with a colon as head:tail, where head is the first element of a list whose remainder is the list tail.
The list length function considers both possible cases: the empty list and a nonempty list. The underscore used to represent the head of the list is a wild card, and indicates that a name is not needed for that part of the pattern match (since the value of the head of the list is not needed). Notice that function application is just denoted by juxtaposition and has higher precedence than operators. Therefore, parenthesis are needed to ensure that the length function patten matches on all of (_:xs) but not needed for the body of the expression, as the plus operation’s argument is the complete length xs subexpression. Any identifier made up entirely of the !#\$%&*+./?@\ˆ˜ symbols is considered to be an (inline) binary operator. So list append can be defined as: notation for even the simplest instances of bind. Similarly, this thesis generally avoids point free notation. Some recommendations can be found at http://haskell.org/haskellwiki/Learning_ Haskell 3
Chapter 2: Background
23
[] ++ y = y (x:xs) ++ y = x:(xs ++ y)
This definition has two cases, differentiated by pattern matching on the first list. When the first list is empty, the second list is returned. When the first list is a nonempty list, the first element is consed onto the result of calling append recursively, passing in the rest of the first list as well as the second list. To use an operator as a normal (prefix) function, it can be surrounded by parenthesis: (++) x y is the same as x ++y. For the opposite effect, turning a function into an
(inline) operator, backticks are used. For example, to check if an element x is in a list l, it is common to write x ‘elem‘ l, which means the same thing as elem x l. Functional languages such as Haskell make it easy to define anonymous functions (also called lambdas). In Haskell, these are written using a backslash and arrow. The identity function, which takes in an argument (called x here) and promptly returns it, is written \x→x.
2.2.2
Types
Haskell is a statically typed language: all values, expressions, and definitions have a static type. Type inference can conveniently infer all needed types4 , however it is good practice to specify the types of top level definitions as (compiler checked) documentation. Types are written after a double colon. Looking at our previous example, the length function could be given the following type, indicating that it is a function from a list to an integer: 4
Unless certain advanced features are used.
Chapter 2: Background
24
length :: [a] → Integer
The length function is polymorphic in the type of the list: it does not care if the list contains integers, characters, or anything else. The a in the type is implicitly generalized, so the type should be read: for any type a, length is a function from a list of a’s to an integer. Note how Haskell uses [] to construct both list values and list types. As another example, the ++ append operator has the following (polymorphic) type: (++) :: [a] → [a] → [a]
As discussed in Section 2.2.1, the parenthesis turn the ++ operator into a normal function. Note that it would also be correct to say that ++ has the type: (++) :: [Integer] → [Integer] → [Integer]
In fact, ++ has an infinite number of types. However, the first type we gave is its most general (principal) type, since ++ doesn’t really care about the type of the elements of the list. While the list type [a] is polymorphic in its elements’ type, it enforces that all its elements must have the same time. To conveniently aggregate values of different types, Haskell provides tuples. Parenthesis and commas are used to denote tuples at both the type and value level. For example, the following defines the fst and snd functions for a pair5 , as well as a function that creates a pair of a string and an integer and immediately projects out the first component using the fst function: fst :: (a, b) → a fst (x, _) → x For historical reasons, the standard library uses these abbreviations rather than spelling out “first” and “second” 5
Chapter 2: Background
25
snd :: (a, b) → b snd (_, y) → y
f :: String f = fst (‘‘hi’’, 4)
New types can be created using the data keyword. These algebraic data types are tagged unions, which are deconstructed via pattern matching, as discussed above. For example, a custom list type can be declared as data List a = Nil  Cons a (List a)
This declares a new type (List) with one type variable (a). It has two constructors: Nil, which carries no data, and Cons, which has an element of the appropriate type and the rest of the list. Constructors can be used both as functions that create data of the appropriate type and as patterns that can be used to deconstruct data. The length function for our custom list type would be defined as: length :: List a → Integer length Nil = 0 length (Cons _ xs) = 1 + length xs
Note that the brackets ( [] ) and colon ( : ) used earlier are special syntax provided by the compiler for the builtin list type. As another example, Boolean values are a normal datatype, declared as: data Bool = True  False
Chapter 2: Background
26
In addition to creating new types, it is convenient to create alternative names for existing types. These type aliases are created with the type keyword. A programmer that likes short names could use the following: type I = Integer
to provide an alternate name for Integer. Type Classes An innovative feature provided by Haskell is its support for adhoc overloading through type classes. Related methods are collected into a type class. A motivating example for type classes is equality. The standard Eq type class defines the == operator. class Eq a where (==) :: a → a → a
A type can be declared to be an instance of a type class by providing the appropriate definitions. For example, equality is defined for Bool (the type of booleans). instance Eq Bool where (True == True) = True (True == False) = False (False == True) = False (False == False) = True
The proliferation of equal signs is slightly confusing, but this states that Bool is an instance of the Eq type class, and defines the equality operation by cases. Note that = is used for definitions, while == (which we just declared) is used for comparison.
Chapter 2: Background
27
Now, let us turn to a use of this equality type class. The elem function mentioned before takes an element and a list and checks if the elements is in the list. It can be defined using our new equality operator as: elem _ [] = False elem x (x’:xs) = i f x == x’ then True else elem x xs
Note that Haskell, owing to its mathematical roots, allows the use of single quotes in identifiers. The identifier x’ should be pronounced as “x prime”. This function is defined for any list whose elements have equality defined for them (are instances of the Eq type class). Its type is written as: elem :: Eq a ⇒ a → [a] → Bool
This should be read: forall types a such that a is an instance of the Eq type class, elem takes an element of type a and a list whose contents have type a and returns a boolean. As usual, this type is automatically inferred by the compiler. In this thesis, we will not need to create new type classes, but will mention the Eq type class just presented as well as the type class for monads, discussed forthwith in Section 2.2.3.
2.2.3
Monads
Haskell strongly distinguishes between pure code and code that can have side effects (other than nontermination). All the code that we have seen so far is pure, making it very easy to reason about. Since there are no hidden dependencies, code can be run in any order (or even in parallel).
Chapter 2: Background
28
On the other hand, code with side effects is sequestered into monads. The monad type class is essentially defined as follows: class Monad m where return :: a → m a (>>=) :: m a → (a → m b) → m b
The return method takes a pure value and turns it into a monadic action that, when run, does nothing and returns the given value. The >>= (pronounced bind) operator sequences two monadic actions, composing them to form a larger monadic action. When the resulting monadic action is run, it runs its first argument. It then passes the return result into the second function, and runs the resulting action. Notice that the final value is again a monadic action: there is no escape. In fact, there is no general way to escape from a monad, ensuring that if a computation has an effect, its type will include the monadic constructor. Most monads provide additional primitives to perform typespecific actions. For example, Haskell uses the IO monad to sequester most effects, including reading/writing references and input/output. A typical example is putStrLn, which prints a line to standard out. putStrLn :: String → IO ()
Note that () (read unit) is a type with only one value, also written (). It indicates that the monadic action does not return any interesting information. Since there is no escaping the IO monad, how are IO actions run? The special main value in a program is a monadic IO action which is run when the program is started. Essentially, the Haskell compiler and runtime are the escape mechanism for the IO monad: running a program runs its associated IO action.
Chapter 2: Background
29
Syntactic Support: do Notation Monads are so heavily used in Haskell programming that they enjoy special syntactic support. The following code asks for and prints a person’s name: do putStrLn ’’Welcome. What is your name?’’ line ← getLine putStrLn (’’Your name is: ’’ ++ line)
This desugars into the following uses of bind: putStrLn ’’Welcome. What is your name?’’>>=(\_ → getLine>>=(\line → putStrLn (’’Your name is: ’’ ++ line)))
This notation also has support for pattern matching, which we will use in conjunction with tuples. If the computation is a monadic action that results in a pair, the following puts the return values in x and y, respectively. do (x, y) ← c
The special do notation just presented has an imperative feel, making it simple to write monadic/imperative code in Haskell. This thesis will generally use do notation and eschew use of the underlying bind operator in an attempt to make code simpler to read for the nonHaskell literate.
2.3
Software Transactional Memory
Now that we have introduced Haskell, we turn to the last bit of background necessary for this thesis: Software Transactional Memory (STM). Combining the two, Section 2.3.1
Chapter 2: Background
30
presents the Haskell interface to STM. Software Transactional Memory provides a simple way to restrict interleavings in a concurrent program. Programmers specify that a block of code should execute as a single transaction. Within that transaction they can reason about the code as if it were sequential: the entire transaction runs unaffected by other transactions. This provides a composable way to restrict the interleavings in a concurrent program, freeing the programmer from having to worry about locking protocols and deadlock. As a first example, we present software transactional memory as introduced by Harris and Fraser (2003), as an extension to the Java language. After this, we will present the Haskell interface, which we will use throughout the rest of this dissertation. To implement the previously presented bank transfer example in a multithreaded environment, we need to ensure that two concurrent calls to transfer do not incorrectly interleave. Additionally, we want to make sure that any other transactions that access either account’s balance do not see incorrect intermediate data. This can be accomplished by simply wrapping the code in an atomic block, as shown in Figure 2.3. The STM implementation will ensure that two transactions run as if they do not interleave. Most STM implementations are optimistic — they attempt to run multiple transactions concurrently. In order to do this, they need a way to track the transactions’ effects and ensure that they do not conflict. If a conflict is detected, one of the transactions needs to be rolled back and restarted. Thus, the STM implementation needs to be able to suppress all the effects of an errant transaction and pretend it was never run. Section 6 shows how to exploit this ability (and the data collected to support it) to implement delimited checkpoints. Not all effects can be rolled back. If data is written to a write once destination (such as
Chapter 2: Background
31
void transfer(account a, account b, int amount) { atomic { if(a.bal < amount) { throw new NotEnoughMoneyException(); } a.bal = amount; b.bal += amount; } }
Figure 2.3: Implementing a bank transfer function with an atomic block a CDR), and the transaction that wrote the data later aborts, the transaction cannot undo all of its effects. As a result, transactional memory systems generally do not allow I/O or other effects that cannot be automatically undone. In fact, one of the major complaints against transactional memory as a language feature is its poor integration with nonmemory resources.
2.3.1
STM Haskell
Since this dissertation focuses on Haskell, this section presents Haskell’s STM interface. Other languages and STM implementations have similar interfaces, as described in more detail by Larus and Rajwar (2007). In particular, they all expose primitives to access memory and to atomically run a transaction. Just as Haskell restricts sideeffecting computations to the IO monad, it encapsulates
Chapter 2: Background
32
transactional computations in the (new) STM monad. This allows the type system to statically ensure that all effects arising during a transaction can be rolled back by the transactional memory system. Since the type of all I/O operations restricts their use to the I/O monad, transactional code (sequestered in the STM monad) simply cannot call such functions. Figure 2.4 shows the interesting parts of the STM Haskell interface, introduced by Harris et al. (2005). The operational semantics they introduce to formally specify this interface are presented in Section 5.1. Transactions are run using the atomically command: the resulting IO computation executes the transaction as a single block, guaranteeing serializability with respect to other transactions. STM Haskell supports three control flow operators to compose transactions, building larger transactions from smaller ones. Transactions can be composed sequentially (one after another), in alternation (either one or the other), or exceptionally (one, with another in case of an exception). The most important building block for transactions is sequencing, provided by the usual monadic bind operation. As discussed in Section 2.2.3, Haskell provides do blocks as syntactic sugar for sequencing monads using bind. Using this notation, the following code runs command1, which returns a boolean. If True is returned, then command2 is run next, otherwise command3 is run. do b ← command1 i f b then command2 else command3
Transactions can also be built in alternation. The command t1 ‘orElse‘ t2 runs t1,
Chapter 2: Background
−− The STM monad data STM a instance Monad STM
−− Exceptions throw
:: Exception → STM a
catchSTM
:: STM a → (Exception → STM a) → STM a
−− Running STM computations atomically :: STM a → IO a retry
:: STM a
orElse
:: STM a → STM a → STM a
−− Transactional references data TVar a newTVar
:: a → STM (TVar a)
readTVar
:: TVar a → STM a
writeTVar :: TVar a → a → STM ()
Figure 2.4: STM monad interface from STM Haskell (Harris et al. 2005)
33
Chapter 2: Background
34
return its value, and commits its effects unless t1 calls retry. If t1 executes retry then its effects are undone and control transfers to t2 instead. If t2 calls retry, control transfers to any parent orElse that needs to be retried. If there is none, the entire transaction is aborted and restarted. Note that the effect of running t1 ‘orElse‘ t2 is that exactly one of the two commands will appear to have run. Finally, STM Haskell supports exception handling inside of a transaction. Exceptions are thrown using the standard throw function, and caught by the new catchSTM function6 . The standard catch function’s type restricts its use to the IO monad, necessitating the introduction of the catchSTM function, which works in the STM monad. To simplify error handling, exceptions that are not caught within a transaction abort the transaction before they are further propagated. Since we can build larger transactions from smaller ones, all we need are small transaction primitives that actually do something. In particular, since software transactional memory is largely concerned with access to shared memory, it must provide primitives to read and write memory. Haskell provides shared memory in the form of IOVars, which are only readable or writable from within the IO monad. Rather than allow transactions to access these general references, STM Haskell introduces a new type of reference, TVars, meant only for transactional use. It provides functions to create, read, and write to such references. Note that these functions are only accessible from the STM monad: if a general IO computation wants to access a transactional reference, it must explicitly use atomically to wrap the access in a minitransaction. GHC Haskell actually supports an extensible hierarchy of dynamically typed extensions, which our implementation takes advantage of, however we simplify the interface for pedagogical purposes. 6
Chapter 2: Background
35
This design choice nicely sidesteps a common problem with transactional memory implementations. What happens when nontransactional code accesses memory that is being written by a transaction? Implementations that guarantee strong atomicity effectively wrap each such access in a minitransaction. Implementations that only guarantee weak atomicity leave the behavior of such an access unspecified, complicating the semantics. In STM Haskell, the type system ensures that this situation never arises. Putting these pieces together, the transfer function from Figure 2.3 could be written in STM Haskell as shown in Figure 2.5. transfer :: Account → Account → Integer → IO Integer transfer a b amount = atomically (do abal ← readTVar (bal a) when (abal < amount) (throw NotEnoughMoneyException) writeTVar (bal a) (abal  amount) bbal ← readTVar (bal b) writeTVar (bal b) (bbal + amount))
Figure 2.5: Implementing a bank transfer function in STM Haskell
This examples creates a transaction using atomically. This transaction reads the first account’s balance and checks that it suffices. If so, it deducts the requested amount and then adds it to the second account. Since all these actions happen atomically, there is no need to worry about other threads seeing an intermediate state. In particular, from the point of view of other threads the sum of the two accounts’ balances is constant. For example, the run function in Figure 2.6 first calculates the sum of two accounts. It then uses forkIO
Chapter 2: Background
36
to spawn two threads, both of which transfer money from one account to the other. The final sum is then calculated, and compared to the original sum. Note that calculating the new sum is interleaved with both transfers: either none of, one of, or both of the transfer functions could have been completed when the new sum is calculated. Nonetheless, the run function will always run before or after a transfer — run cannot run at the same
time as transfer. As a result, the run function will always return True. run :: Account → Account → IO Bool run a b = do sum ← getSum a b forkIO (transfer a b 5) forkIO (transfer b a 10) newsum ← getSum a b return (sum == newsum)
getSum :: Account → Account → STM Integer getSum a b = atomically (do abal ← bal a bbal ← bal b return a + b)
Figure 2.6: The transfer transaction preserves the sum of the accounts
STM Haskell, introduced in this section, serves as a foundation for the core interface introduced in this dissertation: delimited checkpoints. The next chapter introduces this interface, using the types introduced in Figure 2.4. A formal operational semantics STM Haskell is presented in Chapter 5 as the basis for a formal semantics for delimited check
Chapter 2: Background
37
points. Subsequently, Chapter 6 presents an implementation of delimited checkpoints using the underlying implementation of STM Haskell found in the GHC compiler and runtime.
Chapter 3 Delimited Checkpoints This dissertation develops a framework for the dynamic validation of expressive contracts. This chapter introduces the foundation of this framework, the delimited checkpoint interface. Subsequently, Chapter 4 demonstrates the utility of this interface, using it to implement a variety of expressive contracts. After that, Chapter 4.6.1 presents a formal operational semantics for delimited checkpoints and Chapter 6 describes an implementation of delimited checkpoints that reuses the infrastructure supporting software transactional memory. A delimited checkpoint provides an explicit name for the view of memory at a particular point in a computation. Checkpoints are similar to firstclass stores (Johnson and Duggan 1988) and can be used to run computations at a previous view of memory. Checkpoints can also be compared: successive views of memory, reified as checkpoints, reveal how a computation interacted with memory. Figure 3.1 presents a simple Haskell interface for delimited checkpoints. The withCCP (withCurrentCheckPoint) primitive gets a checkpoint of the current view of memory, and 38
Chapter 3: Delimited Checkpoints
−− Delimited Checkpoints data Checkpoint instance Eq Checkpoint
−− get the current checkpoint withCCP
:: (Checkpoint → STM a) → STM a
−− run at a checkpoint atCP
:: Checkpoint → STM a → STM a
−− get the difference between two checkpoints −− deltaWriteCheckPoint deltaWCP :: Checkpoint → Checkpoint → STM [UntypedTVar] −− deltaReadCheckPoint deltaRCP :: Checkpoint → Checkpoint → STM [UntypedTVar] −− deltaAllocatedCheckPoint deltaACP :: Checkpoint → Checkpoint → STM [UntypedTVar]
−− Untyped version of (TVar a) data UntypedTVar = forall a. UntypedTVar (TVar a) instance Eq UntypedTVar
Figure 3.1: Haskell Interface to Delimited Checkpoints
39
Chapter 3: Delimited Checkpoints
40
passes it into a computation. This checkpoint is delimited (scoped)—once the computation returns, the checkpoint is no longer valid. Using atCP (atCheckPoint), a computation can be run with all references temporarily set to their value when the checkpoint was taken. This supports a limited notion of time travel, allowing a computation to effectively query the past. As an example, Section 4.1.1 uses atCP to allow function postconditions to run a computation against memory as it was before the function ran. The interface also provides a way to compare two checkpoints to determine what has changed. The deltaWCP (deltaWriteCheckPoint) primitive returns the set of TVars written between two checkpoints. For simplicity, this set is modeled by a simple list. Similarly, the deltaRCP (deltaReadCheckPoint) and deltaACP (deltaAllocatedCheckPoint) primitives
return the set of references read and allocated between two checkpoints, respectively. We will refer to these three primitives collectively as the deltaCP primitives. The references returned by the deltaCP primitives point to varied types. For example, if a computation modifies both an integer pointer and a string pointer, the set returned by deltaWCP would need to include references of type TVar Integer and TVar String.
This makes it difficult to give a uniform type to the returned set of references. To solve this problem, the primitives return a list of UntypedTVars, which uses an existential to hide the type of the underlying data. Existential quantifiers are introduced in Haskell with the (counterintuitive) forall keyword, used with a data constructor1 . The definition should then be read as: there exists a type a such that the UntypedTVar contains a TVar a. Existential quantifiers are not supported by the Haskell standard, however are provided as an extension by some Haskell compilers, including GHC. 1
Chapter 3: Delimited Checkpoints
41
The delimited checkpoint interface also provides an equality predicate for untyped references, defining an instance of the standard Eq type class (introduced in Section 2.2.2) for the UntypedTVar type. Since any TVar can be turned into an UntypedTVar using the UntypedTVar constructor, this allows a program to check if a supplied reference was write/read/allocated between checkpoints.
3.1
Delimited Difference
The deltaCP primitives just introduced are very flexible. Given any two (still in scope) checkpoints, they return information as to what memory related events occurred between those two checkpoints. As discussed in Section 3.2.2 below, these checkpoints may not even be related in a chronological sense. However, the most common use of the deltaCP primitives is to obtain information about how a given computation interacts with memory. To encapsulate this stylized use, Figure 3.2 introduces the derived delimited deltaCP operations, also called the deltaCPd operations. Rather than take arbitrary checkpoints, these operations take a computation, which they augment to return a pair of both the computation’s result and the appropriate list of references. The mkDeltad helper function captures this augmentation: given a deltaCP primitive and a computation, it obtains a checkpoint, runs the computation, obtains a second checkpoint, and passes both checkpoints into the given deltaCP primitive. The resulting list, as well as the computation’s return value, are then returned. This helper function is then used to create delimited versions of the primitive deltaCP operations. This restricted interface to the underlying deltaCP primitives suffices for all the contracts implemented in Chapter 4. Additionally, as we will see in Chapter 5, the underlying
Chapter 3: Delimited Checkpoints
42
deltaRCPd :: STM a → STM ([UntypedTVar], a) deltaRCPd = mkDeltad deltaRCP
deltaWCPd :: STM a → STM ([UntypedTVar], a) deltaWCPd = mkDeltad deltaWCP
deltaACPd :: STM a → STM ([UntypedTVar], a) deltaACPd = mkDeltad deltaACP
mkDeltad :: (Checkpoint → Checkpoint → STM [UntypedTVar]) → (STM a → STM ([UntypedTVar], a)) mkDeltad delta c = withCCP ( \start → do res ← c d ← withCCP ( \end → delta start end) return (d, res))
Figure 3.2: Derived Delimited Difference Operations
Chapter 3: Delimited Checkpoints
43
primitives are dangerously powerful. They allow programs to make nonlocal observations that can make formal reasoning difficult. For example, a computation can observe what references its parent has read. It is still possible to prove that such programs behave as expected, but erasability will not necessarily hold.
3.2
Discussion
The simple description of the interface just given should suffice for understanding their use in building the contracts of Section 4. To resolve any ambiguities, Section 5 presents a formal operational semantics for the interface, built on top of an existing operational semantics for STM Haskell (Harris et al. 2005). Here, we discuss design decisions underlying delimited checkpoints and clarify the effect of delimitation2 . The first and most obvious question is: why delimitation? The requirement for delimitation restricts the use of checkpoints and complicates the interface. Additionally, despite this limitation, it is still possible to construct unordered views of memory. What do the difference operators mean when given different branches of memory, neither of which happened before the other? Finally, on a more technical level, it seems odd that the deltaCP primitives return a list of untyped references. This seems like a wart on a library for a strongly typed language like Haskell and restricts its usefulness. We use the term delimitation to describe static scoping and lastinfirstout nature of delimited checkpoints 2
Chapter 3: Delimited Checkpoints
3.2.1
44
Delimited Checkpoints
This dissertation focuses on delimited checkpoints, which are only valid for a given scope. Why not first class checkpoints (that never become invalid)? As we will see in Chapter 4, delimited checkpoints suffice for writing all the contracts in which we are interested. Furthermore, as discussed in Chapter 6, delimited checkpoints are simpler to implement. In particular, existing STM implementations can be adapted to support delimited checkpoints with minimal overhead. Thus, delimited checkpoints represent an important tradeoff of (as yet unneeded) expressiveness for implementation simplicity. Nonetheless, for completeness, Section 5.7 presents a formal semantics for undelimited checkpoints that never become invalid and Section 6.4 discusses their implementation. Statically Enforcing Checkpoint Scope All of the combinators and examples presented in this thesis (unless otherwise noted) are careful to use checkpoints only in a delimited fashion. However, this is not explicitly enforced by the delimited checkpoint interface defined in Figure 3.1. For example, it does not prevent the user from writing a getInvalidCP function that gets the current checkpoint and returns it outside the scope of the withCCP: getInvalidCP :: STM Checkpoint getInvalidCP = withCCP return
To prevent checkpoints from leaking, we can use a simple form of region types, based on the calculus of nested regions of Fluet and Morrisett (2006) and its implementation in Haskell by Kiselyov and Shan (2008). A simple embedding of the appropriate restrictions in the Haskell type system, which suffices to implement all the (properly delimited) con
Chapter 3: Delimited Checkpoints
45
tracts presented in this paper, has been constructed. An alternative would be to dynamically check that checkpoints are in scope when used. However, the simple region type discipline suffices for our purposes and avoids the overhead of dynamic checks. Conveniently, type inference obviates the need to write most type annotations.
3.2.2
The Difference of Unordered Checkpoints
While delimitation simplifies the implementation, it does not simplify the semantics. In particular, it is still possible to create snapshots of memory that are not ordered with respect to each other — neither represents an “earlier” view of memory. This is made possible by atCP, which allows a computation to temporarily restore an earlier view of memory and
create a divergent branch of memory. This branch can still access any checkpoints taken by the original branch of memory. Thus, checkpoints cannot be viewed as a totally ordered sequence of memory snapshots, but rather must be viewed as representing a partially ordered set of possible memories. Delimitation serves as a region based mechanism to allow for early reclamation of checkpoints, it does not inherently limit the complexities that can arise from the use of checkpoints. Since checkpoints are not totally ordered, the deltaCP primitives can be called on checkpoints that are not ordered with respect to each other. For example, consider the code in Figure 3.3. After creating an initial (root) checkpoint, it writes to the reference x, and saves a checkpoint (left). It then travels back in time to the root checkpoint, writes the the reference y and saves another checkpoint. Figure 3.4 depicts the checkpoint/memory graph at this point.
Chapter 3: Delimited Checkpoints
46
unordered x y = withCCP (\root → do writeTVar x 1 withCCP (\left → atCP root (do writeTVar y 2 withCCP (\right → deltaWCP left right))))
Figure 3.3: Example that creates unordered checkpoints Finally, deltaWCP is called with the left and right checkpoints. Neither one is an ancestor of the other. One solution to this problem is to simply disallow it. It is possible to restrict the deltaCP primitives, only allowing them to be called on two checkpoints where the for
mer is an ancestor of the latter. This could be enforced statically, using similar ideas to those sketched in Section 3.2.1 for enforcing delimitation. In particular, it is possible to encode a checkpoint ancestor relation in the type system and use it to constrain calls to the deltaCP primitives appropriately.
A more satisfying solution is to extend our deltaCP primitives to work for any two checkpoints. Assume that deltaCP only worked on comparable checkpoints (where one is an ancestor of the other). All sets of checkpoints have a least common ancestor (LCA) (at the worst, this is an implicit checkpoint representing the beginning of the transaction). We can thus define an extended deltaCP primitive as follows: deltaCPext cp1 cp2 = l e t lca = LCA cp1 cp2 in union (deltaCP lca cp1) (deltaCP lca cp2)
Chapter 3: Delimited Checkpoints
47
Figure 3.4: View of memory with unordered checkpoints (created in Figure 3.3) where LCA calculates the least common ancestor of two checkpoints and deltaCP is the simple definition of the deltaCP primitive for linearlyrelated checkpoints (where one is an ancestor of the other). Since a checkpoint and its least common ancestor with another checkpoint are always linearly related, this definition makes sense. Additionally, these extensions are equivalent to the simpler deltaCP for linearlyrelated checkpoints. For any two such checkpoints, the LCA will be one of them. Assuming (without loss of generality) that it is the first one, this yields deltaCPext cp1 cp2 = union (deltaCP cp1 cp1) (deltaCP cp1 cp2)
and deltaCP x x is empty for all the deltaCP primitives. As this definition is a consistent extension of the simpler notion, we take this extended deltaCPext definition as the actual definition of the deltaCP primitives in this thesis.
Chapter 3: Delimited Checkpoints
3.2.3
48
Untyped Delta Sets
As discussed above, the deltaCP primitives all return a list (representing a set) of UntypedTVars. Since they have lost information about their underlying type, most of the TVar operations cannot be used on them.3
As a result, UntypedTVars are just meant to be used for comparison to an existing TVar. Given a TVar, it can be weakened to an UntypedTVar for comparison. If they are
the same, then operations can be performed on the (fully typed) TVar. An alternative is to package additional information along with every UntypedTVar. In particular we could guarantee that the constituent TVar’s type is an instance of a chosen type class. However, this would restrict the types of TVars that could be used within checkpoints to types that were instances of the specified type class. The most natural/general such type class would be Dynamic, which allows for runtime type querying and casting. To do this, we could use: data DynamicTVar = forall a. Dynamic a ⇒ DynamicTVar (TVar a)
As noted though, this would restrict computations run within a checkpoint to only use references to values that can have runtime type information associated with them. We did not need this ability, and so did not impose this restriction. However, there are domains in which this may be a worthwhile tradeoff.
3
Note that there is a clever exception discussed in Section 4.3.1.
Chapter 4 Contracts This chapter demonstrates how to implement a variety of contracts using delimited checkpoints. As examples, we will focus on two systems used for formal reasoning: the Java Modeling Language (JML) (Poll et al. 2006) and Separation Logic (Ishtiaq and O’Hearn 2001). There is existing research into implementing these specification languages, however the implementations and their semantics are adhoc. This chapter demonstrates how delimited checkpoints allow for simple implementations of both systems. Additionally, Chapter 5 provides a formal semantics for delimited checkpoints and uses them to prove that the contract implementations satisfy important properties. To start, we will introduce assertions (Section 4.1) and framing contracts (Section 4.2), both core features of JML. Assertion contracts specify what effect a computation should have, and framing contracts specify what effects a computation may not have. Section 4.3 then takes a bit of a detour, showing how delimited checkpoints can easily implement an interesting twist on framing contracts. Instead of simply checking if a computation attempts to write memory that it does not own, delimited checkpoints enable a 49
Chapter 4: Contracts
50
contract to suppress such accesses. The computation can locally write (and then read) proscribed memory, but its modifications will be suppressed after the computation completes. This ability is particularly useful when applied as a metacontract on assertion contracts. Section 4.4 then presents the final piece of JML that we are modeling: state that is local to contracts. This allows contracts to express temporal properties. A contract can save away information about an event, and a later contract can access that information to determine what happened. This can be used, for example, to enforce the proper usage of a protocol. Of course, the contract local state must be prevented from influencing the behavior of the program to ensure that all contracts remain erasable. The features just discussed are all present in JML, but not necessarily specific to the language. Additionally, JML is designed for Java, which requires many different design decisions than a contract system for Haskell. Section 4.5 presents more information about JML, and connects the contracts described in the preceding sections to the concepts as they exist in JML. Note that the Java Modeling Language models many features that are beyond the scope of this paper: we concern ourselves with features describing memory. This is further discussed in Section 4.5. Finally, Section 4.6 introduces a different language used for specifications, separation logic. Separation logic conveniently combines assertions and framing into a simple, but powerful assertion language. Section 4.6.1 presents an implementation of the core contracts of separation logic in a computational setting. Instead of using first order logic as a foundation, as is commonly done for separation logic specifications, our separation contracts allow arbitrary (possibly sideeffecting) STM Haskell code. In Chapter 5, we present a proof that the key property of separation logic, the frame rule, holds for our implementa
Chapter 4: Contracts
51
tion (Theorem 5.6.1). To understand this chapter, the informal semantics presented in Chapter 3 suffice. Chapter 5 presents a formal operational semantics for STM Haskell extended with delimited checkpoints, precisely specifying the meaning of the delimited checkpoint interface. Using that formalism, Chapter 5 then proves that the contracts implemented in this chapter satisfy the claimed properties.
4.1
Assertions
The simplest and most widelyused type of contract is an assertion. Simple assertions generally take the form assert(e), where e is a computable booleanvalued expression, that halts or throws an exception when e returns false. Many programming languages provide support for simple assertions, which can be turned off to eliminate their overhead. Structured assertions, including preconditions, postconditions, and invariants, are less commonly supported by programming languages. Preconditions and postconditions are common in formal verification, as they make it easy to modularize verification. To check a function call, the verifier needs to ensure that the function’s precondition is implied by the current context. In return, it gets to assume that the function’s postcondition holds upon return. The Design by Contract approach promoted in Eiffel pushes for structured assertions to be used pervasively (Meyer 1997). More recently, .Net 4.0 introduced Code Contracts, which support design by contract style assertions (F¨ahndrich et al. 2010). The Java Modeling Language (JML) has extensive support for these types of contracts, as well as more sophisticated ones that will be discussed in later sections (Poll et al. 2006). JML supports
Chapter 4: Contracts
52
tools for statically verifying these contracts(Chalin 2006; March et al. 2004), as well as for checking them at runtime(Cheon and Leavens 2002; Chalin and Rioux 2008; Sarcar and Cheon 2010). Section 2.1 presented examples of preconditions and postconditions for a bank transfer method written in both Eiffel (Figure 2.1) and JML (Figure 2.2). An important design decision for assertions is the language in which they are written. In formal verification systems, assertions are generally expressed as statements in a logic. This allows for expressive assertions with rigorously understood semantics. However, for a contract system designed for dynamic enforcement, it poses serious problems. Dynamic enforcement can check only computable (or exhaustively checkable) statements. Contract systems therefore generally choose to reuse the host language to write contracts. The contract system may limit the use of problematic host level constructs in an assertion, and may add features (e.g. quantifiers) to ease assertion writing. An additional benefit of this choice is that users need not learn another language (logic), different from the host programming language. Once we allow assertions to be written in the host language, we have to worry about the assertion’s effects. In particular, we want to ensure that assertions are erasable: successfully terminating contracts should have no effect on the program. This justifies the common practice of disabling assertion checking for the release build of a product to improve performance. The Haskell type system makes it simple to statically distinguish “pure” code from code that can have side effects. As discussed in Section 2.2, most side effects are sequestered into monadic computations1 . Thus, an assertion mechanism that takes only pure values Except for nontermination: pure code does not need to terminate. Also, pure code can thrown an exception, although it can only be caught by monadic code 1
Chapter 4: Contracts
53
need not worry about effects (except for nontermination and exceptions, which we ignore for now). In fact, GHC provides such an assert function: assert :: Bool → a → a
This is defined to be equivalent to the following code: assert False _ = error ‘‘assertion failed’’ assert _
x=x
except that that “assertion failed” is rewritten by the compiler to provide helpful source file and line information. Additionally, these assertions are disabled when optimizations are turned on. This simplistic approach suffices for assertions that truly have no sideeffects. Unfortunately, this draconian restriction precludes the assertion validating the contents of mutable memory. Only monadic computations can read mutable memory. As a result, we would like to provide the interface presented in Figure 4.1, which supports monadic assertions. To distinguish from the provided assert method, we will call the monadic assertion function assertM, following a common Haskell convention of appending “M” to the name of monadic operations. Preconditions are provided by requires, invariants by preserves, and postconditions by ensures. Note that the postcondition is provided with both the value returned by the wrapped computation as well as a function that allows it to run any computation with memory in its old state. This allows the postcondition to inspect the state of memory as it was before the wrapped computation ran. While the interface presents separate contracts for preconditions and postconditions, it is common practice to specify both at the same time. This goes back to the formal roots of
Chapter 4: Contracts
type M
54
−− The computation monad
type Assertion −− The type of assertions
assertM :: Assertion → M ()
−− Design By Contract requires :: Assertion → M a → M a preserves:: Assertion → M a → M a ensures :: (a → (forall b. M b → M b) → Assertion) → M a → M a
Figure 4.1: An interface for monadic assertions in Haskell preconditions/postconditions: Hoare logic(Hoare 1969). Figure 4.2 demonstrates how easy it is to compose the requires and ensures contracts to create such a hoare contract.
4.1.1
Implementing with Delimited Checkpoints
Using the delimited checkpoint interface introduced in Section 3, we can implement the assertion contract interface presented in Figure 3.1. We use the same underlying monad as for delimited checkpoints, the STM monad. Our assertion type will just be STM computations which return booleans, encoding the success or failure of the assertion. Figure 4.3 presents the exact type instantiations that we will use. Given these types, Figure 4.4 presents an implementation of the desired interface (Figure 4.1).
Chapter 4: Contracts
55
hoare :: Assertion → (a → (forall b. M b → M b) → Assertion) → M a → M a −− Written using function composition hoare pre post = ensures post . requires pre
−− This can be written more e x p l i c i t l y as −− hoare pre post c = ensures post ( requires pre c )
Figure 4.2: Building a hoare contract
type M = STM type Assertion = STM Bool
Figure 4.3: Types for Delimited Checkpoint Implementation of Assertion Interface
Chapter 4: Contracts
assertM a
56
= do result ← noEffect a unless result signalFailure
requires a comp = do assertM a comp
preserves a comp = do assertM a result ← comp assertM a return result
ensures
a comp = withCCP (\cp → do result ← comp assertM (a result (atCP cp)) return result)
−− helper function : uses GHC’ s assert to trigger an assertion violation signalFailure = assert False undefined
Figure 4.4: Building assertion contracts with delimited checkpoints
Chapter 4: Contracts
57
Figure 4.4 presents assertM, which runs the provided assertion a and then signals an assertion violation using signalFailure if comp returns false. The noEffect helper function, presented in Figure 4.5 and discussed in Section 4.1.2, ensures that assertion does not have any untoward effects. Using assertM, Figure 4.4 then implements structured assertions. Preconditions, implemented by the requires contract, are easy to implement. They just use assertM to check the precondition before continuing to invoke the checked computation. Similarly, invariant contracts, implemented with preserves, check the provided assertion before and after running the checked computation. The ensures contract, which checks postconditions, is complicated by the need to provide the assertion with a way to access memory as it was before the computation runs. To do this, the ensures contract first obtains a checkpoint. It then runs the checked computation, saving the returned value in result. Next, the ensures contract uses assertM to run the requested assertion, passing in the saved result as well as a function that can take any computation and run it with memory as it was before comp ran. This function is often called old and is often built into contract languages. Using delimited checkpoints, it is implemented using atCP and the saved checkpoint.
4.1.2
Detecting Effects: the noEffect Contract
Assertions call the noEffect helper function, defined in Figure 4.5, to guarantee that assertions are erasable. This helper runs a computation while ensuring that it has no problematic side effects. It is essentially a metacontract, ensuring that assertions do not violate their implicit contract — that they have no problematic side effects. For STM computations,
Chapter 4: Contracts
58
noEffect :: STM a → STM a noEffect = noRetry . noExn . noWrite
noExn
:: STM a → STM a
noRetry :: STM a → STM a noWrite :: STM a → STM a noExn
c = catchSTM c (\_ → signalFailure)
noRetry c = c ‘orElse‘ signalFailure noWrite c = do (w, ret) ← deltaWCPd c unless (null w) signalFailure return ret
Figure 4.5: The noEffect helper function for detecting effects in assertions it allows a computation to read from memory, while preventing it from changing memory. Additionally, it prevents the computation from throwing an exception or calling retry, which would unexpectedly change the program’s control flow. The Haskell type system already statically ensures that the computation cannot have any other effects, such as writing to disk. The noEffect function is a simple composition of three functions that detect and prevent problematic effects (Haskell uses the “.” operator for function composition). The noExn and noRetry functions are simple, and use the appropriate handler to detect an
exception or a retry and signaling a contract violation.
Chapter 4: Contracts
59
The noWrite contract uses the functionality provided by the delimited checkpoint interface. Using the deltaWCPd helper (introduced in Section 3.1, Figure 3.2), it runs the given computation and obtains the set of references written by the computation. If this set is nonempty then the computation wrote to memory and a contract violation is signaled. Otherwise, the computation’s return value is propagated.
4.1.3
Benign Writes
The noWrite contract ensures that a computation does not write anything to memory. However, in many cases (particularly sequential cases), it suffices to ensure that memory was not modified. This allows a computation to write to memory, as long as the original values (or equivalent ones) are restored, allowing the computation to use memory for temporary storage. Figure 4.6 presents noModify, which runs a computation and verifies that any modifications it made to memory preserves the specified equality operation. To do this, it first takes an initial checkpoint (start). This will be used to go back in time and determine what value was stored in a reference before the computation ran. It then runs the computation using deltaWCPd, obtaining the set of references written by the computation. For every reference, stillSame determines if the reference has a value equivalent to the one it had beforehand. The monadic map function, mapM, is used to run the stillSame computation over every reference in the list. It returns a (monadic) list of booleans. The standard and function is then used to verify that all the booleans are true: all the writes are accept
able. If so, the computation’s return value is propagated. Otherwise, an assertion violation is signaled.
Chapter 4: Contracts
60
noModify :: (forall a. a → a → STM Bool) → STM a → STM a noModify eq c = withCCP (\start → do (w, ret) ← deltaWCPd c eqs ← mapM (stillSame eq start) w unless (and eqs) signalFailure return ret)
stillSame :: (forall a. a → a → STM Bool) → Checkpoint → UntypedTVar → STM Bool stillSame eq cp (UntypedTVar u) = do current ← readTVar u past ← atCP cp (readTVar u) eq current past
Figure 4.6: The noModify contract The stillSame helper function works by reading the value of the reference from two points of view: the current one and the initial (precomputation) snapshot. These values are then compared with the supplied equality operation. The definition of noModify is parameterized by an equality test. As specified by the type of noModify, this test must work for all values (of all types). For a statically typed language such as Haskell, this is a prohibitively strong definition. For example, note that we cannot use the standard equality operator (==). As discussed in Section 2.2.2, this operator is part of the Eq type class. Using it would result in the type forall a. Eq a ⇒a →a →STM Bool, which would only allow types that have
Chapter 4: Contracts
61
Eq instance declared to be compared. This restriction is not just artificial, but gets to an
important point: not all types have useful userdefined notions of equality. For example, the standard definition of function equality is not computable. In fact, the situation is worse than it seems. Not only cannot we use the standard equality operator, we cannot use any interesting Haskell function. Any normal Haskell function that has the type forall a. a →a →b (here b=STM Bool) must be a constant function that returns the same value of type b, independent of what the inputs are.2 This does not make for a particularly interesting equality function! There are two ways we can deal with this problem. The first is to restrict the STM monad to manipulate only reference types that satisfy a given type class, such as Eq. The definition of UntypedTVar could then be changed to data UntypedTVar = forall a. Eq a ⇒ UntypedTVar (TVar a)}
and the noModify contract could be changed to use the == operation. For domains where the STM is going to be used only with a restricted set of types (which all have computable notions of equality), this may be a reasonable possibility. In fact, this is what languages such as Java do to solve similar problems. In Java, all objects have an equals function defined on them. This is not the assumption, however, in Haskell, and so it is problematic to mandate that all values be comparable. The other possibility is to cheat. In particular, we can provide a primitive operation that checks if two values are actually the very same value (as defined by its location in memory). This primitive is not definable in Haskell, and needs support from the runtime. Also note that it may return false negatives: two values may be the same, for some semantic notion 2
This is a result of parametricity. This type of result is also known as “free theorems” (Wadler 1989).
Chapter 4: Contracts
62
of equality, but actually reside in different locations. In general, the implementation can copy pure values freely, since normal Haskell code cannot observe this copying. Since this primitive breaks that assumption, its semantics are illdefined. Despite these caveats, this (unsafe) primitive allows noModify to support a particular stylized pattern. If a computation uses atCP to read the old (precomputation) value out of a reference and restore the reference to its old value before the computation finishes, then the equality check should confirm that they are indeed the same object, and noModify will allow this modification to proceed, treating it as benign. This allows a computation to locally modify memory, as long as it guarantees that its modifications do not persist. Section 4.3 discusses how to automate this.
4.2
Framing
The previous section introduced the noEffect contract. This contract, as well as its constituent components (noExn, noRetry, and noWrite), restricts a computation’s effects. These contracts are all examples of framing contracts. A framing contract restricts what effects a computation may have. Unlike assertions, which emphasize what properties must be satisfied, framing contracts enable reasoning about what unstated properties are preserved. This is crucial for enabling scalable verification and modular reasoning about code. If a computation circumscribes its allowed effects with a framing contract, any invariants that are independent of those effects are clearly preserved across its execution. For our purposes, we will focus on two classes of effects: control and data effects. Notable control effects include raising/propagating exceptions and nontermination. STM
Chapter 4: Contracts
63
Haskell also adds in retry, which causes control to flow to the closest enclosing orElse handler3 . Data effects include the set of references that have been modified, accessed, or allocated. The most commonly provided framing construct restricts what exceptions can be propagated by a method. For example, Java methods may only propagate checked exceptions derived from classes listed in the method’s throws clause. Note that Java does not restrict the set of unchecked exceptions, essentially treating them as contract violations and not as part of the method’s interface (Gosling et al. 2005). Specification languages inspired by program logics also provide a way to limit data effects. For example, the Java Modeling Language allows specifications to constrain the set of references that may (or may not) be accessed, assigned to, modified, or created by a method. The most commonly used framing specification is a modifies clause, which specifies a set of references that may be modified. The Java Modeling Language (JML) is discussed in more detail in Section 4.5.
4.2.1
Implementing with Delimited Checkpoints
Delimited checkpoints make it possible to implement advanced framing contracts. In fact, Section 4.1.2 already presented examples: the noEffect contract and its constituent helpers. These are used to ensure that assertions do not have any effects, and are therefore safely erasable. In addition to those draconian contracts, we can implement more general framing contracts that allow for finer grained control over the allowed effects. The previous contracts 3
The retry and orElse where discussed in Section 2.3.1
Chapter 4: Contracts
64
can then be seen as special cases of these more general contracts. Starting with the framing contracts for control flow, we will keep the noRetry contract as is: since the retry command does not carry additional information, there is nothing further on which to discriminate. The noExn contract can be generalized to allow only a given set of exceptions. This can be further generalized to discriminate based on the value of the exception. This extension is straightforward, using catch handlers to selectively catch only certain exceptions. To support data framing contracts, we generalize the noWrite contract, built with delimited checkpoints. The resulting writesOnly contract, shown in Figure 4.7, differs from noWrite only in its inputs and its final test. The function now takes the list of references
that may be modified. Rather than ensuring that the list of written references (obtained with deltaWCPd) is empty, writesOnly ensures that everything in it is also in okList. This is done using the Haskell list difference operator (\\): if the difference is equal to the empty list, then its first argument is a subset of its second argument. writesOnly :: [UntypedTVar] → STM a → STM a writesOnly okList c = do (w, ret) ← deltaWCPd c unless (null (w \\okList)) signalFailure return ret
Figure 4.7: The writesOnly contract for limiting effects
The noWrite helper defined in Figure 4.5 can then be written as a special case of writesOnly:
Chapter 4: Contracts
65
noWrite = writesOnly []
As discussed in Section 4.1.3, it is possible to define a variant of noWrite, called noModify, which allows a computation to write to memory as long as it leaves all ref
erences with a value equal to their original (precomputation) value. Figure 4.8 presents modifiesOnly, which generalizes noModify in the same way that writesOnly general
izes noWrite. The only difference is in the choice of list that is tested: as for writesOnly, the list of acceptable references is removed from the list of written variables. This reduced list is then tested to ensure that all the remaining references have the same value as at the beginning of the computation. This proceeds exactly as in Figure 4.7, which also defines the stillSame helper function. modifiesOnly :: (forall a. a → a → STM Bool) → [UntypedTVar] → STM a → STM a modifiesOnly eq okList c = withCCP (\start → do (w, ret) ← deltaWCPd c eqs ← mapM (stillSame eq start) (w \\okList) unless (and eqs) signalFailure return ret)
Figure 4.8: The modifiesOnly contract for limiting effects
Chapter 4: Contracts
4.3
66
Suppression
As presented in Section 4.2, framing contracts limit a computation’s effects, detecting any violations of these limits. In particular, a writesOnly contract signals an assertion violation if the computation writes to any references not in the allowed list. Instead of simply detecting and reporting such a violation, a contract could instead attempt to suppress the proscribed effect, effectively repairing the code. Section 4.3.1 discusses how to do this for all the framing contracts introduced in Section 4.2, using delimited checkpoints to suppress unwanted effects. This suppression mechanism is particularly useful in conjunction with the noEffect helper used to ensure that contracts are erasable. Rather than check if a contract is “pure”, we can make a contract “pure” — even if it modifies memory. This allows contracts to locally exhibit effects, without affecting their erasability.
4.3.1
Implementing with Delimited Checkpoints
We can implement suppression versions of all the framing contracts implemented in Sections 4.1.2 and 4.2.1. The noExn (and its generalization, discussed in Section 4.2.1) and noRetry contracts use catchSTM and orElse to detect a proscribed control effect. When such an effect is detected, they use signalFailure to signal a contract violation. The suppression variants of these contracts, suppressExn and suppressRetry, instead trigger a default action, returning a default value. This value is specified as part of the contract. The data framing contract writesOnly is more interesting. The writesOnly contract uses the deltaWCPd operation to compare memory before and after a computation. If the
Chapter 4: Contracts
67
suppressWritesOnly :: [UntypedTVar] → STM a → STM a suppressWritesOnly okList c = withCCP (\start → do (w, ret) ← deltaWCPd c mapM_ (restoreTVar start) (d \\okList) return ret)
restoreTVar :: Checkpoint → UntypedTVar → STM () restoreTVar cp (UntypedTVar u) = do val ← atCP cp (readTVar u) writeTVar u val
Figure 4.9: Suppressing writes with delimited checkpoints computation has written to any disallowed references, signalFailure is called to signal a contract violation. The suppressWritesOnly contract presented in Figure 4.9 detects proscribed modifications in the same manner. However, instead of signaling an error, it uses the restoreTVar helper to restore all such references to their precomputation’s values. The mapM_ function is used to apply the restoreTVar helper to all the appropriate references. The restoreTVar helper merits scrutiny. In order to revert an UntypedTVar, it reads the old value with atCP and writes it out in the current state. Even though the reference is untyped, restoreTVar is still typesafe and accepted by GHC. Destructuring the UntypedTVar unpacks the existential, yielding a TVar a, where a is a fresh type variable.
Chapter 4: Contracts
68
A value of type a is then returned from readTVar, and writeTVar can write that value back into the TVar a since it is the same type variable. The suppressWritesOnly contract just presented ensures that only the declared references are modified by a computation. It does not, however, prevent other references from being written — it just ensures that they are not modified. In particular, a computation run by suppressWritesOnly will not necessarily pass the noWrite contract. If suppressWritesOnly needs to suppress an illicit write by reverting the reference’s value, the noWrite contract will still detect that the reference was written. However, the more permissive noModify contract introduced in Section 4.1.3 is compatible with the suppressWritesOnly. In particular, as long as the equality function used is reflexive,
then the composition noModifiesOnly eq l (suppressWritesOnly l c) should never signal a contract violation because of the noModifies contract. As noted earlier, a particularly important use of suppression is for the noWrite helper used to ensure that assertions are erasable. As noted in Section 4.2.1, we can define noModify as a simple special case of modifiesOnly where the allowed list of references
is empty. Similarly, we can define a suppression version of noWrite as suppressWrite = suppressWritesOnly []
for this particular special case of suppressWritesOnly there is an alternative, simpler implementation. When the atCP delimited checkpoint primitive returns, it restores memory to its previous state. As will be explicated in the formal semantics (Section 5.2), this is done by suppressing the original write. This suppression is done at a lower level than the manual suppression of Figure 4.9: even deltaWCPd cannot observe the sup
Chapter 4: Contracts
69
suppressWrite c = withCCP (\start → atCP start c)
Figure 4.10: Suppressing all writes with delimited checkpoints pressed writes. This leads to the alternate implementation of suppressWrite given in Figure 4.10. This implementation obtains a checkpoint representing the current state of memory, and then runs the computation at the current checkpoint. When the atCP returns, the computation’s writes will be suppressed. This version of suppressWrite has the advantage that the resulting computation will not only pass noModify, but will even pass noWrite. The atCP mechanism removes all traces of the computation’s writes. However, this version
does not scale to more expressive contracts such as suppressWritesOnly, which need to selectively suppress writes. For that, the manual suppression of Figure 4.9 is needed. Either of these implementations can then be composed with the suppressExn and suppressRetry contracts (each taking a default action to perform if one of those effects
is detected), resulting in a suppressEffect contract. suppressEffect def = suppressRetry def . suppressExn def . suppressWrite
However, to use suppressEffect the caller needs to supply a default action, def, to be used in case of a raised exception or a propagated retry. A simple default is to signal a contract violation: this is equivalent to simply using the noExn and noRetry variants, yielding suppressEffect’ = noRetry . noExn . suppressWrite
We can then define a variant of assertM that uses this contract:
Chapter 4: Contracts
assertM’ c
70
= do r ← suppressEffect’ c unless r signalFailure
Unlike the original assertM presented in Figure 4.4 that detects proscribed writes, this assertM’ variant suppresses such writes.
Note that if the version of suppressWrite in Figure 4.10 is used, then the assertM contract in assertM (assertM’ c) will never signal a contract violation. The assertM’ variant can in fact be taken as the default version of assertM. This provides a nice tradeoff between erasability and usability: code with sideeffects may be used in a contract, but those sideeffects are not permitted to leak out and affect the program. Exceptional controlflow is still considered to be a contractual violation.
4.4
IntraContract Local State: Ghost State
The assertion contracts presented in Section 4.1 do not allow assertions to modify memory. This restriction can be weakened: Section 4.3 presents an alternative that allows assertions to locally modify memory while ensuring that any such modifications do not persist. This ensures that eliding successful assertions preserves the meaning of a program. However, these restrictions still prevent us from writing certain useful contracts. In particular, to enforce temporal properties, contracts need to retain state across calls. For example, consider a contract for a file handle. To enforce its proper use, the contract needs to track if the file has been closed. This is commonly specified as an automata: the file open contract sets the current state to the opened state. The contracts on read and write operations first verify that the file is currently in the opened state, and the contract for close
Chapter 4: Contracts
71
changes the file’s state to closed. Any further read and write operations will produce a contract violation. More generally, allowing data to persist across contracts allows contracts to persist a (relevant) subset of the program’s execution history. This suffices to encode temporal contracts. While it is useful to allow data to leak out from a contract, it is important to control this leakage. In particular, to ensure erasability, the contract system needs to guarantee that the data is read only by another contract. The data must be prevented from influencing the program. This can be viewed as a standard information flow problem. Assertions are considered secret and the rest of the program public. Our desired erasure property is then just a typical noninterference result (Goguen and Meseguer 1982). In theory, this can be further refined. Different assertion groups can have different security levels (as long as all of them are more private than the program’s security level). This can be used to restrict which other assertions an assertion can influence. However, in practice, it is common to just distinguish control state and program state. This is the approach taken by existing contract languages such as the Java Modeling Language. Contracts may not modify normal state. Special ghost state (another name for contract local state) can be updated in a contract (with a special type of assignment statement), but may not be accessed by the program.
4.4.1
Implementing with Delimited Checkpoints
Allowing modifications to leak from assertions is easy. The assertM function (Figure 4.3) prevents such leakage, by using noEffect, which either uses noWrite (Fig
Chapter 4: Contracts
72
ure 4.5) or suppressWrite (Figure 4.10) to prevent modifications from leaking out. Changing assertM to use writesOnly (Figure 4.7) or suppressWritesOnly (Figure 4.9) with an appropriate list enables allow contract local state to persist. This simple solution, however, does not enforce erasability. Once information is leaked, its use is not controlled: the program can read from a modified reference, allowing the contract to influence the program.
4.4.2
Enforcing Noninterference
To separate out contract local state from normal STM state, we will take the same approach used to separate STM state from normal nontransactional state: we will create a new type of reference and a new monad that can manipulate it. Figure 4.11 presents an interface that restricts the delimited checkpoint operators to a new Contract monad. Any STM computation can be lifted into this monad with liftSTM. To run a Contract action (resulting in an STM action), assertM must be called, guaranteeing erasability. The assertM helper is defined as before (either the version of assertM from Section 4.1.2 or the suppression version from Section 4.3.1). The new Contract monad provides the same composition methods as the STM monad: the monadic bind and variants of the orElse and catchSTM functions. Additionally, the Contract interface provides a new type of reference, the GVar (ghost variable), with
allocate, read, and write functions. This new reference type and its attendant operations are presented in Figure 4.12. The type system ensure that this type of reference can never be accessed outside of a contract. This interface is mostly a straightforward adaption of the delimited checkpoint interface
Chapter 4: Contracts
data Contract instance Monad Contract
liftSTM :: STM a → Contract a assertM :: Contract Bool → STM ()
−− Standard STM Haskell primitives orElseC :: Contract a → Contract a → Contract a catchC :: Contract a → (Exception → Contract a) → Contract a
−−−− Delimited Checkpoint primitives data Checkpoint instance Eq Checkpoint withCCP
:: (Checkpoint → Contract a) → Contract a
atCP
:: Checkpoint → Contract a → Contract a
deltaWCP :: Checkpoint → Checkpoint → Contract [UntypedTVar] deltaRCP :: Checkpoint → Checkpoint → Contract [UntypedTVar] deltaACP :: Checkpoint → Checkpoint → Contract [UntypedTVar]
−− Untyped version of (TVar a) data UntypedTVar = forall a. UntypedTVar (TVar a) instance Eq UntypedTVar
Figure 4.11: The Contract Monad: Restricting Access to Delimited Checkpoints
73
Chapter 4: Contracts
74
data GVar a instance Eq (GVar a)
newGVar
:: a → STM (GVar a)
readGVar
:: GVar a → STM a
writeGVar :: GVar a → a → STM ()
Figure 4.12: The Contract Monad: Supporting Ghost State (and parts of the STM interface) and is easy to implement on top of the STM monad. The assertM function can be defined in terms of the other functions, as described in Section 4.1.1. With appropriate runtime support, the new ghost state could be implemented via a new type. Instead, we present an alternative that does not rely on runtime support. Ghost variables are built on top of transactional variables by maintaining a list of ghost variables (technically, untyped ghost variables, where UntypedGVar is defined like UntypedTVar, but for GVars instead of TVars). The newGVar adds new ghost variable to this list. A modified assertM function uses this list with the generalized framing contract from Section 4.2, explicitly allowing references in the ghost variable list to be modified.
4.5
The Java Modeling Language
The Java Modeling Language (JML) is a behavioral specification language for Java(Poll et al. 2006). It is specified using specially formatted comments in Java code. Many tools
Chapter 4: Contracts
75
have been written for it, including runtime verification checkers and static analysis tools. JML provides a well developed infrastructure for specifying contracts for a mainstream language, providing for an interesting case study. Note that this section is not intended as a comprehensive guide to the Java Modeling Language. As a rich specification language designed for an object oriented language, JML provides many features that are beyond the scope of this thesis. This section discusses relevant parts of the JML specification and how they might look in the setting of STM Haskell. In particular, it looks at those specifications that deal with the state of memory. The important ideas have already been developed in previous sections, however this chapter makes the connection to JML explicit. We start with JML method annotations. JML supports two styles of method annotations: lightweight and heavyweight specifications. The only difference is in the default values assumed for absent specification clauses. Multiple method specification cases can be combined with the also keyword. The specification cases are treated as implications: if a case’s precondition holds, then its other clauses must also hold. To model this in Haskell, we note that all of our “method” contracts are computation wrappers: they take in an arguments and return a STM a →STM a computation wrapper. These can be composed with the standard function composition (dot) operator. A specification case can be modeled as a pair of an assertion representing the precondition and a contract wrapper representing the rest of the case (with the various clauses composed together). The also combinator takes in a list of these pairs and returns a contract wrapper that picks out all the pairs where the precondition holds. The associated contract cases are then composed together to get one composite contract wrapper representing the complete
Chapter 4: Contracts
76
method specification. Turning to the clauses that make up a specification case, JML supports preconditions ( requires or pre) and postconditions ( ensures or post ), as well as exceptional postconditions (signals). Implementations of the former two were presented in Section 4.1, and supporting exceptional postconditions is also simple: a catchSTM handler is used to catch an exception and verify the appropriate condition. JML also supports framing contracts, including modifies (also called modifiable and assignable ) to restrict assignments and accessible to restrict reads. Implementations of
these are discussed in Section 4.2. JML also provides a signals_only clause that restricts what exceptions can be thrown by a method. This can be desugared into an exceptional postcondition with an explicit type check. This thesis focuses on staterelated contracts for a functional language. JML supports other contracts that are beyond the scope of this thesis. For example, JML supports captures and callable specifications that limit what locations can be stored in the object
or called by a function. These do not make as much sense in a functional setting, such as Haskell. There is no implicit this object and functions are values, and do not need to have a name. Others, such as diverges and measured_by, which stipulates a termination metric, are beyond the scope of this thesis. Next, we turn to JML annotations for predicates and specification expressions. These expressions are all prefixed with a backslash to avoid collisions with existing variable names, but these are omitted here. JML supports result and old (also called pre) expressions in postconditions. We model these as extra parameters to the postcondition, which can be named. The implementation of the old parameter is discussed in Section 4.1.
Chapter 4: Contracts
77
JML also supports frame conditions as expressions that can be used in a postcondition. These include not_modified, not_assigned, only_accessed, only_assigned, and fresh. These can all be supported using the difference primitives. In particular, a post
condition can be given the checkpoint representing the precomputation state, and can use it to both implement old as well as these frame expressions. JML also supports a reach expression that obtains the set of locations accessible from a given object by following object fields. Supporting this operation requires integration with the garbage collector layout information and is beyond the scope of this work. In order to enable the modular specification of frame properties, JML introduces the idea of a data group. This is a set of locations which can be used in a frame condition. These can be modeled in our setting as simple lists. JML also supports temporal properties via ghost state. Fields can be labeled with the ghost keyword, and are then inaccessible from the normal program. Only specification
annotations may read and write to ghost fields. Section 4.4 discusses how to provide ghost state in our setting.
4.6
Separation Contracts
Separation Logic provides a modular way to reason about heap manipulating programs. Building on Hoare logic’s preconditions and postconditions, it adds a form of implicit access control which restricts what parts of the heap a computation can access. This is a more implicit alternative to the framing contracts discussed in Section 4.2. In traditional hoare logic, if a computation does not have a modifies clause, then any property not explicitly mentioned in the postcondition cannot be assumed preserved. In contrast, the frame condi
Chapter 4: Contracts
78
tion implicit in separation logic ensures that properties unrelated to those mentioned in the precondition are preserved. Traditional separation logic assertions are built out of the “pointsto” relation, written (r � v). As a precondition on a computation, r � v simultaneously asserts that the reference r must have the value v when the computation is invoked and grants the computation permission to access only the reference r. Using framing, this would be written as a standard precondition (in Hoare logic) which checks that r’s value in the heap is v, along with an accessible clause stating that r (and only r) may be accessed (written or read) by the computation. To combine multiple assertions, separation logic provides the separating conjunction, written p� q. Like the pointsto relation, it combines assertionchecking with access control enforcement. The assertion p � q checks that 1. Both p and q hold. 2. The p and q assertions in fact hold for disjoint subheaps. In other words, all the pointsto relations asserted by p must be disjoint from those asserted by q. So separating conjunction acts like conjunction for the assertion checking mechanism and like disjoint union for the access control policy. For treelike data structures in particular, separation logic is convenient. Specifying properties of a data structure typically requires traversing the data structure. Crafting an appropriate modifies clause requires a similar traversal. Separation logic conveniently combines these into a single specification. Separation logic also provides great support for dealing with freshness: when a new variable is allocated it is guaranteed to not alias anything else. This is critical for reasoning
Chapter 4: Contracts
79
about unrelated invariants: it is important to know that modifying the new reference does not affect other parts of the data structure. In JML, this can be handled with a separate freshness contract, but the proliferation of contracts is awkward. In separation logic, freshness is specified by guaranteeing that the new reference is “star” (disjoint) with everything else. Separation logic assertions, built with the help of the pointsto and separation conjunction relations, are used as preconditions and postconditions, much as in Hoare logic. However, in addition to checking if the assertions hold before and after the computation, they are also used to enforce the appropriate access control restrictions. In order to read from a reference, that reference must be mentioned in the precondition (using pointsto). As an example, the read operation (on a reference) typically has a precondition stating that the reference pointsto something, and a postcondition stipulating that the return value is the same as what the reference pointsto in the heap. Since separation logic assertions bake in access control, they allow for modular reasoning. In particular, they obey a fundamental reasoning principle called the frame rule. Shown in Figure 4.13, the frame rule allows a computation to run in a larger environment. In particular, any computation (M ) that satisfies precondition P and postcondition Q can continue to run in a larger context, where R also holds, as long as R is disjoint from P and Q. Furthermore, running the computation is guaranteed to preserve R: it continues to hold in the postcondition. The intuition behind the frame rule is that since R is disjoint from P , and the computation only has permission to access locations mentioned in P , the computation cannot access locations in R. Therefore, it is completely independent of R: it is neither affected nor affects the parts of the heap that R describes.
Chapter 4: Contracts
80
{P }M {Q}
(FRAME )
{P ∗ R}M {Q ∗ R} Figure 4.13: Separation Logic: The Frame Rule The frame rule allows for a convenient separation of concerns: computations need only worry about the parts of the heap they care about. As an example, it allows separation logic to handle fresh allocations in a simple manner. The allocation operation specifies as its precondition the empty heap and as its postcondition, a simple pointsto assertion for the newly allocated reference. Since the empty heap is disjoint from all other heaps, the frame rule allows allocation to happen in any context. Furthermore, it guarantees that the returned reference will be disjoint from any such larger context. Separation logic not only handles allocation naturally, it can also address deallocation, enforcing the proper tracking and cleanup of resources. Postconditions can be forced to refer to the same sets of locations that the precondition referred to, modulo any allocations and deallocations performed by the computation. This allows deallocation to have a simple specification, symmetric to allocation: its precondition consists of a single pointsto assertion about the reference, and its postcondition is the empty heap. If pointsto assertions are viewed as access control capabilities, allocation creates such a capability and deallocation destroys it. Note that the pervasive use of the separating conjunction prevents this capability from being duplicated. It is not necessary to require the postcondition to “remember” everything from the precondition (modulo allocations and deallocations). Technically, a system that requires this
Chapter 4: Contracts
81
is known as classical separation logic. It is useful for enforcing proper resource accounting. It is also possible to allow postconditions to “forget” some information, leading to intuitionistic separation logic.4 This variant is a natural fit for garbage collected resources, which are not explicitly deallocated. Separation logics also generally provide another operator, the “magic wand”. Written p −∗ q, it means that the current heap, if extended with any disjoint heap that satisfied p, will then satisfy q. It is a special form of implication: in the current heap, if p holds then q holds. Like implication in general, this operator is noncomputational: it quantifies over all possible heaps that satisfy p. As the focus of this thesis is on computable contracts, supporting this operator is problematic. It might be possible to model implication contracts, as well as the magic wand operator, by treating them as contract functions: callers would then need to instantiate the contract with a concrete footprint. This, however would add additional complication to the contract language. In a computational or higher order context, however, the magic wand is not as important. Rather than use the magic wand to describe data structures, it is possible to explicitly traverse them and compute the necessary pointsto assertions. As anecdotal evidence, in the author’s experience verifying data structures and programs in Ynot, which embeds separation logic on top of a higher order logic, there was never a need to use the magic wand operation.(Chlipala et al. 2009; Malecha et al. 2010) Computation was always simpler and easier to reason about than the noncomputational magic wand. 4
These names arise from the nature of their underlying logics.
Chapter 4: Contracts
4.6.1
82
Implementing with Delimited Checkpoints
The delimited checkpoint operations suffice to encode a computational variant of the ideas behind separation logic. Separation logic is traditionally defined over first order (or in some cases higher order) logic, forcing programmers to learn another language. Instead, we continue to build assertions as computations. Rather than restrict specifications to using pointsto expressions (r �→ v) to describe the heap, they can use normal read and write commands. The standard pointsto operation can be defined by simply reading from a reference and checking if the value is as expected. This, of course, requires that equality is defined for the value. In traditional separation logic, more complicated predicates are built using an existential quantifier to name the value. For example, ∃v, r �→ v asserts the existence of a valid reference, implicitly acquiring access to the reference. More generally, the expression ∃v, r �→ v ∧ p v is used to assert that r pointsto some value which satisfies predicate p. As our assertion language is computational, it does not support existentials. Instead, Figure 4.14 presents a simple generalization of pointsto, which takes an arbitrary (pure) predicate to check the value against. The → operator5 reads the requested references and checks if the value it pointsto satisfies the given predicate. It also provides the exactly and something values for use with the two common patterns just discussed.
The expression x → exactly v verifies that x pointsto the value v (up to equality), and x → something verifies that x points to something, without imposing any constraints on
its value. Note that the compiler must be prevented from optimizing away otherwise irrelevant 5
Recall from Section 2.2.1 that Haskell allows the declaration of infix operators.
Chapter 4: Contracts
83
x  → p = do v’ ← readTVar x unless (p v’) signalFailure
exactly x v = x == v something = const True
Figure 4.14: Implementing Separation Logic: pointsto reads (arising from x → something, for example). If the compiler is not aware of the interaction of read and deltaRCP, it may erroneously optimize away the read as dead code. Since separation logic uses the footprint of the specification to describe the footprint of a computation, we need a way to calculate both footprints. The footprint of a computation is the set of existing locations that the computation accesses. Since a specification in our language is just a computation, this definition can be reused for specifications. Figure 4.15 presents a function modeled after the deltaCPd primitives (Section 3.1) that determines the footprint of a computation. It does this by combining the sets of references that were read and written, and then filtering out those that were newly allocated. deltaFCPd :: STM a → STM ([UntypedTVar], a) deltaFCPd c = do (w, (r, (a, res))) ← deltaWCPd (deltaRCPd (deltaACPd c)) return (((r ++ w) \\ a), res)
Figure 4.15: Implementing Separation Logic: deltaFootprintCheckPoint Using this footprint function, Figure 4.16 presents an implementation of separation
Chapter 4: Contracts
84
logic’s separating conjunction. Since p * q is already used by Haskell for numeric multiplication, we instead use p # q to denote the separating conjunction. The p # q contract asserts that both the p assertion and the q assertion hold. Furthermore, it asserts that their footprint is disjoint. This is encoded in a straightforward fashion using the new delimited footprint operator just introduced in Figure 4.15. (#) :: Assertion → Assertion → Assertion p # q = do (f1, b1) ← deltaFCPd p (f2, b2) ← deltaFCPd q return (b1 && b2 && (null (f1 ‘intersect‘ f2)))
Figure 4.16: Implementing Separation Logic: Separating Conjunction ( P * Q)
After running both assertions and obtaining their footprints, the separating conjunction ensures that both assertions returned true and that their footprints are disjoint. The standard list intersect function is used to verify that there is no overlap between the footprints of the two assertions. As discussed earlier, the other operator of separation logic, the “magic wand” operator is noncomputational, since it quantifies over all heaps that satisfy an assertion. However, as discussed, it is generally possible to encode assertions in a more computational format. Now that we have encoded the computational operations of separation logic using delimited checkpoints, the only thing remaining is the separation logic contract itself. Figure 4.17 presents an implementation of this contract, encoded as the sep combinator. It takes in a precondition P and postcondition Q and verifies that a given computation c holds under those conditions, where the precondition and postcondition are interpreted as
Chapter 4: Contracts
85
in separation logic. The sep contract must enforce both components of separation logic: it must verify that the precondition and postcondition hold (as in Hoare logic), and it must verify the necessary conditions on the footprints of the computation and assertions. sep :: Assertion → (r → (forall b. M b → M b) → Assertion) → STM r → STM r sep p q c = withCCP ( \start → do −− v e r i f y the precondition (footp, _) ← deltaFCPd (assertM p) −− run the computation (footc, (ac, res)) ← deltaFCPd (deltaACPd c) −− ensure that the computation ’ s fo o tp r int i s contained by the −−
precondition ’ s f oo tp r in t
unless (null (footc\\footp)) signalFailure −− v e r i f y the postcondition (footq, _) ← deltaFCPd (assertM (q res (atCP start))) −− ensure that the postcondition ’ s fo o tp r int i s contained by −−
the precondition ’ s fo o tp r in t plus the new s t u f f
unless (null (footq\\ (footp ++ ac))) signalFailure return res)
Figure 4.17: Implementing Separation Logic Contract with Delimited Checkpoints
The sep combinator first saves a checkpoint (start) representing the initial state. It
Chapter 4: Contracts
86
then verifies the precondition, using assertM to ensure that the assertion has no effects.6 . It also uses the delimited footprint function to get the precondition’s footprint. Next, the actual computation is run and its result bound to res, with the appropriate delimited difference functions capturing both its footprint and allocation set. The sep contract next checks the first footprint condition: the computation’s footprint must be contained within the precondition’s footprint. For the computation to access an existing location, it must have been “claimed” by the precondition. Note that the computation can freely access newly allocated locations. After this check, the sep contract verifies the postcondition. As with the precondition, assertM is used to ensure that it does not affect memory, and deltaFCPd is used to
capture the postcondition’s footprint. The postcondition is given the return result of the computation (stashed away in r) as well as an function that can run any computation in the “old” (precomputation execution) state of memory. This function is created using atCP and the initial (start) checkpoint. Finally, the sep contract checks the second footprint condition. The postcondition is allowed to access only those parts of the heap that were either accessed by the precondition or freshly allocated by the computation. If this check passes, then the sep contract passes, and the result of the computation is returned. As discussed earlier, a defining feature of separation logic is the frame rule. This section’s adaptation of separation logic to our computational setting does indeed satisfy this important reasoning principle. This is formalized and proven in Section 5.6. Either the the detection version of assertM from Section 4.1.2 or the suppression version from Section 4.3.1 can be used throughout this section. As will be clarified by the formal semantics presented in Section 5.2, the suppression version preserves read effects. 6
Chapter 4: Contracts
87
The sep contract follows the idea of intuitionistic separation logic (mentioned in Section 4.6), in that it allows postconditions to “forget” resources. This is a good match for Haskell, which garbage collects memory (including TVars). It is possible to determine what references are being “forgotten”. The list of references claimed by the precondition or allocated by the computation and not referenced by the postcondition is ((f ootp + +ac) f ootq). To implement something close to classical separation logic, everything in this list needs to be checked to ensure that it is unreachable. One way to do this is to hook into the garbage collector. The work by Aftandilian and Guyer (2009) on GC assertions could be used to add an unreachability assertion to the reference.
Chapter 5 Formal Semantics This chapter develops a formal semantics for delimited checkpoints based on the operational semantics for STM Haskell as given by Harris et al. (2005).1 We call this system DC Haskell. This semantics precisely defines the interaction among the various features, including delimited checkpoints, exceptions, and retry. After showing that the semantics for DC Haskell give the same results as the original STM Haskell semantics for programs that do not use the new features (Theorem 5.3.1), we prove some important properties about our derived contracts. In particular, the contracts from Chapter 4 are proven to be erasable, and the separation contract sep introduced in Section 4.6.1 is shown to satisfy a fundamental reasoning principle of separation logic: the frame rule. Throughout this section, theorems and lemmas are asserted. The more complicated theorems are generally accompanied by some intuition as to why the property should hold, but these fall short of a complete proof. To remedy this, Appendix A, which follows the same structure as this chapter, restates all of this chapter’s lemmas and theorems along with 1
As revised in their postpublication (August 18, 2006) version’s appendix.
88
Chapter 5: Formal Semantics
89
more detailed proofs. Before delving into the formal semantics of our system, we pause to note syntax of our languages (Figures 5.1 and 5.5) and how it relates to the Haskell we have seen so far. Values and terms are typical, except that the monadic combinators are treated as values. The do notation (first introduced in Section 2.2.3)is not directly supported, however it can be desugared into uses of return and >>= (pronounced bind), as in Haskell (Peyton Jones et al. 2003) (these functions where introduced in Section 2.2.3): do{x ← e; Q} ≡ e >>= \x → do{Q} do{e; Q} ≡ e >>= \
→ do{Q}
do{e} ≡ e
Since we do not want to deal with the declaration of data structures, the syntax also supports a predefined set of constructors (as does (Harris and PeytonJones 2006)). We assume that the Nil and Cons list constructors are part of that set, but otherwise leave it unspecified (as is done in the original paper).
5.1
Semantics of STM Haskell
Figure 5.4 presents the operational semantics for STM Haskell extended to support delimited checkpoints. It makes use of various auxiliary structures as defined in Figure 5.2. Heaps (Θ) and Allocations (∆) are finite partial maps from names (r) to terms (M ). The evaluation context (E) controls the evaluation order for expressions. The semantics differentiate between I/O and STM transitions. These are both augmented by a set of administrative reductions that define how >>= interacts with return,
Chapter 5: Formal Semantics
Value
Term
90
x, y
∈
Variable
r, t
∈
Name
c
∈
Char
C
∈
Constructor
d
∈
Checkpoint Name
V
::= r

c

\x → M

C M1 · · · Mn

return M
 M >>= N

putChar c
 getChar

throw M
 catch M N

retry
 M ‘orElse‘ N

forkIO M
 atomic M

newTVar
 readTVar r

writeTVar r M
M, N ::= x

V

MN

···
Figure 5.1: STM Haskell: The syntax of values and terms
Chapter 5: Formal Semantics
91
throw, and retry. Additionally, the EVAL transition evaluates pure expressions, aided by
a standard denotation function V. I/O transitions take a set of threads and a heap and return the same, possibly taking a read or write action. The transitions all pick (nondeterministically) a single thread from the thread pool and take a single step. The putChar and getChar primitives do not change the heap, but cause a read/write action as appropriate. The rules for catch are standard. Administrative transitions are supported and do not affect the heap. The forkIO primitive adds a new thread to the thread pool and returns its (fresh) thread identifier. The last two I/O transitions, ARET and ATHROW, formalize atomic transactions. Transactions are run to completion via the transitive closure of the STM transition, culminating in either a normal return or an exceptional return. This is treated as a single I/O transition, enforcing the atomicity of transactions. In the case of an exceptional return, the modifications made to the heap by the transaction are not preserved. One edge case must be dealt with: the value of the exception may leak a reference that was allocated in the transaction. When the heap is reverted, it no longer contains a value for that reference. This would cause a later transactional readTVar or writeTVar to get stuck, since the reference is not in the domain of the heap. While this is not a fundamental problem for a formal semantics, it complicates implementations. To address this, transactions also track allocation effects. When terminated by an exception (the ATHROW transition), the initial heap is extended by these allocation effects. Upon normal return (the ARET transition), however, the allocation effects are silently discarded. STM transitions give meaning to transactional constructs. In addition to the standard expression and heap, it also tracks allocation effects. The operation of readTVar,
Chapter 5: Formal Semantics
92
writeTVar, and newTVar is standard, except that newTVar also records an allocation
effect. As with I/O transitions, administrative transitions are lifted into STM transitions, defining the meaning of simple constructs. Exception handling code using catch is treated specially in a transaction. If an exception is thrown, the state is rolled back before the exception handler is called. Since the exception may contain a locally allocated reference, allocation effects are preserved. The orElse command runs the first alternative and, barring a retry, propagates its result or exception. A retry discards the first alternative’s effects and replaces the entire orElse with the second alternative. The allocation effects do not have to be preserved
since retry cannot leak any newly allocated references.
Chapter 5: Formal Semantics
93
Thread soup P, Q ::= Mt  (P Q) Heap
Θ ::= r �→ M
Allocations
∆ ::= r �→ M
IO Evaluation STM Evaluation contexts Action
E ::= [·]  E >>= M  catch E M S ::= [·]  E >>= M P ::= Et  (P  P )  (P  P) a ::= ! c  ? c  �
Figure 5.2: STM Haskell: The program state and evaluation contexts
Chapter 5: Formal Semantics
94
a
I/O transitions
P;Θ → − Q; Θ�
P[putChar c]; Θ − → P[return ()]; Θ
!c
(PUTC )
?c
(GETC )
P[getChar c]; Θ −→ P[return c]; Θ �
P[return M ]; Θ (CATCH1 )
�
P[N P ]; Θ
P[catch (return M ) N ]; Θ
→ −
P[catch (throw P ) N ]; Θ
→ −
M →N
(CATCH2 )
(ADMIN )
�
P[M ]; Θ → − P[N ]; Θ t �∈ P, Θ, M
(FORK )
�
P[forkIO M ]; Θ → − (P[return t]  Mt ); Θ ∗
M ; Θ, {} ⇒ return N ; Θ� , ∆�
(ARET )
�
P[atomic M ]; Θ → − P[return N ]; Θ
�
∗
M ; Θ, {} ⇒ throw N ; Θ� , ∆� �
(ATHROW )
P[atomic M ]; Θ → − P[throw N ]; Θ ∪ ∆
�
Administrative transitions M → N M →V
if V[M ] = V and M ∼ �= V
return N >>= M
→ MN
(EVAL) (BIND)
throw N >>= M → throw N
(THROW )
retry >>= M → retry
(RETRY )
Figure 5.3: Operational semantics of STM Haskell: I/O and administrative transitions
Chapter 5: Formal Semantics
95
STM transitions M ; Θ, ∆ ⇒ N ; Θ� , ∆� M→ N (AADMIN ) S[M ]; Θ, ∆ ⇒ S[N ]; Θ, ∆ r ∈ dom (Θ) (READ) S[readTVar r]; Θ, ∆ ⇒ S[return Θ(r)]; Θ, ∆ r ∈ dom (Θ) (WRITE ) S[writeTVar rM ]; Θ, ∆ ⇒ S[return ()]; Θ[r �→ M ], ∆ r �∈ dom (Θ) (NEW ) S[newTVar M ]; Θ, ∆ ⇒ S[return r]; Θ[r → � M ], ∆[r �→ M ] ∗
M ; Θ, ∆ ⇒ return P ; Θ� , ∆� (XSTM1 ) S[catch M N ]; Θ, ∆ ⇒ S[return P ]; Θ� , (∆ ∪ ∆� ) ∗
M ; Θ, ∆ ⇒ throw P ; Θ� , ∆� (XSTM2 ) S[catch M N ]; Θ, ∆ ⇒ S[N P ]; (Θ ∪ ∆� ) , (∆ ∪ ∆� ) ∗
M ; Θ, ∆ ⇒ retry; Θ� , ∆� (XSTM3 ) S[catch M N ]; Θ, ∆ ⇒ S[retry]; Θ, ∆ ∗
M1 ; Θ, ∆ ⇒ return N ; Θ� , ∆� (OR1 ) S[M1 ‘orElse‘ M2 ]; Θ, ∆ ⇒ S[return N ]; Θ� , ∆� ∗
M1 ; Θ, ∆ ⇒ throw N ; Θ� , ∆� (OR2 ) S[M1 ‘orElse‘ M2 ]; Θ, ∆ ⇒ S[throw N ]; Θ� , ∆� ∗
M1 ; Θ, ∆ ⇒ retry; Θ� , ∆� (OR3 ) S[M1 ‘orElse‘ M2 ]; Θ, ∆ ⇒ S[M2 ]; Θ, ∆ Figure 5.4: Operational semantics of STM Haskell: STM transitions
Chapter 5: Formal Semantics
5.2
96
Semantics of DC Haskell
Having presented the syntax and semantics of STM Haskell, we now turn to our extension, DC Haskell. To support the new checkpoint operations, we need to extend the syntax and semantics appropriately. It does not, however, suffice to merely layer these additions on top; some more fundamental changes are required. The STM Haskell semantics use a (standard) heap, which is passed around by the various transition relations. To accommodate the ability of exceptions to abort a transaction, it also tracks allocation effects. When an exception rolls back a transaction, the appropriate transition relation reinstates the original heap extended with the subsequent allocation effects. A simple approach to supporting checkpoints would be to save a copy of the heap (a snapshot) associated with a checkpoint. Unfortunately, this approach fails to capture the information needed to support deltaRCP and deltaWCP. These are not properties that are observable solely by their footprint on the heap. Supporting them requires tracking more detailed information on how the heap was used between two checkpoints. Put another way, deltaRCP and deltaWCP are intensional properties of a computation’s interaction with a
heap. They cannot be defined extensionally using the initial and final heap. To support deltaRCP and deltaWCP, DC Haskell switches to using a trace based semantics inside of a transaction. This requires some more invasive changes to the semantics. As much as possible, we have kept to the style of the STM Haskell semantics, and just propagated the changes needed to support the traces. Despite the change in formalism, we prove in Section 5.3 that programs that do not use checkpoints behave identically under both semantics (Theorem 5.3.1).
Chapter 5: Formal Semantics
97
Figure 5.5 presents the extended syntax, which adds a new type of syntactic identifier, Checkpoint name, which represents a checkpoint. It also adds the delimited checkpoints operations from Figure 3.1 as values in the language. The program state and evaluation contexts also need some changes and additions. First, the set of STM evaluation contexts is extended to include the new deltaWCP, deltaRCP, and deltaACP values. The Allocations map is removed, as it is no longer needed. Instead, we reuse ∆ to represent a Trace. A trace is simply a sequence of Trace Actions. These represent the program’s interactions with the heap. In particular, it tracks writes (r ::= M ), reads (!r), allocations (r ← new M ), and the acquisition of a checkpoint (d ← save). Finally, a Checkpoints map is added, which maps a checkpoint to its associated trace. Instead of storing snapshots of the heap, this mapping stores snapshots of a transaction’s trace. With this richer infrastructure in place, we now turn to the operational semantics for DC Haskell. STM Haskell has three transitions relations: IO, STM, and Administrative transitions. The IO relation depends on the STM and Administrative relations, and the STM relation depends on the Administrative relation. DC Haskell adds a new transition relation, the Checkpoint transition, which sits in between the IO and STM relations. We will take a bottomup approach to explaining the new semantics. The Administrative relation is unchanged from STM Haskell. The STM relation uses the heap in a fundamentally different way. This is immediately apparent in the (WRITE ) rule: writes do not modify the heap. Instead, they simply add a write action to the trace. In general, the heap is not modified by STM relations but is used to represent the original (pretransaction) base heap. The current heap is represented as the
Chapter 5: Formal Semantics
Value
Term
98
x, y
∈
Variable
r, t
∈
Name
c
∈
Char
C
∈
Constructor
d
∈
Checkpoint Name
V
::= r

c

\x → M

C M1 · · · Mn

return M
 M >>= N

putChar c
 getChar

throw M
 catch M N

retry
 M ‘orElse‘ N

forkIO M
 atomic M

newTVar
 readTVar r

withCCP M
 atCP M N

deltaWCP M N  deltaRCP M N  deltaACP M N
M, N ::= x

V

MN

 writeTVar r M
···
Figure 5.5: DC Haskell: The syntax of values and terms
Chapter 5: Formal Semantics
99
Thread soup P, Q ::= Mt  (P Q) Heap
Θ ::= r �→ M
Trace
∆ ::= •  δ : ∆
Checkpoints
Φ ::= d �→ ∆
IO Evaluation STM Evaluation
contexts Action Trace Action
E ::= [·]  E >>= M  catch E M S ::= [·]  E >>= M  atCP E M 
deltaWCP E M  deltaWCP d E

deltaRCP E M  deltaRCP d E

deltaACP E M  deltaACP d E
P ::= Et  (P  P )  (P  P) a ::= ! c  ? c  � δ ::= r ::= M  !r  r ← new M  d ← save
Figure 5.6: DC Haskell: The program state and evaluation contexts
Chapter 5: Formal Semantics
100
composition of the heap and the effect of the write actions stored in the current trace. This composite (written Θ[∆] for base heap Θ and trace ∆), defined in Figure 5.10, simply adds the write effects from the trace into the base heap. It applies the trace actions in reverse order so that the newest effects are applied last, ensuring that multiply written references have their final value. The composite heap itself forms a heap, analogous to the concrete heap that was passed around in the STM Haskell semantics. In fact, Lemmas A.3.8 and A.3.3 formalize this relationship and show that these two heaps (the concrete heap of STM Haskell and the composite heap of DC Haskell) act the same way. This composite heap essentially replaces the original heap in the rules. The premises of (READ) and (NEW ) use the composite heap where the old semantics uses the concrete heap. Similarly, (READ) obtains the current value of the reference from the composite heap. While writes do not affect the base heap, allocations are added in to the base heap. This necessitates threading the base heap through everywhere (it must be both an input and output of the STM relation), but allows the transition relation to easily preserve allocation effects. When a rule like (XSTM2 ) needs to revert a nested transaction, it throws away the new trace and reinstates the original one. To preserve allocation effects (in case the thrown exception carries a newly allocated reference), it preserves the new base heap. This base heap is an extension of the original base heap, which includes the new allocation effects. This also allows the premise of (WRITE ) to check if the given reference is in the base heap, without needing to look at the current trace. The (WRITE ) rule just needs to ensure that the reference was allocated, which is a property of the base heap. For a transition relation in DC Haskell, Θ� \ Θ is the set of locations allocated by the
Chapter 5: Formal Semantics transition, playing the same role as ∆� \ ∆ did in the STM Haskell semantics.
101 2
The different uses of the heap and the use of traces also necessitate straightforward changes to the other rules. They are actually simplified by these changes, since allocation effects are simpler to track in the new system. To support the delimited checkpoint operations, DC Haskell adds a new type of transition relation, Checkpoint transitions. This relation is similar to the STM transition relation, but adds a checkpoint map that associates checkpoints with traces. Any STM transition can be lifted to a Checkpoint transition (with any checkpoint map) using the (ASTM ) rule. The other rules support the checkpoint functionality. The (ATCP ) rule runs the requested computation at a checkpoint. To do this, it looks up the trace associated with the checkpoint and runs the computation starting with that trace. The resulting trace is discarded: The effects of the (ATCP ) computation are thrown away. The heap however, with its new allocation effects, is preserved. This preserves the allocations inside an (ATCP ) computation, which is important since the computation may return a newly allocated reference. This makes the same choice as made in STM Haskell semantics (when an exception containing a reference is propagated through an aborted transaction): the reference will map to its value at allocation. The (WITHCCP ) rule runs a computation with access to an additional checkpoint, associated with the current trace. The act of taking a checkpoint is considered an action, and so is added to the trace. The rule first extends the trace by noting that the checkpoint has been saved. The checkpoint map is then extended with a mapping from a fresh checkpoint identifier to the extended trace. As discussed below, the definition of the (DELTACP ) While heaps and allocations are technically defined as partial functions, it is often convenient to treat them as sets of pairs. When viewed as such, Θ� \ Θ is the set difference of Θ� and Θ. 2
Chapter 5: Formal Semantics
102
crucially relies on the extended trace being put into the checkpoint map rather than the original trace. Next, the computation is applied to the fresh checkpoint identifier and run with the base heap, the extended trace, and the extended checkpoint map. Upon return, the resulting heap and trace are preserved, except that the trace has the d ← save action that the trace was extended with beforehand removed. This is done by decomposing the output trace into the tail end, which will just be the input trace (which is preserved by all the rules, see Lemma 5.2.1), followed by the temporarily inserted d ← save action, and finally followed by the new part of the trace. Note that ++ is standard list concatenation. Additionally, the original checkpoint map (sans the new checkpoint) is preserved. Note that the freshness conditions guarantee that the new checkpoint d does not leak from the computation, enforcing that it is used only in the delimited context. Finally, we turn to the (DELTAWCP ), (DELTARCP ), and (DELTAACP ) rules. They are collapsed into a single rule because of their similarity. This rule obtains the traces corresponding to the checkpoint arguments (from the checkpoint map) and calls a helper function diff , passing in the traces and a mode parameter, indicating if writes, reads, or allocations where requested. The diff helper proceeds down the first trace. If writes are requested and the helper finds a write, it adds the written reference to the return list and continues down the trace. If the helper finds a read or allocated trace action, the helper just skips the action and continues down the trace. Similarly if read or allocated effects are requested, the helper filters the first trace by the appropriate type of action. When the diff helper encounters a checkpoint save trace action, it examines the second trace to decide what to do. If the second trace starts with the same save trace action as the
Chapter 5: Formal Semantics
103
first trace (saving the same checkpoint identifier), then the helper is done. The helper has reached the least common ancestor of both traces, and can return the accumulated results (See Section 3.2.2 for a discussion of the least common ancestor and why it is used). Note that when we say d ∈ ∆ we mean that there is a trace action d ← save in the trace ∆. Otherwise, if the checkpoint is in the second trace (but is not the first element), then the helper is done with the first trace: all relevant information has already been culled from it, back to the least common ancestor (with respect to the second trace). So diff turns to the second trace by calling itself with the two traces swapped. Finally, if the first trace starts with a checkpoint that is not in the second trace, then the checkpoint save trace action is simply skipped. The definition of the diff helper seems somewhat problematic: it is both partial and does not always terminate. As an example of the former problem, diff W RA (•, •) is not defined. For an example of the latter problem, diff W RA (d1 ← save : d2 ← save : •, d2 ← save : d1 ← save : •) does not terminate since diff will continuously call itself recursively, swapping the traces each time. Nonetheless, the definition suffices, as it is never invoked with problematic arguments. It is used only by the (DELTACP ) rule, which passes in traces from the checkpoint heap. The only rule that adds traces to the checkpoint map is (WITHCCP ), which is careful to prepend the checkpoint save trace action d ← save to the trace before adding it to the checkpoint map. Furthermore, while checkpoints can be unordered, this can only happen due to a call to atCP d M. Use of this rule ensures that the checkpoint d will be shared by both “branches” of checkpoints, serving as their least common ancestor. Thus, all traces
Chapter 5: Formal Semantics
104
that arise within a given transaction must have a least common ancestor. Having explained the changes to the Administrative transition (none) and the STM transition, as well as the new Checkpoint transition, all that remains is the top level IO transition. The only changes are to the (ARET ) and (ATHROW ) transitions The changes here are minor, and are caused by the other changes. The premises of both rules are changed to use the new Checkpoint transition instead of the STM transition. An empty checkpoint map and trace are passed in: checkpoints do not persist across transactions. The (ARET ) rule recreates a concrete final heap by composing the final base heap and trace. The (ATHROW ) rule, in contrast, throws away the trace, effectively aborting the transaction. The final heap, however, is preserved, ensuring that allocation effects are preserved. In the STM Haskell semantics, this necessitated explicitly adding in the allocation map, but in DC Haskell the heap already contains the new allocation effects (and no other changes).
Chapter 5: Formal Semantics
105 a
I/O transitions
P;Θ → − Q; Θ�
P[putChar c]; Θ − → P[return ()]; Θ
!c
(PUTC )
?c
(GETC )
P[getChar c]; Θ −→ P[return c]; Θ �
P[return M ]; Θ (CATCH1 )
�
P[N P ]; Θ
P[catch (return M ) N ]; Θ
→ −
P[catch (throw P ) N ]; Θ
→ −
M →N
(CATCH2 )
(ADMIN )
�
P[M ]; Θ → − P[N ]; Θ t �∈ P, Θ, M
(FORK )
�
P[forkIO M ]; Θ → − (P[return t]  Mt ); Θ ∗
M ; Θ, • ⇒ return N ; Θ� , ∆� {}
(ARET )
�
�
�
P[atomic M ]; Θ → − P[return N ]; Θ [∆ ] ∗
M ; Θ, • ⇒ throw N ; Θ� , ∆� {}
�
P[atomic M ]; Θ → − P[throw N ]; Θ
(ATHROW ) �
Administrative transitions M → N M →V
if V[M ] = V and M ∼ �= V
return N >>= M
→ MN
(EVAL) (BIND)
throw N >>= M → throw N
(THROW )
retry >>= M → retry
(RETRY )
Figure 5.7: Operational semantics of DC Haskell: I/O and administrative transitions
Chapter 5: Formal Semantics
106
STM transitions M ; Θ, ∆ ⇒ N ; Θ� , ∆� M→ N (AADMIN ) S[M ]; Θ, ∆ ⇒ S[N ]; Θ, ∆ r ∈ dom Θ[∆] (READ) S[readTVar r]; Θ, ∆ ⇒ S[return Θ[∆](r)]; Θ, !r : ∆ r ∈ dom Θ (WRITE ) S[writeTVar r M ]; Θ, ∆ ⇒ S[return ()]; Θ, r ::= M : ∆ r �∈ dom Θ[∆] (NEW ) S[newTVar M ]; Θ, ∆ ⇒ S[return r]; Θ[r → � M ], r ← new M : ∆ ∗
M ; Θ, ∆ ⇒ return P ; Θ� , ∆� (XSTM1 ) S[catch M N ]; Θ, ∆ ⇒ S[return P ]; Θ� , ∆� ∗
M ; Θ, ∆ ⇒ throw P ; Θ� , ∆� + +∆ (XSTM2 ) S[catch M N ]; Θ, ∆ ⇒ S[N P ]; Θ� , filterw ∆� + +∆ ∗
M ; Θ, ∆ ⇒ retry; Θ� , ∆� + +∆ (XSTM3 ) S[catch M N ]; Θ, ∆ ⇒ S[retry]; Θ, filterw ∆� + +∆ ∗
M1 ; Θ, ∆ ⇒ return N ; Θ� , ∆� (OR1 ) S[M1 ‘orElse‘ M2 ]; Θ, ∆ ⇒ S[return N ]; Θ� , ∆� ∗
M1 ; Θ, ∆ ⇒ throw N ; Θ� , ∆� (OR2 ) S[M1 ‘orElse‘ M2 ]; Θ, ∆ ⇒ S[throw N ]; Θ� , ∆� ∗
M1 ; Θ, ∆ ⇒ retry; Θ� , ∆� + +∆ (OR3 ) S[M1 ‘orElse‘ M2 ]; Θ, ∆ ⇒ S[M2 ]; Θ, filterw ∆� + +∆ Figure 5.8: Operational semantics of DC Haskell: STM transitions
Chapter 5: Formal Semantics
107
Delimited Checkpoint transitions M ; Θ, ∆ ⇒ N ; Θ� , ∆� Φ
M ; Θ, ∆ ⇒ N ; Θ� , ∆� �
M ; Θ, ∆ ⇒ N ; Θ , ∆ Φ
∗
M ; Θ, Φ[d] ⇒ N ; Θ� , ∆� + +∆
(ASTM )
�
N ∈ {return N � , throw N � , retry}
Φ
(ATCP )
S[atCP d M ]; Θ, ∆ ⇒ S[N ]; Θ , filterw ∆ + +∆ �
Φ
M d; Θ, (d ← save : ∆) d �∈ Θ, ∆, Φ, M, N, θ� , ∆�
∗
⇒
Φ[d�→(d←save:∆)]
�
N ; Θ� , (∆� + + (d : ∆))
N ∈ {return N � , throw N � , retry} �
(WITHCCP )
�
S[withCCP M ]; Θ, ∆ ⇒ S[N ]; Θ , ∆ + +∆ Φ
S[delta[WRA]CP d1 d2 ]; Θ, ∆ ⇒ S[diff W RA (Φ[d1 ], Φ[d2 ])]; Θ, ∆ (DELTACP ) Φ
Figure 5.9: Operational semantics of DC Haskell checkpoints: Checkpoint transitions
Chapter 5: Formal Semantics
108
• + +∆�
≡ ∆�
(δ : ∆) + +∆�
≡ δ : (∆ + +∆� )
Θ[•]
≡ Θ
Θ[r ::= M : ∆]
≡ Θ[∆][r �→ M ]
Θ[!r : ∆]
≡ Θ[∆]
Θ[r ← new M : ∆]
≡ Θ[∆]
Θ[d ← save : ∆]
≡ Θ[∆]
filterw •
≡ •
filterw r ::= M : ∆
≡ filterw ∆
filterw !r : ∆
≡ !r : filterw ∆
filterw r ← new M : ∆ ≡ r ← new M : filterw ∆ filterw d ← save : ∆
≡ d ← save : filterw ∆
Figure 5.10: Operational semantics of DC Haskell checkpoints: helper definitions
Chapter 5: Formal Semantics
109
diff W RA (d ← save : ∆1 , d ← save : ∆2 ) = Nil diff W RA (d ← save : ∆, ∆� )
if d ∈ ∆�
= diff W RA (∆� , d ← save : ∆)
diff W RA (d ← save : ∆, ∆� )
if d �∈ ∆�
= diff W RA (∆, ∆� )
diff W (r ::= M : ∆, ∆� )
= Cons r diff W RA (∆, ∆� )
diff RA (r ::= M : ∆, ∆� )
= diff W RA (∆, ∆� )
diff R (!r : ∆, ∆� )
= Cons r diff W RA (∆, ∆� )
diff W A (!r : ∆, ∆� )
= diff W RA (∆, ∆� )
diff A (r ← new M : ∆, ∆� )
= Cons r diff W RA (∆, ∆� )
diff RW (r ← new M : ∆, ∆� )
= diff W RA (∆, ∆� )
Figure 5.11: Operational semantics of DC Haskell checkpoints: difference function
Chapter 5: Formal Semantics
5.2.1
110
DC Haskell Properties
This section states some basic properties of the transition relations and the auxiliary definitions of DC Haskell. Proofs of these properties are presented in the Appendix, in Section A.2. They are all straightforward inductions on the appropriate transition derivation or function definition. First, we state an easy lemma (Lemma 5.2.1) about how DC Haskell uses traces, mentioned in Section 5.2. This lemma helps justify the way that the (WITHCCP ) transition rule decomposes the output trace to remove the temporarily inserted d ← save action, as well as how the (ATCP ), (XSTM2 ), (XSTM3 ), and (OR3 ) rules filter writes out of the new part of the trace. Lemma 5.2.1 (DC Haskell only extends traces). ∗
If M ; Θ, ∆ ⇒ N ; Θ� , ∆� then there exists a ∆�� such that ∆� = ∆�� + +∆ Φ
Next, we look at the way that DC Haskell uses the heap. As discussed in Section 5.2, the domain of the heap is supposed to include all the references allocated during the course of execution. This is, however, not enforced by the transition rules. It is possible for a transition to hold even though, for example, the trace contains a reference that is not in the heap. Since some theorems require that the heap indeed contain the expected references, we state here a definition of wellformedness. Definition 5.2.1 defines wellformedness for various structures, all capturing this property. Note that we assume a natural inclusion predicate that checks if a reference is used anywhere in a structure. Definition 5.2.1 (Wellformedness). • Θ is well formed, if all r ∈ Θ are in dom Θ
Chapter 5: Formal Semantics
111
• M is well formed with respect to Θ, if all r ∈ M are in dom Θ • ∆ is well formed with respect to Θ, if all r ∈ ∆ are in dom Θ • Φ is well formed with respect to Θ, if all r ∈ P hi are in dom Θ • M ; Θ, ∆ ⇒ N ; Θ� , ∆� is well formed if Θ is well formed, and M and ∆ are well formed with respect to Θ. Similarly, the same conditions ensure that the transitive closure of the transition is well formed. • M ; Θ, ∆ ⇒ N ; Θ� , ∆� is well formed if Θ is well formed, and M , ∆, and Φ are well Φ
formed with respect to Θ. Similarly, the same conditions ensure that the transitive closure of the transition is well formed. Wellformedness is preserved by our system. Lemmas 5.2.2 and 5.2.3 state that the transition relations preserve wellformedness. Lemma 5.2.2 (Wellformedness preserved by STM transitions). ∗
If M ; Θ, ∆ ⇒ N ; Θ� , ∆� and M , Θ and ∆ are well formed with respect to Θ, then N , Θ� and ∆� are wellformed with respect to Θ� . Lemma 5.2.3 (Wellformedness preserved by CP transitions). ∗
If M ; Θ, ∆ ⇒ N ; Θ� , ∆� and M , Θ, ∆, and Φ are well formed with respect to Θ, then Φ
N , Θ and ∆ are wellformed with respect to Θ� . �
�
The auxiliary definitions from Figures 5.10 and 5.11 satisfy some simple properties, collected below. Many of them follow from the observation that trace composition can be viewed as a simple fold that folds each element of the trace over the heap, where write actions extend the heap and other actions are ignored.
Chapter 5: Formal Semantics
112
Lemma 5.2.4 observes that trace composition never removes references from the heap (although it may change the value to which they map). Lemma 5.2.5 presents the interaction of composition with trace append. In fact, with a suitable definition of composition of a trace action and a heap, Lemma 5.2.5 could be taken as the definition of trace and heap composition. Lemma 5.2.4 (Trace composition does not remove references).
dom Θ ⊆ dom Θ[∆] Lemma 5.2.5 (Trace composition distributes over append). θ[∆1 + +∆2 ] = (θ[∆2 ]) [∆1 ] Finally, Lemmas 5.2.7 and 5.2.6 observe some simple properties of filtering. Filtering distributes over trace append. Also, if all the write trace actions are removed from the trace, the heap is unaffected by the action of the remaining trace elements. Lemma 5.2.6 (Filtering distributes over trace append).
filterw (∆1 + +∆2 ) ≡ filterw ∆1 + +filterw ∆2 Lemma 5.2.7 (Filtering negates a trace’s effect on the heap).
Θ[filterw ∆] ≡ Θ
Chapter 5: Formal Semantics
5.2.2
113
Delimited Difference: DCd Haskell
As discussed in Section 3.1, it generally suffices to restrict our use of the deltaCP primitives to delimited uses that obtain the footprint of a given computation. These uses are encoded in the deltaCPd primitives in Figure 3.2. As these will be used frequently, Lemma 5.2.8 presents invertible inference rules for these functions. The inference rules can be used in both directions: both to derive and deconstruct uses of the deltaCPd functions. Lemma 5.2.8 (Inference rule for deltaCPd). The derived contracts deltaWCP, deltaRCP, deltaACP and deltaFCP have the following inference rule(s). Furthermore, if delta[WRAF]CPd p transitions, then this rule must have been applied (and so is invertible). ∗
P ; Θ, ∆ ⇒ return N ; Θ� , ∆� + +∆ Φ
(DELTACPD)
∗
delta[WRAF]CPd P ; Θ, ∆ ⇒ return(difftW RAF (∆� )), N ); Θ� , ∆� + +∆ Φ
Proof Sketch. This lemma follows from straightforwardly unfolding definitions, and noting that the base DC Haskell rules are invertible. More details can be found in Section A.2.1
The inference rules in the Lemma 5.2.8 make use of the auxiliary difft definition. Presented as Definition 5.2.2, the difft helper uses the underlying diff to obtain the relevant list of references from a trace. Definition 5.2.2 (Definition of difft).
difftW RAF (∆) = diff W RAF (∆ + +d : •, d : •)
Chapter 5: Formal Semantics
114
for some d �∈ ∆ While the definition of difft given in Definition 5.2.2 appears to depend on the choice of a fresh d, this checkpoint is used only to control when diff terminates. Lemma 5.2.9 shows that the definition is in fact agnostic as to the choice of d. Furthermore, while the definition assumes a • suffix to the trace for simplicity, Lemma 5.2.9 shows that the definition would have been equally valid with any suffix trace. Lemma 5.2.9 (difft is well defined). For any ∆� , d �∈ ∆, difftW RAF (∆) = diffW RAF (∆ + +d : ∆� , d : ∆� ) It is also possible to give a simple definition of difft that goes through the trace, finding all appropriate trace actions and adding the accessed reference to a list. However, it is convenient to reuse the existing definition of diff , using a fresh checkpoint to control when it stops. Given this characterization, is is not surprising that difft distributes over the append operation on traces (Lemma 5.2.10). Lemma 5.2.10 (difft distributes over append). For any ∆, ∆� , difftW RAF (∆� + +∆) = diffW RAF (∆� ) + +diffW RAF (∆) So far, this section has presented the deltaCPd as derived functions. The inference rules provided by Lemma 5.2.8 are derived from the inference rules of DC Haskell. As we will see when formalizing garbage collection (Section 5.4.2), the delimitation enforced by deltaCPd is crucial to ensure that a computation cannot make unrestricted
Chapter 5: Formal Semantics
115
observations about its input trace (i.e. previous computations). Thus, we will frequently want to discuss a restricted version of DC Haskell that does not provide the deltaCP primitives, but instead provides only the delimited deltaCPd primitives, with the inference rule given in Lemma 5.2.8 taken as primitive. This restricted system will be called DCd Haskell, standing for “Delimited Checkpoint with Delimited difference Haskell”.
5.3
Conservative Extension
The operational semantics for DC Haskell changes the way STM transitions for STM Haskell work. The old semantics update the heap in place and track allocation effects in ∆. The new semantics treat the heap as appendonly, mutating it only to add in allocation effects. Local modifications are stored in traces, which reuse the symbol ∆. Nonetheless, for programs that do not use any checkpoint related functionality, the semantics coincide. Theorem 5.3.1 states this more precisely by relating the top level IO transitions from the two semantics. Theorem 5.3.1 (DC Haskell is a conservative extension of STM Haskell). For a term P and a heap Θ that do not include any checkpoints or checkpoint related operations (withCCP, atCP, or any of the deltaCP operations), a
P;Θ → − Q; Θ� holds under the STM Haskell semantics if and only if it holds under the DC Haskell semantics. Proof Sketch. For a complete proof, see Section A.3. Here, we provide some intuition as to why the theorem holds. The interesting changes (other than additions to support checkpoint operations) are to
Chapter 5: Formal Semantics
116
the semantics of the STM transition: instead of a concrete heap and allocation set, DC Haskell uses a base heap and trace. As indicated before (in Section 5.2), we can connect the two semantics using trace and heap composition. In particular, the concrete heap (Θ) of STM Haskell is related to the composition of DC Haskell’s base heap (Θ) and trace (∆). The allocation map (∆) of STM Haskell can be recovered as the difference of the final base heap and the initial base heap in an STM transition (Θ� \ Θ). This correspondence allows us to move from one semantics to the other.
5.4
Taking Out the Garbage
The formalisms for DC Haskell presented in Section 5.2 are preserved under some types of garbage collection (and addition) and trace compaction (compressing traces to remove redundant information). The restricted DCd Haskell allows for even more equivalences, since the delimited difference functions can make fewer observations than the unrestricted difference primitives. This section specifies these kinds of operations and explicates their effect on the semantics. This has two benefits. Firstly, some theorems (and their proofs) hold only up to garbage collection. For example, Section 5.5 proves that certain contracts are erasable. This, however, is true only up to garbage collection: the contract may add garbage into the heap or trace. Secondly, these theorems justify optimizations by an implementation. Since these operations preserve the semantics of DC Haskell (or DCd Haskell), they can be optionally run by an implementation. Note that while we talk about garbage “collection”, many of these theorems are also (or primarily) about garbage addition: adding garbage into the heap, checkpoint map, or trace, does not affect derivations.
Chapter 5: Formal Semantics
5.4.1
117
The Freshness of References and Checkpoints
There is one technical detail that should be addressed before we can comfortably talk about “equivalent” transition relations: alphaequivalence (equivalence upto naming) of references and checkpoints.3 This problem is analogous to that encountered when dealing with equivalences in the lambda calculus. The (NEW ) rule creates a new reference, guaranteed to be distinct from any other references appearing in the transition relation. Similarly, the (WITHCCP ) rule creates a fresh checkpoint. In both of these cases, we do not really care what reference is returned, as long as it is distinct from the existing ones. In fact, our language gives no way to distinguish between the different possible choices. The only operations that give information about a reference are read, write, and equality. None of these can distinguish between different choices for a new reference, as long as the reference is fresh. Note that this is different from a language such as C, where the program can cast a pointer into an integer and so differentiate different possible pointers returned by the malloc allocation routine. While we may not care which reference is returned, the conditions for freshness can still cause problems. For example, given a relation ∗
M ; Θ, ∆ ⇒ N ; Θ� , ∆� Φ
and some extraneous reference r (i.e. r �∈ dom Θ), we would like to argue that ∗
M ; Θ[r �→ M � ], ∆ ⇒ N ; Θ� [r �→ M � ], ∆� Φ
i.e. that adding garbage into the heap does not affect anything (see Lemma 5.4.2. However, if M = newTVar 3 and the initial derivation happens to allocate r (so N = return r, Note that this is a somewhat technical issue, and readers that are unconcerned by it may safely skip this section (along with references to it elsewhere in the paper). 3
Chapter 5: Formal Semantics
118
then this is not true. The desired derivation would not satisfy the freshness condition of (NEW ). One way to deal with this is to manually rename references (and checkpoints) as needed. If we assume a standard definition of substitution, which simply replaces one reference name with another (written, for example, as M [r� /r]), then we can prove that ∗
M ; Θ, ∆ ⇒ N ; Θ� , ∆� Φ
implies ∗
M [r� /r]; Θ[r� /r], ∆[r� /r] ⇒ [r� /r] N [r� /r]; Θ� [r� /r], ∆� [r� /r] Φ
for any r� not appearing in any of the constituent parts of the transition derivation. Similar statements hold of the other transition relations. Proving these statements is straightforward, since equality is preserved by renaming, and all other operations care only about what r maps to in either the heap, trace, or heaps in the checkpoint map. Since substitution affects all of these simultaneously, there is no effect on any of the rules. A similar argument holds for checkpoints. This approach works, but requires the frequent introduction of substitutions, polluting both the statement of theorems and their proofs. An alternative is based on nominal logic (Pitts 2003). Instead of treating references as concrete objects, we treat them as representative elements of an equivalence class. This lets us choose a different element instead of performing laborious renaming. To do this we need to show that our semantics are unaffected by such changes: this is essentially the same as the invariance under substitution property just discussed. We will take this latter approach here and simply assume that fresh references remain fresh even when placed in a larger context.
Chapter 5: Formal Semantics
5.4.2
119
Garbage in the Input Structures
We now turn to some garbage collection and weakening theorems: extraneous information in the input structures of the transition relations does not affect the transition. This section relies on the previous discussion concerning the freshness of newly allocated references and checkpoints, since the presence of extraneous references and checkpoints can affect the technical requirements imposed for freshness. There are four different structures that are used as “inputs” in the transition relation: the input expression, the heap, the trace, and for the checkpoint transitions, the checkpoint map. For the input expression, it is possible to perform dead code elimination: code that is never run could be removed. However, since this is not needed by any of our theorems and is not particularly specific to our language, this thesis will not develop this possibility. This leaves the input heap, the input trace, and the checkpoint map. Garbage in the Input Heap In DC Haskell (unlike STM Haskell), checkpoint and STM transitions modify only the heap to record new allocations. The heap is otherwise preserved by all the inference rules. The heap is used in two ways: to determine if a reference is allocated (for the (WRITE ) and (NEW ) rules) and (composed with the trace), for (READ). The (NEW ) rule simply uses the heap to ensure that the newly allocated reference is sufficiently fresh. Pursuant to the prior discussion about freshness and naming in Section 5.4.1, this use can be safely ignored. In order to use the (WRITE ) and (READ) rules, the expression must write or read from a reference. If a reference is unreachable from an expression (in a given heap, trace,
Chapter 5: Formal Semantics
120
and checkpoint map), then these rules can never be used for that reference, and it can safely be added or removed from the heap. Rather than formulate a complicated definition of reachability, we will use a simple conservative approximation that suffices for our needs. However, in a realistic implementation with garbage collection, a more complicated definition of reachability (as used in garbage collection) could be used. These same theorems would hold for that more precise characterization. For our simple formulation, Lemmas 5.4.1 and 5.4.2 and 5.4.3 characterize garbage collection of the initial heap for STM, checkpoint, and IO transitions. More details can be found in Section A.4.1. Lemma 5.4.1 (Adding extra references to the heap (STM transitions)). For any Θg such that forall r ∈ dom Θg , r �∈ P, Θ, ∆, Θ� , ∗
P ; Θ, ∆ ⇒ N ; Θ� , ∆� if and only if ∗
P ; Θ[Θg ], ∆ ⇒ N ; Θ� , ∆� where Θ[Θg ] is the iterated extension of Θ with all the mappings in Θg . Lemma 5.4.2 (Adding extra references to the heap (checkpoint transitions)). For any Θg such that forall r ∈ dom Θg , r �∈ P, Θ, ∆, Θ� , Φ, ∗
P ; Θ, ∆ ⇒ N ; Θ� , ∆� Φ
if and only if ∗
P ; Θ[Θg ], ∆ ⇒ N ; Θ� , ∆� Φ
Chapter 5: Formal Semantics
121
where Θ[Θg ] is the iterated extension of Θ with all the mappings in Θg . Lemma 5.4.3 (Adding extra references to the heap (IO transitions)). For any Θg such that forall r ∈ dom Θg , r �∈ P, Θ, ∆, Θ� , Φ, a
P ; Θ, → − N ; Θ� if and only if a
P ; Θ[Θg ] → − N ; Θ� , where Θ[Θg ] is the iterated extension of Θ with all the mappings in Θg . Garbage in the Input Trace The trace based semantics used by DC Haskell make manifest (in the output trace) what parts of memory the STM transitions reference. The STM transitions in turn use only the current trace to augment the base heap and get the current virtual heap (see the discussion in Section 5.2). This property is formalized by Lemma 5.4.4 (Input trace only used for lookup (STM)). If ∗
M ; Θ, ∆ ⇒ N ; Θ� , ∆� + +∆ Then forall ∆�� such that forall r ∈ Θ, where ∆� = ∆pre + +!r : ∆post for some ∆pre and ∆post we have that Θ[∆](r) = Θ[∆�� ](r), then ∗
M ; Θ, ∆�� ⇒ N ; Θ� , ∆� + +∆�� Proof Sketch. The only real use of the input trace is by the (READ) rule. To read a reference (r), this rule obtains its value as Θ[∆](r). It also adds the reference to the output trace. This lines up nicely with our requirements.
Chapter 5: Formal Semantics
122
Section A.4.1 develops this proof in more detail. However, for checkpoint transitions, the story is a little more complicated. Unrestricted, deltaCP functions allows a computation to view parts of its input trace. A computation
can reify the current trace as a checkpoint using withCCP, and then compare that trace with a checkpoint taken earlier. For example, Figure 5.12 presents a child computation that accepts a reference (r) and a checkpoint (cp). It immediately acquires another checkpoint (current) using withCCP and obtains the set of references read between the current checkpoint and the input checkpoint. Finally, it checks if the passed reference (injected into an UntypedTVar as discussed in Chapter 3) is in the read set that was just obtained. The code for child does not read from any references, but it still depends on its input trace. This code highlights how deltaRCP is a source of information flow. The parent function, by the act of reading from a reference, can communicate with the child function. Dealing with this in general requires logging the invocation of deltaRCP action, along with its input checkpoints, in the trace. However, our semantics do not do this. Instead, we simply restrict our attention to the deltaCPd functions. These do not leak information in the same way, since they require an a priori declaration of what computation will have its memory interactions captured. The deltaCPd delimited difference functions suffice for all the contracts in this paper. As just discussed, to rule out problematic cases such as the code in Figure 5.12, we need to restrict the use of the deltaCP primitive. In particular, an equivalent lemma holds for the restricted DCd Haskell (see Section 5.2.2), which allows only the use of delimited difference functions. For DCd Haskell, Theorem 5.4.5 extends the previous Lemma 5.4.4 to checkpoint transitions.
Chapter 5: Formal Semantics
parent :: TVar → TVar → STM a parent r1 r2 = withCCP (\start → do x ← readTVar r1 y ← readTVar r2 writeTVar r1 y child r1 start
child :: Checkpoint → STM a child r cp = withCCP (\current → do reads ← deltaRCP cp current return ((UntypedTVar r) ‘elem‘ reads))
Figure 5.12: Looking at the input trace with deltaRCP
123
Chapter 5: Formal Semantics
124
Theorem 5.4.5 (Input trace only used for lookup). If (in the restricted DCd Haskell) ∗
M ; Θ, ∆ ⇒ N ; Θ� , ∆� + +∆ Φ
Then forall ∆�� such that f orallr ∈ Θ, where ∆� = ∆pre + +!r : ∆post for some ∆pre and ∆post we have that Θ[∆](r) = Θ[∆�� ](r), then ∗
M ; Θ, ∆�� ⇒ N ; Θ� , ∆� + +∆�� Φ
Proof Sketch. STM transitions lifted into checkpoint transitions by the (ASTM ) rule are handled by Lemma 5.4.4. Now that (DELTACP ) is no longer allowed, none of the other rules use the input trace. Note that the new (DELTACPD) rule (for delimited difference, defined in Section 5.2.2) does not use the input trace. Section A.4.1 develops this proof in more detail. Garbage in the Checkpoint map The checkpoint map is used to look up the trace associated with a checkpoint. This is used by the (ATCP ) and (DELTACP ) (as well as the (DELTACPD) variant) rules, and is updated by the (WITHCCP ) rule. Adding an additional checkpoint to the map doesn’t do any harm, since it will never be referenced. This is formalized by Lemma 5.4.6. Similarly, removing a checkpoint that is unreachable does no harm. To avoid a complicated definition of reachability, Lemma 5.4.6 conservatively ensures that the checkpoint does not appear anywhere else in the structures associated with the checkpoint map. Note that it suffices to be able to remove a single checkpoint at a time: there is never a need to remove a group of (mutually referential) checkpoints simultaneously, since the dependencies among checkpoints forms a tree.
Chapter 5: Formal Semantics
125
Lemma 5.4.6 (Adding extra checkpoints). For any d �∈ P, Θ, ∆, dom Φ, N, Θ� , ∆� , ∗
P ; Θ, ∆ ⇒ N ; Θ� , ∆� Φ
if and only if P ; Θ, ∆
∗
⇒
Φ[d�→∆��
N ; Θ� , ∆ �
Proof Sketch. The checkpoint map is never enumerated: only known references are looked up. More details can be found in Section A.4.1.
5.5
Erasure
Contracts should specify the behavior of a program and not affect its behavior. More precisely, contracts should either fail with an assertion violation or have no impact on the program. In practice, “no impact” is difficult to achieve for dynamic verification techniques that run the contract. As a result, we need to be more precise about what effects are considered to be observable. Two effects that are particularly hard to address are timing and termination effects. Running a contract will probably alter the timing behavior of a program. The runtime overhead of running contracts, in fact, motivates the common practice of eliding contract checking during release builds. For example, GHC omits all calls to assert (the builtin assertion mechanism mentioned in Section 4.1) when optimizations are enabled. This timing difference may be observable by the program. It is possible to obtain the current
Chapter 5: Formal Semantics
126
time from the operating system and use this information to detect if a contract has been run. This kind of information leakage is hard to prevent. Another important issue for erasure is nontermination: if a contract goes into an infinite loop, erasing it allows the program to proceed. This is addressed in specification languages such as JML by specifying that all code used in a contract must be total (must always terminate). Using dynamic verification techniques with a general purpose language, renders the precise detection of nonterminating contracts impossible. It is possible (and straightforward) to conservatively enforce nontermination using timeouts. However, for this thesis we will simply ignore timing and nontermination effects. Technically, we assume that our language has no way to observe timing information, and we treat nontermination as a form of contract violation. These assumptions are common for information flow based noninterference results (of which erasure here is a simple example). Now that we have excluded timing and nontermination based observations, we turn to other possible observations that can be made of our contracts. First, we address the interaction of our contract system and a feature of Haskell: laziness. Haskell values need not be computed until they are actually needed. If a contract requires a value, then it will be computed earlier than it otherwise would have been computed. This does not cause a problem with our erasability result, since it only affects timing properties of the program. If the computation needed to calculate a value does not terminate, then the contract will diverge, which we are modeling as a contract violation. If the computation successfully calculates the requested value, then that value is automatically cached by the runtime, speeding up the subsequent access. As a result, Haskell’s laziness does not present any problems for our
Chapter 5: Formal Semantics
127
contract system, and so is ignored in the rest of this dissertation. Another source of worry is the presence of other threads: what effects do contracts have on a multithreaded program? The transactional mechanism, conveniently, insulates us from worrying about this: by design, transactions cannot observe the behavior of other transactions. Note that the execution of a contract can affect the possible synchronizations of transactions (in an optimistic implementation this would be characterized by additional aborts) due to the contract reading additional memory. However, these differences are not observable by the program (except via timing observations, which we have already excluded). Note that these aborts are not caused by retry and so do not trigger the orElse block, they are just silently rerun. The final source of problematic observations comes from our new delimited checkpoints interface. In particular, the ability of the difference functions (even the restricted delimited difference functions) to observe what memory a computation has read allows the program to detect if a contract has read from memory. To prevent this, we simply disallow the program from using these functions. The delimited checkpoint interface is intended to support contracts, and so it is reasonable to restrict its use to contracts. A crucial property preserved by this restriction is that the program is insensitive to garbage (although contracts are not). The garbage collection theorems in Section 5.4.2 prove that the transitions in DC Haskell (or at least the restricted DCd Haskell) are insensitive to garbage. However, these theorems only talk about additional garbage in the inputs, not about additional garbage being generated by child transitions. The difference functions of DC Haskell (and DCd Haskell) allow a computation to view certain forms of garbage generated by child computations. For example, using deltaACPd, a computation
Chapter 5: Formal Semantics
128
can differentiate code that allocates and then immediately forgets about a new reference: do x ← newTVar v return ()
from code that simply does nothing: return ()
These problematic observations are only made possible by the difference functions. By weakening the observations the program can make (by disallowing its use of the difference functions), we allow more structures to be indistinguishable. In particular, the standard STM Haskell terms use only the trace by concatenating it with the base heap to obtain the current virtual heap. This observation is critical to the proof that the semantics of DC Haskell and STM Haskell coincide (Theorem 5.3.1).
5.5.1
Definition
Given the preceding discussion, we can define a notion of “erasableto”, which says that replacing a computation with another one produces results that are indistinguishable. Note that this is not a symmetric relation, as it requires that the latter computation always complete successfully when the former does (and not vice versa). As just discussed, this is a useful notion of erasability since STM Haskell terms are all insensitive to the difference. Definition 5.5.1 (Erasableto). M1 is “erasableto” M2 if for all wellformed input structures, where N ∈ {return N � , throw N � , retry}, ∗
M1 ; Θ, ∆ ⇒ N ; Θ� , ∆� Φ
Chapter 5: Formal Semantics
129
implies that ∗
M2 ; Θ, ∆ ⇒ N ; Θ�2 , ∆�2 Φ
for some Θ�2 = Θ� [Θg ] such that forall r ∈ dom Θg , r �∈ N, Θ, ∆� , Θ� and some ∆�2 that is well formed with respect to Θ�2 and such that forall r ∈ Θ� , Θ�2 [∆�2 ] = Θ� [∆� ]. The equivalent statements should also hold for STM, IO, and administrative transitions. Definition 5.5.1 formalizes the definition of “erasableto”. An expression M1 is considered erasableto M2 if being able to successfully run M1 implies that M2 can also run successfully and results in an output heap that is the same up to garbage (as in Lemma 5.4.2) and a trace that when concatenated with the heap yields equivalent results (for the nongarbage). To show that this definition is preserved in a larger context, we assume a simple substitution method, written M [M2 /M1 ] which replaces all occurrences in M of M1 with M2 . Theorem 5.5.1 then states that substituting (within a larger expression) using one expressions with an erasableto expression preserves the erasableto relation on the larger expression. This theorem is stated for a bunch of simultaneous substitutions, allowing multiple contracts to be simultaneously erased, so that the result does not otherwise use checkpoints. Theorem 5.5.1, combined with the conservativity and garbage collection theorems justifies our definition. Theorem 5.5.1 (Substitution under “Erasableto”). � is erasableto N � , then P is erasableto P [N � /M � ] as long as P does For any P , if M � subterms. not use the checkpoint functions outside of the M
Chapter 5: Formal Semantics
5.5.2
130
Erasing Contracts
Now that Definition 5.5.1 has formally defined and justified a definition of erasability, we can state erasure theorems for the delimited checkpoints functions and the various contracts and functions defined in Section 4. The primitive atCP checkpoint operation is a natural starting point. By design, atCP filters out any writes so that they do not affect the current trace. Therefore, the only observation that it enables (without the aid of the difference operations) is through its return result. If this is always a fixed value, then Lemma 5.5.2 states that running the atCP is equivalent to just producing that value. Lemma 5.5.2 (Erasing atCP). Given some fixed C (which is independent of the heap, then if ∗
atCP d M; Θ, ∆ ⇒ N ; Θ� , ∆� Φ
implies that N = return C then atCP d M erases to return C Similarly if N is always throw C for a fixed C, or is always retry. The first contract introduced in Chapter 4 is the assertM monadic assertion contract. Lemma 5.5.3, states a general inversion principle for assertM (that will also be used in Section 5.6). Similarly, Lemma 5.5.4 states a similar inversion theorem for the suppression versions of Section 4.3. Lemma 5.5.3 (Inversion of assertM). For N ∈ {return N � , throw N � , retry}, if ∗
(assertM P ) ; Θ, ∆ ⇒ N ; Θ� , ∆� + +∆ Φ
Chapter 5: Formal Semantics
131
Then N = return (), and for some ∆�� , ∆� = filterw ∆��
∗
P ; Θ, ∆ ⇒ return (); Θ� , ∆�� + +∆ Φ
Lemma 5.5.4 (Inversion of assertM’). For N ∈ {return N � , throw N � , retry}, if ∗
(assertM’ P ) ; Θ, ∆ ⇒ N ; Θ� , ∆� + +∆ Φ
Then N = return (), and Θ� [∆� + +∆] = Θ� [∆] and for some ∆��
∗
P ; Θ, ∆ ⇒ return (); Θ� , ∆�� + +∆ Φ
Given these inversion lemmas, the erasability of assertM (and assertM’) follows. This is the basis for our contract erasability results, since assertM is used pervasively to guarantee erasability. Theorem 5.5.5 presents a list of these results; they are discussed below in more detail. Theorem 5.5.5 (Erasing Framing Contracts). • assertM M
erases to
return ()
• requires P M
erases to
M
• preserves P M
erases to
M
• ensures P M
erases to
M
•
hoare P Q M
erases to
M
•
assertM’ M
erases to
return ()
•
sep P Q M
erases to
M
Chapter 5: Formal Semantics
132
Since assertM is erasable, it quickly follows that preconditions and postconditions (and invariants), as defined in Figure 4.3, are also erasable. These all wrap a computation with additional checks and erase to a simple invocation of the computation. Next, we turn to the framing contracts presented in Section 4.2. The writesOnly (Figure 4.7) contract (as well as the similar readsOnly and allocatesOnly contracts) erases to the computation it wraps. The suppression variant of writesOnly in Figure 4.9, suppressWritesOnly, as well as the special case of suppressWrite given in Figure 4.10, change the behavior of the computation, and so are not always erasable. However, if they always return the same result (as set up in Lemma 5.5.2 for atCP), then they erase to just returning that result. Furthermore, the derived assertM’ built using the suppression contracts erases to return (), just like assertM does (since assertM’ guarantees that they return a fixed
result, the unit value). Finally, we come to the separation contract sep, defined in Figure 4.17. Like the simpler hoare contract defined in Figure 4.2, it erases to the wrapped computation.
5.6
Frame Rule
Section 4.6 introduced separation logic, and Section 4.6.1 presented an implementation of the sep contract using delimited checkpoints. As discussed in Section 4.6, an important property of separation logic is the frame rule. Separation contracts only refer to the relevant parts of the heap. Given a separation contract with precondition P , postcondition Q, and computation M (sep P Q M ), if an assertion R that only talks about other (disjoint) parts of the heap holds before the computation runs, it should continue to hold after the
Chapter 5: Formal Semantics
133
computation runs. This is formalized using the separating conjunction as: sep P Q M (FRAME ) sep (P � R) (Q � R) M This frame rule is a critical component of separation logic and motivates its definitions. It allows a computation to worry only about the part of the heap that is being accessed. Callers can then use the frame rule to fit the localized specification into the larger context. It is possible to provide an explicit version of the frame rule that simply checks R as well as its disjointness from the precondition and postcondition before and after the wrapped computation. This can be straightforwardly encoded using delimited checkpoints, just as the separating conjunction was encoded in Figure 4.16. This, however, forces users to specifically use the frame rule. Additionally, it entails additional runtime overhead. Instead, we prove that the frame rule holds for our encoding of separation logic. This justifies reasoning about the guarantees provided by our dynamic contracts using the frame rule, providing assurance that our encoding of separation logic is reasonable. Since the frame rule is the heart of separation logic, this proof is strong evidence that our encoding is correct. The proof of this theorem critically relies on the ability of a trace to log what parts of the heap were accessed by a computation. This is formalized as Theorem 5.4.5. As discussed in Section 5.4.2, this theorem does not hold if the unrestricted deltaCP primitives are allowed. They hold only for DCd Haskell. Similarly, the frame rule holds for only this restricted subset of the language. We will use the definition of the separation contract sep and the separating conjunction given in Figures 4.17 and 4.16, desugared informally into the syntax of DC Haskell. We
Chapter 5: Formal Semantics
134
assume that the required primitives for list manipulation are included with the standard semantics. Additionally, we assume that signalFailure does not throw an exception, but instead desugars into a special fail construct that causes the semantics to get stuck (there are no transitions with fail as the antecedent expression). This conveniently separates contract failures from standard exceptions. Since our contracts are defined only for transactional code, Theorem 5.6.1 states the theorem in terms of Checkpoint transitions, the top level transactional relation. For simplicity, we assume (as in the definition of sep in Section 4.6.1) that the computation itself does not throw an exception or call retry. Accommodating this possibility is straightforward, and the proof remains essentially the same. Theorem 5.6.1 (Frame rule for CP transitions). For q, M , and r in DCd Haskell (i.e. they only use the delimited deltaCPd variants of the delta functions), if ∗
(sep P Q M ) ; Θ, ∆ ⇒ N ; Θ� , ∆� Φ
where N ∈ {return N � , throw N � , retry} and ∗
(assertM (P � R)) ; Θ, ∆ ⇒ return (); Θ�� , ∆�� Φ
then ∗
(sep (P � R) (\res old → Q res old � R) M ) ; Θ, ∆ ⇒ N ; Θ�g , ∆�g Φ
where Θ�g is just Θ� , possibly with some garbage added in (Θ� ⊆ Θ�g and for all l ∈ dom (θg� \ θ� ), l �∈ N, Θ� , ∆� , Φ) and ∆�g is just ∆� with some additional read and allocation trace actions. So ∆�g [Θ� ] = ∆� [Θ� ]
Chapter 5: Formal Semantics
135
Proof. The proof proceeds by decomposing the derivation of the transition of the assumed sep contract, and showing how to construct a derivation of the concluding contract. Lem
mas 5.2.8 and 5.5.3 are used to decompose the uses of the deltaCPd and assertM functions. Lemma 5.4.6 is used to lift the given transition derivation for assertM (P �R) into a context with additional checkpoints in the checkpoint map (added by the initial withCCP). Lemma 5.4.5, which characterizes how the input trace is used in terms of the output trace is used in a crucial way by this proof. First, it is used in order to show that later steps can still run despite the additional garbage (in both the heap and the trace) added by running R. Next, it is used, along with given footprint assertions (from the decomposed sep contract transition derivation), to show that the computation and R have no effect on
each other. Finally, verifying the footprint assertions in the conclusion is straightforward, given that they hold in the original derivation. Along the way, the proof also uses various minor lemmas concerning the properties and interactions of the various definitions. This includes, for example, Lemma 5.2.7, which states that filtering out the writes and composing the result with a heap leave the heap unchanged.
5.7
Undelimited Checkpoints
The checkpoint interface that we have modeled so far is delimited. This is seen in the (WITHCCP ) rule of Figure 5.9: after the nested computation is evaluated the saved checkpoint is removed from the checkpoint map and is no longer usable. This delimitation is similar to region based approaches to memory management, and allows an implementation
Chapter 5: Formal Semantics
136
to garbage collect the checkpoint data when withCCP returns. Section 6 takes advantage of this to simplify the implementation of delimited checkpoints on top of STM Haskell. It is also reasonable to talk about unrestricted checkpoints. The interface remains the same, except that we can replace withCCP with the simpler getCCP, which returns a new checkpoint. Since delimitation is not enforced, there is no need to pass in a computation in which the checkpoint is active, as in the original semantics. Figure 5.13 presents a version of the Checkpoint transition relation from DC Haskell that supports undelimited checkpoints. In DC Haskell, withCCP ensures that the checkpoint map Φ is updated using a stack discipline. This allows the Checkpoint transition relation to simply take Φ as an input, written as a subscript under the transition arrow. The getCCP operation, in contrast, does not use the checkpoint map as a stack, but rather adds
to it with every call to getCCP. This forces the Checkpoint transition relation to both take a checkpoint map as an input and return one as an output. Other than getCCP, the rules all return the same checkpoint map they are given. The changes to the type of the Checkpoint transition relation also necessitate minor changes to the IO transition relation. The (ARET ) and (ATHROW ) rules’ premises are changed to pass in an empty checkpoint map (as they originally did when it was notated underneath the arrow), and to return an arbitrary checkpoint map Φ� . We note that all of the theorems proven in the preceding chapters continue to hold for the extended semantics. The proofs do not make any use of delimitation, as it does not simplify the complexities that arise from using delimited checkpoints. In particular, the views of memory provided by checkpoints are only partially (and not totally) ordered.
Chapter 5: Formal Semantics
137
Undelimited Checkpoint transitions M ; Θ, ∆, Φ ⇒ N ; Θ� , ∆� , Φ� M ; Θ, ∆ ⇒ N ; Θ� , ∆� �
(ASTM )
�
M ; Θ, ∆ Φ ⇒ N ; Θ , ∆ , Φ ∗
M ; Θ, Φ[d], Φ ⇒ N ; Θ� , ∆� + +∆, Φ� N ∈ {return N � , throw N � , retry}
(ATCP )
S[atCP d M ]; Θ, ∆, Φ ⇒ S[N ]; Θ , filterw ∆ + +∆, Φ �
d �∈ Θ, ∆, Φ, M
�
∆� = d ← save : ∆ �
�
(GETCCP ) �
S[getCCP]; Θ, ∆, Φ ⇒ S[return d]; Θ, ∆ , Φ[d �→ ∆ ] (DELTACP ) S[delta[WRA]CP d1 d2 ]; Θ, ∆, Φ ⇒ S[diff W RA (Φ[d1 ], Φ[d2 ])]; Θ, ∆, Φ
Figure 5.13: Operational semantics of undelimited (first class) checkpoints
Chapter 6 Implementation The purpose of this dissertation is to lay out a framework for dynamically checking expressive contracts. Delimited checkpoints, the interface underlying this framework, were introduced in Chapter 3. Chapter 4 then demonstrated how to build expressive contracts using delimited checkpoints. Next, Chapter 5 presented a formal semantics for delimited checkpoints, and used them to prove that the derived contracts satisfy important properties. This chapter completes the foundation of the framework, implementing delimited checkpoints. In particular, we investigate how to reuse infrastructure developed for software transactional memory to support delimited checkpoints. This chapter first describes a prototype implementation built on top of GHC Haskell’s STM infrastructure. The existence of this implementation was a strong motivation for using Haskell in this thesis. After presenting the Haskell implementation, we then discuss how some other transactional systems with different implementation techniques could also be used to implement delimited checkpoints. Software transactional memory (STM) was introduced in Section 2.3. Transactions pro138
Chapter 6: Implementation
139
vide a modular form of concurrency control for shared memory multithreading. Specifically, transactions are guaranteed to execute atomically with respect to each other: two transactions will never appear to interleave. To exploit parallelism and avoid deadlock, most realistic STM implementations are optimistic: multiple transactions run at the same time, on the optimistic assumption that they do not conflict with each other. A common technique to support this is to log accesses to memory. If an intertransactional conflict is detected (such that two transactions appear to interleave) the system rolls back and restarts one of the transactions. Additionally, many STM implementations allow transactions to be nested.1 When a nested transaction aborts, the parent transaction need not be aborted: the nested transaction can be rolled back and restarted. When a nested transaction commits, the parent transaction continues. If the parent transaction commits, the nested transaction’s effects are propagated. If the parent transaction aborts, the entire transaction (including the nested transaction) is rolled back and retried. To implement delimited checkpoints, we use nested transactions. The STM implementation already needs to track enough data to rollback an invalid transaction, exactly the data needed to support delimited checkpoints. Once a nested transaction completes, the STM implementation is allowed to merge its effects into the parent transaction. This may allow it to free some redundant information from the child transaction, as well as bookkeeping data for the child transaction. This meshes well with delimited checkpoints, which go out of scope when their corresponding nested transactions finish. In fact, this observation is the motivation for the delimitation of checkpoints: delimitation greatly simplifies the 1
Technically, we are talking about closed nesting. We also assume support for partial rollback.
Chapter 6: Implementation
140
implementation. Section 6.4 discusses how an implementation could provide unrestricted checkpoints at the cost of greater complexity (and possibly overhead).
6.1
Implementation in GHC
We have extended the GHC runtime to support the delimited checkpoint interface.2 We first review how GHC implements the STM Haskell interface presented in Section 2.3.1. This discussion is simplified to highlight parts relevant to our work. The original paper has more detail (Harris et al. 2005). Then, we describe how we modified it to support delimited checkpoints. The GHC STM implementation uses a threadlocal log to present a local view of memory to a transaction. The writeTVar function does not modify global memory, but rather makes an entry in the threadlocal log. The readTVar function first checks the local log to see if the requested TVar has a corresponding entry. If it does, the value in the log is returned. If not, the value is retrieved from global memory, and an entry is added to the local log. Every log entry stores the TVar that is being read/written, the value that it had when it was first accessed, and its new value. For reads, the new value and original values are equal; for writes they may be different. When a top level transaction (run by atomically) completes, it attempts to atomically commit its local log into global memory. This is achieved by checking that global memory is consistent with what is expected. For every log entry, the associated TVar’s current value A patch to GHC, as well as an implementation of all the contracts in Chapter 4, is available at http: //avi.shinnar.com/thesis/. 2
Chapter 6: Implementation
141
in memory must match the original value stored in the log entry. If the transaction is able to commit, it writes out the changes recorded in the log into global memory, synchronizing with other committing transactions. If the transaction cannot commit, it is aborted: the log is thrown away, a new one is installed, and the transaction is rerun. While the STM Haskell interface does not explicitly provide nested transactions, the semantics of orElse and catchSTM require them. Using orElse or catchSTM starts a nested transaction: a new log is created with a link to the current one, and is installed as the thread’s current log. If a read is not satisfied in the current log, the parent’s log is consulted, and so on. If none of the logs satisfy the read, then the variable’s value is retrieved from global memory. The orElse and catchSTM functions also leave a frame on the stack to clean up after themselves. Upon return, the cleanup code tries to commit by merging the current log into its parent. It then installs the parent as the current log. If the child log cannot be merged into its parent, the child transaction is aborted and restarted. Aborting a nested transaction merges its list of locations the transaction has read into its parent’s log of locations that have been read. This allows transactions aborted with retry to wait until a location read by the transaction has been modified before trying again.
6.1.1
Adding Delimited Checkpoints
GHC’s implementation using local logs is particularly conducive to providing delimited checkpoints, as we can represent a checkpoint as a pointer to a log header. The patched GHC provides an (unsafe) primitive getCCP# 3
By convention, GHC primitives end in #
3
that returns an (unsafe) Checkpoint#.
Chapter 6: Implementation
142
This checkpoint is just the current transaction log for the thread. getCCP# must be called immediately after starting a new nested transaction. It is called by the exported wrapper method withCCP, using orElse: withCCP c = catchSTM (do cp ← getCCP# c cp) (\e → throw e)
Since e→ throw e is an identity for catchSTM (catchSTM c (e→throw e) ==c for all c), this preserves the computation’s semantics. The use of local logs makes implementing atCP and its underlying primitive atCP# fairly straightforward. Given the checkpoint cp, atCP# creates a new log �, whose parent is the log associated with cp. This gives � access to everything that happened before the checkpoint was taken. We record the currently installed log in a stack frame, and then install � as the current transaction log, transparently redirecting reads and write. The overhead of using atCP# is independent of the code run since the relevant checkpoint. When atCP# returns, it aborts the transaction it created and restores the previous log that it saved on the stack. This behavior can be used to optimize the composition of atCP and assertM: as the modifications are already discarded, the noWrite is redundant. Other parts of the system that walk the stack also need to deal with atCP# frames appropriately. In particular, the handlers for synchronous exceptions, asynchronous exceptions, and retry all need to abort the child transaction log and restore the original transaction log whenever they encounter these frames.
Chapter 6: Implementation
6.1.2
143
Difference Functions
In addition to withCCP and atCP, the delimited checkpoint interface from Figure 3.1 also supports three difference functions: deltaWCP, deltaRCP, deltaACP. Supporting these primitives requires some changes to the existing STM machinery. The existing mechanisms do not explicitly differentiate reads from writes, instead assuming that any entries with the same original and new value are read entries. This does not suffice to provide the semantics expected of deltaWCP and deltaRCP. Additionally, the implementation does not track allocation effects. Allocating a transactional variable has no effect on the current log. To address these deficiencies, the new implementation needs to track allocation effects as well as distinguish between read, write, and allocation effects. In the new implementation, allocating a variable therefore adds an appropriate entry into the local log. Additionally, the new implementation adds an extra flag to each log entry, recording if an entry represents a read, a write, an allocation, or some combination thereof. When merging a nested transaction with its parent, if an entry exists in both, then their flags are merged (after the usual validation to ensure that the entries are consistent). Code that needs to treat allocations differently is updated to check this flag to determine if an entry represents only an allocation. With this extra flag in place, it is simple to implement the deltaCP# primitives. They simply iterate over each entry in every log between the two checkpoints and check the flag of each entry. If the entry is of the appropriate type, the deltaCP# primitive adds the recorded transactional variable to a list. Duplicate entries can then be removed using the standard library nub function in conjunction with the provided equality for UntypedTVar.
Chapter 6: Implementation
144
As discussed in Section 3.2.2, checkpoints may be unordered (neither is an ancestor of another). This is addressed by first finding the least common ancestor of the two checkpoints. The difference between each checkpoint and the least common ancestor is calculated in turn, and the two are combined. Figure 6.1 gives a simplified version of the findLCA function that determines the least common ancestor of two checkpoints. It starts off by calling the countAncestors helper function, presented in Figure 6.2, to count the number of ancestor logs the checkpoint has. The countAncestors is also called with a log to stop at, and returns a boolean indicating if the stop log was found. The countAncestors helper is called with both checkpoints, using the other checkpoint as the stop log. If either stop log is encountered, then the one checkpoint is an ancestor of the other, and hence their least common ancestor. Otherwise, findLCA starts with whichever checkpoint has a larger ancestor count, and follows parent links until it finds an ancestor at the same depth (with the same number of ancestors) as the other checkpoint. The findLCA function now has two checkpoints whose least common ancestor is the same as the original two checkpoints, and which have the same number of ancestors. Consequently, these two derived checkpoints must also have the same number of ancestors between them and their least common ancestor. This fact is exploited by findLCA, which proceeds to look at successive ancestors of both checkpoints until their least common ancestor is found.
Chapter 6: Implementation
145
LogHeader *findLCA(LogHeader *start, LogHeader *end) { int count_end_ancestors, count_start_ancestors; bool found_start, found_end; found_start = countAncestors(end, start, &count_end_ancestors); if(found_start) { /* is start an ancestor of end? */ return start; } found_end = countAncestors(start, end, &count_start_ancestors); if(found_end)
{ /* is end an ancestor of start? */
return end; } while(count_end_ancestors > count_start_ancestors) { end = end → parent; count_end_ancestors; } while(count_start_ancestors > count_end_ancestors) { start = start → parent; count_start_ancestors; } while(start != end) { start = start → parent; end = end → parent; } return start; }
Figure 6.1: The findLCA function
Chapter 6: Implementation
146
bool countAncestors(LogHeader *cp, LogHeader *stop, int *count) { while(cp != stop && cp != null) { cp = cp → parent; (*count)++; } return (cp == stop); }
Figure 6.2: The countAncestors helper function
6.2
Evaluation
The implementation just presented is intended as a proofofconcept and is neither optimized nor tuned for efficiency. Nonetheless, we evaluated both the correctness and performance of our implementation. A series of unit tests validate the functionality of each contract presented in Chapter 4. For each contract, these tests check that the contract allows correct code to run while detecting errors and signaling them as appropriate. Details on the tests can be found in Section 6.2.1. We also measured the performance overhead of our implementation and contracts relative to STM Haskell using a set of benchmarks based around a transactional splay tree. Section 6.2.2 provides details about the general setup of the benchmarks, and Sections 6.2.3 and 6.2.4 detail the overheads of the delimited checkpoint operations and the derived contracts, as evaluated by our benchmarks. Next, Section 6.2.5 evaluates the overhead introduced by our modifications to the STM machinery. Finally, Section 6.2.6 contextualizes all the overheads previously presented.
Chapter 6: Implementation
6.2.1
147
Correctness
For each contract, a set of tests check that the contract functions correctly both for the case where the contract holds and when the contract is violated. Since the contracts are implemented using the delimited checkpoint operations, these test cases directly validate the contract implementations and indirectly validate the behavior of the underlying delimited checkpoint implementation. All the test cases are straightforward, checking both passing and failing contracts. Some expressions that return true and some that return false test the various assertion contracts, including assertions, preconditions and postconditions. The contracts all correctly signal an error exactly when the assertion returns false. Expressions with side effects further validate assertion contracts. The contracts correctly detect proscribed effects, signaling a contract violation. Code that that both does and does not write outside a given list of locations test framing contracts. The contracts correctly signal an error only when a proscribed location is written. Tests verify that writes to locations deemed permissible by the contract specification are allowed. Suppression contracts ensure that the suppression mechanism correctly suppresses the required writes at the appropriate place. The suppression contract reverts only writes outside the prescribed set of locations, and reverts them only upon completion of the suppression contract. Within the suppression contract, writes to disallowed locations are temporarily allowed and subsequent reads correctly return the updated (notreverted) value. An exemplary test case for suppression contracts is presented in Figure 6.3. This test creates two new transactional variables, and then starts a suppression contract, specifying
Chapter 6: Implementation
148
testSuppress = atomically (do x ← newTVar 1 y ← newTVar 2 suppressWritesOnly [UntypedTVar x] (do assertM (x  → exactly 1) assertM (y  → exactly 2) writeTVar x 5 writeTVar y 6 assertM (x  → exactly 5) assertM (y  → exactly 6)) assertM (x  → exactly 5) assertM (y  → exactly 2))
Figure 6.3: A test case for the suppressWritesOnly suppression contract
Chapter 6: Implementation
149
that the first variable (x) may be modified. Inside the contract, both variables are modified with a new value, and assertions verify that within the scope of the contract they retain these values: the writes are indeed temporarily allowed. After the contract completes, an assertion verifies that the first variable (x) retains its new value, since the contract specified that it could be modified. A second assertion verifies that the second variable (y) has its original (precontract) value. This ensures that, upon completion, the suppression contract correctly suppressed the proscribed modification. For conciseness, the testSuppress test case uses the pointsto ( →) contract and exactly helper function defined in Figure 4.14 for use with separation contracts. Finally, separation contracts are tested with code that reads outside of its precondition’s footprint, as well as with code that is well behaved. Additionally, it checks that the postcondition behaves as expected. In particular, test cases validate the use of old to view the precomputation state just like the test cases for standard postconditions. In all cases, the separation contract functions correctly.
6.2.2
Benchmarking Setup
The goal of our prototype is to provide safe and expressive contracts by leveraging the existing STM machinery. A reasonable implementation should add no more than twice the overhead of that incurred by STM. We will demonstrate that we do far better than this, typically exhibiting under 13% overhead, relative to STM. To evaluate the performance overhead of our implementation, we first run a set of benchmarks for the underlying delimited checkpoint operations from Chapter 3 and then benchmark the contracts implemented in Chapter 4. We expect to approximate the over
Chapter 6: Implementation
150
heads for the contracts by summing the overheads of their constituent operations. Sections 6.2.3 and 6.2.4 present details about these benchmarks and their results. Next, Section 6.2.5 discusses the overhead of our modifications to the underlying STM implementation. We evaluate this overhead using the subset of the preceding benchmarks that do not rely on the functionality of delimited checkpoints. Finally, Section 6.2.6 summarizes these results. Most of the benchmarks rely on a transactional splay tree implemented in STM Haskell. A splay tree is a binary search tree that implements all of its operations by adjusting (splaying) the tree to move the desired element to the root of the tree (Sleator and Tarjan 1985). In particular, this means that lookup operations have the side effect of modifying the tree so that the requested node is the new root node. This enables quick access to recently accessed nodes in the tree. Splay trees present a realistic example of a benign side effect: the lookup operation modifies the structure of the tree while preserving its contents. Note that this is only a “benign” side effect for operations that depend only on the contents of the tree: the result of a depth first traversal of the tree would be affected by the lookup operation. Thus, splay trees (and their lookup operation) are a motivating example for the suppression contracts presented in Section 4.3. Using a suppression contract allows the lookup to proceed while ensuring that the side effect is temporary and does not affect the behavior of the program. As varying the size of the splay tree did not change the relative overheads, this thesis presents data only for the case of a splay tree with five thousand nodes. The testsuite and benchmarks, as well as implementations of the contracts from Chap
Chapter 6: Implementation
151
ter 4 are available as a Cabal package4 from http://avi.shinnar.com/thesis/. This distribution also includes the raw data from the benchmarks. R CoreTM 2 Duo CPU (E8500) The benchmarks were run on a machine with an Intel�
running at 3.16GHz, with 8GB of memory. The machine was running a 64 bit version of Linux 2.6.3231. We started with a development build of GHC 6.12 and modified it to support delimited checkpoints. The same version (unmodified) provides a comparison point to measure the overhead of the changes. While Haskell supports multithreaded operations, and STM Haskell is designed to support concurrency, our benchmarks were all run sequentially in a single thread. We intentionally do not use concurrency because contracts themselves are run sequentially with the code they protect, inside of a single transaction. All the benchmarks run using the Haskell Criterion package (version 0.5.0.5) so as to obtain statistically valid results. To minimize the perturbations introduced by garbage collection, the garbage collector is invoked between each benchmark. The Criterion package runs each benchmark multiple times and divides to get the time needed to run a single instance. It guarantees that the benchmark is run sufficiently many times to obviate worries about timer precision. We report average times: these are computed by running multiple iterations and dividing. The Criterion package also uses bootstrapping to ensure that the results are not affected by noise, resampling as needed. We used the default settings, collecting 100 samples and bootstrapping with 100, 000 resamples. The following sections just present the mean, as the other values reported are not interesting for our benchmarks. In particular, the standard deviation is under 2% for all cases. 4
Cabal is the standard system for building and packaging Haskell libraries.
Chapter 6: Implementation
152
Operation
Measured
atomically
0.04 µs
getCCP
0.13 µs
lookup
18.26 µs
lookup/catchSTM
22.6 µs
lookup/withCCP
23.86 µs
lookup/atCP
23.34 µs
lookup/deltaWCPd
17.43 µs
lookup/deltaRCPd
18.40 µs
lookup/deltaACPd
16.54 µs
create+lookup
1.07 sec
create+lookup/deltaWCPd
1.06 sec
create+lookup/deltaRCPd
1.06 sec
create+lookup/deltaACPd
1.07 sec
Expected
lookup/catchSTM + getCCP ≈ 22.73
Table 6.1: Overhead of Transaction and Delimited Checkpoint Operations
6.2.3
Delimited Checkpoint Operations
We start by evaluating the operations provided by the delimited checkpoint interface. As presented in Figure 3.1, these are withCCP, atCP, and the three variants of deltaCP. Additionally, there are the derived deltaCPd delimited difference operations presented in Figure 3.2. For each of these, benchmarks evaluate the performance overhead. Table 6.1 summarizes the results. As all of the benchmarks need to run transactional code, they all incur some basic
Chapter 6: Implementation
153
overheads for using atomically to start and complete the top level transaction. The “atomically” benchmark measures this fixed cost at 0.04 µs by running a transaction that does nothing. Starting a transaction creates a new log header and sets it to be the current transaction. Completing a top level transaction checks the logs for consistency, committing the logs to memory, and marking the transaction as done. We now consider each of the delimited checkpoint operations in turn. Recall that the atomically function converts an STM computation into an IO computation that executes
as a top level transaction. Therefore, all STM operations, including the delimited checkpoint operators, must be invoked within an atomically function. As discussed in Section 6.1.1, the withCCP operation checkpoints the current state and runs a computation that can refer to that checkpoint. This is done by creating a new nested transaction using catchSTM, thus fixing the parent log and protecting it from subsequent modification. A pointer to the newly created log is obtained by calling getCCP# and used to refer to the checkpoint. In code: withCCP c = catchSTM (do cp ← getCCP# c cp) (\e → throw e)
The overhead of (unsafely) calling getCCP# is approximately 0.13 µs. Thus the overhead of withCCP should only add about 0.13 µs to the cost of starting and running the code in a new nested transaction. We first time a splay tree lookup operation by running it in a top level transaction, giving us a baseline for comparison. This operation took 18.26 µs to complete. Next, we break apart withCCP into its constituent catchSTM and getCCP# operations. Running the lookup operation inside of a catchSTM, introducing another level of
Chapter 6: Implementation
154
transactional nesting, takes 22.6 µs. Finally, the lookup operation run inside of a complete withCCP takes 23.86 µs. This is slightly larger than the predicted overhead of 22.73 µs.
The discrepancy is probably due to functional operations such as the invocation of bind; our benchmarks are not sufficiently precise to warrant a more detailed explanation. The next delimited checkpoint primitive is atCP, which allows a computation to run with memory as it was when a checkpoint was acquired. Using withCCP to acquire a checkpoint and then immediately using atCP to run lookup at that checkpoint is about 2% faster than running lookup within two withCCP operations. This is likely due to the reduced overhead atCP incurs by unconditionally aborting the nested transaction, as compared with the more complex transactional commit attempted by the withCCP operation. The final class of delimited checkpoint primitives are the deltaCP difference operations and the derived deltaCPd delimited difference operations. Recall from Section 3.1 that the deltaCPd operations acquire checkpoints before and after a computation and use the appropriate deltaCP primitive to calculate the difference. Wrapping a small operation such as lookup in any of the deltaCPd adds approximately 2% overhead. If the tree creation and lookup operations are benchmarked together, the overhead of wrapping them in a deltaCPd operation is not observable. In fact, surprisingly, running them inside of a deltaCPd operations consistently results in (very) slightly faster execution times.
6.2.4
Contracts
The preceding section evaluated the delimited checkpoint primitives. This section evaluates the higher level contracts presented in Chapter 4, implemented using the delimited checkpoint interface. In particular, we focus on assertion, framing, suppression, and sepa
Chapter 6: Implementation
155
Test
top level
nested
assertM
together
0.43 µs
0.80 µs
1.14 µs
separate
0.48 µs
1.36 µs
2.29 µs
Table 6.2: assertM Overhead ration contracts. Table 6.3 presents the overhead of using assertM for a simple assertion and Table 6.2 presents the measurements for the other contract benchmarks. The first type of contract is the assertion contract, discussed in Section 4.1. Our goal is to evaluate the overhead introduced by the machinery used to ensure that assertions do not have problematic side effects. This is the cost of providing safe assertions. We do not care about how long running the assertion body takes, as that is user provided code and is not an overhead introduced by our framework. As a result, we evaluate assertM using a simple assertion body. It reads from three variables, asserting a known true property of its value. Our benchmark runs these checks inside of a single assertion. Using assertM incurs a factor of 2.6 slowdown. Breaking down the overhead of using assertM, we note that it can be decomposed into two operations: starting nested transactions and the use of deltaWCPd to check for proscribed writes. Most of the overhead incurred by using assertM is from the additional nested transactions. Running the assertion body inside of appropriately nested transactions takes 1.9 times longer than running them without any additional nesting. The remainder comes from the 40% additional overhead incurred by the deltaWCPd operation and subsequent check. Next, we investigate the time it takes to run the assertions separately versus together
Chapter 6: Implementation
156
Operation
Measured
Expected
writesOnly
15.65 µs
deltaWCPd ≈ 17.43 µs
getLocations
17, 905.50 µs
lookup/suppress
17.32 µs
lookup/suppressFast
15.56 µs
sep/and
116.94 ms
sep/star
662.49 ms
atCP ≈ 23.34 µs
Table 6.3: Breakdown of Contract Overheads in a top level transaction. We expect that the overhead will be roughly that of the two additional calls to atomically (0.04 µs each). The overhead is actually slightly less than expected, probably due to the smaller transactions being faster to commit. The next type of contract, presented in Section 4.2 is a framing contract. We focus on the writesOnly contract presented in Figure 4.7. Our benchmark recurses through a tree and creates a list with all the locations used in the tree structure. The benchmark uses this list to enforce a writesOnly frame contract ensuring that a call to lookup writes only to locations in the tree. Enforcing the writesOnly contract should have similar overhead to the deltaWCPd operation. In this case, the benchmark actually ran slightly faster than the previous measurement for deltaWCPd. The cost of gathering up the sets of locations dominates the cost of enforcing the contract (by a factor of over 1, 000). This may be avoidable through other application level accounting mechanisms, however gathering up the relevant locations is part of the contract specification, not the contract enforcement, and so not part of the costs that we are
Chapter 6: Implementation
157
measuring. Section 4.3 introduces a variant of the writesOnly framing contract that suppresses modifications made outside the allowed area, called suppressWritesOnly. Figure 4.10 introduced a simpler version, suppressWrite, that only works for the degenerate (but common) case when nothing is allowed to be written. Both versions use atCP: the general suppressWritesOnly uses atCP repeatedly to obtain the old value of written locations,
and the special case suppressWrite uses a single atCP to implicitly discard all writes. Using the suppressWritesOnly contract does not incur observable overhead in this benchmark. The special case suppressWrite variant actually performs about 17% faster then just running a lookup. The final type of contract is the separation contracts introduced in Section 4.6. The separation contract sep combines an explicit precondition and postcondition with an implicit framing contract based on the footprint of the precondition. A commonly used idiom in separation logic is to use the separating conjunction to combine subcontracts while ensuring that they are independent. Our focus is on understanding the overhead of this typical use of the separating conjunction. To measure the overhead of the separating conjunction, we use a precondition that recurses over the entire tree and verifies a known tree invariant. The precondition uses a special version of lookup that does not use splay to adjust the tree, but just recurses down the tree like a normal binary search tree lookup. Using this alternative lookup simplifies the benchmark by avoiding mutations, thus focusing on the use of the separating conjunction and ignoring any issues related to side effects. In order to investigate the overhead of the separating conjunction, we compare two
Chapter 6: Implementation
158
variants of the precondition. The first combines all the individual checks using the classical conjunction (boolean and), and the second combines them using the separating conjunction, ensuring that the tree is well formed. We expected that using the separating conjunction in this manner would be expensive, as it runs a number of nested separating conjunctions (and the attendant deltaFCPd operations) proportional to the depth of the tree. Indeed, using the separating conjunction causes the contract to take almost 6 times as long as using classical conjunction.
6.2.5
Overhead of STM Modifications
Recall that supporting delimited checkpoints required making changes to the STM machinery. For example, our system adds allocation tracking. These changes add additional overhead to all STM operations, even those that do not use the new delimited checkpoint operations. This section evaluates the costs of these modifications. The previous two sections evaluated the overhead of using the delimited checkpoint operations as well as the derived contracts. These were measured relative to the baseline STM costs. All the measurements were done using the version of GHC modified to support delimited checkpoints. To measure the overhead introduced by our modifications, we ran all of our benchmarks that did not use delimited checkpoint operations under an unmodified version of GHC. None of the benchmarks incurred more than an 12% slowdown due to our STM modifications. Larger benchmarks, such as running a tree creation and lookup inside of nested transaction showed a slowdown of less than 3%.
Chapter 6: Implementation
6.2.6
159
What It All Means
As demonstrated by the preceding sections, it is possible to support the delimited checkpoint operations and their derived contracts with reasonable overheads. The implementation presented in this thesis is intended as a proofofconcept implementation. There are many optimization opportunities available for a more efficient implementation. The modifications to the underlying STM machinery could be more carefully tuned for performance. Additionally, common contracts could be given special support and implemented more efficiently than the naive version given in terms of delimited checkpoints. The naive implementation would then serve as a reference implementation: an optimized version should guarantee the same semantics. Nonetheless, despite the lack of optimization, the implementation presented demonstrates that safe and expressive contracts can be supported on top of an existing STM implementation. Supporting optimistic transactions using STM requires significant overheads. Delimited checkpoints show how we can leverage these overheads to not only increase parallelism but to support safe and effective contracts.
6.3
Working with Other STM Implementations
GHC’s implementation of software transactional memory is easily extended to support delimited checkpoints. The use of local “redo” logs closely mimics the local view of memory presented by checkpoints. Other implementation techniques require a bit more work. Common options include using inplace updates5 and multiple versions of memory. 5
Also known as writeahead logging.
Chapter 6: Implementation
6.3.1
160
In Place Updates (Undo Logging)
A common implementation technique is to have transactions write changes directly to shared memory. The STM keeps an undo log with the pretransactional value of written variables, as well as logs of which variables have been written or read. If a transactional conflict is detected, the undo log is used to rollback memory. A checkpoint can be implemented in such as system as an index into the undo log. For example, the Bartok STM has a single undo log per thread (Harris et al. 2006). Nested transactions are indexes into that log. Note that when a nested transaction commits, the implementation reuses the log space for the next nested transaction. As a result, it is critical to enforce checkpoint delimitation. We first consider handling reads in a call to atCP, and then look at writes. The read operation on variables needs to be aware of the current base checkpoint. If it is just the default one, read proceeds as usual. If we are using atCP to run at another checkpoint, the value of a variable at the checkpoint can be obtained from the undo log. We find the first occurrence of the variable in the undo log after the checkpoint: the old value of this entry is the variable’s value at the time of the checkpoint. If there are no such records then the variable is unchanged and we just get its value from global memory. The other complication is how to handle writes inside of an atCP. An inplace implementation strategy is geared toward transactions that frequently commit. However atCP always aborts: its writes never persist. As a result, it makes sense to use a local log approach to implement atCP. Code run in an atCP block uses a local log approach, similar to GHC’s approach. Between the undo log and the local logs, there should be sufficient information to im
Chapter 6: Implementation
161
plement deltaWCP and deltaRCP, using the list of read and written variables found in both the undo and local logs. If the implementation does not track allocations, this needs to be added. Simple approaches include using a log entry, as in the GHC implementation, and using a separate per log set of allocated references. Nested transactions would merge this set with their parent’s set upon transaction commit or abort.
6.3.2
MultiVersion Concurrency Control (MVCC)
Another implementation technique employed by some systems such as Clojure (Hickey 2008) is multiversion concurrency control (MVCC). In MVCC based systems, writing to an object does not overwrite it, but instead creates a new version of the object. Reads can be redirected to use an older version, allowing the system to effectively give a transaction a snapshot of memory. This makes it easy to work from an old view of memory, as is needed for atCP. To handle reads, we set the transaction inside of atCP to have the transaction identifier saved by withCCP. This guarantees that the transaction sees the appropriate view of memory. Handling local writes is also straightforward. MVCC assumes that there is a set of ordered transaction identifiers. This is used to ensure that transactions can see the last version of memory before the transaction started. We can weaken the order on transaction identifiers to be a partial order: not all identifiers will be comparable. Starting a new transaction creates a new identifier, greater than the current ones used at the top level. When atCP runs, it creates a new identifier greater than the current transaction’s identifier. This identifier will not be comparable to future top level transaction identifiers. Since future transaction identifiers will not be comparable to the identifier created by atCP, their
Chapter 6: Implementation
162
transactions will not observe values written by atCP. This generalizes the view of memory from a linear time ordered view to a tree based view. Implementing the difference functions, however, may be more difficult in MVCC. The existing infrastructure does not necessarily track the relevant information. In this case, it may be necessary to add per transaction read/write/allocated logs as appropriate. As a result, supporting these operations may result in substantial overhead.
6.4
Implementing Undelimited Checkpoints
This thesis has emphasized the utility of delimited checkpoints. As pointed out in Section 3.2.2, this restriction (delimitation) does not simplify the meaning of checkpoints. In fact, the formal semantics of undelimited checkpoints given in Section 5.7 are arguably simpler than the formal semantics for delimited checkpoints given in Section 5.2: the additional complexity arises from the need to enforce delimitation. The utility of delimitation is instead apparent in the implementation: delimited checkpoints require only straightforward logging based techniques. In particular, the logging performed by many STM implementations can be reused to support delimited checkpoints. Supporting first class (undelimited checkpoints) is more complicated. Section 7.1 discusses some relevant related work. Conceptually, if memory is thought of as a finite map, first class stores can be implemented with similar techniques as those used to implement persistent finite maps. For those familiar with first class and delimited continuations, we point out that the implementation issues here are similar. In particular, first class checkpoints are akin to first class continuations, just as delimited checkpoints are akin to delimited continuations. In the
Chapter 6: Implementation
163
checkpoint implementation, the heap is represented as a stack of log entries, reminiscent of the stack of local variables that arise with continuations. Thus, implementation techniques created for supporting efficient first class continuations should be applicable to this setting. In this section, we discuss how first class undelimited checkpoints could be supported in the context of the GHC implementation presented in Section 6.1. There are two mismatches between the current implementation and undelimited checkpoints. The first is fairly simple to address: when a nested transaction finishes, the runtime can reuse the log header. This is done to save on allocation time. This problem is easy to fix, by simply removing this functionality and requiring the runtime allocate a new header every time. The old header will be garbage collected when it is unreachable (when any checkpoints are unreachable). This is straightforward because the log headers are standard objects in memory, subject to garbage collection like everything else. In fact, the current code to reuse memory is conditionally compiled (presumably to facilitate measuring the effect of reusing log headers) and is therefore easy to disable. The other mismatch is between the immutable nature of checkpoints and the mutable transactional log. When a nested transaction (during which a checkpoints was taken) returns, it merges its log into its parent log. As the parent continues executing, it extends the log, which alters the state captured by the checkpoint. One solution is to change the way nested transactions are completed. Instead of merging their changes into their parent, they simply continue a new branch off from the current log. When a transaction aborts, it starts a new branch off of the parent log and starts over. Reads may need to be copied over to ensure that the reads of the aborted transaction are still present. Of course, instead of actually copying them, the checkpoint corresponding to the
Chapter 6: Implementation
164
aborted transaction can be saved in a list in the new child transaction, implicitly adding all the reads that happen in between it and the parent transaction into its read set.
Chapter 7 Related Work An important contribution of this thesis is the use of checkpoints to integrate transactional mechanisms with expressive contracts. This chapter presents some related work from each of these areas and especially from their overlap. Section 7.1 discusses prior work reifying the store as a first class object in a programming language. This idea is extended by checkpoints, the fundamental abstraction underlying this thesis. As delimited checkpoints are implemented using software transactional memory infrastructure, Section 7.2 presents relevant research on transactions. Next, this chapter turns to contracts, first discussing contract languages for functional programming languages (Section 7.3), and then some related work on runtime verification of contracts (Section 7.4). Finally, Section 7.5 reviews prior work on two properties that contract frameworks should satisfy, erasability and blame.
165
Chapter 7: Related Work
7.1
166
First Class Stores
A crucial component of this thesis is the introduction of the delimited checkpoint interface. Providing a first class representation of the store is not itself a new idea. This section discusses prior work in this area, showing how delimited checkpoints extends the existing work. Johnson and Duggan (1988) introduce first class stores and discuss how they could be used in a sequential language setting, particularly for debugging. A fully persistent tree implementation is used to support the multiple versions of the store. Later research adapted this idea to Standard ML. Morrisett (1993) discusses how to implement first class stores in Standard ML and Tolmach’s thesis and subsequent work shows how first class checkpoints can be used to implement a time traveling debugger(Tolmach 1992; Tolmach and Appel 1995). All of this work on first class stores and checkpoints focuses on providing a form of timetravel: a program can go back to a previous checkpoint. This is provided by the atCP primitive in our interface. Our interface is novel in that it allows for the comparison of checkpoints. In particular, the deltaCP primitives return the difference between two checkpoints, allowing a contract to determine (and limit) a computation’s effects. This ability is used to great effect in Chapter 4 and is not supported by the previous work. The deltaCP primitives also imply that our checkpoints can not be modeled as simple copies of the store. They are not extensional properties of the store: given two versions of a store, it is impossible to determine what reads have been done in between those versions: reads do not affect the value of the store. Checkpoints instead capture a point in time during
Chapter 7: Related Work
167
a computation. Comparing two checkpoints reveals how the computation has interacted with (not just modified) memory between those points in time. Other key contributions of our work are the observations that checkpoints can be particularly useful for building contracts and that transactional mechanisms can support delimited uses of checkpoints — which suffice for building contracts.
7.1.1
Delimited Control and Delimited Binding
There has also been work on implementing delimited control operators in the presence of dynamic bindings. Delimited control operators, introduced by Felleisen (1988), allow the scope of a continuation to be restricted. In their work on Delimited Dynamic Binding, Kiselyov et al. (2006) formalize a system that supports both delimited control operators as well as delimited binding. As an extension, they additionally support mutable dynamic bindings and a facility (similar to the old function) allowing a computation to obtain bindings other than the most current one. Supporting delimited control is beyond the scope of this thesis. It is not clear if the formalism developed in the Delimited Dynamic Binding paper would be applicable to our setting, where checkpoints encode more information than just the current values of the bindings. Formalizing the meaning of the difference operations in the presence of delimited control operators would be interesting future work.
Chapter 7: Related Work
7.2
168
Transactional Memory
Section 6 presents an implementation of delimited checkpoints based on software transactional memory. An overview of various transactional memory systems is provided by Larus and Rajwar (2007). This section discusses work related to this thesis’s particular use of transactions to implement delimited checkpoints.
7.2.1
Checkpoints and Continuations
The delimited checkpoint implementation in Section 6 uses nested transactions for checkpoints. Koskinen and Herlihy (2008) propose that checkpoints and continuations should be used to accommodate partial rollback, instead of nested transactions. In this context, a checkpoint is a point to which a partial abort can rollback and retry. The program marks where it thinks checkpoints should be taken, but does not otherwise employ checkpoints. This thesis presents an enhanced definition of checkpoints that can be fruitfully manipulated by the program. The implementation of delimited checkpoints presented in Section 6 builds upon conventional (closed) nested transactions. There is no reason however, that an alternative implementation could not be built on top of the checkpoints described by Koskinen and Herlihy.
7.2.2
Abort on Exception
A number of STM implementations, including GHC (Harris et al. 2005) and Bartok STM (Harris et al. 2006), abort the current transaction when an exception is thrown (and
Chapter 7: Related Work
169
not caught by the transaction). Shinnar et al. (2004) suggest that the ability to revert a transaction upon an exception is useful for error handling even in a sequential setting. They use the ability to abort a transaction to make sure that cleanup actions (for untracked resources) do not modify memory and so do not break the transactional model. This is the same ability exploited for delimited checkpoints. In fact, Figure 7.1 implements their tryall construct using the general delimited checkpoint interface. The implementation
of tryall first acquires a checkpoint, and then runs the requested computation inside of catchSTM. If the computation throws an exception, tryall restores memory back to its
previous state using the restoreTVar helper function from Figure 4.9 before propagating the exception. tryall c = withCCP (\start → catchSTM c (\e → withCCP (\end → do d ← deltaWCP start end mapM_ (restoreTVar start) d throw e)))
Figure 7.1: Implementing the tryall construct introduced by Shinnar et al. (2004)
7.2.3
Transactions and Concurrency
Transactional memory provides a simple mechanism for controlling the allowed interleavings in concurrent programs. They have also allowed this thesis to largely ignore
Chapter 7: Related Work
170
concurrency and its attendant complications. Within a transaction, all reasoning can be done sequentially, without worrying about other transactions interleaving between two operations. Semantically, this is convenient. In particular, it ensures that nothing interleaves between a precondition and the method it is protecting. This allows the method to confidently assume that the precondition continues to hold. Even in the transactional model, where transaction are run (asif) sequentially, there is potential for parallelism. In particular, it is possible to run expensive contracts in parallel with the code they are checking. Future Contracts are an implementation of this idea for otherwise sequential Scheme code (Dimoulas et al. 2009). Assertions are turned into futures, which can be evaluated in another thread (assuming that there are sufficient resources). Before the program attempts to access memory or perform IO, any waiting assertion futures are synchronized, ensuring that they have finished evaluating. In a language such as Scheme, where programs often have few side effects, this can allow a contract to run in parallel for a while before synchronization is needed. In a transactional setting, this idea can be further extended. Aftandilian et al. (2011) implement asynchronous assertions using snapshots in Java. When an assertion is run, it first acquires a snapshot of the heap. The assertion then uses that snapshot to satisfy all reads, ensuring that the assertion sees the correct view of memory. The snapshot is implemented using a copyonwrite approach: before the computation modifies memory it saves the old value into any active snapshots (that do not already have a saved value for that location). The same ideas can naturally be used in an implementation of delimited checkpoints.
Chapter 7: Related Work
171
In fact, atCP is naturally parallelizable, as it prevents the computation from modifying the parent computation’s memory. If a value is returned from atCP and requested by the program, then it forces the evaluation of the atCP. For an implementation that takes advantage of the delimited use of checkpoints, returning past a checkpoint also needs to force the evaluation of any atCP expression that uses that checkpoint (in particular, any that are run “at” that checkpoint). This also opens up the possibility for opportunistic contract checking. Assertions are turned into futures, and if there are available resources to execute them in parallel, then they are checked. Otherwise, they are simply discarded as needed. The implementation described in Section 6.1 does not currently support this parallelization. Currently, transaction logs are only accessed by a single thread. Supporting parallel contracts would entail multiple threads accessing the same transaction log. In particular, when a parallel atCP completes and the associated transaction merges its read set into the parent log, it needs to be careful to synchronize with the other thread, which could potentially access the same transaction log. To avoid this additional complexity, the GHC implementation does not support parallel contracts. It would be interesting future work to add support for them and evaluate if the additional complexity and performance overhead is worthwhile.
7.3
Contracts for Functional Languages
A distinguishing feature of functional programming languages is their emphasis on higher order functions. Standard preconditions and postconditions do not scale well to higher order functions, which need to abstract over the pre and post conditions of functions that are passed as arguments to other functions. Indeed, higher order functions blur the
Chapter 7: Related Work
172
distinction between functions and values. To address this problem, Findler and Felleisen (2002) introduce higher order function contracts and implement them in the Racket1 programming environment. As part of this, they ensure that blame is correctly assigned, as discussed in Section 7.5.2. This work has been further extended by the Racket community, including recent support for firstclass modules(Strickland and Felleisen 2009), and firstclass classes(Strickland and Felleisen 2010).
7.3.1
Static Contract Checking for Haskell
Since our host language is Haskell, existing work on contract checking in Haskell is particularly relevant. Xu et al. (2009) develop a static verification framework for Haskell contracts. Any boolean valued (pure) expression can be used as a contract. This work allows for higher order dependent contracts and is careful to correctly track blame assignment. Additionally, the use of arbitrary Haskell expressions for contracts forces this work to explicitly address the affects of laziness and nontermination, as well as failing contracts. This framework for static verification addresses a different design point from this thesis. It permits only pure contracts; their contracts cannot refer to the value of references. Dynamically checking such assertions is easy, as they have no side effects (unless the assertion does not terminate, but this is anyway not checked by the static checker) and can just be run. This thesis focuses on dynamically checking stateful contracts on stateful computations. Static checking is not explicitly addressed, however the formal semantics given to our contracts enables sound static reasoning. 1
Originally called PLT Scheme
Chapter 7: Related Work
7.4
173
Runtime Verification
Chapter 4 presents an assortment of contracts implemented with delimited checkpoints. The runtime enforcement of contracts is not novel, although the ability to enforce a wide variety of contracts using a single mechanism is a contribution of this thesis. Clarke and Rosenblum (2006) provide an excellent historical overview of runtime assertion checking. Here, we call out related work that is closely connected to the contracts implemented in this thesis.
7.4.1
JML Runtime Assertion Checker
The Java Modeling Language comes with an ecosystem of tools that interpret specifications. Burdy et al. (2005) provide an overview of these tools and applications that use them. Of particular relevance to this thesis is jmlc, the runtime assertion checker. Cheon and Leavens (2002) describe the design and implementation of the jmlc assertion checker. It supports many features of JML beyond the scope of this thesis, including (limited) support for quantifiers and Java specific features such as contract inheritance. The jmlc assertion checker offers a limited form of support for old expressions in postconditions: they are replaced by a fresh variable, and the precondition is altered to calculate the requested expression and cache its value (using a shallow copy) in that variable before running the wrapped computation. This approach works for simple expressions that do not depend on the checked computation. However, it does not generalize to uses of old with expressions that are not predetermined. This is unfortunate, as this is precisely the use case that motivates old as adding expressiveness to the contract language. Additionally, the jmlc assertion checker described by Cheon and Leavens does not
Chapter 7: Related Work
174
check JML’s framing contracts, such as the assignable or accessible contracts. Later work by Ye (2006) investigates techniques for supporting runtime checking of restricted framing contracts, and Lehner and M¨uller (2010) extend this work to efficiently check dynamically determined frame conditions. However, this work has not been integrated into the standard runtime assertion checker. Our thesis demonstrates how to check significantly more contracts than are checked by the jmlc tool. Using delimited checkpoints, it supports using arbitrary computations in an old expression and checking framing contracts. Additionally, it supports the dynamic enforcement of separation contracts, a new type of contract not included in JML. Delimited checkpoints enable a straightforward implementation of all of these contracts.
7.4.2
Implementing old with Snapshots
Section 4.1 presented a form of time travel, where postconditions can access the old (precomputation) state of memory. This is implemented in Section 4.1.1 using delimited checkpoints. As mentioned in Section 7.1, delimited checkpoints represent more than a state of memory; they represent a point in time. However, this additional ability (and the difference functions that use it) is not needed for implementing the old function. All that is needed is the ability to take a snapshot of memory. There has been prior work on using snapshots to implement the old function. Kosiuczenko (2009) argue that some form of snapshotting is required to get the proper semantics for the old function. They also outline an adhoc snapshotting implementation to supply the required functionality. Gray and Mycroft (2009) extend Java with support for postconditions that can access
Chapter 7: Related Work
175
the old value of a variable and can check if a variable was modified. These features are supported using snapshots, implemented with transactional mechanisms (but only for sequential code). Our work exposes snapshots via an expressive interface that enables additional contracts, such as framing and separation contracts. Our delimited checkpoint interface also allows effects in contracts to be explicitly suppressed, allowing more code to be safely used in contracts. We also provide a formal semantics and a proof of erasability.
7.4.3
Runtime Checking for Separation Logic
Section 4.6 introduced and implemented contracts based on separation logic. Dynamically enforcing separation logic is not a novel contribution of this thesis: Nguyen et al. (2008) develop SLICK, a runtime checker for separation logic based assertions. Two challenges are addressed by SLICK: checking the footprint of specifications and handling existential quantifiers. To verify the appropriate conditions on the computation’s footprint, SLICK uses a coloring technique. Preconditions “color” the memory used by the precondition. The computation then checks and adjusts the coloring as needed, and the postcondition removes the coloring. To handle disjunctive formulas, they try each case of the disjunction in turn. If a case fails (cannot be satisfied), they undo its coloring effect by rerunning it with the opposite color. By comparison, our implementation of separation contracts in Section 4.6 directly obtains the footprint of the precondition, the postcondition, and the computation. These footprints are then compared to verify the required relationships. This allows Figure 4.17 to implement the separation contract in seven lines of code using the delimited checkpoint
Chapter 7: Related Work
176
interface. In addition to the implementation advantages of using delimited checkpoints, instead of an adhoc solution, there are also formal benefits. Since delimited checkpoints have a formal semantics, they allow for formal reasoning about the derived separation contract. In particular, Section 5.6 proves the crucial property of separation logic, the frame rule. This would be much more difficult for the adhoc solution used by SLICK. The SLICK runtime checker also has a mechanism for inferring the value of existential variables. This is based on the assumption that the existential variables are either totally constrained by the specification (by an equality assertion), or are irrelevant. Both of these cases happen frequently in specifications based on first order logic. In contrast, in the computational setting of this thesis it is possible to avoid these uses of existential variables. Existential variables that are later constrained by an equality assertion can be replaced with a variable declaration, initialized to that same value. Existential variables that represent irrelevant information can be avoided by changing the assertion to simply not care about the irrelevant value.
7.4.4
Transactional Consistency
This thesis explores the intersection of transactions and contracts, using transactional facilities to support expressive contracts. Database transaction systems also support assertions and constraints, sometimes encoded using triggers (GarciaMolina et al. 2008). If an assertion fails, the transaction is aborted. Harris and PeytonJones (2006) consider how to add support for this type of application defined consistency requirement. They extend STM Haskell (Figure 2.4) with a new primitive
Chapter 7: Related Work
177
check :: STM a → STM ()
whose argument is checked on all subsequent commits. The invariant can be locally broken, but must hold at the start and end of all transactions. This feature is complementary to our contracts: check constrains the data whereas our contracts constrain computations. The formal semantics and GHC implementation of STM Haskell are extended to support check. The implementation is careful to only check invariants that depend on a modified location, and garbage collects unreachable invariants. There is little overlap between these extensions and ours: both coexist without a problem. This work shares some design points with ours. Since data invariants should not affect the behavior of the program, the implementation wraps them in a nested transaction, which is used to suppress their effects. Harris and PeytonJones also discuss providing an old expression, enabling access to the pretransactional state of memory, although this extension is not implemented. Both of these features are generalized by delimited checkpoints.
7.4.5
GC Assertions
Our work leverages STM infrastructure to support expressive contracts. Similarly, Aftandilian and Guyer (2009) leverage the garbage collector to check global memory properties such as assertdead and assertunshared. Since the garbage collector has to traverse all memory anyway, checking these properties is relatively efficient. These properties validate simple memory properties; our work validates complex user specified contracts. The types of contracts supported by each system are different. Reichenbach et al. (2010) generalize the preceding work, introducing a domain specific language for properties that
Chapter 7: Related Work
178
can be efficiently checked at garbage collection time. In particular, this language enforces that stated properties can be checked in a single heap traversal, with good locality properties. This allows for efficient checking of reachability related properties, which is not efficiently (or generically) supported by our system. On the other hand, it does not support general assertions. These two systems can be used together. As heap assertions should be erasable, they can be asserted in a contract. They can then be lazily checked as part of garbage collection. Section 4.6 mentioned how heap assertions could augment our separation contract. The separation contract presented in Figure 4.17 is based on classical separation logic and allows the postcondition to “forget” information. A variant could determine exactly what was forgotten (using the footprints of the constituent computation and assertions, as discussed in Section 4.6.1), and use heap assertions to verify that they are all unreachable.
7.5
Contract Properties
The literature has identified two properties that contracts should satisfy: erasure and blame.
7.5.1
Erasure
Contracts should be erasable: they should not affect the behavior of the program. This allows contracts to be ignored when reasoning about the program. A similar idea arises with aspectoriented work. Reasoning about code in the presence of possible advice injected from elsewhere is challenging: this is what makes aspectoriented programming so
Chapter 7: Related Work
179
dangerous. (Dantas and Walker 2006) introduce the idea of “Harmless Advice”, aspectoriented advise that obeys a weak form of erasure / noninterference. Harmless advice is allowed to diverge (does not need to terminate) and can use I/O, which is useful for logging / debugging purposes. Aspectoriented advice can be difficult to reason about, due to its crosscutting nature. Requiring it to be harmless restores the ability to reason locally about the program. This is similar to our reasons for requiring contracts to be erasable. Unlike our work, which focuses on dynamic enforcement techniques, the work on harmless advice introduces a type system for statically enforcing noninterference. Erasable contracts and harmless advice coexist nicely. A motivating use of harmless advice is “invariant checking and security”. The contracts discussed in this thesis could be used in aspectoriented advice, allowing them to be easily integrated into a large program. Since the contracts in this thesis are harmless, they fit naturally into the paradigm endorsed by Dantas and Walker.
7.5.2
Blame
We have focused on erasure as a key property of a contract system. Another important property is blame: when a contract fails, what code is at fault? In a sequential first order setting, ascribing blame is easy. Findler and Felleisen (2002) show how to properly assign blame in the context of a higherorder language, such as Racket or Haskell. Later work by Dimoulas et al. (2011) presents a formal account of blame and Ahmed et al. (2011) demonstrate how to integrate with blame with parametric polymorphism. The contract frameworks for functional languages discussed in the subsequent section
Chapter 7: Related Work
180
(Section 7.3) are careful to ensure that the appropriate component is blamed for a contract violation. This thesis has not explicitly dealt with the assignment of blame, but the existing methodologies should work with delimited checkpoint base contracts. Verifying this is left as future work.
Chapter 8 Conclusions and Future Work This dissertation introduced a unified framework for supporting expressive contracts on top of existing infrastructure used for supporting software transactional memory (STM). To this end, Chapter 3 introduced a novel interface, the delimited checkpoint that serves as a foundation for this work. This interface is an important contribution of our work, providing an abstraction layer between the desired contracts and the underlying STM implementation. Chapter 4 validated our claim that delimited checkpoints support building expressive contracts. Assertion contracts with time travel, framing contracts, and separation contracts are all implemented using delimited checkpoints. Additionally, a novel suppression contract is introduced and implemented. This contract allows code that may modify memory to safely run inside of an assertion. When the assertion completes, its effects are suppressed: memory is reverted back to its original state. This promotes code reuse, allowing code with benign side effects to be called within an assertion, while ensuring that the assertion does not affect the behavior of the rest of the program. Chapter 5 then presented a formal operational semantics for delimited checkpoints. 181
Chapter 8: Conclusions and Future Work
182
Extending the existing semantics for STM Haskell, this semantics precisely defines the meaning of the delimited checkpoint interface. Using this semantics, Chapter 5 proved important properties of the contracts implemented in Chapter 4. In particular, it proved that assertions do not affect the behavior of the rest of the program. Additionally, it proved that the frame rule, a defining feature of separation logic, holds for the separation contract introduced in Section 4.6. Thus, delimited checkpoints not only allow expressive contracts to be easily implemented, but also enable formal reasoning about the derived contracts. After this, Chapter 6 filled in the remaining part of the framework, discussing how to implement delimited checkpoints. A proofofconcept implementation in GHC Haskell was presented and evaluated. Delimited checkpoints are not just convenient for implementing contracts, but themselves are straightforward to implement on top of existing STM infrastructure. Delimited checkpoints thus provide a convenient stepping stone for implementing expressive contracts on top of systems that support software transactional memory.
8.1
Motivation for Expressive Contracts
Using delimited checkpoints, this dissertation has demonstrated how to support expressive contracts with well defined semantics. In particular, assertion, framing, and separation contracts are all supported. As of yet, however this dissertation has not explained why these types of contracts were chosen. In particular, why support framing and separation contracts? The desire to support these types of contracts grew out of the author’s experience with the formal verification of expressive properties using interactive theorem proving (Chlipala et al. 2009). For verification techniques to scale, they must be modular. As an example, the
Chapter 8: Conclusions and Future Work
183
author participated in an effort to formally verify a small relational database management system (Malecha et al. 2010). This effort crucially relied on the use of separation logic to enable local reasoning. More generally, framing contracts (both explicit and implicit ala separation contracts) allow verification efforts to ignore parts of the code not relevant to the task at hand. This is important for managing complexity: only code essential to the specification in question need be analyzed. It is unlikely that programmers will immediately start writing complicated frame conditions or separation contracts. A strength of the integrated framework for contracts provided by this dissertation is the migration path offered contract writers. Programmers can start by using standard assertions, as are provided by all major programming languages. The implementation presented in this dissertation ensures that such assertions are checked safely, and can be erased without altering the behavior of the program. These assertions can then be turned into preconditions and postconditions providing better (checked) documentation for a method and enabling more modular reasoning. These contracts can be partial, and can be used to help find bugs. As they are checked, they provide a form of documentation that does not get out of date. Over time, programmers can add in a frame condition as needed. This can be useful for debugging and analysis purposes, ensuring that a method does not affect data it should not modify. Alternatively, if a function’s contract is sufficiently complete, its precondition and postcondition can be reinterpreted as separation contracts, providing a framing contract for free. Of course, the separating conjunction can also be used to verify that two computations are disjoint from each other, as expected. The separating conjunction is not restricted to separation contracts: it can also be used in standard assertions to verify disjointness
Chapter 8: Conclusions and Future Work
184
assumptions. By providing this migration path and enabling programmers to migrate to expressive contracts, our framework paves the way for static verification. As contracts become more complete, and framing (explicit or implicit) information is specified, static verification techniques can be used to verify contracts. Since delimited checkpoints and the derived contracts have a formal semantics, static analysis tools (or interactive theorem provers) can soundly model the meaning of a contract. Since all successfully terminating contracts provably have no affect on the rest of the program, any contract proven to hold can be safely elided.
8.2
Adoption of Expressive Contracts
For simple assertions (that do not support time travel), the implementation is correspondingly simple, and the benefits clear. As a result, assertions are supported by every modern programming language. However, more sophisticated contracts do not enjoy such widespread support. Language and compiler implementors understandably do not wish to implement complicated mechanisms to support contracts without evidence that these contracts will be used. In the design of Eiffel, Bertrand Meyer evangelized the use of structured assertions, integrating them into the language and promoting their pervasive use. This effort appears to have been successful: Chalin (2005) found that Eiffel code tends to have a higher percentage (6.42%) of assertions per lines of code than either open source (5.10%) or proprietary code (3.27%). Years later, the advantages of supporting structured assertions have become apparent, eventually prompting Microsoft to include support for them in Version 4.0 of
Chapter 8: Conclusions and Future Work
185
their .Net programming environment(F¨ahndrich et al. 2010). Structured assertions are still relatively easy to implement. Supporting the old keyword can be difficult, but implementations generally either restrict the allowed expressions or provide questionable semantics. More expressive contracts, such as framing and separation contracts, are more challenging to implement. Additionally, their benefits may not be as obvious to programmers not familiar with formal reasoning techniques. These present a significant obstacle to their adoption. This dissertation attempts to ameliorate these difficulties and provide a path forward. The delimited checkpoint interface admits a straightforward implementation using existing Software Transactional Memory. This allows language implementors to support both using common infrastructure. Most implementations that support postconditions either restrict the use of old or exhibit undesired behavior in certain cases. As discussed by Kosiuczenko (2009), to properly support the old keyword some form of snapshot is required Therefore, if a language desires to correctly support structured contracts, it already needs to implement snapshots. Given that, implementing the full delimited checkpoint interface is a reasonable possibility, especially if the language already supports transactional memory. Delimited checkpoints, in turn, allow more sophisticated contracts to be easily built.
8.3
Future Work
This thesis introduced a new interface, delimited checkpoints, which enables well defined contracts to be built on top of STM machinery. In this section, we sketch out some particularly interesting directions for future work.
Chapter 8: Conclusions and Future Work
8.3.1
186
Integration with Static Verification
Chapter 5 presented a formal semantics for checkpoints and used it to prove that various properties hold for derived contracts. This formal basis for contracts enables the sound integration of static and dynamic verification techniques. A static analysis can use this formal definition to verify that a contract always holds, and can therefore be safely elided. This is facilitated by the erasure theorems of Section 5.5.2: no further analysis is needed to ensure that successful assertions can be safely erased. While this thesis has laid the foundation for such an integration of static and dynamic verification, it has focused exclusively on dynamically checking contracts. Integrating these contracts with frameworks for static analysis is left as future work. In addition to integration at the contract level, adding support for discharging assertions statically, it might be fruitful to encode the semantics of delimited checkpoints in a mechanized theorem proving environment. In addition to gaining a higher level of assurance through mechanizing the proofs in Section 5, this would enable a programmer to interactively verify selected assertions, which can then be (provably) elided.
8.3.2
Delimited Checkpoints and Delimited Control
This thesis focuses on delimited checkpoints, which are only valid within a given region. This name was chosen because of some similarities to delimited control, alluded to in Section 7.1.1. Unlike delimited control operators, delimitation does not increase the expressiveness of checkpoints; delimitation is enforced solely as a convenience for the implementation. The connection to delimited control does not arise in the semantics, but in the logbased implementation. Logging all accesses to the heap turns the heap into a stack
Chapter 8: Conclusions and Future Work
187
like structure. Delimited checkpoints manipulate that stack in a manner reminiscent of delimited control operators. It would be interesting to make this connection precise, and see if it leads to a natural way to integrate delimited control and checkpoints.
8.3.3
Contracts
Chapter 4 demonstrated that expressive contracts can be built using the delimited checkpoint interface. In particular, contracts inspired by the Java Modeling Language and separation logic are implemented. Here, we discuss some other contracts that would be interesting to explore, as well as extensions to those contracts previously discussed. First, we review some choices made to narrow the scope of this dissertation, and highlight resulting missed opportunities. Next, we present a new type of contract that could be implemented using delimited checkpoints. We then discuss extensions to contracts previously presented, including the separation contract introduced in Section 4.6 and the support for temporal properties discussed in Section 4.4. Beyond Transactional State Extending this work to handle contracts beyond this narrow focus offers some interesting possibilities. A key contribution of this work is embodied in the framing contracts introduced in Section 4.2: the ability to limit effects. Generalizing this ability to other effects, such as IO, forking a thread, and communicating via a channel would allow a greater range of code to be used in contracts. More generally, it would be useful for sandboxing untrusted code. Throughout this dissertation we have relied upon the transactional foundation of our
Chapter 8: Conclusions and Future Work
188
framework to simplify reasoning. Since transactions restore “asif” sequential reasoning to concurrent code, contracts are free to ignore the difficulties that arise from concurrency. Providing a sensible interface that works with more complex locking based methods of concurrency control would enable wider adoption of the delimited checkpoint based framework. Additionally, it would enable the integration of contracts that specify allowed interleavings, locking protocols, and other concurrency related properties. Representation Independence An important advantage offered by our system over existing work is the ability to control memory effects. In particular, Section 4.1.2 introduced the noeffect helper that detects writes to memory and Section 4.3 introducted suppresion contracts that temporarily allow but later revert writes to memory. An interesting variant of suppression effects is sequestering effects: rather than reverting writes to memory, the modifications could be sequestered into intracontract local state. This ability could allow a contract to run multiple versions of a computation simultaneously, checking that they yield consistent results. Such a contract could be used to ensure representation independence: ensuring that multiple implementations of an interface all act equivalently. This would allow for automated equivalence checking amongst multiple implementations of an interface. Separation Contracts Revisited Section 4.6 presented a separation contract based on the intuitionistic separation logic, and discussed how it differed from a classical logic. Delimited checkpoints provide the information needed to determine the exact footprint of a computation, but lack the ability
Chapter 8: Conclusions and Future Work
189
to reason about reachability needed to enforce a classical variant of the separation contract. As discussed in Section 7.4.5, GC Assertions integrate with the garbage collector to provide assertions based on reachability (Aftandilian and Guyer 2009). Combining delimited checkpoints and GC assertions would allow for a more exacting definition of the separation contract. Other extensions to separation logic can also be explored in the computational setting of this thesis. The separating conjunction and the separation contract implemented in Figures 4.17 and 4.16 use the footprint, defined in Figure 4.15, to enforce separation. This footprint operation is coarsegrained, conflating references that are written with those that are read. A more nuanced interpretation is possible, permitting concurrent reads. The separating conjunction can allow both contracts to read from the same reference, as long as neither reads or writes from a reference the other has written. Similarly, the separation contract can be updated to permit the computation to write only to those references which are written by the precondition: a reference that was only read in the precondition can only be read in the wrapped computation. This requires creating two variants of the points to function from Figure 4.14: one for reading and one for writing. Footprints can be generalized further than this simple differentiation into reads and writes. Bornat et al. (2005) introduce fractional and counting permissions, which associate with each reference a permission indicating how much access a computation has. The points to relation is updated to specify the appropriate permission. One interesting type of permission is fractional: computations with a fractional permission may read from a reference, computations with a complete permission may also write to it. Differentiating reads and writes can be seen as a degenerate case of this complex model. Unlike that
Chapter 8: Conclusions and Future Work
190
simple differentiation, however, which can use the read and write logs to track the difference implicitly, more complex permission models require explicit support. This could be provided by using a domain specific language for assertions, embedding a more typical separation logic language into our computational setting, but would lose a key advantage of our approach: the ability to use arbitrary (transactional) computations in an assertion. Investigating this tradeoff further and fleshing out these implementation ideas might help our work extend to some forms of concurrency, a key motivation for fractional permissions. Section 5.6 proved that the separation contract implemented with delimited checkpoints satisfies the frame rule (introduced in Section 4.6). The frame rule allows a computation to be used in a larger context, allowing the context to “hide” irrelevant information from the computation. Similarly, it is possible to posit an antiframe rule that allows a computation to hide local data from the context (Pottier 2008). This can be used to enforce abstraction boundaries, allowing a module to reason about private state without worrying about how the context behaves. If antiframing can be supported using delimited checkpoints, it could allow contracts to dynamically enforce module boundaries. If done correctly, this should also allow contracts to dynamically enforce that intracontract local data does not influence the behavior of the program. The approach outlined in Section 4.4 uses module level abstraction and information hiding to statically enforce this requirement. Dynamic enforcement via antiframing would be more in the spirit of this thesis, promoting more flexible dynamic checking.
Chapter 8: Conclusions and Future Work
191
Temporal Contracts Section 4.4 presented a framework for supporting intracontract local state. This provides a foundation for safely encoding temporal contracts, but does not provide any convenient framework for doing so. It would be interesting to introduce high level domainspecific languages for temporal contracts. This would allow simpler specification of common temporal properties. As an example, it could allow a contract to enforce that one method is only called after a second method is called. The implementation of this language can use the underlying support for intracontract local state to ensure that erasure holds of its contracts. A modest language would allow for declarative specifications of finite automata based contracts. A more ambitious language could encode a full fledged temporal logic.
8.3.4
Implementation
The previous sections discussed extensions to the formalism underlying the checkpoint interface and the contracts derived from that interface. This section discusses further directions for the implementation, described in Section 6. The implementation presented in this thesis is intended as a prototype, and has not been tuned for performance. Such tuning is a necessary step to accurately evaluate the overhead of using contracts based on delimited checkpoints. As part of this, a more realistic implementation should implement optimized versions of important contracts where possible. For example, the noEffect contract can generally be implemented more efficiently by the runtime. Rather than start three nested transactions (one each for noRetry, noExn, and noWrite), an optimized implementation could start a single nested transaction that
Chapter 8: Conclusions and Future Work
192
supports all of the relevant functionality. This thesis is set in the context of Haskell. This is convenient, as it allows the formal semantics for delimited checkpoints to extend the previously defined semantics for STM Haskell. The STM implementation in GHC is also simple, and allowed for rapid prototyping of our implementation. Finally, Haskell makes it very convenient to build and use contracts derived from delimited checkpoints. However, this setting makes it difficult to compare to existing work on contracts. A reimplementation in Java would enable direct support for the Java Modeling Language, and provide a basis for building a runtime assertion checker with support for advanced features of the Java Modeling Language such as framing contracts. Additionally, it would be nice to implement delimited checkpoints in Racket (formerly called PLT Scheme). Racket has support for higher order assertions, which have been integrated into the standard libraries. Additionally, the Racket community has continued to research and implement support for contracts into the language. Integrating delimited checkpoints into Racket would be nice, since the Racket community has already shown itself interested in promoting the use of contracts. It is also a more natural fit for a dynamically typed language like Racket, than for a statically typed language like Haskell. As discussed in Section 8.2, providing an easy migration path to expressive contracts is a goal of the delimited checkpoint interface. However, we have not evaluated the success of delimited checkpoints at attaining that goal. The most important next step for delimited checkpoints is probably a large case study using the framework introduced in this dissertation. This would provide evidence for our belief that delimited checkpoints provide a useful interface.
Chapter 8: Conclusions and Future Work
193
Finally, we revisit delimitation, which was introduced to simplify the implementation. Future work should validate this assumption by implementing support for undelimited (first class) checkpoints, and comparing the performance impact and additional implementation complexity incurred. Section 6.4 sketched out how this could be done, and Section 7.1 presented some relevant related work, however building and evaluating such an implementation is beyond the scope of this thesis.
Appendix A Additional Proofs Chapter 5 elided the full proofs of some theorems for readability. More complete proofs are presented in this appendix. To make things simple to find, this Appendix’s structure mirrors that of Chapter 5, although it elides the parts that do not contain any theorems or lemmas. Note that the beginning of the Appendix spells out the straightforward proofs of some simple properties, going through all the case in the induction. Later, some of the simple proofs simply discuss the interesting cases in the induction, trusting that the reader can fill in the trivial cases.
A.1
Semantics of DC Haskell
This section simply provides the formal semantics of DC Haskell, so there is nothing to prove. However, subsections present some properties of the system as well as the derived delimited difference operators. These are presented below.
194
Appendix A: Additional Proofs
A.2
195
DC Haskell Properties
Section 5.2.1 starts off with a basic lemma about DC Haskell transitions (Lemma 5.2.1). This lemma is presented for checkpoint transitions, but also holds for STM transitions. We first present and prove the variant for STM transitions, and then proceed to the checkpoint version. Lemma A.2.1 (DC Haskell only extends traces (STM transitions)). ∗
If M ; Θ, ∆ ⇒ N ; Θ� , ∆� then there exists a ∆�� such that ∆� = ∆�� + +∆ Proof. This proof proceeds by induction over the derivation of the STM transition. For each case, the choice of ∆�� is explicated in terms of the variables uses in the inference rules in Figure 5.8. (AADMIN ): ∆�� = • (READ): ∆�� = !r : • (WRITE ): ∆�� = r ::= M : • (NEW ): ∆�� = r ← new M : • (XSTM1 ): ∆�� is the same as that given by the inductive hypothesis (XSTM2 ): ∆�� = filterw ∆� (for the ∆� given by the inference rule, not by our assumptions) (XSTM3 ): ∆�� = filterw ∆� (for the ∆� given by the inference rule, not by our assumptions) (OR1 ): ∆�� is the same as that given by the inductive hypothesis
Appendix A: Additional Proofs
196
(OR2 ): ∆�� is the same as that given by the inductive hypothesis (OR3 ): ∆�� = filterw ∆� (for the ∆� given by the inference rule, not by our assumptions)
Lemma A.2.2 (Lemma 5.2.1: DC Haskell only extends traces). ∗
If M ; Θ, ∆ ⇒ N ; Θ� , ∆� then there exists a ∆�� such that ∆� = ∆�� + +∆ Φ
Proof. This proof proceeds by induction over the derivation of the checkpoint transition. For each case, the choice of ∆�� is explicated in terms of the variables uses in the inference rules in Figure 5.9. (ASTM ): This follows from Lemma A.2.1, the equivalent lemma for STM transitions (ATCP ): ∆�� = filterw ∆� (for the ∆� given by the inference rule, not by our assumptions) (WITHCCP ): ∆�� = ∆� (for the ∆� given by the inference rule, not by our assumptions) (DELTACP ): ∆�� = •
Next, Section 5.2.1 defines wellformedness, ensuring that all references in the various structures can be found in the domain of the heap. We repeat the definition here, as well as provide proofs that DC Haskell transitions preserve wellformedness. Definition A.2.1 (Definition 5.2.1: Wellformedness). • Θ is well formed, if all r ∈ Θ is in dom Θ • M is well formed with respect to Θ, if all r ∈ M is in dom Θ
Appendix A: Additional Proofs
197
• ∆ is well formed with respect to Θ, if all r ∈ ∆ is in dom Θ • Φ is well formed with respect to Θ, if all r ∈ P hi is in dom Θ • M ; Θ, ∆ ⇒ N ; Θ� , ∆� is well formed if Θ is well formed, and M and ∆ are well formed with respect to Θ. Similarly, the same conditions ensure that the transitive closure of the transition is well formed. • M ; Θ, ∆ ⇒ N ; Θ� , ∆� is well formed if Θ is well formed, and M , ∆, and Φ are well Φ
formed with respect to Θ. Similarly, the same conditions ensure that the transitive closure of the transition is well formed. Lemma A.2.3 (Lemma 5.2.2: Wellformedness preserved by STM transitions). ∗
If M ; Θ, ∆ ⇒ N ; Θ� , ∆� and M , Θ and ∆ are well formed with respect to Θ, then N , Θ� and ∆� are wellformed with respect to Θ� . Proof. This proof is by straightforward induction, since the heap is only ever extended (Lemma A.3.5). The only time a new reference is introduced is in (NEW ), and that reference is also added in to the heap. Lemma A.2.4 (Lemma 5.2.3: Wellformedness preserved by CP transitions). ∗
If M ; Θ, ∆ ⇒ N ; Θ� , ∆� and M , Θ, ∆, and Φ are well formed with respect to Θ, then Φ
N , Θ and ∆ are wellformed with respect to Θ� . �
�
Proof. This proof is by straightforward induction, since the heap is only modified by (ASTM ), which is handled by Lemma 5.2.3 (Lemma A.2.4). Next, Section 5.2.1 presents some basic properties of the auxiliary definitions. These are reproduced here, along with proofs.
Appendix A: Additional Proofs
198
Lemma A.2.5 (Lemma 5.2.4: Trace composition does not remove references). dom Θ ⊆ dom Θ[∆] Proof. This proof proceeds by induction over ∆. •: Trivial, since Θ[•] = Θ R ::= M : ∆rest : Since dom Θ ⊆ dom Θ[∆rest ] by the inductive hypothesis, and dom Θ[∆rest ] ⊆ dom Θ[∆rest ][r �→ M ] !R : ∆rest : : Follows immediately from the inductive hypothesis R ← new M : ∆rest : Follows immediately from the inductive hypothesis d ← save : ∆rest : Follows immediately from the inductive hypothesis
Lemma A.2.6 (Lemma 5.2.5: Trace composition distributes over append). Θ[∆1 + +∆2 ] = (Θ[∆2 ]) [∆1 ] Proof. This proof proceeds by induction over ∆1 . •: Trivial, since Θ[• + +∆2 ] = Θ[∆2 ] = (Θ[∆2 ]) [•] δ : ∆rest : Since Θ[δ : ∆rest + +∆2 ] = Θ[δ : (∆rest + +∆2 )] • If δ is r ::= M , then this equals Θ[∆rest + +∆2 ][r �→ M ]. By the induction hypothesis, this is the same as ((Θ[∆2 ]) [∆rest ]) [r �→ M ], which in turn is just (Θ[∆2 ]) [r ::= M : ∆rest ].
Appendix A: Additional Proofs
199
• Otherwise, if δ is not a write trace action, then it equals Θ[∆rest + +∆2 ]. By the induction hypothesis this is the same as (Θ[∆2 ]) [∆rest ], which in turn is just (Θ[∆2 ]) [δ : ∆rest ].
Lemma A.2.7 (Lemma 5.2.6: Filtering distributes over trace append). filterw (∆1 + +∆2 ) ≡ filterw ∆1 + +filterw ∆2 Proof. This proof proceeds by induction over ∆1 , and is very similar to the proof of Lemma A.2.6. •: Trivial, since filterw (• + +∆2 ) = filterw ∆2 = filterw ∆1 + +filterw ∆2 δ : ∆rest : Since filterw (δ : ∆rest + +∆2 ) = filterw (δ : (∆rest + +∆2 )) • If δ is r ::= M , then this equals filterw (∆rest + +∆2 ). By the induction hypothesis, this is the same as the expression filterw ∆rest + +filterw ∆2 , which is is the same as filterw (δ : ∆rest ) + +filterw ∆2 . • If δ is !r, r ← new M , or d ← save, this is δ : (filterw (∆rest + +∆2 )). By the induction hypothesis, this is δ : (filterw ∆rest + +filterw ∆2 ), which is the same as filterw (δ : ∆rest ) + +filterw ∆2 .
Appendix A: Additional Proofs
200
Lemma A.2.8 (Lemma 5.2.7: Filtering negates a trace’s effect on the heap).
Θ[filterw ∆] ≡ Θ Proof. This proof proceeds by induction on ∆ •: Trivial, since Θ[filterw •] = Θ[•] = Θ R ::= M : ∆rest : Θ[filterw R ::= M : ∆rest ] = Θ[filterw ∆rest ], which by the induction hypothesis is just Θ !R : ∆rest : : Θ[filterw !R : ∆rest ] = Θ[!R : filterw ∆rest ] = Θ[filterw ∆rest ] which by the induction hypothesis is just Θ R ← new M : ∆rest : Θ[filterw R ← new M : ∆rest ] = Θ[R ← new M : filterw ∆rest ] = Θ[filterw ∆rest ] which by the induction hypothesis is just Θ d ← save : ∆rest : Θ[filterw d ← save : ∆rest ] = Θ[d ← save : filterw ∆rest ] = Θ[filterw ∆rest ] which by the induction hypothesis is just Θ
Appendix A: Additional Proofs
A.2.1
201
Delimited Difference: DCd Haskell
The full DC Haskell system allows computations to compare any two checkpoints. In practice, however, most contracts only need a very stylized use of the difference operators: they run a computation, and analyze the impact of that computation. This stylized use is captured in the deltaCPd delimited difference functions introduced in Section 3.1. Section 5.2.2 formalizes this family of functions and states some of their properties. This section restates these properties and provides their proofs. Definition A.2.2 (Definition 5.2.2: Definition of difft). difftW RAF (∆) = diff W RAF (∆ + +d : •, d : •) for some d �∈ ∆ Lemma A.2.9 (Lemma 5.2.9: difft is well defined). For any ∆� , where d�� ∈ ∆� implies that d�� �∈ ∆, d �∈ ∆, difftW RAF (∆) = diffW RAF (∆ + +d : ∆� , d : ∆� ) Proof. The proof will be for the WRA variants. The footprint variant follows by appealing to this lemma for each of its constituent invocations of difft. Expanding the definition of difft, this Lemma states that for d, d� �∈ ∆, diff W RA (∆ + +d : •, d : •) = diff W RA (∆ + +d� : ∆� , d� : ∆� ) This proof proceeds by induction on ∆ •: diff W RA (• + +d : •, d : •) = diff W RA (d : •, d : •) =•
Appendix A: Additional Proofs
202
and similarly, diff W RA (• + +d� : ∆� , d� : ∆� ) = diff W RA (d� : •, d� : ∆� ) =• d�� ← save : ∆rest : By assumption, d�� cannot be d or d� . Similarly, by assumption d�� �∈ ∆� . Therefore, diff W RA (d�� ← save : ∆rest + +d : •, d : •) = diff W RA (∆rest , d : •) and similarly, diff W RA (d�� ← save : ∆rest + +d� : •, d� : •) = diff W RA (∆rest , d� : •) And so by the induction hypothesis, we conclude. δ : ∆rest : If δ is not a checkpoint save trace action (d ← save), then it is either of the requested action type or it is not. If it is, we will assume that r is the relevant reference. Then diff W RA (δ : ∆ + +d : •, d : •) = diff W RA (δ : (∆ + +d : •), d : •) = Cons r diff W RA ((∆ + +d : •), d : •) and similarly diff W RA (δ : ∆ + +d� : ∆� , d� : ∆� ) = diff W RA (δ : (∆ + +d� : ∆� ), d� : ∆� ) = Cons r diff W RA ((∆ + +d� : ∆� ), d� : ∆� ) from whence we use the induction hypothesis to conclude.
Appendix A: Additional Proofs
203
Similarly, if δ is not of the requested action type. then diff W RA (δ : ∆ + +d : •, d : •) = diff W RA (δ : (∆ + +d : •), d : •) = diff W RA ((∆ + +d : •), d : •) and similarly diff W RA (δ : ∆ + +d� : ∆� , d� : ∆� ) = diff W RA (δ : (∆ + +d� : ∆� ), d� : ∆� ) = diff W RA ((∆ + +d� : ∆� ), d� : ∆� )
Lemma A.2.10 (Lemma 5.2.10: difft distributes over append). For any ∆, ∆� , difftW RAF (∆� + +∆) = diffW RAF (∆� ) + +diffW RAF (∆) Proof. This proof proceeds by induction on ∆� , and is very similar to the proofs of Lemmas A.2.6 and A.2.9. As before, the • base case is trivial. When the current head of a nonlist trace is a checkpoint save trace action, it must be (by the definition of difft skipped), and the induction hypothesis then yields the result. Otherwise, if the action is the requested one, it is added to the returned list. If it is not the requested one, it is skipped. Either way, the induction hypothesis applies (noting that Cons x (a + +b) = (Cons a x) + +b in the former case).
Lemma A.2.11 (Lemma 5.2.8: Inference rule for deltaCPd).
Appendix A: Additional Proofs
204
The derived contracts deltaWCP, deltaRCP, deltaACP and deltaFCP have the following inference rule(s). Furthermore, if delta[WRAF]CFd p transitions, then this rule must have been applied (and so is invertible). ∗
P ; Θ, ∆ ⇒ return N ; Θ� , ∆� + +∆ Φ
(DELTACPD)
∗
delta[WRAF]CPD P ; Θ, ∆ ⇒ return(difftW RAF (∆� )), N ); Θ� , ∆� + +∆ Φ
Proof. The forward direction follows immediately by expanding the definitions and using Lemma 5.4.6 to run the given transition with the new checkpoint added in by the use of withCCP.
Similarly, the inverse direction follows since the inference rules of DC Haskell are generally invertible.
A.3
Conservative Extension
Section 5.3 stated and gave the intuition behind a theorem (Theorem 5.3.1 relating STM Haskell and DC Haskell. When checkpoint operations are not used, programs behave the same under both semantics. To provide a more complete proof, we need a way to differentiate the rules of STM Haskell given in Section 5.1 and those of DC Haskell given in 5.2. To do this, we will use a hash sign (#) postfix superscript to indicate that something is an element of the STM semantics. We also assume a lifting operator, written with an overline (as in M # ) that injects STM Haskell elements into DC Haskell elements as appropriate. Finally, we will abbreviate STM Haskell as STMH and DC Haskell as DCH. Note that it is possible to separate out the checkpoint related operations. In particular,
Appendix A: Additional Proofs
205
the (extended) semantics only introduce checkpoint related stuff in response to existing checkpoint related stuff. This is formalized by Lemma A.3.1. Lemma A.3.1 (Checkpoints do not appear out of nowhere). a
If P # ; Θ# → − Q; Θ� then there exists, Q# , Θ� # such that Q = Q# and Θ� = Θ� # . Proof. Straightforward by inspection of the rules (technically proceeding by induction over the rules). The only ones that introduce checkpoints or checkpoint related syntax start with something checkpoint related. For the Checkpoint transition relations, the only possibly applicable one is that lifting STM transitions into DC transitions. For STM transitions, see the analogous Lemma A.3.7, which appears below. Given this notation, we can now restate Theorem 5.3.1 more succinctly. Theorem A.3.2 (Theorem 5.3.1: DC Haskell is a conservative extension of STM Haskell).
a #
a
P # ; Θ# → − Q; Θ� # if and only if P # ; Θ# → − Q# ; Θ� # . To show this, we will first state and prove a similar lemma about STM transitions in both semantics. To allow the induction to go through, we actually need to strengthen this somewhat, and prove the lemma about the transitive closure of both relations. It is also simpler to split the lemma up, stating each direction separately. Lemma A.3.3 (STM transitions in STM Haskell are in DC Haskell). ∗ #
Given M # , Θ# , ∆# , N # , Θ� # , and∆� # such that M # ; Θ# , ∆# ⇒ N # ; Θ� # , ∆� # and dom ∆# ⊆ dom Θ# , and given any Θ, ∆, such that • dom Θ[∆] = dom Θ (Trace does not contain new references) and
Appendix A: Additional Proofs
206
• ∆# ⊆ Θ (The old STMH allocations are in the DCH heap) and � � • dom ∆� # \ ∆# ∩ dom Θ = ∅ (The new STMH allocations are not in the old DCH heap) and
• Θ[∆] = Θ# (The STMH heap is the DCH heap composed with its trace (this operations is defined in Figure 5.10)), there exists ∆� such that for Θ� = Θ ∪ ∆� # \ ∆# • dom Θ� [∆� ] = dom Θ� (Trace does not contain new references) and • Θ� [∆� ] = Θ� # (STMH heap is DCH heap composed with DC trace) and ∗
• M # ; Θ, ∆ ⇒ N # ; Θ� , ∆� (DCH gives the same result as STMH). ∗ #
Proof. We proceed by rule induction on a derivation of M # , Θ# , ∆# ⇒ N # ; Θ� # , ∆� # . Transitive Case: If it was formed by a transitive step, we have that ∗ #
• M # , Θ# , ∆# ⇒ N �� # ; Θ�� # , ∆�� # and ∗ #
• N �� # , Θ�� # , ∆� # ⇒ N �� # ; Θ�� # , ∆�� # . Using the inductive hypothesis, there exists ∆�� such that • dom Θ�� [∆�� ] = dom Θ�� • Θ�� [∆�� ] = Θ�� # ∗
�
• M # ; Θ, ∆ ⇒ N � # ; Θ�� , ∆�� Using Lemma A.3.6 (STMH allocations remain in the heap) we can then apply the inductive hypothesis to the second part of the transitive step, using ∆�� to derive that there exists ∆� such that
Appendix A: Additional Proofs
207
• dom Θ� [∆� ] = dom Θ� • Θ� [∆� ] = Θ� # and ∗
• N �� # ; Θ�� , ∆�� ⇒ N � # ; Θ� , ∆� The goal is then shown by using transitivity to stitch the two DCH relations together. Base case of transitive closure: If the transitive closure is formed by a single STM step, then we proceed by cases. (AADMIN ): M # is of the form S[M ]. Since the administrative relation is identical in both semantics, this case is straightforward, setting ∆� = ∆. (READ): M # is of the form S[readTVar r]. Since r ∈ dom Θ# , r ∈ dom Θ# , and so r ∈ Θ[∆] by assumption. Therefore, we invoke DCH’s (READ) with ∆� = !r : ∆. Note that Θ[∆� ] = Θ[∆] by definition. (WRITE ): M # is of the form S[writeT V arrM ]. As before, since r ∈ dom Θ, r ∈ dom Θ# , and so r ∈ Θ by assumption. Therefore, we invoke DCH’s (WRITE ) with ∆� = r ::= M : ∆. Note that Θ[∆� ] = Θ[∆][r �→ M ] by definition. (NEW ): M # is of the form S[writeTVar r M ]. Similar to before, since r �∈ dom Θ# , r �∈ dom Θ# , and so r �∈ Θ[∆] by assumption. Therefore, we invoke DCH’s (ALLOC ) with ∆� = r ← new M : ∆. Note that ∆# [r �→ M ] ⊆ Θ[r �→ M ] holds since ∆# ⊆ Θ by assumption. Also note that Θ� [∆� ] = Θ[r �→ M ][∆] by definition. Since r �∈ dom Θ[∆], Θ[r �→ M ][∆] = Θ[∆][r �→ M ] by Lemma A.3.4, yielding the necessary conditions.
Appendix A: Additional Proofs
208
(XSTM1 ): M # is of the form S[catch M N ] (and returns normally). By inversion, we get the relation on M # given by the rule, and use the inductive hypothesis to lift it to DCH. We then apply the (XSTM1 ) rule of DCH, passing in the same choice for ∆� . Note that ∆� # ⊆ Θ� by the inductive hypothesis and ∆# ⊆ Θ, so since Θ ⊆ Θ� by Lemma A.3.5 (DCH STM heap monotonicity), ∆# ⊆ Θ� and so ∆# ∪ ∆� # ⊆ Θ� . (XSTM2 ): M # is of the form S[catch M N ] (and catches an exception). As before, By inversion, we get the relation on M # given by the rule, and use the inductive hypothesis to lift it to DCH. We then apply the (XSTM2 ) rule of DCH, setting ∆� = ∆. We need to show that Θ� [filterw ∆� + +∆] = Θ# ∪ ∆� # . By Lemmas 5.2.7 and 5.2.6, Θ� [filterw ∆� + +∆] = Θ� [∆]. By definition, Θ� [∆] = Θ ∪ ∆� # \ ∆# [∆]. Since we know by assumption that ∆� # \ ∆# is disjoint from Θ, and we know that ∆ only acts on elements in Θ (since dom Θ[∆] = dom Θ, we can commute the operations using Lemma A.3.4 to get Θ� [∆] = Θ[∆] ∪ ∆� # \ ∆# . Since we know by assumption that Θ[∆] = Θ# , this reduces to Θ� [∆] = Θ# ∪ ∆� # \ ∆# . Since we also know by assumption that ∆# ⊆ Θ# , this further reduces to our goal: Θ� [∆] = Θ# ∪ ∆� # . (XSTM3 ): M # is of the form S[catch M N ] (and propagates a retry). By inversion, we get the relation on M # given by the rule, and use the inductive hypothesis to lift it to DCH. We then apply the (XSTM3 ) rule of DCH, setting ∆� = ∆. The rest follows trivially by assumption. Recall that (as in the (XSTM2 ) case), by Lemmas 5.2.7 and 5.2.6, Θ[filterw ∆� + +∆] = Θ[∆]. (OR1 ): M # is of the form S[M1 ‘orElse‘ M2 ] (and returns normally).
Appendix A: Additional Proofs
209
This case follows from inversion, followed by applying DCH’s (OR1 ) rule with the ∆� given by the inversion. (OR2 ): M # is of the form S[M1 ‘orElse‘ M2 ] (and throws an exception). This case again follows from inversion, followed by applying DCH’s (OR2 ) rule with the ∆� given by the inversion. (OR3 ): M # is of the form S[M1 ‘orElse‘ M2 ] (and propagates a retry). This case again follows from inversion, followed by applying DCH’s (OR3 ) rule, this time with ∆� = ∆. Recall that (as in the (XSTM3 ) case), by Lemmas 5.2.7 and 5.2.6, Θ[filterw ∆� + +∆] = Θ[∆].
This proof used some lemmas, stated and proven here: Lemma A.3.4 (Disjoint Extension and Trace Composition Commute). For all Θ, ∆, r, M such that r �∈ dom Θ[∆], Θ[∆][r �→ M ] = Θ[r �→ M ][∆] Proof. This follows by induction on ∆. The only interesting case is for a write trace action r� ::= M � , but the disjointness condition guarantees that r� �= r, and so these operations commute. Lemma A.3.5 (DCH STM transition extends heap monotonically). Given M ; Θ, ∆ ⇒ N ; Θ� , ∆� , Θ ⊆ Θ� . And similarly for the transitive closure of the STM relation. Proof. This follows by inspection of DCH’s STM rules (technically by induction on their transitive closure).
Appendix A: Additional Proofs
210
The only rule that modifies the heap is (NEW ), which adds in a mapping for a reference that must be fresh. Lemma A.3.6 (STMH STM transition allocations in heap). ∗ #
Given M # ; Θ# , ∆# ⇒ N # ; Θ� # , ∆� # such that ∆# ⊆ Θ# , it follows that ∆� # ⊆ Θ� # . And similarly for the transitive closure of the STM relation. Proof. This follows by inspection of STMH’s STM rules (technically by induction on their transitive closure). The only rule that modifies the domain of either the heap or the allocation set is (NEW ), which extends both the heap and the allocation map with the same binding. This concludes the proof that given an STMH STM transition, we can find an appropriate DCH STM transition with the same result. For completeness, we now need to state and prove the other direction: that a DCH STM transition over lifted (non checkpoint related) values can be simplified to an STMH STM transition. We first note that we can define a “checkpointfree” trace as one that does not include any d ← save actions and whose allocation and write actions do not include any checkpoints. Since these traces can be defined/interpreted under the old semantics, we would like to use ∆# to refer to them, but this conflicts with the existing use of ∆# to refer to the allocation maps of the STMH semantics. To disambiguate, we will instead call these ∆# to indicate that they are traces (from the DCH semantics) which are compatible with the STMH semantics. Note hat composition with a checkpoint free heap yields a checkpoints free heap. Using this, we can state the lemma cited above in the proof of Lemma A.3.1. Checkpoints do not appear unless checkpoint related operations are used.
Appendix A: Additional Proofs
211
Lemma A.3.7 (Checkpoints do not appear out of nowhere (STM transitions)). If P # ; Θ# ∆# ⇒ a Q; Θ� , ∆� then there exists, Q# and Θ� # such that Q = Q# and Θ� = Θ� # . Additionally, ∆� is checkpointfree. Proof. This lemma follows immediately by inspection. Technically, it uses induction on the STM transition derivation, but since none of the rules ever introduce a checkpoint, the details are trivial. Finally, using our notation for “checkpointfree” traces, and equipped with this lemma, we can state the backward part of our relation between STM transitions in the two semantics. Lemma A.3.8 (STM transitions in DC Haskell are in STM Haskell). ∗
Given M # ; Θ# , ∆# ⇒ N # ; Θ� # , ∆� # , then for any ∆# , #
#
# # ∗
#
�#
�
#
�
M ; Θ [∆# ], ∆ ⇒ N ; Θ [∆ # ], ∆ ∪ Θ
�#
\Θ
#
�
Proof. This proof is similar in structure and spirit as the proof of the other direction of this relation, Lemma A.3.3. ∗
We proceed by rule induction on a derivation of M # ; Θ# , ∆# ⇒ N # ; Θ� # , ∆� # , Transitive Case: If it was formed by a transitive step, we have that ∗
• M # ; Θ# , ∆# ⇒ N �� ; Θ�� , ∆�� and ∗
• N �� ; Θ�� , ∆�� ⇒ N # ; Θ� # , ∆� # Using Lemma A.3.7 (checkpoints do not manifest from nowhere), we can rewrite this as
Appendix A: Additional Proofs
212
∗
• M # ; Θ# , ∆# ⇒ N �� # ; Θ�� # , ∆�� # and ∗
• N �� # ; Θ�� # , ∆�� # ⇒ N # ; Θ� # , ∆� # Applying the inductive hypothesis, we obtain that for any ∆# , #
# # ∗
#
• M ; Θ [∆# ], ∆ ⇒ N • N
�� #
�� #
��
#
�� #
�
;Θ
�� #
�� #
[∆ #
��
# ], ∆
�
#
∗ #
�
∪ Θ
�� #
\Θ
#
�
∪ Θ \Θ ⇒ � � �� � � N # ; Θ� # [∆� # ], ∆# ∪ Θ�� # \ Θ# ∪ Θ� # \ Θ# ;Θ
[∆
# ], ∆
Putting these together with transitivity, we obtain � � �� � � # # # # ∗ # �# � # �� # # �# # M ; Θ [∆# ], ∆ ⇒ N ; Θ [∆ # ], ∆ ∪ Θ \ Θ ∪ Θ \Θ
Since Θ�� # ⊆ Θ� # by Lemma A.3.5 (DCH STM transitions only add to the heap), this � � # # # # ∗ # �# � # �# # reduces to M ; Θ [∆# ], ∆ ⇒ N ; Θ [∆ # ], ∆ ∪ Θ \ Θ , proving this case.
Base case of transitive closure: If the transitive closure is formed by a single STM step,
then we proceed by cases. (AADMIN ): M # is of the form S[M # ]. Since the administrative relation is identical in both semantics, this case is straightforward. (READ): M # is of the form S[readTVar r# ]. Since r# ∈ dom Θ# [[# ∆]], r# ∈ dom Θ# [∆# ]. Therefore, we invoke STM’s (READ). Note that the DCH rules did not change the heap, and the change in the trace does not affect its composition with the heap. (WRITE ): M # is of the form S[writeTVar r# M # ]. Since r# ∈ dom Θ# , r# ∈ dom Θ# , and so is in r# ∈ dom Θ# [∆# ] by Lemma 5.2.4. Therefore, we invoke STM’s (WRITE ). Note that the DCH rules did not change the heap,
Appendix A: Additional Proofs
213
and the change in the trace affects its composition with the heap by extending it with the requested mapping. (NEW ): M # is of the form S[newTVar M # ]. Since (for the returned fresh reference r# ) r# �∈ dom Θ# [[# ∆]], r# �∈ dom Θ# [∆# ]. Therefore, we invoke STM’s (NEW ). Note that the DCH rules add the allocation into the heap, so the difference between the initial and final heaps is just this allocation, exactly as tracked in the STMH allocation mapping. Also note that the change in the trace does not affect its composition with the heap (although the composition was correctly changed by the change in the heap). (XSTM1 , XSTM2 ), and (XSTM3 ): M # is of the form S[catch M # N # ]. By inversion, we get the relation on M # given by the rule, and use the inductive hypothesis to lower it to STM. We then apply the appropriate (XSTM1 , XSTM2 ) or (XSTM3 ) rule of STM, and everything works out nicely. For (XSTM2 ) and (XSTM3 ), note that by Lemmas 5.2.7 and 5.2.6, Θ[filterw ∆� + +∆] = Θ[∆] for any Θ. (OR1 , OR2 ), and (OR3 ): M # is of the form S[M1 # ‘orElse‘ M2 # ]. By inversion, we get the relation on M # given by the rule, and use the inductive hypothesis to lower it to STM. We then apply the appropriate (OR1 , OR2 ) or (OR3 ) rule of STM, and everything again works out nicely. For (OR3 ), note that by Lemmas 5.2.7 and 5.2.6, Θ[filterw ∆� + +∆] = Θ[∆].
Finally, now that we have proven that we can go back and forth between STMH and DCH STM transitions, we can prove the corresponding theorem for top level IO transitions, demonstrating that DC Haskell is a conservative extension to STM Haskell.
Appendix A: Additional Proofs
214
Proof of Theorem A.3.2: DC Haskell is a conservative extension of STM Haskell. For the forward direction (from STMH to DCH), we proceed by rule induction on the derivation of the IO transition. The only interesting cases are (ARET ) and (ATHROW ), the rest are exactly the same in both semantics. For both of these cases, we apply the forward direction of the analogous lemma for STM transitions, Lemma A.3.3, using the empty trace and keeping the same heap (which trivially satisfy all the requirements of the lemma). Technically the conclusion of this lemma is a transitive closure of the STM relation, but we need a transitive closure of the delimited checkpoint relation. We can simply map uses of (ASTM ) across the transitive closure, lifting each STM relation into a checkpoint relation. This can then be immediately used with DCH’s (ARET ) or (ATHROW ), as appropriate. For the backward direction (from DCH to STMH), we again proceed by rule induction on the derivation of the IO transition, and again note that (ARET ) and (ATHROW ) are the only interesting cases. The premises of both cases are checkpoint relations. Since, by assumption, the expressions do not contain any checkpoint related operations, the only rule that can apply is (ASTM ). We can then apply Lemma A.3.8 (the backward direction of the analogous lemma for STM transitions). Finally, we take the conclusion of this lemma and apply STMH’s (ARET ) or (ATHROW ), as appropriate.
A.4
Taking Out the Garbage
Section 5.4 presents theorems generally related to garbage collection. In particular, Section 5.4.1 discusses how garbage collection and freshness interact. The discussion there is already fairly detailed, and does not need further elaboration. Section 5.4.2 discusses how
Appendix A: Additional Proofs
215
garbage collecting elements of the various structures affects the transition relations of DC Haskell (as well as the restricted DCd Haskell).
A.4.1
Garbage in the Input Structures
Section 5.4.2 discusses how garbage can be defined for the input heap, trace, and checkpoint map. Adding or removing all such garbage is proven to preserve the transition relations of either DC Haskell, or in some cases, the restricted DCd Haskell (see Section 5.2.2. The proofs given in Section 5.4.2 are proof sketches. These theorems are reproduced here with more complete proofs. Garbage in the Input Heap Lemma A.4.1 (Lemma 5.4.1: Adding extra references to the heap (STM transitions)). For any Θg such that forall r ∈ dom Θg , r �∈ P, Θ, ∆, Θ� , ∗
P ; Θ, ∆ ⇒ N ; Θ� , ∆� if and only if ∗
P ; Θ[Θg ], ∆ ⇒ N ; Θ� [Θg ], ∆� where Θ[Θg ] is the iterated extension of Θ with all the mappings in Θg . Proof. This proof proceeds by induction over the derivation of the STM transition. (AADMIN ): Trivial, since this rule does not constrain the heap in any way. (READ): Assuming that reference r is read, it must be in P . Then, by assumption, � r � Θg , so Θ[∆](r) = Θ[Θg ][∆](r).
Appendix A: Additional Proofs
216
(WRITE ): Assuming that reference r is written, it must be in P . Therefore, adding the binding for r to the heap commutes with the addition of Θg to the heap, so Θ[r �→ M ][Θg ] = Θ[Θg ][r �→ M ]. (NEW ): Technically the heap with and without Θg enforce different freshness conditions, but we can appeal to the discussion in Section 5.4.1 to avoid this problem. In the resulting heap, since r �∈ Θg , Θ[r �→ M ][Θg ] = Θ[Θg ][r �→ M ] just like in the (WRITE ) case. (XSTM1 ), (XSTM2 ), (XSTM3 ), (OR1 ), (OR2 ), and (OR3 ) : These cases can immediately appeal to the induction hypothesis, since they do not otherwise use the heap.
Lemma A.4.2 (Lemma 5.4.2: Adding extra references to the heap (checkpoint transitions)). For any Θg such that forall r ∈ dom Θg , r �∈ P, Θ, ∆, Θ� , Φ, ∗
P ; Θ, ∆ ⇒ N ; Θ� , ∆� Φ
if and only if ∗
P ; Θ[Θg ], ∆ ⇒ N ; Θ� , ∆� Φ
where Θ[Θg ] is the iterated extension of Θ with all the mappings in Θg . Proof. This proof proceeds by induction over the derivation of the checkpoint transition. (ASTM ): This case follows immediately from Lemma 5.4.1 (Lemma A.4.1). (ATCP ): This case follows immediately from the induction hypothesis.
Appendix A: Additional Proofs
217
(WITHCCP ): The heap is only used in the freshness criterion. As usual, we appeal to the discussion in Section 5.4.1 to address that use, and so the case follows from the induction hypothesis. (DELTACP ): This case is trivial as it does not constrain the heap in any way.
Lemma A.4.3 (Lemma 5.4.3: Adding extra references to the heap (IO transitions)). For any Θg such that forall r ∈ dom Θg , r �∈ P, Θ, ∆, Θ� , Φ, a
P ; Θ, → − N ; Θ� if and only if a
P ; Θ[Θg ] → − N ; Θ� , where Θ[Θg ] is the iterated extension of Θ with all the mappings in Θg . Proof. This proof proceeds by cases. (PUTC ), (GETC ), (CATCH1 ), (CATCH2 ), (ADMIN ), and (FORK ) : None of these cases constrain the heap, so the result is immediate. (ARET ) and (ATHROW ) : These case follow from Lemma A.4.2.
Garbage in the Input Trace Lemma A.4.4 (Lemma 5.4.4: Input trace only used for lookup (STM)). If ∗
M ; Θ, ∆ ⇒ N ; Θ� , ∆� + +∆
Appendix A: Additional Proofs
218
Then forall ∆�� such that f orallr ∈ Θ, where ∆� = ∆pre + +!r : ∆post for some ∆pre and ∆post we have that Θ[∆](r) = Θ[∆�� ](r), then ∗
M ; Θ, ∆�� ⇒ N ; Θ� , ∆� + +∆�� Proof. This proof proceeds by induction over the derivation of the STM transition. (AADMIN ): This case is trivial as it does not constrain the trace. (READ): ∆� = !r : •, so r ∈ ∆� , so Θ[∆](r) = Θ[∆�� ](r) by assumption (and similarly for the domain assertion). (WRITE ): This case is trivial since it does not constraint the trace. (NEW ): ∆� = r ← new M : •, so r ∈ ∆� , so r ∈ dom Θ[∆] if and only if r ∈ dom Θ[∆�� ] by assumption. (XSTM1 ), (XSTM2 ), (XSTM3 ), (OR1 ), (OR2 ), and (OR3 ) : These cases all follow straightforwardly from the induction hypothesis.
Now, we want to extend this result to checkpoint transitions. As discussed in Section 5.4.2, the equivalent theorem only holds for the restricted DCd Haskell, which only allows the use of the delimited difference functions (not the unrestricted ones). However, to prove the theorem (Theorem 5.4.5), it first needs to be strengthened. Observing that the (withCCP ) and (atCP ) rules together allow the input trace to move into the checkpoint map, and vice versa, the theorem needs to allow for equivalent traces in both the input trace and the checkpoint map. To do this, we first introduce the notion of
Appendix A: Additional Proofs
219
“close enough” traces (relative to the output trace and the input heap). This definition was essentially inlined into the original statement of the theorem. Now that it is needed for both the input trace and the traces stored in the checkpoint map, it is broken out as an explicit definition. It is then used to ensure that all the input traces (both the explicit input trace and the ones stored in the checkpoint map) are close enough with respect to the output trace. The original theorem (Theorem 5.4.5) follows immediately, since a trace is clearly “close enough” to itself, so the checkpoint map is also close enough to itself. Theorem A.4.5 (Theorem 5.4.5 (strengthened): Input trace and checkpoint map only used for lookup). If (in the restricted DCd Haskell) ∗
M ; Θ, ∆ ⇒ N ; Θ� , ∆� + +∆ Φ
For any two traces ∆1 , ∆2 , let us say that ∆2 is close enough to ∆1 if forall r, where ∆� = ∆pre + +!r : ∆post for some ∆pre and ∆post , we have that Θ[∆1 ](r) = Θ[∆2 ](r). Then forall ∆�� such that ∆�� is close enough to ∆, and forall Φ�� such that dom Φ�� = dom Φ and forall d ∈ dom Φ, Φ�� (d) is close enough to Φ(d), ∗
M ; Θ, ∆�� ⇒�� N ; Θ� , ∆� + +∆�� Φ
Proof. This proof proceeds by induction over the derivation of the checkpoint transition. (ASTM ): This case follows immediately from Lemma 5.4.4 (Lemma A.4.4). (ATCP ): This case does not actually constrain ∆, so it follows immediately from the induction hypothesis. Note that Φ[d] is close enough to Φ�� [d] by assumption (since they are in the checkpoint map).
Appendix A: Additional Proofs
220
(WITHCCP ): This case follows from the induction hypothesis. Note that since the input traces are close enough, putting them in the checkpoint map preserves the property that the checkpoint maps are close enough. (DELTACPD): Note that (as discussed) this does not hold for (DELTACP ), since traces in the checkpoint map only have to be “close enough”, which is insufficient to ensure that the diff helper returns the same result. However, the result does hold for the restricted (DELTACPD) rule (Lemma 5.2.8), and in fact follows immediately from the induction hypothesis.
Garbage in the Checkpoint map Finally, we come to the last of the input structures, the checkpoint map. Like the heap, the checkpoint map is insensitive to the presence or absence of checkpoints that are not used. This is formalized as Lemma 5.4.6, which is duplicated here as Lemma A.4.6 with a more detailed proof. Lemma A.4.6 (Lemma 5.4.6: Adding extra checkpoints). For any d �∈ P, Θ, ∆, dom Φ, N, Θ� , ∆� , ∗
P ; Θ, ∆ ⇒ N ; Θ� , ∆� Φ
if and only if P ; Θ, ∆
∗
⇒
Φ[d�→∆��
N ; Θ� , ∆ �
Proof. This proof proceeds by induction over the derivation of the checkpoint transition.
Appendix A: Additional Proofs
221
(ASTM ): Trivial, since this rule does not constrain the checkpoint map in any way. (ATCP ): The checkpoint map is only used to look up the given checkpoint, which cannot be the same as d (since d �∈ P ) and is unaffected by the presence or absence of d in the map, and so we can apply the induction hypothesis. (WITHCCP ): This rule only uses the checkpoint map to determine freshness of the newly allocated checkpoint. The discussion in Section 5.4.1 discusses how to resolve that problem, leaving us able to apply the induction hypothesis. (DELTACP ): Since d �∈ P , d cannot be either checkpoint used in the transition relation. The relation only uses the checkpoint map to look up the two given checkpoints, an operation that is unaffected by the presence or absence of other checkpoints (such as d) in the checkpoint map.
A.5
Erasure
Section 5.5 discusses the complications surrounding erasing contracts. In particular, it limits what set of observations the program is allowed to make, in order to differentiate amongst program states.
A.5.1
Definition
Section 5.5.1 formally defined the “erases to” relation, and states a theorem about how this definition behaves under substitution. This definition and theorem are reproduced here,
Appendix A: Additional Proofs
222
along with a proof of the theorem. Definition A.5.1 (Definition 5.5.1: Erasableto). M1 is “erasableto” M2 if for all wellformed input structures, where N ∈ {return N � , throw N � , retry}, ∗
M1 ; Θ, ∆ ⇒ N ; Θ� , ∆� Φ
implies that ∗
M2 ; Θ, ∆ ⇒ N ; Θ�2 , ∆�2 Φ
for some Θ�2 = Θ� [Θg ] such that forall r ∈ dom Θg , r �∈ N, Θ, ∆� , Θ� and some ∆�2 that is well formed with respect to Θ�2 and such that forall r ∈ Θ� , Θ�2 [∆�2 ] = Θ� [∆� ]. The equivalent statements should also hold for STM, IO, and administrative transitions. Theorem A.5.1 (Theorem 5.5.1: Substitution under “Erasable to”). � is erasableto N � , then P is erasableto P [N � /M � ] as long as P does For any P , if M � subterms. not use the checkpoint functions outside of the M Proof. • For the STM relation this follows by inspection (technically induction). All the rules are clearly preserved under the substitution. For the transitive closure, we can appeal to the garbage collection Lemmas A.4.1 and A.4.4. • For the checkpoint transitions this follows trivially, since P is not allowed to use any checkpoint related functions. • For the top level IO transition, this follows from the garbage collection Lemma A.4.3.
Appendix A: Additional Proofs
A.5.2
223
Erasing Contracts
Using the preceding definition (Definition A.5.1, Section 5.5.2 proceeds to give an erasability result for atCP, as well as a general inversion lemma for the assertM contract. Lemma A.5.2 (Lemma 5.5.2: Erasing atCP). Given some fixed C (which is independent of the heap, then if ∗
atCP d M; Θ, ∆ ⇒ N ; Θ� , ∆� Φ
implies that N = return C then atCP d M erases to return C Similarly if N is always throw C for a fixed C, or is always retry. Proof. This follows from the rule for atCP, noting that by Lemmas 5.2.7 and 5.2.6, Θ[filterw ∆� + +∆] = Θ[∆]
Lemma A.5.3 (Lemma 5.5.3: Inversion of assertM). For N ∈ {return N � , throw N � , retry}, if ∗
(assertM P ) ; Θ, ∆ ⇒ N ; Θ� , ∆� + +∆ Φ
Then N = return (), and for some ∆�� , ∆� = filterw ∆��
∗
P ; Θ, ∆ ⇒ return (); Θ� , ∆�� + +∆ Φ
Appendix A: Additional Proofs
224
Proof. The assertM contract catches any calls to retry or throw, and caused an assertion violation. When the computation returns normally, assertM returns the unit value, as required. Furthermore, assertM uses deltaWCPd to check the resulting trace and ensure that it does not have any writes in it. This means that ∆�� = filterw trace�� , since filterw only affects the write trace actions in a trace.
Lemma A.5.4 (Lemma 5.5.4: Inversion of assertM’). For N ∈ {return N � , throw N � , retry}, if ∗
(assertM’ P ) ; Θ, ∆ ⇒ N ; Θ� , ∆� + +∆ Φ
Then N = return (), and Θ� [∆� + +∆] = Θ� [∆] and for some ∆��
∗
P ; Θ, ∆ ⇒ return (); Θ� , ∆�� + +∆ Φ
Proof. Just like assertM, the assertM’ contract catches any calls to retry or throw, and caused an assertion violation. When the computation returns normally, assertM returns the unit value, as required. Furthermore, assertM’ uses deltaWCPd to check the resulting trace. Any writes in the trace are compensated for by rewriting the original value back. So the ∆� = ∆C + +∆�� , where ∆c is the compensating trace. Specifically, ∆c is a sequence of write actions, one for each reference in difft∆�� . The values of each r in this sequence is Θ� [∆](r). So for all r ∈ ∆� , Θ� [∆� ](r) = Θ� [∆c + +∆�� ](r) = Θ� [∆](r). Therefore, for all r, Θ� [∆� + +∆](r) = Θ� [∆](r).
Appendix A: Additional Proofs
225
Using these results, Section 5.5.2 states a theorem listing the erasures for each of the contracts introduced in Chapter 4. This is restated below as Theorem A.5.5, along with proofs for each of these statements. Theorem A.5.5 (Theorem 5.5.5: Erasing Framing Contracts). • assertM M
erases to
return ()
• requires P M
erases to
M
• preserves P M
erases to
M
• ensures P M
erases to
M
• hoare P Q M
erases to
M
• writesOnly l M
erases to
M
• assertM’ M
erases to
return ()
• sep P Q M
erases to
M
Proof. assertM : This follows from the inversion Lemma A.5.3. requires, preserves, ensures preserves P M erases to M : Since assertM erases to return (), these clearly erase to just running the computation. hoare M : Since the requires and ensures contracts erase to the computation, so does the hoare contract. writesOnly : As discussed earlier, we assume that this contract detects writes (or modifications) in case of exception or retry as well (the version given in Figure 4.7 is
Appendix A: Additional Proofs
226
simplified). Given that, the writesOnly contract ensures that the new part of the trace does not have any write trace actions, and so does not have any effect on the heap concatenation. assertM’ : This follows immediately from the inversion Lemma A.5.4. sep P Q M erases to M : As discussed earlier, we assume that this contract does the appropriate checking even in case of a retry or exception being thrown by the wrapped computation. Since assertM is erasable, and the deltaFCPd operations have no effect except to return data to check the footprints (which all pass, by definition), the contract is clearly erasableto the wrapped computation.
A.6
Frame Rule
Section 5.6 presented the frame rule from separation logic, and provides a theorem (Theorem 5.6.1) and proof sketch stating that it holds on our definition of the sep contract. The frame rule is a powerful reasoning principle that allows for modular reasoning: local results can be easily recast in a broader context. The proof sketch was intended to give a high level intuition as to why the theorem is true. Here, we develop that intuition into a more detailed proof. Theorem A.6.1 (Theorom 5.6.1: Frame rule for CP transitions). For q, M , and r in DCd Haskell (i.e. they only use the delimited deltaCPd variants of the delta functions), if ∗
(sep P Q M ) ; Θ, ∆ ⇒ N ; Θ� , ∆� Φ
Appendix A: Additional Proofs
227
where N ∈ {return N � , throw N � , retry} and ∗
(assertM (P � R)) ; Θ, ∆ ⇒ return (); Θ�� , ∆�� Φ
then ∗
(sep (P � R) (\res old → Q res old � R) M ) ; Θ, ∆ ⇒ N ; Θ�g , ∆�g Φ
where Θ�g is just Θ� , possibly with some garbage added in (Θ� ⊆ Θ�g and forall l ∈ dom (θg� \ θ� ), l �∈ N, Θ� , ∆� , Φ) and ∆�g is just ∆� with some additional read and allocation trace actions. So ∆�g [Θ� ] = ∆� [Θ� ] Proof. This proof will proceed by decomposing the derivation of the transition of the assumed sep contract, and showing how to construct a derivation of the concluding contract. The intuition behind this proof was given in Section 5.6, in the proof sketch accompanying Theorem 5.6.1. Here, we fill in many of the details. First, by Lemma 5.2.1, for some ∆1 where ∆� = ∆1 + +∆, ∗
(sep P Q M ) ; Θ, ∆ ⇒ N ; Θ� , ∆1 + +∆ Φ
Then, by inversion on the withCCP, we get that (calling the do block R) and letting ∆0 = start ← save : ∆ and Φ0 = Φ[start �→ ∆0 ] and ∗
R; Θ, ∆0 ⇒ N ; Θ� , ∆1 + +∆0 Φ0
where start is appropriately fresh. Similarly, the first rule used in the derivation of the conclusion is an application of (withCCP ). The trace and heap will be different, but start will still be fresh.
Appendix A: Additional Proofs
228
Now, we must have a derivation for each command in the do block, where we feed the output heap and trace of each command into its successor. First, we run assertM p inside of a deltaFCPd. We know that
∗
deltaFCPd (assertM p); Θ, ∆0 ⇒ N2 ; Θ2 , ∆2 + +∆0 Φ0
So by Lemma 5.2.8, N2 is (difftF (∆2 ), N2� ) Additionally, ∗
assertM p; Θ, ∆0 ⇒ N2� ; Θ2 , ∆2 + +∆0 Φ0
Then, by Lemma 5.5.3, N2� = return () and ∗
p; Θ, ∆0 ⇒ return True; Θ2 , ∆p + +∆0 Φ0
where ∆2 = filterw ∆p . Turning to the matching step in the concluding derivation, we are given that ∗
(assertM (P � R)) ; Θ, ∆ ⇒ return (); Θ�� , ∆�� Φ
by Lemma 5.4.6 we can conclude that ∗
(assertM (P � R)) ; Θ, ∆ ⇒ return (); Θ�� , ∆�� Φ0
Since adding in a trace save action does not affect heap composition, by Lemma 5.4.5 we obtain ∗
(assertM (p � r)) ; Θ, ∆0 ⇒ return (); Θ�� , ∆�� Φ0
By Lemma 5.5.3, ∗
p � r; Θ, ∆0 ⇒ return True; Θ�� , ∆�2conc + +∆0 Φ0
Appendix A: Additional Proofs
229
where ∆�� = filterw ∆�2conc . decomposing the p � q, we already know that: ∗
p; Θ, ∆0 ⇒ return True; Θ2 , ∆p + +∆0 Φ0
leaving ∗
r; Θ2 , (∆p + +∆0 ) ⇒ return True; Θ�� , ∆r + +∆p + +∆0 Φ0
where ∆�� = filterw ∆�2conc = filterw ∆r + +∆p and we know that Θ2 ⊆ Θ�� and that difft(∆r ) does not intersect with difft(∆p ) So we have that ∗
(assertM (P � R)) ; Θ0 , ∆0 ⇒ return (); Θ�� , ∆r + +∆p + +∆0 Φ0
Using Lemma 5.2.8, we get that ∗
(deltaFCPd(assertM(P � R))) ; Θ0 , ∆0 ⇒ Φ0
return (difftF (∆r + +∆p ), ()) ; Θ�� , filterw ∆r + +∆p + +∆0
Thus, we can take the next step in the concluding derivation. The next step is to run the computation (enclosed in a deltaFCPd and deltaACPd. We get that ∗
deltaF CP d(deltaACP dM ) ; Θ2 , filterw ∆p + +∆0 ⇒ Φ0
return N_3 ; Θ3 , ∆c + +filterw ∆p + +∆0
Using Lemma 5.2.8 twice, this is really ∗
deltaF CP d(deltaACP dM ) ; Θ2 , filterw ∆p + +∆0 ⇒ Φ0
(return (difftF (∆c ), (difftF (∆c ), res))) ; Θ3 , ∆c + +filterw ∆p + +∆0
Appendix A: Additional Proofs
230
where res is the result of running the computation. Then, the next step verifies that difftF (∆c ) ⊆ difftF (∆p ) Now, for the conclusion derivation. We appeal to the discussion in Section 5.4.1 to rename things as needed so that we can run the needed computation in the proper heap (which has additional stuff in it from running R). Additionally, we need to extend the initial trace by filterw ∆r (note that filterw ∆ + +∆� ≡ filterw ∆ + +filterw ∆� by Lemma 5.2.6). For this we appeal to Lemmas 5.4.5 and 5.2.7. As per the lemma, we are able to run the computation successfully, and it will result in the same extensions to the trace (∆M ) and heap. Thus, it will have the same footprint and allocation sets as in the original derivations. The footprint check will always succeed, since it is verifying that difftF (∆M ) ⊆ (difftF (∆r + +∆p )), and this clearly holds since by Lemma 5.2.10, (difftF (∆r + +∆p )) = (difftF (∆r ) + +diff (∆p )). Going back to the original derivation, we now run the postcondition and get its footprint. Using the same inversion process as was used beforehand, it runs and returns a trace with a new fragment appended: ∆q and the footprint is difftF (∆q ). Finally, we ensure that its footprint is contained by the precondition’s footprint augmented by references newly allocated by the computation. Turning to the conclusion derivation, we need to show that we can run the augmented postcondition. Since anything changed in the heap is in the footprint of ∆r , which by assumption is distinct from the footprint of ∆p (and the newly allocated stuff from the computation), and hence ∆q , we can again use Lemma 5.4.5 to move from the trace in the original derivation to the trace in the final derivation.
Appendix A: Additional Proofs
231
Similarly, we can take our (given assumption) that p � r holds, which implies that r holds from the starting trace to the new trace, since the only thing that has changed (and was not filtered by filterw , see Lemma 5.2.7) is not, as was just argued, in the footprint of ∆M . Furthermore, ∆r is still disjoint from ∆p . Additionally, the final check passes, since it is checking that (diff F (∆r ) + +diff F (∆p ) + +diff A (∆M )) ⊆ (diff F (∆r ) + +diff F (∆q )) which is implied by (diff F (∆p ) + +diff A (∆M )) ⊆ (diff F (∆q )), which we know from the original derivation. Finally, both derivations return the same result, since running the computation produced the same result in both derivations.
Bibliography Edward Aftandilian, Samuel Guyer, Martin Vechev, and Eran Yahav. Asynchronous assertions. Technical Report TR201102, Tufts University, March 2011. Edward E. Aftandilian and Samuel Z. Guyer. GC assertions: using the garbage collector to check heap properties. In PLDI ’09: Proceedings of the 2009 ACM SIGPLAN conference on Programming language design and implementation, pages 235–244, New York, NY, USA, 2009. ACM. ISBN 9781605583921. doi: http://doi.acm.org/10.1145/1542476. 1542503. Amal Ahmed, Robert Bruce Findler, Jeremy G. Siek, and Philip Wadler. Blame for all. In Proceedings of the 38th annual ACM SIGPLANSIGACT symposium on Principles of programming languages, POPL ’11, pages 201–214, New York, NY, USA, 2011. ACM. ISBN 9781450304900. American National Standards Institute. Rationale for the ANSI C Programming Language. Silicon Press, Summit, NJ, USA, 1990. ISBN 096153365X. Mike Barnett, Robert Deline, Manuel F¨ahndrich, Bart Jacobs, K. Rustan Leino, Wolfram Schulte, and Herman Venter. The spec# programming system: Challenges and directions. Verified Software: Theories, Tools, Experiments: First IFIP TC 2/WG 2.3 Conference, VSTTE 2005, Zurich, Switzerland, October 1013, 2005, Revised Selected Papers and Discussions, pages 144–152, 2008. Richard Bornat, Cristiano Calcagno, Peter O’Hearn, and Matthew Parkinson. Permission accounting in separation logic. In Proceedings of the 32nd ACM SIGPLANSIGACT symposium on Principles of programming languages, POPL ’05, pages 259–270, New York, NY, USA, 2005. ACM. ISBN 158113830X. Lilian Burdy, Yoonsik Cheon, David R. Cok, Michael D. Ernst, Joseph R. Kiniry, Gary T. Leavens, K. Rustan M. Leino, and Erik Poll. An overview of jml tools and applications. Int. J. Softw. Tools Technol. Transf., 7:212–232, June 2005. ISSN 14332779. Patrice Chalin. Ensuring continued mainstream use of formal methods: An assessment, roadmap and issues. Technical Report 2005001, revision 2, Dependeable Software Research Group, Concordia University, Feb 2005. 232
Bibliography
233
Patrice Chalin. Early detection of JML specification errors using ESC/Java2. In Proceedings of the 2006 conference on Specification and verification of componentbased systems, SAVCBS ’06, pages 25–32, New York, NY, USA, 2006. ACM. ISBN 159593586X. Patrice Chalin and Fr´ed´eric Rioux. JML runtime assertion checking: Improved error reporting and efficiency using strong validity. In Proceedings of the 15th international symposium on Formal Methods, FM ’08, pages 246–261, Berlin, Heidelberg, 2008. SpringerVerlag. ISBN 9783540682356. BenChung Cheng and WenMei W. Hwu. Modular interprocedural pointer analysis using access paths: design, implementation, and evaluation. In Proceedings of the ACM SIGPLAN 2000 conference on Programming language design and implementation, PLDI ’00, pages 57–69, New York, NY, USA, 2000. ACM. ISBN 1581131992. Yoonsik Cheon and Gary T. Leavens. A runtime assertion checker for the java modeling language (JML). In Proceedings Of The International Conference On Software Engineering Research And Practice, pages 322–328, Las Vegas, NV, USA, 2002. CSREA Press. Adam Chlipala, Gregory Malecha, Greg Morrisett, Avraham Shinnar, and Ryan Wisnesky. Effective interactive proofs for higherorder imperative programs. In ICFP ’09: Proceeding of the 14th ACM SIGPLAN international conference on Functional programming, New York, NY, USA, 2009. ACM. David G. Clarke, James Noble, and John Potter. Simple ownership types for object containment. In Proceedings of the 15th European Conference on ObjectOriented Programming, ECOOP ’01, pages 53–76, London, UK, UK, 2001. SpringerVerlag. ISBN 3540422064. Lori A. Clarke and David S. Rosenblum. A historical perspective on runtime assertion checking in software development. SIGSOFT Softw. Eng. Notes, 31:25–37, May 2006. ISSN 01635948. Daniel S. Dantas and David Walker. Harmless advice. In Conference record of the 33rd ACM SIGPLANSIGACT symposium on Principles of programming languages, POPL ’06, pages 383–396, New York, NY, USA, 2006. ACM. ISBN 1595930272. Brian Demsky and Martin Rinard. Automatic detection and repair of errors in data structures. In OOPSLA ’03: Proc. of the 18th annual ACM SIGPLAN conf. on Objectoriented programing, systems, languages, and applications, pages 78–95, New York, NY, USA, 2003. ACM. ISBN 1581137125. Christos Dimoulas, Riccardo Pucella, and Matthias Felleisen. Future contracts. In PPDP ’09: Proceedings of the 11th ACM SIGPLAN conference on Principles and practice of
Bibliography
234
declarative programming, pages 195–206, New York, NY, USA, 2009. ACM. ISBN 9781605585680. doi: http://doi.acm.org/10.1145/1599410.1599435. Christos Dimoulas, Robert Bruce Findler, Cormac Flanagan, and Matthias Felleisen. Correct blame for contracts: no more scapegoating. In Proceedings of the 38th annual ACM SIGPLANSIGACT symposium on Principles of programming languages, POPL ’11, pages 215–226, New York, NY, USA, 2011. ACM. ISBN 9781450304900. Manuel F¨ahndrich, Michael Barnett, and Francesco Logozzo. Embedded contract languages. In Proc. of the 2010 ACM Symposium on Applied Computing, SAC ’10, pages 2103–2110, New York, NY, USA, 2010. ACM. ISBN 9781605586397. Mattias Felleisen. The theory and practice of firstclass prompts. In Proceedings of the 15th ACM SIGPLANSIGACT symposium on Principles of programming languages, POPL ’88, pages 180–190, New York, NY, USA, 1988. ACM. ISBN 0897912527. Robert Bruce Findler and Matthias Felleisen. Contracts for higherorder functions. In Proceedings of the seventh ACM SIGPLAN international conference on Functional programming, ICFP ’02, pages 48–59, New York, NY, USA, 2002. ACM. ISBN 1581134878. Matthew Flatt and PLT. Reference: Racket. Technical Report PLTTR20101, PLT Inc., 2010. http://racketlang.org/tr1/. Matthew Fluet and Greg Morrisett. Monadic regions. J. Funct. Program., 16:485–545, July 2006. ISSN 09567968. Hector GarciaMolina, Jeffrey D. Ullman, and Jennifer Widom. Database Systems: The Complete Book. Prentice Hall Press, Upper Saddle River, NJ, USA, 2 edition, 2008. ISBN 9780131873254. Joseph A. Goguen and Jos´e Meseguer. Security policies and security models. In IEEE Symposium on Security and Privacy, pages 11–20, 1982. James Gosling, Bill Joy, Guy Steele, and Gilad Bracha. Java(TM) Language Specification, The (3rd Edition) (Java (AddisonWesley)). AddisonWesley Professional, 2005. ISBN 0321246780. Kathryn E. Gray and Alan Mycroft. Logical testing: Hoarestyle specification meets executable validation. In FASE ’09: Fundamental Approaches to Software Engineering. SpringerVerlag, March 2009. Tim Harris and Keir Fraser. Language support for lightweight transactions. In Proceedings of the 18th annual ACM SIGPLAN conference on Objectoriented programing, systems, languages, and applications, OOPSLA ’03, pages 388–402, New York, NY, USA, 2003. ACM. ISBN 1581137125.
Bibliography
235
Tim Harris and Simon PeytonJones. Transactional memory with data invariants. In TRANSACT ’06: 1st Workshop on Languages, Compilers, and Hardware Support for Transactional Computing, jun 2006. Tim Harris, Simon Marlow, Simon PeytonJones, and Maurice Herlihy. Composable memory transactions. In PPoPP ’05: Proceedings of the tenth ACM SIGPLAN symposium on Principles and practice of parallel programming, pages 48–60, New York, NY, USA, 2005. ACM. ISBN 1595930809. doi: http://doi.acm.org/10.1145/1065944.1065952. Tim Harris, Mark Plesko, Avraham Shinnar, and David Tarditi. Optimizing memory transactions. In Proceedings of the 2006 ACM SIGPLAN conference on Programming language design and implementation, PLDI ’06, pages 14–25, New York, NY, USA, 2006. ACM. ISBN 1595933204. Rich Hickey. The clojure programming language. In DLS ’08: Proc. of the 2008 symposium on Dynamic languages, pages 1–1, New York, NY, USA, 2008. ACM. ISBN 9781605582702. C. A. R. Hoare. An axiomatic basis for computer programming. Commun. ACM, 12: 576–580, October 1969. ISSN 00010782. Paul Hudak, John Peterson, and Joseph Fasel. A gentle introduction to Haskell, version 1.4. Available at http://haskell.org/tutorial, March 1997. Samin S. Ishtiaq and Peter W. O’Hearn. Bi as an assertion language for mutable data structures. In Proceedings of the 28th ACM SIGPLANSIGACT symposium on Principles of programming languages, POPL ’01, pages 14–26, New York, NY, USA, 2001. ACM. ISBN 1581133367. G. F. Johnson and D. Duggan. Stores and partial continuations as firstclass objects in a language and its environment. In POPL ’88: Proceedings of the 15th ACM SIGPLANSIGACT symposium on Principles of programming languages, pages 158–168, New York, NY, USA, 1988. ACM. ISBN 0897912527. doi: http://doi.acm.org/10.1145/ 73560.73574. C. B. Jones. Tentative steps toward a development method for interfering programs. ACM Trans. Program. Lang. Syst., 5:596–619, October 1983. ISSN 01640925. Oleg Kiselyov and Chungchieh Shan. Lightweight monadic regions. In Haskell ’08: Proceedings of the first ACM SIGPLAN symposium on Haskell, pages 1–12, New York, NY, USA, 2008. ACM. ISBN 9781605580647. doi: http://doi.acm.org/10.1145/ 1411286.1411288.
Bibliography
236
Oleg Kiselyov, Chungchieh Shan, and Amr Sabry. Delimited dynamic binding. In Proceedings of the eleventh ACM SIGPLAN international conference on Functional programming, ICFP ’06, pages 26–37, New York, NY, USA, 2006. ACM. ISBN 1595933093. Piotr Kosiuczenko. On the implementation of @pre. In Proceedings of the 12th International Conference on Fundamental Approaches to Software Engineering: Held as Part of the Joint European Conferences on Theory and Practice of Software, ETAPS 2009, FASE ’09, pages 246–261, Berlin, Heidelberg, 2009. SpringerVerlag. ISBN 9783642005923. Eric Koskinen and Maurice Herlihy. Checkpoints and continuations instead of nested transactions. In SPAA ’08: Proceedings of the twentieth annual symposium on Parallelism in algorithms and architectures, pages 160–168, New York, NY, USA, 2008. ACM. ISBN 9781595939739. doi: http://doi.acm.org/10.1145/1378533.1378563. Jim Larus and Ravi Rajwar. Transactional Memory (Synthesis Lectures on Computer Architecture). Morgan & Claypool Publishers, 2007. ISBN 1598291246. Hermann Lehner and Peter M¨uller. Efficient runtime assertion checking of assignable clauses with datagroups. In David Rosenblum and Gabriele Taentzer, editors, Fundamental Approaches to Software Engineering, volume 6013 of Lecture Notes in Computer Science, pages 338–352. Springer Berlin / Heidelberg, 2010. Gregory Malecha, Greg Morrisett, Avraham Shinnar, and Ryan Wisnesky. Towards a verified relational database management system. In POPL ’10: Proceeding of the 37th ACM SIGACTSIGPLAN Symposium on Principles of Programming Languages, New York, NY, USA, 2010. ACM. C. March, C. PaulinMohring, and X. Urbain. The KRAKATOA tool for certification of JAVA/JAVACARD programs annotated in JML. Journal of Logic and Algebraic Programming, 58(12):89 – 106, 2004. ISSN 15678326. Formal Methods for Smart Cards. Claude March´e and Christine PaulinMohring. Reasoning about java programs with aliasing and frame conditions. In Joe Hurd and Tom Melham, editors, Theorem Proving in Higher Order Logics, volume 3603 of Lecture Notes in Computer Science, pages 179– 194. Springer Berlin / Heidelberg, 2005. Bertrand Meyer. Objectoriented software construction (2nd ed.). PrenticeHall, Inc., Upper Saddle River, NJ, USA, 1997. ISBN 0136291554. J. Gregory Morrisett. Refining firstclass stores. In In Proceedings of the ACM SIGPLAN Workshop on State in Programming Languages, pages 73–87, 1993.
Bibliography
237
Huu Hai Nguyen, Viktor Kuncak, and WeiNgan Chin. Runtime checking for separation logic. In Proceedings of the 9th international conference on Verification, model checking, and abstract interpretation, VMCAI’08, pages 203–217, Berlin, Heidelberg, 2008. SpringerVerlag. ISBN 3540781625, 9783540781622. Simon Peyton Jones et al. The Haskell 98 language and libraries: The revised report. Journal of Functional Programming, 13(1):i–xii,1–255, Jan 2003. http://www. haskell.org/definition/. Andrew M. Pitts. Nominal logic, a first order theory of names and binding. Inf. Comput., 186:165–193, November 2003. ISSN 08905401. Erik Poll, Patrice Chalin, David Cok, Joe Kiniry, and Gary T. Leavens. Beyond assertions: Advanced specification and verification with JML and ESC/Java2. In In Formal Methods for Components and Objects (FMCO) 2005, Revised Lectures, volume 4111 of LNCS, pages 342–363. Springer, 2006. Francois Pottier. Hiding local state in direct style: A higherorder antiframe rule. Logic in Computer Science, Symposium on, 0:331–340, 2008. ISSN 10436871. Christoph Reichenbach, Neil Immerman, Yannis Smaragdakis, Edward E. Aftandilian, and Samuel Z. Guyer. What can the gc compute efficiently?: a language for heap assertions at gc time. In Proceedings of the ACM international conference on Object oriented programming systems languages and applications, OOPSLA ’10, pages 256–269, New York, NY, USA, 2010. ACM. ISBN 9781450302036. Hesam Samimi, Ei Darli Aung, and Todd Millstein. Falling back on executable specifications. In European Conference on ObjectOriented Programming, June 2010. Amritam Sarcar and Yoonsik Cheon. A new eclipsebased JML compiler built using AST merging. Technical Report 1008, Department of Computer Science, University of Texas at El Paso, El Paso, TX, March 2010. Avraham Shinnar, David Tarditi, Mark Plesko, and Bjarne Steensgaard. Integrating support for undo with exception handling. Technical Report MSRTR2004140, Microsoft Research, December 2004. Daniel Dominic Sleator and Robert Endre Tarjan. Selfadjusting binary search trees. J. ACM, 32:652–686, July 1985. ISSN 00045411. doi: http://doi.acm.org.ezpprod1. hul.harvard.edu/10.1145/3828.3835. URL http://doi.acm.org.ezpprod1. hul.harvard.edu/10.1145/3828.3835. T. Stephen Strickland and Matthias Felleisen. Contracts for firstclass modules. In Proceedings of the 5th symposium on Dynamic languages, DLS ’09, pages 27–38, New York, NY, USA, 2009. ACM. ISBN 9781605587691.
Bibliography
238
T. Stephen Strickland and Matthias Felleisen. Contracts for firstclass classes. In Proceedings of the 6th symposium on Dynamic languages, DLS ’10, pages 97–112, New York, NY, USA, 2010. ACM. ISBN 9781450304054. Andrew P. Tolmach and Andrew W. Appel. A debugger for Standard ML. J. Funct. Program., 5(2):155–200, 1995. Andrew Peter Tolmach. Debugging standard ML. PhD thesis, Princeton University, Princeton, NJ, USA, 1992. Philip Wadler. Theorems for free! In Proceedings of the fourth international conference on Functional programming languages and computer architecture, FPCA ’89, pages 347–359, New York, NY, USA, 1989. ACM. ISBN 0897913280. Westley Weimer, ThanhVu Nguyen, Claire Le Goues, and Stephanie Forrest. Automatically finding patches using genetic programming. In ICSE ’09: Proc. of the 31st International Conference on Software Engineering, pages 364–374, Washington, DC, USA, 2009. IEEE Computer Society. ISBN 9781424434534. Dana N. Xu, Simon Peyton Jones, and Koen Claessen. Static contract checking for haskell. In Proceedings of the 36th annual ACM SIGPLANSIGACT symposium on Principles of programming languages, POPL ’09, pages 41–52, New York, NY, USA, 2009. ACM. ISBN 9781605583792. Cui Ye. Improving JML’s assignable clause analysis. Technical Report 0619, Department of Computer Science, Iowa State University, 226 Atanasoff Hall, Ames, Iowa 50011, July 2006.