最近为了解析SQL语法,怀着试一试的心态去翻了翻boost的spirit库,因为该库的文档的简介里写着LL parser framework represents parsers directly as EBNF grammars in inlined C++。看着framework这个词自然觉得这个库很牛B,试用了一下果然如此。
所谓EBNF即扩展巴克斯范式,是一种描述Context-Free Language的文法。在目前常见的非自然语言中,大部分都可以用EBNF表示。例如:
group ::='('exp ')'
factor ::=integer| group
term ::=factor(('*'factor)|('/'factor ))*
exp ::=term(('+'term)|('-'term ))*
这是一个整数表达式的EBNF。该段描述用spirit在C++中的实现则是:
rule<> group, factor, term, exp;
group = '(' >> exp >> ')';
factor = int_p | group;
term = factor >> *(('*' >> factor) | ('/' >> factor));
exp = term >> *(('+' >> term) | ('-' >> term));
这里使用=代替::=, 用>>代替空格连接。并且由于C++语法所限,EBNF中后置的*在spirit中改为前置。
等式左边的单词被称为一个rule,等式右边为rule的定义。我们可以看出一个group是一个exp加上一对括号,一个factor是一个整数或者一个group,一个term是一个或多个factor用*/连接,一个exp是一个或多个term用+-连接。处于最顶端的exp可以据此识别出以下表达式
12345
-12345
+12345
1 + 2
1 * 2
1/2 + 3/4
1 + 2 + 3 + 4
1 * 2 * 3 * 4
(1 + 2) * (3 + 4)
(-1 + 2) * (3 + -4)
1 + ((6 * 200) - 20) / 6
(1 + (2 + (3 + (4 + 5))))
得到一个rule之后,我们就可以用 parse函数对一个串进行识别了。例如
parse( " (1 + (2 + (3 + (4 + 5)))) " , exp);
该函数返回一个结构parse_info,可以通过访问其中的full成员来判断是否成功识别,也可以访问stop成员来获知失败的位置。这里要特别提一点,关于各个符号之间的空格,spirit的文档的正文说的是给parse再传一个参数space_p,通知parse跳过所有的空格,然而在FAQ中又提到,如果使用以上方法定义rule,第三个参数传space_p会失败。原因是使用rule默认定义的规则被称为character level parsing,即字符级别解析,而parse的第3个参数仅适用于phrase level parsing,即语法级别解析。要使用第3个参数可以有几种方法。
1。在parse的第二个参数直接传入一个EBNF表达式,不创建rule对象。
parse( " hello world " , * anychar_p, space_p);
2。以rule<phrase_scanner_t>创建rule。
rule < phrase_scanner_t > exp;
注意虽然可以用这两个办法屏蔽空格,但是这样可能完全改变EBNF文法的语义,尤其是在语言本身需要识别空格的时候。对于这种情况,可以不使用第三个参数,并在需要出现空格的地方加上space_p,或者+space_p及*space_p,其中+和*分别表示后面的符号连续出现一次以上和0次以上。例如一个以空格分隔的整数列表可以写成int_p >> *(+space_p >> int_p)
如上使用parse可以识别一个串,但并不能做更多的操作,例如将语法里的各个成分提取出来。对于这样的需求,可以通过actor实现。下面是使用actor的一个简单例子
bool
parse_numbers(char const* str, vector<double>& v)
{
return parse(str,
// Begin grammar
(
real_p[push_back_a(v)] >> *(',' >> real_p[push_back_a(v)])
)
,
// End grammar
space_p).full;
}
注意到real_p后面的[],中括号里面是一个仿函数(函数指针或者函数对象),该仿函数具有如下调用型别
void operator()(IterT first, IterT last) const;
void operator()(NumT val) const;
void operator()(CharT ch) const;
一旦spase发现了匹配real_p的子串,就会调用该functor。不同的rule可能会对应不同的调用型别。
第一个型别针对一般规则,first和last为两个指向字符的迭代器(一般为char*),匹配的子串为[first, last)
第二个型别针对数字型规则,如real_p和int_p, 参数val是一个数字类型。
第三个性别针对单字符型规则,如space_p, 参数ch是一个字符类型。
real_p[push_back_a(v)]中的push_back_a是一个spirit已经定义好的functor,它会将匹配好的内容依照匹配到的时间顺序调用v的push_back函数加入到v中。
到此spirit的常用功能就都介绍完了。要详细深入了解可以参考spirit的文档。
最后在题一个注意要点。spirit的各种EBNF连接都是指针连接,因此才能在expression被赋值前就在group的定义里面使用。所以在使用EBNF的时候一定要小心不要将局部变量的rule提供给全局或者类成员变量使用,例如:
class A
{
rule<> s;
A()
{
rule<> r = int_p | hex_p;
s = r >> *(+space_p >> r); //error, r destructed after return
}
};
如果真想使用局部作用域,可以在局部的rule前面加上static.
posted on 2005-12-18 12:02
shifan3 阅读(7084)
评论(5) 编辑 收藏 引用 所属分类:
template 、
Boost 、
C++