nFact

n!

自作JVM言語Karaffeについて

nokoです。2014年ぐらいから趣味でKaraffe(カラフェと読みます)という自作のJVM言語を開発しています。

この記事は、Karaffeの開発のきっかけみたいなところを書きます。 細かい実装についてはほとんど触れないのでまた別の機会に。

Karaffeとは

Karaffeは、オブジェクト指向(クラスベース)の静的型付けなプログラミング言語です。
Javaバイトコード(classファイル)を生成するので、Java VMで実行することができます。

実行用のコマンドを持っていないので、生成されたclassファイルをそのままjavaコマンドで実行します。 現在コード生成部に手を入れていて、CIも落ちてる状態なので、少し前のバージョンを使わないとHello Worldすら出来ないと思います。

Karaffe開発のきっかけ

(おそらく)全てはぱらつり氏のこのツイートから始まりました。

このツイートと、その場の雰囲気のノリと勢いでPaLTreeというOrganizationが作られました。 色んな人がいてワイワイと開発やら議論をしていました。(確認したらメンバーは9人も居ました。意外と多かった。。)

Karaffeの前身 = notepad-java

PaLTreeでは「メモ帳言語(notepad-lang)」という枠で様々な実装が出てきました。その中で、notepad-langをJavaで実装したのが私でした。(Javaの他にC#Rubyによる実装がありました)

メモ帳でも書けると言っても普段の環境と違いすぎてあんまりピンと来ないかもしれませんが、現代のプログラミングでは当たり前となっている機能がありません。 例えば、シンタックスハイライトや補完はもちろん、オートインデントも無いのでインデントをしようものならTabやSpaceを連打する必要があります。 なので、インデントやハイライトが無くとも読みやすく、補完なしで全部手打ちしても記述が可能である必要がありました。

その結果生まれたのが、インタプリタにより実行するシンプルな(?)手続き型言語でした。 メモ帳で記述することを前提に考えられていたのと、コンパイラインタプリタも開発したことが無い中での設計だったので、とても奇抜な仕様が爆誕したのは言うまでもありません。

他のメモ帳言語にも無い一番ユニークだった仕様は「{}や()を使っても入れ子になるとメモ帳だと読みにくいので、改行2つでブロックを構成する仕様」だったと思います。

このあたりを記した当時のドキュメントは、現在のnokok/Karaffeには入っていません。 動的言語/静的言語、静的型付け/静的型付け、インタプリタやら何やらを混同したような記述が多数あり、かつ実装もほぼ無く恥ずかしいのでここには載せません。

notepad-langから派生したり、触発されて開発されたプログラミング言語の中で、現在まで開発が続いているのはKaraffeのみです。 他にはLuryというプログラミング言語がありますが、現在開発が休止しているようです。

Karaffeの誕生

手続き型 -> 関数型言語っぽいものにするか? -> やっぱオブジェクト指向だな、という迷走試行錯誤を経て今の形になりました。 この試行錯誤の過程で、notepad-javaは早々にKaraffeという名前になりました。
Karaffeになった頃にはメモ帳の要素は薄まり、だんだん自分が作りたい言語に寄ってくるようになりました。

Karaffeロゴ
Karaffeロゴ
このKaraffeのロゴは、コーヒーがカラフェに貯まって揺らいでいる様子を表現しています。(本来カラフェにコーヒーは入れないような気もするのですが気にしないようにします)

Karaffeが目指しているもの

Karaffeは、Javaにはまだ存在しない現代的な文法や仕様を取り入れながら、よりシームレスなJavaとの相互運用性を実現することを第一に目指しています。

既存のJVM言語は(主語がでかい)、「JVM言語からJavaを使うこと」は考えていても、「JavaからそのJVM言語を使うこと」はあまり前提とされていない感じがしています。 真の 相互 運用性を考えたときに、「対象のライブラリがJava製なのか他のJVM言語製なのか」を考えなくても良いというのが理想像ではないかと考えています。 Karaffeが吐き出すクラスファイルやjarファイルが、(たとえKaraffeにしか無い言語仕様を使ったとしても)Karaffe製であることが言われないと分からないレベルにまで、様々な要素をJavaに寄せられたら良いなぁと思っています。

そもそもKaraffeのような個人開発の言語は、普及して一般の人にまで使ってもらえることは通常考えにくいと思っています。既存の言語を置き換える意図もありません。なので、あくまで自分だけが使う(普段使っている)Javaと組み合わせることを前提とした高度な相互運用性を持つ言語にしたいと思ったのもあります。

Karaffeはだいぶ後発の言語なので、Javaのモジュールシステムにネイティブで対応したいなぁとか色々ありますが、まずは相互運用性が第一で、他は二の次、三の次です。とりあえずはシンプルなオブジェクト指向言語にできればと思っています。

Karaffeの今

今のKaraffeは多分5回目ぐらいの書き直しをしたバージョンです。 「ぐらい」と言ったのは、それだけ何回も失敗して書き直しているために覚えてないからです。 今のバージョンになったのは2018年11月です。半年弱ですね(今回は歴史ごと飛ばして書き直しましたのでfirst commitです) first commit · nokok/Karaffe@0d62de1 · GitHub

夢はでかいのですが、コンパイラを開発できるレベルの知識もなければ実装力も全然ついてきてないので、書き直した実装の中でまともに動作するものを作り出せた試しがありません。 まだまだ絵に描いた餅状態なので、どうにかしていきたい感じですね。

初期のバージョンはJavaCCとか色々使ってましたが、今はレキサ/パーサーにANTLRv4を使用しています。
この記事はこのぐらいにしておきつつ、今後はKaraffeのことをもっと書いていきたいと思います。

Javaのinstanceofについて言語仕様を見てみる

Javaにおけるinstanceofについて言語仕様がどうなっているかを調べた。

文法上の話

instanceof演算子 (引用 https://docs.oracle.com/javase/specs/jls/se11/html/jls-15.html#jls-15.20.2 )

RelationalExpression:
ShiftExpression 
RelationalExpression < ShiftExpression 
RelationalExpression > ShiftExpression 
RelationalExpression <= ShiftExpression 
RelationalExpression >= ShiftExpression 
RelationalExpression instanceof ReferenceType

これを見ると、instanceofは左辺に式、右辺に参照型を受け取ることと、関係演算子と同等の優先度として処理されることがわかる。言語仕様を読む限り、左側に記述する式は参照型にキャスト可能である必要がある。
キャスト不可能な場合は、instanceofの右側は常に参照型を受け取ることから、式を評価しても常にfalse(=デッドコード)となるためエラーとしている模様。つまり1 instanceof Integerは(RelationalExpression > ... > Literal > IntegerLiteralという規則が適用できるため)文法上パースはできるものの、コンパイル時エラーとなる。
ここでオートボクシングは効かない。 また、instanceofは当然実行時に判定が行われるが、Java VMバイトコードが実行される際に全ての型で完全な型情報を利用できるわけではない。そのため、instanceof演算子に記述する参照型は、実行時に完全な型情報を利用できる具象化可能型(Reifiable Types)である必要がある。具象化可能型であるには、次の6つの条件のうち1つ以上満たす必要がある。

  • ジェネリック型またはインターフェース型
  • raw型
  • プリミティブ型
  • 要素が具象化可能型で構成された配列型
  • 全ての型引数がワイルドカードであるパラメータ化された型

class A<T> {
}
var a = new A<Integer>();
a instanceof A<?> // true
a instanceof A<Integer> //エラー。 A<Integer>は非具象化可能型
a instanceof A<Object> //エラー。
a instanceof A<? extends Object> //エラー。 境界も記述できない。
  • ネストされた型で、 . で区切られたそれぞれの型が、全て具象化可能型である

class A<T> {
  class B {}
}
var a = new A<Object>()
var b = a.new B()
b instanceof A<?>.B // これは多分こういうことだと思われる。A<?>とBはどちらも具象化可能型なので比較可能。結果はtrueとなる
b instanceof A<Object>.B // エラー。A<Object>は非具象化可能型

Reifiable Types

ここまでが文法上の話。

instanceofのバイトコード

次のコードは、

Integer i = 1;
if(i instanceof Integer) {
  // 処理A
} else {
  // 処理B
}

次のようなバイトコードに変換される

 0: iconst_1
 1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
 4: astore_1
 5: aload_1
 6: instanceof    #3                  // class java/lang/Integer
 9: ifeq          23
    // 処理A
20: goto          31
    // 処理B
31: return

Java上のinstanceofは、バイトコードに変換されてもそのままinstanceofとなる。 Java VMが実行するバイトコードinstanceofは、スタックの一番上に積まれている参照の型(この場合iの型)と、(コンパイル時にinstanceofに渡された)java.lang.Integerを比較する。

型の比較

https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-6.html#jvms-6.5.instanceof

s instanceof T という式で、sの型をSとしたときに次のいずれかの条件を満たせばこの式の結果はtrueとなる。

1. TS がどちらもクラス型で、 STと同じクラスまたはTのサブクラスである

Number s = 1;
s instanceof Integer // true. SはNumber(=クラス型)であり、Tが同じIntegerである
s instanceof Number // true. IntegerはNumberのサブクラスである
s instanceof String // false. IntegerはStringのサブクラスではない

2. T がインターフェース型のとき、S は インターフェースT を実装している

List<String> s = new ArrayList<>();
s instanceof List<?> // true. ArrayListはListを実装している
s instanceof Set<?> // false. ArrayListはSetを実装していない

3. S が配列型で、T がクラス型であれば、TObjectである

Object s = new Integer[1];
s instanceof Object // true. Sが配列型かつTはクラス型で、TがObjectである。
s instanceof Integer // false. Tはクラス型であるが、Objectではない。

4. S が配列型で、T がインターフェース型であれば、Tが配列によって実装されているインターフェースである

配列によって実装されているインターフェースというのはつまりCloneableSerializableのインターフェースのことである。

Object[] s = new Integer[1];
s instanceof Cloneable // true. Cloneableは配列によって実装されている
s instanceof java.io.Serializable // true. Serializableも配列によって実装されている
s instanceof List<?> // false. 配列はListを実装していない

5. ST が配列型のとき、配列型 T と 配列型 S の要素の型が同じプリミティブ型である

Object s = new int[0];
s instanceof int[] // true. TとSがどちらも配列型であり、要素の型がintである
s instanceof float[] //false. 要素の型が異なる。

6. ST が配列型かつ要素の型がどちらも参照型のとき、要素の型がS から T へ実行時にキャスト可能である

Object s = new Integer[1];
s instanceof Integer[] // true. IntegerからIntegerは同じ型のためキャスト可能である
s instanceof Number[] //true. IntegerからNumberはサブタイプの関係にあるためキャスト可能である
s instanceof String[] //false. IntegerからStringはキャスト不可能である。

以上6条件が、instanceofでtrueが返る条件となっている。

それ以外の場合

上記の条件が適用されるのは、t が非nullかつSの型が解決された後となる。 逆に言うと、t がnullのときにはinstanceofSの型解決すら行わずにfalseをプッシュする。(※もちろんコンパイル時には解決できている必要がある。)

このように、判定する式がnullであれば、実行時にそのクラスが存在するかどうかは(JVMにとっては)関係ないことであることがわかる。

もっと深い話

よくよく思い出したらJJUG CCCでセッションがあった。 Java VMの実装にまで踏み込んだ深い話。

www.slideshare.net

まとめ

比較する型はなんでも良いわけではない(参照型かつ具象化可能型である必要がある)ことがわかってよかった。 一つ一つの判定条件は考えてみれば当たり前かもしれないが、このように言語化できるとすっきりする。

参考文献