TriplesStructural Reasoning Rules
Set Implicit Arguments.
The file LibSepReference.v contains definitions that are essentially the
same as those from Hprop.v and Himpl.v, but with one key difference:
LibSepReference makes the definition of Separation Logic operators opaque.
This chapter and the following ones import LibSepReference.v instead of
Hprop.v and Himpl.v.
As a result, we cannot unfold the definition of hstar, hpure, etc. any
more. To reason, we must instead use the introduction and elimination
lemmas, such as hstar_intro and hstar_elim. These lemmas enforce
abstraction, ensuring that the proofs do not depend on the particular choice
of the definitions used for constructing Separation Logic. For example, we
should not depend on whether \[P] is internally implemented as
fun h ⇒ h = empty ∧ P or as \∃ (p:P), \[].
First Pass
- lets H: L x y introduces an hypothesis named H, whose statement is the specialization of the lemma L on the arguments x and y. It is equivalent to generalize (L _ _ .. _ x _ _ .. _ _ y); intros H, for the appropriate number of underscore symbols.
- forwards H: L x y is very similar to lets, except that it attempts to instantiate all arguments of L. It is equivalent to generalize (L _ _ .. _ x _ _ .. _ _ y _ _ .. _ _); intros H.
- specializes H x y specializes an existing hypothesis H in place. It is equivalent to lets H2: H x y; clear H; rename H2 into H.
Formalized Syntax of the Programming Language
Values and Terms
Inductive val : Type :=
| val_unit : val
| val_bool : bool → val
| val_int : int → val
| val_loc : loc → val
| val_fun : var → trm → val
| val_fix : var → var → trm → val
| val_ref : val
| val_get : val
| val_set : val
| val_free : val
| val_add : val
| val_div : val
| val_rand : val
| val_unit : val
| val_bool : bool → val
| val_int : int → val
| val_loc : loc → val
| val_fun : var → trm → val
| val_fix : var → var → trm → val
| val_ref : val
| val_get : val
| val_set : val
| val_free : val
| val_add : val
| val_div : val
| val_rand : val
The grammar for terms includes values, variables, function definitions,
recursive function definitions, function applications, sequences,
let-bindings, and conditionals.
with trm : Type :=
| trm_val : val → trm
| trm_var : var → trm
| trm_fun : var → trm → trm
| trm_fix : var → var → trm → trm
| trm_app : trm → trm → trm
| trm_seq : trm → trm → trm
| trm_let : var → trm → trm → trm
| trm_if : trm → trm → trm → trm.
Note that trm_fun and trm_fix denote functions that may feature free
variables, unlike val_fun and val_fix, which denote closed values. More
concretely, in the term trm_fun x t, the term t may refer to the
variable x or to other free variables. In contrast, in the term
val_fun x t, the only free variable that t may have is x.
We will see, below, evaluation rules asserting that trm_fun x t reduces,
in the empty context, to val_fun x t. Likewise, trm_fix f x t reduces,
in the empty context, to val_fix f x t.
At a high level, the term val_fun x t may be thought of as the term
trm_fun x t augmented with a proof that this term trm_fun x t is a
closed term. The syntax that we consider therefore embeds "proofs that
values are closed". Thanks to this presentation, the substitution function
subst, which will be presented shortly afterwards, need not recurse
through values. Moreover, we never need to manipulate proofs that specific
terms are closed.
Functions of several arguments are encoded as nested functions (curried
functions). For example, the syntax fun x1 x2 x3 ⇒ t used to define a
top-level function in the chapters Basic and Repr is encoded as
val_fun x1 (trm_fun x2 (trm_fun x3 t)). Observe how the outer constructor
asserts that the whole function is a closed value, whereas the inner
functions are terms. In particular t may refer to x1 and x2, which are
free variables for the term trm_fun x3 t.
Likewise, a closed recursive function fix f x1 x2 x3 ⇒ t is encoded as
val_fix f x1 (trm_fun x2 (trm_fun x3 t)). Here only the outermost function
is recursive (and closed).
In the rest of the course, we will focus on functions with a single
argument. The treatment of n-ary functions is the topic of a
yet-to-be-released chapter.
State
For technical reasons related to the internal representation of finite maps,
to enable reading in a state, we need to justify that the grammar of values
is inhabited. This property is captured by the following command, whose
details are not relevant for understanding the rest of the chapter.
Substitution
Fixpoint subst (y:var) (w:val) (t:trm) : trm :=
let aux t := subst y w t in
let if_y_eq x t1 t2 := if var_eq x y then t1 else t2 in
match t with
| trm_val v ⇒ trm_val v
| trm_var x ⇒ if_y_eq x (trm_val w) t
| trm_fun x t1 ⇒ trm_fun x (if_y_eq x t1 (aux t1))
| trm_fix f x t1 ⇒ trm_fix f x (if_y_eq f t1 (if_y_eq x t1 (aux t1)))
| trm_app t1 t2 ⇒ trm_app (aux t1) (aux t2)
| trm_seq t1 t2 ⇒ trm_seq (aux t1) (aux t2)
| trm_let x t1 t2 ⇒ trm_let x (aux t1) (if_y_eq x t2 (aux t2))
| trm_if t0 t1 t2 ⇒ trm_if (aux t0) (aux t1) (aux t2)
end.
let aux t := subst y w t in
let if_y_eq x t1 t2 := if var_eq x y then t1 else t2 in
match t with
| trm_val v ⇒ trm_val v
| trm_var x ⇒ if_y_eq x (trm_val w) t
| trm_fun x t1 ⇒ trm_fun x (if_y_eq x t1 (aux t1))
| trm_fix f x t1 ⇒ trm_fix f x (if_y_eq f t1 (if_y_eq x t1 (aux t1)))
| trm_app t1 t2 ⇒ trm_app (aux t1) (aux t2)
| trm_seq t1 t2 ⇒ trm_seq (aux t1) (aux t2)
| trm_let x t1 t2 ⇒ trm_let x (aux t1) (if_y_eq x t2 (aux t2))
| trm_if t0 t1 t2 ⇒ trm_if (aux t0) (aux t1) (aux t2)
end.
Implicit Types and Coercions
Next, we give a collection of coercions. Coercions correspond to implicit
function calls, which aim to make statements more concise. For example,
val_loc is declared as a coercion, so that a location p of type loc
can be viewed as the value val_loc p where an expression of type val is
expected. Likewise, a boolean b may be viewed as the value val_bool b,
and an integer n may be viewed as the value val_int n.
The constructor trm_val is also declared as a coercion. Thus, instead of
writing trm_val v at a place where a term is expected, we can write just
v.
The constructor trm_app is declared as a "Funclass" coercion. This bit of
magic enables us to write t1 t2 as a shorthand for trm_app t1 t2. The
idea of associating trm_app as the "Funclass" coercion for the type trm
is that if a term t1 of type trm is applied like a function to an
argument, then t1 should be interpreted as trm_app t1.
The "Funclass" coercion for trm_app can be iterated. The expression
t1 t2 t3 is parsed by Coq as (t1 t2) t3. The first application t1 t2
is interpreted as trm_app t1 t2. This expression, which itself has type
trm, is applied to t3. Hence, t1 t2 t3 is interpreted as
trm_app (trm_app t1 t2) t3.
Loading Definitions from LibSepReference
End Syntax.
Implicit Types f : var.
Implicit Types b : bool.
Implicit Types p : loc.
Implicit Types n : int.
Implicit Types v w r : val.
Implicit Types t : trm.
Implicit Types h s : heap.
Implicit Types H : hprop.
Implicit Types f : var.
Implicit Types b : bool.
Implicit Types p : loc.
Implicit Types n : int.
Implicit Types v w r : val.
Implicit Types t : trm.
Implicit Types h s : heap.
Implicit Types H : hprop.
Standard Big-Step Semantics
1. big for values and function definitions.
A value evaluates to itself. A term function evaluates to a value
function. Likewise for a recursive function.
| big_val : ∀ s v,
big s (trm_val v) s v
| big_fun : ∀ s x t1,
big s (trm_fun x t1) s (val_fun x t1)
| big_fix : ∀ s f x t1,
big s (trm_fix f x t1) s (val_fix f x t1)
2. big for function applications.
The beta-reduction rule asserts that (val_fun x t1) v2 evaluates to the
same result as subst x v2 t1. Likewise, (val_fix f x t1) v2 evaluates
to subst x v2 (subst f v1 t1), where v1 denotes the recursive function
itself, that is, val_fix f x t1.
| big_app_fun : ∀ s1 s2 v1 v2 x t1 v,
v1 = val_fun x t1 →
big s1 (subst x v2 t1) s2 v →
big s1 (trm_app v1 v2) s2 v
| big_app_fix : ∀ s1 s2 v1 v2 f x t1 v,
v1 = val_fix f x t1 →
big s1 (subst x v2 (subst f v1 t1)) s2 v →
big s1 (trm_app v1 v2) s2 v
3. big for structural constructs.
A sequence trm_seq t1 t2 first evaluates t1, taking the state from
s1 to s2, then drops the result of t1 and evaluates t2, taking the
state from s2 to s3.
The let-binding trm_let x t1 t2 is similar, except that the variable x
gets substituted with the result of t1 inside t2.
| big_seq : ∀ s1 s2 s3 t1 t2 v1 v,
big s1 t1 s2 v1 →
big s2 t2 s3 v →
big s1 (trm_seq t1 t2) s3 v
| big_let : ∀ s1 s2 s3 x t1 t2 v1 r,
big s1 t1 s2 v1 →
big s2 (subst x v1 t2) s3 r →
big s1 (trm_let x t1 t2) s3 r
4. big for conditionals.
A conditional in a source program is assumed to be of the form
if t0 then t1 else t2, where t0 is either a variable or a value. If
t0 is a variable, then, by the time it reaches an evaluation position,
the variable must have been substituted by a value. Thus, the evaluation
rule only considers the form if v0 then t1 else t2. The value v0 must
be a boolean value, otherwise evaluation gets stuck.
The term trm_if (val_bool true) t1 t2 behaves like t1, whereas the
term trm_if (val_bool false) t1 t2 behaves like t2. This behavior is
described by a single rule, leveraging Coq's "if" constructor to factor
out the two cases.
| big_if : ∀ s1 s2 b v t1 t2,
big s1 (if b then t1 else t2) s2 v →
big s1 (trm_if (val_bool b) t1 t2) s2 v
5. big for primitive stateless operations.
For similar reasons, the behavior of applied primitive functions only
needs to be described for the case of value arguments.
An arithmetic operation expects integer arguments. The addition of
val_int n1 and val_int n2 produces val_int (n1 + n2).
The division operation, on the same arguments, produces the quotient
n1 / n2, under the assumption that the divisor n2 is non-zero. In
other words, if a program performs a division by zero, then it cannot
satisfy the big judgment.
The random number generator val_rand n produces an integer n1 in the
range from 0 (inclusive) to n (exclusive).
| big_add : ∀ s n1 n2,
big s (val_add (val_int n1) (val_int n2)) s (val_int (n1 + n2))
| big_div : ∀ s n1 n2,
n2 ≠ 0 →
big s (val_div (val_int n1) (val_int n2)) s (val_int (Z.quot n1 n2))
| big_rand : ∀ s n n1,
0 ≤ n1 < n →
big s (val_rand (val_int n)) s (val_int n1)
6. big for primitive operations on memory.
The term val_ref v allocates a fresh cell with contents v and returns
the location, p, of the new cell. This location must not be previously
in the domain of the store s.
The term val_get (val_loc p) reads the value in the store s at
location p. The location must be bound to a value in the store,
otherwise evaluation is stuck.
The term val_set (val_loc p) v updates the store at a location p
assumed to be bound in the store s. The operation modifies the store and
returns the unit value.
The term val_free (val_loc p) deallocates the cell at location p.
| big_ref : ∀ s v p,
¬ Fmap.indom s p →
big s (val_ref v) (Fmap.update s p v) (val_loc p)
| big_get : ∀ s p,
Fmap.indom s p →
big s (val_get (val_loc p)) s (Fmap.read s p)
| big_set : ∀ s p v,
Fmap.indom s p →
big s (val_set (val_loc p) v) (Fmap.update s p v) val_unit
| big_free : ∀ s p,
Fmap.indom s p →
big s (val_free (val_loc p)) (Fmap.remove s p) val_unit.
Omni-Big-Step Semantics
The inductive definition below defines the omni-big-step judgment, which has
the form eval s t Q, where Q is a postcondition. The generalization from
standard-big-step to omni-big-step follows a regular pattern, which should
become apparent while stepping through the rules.
1. eval for values and function definitions.
The judgment eval s v Q asserts that the value v in the state s
satisfies the postcondition Q. The premise for deriving that judgment is
thus that Q v s must hold.
| eval_val : ∀ s v Q,
Q v s →
eval s (trm_val v) Q
| eval_fun : ∀ s x t1 Q,
Q (val_fun x t1) s →
eval s (trm_fun x t1) Q
| eval_fix : ∀ s f x t1 Q,
Q (val_fix f x t1) s →
eval s (trm_fix f x t1) Q
2. eval for function applications.
Consider a function v1 of the form val_fun x t1. The term
trm_app v1 v2 terminates with postcondition Q provided that the
substituted term subst x v2 t1 terminates with postcondition Q. Hence,
to prove eval s1 (trm_app v1 v2) Q, we must establish
eval s1 (subst x v2 t1) Q.
| eval_app_fun : ∀ s1 v1 v2 x t1 Q,
v1 = val_fun x t1 →
eval s1 (subst x v2 t1) Q →
eval s1 (trm_app v1 v2) Q
| eval_app_fix : ∀ s v1 v2 f x t1 Q,
v1 = val_fix f x t1 →
eval s (subst x v2 (subst f v1 t1)) Q →
eval s (trm_app v1 v2) Q
3. eval for sequencing.
Consider a sequence trm_seq t1 t2 in a state s. What is the
requirement for this configuration to terminate with postcondition Q?
First, we need the evaluation of t1 in s to terminate. Let Q1 be (an
overapproximation of) the set of possible results for the execution of
(s,t1). Then, to prove that the sequence trm_seq t1 t2 terminates in
Q, we need to show that, for any intermediate state s2 satisfying
Q1, the evaluation of t2 terminates with postcondition Q.
The case of let-bindings is similar, only with an additional substitution.
| eval_seq : ∀ Q1 s t1 t2 Q,
eval s t1 Q1 →
(∀ v1 s2, Q1 v1 s2 → eval s2 t2 Q) →
eval s (trm_seq t1 t2) Q
| eval_let : ∀ Q1 s x t1 t2 Q,
eval s t1 Q1 →
(∀ v1 s2, Q1 v1 s2 → eval s2 (subst x v1 t2) Q) →
eval s (trm_let x t1 t2) Q
4. eval for conditionals.
The term (trm_if (val_bool b) t1 t2) has the same behaviors as the term
t1 when b is true, and as the term t2 when b is false.
| eval_if : ∀ s (b:bool) t1 t2 Q,
eval s (if b then t1 else t2) Q →
eval s (trm_if (val_bool b) t1 t2) Q
5. eval for primitive stateless operations.
The judgment eval s (val_add n1 n2) Q asserts that the pair of the state
s and the value n1+n2 produced by the addition operation satisfies the
postcondition Q. Hence, the premise is Q (val_int (n1 + n2)) s.
Nondeterministic constructs are more interesting. The rule eval_rand gives
the condition under which the term val_rand n, evaluated in a state s,
produces an output satisfying Q. The first premise of the rule requires
n > 0. The second premise requires that, for any value n1 that
val_rand n may evaluate to -- that is, such that 0 ≤ n1 < n -- the
configuration made of n1 and s satisfies the postcondition Q.
Formally: ∀ n1, (0 ≤ n1 < n) → (Q n1 s).
| eval_add : ∀ s n1 n2 Q,
Q (val_int (n1 + n2)) s →
eval s (val_add (val_int n1) (val_int n2)) Q
| eval_div : ∀ s n1 n2 Q,
n2 ≠ 0 →
Q (val_int (Z.quot n1 n2)) s →
eval s (val_div (val_int n1) (val_int n2)) Q
| eval_rand : ∀ s n Q,
n > 0 →
(∀ n1, 0 ≤ n1 < n → Q n1 s) →
eval s (val_rand (val_int n)) Q
6. eval for primitive operations on memory.
The most interesting case here is that of allocation, which is
nondeterministic. For val_ref v, evaluated in a state s, to produce a
configuration in Q, we require that, for any location p that is "fresh"
for the state s, the pair made of p and "s extended with a binding
from p to v" satisfies Q.
| eval_ref : ∀ s v Q,
(∀ p, ¬ Fmap.indom s p →
Q (val_loc p) (Fmap.update s p v)) →
eval s (val_ref v) Q
| eval_get : ∀ s p Q,
Fmap.indom s p →
Q (Fmap.read s p) s →
eval s (val_get (val_loc p)) Q
| eval_set : ∀ s p v Q,
Fmap.indom s p →
Q val_unit (Fmap.update s p v) →
eval s (val_set (val_loc p) v) Q
| eval_free : ∀ s p Q,
Fmap.indom s p →
Q val_unit (Fmap.remove s p) →
eval s (val_free (val_loc p)) Q.
Note that, given eval s t Q, it is *not* necessarily the case that Q is
the exact set of configurations that are reachable from (s,t); it may
rather be an overapproximation of that set. Trying to set up an inductive
definition that relates (s,t) with exactly its set of reachable
configurations would introduce unnecessary complications.
Note, also, that eval s t Q will not hold if there exist one or more
executions of (s,t) that run into an error, i.e., that reach a
configuration that is stuck. To see why, consider a program of the form
let n = rand 10 in (f n). When applying the rule eval_let to this
program, the postcondition Q1 associated with rand 10 must include all
integers in the range 0..9. The second premise of the rule eval_let is
of the form ∀ v1 s2, Q1 v1 s2 → eval s2 (subst x v1 t2) Q. In our
specific example, this premise asserts that for any result in Q1, which
includes all integers n in the range 0..9, it is the case that the term
f n terminates.
Definition of Triples
Because the predicate eval s t Q captures termination, the predicate
triple t H Q that we build on top of it also captures termination. It
therefore yields a "total correctness" triple. If we had defined the
predicate eval as CoInductive instead of Inductive, we would have
allowed for infinite executions.
With such a coinductive definition of eval, we would have obtained a
"partial correctness" triple instead. Such a partial correctness triple
would still capture the property that none of the possible executions can
get stuck -- each possible execution either terminates safely with a result
satisfying the postcondition, or else diverges.
Throughout the development of program logics, most research has concentrated
on "partial correctness" logics. Depending on the set up, capturing "total
correctness" is often much more challenging. In particular, when considering
nondeterministic small-step semantics, and in particular when targeting
concurrent programs, it can be nonobvious how to account for termination.
But as long as we are concerned with sequential programs (i.e., without
concurrency), omni-big-step semantics simplify reasoning about
nondeterministic termination.
Structural Rules
The frame rule asserts that an arbitrary heap predicate may be added to both
the precondition and the postcondition of any triple.
The consequence rule asserts, as in Hoare Logic, that the validity of a
triple is preserved if we strengthen its precondition or weaken its
postcondition.
The "extraction rule for pure facts" asserts that a judgment of the form
triple t (\[P] \* H) Q is derivable from P → triple t H Q. This
structural rule captures the extraction of the pure facts out of the
precondition of a triple, like himpl_hstar_hpure_l for entailments.
The "extraction rule for existentials" asserts that a judgment of the form
triple t (\∃ x, J x) Q is derivable from
∀ x, triple t (J x) Q. Again, this rule is the counterpart of the
corresponding rule on entailments, himpl_hexists_l.
Parameter triple_hexists : ∀ t (A:Type) (J:A→hprop) Q,
(∀ x, triple t (J x) Q) →
triple t (\∃ x, J x) Q.
(∀ x, triple t (J x) Q) →
triple t (\∃ x, J x) Q.
Derived Structural Rules
Lemma triple_conseq_frame : ∀ H2 H1 Q1 t H Q,
triple t H1 Q1 →
H ==> H1 \* H2 →
Q1 \*+ H2 ===> Q →
triple t H Q.
triple t H1 Q1 →
H ==> H1 \* H2 →
Q1 \*+ H2 ===> Q →
triple t H Q.
Exercise: 1 star, standard, especially useful (triple_conseq_frame)
Prove the combined consequence-frame rule.
Proof using. (* FILL IN HERE *) Admitted.
☐
☐
Exercise: 1 star, standard, optional (triple_hpure')
Prove that triple_hpure' is indeed a corollary of triple_hpure.
Lemma triple_hpure' : ∀ t (P:Prop) Q,
(P → triple t \[] Q) →
triple t \[P] Q.
Proof using. (* FILL IN HERE *) Admitted.
☐
(P → triple t \[] Q) →
triple t \[P] Q.
Proof using. (* FILL IN HERE *) Admitted.
☐
Proof of the Consequence Rule
Lemma eval_conseq : ∀ s t Q1 Q2,
eval s t Q1 →
Q1 ===> Q2 →
eval s t Q2.
Proof using.
introv M W.
asserts W': (∀ v h, Q1 v h → Q2 v h). { auto. } clear W.
induction M; try solve [ constructors* ].
Qed.
eval s t Q1 →
Q1 ===> Q2 →
eval s t Q2.
Proof using.
introv M W.
asserts W': (∀ v h, Q1 v h → Q2 v h). { auto. } clear W.
induction M; try solve [ constructors* ].
Qed.
From there, it is straightforward to derive the consequence rule.
Lemma triple_conseq : ∀ t H' Q' H Q,
triple t H' Q' →
H ==> H' →
Q' ===> Q →
triple t H Q.
Proof using. unfolds triple. introv M MH MQ HF. applys* eval_conseq. Qed.
triple t H' Q' →
H ==> H' →
Q' ===> Q →
triple t H Q.
Proof using. unfolds triple. introv M MH MQ HF. applys* eval_conseq. Qed.
Proof of the Frame Rule
Lemma eval_frame : ∀ h1 h2 t Q,
eval h1 t Q →
Fmap.disjoint h1 h2 →
eval (h1 \u h2) t (Q \*+ (= h2)).
eval h1 t Q →
Fmap.disjoint h1 h2 →
eval (h1 \u h2) t (Q \*+ (= h2)).
This proof goes by induction. The most interesting step is for allocation.
In a derivation built using eval_ref, we are given as assumption a
property that holds for any location p fresh from h1, and we are
requested to prove a property that holds for any location p fresh from
h1 \u h2. We are thus restricting the set of p that can be considered
for allocation, and so the result holds.
Proof using.
introv M HD. gen h2. induction M; intros;
try solve [ hint hstar_intro; constructors* ].
{ rename M into M1, H into M2, IHM into IH1, H0 into IH2.
specializes IH1 HD. applys eval_seq IH1. introv HK.
lets (h1'&h2'&K1'&K2'&KD&KU): hstar_inv HK. subst. applys* IH2. }
{ rename M into M1, H into M2, IHM into IH1, H0 into IH2.
specializes IH1 HD. applys eval_let IH1. introv HK.
lets (h1'&h2'&K1'&K2'&KD&KU): hstar_inv HK. subst. applys* IH2. }
{ (* Here is the interesting case about allocation. *)
rename H into M. applys eval_ref. intros p Hp.
rewrite Fmap.indom_union_eq in Hp. rew_logic in Hp.
destruct Hp as [Hp1 Hp2].
rewrite* Fmap.update_union_not_r. applys hstar_intro.
{ applys* M. } { auto. } { applys* Fmap.disjoint_update_not_r. } }
{ applys eval_get. { rewrite* Fmap.indom_union_eq. }
{ rewrite* Fmap.read_union_l. applys* hstar_intro. } }
{ applys eval_set. { rewrite* Fmap.indom_union_eq. }
{ rewrite* Fmap.update_union_l. applys hstar_intro.
{ auto. } { auto. } { applys* Fmap.disjoint_update_l. } } }
{ applys eval_free. { rewrite* Fmap.indom_union_eq. }
{ rewrite* Fmap.remove_disjoint_union_l. applys hstar_intro.
{ auto. } { auto. } { applys* Fmap.disjoint_remove_l. } } }
Qed.
introv M HD. gen h2. induction M; intros;
try solve [ hint hstar_intro; constructors* ].
{ rename M into M1, H into M2, IHM into IH1, H0 into IH2.
specializes IH1 HD. applys eval_seq IH1. introv HK.
lets (h1'&h2'&K1'&K2'&KD&KU): hstar_inv HK. subst. applys* IH2. }
{ rename M into M1, H into M2, IHM into IH1, H0 into IH2.
specializes IH1 HD. applys eval_let IH1. introv HK.
lets (h1'&h2'&K1'&K2'&KD&KU): hstar_inv HK. subst. applys* IH2. }
{ (* Here is the interesting case about allocation. *)
rename H into M. applys eval_ref. intros p Hp.
rewrite Fmap.indom_union_eq in Hp. rew_logic in Hp.
destruct Hp as [Hp1 Hp2].
rewrite* Fmap.update_union_not_r. applys hstar_intro.
{ applys* M. } { auto. } { applys* Fmap.disjoint_update_not_r. } }
{ applys eval_get. { rewrite* Fmap.indom_union_eq. }
{ rewrite* Fmap.read_union_l. applys* hstar_intro. } }
{ applys eval_set. { rewrite* Fmap.indom_union_eq. }
{ rewrite* Fmap.update_union_l. applys hstar_intro.
{ auto. } { auto. } { applys* Fmap.disjoint_update_l. } } }
{ applys eval_free. { rewrite* Fmap.indom_union_eq. }
{ rewrite* Fmap.remove_disjoint_union_l. applys hstar_intro.
{ auto. } { auto. } { applys* Fmap.disjoint_remove_l. } } }
Qed.
The frame rule is derived from eval_frame and eval_conseq.
Lemma triple_frame : ∀ t H Q H',
triple t H Q →
triple t (H \* H') (Q \*+ H').
Proof.
introv M. intros h HF. lets (h1&h2&M1&M2&MD&MU): hstar_inv (rm HF).
subst. specializes M M1. applys eval_conseq.
{ applys eval_frame M MD. } { xsimpl. intros h' →. applys M2. }
Qed.
triple t H Q →
triple t (H \* H') (Q \*+ H').
Proof.
introv M. intros h HF. lets (h1&h2&M1&M2&MD&MU): hstar_inv (rm HF).
subst. specializes M M1. applys eval_conseq.
{ applys eval_frame M MD. } { xsimpl. intros h' →. applys M2. }
Qed.
Proof of the Extraction Rules
Lemma triple_hpure : ∀ t (P:Prop) H Q,
(P → triple t H Q) →
triple t (\[P] \* H) Q.
Proof using.
introv M. intros h (h1&h2&M1&M2&D&U). destruct M1 as (M1&HP).
lets E: hempty_inv HP. subst. rewrite Fmap.union_empty_l. applys¬M.
Qed.
(P → triple t H Q) →
triple t (\[P] \* H) Q.
Proof using.
introv M. intros h (h1&h2&M1&M2&D&U). destruct M1 as (M1&HP).
lets E: hempty_inv HP. subst. rewrite Fmap.union_empty_l. applys¬M.
Qed.
Lemma triple_hexists : ∀ t (A:Type) (J:A→hprop) Q,
(∀ (x:A), triple t (J x) Q) →
triple t (hexists J) Q.
Proof using. (* FILL IN HERE *) Admitted.
☐
(∀ (x:A), triple t (J x) Q) →
triple t (hexists J) Q.
Proof using. (* FILL IN HERE *) Admitted.
☐
Rule for Naming Heaps
Exercise: 1 star, standard, optional (triple_named_heap)
Prove the reasoning rule hoare_named_heap.
Lemma triple_named_heap : ∀ t H Q,
(∀ h, H h → triple t (= h) Q) →
triple t H Q.
Proof using. (* FILL IN HERE *) Admitted.
☐
(∀ h, H h → triple t (= h) Q) →
triple t H Q.
Proof using. (* FILL IN HERE *) Admitted.
☐
Traditional papers on Separation Logic do not include triple_hexists, but
instead include a rule called here triple_hexists2, which features an
existential quantifier both in the precondition and in the postcondition. As
we show next, in the presence of the consequence rule, the two rules are
equivalent.
Exercise: 2 stars, standard, especially useful (triple_hexists2)
Using triple_hexists and triple_conseq, as well as the tactic xsimpl, prove that triple_hexists2 is derivable.
Lemma triple_hexists2 : ∀ A (Hof:A→hprop) (Qof:A→val→hprop) t,
(∀ x, triple t (Hof x) (Qof x)) →
triple t (\∃ x, Hof x) (fun v ⇒ \∃ x, Qof x v).
Proof using. (* FILL IN HERE *) Admitted.
(* *)
(∀ x, triple t (Hof x) (Qof x)) →
triple t (\∃ x, Hof x) (fun v ⇒ \∃ x, Qof x v).
Proof using. (* FILL IN HERE *) Admitted.
(* *)
Exercise: 2 stars, standard, especially useful (triple_hexists_of_triple_hexists2)
Reciprocally, using triple_hexists2 and triple_conseq, as well as the tactic xsimpl, prove that triple_hexists is derivable. Of course, you may not use triple_hexists in this proof.)
Lemma triple_hexists_of_triple_hexists2 : ∀ t (A:Type) (Hof:A→hprop) Q,
(∀ x, triple t (Hof x) Q) →
triple t (\∃ x, Hof x) Q.
Proof using. (* FILL IN HERE *) Admitted.
(* *)
(∀ x, triple t (Hof x) Q) →
triple t (\∃ x, Hof x) Q.
Proof using. (* FILL IN HERE *) Admitted.
(* *)
Compared with triple_hexists2, the formulation of triple_hexists is more
concise, and is easier to exploit in practice.
Small-Step Semantics
Inductive step : heap → trm → heap → trm → Prop :=
(* Context rules *)
| step_seq_ctx : ∀ s1 s2 t1 t1' t2,
step s1 t1 s2 t1' →
step s1 (trm_seq t1 t2) s2 (trm_seq t1' t2)
| step_let_ctx : ∀ s1 s2 x t1 t1' t2,
step s1 t1 s2 t1' →
step s1 (trm_let x t1 t2) s2 (trm_let x t1' t2)
(* Reductions *)
| step_fun : ∀ s x t1,
step s (trm_fun x t1) s (val_fun x t1)
| step_fix : ∀ s f x t1,
step s (trm_fix f x t1) s (val_fix f x t1)
| step_app_fun : ∀ s v1 v2 x t1,
v1 = val_fun x t1 →
step s (trm_app v1 v2) s (subst x v2 t1)
| step_app_fix : ∀ s v1 v2 f x t1,
v1 = val_fix f x t1 →
step s (trm_app v1 v2) s (subst x v2 (subst f v1 t1))
| step_if : ∀ s b t1 t2,
step s (trm_if (val_bool b) t1 t2) s (if b then t1 else t2)
| step_seq : ∀ s t2 v1,
step s (trm_seq v1 t2) s t2
| step_let : ∀ s x t2 v1,
step s (trm_let x v1 t2) s (subst x v1 t2)
(* Primitive operations *)
| step_add : ∀ s n1 n2,
step s (val_add (val_int n1) (val_int n2)) s (val_int (n1 + n2))
| step_div : ∀ s n1 n2,
n2 ≠ 0 →
step s (val_div (val_int n1) (val_int n2)) s (Z.quot n1 n2)
| step_rand : ∀ s n n1,
0 ≤ n1 < n →
step s (val_rand (val_int n)) s (val_int n1)
| step_ref : ∀ s v p,
¬ Fmap.indom s p →
step s (val_ref v) (Fmap.update s p v) (val_loc p)
| step_get : ∀ s p,
Fmap.indom s p →
step s (val_get (val_loc p)) s (Fmap.read s p)
| step_set : ∀ s p v,
Fmap.indom s p →
step s (val_set (val_loc p) v) (Fmap.update s p v) val_unit
| step_free : ∀ s p,
Fmap.indom s p →
step s (val_free (val_loc p)) (Fmap.remove s p) val_unit.
(* Context rules *)
| step_seq_ctx : ∀ s1 s2 t1 t1' t2,
step s1 t1 s2 t1' →
step s1 (trm_seq t1 t2) s2 (trm_seq t1' t2)
| step_let_ctx : ∀ s1 s2 x t1 t1' t2,
step s1 t1 s2 t1' →
step s1 (trm_let x t1 t2) s2 (trm_let x t1' t2)
(* Reductions *)
| step_fun : ∀ s x t1,
step s (trm_fun x t1) s (val_fun x t1)
| step_fix : ∀ s f x t1,
step s (trm_fix f x t1) s (val_fix f x t1)
| step_app_fun : ∀ s v1 v2 x t1,
v1 = val_fun x t1 →
step s (trm_app v1 v2) s (subst x v2 t1)
| step_app_fix : ∀ s v1 v2 f x t1,
v1 = val_fix f x t1 →
step s (trm_app v1 v2) s (subst x v2 (subst f v1 t1))
| step_if : ∀ s b t1 t2,
step s (trm_if (val_bool b) t1 t2) s (if b then t1 else t2)
| step_seq : ∀ s t2 v1,
step s (trm_seq v1 t2) s t2
| step_let : ∀ s x t2 v1,
step s (trm_let x v1 t2) s (subst x v1 t2)
(* Primitive operations *)
| step_add : ∀ s n1 n2,
step s (val_add (val_int n1) (val_int n2)) s (val_int (n1 + n2))
| step_div : ∀ s n1 n2,
n2 ≠ 0 →
step s (val_div (val_int n1) (val_int n2)) s (Z.quot n1 n2)
| step_rand : ∀ s n n1,
0 ≤ n1 < n →
step s (val_rand (val_int n)) s (val_int n1)
| step_ref : ∀ s v p,
¬ Fmap.indom s p →
step s (val_ref v) (Fmap.update s p v) (val_loc p)
| step_get : ∀ s p,
Fmap.indom s p →
step s (val_get (val_loc p)) s (Fmap.read s p)
| step_set : ∀ s p v,
Fmap.indom s p →
step s (val_set (val_loc p) v) (Fmap.update s p v) val_unit
| step_free : ∀ s p,
Fmap.indom s p →
step s (val_free (val_loc p)) (Fmap.remove s p) val_unit.
The judgment steps s t s' t' corresponds to the reflexive and transitive
closure of step. Concretely, it asserts that the configuration (s,t) can
reduce in zero, one, or several steps to (s',t').
Inductive steps : heap → trm → heap → trm → Prop :=
| steps_refl : ∀ s t,
steps s t s t
| steps_step : ∀ s1 s2 s3 t1 t2 t3,
step s1 t1 s2 t2 →
steps s2 t2 s3 t3 →
steps s1 t1 s3 t3.
Lemma steps_of_step : ∀ s1 s2 t1 t2,
step s1 t1 s2 t2 →
steps s1 t1 s2 t2.
Proof using. introv M. applys steps_step M. applys steps_refl. Qed.
Lemma steps_trans : ∀ s1 s2 s3 t1 t2 t3,
steps s1 t1 s2 t2 →
steps s2 t2 s3 t3 →
steps s1 t1 s3 t3.
Proof using.
introv M1. induction M1; introv M2. { auto. } { constructors*. }
Qed.
| steps_refl : ∀ s t,
steps s t s t
| steps_step : ∀ s1 s2 s3 t1 t2 t3,
step s1 t1 s2 t2 →
steps s2 t2 s3 t3 →
steps s1 t1 s3 t3.
Lemma steps_of_step : ∀ s1 s2 t1 t2,
step s1 t1 s2 t2 →
steps s1 t1 s2 t2.
Proof using. introv M. applys steps_step M. applys steps_refl. Qed.
Lemma steps_trans : ∀ s1 s2 s3 t1 t2 t3,
steps s1 t1 s2 t2 →
steps s2 t2 s3 t3 →
steps s1 t1 s3 t3.
Proof using.
introv M1. induction M1; introv M2. { auto. } { constructors*. }
Qed.
The predicate trm_is_val t asserts that t is a value.
Consider a configuration (s,t), where t is not a value. If this
configuration cannot take any reduction step, it is said to be stuck.
Conversely, a configuration (s,t) that can take a step is said to be
reducible.
Values are not reducible.
The predicate notstuck s t asserts that either t is a value or t is
reducible.
Equivalence Between Small-Step and Omni-Big-Step
Inductive terminates : heap→trm→Prop :=
| terminates_step : ∀ s t,
(∀ s' t', step s t s' t' → terminates s' t') →
terminates s t.
| terminates_step : ∀ s t,
(∀ s' t', step s t s' t' → terminates s' t') →
terminates s t.
The judgment safe s t asserts that no execution may reach a stuck term. In
other words, for any configuration (s',t') reachable from (s,t), it is
the case that the configuration (s',t') is either a value or is reducible.
The judgment correct s t Q asserts that if the execution of (s,t)
reaches a final configuration, then this final configuration satisfies Q.
The conjunction of safe and correct corresponds to "partial
correctness". The conjunction of safe, correct and terminates
corresponds to "total correctness". The soundness theorem that we aim for
establishes that triple t H Q entails total correctness.
triple t H Q →
∀ s, H s → terminates s t ∧ safe s t ∧ correct s t Q.
To prove soundness, we introduce an inductively predicate, named seval,
which captures total correctness in small-step style. On the one hand, we
prove that seval entails safe, correct, and terminates. On the other
hand, we prove that seval is related to the omni-big-step judgment,
eval.
The judgment seval s t Q asserts that any execution of (s,t) terminates
and reaches a configuration satisfying Q. In the "base" case,
seval s v Q holds if the terminal configuration (s,v) satisfies Q. In
the "step" case, seval s t Q holds if (1) the configuration (s,t) is
reducible, and (2) if for any step that (s,t) may take to (s',t'), the
predicate seval s' t' Q holds.
triple t H Q →
∀ s, H s → terminates s t ∧ safe s t ∧ correct s t Q.
Inductive seval : heap→trm->(val→hprop)->Prop :=
| seval_val : ∀ s v Q,
Q v s →
seval s v Q
| seval_step : ∀ s t Q,
reducible s t → (* (exists s' t', step s t s' t') *)
(∀ s' t', step s t s' t' → seval s' t' Q) →
seval s t Q.
| seval_val : ∀ s v Q,
Q v s →
seval s v Q
| seval_step : ∀ s t Q,
reducible s t → (* (exists s' t', step s t s' t') *)
(∀ s' t', step s t s' t' → seval s' t' Q) →
seval s t Q.
Exercise: 2 stars, standard, optional (seval_val_inv)
As a warm-up to get some familiary with seval, prove the following inversion lemma, which asserts that, given a value v, the property seval s v Q implies Q s v.Exercise: 2 stars, standard, especially useful (seval_terminates)
Prove that seval captures termination.
Lemma seval_terminates : ∀ s t Q,
seval s t Q →
terminates s t.
Proof using. (* FILL IN HERE *) Admitted.
☐
seval s t Q →
terminates s t.
Proof using. (* FILL IN HERE *) Admitted.
☐
Exercise: 3 stars, standard, especially useful (seval_correct)
Prove that seval captures correctness.
Lemma seval_correct : ∀ s t Q,
seval s t Q →
correct s t Q.
Proof using. (* FILL IN HERE *) Admitted.
☐
seval s t Q →
correct s t Q.
Proof using. (* FILL IN HERE *) Admitted.
☐
Lemma seval_sound : ∀ s t Q,
seval s t Q →
terminates s t ∧ safe s t ∧ correct s t Q.
Proof using.
introv M. splits.
{ applys* seval_terminates. }
{ applys* seval_safe. }
{ applys* seval_correct. }
Qed.
seval s t Q →
terminates s t ∧ safe s t ∧ correct s t Q.
Proof using.
introv M. splits.
{ applys* seval_terminates. }
{ applys* seval_safe. }
{ applys* seval_correct. }
Qed.
We now establish that the omni-big-step evaluation judgment entails the
small-step-based seval judgment. The proof is carried out by induction on
the omni-big-step relation. It relies on a number of auxiliary results
establishing that, for each term construct, the seval judgment admits an
evaluation rule that mimics the omni-big-step evaluation rule. We begin with
the statements of the auxiliary lemmas.
Lemma seval_fun : ∀ s x t1 Q,
Q (val_fun x t1) s →
seval s (trm_fun x t1) Q.
Proof using.
introv M. applys seval_step.
{ do 2 esplit. constructor. }
{ introv R. inverts R. { applys seval_val. applys M. } }
Qed.
Lemma seval_fix : ∀ s f x t1 Q,
Q (val_fix f x t1) s →
seval s (trm_fix f x t1) Q.
Proof using.
introv M. applys seval_step.
{ do 2 esplit. constructor. }
{ introv R. inverts R. { applys seval_val. applys M. } }
Qed.
Lemma seval_app_fun : ∀ s x v1 v2 t1 Q,
v1 = val_fun x t1 →
seval s (subst x v2 t1) Q →
seval s (trm_app v1 v2) Q.
Proof using.
introv E M. applys seval_step.
{ do 2 esplit. applys* step_app_fun. }
{ introv R. invert R; try solve [intros; false]. introv → → → → → R. inverts E. applys M. }
Qed.
Lemma seval_app_fix : ∀ s f x v1 v2 t1 Q,
v1 = val_fix f x t1 →
seval s (subst x v2 (subst f v1 t1)) Q →
seval s (trm_app v1 v2) Q.
Proof using.
introv E M. applys seval_step.
{ do 2 esplit. applys* step_app_fix. }
{ introv R. invert R; try solve [intros; false]. introv → → → → → R. inverts E. applys M. }
Qed.
Q (val_fun x t1) s →
seval s (trm_fun x t1) Q.
Proof using.
introv M. applys seval_step.
{ do 2 esplit. constructor. }
{ introv R. inverts R. { applys seval_val. applys M. } }
Qed.
Lemma seval_fix : ∀ s f x t1 Q,
Q (val_fix f x t1) s →
seval s (trm_fix f x t1) Q.
Proof using.
introv M. applys seval_step.
{ do 2 esplit. constructor. }
{ introv R. inverts R. { applys seval_val. applys M. } }
Qed.
Lemma seval_app_fun : ∀ s x v1 v2 t1 Q,
v1 = val_fun x t1 →
seval s (subst x v2 t1) Q →
seval s (trm_app v1 v2) Q.
Proof using.
introv E M. applys seval_step.
{ do 2 esplit. applys* step_app_fun. }
{ introv R. invert R; try solve [intros; false]. introv → → → → → R. inverts E. applys M. }
Qed.
Lemma seval_app_fix : ∀ s f x v1 v2 t1 Q,
v1 = val_fix f x t1 →
seval s (subst x v2 (subst f v1 t1)) Q →
seval s (trm_app v1 v2) Q.
Proof using.
introv E M. applys seval_step.
{ do 2 esplit. applys* step_app_fix. }
{ introv R. invert R; try solve [intros; false]. introv → → → → → R. inverts E. applys M. }
Qed.
Exercise: 5 stars, standard, especially useful (seval_seq)
Prove the big-step reasoning rule for sequence for seval.
Lemma seval_seq : ∀ s t1 t2 Q1 Q,
seval s t1 Q1 →
(∀ s1 v1, Q1 v1 s1 → seval s1 t2 Q) →
seval s (trm_seq t1 t2) Q.
Proof using. (* FILL IN HERE *) Admitted.
☐
seval s t1 Q1 →
(∀ s1 v1, Q1 v1 s1 → seval s1 t2 Q) →
seval s (trm_seq t1 t2) Q.
Proof using. (* FILL IN HERE *) Admitted.
☐
Exercise: 5 stars, standard, optional (seval_let)
The proof of seval_let is the same as that of seval_seq. We also present it as an exercise to avoid giving away the solution.
Lemma seval_let : ∀ s x t1 t2 Q1 Q,
seval s t1 Q1 →
(∀ s1 v1, Q1 v1 s1 → seval s1 (subst x v1 t2) Q) →
seval s (trm_let x t1 t2) Q.
Proof using. (* FILL IN HERE *) Admitted.
☐
seval s t1 Q1 →
(∀ s1 v1, Q1 v1 s1 → seval s1 (subst x v1 t2) Q) →
seval s (trm_let x t1 t2) Q.
Proof using. (* FILL IN HERE *) Admitted.
☐
Lemma seval_if : ∀ s b t1 t2 Q,
seval s (if b then t1 else t2) Q →
seval s (trm_if b t1 t2) Q.
Proof using.
introv M. applys seval_step.
{ do 2 esplit. constructors*. }
{ introv R. inverts R; tryfalse. { applys M. } }
Qed.
seval s (if b then t1 else t2) Q →
seval s (trm_if b t1 t2) Q.
Proof using.
introv M. applys seval_step.
{ do 2 esplit. constructors*. }
{ introv R. inverts R; tryfalse. { applys M. } }
Qed.
We are now ready to prove by induction that the omni-big-step judgment
entails the small-step-based judgment seval. Note that the reciprocal
entailment also holds, however it is not needed for establishing soundness.
Lemma seval_of_eval : ∀ s t Q,
eval s t Q →
seval s t Q.
Proof using.
introv M. induction M.
{ applys* seval_val. }
{ applys* seval_fun. }
{ applys* seval_fix. }
{ applys* seval_app_fun. }
{ applys* seval_app_fix. }
{ applys* seval_seq. }
{ applys* seval_let. }
{ applys* seval_if. }
{ applys seval_step.
{ do 2 esplit. applys* step_add. }
{ introv K. inverts K. applys* seval_val. } }
{ applys seval_step.
{ do 2 esplit. applys* step_div. }
{ introv K. inverts K. applys* seval_val. } }
{ applys seval_step.
{ do 2 esplit. applys* step_rand 0. math. }
{ introv K. inverts K; tryfalse. applys* seval_val. } }
{ applys seval_step.
{ forwards¬(p&D&N): (exists_fresh null s).
do 2 esplit. applys* step_ref. }
{ introv K. inverts K; tryfalse. applys* seval_val. } }
{ applys seval_step.
{ do 2 esplit. applys* step_get. }
{ introv K. inverts K; tryfalse. applys* seval_val. } }
{ applys seval_step.
{ do 2 esplit. applys* step_set. }
{ introv K. inverts K; tryfalse. applys* seval_val. } }
{ applys seval_step.
{ do 2 esplit. applys* step_free. }
{ introv K. inverts K; tryfalse. applys* seval_val. } }
Qed.
eval s t Q →
seval s t Q.
Proof using.
introv M. induction M.
{ applys* seval_val. }
{ applys* seval_fun. }
{ applys* seval_fix. }
{ applys* seval_app_fun. }
{ applys* seval_app_fix. }
{ applys* seval_seq. }
{ applys* seval_let. }
{ applys* seval_if. }
{ applys seval_step.
{ do 2 esplit. applys* step_add. }
{ introv K. inverts K. applys* seval_val. } }
{ applys seval_step.
{ do 2 esplit. applys* step_div. }
{ introv K. inverts K. applys* seval_val. } }
{ applys seval_step.
{ do 2 esplit. applys* step_rand 0. math. }
{ introv K. inverts K; tryfalse. applys* seval_val. } }
{ applys seval_step.
{ forwards¬(p&D&N): (exists_fresh null s).
do 2 esplit. applys* step_ref. }
{ introv K. inverts K; tryfalse. applys* seval_val. } }
{ applys seval_step.
{ do 2 esplit. applys* step_get. }
{ introv K. inverts K; tryfalse. applys* seval_val. } }
{ applys seval_step.
{ do 2 esplit. applys* step_set. }
{ introv K. inverts K; tryfalse. applys* seval_val. } }
{ applys seval_step.
{ do 2 esplit. applys* step_free. }
{ introv K. inverts K; tryfalse. applys* seval_val. } }
Qed.
Putting it all together, we conclude on the soundness of Separation Logic
with respect to the small-step semantics defined by the relation step.
Lemma soundness_small_step : ∀ t H Q,
triple t H Q →
∀ s, H s → terminates s t ∧ safe s t ∧ correct s t Q.
Proof using.
unfold triple. introv M Hs. specializes M Hs.
lets R: seval_of_eval M. applys seval_sound R.
Qed.
triple t H Q →
∀ s, H s → terminates s t ∧ safe s t ∧ correct s t Q.
Proof using.
unfold triple. introv M Hs. specializes M Hs.
lets R: seval_of_eval M. applys seval_sound R.
Qed.
Small-Step-Based Triples
The first step is to check that this new definition of triple is sound. This
property has been essentially been established in the previous section via
the lemma seval_sound.
Lemma striple_sound : ∀ t H Q,
striple t H Q →
∀ s, H s → terminates s t ∧ safe s t ∧ correct s t Q.
Proof using. introv M Hs. specializes M Hs. applys seval_sound M. Qed.
striple t H Q →
∀ s, H s → terminates s t ∧ safe s t ∧ correct s t Q.
Proof using. introv M Hs. specializes M Hs. applys seval_sound M. Qed.
The second step is to establish reasoning rules for striple. Let us begin
with the consequence rule and the frame rule. For those, we need to
establish the consequence and the frame property directly for seval. The
proofs differ from the corresponding proofs on eval.
The consequence rule for seval is established by induction.
Lemma seval_conseq : ∀ s t Q Q',
seval s t Q' →
Q' ===> Q →
seval s t Q.
Proof using.
introv M WQ. induction M.
{ applys seval_val. applys* WQ. }
{ rename H1 into IH.
applys seval_step.
{ auto. }
{ introv HR. applys* IH. } }
Qed.
seval s t Q' →
Q' ===> Q →
seval s t Q.
Proof using.
introv M WQ. induction M.
{ applys seval_val. applys* WQ. }
{ rename H1 into IH.
applys seval_step.
{ auto. }
{ introv HR. applys* IH. } }
Qed.
The consequence rule follows directly.
Lemma striple_conseq : ∀ t H' Q' H Q,
striple t H' Q' →
H ==> H' →
Q' ===> Q →
striple t H Q.
Proof using.
introv M MH MQ HF. applys seval_conseq M MQ. applys* MH.
Qed.
striple t H' Q' →
H ==> H' →
Q' ===> Q →
striple t H Q.
Proof using.
introv M MH MQ HF. applys seval_conseq M MQ. applys* MH.
Qed.
The frame property for seval is also established by induction, however the
proof is more involved.
Section stepsFrame.
Hint Constructors step.
Lemma seval_frame : ∀ h1 h2 t Q,
seval h1 t Q →
Fmap.disjoint h1 h2 →
seval (h1 \u h2) t (Q \*+ (= h2)).
Proof using.
introv M HD. gen h2. induction M; intros.
{ applys seval_val. applys* hstar_intro. }
{ rename H into M1, H0 into M2, H1 into IH2.
applys seval_step.
{ unfolds reducible. clear M2 IH2. destruct M1 as (s'&t'&R).
induction R; tryfalse; try solve [ do 2 esplit; constructors* ].
{ forwards* (s'&t'&R'): IHR. }
{ forwards* (s'&t'&R'): IHR. }
{ lets (p'&F&_): exists_fresh null (Fmap.union s h2). do 2 esplit.
applys step_ref v p'. eauto. }
{ do 2 esplit. applys step_get. applys* Fmap.indom_union_l. }
{ do 2 esplit. applys step_set. applys* Fmap.indom_union_l. }
{ do 2 esplit. applys step_free. applys* Fmap.indom_union_l. } }
{ introv R. cuts (s1'&E'&D'&R'):
(∃ s1', s' = s1' \u h2 ∧ Fmap.disjoint s1' h2 ∧ step s t s1' t').
{ subst. applys* IH2. }
clear M2 IH2.
gen_eq su: (s \u h2). gen s.
unfolds reducible. induction R; intros; subst; eauto.
{ destruct M1 as (s0&t0&R0).
rename R into R1. forwards* (s1'&E&D&R1'): IHR s.
{ inverts R0. { eauto. } { inverts R1. } } }
{ destruct M1 as (s0&t0&R0).
rename R into R1. forwards* (s1'&E&D&R1'): IHR s.
{ inverts R0. { eauto. } { inverts R1. } } }
{ rename H into D. rewrite Fmap.indom_union_eq in D. rew_logic in D.
destruct D as (D1&D2). esplit. splits.
{ rewrite* Fmap.update_union_not_r. }
{ applys* Fmap.disjoint_update_not_r. }
{ eauto. } }
{ destruct M1 as (se&te&Re). inverts Re; tryfalse.
rewrite* Fmap.read_union_l. }
{ destruct M1 as (se&te&Re). inverts Re; tryfalse. esplit. splits.
{ rewrite* Fmap.update_union_l. }
{ applys* Fmap.disjoint_update_l. }
{ eauto. } }
{ destruct M1 as (se&te&Re). inverts Re; tryfalse. esplit. splits.
{ rewrite* remove_disjoint_union_l. }
{ applys* Fmap.disjoint_remove_l. }
{ eauto. } } } }
Qed.
End stepsFrame.
Hint Constructors step.
Lemma seval_frame : ∀ h1 h2 t Q,
seval h1 t Q →
Fmap.disjoint h1 h2 →
seval (h1 \u h2) t (Q \*+ (= h2)).
Proof using.
introv M HD. gen h2. induction M; intros.
{ applys seval_val. applys* hstar_intro. }
{ rename H into M1, H0 into M2, H1 into IH2.
applys seval_step.
{ unfolds reducible. clear M2 IH2. destruct M1 as (s'&t'&R).
induction R; tryfalse; try solve [ do 2 esplit; constructors* ].
{ forwards* (s'&t'&R'): IHR. }
{ forwards* (s'&t'&R'): IHR. }
{ lets (p'&F&_): exists_fresh null (Fmap.union s h2). do 2 esplit.
applys step_ref v p'. eauto. }
{ do 2 esplit. applys step_get. applys* Fmap.indom_union_l. }
{ do 2 esplit. applys step_set. applys* Fmap.indom_union_l. }
{ do 2 esplit. applys step_free. applys* Fmap.indom_union_l. } }
{ introv R. cuts (s1'&E'&D'&R'):
(∃ s1', s' = s1' \u h2 ∧ Fmap.disjoint s1' h2 ∧ step s t s1' t').
{ subst. applys* IH2. }
clear M2 IH2.
gen_eq su: (s \u h2). gen s.
unfolds reducible. induction R; intros; subst; eauto.
{ destruct M1 as (s0&t0&R0).
rename R into R1. forwards* (s1'&E&D&R1'): IHR s.
{ inverts R0. { eauto. } { inverts R1. } } }
{ destruct M1 as (s0&t0&R0).
rename R into R1. forwards* (s1'&E&D&R1'): IHR s.
{ inverts R0. { eauto. } { inverts R1. } } }
{ rename H into D. rewrite Fmap.indom_union_eq in D. rew_logic in D.
destruct D as (D1&D2). esplit. splits.
{ rewrite* Fmap.update_union_not_r. }
{ applys* Fmap.disjoint_update_not_r. }
{ eauto. } }
{ destruct M1 as (se&te&Re). inverts Re; tryfalse.
rewrite* Fmap.read_union_l. }
{ destruct M1 as (se&te&Re). inverts Re; tryfalse. esplit. splits.
{ rewrite* Fmap.update_union_l. }
{ applys* Fmap.disjoint_update_l. }
{ eauto. } }
{ destruct M1 as (se&te&Re). inverts Re; tryfalse. esplit. splits.
{ rewrite* remove_disjoint_union_l. }
{ applys* Fmap.disjoint_remove_l. }
{ eauto. } } } }
Qed.
End stepsFrame.
The frame rule for striple then follows from the frame and the consequence
properties for seval.
Lemma striple_frame : ∀ t H Q H',
striple t H Q →
striple t (H \* H') (Q \*+ H').
Proof.
introv M. intros h HF. lets (h1&h2&M1&M2&MD&MU): hstar_inv (rm HF).
subst. specializes M M1. applys seval_conseq.
{ applys seval_frame M MD. } { xsimpl. intros h' →. applys M2. }
Qed.
striple t H Q →
striple t (H \* H') (Q \*+ H').
Proof.
introv M. intros h HF. lets (h1&h2&M1&M2&MD&MU): hstar_inv (rm HF).
subst. specializes M M1. applys seval_conseq.
{ applys seval_frame M MD. } { xsimpl. intros h' →. applys M2. }
Qed.
To establish other reasoning rules for striple, one need to prove
big-step-style evaluation rules for seval, such as the lemma seval_let
stated earlier.
In summary, it is possible to set up a Separation Logic directly based on
the small-step-based judgment seval, without going through a definition of
the omni-big-step judgment eval. However, most of the proof effort
remains, as one needs to establish the frame property and to derive
big-step-style evaluation rules such as seval_let.
Consider a programming language that is deterministic except for the
allocation operation, which may pick any fresh location. For such languages,
it is possible to define Separation Logic triples in terms of the standard
big-step semantics, using a technique known as the "baked-in frame rule".
Recall the big-step judgment.
First, we define a (total correctness) Hoare triple, written hoare t H Q.
This judgment asserts that, starting from a state s satisfying the
precondition H, the term t evaluates to a value v and to a state s'
that, together, satisfy the postcondition Q. Formally:
Definition hoare (t:trm) (H:hprop) (Q:val→hprop) : Prop :=
∀ (s:heap), H s →
∃ (s':heap) (v:val), eval s t s' v ∧ Q v s'.
∀ (s:heap), H s →
∃ (s':heap) (v:val), eval s t s' v ∧ Q v s'.
Exercise: 2 stars, standard, especially useful (hoare_conseq)
To gain familiarity with the hoare judgment, prove the consequence rule for Hoare triples.
Lemma hoare_conseq : ∀ t H Q H' Q',
hoare t H' Q' →
H ==> H' →
Q' ===> Q →
hoare t H Q.
Proof using. (* FILL IN HERE *) Admitted.
☐
hoare t H' Q' →
H ==> H' →
Q' ===> Q →
hoare t H Q.
Proof using. (* FILL IN HERE *) Admitted.
☐
Definition btriple (t:trm) (H:hprop) (Q:val→hprop) : Prop :=
∀ (H':hprop), hoare t (H \* H') (Q \*+ H').
∀ (H':hprop), hoare t (H \* H') (Q \*+ H').
This definition inherently satisfies the frame rule, as we show below. The
proof essentially exploits the associativity of the star operator.
Exercise: 3 stars, standard, especially useful (btriple_frame)
Prove that btriple t H Q is a judgment satisfying the frame rule.
Lemma btriple_frame : ∀ t H Q H',
btriple t H Q →
btriple t (H \* H') (Q \*+ H').
Proof using. (* FILL IN HERE *) Admitted.
☐
btriple t H Q →
btriple t (H \* H') (Q \*+ H').
Proof using. (* FILL IN HERE *) Admitted.
☐
Definition btriple_lowlevel (t:trm) (H:hprop) (Q:val→hprop) : Prop :=
∀ h1 h2,
Fmap.disjoint h1 h2 →
H h1 →
∃ h1' v,
Fmap.disjoint h1' h2
∧ eval (h1 \u h2) t (h1' \u h2) v
∧ Q v h1'.
∀ h1 h2,
Fmap.disjoint h1 h2 →
H h1 →
∃ h1' v,
Fmap.disjoint h1' h2
∧ eval (h1 \u h2) t (h1' \u h2) v
∧ Q v h1'.
Let us establish the equivalence between this alternative definition of
triple and the original one.
Lemma btriple_iff_btriple_lowlevel : ∀ t H Q,
btriple t H Q ↔ btriple_lowlevel t H Q.
Proof using.
unfold triple, btriple_lowlevel, hoare. iff M.
{ introv D P1.
forwards (h'&v&HR&HQ): M (=h2) (h1 \u h2). { applys* hstar_intro. }
destruct HQ as (h1'&h2'&N0&N1&N2&N3). subst.
∃ h1' v. auto. }
{ intros H' h. introv (h1&h2&N1&N2&D&U).
forwards (h1'&v&D'&HR&HQ): M h1 h2; auto. subst.
∃ (h1' \u h2) v. split. { eauto. } { applys* hstar_intro. } }
Qed.
btriple t H Q ↔ btriple_lowlevel t H Q.
Proof using.
unfold triple, btriple_lowlevel, hoare. iff M.
{ introv D P1.
forwards (h'&v&HR&HQ): M (=h2) (h1 \u h2). { applys* hstar_intro. }
destruct HQ as (h1'&h2'&N0&N1&N2&N3). subst.
∃ h1' v. auto. }
{ intros H' h. introv (h1&h2&N1&N2&D&U).
forwards (h1'&v&D'&HR&HQ): M h1 h2; auto. subst.
∃ (h1' \u h2) v. split. { eauto. } { applys* hstar_intro. } }
Qed.
The low-level definition of triple leveraging the baked-in frame rule may be
convenient for formalizing certain extensions of Separation Logic.
Historical Notes
(* 2024-08-25 14:53 *)