30
Jvmlang.daitokai 1.0.0 MinCamlJを作ってみた K. Kamitsukasa

jvmlang.daitokai 1.0.0 MinCamlJを作ってみた

Embed Size (px)

Citation preview

Jvmlang.daitokai 1.0.0MinCamlJを作ってみた

K. Kamitsukasa

自己紹介

上司 和善 (かみつかさ かずよし)

@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)

導入

MinCamlJの背景と概要

動機

コンパイラ開発が趣味。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

ラムダと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();

感想戦

MinCamlJを作ってみて

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の宣伝

広島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

タイムテーブル(調整中)

寺田さんにお越しいただきます!