使用BOOST.SPIRIT.X3的RULE和ACTION进行复杂的语法制导过程

Preface

上一篇简述了boost.spirit.x3的基本使用方法。在四个简单的示例中,展示了如何使用x3组织构造一个语法产生式,与源码串匹配并生成一个综合属性。这些简单的示例中通过组合x3库中的基本语法单元,创建了一些复杂语法单元,也就是非终结符。但这些示例中的语法单元完成的事情还不够,它们只能配合phrase_parse函数告诉我们,与源码是否匹配;并且通过一个简单赋值操作返回一个综合属性。如果我想要在匹配成功的时候完成一些用户自定义的Action,如何完成这种需求?此外,仅使用基本语法单元的组合来实现一个比较复杂的DSL的时候,会让产生式变得非常复杂。这些问题都是鄙文将要解决的问题。

Semantic Action

Semantic Action,姑且翻译成语义作用,是x3提供的一个Unary Parser.  它可以包装一个语法单元和一个泛型仿函数。当Action包装语法的单元匹配成功的时候会调用这个泛型的仿函数。首先来看一个例子。

 1 #include <boost/config/warning_disable.hpp>
 2 #include <boost/spirit/home/x3.hpp>
 3
 4 #include <string>
 5 #include <iostream>
 6
 7 namespace x3 = boost::spirit::x3;
 8
 9 auto const print = [](auto& ctx) { std::cout << _attr(ctx) << std::endl; };
10
11 int main(void)
12 {
13     std::string source = "_123123_foo";
14     auto itr = source.cbegin();
15     auto end = source.cend();
16
17     std::string attr;
18     auto r = phrase_parse(itr, end
19         , (*x3::char_)[print]
20         , x3::ascii::space);
21
22     return 0;
23 }

x3::parser类模板重载了operator index. 当一个parser对象以一个泛型仿函数对象为实参,调用这个operator index,parser会返回一个对象。这个对象的类型就是x3::action根据parser和f的类型后实例化的类型。而x3::action内部在发现包装的parser匹配成功以后,就会调用这个泛型仿函数。

泛型仿函数(杜撰的),得满足一个条件。把条件写成一个普通的仿函数如下。

struct action_func
{
    template <typename Context>
    void operator() (Context& ctx) const
    {
        std::cout << _attr(ctx) << std::endl;
    }
};

泛型仿函数必须要有一个,具有一个模板参数的operator call. x3中是不关心这个operator call的返回值,因此可有可无。形参类型是一个依赖模板参数的类型,可以有const限定,但会失去对ctx对象的写权限。C++14标准中的Generic Lambda正好可以满足需求,这里使用lambda非常方便。在boost::spirit::qi中使用了boost::pheonix库来实现Semantic Action,但是局限性很大,使用用户自定义的仿函数也需要一些额外的代码来适配,x3的设计要合理多了。

我们详细讨论这个模板参数Context. Context的类型是由parser的综合属性决定的。它本质上是一个tuple. 而我们在operator call中使用的_attr()函数,可以类比为std::get<I>()函数,来获取上下文中传递的数据。_attr()获取的就是包装的子parser的综合属性。这样的函数还有三个,_val()获取rule的综合属性,_where()获取源码匹配串的当前迭代器位置,_pass()获取匹配的结果。_val()与_attr()的细节,鄙文在介绍rule的时候会展开。这里先用一个简单示例展示_pass()的使用方法。

//#include ......

auto const foo = [](auto& ctx)
{
    if (_attr(ctx) > 100 || _attr(ctx) < 0)
        _pass(ctx) = false;
};

int main(void)
{
    std::string source = "97";
    auto itr = source.cbegin();
    auto end = source.cend();

    int result;
    auto r = phrase_parse(itr, end
        , x3::int_[foo]
        , x3::ascii::space, result);

    return 0;
}

当attribute,在这里就是x3::int_的综合属性,大于100或者小于0的时候把匹配设置为失败。

Rule

x3::rule可以管理组合在一起的基本语法单元,成为一个更复杂的parser。通常在实践中,我们自己定义的DSL语法会很复杂,使用rule管理基本的parsers,就是使用自底向上的方法构建我们的语法产生式,使得层次更加的清晰。下面,依旧以C++ Identifier为示例。

 1 #include <boost/config/warning_disable.hpp>
 2 #include <boost/spirit/home/x3.hpp>
 3
 4 #include <string>
 5 #include <iostream>
 6
 7 namespace x3 = boost::spirit::x3;
 8
 9 namespace client
10 {
11     using x3::lexeme;
12     using x3::raw;
13     using x3::char_;
14     using x3::ascii::alnum;
15     using x3::ascii::alpha;
16
17     x3::rule<class identifier, std::string> const identifier = "identifier";
18     auto const identifier_def = raw[lexeme[(alpha | ‘_‘) >> *(alnum | ‘_‘)]];
19
20     template <typename Iterator, typename Context, typename Attribute>
21     inline bool parse_rule(
22         x3::rule<class identifier, std::string> rule,
23         Iterator& first, Iterator const& last,
24         Context const& ctx, Attribute& attr)
25     {
26         using boost::spirit::x3::unused;
27         static auto def = (identifier = identifier_def);
28         return def.parse(first, last, ctx, unused, attr);
29     }
30 }
31
32
33 int main(void)
34 {
35     std::string source = "_identifier";
36     auto itr = source.cbegin();
37     auto end = source.cend();
38
39     std::string str;
40     auto r = phrase_parse(itr, end, client::identifier, x3::ascii::space, str);
41
42     return 0;
43 }

x3::rule一般情况下要定义两个模板参数,第一个模板参数是一个ID,仅仅只是当做ID的参数,可以只是一个前向申明;第二个参数是这个rule的综合属性。第十七行代码中用一个字符串初始化这个rule,给这个rule一个名字,这个行为是可选的,rule保存的字符串可以在调试的时候起到一定的作用。同事,我们可以发现这个rule使用了const修饰符,因为我们的语法都是不变的。定义一个rule对象是申明一个语法,接下来我们要定义它,第十八行的代码就是定义它的方法。那么identifier与identifier_def是如何绑定到一起的?下面我们对x3的源码探究一下,浅尝辄止。

x3::phrase_parser作为整个解析的入口,这个函数会进入x3::phrase_parse_main这个函数。

// line 94 in <boost/spirit/home/x3/cord/parse.hpp>

template <typename ... >
inline bool phrase_parse_main(...)
{
      /* lines of code */
      bool r = as_parser(p).parse(first, last, skipper_ctx, unused, attr);
      /* we don`t care now! */
}

phrase_parse_main会从这个用Expression Tetmplate构造的静态语法产生式的根节点开始,调用parser的parse方法。实际上rule是从parser使用CRTP模式派生出来的,rule也是一个parser,pharse_parse_main就会进入到rule的parse方法中。

// line 74 in <boost/spirit/home/x3/nonterminal/rule.hpp>
template <typename ID, typename Attribute, bool force_attribute_>
struct rule : parser<rule<ID, Attribute>>
{
      /* lines of code */

      template<...>
      rule_defintion<...> operator=(RHS ....);

      template <typename Iterator, typename Context, typename Attribute_>
      bool parse(Iterator& first, Iterator const& last
          , Context const& context, unused_type, Attribute_& attr) const
      {
          return parse_rule(*this, first, last, context, attr);
      }
      /* we don`t care now! */
}

rule的parse成员模板方法调用了一个重要的函数,parse_rule函数。这个函数本来可以用一个通用的模板函数处理所有通用的情况,但是x3并没有这么做。x3的设计是在这里希望用户自己实现一个parse_rule的重载函数。注意,parse_rule是一个非限定性名称(Unqualified Name),因此parse_rule的重载方法因ADL查找的机制,可以定义用户的命名空间下面,rule其实也是定义在用户的命名空间下面。从这里就能够看到x3良好设计的思想。

这里有一个问题,为何不把rule管理的语法单元包含的rule对象中?通过阅读rule的源码可以发现,其成员仅有一个string成员变量。如果大家熟悉Expression Template的设计原理,就可以知道identifier_def是带有复杂的类型信息的。如果我们在rule中包含这个identifier_def,我们只有两种选择。增加一个模板参数<typename RHS>,使用CRTP的方式再一次继承;或者使用一个抽象类型把分派交给运行期。第一种方法,定然会让用户定义rule的时候非常不方便,第二种方法显然也已经违背了x3设计的初衷。然而x3在这里来了一记金蝉脱壳!且看示例代码的第22行。x3使用了rule_defintion来真正管理复杂的Expression Template,它以静态对象的方式在用户命名空间下重载的parse_rule的scope中存在。ADL查找配合重载决议,解析rule的代码进入用户自定义的parse_rule函数中,并由rule_defintion的parse方法继续递归向下!x3此处的设计方法,非常惊艳!!

大部分情况下,我们重载的parse_rule的方法就只有这几行代码了,而且是重复的。x3为了减少大家重载函数的工作量,定义了一个宏BOOST_SPIRIT_DEFINE来完成这项工作。但是有一个约定,rule_definition的对象名只能是rule的对象名跟一个"_def"的后缀。BOOST_SPIRIT_DEFINE还是一个可变参数的宏,可以让我们只使用一个宏就能够定义出我们需要的全部工作。

Problems in MSVC

上午刚刚从群里面的伙伴得知vs2015 update 1已经部分支持Expression SFINAE的特性了,x3的代码是不是可以不用修改就能使用。此时我正在下载安装update 1,稍后验证一下,会在后面的文章给出测试结果。

上一篇中也有一些纰漏。x3在vs2015中不能使用而修改的SFINAE的实现方法有点问题,并不能正确traits出ID是否有on_error和on_success函数,正确的修改做法请参考这一篇博客。我写的SFINAE为什么没有生效,小生还需研究一下,还请知道的大神们不吝赐教。

再提一提BOOST_SPIRIT_DEFINE宏,上一篇的文章中,我们有修改这个宏。官方的示例是允许如下的使用方式的。

1 BOOST_SPIRIT_DEFINE(identifier = identifier_def)
2 // or
3 BOOST_SPIRIT_DEFINE(identifier =
4       raw[lexeme[(alpha | ‘_‘) >> *(alnum | ‘_‘)]])

以上两种方式在vs2015中也不不能编译通过的,不过使用本文示例中的方法暂时能满足需求。我也会在update1中也验证一下,decltype的bug是不是修复了。

Ending

x3提供了Semantic Action给用户处理复杂的解析行为,还提供了rule给用户管理复杂的语法单元。但是x3并没有把复杂的问题交给用户,而是使用了精妙的设计来规避静态类型带来的问题。下一篇,小生会谈谈spirit的性能问题,如何高效使用spirit的注意事项;还会详细讲述x3的黑科技,自定义parser来扩展x3的功能。

时间: 2024-10-06 20:17:39

使用BOOST.SPIRIT.X3的RULE和ACTION进行复杂的语法制导过程的相关文章

boost spirit 解析字符串 (一)

项目中需要解析一段sql 语句,然后各种百度,看了一些文章,然后就头晕了,根本不知道他们在讲啥,感觉好像非常深奥的一样.感觉他们讲的太专业了,不能通俗易懂.所以把自己学习的记录下来,以便后面查看 1) 要解析一个字符串,首先你得要有一些规则吧,比如说字符串按照逗号分割,取出字符串中的特定字符串,或者把字符串中的整数取出来,这些我们都称为规则.在boost 库中有一个专门对应的模版类.翻译成中文名字也是"规则" boost::spirit::rule<>. 2)有了规则就可以

boost spirit 语法解析

使用spirit能很方便的解析自定义的语法规则,在他的文档中也说明了spirit与regex还有其他库的不同点.灵活,伸缩性好,可以用来搭建小的语法解析器也可以用来开发大型编译器等等. 定义语法规则之前首先要了解一下Extended Backus-Normal Form (EBNF) EBNF可以定义一下生成合法字符串的公式,例如: 例1: rule1 = "0" | "1" | "2" | "3". rule2 = &quo

ceph 源码安装 configure: error: &quot;Can&#39;t find boost spirit headers&quot;

问题:configure: error: "Can't find boost spirit headers" 解决: 下载boost_1_70_0, 链接地址:https://www.boost.org/users/history/version_1_70_0.html 编译: sudo tar -xf Boost_1_70_0.tar.bz2 cd Boost_1_70_0 sudo ./booststrap.sh --prefix=/usr/local/lib/boost ./b2

boost spirit parser for XQuery regexp

语法列表: /* from http://www.w3.org/TR/xmlschema-2/#regexs */ // [1] regExp ::= branch ( '|' branch )* re_reg_exp = re_branch [push_back( at_c<0>(_val), _1 )] % '|'; // [2] branch ::= piece* re_branch = *( re_piece ) [push_back( at_c<0>(_val), _1

Boost程序库完全开发指南——深入C++“准”标准库(第3版)

内容简介  · · · · · · Boost 是一个功能强大.构造精巧.跨平台.开源并且完全免费的C++程序库,有着“C++‘准’标准库”的美誉. Boost 由C++标准委员会部分成员所设立的Boost 社区开发并维护,使用了许多现代C++编程技术,内容涵盖字符串处理.正则表达式.容器与数据结构.并发编程.函数式编程.泛型编程.设计模式实现等许多领域,极大地丰富了C++的功能和表现力,能够使C++软件开发更加简捷.优雅.灵活和高效. <Boost程序库完全开发指南——深入C++“准”标准库(

《超越C++标准库:Boost库导引》:序

序(Foreword) C++社区正在发生着一些美妙的事情.尽管C++仍然是世界上使用最广泛的编程语言,它依旧在变得更加强大而且易用.不信么?容我慢慢道来. 当前版本的标准C++是在1998年最终确定下来的,它为传统的过程式编程(procedural programming)以及面向对象和泛型编程(generic programming)提供了强有力的支持.正如老的(1998年以前的)C++单枪匹马地把面向对象引入软件开发者日常工作可及的范围那样,C++98针对泛型编程做了同样的事情.1990年

Boost,Eigen,Flann—C++标准库预备役

Boost,Eigen,Flann—C++标准库预备役 第一预备役:Boost Boost库是为C++语言标准库提供扩展的一些C++程序库的总称. Boost库由Boost社区组织开发.维护.其目的是为C++程序员提供免费.同行审查的.可移植的程序库.Boost库可以与C++标准库完美共同工作,并且为其提供扩展功能.Boost库使用Boost License来授权使用,根据该协议,商业的非商业的使用都是允许并鼓励的. Boost社区建立的初衷之一就是为C++的标准化工作提供可供参考的实现,Boo

Visual Studio 2013 boost

E:\Visual Studio 2013\install\VC\bin\amd64>E:\IFC\boost_1_56_0_vs2013'E:\IFC\boost_1_56_0_vs2013' 不是内部或外部命令,也不是可运行的程序或批处理文件. E:\Visual Studio 2013\install\VC\bin\amd64>cd E:\IFC\boost_1_56_0_vs2013 E:\IFC\boost_1_56_0_vs2013>E:\IFC\boost_1_56_0_v

boost asio io_service学习笔记

构造函数 构造函数的主要动作就是调用CreateIoCompletionPort创建了一个初始iocp. Dispatch和post的区别 Post一定是PostQueuedCompletionStatus并且在GetQueuedCompletionStatus 之后执行. Dispatch会首先检查当前thread是不是io_service.run/runonce/poll/poll_once线程,如果是,则直接运行. poll和run的区别 两者代码几乎一样,都是首先检查是否有outstan