Monthly Archives: 7月 2008

2008-07-22

Boost.Spiritで構文木を作る……?

困った。pt_parseを使おうが、ast_parseを使おうが、マッチ文字長が0のノードが消えてしまう。

文法:
nums = *int_p
alps = *alpha_p
nums_alps = nums >> alps

入力:
"ABC"

出力(pt_parseの戻り値のtrees。idはルール名に置き換え):
nums_alps
 alps
  alps A
  alps B
  alps C

入力:
"0ABC"

出力:
nums_alps
 nums
  nums 0
 alps
  alps A
  alps B
  alps C

入力:
""

出力:
(ID=0な空のノード一つ)

構文木のnumsに対応する部分を処理するとき、numsが消えているかどうか、もっと言えばnums_alps自体が消えているかどうかをいちいちチェックしないといけないわけですか? 構文解析後なのに? 意味が分かりません。

トレースしてみたら、boost_1_35_0/boost/spirit/tree/common.hppのconcat_match()内でlength()==0故にマッチ情報が捨てられてしまうみたい。えー、ちゃんと連結してくれよー。何か意図があるのかな。

独自のポリシークラスで処理すれば何とかなるんだろうけど、メンテナンスのことを考えるとそこまではしたくないので、この方向はあきらめることにする。

なので、次はクロージャでなんとかする方向で考えてみる。構文定義部分が汚くなっちゃうけどしかたない。

2008-07-20

Boost.Spiritでオプションファイル解析

Boost.Spiritでオプションファイル解析。

アプリケーションに渡すちょっとしたオプション指定ファイルを作ろうと思ったので、boost::spiritで構文解析器を書いてみることにしました。

まずは文法を決めなければなりません。

OptionA 123
OptionB "ABCDE"

みたいな単純なkeyとvalueの組み合わせでも良いんですが、もう少し複雑な構造を表現しようとすると、少し面倒なことになってしまいます。

OptionC_Item0 "AAAA"
OptionC_Item1 "BBBB"
OptionC_Item2 "CCCC"

OptionD_Item0_Name "AAAA"
OptionD_Item0_Type 0
OptionD_Item1_Name "BBBB"
OptionD_Item1_Type 0
OptionD_Item2_Name "CCCC"
OptionD_Item2_Type 1
OptionD_Item2_Value 123

なので、値の部分にタプルとマップを指定できるようにしようと思います。
タプルは値の有限の列、マップは識別子から値への対応関係とします。
タプルとマップでOptionCとOptionDを書き直すと次のようになります。

OptionC_Items ["AAAA" "BBBB" "CCCC"]

OptionD_Items [
  {Name "AAAA" Type 0}
  {Name "BBBB" Type 0}
  {Name "CCCC" Type 1 Value 123}
]

よく見ると、このファイル全体も、{ }で囲まれては居ませんが一種のマップのようなものです。このあたりはまとめられそうです。

文法をまとめます。

option-file ::= map-body オプションファイルはマップの{ }内の中身と同じです。
map ::= '{' map-body '}' マップは{ }とその中身です。
map-body ::= map-element* マップの中身は0個以上のマップ要素です。
map-element ::= map-key map-value マップ要素はマップキーとマップ値の対です。
map-key ::= ident マップキーは識別子(Identifier)です。
map-value ::= value マップ値は任意の値です。
tuple ::= '[' tuple-body ']' タプルは[ ]とその中身です。
tuple-body ::= tuple-element* タプルの中身は0個以上のタプル要素です。
tuple-element ::= value タプル要素は任意の値です。
value ::= integer | string | map | tuple 値は整数、文字列、マップ、タプルのいずれかです。
integer ::= [1-9][0-9]* 整数は1から9で始まる数字です。
string ::= '"' [^"n]* '"' 文字列は"で囲まれた文字の列です。
ident ::= [a-zA-Z_][a-zA-Z0-9_]* 識別子はアルファベットやアンダーバーで始まる文字列です。
  • ※字句間は自由に空白、タブ、改行が許される。
  • ※字句間の';'は行末まではコメント。

以上で文法がきまりましたので、これをboost::spiritで解析しようと思います。

struct OptionFileGrammer : public boost::spirit::grammar<OptionFileGrammer>
{
  template<typename ScannerT>
  struct definition
  {
    typedef boost::spirit::rule<ScannerT> rule_t;

    rule_t opt_file;
    rule_t map, map_body, map_element, map_value, map_key;
    rule_t tuple, tuple_body, tuple_element;
    rule_t value;
    rule_t integer_l;
    rule_t string_l;
    rule_t ident;

    definition(const OptionFileGrammer &self)
    {
      using namespace boost::spirit;

      opt_file = map_body;

      map = ch_p('{') >> map_body >> ch_p('}');
      map_body = *map_element;
      map_element = map_key >> map_value;
      map_value = value;
      map_key = ident;

      tuple = ch_p('[') >> tuple_body >> ch_p(']');
      tuple_body = *tuple_element;
      tuple_element = value;

      value = integer_l | string_l | map | tuple;

      integer_l  = int_p;
      string_l   = lexeme_d[ confix_p('"', *c_escape_ch_p, '"') ];

      ident = lexeme_d[ (alpha_p | ch_p('_')) >> *(alnum_p | ch_p('_'));

    }

    const rule_t &start() const { return opt_file;}
  };
};


int main(int argc, char **argv)
{
  std::string s;

  // sへ読み込み。


  OptionFileGrammer optGr;
  parse_info<std::string::const_iterator> info = parse(s.begin(), s.end(), optGr >> end_p, space_p | comment_p(';'));
  if(!info.full){
    std::cout << "Parse Error." << std::endl;
    return -1;
  }
}
        

definitionの中はわりと文法定義そのものです。
整数値はint_pそのままを、文字列はconfix_pc_escape_ch_pというおあつらえ向きなものがありました。元の文法では抜けていましたが、符号やエスケープシーケンスにも対応しましょう。

スキップパーサーのおかげで、空白スキップ、コメントスキップも簡潔に書けます。
ここではparseの引数にspace_p | comment_p(';')を指定することで実現しています。

しかし、スキップパーサーのおかげで、string_lやidentの定義でハマりました。例えばA128は有効な識別子ですが、スキップパーサーのおかげでA12(空白)8もA128と同等に扱われてしまいます。これを回避するには、lexeme_d[ ]で囲って、スキップパーサーを一時的に無効にすればいいようです。

終端前の空白文字が残ってしまいフルマッチしない現象にも遭遇しました。これはここのNovember 28, 2007に書いてありますが、Boost1.34以降で変わった挙動らしく、end_pを最後に置けばいいようです。

以上でとりあえず構文解析ができるようになりました。

ただ、これだけでは入力テキストが文法にマッチするか否かが分かるだけなので、本来の目的は達成できていません。
このオプションの指定値は何?といったアプリケーション側からの問い合わせに答えられるよう、何らかのデータ構造を構築する必要があります。

そのあたりはまだよく調べていないので次の機会に。セマンティックアクションでゴリゴリやればいいのか、それとも直接的に木を構築する方法が用意されているのか。

2008-07-18

Firefox3のライブマークを普通のブックマークへ変換する

Firefox3にしてからというもの、Firefox3の起動直後が死ぬほど重くて困っていたのだけど、ライブマークが多いともたつくという情報を見たのでSage Feeds以下のライブマークを通常のブックマークへ変換したら軽くなった。

何か変換する拡張でも無いかと思ったのだけど見つからなかったので、いったんブックマークのバックアップで.jsonファイルを出力し、Emacs上で変換し、再びブックマークの復元で戻す方法をとった。.jsonファイルのSage Feedsの子孫部分だけをNarrowingして、次のような置換を実行。

(defun firefox3-strip-livemark ()
  (interactive)
  (beginning-of-buffer)
  (replace-regexp ""livemark":1," "")
  (beginning-of-buffer)
  (replace-regexp "{"name":"placesInternal/READ_ONLY"[^}]+}," "")
  (beginning-of-buffer)
  (replace-regexp "{"name":"livemark/feedURI",[^}]+"value":"\(http://[^"]+\)"},\([^]]+}],\)"type":"text/x-moz-place-container","children":\[\]" "\2"type":"text/x-moz-place","uri":"\1"")
  (beginning-of-buffer)
  (replace-regexp "{"name":"livemark/[^}]+},?" "")
)
        

一応手元のは全部置換できたみたい。タイトルに]とか"とか}とかが入っているとうまくいかないかも。

2008-07-12

GNU gettextもどき

GNU gettextのようなプログラム中の文字列を置き換えるライブラリを作った。いちいち全てのメッセージに整数の識別子を付けるなんてやってられないので、この英語の文字列を識別子にする方法は現実的だと思う。

やれやれ、これでようやくソースの中に文章を書く気持ち悪さから解放される。