LoginSignup
2
0

More than 1 year has passed since last update.

型付きPPXに関するメモ - toward the typed ppx

Last updated at Posted at 2021-07-31

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 の設定について。 kindppx_rewriter に。あとは ppxlibcompiler-libs を使うようにし、[%expr ] を使うために ppxlib.metaquot も入れておく。

src/dune
(library
 (name ppx_ty_test)
 (public_name ppx_ty_test)
 (kind ppx_rewriter)
 (libraries
  ppxlib
  compiler-libs.common
  )
 (preprocess
  (pps ppxlib.metaquot)))

これを使う側は簡単

test/dune
(executable
 (name test)
 (preprocess (pps ppx_ty_test)))

ソースコード

Ppxlib を使った ppx_rewriter は全体としてこのようになる:

src/ppx_ty_test.ml
let () =
  Ppxlib.Driver.register_transformation 
    ~impl:transform
    "ppx_ty_test"

ここで transform は型付きのPPXの変換を行う処理だ。

src/ppx_ty_test.ml
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 の型を持つ。
src/ppx_ty_test.ml
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 の該当部分

ppx_locs.re#L199

staged_pps を使う?

依存関係に影響するためか staged_pps を使っている。

OCaml のソースツリーをコピーしている?

OCamlのソースツリーをコピーして、色々とフックを加えることで型付きの変換を実現している。その理由は一見してわかりかねたが、

  • ocaml-migrate-parsetree を利用してバージョン間の差異をなくしたかった?
  • 型エラーがあってもプリプロセッサが止まらないように工夫がある?

…といった理由があるのかもしれない。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0