提高Spirit语义动作参数

51
在这篇关于Boost Spirit语义动作的文章中提到:实际上还有两个参数被传递:解析器上下文和一个布尔类型的“hit”参数的引用。只有当语义动作附加到规则的右侧时,解析器上下文才有意义。我们很快就会看到更多相关信息。布尔值可以在语义动作内设置为false,这样会回溯地使匹配无效,从而使解析器失败。
尽管如此,我一直在尝试找到一个使用其他参数(解析器上下文和hit布尔值)的函数对象作为语义动作的例子,但我没有找到任何例子。 我想看到使用常规函数或者函数对象的例子,因为我对Phoenix不太理解。
1个回答

66

这是一个非常好的问题(也是个棘手的问题),因为它涉及到qi和phoenix之间的接口。我还没有看到过例子,所以我会在这个方向上扩展一下文章。

正如你所说,语义动作的函数可以有三个参数:

  1. 匹配属性 - 在文章中有介绍
  2. 上下文 - 包含了qi-phoenix之间的接口
  3. 匹配标志 - 操纵匹配状态

匹配标志

正如文章所述,除非表达式是规则的一部分,否则第二个参数是没有意义的,所以让我们从第三个开始。虽然第二个参数的占位符仍然需要而且使用 boost::fusion::unused_type。所以,一个修改过的函数来使用第三个参数是:

#include <boost/spirit/include/qi.hpp>
#include <string>
#include <iostream>

void f(int attribute, const boost::fusion::unused_type& it, bool& mFlag){
    //output parameters
    std::cout << "matched integer: '" << attribute << "'" << std::endl
              << "match flag: " << mFlag << std::endl;

    //fiddle with match flag
    mFlag = false;
}

namespace qi = boost::spirit::qi;

int main(void){
   std::string input("1234 6543");
   std::string::const_iterator begin = input.begin(), end = input.end();

   bool returnVal = qi::phrase_parse(begin, end, qi::int_[f], qi::space);

   std::cout << "return: " << returnVal << std::endl;
   return 0;
}

输出结果如下:

匹配到的整数: '1234'
匹配标志: 1
返回值: 0

这个例子所做的就是将匹配转换为不匹配,这反映在解析器输出中。根据hkaiser的说法,在boost 1.44及以上版本中将匹配标志设置为false将导致正常方式下匹配失败。如果定义了备选项,解析器将回溯并尝试像预期的那样将它们匹配。然而,在boost<=1.43中,Spirit bug阻止了回溯,这会导致奇怪的行为。要查看此内容,请添加phoenix include boost/spirit/include/phoenix.hpp并将表达式更改为:

qi::int_[f] | qi::digit[std::cout << qi::_1 << "\n"]

您会期望,当qi::int解析器失败时,备选的qi::digit将匹配输入的开头处的“1”,但输出结果是:
matched integer: '1234'
match flag: 1
6
return: 1
其中的6是输入中第二个整数的第一个数字,这表明备选项使用了skipper且没有回溯。请注意,基于备选项,匹配被认为是成功的。
一旦boost 1.44发布,匹配标志将有助于应用可能在解析器序列中难以表达的匹配条件。请注意,可以使用_pass占位符在phoenix表达式中操作匹配标志。 上下文参数 更有趣的参数是第二个参数,它包含了qi-phoenix接口,或者在qi术语中,语义动作的上下文。为了说明这一点,首先看一条规则:
rule<Iterator, Attribute(Arg1,Arg2,...), qi::locals<Loc1,Loc2,...>, Skipper>

上下文参数包含Attribute、Arg1...ArgN和qi::locals模板参数,这些参数被包装在boost::spirit::context模板类型中。与函数参数不同,函数参数属性是解析后的值,而此属性是规则本身的值。语义动作必须将前者映射到后者。以下是可能的上下文类型示例(使用Phoenix表达式等效方式):

using namespace boost;
spirit::context<              //context template
    fusion::cons<             
        int&,                 //return int attribute (phoenix: _val)
        fusion::cons<
            char&,            //char argument1       (phoenix: _r1)
            fusion::cons<
                float&,       //float argument2      (phoenix: _r2) 
                fusion::nil   //end of cons list
            >,
        >,
    >,
    fusion::vector2<          //locals container
        char,                 //char local           (phoenix: _a)
        unsigned int          //unsigned int local   (phoenix: _b)
    > 
>

注意,返回属性和参数列表采用LISP风格的列表形式(即cons list)。要在函数内访问这些变量,请使用fusion :: at < >()访问context struct模板的attribute或locals成员。例如,对于上下文变量con。
//assign return attribute
fusion::at_c<0>(con.attributes) = 1;

//get the second rule argument
float arg2 = fusion::at_c<2>(con.attributes);

//assign the first local
fusion::at_c<1>(con.locals) = 42;

要修改文章示例以使用第二个参数,请更改函数定义和phrase_parse调用:

...
typedef 
    boost::spirit::context<
        boost::fusion::cons<int&, boost::fusion::nil>, 
        boost::fusion::vector0<> 
    > f_context;
void f(int attribute, const f_context& con, bool& mFlag){
   std::cout << "matched integer: '" << attribute << "'" << std::endl
             << "match flag: " << mFlag << std::endl;

   //assign output attribute from parsed value    
   boost::fusion::at_c<0>(con.attributes) = attribute;
}
...
int matchedInt;
qi::rule<std::string::const_iterator,int(void),ascii::space_type> 
    intRule = qi::int_[f];
qi::phrase_parse(begin, end, intRule, ascii::space, matchedInt);
std::cout << "matched: " << matchedInt << std::endl;
....

这是一个非常简单的例子,只是将解析后的值映射到输出属性值,但扩展应该相当明显。只需使上下文结构体模板参数与规则输出、输入和本地类型匹配即可。请注意,使用自动规则时,可以使用%=而不是=来定义规则,从而实现解析类型/值与输出类型/值之间的直接匹配:

qi::rule<std::string::const_iterator,int(void),ascii::space_type> 
    intRule %= qi::int_;

IMHO,为每个操作编写一个函数会比使用简洁且易读的Phoenix表达式等效方法更加繁琐。虽然我同情巫术观点,但一旦您使用Phoenix一段时间后,语义和语法并不是非常困难。
编辑:使用Phoenix访问规则上下文
当解析器是规则的一部分时,上下文变量才被定义。将解析器视为消耗输入的任何表达式,在其中规则将解析器值(qi::_1)转换为规则值(qi::_val)。区别通常是非平凡的,例如当qi::val具有需要从POD解析值构造的Class类型时。以下是一个简单的例子。
假设我们的输入的一部分是三个CSV整数序列(x1、x2、x3),我们只关心这三个整数的算术函数(f = x0 +(x1 + x2)* x3),其中x0是在其他地方获得的值。一种选择是读取整数并计算函数,或者使用phoenix同时完成两个任务。
对于此示例,请使用具有输出属性(函数值)、输入(x0)和本地变量(用于在规则中传递信息)的一个规则。以下是完整的示例。
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix.hpp>
#include <string>
#include <iostream>

namespace qi = boost::spirit::qi;
namespace ascii = boost::spirit::ascii;

int main(void){
   std::string input("1234, 6543, 42");
   std::string::const_iterator begin = input.begin(), end = input.end();

   qi::rule<
      std::string::const_iterator,
      int(int),                    //output (_val) and input (_r1)
      qi::locals<int>,             //local int (_a)
      ascii::space_type
   >
      intRule =
            qi::int_[qi::_a = qi::_1]             //local = x1
         >> ","
         >> qi::int_[qi::_a += qi::_1]            //local = x1 + x2
         >> ","
         >> qi::int_
            [
               qi::_val = qi::_a*qi::_1 + qi::_r1 //output = local*x3 + x0
            ];

   int ruleValue, x0 = 10;
   qi::phrase_parse(begin, end, intRule(x0), ascii::space, ruleValue);
   std::cout << "rule value: " << ruleValue << std::endl;
   return 0;
}

另一种方法是将所有整数解析为向量,然后使用单个语义动作(下面的 % 是列表运算符,并使用 phoenix::at 访问向量的元素)来评估函数:

namespace ph = boost::phoenix;
...
    qi::rule<
        std::string::const_iterator,
        int(int),
        ascii::space_type
    >
    intRule =
        (qi::int_ % ",")
        [
            qi::_val = (ph::at(qi::_1,0) + ph::at(qi::_1,1))
                      * ph::at(qi::_1,2) + qi::_r1
        ];
....

对于上述情况,如果输入不正确(只有两个整数而不是三个),在运行时可能会发生错误。因此最好明确指定解析值的数量,以便于针对错误的输入进行解析失败。下面使用_1_2_3来分别引用第一个、第二个和第三个匹配值:
(qi::int_ >> "," >> qi::int_ >> "," >> qi::int_)
[
    qi::_val = (qi::_1 + qi::_2) * qi::_3 + qi::_r1
];

这只是一个虚构的例子,但应该能让你理解。我发现Phoenix语义动作在直接从输入构造复杂对象方面非常有帮助;这是可能的,因为在语义动作中可以调用构造函数和成员函数。


3
谢谢你的解释。你介意我“借用”一下并在Spirit网站上重新发布它吗(当然会给予应有的信用)?你提到的回溯问题是Spirit中的一个bug。在第一个选择失败后,正确的行为应该是第二个选择从与第一个相同的输入位置重新开始。我会尽力修复这个问题。此外,请不要在语义动作中使用Phoenix占位符。请始终使用相应的Spirit占位符,即qi::_1。 - hkaiser
1
好的,回溯问题现在已经解决了,并且将在下一个版本(Boost V1.44)中得到修复。 - hkaiser
1
@hkaiser 很高兴你喜欢它,如果你想要的话,请随意重用。我本来想在邮件列表上问一下关于回溯问题的事情,感谢你处理了它。有一个关于占位符的问题:phoenix::_1qi::_1都被定义为const phoenix::actor<argument<0>>,这会改变吗? - academicRobot
1
谢谢!我会尽快写一篇文章,将你的东西融入其中。你说得对,phoenix::_1qi::_1都被定义为const phoenix::actor<argument<0>>,但它们基于不同的argument<>类型。phoenix::_1基于phoenix::argument<0>,而qi::_1基于spirit::argument<0>。在你的情况下,这只是巧合而已。 - hkaiser
4
@lurscher,你提供的例子是一个解析器(我想你的意思是 >> 而不是 <<),但它不是一个规则。因此它没有上下文(语义操作的上下文参数是未使用的类型)。你将它分配给一个规则:rule4 %= rule1 >> lit("(") >> rule2 >> lit(")") >> lit("->") >> rule3。然后,rule4 有一个上下文,每个组成规则都有自己的上下文。我会编辑我的答案以展示 phoenix 的等效方法。 - academicRobot
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接