diff --git a/ppx_2018.md b/ppx_2018.md index 35b4c34..8cc5143 100644 --- a/ppx_2018.md +++ b/ppx_2018.md @@ -4,6 +4,7 @@ PPXとはOCamlのプリプロセッサ方式の一つです。 今流行ってます。 + 簡単に言うとOCamlの構文解析木(AST)を受け取ってASTを返すプログラムです。 入力されたASTを変化させることでプリプロセッシングを行います。 @@ -30,10 +31,8 @@ PPXはOCamlのソースコードの文字列自体は取り扱わず、 $ ocamlc -ppx ppx.exe -ppx ppx2.exe x.ml ``` -とすると`x.ml`のASTがまず`ppx.exe`に渡され、 -次にその結果が`ppx2.exe`に渡されます。 -このように複数のPPXを組み合わせることで、 -複数の別の機能のあるプリプロセッサをOCamlのソースコードに連続して適用できます。 +とすると`x.ml`のASTがまず`ppx.exe`に渡され、次にその結果が`ppx2.exe`に渡されます。 +このように複数のPPXを組み合わせることで、複数の別の機能のあるプリプロセッサをOCamlのソースコードに連続して適用できます。 # Attribute と extension point @@ -42,7 +41,9 @@ OCamlの文法には、PPXがプリプロセス時に利用できるヒントと * Attribute: `[@...]`, `[@@...]`, `[@@@...]` * Extension point: `[%...]%, `[%%...]` -が追加されています。なんじゃこりゃ。 +が追加されました。なんじゃこりゃ。 + +## Attribute AttributeはASTのノードに情報を足します: @@ -52,7 +53,7 @@ AttributeはASTのノードに情報を足します: と書くと`1`という式に`hello`という情報を付加できます。 -## `ocamlc -dparsetree`は友達 +### `ocamlc -dparsetree`は友達 ... @@ -90,18 +91,16 @@ $ ocamlc -dparsetree x.ml なんか出ました。これが上のソースコードを構文解析した結果のAST(構文木)です。 慣れていないとなんのことだかわからないかもしれませんが、 式`expression`が`x.ml`の一行目0文字目から1文字目にあって、 -その内容は整数定数`1`で、attributeとして`"hello"`という文字列が -付加されている、と言われればなんとなくわかるのではないでしょうか。 - -PPXはこのASTに付加されたアトリビュートを利用してプログラム変換を行うことができます。 +その内容は整数定数`1`で、attributeとして`"hello"`という文字列が付加されている、 +と言われればなんとなくわかるのではないでしょうか。 -あっ、使わなくてもいいですよ。単に無視すれば、 -`ocaml.warning`や`unboxed`などのOCamlコンパイラが使用するアトリビュートを除いて、 -コンパイルには影響はありません。 +PPXプリプロセッサはこのASTに付加されたattributeを利用してプログラム変換を行います。 +あっ、使わなくてもいいですよ。単に無視すれば、コンパイラはattributeを無視してコンパイルを行います。 +(ただし、後述の`ocaml.warning`や`unboxed`などのOCamlコンパイラが使用するattributeを除く。) -## 式、型、変数へのアトリビュート `[@...]` +### 式、型、変数へのattribute `[@...]` -アトリビュート`[@...]`は直前の式、型、変数に付加されます。 +Attribute `[@...]`はその直前の式、型、変数に付加されます。 ``` 1 + 2 [@hello] @@ -113,15 +112,15 @@ PPXはこのASTに付加されたアトリビュートを利用してプログ (1 + 2) [@hello] ``` -とします。 +と括弧を使います。 -## 宣言へのアトリビュート `[@@...]` +### 宣言へのattribute `[@@...]` ```ocaml let x = 1 ``` -のような宣言があった時に、式ではなくて、宣言全体にアトリビュートを付けたい、 +のような宣言があった時に、式ではなくて、宣言全体にattributeを付けたい、 ということがあります。その時に利用するのが`[@@...]`です: ```ocaml @@ -131,15 +130,18 @@ let x = 1 [@@hello] `@`の数で区別ってダサいと思われるかもしれませんが、 `(let x = 1) [@hello]`とかとするとカッコ付けが大変なので。 -## ファイル全体につけるアトリビュート `[@@@...]` +### ファイル全体につけるattribute `[@@@...]` -`[@@@...]`はファイル全体につけるアトリビュートです。 +`[@@@...]`はファイル全体につけるattributeです。 -## アトリビュートの実用例 +## OCamlコンパイラが使うAttributeの例 -このアトリビュートはPPX以外にも使い道があって、 -例えば警告スイッチを操作することができます。 -例えば次のプログラムはコンパイルが警告8を出します: +いくつかのattributeはOCamlコンパイラにとって特別な意味があり、 +これらのattributeを使うとコンパイラの挙動を変えることができます。 +(それ以外のattributeはコンパイラに無視されます。) + +例えば`ocaml.warning` attributeは警告スイッチを操作することができます。 +例えば次のプログラム: ```ocaml (* ocamlc -c x.ml *) @@ -147,7 +149,7 @@ let f = function | 1 -> true ``` -やってみます: +これはコンパイルが警告8を出します: ```shell $ ocamlc -c x.ml @@ -172,23 +174,186 @@ $ ocamlc -c x.ml (警告でなーい) ``` -このアトリビュート`[@@..]`はこの宣言だけに影響するので、 +このattribute`[@@..]`はこの宣言だけに影響するので、 もしプログラムの他の部分に警告8が出るところがあれば、 その警告はそのままです。 プログラム全体で警告8を無視したければ、`[@@ocaml.warning "-8"]`のかわりに `[@@@ocaml.warning "-8"]`を使います。 -PPX抜きでのアトリビュートの使用は、他にもC言語関数との間で -unbox化されたレコードを使った最適化を使うための -`[@@unboxed]`アトリビュート -(OCaml reference manualの20.3.2 Tuples and records -http://caml.inria.fr/pub/docs/manual-ocaml/intfc.html -を参照)などがあります。 +他にも、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 を参照) +などがあります。 + +## Extension point + +Attributeは存在するASTノードに情報を付加するものですが、 +ではextension pointってなんでしょうか。 + +extension pointは式、型、宣言に置き換える + +attributeは指定したASTのノードに情報を付加するものでしたが、 +extension pointはASTのノードそのものを置き換えるためにあります。 +例えば + +## 式、型を置き換えるextension point`[%..]` + +```ocaml +let x = [%hello] +``` + +は`let`束縛の右辺の式が`[%hello]`というextension pointになっています。 +他にも + +```ocaml +type t = [%hello] +``` + +のように型にも書けます。 + +## 宣言を置き換えるextension point`[%%..]` +attributeと同様、宣言を置き換えるextension point`[%%..]`もあります。 +(モジュール全体を置き換える、というのはあまり意味がないので`[%%%..]`はありません。) + +## 消える運命のextension point + +extension pointが書かれたOCamlプログラムはパースはできますが、コンパイルはできず、 +エラーになります: + +```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のバイナリ表現を +一時ファイルに保存して次のコマンドを実行します: -## エクステンションポイント +```shell +ppx.exe ファイル1 ファイル2 +``` + +ファイル1は読み込み元:オリジナルのASTバイナリ表現を保存した一時ファイル。 +ファイル2は書き込み先:PPXが変換したASTのバイナリ表現の保存先です。 + +この形式さえあっていれば、PPXコマンドは何を使っても構いません。 +OCamlのプログラムである必要さえ無い。一番簡単なPPXはshellスクリプトで書けます: + +```shell +#!/bin/sh +cat $1 > $2 +``` + +これを`ppx.sh`とかいう名前にして実行できるようにすれば、 + +```shell +$ ocamlc -ppx ppx.sh x.ml +``` + +でちゃんと動きます。まあ入力をそのまま返すだけですけれど、チェーンすることもできます: + +```shell +$ ocamlc -ppx ./ppx.sh -ppx ./ppx.sh x.ml +``` -アトリビュートは存在するASTノードに情報を付加するものですが、 -ではエクステンションポイントってなんでしょうか。 +## PPXを作る + +もうすこし複雑な事をしたければさすがにOCamlのコードを書かなければいけません。 +PPXをOCamlで書くにはいくつか方法がありますが、一番単純なものを紹介します。 +(もっと低レベルな方法も使えますが、それは紹介するAPI関数のソースから辿ってください。) + +OCamlのソースコードに`parsing/ast_mapper.mli`というモジュールがあって、 +そこに`Ast_mapper.run_main`という関数があります。これを使います。 + +```ocaml +(* 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`という名前で保存して、 + +```shell +$ 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`って何と言う前に動かしてみます: + +```shell +$ ocamlc -ppx ./ppx.exe x.ml +``` + +`x.ml`がちゃんとしたOCamlのコードであれば、特に何も起こらず、普通にコンパイルできるはずです。 +PPXコマンドに引数を与えて、ちゃんとPPXの関数に引数が渡っているのを確認します: + +```shell +$ ocamlc -ppx "./ppx.exe a -hello" x.ml +a +-hello +``` + +## AST mapperでASTを変更する + +上のOCamlで書く初めてのPPXの例では`Ast_mapper.default_mapper`という、 +`Ast_mapper.mapper`型のデータを使いました。この`mapper`はOCamlのASTをASTに写す「関数」です。 +「関数」とカギカッコなのは、実際は関数群になっているからです。 +`parsing/ast_mapper.mli`を参照してください: + +```ocaml +(** {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; + ... +``` -エクステンションポイントは式、型、宣言に置き換える \ No newline at end of file +山のように関数メンバのあるレコードです。