If (Eq(IntLit 2, IntLit 1), Times(IntLit 1, IntLit 2), Times(IntLit 1, Plus(IntLit 2,IntLit 3)))というように exp型の式として書かなくてはならず、入力が大変であり、間違いやすい。 もし、
parse "if 2=1 then 1*2 else 1*(2+3)"とすると、上記のexp型の式を生成してくれる関数parseがあれば、大変に便利である。
ここで、「構文解析する」という言葉を使ってきたが、正確には以下の2つのプログラムから構成される。
lexerやparserをゼロから書くのは、プログラミングとしては大変面白い課題であるが、 今日では、言語の文法ルールを一定の書式で記述すれば、 lexerとparser (のプログラム)を自動的に生成してくれる、という便利なプログラムがあるので、 この実験では、それを使うことにする。 C言語用のそれは lex, yacc という名称であり、 OCaml でもそれに対応して、ocamllex, ocamlyacc というプログラムがある。
[2024/10/01追加] 現在では、OCaml用の構文解析器としては ocamlyacc のかわりにmenhir というプログラムが使われるので、本実験でもそちらを推奨する。 使い方は、ocamlyacc とほとんど同じなので、以下の説明はそのまま menhir 用としても通用する。
ミニOCaml言語の構文解析において準備すべきファイルは以下の4つである。
exp ::= arg_exp | exp arg_exp | MINUS exp | exp PLUS exp | ...という BNF に対応している。arg_exp などの後に { $1 } と書いてあるのは、 「arg_exp型の式が入力されたら、それを、どういう(OCamlの)データに変換するか」という 処理である。たとえば、exp EQUAL exp { Eq ($1, E3) } は、 e1 = e2 の形の式が入力されたら、Eq(e1,e2) の形のOCamlデータを生成する、 という意味である。ここで $1と$3 は、入力トークン列の1番目と3番目のデータを意味する (この例の場合、2番目は = というトークンである)。
このファイルの上の方にある %token EQUAL 等の行は、すべてのトークンを列 挙したものである。将来、lexer が解析する対象を拡張したら (lexer.mllを 拡張したら)この %token 宣言も増やさなければいけない。
「演算子優先順位」と記述したところから数行は、たとえば、 1+2*3 を (1+2)*3 とするのか、1+(2*3)とするのか、という 演算子の優先度を指定する部分である。 ここでは、PLUSや MINUS が ASTERRISK (かけ算の記号)や SLASH (割り算の記号) より先に書いてあるので、PLUSやMINUS が、かけ算、割り算より優先度が低いことを意味する。 left, right はそれぞれ、「左結合的」、「右結合的」を意味し、nonassoc は「結合的でない」 ことを意味するが、詳細は省略する。
ここでの処理により、入力トークン列(lexerが生成したもの)は、exp型のデータに変換される。 ここでは、exp型だけでなく(将来 match構文などに拡張することを見越して) パターンマッチのための構文も定義しているので、若干ファイルが大きくなっ ている。
% opam install menhir
% ocamllex lexer.mll (これで、lexer.ml というファイルができる。) % ocamlyacc parser.mly (これで、parser.ml というファイルができる。)なお、ocamlyacc のかわりに menhir を使う場合は、 以下のようにする.
% ocamllex lexer.mll (これで、lexer.ml というファイルができる。) % ocamlc -c syntax.ml (先に syntax.mlをコンパイルする必要がある.) % menhir --infer parser.mly (これで、parser.ml というファイルができる。)この段階でエラーが出たら、先に進まずにその理由を吟味すること。 不明な点は TA に聞くと良い。
% make (これだけで、miniocamlというファイルができる。)そのあと、Unixシェルから (ocamlを起動するかわりに) miniocaml を起動すると、 syntax.ml,eval.ml など上記のファイルがすべて読み込み済みの状態の OCaml が起動するので、便利である。
% miniocaml OCaml version 4.14.1 #また、1度make をしておくと、そのあと parser.mly だけを変更した場合、 make とやると、変更したファイルに依存する部分のみ再コンパイルし、最新の miniocaml を作ってくれる。 Makefile の詳細な使い方は make のマニュアルページを読むこと。
補足: Makefileは大変便利だが、結局何をやっているかわからないので、 「高度な利用法」のかわりに、手動で同じことをやってみよう。 自分が作成したファイル(本実験で作成中の eval3 関数などがはいったファイル)以外に、 lexer.ml, parser.ml, syntax.ml, main.ml の4つのファイルを読み込めばよい。 ただし、ここでは、ファイルが分割された関係で、 他のファイルの内容を「モジュール」として読みこみたいので、 各ファイルをコンパイルしてから読みこむことにする。 (C言語でも、個別のファイルを cc -c xxx.c とコンパイルしてから、それを 最後にリンクして、1つの実行ファイルを作る。それと同様である。)
OCamlのコンパイラは、ocamlc という名称であり、コンパイルされたオブジェ クトファイルは、xxx.cmo という名前である。そこで以下のようにする。
% ocamlc -c "syntax.ml" (これを実行済みの場合は不要) % ocamlc -c "eval.ml" (自分のインタープリタのファイル名を eval.ml とする) % ocamlc -c "parser.mli" % ocamlc -c "lexer.ml" % ocamlc -c "parser.ml" % ocamlc -c "main.ml" (この時点で、syntax.cmo 等ができている) % ocaml OCaml version 4.14.1 # #load "syntax.cmo";; (xxx.cmoファイルを読みこむのは #use でなく #load である。) # #load "eval.cmo";; # #load "lexer.cmo";; # #load "parser.cmo";; # #load "main.cmo";; # parse "1+2*3";; # eval3 (parse "1+2*3") ;;
亀山 幸義