References: 変更可能な参照の型付け
ここまでは、いろいろな「純粋な」(pure)言語機能を考えてきました。
関数抽象、数値やブール値などの基本型、レコードやバリアントのような構造型などです。
これらの機能はほとんどのプログラミング言語のバックボーンを構成しています。
その言語の中にはHaskellのような純粋な関数型言語、MLのような「ほとんど関数型の("mostly functional")」言語、Cのような命令型言語、JavaやC#、Scalaのようなオブジェクト指向言語を含みます。
ほとんどの実際のプログラミング言語は、ここまで使ってきた単純な意味論の枠組みでは記述できない様々な「不純な(impure)」機能も持っています。
特に、これらの言語では項を評価することで、単に結果を得る他に、変更可能な変数(あるいは参照セル、配列、変更可能なレコードフィールド、等)に代入したり、ファイルや画面やネットワークに入出力したり、
例外やジャンプ、継続によってローカルな枠を越えて制御を移したり、プロセス間の同期や通信を行ったりします。
プログラミング言語についての文献では、これらの計算の副作用("side effects")はより一般に計算作用(computational effects)と参照されます。
この章では、ここまで学習してきた計算体系に一つの計算作用「変更可能な参照」を追加する方法を見ます。
主要な拡張は、「記憶(store、あるいはヒープ(heap))」、そして位置を表す「ポインタ(pointer)」を明示的に扱うことです。
この拡張は直接的に定義できます。
一番興味深い部分は、型保存定理の主張のために必要なリファインメント(refinement)です。
Set Warnings "-notation-overridden,-parsing".
From Coq Require Import Strings.String.
From Coq Require Import Arith.Arith.
From Coq Require Import omega.Omega.
From PLF Require Import Maps.
From PLF Require Import Smallstep.
From Coq Require Import Lists.List.
Import ListNotations.
ほとんどすべてのプログラミング言語が、記憶に以前に置かれた内容を変更する何らかの代入操作を持っています。
(Coqの内部言語Gallinaは稀な例外です!)
いくつかの言語(特にMLやその親戚)では、名前束縛の機構と代入の機構を区別しています。
「値」として数値5を持つ変数xを持つことも、現在の内容が5である変更可能なセルへの参照(reference)またはポインタ(pointer)を値とする変数yを持つこともできます。
この2つは別のものです。プログラマにも両者の違いは見ることができます。
xと別の数を足すことは可能ですが、それをxに代入することはできません。
yを使って、yが指すセルに別の値を代入することが(y:=84 と書くことで)できます。
しかし、yを+のような操作の引数として直接使うことはできません。
その代わり、現在の内容を得るために明示的に参照を手繰る(dereference、逆参照する)ことが必要です。
これを!yと書きます。
他のほとんどの言語、特にJavaを含むCファミリーのメンバーのすべてでは、すべての変数名は変更可能なセルを指します。
そして、現在の値を得るための変数の逆参照操作は暗黙に行われます。
形式的な学習の目的には、この2つの機構を分離しておいた方が便利です。
この章の進行は、MLのやり方にほとんど従います。
ここでやったことをCのような言語に適用するのは、分離していたものを一緒にすることと、逆参照のような操作を明示的なものから暗黙のものにするという単純な問題です。
この章では、自然数を持つ単純型付きラムダ計算に変更可能な参照を追加すること学習します。
参照についての基本操作はアロケート(allocation)、逆参照(dereferencing)、代入(assignment)です。
自然数の上の単純型付きラムダ計算から始めます。
基本の自然数型と関数型に加えて、参照を扱うために2つの型を追加する必要があります。
第一に「Unit型」です。これは代入演算子の結果の型として使います。
それから参照型(reference types)を追加します。
変数、関数抽象、関数適用、自然数に関する項、unitの他に、変更可能な参照を扱うために4種類の項を追加する必要があります:
t ::= ... Terms | ref t allocation | !t dereference | t := t assignment | l location
Inductive tm : Type :=
| var : string -> tm
| app : tm -> tm -> tm
| abs : string -> ty -> tm -> tm
| const : nat -> tm
| scc : tm -> tm
| prd : tm -> tm
| mlt : tm -> tm -> tm
| test0 : tm -> tm -> tm -> tm
| unit : tm
| ref : tm -> tm
| deref : tm -> tm
| assign : tm -> tm -> tm
| loc : nat -> tm.
直観的には...
非形式的な例では、MoreStlc章で行ったSTLCの拡張も自由に使います。
しかし、証明を小さく保つため、ここでそれらを再度形式化することに煩わされることはしません。
(それ自体はやろうと思えば簡単です。
なぜなら、それらの拡張と参照とには興味深い相互作用はないからです。)
非形式的には、アロケーション、逆参照、代入の型付け規則は以下のようになります:
Gamma |- t1 : T1 ------------------------ (T_Ref) Gamma |- ref t1 : Ref T1 Gamma |- t1 : Ref T11 --------------------- (T_Deref) Gamma |- !t1 : T11 Gamma |- t1 : Ref T11 Gamma |- t2 : T11 ------------------------ (T_Assign) Gamma |- t1 := t2 : Unit場所についての規則はもう少し仕掛けが必要になり、それが他の規則にいくらかの変更を求めることになります。 これについては後にまた戻ってきます。
関数抽象と数値に加えて、新たに2種類の値を持ちます:
unit値と場所です。
Inductive value : tm -> Prop :=
| v_abs : forall x T t,
value (abs x T t)
| v_nat : forall n,
value (const n)
| v_unit :
value unit
| v_loc : forall l,
value (loc l).
Hint Constructors value.
新しい項の構文を扱うための置換の拡張は直接的です。
Fixpoint subst (x:string) (s:tm) (t:tm) : tm :=
match t with
| var x' =>
if eqb_string x x' then s else t
| app t1 t2 =>
app (subst x s t1) (subst x s t2)
| abs x' T t1 =>
if eqb_string x x' then t else abs x' T (subst x s t1)
| const n =>
t
| scc t1 =>
scc (subst x s t1)
| prd t1 =>
prd (subst x s t1)
| mlt t1 t2 =>
mlt (subst x s t1) (subst x s t2)
| test0 t1 t2 t3 =>
test0 (subst x s t1) (subst x s t2) (subst x s t3)
| unit =>
t
| ref t1 =>
ref (subst x s t1)
| deref t1 =>
deref (subst x s t1)
| assign t1 t2 =>
assign (subst x s t1) (subst x s t2)
| loc _ =>
t
end.
Notation "'[' x ':=' s ']' t" := (subst x s t) (at level 20).
代入式の結果としてつまらない値unitを選んだことから、順次処理(sequencing)のうまい略記が使えます。
例えば、
なお、もし2つ目の式もまた代入ならば、2つの式の列全体の型がUnitになります。
これから、より長い代入の列を作るために別の;の左側に置いても問題ありません:
(\x:Unit. !r) (r := succ(!r))の略記として
r:=succ(!r); !rと書くことができます。 これは2つの式を順番に評価するという作用を持ち、2つ目の式の値を返します。 1つ目の式の型をUnitに限定することで、1つ目の値を捨てることができるのは本当にそれがつまらない値であることが保証されているときだけになり、型チェッカで馬鹿なエラーをチェックするのに役立ちます。
r:=succ(!r); r:=succ(!r); r:=succ(!r); r:=succ(!r); !r形式的には、順次処理 tseq を「派生形(derived form)」として導入します。 この派生形は、関数抽象と関数適用に展開されます。
rなどの変数に束縛される参照(reference)と、この参照によって指されているセル(cell)の違いを心に留めておく必要があります。
例えばrを別の変数sに束縛することでrのコピーを作るとすると、コピーされるのは参照だけで、セルの中身自身ではありません。
例えば、次の式を簡約します:
別名を付けられる能力があることによって、参照を持つプログラムに関する推論は、きわめてトリッキーになります。
例えば、式
let r = ref 5 in let s = r in s := 82; (!r)+1するとその後でrによって参照されたセルは値82を格納している状態になります。 一方、式全体の結果は83になります。参照rとsは同じセルの別名(aliases)と言われます。
r := 5; r := !sはrに5を代入し、直ぐにそれをsの現在の値で上書きします。 これは、単一の代入
r := !sと完全に同じ作用をします。 ただし、「rとsがたまたま同じセルの別名であるという状況でない限り」です!
もちろん、別名も、参照を便利なものにする大きな部分です。
特に参照は、プログラムの異なる部分の間の暗黙の通信チャンネル("implicit communication channels")、つまり共有状態(shared state)としてはたらきます。
例えば、参照セルと、その内容を扱う2つの関数を定義するとします:
let c = ref 0 in let incc = \_:Unit. (c := succ (!c); !c) in let decc = \_:Unit. (c := pred (!c); !c) in ...
ここで、それぞれ引数の型はUnitなので、inccとdeccの定義において関数抽象を適用する引数は関数本体にとって何も意味がないことに注意します(束縛変数名にワイルドカード_を使っているのは、このことを意図したものです)。
そうではなく、関数抽象の目的は関数本体の実行を「遅く」するためです。
関数抽象は値であることから、2つのletは単に2つの関数を名前inccとdeccに束縛するだけで、実際にcを増やしたり減らしたりはしません。
後に、これらの関数の1つを呼び出すたびに、その本体が1度実行されcについて対応する変更が行われます。
こういった関数はしばしば thunk と呼ばれます。
これらの宣言のコンテキストで、inccを呼ぶとcが変更されますが、これはdeccを呼ぶことで確認できます。
例えば ... を (incc unit; incc unit; decc unit) に換えると、プログラム全体の結果は1になります。
もう一歩進んで、c、incc、deccを生成し、inccとdeccをレコードにパッケージ化し、このレコードを返す「関数」を記述することもできます:
newcounter = \_:Unit. let c = ref 0 in let incc = \_:Unit. (c := succ (!c); !c) in let decc = \_:Unit. (c := pred (!c); !c) in {i=incc, d=decc}
このとき、newcounterを呼ぶたびに、
同じ記憶セルcのアクセスを共有する2つの関数の新たなレコードが得られます。
newcounterを呼び出す側はこの記憶セルには直接手が届きませんが、2つの関数を呼ぶことで間接的に影響を及ぼすことができます。
言い換えると、簡単な形のオブジェクト(object)を作ったのです。
let c1 = newcounter unit in let c2 = newcounter unit in // ここで2つの別個の記憶セルをアロケートしたことに注意! let r1 = c1.i unit in let r2 = c2.i unit in r2 // 1 を返します。2ではありません!
参照セルの中身は数値でなければならないわけではありません。
上で定義したプリミティブによって、任意の型の値への参照を作ることができます。
その任意の型の中には関数型も含まれます。
例えば、関数への参照を使って、数値の配列の(非効率的な)実装をすることができます。
以下の通りです。型 Ref (Nat->Nat) を NatArray と書きます。
MoreStlc章でのequal関数を思い出してください:
equal = fix (\eq:Nat->Nat->Bool. \m:Nat. \n:Nat. if m=0 then iszero n else if n=0 then false else eq (pred m) (pred n))このとき、新しい配列を作るために、参照セルをアロケートし、そのセルに関数を入れます。 その関数はインデックスを与えられると常に0を返します。
newarray = \_:Unit. ref (\n:Nat.0)配列の要素をとりだすためには、その関数を求められたインデックスに適用するだけです。
lookup = \a:NatArray. \n:Nat. (!a) nこのエンコードの興味深いところはupdate関数です。 update関数は、配列、インデックス、そのインデックスの場所に格納する新しい値をとり、新しい関数を生成し(そしてそれを参照に格納し)ます。 その関数は、この特定のインデックスの値を尋かれたときにはupdateに与えられた新しい値を返します。 他のインデックスについては、以前にその参照に格納されていた関数にまかせます。
update = \a:NatArray. \m:Nat. \v:Nat. let oldf = !a in a := (\n:Nat. if equal m n then v else oldf n);別の参照を含む値への参照もまたとても有用です。 これにより、変更可能なリストや木などのデータ構造が定義できるようになります。
練習問題: ★★, standard, recommended (compact_update)
update = \a:NatArray. \m:Nat. \v:Nat. a := (\n:Nat. if equal m n then v else (!a) n)これは前の定義と同じように振る舞うでしょうか?
☐
ここで定義した参照と、C言語スタイルの変更可能な変数には最後にもう一つ大きな違いがあります。
Cのような言語では、ヒープへのポインタを持つ変数は値NULLを持つことがあります。
そのような「nullポインタ」の逆参照はエラーで、結果として例外を発生したり(JavaとC#)、偶然任せの場合によっては安全でないような挙動をしたり(CやC++など)します。
nullポインタはC似の言語では大きな問題を引き起こします。
どのポインタもnullになり得るため、実行時の逆参照は常に失敗する可能性を秘めているためです。
MLのような言語でも、時には正しいポインタでないこともあるような場面は発生し得ます。
幸い、参照の基本メカニズムを拡張しなくてもこれは実現できます。
MoreStlc章で導入された直和型によってそれが可能になります。
最初に、直和を使って、「論理の基礎」のLists章で導入したoption型に対応するものを構築します。
Option T を Unit + T の略記法として定義します。
すると、「nullになり得るTへの参照」は単に型 Option (Ref T) の要素となります。
参照の形式化に移る前に述べておくべき最後の問題が、記憶のデアロケーション(de-allocation)です。
参照セルが必要なくなったときにそれを解放する何らかのプリミティブを提供していません。
その代わり、多くの近代的な言語(MLとJavaを含む)のように、実行時システムがガベージコレクション(garbage collection)を行うことに頼っています。
ガベージコレクションはプログラムから到達しなくなったセルを自動で判定し再利用するものです。
これは言語デザインの上で単なる趣味の問題ではありません。
明示的なデアロケーション操作が存在した場合、型安全性を保つのが極度に困難になるのです。
その理由の一つはよく知られたダングリング参照(dangling reference)問題です。
数値を持つセルをアロケートし、何かのデータ構造にそれへの参照を持たせ、それをしばらく利用し、そしてそれをデアロケートし、ブール値を持つ新しいセルをアロケートします。
このとき同じ記憶が再利用されるかもしれません。
すると、同じ記憶セルに2つの名前があることになります。
1つは Ref Nat 型で、もう1つは Ref Bool 型です。
☐
参照の扱いについての一番巧妙な面は、操作的振る舞いをどのように形式化するかを考えるときに現れます。
それが何故かを見る1つの方法は、「何が型 Ref T の値であるべきか?」と問うことです。
考慮すべき重要な点は、ref演算子の評価は何かを(つまり記憶のアロケートを)「行わ」なければならず、操作の結果はこの記憶への参照とならなければならないということです。
それでは、参照とは何でしょうか?
ほとんどのプログラミング言語の実装では、実行時の記憶は本質的には大きなバイト(byte)列です。
実行時システムは、この配列のどの部分が現在使用されているかを常に監視しています。
新しい参照セルをアロケートしなければならないとき、記憶の自由範囲から十分な大きさのセグメント(整数セルには4バイト、Floatのセルには8バイト、等)をアロケートします。
このセグメントが「使用中」であることをどこかに保存して、新しくアロケートされた領域のインデックス(典型的には32ビットまたは64ビットの整数)を返却します。
参照とはこのインデックスのことです。
現在の目的のためには、これほど具体的である必要はありません。
記憶を、バイトではなく「値」の配列と考え、異なる値の実行時表現のサイズの違いは捨象します。
すると参照は、単に記憶へのインデックスになります。
(望むならば、これらのインデックスが数値であるという事実さえも捨象することができます。
しかし、Coqでの形式化には、数値を使った方が便利です。)
この抽象度であることを強調するため、これ以降、用語として「参照」や「ポインタ」の代わりに「場所(location)」を使います。
この方法で場所を抽象的に扱うことで、Cのような低レベルの言語で見られる「ポインタ算術(pointer arithmetic)」をモデル化することができなくなります。
この制約は意図的なものです。
ポインタ算術は時には非常に便利です。
特にガベージコレクタのような低レベルのサービスを実装するときなどです。
しかし、ほとんどの型システムではポインタ算術を追跡することができません。
記憶の場所nにfloatが格納されていることを知っても、場所n+4の型について何も意味のあることはわかりません。
Cにおいては、ポインタ算術は型安全性破壊の悪名高き温床です。
IMPのスモールステップ操作的意味論では、ステップ関係は、実行されるプログラムに加えて補助的な状態を持ちまわる必要があったことを思い出して下さい。
同様に、STLCに参照セルを追加すると、参照セルの内容を追跡するために記憶を持ち回る必要が出てきます。
IMPの状態で使ったのと同じ関数表現を再利用することもできます。
しかし、この章での証明をするためには、記憶を単に値の「リスト」として表現した方が実際は便利です。
(この表現を以前には使わなかった理由は、IMPでは、プログラムはいつでも任意の場所を変更することができるので、状態はいつでも任意の変数を値に写像することができるようになっていないといけなかったからです。
しかし、STLCに参照を追加したものでは、参照セルを作る唯一の方法は ref t1 を使うことです。
ここで ref t1 はt1の値を新しい参照セルに置き、簡約結果として新しく生成された参照セルの場所を返します。
この式を簡約するときには、記憶を表現するリストの最後に新しい参照セルを追加すればよいのです。)
記憶stの場所nのセルの値をとりだすために、
store_lookup n st を使います。
インデックスが大きすぎるときにはnthにデフォルト値を与えなければならないことに注意します。
(実際には大きすぎるインデックスを与えることは行いません。
ただ、絶対にしないということを証明することはそれなりに作業をしなければなりません。)
記憶を更新するために、replace関数を使います。
この関数は特定のインデックスのセルの中身を置き換えます。
Fixpoint replace {A:Type} (n:nat) (x:A) (l:list A) : list A :=
match l with
| nil => nil
| h :: t =>
match n with
| O => x :: t
| S n' => h :: replace n' x t
end
end.
想像しているとは思いますが、replaceについてもいくつか補題が必要です。
証明はそのままです。
Lemma replace_nil : forall A n (x:A),
replace n x nil = nil.
Proof.
destruct n; auto.
Qed.
Lemma length_replace : forall A n x (l:list A),
length (replace n x l) = length l.
Proof with auto.
intros A n x l. generalize dependent n.
induction l; intros n.
destruct n...
destruct n...
simpl. rewrite IHl...
Qed.
Lemma lookup_replace_eq : forall l t st,
l < length st ->
store_lookup l (replace l t st) = t.
Proof with auto.
intros l t st.
unfold store_lookup.
generalize dependent l.
induction st as [|t' st']; intros l Hlen.
-
inversion Hlen.
-
destruct l; simpl...
apply IHst'. simpl in Hlen. omega.
Qed.
Lemma lookup_replace_neq : forall l1 l2 t st,
l1 <> l2 ->
store_lookup l1 (replace l2 t st) = store_lookup l1 st.
Proof with auto.
unfold store_lookup.
induction l1 as [|l1']; intros l2 t st Hneq.
-
destruct st.
+ rewrite replace_nil...
+ destruct l2... contradict Hneq...
-
destruct st as [|t2 st2].
+ destruct l2...
+
destruct l2...
simpl; apply IHl1'...
Qed.
次に、操作的意味を記憶を考慮した形に拡張します。
式を簡約した結果は一般には簡約したときの記憶の中身に依存するので、簡約規則は項だけでなく記憶も引数としてとらなければなりません。
さらに、項の評価は記憶に副作用を起こし、それが後の別の項の評価に影響を及ぼすので、簡約規則は新しい記憶を返す必要があります。
これから、1ステップ評価関係の形を t --> t' から t / st --> t' / st' に変えなければなりません。
ここでstとst'は記憶の開始状態と終了状態です。
この変更を達成するため、最初に、既存のすべての簡約規則に記憶を追加する必要があります:
さて、ref式の簡約結果は新しい場所です。これが項の構文と値の集合に場所を含めた理由です。
これは重要なことですが、項の構文のこの拡張は、「プログラマ」が直接、具体的場所を含む項を書くことを意図したものではありません。
そのような項は簡約の中間結果だけに現れます。
これは奇妙に見えるかもしれません。
しかしこの考え方は、簡約のすべてのステップの結果を項の変形で表現するという設計判断から自然に得られます。
もし、より「機械に近い("machine-like")」モデル、例えば束縛された識別子の値を格納するのにスタックを利用するモデルを選んでいたならば、値の集合に場所を追加するアイデアはおそらくもっと自明だったでしょう。
この拡張構文に関して、場所と記憶を扱う新しい言語要素についての簡約規則を述べることができます。
最初に、逆参照式!t1の簡約のため、t1を値になるまで簡約しなければなりません:
次に、代入式 t1:=t2 を簡約するため、先にt1が値(場所)になるまで簡約し、次にt2が(任意の種類の)値になるまで簡約します:
最後に、ref t1 という形の式を簡約するために、先にt1が値になるまで簡約します:
これらの簡約規則はどのようなガベージコレクションも行わないことに注意します。
単に簡約が進むにつれて記憶が限りなく大きくなることとします。
これは簡約結果の正しさには影響しません(なにせ「ガベージ」の定義は、まさに記憶のもはや到達できない部分なので、簡約で何の影響も与えられません)。
しかしこれは、洗練された評価器ならばガベージとなったものの場所を再利用して実行を続けることができる場面で、素朴な実装はメモリを使い果たす可能性があることを意味します。
形式的な規則は以下の通りです。
value v2 -------------------------------------- (ST_AppAbs) (\x:T.t12) v2 / st --> [x:=v2]t12 / st t1 / st --> t1' / st' --------------------------- (ST_App1) t1 t2 / st --> t1' t2 / st' value v1 t2 / st --> t2' / st' ---------------------------------- (ST_App2) v1 t2 / st --> v1 t2' / st'ここで最初の規則は記憶を変えずに返すことに注意します。 関数適用はそれ自体は副作用を起こさないからです。 残りの2つの規則は単に副作用を前提から結論に伝播します。
t1 / st --> t1' / st' ----------------------- (ST_Deref) !t1 / st --> !t1' / st't1の簡約が終わったならば、!lという形の式が得られるはずです。ここでlは何らかの場所です。 (関数やunitなどの他の種類の値を逆参照しようとする項は、エラーです。 現在アロケートされた記憶のサイズ|st|より大きな場所を逆参照しようとする項も同様です。 この場合、簡約規則は単に行き詰まります。 後に確立する型安全性は、型付けがされた項がこの方法で間違った振る舞いをすることがないことを保証します。)
l < |st| ---------------------------------- (ST_DerefLoc) !(loc l) / st --> lookup l st / st
t1 / st --> t1' / st' ----------------------------------- (ST_Assign1) t1 := t2 / st --> t1' := t2 / st' t2 / st --> t2' / st' --------------------------------- (ST_Assign2) v1 := t2 / st --> v1 := t2' / st't1とt2が終わったならば、l:=v2 という形の式が得られます。 この式の実行として、記憶を、場所lにv2が格納されるように更新します:
l < |st| ------------------------------------- (ST_Assign) loc l := v2 / st --> unit / [l:=v2]st記法 [l:=v2]st は「lをv2に写像し、他の場所はstと同じものに写像する記憶」を意味します。 この評価ステップの結果の項は単にunitであることに注意します。 興味を向ける結果は更新された記憶です。
t1 / st --> t1' / st' ----------------------------- (ST_Ref) ref t1 / st --> ref t1' / st'次にref自体を簡約するため、現在の記憶の最後の新しい場所を選びます。 つまり、場所|st|です。 そして新しい値v1についてstを拡張した新しい記憶を与えます。
-------------------------------- (ST_RefValue) ref v1 / st --> loc |st| / st,v1このステップの結果の値は新しくアロケートされた場所自身です。 (形式的には st,v1 は st ++ v1::nil 、つまり参照セルを末尾に追加するために、記憶の後ろに結合したものを意味します。)
Reserved Notation "t1 '/' st1 '-->' t2 '/' st2"
(at level 40, st1 at level 39, t2 at level 39).
Import ListNotations.
Inductive step : tm * store -> tm * store -> Prop :=
| ST_AppAbs : forall x T t12 v2 st,
value v2 ->
app (abs x T t12) v2 / st --> [x:=v2]t12 / st
| ST_App1 : forall t1 t1' t2 st st',
t1 / st --> t1' / st' ->
app t1 t2 / st --> app t1' t2 / st'
| ST_App2 : forall v1 t2 t2' st st',
value v1 ->
t2 / st --> t2' / st' ->
app v1 t2 / st --> app v1 t2'/ st'
| ST_SuccNat : forall n st,
scc (const n) / st --> const (S n) / st
| ST_Succ : forall t1 t1' st st',
t1 / st --> t1' / st' ->
scc t1 / st --> scc t1' / st'
| ST_PredNat : forall n st,
prd (const n) / st --> const (pred n) / st
| ST_Pred : forall t1 t1' st st',
t1 / st --> t1' / st' ->
prd t1 / st --> prd t1' / st'
| ST_MultNats : forall n1 n2 st,
mlt (const n1) (const n2) / st --> const (mult n1 n2) / st
| ST_Mult1 : forall t1 t2 t1' st st',
t1 / st --> t1' / st' ->
mlt t1 t2 / st --> mlt t1' t2 / st'
| ST_Mult2 : forall v1 t2 t2' st st',
value v1 ->
t2 / st --> t2' / st' ->
mlt v1 t2 / st --> mlt v1 t2' / st'
| ST_If0 : forall t1 t1' t2 t3 st st',
t1 / st --> t1' / st' ->
test0 t1 t2 t3 / st --> test0 t1' t2 t3 / st'
| ST_If0_Zero : forall t2 t3 st,
test0 (const 0) t2 t3 / st --> t2 / st
| ST_If0_Nonzero : forall n t2 t3 st,
test0 (const (S n)) t2 t3 / st --> t3 / st
| ST_RefValue : forall v1 st,
value v1 ->
ref v1 / st --> loc (length st) / (st ++ v1::nil)
| ST_Ref : forall t1 t1' st st',
t1 / st --> t1' / st' ->
ref t1 / st --> ref t1' / st'
| ST_DerefLoc : forall st l,
l < length st ->
deref (loc l) / st --> store_lookup l st / st
| ST_Deref : forall t1 t1' st st',
t1 / st --> t1' / st' ->
deref t1 / st --> deref t1' / st'
| ST_Assign : forall v2 l st,
value v2 ->
l < length st ->
assign (loc l) v2 / st --> unit / replace l v2 st
| ST_Assign1 : forall t1 t1' t2 st st',
t1 / st --> t1' / st' ->
assign t1 t2 / st --> assign t1' t2 / st'
| ST_Assign2 : forall v1 t2 t2' st st',
value v1 ->
t2 / st --> t2' / st' ->
assign v1 t2 / st --> assign v1 t2' / st'
where "t1 '/' st1 '-->' t2 '/' st2" := (step (t1,st1) (t2,st2)).
One slightly ugly point should be noted here: In the ST_RefValue
rule, we extend the state by writing st ++ v1::nil rather than
the more natural st ++ [v1]. The reason for this is that the
notation we've defined for substitution uses square brackets,
which clash with the standard library's notation for lists.
Hint Constructors step.
Definition multistep := (multi step).
Notation "t1 '/' st '-->*' t2 '/' st'" :=
(multistep (t1,st) (t2,st'))
(at level 40, st at level 39, t2 at level 39).
自由変数に型を割り当てるコンテキストはSTLCのものと完全に同じで、識別子から型への部分関数です。
参照に対応するように構文と簡約規則を拡張したので、最後の仕事は新しい言語要素に対する型付け規則を書き下すこと(そして、もちろん、それが健全であることをチェックすることも!)です。
自然と、キーとなる問題は「場所の型は何か?」になります。
何よりもまず、プログラマが実際に書く項の型チェックの目的のためには、この問題は現れないことに注意しましょう。
具体的な場所定数は簡約の中間結果の項にのみ現れます。
プログラマが書く言語の中には含まれません。
すると、場所の型を決める必要があるのは、簡約列の途中にある時、例えば、進行補題や保存補題を適用しようとする時だけです。
そのため、型付けは通常プログラムの「静的」性質と考えますが、場所の型付けがプログラムの「動的」状況にも依存するのは道理です。
まず、具体的場所を含む項を簡約するとき、結果の型は開始時の記憶の中身に依存することに注意します。
例えば、 記憶が [unit, unit] のときに項 !(loc 1) を簡約するならば、結果はunitです。
記憶が [unit, \x:Unit.x] のとき同じ項を簡約すると、結果は \x:Unit.x です。
前者の記憶に関して、場所1は型Unitを持ち、後者の記憶に関しては型Unit->Unitを持ちます。
これらのことから、場所の型付け規則の最初の試みとして、直ぐに次のものが考えられます:
この方法から始めましたが、整合性のとれる状態に逹するまでもうちょっと進んでみる必要があります。
事実上、記憶に依存した項の型を作ることで、型付け関係を、(コンテキスト、項、型の間の)3項関係から(コンテキスト、記憶、項、型の間の)4項関係に変更したことになります。
記憶は、直観的には、項の型を計算するコンテキストの一部であることから、この4項関係を書くとき、記憶を|-記号の左側において、Gamma; st |- t : Tのように書くことにしましょう。
すると、参照に型付けする規則は次の形になります:
しかしながら、この規則ではうまくいきません。
1つに、型チェックがかなり非効率的になる点があります。
なぜなら場所lの型の計算にはlの現在の中身vの型の計算が含まれています。
もしlが項tに何回も現れるときには、tの型導出を構成する過程の中でvの型を何回も再計算することになります。
下手をするとv自体が場所を含んでいるかもしれません。
すると毎回その型を計算しなければなりません。
またまずいことに、この型付け規則は、もし記憶に「循環(cycle)」を含んでいるとき何も導出できません。
例えば、記憶:
Gamma |- lookup l st : T1 ---------------------------- Gamma |- loc l : Ref T1つまり、場所lの型を見付けるために、記憶内のlの現在の中身を探索し、その中身の型T1を計算するものです。 するとこの場所の型は Ref T1 になります。
Gamma; st |- lookup l st : T1 -------------------------------- Gamma; st |- loc l : Ref T1そして残りの型付け規則のすべては記憶について同様の拡張がされます。 (他の規則は記憶に対して何も特別なことをする必要はありません。 単に前提から結論に渡すだけです。)
[\x:Nat. (!(loc 1)) x, \x:Nat. (!(loc 0)) x]について、場所0の有限な型導出は存在しません。
☐
これらの問題はどちらも、上記の場所についての型付け規則が、項の中に記述があるたびに場所の型を再計算することが必要であるという事実から生じます。
しかしこれは、直観的に、必要であるべきではありません。
とにかく、場所が最初に生成されたとき、そこに格納された初期値の型はわかっています。
特定の場所に格納される値の型は「決して変化しない」という不変条件が定められていると仮定しましょう。
つまり、この場所に後に別の値を格納することがあったとしても、その値は常に初期値と同じ型を持つ、ということです。
言い換えると、記憶のすべての場所に対して、それがアロケートされたときに決まる1つの確定した型を常に記憶しているということです。
すると、これらの意図した型を記憶型付け(store typing)としてまとめることができます。
これは、場所を型に写像する有限関数です。
これまでの型システム同様、更新についてのこの「保守的」型付け制約は、行き詰まることなく完全に評価できるいくつかのプログラムを ill-typed として除外します。
記憶について行ったのと同様に、記憶型を単に型のリストで表現します。
インデックスiの型はセルiに格納されうる値の型を記録したものです。
store_Tlookup 関数は、特定のインデックスの型をとりだします。
記憶stを記述する記憶型付けSTが与えられ、そこである項tが簡約されると仮定します。
すると、tの結果の型をstを直接見ることなく計算するのにSTを使うことができます。
例えば、もしSTが [Unit, Unit->Unit] ならば、 !(loc 1) が型 Unit->Unit を持つとすぐに推論できます。
より一般に、場所の型付け規則は記憶型付けの点から再構成され、次のようになります:
l < |ST| ------------------------------------- Gamma; ST |- loc l : Ref (lookup l ST)つまり、lが正しい場所である限りは、lの型をSTから調べるだけで計算できます。 型付けはやはり4項関係ですが、それは具体的記憶ではなく、 記憶「型付け」がパラメータになります。 型付け規則の残りも、同様に記憶型付けを引数とします。
ついに参照を持つSTLCについての型付け関係を形式化できます。
また、基本の(数値とUnitを持つ)STLCに追加する規則を与えます。
l < |ST| -------------------------------------- (T_Loc) Gamma; ST |- loc l : Ref (lookup l ST) Gamma; ST |- t1 : T1 ---------------------------- (T_Ref) Gamma; ST |- ref t1 : Ref T1 Gamma; ST |- t1 : Ref T11 ------------------------- (T_Deref) Gamma; ST |- !t1 : T11 Gamma; ST |- t1 : Ref T11 Gamma; ST |- t2 : T11 ----------------------------- (T_Assign) Gamma; ST |- t1 := t2 : Unit
Reserved Notation "Gamma ';' ST '|-' t '\in' T" (at level 40).
Inductive has_type : context -> store_ty -> tm -> ty -> Prop :=
| T_Var : forall Gamma ST x T,
Gamma x = Some T ->
Gamma; ST |- (var x) \in T
| T_Abs : forall Gamma ST x T11 T12 t12,
(update Gamma x T11); ST |- t12 \in T12 ->
Gamma; ST |- (abs x T11 t12) \in (Arrow T11 T12)
| T_App : forall T1 T2 Gamma ST t1 t2,
Gamma; ST |- t1 \in (Arrow T1 T2) ->
Gamma; ST |- t2 \in T1 ->
Gamma; ST |- (app t1 t2) \in T2
| T_Nat : forall Gamma ST n,
Gamma; ST |- (const n) \in Nat
| T_Succ : forall Gamma ST t1,
Gamma; ST |- t1 \in Nat ->
Gamma; ST |- (scc t1) \in Nat
| T_Pred : forall Gamma ST t1,
Gamma; ST |- t1 \in Nat ->
Gamma; ST |- (prd t1) \in Nat
| T_Mult : forall Gamma ST t1 t2,
Gamma; ST |- t1 \in Nat ->
Gamma; ST |- t2 \in Nat ->
Gamma; ST |- (mlt t1 t2) \in Nat
| T_If0 : forall Gamma ST t1 t2 t3 T,
Gamma; ST |- t1 \in Nat ->
Gamma; ST |- t2 \in T ->
Gamma; ST |- t3 \in T ->
Gamma; ST |- (test0 t1 t2 t3) \in T
| T_Unit : forall Gamma ST,
Gamma; ST |- unit \in Unit
| T_Loc : forall Gamma ST l,
l < length ST ->
Gamma; ST |- (loc l) \in (Ref (store_Tlookup l ST))
| T_Ref : forall Gamma ST t1 T1,
Gamma; ST |- t1 \in T1 ->
Gamma; ST |- (ref t1) \in (Ref T1)
| T_Deref : forall Gamma ST t1 T11,
Gamma; ST |- t1 \in (Ref T11) ->
Gamma; ST |- (deref t1) \in T11
| T_Assign : forall Gamma ST t1 t2 T11,
Gamma; ST |- t1 \in (Ref T11) ->
Gamma; ST |- t2 \in T11 ->
Gamma; ST |- (assign t1 t2) \in Unit
where "Gamma ';' ST '|-' t '\in' T" := (has_type Gamma ST t T).
Hint Constructors has_type.
もちろん、これらの型付け規則が簡約結果を正確に予言できるのは、
簡約の間使われる具体的な記憶が、型チェックの目的で仮定する記憶型付けに整合しているときだけです。
このただし書きは、通常のSTLCにおける自由変数と丁度同じ状況です。
STLCでは、置換補題から、Gamma |- t : T のとき、t内の自由変数をGamma
内にリストアップされた型の値で置換することで型Tの閉じた項を得ることができます。
そして、型保存定理より、この閉じた項は、もし必要なら型Tの最終結果に簡約されます。
以降、記憶と記憶型付けについての同様の直観をどのように形式化するかを見ます。
しかしながら、プログラマが実際に書く項の型チェックをするには、どのような記憶型付けを使うべきかを頑張って推測する必要はありません。
具体的場所定数は、評価の中間結果の項にのみ現れ、プログラマが書く言語には現れません。
よって、プログラマが書く項は、「空の」記憶型付けによって型チェックできます。
簡約が進み新しい場所が生成される場合も、新しくアロケートされたセルに置かれた初期値の型を見ることで、記憶型付けをどのように拡張したら良いかがわかります。
この直観は以下で型保存定理の主張として形式化されます。
最後の仕事は、標準的な型安全性が参照を追加したSTLCでも成立することをチェックすることです。
進行定理(「型付けできる項は行き詰まらない」)が主張でき、ほとんどSTLCと同じように証明できます。
証明に、新しい言語要素を扱ういくつかの場合を単に追加すれば良いのです。
保存定理はもうちょっとやりがいがあります。それでは早速、見てみましょう。
簡約関係と型付け関係の両者を拡張した(評価関係については初期記憶と最終記憶、型付け関係については記憶型付け)ことから、保存定理の主張は、これらのパラメータを含むように変えなければなりません。
しかしながら明らかに、記憶と記憶型付けを、その両者の関係について何も言わずにただ追加することはできません。
以下の主張は間違っています。
Theorem preservation_wrong1 : forall ST T t st t' st',
empty; ST |- t \in T ->
t / st --> t' / st' ->
empty; ST |- t' \in T.
Abort.
もし記憶内の値の型についてのいくつかの仮定の上で型チェックを行い、その後、その仮定をやぶる記憶のもとで簡約をしたならば、結果は悲惨なものになるでしょう。
記憶stが記憶型付けSTのもとで「型付けできる」(well typed)とは、stのそれぞれの場所lの項がSTの場所lの型を持つことです。
閉じた項だけが場所に格納されていることから(なぜでしょう?)、それらは空コンテキストで型付けすれば十分です。
以下の store_well_typed の定義はそれを形式化したものです。
Definition store_well_typed (ST:store_ty) (st:store) :=
length ST = length st /\
(forall l, l < length st ->
empty; ST |- (store_lookup l st) \in (store_Tlookup l ST)).
直観的に、記憶stが記憶型付けSTと整合的であるのは、記憶内のすべての値が記憶型付けに定められた型を持っていることです。
唯一の巧妙な点は、記憶内の値を型付けするとき、ほとんど同じ記憶型付けを型付け関係に提供することです。
これにより、上で見たような循環を持つ記憶に型付けできるようになります。
練習問題: ★★, standard (store_not_unique)
☐
ここまで来ると求められる保存性に近いものを主張することができます:
Theorem preservation_wrong2 : forall ST T t st t' st',
empty; ST |- t \in T ->
t / st --> t' / st' ->
store_well_typed ST st ->
empty; ST |- t' \in T.
Abort.
この主張は、アロケーション規則ST_RefValueを除くすべての評価規則について成立します。
問題は、この規則は初期記憶より大きな領域の記憶を必要とすることから、上記主張の結論が成立しなくなることです。
もしst'が新しい場所lについての束縛を含むなら、lはSTの領域に含まれないことから、(間違いなくlに言及している)t'はSTのもとで型付けできなくなります。
明らかに、記憶は簡約が進むにつれてサイズを増大させる可能性があることから、
記憶型付けも同様にサイズを増大できるようにする必要があります。
これから以下の定義が導かれます。
記憶型付けST'がSTを拡張する(extends)とは、
ST'が単にSTの最後にいくつかの新しい型を追加したものであることです。
Inductive extends : store_ty -> store_ty -> Prop :=
| extends_nil : forall ST',
extends ST' nil
| extends_cons : forall x ST' ST,
extends ST' ST ->
extends (x::ST') (x::ST).
Hint Constructors extends.
拡張されたコンテキストについてのいくつかの技術的補題が必要です。
最初に、拡張された記憶型付けから型を探索すると、オリジナルと同じ結果を返します:
Lemma extends_lookup : forall l ST ST',
l < length ST ->
extends ST' ST ->
store_Tlookup l ST' = store_Tlookup l ST.
Proof with auto.
intros l ST ST' Hlen H.
generalize dependent ST'. generalize dependent l.
induction ST as [|a ST2]; intros l Hlen ST' HST'.
- inversion Hlen.
- unfold store_Tlookup in *.
destruct ST'.
+ inversion HST'.
+
inversion HST'; subst.
destruct l as [|l'].
* auto.
* simpl. apply IHST2...
simpl in Hlen; omega.
Qed.
Lemma length_extends : forall l ST ST',
l < length ST ->
extends ST' ST ->
l < length ST'.
Proof with eauto.
intros. generalize dependent l. induction H0; intros l Hlen.
inversion Hlen.
simpl in *.
destruct l; try omega.
apply lt_n_S. apply IHextends. omega.
Qed.
Lemma extends_app : forall ST T,
extends (ST ++ T) ST.
Proof with auto.
induction ST; intros T...
simpl...
Qed.
Lemma extends_refl : forall ST,
extends ST ST.
Proof.
induction ST; auto.
Qed.
ついに、型保存性の最後の正しい主張ができます:
Definition preservation_theorem := forall ST t t' T st st',
empty; ST |- t \in T ->
store_well_typed ST st ->
t / st --> t' / st' ->
exists ST',
(extends ST' ST /\
empty; ST' |- t' \in T /\
store_well_typed ST' st').
保存定理は、新しい項t'の型付けができる、STを拡張する(つまり、すべての古い場所の値についてSTと一致する)「何らかの」記憶型付けST'が存在することを主張するだけであることに注意します。
この定理は具体的にST'が何であるかは示しません。
もちろん直観的にはST'は、STであるか、そうでなければ拡張された記憶 st ++ v1::nil の値v1の型T1についての ST ++ T1::nil であることは明らかです。
しかしこれを明示的に述べようとすると、定理の主張がいたずらに複雑になります(これによってより有用になることは何もありません)。
上記のより弱いバージョンでも繰り返し「クランクを回す」のに適切な形をしており(なぜなら結論が仮定を含意するため)、すべての簡約ステップの「列」が型付け可能性を保存することが導かれます。
この定理と進行性を組み合わせることで、「型付けできるプログラムはエラーにならない」という通常の保証が得られます。
これを証明するために、いつもの通りいくつかの補題が必要になります。
最初に、標準的な置換補題の簡単な拡張が必要です。
これはSTLCの置換補題の証明で使ったコンテキスト不変条件と同じ機構にさらに追加するものです。
Inductive appears_free_in : string -> tm -> Prop :=
| afi_var : forall x,
appears_free_in x (var x)
| afi_app1 : forall x t1 t2,
appears_free_in x t1 -> appears_free_in x (app t1 t2)
| afi_app2 : forall x t1 t2,
appears_free_in x t2 -> appears_free_in x (app t1 t2)
| afi_abs : forall x y T11 t12,
y <> x ->
appears_free_in x t12 ->
appears_free_in x (abs y T11 t12)
| afi_succ : forall x t1,
appears_free_in x t1 ->
appears_free_in x (scc t1)
| afi_pred : forall x t1,
appears_free_in x t1 ->
appears_free_in x (prd t1)
| afi_mult1 : forall x t1 t2,
appears_free_in x t1 ->
appears_free_in x (mlt t1 t2)
| afi_mult2 : forall x t1 t2,
appears_free_in x t2 ->
appears_free_in x (mlt t1 t2)
| afi_if0_1 : forall x t1 t2 t3,
appears_free_in x t1 ->
appears_free_in x (test0 t1 t2 t3)
| afi_if0_2 : forall x t1 t2 t3,
appears_free_in x t2 ->
appears_free_in x (test0 t1 t2 t3)
| afi_if0_3 : forall x t1 t2 t3,
appears_free_in x t3 ->
appears_free_in x (test0 t1 t2 t3)
| afi_ref : forall x t1,
appears_free_in x t1 -> appears_free_in x (ref t1)
| afi_deref : forall x t1,
appears_free_in x t1 -> appears_free_in x (deref t1)
| afi_assign1 : forall x t1 t2,
appears_free_in x t1 -> appears_free_in x (assign t1 t2)
| afi_assign2 : forall x t1 t2,
appears_free_in x t2 -> appears_free_in x (assign t1 t2).
Hint Constructors appears_free_in.
Lemma free_in_context : forall x t T Gamma ST,
appears_free_in x t ->
Gamma; ST |- t \in T ->
exists T', Gamma x = Some T'.
Proof with eauto.
intros. generalize dependent Gamma. generalize dependent T.
induction H;
intros; (try solve [ inversion H0; subst; eauto ]).
-
inversion H1; subst.
apply IHappears_free_in in H8.
rewrite update_neq in H8; assumption.
Qed.
Lemma context_invariance : forall Gamma Gamma' ST t T,
Gamma; ST |- t \in T ->
(forall x, appears_free_in x t -> Gamma x = Gamma' x) ->
Gamma'; ST |- t \in T.
Proof with eauto.
intros.
generalize dependent Gamma'.
induction H; intros...
-
apply T_Var. symmetry. rewrite <- H...
-
apply T_Abs. apply IHhas_type; intros.
unfold update, t_update.
destruct (eqb_stringP x x0)...
-
eapply T_App.
apply IHhas_type1...
apply IHhas_type2...
-
eapply T_Mult.
apply IHhas_type1...
apply IHhas_type2...
-
eapply T_If0.
apply IHhas_type1...
apply IHhas_type2...
apply IHhas_type3...
-
eapply T_Assign.
apply IHhas_type1...
apply IHhas_type2...
Qed.
Lemma substitution_preserves_typing : forall Gamma ST x s S t T,
empty; ST |- s \in S ->
(update Gamma x S); ST |- t \in T ->
Gamma; ST |- ([x:=s]t) \in T.
Proof with eauto.
intros Gamma ST x s S t T Hs Ht.
generalize dependent Gamma. generalize dependent T.
induction t; intros T Gamma H;
inversion H; subst; simpl...
-
rename s0 into y.
destruct (eqb_stringP x y).
+
subst.
rewrite update_eq in H3.
inversion H3; subst.
eapply context_invariance...
intros x Hcontra.
destruct (free_in_context _ _ _ _ _ Hcontra Hs)
as [T' HT'].
inversion HT'.
+
apply T_Var.
rewrite update_neq in H3...
- subst.
rename s0 into y.
destruct (eqb_stringP x y).
+
subst.
apply T_Abs. eapply context_invariance...
intros. rewrite update_shadow. reflexivity.
+
apply T_Abs. apply IHt.
eapply context_invariance...
intros. unfold update, t_update.
destruct (eqb_stringP y x0)...
subst.
rewrite false_eqb_string...
Qed.
次に、記憶中のセルの中身を適切な型を持つ新しい値で置き換えたときに、記憶の全体的な型を変えないことを示す必要があります。
(これは、ST_Assign規則に必要になります。)
Lemma assign_pres_store_typing : forall ST st l t,
l < length st ->
store_well_typed ST st ->
empty; ST |- t \in (store_Tlookup l ST) ->
store_well_typed ST (replace l t st).
Proof with auto.
intros ST st l t Hlen HST Ht.
inversion HST; subst.
split. rewrite length_replace...
intros l' Hl'.
destruct (l' =? l) eqn: Heqll'.
-
apply Nat.eqb_eq in Heqll'; subst.
rewrite lookup_replace_eq...
-
apply Nat.eqb_neq in Heqll'.
rewrite lookup_replace_neq...
rewrite length_replace in Hl'.
apply H0...
Qed.
最後に、記憶型付けについての補題が必要です。
その補題とは、記憶型付けが新しい場所について拡張されたときに、拡張された記憶型付けがオリジナルと同じ項に同じ型をつけることを主張するものです。
(この補題は store_weakening(記憶弱化)と呼ばれます。
なぜなら、証明論で見られる弱化("weakening")補題と似ているからです。
証明論の弱化補題は、ある理論に新しい仮定を追加しても証明可能な定理が減ることはないことを言うものです。)
Lemma store_weakening : forall Gamma ST ST' t T,
extends ST' ST ->
Gamma; ST |- t \in T ->
Gamma; ST' |- t \in T.
Proof with eauto.
intros. induction H0; eauto.
-
erewrite <- extends_lookup...
apply T_Loc.
eapply length_extends...
Qed.
記憶がある記憶型付けのもとで型付けできるとき、新しい項tを拡張した記憶も、記憶型付けを項tの型について拡張したもののもとで型付けできることを、store_weakening補題を使って示すことができます。
Lemma store_well_typed_app : forall ST st t1 T1,
store_well_typed ST st ->
empty; ST |- t1 \in T1 ->
store_well_typed (ST ++ T1::nil) (st ++ t1::nil).
Proof with auto.
intros.
unfold store_well_typed in *.
inversion H as [Hlen Hmatch]; clear H.
rewrite app_length, plus_comm. simpl.
rewrite app_length, plus_comm. simpl.
split...
-
intros l Hl.
unfold store_lookup, store_Tlookup.
apply le_lt_eq_dec in Hl; inversion Hl as [Hlt | Heq].
+
apply lt_S_n in Hlt.
rewrite !app_nth1...
* apply store_weakening with ST. apply extends_app.
apply Hmatch...
* rewrite Hlen...
+
inversion Heq.
rewrite app_nth2; try omega.
rewrite <- Hlen.
rewrite minus_diag. simpl.
apply store_weakening with ST...
{ apply extends_app. }
rewrite app_nth2; try omega.
rewrite minus_diag. simpl. trivial.
Qed.
さて、準備が整いました。
保存の証明は実際本当に簡単です。
Begin with one technical lemma:
Lemma nth_eq_last : forall A (l:list A) x d,
nth (length l) (l ++ x::nil) d = x.
Proof.
induction l; intros; [ auto | simpl; rewrite IHl; auto ].
Qed.
And here, at last, is the preservation theorem and proof:
Theorem preservation : forall ST t t' T st st',
empty; ST |- t \in T ->
store_well_typed ST st ->
t / st --> t' / st' ->
exists ST',
(extends ST' ST /\
empty; ST' |- t' \in T /\
store_well_typed ST' st').
Proof with eauto using store_weakening, extends_refl.
remember (@empty ty) as Gamma.
intros ST t t' T st st' Ht.
generalize dependent t'.
induction Ht; intros t' HST Hstep;
subst; try solve_by_invert; inversion Hstep; subst;
try (eauto using store_weakening, extends_refl).
- exists ST.
inversion Ht1; subst.
split; try split... eapply substitution_preserves_typing...
-
eapply IHHt1 in H0...
inversion H0 as [ST' [Hext [Hty Hsty]]].
exists ST'...
-
eapply IHHt2 in H5...
inversion H5 as [ST' [Hext [Hty Hsty]]].
exists ST'...
-
+
eapply IHHt in H0...
inversion H0 as [ST' [Hext [Hty Hsty]]].
exists ST'...
-
+
eapply IHHt in H0...
inversion H0 as [ST' [Hext [Hty Hsty]]].
exists ST'...
-
eapply IHHt1 in H0...
inversion H0 as [ST' [Hext [Hty Hsty]]].
exists ST'...
-
eapply IHHt2 in H5...
inversion H5 as [ST' [Hext [Hty Hsty]]].
exists ST'...
-
+
eapply IHHt1 in H0...
inversion H0 as [ST' [Hext [Hty Hsty]]].
exists ST'... split...
-
exists (ST ++ T1::nil).
inversion HST; subst.
split.
apply extends_app.
split.
replace (Ref T1)
with (Ref (store_Tlookup (length st) (ST ++ T1::nil))).
apply T_Loc.
rewrite <- H. rewrite app_length, plus_comm. simpl. omega.
unfold store_Tlookup. rewrite <- H. rewrite nth_eq_last.
reflexivity.
apply store_well_typed_app; assumption.
-
eapply IHHt in H0...
inversion H0 as [ST' [Hext [Hty Hsty]]].
exists ST'...
-
exists ST. split; try split...
inversion HST as [_ Hsty].
replace T11 with (store_Tlookup l ST).
apply Hsty...
inversion Ht; subst...
-
eapply IHHt in H0...
inversion H0 as [ST' [Hext [Hty Hsty]]].
exists ST'...
-
exists ST. split; try split...
eapply assign_pres_store_typing...
inversion Ht1; subst...
-
eapply IHHt1 in H0...
inversion H0 as [ST' [Hext [Hty Hsty]]].
exists ST'...
-
eapply IHHt2 in H5...
inversion H5 as [ST' [Hext [Hty Hsty]]].
exists ST'...
Qed.
練習問題: ★★★, standard (preservation_informal)
☐
前に言ったとおり、このシステムの進行はかなり簡単に証明できます。
証明は、STLCの進行の証明とほとんど同じです。
いくつかの新しい構文要素について新しい場合を追加するだけです。
Theorem progress : forall ST t T st,
empty; ST |- t \in T ->
store_well_typed ST st ->
(value t \/ exists t' st', t / st --> t' / st').
Proof with eauto.
intros ST t T st Ht HST. remember (@empty ty) as Gamma.
induction Ht; subst; try solve_by_invert...
-
right. destruct IHHt1 as [Ht1p | Ht1p]...
+
inversion Ht1p; subst; try solve_by_invert.
destruct IHHt2 as [Ht2p | Ht2p]...
*
inversion Ht2p as [t2' [st' Hstep]].
exists (app (abs x T t) t2'), st'...
+
inversion Ht1p as [t1' [st' Hstep]].
exists (app t1' t2), st'...
-
right. destruct IHHt as [Ht1p | Ht1p]...
+
inversion Ht1p; subst; try solve [ inversion Ht ].
*
exists (const (S n)), st...
+
inversion Ht1p as [t1' [st' Hstep]].
exists (scc t1'), st'...
-
right. destruct IHHt as [Ht1p | Ht1p]...
+
inversion Ht1p; subst; try solve [inversion Ht ].
*
exists (const (pred n)), st...
+
inversion Ht1p as [t1' [st' Hstep]].
exists (prd t1'), st'...
-
right. destruct IHHt1 as [Ht1p | Ht1p]...
+
inversion Ht1p; subst; try solve [inversion Ht1].
destruct IHHt2 as [Ht2p | Ht2p]...
*
inversion Ht2p; subst; try solve [inversion Ht2].
exists (const (mult n n0)), st...
*
inversion Ht2p as [t2' [st' Hstep]].
exists (mlt (const n) t2'), st'...
+
inversion Ht1p as [t1' [st' Hstep]].
exists (mlt t1' t2), st'...
-
right. destruct IHHt1 as [Ht1p | Ht1p]...
+
inversion Ht1p; subst; try solve [inversion Ht1].
destruct n.
* exists t2, st...
* exists t3, st...
+
inversion Ht1p as [t1' [st' Hstep]].
exists (test0 t1' t2 t3), st'...
-
right. destruct IHHt as [Ht1p | Ht1p]...
+
inversion Ht1p as [t1' [st' Hstep]].
exists (ref t1'), st'...
-
right. destruct IHHt as [Ht1p | Ht1p]...
+
inversion Ht1p; subst; try solve_by_invert.
eexists. eexists. apply ST_DerefLoc...
inversion Ht; subst. inversion HST; subst.
rewrite <- H...
+
inversion Ht1p as [t1' [st' Hstep]].
exists (deref t1'), st'...
-
right. destruct IHHt1 as [Ht1p|Ht1p]...
+
destruct IHHt2 as [Ht2p|Ht2p]...
*
inversion Ht1p; subst; try solve_by_invert.
eexists. eexists. apply ST_Assign...
inversion HST; subst. inversion Ht1; subst.
rewrite H in H5...
*
inversion Ht2p as [t2' [st' Hstep]].
exists (assign t1 t2'), st'...
+
inversion Ht1p as [t1' [st' Hstep]].
exists (assign t1' t2), st'...
Qed.
STLCは(Norm章で示したとおり)正規化性を持つのでした。
つまり、型付けされるすべての項は有限回のステップで値に簡約されるということです。
参照を加えたSTLCではどうでしょうか?
驚くべきことに、参照を追加したことで、正規化性は成立しなくなります。
参照を持つSTLCの型付けされる項で、正規形に逹することなく永遠に簡約を続けられる項が存在します!
そのような項をどのように構成したらよいでしょうか?
一番のアイデアは、自分自身を呼ぶ関数を作る、ということです。
最初に参照セルに格納された別の関数を呼ぶ関数を作ります。
トリックは、その後で自分自身への参照を持ち込むことです!
この項をCoqで記述すると以下のようになります。
(\r:Ref (Unit -> Unit). r := (\x:Unit.(!r) unit); (!r) unit) (ref (\x:Unit.unit))最初に、ref (\x:Unit.unit) が型 Unit -> Unit のセルへの参照を作ります。 次にこの参照を、ある関数に引数として渡します。 その関数は、引数を名前rに束縛し、そして引数に関数 (\x:Unit.(!r) unit) を代入するものです。 ここで関数 \x:Unit.(!r) unit は、引数を無視してrに格納された関数を引数unitに適用するものです。 しかしもちろん、rに格納された関数とは自分自身です! 無限ループを開始するために、この関数を (! r) unit に適用することで実行を開始します。
Module ExampleVariables.
Open Scope string_scope.
Definition x := "x".
Definition y := "y".
Definition r := "r".
Definition s := "s".
End ExampleVariables.
Module RefsAndNontermination.
Import ExampleVariables.
Definition loop_fun :=
abs x Unit (app (deref (var r)) unit).
Definition loop :=
app
(abs r (Ref (Arrow Unit Unit))
(tseq (assign (var r) loop_fun)
(app (deref (var r)) unit)))
(ref (abs x Unit unit)).
この項は型付けされます:
Lemma loop_typeable : exists T, empty; nil |- loop \in T.
Proof with eauto.
eexists. unfold loop. unfold loop_fun.
eapply T_App...
eapply T_Abs...
eapply T_App...
eapply T_Abs. eapply T_App. eapply T_Deref. eapply T_Var.
unfold update, t_update. simpl. reflexivity. auto.
eapply T_Assign.
eapply T_Var. unfold update, t_update. simpl. reflexivity.
eapply T_Abs.
eapply T_App...
eapply T_Deref. eapply T_Var. reflexivity.
Qed.
項が発散することを形式的に示すために、最初に1ステップ簡約関係のstep_closure(ステップ閉包)-->+を定義します。
これは1ステップ簡約のステップ閉包(-->*と書いてきたもの)とほとんど同じですが、反射的ではないという点が違います。
つまり、t -->+ t' はtからt'へ1以上のステップの簡約で到達できることを意味します。
Inductive step_closure {X:Type} (R: relation X) : X -> X -> Prop :=
| sc_one : forall (x y : X),
R x y -> step_closure R x y
| sc_step : forall (x y z : X),
R x y ->
step_closure R y z ->
step_closure R x z.
Definition multistep1 := (step_closure step).
Notation "t1 '/' st '-->+' t2 '/' st'" :=
(multistep1 (t1,st) (t2,st'))
(at level 40, st at level 39, t2 at level 39).
便宜上、normalizeタクティックの若干の変種であるreduceを導入します。
これは、ゴールがそれ以上簡約できなくなるまで待つのではなく、各ステップでゴールをmulti_reflを使って解こうとします。
もちろん、全体としてのポイントはloopが正規化されないことです。
このため、前のnormalizeタクティックでは簡約を永遠に続ける無限ループに陥るだけです!
Ltac print_goal := match goal with |- ?x => idtac x end.
Ltac reduce :=
repeat (print_goal; eapply multi_step ;
[ (eauto 10; fail) | (instantiate; compute)];
try solve [apply multi_refl]).
Lemma loop_steps_to_loop_fun :
loop / nil -->*
app (deref (loc 0)) unit / cons ([r:=loc 0]loop_fun) nil.
Proof.
unfold loop.
reduce.
Qed.
最後に、後者の式は2ステップで自分自身に簡約されることを示します!
Lemma loop_fun_step_self :
app (deref (loc 0)) unit / cons ([r:=loc 0]loop_fun) nil -->+
app (deref (loc 0)) unit / cons ([r:=loc 0]loop_fun) nil.
Proof with eauto.
unfold loop_fun; simpl.
eapply sc_step. apply ST_App1...
eapply sc_one. compute. apply ST_AppAbs...
Qed.
練習問題: ★★★★, standard (factorial_ref)
Definition factorial : tm
. Admitted.
Lemma factorial_type : empty; nil |- factorial \in (Arrow Nat Nat).
Proof with eauto.
Admitted.
もし定義が正しいのならば、以下の例のコメントを外してみなさい。
証明はreduceタクティックを使って完全自動で行われるはずです。