语法分析程序的设计与实现_编译工程7:语法分析(5)

在本节中,我们将扩展SLR,在输入中向前看一个符号。有两种不同的方法:

  1. 规范LR方法,或直接简称为LR,它充分利用了向前看符号,这个方法使用了一个很大的项集,称为LR(1)项集。
  2. 向前看LR,或称为LALR,基于LR(0)项集,和LR(1)项的典型语法分析器相比,它的状态要少很多。通过向LR(0)项中谨慎地引入向前看符号,使用LALR方法处理的文法比使用SLR方法时处理的文法更多,同时构造得到的语法分析表却不比SLR分析表大。在很多情况下,LALR方法是最合适的选择。

为什么在介绍完SLR之后还要介绍LR和LALR呢?原因是SLR分析器的功能不够强大,不能够记住足够多的上下文信息。二义性的文法肯定不是SLR文法,但是很多不是二义性的文法也不是SLR的。也即,SLR不能处理这些文法。譬如看下面的例子。

以下文法:

S -> L = R | R
L -> *R | id
R -> L

这里的*可以当做是代表“左值指向的内容”的运算符。这个文法可以推导出类似于

a, *a, a = b, *a = b, b = *a, *a = *b

等句子。对这个文法构建LR(0)项集族,如下:

ed3909e3a710778e98cdf054369ead7a.png

考虑项

,这里的
使得Action[2,=]的动作时移入,进入状态6。但同时
使得当遇到=时,应该进行规约(因为FOLLOW(R)中有=)。这里,就产生了移入/归约冲突。

请问:这里冲突的两个动作,应该选择哪一个?为什么?

【这里的主要问题是,如果选择规约,那么接下来会规约成R=;但是状态2由状态0通过接受L而来,在这之前并没有接受*,也即并不会有*R=这样的形式。所以此时,面对=,并不应该选择归约。】

可以使用两个例子看一下。一个是输入字符串为a,一个是输入字符串*a。输入为a的最右推导式这样的:

S -> R -> L -> id

相对应的分析过程是从状态0接受id到状态5;状态5进行规约,状态0接受L进入到状态2;规约进行到状态3;再次规约进入状态1。最后是接受状态。

输入为*a的最右推导式这样的:

S -> R -> L -> *R -> *L ->*id

相对应的分析过程是从状态0接受*到状态4, 状态4接受id到状态5;状态5进行规约,状态4接受L进入到状态8;状态8进行规约,状态4接受R进入到状态7;状态7进行规约,弹出状态4和状态7,状态0接受L进入状态2;进行规约到状态3;再次规约进入状态1。最后是接受状态。

从上面的两个例子可以看出,两次应用R->L进行规约时,R的后继符号都是$。

跟上面的例子相对应的,什么情况下R->L时,R的后继是=呢?在下面的图中,可以看到,为了推导出类似于

*a = b

这样的句子,在=的左右两边分别需要使用

R -> L

这个产生式进行规约。但是在两次规约中,面临的后继符号是不同的。

cdd46b30e8d5afcca0fc12c1e282dc92.png

对于项

的规约,在不同的位置,A会要求不同的后继符号。在特定位置,A的后继符号是FOLLOW(A)的子集。

因此,SLR的主要问题是因为没有考虑上下文,没有考虑到栈中的已有状况,而简单根据FOLLOW集进行了归约。为了解决这个问题,需要对状态进行细分,确保在特定的输入的情况下,才选择归约动作。【上面的例子中,在$时,才应进行归约。】

结合上面的状态图在做一下。

输入为*a = b的最右推导式这样的:

S -> L=R -> L=L -> L=id -> *R=id -> *L=id ->*id=id

相对应的分析过程是从状态0接受*到状态4, 状态4接受id到状态5;状态5进行规约,状态4接受L进入到状态8;状态8进行规约,状态4接受R进入到状态7;状态7进行规约,弹出状态4和状态7,状态0接受L进入状态2;状态2接受=进入状态6;状态6接受id进入状态5;状态5进行规约,状态6接受L进入到状态8;状态8进行规约,状态6接受R进入到状态9;状态9进行规约,弹出L=R,状态0接受S进入状态1。最后是接受状态。

这样的文法可以使用接下来要讲的LR来解决。

主要思想是在状态中包含更多的信息,这样就能够排除掉一些不正确的规约。也即,需要分裂某些状态,让LR语法分析器的每个状态精确地指明那个输入符号可以跟在句柄的后面,从而可以进行规约。

为了将这个额外的信息加入状态中,需要对项进行处理,使得它包含第二个分量,这个分量的值是一个终结符号。项的一般形式是

,其中
是一个产生式,而
是一个终结符号或者右端结束标记$。这样的项称为LR(1)项,其中1指的是第二个分量的长度,第二个分量称为这个项的
向前看符号。在形如
不为空的时候,向前看符号没有任何作用,但是在
的项中,只有在下一个输入符号等于
时,才选择进行规约。这样的
是FOLLOW(A)的子集,而且往往是真子集。

cf11c35ed1587765b58db08d6c3af006.png

在理解了第二个分量的作用后,接下来就是如何构造第二个分量的问题了。可以看龙书中给出的算法。

d59795253bbd4a4fbe63dd12a414b008.png

算法一眼看上去不好理解。下面是简化的理解。

算法中items部分在将$符加入到

之后, 和之前的是一样的;Closure的部分是主要的区分点。也可以将Closure部分认为是在生成向前看符号【搜索符号】的主要部分。在对非终结符号进行扩展的时候,要看到原始的产生式中该非终结符号之后的内容。之所以是FIRST(
),主要是考虑到
为空或者可以推出空的情况,此时相当于向前看符号
进行了传播和继承。

【为什么是这样?考虑到FIRST集和FOLLOW集的关系,此时产生的搜索符号肯定是FOLLOW集的子集】

GOTO的部分主要是继承,此时不涉及生成新的向前看符号。

【大概是龙书的作者也觉得这个算法不好懂,在下面的例子对算法的使用进行了体贴入微的解释;相当感人;课上如果没听懂,可以去看看书】

S' -> S
S -> C C
C -> c C | d

因为排版困难,这里就总而言之了。使用这个算法,下面的文法所产生的LR(1)的GOTO图如下:

5be2b7e6fd6034b80be7cf967b839309.png

强调一下,在每一个状态中,第一项都是使用GOTO的算法,属于传播,所以向前看符号没有变化;下面的项都采用的是CLOSURE算法,取决于

是否为空,向前看符号可能产生变化。

我们可以通过同时构造SLR项集,来对于LR和SLR的区别。主要的不同在于,譬如I4和I7,它们的产生式都是一样的,而向前看符号不同。所以,相当于是对SLR的项集进行了分裂,从而导致动作更精细。

规范LR语法分析表的构造算法如下。

df790d1117113876c977d451426f4854.png

使用上述方法生成的LR(1)的分析表如下所示:

09b3946852372976ccd32c557109b005.png

练习:

对于下面的文法构造LR(1)集。

S -> L = R | R
L -> *R | id
R -> L

答案:

b1ce529c7c241ac54e37b9045e48c2e9.png

接下来讨论一下LALR。(向前看LR)LALR经常在实践中使用,因为它得到的分析表比规范LR分析表小很多,而且大部分常见的程序设计语言构造都可以方便地使用LALR文法表示。【其实,对于SLR,这一点也基本成立】

一个文法的SLR和LALR分析表总是具有相同数量的状态,对于像C这样的语言,通常有几百个状态。对于同样大小的语言,规范LR分析表通常有几千个状态。

LALR文法的构造,我们来考虑下上面的文法。就像上面所讨论的,

的第一个分量是相同的,区别是向前看符号。LALR中会将这两项进行合并。合并后的状态会在所有的终结符输入都进行归约。虽然对有的输入如ccd和cdcdc,原有的分析器会报错,但是新的分析器却会进行归约,但是新的分析器最终能够找到这个错误,并且在移入新的符号之前就会报错。

32c2d5d110f37c10c373b2a067664d67.png

这里我们简单讨论下在面临ccd的时候,LR(1)自动机的动作,以及LALR自动机的动作。LR(1)的话,

0 -> 3 -> 3 -> 4 

状态4的时候进行规约,结果发现接下来的字符是$,而此时,在接下来的输入字符是c/d的时候才应该进行规约,直接报错。

对于LALR

0 -> 3 -> 3 -> 4 
0 -> 3 -> 3 -> 8
0 -> 3 -> 8
0 -> 2

状态4的时候进行规约;状态8也需要进行规约;再次进入状态8;再次规约;进入状态2。此时报错。

类似地,对于cdcdc,LR(1)会在进入状态7试图规约的时候报错;而LALR会在规约到状态5之后,才发现错误。

更一般地,可以寻找具有相同核心(core)的LR(1)项集,并将这些项集合并为一个项集。项集的核心就是第一个分量。除了

之外,
也是相同核心。

上面文法对应的LALR分析表如下所示:

ae69b50d4cfd72c07bbdedb480ec007f.png

合并之后会导致的问题是,将原来的相同核心的状态替换为它们的并集之后,那么得到的并集有可能产生冲突。可以证明,合并之后不会引入原有状态中没有的新的移入/归约冲突,因为核心集是相同的,如果合并前没有移入/规约冲突,也即,要么是移入,要么是规约,那么合并之后也不会有移入/规约冲突。另外,直观上讲,移入动作仅由核心决定,不考虑向前看符号。

但是,合并项集确实可能导致新的归约/归约冲突。譬如,考虑下面的文法:

S' -> S
S -> a A d | b B d | a B e | b A e
A -> c
B -> c

该文法产生四个串acd、ace、bcd和bce。可以构造出这个文法的LR(1)项集,并没有任何冲突,因此是LR(1)的。然而可以发现项集{[A->c·,d],[B ->c·,e]}和项集{[A->c·,e],[B ->c·,d]}是同核心的,然而分别对应着acd/ace和bcd/bce的推导。如果将它们进行同心集合并,那么会出现出现d/e时可以使用两个产生式进行归约。

练习:

  1. 说明下面的文法
S -> A a| b A c | d c| b d a
A -> d

是LALR(1)的,但不是SLR(1)的。

2. 说明下面的文法

S -> A a | b A c | B c | b B a
A -> d
B -> d

是LR(1)的,但不是LALR(1)的。


使用二义性文法

每个二义性文法都不是LR的,因此二义性文法不在我们之前讨论的文法之列。然而,某些类型 的二义性文法在语言的规约和实现中很有用。二义性文法可以对文法进行特定的优化。虽然文法是二义性的,但是在实现的时候可以给出消除二义性的规则,这样语言的规约在整体上是无二义性的,有时候也可以构造出遵循二义性解决方案构造出LR语法分析器。

譬如,以下我们很熟悉的二义性文法:

E -> E + E | E * E | (E) | id

所得到的的LR(0)项集是:

7cde2bca2a18751d8094e23f870e7253.png

在这里,我们可以发现,在状态

和状态
中,当向前看到*/+时,会出现移入和规约冲突。

在之前介绍LL文法时,我们讨论了消除二义性的方法。通过使用

以及
,指定了+和*的优先级和结合性。同时使用
也保证了优先级和结合性。而这些单表达式的归约会浪费时间。

在遇到冲突的时候,可以通过明确地指定优先级和结合性来解决。譬如在输入id+id*id的时候,在根据上面的分析器,处理完id + id之后,进入状态7。

5d1c1acd2d101a171ebe7ddcc8061959.png

此时会产生归约和移入的冲突。在进行选择的时候,因为明确指定了*的优先级高于+,所以此时应该选择移入而不是归约。

假如输入是id + id + id,那么在处理完id + id之后,同样会面临冲突,但是因为指定了+是左结合的,那么此时应该选择归约而不是移入。

类似地,状态8同样存在冲突,但是*的优先级比较高,所以选择了归约。

我们使用bison的时候,也是这个思路。

%left '+' '-'
%left '*' '/'

%%

S   :   S E 'n'        { printf("ans = %lfn", $2); }
    |   /* empty */     { /* empty */ }
    ;

E   :   E '+' E         { $$ = $1 + $3; }
    |   E '-' E         { $$ = $1 - $3; }
    |   E '*' E         { $$ = $1 * $3; }
    |   E '/' E         { $$ = $1 / $3; }
    |   T_NUM           { $$ = $1;  printf(" val is %lf.n",$1.val);}
    |   '(' E ')'       { $$ = $2; }
    ;

%%

按照这个思路,上述二义性文法的语法分析表如下:

897c9054b149b813f8e5f96f4dd9f9d0.png

悬空-else的二义性

再次考虑下面的条件语句文法:

242516ffd5452a905fa34343024adf8f.png

可以重写该文法:

d3989063defa40e31c1ba50d9e05d08c.png

它对应的LR(0)项集是:

8867e8dd817a93aea743f54cae1ac4e0.png

类似地,可以指定在

遇到冲突移入和归约的时候,选择进行移入。

为什么呢?考虑下 if if S else S 的情况,在读入if, if,之后可以进入到状态4;此时如果规约的话,意味着 else和外层的 if进行配对;如果选择移入的话,就是和内层的if进行配对。

这样,新生成的LR分析表如下,

75b13bbd9b2b4faad5a7397f28ec408a.png

在处理输入iiaea【if,if,else】的时候,语法分析动作如下:

b8b6a767cb39cf036df1a3356d31914c.png

我们使用bison写if语句的时候可以这样:

%nonassoc LOWER_THAN_ELSE
%nonassoc ELSE

if_block:  IF '(' expression ')' stmt   %prec LOWER_THAN_ELSE  
           |IF '(' expression ')'stmt  ELSE stmt

                      

这里ELSE和LOWER_THAN_ELSE的结合性其实并不重要,重要的是当语法分析程序读到 IF LP Exp RP时,如果它面临归约和移入ELSE这两种选择,它会根据优先级自动选择移入 ELSE。


总结:

1336f6a7e4e6cd51a3521ebd20249750.png

LL(1)分析法是自上而下的分析法。LR(0),LR(1),SLR(1),LALR(1)是自下而上的分析法。
自上而下:从开始符号出发,根据产生式规则推导给定的句子。
自下而上:从给定的句子规约到文法的开始符号。

SLR(1)与LR(0):简单的LR语法分析技术(即SLR(1)分析技术)的中心思想是根据文法构造出LR(0)自动机。

LR(0):见到First集就移进,见到终态就归约

SLR(1)见到First集就移进,见到终态先看Follow集,与Follow集对应的项目归约,其它报错。

规范LR(1)语法分析技术的中心思想是根据文法构造出LR(1)自动机 ,规范LR(1)自动机构造方法和LR(0)自动机的构造方法相同,只是多增加了向前搜索符号。

LALR(1)是对LR(1)项集族I中具有同心项的项集进行合并得到I',然后根据I’进行分析的方法。

再看一个例子,理解一下向前看。

下面的文法不是LR(1)文法,对它略作修改,使之成为一个等价的LR(1)文法。

begin declist ; statement end

修改之后的文法是:

begin declist statement end

分析:

该文法产生的句子的形式是:

begin d; d; d;… d; s; s; s;…s end

这个句子进行规约,应该是在看到第一个s的时候开始进行规约;但是按照第一种文法,;同时用作d的分隔符,也用作declist和statement的分隔符。读入d之后,无法只根据后面的一个;判断到底是将d规约成declist,还是继续读入(移入—规约冲突);也即它无法判断当前的这个d是不是最后一个d。

如果改成第二种文法,看到分号肯定是要移入的;然后在看到第一个s的地方开始进行规约。这样,只朝前看一个字符就行了。

如果是第一种文法,朝前看几个字符也没有移入-规约冲突?

参考:

  1. https://blog.csdn.net/jzyhywxz/article/details/78443192
  2. https://www.cnblogs.com/Alexkk/p/6033159.html
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值