编译原理_复习笔记5章

版权声明:本文为博主原创文章,未经博主允许不得转载。(Copyright © https://blog.csdn.net/s_gy_zetrov. All Rights Reserved) https://blog.csdn.net/S_gy_Zetrov/article/details/82315340

第五章 自下而上语法分析

自下而上分析法(Bottom-up)

  • 基本思想:从输入串开始,逐步进行“归约”,直到文法的开始符号。即从树末端开始,构造语法树。核心问题:确定可归约串。
    • 所谓归约,是指根据文法的产生式规则,如果一个串中出现了产生式的右部,那就把那个产生式的右部替换成该产生式的左部符号。
  • 两类自下而上分析方法
    • 算符优先分析法:按照算符的优先关系和结合性质进行语法分析。适合分析表达式。
      • 最左素短语作为可归约串
    • LR分析法:规范归约–确保每次归约都是用当前句子的句柄进行归约。
      • 规范归约的过程从开始符号这样倒过来写完,就是规范推导的过程(而且因为句柄都是最左边的,所以倒过来后的规范推导是最右推导)
      • 句柄作为可归约串

“移进–归约”思想进行自下而上分析:把输入串一个一个移进栈中,当栈顶形成某个产生式的右部时,就把那部分替换为产生式的左部符号。到栈里只剩一个文法开始符号且串已经分析完毕,则返回“输入串是合法符号”

  • 短语:需要归约的目标。定义是设S是文法G开始符号,假设αßδ是文法G的一个句型(α、ß、δ均为任意长度串),如果有A=*=>ß,即ß可以归约为A,并且归约为A后,αAδ仍然是文法G的一个句型,则ß称为句型αßδ对非终结符A的短语
  • 句柄:特别的,如果上面短语定义中的ß可以一步直接归约成A,而不是A=*=>ß这样多步,则此时ß为句型αßδ对规则A->ß的直接短语(对应短语,短语是需要归约的目标,直接短语是马上就要归约的所有目标,因为一步就能归约),而一个句型的最左直接短语称为该句型的句柄(马上就要归约的那一个目标)。
  • 快捷找短语、直接短语、句柄:把问的句型的语法树画出来,以非终结符为根的两代以上的子树的所有末端结点从左到右排列就是相对于该非终结符的一个短语,如果子树只有两代,则该短语就是直接短语
  • 通过不断画语法树找句柄来对句子进行归约,归约后继续画语法树找句柄…(因为句柄是马上要归约的短语)
    • 演示目的,实际不可能画出语法树,因为语法分析的最终目的就是画出语法树,怎么可能一开始就把语法树画出来,那还用算什么
  • 规范归约是最右推导的逆过程,规范归约是最左归约,规范推导是最右推导
  • 凡是由规范推导(最右推导)推出的句型成为规范句型
    • 规范句型句柄后都是终结符
  • 规范归约中得到的每一个串都是规范句型,因为规范归约是规范推导的逆过程

符号栈的使用:

步骤    符号栈  输入串          动作
0       #       i1*i2+i3#      预备
1       #i1     *i2+i3#         进
2       #F      *i2+i3#         归,用F->i
...     ...     ...             ...
13      #E      #               归,用E->E+T

算符优先分析法

为什么有算符优先分析法?首先以四则运算为例,是有固定顺序的。但是对于二义文法中的四则运算,却有不同的计算顺序。这是由不同的归约顺序造成的。不同的归约顺序导致了不同的语法树,也就导致了不同的语义解释,这也就导致了最后计算出来的值不一样,因为你先算加减再算乘除和先算乘除再算加减最后结果肯定不一样啊。

但是如果规定了算符的优先次序,并要求要按这种规定进行归约,则归约过程是唯一的,结果也是一致的。

算符优先分析就是找算符(终结符)之间的优先关系,根据优先关系找可归约串。

我们说过自下而上的语法分析核心是确定可归约串,规范归约是根据句柄来确定,算符优先分析则是根据算符之间的优先关系来确定可归约串(而且这个优先关系可以由产生式本身反映出来)。

优先关系分为低于、高于、等于:表示在相邻出现的情况下(如终结符a、b,…ab…和…aQb…都叫相邻出现,Q为一个非终结符),谁先归约,谁后归约。注意,优先级相等是ab一起归约的意思。

  • 适合算符优先分析的文法–算符文法:任何产生式的右部不可能出现两个非终结符并列靠在一起,也就是任何两个非终结符中间一定有一个或者多个终结符靠在一起

    • 即不能出现…PQR…,比如四则运算语法,要表示1+2不可能直接写成12,中间一定要有‘+’这个终结符
    • P、Q、R:非终结符
    • a、b:终结符
    • …非终结符和终结符组成的任意串,包含空字 ℇ
  • 具体什么时候是优先级相等、低、高呢

    • 文法G中含有像P->…ab…或者P->…aQb…这样的产生式(Q是一个非终结符)则a优先级等于b
    • 文法G中含有像P->…aR…的产生式,并且R=+=>b…或R=+=>Qb…(R一步或多步推出b开头的串或一个非终结符接b开头的串)则a优先级低于b(a ⋖ b)
    • 文法G中含有像P->…Rb…的产生式,而R=+=>…a或R=+=>…aQ(R一步或多步推出a结尾的串或a接一个非终结符结尾的串)则a优先级高于b(a ⋗ b)
    • 如果文法G中任意终结符的对(a,b)要么没有,要么只有以上三种优先级关系中的一种,则G是算符优先文法(算符文法基础上加入了”优先级最多一种”的要求,也就是优先关系表中无冲突)

用自己的话总结FIRSTVT、LASTVT集求法、优先关系表的构造

  • 算符优先关系表
    • 行名:在左边的终结符、列名:在右边的终结符。
    • 优先级相等的格子很好找,直接扫描一遍所有产生式,找所有像P->…ab…或者P->…aQb…这样的产生式就知道(a,b)那个格子应该填相等。
    • 优先级高低则需要再次引入集合的概念。如果我能够把P->…aR…中R推出所有串中排在第一个位置的终结符或者排在第二个位置上的终结符(此时第一个位置要求是非终结符),把这些所有的终结符构成一个集合,取名为FIRSTVT集,这样找P->…aR…时只要看FIRSTVT(R)中的元素,在a的右边,那么此时就直接在优先关系表中对应的格子里面填入优先级a小于FIRSTVT(R)中元素即可( ⋖ )
    • 同理,定义一个P->…Rb…中R推出所有串中最后出现的或倒数第二个出现的终结符(此时倒数第一个要求是非终结符),把这些所有的终结符构成一个集合,取名为LASTVT集,这样P->…Rb…时看LASTVT(R)中的元素,在b的左边,那么此时就直接在优先关系表中对应的格子里面填入优先级LASTVT(R)中元素大于b( ⋗ )
  • 所以说就是不断遍历产生式,对于计算FIRSTVT(P):
    • 若有产生式P->a…或P->Qa…则 a ∈ FIRSTVT(P)
    • 若a ∈ FIRSTVT(Q),且有产生式P->Q…,则a ∈ FIRSTVT(P) 这是因为如果a ∈ FIRSTVT(Q),说明Q能推出a开头的串,所以直接Q就换成了a…,那不就相当于P->a…,自然a ∈ FIRSTVT(P)啊!
    • 用自己的话总结:
      • A->B*C,那*要进入FIRSTVT(A)中,B的FIRSTVT也要进入到FIRSTVT(A)中(A->B时B的FIRSTVT也要进入到FIRSTVT(A)中)
      • A->a,a∈VT,则a∈FIRSTVT(A)
      • A->aBb,则a ∈ FIRSTVT(A)
  • 对于计算LASTVT(P):
    • 若有产生式P->…a或P->…aQ,则 a ∈ LASTVT(P)
    • 若a ∈ LASTVT(Q),且有产生式P->…Q,则a ∈ LASTVT(P),这是因为既然P->…Q,P能推出以Q结尾的串,而a ∈ LASTVT(Q),Q又能推出a结尾的串,所以必然P->……a,自然a ∈ LASTVT(P)
    • 用自己的话总结:
      • A->B*C,那*要进入LASTVT(A)中,C的LASTVT也要进入到LASTVT(A)中(A->B时B的LASTVT也要进入到LASTVT(A)中)
      • A->a,a∈VT,则a ∈ LASTVT(A)
      • A->aBb,则b ∈ LASTVT(A)

优先关系表的构建

  • 对于要求考虑‘#’的文法,则考虑句型#1S#2,#1 ⋖ FIRSTVT(S)、LASTVT(S) ⋗ #2
  • 对于其他关系,也是如此,αB => α ⋖ FIRSTVT(B)、Bß => LASTVT(B) ⋗ ß

引入素短语的概念

  • 对于算符文法要引入一个素短语的概念
    • 素短语是算符优先文法中要归约的对象
    • 首先是短语,然后不能全是非终结符,还要有终结符
    • 内部子串中没有更小的素短语,也就是子串应为不含终结符的短语
    • 一个句型中最左边的素短语就是在算符优先分析方法中要找的可归约串(最左是因为从左到右分析句子)

虽然使用语法树能很轻松地找出短语、直接短语、最左素短语等,但是由于我们最终的目的就是求语法树,所以不能通过画语法树的方式来求最左素短语。于是通过以下方式来求最左素短语:

由于算符优先句型可以写成#N1a1N2a2…NnanNn+1#,an是终结符、Nn是可有可无的非终结符,有定理:该句型的最左素短语是如Njaj…NkakNk+1的子串中的最左边那个,且这些子串都需要满足最左终结符aj的优先级大于句型中排在它前面但是不属于这个子串的终结符aj-1;子串最右终结符ak的优先级比紧接着子串但不属于这个子串的终结符ai+1的优先级要高;而子串中所有终结符的优先级应该是相等的。如下所示:

            aj-1 ⋖ aj         ak ⋗ ak+1
#N1a1N2a2...aj-1Njaj...NkakNk+1ak+1...NnanNn+1#
                |->  相等 <-|    

由上可得,中间那个子串优先级比子串左边的其它子串高,也比子串右边的其他子串高,而该子串内部优先级又相等,这样在所有子串里面这个子串优先级最高,那自然就先归约这个子串嘛

对于找最左素短语的算法,涉及到了栈的数据结构,不停用栈顶的终结符与栈外目前指针指向的终结符优先级进行比较,如果栈顶终结符优先级低与外面的终结符,就移入外面的终结符,指针后移,再比较,再移入,知道栈顶优先级比栈外高,这时就找到了最左素短语的尾巴(最右边终结符),然后再往回找,知道优先级比前一个要高了,那这个就是最左素短语的头(最左边终结符),这样最左素短语就找出来了

规范归约的结果一定是语法树,但算符优先分析的结果不一定是语法树,因为算符优先分析总是关注非终结符,有时会略过不含有终结符的产生式。(分析树 != 语法树)

算符优先分析法简单快速,但有可能接受非法句子,能力有限。然后不管什么时候,栈里栈外内容合起来始终是一个句型。

最后介绍一个优化方法:优先函数f、g

  • 入栈优先函数f,对应在左边的终结符
  • 比较优先函数g,对应在右边的终结符
  • 如果a1 ⋖ a2 则 f(a1) < g(a2)
  • 如果a1 ⋗ a2 则 f(a1) > g(a2)
  • 如果a1 a2 优先级相等 则 f(a1) = g(a2)

优先函数存在两个问题:

  • 有些优先关系表中存在空格,意味着不能比较优先级。但是用优先函数的话,是一定会找出比较的f、g值的。这相当于非法。但好解决,自行做特殊的判断
  • 不是所有没有冲突的优先关系表都有优先函数,比如有的优先关系表转化成函数会引起函数值矛盾

如果优先函数存在,不唯一。全体自然数全部加一也是成立的,所以优先函数存在一定是无穷的。

LR分析法

L表示从左到右进行扫描、R表示进行规范归约

LR分析方法:把“历史”(已移入符号栈的内容)和“展望”(产生式推测未来可能遇到的输入符号)综合抽象成状态;由栈顶的状态现行(当前)的输入符号查分析表,然后唯一决定每一步的工作(是移进,还是归约,按照哪个产生式归约)。当一串貌似句柄的符号串呈现于分析栈的顶端时,我们希望能根据所记载的“历史”和“展望”以及现行(当前)的输入符号这3方面的材料,来确定栈顶的符号串是否构成想对某一产生式的句柄。

LR的4种分析表

  • LR(0):最简单,局限性大,是建立其他表的基础
  • SLR:简单LR表,不是所有文法都能够早SLR分析表
  • 规范LR:能力最强但表体积过大
  • LALR:能力介于SLR和规范LR之间,稍加努力就能高效实现

LR分析器的核心–LR分析表(ACTION子表、GOTO子表)

设某时刻栈内为:
(Sm,Xm)
...
(S1,X1)
(S0,#)
  • ACTION表和GOTO表的行名均为状态序号
  • ACTION表的列名为终结符,GOTO表的列名为非终结符
  • ACTION表:当栈顶是某个状态S,面临输入符号是终结符a时,对应的格子里面存放相应的动作
    • sj 把格子中的下一状态j和当前输入符号a移进栈【(j,a)进栈】,然后下一输入符号变为当前输入符号
    • rj 按第j个产生式进行归约,若第j个产生式为A->ß,ß的长度为r,则去除栈顶r个项,此时新的栈顶为(S(m-r),X(m-r)),接着查GOTO表中(S(m-r),A)处格子里面的状态号Snum(S0~Snum均为自然数),把(Snum,A)压进栈中
    • acc 接受
    • 空白 出错,报错
  • GOTO表:当栈顶是某个状态S,刚刚归约形成了一个X压到栈顶之后,格子中存放的是跟这个X对应的状态号是什么

由于具体分析的时候画栈太麻烦,所以用三元组表示:

栈中的状态      已在栈中的符号串        栈外的还未归约的部分输入串
( S0,               #,                   a1a2a3...an#)
...
( S0S1...Sm,      #X1...Xm,              aiai+1...an#)// 接下来就去ACTION表中查(Sm,ai)处格子看下一步动作是什么:
// 若为sj(将下一状态j移进),则三元组变为( S0S1...Sm j,#X1...Xm ai,ai+1...an#);
// 若为rj(按照第j条产生式A->ß进行归约),弹出栈顶长度为r的项然后查完GOTO表(S(m-r),A)处格子里面的状态号Snum,此时三元组变为(S0S1...Sm-r Snum,#X1...Xm-r A,aiai+1...an#)
// 若ACTION(Sm,ai)处格子为acc,则三元式不再变化,返回“分析成功!”信息
// 若ACTION(Sm,ai)处格子为空,则分析过程终止,返回“出错了!”信息
...
  • 并不是所有的文法都能得到一张ACTION+GOTO分析表。若一个文法能构造出分析表,中间的每一个动作都是唯一确定的(表中格子内动作唯一),则成此文法为LR文法。
  • 如果ACTION表中的列名都是单个终结符,那就是根据当前的状态和当前的一个输入符号,就能确定下一个动作,但是有的文法只查一个符号并不能确定下一个动作,会有多个候选动作,接着查多个符号就能确定唯一动作,它们的ACTION表中的列名就包括k个符号组成的串,这些文法就叫LR(k)文法
  • 并不是k足够大LR(k)就能覆盖所有文法,有些文法就不是LR文法,比如有些文法是二义的,那这些文法就一定不是LR文法。(也就是说通过构造LR分析表发现无冲突,说明文法是LR文法的话,则证明文法是无二义的。这也是证明文法不是二义的另一个充分条件,反之,构造LR分析表,有冲突,不能说明文法是二义的,只能说明不是LR文法)
  • 程序设计语言中if-then/if-then-else结构,从语法上描述,不是LR结构,有冲突,解决方法如人工消解冲突,坚持就近匹配,这样也可以照常使用LR分析器来分析。
  • LR分析器任何时候栈内的符号串和栈外剩下的符号串会构成了一个规范句型,并且记住规范句型句柄后都是终结符
  • 栈内永远不会出现句柄之后的符号,其实想表达的意思是句柄和句柄之后的符号不可能同时存在于栈中,因为句柄一旦出现一定是会被归约的。所以用一个活前缀的概念来定义存在于栈中的串:
    • 活前缀:前缀是一个串的任意首部,如abc的前缀包含ℇ、a、ab、abc。所以栈内的串总是规范句型的前缀。但是由于LR文法栈内永不出现句柄后的符号,则把这种前缀称为活前缀,也就是可以包含句柄中间的符号或者整个句柄的符合,但不能同时包含句柄和句柄后的符号。

所以需要时刻保持栈中始终是活前缀。于是就需要识别活前缀,然后就想到构造一个有限自动机DFA来识别活前缀。引入项目和项目集族的概念:

文法G的每一个产生式的右部添加一个小圆点,成为G的LR(0)项目,如A->XYZ有4个项目:A->.XYZ;A->X.YZ;A->XY.Z;A->XYZ.;

  • ‘.’在最右边的项目,称为“归约项目”,意思就是点左边的串ready for归约了,特别的,对于文法的开始符号所在的产生式的归约项目如【E->ABC.】,称为“接受项目”
  • ‘.’在产生式的中间且点后面是终结符,称为“移进项目”,就是还需要再移进一些符号才能归约成产生式左边非终结符
  • ‘.’在产生式的中间且点后面是非终结符,称为“待约项目”,就是要先把未进栈的一些符号进栈归约成非终结符,再把非终结符进栈然后继续移入其他符号好最后归约成产生式左侧非终结符

点的位置代表了在分析过程中看到的产生式的多大部分,点的左边都是我们已经看到的,在栈内的,点的右边是希望能出现的。

文法-项目-NFA-DFA流程:

  • 拿到文法,找出所有项目:方法是从第一条产生式的右边开始移动小圆点。所有的项目合起来,即称为项目集规范族。
  • 每个项目就是1个状态,根据小圆点的移动是识别一个符号的规则和遇到要识别非终结符就射出ℇ弧到所有该非终结符在左边的项目(a.k.a 状态)这两个规则构建非确定有限自动机NFA,之所以是非确定的是因为存在ℇ。然后不能再射出任何弧的状态就是终态,(初态符号就是一个圆圈,终态符号是两层加粗同心圆)
    • 正常情况下NFA中的终态都是归约项目,也就是产生式本身,也就是识别出了句柄,可以归约了
    • NFA中从开始状态到终态,识别路上所有中间的串,都是活前缀,所以称为识别活前缀的NFA
  • NFA确定化得出DFA(自己复习词法分析中的子集法)

或者,

一步从项目集规范族确定成DFA骚操作:

定义文法G的拓广文法G’,G’包含了整个G,还引进了一个新的开始符号S‘,对应与G的开始符号S,有S’->S。这样做是使得文法G有唯一的“接受”态:仅含项目S’->S.这个状态

定义一个闭包运算CLOSURE:假设I是文法G’的任一项目集,则

  • I的所有项目属于CLOSURE(I);
  • 若有闭包中存在待约项目,则以待约项目点后面的非终结符为左的产生式,将小圆点放在产生式右边的最左边得到的项目(如E->.ABC),也属于CLOSURE(I);
  • 反复执行上面两个步骤直至闭包不再变化

定义状态转换函数GO(I,X),I是一个项目集,X既可以是终结符也可以是非终结符。GO(I,X) = CLOSURE(J)// 项目集J的闭包

  • J是项目集I中任何A->~.X~的项目,把小圆点移至X后的项目A->~X.~组成的项目集
  • 如果I对某个活前缀γ有效,则GO(I,X)便是对 γX 有效的项目集,结合活前缀识别DFA用人话讲就是DFA中从初态出发识别了一个活前缀后到达的状态(那个有编号的方块)中所有的项目,都是对活前缀有效的。

从我个人而言,上面从有效项目开始一直到拓广文法所有这些定义的语句,其实就是在说怎么直接从所有项目组成的项目集规范族中直接推出DFA,然后顺便介绍了一下什么是拓广文法。但是这么一堆让人厌烦的八股文,读起来让人吐血,老师讲也心累,不如直接拿一道例题来说最直观(它们存在当然是有意义的,提供了最精确的定义描述。只是从复习的角度,还是用白话把一道例题的解题过程说出来最直观):

例:有拓广文法G(S')
    S'->E
    E->aA|bB
    A->cA|d
    B->cB|d
画DFA。

项目集族我不写了,很基础。要画DFA,我们就需要先确定状态0,那必然是要从开始符号S‘开始确定。(Copyright © https://blog.csdn.net/s_gy_zetrov. All Rights Reserved)
对项目集族中的第一个项目S'->.E做闭包,那就需要把E在左边的两个产生式,将小圆点放在产生式右边的最左边得到的项目(E->.aA;E->.bB),也放进来。
最后得到I0={S'->.EE->.aA,E->.bB}
设I0为状态0I0本身是G’的项目集族的任一项目集。
接下来从I0开始往下GO函数,
GO(I0E),GO(I0E)=CLOSURE(J),J就是I0中小圆点在E前面的项目,把小圆点移到E后面,即GO(I0E)=CLOSURE({S'->E.})={S’->E.}=I1 // 因为「S'->E.」做闭包无法再包进其它项目,所以就这样了。把这个新项目集命名为I1I0发射一条E弧到I1
GO(I0,a),GO(I0,a)=CLOSURE(J),J就是I0中小圆点在a前面的项目,把小圆点移到a后面,即GO(I0,a)=CLOSURE({E->a.A})={E->a.AA->.cA,A->.d}=I2 // 「E->a.A」做闭包会把A为左的产生式,将小圆点放在产生式右边的最左边得到的项目(「A->.cA」「A->.d」)也加进来。把这个新项目集命名为I2I0发射一条a弧到I2
GO(I0,b),GO(I0,b)=CLOSURE(J),J就是I0中小圆点在b前面的项目,把小圆点移到b后面,即GO(I0,b)=CLOSURE({E->b.B})={E->b.BB->.cB,B->.d}=I3 // 同理,把这个新项目集命名为I3I0发射一条b弧到I3
接着从I1开始GOI2开始GOI3开始GO,...,直到没有新的变化,所有状态都不能再GO出新的状态,则终止。

这样GO函数就把所有的项目集连成了DFA

以上就是两种构造活前缀识别DFA的方法,推荐第二种。(Copyright © https://blog.csdn.net/s_gy_zetrov. All Rights Reserved)

下面介绍如何构造LR分析表。首先引出LR(0)文法的概念:

假设文法G的拓广文法G’的活前缀识别DFA中的每个项目集(状态,那个有编号的方块)不存在下面两种情况:

  • 既含有移进项目又含有归约项目 // 也就是将来到达这个状态后发现既要移进又要归约,那不是矛盾了
  • 含有多个归约项目 // 来到这个状态后发现有多个候选的归约,那到底选哪个,又矛盾了

排除了以上两个矛盾后的文法,称为LR(0)文法,可以构造LR(0)分析表。

构造LR(0)分析表算法:

  • DFA中的所有状态I0、I1、…的下标0、1、…,作为分析表中的状态编号
  • DFA中的I0为分析表的初态
  • DFA中从初态出发识别了一个活前缀后到达的状态的编号,就是你在栈中识别了一个活前缀时处于栈顶的那个状态编号。
  • ACTION表和GOTO表的构造
    • 若在状态Ik中有一个移进项目(‘.’在产生式的中间且点后面是终结符,形如A->α.aß)且从状态Ik出发有一个a弧射到状态Ij(即GO(Ik,a)=Ij),则令ACTION表中[k,a]格子为“sj”
    • 若在状态Ik中有一个归约项目(’.’在最右边的项目,形如A->α.)则在ACTION表中k这一行中包括‘#’的所有终结符所在的列,都填入“rj”(j是第j个产生式,也就是找A->α是文法中的第几个产生式)
    • 若在状态Ik中有一个接受项目(文法的开始符号所在的产生式的归约项目如拓广文法的【S’->S.】)则在ACTION表中k那一行‘#’那一列,填入“acc”
    • 若从状态Ik出发有一个非终结符弧(如一条A弧)射到状态Ij,那么GOTO表中k行非终结符A那一列,填入j这个数字
    • 其他格子都是空白,或者填入“ERROR”

LR(0)文法太简单,没有实用价值,稍微有用一点的文法,都有可能出现【移进-归约】冲突。化解的方式就是SLR(1)分析表,也叫SLR分析表

SLR与LR(0)的区别其实就是当一个项目集中有归约项目时,不再像LR(0)一样把ACTION表中项目集对应的那一整行上都放上归约动作,只在那一行的归约动作要归约成的非终结符的FOLLOW集中的终结符对应的那些列中放入归约动作,【移进-归约】冲突中的移进动作放到对于要继续读入的那个终结符所在的列。(Copyright © https://blog.csdn.net/s_gy_zetrov. All Rights Reserved)

用更严谨的方式说一下SLR分析表的构造算法:

假设项目集I={X->α.bß,A->α.,B->α.},这就是一个移进两个归约互相冲突。如果FOLLOW(A)和FOLLOW(B)交集为空(‘#’也算进去,不允许有两个FOLLOW集有‘#’),且均不包含b,则按照下面的方法构造表格:

  • 若下一输入符号为b,则移进
  • 若下一输入符号在FOLLOW(A)中,用A->α归约
  • 若下一输入符号在FOLLOW(B)中,用B->α归约
  • 其他情况,格子为空,或填写“ERROR”

如果这样构造出的SLR分析表不含多重入口,就成文法为SLR(1)文法,SLR(1)文法都是无二义的,但也有无二义文法不是SLR(1)的。

【无二义文法 包含 【SLR(1) 包含 LR(0)】】

这就又找到了一个判断无二义文法的充分条件(在之前充分条件的基础上补充):如果文法构造出LR(0)分析表没有冲突,一定是无二义的,但如果有冲突,但能用SLR冲突消解方法构造一个没有冲突的SLR分析表,如果能,那说明文法是二义的。

SLR用的FOLLOW集也存在问题,如果FOLOW集中出现了【移进-归约】冲突中的移进动作的要继续读入的那个终结符,那就需要在格子里面放入归约动作,可移进项目有要求放入移进动作,于是产生了新的冲突。解决方法是看此时栈中的串是否允许归约或移进,因为有可能出现虽然FOLLOW集中存在要读入的终结符,但文法产生式并不允许在当时栈中活前缀的情况下FOLLOW栈顶的符号。如果归约或移进后栈中剩下的串与输入串组合后与任何产生式都不匹配,那就不能选择填入移进或归约动作。

于是介绍规范LR分析表的构造

需要重新定义“项目”的概念,此时项目为[A->α.ß,a1a2…ak],这样的项目称为LR(k)项目,后续只涉及LR(1),但是记住k越大展望能力越强,消歧义的能力也越强,但是分析表也会越大。

a1…ak称为向前搜索串,对归约项目有意义,LR(1)中,归约项目[A->α.,a1]的a1与每一个项目关联在一起,不是与A关联在一起。只有当该项目的所在的状态(状态号,自然数)处于栈顶且后续输入符号是a1时,才允许归约把栈顶的α归约为A 。对于移进项目和待约项目,无直接作用,是为了以后称为归约项目的时候用。

在构造ACTION分析表的归约项目的时候与LR(0)的区别是把展望串所在的列置为rj,而不是整个一行全部置为rj

  • 对于展望串的构造,包含拓广文法的开始符号的项目(如S‘->.S),其展望串为#,即[S‘->.S,#]
  • 对于[A->α·Bß,a],要算FIRST(ßa),ß可以为 ℇ 。FIRST(ßa)中的符号即为B->.ξ的展望串。这句话说了半天,其实想表达的意思就是在算项目集族中的每一个项目的时候,不是需要找出所有对当前活前缀有效的项目么,那么之前LR(0)的时候,如果有项目S->.Ab,还需要把A->.α加进来,因为S->.Ab点的后面是非终结符。现在在LR(1)中,由于要算展望串,那新加进来的A->.α这个的展望串,就是[S->.Ab,a]中的FIRST(ba)。所以相当于,还是跟LR(0)一样,S->.Ab时要把A->.α加进来,但是A->.α的展望串是用前面的S->.Ab求的。由于起始状态展望串定了,就是‘#’,所以初始状态项目集中所有展望串全都能求了。注意,后续状态项目集中的项目的小圆点如果移动后由S->.BAb变成了S->B.Ab,则S->B.Ab的展望串与S->.BAb一致,后续要包进来的A->.α的展望串,是从S->B.Ab中重新算FIRST算出来的!

终于开始觉得符号表达式好厉害了,上面的一段白话精炼成一句话就是第一句:对于[A->α·Bß,a],ß可以为 ℇ ,要算FIRST(ßa)作为B->.ξ的展望串,言简意赅。

除了展望串,其他的部分与LR(0)基本一样,最后正常根据DFA构建分析表就行了,注意归约项目的地方要填入的格子是展望串不是一整行。

具有规范的LR(1)分析表的文法称为一个LR(1)文法(Copyright © https://blog.csdn.net/s_gy_zetrov. All Rights Reserved)

【无二义文法 包含 【LR(1) 包含 【SLR(1) 包含 LR(0)】】】

最后简单介绍一下LALR,本质上是对LR(1)做进一步处理,就是合并同心集([A->α·Bß,a]和[A->α·Bß,b]和[A->α·Bß,#]的心“A->α·Bß”是一样的,合并为[A->α·Bß,a/b/#])。

LR(1)、LALR、SLR(1)、LR(0)之间的关系:

【无二义文法 包含 【LR(1) 包含 LALR 包含 【【SLR(1) 包含 LR(0)】】】】

- 无二义文法
    - LR(1)
        - LALR
            - SLR(1)
                - LR(0) 
  • LR分析过程是规范归约过程
  • 符号栈中的符号串被定义为活前缀
  • 分析过程中依据栈顶状态和现行输入符号查分析表
  • 为构造LR分析表,可先构造识别活前缀DFA
  • LR分析器的关键是分析表的构造
  • LR(0) 、SLR 、 LALR 、 LR 功能逐个增强
  • 四种LR类型的文法是真包含关系
  • LR类型文法一定是非二义的

给一个文法,判断是LR(0)?LR(1)?LALR?还是SLR(1)的?

老老实实从LR(0)开始写项目集,项目集族,构建识别活前缀的DFA,查冲突,有【移进-归约】冲突或【归约-归约】冲突,那就查能不能用FOLLOW集的方法消解冲突(FOLLOW集两两之间交集为空,不包含冲突中要移进的终结符且不同时包含‘#’),不能的话就老老实实从头开始按照LR(1)文法构建DFA,查是否有展望串相同下的【移进-归约】冲突,有的话文法就既不是LR(0)也不是LR(1)也不是LALR也不是SLR(1)的。如果没有冲突,再查一下LR(1)基础上同心集合并后有没有【归约-归约】冲突,没有就是LALR的了。

(Copyright © https://blog.csdn.net/s_gy_zetrov. All Rights Reserved)


visitor tracker
访客追踪插件


猜你喜欢

转载自blog.csdn.net/S_gy_Zetrov/article/details/82315340