diff --git a/ConcurrentSeparationLogic.v b/ConcurrentSeparationLogic.v
index 8a51ba5..7990284 100644
--- a/ConcurrentSeparationLogic.v
+++ b/ConcurrentSeparationLogic.v
@@ -1,5 +1,5 @@
(** Formal Reasoning About Programs
- * Chapter 17: Concurrent Separation Logic
+ * Chapter 18: Concurrent Separation Logic
* Author: Adam Chlipala
* License: https://creativecommons.org/licenses/by-nc-nd/4.0/ *)
diff --git a/MessagesAndRefinement.v b/MessagesAndRefinement.v
index 9d3c3e7..04ef22f 100644
--- a/MessagesAndRefinement.v
+++ b/MessagesAndRefinement.v
@@ -1,5 +1,5 @@
(** Formal Reasoning About Programs
- * Chapter 18: Process Algebra and Behavioral Refinement
+ * Chapter 19: Process Algebra and Behavioral Refinement
* Author: Adam Chlipala
* License: https://creativecommons.org/licenses/by-nc-nd/4.0/ *)
diff --git a/ProgramDerivation.v b/ProgramDerivation.v
index c8c0390..08033e5 100644
--- a/ProgramDerivation.v
+++ b/ProgramDerivation.v
@@ -4,7 +4,7 @@
* License: https://creativecommons.org/licenses/by-nc-nd/4.0/
* Some material borrowed from Fiat *)
-Require Import FrapWithoutSets.
+Require Import Frap.
Require Import Program Setoids.Setoid Classes.RelationClasses Classes.Morphisms Morphisms_Prop.
Require Import Eqdep.
@@ -14,17 +14,49 @@ Ltac inv_pair :=
end.
+(* We have generally focused so far on proving that programs meet
+ * specifications. What if we could generate programs from their
+ * specifications, in ways that guarantee correctness? Let's explore that
+ * direction, in the tradition of *program derivation* via
+ * *stepwise refinement*. *)
+
+
(** * The computation monad *)
-Definition comp (A : Type) := A -> Prop.
+(* One counterintuitive design choice will be to represent specifications and
+ * implementations in the same "language," which is essentially Gallina with the
+ * added ability to pick elements nondeterministically from arbitrary sets. *)
+(* Specifically, a process producing type [A] is represents as [comp A]. *)
+Definition comp (A : Type) := A -> Prop.
+(* The computation is represented by the set of legal values it might
+ * generate. *)
+
+(* Computations form a monad, with the following two operators. *)
Definition ret {A} (x : A) : comp A := eq x.
+(* Note how [eq x] is one way of writing "the singleton set of [x],", using
+ * partial application of the two-argument equality predicate [eq]. *)
Definition bind {A B} (c1 : comp A) (c2 : A -> comp B) : comp B :=
fun b => exists a, c1 a /\ c2 a b.
-Definition pick_ {A} (P : A -> Prop) : comp A := P.
+(* As in some of our earlier examples, [bind] is for sequencing one computation
+ * after another. For this monad, existential quantification provides a natural
+ * explanation of sequencing. *)
+Definition pick_ {A} (P : A -> Prop) : comp A := P.
+(* Here is a convenient wrapper function for injecting an arbitrary set into
+ * [comp]. This operator stands for nondeterministic choice of any value in the
+ * set. *)
+
+(* Here is the correctness condition, for when [c2] implements [c1]. From left
+ * to right in the operands of [refine], we move closer to a concrete
+ * implementation. *)
Definition refine {A} (c1 c2 : comp A) :=
forall a, c2 a -> c1 a.
+(* Note how this definition is just subset inclusion, in the right direction. *)
+
+(* Next, we use Coq's *setoid* feature to declare compatibility of our
+ * definitions with the [rewrite] tactic. See the Coq manual on setoids for
+ * background on what we are doing and why. *)
Ltac morphisms := unfold refine, impl, pointwise_relation, bind, ret, pick_; hnf; first_order; subst; eauto.
@@ -58,12 +90,19 @@ Proof.
morphisms.
Qed.
+(** ** OK, back to the details we want to focus on. *)
+
+(* Here we have one of the monad laws, showing how traditional computational
+ * reduction is compatible with refinement. *)
Theorem bind_ret : forall A B (v : A) (c2 : A -> comp B),
refine (bind (ret v) c2) (c2 v).
Proof.
morphisms.
Qed.
+(* Here's an example specific to this monad. One way to resolve a
+ * nondeterministic pick from a set is to replace it with a specific element
+ * from the set. *)
Theorem pick_one : forall {A} {P : A -> Prop} v,
P v
-> refine (pick_ P) (ret v).
@@ -77,6 +116,8 @@ Notation "x <- c1 ; c2" := (bind c1 (fun x => c2)) (at level 81, right associati
(** * Picking a number not in a list *)
+(* Let's illustrate the big idea with an example derivation. *)
+
(* A specification of what it means to choose a number that is not in a
* particular list *)
Definition notInList (ls : list nat) :=
@@ -125,6 +166,8 @@ Proof.
linear_arithmetic.
Qed.
+(** ** Interlude: defining some tactics for key parts of derivation *)
+
(* We run this next step to hide an evar, so that rewriting isn't too eager to
* make up values for it. *)
Ltac hide_evars :=
@@ -134,9 +177,12 @@ Ltac hide_evars :=
| [ |- refine _ ?f ] => set f
end.
+(* This tactic starts a script that finds a term to refine another. *)
Ltac begin :=
eexists; simplify; hide_evars.
+(* This tactic ends such a derivation, in effect undoing the effect of
+ * [hide_evars]. *)
Ltac finish :=
match goal with
| [ |- refine ?e (?f ?arg1 ?arg2) ] =>
@@ -158,24 +204,24 @@ Ltac finish :=
unify f' e; reflexivity
end.
+(** ** Back to the example *)
+
(* Let's derive an efficient implementation. *)
-Theorem implementation : { f : list nat -> comp nat | forall ls, refine (notInList ls) (f ls) }.
+Theorem implementation : sig (fun f : list nat -> comp nat => forall ls, refine (notInList ls) (f ls)).
Proof.
begin.
rewrite notInList_decompose.
rewrite listMax_refines.
- setoid_rewrite increment_refines.
- (* ^-- Different tactic here to let us rewrite under a binder! *)
rewrite bind_ret.
+ rewrite increment_refines.
finish.
Defined.
(* We can extract the program that we found as a standlone, executable Gallina
* term. *)
Definition impl := Eval simpl in proj1_sig implementation.
-Print impl.
-(* We'll temporarily expose the definition of [max], so we can compute neatly
+(* We'll locally expose the definition of [max], so we can compute neatly
* here. *)
Transparent max.
Eval compute in impl (1 :: 7 :: 8 :: 2 :: 13 :: 6 :: nil).
@@ -183,13 +229,32 @@ Eval compute in impl (1 :: 7 :: 8 :: 2 :: 13 :: 6 :: nil).
(** * Abstract data types (ADTs) *)
+(* Stepwise refinement can be most satisfying to build objects with multiple
+ * methods. The specification of such an object is often called an abstract
+ * data type (ADT), and we studied them (from a verification perspective) in
+ * module DataAbstraction. Let's see how we can build implementations
+ * automatically from ADTs. First, some preliminary definitions. *)
+
+(* Every method inhabits this type, where [state] is the type of private state
+ * for the object. *)
Record method_ {state : Type} := {
MethodName : string;
MethodBody : state -> nat -> comp (state * nat)
}.
+(* A body takes the current state as input and produces the new state as output.
+ * Additionally, we have hardcoded both the parameter type and the return type
+ * to [nat], for simplicity. The only wrinkle is that a body result is in the
+ * [comp] monad, to let it mix features from specification and
+ * implementation. *)
Arguments method_ : clear implicits.
+Notation "'method' name [[ self , arg ]] = body" :=
+ {| MethodName := name;
+ MethodBody := fun self arg => body |}
+ (at level 100, self at level 0, arg at level 0).
+(* Next, this type collects several method definitions, given a list of their
+ * names. *)
Inductive methods {state : Type} : list string -> Type :=
| MethodsNil : methods []
| MethodsCons : forall (m : method_ state) {names},
@@ -198,16 +263,15 @@ Inductive methods {state : Type} : list string -> Type :=
Arguments methods : clear implicits.
-Notation "'method' name [[ self , arg ]] = body" :=
- {| MethodName := name;
- MethodBody := fun self arg => body |}
- (at level 100, self at level 0, arg at level 0).
-
+(* Finally, the definition of an abstract data type, which will apply to both
+ * specifications (the classical sense of ADT) and implementations. *)
Record adt {names : list string} := {
AdtState : Type;
AdtConstructor : comp AdtState;
AdtMethods : methods AdtState names
}.
+(* An ADT has a state type, one constructor to initialize the state, and a set
+ * of methods that may read and write the state. *)
Arguments adt : clear implicits.
@@ -219,26 +283,54 @@ Notation "'ADT' { 'rep' = state 'and' 'constructor' = constr ms }" :=
Notation "'and' m1 'and' .. 'and' mn" :=
(MethodsCons m1 (.. (MethodsCons mn MethodsNil) ..)) (at level 101).
+(* Here's one quick example, of a counter with duplicate "increment" methods. *)
+
+Definition counter := ADT {
+ rep = nat
+ and constructor = ret 0
+ and method "increment1"[[self, arg]] = ret (self + arg, 0)
+ and method "increment2"[[self, arg]] = ret (self + arg, 0)
+ and method "value"[[self, _]] = ret (self, self)
+}.
+
+(* This example hasn't used the power of the [comp] monad, but we will get
+ * there later. *)
+
(** * ADT refinement *)
+(* What does it mean to take sound implementation steps from an ADT toward an
+ * efficient implementation? We formalize refinement for ADTs as well. The key
+ * principle will be *simulation*, very similarly to how we used the idea for
+ * compiler verification. *)
+
+(* For a "before" state type [state1] and an "after" state type [state2], we
+ * require choice of a simulation relation [R]. This next inductive relation
+ * captures when all methods are pairwise compatible with [R], between the
+ * "before" and "after" ADTs. *)
Inductive RefineMethods {state1 state2} (R : state1 -> state2 -> Prop)
: forall {names}, methods state1 names -> methods state2 names -> Prop :=
| RmNil : RefineMethods R MethodsNil MethodsNil
| RmCons : forall name names (f1 : state1 -> nat -> comp (state1 * nat))
(f2 : state2 -> nat -> comp (state2 * nat))
(ms1 : methods state1 names) (ms2 : methods state2 names),
+
+ (* This condition is the classic "simulation diagram." *)
(forall s1 s2 arg s2' res,
R s1 s2
-> f2 s2 arg (s2', res)
-> exists s1', f1 s1 arg (s1', res)
/\ R s1' s2')
+
-> RefineMethods R ms1 ms2
-> RefineMethods R (MethodsCons {| MethodName := name; MethodBody := f1 |} ms1)
(MethodsCons {| MethodName := name; MethodBody := f2 |} ms2).
Hint Constructors RefineMethods.
+(* When does [adt2] refine [adt1]? When there exists a simulation relation,
+ * with respect to which the constructors and methods all satisfy the usual
+ * simulation diagram. *)
Record adt_refine {names} (adt1 adt2 : adt names) : Prop := {
ArRel : AdtState adt1 -> AdtState adt2 -> Prop;
ArConstructors : forall s2,
@@ -248,6 +340,7 @@ Record adt_refine {names} (adt1 adt2 : adt names) : Prop := {
ArMethods : RefineMethods ArRel (AdtMethods adt1) (AdtMethods adt2)
}.
+(* We will use this handy tactic to prove ADT refinements. *)
Ltac choose_relation R :=
match goal with
| [ |- adt_refine ?a ?b ] => apply (Build_adt_refine _ a b R)
@@ -255,13 +348,8 @@ Ltac choose_relation R :=
(** ** Example: numeric counter *)
-Definition counter := ADT {
- rep = nat
- and constructor = ret 0
- and method "increment1"[[self, arg]] = ret (self + arg, 0)
- and method "increment2"[[self, arg]] = ret (self + arg, 0)
- and method "value"[[self, _]] = ret (self, self)
-}.
+(* Let's refine the previous counter spec into an implementation that maintains
+ * two separate counters and adds them on demand. *)
Definition split_counter := ADT {
rep = nat * nat
@@ -273,6 +361,7 @@ Definition split_counter := ADT {
Hint Extern 1 (@eq nat _ _) => simplify; linear_arithmetic.
+(* Here is why the new implementation is correct. *)
Theorem split_counter_ok : adt_refine counter split_counter.
Proof.
choose_relation (fun n p => n = fst p + snd p).
@@ -292,6 +381,8 @@ Qed.
(** * General refinement strategies *)
+(* ADT refinement forms a preorder, as the next two theorems show. *)
+
Lemma RefineMethods_refl : forall state names (ms : methods state names),
RefineMethods eq ms ms.
Proof.
@@ -346,6 +437,13 @@ Proof.
first_order.
Qed.
+(* Note the use of relation composition for [refine_trans]. *)
+
+(** ** Refining constructors *)
+
+(* One way to refine an ADT is to perform [comp]-level refinement within its
+ * constructor definition. *)
+
Theorem refine_constructor : forall names state constr1 constr2 (ms : methods _ names),
refine constr1 constr2
-> adt_refine {| AdtState := state;
@@ -359,6 +457,13 @@ Proof.
choose_relation (@eq state); eauto.
Qed.
+(** ** Refining methods *)
+
+(* Conceptually quite similar, refining within methods requires more syntactic
+ * framework. *)
+
+(* This relation captures the process of replacing [oldbody] of method [name]
+ * with [newbody]. *)
Inductive ReplaceMethod {state} (name : string)
(oldbody newbody : state -> nat -> comp (state * nat))
: forall {names}, methods state names -> methods state names -> Prop :=
@@ -373,7 +478,9 @@ Inductive ReplaceMethod {state} (name : string)
(MethodsCons {| MethodName := name'; MethodBody := oldbody' |} ms1)
(MethodsCons {| MethodName := name'; MethodBody := oldbody' |} ms2).
-Theorem ReplaceMethod_ok : forall state name
+(* Let's skip ahead to the next [Theorem]. *)
+
+Lemma ReplaceMethod_ok : forall state name
(oldbody newbody : state -> nat -> comp (state * nat))
names (ms1 ms2 : methods state names),
(forall s arg, refine (oldbody s arg) (newbody s arg))
@@ -391,6 +498,7 @@ Qed.
Hint Resolve ReplaceMethod_ok.
+(* It is OK to replace a method body if the new refines the old as a [comp]. *)
Theorem refine_method : forall state name (oldbody newbody : state -> nat -> comp (state * nat))
names (ms1 ms2 : methods state names) constr,
(forall s arg, refine (oldbody s arg) (newbody s arg))
@@ -406,6 +514,12 @@ Proof.
choose_relation (@eq state); eauto.
Qed.
+(** ** Representation changes *)
+
+(* Some of the most interesting refinements select new data structures. That
+ * is, they pick new state types. Here we formalize that idea in terms of
+ * existence of an *abstraction function* from the new state type to the old. *)
+
Inductive RepChangeMethods {state1 state2} (absfunc : state2 -> state1)
: forall {names}, methods state1 names -> methods state2 names -> Prop :=
| RchNil :
@@ -418,6 +532,12 @@ Inductive RepChangeMethods {state1 state2} (absfunc : state2 -> state1)
p <- oldbody (absfunc s) arg;
s' <- pick s' where absfunc s' = fst p;
ret (s', snd p)) |} ms2).
+(* Interestingly, we managed to rewrite all method bodies automatically, to be
+ * compatible with a new data structure! The catch is that our language of
+ * method bodies is inherently noncomputational. We leave nontrivial work for
+ * ourselves, in further refinement of method bodies to remove "pick"
+ * operations. Note how the generic method template above relies on "pick"
+ * operations to invert abstraction functions. *)
Lemma RepChangeMethods_ok : forall state1 state2 (absfunc : state2 -> state1)
names (ms1 : methods state1 names) (ms2 : methods state2 names),
@@ -461,12 +581,19 @@ Ltac refine_method nam := eapply refine_trans; [ eapply refine_method with (name
| repeat (refine (RepmHead _ _ _ _ _)
|| (refine (RepmSkip _ _ _ _ _ _ _ _ _ _); [ equality | ])) ];
cbv beta; simplify; hide_evars | ].
+(* Don't be thrown off by the [refine] tactic used here. It is not related to
+ * our notion of refinement! See module SubsetTypes for an explanation of
+ * it. *)
Ltac refine_finish := apply refine_refl.
(** ** Example: the numeric counter again *)
-Definition derived_counter : { counter' | adt_refine counter counter' }.
+(* Let's generate the two-counter variant through the process of finding a
+ * proof, in contrast to theorem [split_counter_ok], which started with the full
+ * code of the transformed ADT. *)
+
+Definition derived_counter : sig (adt_refine counter).
unfold counter; eexists.
refine_rep (fun p => fst p + snd p).
@@ -505,15 +632,28 @@ Eval simpl in proj1_sig derived_counter.
(** * Another refinement strategy: introducing a cache (a.k.a. finite differencing) *)
+(* Some methods begin life as expensive computations, such that it pays off to
+ * precompute their values. A generic refinement strategy follows this plan by
+ * introducing *caches*. *)
+
+(* Here, [name] names a method whose body leaves the state alone and returns the
+ * result of [func] applied to that state. *)
Inductive CachingMethods {state} (name : string) (func : state -> nat)
: forall {names}, methods state names -> methods (state * nat) names -> Prop :=
| CmNil :
CachingMethods name func MethodsNil MethodsNil
+
+(* Here is how we rewrite [name] itself. We are extending state with an extra
+ * cache of [func]'s value, so it is legal just to return that cache. *)
| CmCached : forall names (ms1 : methods state names) (ms2 : methods _ names),
CachingMethods name func ms1 ms2
-> CachingMethods name func
(MethodsCons {| MethodName := name; MethodBody := (fun s _ => ret (s, func s)) |} ms1)
(MethodsCons {| MethodName := name; MethodBody := (fun s arg => ret (s, snd s)) |} ms2)
+
+(* Any other method now picks up an obligation to recompute the cache value
+ * whenever changing the state. We express that recomputation with a pick, to
+ * be refined later into efficient logic. *)
| CmDefault : forall name' names oldbody (ms1 : methods state names) (ms2 : methods _ names),
name' <> name
-> CachingMethods name func ms1 ms2
@@ -575,6 +715,8 @@ Ltac refine_cache nam := eapply refine_trans; [ eapply refine_cache with (name :
(** ** An example with lists of numbers *)
+(* Let's work out an example of caching. *)
+
Definition sum := fold_right plus 0.
Definition nats := ADT {
@@ -584,7 +726,7 @@ Definition nats := ADT {
and method "sum"[[self, _]] = ret (self, sum self)
}.
-Definition optimized_nats : { nats' | adt_refine nats nats' }.
+Definition optimized_nats : sig (adt_refine nats).
unfold nats; eexists.
refine_cache "sum".
diff --git a/README.md b/README.md
index bee7374..1d387c4 100644
--- a/README.md
+++ b/README.md
@@ -27,9 +27,10 @@ The main narrative, also present in the book PDF, presents standard program-proo
* Chapter 13: `DeepAndShallowEmbeddings.v`
* Chapter 14: `SeparationLogic.v`
* Chapter 15: `Connecting.v`
-* Chapter 16: `SharedMemory.v`
-* Chapter 17: `ConcurrentSeparationLogic.v`
-* Chapter 18: `MessagesAndRefinement.v`
+* Chapter 16: `ProgramDerivation.v`
+* Chapter 17: `SharedMemory.v`
+* Chapter 18: `ConcurrentSeparationLogic.v`
+* Chapter 19: `MessagesAndRefinement.v`
There are also two supplementary files that are independent of the main narrative, for introducing programming with dependent types, a distinctive Coq feature that we neither use nor recommend for the problem sets, but which many students find interesting (and useful in other contexts).
* `SubsetTypes.v`: a first introduction to dependent types by attaching predicates to normal types (used after `CompilerCorrectness.v` in the last course offering)
diff --git a/SharedMemory.v b/SharedMemory.v
index af5602a..2f77dbf 100644
--- a/SharedMemory.v
+++ b/SharedMemory.v
@@ -1,5 +1,5 @@
(** Formal Reasoning About Programs
- * Chapter 16: Operational Semantics for Shared-Memory Concurrency
+ * Chapter 17: Operational Semantics for Shared-Memory Concurrency
* Author: Adam Chlipala
* License: https://creativecommons.org/licenses/by-nc-nd/4.0/ *)