语法分析——自下而上

  这一章作为上一章的姐妹篇,本以为会有很大的关联。但整体下来后发现还是比较独立的,属于两种方法,用不同的思想实现同一个作用。

  还是要吐槽一下~我感觉这章的内容比较多,乍一听一大堆一大堆的。但梳理过后,都是一块一块的内容。下面就开始吧!

  在正式开始之前,我先梳理下内容的主线。第一瞬间不太了解也是正常的,希望读完本篇文章后能够明白。

  主线:自下而上的主要思想是移进-规约。移进-规约的关键在于如何选择合适的规约字符串以及规约到哪个非终结符。算符优先分析法告诉了你什么时候进行规约,LR分析法告诉了你具体用哪个产生式进行规约。LR分析法包括了很多种:==LR(0),SLR(1),LALR(1)==等等。它们存在的价值是不断优化,改良前一种方法存在的某一方面问题。

  如果觉得我的主线很晦涩,那么请继续往下看吧!

  一、自上而下分析基本问题

  规约

  规约在很多的地方都有提,在语法分析中的规约简单可理解为“右变左”(当然这只是我的理解),下面看看形式化的定义:

在这里插入图片描述
  从定义中可了解到,规约是用产生式左部代替右部的动作。从是否为终结符的角度考虑,左部一定是非终结符,右部两种均可。任其不断规约(正确的情况下),最后将得到唯一的非终结符,即开始符号。

  还是举一个例子,帮助了解规约的整个过程。

在这里插入图片描述

  先看这个例题的动作包括“进”和“规”。“规”代表的就是上文提到的规约,这里对进做一下解释:在语法分析中使用栈(特点为后进先出)作为表示的数据结构,这里的“进”即代表进栈。

  从这个例子里,了解到两件事就可以了:

    (1)规约就是把栈顶的一个或多个字符根据某一产生式变换成对应的左部

    (2) 规约的终点是开始符号。

  善于思考的人想必会发现这个过程中遗留下了两个问题:

    (1)什么时候进,什么时候规呢?

    (2) 规的时候如果多个产生式符合条件,选哪个呢?

  (如果你能发现这个,你真是太棒了。像我这种,只会跟着老师说对对对~)对于这两个问题,后面会进行解答,还请稍安勿躁。

  短语,直接短语,句柄,素短语,最左素短语

  先来看看定义怎么说:

在这里插入图片描述
  短语有两个特点:

    (1)短语是由某一个非终结符A经过一步或若干步推导得到的。

    (2)该非终结符A一定能由开始符号推导得到(此处推到类型为*)

  但还有一种更为简便的方法,求解短语、直接短语、句柄。就是利用语法分析树

  语法分析树方法的解读就是对于所有的非叶子节点(非终结符)都可能存在短语,短语的组成就是由某非终结符节点散发出的全部叶子节点,从左至右的书写。直接短语是非叶子节点正好是叶子节点正上方那一个。句柄就是直接短语中最左边的那个。三者的关系图如下:

在这里插入图片描述
  对于短语求解,可以按照层的方法。从根节点开始,为每一层的非终结符求出其短语。

  下面来举一个例子,文法如下:

在这里插入图片描述
  句型为i1*i2+i3

  画出语法分析树:
在这里插入图片描述
  解析:对每一个非叶子节点求解,对E(1排)短语(即叶子节点组合)为i1i2+i3,对E(2排)为i1i2,T(2排)为i3。对T(3排)为i1*i3、F(3排)为i3,T(四排)为i1、F(四排)为i2。F(五排)为i1。从里面剔除相同的。i1,i2,i3最底层叶子节点组成了直接短语,句柄是直接短语中最左边那个就是i1了。

  答案:
在这里插入图片描述

  素短语是对短语的另一种限制,即素短语中必须至少含有一个终结符,并且必须为最小(指的是没有其子集仍作为素短语):
在这里插入图片描述
  举个例子:

在这里插入图片描述
在这里插入图片描述
  对于E+TF和E+TF+i均不满足最小要求。

  最左素短语和句柄之间的关系:句柄不一定是最左素短语(因为句柄中不一定含有非终结符)。

  二、算符优先文法

  算符文法

在这里插入图片描述
  两个特点:

    (1)没有连续的两个非终结符

    (2)任何一个产生式的右部不含空字

  在此采用倒叙的叙述方法(倒叙的优点:此处省略无数个字儿):先说算符优先文法可以实现选择合适的规约子串。形式化说法如下:

在这里插入图片描述
  简单点说呢就是每次都比较栈顶字符和输入字符的优先级。如果栈顶字符优先级小于等于输入字符则将该输入字符入栈,否则说明可进行规约。那问题来了,栈里面那么多个字符选几个进行规约呢?找寻可规约串的方法就是从栈顶开始向下选择终结符(如果栈顶是非终结符,那么选择其下面相邻的终结符),即从栈顶出发,在栈里比较相邻终结符之间的优先关系,如果栈下面的终结符比其上面相邻的终结符之间的优先关系低,则将其前面的到栈顶的符号进行规约。(注意这里相邻的终结符关系存在两种可能性:小于和等于,小于就规约,等于就不管)直到终结符的优先级大于当前字符为止。之间字符(包括栈顶字符)可作为规约串。以此类推,直到开始字符。

  说完了这个,问题又转变成了如何求各个字符之间的优先级关系呢?当然是有一套独特的方法了。

  先说一下在算符优先文法中优先级的表示方法,和传统的大于号,小于号不同。

   算符:这里的算符指的是文法中的终结符,终结符a,b之间的关系有三种:

   a<·b a的优先级小于b

  a·>b a的优先级大于b

  a=·b a的优先级等于b

   有两个点需要注意下:

    (1)比较优先级的一定都是非终结符

    (2) 表示的符号里有一个·(千万别忘)

  算符的优先级自然是从产生式中定义的,下面看一下从产生式角度,形式化的描述吧:

在这里插入图片描述
  虽说上面定义的方法能够比较出算符优先级,但求解起来很零散。下面介绍一种系统的求解算符优先关系的方法,并把关系用算符优先分析表的形式表现出来,即列个表格(横轴关系纵轴)。

  FIRSTVT§和LASTVT§

  其实FIRSTVT和LASTVT是上文描述的关系的一种演化。二者都是对于非终结符来说的,构造算符优先分析表的时候需要对每一个非终结符都求FIRSTVT和LASTVT。下面给出求解FIRSTVT和LASTVT的形式化描述:

在这里插入图片描述
在这里插入图片描述
  FIRSTVT简单点说就是找每一个非终结符出发的第一个或者是第二个终结符(此时第一个必须是非终结符)。对于自己是B,第一个是非终结符A,那么把所有的FIRSTVT(A)中的字符加入B中。

   LASTVT简单点说就是找最后一个是终结符或者倒数第二个是终结符(此时倒数第一个必须是非终结符)。对于自己是B,最后一个是非终结符A,那么把所有LASTVT(A)中的字符加入B中。

   还有一点需要特别特别注意,使用栈的结构,还需要引入一个’#’作为终止符。这个也要参与算符优先级的比较(尽管明面上没有它)。

  下面还是举个例子,说明下算符优先分析表的构造过程:
在这里插入图片描述
  看上去第一眼不知道从哪里下手吧!其实求算符优先分析表的基本步骤就是先求每一个非终结符的FIRSTVT和LASTVT。

  为了节省篇幅,我只求下,FIRSTVT§和FIRSTVT(F),LASTVT§和FIRSTVT(F)了,其他的同理可知。

  FIRSTVT§,按照FIRSTVT第1/2个终结符的要求,找到只有P->(E)|i左侧是由P组成的。包括P->(E)中的(,由于P->i中直接就是终结符,i也入围。最后的结果是FIRSTVT§={ ),i}。对于F,两个候选式中第一个都是非终结符P,所以P中的所有选项也归到F中,并且在F->P⬆F中满足第一个是非终结符第二个是终结符的情况。于是将⬆归为F中。结果FIRSTVT(F)为{ (, i, ⬆ }。

   LASTVT§,按照倒数第1/2个终结符的要求,在P->(E)|i中)和i都满足入围。LASTVT(F)有两个候选式,F直接指向P,P的LASTVT集也归入F中。对于F->P⬆F,上箭头是倒数第二个且倒数第一个为非终结符。满足条件。将⬆加入LASTVT(F)中。最终得到LASTVT§={ ),i},LASTVT(F)={ ),i, ⬆}。

在这里插入图片描述
  求出了FIRSTVT和LASTVT就该向分析表转化了。两者的转化还是有那么一点点的麻烦滴!

  方法口诀为:确定一个终结符(左边或者右边有非终结符),该终结符小于右边非终结符的FIRSTVT中所有符号,大于左边非终结符的LASTVT中的所有符号。填表的时候,小于符号横向填,大于符号纵向填。

  看完口诀,其实也不是那么懂,还是结合例子看看吧:

在这里插入图片描述
  在E->E+T中,+位于E和T之间,就应当大于LASTVT(E)小于FIRSTVT(T)。FIRSTVT(T)中包括{ ,⬆,(,i}于是在横向的(+,xxx)中填入<·。LASTVT(E)中包括{+,,⬆,i,)}。于是在(xxx,+)中填入·>。特别注意:#和各个优先级的比较方法,规约最后的形式是#S#(其中S为起始字符),所以#就利用FIRSTVT(S)和LASTVT(S)完成。方法同上面,#横向小于FIRSTVT(S),纵向大于LASTVT(S)。

  注意在(E)中满足等号的形式,即( =· )。在构造算符优先分析表的时候应当先判断等号。留有空地的地方是无法比较的,若出现可认定为语法错误。

  优先函数

  优先函数是另一种表示优先级的方法(基本可以代替算符优先分析表):
在这里插入图片描述
  所以如果知道f(a)和g(b)的大小就可以反推出优先级关系。大小的求解先要画出一张优先函数构造的图,如果a>=·b就从fa画一条线(带箭头a->b)到gb,如果a<=·b就从gb画一条线(带箭头b->a)到fa。画出来的图类似于这个样子:

在这里插入图片描述

  Fa的值就是该点能够到达其他所有点(包括自身)的总个数,同理gb。

  你可能会觉得很奇怪,想求优先级但是却用优先级求。其实优先函数存在的价值就是节省控件,原来利用分析表的时候要占用(n+2)(n+2)空间,n代表终结符个数。利用这个优先函数,结果表示在n2即可,像这个样子:

![在这里插入图片描述](https://img-blog.csdnimg.cn/20201027001058636.png#pic_center

  三、 LR分析法

  LR分析法是一类方法,先对LR分析法做个基本介绍再举个例子看一下利用LR分析法是如何进行自上而下的规约语法分析的吧!

  LR分析法的模型如图:

在这里插入图片描述  与前面有较大变化的就是栈变成两组,一组代表终结符另一组代表非终结符。LR分析法就是按照分析表里限定的规则,对输入串中的各个字符进栈出栈的一系列操作组成。可以看出:分析表是LR分析法的核心内容。

  介绍几个LR分析法的相关概念:

   LR分析表有两大状态:ACTION(动作)和GOTO(转换)。ACTION相对于终结符,GOTO相对于非终结符。下面看个成品:

在这里插入图片描述
  这框框里的s啥,和r啥都代表啥呢?这就与ACTION中规定的四种动作有关了:

在这里插入图片描述
  所以在ACTION中,前面的字母代表动作,后面的数字代表状态。S代表shift(移进),r代表reduction(规约)。s后面的数字是状态数,r后面的数是产生式的标号。

  下面举个例子,看看LR分析法是怎么实现自下而上的语法分析的。

在这里插入图片描述
  解(PPT里面已经很详细了,我就直接贴图了):
在这里插入图片描述
  读到这里,希望你已经明白了LR分析的整个过程,但LR分析中主要的工作就是对于分析表的构造。下面就介绍一下分析表构造方法吧!课程内容里面只设计到LR(0)分析表和SLR(1)分析表的构造。下面就主要说这两种,对于ALSR(1)也会有一点儿简单的介绍。

  SLR(1)是在LR(0)的基础上构成的,所以LR(0)的基础应当打牢。

  LR(0)分析表的构造

  在正式介绍之前,先了解几个基本的概念。

  项目(很明显嘛,就是加点~前后加点)

在这里插入图片描述

  拓广文法

在这里插入图片描述
  其实拓广文法的作用就是使初始状态唯一,便于我们我后面的分析。以此为0态。

  项目集I的闭包(CLOSURE(I))

在这里插入图片描述

  项目集I的转移函数:

在这里插入图片描述
  下面还是举个例子说明问题,怎么利用上述讲的一些概念构造分析表:

  例题(该文法已经被拓广处理,目前起始字符为S‘):

在这里插入图片描述
  1.列出所有产生式,并为其编号(注意在这个里面对于|,要分开处理了,A->a|b分为A->a和A->b按两个编号

  2.列出所有项目:

    S‘->·E、S‘->E·

    E->·aA、E->a·A、E->aA·

    E->·bB、E->b·B、E->bB·

    A->·cA、A->c·A、A->cA·

    A->·d、A->d·

    B->·cB、B->c·B、B->cB·

    E->·d、E->d·

  3.按照项目集闭包画出DFA图:
在这里插入图片描述
  简单说明下这个DFA图是咋画的:以开始状态为0态,首先将包括开始字符的产生式置于其中,如果点后直接跟着一个非终结符,就将该非终结符为产生式左部且点位于最左侧的项目也放到0态中。后面就开始延伸,点后面第一个符号是啥(不管是不是终结符)引出一条线,标号新状态,第一个式子写点后移一位,如果面临非终结符和上面的处理方法一样。需要注意的是可能需要加入多个非终结符,比如A->·E,E->·B,B->·C …。这样每个E、B、C的都要写。

  将DFA图转换成分析表:

在这里插入图片描述
  转化分析表这个部分看上去很麻烦,但其实只是按照DFA图填进去就可以了。目前介绍的是LR(0)分析,上文提过LR(0)分析有一个特点:不存在冲突。对应于分析表中就是一行状态都可为规约操作。

  首先,按照DFA中写出的状态总数从0依次排列作为纵轴。横轴分为两大部分,第一部分是ACTION部分,内容是所有的终结符加一个#,第二部分是GOTO,内容是所有非终结符。

  内容填写的方法是,选择任何一个状态(通常以0态为始,此处也选择0态)从0态出发分别是a,b,E。经过a,b分别进入2,3状态。2,3状态中均存在点后仍有其他字符的情况且不存在点位于最后的情况。这种认定为移进状态。在分析表中用s+后续状态表示,如s2。经过非终结符E进入状态1,就在0状态对应于非终结符E的位置填入1。大多数状态的前期分析用上面的方法就足够了。但如果出现点位于末尾的句型,分析就有所不同了。对于状态1,点位于末尾且包含起始字符。这个就是我们规约的终点,当遇到这种情况就在(状态,#)的位置写上”acc”,代表分析到此为止,并且分析正确。对于状态10这种,点在最后,左部不是开始字符即代表普通规约项。使用r+数字,数字就是该产生式在之前的编号。对于LR(0)分析,当出现规约项时可在所有的非终结符出写入规约式,对非终结符不做处理。(原因稍后解释!

  SLR(1)分析表

  看到这个第一个问题应该是:为什么需要SLR(1)分析呢?那就要首先介绍一下两种简单的冲突。从一个例子引入吧!

  如果有这么个状态:

在这里插入图片描述
  我们下一步是该规约呢?还是移进呢?(看上去都有道理,各自的支撑分别是1,2产生式)。当计算机不知道下面该干什么的时候,冲突就产生了哇!

  上面这种情况就叫做移进——规约冲突,也就是当一个状态同时出现移进操作和规约操作的情况。

  看完一种先别急,再来看看下一种:

在这里插入图片描述
  当同一个状态出现这种情况怎么办呢?我是规约成A呢?还是规约成B呢?(计算机又要哭晕在厕所)。这就是第二种冲突:规约——规约冲突。

   有问题,不要担心。一物降一物,克服这两种“妖怪”的方法就是SLR(1)分析。

  SLR(1)分析 —— simple left right

  简单LR分析,说白了就是结合了FOLLOW集去更精确的确定下一步的操作。那为什么FOLLOW集可以呢?

  先看FOLLOW集是如何解决移进-规约冲突的:其实点之后就是我们要和当前输入字符进行比较的,就是根据输入符合某条件进行对应的选择。如果下一个字符在FOLLOW集里面就说明下一个字符归A后面管了,此时就可以进行规约了。那再一种极端情况呢?这个字符即在FOLLOW集中又恰好是移进操作的下一个字符。那这个时候情况就比较复杂了,就超出SLR(1)分析能解决的范围了。所以同一状态对于同一输入字符也可以作为判断是否符合SLR(1)分析的标准。

  对于规约——规约冲突,也比较好理解。就是下一个输入字符在哪一个产生式的FOLLOW集中就用对应的进行规约。再特殊一点:要是都在呢?还是那句话:后面还有更加高深的方法呢!

  较之LR(0)分析,SLR(1)分析更为复杂。分析表的构造也更为苛刻。之前说过规约的时候在所有的终结符下面都写上规约运算,而SLR(1)中只在FOLLOW集中存在的终结符下有所表示。

  大致就是这个样子的~:

在这里插入图片描述
  本来还想说说更为复杂的LR分析法,但想到我还不会而且写了这么多有点烦。就偷个懒吧,以后如果想起来再补上吧!

  感谢编译原理课程谢老师的耐心修改!

由于作者水平有限,如有错误之处,请下方评论区指正,谢谢!

猜你喜欢

转载自blog.csdn.net/gls_nuaa/article/details/109301983