bnf::BnfParser - BNF パーザジェネレータbnf::BnfParser は,BNF で書かれたルールを読み込み,その通りに入力データを解析するための C++ ライブラリです.
本ライブラリは boost::spirit の機能を目標にしており,出来る限り多くの機能や「癖」を模しています.
boost::spirit とは異なり,ルールを外部ファイルに記述しておく事が出来ます.そのためルールの修正が容易かも? ただしソースコードのコンパイル時にルールも構築される boost::spirit に比べて,本ライブラリは実行時にルールを構築するので,パフォーマンス的には若干劣るかもしれません.また,ルールの記述に誤りがあっても,検出するのが困難な場合があります.
で,作ってみたのはいいんですけど,面白い使い道が思いつかないっすねー.
ver.0.02: v0_02 ディレクトリにソースファイルをそのまま置いていますが,面倒な方は zip アーカイブをダウンロードして下さい.tests ディレクトリはテストコードで,ライブラリの構築には不要です.
ver.0.01: こちらは旧いものですが,v0_01 ディレクトリにソースファイルを置いています.zip アーカイブも.
だいたいどんな都合のプロジェクトでも組み込めるように配慮してみました.
使用する文字型は,bnf.hpp の char_t を typedef して下さい...といいつつ,char 以外の型でのテストはさっぱりやっていませんけど...
内部で使う文字列形,リスト型,連想配列型は,配布時点では STL の basic_string<>,list<>,map<> を使っています.プロジェクトの都合に合わせたクラスを用いるには,それぞれ bnf_string.hpp の String クラス,bnf_list.hpp の List<> クラステンプレート,bnf_map.hpp の Map<> クラステンプレートを実装しなおせば ok です.
また,自作スマートポインタを使用しています.bnf_ptr.hpp の Ptr<> クラステンプレートですが,これ,いかんせん実績がありません == 信用ゼロ.プロジェクトで boost が使えるならば,shared_ptr<> を使った方がいいに決まってます.
一応,テストコード test_ptr.cpp も用意してはいますけど.
こんなサイトで配布しているコードを使おうとするプロジェクトなら,十中八九,boost だって使えるはずですね.わらぃ
もうちょっと特殊な都合 ―― 例えば SymbianOS 向けに改造するには一筋縄ではいきません.コーディング規約とか全然アレですし (そんな事言うなら,フルブラウザでよく使われる NetFront のライブラリも,SybmianOS のコーディング規約とは違いますけど ← こんな事書いて大丈夫か自分).ここらへんについては需要があれば,メール頂ければなんか考えようかなと思ってます...
大して難しい事は考えず,bnf.cpp,bnf_cxt.cpp.bnf_rparse.cpp をコンパイルしてライブラリファイルを作ってしまいましょう.
ライブラリファイルを作ってしまった後は,多くのファイルは不要になります.ライブラリの使用に必要なファイルは
のみです.
ルールは,XML 1.1 の仕様書で使われている BNF(EBNF) とほぼ同じ書き方が出来るようにしました.ただし,演算子 "-" (除外するルール) の書き方についてはちょっと独自色が濃いです.というか,どういう扱いにするべきか迷っています.はっきりいって "-" 演算子はまだつかっちゃいけません.
TODO: このあたりの解説は,作成中です...
bnf::BnfParser を使ったコーディングこのサンプルは,BNF で BNF のルールが書かれてある bnf.bnf を読み込み,bnf.bnf を解析するものです.自分で自分を解析できてしまうので,なんだか楽しいですね.
ファイル bnf.bnf は,本ライブラリが BNF を解析するためのルールでもあります (いえ,ライブラリ稼動時にこのファイルが必要なわけではありませんが).なので,あなたが書いた BNF が正しいかどうかを検証するのに役立つかもしれません.
bnf.bnf の "rule" にあたる文字列を出力します.
rule => rules ::= blank? rule (blank rule)* blank?
rule => blank ::= (#x09 | #x20 | BR)+
rule => S ::= (#x09 | #x20 | BR_continue)+
rule => BR_continue ::= BR #x09
rule => BR ::= comment? (#x0D #x0A | #x0A | #x0D)
rule => comment ::= ";" [^#x0A#x0D]*
rule => rule ::= rulename S? "::=" S? expression
rule => rulename ::= id
rule => expression ::= term (S? "|" S? term)*
rule => term ::= factor (S factor)*
rule => factor ::= element (S? "-" S? element)?
rule => element ::= atom [+*?]?
rule => atom ::= rulename | literal | charclass | escaped | "(" S? expression S? ")"
rule => literal ::= d_literal | s_literal
rule => d_literal ::= '"' ([^"#x5c] | #x5c [#x20-#x7e])* '"'
rule => s_literal ::= "'" ([^'#x5c] | #x5c [#x20-#x7e])* "'"
rule => charclass ::= "[" "^"?
(alphanum alphanum_range?
| escaped escaped_range?
| [^\\\]\-]
| #x5c [\\\]\-]
)+
"]"
rule => id ::= alphabet (alphanum | "_")*
rule => alphabet ::= [a-zA-Z]
rule => number ::= [0-9]
rule => alphanum ::= alphabet | number
rule => alphanum_range ::= "-" alphanum
rule => escaped ::= "#x" hex hex (hex hex (hex hex)?)?
rule => escaped_range ::= "-" escaped
rule => hex ::= [0-9a-fA-F]
current matchers: 393
last matchers: 0
最後の "current matchers: 393" は BnfParser のインスタンスの破棄前に解析用オブジェクトがいくつ存在しているか,"last matchers: 0" は破棄後にどれだけ残ってしまっているかを出力しています.
test03.cpp は,本ライブラリの問題点を指摘します.t03_xml.bnf と t03_xml.xml はサイト「boost::spiritっちゃえ!」内の「04 簡易XMLパーザ」から無断で拝借しています.一言差し上げたいのですが,連絡方法がわからないもので...すみません...
前述の通り,本ライブラリは boost::spirit を目標にしており,あまり嬉しくない「癖」もそのまま引き継いでいます (むしろ,bnf パーザを実装するとしたらこうするしかないよなあ,といった感じですけども).なので,本ライブラリでも上記サイトに書かれてある問題がそのまま再現します.
boost::spirit の「セマンティックアクション」に相当する機能として,本ライブラリではコールバック関数を用意します.
boost::spirit では個別のルールに対して個別のセマンティックアクションを設定できますが,本ライブラリではルールがマッチする毎に同一のコールバック関数が呼ばれます.コールバック関数の引数にはマッチしたルール名が渡されるので,そのルール名で処理を分岐させて下さい.
問題を単純にするため,xmp1.cpp を用意しました.ルールはソースファイルの中にハードコードしています.
; ルールの例
rule ::= foo bar | foo baz ; これをトップルールとします
foo ::= "foo"
bar ::= "bar"
baz ::= "baz"
// コールバック関数の例
bool semantic_action(
void * option, char const * rule_name, int start, int len)
{
static string matched ="";
if (len < 0)
{
// メタ情報はこのサンプルでは無視
}
else if (strcmp(rule_name, "rule") == 0)
{
printf("マッチしたルール:%s\n", matched.c_str());
}
else
{
// マッチしたルールをメモしていく
matched += " ";
matched += rule_name;
}
return true; // 戻り値は,実は現バージョンでは使われていません.わらぃ
}
ここで,次のデータを入力して解析してみると...
foobar
次の出力結果が得られます.
マッチしたルール: foo bar
パーザはまずルール foo がマッチした事に気づきコールバック関数を呼び出します.次に bar がマッチした事に気づきコールバック,最後に rule 全体がマッチした事が分かったのでコールバック関数を呼び出しました.この挙動は問題ないと思います.では,同じルールで次の入力を解析してみると...
foobaz
出力結果は次の通り.
マッチしたルール: foo foo baz
ルール foo のマッチが 2 回も報告されてしまいます.パーザはまず選択演算子の前半,foo bar の並びがマッチするかを試し,foo がマッチしたのでコールバックしました.
rule ::= foo(この foo にマッチ) bar | foo baz
次に bar にマッチするかを試したものの,マッチせずにリタイア.次に rule の選択演算子の後半,foo baz を試し,最終的に上記のような出力結果が得られる事になります.
単純に考えれば,これはルールがよろしくありません.次のようなルールにすれば...
rule ::= foo (bar | baz)
foo ::= "foo"
bar ::= "bar"
baz ::= "baz"
ルールも若干短くなるし,問題も解決しています.でも,実際にもっと複雑なルールを設計していくうちに,このような解法ではどうにも解決されない場面が出てくるのです (実際,サンプルで使用しているルールも,こう簡単にはいかないものですもんね).
サンプル 2 での問題を,本ライブラリ的に解決したものが test03_2.cpp です.
コールバックされても本当にマジで最終的にマッチするかどうか分からない要素は.一度どこかに蓄えておき,上位ルールがマッチした事が分かった時に,はじめて先ほどの要素を「じゃあアンタも正しかったよ!」と認めてやる,といった発想です.
この発想自体は,前述のサイトさんが boost::spirit を用いて行っているものと同じです.しかし具体的な方法となると,boost::spirit と本ライブラリでは若干異なります.
前述の xmp1.cpp の例を用いて,どう問題を解決するかを解説します.ここでは BNF には手を加えず,コールバック関数の中身を改良します.
// 改良版コールバック関数の例
bool semantic_action2(
void * option, char const * rule_name, int start, int len)
{
static string foo_buf = "";
static string bar_or_baz_buf = "";
if (len < 0)
{
// メタ情報はこのサンプルでは無視
}
else if (strcmp(rule_name, "rule") == 0)
{
printf("マッチしたルール: %s\n", (foo_buf + " " + bar_or_baz_buf).c_str());
}
else if (strcmp(rule_name, "foo") == 0)
{
foo_buf = rule_name;
}
else if (strcmp(rule_name, "bar") == 0
|| strcmp(rule_name, "baz") == 0)
{
bar_or_baz_buf = rule_name;
}
return true;
}
じっくり見てみると,なんとなく,本来ルール側で行うべき改良をコールバック関数の中に持ってきた感じが分かると思います.
前述のサイトさん「04 簡易XMLパーザ」で最終的に解析している XML を,本ライブララリでも解析してみました.データはやはり無断借用です.すいません...
TODO: 以下,執筆途中です...
あんまし厳密には決めていませんが...