2012年09月15日

N6XBasicChecker(5)boost::spirit::qi::ruleの勘所

前回の続きです。
私がspiritを使う過程ではまったり悩んだりした点を書き連ねていきます。
■「>>」と「>」の違い
今度は前回と同じルールで、「A1」という文字列を数値型変数としてパースするケースを考えます。

StringRule num_var
= qi::raw[sw::alpha >> qi::repeat(0, 4)[sw::alnum]];
StringRule str_var
= qi::raw[sw::alpha >> qi::repeat(0, 4)[sw::alnum] >> sw::string(L"$")];
StringRule var
= qi::raw[str_var | num_var];

std::wstring a = L"A1";
bool r = qi::phrase_parse(a.begin(), a.end(), var, sw::blank);

前回触れたとおり、ルールは文字列変数str_varから評価します。
str_varの評価に置いては、最後の「$」がマッチしないため、「>>」でつながれたルールの分だけ後戻りし(これをバックトラックと呼びます)次の候補であるnum_varを評価します。
ところで、spiritのマニュアルをよく見ると、連続したルールを記述する演算子に「>>」と「>」があるのに気がつくと思います。
「$」の前の演算子が「>」であった場合を考えてみましょう。
この場合、「A1」までマッチした時点でもう後戻りはできず、「$」がこのあとに続かなければエラーになります。
spirit_backtrack.png
使い分けがイメージしにくいですが、
spiritの公式サンプル
http://www.boost.org/doc/libs/1_47_0/libs/spirit/example/qi/compiler_tutorial/mini_c/expression_def.hpp
が参考になると思いますが、式はオペランドからなり、オペランドは式からなる場合があります。
ルールの定義が循環しており、この場合入力がどのルールにもマッチしないとバックトラックを繰り返して無限ループに陥る場合があり、どこかでパースを打ち切る必要があります。
なので、バックトラックさせたくないところに「>」を入れるわけですが、どこに入れるのが適切かというのは正直「考えるな、感じろ」としか言いようのないところもあります。
経験的には、途中まで一致しているルールが複数ある場合以外「>」は必要無さそうな感じです。

■「lit」と「string」の違い
「lit」も「string」も文字列リテラルにマッチするルールという点では同じですが、属性が違います。
「string」は文字列型の属性を持ちますが、「lit」は属性を持ちません。
なので、マッチした文字列をセマンティックアクションで処理をしたい場合は、「string」を使用しましょう。
N6XBasicCheckerでは、「string」は非常に大量に出現するので、

#define L(a) (sw::string(L##a))

というマクロを定義してコードを短縮してます。

■セマンティックアクション
セマンティックアクションとは、入力がルールにマッチした際に実行する処理です。
処理は、ルールの後ろに[]をつけ、そこにラムダ式の形で記述します。
N6XBasicCheckerでは以下のような使い方をしています。

ParserStatus status;
//行番号
UintRule linenumber = uint_;
//行
BasicRule line
= linenumber[phoenix::bind(&ParserStatus::registerLineNumber, ref(status), _1)]
> +(L(":") || statement);

これは、行の先頭でマッチした数値(linenumber)を現在処理中のBASIC行として登録する処理です。

「_1」というのはルールにマッチした値で、型はルールの属性によって決まります。linenumberルールの実体はuint_というspiritの組み込みルールで、属性はunsigned intになります。
statusはParserStatusというクラスのインスタンスであり、これは現在読み込んでいる行番号、使われている変数などの情報を管理しています。
ParserStatus::registerLineNumber()はBASIC行番号を登録する関数です。
phoenix::bindはラムダ式内で関数を呼び出すための仕組みです。
refというのは、ラムダ式の外で宣言されている変数を参照渡しするためのおまじないです。

わかりにくいと思うので実例を示すと、
「100 A=10」という行をパースした際、ルールlinenumberに100という数値がマッチし、これが「_1」に入ります。
この時の処理として

status.registerLineNumber(100);

という処理を実行させたい場合、spiritでは

linenumber[phoenix::bind(&ParserStatus::registerLineNumber, ref(status), _1)]

という記述になります。

以上、自分の経験から書けるspiritの知見としてはこんなものでしょうかね。
自分でも感覚でやっと理解している程度のものを他人に説明しようというのが無謀かもしれません。
それでもなんでこんな記事を書いてるかというと、N6XBasicCheckerをつくとうと思って調べ物をしていた過程で痛感した「構文解析器のサンプルは電卓しかない」という状況に一石を投じたかったからです。
次回はユニットテストについて書きたいと思います。
posted by eighttails at 03:00| Comment(0) | N6XBasicChecker | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント: