ABSTRACT Title of dissertation: MECHANIZING ABSTRACT INTERPRETATION David Darais Doctor of Philosophy, 2017 Dissertation directed by: Professor David Van Horn Department of Computer Science It is important when developing software to verify the absence of undesirable behavior such as crashes, bugs and security vulnerabilities. Some settings require high assurance in verification results, e.g., for embedded software in automobiles or airplanes. To achieve high assurance in these verification results, formal methods are used to automatically construct or check proofs of their correctness. However, achieving high assurance for program analysis results is challenging, and current methods are ill suited for both complex critical domains and mainstream use. To verify the correctness of software we consider program analyzers—automated tools which detect software defects—and to achieve high assurance in verification results we consider mechanized verification—a rigorous process for establishing the correctness of program analyzers via computer-checked proofs. The key challenges to designing verified program analyzers are: (1) achieving an analyzer design for a given programming language and correctness property; (2) achieving an implementation for the design; and (3) achieving a mechanized verification that the implementation is correct w.r.t. the design. The state of the art in (1) and (2) is to use abstract interpretation: a guiding mathematical framework for systematically constructing analyzers directly from programming language semantics. However, achieving (3) in the presence of abstract interpretation has remained an open problem since the late 1990’s. Furthermore, even the state-of-the art which achieves (3) in the absence of abstract interpretation suffers from the inability to be reused in the presence of new analyzer designs or programming language features. First, we solve the open problem which has prevented the combination of abstract interpretation (and in particular, calculational abstract interpretation) with mechanized verification, which advances the state of the art in designing, implementing, and verifying analyzers for critical software. We do this through a new mathematical framework Constructive Galois Connections which supports synthesizing specifications for program analyzers, calculating implementations from these induced specifications, and is amenable to mechanized verification. Finally, we introduce reusable components for implementing analyzers for a wide range of designs and semantics. We do this though two new frameworks Galois Transformers and Definitional Abstract Interpreters. These frameworks tightly couple analyzer design decisions, implementation fragments, and verification properties into compositional components which are (target) programming-language independent and amenable to mechanized verification. Variations in the analysis design are then recovered by simply re-assembling the combination of components. Using this framework, sophisticated program analyzers can be assembled by non-experts, and the result are guaranteed to be verified by construction. MECHANIZING ABSTRACT INTERPRETATION by David Darais Dissertation submitted to the Faculty of the Graduate School of the University of Maryland, College Park in partial fulfillment of the requirements for the degree of Doctor of Philosophy 2017 Advisory Committee: Professor David Van Horn, Chair/Advisor Professor Patrick Cousot Professor Jeff Foster Professor Michael Hicks Professor Larry Washington © Copyright by David Darais 2017 Preface Much of the material in this thesis has previously appeared in the following peer- reviewed publications, authored jointly with David Van Horn, Matthew Might, Nicholas Labich and Phu´c C. Nguye˜ˆn: David Darais, Matthew Might, and David Van Horn. Galois transformers and modular abstract interpreters: Reusable metatheory for program analy- sis. In Object-Oriented Programming, Systems, Languages and Applications (OOPSLA). ACM, New York, NY, USA, 2015 David Darais and David Van Horn. Constructive Galois connections: Taming the Galois connection framework for mechanized metatheory. In International Conference on Functional Programming (ICFP). ACM, New York, NY, USA, 2016 David Darais, Nicholas Labich, Phu´c C. Nguye˜ˆn, and David Van Horn. Def- initional abstract interpreters for higher-order programming languages. In International Conference on Functional Programming (ICFP). ACM, New York, NY, USA, 2017 ii Dedicated to my grandparents. Alexander and Norma Darais Byron and Ingrid Forsyth Bob and Doris Edmondson Lewis and Doris Miner Bob and Joyce Dustman iii Acknowledgments Thanks to my advisor David Van Horn for being such an amazing mentor, and for helping me every step of the way, even before I became his student. Thanks to Matt Might for being such an amazing collaborator and role model. Thanks to Mike Hicks, Jeff Foster and Nate Foster for their continued encouragement and support. Thanks to E´ric Tanter and Ron Garcia for many helpful discussions of their work, as well as their warm encouragement ever since our meeting at OOPSLA ’15. Thanks to Matthias Felleisen for giving me key advice at a pivotal moment during my PhD. Thanks to Patrick Cousot for many detailed and insightful comments on this thesis. Thanks to my partner and love of my life Olivia, for always believing in me and taking care of me. Thanks to my parents—Tom and Suzanne Darais, and Karin and Jay Larson—for their unwavering love and support. Thanks to my uncle Steve for helping take care of me throughout my PhD studies, and for always being so loving, wise and amazing. Thanks to my brothers Abraham and Jeremiah Darais and my good friend Simon Williams for always being there for me. Thanks to my boston fam: Guillaume Basse, Omar Shammas, John Coglianese, Yazan Abu Ghazal, Dan Huang, Dan King, Scott Moore and Andrew Johnson; you never stopped supporting me, believing in me and cheering me on. Thanks to Kris Micinski for being such a solid friend ever since I moved to Maryland. Thanks to those who peer-reviewed my scholarly submissions and provided helpful feedback, regardless of the acceptance outcome. And finally, thanks to those who I forgot to mention who have undoubtedly helped me along the way. iv Table of Contents Preface ii Dedication iii Acknowledgements iv List of Figures ix 1 Introduction 1 1.1 Outline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2 Technical Background 8 2.1 Abstract Interpretation . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.1.1 Galois Connection Mappings . . . . . . . . . . . . . . . . . . . 9 2.1.2 Galois Connection Laws . . . . . . . . . . . . . . . . . . . . . 11 2.1.3 Abstract Interpreters . . . . . . . . . . . . . . . . . . . . . . . 13 2.1.4 Calculational Abstract Interpretation . . . . . . . . . . . . . . 14 2.1.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 2.2 Abstracting Abstract Machines . . . . . . . . . . . . . . . . . . . . . 17 2.2.1 Small-step Semantics . . . . . . . . . . . . . . . . . . . . . . . 19 2.2.2 Adding Higher-order Functions . . . . . . . . . . . . . . . . . 20 2.2.3 Adding Indirection through a Store . . . . . . . . . . . . . . . 22 2.2.4 Abstraction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 2.2.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 2.3 Mechanized Verification . . . . . . . . . . . . . . . . . . . . . . . . . 25 2.3.1 Equality . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 2.3.2 Embedding Classical Powersets . . . . . . . . . . . . . . . . . 28 2.3.3 Embedding General Classical Reasoning . . . . . . . . . . . . 29 2.3.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 v 3 Technical Overview 32 3.1 Constructive Galois Connections . . . . . . . . . . . . . . . . . . . . . 32 3.1.1 The Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 3.1.2 The Main Ideas . . . . . . . . . . . . . . . . . . . . . . . . . . 35 3.1.3 Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 3.2 Galois Transformers . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 3.2.1 The Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 3.2.2 The Main Ideas . . . . . . . . . . . . . . . . . . . . . . . . . . 41 3.2.3 Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 3.3 Abstracting Definitional Interpreters . . . . . . . . . . . . . . . . . . 45 3.3.1 The Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 3.3.2 The Main Ideas . . . . . . . . . . . . . . . . . . . . . . . . . . 48 3.3.3 Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 4 Constructive Galois Connections 52 4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 4.2 Verifying a Simple Static Analyzer . . . . . . . . . . . . . . . . . . . 57 4.2.1 The Direct Approach . . . . . . . . . . . . . . . . . . . . . . . 58 4.2.2 Classical Abstract Interpretation . . . . . . . . . . . . . . . . 62 4.3 Constructive Galois Connections . . . . . . . . . . . . . . . . . . . . . 71 4.3.1 Partial Orders and Monotonicity . . . . . . . . . . . . . . . . 77 4.3.2 Relationship to Classical Galois Connections . . . . . . . . . . 79 4.3.3 The “Specification Effect” . . . . . . . . . . . . . . . . . . . . 82 4.4 Case Study 1: Calculational AI . . . . . . . . . . . . . . . . . . . . . 85 4.4.1 Concrete Semantics . . . . . . . . . . . . . . . . . . . . . . . . 86 4.4.2 Abstract Semantics with Constructive GCs . . . . . . . . . . . 87 4.5 Case Study 2: Gradual Type Systems . . . . . . . . . . . . . . . . . . 98 4.6 Constructive Galois Connection Metatheory . . . . . . . . . . . . . . 103 4.7 Constructing Constructive Galois Connections . . . . . . . . . . . . . 110 4.7.1 Strictly Classical Galois Connections . . . . . . . . . . . . . . 111 4.7.2 Strictly Constructive Galois Connections . . . . . . . . . . . . 111 4.7.3 Primitive Galois Connections—Classical and Constructive . . 112 4.7.4 Composing Galois Connections—Classical and Constructive . 113 4.8 Comparing Classical and Constructive Approaches . . . . . . . . . . . 115 4.8.1 Review: Cousot’s Original Classical Calculation . . . . . . . . 116 4.8.2 Using Independent Attributes Explicitly . . . . . . . . . . . . 119 4.8.3 Calculating with Constructive Galois Connections . . . . . . . 121 4.9 Optimal Calculations—Constructive and Classical . . . . . . . . . . . 123 4.10 Multivalued Constructive Galois Connections . . . . . . . . . . . . . 130 4.10.1 Review: Cousot’s Original Classical Calculation . . . . . . . . 130 4.10.2 The Constructive Calculation . . . . . . . . . . . . . . . . . . 132 4.11 Related Work . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138 4.12 Conclusions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140 vi 5 Galois Transformers 142 5.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142 5.2 Semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146 5.3 Path and Flow Sensitivity in Analysis . . . . . . . . . . . . . . . . . . 151 5.4 Analysis Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . 153 5.4.1 The Analysis Monad . . . . . . . . . . . . . . . . . . . . . . . 154 5.4.2 The Abstract Domain . . . . . . . . . . . . . . . . . . . . . . 155 5.4.3 Abstract Time . . . . . . . . . . . . . . . . . . . . . . . . . . 157 5.5 The Interpreter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158 5.6 Recovering Analyses . . . . . . . . . . . . . . . . . . . . . . . . . . . 162 5.6.1 Recovering a Concrete Interpreter . . . . . . . . . . . . . . . . 162 5.6.2 Recovering an Abstract Interpreter . . . . . . . . . . . . . . . 165 5.6.3 End-to-end Correctness . . . . . . . . . . . . . . . . . . . . . . 167 5.7 Varying Path and Flow Sensitivity . . . . . . . . . . . . . . . . . . . 168 5.7.1 Flow Insensitive Monad . . . . . . . . . . . . . . . . . . . . . 169 5.8 A Compositional Monadic Framework . . . . . . . . . . . . . . . . . . 171 5.8.1 State Galois Transformer . . . . . . . . . . . . . . . . . . . . . 173 5.8.2 Nondeterminism Galois Transformer . . . . . . . . . . . . . . 173 5.8.3 Flow Sensitivity Galois Transformer . . . . . . . . . . . . . . . 176 5.8.4 Galois Transformers . . . . . . . . . . . . . . . . . . . . . . . 178 5.8.5 End-to-End Correctness with Galois Transformers . . . . . . . 181 5.8.6 Applying the Framework to Our Semantics . . . . . . . . . . . 183 5.8.7 Applying the Framework to Another Semantics . . . . . . . . 184 5.9 Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 5.10 Related Work . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187 5.11 Conclusions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191 6 Abstracting Definitional Interpreters 192 6.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192 6.1.1 Outline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194 6.2 From Machines to Compositional Evaluators . . . . . . . . . . . . . . 196 6.3 A Definitional Interpreter . . . . . . . . . . . . . . . . . . . . . . . . 198 6.3.1 Instantiating the Interpreter . . . . . . . . . . . . . . . . . . . 201 6.3.2 Collecting Variations . . . . . . . . . . . . . . . . . . . . . . . 204 6.3.3 Abstracting Base Values . . . . . . . . . . . . . . . . . . . . . 208 6.3.4 Abstracting Closures . . . . . . . . . . . . . . . . . . . . . . . 210 6.4 Caching and Finding Fixed-points . . . . . . . . . . . . . . . . . . . . 212 6.4.1 Formal soundness and termination . . . . . . . . . . . . . . . 217 6.5 Pushdown a` la Reynolds . . . . . . . . . . . . . . . . . . . . . . . . . 218 6.6 Widening the Store . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219 6.7 An Alternative Abstraction . . . . . . . . . . . . . . . . . . . . . . . 221 6.8 Symbolic Execution and Garbage Collection . . . . . . . . . . . . . . 224 6.9 Try It Out . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225 6.10 Formalism . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226 6.11 Related Work . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244 vii 6.12 Conclusions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 7 Concluding Remarks 249 A Galois Transformer Proofs 251 A.0.1 Lemma 5 [Galois Transformers] (Section 5.8.4) . . . . . . . . 251 A.0.2 Lemma 3 [P t laws] (Section 5.8.2) . . . . . . . . . . . . . . . 272 A.0.3 Lemma 4 [F t laws] (Section 5.8.3) . . . . . . . . . . . . . . . 277 Bibliography 283 viii List of Figures 4.1 Case Study 1: WHILE Abstract Syntax . . . . . . . . . . . . . . . . . . 86 4.2 Case Study 1: WHILE Concrete Semantics . . . . . . . . . . . . . . . . 88 4.3 Case Study 1: Select Constructive Galois Connection Calculations . . 96 4.4 Case Study 1: Constructive Galois Connection Calculations in Agda . 97 4.5 Case Study 2: Syntax Directed Precise Type System . . . . . . . . . 100 4.6 Case Study 2: Systematically Constructed Gradual Type System . . . 102 4.7 Comparison of Constructive and Classical Galois Connection Adjunctions103 4.8 Relationship Between Classical, Kleisli and Constructive GCs . . . . 107 4.9 Review: Calculational Derivation for Binary Arithmetic Expressions . 116 4.10 Classical Calculation for Binary Arithmetic Expressions . . . . . . . . 118 4.11 Classical Calculation for Binary Arithmetic Expressions Using Inde- pendent Attributes . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120 4.12 Constructive Calculation for Binary Arithmetic Expressions . . . . . 124 4.13 Constructive Calculation for Binary Arithmetic Expressions—Optimal and η-directed . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127 4.14 Classical Calculation for Binary Arithmetic Expressions—Optimal and α-directed . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 4.15 Review: calculating abstraction for conditional expressions . . . . . . 131 4.16 Classical Calculation for Conditional Command Expressions . . . . . 133 4.17 Conditional Expressions Constructive Calculation . . . . . . . . . . . 136 4.18 Conditional Expressions Constructive Calculation (Cont.) . . . . . . . 137 5.1 λIF Syntax and Concrete State Space . . . . . . . . . . . . . . . . . . 147 5.2 Concrete Semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149 5.3 Garbage Collected Collecting Semantics . . . . . . . . . . . . . . . . . 150 5.4 Monadic Semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159 5.5 Monadic helper functions . . . . . . . . . . . . . . . . . . . . . . . . . 160 5.6 Concrete Interpreter Values and Time . . . . . . . . . . . . . . . . . . 163 5.7 Concrete Interpreter Monad . . . . . . . . . . . . . . . . . . . . . . . 164 5.8 Abstract Interpreter Parameters . . . . . . . . . . . . . . . . . . . . . 166 5.9 Flow Insensitive Monad Parameter . . . . . . . . . . . . . . . . . . . 170 5.10 State Galois Transformer . . . . . . . . . . . . . . . . . . . . . . . . . 174 5.11 Nondeterminism Galois Transformer . . . . . . . . . . . . . . . . . . 175 ix 5.12 Flow Sensitivity Galois Transformer . . . . . . . . . . . . . . . . . . . 177 5.13 Galois Transformer Commuting Cube of Abstractions . . . . . . . . . 180 6.1 Programming Language Syntax . . . . . . . . . . . . . . . . . . . . . 198 6.2 The Extensible Definitional Interpreter . . . . . . . . . . . . . . . . . 200 6.3 Components for Definitional Interpreters . . . . . . . . . . . . . . . . 202 6.4 Trace Collecting Semantics . . . . . . . . . . . . . . . . . . . . . . . . 205 6.5 Dead Code Collecting Semantics . . . . . . . . . . . . . . . . . . . . . 207 6.6 Abstracting Primitive Operations . . . . . . . . . . . . . . . . . . . . 208 6.7 Abstracting Allocation: 0CFA . . . . . . . . . . . . . . . . . . . . . . 211 6.8 Co-inductive Caching Algorithm . . . . . . . . . . . . . . . . . . . . . 214 6.9 Finding Fixed-Points in the Cache . . . . . . . . . . . . . . . . . . . . 215 6.10 An Alternative Abstraction for Precise Primitives . . . . . . . . . . . 222 6.11 λIF Big-step Concrete Evaluation Semantics . . . . . . . . . . . . . . 227 6.12 λIF Big-step Concrete Reachability Semantics . . . . . . . . . . . . . 229 6.13 Big-step Collecting Evaluation Semantics . . . . . . . . . . . . . . . . 231 6.14 Big-step Collecting Reachability Semantics . . . . . . . . . . . . . . . 232 6.15 Big-step Abstract Evaluation Semantics . . . . . . . . . . . . . . . . 235 6.16 Big-step Abstract Reachability Semantics . . . . . . . . . . . . . . . . 236 x Chapter 1: Introduction This thesis aims to improve the correctness and reliability of software. The results from this thesis offer methods to cost-effectively prevent software failures and exploits, and reduce their costs on society. The methods we develop are a new mathematical theory which improves the state-of-the art in foundational approaches to software reliability, and two new program analysis frameworks which help reduce the cost of achieving software reliability. These contributions support the following thesis: Constructing mechanically verified program analyzers via calculation and composition is feasible using constructive Galois connections and modular abstract interpreters. The Software Reliability Problem Software bugs are expensive. Software plays an important role in critical systems like automobiles, aircraft, medical de- vices and military systems. When bugs appear in these systems the result can be catastrophic. Software also appears in general-purpose systems like cell-phones, smart-devices, and web infrastructure like email, banking and e-commerce. Bugs in general-purpose software systems are also costly: malware on cell-phones and web- sites compromise user privacy, and bugs in web-infrastructure lead to cyber-attacks, data corruption and service failures, costing billions of dollars annually [Tassey, 2002, Zhivich and Cunningham, 2009]. 1 Achieving Software Reliability To achieve high assurance for software, we must establish the absence of entire classes of bugs and/or conformance with specific behavior. For example, a high-assurance medical device should not only be immune to a well-specified class of security exploits, it should also guaranteed to perform its intended medical function for the patient. Establishing high assurance is challenging, and current techniques are either unable to achieve it or too costly to adopt for many important applications. What we do know is that testing software is not enough on its own to achieve high assurance. Most software systems have an infinite number of possible in- put/output behaviors, and testing is restricted to exploring only a finite subset of such behaviors. To achieve high assurance, one must use verification tools, which are able to reason symbolically about the infinite behavior of software. Program Analyzers This thesis considers program analyzers, which are tools for automating large portions of the verification proofs required to establish high assurance in software. Our results address the limitations of current approaches to building program analyzers, and contribute towards increased adoptions of tools which achieve high assurance in settings where software reliability remains an expensive and unsolved problem. The Importance of Reusable Tools In order to have a positive impact on the way we produce software, program analyzers must not only be usable, they must be reusable. New programming languages are invented every year, for which 2 we lack tools like program analyzers. Likewise, new analysis techniques are also invented every year, for which implementations only exist for our oldest, most decrepit programming languages. What is missing is program analysis machinery which supports reuse across new programming languages, emerging software domains, and changing software correctness criteria. To achieve reuse in program analyzer implementations, support for program- ming language features (e.g., while loops) must be isolated from analysis properties (e.g., buffer overflows). Techniques exist for isolating simple properties like arith- metic relationships (e.g., x < y), but not for sophisticated properties like those used for security (e.g., passwords are not leaked). Even in cases where implementation fragments can be reused, it is not possible to reuse the proof fragments used to establish the correctness of the resulting analyzer. program analyzer compiler . . . Software Pipeline The Importance of Verified Tools Software is cre- ated through a complex pipeline: a program is written in a programming language, translated to machine code by a compiler, loaded by an operating system, and executed with hardware. To gain any amount of trust in the result, each component of the pipeline should be trustworthy. For this reason, it is just important to achieve verified software tools as it is to achieve the end goal of verified software, for the latter is not achievable without the former. Integrating program analyzers into this pipeline (see figure) comes with challenges similar to designing a compiler: implementations are often not reusable, and the 3 correctness of the tool must be established to achieve high assurance in the resulting software. This means analyzers must often be written from scratch to support new programming languages, and these new analyzers must then be verified if their results are to be trusted. Mechanized Verification To achieve high assurance in program analyzer imple- mentations, the gold standard is mechanized verification using automated theorem provers or semi-automated proof assistants. Consider for instance the compiler phase of the pipeline: a recent study showed that each of the 11 industry-strength C compilers examined had correctness bugs [Yang et al., 2011]. One of these compilers was CompCert, a mechanically verified C compiler [Leroy, 2009], however the only bugs present were in the unverified front-end. Program analyzers are similarly complex components of the software pipeline, and—like compilers—mechanized verification is the only technique known which can guarantee the absence of bugs in an implementation. For mechanization, the state of the art is to use proof assistants based on dependent type theory, which support extraction of certified algorithms. Contributions This thesis improves the state of the art for both lightweight and heavyweight verification. One goal is for practitioners to eventually use at least lightweight verification for every piece of software—there is no reason not to. Another goal is to achieve heavyweight verification for mission critical software in settings which weren’t possible or feasible before. This thesis addresses reuse and high assurance—and their combination—for 4 program analyzer implementations, and our overarching insight is to tightly couple the implementation of a program analyzer with its proof of correctness. By tightly coupling implementation with proof, we design building blocks for constructing reliable analyzers from reusable components, and identify ways to reduce the proof effort required to mechanically verify analyzers. High Assurance for Analyzer Implementations In this thesis we develop a new mathematical framework called Constructive Galois Connections (CGCs) [Darais and Van Horn, 2016] to mechanically verify a large class high- assurance program analyzers which previous approaches were unable to verify. These analyzers are called correct-by-construction because the implementation and proof of correctness for the analyzer are tightly coupled throughout their definition. Correct- by-construction analyzers are advantageous for mechanized verification because there is only one artifact to verify (the coupled implementation/proof), rather than two artifacts (the uncoupled implementation and proof), effectively reducing the proof burden by half. Constructing analyzers in this way also has the benefit of catching implementation bugs early, because incorrect implementation are not even possible to define. Central to these correct-by-construction program analyzers is a mathematical theory of sound approximation called abstract interpretation [Cousot and Cousot, 1977], however this theory is fundamentally limited in ways that prevent mechanized verification. To design CGCs we addressed the limitations of abstract interpretation by re- instantiating the more general mathematical theory of adjunctions, of which abstract 5 interpretation is one instance. CGCs are an alternative instantiation of adjunctions which supports defining correct-by-construction program analyzers, but doesn’t suffer from the same limitations to mechanized verification. One result of CGCs is the first mathematical foundation for program analyzers which simultaneously supports correct-by-construction design and mechanized verification. Other results from CGCs are case studies which construct the first mechanically verified and correct-by-construction program analyzer, as well as other mechanically verified applications which benefit from using abstract interpretation. Reuse for Analyzer Implementations In this thesis we develop a program analysis framework called Galois Transformers (GTs) [Darais et al., 2015] to build reliable program analyzers from reusable components. The central principle of GTs is to unify the mathematical design of the analyzer with its implementation using executable state transition systems [Van Horn and Might, 2010]. However, state transition systems are not reusable across programming languages or for obtaining variations in analyzer precision. GTs solve half of the reuse problem for program analyzers—reuse of analyzer precision—by enriching the general structure of state transitions, and by implementing analysis machinery within this structure. As a result, GTs support implementing one important aspect of precision called path and flow sensitivity in a library. This library can be reused to construct new analyzers which feature path and flow sensitive precision for arbitrary programming languages, and without re-implementing complex analysis machinery. Building on GTs, we develop a program analysis framework called Abstracting 6 Definitional Interpreters (ADI) [Darais et al., 2017], also for the purpose of building reliable analyzers from reusable components. ADI solves the other half of the reuse problem for program analyzers—reuse of programming language features—by adopting programming language interpreters in place of state transition systems as the unified platform for designing and implementing analyzers. As a result, ADI supports implementing analysis machinery for individual programming language features in a library, as well as a second important variation in analysis precision called pushdown precision. In combination with the results of GTs, ADI supports rapidly prototyping reliable program analyzers using reusable components: first for the features of the programming language being analyzed, and second for obtaining variations in analysis precision required by the application domain. 1.1 Outline The remainder of this thesis is structured as follows: Chapter 2 presents necessary technical background. Chapter 3 presents an overview of the technical problems, main ideas, and evaluation methods presented in the remainder of the thesis. Chapters 4, 5 and 6 present the three main results of this thesis: Constructive Galois Connections, Galois Transformers and Abstracting Definitional Interpreters respectively. Chapter 7 concludes, and Chapter A provides supplementary proofs for Galois Transformer theorems from Chapter 5. 7 Chapter 2: Technical Background 2.1 Abstract Interpretation Abstract Interpretation is a foundational framework for designing and implementing program analyzers and type systems—as well as a plethora of other useful pro- gramming language tools [Cousot, 2008]—invented and developed by Cousot and Cousot [1999, 1976, 1977, 1979, 1992, 1994, 2014]. At a high level, the goal of abstract interpretation is to make precise what it means for some collection of objects to be an “abstraction” of another, and what it means for operations over abstract objects to be “representative” of operations over the objects which they abstract. This concept of abstraction is made precise as a particular mathematical relationship between sets. For example, any classification hierarchy can be seen as an abstraction, such as the class of “fruit,” for which both “apples” and “mangos” are represented. There are operations which can performed on fruit, such as juicing—which turns “apples” into “apple juice“—blending—which turns “apples” into “an apple smoothie”—and slicing plus dehydrating—which turns “apples” into “apple chips.” Likewise, these operations can be performed on “mangos,” with similar results. In the framework of abstract interpretation, the notion of an “abstract oper- 8 ation” is made precise such that one can specify each of juicing, blending, slicing and dehydrating at the abstract level of “fruit.” These abstract operations for creating “fruit juice,” “fruit smoothies” and “fruit chips” can then be shown to be compatible with all of the representative elements of “fruit,” that is “apples,” “mangos,” “pineapples” etc. We apply abstract interpretation in this thesis not for the purposes of describing fruit operations, but for describing program analyzers and their correctness criteria. The “apples” and “mangos” in this setting are computer programs—like the ones that implement Google’s search algorithm, or instruct the camera on your mobile phone to take a photo and store the contents in memory. The “fruit” in this setting are abstract classifications of these programs such as “safe,” “secure” or “efficient.” By making a formal connection between a particular program (the “apple”) and a property of interest like safety (the “fruit”), we design algorithms which automatically check whether or not this property holds—and justify their correctness—all within the guiding mathematical framework of abstract interpretation. 2.1.1 Galois Connection Mappings Central to the framework of abstract interpretation is a mathematical structure called a Galois connection, consisting of an abstraction mapping α which maps from a concrete domain C to an abstract domain A, and a concretization mapping γ which maps in the reverse direction.1 In general, the concrete and abstract domains are 1According to Cousot, the abstraction and concretizatio mappings are notated α and γ because they appear as the first and third letters of the greek alphabet, which mirror “a” and “c”, the first letters for each mapping, and the first and third letters of the english alphabet. 9 partially ordered sets, and the mappings are required to be monotonic, which we notate with an upward slanted arrow. (concrete domain) C : poset (abstract domain) A : poset (abstraction mapping) α : C → A (concretization mapping) γ : A → C Example Consider a very simple abstraction: the latin alphabet (L) which includes both lowercase and uppercase letters, and the logical latin alphabet (L̂) which unifies the letters a and A as the same “logical” letter. (latin characters) L = {a, A, . . . , z, Z} (logical characters) L̂ = {A, . . . , Z} We represent “logical” letters as the uppercase form. In order to map between each set, we lift them to powersets. This means elements of the concrete domain are sets of latin characters (e.g., {x, Y, z}), and elements of the abstract domain are sets of logical characters (e.g., {X, Y, Z}). (concrete domain) C := ℘(L) (abstract domain) A := ℘(L̂) The abstraction function (α) maps a set of latin characters (e.g., {x, y, Y, Z}) to the set of logical characters in that set (e.g., {X, Y, Z}). The concretization function is not an inverse mapping, rather it maps set of logical characters to the smallest set of latin characters which contains every set that abstracts to the original logical set of characters. For example, the concretization of {X, Y, Z} is {x, X, y, Y, z, Z}, because it is the smallest set that contains {x, y, z}, {X, y, z}, {x, X, y, z}, {x, Y, z}, etc., each of which abstract to {X, Y, Z}. For this example, we notate the pointwise abstraction of a single latin character η, and the pointwise concretization of a single logical 10 character µ. α : ℘(L) → ℘(L̂) γ : ℘(L̂) → ℘(L) η : L → L̂ µ : L̂ → ℘(L) α(X) := {η(x) | x ∈ X} γ(Y ) := ⋃ y∈Y µ(y) η(a) := A . . . η(z) := Z η(A) := A . . . η(Z) := Z µ(A) := {a, A} . . . µ(Z) := {z, Z} 2.1.2 Galois Connection Laws In addition to mapping between concrete and abstract domains, a Galois connection 〈α, γ〉 must obey the following laws: X v γ(α(X)) (GC-Extensive) α(γ(Y )) v Y (GC-Reductive) or equivalently, the following correspondence: X v γ(Y ) ⇐⇒ α(X) v Y (GC-Corr) Example Continuing the previous example: consider the collection of characters c := {X, y, Y, z}. We can describe which logical characters are contained in c as α(c) = {X, Y, Z}. We can also describe which literal characters are represented by this set of logical characters as γ(α(c)) = {x, X, y, Y, z, Z}. However, repeatedly applying 11 α and γ eventually converges: c = {X, y, Y, z} α(c) = {X, Y, Z} γ(α(c)) = {x,X, y, Y, z, Z} α(γ(α(c))) = {X, Y, Z} = α(c) γ(α(γ(α(c)))) = {x,X, y, Y, z, Z} = γ(α(c)) What (GC-Extensive) ensures in our example is that γ(α(c)) ⊇ c, or that “the abstraction for c (α(c)) includes c in its representation (γ(α(c))).” The second law (GC-Reductive) ensures that α(γ(α(c))) ⊆ α(c), or that “the abstraction for c (α(c)) is no smaller than the abstraction of its representation (α(γ(α(c)))).” Repeated applications of α and γ (in either direction) will necessarily converge after one iteration, which is referred to as idempotenecy : γ(α(γ(α(X)))) = γ(α(X)) α(γ(α(γ(Y )))) = α(γ(Y )) (Idempotency follows as a consequence of (GC-Extensive), (GC-Reductive), and partial order antisymmetry.) It is often the case (as in our example) that a stronger form of (GC-Reductive) holds, that is with an equality rather than partial ordering: α(γ(Y )) = Y (GC-Red-Strict) at which point the Galois connection is called a Galois insertion, or Galois surjection. 12 2.1.3 Abstract Interpreters The structure of a Galois connection 〈α, γ〉 determines both the meaning of soundness and optimality for an abstract operation (f̂)—which maps between elements of the abstract domain (A → A)—w.r.t. a concrete operation (f)—which maps between elements of the concrete domain (C → C). concrete operation: f : C → C abstract operation: f̂ : A → A Soundness or optimality is then demonstrated by relating the abstract operation (f̂) to an optimal specification induced from the concrete operation (α ◦ f ◦ γ), either using a partial order to establish soundness, or equality to establish optimality. α ◦ f ◦ γ v f̂ (GC-Sound) α ◦ f ◦ γ = f̂ (GC-Optimal) Example Continuing the running example: consider the concrete operation of concatenating two latin characters together to form a string, notated c1 ++ c2, e.g., x ++ y = xy. We lift this operation to operate over powersets in order to express concatenation over properties of characters (℘(L)), which is also the concrete domain C. This is often called the collecting semantics, because it supports expressing properties of interest over inputs and outputs to the operation, encoded as powersets. +˜+ : ℘(L)× ℘(L) → ℘(L × L) X1 +˜+ X2 := {c1 ++ c2 | c1 ∈ X1 ∧ c2 ∈ X2} 13 An abstraction of this operation Y1 +̂+ Y2 is considered sound if the abstract concate- nation of two sets of logical characters Y1 and Y2 contains all of the concatenations of sets of concrete characters γ(Y1) and γ(Y2), and optimal if the abstract concatenation of two sets of logical characters Y1 and Y1 is equal to all of the concrete concatenations. These are exactly the notions generated by the induced specifications (GC-Sound) and (GC-Optimal). It turns out that for this example, abstract concatenation is identical to concrete concatenation, that is: +̂+ : ℘(L̂)× ℘(L̂) → ℘(L̂ × L̂) Y1 +̂+ Y2 := {d1 ++ d2 | d1 ∈ Y1 ∧ d2 ∈ Y2} The statement that abstract concatenation (+̂+) is a sound approximation of the concrete concatenation (+˜+) is induced by the Galois connection defined previously: α(γ(Y1) +˜+ (Y2)) v Y1 +̂+ Y2 although its proof is nontrivial, and likewise for optimality: α(γ(Y1) +˜+ (Y2)) = Y1 +̂+ Y2 2.1.4 Calculational Abstract Interpretation Rather than postulate the definition of an abstract operation (f̂) and verify its soundness or optimality (via (GC-Sound) or (GC-Optimal)), one can instead derive a sound or optimal implementation directly from the induced specification. The chain of reasoning begins on the left-hand side with the induced optimal specification— 14 which is often not directly implementable as an algorithm—and proceeds through directed rewrites of the specification. At some point, the current state of reasoning is observed to have algorithmic content, or can be easily translated into an algorithm, and is declared to be the implementation of the abstract operator: α(f(γ(Y ))) . . . v . . . v . . . , f̂ If directed reasoning was used, as shown in the above mock-derivation, then the result is guaranteed to be sound by construction. If purely equational reasoning was used—so equalities (=) for each step rather than partial orders (v)—then the result is guaranteed to be optimal by construction as well. Example Continuing the running example: we will now derive a sound and optimal abstract concatenation operator using calculational abstract interpretation. First, the concrete collecting operation (+˜+) is lifted to a specification for an abstract operation through composition with abstraction and concretization mappings. We demonstrate the calculation using a specific instantiation of parameters Y1 and Y2, and later generalize the result. For now, consider Y1 = {X, Y} and Y2 = {Z}: α(γ({X, Y}) +˜+ γ({Z})) = . . . The first step in the calculation is to apply the concretization mapping (γ): . . . = α({x, X, y, Y} +˜+ {z, Z}) = . . . 15 The next step is to apply the collecting concatenation operation (+˜+): . . . = α({xz, yz, Xz, Yz, xZ, yZ, XZ, YZ}) = . . . The final step is to apply the abstraction mapping (α), after which we declare the result the implementation of abstract concatenation: . . . = {XZ, YZ} , {X, Y} +̂+ {Z} To generalize over arbitrary inputs Y1 and Y2, the derivation is carried out symbolically. The first step applies concretization, effectively containing the union of Y1 interpreted as both uppercase and lowercase, and likewise for Y2. The next step applies the collecting concatenation of these sets, which interleaves every possible combination of uppercase and lowercase. The final step applies abstraction, which eliminates redundant occurrences of uppercase and lowercase in the concrete set: α(γ(Y1) +˜+ γ(Y2)) = * applying γ + α((upper(Y1) ∪ lower(Y1)) +˜+ (upper(Y2) ∪ lower(Y2))) = * applying +˜+ + α  ⋃  {x1x2 | x1 ∈ upper(Y1) ∧ x2 ∈ upper(Y2)} {x1x2 | x1 ∈ lower(Y1) ∧ x2 ∈ upper(Y2)} {x1x2 | x1 ∈ upper(Y1) ∧ x2 ∈ lower(Y2)} {x1x2 | x1 ∈ lower(Y1) ∧ x2 ∈ lower(Y2)}  = * applying α + {y1y2 | y1 ∈ Y1 ∧ y2 ∈ Y2} , * by defining Y1 +̂+ Y2 := {y1y2 | y1 ∈ Y1 ∧ y2 ∈ Y2} + Y1+̂+Y2  16 2.1.5 Conclusion In this section we reviewed the essential structure of calculational abstract interpretation— both through its general definition, and through a simple running example based on an abstraction for latin characters which ignores whether or not a character is uppercase or lowercase. The capstone of the exercise was an implementation for an abstract concatenation operator, which was derived by calculus, and is therefore both sound and optimal by construction. The remainder of this thesis will make heavy use of abstract interpretation as a technique for justifying the soundness and optimality of program analyzers. One of the contributions in this thesis is an alternative setup for abstract interpretation called constructive Galois connections which allows deriving algorithms which not only sound or optimal, but computable by construction as well. 2.2 Abstracting Abstract Machines Abstracting Abstract Machines (AAM) is a technique for systematically deriving program analyzers directly from a description of that programming language, invented by Van Horn and Might [2010, 2012]. At a high level, the goal of AAM is to make designing program analyzers easier, and in particular for new and feature-rich programming languages. A program analyzer is essentially an algorithm which attempts to predict the behavior of individual programs, typically by classifying programs as either “definitely good” or “possibly bad.” To justify the correctness of the prediction, an exercise must be 17 performed which examines the semantics of the programming language—i.e. a formal description of what individual programs “mean”—and the content of the program analysis algorithm. Given these two artifacts, the exercise is to establish that every result computed by the algorithm offers a reliable prediction of the behavior of the program being analyzed. The core approach of AAM is: 1. To describe the semantics of the programming language using small-step operational semantics [Felleisen and Hieb, 1992, Plotkin, 1981]; and 2. A technique for systematically abstracting a concrete small-step semantics into an abstract small-step semantics Because the technique is systematic, it leaves little room for error (a good thing) or complex analysis techniques (a limitation). Therefore, to recover complex analysis techniques, the essence of the technique must be embedding in the concrete version of the semantics, such that it is present in the program analyzer after systematic abstraction. Although the abstraction process is mostly systematic, there is one parameter exposed after abstraction which has a large determining factor on the resulting program analysis: abstract allocation. In order to execute the program analysis, one must define an allocation strategy, and different strategies give rise to a wide range of possible program analysis techniques, with varying precision and performance tradeoffs [Gilray et al., 2016a]. 18 2.2.1 Small-step Semantics Central to the Abstracting Abstract Machines (AAM) technique [Van Horn and Might, 2010, 2012] is the setting of small-step operational semantics [Felleisen and Hieb, 1992, Felleisen et al., 1987, Plotkin, 1981]. A small-step operational semantics for a programming language is a mathematical relation between purely syntactic terms, which describes a relatively small unit of computation. The reflexive-transitive- closure of this relation is then taken to describe evaluation for the programming language, which fully reduces a program text to the output it computes. One distinguishing feature between approaches to small-step semantics is the treatment of the context of sub-computations. The approach we take is to treat contexts as an explicit object in the reduction system, a` la Felleisen and Friedman’s CEK machine [1987]. Example Consider a very simple programming language for adding natural num- bers, e.g., 5, PLUS(1, 2) and PLUS(PLUS(1, 2), PLUS(3, 4)) are all valid programs. The syntax for this language is described in BNF [Backus, 1959]: n ∈ N := {0, 1, 3, 4, . . .} e ∈ exp ::= n | PLUS(e, e) To represent contexts explicitly in the semantics, we define a language for evaluation contexts, and define configurations as a pairing of an expression to evaluate, and the 19 context for the evaluation: κ ∈ context ::= PLUS(, e) :: κ | PLUS(e,) :: κ | HALT ς ∈ config := exp× context The small-step semantics for these expressions is then expressed as a set of relational rules, describing in which configurations a step of computation occurs: (Small-step Evaluation) ς ς (Plus) 〈PLUS(n1, n2), κ〉 〈n1 + n2, κ〉 (PPushL) 〈PLUS(e1, e2), κ〉 〈e1, PLUS(, e2) :: κ〉 (PPushR) 〈PLUS(e1, e2), κ〉 〈e2, PLUS(e1,) :: κ〉 (PPopL) 〈n, PLUS(, e2) :: κ〉 〈PLUS(n, e2), κ〉 (PPopR) 〈n, PLUS(e1,) :: κ〉 〈PLUS(e1, n), κ〉 This relation is nondeterministic; for example, both 〈PLUS(PLUS(1, 2), PLUS(3, 4)), HALT〉 〈PLUS(1, 2), PLUS(, PLUS(3, 4)) :: HALT〉 〈PLUS(PLUS(1, 2), PLUS(3, 4)), HALT〉 〈PLUS(3, 4), PLUS(PLUS(1, 2),) :: HALT〉 are described by the relation. The reflexive-transitive-closure is often notated ∗, and for our example relates the example program to its evaluation result of 10: 〈PLUS(PLUS(1, 2), PLUS(3, 4)), HALT〉 ∗ 〈10, HALT〉 2.2.2 Adding Higher-order Functions To transition our concrete semantics to an abstract semantics, the primary goal is to achieve a finite state space for the domain of the relation. The reason for this is so that all the behavior of a program can be explored in finite time, which constitutes a decidable program analysis algorithm. This becomes challenging for 20 inductively defined components of the domain, and particularly challenging for mutually inductively defined components. The AAM approach to this problem is to introduce an explicit level of indirection between recursive occurrences of a structure, and to apply an allocation mechanism for referencing child-structures from parent-structures. Example Continuing the running example: currently the only source of non- finiteness in the state space for the relation (℘(exp × exp)) is the set of natural numbers (N). Thus, the goal of abstracting the current semantics poses no great challenge, and the AAM technique doesn’t necessarily apply. Let’s extend the language with higher-order functions, i.e lambda-terms, to see the AAM technique at work. First, we add variables (x), anonymous functions (λx. e), and function application (e(e)) as syntactic terms to the expression language: n ∈ N := {0, 1, 3, 4, . . .} x ∈ var := {x, y, . . .} e ∈ exp ::= n | PLUS(e, e) | x | λx. e | e(e) Next, we add environments to the domain of configurations, closures (〈λx. e, ρ〉) to the domain of values and control expressions, and extend contexts to carry the 21 environment under which that computation was initiatied: v ∈ val := N ∪ {〈λx. e, ρ〉} ρ ∈ env := var ⇀ val c ∈ control ::= v | n | PLUS(c, c) | x | λx. e | c(c) κ ∈ context := 〈PLUS(, c), ρ〉 :: κ | 〈PLUS(c,), ρ〉 :: κ | 〈(c), ρ〉 :: κ | 〈c(), ρ〉 :: κ | HALT ς ∈ config := control× env× context and we add the following rules to the small-step semantic relation: (Small-step Evaluation) ς ς (Var) 〈x, ρ, κ〉 〈ρ(x), ρ, κ〉 (Lam) 〈λx. e, ρ, κ〉 〈〈λx. e, ρ〉, ρ, κ〉 (Apply) 〈〈λx. e, ρ′〉(v), ρ, κ〉 〈e, ρ′[x 7→ v], κ〉 (APushL) 〈c1(c2), ρ, κ〉 〈c1, ρ, 〈(c2), ρ〉 :: κ〉 (APushR) 〈c1(c2), ρ, κ〉 〈c2, ρ, 〈c1(), ρ〉 :: κ〉 (APopL) 〈v, ρ, 〈(e), ρ′〉 :: κ〉 〈v(e), ρ′, κ〉 (APopR) 〈v, ρ, 〈e(), ρ′〉 :: κ〉 〈e(v), ρ′, κ〉 2.2.3 Adding Indirection through a Store Now our language supports higher-order-functions, but it has become much harder to abstract and finitize the state space. The key challenge is how to abstract contexts, which are defined recursively, and how to abstract both values and environments, which are defined mutually recursively. The AAM solution to abstraction in this setting is to introduce an explicit level of indirection through a store for recursively defined constructs. 22 Example To continue the running example: we modify the language with a level of indirection between linked contexts, and between values and environments: v ∈ val := N ∪ {〈λx. e, ρ〉} ` ∈ addr := (parameter) ρ ∈ env := var ⇀ addr σ ∈ store := addr ⇀ val ∪ context c ∈ control ::= v | n | PLUS(c, c) | x | λx. e | c(c) κ ∈ context := 〈PLUS(, c), ρ〉 :: ` | 〈PLUS(c,), ρ〉 :: ` | 〈(c), ρ〉 :: ` | 〈c(), ρ〉 :: ` | HALT ς ∈ config := control× env× store× context which requires modifying each reduction rule, only four of which we show here: (Small-step Evaluation) ς ς (Var) 〈x, ρ, σ, κ〉 〈σ(ρ(x)), ρ, σ, κ〉 (Apply) 〈〈λx. e, ρ′〉(v), ρ, σ, κ〉 〈e, ρ′[x 7→ `], σ[` 7→ v], κ〉 where ` := fresh (APushL) 〈c1(c2), ρ, σ, κ〉 〈c1, ρ, σ[` 7→ κ], 〈(c2), ρ〉 :: `〉 where ` := fresh (APopL) 〈v, ρ, σ, 〈(e), ρ′〉 :: `〉 〈v(e), ρ′, σ, σ(`)〉 2.2.4 Abstraction Once we have introduced indirection into the semantics to mediate between values and environments, we can then finitize the entire configuration space ς merely by picking finite abstractions for each of natural numbers (N) and addresses (addr). Once finitizing abstractions are picked for numbers and addresses, then the AAM approach systematically constructs an abstract semantics, which are directly implementable as an executable program analyzer. 23 Example To continue ther unning example, consider some abstraction for natural numbers N̂ and abstraction for addresses âddr, after which the state space becomes: v ∈ v̂al := N̂ ∪ ℘({〈λx. e, ρ〉}) ρ ∈ ênv := var ⇀ âddr σ ∈ ŝtore := âddr→ v̂al ∪ ℘(ĉontext) c ∈ ĉontrol ::= v | n | PLUS(c, c) | x | λx. e | c(c) κ ∈ ĉontext := 〈PLUS(, c), ρ〉 :: ` | 〈PLUS(c,), ρ〉 :: ` | 〈(c), ρ〉 :: ` | 〈c(), ρ〉 :: ` | HALT ς ∈ ĉonfig := ̂control × ênv× ŝtore× âddr and the following six (only, for brevity) rules become: (Small-step Evaluation) ς ς (Plus) 〈PLUS(v1, v2), ρ, σ, `〉 〈v1 +̂ v2, ρ, σ, `〉 (PPushL) ς︷ ︸︸ ︷ 〈PLUS(c1, c2), ρ, σ, `〉 〈c1, ρ, σ unionsq [`′ 7→ 〈PLUS(, c2), ρ〉 :: `], `′〉 where `′ := alloc(ς) (PPopL) 〈v, ρ, σ, `〉 〈PLUS(v, c2), ρ′, σ, `′〉 where 〈PLUS(, c2), ρ′〉 :: `′ ∈ σ(`) (Apply) ς︷ ︸︸ ︷ 〈v1(v2), ρ, σ, κ〉 〈e, ρ′[x 7→ `], σ unionsq [` 7→ v2], κ〉 where 〈λx. e, ρ′〉 ∈ v1 ` := alloc(〈λx. e, ρ′〉, ς〉) (APushL) ς︷ ︸︸ ︷ 〈c1(c2), ρ, σ, κ〉 〈c1, ρ, σ unionsq [`′ 7→ 〈(c2), ρ〉 :: `], `′〉 where `′ := alloc(ς) (APopL) 〈v, ρ, σ, `〉 (v(c2), ρ′, σ, `′〉 where 〈(c2), ρ′〉 :: `′ ∈ σ(`) (The alteration of the rest of the rules is analogous.) Note in particular the transition to multiple elements found in the store on function calls and stack popping, and the joining of results in the store on function application and stack pushing, e.g., when 24 the address already exists in the store from a separate binding instance. 2.2.5 Conclusion In this section we reviewed small-step operational semantics and the AAM approach to systematic abstraction of a concrete to an abstract semantics. This was done through a running example which extended a simple arithmetic expression programming language into one which includes higher-order functions. Allocation was introduced to break the recursive structure of the state space, and finitization was achieved by finitizing the domains for natural numbers and addresses. The remainder of this thesis will build heavily on the AAM approach to semantics abstraction. One of the contributions in this thesis is a technique called Galois transformers which introduces another parameter to the semantics alongside alloc for recovering variations in path and flow sensitivity. Another contribution in this thesis is a technique called abstract definitional interpreters which extends the AAM technique in full generality to the semantic setting of definitional interpreters (as opposed to operational small-step relations). 2.3 Mechanized Verification Mechanized Verification is a technique for establishing the correctness of a piece of software with the highest possible confidence we know how to achieve. In mechanized verification, formal proofs are constructed to establish the correctness of a piece of software—down to the smallest detail—and computers are used to check the validity 25 of these proofs rather than an expert human. To gain the highest level of assurance, the software which checks the proofs must be small, simple, and easy to inspect by experts to establish its correctness. The approach to mechanized verification for software considered in this thesis is that which uses (in spirit) intuitionist type theory (ITT) [Martin-Lo¨f, 1975, 1984], as embodied in proof assistants like Coq [development team, 2004] and Agda [Norell, 2007], each of which are implementations based on the calculus of inductive con- structions (CIC) [Coquand and Huet, 1988, 1985, Coquand and Paulin, 1990], a descendent of ITT. These proof assistants unify the language used to describe pro- grams, the language used to describe properties of programs, and the language used to describe proofs of these properties. At the center of this unified framework is an intrinsic notion of computation, which each of programs, properties, and proofs must carry. As a consequence of this, classical reasoning principles—such as the Law of Excluded Middle (LEM)—are disallowed in the theory, because they do not carry computational content. (Although LEM can be added as an axiom without interfering with the logic’s consistency, its use will interfere with the computational nature of the system.) Because ITT terms carry computational content, they can be interpreted and run as programs, typically by extraction to a functional language like OCaml, Haskell, or Scheme. It is in this way that certified implementations of various algorithms (like program analyzers) are produced: by embedding the algorithm as well as its proof of correctness in a proof assistant, and then extracting a functional program from the algorithm description, which is run using a conventional functional programming 26 language compiler and runtime. 2.3.1 Equality Central to any mechanized verification technique is a fundamental tradeoff between automatic proof construction (what you get for free), and manual proof construction (what you don’t get for free). In ITT (and therefore CIC), this tradeoff is embodied in two distinct notions of equality: so-called definitional equality, and so-called propositional equality, respectively. Definitional equalities, notated with =, are judgments which can be justified entirely through computation, e.g., 1 + 2 = 3 or [1, 2] ++ [3] = [1, 2, 3]. These equalities cannot be mentioned internally in the proof system, rather this equality is used as a congruence, that is the proof system is always considering the validity of judgemental equalities modulo definitional equality, i.e. modulo computation. Propositional equalities, notated with ≡, are judgements which require a manually or semi-automatically constructed proof. When the judgment is an embedding of a definitional equality, e.g., 1 + 2 ≡ 3, then the trivial proof term Refl is sufficient evidence, because the system carries out the reasoning of equality modulo computation automatically. When the judgment is non-trivial, e.g., ∀x. x+ 0 ≡ x, then a proof must be supplied in the form of a witness term, which is also a program with computational content, due to the unification of these concepts. 27 Example Consider the proof term of the right identity for addition in Agda: right-identity : ∀ x→ x+ 0 ≡ x right-identity Zero = Refl right-identity (Succ x′) rewrite right-identity x′ = Refl The proof performs case analysis on the universally quantified x. In the case x = 0, the goal becomes 0 + 0 ≡ 0. The system reasons automatically that 0 + 0 = 0, and so the proof term Refl is able to discharge the goal, which is equivalent to 0 ≡ 0 modulo computation. In the case x = 1+x′ for some x′, the goal becomes (1+x′)+0 ≡ 1+x′, which the system automatically converts to 1 + (x′ + 0) ≡ 1 + x′ via computation. The rewrite command explicitly rewrites the goal using the inductive hypothesis, that is x′ + 0 ≡ x, resulting in the goal 1 + x′ ≡ 1 + x′, which is directly provable by the reflexivity judgment Refl. 2.3.2 Embedding Classical Powersets A common occurrence in mathematics is to represent the set of predicates, or classifications, over some set A as the powerset ℘(A). In ITT, these powersets are represented directly as their characteristic functions φ : A→ prop. Because these characteristic functions can be undecidable in general, there is little that can be done with powersets other than construct other powersets. This leads to the powerset ℘(A) := A→ prop behaving as a modality in ITT which can’t be escaped from. 28 Example Consider the set of of integers which are odd. Classically this is repre- sented: odd-integers ∈ ℘(Z) := {z | odd(z)} However, In ITT this set is represented directly as its characteristic function φ := odd : Z→ prop. Next consider the following classical function which classifies an arbitrary set of integers using an abstract description: classify ∈ ℘(Z)→ {all-even, all-odd, empty, even-and-odd} classify(I) :=  all-even if ∃i ∈ I ∧ ∀i ∈ I. even(i) all-odd if ∃i ∈ I ∧ ∀i ∈ I. odd(i) empty if I = ∅ even-and-odd if ∃i1, i2 ∈ I. even(i1) ∧ odd(i2) This function is not representable in ITT because it requires computing the abstract description given an arbitrary set of integers. However, this function is definable by mapping to a singleton powerset: classify : ℘(Z)→ 1℘({all-even, all-odd, empty, even-and-odd}) classify(I) := ⋃  {all-even | ∃i ∈ I ∧ ∀i ∈ I. even(i)} {all-odd | ∃i ∈ I ∧ ∀i ∈ I. odd(i)} {empty | I = ∅} {even-and-odd | ∃i1, i2 ∈ I. even(i1) ∧ odd(i2)} This is a mapping between specifications, and is perfectly definable in ITT. The escape-hatch used here is the fact that singleton powersets 1 ℘(A) are not isomorphic to the underlying carrier A in a constructive setting (no mapping exists for 1 ℘(A)→ A), whereas in a classical setting 1 ℘(A) and A are isomorphic and interchangeable. 29 2.3.3 Embedding General Classical Reasoning Although variants of ITT do not allow direct definitions of the Law of Excluded Middle (LEM), they do support defining LEM and all other classical logical formulas via an embedding called double negation. This embedding serves as a modality in the logic which explicitly separates proofs which carry computational content (i.e. constructive) from those that don’t (i.e. classical), while still supporting fully general mathematical reasoning. Because of the existence of the double negation embedding, it would be incorrect to say ITT supports fewer theorems than a non-constructive higher-order logic. Terms embedded in the double negation type are still manifestations of “truth.” Example Consider the law of excluded middle, as stated classically: ∀p ∈ prop. p ∨ ¬p (LEM) The direct embedding of this proposition in ITT is a dependent product: ∏ p : prop p ∨ ¬p (LEM-ITT) the proof of which in ITT would be a dependent function: λp : prop. (. . . : p ∨ ¬p) (LEM-ITT-TERM) However, the constructive interpretation of λ-terms prohibit such a definition for arbitrary propositions, which are not always decidable. For example, consider instantiating (LEM-ITT) with the proposition “the Nth turing machine halts.” The 30 hypothetical (LEM-ITT-TERM) would then have to compute in finite time whether or not the proposition is true, however it is well known that this particular proposition is not decidable in general, hence there is no term which can inhabit (LEM-ITT). LEM can, however, be embedded in ITT in a way which explicitly discards the constraint that it carries computational content. The embedding is that of double negation, so: ∏ p : prop ¬¬(p ∨ ¬p)) (LEM-ITT-DN) where the definition of negation is: ¬p := p→ false The proposition (LEM-ITT-DN) is then a refutation of terms which claim to refute LEM. This proposition is inhabited in ITT: λp : prop. λX : ¬(p ∨ ¬p). X(Inr(λx : p. X(Inl(x)))) 2.3.4 Conclusion In this section we reviewed mechanized verification as achieved through Intuitionistic Type Theory (ITT). This was done through a discussion of how ITT is formulated as a logic where constructions carry computational content, which are then extracted and executed as certified programs. We then followed with discussions of equality and embedding classical notions like powersets and the Law of Excluded Middle (LEM), as well as supporting examples. 31 Portions of this thesis rely on mechanized verification using the Agda proof assistant, based on CIC, a descendent in the ITT family of logics. The computational nature of ITT is crucial in our use of these tools, as we extract certified programs not only from algorithms directly embedded in Agda, but we extract programs directly from proofs as well. The ability to extract programs directly from proofs is a well known feature of ITT, but has yet to be realized in the setting of calculational abstract interpretation until the results presented in this thesis. Chapter 3: Technical Overview 3.1 Constructive Galois Connections Galois connections are a foundational tool for structuring abstraction in semantics and their use lies at the heart of the theory of abstract interpretation. Yet, mechanization of Galois connections remains limited to restricted modes of use, preventing their general application in mechanized metatheory and certified programming. We present Constructive Galois Connections [Darais and Van Horn, 2016], a variant of Galois connections that is effective both on paper and in proof assistants; is complete w.r.t a large subset of classical Galois connections; and enables more general reasoning principles, including the “calculational” style advocated by Cousot. To design constructive Galois connection we identify a restricted mode of use of classical ones which is both general and amenable to mechanization in dependently- 32 typed functional programming languages. Crucial to the metatheory is the addition of monadic structure to Galois connections to control a “specification effect.” Ef- fectful calculations may reason classically, while pure calculations have extractable computational content. Explicitly moving between the worlds of specification and implementation is enabled by the metatheory. To validate the approach we provide two case studies in mechanizing existing proofs from the literature: one uses calculational abstract interpretation to design a static analyzer [Cousot, 1999], the other forms a semantic basis for gradual typing [Garcia et al., 2016]. Both mechanized proofs closely follow their original paper-and-pencil counterparts, employ reasoning principles not captured by previous mechanization approaches [Monniaux, 1998, Pichardie, 2005], support the extraction of verified algorithms, and are novel. 3.1.1 The Problem The issue with the classical Galois connection framework is that some functions cannot be defined constructively, and the consequence of this is that definitions which use Galois connections cannot always be extracted as verified algorithms. To illustrate the problems with mechanizing classical Galois connections, con- sider a simple parity program analyzer designed using the following Galois connection 33 between natural numbers N and parities P (plus Galois Connection laws not shown): P := {even,odd} α : ℘(N)→ ℘(P) γ : ℘(P)→ ℘(N) α(N) := ⋃ n∈N {even} if even(n){odd} if odd(n) γ(P ) := ⋃ p∈P {n | even(n)} if p = even{n | odd(n)} if p = odd A program analyzer A : ℘(P)→ ℘(P) for a concrete semantics C : ℘(N)→ ℘(N) is then justified by relating to the composition of C with α and γ: α ◦ C ◦ γ v A (Soundness) The trouble in mechanizing (Soundness) is that A is expected to be computable, meaning its type ℘(P)→ ℘(P) represents an algorithm mapping between finite sets of parities. However, the specification α ◦ C ◦ γ, also at type ℘(P)→ ℘(P), represents an induced specification which cannot in general be computed. In a constructive setting, these two powerset types have different representations. Constructed powersets ℘(P) are modeled with a datatype like a list or binary tree, or in the case of ℘(P) as an enumeration of its inhabitants: ℘(P) ≈ P+ := {⊥,even,odd,>} However, specification powersets ℘(P) are modeled as predicates on P: ℘(P) ≈ P→ prop On paper the encoding of ℘(P) doesn’t matter, but to perform verified program extraction on A, a solution must be found for encoding proofs like sound which transition between specification and algorithm. 34 The current state-of-the art in mechanized abstract interpretation is to only embed γ in the proof assistant, because α is the problematic mapping w.r.t. construc- tivity. This results in so-called γ-only definitions and proofs, for example (Soundness) has an equivalent formulation using only γ: C ◦ γ v γ ◦ A (Soundness [γ-only]) However, this approach doesn’t allow for the calculational approach to abstract interpretation, where the very definition of A is derived directly from its induced specification α ◦ C ◦ γ. 3.1.2 The Main Ideas We develop constructive Galois connections from the insight that many classical Galois connections used in practice are of a particular restricted form, which is reminiscent of a direct-style verification. Constructive Galois connections are the general abstraction theory for this setting and can be mechanized effectively. We observe that constructive Galois connections contain monadic structure which isolates classical specifications from constructive algorithms. Within the effectful fragment, all of classical Galois connection reasoning can be employed, while within the pure fragment, functions must carry computational content. Remarkably, calculations can move between these modalities and verified programs may be extracted from the end result of calculation. 35 Constructive Galois Connections Our constructive theory of Galois connec- tions can be seen as a restricted mode of use of classical Galois connections. The essence of the theory is a different adjunction η/µ instead of α/γ: η : N→ P µ : P→ ℘(N) η(n) := even if even(n)odd if odd(n) µ(p) := {n | even(n)} if p = even{n | odd(n)} if p = odd along with the adjunction correspondence: n ∈ µ(p) ⇐⇒ η(n) v p (CGC-Corr) In this restricted theory, executable algorithms can be extracted directly from the results of proofs in the abstract interpretation paradigm. This setting supports all the benefits of a general abstraction framework like classical Galois connections: synthesized specifications, soundness and completeness properties, and even calculational derivations of program analyzers. Classical Galois connections can be recovered from constructive Galois through a lifting: α : ℘(N)→ ℘(P) γ : ℘(P)→ ℘(P) α(N) := {η(n) | n ∈ N} γ(P ) := ⋃ p∈P µ(p) as well as the classical Galois connection correspondence: N ⊆ γ(P ) ⇐⇒ α(N) ⊆ P We also demonstrate that when a constructive Galois connection exists underneath a classical Galois connection, all properties which could be proved in the classical 36 setting can likewise be proved in the constructive setting, which results in much simpler proofs. The Specification Effect We call the powerset type ℘(A) a specification effect because it has monadic structure, supports encoding arbitrary properties over values in A, and cannot be “escaped from” in constructive logic, similar to the IO monad in Haskell. In classical mathematics, there is an isomorphism between singleton powersets ℘1(A) and the set A. However, no such constructive mapping exists for ℘1(A) → A. Such a function would decide arbitrary predicates in A → prop to compute the A inside the singleton set. This observation, that you can program inside ℘( ) monadically in constructive logic, but you can’t escape the monad, is why we call it a specification effect. The soundness and completeness conditions generated by constructive Galois connections come from a monadic adjunction, and are therefore recast in a monadic setting. For example, the constructive equivalent to (Soundness) is: pure(η)~ C ~ µ v A (Soundness η/µ) Both sides of the equation have type P→ ℘(P), the monadic interpretation of which is “a function from P to P which performs specification effects.” This is empowering because it allows one to be explicit about the induced pure(η) ~ C ~ µ being a specification, meaning it has effects, and the analysis A being an algorithm, meaning it has no effects. One can even derive the definition of A from this specification, and in the process eliminate the “specification effect” through program calculation, the 37 results of which can immediately be extracted and executed. 3.1.3 Evaluation To support the utility of our theory we build a library for constructive Galois connections in Agda [Norell, 2007] and mechanize two existing abstract interpretation proofs from the literature. The first is drawn from Cousot’s monograph [1999], which derives a correct-by-construction analyzer from a specification induced by a concrete interpreter and Galois connection. The second is drawn from Garcia et al.’s Abstracting Gradual Typing [2016], which uses abstract interpretation to derive static and dynamic semantics for gradually typed languages from traditional static types. Both proofs use the calculational style of abstract interpretation which is not handled by prior mechanization approaches. The mechanized proofs closely follow the original pencil-and-paper proofs, which use both abstraction and concretization, while still enabling the extraction of certified algorithms. Neither of these papers have been previously mechanized. Moreover, we know of no existing mechanized proof involving calculational abstract interpretation. Finally, we develop the metatheory of constructive Galois connections, prove them sound, and make precise their relationship to classical Galois connections. The metatheory is also itself mechanized in Agda. 38 3.2 Galois Transformers The design and implementation of static analyzers has become increasingly systematic. Yet although the design is systematic, implementing an analyzer and proving it sound remains a tedious and error prone effort. The issue is that static analysis features and their proofs of soundness do not compose well, preventing reuse in both implementation and metatheory. Due to the lack of compositional components, small changes to an analyzer’s design often require large changes to its implementation and proof. We solve the problem of constructing static analyzers and their proofs from reusable components by introducing Galois Transformers [Darais et al., 2015]: monad transformers that transport Galois connection properties. In concert with a monadic interpreter, we define analysis parameters that implement building blocks for classic analysis features like context, object, heap, path and flow (in)sensitivity. Each component comes with modular proofs and is defined independently of a particular programming language semantics. Significantly, Galois transformers are proven sound once and for all, making them truly reusable analysis components. As new analysis features and abstractions are developed and mixed in, soundness proofs need not be reconstructed, as the composition of a monad transformer stack is sound by virtue of its constituents. Galois transformers provide a viable foundation for reusable and composable metatheory for program analysis, and are amenable to mechanized verification with proof assistants. Finally, Galois transformers shift the level of abstraction in analysis design and 39 implementation. Using Galois transformers, non-specialists are able to synthesize sound analyzers over a number of parameters, which can then be then be tuned in plug-and-play fashion to recovering a wide range of analyses. Tuning parameters in our framework requires no change to the implementation or proof of correctness, which enables rapids prototyping of the analyzer design space. 3.2.1 The Problem The problem with current approaches to program analysis design is they are unable to account for path and flow sensitivity as a parameter to the analysis, both in implementation and proof. To illustrate path and flow sensitivity, consider verifying the absence of division by zero errors in the following program: (1) function example(i : int)→ int (2) var x, y : int (3) if i 6= 0 (4) then x := 0 (5) else x := 1 (6) if i 6= 0 (7) then y := 100/i (8) else y := 100/x (9) return y This program branches on the function argument i to define x such that i 6= 0⇔ x = 0 (and therefore i = 0⇔ x 6= 0) in lines 3–5. The goal of the analysis is to discover division by zero errors, and the two divisions at lines 7 and 8 are always safe because 40 of the above correlation between x and i. To verify the above program as free of division by zero errors, a Path Sensitive (PS) analysis is required, which is computationally expensive. A less precise but more performant design is a Flow Sensitive (FS) analysis, which is only able to verify the first division at line 7. Finally, a Flow Insensitive (FI) analysis is the least precise design choice, is unable to verify either of the divisions, but is far more performant that a PS or FS design. These three variations of analysis—path sensitive (PS), flow sensitive (FS) and flow insensitive (FI)—are strictly ordered in terms of precision: prec(PS) > prec(FS) > prec(FI) and inversely ordered in terms of both average and worst-case performance: perf(FI) > perf(FS) > perf(PS) In security-critical settings, path sensitivity is often the right choice despite the added cost. However, in performance-critical settings, path sensitivity is infeasible because of its cost, which suggests using a flow sensitive or flow-insensitive analysis. In order to rapidly prototype this design space to find the best fit for a particular application, the path and flow sensitivity of the analyzer must be compartmentalized and supplied as a parameter. Previous approaches to program analysis require rewriting large parts of the design to support each variant of path and flow sensitivity. The issue is magnified in the setting of mechanized verification, where rewriting an implementation means 41 rewriting a proof, and where the proof effort of a development is much more costly than that of a pencil-and-paper formalization. 3.2.2 The Main Ideas Galois transformers are reusable building blocks for building analysers that supply each choice in the path sensitivity spectrum: flow insensitive, flow sensitive and path sensitive. A flow insensitive Galois transformers can simply be replaced by a path sensitive Galois transformer, requiring no further change to the analyzer or its proof. In this way, one can rapidly prototype the path and flow sensitivity design space for a particular program analysis. Proofs of correctness for the analyzer also carry over between different instantiations of Galois transformers. Galois transformers support rapidly prototyping choices in path and flow sensitivity by introducing a novel parameter to the program analyzer: the monad used for executing the analysis. Monads are introduced in the analyzer design to capture the interaction between analysis results (like x 6= 0) and nondeterministic branching in the analyzer (like analyzing if x = 5 then y else z when x 6= 0). By changing the monad, and therefore how analysis results and nondeterminism interact, we recover each of PS, FS and FI implementations for the analyzer. PS Monad Analyzer FS Monad FI Monad 42 Designing Parameterized Analyzers To design an analyzer parameterized by a monad, one first identifies the parts of the analysis which communicate analysis results and the parts which branch due to nondeterminism. Rather than implement these parts of the analysis directly, the analyzer is instead written using a monadic effect interface consisting of state and nondeterminism effects. The state effect is used for manipulating analysis results, and the nondeterminism effect is used for branching. The analyzer can be executed only after instantiating its monad parameter with some monad that implements state and nondeterminism effects. Most importantly, a monadic analyzer can be proven correct using monad and monad effect laws, independent from a particular monad instantiation. Constructing Monad Parameters To help construct monads for a parame- terized analyzer we design a library of monad transformers. Monad transformers are compositional building blocks for constructing monads. The monad transformers used to construct a monad, as well as their order of assembly, determine the path and flow sensitivity properties of the analysis. We identify three monads for use in our setting: the state monad transformer (StateT) which implements state effects, the nondeterminism monad transformer (NondetT) which implements nondeterminism effects, and the finite map monad transformer (FinMapT) which implements both nondeterminism and state effects. [Each of StateT, NodnetT and FinMapT are generally useful, even outside our application to program analyzers. StateT is standard from the literature [Liang et al., 1995, Moggi, 1989], and NondetT and FinMapT are novel in this work.] These 43 monad transformers can be assembled in any order, and use the identity monad (ID) as the starting point of composition. StateT NondetT IDFinMapT Enumerating the combinations of monad transformers which implement both state and nondeterminism results in PS, FS and FI monads. When plugged into a parameterized interpreter the result is a path sensitive, flow sensitive and flow insensitive program analyzer respectively. PS Monad StateT NondetT = FS Monad= FI Monad= ID FinMapT ID StateT NondetT ID Furthermore we show that these monad transformers also propagate Galois con- nections, which is essential for achieving modularity in the soundness proofs for a parameterized analyzer. We call these monad transformers Galois transformers because of their Galois connection properties. 3.2.3 Evaluation We evaluate Galois transformers by proving key metatheory properties of end-to-end static analysis verification, and by implementing a Galois transformers library and prototype client analysis in Haskell. 44 End-to-end Correctness The end-to-end correctness of a static analyzer in our setting is justified using compositional components: 1. a proof that the monadic interpreter recovers the concrete semantics, 2. a proof that the monadic interpreter is monotonic, and 3. a proof of abstraction between concrete and abstract monad parameters. The user of our framework is responsible for (1) and (2). We prove that (3) is synthesized by the properties of Galois transformers, in addition to the implication that (1–3) yields a sound program analysis. 3.3 Abstracting Definitional Interpreters Two dominant schools of thought for designing program analyzers are the constraint- based approach and small-step state-machines-based approach. In each paradigm (respectively), the analysis is computed by the least-fixed-point of a set of constraint equations, or through reachability of a relational small-step collecting semantics. On the other hand, there is a large body of work on denotational semantics and definitional interpreters, or so-called “big-step” interpreters. Definitional interpreters are popular for describing concrete semantics because they are compositional by nature. However, definitional interpreters have not seen adoption for describing abstract semantics, or as the basis for defining program analyzers, particularly in the higher order setting. To bridge this gap we develop Abstract Definitional Interpreters [Darais et al., 45 2017] and show that definitional interpreters written in monadic style can express not only the usual notion of (concrete) interpretation, but also a wide variety of collecting semantics, abstract interpretations, symbolic execution, and their intermixings. In this work we reconstruct a definitional abstract interpreter for a higher-order language to use monadic operations and a novel fixpoint iteration strategy. Through a monadic definitional design, we achieve a computable abstract interpreter that arises from the composition of simple, independent components. Remarkably, the resulting program analyzer implements a form of pushdown control flow analysis (PDCFA) in which calls and returns are always properly matched in the abstract semantics. True to the definitional style of Reynolds, the evaluator involves no explicit mechanics to achieve this property; it is simply inherited from the defining language. 3.3.1 The Problem The challenge when using definitional interpreters as a foundation for program analysis is the treatment of fixpoints. For a definitional interpreter, the meaning of fixpoints in the object language is inherited from the metalanguage. This is problematic when metalanguage fixpoints involve nontermination, which prevents obtaining a computable program analysis. Another challenge with definitional interpreters is they omit description of 46 intermediate execution configurations. For example, consider this program: function loop()→ void var x := 42 while(true) skip The concrete denotation of calling loop is ⊥, which does not mention intermediate facts about the program, like x = 42. Small-step and constraint-based approaches support analysis of intermediate results because they are by-nature explicit about reachable program configurations. When tuning the precision of a program analysis, a challenging point of the design is approximating the call-and-return structure of program execution. To illustrate this, consider analyzing the following program: (1) function id(x : any)→ any (2) return x (3) function main()→ void (4) var y := id(1) (5) print("Y") (6) var z := id(2) (7) print("Z") The printed output of this program is "YZ". However, most control-flow analyzers will report that the output could be any string that matches the regular expression "Y*Z". The problem is that control flow analyzers construct a global graph of call edges, in this case from lines 5 and 7 to the body of id, and return edges, in this case from id back to lines 5 and 7. Without precise call-return matching, control-flow analyzers get confused and think the program could call id at line 5 and 47 then return to line 7, or call id at line 7 and return to line 5. A “k-call-site-sensitive” analysis can distinguish these cases, but only up to a finite call-depth. A pushdown analysis solves the call/return matching problem up to infinite depth. Prior descriptions of pushdown analysis are set in the context of actual pushdown automata [Reps et al., 1995], Dyck state graphs [Earl et al., 2012] or small- step state machines [Gilray et al., 2016b, Johnson and Van Horn, 2014, Vardoulakis and Shivers, 2010], and each approach requires ad-hoc extensions and instrumentation to the design of the program analyzer. 3.3.2 The Main Ideas Our key insights are to design definitional interpreters in monadic, open-recursive style, and to design a novel fixpoint algorithm tailored specifically to the setting of higher-order definitional interpreters. The extensible nature of the interpreter allows us to recover a wide-range of analyses through its instantiation, including widening techniques, precision preserving abstractions, and symbolic execution for program verification. We also realize a new technique for defining abstract interpreters with pushdown precision, meaning the analysis precisely matches function calls to returns. In the setting of definitional interpreters, this property is inherited from the defining metalanguage and requires no instrumentation to the analysis at all. Unfixing Interpreters To support finding fixpoints for definitional interpreters, we first design definitional interpreters in open-recursive style. For example, the 48 denotation of an if expression is written: E : (exp→ val)→ exp→ val . . . E(E ′)(if e1 then e2 else e3) := if E ′(e1) ? = True then E ′(e2) else E ′(e3) . . . The standard evaluator is then recovered by Y (E), and we allow abstract evaluators to be defined through (total) approximating fixpoint finding functions. To find fixpoints for abstract definitional interpreters, we design a novel caching algorithm (Y ]), which when applied to an unfixed interpreter yields a sound and computable analysis. To support abstract fixpoints, we redesign the unfixed evaluator to consume and output a cache so it can communicate with the fixpoint algorithm. (1) Y ] : ((exp× cache→ val× cache)→ exp× cache→ val× cache) (2) → exp→ cache (2) Y ](F )(e) := lfp(λ$o. (3) let rec E := F (λ〈e, $I〉. (4) if e ∈ $I then 〈$I(e), $I〉 else (5) let 〈v, $I′〉 := E(e, $I [e 7→ $O(e)]) (6) in 〈v, $I′[e 7→ v]〉) (7) in pi2(E(e))) The algorithm computes the least-fixed-point of a cache ($o) which is computed by calling the unfixed evaluator (F ) and intercepting recursive calls (at (e, $I)) to either not repeat work if it has already been done (line 4), record the current configuration so as to not loop in the future (line 5), and record the results of evaluation in the cache (line 6). 49 Monadic Definitional Interpreters To support a multitude of different analyzers from a single definitional interpreter, we write the open-recursive evaluator in monadic style, so the above example becomes: E : (exp→M(val))→ exp→M(val) . . . E(E ′)(if e1 then e2 else e3) := do v1 ← E ′(e1) if v1 ? = True then E ′(e2) else E ′(e3) . . . Different monads can then be plugged into the evaluator to recover different analyzers. The monadic abstraction is also essential to treat the cache state-passing version of the evaluator in a systematic way, as just another cell of monadic state. Inheriting Pushdown Precision Small-step methods to programming language semantics must model the context of evaluation, either through syntactic evaluation contexts or stack frames. Perfect stack precision is lost in this approach because stack frames are modeled explicitly, and the process of approximation is applied naively to the model of the stack. We observe that perfect stack precision is already present in the definitional interpreter, and therefore yields a pushdown analysis, even when executed as an approximating abstract interpreter. In the case of definitional interpreters, the evalu- ation context is implicit in the call-and-return semantics of the defining programming language, which is already perfectly precise. Because no approximation is made in the model for evaluation contexts, the resulting abstraction for evaluation contexts 50 is perfectly precise. 3.3.3 Evaluation We implement a general framework of abstract definitional interpreters in Racket and recover various abstract interpreters, including various widening techniques, a mixed concrete/abstract abstraction for arithmetic expressions, and a symbolic executor which can perform program verification. We prove the approach sound w.r.t. a derived big-step semantics, where the key insight in the formalism is to model not only standard big-step evaluation relations, but also big-step reachability relations, which we carry out through each of concrete, collecting, and abstract semantics. The formalism begins with a big-step semantics (ρ ` e, σ ⇓ σ′) augmented with big-step reachability (ρ ` e, σ ⇑ ς) which describes reachable configurations ς. We show a combination of these relations (JeKbs) forms a “complete” big-step semantics in that it contains the same information as the small-step setting (JeKss). We then perform systematic abstraction of the complete big-step semantics (JeKbs) and justify computing analysis solutions as the least-fixpoint of a cache which simulates both big-step evaluation and reachability. 51 Chapter 4: Constructive Galois Connections 4.1 Introduction Abstract interpretation is a general theory of sound approximation widely applied in programming language semantics, formal verification, and static analysis [Cousot and Cousot, 1976, 1977, 1979, 1992, 2014]. In abstract interpretation, properties of programs are related between a pair of partially ordered sets: a concrete domain, 〈C,v〉, and an abstract domain, 〈A,〉. When concrete properties have a -most precise abstraction, the correspondence is a Galois connection, formed by a pair of mappings between the domains known as abstraction α ∈ C 7→ A and concretization γ ∈ A 7→ C such that c v γ(a) ⇐⇒ α(c)  a. Since its introduction by Cousot and Cousot in the late 1970s, this theory has formed the basis of static analyzers, type systems, model-checkers, obfuscators, program transformations, and many more applications [Cousot, 2008]. Given the remarkable set of tools contributed by this theory, an obvious desire is to incorporate its use into proof assistants to mechanically verify proofs by abstract interpretation. When embedded in a proof assistant, verified algorithms such as static analyzers can then be extracted from these proofs. Monniaux first achieved mechanization for the theory of abstract interpretation 52 with Galois connections in Coq [1998]. However, he notes that the abstraction (α) side of Galois connections is problematic since it requires the admission of non-constructive axioms. Use of these axioms prevents the extraction of certified programs. So while Monniaux was able to mechanically verify proofs by abstract interpretation in its full generality, certified artifacts could not extracted in general. Pichardie subsequently tackled the extraction problem by using a restricted formulation of abstract interpretation that relied only on the concretization (γ) side of Galois connections [2005]. Doing so avoids the use of axioms and enables extraction of certified artifacts. This technique is effective and has been used to construct several certified static analyzers [Barthe et al., 2007, Blazy et al., 2013, Cachera and Pichardie, 2010, Pichardie, 2005], most notably the Verasco static analyzer, part of the CompCert C compiler [Jourdan et al., 2015, Leroy, 2009]. Unfortunately, this approach sacrifices the full generality of the theory. While in principle the technique could achieve mechanization of existing soundness theorems, it cannot do so faithful to existing proofs. In particular, Pichardie writes [2005, p. 55]:1 The framework we have retained nevertheless loses an important property of the standard framework: being able to derive a correct approximation f ] from the specification α ◦ f ◦ γ. Several examples of such derivations are given by Cousot [1999]. It seems interesting to find a framework for this kind of symbolic manipulation, while remaining easily formalizable in Coq. 1Translated from French by the present author. 53 This important property is the so-called “calculational” style, where an abstract interpreter (f ]) is derived in a correct-by-construction manner from a concrete interpreter (f) composed with abstraction and concretization (α ◦ f ◦ γ). This calculational method is detailed in Cousot’s monograph [1999], which concludes: The emphasis in these notes has been on the correctness of the design by calculus. The mechanized verification of this formal development using a proof assistant can be foreseen with automatic extraction of a correct program from its correctness proof. In the subsequent 17 years, this vision has remained unrealized, and clearly the paramount technical challenge in achieving it is obtaining both generality and constructivity in a single framework. In this chapter we contribute constructive Galois connections, a framework for abstract interpretation with Galois connections that achieves both generality and constructivity, thereby enabling calculational style proofs which make use of both abstraction (α) and concretization (γ), while also maintaining the ability to extract certified static analyzers. We develop constructive Galois connections from the insight that many classical Galois connections used in practice are of a particular restricted form—which is reminiscent of a direct-style verification—and that this restricted form both supports calculation and is amenable to mechanized verification. Constructive Galois connections are the general abstraction theory for this restricted setting of classical Galois connections. We observe that constructive Galois connections contain monadic structure 54 which isolates classical specifications from constructive algorithms. Within the effectful fragment, all of classical Galois connection reasoning can be employed, while within the pure fragment, functions must carry computational content. Remarkably, calculations can move between these modalities and verified programs may be extracted from the end result of calculation, which must be “effect-free.” To support the utility of our theory we build a library for constructive Galois connections in Agda [Norell, 2007] and mechanize two existing abstract interpretation proofs from the literature. The first is drawn from Cousot’s monograph [1999], which derives a correct-by-construction analyzer from a specification induced by a concrete interpreter and Galois connection. The second is drawn from Garcia et al.’s Abstracting Gradual Typing [2016], which uses abstract interpretation to derive static and dynamic semantics for gradually typed languages from traditional static types. Both proofs use the “important property of the standard framework” identified by Pichardie, which is not handled by prior mechanization approaches. The mechanized proofs closely follow the original pencil-and-paper proofs, which use both abstraction and concretization mappings, while still enabling the extraction of certified algorithms. Neither of these papers have been previously mechanized. Moreover, we know of no existing mechanized proof involving calculational abstract interpretation. Next, we develop the metatheory of constructive Galois connections, prove they are sound and complete, and make precise their relationship to classical Galois connections. The metatheory is itself mechanized; claims are marked with “AGDAX” whenever they are proved in Agda. (All claims are marked.) 55 Finally, we explore the relationship between classical and constructive Galois connections in much more detail. We do this through defining constructive analogs to classical Galois connection primitive and connectives, and through two examples drawn from our first case study which we work out in full detail. Through these extended examples, we compare and contrast the differences between abstraction- directed and concretization-directed calculations, and between sound and complete calculations, for both classical and constructive styles. The outcome of this study is a better understanding of how constructive calculations interact with classical Galois connections, how the mechanics of optimality changes between frameworks, and how to calculate multivalued algorithms in the constructive setting. Contributions This chapter contributes the following: • A foundational theory of constructive Galois connections which is both gen- eral and amenable to mechanization using a dependently typed functional programming language; • A proof library and two case studies from the literature for mechanized abstract interpretation; and • The first mechanization of calculational abstract interpretation; and • A detailed discussion of the relationship between constructive and classical Galois connections, and their interaction. The remainder of the chapter is organized as follows. First we give a tutorial on verifying a simple analyzer from two different perspectives: direct verification 56 (§ 4.2.1) and abstract interpretation with Galois connections (§ 4.2.2), highlighting mechanization issues along the way. We then present constructive Galois connections as a marriage of the two approaches (§ 4.3). We provide two case studies: the mechanization of an abstract interpreter from Cousot’s calculational monograph (§ 4.4), and the mechanization of Garcia, Clark and Tanter’s work on gradual typing via abstract interpretation (§ 4.5). Next, we formalize the metatheory of constructive Galois connections (§ 4.6), define constructive analogs of common classical Galois connection primitives and connectives (§ 4.7), and work through two extended examples in detail: the first to compare and contrast calculation styles (§ 4.8) and discuss deriving optimal interpreters (§ 4.9), and the second to explore multivalued constructive calculations (§ 4.10). Finally, we relate our work to the literature (§ 4.11), and conclude (§ 4.12). 4.2 Verifying a Simple Static Analyzer In this section we contrast two perspectives on verifying a static analyzer: using a direct approach, and using the theory of abstract interpretation with Galois connections. The direct approach is simple but lacks the benefits of a general abstraction framework. Abstract interpretation provides these benefits, but at the cost of added complexity and resistance to mechanized verification. In Section 4.3 we present an alternative perspective: abstract interpretation with constructive Galois connections—the topic of this chapter. Constructive Galois connections marry the two worlds presented in this section, providing the simplicity of direct verification, the 57 benefits of a general abstraction framework, and support for mechanized verification. To demonstrate both verification perspectives we design a parity analyzer in each style. For example, a parity analysis discovers that 2 has parity even, succ(1) has parity even, and n+ n has parity even if n has parity odd. Rather than sketch the high-level details of a complete static analyzer, we instead zoom into the low-level details of a tiny fragment: analyzing the successor arithmetic operation succ(n). At this level of detail the differences, advantages and disadvantages of each approach become apparent. 4.2.1 The Direct Approach Using the direct approach to verification one designs the analyzer, defines what it means for the analyzer to be sound, and then completes a proof of soundness. Each step is done from scratch, and in the simplest way possible. This approach should be familiar to most readers, and exemplifies how most researchers approach formalizing soundness for static analyzers: first posit the analyzer and soundness framework, then attempt the proof of soundness. One limitation of this approach is that the setup—which gives lots of room for error—isn’t known to be correct until after completing the final proof. However, a benefit of this approach is it can easily be mechanized. Analyzing Successor A parity analysis answers questions like: “what is the parity of succ(n), given that n is even?” To answer these questions, imagine replacing n with the symbol even, a stand-in for an arbitrary even number. This hypothetical 58 expression succ(even) is interpreted by defining a successor function over parities, rather than numbers, which we call succ]. This successor operation on parities is designed such that if p is the parity for n, succ](p) will be the parity of succ(n): P := {even, odd} succ] : P→ P succ](even) := odd succ](odd) := even Soundness The soundness of succ] is defined using an interpretation for parities, which we notate JpK: J K : P→ ℘(N) JevenK := {n | even(n)}JoddK := {n | odd(n)} Given this interpretation, a parity p is a valid analysis result for a number n if the interpretation for p contains n, that is n ∈ JpK. The analyzer succ](p) is then sound if, when p is a valid analysis result for some number n, succ](p) is a valid analysis result for succ(n): n ∈ JpK =⇒ succ(n) ∈ Jsucc](p)K (DA-Snd) The proof is by case analysis on p; we show the case p = even: n ∈ JevenK ⇔ even(n) * defn. of J K + ⇔ odd(succ(n)) * defn. of even/odd + ⇔ succ(n) ∈ JoddK * defn. of J K + ⇔ succ(n) ∈ Jsucc](even)K * defn. of succ] + An Even Simpler Setup There is another way to define and prove soundness: use a function which computes the parity of a number in the definition of soundness. This approach is even simpler, and will foreshadow the constructive Galois connection 59 setup. parity : N→ P parity(0) := even parity(succ(n)) := flip(parity(n)) where flip(even) := odd and flip(odd) := even. This gives an alternative and equivalent way to relate a number and a parity, due to the following correspondence: n ∈ JpK ⇐⇒ parity(n) = p (DA-Corr) The soundness of the analyzer is then restated: parity(n) = p =⇒ parity(succ(n)) = succ](p) or by substituting parity(n) = p: parity(succ(n)) = succ](parity(n)) (DA-Snd*) Both this statement for soundness and its proof are simpler than before. The proof follows directly from the definition of parity and the fact that succ] is identical to flip. The Main Idea Correspondences like (DA-Corr)—between an interpretation for analysis results (JpK) and a function which computes them (parity(n))—are central to the constructive Galois Connection framework we will describe in Section 4.3. Using correspondences like these, we build a general theory of abstraction that recovers this direct approach to verification, mirrors all of the benefits of abstract interpretation with classical Galois connections, supports mechanized verification, and in some cases simplifies the proof effort. We also observe that many classical 60 Galois connections used in practice can be ported to this simpler setting. Mechanized Verification This direct approach to verification is amenable to mechanization using proof assistants like Coq and Agda. These tools are founded on constructive logic in part to support verified program extraction. In constructive logic, functions f : A → B are computable and often defined inductively to ensure they can be extracted and executed as programs. Analogously, powersets X : ℘(A) are encoded constructively as undecidable predicates P : A → prop where x ∈ X ⇔ P (x). To mechanize the verification of succ] we first translate its definition to a constructive setting unmodified. Next we translate JpK to a relation I(p, n) defined inductively on n: I(even, 0) I(p, n) I(flip(p), succ(n)) The mechanized proof of (DA-Snd) using I is analogous to the one we sketched, and the mechanized proof of (DA-Snd*) follows directly by computation. The proof term for (DA-Snd*) in both Coq and Agda is simply refl, the reflexivity judgment for syntactic equality modulo computation in constructive logic. Wrapping Up The two different approaches to verification we present are distin- guished by which parts are postulated, and which parts are derived. Using the direct approach, the analyzer (succ]), the interpretation for parities (JpK) and the definition of soundness (DA-Snd) are postulated up-front. When the soundness setup is correct but the analyzer is wrong, the proof at the end will not go through and the analyzer 61 must be redesigned. Even worse, when both the soundness setup and analyzer are wrong, the proof might actually succeed, giving a false assurance in the soundness of the analyzer. However, the direct approach is attractive because it is simple and supports mechanized verification. 4.2.2 Classical Abstract Interpretation To verify an analyzer using abstract interpretation with Galois connections, one first designs abstraction and concretization mappings between sets N and P. These mappings are used to synthesize an optimal specification for succ]. One then proves that a postulated succ] meets this synthesized specification, or alternatively derives the definition of succ] directly from the optimal specification. In contrast to the direct approach, rather than design the definition of soundness, one instead designs the definition of abstraction within a structured framework. Soundness is therefore not designed, it is derived directly from the definition of abstraction. Finally, there is added boilerplate in the abstract interpretation approach, which requires lifting definitions and proofs to powersets. Abstracting Sets Powersets are introduced in abstraction and concretization functions to support relational mappings, like mapping the symbol even to the set of all even numbers. The mappings are therefore between powersets ℘(N) and ℘(P). The abstraction and concretization mappings must also satisfy correctness criteria, detailed below, after which they are called a Galois connection. The abstraction mapping from ℘(N) to ℘(P) is notated α, and is defined as 62 the pointwise lifting of parity(n): α : ℘(N)→ ℘(P) α(N) := {parity(n) | n ∈ N} The concretization mapping from ℘(P) to ℘(N) is notated γ, and is defined as the flattened pointwise lifting of JpK: γ : ℘(P)→ ℘(N) γ(P ) := {n | p ∈ P ∧ n ∈ JpK} The correctness criteria for α and γ is the following correspondence: N ⊆ γ(P ) ⇐⇒ α(N) ⊆ P (GC-Corr) The correspondence means that, to relate elements of different sets—in this case ℘(N) and ℘(P)—it is equivalent to relate them through either α or γ. Mappings like α and γ which share this correspondence are called Galois connections. An equivalent formulation of (GC-Corr) is two laws relating compositions of α and γ, called expansive and reductive: N ⊆ γ(α(N)) (GC-Exp) α(γ(P )) ⊆ P (GC-Red) Property (GC-Red) ensures α is the best abstraction possible w.r.t. γ. For example, a hypothetical definition α(N) := {even, odd} is expansive, but not reductive because α(γ({even})) 6⊆ {even}. In general, Galois connections are defined for arbitrary posets 〈A,vA〉 and 〈B,vB〉. The correspondence (GC-Corr) and its expansive/reductive variants are 63 generalized in this setting to use partial orders vA and vB instead of subset order- ing. We are omitting monotonicity requirements for α and γ at this point in our presentation, although these requirements are essential in the complete approach. Powerset Lifting The original functions succ and succ] cannot be related through α and γ because they are not functions between powersets. To remedy this they are lifted pointwise: ↑succ : ℘(N)→ ℘(N) ↑succ] : ℘(P)→ ℘(P) ↑succ(N) := {succ(n) | n ∈ N} ↑succ](P ) := {succ](p) | p ∈ P} These lifted operations are called the concrete interpreter and abstract interpreter, because the former operates over the concrete domain ℘(Z) and the latter over the abstract domain ℘(P). In the framework of abstract interpretation, static analyzers are just abstract interpreters. Lifting to powersets is necessary to use the abstract interpretation framework, and has the negative effect of adding boilerplate to definitions and proofs of soundness. Soundness The definition of soundness for succ] is synthesized by relating ↑succ] to ↑succ composed with α and γ: α(↑succ(γ(P ))) ⊆ ↑succ](P ) (GC-Snd) The left-hand side of the ordering is an optimal specification for any abstraction of ↑succ (optimality being a consequence of (GC-Corr)), and the subset ordering says ↑succ] is an over-approximation of this optimal specification. The reason to over-approximate is because the specification is a mathematical description, and the 64 abstract interpreter is usually an algorithm, and therefore not always able to match the specification precisely. The proof of (GC-Snd) is by case analysis on P . We do not show the proof, rather we demonstrate a proof later in this section which also synthesizes the definition of succ]. One advantage of the abstract interpretation framework is that it provides a choice between four soundness properties, all of which are generated by α and γ, and equivalent as a consequence of (GC-Corr): α(↑succ(γ(P ))) ⊆ ↑succ](P ) (GC-Snd/αγ) ↑succ(γ(P )) ⊆ γ(↑succ](P )) (GC-Snd/γγ) α(↑succ(N)) ⊆ ↑succ](α(N)) (GC-Snd/αα) ↑succ(N) ⊆ γ(↑succ](α(N))) (GC-Snd/γα) Because each soundness property is equivalent, one can choose whichever variant is easiest to prove. The soundness setup (GC-Snd) is the αγ rule, however any of the other rules can also be used. For example, one could choose αα or γα; in these cases the proof considers four disjoint cases for N : N is empty, N contains only even numbers, N contains only odd numbers, and N contains both even and odd numbers. Completeness The mappings α and γ also synthesize an optimality statement for ↑succ], by stating that it under -approximates the optimal specification: α(↑succ(γ(P ))) ⊇ ↑succ](P ) 65 Because the left-hand-side is an optimal specification, an abstract interpreter will never be strictly more precise. Therefore, optimality is written equivalently using an equality: α(↑succ(γ(P ))) = ↑succ](P ) (GC-Opt) Not all analyzers are optimal, however optimality helps identify those which approxi- mate too much. Consider the analyzer ↑succ]′: ↑succ]′ : ℘(P)→ ℘(P) ↑succ]′(P ) := {even, odd} This analyzer reports that succ(n) could have any parity regardless of the parity for n; it’s the analyzer that always says “I don’t know.” This analyzer is perfectly sound but non-optimal because ↑succ]′({even}) = {even, odd} 6= α(↑succ(γ({even}))). Just like soundness, four completeness statements are generated by α and γ, however each of these statements are not equivalent: α(↑succ(γ(P ))) = ↑succ](P ) (GC-Cmp/αγ) ↑succ(γ(P )) = γ(↑succ](P )) (GC-Cmp/γγ) α(↑succ(N)) = ↑succ](α(N)) (GC-Cmp/αα) ↑succ(N) = γ(↑succ](α(N))) (GC-Cmp/γα) Abstract interpreters which satisfy the αγ variant are called optimal because they lose no more information than necessary, and those which satisfy the γα variant are called precise because they lose no information at all. The abstract interpreter succ] is optimal, but not precise because γ(↑succ](α({1}))) 6= ↑succ({1}). 66 To overcome mechanization issues with Galois connections, the state-of-the-art is restricted to use γγ rules only for soundness (GC-Snd/γγ) and completeness (GC-Cmp/γγ). This is unfortunate for completeness properties because unlike soundness, each completeness variant is not equivalent. Calculational Derivation of Abstract Interpreters Rather than posit ↑succ] and prove it correct directly, one can instead derive its definition through a calculational process. The process begins with the optimal specification on the left-hand-side of (GC-Opt), and reasons equationally towards the definition of an algorithm. In this way, ↑succ] is not postulated, rather it is derived by calculation, and the result is both sound and optimal by construction. The derivation is by case analysis on P which has four cases: {}, {even}, {odd} and {even, odd}; we show P = {even}: α(↑succ(γ({even}))) = α(↑succ({n | even(n)})) * defn. of γ + = α({succ(n) | even(n)}) * defn. of ↑succ + = α({n | odd(n)}) * defn. of even/odd + = {odd} * defn. of α + , ↑succ]({even}) * defining ↑succ] + The derivations for the other cases are analogous, and together they define the implementation of ↑succ]. Deriving analyzers by calculus is attractive because it is systematic, and because it prevents the issue where an analyzer is postulated and discovered to be unsound only after failing to complete its soundness proof. However, this calculational style 67 of abstract interpretation is not amenable to mechanized verification with program extraction because α is often non-constructive, an issue we describe later in this section. Added Complexity The abstract interpretation approach requires a Galois connection up-front which necessitates the introduction of powersets ℘(N) and ℘(P). This results in powerset-lifted definitions and adds boilerplate set-theoretic reasoning to the proofs. This is in contrast to the direct approach which never mentions powersets of parities. Not using powersets results in more understandable soundness criteria, requires no boilerplate set-theoretic reasoning, and results in fewer cases for the proof of soundness. This boilerplate becomes magnified in a mechanized setting where all details must be spelled out to a proof assistant. Furthermore, the simpler proof of (DA-Snd*)—which was immediate from the definition of parity—cannot be recovered within the general abstract interpretation framework, rather it must be formulated as a special case. Therefore, in the current state of affairs, one is required to abandon potentially simpler proof techniques in exchange for the benefits of the abstract interpretation framework. Resistance to Mechanized Verification Despite the beauty and utility of abstract interpretation with Galois connections, advocates of the approach have yet to reconcile their use with advances in mechanized reasoning: every mechanized verification of an executable abstract interpreter to-date has resisted the use of Galois 68 connections, even when initially designed on paper to take advantage of the framework. The issue in mechanizing Galois connections amounts to a conflict between supporting both classical set-theoretic reasoning and executable static analyzers. Supporting executable analyzers calls for constructive mathematics, which is a problem for α functions because they are often non-constructive, an observation first made by Monniaux [1998]. To work around this limitation, Pichardie [2005] advocates for designing abstract interpreters which are merely inspired by Galois connections, but ultimately avoid their use in verification, which he terms the “γ- only” approach. Successful verification projects such as Verasco adopt this “γ-only” technique [Jourdan et al., 2015, Leroy, 2009], despite the use of Galois connections in designing the original Astre´e analyzer [Blanchet et al., 2003]. To understand the foundational issues with Galois connections and α functions, consider verifying the soundness of the parity analyzer using a proof assistant and abstract interpretation. In this setting, the encoding of the Galois connection must support elements of infinite powersets—like the set of all even numbers—as well as executable abstract interpreters which manipulate elements of finite powersets—like {even, odd}. To support representing infinite sets, the powerset ℘(N) is modeled constructively as a predicate N→ prop. To support defining executable analyzers that manipulate finite sets of parities, the powerset ℘(P) is modeled as an enumeration of its inhabitants, which we call Pc: Pc := {even, odd,⊥,>} where ⊥ and > represent {} and {even, odd}. This enables a definition for ↑succ] : 69 Pc → Pc which can be extracted and executed. The consequence of this design is a Galois connection between N→ prop and Pc; the issue is now α: α : (N→ prop)→ Pc This version of α cannot be defined constructively, as doing would require deciding arbitrary predicates φ where φ : N→ prop. A hypothetical definition for α would perform case analysis on predicates like ∃n, φ(n) ∧ even(n) to compute an element of Pc, which is not possible for arbitrary φ. (The exercise also fails if powersets are modeled with decidable predicates φ : N → B.) However, γ can be defined constructively as a relation (2-ary proposition): γ : Pc → (N→ prop) In general, any theorem of soundness using Galois connections can be rewritten to use only γ, making use of (GC-Corr); this is the essence of the “γ-only” approach, embodied by the soundness variant (GC-Snd/γγ). However, this principle does not apply to all proofs of soundness using Galois connections, many of which mention α in practice. For example, the γ-only setup does not support calculation in the style advocated by Cousot [1999]. Furthermore, not all completeness theorems can be translated to γ-only style, such as (GC-Cmp/γα) which is used to show an abstract interpreter is fully precise. Wrapping Up Abstract interpretation differs from the direct approach in which parts of the design are postulated and which parts are derived. The direct approach 70 requires postulating the analyzer and definition of soundness. Using abstract inter- pretation, a Galois connection between sets is postulated instead, and definitions for soundness and completeness are synthesized from the Galois connection. Also, abstract interpretation support deriving the definition of a static analyzer directly from its proof of correctness. The downside of abstract interpretation is that it requires lifting succ and succ] into powersets, which results in boilerplate set-theoretic reasoning in the proof of soundness. Finally, due to foundational issues, the abstract interpretation framework is not amenable to mechanized verification while also supporting program extraction using constructive logic. 4.3 Constructive Galois Connections In this section we describe abstract interpretation with constructive Galois connec- tions: a parallel universe of Galois connections analogous to classical ones. The framework enjoys all the benefits of abstract interpretation, but like the direct approach avoids the pitfalls of added complexity and resistance to mechanized verification. We will describe the framework of constructive Galois connections between sets A and B. When instantiated to N and P, the framework recovers exactly the direct approach from Section 4.2.1. We will also describe constructive Galois connections in the absence of partial orders, or more specifically, we will assume the discrete partial order: x v y ⇔ x = y. (Partial orders didn’t appear in our demonstration of classical 71 abstract interpretation, but they are essential to the general theory.) We describe generalizing to partial orders and recovering classical results from constructive ones at the end of this section. The fully general theory of constructive Galois connections is described in Section 4.6 where it is compared side-by-side to classical Galois connections. Abstracting Sets A constructive Galois connection between sets A and B contains two mappings: the first is called extraction, notated η, and the second is called interpretation, notated µ: η : A→ B µ : B → ℘(A) η and µ are analogous to classical Galois connection mappings α and γ. In the parity analysis described in Section 4.2.1, the extraction function was parity and the interpretation function was J K. Constructive Galois connection mappings η and µ must form a correspondence similar to (GC-Corr): x ∈ µ(y) ⇐⇒ η(x) = y (CGC-Corr) The intuition behind the correspondence is the same as before: to compare an element x in A to an element y in B, it is equivalent to compare them through either η or µ. Like classical Galois connections, the correspondence between η and µ is stated equivalently through two composition laws. Extraction functions η which form a constructive Galois connection are also a “best abstraction,” analogously to α in the 72 classical setup: x ∈ µ(η(x)) (CGC-Ext) x ∈ µ(y) =⇒ η(x) = y (CGC-Red) Aside We use the term extraction function and symbol η from Nielson et al. [1999] where η is used to simplify the definition of an abstraction function α. We recover α functions from η in a similar way. However, their treatment of η is a side-note to simplifying the definition of α and nothing more. We take this simple idea much further to realize an entire theory of abstraction around η/µ functions and their correspondences. In this “lowered” theory of η/µ we describe soundness/optimality criteria and calculational derivations analogous to that of α/γ while support mechanized verification, none of which is true of Nielson et al.’s use of η. Induced Specifications Four equivalent soundness criteria are generated by η and µ just like in the classical framework. Each soundness statement uses η and µ in a different but equivalent way (assuming (CGC-Corr)). For a concrete f : A→ A and abstract f ] : B → B, f ] is sound iff any of the following properties hold: x ∈ µ(y) ∧ y′ = η(f(x)) =⇒ y′ = f ](y) (CGC-Snd/ηµ) x ∈ µ(y) ∧ x′ = f(x) =⇒ x′ ∈ µ(f ](y)) (CGC-Snd/µµ) y = η(f(x)) =⇒ y = f ](η(x)) (CGC-Snd/ηη) x′ = f(x) =⇒ x′ ∈ µ(f ](η(x))) (CGC-Snd/µη) 73 In the direct approach to verifying an example parity analysis described in Sec- tion 4.2.1, the first soundness property (DA-Snd) is generated by the µµ variant, and the second soundness property (DA-Snd*) which enjoyed a simpler proof is generated by the ηη variant. We write these soundness rules in a slightly strange way so we can write their completeness analogs simply by replacing ⇒ with ⇔. The origin of these rules comes from an adjunction framework, which we discuss in Section 4.6. The mappings η and µ also generate four completeness criteria which, like classical Galois connections, are not equivalent: x ∈ µ(y) ∧ y′ = η(f(x)) ⇐⇒ y′ = f ](y) (CGC-Cmp/ηµ) x ∈ µ(y) ∧ x′ = f(x) ⇐⇒ x′ ∈ µ(f ](y)) (CGC-Cmp/µµ) y = η(f(x)) ⇐⇒ y = f ](η(x)) (CGC-Cmp/ηη) x′ = f(x) ⇐⇒ x′ ∈ µ(f ](η(x))) (CGC-Cmp/µη) Inspired by classical Galois connections, we call abstract interpreters f ] which satisfy the ηµ variant optimal and those which satisfy the µη variant precise. The above soundness and completeness rules are stated for concrete and abstraction functions f : A → A and f ] : B → B. However, they generalize easily to relations R : ℘(A×A) and predicate transformers F : ℘(A)→ ℘(A) (i.e. collecting semantics) through the adjunction framework discussed in Section 4.6. The case studies in Sections 4.4 and 4.5 describe abstract interpreters over concrete relations and their soundness conditions. 74 Calculational Derivation of Abstract Interpreters The constructive Galois connection framework also supports deriving abstract interpreters through calculation, analogously to the calculation we demonstrated in Section 4.2.2. To support calculational reasoning, the four logical soundness criteria are rewritten into statements about subsumption between powerset elements: {η(f(x)) | x ∈ µ(y)} ⊆ {f ](y)} (CGC-Snd/ηµ*) {f(x) | x ∈ µ(y)} ⊆ µ(f ](y)) (CGC-Snd/µµ*) {η(f(x))} ⊆ {f ](η(x))} (CGC-Snd/ηη*) {f(x)} ⊆ µ(f ](η(x))) (CGC-Snd/µη*) The completeness analog to the four rules replaces set subsumption with equality. Using the ηµ* completeness rule, one calculates towards a definition for f ] starting from the left-hand-side, which is the optimal specification for abstract interpreters of f . To demonstrate calculation using constructive Galois connections, we show the derivation of succ] from its induced specification, the result of which is sound and optimal (because each step is = in addition to ⊆) by construction; we show 75 p = even: {parity(succ(n)) | n ∈ JevenK} = {parity(succ(n)) | even(n)} * defn. of J K + = {flip(parity(n)) | even(n)} * defn. of parity + = {flip(even)} * Eq. DA-Corr + = {odd} * defn. of flip + , {succ](even)} * defining succ] + We will show another perspective on this calculation later in this section, where the derivation of succ] is not only sound and optimal by construction, but computable by construction as well. Mechanized Verification In addition to the benefits of a general abstraction framework, constructive Galois connections are amenable to mechanization in a way that classical Galois connections are not. In our Agda library and case studies we mechanize constructive Galois connections in full generality, as well as proofs that use both mapping functions, such as calculational derivations. As we discussed in Sections 4.2.1 and 4.2.2, the constructive encoding for infinite powersets ℘(A) is A→ prop. This results in the following types for η and µ when encoded constructively: η : N→ P µ : P→ N→ prop In constructive logic, the arrow type N → P classifies computable functions, and the arrow type P→ N→ prop classifies undecidable relations. (CGC-Corr) is then 76 mechanized without issue: µ(p, n) ⇐⇒ η(n) = p See the mechanization details in Section 4.2.1 for how η and µ are defined construc- tively for the example parity analysis. Wrapping Up Constructive Galois connections are a general abstraction frame- work similar to classical Galois connections. At the heart of the constructive Galois connection framework is a correspondence (CGC-Corr) analogous to its classical counterpart. From this correspondence, soundness and completeness criteria are synthesized for abstract interpreters. Constructive Galois connections also support calculational derivations of abstract interpreters which and sound and optimal by construction. In addition to these benefits of a general abstraction framework, constructive Galois connections are amenable to mechanized verification. Both extraction (η) and interpretation (µ) can be mechanized effectively, as well as proofs of soundness, completeness, and calculational derivations. 4.3.1 Partial Orders and Monotonicity The full theory of constructive Galois connections generalizes to posets 〈A,vA〉 and 〈B,vB〉 by making the following changes: • Powersets must be downward-closed, that is for X : ℘(A): x ∈ X ∧ x′ v x =⇒ x′ ∈ X (PowerMon) 77 Singleton sets {x} are reinterpreted to mean {x′ | x′ v x}. For mechanization, this means ℘(A) is encoded as an antitonic function, notated with a down-right arrow A → prop, where the partial ordering on prop is by implication. • Functions must be monotonic, that is for f : A→ A: x v x′ =⇒ f(x) v f(x′) (FunMon) We notate monotonic functions f : A → A. Monotonicity is required for mappings η and µ, and concrete and abstract interpreters f and f ]. • The constructive Galois connection correspondence is generalized to partial orders in place of equality, that is for η and µ: x ∈ µ(y) ⇐⇒ η(x) v y (CGP-Corr) or alternatively, by generalizing the reductive property: x ∈ µ(y) =⇒ η(x) v y (CGP-Red) • Soundness criteria are also generalized to partial orders: x ∈ µ(y) ∧ y′ v η(f(x)) =⇒ y′ v f ](y) (CGP-Snd/ηµ) x ∈ µ(y) ∧ x′ v f(x) =⇒ x′ ∈ µ(f ](y)) (CGP-Snd/µµ) y v η(f(x)) =⇒ y v f ](η(x)) (CGP-Snd/ηη) x′ v f(x) =⇒ x′ ∈ µ(f ](η(x))) (CGP-Snd/µη) We were careful to write the equalities in Section 4.3 in the right order so this 78 change is just swappping = for v. Completeness criteria are identical with ⇔ in place of ⇒. To demonstrate when partial orders and monotonicity are necessary, consider design- ing a parity analyzer for the max operator: max] : P× P→ P max ](even, even) := even max](odd, odd) := odd max](even, odd) := ? max](odd, even) := ? The last two cases for max] cannot be defined because the maximum of an even and odd number could be either even or odd, and there is no representative for “any number” in P. To remedy this, we add any to the set of parities: P+ := P∪{any}; the new element any is interpreted: JanyK := {n | n ∈ N}; the partial order on P+ becomes: even, odd v any; and the correspondence continues to hold using this partial order: n ∈ Jp+K ⇐⇒ parity(n) v p+. max] is then defined using the abstraction P+ and proven sound and optimal following the abstract interpretation paradigm. 4.3.2 Relationship to Classical Galois Connections We clarify the relationship between constructive and classical Galois connections in three ways: • Any constructive Galois connection can be lifted to obtain an equivalent classical Galois connection, and likewise for soundness and completeness proofs. • Any classical Galois connection which can be recovered by a constructive one contains no additional expressive power, rendering it an equivalent theory with 79 added boilerplate reasoning. • Not all classical Galois connections can be recovered by constructive ones. From these relationships we conclude that one benefits from using constructive Galois connections whenever possible, classical Galois connections when no constructive one exists, and both theories together as needed. We make these claims precise in Section 4.6, and explore the subtleties of their relationship in detail in sections 4.8, 4.9 and 4.10. A classical Galois connection is recovered from a constructive one through the following lifting: α : ℘(A)→ ℘(B) γ : ℘(B)→ ℘(A) α(X) := {η(x) | x ∈ X} γ(Y ) := {x | y ∈ Y ∧ x ∈ µ(y)} When a classical Galois connection can be written in this form for some η and µ, then one can use the simpler setting of abstract interpretation with constructive Galois connections without any loss of generality. We also observe that many classical Galois connections in practice can be written in this form, and therefore can be mechanized effectively using constructive Galois connections. The case studies in presented in Sections 4.4 and 4.5 are two such cases, although the original authors of those works did not initially write their classical Galois connections in this explicitly lifted form. An example of a classical Galois connection which is not recovered by lifting is the Independent Attributes (IA) abstraction, which abstracts relations R : ℘(A×B) 80 with their component-wise splitting 〈Rl, Rr〉 : ℘(A)× ℘(B): α : ℘(A×B)→ ℘(A)× ℘(B) γ : ℘(A)× ℘(B)→ ℘(A×B) α(R) := 〈{x | ∃y.〈x, y〉 ∈ R}, {y | ∃x.〈x, y〉 ∈ R}〉 γ(Rl, Rr) := {〈x, y〉 | x ∈ Rl, y ∈ Rr} This Galois connection is amenable to mechanized verification. In a constructive setting, α and γ are maps between A× B → prop and (A→ prop)× (B → prop), and can be defined directly using logical connectives ∃ and ∧: α(R) := 〈λx.∃y.R(x, y), λy.∃x.R(x, y)〉 γ(Rl, Rr) := λ〈x, y〉.Rl(x) ∧Rr(y) IA can be mechanized effectively because the Galois connection consists of mappings between specifications, and the foundational issue of constructing values from speci- fications does not appear. IA is not a constructive Galois connection because there is no pure function µ underlying the abstraction function α. Because constructive Galois connections can be lifted to classical ones, a constructive Galois connection can interact directly with IA through its lifting, even in a mechanized setting. However, once a constructive Galois connection is lifted it loses its computational properties and cannot be extracted and executed. In practice, IA is used to weaken (v) an induced optimal specification after which the calculated interpreter is shown to be optimal (=) up-to-IA. IA never appears in the final calculated interpreter, so not having a constructive Galois connection formulation poses no issue. We explore how a constructive Galois connection derivation interacts with IA in detail in sections 4.8 and 4.9. 81 4.3.3 The “Specification Effect” The machinery of constructive Galois connections follow a monadic effect discipline, where the effect type is the classical powerset ℘( ); we call this a specification effect. First we will describe the monadic structure of powersets ℘( ) and what we mean by “specification effect.” Then we will recast the theory of constructive Galois connections in this monadic style, giving insights into why the theory supports mechanized verification, and foreshadowing key fragments of the metatheory we develop in Section 4.6. The monadic structure of classical powersets is standard, and is analogous to the nondeterminism monad familiar to Haskell programmers. However, the model ℘(A) := A → prop is the uncomputable nondeterminism monad and mirrors the use of set-comprehensions on paper to describe uncomputable sets (specifications), rather than the use of monad comprehensions in Haskell to describe computable sets (constructed values). We generalize ℘( ) to a monotonic monad, similarly to how we generalized powersets to posets in Section 4.3.1. This results in monotonic versions of monad operators ret and bind: ret : A → ℘(A) ret(x) := {x′ | x′ v x} bind : ℘(A)× (A → ℘(B)) → ℘(B) bind(X, f) := {y | x ∈ X ∧ y ∈ f(x)} We adopt Moggi’s notation [1989] for monadic extension where bind(X, f) is written f ∗(X), or just f ∗ for λX.f ∗(X). We call the powerset type ℘(A) a specification effect because it has monadic 82 structure, supports encoding arbitrary properties over values in A, and cannot be “escaped from” in constructive logic, similar to the IO monad in Haskell. In classical mathematics, there is an isomorphism between singleton powersets ℘1(A) and the set A. However, no such constructive mapping exists for ℘1(A)→ A. Such a function would decide arbitrary predicates in A→ prop to compute the A inside the singleton set. This observation, that you can program inside ℘( ) monadically in constructive logic, but you can’t escape the monad, is why we call it a specification effect. Given the monadic structure for powersets, and the intuition that they encode a specification effect in constructive logic, we can recast the theory of constructive Galois connections using monadic operators. To do this we define a helper operator which injects “pure” functions into the “effectful” function space: pure : (A → B) → (A → ℘(B)) pure(f)(x) := ret(f(x)) We then rewrite (CGC-Corr) using ret and pure: ret(x) ⊆ µ(y) ⇐⇒ pure(η)(x) ⊆ ret(y) (CGM-Corr) and we rewrite the expansive and reductive variant of the correspondence using ret, bind (notated f ∗) and pure: ret(x) ⊆ µ∗(pure(η)(x)) (CGM-Exp) pure(η)∗(µ(y)) ⊆ ret(y) (CGM-Red) The four soundness and completeness conditions can also be written in monadic 83 style; we show the ηµ soundness property here: pure(η)∗(pure(f)∗(µ(y))) ⊆ pure(f ])(y) (CGM-Snd) The left-hand-side of the ordering is the optimal specification for f ], just like (CGC-Snd/ηµ) but using monadic operators. The right-hand-side of the ordering is f ] lifted to the monadic function space. The constructive calculation of succ] we showed earlier in this section is a calculation of this form. The specification on the left has type ℘(P), and it has effects, meaning it uses classical reasoning and can’t be executed. The abstract interpreter on the right also has type ℘(P), but it has no effects, meaning it can be extracted and executed. The calculated abstract interpreter is thus not only sound and optimal by construction, it is computable by construction. Constructive Galois connections are empowering because they treat specification like an effect, which optimal specifications ought to have, and which computable abstract interpreters ought not to have. Using a monadic effect discipline we support calculations which start with a specification effect, and where the “effect” is eliminated through the process of calculation. The monad laws are crucial in canceling uses of ret with bind to arrive at a final pure computation. For example, the first step in a derivation for (CGM-Snd) can immediately simplify using monad laws to: pure(η ◦ f)∗(µ(y)) ⊆ pure(f ])(y) 84 4.4 Case Study 1: Calculational AI In this section we apply constructive Galois connections to the Calculational Design of a Generic Abstract Interpreter from Cousot’s monograph [1999]. To our knowledge, we achieve the first mechanically verified abstract interpreter derived by calculus. The key challenge in mechanizing the interpreter is supporting both abstraction (α) and concretization (γ) mappings, which are required by the calculational approach. Classical Galois connections do not support mechanization of α without the use of axioms, and these required axioms block computation, preventing the extraction of verified algorithms. To verify Cousot’s generic abstract interpreter we use constructive Galois connections, which we describe in Section 4.3 and formalize in Section 4.6. Using constructive Galois connections we encode extraction (η) and interpretation (µ) mappings as constructive analogs to α and γ, calculate an abstract interpreter for an imperative programming language which is sound and computable by construction, and recover the original classical Galois connection results through a systematic lifting. First we describe the setup for the analyzer: the abstract syntax, the concrete semantics, and the constructive Galois connections involved. Following the abstract interpretation paradigm with constructive Galois connections we design abstract interpreters for denotation functions and semantics relations. We show a fragment of our Agda mechanization which closely mirrors the pencil-and-paper proof, as well as Cousot’s original derivation. 85 i ∈ Z := {. . . ,−1, 0, 1, . . .} integers b ∈ B := {true, false} booleans x ∈ var ::= . . . variables ⊕ ∈ aop ::= + | − | × | / arithmetic op.< ∈ cmp ::= < | = comparison op. < ∈ bop ::= ∨ | ∧ boolean op. ae ∈ aexp ::= i | x | rand | ae⊕ ae arithmetic exp. be ∈ bexp ::= b | ae< ae | be < be boolean exp. ce ∈ cexp ::= skip | ce ; ce skip & sequence exp. | x := ae assignment exp. | if be then ce else ce conditional exp. | while be do ce while loop exp. Figure 4.1: Case Study 1: WHILE Abstract Syntax 4.4.1 Concrete Semantics The WHILE language is an imperative programming language with arithmetic expres- sions, variable assignment and while-loops. We show the syntax for this language in Figure 4.1. WHILE syntactically distinguished arithmetic, boolean and command expressions. rand is an arithmetic expression which can evaluate to any integer. Syntactic categories ⊕, < and < range over arithmetic, comparison and boolean operators, and are introduced to simplify the presentation. The WHILE language is taken from Cousot’s monograph [Cousot, 1999]. The concrete semantics of WHILE is sketched without full definition in Figure 4.2. 86 Denotation functions J Ka, J Kc and J Kb give semantics to arithmetic, conditional and boolean operators. The semantics of compound syntactic expressions are given operationally with relations ⇓a, ⇓b and 7→c. Relational semantics are given for arithmetic and boolean expressions due to the nondeterminism of rand and, for command expressions due to the nontermination of while. These semantics serve as the starting point for designing an abstract interpreter. 4.4.2 Abstract Semantics with Constructive GCs Using abstract interpretation with constructive Galois connections, we design an abstract semantics for WHILE in the following steps: 1. An abstraction for each set Z, B and env. 2. An abstraction for each denotation function J Ka, J Kc and J Kb. 3. An abstraction for each semantics relation ⇓a, ⇓b and 7→c. Each abstract set forms a constructive Galois connection with its concrete counter- part. Soundness criteria is synthesized for abstract functions and relations using constructive Galois connection mappings. Finally, we verify and calculate abstract interpreters from these specifications which are sound and computable by construc- tion. We describe the details of this process only for integers and environments (the sets Z and env), arithmetic operators (the denotation function J Ka), and arithmetic expressions (the semantics relation ⇓a). See the Agda development accompanying this chapter for the full mechanization of WHILE, and sections 4.8, 4.9, and 4.10 87 ρ ∈ env := var⇀ ZJ Ka : aop→ Z× Z⇀ ZJ Kc : cmp→ Z× Z→ BJ Kb : bop→ B× B→ B ς ∈ Σ := env× cexp ` ⇓a : ℘(env× aexp× Z) ` ⇓b : ℘(env× bexp× B) 7→c : ℘(Σ× Σ) ρ ` rand ⇓a i ARand ρ ` ae1 ⇓a i1 ρ ` ae2 ⇓a i2 ρ ` ae1 ⊕ ae2 ⇓a J⊕Ka(i1, i2) AOp ρ ` ae ⇓a i 〈ρ, x := ae〉 7→c 〈ρ[x← i], skip〉 CAssign ρ ` be ⇓b true 〈ρ, if be then ce1 else ce2〉 7→c 〈ρ, ce1〉 CIf-T ρ ` be ⇓b false 〈ρ, if be then ce1 else ce2〉 7→c 〈ρ, ce2〉 CIf-F ρ ` be ⇓b true 〈ρ, while be do ce〉 7→c 〈ρ, ce ; while be do ce〉 CWhile-T ρ ` be ⇓b false 〈ρ, while be do ce〉 7→c 〈ρ, skip〉 CWhile-F Figure 4.2: Case Study 1: WHILE Concrete Semantics 88 for a detailed account of binary arithmetic operators and conditional command expressions. Abstracting Integers We design a simple sign abstraction for integers, although more powerful abstractions are certainly possible [Cousot, 1999, Mine´, 2006]. The final abstract interpreter for WHILE is parameterized by any abstraction for integers, meaning another abstraction can be plugged in without added proof effort. The sign abstraction begins with three representative elements: neg, zer and pos, representing negative integers, the integer 0, and positive integers. To support representing integers which could be negative or 0, negative or positive, or 0 or positive, etc. we design a set which is complete w.r.t these logical disjunctions: i] ∈ Z] := {none, neg, zer, pos, negz, nzer, posz, any} Z] is given meaning through an interpretation function µz, the analog of a γ from the classical Galois connection framework: µz : Z] → ℘(Z) µz(none) := {} µz(neg) := {i | i < 0} µz(zer) := {0} µz(pos) := {i | i > 0} µz(negz) := {i | i ≤ 0} µz(nzer) := {i | i 6= 0} µz(posz) := {i | i ≥ 0} µz(any) := {i | i ∈ Z} The partial ordering on abstract integers coincides with subset ordering under µz, that is, i]1 vz i]2 ⇐⇒ µz(i]1) ⊆ µz(i]2): none vz i] vz any neg vz negz, nzer zer vz negz, posz pos vz nzer, posz 89 To be a constructive Galois connection, µz forms a correspondence with a best abstraction function ηz: ηz : Z→ Z] ηz(n) :=  neg if i < 0 zer if i = 0 pos if i > 0 and we prove the constructive Galois connection correspondence: i ∈ µz(i]) ⇐⇒ ηz(i) vz i] The Classical Design To contrast with Cousot’s original design using classical abstract interpretation, the key difference is the abstraction function. The abstraction function using classical Galois connections is recovered through a lifting of our ηz: αz : ℘(Z) → Z] αz(I) := ⊔ i∈I ηz(i) Abstraction functions of this form—℘(B) → A, for some concrete set A and abstract set B—are representative of most Galois connections used in the literature for static analyzers. However, these abstraction functions are precisely the part of classical Galois connections which inhibit mechanized verification. The extraction function ηz does not manipulate powersets, does not inhibit mechanized verification, and recovers the original non-constructive αz through this standard lifting. 90 Abstracting Environments An abstract environment maps variables to ab- stract integers rather than concrete integers. ρ] ∈ env] := var→ Z] env] is given meaning through an interpretation function µr: µr : env] → ℘(env) µr(ρ]) := {ρ | ∀x.ρ(x) ∈ µz(ρ](x))} An abstract environment represents concrete environments that agree pointwise with some represented integer in the codomain. The order on abstract environments is the standard pointwise ordering and coincides with subset ordering under µr, that is, ρ]1 vr ρ]2 ⇐⇒ µr(ρ]1) ⊆ µr(ρ]2): ρ]1 vr ρ2 := ∀x.ρ]1(x) vz ρ]2(x) To form a constructive Galois connection, µr forms a correspondence with a best abstraction function ηr: ηr : env→ env] ηr(ρ) := λx.ηz(ρ(x)) and we prove the constructive Galois connection correspondence: ρ ∈ µr(ρ]) ⇐⇒ ηr(ρ) vr ρ] The Classical Design To contrast with Cousot’s original design using classical abstract interpretation, the key difference is again the abstraction function. 91 The abstraction function using classical Galois connections is: αr : ℘(env) → env] αr(R) := λx.αz({ρ(x) | ρ ∈ R}) which is also not amenable to mechanized verification. Abstracting Functions After designing constructive Galois connections for Z and env we define what it means for J Ka], some abstract denotation for arithmetic operators, to be a sound abstraction of J Ka, its concrete counterpart. This is done through a specification induced by mappings η and µ, analogously to how specifications are induced using classical Galois connections. The specification which encodes soundness and optimality for J Ka] is generated using the constructive Galois connection for Z: 〈i1, i2〉 ∈ µz×z(i]1, i]2) ∧ 〈i]′1 , i]′2 〉 v ηz(JaeKa(i1, i2))⇔ 〈i]′1 , i]′2 〉 v JaeKa](i]1, i]2) (See (CGC-Cmp/ηµ) in Section 4.3 for the origin of this equation.) For J Ka], we postulate its definition and verify its correctness post-facto using the above property, although we omit the proof details here. The definition of J Ka] is standard, and returns none in the case of division by zero. We show only the definition of + here: J Ka] : aexp→ Z] × Z] → Z] J+Ka](i]1, i]2) := ⊔  pos if pos vz i]1 ∨ pos vz i]2 neg if neg vz i]1 ∨ neg vz i]2 zer if zer vz i]1 ∧ zer vz i]2 zer if pos vz i]1 ∧ neg vz i]2 zer if neg vz i]1 ∧ pos vz i]2 92 The Classical Design To contrast with Cousot’s original design using classical abstract interpretation, the key difference is that we avoid powerset liftings all-together. Using classical Galois connections, the concrete denotation function must be lifted to powersets: J Ka℘ : aexp→ ℘(Z× Z)→ ℘(Z) JaeKa℘(II) := {JaeKa(i1, i2) | 〈i1, i2〉 ∈ II} and then J Ka] is proven correct w.r.t. this lifting using αz and γz: αz(JaeKa℘(γz(i]1, i]2))) = JaeKa](i]1, i]2) This property cannot be mechanized without axioms because αz is non-constructive. Furthermore, the proof involves additional powerset boilerplate reasoning, which is not present in our mechanization of correctness for J Ka] using constructive Galois connections. The state-of-the art approach of “γ-only” verification would instead mechanize the γγ variant of correctness: JaeKa℘(γz(i]1, i]2)) = γz(JaeKa](i]1, i]2)) which is similar to our µµ rule: 〈i1, i2〉 ∈ µz×z(i]1, i]2) ∧ 〈i′1, i′2〉 = JaeKa(i1, i2)⇔ 〈i′1, i′2〉 ∈ µz×z(JaeKa](i]1, i]2)) The benefit of our approach is that soundness and completeness properties which also mention extraction (η) can also be mechanized, like calculating abstract interpreters from their specification. 93 Abstracting Relations The verification of an abstract interpreter for relations is similar to the design for functions: induce a specification using the constructive Galois connection, and prove correctness w.r.t. the induced spec. The relations we abstract are ⇓a, ⇓b and 7→c, and we call their abstract interpreters A], B] and C]. Rather than postulate the definitions of the abstract interpreters, we calculate them from their specifications, the results of which are sound and computable by construction. The arithmetic and boolean abstract interpreters are functions from abstract environments to abstract integers, and the abstract interpreter for commands computes the next abstract transition states of execution. (We only present select calculations for A]; see our accompanying Agda development for each calculation in mechanized form, and sections 4.8, 4.9 and 4.10 for detailed calculations of binary arithmetic operators and conditional command expressions.) A] has type: A][ ] : aexp→ env] → Z] To induce a spec for A], we first revisit the concrete semantics relation as a powerset- valued function, which we call A: A[ ] : aexp→ env→ ℘(Z) A[ae](ρ) := {i | ρ ` ae ⇓a i} The induced spec for A] is generated with the monadic bind operator, which we notate using Moggi’s star notation ∗: pure(ηz)∗(A[ae]∗(µr(ρ]))) ⊆ pure(A][ae])(ρ]) 94 which unfolds to: {ηz(i) | ρ ∈ µr(ρ]) ∧ ρ ` ae ⇓a i} ⊆ {A][ae](ρ])} To calculate A] we reason equationally from the spec on the left towards the singleton set on the right, and declare the result the definition of A]. We do this by case analysis on ae; we show the cases for ae = rand and ae = x in Figure 4.3. Each calculation can also be written in monadic form, which is the style we mechanize; we repeat the variable case in monadic form in the figure. Mechanized Calculation Our Agda calculation of A] strongly resembles the on-paper monadic one. We show the Agda proof code for abstract variable references in Figure 4.4. The first line is the top level definition site for the derivation of A] for the Var case. The proof-mode term is part of our “proof-mode” library which gives support for calculational reasoning in the form of Agda proof combinators with mixfix syntax. Statements surrounded by double square brackets [[e]] restate the current proof state, which Agda will check is correct. Reasoning steps are employed through * e + terms, which transform the proof state from the previous form to the next. The term [focus-right [· ] of e] focuses the goal to the right of the outermost application, scoped between begin and end. Using constructive Galois connections, our mechanized calculation closely follows Cousot’s classical one, uses both η and µ mappings, and results in a verified, executable static analyzer. Such a result is not possible using classical Galois connections, due to the inability to encode α functions constructively. 95 Case ae = rand: {ηz(i) | ρ ∈ µr(ρ]) ∧ ρ ` rand ⇓a i} = {ηz(i) | ρ ∈ µr(ρ]) ∧ i ∈ Z} * defn. of ρ ` rand ⇓a i + ⊆ {ηz(i) | i ∈ Z} * ∅ when µr(ρ]) = ∅ + ⊆ {any} * {any} mon. w.r.t. vz + , {A][rand](ρ])} * defining A][rand] + Case ae = x: {ηz(i) | ρ ∈ µr(ρ]) ∧ ρ ` x ⇓a i} = {ηz(ρ(x)) | ρ ∈ µr(ρ])} * defn. of ρ ` x ⇓a i + = {ηz(i) | i ∈ µz(ρ](x))} * defn. of µr(ρ]) + ⊆ {ρ](x)} * Eq. CGC-Red + , {A][x](ρ])} * defining A][x] + Case ae = x (Monadic): pure(ηz)∗(A[x]∗(µr(ρ]))) = pure(λρ.ηz(ρ(x)))∗(µr(ρ])) * defn. of A[x] + = pure(ηz)∗(µz∗(ρ](x))) * defn. of µr(ρ]) + ⊆ ret(ρ](x)) * Eq. CGC-Red + , pure(A][x])(ρ]) * defining A][x] + Figure 4.3: Case Study 1: Select Constructive Galois Connection Calculations 96 -- Agda Calculation of Case ae = x: α[A] (Var x) ρ] = [proof-mode] do [[ (pure · ηz) ∗ · (A[ Var x ] ∗ · (µr · ρ])) ]]  [focus-right [· ] of (pure · ηz) ∗ ] begin do [[ A[ Var x ] ∗ · (µr · ρ]) ]]  * A[Var]/≡ +  [[ (pure · lookup[ x ]) ∗ · (µr · ρ]) ]]  * lookup/µr/≡ +  [[ µz ∗ · (pure · lookup][ x ] · ρ]) ]] end  [[ (pure · ηz) ∗ · (µz ∗ · (pure · lookup][ x ] · ρ])) ]]  * reductive[ηµ] +  [[ ret · (lookup][ x ] · ρ]) ]]  [[ pure · A][ Num n ] · ρ] ]]  Figure 4.4: Case Study 1: Constructive Galois Connection Calculations in Agda 97 We complete the full calculation of Cousot’s generic abstract interpreter for WHILE in Agda as supplemental material to this chapter, where the resulting in- terpreter is both sound and computable by construction. We also provide our “proof-mode” library which supports general calculational reasoning with posets. The Classical Design Classically, one first designs a powerset lifting of the concrete semantics, called a collecting semantics : A℘[ ] : aexp→ ℘(env) → ℘(Z) A℘[ae](R) := {i | ρ ∈ R ∧ ρ ` ae ⇓a} The classical soundness specification for A][ae](ρ]) is then: αz(A℘[ae](γr(ρ]))) v A][ae](ρ]) However, as usual, the abstraction αz cannot be mechanized effectively, preventing a mechanized derivation of A] by calculus. 4.5 Case Study 2: Gradual Type Systems Recent work in metatheory for gradual type systems [Garcia et al., 2016] shows how a Galois connection discipline can guide the design of gradual typing systems. Starting with a Galois connection between precise and gradual types, both the static and dynamic semantics of the gradual language are derived systematically. This technique is called Abstracting Gradual Typing (AGT). The design presented by Garcia et al is to begin with a precise type system, like the simply typed lambda calculus, and add a new type (?) which functions as 98 the top element (>) in the lattice of type precision. The precise typing rules are presented with meta-operators for subtyping (<:) and for the join operator in the subtyping lattice ( ..∨). The gradual type system is then written using abstract variants of subtyping and join (<:] and ..∨]) which are proven correct w.r.t. specifications induced by the Galois connection. The Precise Type System The AGT paper describes two designs for gradual type systems in increasing complexity. We chose to mechanize a hybrid of the two which is simple, like the first design, yet still exercises key challenges addressed by the second. We also made slight modifications to the design at parts to make mechanization easier, but without changing the nature of the system. The precise type system we mechanized is the simply typed lambda calculus with booleans, and top and bottom elements for a subtyping lattice, which we call any and none: τ ∈ type ::= none | B | τ → τ | any The first design in the AGT paper does not involve subtyping, and their second design incorporates record types with width and depth subtyping. By just focusing on none and any, we exercise the subtyping machinery of their approach without the blowup in complexity from formalizing record types. The typing rules in AGT are written in strictly syntax-directed form, with explicit use of subtyping in rule hypotheses. We show three precise typing rules for if-statements, application and coercion in Figure 4.5. The subtyping lattice in the 99 Γ ` e1 : τ1 τ1 <: B Γ ` e2 : τ2 Γ ` e3 : τ3 Γ ` if e1 then e2 else e3 : τ1 ..∨ τ2 If Γ ` e1 : τ1 τ1 <: τ11 → τ21 Γ ` e2 : τ2 τ2 <: τ11 Γ ` e1(e2) : τ21 App Γ ` e : τ1 τ1 <: τ2 Γ ` e :: τ2 : τ2 Coe Figure 4.5: Case Study 2: Syntax Directed Precise Type System precise system is the “safe for substitution” lattice, and well typed programs enjoy progress and preservation. Gradual Types The essence of AGT is to design a gradual type system by abstract interpretation of the precise type system. To do this, a new top element is added to the precise type system, although rather than representing the top of the subtyping lattice like any, it represents the top of the precision lattice, and is notated ?: τ ] ∈ type] ::= none | B | τ ] → τ ] | any | ? The partial ordering has ? at the top (τ ] v ?) and is otherwise discrete, and arrow types are monotonic (covariant) in both the domain and codomain: τ ]11 v τ ]12 ∧ τ ]21 v τ ]22 =⇒ τ ]11 → τ ]21 v τ ]12 → τ ]22 100 Just as in our other designs by abstract interpretation, type] is given meaning by an interpretation function µ, which is the constructive analog of a classical concretization (γ) function: µ : type] → ℘(type) µ(τ ]) := τ when τ ] = τ ∈ {none,B, any} µ(τ ]1 → τ ]2) := {τ1 → τ2 | τ1 ∈ µ(τ ]1) ∧ τ2 ∈ µ(τ ]2)} µ(?) := {τ | τ ∈ type} The extraction function η is, remarkably, the identity function: η : type→ type] η(τ) = τ and the constructive Galois correspondence holds: τ ∈ µ(τ ]) ⇐⇒ η(τ) v τ ] Gradual Operators Given the constructive Galois connection between gradual and precise types, we synthesize specifications for abstract analogs of subtyping (<:) and the subtyping join operator ( ..∨), and relate them to their abstractions (<:] and ..∨]): τ1 ∈ µ(τ ]1) ∧ τ2 ∈ µ(τ ]2) ∧ τ1 <: τ2 ⇐⇒ τ ]1 <:] τ ]2 τ1 ∈ µ(τ ]1) ∧ τ2 ∈ µ(τ ]2) ∧ τ ]3 v η(τ1 ..∨ τ2) ⇐⇒ τ ]3 v τ ]1 ..∨] τ ]2 Key properties of gradual subtyping and the gradual join operator is how they operate over the unknown type ?: ? <:] τ ] τ ] <:] ? ? ..∨] τ ] = τ ] ..∨] ? = ? 101 Γ] `] e]1 : τ ]1 τ ]1 <:] B Γ] `] e]2 : τ ]2 Γ] `] e]3 : τ ]3 Γ] `] if e1 then e2 else e3 : τ ]2 ..∨] τ ]3 G-If Γ] `] e]1 : τ ]1 τ ]1 <:] τ ]11 → τ ]21 Γ] `] e]2 : τ ]2 τ ]2 <:] τ ]11 Γ] ` e]1(e]2) : τ ]21 G-App Γ] `] e] : τ ]1 τ ]1 <:] τ ]2 Γ] `] e] :: τ ]2 : τ ]2 G-Coe Figure 4.6: Case Study 2: Systematically Constructed Gradual Type System Gradual Metatheory Using AGT, the gradual type system is a syntactic analog to the precise one but with gradual types and operators, which we show in Figure 4.6. Using this system, and constructive Galois connections, we mechanize in Agda the key AGT metatheory results from the paper: equivalence for fully-annotated terms (FAT), embedding of dynamic language terms (EDL), and the gradual guarantee (GG): ` e : τ ⇐⇒ `] e : τ (FAT) closed(un) =⇒ `] dune : ? (EDL) `] e]1 : τ ]1 ∧ e]1 v e]2 =⇒ `] e]2 : τ ]2 ∧ τ ]1 v τ ]2 (GG) 102 Adjunction classical GCs Kleisli GCs Category posets posets Adjoints monotonic functions monotonic ℘-monadic functions Left Adjoint α : A → B κα : A → ℘(B) Right Adjoint γ : B → A κγ : B → ℘(A) Correspondence id(x) v γ(y) ⇐⇒ α(x) v id(y) ret(x) ⊆ κγ(y) ⇐⇒ κα(x) ⊆ ret(y) Extensive id v γ ◦ α ret v κγ ~ κα Reductive α ◦ γ v id κα~ κγ v ret Soundness α ◦ f ◦ γ v f ] κα~ f ~ κγ v f ] Optimality α ◦ f ◦ γ = f ] κα~ f ~ κγ = f ] Figure 4.7: Comparison of Constructive and Classical Galois Connection Adjunctions 4.6 Constructive Galois Connection Metatheory In this section we develop the full metatheory of constructive Galois connection and prove precise claims about their relationship to classical Galois connections. We develop the metatheory of constructive Galois connections as an adjunction between posets with powerset-Kleisli adjoint functors. This is in contrast to classical Galois connections which come from an identical setup, but with the monotonic function space as adjoint functors, as shown in Figure 4.7. We connect constructive to classical Galois connections through an isomorphism between a subset of classical to the entire space of constructive. To form this isomorphism we introduce an intermediate structure, Kleisli Galois connections, 103 which we show are isomorphic to the classical subset, and isomorphic to constructive ones. This second isomorphism uses the constructive theorem of choice, as depicted in Figure 4.8. Classical Galois Connections We review classical Galois connections in Figure 4.7. A Galois connection between posets A and B contains two adjoint functors α and γ which share a correspondence. An equivalent formulation of the correspondence is two unit equations called extensive and reductive. Abstract interpreters are sound by over-approximating a specification induced by α and γ. Powerset Monad See Sections 4.3.1 and 4.3.3 for the downward-closure mono- tonicity property, and monad definitions and notation for the monotonic powerset monad. The monad operators obey standard monad laws. We introduce one new operator for monadic function composition: (g ~ f)(x) := g∗(f(x)). Kleisli Galois Connections We summarize Kleisli Galois connections in Fig- ure 4.7. Kleisli Galois connections are analogous to classical ones, but with monadic analogs to α and γ, and monadic identity and composition operators ret and ~ in place of the function space identity and composition operators id and ◦. Kleisli to Classical and Back All Kleisli Galois connections 〈κα, κγ〉 between A and B can be lifted to recover a classical Galois connection 〈α, γ〉 between ℘(A) and ℘(B) through a monadic lifting operator on Kleisli Galois connections 〈κα, κγ〉∗: 〈α, γ〉 , 〈κα, κγ〉∗ := 〈κα∗, κγ∗〉 104 This lifting is sound, meaning Kleisli soundness and optimality results can be translated to classical ones. Theorem 1 (KGC-SoundAGDAX). For any Kleisli relationship of soundness between f and f ], that is κα ~ f ~ κγ v f ], its lifting to classical is also sound, that is α ◦ f ∗ ◦ γ v f ]∗ where 〈α, γ〉 = 〈κα, κγ〉∗, and likewise for optimality relationships α~ f ~ κy = f ]. This lifting is also complete, meaning classical Galois connection soundness and optimality results can always be translated to Kleisli ones, when α and γ are of lifted form. Theorem 2 (KGC-CompleteAGDAX). For any classical relationship of soundness between f ∗ and f ]∗, that is α ◦ f ∗ ◦ γ v f ]∗, its lowering to Kleisli is also sound when 〈α, γ〉 = 〈κα, κγ〉∗, that is κα~f ~κγ v f ], and likewise for optimality relationships α ◦ f ∗ ◦ γ = f ]∗. Due to soundness and completeness, one can work with the simpler setup of Kleisli Galois connections without any loss of generality. The setup is simpler because Kleisli Galois connection theorems only quantify over individual elements rather than elements of powersets. For example, the soundness criteria κα~ f ~ κγ v f ] is proved by showing κα∗(f ∗(κγ(x))) ⊆ f ](x) for an arbitrary element x : A, whereas in the classical proof one must show κα∗(f ∗(κγ∗(X))) ⊆ f ]∗(X) for arbitrary sets X : ℘(A). 105 Constructive Galois Connections Constructive Galois connections are a restriction of Kleisli Galois connections where the abstraction mapping is a pure rather than monadic function. We call the left adjoint extraction, notated η, and the right adjoint interpretation, notated µ. The constructive Galois connection correspondence, alternative expansive and reductive formulation of the correspondence, and soundness and optimality criteria are identical to Kleisli Galois connections where 〈κα, κγ〉 = 〈pure(η), µ〉. Constructive to Kleisli and Back Our main theorem which justifies the soundness and completeness of constructive Galois connections is an isomorphism between constructive and Kleisli Galois connections. The easy direction is soundness, where a Kleisli Galois connection is formed by defining 〈κα, κγ〉 := 〈pure(η), µ〉. Soundness and optimality theorems are then lifted from constructive to Kleisli without modification. Theorem 3 (CGC-SoundAGDAX). For any constructive relationship of soundness between f and f ], that is pure(η)~ f ~ µ v f ], its lifting to Kleisli is sound, that is κα ~ f ~ κγ v f ] where 〈κα, κγ〉 = 〈pure(η), µ〉, and likewise for optimality relationships pure(η)~ f ~ µ = f ]. The other direction, completeness, is much more surprising. First we establish a lowering for Kleisli Galois connections. Lemma 1 (CGC-InduceAGDAX). For every Kleisli Galois connection 〈κα, κγ〉, there exists a constructive Galois connection 〈η, µ〉 where 〈pure(η), µ〉 = 〈κα, κγ〉. 106 Classical Computational Kleisli Constructive Set inclusion Theorem of choice Figure 4.8: Relationship Between Classical, Kleisli and Constructive GCs Proof. Because the mapping from Kleisli to constructive is interesting we provide a proof, which to our knowledge is novel. The proof builds a constructive Galois connection 〈η, µ〉 from a Kleisli 〈κα, κγ〉 by exploiting the Kleisli correspondence and making use of the constructive theorem of choice. To turn an arbitrary Kleisli Galois connection into a constructive one, we show that the effect on κα : A → ℘(B) is benign, or in other words, that there exists some η such that κα = pure(η). We prove this using two ingredients: a constructive interpretation of the Kleisli extensive law, and the constructive theorem of choice. We first expand the Kleisli expansive property, unfolding definitions of ~ and ret, to get an equivalent logical statement: ∀x.∃y.y ∈ κα(x) ∧ x ∈ κγ(y) (KGC-Exp) Statements of this form can be used in conjunction with an axiom of choice in classical mathematics, which is: (∀x.∃y.R(x, y)) =⇒ ∃f.∀x.R(x, f(x)) (AxChoice) This theorem is admitted as an axiom in classical mathematics, but in constructive 107 logic—the setting used for extracting verified algorithms–(AxChoice) is definable as a theorem, due to the computational interpretation of logical connectives ∀ and ∃. We define (AxChoice) as a theorem in Agda without trouble: choice : ∀ {A B} {R : A → B → Set} → (∀ x → ∃ y st R x y) → (∃ f st ∀ x → R x (f x)) choice P = 〈∃ (λ x → pi1 (P x)) , (λ x → pi2 (P x)) 〉 Applying (AxChoice) to (KGC-Exp) then gives: ∃η.∀x.η(x) ∈ κα(x) ∧ x ∈ κγ(η(x)) (ExpChioce) which proves the existence of a pure function η : A → B. In order to form a constructive Galois connection η and µ must satisfy the correspondence, which we prove in split form: x ∈ µ(η(x)) (CGC-Exp) x ∈ µ(y) =⇒ η(x) v y (CGC-Red) The expansive property is immediate from the second conjunct in (ExpChioce). The reductive property follows from the Kleisli reductive property: x ∈ κγ(y) ∧ y′ ∈ κα(x) =⇒ y′ v y (KGC-Red) The constructive variant of reductive is proved by satisfying the first two premises of (KGC-Red), where x ∈ κγ(y) is by assumption and y′ ∈ κα(x) is by the first conjunct in (ExpChioce). So far we have shown that for a Kleisli Galois connection 〈κα, κγ〉, there exists 108 a constructive Galois connection 〈η, µ〉 where µ = κγ. However, we have yet to show pure(η) = κα. To show this, we prove an analog of a standard result for classical Galois connections: that α and γ uniquely determine each other. Lemma 2 (Unique AbstractionAGDAX). For any two Kleisli Galois connections 〈κα1, κγ1〉 and 〈κα2, κγ2〉, κα1 = κα2 iff κγ1 = κγ2 We then conclude pure(η) = κα as a consequence of the above lemma and the fact that µ = κγ. Given the above mapping from Kleisli Galois connections to constructive ones, we prove the completeness of this mapping. Theorem 4 (CGC-CompleteAGDAX). For any Kleisli relationship of soundness between f and f ], that is κα~ f ~ κγ v f ], its lowering to constructive is also sound, that is pure(η)~f~µ v f ] where 〈η, µ〉 is induced, and likewise for optimality relationships κα~ f ~ κγ = f ]. Mechanization We mechanize the metatheory for constructive Galois connections and both case studies from Sections 4.4 and 4.5 in Agda, as well as a general purpose proof library for posets and calculational reasoning with the monotonic powerset monad. The development is available at: github.com/plum-umd/cgc. Wrapping Up In this section we showed that constructive Galois connections are sound w.r.t. classical Galois connections, and complete w.r.t. the subset of 109 classical Galois connections recovered by lifting constructive ones. We showed this by introducing an intermediate space of Galois connections called Kleisli Galois connections, and by establishing two sets of isomorphisms between a subset of classical and Kleisli, and between Kleisli and constructive. The proof of isomorphism between constructive and Kleisli yielded an interesting proof which applies the constructive theorem of choice to one of the Kleisli Galois connection correspondence laws. 4.7 Constructing Constructive Galois Connections The classical Galois connection framework comes with a library of connectives which are used to build larger Galois connections out of smaller, primitive ones [Cousot and Cousot, 1994]. For example, it is common to create a Galois connection for Cartesian products (A×B) as the product abstraction of two Galois connections, one for each side (A and B). In this section, we define the constructive analog of many classical Galois connection connectives and primitives. In later sections we will highlight similarities and differences between constructive and classical calculations (§ 4.8), how derivations of optimal abstract interpreters varies between the two settings (§ 4.9), and how multivalued computations are supported in the constructive setting (§ 4.10). Each section will make use of the connectives and primitives defined in this section without explicit introduction. Some readers may choose to skip this section, and refer back to the definitions as each connective appears in later sections. 110 By convention, we notate classical Galois connections A −−→←−−α γ B, that is with α and γ symbols below and above the arrows, and constructive Galois connections A −−→←−−η µ B, that is with η and µ symbols below and above the arrows. Note that in the case of classical Galois connections, the domain and codomain of abstraction (α) and concretization (γ) are immediate from the notation, that is, α : A → B and γ : B → A. However for constructive Galois connections, the domain and codomain is only immediate from the notation for abstraction (η), but not concretization (µ) which maps to a powerset in the codomain, that is η : A → B but µ : B → ℘(A). We notate pure(x) compactly as bxc, and assume all powersets are downward closed. 4.7.1 Strictly Classical Galois Connections Independent Attributes Abstraction The independent attributes abstrac- tion is defined for relations (℘(A×B)), and constructs the classical Galois connection: ℘(A×B) −−−→←−−− IA α IA γ ℘(A)× ℘(B) IA α : ℘(A×B) → ℘(A)× ℘(B) IA γ : ℘(A)× ℘(B) → ℘(A×B) IA α (XY ) := 〈{x | ∃y.〈x, y〉 ∈ XY }, {y | ∃x.〈x, y〉 ∈ XY }〉 IA γ (X, Y ) := 〈{〈x, y〉 | x ∈ X ∧ y ∈ Y }〉 4.7.2 Strictly Constructive Galois Connections Singleton Abstraction The singleton abstraction is defined for powersets of partially ordered sets (℘(A)), and constructs the constructive Galois connection: A −−→←−− 1 η 1 µ ℘(A) 1 η : A → ℘(A) 1 µ : ℘(A) → ℘(A) 1 η(x) := {x} 1 µ(X) := X 111 4.7.3 Primitive Galois Connections—Classical and Constructive Least-upper-bound Abstraction The least-upper-bound abstraction is defined for powersets of partially ordered sets (℘(A)), and constructs the classical Galois connection: ℘(A) −−→←−−unionsq α unionsq γ A unionsq α : ℘(A) → A unionsq γ : A → ℘(A) unionsq α(X) := ⊔ x∈X x unionsq γ(x) := {x} The constructive analog is defined for powersets of partially ordered sets (℘(A)), and constructs the classical Galois connection: ℘(A) −−−→←−−−unionsq℘ α unionsq℘ γ ℘1(A) unionsq℘ α : ℘(A) → ℘1(A) unionsq℘ γ : ℘1(A) → ℘(A) unionsq℘ α (X) := {x | x v ⊔ x∈X x} unionsq℘ γ (X) := {x | x ∈ X} We notate singleton (downward closed) powersets ℘1( ), which classically are iso- morphic to the carrier set (℘1(A) −−→←−− A), but not constructively. Elementwise Abstraction The elementwise abstraction is defined given a function f : A→ B, and constructs the classical Galois connection: ℘(A) −−−→←−−− [f ] α [f ] γ ℘(B) [f ] α : ℘(A) → ℘(B) [f ] γ : ℘(B) → ℘(A) [f ] α(X) := {f(x) | x ∈ X} [f ] γ (Y ) := {x | f(x) ∈ Y } The constructive analog is defined given a monotonic function f : A → B and constructs a constructive Galois connection A −−→←−−η µ B where: [f ] η : A → B [f ] µ : B → ℘(A) [f ] η (x) := f(x) [f ] µ(y) := {x | f(x) v y} Fact 1 (Elementwise Abstraction Correspondence). The classical elementwise ab- straction is equal to the classical lifting of the constructive elementwise abstraction, 112 that is: α = bηc∗ and γ = µ∗. 4.7.4 Composing Galois Connections—Classical and Constructive Abstraction Composition The composition of two abstractions is defined given abstractions B −−−→←−−−α1 γ1 C and A −−−→←−−−α2 γ2 B, and constructs the classical Galois connection: A −−−→←−−− 1◦2 α 1◦2 γ C 1◦2 α : A → C 1◦2 γ : C → A 1◦2 α (x) := α1(α2(x)) 1◦2 γ (z) := γ2(γ1(z)) The constructive analog is defined given abstractions B −−−→←−−−η1 µ1 C and A −−−→←−−−η2 µ2 B, and constructs the constructive Galois connection: A −−−→←−−− 1◦2 η 1◦2 µ C 1◦2 η : A → C 1◦2 µ : C → ℘(A) 1◦2 η (x) := η1(η2(x)) 1◦2 µ (z) := µ∗2(µ1(z)) Product Abstraction The product abstraction is defined given abstractions ℘(A) −−−→←−−− αA γA A] and ℘(B) −−−→←−−− αB γB B], and constructs the classical Galois connection: ℘(A)× ℘(B) −−−−→←−−−− A×B α A×B γ A] ×B] A×B α : ℘(A)× ℘(B) → A] ×B] A×B γ : A] ×B] → ℘(A)× ℘(B) A×B α (X, Y ) := 〈αA(X), αB(Y )〉 A×B γ (x], y]) := 〈γA(x]), γB(y])〉 113 The constructive analog is defined given abstractions A −−−→←−−− ηA µA A] and B −−−→←−−− ηB µB B], and constructs the constructive Galois connection: A×B −−−−→←−−−− A×B η A×B µ A] ×B] A×B η : A×B → A] ×B] A×B µ : A] ×B] → ℘(A×B) A×B η (x, y) := 〈ηA(x), ηB(y)〉 A×B µ (x], y]) := {〈x, y〉 | x ∈ µA(x]) ∧ y ∈ µB(y)} Functional Abstraction The functional abstraction is defined given abstrac- tions ℘(A) −−−→←−−− αA γA A] and ℘(B) −−−→←−−− αB γB B], and constructs the classical Galois connection: ℘(A) → ℘(B) −−−−→←−−−− A7→B α A7→B γ A] → B] A 7→B α : (℘(A) → ℘(B)) → A] → B] A7→B γ : (A] → B]) → ℘(A) → ℘(A) A 7→B α (f)(x]) := αB(f(γA(x]))) A 7→B γ (f ])(X) := γB(f ](αA(X))) The constructive analog is defined given constructive abstractions A −−−→←−−− ηA µA A] and B −−−→←−−− ηB µB B], and constructs the classical Galois connection: A → ℘(B) −−−−→←−−−− A ℘7→B α A ℘7→B γ A] → ℘(B]) A ℘7→B α : (A → ℘(B)) → A] → ℘(B]) A ℘7→B γ : (A] → ℘(B])) → A → ℘(B) A ℘7→B α (f)(x]) := bηBc∗(f ∗(γA(x]))) A ℘7→B γ (f ])(x) := µB∗(f ](ηA(x))) Fact 2 (Functional Abstraction Correspondence). The classical functional abstraction is equal to the classical lifting of the constructive elementwise abstraction composed with the least-upper-bound abstraction, that is, for (f : A → ℘(B)), (f ] : A] → B]), 114 (X : ℘(A)) and (x] : A]): A 7→B α (f ∗)(x]) = ⊔ y]∈A ℘7→B α (f)(x]) y] and A 7→B γ (f ])(X) = A ℘7→B γ (bf ]c)∗(X) 4.8 Comparing Classical and Constructive Approaches In this section we aim to further clarify to what extent classical Galois connection calculations, which have been used successfully for decades, are related and/or inter- derivable with constructive Galois connection calculations. We will demonstrate this relationship between classical and constructive calculations through an extended example drawn from our first case study. In Section 4.4 we showed calculations for the random number expression (rand) and variable reference (x). The inductive case for binary operators (ae ⊕ ae) was omitted for brevity, however its calculation is particularly interesting because it involves interacting with a classical Galois connection during the calculation (in both constructive and classical settings). In this section we will work through this calculation in detail to demonstrate the differences and similarities between classical and constructive approaches, as well as to demonstrate the effectiveness of constructive Galois connections used in conjunction with classical ones. Setup To set the stage, we review in Figure 4.9 the types for the arithmetic operator denotation (J Ka), its abstraction (J Ka]), the arithmetic expression relational semantics ( ` ⇓a ), its functional variant (A[ ]) and collecting semantics (A℘[ ]), its abstraction (A][ ]), as well as classical and constructive Galois connections 115 J Ka : Z× Z⇀ ZJ Ka] : Z] × Z] → Z] ` ⇓a : ℘(env× aexp× Z) A[ ] : aexp→ env→ ℘(Z) A℘[ ] : aexp→ ℘(env) → ℘(Z) A][ ] : aexp→ env] → Z] ηz : Z→ Z] αz : ℘(Z) → Z] ηr : env→ env] αr : ℘(env) → env] µz : Z] → ℘(Z) γz : Z] → ℘(Z) µr : env] → ℘(env) γr : env] → ℘(env) Figure 4.9: Review: Calculational Derivation for Binary Arithmetic Expressions for integers (Z −−−→←−−− ηz µz Z] and Z −−−→←−−− αz γz Z]) and environments (env −−−→←−−− ηr µr env] and env −−−→←−−− αr γr env]). First we will show the original classical calculation for binary arithmetic operator expressions which does not make explicit use of the independent attributes abstraction (§ 4.8.1). We will then make independent attributes explicit in the classical calculation (§ 4.8.2), and then show the constructive analog with explicit use of independent attributes (§ 4.8.3). 4.8.1 Review: Cousot’s Original Classical Calculation In the classical Galois connection framework, the abstraction (A][ ]) for the arith- metic relational semantics ( ` ⇓a ) is calculated by first defining the collecting 116 semantics (A℘[ ] : aexp → ℘(env) → ℘(Z)), and then relating the collecting semantics to the abstract semantics through a functional abstraction, that is: r 7→z α (A℘[ae])(ρ]) , αz(A℘[ae](γr(ρ]))) v . . . , A][ae](ρ]) Cousot’s original calculation proceeds by case analysis on the syntax for arithmetic expressions, so for arithmetic operator expressions, the calculation is: αz(A℘[ae1 ⊕ ae2](γr(ρ]))) v . . . , A][ae1 ⊕ ae2](ρ]) The calculation is shown in Figure 4.10. Steps 1–3 unfold semantic function and relation definitions; at Step 4 the specification is weakened explicitly to break the equality relationship between the environment used to evaluate ae1 and ae2; Step 5 rewrites the goal in terms of collecting semantics operations; Step 6 applies the inductive hypothesis; Step 7 applies a correct abstract interpreter for binary operators (a parameter to the calculation); Step 8 collapses neighboring abstraction and concretization functions; and Step 9 declares the final state of the calculation to be the definition of the algorithm. Although there was no mention of the independent attributes abstraction in this calculation, its effects are there implicitly. In particular, Step 4, which breaks the equality relationship between environments, is implicitly performing the function of the independent attributes abstraction: to break relationships between elements of concrete sets of pairs. Step 4 is also the only step in the derivation which loses precision (uses v instead of =) unnecessarily, whereas the other losses of precision are unavoidable (inductive hypothesis, abstraction for binary operators, and collapsing 117 αz(A℘[ae1 ⊕ ae2](γr(ρ]))) (1) = * defn. of A℘[ae1 ⊕ ae2] + αz( ⋃ ρ∈γr(ρ]) A[ae1 ⊕ ae2](ρ)) (2) = * defn. of A[ae1 ⊕ ae2] + αz( ⋃ ρ∈γr(ρ]) {J⊕Ka(i1, i2) | ρ ` ae1 ⇓a i1 ∧ ρ ` ae2 ⇓a i2}) (3) = * defn. of A[ae1] and A[ae2] + αz( ⋃ ρ∈γr(ρ]) {J⊕Ka(i1, i2) | i1 ∈ A[ae1](ρ) ∧ i2 ∈ A[ae2](ρ)}) (4) v * monotonicity of αz + αz( ⋃ ρ1∈γr(ρ]) ⋃ ρ2∈γr(ρ]) {J⊕Ka(i1, i2) | i1 ∈ A[ae1](ρ1) ∧ i2 ∈ A[ae2](ρ2)}) (5) = * set equality + αz({J⊕Ka(i1, i2) | i1 ∈ A℘[ae1](γr(ρ])) ∧ i2 ∈ A℘[ae2](γr(ρ]))}) (6) v * inductive hypothesis (A℘[ae] ◦ γr v γz ◦ A][ae]) + αz({J⊕Ka(i1, i2) | i1 ∈ γz(A][ae1](ρ])) ∧ i2 ∈ γz(A][ae2](ρ]))}) (7) v * J⊕Ka] correct (J⊕Ka℘ ◦ z×zγ v γz ◦ J⊕Ka]) + αz(γz(J⊕Ka](A][ae1](ρ]),A][ae2](ρ])))) (8) v * αz ◦ γz reductive (αz ◦ γz v id) +J⊕Ka](A][ae1](ρ]),A][ae2](ρ])) (9) , * by A][ae1 ⊕ ae2](ρ]) := J⊕Ka](A][ae1](ρ]),A][ae2](ρ])) + A][ae1 ⊕ ae2](ρ])  Figure 4.10: Classical Calculation for Binary Arithmetic Expressions 118 abstraction and concretization function). In the next subsection, we will make explicit use of the independent attributes abstraction, rather than through the ad-hoc line of reasoning contained in Step 4. 4.8.2 Using Independent Attributes Explicitly In this section we recreate the calculation for binary arithmetic operator expressions from last section, but in a way that makes explicit use of the independent attributes abstraction. The calculation is shown in Figure 4.11. The beginning of the derivation is as before (steps 1–3); Step 4.1 rewrites the calculation into a form that mentions inde- pendent attributes concretization; Step 4.2 pulls the collecting semantics for binary operators out of the union operation; Step 5.1 introduces the explicit independent attributes abstraction; Step 5.2 collapses the union operation between independent attributes abstraction and concretization based on a key observation (see below); Step 5.3 unfolds the definition of independent attributes concretization; and the rest of the derivation is as before (steps 6–9). The key observation in this derivation is the fact that the independent attributes abstraction is transparent w.r.t. element-wise relationships, that is pairing ( IA γ ) and splitting ( IA α ) two functions over related elements (f(x1) and g(x2) for x1 = x2 ∈ X), is equivalent to pairing each functions applied to unrelated elements (f ∗(X) and g∗(X)): 119 . . . initial calculation as before (steps 1–3) αz( ⋃ ρ∈γr(ρ]) {J⊕Ka(i1, i2) | i1 ∈ A[ae1](ρ) ∧ i2 ∈ A[ae2](ρ)}) (4.1) = * defn. of IAγ and J⊕Ka℘ + αz( ⋃ ρ∈γr(ρ]) J⊕Ka℘(IAγ (A[ae1](ρ),A[ae2](ρ)))) (4.2) = * set equality + αz(J⊕Ka℘( ⋃ ρ∈γr(ρ]) IA γ (A[ae1](ρ),A[ae2](ρ)))) (5.1) v * IAγ ◦ IAα extensive (id v IAγ ◦ IAα ) + αz(J⊕Ka℘(IAγ (IAα ( ⋃ ρ∈γr(ρ]) IA γ (A[ae1](ρ),A[ae2](ρ)))))) (5.2) = * set equality (see (IA-Split) below) + αz(J⊕Ka℘(IAγ (A℘[ae1](γr(ρ])),A℘[ae2](γr(ρ]))))) (5.3) v * defn. of IAγ and J⊕Ka℘ + αz({J⊕Ka(i1, i2) | i1 ∈ A℘[ae1](γr(ρ])) ∧ i2 ∈ A℘[ae2](γr(ρ]))}) . . . final calculation as before (steps 6–9) Figure 4.11: Classical Calculation for Binary Arithmetic Expressions Using Indepen- dent Attributes 120 Fact 3 (Independent Attributes Split Equality). IA α ( ⋃ x∈X IA γ (f(x), g(x))) = 〈f ∗(X), g∗(X)〉 (IA-Split) This observation captures locally the fact that if relational information is eventually going to be explicitly removed, then nothing is lost by splitting the equality relationship between arguments to each function. One of the benefits of the calculational approach to abstract interpretation is that any loss of precision w.r.t. the induced specification is made explicit. In this derivation, the only non-essential loss in precision came from an explicit introduction of the independent attributes abstraction, which in turn makes explicit the fact that the resulting analysis is non-relational. If a relational analyzer was desired, one could point exactly where in the calculation this information was lost via the independent attributes abstraction, and correct it locally. 4.8.3 Calculating with Constructive Galois Connections In the constructive framework, the abstract interpretation of binary arithmetic operator expressions (A][ae1 ⊕ ae2]) is derived in a similar way, and also has the option of explicitly using the classical independent attributes abstraction along the way. The constructive calculation proceeds from the induced specification: r ℘7→z α (A[ae])(ρ]) , bηzc∗(A[ae]∗(µr(ρ]))) v . . . , bA][ae]c(ρ]) Two notable difference in the constructive calculation setup are: 1. The codomain type for both sides is ℘(Z]), not Z]. This powerset modality 121 makes explicit the transition from “specification“ to “algorithm.” 2. The specification on the left-hand-side is stronger than the classical one, because it does not collapse the set of abstract integers I] : ℘(Z]) into a single least-upper-bound abstract integer i] = ⊔ i]′∈I] i]′. The original classical equation is recovered (in a constructive setting) by composing with the constructive least-upper-bound-abstraction ( unionsq℘ α : ℘(Z]) → ℘1(Z])): unionsq℘ α (bηzc∗(A[ae]∗(µr(ρ])))) v . . . , bA][ae]c(ρ]) However, we will continue our demonstration with the original induced equation, where the constructive least-upper-bound-abstraction is not present. The constructive calculation for the binary expression case proceeds in a similar fashion to Cousot’s classical derivation. To mimic the classical derivation, the independent attributes abstraction is introduced to weaken the specification to discard the equality relationship between evaluation environments used to evaluate ae1 and ae2. The calculation is shown in Figure 4.12. Steps 1–4 unfold semantic function and relation definitions; Step 5 explicitly weakens the specification using independent attributes; Step 6 applies the key independent attributes observation; Step 7 applies the inductive hypothesis; Step 8 combines concretization for independent attributes and the abstraction for integers; Step 9 applies a correct abstract interpreter for binary arithmetic operators (a parameter to the calculation); Step 10 collapses neighboring abstraction and concretization functions; and Step 11 declares the final 122 state of the calculation to be the definition of the algorithm. What this calculation shows is that constructive Galois connections are able to work in tandem with classical Galois connections, as this constructive calculation made use of the classical independent attributes abstraction. 4.9 Optimal Calculations—Constructive and Classical All of the derivations shown in the previous section follow a γ-directed approach to calculation. In this style, the next step of the calculation pushes concretization (γ) through the concrete semantics, from right to left, until it meets abstraction (α) on the far left-hand-side, at which point they collapse. In this section we explore the alternative approach of going the other direction: push abstraction from left-to-right until it meets concretization. In the classical Galois connection framework, both γ-directed and α-directed approaches are similar, and the choice to use one or the other is mostly cosmetic. However, in the constructive framework, abstraction (η) is of a different nature than concretization (µ): it is a pure function with algorithmic content, rather than a relation. This means abstraction is easier to push through the concrete semantics, and therefore η-directed derivations can be simpler than η-directed ones. Because constructive and classical Galois connections are so tightly connected, we show how this insight of η-directed calculations can be translated back to the world of classical Galois connections. To do this, we first make an observation about two restrictions often placed on collecting semantics and classical Galois connections 123 bηzc∗(A[ae1 ⊕ ae2]∗(µr(ρ]))) (1) = * defn. of A[ae1 ⊕ ae2] + bηzc∗( ⋃ ρ∈µr(ρ]) {J⊕Ka(i1, i2) | ρ ` ae1 ⇓a i1 ∧ ρ ` ae2 ⇓a i2}) (2) = * defn. of A[ae1] and A[ae2] + bηzc∗( ⋃ ρ∈µr(ρ]) {J⊕Ka(i1, i2) | i1 ∈ A[ae1](ρ) ∧ i2 ∈ A[ae2](ρ)}) (3) = * defn. of IAγ + bηzc∗( ⋃ ρ∈µr(ρ]) bJ⊕Kac∗(IAγ (A[ae1](ρ),A[ae2](ρ)))) (4) = * set equality + bηzc∗(bJ⊕Kac∗( ⋃ ρ∈µr(ρ]) IA γ (A[ae1](ρ),A[ae2](ρ)))) (5) v * IAγ ◦ IAα extensive (id v IAγ ◦ IAα ) + bηzc∗(bJ⊕Kac∗(IAγ (IAα ( ⋃ ρ∈µr(ρ]) IA γ (A[ae1](ρ),A[ae2](ρ)))))) (6) = * set equality (see (IA-Split) above) + bηzc∗(bJ⊕Kac∗(IAγ (A[ae1]∗(µr(ρ])),A[ae2]∗(µr(ρ]))))) (7) v * inductive hypothesis (A[ae]~ µr v µz ~ bA][ae]c) + bηzc∗(bJ⊕Kac∗(IAγ (µz(A][ae1](ρ])), µz(A][ae2](ρ]))))) (8) = * defn. of IAγ and z×zµ + bηzc∗(bJ⊕Kac∗(z×zµ (A][ae1](ρ]),A][ae2](ρ])))) (9) v * J⊕Ka] correct (bJ⊕Kac~ z×zµ v µz ~ bJ⊕Ka]c) + bηzc∗(µz(J⊕Ka](A][ae1](ρ]),A][ae2](ρ])))) (10) v * bηzc~ µz reductive (bηzc~ µz v ret) + {J⊕Ka](A][ae1](ρ]),A][ae2](ρ]))} (11) , * by A][ae1 ⊕ ae2](ρ]) := J⊕Ka](A][ae1](ρ]),A][ae2](ρ])) + bA][ae1 ⊕ ae2]c(ρ])  Figure 4.12: Constructive Calculation for Binary Arithmetic Expressions 124 in practice: 1. Restricting a predicate transformer (t : ℘(A)→ ℘(B)) to be a complete union morphisms, that is: f( ⋃ i∈I Xi) = ⋃ i∈I (f(Xi)) for some indexed family of sets X : I → ℘(A); and/or 2. Restricting an abstraction function (α : ℘(A) → A]) is required to be a complete join morphism, that is: α( ⋃ i∈I Xi) = ⋃ i∈I (α(Xi)) for some indexed family of sets X : I → A] The main insight of this section is that the first property is equivalent to the existence of a monadic semantics relation, or f : A→ ℘(B), where: t(X) = ⋃ x∈X f(x) and f(x) = t({x}) and the second property is equivalent to the existence of a constructive Galois connection, or η : A → A], where: α(X) = ⊔ x∈X η(x) and η(x) = α({x}) It follows that, in any setting where classical Galois connections are used where the collecting semantics t : ℘(A) → ℘(B) is a complete union morphism, and the abstraction functions αA : ℘(A) → A] and αB : ℘(B) → B] are complete join morphisms, it suffices to work purely with constructive Galois connections without 125 any loss of generality. As a consequence of this, our observation above about η-directed calculations being easier to “push through” the calculation for constructive Galois connections also holds for α-directed classical calculations when the collecting semantics and abstraction function are both complete join/union morphisms. The η-directed calculation of an abstract interpreter for binary arithmetic operator expressions is shown in Figure 4.13. The beginning of the calculation is as before (steps 1–2); Step 3 pushes the abstraction function through the union operation; Step 4 applies a correct abstract interpretation for binary operators (a parameter to the calculation); Step 5 pushes the abstraction function through the set comprehension; Step 6 applies the inductive hypothesis; Step 7 applies the fact that the abstract denotation for binary operators is monotonic, and that powerset are downward closed; Step 8 pushes abstraction again through the set comprehension; Step 9 collapses the neighboring abstraction and concretization functions; and Step 10 declares the final state of the calculation to be the definition of the algorithm. This abstraction-directed calculation is not only simpler due to how easily the abstraction function distributes through powerset operations, but it is also optimal. Unlike the classical calculation (and the constructive µ-directed calculation), no loss in precision is explicitly introduced, and no use of independent attributes is made, explicitly or implicitly. Next, we show how to port this optimal calculation back to the classical Galois connection framework. 126 . . . initial calculation as before (steps 1–2) bηzc∗( ⋃ ρ∈µr(ρ]) {J⊕Ka(i1, i2) | i1 ∈ A[ae1](ρ) ∧ i2 ∈ A[ae2](ρ)}) (3) = * set equality +⋃ ρ∈µr(ρ]) {ηz(J⊕Ka(i1, i2)) | i1 ∈ A[ae1](ρ) ∧ i2 ∈ A[ae2](ρ)} (4) v * J⊕Ka] correct (ηz ◦ J⊕Ka v J⊕Ka] ◦ z×zη ) +⋃ ρ∈µr(ρ]) {J⊕Ka](ηz(i1), ηz(i2)) | i1 ∈ A[ae1](ρ) ∧ i2 ∈ A[ae2](ρ)} (5) = * set equality +⋃ ρ∈µr(ρ]) {J⊕Ka](i]1, i]2) | i]1 ∈ bηzc∗(A[ae1](ρ)) ∧ i]2 ∈ bηzc∗(A[ae2](ρ))} (6) v * inductive hypothesis (bηzc~A[ae] v bA][ae]c~ bηrc) +⋃ ρ∈µr(ρ]) {J⊕Ka](i]1, i]2) | i]1 v A][ae1](ηr(ρ)) ∧ i]2 v A][ae1](ηr(ρ))} (7) = * powerset downward-closed +⋃ ρ∈µr(ρ]) {J⊕Ka](A][ae1](ηr(ρ)),A][ae2](ηr(ρ)))} (8) = * powerset equality +⋃ ρ]′∈bηrc∗(µr(ρ])) {J⊕Ka](A][ae1](ρ]′),A][ae2](ρ]′))} (9) v * bηrc~ µr reductive (bηrc~ µr v ret) + {J⊕Ka](A][ae1](ρ]),A][ae2](ρ]))} (10) , * by A][ae1 ⊕ ae2](ρ]) := J⊕Ka](A][ae1](ρ]),A][ae2](ρ])) + bA][ae1 ⊕ ae2]c(ρ])  Figure 4.13: Constructive Calculation for Binary Arithmetic Expressions—Optimal and η-directed 127 Porting the Optimal Derivation Back to Classical In this η-directed constructive calculation, no steps lose precision unnecessarily. However, the classical calculation required an explicit loss of precision through the independent attributes abstraction. How can this be? To shed light on this question, we show that the constructive abstraction-directed calculation can be back-ported to a classical calculation, leveraging the fact that the abstraction side of Galois connections are complete join morphisms, that is: αz( ⋃ i∈I Xi) = ⊔ i∈I (αz(Xi)) With this observation, a classical derivation is possible which doesn’t need to interact with independent attributes to induce a final algorithm. The classical calculation of binary arithmetic operator expressions is shown in Figure 4.14. The beginning of the calculation is as before (steps 1–3); Step 4 pushes abstraction through the union operation, due to being a complete join morphism; Step 5 applies a correct abstraction for binary operators; Step 6 applies the inductive hypothesis; Step 7 pulls abstraction out of the set comprehension; Step 8 pushes abstraction through the set comprehension, due to being a complete join morphism; Step 9 collapses adjacent abstraction and concretization functions; and Step 10 declares the final state of the calculation to be the definition of the algorithm. 128 . . . initial calculation as before (steps 1–3) αz( ⋃ ρ∈γr(ρ]) {J⊕Ka(i1, i2) | i1 ∈ A[ae1](ρ) ∧ i2 ∈ A[ae2](ρ)}) (4) * αz complete join morphism +⊔ ρ∈γr(ρ]) αz({J⊕Ka(i1, i2) | i1 ∈ A[ae1](ρ) ∧ i2 ∈ A[ae2](ρ)}) (5) v * J⊕Ka] correct (αz ◦ J⊕Ka℘vJ⊕Ka] ◦ z×zα ) +⊔ ρ∈γr(ρ]) J⊕Ka](αz(A[ae1](ρ)), αz(A[ae2](ρ))) (6) v * inductive hypothesis (αz ◦ A℘[ae] = A℘[ae] ◦ αr) +⊔ ρ∈γr(ρ]) J⊕Ka](A][ae1](αr({ρ})),A][ae2](αr({ρ}))) (7) = * set equality +⊔ ρ]′∈{αr({ρ}) | ρ∈γr(ρ])} J⊕Ka](A][ae1](ρ]′),A][ae2](ρ]′)) (8) = * αr complete join morphism +⊔ ρ]′∈{αr(γr(ρ]))} J⊕Ka](A][ae1](ρ]′),A][ae2](ρ]′)) (9) v * αr ◦ γr reductive (αr ◦ γr v id) +J⊕Ka](A][ae1](ρ]),A][ae2](ρ])) (10) , * by A][ae1 ⊕ ae2](ρ]) := J⊕Ka](A][ae1](ρ]),A][ae2](ρ])) + A][ae1 ⊕ ae2](ρ])  Figure 4.14: Classical Calculation for Binary Arithmetic Expressions—Optimal and α-directed 129 4.10 Multivalued Constructive Galois Connections In this section we argue that constructive Galois connections support multivalued Galois connections, concrete semantics, and abstract interpreters, while maintaining their ability to be mechanized effectively. To explore multivalued constructive Galois connections, we again work through an extended example based on the first case study, but this time deriving an ab- stract interpreter for conditional expressions (if be then ce else ce) in the command language (cexp) rather than arithmetic expressions (aexp). Setup To set the stage, we review in Figure 4.15 the types for the command expression relational semantics ( 7→c ), its functional variant (C[ ]) and collecting semantics (C℘[ ]), its abstraction (C][ ]), as well as classical and constructive Galois connections for integers (Z −−−→←−−− ηz µz Z] and Z −−−→←−−− αz γz Z]) and environments (env −−−→←−−− ηr µr env] and env −−−→←−−− αr γr env]). 4.10.1 Review: Cousot’s Original Classical Calculation In the classical Galois connection framework, the abstraction (C][ ]) for the com- mand small-step relational semantics ( 7→c ) is calculated first by constructing the collecting semantics (C℘[ ]), and then relating the collecting semantics to the abstract semantics through a functional abstraction, that is: Σ7→Σ α (C℘[ce])(Σ]) , αΣ(C℘[ce](γΣ(Σ]))) v . . . , C][ce](Σ]) 130 ς ∈ Σ := env× cexp ς] ∈ Σ] := env] × ℘(cexp) 7→c : ℘(Σ× Σ) C[ ] : cexp→ Σ → ℘(Σ) C℘[ ] : cexp→ ℘(Σ) → ℘(Σ) C][ ] : cexp→ Σ] → Σ] ηz : Z→ Z] αz : ℘(Z) → Z] ηr : env→ env] αr : ℘(env) → env] µz : Z] → ℘(Z) γz : Z] → ℘(Z) µr : env] → ℘(env) γr : env] → ℘(env) Figure 4.15: Review: calculating abstraction for conditional expressions where configurations (ς ∈ Σ) are abstracted through a composition of independent attributes and a product abstraction over environments: ℘(Σ) −−−→←−−− IA α IA γ ℘(env)× ℘(cexp) −−−−→←−−−− r×id α r×id γ Σ] αΣ : ℘(Σ) → Σ] γΣ : Σ] → ℘(Σ) αΣ := r×id α ◦ IAα γΣ := IA γ ◦ r×idγ In Cousot’s original derivation, the abstract interpreter is derived for the reflexive transitive closure of the small step relation directly. We will instead present the abstract interpreter for the just the small step relation, factored out from the reflexive transitive closure. The classical calculation begins by case analysis on the syntax for command expressions, so for conditional expressions the calculation is: αΣ(C℘[if be then ce1 else ce2](γr(ρ]))) v . . . , C][if be then ce1 else ce2](ρ]) 131 The calculation is shown in Figure 4.16. Steps 1–4 unfold semantic function and relation definitions; Step 5 weakens the specification through an (implicit) independent attributes abstraction; Step 6 applies a correct abstract interpreter for boolean expressions (a parameter to the calculation); Step 7 weakens the case when neither branch is valid, which would result in the returned abstract environment being bottom (⊥), or the empty map (∅); Step 8 collapses adjacent abstraction and concretization functions; and Step 9 declares the final state of the calculation as the definition of the algorithm. 4.10.2 The Constructive Calculation The goal is now to recreate this calculation using constructive Galois connections. Up until this point, the use of powersets has been entirely restricted to describing classical specifications. However, in this classical derivation, finite powersets appear in the resulting algorithm. Thus, powersets served double-duty: both for classical specification and for multivalued algorithmic results. When porting to constructive Galois connections, this distinction must be made explicit in order to support extraction of a verified algorithm. Constructive Finite Sets To distinguish between classical powersets and algorithmic finite sets, we will continue to notate classical powersets as ℘(A), which are modeled as downward-closed A → prop. We will notate constructive finite sets as p(A), which are representable in an algorithm using a data structure such as a sorted list, binary tree, or hashed dictionary. To distinguish classical powersets 132 αΣ(C℘[if be then ce1 else ce2](γr(ρ]))) (1) = * defn. of C℘[if be then ce1 else ce2] + αΣ( ⋃ ρ∈γr(ρ]) {〈ρ′, ce〉 | 〈ρ, if be then ce1 else ce2〉 7→c 〈ρ′, ce〉}) (2) = * defn. of 〈ρ, if be then ce1 else ce2〉 7→c 〈ρ′, ce′〉 + αΣ( ⋃ ρ∈γr(ρ]) {〈ρ, ce1〉 | ρ ` be ⇓b true} ∪ {〈ρ, ce2〉 | ρ ` be ⇓b false}) (3) = * defn. of ρ ` be ⇓b b + αΣ( ⋃ ρ∈γr(ρ]) {〈ρ, ce1〉 | true = B[be](ρ)} ∪ {〈ρ, ce2〉 | false = B[be](ρ)}) (4) = * set equality (union commutativity) + αΣ ⋃  ⋃ ρ∈γr(ρ]) {〈ρ, ce1〉 | true = B[be](ρ)}⋃ ρ∈γr(ρ]) {〈ρ, ce2〉 | false = B[be](ρ)}  (5) v * monotonicity (independent attributes) + αΣ ⋃{〈ρ, ce1〉 | ρ ∈ γr(ρ]) ∧ ∃ρ′.true = B[be](ρ′)}{〈ρ, ce2〉 | ρ ∈ γr(ρ]) ∧ ∃ρ′.false = B[be](ρ′)}  (6) v * B][be] correct (B℘[be] ◦ γr v γb ◦ B][be]) + αΣ ⋃{〈ρ, ce1〉 | ρ ∈ γr(ρ])} if true v B][be](ρ]){〈ρ, ce2〉 | ρ ∈ γr(ρ])} if false v B][be](ρ])  (7) v * ignore case ¬(true v B][be](ρ]) ∨ false v B][be](ρ])) +〈 αr(γr(ρ])), ⋃{ce1} if true v B][be](ρ]){ce2} if false v B][be](ρ]) 〉 (8) v * αr ◦ γr reductive (αr ◦ γr v id) +〈 ρ], ⋃{ce1} if true v B][be](ρ]){ce2} if false v B][be](ρ]) 〉 (9) , * by C][if be then ce1 else ce2](ρ]) := 〈ρ],⋃{ce1} if true v B][be](ρ]){ce2} if false v B][be](ρ]) 〉 + C][if be then ce1 else ce2](ρ])  Figure 4.16: Classical Calculation for Conditional Command Expressions 133 from constructive finite sets notationally, we will continue to notate elements of powersets of posets X : ℘(A) as {x | P (x)}, which is valid for any downward- closed proposition P : A → prop, and notate elements of constructive finite sets (X : p(A)) as {{x | P (x)}}, which is valid for any decidable downward-closed proposition P : A → B. We relate classical powersets (℘(A)) to constructive finite sets (p(A)) using a constructive Galois connection: p(A) −−→←−− p η p µ ℘(A) p η : p(A) → ℘(A) p µ : ℘(A) → ℘(p(A)) p η(X) := {x | x ∈ X} p µ(X) := {X | ∀x.x ∈ X ⇔ x ∈ X} and define a singleton abstraction for constructive finite sets: A −−−→←−−− 1p η 1p µ p(A) 1p η : A → p(A) 1p µ : p(A) → ℘(A) 1p η (x) := {{x}} 1p µ(X) := {x | x ∈ X} Finally, we redefine abstract configurations (ς] ∈ Σ]) to use constructive finite sets: ς] ∈ Σ] := env] × p(cexp) In this new setting for abstract configurations, the constructive Galois connection for concrete configurations (ς ∈ Σ) is: Σ −−−−→←−−−− r×1p η r×1 µ Σ] r×1p η : Σ→ Σ] r×1p µ : Σ] → ℘(Σ) r×1p η (ρ, ce) := 〈ηr(ρ), {{ce}}〉 r×1p µ (ρ], CE) := {〈ρ, ce〉 | ρ ∈ µr(ρ]) ∧ cd ∈ CE} Using constructive finite sets and this new definition for abstract configurations, we will perform the same calculation as before, but entirely within the constructive 134 Galois connection framework, and in abstraction-directed form. The Calculation We show the calculation for the abstract interpretation of conditional expressions using constructive Galois connections in figures 4.17 and 4.18. Steps 1–3 unfold semantic function and relation definitions; Step 4 applies commuta- tivity of set union; Step 5 pushes abstraction through the set comprehension; Step 6 introduces adjacent concretization and abstraction functions, justified by Galois connection extensiveness (an explicit loss in precision); Step 7 applies the constructive Galois connection correspondence; Step 8 applies a correct abstract interpreter for boolean expressions; Step 9 pulls abstraction out of the set comprehension; Step 10 collapses adjacent abstraction and concretization functions; and Step 11 declares the final state of the calculation as the definition of the algorithm. What this calculation shows is that constructive Galois connections support manipulating multivalued abstractions and algorithms, via an explicit finite set construction, which carries algorithmic content in a constructive logic setting. What classically was just a powerset with finite elements becomes an explicit finite set, and what classically was an undecidable specification of potentially infinite elements remains a powerset. Supporting relational abstraction can be done in this way as well, for example a relational abstraction for environments would have the shape of: rel ηr : p(env) → env] rel µr : env] → ℘(p(env)) 135 bηΣc∗(C[if be then ce1 else ce2]∗(µr(ρ]))) (1) = * defn. of C[if be then ce1 else ce2] + bηΣc∗( ⋃ ρ∈µr(ρ]) {〈ρ′, ce〉 | 〈ρ, if be then ce1 else ce2〉 7→c 〈ρ′, ce〉}) (2) = * defn. of 〈ρ, if be then ce1 else ce2〉 7→c 〈ρ′, ce′〉 + bηΣc∗( ⋃ ρ∈µr(ρ]) {〈ρ, ce1〉 | ρ ` be ⇓b true} ∪ {〈ρ, ce2〉 | ρ ` be ⇓b false}) (3) = * defn. of ρ ` be ⇓b b + bηΣc∗( ⋃ ρ∈µr(ρ]) {〈ρ, ce1〉 | true = B[be](ρ)} ∪ {〈ρ, ce2〉 | false = B[be](ρ)}) (4) = * set equality (union commutativity) + bηΣc∗ ⋃  ⋃ ρ∈µr(ρ]) {〈ρ, ce1〉 | true = B[be](ρ)}⋃ ρ∈µr(ρ]) {〈ρ, ce2〉 | false = B[be](ρ)}  (5) = * set equality + ⋃  ⋃ ρ∈µr(ρ]) {〈ηr(ρ), {{ce1}}〉 | true = B[be](ρ)}⋃ ρ∈µr(ρ]) {〈ηr(ρ), {{ce2}}〉 | false = B[be](ρ)} (6) v * µb ~ bηbc extensive (ret v µb ~ bηbc) + ⋃  ⋃ ρ∈µr(ρ]) {〈ηr(ρ), {{ce1}}〉 | true ∈ µb(ηb(B[be](ρ)))}⋃ ρ∈µr(ρ]) {〈ηr(ρ), {{ce2}}〉 | false ∈ µb(ηb(B[be](ρ)))} . . . Figure 4.17: Conditional Expressions Constructive Calculation 136 . . . (7) = * constructive GC correspondence (b ∈ µb(b])⇔ ηb(b) v b]) + ⋃  ⋃ ρ∈µr(ρ]) {〈ηr(ρ), {{ce1}}〉 | true v ηb(B[be](ρ))}⋃ ρ∈µr(ρ]) {〈ηr(ρ), {{ce2}}〉 | false v ηb(B[be](ρ))} (8) v * B][ ] correct (ηb ◦ B[be] v B][be] ◦ ηr) + ⋃  ⋃ ρ∈µr(ρ]) {〈ηr(ρ), {{ce1}}〉 | true v B][be](ηr(ρ))}⋃ ρ∈µr(ρ]) {〈ηr(ρ), {{ce2}}〉 | false v B][be](ηr(ρ))} (9) = * set equality + ⋃  ⋃ ρ]′∈bηrc∗µr(ρ]) {〈ρ]′, {{ce1}}〉 | true v B][be](ρ]′)}⋃ ρ]′∈bηrc∗µr(ρ]) {〈ρ]′, {{ce2}}〉 | false v B][be](ρ]′)} (10) v * bηrc~ µr reductive (bηrc~ µb v ret) + 〈 ρ], ⋃{{ce1}} if true v B][be](ρ]){{ce2}} if false v B][be](ρ]) 〉 (11) , * by C][if be then ce1 else ce2](ρ]) := 〈ρ],⋃{{ce1}} if true v B][be](ρ]){{ce2}} if false v B][be](ρ]) 〉 + bC][if be then ce1 else ce2]c(ρ]) Figure 4.18: Conditional Expressions Constructive Calculation (Cont.) 137 4.11 Related Work This work connects two long strands of research: abstract interpretation via Galois connections and mechanized verification via dependently typed functional program- ming. The former is founded on the pioneering work of Cousot and Cousot [1977, 1979]; the latter on that of Martin-Lo¨f [1984], embodied in Norell’s Agda [Norell, 2007]. Our key technical insight is to use a monadic structure for Galois connections, following the example of Moggi [1989] for the λ-calculus. Calculational Abstract Interpretation Cousot describes calculational abstract interpretation by example in his lecture notes [2005] and monograph [1999], and Cousot and Cousot recently introduced a unifying calculus for Galois connec- tions [2014]. Our work mechanizes Cousot’s calculations and provides a foundation for mechanizing other instances of calculational abstract interpretation (e.g., [Midtgaard and Jensen, 2008, Sergey et al., 2012]). We expect our work to have applications to the mechanization of calculational program design [Bird and de Moor, 1996, Bird, 1990] by employing only Galois retractions, i.e. α ◦ γ is an identity [Cousot and Cousot, 2014]. There is prior work on mechanized program calculation [Tesson et al., 2011], but it is not based on abstract interpretation. Verified Static Analyzers Verified abstract interpretation has shown many promising results [Barthe et al., 2007, Blazy et al., 2013, Cachera and Pichardie, 2010, Pichardie, 2005], scaling up to large-scale real-world static analyzers [Jourdan et al., 2015]. However, mechanized abstract interpretation has yet to benefit from 138 the Galois connection framework. Until now, approaches use classical axioms or “γ-only” encodings of soundness and (sometimes) completeness. Our techniques for mechanizing Galois connections should complement these approaches. Galculator The Galculator [Silva and Oliveira, 2008] is a proof assistant founded on an algebra of Galois connections. This tool is similar to ours in that it mechanically verifies Galois connection calculations. Our approach is more general, supporting arbitrary set-theoretic reasoning and embedded within a general purpose proof assistant, however their approach is fully automated for the small set of derivations which reside within their supported theory. Deductive Synthesis Fiat [Delaware et al., 2015] is a library for the Coq proof assistant which supports semi-automated synthesis of programs as refinements of their specifications. Fiat uses the same powerset type and monad as we do, and their “deductive synthesis” process similarly derives correct-by-construction programs by calculus. Fiat derivations start with a user-defined specification and calculate towards an under -approximation (w), whereas calculational abstract interpretation starts with an optimal specification and calculates towards an over -approximation (v). It should be possible to generalize their framework to use partial orders to recover aspects of our work, or to invert the lattice used in our abstract interpretation framework to recover aspects of theirs. A notable difference in approach is that Fiat makes heavy use of Coq’s tactic programming language to automate rewrites inside respectful contexts, whereas our system provides no interactive proof automation 139 and each calculational step must be notated explicitly. Monadic Abstract Interpretation Monads in abstract interpretation have recently been applied to good effect for modularity [Darais et al., 2015, Sergey et al., 2013]. However, that work uses monads to structure the semantics, not the Galois connections and proofs. Future Directions Now that we have established a foundation for constructive Galois connection calculation, we see value in verifying larger derivations (e.g., [Midt- gaard and Jensen, 2008, Sergey et al., 2012]). Furthermore we would like to explore whether or not our techniques have any benefit in the space of general-purpose program calculations a` la Bird. Currently our framework requires the user to justify every detail of the program calculation, including monotonicity proofs and proof scoping for rewrites inside monotonic contexts. We imagine much of this can be automated, requiring the user to only provide the interesting parts of the proof, a` la Fiat [Delaware et al., 2015]. Our experience has been that even Coq’s tactic system slows down considerably when automating all of these details, and we foresee using proof by reflection in either Coq (e.g., Rtac [Malecha and Bengtson, 2016]) or Agda to automate these proofs in a way that maintains proof-checker performance. There have been recent developments on compositional abstract interpretation frameworks [Darais et al., 2015] where abstract interpreters and their proofs of soundness are systematically derived side-by-side. That framework relies on correct- 140 ness properties transported by Galois transformers, which we posit would benefit from mechanization since they hold both computational and specification content. 4.12 Conclusions This chapter realizes the vision of mechanized and constructive Galois connections foreshadowed by Cousot [1999, p. 85], giving the first mechanically verified proof by calculational abstract interpretation; once for his generic static analyzer and once for the semantics of gradual typing. Our proofs by calculus closely follow the originals. The primary discrepancy is the use of monads to isolate specification effects. By maintaining this discipline, we are able to verify calculations by Galois connections and extract computational content from pure results. The resulting artifacts are correct-by-verified-construction, thereby avoiding known bugs in the original.2 2http://www.di.ens.fr/~cousot/aisoftware/Marktoberdorf98/Bug_History 141 Chapter 5: Galois Transformers 5.1 Introduction Traditional practice in program analysis via abstract interpretation is to fix a language (as a concrete semantics) and an abstraction (as an abstraction map, concretization map or Galois connection) before constructing a static analyzer that is sound with respect to both the abstraction and the concrete semantics. Thus, each pairing of abstraction and semantics requires a one-off manual derivation of the static analyzer and construction of its proof of soundness. Work has focused on endowing abstractions with knobs, levers, and dials to tune precision and compute efficiently. These parameters come with overloaded meanings such as object, context, path and heap sensitivities, or some combination thereof. These efforts develop families of analyses for a specific language and prove the framework sound. But this framework approach suffers from many of the same drawbacks as the one-off analyzers. They are language-specific, preventing reuse of concepts across languages, and require similar re-implementations and soundness proofs. This process is still manual, tedious, difficult and error-prone. And, changes to the structure of the parameter-space require a completely new proof of soundness. And, it prevents 142 fruitful insights and results developed in one paradigm from being applied to others, e.g., functional to object-oriented and vice versa. We propose an automated alternative to structuring and implementing pro- gram analysis. Inspired by Liang et al.’s Monad Transformers and Modular Inter- preters [1995], we propose to start with concrete interpreters written in a specific monadic style. Changing the monad will transform the concrete interpreter into an abstract interpreter. As we show, classical program abstractions can be embodied as language-independent monads. Moreover, these abstractions can be written as monad transformers, thereby allowing their composition to achieve new forms of analysis. We show that these monad transformers obey the properties of Galois con- nections [Cousot and Cousot, 1979] and introduce the concept of a Galois transformer, a monad transformer which transports Galois connection properties. Most significantly, Galois transformers are proven sound once and for all. Abstract interpreters, which take the form of monad transformer stacks coupled with a monadic interpreter, inherit the soundness properties of each element in the stack. This approach enables reuse of abstractions across languages and lays the foundation for a modular metatheory of program analysis. Setup We describe a simple programming language and a garbage-collecting allocating semantics as the starting point of analysis design (§ 5.2). We then briefly discuss three types of path and flow sensitivity and their corresponding variations in analysis precision (§ 5.3). 143 Monadic Abstract Interpreters We develop an abstract interpreter for our example language as a monadic function with parameters (§ 5.2 and 5.5), one of which is a monadic effect interface combining state and nondeterminism effects (§ 5.4.1). These monadic effects—state and nondeterminism—encode arbitrary relational small- step state-machine semantics and correspond to state-machine components and relational nondeterminism, respectively. Interpreters written in this style are reasoned about using various laws, including monadic effect laws, and are verified correct independent of any particular choice of parameters. Likewise, choices for these parameters are proven correct in isolation from their instantiation. When instantiated, our generic interpreter recovers the concrete semantics and a family of abstract interpreters with variations in abstract domain, abstract garbage collection, call-site sensitivity, object sensitivity, and path and flow sensitivity (§ 5.6). Furthermore, each derived abstract interpreter is proven correct by construction through a reusable, semantics independent proof framework (§ 5.8). Isolating Path and Flow Sensitivity We give specific monads for instan- tiating the interpreter from Section 5.5 to path-sensitive, flow-sensitive and flow- insensitive analyses (§ 5.7). This leads to an isolated understanding of path and flow sensitivity as mere variations in the monad used for execution. Furthermore, these monads are language independent, allowing one to reuse the same path and flow sensitivity machinery for any language of interest, and compose seamlessly with other analysis parameters. 144 Galois Transformers To ease the construction of monads for building abstract interpreters and their proofs of correctness, we develop a framework of Galois trans- formers (§ 5.8). Galois transformers are an extension of monad transformers which transport Galois connection properties (§ 5.8.4). The Galois transformer framework allows us to both execute and justify the correctness of an abstract interpreter piecewise for each transformer. Galois transformers are language independent and they are proven correct once and for all in isolation from a particular semantics. Implementation We implement our technique as a Haskell library and example client analysis (§ 5.9). Developers are able to reuse our language-independent framework for prototyping the design space of analysis features for their language of choice. Our implementation is publicly available on Hackage1, Haskell’s package manager. Contributions We make the following contributions: • A methodology for constructing monadic abstract interpreters based on monadic effects. • A compositional, language-independent framework for constructing monads with varying analysis properties based on monad transformers. • A compositional, language-independent proof framework for constructing Galois connections and end-to-end correctness proofs based on Galois transformers, an extension of monad transformers which transports Galois connection properties. 1http://hackage.haskell.org/package/maam 145 • Two new general purpose monad transformers for nondeterminism which are not present in any previous work on monad transformers (even outside static analysis literature). Although applicable to settings other than static analysis, these two transformers give rise naturally to variations in path and flow sensitivity when applied to abstract interpreters. • An isolated understanding of path and flow sensitivity in analysis as properties of the interpreter monad, which we develop independently of other analysis features. Collectively, these contributions make progress toward a reusable metatheory for program analysis. 5.2 Semantics To demonstrate our framework we design an abstract interpreter for λIF, a simple applied lambda calculus shown in Figure 5.1. λIF extends traditional lambda calculus with integers, addition, subtraction and conditionals. We write @ as explicit abstract syntax for function application. The state-space Σ for λIF makes allocation explicit using two separate stores for values (Store) and for the stack (KStore). Guided by the syntax and semantics of λIF we develop interpretation param- eters in Section 5.4, a monadic interpreter in Section 5.5, and both concrete and abstract instantiations for the interpretation parameters in Section 5.6. The varia- tions in path and flow sensitivity developed in sections 5.7 and 5.8 are independent of this (or any other) semantics. 146 i ∈ Z x ∈ Var a ∈ Atom ::= i | x | λx.e ⊕ ∈ IOp ::= + | − ∈ Op ::= ⊕ | @ e ∈ Exp ::= a | e e | if0(e){e}{e} τ ∈ Time := Z l ∈ Addr := Var× Time ρ ∈ Env := Var ⇀ Addr σ ∈ Store := Addr ⇀ Val c ∈ Clo ::= 〈λx.e, ρ〉 v ∈ Val ::= i | c κl ∈ KAddr := Time κσ ∈ KStore := KAddr ⇀ Frame×KAddr fr ∈ Frame ::= 〈 e, ρ〉 | 〈v 〉 | 〈if0(){e}{e}, ρ〉 ς ∈ Σ ::= 〈e, ρ, σ, κl, κσ, τ〉 Figure 5.1: λIF Syntax and Concrete State Space 147 We define semantics for atomic expressions and primitive operators denota- tionally with AJ K and δJ K, and to compound expressions relationally with , shown in Figure 5.2. Our abstract interpreter supports abstract garbage collection [Might and Shivers, 2006a], the concrete analogue of which is just standard garbage collection. We include abstract garbage collection for two reasons. First, it is one of the few techniques that results in both performance and precision improvements for abstract interpreters. Second, we will systematically recover concrete and abstract garbage collectors with varying path and flow sensitivities through a single monadic garbage collector, an axis of generality novel in this work. We show the garbage collected semantics in Figure 5.3, as well as a final collecting semantics collect, which will serve as the starting point for abstraction. The concrete, garbage-collected collecting semantics collect and a sound static analyzer will both be recovered from instantiations of a generic monadic interpreter in Section 5.6. The garbage collected semantics gc is defined with reachability functions KR and R which define transitively reachable addresses. We write µX.f(X) as the least-fixed-point of the function f . R is defined in terms of R-Frm and R-Val, which define the immediately reachable locations from a frame and value respectively. We omit the definition of FV, which is the standard recursive definition for computing free variables of an expression. 148 AJ K : Atom→ Env× Store ⇀ Val AJiK(ρ, σ) := i AJxK(ρ, σ) := σ(ρ(x)) AJλx.eK(ρ, σ) := 〈λx.e, ρ〉 δJ K : IOp→ Z× Z→ Z δJ+K(i1, i2) := i1 + i2 δJ−K(i1, i2) := i1 − i2 : ℘(Σ× Σ) 〈e1 e2, ρ, σ, κl, κσ, τ〉 〈e1, ρ, σ, τ, κσ′, τ + 1〉 where κσ′ := κσ[τ 7→ 〈〈 e2, ρ〉, κl〉] 〈if0(e1){e2}{e3}, ρ, σ, κl, κσ, τ〉 〈e1, ρ, σ, τ, κσ′, τ + 1〉 where κσ′ := κσ[τ 7→ 〈〈if0(){e2}{e3}, ρ〉, κl〉] 〈a, ρ, σ, κl, κσ, τ〉 〈e, ρ′, σ, τ, κσ′, τ + 1〉 where 〈〈 e, ρ′〉, κl′〉 := κσ(κl) κσ′ := κσ[τ 7→ 〈〈AJaK(ρ, σ) 〉, κl′〉] 〈a, ρ, σ, κl, κσ, τ〉 〈e, ρ′′, σ′, κl′, κσ, τ + 1〉 where 〈〈〈λx.e, ρ′〉@〉, κl′〉 := κσ(κl) ρ′′ := ρ′[x 7→ 〈x, τ〉] σ′ := σ[〈x, τ〉 7→ AJaK(ρ, σ)] 〈i2, ρ, σ, κl, κσ, τ〉 〈i, ρ, σ, κl′, κσ, τ + 1〉 where 〈〈i1 ⊕〉, κl′〉 := κσ(κl) i := δJ⊕K(i1, i2) 〈i, ρ, σ, κl, κσ, τ〉 〈e, ρ′, σ, κl′, κσ, τ + 1〉 where 〈〈if0(){e1}{e2}, ρ′〉, κl′〉 := κσ(κl) e := e1 when i = 0 ; e2 when i 6= 0 Figure 5.2: Concrete Semantics 149 gc : ℘(Σ× Σ) ς gc ς ′ where ς ς ′ 〈e, ρ, σ, κl, κσ, τ〉 gc 〈e, ρ, σ′, κl, κσ′, τ〉 where κσ′ := {κl 7→ κσ(κl) | κl ∈ KR(κl, κσ)} σ′ := {l 7→ σ(l) | l ∈ R(e, ρ, σ, κl, κσ)} KR : KAddr×KStore→ ℘(KAddr) KR(κl, κσ) := µX. X ∪ {κl} ∪ {pi2(κσ(κl)) | κl ∈ X} R : Exp× Env× Store×KAddr×KStore→ ℘(Addr) R(e, ρ, σ, κl, κσ) := µX. X ∪ {ρ(x) | x ∈ FV(e)} ∪ {l | l ∈ R-Frm(pi1(κσ(κl))) ; κl ∈ KR(κl, κσ)} ∪ {l′ | l′ ∈ R-Val(σ(l)) ; l ∈ X} R-Frm : Frame→ ℘(Addr) R-Frm(〈 e, ρ〉) := {ρ(x) | x ∈ FV(e)} R-Frm(〈v 〉) := R-Val(v) R-Frm(〈if0(){e2}{e3}, ρ〉) := {ρ(x) | x ∈ FV(e1) ∪ FV(e2)} R-Val ∈ Val→ ℘(Addr) R-Val(i) := {} R-Val(〈λx.e, ρ〉) := {ρ(y) | y ∈ FV(λx.e)} collect : ℘(Σ) collect := µX.X ∪ {ς0} ∪ {ς ′ | ς gc ς ′ ; ς ∈ X} where ς0 := 〈e0,⊥,⊥, 0,⊥, 1〉 Figure 5.3: Garbage Collected Collecting Semantics 150 5.3 Path and Flow Sensitivity in Analysis We identify three specific variants of path and flow sensitivity in analysis: path- sensitive, flow-sensitive and flow-insensitive. Our framework exposes the essence of path and flow sensitivity through a monadic effect interface in Section 5.4, and we recover each of these variations through specific monad instances in sections 5.7 and 5.8. Consider a combination of if-statements in our example language λIF (extended with let-bindings) where an analysis cannot determine the value of N : (1) let x := (2) if0(N){ (3) if0(N){1}{2} }{ (4) if0(N){3}{4} } in (5) let y := if0(N){5}{6} in (6) exit(x, y) Path-Sensitive A path-sensitive analysis tracks both data and control flow precisely. At lines 3 and 4 the analysis considers separate worlds: 3 : {N = 0} 4 : {N 6= 0} At Line 5 the analysis continues in two separate, precise worlds: 5 : {N = 0, x = 1} {N 6= 0, x = 4} 151 At Line 6 the analysis correctly correlates x and y: 6 : {N = 0, x = 1, y = 5} {N 6= 0, x = 4, y = 6} Flow-Sensitive A flow-sensitive analysis collects a single set of facts for each variable at each program point. At lines 3 and 4, the analysis considers separate worlds: 3 : {N = 0} 4 : {N 6= 0} Each nested if-statement then evaluates only one side of the branch, resulting in values 1 and 4. At Line 5 the analysis is only allowed one set of facts, so it must merge the possible values that x and N could take: 5 : {N ∈ Z, x ∈ {1, 4}} The analysis then explores both branches at Line 5 resulting in no correlation between values for x and y at Line 6: 6 : {N ∈ Z, x ∈ {1, 4}, y ∈ {5, 6}} Flow-Insensitive A flow-insensitive analysis collects a single set of facts about each variable which must hold true for the entire program. Because the value of N is unknown at some point in the program, the value of x must consider both branches of the nested if-statement. This results in the global set of facts giving four values 152 to x: 1–6 : {N ∈ Z, x ∈ {1, 2, 3, 4}, y ∈ {5, 6}} 5.4 Analysis Parameters Before constructing the abstract interpreter we first design its parameters. The interpreter, which we develop in Section 5.5, will be designed such that variations in these parameters will recover both concrete and a family of abstract interpreters, which we show in Section 5.6. To do this we extend the ideas developed in Van Horn and Might [2010] with a new parameter for path and flow sensitivity: the interpreter monad. There will be three parameters to our abstract interpreter: 1. The monad, novel in this work, which captures control effects and gives rise to path and flow sensitivity. 2. The abstract domain, which captures the abstraction of values like integers or datatypes. 3. The abstraction for time, which captures call-site and object sensitivities. We place each of these parameters behind an abstract interface and leave their implementations opaque when defining the monadic interpreter in Section 5.5. Each parameter comes with laws which can be used to reason about the generic interpreter independent of a particular instantiation. Likewise, an instantiation of the interpreter 153 need only justify that each parameter meets its local interface, which we justify in isolation from the generic interpreter. 5.4.1 The Analysis Monad The monad for the interpreter captures the effects of interpretation. There are two effects in the interpreter: state and nondeterminism. The state effect will mediate how the interpreter interacts with state cells in the state space: Env, Store, KAddr, KStore and Time. The nondeterminism effect will mediate branching in the execution of the interpreter. Path and flow sensitivity will be recovered by altering how these effects interact in a particular choice of monad. We use monadic state and nondeterminism effects to abstract over arbitrary relational small-step state-machine semantics. State effects correspond to the com- ponents of the state-machine and nondeterminism effects correspond to potential nondeterminism in the relation’s definition. We briefly review monad, state and nondeterminism operators and their laws. For a more details see Gibbons and Hinze [2011], Liang et al. [1995], Moggi [1989]. Monad Operators A type operator m is a monad if it supports bind, a sequencing operator, and its unit return: m : Type→ Type return : ∀A.A→ m(A) bind : ∀AB.m(A)→ (A→ m(B))→ m(B) and obeys left unit, right unit and associativity laws. 154 We use semicolon notation for bind (e.g., x← X ; k(x) is sugar for bind(X)(k)) and we replace semicolons with line breaks headed by do for multiline monadic definitions. State Effect A type operator m supports the monadic state effect for a type s if it supports get and put operations over s: s : Type m : Type→ Type get : m(s) put : s→ m(unit) and obeys get-get, get-put, put-get and put-put laws [Gibbons and Hinze, 2011]. Nondeterminism Effect A type operator m supports the monadic nondeter- minism effect if it supports an alternation operator  and its unit mzero: m : Type→ Type  : ∀A.m(A)×m(A)→ m(A) mzero : ∀A.m(A) The type m(A) must have a join-semilattice structure, mzero must be a zero for bind, and bind must distribute through . The interpreter in Section 5.5 will be defined generic to a monad which supports monad operators, state effects and nondeterminism effects. As a consequence, we do not reference an explicit configuration ς or collections of results; instead we interact with an interface of state and nondeterminism effects. This level of indirection will be exploited in Section 5.7, where different monads will meet the same effect interface but yield different analysis properties. 155 5.4.2 The Abstract Domain To expose the abstract domain we parameterize over Val, introduction and elimination forms for Val, and the denotation for primitive operators δJ K. Val must be a join-semilattice with unionsq and its unit ⊥: ⊥ : Val unionsq : Val× Val→ Val and respect the usual join-semilattice laws. Val must be a join-semilattice so it can be merged in updates to Store to preserve soundness. Val must also support introduction and elimination between finite sets of concrete values Z and Clo: int-I : Z→ Val clo-I : Clo→ Val if0-E : Val→ ℘(Bool) clo-E : Val→ ℘(Clo) Introduction functions inject concrete values into abstract values. Elimination functions project abstract values into a finite set of concrete observations. For example, we do not require that abstract values support elimination to integers, only to finite observation of comparison with zero. The laws for the introduction and elimination functions induce a Galois connection between ℘(Z) and Val : {true} ⊆ if0-E(int-I(i)) if i = 0 {false} ⊆ if0-E(int-I(i)) if i 6= 0⊔ b∈if0-E(v) i∈θ(b) int-I(i) v v where θ(true) := {0} θ(false) := {i | i ∈ Z ; i 6= 0} 156 Closures must follow similar laws, inducing a Galois connection between ℘(Clo) and Val : {c} ⊆ clo-E(cloI(c))⊔ c∈clo-E(v) clo-I(c) v v Finally, δJ K must be sound w.r.t. the Galois connection between concrete values and Val : int-I(i1 + i2) v δJ+K(int-I(i1), int-I(i2)) int-I(i1 − i2) v δJ−K(int-I(i1), int-I(i2)) Supporting additional primitive types like booleans, lists, or arbitrary inductive datatypes is analogous. Introduction functions inject the type into Val and elimina- tion functions project a finite set of discrete observations. Introduction, elimination and δ operators must all be sound and complete following a Galois connection discipline. 5.4.3 Abstract Time The interface we use for abstract time is familiar from Van Horn and Might [2010], which introduces abstract time as a single parameter to control various forms of context sensitivity, and Smaragdakis et al. [2011], which instantiates the parameter to achieve various forms of object sensitivity. We only demonstrate call-site sensitivity in this presentation; our semantics-independent Haskell library supports object sensitivity following the same methodology. 157 Abstract time need only support a single operation: tick : Time : Type tick : Exp×KAddr× Time→ Time Remarkably, we need not state laws for tick. The interpreter will merge values which reside at the same address to preserve soundness. Therefore, any supplied implementations of tick is valid from a soundness perspective. However, different choices in tick will yield different trade-offs in precision and performance of the abstract interpreter. 5.5 The Interpreter We now present a monadic interpreter for λIF parameterized over m, Val and Time from Section 5.4. We instantiate these parameters to obtain an analysis in Section 5.6. We translate AJ K, a partial denotation function, to AmJ K, a total monadic denotation function, shown in Figure 5.4. Next we implement stepm, a monadic small-step function for compound ex- pressions, also shown in Figure 5.4. stepm is a translation of from a relation to a monadic function with state and nondeterminism effects. stepm uses push and pop for manipulating stack frames, ↑p for lifting values from ℘ into m, refine for value refinement after branching, and a monadic version of tick called tickm, each shown in Figure 5.5. Frames are pushed when the control expression e is compound and popped when e is atomic. The interpreter looks deterministic, however the nondeterminism is hidden behind ↑p and monadic bind 158 AmJ K : Atom→ m(Val) AmJiK := return(int-I(i)) AmJxK := do ρ← get-Env ; σ ← get-Store if x ∈ ρ then return(σ(ρ(x))) else return(⊥) AmJλx.eK := ρ← get-Env ; return(clo-I(〈λx.e, ρ〉)) stepm : Exp→ m(Exp) stepm(e) := do tickm(e) ; ρ← get-Env e′ ← case e of e1 e2 → push(〈 e2, ρ〉) ; return(e1) if0(e1){e2}{e3} → push(〈if0(){e2}{e3}, ρ〉) ; return(e1) a→ do v ← AmJaK ; fr ← pop case fr of 〈 e, ρ′〉 → put-Env(ρ′) ; push(〈v 〉) ; return(e) 〈v′@〉 → do τ ← get-Time ; σ ← get-Store 〈λx.e, ρ′〉 ← ↑p(clo-E(v′)) put-Env(ρ′[x 7→ 〈x, τ〉]) ; put-Store(σ unionsq [〈x, τ〉 7→ v]) ; return(e) 〈v′ ⊕〉 → return(δJ⊕K(v′, v)) 〈if0(){e1}{e2}, ρ′〉 → do put-Env(ρ′) ; b← ↑p(if0-E(v)) ; refine(a, b) if(b) then return(e1) else return(e2) gc(e′) ; return(e′) Figure 5.4: Monadic Semantics 159 operations x← e1 ; e2. The use of refine enforces a limited form of path-condition, and will yield each variation of path and flow sensitivity given the appropriate monad. We implement abstract garbage collection gc in a general way using the monadic effect interface, also shown in Figure 5.5. R and KR are as defined in Section 5.2. Remarkably, this single implementation supports instantiation to analyses with varying path and flow sensitivities. Preserving Soundness In the monadic interpreter, updates to both the data- store and stack-store must merge rather than overwrite values. To support unionsq for the stack store we redefine the domain to map to a powerset of frames: κσ ∈ KStore : KAddr→ ℘(Frame×KAddr) Execution In the concrete semantics, execution takes the form of a least-fixed- point computation over the collecting semantics collect. This in general requires a join-semilattice structure for some Σ and a transition system Σ→ Σ. However, we no longer have a transition system Σ→ Σ; we have a monadic function Exp→ m(Exp) which cannot be iterated to least-fixed-point to execute the analysis. To solve this we require the existence of a Galois connection between monadic actions and some transition system: Σ→ Σ −−−−−→←−−−−− αΣ↔m γΣ↔m Exp→ m(Exp). This Galois connection allows us to implement the analysis by transporting our interpreter to the transition system Σ → Σ through γΣ↔m, and then iterating to fixed-point in Σ. Furthermore, it serves to transport other Galois connections as part of our correctness framework. This will allow us to construct Galois connections between 160 push : Frame→ m(unit) push(fr) := do κl← get-KAddr ; κσ ← get-KStore ; κl′ ← get-Time put-KStore(κσ unionsq [κl′ 7→ {fr :: κl}]) ; put-KAddr(κl′) pop : m(Frame) pop := do κl← get-KAddr ; κσ ← get-KStore ; fr :: κl′ ← ↑p(κσ(κl)) put-KAddr(κl′) ; return(fr) ↑p : ∀A.℘(A)→ m(A) ↑p({a1, . . . , an}) := return(a1) · · · return(an) refine : Atom× Bool→ m(unit) refine(i, b) := return(unit) refine(x, b) := do ρ← get-Env ; σ ← get-Store put-Store(σ[ρ(x) 7→ b]) tickm : Exp→ m(unit) tickm(e) := do τ ← get-Time ; κl← get-KAddr put-Time(tick(e, κl, τ)) gc : Exp→ m(unit) gc(e) := do ρ← get-Env ; σ ← get-Store κl← get-KAddr ; κσ ← get-KStore put-KStore({κl 7→ κσ(κl) | κl ∈ KR(κl, κσ)}) put-Store({l 7→ σ(l) | l ∈ R(e, ρ, σ, κl, κσ)}) Figure 5.5: Monadic helper functions 161 monads m1 −−−−→←−−−− αm γm m2 and derive Galois connections between transition systems Σ1 −−−→←−−− αΣ γΣ Σ2. An execution of our interpreter is then the least-fixed-point iteration of stepm transported through γΣ↔m: analysis := µX.X unionsq ς0 unionsq γΣ↔m(stepm)(X) where ς0 is the injection of the initial program e0 into Σ and γ Σ↔m has type (Exp→ m(Exp))→ Σ→ Σ. 5.6 Recovering Analyses In Section 5.5, we defined a monadic interpreter with the uninstantiated parameters from Section 5.4: m, Val and Time. To recover a concrete interpreter, we instantiate these parameters to concrete components M \, Val\ and Time\, and to recover an abstract interpreter we instantiate them to abstract components M ], Val] and Time]. Furthermore, the concrete transition system Σ\ induced by M \ will recover the collecting semantics, which is our final target of abstraction, and the resulting analysis will take the form of an abstract transition system Σ] induced by M ]. 5.6.1 Recovering a Concrete Interpreter To recover a concrete interpreter, we instantiate the generic monadic interpreter from Section 5.5 with concrete parameters Val\, δ\, Time\ and M \, shown in figures 5.6 and 5.7. 162 v ∈ Val\ := ℘(Clo\ ∪ Z) τ ∈ Time\ := (Exp×KAddr\)∗ int-I\ : Z→ Val\ int-I\(i) := {i} if0-E\ : Val\ → ℘(Bool) if0-E\(v) := {true | 0 ∈ v} ∪ {false | ∃i ∈ v ; i 6= 0} Clo-I\ : Clo\ → Val\ Clo-I\(c) := {c} Clo-E\ : Val\ → ℘(Clo\) Clo-E\(v) := {c | c ∈ v} δ\ : Val\ × Val\ → Val\ δ\J+K(v1, v2) := {i1 + i2 | i1 ∈ v1 ; i2 ∈ v2} δ\J−K(v1, v2) := {i1 − i2 | i1 ∈ v1 ; i2 ∈ v2} tick\ : Exp× Time\ → Time\ tick\(e, κl, τ) := 〈e, κl〉 :: τ Figure 5.6: Concrete Interpreter Values and Time 163 ψ ∈ Ψ\ := Env\ ×KAddr\ ×KStore\ × Time\ X ∈M \(A) := Ψ\ × Store\ → ℘(A×Ψ\ × Store\) ς ∈ Σ\ := ℘(Exp×Ψ\ × Store\) return\ : ∀A.A→M \(A) return\(x)(ψ, s) := {〈x, ψ, s〉} bind\ : ∀AB.M \(A)→ (A→M \(B))→M \(B) bind\(X)(f)(ψ, σ) := ⋃ 〈x,ψ′,σ′〉∈X(ψ,σ) f(x)(ψ′, σ′) get-env\ : M \(Env\) get-env\(〈ρ, κl, κσ, τ〉, σ) := {〈ρ, 〈ρ, κl, κσ, τ〉, σ〉} put-Env\ : Env\ →M \(unit) put-Env\(ρ′)(〈ρ, κl, κσ, τ〉, σ) := {〈•, 〈ρ′, σ, κ, τ〉, σ〉} mzero\ : ∀A.M \(A) mzero\(ψ, σ) := {} \ : ∀A.M \(A)×M \(A)→M \(A) (X1 \ X2)(ψ, σ) := X1(ψ, σ) ∪X2(ψ, σ) αΣ \↔M\ : (Σ\ → Σ\)→ Exp→M \(Exp) αΣ \↔M\(f)(e)(ψ, σ) := f({〈e, ψ, σ〉}) γΣ \↔M\ : (Exp→M \(Exp))→ Σ\ → Σ\ γΣ \↔M\(f)(eψσ∗) := ⋃ 〈e,ψ,σ〉∈eψσ∗ f(e)(ψ, σ) Figure 5.7: Concrete Interpreter Monad 164 The Concrete Domain We instantiate Val to Val\, a powerset of concrete values. Val\ has precise introduction and elimination functions int-I\, if0-E\, Clo-I\ and Clo-E\, and primitive operator denotation δ\. Concrete Time We instantiate Time to Time\, which captures the execution context as a sequence of previously visited expressions. tick\ is then a cons operation. The Concrete Monad We instantiate m to M \, a powerset of concrete state space components. Monadic operators bind\ and return\ encapsulate both state- passing and set-flattening. State effects return singleton sets and nondeterminism effects are implemented with set union. Concrete Execution To execute the interpreter we establish the Galois con- nection Σ\ → Σ\ −−−−−−−→←−−−−−−− αΣ \↔M\ γΣ \↔M\ Exp→M \(Exp) and transport the monadic interpreter through γΣ \↔M\ . The injection for a program e0 into Σ\ is ς0 := {〈e0,⊥,⊥, ,⊥, 〉}. 5.6.2 Recovering an Abstract Interpreter To recover an abstract interpreter we instantiate the generic monadic interpreter from Section 5.5 with abstract parameters Val], δ], Time] and M ], shown in Figure 5.8. The abstract monad operators, effects and transition system are not shown for M ]; they are identical to M \ but with abstract components. The Abstract Domain We pick a simple abstraction for integers, {−, 0,+}, although our technique scales to other abstract domains. Abstract values Val] 165 v ∈ Val] := ℘(Clo] ∪ {−, 0,+}) τ ∈ Time] := (Exp×KAddr])∗k ψ ∈ Ψ] := Env] ×KAddr] ×KStore] × Time] X ∈M ](A) := Ψ] × Store] → ℘(A×Ψ] × Store]) ς ∈ Σ] := ℘(Exp×Ψ] × Store]) int-I] : Z→ Val] if0-E] : Val] → ℘(Bool) Clo-I] : Clo] → Val] Clo-E] : Val] → ℘(Clo) int-I](i) :=  {−} if i < 0 {0} if i = 0 {+} if i > 0 if0-E](v) := ⋃{true} when 0 ∈ v{false} when − ∈ v ∨+ ∈ v Clo-I](c) := {c} Clo-E](v) := {c | c ∈ v} δ] : Val] × Val] → Val] δ]J+K(v1, v2) := ⋃  {i | i ∈ v2} when 0 ∈ v1 {i | i ∈ v1} when 0 ∈ v2 {+} when + ∈ v1 ∧+ ∈ v2 {−} when − ∈ v1 ∧ 0 ∈ v2 {−, 0,+} when + ∈ v1 ∧ − ∈ v2 {−, 0,+} when − ∈ v1 ∧+ ∈ v2 δ]J−K(v1, v2) := . . . analogous . . . tick] : Exp× Time] → Time] tick](e, κl, τ) := b〈e, κl〉 :: τck Figure 5.8: Abstract Interpreter Parameters 166 is defined as a powerset of abstract values. Val] has introduction and elimination functions int-I], if0-E], clo-I] and clo-E], and primitive operator denotation δ]. if0-E] and δ] must be conservative, returning an upper bound of the precise results returned by their concrete counterparts. Abstract Time Abstract time Time] captures an approximation of the execution context as a finite sequence of previously visited expressions. tick] is a cons operation followed by k-truncation, yielding a kCFA analysis [Van Horn and Might, 2010]. The Abstract Monad and Execution The abstract monad M ] is identical to M \ up to the definition of Ψ]. The induced state space Σ] is finite, and its least-fixed-point iteration will give a sound and computable analysis. 5.6.3 End-to-end Correctness The end-to-end correctness of the abstract instantiation of the interpreter is factored into three steps: (1) proving the parameterized monadic interpreter correct for any instantiation of m, Val and Time; (2) constructing Galois connections M \ −−−−→←−−−− αm γm M ], Val\ −−−→←−−− αv γv Val] and Time\ −−−→←−−− αt γt Time] piecewise; and (3) transporting the combination of (1) and (2) from the monadic function space A → m(B) to its induced transition system Σ→ Σ. The benefit of our approach is that the first step is proved once and for all (for a particular semantics) against any instantiation of m, Val and Time using the reasoning principles established in Section 5.4. Furthermore the second step can be proved in isolation of the first, and the construction of the 167 third step is fully systematic. We do not give proofs for (1) or the abstractions for Val and Time for (2) in this chapter, although the details can be found in prior work [Cousot, 1999, Van Horn and Might, 2010]. Rather, we give definitions and proofs for the monad abstractions for (2) and their systematic mappings to transition systems for (3) through a compositional framework in Section 5.8. The final correctness of the abstract interpreter is established as a partial order relationship between an abstraction of γΣ \↔M\(stepm[M \]), which recovers the collecting semantics, and γΣ ]↔M](stepm[M ]]), the induced abstract semantics: Proposition 1. αΣ \ (γΣ \↔M\(stepm[M \])) v γΣ]↔M](stepm[M ]]) The left-hand-side of the relationship is the induced “best specification” of the collecting semantics via Galois connection, and should be familiar from the literature on abstract interpretation [Cousot, 1999, Cousot and Cousot, 1979, Nielson et al., 1999]. This end-to-end correctness statement will be justified in a compositional setting in Section 5.8. 5.7 Varying Path and Flow Sensitivity Sections 5.5 and 5.6 describe the construction of a path-sensitive analysis using our framework. In this section, we show an alternate definition for M ] which yields a flow-insensitive analysis. Section 5.8 will generalize the definitions from this section 168 into compositional components (monad transformers) in addition to introducing another definition for M ] which yields a flow-sensitive analysis. Before going into the details of the flow-insensitive monad, we wish to build intuition regarding what one would expect from such a development. Recall the path-sensitive monad M ] and its state space Σ] from Section 5.6: M ](Exp) := Ψ] × Store] → ℘(Exp×Ψ] × Store]) Σ](Exp) := ℘(Exp×Ψ] × Store]) where Ψ := Env] × KAddr] × KStore] × Time]. This is path-sensitive because Σ](Exp) can represent arbitrary relations between (Exp×Ψ) and Store]. As discussed in Section 5.3, a flow-sensitive analysis will give a single set of facts per program point. This results in the following monad M ]fs and state space Σ]fs which encode finite maps to Store] rather than relations: M ]fs(Exp) := Ψ] × Store] → [Exp×Ψ] 7→ Store]] Σ]fs(Exp) := [Exp×Ψ] 7→ Store]] Finally, a flow-insensitive analysis must contain a global set of facts for each variable, which we achieve by pulling Store] out of the powerset: M ]fi(Exp) := Ψ] × Store] → ℘(Exp×Ψ])× Store] Σ]fi(Exp) := ℘(Exp×Ψ])× Store] These three resulting structures, Σ], Σ]fs and Σ]fi, capture the essence of path- sensitive, flow-sensitive and flow-insensitive transition systems, and arise naturally from M ], M ]fs and M ]fi, which each have monadic structure. We only describe M ]fi directly in this section; in Section 5.8 we describe a more compositional set of building blocks, from which M ], M ]fs and M ]fi are recovered. 169 5.7.1 Flow Insensitive Monad We show the definitions for monad operators, state effects, nondeterminism effects, and mapping to transition system for the flow-insensitive monad M ]fi in Figure 5.9. The bind]fi operation performs the global store merging required to capture a flow-insensitive analysis. The unit for bind]fi returns one nondeterminism branch and a single global store. State effects get-Env]fi and put-Env]fi return a single branch of nondeterminism. Nondeterminism operations union the powerset and join the store pairwise. Finally, the Galois connection relating M ]fi to the state space Σ]fi also computes powerset unions and store joins pairwise. Instantiating the generic monadic interpreter with M \, M ] and M ]fi yields a concrete interpreter, path-sensitive abstract interpreter, and flow-insensitive abstract interpreter respectively, purely by changing the underlying monad. Furthermore, the proofs of abstraction between interpreters and their induced transition systems is isolated to a proof of abstraction between monads. 5.8 A Compositional Monadic Framework In our development thus far, any modification to the interpreter requires redesigning the monad M ] and constructing new proofs relating M ] to M \. We want to avoid reconstructing complicated monads for interpreters, especially as languages and analyses grow and change. Even more, we want to avoid reconstructing complicated proofs that such changes require. Toward this goal, we introduce a compositional framework for constructing monads which are correct-by-construction by extending 170 M ]fi(A) := Ψ] × Store] → ℘(A×Ψ])× Store] ς ∈ Σ]fi := ℘(Exp×Ψ])× Store] return]fi : ∀A.A→M ]fi(A) return]fi(x)(ψ, σ) := ({x, ψ}, σ) bind]fi : ∀AB.M ]fi(A)→ (A→M ]fi(B))→M ]fi(B) bind]fi(X)(f)(ψ, σ) := ({yψ11, . . . , yψ1m1 , . . . , yψn1, . . . , yψnmn}, σ1 unionsq · · · unionsq σn) where ({〈x1, ψ1〉, . . . , 〈xn, ψn〉}, σ′) := X(ψ, σ) ({yψi1, . . . , yψimi}, σi) := f(xi)(ψi, σ′) get-Env]fi : M ]fi(Env]) get-Env]fi(〈ρ, κ, τ〉, σ) := ({(ρ, 〈ρ, κ, τ〉)}, σ) put-Env]fi : Env] →M ]fi(unit) put-Env]fi(ρ′)(〈ρ, κ, τ〉, σ) := ({〈•, 〈ρ′, κ, τ〉〉}, σ) mzero]fi : ∀A.M ]fi(A) mzero]fi(ψ, σ) := 〈{},⊥〉 ]fi : ∀A.M ]fi(A)×M ]fi(A)→M ]fiA (X1 ]fi X2)(ψ, σ) := (xψ∗1 ∪ xψ∗2, σ1 unionsq σ2) where (xψ∗i , σi) := Xi(ψ, σ) αΣ ]↔M]fi : (Σ]fi → Σ]fi)→ Exp→M ]fi(Exp) αΣ ]↔M]fi(f)(e)(ψ, σ) := f({〈e, ψ〉}, σ) γΣ ]↔M]fi : (Exp→M ]fi(Exp))→ Σ]fi → Σ]fi γΣ ]↔M]fi(f)(eψ∗, σ) := ({eψ11, . . . , eψn1, . . . , eψnmn}, σ1 unionsq · · · unionsq σn) where {〈e1, ψ1〉, . . . , 〈en, ψn〉} := eψ∗ ({eψi1, . . . , eψimi}, σi) := f(ei)(ψi, σ) Figure 5.9: Flow Insensitive Monad Parameter 171 the well-known structure of monad transformer to that of Galois transformer. Galois transformers are monad transformers which transport Galois connections and mappings to an executable transition system. We make this definition precise and prove our Galois transformers correct in Section 5.8.4. For now we present monad transformer operations augmented with the computational part of Galois transformers: the mapping to a transition system, which we called αΣ \↔M\ , γΣ \↔M\ , αΣ ]↔M]fi and γΣ ]↔M]fi in sections 5.6 and 5.7. There are two monadic effects used in our monadic interpreter: state and nondeterminism. For state, we review the state monad transformer St[s], which is standard [Liang et al., 1995, Moggi, 1989], however we also show how St[s] maps to a transition system and obeys Galois transformer properties. For nondeterminism we develop two new monad transformers: ℘t and F t[s]. These monad transformers are fully general purpose, even outside the context of program analysis, and are novel in this work. Finally we show that ℘t and F t[s] map to transition systems and obey Galois transformer properties. To create a monad with various state and nondeterminism effects, one need only construct some composition of these three monad transformers. Implementations and proofs for monadic sequencing, state effects, nondeterminism effects, and mappings to an executable transition system will come entirely for free. This means that for a language which has a different state space than the example in this chapter, no added effort is required to construct a monad stack for that language; it will merely require a different selection and permutation of the same monad transformer components. Path and flow sensitivity properties arise from the order of composition of 172 state and nondeterminism monad transformers. Placing state after nondeterminism (St[s] ◦ ℘t or St[s] ◦ F t[s′]) will result in s being path-sensitive. Placing state before nondeterminism (℘t ◦ St[s] or F t[s′] ◦ St[s]) will result in s being flow-insensitive. Finally, when F t[s] is used in place of St[s] ◦ ℘t or ℘t ◦ St[s], s will be flow-sensitive. The combination of all three sensitivities is M := St[s1]◦F t[s2]◦St[s3] which induces the transition system Σ(Exp) := [Exp× s1 7→ s2]× s3, where s1 is path-sensitive, s2 is flow-sensitive, and s3 is flow-insensitive. Using S t[s], ℘t and F t[s], one can easily choose which components of the state space should be path-sensitive, flow-sensitive or flow-insensitive, purely by the order of monad composition. In the following definitions we must refer to bind, return and other operations from the underlying monad, which we notate bindm, returnm, ←m, etc. 5.8.1 State Galois Transformer The state Galois transformer is shown in Figure 5.10. returnS t , bindS t , getS t and putS t require that m be a monad. mzeroS t and St require that m be a monad with nondeterminism effects. And finally, αS t and γS t require that m maps to Σm via Galois connection Σ(A)→ Σ(B) −−−−→←−−−− αm γm A→ m(B). 5.8.2 Nondeterminism Galois Transformer The nondeterminism Galois transformer is shown in Figure 5.11. Crucially, return℘ t and bind℘ t require that m be both a monad and a join-semilattice functor. We attribute this requirement (and the difficulty of expressing it in Haskell) as a possible 173 St[s] : (Type→ Type)→ Type→ Type St[s](m)(A) := s→ m(A× s) ΠS t [s] : (Type→ Type)→ Type→ Type ΠS t [s](Σ)(A) := Σ(A× s) returnS t : ∀A.A→ St[s](m)(A) returnS t (x)(s) := returnm(x, s) bindS t : ∀AB.St[s](m)(A)→ (A→ St[s](m)(B))→ St[s](m)(B) bindS t (X)(f)(s) := 〈x, s′〉 ←m X(s) ; f(x)(s′) getS t : St[s](m)(s) getS t (s) := returnm(s, s) putS t : s→ St[s](m)(unit) putS t (s′)(s) := returnm(•, s′) mzeroS t : ∀A.St[s](m)(A) mzeroS t (s) := mzerom St : ∀A.St[s](m)(A)× St[s](m)(A)→ St[s](m)(A) (X1 S t X2)(s) := X1(s)m X2(s) αS t : ∀AB.(ΠSt [s](Σm)(A)→ ΠSt [s](Σm)(B))→ A→ St[s](m)(B) αS t (f)(x)(s) := αm(f)(x, s) γS t : ∀AB.(A→ St[s](m)(B))→ ΠSt [s](Σm)(A)→ ΠSt [s](Σm)(B) γS t (f) := γm(λ〈x, s〉.f(x)(s)) Figure 5.10: State Galois Transformer 174 reason why it has not been discovered thus far. This functorality of m is instantiated with ℘( ) using the usual join-semilattice on powersets: {} for ⊥ and ∪ for unionsq. get℘ t and put℘ t require that m be a monad with state effects. Like the state Galois transformer, α℘ t and γ℘ t require that m maps to Σm via Galois connection. Lemma 3. [℘t laws] bind℘ t and return℘ t satisfy monad laws, get℘ t and put℘ t satisfy state monad laws, and mzero℘ t and ℘t satisfy nondeterminism monad laws. See our proofs in Section A, where the key lemma in proving monad laws is the join-semilattice functorality of m, namely that: returnm(x unionsq y) = returnm(x) unionsqm returnm(y) bindm(X unionsq Y )(f) = bindm(X)(f) unionsqm bindm(Y )(f) 5.8.3 Flow Sensitivity Galois Transformer The flow sensitivity monad transformer, shown in Figure 5.12, is a unique monad transformer that combines state and nondeterminism effects, and does not arise naturally from composing vanilla nondeterminism and state transformers. The finite map in the definition of F t[s] is what yields flow sensitivity when instantiated to a monadic interpreter. After instantiation, F t[s](m)(A) will be Store] → [Exp×Ψ] → Store]], which maps each possible expression and context to a unique abstract store. Like nondeterminism, returnF t and bindF t require that m be both a monad and a join-semilattice functor. This functorality of m is instantiated with [ 7→ s] using the usual join-semilattice on finite maps: {} for ⊥ and: Y unionsq Z := {x 7→ y unionsq z | {x 7→ y} ∈ X ∧ {x 7→ z} ∈ Y } 175 ℘t : (Type→ Type)→ Type→ Type ℘t(m)(A) := m(℘(A)) Π℘ t : (Type→ Type)→ Type→ Type Π℘ t (Σ)(A) := Σ(℘(A)) return℘ t : ∀A.A→ ℘t(m)(A) return℘ t (x) := returnm({x}) bind℘ t : ∀AB.℘t(m)(A)→ (A→ ℘t(m)(B))→ ℘t(m)(B) bind℘ t (X)(f) := do {x1, . . . , xn} ←m X f(x1) unionsqm · · · unionsqm f(xn) get℘ t : ℘t(m)(s) get℘ t := s←m getm ; returnm({s}) put℘ t : s→ ℘t(m)(unit) put℘ t (s) := u←m putm(x) ; returnm({u}) mzero℘ t : ∀A.℘t(m)(A) mzero℘ t := ⊥m ℘t : ∀A.℘t(m)(A)x℘t(m)(A)→ ℘t(m)(A) X1 ℘ t X2 := X1 unionsqm X2 α℘ t : ∀AB.(Π℘t(Σm)(A)→ Π℘t(Σm)(B))→ A→ ℘t(m)(B) α℘ t (f)(x) := αm(f)({x}) γ℘ t : ∀AB.(A→ ℘t(m)(B))→ Π℘t(Σm)(A)→ Π℘t(Σm)(B) γ℘ t (f) := γm(λ{x1, . . . , xn}.f(x1) unionsqm · · · unionsqm f(xn)) Figure 5.11: Nondeterminism Galois Transformer 176 F t[s] : (Type→ Type)→ Type→ Type F t[s](m)(A) := s→ m([A 7→ s]) ΠF t [s] : (Type→ Type)→ Type→ Type ΠF t [s](Σ)(A) := Σ([A 7→ s]) returnF t : ∀A.A→ F t[s](m)(A) returnF t (x)(s) := returnm({x 7→ s}) bindF t : ∀AB.F t[s](m)(A)→ (A→ F t[s](m)(B))→ F t[s](m)(B) bindF t (X)(f)(s) := do {x1 7→ s1, . . . , xn 7→ sn} ←m X(s) f(x1)(s1) unionsqm · · · unionsqm f(xn)(sn) getF t : F t[s](m)(s) getF t (s) := returnm({s 7→ s}) putF t : s→ F t[s](m)(unit) putF t (s′)(s) := returnm({• 7→ s′}) mzeroF t : ∀A.F t[s](m)(A) mzeroF t (s) := ⊥m F t : ∀A.F t[s](m)(A)xF t[s](m)(A)→ F t[s](m)(A) (X1 F t X2)(s) := X1(s) unionsqm X2(s) αF t : ∀AB.(ΠF t [s](Σm)(A)→ ΠF t [s](Σm)(B))→ A→ F t[s](m)(B) αF t (f)(x)(s) := αm(f)({x 7→ s}) γF t : ∀AB.(A→ F t[s](m)(B))→ ΠF t [s](Σm)(A)→ ΠF t [s](Σm)(B) γF t (f) := γm(λ{x1 7→ s1, . . . , xn 7→ sn}.f(x1)(s1) unionsqm · · · unionsqm f(xn)(sn)) Figure 5.12: Flow Sensitivity Galois Transformer 177 get℘ t and put℘ t require that m be a monad. Like the nondeterminism Galois trans- former, α℘ t and γ℘ t require that m maps to Σm via Galois connection. Lemma 4. [F t laws] bindF t and returnF t satisfy monad laws, getF t and putF t satisfy state monad laws, and mzeroF t and F t satisfy nondeterminism monad laws. See our proofs in A. Monad and nondeterminism laws are are analogous to those for nondeterminism, and also rely on the join-semilattice functorailty of m. State monad laws are proved by calculation. 5.8.4 Galois Transformers The capstone of our framework is the fact that monad transformers St[s], ℘t and F t[s] are also Galois transformers. Definition 1. A monad transformer T is a Galois transformer with transition system Π if: 1. T transports a Galois connection between monads m1 and m2 into a Galois connection between T (m1) and T (m2): A→ m2(B) A→ T (m2)(B) A→ m1(B) A→ T (m1)(B) γm T [m2] αm T [m1] T [γm]T [αm] T [m] must be monotonic, and T must commute with Galois connections, that is for all f : A→ m1(B): T [m2](α m(f)) = T [αm](T [m1](f)) 178 2. Π transports Galois connections between induced transition systems Σ1 and Σ2 into Galois connections between Π(Σ1) and Π(Σ2): Σ2(A)→ Σ2(B) Π(Σ2)(A)→ Π(Σ2)(B) Σ1(A)→ Σ1(B) Π(Σ1)(A)→ Π(Σ1)(B) γΣ Π[Σ2] αΣ Π[Σ1] Π[γΣ]Π[αΣ] Π[Σ] must be monotonic, and Π must commute with Galois connections, that is for all f : Σ1(A)→ Σ1(B): Π[Σ2](α Σ(f)) = Π[αΣ](Π[Σ1](f)) 3. T and Π transport transition system mappings between m and Σ into transition system mappings between T (m) and Π(Σ): A→ m(B) A→ T (m)(B) Σ(A)→ Σ(B) Π(Σ)(A)→ Π(Σ)(B) γΣ↔m T [m] αΣ↔m Π[Σ] T [γΣ↔m]T [αΣ↔m] T [γΣ↔m] must commute asymmetrically (in the partial order) with T and Π, that is for all functions f : A→ m(B): Π[Σ](γΣ↔m(f)) v T [γΣ↔m](T [m](f)) Lemma 5 (Galois Transformer Properties). St[s], ℘t and F t[s] are Galois trans- formers. 179 A→ m2(B) A→ T (m2)(B) A→ m1(B) A→ T (m1)(B) Σ2(A)→ Σ2(B) Π(Σ2)(A)→ Π(Σ2)(B) Σ1(A)→ Σ1(B) Π(Σ1)(A)→ Π(Σ1)(B) T [m2] T [m1] Π[Σ2] Π[Σ1] Figure 5.13: Galois Transformer Commuting Cube of Abstractions Definitions for αΣ↔γ and γΣ↔γ from property (3) are shown in figures 5.10, 5.11 and 5.12. Definitions of other Galois connections and commutativity proofs are given in the appendix. These three properties of Galois transformers snap together to form a three- dimensional diagram, shown in Figure 5.13 which relates abstractions between monads m1 and m2 and their transition systems Σ1 and Σ2 to their actions under T and Π. The left-hand side of the cube is a commuting square of abstractions between m1, m2, Σ1 and Σ2. The right-hand side of the cube is constructed from the composition of properties (1) through (3) as the front, top, back, and bottom faces of the cube, and is a commuting square of abstractions between T (m1), T (m2), Π(Σ1) and Π(Σ2). The whole cube commutes, by combining the commuting properties of the left face and the commuting properties of (1) through (3). Theorem 5. If T is a Galois transformer with transition system Π, given a commut- ing square of abstractions between monads m1 and m2 and their transition systems Σ1 and Σ2, T and Π construct a commuting square of abstractions between monads 180 T (m1) and T (m2) and their transition systems Π(Σ1) and Π(Σ2). The proof is the composition of Galois transformer properties, as shown in the Figure 5.13. The consequence of this theorem is that any two compositions of Galois transformers T1 ◦ · · · ◦ Tn and U1 ◦ · · · ◦ Un where Ui is an abstraction of Ti will yield a commuting square of abstractions between monads (T1 ◦ · · · ◦ Tn)(ID) and (U1 ◦ · · · ◦ Un)(ID) and their induced transition systems (ΠT1 ◦ · · · ◦ ΠTn)(ID) and (ΠU1 ◦· · ·◦ΠUn)(ID). This is the first step in proving the resulting abstract interpreter correct; we need to establish a commuting square of abstractions between a concrete monad, an abstract monad, and their induced concrete and abstract transition systems. 5.8.5 End-to-End Correctness with Galois Transformers In the setting of abstract interpretation, we instantiate the Galois transformer framework described above with two compositions of monad transformers yielding a commuting square of abstractions between the concrete monad M \, the abstract monad M ], and concrete and abstract transition systems Σ\ and Σ]: Exp→M \(Exp) Exp→M ](Exp) Σ\(Exp)→ Σ\(Exp) Σ](Exp)→ Σ](Exp) αM \ γM \ αΣ \ γΣ \ γΣ \↔M\αΣ \↔M\ γΣ ]↔M]αΣ ]↔M] This diagram shows how to relate monadic interpreters to transition systems (the 181 vertical axis of the diagram), and concrete semantics to abstract semantics (the horizontal axis of the diagram). The top half is where we write the monadic interpreter, and the bottom half is where we execute the analysis as the least-fixed point of a transition system. We use this commuting square to systematically relate a recovered collecting semantics with the induced abstract transition system in the following theorem: Theorem 6. Given a commuting square of abstraction between M \, M ], Σ\ and Σ], and a generic monadic interpreter stepm, if collect = γΣ \↔M\(stepm[M \]) recovers the collecting semantics, then analysis = γΣ ]↔M](stepm[M ]]) is a sound abstraction of the collecting semantics. Proof. Given that stepm is monotonic in the monad parameter m, instantiating it with M \ and M ] will result in: αM \ (stepm[M \]) v stepm[M ]] Transporting through γΣ ]↔M] , which is monotonic by virtue of forming a Galois connection with αΣ ]↔M] , we have: (1) γΣ ]↔M](αM \ (stepm[M \])) v γΣ]↔M](stepm[M ]]) = analysis Next, we abstract the recovered collecting semantics to form its best specification for abstraction: (2) αΣ ] (collect) = αΣ ] (γΣ \↔M\(stepm[M \])) Finally, we exploit the commutativity of the square of abstractions between M \, M ], 182 Σ\ and Σ] to relate the recovered collecting semantics with the abstract monadic semantics: (3) αΣ ] (γΣ \↔M\(stepm[M \])) v γΣ]↔M](αM\(stepm[M \])) The transitive combination of (1), (2) and (3) establishes the soundness of the derived abstract execution system w.r.t. the recovered collecting semantics: αΣ ] (collect) v analysis. This theorem proves Proposition 1 in Section 5.6.3 after instantiating the example to the Galois transformer framework. 5.8.6 Applying the Framework to Our Semantics Our setting is the ground-truth semantics gc from Section 5.2 and the generic interpreter stepm from Section 5.5. To recover the concrete collecting semantics, we instantiate stepm to the concrete parameters for the domain and time from Section 5.6.1, and synthesize the monad as a combination of state and nondeterminism Galois transformers: M \ := (St[Ψ\] ◦ St[Store\] ◦ ℘t)(ID) To recover a path-sensitive abstract interpreter we instantiate stepm to the abstract parameters for the domain and time from Section 5.6.2, and synthesize the monad as a combination of state and nondeterminism Galois transformers: M ] := (St[Ψ]] ◦ St[Store]] ◦ ℘t)(ID) 183 which abstract M \ piecewise. Both the implementation and correctness of the induced abstract transition system are constructed for free by theorems 5 and 6. To recover a flow-sensitive abstract interpreter we synthesize the monad as a combination of state and flow-sensitive Galois transformers: M ]fs := (St[Ψ]] ◦ F t[Store]])(ID) which abstracts M ] piecewise. Finally, to recover a flow-insensitive abstract interpreter we synthesize the monad as a permuted combination of state and nondeterminism Galois transformers: M ]ps := (St[Ψ]] ◦ ℘t ◦ St[Store]])(ID) which abstracts M ]ps piecewise. 5.8.7 Applying the Framework to Another Semantics Our Galois transformers framework is semantics independent, and the proofs in Section 5.8.4 need not be reproved for another semantic setting. To use our framework and establish an end-to-end correctness theorem, the user must: • Design a generic monadic interpreter for their semantics using an interface of monadic effects • Prove their interpreter monotonic w.r.t. parameters • Prove that the induced concrete transition system recovers the concrete col- lecting semantics of interest. 184 The user then enjoys the following for free: • A combination of state, nondeterminism and flow-sensitive Galois transformers which supports the monadic effect interface unique to the semantics. • The ability to rearrange monad transformers to recover variations in path and flow sensitivities. • An induced, executable abstract interpreter for each stack of monad transform- ers. • A proof that each induced abstract interpreter is a sound abstraction of the collecting semantics, as a result of theorems 5 and 6. 5.9 Implementation We have implemented our framework in Haskell and applied it to compute analyses for λIF. Our implementation provides path sensitivity, flow sensitivity, and flow insensitivity as a semantics-independent monad library. The code shares a striking resemblance with the math. Our implementation is suitable for prototyping and exploring the design space of static analyzers. Our analyzer supports exponentially more compositions of analysis features than any current analyzer. For example, our implementation is the first which can combine arbitrary choices in call-site, object, path and flow sensitivities. Furthermore, the user can choose different path and flow sensitivities independently for each component of the state space. 185 Our implementation maam supports command-line flags for garbage collection, mCFA, call-site sensitivity, object sensitivity, and path and flow sensitivity. ./maam prog.lam --gc --mcfa --kcfa=1 --ocfa=2 \ --data-store=flow-sen --stack-store=path-sen Each flag is implemented independently of each other applied to a single parameterized monadic interpreter. Furthermore, using Galois transformers allows us to prove each combination correct in one fell swoop. A developer wishing to use our library to develop analyzers for their language of choice inherits as much of the analysis infrastructure as possible. We provide call-site, object, path and flow sensitivities as language-independent libraries. To support analysis for a new language a developer need only implement: • A monadic semantics for their language, using state and nondeterminism effects. • The abstract value domain, and optionally the concrete value domain if they wish to recover concrete execution. • Intentional optimizations for their semantics like garbage collection and mcfa. The developer then receives the following for free through our analysis library: • A family of monads which implement their effect interface and give different path and flow sensitivities. • Mechanisms for call-site and object sensitivities. • An execution engine for each monad to drive the analysis. 186 Not only is a developer able to reuse our implementation of call-site, object, path and flow sensitivities, they need not understand the execution machinery or soundness proofs for them either. They need only verify that their monadic semantics is monotonic w.r.t. the analysis parameters, and that their abstract value domain forms a Galois connection. The execution and correctness of the final analyzer is constructed automatically given these two properties. Our implementation is publicly available and can be installed as a cabal package: cabal install maam. 5.10 Related Work Overview Program analysis comes in many forms such as points-to [Andersen, 1994], flow [Jones, 1981], or shape analysis [Chase et al., 1990], and the literature is vast. (See Hind [2001], Midtgaard [2012] for surveys.) Much of the research has focused on developing families or frameworks of analyses that endow the abstraction with a number of knobs, levers, and dials to tune precision and compute efficiently (some examples include Milanova et al. [2005], Nielson and Nielson [1997], Shivers [1991], Van Horn and Might [2010]; there are many more). These parameters come in various forms with overloaded meanings such as object [Milanova et al., 2005, Smaragdakis et al., 2011], context [Sharir and Pnueli, 1981, Shivers, 1991], path [Das et al., 2002], and heap [Van Horn and Might, 2010] sensitivities, or some combination thereof [Kastrinis and Smaragdakis, 2013]. These various forms can all be cast in the theory of abstraction interpretation 187 of Cousot and Cousot [1977, 1979] and understood as computable approximations of an underlying concrete interpreter. Our work demonstrates that if this underlying concrete interpreter is written in monadic style, monad transformers are a useful way to organize and compose these various kinds of program abstractions in a modular and language-independent way. This work is inspired by the trifecta combination of Cousot, Cousot and Cousot, Cousot and Cousot’s theory of abstract interpretation based on Galois connections [1999, 1977, 1979], Moggi’s original monad transformers [1989] which were later popularized in Liang et al.’s Monad Transformers and Modular Inter- preters [1995], and Sergey et al.’s Monadic Abstract Interpreters [Sergey et al., 2013]. Liang et al. [1995] first demonstrated how monad transformers could be used to define building blocks for constructing (concrete) interpreters. Their interpreter monad InterpM bears a strong resemblance to ours. We show this “building blocks” approach to interpreter construction also extends to abstract interpreter construction using Galois transformers. Moreover, we show that these monad transformers can be proved sound via a Galois connection to their concrete counterparts, ensuring the soundness of any stack built from sound blocks of Galois transformers. Soundness proofs of various forms of analysis are notoriously brittle with respect to language and analysis features. A reusable framework of Galois transformers offers a potential way forward for a modular metatheory of program analysis. Cousot [1999] develops a “calculational approach” to analysis design whereby analyses are not designed and then verified post facto, but rather derived by positing 188 an abstraction and calculating it from the concrete interpreter using Galois con- nections. These calculations are done by hand. Our approach offers the ability to automate the calculation process for a limited set of abstractions for small-step state machines, where the abstractions are correct-by-construction through the composition of monad transformers. We build directly on the work of Abstracting Abstract Machines (AAM) by Smaragdakis et al. [2011], Van Horn and Might [2010] in our parameterization of abstract time to achieve call-site and object sensitivity. We follow the AAM philosophy of instrumenting a concrete semantics first and performing a systematic abstraction second. This greatly simplifies the Galois connection arguments during systematic abstraction, at the cost of proving the correctness of the instrumented semantics. Monadic Abstract Interpreters Sergey et al. [2013] first introduced the con- cept of writing abstract interpreters in monadic style in Monadic Abstract Interpreters (MAI), where variations in analysis are also recovered through monads. In MAI, the framework’s interface is based on denotation functions for every syntactic form of the language. The denotation functions in MAI are language- specific and specialized to their example language. MAI uses a single monad stack fixed to the denotation function interface: state on top of list. New analyses are achieved through multiple denotation functions into this single monad. Analyses in MAI are all fixed to be path-sensitive, and the methodology for incorporating other path or flow properties is to surgically instrument the execution of the analysis with 189 a custom Galois connection. Lastly, the framework provides no reasoning principles or proofs of soundness for the resulting analysis. A user of MAI must inline the definitions of each analysis and prove each implementation correct from scratch. Our framework is instead based on state and nondeterminism monadic effects. This interface comes equipped with laws, allowing one to verify the correctness of a monadic interpreter independent of a particular monad. State and nondeterminism monadic effects capture arbitrary small-step relational semantics, and are language independent. Because we place the monadic interpreter behind an interface of effects with laws, we are able to introduce language-independent monads which capture flow-sensitivity and flow-insensitivity, and we show how to compose these features with other analysis design choices. The monadic effect interface also allows us to separate the monad from the abstract domain. Finally, our framework is compositional through the use of monad transformers, and constructs execution engines and end-to-end soundness proofs for free. Widening for Control-Flow Hardekopf et al. [2014] also introduce a unifying account of control flow properties in Widening for Control-Flow (WCF), which accounts for path, flow and call-site sensitivities. WCF achieves this through an instrumentation of the abstract machine’s state space which is allowed to track arbitrary contextual information, up to the path-history of the entire execution. WCF also develops a modular proof framework, proving the bulk of soundness proofs for each instantiation of the instrumentation at once. Our work achieves similar goals, although isolating path and flow sensitivity is 190 not our primary objective. WCF is based on a language-dependent instrumentation of the semantics, whereas we achieve variations in path and flow sensitivity through language-independent monads. Particular strengths of WCF are the wide range of choices for control-flow sensitivity which are shown to be implementable within the design, and the modular proof framework. For example, WCF is able to account for call-site sensitivity in their design; we account for call-site sensitivity through a different mechanism. Particular strengths of our work is the understanding of path and flow sensitivity not through instrumentation but through semantics-independent control properties of the interpreter, and also a modular proof framework, although modular in a different sense from WCF. We also show how to compose different path and flow sensitivity choices for independent components of the state space, like a flow-sensitive data-store and path-sensitive stack-store, for example. 5.11 Conclusions We have shown that Galois transformers, monad transformers that transport Galois connections and mappings to an executable transition system, are effective, language- independent building blocks for constructing program analyzers, and form the basis of a modular, reusable and composable metatheory for program analysis. In the end, we hope language independent characterizations of analysis ingredi- ents will both facilitate the systematic construction of program analyses and bridge the gap between various communities which often work in isolation. 191 Chapter 6: Abstracting Definitional Interpreters 6.1 Introduction An abstract interpreter is intended to soundly and effectively compute an over- approximation to its concrete counterpart. For higher-order languages, these concrete interpreters tend to be formulated as state-machines (e.g., Jagannathan and Weeks [1995], Jagannathan et al. [1998], Midtgaard and Jensen [2008, 2009], Might and Shivers [2006b], Might and Van Horn [2011], Sergey et al. [2013], Wright and Jagannathan [1998]). There are several reasons for this choice: they operate with simple transfer functions defined over similarly simple data structures, they make explicit all aspects of the state of a computation, and computing fixed-points in the set of reachable states is straightforward. The essence of the state-machine based approach was distilled by Van Horn and Might in their “abstracting abstract machines” (AAM) technique, which provides a systematic method for constructing abstract interpreters from standard abstract machines like the CEK- or Krivine- machines [Van Horn and Might, 2010]. Language designers who would like to build abstract interpreters and program analysis tools for their language can now, in principle at least, first build a state-machine interpreter and then turn the crank to construct the approximating abstract counterpart. 192 A natural pair of questions that arise from this past work is to wonder: 1. Can a systematic abstraction technique similar to AAM be carried out for interpreters written, not as state-machines, but instead as high-level definitional interpreters, i.e. recursive, compositional evaluators? 2. is such a perspective fruitful? In this chapter, we seek to answer both questions in the affirmative. For the first question, we show the AAM recipe can be applied to definitional interpreters in a straightforward adaptation of the original method. The primary technical challenge in this new setting is handling interpreter fixed-points in a way that is both sound and always terminates—a naive abstraction of fixed-points will be sound but isn’t always terminating, and a naive use of caching for fixed-points will guarantee termination but is inherently unsound. We address this technical challenge with a straightforward caching fixed-point-finding algorithm which is both sound and guaranteed to terminate when abstracting arbitrary definitional interpreters. For the second question, we claim that the abstract definitional interpreter perspective is fruitful in two regards. The first is unsurprising: high-level abstract interpreters offer the usual beneficial properties of their concrete counterparts in terms of being re-usable and extensible. In particular, we show that abstract interpreters can be structured with monad transformers to good effect. The second regard is more surprising, and we consider its observation to be the main contribution of this chapter. Definitional interpreters, in contrast to abstract machines, can leave aspects of 193 computation implicit, relying on the semantics of the defining-language to define the semantics of the defined -language, an observation made by Reynolds in his landmark paper, Definitional Interpreters for Higher-order Programming Languages [Reynolds, 1972]. For example, Reynolds showed it is possible to write a definitional interpreter such that it defines a call-by-value language when the metalanguage is call-by-value, and defines a call-by-name language when the metalanguage is call-by-name. Inspired by Reynolds, we show that definitional abstract interpreters can likewise inherit properties of the metalanguage. In particular we construct an abstract definitional interpreter where there is no explicit representation of continuations or a call stack. Instead the interpreter is written in a straightforward recursive style, and the call stack is implicitly handled by the metalangauge. What emerges from this construction is a total abstract evaluation function that soundly approximates all possible concrete executions of a given program. But remarkably, since the abstract evaluator relies on the metalanguage to manage the call stack implicitly, it is easy to observe that it introduces no approximation in the matching of calls and returns, and therefore implements a “pushdown” analysis [Earl et al., 2010, Vardoulakis and Shivers, 2011], all without the need for any explicit machinery to do so. 6.1.1 Outline In the remainder of this chapter, we present an adaptation of the AAM method to the setting of recursively-defined, compositional evaluation functions, a.k.a. definitional interpreters. We first briefly review the basic ingredients in the AAM recipe (§ 6.2) 194 and then define our definitional interpreter (§ 6.3). The interpreter is largely standard, but is written in a monadic and extensible style, so as to be re-usable for various forms of semantics we examine. The AAM technique applies in a basically straightforward way by store-allocating bindings and soundly finitizing the heap. But when naively run, the interpreter will not always terminate. To solve this problem we introduce a caching strategy and a simple fixed-point computation to ensure the interpreter terminates (§ 6.4). It is at this point that we observe the interpreter we have built enjoys the “pushdown” property a` la Reynolds—it is inherited from the defining language of our interpreter and requires no explicit mechanism (§ 6.5). Having established the main results, we then explore some variations in brief vignettes that showcase the flexibility of our definitional abstract interpreter approach. First we consider the widely used technique of so-called “store-widening,” which trades precision for efficiency by modeling the abstract store globally instead of locally (§ 6.6). Thanks to our monadic formulation of the interpreter, this is achieved by a simple re-ordering of the monad transformer stack. We also explore some alternative abstractions, showing that due to the extensible construction, it’s easy to experiment with alternative components for the abstract interpreter. In particular, we define an alternative interpretation of the primitive operations that remains completely precise until forced by joins in the store to introduce approximation (§ 6.7). As another variation, we explore computing a form of symbolic execution as yet another instance of our interpreter, as well as how to incorporate so-called “abstract garbage collection,” a well-known technique for improving the precision of abstract interpretation by clearing out unreachable store locations, thus avoiding 195 future joins which cause imprecision (§ 6.8). This last variation is significant because it demonstrates that even though we have no explicit representation of the stack, it is possible to compute analyses that typically require such explicit representations in order to calculate root sets for garbage collection. Next, we prove the approach sound w.r.t. a derived big-step collecting and abstract semantics (§ 6.10), where the key insight in the formalism is to model not only standard big-step evaluation relations, but also big-step reachability relations. Finally, we place our work in the context of the prior literature on higher-order abstract interpretation (§ 6.11) and draw some conclusions (§ 6.12). To convey the ideas of this chapter as concretely as possible, we present code implementing our definitional abstract interpreter and all its variations. As a metalanguage, we use an applicative subset of Racket [Flatt and PLT, 2010], a dialect of Scheme. This choice is largely immaterial: any functional language would do. However, to aide extensibility, we use Racket’s unit system [Flatt and Felleisen, 1998] to write program components that can be linked together. 6.2 From Machines to Compositional Evaluators In recent years, there has been considerable effort in the systematic construction of abstract interpreters for higher-order languages using abstract machines—first- order transition systems—as a semantic basis. The so-called Abstracting Abstract Machines (AAM) approach to abstract interpretation [Van Horn and Might, 2010] is a recipe for transforming a machine semantics into an easily abstractable form. The 196 transformation includes the following ingredients: • Allocating continuations in the store; • Allocating variable bindings in the store; • Using a store that maps addresses to sets of values; • Interpreting store updates as a join; and • Interpreting store dereference as a non-deterministic choice. These transformations are semantics-preserving due to the original and derived machines operating in a lock-step correspondence. After transforming the semantics in this way, a computable abstract interpreter is achieved by: • Bounding store allocation to a finite set of addresses; and • Widening base values to some abstract domain. After performing these transformations, the soundness and computability of the resulting abstract interpreter are then self-evident and easily proved. The AAM approach has been applied to a wide variety of languages and applications, and given the success of the approach it’s natural to wonder what is essential about its use of low-level machines. It is not at all clear whether a similar approach is possible with a higher-level formulation of the semantics, such as a compositional evaluation function defined recursively over the syntax of expressions. This chapter shows that the essence of the AAM approach can be applied to a high-level semantic basis. We show that compositional evaluators written in monadic 197 style can express similar abstractions to that of AAM, and like AAM, the design remains systematic. Moreover, we show that the high-level semantics offers a number of benefits not available to the machine model. There is a rich body of work concerning tools and techniques for extensible interpreters [Jaskelioff, 2009, Kiselyov, 2010, Liang et al., 1995], all of which applies to high-level semantics. By putting abstract interpretation for higher-order languages on a high-level semantic basis, we can bring these results to bear on the construction of extensible abstract interpreters. 6.3 A Definitional Interpreter We begin by constructing a definitional interpreter for a small but representative higher-order, functional language. The abstract syntax of the language is defined in Figure 6.1; it includes variables, numbers, binary operations on numbers, conditionals, letrec expressions, functions and applications. The interpreter for the language is defined in Figure 6.2. At first glance, it has many conventional aspects: • It is compositionally defined by structural recursion on the syntax of expressions. • It represents function values as a closure data structure which pairs the lambda term with the evaluation environment. • It is structured monadically and uses monad operations to interact with the environment and store. 198 x ∈ var [variable names] e ∈ exp ::= (vbl x) [variable] | (num n) [number] | (if0 e e e) [conditional] | (op2 b e e) [binary op] | (app e e) [application] | (lam x e) [lambda] | (rec x e e) [letrec] b ∈ binop := {+,−, . . .} [binary prim] Figure 6.1: Programming Language Syntax • It relies on a helper function δ to interpret primitive operations. There are a few superficial aspects that deserve a quick note: environments ρ are finite maps and the syntax (ρ x) denotes ρ(x) while (ρ x a) denotes ρ[x 7→ a]. The do-notation is just shorthand for bind, as usual: (do x← e . r) ≡ (bind e (λ (x) (do . r))) (do e . r) ≡ (bind e (λ ( ) (do . r))) (do x := e . r) ≡ (let ((x e)) (do . r)) (do b) ≡ b Finally, there are two unconventional aspects worth noting. First, the interpreter is written in an open recursive style; the evaluator does not call itself recursively, instead it takes as an argument a function ev—shadowing the name of the function ev being defined—and ev (the argument) is called instead of self-recursion. This is a standard encoding for recursive functions in a setting 199 (define ((ev ev) e) (match e [(num n) (return n)] [(vbl x) (do ρ← ask-env (find (ρ x)))] [(if0 e0 e1 e2) (do v ← (ev e0) z?← (zero? v) (ev (if z? e1 e2)))] [(op2 o e0 e1) (do v0 ← (ev e0) v1 ← (ev e1) (δ o v0 v1))] [(rec f l e) (do ρ← ask-env a← (alloc f) ρ′ := (ρ f a) (ext a (cons l ρ′)) (local-env ρ′ (ev e)))] [(lam x e0) (do ρ← ask-env (return (cons (lam x e0) ρ)))] [(app e0 e1) (do (cons (lam x e2) ρ)← (ev e0) v1 ← (ev e1) a← (alloc x) (ext a v1) (local-env (ρ x a) (ev e2)))])) ev@ Figure 6.2: The Extensible Definitional Interpreter 200 without recursive binding. It is up to an external function, such as the Y-combinator, to close the recursive loop. This open recursive form is crucial because it allows intercepting recursive calls to perform “deep” instrumentation of the interpreter. Second, the code is clearly incomplete. There are a number of free variables, typeset as italics, which implement the following: • The underlying monad of the interpreter: return and bind ; • An interpretation of primitives: δ and zero? ; • Environment operations: ask-env for retrieving the environment and local-env for installing an environment; • Store operations: ext for updating the store, and find for dereferencing locations; and • An operation for allocating new store locations. Going forward, we make frequent use of definitions involving free variables, and we call such a collection of such definitions a component. We assume components can be named (in this case, we’ve named the component ev@, indicated by the box in the upper-right corner) and linked together to eliminate free variables. We use Racket units [Flatt and Felleisen, 1998] to model components in our implementation. 6.3.1 Instantiating the Interpreter Next we examine a set of components which complete the definitional interpreter, shown in Figure 6.3. The first component monad@ uses a macro define-monad which 201 (define-monad ( env︷ ︸︸ ︷ ReaderT ( errors︷ ︸︸ ︷ FailT ( store︷ ︸︸ ︷ StateT ID)))) monad@ (define (δ o n0 n1) (match o ['+ (return (+ n0 n1))] ['- (return (− n0 n1))] ['* (return (∗ n0 n1))] ['/ (if (= 0 n1) fail (return (/ n0 n1)))])) (define (zero? v) (return (= 0 v))) δ@ (define (find a) (do σ ← get-store (return (σ a)))) (define (ext a v) (update-store (λ (σ) (σ a v)))) store@ (define (alloc x) (do σ ← get-store (return (size σ)))) alloc@ Figure 6.3: Components for Definitional Interpreters generates a set of bindings based on a monad transformer stack. We use a failure monad to model divide-by-zero errors, a state monad to model the store, and a reader monad to model the environment. The define-monad form generates bindings for return, bind, ask-env, local-env, get-store and update-store; their definitions are standard [Liang et al., 1995]. We also define mrun for running monadic computations, starting with the 202 empty environment and store ∅: (define (mrun m) (run-StateT ∅ (run-ReaderT ∅ m))) While the define-monad form is hiding some details, this component could have equivalently been written out explicitly. For example, return and bind can be defined as: (define (((return a) r) s) (cons a s)) (define (((bind ma f) r) s) (match ((ma r) s) [(cons a s′) (((f a) r) s′)] ['failure 'failure])) So far our use of monad transformers is as a mere convenience, however the monad abstraction will become essential for easily deriving new analyses later on. The δ@ component defines the interpretation of primitives, which is given in terms of the underlying monad. The alloc@ component provides a definition of alloc, which fetches the store and uses its size to return a fresh address, assuming the invariant (∈ a σ)⇔ a < (size σ). The alloc function takes a single argument, which is the name of the variable whose binding is being allocated. For the time being, it is ignored, but will become relevant when abstracting closures (§ 6.3.4). The store@ component defines find and ext for finding and extending values in the store. The only remaining pieces of the puzzle are a fixed-point combinator and the 203 main entry-point for the interpreter, which are straightforward to define: (define ((fix f) x) ((f (fix f)) x)) (define (eval e) (mrun ((fix ev) e))) Using Racket’s languages-as-libraries features [Tobin-Hochstadt et al., 2011], we construct REPLs for interacting with this interpreter. Here are a few evaluation examples in a succinct concrete syntax: > (λ (x) x) ;; Closure over the empty '(((λ (x) x) . ()) . ()) ;; environment and store. > ((λ (x) (λ (y) x)) 4) ;; Closure over a non-empty '(((λ (y) x) . ((x . 0))) . ((0 . 4))) ;; environment and store. > (∗ (+ 3 4) 9) ;; Primitive operations work '(63 . ()) ;; as expected. > (/ 5 (− 3 3)) ;; Divide-by-zero errors '(failure . ()) ;; result in failures. Because our monad stack places FailT above StateT, the answer includes the (empty) store at the point of the error. Had we changed monad@ to use: (ReaderT (StateT (FailT ID)))) then failures would not include the store: > (/ 5 (− 3 3)) 'failure At this point we’ve defined a simple definitional interpreter, although the exten- sible components involved—monadic operations and open recursion—will allow us to instantiate the same interpreter to achieve a wide range of useful abstract interpretations. 204 (define-monad ( env︷ ︸︸ ︷ ReaderT ( errors︷ ︸︸ ︷ FailT ( store︷ ︸︸ ︷ StateT ( traces︷ ︸︸ ︷ WriterT List ID))))) trace-monad@ (define (((ev-tell ev0) ev) e) (do ρ← ask-env σ ← get-store (tell (list e ρ σ)) ((ev0 ev) e))) ev-tell@ Figure 6.4: Trace Collecting Semantics 6.3.2 Collecting Variations The formal development of abstract interpretation often starts from a so-called “non-standard collecting semantics.” A common form of collecting semantics is a trace semantics, which collects streams of states the interpreter reaches. Figure 6.4 shows the monad stack for a tracing interpreter and a “mix-in” for the evaluator. The monad stack adds WriterT List, which provides a new operation tell for writing lists of items to the stream of reached states. The ev-tell function is a wrapper around an underlying ev0 unfixed evaluator, and interposes itself between each recursive call by tell ing the current state of the evaluator: the current expression, environment and store. The top-level evaluation function is then: (define (eval e) (mrun ((fix (ev-tell ev)) e))) Now when an expression is evaluated, we get an answer and a list of all states 205 seen by the evaluator, in the order in which they were seen. For example (not showing ρ or σ in results): > (∗ (+ 3 4) 9) '((63 . ()) (* (+ 3 4) 9) (+ 3 4) 3 4 9) > ((λ (x) (λ (y) x)) 4) '((((λ (y) x) . ((x . 0))) . ((0 . 4))) (((λ (x) (λ (y) x)) 4) () ()) ((λ (x) (λ (y) x)) () ()) (4 () ()) ((λ (y) x) ((x . 0)) ((0 . 4)))) Were we to swap List with Set in the monad stack, we would obtain a reachable state semantics, another common form of collecting semantics, that loses the order and repetition of states. As another collecting semantics variant, we show how to collect the dead code in a program. Here we use a monad stack that has an additional state component (with operations named put-dead and get-dead) which stores the set of dead expressions. Initially this will contain all subexpressions of the program. As the interpreter evaluates expressions it will remove them from the dead set. Figure 6.5 defines the monad stack for the dead code collecting semantics and the ev-dead@ component, another mix-in for an ev0 evaluator to remove the given subexpression before recurring. Since computing the dead code requires an outer wrapper that sets the initial set of dead code to be all of the subexpressions in the program, we define eval-dead@ which consumes a closed evaluator, i.e. something of the form (fix ev). Putting these pieces together, the dead code collecting semantics 206 (define-monad ( env︷ ︸︸ ︷ ReaderT ( store︷ ︸︸ ︷ StateT ( dead︷ ︸︸ ︷ StateT ( errors︷ ︸︸ ︷ FailT ID))))) dead-monad@ (define (((ev-dead ev0) ev) e) (do θ ← get-dead (put-dead (set-remove θ e)) ((ev0 ev) e))) ev-dead@ (define ((eval-dead eval) e0) (do (put-dead (subexps e0)) (eval e0))) eval-dead@ Figure 6.5: Dead Code Collecting Semantics is defined: (define (eval e) (mrun ((eval-dead (fix (ev-dead ev))) e))) Running a program with the dead code interpreter produces an answer and the set of expressions that were not evaluated during the running of a program: > (if0 0 1 2) (cons '(1 . ()) (set 2)) > (λ (x) x) (cons '(((λ (x) x) . ()) . ()) (set 'x)) > (if0 (/ 1 0) 2 3) (cons '(failure . ()) (set 3 2)) Our setup makes it easy not only to express the concrete interpreter, but also 207 (define-monad ( env︷ ︸︸ ︷ ReaderT ( errors︷ ︸︸ ︷ FailT ( store︷ ︸︸ ︷ StateT ( mplus︷ ︸︸ ︷ NondetT ID))))) monad^@ (define (δ o n0 n1) (match* (o n0 n1) [('+ ) (return 'N)] [('/ (? num?)) (if (= 0 n1) fail (return 'N))] [('/ 'N) (mplus fail (return 'N))] . . .)) (define (zero? v) (match v ['N (mplus (return #t) (return #f))] [ (return (= 0 v))])) δ^@ Figure 6.6: Abstracting Primitive Operations these useful forms of collecting semantics. 6.3.3 Abstracting Base Values Our interpreter must become decidable before it can be considered an analysis, and the first step towards decidability is to abstract the base types of the language to something finite. We do this for our number base type by introducing a new abstract number, written 'N, which represents the set of all numbers. Abstract numbers are introduced by an alternative interpretation of primitive operations, given in Figure 6.6, which simply produces 'N in all cases. Some care must be taken in the abstraction of '/. If the denominator is 208 the abstract number 'N, then it is possible the program could fail as a result of divide-by-zero, since 0 is contained in the representation of 'N. Therefore there are two possible answers when the denominator is 'N: 'N and 'failure. Both answers are returned by introducing non-determinism NondetT into the monad stack. Adding non-determinism provides the mplus operation for combining multiple answers. Non-determinism is also used in zero?, which returns both true and false on 'N. By linking together δ^@ and the monad stack with non-determinism, we obtain an evaluator that produces a set of results: > (∗ (+ 3 4) 9) '((N . ())) > (/ 5 (+ 1 2)) '((failure . ()) (N . ())) > (if0 (+ 1 0) 3 4) '((3 . ()) (4 . ())) If we link δ^@ with the tracing monad stack plus non-determinism: ( env︷ ︸︸ ︷ ReaderT ( errors︷ ︸︸ ︷ FailT ( store︷ ︸︸ ︷ StateT ( traces︷ ︸︸ ︷ WriterT List ( mplus︷ ︸︸ ︷ NondetT ID))))) we get an evaluator that produces sets of traces (again not showing ρ or σ in the results): > (if0 (+ 1 0) 3 4) (set '((3 . ()) (if0 (+ 1 0) 3 4) (+ 1 0) 0 3) '((4 . ()) (if0 (+ 1 0) 3 4) (+ 1 0) 0 4)) It is clear that the interpreter will only ever see a finite set of numbers (including 209 'N), but it’s definitely not true that the interpreter halts on all inputs. First, it’s still possible to generate an infinite number of closures. Second, there’s no way for the interpreter to detect when it sees a loop. To make a terminating abstract interpreter requires tackling both. We look next at abstracting closures. 6.3.4 Abstracting Closures Closures consist of code—a lambda term—and an environment—a finite map from variables to addresses. Since the set of lambda terms and variables is bounded by the program text, it suffices to finitize closures by finitizing the set of addresses. Following the AAM approach, we do this by modifying the allocation function to produce elements drawn from a finite set. In order to retain soundness in the semantics, we modify the store to map addresses to sets of values, model store update as a join, and model dereference as a non-deterministic choice. Any abstraction of the allocation function that produces a finite set will do, but the choice of abstraction will determine the precision of the resulting analysis. A simple choice is to allocate variables using the variable’s name as its address. This gives a monomorphic, or 0CFA-like, abstraction. Figure 6.7 shows the component alloc^@ which implements monomorphic allocation, and the component store-nd@ for implementing find and ext which interact with a store mapping to sets of values. The for/monad+ form is a convenience for combining a set of computations with mplus, and is used so find returns all of the values in the store at a given address. The ext function joins whenever an address is 210 (define (alloc x) (return x)) alloc^@ (define (find a) (do σ ← get-store (for/monad+ ([v (σ a)]) (return v)))) (define (ext a v) (update-store (λ (σ) (σ a (if (∈ a σ) (set-add (σ a) v) (set v)))))) store-nd@ Figure 6.7: Abstracting Allocation: 0CFA already allocated, otherwise it maps the address to a singleton set. By linking these components with the same monad stack from before, we obtain an interpreter that loses precision whenever variables are bound to multiple values. For example, this program binds x to both 0 and 1 and produces both answers when run: > (let f (λ (x) x) (let (f 0) (f 1)))] '((0 . ((x 1 0) (f ((λ (x) x) . ())))) (1 . ((x 1 0) (f ((λ (x) x) . ()))))) Our abstract interpreter now has a truly finite domain; the next step is to detect loops in the state-space to achieve termination. 211 6.4 Caching and Finding Fixed-points At this point, the interpreter obtained by linking together monad^@, δ^@, alloc^@ and store-nd@ components will only ever visit a finite number of configurations for a given program. A configuration (ς) consists of an expression (e), environment (ρ) and store (σ). This configuration is finite because: expressions are finite in the given program; environments are maps from variables (again, finite in the program) to addresses; the addresses are finite thanks to alloc^; the store maps addresses to sets of values; base values are abstracted to a finite set by δ^; and closures consist of an expression and environment, which are both finite. Although the interpreter will only ever see a finite set of inputs, it doesn’t know it. A simple loop will cause the interpreter to diverge: > (rec f (λ (x) (f x)) (f 0)) timeout To solve this problem, we introduce a cache ($in) as input to the algorithm, which maps from configurations (ς) to sets of value-and-store pairs (v×σ). When a config- uration is reached for the second time, rather than re-evaluating the expression and entering an infinite loop, the result is looked up from $in, which acts as an oracle. It is important that the cache is used co-inductively: it is only safe to use $in as an oracle so long as some progress has been made first. The results of evaluation are then stored in an output cache ($out), which after the end of evaluation is “more defined” than the input cache ($in), again following a co-inductive argument. The least fixed-point $+ of an evaluator which transforms 212 an oracle $in and outputs a more defined oracle $out is then a sound approximation of the program, because it over-approximates all finite unrollings of the unfixed evaluator. The co-inductive caching algorithm is shown in Figure 6.8, along with the monad transformer stack monad-cache@ which has two new components: ReaderT for the input cache $in, and StateT+ for the output cache $out. We use a StateT+ instead of WriterT monad transformer in the output cache so it can double as tracking the set of seen states. The “+” in StateT+ signifies that caches for multiple non-deterministic branches will be merged automatically, producing a set of results and a single cache, rather than a set of results paired with individual caches. In the algorithm, when a configuration ς is first encountered, we place an entry in the output cache mapping ς to ($in ς), which is the “oracle” result. Also, whenever we finish computing the result v×σ′ of evaluating a configuration ς, we place an entry in the output cache mapping ς to v×σ′. Finally, whenever we reach a configuration ς for which a mapping in the output cache exists, we use it immediately, returning each result using the for/monad+ iterator. Therefore, every “cache hit” on $out is in one of two possible states: 1) we have already seen the configuration, and the result is the oracle result, as desired; or 2) we have already computed the “improved” result (w.r.t. the oracle), and need not recompute it. To compute the least fixed-point $+ for the evaluator ev-cache we perform a standard Kleene fixed-point iteration starting from the empty map, the bottom element for the cache, as shown in Figure 6.9. The algorithm runs the caching evaluator eval on the given program e from the 213 (define-monad ( env︷ ︸︸ ︷ ReaderT ( errors︷ ︸︸ ︷ FailT ( store︷ ︸︸ ︷ StateT ( mplus︷ ︸︸ ︷ NondetT ( $in︷ ︸︸ ︷ ReaderT ( $out︷ ︸︸ ︷ StateT+ ID))))))) monad-cache@ (define (((ev-cache ev0) ev) e) (do ρ← ask-env σ ← get-store ς := (list e ρ σ) $out ← get-cache-out (if (∈ ς $out) (for/monad+ ([v×σ ($out ς)]) (do (put-store (cdr v×σ)) (return (car v×σ)))) (do $in ← ask-cache-in v×σ0 := (if (∈ ς $in) ($in ς) ∅) (put-cache-out ($out ς v×σ0)) v ← ((ev0 ev) e) σ′ ← get-store v×σ′ := (cons v σ′) (update-cache-out (λ ($out) ($out ς (set-add ($out ς) v×σ′)))) (return v))))) ev-cache@ Figure 6.8: Co-inductive Caching Algorithm 214 (define ((fix-cache eval) e) (do ρ← ask-env σ ← get-store ς := (list e ρ σ) $+ ← (mlfp (λ ($) (do (put-cache-out ∅) (put-store σ) (local-cache-in $ (eval e)) get-cache-out))) (for/monad+ ([v×σ ($+ ς)]) (do (put-store (cdr v×σ)) (return (car v×σ)))))) (define (mlfp f) (let loop ([x ∅]) (do x′ ← (f x) (if (equal? x′ x) (return x) (loop x′))))) fix-cache@ Figure 6.9: Finding Fixed-Points in the Cache 215 initial environment and store. This is done inside of mlfp, a monadic least fixed-point finder. After finding the least fixed-point, the final values and store for the initial configuration ς are extracted and returned. Termination of the least fixed-point is justified by the monotonicity of the evaluator (it always returns an “improved” oracle), and the finite domain of the cache, which maps abstract configurations to pairs of values and stores, all of which are finite. With these pieces in place we construct a complete interpreter: (define (eval e) (mrun ((fix-cache (fix (ev-cache ev))) e))) When linked with δ^ and alloc^, this abstract interpreter is sound and computable, as demonstrated on the following examples: > (rec f (λ (x) (f x)) (f 0)) '() > (rec f (λ (n) (if0 n 1 (∗ n (f (− n 1))))) (f 5)) '(N) > (rec f (λ (x) (if0 x 0 (if0 (f (− x 1)) 2 3))) (f (+ 1 0))) '(0 2 3) 216 6.4.1 Formal soundness and termination In this chapter, we have focused on the code and its intuitions rather than rigorously establishing the usual formal properties of our abstract interpreter, but this is just a matter of presentation: the interpreter is indeed proven sound and computable. We have formalized this co-inductive caching algorithm in Section 6.10, where we prove both that it always terminates, and that it computes a sound over-approximation of concrete evaluation. Here, we give a short summary of our metatheory approach. In formalising the soundness of this caching algorithm, we extend a standard big-step evaluation semantics into a big-step reachability semantics, which charac- terizes all intermediate configurations which are seen between the evaluation of a single expression and its eventual result. These two notions—evaluation which relates expressions to fully evaluated results, and reachability which characterizes intermediate configuration states—remain distinct throughout the formalism. After specifying evaluation and reachability for concrete evaluation, we develop a collecting semantics which gives a precise specification for any abstract interpreter, and an abstract semantics which partially specifies a sound, over-approximating algorithm w.r.t. the collecting semantics. The final step is to compute an oracle for the abstract evaluation relation, which maps individual configurations to abstractions of the values they evaluate to. To construct this cache, we mutually compute the least-fixed point of both the evaluation and reachability relations: based on what is evaluated, discover new things which are reachable, and based on what is reachable, discover new results 217 of evaluation. The caching algorithm developed in this section is a slightly more efficient strategy for solving the mutual fixed-point, by taking a deep exploration of the reachability relation (up-to seeing the same configuration twice) rather than applying just a single rule of inference. 6.5 Pushdown a` la Reynolds By combining the finite abstraction of base values and closures with the termination- guaranteeing cache-based fixed-point algorithm, we have obtained a terminating abstract interpreter. But what kind of abstract interpretation did we get? We have followed the basic recipe of AAM, but adapted to a compositional evaluator instead of an abstract machine. However, we did manage to skip over one of the key steps in the AAM method: we never store-allocated continuations. In fact, there are no continuations at all. A traditional abstract machine formulation of the semantics would model the object-level stack explicitly as an inductively defined data structure. Because stacks may be arbitrarily large, they must be finitized like base values and closures, and like closures, the AAM trick is to thread them through the store, which itself must become finite. But in the definitional interpreter approach, the story of this chapter, the model of the stack is implicit and simply inherited from the meta-language. But here is the remarkable thing: since the stack is inherited from the meta- language, the abstract interpreter inherits the “call-return matching” of the meta- language, which is to say there is no loss of precision of in the analysis of the 218 control stack. This is a property that usually comes at considerable effort and engineering in the formulations of higher-order flow analysis that model the stack explicitly. So-called higher-order “pushdown” analysis has been the subject of multiple publications and two dissertations [Earl, 2014, Earl et al., 2010, 2012, Gilray et al., 2016b, Johnson and Van Horn, 2014, Johnson et al., 2014, Van Horn and Might, 2012, Vardoulakis, 2012, Vardoulakis and Shivers, 2011]. Yet when formulated in the definitional interpreter style, the pushdown property requires no mechanics and is simply inherited from the meta-language. Reynolds, in his celebrated paper Definitional Interpreters for Higher-order Programming Languages [Reynolds, 1972], first observed that when the semantics of a programming language is presented as a definitional interpreter, the defined language could inherit semantic properties of the defining metalanguage. We have now shown this observation can be extended to abstract interpretation as well, namely in the important case of the pushdown property. In the remainder of this chapter, we explore a few natural extensions and variations on the basic pushdown abstract interpreter we have established up to this point. 6.6 Widening the Store In this section, we show how to recover the well-known technique of store-widening in our formulation of a definitional abstract interpreter. This example demonstrates the ease of which we can construct existing abstraction choices. 219 The abstract interpreter we’ve constructed so far uses a store-per-program-state abstraction, which is precise but prohibitively expensive. A common technique to combat this cost is to use a global “widened” store [Might, 2007a, Shivers, 1991], which over-approximates each individual store in the current set-up. This change is achieved easily in the monadic setup by re-ordering the monad stack, a technique due to Darais et al. [2015]. Whereas before we had monad-cache@ we instead swap the order of StateT for the store and NondetT : ( env︷ ︸︸ ︷ ReaderT ( errors︷ ︸︸ ︷ FailT ( mplus︷ ︸︸ ︷ NondetT ( store︷ ︸︸ ︷ StateT+ ( $in︷ ︸︸ ︷ ReaderT ( $out︷ ︸︸ ︷ StateT+ ID)))))) we get a store-widened variant of the abstract interpreter. Because StateT for the store appears underneath nondeterminism, it will be automatically widened. We write StateT+ to signify that the cell of state supports such widening. To see the difference, here is an example without store-widening: > (let x (+ 1 0) (let y (if0 x 1 2) (let z (if0 x 3 4) (if0 x y z)))) '((4 . ((x N) (y 2) (z 4))) (1 . ((x N) (y 1) (z 3))) (2 . ((x N) (y 2) (z 3))) (3 . ((x N) (y 1) (z 3))) (1 . ((x N) (y 1) (z 4))) (3 . ((x N) (y 2) (z 3))) (2 . ((x N) (y 2) (z 4))) (4 . ((x N) (y 1) (z 4)))) 220 and an example with store-widening: > (let x (+ 1 0) (let y (if0 x 1 2) (let z (if0 x 3 4) (if0 x y z)))) '((1 3 2 4) . ((x N) (y 1 2) (z 3 4))) Notice that before widening, the result is a set of value, store pairs. After widening the result is a pair of a set of values and a store. Importantly, the cache, which bounds the overall run-time of the abstract interpreter, is potentially exponential without store-widening, but collapses to polynomial after store-widening. 6.7 An Alternative Abstraction In this section, we demonstrate how easy it is to experiment with alternative abstrac- tion strategies by swapping out components. In particular we look at an alternative abstraction of primitive operations and store joins that results in an abstraction that—to the best of our knowledge—has not been explored in the literature. This example shows the potential for rapidly prototyping novel abstractions using our approach. Figure 6.10 defines two new components: precise-δ@ and store-crush@. The first is an alternative interpretation for primitive operations that is precision preserving. Unlike δ^@, it does not introduce abstraction, it merely propagates it. When two concrete numbers are added together, the result will be a concrete number, but if either number is abstract then the result is abstract. 221 (define (δ o n0 n1) (match* (o n0 n1) [('+ (? num?) (? num?)) (return (+ n0 n1))] [('+ ) (return 'N)] . . .)) (define (zero? v) (match v ['N (mplus (return #t) (return #f))] [ (return (= 0 v))])) precise-δ@ (define (find a) (do σ ← get-store (for/monad+ ([v (σ a)]) (return v)))) (define (crush v vs) (if (closure? v) (set-add vs v) (set-add (set-filter closure? vs) 'N))) (define (ext a v) (update-store (λ (σ) (if (∈ a σ) (σ a (crush v (σ a))) (σ a (set v)))))) store-crush@ Figure 6.10: An Alternative Abstraction for Precise Primitives 222 This interpretation of primitive operations clearly doesn’t impose a finite abstraction on its own, because the state space for concrete numbers is infinite. If precise-δ@ is linked with the store-nd@ implementation of the store, termination is therefore not guaranteed. The store-crush@ operations are designed to work with precise-δ@ by per- forming widening when joining multiple concrete values into the store. This abstrac- tion offers a high-level of precision; for example: > (∗ (+ 3 4) 9) ;; Constant arithmetic expressions are '(63) ;; computed with full precision. > ((λ (x) (∗ x x)) 5) ;; Even linear binding and arithmetic '(25) ;; preserves precision. > (let f (λ (x) x) (∗ (f 5) (f 5))) ;; Precision only lost when bindings ;; contact base values. '(N) This combination of precise-δ@ and store-crush@ allows termination for most programs, but still not all. In the following example, id is eventually applied to a widened argument 'N, which makes both conditional branches reachable. The function returns 0 in the base case, which is propagated to the recursive call and added to 1, which yields the concrete answer 1. This results in a cycle where the intermediate sum returns 2, 3, 4 when applied to 1, 2, 3, etc. > (rec id (λ (n) (if0 n 0 (+ 1 (id (− n 1))))) (id 3)) timeout To ensure termination for all programs, we assume all references to primitive opera- 223 tions are η-expanded, so that store-allocations also take place at primitive applications, ensuring widening at repeated bindings. In fact, all programs terminate when using precise-δ@, store-crush@ and η-expanded primitives, which means we have a achieved a computable and uniquely precise abstract interpreter. Here we see one of the strengths of the extensible, definitional approach to abstract interpreters. The combination of added precision and widening is encoded quite naturally. In contrast, it’s hard to imagine how such a combination could be formulated as, say, a constraint-based flow analysis. 6.8 Symbolic Execution and Garbage Collection In the published version of this work [Darais et al., 2017] we carry out two exam- ples which demonstrate the wide range of possibilities enabled by the Abstracting Definitional Interpreters technique. First, we work through an example which shows how to instantiate our defini- tional abstract interpreter to obtain a symbolic execution engine that performs sound program verification. In the example we describe the monad stack and metafunctions that implement a symbolic executor [King, 1976], then we show how abstractions discussed in previous sections can be applied to enforce termination, turning a traditional symbolic execution into a path-sensitive verification engine. Next, we show how to incorporate abstract garbage collection [Might and Shivers, 2006a] into our definitional abstract interpreter. The difficulty in defining abstract garbage collection for definitional interpreters is that there is no repre- 224 sentation of the execution stack to crawl for establishing a root set of reachable addresses. We show how abstract garbage collection can be achieved by extending the instantiated monad with an explicit root set of addresses, and extending the interpreter to perform what looks remarkably similar to off-the-shelf concrete garbage collection. The result of each of these exercises is the realization that although definitional interpreters defer much of the interpretation structure to the implementing meta- language, complex analysis techniques (like symbolic execution) and introspective analysis techniques (like abstract garbage collection) can still be achieved within the interpreter framework. The key challenges are to (1) achieve the desired seman- tics through an instrumented definitional interpreter, (2) model the instrumented semantics using new monadic effects as needed, and (3) finitize the state space for the instrumentation. 6.9 Try It Out All of the components discussed in this chapter have been implemented as units [Flatt and Felleisen, 1998] in Racket [Flatt and PLT, 2010]. We have also implemented a #lang language so that composing and experimenting with these interpreters is easy. Assuming Racket is installed, you can install the monadic-eval package with: raco pkg install https://github.com/plum-umd/monadic-eval.git 225 A #lang monadic-eval program starts with a list of components, which are linked together, and an expression producing an evaluator. Subsequent forms are interpreted as expressions when run. Programs can be run from the command-line or interactively in the DrRacket IDE. 6.10 Formalism In this section we formalize our approach to designing definitional abstract inter- preters. We begin with a “ground truth” big-step semantics and concludes with the fixed-point iteration strategy described in Section 6.4, which we prove sound and computable w.r.t. a synthesized abstract semantics. The design is systematic, and applies to arbitrary developments which use big-step operational semantics. We demonstrate the systematic process as applied to a subset of the language described in Figure 6.1, which we call λIF: n ∈ N x ∈ variables b ∈ binop := {plus, . . .} e ∈ exp ::= n | x | λx.e | e(e) | if0(e){e}{e} | b(e, e) τ ∈ time := N ` ∈ addr := var× time ρ ∈ env := var→ addr⊥ σ ∈ store := addr→ val⊥ v ∈ val ::= n | 〈λx.e, ρ〉 Concrete Semantics We begin with the concrete semantics of λIF as a big-step evaluation relation ρ, τ ` e, σ ⇓ v, σ′, shown in Figure 6.11. The definition is mostly 226 (Concrete Evaluation) ρ, τ ` e, σ ⇓ v, σ′ (Lit) ρ, τ ` n, σ ⇓ n, σ (Var) ρ, τ ` x, σ ⇓ σ(ρ(x)), σ (Lam) ρ, τ ` λx.e, σ ⇓ 〈λx.e, ρ〉, σ (Bin) ρ, τ ` e1, σ ⇓ v1, σ1 ρ, τ ` e2, σ1 ⇓ v2, σ2 ρ, τ ` b(e1, e2), σ ⇓ JbK(v1, v2), σ2 (App) ρ, τ ` e1, σ ⇓ v1, σ1 ρ, τ ` e2, σ1 ⇓ v2, σ2 ρ′[x 7→ `], τ ′ ` e′, σ2[` 7→ v2] ⇓ v′, σ3 ρ, τ ` e1(e2), σ ⇓ v′, σ3 〈λx.e′, ρ′〉 = v1 ` = 〈x, τ ′〉 τ ′ fresh (IfT) ρ, τ ` e1, σ ⇓ n, σ1 ρ, τ ` e2, σ1 ⇓ v, σ2 ρ, τ ` if0(e1){e2}{e3}, σ ⇓ v, σ2 n = 0 (IfF) ρ, τ ` e1, σ ⇓ n, σ1 ρ, τ ` e3, σ1 ⇓ v, σ2 ρ, τ ` if0(e1){e2}{e3}, σ ⇓ v, σ2 n 6= 0 Figure 6.11: λIF Big-step Concrete Evaluation Semantics standard: ρ and σ are the environment and store, e is the initial expression, and v is the resulting value. The argument τ represents “time,” which when abstracted supports modeling execution contexts like call-site sensitivity. Concretely time is modeled as a natural number, and all that is required is that “fresh” numbers are available for allocating values in the store. 227 Reachability The primary limitation of using big-step semantics as a starting point for abstraction is that intermediate computations are not represented in the model for evaluation. For example, consider the program that applies the identity function to an expression that loops, which we notate Ω: (λx.x)(Ω) A big-step evaluation relation can only describe results of terminating computations, and because this program never terminates, such a relation says nothing about the behavior of the program. A good static analyzer will explore the behavior of Ω to (possibly) discover that it loops, or more importantly, to provide analysis results (like data-flow or side-effects) for intermediate computation states. The need to analyze intermediate states is the primary reason that big-step semantics are overlooked as a starting point for abstract interpretation. To remedy the situation, while remaining in a big-step setting, we introduce a big-step reachability relation, notated ρ, τ ` e, σ ⇑ ς and shown in Figure 6.12. Configurations ς are tuples 〈e, ρ, σ, τ〉, and are reachable when evaluation passes through the configuration at any point on its way to a final value, or during an infinite loop. The complete big-step semantics of an expression (e) under environment (ρ), store (σ) and time (τ), which we notate JeKbs(ρ, σ, τ), is then the set of all reachable evaluations : JeKbs(ρ, σ, τ) := {〈v, σ′′〉 | ρ, σ, τ ⇑ 〈e′, ρ′, σ′, τ ′〉 ∧ ρ′, τ ′ ` e′, σ′ ⇓ v, σ′′} We construct a formal bridge between the big-step and small-step worlds through 228 (Concrete Reachability) ρ, τ ` e, σ ⇑ ς (Refl) ρ, τ ` e, σ ⇑ 〈e, ρ, σ, τ〉 (RBin1) ρ, τ ` e1, σ ⇑ ς ρ, τ ` b(e1, e2), σ ⇑ ς (RBin2) ρ, τ ` e1, σ ⇓ v1, σ1 ρ, τ ` e2, σ1 ⇑ ς ρ, τ ` b(e1, e2), σ ⇑ ς (RApp1) ρ, τ ` e1, σ ⇑ ς ρ, τ ` e1(e2), σ ⇑ ς (RApp2) ρ, τ ` e1, σ ⇓ v1, σ1ρ, τ ` e2, σ1 ⇑ ς ρ, τ ` e1(e2), σ ⇑ ς 〈λx.e′, ρ′〉 = v1 (RApp3) ρ, τ ` e1, σ ⇓ v1, σ1 ρ, τ ` e2, σ1 ⇓ v2, σ2 ρ′[x 7→ `], τ ′ ` e′, σ2[` 7→ v2] ⇑ ς ρ, τ ` e1(e2), σ ⇑ ς 〈λx.e′, ρ′〉 = v1 ` = 〈x, τ ′〉 τ ′ fresh (RIf1) ρ, τ ` e1, σ ⇑ ς ρ, τ ` if0(e1){e2}{e3}, σ ⇑ ς (RIfT) ρ, τ ` e1, σ ⇓ n, σ1 ρ, τ ` e2, σ1 ⇑ ς ρ, τ ` if0(e1){e2}{e3}, σ ⇑ ς n = 0 (RIfF) ρ, τ ` e1, σ ⇓ n, σ1 ρ, τ ` e3, σ1 ⇑ ς ρ, τ ` if0(e1){e2}{e3}, σ ⇑ ς n 6= 0 Figure 6.12: λIF Big-step Concrete Reachability Semantics 229 the complete big-step semantics (JeKbs) and a complete small-step semantics ∗, which is traditionally used as the starting point of abstraction for program analysis: JeKss(ρ, σ, τ) := {〈v, σ′′〉 | ∀κ. 〈e, ρ, σ, τ, κ〉 ∗ 〈e′, ρ′, σ′, τ ′, κ′ ++ κ〉 ∧ 〈e′, ρ′, σ′, τ ′, κ′ ++ κ〉 ∗ 〈v, ρ′′, σ′′, τ ′′, κ′ ++ κ〉} We connect the complete big-step and small-step semantics through the following theorem: Theorem 7 (Complete Big-step/Small-step Equivalence). JeKbs(ρ, σ, τ) = JeKss(ρ, σ, τ) The proof is by induction on the big-step derivation for ⊆, and on the transitive small-step derivation for ⊇. Collecting Semantics Before abstracting the semantics—in pursuit of a sound static analysis algorithm—we pass through a big-step collecting evaluation and reachability semantics, notated ρ, τ ` e, σ˜ ⇓ v˜, σ˜ and ρ, τ ` e, σ˜ ⇑ ς˜ and shown in figures 6.13 and 6.14, where v˜, σ˜ and ς˜ range over collecting state spaces: v˜ ∈ v˜al := ℘(val) σ˜ ∈ s˜tore := addr 7→ v˜al ς˜ ∈ c˜onfig := exp× env× s˜tore× time and the denotation for binary operators (JbK) is lifted to a collecting denotation operator J˜bK: J˜bK(v˜1, v˜2) := {JbK(v1, v2) | v1 ∈ v˜1 ∧ v2 ∈ v˜2} The big-step collecting and reachability relations are structurally similar to the 230 (Collecting Evaluation) ρ, τ ` e, σ˜ ⇓ v˜, σ˜′ (OLit) ρ, τ ` n, σ˜ ⇓ {n}, σ˜ (OVar) ρ, τ ` x, σ˜ ⇓ σ˜(ρ(x)), σ˜ (OLam) ρ, τ ` λx.e, σ˜ ⇓ {〈λx.e, ρ〉}, σ˜ (OBin) ρ, τ ` e1, σ˜ ⇓ v˜1, σ˜1 ρ, τ ` e2, σ˜1 ⇓ v˜2, σ˜2 ρ, τ ` b(e1, e2), σ˜ ⇓ J˜bK(v˜1, v˜2), σ˜2 (OApp) ρ, τ ` e1, σ˜ ⇓ v˜1, σ˜1 ρ, τ ` e2, σ˜1 ⇓ v˜2, σ˜2 ρ′[x 7→ `], τ ′ ` e′, σ˜2[` 7→ v˜2] ⇓ v˜′, σ˜3 ρ, τ ` e1(e2), σ˜ ⇓ v˜′, σ˜3 〈λx.e′, ρ′〉 ∈ v˜1 ` = 〈x, τ ′〉 τ ′ fresh (OIfT) ρ, τ ` e1, σ˜ ⇓ v˜1, σ˜1 ρ, τ ` e2, σ˜1 ⇓ v˜, σ˜2 ρ, τ ` if0(e1){e2}{e3}, σ˜ ⇓ v˜, σ˜2 0 ∈ v˜1 (OIfF) ρ, τ ` e1, σ˜ ⇓ v˜1, σ˜1 ρ, τ ` e3, σ˜1 ⇓ v˜, σ˜2 ρ, τ ` if0(e1){e2}{e3}, σ˜ ⇓ v˜, σ˜2 n ∈ v˜1 n 6= 0 Figure 6.13: Big-step Collecting Evaluation Semantics 231 (Collecting Reachability) ρ, τ ` e, σ˜ ⇑ ς˜ (ORefl) ρ, τ ` e, σ˜ ⇑ 〈e, ρ, σ˜, τ〉 (ORBin1) ρ, τ ` e1, σ˜ ⇑ ς ρ, τ ` b(e1, e2), σ˜ ⇑ ς˜ (ORBin2) ρ, τ ` e1, σ˜ ⇓ v˜1, σ˜1 ρ, τ ` e2, σ˜1 ⇑ ς˜ ρ, τ ` b(e1, e2), σ˜ ⇑ ς˜ (ORApp1) ρ, τ ` e1, σ˜ ⇑ ς˜ ρ, τ ` e1(e2), σ˜ ⇑ ς˜ (ORApp2) ρ, τ ` e1, σ˜ ⇓ v˜1, σ˜1 ρ, τ ` e2, σ˜1 ⇑ ς˜ ρ, τ ` e1(e2), σ˜ ⇑ ς˜ λx.e′, ρ′〉 ∈ v˜1 (ORApp3) ρ, τ ` e1, σ˜ ⇓ v˜1, σ˜1 ρ, τ ` e2, σ˜1 ⇓ v˜2, σ˜2 ρ′[x 7→ `], τ ′ ` e′, σ˜2[` 7→ v˜2] ⇑ ς˜ ρ, τ ` e1(e2), σ˜ ⇑ ς˜ 〈λx.e′, ρ′〉 ∈ v˜1 ` = 〈x, τ ′〉 τ ′ fresh (ORIf1) ρ, τ ` e1, σ˜ ⇑ ς˜ ρ, τ ` if0(e1){e2}{e3}, σ˜ ⇑ ς˜ (ORIfT) ρ, τ ` e1, σ˜ ⇓ v˜1, σ˜1 ρ, τ ` e2, σ˜1 ⇑ ς˜ ρ, τ ` if0(e1){e2}{e3}, σ˜ ⇑ ς˜ 0 ∈ v˜1 (ORIfF) ρ, τ ` e1, σ˜ ⇓ v˜1, σ˜1 ρ, τ ` e3, σ˜1 ⇑ ς˜ ρ, τ ` if0(e1){e2}{e3}, σ˜ ⇑ ς˜ n ∈ v˜1 n 6= 0 Figure 6.14: Big-step Collecting Reachability Semantics 232 concrete semantics. The primary differences are the use of set containment (∈) in place of equality (=) when branching on application and conditional expressions. The big-step collecting reachability semantics is a sound approximation of the big-step concrete reachability semantics: Theorem 8 (Collecting Reachability Semantics Soundness). If ρ, τ ` e, σ ⇑ 〈e′, ρ′, σ′, τ ′〉 and ρ′, τ ′ ` e′, σ′ ⇓ v, σ′′ where η(σ) v σ˜ then ρ, τ ` e, σ˜ ⇑ 〈e′, ρ′, σ˜′, τ ′〉 and ρ′, τ ′ ` e, σ˜′ ⇓ v˜, σ˜′′ where η(σ′) v σ˜′ and v ∈ v˜ and η(σ′′) v σ˜′′ The proof is by induction on the concrete big-step derivation. The extraction function η is defined separately for stores (σ) and configurations (ς): η(σ)(`) := {σ(`)} η(〈e, ρ, σ, τ〉) := 〈e, ρ, η(σ), τ〉 and the partial ordering on stores and configurations is pointwise: σ˜1 v σ˜2 iff ∀`. σ˜1(`) ⊆ σ˜2(`) 〈e1, ρ1, σ˜1, τ1〉 v 〈e2, ρ2, σ˜2, τ2〉 iff e1 = e2 ∧ ρ1 = ρ2 ∧ σ˜1 v σ˜2 ∧ τ1 = τ2 Finite Abstraction The next step towards a computable static analysis is an abstract semantics with a finite state space that approximates the big-step collecting semantics, notated ρ̂, τ̂ ` e, σ̂ ⇓ v̂, σ̂ and ρ̂, τ̂ ` e, σ̂ ⇑ ς̂ and shown in figures 6.15 233 and 6.16, where ρ̂, τ̂ , v̂, σ̂ and ς̂ are finite abstractions of their collecting counterparts: ρ̂ ∈ ênv := var 7→ âddr⊥̂`∈ âddr := var× t̂ime τ̂ ∈ t̂ime := . . . v̂ ∈ v̂al := . . . σ̂ ∈ ŝtore := âddr 7→ v̂al ς̂ ∈ ĉonfig := exp× ênv× ŝtore× t̂ime The primary structural difference from the collecting semantics is the use of join when updating the store (σ̂ unionsq [̂` 7→ v̂]) rather than strict replacement (σ˜[` 7→ v˜]). This is to preserve soundness in the presence of address reuse, which occurs from the finite size of the address space. The abstract denotation (ĴbK) is any over-approximation of the collecting denotation (J˜bK) w.r.t. a Galois connection v˜al −−→←−−αγ v̂al: ĴbK(v̂1, v̂2) w α(J˜bK(γ(v̂1), γ(v̂2))) Concretization functions bγcclo, bγc0 and bγc¬0 are computable finite subsets of the full concretization function γ s.t.: bγcclo(v̂) := {〈λx.e, ρ̂〉 | 〈λx.e, ρ̂〉 ∈ γ(v̂)} bγc0(v̂) := {0 | 0 ∈ γ(v̂)} bγc¬0(v̂) := {¬0 | n ∈ γ(v̂) ∧ n 6= 0} Abstract sets t̂ime and v̂al are left as parameters to the analysis along with their operations n̂ext, ĴbK, bγcclo, bγc0, bγc¬0 and unionsqv̂al. The abstract semantics is a sound approximation of the collecting semantics, which we establish through the theorem: 234 (Abstract Evaluation) ρ̂, τ̂ ` e, σ̂ ⇓ v̂, σ̂′ (ALit) ρ̂, τ̂ ` n, σ̂ ⇓ η̂(n), σ̂ (AVar) ρ̂, τ̂ ` x, σ̂ ⇓ σ̂(ρ̂(x)), σ̂ (ALam) ρ̂, τ̂ ` λx.e, σ̂ ⇓ η̂(〈λx.e, ρ̂〉), σ̂ (ABin) ρ̂, τ̂ ` e1, σ̂ ⇓ v̂1, σ̂1 ρ̂, τ̂ ` e2, σ̂1 ⇓ v̂2, σ̂2 ρ̂, τ̂ ` b(e1, e2), σ̂ ⇓ ĴbK(v̂1, v̂2), σ̂2 (AApp) ρ̂, τ̂ ` e1, σ̂ ⇓ v̂1, σ̂1 ρ̂, τ̂ ` e2, σ̂1 ⇓ v̂2, σ̂2 ρ̂′[x 7→ ̂`], τ̂ ′ ` e′, σ̂2 unionsq [̂` 7→ v̂2] ⇓ v̂′, σ̂3 ρ̂, τ̂ ` e1(e2), σ̂ ⇓ v̂′, σ̂3 〈λx.e′, ρ̂′〉 ∈ bγcclo(v̂1) ς̂ = 〈e1(e2), ρ̂, σ̂, τ̂〉̂`= 〈x, τ̂ ′〉 τ̂ ′ = n̂ext(τ̂ , ς̂) (AIfT) ρ̂, τ̂ ` e1, σ̂ ⇓ v̂1, σ̂1ρ̂, τ̂ ` e2, σ̂1 ⇓ v̂, σ̂2 ρ̂, τ̂ ` if0(e1){e2}{e3}, σ̂ ⇓ v̂, σ̂2 0 ∈ bγc0(v̂1) (AIfF) ρ̂, τ̂ ` e1, σ̂ ⇓ v̂1, σ̂1 ρ̂, τ̂ ` e3, σ̂1 ⇓ v̂, σ̂2 ρ̂, τ̂ ` if0(e1){e2}{e3}, σ̂ ⇓ v̂, σ̂2 ¬0 ∈ bγc¬0(v̂1) Figure 6.15: Big-step Abstract Evaluation Semantics 235 (Abstract Reachability) ρ̂, τ̂ ` e, σ̂ ⇑ ς̂ (ARefl) ρ̂, τ̂ ` e, σ̂ ⇑ 〈e, ρ̂, σ̂, τ̂〉 (ARBin1) ρ̂, τ̂ ` e1, σ̂ ⇑ ς ρ̂, τ̂ ` b(e1, e2), σ̂ ⇑ ς̂ (ARBin2) ρ̂, τ̂ ` e1, σ̂ ⇓ v̂1, σ̂1 ρ̂, τ̂ ` e2, σ̂1 ⇑ ς̂ ρ̂, τ̂ ` b(e1, e2), σ̂ ⇑ ς̂ (ARApp1) ρ̂, τ̂ ` e1, σ̂ ⇑ ς̂ ρ̂, τ̂ ` e1(e2), σ̂ ⇑ ς̂ (ARApp2) ρ̂, τ̂ ` e1, σ̂ ⇓ v̂1, σ̂1 ρ̂, τ̂ ` e2, σ̂1 ⇑ ς̂ ρ̂, τ̂ ` e1(e2), σ̂ ⇑ ς̂ 〈λx.e′, ρ̂′〉 ∈ bγcclo(v̂1) (ARApp3) ρ̂, τ̂ ` e1, σ̂ ⇓ v̂1, σ̂1 ρ̂, τ̂ ` e2, σ̂1 ⇓ v̂2, σ̂2 ρ̂′[x 7→ ̂`], τ̂ ′ ` e′, σ̂2 unionsq [̂` 7→ v̂2] ⇑ ς̂ ρ̂, τ̂ ` e1(e2), σ̂ ⇑ ς̂ 〈λx.e′, ρ̂′〉 ∈ bγcclo(v̂1) ς̂ = 〈e1(e2), ρ̂, σ̂, τ̂〉̂`= 〈x, τ̂ ′〉 τ̂ ′ = n̂ext(τ̂ , ς̂) (ARIf1) ρ̂, τ̂ ` e1, σ̂ ⇑ ς̂ ρ̂, τ̂ ` if0(e1){e2}{e3}, σ̂ ⇑ ς̂ (ARIfT) ρ̂, τ̂ ` e1, σ̂ ⇓ v̂1, σ̂1 ρ̂, τ̂ ` e2, σ̂1 ⇑ ς̂ ρ̂, τ̂ ` if0(e1){e2}{e3}, σ̂ ⇑ ς̂ 0 ∈ bγc0(v̂1) (ARIfF) ρ̂, τ̂ ` e1, σ̂ ⇓ v̂1, σ̂1 ρ̂, τ̂ ` e3, σ̂1 ⇑ ς̂ ρ̂, τ̂ ` if0(e1){e2}{e3}, σ̂ ⇑ ς̂ ¬0 ∈ bγc¬0(v̂1) Figure 6.16: Big-step Abstract Reachability Semantics 236 Theorem 9 (Abstract Reachability Semantics Soundness). If ρ, τ ` e, σ˜ ⇑ 〈e′, ρ′, σ˜′, τ ′〉 and ρ′, τ ′ ` e′, σ˜′ ⇓ v˜, σ˜′′ where η(ρ) v ρ̂ and η(τ) v τ̂ and η(σ˜) v σ̂ then ρ̂, τ̂ ` e, σ̂ ⇑ 〈e′, ρ̂′, σ̂′, τ̂ ′〉 and ρ̂′, τ̂ ′ ` e, σ̂′ ⇓ v̂, σ̂′′ where η(ρ′) v ρ̂′, η(τ ′) v τ̂ ′, η(σ˜′) v σ̂′, v ∈ v˜, η(σ′′) v σ˜′′ The proof is by induction on the big-step derivation. The extraction function η is defined separately for environments (ρ), time (τ), collecting stores (σ˜), values (v˜) and configurations (ς˜). η(τ) and η(v˜) are given with parameters t̂ime and v̂al. η(ρ), η(σ˜) and η(ς˜) are defined pointwise: η(ρ)(x) := η(ρ(x)) η(σ˜)(̂`) := ⊔ `∈γ(̂`) η(σ˜(`)) η(〈e, ρ, τ, σ˜〉) := 〈e, η(ρ), η(τ), η(σ˜)〉 Computing the Analysis An analysis for the program e0 w.r.t. the abstract semantics is some cache $ ∈ ĉonfig 7→ ℘(v̂al × ŝtore) that maps all configurations reachable from the initial configuration 〈e0, ρ̂0, σ̂0, τ̂0〉 to their final values and stores v̂, σ̂, which we notate $ |= e0: $ |= e0 iff If ρ̂0, τ̂0 ` e0, σ̂0 ⇑ 〈e, ρ̂, σ̂, τ̂〉 and ρ̂, τ̂ ` e, σ̂ ⇓ v̂, σ̂′ then 〈v̂, σ̂′〉 ∈ $(〈e, ρ̂, σ̂, τ̂〉) The best cache $+ is then computed as the least fixed point of the functional F : F ∈ (ĉonfig 7→ ℘(v̂al× ŝtore))→ (ĉonfig 7→ ℘(v̂al× ŝtore)) F := λ$. ⊔ 〈e,ρ̂,σ̂,τ̂〉∈$ {〈e, ρ̂, σ̂, τ̂〉 7→ {〈v̂, σ̂′〉} | ρ̂, τ̂ ` e, σ̂ ⇓$ v̂, σ̂′}{ς̂ 7→ {} | ρ̂, τ̂ ` e, σ̂ ⇑$ ς̂} 237 which also includes the initial configuration: $+ := lfp(λ$.F($) unionsq {〈e0, η(ρ0), η(σ0), η(τ0)〉 7→ {}}) The relations ρ̂, τ̂ ` e, σ̂ ⇓$ v̂, σ̂′ and ρ̂, τ̂ ` e, σ̂ ⇑$ ς̂ are modified versions of the original abstract semantics, but with recursive judgements replaced by 〈v̂, σ̂′〉 ∈ $(e, ρ̂, σ̂, τ̂) and ς̂ ∈ $(e, ρ̂, σ̂, τ̂) respectively. Therefore F is not recursive; the recursion in the relations is lifted to the outer fixed-point of the analysis. Because the state space ĉonfig 7→ ℘(v̂al× ŝtore) is finite and F is monotonic, $+ can be computed algorithmically in finite time by Kleene fixed-point iteration. See Nielson et al. [1999] for more background and examples of static analyzers computed in this style, and from which the current development was largely inspired. Theorem 10 (Algorithm Correctness). $+ is a valid analysis for e0, that is: $ + |= e0. The proof is by induction on the assumed derivations ρ̂0, τ̂0 ` e0, σ̂0 ⇑ 〈ê, ρ̂, σ̂, τ̂〉 and ρ̂, τ̂ ` e, σ̂ ⇓ v̂, σ̂′, and utilizes the fact that $+ is a fixed point, that is: F($+) = $+. Our final theorem relates the analysis cache $+ back to the concrete semantics of the initial program as a sound approximation: Theorem 11 (Algorithm Soundness). If ρ0, τ0 ` e0, σ0 ⇑ 〈e, ρ, σ, τ〉 and ρ, τ ` e, σ ⇓ v, σ′ then 〈v̂, σ̂′〉 ∈ $+(〈e, ρ̂, σ̂, τ̂〉) where η(ρ) v ρ̂, η(τ) v τ̂ , η(σ) v σ̂, η(v) v v̂, η(σ′) v σ̂′ The proof follows by composing Theorems 1-4. 238 Computing with Definitional Interpreters The algorithm described in Section 6.4 is a more efficient strategy for computing $+ using an extensible open- recursive definitional interpreter. This technique is general, and bridges the gap between the big-step abstract semantics formalized in this section and the definitional interpreters we wish to execute to obtain analyses. An extensible open-recursive definitional interpreter for λIF (the small language formalized in this section) has domain: E ∈ Σ→ Σ where Σ := ĉonfig→ ℘(v̂al× ŝtore) and is defined such that its denotational-fixed-point (Y (E)) recovers concrete inter- pretation when instantiated with the concrete state-space. For example, the recursive case for binary operator expressions is defined: E(E ′)(〈b(e1, e2), ρ̂, σ̂, τ̂) := {ĴbK(v̂1, v̂2) | 〈v̂1, σ̂1〉 ∈ E ′(〈e1, ρ̂, σ̂, τ̂〉) ∧ 〈v̂2, σ̂2〉 ∈ E ′(〈e2, ρ̂, σ̂1, τ̂〉)} The iteration strategy to analyze the program e0 is then to run e0 using E , but intercepting recursive calls to: 1. Cache results for all intermediate configurations ς̂; and 2. Cache seen states to prevent infinite loops. (1) is required to fulfill the specification that $+ include results for all reachable configurations from e0, and (2) is required to reach a fixed point of the analysis. To track this extra information we add functional state to the interpreter (which was 239 done through a monad transformer in Section 6.4) of type: ĉache := ĉonfig 7→ ℘(v̂al× ŝtore) such that the open-recursive evaluator has type: E ∈ Σ→ Σ where Σ := ĉonfig× ĉache→ ℘(v̂al× ŝtore)× ĉache The iteration to compute $+ given E is then defined: $+ := lfp(λ$o. let E∗ := Y (λE ′.E(λ〈ς̂ , $i〉. if ς̂ ∈ $i then 〈$i(ς̂), $i〉 else let 〈V̂ S, $i′〉 := E ′(ς̂ , $i[ς̂ 7→ $o(ς̂)]) in 〈V̂ S, $i′[ς̂ 7→ V̂ S]〉)) in pi2(E∗(〈e0, ρ̂0, σ̂0, τ̂0〉, {}))) The fixed interpreter E∗ calls the unfixed interpreter E , but intercepts recursive calls to perform (1) and (2) described above. When loops are detected, the results from the previous complete result $o is used, and the outer fixed-point computes the least fixed point of this $o. The end result is that, rather than compute analysis results and reachable states naively with Kleene fixed-point iteration, we are able to reuse the standard definitional interpreter—written in open-recursive form—to simultaneously explore reachable states, cache intermediate configurations, and iterate towards a least fixed- point solution for the analysis. This method is more efficient, and reuses an extensible definitional interpreter which can recover a wide range of analyses, including concrete interpretation. 240 Widening Two forms of widening can be employed to the semantics and iteration algorithm to achieve acceptable performance for the abstract interpreter. The first form of widening is to widen the store in the result set ℘(v̂al× ŝtore) to ℘(v̂al)× ŝtore in the evaluator E : E ∈ Σ→ Σ where Σ := ĉonfig× ĉache→ ℘(v̂al)× ŝtore× ĉache We perform this widening systematically and with no added effort through the use of Galois Transformers [Darais et al., 2015] in Section 6.6. The iteration strategy for this widened state space is the same as before, which computes a fixed point of the outer cache $o. The next form of widening is to pull the store out of the configuration space entirely, that is: ς̂ ∈ ĉonfig := exp× ênv× t̂ime $ ∈ ĉache := ĉonfig 7→ ℘(v̂al) and: E ∈ Σ→ Σ where Σ := ĉonfig× ŝtore× ĉache→ ℘(v̂al)× ŝtore× ĉache The fixed point iteration then finds a mutual least fixed-point of both the outer 241 cache $o and the store σ̂: 〈$+, σ̂+〉 := lfp(λ〈$o, σ̂〉. let E∗ := Y (λE ′.E(λ〈ς̂ , σ̂i, $i〉. if ς̂ ∈ $i then 〈$i(ς̂), σi, $i〉 else let 〈V̂ , σ̂i′, $i′〉 := E ′(ς̂ , σ̂i, $i[ς̂ 7→ $o(ς̂)]) in 〈V̂ , σ̂i′, $i′[ς̂ 7→ V̂ ]〉)) in pi2×3(E∗(〈e0, ρ̂0, τ̂0〉, σ̂, {}))) This second version of widening, which computes a fixed-point also over the store, recovers a so-called flow-insensitive analysis. In this model, all program states are re-analyzed in the store resulting from execution. Also, the cache ($) does not index over store states σ̂ in its domain, greatly reducing its size, and leading to a much more efficient (although less precise) static analyzer. Recovering Classical 0CFA From the fully widened static analyzer, which computes a mutual fixed-point between a cache and store, we can easily recover a classical 0CFA analysis. We do this by instantiating t̂ime to the singleton abstraction {•}, as was shown in Section 6.3. In this setting, the lexical environment ρ is uniquely determined by the program expression e, and can therefore be eliminated, resulting in the analysis state space: ς̂ ∈ ĉonfig := exp $ ∈ ĉache := exp 7→ ℘(v̂al) σ̂ ∈ ŝtore := var 7→ ℘(v̂al) The specification for the analysis and the fully store-widened least fixed-point iteration for computing it recovers the constraint-based description of 0CFA given by Nielson 242 et al. [1999], where 0CFA is defined as the smallest cache ($) and store (σ) which satisfy a co-inductively defined judgment: $, σ |= e. Recovering Pushdown Analysis We borrow from the recent result in push- down analysis by Gilray et al. [2016b] which shows that full pushdown precision can be achieved in a small-step store-widened abstract semantics by allocating continu- ations using a particular address space: program expressions paired with abstract environments (〈e, ρ̂〉). In other words, 〈e, ρ̂〉 is sufficient to achieve full pushdown precision because the tuple uniquely identifies the evaluation context up to the final result of evaluation. Our fully widened semantics recovers pushdown precision because the cache maps tuples 〈e, ρ̂, τ̂〉, which contains 〈e, ρ̂〉. We then see that abstract time τ̂ is redundant and eliminate it from the cache, resulting in a smaller domain for the same analysis: ς̂ ∈ ĉonfig := exp× ênv× t̂ime $ ∈ ĉache := exp× ênv 7→ ℘(v̂al) σ̂ ∈ ŝtore := var× âddr 7→ ℘(v̂al) An advantage of our setting is that we recover pushdown analysis also for varying degrees of store-widening, which is not the case in Gilray et al., although push- down precision for non-widened semantics has been achieved by Johnson and Van Horn [Johnson and Van Horn, 2014]. Furthermore, the implementation of our an- alyzer inherits this precision through precise call-return matching in the defining metalanguage, requiring no added instrumentation to the state-space of the analyzer. 243 Going back to Nielson et al. [1999], it would be interesting to redevelop their constraint-based analysis descriptions of kCFA in a form that recovers pushdown precision. Such an exercise would amount to translating our big-step abstract semantics instantiated to kCFA to a constraint system. The resulting system would differ from classical kCFA by the addition of environments ρ̂ (which Nielson et al. call context environments) to the domain of the cache. In this way our formal framework is able to bridge the gap between results in pushdown analysis described via small-step machines a` la Van Horn and Might [Van Horn and Might, 2010], and constraint-based systems a` la Nielson et al. for which pushdown analysis has yet to be described effectively. 6.11 Related Work This work draws upon and re-presents many ideas from the literature on abstract interpretation for higher-order languages [Midtgaard, 2012]. In particular, it closely follows the abstracting abstract machines [Van Horn and Might, 2010, 2012] approach to deriving abstract interpreters from a small-step machine. The key difference here is that we operate in the setting of a monadic definitional interpreter instead of an abstract machine. In moving to this new setting we developed a novel caching mechanism and fixed-point algorithm, but otherwise followed the same recipe. Re- markably, in the setting of definitional interpreters, the pushdown property for the analysis is simply inherited from the meta-language rather than requiring explicit instrumentation to the abstract interpreter. 244 Compositionally defined abstract interpretation functions for higher-order languages were first explored by Jones and Nielson [1995], which introduces the technique of interpreting a higher-order object language directly as terms in a meta- language to perform abstract interpretation. While their work lays the foundations for this idea, it does not consider abstractions for fixed-points in the domain, so although their abstract interpreters are sound, they are not in general computable. They propose a na¨ıve solution of truncating the interpretation of syntactic fixed- points to some finite depth, but this solution isn’t general and doesn’t account for non-syntactic occurrences of bottom in the concrete domain (e.g., via Y combinators). Our work develops such an abstraction for concrete denotational fixed-points using a fixed-point caching algorithm, resulting in general, computable abstractions for arbitrary definitional interpreters. The use of monads and monad transformers to make extensible (concrete) interpreters is a well-known idea [Liang et al., 1995, Moggi, 1989, Steele, 1994], which we have extended to work for compositional abstract interpreters. The use of monads and monad transformers in machine-based formulations of abstract interpreters has previously been explored by Sergey et al. [2013] and Darais et al. [2015], respectively, and inspired our own adoption of these ideas. Darais has also shown that certain monad transformers are also Galois transformers, i.e., they compose to form monads that transport Galois connections. Using Galois transformers may enable both compositional code and proofs for abstract interpreters in the style presented here. The caching mechanism used to ensure termination in our abstract interpreter is similar to that used by Johnson and Van Horn [2014]. They use a local- and 245 meta-memoization table in a machine-based interpreter to ensure termination for a pushdown abstract interpreter. This mechanism is in turn reminiscent of Glu¨ck’s use of memoization in an interpreter for two-way non-deterministic pushdown au- tomata [Glu¨ck, 2013]. Caching recursive, non-deterministic functions is a well-studied problem in the functional logic programming community through a technique called “tabling” [Bol and Degerstedt, 1993, Chen and Warren, 1996, Swift and Warren, 2012, Tamaki and Sato, 1986], which has been successfully applied to program verification and analysis [Dawson et al., 1996, Janssens and Sagonas, 1998]. Unlike these systems, our approach uses a shallow embedding of cached non-determinism that can be applied in general-purpose functional languages. Monad transformers that enable shallow embedding of cached non-determinism are of continued interest since Hinze’s Deriving Backtracking Monad Transformers [Fischer et al., 2009, Hinze, 2000, Kiselyov et al., 2005], and recent work [Ploeg and Kiselyov, 2014, Vandenbroucke et al., 2015] points to potential optimizations that can be applied to our naive iteration strategy. Vardoulakis, who was the first to develop the idea of a pushdown abstraction for higher-order flow analysis [Vardoulakis and Shivers, 2011], formalized CFA2 using a CPS model, which is similar in spirit to a machine-based model. However, in his dissertation [Vardoulakis, 2012] he sketches an alternative presentation dubbed “Big CFA2” which is a big-step operational semantics for doing pushdown analysis quite similar in spirit to the approach presented here. One key difference is that Big CFA2 fixes a particular coarse abstraction of base values and closures—for example, both branches of a conditional are always evaluated. Consequently, it only uses 246 a single iteration of the abstract evaluation function, and avoids the need for the cache-based fixed-point of Section 6.4. We believe Big CFA2 as stated is sound, however if the underlying abstractions were tightened, it may then require a more involved fixed-point finding algorithm like the one we developed. Our formulation of a pushdown abstract interpreter computes an abstraction similar to the many existing variants of pushdown flow analysis [Earl et al., 2010, Gilray et al., 2016b, Johnson and Van Horn, 2014, Van Horn and Might, 2012, Vardoulakis, 2012, Vardoulakis and Shivers, 2011]. Our incorporation of an abstract garbage collector into a pushdown abstract interpreter achieves a similar goal as that of so-called introspective pushdown abstract interpreters [Earl et al., 2012, Johnson et al., 2014]. The mixing of symbolic execution and abstract interpretation is similar in spirit to the logic flow analysis of Might [Might, 2007b], albeit in a pushdown setting and with a stronger notion of negation; generally, our presentation resembles traditional formulations of symbolic execution more closely [King, 1976]. Our approach to symbolic execution only handles the first-order case of symbolic values, as is common. However, Nguye˜ˆn’s work on higher-order symbolic execution [Nguye˜ˆn and Van Horn, 2015] demonstrates how to scale to behavioral symbolic values. In principle, it should be possible to handle this case in our approach by adapting Nguye˜ˆn’s method to a formulation in a compositional evaluator, but this remains to be carried out. Now that we have abstract interpreters formulated with a basis in abstract machines and with a basis in monadic interpreters, an obvious question is can we obtain a correspondence between them similar to the functional correspondence 247 between their concrete counterparts [Ager et al., 2005]. An interesting direction for future work is to try to apply the usual tools of defunctionalization, CPS, and refocusing to see if we can interderive these abstract semantic artifacts. 6.12 Conclusions We have shown that the AAM methodology can be adapted to definitional interpreters written in monadic style. Doing so captures a wide variety of semantics, such as the usual concrete semantics, collecting semantics, and various abstract interpretations. Beyond recreating existing techniques from the literature such as store-widening and abstract garbage collection, we can also design novel abstractions and capture disparate forms of program analysis such as symbolic execution. Further, our approach enables the novel combination of these techniques. To our surprise, the definitional abstract interpreter we obtained implements a form of pushdown control flow abstraction in which calls and returns are always properly matched in the abstract semantics. True to the definitional style of Reynolds, the evaluator involves no explicit mechanics to achieve this property; it is simply inherited from the metalanguage. We believe this formulation of abstract interpretation offers a promising new foundation towards re-usable components for the static analysis and verification of higher-order programs. Moreover, we believe the definitional abstract interpreter approach to be a fruitful new perspective on an old topic. We are left wondering: what else can be profitably inherited from the metalanguage of an abstract interpreter? 248 Chapter 7: Concluding Remarks In this thesis we have aimed to lower the barrier to adopting high assurance program analyzers for use in creating reliable software systems. These barriers are the feasibility of mechanically verifying individual program analyzers, and the degree to which general purpose program analysis machinery supports reuse. Without feasibility and reuse, program analyzers will never make a meaningful impact on the quality of software produced by practitioners. Our first contribution, Constructive Galois Connections, addresses feasibility by making it possible to mechanically verify a large class of correct-by-construction pro- gram analyzers which previous approaches were unable to verify. This was achieved by solving an open problem from the literature which was the primary barrier to achieving mechanized verification for this class of analyzers. Using Constructive Galois Connections, it is now possible to synthesize correct-by-construction program analyzers directly from programming language semantics, all while remaining embed- ded in a mechanized verification framework which supports immediate extraction of verified program analyzers from the results of synthesis. Our second contribution, Galois Transformers, makes it possible to reuse program analysis machinery across different program analyzer implementations. This 249 was achieved by isolating a large class of analyzer design decisions using a novel interface for separating these concerns. Using Galois Transformers, it is now possible to design a single program analyzer—for, say, Java or C programming languages—and tune each of context, object, path, and flow sensitivity for the analyzer, all without needing to modify the implementation. The ability to tune these precision parameters is important for practitioners because there is no one-size-fits-all point in their design space. E.g., analyzing buffer overflows requires a very different instantiation for these parameters than analyzing data integrity and confidentiality. Our third and final contribution, Abstracting Definitional Interpreters, makes it possible to reuse programming language features across different program analyzer implementations. This was achieved by transplanting an existing systematic approach for designing program analyzers into a new setting which supports plug-and-play composition of programming language features. Using Abstracting Definitional Interpreters, it is now possible to design a single program analyzer—for, say, Ruby or Python—merely as the composition of its programming language features. The ability to quickly construct new analyzers from existing components is becoming more and more important as the number of programming languages used by practitioners continues to expand. E.g., Ruby and Python share many language features in common (object-orientation, first-class procedures, late binding, etc.) and our work paves the way towards a modular analysis tool which supports a wide range of similar programming languages, as opposed to most tools which only support one language. 250 Appendix A: Galois Transformer Proofs A.0.1 Lemma 5 [Galois Transformers] (Section 5.8.4) State St[s] is a Galois transformer. Recall the definition of St[s] and ΠS t [s]: St[s](m)(A) := s→ m(A× s) ΠS t [s](Σ)(A) := Σ(A× s)→ Σ(A× s) State Property (1) The action St[s] on functions: St[s] : (A→ m(B))→ A→ St[s](m)(B) St[s](f)(x)(s) := y ←m f(x) ; returnm(y, s) To transport Galois connections, we assume a Galois connection A→ m1(B) −−−−→←−−−− αm γm A→ m2(B) and define α and γ: α : (A→ St[s](m1)(B))→ A→ St[s](m2)(B) γ : (A→ St[s](m2)(B))→ A→ St[s](m1)(B) α(f)(x)(s) := αm(λ〈x, s〉.f(x)(s))(x, s) γ(f)(x)(s) := γm(λ〈x, s〉.f(x)(s))(x, s) 251 α and γ are monotonic by inspection, and extensive and reductive: extensive : ∀fxs.f(x)(s) v γ(α(f))(x)(s) γ(α(f))(x)(s) = * definition of α and γ + γm(λ〈x, s〉.αm(λ〈x, s〉.f(x)(s))(x, s))(x, s) = * η-reduction + γm(αm(λ〈x, s〉.f(x)(s)))(x, s) w * γm ◦ αm extensive + (λ〈x, s〉.f(x)(s))(x, s) = * β-reduction + f(x)(s)  reductive : ∀fxs.α(γ(f))(x)(s) v f(x)(s) α(γ(f))(x)(s) = * definition of α and γ + αm(λ〈x, s〉.γm(λ〈x, s〉.f(x)(s))(x, s))(x, s) = * η-reduction + αm(γm(λ〈x, s〉.f(x)(s))(x, s))(x, s) v * αm ◦ γm reductive + (λ〈x, s〉.f(x)(s))(x, s) 252 = * β-reduction + f(x)(s)  Finally, Property (1) commutes, assuming that A→ m1(B) −−−−→←−−−− αm γm A→ m2(B) is homomorphic: goal : St[s][m2](α m(f))(x)(s) = α(St[s][m1](f))(x)(s) α(St[s][m1](f))(x)(s) = * definition of α and St[s][m1] + αm(λ〈x, s〉.y ←m1 f(x) ; returnm1(y, s))(s, x) = * αm homomorphic on bindm1 and returnm1 + (λ〈x, s〉.y ←m1 αm(f)(x) ; returnm2(y, s))(s, x) = * β-reduction + y ←m2 αm(f)(x) ; returnm2(y, s) = * definition of St[s] + St[s][m2](α m(f))(s)(x)  State Property (2) The action ΠS t [s] on functions uses the mapping to monadic functions defined in Property (3): ΠS t [s] : (Σ(A)→ Σ(B))→ ΠSt [s](Σ)(A)→ ΠSt [s](Σ)(B) ΠS t [s](f)(ς) := γΣ↔m(St[s](αΣ↔m(f)))(ς) 253 To transport Galois connections, we assume Σ1(A)→ Σ1(B) −−−→←−−− αΣ γΣ Σ2(A)→ Σ2(B) and define α and γ as instantiations of αΣ and γΣ: α : (ΠS t [s](Σ1)(A)→ ΠSt [s](Σ1)(B))→ ΠSt [s](Σ2)(A)→ ΠSt [s](Σ2)(B) γ : (ΠS t [s](Σ2)(A)→ ΠSt [s](Σ2)(B))→ ΠSt [s](Σ1)(A)→ ΠSt [s](Σ1)(B) γ(f)(ς) := γΣ(f)(ς) α(f)(ς) := αΣ(f)(ς) Monotonicity, reductive and extensive properties carry over by definition. Finally, Property (2) commutes, assuming that αΣ and αm commute with both γΣ↔m and αΣ↔m: goal : ΠS t [s][Σ2](α Σ(f))(ς) = αΣ(ΠS t [s][Σ1](f))(ς) αΣ(ΠS t [s][Σ1](f)(ς) = * definition of ΠSt [s][Σ1] + αΣ(γΣ↔m(St[s](αΣ↔m(f))))(ς) = * definition of St[s] + αΣ(γΣ↔m(λx.λs.y ←m1 αΣ↔m(f)(x) ; returnm1(y, s)))(ς) = * αΣ and γΣ↔m commute + γΣ↔m(αm(λx.λs.y ←m1 αΣ↔m(f)(x) ; returnm1(y, s)))(ς) = * αm homomorphic + γΣ↔m(λx.λs.y ←m2 αm(αΣ↔m(f))(x) ; returnm2(y, s))(ς) = * αm and αΣ↔m commute + γΣ↔m(λx.λs.y ←m2 αΣ↔m(αΣ(f))(x) ; returnm2(y, s))(ς) 254 = * definition of St[s] + γΣ↔m(St[s](αΣ↔m(αΣ(f))))(ς) = * definition of ΠSt [s][Σ2] + ΠS t [s][Σ2](α Σ(f))(ς)  State Property (3) Assume a Galois connection Σ(A)→ Σ(B) −−−−−→←−−−−− αΣ↔m γΣ↔m A→ m(B). The Galois connection between St[s](m) and ΠSt [s](Σ) is defined: α : (ΠS t [s](Σ)(A)→ ΠSt [s](Σ)(B))→ A→ St[s](m)(B) γ : (A→ St[s](m)(B))→ ΠSt [s](Σ)(A)→ ΠSt [s](Σ)(B) α(f)(x)(s) := αΣ↔m(f)(x, s) γ(f)(ς) := γΣ↔m(λ〈x, s〉 → f(x)(s))(ς) α and γ are monotonic by inspection, and extensive and reductive: extensive : ∀fς.f(ς) v γ(α(f))(ς) γ(α(f))(ς) = * definition of α and γ + γΣ↔m(λ〈x, s〉 → αΣ↔m(f)(x, s))(ς) = * η-reduction + γΣ↔m(αΣ↔m(f))(ς) w * γΣ↔m ◦ αΣ↔m extensive + f(ς)  255 reductive : ∀fxs.α(γ(f))(x)(s) v f(x)(s) α(γ(f))(x)(s) = * definition of α and γ + αΣ↔m(γΣ↔m(λ〈x, s〉 → f(x)(s)))(x, s) v * αΣ↔m ◦ γΣ↔m reductive + (λ〈x, s〉 → f(x)(s))(x, s) = * β-reduction + f(x)(s)  Finally, Property (3) commutes: goal : ΠS t [s][Σ](γΣ↔m(f))(ς) v γ(St[s](f))(ς) ΠS t [s][Σ](γΣ↔m(f))(ς) = * definition of ΠSt [s][Σ] + γΣ↔m(λ〈x, s〉 → St[s](αΣ↔m(γΣ↔m(f)))(x)(s))(ς) v * αΣ↔m ◦ γΣ↔m reductive + γΣ↔m(λ〈x, s〉 → St[s](f)(x)(s))(ς) = * definition of γ + γ(St[s](f))(ς)  Nondeterminism ℘t is a Galois transformer. 256 Recall the definition of ℘t and Π℘ t : ℘t(m)(A) := m(℘(A)) Π℘ t (Σ)(A) := Σ(℘(A)) Nondeterminism Property (1) The action ℘t on functions: ℘t : (A→ m(B))→ A→ ℘t(m)(B) ℘t(f)(x) := y ←m f(x) ; returnm(y) To transport Galois connections, we assume a Galois connection A→ m1(B) −−−−→←−−−− αm γm A→ m2(B) define α and γ: α : (A→ ℘(m1)(B))→ A→ ℘(m2)(B) γ : (A→ ℘(m2)(B))→ A→ ℘(m1)(B) α(f)(x) := αm(λ{x1, . . . , xn}.f(x1) unionsqm1 · · · unionsqm1 f(xn))({x}) γ(f)(x) := γm(λ{x1, . . . , xn}.f(x1) unionsqm2 · · · unionsqm2 f(xn))({x}) α and γ are monotonic by inspection, and extensive and reductive: extensive : ∀fx.f(x) v γ(α(f))(x) γ(α(f))(x) = * definition of α and γ + γm(λ{x1, . . . , xn}. αm(λ{x1, . . . , xn}.f(x1) unionsqm1 · · · unionsqm1 f(xn))({x1}) unionsqm2 · · · unionsqm2 αm(λ{x1, . . . , xn}.f(x1) unionsqm1 · · · unionsqm1 f(xn))({xn}))({x}) 257 = * left-unit of m2 + γm(λ{x1, . . . , xn}. ({x1, . . . , xn} ←m2 returnm2({x1}) ; αm(λ{x1, . . . , xn}. f(x1) unionsqm1 · · · unionsqm1 f(xn))({x1, . . . , xn})) unionsqm2 · · · unionsqm2 ({x1, . . . , xn} ←m2 returnm2({xn}) ; αm(λ{x1, . . . , xn}. f(x1) unionsqm1 · · · unionsqm1 f(xn))({x1, . . . , xn})))({x}) w * αm ◦ γm reductive + γm(λ{x1, . . . , xn}. ({x1, . . . , xn} ←m2 αm(γm(returnm2({x1}))) ; αm(λ{x1, . . . , xn}.f(x1) unionsqm1 · · · unionsqm1 f(xn))({x1, . . . , xn})) unionsqm2 · · · unionsqm2 ({x1, . . . , xn} ←m2 αm(γm(returnm2({xn}))) ; αm(λ{x1, . . . , xn}.f(x1) unionsqm1 · · · unionsqm1 f(xn))({x1, . . . , xn})))({x}) = * αm and γm homomorphic on bindm2 and returnm2 + γm(λ{x1, . . . , xn}. (αm({x1, . . . , xn} ←m1 returnm1({x1}) ; f(x1) unionsqm1 · · · unionsqm1 f(xn))) unionsqm2 · · · unionsqm2 (αm({x1, . . . , xn} ←m1 returnm1({xn}) ; f(x1) unionsqm1 · · · unionsqm1 f(xn))))({x}) 258 = * join-semilattice functorality of m + γm(αm(λ{x1, . . . , xn}.{x1, . . . , xn} ← returnm1({x1, . . . , xn}) ; f(x1) unionsqm1 · · · unionsqm1 f(xn)))({x}) w * γm ◦ αm extensive + {x1, . . . , xn} ← returnm1({x}) ; f(x1) unionsqm1 · · · unionsqm1 f(xn) = * left-unit of m + f(x)  reductive : ∀fx.α(γ(f))(x) v f(x) α(γ(f))(x) = * definition of α and γ + αm(λ{x1, . . . , xn}. γm(λ{x1, . . . , xn}.f(x1) unionsqm2 · · · unionsqm2 f(xn))({x1}) unionsqm1 · · · unionsqm1 γm(λ{x1, . . . , xn}.f(x1) unionsqm2 · · · unionsqm2 f(xn))({xn}))({x}) 259 = * left-unit of m1 + αm(λ{x1, . . . , xn}. ({x1, . . . , xn} ←m1 returnm1({x1}) ; γm(λ{x1, . . . , xn}. f(x1) unionsqm2 · · · unionsqm2 f(xn))({x1, . . . , xn})) unionsqm1 · · · unionsqm1 ({x1, . . . , xn} ←m1 returnm1({x2}) ; γm(λ{x1, . . . , xn}. f(x1) unionsqm2 · · · unionsqm2 f(xn))({x1, . . . , xn})))({x}) v * γm ◦ αm extensive + αm(λ{x1, . . . , xn}. ({x1, . . . , xn} ←m1 γm(αm(returnm1({x1}))) ; γm(λ{x1, . . . , xn}.f(x1) unionsqm2 · · · unionsqm2 f(xn))({x1, . . . , xn})) unionsqm1 · · · unionsqm1 ({x1, . . . , xn} ←m1 γm(αm(returnm1({xn}))) ; γm(λ{x1, . . . , xn}.f(x1) unionsqm2 · · · unionsqm2 f(xn))({x1, . . . , xn})))({x}) = * αmandγm homomorphic on bindm1 and returnm1 + αm(λ{x1, . . . , xn}. γm({x1, . . . , xn} ←m2 returnm2({x1}) ; f(x1) unionsqm2 · · · unionsqm2 f(xn)) unionsqm1 · · · unionsqm1 γm({x1, . . . , xn} ←m2 returnm2({x1}) ; f(x1) unionsqm2 · · · unionsqm2 f(xn)))({x}) 260 = * join-semilattice functorailty of m + αm(γm(λ{x1, . . . , xn}.{x1, . . . , xn} ←m2 returnm2({x1, . . . , xn}) ; f(x1) unionsqm2 · · · unionsqm2 f(xn)))({x}) v * αm ◦ γm reductive + {x1, . . . , xn} ←m2 returnm2({x}) ; f(x1) unionsqm2 · · · unionsqm2 f(xn) = * left-unit of m + f(x)  Finally, Property (1) commutes, assuming that A→ m1(B) −−−−→←−−−− αm γm A→ m2(B) is homomorphic: goal : ∀fs.℘t[m2](αm(f))(x) = α(℘t[m1](f))(x) α(℘t[m1](f))(x) = * definition of α and ℘t[m1](f) + αm(λ{x1, . . . , xn}. (y ←m1 f(x1) ; returnm1({y})) unionsqm1 · · · unionsqm1 (y ←m1 f(xn) ; returnm1({y})))({x}) = * homomorphic on bindm1 and returnm1 + y ←m2 αm(f)(x) ; returnm2({y}) 261 = * definition of ℘t[m2] + ℘t[m2](α m(f))(x)  Nondeterminism Property (2) The action Π℘ t on functions uses the mapping to monadic functions defined in Property (3): Π℘ t : (Σ(A)→ Σ(B))→ Π℘t(Σ)(A)→ Π℘t(Σ)(B) Π℘ t (f)(ς) := γΣ↔γ(℘t(αΣ↔γ(f))) To transport Galois connections, we assume Σ1(A)→ Σ1(B) −−−→←−−− αΣ γΣ Σ2(A)→ Σ2(B) and define α and γ as instantiations of αΣ and γΣ: α : (Π℘ t (Σ1)(A)→ Π℘t(Σ1)(B))→ Π℘t(Σ2)(A)→ Π℘t(Σ2)(B) γ : (Π℘ t (Σ2)(A)→ Π℘t(Σ2)(B))→ Π℘t(Σ1)(A)→ Π℘t(Σ1)(B) α(f)(ς) := αΣ(f)(ς) γ(f)(ς) := γΣ(f)(ς) Monotonicity, reductive and extensive properties carry over by definition. Finally, Property (2) commutes, assuming that αΣ and αm commute with both γΣ↔m and αΣ↔m: goal : Π℘ t [Σ2](α Σ(f))(ς) = αΣ(Π℘ t [Σ1](f))(ς) αΣ(Π℘ t [Σ1](f))(ς) = * definition of Π℘t + αΣ(γΣ↔γ(℘t(αΣ↔γ(f))))(ς) = * definition of ℘t + αΣ(γΣ↔γ(λx.y ←m1 αΣ↔γ(f)(x) ; returnm1({y})))(ς) 262 = * αΣ and γΣ↔γ commute + γΣ↔γ(αm(λx.y ←m1 αΣ↔γ(f)(x) ; returnm1({y})))(ς) = * αm homomorphic on bindm1 and returnm2 + γΣ↔γ(λx.y ←m2 αm(αΣ↔γ(f))(x) ; returnm2({y}))(ς) = * αm and αΣ↔γ commute + γΣ↔γ(λx.y ←m2 αΣ↔γ(αΣ(f))(x) ; returnm2({y}))(ς) = * definition of Π℘t [Σ2] and αΣ + Π℘ t [Σ2](α Σ(f))(ς)  Nondeterminism Property (3) Assume a Galois connection Σ(A) → Σ(B) −−−−−→←−−−−− αΣ↔m γΣ↔m A→ m(B). The Galois connection between ℘t(m) and Π℘t(Σ) is: α : (Π℘ t (Σ)(A)→ Π℘t(Σ)(B))→ A→ ℘t(m)(B) γ : (A→ ℘t(m)(B))→ Π℘t(Σ)(A)→ Π℘t(Σ)(B) α(f)(x) := αΣ↔m(f)({x}) γ(f)(ς) := γΣ↔m(λ{x1, . . . , xn}.f(x1) unionsqm · · · unionsqm f(xn))(ς) α and γ are monotonic by inspection, and extensive and reductive: extensive : ∀fς.f(ς) v γ(α(f))(ς) γ(α(f))(ς) = * definition of α and γ + γΣ↔m(λ{x1, . . . , xn}.αΣ↔m(f)({x1}) unionsqm · · · unionsqm αΣ↔m(f)({xn}))(ς) 263 = * join-semilattice functorality of m + γΣ↔m(λ{x1, . . . , xn}.αΣ↔m(f)({x1, . . . , xn}))(ς) w * γΣ↔m ◦ αΣ↔m extensive and η-reduction + f(ς)  reductive : ∀fx.α(γ(f))(x) v f(x) α(γ(f))(x) = * definition of α and γ + αΣ↔m(γΣ↔m(λ{x1, . . . , xn}.f(x1) unionsqm · · · unionsqm f(xn)))({x}) v * αΣ↔m ◦ γΣ↔m reductive + (λ{x1, . . . , xn}.f(x1) unionsqm · · · unionsqm f(xn))({x}) = * β-reduction + f(x)  Finally, Property (3) commutes: goal : Π℘ t (γΣ↔m(f))(ς) v γ(℘t(f))(ς) Π℘ t (γΣ↔m(f))(ς) = * definition of Π℘t + γΣ↔m(℘t(αΣ↔m(γΣ↔m(f))))(ς) v * αΣ↔m ◦ γΣ↔m reductive + γΣ↔m(℘t(f))(ς) 264 = * definition of γ + γ(℘t(f))(ς)  Flow Sensitivity F t[s] is a Galois transformer. Recall the definition of F t[s] and ΠF t [s]: F t[s](m)(A) := s→ m([A 7→ s]) ΠF t [s](Σ)(A) := Σ([A 7→ s]) Flow Sensitivity Property (1) The action F t[s] on functions: F t[s] : (A→ m(B))→ A→ F t[s](m)(B) F t[s](f)(x)(s) := y ←m f(x) ; returnm({y 7→ s}) To transport Galois connections we assume A → m1(B) −−−−→←−−−− αm γm A → m2(B) and define α and γ: α : (A→ F t[s](m1)(B))→ A→ F t[s](m2)(B) γ : (A→ Ft[s](m2)(B))→ A→ F t[s](m1)(B) α(f)(x)(s) := αm(λ{x1 7→ s1, . . . , xn 7→ sn}. f(x1)(s1) unionsqm · · · unionsqm f(xn)(sn))({x 7→ s}) γ(f)(x)(s) := γm(λ{x1 7→ s1, . . . , xn 7→ sn}. f(x1)(s1) unionsqm · · · unionsqm f(xn)(sn))({x 7→ s}) α and γ are monotonic by inspection. α and γ are extensive and reductive: extensive : ∀fxs.f(x)(s) v γ(α(f))(x)(s) γ(α(f))(x)(s) 265 = * definition of α and γ + γm(λ{x1 7→ s1, . . . , xn 7→ sn}. αm(λ{x1 7→ s1, . . . , xn 7→ sn}. f(x1)(s1) unionsqm1 · · · unionsqm1 f(xn)(sn))({x1 7→ s1}) unionsqm2 · · · unionsqm2 αm(λ{x1 7→ s1, . . . , xn 7→ sn}. f(x1)(s1) unionsqm1 · · · unionsqm1 f(xn)(sn))({xn 7→ sn}))({x 7→ s}) w * left-unit of m and αm ◦ γm reductive + γm(λ{x1 7→ s1, . . . , xn 7→ sn}. ({x1 7→ s1, . . . , xn 7→ sn} ←m2 αm(γm(returnm2({x1 7→ s1}))) ; αm(f(x1)(s1) unionsqm1 · · · unionsqm1 f(xn)(sn))) unionsqm2 · · · unionsqm2 ({x1 7→ s1, . . . , xn 7→ sn} ←m2 αm(γm(returnm2({xn 7→ sn}))) ; αm(f(x1)(s1) unionsqm1 · · · unionsqm1 f(xn)(sn))))({x 7→ s}) = * αm and γm homomorphic and join functorality + γm(αm(λ{x1 7→ s1, . . . , xn 7→ sn}. {x1 7→ s1, . . . , xn 7→ sn} ←m1 returnm1({x1 7→ s1, . . . , xn 7→ sn}) ; f(x1)(s1) unionsqm1 · · · unionsqm1 f(xn)(sn)))({x 7→ s}) w * γm ◦ αm extensive and left-unit of m + f(x)(s)  266 reductive : ∀fxs.α(γ(f))(x)(s) v f(x)(s) α(γ(f))(x)(s) = * definition of α and γ + αm(λ{x1 7→ s1, . . . , xn 7→ sn}. γm(λ{x1 7→ s1, . . . , xn 7→ sn}. f(x1)(s1) unionsqm2 · · · unionsqm2 f(xn)(sn))({x1 7→ s1}) unionsqm1 · · · unionsqm1 γm(λ{x1 7→ s1, . . . , xn 7→ sn}. f(x1)(s1) unionsqm2 · · · unionsqm2 f(xn)(sn))({xn 7→ sn}))({x 7→ s}) v * left-unit of m and γm ◦ αm extensive + αm(λ{x1 7→ s1, . . . , xn 7→ sn}. ({x1 7→ s1, . . . , xn 7→ sn} ←m1 γm(αm(returnm1({x1 7→ s1}))) ; γm(f(x1)(s1) unionsqm2 · · · unionsqm2 f(xn)(sn))) unionsqm1 · · · unionsqm1 ({x1 7→ s1, . . . , xn 7→ sn} ←m1 γm(αm(returnm1({xn 7→ sn}))) ; γm(f(x1)(s1) unionsqm2 · · · unionsqm2 f(xn)(sn))))({x 7→ s}) = * αm and γm homomorphic and join functorality + αm(γm(λ{x1 7→ s1, . . . , xn 7→ sn}. {x1 7→ s1, . . . , xn 7→ sn} ←m2 returnm2({x1 7→ s1, . . . , xn 7→ sn}) ; f(x1)(s1) unionsqm2 · · · unionsqm2 f(xn)(sn)))({x 7→ s}) 267 v * αm ◦ γm extensive and left-unit of m + f(x)(s)  Finally, Property (1) commutes, assuming that A→ m1(B) −−−−→←−−−− αm γm A→ m2(B) is homomorphic: goal : ∀fs.F t[s][m2](αm(f))(x)(s) = α(F t[s][m1](f))(x)(s) α(F t[s][m1](f))(x)(s) = * definition of α and F t[s][m1] + αm(λ{x1 7→ s1, . . . , xn 7→ sn}. (y ←m1 f(x) ; returnm1(y1)(s1)) unionsqm1 · · · unionsqm1 (y ←m1 f(x) ; returnm1(yn)(sn)))({x 7→ s}) = * homomorphic on bindm1 and returnm1 + y ←m2 αm(f)(x) ; returnm2(y)(s) = * definition of F t[s][m2] + F t[s][m2](α m(f))(x)  Flow Sensitivity Property (2) The action ΠF t[s] on functions uses the mapping to monadic functions defined in Property (3): ΠF t [s] : (Σ(A)→ Σ(B))→ ΠF t [s](Σ)(A)→ ΠF t [s](Σ)(B) ΠF t [s](f)(ς) := γΣ↔γ(F t[s](αΣ↔γ(f))) 268 To transport Galois connections, we assume Σ1(A)→ Σ1(B) −−−→←−−− αΣ γΣ Σ2(A)→ Σ2(B) and define α and γ as instantiations of αΣ and γΣ: α : (ΠF t [s](Σ1)(A)→ ΠF t [s](Σ1)(B))→ ΠF t [s](Σ2)(A)→ ΠF t [s](Σ2)(B) γ : (ΠF t [s](Σ2)(A)→ ΠF t [s](Σ2)(B))→ ΠF t [s](Σ1)(A)→ ΠF t [s](Σ1)(B) α(f)(ς) := αΣ(f)(ς) γ(f)(ς) := γΣ(f)(ς) Monotonicity, reductive and extensive properties carry over by definition. Finally, Property (2) commutes, assuming that αΣ and αm commute with both γΣ↔m and αΣ↔m: goal : ΠF t [s][Σ2](α Σ(f))(ς) = αΣ(ΠF t [s][Σ1](f))(ς) αΣ(ΠF t [s][Σ1](f))(ς) = * definition of ΠF t [s] + αΣ(γΣ↔γ(F t[s](αΣ↔γ(f))))(ς) = * definition of F t[s] + αΣ(γΣ↔γ(λx.λs.y ←m1 αΣ↔γ(f)(x) ; returnm1({y 7→ s})))(ς) = * αΣ and γΣ↔γ commute + γΣ↔γ(αm(λx.λs.y ←m1 αΣ↔γ(f)(x) ; returnm1({y 7→ s})))(ς) = * αm homomorphic + γΣ↔γ(λx.λs.y ←m2 αm(αΣ↔γ(f))(x) ; returnm2({y 7→ s}))(ς) = * αm and αΣ↔γ commute + γΣ↔γ(λx.λs.y ←m2 αΣ↔γ(αΣ(f))(x) ; returnm2({y 7→ s}))(ς) 269 = * definition of Π℘t [Σ2] and αΣ + Π℘ t [Σ2](α Σ(f))(ς)  Flow Sensitivity Property (3) Assume a Galois connection: Σ(A)→ Σ(B) −−−−−→←−−−−− αΣ↔m γΣ↔m A→ m(B) The Galois connection between F t[s](m) and ΠF t [s](Σ) is: α : (ΠF t [s](Σ)(A)→ ΠF t [s](Σ)(B))→ A→ F t[s](m)(B) γ : (A→ F t[s](m)(B))→ ΠF t [s](Σ)(A)→ ΠF t [s](Σ)(B) α(f)(x)(s) := αΣ↔m(f)({x 7→ s}) γ(f)(ς) := γΣ↔m(λ{x1 7→ s1, . . . , xn 7→ sn}.f(x1)(s1) unionsqm · · · unionsqm f(xn)(sn))(ς) α and γ are monotonic by inspection. α and γ are extensive and reductive: extensive : ∀fς.f(ς) v γ(α(f))(ς) γ(α(f))(ς) = * definition of α and γ + γΣ↔m(λ{x1 7→ s1, . . . , xn 7→ sn}. αΣ↔m(f)({x1 7→ s1}) unionsqm · · · unionsqm αΣ↔m(f)({xn 7→ sn}))(ς) = * join-semilattice functorality of m + γΣ↔m(αΣ↔m(f))(ς) w * γΣ↔m ◦ αΣ↔m extensive + f(ς)  270 reductive : ∀fx.α(γ(f))(x)(s) v f(x)(s) α(γ(f))(x)(s) = * definition of α and γ + αΣ↔m(γΣ↔m(λ{x1 7→ s1, . . . , xn 7→ sn}. f(x1)(s1) unionsqm · · · unionsqm f(xn)(sn)))({x 7→ s}) v * αΣ↔m ◦ γΣ↔m reductive + (λ{x1 7→ s1, . . . , xn 7→ sn}.f(x1)(s1) unionsqm · · · unionsqm f(xn)(sn))({x 7→ s}) = * β-reduction + f(x)(s)  Finally, Property (3) commutes: goal : ΠF t [s](γΣ↔m(f))(ς) v γ(F t[s](f))(ς) ΠF t [s](γΣ↔m(f))(ς) = * definition of ΠF t [s] + γΣ↔m(F t[s](αΣ↔m(γΣ↔m(f))))(ς) v * αΣ↔m ◦ γΣ↔m reductive + γΣ↔m(F t[s](f))(ς) = * definition of γ + γ(F t[s](f))(ς)  271 A.0.2 Lemma 3 [P t laws] (Section 5.8.2) bindP t and returnP t satisfy monad laws, getP t and putP t satisfy state monad laws, and mzeroP t and Pt satisfy nondeterminism monad laws: left-unit : ∀fx.bindPt(returnPt(x))(f) = f(x) bindP t (returnP t (x))(f) = * definition of bindPt + {x1, . . . , xn} ←m returnPt(x) ; f(x1) unionsqm · · · unionsqm f(xn) = * definition of returnPt + {x1, . . . , xn} ←m returnm({x}) ; f(x1) unionsqm · · · unionsqm f(xn) = * do-notation for m + bindm(returnm({x}))(λ{x1, . . . , xn}.f(x1) unionsqm · · · unionsqm f(xn)) = * left-unit for m + f(x)  right-unit : ∀X.bindPt(X)(returnPt) = X bindP t (X)(returnP t ) = * definition of bindPt + {x1, . . . , xn} ←m X ; returnPt(x1) unionsqm · · · unionsqm returnPt(xn) = * definition of returnPt + {x1, . . . , xn} ←m X ; returnm({x1}) unionsqm · · · unionsqm returnm({xn}) 272 = * join-semilattice functorality of m distribution over returnm + {x1, . . . , xn} ←m X ; returnm({x1} ∪ · · · ∪ {xn}) = * definition of ∪ + {x1, . . . , xn} ←m X ; returnm({x1, . . . , xn}) = * do-notation for m + bindm(X)(returnm) = * right-unit for m + X  associativity : ∀fgX.bindPt(bindPt(X)(f))(g) = bindPt(X)(λx.bindPt(f(x))(g)) bindP t (bindP t (X)(f))(g) = * definition of bindPt + {y1, . . . , yn} ←m bindPt(X)(f) ; g(y1) unionsqm · · · unionsqm g(yn) = * definition of bindPt + {y1, . . . , yn} ←m ({x1, . . . , xn} ←m X ; f(x1) unionsqm · · · unionsqm f(xn)) ; g(y1) unionsqm · · · unionsqm g(yn) = * do-notation for m + {y1, . . . , yn} ←m bindm(X)(λ{x1, . . . , xn}.f(x1) unionsqm · · · unionsqm f(xn)) ; g(y1) unionsqm · · · unionsqm g(yn) 273 = * do-notation for m + bindm(bindm(X)(λ{x1, . . . , xn}.f(x1) unionsqm · · · unionsqm f(xn))) (λ{y1, . . . , yn}.g(y1) unionsqm · · · unionsqm g(yn)) = * associativity for m + bindm(X)(λ{x1, . . . , xn}.bindm(f(x1) unionsqm · · · unionsqm f(xn)) (λ{y1, . . . , yn}.g(y1) unionsqm · · · unionsqm g(yn))) = * do-notation for m + {x1, . . . , xn} ←m X ; bindm(f(x1) unionsqm · · · unionsqm f(xn))(λ{y1, . . . , yn}.g(y1) unionsqm · · · unionsqm g(yn)) = * do-notation for m + {x1, . . . , xn} ←m X ; {y1, . . . , yn} ←m (f(x1) unionsqm · · · unionsqm f(xn)) ; g(y1) unionsqm · · · unionsqm g(yn) = * join-semilattice functorality of m distribution over bindm + {x1, . . . , xn} ←m X ; ({y1, . . . , yn} ←m f(x1) ; g(y1) unionsqm · · · unionsqm g(yn)) unionsqm · · · unionsqm ({y1, . . . , yn} ←m f(xn) ; g(y1) unionsqm · · · unionsqm g(yn)) = * definition of bindPt + {x1, . . . , xn} ←m X ; bindPt(f(x1))(g) unionsqm · · · unionsqm bindPt(f(xn)(g)) 274 = * definition of bindPt + bindP t (X)(λx.bindP t (f(x))(g))  get-get : s1 ← getPt ; s2 ← getPt ; returnPt(s1, s2) = s← getPt ; returnPt(s, s) s1 ← getPt ; s2 ← getPt ; returnPt(s1, s2) = * definition of getPt + s1 ← (s←m getm ; return({s})) ; s2 ← (s← getm ; return({s})) ; returnPt(s1, s2) = * definition of returnPt + s1 ← (s←m getm ; return({s})) ; s2 ← (s← getm ; return({s})) ; returnm({〈s1, s2〉}) = * do-notation for m and definition of bindPt + {s11, . . . , s1n} ←m (s←m getm ; return({s})) ; (s2 ← (s←m getm ; return({s})) ; returnm({〈s11, s2〉})) unionsqm · · · unionsqm (s2 ← (s←m getm ; return({s})) ; returnm({〈s1n, s2〉})) = * associativity and left-unit of m + s1 ←m getm ; (s2 ← (s←m getm ; return({s})) ; returnm({〈s1, s2〉})) 275 = * do-notation for m and definition of bindPt + s1 ←m getm ; {s21, . . . , s2n} ←m (s←m getm ; return({s})) ; returnm({〈s1, s21〉}) unionsqm · · · unionsqm returnm({〈s1, s2n〉}) = * associativity and left-unit of m + s1 ←m getm ; s2 ←m getm ; returnm({〈s1, s2〉}) = * associativity and left-unit of m + p←m (s1 ←m getm ; s2 ←m getm ; returnm(s1, s2)) ; returnm({p}) = * get-get of m + p←m (s← getm ; returnm(s, s)) ; returnm({p}) = * associativity and left-unit of m + s←m getm ; returnm({〈s, s〉}) = * associativity and left-unit of m + {s1, . . . , sn} ←m (s←m getm ; returnm({s})) ; returnm({〈s1, s1〉}) unionsqm · · · unionsqm returnm({〈sn, sn〉}) = * definition of getPt and returnPt + s← getPt ; returnPt(s, s)  get-put : (s← getPt ; putPt(s)) = return(•) put-get : ∀s.(• ← putPt(s) ; getPt) = (• ← putPt(s) ; returnPt(s)) put-put : ∀s1s2.(• ← putPt(s1) ; putPt(s2)) = putPt(s2) 276 get-put, put-get and put-put are analogous to get-get ; they follow from monad associativity and the property from the underlying monad. mzero-unit : ∀X.mzeroPt Pt X = X -associativity : ∀XY Z.(X Pt Y )Pt ZPt = X Pt (Y Pt Z) -commutativity : ∀XY.X Pt Y = Y Pt X -idempotence : ∀X.X Pt X = X mzero-left-zero : ∀k.(x← mzeroPt ; k(x)) = mzeroPt mzero-right-zero : ∀X.(x← X ; mzeroPt) = mzeroPt -distributivity : ∀XY k. (x← X Pt Y ; k(x)) = (x← X ; k(x))Pt (x← Y ; k(x)) These follow directly from the definition of mzeroP t and Pt and the join-semilattice properties from the underlying monad. A.0.3 Lemma 4 [F t laws] (Section 5.8.3) bindF t and returnF t satisfy monad laws, getF t and putF t satisfy state monad laws, and mzeroF t and F t satisfy nondeterminism monad laws. We go into slightly less detail in these proofs than was done for P t. left-unit : ∀fxs.bindF t(returnF t(x))(f)(s) = f(x)(s) bindF t (returnF t (x))(f)(s) 277 = * definition of bindF t and returnF t + {x1 7→ s1, . . . , xn 7→ sn} ←m returnm({x 7→ s}) ; f(x1)(s1) unionsqm · · · unionsqm f(xn)(sn) = * left-unit for m + f(x)(s)  right-unit : ∀Xs.bindF t(X)(returnF t)(s) = X(s) bindF t (X)(returnF t )(s) = * definition of bindF t and returnF t + {x1 7→ s1, . . . , xn 7→ sn} ←m X(s) ; returnm({x1 7→ s1}) unionsqm · · · unionsqm returnm({xn 7→ sn}) = * join-semilattice functorality of m distribution over returnm + {x1 7→ s1, . . . , xn 7→ sn} ←m X(s) ; returnm({x1 7→ s1, . . . , xn 7→ sn}) = * right-unit of m + X(s)  associativity : ∀fgXs. bindF t (bindF t (X)(f))(g)(s) = bindF t (X)(λx.bindF t (f(x))(g))(s) bindF t (bindF t (X)(f))(g)(s) = * definition of bindF t + {y1 7→ sy1, . . . , yn 7→ syn} ←m ({x1 7→ sx1, . . . , xn 7→ sxn} ← X(s) ; f(x1)(sx1) unionsqm · · · unionsqm f(xn)(sxn)) ; g(y1)(sy1) unionsqm · · · unionsqm g(yn)(syn) 278 = * associativity of m + {x1 7→ sx1, . . . , xn 7→ sxn} ←m X(s) ; {y1 7→ sy1, . . . , yn 7→ syn} ←m f(x1)(sx1) unionsqm · · · unionsqm f(xn)(sxn) ; g(y1)(sy1) unionsqm · · · unionsqm g(yn)(syn) = * join-semilattice functorality of m distribution over bindm + {x1 7→ sx1, . . . , xn 7→ sxn} ←m X(s) ; ({y1 7→ sy1, . . . , yn 7→ syn} ←m f(x1)(sx1) ; g(y1)(sy1) unionsqm · · · unionsqm g(yn)(syn)) unionsqm · · · unionsqm ({y1 7→ sy1, . . . , yn 7→ syn} ←m f(xn)(sxn) ; g(y1)(sy1) unionsqm · · · unionsqm g(yn)(syn)) = * definition of bindF t + bindF t (X)(λx.bindF t (f(x))(g))(s)  get-get : ∀s. (s1 ← getF t ; s2 ← getF t ; returnF t(s1, s2))(s) = (s← getF t ; returnF t(s, s))(s) (s1 ← getF t ; s2 ← getF t ; returnF t(s1, s2))(s) = * definition of bindF t and getF t + {x1 7→ sx1, . . . , xn 7→ sxn} ←m returnm{s 7→ s} ; (s2 ← getF t ; returnF t(x1, s2))(sx1) unionsqm · · · unionsqm (s2 ← getF t ; returnF t(xn, s2))(sxn) 279 = * left-unit of m + (s2 ← getF t ; returnF t(s, s2))(s) = * definition of bindF t and getF t + {x1 7→ sx1, . . . , xn 7→ sxn} ←m returnm{s 7→ s} ; returnF t (s, x1)(sx1) unionsqm · · · unionsqm returnF t(s, xn)(sxn) = * left-unit of m + returnF t (s, s)(s) = * left-unit of m and definition of getF t + (s← getF t ; returnF t(s, s))(s)  get-put : ∀s.(s1 ← getF t ; putF t(s1))(s) = return(•)(s) (s1 ← getF t ; putF t(s1))(s) = * definition of bindF t and getF t + {x1 7→ sx1, . . . , xn 7→ sxn} ←m returnm({s 7→ s}) ; putF t (x1)(sx1) unionsqm · · · unionsqm putF t(xn)(sxn) = * right-unit of m + putF t (s)(s) = * definition of putF t + returnm({• 7→ s}) = * definition of returnF t + returnF t (•)(s)  280 put-get : ∀ss1.(• ← putF t(s1) ; getF t)(s) = (• ← putF t(s1) ; returnF t(s1))(s) (• ← putF t(s1) ; getF t)(s) = * definition of bindF t and putF t + {• 7→ s} ←m returnm{• 7→ s1} ; getF t(s) = * right-unit of m + getF t (s1) = * definition of getF t and returnF t + returnF t (s1)(s1) = * definition of bindF t and putF t + (• ← putF t(s1) ; returnFt(s1))(s)  put-put : ∀ss1s2.(• ← putF t(s1) ; putF t(s2))(s) = putF t(s2)(s) (• ← putFt(s1) ; putF t(s2))(s) = * definition of bindF t and putF t + {• 7→ s} ←m returnm{• 7→ s1} ; returnm{• 7→ s2} = * right-unit of m + returnm{• 7→ s2} = * definition of putF t + putF t (s2)(s)  mzero-unit : ∀X.mzeroF t F t X = X -associativity : ∀XY Z.(X F t Y )F t ZF t = X F t (Y F t Z) 281 -commutativity : ∀XY.X F t Y = Y F t X -idempotence : ∀X.X F t X = X mzero-left-zero : ∀k.(x← mzeroF t ; k(x)) = mzeroF t mzero-right-zero : ∀X.(x← X ; mzeroF t) = mzeroF t -distributivity : ∀XY k. (x← X F t Y ; k(x)) = (x← X ; k(x))F t (x← Y ; k(x)) These follow directly from the definition of mzeroF t and F t and the join-semilattice properties from the underlying monad. 282 Bibliography Mads Sig Ager, Olivier Danvy, and Jan Midtgaard. A functional correspondence between monadic evaluators and abstract machines for languages with computa- tional effects. In Theoretical Computer Science (TCS). Elsevier Science Publishers Ltd., Essex, UK, 2005. Lars Ole Andersen. Program Analysis and Specialization for the C Programming Language. PhD thesis, DIKU, University of Copenhagen, 1994. J. W. Backus. The syntax and semantics of the proposed international algebraic language of the Zurich ACM-GAMM conference. In International Conference on Information Processsing (ICIP). UNESCO, Paris, France, 1959. Gilles Barthe, David Pichardie, and Tamara Rezk. A certified lightweight non- interference Java bytecode verifier. In European Symposium on Programming (ESOP). Springer-Verlag, Berlin, Heidelberg, 2007. Richard Bird and Oege de Moor. The Algebra of Programming. Prentice Hall, Upper Saddle River, NJ, USA, 1996. Richard S. Bird. A calculus of functions for program derivation. In Research Topics in Functional Programming. Addison-Wesley Longman Publishing Co., Inc., Boston, MA, USA, 1990. Bruno Blanchet, Patrick Cousot, Radhia Cousot, Je´rome Feret, Laurent Mauborgne, Antoine Mine´, David Monniaux, and Xavier Rival. A static analyzer for large safety-critical software. In Programming Language Design and Implementation (PLDI). ACM, New York, NY, USA, 2003. Sandrine Blazy, Vincent Laporte, Andre´ Maroneze, and David Pichardie. Formal verification of a C value analysis based on abstract interpretation. In Static Analysis Symposium (SAS). Springer-Verlag, Berlin, Heidelberg, 2013. Roland Bol and Lars Degerstedt. Tabulated resolution for well founded semantics. In International Logic Programming Symposium (ILPS). MIT Press, Cambridge, MA, USA, 1993. 283 David Cachera and David Pichardie. A certified denotational abstract interpreter. In Interactive Theorem Proving (ITP). Springer-Verlag, Berlin, Heidelberg, 2010. David R. Chase, Mark Wegman, and F. Kenneth Zadeck. Analysis of pointers and structures. In Programming Language Design and Implementation (PLDI). ACM, New York, NY, USA, 1990. Weidong Chen and David S. Warren. Tabled evaluation with delaying for general logic programs. In Journal of the ACM (JACM). ACM, New York, NY, USA, 1996. Thierry Coquand and Gerard Huet. The calculus of constructions. In Information and Computation: Semantics of Data Types. Academic Press, Inc., Duluth, MN, USA, 1988. Thierry Coquand and Ge´rard P. Huet. Constructions: A higher order proof system for mechanizing mathematics. In European Conference on Computer Algebra (EUROCAL). Springer-Verlag, London, UK, 1985. Thierry Coquand and Christine Paulin. Inductively defined types. In International Conference on Computer Logic (COLOG). Springer-Verlag, London, UK, 1990. Patrick Cousot. The calculational design of a generic abstract interpreter. In Calculational System Design, NATO ASI Series F. IOS Press, Amsterdam, The Netherlands, 1999. Patrick Cousot. Abstract interpretation. MIT Course 16.399, 2005. URL http: //web.mit.edu/16.399/www/. Patrick Cousot. Abstract interpretation, 2008. URL http://www.di.ens.fr/ ~cousot/AI/. Patrick Cousot and Radhia Cousot. Static determination of dynamic properties of programs. In International Symposium on Programming (ISOP). Dunod, Paris, France, 1976. Patrick Cousot and Radhia Cousot. Abstract interpretation: A unified lattice model for static analysis of programs by construction or approximation of fixpoints. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 1977. Patrick Cousot and Radhia Cousot. Systematic design of program analysis frame- works. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 1979. Patrick Cousot and Radhia Cousot. Inductive definitions, semantics and abstract interpretations. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 1992. 284 Patrick Cousot and Radhia Cousot. Higher-order abstract interpretation (and appli- cation to comportment analysis generalizing strictness, termination, projection and PER analysis of functional languages), invited paper. In International Conference on Computer Languages (ICCL). IEEE Computer Society Press, Los Alamitos, CA, USA, 1994. Patrick Cousot and Radhia Cousot. A Galois connection calculus for abstract interpretation. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 2014. David Darais and David Van Horn. Constructive Galois connections: Taming the Galois connection framework for mechanized metatheory. In International Conference on Functional Programming (ICFP). ACM, New York, NY, USA, 2016. David Darais, Matthew Might, and David Van Horn. Galois transformers and modular abstract interpreters: Reusable metatheory for program analysis. In Object-Oriented Programming, Systems, Languages and Applications (OOPSLA). ACM, New York, NY, USA, 2015. David Darais, Nicholas Labich, Phu´c C. Nguye˜ˆn, and David Van Horn. Definitional abstract interpreters for higher-order programming languages. In International Conference on Functional Programming (ICFP). ACM, New York, NY, USA, 2017. Manuvir Das, Sorin Lerner, and Mark Seigle. ESP: Path-sensitive program verification in polynomial time. In Programming Language Design and Implementation (PLDI). ACM, New York, NY, USA, 2002. Steven Dawson, C. R. Ramakrishnan, and David S. Warren. Practical program analysis using general purpose logic programming systems—a case study. In Programming Language Design and Implementation (PLDI). ACM, New York, NY, USA, 1996. Benjamin Delaware, Cle´ment Pit-Claudel, Jason Gross, and Adam Chlipala. Fiat: Deductive synthesis of abstract data types in a proof assistant. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 2015. The Coq development team. The Coq proof assistant reference manual. LogiCal Project, 2004. Christopher Earl. Introspective Pushdown Analysis and Nebo. PhD thesis, University of Utah, 2014. Christopher Earl, Matthew Might, and David Van Horn. Pushdown control-flow analysis of higher-order programs. In Workshop on Scheme and Functional Programming (Scheme), 2010. Christopher Earl, Ilya Sergey, Matthew Might, and David Van Horn. Introspective pushdown analysis of higher-order programs. In International Conference on Functional Programming (ICFP). ACM, New York, NY, USA, 2012. 285 Matthias Felleisen and Robert Hieb. The revised report on the syntactic theories of sequential control and state. In Theoretical Computer Science (TCS). Elsevier Science Publishers Ltd., Essex, UK, 1992. Mattias Felleisen and Daniel P. Friedman. A calculus for assignments in higher-order languages. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 1987. Mattias Felleisen, Daniel P. Friedman, Eugene Kohlbecker, and Bruce Duba. A syntactic theory of sequential control. In Theoretical Computer Science (TCS). Elsevier Science Publishers Ltd., Essex, UK, 1987. Sebastian Fischer, Oleg Kiselyov, and Chung-chieh Shan. Purely functional lazy non-deterministic programming. In International Conference on Functional Pro- gramming (ICFP). ACM, New York, NY, USA, 2009. Matthew Flatt and Matthias Felleisen. Units: Cool modules for HOT languages. In Programming Language Design and Implementation (PLDI). ACM, New York, NY, USA, 1998. Matthew Flatt and PLT. Reference: Racket. Technical report, PLT Design Inc., 2010. URL https://racket-lang.org/tr1/. Ronald Garcia, Alison M. Clark, and E´ric Tanter. Abstracting gradual typing. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 2016. Jeremy Gibbons and Ralf Hinze. Just do it: Simple monadic equational reasoning. In International Conference on Functional Programming (ICFP). ACM, New York, NY, USA, 2011. Thomas Gilray, Michael D. Adams, and Matthew Might. Allocation characterizes polyvariance: A unified methodology for polyvariant control-flow analysis. In International Conference on Functional Programming (ICFP). ACM, New York, NY, USA, 2016a. Thomas Gilray, Steven Lyde, Michael D. Adams, Matthew Might, and David Van Horn. Pushdown control-flow analysis for free. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 2016b. Robert Glu¨ck. Simulation of two-way pushdown automata revisited. In Electronic Proceedings in Theoretical Computer Science (EPTCS), volume Semantics, Ab- stract Interpretation, and Reasoning about Programs: Essays Dedicated to David A. Schmidt on the Occasion of his Sixtieth Birthday (Festschrift for Dave Schmidt). Open Publishing Association, 2013. Ben Hardekopf, Ben Wiedermann, Berkeley Churchill, and Vineeth Kashyap. Widen- ing for control-flow. In Verification, Model Checking, and Abstract Interpretation (VMCAI). Springer-Verlag New York, Inc., New York, NY, USA, 2014. 286 Michael Hind. Pointer analysis: Haven’t we solved this problem yet? In Program Analysis for Software Tools and Engineering (PASTE). ACM, New York, NY, USA, 2001. Ralf Hinze. Deriving backtracking monad transformers. In International Conference on Functional Programming (ICFP). ACM, New York, NY, USA, 2000. Suresh Jagannathan and Stephen Weeks. A unified treatment of flow analysis in higher-order languages. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 1995. Suresh Jagannathan, Peter Thiemann, Stephen Weeks, and Andrew Wright. Single and loving it: Must-alias analysis for higher-order languages. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 1998. Gerda Janssens and Konstantinos Sagonas. On the use of tabling for abstract interpretation: An experiment with abstract equation systems. In Tabulation in Parsing and Deduction (TAPD), 1998. Mauro Javier Jaskelioff. Lifting of Operations in Modular Monadic Semantics. PhD thesis, University of Nottingham, 2009. James Ian Johnson and David Van Horn. Abstracting abstract control. In Symposium on Dynamic Languages (DLS). ACM, New York, NY, USA, 2014. James Ian Johnson, Ilya Sergey, Christopher Earl, Matthew Might, and David Van Horn. Pushdown flow analysis with abstract garbage collection. In Journal of Functional Programming (JFP). Cambridge University Press, Cambridge, UK, 2014. Neil D. Jones. Flow analysis of lambda expressions (preliminary version). In International Colloquium on Automata, Languages and Programming (ICALP). Springer-Verlag, London, UK, 1981. Neil D. Jones and Flemming Nielson. Abstract interpretation: A semantics-based tool for program analysis. In Handbook of Logic in Computer Science. Oxford University Press, Oxford, UK, 1995. Jacques-Henri Jourdan, Vincent Laporte, Sandrine Blazy, Xavier Leroy, and David Pichardie. A formally-verified C static analyzer. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 2015. George Kastrinis and Yannis Smaragdakis. Hybrid context-sensitivity for points-to analysis. In Programming Language Design and Implementation (PLDI). ACM, New York, NY, USA, 2013. James C. King. Symbolic execution and program testing. In Communications of the ACM (CACM). ACM, New York, NY, USA, 1976. 287 Oleg Kiselyov. Typed tagless final interpreters. In Spring School Conference on Generic and Indexed Programming (SSGIP). Springer-Verlag, Berlin, Heidelberg, 2010. Oleg Kiselyov, Chung-chieh Shan, Daniel P. Friedman, and Amr Sabry. Backtrack- ing, interleaving, and terminating monad transformers: (functional pearl). In International Conference on Functional Programming (ICFP). ACM, New York, NY, USA, 2005. Xavier Leroy. Formal verification of a realistic compiler. In Communications of the ACM (CACM). ACM, New York, NY, USA, 2009. Sheng Liang, Paul Hudak, and Mark Jones. Monad transformers and modular interpreters. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 1995. Gregory Malecha and Jesper Bengtson. Extensible and efficient automation through reflective tactics. In Programming Languages and Systems (PLAS). Springer-Verlag New York, Inc., New York, NY, USA, 2016. Per Martin-Lo¨f. An intuitionistic theory of types: Predicative part. In Studies in Logic and the Foundations of Mathematics (SLFM). Elsevier, Amsterdam, The Netherlands, 1975. Per Martin-Lo¨f. Intuitionistic type theory. In Studies in Proof Theory. Bibliopolis, Naples, Italy, 1984. Jan Midtgaard. Control-flow analysis of functional programs. In ACM Computing Surveys (CSUR). ACM, New York, NY, USA, 2012. Jan Midtgaard and Thomas Jensen. A calculational approach to control-flow analysis by abstract interpretation. In Static Analysis Symposium (SAS). Springer-Verlag, Berlin, Heidelberg, 2008. Jan Midtgaard and Thomas P. Jensen. Control-flow analysis of function calls and returns by abstract interpretation. In International Conference on Functional Programming (ICFP). ACM, New York, NY, USA, 2009. Matthew Might. Environment Analysis of Higher-order Languages. PhD thesis, Georgia Institute of Technology, 2007a. Matthew Might. Logic-flow analysis of higher-order programs. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 2007b. Matthew Might and Olin Shivers. Improving flow analyses via γCFA: Abstract garbage collection and counting. In International Conference on Functional Programming (ICFP). ACM, New York, NY, USA, 2006a. 288 Matthew Might and Olin Shivers. Environment analysis via δCFA. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 2006b. Matthew Might and David Van Horn. Family of abstract interpretations for static analysis of concurrent higher-order programs. In Static Analysis Symposium (SAS). Springer-Verlag, Berlin, Heidelberg, 2011. Ana Milanova, Atanas Rountev, and Barbara G. Ryder. Parameterized object sensitivity for points-to analysis for Java. In Transactions on Software Engineering and Methodology (TOSEM). ACM, New York, NY, USA, 2005. Antoine Mine´. The octagon abstract domain. In Higher Order and Symbolic Computation (HOSC). Kluwer Academic Publishers, Dordrecht, The Netherlands, 2006. Eugenio Moggi. An abstract view of programming languages. Technical report, University of Edinburgh, 1989. David Monniaux. Re´alisation me´canise´e d’interpre´teurs abstraits. Rapport de DEA, Universite´ Paris VII, 1998. In French. Phu´c C. Nguye˜ˆn and David Van Horn. Relatively complete counterexamples for higher-order programs. In Programming Language Design and Implementation (PLDI). ACM, New York, NY, USA, 2015. Flemming Nielson and Hanne Riis Nielson. Infinitary control flow analysis: A collecting semantics for closure analysis. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 1997. Flemming Nielson, Hanne R. Nielson, and Chris Hankin. Principles of Program Analysis. Springer-Verlag, Berlin, Heidelberg, 1999. Ulf Norell. Towards a Practical Programming Language Based on Dependent Type Theory. PhD thesis, Chalmers University of Technology, 2007. David Pichardie. Interpre´tation Abstraite en Logique Intuitionniste: Extraction d’Analyseurs Java Certifie´s. PhD thesis, Universite´ Rennes 1, 2005. In French. Atze van der Ploeg and Oleg Kiselyov. Reflection without remorse: Revealing a hidden sequence to speed up monadic reflection. In Haskell Symposium (Haskell). ACM, New York, NY, USA, 2014. Gordon D. Plotkin. A structural approach to operational semantics. Technical report, Aarhus University, 1981. Thomas Reps, Susan Horwitz, and Mooly Sagiv. Precise interprocedural dataflow analysis via graph reachability. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 1995. 289 John C. Reynolds. Definitional interpreters for higher-order programming languages. In ACM Annual Conference (ACM). ACM, New York, NY, USA, 1972. Ilya Sergey, Jan Midtgaard, and Dave Clarke. Calculating graph algorithms for dominance and shortest path. In Mathematics of Program Construction (MPC). Springer-Verlag, Berlin, Heidelberg, 2012. Ilya Sergey, Dominique Devriese, Matthew Might, Jan Midtgaard, David Darais, Dave Clarke, and Frank Piessens. Monadic abstract interpreters. In Programming Language Design and Implementation (PLDI). ACM, New York, NY, USA, 2013. Micha Sharir and Amir Pnueli. Two approaches to interprocedural data flow analysis. In Program Flow Analysis: Theory and Applications. Prentice Hall, Upper Saddle River, NJ, USA, 1981. Olin Grigsby Shivers. Control-Flow Analysis of Higher-Order Languages or Taming Lambda. PhD thesis, Carnige-Mellon Univeristy, 1991. Paulo F. Silva and Jose´ N. Oliveira. Galculator: Functional prototype of a Galois- connection based proof assistant. In Principles and Practice of Declarative Pro- gramming (PPDP). ACM, New York, NY, USA, 2008. Yannis Smaragdakis, Martin Bravenboer, and Ondrej Lhota´k. Pick your contexts well: Understanding object-sensitivity. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 2011. Guy L. Steele, Jr. Building interpreters by composing monads. In Principles of Programming Languages (POPL). ACM, New York, NY, USA, 1994. Terrance Swift and David S. Warren. XSB: Extending prolog with tabled logic programming. In Theory and Practice of Logic Programming (TPLP). Cambridge University Press, Cambridge, UK, 2012. Hisao Tamaki and Taisuke Sato. OLD resolution with tabulation. In International Conference on Logic Programming (ICLP). Springer-Verlag, London, UK, 1986. Gregory Tassey. The Economic Impacts of Inadequate Infrastructure for Software Testing. National Institute Of Standards and Technology, Gaithersburg, MD, USA, 2002. Julien Tesson, Hideki Hashimoto, Zhenjiang Hu, Fre´de´ric Loulergue, and Masato Takeichi. Program calculation in Coq. In Algebraic Methodology and Software Technology (AMAST). Springer-Verlag, Berlin, Heidelberg, 2011. Sam Tobin-Hochstadt, Vincent St-Amour, Ryan Culpepper, Matthew Flatt, and Matthias Felleisen. Languages as libraries. In Programming Language Design and Implementation (PLDI). ACM, New York, NY, USA, 2011. 290 David Van Horn and Matthew Might. Abstracting abstract machines. In International Conference on Functional Programming (ICFP). ACM, New York, NY, USA, 2010. David Van Horn and Matthew Might. Systematic abstraction of abstract machines. In Journal of Functional Programming (JFP). Cambridge University Press, Cam- bridge, UK, 2012. Alexander Vandenbroucke, Tom Schrijvers, and Frank Piessens. Fixing non- determinism. In Implementation and Application of Functional Programming Languages (IFL). ACM, New York, NY, USA, 2015. Dimitrios Vardoulakis. CFA2: Pushdown Flow Analysis for Higher-Order Languages. PhD thesis, Northeastern University, 2012. Dimitrios Vardoulakis and Olin Shivers. CFA2: A context-free approach to control- flow analysis. In European Symposium on Programming (ESOP). Springer-Verlag, Berlin, Heidelberg, 2010. Dimitrios Vardoulakis and Olin Shivers. CFA2: a context-free approach to control- flow analysis. In Logical Methods in Computer Science (LMCS). Logical Methods in Computer Science e.V., Braunschweig, Germany, 2011. Andrew K. Wright and Suresh Jagannathan. Polymorphic splitting: An effective polyvariant flow analysis. In Transactions on Programming Languages and Systems (TOPLAS). ACM, New York, NY, USA, 1998. Xuejun Yang, Yang Chen, Eric Eide, and John Regehr. Finding and understanding bugs in C compilers. In Programming Language Design and Implementation (PLDI). ACM, New York, NY, USA, 2011. Michael Zhivich and Robert K. Cunningham. The real cost of software errors. In IEEE Security and Privacy. IEEE, Washington D.C., USA, 2009. 291