言語処理系入門
第 8 回:コンパイラ I : CPS 変換
2009 年 12 月 18 日(金)
【改 :2009/12/21 】
服部 健太
コンパイルの流れ
 関数型言語における典型的なコンパイルパス
ソース
プログラム 字句・構文解析 型検査 CPS 変換
Cps.convTyping.checkparse
クロージャ変換コード生成
目的
コード
実行時
ライブラリ
中間言語
抽象構文木
GC など実行時に必要な処理
実行時にロード・リンクされる
C 言語で実装されることが多い
C 言語,アセンブリコード,
VM の中間言語(バイトコード) etc
2009/12/18 2言語処理系入門 8
中間言語(中間表現とも呼ぶ)
 利点
 最適化をしやすくする
 複数の目的言語をサポートするため
 複数の段階に分けることでコンパイルの処理を簡
潔にする
 どのような形式の中間言語を採用するかは言
語のデザインによって異なる
 命令型言語では, SSA や RTL ,
 関数型言語では, CPS や A 正規形, K 正規形
 複数の中間言語を採用するコンパイラも
我々の言語で
は CPS を採
用
2009/12/18 3言語処理系入門 8
復習:継続( continuation )とは
 残りの計算を表す概念
 プログラムの実行のある時点から最終的な答えを
得るまでの計算
 例:
let x = 2 in
let y = 3 + x * (f x) in
y * y;
 部分式 (f x) に注目したとき,このときの継続
let y = 3 + x * [<(f x) の値 >] in y * y
2009/12/4 4言語処理系入門 62009/12/18 4言語処理系入門 8
復習:継続渡しスタイル
( CPS )
 継続を明示的に渡す関数のスタイル
 ダイレクトスタイル(通常の形式)
let rec fact n =
if n == 0 then 1
else n * (fact (n – 1));
 継続渡しスタイル
let rec fact n k =
if n == 0 then k 1
else fact (n-1) (fn v -> k (v * n));
fact(n-1) を計算し
た後にする計算
(継続)
2009/12/4 5言語処理系入門 62009/12/18 5言語処理系入門 8
中間言語としての CPS
 制御の流れ , データの流れがコードに明示的に現れる
 最適化がしやすい(末尾呼び出し最適化,不要コードの除去
etc )
 shift/reset, callcc, letcc などの一級継続も CPS に変換することで
簡単に実装できる
 例:
let rec prod_primes n =
if n == 1 then
1
else
if is_prime n then
n * prod_primes(n-1)
else
prod_primes(n-1)
2009/12/18 6言語処理系入門 8
中間言語としての CPS ( 2 )
let rec prod_primes c n =
if n == 1 then c 1
else let k =   fn b ->
if b == true then
let j = fn p ->
let a = n * p in c a
and m = n – 1
in prod_primes j m
else
let h = fn q -> c q
and i = n – 1
in prod_primes h i
in is_prime k n
k: 継続 1
j: 継続
2
h: 継続
3
2009/12/18 7言語処理系入門 8
ソース言語の抽象構文
 E ::= F | V
 | let x1 = E1 and … and xn = En in E
 | let rec x1 = E1 and … and xn = En in E
 | if E1 then E2 else E3
 | handle E1 with x in E2
 | raise E
 | E.l
 | E1 E2
 | op(E1,…,En )
 | { l1=E1; …; ln=En }
 | fn x -> E
 V ::= c | x
op は +,-,*,/,strlen など
のプリミティブ演算子
2009/12/18 8言語処理系入門 8
CPS 式の構文
 K ::= E
 | let x1 = D1 and … and xn = Dn in K
 | let rec x1 = D1 and … and xn = Dn in K
 | if E then K1 else K2
 | V k E | k E
 D ::= E
 |{ l1=V1;…; ln = Vn }
 | fn k x -> K | fn x -> K
 E ::= V
    | let x1 = D1 and … and xn = Dn in E
    | let rec x1 = D1 and … and xn = Dn in E
 | E.l
 | op(E1, … ,En)
 V ::= c | x | k --- ただし, k, x∈Variable
関数呼び出しや分岐を
伴わない単純な式
レコードと関数は let/let rec で束縛
されてから使用される
2009/12/18 9言語処理系入門 8
CPS 変換
 変換関数を [[E]]к⇒K のように記述する
 E は,変換元の構文木, K は変換後の CPS 式, к
は単純な CPS 式 (E) を受け取り,完全な CPS 式
を出力する関数
 [[ ・ ]] : Esource→(Ecps→Kcps) → Kcps
 к : Ecps→Kcps
 Vsource の変換
 単に к に渡して, CPS 式の穴を埋める
[[csource]]к к⇒ ccps
[[xsource]]к к⇒ xcps
以降,あきらかな場合
には subscript を省略
2009/12/18 10言語処理系入門 8
if 式
 素朴な変換
[[if E1 then E2 else E3]]к⇒
[[E1]](λv.if v then [[E2]]к else [[E3]]к)
 к が複製されるため,コードが爆発する可能性あ
り
 正しい変換
[[if E1 then E2 else E3]]к⇒
[[E1]](λv.let j = fn x ->кx
in if v then [[E2]](λv.j v)
else [[E3]](λv.j v))2009/12/18 11言語処理系入門 8
let/let rec 式
 x=E の右辺式 E を単純な式に変換
 let 式
[[let x1 = E1 … and xn = En in E]]к⇒
[[Es]](λvs. к (let xs=vs in [[E]]кinit))  
  [[Es]](λvs.let xs=vs in [[E]]к)
 let rec 式
[[let rec x1 = E1 … and xn = En in E]]к⇒
[[Es]](λvs.к(let rec xs=vs in [[E]]кinit))
[[Es]](λvs.let rec xs=vs in [[E]]к)
2009/12/18 12言語処理系入門 8
E is simple
E is not simple
E is simple
E is not simple
関数・プリミティブ演算子の変換
 無名関数
[[fn x -> E]]к⇒
   let f = fn k x -> [[E]](λv.k v) in кf
 f や k はフレッシュな変数
 後のフェーズで便利なように無名の関数に名前を付けてお
く
 関数適用
[[E1 E2]]к⇒
   [[E1]](λf.[[E2]](λv.let k = fn r ->кr in f k v))
 プリミティブ演算子
[[op(E1,…,En)]]к⇒
   [[Es]](λvs.кop(vs))
呼び出し元に戻ってくるための継続
継続を受け取るた
めの引数を1つ増
やす
2009/12/18 13言語処理系入門 8
レコード式の変換
 レコード生成
 各フィールドの右辺式を単純な式に変換
[[{ l1=E1; …; ln=En }]]к⇒
[[Es]](λvs.к(let xs=vsin let r ={ls=xs} in r))
 フィールド参照
 参照元の式を変換し,そのフィールド値を x に束
縛
[[E.l]]к⇒
[[E]](λv.к(v.l))
2009/12/18 14言語処理系入門 8
例外捕捉・発生
 例外捕捉
 新しいハンドラをセットし,式を評価
 例外を捕捉したら元のハンドラをセットし直して, E2 を
評価
[[handle E1 with x in E2]]к⇒
   let h = gethdlr()
and k = fn x ->кx in
let h’ = fn x -> let _ = sethdlr h in
[[E2]](λv.k v)
in let _ = sethdlr h’ in
[[E1]](λv.let _ = sethdlr h in k v)
 例外発生
 ハンドラを取得して,呼び出すだけ
[[raise E]]к ⇒
     [[E]] (λv.let h = gethdlr() in h v)
2009/12/18 15言語処理系入門 8
変換例
 [[+(x,(f 0).l)]]к
⇒[[<x;(f 0).l>]](λvs.let x1 = +(vs) in кx1)
⇒[[ (f 0).l]](λv.let x1 = +(x,v) in кx1)
⇒[[(f 0)]](λv.let x2=v.l in (λv.let x1 = +(x,v) in кx1)x2)
⇒[[(f 0)]](λv.let x2=v.l in let x1 = +(x, x2) in кx1)
⇒[[f]](λf.[[0]] (λv.let k=fn r->
(λv.let x2=v.l in let x1 = +(x,x2) in к x1)r
in f k v))
⇒[[f]](λf.[[0]](λv.let k=fn r->
let x2=r.l in let x1 = +(x,x2) in кx1
in f k v))
⇒let k=fn r->let x2=r.l in
let x1 = +(x,x2)
in кx1
in f k 0
 演習問題
 [[let rec fact = fn n -> if ==(n,0) then 1 else *(n,fact(-(n,1))) in fact 5]]к
⇒?
2009/12/18 16言語処理系入門 8
Ocaml での実装( cps.ml )
let rec conv_expr k e = match e with
| Syntax.ValExpr v -> k (conv_value v)
| Syntax.IfExpr(e1,e2,e3) ->
let j = Symbol.fresh() in
let x = Symbol.fresh() in
let k' = fun v -> AppExpr(VarVal j,[v]) in
conv_expr (
fun v ->
LetExpr([j,FunExpr([x],k (VarVal x))],
IfExpr(v,conv_expr k' e2,conv_expr k' e3))
) e1
| Syntax.FunExpr((x,_),_,e1) ->
let k’ = Symbol.fresh() in
let f = FunExpr([k’;x],
conv_expr (fun v -> AppExpr(VarVal k’,[v])) e1)
in
k (VarVal f)
…
2009/12/18 17言語処理系入門 8
末尾呼び出しの最適化
 以下の式を CPS 変換すると
 let f x = g (x + 1)
 こうなるが こう最適化でき
る
let f k x = let f k x =
let k’ a = k a in let b = x + 1 in
let b = x + 1 in      g k b
g k’ b
 λ 計算において λx.M(x) を M に簡約化することを η
簡約と呼ぶ
 「 C 言語的に考えると, int foo(int x) { return bar(x); } とい
う関数定義がある場合,すべての foo の呼び出しを bar に
置き換えることができる」のと同じことである
2009/12/18 18言語処理系入門 8
参考文献
 A.W.Appel, “Compiling with Continuations”,
Cambridge Univ.Press, 1992.
 田浦さんの資料
 http://www.is.s.u-tokyo.ac.jp/vu/97/jugyo/processor/com
2009/12/18 19言語処理系入門 8
演習問題
 今週のサンプルプログラムを動かしてみよ
 無駄な変数のコピー伝播を削除する最適化を
実装せよ
 let x = y in …x… … y …⇒
 末尾呼び出しの最適化を実装せよ
2009/12/18 20言語処理系入門 8
次回予定
 日時:
 2009 年 12 月 25 日(金) 10 : 30 - 12 : 00
 場所:
 LB2 3F/A
 内容:
 コンパイル II :クロージャ変換
2009/12/10 21言語処理系入門 72009/12/18 21言語処理系入門 8

言語処理系入門€8

  • 1.
    言語処理系入門 第 8 回:コンパイラI : CPS 変換 2009 年 12 月 18 日(金) 【改 :2009/12/21 】 服部 健太
  • 2.
    コンパイルの流れ  関数型言語における典型的なコンパイルパス ソース プログラム 字句・構文解析型検査 CPS 変換 Cps.convTyping.checkparse クロージャ変換コード生成 目的 コード 実行時 ライブラリ 中間言語 抽象構文木 GC など実行時に必要な処理 実行時にロード・リンクされる C 言語で実装されることが多い C 言語,アセンブリコード, VM の中間言語(バイトコード) etc 2009/12/18 2言語処理系入門 8
  • 3.
    中間言語(中間表現とも呼ぶ)  利点  最適化をしやすくする 複数の目的言語をサポートするため  複数の段階に分けることでコンパイルの処理を簡 潔にする  どのような形式の中間言語を採用するかは言 語のデザインによって異なる  命令型言語では, SSA や RTL ,  関数型言語では, CPS や A 正規形, K 正規形  複数の中間言語を採用するコンパイラも 我々の言語で は CPS を採 用 2009/12/18 3言語処理系入門 8
  • 4.
    復習:継続( continuation )とは 残りの計算を表す概念  プログラムの実行のある時点から最終的な答えを 得るまでの計算  例: let x = 2 in let y = 3 + x * (f x) in y * y;  部分式 (f x) に注目したとき,このときの継続 let y = 3 + x * [<(f x) の値 >] in y * y 2009/12/4 4言語処理系入門 62009/12/18 4言語処理系入門 8
  • 5.
    復習:継続渡しスタイル ( CPS ) 継続を明示的に渡す関数のスタイル  ダイレクトスタイル(通常の形式) let rec fact n = if n == 0 then 1 else n * (fact (n – 1));  継続渡しスタイル let rec fact n k = if n == 0 then k 1 else fact (n-1) (fn v -> k (v * n)); fact(n-1) を計算し た後にする計算 (継続) 2009/12/4 5言語処理系入門 62009/12/18 5言語処理系入門 8
  • 6.
    中間言語としての CPS  制御の流れ, データの流れがコードに明示的に現れる  最適化がしやすい(末尾呼び出し最適化,不要コードの除去 etc )  shift/reset, callcc, letcc などの一級継続も CPS に変換することで 簡単に実装できる  例: let rec prod_primes n = if n == 1 then 1 else if is_prime n then n * prod_primes(n-1) else prod_primes(n-1) 2009/12/18 6言語処理系入門 8
  • 7.
    中間言語としての CPS (2 ) let rec prod_primes c n = if n == 1 then c 1 else let k =   fn b -> if b == true then let j = fn p -> let a = n * p in c a and m = n – 1 in prod_primes j m else let h = fn q -> c q and i = n – 1 in prod_primes h i in is_prime k n k: 継続 1 j: 継続 2 h: 継続 3 2009/12/18 7言語処理系入門 8
  • 8.
    ソース言語の抽象構文  E ::=F | V  | let x1 = E1 and … and xn = En in E  | let rec x1 = E1 and … and xn = En in E  | if E1 then E2 else E3  | handle E1 with x in E2  | raise E  | E.l  | E1 E2  | op(E1,…,En )  | { l1=E1; …; ln=En }  | fn x -> E  V ::= c | x op は +,-,*,/,strlen など のプリミティブ演算子 2009/12/18 8言語処理系入門 8
  • 9.
    CPS 式の構文  K::= E  | let x1 = D1 and … and xn = Dn in K  | let rec x1 = D1 and … and xn = Dn in K  | if E then K1 else K2  | V k E | k E  D ::= E  |{ l1=V1;…; ln = Vn }  | fn k x -> K | fn x -> K  E ::= V     | let x1 = D1 and … and xn = Dn in E     | let rec x1 = D1 and … and xn = Dn in E  | E.l  | op(E1, … ,En)  V ::= c | x | k --- ただし, k, x∈Variable 関数呼び出しや分岐を 伴わない単純な式 レコードと関数は let/let rec で束縛 されてから使用される 2009/12/18 9言語処理系入門 8
  • 10.
    CPS 変換  変換関数を[[E]]к⇒K のように記述する  E は,変換元の構文木, K は変換後の CPS 式, к は単純な CPS 式 (E) を受け取り,完全な CPS 式 を出力する関数  [[ ・ ]] : Esource→(Ecps→Kcps) → Kcps  к : Ecps→Kcps  Vsource の変換  単に к に渡して, CPS 式の穴を埋める [[csource]]к к⇒ ccps [[xsource]]к к⇒ xcps 以降,あきらかな場合 には subscript を省略 2009/12/18 10言語処理系入門 8
  • 11.
    if 式  素朴な変換 [[ifE1 then E2 else E3]]к⇒ [[E1]](λv.if v then [[E2]]к else [[E3]]к)  к が複製されるため,コードが爆発する可能性あ り  正しい変換 [[if E1 then E2 else E3]]к⇒ [[E1]](λv.let j = fn x ->кx in if v then [[E2]](λv.j v) else [[E3]](λv.j v))2009/12/18 11言語処理系入門 8
  • 12.
    let/let rec 式 x=E の右辺式 E を単純な式に変換  let 式 [[let x1 = E1 … and xn = En in E]]к⇒ [[Es]](λvs. к (let xs=vs in [[E]]кinit))     [[Es]](λvs.let xs=vs in [[E]]к)  let rec 式 [[let rec x1 = E1 … and xn = En in E]]к⇒ [[Es]](λvs.к(let rec xs=vs in [[E]]кinit)) [[Es]](λvs.let rec xs=vs in [[E]]к) 2009/12/18 12言語処理系入門 8 E is simple E is not simple E is simple E is not simple
  • 13.
    関数・プリミティブ演算子の変換  無名関数 [[fn x-> E]]к⇒    let f = fn k x -> [[E]](λv.k v) in кf  f や k はフレッシュな変数  後のフェーズで便利なように無名の関数に名前を付けてお く  関数適用 [[E1 E2]]к⇒    [[E1]](λf.[[E2]](λv.let k = fn r ->кr in f k v))  プリミティブ演算子 [[op(E1,…,En)]]к⇒    [[Es]](λvs.кop(vs)) 呼び出し元に戻ってくるための継続 継続を受け取るた めの引数を1つ増 やす 2009/12/18 13言語処理系入門 8
  • 14.
    レコード式の変換  レコード生成  各フィールドの右辺式を単純な式に変換 [[{l1=E1; …; ln=En }]]к⇒ [[Es]](λvs.к(let xs=vsin let r ={ls=xs} in r))  フィールド参照  参照元の式を変換し,そのフィールド値を x に束 縛 [[E.l]]к⇒ [[E]](λv.к(v.l)) 2009/12/18 14言語処理系入門 8
  • 15.
    例外捕捉・発生  例外捕捉  新しいハンドラをセットし,式を評価 例外を捕捉したら元のハンドラをセットし直して, E2 を 評価 [[handle E1 with x in E2]]к⇒    let h = gethdlr() and k = fn x ->кx in let h’ = fn x -> let _ = sethdlr h in [[E2]](λv.k v) in let _ = sethdlr h’ in [[E1]](λv.let _ = sethdlr h in k v)  例外発生  ハンドラを取得して,呼び出すだけ [[raise E]]к ⇒      [[E]] (λv.let h = gethdlr() in h v) 2009/12/18 15言語処理系入門 8
  • 16.
    変換例  [[+(x,(f 0).l)]]к ⇒[[<x;(f0).l>]](λvs.let x1 = +(vs) in кx1) ⇒[[ (f 0).l]](λv.let x1 = +(x,v) in кx1) ⇒[[(f 0)]](λv.let x2=v.l in (λv.let x1 = +(x,v) in кx1)x2) ⇒[[(f 0)]](λv.let x2=v.l in let x1 = +(x, x2) in кx1) ⇒[[f]](λf.[[0]] (λv.let k=fn r-> (λv.let x2=v.l in let x1 = +(x,x2) in к x1)r in f k v)) ⇒[[f]](λf.[[0]](λv.let k=fn r-> let x2=r.l in let x1 = +(x,x2) in кx1 in f k v)) ⇒let k=fn r->let x2=r.l in let x1 = +(x,x2) in кx1 in f k 0  演習問題  [[let rec fact = fn n -> if ==(n,0) then 1 else *(n,fact(-(n,1))) in fact 5]]к ⇒? 2009/12/18 16言語処理系入門 8
  • 17.
    Ocaml での実装( cps.ml) let rec conv_expr k e = match e with | Syntax.ValExpr v -> k (conv_value v) | Syntax.IfExpr(e1,e2,e3) -> let j = Symbol.fresh() in let x = Symbol.fresh() in let k' = fun v -> AppExpr(VarVal j,[v]) in conv_expr ( fun v -> LetExpr([j,FunExpr([x],k (VarVal x))], IfExpr(v,conv_expr k' e2,conv_expr k' e3)) ) e1 | Syntax.FunExpr((x,_),_,e1) -> let k’ = Symbol.fresh() in let f = FunExpr([k’;x], conv_expr (fun v -> AppExpr(VarVal k’,[v])) e1) in k (VarVal f) … 2009/12/18 17言語処理系入門 8
  • 18.
    末尾呼び出しの最適化  以下の式を CPS変換すると  let f x = g (x + 1)  こうなるが こう最適化でき る let f k x = let f k x = let k’ a = k a in let b = x + 1 in let b = x + 1 in      g k b g k’ b  λ 計算において λx.M(x) を M に簡約化することを η 簡約と呼ぶ  「 C 言語的に考えると, int foo(int x) { return bar(x); } とい う関数定義がある場合,すべての foo の呼び出しを bar に 置き換えることができる」のと同じことである 2009/12/18 18言語処理系入門 8
  • 19.
    参考文献  A.W.Appel, “Compilingwith Continuations”, Cambridge Univ.Press, 1992.  田浦さんの資料  http://www.is.s.u-tokyo.ac.jp/vu/97/jugyo/processor/com 2009/12/18 19言語処理系入門 8
  • 20.
    演習問題  今週のサンプルプログラムを動かしてみよ  無駄な変数のコピー伝播を削除する最適化を 実装せよ let x = y in …x… … y …⇒  末尾呼び出しの最適化を実装せよ 2009/12/18 20言語処理系入門 8
  • 21.
    次回予定  日時:  2009年 12 月 25 日(金) 10 : 30 - 12 : 00  場所:  LB2 3F/A  内容:  コンパイル II :クロージャ変換 2009/12/10 21言語処理系入門 72009/12/18 21言語処理系入門 8