Skip to content

Latest commit

 

History

History
359 lines (253 loc) · 11.9 KB

ppx_2018.md

File metadata and controls

359 lines (253 loc) · 11.9 KB

2018年のPPX

PPXとは

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のソースコードに連続して適用できます。

Attribute と extension point

OCamlの文法には、PPXがプリプロセス時に利用できるヒントとして、

  • Attribute: [@...], [@@...], [@@@...]
  • Extension point: [%...]%, [%%...]`

が追加されました。なんじゃこりゃ。

Attribute

AttributeはASTのノードに情報を足します:

1 [@hello]

と書くと1という式にhelloという情報を付加できます。

ocamlc -dparsetreeは友達

...

...

...

よくわからない? よくわからないですね。 実際に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(構文木)です。 慣れていないとなんのことだかわからないかもしれませんが、 式expressionx.mlの一行目0文字目から1文字目にあって、 その内容は整数定数1で、attributeとして"hello"という文字列が付加されている、 と言われればなんとなくわかるのではないでしょうか。

PPXプリプロセッサはこのASTに付加されたattributeを利用してプログラム変換を行います。 あっ、使わなくてもいいですよ。単に無視すれば、コンパイラはattributeを無視してコンパイルを行います。 (ただし、後述のocaml.warningunboxedなどのOCamlコンパイラが使用するattributeを除く。)

式、型、変数へのattribute [@...]

Attribute [@...]はその直前の式、型、変数に付加されます。

1 + 2 [@hello]

において、[@hello]2に対して付加されます。1 + 2全体に付加するには、

(1 + 2) [@hello]

と括弧を使います。

宣言へのattribute [@@...]

let x = 1

のような宣言があった時に、式ではなくて、宣言全体にattributeを付けたい、 ということがあります。その時に利用するのが[@@...]です:

let x = 1 [@@hello]

@の数で区別ってダサいと思われるかもしれませんが、 (let x = 1) [@hello]とかとするとカッコ付けが大変なので。

ファイル全体につけるattribute [@@@...]

[@@@...]はファイル全体につけるattributeです。

OCamlコンパイラが使う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 を参照) などがあります。

Extension point

Attributeは存在するASTノードに情報を付加するものですが、 ではextension pointってなんでしょうか。

extension pointは式、型、宣言に置き換える

attributeは指定したASTのノードに情報を付加するものでしたが、 extension pointはASTのノードそのものを置き換えるためにあります。 例えば

式、型を置き換えるextension point[%..]

let x = [%hello]

let束縛の右辺の式が[%hello]というextension pointになっています。 他にも

type t = [%hello]

のように型にも書けます。

宣言を置き換えるextension point[%%..]

attributeと同様、宣言を置き換えるextension point[%%..]もあります。 (モジュール全体を置き換える、というのはあまり意味がないので[%%%..]はありません。)

消える運命の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

PPXを作る

もうすこし複雑な事をしたければさすがに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

AST mapperでASTを変更する

上の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;
  ...

山のように関数メンバのあるレコードです。