jvmlang.daitokai 1.0.0 MinCamlJを作ってみた
-
Upload
kazuyoshi-kamitsukasa -
Category
Software
-
view
206 -
download
0
Transcript of jvmlang.daitokai 1.0.0 MinCamlJを作ってみた
自己紹介
上司 和善 (かみつかさ かずよし)
@jou4
広島のメーカーのIT部門に所属。Javaフレームワークや開発支援ツールの整備を担当。
Javaは苦手。JVMは好き。
最近とあるきっかけから広島JUGを立ち上げることに。
本日の概要
MinCamlJを作ってみたら色々と大変だった、という話。
MinCamlJ (https://github.com/jou4/MinCamlJ)
MinCamlコンパイラをJavaで書き直したもの。Javaバイトコードを出力。
MinCaml
住井英二郎さんが開発された教育目的のコンパイラ。言語仕様はOCamlのサブセット。
MinCamlの言語仕様
型はbool, int, float, array, tuple。
四則演算、条件分岐、変数定義、関数定義、クロージャをサポート。
構文はOCamlを最小化したもの。
コンパイラの理解が目的なので言語仕様は最低限。
let rec f a b = a + b in print_int (f 1 2)
動機
コンパイラ開発が趣味。MinCamlはバイブルの1つ。 現在、JVMで動く言語とそのコンパイラを開発中。(全然人に見せ
られるレベルではないが…)
時間ができたらMinCamlもJVMで動くようにしたかった。
最近、広島JUGの立ち上げ準備とその宣伝活動中。 2/14 広島オープンセミナーで広報したところ、本日のイベントを紹
介された。
広島JUGの宣伝活動を兼ねて参加してみるか。ネタはどうしよう。
以前から温めていたMinCamlJで行こう。2週間あればいけるはず。
作戦
基本はJavaへの焼き直し 単純作業化。ロジックを考える時間を省く。
時間がないのでひとまずエイヤッで 体裁を整えるのはあとからで良い。
Java8のラムダを積極的に活用 MinCamlはOCamlで書かれているので高階関数など関数プログ
ラミング要素がてんこ盛り。ラムダがないとやっとれん。
クロージャ周りはinvokedynamic それ以外は基本的なバイトコードでいけるはず。
中間表現への変換 マシンコードへの変換各種最適化
MinCamlの構成
字句解析
構文解析
型推論
α変換
K正規化
β簡約
ネストしたletの簡約
インライン展開
定数畳み込み
不要定義削除
クロージャ変換
仮想マシンコード生成
13bit即値最適化
レジスタ割当
アセンブリ生成
そのまま置き換えできそう Javaバイトコード用にアレンジが必要
中間表現への変換 マシンコードへの変換各種最適化
MinCamlJの構成
字句解析
構文解析
型推論
α変換
K正規化
β簡約
ネストしたletの簡約
インライン展開
定数畳み込み
不要定義削除
クロージャ変換
Javaバイトコード生成
* 字句解析・構文解析にAntlr、Javaバイトコード生成にAsmを使用
main class
ラムダとinvokedynamic
今回最も興味があった部分であり、かつ、苦労した部分。
クロージャ周りの実装に使った。今回は実装していないが部分適用にも使える。クロージャの説明は分かりにくいのでここでは部分適用を例にとる。
MinCamlJでは、クロージャを、自由変数と呼ばれる変数を先に部分適用することで実装しているので無関係ではない。
let rec f a b = a + b inlet g = (f 1) ing 2
通常invoke*を使ってメソッドを呼ぶときは全ての引数を渡すことが前提。一部の引数だけを先に渡しておき、残りは後から渡したい。どうすれば?
ラムダとinvokedynamic
前頁の例は、Java8のラムダを使うと次のように書ける。
int f(int a, int b) { return a + b; }IntFunction<IntUnaryOperator> f_curry = a -> b -> f(a, b);
IntUnaryOperator g = f_curry.apply(1); g.applyAsInt(2);
これがどのようなバイトコードになるか探れば良さそう。
ラムダとinvokedynamic
意味不明…
static {};descriptor: ()Vflags: ACC_STATICCode:stack=1, locals=0, args_size=0
0: invokedynamic #15, 0 // IntFunction;5: putstatic #16 // Field f_curry8: return
…private static java.util.function.IntUnaryOperator lambda$0(int);
descriptor: (I)Ljava/util/function/IntUnaryOperator;flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETICCode:stack=1, locals=1, args_size=1
0: iload_01: invokedynamic #62, 0 // IntUnaryOperator;6: areturn
invokedynamicでラムダを得られるみたい
見知らぬラムダがある
ラムダとinvokedynamic
Java8では、invokedynamicとLambdaMetafactory#metafactoryを使ってラムダを実行時に生成している。
この情報を元に、LambdaMetafactory#metafactoryは、「ラムダを表すクラス(FunctionalInterfaceの実装クラス)」と「そのクラスをインスタンス化するためのメソッド(ハンドル)」を生成。
invokedynamicは、そのメソッドを実行しラムダのインスタンスを得る。
CallSite metafactory(MethodHandles.Lookup caller, // MethodHandleのルックアップ・コンテキストString invokedName, // FunctionalInterfaceの抽象メソッド(SAM)の名前MethodType invokedType, // invokedynamicで実行されるメソッドの型MethodType samMethodType, // SAMの型MethodHandle implMethod, // 目的のメソッドMethodType instantiatedMethodType // SAMの具体化した型
)
ラムダとinvokedynamic
InvokeDynamic
Bootstrap CallSite
MethodHandle
目的の処理
InvokeDynamic
Bootstrap(metafactory) CallSite
MethodHandle
ラムダをインスタンス化し、返却する
処理
ラムダを表すクラス
基本的なinvokedynamic ラムダを得る場合
(生成) (生成)
(生成)
(使用)
(初回呼出前)
(ポインタ) (ポインタ)
(初回呼出前)(呼出) (呼出)
ラムダとinvokedynamic
LambdaMetafactoryについて自分はこう理解した。
先ほどの例を再掲。
int f(int a, int b) { return a + b; }IntFunction<IntUnaryOperator> f_curry = a -> b -> f(a, b);
IntUnaryOperator g = f_curry.apply(1); g.applyAsInt(2);
IntFunction<IntUnaryOperator> f$(){return a -> f$$(a);
}IntUnaryOperator f$$(int a){
return b -> f(a, b); }IntUnaryOperator g = f$().apply(1); g.applyAsInt(2);
ラムダを分解しメソッドに振り分け。
Javaコンパイラが作るクラスファイルを覗くと、ラムダ式から、lambda.0, lambda.1, …, lambda.N というメソッドが作られていることがわかる。
ラムダとinvokedynamic
LambdaMetafactoryを使ってラムダを生成する。
int f(int a, int b) { … }
IntFunction<IntUnaryOperator> f$(){return a -> f$$(a);
}
IntUnaryOperator f$$(int a){return b -> f(a, b);
}
IntUnaryOperator g = f$().apply(1); g.applyAsInt(2);
このラムダを得るために、InvokeDyanmicとLambdaMetafactoryを用いる。
このラムダを得るために、InvokeDyanmicとLambdaMetafactoryを用いる。
ラムダとinvokedynamic
LambdaMetafactoryは実行時にラムダを表すクラスを生成する。
class F$ implements IntFunction<IntUnaryOperator> {public IntUnaryOperator apply(int a){return f$$(a);
}}class F$$ implements IntUnaryOperator {private int a;public F$$(int a){ this.a = a; }public int applyAsInt(int b){return f(a, b);
}}
IntFunction<IntUnaryOperator> f$(){ return new F$(); }IntUnaryOperator f$$(int a){ return new F$$(a); }
IntUnaryOperator g = new f$().apply(1); g.applyAsInt(2);
先に渡された引数はフィールドに保持。apply*が呼ばれたら次へ渡す。
ラムダの正体。匿名クラスとほぼ同じ。
ラムダとinvokedynamic
MinCamlJでは、カリー化した関数を派生させて、invokedynamicとLambdaMetafactoryによりラムダを生成している。関数適用の種類により使い分けている。
直接コールの場合は元々の関数を呼ぶ。
クロージャや部分適用(今回は実装していないが)の場合は、カリー化した関数に順に1つずつ関数を渡す。
let rec f a b = a + b // f から f$, f$$ を派生
f 1 2 // f を呼ぶ
let g = (f 1) in g 2 // f$を呼びラムダオブジェクトを取得、1 -> 2と引数を順番に渡す
バイトコード生成(参考)
// f : int -> int -> int// f a b = a + bmv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "f", "(II)I", null, null);mv.visitCode();mv.visitVarInsn(ILOAD, 0);mv.visitVarInsn(ILOAD, 1);mv.visitInsn(IADD);mv.visitInsn(IRETURN);mv.visitMaxs(2, 2);mv.visitEnd();
int f(inta, int b) { return a + b; }IntFunction<IntUnaryOperator> f$(){ return a -> f$$(a); }IntUnaryOperator f$$(int a){ return b -> f(a, b); }
バイトコード生成(参考)
// f$ : () -> (int -> (int -> int))// f$ () = \a -> f$$ amv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "f$", "()Ljava/util/function/IntFunction;", null, null);mv.visitCode();
// Functionを得るためのメソッドを動的に生成し、実行するmv.visitInvokeDynamicInsn("apply", "()Ljava/util/function/IntFunction;"
, new Handle(H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory",MethodType.methodType(
CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class,MethodType.class, MethodHandle.class, MethodType.class).toMethodDescriptorString())
, Type.getType(MethodType.methodType(Object.class, int.class).toMethodDescriptorString()), new Handle(H_INVOKESTATIC, className, "f$$",
MethodType.methodType(IntUnaryOperator.class, int.class).toMethodDescriptorString()), Type.getType(MethodType.methodType(IntUnaryOperator.class,
int.class).toMethodDescriptorString()));
mv.visitInsn(ARETURN);mv.visitMaxs(1, 0);mv.visitEnd();
バイトコード生成(参考)
// f$$ : int -> (int -> int)// f$$ a = \b -> f a bmv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "f$$", "(I)Ljava/util/function/IntUnaryOperator;", null, null);mv.visitCode();
// apply実行時に実行するメソッドの引数mv.visitVarInsn(ILOAD, 0);
// Functionを得るためのメソッドを動的に生成し、実行するmv.visitInvokeDynamicInsn("applyAsInt", "(I)Ljava/util/function/IntUnaryOperator;"
, new Handle(H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory", MethodType.methodType(
CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class,MethodType.class, MethodHandle.class, MethodType.class).toMethodDescriptorString())
, Type.getType(MethodType.methodType(int.class, int.class).toMethodDescriptorString()), new Handle(H_INVOKESTATIC, className, "f",
MethodType.methodType(int.class, int.class, int.class).toMethodDescriptorString()), Type.getType(MethodType.methodType(int.class, int.class).toMethodDescriptorString()));
mv.visitInsn(ARETURN);mv.visitMaxs(1, 1);mv.visitEnd();
Javaへの置き換えが面倒だった
代数的データ型やパターンマッチングをJavaに置き換えるのが面倒。
極力実装を急ぎたかったので細かいことは考えなかった
Javaなりの書き方があるけど今回は…
(* MinCaml *)| FNeg(e) -> FNeg(deref_term e)| FAdd(e1, e2) -> FAdd(deref_term e1, deref_term e2)
// MinCamlJ} else if (e instanceof SFNeg) {
SFNeg e1 = (SFNeg) e;return new SFNeg(derefTerm(e1.getExpr()));
} else if (e instanceof SFAdd) {SFAdd e1 = (SFAdd) e;return new SFAdd(derefTerm(e1.getLeft()), derefTerm(e1.getRight()));
Stack Map Frames がよくわからなかった
条件分岐や例外送出によるジャンプが発生する場合に必要
ローカル変数とスタックの状態を効率良く検証するのに必要という認識
最初は頑張ってみたけど結構面倒、やれなくはない
どのスロットにどの型が入っているか
COMPUTE_FRAMES オプションで楽をすることにした
オプションを使うとシミュレートにコストがかかるという記述を目にしたが、自前でやってもそのコストは同じような気がするし、自前でやるメリットは何なのか
今後勉強したい
型情報をJVMに与えるのが煩わしかった アセンブリならレジスタとメモリ操作だけなので型など不要
JVMが安全を確保してくれるゆえの制約と認識している あまり意識せずスタートしたが実際はかなり手間がかかった
プリミティブが絡むと Function, IntFunction, IntFunction<IntUnaryOperator , IntToDoubleFunction などを使い分けなければいけない さらにこれらはInterfaceの定義するメソッドが異なる
Functionならapply, IntToDoubleFunctionならapplyAsDoubleとか…
ジェネリックな型の場合キャストが必須 例えばFunction<T,R>だと戻り値の型がObjectなので、その後、他に
使う場合はキャストが必要 IntFunction<IntUnaryOperator>なら、引数にintを渡して戻って
きたObjectをIntUnaryOperatorにキャストして引数intを適用
まとめ
MinCamlのJava版を作ってみて、バイトコードについて学んだ。
バイトコードの扱いは難しいところもある。でも習熟すれば、JVMという優秀な実行環境を活用できる、のは魅力。
今回は、invokedynamicとLambdaMetafactoryの使い方を理解できて良かった。
MinCamlJは一応のところまでは実装・テストするつもり。機能追加は考えていない。
広島JUG 第1回のご案内
時間 テーマ スピーカー
10:00-11:00 Java Day Tokyoフィードバック 寺田 佳央さん
11:00-12:00 未定 未定(調整中)
4月25日(土) 10:00-12:00@グリーンアリーナ小会議室
http://hiroshima-jug.doorkeeper.jp/events/20660
タイムテーブル(調整中)
寺田さんにお越しいただきます!