Proof Pearl: Magic Wand as Frame

09/19/2019 ∙ by Qinxiang Cao, et al. ∙ 0

Separation logic adds two connectives to assertion languages: separating conjunction * ("star") and its adjoint, separating implication -* ("magic wand"). Comparatively, separating implication is less widely used. This paper demonstrates that by using magic wand to express frames that relate mutable local portions of data structures to global portions, we can exploit its power while proofs are still easily understandable. Many useful separation logic theorems about partial data structures can now be proved by simple automated tactics, which were usually proved by induction. This magic-wand-as-frame technique is especially useful when formalizing the proofs by a high order logic. We verify binary search tree insert in Coq as an example to demonstrate this proof technique.



There are no comments yet.


page 2

page 4

page 5

page 6

page 12

page 14

page 16

page 17

This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1 Introduction

Separation logic [15] is an extension of Hoare logic that has been widely used in program verification. The separating conjunction (“star”) in assertions represents the existence of two disjoint states, one that satisfies and one that satisfies . Formally,

Here, represents the disjoint union of two pieces of state/memory. The concisely expresses address (anti)aliasing. For example, if “” is the assertion that data is stored at address , then says is stored at address , is stored at address , and . Separation logic enables one to verify a Hoare triple locally but use it globally, using the frame rule:

Star has a right adjoint separating implication, a.k.a. “magic wand”:

And the following rules are called adjoint rules:

Magic wands are famously difficult to control [16]. In the early days of separation logic, magic wand was used to generate weakest preconditions and verification conditions for automated program verification. However, those verification conditions are not human readable or understandable, and decision procedures for entailment checking with magic wand are quite complex.

In most works of interactive program verification, magic wand is not a necessary component. Authors tend to use forward verification instead of backward verification since it is easier to understand a program correctness proof that goes in the same direction as program execution. “Forward” Hoare logic rules do not generate magic wand expressions; therefore, most authors find that the expressive power of star is already strong enough. For example, we need to define separation logic predicates for different data structures (like records, arrays, linked list and binary trees) in order to verify related programs. Berdine et al. [4] and Charguéraud [8] show that these predicates can be defined with separating conjunction only.

struct tree {int key; void *value;              struct tree *left, *right;}; typedef struct tree **treebox; void insert (treebox p, int x, void *v) {   struct tree *q;   while (1) {   q = *p;   if (q==NULL) {     q = (struct tree *) surely_malloc (sizeof *p);     q$\to$key=x; q$\to$value=v;     q$\to$left=NULL; q$\to$right=NULL;     *p=q;     return;   } else {     int y = q$\to$key;     if (x<y)       p= &q$\to$left;     else if (y<x)       p= &q$\to$right;     else {       q$\to$value=v;       return; } } } }
Figure 1: Binary Search Tree insertion
Figure 2: Execution of .

In this paper, we propose a proof technique: magic wand as frame. Specifically, we propose to use magic wand together with quantifiers to define separation logic predicates for partial data structures.

The main content of this paper is a proof pearl. We use our magic-wand-as-frame technique to verify the C program in Fig. 2, insertion into a binary search tree (BST). The program uses a while loop to walk down from the root to the location to insert the new element.

A pointer to a tree has type struct tree *, but we also need the type pointer-to-pointer-to-tree, which we call treebox. The insert function does not return a new tree, it modifies the old tree, perhaps replacing it entirely (if the old tree were the empty tree).

Consider running insert(,8,“h")", where points to a treebox containing the root of a tree as shown in Fig. 2. After one iteration of the loop or two iterations, variable contains address , which is a treebox containing a pointer to a subtree. This subtree and a partial tree (shown within the dashed line) form the original BST.

Naturally, we can verify such a program using a loop invariant with the following form:

To describe partial trees, most authors [4, 8] would have you introduce a new inductive description of tree with exactly one hole—in addition to the inductive description of ordinary trees—and define a corresponding recursive separation-logic predicate “partial tree ”, in addition to the recursive predicate for ordinary trees. (Similarly, “list segment” is inductively defined as a list with one hole.)

That’s a lot of duplication. We propose a different approach in this paper: using magic wand and universal quantifier to express “”. Specifically, it is defined as:

in which represents the result of filling the hole in with .

It has two benefits. (1) Important properties of the partial-tree predicate can now be proved by wand-adjoint directly. In our Coq formalization, most of these proofs have only a simple line of tacitcs. In comparions, similar properties were usually proved by induction which are hard to automate. (2) We do not even need to define “partial trees” as a new inductive type in Coq. Instead, we treat partial trees as functions from trees to trees. As a result, we get equations like for free.

We organize the rest of this paper as follows:

  • We verify this C implementation of BST insert using magic-wand-as-frame.

  • We formalize this correctness proof in Coq using Verifiable C [3].

  • We show that magic-wand-as-frame also works for other implementations of BST insert, other operations of BST, and other data structures such as linked lists.

  • We demonstrate our tactic program for proving partial data structure’s elimination rules and composition rules.

  • We compare our proofs with traditional approaches which use recursively defined predicates. We discuss the power and limitation of using magic wand and we explain the name of our proof technique magic-wand-as-frame.

  • We discuss related work of using magic wand and summarize our contributions.

Remark 1. The purpose of this paper is NOT about GENERAL magic-wand-involved program verification. What we do propose is a very disciplined way of using magic wand, which can make program correctness proofs more elegant.

Remark 2. Using magic wand, using quantifiers and using high order logics are known methods and tools in program verification. We do not claim the invention of any one of them. Instead, we show a specific way to put them together and make proofs easier.

2 Proof Pearl: Binary search tree insertion

This section demonstrates the main content of this paper: a magic-wand-as-frame verification of BST insert. Here we use standard mathematical notation; in the next section we give details about the Coq formalization and the proof notation of Verifiable C.

2.1 Specification

Correctness for BSTs means that the insert function—considered as an operation of an abstract data type—implements the update operation on finite maps from the key type (in this case, integer) to the range type (in this case void*). The client of a finite map does not need to know that trees are used; we should hide that information in our specifications. For that purpose, we define separation logic predicates for binary trees, and we define map predicates based on tree predicates. Only map predicates show up in the specification of this insert function.


Here represents the search-tree property. That is, at any node of the tree , the keys in the left subtree are strictly less than the key at the node, and the keys in the right subtree are strictly greater. is an abstraction relation, which says that is an abstraction of tree , i.e., the key-value pairs in map and tree are identical. and are formally defined in the SearchTree chapter of Verified Functional Algorithms [2]. Their exact definitions are not needed in our proof here.

Our high-level separation-logic specification of insert function is:

We use to represent the value of program variable . In the postcondition, is the usual update operation on maps.

2.2 Two-level proof strategy

One could directly prove the correctness of the C-language insert function, using the search-tree property as an invariant. But it is more modular and scalable to do a two-level proof instead [1, 9]: First, prove that the C program (imperatively, destructively) implements the (mathematical, functional) function on binary search trees; then prove that the (pure functional) binary search trees implement (mathematical) finite maps, that implements update, and that preserves the search-tree property. So let us define insertion on pure-functional tree structures:

The SearchTree chapter of VFA [2] proves (via the Abs relation) that this implements update on abstract finite maps. Next, we’ll prove that the C program refines this functional program; then compose the two proofs to show that the C program satisfies its specification given at the end of §2.1.

For that refinement proof, we give a low-level separation-logic specification of the insert function, i.e., the C program refines the functional program:


2.3 Magic wand for partial trees

The function body of is just one loop. We will need a loop invariant! As shown in Fig. 2, the original binary tree can always be divided into two parts after every loop body iteration: one is a subtree whose root is tracked by program variable (that is, is the address of ’s root node) and another part is a partial tree whose root is identical with the original tree and whose hole is marked by address .

The separation logic predicate for trees (also subtrees) is . We define the separation logic predicate for partial trees as follows. Given a partial tree , which is a function from binary trees to binary trees:

This predicate has some important properties and we will use these properties in the verification of . (3a) and (3b) show how single-layer partial trees are constructed. (3c) shows the construction of empty partial trees. (3d) shows that a subtree can be filled in the hole of a partial tree. And (3e) shows the composition of partial trees.



2.4 Wand-frame proof rules

These properties are direct instances of the magic-wand-as-frame proof rules, which are all derived rules from minimum first-order separation logic (i.e. intuitionistic first order logic + commutativity and associativity of separating conjunction + being separating conjunction unit + wand-ajoint).

Here are the proof rules:


wandQ-frame-intro proves (3a), (3b) and (3c). wandQ-frame-elim proves (3d). wandQ-frame-ver and wandQ-frame-refine together prove (3e).

Remark 1. Theoretically, in order to prove (3a)-(3e), these wandQ-frame rules are the only additional proof rules that we need for magic wand and quantifiers. But in practice, when we formalize these proofs in Coq, we also use other proof rules like wand-adjoint and the universal quantifier introduction rule.

Remark 2. The soundness of (3c) (3d) and (3e) do not even depend on the definition of .

2.5 Implementation correctness proof

Now, we can verify the function with the loop invariant,

It says, a partial tree and a tree are stored in disjoint pieces of memory, and if we apply the function to locally and fill the hole in with that result, then we will get the same as directly applying to the original binary tree .

The correctness of is based on the following two facts. First, the precondition of implies this loop invariant because we can instantiate the existential variables , and with , and and apply property (3c). Second, the loop body preserves this loop invariant and every return command satisfies the postcondition of the whole C function. Fig. 3 shows our proof (for conciseness, we omit in all assertions).

2q = * p;
3if (q == NULL) {
4   \\
5   \\
6   q = (struct tree *) surely_malloc (sizeof *q);
7   q$\to$key=x; q$\to$value=v;
8   q$\to$left=NULL; q$\to$right=NULL;
9   *p=q;
10   \\ 
11   \\ 
12   \\
13   return; 
14} else {
15   \\ 
16    int y = q$\to$key; 
17    if (x<y)
18        p= &q$\to$left; 
19        \\ 
20        \\
21    else if (y<x)
22        p= &q$\to$right; 
23        \\
24    else {
25        p$\to$value=v; 
26        \\
27        \\
28        \\
29        return; 
30   } }
Figure 3: Proof of loop body

This loop body has four branches: two of them end with return commands and the other two end normally. In the first branch, the inserted key does not appear in the original tree. This branch ends with a return command at line 3. We show that the program state at that point satisfies the postcondition of the whole function body (line 3). The transition from line 3 to line 3 is sound due to rule (3d). The second branch contains only one command at line 3. We re-establish our loop invariant in this branch (line 3). The transition from line 3 to line 3 is due to rule (3a) and the transition from line 3 to line 3 is due to rule (3e). The third branch at line 3 is like the second, and the last branch is like the first one.

In summary, the partial tree is established as an empty partial tree () in the beginning. The program merges one small piece of subtree into the partial tree in each iteration of the loop body. Finally, when the program returns, it establishes a local insertion result () and fills it in the hole of that partial tree—we know the result must be equivalent with directly applying insertion to the original binary tree. The diagrams above illustrate the situations of these four branches and our proof verifies this process.

3 Coq formalization in Verifiable C

We machine-check this proof in Coq, using the Verified Software Toolchain’s Verifiable C program logic [3], which is already proved sound w.r.t. CompCert Clight [6]. We import from Verified Functional Algorithms the definition of purely functional search trees and their properties. Readers can find our Coq development online:

We formalize our proof using Verifiable C’s interactive symbolic execution system in Coq [7]. Until now, Verifiable C had not included much proof theory for wand, except the basic wand-adjoint. Now we add the proof rules of wand-frame (see wandQ_frame.v) as derived lemmas from Verifiable C’s basic separation logic. We use them in the Coq proof of ’s properties (see §3.1 and bst_lemmas.v).

3.1 Separation logic predicates and properties for BST

Binary trees with keys and values are already formalized in VFA as an inductive data type in Coq. Here, we will formalize the separation logic predicate . Instead of defining directly as in (1), we first define , then define based on that. Finally, we prove that it satisfies the equalities in (1). We choose to do this because C functions for BST operations do not always take arguments with type (struct tree * *) (or equivalently, treebox). For example, a look-up operation does not modify a BST, so it can just take a BST by an argument with type (struct tree *).

  Fixpoint tree_rep (t: tree val) (p: val) : mpred :=
   match t with
   | E => !!(p=nullval) && emp
   | T a x v b =>
      EX pa:val, EX pb:val,
         data_at Tsh t_struct_tree (Vint (Int.repr (Z.of_nat x)),(v,(pa,pb))) p *
         tree_rep a pa * tree_rep b pb
  Definition treebox_rep (t: tree val) (b: val) :=
   EX p: val, data_at Tsh (tptr t_struct_tree) p b * tree_rep t p.
  Lemma treebox_rep_spec: forall (t: tree val) (b: val),
    treebox_rep t b =
    EX p: val,
    data_at Tsh (tptr t_struct_tree) p b *
    match t with
    | E => !!(p=nullval) && emp
    | T l x v r =>
        field_at Tsh t_struct_tree [StructField _key] (Vint (Int.repr (Z.of_nat x))) p *
        field_at Tsh t_struct_tree [StructField _value] v p *
        treebox_rep l (field_address t_struct_tree [StructField _left] p) *
        treebox_rep r (field_address t_struct_tree [StructField _right] p)

Here, val is CompCert Clight’s value type; nullval has type val and represents the value of NULL pointer. The Coq type mpred is the type of Verifiable C’s separation logic predicates. “&&”, “” and “” are notations for conjunction, separating conjunction and existential quantifiers in Verifiable C’s assertion language. “!! _” is the notation that injects Coq propositions into the assertion language. The expression (Vint (Int.repr (Z.of_nat x)) injects a natural number x into the integers, then to a 32-bit integer,111Mapping to is not injective; in a practical application the client of this search-tree module should prove that . then to CompCert Clight’s value type, val.

Data_at is a mapsto-like predicate for C aggregate types. Here,

  data_at Tsh t_struct_tree (Vint (Int.repr (Z.of_nat x)),(v,(pa,pb))) p

means that x, v, pa, pb are four fields of the “struct tree” stored at address p. Tsh means top share (full read/write permission). Verifiable C’s field_at is like data_at but permits a field name such as “”.

Our partial tree predicate partialT does not care how treebox_rep works internally. Thus, in defining the proof theory of partial trees, we parameterize over the treebox predicate. In consequence, these parameterized proofs can be applied on both partial_treebox_rep and partial_tree_rep.

Definition partialT (rep: tree val  val  mpred) (P: tree val  tree val) (p_root p_in: val) :=
      ALL t: tree val, rep t p_in  rep (P t) p_root.
Definition partial_treebox_rep := partialT treebox_rep.
Definition partial_tree_rep := partialT tree_rep.

As claimed in §2, the soundness of rules (3c) (3d) and (3e) do not depend on the definition of ; we prove them sound for arbitrary partial tree predicates. For example, the following lemma is the generalized version of (3d). We directly prove it by wandQ-frame-elim.

Lemma rep_partialT_rep: forall rep t P p q, rep t p * partialT rep P q p  rep (P t) q.
Proof. intros. exact (wandQ_frame_elim _ (fun t => rep t p) (fun t => rep (P t) q) t). Qed.

We define based on , and as described in §2; and and are already defined in VFA. Similarly, we define based on ; application of it can be found in §4.

3.2 C program specification and verification

Specification and Coq proof goal. Verifiable C requires users to write C function specification in a canonical form. The following is the specification of C function . The WITH clause there says that this specification is a parameterized Hoare triple—that is, for any p0, x, v, m0, this specific triple is valid. The brackets after PRE hold the C argument list. CompCert Clight turns every C variable into an identifier in the Clight abstract syntax tree defined in Coq. In this argument list, _p is the identifier for C variable , etc. The brackets after POST hold the C function return type.

Definition insert_spec :=
 DECLARE _insert
  WITH p0: val, x: nat, v: val, m0: total_map val
  PRE  [ _p OF (tptr (tptr t_struct_tree)), _x OF tint, _v OF (tptr Tvoid)   ]
    LOCAL(temp _p p0; temp _x (Vint (Int.repr (Z.of_nat x))); temp _v v)
    SEP (Mapbox_rep m0 p0)
  POST [ Tvoid ]
    PROP() LOCAL() SEP (Mapbox_rep (t_update m0 x v) p0).

Both precondition and postcondition are written in a PROP/LOCAL/SEP form. PROP clauses are for program-variable-irrelevant pure facts; there happen to be none here. LOCAL clauses talk about the values of program variables. For example, temp _p p0 says . SEP clauses are separating conjuncts. Verifiable C requires users to isolate programs variables in their assertions—SEP conjuncts do not refer directly to C program variables—so we use LOCAL clauses to connect program variables to PROP and SEP clauses.

Theorem body_insert: semax_body Vprog Gprog f_insert insert_spec.
 (* The C function f_insert (Fig. 1) implements its specification *)
  . . . (* 6 lines of reduction proof and *)
  . . . (* 65 lines of forward proof in separation logic *)

The proof contains two parts. One is to reduce the abstract specification to the concrete specification (6 lines). The other is forward verification using separation logic (65 lines)—it shares exactly the same structure with the pen-and-paper proof (30 lines) in Fig. 3. The proof scripts can be found in our Coq development. We omit them here.

4 Other data structures, programs and proofs

Magic-wand-as-frame is a pretty flexible proof technique. We briefly introduce some other possibilities in magic-wand-as-frame proofs here. Interested readers can download our Coq development for more details.

4.1 Other BST operations.

We also verify C implementations of BST delete and look-up operation with the magic-wand-as-frame technique. In the verification of BST delete, we also use to describe partial trees and use rules (3a-3e) to complete the proof. In the verification of BST look-up, we define using parameterized partialT (see §3) and prove similar proof rules for it. Especifically, we get the counterparts of (3c-3e) for free because we have already proved them for general partialT predicates. Proofs of the other two are also very straightforward using wandQ-intro.

4.2 Other data structure: linked list.

We also use magic-wand-as-frame to verify linked list append (see verif_list.v). In that proof, we use the following separation logic predicates and proof rules (see list_lemmas.v). These proof rules are direct instances of wandQ-frame rules. Here, we use to represent the list concatenation of and .


4.3 Other data structure: C aggregate type.

Another application of magic-wand-as-frame to verify load/store rules for nested C structs. For example, the following is a nested struct definition in C.

struct s1 { int f11; int f12; int f13; int f14; int f15; };
struct s2 { struct s2 f21; struct s2 f22; struct s2 f23; };
struct s2 * p; int x;

We want to derive the following rules

from the following primary rules:

in which represents a tuple of integer tuples, represents the -th element of the -th element of and is the result of replacing the -th element of the -th element of with . And later, we will use to represent the -th element of . We will use and to represent results of replacing the -th element of , and replacing the -th element of .

The derivation above is completed by rule of consequence, the frame rule and the following facts (5) (6):