Equiv: プログラムの同値性


Set Warnings "-notation-overridden,-parsing".
From PLF Require Import Maps.
From Coq Require Import Bool.Bool.
From Coq Require Import Arith.Arith.
From Coq Require Import Init.Nat.
From Coq Require Import Arith.PeanoNat. Import Nat.
From Coq Require Import Arith.EqNat.
From Coq Require Import omega.Omega.
From Coq Require Import Lists.List.
From Coq Require Import Logic.FunctionalExtensionality.
Import ListNotations.
From PLF Require Import Imp.

課題についてのアドバイス

  • Coqによる証明問題は、そこまでに文中で行ってきた証明となるべく同じようにできるようにしています。 課題に取り組む前に、そこまでの証明を自分でも(手とCoqの両方で)やってみなさい。 そして、細部まで理解していることを確認しなさい。そうすることは、多くの時間を節約することになるでしょう。
  • 問題にする Coq の証明はそれなりに複雑なため、怪しそうなところをランダムに探ってみたり、直感に任せたりといった方法で解くことはまず無理です。 なぜその性質が真で、どう進めば証明になるかを最初に考える必要があります。 そのための一番良い方法は、形式的な証明を始める前に、紙の上に非形式的な証明をスケッチでも良いので書いてみることです。 そうすることで、定理が成立することを直観的に確信できます。
  • 作業を減らすために自動化を使いましょう。 この章に出てくる証明は、すべての場合を明示的に書き出すととても長くなります。

振る舞い同値性


これまでの章で、簡単なプログラム変換の正しさを調べました。 optimize_0plus関数です。 対象としたプログラミング言語は、算術式の言語の最初のバージョンでした。 それには変数もなく、そのためプログラム変換が正しいとはどういうことを意味するかを定義することはとても簡単でした。 つまり、変換の結果得られるプログラムが常に、それを評価すると元のプログラムと同じ数値になるということでした。
Imp言語全体、特に代入について、プログラム変換の正しさを語るためには、変数の役割、および状態について考えなければなりません。

定義


変数が入っているaexpbexpについては、どう定義すれば良いかは明らかです。 2つのaexpまたはbexpが振る舞い同値である(behaviorally equivalent)とは、すべての状態で2つの評価結果が同じになることです。

Definition aequiv (a1 a2 : aexp) : Prop :=
  forall (st : state),
    aeval st a1 = aeval st a2.

Definition bequiv (b1 b2 : bexp) : Prop :=
  forall (st : state),
    beval st b1 = beval st b2.

Here are some simple examples of equivalences of arithmetic and boolean expressions.

Theorem aequiv_example: aequiv (X - X) 0.
Proof.
  intros st. simpl. omega.
Qed.

Theorem bequiv_example: bequiv (X - X = 0)%imp true.
Proof.
  intros st. unfold beval.
  rewrite aequiv_example. reflexivity.
Qed.

コマンドについては、状況はもうちょっと微妙です。 簡単に「2つのコマンドが振る舞い同値であるとは、両者を同じ状態から開始すれば同じ状態で終わることである」と言うわけには行きません。 コマンドによっては、特定の状態から開始したときに停止しないためどのような状態にもならないことがあるからです! すると次のように言う必要があります。 2つのコマンドが振る舞い同値であるとは、任意の与えられた状態から両者をスタートすると、(1)両者ともに発散するか、(2)両者ともに停止して同じ状態になることです。 これを簡潔に表現すると、「1つ目が停止して特定の状態になるならば2つ目も同じになり、逆もまた成り立つ」となります。

Definition cequiv (c1 c2 : com) : Prop :=
  forall (st st' : state),
    (st =[ c1 ]=> st') <-> (st =[ c2 ]=> st').

簡単な例


コマンドの同値性の例として、 SKIPにからんだ自明な変換から見てみましょう:

Theorem skip_left : forall 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.

練習問題: ★★ (skip_right)

コマンドの後ろにSkipをつけても元のコマンドと等価なプログラムになることを示しなさい。

Theorem skip_right : forall c,
  cequiv
    (c ;; SKIP)
    c.
Proof.
Admitted.

同様にして、TESTコマンドを最適化します:

Theorem TEST_true_simple : forall c1 c2,
  cequiv
    (TEST true THEN c1 ELSE c2 FI)
    c1.
Proof.
  intros c1 c2.
  split; intros H.
  -
    inversion H; subst. assumption. inversion H5.
  -
    apply E_IfTrue. reflexivity. assumption. Qed.

もちろん、ガードがtrueそのままである条件文を書こうとするプログラマなんていないでしょう。 興味深いのは、ガードが真と「同値である」場合です。
「定理」:もしbBTrueと同値ならば、TEST b THEN c1 ELSE c2 FIc1と同値である。 「証明」:
  • (->) すべてのstst'に対して、もしst =[ TEST b THEN c1 ELSE c2 FI ]=> st'ならばst =[ c1 ]=> st'となることを示す。
    [st =[ TEST b THEN c1 ELSE c2 FI ]=> st']を示すのに使うことができた可能性のある規則、つまり[E_IfTrue]と[E_IfFalse]とで、場合分けをする。 - [st =[ TEST b THEN c1 ELSE c2 FI ]=> st'] の導出の最後の規則が[E_IfTrue]であると仮定する。 このとき、[E_IfTrue]の仮定より[st =[ c1 ]=> st']となる。 これはまさに証明したいことである。 - 一方、[st =[ TEST b THEN c1 ELSE c2 FI ]=> st'] の導出の最後の規則が[E_IfFalse]と仮定する。 すると、[beval st b = false]かつ[st =[ c2 ]=> st']となる。 [b]が[BTrue]と同値であったことから、すべての[st]について、[beval st b = beval st BTrue]が成立する。 ここで[beval st BTrue = true]であるため、これは特に[beval st b = true]を意味する。 しかしこれは矛盾である。 なぜなら、[E_IfFalse]から[beval st b = false]でなければならないからである。 従って、最後の規則は[E_IfFalse]ではあり得ない。 - ([<-]) すべての[st]と[st']について、もし[st =[ c1 ]=> st']ならば[st =[ TEST b THEN c1 ELSE c2 FI ]=> st']となることを示す。 [b]が[BTrue]と同値であることから、[beval st b] = [beval st BTrue] = [true]となる。 仮定[st =[ c1 ]=> st']より[E_IfTrue]が適用でき、[st =[ TEST b THEN c1 ELSE c2 FI ]=> st']となる。 [] 以下がこの証明の形式化版です:

Theorem TEST_true: forall b c1 c2,
  bequiv b BTrue ->
  cequiv
    (TEST 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.

練習問題: ★★, standard, recommended (TEST_false)

Theorem TEST_false : forall b c1 c2,
  bequiv b BFalse ->
  cequiv
    (TEST b THEN c1 ELSE c2 FI)
    c2.
Proof.
Admitted.

Exercise: 3 stars, standard (swap_if_branches)

Show that we can swap the branches of an IF if we also negate its guard.

練習問題: ★★★, standard (swap_if_branches)

ガード部の否定を取ることで、条件文の中身を入れ替えることができることを示しなさい。

Theorem swap_if_branches : forall b e1 e2,
  cequiv
    (TEST b THEN e1 ELSE e2 FI)
    (TEST BNot b THEN e2 ELSE e1 FI).
Proof.
Admitted.

WHILEループについては、同様の2つ定理があります。 ガードがBFalseと同値であるループはSKIPと同値である、というものと、ガードがBTrueと同値であるループはWHILE BTrue DO SKIP END(あるいは他の任意の停止しないプログラム)と同値である、というものです。 1つ目のものは簡単です。

Theorem WHILE_false : forall b c,
  bequiv b BFalse ->
  cequiv
    (WHILE b DO c END)
    SKIP.
Proof.
  intros b c Hb. split; intros H.
  -
    inversion H; subst.
    +
      apply E_Skip.
    +
      rewrite Hb in H2. inversion H2.
  -
    inversion H; subst.
    apply E_WhileFalse.
    rewrite Hb.
    reflexivity. Qed.

練習問題: ★★, advanced, optional (WHILE_false_informal)

WHILE_falseの非形式的証明を記述しなさい。

2つ目の事実を証明するためには、ガードがBTrueと同値であるwhileループが停止しないことを言う補題が1つ必要です:

「補題」:bBTrueと同値のとき、st =[ WHILE b DO c END ]=> st'となることはない。
「証明」:st =[ WHILE b DO c END ]=> st'と仮定する。 st =[ WHILE b DO c END ]=> st'の導出についての帰納法によって、この仮定から矛盾が導かれることを示す。 考える必要のある場合分けは E_WhileFalseE_WhileTrue であり、それ以外はコマンドが矛盾する。
  • st =[ WHILE b DO c END ]=> st' が規則E_WhileFalseから証明されると仮定する。 すると仮定からbeval st b = false となる。 しかしこれは、bBTrueと同値という仮定と矛盾する。
  • st =[ WHILE b DO c END ]=> st' が規則E_WhileTrueを使って証明されると仮定する。 このとき、以下のことがわかる。
    1. beval st b = trueが成り立つ。 2. ある状態 st0 があり、st =[ c ]=> st0st0 =[ WHILE b DO c END ]=> st' が成り立つ。 3. 帰納法の仮定から、st0 =[ WHILE b DO c END ]=> st' は矛盾を導く。
    この2と3から矛盾が導かれる。

Lemma WHILE_true_nonterm : forall b c st st',
  bequiv b BTrue ->
  ~( st =[ WHILE b DO c END ]=> st' ).
Proof.
  intros b c st st' Hb.
  intros H.
  remember (WHILE b DO c END)%imp 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.

練習問題: ★★, standard, optional (WHILE_true_nonterm_informal)

補題WHILE_true_nontermが意味するものを日本語で書きなさい。

練習問題: ★★, standard, recommended (WHILE_true)

以下の定理を示しなさい。 ヒント:ここでWHILE_true_nontermを使いなさい。

Theorem WHILE_true : forall b c,
  bequiv b true ->
  cequiv
    (WHILE b DO c END)
    (WHILE true DO SKIP END).
Proof.
Admitted.
A more interesting fact about WHILE commands is that any number of copies of the body can be "unrolled" without changing meaning. Loop unrolling is a common transformation in real compilers.

Theorem loop_unrolling : forall b c,
  cequiv
    (WHILE b DO c END)
    (TEST 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_WhileTrue with (st' := st'0).
      assumption. assumption. assumption.
    +
      inversion H5; subst. apply E_WhileFalse. assumption. Qed.

練習問題: ★★, standard, optional (seq_assoc)

Theorem seq_assoc : forall c1 c2 c3,
  cequiv ((c1;;c2);;c3) (c1;;(c2;;c3)).
Proof.
Admitted.
Proving program properties involving assignments is one place where the fact that program states are treated extensionally (e.g., x !-> m x ; m and m are equal maps) comes in handy.

Theorem identity_assignment : forall x,
  cequiv
    (x ::= x)
    SKIP.
Proof.
  intros.
  split; intro H; inversion H; subst.
  -
    rewrite t_update_same.
    apply E_Skip.
  -
    assert (Hx : st' =[ x ::= x ]=> (x !-> st' x ; st')).
    { apply E_Ass. reflexivity. }
    rewrite t_update_same in Hx.
    apply Hx.
Qed.

練習問題: ★★, standard, recommended (assign_aequiv)

Theorem assign_aequiv : forall (x : string) e,
  aequiv x e ->
  cequiv SKIP (x ::= e).
Proof.
Admitted.

Exercise: 2 stars, standard (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.

Definition prog_a : com :=
  (WHILE ~(X <= 0) DO
    X ::= X + 1
  END)%imp.

Definition prog_b : com :=
  (TEST X = 0 THEN
    X ::= X + 1;;
    Y ::= 1
  ELSE
    Y ::= 0
  FI;;
  X ::= X - Y;;
  Y ::= 0)%imp.

Definition prog_c : com :=
  SKIP%imp.

Definition prog_d : com :=
  (WHILE ~(X = 0) DO
    X ::= (X * Y) + 1
  END)%imp.

Definition prog_e : com :=
  (Y ::= 0)%imp.

Definition prog_f : com :=
  (Y ::= X + 1;;
  WHILE ~(X = Y) DO
    Y ::= X + 1
  END)%imp.

Definition prog_g : com :=
  (WHILE true DO
    SKIP
  END)%imp.

Definition prog_h : com :=
  (WHILE ~(X = X) DO
    X ::= X + 1
  END)%imp.

Definition prog_i : com :=
  (WHILE ~(X = Y) DO
    X ::= Y + 1
  END)%imp.

Definition equiv_classes : list (list com)
  . Admitted.

Definition manual_grade_for_equiv_classes : option (nat*string) := None.

振る舞い同値の性質


ここからは、プログラムの同値性に関する性質を調べていきましょう。

振る舞い同値は同値関係である


最初に、aexpbexpcomの同値が、本当に「同値関係」であること、つまり、反射性、対称性、推移性を持つことを検証します。 証明は簡単です。

Lemma refl_aequiv : forall (a : aexp), aequiv a a.
Proof.
  intros a st. reflexivity. Qed.

Lemma sym_aequiv : forall (a1 a2 : aexp),
  aequiv a1 a2 -> aequiv a2 a1.
Proof.
  intros a1 a2 H. intros st. symmetry. apply H. Qed.

Lemma trans_aequiv : forall (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 : forall (b : bexp), bequiv b b.
Proof.
  unfold bequiv. intros b st. reflexivity. Qed.

Lemma sym_bequiv : forall (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 : forall (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 : forall (c : com), cequiv c c.
Proof.
  unfold cequiv. intros c st st'. apply iff_refl. Qed.

Lemma sym_cequiv : forall (c1 c2 : com),
  cequiv c1 c2 -> cequiv c2 c1.
Proof.
  unfold cequiv. intros c1 c2 H st st'.
  assert (st =[ c1 ]=> st' <-> st =[ c2 ]=> st') as H'.
  { apply H. }
  apply iff_sym. assumption.
Qed.

Lemma iff_trans : forall (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 : forall (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 (st =[ c2 ]=> st'). apply H12. apply H23. Qed.

振る舞い同値は合同関係である


少しわかりにくいですが、振る舞い同値は、合同関係(congruence)でもあります。 つまり、2つのサブプログラムが同値ならば、それを含むプログラム全体も同値です:
              aequiv a1 a1' 
      ----------------------------- 
      cequiv (x ::= a1) (x ::= a1') 
 
              cequiv c1 c1' 
              cequiv c2 c2' 
         -------------------------- 
         cequiv (c1;;c2) (c1';;c2') 
他のコマンドも同様です。

(注意して欲しいのですが、ここで推論規則の記法を使っていますが、これは定義の一部ではありません。 単に正しい含意を読みやすい形で書き出しただけです。 この含意は以下で証明します。)

合同性がなぜ重要なのか、次の節で具体的な例(fold_constants_com_soundの証明)によって見ます。 ただ、メインのアイデアは、大きなプログラムの小さな部分を同値の小さな部分で置き換えると、大きなプログラム全体が元のものと同値になることを、変化していない部分についての明示的な証明「なしに」わかるということです。 つまり、大きなプログラムの小さな変更についての証明の負担が、プログラムではなく変更に比例するということです。

Theorem CAss_congruence : forall x a1 a1',
  aequiv a1 a1' ->
  cequiv (CAss x a1) (CAss x a1').
Proof.
  intros x a1 a2 Heqv st st'.
  split; intros Hceval.
  -
    inversion Hceval. subst. apply E_Ass.
    rewrite Heqv. reflexivity.
  -
    inversion Hceval. subst. apply E_Ass.
    rewrite Heqv. reflexivity. Qed.

ループの合同性は帰納法が必要になるので、さらに少しおもしろいものになります。
「定理」:WHILEについての同値は合同関係である。 すなわち、もしb1b1'と同値であり、c1c1'と同値ならば、WHILE b1 DO c1 ENDWHILE b1' DO c1' ENDと同値である。
「証明」:b1b1'と同値、c1c1'と同値であるとする。 すべてのstst'について、証明すべきことは、st =[ WHILE b1 DO c1 END ]=> st'の必要十分条件はst =[ WHILE b1' DO c1' END ]=> st'であることである。 必要条件と十分条件の両方向を別々に証明する。

Theorem CWhile_congruence : forall 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)%imp as cwhile
      eqn:Heqcwhile.
    induction Hce; inversion Heqcwhile; subst.
    +
      apply E_WhileFalse. rewrite <- Hb1e. apply H.
    +
      apply E_WhileTrue with (st' := st').
      * rewrite <- Hb1e. apply H.
      *
        apply (Hc1e st st'). apply Hce1.
      *
        apply IHHce2. reflexivity.
  -
    remember (WHILE b1' DO c1' END)%imp as c'while
      eqn:Heqc'while.
    induction Hce; inversion Heqc'while; subst.
    +
      apply E_WhileFalse. rewrite -> Hb1e. apply H.
    +
      apply E_WhileTrue with (st' := st').
      * rewrite -> Hb1e. apply H.
      *
        apply (Hc1e st st'). apply Hce1.
      *
        apply IHHce2. reflexivity. Qed.

練習問題: ★★★, standard, optional (CSeq_congruence)

Theorem CSeq_congruence : forall c1 c1' c2 c2',
  cequiv c1 c1' -> cequiv c2 c2' ->
  cequiv (c1;;c2) (c1';;c2').
Proof.
Admitted.

練習問題: ★★★, standard (CIf_congruence)

Theorem CIf_congruence : forall b b' c1 c1' c2 c2',
  bequiv b b' -> cequiv c1 c1' -> cequiv c2 c2' ->
  cequiv (TEST b THEN c1 ELSE c2 FI)
         (TEST b' THEN c1' ELSE c2' FI).
Proof.
Admitted.

同値である2つのプログラムとその同値の証明の例です...

Example congruence_example:
  cequiv
    
    (X ::= 0;;
     TEST X = 0
     THEN
       Y ::= 0
     ELSE
       Y ::= 42
     FI)
    
    (X ::= 0;;
     TEST X = 0
     THEN
       Y ::= X - X
     ELSE
       Y ::= 42
     FI).
Proof.
  apply CSeq_congruence.
  - apply refl_cequiv.
  - apply CIf_congruence.
    + apply refl_bequiv.
    + apply CAss_congruence. unfold aequiv. simpl.
      * symmetry. apply minus_diag.
    + apply refl_cequiv.
Qed.

Exercise: 3 stars, advanced, optional (not_congr)

We've shown that the cequiv relation is both an equivalence and a congruence on commands. Can you think of a relation on commands that is an equivalence but not a congruence?


プログラム変換


プログラム変換(program transformation)とは、プログラムを入力とし、出力としてそのプログラムの何らかの変形を生成する関数です。 定数畳み込みのようなコンパイラの最適化は標準的な例ですが、それ以外のものもたくさんあります。

プログラム変換が健全(sound)とは、その変換が元のプログラムの振る舞いを保存することです。

Definition atrans_sound (atrans : aexp -> aexp) : Prop :=
  forall (a : aexp),
    aequiv a (atrans a).

Definition btrans_sound (btrans : bexp -> bexp) : Prop :=
  forall (b : bexp),
    bequiv b (btrans b).

Definition ctrans_sound (ctrans : com -> com) : Prop :=
  forall (c : com),
    cequiv c (ctrans c).

定数畳み込み変換


式が定数(constant)であるとは、その式が変数参照を含まないことです。
定数畳み込みは、定数式をその値に置換する最適化です。

Fixpoint fold_constants_aexp (a : aexp) : aexp :=
  match a with
  | ANum n => ANum n
  | AId x => AId x
  | APlus a1 a2 =>
    match (fold_constants_aexp a1,
           fold_constants_aexp a2)
    with
    | (ANum n1, ANum n2) => ANum (n1 + n2)
    | (a1', a2') => APlus a1' a2'
    end
  | AMinus a1 a2 =>
    match (fold_constants_aexp a1,
           fold_constants_aexp a2)
    with
    | (ANum n1, ANum n2) => ANum (n1 - n2)
    | (a1', a2') => AMinus a1' a2'
    end
  | AMult a1 a2 =>
    match (fold_constants_aexp a1,
           fold_constants_aexp a2)
    with
    | (ANum n1, ANum n2) => ANum (n1 * n2)
    | (a1', a2') => AMult a1' a2'
    end
  end.

Example fold_aexp_ex1 :
    fold_constants_aexp ((1 + 2) * X)
  = (3 * X)%imp.
Proof. reflexivity. Qed.

定数畳み込みのこのバージョンは簡単な足し算等を消去していないことに注意します。 複雑になるのを避けるため、1つの最適化に焦点を絞っています。 式の単純化の他の方法を組込むことはそれほど難しいことではありません。 定義と証明が長くなるだけです。

Example fold_aexp_ex2 :
  fold_constants_aexp (X - ((0 * 6) + Y))%imp = (X - (0 + Y))%imp.
Proof. reflexivity. Qed.

BEqBLeの場合に)fold_constants_aexpbexpに持ち上げることができるだけでなく、定数「ブール」式をみつけてその場で置換することもできます。

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 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 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 (true && ~(false && true))%imp
  = true.
Proof. reflexivity. Qed.

Example fold_bexp_ex2 :
  fold_constants_bexp ((X = Y) && (0 = (2 - (1 + 1))))%imp
  = ((X = Y) && true)%imp.
Proof. reflexivity. Qed.

コマンド内の定数を畳み込みするために、含まれるすべての式に対応する畳み込み関数を適用します。

Open Scope imp.
Fixpoint fold_constants_com (c : com) : com :=
  match c with
  | SKIP =>
      SKIP
  | x ::= a =>
      x ::= (fold_constants_aexp a)
  | c1 ;; c2 =>
      (fold_constants_com c1) ;; (fold_constants_com c2)
  | TEST b THEN c1 ELSE c2 FI =>
      match fold_constants_bexp b with
      | BTrue => fold_constants_com c1
      | BFalse => fold_constants_com c2
      | b' => TEST b' THEN fold_constants_com c1
                     ELSE fold_constants_com c2 FI
      end
  | WHILE b DO c END =>
      match fold_constants_bexp b with
      | BTrue => WHILE BTrue DO SKIP END
      | BFalse => SKIP
      | b' => WHILE b' DO (fold_constants_com c) END
      end
  end.
Close Scope imp.

Example fold_com_ex1 :
  fold_constants_com
    
    (X ::= 4 + 5;;
     Y ::= X - 3;;
     TEST (X - Y) = (2 + 4) THEN SKIP
     ELSE Y ::= 0 FI;;
     TEST 0 <= (4 - (2 + 1)) THEN Y ::= 0
     ELSE SKIP FI;;
     WHILE Y = 0 DO
       X ::= X + 1
     END)%imp
  =
    (X ::= 9;;
     Y ::= X - 3;;
     TEST (X - Y) = 6 THEN SKIP
     ELSE Y ::= 0 FI;;
     Y ::= 0;;
     WHILE Y = 0 DO
       X ::= X + 1
     END)%imp.
Proof. reflexivity. Qed.

定数畳み込みの健全性


上でやったことが正しいことを示さなければなりません。

以下が算術式に対する証明です:

Theorem fold_constants_aexp_sound :
  atrans_sound fold_constants_aexp.
Proof.
  unfold atrans_sound. intros a. unfold aequiv. intros st.
  induction a; simpl;
    
    try reflexivity;
    
    try (destruct (fold_constants_aexp a1);
         destruct (fold_constants_aexp a2);
         rewrite IHa1; rewrite IHa2; reflexivity). Qed.

練習問題: ★★★, standard, optional (fold_bexp_Eq_informal)

ここに、ブール式の定数畳み込みに関する健全性の議論のBEqの場合の非形式的証明を示します。 これを丁寧に読みその後の形式的証明と比較しなさい。 次に、形式的証明のBLe部分を(もし可能ならばBEqの場合を見ないで)記述しなさい。
「定理」:ブール式に対する定数畳み込み関数fold_constants_bexpは健全である。
「証明」:すべてのブール式bについてbfold_constants_bexp bと同値であることを示す。 bについての帰納法を行う。 bBEq a1 a2という形の場合を示す。
この場合、
       beval st (BEq a1 a2)
     = beval st (fold_constants_bexp (BEq a1 a2))
を示せば良い。これには2種類の場合がある:

Theorem fold_constants_bexp_sound:
  btrans_sound fold_constants_bexp.
Proof.
  unfold btrans_sound. intros b. unfold bequiv. intros st.
  induction b;
    
    try reflexivity.
  -
    simpl.

(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.)

    remember (fold_constants_aexp a1) as a1' eqn:Heqa1'.
    remember (fold_constants_aexp a2) as a2' eqn:Heqa2'.
    replace (aeval st a1) with (aeval st a1') by
       (subst a1'; rewrite <- fold_constants_aexp_sound; reflexivity).
    replace (aeval st a2) with (aeval st a2') by
       (subst a2'; rewrite <- fold_constants_aexp_sound; reflexivity).
    destruct a1'; destruct a2'; try reflexivity.

      simpl. destruct (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.

練習問題: ★★★, standard (fold_constants_com_sound)

次の証明のWHILEの場合を完成させなさい。

Theorem fold_constants_com_sound :
  ctrans_sound fold_constants_com.
Proof.
  unfold ctrans_sound. intros c.
  induction c; simpl.
  - apply refl_cequiv.
  - apply CAss_congruence.
              apply fold_constants_aexp_sound.
  - apply CSeq_congruence; assumption.
  -
    assert (bequiv b (fold_constants_bexp b)). {
      apply fold_constants_bexp_sound. }
    destruct (fold_constants_bexp b) eqn:Heqb;
      try (apply CIf_congruence; assumption).
    +
      apply trans_cequiv with c1; try assumption.
      apply TEST_true; assumption.
    +
      apply trans_cequiv with c2; try assumption.
      apply TEST_false; assumption.
  -
     Admitted.

(0 + n)の消去の健全性、再び


練習問題: ★★★★, advanced, optional (optimize_0plus)

「論理の基礎」のImpの章のoptimize_0plusの定義をふり返ります。
    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.
この関数は、aexpの上で定義されていて、状態を扱わないことに注意します。
変数を扱えるようにした、この関数の新しいバージョンを記述しなさい。 加えて、bexpおよびコマンドに対しても、同様のものを記述しなさい:
     optimize_0plus_aexp
     optimize_0plus_bexp
     optimize_0plus_com
これらの関数の健全性を、fold_constants_*について行ったのと同様に証明しなさい。 optimize_0plus_comの証明においては、合同性補題を使いなさい(そうしなければ証明はとても長くなるでしょう!)。
次に、コマンドに対して次の処理を行う最適化関数を定義しなさい。 行うべき処理は、まず定数畳み込みを(fold_constants_comを使って)行い、次に0 + n項を(optimize_0plus_comを使って)消去することです。
  • この最適化関数の出力の意味のある例を示しなさい。
  • この最適化関数が健全であることを示しなさい。(この部分は「とても」簡単なはずです。)


非等価性の証明


c1X ::= a1; Y ::= a2という形のコマンドで、c2がコマンドX ::= a1; Y ::= a2'であると仮定します。 ただしa2'は、a2の中のすべてのXa1で置換したものとします。 例えば、c1c2は次のようなものです。
       c1 = (X ::= 42 + 53;;
               Y ::= Y + X)
       c2 = (X ::= 42 + 53;;
               Y ::= Y + (42 + 53))
明らかに、この例の場合はc1c2は同値です。 しかし、一般にそうだと言えるでしょうか?

この後すぐ、そうではないことを示します。 しかし、ここでちょっと立ち止まって、自分の力で反例を見つけることができるか試してみるのは意味があることです。

次が、式aの中の変数xを別の算術式uで置換する関数の形式的定義です。

Fixpoint subst_aexp (x : string) (u : aexp) (a : aexp) : aexp :=
  match a with
  | ANum n =>
      ANum n
  | AId x' =>
      if eqb_string x x' then u else AId x'
  | APlus a1 a2 =>
      APlus (subst_aexp x u a1) (subst_aexp x u a2)
  | AMinus a1 a2 =>
      AMinus (subst_aexp x u a1) (subst_aexp x u a2)
  | AMult a1 a2 =>
      AMult (subst_aexp x u a1) (subst_aexp x u a2)
  end.

Example subst_aexp_ex :
  subst_aexp X (42 + 53) (Y + X)%imp
  = (Y + (42 + 53))%imp.
Proof. reflexivity. Qed.

そして次が、興味対象の性質です。 上記コマンドc1c2が常に同値であることを主張するものです。

Definition subst_equiv_property := forall x1 x2 a1 a2,
  cequiv (x1 ::= a1;; x2 ::= a2)
         (x1 ::= a1;; x2 ::= subst_aexp x1 a1 a2).

残念ながら、この性質は、常には成立「しません」。 つまり、すべてのx1, x2, a1, a2について、次が成立するわけではない、ということです。
      cequiv (x1 ::= a1;; x2 ::= a2)
             (x1 ::= a1;; x2 ::= subst_aexp x1 a1 a2)
仮にすべてのx1, x2, a1, a2について
      cequiv (x1 ::= a1;; x2 ::= a2)
             (x1 ::= a1;; x2 ::= subst_aexp x1 a1 a2)
が成立するとしましょう。 ここで、次のプログラムを考えます。
      X ::= X + 1;; Y ::= X
次のことに注意しましょう。
      empty_st =[ X ::= X + 1;; Y ::= X ]=> st1
ここで st1 = (Y !-> 1 ; X !-> 1) です。
仮定より、次が言えます。
      cequiv (X ::= X + 1;;
              Y ::= X)
             (X ::= X + 1;;
              Y ::= X + 1)
すると、cequivの定義より、次が言えます。
      empty_st =[ X ::= X + 1;; Y ::= X + 1 ]=> st1
しかし次のことも言えます。
      empty_st =[ X ::= X + 1;; Y ::= X + 1 ]=> st2
ただし st2 = (Y !-> 2 ; X !-> 1) です。 st1 <> st2であるはずなので、これはcevalが決定性を持つことに矛盾します!

Theorem subst_inequiv :
  ~ subst_equiv_property.
Proof.
  unfold subst_equiv_property.
  intros Contra.

  remember (X ::= X + 1;;
            Y ::= X)%imp
      as c1.
  remember (X ::= X + 1;;
            Y ::= X + 1)%imp
      as c2.
  assert (cequiv c1 c2) by (subst; apply Contra).

  remember (Y !-> 1 ; X !-> 1) as st1.
  remember (Y !-> 2 ; X !-> 1) as st2.
  assert (H1 : empty_st =[ c1 ]=> st1);
  assert (H2 : empty_st =[ c2 ]=> st2);
  try (subst;
       apply E_Seq with (st' := (X !-> 1));
       apply E_Ass; reflexivity).
  apply H in H1.

  assert (Hcontra : st1 = st2)
    by (apply (ceval_deterministic c2 empty_st); assumption).
  assert (Hcontra' : st1 Y = st2 Y)
    by (rewrite Hcontra; reflexivity).
  subst. inversion Hcontra'. Qed.

練習問題: ★★★★, standard, optional (better_subst_equiv)

上で成立すると考えていた同値は、完全に意味がないものではありません。 それは実際、ほとんど正しいのです。それを直すためには、最初の代入の右辺に変数Xが現れる場合を排除すれば良いのです。

Inductive var_not_used_in_aexp (x : string) : aexp -> Prop :=
  | VNUNum : forall n, var_not_used_in_aexp x (ANum n)
  | VNUId : forall y, x <> y -> var_not_used_in_aexp x (AId y)
  | VNUPlus : forall a1 a2,
      var_not_used_in_aexp x a1 ->
      var_not_used_in_aexp x a2 ->
      var_not_used_in_aexp x (APlus a1 a2)
  | VNUMinus : forall a1 a2,
      var_not_used_in_aexp x a1 ->
      var_not_used_in_aexp x a2 ->
      var_not_used_in_aexp x (AMinus a1 a2)
  | VNUMult : forall a1 a2,
      var_not_used_in_aexp x a1 ->
      var_not_used_in_aexp x a2 ->
      var_not_used_in_aexp x (AMult a1 a2).

Lemma aeval_weakening : forall x st a ni,
  var_not_used_in_aexp x a ->
  aeval (x !-> ni ; st) a = aeval st a.
Proof.
Admitted.

var_not_used_in_aexpを使って、subst_equiv_propertyの正しいバージョンを形式化し、証明しなさい。


練習問題: ★★★, standard (inequiv_exercise)

無限ループがSKIPと同値でないことを示しなさい。

Theorem inequiv_exercise:
  ~ cequiv (WHILE true DO SKIP END) SKIP.
Proof.
Admitted.

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
x = 0;; f(++x, x)
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:
HAVOC Y;; Z ::= Y * 2
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 uninitialized 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'').

Module Himp.

To formalize Himp, we first add a clause to the definition of commands.

Inductive com : Type :=
  | CSkip : com
  | CAss : string -> aexp -> com
  | CSeq : com -> com -> com
  | CIf : bexp -> com -> com -> com
  | CWhile : bexp -> com -> com
  | CHavoc : string -> com.
Notation "'SKIP'" :=
  CSkip : imp_scope.
Notation "X '::=' a" :=
  (CAss X a) (at level 60) : imp_scope.
Notation "c1 ;; c2" :=
  (CSeq c1 c2) (at level 80, right associativity) : imp_scope.
Notation "'WHILE' b 'DO' c 'END'" :=
  (CWhile b c) (at level 80, right associativity) : imp_scope.
Notation "'TEST' e1 'THEN' e2 'ELSE' e3 'FI'" :=
  (CIf e1 e2 e3) (at level 80, right associativity) : imp_scope.
Notation "'HAVOC' l" :=
  (CHavoc l) (at level 60) : imp_scope.

Exercise: 2 stars, standard (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?

Reserved Notation "st '=[' c ']=>' st'" (at level 40).

Open Scope imp_scope.
Inductive ceval : com -> state -> state -> Prop :=
  | E_Skip : forall st,
      st =[ SKIP ]=> st
  | E_Ass : forall st a1 n x,
      aeval st a1 = n ->
      st =[ x ::= a1 ]=> (x !-> n ; st)
  | E_Seq : forall c1 c2 st st' st'',
      st =[ c1 ]=> st' ->
      st' =[ c2 ]=> st'' ->
      st =[ c1 ;; c2 ]=> st''
  | E_IfTrue : forall st st' b c1 c2,
      beval st b = true ->
      st =[ c1 ]=> st' ->
      st =[ TEST b THEN c1 ELSE c2 FI ]=> st'
  | E_IfFalse : forall st st' b c1 c2,
      beval st b = false ->
      st =[ c2 ]=> st' ->
      st =[ TEST b THEN c1 ELSE c2 FI ]=> st'
  | E_WhileFalse : forall b st c,
      beval st b = false ->
      st =[ WHILE b DO c END ]=> st
  | E_WhileTrue : forall st st' st'' b c,
      beval st b = true ->
      st =[ c ]=> st' ->
      st' =[ WHILE b DO c END ]=> st'' ->
      st =[ WHILE b DO c END ]=> st''


  where "st =[ c ]=> st'" := (ceval c st st').
Close Scope imp_scope.

As a sanity check, the following claims should be provable for your definition:

Example havoc_example1 : empty_st =[ (HAVOC X)%imp ]=> (X !-> 0).
Proof.
Admitted.

Example havoc_example2 :
  empty_st =[ (SKIP;; HAVOC Z)%imp ]=> (Z !-> 42).
Proof.
Admitted.

Definition manual_grade_for_Check_rule_for_HAVOC : option (nat*string) := None.
Finally, we repeat the definition of command equivalence from above:

Definition cequiv (c1 c2 : com) : Prop := forall st st' : state,
  st =[ c1 ]=> st' <-> st =[ c2 ]=> st'.

Let's apply this definition to prove some nondeterministic programs equivalent / inequivalent.

Exercise: 3 stars, standard (havoc_swap)

Are the following two programs equivalent?

Definition pXY :=
  (HAVOC X;; HAVOC Y)%imp.

Definition pYX :=
  (HAVOC Y;; HAVOC X)%imp.

If you think they are equivalent, prove it. If you think they are not, prove that.

Theorem pXY_cequiv_pYX :
  cequiv pXY pYX \/ ~cequiv pXY pYX.
Proof. Admitted.

Exercise: 4 stars, standard, optional (havoc_copy)

Are the following two programs equivalent?

Definition ptwice :=
  (HAVOC X;; HAVOC Y)%imp.

Definition pcopy :=
  (HAVOC X;; Y ::= X)%imp.

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.)

Theorem ptwice_cequiv_pcopy :
  cequiv ptwice pcopy \/ ~cequiv ptwice pcopy.
Proof. Admitted.
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: 4 stars, advanced (p1_p2_term)

Consider the following commands:

Definition p1 : com :=
  (WHILE ~ (X = 0) DO
    HAVOC Y;;
    X ::= X + 1
  END)%imp.

Definition p2 : com :=
  (WHILE ~ (X = 0) DO
    SKIP
  END)%imp.

Intuitively, p1 and p2 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:

Lemma p1_may_diverge : forall st st', st X <> 0 ->
  ~ st =[ p1 ]=> st'.
Proof. Admitted.

Lemma p2_may_diverge : forall st st', st X <> 0 ->
  ~ st =[ p2 ]=> st'.
Proof.
Admitted.

Exercise: 4 stars, advanced (p1_p2_equiv)

Use these two lemmas to prove that p1 and p2 are actually equivalent.

Theorem p1_p2_equiv : cequiv p1 p2.
Proof. Admitted.

Exercise: 4 stars, advanced (p3_p4_inequiv)

Prove that the following programs are not equivalent. (Hint: What should the value of Z be when p3 terminates? What about p4?)

Definition p3 : com :=
  (Z ::= 1;;
  WHILE ~(X = 0) DO
    HAVOC X;;
    HAVOC Z
  END)%imp.

Definition p4 : com :=
  (X ::= 0;;
  Z ::= 1)%imp.

Theorem p3_p4_inequiv : ~ cequiv p3 p4.
Proof. Admitted.

Exercise: 5 stars, advanced, optional (p5_p6_equiv)

Prove that the following commands are equivalent. (Hint: As mentioned above, our definition of cequiv for Himp only takes into account the sets of possible terminating configurations: two programs are equivalent if and only if the set of possible terminating states is the same for both programs when given a same starting state st. If p5 terminates, what should the final state be? Conversely, is it always possible to make p5 terminate?)

Definition p5 : com :=
  (WHILE ~(X = 1) DO
    HAVOC X
  END)%imp.

Definition p6 : com :=
  (X ::= 1)%imp.

Theorem p5_p6_equiv : cequiv p5 p6.
Proof. Admitted.

End Himp.

さらなる練習問題


練習問題: ★★★★, standard, optional (for_while_equiv)

この練習問題は、Impの章のoptionalの練習問題 add_for_loop を拡張したものです。 もとの add_for_loop は、コマンド言語に C言語のスタイルの forループを追加しなさい、というものでした。 ここでは次のことを証明しなさい:
      for (c1 ; b ; c2) {
          c3
      }

       c1 ;
       WHILE b DO
         c3 ;
         c2
       END
と同値である。

練習問題: ★★★, standard, optional (swap_noninterfering_assignments)

(ヒント: 証明にはfunctional_extensionalityが必要でしょう。)

Theorem swap_noninterfering_assignments: forall l1 l2 a1 a2,
  l1 <> l2 ->
  var_not_used_in_aexp l1 a2 ->
  var_not_used_in_aexp l2 a1 ->
  cequiv
    (l1 ::= a1;; l2 ::= a2)
    (l2 ::= a2;; l1 ::= a1).
Proof.
Admitted.

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:

Definition capprox (c1 c2 : com) : Prop := forall (st st' : state),
  st =[ c1 ]=> st' -> st =[ c2 ]=> st'.

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.

Definition c3 : com
  . Admitted.
Definition c4 : com
  . Admitted.

Theorem c3_c4_different : ~ capprox c3 c4 /\ ~ capprox c4 c3.
Proof. Admitted.

Find a program cmin that approximates every other program.

Definition cmin : com
  . Admitted.

Theorem cmin_minimal : forall c, capprox cmin c.
Proof. Admitted.

Finally, find a non-trivial property which is preserved by program approximation (when going from left to right).

Definition zprop (c : com) : Prop
  . Admitted.

Theorem zprop_preserving : forall c c',
  zprop c -> capprox c c' -> zprop c'.
Proof. Admitted.