racc は yacc を知っていることを前提にしていますので、もし知らないのな ら先に yacc を勉強しましょう。いきなり racc を使うのは不可能です(これ は断言できます)。…と書いていたのですが、C マガでごとけんさんに
そんなことはないです。これは断言できますと書かれてしまったので筆者ももう少しねばってみます。しかしやっぱり初歩 から親切丁寧というわけにはいかないので、詳しくは 2001 年 2 月末発売の 「Ruby を 256 倍使う本 無道編」(青木峰郎著、ASCII) を読んでください。
Racc は文法を処理するツールです。
文字列はただの文字の列で、コンピュータにとっては意味を持ちません。しかし
人間はその文字の列の中になにか意味を見出すことができます。コンピュー
タにもそのようなことを、部分的にでも、させられたら便利でしょう。Racc は
その手伝いをしてくれます。完全な自動化ではありませんが、人間が全部やるよ
りも遥かに簡単な手段で可能になります。
その、Racc が自動化してくれる部分とは、文字列の含む「構造」の処理です。 たとえば Ruby の if 文を考えてみると、次のように定式化できます。
if 文では if という単語が最初になくてはならず、elsif 節は else 節より 前になくてはいけません。このような配置の関係(構造)が、Racc で処理でき るものです。if 条件式 [then] 文 : [elsif 条件式 [then] 文 :] [else 文 :] end
一方、Racc で処理できないのはどういうことでしょうか。それは、たとえば if の条件式にあたる部分が「なんであるか」ということです。つまり、条件 式が if の条件だということです。これは、こっちで条件として扱うコードを 書いてやらないといけません。
と言っても、わかりにくいでしょう。こういう抽象的なものは実際にいじって みるのが一番です。
実際に Racc をどのように使うかという話をします。Racc には独自のソース コードみたいなものがあって、この中に処理したい「構造」を記述しておきま す。このソースファイルを「文法ファイル」と呼ぶことにしましょう。この文 法ファイルの名前が parse.y と仮定すると、コマンドラインから以下のよう に打ちこめば、その構造を処理するためのクラスを含んだファイルが得られま す。
生成されるファイルはデフォルトでは "ファイル名.tab.rb" です。他の名前 にしたいなら、-o オプションで変更できます。$ racc parse.y
このようにして作ったクラス、またはそのような処理を担当するパート、のこ とはパーサ(parser)と呼ぶことになっています。解析するヤツ、っていうくら いに適当にとらえてください。$ racc parse.y -o myparser.rb
Racc は文法ファイルから Ruby のクラスを生成するツールだと言いました。 そのクラスは全て Racc::Parser の下位クラスで、名前は文法ファイル中で 指定します。以下、ここに書くべきことが「なんなのか」を説明します。 ここでは内容に重点を置くので、文法ファイル自体の文法の詳細は 文法リファレンス を見てください。
まずは、全体の概形です。
Ruby スクリプトのように class でパーサクラス名を指定し、rule ... end の間にパーサに解析させたい文法を記述します。class MyParser rule if_stmt: IF expr then stmt_list elsif else END then : THEN | elsif : | ELSIF stmt_list else : | ELSE stmt_list expr : NUMBER | IDENT | STRING stmt_list : ふにゃふにゃ end
文法は、記号の並びでもって表します。rule ... end の間にあるコロンとバー 以外のもの、if_stmt IF expr then などが全て「記号」です。そしてコロン が日本語で言う「〜は××だ」の「は」みたいなもんで、その左の記号が右の 記号の列と同じものを指す、というふうに定義します。また、バーは「または」 を意味します。それと、単純にコロンの左の記号のことを左辺、右を右辺とも 言います。以下はこちらのほうを使って説明しましょう。
少し注意が必要な点を述べます。まず、then の、バーのあとの定義(規則)を 見てください。ここには何も書いていないので、これはその通り「無」であっ てもいい、ということを表しています。つまり、then は記号 THEN 一個か、 またはなにもなし(省略する)でよい、ということです。記号 then は実際の Ruby のソースコードにある then とは切り離して考えましょう(それは実は大 文字の記号 THEN が表しています)。
さて、そろそろ「記号」というものがなんなのか書きましょう。 ただし順番に話をしないといけないので、まずは聞いていてください。 この文章の最初に、パーサとは文字の列から構造を見出す部分だと言いました。 しかし文字の列からいきなり構造を探すのは面倒なので、実際にはまず 文字の列を単語の列に分割します。その時点でスペースやコメントは捨てて しまい、以降は純粋にプログラムの一部をなす部分だけを相手にします。 たとえば文字列の入力が次のようだったとすると、
単語の列は次のようになります。if flag then # item found. puts 'ok' end
ここで、工夫が必要です。どうやら flag はローカル変数名だと思われますが、 変数名というのは他にもいろいろあります。しかし名前が i だろうが a だろ うが vvvvvvvvvvvv だろうが、「構造」は同じです。つまり同じ扱いをされる べきです。変数 a を書ける場所なら b も書けなくてはいけません。だったら 一時的に同じ名前で読んでもいいじゃん。ということで、この単語の列を以下 のように読みかえましょう。if flag then puts 'ok' end
これが「記号」の列です。パーサではこの記号列のほうを扱い、構造を見付け ていきます。IF IDENT THEN IDENT STRING END
さらに記号について見ていきましょう。 記号は二種類に分けられます。「左辺にある記号」と「ない記号」です。 左辺にある記号は「非終端」記号と言います。ないほうは「終端」記号と 言います。最初の例では終端記号はすべて大文字、非終端記号は小文字で 書いてあるので、もう一度戻って例の文法を見てください。
なぜこの区分が重要かと言うと、入力の記号列はすべて終端記号だからです。 一方、非終端記号はパーサの中でだけ、終端記号の列から「作りだす」ことに よって始めて存在します。例えば次の規則をもう一度見てください。
expr は NUMBER か IDENT か STRING だと言っています。逆に言うと、 IDENT は expr に「なることができます」。文法上 expr が存在できる 場所に IDENT が来ると、それは expr になります。例えば if の条件式の 部分は expr ですから、ここに IDENT があると expr になります。その ように文法的に「大きい」記号を作っていって、最終的に一個になると、 その入力は文法を満たしていることになります。実際にさっきの入力で 試してみましょう。入力はこうでした。expr : NUMBER | IDENT | STRING
まず、IDENT が expr になります。IF IDENT THEN IDENT STRING END
次に THEN が then になります。IF expr THEN IDENT STRING END
IDENT STRING がメソッドコールになります。この定義はさきほどの例には ないですが、実は省略されているんだと考えてください。そしていろいろな 過程を経て、最終的には stmt_list (文のリスト)になります。IF expr then IDENT STRING END
elsif と else は省略できる、つまり無から生成できます。IF expr then stmt_list END
最後に if_stmt を作ります。IF expr then stmt_list elsif else END
ということでひとつになりました。つまりこの入力は文法的に正しい入力です。if_stmt
ここまでで入力の文法が正しいかどうかを確認する方法はわかりましたが、 これだけではなんにもなりません。最初に説明したように、ここまででは 構造が見えただけで、プログラムは「意味」を理解できません。そしてその 部分は Racc では自動処理できないので、人間が書く、とも言いました。 それを書くのが以下に説明する「アクション」という部分です。
前項で、記号の列がだんだんと大きな単位にまとめられていく過程を見ました。 そのまとめる時に、同時になにかをやらせることができます。それが アクションです。アクションは、文法ファイルで以下のように書きます。
見てのとおり、規則のあとに { と } で囲んで書きます。 アクションにはだいたい好きなように Ruby スクリプトが書けます。class MyParser rule if_stmt: IF expr then stmt_list elsif else END { puts 'if_stmt found' } then : THEN { puts 'then found' } | { puts 'then is omitted' } elsif : { puts 'elsif is omitted' } | ELSIF stmt_list { puts 'elsif found' } else : { puts 'else omitted' } | ELSE stmt_list { puts 'else found' } expr : NUMBER { puts 'expr found (NUMBER)' } | IDENT { puts 'expr found (IDENT)' } | STRING { puts 'expr found (STRING)' } stmt_list : ふにゃふにゃ end
(このドキュメントはまだ不完全です。続きはこれから書きます)
yacc での $$
は Racc ではローカル変数 result
で、$1,$2...
は配列 valです。
result
は val[0]
($1) の値に初期化され、アク
ションを抜けたときの result
の値が左辺値になります。Racc
ではアクション中の return
はアクションから抜けるだけで、
パース自体は終わりません。アクション中からパーズを終了するには、メソッ
ド yyaccept
を使ってください。
演算子の優先順位、スタートルールなどの yacc の一般的な機能も用意されて います。ただしこちらも少し文法が違います。
yacc では生成されたコードに直接転写されるコードがありました。Racc でも 同じように、ユーザ指定のコードが書けます。racc ではクラスを生成するの で、クラス定義の前/中/後の三個所があります。Racc ではそれを上から順番 に header inner footer と呼んでいます。
パースのエントリポイントとなるメソッドは二つあります。ひとつは
do_parse
で、こちらはトークンを
Parser#next_token
から得ます。もうひとつは
yyparse
で、こちらはスキャナから yield
され
ることによってトークンを得ます。ユーザ側ではこのどちらか(両方でもいい
けど)を起動する簡単なメソッドを inner に書いてください。これらメソッド
の引数など、詳しいことはリファレンスを見てください。
どちらのメソッドにも共通なのはトークンの形式です。必ずトークンシンボル
とその値の二要素を持つ配列を返すようにします。またスキャンが終了して、
もう送るものがない場合は
パーザは別に文字列処理にだけ使われるものではありませんが、実際問題とし
て、パーザを作る場面ではたいてい文字列のスキャナとセットで使うことが多
いでしょう。Ruby ならスキャナくらい楽勝で作れますが、高速なスキャナと
なると実は難しかったりします。そこで高速なスキャナを作成するためのライ
ブラリも作っています。詳しくは
「スキャナを作る」の項を見てください。
Racc には error トークンを使ったエラー回復機能もあります。yacc の
ユーザがアクション中でパースエラーを発見した場合は、メソッド
これだけあればだいたい書けると思います。あとは、最初に示した方法で文法
ファイルを処理し、Ruby スクリプトを得ます。
うまくいけばいいのですが、大きいものだと最初からはうまくいかないでしょ
う。racc に -g オプションをつけてコンパイルし、@yydebug を true にする
とデバッグ用の出力が得られます。デバッグ出力はパーザの @racc_debug_out
に出力されます(デフォルトは stderr)。また、racc に -v オプションをつけ
ると、状態遷移表を読みやすい形で出力したファイル(*.output)が得られます。
どちらもデバッグの参考になるでしょう。
Racc の生成したパーザは動作時にランタイムルーチンが必要になります。具
体的には parser.rb と cparse.so です。ただし cparse.so は単にパースを
高速化するためにあり、必須ではありません。これらのファイルを自分でセッ
トアップするのはなかなか面倒です。Racc をユーザみんなにインストールし
てもらうのも一つの手ですが、これでは不親切です。そこでRacc では回避策
を用意しました。
racc に -E オプションをつけてコンパイルすると必要なものを全部結合した
ファイルが得られます。これだとファイルはひとつだけなので扱いが楽です
(この形式のパーサのファイルが複数あったとしてもクラスやメソッドが衝突
することはありません)。ただし cparse.so が使えませんので、必然的に動作
は全て Ruby スクリプトレベルで行われ、速度は低下します。ただしこれにも
例外があって、配布先に既に Racc ランタイムがあるときは自動的にそちらが
使われます。
パーサを使うときは、たいてい文字列をトークンに切りわけてくれるスキャナ
が必要になります。しかし実は Ruby は文字列の最初からトークンに切りわけ
ていくという作業があまり得意ではありません。正確に言うと、簡単にできる
のですが、非常に大きいオーバーヘッドがかかります。
そのオーバーヘッドを回避しつつ、手軽にスキャナをつくれるように strscan
というパッケージを同梱しています。ドキュメントなどは
筆者のホームページから取れるので、試してみてください。
Copyright (c) 1999-2001 Minero Aoki
<aamine@cd.xdsl.ne.jp>
[false,なにか を返し
てください。これは一回返せば十分です (逆に、
yyparse
を使
う場合は二回以上 yield
してはいけない)。
yyerror()
は Racc では
Parser#on_error
で、エラーが
おきたトークンとその値、値スタック、の三つの引数をとります。
on_error
はデフォルトでは例外 ParseError
を
発生するようになっています。
yyerror
を呼べばパーサがエラー回復モードに入ります。ただし
このときは on_error
は呼ばれないので、なにか報告をしたい時
はユーザが明示的に on_error
を呼んだりする必要があります。
パーザを生成する
作ったパーザを配布する
おまけ:スキャナを作る