これはなに
僕が趣味で作っているMindソースコードを「.class」ファイルへ変換するプログラムの動作原理を説明します。
このプログラムの名前は「JMind」です。
※少し前に回想を書きましたが、あれは日記のノリだったのでここではどのように動作するかについて説明します。
※あくまで "Mindのソースコードを「.class」ファイルに変換するプログラム" ですが、長ったらしいので以下では恐れながら「コンパイラ」と言い換えます。
ソースコード:https://github.com/Snowman-s/JMind
「.class」ファイルについて
この記事を読んでいるような酔狂な方ならもうご存じのように、Javaではソースコードをまずコンパイルして、その生成されたファイルを起動引数に渡して「java」コマンドを実行する、という手順でプログラムが実行されます。
このソースコードをコンパイルしたものが「.class」ファイルですね。詳細な仕様はここ(公式ドキュメント、Java14)に書いてあります。
このコンパイラはMindソースコードをJava言語を一切介さずに「.class」ファイルに変換します。
言い換えれば、ソースコードの構文を読み取って「.class」ファイルにバイナリを書き込みます。
スタック言語
Mindはスタック言語です。
したがって基本的には、このコンパイラは一つ一つの単語を読み取りオペコードを生成する、という手順で解決できました。
例:単語「終わり」はオペコード「return」に対応する。
JVMにもMindと同じようにスタックの概念がありますが、JVMのスタックはMindと違ってメソッド終了時にスタックを破棄します。(https://docs.oracle.com/javase/specs/jvms/se14/html/jvms-6.html#jvms-6.5.return)
私の技術ではJVMのスタックをMindのスタックと同じように扱うことはできなかったので、
苦肉の策として一つのコレクションを使いまわすことにしました。
すなわち、メイン関数にて一つのコレクションをnewし、他の単語はそのコレクションを第一引数に受け取るように実装されました。
これであらゆる単語でスタックを共有でき、分割も簡単です。
現在(2020/7)はjava.util.LinkedListがその役目を担っています。
例:単語定義「メインとは」は
public static void main(String[] args){
LinkedList<Object> = new LinkedList<>(); //Mindにおけるスタック
//other opecodes...
}
に対応する。
例:単語定義「sayとは」は
public static void say(LinkedList<Object> list)[
//opecodes...
}
に対応する。(Mindでは一定のルールで送り仮名は無視されます。)
実際の動作
空のソースコード
メインとは。
これをこのコンパイラでコンパイルしてjavapで中身を見るとこうなります。
> jmindc test.src
> javap -v test
(前略...)
public class test
minor version: 0
major version: 52
flags: (0x0001) ACC_PUBLIC
this_class: #25 // test
super_class: #23 // java/lang/Object
interfaces: 0, fields: 0, methods: 1, attributes: 0
Constant pool:
#1 = Class #2 // java/util/LinkedList
#2 = Utf8 java/util/LinkedList
#3 = Methodref #1.#4 // java/util/LinkedList.addFirst:(Ljava/lang/Object;)V
#4 = NameAndType #5:#6 // addFirst:(Ljava/lang/Object;)V
#5 = Utf8 addFirst
#6 = Utf8 (Ljava/lang/Object;)V
#7 = Methodref #1.#8 // java/util/LinkedList.removeFirst:()Ljava/lang/Object;
#8 = NameAndType #9:#10 // removeFirst:()Ljava/lang/Object;
#9 = Utf8 removeFirst
#10 = Utf8 ()Ljava/lang/Object;
#11 = Methodref #1.#12 // java/util/LinkedList."<init>":()V
#12 = NameAndType #13:#14 // "<init>":()V
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Class #16 // "[Ljava/lang/String;"
#16 = Utf8 [Ljava/lang/String;
#17 = Methodref #1.#18 // java/util/LinkedList.remove:(I)Ljava/lang/Object;
#18 = NameAndType #19:#20 // remove:(I)Ljava/lang/Object;
#19 = Utf8 remove
#20 = Utf8 (I)Ljava/lang/Object;
#21 = Utf8 main
#22 = Utf8 ([Ljava/lang/String;)V
#23 = Class #24 // java/lang/Object
#24 = Utf8 java/lang/Object
#25 = Class #26 // test
#26 = Utf8 test
#27 = Utf8 Code
{
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=256, locals=2, args_size=1
0: new #1 // class java/util/LinkedList
3: dup
4: invokespecial #11 // Method java/util/LinkedList."<init>":()V
7: astore_1
8: return
}
まず、ランタイム定数フィールドを見ると分かるように、最初にランタイム定数フィールドに「スタックを使うのに必要な情報」を無条件で書き込みます。
つぎに「main」メソッドの中身も見ると、Mindのスタックに相当するLinkedList
を無条件で生成しているのが分かります。
全体を見ると、最低限動くだけの情報しかないですね、これはいけません。
JMindにおけるスタック
メインとは 「こんにちは! 世界!」を 一行表示すること。
これをこのコンパイラでコンパイルしてjavapで中身を見るとこうなります。
> jmindc test.src
> javap -v test
(前略...)
stack=256, locals=2, args_size=1
0: new #1 // class java/util/LinkedList
3: dup
4: invokespecial #11 // Method java/util/LinkedList."<init>":()V
7: astore_1
8: aload_1
9: ldc #21 // String こんにちは! 世界!
11: invokevirtual #3 // Method java/util/LinkedList.addFirst:(Ljava/lang/Object;)V
14: getstatic #36 // Field java/lang/System.out:Ljava/io/PrintStream;
17: aload_1
18: invokevirtual #7 // Method java/util/LinkedList.removeFirst:()Ljava/lang/Object;
21: checkcast #29 // class java/lang/String
24: invokevirtual #28 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: return
(...後略)
オペコードの8バイトから11バイト目が「こんにちは! 世界!」、14バイトから24バイトまでが「一行表示」に対応するオペコードです。
8-11をみると、スタックが入っている変数を参照し、StringオブジェクトをaddFirst()
、つまり積んでいることが分かります。
17-18をみると、スタックが入っている変数を参照し、オブジェクトをremoveFirst()
、つまり降ろしていることが分かります。
このようにして、スタック言語であるMindは、スタックにオブジェクトを積んだり降ろしたりしながら進んでいきます。
これをみてわかったように、JMindには必要ないオペコードがたくさん増えるという重大な欠点があります。
数値(int)の扱い
メインとは 1。
(これでも立派?なMindコードです。)
これをこのコンパイラでコンパイルしてjavapで中身を見るとこうなります。
> jmindc test.src
> javap -v test
(前略...)
stack=256, locals=2, args_size=1
0: new #1 // class java/util/LinkedList
3: dup
4: invokespecial #11 // Method java/util/LinkedList."<init>":()V
7: astore_1
8: aload_1
9: ldc #27 // int 1
11: invokestatic #26 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
14: invokevirtual #3 // Method java/util/LinkedList.addFirst:(Ljava/lang/Object;)V
17: return
(...後略)
このように、整数はスタックに積むためにいちいちInteger.valueOf()やInteger.intValue()を通すので実行が遅くなります。
(iconst_1も使ってないし。)
最適化を図っていますがいったいいつになるのか。
ただし、1程度のこの場合では動作に影響はほぼないと思われます。
おわりに
ここまで読んでくださった有難い人はいるのか
とりあえず動かすのに必死になっていてJMindのソースコードはまさにフランケンシュタインみたいにつぎはぎですけれど、この記事が誰かの参考になれば幸いです。(なるとは思えない)
以上です。