web-dev-qa-db-ja.com

コンパイル時にDSLのテキストを解析する方法は?

はい。そのとおり。次のような式を貼り付けられるようにしたい:

"a && b || c"

文字列としてソースコードに直接:

const std::string expression_text("a && b || c");

それを使用して遅延評価構造を作成します。

Expr expr(magical_function(expression_text));

その後、既知の値での置換を評価します。

evaluate(expr, a, b, c);

後でこの小さなDSLを拡張したいので、C++以外の構文を使用してもう少し複雑なことを行うので、式を単純な方法で単純にハードコーディングすることはできません。ユースケースは、C++構文に従うために毎回それを適応させる必要がなく、別の言語の異なる開発領域で使用される別のモジュールから同じロジックをコピーして貼り付けることができるということです。

誰かが私に少なくとも1つの式と2つのブール演算子の上記の単純な概念を実行する方法を教えてくれるなら、それは本当にありがたいです。

注:私が投稿した別の質問からのフィードバックのために、この質問を投稿しました: 高性能式テンプレートへのDSL入力を解析する方法 。ここで私は実際にわずかに異なる問題への回答を求めていましたが、コメントは、潜在的な回答は本当に文書化する価値があるので、投稿する価値があると私が考えたこの特定の質問を引き起こしました。

34
Benedict

免責事項:私はメタ解析については何も知りませんし、プロトについてはほとんど知りません。次のコードは、(主に試行錯誤を介して)変更する私の試みです この例 あなたが望むものと同様のことをするために。

コードは簡単にいくつかの部分に分けることができます。

1.文法


1.1トークン定義

typedef token < lit_c < 'a' > > arg1_token;
typedef token < lit_c < 'b' > > arg2_token;
typedef token < lit_c < 'c' > > arg3_token;
  • token<Parser>
    tokenは、Parserを使用して入力を解析し、その後すべての空白を消費(および破棄)するパーサーコンビネーターです。解析の結果はParserの結果です。
  • lit_c<char>
    lit_cは特定のcharと一致し、解析の結果は同じ文字になります。文法では、この結果はalwaysの使用によって上書きされます。
typedef token < keyword < _S ( "true" ), bool_<true> > > true_token;
typedef token < keyword < _S ( "false" ), bool_<false> > > false_token;
  • keyword<metaparse_string,result_type=undefined>
    キーワードは特定のmetaparse_stringに一致し(_S("true")metaparse::string<'t','r','u','e'>を返します。これは、メタ解析が内部で魔法を実行するために使用するものです)、解析の結果はresult_typeです。
typedef token < keyword < _S ( "&&" ) > > and_token;
typedef token < keyword < _S ( "||" ) > > or_token;
typedef token < lit_c < '!' > > not_token;

and_tokenおよびor_tokenの場合、結果は未定義であり、以下の文法では無視されます。


1.2文法の「ルール」

struct paren_exp;

最初にparen_expが前方宣言されます。

typedef one_of< 
        paren_exp, 
        transform<true_token, build_value>,
        transform<false_token, build_value>, 
        always<arg1_token, arg<0> >,
        always<arg2_token, arg<1> >, 
        always<arg3_token, arg<2> > 
    >
    value_exp;
  • one_of<Parsers...>
    one_ofは、入力をそのパラメーターの1つに一致させようとするパーサーコンビネーターです。結果は、一致する最初のパーサーが返すものです。
  • transform<Parser,SemanticAction>
    transformは、Parserに一致するパーサーコンビネーターです。結果タイプは、Parserによって変換されたSemanticActionの結果タイプです。
  • always<Parser,NewResultType>
    Parserと一致し、NewResultTypeを返します。

    同等の精神ルールは次のようになります。

    value_exp = paren_exp [ _val=_1 ]
        | true_token      [ _val=build_value(_1) ]
        | false_token     [ _val=build_value(_1) ]
        | argN_token      [ _val=phx::construct<arg<N>>() ];
    
typedef one_of< 
        transform<last_of<not_token, value_exp>, build_not>, 
        value_exp
    >
    not_exp;
  • last_of<Parsers...>
    last_ofは、Parsersのすべてに順番に一致し、その結果タイプは最後のパーサーの結果タイプです。

    同等の精神ルールは次のようになります。

    not_exp = (omit[not_token] >> value_exp) [ _val=build_not(_1) ] 
        | value_exp                          [ _val=_1 ];
    
typedef
foldl_start_with_parser<
        last_of<and_token, not_exp>,
        not_exp,
        build_and
    > and_exp; // and_exp = not_exp >> *(omit[and_token] >> not_exp);

typedef
foldl_start_with_parser<
    last_of<or_token, and_exp>,
    and_exp,
    build_or
> or_exp;     // or_exp = and_exp >> *(omit[or_token] >> and_exp);
  • foldl_start_with_parser<RepeatingParser,InitialParser,SemanticAction>
    このパーサーコンビネーターは、失敗するまでInitialParserと一致し、次にRepeatingParserと複数回一致します。結果タイプはmpl::fold<RepeatingParserSequence, InitialParserResult, SemanticAction>の結果です。ここで、RepeatingParserSequenceRepeatingParserのすべてのアプリケーションの結果タイプのシーケンスです。 RepeatingParserが成功しない場合、結果のタイプは単にInitialParserResultです。

    私は(xd)同等の精神ルールは次のようになると信じています:

    or_exp = and_exp[_a=_1] 
        >> *( omit[or_token] >> and_exp [ _val = build_or(_1,_a), _a = _val ]);  
    
struct paren_exp: middle_of < lit_c < '(' > , or_exp, lit_c < ')' > > {}; 
   // paren_exp = '(' >> or_exp >> ')';
  • middle_of<Parsers...>
    これはParsersのシーケンスと一致し、結果タイプは中央にあるパーサーの結果です。
typedef last_of<repeated<space>, or_exp> expression; 
   //expression = omit[*space] >> or_exp;
  • repeated<Parser>
    このパーサーコンビネーターはParserを複数回照合しようとします。結果は、パーサーのすべてのアプリケーションの結果タイプのシーケンスです。パーサーが最初の試行で失敗した場合、結果は空のシーケンスになります。このルールは、先頭の空白を削除するだけです。
typedef build_parser<entire_input<expression> > function_parser;

この行は、入力文字列を受け入れ、解析の結果を返すメタ関数を作成します。


2.式の構築

式の構築のウォークスルーの例を見てみましょう。これは2つのステップで行われます。最初に、文法はbuild_orbuild_andbuild_valuebuild_not、およびarg<N>に依存するツリーを構築します。その型を取得したら、proto_typetypedefを使用してプロト式を取得できます。

"a ||!b"

or_exprから始めます:

  • or_exprand_exprであるInitialParserを試してみます。
    • and_exprnot_exprであるInitialParserを試してみます。
      • not_expr:not_tokenが失敗するため、value_exprを試してみます。
        • value_expr:arg1_tokenは成功します。戻り値の型はarg<0>で、not_exprに戻ります。
      • not_expr:戻り値の型はこのステップでは変更されません。 and_exprに戻ります。
    • and_expr:RepeatingParserを試しましたが、失敗しました。 and_exprは成功し、その戻り値の型はそのInitialParserの戻り値の型です:arg<0>or_exprに戻ります。
    • or_expr:RepeatingParser、または_tokenの一致を試し、and_exprを試します。
    • and_expr:InitialParsernot_exprを試してみます。
      • not_expr:not_tokenは成功し、value_exprを試します。
        • value_expr:arg2_tokenは成功します。戻り値の型はarg<1>で、not_exprに戻ります。
      • not_expr:戻り値の型はbuild_notを使用した変換によって変更されます:build_not :: apply <arg <1 >>and_exprに戻ります。
    • and_expr:RepeatingParserを試しましたが、失敗しました。 and_exprは成功し、build_not :: apply <arg <1 >>を返します。 or_exprに戻ります。
  • or_expr:RepeatingParserが成功し、foldlpはbuild_not::apply< arg<1> >arg<0>でbuild_orを使用して、build_or::apply< build_not::apply< arg<1> >, arg<0> >を取得します。

このツリーを構築したら、そのproto_typeを取得します。

build_or::apply< build_not::apply< arg<1> >, arg<0> >::proto_type;
proto::logical_or< arg<0>::proto_type, build_not::apply< arg<1> >::proto_type >::type;
proto::logical_or< proto::terminal< placeholder<0> >::type, build_not::apply< arg<1> >::proto_type >::type;
proto::logical_or< proto::terminal< placeholder<0> >::type, proto::logical_not< arg<1>::proto_type >::type >::type;
proto::logical_or< proto::terminal< placeholder<0> >::type, proto::logical_not< proto::terminal< placeholder<1> >::type >::type >::type;

完全なサンプルコード( Wandboxで実行

#include <iostream>
#include <vector>

#include <boost/metaparse/repeated.hpp>
#include <boost/metaparse/sequence.hpp>
#include <boost/metaparse/lit_c.hpp>
#include <boost/metaparse/last_of.hpp>
#include <boost/metaparse/middle_of.hpp>
#include <boost/metaparse/space.hpp>
#include <boost/metaparse/foldl_start_with_parser.hpp>
#include <boost/metaparse/one_of.hpp>
#include <boost/metaparse/token.hpp>
#include <boost/metaparse/entire_input.hpp>
#include <boost/metaparse/string.hpp>
#include <boost/metaparse/transform.hpp>
#include <boost/metaparse/always.hpp>
#include <boost/metaparse/build_parser.hpp>
#include <boost/metaparse/keyword.hpp>

#include <boost/mpl/apply_wrap.hpp>
#include <boost/mpl/front.hpp>
#include <boost/mpl/back.hpp>
#include <boost/mpl/bool.hpp>

#include <boost/proto/proto.hpp>
#include <boost/fusion/include/at.hpp>
#include <boost/fusion/include/make_vector.hpp>

using boost::metaparse::sequence;
using boost::metaparse::lit_c;
using boost::metaparse::last_of;
using boost::metaparse::middle_of;
using boost::metaparse::space;
using boost::metaparse::repeated;
using boost::metaparse::build_parser;
using boost::metaparse::foldl_start_with_parser;
using boost::metaparse::one_of;
using boost::metaparse::token;
using boost::metaparse::entire_input;
using boost::metaparse::transform;
using boost::metaparse::always;
using boost::metaparse::keyword;

using boost::mpl::apply_wrap1;
using boost::mpl::front;
using boost::mpl::back;
using boost::mpl::bool_;


struct build_or
{
    typedef build_or type;

    template <class C, class State>
    struct apply
    {
        typedef apply type;
        typedef typename boost::proto::logical_or<typename State::proto_type, typename C::proto_type >::type proto_type;
    };
};

struct build_and
{
    typedef build_and type;

    template <class C, class State>
    struct apply
    {
        typedef apply type;
        typedef typename boost::proto::logical_and<typename State::proto_type, typename C::proto_type >::type proto_type;
    };
};



template<bool I>
struct value //helper struct that will be used during the evaluation in the proto context
{};

struct build_value
{
    typedef build_value type;

    template <class V>
    struct apply
    {
        typedef apply type;
        typedef typename boost::proto::terminal<value<V::type::value> >::type proto_type;
    };
};

struct build_not
{
    typedef build_not type;

    template <class V>
    struct apply
    {
        typedef apply type;
        typedef typename boost::proto::logical_not<typename V::proto_type >::type proto_type;
    };
};

template<int I>
struct placeholder //helper struct that will be used during the evaluation in the proto context
{};

template<int I>
struct arg
{
    typedef arg type;
    typedef typename boost::proto::terminal<placeholder<I> >::type proto_type;
};

#ifdef _S
#error _S already defined
#endif
#define _S BOOST_METAPARSE_STRING

typedef token < keyword < _S ( "&&" ) > > and_token;
typedef token < keyword < _S ( "||" ) > > or_token;
typedef token < lit_c < '!' > > not_token;

typedef token < keyword < _S ( "true" ), bool_<true> > > true_token;
typedef token < keyword < _S ( "false" ), bool_<false> > > false_token;

typedef token < lit_c < 'a' > > arg1_token;
typedef token < lit_c < 'b' > > arg2_token;
typedef token < lit_c < 'c' > > arg3_token;


struct paren_exp;

typedef
one_of< paren_exp, transform<true_token, build_value>, transform<false_token, build_value>, always<arg1_token, arg<0> >, always<arg2_token, arg<1> >, always<arg3_token, arg<2> > >
value_exp; //value_exp = paren_exp | true_token | false_token | arg1_token | arg2_token | arg3_token;

typedef
one_of< transform<last_of<not_token, value_exp>, build_not>, value_exp>
not_exp; //not_exp = (omit[not_token] >> value_exp) | value_exp;

typedef
foldl_start_with_parser <
last_of<and_token, not_exp>,
         not_exp,
         build_and
         >
         and_exp; // and_exp = not_exp >> *(and_token >> not_exp);

typedef
foldl_start_with_parser <
last_of<or_token, and_exp>,
         and_exp,
         build_or
         >
         or_exp; // or_exp = and_exp >> *(or_token >> and_exp);

struct paren_exp: middle_of < lit_c < '(' > , or_exp, lit_c < ')' > > {}; //paren_exp = lit('(') >> or_exp >> lit('(');

typedef last_of<repeated<space>, or_exp> expression; //expression = omit[*space] >> or_exp;

typedef build_parser<entire_input<expression> > function_parser;


template <typename Args>
struct calculator_context
        : boost::proto::callable_context< calculator_context<Args> const >
{
    calculator_context ( const Args& args ) : args_ ( args ) {}
    // Values to replace the placeholders
    const Args& args_;

    // Define the result type of the calculator.
    // (This makes the calculator_context "callable".)
    typedef bool result_type;

    // Handle the placeholders:
    template<int I>
    bool operator() ( boost::proto::tag::terminal, placeholder<I> ) const
    {
        return boost::fusion::at_c<I> ( args_ );
    }

    template<bool I>
    bool operator() ( boost::proto::tag::terminal, value<I> ) const
    {
        return I;
    }
};

template <typename Args>
calculator_context<Args> make_context ( const Args& args )
{
    return calculator_context<Args> ( args );
}

template <typename Expr, typename ... Args>
int evaluate ( const Expr& expr, const Args& ... args )
{
    return boost::proto::eval ( expr, make_context ( boost::fusion::make_vector ( args... ) ) );
}

#ifdef LAMBDA
#error LAMBDA already defined
#endif
#define LAMBDA(exp) apply_wrap1<function_parser, _S(exp)>::type::proto_type{}

int main()
{
    using std::cout;
    using std::endl;

    cout << evaluate ( LAMBDA ( "true&&false" ) ) << endl;
    cout << evaluate ( LAMBDA ( "true&&a" ), false ) << endl;
    cout << evaluate ( LAMBDA ( "true&&a" ), true ) << endl;
    cout << evaluate ( LAMBDA ( "a&&b" ), true, false ) << endl;
    cout << evaluate ( LAMBDA ( "a&&(b||c)" ), true, false, true ) << endl;
    cout << evaluate ( LAMBDA ( "!a&&(false||(b&&!c||false))" ), false, true, false ) << endl;
}

/*int main(int argc , char** argv)
{
    using std::cout;
    using std::endl;

    bool a=false, b=false, c=false;

    if(argc==4)
    {
        a=(argv[1][0]=='1');
        b=(argv[2][0]=='1');
        c=(argv[3][0]=='1');
    }

    LAMBDA("a && b || c") expr;

    cout << evaluate(expr, true, true, false) << endl;
    cout << evaluate(expr, a, b, c) << endl;

    return 0;
}*/
49
llonesmiz

長い間、コンパイル時の解析は、テンプレートメタプログラミングを使用することを意味していました。これは、ほとんどの初心者から中級のC++プログラマーにとってもラインノイズのようです。

ただし、C++ 11ではconstexprを取得し、C++ 14ではconstexprに対する多くの制限が削除されました。 C++ 17は、標準ライブラリのいくつかをconstexprにします。

高度な最新のC++を学習しようとしているときに、コンパイル時のHTMLパーサーを作成することにしました。そのアイデアは、高速なHTMLテンプレートエンジンを作成することでした。

コード全体はここにあります: https://github.com/rep-movsd/see-phit

これを機能させるときに学んだことを簡単に説明します。

動的データ構造の処理

Const char *を解析して多方向ツリーに変換する必要がありましたが、constexprランドでは動的割り当てはノーノーです。

ソリューション?子と兄弟を指すインデックスを持つノードの配列を使用します-基本的にはFORTRANでそれを行う方法です!

注意点は、ノードのリストは最初は固定サイズでなければならないということです。それを非常に大きく保つと、gccはコンパイルを大幅に遅くするように見えました。配列の終わりを超えてしまうと、コンパイラはエラーをスローします。私は完全にconstexprであるラッパーのような小さなstd :: arrayを書きました。

解析

ランタイム解析から作成するほとんどの標準コードは、コンパイル時に機能します。ループ、再帰、条件-すべてが完全に機能します。

1つの問題は、文字列をどのように表現するかということでした。上記のアプローチ(charの配列)を使用することは、非常にメモリを消費し、退屈な方法です。幸い、私の場合、必要なのは元のconst char *入力の部分文字列だけでした。そこで、関連する解析済みトークンの開始と終了へのポインターを保持するだけのクラスのような小さなconstexprstring_viewを作成しました。新しいリテラル文字列を作成するには、これらのビューをconst char *リテラルにするだけです。

エラー報告

Constexprコードのエラーを処理する基本的な方法は、constexprではない関数を呼び出すことです。コンパイラーは、エラー文字列を簡単に含む可能性のある問題のある行を停止して出力します。

ただし、もっと欲しかったのですが、パーサーに行と列も表示させたいと思いました。しばらく苦労して、無理だと思ってしまいました。しかし、私はそれに戻って、私が考えることができるすべての可能なことを試みました。最後に、gccに2つの数値とエラーの説明メッセージを出力させる方法を見つけました。基本的に、値がconstexprパーサーから取得される2つの整数パラメーター(rowとcol)を使用してテンプレートを作成する必要があります。

パフォーマンス

どのような種類のconstexprコードがコンパイラーの速度を低下させる傾向があるかについて、明確なパターンを見つけることができませんでしたが、デフォルトのパフォーマンスはそれほど粗末ではありません。 gccで約1.5秒で1000ノードのHTMLファイルを解析できます。

clangは少し高速です。


Githubリポジトリのwikiで、コードがどのように機能するかについて、より詳細な説明を書くつもりです。しばらくお待ちください。

3
rep_movsd

テンプレートやconstexprを使用してそのようなメタプログラミングを行うことは技術的には可能ですが、そのアプローチはお勧めしません。非常に複雑なコードが大量に発生することになります。デバッグが難しく、保守と拡張に費用がかかります。

代わりに、他の言語を使用して、式からC++コードを生成します。

Visual Studioを使用している場合、組み込みの優れた方法の1つはT4テキストテンプレートです。 詳細はこちら

それ以外の場合は、プラットフォームで利用可能な他の言語を使用してください。私はPythonで同様のことをしました。

0
Soonts