Boost.Spiritの実用化

開発者はBoost.Spiritライブラリに対して完全に両極性の態度を持っていることに気付きました。彼らは本当にそれを好まないか、彼らのファンです。 もちろん、C ++で文法を記述することは趣味ではありません。 スピリットに出会ったとき、私もそのような恋人であることがわかりました。 私は、Spiritの助けを借りて、テキストを解析する日常的なタスクを非常に簡単に解決できることを示したいと思います。



簡単なタスク-2本の指のような



Spiritでは、小さなパーサーを「その場で」-C ++コードで書くのが非常に便利です。 たとえば、印刷するページの範囲を設定する「number-number」という形式の文字列を解析する必要がある場合はどうしますか? スピリット-1行:



  bool ok = parse(最初、最後、(uint_ >> L "-" >> uint _)、MinMax)&&(最初==最後); 




より複雑な...



さらに-パーサーを作成してパーサーを作成することは、もう少し難しいことです。 例として、Yandex.Bar APIに対して行ったミニ言語パーサーを検討します 。 タスクはこれでした。プラグインの読み込みを容易にするために、バーはXMLを使用しますが、それ自体は非常に冗長です。 ただし、XMLは、任意の形式を解析するよりもJavaScriptから読み込む方が簡単です(FireFoxの拡張機能はJ. Barを含むJSで記述されています)。



だから、私が必要としたのは、入力に通常の挿入記法を持つことでした



  Hello * Interval * 60 + xpath( "number(// hello [id = '"#id# "')"、World) 


XML形式の標準ASTを取得します。



  <追加>
     <mul>
         <value-of name = "Hello" />
         <value-of name = "Interval" />
         <value type = "number"> 60 </ f:値>
     </ f:mul>
     <xpath>
         <concat>
            番号(// hello [id = '<value-of name = "id" />')
         </ f:concat>
         <value-of name = "World" />
     </ f:xpath>
 </ f:追加> 


この場合、式自体に加えてプレーンXMLを記述する機会を残す必要がありました。 しかし、山括弧、等号、引用符、終了タグが豊富にあるため、私は落胆し、これらのエンティティの言語をクリアすることにしました。 この形式でXMLを書くことにしました。



 根
     child1:テキスト
         @ attribute1:テキスト
         @ attribute2 =数式
        孫
            孫孫
     child2 =式 


つまり、ネストはタブの数によって決まり、XMLノードの名前(要素または属性)になります。 その背後にあるのは、テキストまたは式:を定義する特定のシンボルです。 テキストは、「裸の」形式、式-ASTの形式で出力に送信する必要があります。



合計で、2つのパーサーがあります。1つは文字列を解析して、ノード名とテキストまたは数式を取得します。 2番目-数式を解析し、ASTを生成します。 古き良きstd :: find_ifを使用して、外側のタブの数の処理に費やしています。



文字列の解析。 セマンティックアクション-Boost.Bind経由



単純な行解析から始めます。 行は次のとおりです。



 タグ付け
タグ:テスト
タグ=数式
 =式
名前::(インスタンス|ウィジェット)(設定|変数)
名前:=式 


パーサーは非常に簡単です。



  bool parse_definition(string :: const_iterator&iter、string :: const_iterator end、mini_xml&root)
 {
     qi ::ルール<string :: const_iterator、string()、space_type> id、any_string、scope、type;
     id%= raw [lexeme [-char _( '@')>> alpha >> *(alnum | '_' | '-' |( ':' >> alnum))]];
     any_string%=語彙素[+ char_];
     scope%= raw [lit( "widget")| 点灯(「インスタンス」)];
     type%= raw [lit( "setting")| 点灯(「変数」)];

     phrase_parse(iter、end、
         (
             (id >> "::" >> scope >> type)[bind(&add_identifier、ref(root)、_1)] |
             (id >> ":=" >> any_string)[bind(&add_definition、ref(root)、_1)] |
             (id >> ':' >> any_string)[bind(&add_raw、ref(root)、_1)] |
             (id >> '=' >> any_string)[bind(&add_calculated、ref(root)、_1)] |
             ( '=' >> any_string)[bind(&add_expression、ref(root)、_1)] |
             id [バインド(&add_subnode、ref(root)、_ 1)]
         )、
        スペース)&&(iter == end);
 } 


parse()の代わりにphrase_parse()を使用すると、式内の空白(スペース、タブなど)の処理を白にシフトできました。 これにより、「tag:text」と「tag:text」の両方を記述できます。 そして、私のコードは明らかにスペースの処理から解放されています-phrase_parse()はすべてを行います。 この動作を無効にする場合はlexeme []のみを使用し、スペースをカットせずにソーステキストを取得する場合はraw []のみを使用できます。



ところで、Spiritのルールの構文は次のとおりです。



 ルール[semantic_action] 


つまり、各ルールの後に、ルールが「機能した」場合に実行されるアクションを角括弧で指定できます。



私の場合、各タイプの行には独自の動作があり、最初に後続のコードを簡素化するために、id、any_stringなどの個別のルールを導入しました。 文字列が特定のルールに一致したときに呼び出されるコードは、boost :: bindを使用して作成されたラムダ関数によって指定されます。 バインド構文は非常に簡単です。



  boost :: bind(関数、引数、引数、...) 


引数として、ラムダ関数の引数を挿入する場所を示す特別なプレースホルダー(_1、_2、...の形式)を指定できます。 各パーサーの出力で1つの値が取得され、それを関数の引数として渡します。 たとえば、パーサー



  id >> '=' >> any_string 


出力の数行(boost :: fusion :: vector <string、string>の形式)を生成し、そのようなインターフェイスを持つadd_calculated関数の2番目のパラメーターとして渡します。



  void add_calculated(mini_xml&root、fusion :: vector <string、string> const&); 


この関数に渡す必要がある最初のパラメーターはルートリンクなので、boost :: bind呼び出しは次のようになります。



 バインド(&add_calculated、ref(root)、_ 1) 


ルールとセマンティックアクションの要約:



  (id >> '=' >> any_string)[bind(&add_calculated、ref(root)、_1)] 




数式の解析。 セマンティックアクション-Boost.Phoenix経由



解析する必要がある関数の種類を思い出させてください:



  Hello * Interval * 60 + xpath( "number(// hello [id = '"#id# "')"、World) 


数式を解析すると、次のことが発生する場合があります。



解析の結果を処理するために、1つの大きなファンクターを作成し、すべてのセマンティックアクションでBooost.Phoenixで使用します 。 すべてのファンクターと同様に、アクションは名前ではなく、パラメーターの数とタイプが異なります。



 構造体コンパイラ
 {
     //同じ引数を持つ他の関数と区別するためにラベルが必要です
    構造体識別子{};  //ラベル「識別子」
     struct function {};  //ラベル「関数」
    構造体パラメーター{};  //ラベル「パラメータ」
    構造体の割り当て{};  //ラベル「割り当て」

     void演算子()(mini_xml&node、std :: string const&id、identifier)const;  // id
     void operator()(mini_xml&node、std :: string const&id、function)const;  //関数
     void operator()(mini_xml&node、std :: string const&id、parameter)const;  //関数パラメーター
     void演算子()(mini_xml&node、std :: string const&id、assignment)const;  //割り当て
     void演算子()(mini_xml&node、std :: string const&value、char const * type)const;  // <値>
     void演算子()(mini_xmlとノード、mini_xml constとサブノード)const;
     void演算子()(mini_xml&node、mini_xml const&subnode、std :: string const&id、bool allow_join = false)const;
 }; 


私の文法の中に、クラスメンバーを追加しました-私の同じファンクター:



 テンプレート<typename Iterator>
 struct expression_grammar:grammar <イテレーター、mini_xml()、space_type>
 {
     expression_grammar():expression_grammar :: base_type(expressions)
     {
         式= ...;
     }

    ルール<Iterator、mini_xml()、space_type>式、...;
     boost :: phoenix :: function <コンパイラ> op;
 } 


PS。 タイプmini_xmlは、生成されたXMLです。

識別子、文字列、数値、およびブール定数を解析するためのルールは非常に簡単です。



  id%= raw [lexeme [alpha >> *(alnum | '_' |( '-' >> alnum))]];
 quoted_string%= lexeme ['"' >> *(char_-'"')>> '"'];
 numeric_value%= raw [lexeme [-(char _( '+')| char _( '-'))>> + digit >>-(char_( '。')>> + digit)]];
 boolean_value%= raw [lit( "true")| 点灯( "false")]; 


これらのすべてのルールは、文字列(たとえば、識別子の名前)を出力します。 「rule%= parser」構成の%=演算子を使用すると、パーサーによって生成された値をパーサーの出力に直接渡すことができます。 さらに、それらの結果を他のルールで直接使用できます。



  string = quoted_string [op(_val、_1、 "string")];
数値= numeric_value [op(_val、_1、 "number")];
 boolean = boolean_value [op(_val、_1、 "bool")];
 empty = lit( "empty")[op(_val、std :: string()、 "empty")];
 identifier = id [op(_val、_1、compiler :: identifier())]; 


ご覧のとおり、ここではいずれの場合も、たとえばquoted_stringなどのパーサーが呼び出され、その値がopファンクターの呼び出しに使用されます。 最初の場合(文字列ルール)、ファンクターはファンクターの入力を入力します:最初の引数は生成される値(私の場合、XMLツリー要素)、2番目はquoted_stringパーサーの結果、3番目は用語「文字列」です。 そしてすでに、ファンクターはXMLツリーで必要なすべてのアクションを実行します。

関数の定義はそれほど複雑ではありません-XMLを生成しているからです。 関数のパラメーターは、「children」として関数のxml-nodeに「アタッチ」するのは非常に簡単です。



 関数=
     id [op(_val、_1、コンパイラ::関数())]
     >> '('
     >>-(パラメーター[op(_val、_1)]% '、')
     >> ')'; 


式「パラメータ[op(_val、_1)]」は、子を関数にアタッチするだけです。親はopファンクタに渡されます(「op(_val、_1、compiler :: function())」で設定された関数ノード) 「子」(パラメータパーサーを生成したパラメータノード)。



合計で、2項演算と3項演算(* / +-?などの2つと3つの引数を持つ演算)を除いて、次のルールが取得されます。



 係数=
     関数[_val = _1]
      | ブール値[_val = _1]
      | 空[_val = _1]
      | 識別子[_val = _1]
      | 文字列[_val = _1]
      | 番号[_val = _1]
      |  ( '(' >>式[_val = _1] >> ')')
      |  (lit( '!')[op(_val、 "not"、compiler :: function())] >> factor [op(_val、_1)])
      |  (lit( '-')[op(_val、 "neg"、compiler :: function())] >> factor [op(_val、_1)])
      |  ( '+' >>係数[_val = _1])
      ; 


操作を処理するとき、優先順位を忘れてはなりません。 ある操作の定義を別の操作の定義に「埋め込む」ことにより、簡単に実装できます。



 追加=
    乗算[_val = _1]
     >> *(( '+' >>乗算[op(_val、_1、 "add"、true)]))
         |  ( '-' >>乗算[op(_val、_1、 "sub"、true)])
         )
     ;

乗算=
    係数[_val = _1]
     >> *(( '*' >> factor [op(_val、_1、 "mul"、true)]))
         |  ( '/' >> factor [op(_val、_1、 "div"、true)]))
         )
     ; 


この場合、乗算と除算の関数は、加算と減算よりも早く解凍されます。これは、乗算が加算に「埋め込まれる」ためです。 これは、追加するために、最初に内部の乗算を含むすべての内部ルールを解析する必要があるために発生します。 実際には、必要に応じて。



すべてをまとめる



すべてのソースコードは、 http//download.yandex.ru/bar/tools/easierxb-src.zipから入手できます(アーカイブ内には、WindowsおよびMacOSでビルドするためのプロジェクトがあります)。

入力ファイルの例: http : //download.yandex.ru/bar/tools/easierxb-example.zip



All Articles