PPXとはOCamlのプリプロセッサ方式の一つです。 今流行ってます。
簡単に言うとOCamlの構文解析木(AST)を受け取ってASTを返すプログラムです。 入力されたASTを変化させることでプリプロセッシングを行います。
PPXプリプロセッサはOCamlコンパイラに-ppx
オプションを与えることで起動できます:
$ ocamlc -ppx ppx.exe x.ml
とすると、
- OCamlコンパイラがソースコード
x.ml
を構文解析しASTを作る - ASTを
-ppx
オプションで指定したPPXプリプロセッサppx.exe
に渡す - PPXプリプロセッサはASTを変化させて、OCamlコンパイラに返す
- OCamlコンパイラは変化したASTを入力として型検査、コンパイルを行う
という手順でプリプロセスとコンパイルが行われます。
PPXはOCamlのソースコードの文字列自体は取り扱わず、 その構文解析結果であるASTを入力と出力にします。 ですから、PPXの出力にさらに別のPPXを適用することができます:
$ ocamlc -ppx ppx.exe -ppx ppx2.exe x.ml
とするとx.ml
のASTがまずppx.exe
に渡され、次にその結果がppx2.exe
に渡されます。
このように複数のPPXを組み合わせることで、複数の別の機能のあるプリプロセッサをOCamlのソースコードに連続して適用できます。
OCamlの文法には、PPXがプリプロセス時に利用できるヒントとして、
- Attribute:
[@...]
,[@@...]
,[@@@...]
- Extension point:
[%...]%,
[%%...]`
が追加されました。なんじゃこりゃ。
AttributeはASTのノードに情報を足します:
1 [@hello]
と書くと1
という式にhello
という情報を付加できます。
...
...
...
よくわからない? よくわからないですね。 実際にASTを見てみましょう。 よけいわからなくなるかもですが。
OCamlには-dparsetree
という素晴らしい「ひみつ(undocumented)」機能があります。
これを使うとプログラムの構文解析結果をなんとなく見ることができます:
1 [@hello]
という内容のファイルx.ml
を作ってやると
$ ocamlc -dparsetree x.ml
[
structure_item (x.ml[1,0+0]..[1,0+1])
Pstr_eval
expression (x.ml[1,0+0]..[1,0+1])
attribute "hello"
[]
Pexp_constant PConst_int (1,None)
]
なんか出ました。これが上のソースコードを構文解析した結果のAST(構文木)です。
慣れていないとなんのことだかわからないかもしれませんが、
式expression
がx.ml
の一行目0文字目から1文字目にあって、
その内容は整数定数1
で、attributeとして"hello"
という文字列が付加されている、
と言われればなんとなくわかるのではないでしょうか。
PPXプリプロセッサはこのASTに付加されたattributeを利用してプログラム変換を行います。
あっ、使わなくてもいいですよ。単に無視すれば、コンパイラはattributeを無視してコンパイルを行います。
(ただし、後述のocaml.warning
やunboxed
などのOCamlコンパイラが使用するattributeを除く。)
Attribute [@...]
はその直前の式、型、変数に付加されます。
1 + 2 [@hello]
において、[@hello]
は2
に対して付加されます。1 + 2
全体に付加するには、
(1 + 2) [@hello]
と括弧を使います。
let x = 1
のような宣言があった時に、式ではなくて、宣言全体にattributeを付けたい、
ということがあります。その時に利用するのが[@@...]
です:
let x = 1 [@@hello]
@
の数で区別ってダサいと思われるかもしれませんが、
(let x = 1) [@hello]
とかとするとカッコ付けが大変なので。
[@@@...]
はファイル全体につけるattributeです。
いくつかのattributeはOCamlコンパイラにとって特別な意味があり、 これらのattributeを使うとコンパイラの挙動を変えることができます。 (それ以外のattributeはコンパイラに無視されます。)
例えばocaml.warning
attributeは警告スイッチを操作することができます。
例えば次のプログラム:
(* ocamlc -c x.ml *)
let f = function
| 1 -> true
これはコンパイルが警告8を出します:
$ ocamlc -c x.ml
File "x.ml", line 2, characters 8-30:
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
0
ここで、この宣言に[@@ocaml.warning "-8"]
をつけると、
この警告を消すことができます:
(* ocamlc -c x.ml *)
let f = function
| 1 -> true
[@@ocaml.warning "-8"]
$ ocamlc -c x.ml
(警告でなーい)
このattribute[@@..]
はこの宣言だけに影響するので、
もしプログラムの他の部分に警告8が出るところがあれば、
その警告はそのままです。
プログラム全体で警告8を無視したければ、[@@ocaml.warning "-8"]
のかわりに
[@@@ocaml.warning "-8"]
を使います。
他にも、OCamlコンパイラの挙動をコントロールできるattributeには、
C言語関数との間でunbox化されたレコードを使った最適化を使うための[@@unboxed]
attribute
(OCaml reference manualの20.3.2 Tuples and records http://caml.inria.fr/pub/docs/manual-ocaml/intfc.html を参照)
などがあります。
Attributeは存在するASTノードに情報を付加するものですが、 ではextension pointってなんでしょうか。
extension pointは式、型、宣言に置き換える
attributeは指定したASTのノードに情報を付加するものでしたが、 extension pointはASTのノードそのものを置き換えるためにあります。 例えば
let x = [%hello]
はlet
束縛の右辺の式が[%hello]
というextension pointになっています。
他にも
type t = [%hello]
のように型にも書けます。
attributeと同様、宣言を置き換えるextension point[%%..]
もあります。
(モジュール全体を置き換える、というのはあまり意味がないので[%%%..]
はありません。)
extension pointが書かれたOCamlプログラムはパースはできますが、コンパイルはできず、 エラーになります:
# let x = [%hello];;
Characters 10-15:
let x = [%hello];;
^^^^^
Uninterpreted extension 'hello'.
extension pointはPPXによってextension pointのない OCamlコードに書き換えられる必要があります。
let%xxx p = e
は[%%xxx let p = e]
let%xxx p = e in e'
は[%xxx let p = e in e']
begin%xxx .. end
は[%xxx ..]
parsing/parser.mly
に出てくるPほにゃ_extension
という文字列のあたりが
extension pointを扱っている文法
$ grep 'P[a-z]*_extension' parsing/parser.mly
するとどの文法要素にextension pointが書けるかわかります。
# りろんはわかった、で実際は?
## PPXはコマンド
PPXはOCamlのASTを受け取ってASTを返す関数のようなものだという事はわかりました。
では実際どうやって実装するか。
PPXの基本は外部実行コマンドです。例えばあなたの書いたPPXコマンドが`ppx.exe`という名前だとすると、それを利用するには:
```shell
$ ocamlc -ppx ppx.exe x.ml
とします。OCamlコンパイラはx.ml
を読み込んでパースした後、そのASTのバイナリ表現を
一時ファイルに保存して次のコマンドを実行します:
ppx.exe ファイル1 ファイル2
ファイル1は読み込み元:オリジナルのASTバイナリ表現を保存した一時ファイル。 ファイル2は書き込み先:PPXが変換したASTのバイナリ表現の保存先です。
この形式さえあっていれば、PPXコマンドは何を使っても構いません。 OCamlのプログラムである必要さえ無い。一番簡単なPPXはshellスクリプトで書けます:
#!/bin/sh
cat $1 > $2
これをppx.sh
とかいう名前にして実行できるようにすれば、
$ ocamlc -ppx ppx.sh x.ml
でちゃんと動きます。まあ入力をそのまま返すだけですけれど、チェーンすることもできます:
$ ocamlc -ppx ./ppx.sh -ppx ./ppx.sh x.ml
もうすこし複雑な事をしたければさすがにOCamlのコードを書かなければいけません。 PPXをOCamlで書くにはいくつか方法がありますが、一番単純なものを紹介します。 (もっと低レベルな方法も使えますが、それは紹介するAPI関数のソースから辿ってください。)
OCamlのソースコードにparsing/ast_mapper.mli
というモジュールがあって、
そこにAst_mapper.run_main
という関数があります。これを使います。
(* ppx.ml
Compile: ocamlfind ocamlc -o ppx.exe -package compiler-libs.common -linkpkg ppx.ml
*)
let () =
Ast_mapper.run_main
(fun args (* コマンド引数 *) ->
List.iter prerr_endline args; (* 引数を標準エラーに出してみる *)
Ast_mapper.default_mapper)
コンパイルするにはppx.ml
という名前で保存して、
$ ocamlfind ocamlc -o ppx.exe -package compiler-libs.common -linkpkg ppx.ml
とします。findlib
の警告が出ますが無視して結構です。
args
はPPXコマンドに与えられた引数です。最後の2つの引数はASTファイルの入出力に使われるのでそれは除外されていますAst_mapper
を始め、OCamlのASTをいじるときにはcompiler-libs.common
というパッケージを使います。OCamlコンパイラのパーサ部分のモジュールが入っています。compiler-libs.common
を使ったコンパイルやリンクのオプションを手で書くの面倒なのでocamlfind
にやってもらいます。中で何が起こっているか知りたい人はocamlfind ocamlc -verbose -o ppx.exe -package compiler-libs.common -linkpkg ppx.ml
してください。
Ast_mapper.default_mapper
って何と言う前に動かしてみます:
$ ocamlc -ppx ./ppx.exe x.ml
x.ml
がちゃんとしたOCamlのコードであれば、特に何も起こらず、普通にコンパイルできるはずです。
PPXコマンドに引数を与えて、ちゃんとPPXの関数に引数が渡っているのを確認します:
$ ocamlc -ppx "./ppx.exe a -hello" x.ml
a
-hello
上のOCamlで書く初めてのPPXの例ではAst_mapper.default_mapper
という、
Ast_mapper.mapper
型のデータを使いました。このmapper
はOCamlのASTをASTに写す「関数」です。
「関数」とカギカッコなのは、実際は関数群になっているからです。
parsing/ast_mapper.mli
を参照してください:
(** {1 A generic Parsetree mapper} *)
type mapper = {
attribute: mapper -> attribute -> attribute;
attributes: mapper -> attribute list -> attribute list;
case: mapper -> case -> case;
cases: mapper -> case list -> case list;
class_declaration: mapper -> class_declaration -> class_declaration;
class_description: mapper -> class_description -> class_description;
...
山のように関数メンバのあるレコードです。