PPX は OCaml のプリプロセッサの仕組みの一つで構文木に対して働く。モダンな OCaml であればduneにたとえば (preprocess (pps ppxlib.metaquot)))
とか書いておけば気軽に構文拡張を導入できる。しかしながら単なる構文木に対するプリプロセッサだけでは足りず、もう少し型に踏み込んだ変換が欲しくなることもあるかもしれない。
そのような型付きの構文木に対するPPXのテクニックとして 古瀬/camlspotter さんの typpx や ppx_implicits といった超テクニックがかつては存在した。しかしこの時点では dune などのツールチェインは整備されておらず、型付きの PPX の使用例も他にはみられなかった(私は知らない)。
Eduardo Rafael氏の ppx_let_locs はそのような型付きの構文木を操作する PPX の一つであり、Dune にも対応している。これ自体も面白いのだが、これをきっかけとして、自分でも型付きの PPX を書いてみた。
PPX の教科書
PPX を書くためのテキストとしては古瀬さんの解説が参考になる:
OCaml PPX http://dailambda.jp/slides/2021-04-09-ppx.html
ppx_ty_test: 型付きPPXにトライ
この型付きPPXは、int
型をもつ部分式 e
を e + 42
に変換する。たとえば
let f x = x + 1
は
let f x = ((x + 42) + (1 + 42)) + 42
のようになる。
Dune
これから作るプリプロセッサの Dune の設定について。 kind
を ppx_rewriter
に。あとは ppxlib
と compiler-libs
を使うようにし、[%expr ]
を使うために ppxlib.metaquot
も入れておく。
(library
(name ppx_ty_test)
(public_name ppx_ty_test)
(kind ppx_rewriter)
(libraries
ppxlib
compiler-libs.common
)
(preprocess
(pps ppxlib.metaquot)))
これを使う側は簡単
(executable
(name test)
(preprocess (pps ppx_ty_test)))
ソースコード
Ppxlib を使った ppx_rewriter は全体としてこのようになる:
let () =
Ppxlib.Driver.register_transformation
~impl:transform
"ppx_ty_test"
ここで transform
は型付きのPPXの変換を行う処理だ。
let transform str =
let env = Lazy.force env in
let (tstr, _, _, _) = Typemod.type_structure env str in
let super = Untypeast.default_mapper in
let mapper =
{super with expr = tyexp_to_exp env super}
in
mapper.structure mapper tstr
-
Typemod.type_structure
で構文木に型をつける (ここで型エラーになるとプリプロセッサのエラーになるのが厄介。回避法は以下の ppx_let_locs が参考になる?)。 - その木のトラバースを自力でやるのは大変なので、型をはずす mapper であるところの
Untypeast
をオーバーライドし、型をはずしつつ構文木の変換を行う。 - ここで
tyexp_to_exp
が、Typedtree.expression -> Parsetree.expression
の型を持つ。
let tyexp_to_exp env (super:Untypeast.mapper) =
fun self (texp : Typedtree.expression) ->
let e = super.expr self texp in
let loc = texp.exp_loc in
if Ctype.matches env texp.exp_type Predef.type_int then begin
let open Ppxlib in
[%expr [%e e] + 42]
end else begin
e
end
まず、オーバーライドした Untypeast
の super.expr
を呼び出すことで型を外した構文木 e
を得ておく。
つぎに、部分式の型を Ctype.matches
を使って int
型と比較して、 e + 42 に変換する。
所感
これはやってみると思ったより簡単だったが、たとえば型推論を変えるような変換はすごく大変になるように思う。
備考: ppx_let_locs と型付きPPX
この PPX は Lwt などのモナディックな処理において stack backtrace の見え方を改善するもの、らしい。
型付きPPXの部分は「関数 f
の呼び出し時に、関数 backtrace_f
が存在し型 (exn -> exn) -> 'a
を持つ場合に backtrace_f %reraise
に差し替える」というものだ。そのようにしてスタックトレースが少し見やすくなる。
ppx_let_locs の該当部分
staged_pps を使う?
依存関係に影響するためか staged_pps を使っている。
OCaml のソースツリーをコピーしている?
OCamlのソースツリーをコピーして、色々とフックを加えることで型付きの変換を実現している。その理由は一見してわかりかねたが、
- ocaml-migrate-parsetree を利用してバージョン間の差異をなくしたかった?
- 型エラーがあってもプリプロセッサが止まらないように工夫がある?
…といった理由があるのかもしれない。