OCamlのlambda IRをいじる(関数)
前回に続いて、ローカルとモジュールレベルでの関数定義がどうlambda IRにコンパイルされるかを見ていく。
ローカル関数
まずはこんな感じの簡単な関数:
let () = let f x = x + 1 in print_int @@ f 5
ocamlopt -c -dlambda localfunc.ml
などとすると:
(seq (let (*match*/84 = (apply (field 43 (global Stdlib!)) (let (x/85 =[int] 5) (+ x/85 1)))) 0a) 0a)
見てのとおり、関数定義が消え、呼び出しがlet
に変換される、というインライン化が行われている。
ちなみにlambda IR変換ではいくつかの最適化も行われる。そんな最適化をする前の「変換しただけのlambda IR」をみたい場合は-dlambda
ではなく-drawlambda
フラグを渡す。
ocamlopt -c -drawlambda localfunc.ml
の結果:
(seq (let (*match*/84 = (let (f/80 = (function x/82[int] : int (+ x/82 1))) (dirapply (field 43 (global Stdlib!)) (apply f/80 5)))) 0a) 0a)
しっかり関数定義・呼び出しが残っているのがわかる。興味深いポイントとしては:
(function x/82[int] : int (+ x/82 1))
のように整数を受け取る・返す場合は型アノテーションが残っている。返り値が文字列などの場合はこのような型アノテーションはないので、=[int]
と同じくboxing省略の関係なのだと思うがまだよくわからない- 見慣れない
dirapply
というキーワードが(print_int
呼び出しのために)出てくる。apply
との違いはよくわからない。direct applyの意味だと思うが・・・
引数2のローカル関数
引数2の場合も前回と同様:
let () = let f x y = x + y in print_int @@ f 1 2
-dlambda
だと:
(seq (let (*match*/85 = (apply (field 43 (global Stdlib!)) (let (x/86 =[int] 1 y/87 =[int] 2) (+ x/86 y/87)))) 0a) 0a)
やはり関数定義・呼び出しが変換で消えている。
-drawlambda
だと残っている&型アノテーションつき:
(seq (let (*match*/85 = (let (f/80 = (function x/82[int] y/83[int] : int (+ x/82 y/83))) (dirapply (field 43 (global Stdlib!)) (apply f/80 1 2)))) 0a) 0a)
ローカル関数の部分適用
先ほどのローカル関数を部分適用したものを別の変数に束縛してみる:
let () = let f x y = x + y in let g = f 1 in print_int @@ g 2
すると-dlambda
でも関数定義・呼び出しのまま残る:
(seq (let (*match*/86 = (let (f/80 = (function x/82[int] y/83[int] : int (+ x/82 y/83)) g/84 = (apply f/80 1)) (apply (field 43 (global Stdlib!)) (apply g/84 2)))) 0a) 0a)
別のIR変換ステップ(例えばclosureつきのclambdaへの変換)で最適化される可能性はあるが、少なくともlambda IRの段階ではこのような簡単な部分適用でも最適化が行われなくなる、ということは面白い。
引数が関数適用の結果
関数の引数部分に別の関数適用の式を入れてみる:
let () = let f x y = x + y in let g x = x - 1 in print_int @@ f 1 (g 2)
この場合は-dlambda
だと普通に関数がインライン化されてlet
に置き換わる:
(seq (let (*match*/88 = (apply (field 43 (global Stdlib!)) (let (x/90 =[int] 1 y/91 =[int] (let (x/89 =[int] 2) (- x/89 1))) (+ x/90 y/91)))) 0a) 0a)
f
の適用とg
の適用で別々のネストしたlet
式になっている。(よく考えると自然な変換ではある)
関数定義内で別のローカル関数を使う
まずローカル関数g
を定義し、それをローカル関数f
の定義内で使う:
let () = let g x = x + 1 in let f x y = x + (g y) in print_int @@ f 1 2
-dlambda
でコンパイルすると関数定義は両方消える:
(seq (let (*match*/88 = (apply (field 43 (global Stdlib!)) (let (x/90 =[int] 1 y/91 =[int] 2) (+ x/90 (+ y/91 1))))) 0a) 0a)
思ったよりシンプルな形になっている。
(let (x/90 =[int] 1 y/91 =[int] 2) (+ x/90 (+ y/91 1)))
の部分が
(let (x/90 =[int] 1 y/91 =[int] 2) (+ x/90 (let (x/92 =[int] y/91) (+ x/92 1))))
になるかと思っていた。
変数を別の変数に束縛
関数関係なくこのようなコードを試してみたら:
let () = let x = 1 in let y = x in print_int @@ x + y
-dlambda
だと:
(seq (let (*match*/83 = (let (x/80 =[int] 1) (apply (field 43 (global Stdlib!)) (+ x/80 x/80)))) 0a) 0a)
とy
が消えている。
-drawlambda
だと:
(seq (let (*match*/83 = (let (x/80 =[int] 1 y/81 =[int] x/80) (dirapply (field 43 (global Stdlib!)) (+ x/80 y/81)))) 0a) 0a)
とy
が残っているので、lambda IR内での最適化として意味のないletによる変数束縛は省略されるようだ。
モジュールトップレベルでの関数定義
let f x = x + 1 let () = print_int @@ f 5
-dlambda
:
(seq (let (f/80 = (function x/82[int] : int (+ x/82 1))) (setfield_ptr(root-init) 0 (global Func!) f/80)) (let (*match*/85 = (apply (field 43 (global Stdlib!)) (apply (field 0 (global Func!)) 5))) 0a) 0a)
正直あまり面白みはない。トップレベルにある関数に関してはlambda IRの段階ではインライン化は効かない。
次回
次はタプルがどうコンパイルされるかをみていく。