You are on page 1of 27

1

Camlp4 を理解する.................................................................................................2
はじめに..............................................................................................................2
プリプロセッサとは何か........................................................................................2
OCaml でいろいろなプリプロセッサを使う.............................................................3
Camlp4 を使う....................................................................................................4
Camlp4 で何もしない...........................................................................................5
構文拡張を使ってみる...........................................................................................7
OCaml トップレベルで Camlp4 を使う.................................................................8
構文木の姿を捉える..............................................................................................9
何でも Hello World にするパーサ.........................................................................10
クォーテーション................................................................................................12
代表的なクォーテーションと改訂構文...................................................................13
str_item.........................................................................................................14
expr...............................................................................................................14
patt...............................................................................................................14
アンチクォーテーション......................................................................................15
逆ポーランド記法を OCaml に変換する.................................................................16
Grammar モジュール.........................................................................................17
文法と字句解析器とエントリ................................................................................18
Grammar モジュールを使う(中置演算子記法パーサを作る)................................19
字句解析器を作る............................................................................................19
文法を作る......................................................................................................20
エントリを作る................................................................................................21
エントリを拡張する.........................................................................................21
エントリに優先順位をつける.............................................................................22
エントリを拡張する.........................................................................................23
レベルに名前をつける......................................................................................24
Pcaml モジュールの OCaml パーサ......................................................................25
OCaml の字句解析器........................................................................................25
OCaml の文法.................................................................................................26
Ocaml 文法のエントリ.....................................................................................26
Pcaml.expr エントリのレベル..........................................................................27
構文拡張を作る...................................................................................................27
2

Camlp4 を理解する

はじめに

この文書は OCaml の標準ディストリビューションに含まれる Camlp4 についてのチュ


ートリアルです。普通のチュートリアルとは異なり、実際に Camlp4 を使って何か有意義
なことをするのはずっと後になって出てきます。
このような構成をとったことには理由があります。 Camlp4 は非常に理解するのが難しい
ツールです。その原因の一つは Camlp4 には通常の OCaml の文法や使い方を習得した人に
とってさえ新奇な概念がいくつも絡み合って出てきていることです。
そのようなツールは「とりあえず使ってみる」やり方では理解に至るのが困難です。そこで、
このチュートリアルでは可能な限り一つずつ新たな物事を導入し、一歩一歩ボトムアップ
に Camlp4 を理解することができるように心がけました。

プリプロセッサとは何か

Camlp4 の基本的な用途はプリプロセッサです。プリプロセッサとはコンパイラがソース
コードを解釈する前段階でソースに変換を加えるプログラムです。例えば C 言語ではプロ
グラムソースファイルはコンパイラが解釈するまえにまず cpp と呼ばれるプリプロセッサ
によって変換されます。

ソースファイル プリプロセッサ 変換されたコード コンパイラ オブジェクトファイル

cpp の一般的な用途はマクロ定義と呼ばれる文字列の置き換え、ヘッダファイルの読み込
1
み、そして条件コンパイルです 。

/* Cのマクロの例: ソース中の PI の出現を 3.14 で置き換える */


#define PI 3.14

/* ヘッダファイルの読み込みの例: この位置に stdio.h の内容を読み込ませる */


#include <stdio.h>

/* Cの条件コンパイルの例: デバッグ版のコンパイル時のみ printf() をコンパイルする */


int fact(int n)
{
#ifdef DEBUG

1
C 以外の言語ではこうしたことをコンパイラの機能で行うものもあります
3

printf("fact(%d)\n", n);
#endif
if (n == 0) {
return 1;
} else {
return n * fact(n - 1);
}

cpp は # で始まる行(ディレクティブ)を解釈し、その指示に従ってソースコードを変換
していきます。コンパイラは変換結果だけを受け取るため、ディレクティブを読むことはあ
りません。

OCaml でいろいろなプリプロセッサを使う

ocamlc は通常ソースファイルを直接解釈しますが -pp オプションを使うことにより任意


のコマンドをプリプロセッサとして使うことができます。

ocamlc –pp プリプロセッサのコマンド ソースファイル

使用できるプリプロセッサは Camlp4 に限られません。以下の例は sed を使ってソース


中の文字列置換をしています。ソースファイルは識別子 PI を定義していませんが、 sed
s/PI/3.14/ により文字列 PI を 3.14 に置き換えることで、コンパイルが通るようになってい
ます。

$ cat tryme.ml
let _ = print_float PI
$ ocamlc tryme.ml
File "tryme.ml", line 1, characters 20-22:
Unbound constructor PI
$ ocamlc -pp 'sed s/PI/3.14/' tryme.ml
$ ./a.out
3.14$

以下のように cpp を使うこともできます。

$ cat tryme.ml
#define PI 3.14
let _ = print_float PI
$ ocamlc tryme.ml
File "tryme.ml", line 1, characters 0-1:
Syntax error
$ ocamlc -pp cpp tryme.ml
$ ./a.out
3.14$

OCaml 専用に作られた Camlp4 以外のプリプロセッサもあります。"The Whitespace


4

Thing" for OCaml (ocaml+twt) は OCaml で Python や Haskell のようなインデント


レベルによるグルーピングを行うプリプロセッサです。
以下のプログラムで最終行のパターンは 2 つある match のどちらに属するでしょうか。

F:\ocaml+twt>type tryme.ml
let f x y =
match x with
| Some s1 ->
match y with
| Some s2 -> s1^s2
| None -> ""
| None -> ""

F:\ocaml+twt>ocamlc tryme.ml
File "tryme.ml", line 2, characters 2-113:
Warning P: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
None
File "tryme.ml", line 7, characters 5-9:
Warning U: this match case is unused.

F:\ocaml+twt>ocamlc -pp ocaml+twt tryme.ml

F:\ocaml+twt>ocaml+twt tryme.ml
#1 "tryme.ml"
let f x y =
( ( match x with
| Some s1 ->
begin ( match y with
| Some s2 -> s1^s2
| None -> ""
) end | None -> ""
) )
F:\ocaml+twt>

このようなコードでは本来下の match に属するとみなされてしまうのですが、ocaml+twt


を通すことにより「同じインデントレベルの」上の match に対応させることができます。

ま と め :
ocamlc でプリプロセッサを使うには –pp オプションを使う。Camlp4 はその中で使え
る選択肢の一つ。

Camlp4 を使う

Camlp4 は前節で紹介した例と同様に -pp オプションの中で使われることを想定したコ


マンドです。単独で使うこともできますので、ここでは Camlp4 が何をしているかをよく
知るためにまずは単体で使ってみることにしましょう。
5

Camlp4 で何もしない

最初に camlp4 pa_o.cmo pr_o.cmo というコマンドラインを使ってみましょう。


これは入力されたソースファイルに何もせずに出力するというものです。

F:\>type tryme.ml
let
identity
x
=
x

F:\>camlp4 pa_o.cmo pr_o.cmo tryme.ml


let identity x = x

F:\>

上記の例ではわざとソースファイルに不必要な改行を入れています。しかし camlp4 が処
理した後の出力ではそれらがなくなっています。これは何故でしょうか。
実はここに Camlp4 のプリプロセシングの特徴が現れています。先に紹介した sed や cpp
や ocaml+twt のようなプリプロセッサは多かれ少なかれテキストレベルでの置換を行う
ものでした。これに対して Camlp4 は入力ソースファイルを一旦「構文木 (abstract
syntax tree)」と呼ばれる抽象的な表示に変換しているのです。
上記のコマンドは以下のような変換を行います。

ソースファイル パーサ 構文木 プリンタ 変換後のコード


pa_o.cmo pr_o.cmo

構文木には元のソースファイルの空白は残らないので、構文木から再構成されたコードは
元のソースコードと同じとは限りません。
camlp4 を使うには最低 2 つの .cmo ファイルを引数として与えなければいけません。
pa_*.cmo パーサ:入力ソースを構文木に変換する
pr_*.cmo プリンタ:構文木から何らかの別の表示への変換を行う

pa_o.cmo は OCaml コ ー ド を 構 文 木 に 変 換 す る パ ー サ で 、 pr_o.cmo は 構 文 木 を


OCaml コードに変換するプリンタでした。

ポイント: camlp4 はコードを構文木に変換して扱う

今度は ocamlc と camlp4 を組み合わせて使ってみましょう。


6

F:\>ocamlc -pp "camlp4 pa_o.cmo pr_o.cmo" tryme.ml

F:\>

ちゃんとコンパイルできました。さて、今度は以下のようにソースファイルにバグを紛れ込
ませて同じことをして見ましょう。

F:\>type tryme.ml
let
identity
x
=
y

F:\>ocamlc -pp "camlp4 pa_o.cmo pr_o.cmo" tryme.ml


File "E:\DOCUME~1\ether\LOCALS~1\Temp\camlppc2bba7", line 1, characters 17-
18:
Unbound value y

F:\>

変数 y が束縛されていないのでエラーになります。ここでエラーの表示に注目しましょう。
まず(a)ファイルが tryme.ml ではなく、(b)エラー発生箇所が本来の 5 行目ではなく 1 行
目となっています。
(a)はプリプロセシングの際に一時ファイルが作られ、それがコンパイラに渡されていると
いうことを意味しています。(b)は先ほど見たように変換後のファイルでは改行が取り除か
れていたのでコンパイラにとっては 1 行目でエラーが発生したというように見えるわけで
す。しかし複雑なソースファイルで適切なエラー箇所の表示が出なければデバッグ作業は
絶望的です。
この問題はプリンタとして pr_dump.cmo を使うことにより解決できます。

F:\>ocamlc -pp "camlp4 pa_o.cmo pr_dump.cmo" tryme.ml


File "tryme.ml", line 5, characters 0-1:
Unbound value y

F:\>

今度は適切に 5 行目にエラーがあることが分かりました。
実は ocamlc はプリプロセッサからの入力として通常の OCaml ソースコードだけではな
く、構文木を直接シリアライズした特殊なバイナリ形式を受け取ることができます。
pr_dump.cmo は構文木をその形式でエクスポートするためのプリンタです。 OCaml の構
文木には元のソースコードの位置情報は保存されているため、コンパイルでエラーが出た
場合にはその情報を使ってエラー表示を出すことができるのです。
7

構文木 プリンティング 変換後のコード コンパイラ


pr_o.cmo ocamlc

プリンティング 構文木のダンプ コンパイラ


pr_dump.cmo ocamlc

以上のことから ocamlc の -pp オプションの中で使う場合は常に pr_dump.cmo を、一


方で Camlp4 の変換の様子を確認したい場合は pr_o.cmo を使うとよいでしょう。

メ モ : ソースコードを構文木に変換するという工程は Camlp4 を通さなくてもコンパ


イル時には必ず行われるものですが pr_dump.cmo を使って直接構文木を渡す場合は
ocamlc はそれを使い、コンパイラ内での工程をスキップします。また pa_o.cmo はソ
ースから構文木への変換を ocamlc とは独立に自前で実装しています。

構文拡張を使ってみる

「Camlp4 を使う」というのは多くの場合「入力ソースを構文木に変換するルールに対して
変更を加える」ということを意味しています。Camlp4 が「文法を変えるプリプロセッサ」と
呼ばれることがあるのはこのためです。変更を加えるには pa_o.cmo に定義されたデフォ
ルトのルールを変更するための pa_*.cmo を作成し、それを camlp4 のオプションに与
えます。

ocamlc –pp “camlp4 pa_o.cmo pa_*.cmo pr_dump.cmo” …

自分で作るのはまだ後回しにして、まずは既に用意されている構文拡張を使ってみること
にしましょう。
標準添付の構文拡張の一つである pa_macro.cmo は cpp のような機能を提供します。以
下では条件コンパイルを使ってみました。

F:\>type tryme.ml
let rec fact n =
IFDEF DEBUG THEN
Printf.printf "fact(%d)\n" n
ELSE
()
END;
8

if n = 0 then 1 else n * fact (n - 1)

let _ = print_int (fact 5)

F:\>ocamlc -pp "camlp4 pa_o.cmo pa_macro.cmo pr_dump.cmo" tryme.ml

F:\>camlprog
120
F:\>ocamlc -pp "camlp4 pa_o.cmo pa_macro.cmo pr_dump.cmo -DDEBUG" tryme.ml

F:\>camlprog
fact(5)
fact(4)
fact(3)
fact(2)
fact(1)
fact(0)
120
F:\>

他に Camlp4 のパッケージに含まれる構文拡張で良く使われるものに pa_op.cmo があ


ります。これは Stream モジュールで定義されるストリーム型を操作するための使いやす
い構文をそろえたものです。
pa_op.cmo の 詳 し い 説 明 は こ こ で は 省 略 し ま す が 、 pa_o.cmo pa_op.cmo
pr_dump.cmo の組み合わせは良く使われるのでショートカットコマンドが用意されてい
ます。camlp4o コマンドは以下のコマンドラインの代替になります。

camlp4 pa_o.cmo pa_op.cmo pr_dump.cmo

Camlp4 添付の他にもサードパーティの構文拡張を作成して公開しているサイトが存在し
ます。ここでは http://martin.jambon.free.fr/p4ck.html を紹介しておきます。

OCaml トップレベルで Camlp4 を使う

ここまでは ocamlc で Camlp4 を使ってきました。実はトップレベルでも Camlp4 を使


うことができます。対話環境は試行錯誤に何かと便利なのでこの方法も覚えておきましょ
う。
トップレベルで以下のようにモジュールをロードすることで Camlp4 が利用可能になり
ます。

# #load "camlp4o.cma";;
Camlp4 Parsing version 3.09.0
#

ocaml トップレベルで camlp4o.cma をロードすることは ocamlc で camlp4 pa_o.cmo


9

pa_op.cmo を使うことに相当します。

構文木の姿を捉える

先に述べたように pa_o.cmo は OCaml ソースコードを構文木に変換するためのルールを


定義しています。ここではそれを実際に使ってみることにしましょう。
pa_o.cmo はロードされると Pcaml モジュールの中に OCaml パーサを定義します。
Pcaml.parse_implem というのがそれです2。トップレベルで以下のように打ち込んでみて
ください。

# #load "camlp4o.cma";;
Camlp4 Parsing version 3.09.0

# !Pcaml.parse_implem (Stream.of_string "let _ = 1");;


- : (MLast.str_item * MLast.loc) list * bool =
([(MLast.StExp
(({Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 0},
{Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 9}),
MLast.ExInt
(({Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 8},
{Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 9}),
"1")),
({Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 0},
{Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 9}))],
false)
#

これは "let _ = 1" という OCaml コードを解析した結果です。パーサは char Stream.t 型


でソースを受け取るので string 型の文字列から Stream.of_string を使って char Stream.t
型に変換しています。
実行結果は(MLast.str_item * MLast.loc) list 型と bool 型のタプルです。bool の方は無視す
ることにして、前半の長々しいものは一体何でしょうか。実はこれが "let _ = 1" というコ
ードに対応する構文木の(OCaml コードで表現された)姿なのです。
構文木はその名の通りツリー構造をとりますが、OCaml ではツリー構造は通常ネストされ
たヴァリアント型を使って表現されます。OCaml 構文木を表現するためのヴァリアント型
は MLast モジュールで定義されています(モジュール名の ast は abstract syntax tree
の略です)。

2
これは大まかに言うと.ml ファイルを読むためのパーサです。.mli ファイルに対しては
Pcaml.parse_interf を使います。
10

「 {Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;


Lexing.pos_cnum =
0}」のようなレコード型はソースコード上の位置を表す MLast.loc 型です。これを 2 つ組み
合わせたタプルにより、構文木の該当するノードがソースコード上のどの範囲に対応する
かを表現します。これによりコンパイラは元々のソースコードの位置を知ることができる
のです。
なお Pcaml.parse_implem の前に「!」をつけていたのは Pcaml.parse_implem が OCaml
コードをパースする関数へのリファレンスとして定義されているからです。

まとめ: pa_o.cmo は Pcaml.parse_implem を定義し、Camlp4 はそれを呼んで構文木


を作る。構文木の姿はとても複雑。

何でも Hello World にするパーサ

ではいよいよ自分でパーサを書いてみましょう。まずは単純で乱暴で役に立たない例から
はじめます。Pcaml.parse_implem がパース用の関数でしかもリファレンス型だというこ
とが分かりましたから、これを書き換えれば動きを変えることができるはずです。以下のよ
うなファイルを pa_hello.ml として作って見ましょう。

(* pa_hello.ml *)
let _ =
Pcaml.parse_implem := function s ->
ignore (Stream.count s);
([(MLast.StExp
(({Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 0},
{Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 26}),
MLast.ExApp
(({Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 0},
{Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 26}),
MLast.ExLid
(({Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 0},
{Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 12}),
"print_string"),
MLast.ExStr
(({Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 13},
{Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 26}),
"hello world"))),
({Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 0},
11

{Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;


Lexing.pos_cnum = 26}))],
false)

このプログラムは Pcaml.parse_implem の内容を破壊的に書き換え、


「入力文字ストリー ム
が何であろうと同じ構文木を返す関数」に替えています。
こ の 構 文 木 は ト ッ プ レ ベ ル で 「 !Pcaml.parse_implem (Stream.of_string
"print_string \"hello world\"");;」と打った結果をそのまま貼り付けたものです。

コンパイルは以下のようにします。Pcaml モジュールが見えるように「-I +camlp4」をつけ


ています。

ocamlc -c -I +camlp4 pa_hello.ml

そうすると pa_hello.cmo が出来上がります。 Camlp4 にこのパーサをロードすると、


Pcaml.parse_implem が書き換わり、何が入力に来ようと Hello World プログラムを生成
するようになります。

ロードの順序

pa_o.cmo pa_hello.cmo

定義 書き換え

Pcaml.parse_implem

F:\>type test.ml
print_int 7
F:\>ocamlc -pp "camlp4o ./pa_hello.cmo" test.ml

F:\>camlprog
hello world

なおプリンタとして pr_o.cmo を使うと予期しないコードが出力(元のソースの一部がそ


のまま出力される)されてコンパイルが通らなくなりますが、これは pr_o.cmo のバグの
ようです。
12

クォーテーション

先ほどの例では構文木を直接扱ってきました。しかし単純なコードでさえ非常に長いヴァ
リアント型定義になってしまいます。面倒です。そして Camlp4 の開発者もこれを面倒だと
思いました。そしてこれを楽にするためにクォーテーションという仕組みを用意しました。
クォーテーションを使うと、例えば MLast.StExp コンストラクタは <:str_item< OCaml
コード >>と書き換えることができます。「OCaml コード」には OCaml のソースコードを
そのまま書くことができます。

(* pa_hello2.ml *)

let _ =
Pcaml.parse_implem := function s ->
ignore (Stream.count s);
[<:str_item< print_string "hello world" >>,
({Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 0},
{Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 26})
], false;

随分短くできましたが、そもそも <:str_item<>>なんて OCaml のコードのように見えま


せん。実はクォーテーションは Camlp4 のプリプロセシング機能として提供されているの
で、この pa_hello2.ml 自体も Camlp4 に通してコンパイルするのです。クォーテーション
を提供するモジュールは q_MLast.cmo です。やってみましょう。

F:\>ocamlc -c -I +camlp4 -pp "camlp4o q_Mlast.cmo" pa_hello2.ml


File "pa_hello2.ml", line 6, characters 0-1:
Unbound value _loc

F:\>

コンパイルエラーが出てしまいました。pa_hello2.ml はどのように変換されているのでし
ょうか。pr_o.cmo を使って確認してみましょう。

F:\>camlp4o q_MLast.cmo pr_o.cmo pa_hello2.ml


(* pa_hello2.ml *)

let _ =
Pcaml.parse_implem :=
fun s ->
ignore (Stream.count s);
[MLast.StExp
(_loc,
MLast.ExApp
(_loc, MLast.ExLid (_loc, "print_string"),
MLast.ExStr (_loc, "hello world"))),
({Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;
Lexing.pos_cnum = 0},
13

{Lexing.pos_fname = ""; Lexing.pos_lnum = 1; Lexing.pos_bol = 0;


Lexing.pos_cnum = 26})],
false

F:\>

確かに<:str_item<>>の中に書いたコードが構文木に変換されていました。しかしこの中
で_loc という変数を使っていて、それが位置情報に束縛されている必要があるようです。と
いうわけで_loc 変数を定義してあげましょう。「({Lexing.pos_fname…}と位置情報のレコ
ード型リテラルを直接書いても良いのですが、Token モジュールに Token.dummy_loc とい
うダミーの位置情報が定義されているのでそれを使ってしまいましょう。ついでに
MLast.StExp と一緒にタプルに入れていた位置情報も_loc にしてしまいます。

(* ocamlc -c -I +camlp4 -pp "camlp4o q_MLast.cmo" pa_hello2.ml *)

let _ =
Pcaml.parse_implem := function s ->
let _loc = Token.dummy_loc in
ignore (Stream.count s);
[<:str_item< print_string "hello world" >>, _loc], false;

これでコンパイルが通るようになりました。

F:\>ocamlc -c -I +camlp4 -pp "camlp4o q_Mlast.cmo" pa_hello2.ml

F:\>ocamlc -pp "camlp4o ./pa_hello2.cmo" test.ml

F:\>camlprog
hello world
F:\>

代表的なクォーテーションと改訂構文

前節ではクォーテーションの書き方として <:str_item<>> を紹介しました。クォーテー


ションとしてはこれ以外にも OCaml の文法に対応した多くの種類が用意されていますが、
大抵の基本的な用途のために覚えておけばいいのは以下で取り上げる 3 つです。
この章の例はトップレベルで以下のように q_MLast.cmo を読み込み、_loc 変数を定義して
から実際に打ち込んで試してみましょう。

# #load "camlp4o.cma";;
Camlp4 Parsing version 3.09.0

# #load "q_MLast.cmo";;
# let _loc = Token.dummy_loc;;
14

str_item

<:str_item<>>は内部に OCaml コードの最上位に来る部分を記述することができます。

<:str_item<open Printf>>
<:str_item<exception My_exception>>
<:str_item<type url = string>>
<:str_item<print_int 256>>

ところで以下の例はエラーになります。

# <:str_item<let x = 256>>;;
^
While expanding quotation "str_item":
Parse error: 'in' expected (in [expression])

「let x = 256」は OCaml の最上位に来られるコードのはずなのに何故でしょうか。実は


Camlp4 のクォーテーションの中に書くことができる OCaml コードは通常の OCaml コ
ードとは微妙に異なる「改訂構文」で書かなければならないことになっていて、改訂構文で
は以下のように書く必要があるのです。

<:str_item<value x = 256>>

改訂構文はクォーテーションを書く上で悩ましい制約ですが q_MLast.cmo の代わりに


qo_MLast.cmo3 を使うことで通常の OCaml 構文を使うこともできます4。

expr

<:expr<>>は内部に式を書くことができます。OCaml では大抵のものが式です。

<:expr<1+1>>
<:expr<let x = 12 in x * x>>
<:expr<if x = 1 then 2 else 3>>
<:expr<print_int 256>>

局所変数の定義には改訂構文でも let を使います。

patt

<:patt<>>は内部にパターンを書くことができます。パターンとは「let x = 1」などと書く

3
http://wwwtcs.inf.tu-dresden.de/~tews/ocamlp4/
4
次期バージョンの Camlp4 3.10 でも可能となります
15

とくの「=」の左辺や「match x with Some c -> true | None -> false」などと書くときの「-


>」の左辺を言います。

<:patt<_>>
<:patt<x>>
<:patt<[x::xs]>>
<:patt<(first,second)>>
<:patt<('A'..'Z' as c)>>

改訂構文では cons 演算子を使う場合でもリストを囲む大括弧が必要なことに注意してく


ださい。

メモ: ここで取り上げた例はごく一部です。より完全なクォーテーションの情報として
は 以 下 の ペ ー ジ を 参 照 し て く だ さ い 。
http://caml.inria.fr/pub/docs/manual-camlp4/manual010.html
ま た 、 改 訂 構 文 に つ い て は 以 下 に ま と め ら れ て い ま す 。
http://caml.inria.fr/pub/docs/manual-camlp4/manual007.html

アンチクォーテーション

クォーテーションの内部にはスクリプト言語の string interpolation のような要領で変数


を埋め込むことができます。

# let e = <:expr< 15 >>;;



# let s = <:str_item< value x = $e$ >>;;

クォーテーションの中に $ で囲まれた部分があると、その部分は式として評価され、値がそ
の位置に埋め込まれます。これによりクォーテーションのテンプレート化を行うことがで
き、プログラムによる柔軟な構文木構築が可能になります。このような仕組みをアンチクォ
ーテーションと呼びます。
以下のような記法で文字列式をリテラルや識別子として埋め込むこともできます。

<:expr<$int:"5"$>> 整数の5
<:expr<$flo:"3.14"$>> 実数の3.14
<:expr<$str:"hello"$>> 文字列のhello
<:expr<$lid:"x"$>> 小文字で始まる識別子のx
<:expr<$uid:"None"$>> 大文字で始まる識別子のNone
16

逆ポーランド記法を OCaml に変換する

クォーテーションとアンチクォーテーションを学んだまとめとして「逆ポーランド記法を
OCaml 構文木に変換するプリプロセッサ」を作ってみましょう。HelloWorld プリプロセッ
サと同様に Pcaml.parse_implem を書き換えます。

(* ocamlc -c -I +camlp4 -pp "camlp4o q_MLast.cmo" pa_rpn.ml *)

let _ =
Pcaml.parse_implem := function strm ->
let _loc = Token.dummy_loc in
let stack = Stack.create () in
let rec process stack strm =
match (Stream.peek strm) with
| Some ('0'..'9')
-> let c = String.make 1 (Stream.next strm) in
Stack.push <:expr<$int:c$>> stack;
process stack strm
| Some ('+')
-> Stream.junk strm;
let x = Stack.pop stack and y = Stack.pop stack in
Stack.push <:expr< $y$ + $x$ >> stack;
process stack strm
| Some ('-')
-> Stream.junk strm;
let x = Stack.pop stack and y = Stack.pop stack in
Stack.push <:expr< $y$ - $x$ >> stack;
process stack strm
| Some ('*')
-> Stream.junk strm;
let x = Stack.pop stack and y = Stack.pop stack in
Stack.push <:expr< $y$ * $x$ >> stack;
process stack strm
| Some ('/')
-> Stream.junk strm;
let x = Stack.pop stack and y = Stack.pop stack in
Stack.push <:expr< $y$ / $x$ >> stack;
process stack strm
| Some _
-> raise (Failure "unknown char")
| None
-> let e = Stack.pop stack in
<:str_item< print_int $e$ >>
in
[(process stack strm), _loc], false;

このパーサは char Stream.t 型のストリームを 1 字ずつ読み取り、数字であればそれを構文


木にしたものをスタックに積み、演算子であればスタックから 2 つとって式の構文木を構
成してスタックに積む、ということを繰り返して入力に相当する OCaml 構文木を作り上げ
ます。このプリプロセッサ自身は四則演算をしているわけではなく構文木を作っているだ
けだということに注意してください。
コンパイルは以下のようにします。
17

ocamlc -c -I +camlp4 -pp "camlp4o q_MLast.cmo" pa_rpn.ml

ソースとする入力は拡張子は.ml としますが、内容は逆ポーランド記法にします。

12-3*

このソースを先ほど作ったプリプロセッサに通すと (1-2)*3 を計算する実行ファイルがで


きあがります。

F:\>ocamlc -pp "camlp4o ./pa_rpn.cmo" rpn.ml

F:\>camlprog
-3
F:\>

Grammar モジュール

ここまでは pa_o.cmo が定義してくれた Pcaml.parse_implem を丸ごと別の処理に書き換


えて自前のパーサを実装していました。しかし「OCaml の文法をベースに少しだけ変更を
加える」というのが多くの人が Camlp4 でやりたいことのはずです。そこで pa_o.cmo が定
義する「OCaml パーサ」の仕組みについて入り込んでいくことにしましょう。
既 出 の コ ー ド 「 !Pcaml.parse_implem (Stream.of_string "let _ =
1");; 」(parse_implem を書き換えない状態で実行する)は、実は以下のように書いても

同じ結果になります。

Grammar.Entry.parse Pcaml.implem (Stream.of_string "let _ = 1");;

これは実のところ pa_o.ml のソースコード中で以下のように定義されているからです(改


訂構文)。

Pcaml.parse_implem.val := Grammar.Entry.parse implem;

Grammar モジュールは pa_o.cmo の OCaml パーサが使用している汎用的なパーサモジ


ュールです。つまり Grammar モジュール自体は OCaml をパースするだけのものではなく、
何に対しても使うことができ、pa_o.cmo はあくまでもそれを使っているだけです 。
5

5
このように Camlp4 は所々で徹底的な汎用化・一般化がなされた作りになっています。一
方でそのような極度の一般化が Camlp4 の理解をより一層難しいものにしているという側
面も否定できません。
18

Grammar モジュールは汎用的なパーサ機構を提供するのに対して、「OCaml 特有」の部分


はすべて Pcaml モジュールの中に定義されています。

まとめ : pa_o.cmo は Grammar モジュールの汎用的な仕組みを使って OCaml のパー


サを構築している。

文法と字句解析器とエントリ

「OCaml パーサ」について知る前に Grammar モジュールについて知る必要があるようです。


Grammar モジュールが提供する仕組みを擬似的な UML クラス図で表現すると以下のよう
になります。

Token.glexer<Token.t> ‘_a
Grammar.Entry.e
+tok_func: lexer_func<Token.t> *
-egram: Grammar.g
1 glexer -ename: string

* 1
Grammar.g egram

-glexer: Token.glexer<Token.t>
-gtokens

文法は Grammar.g 型の値であり、1 個の字句解析器(Token.t Token.glexer 型の値)と関


連付けられます。この字句解析器は大まかに言うと char Stream.t 型(キャラクタのストリ
6
ーム)の入力を Token.t Stream.t 型(Token.t 型 のストリーム)に変換する役目を持って
います。Grammar.Entry.parse の入力の char Stream.t はまずはこの字句解析器を通して
Token.t Stream.t に変換されます(より具体的には Token.t Token.glexer 型のメンバであ
る tok_func という関数がその変換を行います)。
そして Token.t Stream.t は「エントリ」と呼ばれる文法ルールによって何らかの型の値に変
換されます。この文法ルールが定義されるのが’a Grammar.Entry.e 型の値です。
「何らか の
型」とは仕組みとしては何でも良く、OCaml パーサであれば先に見た構文木の型となりま
す。文法(Grammar.g 型の値)は任意の数のエントリ(’a Grammar.Entry.e 型の値)を持
つことができます。

6
Token.t 型は string * string 型として定義されています。
19

Token.t 型の
char型のストリーム 字句解析器 ‘ a 型のエントリ ‘ a型の値
ストリーム

まとめ: 文字列ストリームが字句解析器を通してトークンのストリームになり、トーク
ンのストリームをエントリが解釈して何らかの別の表示(例えば構文木)へ変換する。

Grammar モジュールを使う(中置演算子記法パーサを作る)

抽象的な話が続いたのでここで Grammar モジュールを実際に使ってみましょう。おなじみ


の中置記法を使った式の評価を作ってみたいと思います。この場合、エントリは Token.t ス
トリームを読んで int 型に変換するものにはるはずです。この章はトップレベルを使用する
ので予め camlp4o.cma と q_MLast.cmo をロードしておいてください。

#load “camlp4o.cma”;;

字句解析器を作る

まずは字句解析器を作りましょう。ここは逆ポーランド記法の時と同様に 1 桁の数字と演
算子を読んで 1 字ずつトークンにするだけの簡単なものを作るにとどめたいと思います。
Camlp4 用の字句解析器を作るとは Token.t Token.glexer 型の値を作るということです。
その Token.t Token.glexer 型はいくつかのメンバを持つレコード型ですが、主たる仕事を
するのはその中の tok_func メンバとして定義された関数のみで、他の実装はサボることが
できますので今回はそのようにしたいと思います。以下のテンプレートを使い、関数 f だけ
を自分で実装します。

{ Token.tok_func = f;
Token.tok_using = (fun _ -> ());
Token.tok_removing = (fun _ -> ());
Token.tok_match = Token.default_match;
Token.tok_text = Token.lexer_text;
Token.tok_comm = None }

その f 関数は以下のように Token.lexer_func_of_parser 関数に自作関数を与えることで


作ることができます。
20

# let f =
let p strm =
match (Stream.peek strm) with
| Some ('0'..'9')
-> ("DIGIT", String.make 1 (Stream.next strm)), Token.dummy_loc
| Some ('+')
-> Stream.junk strm; ("PLUS", ""), Token.dummy_loc
| Some ('-')
-> Stream.junk strm; ("MINUS", ""), Token.dummy_loc
| Some ('*')
-> Stream.junk strm; ("MUL", ""), Token.dummy_loc
| Some ('/')
-> Stream.junk strm; ("DIV", ""), Token.dummy_loc
| Some _
-> raise (Stream.Error "unknown char")
| None
-> raise Stream.Failure
in
Token.lexer_func_of_parser p;;
val f : (string * string) Token.lexer_func = <fun>
#

Token.lexer_func_of_parser に与える関数は char Stream.t を読み、(string * sting) と位


置情報のタプル型を返すようにします。位置情報は例によってダミーを使います。あとは先
ほどのテンプレートを使ってレコードを作ります。

# let mylex = { Token.tok_func = f;


Token.tok_using = (fun _ -> ());
Token.tok_removing = (fun _ -> ());
Token.tok_match = Token.default_match;
Token.tok_text = Token.lexer_text;
Token.tok_comm = None }
;;
val mylex : (string * string) Token.glexer =
{Token.tok_func = <fun>; Token.tok_using = <fun>;
Token.tok_removing = <fun>; Token.tok_match = <fun>;
Token.tok_text = <fun>; Token.tok_comm = None}
#

string * string は Token.t と互換性がありますのでこれで Token.t Token.glexer 型の字句


解析器ができたことになります。

メモ: Token.lexer_func_of_parser を使う方法の代わりに、ocamllex を使って作った字


句解析器を Token. lexer_func_of_ocamllex に渡しても同様に作ることができます。本
格的なことがしたい場合はそちらを使うと良いでしょう。

文法を作る

字句解析器と文法は文法を最初に作る時点で結び付けられます。文法は以下のように
21

Grammar.gcreate 関数を使って作成します。

# let mygram = Grammar.gcreate mylex;;


val mygram : Grammar.g = <abstr>
#

ここはこれだけです。

エントリを作る

エントリは Grammar.Entry.create 関数を使って作成します。作成時の第 1 引数で対応する


文法と関連付けられます。第 2 引数はエントリの名称で、任意の文字列です。この名称はエ
ラーメッセージ表示の際などに使われます。

# let myexpr = Grammar.Entry.create mygram "myexpr";;


val myexpr : '_a Grammar.Entry.e = <abstr>

この時点ではエントリは空の状態で作られただけです。今回作るエントリは Token.t ストリ


ームを int 型に変換するものなので本来は int Grammar.Entry.e 型になるはずですが、まだ
ルールを定義していないので型も確定していません。

エントリを拡張する

ここからが本番です。今作ったエントリに四則演算のルールを定義していきます。エントリ
の拡張には EXNTEND…END 文を使います。これも OCaml の標準の構文ではなく、これ自
体が Camlp4 による拡張構文です。生の OCaml コードでもエントリの拡張はできるのです
が、あまりにもコードが煩雑になるため拡張構文が用意されています。EXNTEND…END 文
を使うには pa_extend.cmo をロードします。

# #load "pa_extend.cmo";;

EXNTEND…END 文は以下のように使います。

# EXTEND myexpr: [[
x = myexpr; PLUS ; y = myexpr -> x + y
|x = myexpr; MINUS; y = myexpr -> x – y
|x = myexpr; MUL; y = myexpr -> x * y
|x = myexpr; DIV; y = myexpr -> x / y
|x = DIGIT -> int_of_string x
]];
END;;
22

- : unit = ()
#

EXTEND の後に来るのは拡張の対象となるエントリの変数名です(エントリ名ではない)。
「x = myexpr; PLUS ; y = myexpr -> x + y」のような部分を「規則」と呼びます。「->」の左
側に規則が適用されるパターンを書き、右側にそのパターンが現れたときのアクションを
記述します。パターンは「DIGIT; PLUS; DIGIT」のようにシンボル(Token.t の最初の要素)
をセミコロン区切りで並べますが、「myexpr; PLUS; myexpr」のように他のエントリや自身
のエントリが来てもかまいません。また、「x = DIGIT」のようにすることで出現した値(シ
ンボルの場合 Token.t の第 2 の要素、エントリの場合はアクションの結果)に変数を束縛し、
アクションの中で使うことができます。規則はパイプで区切って複数並べることができま
す。規則のアクションの値は同一エントリ内のすべての規則で同じ型でなければいけませ
ん 。
このエントリを使って式をパースしてみましょう。

# Grammar.Entry.parse myexpr (Stream.of_string "1+1");;


- : int = 2
# Grammar.Entry.parse myexpr (Stream.of_string "1+2*3");;
- : int = 9
#

エントリに優先順位をつける

パイプで並列された規則はデフォルトでは優先順位を持たず、左結合として解釈されます。
先ほどのエントリの実行例でも単に左から順に計算したものとなっており、通常の数学の
規則とは異なっています。
規則に優先順位をつけるためには以下のように記述します。

# let myexpr = Grammar.Entry.create mygram "myexpr";;


val myexpr : '_a Grammar.Entry.e = <abstr>
# EXTEND myexpr: [
[x = myexpr; PLUS ; y = myexpr -> x + y
|x = myexpr; MINUS; y = myexpr -> x - y]
|[x = myexpr; MUL; y = myexpr -> x * y
|x = myexpr; DIV; y = myexpr -> x / y]
|[x = DIGIT -> int_of_string x]
];
END;;
- : unit = ()
#

先ほどのエントリでは大括弧を単に 2 つ重ねて書いていましたが、今度は優先順位のグル
ープごとに規則を 1 組の大括弧でまとめ、それをパイプで並列して外側の大括弧で括って
23

います。優先順位は下に行くほど強くなります。
これで掛け算が先に計算されるようになりました。

# Grammar.Entry.parse myexpr (Stream.of_string "1+2*3");;


- : int = 7
#

優先順位をつけられるような規則のまとまりを「レベル」と呼びます。エントリは複数のレ
ベルから成っており、レベルは複数の規則から成っています。

エントリ

レベル
規則
レベル
規則
レベル
規則
規則
規則
規則
規則
規則
規則

優先順位

エントリを拡張する

エントリは EXNTEND…END 文を使って規則を追加していくことができます。次のような規


則を新たに追加してどうなるか見てみましょう。

EXTEND myexpr: [[x = myexpr; MUL; MUL; y = myexpr -> int_of_float


((float_of_int x) ** (float_of_int x))]]; END;;

もっともどうなるかを観察するにはエントリの中身を見ることができなくてはいけません
そのためのコマンドが Grammar.Entry.print です。まずは現時点での myexpr の内容を見
ます。

# Grammar.Entry.print myexpr;;
[ LEFTA
[ SELF; PLUS; SELF
| SELF; MINUS; SELF ]
| LEFTA
[ SELF; MUL; SELF
| SELF; DIV; SELF ]
24

| LEFTA
[ DIGIT ] ]
- : unit = ()
#

規則内で再帰的に自分自身のエントリを参照する場合は SELF と表示されます。レベルの前


の LEFTA はそのレベルが左結合であることを示しています。 Grammar.Entry.print はアク
ションまでは表示してくれません。
次に規則を追加して再度内容を Grammar.Entry.print してみます。

# EXTEND myexpr: [[x = myexpr; MUL; MUL; y = myexpr -> int_of_float


((float_of_I
nt x) ** (float_of_int x))]]; END;;
- : unit = ()
# Grammar.Entry.print myexpr;;
[ LEFTA
[ SELF; MUL; MUL; SELF
| SELF; PLUS; SELF
| SELF; MINUS; SELF ]
| LEFTA
[ SELF; MUL; SELF
| SELF; DIV; SELF ]
| LEFTA
[ DIGIT ] ]
- : unit = ()
#

新しい規則は一番上の(最も優先順位が低い)レベルに追加されたことが分かります。し
かし任意の位置に規則を追加したい場合はどうすればいいのでしょうか。

レベルに名前をつける

新しい規則を指定した位置に追加するには、予めレベルに名前をつけておき、追加時にその
名前を指定します。この名前をラベルと呼びます。ラベルをつけるにはレベルの大括弧の手
前に文字列リテラルを置きます。エントリを作り直しましょう。

# let myexpr = Grammar.Entry.create mygram "myexpr";;


val myexpr : '_a Grammar.Entry.e = <abstr>
# EXTEND myexpr: [
"plusminus"
[x = myexpr; PLUS ; y = myexpr -> x + y
|x = myexpr; MINUS; y = myexpr -> x - y]
|"muldiv"
[x = myexpr; MUL; y = myexpr -> x * y
|x = myexpr; DIV; y = myexpr -> x / y]
|"atom"
[x = DIGIT -> int_of_string x]
];
END;;
- : unit = ()
25

このエントリを Grammar.Entry.print すると以下のように見えます。

# Grammar.Entry.print myexpr;;
[ "plusminus" LEFTA
[ SELF; PLUS; SELF
| SELF; MINUS; SELF ]
| "muldiv" LEFTA
[ SELF; MUL; SELF
| SELF; DIV; SELF ]
| "atom" LEFTA
[ DIGIT ] ]
- : unit = ()
#

ラベルを指定して規則を追加するには EXTEND 文で以下のように LEVEL を指定します。

# EXTEND myexpr: LEVEL "muldiv" [[x = myexpr; MUL; MUL; y = myexpr ->
int_of_flo
at ((float_of_int x) ** (float_of_int x))]]; END;;
- : unit = ()

この例では新しい規則を ”muldiv”レベルに追加するように指定しています。変更結果を見
てみましょう。

# Grammar.Entry.print myexpr;;
[ "plusminus" LEFTA
[ SELF; PLUS; SELF
| SELF; MINUS; SELF ]
| "muldiv" LEFTA
[ SELF; MUL; MUL; SELF
| SELF; MUL; SELF
| SELF; DIV; SELF ]
| "atom" LEFTA
[ DIGIT ] ]
- : unit = ()
#

Pcaml モジュールの OCaml パーサ

ここまでで Grammar モジュールの一般的な側面を見てきました。ここからはいよいよ


(今度こそ)Camlp4 の OCaml パーサの姿を見ていきましょう。

OCaml の字句解析器

Camlp4 の Grammar を使うには Token.t Token.glexer 型の字句解析器が必要なのでした。


OCaml 用の字句解析器は Plexer モジュールで定義されており、 Plexer.gmake 関数で取得
26

することができます。

# Plexer.gmake ();;
- : Token.t Token.glexer =
{Token.tok_func = <fun>; Token.tok_using = <fun>; Token.tok_removing = <fun>;
Token.tok_match = <fun>; Token.tok_text = <fun>; Token.tok_comm = None}
#

もっとも構文拡張をする上ではこのモジュールを意識することはあまりありません。

OCaml の文法

OCaml 用の Grammar.g 型の文法は Pcaml モジュールの Pcaml.gram です。

# Pcaml.gram;;
- : Grammar.g = <abstr>

Ocaml 文法のエントリ

Pcaml.gram は以下の 17 のエントリを保持しています。それぞれアクションの結果となる


値の型が異なっています。
 Pcaml.interf
 Pcaml.implem
 Pcaml.top_phrase
 Pcaml.use_file
 Pcaml.module_type
 Pcaml.module_expr
 Pcaml.sig_item
 Pcaml.str_item
 Pcaml.expr
 Pcaml.patt
 Pcaml.ctyp
 Pcaml.let_binding
 Pcaml.type_declaration
 Pcaml.class_sig_item
 Pcaml.class_str_item
 Pcaml.class_expr
 Pcaml.class_type
このように沢山あるとうんざりしてきそうですが、殆どの場合構文拡張の対象になるのは
27

ほぼ Pcaml.expr エントリのみといえます。従ってこのチュートリアルでは Pcaml.expr エ


ントリのみに注目します。

Pcaml.expr エントリのレベル

Pcaml.expr の名前付きレベルは以下の 16 個です(「Grammar.Entry.print Pcaml.expr」で


確かめてみましょう)。
"top" これより下のレベルの式をセミコロンで連結した式のレベルです。
"expr1" “if” “match” “let … in …” “for” “while” などの構文的な式が定義され
るレベルです。なお expr1 があるから expr2 もあるかというとそういう
わけではないようです。
":=" 破壊的代入を行う演算子 “:=” “<-“ が定義されるレベルです。
"||" 論理和演算子 “||” “or” が定義されるレベルです。
"&&" 論理積演算子 “&&” “&” が定義されるレベルです。
"<" 比較演算子が定義されるレベルです。
"^" 文字列とリストの連結演算子 “^” “@” が定義されるレベルです。
"+" 演算子 “+” “-“ が定義されるレベルです。
"*" 演算子 “*” “/” などが定義されるレベルです。
"**" 演算子 “**” などが定義されるレベルです。
"unary minus" マイナス符号としての “-“ “-.” が定義されるレベルです。
"apply" 関数適用のレベルです。
"label" 関数のラベル “~x” などが定義されるレベルです。
"." ドットを含む式(レコードやモジュールのアクセス、文字列や配列の添
え字参照 “.()” “.[]” など)が定義されるレベルです。
"~-" 参照を解決する演算子 “!” などが定義されるレベルです。
"simple" リテラル式や式を中括弧で囲んだものが定義される最もアトミックなレ
ベルです。

構文拡張を作る

You might also like