簡単なタスク-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)
数式を解析すると、次のことが発生する場合があります。
- 数字
- ブール定数(true、false)
- 文字列(引用符内)
- 識別子
- 関数呼び出し
- 操作
解析の結果を処理するために、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