LEMON源码分析笔记——压缩分析表
2011-4-1 陕师大长安区 小雨
在几乎没有注释的情况下分析此段代码,是一件令人头疼的事!分析表本来是一个二维数组。为了节省空间,LEMON对分析表进行了巧妙的压缩。
压缩过程
一、 线性化二维表
分析表的行标为状态,列标为文法符号的编号。假设有m个状态,与n个文法符号。则每个状态对应一个长度为n的一维数组,这里把它叫状态数组,分析表为二维数组:
S0 S1
Sm-1 | D0 | … | Dn-1 |
D0 | … | Dn-1 | |
… | … | … | |
D0 | … | Dn-1 |
压缩最后要得到一个一维数组。这里称之为线性表。压缩第一步将二维数组线性代。
S0 S2 … Sm-1 | |||||||||
D0 | … | Dn-1 | D0 | … | Dn-1 | … | D0 | … | Dn-1 |
二、 削两端
状态数组存放的是可接收文法符号的动作。显然,状态不可能一定能接收所有文法符号。也就是说状态数组中,可能有没有动作的元素,这里称之为空档(嗜打球,惯用空档一词,呵呵)。削两端的意思是,在状态数组并入线性表之前,削去两端空档。要注意的时,中间还可能存在空档。
比如某一状态数组如下,蓝色表示已经存放了动作,白的表示空档。
|
|
|
|
|
|
|
|
削去两端之后为:
|
|
|
|
削去两端之后,带来索引麻烦是显然的。经过第三步处理之后,我们才着手解决这个问题。
三、 犬牙交错或融入其中
削完两端之后,每个状态数组中依然可能存在着空档。不能让这些空档在那耗着呀。为了利用这些空档,在状态数组依次并入过程中,如果存在一个块区域,其中的空档可以放入削去两端后的状态数组,那么这个数组就放入这块区域,使得状态数组之间形成犬牙交错的状态。
下面是并入过程中,线性表的某一状态
|
|
|
|
|
|
|
| null |
| null | null |
|
此时要加入前面示例中的状态数组,此时标有null的区域刚好可以放下并入数组,于是并入此区域。
若当状态为:
|
|
| V |
| V | V |
|
|
|
|
|
|
若标有V字样的区域与待并入状态数组完全一致,就将状态数组融入其中。
问题及解决方案
一、 从何下手
削去了状态数组两端空档后,将状态将如何索引线性表呢?用一个变量mnLookahead记下状态数组前端削去的空档个数。再记下状态数组插入点的位置i,那么状态数组中的元素在线性表中就从i-mnLookahead数起了。可以将这个偏移量i-mnLookahead作为状态的属性。当要在线性表中索引状态数组文法符号j的动作时,只需将j加上状态偏移量作索引即可。
二、 来者不拒
状态数组中的空档并不是没有用的,空档表示文法符号不被状态接收。于是状态数组中的空档不占用则罢了,一旦被占用,万一碰到被占用空档所对应文法符号,岂不是误解成可接收符号!导致一些本来状态拒绝的文法符号被接收了。解决的方法是,在动作中增加一个属性——lookahead。用来表示该动作处理文法符号的编号。这个属性在没有压缩的二维表中是没须要的。因为动作的索引号就是文法符号的编号。当任取一个文法符号j塞给一状态时,可以由状态取得偏移量,再加上文法符号编号,再到线性表中索引得动作,若动作的lookahead域与j不同,说明这个位置不是此状态赋值的,是由别的状态填入的。这个文法符号是不能接收的。每次“问取”某个文法符号动作时,都会进行这样的判断,下面是yy_find_reduce_action中的一个语句:
assert( yy_lookahead[i]==iLookAhead );
问题还是显然的,万一别的状态填入的动作的lookahead也跟j一样,那么不一样出错吗?
三、 鸠占鹊巢
自家的空档被它人占用了,别人还假冒成自家人,这是新问题所在。
先看看状态数组融入线性表中的情况。融入不会对已经并入线性表中的元素产生任何影响,因为没有添加任何新元素,也就不可能占用别人家的空档。此时需要考虑的是,没有没可能被线性表已有的动作占了自己的空档。查找线性表中,是否还有动作会被当成自己人,只要有,那就不能融入,换个位置再试。
这段代码反映了这个事实(acttab_insert函数中):
for(j=0; j<p->nAction; j++)
{
if( p->aAction[j].lookahead<0 ) continue;
if( p->aAction[j].lookahead==j+p->mnLookahead-i ) n++; }
if( n==p->nLookahead )
{
break; /* Same as a prior transaction set */
}
以犬牙交错方式嵌入时,是考虑的是自己是否会破坏线性表,还是线性表是否会欺骗自己呢?如果考虑自己是否会破坏线性表的话,你将会不堪其扰。因为你得为前面并入的每个状态数组考虑。于是,我们转向考虑,线性表是否会有动作欺骗自己。若有的话,换下一个位置试试。但这样做,对已并入状态是否会产生影响呢?答案是否定的。因为若新来状态数组中有一个位置占用并欺骗了另一个状态数组。那么这两个状态一定重叠,也就是新状态一定不会挑中这个位置的。所以新状态数组不可能欺骗到线性表中已有的任何一个状态数组。
关键源码如下(acttab_insert函数中):
for(j=0; j<p->nAction; j++)
{
if( p->aAction[j].lookahead==j+p->mnLookahead-i ) break;
}
if( j==p->nAction )
{
break; /* Fits in empty slots */
}
最头疼的莫过于这句话:
p->aAction[j].lookahead==j+p->mnLookahead-i
这块代码就就连《LEMON语法分析生成器源代码情景分析》作者虞森林,也没有解释好,还说是作者留下的“盲肠”。这句话其实是在判断线性表中是否有会欺骗状态数组的动作。这样写可能好理解一些:
yy_lookahead[i-p->mnLookahead+k]==k//状态数组的第k个元素是k的话就会有欺骗
但要遍历的不是状态数组呀,而是线性表。于是作一个“简单的”变量替换:
令j = i-p->mnLookahead+k则k = j+p->mnLookahead-i,再代入上式,不就明白了吗?
好一个变量替换,整整弄了两天才想通。看来代数确实推动了数学的发展,这么 “复杂”的问题用几个式子就搞定了。