Imp: 単純な命令型プログラム
この章では、学習のためにCoqを使う方法に焦点を当てます。
ここで学ぶ対象は、Imp と呼ばれる単純な命令型プログラミング言語です。
Imp は C や Java のような標準的に広く使われている言語の中心部分の機能だけを取り出したものです。
下の例は、おなじみの数学的関数を Imp で書いたものです。
Z ::= X;; Y ::= 1;; WHILE ~(Z = 0) DO Y ::= Y * Z;; Z ::= Z - 1 END
この章ではImpの構文(syntax)と意味(semantics)の定義方法について学びます。
「プログラミング言語の基礎(Programming Language Foundations)」(ソフトウェアの基礎(Software Foundations)、第二巻)の章では、プログラムの同値性(program equivalence)の理論を構築し、命令型プログラムについての推論のための論理として一番知られているホーア論理(Hoare Logic)を導入します。
Set Warnings "-notation-overridden,-parsing".
From Coq Require Import Bool.Bool.
From Coq Require Import Init.Nat.
From Coq Require Import Arith.Arith.
From Coq Require Import Arith.EqNat.
From Coq Require Import omega.Omega.
From Coq Require Import Lists.List.
From Coq Require Import Strings.String.
Import ListNotations.
From LF Require Import Maps.
Impを三つの部分に分けて示します:
最初に算術式(arithmetic expressions)とブール式(boolean expressions)、次にこれらの式に変数(variables)を加えたもの、
そして最後に代入(assignment)、条件分岐(conditions)、コマンド合成(sequencing)、ループ(loops)を持つコマンド(commands)の言語です。
次の2つの定義は、算術式とブール式の抽象構文(abstract syntax)を定めます。
Inductive aexp : Type :=
| ANum (n : nat)
| APlus (a1 a2 : aexp)
| AMinus (a1 a2 : aexp)
| AMult (a1 a2 : aexp).
Inductive bexp : Type :=
| BTrue
| BFalse
| BEq (a1 a2 : aexp)
| BLe (a1 a2 : aexp)
| BNot (b : bexp)
| BAnd (b1 b2 : bexp).
この章では、プログラマが実際に書く具象構文から抽象構文木への変換はほとんど省略します。
例えば、文字列"1 + 2 * 3"をAST(Abstract Syntax Tree, 抽象構文木)
APlus (ANum 1) (AMult (ANum 2) (ANum 3))
にする変換のことです。
この変換に使う単純な字句解析器と構文解析器をオプションの章ImpParserで実装します。
この章を理解するのにImpParser章の理解は必要ではありませんが、もしこれらの技術について(例えばコンパイラの講義などで)触れたことがないならば、ざっと見てみるのも良いでしょう。
APlus (ANum 1) (AMult (ANum 2) (ANum 3))
比較のため、同じ抽象構文を定義する慣習的なBNF(Backus-Naur Form)文法を以下に示します:
a ::= nat | a + a | a - a | a * a b ::= true | false | a = a | a <= a | ~ b | b && b
上述のCoq版と比較して...
両方の記法に通じているのは良いことです。
非形式的なものは人間とのコミュニケーションに、形式的なものは実装と証明に便利です。
- BNFはより非形式的です。
例えば、BNFは式の表面的な構文についていくらかの情報(可算は中置記号+を使って記述される、など)を与えていますが、字句解析と構文解析の他の面(+、-、*の優先順位、括弧による部分式のくくりだし、など)は定めないままになっています。
コンパイラを実装するときなどは、この記述を形式的定義にするために、追加の情報(および人間の知性)が必要でしょう。
- 逆に、BNF版はより軽くて、読むのがより簡単です。
非形式的であることで柔軟性を持っているので、黒板を使って議論する場面などでは特段に有効です。
そういう場面では、細部をいちいち正確に確定させていくことより、全体的アイデアを伝えることが重要だからです。
算術式を評価する(evaluating)と、式から1つの数を生成します。
Fixpoint aeval (a : aexp) : nat :=
match a with
| ANum n => n
| APlus a1 a2 => (aeval a1) + (aeval a2)
| AMinus a1 a2 => (aeval a1) - (aeval a2)
| AMult a1 a2 => (aeval a1) * (aeval a2)
end.
Example test_aeval1:
aeval (APlus (ANum 2) (ANum 2)) = 4.
Proof. reflexivity. Qed.
同様に、ブール式を評価するとブール値になります。
Fixpoint beval (b : bexp) : bool :=
match b with
| BTrue => true
| BFalse => false
| BEq a1 a2 => (aeval a1) =? (aeval a2)
| BLe a1 a2 => (aeval a1) <=? (aeval a2)
| BNot b1 => negb (beval b1)
| BAnd b1 b2 => andb (beval b1) (beval b2)
end.
ここまで定義したものはわずかですが、その定義から既にいくらかのものを得ることができます。
算術式をとって、それを少し簡単化する関数を定義するとします。
すべての 0 + e (つまりAPlus (ANum 0) e)を単にeにするものです。
Fixpoint optimize_0plus (a:aexp) : aexp :=
match a 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.
この最適化が正しいことをすることを確認するために、
いくつかの例についてテストして出力がよさそうかを見てみることができます。
Example test_optimize_0plus:
optimize_0plus (APlus (ANum 2)
(APlus (ANum 0)
(APlus (ANum 0) (ANum 1))))
= APlus (ANum 2) (ANum 1).
Proof. reflexivity. Qed.
しかし、もし最適化が正しいことを確認したいならば、つまり、最適化した式がオリジナルの式と同じ評価結果を返すことを確認したいならば、証明すべきです。
Theorem optimize_0plus_sound: forall a,
aeval (optimize_0plus a) = aeval a.
Proof.
intros a. induction a.
- reflexivity.
- destruct a1 eqn:Ea1.
+ destruct n eqn:En.
* simpl. apply IHa2.
* simpl. rewrite IHa2. reflexivity.
+
simpl. simpl in IHa1. rewrite IHa1.
rewrite IHa2. reflexivity.
+
simpl. simpl in IHa1. rewrite IHa1.
rewrite IHa2. reflexivity.
+
simpl. simpl in IHa1. rewrite IHa1.
rewrite IHa2. reflexivity.
-
simpl. rewrite IHa1. rewrite IHa2. reflexivity.
-
simpl. rewrite IHa1. rewrite IHa2. reflexivity. Qed.
前の証明の最後の繰り返しはちょっと面倒です。
証明対象の言語や最適化の健全性証明が今に比べて著しく複雑だったら、現実的な問題になるでしょう。
ここまで、Coq のタクティックのほんのひとつかみだけですべての証明をしてきていて、証明を自動的に構成する非常に強力な機構を完全に無視してきました。
このセクションではこれらの機構のいくつかを紹介します。
それ以上のものを、以降のいくつかの章で次第に見ることになるでしょう。
それらに慣れるには多少エネルギーが必要でしょう。
Coq の自動化は電動工具です。--
しかし自動化機構を使うことで、より複雑な定義や、より興味深い性質について、退屈で繰り返しの多いローレベルな詳細に飲み込まれることなく、作業をスケールアップできます。
タクティカル(tactical)は Coq の用語で、他のタクティックを引数に取るタクティックのことです。
「高階タクティック(higher-order tactics)」と言っても良いでしょう。
Theorem silly1 : forall ae, aeval ae = aeval ae.
Proof. try reflexivity. Qed.
Theorem silly2 : forall (P : Prop), P -> P.
Proof.
intros P HP.
try reflexivity. apply HP. Qed.
There is no real reason to use try in completely manual
proofs like these, but it is very useful for doing automated
proofs in conjunction with the ; tactical, which we show
next.
一番よくある形として、; タクティカルは二つのタクティックを引数として取ります。
TとT'がタクティックのとき、T;T' はタクティックで、最初にTを行い、Tが生成した「サブゴールそれぞれ」にT'を行います。
例えば、次の簡単な補題を考えます:
Lemma foo : forall n, 0 <=? n = true.
Proof.
intros.
destruct n.
- simpl. reflexivity.
- simpl. reflexivity.
Qed.
上の証明を;タクティカルを使って簡単化できます。
tryと;の両方を使うと、ちょっと前に悩まされた証明の繰り返しを取り除くことができます。
Theorem optimize_0plus_sound': forall a,
aeval (optimize_0plus a) = aeval a.
Proof.
intros a.
induction a;
try (simpl; rewrite IHa1; rewrite IHa2; reflexivity).
- reflexivity.
-
destruct a1 eqn:Ea1;
try (simpl; simpl in IHa1; rewrite IHa1;
rewrite IHa2; reflexivity).
+ destruct n eqn:En;
simpl; rewrite IHa2; reflexivity. Qed.
Coqの専門家は、tryをinductionのようなタクティックと一緒に使うことで、多くの似たような「簡単な」場合を一度に処理します。
これは自然に非形式的な証明に対応します。
例えば、この定理の形式的な証明の構造にマッチする非形式的な証明は次の通りです:
「定理」: すべての算術式aについて
aeval (optimize_0plus a) = aeval a
「証明」: aについての帰納法を使う。
ほとんどの場合は帰納仮定から直ちに得られる。
残るのは以下の場合である:
aeval (optimize_0plus a) = aeval a
- あるnについて a = ANum n とする。示すべきことは次の通りである:
aeval (optimize_0plus (ANum n)) = aeval (ANum n) - あるa1とa2について a = APlus a1 a2 とする。
示すべきことは次の通りである:
aeval (optimize_0plus (APlus a1 a2)) = aeval (APlus a1 a2)
optimize_0plus (APlus a1 a2) = optimize_0plus a2
しかし、この証明はさらに改良できます。
最初の場合(a = ANum n のとき)はかなり自明です。
帰納仮定からすぐに得られるとわざわざ言う必要もないくらい自明でしょう。
それなのに完全に記述しています。
これを消して、単に最初に
「ほとんどの場合、すぐに、あるいは帰納仮定から直接得られる。興味深いのはAPlusの場合だけである...」
と言った方がより良く、より明快でしょう。
同じ改良を形式的な証明にも行うことができます。
以下のようになります:
Theorem optimize_0plus_sound'': forall a,
aeval (optimize_0plus a) = aeval a.
Proof.
intros a.
induction a;
try (simpl; rewrite IHa1; rewrite IHa2; reflexivity);
try reflexivity.
-
destruct a1; try (simpl; simpl in IHa1; rewrite IHa1;
rewrite IHa2; reflexivity).
+ destruct n;
simpl; rewrite IHa2; reflexivity. Qed.
The ; Tactical (General Form)
The repeat Tactical
The tactic repeat T never fails: if the tactic T doesn't apply
to the original goal, then repeat still succeeds without changing
the original goal (i.e., it repeats zero times).
Theorem In10' : In 10 [1;2;3;4;5;6;7;8;9;10].
Proof.
repeat (left; reflexivity).
repeat (right; try (left; reflexivity)).
Qed.
The tactic repeat T also does not have any upper bound on the
number of times it applies T. If T is a tactic that always
succeeds, then repeat T will loop forever (e.g., repeat simpl
loops, since simpl always succeeds). While evaluation in Coq's
term language, Gallina, is guaranteed to terminate, tactic
evaluation is not! This does not affect Coq's logical
consistency, however, since the job of repeat and other tactics
is to guide Coq in constructing proofs; if the construction
process diverges (i.e., it does not terminate), this simply means
that we have failed to construct a proof, not that we have
constructed a wrong one.
練習問題: ★★★, standard (optimize_0plus_b_sound)
Fixpoint optimize_0plus_b (b : bexp) : bexp
. Admitted.
Theorem optimize_0plus_b_sound : forall b,
beval (optimize_0plus_b b) = beval b.
Proof.
Admitted.
☐
練習問題: ★★★★, standard, optional (optimize)
Coqはまた、タクティックスクリプトを「プログラミングする」いろいろな方法も提供します。
Tactic Notation 機構は取り組むのに一番簡単で、多くの目的に十分なパワーを発揮します。
例を挙げます。
- Tactic Notation コマンドは、「略記法タクティック("shorthand tactics")」を定義する簡単な方法を提供します。
「略記法タクティック」は、呼ばれると、いろいろなタクティックを一度に適用します。
- より洗練されたプログラミングのために、CoqはLtacと呼ばれる小さな組込みの言語と、証明の状態を調べたり変更したりするためのLtacのプリミティブを提供します。
その詳細はここで説明するにはちょっと複雑過ぎます(しかも、LtacはCoqの設計の一番美しくない部分だというのが共通見解です!)。
しかし、詳細はリファレンスマニュアルにありますし、Coqの標準ライブラリには、読者が参考にできるLtacの定義のたくさんの例があります。
- Coqの内部構造のより深いレベルにアクセスする新しいタクティックを作ることができるOCaml API も存在します。 しかしこれは、普通のCoqユーザにとっては、苦労が報われることはほとんどありません。
Tactic Notation "simpl_and_try" tactic(c) :=
simpl;
try c.
これは1つのタクティックcを引数としてとるsimpl_and_tryという新しいタクティカルを定義しています。
simpl_and_try c はタクティック simpl; try c と同値です。
例えば、証明内で"simpl_and_try reflexivity."と書くことは「simpl; try reflexivity.」と書くことと同じでしょう。
omegaタクティックは「プレスバーガー算術」(Presburger arithmetic、「プレスブルガー算術」とも)と呼ばれる一階述語論理のサブセットの決定手続き(decision procedure)を実装します。
William Pugh が発明したOmegaアルゴリズム Pugh 1991 (Bib.v 参照)に基いています。
ゴールが以下の要素から構成された全称限量された論理式とします。以下の要素とは:
です。
このとき、omegaを呼ぶと、ゴールを解くか、失敗によりそのゴールが偽であることを表すか、いずれかになります。
(ただしゴールが上記の形「ではない」場合にもomegaは失敗します。)
Example silly_presburger_example : forall m n o p,
m + n <= n + o /\ o + 3 = p + 3 ->
m <= p.
Proof.
intros. omega.
Qed.
(Note the From Coq Require Import omega.Omega. at the top of
the file.)
最後に、役に立ちそうないろいろなタクティックをいくつか紹介します。
進むにしたがって、これら全てについてたくさんの例を見ることになります。
- clear H: 仮定Hをコンテキストから消去します。
- subst x: 変数xについて、コンテキストから仮定 x = e または e = x を発見し、xをコンテキストおよび現在のゴールのすべての場所でeに置き換え、この仮定を消去します。
- subst: x = e および e = x の形(xは変数)のすべての仮定を置換します。
- rename... into...: 証明コンテキストの仮定の名前を変更します。
例えば、コンテキストがxという名前の変数を含んでいるとき、rename x into y は、すべてのxの出現をyに変えます。
- assumption: ゴールにちょうどマッチする仮定Hをコンテキストから探そうとします。
発見されたときは apply H と同様に振る舞います。
- contradiction: Falseと同値の仮定Hをコンテキストから探そうとします。
発見されたときはゴールを解きます。
- constructor: 現在のゴールを解くのに使えるコンストラクタcを(現在の環境のInductiveによる定義から)探そうとします。 発見されたときは apply c と同様に振る舞います。
aevalとbevalをFixpointによって定義された関数として示しました。
評価について考える別の方法は、それを式と値との間の関係(relation)と見ることです。
この方法は、多くの場合Fixpointを用いたものより柔軟です。
この考えに立つと、算術式についてCoqのInductiveによる以下の定義が自然に出てきます...
Module aevalR_first_try.
Inductive aevalR : aexp -> nat -> Prop :=
| E_ANum n :
aevalR (ANum n) n
| E_APlus (e1 e2: aexp) (n1 n2: nat) :
aevalR e1 n1 ->
aevalR e2 n2 ->
aevalR (APlus e1 e2) (n1 + n2)
| E_AMinus (e1 e2: aexp) (n1 n2: nat) :
aevalR e1 n1 ->
aevalR e2 n2 ->
aevalR (AMinus e1 e2) (n1 - n2)
| E_AMult (e1 e2: aexp) (n1 n2: nat) :
aevalR e1 n1 ->
aevalR e2 n2 ->
aevalR (AMult e1 e2) (n1 * n2).
Module TooHardToRead.
Inductive aevalR : aexp -> nat -> Prop :=
| E_ANum n :
aevalR (ANum n) n
| E_APlus (e1 e2: aexp) (n1 n2: nat)
(H1 : aevalR e1 n1)
(H2 : aevalR e2 n2) :
aevalR (APlus e1 e2) (n1 + n2)
| E_AMinus (e1 e2: aexp) (n1 n2: nat)
(H1 : aevalR e1 n1)
(H2 : aevalR e2 n2) :
aevalR (AMinus e1 e2) (n1 - n2)
| E_AMult (e1 e2: aexp) (n1 n2: nat)
(H1 : aevalR e1 n1)
(H2 : aevalR e2 n2) :
aevalR (AMult e1 e2) (n1 * n2).
Instead, we've chosen to leave the hypotheses anonymous, just
giving their types. This style gives us less control over the
names that Coq chooses during proofs involving aevalR, but it
makes the definition itself quite a bit lighter.
Notation "e '\\' n"
:= (aevalR e n)
(at level 50, left associativity)
: type_scope.
End aevalR_first_try.
実際は、CoqにはaevalR自身の定義中でこの記法を使う方法があります。
これにより、e \\ n の形の主張を含む証明で、aevalR e nという形の定義に戻らなければならない状況にならずに済みます。
このためには、最初に記法を「予約」し、それから定義と、記法が何を意味するかの宣言とを一緒に行います。
Reserved Notation "e '\\' n" (at level 90, left associativity).
Inductive aevalR : aexp -> nat -> Prop :=
| E_ANum (n : nat) :
(ANum n) \\ n
| E_APlus (e1 e2 : aexp) (n1 n2 : nat) :
(e1 \\ n1) -> (e2 \\ n2) -> (APlus e1 e2) \\ (n1 + n2)
| E_AMinus (e1 e2 : aexp) (n1 n2 : nat) :
(e1 \\ n1) -> (e2 \\ n2) -> (AMinus e1 e2) \\ (n1 - n2)
| E_AMult (e1 e2 : aexp) (n1 n2 : nat) :
(e1 \\ n1) -> (e2 \\ n2) -> (AMult e1 e2) \\ (n1 * n2)
where "e '\\' n" := (aevalR e n) : type_scope.
Inference Rule Notation
(E_APlus) APlus e1 e2 \\ n1+n2
(E_ANum) ANum n \\ n
(E_APlus) APlus e1 e2 \\ n1+n2
(E_AMinus) AMinus e1 e2 \\ n1-n2
(E_AMult) AMult e1 e2 \\ n1*n2
Exercise: 1 star, standard, optional (beval_rules)
☐
評価の、関係による定義と関数による定義が一致することを証明するのは簡単です。
Theorem aeval_iff_aevalR : forall a n,
(a \\ n) <-> aeval a = n.
Proof.
split.
-
intros H.
induction H; simpl.
+
reflexivity.
+
rewrite IHaevalR1. rewrite IHaevalR2. reflexivity.
+
rewrite IHaevalR1. rewrite IHaevalR2. reflexivity.
+
rewrite IHaevalR1. rewrite IHaevalR2. reflexivity.
-
generalize dependent n.
induction a;
simpl; intros; subst.
+
apply E_ANum.
+
apply E_APlus.
apply IHa1. reflexivity.
apply IHa2. reflexivity.
+
apply E_AMinus.
apply IHa1. reflexivity.
apply IHa2. reflexivity.
+
apply E_AMult.
apply IHa1. reflexivity.
apply IHa2. reflexivity.
Qed.
さらにタクティカルを使うことで証明をより短くできます。
Theorem aeval_iff_aevalR' : forall a n,
(a \\ n) <-> aeval a = n.
Proof.
split.
-
intros H; induction H; subst; reflexivity.
-
generalize dependent n.
induction a; simpl; intros; subst; constructor;
try apply IHa1; try apply IHa2; reflexivity.
Qed.
Inductive bevalR: bexp -> bool -> Prop :=
.
Lemma beval_iff_bevalR : forall b bv,
bevalR b bv <-> beval b = bv.
Proof.
Admitted.
☐
算術式とブール式の評価の定義について、関数を使うか関係を使うかはほとんど趣味の問題です。
どちらでも問題ありません。
しかしながら、評価の定義として、関係による定義が関数による定義よりはるかに望ましい状況があります。
For example, suppose that we wanted to extend the arithmetic
operations with division:
Inductive aexp : Type :=
| ANum (n : nat)
| APlus (a1 a2 : aexp)
| AMinus (a1 a2 : aexp)
| AMult (a1 a2 : aexp)
| ADiv (a1 a2 : aexp).
Extending the definition of aeval to handle this new operation
would not be straightforward (what should we return as the result
of ADiv (ANum 5) (ANum 0)?). But extending aevalR is
straightforward.
Reserved Notation "e '\\' n"
(at level 90, left associativity).
Inductive aevalR : aexp -> nat -> Prop :=
| E_ANum (n : nat) :
(ANum n) \\ n
| E_APlus (a1 a2 : aexp) (n1 n2 : nat) :
(a1 \\ n1) -> (a2 \\ n2) -> (APlus a1 a2) \\ (n1 + n2)
| E_AMinus (a1 a2 : aexp) (n1 n2 : nat) :
(a1 \\ n1) -> (a2 \\ n2) -> (AMinus a1 a2) \\ (n1 - n2)
| E_AMult (a1 a2 : aexp) (n1 n2 : nat) :
(a1 \\ n1) -> (a2 \\ n2) -> (AMult a1 a2) \\ (n1 * n2)
| E_ADiv (a1 a2 : aexp) (n1 n2 n3 : nat) :
(a1 \\ n1) -> (a2 \\ n2) -> (n2 > 0) ->
(mult n2 n3 = n1) -> (ADiv a1 a2) \\ n3
where "a '\\' n" := (aevalR a n) : type_scope.
End aevalR_division.
Module aevalR_extended.
Or suppose that we want to extend the arithmetic operations by a
nondeterministic number generator any that, when evaluated, may
yield any number. (Note that this is not the same as making a
probabilistic choice among all possible numbers -- we're not
specifying any particular probability distribution for the
results, just saying what results are possible.)
Reserved Notation "e '\\' n" (at level 90, left associativity).
Inductive aexp : Type :=
| AAny
| ANum (n : nat)
| APlus (a1 a2 : aexp)
| AMinus (a1 a2 : aexp)
| AMult (a1 a2 : aexp).
Again, extending aeval would be tricky, since now evaluation is
not a deterministic function from expressions to numbers, but
extending aevalR is no problem...
Inductive aevalR : aexp -> nat -> Prop :=
| E_Any (n : nat) :
AAny \\ n
| E_ANum (n : nat) :
(ANum n) \\ n
| E_APlus (a1 a2 : aexp) (n1 n2 : nat) :
(a1 \\ n1) -> (a2 \\ n2) -> (APlus a1 a2) \\ (n1 + n2)
| E_AMinus (a1 a2 : aexp) (n1 n2 : nat) :
(a1 \\ n1) -> (a2 \\ n2) -> (AMinus a1 a2) \\ (n1 - n2)
| E_AMult (a1 a2 : aexp) (n1 n2 : nat) :
(a1 \\ n1) -> (a2 \\ n2) -> (AMult a1 a2) \\ (n1 * n2)
where "a '\\' n" := (aevalR a n) : type_scope.
End aevalR_extended.
At this point you maybe wondering: which style should I use by
default? In the examples we've just seen, relational definitions
turned out to be more useful than functional ones. For situations
like these, where the thing being defined is not easy to express
as a function, or indeed where it is not a function, there is no
real choice. But what about when both styles are workable?
One point in favor of relational definitions is that they can be
more elegant and easier to understand.
Another is that Coq automatically generates nice inversion and
induction principles from Inductive definitions.
On the other hand, functional definitions can often be more
convenient:
Furthermore, functions can be directly "extracted" from Gallina to
executable code in OCaml or Haskell.
Ultimately, the choice often comes down to either the specifics of
a particular situation or simply a question of taste. Indeed, in
large Coq developments it is common to see a definition given in
both functional and relational styles, plus a lemma stating that
the two coincide, allowing further proofs to switch from one point
of view to the other at will.
- Functions are by definition deterministic and defined on all arguments; for a relation we have to show these properties explicitly if we need them.
- With functions we can also take advantage of Coq's computation mechanism to simplify expressions during proofs.
Impの定義に戻りましょう。
次にしなければならないことは、算術式とブール式に変数を拡張することです。
話を単純にするため、すべての変数はグローバルで、数値だけを持つとしましょう。
状態
簡単にするため、どのようなプログラムも有限個の変数しか使いませんが、状態はすべての変数について定義されているものとします。
状態はメモリ中に格納されている全ての情報を持ちます。
Imp で書かれたプログラムでは全ての変数が自然数だけを格納することから、状態を文字列から nat への写像で表現できます。
このとき状態の初期値は0ということにしておきます。
複雑なプログラミング言語では、状態はより複雑な構造となるでしょう。
前に定義した算術式に、単にもう1つコンストラクタを追加することで変数を追加できます:
Inductive aexp : Type :=
| ANum (n : nat)
| AId (x : string)
| APlus (a1 a2 : aexp)
| AMinus (a1 a2 : aexp)
| AMult (a1 a2 : aexp).
Defining a few variable names as notational shorthands will make
examples easier to read:
Definition W : string := "W".
Definition X : string := "X".
Definition Y : string := "Y".
Definition Z : string := "Z".
Inductive bexp : Type :=
| BTrue
| BFalse
| BEq (a1 a2 : aexp)
| BLe (a1 a2 : aexp)
| BNot (b : bexp)
| BAnd (b1 b2 : bexp).
Notations
Coercion AId : string >-> aexp.
Coercion ANum : nat >-> aexp.
Definition bool_to_bexp (b : bool) : bexp :=
if b then BTrue else BFalse.
Coercion bool_to_bexp : bool >-> bexp.
Bind Scope imp_scope with aexp.
Bind Scope imp_scope with bexp.
Delimit Scope imp_scope with imp.
Notation "x + y" := (APlus x y) (at level 50, left associativity) : imp_scope.
Notation "x - y" := (AMinus x y) (at level 50, left associativity) : imp_scope.
Notation "x * y" := (AMult x y) (at level 40, left associativity) : imp_scope.
Notation "x <= y" := (BLe x y) (at level 70, no associativity) : imp_scope.
Notation "x = y" := (BEq x y) (at level 70, no associativity) : imp_scope.
Notation "x && y" := (BAnd x y) (at level 40, left associativity) : imp_scope.
Notation "'~' b" := (BNot b) (at level 75, right associativity) : imp_scope.
Definition example_aexp := (3 + (X * 2))%imp : aexp.
Definition example_bexp := (true && ~(X <= 4))%imp : bexp.
One downside of these coercions is that they can make it a little
harder for humans to calculate the types of expressions. If you
get confused, try doing Set Printing Coercions to see exactly
what is going on.
算術とブールの評価器は、状態を引数に追加するという自明な方法で変数を扱うように拡張できます:
Fixpoint aeval (st : state) (a : aexp) : nat :=
match a with
| ANum n => n
| AId x => st x
| APlus a1 a2 => (aeval st a1) + (aeval st a2)
| AMinus a1 a2 => (aeval st a1) - (aeval st a2)
| AMult a1 a2 => (aeval st a1) * (aeval st a2)
end.
Fixpoint beval (st : state) (b : bexp) : bool :=
match b with
| BTrue => true
| BFalse => false
| BEq a1 a2 => (aeval st a1) =? (aeval st a2)
| BLe a1 a2 => (aeval st a1) <=? (aeval st a2)
| BNot b1 => negb (beval st b1)
| BAnd b1 b2 => andb (beval st b1) (beval st b2)
end.
We specialize our notation for total maps to the specific case of
states, i.e. using (_ !-> 0) as empty state.
Now we can add a notation for a "singleton state" with just one
variable bound to a value.
Notation "a '!->' x" := (t_update empty_st a x) (at level 100).
Example aexp1 :
aeval (X !-> 5) (3 + (X * 2))%imp
= 13.
Proof. reflexivity. Qed.
Example bexp1 :
beval (X !-> 5) (true && ~(X <= 4))%imp
= true.
Proof. reflexivity. Qed.
Example aexp1 :
aeval (X !-> 5) (3 + (X * 2))%imp
= 13.
Proof. reflexivity. Qed.
Example bexp1 :
beval (X !-> 5) (true && ~(X <= 4))%imp
= true.
Proof. reflexivity. Qed.
さて、Imp コマンド(「文(statement)」と呼ばれることもあります)の構文と挙動を定義する準備が出来ました。
非形式的には、コマンド c は以下の BNF で表現されます。
c ::= SKIP | x ::= a | c ;; c | TEST b THEN c ELSE c FI | WHILE b DO c END(すこし不格好な具象構文を使っていますが、これはImpの構文をCoqのNotationの機能を使って記述するためです。 特に、TESTは標準ライブラリのifやIFとの衝突を避けてのものです。) 例えば、Imp における階乗の計算は以下のようになります。
Z ::= X;; Y ::= 1;; WHILE ~(Z = 0) DO Y ::= Y * Z;; Z ::= Z - 1 ENDこのコマンドが終わったとき、変数 Y は変数 X の初期値の階乗を持つでしょう。
以下に、コマンドの抽象構文の形式的定義を示します。
Inductive com : Type :=
| CSkip
| CAss (x : string) (a : aexp)
| CSeq (c1 c2 : com)
| CIf (b : bexp) (c1 c2 : com)
| CWhile (b : bexp) (c : com).
式と同様、Impプログラムを読み書きしやすくするために、いくつかの Notation 宣言が使えます。
Bind Scope imp_scope with 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' c1 'THEN' c2 'ELSE' c3 'FI'" :=
(CIf c1 c2 c3) (at level 80, right associativity) : imp_scope.
例えば先の階乗関数を Coq での形式的な定義として記述し直すと、以下のようになります。
Definition fact_in_coq : com :=
(Z ::= X;;
Y ::= 1;;
WHILE ~(Z = 0) DO
Y ::= Y * Z;;
Z ::= Z - 1
END)%imp.
Desugaring notations
- Unset Printing Notations (undo with Set Printing Notations)
- Set Printing Coercions (undo with Unset Printing Coercions)
- Set Printing All (undo with Unset Printing All)
Unset Printing Notations.
Print fact_in_coq.
Set Printing Notations.
Set Printing Coercions.
Print fact_in_coq.
Unset Printing Coercions.
Finding notations
Locate "&&".
Locate ";;".
Locate "WHILE".
Locate ";;".
Locate "WHILE".
Finding identifiers
Locate aexp.
代入:
Definition plus2 : com :=
X ::= X + 2.
Definition XtimesYinZ : com :=
Z ::= X * Y.
Definition subtract_slowly_body : com :=
Z ::= Z - 1 ;;
X ::= X - 1.
Definition subtract_slowly : com :=
(WHILE ~(X = 0) DO
subtract_slowly_body
END)%imp.
Definition subtract_3_from_5_slowly : com :=
X ::= 3 ;;
Z ::= 5 ;;
subtract_slowly.
次に、Imp のコマンドの実行が何を意味するかを定義する必要があります。
WHILE ループが終了しなくても良くするため、評価関数の定義を少し変わった形にしています ...
以下は WHILE 以外のコマンドの評価関数を定義しようとした、最初の試みです。
The following declaration is needed to be able to use the
notations in match patterns.
Open Scope imp_scope.
Fixpoint ceval_fun_no_while (st : state) (c : com)
: state :=
match c with
| SKIP =>
st
| x ::= a1 =>
(x !-> (aeval st a1) ; st)
| c1 ;; c2 =>
let st' := ceval_fun_no_while st c1 in
ceval_fun_no_while st' c2
| TEST b THEN c1 ELSE c2 FI =>
if (beval st b)
then ceval_fun_no_while st c1
else ceval_fun_no_while st c2
| WHILE b DO c END =>
st
end.
Close Scope imp_scope.
Fixpoint ceval_fun_no_while (st : state) (c : com)
: state :=
match c with
| SKIP =>
st
| x ::= a1 =>
(x !-> (aeval st a1) ; st)
| c1 ;; c2 =>
let st' := ceval_fun_no_while st c1 in
ceval_fun_no_while st' c2
| TEST b THEN c1 ELSE c2 FI =>
if (beval st b)
then ceval_fun_no_while st c1
else ceval_fun_no_while st c2
| WHILE b DO c END =>
st
end.
Close Scope imp_scope.
In a traditional functional programming language like OCaml or
Haskell we could add the WHILE case as follows:
Fixpoint ceval_fun (st : state) (c : com) : state :=
match c with
...
| WHILE b DO c END =>
if (beval st b)
then ceval_fun st (c ;; WHILE b DO c END)
else st
end.
Coq doesn't accept such a definition ("Error: Cannot guess
decreasing argument of fix") because the function we want to
define is not guaranteed to terminate. Indeed, it doesn't always
terminate: for example, the full version of the ceval_fun
function applied to the loop program above would never
terminate. Since Coq is not just a functional programming
language but also a consistent logic, any potentially
non-terminating function needs to be rejected. Here is
an (invalid!) program showing what would go wrong if Coq
allowed non-terminating recursive functions:
Fixpoint loop_false (n : nat) : False := loop_false n.
That is, propositions like False would become provable
(loop_false 0 would be a proof of False), which
would be a disaster for Coq's logical consistency.
Thus, because it doesn't terminate on all inputs,
of ceval_fun cannot be written in Coq -- at least not without
additional tricks and workarounds (see chapter ImpCEvalFun
if you're curious about what those might be).
これは重要な変更です。
厄介な回避策から解放されるばかりでなく、定義での柔軟性も与えてくれます。
例えば、もし言語にanyのような並行性の要素を導入したら、評価の定義を非決定的に書きたくなるでしょう。
つまり、それは全域でないだけでなく、そもそも関数ですらないかも知れないのです!
ceavl 関係に対する表記として st =[ c ]=> st' を使います。
st =[ c ]=> st' と書いたら、プログラム c を初期状態 st で評価すると、その結果は最終状態 st' になる、ということを意味します。
これは「c は状態 st を st' にする」と読みます。
評価の推論規則を読みやすく非形式的に書くと、以下のようになります。
----------------- (E_Skip) st =[ SKIP ]=> st aeval st a1 = n -------------------------------- (E_Ass) st =[ x := a1 ]=> (x !-> n ; st) st =[ c1 ]=> st' st' =[ c2 ]=> st'' --------------------- (E_Seq) st =[ c1;;c2 ]=> st'' beval st b1 = true st =[ c1 ]=> st' --------------------------------------- (E_IfTrue) st =[ TEST b1 THEN c1 ELSE c2 FI ]=> st' beval st b1 = false st =[ c2 ]=> st' --------------------------------------- (E_IfFalse) st =[ TEST b1 THEN c1 ELSE c2 FI ]=> st' beval st b = false ----------------------------- (E_WhileFalse) st =[ WHILE b DO c END ]=> st beval st b = true st =[ c ]=> st' st' =[ WHILE b DO c END ]=> st'' -------------------------------- (E_WhileTrue) st =[ WHILE b DO c END ]=> st''
以下に形式的な定義を挙げます。
上の推論規則とどのように対応するか、確認しておきましょう。
Reserved Notation "st '=[' c ']=>' st'"
(at level 40).
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').
評価を関数ではなく関係として定義することのコストは、あるプログラムを実行した結果がとある状態になる、というのを Coq の計算機構にやってもらう代わりに、その「証明」を構築する必要がある、ということです。
Example ceval_example1:
empty_st =[
X ::= 2;;
TEST X <= 1
THEN Y ::= 3
ELSE Z ::= 4
FI
]=> (Z !-> 4 ; X !-> 2).
Proof.
apply E_Seq with (X !-> 2).
-
apply E_Ass. reflexivity.
-
apply E_IfFalse.
reflexivity.
apply E_Ass. reflexivity.
Qed.
Example ceval_example2:
empty_st =[
X ::= 0;; Y ::= 1;; Z ::= 2
]=> (Z !-> 2 ; Y !-> 1 ; X !-> 0).
Proof.
Admitted.
empty_st =[
X ::= 0;; Y ::= 1;; Z ::= 2
]=> (Z !-> 2 ; Y !-> 1 ; X !-> 0).
Proof.
Admitted.
☐
Write an Imp program that sums the numbers from 1 to
X (inclusive: 1 + 2 + ... + X) in the variable Y.
Prove that this program executes as intended for X = 2
(this is trickier than you might expect).
Exercise: 3 stars, standard, optional (pup_to_n)
Definition pup_to_n : com
. Admitted.
Theorem pup_to_2_ceval :
(X !-> 2) =[
pup_to_n
]=> (X !-> 0 ; Y !-> 3 ; X !-> 1 ; Y !-> 2 ; Y !-> 0 ; X !-> 2).
Proof.
Admitted.
☐
評価の定義を計算的なものから関係的なものに変更するのはとても良いことです。
というのも、評価が全関数でなければならないという不自然な要求から解放されるからです。
しかしこの変更は新たな疑問を生みます。
2つ目の評価の定義は本当に部分関数なのか?
それとも、同じ状態 st から始めて、あるコマンド c を違った方法で評価し、2つの異なる出力状態 st' と st'' に至るのが可能なのか?
というものです。
実際には、こうなることはありません。
評価関係 ceval は確かに部分関数です。
Theorem ceval_deterministic: forall c st st1 st2,
st =[ c ]=> st1 ->
st =[ c ]=> st2 ->
st1 = st2.
Proof.
intros c st st1 st2 E1 E2.
generalize dependent st2.
induction E1;
intros st2 E2; inversion E2; subst.
- reflexivity.
- reflexivity.
-
assert (st' = st'0) as EQ1.
{ apply IHE1_1; assumption. }
subst st'0.
apply IHE1_2. assumption.
-
apply IHE1. assumption.
-
rewrite H in H5. discriminate H5.
-
rewrite H in H5. discriminate H5.
-
apply IHE1. assumption.
-
reflexivity.
-
rewrite H in H2. discriminate H2.
-
rewrite H in H4. discriminate H4.
-
assert (st' = st'0) as EQ1.
{ apply IHE1_1; assumption. }
subst st'0.
apply IHE1_2. assumption. Qed.
「プログラミング言語の基礎」では、Imp におけるプログラムの推論手法としてより強力で系統だったテクニックに触れていきます。
しかし、そのままの定義を用いてもある程度は推論できます。
いくつかの例を見ていきましょう。
Theorem plus2_spec : forall st n st',
st X = n ->
st =[ plus2 ]=> st' ->
st' X = n + 2.
Proof.
intros st n st' HX Heval.
Inverting Heval essentially forces Coq to expand one step of
the ceval computation -- in this case revealing that st'
must be st extended with the new value of X, since plus2
is an assignment.
☐
Theorem loop_never_stops : forall st st',
~(st =[ loop ]=> st').
Proof.
intros st st' contra. unfold loop in contra.
remember (WHILE true DO SKIP END)%imp as loopdef
eqn:Heqloopdef.
~(st =[ loop ]=> st').
Proof.
intros st st' contra. unfold loop in contra.
remember (WHILE true DO SKIP END)%imp as loopdef
eqn:Heqloopdef.
Proceed by induction on the assumed derivation showing that
loopdef terminates. Most of the cases are immediately
contradictory (and so can be solved in one step with
discriminate).
Admitted.
☐
Open Scope imp_scope.
Fixpoint no_whiles (c : com) : bool :=
match c with
| SKIP =>
true
| _ ::= _ =>
true
| c1 ;; c2 =>
andb (no_whiles c1) (no_whiles c2)
| TEST _ THEN ct ELSE cf FI =>
andb (no_whiles ct) (no_whiles cf)
| WHILE _ DO _ END =>
false
end.
Close Scope imp_scope.
この性質はプログラムが while ループを含まない場合 true を返します。
Inductive を使って c が while ループのないプログラムのとき、かつそのときに限り no_whilesR c が証明可能となる性質 no_whilesR を書きなさい。
さらに、それが no_whiles と等価であることを示しなさい。
Inductive no_whilesR: com -> Prop :=
.
Theorem no_whiles_eqv:
forall c, no_whiles c = true <-> no_whilesR c.
Proof.
Admitted.
☐
練習問題: ★★★★, standard (no_whiles_terminating)
☐
練習問題: ★★★, standard (stack_compiler)
(2*3)+(3*(4-2))という式は
2 3 * 3 4 2 - * +と記述され、以下のように実行されるでしょう: (処理されるプログラムを右に、スタックの状態を左に書いています。)
[ ] | 2 3 * 3 4 2 - * + [2] | 3 * 3 4 2 - * + [3, 2] | * 3 4 2 - * + [6] | 3 4 2 - * + [3, 6] | 4 2 - * + [4, 3, 6] | 2 - * + [2, 4, 3, 6] | - * + [2, 3, 6] | * + [6, 6] | + [12] |この練習問題の目標は、aexp をスタック機械の命令列に変換する小さなコンパイラを書き、その正当性を証明することです。
スタック言語のプログラムを評価するための関数を書きなさい。
関数は入力として、状態、数のリストで表現されたスタック(スタックの先頭要素はリストの先頭)、および命令のリストで表現されたプログラムを受け取り、受け取ったプログラムの実行後のスタックを返す。
下にある例で、その関数のテストをしなさい。
上の仕様では、スタックが 2 つ未満の要素しか含まずに SPlus や SMinus、SMult 命令に至った場合を明示していないままなことに注意しましょう。
我々のコンパイラはそのような不正な形式のプログラムは生成しないので、ある意味でこれは私たちがすることと無関係です。
Fixpoint s_execute (st : state) (stack : list nat)
(prog : list sinstr)
: list nat
. Admitted.
Example s_execute1 :
s_execute empty_st []
[SPush 5; SPush 3; SPush 1; SMinus]
= [2; 5].
Admitted.
Example s_execute2 :
s_execute (X !-> 3) [3;4]
[SPush 4; SLoad X; SMult; SPlus]
= [15; 4].
Admitted.
次に、aexp をスタック機械のプログラムにコンパイルする関数を書きなさい。
このプログラムを実行した後の状態は、単に式の評価結果をスタックに積んだのと同じでなければなりません。
After you've defined s_compile, prove the following to test
that it works.
Example s_compile1 :
s_compile (X - (2 * Y))%imp
= [SLoad X; SPush 2; SLoad Y; SMult; SMinus].
Admitted.
☐
練習問題: ★★★★, advanced (stack_compiler_correct)
Theorem s_compile_correct : forall (st : state) (e : aexp),
s_execute st [] (s_compile e) = [ aeval st e ].
Proof.
Admitted.
☐
Most modern programming languages use a "short-circuit" evaluation
rule for boolean and: to evaluate BAnd b1 b2, first evaluate
b1. If it evaluates to false, then the entire BAnd
expression evaluates to false immediately, without evaluating
b2. Otherwise, b2 is evaluated to determine the result of the
BAnd expression.
Write an alternate version of beval that performs short-circuit
evaluation of BAnd in this manner, and prove that it is
equivalent to beval. (N.b. This is only true because expression
evaluation in Imp is rather simple. In a bigger language where
evaluating an expression might diverge, the short-circuiting BAnd
would not be equivalent to the original, since it would make more
programs terminate.)
Exercise: 3 stars, standard, optional (short_circuit)
Exercise: 4 stars, advanced (break_imp)
Inductive com : Type :=
| CSkip
| CBreak
| CAss (x : string) (a : aexp)
| CSeq (c1 c2 : com)
| CIf (b : bexp) (c1 c2 : com)
| CWhile (b : bexp) (c : com).
Notation "'SKIP'" :=
CSkip.
Notation "'BREAK'" :=
CBreak.
Notation "x '::=' a" :=
(CAss x a) (at level 60).
Notation "c1 ;; c2" :=
(CSeq c1 c2) (at level 80, right associativity).
Notation "'WHILE' b 'DO' c 'END'" :=
(CWhile b c) (at level 80, right associativity).
Notation "'TEST' c1 'THEN' c2 'ELSE' c3 'FI'" :=
(CIf c1 c2 c3) (at level 80, right associativity).
Next, we need to define the behavior of BREAK. Informally,
whenever BREAK is executed in a sequence of commands, it stops
the execution of that sequence and signals that the innermost
enclosing loop should terminate. (If there aren't any
enclosing loops, then the whole program simply terminates.) The
final state should be the same as the one in which the BREAK
statement was executed.
One important point is what to do when there are multiple loops
enclosing a given BREAK. In those cases, BREAK should only
terminate the innermost loop. Thus, after executing the
following...
X ::= 0;;
Y ::= 1;;
WHILE ~(0 = Y) DO
WHILE true DO
BREAK
END;;
X ::= 1;;
Y ::= Y - 1
END
... the value of X should be 1, and not 0.
One way of expressing this behavior is to add another parameter to
the evaluation relation that specifies whether evaluation of a
command executes a BREAK statement:
Inductive result : Type :=
| SContinue
| SBreak.
Reserved Notation "st '=[' c ']=>' st' '/' s"
(at level 40, st' at next level).
Intuitively, st =[ c ]=> st' / s means that, if c is started in
state st, then it terminates in state st' and either signals
that the innermost surrounding loop (or the whole program) should
exit immediately (s = SBreak) or that execution should continue
normally (s = SContinue).
The definition of the "st =[ c ]=> st' / s" relation is very
similar to the one we gave above for the regular evaluation
relation (st =[ c ]=> st') -- we just need to handle the
termination signals appropriately:
Based on the above description, complete the definition of the
ceval relation.
- If the command is SKIP, then the state doesn't change and
execution of any enclosing loop can continue normally.
- If the command is BREAK, the state stays unchanged but we
signal a SBreak.
- If the command is an assignment, then we update the binding for
that variable in the state accordingly and signal that execution
can continue normally.
- If the command is of the form TEST b THEN c1 ELSE c2 FI, then
the state is updated as in the original semantics of Imp, except
that we also propagate the signal from the execution of
whichever branch was taken.
- If the command is a sequence c1 ;; c2, we first execute
c1. If this yields a SBreak, we skip the execution of c2
and propagate the SBreak signal to the surrounding context;
the resulting state is the same as the one obtained by
executing c1 alone. Otherwise, we execute c2 on the state
obtained after executing c1, and propagate the signal
generated there.
- Finally, for a loop of the form WHILE b DO c END, the semantics is almost the same as before. The only difference is that, when b evaluates to true, we execute c and check the signal that it raises. If that signal is SContinue, then the execution proceeds as in the original semantics. Otherwise, we stop the execution of the loop, and the resulting state is the same as the one resulting from the execution of the current iteration. In either case, since BREAK only terminates the innermost loop, WHILE signals SContinue.
Inductive ceval : com -> state -> result -> state -> Prop :=
| E_Skip : forall st,
st =[ CSkip ]=> st / SContinue
where "st '=[' c ']=>' st' '/' s" := (ceval c st s st').
Now prove the following properties of your definition of ceval:
Theorem break_ignore : forall c st st' s,
st =[ BREAK;; c ]=> st' / s ->
st = st'.
Proof.
Admitted.
Theorem while_continue : forall b c st st' s,
st =[ WHILE b DO c END ]=> st' / s ->
s = SContinue.
Proof.
Admitted.
Theorem while_stops_on_break : forall b c st st',
beval st b = true ->
st =[ c ]=> st' / SBreak ->
st =[ WHILE b DO c END ]=> st' / SContinue.
Proof.
Admitted.
Theorem while_break_true : forall b c st st',
st =[ WHILE b DO c END ]=> st' / SContinue ->
beval st' b = true ->
exists st'', st'' =[ c ]=> st' / SBreak.
Proof.
Admitted.
st =[ WHILE b DO c END ]=> st' / SContinue ->
beval st' b = true ->
exists st'', st'' =[ c ]=> st' / SBreak.
Proof.
Admitted.
Theorem ceval_deterministic: forall (c:com) st st1 st2 s1 s2,
st =[ c ]=> st1 / s1 ->
st =[ c ]=> st2 / s2 ->
st1 = st2 /\ s1 = s2.
Proof.
Admitted.
st =[ c ]=> st1 / s1 ->
st =[ c ]=> st2 / s2 ->
st1 = st2 /\ s1 = s2.
Proof.
Admitted.
☐