MinCamlをjbuilderでビルドする

org-modeで書いてはてなに公開したものを貼る。

文章さえ書けばはてなに移すのは楽だろうと思ったが、画像をアップロードしたりリンクを張り替えたりするのは結構な手間だったので次からはやめよう。

(以下は元記事)

この記事はIS17er Advent Calendar 2017の13日目の記事です。

jbuilderはOCaml用のビルドツールで、OCamlbuildOMake, OASISの親戚です。jbuilderを使うとOCamlを使って書いたプログラムのビルドが楽にできます。

MinCamlをjbuilderでビルドするのを試したところうまく設定できたので、この記事ではその方法を書きます。

フォークレポジトリはこれです。

下準備

MinCamlのソースコードをダウンロードし、 ./to_x86 というスクリプトを実行しておきます。

このコマンドがないとそもそもビルドができません。

jbuilderのインストール

opamから行えます。

opam install jbuilder

jbuildの記述

OCamlMakefileを使うのに Makefile が必要なように、jbuilderを使うには jbuild が必要です。

MinCamlと同じディレクトリに jbuild というファイルを作り、以下を記述します。 このファイルの書き方はかなり特殊だと思うので、ドキュメントを参照してください(自分でもこれであってるのかイマイチ自信がありません…)。

(jbuild_version 1)

;lexer, parser はモジュール名
(ocamllex (lexer))
(ocamlyacc (parser))

(library ; 実行形式以外のモジュールを詰めたライブラリを作る
 (; トップモジュールの名前はMinCaml
  (name           MinCaml)
  ; MinCamlモジュールに詰めるモジュールを書く
  ; 読み込むと入力を受け付け始めるMain, Anchor以外すべて
  ; ":standard"でこのフォルダにあるモジュールを表し、 "\"以降で除外するモジュールを選択している
  (modules        (:standard \ main anchor))
  ; 依存している外部ライブラリ
  (libraries      (str))
  ; cのスタブの名前(float.c)
  (c_names         (float))))

(executable ; 実行形式
 (; 実行形式の名前
  (name           main)
  ; Mainモジュールのみをコンパイルする
  (modules        Main)
  ; 依存ライブラリ: 上で定義したMinCamlモジュール
  (libraries      (MinCaml))))

各モジュール間の依存関係を書く必要はなく勝手に推論してくれます。もし依存関係にループがあった場合はどのようにループしているかを示してくれます。そのため新しいファイルを後から追加しても設定の変更は必要ありません。

なお後述する jbuilder utop という機能を利用するために、「 main.mlanchor.ml 以外のファイルをコンパイルしてライブラリ MinCaml を作り、そのライブラリを使って main.ml をコンパイルする」という、多少遠回りな方法を取りました。

main.mlの編集

main.ml が (同じディレクトリにある\*.mlファイルで定義されるモジュールではなく) MinCaml モジュール内のモジュールを使うように、 冒頭に open MinCaml を追加します。

open MinCaml (* 追加 *)

(* 以下は同じ *)
let limit = ref 1000

...

ビルド

以上の設定でビルドができるようになります。次のコマンドでビルドします。

jbuilder build main.exe

これで main.ml をコンパイルしたファイル ./_build/default/main.exe が作られます。

jubuilder utop コマンド

jubuilder utop コマンドを実行すると、そのフォルダの =jbuild=ファイルで定義されたライブラリのビルドが行われ、そのライブラリをロードしたutopが起動します。

上のように `jbuild` を記述した上で `jbuilder utop` を実行すると以下のようになります。

ライブラリのロードが行われるので、 モジュール名 (画像では`MinCaml`) を打つと補完が効きます。ソースコードの修正とデバッグを繰り返すときに便利です。

main.ml をコンパイルするのに MinCaml モジュールををわざわざ作った理由は、 Main モジュールがutopから読み込まれると標準入力を読み込み始めてしまうので除外する必要があったためです。

Makfileと組み合わせる

もともとのMinCamlはMakefileを使ってテストをするようになっているので、jbuilderと結合させます。

RESULT = min-caml
SRC   = $(shell ls *.ml *.mli)
TESTS = $(shell ls test/*.ml | grep -v toomanyargs)
JBUILD_BUILD_PATH=./_build/default

default: $(RESULT)

# jbuilderを使ったビルド
$(RESULT): $(JBUILD_BUILD_PATH)/main.exe $(SRC)
        cp $(JBUILD_BUILD_PATH)/main.exe $(RESULT)

$(JBUILD_BUILD_PATH)/main.exe: $(SRC)
        jbuilder build main.exe

# モジュールをすべてロードしたutopを起動する
utop:
        jbuilder utop

# 以下同じ
do_test: $(TESTS:%.ml=%.cmp)
...

これで `make`, `make do_test` が元のMinCamlと同じ意味で使えるようになり、 `make utop` でライブラリをロードしたutopが起動するようになります。

jbuilderの紹介はここまでです。

その他OCaml小ネタ

思いついたものを書きます。

install_printer

ocamlのREPLで #install_printer hoge とするとそれ以降プリント関数として使うことができます。

具体的には

# type foo = A | B;;
# let print_foo fmt = function ;; 型fooのプリント関数
  | A -> Format.fprintf fmt "this is value A"
  | B -> Format.fprintf fmt "this is value B";;
val print_foo : Format.formatter -> foo -> unit = <fun>
# A;;  ; print_installする前
- : foo = A
# B;;
 - : foo = B
# #install_printer print_foo;;
# A;;  ;; print_installした後
- : foo = this is value A
# B;;
- : foo = this is value B
# (A, B);;  ;; 組み合わさっていてもOK
- : foo * foo = (this is value A, this is value B)

とします。

なおプリント関数の型が t -> unit であっても動きますが、これは deprecated だそうです。(ソース)

バックトレースの有効化

REPLで Printexc.record_backtrace true とすると例外が出たときにどこから出たのか教えてくれます。

これがあればコンパイラをいじった結果 `Exception: Assert_failure` が全く予期しないところから生じても大丈夫です。

.ocamlinit

ocamlのREPLは立ち上がるときに同じディレクトリにある =.ocamlinit=を読むので、ここに毎回実行する処理を書いておくと楽です。

上のMinCamlでは

open MinCaml;;
(* エラーが出たときのバックトレースの有効化 *)
Printexc.record_backtrace true;;
(* プリント関数の有効化*)
#install_printer Syntax.print
#install_printer KNormal.print
#install_printer Closure.print
#install_printer Asm.print

と書いています。

flambdaの有効化

flambdaとはOCamlのネイティブコンパイラが持つ最適化機能の総称です。デフォルトの状態では(多分)有効化されないので、以下のコマンドで有効化されたコンパイラをビルドする必要があります。

opam switch 4.05.0+flambda

これで -O3 オプションを付けてコンパイルすれば最適化が有効になります。

自分が書いたシミュレーターでは40%程度高速化しました。