{'fraction_non_alphanumeric': 0.028879173411191627, 'fraction_numerical': 0.003691209173562471, 'mean_word_length': 5.028636570907433, 'pattern_counts': {'":': 0, '<': 1, '<?xml version=': 0, '>': 0, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
MirrorShard: Proof by Computational Reflection with Verified Hints

We describe a method for building composable and extensible verification procedures within the Coq proof assistant. Unlike traditional methods that rely on run-time generation and checking of proofs, we use verified-correct procedures with Coq soundness proofs. Though they are internalized in Coq's logic, our provers support sound extension by users with hints over new domains, enabling automated reasoning about user-defined abstract predicates. We maintain soundness by developing an architecture for modular packaging, construction, and composition of hint databases, which had previously only been implemented in Coq at the level of its dynamically typed, proof-generating tactic language. Our provers also include rich handling of unification variables, enabling integration with other tactic-based deduction steps within Coq. We have implemented our techniques in MirrorShard, an open-source framework for reflective verification. We demonstrate its applicability by instantiating it to separation logic in order to reason about imperative program verification.


Introduction
In using proof assistants to establish theorems with very high assurance at relatively low human cost, two main methods are employed. One can implement proof-generating theorem provers that justify their decisions in terms of more primitive reasoning steps, or one can employ the proof by reflection style, which involves verifying the correctness of provers formally, so that there is no need to audit their outputs. The latter style has been used fairly heavily in the type theory community, where it is often viewed as a straightforward implementation technique to improve both performance of and assurance about proof search. To our knowledge, however, proof by reflection has only been applied to fairly focused problems where the domain of discourse (or its axiomatization) is known a priori. To explore its limits, we explore mostly automated correctness verification of imperative programs, in a framework supporting higherorder logic and abstract predicates. Extensibility is crucial in this domain, as the (expert) author of an abstract data type (ADT) would like to teach the prover how to reason about related proof goals that arise in verifying (non-expert) client code that calls the ADT's methods. We hope to achieve this modularity and expressivity while minimizing the overhead of proofs.
Realistic program verification involves consideration of many details of program behavior. Automated theorem provers play a crucial role in discharging the many straightforward proof obli-gations, freeing the programmer to focus on providing invariants and other pieces of high-level insight. However, conventional automated program verifiers as in the ESC family [18] suffer from a number of disadvantages.
• Debuggability. Theorem prover implementations can be quite complex. While the prover will facilitate effective static verification for many other programs, the prover itself is usually debugged tediously via testing, and "bugs" often manifest as confusing failures of the tool to discharge specific goals. • Trustworthiness. As a corollary of the last concern, we might worry that an off-the-shelf theorem prover is unsound in a way that can have serious consequences for security and so forth, if we use it as a trusted component of a verification system. • Flexibility. The logics of automated theorem provers are generally chosen to be decidable or otherwise tractable in practice, ruling out expressive higher-order features. As a result, some important correctness theorems (e.g., semantic correctness of a compiler) may not be possible to state, and others must be stated in prohibitively verbose ways. • Composability. Many theorem provers act as standalone systems that do not easily produce results that can be combined with results arising from other tools using a unified logical language. For instance, we may want to verify the correctness of a compiler (which is outside the reach of traditional program verifiers), verify the correctness of a program in the compiler's source language (which is well within the scope of traditional verifiers), and compose the results into a high-confidence deduction about the compiled version of that source program.
All of the above reasons have contributed to the popularity of program verification with proof assistants like Coq [16] and Isabelle [29]. Such systems are based on small, trustworthy proof checkers for relatively simple sets of axioms and inference rules. They achieve trustworthiness by allowing the use of arbitrary proof search techniques without expanding the trusted code base for proof checking. The underlying logics are higher order, providing very good flexibility. It is even possible to leverage external tools, like SMT solvers, to generate proof assistant proofs, addressing the composability concern.
However, debuggability remains a serious concern. While automated provers support many features, they are not a panacea. For example, combinations of modal, linear, and higher-order logic are not supported by any prover that we are aware of. And even when they are, the undecidability of powerful logics forces frequent use of new, problem-specific heuristics that must be embedded into automation or applied manually each time they are needed. Without the ability to justify such extensions, user customization compromises the trustworthiness of the entire system. While, it is possible to use arbitrary implementations to generate proof traces in novel ways, such procedures can be nearly as hard to debug as conventional, non-proof-generating theorem provers.
It is also true that switching to an approach based on proof generation can bring substantial performance costs. A proof language with few orthogonal features is very attractive because it allows proof checkers to be small and trustworthy, but small proof languages tend to promote large proofs. Automated provers must be augmented to support generation of proofs, which adds code complexity and performance overhead. Later, it is necessary to check the proofs, which adds further overhead.
An established best-of-both-worlds technique for type-theoretic proof assistants is proof by reflection [9]. Here, instead of writing a procedure that generates a proof on each invocation, we instead verify the procedure itself. Now no proofs need to be generated, freeing us from the associated overhead. However, the formal guarantees are just as strong, because we have proved the correctness of the procedure, which is generally implemented in a functional programming language within the proof assistant's logic.
Proof by reflection has previously been implemented for conventional mathematical decision problems. For instance, Coq comes packaged with a tactic for algebraic simplification over mathematical rings. This procedure, and others out there, operate over a small vocabulary of logical symbols whose semantics must be understood. Often the procedures are extensible in the sense of allowing customization of the set of symbols. However, such procedures can be thought of as single-minded, never combining multiple, user-defined reasoning strategies in the way that successful automated program verifiers do.
To investigate how far the scope of proof by reflection may be expanded, we built reflective implementations of two key proof procedures for imperative program verification with higher-order separation logic [32] within the Bedrock [13] library for Coq. Previously, these procedures were implemented in a proof-generating way, with Coq's domain-specific tactic language Ltac [17].
Listing 1 shows how our verification procedures handle userdefined abstractions by showing a verification of binary search tree "lookup." The code defines a representation invariant for binary search trees (bst). Afterward, a few hint lemmas are proved about the invariant. These lemmas encapsulate the semantic knowledge that the reflective procedures will need to reason about bst. Specifications for the imperative code make heavy use of logical quantifiers, which create additional challenges for our automation. Listing 1 ends with a short proof of program correctness, via the sep tactic that calls the reflective procedures with a particular package of program-specific hints.
Our high-level contributions come from adapting the proofby-reflection approach to a more open-ended setting. In particular: • To the best of our knowledge, ours are the first reflective tactics to support a notion of reusable hints, similar to the notions exposed in Coq Ltac programming. Our approach leverages three types of hints to teach our core procedures about new abstract predicates. Even the usual "points-to" predicate of separation logic is not built into our tactics, but rather taught to it via hints. We identify three hint mechanisms that suffice to support all of the above from a small core, and we prove the soundness of the hint architecture. We also demonstrate (proved If ( "k" < "tmp") { (* Searching for a lower key *) "s" ← * "s" } else { (* Searching for a higher key *) "s" ← * "s" + 8 } } };; Return 0 . . . }.
(* Prove our implementation partially correct. *) Theorem bstMOk : moduleOk bstM. Proof. vcgen; abstract (sep hints; auto). Qed. correct) classic data structure examples like arrays, linked lists, and search trees. • We extend the proof-by-reflection approach to handle proof goals containing quantifiers, and we implement and verify a unification algorithm to facilitate related reasoning. Coq itself includes a disjoint treatment of unification, and we have developed an approach to two-way communication between the two unification systems. This interface is not trivial because unification is not part of Coq's logic, but rather added on outside the trusted base and interfaced with using Ltac, Coq's tactic language for building proofs.
• Hints are naturally expressed over different logical theories, involving different data structure representation predicates and different background theories (e.g., bitvectors, lists, strings) for stating side conditions of lemmas. We have developed a modular architecture for composing verified hints over different theories, including a mechanism for carving out and combining smaller domains. • We also provide a performance analysis of design choices in implementation of extensible reflective tactics. We encountered surprising challenges in achieving reasonable performance while supporting all of the above features.
We begin with background on reflective proofs (Section 2). We then discuss the broadly applicable novel technical devices behind our implementation, which enable us to apply reflective reasoning to the complicated expressions that arise in our verification tasks (Section 3). We then try to distill the reusable engineering lessons we have learned about implementing and optimizing reflective procedures at this scale (Section 5). Next comes an evaluation of the automation achieved by our procedures and an analysis of their performance characteristics compared to non-reflective verification (Section 6), where our overall conclusion is that we improve asymptotic performance substantially, though well-abstracted programs and specifications may not lead to large enough invariants to exhibit the scaling improvements clearly. We wrap up with a discussion of related work (Section 7).
Our techniques, and many of the components that we built, are implemented in the MirrorShard library, available for download at: https://github.com/gmalecha/mirror-shard/

A Primer on Proof By Computational Reflection
In this section, we discuss the idea of reflection [9]. For the sake of simplicity, we shall present reflection using a stripped-down example: here, we are interested in reducing the proof of equalities like f a (g b) = f c (g (h a)) to the proof of a = c and b = h a. That is: we are computationally discharging equalities between terms in the (multi-sorted) algebra generated by an arbitrary signature, generating new proof obligations for equalities between subterms that do not follow from basic properties of equality. The first step of reflection is to encode the syntax of proof goals in a datatype defined within your proof assistant's logic. In the case of Coq, this logic is Gallina, a dependently typed lambda calculus with inductive definitions. Thus, we start by defining a datatype expr to represent terms: in this syntaxified representation, an expression is just a function symbol applied to a list of expressions (see Listing 2). We break the circularity using 0-argument functions to represent variables and constants. This is important because it is imperative that the expr type has a decidable equality so we can avoid generating obligations such as x = x. We achieve this by representing functions as indices into an environment of functions.
To make the meaning of our syntax formal, we define a denotation function denote that maps an expression e supposedly of the type represented by ty, to a value of type option (nth types ty). Note that this denotation function is partial, because our data type admits the encoding of ill-typed terms. (We will return to this particular design choice in Section 5.) The denotation function is parameterized by an environment of types and function signatures (of type sig indexed by the type environment), and will perform dynamic type-checking to ensure that the Gallina term that it produces is well-typed. Thus, where ltb is a Boolean-valued less-than test for natural numbers, the term ltb (x + y) z can be represented using the following environments and term. Using this representation, we can implement a (heuristic) decision procedure in Gallina. The procedure considers the head symbols of two expressions. If they are the same, it proceeds recursively on their arguments. Otherwise it accumulates a "proof obligation" (i.e., a pair of expressions whose denotations must be proven equal). Finally, we prove the procedure sound. That is, we prove that if all of the constraints are satisfied then the denotations of the original terms are equal.
Applying this lemma makes it possible to replace several proof steps (one proof step for every common head symbol between the lefthand-side and righthand-side expressions) by a single proof step, plus a computation. That is, we come to the final crucial element of a proof by reflection: a goal is proved by appealing to a theorem and then proving its hypothesis by "running" the hypothesis to reduce it to a normal form, which should then be much easier to prove than the original goal.
For example, suppose that we want to prove ltb (x+y) z = ltb (x+y) w. We can apply the aforementioned lemma, using suitable values a and b of type expr, and Coq will check that denote 1 a (resp. denote 1 b) is convertible to the lefthand side (resp. righthand side) of the goal, according to the lambda calculus reduction rules of Gallina. Here we take advantage of the fact that, in Coq's logic, reduction-equivalent terms may always be used interchangeably, with no need to include explicit proof steps as justification.

Reification
In the above discussion, we side-stepped a difficulty: we have to automate the construction of the syntactic representation (i.e., terms of type expr) from the terms that appear in the goal. While this operation, called reification, is conceptually the dual Listing 3. Representing separation logic expressions with binders of denote, it must be performed at the meta level using specialpurpose tactics. We shall return to this problem in Section 5.

An Extensible Verification Architecture
Compared to past work on proof by reflection, our verification architecture achieves its powerful form of extensibility through three novel technical devices: Extensible Syntax enables us to encode terms with arbitrary Coq constants and quantifiers, while retaining the ability to compute on constants embedded in terms. (Section 3.1) Composable Soundness enables us to combine soundness proofs about procedures that reason about different logical domains.

(Section 3.2)
Integration with Unification enables flexible integration of reflective procedures with traditional Ltac-based automation that uses unification variables that are not formalized in Coq's core logic. (Section 3.3)

An Extensible Syntax with Binders
Our reified syntax for assertions of separation logic [32] (summarized in Listing 3) is similar in spirit to the generic syntax described in Section 2 but incorporates several new forms for dealing with binders, manipulating proofs, and performing domain-specific reasoning. The four key differences are: A distinguished encoding for the type of logical propositions (tvProp) simplifies representing logical properties and enables us to represent polymorphic equality (Equal), even though our encoding does not support polymorphic functions in general.
An expression type family indexed by a type environment, which gives for each type both its Coq representation and a compatible equality testing function. The Const constructor is used to inject terms that our unification algorithm should consider comparing for equality by calling type-specific procedures, in contrast to the syntactic equality check used for the rest of the constructors.

Binders and local variables are represented by the constructors
Exists of sexpr and Var of expr. Our encoding is similar to the locally nameless technique for lambda terms, in that we maintain distinct representations of global/free variables (nullary function applications via Func) and local/bound variables (via Var).
Unification variables (represented with UVar) are represented explicitly so that our procedures may deduce and substitute values for them. Informally, a Var expression supports universal-quantifier reasoning, while a UVar expression supports existential-quantifier reasoning. We must prove any theorem considering all possible Var values, but we are allowed to choose specific UVar values that make the theorem true.
To support our new features, the denotation functions exprD and sexprD have several new parameters for variables, unification variables, and separation logic predicates. To see how these components fit together, we show a simple heap assertion for a cons cell where the first value is a unification variable from the context (?a) and the second (occurring 4 bytes later) is an existentially quantified value (v): This term can be represented as:

Achieving Compositionality
To be useful, hints must be both self-contained, packaging together the hint and its soundness proof, and compositional, enabling us to combine separately defined hints in a meaningful way. Here, our type-family representation introduces problems. The most naïve prover type, with one environment of types fixed for all provers, will never compose with provers using different types. We solve the problem using a sort of universal quantification over type environments that imposes constraints on the presence of specific identifiers.
A Constraint Formulation One way of representing the constraints is propositionally, with explicit logical assertions. We can encode constraints using partial environments and say that an environment (e) satisfies a constraint (C), written C |= e, when all mappings in the constraint are consistent with the environment. Using this formulation, we can define two provers, one for lists and the other for machine words. Each constraint is a list of optional types, where the presence of a type forces the final environment to contain that type in that list position. Since C1 and C2 are compatible, these provers can be composed into a new prover that accepts an environment that satisfies both C1 and C2. The difficulty of this formulation arises in type-checking prover implementations, where it is generally necessary to use casts justified by appealing to the consistency proof. For example, suppose that prover_2 is determining whether a number is a multiple of 4. An ideal formulation of the soundness of this prover would be: Theorem prover2_sound' : ∀ ts fs vars uvars e v (pf : C2 |= ts), FC2 |= fs → prover_2 ts pf e = true → exprD ts fs vars uvars (tvType 0) e = Some v → v mod 4 = 0.
where FC2 is prover_2's constraint on the function environment. Unfortunately, this soundness statement is not well typed. The problem stems from v. In line 3, v must have type tvarD ts ( tvType 0), while in line 4 v must have type word.
The core problem lies in the intensional nature of Coq's type theory. Coq distinguishes between two notions of equality.
Definitionally equal (≡) terms are identical after reduction. This notion of equality is part of Coq's core logic and requires no extra work to apply during type-checking.
Provably equal (=) terms are defined by a binary inductive predicate x = y. This type encodes an explicit proof (in Coq's logic) that x and y are equal. To use this type of equality, we must perform a cast using the proof.
For a concrete example, we return to the problem above. From the meaning of |= we can prove tvarD ts (tvType 0) = word, but the two are not definitionally equal under Coq's reduction rules, since ts is a variable and not a concrete environment; the reduction to determine equality gets stuck examining the structure of ts. We can solve this problem using the following cast: Theorem prover2_sound : ∀ ts fs vars uvars e v (pf : C2 |= ts), FC2 |= fs → prover_2 ts pf e = true → exprD ts fs vars uvars where GetConsistent takes the consistency proof and the index and returns a proof of tvarD ts (tvType 0) = word (i.e. by looking up the index in the constraint). The cast is present for a reason: removing it produces an ill-typed term. Therefore, it would be unsound to include a reduction rule that removes useful casts. The problem is that proving triviality of casts (i.e., that they convert between definitionally equal terms) is extra work with no counterpart in pencil-and-paper reasoning. A proof step like this one must be preceded by another proof step that rearranges the proof context to a form that is well-typed both before and after the cast is removed, which can be surprisingly subtle and case-specific. Furthermore, justifying this step ("removing a cast between definitionally equal terms has no effect") is not possible in Coq's core logic without appealing to axioms.
The Computational Formulation While Coq's reduction mechanism does not handle the above formulation well, we can give an alternative formulation that enjoys better computational properties. In this formulation we achieve constrained quantification over environments not by starting from an arbitrary environment and asserting a constraint over it, but rather by starting with an arbitrary environment and performing a computation on it to make it constraint-compliant by construction. The heart of the technique is a recursive function called like applyC c e, which "instruments" environment e to satisfy constraint c. Reformulating the previous theorem leads to the following: Definition prover_2 : ∀ ts, let ts' := applyC C2 ts in expr ts' → bool.
Theorem prover2_sound : ∀ ts fs vars uvars e v, let ts' := applyC C2 ts in let fs' := applyC FC2 fs in exprD ts' fs' vars uvars (tvType 0) e = Some v → prover_2 ts' e = true → v mod 4 = 0. Now, tvarD (applyC C2 ts) (tvType 0) is definitionally equal to word since the environment has been reformed by applyC so that it is manifestly a series of "cons" operations, containing the proper type in the proper position. In particular, reduction tells us applyC C2 ts ≡ word :: tl ts. In general, we may now extract any constant index occurring in the constraint from the updated environment, without any need for dependent casts.
In fact, this formulation gives us much more: it enables computational composition. When two constraint environments are compatible, i.e. they do not specify different values for any index, applying applyC to them commutes definitionally.
This feature makes composition of two provers trivial. If we wish to compose two provers, say p1 and p2, with different, but compatible, environments TC1 and TC2, then we can pre-compose each function by applying the other's type constraint to produce two provers on the same environment. Concretely: : ∀ ts, expr (applyC TC2 (applyC TC1 ts)) → bool Since these types are definitionally equal, we can treat them identically, applying both functions to the exact same term. That is, any composition operation can be written free of both explicit proofs and explicit casts.
Using this representation, we can package together reusable, self-contained verification hints using Coq's dependent records.
The first three fields express the constraints on the type, function, and heap predicate environments. The fourth field contains a record that packages together the three different types of hints that our system uses (discussed in Section 4). Note that the type of Hints is dependent on the value of the Types field. The final field encapsulates the soundness proofs for the hints. When applying our reflective procedures, our tactics use the first three fields to seed the environments used to reify terms, the hints to compute the results, and the soundness proof to justify reasoning with the hints.
Because we use a shallow embedding of constraints, we do not need to write a proof that two packages compose. Unfortunately, this means that we cannot write a Gallina function that combines two hint databases. Rather, we use Ltac (a dynamically typed language) to construct the term and turn the type checker loose on it. If the result type-checks, then the environments are consistent and the hints compose; otherwise, the programmer gets an error message about compatibility. p, q, r : word

Interfacing with Unification Variables
Traditional proofs in Coq are done in "proof mode" using tactics that manipulate a goal that is displayed to resemble a standard sequent calculus. Universally quantified variables are displayed in a "proof context" above a double line, while the goal is displayed below the line. In order to integrate well with existing tactic-based and interactive proof techniques, our reflective procedures must fit naturally into this view. Figure 1 demonstrates how our reflective procedures manipulate binders and unification variables. The implementation of these procedures is complicated by manipulation of de'Bruijn indicies but is otherwise mostly standard. It is the phrasing of the soundness theorems that we focus on here. We begin with an illustrative example, Figure 1.(a), which shows a simple heap implication (denoted by =⇒) with three internal quantifiers and a unification variable (?a). Unlike in normal Coq output, we include unification variables explicitly in proof contexts, i.e. we can pick any term of type word for ?a as long as it mentions only globals and variables that occur above it.
Our procedures take as inputs goals, like the one in Figure 1.(a), after they are reified as terms in the logic. For explanatory purposes, here we consider a simple procedure that only performs unification, attempting to learn the values of both bona fide Coq unification variables (e.g., ?a) and variables that are quantified existentially in the conclusion of the implication (e.g., y and z). A key question is how the unification procedure, a pure function in the logic, can cause side effects to resolve unification variables in the original Coq proof context. As in proof by reflection in general, our procedures may only announce results by replacing one logical formula with another that has been proven to imply the original.
The result of the unification procedure is shown in Figure 1.(b). Four different sorts of variables have been handled in four different ways. First, a variable existentially quantified on the lefthand side of the original implication (e.g., x here) is returned via a top-level universal quantification. Second, a normal Coq unification variable (e.g., ?a here) is asserted to be equal to whatever replacement has been inferred for it. Finally, there are two cases for a variable existentially quantified on the righthand side of the implication. Either no unification was found for it (e.g., z in this example), in which case it gets a top-level existential quantifier in the new goal; or some unification was found (e.g., y in this example) and the variable is simply removed by substituting for it everywhere it appears.  Figure 2. The high-level verification strategy applied to a simple program that increments a memory cell A unification is represented as a map from unification variable to syntactic expression. As for our other syntactic representations, our soundness proofs ascribe a denotation to unifications, this time as a conjunction of provable equalities (substD). Using this denotation function in a premise, we can prove that syntactic instantiation, by instantiate, preserves the semantic meaning of terms.

Extension point
Theorem substD_instantiate : ∀ funcs U G e t sub, substD funcs U G sub → exprD funcs U G (instantiate sub e) t = exprD funcs U G e t.
The final step occurs in Ltac. We simplify the goal by moving variables "above the line" into the proof context. All ∀ quantifiers lead to normal Coq variables (e.g., x), and all ∃ quantifiers lead to Coq unification variables (e.g., ?a). More importantly, we remove the unification equations like ?a = q by first performing the side effect of setting ?a equal to q and then proving the equation trivially by reflexivity. These side effects are possible in Ltac but not Gallina, and our strategy for generating initial output goals is designed to be very telegraphic in suggesting side effects.

Reflective Procedures
The techniques described in the previous sections enable extensible reflective verification with rich formulas. In this section, we show how to apply MirrorShard, the Coq library we have built with those techniques, to create reflective automation for the Bedrock [13] library, which supports program verification in separation logic. Our development leverages MirrorShard to build the two core verification components: symbolic execution and separation logic cancellation. The procedure is illustrated in Figure 2 and described in the rest of the section.
Symbolic Execution First, symbolic execution takes a precondition and a sequence of instructions and computes a post-condition. Symbolic execution starts by using pure facts in the context to refine abstract predicates. In the example, the procedure finds that the linked list predicate can be unfolded since the head pointer is not null. This information is encoded in our first type of hint: Refinement Hints are stylized Coq theorems that express predicated heap implications. These theorems are reified into Coq as inhabitants of a record type with fields for a list of universally quantified variables, a list of pure premises (exprs), and the expressions on each side of the separation logic implication. Due to their first-order nature, these hints can be constructed completely automatically (using Ltac programs) from actual Coq theorems that have a particular syntactic form.
Pure facts, such as the side conditions on refinement hints (e.g., "pointer is not null"), are discharged by our second type of hint: Base Theory Provers are verified Coq functions taking the place of Coq tactics (since we cannot call Ltac from Gallina code), proving expr-encoded proof goals with arbitrary algorithms that can be coded and verified in Gallina.
We have developed four provers: • A reflexivity prover, which proves statements of the form e = e.
• An assumption prover, which maintains a list of known facts and attempts to find the goal as a syntactic match to one of these facts. • A prover for reasoning about linear arithmetic on width-32 bitvectors to prove equalities and inequalities. This prover makes inferences by combining hypotheses representing expressions e1 = e2 + k for constants k. (This last form of reasoning is especially applicable to common patterns of pointer arithmetic.) • A prover oriented toward array bounds checks, which understands that array writes preserve length.
We support composing provers in a simple disjunctive style, i.e. a proposition is provable if either of two provers can prove it.
After predicate refinement, symbolic execution begins interpreting instructions. Total arithmetic and logical instructions are trivial to model by converting the transfer functions into their syntaxified forms. Instructions that access memory (both reading and writing) require more care, but our use of separation logic enables an effective algorithm based on our third type of hint: Memory Evaluators are verified functions that reason about reads from and writes to heaps satisfying a separation logic assertion. This approach enables the symbolic evaluator to interpret memory operations in terms of many different data structure predicates, without the need to expose individual points-to assertions algebraically.
In addition to a composition operator that combines two memory evaluators into one, we have implemented memory evaluators for 32-bit points-to, arrays (of both words and bytes), and local variable stack frames.
In the example, the memory evaluator uses the provers to inspect the separation logic formula and determines that the value read from p is x (because a subformula p → x appears) and the value written is therefore x+1, which is constructed syntactically when interpreting the addition.
Cancellation Cancellation proves that the strongest post-condition, computed by symbolic execution, implies the specification's postcondition. The algorithm begins with backward refinement, which uses the same type of refinement hints but refines in the conclusion of the heap implication 1 . In the example, an analogous hint refines the list predicate, exposing the first cell. In the conclusion, existential variables are introduced as new unification variables that the rest of cancellation will attempt to instantiate. The core of the cancellation algorithm uses the cancellative properties of separation logic to prove the implication. Since cancellation leverages unification to resolve unification variables and does not backtrack, the order of considering predicates to cancel matters. We use a simple heuristic based on a lexicographic ordering of syntactic expressions where unification variables have the highest values and are thus unified last. This ordering, for example, will attempt to unify p → ?a with p → v before it tries to unify it with ?b → ?c.

Lessons Learned: Engineering Reflective Proof Procedures in Coq
In this section we highlight a variety of design choices that arise in the development of reflective decision procedures. While the details are Coq-specific, the ideas generalize and shed light on interesting design decisions both for users and developers of proof assistants.

Term representation
General dependent types provide many representation alternatives that are not available in most programming languages. The first implementation choice is whether the type of terms should guarantee that every term is well-typed. Such a representation allows us to make the denotation function (exprD) total, simplifying theorem statements and avoiding the need to prove that functions preserve the well-typedness of terms. The cost of this convenience is indexing terms by additional environments. In our setting, we would need to parameterize expr by the environments of functions, variables, and unification variables in addition to the expression type, leading to a type like: This representation moves the hard work from the soundness proofs to the computational operations that manipulate terms. For example, with dependent types, we weaken terms by structural recursion: Fixpoint dexpr_weaken ts fs u u' g g' t (e : dexpr ts fs u g t) : dexpr ts fs (u ∪ u') ( g ∪ g') t := . . .
Only the variable cases are interesting, essentially needing to justify that valid references into u (respectively g) are the same as references into u ∪ u' (respectively g ∪ g').
Using a non-dependent representation, this function becomes a no-op since environments are extended at the end. Our proofs appeal to the following lemma that relates the meanings of expressions to their meanings under weakened environments.
Theorem exprD_weaken : ∀ ts fs u g t e v, exprD ts fs u g e t = Some v → exprD ts fs (u ∪ u') ( g ∪ g') e t = Some v.
This ease of implementation also translates to performance improvements for procedures like cancellation, as shown in Figure 3. The first bar uses the dependent representation while the second uses the minimally dependent representation described in Section 3.1. We believe that the large difference between the bars is due to the need to evaluate proof terms to reduce dependent casts, though Coq provides no profiling tools for verifying this hypothesis.

Cancellation Performance (40 conjuncts)
Check Verify Figure 3. Verification times for two term encodings

Efficient Computation
Analogous to the choice of term representation is the choice of function implementation. We may implement functions with dependent types, making their properties manifest directly; or we may choose simple types and then prove properties after the fact. The most common manifestation of this choice is for equality-testing functions. In the dependent style, it is common to write the type of an equality function as: ∀ x y, {x = y} + {x = y} (which is a particular case of dependent sum type carrying proofs). The nondependent version (eqb) returns a Boolean, and provides a separate proof: ∀ x y, eqb x y = true ↔ x = y.
From a computational perspective, the latter is much more efficient under call-by-value reduction, since in the former the proofs that are constructed must be reduced completely. Changing our algorithms to use the non-dependent version of equality checks resulted in a 40% reduction in the proof generation and checking time. (Note also that in the context of code extraction from Coq, the difference between the solutions disappears.) While this fact may be well-known for seasoned implementers of reflective decision procedures, there is little guidance from Coq's standard library toward this choice. In adapting the proofs, we have found it easy to recover the proof behavior of the dependent equality using Coq's dependent type classes [33].
Traditional type classes carry additional information about types, e.g. an equality decider. Dependent types enable type classes to carry additional information about values. For example, a type class indexed by a function can carry a proof about that function: Proofs and automation can now reference the symbol eq_ok and Coq's type class resolution will attempt to find an appropriate instance. This approach is similar to the development in the math classes project [34] and is the core principle underlying recent work on ExtLib 2 .

Reification
Most sources gloss over the (mostly) uninteresting problem of reifying terms. While not particularly glamorous, the reification process can dramatically affect verification time.
Our first version of reification for pure expressions, separation logic formulas, and object-language commands used Ltac. However, an initial performance evaluation showed that reification was a major performance bottleneck (taking almost 50% of total verification time). First, some of this overhead can be attributed to Ltac itself: the language is dynamically typed and built for writing backtracking tactics for proof search, rather than building actual terms. Second, we had to circumvent the lack of support for manipulating open terms (i.e., terms with free variables) caused by embedded binders. The trick requires copious use of second-order pattern matching, which is considerably more time-intensive and results in code that is more difficult to read and maintain. Third, we had to split reification into two passes: first, to gather the type environment, and then to build the reified terms indexed by this environment.
To address the performance problem, we implemented a second version of our reification as an OCaml plugin. This alleviates all of the previously mentioned problems: we use OCaml data structures (which are more efficient than their Ltac counterparts) to build environments and terms; we manipulate open terms, which makes reification more direct; we make a single pass on the Coq term to build the reification environment and the reified terms, rather than two in Ltac. This plugin does not need to be trusted more than any Ltac tactic since it constructs terms that are fed into the Coq kernel.
Using this plugin dramatically reduced the time spent on reification. Whereas Ltac reification had taken approximately 32 minutes for a test suite of 10 examples, OCaml reification takes approximately 22 seconds (an 88X speedup). Processing of a single file will typically perform hundreds of reifications, so these figures are quite reasonable.
The reason for this slowdown is more than the interpretive overhead of Ltac. The explanation is a Coq feature (misfeature?) dealing with building terms in Ltac. To illustrate, consider two different Ltac expressions that build a natural number by repeatedly applying the successor function S to the zero constant O. The underlying problem is that Coq re-typechecks all parts of each new term that is constructed in Ltac. This is not an unreasonable-sounding requirement for a dynamically typed language. Ltac-bound variables appearing in Gallina terms have their contents substituted in explicitly. Reification naturally builds terms step-by-step through a recursive process. The overhead of repeated typechecking can become overwhelming in such cases. In contrast, all Coq-level type checking in OCaml is done explicitly. This allows dramatic speed-ups by only typechecking the final term once.

Engineering Proof Terms
While Coq proofs are often thought of as tactic scripts, the final product of Coq proving is proof terms in a core type theory. Tactics merely provide a more convenient way to construct these terms (which often make heavy use of dependency). A proof that looks straightforward at the tactic level may produce proof terms that take substantially longer to check than the proof script took to run.
(* denote (4) *) The benefit, and curse, of this formulation lies in the quantification over l' and r', which together act as the result of the cancellation procedure. To apply the above theorem, our proof must directly record the values of l' and r'. Thus, when Coq checks the proof it knows exactly what type line (3) has, regardless of the unification procedure used to justify the equation in line (2). Representing and type checking these embedded terms in the final proof, however, can be expensive if they are large. While our representation does not necessarily look large on the surface, the dependency of terms on the type environment requires that the type environment be repeated syntactically at every constructor. Even paring down the type environment to contain only the type (eliding the equality function and its proof) does not shrink the term enough. This is a limitation of Coq's type checker, but not a theoretical one. Adapting Coq to use bi-directional type checking [31] could solve this problem.
To circumvent the embedded term problem, we replace the quantifier with a let declaration scoped over the relevant premises.
(* denote (4) *) The drawback to this approach is that naïve uses of this theorem do not record the result of the reduction. During proof checking, Coq will lazily evaluate this term, leaving large, partially reduced terms unevaluated at key places, requiring them to be reduced multiple times during subsequent proof checking. While Coq provides only limited methods for specifying reduction strategies in proof terms, in practice knowing the result is enough to make proof checking reasonably efficient. In general, we can save the result by performing cut elimination with an explicit type annotation. We revisit this problem in Section 5.5, discussing our solution for making this phrasing efficient.

Engineering Reduction
The workhorse of proof by reflection is the computation step, and fast evaluation of terms is essential to making reflective proofs efficient. Unfortunately, as we demonstrated in Section 5.4, standard formulations of reflective theorems do not work well in our setting. Our formulation requires fast delimited evaluation, i.e. evaluation keeping certain symbols opaque. Since Coq's logic admits reduction under binders, evaluation strategies are able to handle opaque terms. However, to maintain abstraction by not unfolding user-defined logical symbols, we need the ability to engineer the handling of particular identifiers. Throughout our development we evaluated several reduction mechanisms, two of which are noteworthy.
Delimited cbv Coq's full-beta, call-by-value reduction mechanism is reasonably fast and supports the abstraction that we need to avoid the reduction of certain terms. As an illustration, consider the goal after an application of cancel_correct.
let (l', r') := cancel funcs l r in sexprD funcs uvars l' =⇒ sexprD funcs uvars r' We would like to reduce the above proposition to a heap implication using the standard separation logic connectives, e.g. * and emp.
Since we know the definition of cancel, we can customize Coq's cbv tactic to leave certain constants, like * , opaque by specifying a whitelist of identifiers that should be reduced. Unfortunately, specifying this list modularly is not possible. The customization available to cbv requires an explicit whitelist (or blacklist) of identifiers, with no facility for supporting dynamically constructed lists or using wildcards to include all values from a module. The whitelist required to reduce the above goal contains roughly 450 symbols, is sensitive to any refactoring (including adding additional provers), and is very difficult to debug. Missing symbols cause evaluation to get stuck producing enormous, partially evaluated terms that can chew through 8G of RAM in under a minute. Nevertheless, once the whitelist is complete, evaluation is fast. We post-process the resulting term folding named definitions that may have occurred in the reflective code and in the user function environment. That is, we substitute an identifier for its associated definition, to simplify the term.
It is this final point that makes a blacklist unattractive. Though the list would be considerably shorter, we would not know which terms to refold or how to refold them at the end of the reduction. vm compute vm_compute [21] is an even faster Coq reduction mechanism based on compiling Gallina terms to the OCaml virtual machine, executing them there, and translating the results back to Gallina. The price for this speed is reduced flexibility in two ways: 1. Neither whitelists nor blacklists are supported. All identifier are unfolded if definitions for them exist.

vm_compute fails if it encounters Coq unification variables.
On the surface, both limitations appear to be show-stoppers. Without a whitelist (or blacklist), separation logic abstractions (in addition to simple functions like plus) will be torn apart, revealing symbolic memories and 32-bit words. Further, the goals fed to our tactics routinely contain several unification variables introduced by program-specific Ltac code.
Our solution to these problems relies on building an anonymous function that explicitly abstracts over terms that should not be reduced. This allows us to reduce only in the anonymous function and leave the dangerous subterms alone. Consider the following simplified example, where we want to reduce the lefthand side to the righthand side: 2 * 9 =⇒ 9 + 9 + 0 Naïvely using vm_compute produces 18. It is easy to abstract 9, but + does not occur syntactically in the term. To expose it, we use a special form of * that takes + as an argument: Using this definition, we can engineer the following reduction: which differs from our target reduction by a single, cheap βreduction when applied to + and 9.
This technique works to make many terms opaque while still avoiding the problems of embedding the intermediate syntactic representation in the proof term (see Section 5.4); however, there are several limitations. First, we cannot use it to limit reduction in types since abstracting by a type will often produce an ill-typed term. Second, the abstractions are manifest in the resulting proof term, making it larger than it would be using delimited cbv. Finally, producing the abstraction using Ltac can be expensive since each abstraction must perform a linear walk over the term. Because we need to abstract all terms in the function and separation predicate environments, it is not uncommon to be blacklisting 30 or more symbols, each of which requires a linear pass over the term. To improve efficiency, we packaged the functionality with vm_compute into a tactic called evm_compute in a Coq plugin available online 3 . Unlike cbv, our tactic supports dynamic blacklists by accepting Coq lists of identifiers.  Comparison To make the performance difference concrete, Figure 5.5 shows the reduction and proof checking times for delimited cbv, evm_compute, and vm_compute. We use the number of conjuncts as a proxy for the amount of computation since the cancellation algorithm is O(n 2 ) in this case. First, note that cbv is considerably slower than the virtual machine-based strategies for large problems. The further slowdown during checking cbv proofs is due to the customized reduction strategy not being recorded in the proof term. This causes the proof checker to fall back on lazy evaluation, which is considerably slower.
The virtual-machine based strategies are considerably faster with much better scaling properties. The overhead of blacklisting is roughly constant, becoming negligible for problems with more than 64 conjuncts. This behavior justifies the efficiency of the lightly dependent design that MirrorShard advocates. However, better facilities for customizing reduction would still be beneficial. One promising idea is to use a delimiting function such as: While such a term would have no effect on the logical meaning of a statement, certain reduction strategies could treat occurrences of block x opaquely, not unfolding x. This would enable blocking reduction inside types and avoid the need to write functions like mult' in Section 5.4 that abstract their dependencies to make them visible at the top level. Reduction strategies like vm_compute could then be parameterized by a set of these blocking functions.

Evaluation
In this section we evaluate reflective proof techniques, comparing them to the standard Ltac-style verify-and-check approach to mechanized verification. We begin with a brief discussion of the automation level of our verification framework by discussing our test suite, before focusing on two grounds for comparison with Ltac-based verification methodologies: performance and debuggability.
Usability & Automation In addition to the example excerpted in Figure 1, we have carried out a number of other library module verifications, to validate the usefulness and extensibility of Mirror-Shard. Figure 5 shows some statistics of our six largest case studies. In order, the columns of Figure 5 count the executable part of the module being verified, the function specifications and invariants asserted in code, the Ltac tactic proof scripts (including commands to register hints), all the remaining lines, and finally the ratio of verification lines to program lines. The lines that we account for under "Other" are almost all definitions of data structure representation predicates and statements of theorems about them.  Our case studies exercise reasoning about a variety of userdefined abstract predicates. With the exception of a small set of obligations about words (mostly pertaining to memory access), the correctness side conditions (such as theorems about lists and sets) are verified by Ltac proof search. Our case studies are: LinkedList, consisting of the classic functions is-empty, length, reverse, and concatenate (the latter two performed in-place with mutation); Malloc, a naïve memory allocator, based on an unsorted free list with no coalescing, used by all the later case studies; ListSet and TreeSet, implementations of a common finite set interface specified with mathematical sets, respectively using unsorted lists and binary search trees; Queue, a standard FIFO queue specified mathematically using bags; and Memoize, a higher-order function that memoizes Bedrock code that implements a mathematical function. The last of these requires interesting interplay between our automation and custom Ltac code to handle higher-order proof obligations related to first-class code pointers.
The proof overhead is slightly lower than with the case studies used for the old fully Ltac-based Bedrock [13]. The decrease arises mostly from our modularization of hint databases. Our ability to verify the same examples demonstrates that we have achieved a similar level of automation and integration with Ltac.
Our procedures have also been used in a larger case study [14] that has built a verified cooperative threading library and then verified a Web server running on top of the library. The thread library includes about 400 lines of implementation code and 3000 additional lines for its verification, while the Web server has 200 lines of implementation and 500 more for the proof, which establishes that representation invariants are maintained for key data structures.
Performance Beyond expressive power, a crucial benchmark for verification tools is performance. Long-running tactics (or tools) cut the programmer out of the loop, making iterative development difficult. Figure 6 uses a microbenchmark to compare the performance of our reflective procedures based on MirrorShard to those Chlipala developed for his initial version of Bedrock [13]. The background for this task is an abstract predicate sll for singly linked lists, along with two theorems that we use as refinement hints: Out of these theorems, we can derive variants for concrete list lengths. For readability, we leave out side conditions on nullness or non-nullness of pointer variables, which appear in our actual benchmark theorem statements.
...and so on, generalizing to an arbitrary number of list cells. If the length of the list is n, solving this problem requires n + 1 refinements, with n refinements via the theorem for non-empty lists and the final refinement using the empty-list theorem. In the process, we introduce n unification variables and n pure facts (that none of the intermediate pointers are equal to 0).
Proving this family of theorems using the Ltac automation from the old Bedrock system [13] is painfully slow both to find a proof (Ltac) and check it (Ltac-Qed). Our experiments time out for a list of length 32, while our new reflective automation (Refl) finishes in under a second. We also see that the reflective tactic spends only slightly longer on proof search than checking, while with the old Ltac approach we see proof search running for at least 10 times longer. It is now faster to find proofs than it had been to check them. This straightforward result is the "good news" arising from our experiments. We achieve asymptotically better performance scaling than the Ltac-based alternative, and the constants are low enough that the performance gap becomes clear even for relatively small microbenchmarks.
The "bad news" arising from our experiments is that we see no clear change in overall performance for our full case studies like in Figure 5. Our experimental set-up is quite conservative, since our new Bedrock system involves a number of complexities not found in the original. For instance, we added support for higher-order quantification in assertions; we switched the machine word representation from natural numbers to size-32 bitvectors; and we introduced the possibility for programs to crash by accessing invalid memory addresses, creating many new crash-safety proof obligations for each program. Seen from this perspective, one might consider it a very promising sign that we hold overall verification performance at approximately the same level. It is probably also true that programs making good use of data structure encapsulation will tend to feature relatively small assertions that do not provide much opportunity to show off the asymptotic scaling of proof procedures. Figure 7 shows how our reflective proofs fit into the overall verification. The flexibility afforded to us by this method is, in some sense, its downfall. Two-thirds of verification time is spent in Ltac, and pushing more reasoning into Gallina procedures is likely to reduce verification time drastically. We expect that our general techniques to support quantifiers and integrate pure provers should streamline further development of similar procedures. As we experiment with more programs to verify, we expect both to improve the performance of our pure provers, by introducing more efficient Gallina data structures; and to add new procedures to discharge obligations in new mathematical theories.
Debuggability A crucial benefit of reflective proofs over their Ltac counterparts beyond performance is the ability to reason about the correctness of the proof-generating procedure. Ltac programs  have complicated backtracking semantics that can make them difficult to write and even more difficult to debug. For example, the backtracking severely complicated debugging our Ltac-based reification code, since a typo in a single case would cause an exponential backtrack through the algorithm. In addition, debugging tools are difficult, and tactics that compute terms must be hand-coded in continuation-passing style to get reasonable debugging support via idtac, Ltac's equivalent of printf debugging. On the other hand, even with a minimally dependent term representation, coding in Gallina enables us to use Coq's type checker to get shallow "sanity" properties. Our soundness theorems allow us to prove the deeper properties that we are relying on. During development we found ourselves frequently fixing bugs related to de Bruijn indices and binders, up until the point when the proofs about the components were completed. The proving process contributed considerably to the development process, pointing out bugs that our initial test cases did not cover.

Related Work
Our work is part of a recent trend to improve the automation available in proof assistants, which have traditionally supported only very manual proof styles. Researchers have proposed several alternative approaches.
Proof by reflection [9] is a well-established technique in the communities of Coq and other closely related proof assistants. Grégoire and Mahboubi built a reflective tactic to simplify terms using the operators of any ring algebraic structure [22], and Braibant and Pous built a reflective implementation of rewriting modulo associativity and commutativity of user-specified operators [10]. These past projects consider self-contained, well-defined problems in the style of classical decision problems. In contrast, our work considers open-ended, extensible procedures more along the lines of those commonly used for automated program verification. Such an expansion of scope raises the new issues that we have described, like supporting quantifiers, an interface with a proof assistant's unification engine, and modular combination of verified decision procedures over different theories. The last of these has been considered by Lescuyer [23], who developed an SMT implementation in Coq. The theory composition that he achieves is more integrated than the simple composite provers that we implemented, but the approach does not share the computational composition that enables us to achieve lightweight extension. The Ssreflect Coq library [19] employs a small-scale reflection style where many predicates are coded as functional programs returning Booleans, sidestepping concerns of decidability. The approach of Gonthier et al. [20] uses Coq's canonical structures mechanism as a clever means of building proof-generating procedures, retaining most of the usual relative advantages and disadvantages of proof generation versus verification of proof procedures like ours.
Other recent work has proposed Mtac [37], a new style of proof automation in Coq. Like reflective proofs, Mtac proof procedures are implemented in Gallina; however, in order to to provide the types of operations necessary for making tactic development simple, these "tactics" have a monadic type. The monad supports non-termination, failure, and syntactic matching of patterns against terms. The last of these features makes it impossible to reason about Mtac procedures inside of Coq, since syntactic matching breaks the substitution property of equality, i.e. x = y → f x = f y. Execution of these programs is done at type-elaboration time through a special run expression that exists outside of Gallina.
Several projects [8,27] have studied translation of SMT-solver proof traces into forms acceptable to proof assistants, and some of these projects [3,7] are based on reflective Coq tactics. In the latter case, one verifies a proof checker rather than the prover itself. Compared to our approach, there are non-obvious performance trade-offs. Verifying the prover removes the need for potentially expensive proof generation and checking, but the proof-generating approach is compatible with using efficient low-level languages and optimizing compilers to implement the provers. Verifying the prover helps avoid completeness bugs, where a tool may sometimes generate invalid proof traces; but proof checkers are generally easier to verify than provers. Lescuyer and Conchon [24] built a reflective SAT solver tactic for Coq, and Nanevski et al. [28] and Oe et al. [30] have verified efficient low-level code for a part of an SMT solver and a full SAT solver, respectively. None of this past work supports modular extension with new provers rather than just proof checkers, and none supports a rich formula language including quantifiers and user-specified predicates with associated axioms that should be applied automatically.
A few past projects have proved the correctness of non-extensible separation logic proof procedures. Marti and Affeldt [25] verified a simplification of Smallfoot [5] using Coq. Stewart et al. have done Coq verification of a Smallfoot-style verification tool VeriSmall [2] that relies on a novel verified heap theorem prover VeriStar [35]. The prior work of this kind has considered none of functional correctness verification (as opposed to just memory safety), extension with abstract predicates, or higher-order programs or specifications.
Many standalone tools do efficient, automated analysis of large low-level code bases for memory safety, using separation logic, outside of the context of proof assistants. Examples include Smallfoot [5], SpaceInvader [11], and SLAyer [6]. Xisa [12] bears a special relationship to our new work, as it is extensible with new predicate definitions in separation logic. Several other proof assistant libraries provide support for separation logic proofs, including the tactic libraries of Appel [1] and McCreight [26], Holfoot [36], Ynot [15], and Charge! [4]. Some of the libraries in this latter category provide proof automation comparable to that of the standalone tools. Our work described in this paper is the first to verify such automation formally, rather than merely constructing it to output program-specific proofs. One disadvantage of all such approaches is greater performance overhead compared to standalone tools, though traditional proof techniques can be applied directly in places where sophisticated, custom reasoning is necessary.

Conclusion
We have built a core reflective proof framework that supports user extensions via reflected lemmas and custom proof procedures that are packaged into reusable strategies. Our framework allows bidirectional communication with Coq's unification variables, supporting both the instantiation of existing unification variables and the construction of new unification variables.
To justify the framework's applicability, we have instantiated it to reason about a combination of higher-order and separation logic including support for user-defined abstract predicates. In addition to the benefit of user extension for handling abstract predicates, our reflective tactics scale much better performance-wise than Bedrock's original tactic-based verification procedure [13], while producing at least comparable performance for the realistic case studies that we have experimented with.
We succeeded in isolating two large chunks of separation logicbased verification engines. Looking forward, it seems that the ripest areas for performance improvements are the non-reflective portions. Building a larger library of (reusable) base theory provers in our framework would reduce the number of goals passed back to Ltac and enable us to apply more refinements reflectively. It would also be interesting see how the framework can be extended to capture fragments of higher-order logic that are common during verification. This would enable us, in simple cases, to avoid some of the ping-ponging which we believe is the source of much of our overhead. While full reflective verification would be ideal, the ability of our framework to integrate nicely with more manual proofs enables us to choose to invest time in automation only when there will be a comparable payoff, i.e. where similar obligations crop up repeatedly. Highly specialized reasoning can still be proved manually or semi-automatically.