Mercurial > cpdt > repo
changeset 469:b36876d4611e
Batch of changes based on proofreader feedback
author | Adam Chlipala <adam@chlipala.net> |
---|---|
date | Wed, 26 Sep 2012 16:35:35 -0400 |
parents | 62475ab7570b |
children | 0df6dde807ab |
files | src/GeneralRec.v src/InductiveTypes.v src/Intro.v src/LogicProg.v src/Predicates.v src/StackMachine.v src/Subset.v staging/updates.rss |
diffstat | 8 files changed, 36 insertions(+), 44 deletions(-) [+] |
line wrap: on
line diff
--- a/src/GeneralRec.v Wed Sep 05 15:22:13 2012 -0400 +++ b/src/GeneralRec.v Wed Sep 26 16:35:35 2012 -0400 @@ -853,7 +853,7 @@ Qed. (* end hide *) -(** Not only can we define the Fibonacci function with the new monad, but even our running example of merge sort becomes definable. By shadowing our previous notation for "bind,", we can write almost exactly the same code as in our previous [mergeSort'] definition, but with less syntactic clutter. *) +(** Not only can we define the Fibonacci function with the new monad, but even our running example of merge sort becomes definable. By shadowing our previous notation for "bind," we can write almost exactly the same code as in our previous [mergeSort'] definition, but with less syntactic clutter. *) Notation "x <- m1 ; m2" := (Bnd m1 (fun x => m2)).
--- a/src/InductiveTypes.v Wed Sep 05 15:22:13 2012 -0400 +++ b/src/InductiveTypes.v Wed Sep 26 16:35:35 2012 -0400 @@ -60,16 +60,11 @@ Check (fun x : False => x). (** [: False -> False] *) -(** %\smallskip{}%In fact, [False] implies anything, and we can take advantage of this fact with an odd looking [match] expression that has no branches. Since there are no rules for deducing [False], there are no cases to consider! *) - -Check (fun x : False => match x with end : True). -(** [: False -> True] *) - (** %\smallskip{}%Every one of these example programs whose type looks like a logical formula is a%\index{proof term}% _proof term_. We use that name for any Gallina term of a logical type, and we will elaborate shortly on what makes a type logical. -In the rest of this chapter, we will introduce different ways of defining types. Every example type can be interpreted alternatively as a type of programs or %\index{proposition}%propositions (i.e., formulas or theorem statements). +In the rest of this chapter, we will introduce different ways of defining types. Every example type can be interpreted alternatively as a type of programs or proofs. -One of the first types we introduce will be [bool], with constructors [true] and [false]. Newcomers to Coq often wonder about the distinction between [True] and [true] and the distinction between [False] and [false]. One glib answer is that [True] and [False] are types, but [true] and [false] are not. A more useful answer is that Coq's metatheory guarantees that any term of type [bool] _evaluates_ to either [true] or [false]. This means that we have an _algorithm_ for answering any question phrased as an expression of type [bool]. Conversely, most propositions do not evaluate to [True] or [False]; the language of inductively defined propositions is much richer than that. We ought to be glad that we have no algorithm for deciding mathematical truth, since otherwise it would be clear that we could not formalize undecidable properties, like most any properties of general-purpose programs. *) +One of the first types we introduce will be [bool], with constructors [true] and [false]. Newcomers to Coq often wonder about the distinction between [True] and [true] and the distinction between [False] and [false]. One glib answer is that [True] and [False] are types, but [true] and [false] are not. A more useful answer is that Coq's metatheory guarantees that any term of type [bool] _evaluates_ to either [true] or [false]. This means that we have an _algorithm_ for answering any question phrased as an expression of type [bool]. Conversely, most propositions do not evaluate to [True] or [False]; the language of inductively defined propositions is much richer than that. We ought to be glad that we have no algorithm for deciding mathematical truth, since otherwise it would be clear that we could not formalize undecidable properties, like almost any interesting property of general-purpose programs. *) (** * Enumerations *) @@ -253,15 +248,7 @@ | S n' => n' end. -(** We can prove theorems by case analysis: *) - -Theorem S_isZero : forall n : nat, isZero (pred (S (S n))) = false. -(* begin thide *) - destruct n; reflexivity. -Qed. -(* end thide *) - -(** We can also now get into genuine inductive theorems. First, we will need a recursive function, to make things interesting.%\index{Gallina terms!plus}% *) +(** We can prove theorems by case analysis with [destruct] as for simpler inductive types, but we can also now get into genuine inductive theorems. First, we will need a recursive function, to make things interesting.%\index{Gallina terms!plus}% *) Fixpoint plus (n m : nat) : nat := match n with @@ -287,7 +274,7 @@ reflexivity. -(** Our second subgoal is more work and also demonstrates our first inductive hypothesis. +(** Our second subgoal requires more work and also demonstrates our first inductive hypothesis. [[ n : nat @@ -489,11 +476,9 @@ (* end thide *) End list. -(* begin hide *) Implicit Arguments Nil [T]. -(* end hide *) -(** After we end the section, the [Variable]s we used are added as extra function parameters for each defined identifier, as needed. We verify that this has happened using the [Print] command, a cousin of [Check] which shows the definition of a symbol, rather than just its type. *) +(** After we end the section, the [Variable]s we used are added as extra function parameters for each defined identifier, as needed. With an [Implicit Arguments]%~\index{Vernacular commands!Implicit Arguments}% command, we ask that [T] be inferred when we use [Nil]; Coq's heuristics already decided to apply a similar policy to [Cons], because of the [Set Implicit Arguments]%~\index{Vernacular commands!Set Implicit Arguments}% command elided at the beginning of this chapter. We verify that our definitions have been saved properly using the [Print] command, a cousin of [Check] which shows the definition of a symbol, rather than just its type. *) Print list. (** %\vspace{-.15in}% [[ @@ -627,7 +612,7 @@ (** * Reflexive Types *) -(** A kind of inductive type called a _reflexive type_ is defined in terms of functions that have the type being defined as their range. One very useful class of examples is in modeling variable binders. Our example will be an encoding of the syntax of first-order logic. Since the idea of syntactic encodings of logic may require a bit of acclimation, let us first consider a simpler formula type for a subset of propositional logic. *) +(** A kind of inductive type called a _reflexive type_ includes at least one constructor that takes as an argument _a function returning the same type we are defining_. One very useful class of examples is in modeling variable binders. Our example will be an encoding of the syntax of first-order logic. Since the idea of syntactic encodings of logic may require a bit of acclimation, let us first consider a simpler formula type for a subset of propositional logic. We are not yet using a reflexive type, but later we will extend the example reflexively. *) Inductive pformula : Set := | Truth : pformula @@ -704,7 +689,7 @@ %\medskip% -Up to this point, we have seen how to encode in Coq more and more of what is possible with algebraic datatypes in %\index{Haskell}%Haskell and %\index{ML}%ML. This may have given the inaccurate impression that inductive types are a strict extension of algebraic datatypes. In fact, Coq must rule out some types allowed by Haskell and ML, for reasons of soundness. Reflexive types provide our first good example of such a case. +Up to this point, we have seen how to encode in Coq more and more of what is possible with algebraic datatypes in %\index{Haskell}%Haskell and %\index{ML}%ML. This may have given the inaccurate impression that inductive types are a strict extension of algebraic datatypes. In fact, Coq must rule out some types allowed by Haskell and ML, for reasons of soundness. Reflexive types provide our first good example of such a case; only some of them are legal. Given our last example of an inductive type, many readers are probably eager to try encoding the syntax of %\index{lambda calculus}%lambda calculus. Indeed, the function-based representation technique that we just used, called%\index{higher-order abstract syntax}\index{HOAS|see{higher-order abstract syntax}}% _higher-order abstract syntax_ (HOAS)%~\cite{HOAS}%, is the representation of choice for lambda calculi in %\index{Twelf}%Twelf and in many applications implemented in Haskell and ML. Let us try to import that choice to Coq: *) (* begin hide *) @@ -800,7 +785,7 @@ To prove that [unit_rect] is nothing special, we can reimplement it manually. *) Definition unit_rect' (P : unit -> Type) (f : P tt) (u : unit) := - match u with + match u return P u with | tt => f end. @@ -810,9 +795,7 @@ (* end thide *) (* end hide *) -(** We rely on Coq's heuristics for inferring [match] annotations, which are not consulted in the pretty-printing of terms. - -We can check the implementation [nat_rect] as well: *) +(** We can check the implementation [nat_rect] as well: *) Print nat_rect. (** %\vspace{-.15in}% [[
--- a/src/Intro.v Wed Sep 05 15:22:13 2012 -0400 +++ b/src/Intro.v Wed Sep 26 16:35:35 2012 -0400 @@ -20,9 +20,9 @@ Around the beginning of the 21st century, the pace of progress in practical applications of interactive theorem proving accelerated significantly. Several well-known formal developments have been carried out in Coq, the system that this book deals with. In the realm of pure mathematics, Georges Gonthier built a machine-checked proof of the four color theorem%~\cite{4C}%, a mathematical problem first posed more than a hundred years before, where the only previous proofs had required trusting ad-hoc software to do brute-force checking of key facts. In the realm of program verification, Xavier Leroy led the CompCert project to produce a verified C compiler back-end%~\cite{CompCert}% robust enough to use with real embedded software. -Many other recent projects have attracted attention by proving important theorems using computer proof assistant software. For instance, the L4.verified project%~\cite{seL4}% has given a mechanized proof of correctness for a realistic microkernel, using the Isabelle/HOL proof assistant%~\cite{Isabelle/HOL}\index{Isabelle/HOL}%. The amount of ongoing work in the area is so large that I cannot hope to list all the recent successes, so from this point I will assume that the reader is convinced both that we ought to want machine-checked proofs and that they seem to be feasible to produce. (To readers not yet convinced, I suggest a Web search for "machine-checked proof"!) +Many other recent projects have attracted attention by proving important theorems using computer proof assistant software. For instance, the L4.verified project%~\cite{seL4}% led by Gerwin Klein has given a mechanized proof of correctness for a realistic microkernel, using the Isabelle/HOL proof assistant%~\cite{Isabelle/HOL}\index{Isabelle/HOL}%. The amount of ongoing work in the area is so large that I cannot hope to list all the recent successes, so from this point I will assume that the reader is convinced both that we ought to want machine-checked proofs and that they seem to be feasible to produce. (To readers not yet convinced, I suggest a Web search for "machine-checked proof"!) -The idea of %\index{certified program}% _certified program_ features prominently in this book's title. Here the word "certified" does _not_ refer to governmental rules for how the reliability of engineered systems may be demonstrated to sufficiently high standards. Rather, this concept of certification, a standard one in the programming languages and formal methods communities, has to do with the idea of a _certificate_, or formal mathematical artifact proving that a program meets its specification. Government certification procedures rarely provide strong mathematical guarantees, while certified programming provides guarantees about as strong as anything we could hope for. We trust the definition of a foundational mathematical logic, we trust an implementation of the logic, and we trust that we have encoded our informal intent properly in formal specifications, but little else is left open as an opportunity to certify incorrect software. For programs like compilers that run in batch mode, the notion of a %\index{certifying program}% _certifying_ program is also common, where each run of the program outputs both an answer and a proof that it is correct. Certified software can be considered to subsume certifying software, and this book focuses on the certified case, while also introducing principles and techniques of general interest for stating and proving theorems in Coq. +The idea of %\index{certified program}% _certified program_ features prominently in this book's title. Here the word "certified" does _not_ refer to governmental rules for how the reliability of engineered systems may be demonstrated to sufficiently high standards. Rather, this concept of certification, a standard one in the programming languages and formal methods communities, has to do with the idea of a _certificate_, or formal mathematical artifact proving that a program meets its specification. Government certification procedures rarely provide strong mathematical guarantees, while certified programming provides guarantees about as strong as anything we could hope for. We trust the definition of a foundational mathematical logic, we trust an implementation of the logic, and we trust that we have encoded our informal intent properly in formal specifications, but little else is left open as an opportunity to certify incorrect software. For programs like compilers that run in batch mode, the notion of a %\index{certifying program}% _certifying_ program is also common, where each run of the program outputs both an answer and a proof that the answer is correct. Certified software can be considered to subsume certifying software, and this book focuses on the certified case, while also introducing principles and techniques of general interest for stating and proving theorems in Coq. %\medskip% @@ -79,7 +79,7 @@ %\index{ACL2}%ACL2 and %\index{HOL}%HOL lack dependent types outright. %\index{PVS}%PVS and %\index{Twelf}%Twelf each supports a different strict subset of Coq's dependent type language. Twelf's type language is restricted to a bare-bones, monomorphic lambda calculus, which places serious restrictions on how complicated _computations inside types_ can be. This restriction is important for the soundness argument behind Twelf's approach to representing and checking proofs. -In contrast, %\index{PVS}%PVS's dependent types are much more general, but they are squeezed inside the single mechanism of _subset types_, where a normal type is refined by attaching a predicate over its elements. Each member of the subset type is an element of the base type that satisfies the predicate. Chapter 7 of this book introduces that style of programming in Coq, while the remaining chapters of Part II deal with features of dependent typing in Coq that go beyond what PVS supports. +In contrast, %\index{PVS}%PVS's dependent types are much more general, but they are squeezed inside the single mechanism of _subset types_, where a normal type is refined by attaching a predicate over its elements. Each member of the subset type is an element of the base type that satisfies the predicate. Chapter 6 of this book introduces that style of programming in Coq, while the remaining chapters of Part II deal with features of dependent typing in Coq that go beyond what PVS supports. Dependent types are not just useful because they help you express correctness properties in types. Dependent types also often let you write certified programs _without writing anything that looks like a proof_. Even with subset types, which for many contexts can be used to express any relevant property with enough acrobatics, the human driving the proof assistant usually has to build some proofs explicitly. Writing formal proofs is hard, so we want to avoid it as far as possible, so dependent types are invaluable. @@ -90,7 +90,7 @@ (** %\index{de Bruijn criterion}%Scores of automated decision procedures are useful in practical theorem proving, but it is unfortunate to have to trust in the correct implementation of each procedure. Proof assistants satisfy the "de Bruijn criterion" when they produce _proof terms_ in small kernel languages, even when they use complicated and extensible procedures to seek out proofs in the first place. These core languages have feature complexity on par with what you find in proposals for formal foundations for mathematics (e.g., ZF set theory). To believe a proof, we can ignore the possibility of bugs during _search_ and just rely on a (relatively small) proof-checking kernel that we apply to the _result_ of the search. -Coq meets the de Bruijn criterion, while %\index{ACL2}%ACL2 and %\index{PVS}%PVS do not, as they employ fancy decision procedures that produce no "evidence trails" justifying their results. +Coq meets the de Bruijn criterion, while %\index{ACL2}%ACL2 and %\index{PVS}%PVS do not, as they employ fancy decision procedures that produce no "evidence trails" justifying their results. The HOL implementations also meet the de Bruijn criterion; for Twelf, the situation is murkier. *) (** ** Convenient Programmable Proof Automation *) @@ -108,7 +108,7 @@ (** ** Proof by Reflection *) (** -%\index{reflection}\index{proof by reflection}%A surprising wealth of benefits follow from choosing a proof language that integrates a rich notion of computation. Coq includes programs and proof terms in the same syntactic class. This makes it easy to write programs that compute proofs. With rich enough dependent types, such programs are _certified decision procedures_. In such cases, these certified procedures can be put to good use _without ever running them_! Their types guarantee that, if we did bother to run them, we would receive proper "ground" proofs. +%\index{reflection}\index{proof by reflection}%A surprising wealth of benefits follows from choosing a proof language that integrates a rich notion of computation. Coq includes programs and proof terms in the same syntactic class. This makes it easy to write programs that compute proofs. With rich enough dependent types, such programs are _certified decision procedures_. In such cases, these certified procedures can be put to good use _without ever running them_! Their types guarantee that, if we did bother to run them, we would receive proper "ground" proofs. The critical ingredient for this technique, many of whose instances are referred to as _proof by reflection_, is a way of inducing non-trivial computation inside of logical propositions during proof checking. Further, most of these instances require dependent types to make it possible to state the appropriate theorems. Of the proof assistants I listed, only Coq really provides this support. *) @@ -165,7 +165,7 @@ (** ** Reading This Book *) (** -For experts in functional programming or formal methods, learning to use Coq is not hard, in a sense. The Coq manual%~\cite{CoqManual}% and the textbook by Bertot and Cast%\'%eran%~\cite{CoqArt}% have helped many people become productive Coq users. However, I believe that the best ways to manage significant Coq developments are far from settled. In this book, I mean to propose my own techniques, and, rather than treating them as advanced material for a final chapter or two, I employ them from the very beginning. After a first chapter showing off what can be done with dependent types, I retreat into simpler programming styles for the first part of the book. The other main thrust of the book, Ltac proof automation, I adopt more or less from the very start of the technical exposition. +For experts in functional programming or formal methods, learning to use Coq is not hard, in a sense. The Coq manual%~\cite{CoqManual}% and the textbook by Bertot and Cast%\'%eran%~\cite{CoqArt}% have helped many people become productive Coq users. However, I believe that the best ways to manage significant Coq developments are far from settled. In this book, I mean to propose my own techniques, and, rather than treating them as advanced material for a final chapter or two, I employ them from the very beginning. After a first chapter showing off what can be done with dependent types, I retreat into simpler programming styles for the first part of the book. I adopt the other main thrust of the book, Ltac proof automation, more or less from the very start of the technical exposition. Some readers have suggested that I give multiple recommended reading orders in this introduction, targeted at people with different levels of Coq expertise. It is certainly true that Part I of the book devotes significant space to basic concepts that most Coq users already know quite well. However, as I am introducing these concepts, I am also developing my preferred automated proof style, so I think even the chapters on basics are worth reading for experienced Coq hackers.
--- a/src/LogicProg.v Wed Sep 05 15:22:13 2012 -0400 +++ b/src/LogicProg.v Wed Sep 26 16:35:35 2012 -0400 @@ -406,7 +406,7 @@ Finished transaction in 0. secs (0.004u,0.s) >> -This [eauto] fails to prove the goal, but it least it takes substantially less than the 2 seconds required above! *) +This [eauto] fails to prove the goal, but at least it takes substantially less than the 2 seconds required above! *) Abort. (* end thide *)
--- a/src/Predicates.v Wed Sep 05 15:22:13 2012 -0400 +++ b/src/Predicates.v Wed Sep 26 16:35:35 2012 -0400 @@ -373,6 +373,8 @@ ex_intro : forall x : A, P x -> ex P ]] + (Note that here, as always, each [forall] quantifier has the largest possible scope, so that the type of [ex_intro] could also be written [forall x : A, (P x -> ex P)].) + The family [ex] is parameterized by the type [A] that we quantify over, and by a predicate [P] over [A]s. We prove an existential by exhibiting some [x] of type [A], along with a proof of [P x]. As usual, there are tactics that save us from worrying about the low-level details most of the time. We use the equality operator [=], which, depending on the settings in which they learned logic, different people will say either is or is not part of first-order logic. For our purposes, it is. *) Theorem exist1 : exists x : nat, x + 1 = 2. @@ -519,7 +521,7 @@ ]] - What on earth happened here? Internally, [destruct] replaced [1] with a fresh variable, and, trying to be helpful, it also replaced the occurrence of [1] within the unary representation of each number in the goal. This has the net effect of decrementing each of these numbers. *) + What on earth happened here? Internally, [destruct] replaced [1] with a fresh variable, and, trying to be helpful, it also replaced the occurrence of [1] within the unary representation of each number in the goal. Then, within the [O] case of the proof, we replace the fresh variable with [O]. This has the net effect of decrementing each of these numbers. *) Abort.
--- a/src/StackMachine.v Wed Sep 05 15:22:13 2012 -0400 +++ b/src/StackMachine.v Wed Sep 26 16:35:35 2012 -0400 @@ -42,7 +42,7 @@ %\medskip% -Now we are ready to say what these programs mean. We will do this by writing an %\index{interpreters}%interpreter that can be thought of as a trivial operational or denotational semantics. (If you are not familiar with these semantic techniques, no need to worry; we will stick to "common sense" constructions.)%\index{Vernacular commands!Definition}% *) +Now we are ready to say what programs in our expression language mean. We will do this by writing an %\index{interpreters}%interpreter that can be thought of as a trivial operational or denotational semantics. (If you are not familiar with these semantic techniques, no need to worry; we will stick to "common sense" constructions.)%\index{Vernacular commands!Definition}% *) Definition binopDenote (b : binop) : nat -> nat -> nat := match b with @@ -187,13 +187,12 @@ (** We are ready to prove that our compiler is implemented correctly. We can use a new vernacular command [Theorem] to start a correctness proof, in terms of the semantics we defined earlier:%\index{Vernacular commands!Theorem}% *) Theorem compile_correct : forall e, progDenote (compile e) nil = Some (expDenote e :: nil). -(* begin hide *) -Abort. -(* end hide *) (* begin thide *) (** Though a pencil-and-paper proof might clock out at this point, writing "by a routine induction on [e]," it turns out not to make sense to attack this proof directly. We need to use the standard trick of%\index{strengthening the induction hypothesis}% _strengthening the induction hypothesis_. We do that by proving an auxiliary lemma, using the command [Lemma] that is a synonym for [Theorem], conventionally used for less important theorems that appear in the proofs of primary theorems.%\index{Vernacular commands!Lemma}% *) +Abort. + Lemma compile_correct' : forall e p s, progDenote (compile e ++ p) s = progDenote p (expDenote e :: s). @@ -393,7 +392,7 @@ What we need is the associative law of list concatenation, which is available as a theorem [app_assoc_reverse] in the standard library.%\index{Vernacular commands!Check}% *) -Check app_assoc. +Check app_assoc_reverse. (** %\vspace{-.15in}%[[ app_assoc_reverse : forall (A : Type) (l m n : list A), (l ++ m) ++ n = l ++ m ++ n @@ -554,7 +553,7 @@ ML and Haskell have indexed algebraic datatypes. For instance, their list types are indexed by the type of data that the list carries. However, compared to Coq, ML and Haskell 98 place two important restrictions on datatype definitions. -First, the indices of the range of each data constructor must be type variables bound at the top level of the datatype definition. There is no way to do what we did here, where we, for instance, say that [TPlus] is a constructor building a [tbinop] whose indices are all fixed at [Nat]. %\index{generalized algebraic datatypes}\index{GADTs|see{generalized algebraic datatypes}}% _Generalized algebraic datatypes_ (GADTs)%~\cite{GADT}% are a popular feature in %\index{GHC Haskell}%GHC Haskell and other languages that removes this first restriction. +First, the indices of the range of each data constructor must be type variables bound at the top level of the datatype definition. There is no way to do what we did here, where we, for instance, say that [TPlus] is a constructor building a [tbinop] whose indices are all fixed at [Nat]. %\index{generalized algebraic datatypes}\index{GADTs|see{generalized algebraic datatypes}}% _Generalized algebraic datatypes_ (GADTs)%~\cite{GADT}% are a popular feature in %\index{GHC Haskell}%GHC Haskell, OCaml 4, and other languages that removes this first restriction. The second restriction is not lifted by GADTs. In ML and Haskell, indices of types must be types and may not be _expressions_. In Coq, types may be indexed by arbitrary Gallina terms. Type indices can live in the same universe as programs, and we can compute with them just like regular programs. Haskell supports a hobbled form of computation in type indices based on %\index{Haskell}%multi-parameter type classes, and recent extensions like type functions bring Haskell programming even closer to "real" functional programming with types, but, without dependent typing, there must always be a gap between how one programs with types and how one programs normally. *)
--- a/src/Subset.v Wed Sep 05 15:22:13 2012 -0400 +++ b/src/Subset.v Wed Sep 26 16:35:35 2012 -0400 @@ -519,17 +519,17 @@ Eval compute in In_dec eq_nat_dec 2 (1 :: 2 :: nil). (** %\vspace{-.15in}% [[ = Yes - : {In 2 (1 :: 2 :: nil)} + {~ In 2 (1 :: 2 :: nil)} + : {In 2 (1 :: 2 :: nil)} + { ~ In 2 (1 :: 2 :: nil)} ]] *) Eval compute in In_dec eq_nat_dec 3 (1 :: 2 :: nil). (** %\vspace{-.15in}% [[ = No - : {In 3 (1 :: 2 :: nil)} + {~ In 3 (1 :: 2 :: nil)} + : {In 3 (1 :: 2 :: nil)} + { ~ In 3 (1 :: 2 :: nil)} ]] -[In_dec] has a reasonable extraction to OCaml. *) +The [In_dec] function has a reasonable extraction to OCaml. *) Extraction In_dec. (* end thide *)
--- a/staging/updates.rss Wed Sep 05 15:22:13 2012 -0400 +++ b/staging/updates.rss Wed Sep 26 16:35:35 2012 -0400 @@ -12,6 +12,14 @@ <docs>http://blogs.law.harvard.edu/tech/rss</docs> <item> + <title>Batch of changes based on proofreader feedback</title> + <pubDate>Wed, 26 Sep 2012 16:31:01 EDT</pubDate> + <link>http://adam.chlipala.net/cpdt/</link> + <author>adamc@csail.mit.edu</author> + <description>Thanks to everyone who is helping with the final proofreading!</description> +</item> + +<item> <title>Ready for final proofreading</title> <pubDate>Thu, 30 Aug 2012 08:31:25 EDT</pubDate> <link>http://adam.chlipala.net/cpdt/</link>