Arantium Maestum

プログラミング、囲碁、読書の話題

OCamlでLLVM JITを試した

自作言語熱が高まっていて、バックエンドをLLVMでやりたいと思っている。

OCamlLLVMというとLLVMプロジェクト公式のKaleidoscope言語をOCamlで実装するチュートリアルが有名、なのだが、10年近く前に壊れたAPIを使っていたりと実際に使えるものではなく、最近のチュートリアルサイトからは消されている。

最大の問題はJITコンパイルするためのAPIが変更され、コンパイルされた結果を実行するためにはC関数ポインタを返すLlvm_executionengine.get_function_addressを使って、FFIでその関数を呼び出す必要があるようになった点。

FFIするためにLLVMとは別にCtypesというライブラリを入れる必要が出てくる。

lists.llvm.org

Ctypesを使ったFFIの詳細に関してはReal World OCamlに詳しく載っている:

dev.realworldocaml.org

Ctypesに関して特筆すべきは(Real World OCamlより引用):

open Foreign

let initscr =
  foreign "initscr" (void @-> returning window)

上記のvoid @-> returning windowのような「Cの関数型を表現するデータ」の存在である。

Llvm_executionengine.get_function_addressシグニチャstring -> 'a Ctypes.typ -> llexecutionengine -> 'aなので、このC言語の型の表現を作成して渡してやる必要がある。

ありがたいことにKaleidoscopeチュートリアルのコードをこの新しいAPIに合わせて書いている人がいた:

discourse.llvm.org

github.com

文章付きのチュートリアル形式にはなっていないが、非常に参考になる。

勉強がてら最小構成、単一ファイル(ビルド用のduneコンフィグ以外)で完結する例を書いてgistに上げてみた:

gist.github.com

実行すると以下のような出力になる:

define double @__toplevel1() #0 {
entry:
  ret double 3.000000e+00
}
3.

とりあえずOCamlプログラム内でLLVM IRを作成しJITコンパイルして実行できるようになっている。

よく見ると

let n = L.const_float double_type 1.0
let m = L.const_float double_type 2.0
let ret_val = L.build_add n m "addtmp" builder

の部分が定数畳み込みで簡略化されているのがわかる。最適化パスは指定していないので何故こうなるのかはよくわからないが・・・

あと多分Llvm_analysis.verify_function the_functionを省略しているのは危険なんじゃないかという気がしている。具体的にどれだけの問題が起き得るのかはわからないが、メモリ安全性などは完全に度外視された問題のあるコードが実行され得るのではないか。悪魔が鼻から出てくるかもしれない。

とにかくとりあえずOCamlLLVMバインディングを使ってLLVM IRを作成し、Llvm_executionengineとCtypesを使ってJITコンパイル&実行できたので、これから自作言語のバックエンドとして使っていきたい。