形式语言与自动机13 自下而上分析方法 LR类分析方法
语法分析之自上而下
一、上下文无关文法与程序结构
这种文法的 格式(模式) \(A->\alpha\) \(\alpha\)是变量与常量组成的字符串。
化成乔姆斯基范式(CNF)和格林巴赫范式(GNF)是为了写程序做准备
通常我们程序句子可以划分如下:
- 说明、定义语句
- 普通语句
- 表达式语句以及赋值语句
- 控制语句
- 函数语句
要是一个文法能够把这两大类(四小类)语句能够描述清楚,就可以说其能够描述程序。
顺序语句文法:
控制语句文法:
二、语法分析的任务与分类
语法分析可以作为主函数,按需调用词法分析器。不用一下子全部把Token流写出来。
符号表是一种数据结构,可以读写。供以后使用。
归约:从终结符串(句子、常量串) 一直到变量S
三、自上而下分析面临的问题
文法的左递归;
回溯;
替换符的顺序会影响接受的语言; \(A->ab|a;A->a|ab\)
难报告出错位置
引起上面这些问题的不确定性有3种:
选择哪一个非终结符、选择哪一个产生式、处理句子时读入几个单词符号(Token )
-
仅有产生式选择是非确定的
采用穷尽+回溯的方法
需要对上面的文法做一定的限制
消除左递归 略
消除回溯;
图示情况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)文法,就采用两个条件即可:
- $FIRST(\alpha_1) \cap FIRST(\alpha_2) \neq \Phi $
- \(FIRST(\alpha_1) \cap FOLLOW(A) \neq \Phi\)
要是不是 \(\Phi\) 的话,都意味着会有两条产生式仍然面临回溯。也就是说只要有交集,那么就不会是LL(1)文法。
面对这种非 LL(1)的文法,两条产生式在一个单元格。我们强行删掉一条。(强删一条也就是约定,既然约定了,也就面临抉择,不会产生回溯,选择是唯一的)
改的方式简单粗暴,但是会不会对文法产生较大影响,具体问题具体分析
对于规范归约 ,它的可归约串是:拿着句柄归约。
不同归约方式的可归约串不一样。
规范归约——最左归约;
规范推导——最右推导,规范推导得到的句型叫做 规范推导 。
规范归约非常好,难点是句柄的找到算法(不需要把分析树画出再找)
句柄一定是在栈顶(因为我们都是在栈顶完成归约)
通过优先级表,区分移进和归约
操作符栈顶的优先级最高,栈底的优先级最低。将操作数和操作符放在两个栈中
算符优先分析表构造(终结符优先级表) 任何两个终结符的优先关系
从算符优先文法 构建 算符优先关系表
注意比较:\(FIRST(\alpha),Follow(A),FIRSTVT(P),LASTVT(P)\) 是不同的
\(LASTVT(变量),FIRSTVT(变量)\) 是终结符的集合 。
对文法拓展,就是在终结符中加入 #(原来的表增加一行一列),然后构造优先关系表
构造文法的算符优先关系表的时候,应该遵循下列步骤:
-
开始符号 变成 次开始符号 文法改造。添加一条产生式:\(S'->\#S\#\)
-
计算每一个非终结符的\(FIRSTVT(P),LASTVT(P)\)
-
得到集合后,逐条扫描产生式;
- 变量在左边 常量在右边 比如:\(E~+\) 就会有:\(LASTVT(E)~>~+\)
- 变量在右边 常量在左边 比如:\(+~E\) 就会有:\(FIRSTVT(E)~>~+\)
-
得到这张表以后,就可以用简单分析文法来处理算符优先关系
算符文法:一个文法的产生式不含有连续两个变量。比如:\(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得到的常量终结符串进行寻找素短语行动,先确定栈顶,栈顶确定后再自上而下确定素短语的下限
- 找到最左素短语(可归约串) 然后进行后续归约(弹出,弹入操作)
核心目的是一直寻找终结符进栈,使得栈中的终结符从左到右,优先级先增大后减小,凸出来的部分就叫做素短语 。当有满足这种趋势的终结符的时候,就不停的移进终结符;不满足的话,就弹出,归约。
这种自下而上不是规范归约,这是找素短语,规范归约是找句柄
算符优先文法特点:
优点:简单、快速
缺点:能力有限,可能接受错误句子
尽管如此,算符优先文法仍然简单有效,广为使用。
规范归约显然相对算符优先文法比较复杂,但是它却是功能比较强大的。现在在自动机里的难点:就是我们需要找到句柄。
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)虽然最强,但是还是太复杂了,超出一般计算机计算能力