ここ数日、LLVMについて少し勉強している。そもそもなんでLLVMを触り始めたかというと、Twitter上で「今コンパイル欲求に駆られている」と(割と何も考えずに)つぶやいたところ、

  1. 「じゃあDTコンパイルしようぜ」
  2. srd!
  3. でもあれコンパイラとは名ばかりでぶっちゃけ文字列をRubyコードにtranslateしてるだけだったりするね
  4. 「DTパーサを改良してLLVMにブリッジして、クロスプラットフォーム環境で高速に動作するDT処理系にするといいよ」
  5. 何その無駄に敷居高いお仕事!誰得!でもなんか面白そう!

というやりとりがあって、じゃあ本当に誰が得するのかわからないけど面白そうだからLLVMをバックエンドで使ってesotericがサポートしてるコードからバイナリを生成するコンパイラ作ろうぜ、という流れになったから。実にLLVMの無駄遣いですね。この記事ブクマするときは「LLVMの無駄遣い」ってタグを付けるといいですよ。

LLVMって何さ?

まずそもそも、iPod touchのJail Breakに手を出そうとしたときにちょこっと調べたりした(iPhoneSDK発表以前、JBしたiPod touch/iPhone向けにアプリを開発する際にはLLVMを使ってiPhone OS向けにコンパイルする必要があった。最近は追ってないので今どうなってるかは知らない)程度だったので、実のところLLVMが何をするものなのかちゃんと分かってなくて、どうすんべーとまず調べ直すところから始める。

LLVMとはLow Level Virtual Machineの略で、Lightweight Languageとは特に関係がないようだ(お約束)。知ってたよ。うん。知ってたってば。調べた結果、コンパイラ基盤と呼ばれるものでコンパイラを作るのに使うライブラリ・ツール群であること、VMの名の通り一度LLVMで処理できる中間言語にした後、それをLLVMのツールで実行したりネイティブのバイナリに変換したりして使うということ、コンパイル時や実行時に様々な最適化を施してくれて、より実行効率の良いプログラムを作れるようにしてくれるらしいこと、なんかが分かった。他にも色々あるけど、それはきっと詳しい人がどっかでまとめてくれてるから割愛。うん。分かんなかったわけじゃないよ。違うってば。

あと、llvm-gccとかclangとかを使えばC/C++/Objective-CのコードをLLVMに対応させてコンパイルできるようだ。それはそれで面白いけど今回は「自作の言語をコンパイルするのにLLVMを使う」のが目的なので、どっちかというとそれ相当のものを作ることになるのでとりあえずその辺は後で見ることにする。DTコンパイラをllvm-gccと並べてよいものかとかそこにツッコミを入れるのは無しの方向で。石を投げたりしないで下さい。

Rubyから使う

大雑把に何なのかはわかったけど、さてどう使おう。C++では使えるようだしサンプルもC++のコードが並んでるんだけど、C++書いたことないしなぁ、ってかRubyで書いてたのをC++を勉強しつつ書き直すとか面倒くさいなぁ、むしろObjC書きたいなぁObjC、とか段々脱線しはじめたときに、RubyからLLVMを使うgemがあるのを見つけた。

これこれ、これが欲しかったのよと早速gem installしてみたところ、コンパイルは通ってちゃんとインストールできるんだけど、サンプルコードを動かしてみたところdylibのload errorが出る。MacOSX10.5のRuby1.8.7(LLVM、RubyともにMacPortsからインストール)でしか試してないけど、どうも上手く行かない様子。調べてみようと思ってgithubからソース落としてきて、ローカルでgemをビルドするところからやってみたら、そっちはちゃんと動くようになった。あれかなぁ、リンクするライブラリとか違うとこ見ちゃうのかな。まあいいや、とりあえず深追いするのはもう少し後にしよう。

大雑把にllvmrubyの使い方を説明すると、大体以下の通り。

  1. LLVM::Moduleのオブジェクトを作る
  2. LLVM::Module#external_functionで外部のライブラリの関数を使えるようにする
    • CやC++のライブラリから取ってこれるようなので、Rubyの組み込み関数とかも呼べる
  3. LLVM::Module#get_or_insert_functionで関数を定義する
    • 上のメソッドがLLVM::Functionのオブジェクトを返すので、それのcreate_blockメソッドでブロック(関数の中身)を用意する
    • ブロックのBuilderを取得して、それに対してごにょごにょして中身を作る
    • b = method.create_block.builderですね
  4. LLVM::Module#write_bitcodeでコンパイルしてファイルに書き出すか、LLVM::ExecutionEngineのrun系メソッドでそのまま実行する

ちなみにこれはLLVMのバイトコードを生成する手順だけど、他にRuby1.9のVMの命令セット互換の中間表現を実行するruby_vm.rbが添付されてたり、Rubyのコードをllvmrubyを使ってJITコンパイルするものだと思われる(ちゃんと見てないので間違ってたらツッコミ歓迎)yarv2llvmってgemがあったりする。ただまぁ、「DTをRuby上で高速に実行したい」のではなくて「DTからスタンドアローンのネイティブプログラムを生成したい」ので、こっちは今回は使わない。Cでの拡張をしないアプローチでRubyのプログラムを高速化したくなったらこの辺のを使うのかな、多分。ruby_vm.rbの方は後でVMを実装するときに参考になりそうだからそのときにまた読もう。

ぐぬぬ

ここまでは何とか辿りついた。実はここまでで結構試行錯誤してるけど、まぁ何とかここまではよしとする。で、だ。ここで問題なのは、前回のバージョンアップで中間表現をRuby2Rubyで扱えるASTにしてしまったこと。何が問題かって、これのコンパイラを作るってのはつまりRubyのコンパイラを作るってことになるわけですよね。うん。…できるかっ!

そもそも言語処理系の基礎知識も何もない状態でネタで始めた言語作り、いきなりRubyのASTを処理できるコンパイラを作るとか正直敷居が高すぎるので、もう一度中間表現の見直しをすることにした。もっとシンプルなASTにしておけばコンパイラの方でも扱いやすいし、前回ぶっ殺したEsotericVMの方ももう少し楽に作れるかもしれない。

そこで今少しずつ仕様の見直しをしつつコンパイラを書きつつ進めてるのだけど、Low Level Virtual Machineと言うだけあってライブラリ側では大分コアなところしか用意はしてくれないわけですよね。てことで諸々のデータ型とか最低限必要な組み込みの関数とか自分で用意してやらなきゃいけないわけで、その辺考えてたら大分混乱してきた。

どう考えてもDTにオブジェクトシステムとかGCとかは要らないので適当に端折ろうとは思うのだけど、esotericはパーサ部分で大方の言語固有の要素を解決しちゃってコンパイラやVMの方は汎用的な言語処理系の基盤にしたいわけで、そうすると一応はちゃんと考えて設計しないといけないよなぁ。いかにミユビナマケモノとはいえそろそろノリでなんとかできる範囲を超えてきた感じ。助けてくだしあ。とりあえずお薦めの書籍とか誰か教えてくれると嬉しいなぁ。情報系の学生が読むようなの。