Acknowledgements
This material is taken verbatim from Software Foundations V4.0
Copyright (c) 2016
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Assumed Knowledge:
* TODO
Learning Outcomes:
* TODO
Some general advice for working on exercises:
- Most of the Coq proofs we ask you to do are similar to proofs
that we've provided. Before starting to work on exercises
problems, take the time to work through our proofs (both
informally, on paper, and in Coq) and make sure you understand
them in detail. This will save you a lot of time.
- The Coq proofs we're doing now are sufficiently complicated that
it is more or less impossible to complete them simply by random
experimentation or "following your nose." You need to start
with an idea about why the property is true and how the proof is
going to go. The best way to do this is to write out at least a
sketch of an informal proof on paper — one that intuitively
convinces you of the truth of the theorem — before starting to
work on the formal one. Alternately, grab a friend and try to
convince them that the theorem is true; then try to formalize
your explanation.
- Use automation to save work! Some of the proofs in this
chapter's exercises are pretty long if you try to write out all
the cases explicitly.
Behavioral Equivalence
In an earlier chapter, we investigated the correctness of a very
simple program transformation: the
optimize_0plus function. The
programming language we were considering was the first version of
the language of arithmetic expressions — with no variables — so
in that setting it was very easy to define what it means for a
program transformation to be correct: it should always yield a
program that evaluates to the same number as the original.
To go further and talk about the correctness of program
transformations in the full Imp language, we need to consider the
role of variables and state.
Definitions
For
aexps and
bexps with variables, the definition we want is
clear. We say
that two
aexps or
bexps are
behaviorally equivalent if they
evaluate to the same result
in every state.
For commands, the situation is a little more subtle. We can't
simply say "two commands are behaviorally equivalent if they
evaluate to the same ending state whenever they are started in the
same initial state," because some commands, when run in some
starting states, don't terminate in any final state at all! What
we need instead is this: two commands are behaviorally equivalent
if, for any given starting state, they either both diverge or both
terminate in the same final state. A compact way to express this
is "if the first one terminates in a particular state then so does
the second, and vice versa."
Exercise: 2 stars (equiv_classes)
Given the following programs, group together those that are
equivalent in Imp. Your answer should be given as a list of lists,
where each sub-list represents a group of equivalent programs. For
example, if you think programs (a) through (h) are all equivalent
to each other, but not to (i), your answer should look like this:
[ [
prog_a;
prog_b;
prog_c;
prog_d;
prog_e;
prog_f;
prog_g;
prog_h] ;
[
prog_i] ]
Write down your answer below in the definition of
equiv_classes.
☐
Examples
Here are some simple examples of equivalences of arithmetic
and boolean expressions.
For examples of command equivalence, let's start by looking at
some trivial program transformations involving SKIP:
Theorem skip_left:
∀c,
cequiv
(
SKIP;;
c)
c.
Proof.
intros c st st'.
split;
intros H.
-
inversion H.
subst.
inversion H2.
subst.
assumption.
-
apply E_Seq with st.
apply E_Skip.
assumption.
Qed.
Exercise: 2 stars (skip_right)
Prove that adding a
SKIP after a command results in an
equivalent program
☐
Similarly, here is a simple transformations that optimizes
IFB
commands:
Of course, few programmers would be tempted to write a conditional
whose guard is literally
BTrue. A more interesting case is when
the guard is
equivalent to true:
Theorem: If
b is equivalent to
BTrue, then
IFB b THEN c1
ELSE c2 FI is equivalent to
c1.
Proof:
- (→) We must show, for all st and st', that if IFB b
THEN c1 ELSE c2 FI / st ⇓ st' then c1 / st ⇓ st'.
Proceed by cases on the rules that could possibly have been
used to show IFB b THEN c1 ELSE c2 FI / st ⇓ st', namely
E_IfTrue and E_IfFalse.
- Suppose the final rule rule in the derivation of IFB b THEN
c1 ELSE c2 FI / st ⇓ st' was E_IfTrue. We then have, by
the premises of E_IfTrue, that c1 / st ⇓ st'. This is
exactly what we set out to prove.
- On the other hand, suppose the final rule in the derivation
of IFB b THEN c1 ELSE c2 FI / st ⇓ st' was E_IfFalse.
We then know that beval st b = false and c2 / st ⇓ st'.
Recall that b is equivalent to BTrue, i.e., forall st,
beval st b = beval st BTrue. In particular, this means
that beval st b = true, since beval st BTrue = true. But
this is a contradiction, since E_IfFalse requires that
beval st b = false. Thus, the final rule could not have
been E_IfFalse.
- (←) We must show, for all st and st', that if c1 / st
⇓ st' then IFB b THEN c1 ELSE c2 FI / st ⇓ st'.
Since b is equivalent to BTrue, we know that beval st b =
beval st BTrue = true. Together with the assumption that
c1 / st ⇓ st', we can apply E_IfTrue to derive IFB b THEN
c1 ELSE c2 FI / st ⇓ st'. ☐
Here is the formal version of this proof:
Theorem IFB_true:
∀b c1 c2,
bequiv b BTrue →
cequiv
(
IFB b THEN c1 ELSE c2 FI)
c1.
Proof.
intros b c1 c2 Hb.
split;
intros H.
-
inversion H;
subst.
+
assumption.
+
unfold bequiv in Hb.
simpl in Hb.
rewrite Hb in H5.
inversion H5.
-
apply E_IfTrue;
try assumption.
unfold bequiv in Hb.
simpl in Hb.
rewrite Hb.
reflexivity.
Qed.
Exercise: 2 stars, recommended (IFB_false)
☐
Exercise: 3 stars (swap_if_branches)
Show that we can swap the branches of an IF by negating its
condition
☐
For
WHILE loops, we can give a similar pair of theorems. A loop
whose guard is equivalent to
BFalse is equivalent to
SKIP,
while a loop whose guard is equivalent to
BTrue is equivalent to
WHILE BTrue DO SKIP END (or any other non-terminating program).
The first of these facts is easy.
Exercise: 2 stars, advanced, optional (WHILE_false_informal)
Write an informal proof of
WHILE_false.
☐
To prove the second fact, we need an auxiliary lemma stating that
WHILE loops whose guards are equivalent to
BTrue never
terminate:
Lemma: If
b is equivalent to
BTrue, then it cannot be the
case that
(WHILE b DO c END) / st ⇓ st'.
Proof: Suppose that
(WHILE b DO c END) / st ⇓ st'. We show,
by induction on a derivation of
(WHILE b DO c END) / st ⇓ st',
that this assumption leads to a contradiction.
- Suppose (WHILE b DO c END) / st ⇓ st' is proved using rule
E_WhileEnd. Then by assumption beval st b = false. But
this contradicts the assumption that b is equivalent to
BTrue.
- Suppose (WHILE b DO c END) / st ⇓ st' is proved using rule
E_WhileLoop. Then we are given the induction hypothesis
that (WHILE b DO c END) / st ⇓ st' is contradictory, which
is exactly what we are trying to prove!
- Since these are the only rules that could have been used to
prove (WHILE b DO c END) / st ⇓ st', the other cases of
the induction are immediately contradictory. ☐
Lemma WHILE_true_nonterm :
∀b c st st',
bequiv b BTrue →
~( (
WHILE b DO c END) /
st ⇓ st' ).
Proof.
intros b c st st' Hb.
intros H.
remember (
WHILE b DO c END)
as cw eqn:
Heqcw.
induction H;
inversion Heqcw;
subst;
clear Heqcw.
-
unfold bequiv in Hb.
rewrite Hb in H.
inversion H.
-
apply IHceval2.
reflexivity.
Qed.
Exercise: 2 stars, optional (WHILE_true_nonterm_informal)
Explain what the lemma
WHILE_true_nonterm means in English.
☐
Exercise: 2 stars, recommended (WHILE_true)
Prove the following theorem.
Hint: You'll want to use
WHILE_true_nonterm here.
☐
Theorem loop_unrolling:
∀b c,
cequiv
(
WHILE b DO c END)
(
IFB b THEN (
c;;
WHILE b DO c END)
ELSE SKIP FI).
Proof.
intros b c st st'.
split;
intros Hce.
-
inversion Hce;
subst.
+
apply E_IfFalse.
assumption.
apply E_Skip.
+
apply E_IfTrue.
assumption.
apply E_Seq with (
st' :=
st'0).
assumption.
assumption.
-
inversion Hce;
subst.
+
inversion H5;
subst.
apply E_WhileLoop with (
st' :=
st'0).
assumption.
assumption.
assumption.
+
inversion H5;
subst.
apply E_WhileEnd.
assumption.
Qed.
Exercise: 2 stars, optional (seq_assoc)
☐
Proving program properties involving assignments is one place
where the functional-extensionality axiom introduced in the
Logic chapter often comes in handy. Here are an example and an
exercise.
Exercise: 2 stars, recommended (assign_aequiv)
☐
Properties of Behavioral Equivalence
We now turn to developing some of the properties of the program
equivalences we have defined.
Behavioral Equivalence Is an Equivalence
First, we verify that the equivalences on
aexps,
bexps, and
coms really are
equivalences — i.e., that they are reflexive,
symmetric, and transitive. The proofs are all easy.
Lemma refl_aequiv :
∀(
a :
aexp),
aequiv a a.
Proof.
intros a st. reflexivity. Qed.
Lemma sym_aequiv :
∀(
a1 a2 :
aexp),
aequiv a1 a2 → aequiv a2 a1.
Proof.
intros a1 a2 H. intros st. symmetry. apply H. Qed.
Lemma trans_aequiv :
∀(
a1 a2 a3 :
aexp),
aequiv a1 a2 → aequiv a2 a3 → aequiv a1 a3.
Proof.
unfold aequiv.
intros a1 a2 a3 H12 H23 st.
rewrite (
H12 st).
rewrite (
H23 st).
reflexivity.
Qed.
Lemma refl_bequiv :
∀(
b :
bexp),
bequiv b b.
Proof.
unfold bequiv.
intros b st.
reflexivity.
Qed.
Lemma sym_bequiv :
∀(
b1 b2 :
bexp),
bequiv b1 b2 → bequiv b2 b1.
Proof.
unfold bequiv.
intros b1 b2 H.
intros st.
symmetry.
apply H.
Qed.
Lemma trans_bequiv :
∀(
b1 b2 b3 :
bexp),
bequiv b1 b2 → bequiv b2 b3 → bequiv b1 b3.
Proof.
unfold bequiv.
intros b1 b2 b3 H12 H23 st.
rewrite (
H12 st).
rewrite (
H23 st).
reflexivity.
Qed.
Lemma refl_cequiv :
∀(
c :
com),
cequiv c c.
Lemma sym_cequiv :
∀(
c1 c2 :
com),
cequiv c1 c2 → cequiv c2 c1.
Proof.
unfold cequiv.
intros c1 c2 H st st'.
assert (
c1 /
st ⇓ st' ↔ c2 /
st ⇓ st')
as H'.
{
apply H. }
apply iff_sym.
assumption.
Qed.
Lemma iff_trans :
∀(
P1 P2 P3 :
Prop),
(
P1 ↔ P2)
→ (
P2 ↔ P3)
→ (
P1 ↔ P3).
Proof.
intros P1 P2 P3 H12 H23.
inversion H12. inversion H23.
split; intros A.
apply H1. apply H. apply A.
apply H0. apply H2. apply A. Qed.
Lemma trans_cequiv :
∀(
c1 c2 c3 :
com),
cequiv c1 c2 → cequiv c2 c3 → cequiv c1 c3.
Proof.
unfold cequiv.
intros c1 c2 c3 H12 H23 st st'.
apply iff_trans with (
c2 /
st ⇓ st').
apply H12.
apply H23.
Qed.
Behavioral Equivalence Is a Congruence
Less obviously, behavioral equivalence is also a
congruence.
That is, the equivalence of two subprograms implies the
equivalence of the larger programs in which they are embedded:
aequiv a1 a1' |
|
|
cequiv (i ::= a1) (i ::= a1') |
|
cequiv c1 c1' |
|
cequiv c2 c2' |
|
|
cequiv (c1;;c2) (c1';;c2') |
|
...and so on for the other forms of commands.
(Note that we are using the inference rule notation here not as
part of a definition, but simply to write down some valid
implications in a readable format. We prove these implications
below.)
We will see a concrete example of why these congruence
properties are important in the following section (in the proof of
fold_constants_com_sound), but the main idea is that they allow
us to replace a small part of a large program with an equivalent
small part and know that the whole large programs are equivalent
without doing an explicit proof about the non-varying parts —
i.e., the "proof burden" of a small change to a large program is
proportional to the size of the change, not the program.
The congruence property for loops is a little more interesting,
since it requires induction.
Theorem: Equivalence is a congruence for
WHILE — that is, if
b1 is equivalent to
b1' and
c1 is equivalent to
c1', then
WHILE b1 DO c1 END is equivalent to
WHILE b1' DO c1' END.
Proof: Suppose
b1 is equivalent to
b1' and
c1 is
equivalent to
c1'. We must show, for every
st and
st', that
WHILE b1 DO c1 END / st ⇓ st' iff
WHILE b1' DO c1' END / st
⇓ st'. We consider the two directions separately.
- (→) We show that WHILE b1 DO c1 END / st ⇓ st' implies
WHILE b1' DO c1' END / st ⇓ st', by induction on a
derivation of WHILE b1 DO c1 END / st ⇓ st'. The only
nontrivial cases are when the final rule in the derivation is
E_WhileEnd or E_WhileLoop.
- E_WhileEnd: In this case, the form of the rule gives us
beval st b1 = false and st = st'. But then, since
b1 and b1' are equivalent, we have beval st b1' =
false, and E-WhileEnd applies, giving us WHILE b1' DO
c1' END / st ⇓ st', as required.
- E_WhileLoop: The form of the rule now gives us beval st
b1 = true, with c1 / st ⇓ st'0 and WHILE b1 DO c1
END / st'0 ⇓ st' for some state st'0, with the
induction hypothesis WHILE b1' DO c1' END / st'0 ⇓
st'.
Since c1 and c1' are equivalent, we know that c1' /
st ⇓ st'0. And since b1 and b1' are equivalent, we
have beval st b1' = true. Now E-WhileLoop applies,
giving us WHILE b1' DO c1' END / st ⇓ st', as
required.
- (←) Similar. ☐
Theorem CWhile_congruence :
∀b1 b1' c1 c1',
bequiv b1 b1' → cequiv c1 c1' →
cequiv (
WHILE b1 DO c1 END) (
WHILE b1' DO c1' END).
Proof.
unfold bequiv,
cequiv.
intros b1 b1' c1 c1' Hb1e Hc1e st st'.
split;
intros Hce.
-
remember (
WHILE b1 DO c1 END)
as cwhile
eqn:
Heqcwhile.
induction Hce;
inversion Heqcwhile;
subst.
+
apply E_WhileEnd.
rewrite ← Hb1e.
apply H.
+
apply E_WhileLoop with (
st' :=
st').
*
rewrite ← Hb1e.
apply H.
*
apply (
Hc1e st st').
apply Hce1.
*
apply IHHce2.
reflexivity.
-
remember (
WHILE b1' DO c1' END)
as c'while
eqn:
Heqc'while.
induction Hce;
inversion Heqc'while;
subst.
+
apply E_WhileEnd.
rewrite → Hb1e.
apply H.
+
apply E_WhileLoop with (
st' :=
st').
*
rewrite → Hb1e.
apply H.
*
apply (
Hc1e st st').
apply Hce1.
*
apply IHHce2.
reflexivity.
Qed.
Exercise: 3 stars, optional (CSeq_congruence)
☐
Exercise: 3 stars (CIf_congruence)
☐
For example, here are two equivalent programs and a proof of their
equivalence...
Program Transformations
A
program transformation is a function that takes a program as
input and produces some variant of the program as output.
Compiler optimizations such as constant folding are a canonical
example, but there are many others.
A program transformation is
sound if it preserves the
behavior of the original program.
The Constant-Folding Transformation
An expression is
constant when it contains no variable
references.
Constant folding is an optimization that finds constant
expressions and replaces them by their values.
Note that this version of constant folding doesn't eliminate
trivial additions, etc. — we are focusing attention on a single
optimization for the sake of simplicity. It is not hard to
incorporate other ways of simplifying expressions; the definitions
and proofs just get longer.
Not only can we lift fold_constants_aexp to bexps (in the
BEq and BLe cases), we can also find constant boolean
expressions and evaluate them in-place.
Fixpoint fold_constants_bexp (
b :
bexp) :
bexp :=
match b with
|
BTrue ⇒
BTrue
|
BFalse ⇒
BFalse
|
BEq a1 a2 ⇒
match (
fold_constants_aexp a1,
fold_constants_aexp a2)
with
| (
ANum n1,
ANum n2) ⇒
if beq_nat n1 n2 then BTrue else BFalse
| (
a1',
a2') ⇒
BEq a1' a2'
end
|
BLe a1 a2 ⇒
match (
fold_constants_aexp a1,
fold_constants_aexp a2)
with
| (
ANum n1,
ANum n2) ⇒
if leb n1 n2 then BTrue else BFalse
| (
a1',
a2') ⇒
BLe a1' a2'
end
|
BNot b1 ⇒
match (
fold_constants_bexp b1)
with
|
BTrue ⇒
BFalse
|
BFalse ⇒
BTrue
|
b1' ⇒
BNot b1'
end
|
BAnd b1 b2 ⇒
match (
fold_constants_bexp b1,
fold_constants_bexp b2)
with
| (
BTrue,
BTrue) ⇒
BTrue
| (
BTrue,
BFalse) ⇒
BFalse
| (
BFalse,
BTrue) ⇒
BFalse
| (
BFalse,
BFalse) ⇒
BFalse
| (
b1',
b2') ⇒
BAnd b1' b2'
end
end.
Example fold_bexp_ex1 :
fold_constants_bexp (
BAnd BTrue (
BNot (
BAnd BFalse BTrue)))
=
BTrue.
Proof. reflexivity. Qed.
Example fold_bexp_ex2 :
fold_constants_bexp
(
BAnd (
BEq (
AId X) (
AId Y))
(
BEq (
ANum 0)
(
AMinus (
ANum 2) (
APlus (
ANum 1)
(
ANum 1)))))
=
BAnd (
BEq (
AId X) (
AId Y))
BTrue.
Proof. reflexivity. Qed.
To fold constants in a command, we apply the appropriate folding
functions on all embedded expressions.
Soundness of Constant Folding
Now we need to show that what we've done is correct.
Here's the proof for arithmetic expressions:
Exercise: 3 stars, optional (fold_bexp_Eq_informal)
Here is an informal proof of the
BEq case of the soundness
argument for boolean expression constant folding. Read it
carefully and compare it to the formal proof that follows. Then
fill in the
BLe case of the formal proof (without looking at the
BEq case, if possible).
Theorem: The constant folding function for booleans,
fold_constants_bexp, is sound.
Proof: We must show that
b is equivalent to
fold_constants_bexp,
for all boolean expressions
b. Proceed by induction on
b. We
show just the case where
b has the form
BEq a1 a2.
In this case, we must show
beval st (
BEq a1 a2)
=
beval st (
fold_constants_bexp (
BEq a1 a2)).
There are two cases to consider:
- First, suppose fold_constants_aexp a1 = ANum n1 and
fold_constants_aexp a2 = ANum n2 for some n1 and n2.
In this case, we have
fold_constants_bexp (
BEq a1 a2)
=
if beq_nat n1 n2 then BTrue else BFalse
and
beval st (
BEq a1 a2)
=
beq_nat (
aeval st a1) (
aeval st a2).
By the soundness of constant folding for arithmetic
expressions (Lemma fold_constants_aexp_sound), we know
aeval st a1
=
aeval st (
fold_constants_aexp a1)
=
aeval st (
ANum n1)
=
n1
and
aeval st a2
=
aeval st (
fold_constants_aexp a2)
=
aeval st (
ANum n2)
=
n2,
so
beval st (
BEq a1 a2)
=
beq_nat (
aeval a1) (
aeval a2)
=
beq_nat n1 n2.
Also, it is easy to see (by considering the cases n1 = n2 and
n1 ≠ n2 separately) that
beval st (
if beq_nat n1 n2 then BTrue else BFalse)
=
if beq_nat n1 n2 then beval st BTrue else beval st BFalse
=
if beq_nat n1 n2 then true else false
=
beq_nat n1 n2.
So
beval st (
BEq a1 a2)
=
beq_nat n1 n2.
=
beval st (
if beq_nat n1 n2 then BTrue else BFalse),
as required.
- Otherwise, one of fold_constants_aexp a1 and
fold_constants_aexp a2 is not a constant. In this case, we
must show
beval st (
BEq a1 a2)
=
beval st (
BEq (
fold_constants_aexp a1)
(
fold_constants_aexp a2)),
which, by the definition of beval, is the same as showing
beq_nat (
aeval st a1) (
aeval st a2)
=
beq_nat (
aeval st (
fold_constants_aexp a1))
(
aeval st (
fold_constants_aexp a2)).
But the soundness of constant folding for arithmetic
expressions (fold_constants_aexp_sound) gives us
aeval st a1 =
aeval st (
fold_constants_aexp a1)
aeval st a2 =
aeval st (
fold_constants_aexp a2),
completing the case. ☐
(Doing induction when there are a lot of constructors makes
specifying variable names a chore, but Coq doesn't always
choose nice variable names. We can rename entries in the
context with the rename tactic: rename a into a1 will
change a to a1 in the current goal and context.)
The only interesting case is when both a1 and a2
become constants after folding
simpl.
destruct (
beq_nat n n0);
reflexivity.
-
admit.
-
simpl.
remember (
fold_constants_bexp b)
as b' eqn:
Heqb'.
rewrite IHb.
destruct b';
reflexivity.
-
simpl.
remember (
fold_constants_bexp b1)
as b1' eqn:
Heqb1'.
remember (
fold_constants_bexp b2)
as b2' eqn:
Heqb2'.
rewrite IHb1.
rewrite IHb2.
destruct b1';
destruct b2';
reflexivity.
Admitted.
☐
Exercise: 3 stars (fold_constants_com_sound)
Complete the
WHILE case of the following proof.
(If the optimization doesn't eliminate the if, then the
result is easy to prove from the IH and
fold_constants_bexp_sound.)
☐
Soundness of (0 + n) Elimination, Redux
Exercise: 4 stars, advanced, optional (optimize_0plus)
Recall the definition
optimize_0plus from the
Imp chapter:
Fixpoint optimize_0plus (
e:
aexp) :
aexp :=
match e with
|
ANum n ⇒
ANum n
|
APlus (
ANum 0)
e2 ⇒
optimize_0plus e2
|
APlus e1 e2 ⇒
APlus (
optimize_0plus e1) (
optimize_0plus e2)
|
AMinus e1 e2 ⇒
AMinus (
optimize_0plus e1) (
optimize_0plus e2)
|
AMult e1 e2 ⇒
AMult (
optimize_0plus e1) (
optimize_0plus e2)
end.
Note that this function is defined over the old
aexps,
without states.
Write a new version of this function that accounts for variables,
plus analogous ones for
bexps and commands:
optimize_0plus_aexp
optimize_0plus_bexp
optimize_0plus_com
Prove that these three functions are sound, as we did for
fold_constants_*. Make sure you use the congruence lemmas in
the proof of
optimize_0plus_com — otherwise it will be
long!
Then define an optimizer on commands that first folds
constants (using
fold_constants_com) and then eliminates
0 + n
terms (using
optimize_0plus_com).
- Give a meaningful example of this optimizer's output.
- Prove that the optimizer is sound. (This part should be very
easy.)
☐
Proving That Programs Are Not Equivalent
Suppose that
c1 is a command of the form
X ::= a1;; Y ::= a2
and
c2 is the command
X ::= a1;; Y ::= a2', where
a2' is
formed by substituting
a1 for all occurrences of
X in
a2.
For example,
c1 and
c2 might be:
c1 = (
X ::= 42 + 53;;
Y ::=
Y +
X)
c2 = (
X ::= 42 + 53;;
Y ::=
Y + (42 + 53))
Clearly, this
particular c1 and
c2 are equivalent. Is this
true in general?
We will see in a moment that it is not, but it is worthwhile
to pause, now, and see if you can find a counter-example on your
own.
Here, formally, is the function that substitutes an arithmetic
expression for each occurrence of a given variable in another
expression:
And here is the property we are interested in, expressing the
claim that commands c1 and c2 as described above are
always equivalent.
Sadly, the property does
not always hold.
Theorem: It is not the case that, for all
i1,
i2,
a1,
and
a2,
cequiv (
i1 ::=
a1;;
i2 ::=
a2)
(
i1 ::=
a1;;
i2 ::=
subst_aexp i1 a1 a2).
Proof: Suppose, for a contradiction, that for all
i1,
i2,
a1, and
a2, we have
cequiv (
i1 ::=
a1;;
i2 ::=
a2)
(
i1 ::=
a1;;
i2 ::=
subst_aexp i1 a1 a2).
Consider the following program:
X ::=
APlus (
AId X) (
ANum 1);;
Y ::=
AId X
Note that
(
X ::=
APlus (
AId X) (
ANum 1);;
Y ::=
AId X)
/
empty_state ⇓ st1,
where
st1 = { X ↦ 1, Y ↦ 1 }.
By our assumption, we know that
cequiv (
X ::=
APlus (
AId X) (
ANum 1);;
Y ::=
AId X)
(
X ::=
APlus (
AId X) (
ANum 1);;
Y ::=
APlus (
AId X) (
ANum 1))
so, by the definition of
cequiv, we have
(
X ::=
APlus (
AId X) (
ANum 1);;
Y ::=
APlus (
AId X) (
ANum 1))
/
empty_state ⇓ st1.
But we can also derive
(
X ::=
APlus (
AId X) (
ANum 1);;
Y ::=
APlus (
AId X) (
ANum 1))
/
empty_state ⇓ st2,
where
st2 = { X ↦ 1, Y ↦ 2 }. Note that
st1 ≠ st2; this
is a contradiction, since
ceval is deterministic!
☐
Exercise: 4 stars, optional (better_subst_equiv)
The equivalence we had in mind above was not complete nonsense —
it was actually almost right. To make it correct, we just need to
exclude the case where the variable
X occurs in the
right-hand-side of the first assignment statement.
Using var_not_used_in_aexp, formalize and prove a correct verson
of subst_equiv_property.
☐
Exercise: 3 stars, optional (inequiv_exercise)
Prove that an infinite loop is not equivalent to
SKIP
☐
Extended Exercise: Nondeterministic Imp
As we have seen (in theorem
ceval_deterministic in the
Imp
chapter), Imp's evaluation relation is deterministic. However,
non-determinism is an important part of the definition of many
real programming languages. For example, in many imperative
languages (such as C and its relatives), the order in which
function arguments are evaluated is unspecified. The program
fragment
might call
f with arguments
(1, 0) or
(1, 1), depending how
the compiler chooses to order things. This can be a little
confusing for programmers, but it gives the compiler writer useful
freedom.
In this exercise, we will extend Imp with a simple
nondeterministic command and study how this change affects
program equivalence. The new command has the syntax
HAVOC X,
where
X is an identifier. The effect of executing
HAVOC X is
to assign an
arbitrary number to the variable
X,
nondeterministically. For example, after executing the program:
the value of
Y can be any number, while the value of
Z is
twice that of
Y (so
Z is always even). Note that we are not
saying anything about the
probabilities of the outcomes — just
that there are (infinitely) many different outcomes that can
possibly happen after executing this nondeterministic code.
In a sense, a variable on which we do
HAVOC roughly corresponds
to an unitialized variable in a low-level language like C. After
the
HAVOC, the variable holds a fixed but arbitrary number. Most
sources of nondeterminism in language definitions are there
precisely because programmers don't care which choice is made (and
so it is good to leave it open to the compiler to choose whichever
will run faster).
We call this new language
Himp (``Imp extended with
HAVOC'').
To formalize Himp, we first add a clause to the definition of
commands.
Inductive com :
Type :=
|
CSkip :
com
|
CAss :
id → aexp → com
|
CSeq :
com → com → com
|
CIf :
bexp → com → com → com
|
CWhile :
bexp → com → com
|
CHavoc :
id → com.
Notation "'SKIP'" :=
CSkip.
Notation "X '::=' a" :=
(
CAss X a) (
at level 60).
Notation "c
1 ;; c
2" :=
(
CSeq c1 c2) (
at level 80,
right associativity).
Notation "'WHILE' b 'DO' c 'END'" :=
(
CWhile b c) (
at level 80,
right associativity).
Notation "'IFB' e
1 'THEN' e
2 'ELSE' e
3 'FI'" :=
(
CIf e1 e2 e3) (
at level 80,
right associativity).
Notation "'HAVOC' l" := (
CHavoc l) (
at level 60).
Exercise: 2 stars (himp_ceval)
Now, we must extend the operational semantics. We have provided
a template for the
ceval relation below, specifying the big-step
semantics. What rule(s) must be added to the definition of
ceval
to formalize the behavior of the
HAVOC command?
As a sanity check, the following claims should be provable for
your definition:
☐
Finally, we repeat the definition of command equivalence from above:
Let's apply this definition to prove some nondeterministic
programs equivalent / inequivalent.
Exercise: 3 stars (havoc_swap)
Are the following two programs equivalent?
If you think they are equivalent, prove it. If you think they are
not, prove that.
☐
Exercise: 4 stars, optional (havoc_copy)
Are the following two programs equivalent?
If you think they are equivalent, then prove it. If you think they
are not, then prove that. (Hint: You may find the assert tactic
useful.)
☐
The definition of program equivalence we are using here has some
subtle consequences on programs that may loop forever. What
cequiv says is that the set of possible
terminating outcomes
of two equivalent programs is the same. However, in a language
with nondeterminism, like Himp, some programs always terminate,
some programs always diverge, and some programs can
nondeterministically terminate in some runs and diverge in
others. The final part of the following exercise illustrates this
phenomenon.
Exercise: 5 stars, advanced (p1_p2_equiv)
Prove that
p1 and
p2 are equivalent. In this and the following
exercises, try to understand why the
cequiv definition has the
behavior it has on these examples.
Intuitively, the programs have the same termination
behavior: either they loop forever, or they terminate in the
same state they started in. We can capture the termination
behavior of p1 and p2 individually with these lemmas:
You should use these lemmas to prove that p1 and p2 are actually
equivalent.
☐
Exercise: 4 stars, advanced (p3_p4_inquiv)
Prove that the following programs are
not equivalent.
☐
Exercise: 5 stars, advanced, optional (p5_p6_equiv)
☐
Additional Exercises
Exercise: 4 stars, optional (for_while_equiv)
This exercise extends the optional
add_for_loop exercise from
the
Imp chapter, where you were asked to extend the language
of commands with C-style
for loops. Prove that the command:
is equivalent to:
c1 ;
WHILE b DO
c3 ;
c2
END
☐
Exercise: 3 stars, optional (swap_noninterfering_assignments)
(Hint: You'll need
functional_extensionality for this one.)
☐
Exercise: 4 stars, advanced, optional (capprox)
In this exercise we define an asymmetric variant of program
equivalence we call
program approximation. We say that a
program
c1 approximates a program
c2 when, for each of
the initial states for which
c1 terminates,
c2 also terminates
and produces the same final state. Formally, program approximation
is defined as follows:
For example, the program
c1 = WHILE X ≠ 1 DO X ::= X - 1 END
approximates
c2 = X ::= 1, but
c2 does not approximate
c1
since
c1 does not terminate when
X = 0 but
c2 does. If two
programs approximate each other in both directions, then they are
equivalent.
Find two programs
c3 and
c4 such that neither approximates
the other.
Find a program cmin that approximates every other program.
Finally, find a non-trivial property which is preserved by
program approximation (when going from left to right).