如何在编译时解析DSL的文本?

时间:2013-07-22 08:56:15

标签: c++ 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个布尔运算符,那将非常感激。

注意:由于我发布的另一个问题的反馈,我发布了此问题:How to parse DSL input to high performance expression template。在这里,我实际上想要一个稍微不同的问题的答案,但是评论引发了我认为值得发布的这个具体问题,因为潜在的答案真的值得记录。

3 个答案:

答案 0 :(得分:46)

免责声明:我对metaparse一无所知,而且对proto一无所知。以下代码是我尝试(主要通过试验和错误)来修改this example以执行类似于您想要的操作。

代码可以很容易地分成几个部分:

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和结果 解析就是那个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'>这是metaparse内部使用的 做它的魔力),解析的结果是result_type
 
typedef token < keyword < _S ( "&&" ) > > and_token;
typedef token < keyword < _S ( "||" ) > > or_token;
typedef token < lit_c < '!' > > not_token;

and_tokenor_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是一个解析器组合器,它尝试将输入与其中一个参数进行匹配。结果是第一个匹配返回的解析器。
  • 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。构造表达式

让我们看一下表达式构建的示例演练。这是通过两个步骤完成的:首先,语法构造一个依赖于build_orbuild_andbuild_valuebuild_notarg<N>的树。获得该类型后,您可以使用proto_type typedef。

获取proto表达式

“a ||!b”

我们从or_expr开始:

  • or_expr:我们尝试使用and_expr的InitialParser。
    • and_expr:我们尝试使用not_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或or_token匹配,我们尝试and_expr
    • and_expr:我们尝试使用InitialParser not_expr
      • not_expr:not_token成功,我们尝试value_expr
        • value_expr:arg2_token成功。返回类型为 arg<1> ,我们将返回not_expr
      • not_expr:使用build_not进行转换修改返回类型: build_not :: apply&lt; ARG&LT 1为卤素; &GT; 即可。我们回到and_expr
    • and_expr:我们尝试使用RepeatingParser,但它失败了。 and_expr成功并返回 build_not :: apply&lt; ARG&LT 1为卤素; &GT; 即可。我们回到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;

完整示例代码(Running on 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;
}*/

答案 1 :(得分:1)

很长一段时间,编译时解析意味着使用模板元编程 - 这对于大多数初学者甚至是中级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减慢了大量的编译速度。 如果最终超过数组的末尾,编译器将抛出错误。 我写了一个小的std :: array就像包装器一样,完全是constexpr。

<强>解析

您为运行时解析编写的相当标准代码将在编译时工作! 循环,递归,条件 - 一切都很完美。

一个问题是 - 如何表示字符串?使用上面提到的方法 - 一个字符数组 - 是非常记忆饥饿,繁琐的做事方式。 幸运的是,就我而言,我所需要的仅仅是原始const char *输入的子串。 所以我写了一个小的constexpr string_view,类似于只保存指向相关解析标记的开头和结尾的指针。 创建新的文字字符串只是将这些视图转换为const char * literal。

报告错误

处理constexpr代码中的错误的基本方法是调用一个不是constexpr的函数 - 编译器停止并打印有问题的行,它可以很容易地包含错误字符串。

但是我想要更多 - 我希望解析器也显示行和列。我挣扎了一段时间,最后认为这是不可能的。 但我回到它并尝试了我能想到的所有可能的事情。最后我发现了一种使gcc打印2个数字和错误描述信息的方法。 它主要涉及创建一个带有两个整数参数(row和col)的模板,其值来自constexpr解析器。

<强>性能

我无法清楚地找到任何类型的constexpr代码往往会降低编译器速度的模式,但默认性能并不是太糟糕。我能够在gcc上大约1.5秒内解析一个1000节点的HTML文件。

铿锵有点快。

我打算在github repo的wiki中写一个代码如何工作的更详细的描述 - 请继续关注。

答案 2 :(得分:0)

虽然技术上可以使用模板或constexpr进行这种元编程,但我不推荐这种方法。你最终会得到大量非常复杂的代码。将难以调试,维护和扩展也很昂贵。

相反,使用任何其他语言从表达式生成C ++代码。

如果您使用的是Visual Studio,那么一种优秀的内置方式是T4文本模板。 Here’s more details

否则,请使用您平台上提供的任何其他语言,例如:我在Python中做了类似的事情。