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

PPX は OCaml のプリプロセッサの仕組みの一つで構文木に対して働く。モダンな OCaml であればduneにたとえば  `(preprocess (pps ppxlib.metaquot)))` とか書いておけば気軽に構文拡張を導入できる。しかしながら単なる構文木に対するプリプロセッサだけでは足りず、もう少し型に踏み込んだ変換が欲しくなることもあるかもしれない。

そのような型付きの構文木に対するPPXのテクニックとして 古瀬/camlspotter さんの typpx や ppx_implicits といった超テクニックがかつては存在した。しかしこの時点では dune などのツールチェインは整備されておらず、型付きの PPX の使用例も他にはみられなかった（私は知らない）。

Eduardo Rafael氏の [ppx_let_locs](https://github.com/EduardoRFS/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` に変換する。たとえば

```ocaml
let f x = x + 1
```

は

```
let f x = ((x + 42) + (1 + 42)) + 42
```

のようになる。



https://github.com/keigoi/ppx_ty_test

## Dune

これから作るプリプロセッサの Dune の設定について。 `kind` を `ppx_rewriter` に。あとは `ppxlib` と `compiler-libs` を使うようにし、`[%expr ]` を使うために `ppxlib.metaquot` も入れておく。

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

これを使う側は簡単

```lisp:test/dune
(executable
 (name test)
 (preprocess (pps ppx_ty_test)))
```

## ソースコード

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

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

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

```ocaml: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` の型を持つ。


```ocaml: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

https://github.com/EduardoRFS/ppx_let_locs

この PPX は Lwt などのモナディックな処理において stack backtrace の見え方を改善するもの、らしい。
型付きPPXの部分は「関数 `f` の呼び出し時に、関数 `backtrace_f` が存在し型 `(exn -> exn) -> 'a` を持つ場合に `backtrace_f %reraise` に差し替える」というものだ。そのようにしてスタックトレースが少し見やすくなる。

## ppx_let_locs の該当部分

[ppx_locs.re#L199](https://github.com/EduardoRFS/ppx_let_locs/blob/8c3f86f673f0f04f57e09e5d2f29687d545abe95/src/ppx_locs.re#L199) 

## staged_pps を使う?

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

https://dune.readthedocs.io/en/stable/concepts.html#preprocessing-spec

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

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

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

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