可配置语法分析器开发纪事(六)——构造一个真正能用的状态机(下)
上一篇文章对大部分文法都构造出了一个使用的状态机了,这次主要来讲右递归的情况。右递归不像左递归那么麻烦,因为大部分右递归写成循环也不会过分的让语法树变得难以操作,不过仍然有少数情况是我们仍然希望保留递归的语法树形状,譬如C++的连等操作,因此这里就来讲一下这个问题。
右递归是怎么形成的呢?在这里我们先不想这个问题,我们来看一个普通的文法。在上一篇文章我们已经说过了,如果一条文法有一个非终结符引用了另一条文法,那么就要做一条shift和reduce来从这个状态机穿插到那个状态机上:
在这里需要讲一下,绿色的箭头是shift,紫色的箭头是reduce,他们都是ε边。更进一步说,如果A刚好以B作为结尾,那么A的最后一个输入就不是终结符输入,不过因为她不是右递归,所以现在看起来还没什么问题:
我们已经接近右递归的形状了。右递归的一个根本特征当然是递归(废话)。为了制作一个右递归,我们可以想一下,如果A和B不是两个rule而是同一个rule会怎么样?当然咋这么一看,好像就是A可以访问自己了:
实际上这已经构成了一个ε边的循环。左递归是shift的循环,右递归是reduce的循环,其实他们都一样。那你可能会想,既然左递归和右递归只是相反的情况,为什么左递归处理起来就那么容易,右递归好像就没什么方法呢?其实如果你只是想要检查一个字符串是不是一个文法的其中一个元素而不建立语法树的话,你完全可以把这条循环的ε reduce边给压缩成一条。为什么呢?在之前讲到,我们可以判断一个reduce是不是由左递归造成的,我们也可以判断一个shift是不是由右递归造成的。这种shift只要不压状态进栈,那么右递归的reduce循环不管循环多少次,其实都是pop一个状态出来,于是问题就没有了。等价地,不处理语法树的话,其实左递归也可以用相同的方法处理。
但是一旦当你涉及到创建语法树的问题,你就等于给每一条边都加上了一些semantic actions。这个时候shift和reduce就不是简单地可以互相抵消的关系了,于是你就不能把一个循环的ε reduce边压缩成一条,那怎么办呢?
方法其实很简单,只要我们在状态机走着走着发现无路可走的时候,看看有没有一条右递归reduce可以给我们“试一试”就好了。为什么可以这样做呢?我们还记得,当我们把整个状态及压缩到没有ε边的时候,每一个输入都需要对堆栈的情况进行一次匹配。令人欣慰的事,没有什么边可以跟右递归的reduce边一样产生同样的匹配结构(但是我不想在这里证明),所以这样做是安全的。
到了这里,我们已经把构造不带lookahead状态机的所有情况都说清楚了。一个文法如果需要构造lookahead的话,其实就等于在边的匹配规则里面加上一条对未来的一些token的要求,并没有本质上改变语法分析的结构。但是我们知道,还有两种上下文无关文法是不在这里面的,C语言全占了。我在这里举两个简单的例子:
变量声明:对于一个已经typedef过的结构我们完全可以写出这样的代码:A*B;。这个时候A如果是类型,那这就需要走VariableDeclarationStatement的rule。如果A是一个表达式,那这就需要走ExpressionStatement的rule。但是对于语法分析来说,A就是一个简单的token(除了typedef过的类型以外,所有C语言的类型都是以关键字开头的,所以如果你们想做简单的C语言的parser,就去掉typedef吧,啊哈哈哈哈),在语法分析的时候是无法做出预测的。
这种时候有两种方法,第一种是准备更加丰富的semantic actions,让符号表可以在parse的时候构造出来。那到了这里,我们根据A究竟是不是一个类型,就可以赚到不同的分支上了。另一种就是,我们保留一个AmbiguousStatement的语法树节点,把语法树的一颗子树遇到的不能处理的歧义的情况都写进去。我们可能回想,为什么我们不干脆一个parser返回多个分析结果呢?因为如果不这么做的话,一个函数里面有10个这样子的变量声明,那你就有1024个结果了。如果我们把歧义收缩到一颗子树上,那其实还是1个结果,只是多了10颗子树,效果完全不同。
强制类型转换:写C语言的时候是不可能没有强制类型转换的,但是当parser看到类似这样的代码的时候:(A*****)B,因为类型的结构和表达式的结构是不一样的,但是你这个时候并不能在看到“(”的时候就做lookahead——因为这个lookahead是无限长的,括号里面的表达式或者类型都可以无限长。不过就算你想把他局限成有限长,就算你给100个token,那也会长出成千上万种lookahead的模式,所以在这里我们就不要用lookahead了。
那怎么做呢?我们只需要把这个状态机当成NDA(因为到了这里他已经是NDA了),从deterministic push-down automaton变成了non-deterministic push-down automaton,我们也唯有让我们的parser也变成non-deterministic了。关于这个内容,就等到下一篇——也就是这个系列的最后一篇文章——来详细讲解了。