Require Export lecture3. (* # Contents *) (* In this lecture, we will be formalizing a separation logic for the ML-like language that we discussed in the second lecture. The topics that we discuss during the lecture are as follows: - Separation logic - Deep embedding versus shallow embedding - Weakest preconditions *) (* # Separation logic *) (* The syntax of our separation logic is as follows: P, Q ::= emp | P ** Q | P -* Q | emp | l ~> v | All x, P | Ex x, P | | wp e (fun v => Q) We will not explain all of these connectives in detail right immediately. The semantics of most will be described in the course of this lecture, accompanied by a formal description in Coq. However, we will highlight the core connectives of separation logic, whose informal semantics is as follows: l ~> v := _the points-to connective_ the memory consists of exactly one location `l` with value `v` P ** Q := _separating conjunction_ the memory can be split into two _disjoint_ parts, so that the first satisfies P and the second satisfies Q emp := _the empty connective_ the memory is empty Using these connectives, one can give very precise descriptions of memory footprints, for example: l1 ~> 6 ** l2 ~> 8 \Ex v, l1 ~> v ** l2 ~> 2 * v The first example describes a memory that consists of two locations `l1` and `l2` that respectively contain the values 6 and 8. Since the separation conjunction `P ** Q` ensures that the parts of the memory described by `P` and `Q` are disjoint, we know that `l1` and `l2` are in different (i.e. they do not alias). This makes separating conjunction very different from conjunction `P /\ Q`, which says that `P` and `Q` both hold for the same memory. The second example describes the memories that contain two different locations `l1` and `l2`, so that the value of `l2` is twice that of `l1`. Making use of separating conjunction, we can give very concise specifications of programs that manipulate pointers, for example, take the following Hoare logic specification of the swap function: {{ l ~> v ** k ~> w }} swap l k {{ l ~> w ** k ~> v }} ^ ^ precondition postcondition This specification expresses that swap is indeed swapping, as witnessed by the fact that the values of the locations `l` and `k` have been swapped. But apart from that, it also expresses in the precondition that the locations `l` and `k` should be different before executing the program, and in the postcondition that these are still different. *) (* # Shallow embedding versus deep embedding *) (* In the second lecture, we have already seen how we could use Coq to model a programming language. In that section, we started by defining a _syntax_ for our language in terms of an inductive type, and then gave an operational semantics for this language. The approach of first defining a syntax is called a _deep embedding_. In this section, we proceed differently: we are not going to define an explicit syntax for our separation logic. Instead, we are going to define the connectives of our separation logic directly by their _semantic interpretation_. This is called a _shallow embedding_. *) (* # Shallow embedding of separation logic *) (* The first question that we need to answer is: what will be the semantic interpretation of the connectives of our separation logic? As we have just seen, formulas in separation logic describe sets of memories. For example: \Ex v, l1 ~> v ** l2 ~> 2 * v Describes describes all memories that contain two different locations `l1` and `l2`, so that the value of `l2` is twice that of `l1`. The natural way to describe sets of memories in Coq is by means of a predicate: *) Definition iProp := mem -> Prop. (* Now, let us try to give a semantic interpretation of separating conjunction. For this, recall the informal description of `P ** Q`: the memory can be split into two _disjoint_ parts, so that the first satisfies `P` and the second satisfies `Q`. In the previous lecture we put a lot of effort in formalizing disjointness and unions of finite maps. Since our memories are represented as finite maps, that effort will pay of now: *) Definition iSep (P Q : iProp) : iProp := fun m => exists m1 m2, m = munion m1 m2 /\ mdisjoint m1 m2 /\ P m1 /\ Q m2. Notation "P ** Q" := (iSep P Q) (at level 80, right associativity). (* Recall that in predicates are functions. So, to construct an element of type `iProp` (i.e. `mem -> Prop`) we use a lambda abstraction of Coq. In order to write down separation logic propositions in a concise way, we use the `Notation` command to setup Coq's parser and pretty printer. *) (* In the same spirit as separating conjunction, we can now describe the points-to connective and the empty connective. These definitions crucially relies on the singleton finite map `msingleton l v` and the empty finite map `mempty` that we have defined in the first lecture. *) Definition points_to (l : nat) (v : val) : iProp := fun m => m = msingleton l v. Notation "l ~> v" := (points_to l v) (at level 20). Definition iEmp : iProp := fun m => m = mempty. Notation "'emp'" := iEmp. (* Together with separating conjunction, we have a corresponding form of implication: the magic wand `P -* Q`. It describes the memories described by `Q` minus those described by `P`, i.e., it describes the memories such that, if you (disjointly) add memories satisfying `P`, you obtain resources satisfying `Q`. *) Definition iWand (P Q : iProp) : iProp := fun m => forall m2, mdisjoint m2 m -> P m2 -> Q (munion m2 m). Notation "P -* Q" := (iWand P Q) (at level 99, Q at level 200, right associativity). (* Finally, we lift the quantifiers of Coq into our separation logic. Note that we use the functions of Coq to represent the binder. *) Definition iForall {A} (P : A -> iProp) : iProp := fun m => forall x, P x m. Notation "'All' x1 .. xn , P" := (iForall (fun x1 => .. (iForall (fun xn => P)) ..)) (at level 200, x1 binder, xn binder, right associativity). Definition iExists {A} (P : A -> iProp) : iProp := fun m => exists x, P x m. Notation "'Ex' x1 .. xn , P" := (iExists (fun x1 => .. (iExists (fun xn => P)) ..)) (at level 200, x1 binder, xn binder, right associativity). (* In order to express mathematical statements in our separation logic (for as equality, that a number is even, ...), we will now define an embedding of Coq propositions (type `Prop`) into the propositions of our separation logic (type `iProp`). We define this using (higher-order) existential quantification. *) Definition iPure (p : Prop) : iProp := Ex _ : p, emp. Notation "@[ p ]" := (iPure p) (at level 20, p at level 200). (* ## Weakest preconditions and Hoare triples *) (* So far, we have defined basic logical connectives of our separation logic. But last, but not least, we of course need some way of expressing properties of programs. We do this using Hoare triples: {{ P }} e {{ Q }} In this lecture, we consider Hoare triples for total program correctness, so the intuitive meaning of the Hoare triple is: If the precondition holds for the memory before hand, then the expression `e` results in a value `v`, such that `Q v` holds. Note that our postconditions have type `val -> iProp`, which allows us to talk about the result value of an expression. For example: {{ l ~> v ** k ~> w }} swap l k {{ fun v => @[ v = VUnit ] ** l ~> w ** k ~> v }} Instead of defining Hoare triples directly, we first define the notion of weakest preconditions. wp e Q And then define Hoare triples as : {{ P }} e {{ Q }} := P |- wp e Q Where |- is the point-wise entailment of `iProp`: *) Definition iEntails (P Q : iProp) : Prop := forall m, P m -> Q m. Notation "P |- Q" := (iEntails P Q) (at level 99, Q at level 200). (* Now, before we give the formal definition of weakest preconditions, there is one final catch. An important part of separation logic is that we have the so-called _framing_ rule: {{ P }} e {{ Q }} ----------------------------- {{ P ** R }} e {{ P ** R }} Let us see this rule in action. As we have seen before, we can give the following specification to the swap function: {{ l ~> v ** k ~> w }} swap l k {{ l ~> w ** k ~> v }} This specification looks overly restrictive as the pre- and post-condition say that the memory should exactly contain the locations `l` and `k`. However, using the frame rule, we can derive also: {{ l ~> v ** k ~> w ** R }} swap l k {{ l ~> w ** k ~> v ** R }} Where `R` is any formula of separation logic, for example, `k' ~> w'` for some other location `k'`. As we see here, the frame rule allows us to reason locally: we can state and prove specifications with a small memory footprint, and then use framing to extend them to bigger memory footprints, so that we can use the specification in larger contexts too. In order to make sure that we can prove the framing rule, the definition of weakest preconditions becomes slightly more complicated. We also have to qualify over all possible frames `mf`: *) Definition wp (e : expr) (Q : val -> iProp) : iProp := fun m => forall mf, mdisjoint m mf -> exists m' v, mdisjoint m' mf /\ big_step e (munion m mf) v (munion m' mf) /\ Q v m'. Definition hoare (P : iProp) (e : expr) (Q : val -> iProp) : Prop := P |- wp e Q. (* # Basic rules of the logic *) (* Let us first prove that entailment |- is a pre-order, i.e. it is reflexive and transitive. These properties are crucial to compose proofs. *) Lemma iEntails_refl P : P |- P. Proof. unfold iEntails. intros m Hm. assumption. Qed. Lemma iEntails_trans P Q R : (P |- Q) -> (Q |- R) -> P |- R. Proof. (* Note that we do not really have to unfold `iEntails`: the `intros` tactic will do that for us. *) intros HPQ HQR m HP. apply HQR. apply HPQ. assumption. Qed. (* ## Rules for separating conjunction *) (* We now prove the key properties of separating conjunction: monotonicity, the identity laws w.r.t. `emp`, commutativity, and associativity. *) Lemma iSep_mono_l P1 P2 Q : (P1 |- P2) -> P1 ** Q |- P2 ** Q. Proof. intros HP m Hm. unfold iSep in Hm. destruct Hm as [m1 Hm]. destruct Hm as [m2 Hm]. destruct Hm as [Heq Hm]. destruct Hm as [Hdisj Hm]. destruct Hm as [HPm1 HQm2]. (* As we see, writing down this sequence of existential and conjunction eliminations becomes pretty tedious. It requires many tactics, and we have to name all the auxiliary results. Fortunately, Coq allows us to nest these `destruct`s in the following way. *) Restart. intros HP m Hm. unfold iSep in Hm. destruct Hm as [m1 [m2 [Heq [Hdisj [HPm1 HQm2]]]]]. (* This is much shorter, but still results in a lot of brackets. Coq provides yet another syntax to make this more concise. *) Restart. intros HP m Hm. unfold iSep in Hm. destruct Hm as (m1 & m2 & Heq & Hdisj & HPm1 & HQm2). (* The syntax `(x1 & x2 & ... & xn)` is just sugar for `[x1 [x2 [... [xn-1 xn]]]]`. And finally, we can even perform the eliminations directly while introducing: *) Restart. intros HP m (m1 & m2 & Heq & Hdisj & HP1 & HQ). subst m. unfold iSep. exists m1, m2. split. { reflexivity. } split. { assumption. } split. { apply HP. assumption. } assumption. Restart. (* Or to make the proof even shorter, we can use `eauto`, which uses all hypotheses (including implications, like the entailment) in the context by default. *) intros HP m (m1 & m2 & Heq & Hdisj & HPm1 & HQm2). subst m. unfold iSep. eauto 10. Qed. Lemma iSep_comm P Q : P ** Q |- Q ** P. Proof. intros m (m1 & m2 & Heq & Hdisj & HP & HQ). exists m2, m1. rewrite <-munion_comm by assumption. auto using mdisjoint_sym. Qed. Lemma iSep_assoc P Q R : P ** (Q ** R) |- (P ** Q) ** R. Proof. intros m (m1 & m' & Heq & Hdisj & HP & (m2 & m3 & Heq' & Hdisj' & HQ & HR)). subst. exists (munion m1 m2), m3. split. { rewrite munion_assoc. reflexivity. } split. { eauto using mdisjoint_union_l, mdisjoint_union_inv_rr. } split. { exists m1, m2. eauto using mdisjoint_union_inv_rl. } assumption. Qed. (** ### Exercise *) (* Prove the following laws. *) Lemma iSep_emp_l P : P |- emp ** P. Proof. Admitted. Lemma iSep_emp_l_inv P : emp ** P |- P. Proof. Admitted. (* ## Rules for magic wand *) Lemma iWand_intro_r P Q R : (P ** Q |- R) -> P |- Q -* R. Proof. intros H m Hm m' ? HQ. apply H. rewrite munion_comm by assumption. exists m, m'; auto using mdisjoint_sym. Qed. Lemma iWand_elim P Q : P ** (P -* Q) |- Q. Proof. intros m (m1 & m2 & ? & ? & ? & ?). subst. apply H2; auto. Qed. (* ## Rules for universal quantification *) Lemma iForall_intro {A} P (Q : A -> iProp) : (forall x, P |- Q x) -> (P |- All x, Q x). Proof. intros H m HP x. apply H. assumption. Qed. Lemma iForall_elim {A} (P : A -> iProp) x : (All z, P z) |- P x. Proof. intros m HP. apply HP. Qed. (* ## Rules for existential quantification *) (* ### Exercise *) (* Prove the following laws. *) Lemma iExist_intro {A} (P : A -> iProp) x : P x |- Ex z, P z. Proof. Admitted. Lemma iExist_elim {A} (P : A -> iProp) Q : (forall x, P x |- Q) -> (Ex z, P z) |- Q. Proof. Admitted. (* ## Derived rules of the logic *) (* So far, we have proved a bunch of rules for our separation logic by unfolding the definition of the separation logic connectives. However, it turns out that many additional rules can be _derived_ from the rules we have seen so far. *) Lemma iSep_emp_r P : P |- P ** emp. Proof. apply iEntails_trans with (emp ** P). { apply iSep_emp_l. } apply iSep_comm. Qed. Lemma iSep_emp_r_inv P : P ** emp |- P. Proof. apply iEntails_trans with (emp ** P). { apply iSep_comm. } apply iSep_emp_l_inv. Qed. Lemma iSep_mono_r P Q1 Q2 : (Q1 |- Q2) -> P ** Q1 |- P ** Q2. Proof. intros HQ. apply iEntails_trans with (Q1 ** P). { apply iSep_comm. } apply iEntails_trans with (Q2 ** P). { apply iSep_mono_l. assumption. } apply iSep_comm. Qed. (*** ## Exercise *) (* Prove the following derived laws. Make sure to not unfold the definitions of the connectives of our separation logic, but only use the rules we have already proven. Hint: you need to use transitivity of entailment many times. *) Lemma iSep_mono P1 P2 Q1 Q2 : (P1 |- P2) -> (Q1 |- Q2) -> P1 ** Q1 |- P2 ** Q2. Proof. Admitted. (* This lemma is quite subtle: you need to use the rules for commutativity, associativity and monotonicity of separating conjunction. I advice to first work out the proof on paper, and then do it in Coq. *) Lemma iSep_assoc' P Q R : (P ** Q) ** R |- P ** (Q ** R). Proof. Admitted. Lemma iWand_intro_l P Q R : (Q ** P |- R) -> P |- Q -* R. Proof. Admitted. (* This lemma is very difficult: you need to use the introduction and elimination rules for magic wand `-*`. *) Lemma iExist_sep {A} (P : A -> iProp) Q : (Ex x, P x) ** Q |- Ex x, P x ** Q. Proof. Admitted. Lemma iPure_intro (p : Prop) : p -> emp |- @[ p ]. Proof. Admitted. Lemma iPure_elim (p : Prop) P Q : (p -> P |- Q) -> @[ p ] ** P |- Q. Proof. Admitted. (* ## Logical rules for weakest preconditions *) (* We now prove the framing rule for weakest preconditions. Convince yourself that from this rule, we get the framing rule for Hoare triples. The proof below is a bit subtle, as we need to use properties about `munion` and `mdisjoint` many times. *) Lemma wp_frame Q R e : wp e Q ** R |- wp e (fun v => Q v ** R). Proof. intros m (m1 & m2 & Heq & Hdisj & Hwp & HR). subst m. intros mf Hm. destruct (Hwp (munion m2 mf)) as (m' & v & Hdisj' & Hbig & HQ). { eauto using mdisjoint_union_r, mdisjoint_union_inv_ll. } exists (munion m' m2), v. split. { eauto 4 using mdisjoint_union_l, mdisjoint_union_inv_rr, mdisjoint_union_inv_lr. } split. { rewrite <-!munion_assoc. assumption. } exists m', m2. split. { reflexivity. } eauto using mdisjoint_union_inv_rl. Qed. (* ### Exercise *) (* Prove the rule below. *) Lemma wp_mono Q R e : (forall v, Q v |- R v) -> wp e Q |- wp e R. Proof. Admitted. (* # Structural rules for weakest preconditions *) Lemma wp_val v Q : Q v |- wp (EVal v) Q. Proof. intros m HQ mf Hm. exists m, v. eauto with big_step. Qed. Lemma wp_ctx E e Q : wp e (fun w => wp (fill E (EVal w)) Q) |- wp (fill E e) Q. Proof. intros m Hwp mf Hdisj. destruct (Hwp mf) as (m' & v & Hdisj' & Hbig & Hwp'). { assumption. } destruct (Hwp' mf) as (m'' & v'' & Hdisj'' & Hbig' & HQ). { assumption. } eauto 10 using big_step_fill. Qed. Lemma wp_let x e1 e2 Q : wp e1 (fun v => wp (subst x v e2) Q) |- wp (ELet x e1 e2) Q. Proof. intros m Hwp mf Hdisj. destruct (Hwp mf Hdisj) as (mf' & v' & Hdisj' & Hbig & Hwp2). destruct (Hwp2 mf Hdisj') as (mf'' & v'' & Hdisj'' & Hbig' & HQ). exists mf'', v''. eauto with big_step. Qed. (* ### Exercise *) (* Prove the rules below: all of these follow the same structure as the proofs above. *) Lemma wp_seq e1 e2 Q : wp e1 (fun _ => wp e2 Q) |- wp (ESeq e1 e2) Q. Proof. Admitted. Lemma wp_if_true e2 e3 Q : wp e2 Q |- wp (EIf (EVal (VBool true)) e2 e3) Q. Proof. Admitted. Lemma wp_if_false e2 e3 Q : wp e3 Q |- wp (EIf (EVal (VBool false)) e2 e3) Q. Proof. Admitted. Lemma wp_while e1 e2 Q : wp (EIf e1 (ESeq e2 (EWhile e1 e2)) (EVal VUnit)) Q |- wp (EWhile e1 e2) Q. Proof. Admitted. Lemma wp_op op v1 v2 v Q : eval_bin_op op v1 v2 = Some v -> Q v |- wp (EOp op (EVal v1) (EVal v2)) Q. Proof. Admitted. (* # Stateful rules for weakest preconditions *) (* We finish by proving the rules for the operations of our language that manipulate the state. Let us take a look at the rules for load and store (in mixed informal and Coq notations): l ~> v ** (l ~> v -* Q v) |- wp !l Q. l ~> v ** (l ~> w -* Q VUnit) |- wp (!l := w) Q. So, what does the second rule say: In order to prove the weakest precondition of a store, we have to show that `l` already exists in the memory. This is done by showing that we have the points-to connective `l ~> v`. Now, since the only way of introducing a separating conjunction is monotonicity, this means we have to give up the point-to connective `l ~> v`, which leaves us with `l ~> w -* Q VUnit`. By introducing the magic wand, we consequentially get the point-to connective back, but now with the new value `w`. The same kind of pattern is used for the other rules. *) Lemma wp_load l v Q : l ~> v ** (l ~> v -* Q v) |- wp (ELoad (EVal (VLoc l))) Q. Proof. intros m HQ mf Hdisj. destruct HQ as (m1 & m2 & Heq & Hdisj' & Hpointsto & HQ). subst m. unfold points_to in Hpointsto. subst m1. exists (munion (msingleton l v) m2), v. split. { assumption. } split. { eapply Load_big_step. - eapply Val_big_step. - rewrite <-munion_assoc. rewrite munion_lookup. rewrite msingleton_lookup. simpl. reflexivity. } apply HQ. { assumption. } unfold points_to. reflexivity. Qed. (* ### Exercise (very difficult) *) (* Prove the rules below: all of these follow the same structure as the proofs above. Hint: you need to make extensive use of properties about finite maps. Use Coq's `Search` command to find these. For example: *) Search (minsert _ _ (msingleton _ _) = _). (* Yields: minsert_singleton: forall (A : Type) (i : nat) (x y : A), minsert i x (msingleton i y) = msingleton i x *) Lemma wp_store l v w Q : l ~> v ** (l ~> w -* Q VUnit) |- wp (EStore (EVal (VLoc l)) (EVal w)) Q. Proof. Admitted. Lemma wp_alloc v Q : (All l, l ~> v -* Q (VLoc l)) |- wp (EAlloc (EVal v)) Q. Proof. Admitted. Lemma wp_free l v Q : l ~> v ** Q VUnit |- wp (EFree (EVal (VLoc l))) Q. Proof. Admitted. (* # An example *) (* Finally, we will look at an example. We take the simplest program that manipulates pointers: the swap function. *) (* swap x y := let tmp := !x in x := !y; y := tmp *) Definition swap (x y : val) : expr := ELet "tmp" (ELoad (EVal x)) (ESeq (EStore (EVal x) (ELoad (EVal y))) (EStore (EVal y) (EVar "tmp"))). (* Transitivity with the premises swapped *) Lemma iEntails_trans' P Q R : (Q |- R) -> (P |- Q) -> P |- R. Proof. eauto using iEntails_trans. Qed. Lemma swap_correct l k v w : hoare (l ~> v ** k ~> w) (swap (VLoc l) (VLoc k)) (fun ret => @[ret = VUnit] ** l ~> w ** k ~> v). Proof. unfold hoare. unfold swap. eapply iEntails_trans'. { apply wp_let. } eapply iEntails_trans'. { apply wp_load. } apply iSep_mono_r. apply iWand_intro_l. simpl. eapply iEntails_trans'. { apply (wp_ctx (SeqCtx _ :: StoreCtxR _ :: nil)). } eapply iEntails_trans'. { apply wp_load. } eapply iEntails_trans'. { apply iSep_comm. } apply iSep_mono_l. apply iWand_intro_r. simpl. eapply iEntails_trans'. { apply (wp_ctx (SeqCtx _ :: nil)). } eapply iEntails_trans'. { apply wp_store. } apply iSep_mono_r. apply iWand_intro_l. simpl. eapply iEntails_trans'. { apply wp_seq. } eapply iEntails_trans'. { apply wp_val. } eapply iEntails_trans'. { apply wp_store. } eapply iEntails_trans'. { apply iSep_comm. } apply iSep_mono_l. apply iWand_intro_r. eapply iEntails_trans. { apply iSep_emp_l. } apply iSep_mono_l. apply iPure_intro. reflexivity. Qed. (* # Conclusion *) (* As you have seen, proving the correctness of our simple example already takes a lot of work. For example, we have to: - Use transitivity of entailment to use the weakest precondition rules. - After we have used a rule for a load or store, we have to "frame". - We often have to reorder the premises on the LHS of the turnstile. - We have to pick evaluation contexts ourselves when we use `wp_ctx`. Of course, most of these steps are completely mechanical, and as such, Coq is well capable of automating this. Unfortunately, automating proofs in separation logic in Coq is beyond the scope of these lectures. If you would like to know more about it, please take a look at the following paper: Robbert Krebbers, Amin Timany and Lars Birkedal Interactive Proofs in Higher-Order Concurrent Separation Logic POPL 2017 If you want to see separation logic in Coq in practice, try the Iris project, a state-of-the art higher-order concurrent separation logic that has been implemented and verified in Coq: http://iris-project.org *)