24
1 Camlp4 ををををを......................................................2 をををを............................................................2 ををををををををををを.....................................................2 OCaml をををををををををををををををを..........................................3 Camlp4 ををを......................................................4 Camlp4 をををををを...................................................4 をををををををををを......................................................7 OCaml ををををををを Camlp4 ををを........................................8 ををををををををを.......................................................8 ををを Hello World をををををを............................................9 をををををををを.......................................................11 ををををををををををををををををを..............................................13 str_item......................................................13 expr..........................................................14 patt..........................................................14 ををををををををををを....................................................14 ををををををををを OCaml ををををを............................................15 Grammar ををををを...................................................16 ををををををををををををを..................................................17 Grammarを をを () .................................18 をををををををを......................................................18 ををををを.........................................................19 ををををををを.......................................................20 ををををををををを.....................................................20 ををををををををををををを.................................................21 ををををををををを.....................................................22 をををををををををを....................................................23 Pcaml をををををを OCaml ををを...........................................24 OCaml をををををを..................................................24 OCaml ををを.....................................................24 Ocaml ををををををを.................................................25 Pcaml.expr をををををををを...........................................25 ををををををを........................................................26

Understanding Camlp4

  • Upload
    ether

  • View
    595

  • Download
    0

Embed Size (px)

DESCRIPTION

Yet another tutorial on Camlp4, written in Japanese.

Citation preview

Page 1: Understanding Camlp4

1

Camlp4 を理解する...............................................................................................2

はじめに............................................................................................................2

プリプロセッサとは何か......................................................................................2

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

Camlp4 を使う...................................................................................................4

Camlp4 で何もしない.........................................................................................4

構文拡張を使ってみる.........................................................................................7

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

構文木の姿を捉える.............................................................................................8

何でも Hello World にするパーサ.........................................................................9

クォーテーション..............................................................................................11

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

str_item.......................................................................................................13

expr.............................................................................................................14

patt..............................................................................................................14

アンチクォーテーション....................................................................................14

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

Grammar モジュール........................................................................................16

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

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

字句解析器を作る...........................................................................................18

文法を作る....................................................................................................19

エントリを作る..............................................................................................20

エントリを拡張する.......................................................................................20

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

エントリを拡張する.......................................................................................22

レベルに名前をつける....................................................................................23

Pcaml モジュールの OCaml パーサ.....................................................................24

OCaml の字句解析器......................................................................................24

OCaml の文法...............................................................................................24

Ocaml 文法のエントリ...................................................................................25

Pcaml.expr エントリのレベル........................................................................25

構文拡張を作る.................................................................................................26

Page 2: Understanding Camlp4

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 printf("fact(%d)\n", n);#endif

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

Page 3: Understanding Camlp4

3

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.mllet _ = print_float PI$ ocamlc tryme.mlFile "tryme.ml", line 1, characters 20-22:Unbound constructor PI$ ocamlc -pp 'sed s/PI/3.14/' tryme.ml$ ./a.out3.14$

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

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

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

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

レベルによるグルーピングを行うプリプロセッサです。

以下のプログラムで最終行のパターンは 2 つある match のどちらに属するでしょうか。

Page 4: Understanding Camlp4

4

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

F:\ocaml+twt>ocamlc tryme.mlFile "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:NoneFile "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 が何をしているかをよ

く知るためにまずは単体で使ってみることにしましょう。

Camlp4 で何もしない

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

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

Page 5: Understanding Camlp4

5

F:\>type tryme.mlletidentityx=x

F:\>camlp4 pa_o.cmo pr_o.cmo tryme.mllet identity x = x

F:\>

上記の例ではわざとソースファイルに不必要な改行を入れています。しかし camlp4 が

処理した後の出力ではそれらがなくなっています。これは何故でしょうか。

実はここに Camlp4 のプリプロセシングの特徴が現れています。先に紹介した sed や

cpp や ocaml+twt のようなプリプロセッサは多かれ少なかれテキストレベルでの置換を

行うものでした。これに対して Camlp4 は入力ソースファイルを一旦「構文木(abstract

syntax tree)」と呼ばれる抽象的な表示に変換しているのです。

上記のコマンドは以下のような変換を行います。

構文木には元のソースファイルの空白は残らないので、構文木から再構成されたコード

は元のソースコードと同じとは限りません。

camlp4 を使うには最低 2 つの .cmo ファイルを引数として与えなければいけません。

pa_*.cmo パーサ:入力ソースを構文木に変換する

pr_*.cmo プリンタ:構文木から何らかの別の表示への変換を行う

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

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

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

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

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

F:\>

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

れ込ませて同じことをして見ましょう。

Page 6: Understanding Camlp4

6

F:\>type tryme.mlletidentityx=y

F:\>ocamlc -pp "camlp4 pa_o.cmo pr_o.cmo" tryme.mlFile "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.mlFile "tryme.ml", line 5, characters 0-1:Unbound value y

F:\>

今度は適切に 5 行目にエラーがあることが分かりました。

実は ocamlcはプリプロセッサからの入力として通常の OCaml ソースコードだけではな

く、構文木を直接シリアライズした特殊なバイナリ形式を受け取ることができます 。

pr_dump.cmoは構文木をその形式でエクスポートするためのプリンタです。OCaml の

構文木には元のソースコードの位置情報は保存されているため、コンパイルでエラーが

出た場合にはその情報を使ってエラー表示を出すことができるのです。

Page 7: Understanding Camlp4

7

以上のことから 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.mllet rec fact n =IFDEF DEBUG THEN Printf.printf "fact(%d)\n" nELSE ()END; 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:\>camlprog120F:\>ocamlc -pp "camlp4 pa_o.cmo pa_macro.cmo pr_dump.cmo -DDEBUG" tryme.ml

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

Page 8: Understanding Camlp4

8

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 pa_op.cmo を使うことに相当します。

構文木の姿を捉える

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

定義しています。ここではそれを実際に使ってみることにしましょう。

pa_o.cmo はロードされると Pcaml モジュールの中に OCaml パーサを定義します。

Pcaml.parse_implem というのがそれです2。トップレベルで以下のように打ち込んでみ

てください。

# #load "camlp4o.cma";;

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

Page 9: Understanding Camlp4

9

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 の略です)。

「 {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 はそれを呼んで構文木を

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

Page 10: Understanding Camlp4

10

何でも 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}, {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 プログラムを生

Page 11: Understanding Camlp4

11

成するようになります。

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

F:\>camlproghello world

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

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

グのようです。

クォーテーション

先ほどの例では構文木を直接扱ってきました。しかし単純なコードでさえ非常に長いヴ

ァリアント型定義になってしまいます。面倒です。そして 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;

Page 12: Understanding Camlp4

12

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

ません。実はクォーテーションは Camlp4 のプリプロセシング機能として提供されてい

るので、この pa_hello2.ml自体も Camlp4 に通してコンパイルするのです。クォーテ

ーションを提供するモジュールは q_MLast.cmo です。やってみましょう。

F:\>ocamlc -c -I +camlp4 -pp "camlp4o q_Mlast.cmo" pa_hello2.mlFile "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}, {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 inignore (Stream.count s);[<:str_item< print_string "hello world" >>, _loc], false;

Page 13: Understanding Camlp4

13

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

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

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

F:\>camlproghello worldF:\>

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

前節ではクォーテーションの書き方として <: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;;

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>>

Page 14: Understanding Camlp4

14

改訂構文はクォーテーションを書く上で悩ましい制約ですが 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」などと

書くとくの「=」の左辺や「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 のような要領で変数

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

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

Page 15: Understanding Camlp4

15

# 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

逆ポーランド記法を 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;

Page 16: Understanding Camlp4

16

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 構文木を作

り上げます。このプリプロセッサ自身は四則演算をしているわけではなく構文木を作っ

ているだけだということに注意してください。

コンパイルは以下のようにします。

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-3F:\>

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");;

Page 17: Understanding Camlp4

17

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

(改訂構文)。

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

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

ュールです。つまり Grammar モジュール自体は OCaml をパースするだけのものではな

く、何に対しても使うことができ、pa_o.cmo はあくまでもそれを使っているだけです5。

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

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

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

を構築している。

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

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

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

下のようになります。

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

関連付けられます。この字句解析器は大まかに言うと char Stream.t型(キャラクタのス

トリーム)の入力を Token.t Stream.t型(Token.t型6のストリーム)に変換する役目を持

っています。Grammar.Entry.parse の入力の char Stream.t はまずはこの字句解析器を

通して Token.t Stream.t に変換されます(より具体的には Token.t Token.glexer型のメ

ンバである tok_func という関数がその変換を行います)。

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

Page 18: Understanding Camlp4

18

そして Token.t Stream.t は「エントリ」と呼ばれる文法ルールによって何らかの型の値

に変換されます。この文法ルールが定義されるのが ’a Grammar.Entry.e型の値です。

「何らかの型」とは仕組みとしては何でも良く、OCaml パーサであれば先に見た構文木

の型と な り ま す 。 文 法 ( Grammar.g 型の値) は 任 意 の数の エ ン ト リ ( ’ a

Grammar.Entry.e型の値)を持つことができます。

まとめ: 文字列ストリームが字句解析器を通してトークンのストリームになり、トークン

のストリームをエントリが解釈して何らかの別の表示(例えば構文木)へ変換する。

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 }

Page 19: Understanding Camlp4

19

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

作ることができます。

# 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 に渡しても同様に作ることができます。本格的

なことがしたい場合はそちらを使うと良いでしょう。

Page 20: Understanding Camlp4

20

文法を作る

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

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;;

Page 21: Understanding Camlp4

21

- : 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組の大括弧でまとめ、それをパイプで並列して外側の大括弧で括

っています。優先順位は下に行くほど強くなります。

Page 22: Understanding Camlp4

22

これで掛け算が先に計算されるようになりました。

# 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 ]| LEFTA [ DIGIT ] ]- : unit = ()

Page 23: Understanding Camlp4

23

#

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

前の LEFTA はそのレベルが左結合であることを示しています。Grammar.Entry.print は

アクションまでは表示してくれません。

次に規則を追加して再度内容を Grammar.Entry.print してみます。

# EXTEND myexpr: [[x = myexpr; MUL; MUL; y = myexpr -> int_of_float ((float_of_Int 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 = ()#

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

Page 24: Understanding Camlp4

24

# 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_float ((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関数で取

得することができます。

# 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}#

Page 25: Understanding Camlp4

25

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

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

このように沢山あるとうんざりしてきそうですが、殆どの場合構文拡張の対象になるの

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

Pcaml.expr エントリのみに注目します。

Pcaml.expr エントリのレベル

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

Pcaml.expr」で確かめてみましょう)。

Page 26: Understanding Camlp4

26

"top" これより下のレベルの式をセミコロンで連結した式のレベルです。

"expr1" “if” “match” “let … in …” “for” “while” などの構文的な式が定義され

るレベルです。なお expr1 があるから expr2 もあるかというとそうい

うわけではないようです。

":=" 破壊的代入を行う演算子 “:=” “<-“ が定義されるレベルです。

"||" 論理和演算子 “||” “or” が定義されるレベルです。

"&&" 論理積演算子 “&&” “&” が定義されるレベルです。

"<" 比較演算子が定義されるレベルです。

"^" 文字列とリストの連結演算子 “^” “@” が定義されるレベルです。

"+" 演算子 “+” “-“ が定義されるレベルです。

"*" 演算子 “*” “/” などが定義されるレベルです。

"**" 演算子 “**” などが定義されるレベルです。

"unary minus" マイナス符号としての “-“ “-.” が定義されるレベルです。

"apply" 関数適用のレベルです。

"label" 関数のラベル “~x” などが定義されるレベルです。

"." ドットを含む式(レコードやモジュールのアクセス、文字列や配列の

添え字参照 “.()” “.[]” など)が定義されるレベルです。

"~-" 参照を解決する演算子 “!” などが定義されるレベルです。

"simple" リテラル式や式を中括弧で囲んだものが定義される最もアトミックな

レベルです。

構文拡張を作る