形式语言与自动机13 自下而上分析方法 LR类分析方法

语法分析之自上而下

一、上下文无关文法与程序结构

这种文法的 格式(模式) \(A->\alpha\) \(\alpha\)是变量与常量组成的字符串。

化成乔姆斯基范式(CNF)和格林巴赫范式(GNF)是为了写程序做准备

通常我们程序句子可以划分如下:

  • 说明、定义语句
  • 普通语句
    • 表达式语句以及赋值语句
    • 控制语句
    • 函数语句

要是一个文法能够把这两大类(四小类)语句能够描述清楚,就可以说其能够描述程序。

顺序语句文法:

控制语句文法:

二、语法分析的任务与分类

语法分析可以作为主函数,按需调用词法分析器。不用一下子全部把Token流写出来。

符号表是一种数据结构,可以读写。供以后使用。

归约:从终结符串(句子、常量串) 一直到变量S

三、自上而下分析面临的问题

文法的左递归;

回溯;

替换符的顺序会影响接受的语言; \(A->ab|a;A->a|ab\)

难报告出错位置

引起上面这些问题的不确定性有3种:

选择哪一个非终结符、选择哪一个产生式、处理句子时读入几个单词符号(Token )

  1. 仅有产生式选择是非确定的

    采用穷尽+回溯的方法

需要对上面的文法做一定的限制

消除左递归 略

消除回溯;

图示情况1可以唯一确定

这种是比较好区分的,通过查看产生式体的第一个字符

这种情况下就不容易区别哪一个产生式了,因为产生式体的第一个字符相同

解决办法:提取左公因子(实际就是对原来产生式进行修改,公共前缀只出现在单个产生式体中)

情况三

\(N_1,N_2\) 一直往下推,直到推出常量,这个句型串的第一个字符是常量符号就可以啦(可以理解,情况二和情况三是情况一的变式和拓展)

推出\(\epsilon\) 的时候,我们当然就得向后看了,比如上面的 \(N_1,N_2\) 推出\(\epsilon\) ,我们就需要看 \(\alpha_1,\alpha_2\)

\(A ~ ->~\alpha_1,~\alpha_2\) 这些要看透,一直到露出其第一个常量符号

\(FIRST(\alpha)\) 表示的是 \(\alpha\) 的首字母集合

为什么对于选择产生式,要求\(FIRST(\alpha)\) ??

因为对于变量 \(A->\alpha_1|\alpha_2\) 这两个产生式体,我们是选择 \(\alpha_1\) 呢还是选择 \(\alpha_2\) 呢 由于我们有这样的定义 \(FIRST(\alpha_1) \cap FIRST(\alpha_2) = \Phi\) 所以呢,对于输入符号a,如果 \(a\in FIRST(\alpha_1)\) ,则 \(a\notin FIRST(\alpha_2)\) ,所以可以选定产生式 \(A-> \alpha_1\)

等一下,我们上面说消除回溯,那么消除回溯的目的是什么??

Ans: 让我们产生式 \(A->\alpha_1|\alpha_2|\alpha_3|…\) 这样的产生式体推导的首字符集不会相交!! 即\(FIRST(\alpha_1)\cap FIRST(\alpha_2) \cap FIRST(\alpha_3) \cap … =\Phi\)

四、TopDown 之 递归下降法

核心就是 不含左递归\(FIRST(\alpha_1 )\cap FIRST(\alpha_2)\)

CFG与扩展的BNF

五、TopDown之预测分析法

可以把递归改为非递归 ,时空转换,从而提高效率

输出: 正确与否 分析树

和下推自动机类似 PDA的功能与预测分析表功能一样

分析表的格式 :一个\(2*2\) 的表格

行代表 变量;列代表常量+1(1是额外加入的终结符 # )

表格内容是 产生式 或者 error出错处理程序

预测分析表实际工作过程 与我们的PDA是一样的;产生式输出 就指导了一个最右推导

这个表的用途:句子的规范推导可以在这样一张表的指导下最终推导出正确结论

工作动作示例:

**对待变量 : 左部出栈,右部反序压栈 **

那么上面这张表怎么来的 怎么填的??

对于 \(A->\ X_1X_2X_3\) 如果\(X_1\)是常量\(a\) ,那么就把\(A->\ X_1X_2X_3\) 填入\([A,a]\)

如果\(X_1\) 是变量,那么就把\(FIRST(X_1X_2X_3)\) 所有可能的常量符展开\(x,y,z\) ,把 \(A->\ X_1X_2X_3\) 填入到 \([A,x],[A,y],[A,z]\) ;

如果\(\ X_1X_2X_3\)\(\epsilon\) , \(\epsilon \in FIRST(\ X_1X_2X_3)\) ,那么把\(A->\ X_1X_2X_3\) ,加入到 \([A,b_1],[A,b_2]\) 其中 \(b_1,b_2 \in FOLLOW(A)\)


什么时候使用 \(A->\epsilon\) ?? 当前字符(c)属于 \(FOLLOW(A)\)时,我们使用 \(A->\epsilon\)

\(FOLLOW(A)\) 表示 A的后面跟什么?

注意细节:\(FIRST(\alpha)\)\(\alpha\) 是字符串;

\(FOLLOW(A)\) 的A是变量。

从起始变量开始观察每一个含有A变量的产生式 ,取A后面跟这的常量。然后组成集合。和\(FIRST(\alpha)\) 一样,也是常量符号组成的集合

第四条可以这样理解: \(N->S\beta|other\) 这样的话,就一定有\(FIRST(S\beta) \subseteq FIRST(N)\)

注意到 \(X->Y_1Y_2Y_3\)\(FIRST(x)\) 则先求 \(FIRST(Y_1)\) ,若\(Y_1\) 没有\(\epsilon\) ,则\(FIRST(x) = FIRST(Y_1)\) 结束;若 \(Y_1\)\(\epsilon\) ,则把踢掉\(\epsilon\) 的其他常量符加入 \(FIRST(x)\) ,然后继续求\(FIRST(Y_2)\) ,对\(FIRST(Y_2)\) 也做类似分析,看看是否含有\(\epsilon\)


FOLLOW

对3可以做如下理解:

本来有 \(A->\alpha B\) 我们要求\(FOLLOW(B)\) ,本来对 \(\gamma_1A\gamma_2\) 我们有替换,\(\gamma_1\alpha B \gamma_2\) 则我们要求,发现\(FOLLOW(A) \subseteq FOLLOW(B)\)

预测分析表有一部分需要使用first与follow集合,现在我们回去再看预测分析表怎么填

\(first(\alpha)\) 把一个变量彻底暴露出所有可能的常量集合

分析表的构造还是通过文法得到的 输入:文法 输出:分析表

六、TopDown之LL(1)

LL(1)分析法

第一个L表示从左到右扫描输入串;

第二个L表示 最左推导

(1) 表示分析查表时,只向前看一个符号

LL(1)文法:分析表每个格子要么没有产生式,要么只有一条产生式

使用 LL(1)文法 一定可以实现不带回溯且自上而下 的语法分析

判断G(S)文法是LL(1)文法,就采用两个条件即可:

  1. $FIRST(\alpha_1) \cap FIRST(\alpha_2) \neq \Phi $
  2. \(FIRST(\alpha_1) \cap FOLLOW(A) \neq \Phi\)

要是不是 \(\Phi\) 的话,都意味着会有两条产生式仍然面临回溯。也就是说只要有交集,那么就不会是LL(1)文法。

面对这种非 LL(1)的文法,两条产生式在一个单元格。我们强行删掉一条。(强删一条也就是约定,既然约定了,也就面临抉择,不会产生回溯,选择是唯一的)

改的方式简单粗暴,但是会不会对文法产生较大影响,具体问题具体分析

对于规范归约 ,它的可归约串是:拿着句柄归约。

不同归约方式的可归约串不一样。

规范归约——最左归约;

规范推导——最右推导,规范推导得到的句型叫做 规范推导

规范归约非常好,难点是句柄的找到算法(不需要把分析树画出再找)

句柄一定是在栈顶(因为我们都是在栈顶完成归约)

通过优先级表,区分移进和归约

操作符栈顶的优先级最高,栈底的优先级最低。将操作数和操作符放在两个栈中

算符优先分析表构造(终结符优先级表) 任何两个终结符的优先关系

从算符优先文法 构建 算符优先关系表

注意比较:\(FIRST(\alpha),Follow(A),FIRSTVT(P),LASTVT(P)\) 是不同的

\(LASTVT(变量),FIRSTVT(变量)\) 是终结符的集合 。

对文法拓展,就是在终结符中加入 #(原来的表增加一行一列),然后构造优先关系表


构造文法的算符优先关系表的时候,应该遵循下列步骤:

  1. 开始符号 变成 次开始符号 文法改造。添加一条产生式:\(S'->\#S\#\)

  2. 计算每一个非终结符的\(FIRSTVT(P),LASTVT(P)\)

  3. 得到集合后,逐条扫描产生式;

    • 变量在左边 常量在右边 比如:\(E~+\) 就会有:\(LASTVT(E)~>~+\)
    • 变量在右边 常量在左边 比如:\(+~E\) 就会有:\(FIRSTVT(E)~>~+\)

  4. 得到这张表以后,就可以用简单分析文法来处理算符优先关系

算符文法:一个文法的产生式不含有连续两个变量。比如:\(A->…QR…\)

而算符优先文法一般的格式是:\(\#N_1aN_2bN_3c…N_{n+1}\#\)

素短语

含有终结符 且 这个素短语最短。(含有终结符比较容易理解,后面的条件意思是:素短语一定是特别短的,是那种可以作为别的素短语的因子的那种,我觉得可以叫元短语,无法再次分隔的最小单位)

句型的最左素短语满足条件:

假如说这个是素短语 \(N_ja_jN_{j+1}a_{j+1}…N_ia_{i}N_{i+1}\) ,那么对于句型\(N_{j-1}a_{j-1}~~~~N_ja_jN_{j+1}a_{j+1}…N_ia_{i}N_{i+1}~~~~~a_{i+1}\) 终结符的优先级关系会有类似"峰"型曲线(正态分布) 。即 \(a_{j-1}<a_j = a_{j+1}=a_{j+2}=a_{j+3}>a_{j+1}\)

最左素短语和句柄是两种不同的可归约串。上面这种方法的目的还是找到最左素短语类型的可归约串。值得注意的是,最左素短语内部的常量优先级是相等的且是最大的

算法步骤:

  1. 我们只处理终结符常量,把句型的变量非终结符跳过
  2. 由1得到的常量终结符串进行寻找素短语行动,先确定栈顶,栈顶确定后再自上而下确定素短语的下限
  3. 找到最左素短语(可归约串) 然后进行后续归约(弹出,弹入操作)

核心目的是一直寻找终结符进栈,使得栈中的终结符从左到右,优先级先增大后减小,凸出来的部分就叫做素短语 。当有满足这种趋势的终结符的时候,就不停的移进终结符;不满足的话,就弹出,归约。

这种自下而上不是规范归约,这是找素短语,规范归约是找句柄

算符优先文法特点:

优点:简单、快速

缺点:能力有限,可能接受错误句子

尽管如此,算符优先文法仍然简单有效,广为使用。


规范归约显然相对算符优先文法比较复杂,但是它却是功能比较强大的。现在在自动机里的难点:就是我们需要找到句柄

LR()分析程序的功能:完成句柄识别工作。进而可以完成自下而上的归约工作。

LR(1)完成的是规范归约;

LALR(1)的速度更快一点 (LookAhead LR)

SLR(1) (Simple LR)

后面两种用的比较多;前面两种更加偏向理论

我们所讨论的LR(0),SLR(1),LR(1),LALR(1)四种方法只是针对于分析表的的产生。得到分析表后,由总控程序+分析表结合输入输出都是相同的。另外,四种方法最终得到的分析表是相同的,只是方法不同。

这一段总结的很好!!! 规范LR就是LR(1)分析法

算符优先方法 栈中放入的是句子,具体是句子的一系列终结符,根据栈内终结符和栈外终结符还有优先级表确定每一步走向;

LR方法 栈中放入的是状态,状态又分为"历史"状态,"未来"状态,根据栈顶的状态和输入符号,确定每一步走向

这种简单的状态图,就可以反映"历史" "未来"。已推导部分和待推导部分用 点号 区分。

终结符 移进;非终结符 等待归约\归约

栈中的都是历史,未来的用\(FOLLOW(栈顶)\) 表示 预测只与栈顶元素有关系。不是\(FOLLOW(T)\)就会出错

\(goto(S_m)\) 表示当前状态是\(S_m\) ,\(goto(S_m)\) 的值就是 \(S_m\) 状态跳转到下一个状态,相当于转移函数\(\delta\)

\(action\)表是动作表,有移进和归约两种动作;

\(goto\)表 是状态转移表

\(action\)表:当前栈顶状态为k,当前输入符号为a,这个时候应该怎么做?

  • 有四种情况:

    • \(action(k,a) = shift~~~~i\), 状态i移进栈顶

    a移进符号栈的时候,也要相应的给状态栈移进一个状态i

    • \(action(k,a) = reduce~~~~j\), 按照产生式j进行归约
    • \(action(k,a) = accept\), 分析完成
    • \(action(k,a) = error\), 发现错误

\(goto\)表:当符号栈进行归约之后,状态栈也要进行相应的弹出一些状态,再填入一个新的状态。 对应一个产生式\(A->\beta\) ,对这个产生式进行归约是发生在符号栈中的,但是要知道此过程中对应的状态栈也在发生一系列变化,比如弹出\(\\|\beta|\) 个状态,再填入一个状态(对应A)。

移进 常规的给符号栈、状态栈各移入一个

归约 先给符号栈完成归约,弹出状态栈的状态后,再结合\(goto\)表,移入一个状态


给出文法、分析表,检查句子是否正确(分析表构造后面再讲)

\(S5\)表示把5状态移进;\(r6\) 表示按6产生式归约

归约完之后要立即查询\(goto\) 表给状态栈中缺失的部分填入新的状态

\(goto(当前状态栈顶,已完成归约的符号栈栈顶)=新的状态栈顶\)

查表的时候是以状态栈和输入字符为主。


\(得到LR(0)项目->NFA->DFA->LR()分析表\)

这一个项目就是一个状态

活前缀:包含句柄的前缀之前的所有前缀

活前缀与句柄有三种位置关系:

对于这三种位置关系,我们的最终希望:都是盼望着句柄的所有部分都被放到符号栈中,即使没有如愿,我们也要采取办法把句柄塞进栈中。

一个句型是被拆分了的,栈中是前一部分表示已经发生了的"历史";栈外是还没有发生的,表示"未来"。

1 句柄已经形成

2 句柄未形成,且输入串即将输入的是终结符

3 句柄还未形成,且输入串即将输入的是非终结符。这个表示等待这个待归约的非终结符归约完成后,才可以进行自己的归约工作。

4 归约项目的特殊情况

实例:

\(文法——>LR(0)项目——>\epsilon-NFA——>DFA\)

多个终止状态是由于有多个变量分析完成。

\(NFA确定化成DFA\) \(\epsilon\) 状态合并成项目集 这里使用\(CLOSURE\)闭包方法。

  • 初始产生式的状态计算项目集(计算项目集就是把所有\(\epsilon弧\) 揉在一块儿,当成一个大状态 );
  • 然后由这个初始大状态集,读入一个文法符号,转向不同的大状态集(还是由\(\epsilon-closure\)求得,GO(I,X)=CLOSURE(J) ,J表示\(A->\alpha X.\beta|由A->\alpha.X\beta跨一步形成\) ) 读入X之后的的状态,对这个状态求取closure闭包,得到的是大状态集

分析表是由上面最后得到的DFA构造得到

例题:

已知文法,求LR(0)分析表

由这个DFA写出下面的LR(0)比较简单

状态+变量 = 状态转换函数

状态+常量 = 移进\归约 归约基本都是自动机的终止状态

LR(0)状态的0 表示在归约时,对于任何输入符号都会归约,对应到表上的话就是 6,7,8,9状态面对输入a,b,c,d,#都会发生归约。要知道这是LR(0),要是LR(1)的话,这里就不会全部归约。

典型的LR(0) 不关心下一个输入字母是什么

​ 我们希望的是读入不同的字母,选择不同的候选式;

闭包的计算:遇到终结符才会停止

goto函数其实还是闭包函数,goto函数得到的状态集合的闭包

归约确实是相对偏后一点的


SLR(0)

采用\(FOLLOW\)的方法,向前多看一个

SLR可以解决**移进-移进\移进-归约 ** 这个和自上而下的二义性实际是一种问题,只是自下而上叫做移进-移进\移进-归约 罢了。解决方法都是Follow法

A和B后面的字符不一样,这个**移进-移进\移进-归约 ** 就解决了

总体上SLR()表的构造方法与LR(0)分析表构造方法一样。

唯一区别的地方是归约时,不再是接受任意字符就能归约。而是只接受\(FOLLOW(产生式)\)集合里的终结符,才会归约。

反映到分析表上就是,左下部分不会再有一整行的情况了。

给LR(0)增加一些向前看的信息变成SLR(1)

goto表是个补充,action表是主体

一个状态对应一个状态规范族,里面会有多个项,多个项就可能会产生 移进-归约冲突,归约-归约冲突。

要是还是有这两种冲突呢??展望的更多

规范LR,就是LR(1)方法

LR(0)和SLR(1)的项目都是一样的; LR(1)的项目和前面LR(0)项目不一样

重新定义项目:使每个项目都带有k个终结符

与前两种LR(0)\SLR(1)步骤一样

新的项目就是 \([原来项目形式,终结符\cup \# ]\)

B旧版项目有3+2 =5种, ,后面的a,b,#有三种 5*3=15种

这种项比较大,后面的LALR(1)会减少项,伏笔

与LR(0) 基本步骤都是一样的,只是项目集不一样

得到DFA,填LR分析表。这里由于前面项目集不一样,所以填表有一点不一样

几种LR分析方法,造表的方法不一样,但是最终造的表是一样的。

既然表是一样的,导致我们的分析程序是一样的,构造表的方法不一样

上面的LR(1)分析方法缺点是状态太多。实际中甚至用不了

思路:发现 状态集除了搜索字符不一样之外,其他是一样的

解决方法:构造同心集。把表压缩

LALR略

实践中能用的是LL(1)和LALR(1)

LR(1)虽然最强,但是还是太复杂了,超出一般计算机计算能力

posted @ 2020-07-16 15:35  _Sandman  阅读(512)  评论(0编辑  收藏  举报