From iris.algebra Require Export list gmap. From iris.heap_lang Require Export notation proofmode metatheory. From iris.heap_lang.lib Require Export assert. (** * Iris's HeapLang language *) (** The Iris framework is parameterized by the language of program expressions that one wishes to reason about, which makes it possible that the same logic can be used for a wide variety of languages. For the purpose of the lecture material, we consider the concurrent ML-style language *HeapLang*---the default language shipped with the Iris Coq development. We use HeapLang instead of defining our own language since the Iris development provides support for nice syntax for HeapLang programs, contains a number of useful tactics for reasoning about HeapLang programs, and contains a number of results about HeapLang's metatheory (e.g. substitution). While HeapLang itself is an untyped language, the purpose of this tutorial is to define a type system for HeapLang, and then use semantic typing via logical relations to prove type safety of HeapLang. Before we consider typing, we will see the following in this file: - How to write HeapLang programs. - How to write specifications using weakest preconditions. - How to use the Iris tactic language to prove such specifications. Before we get started, let us give a brief overview of the features and syntax of HeapLang: - HeapLang has a substitution-based call-by-value semantics with a *right-to-left* evaluation order (inspired by OCaml and CakeML). - HeapLang's values are either literals, products, sums, or functions. + Literals are denoted [# x] where [x] can be the unit (denoted [()]), a Boolean (of Coq type [bool]), an integer (of Coq type [Z]), or a location (of Coq type [loc]). Booleans can be eliminated using the conditional operator [if: e1 then e2 else e3]. HeapLang supports the various unary and binary operators, like [e1 + e2], [e1 * e2], etc. + Products can be introduced as [(e1,e2)], and sums can be introduced as [InjL e] or [InjR e]. Products can be eliminated using the projections [Fst e] and [Snd e], and sums can be eliminated using pattern-matching [match: e with InjL x => e1 | InjR x => e2 end]. + The syntax for non-recursive functions is [λ: x, e] and that of recursive functions is [rec: f x := e]. Here (and also in [match:]), [f] and [x] can either be variable names (of Coq type [string]) or the *anonymous binder* denoted [<>]. - HeapLang has dynamically allocated references that can be created using the [ref e] construct (in fact, HeapLang also has arrays, but we ignore in this tutorial). Dereferencing is done using the load construct [! e], and storing is done using the store construct [e1 <- e2]. Like most ML-style languages, HeapLang is garbage collected. That is, there is no explicit [free] construct. - Let bindings are written as [let: x := e1 in e2], and sequencing is written as [e1 ;; e2]. - HeapLang supports dynamic thread creation using the [Fork e] construct, which spawns a new thread [e], and returns the unit value [()]. To support synchronization between threads, HeapLang has the following constructs: + The "fetch-and-add" operation [FAA e1 e2], which atomically adds the integer [e2] to the integer stored at location [e1]. It returns the old value of the integer at location [e1]. + The "compare-and-exchange" operation [CmpXchg e1 e2 e3], which compares the value of the location [e1] with the value [e2]. If they are equal, it replace the value of the location [e1] with [e3]. It returns a tuple with the old value of the location [e1], and a Boolean indicating whether the value of [e1] was equal to [e2]. Note that compare-and-exchange can only be used on unboxed values, i.e. values that fit into one machine word: the unit value, Booleans, integers, and locations. Note that most of the HeapLang notations are designed to mimic Coq's syntax as much as possible. However, to disambiguate HeapLang notation from Coq's, colons [:] are added to many parts of the syntax. For example, one would write [λ: x, e] and [let: x := e1 in e2]. More details about HeapLang can be found in the Iris Coq development. Most importantly: - The documentation of HeapLang: https://gitlab.mpi-sws.org/iris/iris/blob/master/docs/heap_lang.md - The syntax and operational semantics of the language: https://gitlab.mpi-sws.org/iris/iris/blob/master/theories/heap_lang/lang.v - The notations for the various constructs: https://gitlab.mpi-sws.org/iris/iris/blob/master/theories/heap_lang/notation.v *) (** * Reasoning about HeapLang programs *) (** We will now demonstrate how we can use Iris to prove properties about HeapLang programs. For this, we make use of Iris's tactics for separation logic, as provided by the *MoSeL proof mode* (formerly IPM, Iris Proof Mode). Since the details of MoSeL are beyond the scope of this tutorial, we demonstrate it in an example driven way. More information about MoSeL can be found at: - The original paper POPL'17 about the Iris Proof Mode (IPM): https://robbertkrebbers.nl/research/articles/proofmode.pdf - The ICFP'18 paper about MoSeL (which, among other things, made IPM parametric in the separation logic that is used): https://robbertkrebbers.nl/research/articles/mosel.pdf - The overview of the MoSeL tactics: https://gitlab.mpi-sws.org/iris/iris/blob/master/docs/proof_mode.md - The overview of the MoSeL tactics for HeapLang: https://gitlab.mpi-sws.org/iris/iris/blob/master/docs/heap_lang.md#tactics *) (** Let us start with a simple example: the function [swap] takes two locations, and swaps their values: *) Definition swap : val := λ: "l1" "l2", let: "x" := !"l1" in "l1" <- !"l2";; "l2" <- "x". (** It is important to point out that programs in the HeapLang are written using Coq definitions of type [val], the type of HeapLang values. Such definitions are of type [val], instead of the type of HeapLang expressions [expr], as it is assumed by HeapLang's substitution function ([subst]) that values are closed, i.e. that substitution will not go into them. *) (** Now that we have defined our first HeapLang program, let us write a specification for using Iris's weakest preconditions: *) Lemma wp_swap `{!heapG Σ} l1 l2 v1 v2 : l1 ↦ v1 -∗ l2 ↦ v2 -∗ WP swap #l1 #l2 {{ v, ⌜ v = #() ⌝ ∗ l1 ↦ v2 ∗ l2 ↦ v1 }}. (** The lemma states that if [l1] and [l2] initially contain values the [v1] and [v2], respectively, then after [swap #l1 #l2] has been executed, the values of [l1] and [l2] have been swapped, i.e. they are [v2] and [v1], respectively. This specification already points out a number of interesting features of both separation logic and the Iris framework: - First, a very technical one: each Iris lemma that involves HeapLang programs, should have the parameter [`{!heapG Σ}]. This has to do with Iris's generic infrastructure for handling ghost state. For now, these parameters can be ignored, but we will come back to them in the file [unsafe.v]. More information about the handling of ghost state in Iris can be found at: https://gitlab.mpi-sws.org/iris/iris/blob/master/docs/proof_guide.md#resource-algebra-management - More interestingly, this lemma makes use of the *points-to* or *maps-to* connective [l ↦ v] of separation logic. Here, [l] is a location (of Coq type [loc]) and [v] is a HeapLang value (of Coq type [val]). The points-to connective [l ↦ v] expressive exclusive ownership of the location [l], and states that the location has value [v]. - The Iris *weakest precondition* connective [WP e {{ v. Q }}] (of type [iProp Σ]) expresses that executing [e] is safe, and that when [e] returns a value [v], it satisfies the *postcondition* [Q] (note that [v] is a binder in [Q]). - The points-to [l ↦ v] and weakest precondition [WP e {{ v, Q }}] connectives are not an ordinary Coq propositions (of type [Prop]), but are Iris propositions (of type [iProp Σ]). - Iris lemmas look mostly identical to Coq lemmas (which is achieved by overloading the syntax of all logical connectives). However, in addition to the usual connectives from higher-order logic, they can also make use of the *separating conjunction* [P ∗ Q] and the *magic wand* [P -∗ Q]. Iris lemmas should start with a [P -∗ Q] at the top-level. - Coq propositions [φ : Prop] can be embedded into Iris propositions using the syntax [⌜ φ ⌝]. In the lemma, we use the Coq's equality ([=]) in combination with the embedding to state that the return value of [swap] is the unit value. *) (** Now, let us see how we can prove the specification of [swap]. Like ordinary Coq lemmas, we start a proof using the [Proof] command. *) Proof. (** This gives us a goal of the shape [.. -∗ .. -∗ WP ..]. Since this goal is not an ordinary Coq goal, but an Iris goal, we make use of the MoSeL proof mode tactics. We start by introducing the points-to assumptions [l1 ↦ v1] and [l2 ↦ v2] into the MoSeL proof context using the [iIntros] tactic. *) iIntros "Hl1 Hl2". (** At this point our goal is of the form [WP swap #l1 #l2 {{ .. ]}]. Let us start by unfolding the definition of [swap]. For this, we make use of the Ssreflect [rewrite] tactic (note: [rewrite /def] is like [unfold def], but unlike [unfold], it can more easily be chained---which enables one to write more compact proofs when considering more complicated lemmas). *) rewrite /swap. (** At this point, our goal is [WP (λ: ..) .. {{ .. }}]. We now make use of the tactic [wp_pures] to reduce pure redexes (β-reduction, but also reduction of operators, etc.) in the goal. *) wp_pures. (** At this point, the expression in evaluation position is [! #l1], i.e. a load from the location [l1]. We make use of the tactic [wp_load]. This tactic looks up the points-to connective [l1 ↦ v1] in the proof context, and replaces [! #l1] by the value of [l1], i.e. [v1]. *) wp_load. (** The next expression in evaluation position is [! #l2], so we use [wp_load] again. Note that all [wp_]-tactics implicitly perform [wp_pures] first, so there is no need to do that ourselves, but for clarity's sake it is sometimes useful to do perform [wp_pures] manually. *) wp_load. (** The next expression in evaluation position is [#l1 <- v2], i.e. a store to the location [l1] with value [v2]. We make use of the tactic [wp_store], which looks up the points-to connective [l1 ↦ v1] in the proof context, and replaces it by [l1 ↦ v2]. Since the store operation returns the unit value, the [#l1 <- v2] expression in the goal is replaced by [#()]. However, in this case, the result of the expression in evaluation position ([#l1 <- v2]) is unused (it appears in the LHS of HeapLang's sequencing operator [;;], so the [;;] is immediately reduced. *) wp_store. (** The next expression to consider is [#l2 <- v1]. We proceed as before, by making use of the [wp_store] tactic. *) wp_store. (** We have now reached the point where the [swap] function has returned. It thus remains to prove the postcondition [⌜#() = #()⌝ ∗ l1 ↦ v2 ∗ l2 ↦ v1]. We first make use of the [iFrame] tactic, which tries to *cancel out* as many hypotheses from the MoSeL proof context in the goal. *) iFrame. (** Since both hypotheses [l1 ↦ v2] and [l2 ↦ v1] appeared in the goal, we are left with the goal [⌜#() = #()⌝]. We can now make use of the [iPureIntro] tactic, which turn the MoSeL goal into the Coq goal [#() = #()]. Alternatively, we may use the [auto] tactic, which performs [iPureIntro] implicitly, and finishes the goal using Coq's [reflexivity] tactic. *) auto. Qed. (** ** Exercise (swap_and_add, easy) *) (** Consider the following variant of the [swap] function: *) Definition swap_and_add : val := λ: "l1" "l2", let: "x" := !"l1" in "l1" <- "x" + !"l2";; "l2" <- "x". (** The specification is as follows: *) Lemma wp_swap_and_add `{!heapG Σ} l1 l2 (x1 x2 : Z) : l1 ↦ #x1 -∗ l2 ↦ #x2 -∗ WP swap_and_add #l1 #l2 {{ v, ⌜ v = #() ⌝ ∗ l1 ↦ #(x1 + x2) ∗ l2 ↦ #x1 }}. (** Note that since [swap_and_add] only works on integer values, we quantify over integers [x1 x2 : Z] instead of values [v1 v2 : val] in the parameters of the lemma. Furthermore, since we use integers, we need to make use of the embedding [#] of literals, both in the program and in the points-to connectives. You should prove this lemma. Hint: [wp_pures] also executes the [+] operator. Carefully check how it affects the embedded [#] and convince yourself why that makes sense. *) (* REMOVE *) Proof. iIntros. rewrite /swap_and_add. do 2 wp_load. do 2 wp_store. by iFrame. Qed. (** ** Reasoning about higher-order functions *) (** For the next example, let us consider the higher-order function [twice]. This function takes a function [f] and a value [x] as its arguments, and applies [f] twice to [x] *) Definition twice : val := λ: "f" "x", "f" ("f" "x"). (** Since weakest-preconditions are first-class Iris propositions, we cannot only use them in the conclusions of a lemma, but also in the premises, and in nested ways. This is crucial to reason about higher-order functions. Let us see that in action to give a specification of the [twice] function. *) Lemma wp_twice `{!heapG Σ} (Ψ : val → iProp Σ) (vf vx : val) : WP vf vx {{ w, WP vf w {{ Ψ }} }} -∗ WP twice vf vx {{ Ψ }}. (** This lemma is quite a mouthful. First, it quantifies over an arbitrary postcondition [Ψ : val → iProp Σ] (note, instead of writing [WP .. {{ v, Ψ v }}], we may write [WP .. {{ Ψ }}]), and a HeapLang function value [vf]. In the precondition of the lemma, it nests WP twice to account for the fact that [twice] calls the function argument twice. *) Proof. (** The proof of this lemma proceeds in the usual way: we use [iIntros] to introduce the premise into the MoSeL proof context, unfold the definition of [twice] using [rewrite /twice], and then perform pure reductions using the [wp_pures] tactic. *) iIntros "Hvf". rewrite /twice. wp_pures. (** Our goal is now [WP vf (vf vx) {{ v, Ψ v }}]. To prove this goal, we make use of the *bind* rule for weakest preconditions, which is: << WP e {{ v, WP fill K v {{ Ψ }} }} -∗ WP fill K e {{ Ψ }} >> Here, [K] is a call-by-value (one-hole) evaluation context, and [fill K e] puts [e] into the hole of [K]. The bind rule expresses that to prove [fill K e], we should first prove [e], and then [fill K v] where [v] is the result of [e]. To use this rule, we make use of the [wp_bind e] tactic, which automatically finds the context [K] given the expression [e]. *) wp_bind (vf vx). (** At this point, the goal is precisely our hypothesis. We can thus finish the proof by the [iAssumption] tactic, which is implicitly performed by [auto]. *) auto. Qed. (** To see [twice] in action, let us use it to implement the function [add_two], which takes an integer value, and adds [2] to it. *) Definition add_two : val := λ: "x", twice (λ: "y", #1 + "y") "x". (** While this function is rather convoluted, the specification is precisely what you would expect---it adds [2]. *) Lemma wp_add_two `{!heapG Σ} (x : Z) : WP add_two #x {{ w, ⌜ w = #(2 + x) ⌝ }}%I. (** Note that this weakest precondition does not have a premise (i.e. the lemma does not involve a magic wand [-∗] at the top-level, as we have seen in previous examples). We thus use the scope [%I] ([I] of "Iris") to instruct Coq to interpret this lemma as an Iris lemma. *) Proof. rewrite /add_two. wp_pures. (** We use the [iApply] tactic to make use of the specification [wp_twice] of [twice] that we just proved. *) iApply wp_twice. (** This turns the goal into two nested weakest preconditions, for the two calls to the higher-order function [(λ: "y", #1 + "y")] to [x]. Since the function is pure, [wp_pures] will reduce the entire program (it processes nested weakest preconditions automatically. *) wp_pures. (** The remaining goal is ⌜#(1 + (1 + x)) = #(2 + x)⌝, which can be solved by the following tactic. *) auto with f_equal lia. (** As usual, [auto] implicitly performs [iPureIntro]. The [with f_equal lia] argument instructs [auto] to additionally use Coq's [f_equal] tactic when it is faced a goal [f x1 .. xn = f y1 .. yn], which is turned into goals [xi = yi], and Coq's [lia] tactic for linear integer arithmetic when it is faced a goal on integers. *) Qed. (** ** Exercise (add_two_ref, moderate) *) (** Instead of using [twice] to add [2] to an integer value, we now use it to add [2] to the value of a reference to an integer. That is, we use [twice] with a function that reads the reference and adds [1] to it. *) Definition add_two_ref : val := λ: "l", twice (λ: <>, "l" <- #1 + !"l") #(). (** The specification is as expected (it follows the pattern we have seen in e.g. [swap_and_add]---we make use of the points-to connective [l ↦ #x]. *) Lemma wp_add_two_ref `{!heapG Σ} l (x : Z) : l ↦ #x -∗ WP add_two_ref #l {{ w, ⌜ w = #() ⌝ ∗ l ↦ #(2 + x) }}%I. (** You should complete this proof. Hint: you need to use the usual Coq lemmas about addition on [Z] (or the [replace] or [rewrite (_ : x = y)] tactic with [lia]) to turn [2 + x] into [1 + (1 + x)]. Tactics like [replace] and [rewrite] work operate both on the MoSeL proof goal and the MoSeL proof context. *) (* REMOVE *) Proof. iIntros "Hl". rewrite /add_two_ref. wp_pures. iApply wp_twice. wp_load. wp_store. wp_load. wp_store. rewrite (_ : 2 = (1 + 1)) // -Z.add_assoc. iFrame. auto. Qed. (** ** Reasoning about higher-order state *) (** To see how Iris can be used to reason about higher-order state---that is, references that contain functions---let us consider a variant of [twice] where we do not pass the function and value directly, but instead pass them using a reference. Moreover, instead of returning the result of applying the function twice, it puts the result in the value reference. *) Definition twice_ref : val := λ: "lf" "lx", "lx" <- !"lf" (!"lf" !"lx"). (** The specification of [twice_ref] follows that of [twice], but it quantifies both over locations [lf] and [lx], and the HeapLang function [vf] and value [vx] that they contain, respectively. *) Lemma wp_twice_ref `{!heapG Σ} (Ψ : val → iProp Σ) lf lx (vf vx : val) : lf ↦ vf -∗ lx ↦ vx -∗ WP vf vx {{ w, WP vf w {{ w', lf ↦ vf -∗ lx ↦ w' -∗ Ψ #() }} }} -∗ WP twice_ref #lf #lx {{ Ψ }}. (** The postcondition of the weakest precondition in the premise is somewhat more complicated. It makes use of the magic wand [-∗] to give back ownership of the references [lf] and [lx] to the client of the lemma. Since the value of [lx] has been changed to [w'], the points-to connective that is given back is [lx ↦ w']. *) Proof. (** The beginning of the proof is as usual. *) iIntros "Hlf Hlx Hvf". rewrite /twice_ref. wp_pures. do 2 wp_load. wp_bind (vf vx). (** We now end up in the critical part of the proof. Our goal is a weakest precondition for [vf vx] and we also have a hypotheses that contains a weakest precondition for [vf vx]. However, the postconditions of both weakest preconditions do not match up. We make use of the lemma [wp_wand], which is the weakest precondition-version of the rule of consequence for Hoare triples: << WP e {{ Φ }} -∗ (∀ v, Φ v -∗ Ψ v) -∗ WP e {{ Ψ }} >> We use the [with] argument of [iApply] to use the hypotheses [Hvf] containing the weakest precondition for the first argument of [wp_wand]. *) iApply (wp_wand with "Hvf"). (** At this point, we need to show that the postcondition of the weakest precondition from the hypothesis implies the postcondition of the weakest precondition in the goal. *) iIntros (w) "Hvf". wp_load. wp_bind (vf w). (** We are now in the same situation, we have to prove a weakest precondition for [vf w], but the postcondition is different from the weakest precondition in the hypothesis. We thus use [wp_wand] again. *) iApply (wp_wand with "Hvf"). iIntros (w') "Hvf". wp_store. iApply ("Hvf" with "Hlf Hlx"). Qed. (** ** Exercise (add_two_fancy, moderate) *) Definition add_two_fancy : val := λ: "x", let: "lf" := ref (λ: "y", #1 + "y") in let: "lx" := ref "x" in twice_ref "lf" "lx";; !"lx". Lemma wp_add_two_fancy `{!heapG Σ} (x : Z) : WP add_two_fancy #x {{ w, ⌜ w = #(2 + x) ⌝ }}%I. (* REMOVE *) Proof. rewrite /add_two_fancy. wp_pures. wp_alloc lf as "Hlf". wp_alloc lx as "Hlx". wp_pures. wp_bind (twice_ref _ _). iApply (wp_twice_ref with "Hlf Hlx"). wp_pures. iIntros "Hlx Hfx". wp_load. auto with f_equal lia. Qed. (** * Reasoning about "unsafe" functions *) (** Since HeapLang is an untyped language, we can write down arbitrary programs, i.e. programs that are not typeable by conventional type systems, and prove logical specifications of them. We will show this on two small examples that will use at other places in this tutorial to demonstrate the advances of semantic typing. *) (** The program below contains the expression [#13 #37] in the else-branch of the conditional. The expression [#13 #37] will get stuck in the operational semantics, i.e. it is unsafe. However, the else-branch is never executed, so we can still show that this function is safe by proving a specification. *) Definition unsafe_pure : val := λ: <>, if: #true then #13 else #13 #37. Lemma wp_unsafe_pure `{!heapG Σ} : WP unsafe_pure #() {{ v, ⌜ v = #13 ⌝ }}%I. Proof. rewrite /unsafe_pure. wp_pures. auto. Qed. (** Exercise (unsafe_ref, easy) *) (** The function below reuses a reference using values of different types. *) Definition unsafe_ref : val := λ: <>, let: "l" := ref #0 in "l" <- #true;; !"l". Lemma wp_unsafe_ref `{!heapG Σ} : WP unsafe_ref #() {{ v, ⌜ v = #true ⌝ }}%I. (* REMOVE *) Proof. rewrite /unsafe_ref. wp_pures. wp_alloc l. wp_store. wp_load. auto. Qed.