Boost.Spirit能使我们轻松地编写出一个简单脚本的语法解析器,它巧妙利用了元编程并重载了大量的C++操作符使得我们能够在C++里直接使用类似EBNF的语法构造出一个完整的语法解析器(同时也把C++弄得面目全非-_-)。
关于EBNF的内容大家可以到网上或书店里找:

EBNF基本形式<符号> ::= <表达式> 或 <符号> = <表达式>
表达式里常用的操作符有:

  1. |   分隔符,表示由它分隔的某一个子表达式都可供选择
  2. *   重复,和正则表达式里的*类似,表示它之前的子表达式可重复多次
  3. -   排除,不允许出现跟在它后面的那个子表达式
  4. ,   串接,连接左右子表达式
  5. ;   终止符,一条规则定义结束
  6. ''  字符串
  7. ""  字符串
  8. (...)  分组,就是平时括号的功能啦,改变优先级用的。
  9. (*...*) 注释
  10. [...]  可选,综括号内的子表达式允许出现或不出现
  11. {...}  重复,大括号内的子表达式可以多次出现
  12. ?...?   特殊字符,由ISO定义的一些特殊字例如:

只允许赋值的简单编程语言可以用 EBNF 定义为:

  1. (* a simple program in EBNF ? Wikipedia *)
  2. program = 'PROGRAM' , white space , identifier , white space ,
  3. 'BEGIN' , white space ,
  4. { assignment , ";" , white space } ,
  5. 'END.' ;
  6. identifier = alphabetic character , [ { alphabetic character | digit } ] ;
  7. number = [ "-" ] , digit , [ { digit } ] ;
  8. string = '"' , { all characters ? '"' } , '"' ;
  9. assignment = identifier , ":=" , ( number | identifier | string ) ;
  10. alphabetic character = "A"|"B"|"C"|"D"|"E"|"F"|"G"|"H"|"I"|"J"|"K"|"L"|"M"|"N"|"O"|"P"|"Q"|"R"|"S"|"T"|"U"|"V"|"W"|"X"|"Y"|"Z" ;
  11. digit = "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9" ;
  12. white space = ? white space characters ? ;
  13. all characters = ? all visible characters ? ;

一个语法上正确的程序:

  1. PROGRAM DEMO1
  2. BEGIN
  3. A0:=3;
  4. B:=45;
  5. H:=-100023;
  6. C:=A;
  7. D123:=B34A;
  8. BABOON:=GIRAFFE;
  9. TEXT:="Hello world!";
  10. END.

这个语言可以轻易的扩展上控制流,算术表达式和输入/输出指令。就可以开发出一个小的、可用的编程语言了。
 
由于C++语法规则的限制,Spirit改变了EBNF中的一部分操作符的使用方式,如:

  • 星号重复符(*)由原来的后置改为前置
  • 逗号串接符(,)由>>或&&代替
  • 中括号可选功能([表达式])改为(!表达式)
  • 大括号重复功能({表达式})由重复符(*表达式)替代
  • 取消注释功能
  • 取消特殊字符功能
  • 同时Spirit又提供了大量的预置解析器加强了它的表达能力,因此可以把Spirit的语法看成是一种EBNF的变种。

版本1.6.x之前的spirit能支持大部分的编译器。在1.8.0之后,由于spirit加入了很多C++的新特性,使兼容各种不标准的编译器的工作变得非常郁闷,于是Spirit不再支持不标准的C++编译器,这意味着VC7.1,BCB2006以及GCC3.1之前版本将不再被支持。(注:据说江湖上有新版Spirit的牛人修改版,可以工作在VC6和VC7上,具体情况不明)

入门

头文件:
    #include <boost/spirit.hpp>

例一,解析一个浮点数   

首先,要弄一个关于浮点数的EBNF规则
    假设我们的浮点数形式是: [±]xxxx[.xxxx][Ex],其中正负号可有可无,后面的幂可有可无,允许不带小数点
    则对应的EBNF规则是:
    digit = "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9";

real = ["+"|"-"], digit, [{digit}], [".", digit, [{digit}]], ["E"|"e", ["+"|"-"], digit, {digit}]

那么对应在Spirit里的是什么样的呢?

  1. !(ch_p('+')|ch_p('-'))>>+digit_p>>! (ch_p('.')>>+digit_p)>>
  2. !((ch_p('e')|ch_p('E')) >> !(ch_p('+')|ch_p('-'))>>+digit_p)

Spirit中,用于匹配表达式的对象叫解析器,如这里的ch_p, digit_p以及由它们和操作符组成的整个或部分都可以称为解析器

  1. !符号代表其后的表达式是可选的,它代替了EBNF里的中括号功能。
  2. ch_p()是一个Spirit预置的解析器生成函数,这个解析器用于匹配单个字符
  3.     >>用于代替逗号顺序连接后面的解析器
  4.     +符号代表1次或多次重复
  5. digit_p也是一个Spirit预置的解析器,它匹配数字字符

这样,再看上面就好理解了:可选的+-号,接着是数字,再跟着是可选的小数点和数字,最后是可选的E跟一个可接+-号的数字

现在,把这个式子写到代码里:

  1. #include <iostream>
  2. #include <boost/spirit.hpp>
  3. using namespace std;
  4. using namespace boost::spirit;
  5. int main()
  6. {
  7. parse_info<> r = parse("-12.33E-10",
  8. !(ch_p('+')|ch_p('-'))>>+digit_p>>
  9. !(ch_p('.')>>+digit_p)>>
  10. !((ch_p('e')|ch_p('E')) >>
  11. !(ch_p('+')|ch_p('-'))>>+digit_p)
  12. );
  13. cout << "parsed " << (r.full?"successful":"failed") << endl;
  14. return 0;
  15. }

这就是Spirit,这个变种的EBNF语法直接就写在C++代码里就可以了,实际上它们是由一系列的简单解析器对象通过重载操作符后组合而成的复杂解析器
    解析器重载的操作符也可以帮我们自动作一些转换工作,如上面的式子中ch_p('+')|ch_p('-')就可以改成ch_p('+')|'-',只要左边或右边的数值其中之一是解析器,它就能自动和另一边的数值组合。
    简化后如下:

  1. !(ch_p('+')|'-')>>+digit_p>>!('.'>>+digit_p)>>!((ch_p('e')|'E') >> !(ch_p('+')|'-')>>+digit_p)

parse函数调用解析器来解析指定的字符串,它的原型是:

  1. parse_info<charT const*> parse(字符串, 解析器);
  2. parse_info<charT const*> parse(字符串, 解析器1, 解析器2);

第二个版本中的解析器2指出解析时可以忽略的一些字符,比如语句中的空格之类的。
    另外,parse还有迭代器的版本

  1. parse_info parse(IteratorT first, IteratorT last, 解析器);
  2. parse_info parse(IteratorT first, IteratorT last, 解析器1, 解析器2);

IteratorT可以是任何迭代器类包括字符串指针,前面的这个两个版本其实只是简单地包装了一下这两个函数。
    返回的parse_info类(其中的IteratorT模板默认为char const*)包含了解析结果信息,里面的成员有:

  1. IteratorT   stop;   //最后解析的位置
  2. bool        hit;    //是否与整个解析器匹配
  3. bool        full;   //是否与整个字符串匹配
  4. std::size_t length; //解析器解析了多少个字符,注意,first+length不一定与stop相同

其实,Spirit已经帮我们准备好了很多解析器,比如上面我们写得要死的浮点数匹配,只要一个real_p就行了(冷静,冷静,上面的一长串到后面还是会用到的)

  1. parse_info<> r = parse("-12.33E-10",real_p);

Spirit预置的一些原始解析器,它们的名字都是以"xxxx_p"的形式出现。
字符解析器

  • ch_p('X') 返回单字符解析器
  • range_p('a','z')    返回一个字符范围解析器,本例中匹配'a'..'z'
  • str_p("Hello World")    返回一个字符串解析器
  • chseq_p("ABCDEFG")  返回一个字符序列解析器,它可以匹配"ABCDEFG","A B C D E F G","AB CD EFG"等
  • anychar_p 匹配任何字符(包括'\0')
  • alnum_p 匹配A-Z,a-z,0-9
  • alpha_p 匹配字母
  • blank_p 匹配空格和TAB
  • cntrl_p 匹配控制字符
  • digit_p 匹配数字字符
  • graph_p 匹配可显示字符(除空格,回车,TAB等)
  • lower_p 匹配小写字符
  • print_p 匹配可打印字符
  • punct_p 匹配标点符号
  • space_p 匹配空格,回车,换行,TAB
  • upper_p 匹配大写字符
  • xdigit_p 匹配十六进制数字符串
  • eol_p   匹配行尾
  • nothing_p 不匹配任何字符,总是返回Fail(不匹配)
  • end_p   匹配结尾

字符解析器支持的操作符

  • ~a      排除操作,如~ch_p('x')表示排除'x'字符
  • a|b     二选一操作,或称为联合,匹配a or b
  • a&b    交集,同时匹配a和b
  • a-b     差,匹配a但不匹配b
  • a^b    异或,匹配a 或 匹配b,但不能两者同时匹配
  • a>>b  序列连接,按顺序先匹配a,接下来的字符再匹配b
  • a&&b  同上(象C语言一样,有短路效果,若a不匹配,则b不会被执行)
  • a||b    连续或,按顺序先匹配a,接下来的字符匹配b(象C语言一样,有短路效果,若a已匹配,则b不会被执行)
  • *a      匹配0次或多次
  • +a      匹配1次或多次
  • !a       可选,匹配0或1次
  • a%b   列表,匹配a b a b a b a...,效果与 a >> *(b >> a)相同

整数解析器    Spirit给我们准备了两个整数解析器类,对应于有符号数和无符号数int_parser和uint_parser
    它们都是模板类,定义如下:

  1. template <
  2. typename T = int,
  3. int Radix = 10,
  4. unsigned MinDigits = 1,
  5. int MaxDigits = -1>
  6. struct int_parser;
  7. template <
  8. typename T = unsigned,
  9. int Radix = 10,
  10. unsigned MinDigits = 1,
  11. int MaxDigits = -1>
  12. struct uint_parser;

模板参数用法:

  • T为数字类型
  • Radix为进制形式
  • MinDigits为最小长度
  • MaxDigits为最大长度,如果是-1表示不限制

比如下面这个例子可以匹配象 1,234,567,890 这种形式的数字

  1. uint_parser<unsigned, 10, 1, 3> uint3_p;        //  1..3 digits
  2. uint_parser<unsigned, 10, 3, 3> uint3_3_p;      //  exactly 3 digits
  3. ts_num_p = (uint3_p >> *(',' >> uint3_3_p));    //  our thousand separated number parser

Spirit已预置的几个int_parser/uint_parser的特化版本:

  1. int_p int_parser<int, 10, 1, -1> const
  2. bin_p uint_parser<unsigned, 2, 1, -1> const
  3. oct_p uint_parser<unsigned, 8, 1, -1> const
  4. uint_p uint_parser<unsigned, 10, 1, -1> const
  5. hex_p uint_parser<unsigned, 16, 1, -1> const

实数解析器Spirit当然也会给我们准备实数解析器,定义如下:

  1. template<
  2. typename T = double,
  3. typename RealPoliciesT = ureal_parser_policies >
  4. struct real_parser;

模板参数用法:

  • T表示实数类型
  • RealRoliciesT是一个策略类,目前不用深究,只要知道它决定了实数解析器的行为就行了。

已预置的实数解析器的特化版本:

  1. ureal_p real_parser<double, ureal_parser_policies<double=""> > const
  2. real_p real_parser<double, real_parser_policies<double=""> > const
  3. strict_ureal_p real_parser<double, strict_ureal_parser_policies<double=""> > const
  4. strict_real_p real_parser<double, strict_real_parser_policies<double=""> > const

real_p前面实例里已经见过,ureal_p是它的unsigned版本。strict_*则更严格地匹配实数(它不匹配整数)

例二,解析实数序列  
    有了上面的知识,我们可以试试解析以逗号分隔的实数序列
    字符串形式为"real,real,real,...real"
    参考上面的一堆预置解析器,我们可以这样组合:

  1. real_p >> *(',' >> real_p);

更简单点,我们可以使用%操作符

  1. real_p%','

于是很简单地写下这样的代码:

  1. {
  2. //用于解析的字符串
  3. const char *szNumberList = "12.4,1000,-1928,33,30";
  4. parse_info<> r = parse( szNumberList, real_p % ',' );
  5. cout << "parsed " << (r.full?"successful":"failed") << endl;
  6. cout << szNumberList << endl;
  7. //使用parse_info::stop确定最后解析的位置便于查错
  8. cout << string(r.stop - szNumberList, ' ') << '^' << endl;
  9. }

解析成功!接下来我们就把里面的数字取出来,解析器重载了[]操作符,在这里可以放入函数或函数对象,放在这里面的函数或函数对象在Spirit里称之为Actor
    对于real_p,它要求形式为:void func(double v)的函数或函数对象,下面我们就来取出这些数字:

  1. #include <iostream>
  2. #include <boost/spirit.hpp>
  3. using namespace std;
  4. using namespace boost::spirit;
  5. //定义函数作为解析器的Actor
  6. void showreal(double v)
  7. {
  8. cout << v << endl;
  9. }
  10. int main()
  11. {
  12. //用于解析的字符串
  13. const char *szNumberList = "12.4,1000,-1928,33,30";
  14. //加入函数
  15. parse_info<> r = parse( szNumberList, real_p[&showreal] % ',' );
  16. cout << "parsed " << (r.full?"successful":"failed") << endl;
  17. cout << szNumberList << endl;
  18. //使用parse_info::stop确定最后解析的位置便于查错
  19. cout << string(r.stop - szNumberList, ' ') << '^' << endl;
  20. return 0;
  21. }

再次运行,显示了一列数字了吧:)
    再写一个函数对象版本的,这次把这列数字写到vector

  1. #include <iostream>
  2. #include <vector>
  3. #include <boost/spirit.hpp>
  4. using namespace std;
  5. using namespace boost::spirit;
  6. int main()
  7. {
  8. // pushreal函数对象,把数字放入vector中
  9. struct pushreal
  10. {
  11. void operator()(double v) const
  12. {
  13. m_vec.push_back(v);
  14. }
  15. pushreal(vector<double> &vec)
  16. :m_vec(vec){;}
  17. private:
  18. vector<double> &m_vec;
  19. };
  20. vector<double> reallist;
  21. //用于解析的字符串
  22. const char *szNumberList = "12.4,1000,-1928,33,30";
  23. //这次用pushreal对象作为Actor
  24. parse_info<> r = parse( szNumberList, real_p[pushreal(reallist)] % ',' );
  25. cout << "parsed " << (r.full?"successful":"failed") << endl;
  26. cout << szNumberList << endl;
  27. //使用parse_info::stop确定最后解析的位置便于查错
  28. cout << string(r.stop - szNumberList, ' ') << '^' << endl;
  29. //显示结果
  30. copy(reallist.begin(),reallist.end(),ostream_iterator<double>(cout," "));
  31. return 0;
  32. }

我不得不告诉你,Spirit也提供了比偶的这个pushreal强得多的函数对象push_back_a(需要冷静哈)

  1. #include <iostream>
  2. #include <vector>
  3. #include <boost/spirit.hpp>
  4. using namespace std;
  5. using namespace boost::spirit;
  6. int main()
  7. {
  8. vector<double> reallist;
  9. //用于解析的字符串
  10. const char *szNumberList = "12.4,1000,-1928,33,30";
  11. //使用自带的push_back_a
  12. parse_info<> r = parse( szNumberList, real_p[push_back_a(reallist)] % ',' );
  13. cout << "parsed " << (r.full?"successful":"failed") << endl;
  14. cout << szNumberList << endl;
  15. //使用parse_info::stop确定最后解析的位置便于查错
  16. cout << string(r.stop - szNumberList, ' ') << '^' << endl;
  17. //显示结果
  18. copy(reallist.begin(),reallist.end(),ostream_iterator<double>(cout," "));
  19. return 0;
  20. }

在上面的实数序列中,如果中间含有空格或TAB,这个解析就不能成功,这时可以使用parse函数的另一个重载版本:

  1. parse_info<charT const*> parse(字符串, 解析器1, 解析器2);
  2. //或
  3. parse_info parse(IteratorT first, IteratorT last, 解析器1, 解析器2);

其中的解析器2用于跳过其匹配的字符,我们要跳过空格,所以解析器2可以使用space_p

  1. parse_info<> r = parse( szNumberList,
  2. real_p[push_back_a(reallist)] % ',',
  3. space_p);

如果更进一步,我们甚至可以连逗号也跳过,直接取得一列数字:

  1. parse_info<> r = parse( szNumberList,
  2. *real_p[push_back_a(reallist)],
  3. space_p|ch_p(','));

push_back_a外,Spirit还提供了不少有用的Actor(就是函数对象啦),如下
注:这里的ref是外部数据,就象上例中的reallist,value_ref是外部数值,value是解析出的数值

  • increment_a(ref)    自增  ++ref
  • decrement_a(ref)    自减  --ref

赋值操作

  • assign_a(ref)                   赋值  ref = value
  • assign_a(ref, value_ref)    常量赋值  ref = value_ref

容器操作

  • push_back_a(ref)                   ref.push_back(value)
  • push_back_a(ref, value_ref)    ref.push_back(value_ref)
  • push_front_a(ref)                  ref.push_front(value)
  • push_front_a(ref, value_ref)   ref.push_front(value_ref)
  • clear_a(ref)                           ref.clear()

关联容器操作(vt类型是typeof(ref)::value_type)

  • insert_key_a(ref, value_ref)                ref.insert(vt(value, value_ref))
  • insert_at_a(ref, key_ref_, value_ref)    ref.insert(vt(key_ref,value_ref))
  • insert_at_a(ref, key_ref)                    ref.insert(vt(key_ref,value))
  • assign_key_a(ref, value_ref)               ref[value] = value_ref
  • erase_a(ref)                                      ref.erase(ref,value)
  • erase_a(ref, key_ref)                         ref.erase(ref,key_ref)

其它操作

  • swap_a(aref, bref)      交换aref和bref

例三,四则运算

如果说上面的两个例子用正则表达式也能轻松搞定了话,那么接下来你就能体会到Spirit的强大威力!
    解析四则运算表达式,同样先要把EBNF规则写出来:

  • //实数或者是括号包围的子表达式
  • 因子 = 实数 | '(' , 表达式 , ')';
  • //因子*因子或因子/因子,可连续乘除也可只是一个因子
  • 乘除计算 = 因子,{('*',因子)|('/',因子)};
  • //加减计算,与上面类似
  • 表达式 = 乘除计算,{('+',乘除计算)|('-',乘除计算)};

这个定义已经隐含了优先级:

  • 要计算表达式(加减计算),必然要先计算乘除计算;
  • 要计算乘除计算,就要先计算因子;
  • 要计算因子,要么得到一个数字,要么就要计算括号内的子表达式。

转成Spirit解析器组合:

  1. rule<phrase_scanner_t> factor, term, exp;
  2. factor = real_p | ('(' >> exp >> ')');
  3. term   = factor >> *(('*' >> factor) | ('/' >> factor));
  4. exp    = term >> *(('+' >> term) | ('-' >> term));

这里的rule是一个规则类,它可以作为所有解析器的占位符,定义如下:

  1. template<
  2. typename ScannerT = scanner<>,
  3. typename ContextT = parser_context<>,
  4. typename TagT = parser_address_tag>
  5. class rule;

其中的模板参数作用是:
ScannerT    扫描器策略类
        它有两类工作模式,一种是字符模式,一种是语法模式,默认的scanner<>是工作于字符模式的。
ContextT    内容策略类
        它决定了rule里的成员变量以及Actor的类型,稍后会有利用这个模板参数来加入自定义的成员变量的例子
TagT          标识策略类
        每个rule都有一个id()方法,用于识别不同的rule,TagT就用于决定id()返回的数据(后面会讲到)。
    这三个策略类可以不按顺序地输入,如

  1. rule<parser_address_tag,parser_context<>,scanner<> >;
  2. rule<parser_context<> >;
  3. rule<scanner<>,parser_address_tag >;

是同一个类。
    值得注意的是ScannerT,我们上面没有使用默认的scanner<>,而是使用了phrase_scanner_t,因为工作于字符模式的扫描器无法与parse的解析器2参数(跳过匹配字符,见上)一同工作,这样就无法解析含有空格的表达式,这可不完美,所以我们使用的工作于语法模式phrase_scanner_t

  1. #include <iostream>
  2. #include <vector>
  3. #include <boost/spirit.hpp>
  4. using namespace std;
  5. using namespace boost::spirit;
  6. int main()
  7. {
  8. rule<phrase_scanner_t> factor, term, exp;
  9. factor = real_p | ('(' >> exp >> ')');
  10. term   = factor >> *(('*' >> factor) | ('/' >> factor));
  11. exp    = term >> *(('+' >> term) | ('-' >> term));
  12. const char *szExp = "1 + (2 * (3 / (4 + 5)))";
  13. parse_info<> r = parse( szExp , exp, space_p);
  14. cout << "parsed " << (r.full?"successful":"failed") << endl;
  15. return 0;
  16. }

接下来,要得到这个四则表达式的计算结果,这才是我们要的,于是Spirit自带的lambda支持:phoenix登场!

头文件:
    #include <boost/spirit/phoenix.hpp>

    phoenix提供和与Boost.Lambda类似的功能,它可以直接就地生成匿名函数对象phoenix使用arg1,arg2,arg3...作为占位符,Boost.Lambda则使用_1,_2,_3...,使用举例:

  1. #include <iostream>
  2. #include <vector>
  3. #include <boost/spirit.hpp>
  4. #include <boost/spirit/phoenix.hpp>
  5. using namespace std;
  6. using namespace boost::spirit;
  7. using namespace phoenix;
  8. int main()
  9. {
  10. vector<int> vec(10);
  11. int i=0;
  12. //arg1 = var(i)++ 把i++赋值给vec里各单元
  13. for_each(vec.begin(),vec.end(),arg1 = var(i)++);
  14. //cout<<arg1<<endl 把vec各单元输出至cout
  15. for_each(vec.begin(),vec.end(),cout << arg1 << endl);
  16. return 0;
  17. }

这样我们就可以利用phoenix提供的匿名函数对象作为Actor, 同时利用Spirit提供的closure类为rule添加一个val成员变量存储计算结果(还记得rule的ContextT策略吗?)

  1. #include <iostream>
  2. #include <vector>
  3. #include <boost/spirit.hpp>
  4. #include <boost/spirit/phoenix.hpp>
  5. using namespace std;
  6. using namespace boost::spirit;
  7. using namespace phoenix;
  8. int main()
  9. {
  10. //为rule准备一个val变量,类型为double
  11. //准确地说:是一个phoenix类(这里的member1),它和其它phoenix类组成lambda表达式,在lambda中可以把它看成是一个double。
  12. struct calc_closure : boost::spirit::closure<calc_closure, double>
  13. {
  14. member1 val;
  15. };
  16. //定义ContextT策略为calc_closure::context_t
  17. rule<phrase_scanner_t, calc_closure::context_t> factor, term, exp;
  18. //直接使用phoenix的lambda表达式作为Actor
  19. factor = real_p[factor.val = arg1] | ('(' >> exp[factor.val = arg1] >> ')');
  20. term   = factor[term.val = arg1] >> *(('*' >> factor[term.val *= arg1]) | ('/' >> factor[term.val /= arg1]));
  21. exp    = term[exp.val = arg1] >> *(('+' >> term[exp.val += arg1]) | ('-' >> term[exp.val -= arg1]));
  22. const char *szExp = "1 + (2 * (3 / (4 + 5)))";
  23. double result;
  24. parse_info<> r = parse( szExp , exp[assign_a(result)], space_p);
  25. cout << szExp;
  26. if(r.full)
  27. {
  28. //成功,得到结果
  29. cout << " = " << result << endl;
  30. }
  31. else
  32. {
  33. //失败,显示错误位置
  34. cout << endl << string(r.stop - szExp, ' ') << '^' << endl;
  35. }
  36. return 0;
  37. }

感到很神奇?这里有必要多说一下boost::spirit::closure的作用,它的使用方法是:

  1. struct name : spirit::closure<name, type1, type2, type3,... typen>
  2. {
  3. member1 m_name1;
  4. member2 m_name2;
  5. member3 m_name3;
  6. ...
  7. memberN m_nameN;
  8. };

一种类型对应一个member,使用name::context_t作为ContextT策略的rule就会含有N个相应的变量,而且这个rule的Actor将会接收到member1对应的数据。
    也可以用于语法类,如grammar<t, name::context_t="">,关于语法类,后面章节将会提到。
    注:默认最多到member3,要想使用更多数据,在包含Spirit头文件前预定义PHOENIX_LIMIT和BOOST_SPIRIT_CLOSURE_LIMIT,如

  1. #define PHOENIX_LIMIT 10
  2. #define BOOST_SPIRIT_CLOSURE_LIMIT 10
提高 例四,使用自定义语法类框架

    在例三中,我们用了不足50行的代码搞定了一个四则运算字符串的解析,可见Spirit的威力巨大。不过仅仅这样还不够,Spirit可是号称轻量语法解析库,一个基本可用的脚本可不止四则运算那么点东西,起码要有赋值、条件、循环、输入输出吧?
    有了上面的知识,再加上一些编程经验,一个个搞定它们应该不是太难的事,但把所有的规则堆在一起不仅恶心,而且难以维护,于是Spirit提供了语法类grammar来集中管理。
    grammar的定义如下:
  1. template<
  2. typename DerivedT,
  3. typename ContextT = parser_context<> >
  4. struct grammar;
    DerivedT参数是反引用自身类型,如果用过WTL库的可能对这个比较熟悉,使用这种技术可以保持多态性的同时消除虚函数带来的性能开稍。
    ContextT参数就是内容策略类,在例三中提到过。
    编写一个语法类框架的基本形式如下:
  1. struct my_grammar : public grammar<my_grammar>
  2. {
  3. template <typename ScannerT>
  4. struct definition
  5. {
  6. rule  r;
  7. definition(my_grammar const& self)  { r = /*..define here..*/; }
  8. rule const& start() const { return r; }
  9. };
  10. };
    它继承自grammar,模板参数DerivedT就是自身类型,ContextT可以使用默认的parser_context<>或者自己定义一个(比如例三中的closure)。
    这个类内部必须要有一个definition类的定义,这个definition类的模板参数ScannerT由框架使用环境决定。它由两个重要方法:
  • start() const函数:它返回一个rule。使用my_grammar解析时,就从这个rule开始。
  • definition构造函数:这里是初始化rule的最好场所。它的self参数是整个my_grammar的实例引用,接下去你会发现这可是个很有用的东西。
    同时,很重要的一点:语法类本身也是一个解析器,它也能与其它解析器组合。

下面,我们把例三中的四则运算解析功能放到一个语法类中,然后再用这个语法类与其它解析器合作弄一个简单的赋值操作出来:

  1. #include <iostream>
  2. #include <boost/spirit.hpp>
  3. #include <boost/spirit/phoenix.hpp>
  4. using namespace std;
  5. using namespace boost::spirit;
  6. using namespace phoenix;
  7. //closure,为解析器提供存储策略,见例三
  8. struct calc_closure : boost::spirit::closure<calc_closure, double>
  9. {
  10. member1 val;
  11. };
  12. //四则运算语法类,它也使用了closure的内容策略
  13. struct calculator : public grammar<calculator, calc_closure::context_t>
  14. {
  15. //语法类重要成员:struct definition
  16. template <typename ScannerT>
  17. struct definition
  18. {
  19. // factor, term, exp的rule类型,同例三(ScannerT模板在使用时决定)
  20. typedef rule<scannert, calc_closure::context_t> rule_type;
  21. rule_type factor, term, exp;
  22. // 启动rule,在这个例子中,它也是递归的最顶层,负责把exp的最终结果赋值给框架本身。
  23. rule rlStart;
  24. const rule& start() const { return rlStart; }
  25. //definition的构造函数,self参数引用的是calculator类的实例
  26. definition(calculator const& self)
  27. {
  28. // 四则运算规则定义与例三相同
  29. factor = real_p[factor.val = arg1] |
  30. ('(' >> exp[factor.val = arg1] >> ')');
  31. term   = factor[term.val = arg1] >>
  32. *(('*' >> factor[term.val *= arg1]) |
  33. ('/' >> factor[term.val /= arg1]));
  34. exp    = term[exp.val = arg1] >>
  35. *(('+' >> term[exp.val += arg1]) |
  36. ('-' >> term[exp.val -= arg1]));
  37. //self.val=arg1也是phoenix的匿名函数:把exp的结果赋值给框架本身(self的作用)
  38. rlStart  = exp[self.val = arg1];
  39. }
  40. };
  41. };
  42. int main()
  43. {
  44. string strVar; //变量名
  45. double result; //结果
  46. calculator calc;
  47. // 赋值语法:变量名 = 表达式
  48. rule<phrase_scanner_t> rlEqu =  (+alpha_p)[assign(strVar)] >> '=' >> calc[assign_a(result)];
  49. const char *szEqu = "value = 1 + (2 * (3 / (4 + 5)))";
  50. parse_info<> r = parse( szEqu , rlEqu, space_p);
  51. if(r.full) //成功,得到结果
  52. cout << strVar << " = " << result << endl;
  53. else  //失败,显示错误位置
  54. cout << endl << string(r.stop - szEqu, ' ') << '^' << endl;
  55. return 0;
  56. }
    如果没拼写出错的话,应该会显示出"value = 1.66667"。
 

    例五,在四则运算表达式中使用变量

    在例四中,我们可以解析"变量名 = 表达式"这种形式的语句。现在,我们再进一步,允许在表达式中使用变量,如value = value * pi + 5
    那么,还是先从规则动手。
    这里我把变量名的规则放松了一点,例四里变量名只能用字母,这里除了第一位是字母后面允许使用数字。于是变量名规则写成(alpha_p >> *(alnum_p))
    变量代表的是一个数值,它和实数应该属于同一级别,所以我们把变量规则加入到factor规则里:
  1. factor = real_p[factor.val = arg1] |
  2. // 在表达式中使用变量
  3. (alpha_p >> *(alnum_p))[/*这里写什么呢*/]|
  4. ('(' >> exp[factor.val = arg1] >> ')');
    那么,变量名对应的Actor写什么呢?具体地说是"factor.val = 什么"呢?
    对了,我们只要把变量名和它的数值一一对应起来,那么这里只要把此变量名对应的数值送给factor.val就行了,标准库里的map在这里用是再适合不过了。
    为了把变量和它的数值放到map里,main里的rlEqu规则我们也要小改改:
  1. rule<phrase_scanner_t> rlEqu =
  2. ((alpha_p >> *(alnum_p))[assign(strVar)] >>
  3. '=' >> calc[assign_a(result)] ) [ insert_at_a(mapVar,strVar,result) ];
    后面又加了一个Actor,把strVar及result放到map类型的mapVar中。
    
    回到factor规则,我们试着把变量名规则的Actor写成[factor.val = getvalue(arg1, arg2)],注意所有字符串规则的Actor都会有两个参数,它们是两个迭代器,分别指向起始位置和结束位置。所以这里使用了phoenix的arg1和arg2占位符。
    这个getvalue我们把它写成一个函数,它从map中取出变量名对应的数值。
  1. double getvalue(const char*first, const char*last)
  2. {
  3. return mapVar[string(first,last)];
  4. }
    编译,出现错误,说不能把arg1转换成字符串云云。看来这招不行,查phoenix手册,手册说想要在phoenix的表达式中使用函数,就得按它说的去做-_-
    它的要求是这样地:
    1.先按如下形式做一个函数对象
  1. struct func_impl
  2. {
  3. //Param1等对就的是各个输入参数的类型
  4. template<typename Param1,typename Param2,...,typename ParamN>
  5. struct result{
  6. //定义输出参数的类型
  7. typedef returntype type;
  8. };
  9. //在这里该干啥干啥
  10. template<typename Param1,typename Param2,...,typename ParamN>
  11. returntype operator()(...)
  12. {
  13. ...
  14. }
  15. };
 
    2.使用phoenix::function类来包装第一步做的函数对象,这样才能和phoenix配合呢
    另外,也可以直接用phoenix::bind把简单函数包装起来使用,不过这样虽然简单很多,在我们这个例子中却不便于封装于是作罢(主要还是想秀一下)。
    嗯,动手做吧:
  1. //适配phoenix的函数对象
  2. struct getvalue_impl
  3. {
  4. template <typename ParamA,typename ParamB>    //输入参数类型
  5. struct result{
  6. typedef double type;    //返回类型
  7. };
  8. //函数主体,其实这里的ParamA和ParamB都是char*
  9. template <typename ParamA,typename ParamB>
  10. double operator()(ParamA const& start,ParamB const& end) const
  11. {
  12. //返回变量名对应的数值
  13. return m_mapVar[string(start,end)];
  14. }
  15. getvalue_impl(map<string,double
  16. :m_mapVar(mapVar){;}
  17. private:
  18. map<string,double
  19. };
  20. // phoenix表达式中能接受的仿函数类型
  21. const function<getvalue_impl> getValue = getvalue_impl();
    现在终于可以使用了,所有难点已经突破,可以完工了:
  1. #include <iostream>
  2. #include <map>
  3. #include <boost/spirit.hpp>
  4. #include <boost/spirit/phoenix.hpp>
  5. #include <boost/spirit/actor.hpp>    // insert_at_a需要
  6. using namespace std;
  7. using namespace boost::spirit;
  8. using namespace phoenix;
  9. struct calc_closure : boost::spirit::closure<calc_closure, double>
  10. {
  11. member1 val;
  12. };
  13. struct calculator : public grammar<calculator, calc_closure::context_t>
  14. {
  15. template <typename ScannerT>
  16. struct definition
  17. {
  18. typedef rule<scannert, calc_closure::context_t> rule_type;
  19. rule_type factor, term, exp;
  20. rule rlStart;
  21. const rule& start() const { return rlStart; }
  22. definition(calculator const& self)
  23. {
  24. factor = real_p[factor.val = arg1] |
  25. // 允许在表达式中使用变量,结果用calculator::m_getValue从map中取
  26. (alpha_p >> *(alnum_p))[ factor.val = self.m_getValue(arg1, arg2) ] |
  27. ('(' >> exp[factor.val = arg1] >> ')');
  28. term   = factor[term.val = arg1] >>
  29. *(('*' >> factor[term.val *= arg1]) |
  30. ('/' >> factor[term.val /= arg1]));
  31. exp    = term[exp.val = arg1] >>
  32. *(('+' >> term[exp.val += arg1]) |
  33. ('-' >> term[exp.val -= arg1]));
  34. rlStart  = exp[self.val = arg1];
  35. }
  36. };
  37. calculator(map<string,double
  38. :m_getValue( getvalue_impl(mapVar) )    //初始化,把map传给m_getValue
  39. {}
  40. //适配phoenix的函数对象
  41. struct getvalue_impl
  42. {
  43. template <typename ParamA,typename ParamB>    //输入参数类型
  44. struct result{
  45. typedef double type;    //返回类型
  46. };
  47. //函数主体,其实这里的ParamA和ParamB都是char*
  48. template <typename ParamA,typename ParamB>
  49. double operator()(ParamA const& start,ParamB const& end) const
  50. {
  51. //返回变量名对应的数值
  52. return m_mapVar[string(start,end)];
  53. }
  54. getvalue_impl(map<string,double
  55. :m_mapVar(mapVar){;}
  56. private:
  57. map<string,double
  58. };
  59. // phoenix表达式中能接受的仿函数类型
  60. const function<getvalue_impl> m_getValue;
  61. };
  62. //用来显示map中变量的值
  63. void showPair(const pair<string,< span="">double> &val)
  64. {
  65. cout << val.first << " = " << val.second << endl;
  66. }
  67. int main()
  68. {
  69. string strVar;
  70. double result;
  71. //用来保存变量和对应的数值
  72. map<string,double
  73. //把map传给语法类,让解析器知道变量的值
  74. calculator calc(mapVar);
  75. // 变量名规则(alpha_p >> +(alnum_p)),除第一位外后面可以跟数字。
  76. // 整个等式末尾加入insert_at_a的actor,匹配成功后把变量和数值存到map中。
  77. rule<phrase_scanner_t> rlEqu =
  78. (
  79. (alpha_p >> *(alnum_p))[assign(strVar)] >>
  80. '=' >> calc[assign_a(result)] ) [ insert_at_a(mapVar,strVar,result) ];
  81. // 多行赋值语句,表达式用使用变量
  82. const char *szEqus[3] = {
  83. "PI = 3.1415926",
  84. "Rad = PI*2.0/3.0",
  85. "Deg = Rad*180/PI"};
  86. // 逐句解析
  87. for(int i=0; i<3; i++)    parse(szEqus[i], rlEqu, space_p);
  88. // 显示每个变量的数值
  89. for_each(mapVar.begin(), mapVar.end(), showPair );
  90. return 0;
  91. }
显示结果:

Deg = 120     PI = 2.14159     Rad = 2.0944

到现在,我们已经可以向别人吹嘘说:脚本解析?小菜!!!哈哈...
 
持续改进...    例五的代码用起来很爽吧,不算注释的话100行不到,已经有个脚本的雏形了。只是...只是有个小问题,因为我们设置了跳过空格,这对于语句来说是必须的,但却带来了一个副作用。
    试试把szEqus里的变量名中间加个空格,比如改成"R ad = P   I*2.0/3.0",这样的语句居然也能正确解析,这显然不是我们想要的(要的就是这种效果?!!偶无语...)。
    那么怎样才能解析变量名时不许跳过空格,而解析语句的又允许跳过呢(搞双重标准)?下面介绍的命令就可以帮上忙了,首先赶快在没人发现这个错误之前把它搞定先:
    把所有的变量名规则(factor规则定义里有一个,rlEqu规则定义里有一个)用lexeme_d包裹起来:

  1. lexeme_d[(alpha_p >> *(alnum_p))]

再测试,嗯,现在不允许出现含有空格的变量名了。
下面介绍各种预置命令    使用形式: 命令[解析器表达式]
    lexeme_d
    不跳过空白字符,当工作于语法级时,解析器会忽略空白字符,lexeme_d使其临时工作于字符级
    如整数定义应该是: integer = lexeme_d[ !(ch_p('+') | '-') >> +digit ];,这样可以防止"1 2 345"被解析为"12345"
    
    as_lower_d
    忽略大小写,解析器默认是大小写敏感的,如果要解析象PASCAL一样的大小写不敏感的语法,使用r = as_lower_d["begin"];(注,里面的参数都得小写)
    
    no_actions_d
    停止触发Actor
    
    longest_d
    尝试最长匹配
    如number = integer | real;用它匹配123.456时,integer会匹配123直到遇到小数点结束,使用number=longest_d[integer | real];可以避免这个问题。
    
    shortest_d
    与longest_d相反
    
    limit_d
    定义范围,用法limit_d(min, max)[expression]
    如

  1. uint_parser<int, 10, 2, 2> uint2_p;
  2. r = lexeme_d
  3. [
  4. limit_d(0u, 23u)[uint2_p] >> ':'    //  Hours 00..23
  5. >>  limit_d(0u, 59u)[uint2_p] >> ':'    //  Minutes 00..59
  6. >>  limit_d(0u, 59u)[uint2_p]           //  Seconds 00..59
  7. ];

min_limit_d/max_limit_d
    定义最小/最大值,用法:min_limit_d(min)[expression]
例七,牛叉型解析器    相对于Spirit预置的一些简单解析器,它也提供了很多功能更强大的“牛叉型”解析器。现介绍如下:
    f_ch_p
        语法:f_ch_p(ChGenT chgen)
        作用:和ch_p类似,它解析的字符由chgen的返回值决定,chgen是一个类型为"CharT func()"的函数(或函数对象)
        例如:char X(){return 'X';} f_ch_p(&X); 
    
    f_range_p
        语法:f_range_p(ChGenAT first, ChGenBT last)
        作用:和range_p类似,它由first和last两个函数(或函数对象)的返回值决定解析的字符范围。
    
    f_chseq_p
        语法:f_chseq_p(IterGenAT first, IterGenBT last)
        作用:和chseq_p类似,同样由first和last两个函数(或函数对象)的返回值决定起始和终止迭代器。
        
    f_str_p
        语法:f_str_p(IterGenAT first, IterGenBT last)
        作用:和str_p类似,参数同f_chseq_p 
    
    if_p
        语法:if_p(condition)[then-parser].else_p[else-parser],其中.else_p可以不要
        作用:如果condition成立,就使用then-parser,否则用else-parset
        例如:if_p("0x")[hex_p].else_p[uint_p]
    
    for_p
        语法:for_p(init, condition, step)[body-parser]
        作用:init和step是一个无参数的函数或函数对象,各参数与for的作用类似(先init,再检查condition,有效则执行body-parser及step,再检查condition...)
        例如:for_p(var(i)=0, var(i) < 10, ++var(i) ) [ int_p[var(sum) += arg1] ]
        
    while_p, do_p
        语法:while_p(condition)[body-parser] 及 do_p[body-parser].while_p(condition)
        作用:条件循环,直接condition不成立为止。
    
    select_p, select_fail_p
        语法:select_p(parser_a , parser_b /* ... */, parser_n);
        作用:从左到右接顺序测试各解析器,并得到匹配的解析器的序号(0表示匹配parser_a,1匹配parser_b...)
        例如:见switch_p
    
    switch_p
        语法:switch_p(value)[case_p<value_a>(parser_a),case_p<value_b>(parser_b),...,default_p(parser_def)]
        作用:按value的值选择解析器
        例如:下例中匹配的形式为:字符a后是整数,b后是个逗号,c后跟着"bcd",d后什么也没有。

  1. int choice = -1;
  2. rule<> rule_select =
  3. select_fail_p('a', 'b', 'c', 'd')[assign_a(choice)]
  4. >> switch_p(var(choice))
  5. [
  6. case_p<0>(int_p),
  7. case_p<1>(ch_p(',')),
  8. case_p<2>(str_p("bcd")),
  9. default_p
  10. ];

 c_escape_ch_p, lex_escape_ch_p
        语法:c_escape_ch_p
        作用:和ch_p类似,其牛叉的地方在于能解析C语言里的转义字符:\b, \t, , \f, , \\, \", \', \xHH, \OOO
        例如:confix_p('"', *c_escape_ch_p, '"')
        
    repeat_p
        语法、作用:
        repeat_p (n) [p]        重复n次执行解析器p 
        repeat_p (n1, n2) [p]   重复n1到n2次解析器p 
        repeat_p (n, more) [p]  至少重复n次解析 
    
        例如:检验是否是有效的文件名

  1. valid_fname_chars = /*..*/;
  2. filename = repeat_p(1, 255)[valid_fname_chars];

confix_p
        语法:confix_p(open,expr,close)
        作用:解析独立元素,如C语言里的字符串,注释等,相当于open >> (expr - close) >> close
        例如:解析C注释confix_p("/*", *anychar_p, "*/")
    
    comment_p,comment_nest_p
        语法:comment_p(open,close),如果close不指定,默认为回车
        作用:confix_p的辅助解析器,comment_p遇到第一个close时即返回,而comment_nest_p要open/close对匹配才返回。
        例如:

  1. comment_p("//") C++风格注释
  2. comment_nest_p('{', '}')|comment_nest_p("(*", "*)") pascal风格注释

list_p
        语法:list_p(paser,delimiter)
        作用:匹配以delimiter作为分隔符的列表
        
    regex_p
        语法:regex_p("正则表达式")
        作用:使用正则表达式来匹配字符串(强强联手啊~~啥也不说了)
        
    symbols类
        定义:

  1. template
  2. <
  3. typename T = int,
  4. typename CharT = char,
  5. typename SetT = impl::tst<t, chart>
  6. >
  7. class symbols;

初始化方式:

  1. symbols<> sym;
  2. sym = "pineapple", "orange", "banana", "apple", "mango";
  3. sym.add("hello", 1)("crazy", 2)("world", 3);

作用:匹配字符串(CharT*)返回对应的整数(T)
        例如:

  1. struct Show{
  2. void operator()( int n ) const
  3. {
  4. cout << n;
  5. }
  6. };
  7. symbols<> sym;
  8. sym.add("零",0) ("一",1) ("二",2) ("三",3) ("四",4) ("五",5) ("六",6) ("七",7) ("八",8) ("九",9);
  9. parse("二零零八",*(sym[Show()]));

functor_parser
        作用:可以方便地用它来创建一个解析器
        例如:见下例
演示怎样自己写一个解析器,解析一个整数

  1. struct number_parser
  2. {
  3. typedef int result_t;   //定义解析器结果类型
  4. //参数是:扫描器,结果
  5. template <typename ScannerT>
  6. std::ptrdiff_t operator()(ScannerT const& scan, result_t& result) const
  7. {
  8. if (scan.at_end())  //如果结果或出错,返回-1
  9. return -1;
  10. char ch = *scan;
  11. if (ch < '0' || ch > '9')
  12. return -1;
  13. result = 0;
  14. std::ptrdiff_t len = 0;
  15. do              //解析字符串,得到结果
  16. {
  17. result = result*10 + int(ch - '0');
  18. ++len;
  19. ++scan;
  20. } while (!scan.at_end() && (ch = *scan, ch >= '0' && ch <= '9'));
  21. return len; //返回解析的字符串长度
  22. }
  23. };
  24. //用functor_parser包装成解析器
  25. functor_parser<number_parser> number_parser_p;
例八,抽象语法树(abstract syntax tree,简称AST)
 
 
    上面的例子都是就地解析。在比较大型的语法解析中,一种更通用的方式是先产生抽象语法树再遍历它来做解析工作。比如著名的GCC,观察它的源代码就可以发现,它解析源代码时首先生成AST再开始编译。其它编译器因为看不到源码不好说,想来也该是如此吧。
    Spirit也支持生成抽象语法树的功能(不过用它来解析C++代码可就不太合适了,Spirit针对的是轻量的小型脚本)
    头文件
    #include <boost/spirit/include/classic_ast.hpp>
    使用AST和之前的解析步骤很相似,一个重要的区别是所有的子规则都应该是字符串形式的,也就是说real_p,int_p之类的帮不上忙了,我们得自力更生。
    我们在例一中使用过的浮点数解析器这次可以派上用场了。    
    下面的例子参考了例四中的解析器规则:

  1. #include <iostream>
  2. #include <boost/spirit.hpp>
  3. #include <boost/spirit/include/classic_ast.hpp>
  4. using namespace std;
  5. using namespace boost::spirit;
  6. struct calculator : public grammar
  7. {
  8. template <typename ScannerT>
  9. struct definition
  10. {
  11. typedef rule rule_type;
  12. rule_type factor, term, exp, str_real_p;
  13. const rule_type& start() const { return exp; }
  14. definition(calculator const& self)
  15. {
  16. str_real_p = leaf_node_d[
  17. !(ch_p('+')|'-')>>+digit_p>>
  18. !('.'>>+digit_p)>>!((ch_p('e')|'E') >>
  19. !(ch_p('+')|'-')>>+digit_p)
  20. ];
  21. factor = str_real_p | inner_node_d[('(' >> exp >> ')')];
  22. term   = factor >> *((root_node_d[ch_p('*')] >> factor)
  23. | (root_node_d[ch_p('/')] >> factor));
  24. exp    = term >> *((root_node_d[ch_p('+')] >> term)
  25. | (root_node_d[ch_p('-')] >> term));
  26. }
  27. };
  28. };
  29. //显示AST的结构,Indent是缩进宽度
  30. typedef tree_match<char const*>::container_t container_t;
  31. void showTree(const container_t& con, int Indent)
  32. {
  33. for(container_t::const_iterator itr=con.begin(); itr!=con.end(); ++itr)
  34. {
  35. //tree_node: value, children
  36. //显示当前值
  37. cout << string(Indent*4, ' ') << "|--(" <<
  38. string(itr->value.begin(), itr->value.end()) << ')' << endl;
  39. //显示子节点
  40. showTree(itr->children, Indent+1);
  41. }
  42. }
  43. int main()
  44. {
  45. calculator calc;
  46. const char *szExq = "12 * (24 - 15) / (17 + 6)";
  47. tree_parse_info<> info = ast_parse(szExq, calc, space_p);
  48. showTree(info.trees, 0);
  49. return 0;
  50. }

这个程序可以显示出整个AST的结构,比如例子中的“12 * (24 - 15) / (17 + 6)”, 解析结果(用图片美化了一下):


    这个代码和之前的代码主要区别是多了几个xxxx_node_d形式的命令,以及使用ast_parse函数来解析。 tree_parse_info类型       ast_parse的参数与parse相同,主要区别就在于它的返回值不是parse_info而是tree_parse_info。
    tree_parse_info的成员有:

  1. IteratorT   stop;
  2. bool        match;
  3. bool        full;
  4. std::size_t length;
  5. typename tree_match<IteratorT, NodeFactoryT, T>::container_t trees;

前四个和parse_info相同,多出来的trees是一个含有tree_node的容器(默认的容器是std::vector,如果预定义了BOOST_SPIRIT_USE_LIST_FOR_TREES,就会使用std::list)
    tree_node有两个重要的成员:

  • children:   子节点,与tree_parse_info里的trees类型相同:std::vector<tree_node<T>>(或std::list<...>)
  • value:      数据,类型为模板T,这个参数默认类型是node_val_data<IteratorT, ValueT>

整个AST就是由tree_node的数据value以及子节点children组成的。(参考例子中showTree的代码就可以看出)

node_val_data<IteratorT, ValueT>的模板参数IteratorT默认是const char*, ValueT是nil_t(空数据,定义为struct nil_t {};)。
        在这个类内部维护着一个vector(或list),它保存着解析出来的脚本字符串,比如上面例子中的"12","*","24"等。node_val_data向外提供的重要方法有:

  • begin()/end():              直接返回内部vector(或list)的begin()和end()
  • is_root()/is_root(bool):    取得/设置对应节点的root状态(由root_node_d命令设置)
  • value()/value(const ValueT&)取得/设置用户自定义数值(默认的nil_t没法带数据,必须通过指定NodeFactoryT来改变ValueT类型,马上会讲到)
  • id()/id(parser_id):         取得/设置解析此节点的解析器id(还记得rule的TagT策略吗,下面还会讲到)

它的value()方法可以设置和取得自定义数据,不过默认的nil_t却是个空结构,根本不能使用。这时我们可以通过指定“工厂类”来改变ValueT的类型,方法如下(假设使用double):

  1. typedef node_val_data_factory<double> factory_t;
  2. my_grammar gram;
  3. my_skip_grammar skip;
  4. tree_parse_info<iterator_t, factory_t> i =
  5. ast_parse<factory_t>(first, last, gram, skip);

rule有一个id()方法可以返回一个parser_id类型的标记,用它可以区分各个不同的rule,它返回什么值由TagT模板参数决定,默认的parser_address_tag返回的是rule的内存地址。
    我们可以用其它参数代替它以实现更适用的标记,Spirit已准备好的TagT策略有:
    parser_tag<N>,它接收一个整数,如

  1. rule<parser_tag > my_rule;
  2. assign(rule.id().to_long() == 123);

dynamic_parser_tag, 它给rule加入了set_id(int)的能力,如:

  1. rule<dynamic_parser_tag> my_dynrule;
  2. my_dynrule.set_id(1234);    // set my_dynrule's id to 1234

利用这些TagT策略再和node_val_data里的id()相比较就能知道这个数据是由哪个解析器解析的。
下面介绍Spirit为AST而引入的几个命令:    leaf_node_d
        由leaf_node_d命令包裹的规则将被视为一个整体,它还由另一个名字token_node_d
        尝试把上例中的leaf_node_d命令去掉,再看解析结果:所有的数字都被折成了一个个字节。
    
    inner_node_d
        这个命令会忽略第一个子规则和最后一个子规则,只取中间部分。
        把上例中的inner_node_d去掉,那么括号也被参与解析。
    
    root_node_d
        这个命令对于AST至关重要,由root_node_d命令包裹的节点将成为同一规则中其它节点的父节点。它的工作方式如下:
假设A是前一节点 B是新产生的节点 如果B是根节点 A成为B的第一个子节点 否则,如果A是根节点而B不是,那么 B成为A的最后一个子节点 其它情况 A和B处于同一级
        比如这个例子中的“12 * (24 - 15) / (17 + 6)”
        对于解析器解析顺序是:
exp = term term = 12{factor} *{root} (24 - 15){exp} /{root} (17 + 6){exp} ...
        首先解析12, 然后是 *, 这时发现*是root,于是12成为*的第一个子节点
        接着解析(24 - 15)这个exp,同理,24成为-的第一个子节点,然后是15,它不是root,而前一个是,于是15成为-的最后一个子节点。
        因为(24 - 15)这个exp不是root,同样成为了*的最后一个子节点。
        再解析/,是root, 于是把前一节点(是*哦,因为其它的都成了*的子节点)变成了它的首个子节点。
        最后解析(17+6)这个exp,最终成为了/的最后一个子节点。
    
    no_node_d
        忽略由它包裹的规则,比如例子中的:

  1. factor = str_real_p | inner_node_d[('(' >> exp >> ')')];

也可以这样表示:

  1. factor = str_real_p | (no_node_d[ch_p('(')] >> exp >> no_node_d[ch_p(')')]);

infix_node_d
        这个命令会删除其规则匹配出的所有节点中偶数位置上的节点,比如:

  1. rule_t intlist = infix_node_d[ integer >> *(',' >> integer) ];

这条规则将只产生整数数组,偶数位置上的逗号将被删除。
    
    discard_first_node_d/discard_last_node_d
        忽略第一个/最后一个子规则(半个inner_node_d功能)
    
我们的Spirit学习先到这里,这些只不过是Spirit里的冰山一角,要发挥Spirit的强大威力,还得继续前进...
在/libs/spirit/example里有不少“很好很强大”的例子,比如:小型的C语言解释器,XML解释器等,大家有兴趣可以去研究研究。

Boost学习之语法解析器--Spirit的更多相关文章

  1. boost之词法解析器spirit

    摘要:解析器就是编译原理中的语言的词法分析器,可以按照文法规则提取字符或者单词.功能:接受扫描器的输入,并根据语法规则对输入流进行匹配,匹配成功后执行语义动作,进行输入数据的处理. C++ 程序员需要 ...

  2. 用java实现编译器-算术表达式及其语法解析器的实现

    大家在参考本节时,请先阅读以下博文,进行预热: http://blog.csdn.net/tyler_download/article/details/50708807 本节代码下载地址: http: ...

  3. 使用 java 实现一个简单的 markdown 语法解析器

    1. 什么是 markdown Markdown 是一种轻量级的「标记语言」,它的优点很多,目前也被越来越多的写作爱好者,撰稿者广泛使用.看到这里请不要被「标记」.「语言」所迷惑,Markdown 的 ...

  4. Anrlr4 生成C++版本的语法解析器

    一. 写在前面 我最早是在2005年,首次在实际开发中实现语法解析器,当时调研了Yacc&Lex,觉得风格不是太好,关键当时yacc对多线程也支持的不太好,接着就又学习了Bison&F ...

  5. 在.NET Core中使用Irony实现自己的查询语言语法解析器

    在之前<在ASP.NET Core中使用Apworks快速开发数据服务>一文的评论部分,.NET大神张善友为我提了个建议,可以使用Compile As a Service的Roslyn为语 ...

  6. day89 DjangoRsetFramework学习---restful规范,解析器组件,Postman等

     DjangoRsetFramework学习---restful规范,解析器组件,Postman等           本节目录 一 预备知识 二 restful规范 三 DRF的APIView和解析 ...

  7. 语法解析器续:case..when..语法解析计算

    之前写过一篇博客,是关于如何解析类似sql之类的解析器实现参考:https://www.cnblogs.com/yougewe/p/13774289.html 之前的解析器,更多的是是做语言的翻译转换 ...

  8. 手写token解析器、语法解析器、LLVM IR生成器(GO语言)

    最近开始尝试用go写点东西,正好在看LLVM的资料,就写了点相关的内容 - 前端解析器+中间代码生成(本地代码的汇编.执行则靠LLVM工具链完成) https://github.com/daibinh ...

  9. springMVC学习 七 视图解析器

    在springMVC中,如果不配置视图解析器,会走默认的视图解析器,但是有时候配置视图解析器,还有一定的作用 <bean id="viewResolver" class=&q ...

随机推荐

  1. 【转】C#父类与子类的静态成员变量、实例成员变量、构造函数的执行顺序

    原文地址:http://www.xuebuyuan.com/1092603.html Win7+VS2010测试的结果如下: ①子类静态成员变量②子类静态构造函数③子类实例成员变量④父类静态成员变量⑤ ...

  2. Python 小知识点(8)-- __new__

    第一段代码如下: class Foo(object): def __init__(self,name): self.name = name print("Foo __init__" ...

  3. Java-从堆栈常量池解析equals()与==

    一.基本概念 ①JAVA中的基本数据类型(简单类型,内置类型): 字节型(byte),短整型(short),整型(int),长整型(long),字符型(char),浮点型(float),双精度型(do ...

  4. 「小程序JAVA实战」小程序头像图片上传(中)(44)

    转自:https://idig8.com/2018/09/09/xiaochengxujavashizhanxiaochengxutouxiangtupianshangchuan43/ 用户可以上传了 ...

  5. Django xadmin的使用 (一)

    Django  xadmin的使用 xadmin是django的一个第三方的管理后台实现,它的功能比自带的admin功能更加强大. xadmin项目在github上的地址为:https://githu ...

  6. Redis实战——redis主从备份和哨兵模式实践

    借鉴:http://redis.majunwei.com/topics/sentinel.html     https://blog.csdn.net/u011784767/article/detai ...

  7. js中格式化时间

    //时间格式化 Date.prototype.Format = function (fmt) { var o = { "M+": this.getMonth() + 1, &quo ...

  8. MySQL Root密码丢失解决方法总结

    1. 检查my.cnf,看看有没有密码......靠这也算一条啊 2. 如果能够重启,首先使用–skip-grant-tables参数重启,然后改密码,再去掉–skip-grant-tables参数重 ...

  9. java aop 日志打印 正则设置

    package tz.lion.Utils.aop; import com.alibaba.fastjson.JSON;import org.springframework.web.multipart ...

  10. jquery offset positon 获取位置不准的解决方法

    问题: 本地开发时,由于使用了图片,而且使用了offset().top涉及到图片所在的div距离计算的部分,本地开发一切都没问题:但是部署到服务器上时却出现布局错乱,经过排查发现总是少了一个图片高度的 ...