Professional Documents
Culture Documents
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 を理解する
はじめに
プリプロセッサとは何か
Camlp4 の基本的な用途はプリプロセッサです。プリプロセッサとはコンパイラがソース
コードを解釈する前段階でソースに変換を加えるプログラムです。例えば C 言語ではプロ
グラムソースファイルはコンパイラが解釈するまえにまず cpp と呼ばれるプリプロセッサ
によって変換されます。
cpp の一般的な用途はマクロ定義と呼ばれる文字列の置き換え、ヘッダファイルの読み込
1
み、そして条件コンパイルです 。
1
C 以外の言語ではこうしたことをコンパイラの機能で行うものもあります
3
printf("fact(%d)\n", n);
#endif
if (n == 0) {
return 1;
} else {
return n * fact(n - 1);
}
cpp は # で始まる行(ディレクティブ)を解釈し、その指示に従ってソースコードを変換
していきます。コンパイラは変換結果だけを受け取るため、ディレクティブを読むことはあ
りません。
OCaml でいろいろなプリプロセッサを使う
$ 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$
$ 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$
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>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>
ま と め :
ocamlc でプリプロセッサを使うには –pp オプションを使う。Camlp4 はその中で使え
る選択肢の一つ。
Camlp4 を使う
Camlp4 で何もしない
F:\>type tryme.ml
let
identity
x
=
x
F:\>
上記の例ではわざとソースファイルに不必要な改行を入れています。しかし camlp4 が処
理した後の出力ではそれらがなくなっています。これは何故でしょうか。
実はここに Camlp4 のプリプロセシングの特徴が現れています。先に紹介した sed や cpp
や ocaml+twt のようなプリプロセッサは多かれ少なかれテキストレベルでの置換を行う
ものでした。これに対して Camlp4 は入力ソースファイルを一旦「構文木 (abstract
syntax tree)」と呼ばれる抽象的な表示に変換しているのです。
上記のコマンドは以下のような変換を行います。
構文木には元のソースファイルの空白は残らないので、構文木から再構成されたコードは
元のソースコードと同じとは限りません。
camlp4 を使うには最低 2 つの .cmo ファイルを引数として与えなければいけません。
pa_*.cmo パーサ:入力ソースを構文木に変換する
pr_*.cmo プリンタ:構文木から何らかの別の表示への変換を行う
F:\>
ちゃんとコンパイルできました。さて、今度は以下のようにソースファイルにバグを紛れ込
ませて同じことをして見ましょう。
F:\>type tryme.ml
let
identity
x
=
y
F:\>
変数 y が束縛されていないのでエラーになります。ここでエラーの表示に注目しましょう。
まず(a)ファイルが tryme.ml ではなく、(b)エラー発生箇所が本来の 5 行目ではなく 1 行
目となっています。
(a)はプリプロセシングの際に一時ファイルが作られ、それがコンパイラに渡されていると
いうことを意味しています。(b)は先ほど見たように変換後のファイルでは改行が取り除か
れていたのでコンパイラにとっては 1 行目でエラーが発生したというように見えるわけで
す。しかし複雑なソースファイルで適切なエラー箇所の表示が出なければデバッグ作業は
絶望的です。
この問題はプリンタとして pr_dump.cmo を使うことにより解決できます。
F:\>
今度は適切に 5 行目にエラーがあることが分かりました。
実は ocamlc はプリプロセッサからの入力として通常の OCaml ソースコードだけではな
く、構文木を直接シリアライズした特殊なバイナリ形式を受け取ることができます。
pr_dump.cmo は構文木をその形式でエクスポートするためのプリンタです。 OCaml の構
文木には元のソースコードの位置情報は保存されているため、コンパイルでエラーが出た
場合にはその情報を使ってエラー表示を出すことができるのです。
7
構文拡張を使ってみる
「Camlp4 を使う」というのは多くの場合「入力ソースを構文木に変換するルールに対して
変更を加える」ということを意味しています。Camlp4 が「文法を変えるプリプロセッサ」と
呼ばれることがあるのはこのためです。変更を加えるには pa_o.cmo に定義されたデフォ
ルトのルールを変更するための pa_*.cmo を作成し、それを camlp4 のオプションに与
えます。
自分で作るのはまだ後回しにして、まずは既に用意されている構文拡張を使ってみること
にしましょう。
標準添付の構文拡張の一つである pa_macro.cmo は cpp のような機能を提供します。以
下では条件コンパイルを使ってみました。
F:\>type tryme.ml
let rec fact n =
IFDEF DEBUG THEN
Printf.printf "fact(%d)\n" n
ELSE
()
END;
8
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 添付の他にもサードパーティの構文拡張を作成して公開しているサイトが存在し
ます。ここでは http://martin.jambon.free.fr/p4ck.html を紹介しておきます。
# #load "camlp4o.cma";;
Camlp4 Parsing version 3.09.0
#
pa_op.cmo を使うことに相当します。
構文木の姿を捉える
# #load "camlp4o.cma";;
Camlp4 Parsing version 3.09.0
2
これは大まかに言うと.ml ファイルを読むためのパーサです。.mli ファイルに対しては
Pcaml.parse_interf を使います。
10
ではいよいよ自分でパーサを書いてみましょう。まずは単純で乱暴で役に立たない例から
はじめます。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
ロードの順序
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
クォーテーション
先ほどの例では構文木を直接扱ってきました。しかし単純なコードでさえ非常に長いヴァ
リアント型定義になってしまいます。面倒です。そして 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;
F:\>
コンパイルエラーが出てしまいました。pa_hello2.ml はどのように変換されているのでし
ょうか。pr_o.cmo を使って確認してみましょう。
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
F:\>
確かに<:str_item<>>の中に書いたコードが構文木に変換されていました。しかしこの中
で_loc という変数を使っていて、それが位置情報に束縛されている必要があるようです。と
いうわけで_loc 変数を定義してあげましょう。「({Lexing.pos_fname…}と位置情報のレコ
ード型リテラルを直接書いても良いのですが、Token モジュールに Token.dummy_loc とい
うダミーの位置情報が定義されているのでそれを使ってしまいましょう。ついでに
MLast.StExp と一緒にタプルに入れていた位置情報も_loc にしてしまいます。
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:\>camlprog
hello world
F:\>
代表的なクォーテーションと改訂構文
# #load "camlp4o.cma";;
Camlp4 Parsing version 3.09.0
# #load "q_MLast.cmo";;
# let _loc = Token.dummy_loc;;
14
str_item
<: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])
<:str_item<value x = 256>>
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>>
patt
<:patt<>>は内部にパターンを書くことができます。パターンとは「let x = 1」などと書く
3
http://wwwtcs.inf.tu-dresden.de/~tews/ocamlp4/
4
次期バージョンの Camlp4 3.10 でも可能となります
15
<:patt<_>>
<:patt<x>>
<:patt<[x::xs]>>
<:patt<(first,second)>>
<:patt<('A'..'Z' as c)>>
メモ: ここで取り上げた例はごく一部です。より完全なクォーテーションの情報として
は 以 下 の ペ ー ジ を 参 照 し て く だ さ い 。
http://caml.inria.fr/pub/docs/manual-camlp4/manual010.html
ま た 、 改 訂 構 文 に つ い て は 以 下 に ま と め ら れ て い ま す 。
http://caml.inria.fr/pub/docs/manual-camlp4/manual007.html
アンチクォーテーション
クォーテーションの中に $ で囲まれた部分があると、その部分は式として評価され、値がそ
の位置に埋め込まれます。これによりクォーテーションのテンプレート化を行うことがで
き、プログラムによる柔軟な構文木構築が可能になります。このような仕組みをアンチクォ
ーテーションと呼びます。
以下のような記法で文字列式をリテラルや識別子として埋め込むこともできます。
<:expr<$int:"5"$>> 整数の5
<:expr<$flo:"3.14"$>> 実数の3.14
<:expr<$str:"hello"$>> 文字列のhello
<:expr<$lid:"x"$>> 小文字で始まる識別子のx
<:expr<$uid:"None"$>> 大文字で始まる識別子のNone
16
クォーテーションとアンチクォーテーションを学んだまとめとして「逆ポーランド記法を
OCaml 構文木に変換するプリプロセッサ」を作ってみましょう。HelloWorld プリプロセッ
サと同様に Pcaml.parse_implem を書き換えます。
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;
ソースとする入力は拡張子は.ml としますが、内容は逆ポーランド記法にします。
12-3*
F:\>camlprog
-3
F:\>
Grammar モジュール
同じ結果になります。
5
このように Camlp4 は所々で徹底的な汎用化・一般化がなされた作りになっています。一
方でそのような極度の一般化が Camlp4 の理解をより一層難しいものにしているという側
面も否定できません。
18
文法と字句解析器とエントリ
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
6
Token.t 型は string * string 型として定義されています。
19
Token.t 型の
char型のストリーム 字句解析器 ‘ a 型のエントリ ‘ a型の値
ストリーム
まとめ: 文字列ストリームが字句解析器を通してトークンのストリームになり、トーク
ンのストリームをエントリが解釈して何らかの別の表示(例えば構文木)へ変換する。
Grammar モジュールを使う(中置演算子記法パーサを作る)
#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 }
# 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>
#
文法を作る
字句解析器と文法は文法を最初に作る時点で結び付けられます。文法は以下のように
21
Grammar.gcreate 関数を使って作成します。
ここはこれだけです。
エントリを作る
エントリを拡張する
ここからが本番です。今作ったエントリに四則演算のルールを定義していきます。エントリ
の拡張には 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 の要素、エントリの場合はアクションの結果)に変数を束縛し、
アクションの中で使うことができます。規則はパイプで区切って複数並べることができま
す。規則のアクションの値は同一エントリ内のすべての規則で同じ型でなければいけませ
ん 。
このエントリを使って式をパースしてみましょう。
エントリに優先順位をつける
パイプで並列された規則はデフォルトでは優先順位を持たず、左結合として解釈されます。
先ほどのエントリの実行例でも単に左から順に計算したものとなっており、通常の数学の
規則とは異なっています。
規則に優先順位をつけるためには以下のように記述します。
先ほどのエントリでは大括弧を単に 2 つ重ねて書いていましたが、今度は優先順位のグル
ープごとに規則を 1 組の大括弧でまとめ、それをパイプで並列して外側の大括弧で括って
23
います。優先順位は下に行くほど強くなります。
これで掛け算が先に計算されるようになりました。
優先順位をつけられるような規則のまとまりを「レベル」と呼びます。エントリは複数のレ
ベルから成っており、レベルは複数の規則から成っています。
エントリ
レベル
規則
レベル
規則
レベル
規則
規則
規則
規則
規則
規則
規則
優先順位
エントリを拡張する
もっともどうなるかを観察するにはエントリの中身を見ることができなくてはいけません
そのためのコマンドが 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 = ()
#
新しい規則は一番上の(最も優先順位が低い)レベルに追加されたことが分かります。し
かし任意の位置に規則を追加したい場合はどうすればいいのでしょうか。
レベルに名前をつける
新しい規則を指定した位置に追加するには、予めレベルに名前をつけておき、追加時にその
名前を指定します。この名前をラベルと呼びます。ラベルをつけるにはレベルの大括弧の手
前に文字列リテラルを置きます。エントリを作り直しましょう。
# 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 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 = ()
#
OCaml の字句解析器
することができます。
# 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 の文法
# Pcaml.gram;;
- : Grammar.g = <abstr>
Ocaml 文法のエントリ
Pcaml.expr エントリのレベル
構文拡張を作る