Vczhl Library++3.0之Parser Combinator为常见的语法结构做优化
前曾经为Parser Combinator写过一篇教程。这次为了处理Vczh Library++新设计的ManagedX托管语言,我为Parser Combinator新增了三个组合子。
第一个是def,第二个是let。它们组合使用。def(pattern, defaultValue)的意思是,如果pattern成功了那么返回pattern的分析结构,否则返回defaultValue。let(pattern, value)的意思是,如果pattern成功了则返回value,否则失败。因此他们可以一起使用。举个例子,ManagedX跟C#一样具有5种type accessor:public, protected, protected internal, private, internal。其中四种accessor的文法类型是token,剩下的protected internal则是tuple<token, token>。因此我们无法很方便地为它写一个记号到语法树的转换函数。而且对于缺省情况要返回private的这种行为,在EBNF+handler上直接表达出来也比较困难。当def和let还不存在的时候,我们需要这么写:
accessor = (PUBLIC[ToAccessor] | PROTECTED[ToAccessor] | PRIVATE[ToAccessor] | INTERNAL[ToAccessor] | (PROTECTED + INTERNAL)[ToProtectedInternal])[ToAccessorWithDefault];
这个时候我们需要创建三个函数,分别是ToAccessor、ToProtectedInternal和ToAccessorWithDefault。因为accessor本身不是一个重要的语法元素,所以我们不需要为accessor记录一些源代码的位置信息。表达式则需要位置信息,这可以在我们产生错误信息的时候知道错误发生在源代码中的位置。而accessor总是直接属于某一个重要的语法元素的,所以不需要保存。如果不需要保存位置信息的话,那么一个ToXXX的函数其实就是没有必要的。这个时候可以让def和let来简化操作:
accessor = def(let(PUBLIC, acc::Public) | let(PROTECTED, acc::Protected) | let(PRIVATE, acc::Private) | let(INTERNAL, acc::Internal) | let(PROTECTED+INTERNAL, acc::ProtectedInternal), acc::Private);
看起来好像差不多,但实际上我们已经减少了那三个不需要存在的函数。
============================易做图的分割线====================================
第三个是binop。做这个主要是因为那个通用的lrec(左递归组合子)在对付带大量括号的表达式的时候性能表现不好。这里稍微解释一下原因。假设我们的语言有>、+、*和()四种操作符,那文法一般都写成:
exp0 = NUMBER | ( exp3 )
exp1 = exp1 * exp0 | exp0
exp2 = exp2 + exp1 | exp1
exp3 = exp3 > exp2 | exp2
因此可以很容易的知道,当我们分析1*2*3的时候,走的是下面的路子:
exp3
= exp2
= exp1
= exp1 * exp0
= exp1 * exp1 * exp0
= 1 * 2 * 3
现在我们做一个简单的变换,把1*2*3变成((1*2)*3)。意义不变,但是分析的路径却完全改变了:
exp3
= exp2
= exp1
= exp0
= ( exp3 )
= ( exp2 )
= ( exp1 )
= ( exp1 * exp0 )
= ( exo0 * exp0 )
= ( ( exp3 ) * exp0 )
= ( ( exp2 ) * exp0 )
= ( ( exp1 ) * exp0 )
= ( ( exp1 * exp0 ) * exp0 )
= ( ( exp0 * exp0 ) * exp0 )
= ( ( 1 * 2 ) * 3 )
咋一看好像没什么区别,但是对于ManagedX这种有十几个优先级的操作符的语言来说,如果给一个复杂的表达式的每一个节点都加上括号,等于一下子增加了上千层文法的递归分析。由于Parser Combinator是递归向下分析器,因此路径有这么长,那么递归的层次也会有这么长。而且为了避免boost::Spirit那个天杀的超慢编译速度的问题,这里牺牲了一点点性能,将组合字的Parse函数做成了虚函数,所以编译速度提高了超多。一般来说一个需要编译一个半小时的boost::Spirit语法分析器用我的库只需要几秒钟就可以编译完了。不过现在却带来了问题。括号一多,性能下降的比较明显。但是我们显然不能因噎废食,因此我决定往Parser Combinator提供一个手写的带优先级的左右结合一二元操作符语法分析器。为了将这个手写的分析器插入框架并变得通用,我决定采用下面的结构。下面的代码是从ManagedX的语法分析器中截取出来的: 1 expression = binop(exp0)
2 .pre(ADD_SUB, ToPreUnary).pre(NOT_BITNOT, ToPreUnary).pre(INC_DEC, ToPreUnary).precedence()
3 .lbin(MUL_DIV_MOD, ToBinary).precedence()
4 .lbin(ADD_SUB, ToBinary).precedence()
5 .lbin(LT << LT, ToBinaryShift).lbin(GT >> GT, ToBinaryShift).precedence()
6 .lbin(LT, ToBinary).lbin(LE, ToBinary).lbin(GT, ToBinary).lbin(GE, ToBinary).precedence()
7 .post(AS + type, ToCasting).post(IS + type, ToIsType).precedence()
8 .lbin(EE, ToBinary).lbin(NE, ToBinary).precedence()
9 .lbin(BITAND, ToBinary).precedence()
10 .lbin(XOR, ToBinary).precedence()
11 .lbin(BITOR, ToBinary).precedence()
12 .lbin(AND, ToBinary).precedence()
13 .lbin(OR, ToBinary).precedence()
14 .lbin(QQ, ToNullChoice).precedence()
15 .lbin(QT + (expression << COLON(NeedColon)), ToChoice).precedence()
16 .rbin(OPEQ, ToBinaryEq).rbin(EQ, ToAssignment).precedence()
17 ;
binop组合子的参数代表整个带优先级的最高优先级表达式组合字(参考上面给出的>+*()文法,可以知道这里的exp0是什么意思)。binop给出了四个子组合子,分别是pre(前缀一元操作符)、post(后缀一元操作符)、lbin(左结合二元操作符)和rbin(右结合二元操作符)。precedence代表一个优先级的所有操作符定义结束。这里我做了一个小限制,也就是每一个precedence只能包含pre、post、lbin和rbin的其中一种。实践表明这种限制不会带来任何问题。因此这里我们得到了一张操作符和优先级的关系表。到了这里我们就可以在Parser Combinator的框架下写一个手写的语法分析器(下载源代码并打开LibraryCombinator\_Binop.h)来做了。至于如何手写语法分析器,我之前给出了一篇文章,大家可以参考这个来阅读_Binop.h。
binop比起简单的用lrec做同样的事情,性能在debug下提高了100多倍,release下面则少一点。到了这里,Parser Combinator重新满足了性能要求,我们可以放心大胆的用一点点无所谓的性能换取一千多倍的编译时间了。在这里贴出当binop还没出现的时候我用lrec给出的操作符文法的实现: 1 exp1 = exp0
2 | ((ADD_SUB | NOT_BITNOT | INC_DEC) + exp1)[ToUnary]
3 ;
4
5 exp2 = lrec(exp1 + *((MUL_DIV_MOD + exp1)[ToBinaryLrec]), ToLrecExpression);
6 exp3 = lrec(exp2 + *((ADD_SUB + exp2)[ToBinaryLrec]), ToLrecExpression);
7 exp4 = lrec(
补充:软件开发 , C语言 ,