本文是本人撰写的编译原理讲义。
本系列讲义适用于:被强迫学习编译原理前端,或者希望弄明白如何做科研的人。
1. 归约:人类语法分析过程核心
为编程语言设计出语法,终究是要给人用来编程。也就是说,语法一方面是用来组织语言的,另一方面也是要用来给机器和人来理解这个被组织出来的符号串的。
那么,既然前面单纯地以机器为中心的理解过程被证明暂时不太可行,那不如试一下分析人类是怎么理解一句话吧,然后模仿这个过程写一个分析算法?
还是随便先拿个简单例子,就"我是大学生"这句话吧。
首先人肯定是一个一个字去听,听的过程中如果发现了一个词,肯定会根据词性来进行归纳,然后再根据各种语法合并这些语法概念,直到最后发现这是一个句子。
下面的过程就是一个模拟理解这句话的过程,其中|模拟了读入的进度。
xml
我|是大学生 →<主语>是|大学生 →<主语><动词>大学生| →<主语><动词><名词>| → <主语><谓语>| → <句子>|
如果用语法树的构建来表示,那就是:

看起来,这样的理解方式似乎是为了给大脑减负:一方面我们有从左到右的阅读习惯,另一方面由于人类的短时记忆极其有限(通常只能维持 7 个左右的单位),所以总会想办法把已经读进去的符号尽可能抽象成一个语法符号,从而化简这段东西在脑海里的复杂度,方便理解。
那如果说组织语言是理解语言的逆过程,意味着组织语言就是类似于LL1一样的不断在非终结符中往下挖出来一棵树的过程,而顺序和我们的阅读顺序相反,是从右往左深度优先遍历。
之所以按照这种顺序,因为这样子左边相对右边要更迟推导出来,与理解过程中最先被处理相对应。
话说回来,理解这个词也太宽泛了,感觉离语法树这个语境太远了,换个什么词比较好呢。。
这些游兵散勇一样的单词,一簇一簇地归纳到了某个语法概念的旗下,有点像是约分、约简一样,把复杂的问题给简化。。。归纳。。约简。。归约???可还行。
在电脑敲下归约这个词的时候,输入法总是情不自禁把另一个同音词规约放到了首位。莫非这两词差不多??好像不对,规更像是规范,由于是名词,所以后面的约也更像是约定的约,跟归约的意思差远了!
说罢,便将归约设为了固定首位。
2. 找出句柄:基于归约的语法分析重点
总结下现在的情况:从左往右扫描待解析的命令,然后一旦发现可以归约的东西就进行归约。
Emm,怎么越想越像拼拼图的过程了:先把一个一个的拼图碎片按相似度拼出一个个模块,之后要是模块之间又有相似性,就再次拼装到一起。
那现在问题来了,拼图是有图像让我发现他们的相似性的;这符号串中,怎么去发现符号之间的关联性呢?
啊对,这些符号串是通过一定的规则,也就是产生式,从左部推导出右部伸展来的。
那我现在就应该反过来,在扫描过程中一旦从我手上已读入的内容中识别出了某条产生式的右部,那就进行归约呗!
产生式右部,这名字太长了,换个什么名字好呢。。它们就像是被识别句子的把柄一样,干脆就叫这些东西做句柄吧!
好好好,现在问题就变成了,怎么在扫描的过程中识别句柄呢。。?
等一下,识别识别,之前词法分析器识别的是单词,那能不能魔改一下DFA,让它从识别符号串构成的单词,变成识别由单词串组成的句子??
"还真别说,原理上完全可行啊,DFA核心也就是只要走到终态就成功识别,现在只要把激励从单纯的终结符变成和非终结符的并集,好像就能搞定啊!"午饭时间,师傅简单听了一下方案汇报,立马就给予了肯定。
"就是我感觉有几个小问题,你想想咋个解决。
首先,你刚刚不是说这个句柄是组装着突然发现的么,那我就当你是不断地扫描新符号,规约交替进行。但在马上就要组装出来的句柄之前,感觉有很大可能会有一些和这个句柄毫无关系的符号存在于你的识别缓存区中。
按照DFA的逻辑,只能是从开始符号接受正确的激励,只有能到达终态才叫做识别成功。那现在有一些无关的符号在你要识别的句柄之前,你就需要想出来一个机制去排除这些额外符号的干扰。或者说,即使有这些不相干符号提前给状态机一些无关的激励,也能让他最后识别正确。
第二个问题,你之前不是说过要有层次结构就必须要有栈么,那现在栈到哪里去了?先这么多吧,我稍后还有个会,先撤了。"说罢便匆匆离去了。
对啊,栈去了哪里呢?等一下,识别缓冲区?对啊,这不就是那个栈么!符号一个一个压入缓冲区里面,然后要是满足规约条件,就把这些符号组装成,或者说归约成一个非终结符,组装出来的东西就还是出现在缓冲区指示器的顶端,这不就是栈的后进先出嘛!
这么说来,和单纯的DFA相比,现在就是多了一个栈来保存过往的路径以及当前的状态!
那只剩下师傅说的那个问题了:怎么确保在初态不确定的前提下还能在合适的激励下走到终态呢?
等一下,初态真的不确定吗??
如果按照之前LL(1)里面First集合的思路,First(S)总能把句子头部可能出现的单词给求出来;而Follow(β)总能把这个符号后面可能有哪些符号给算出来,而且还很大可能不是符号表的全集。
这说明,符号之间的前后顺序肯定有关联的,也就是说,句柄前面的符号应该也是能计算出来的!
那如果,我能提前计算出,从句首一直到句柄开始前会有哪些先导的符号,然后把这个状态作为识别句柄的开始状态,接着拼接那个识别具体句柄的DFA,不就解决了这个问题了??
所以接下来,就应该把所有状态都列出来,再找出他们的相互关系。
3. 进度条=产生式+指示器=状态
既然知道大方向是要算出来特定句柄的前缀,那么就老方法,从简单例子那里总结吧。出来吧随机文法生成器!
习题1:文法G[E]
css
E→aAc
A→b
毫无头绪的时候,人就喜欢乱点鼠标。
鼠标在a的左边点击一下,闪呀闪呀。按一下右键,按一下右键。。。
Emm,怎么感觉 ,这个扫描、组装的过程,完全就是光标扫描的过程:
就像是,一开始有一个光标从a的左边开始闪烁,然后接受一个a之后,光标移动一下;来到A之后,光标在第二行左边开始闪烁。这个时候上下两层平行世界都在等待各自想要的符号。然后b读入之后,A的这个式子的光标移到了最右边,这个b归约成A,第一条式子心心念念的A这不就拿到了,最后再加上c,结束。
如果把"E->|aAc"看作是一个时刻,接收完a之后的场合明显不一样了,那就可以看作是发生了状态转移,变成"E->a|Ac"!然后这个时候由于要进入A这个子程序,这个时候的状态似乎就应该包括这个子函数的产生式组装进度。
对啊,指示符在串中有不同位置,这完全就是个进度条!而进度条本身就可以代表状态,那么只要把句柄出来之前的状态全部列出来,就可以把DFA拼凑完整了。不过这个竖线光标也太容易和字母l或者或 | 混淆了。还是用个间隔号 "·"来做进度指示器吧!
所以,对于同一个产生式的两个不同进度条
a. X→X_1 X_2...X_(i-1) · X_i X_(i+1)...X_n
b. X→X_1 X_2...X_(i-1) X_i · X_(i+1)...X_n
可以把它们看作是不同的状态。
如果X_i是终结符, 意味着当处于状态a时,则只需读取X_i,就会让状态a跳转到状态b;此外,两个状态之间应连一条标记为X_i的弧
如果X_i是非终结符,意味着当处于a状态时,需要进入到X_i的子程序中。
假设存在如下以X_i为左部的产生式:
c. X_i→·ab
d. X_i→·ac
e. X_i→·bc
意味着X_i可能存在多个分支。而这些产生式实际上与b一样都处于同样的状态。
因此与a产生式处于同一个状态的进度条有a、c、d、e四条。
假设该状态为A,且系统正处于状态A。
则当输入符号a时,所有可以处理符号a的产生式进度都会更新,进入只包含以下两条进度条的状态B:
f. X_i→a·b
g. X_i→a·c
意味着所有不能处理a的产生式全部消失。
当某个进度条到达100%,也就是进度条指示器到达了最右边,意味着前面的零件已经足以组装成产生式左部的非终结符,之后就可以把缓冲区中所有符号用于归约,并得到新的非终结符X_i。
由于此时相当于从子程序中返回,应该回到调用子程序的位置,也就是状态A。
意味着:状态A获得了非终结符X_i,X_i进入符号栈;而只有a进度条可以处理X_i,因此更新后得到状态b。
啊!!爽!!!又多一个神奇算法了,叫什么好呢。。
首先它也是从左到右(Left to right)扫描,也给一个L开头吧;然后每次都是把当前输入串的最右(Rightmost)句柄进行归约;然后完全不用像LL(1)那样往前看哪怕1个符号。。那就叫它LR(0)分析法吧!
4. 盲目的LR(0)分析法
4.1 拓广文法
来来来,赶紧打铁趁热试试别的案例。
习题2:分析串a#在文法G[S]下的语法树
css
S→A|a A→S
首先,这是个以S为开始符号的文法,那么就从S的产生式为始,写出来相关的进度条。而由于S→·A需要进入A这个子程序,所以还要加上一条A→·S。啊等一下,接下来又由于A调用S这个子程序。。还写吗?算了先不写吧,反正写了跟上面两条也是一样的。
然后,对状态0中的每个进度条挨个施加不同的激励,生成新的子状态,得到状态1、2、3。
css
状态0: S→·A S→·a A→·S
状态1: S→A·
状态2: S→a·
状态3: A→S·
从状态A开始,读入一个符号a,跳转到状态C;既然C的进度条已经满了,那就直接归约成S了呗。。等一下,那我现在应该在。。状态0,获得了S。。接下来我是该跳到状态3,还是直接结束??假如跳到状态3,就又会经历归约,状态0遇到A。。这不循环了吗??啥时候是个头?
哎,既然a后面都跟着句子结束符#,那就应该直接结束才对。
估计这文法存在自环的还不是个例,得想个通用方法来处理。
要不这样吧,每次分析前,为所有文法加上一句:S'→·S#作为初始进度条。这样一旦归约出了S,状态就会跳转成S'→S·#。接下来如果看到输入串已经结束,那就表示算法结束。
这么修改之后,开始符号S'只出现一次,这样就避免循环啦。
修改之后的文法比原来拓展增广了一个产生式,干脆就叫他拓广文法吧!
4.2 LR(0)的完整运行架构构建
从初始文法G[S]开始,先对其进行拓广,增加新产生式,文法变成G[S']。
然后通过对初始产生式S'→·S#开始处理:
-
如果点在非终结符(假设为A)旁边,就把子程序A的初始进度条加入到这个状态中;
-
如果点在终结符旁边,那就是在等待发现符号a;
在上述操作无法创造新产生式之后,就对这个状态中施加可以处理的符号作为激励,从而得到新的状态进度条;原状态中不能处理这些激励的,在新状态中消亡。
由于每个状态都包含了多个进度条,就叫他们做进度条集合;而通过这样不断施加激励,生成的都是它的兄弟姐妹,所有的状态构成的集合就叫他们进度条集合大家族吧!
有了状态之间的跳转关系,为了方便查表,肯定要弄一个类似状态转移表那样的东西出来啦。算了,为了做区分,还是把这个表叫做LR(0)分析表吧。
首先把状态写到第一列上,把对应的终结符、终结符和#写到第一行去。接下来,如果状态A遇到符号a实现了正常的状态跳转,就在对应的格子中直接填写对应的数字;如果那个状态对应的是归约操作,那就直接为整行都覆盖同一个标记,这个标记的首字母是归约的首字母r,其后跟着对应产生式的序号,这样就可以知道是用第几条式子归约。最后,对于S'->S·#这一条,在#下面对应的格子里填上acc(ept),表示语法分析结束,句子被接受。
运行的时候最重要的两个东西,一个是当前状态从哪个状态来的历史记录,一个是我手上有什么符号。
那干脆,这些东西全都用栈来记录吧!Emm,好像有点问题。状态我可以用整形来存,符号是字符型变量,两边搞到同一个数据结构去太烦了,干脆拆成两个不同数据类型的栈好了。
等一下,系统一开始要处于初态0,这个时候没有输入符号,两边对不上了呀!算了,反正也是占位符,加一个#在状态0对应的符号栈那里。这样结束的时候符号栈上正好是#S,加上还没有输入的#,正好构成了#S#,完美对称。
好好好,可以跟老板汇报了!
4.3 命名权:归老板所有
老板听完了汇报,点点头表示认可。
然而师傅敏锐注意到,老板每每听到进度条、进度条集合、进度条集合大家族的时候,都会不自绝地皱眉。于是他试探到,"老板,我感觉进度条这几个词的起名好像不够高大上,要不您用您跨学科思维给小年轻打个样?"
老板不好意思笑笑"倒也没啥太好的想法,我就觉着这进度条这名字太长了。我看它本身也就像文章里面那些item一样一直往后列,干脆就叫项吧?"
好好的进度条被改口叫个不知所谓的项,表情一个不小心没控制住。还好师傅眼疾手快,赶紧碰了碰提醒了下,才恢复了表情管理。
"然后是大家族这个词确实太,接地气了。我学复分析的时候记得里面有个叫规范族的概念。我看你这个所谓家族也是按照一定规范构建的,干脆就叫项目集规范族怎么样?"
这一点倒是没意见。哎,反正命名啥的你爱叫啥叫啥吧。
"这样,你交给测试部门测一下,看看这玩意儿的通用性如何。好好过个周末吧!"
4.4 项的分类与冲突
周一,刚到办公室,发现测试部门的报告已经呈上来了。
LR(0)的测试报告
本报告使用提交的LR(0)文法对定义变量的已拓广文法进行了分析:
(习题3)
css
0.S′→S
1.S→tD
2.D→D,i
3.D→i
在构建项目集规范族的过程中,发现其中有一个状态,同时存在两个冲突的操作:
css
S→rD· D→D·,i
可以看出,该状态既可以直接进行归约,也可以等待下一个逗号的移入。
如果直接归约,则永远无法解析形如int a,b的串;
如果忽略归约,直接等待移入,则其他状态都无法归约出S,导致算法无法结束。
鉴于LR(0)分析方法未能解析常见的变量定义语法,其在使用上存在较大限制。
测试结论:不通过。如需要详细测试数据可联系工作号**********。
看完报告,心凉了半截,脑子里只有一个想法:什么鬼,模版上的星星都不删干净就交出来了么,就这细致程度还好意思说LR(0)不好用么 。
唉算了,这次确实是我这边没测好就交导致的。知耻而后勇,看看能不能举一反三把其他类似问题一起找出来。。
现在的问题应该是我之前只考虑了状态中只会有一个进度条满了可以归约,但其实还有可能出现其他可能的操作,包括但不限于,等待下一个终结符,非终结符。。怎么感觉情况很杂的样子。。
算了,要不我先根据圆点的位置和后续符号对项分类看看:
1.移进项目: 形如A→α·aβ,a∈V_T的项。
2.待约项目: 形如A→α·Bβ,B∈V_N的项目,表明要等待后续串归约成B之后才能继续分析。
3.归约项目: 形如A→α·的项目。它表明一个产生式右部即句柄α已形成,可归约成A。
4.接受项目: 形如S'→α · #的项目,表明语法分析完成。
哦哦,一共才四类嘛,并没有想象中的那么复杂。
那再试着总结一次:
LR(0)只能处理项目集中单单只有一个归约的情况。
假如除了归约项目以外还有移进项目,那么就会导致移进-归约冲突;
假如项目集中有两个或以上归约项目,那么可以肯定的是,这些项目的左部肯定不一样,那对于要用那条式子进行归约就又成问题了。对于这类问题,称为归约-归约冲突吧。
假如除了归约项目外还有待约项目,那这个待约项目首先会被当作子程序处理,生成新的移进项目或者待约项目;对其中的待约项目又会重复循环,直到全部生成了新的移进项目或类似A-> ·这样的空串产生式对应的归约项目,那就又对应回上面两类冲突。因此,不存在归约待约冲突。
假如项目集中除了归约项目外还有接受项目,这种情况嘛,当然是结束优先,不存在冲突!
等一下,会不会有移进-移进冲突??
应该不会吧。。?移进移进冲突不就是说有多条移进项目么。这些项目在遇到处理不同的符号自己就会挂掉了,还冲突个啥啊。
再想深一层,这些冲突的核心到底是什么?
我倒是知道政治老师教过现实中冲突的核心无非就是资源的争夺╮( ̄▽ ̄)╭。总不能这句话也能用在这里吧。。哎?还真别说,如果把归约对应的动作在于把符号栈的元素出栈,如果把这些符号拼装成终结符,就再也回不去了。无论后面再来些什么符号,也没法拼装出正确的非终结符,这就解释了两种冲突。同时,这个也可以解释为啥不存在移进移进冲突了:那些符号都在栈上,一切都可逆,所以就没有冲突啦!
总结起来就是一句话:归约导致了栈上面的符号回不去,所以一旦在这个状态中无法区分自己到底应该是要移入,还是选择哪些特定的符号进行归约,就会出现相应的冲突。
5. 粗粒度与细粒度:Simple LR(1) 与 LR(1),
5.1 LR(0)的升级思路
既然现在知道了一切的犹豫都来自于信息不足,那么改进的方向当然是让它获得更多信息啦。至于什么信息,(看看表)吃完午饭再想。
悠哉悠哉来到园区门口,恰巧目睹了一起争执:只见一辆丰田凯美瑞停在出场区这里,估计是外来车没登记,要等待保安手动抬杆。没想到那保安的脸色黑得吓人,跟那车子吼了几声,说的"不要按喇叭,我们会给你开的。"司机似乎是一脸懵,回复说"我也妹按喇叭呀?"保安没好气说"昨天按喇叭的不是你吗?丰田银色凯美瑞,我都认到你了。"司机一脸无奈:"我今天第一天来啊,你说的他的车应该不会也是粤E的吧?"貌似这个时候保安才留意到车牌这回事,看了一眼,没搭话,车也就开走了。

吃饭的时候跟师傅聊起这茬,师傅感慨道:"说句政治不正确的话,有时候真不由得感慨,某些人的职位真的对得起他的智商。你说他记不住车牌也就算了,居然连粤E粤S都完全不分,还那么情绪化,难怪口碑一直都那么差。假如他能稍稍能聪明一点,在摆臭脸之前稍稍看一下这个车的来源,估计也没这么尴尬。"
说者无心听着有意:既然现在LR(0)需要更多的信息,往往就是不知道归约的时机对不对,就像是那个保安,眉毛胡子一把抓,看到银色丰田凯美瑞就摆臭脸,那肯定不行。既然现在要归约成特定非终结符,比如A,那如果发现后面的符号a确实好在Follow(A)之中,又不会有其他移进符号等待这个符号a,那就可以直接进行归约而无需纠结啦;相反,如果这个符号a被其他移进项目虎视眈眈,但又不在Follow(A)之中,那也可以马上确定就是要先处理移进项目。这样冲突就解决啦。
师傅听完,点评道:"我是看那保安可能记忆力有限才让他只记住前两位,假如他记忆力够何不直接记车牌?那多精准。
回到你的算法上,现在又没有成本限制,你何不让这算法一步到位?
假如有S=>*αAβ_1,S=>*αAβ_2,那你的Follow(A)实际上就对应于First(β_1)∪First(β_2)。如果你直接给A的每一条产生式求它之后出现的β_1的First集合,那就能区分得更细,遇上冲突的概率肯定要比你Follow(A)要低。"
(想了好一阵)要不,我还是两个都实现一下,弄两个不同精度的算法。这样复杂的文法用复杂的算法分析,简单的就用简单的应付,这样成本啥的也不至于太高。师傅你说的那种高精度算法,由于总要往前看一个符号,干脆就直接叫LR(1);而求Follow的版本,算是LR(1)的简化版,干脆就叫SLR(1)。
师傅赞赏地点点头,"你这让我我想起鲁迅的一句话:人的智能,就体现在会根据不同成本创造和使用合适的工具!"
我也想起鲁迅说的另一句话,"我没说过"。哈哈哈哈哈哈
5.2 粗粒度的SLR(1)
总结下来,SLR(1)的核心也就是这么一段话:
如果某个项目集I={X→α·bβ, X→α·cβ, A→γ·, B→δ·}, 对于输入符号x有:
-
若x∈{b,c},移进;
-
若x∈FOLLOW(A),则用产生式A→γ规约
-
若x∈FOLLOW(B),则用产生式B→δ规约
-
若x不在这三个集合中,则句子有错
如果3个集合互有交集,则说明这种思路无法处理冲突,这个文法就不是SLR(1)文法。
相比于LR(0),只要在有冲突的集合中,多计算一下归约项目中等待被归约的非终结符的Follow集合,只要和其他移入项目归约项目不冲突,那就完事了。
由于SLR(1)没有改变构建项目集规范族的方式,所以原来的LR(0)项目集规范族这个名字倒还可以继续用,就是那个LR(0)分析表得改名叫做SLR(1)分析表了。
毕竟,原来的LR(0)分析表最大的特点就是一旦到了某个需要归约的状态后,就不管三七二十一整行都填满rx。现在相当于更细化了,只有Follow(A)中的符号才需要填rx,其次就是根据移进项目在对应的格子中填写要跳转的项目。
5.3 细粒度的LR(1)
比SLR(1)只在归约的时候只计算全局性的Follow(A)更进一步的,是LR(1)需要在每次归约的时候都得知道接下来跟着当前产生式的是什么符号,而这个信息是局部的。
比如,对于S'→S#, 谜底就在谜面上,只有当看到#,才能进行接受;
再比如,假如有第二条产生式S→Ab,那么假如要归约成S,就需要看到上一条表达式中S后面的符号,也就是#;
又比如,假如有第三条产生式A→a,那么根据前面一条产生式可知,这条A能归约的条件就是看到b。
要注意的是,尽管归约条件只有在归约的时候才会真正被用到,但由于要计算的信息是局部性的而不是像Follow那样的全局性,因此只有在计算LR(1)项目集规范族的时候把归约所需条件层层传递,才能让这些信息到达有需要的项中。
另外比较容易发现的是,即使这个产生式带了进度指示器,它的归约条件也不会变化。
后面为了规范化,将以类似"A→α·Bβ,a"的格式来表示某个项及其归约条件。其中逗号为间隔符。
我已想到一种类似于DFA探索地图一样绝妙而简便的方法,从而不混乱地进行LR(1)项目族规范族的构建。所幸这里有大把空间,可以用如下已拓广文法为例进行说明:
(习题4)
markdown
0. S′→S$
1. S→BB
2. B→aB
3. B→b
首先为开始符号S'产生式生成带归约条件的项目:S′→·S, $。(这么说来S后面还真加不加#都没所谓,反正知道S'就是收到S看到句子结束符就acc就好。)
生成一个有四列的表格,如下所示。其中,第一行数据填上:状态名称:0;来源:空;初始项:S'→·S,,以及求子程序所增加项:{S→·BB,;B→·aB, a/b;B→·b, a/b}。
在初始条件准备妥当后,就开始循环:
每一轮中,对当前表格中还没被完全处理完毕的状态,找出它里面还没被处理的项,然后对这个项施加可以让它的进度发生变化的激励,如S、B等,从而产生新临时状态的初始项。如果这个初始项从未出现过,则确认为发现了新状态,记录在表格新的一列中。同时还可以在这个已处理的项后面标记接下来会进行的相应操作标记,从而防止重复处理该项,并方便后续LR(1)分析表的构建。
重复上述过程,直到没有新状态产生,所得项目集即为LR(1)项目集规范族。
状态名称
上一个状态(来源)
初始项
求子程序所增加项
0
S'→·S,$,1
S→·BB,$,2
B→·aB, a/b,3
B→·b, a/b,4
1
0
I1: S'→S·, $,acc
2
0
S→B·B,$,5
B→·aB, <math xmlns="http://www.w3.org/1998/Math/MathML"> , 6 B → ⋅ b , ,6 B→·b, </math>,6B→⋅b,,7
3
0, 3
B→a·B, a/b,8
B→·aB, a/b,3
B→·b, a/b,4
4
0, 3
B→b·,a/b,r3
5
2
S→BB·, $,r1
6
2, 6
B→a·B,$,8
B→·aB, <math xmlns="http://www.w3.org/1998/Math/MathML"> , 6 B → ⋅ b , ,6 B→·b, </math>,6B→⋅b,,7
7
2, 6
B→b·,$,r3
8
3
B→aB·,a/b,r2
9
6
B→aB·,$,r2
接下来就可以构建相应的LR(1)分析表了
a
b
$
S
B
0
3
4
1
2
1
acc
2
6
7
5
3
3
4
8
4
r3
r3
5
r1
6
6
7
8
7
r3
8
r2
r2
9
r2
5.4 LALR(1): LR(1)的成本优化方案
我去,终于写完这10个状态了,LR(1)分析实在是太繁琐了,这工作是给人类做的吗??要不是非要写文档,这工作机器做绝对比人强啊!
而且话说回来,上面这个文法中的状态4和7,状态8和9,以及状态3和6,他们除了归约条件不同以外所有项都一样的呀。这相比于LR(0)的项目集规范族,完全就是脱裤子放屁啊。
既然前面都试过用奥卡姆剃刀来化简状态,那LR(1)的状态估计也是可以被美容一下的。
状态名称
上一个状态(来源)
初始项
求子程序所增加项
0
S'→·S,$,1
S→·BB, <math xmlns="http://www.w3.org/1998/Math/MathML"> , 2 B → ⋅ a B , a / b / ,2 B→·aB, a/b/ </math>,2B→⋅aB,a/b/,3,6
B→·b, a/b/$,4,7
1
0
I1: S'→S·, $,acc
2
0
S→B·B,$,5
B→·aB, a/b/ <math xmlns="http://www.w3.org/1998/Math/MathML"> , 3 , 6 B → ⋅ b , a / b / ,3,6 B→·b, a/b/ </math>,3,6B→⋅b,a/b/,4,7
3,6
0, 3
B→a·B, a/b/$,8
B→·aB, a/b/ <math xmlns="http://www.w3.org/1998/Math/MathML"> , 3 , 6 B → ⋅ b , a / b / ,3,6 B→·b, a/b/ </math>,3,6B→⋅b,a/b/,4,7
4,7
0, 3
B→b·,a/b/$,r3
5
2
S→BB·, $,r1
8,9
3
B→aB·,a/b/$,r2
对应的分析表:
a
b
$
S
B
0
3,6
4,7
1
2
1
acc
2
3,6
4,7
5
3,6
3,6
4,7
8,9
4,7
r3
r3
r3
5
r1
8,9
r2
r2
r2
合并后没有任何冲突发生,说明这个方法是work的,状态得到了削减!
命个什么名好呢。。这个算法合并了LR(1)中内核相同的状态,那干脆就叫LA(kerneL Aggregation)LR(1)吧!
......
师傅看完递交上来的三个算法方案,随口吐槽道:"前面两个暂且不说,光看你这LALR(1),我还以为是Look-Ahead LR(1)呢,不过想想那个1已经是向前看一个符号的意思了,想必你也不会脱裤子放屁多讲一遍。
另外我感觉你这LALR(1)虽说是少了状态了,但是它解决冲突的能力应该是要差过LR(1)?"
嗯是的,在极端情况下如果LR(1)原来的状态中如果有如下两个状态:
-
状态 1:
-
A->α·,{a}
-
B->β·,{b}
-
状态 2:
-
A->α·,{b}
-
B->β·,{a//其实这里是啥已经不重要了}
这样合并之后A->α·的归约条件集合就一定会和B->β·的有非空交集,归约-归约的纷争就开始了。
相反,合并内核是绝对不会产生移进-归约冲突的。
原因在于:要产生移进归约项目,说明这个状态内部本来就要有移进项目。合并状态之后,也只是把它们的归约条件合并了。只要原来没有冲突,那么说明这个移进项目等待移进的符号与原始状态的归约项目的条件不会冲突,所以合并后也绝对不会冲突。
一旦合并后有冲突产生的, 就不叫LALR(1)文法;
只有合并后没有出现冲突的,才叫LALR(1)文法。
他们四兄弟按能力排序应为:LR(1)⊇LALR(1) ⊇ SLR(1) ⊇ LR(0)
师傅心中os:"为啥用包含号不用大于号来着?啊对,这是四大类文法,是四个集合,比大小得用包含符号。"
6. 二义性及其处理
既然自顶向下语法分析、自底向上语法分析都开发完成了,两人目光便又落在那个一切的开端,if else文法。似乎只要解决这一切,客户的其他问题便都能迎刃而解。
(习题5)
S→if B S | if B S else S
给出句型if B if B S else S的语法树。


啊,这,都不是LR(1)文法,分析个屁╮( ̄▽ ̄)╭,连这么简单的文法都搞不定,搞了这么久白忙活了。果然这语法写得太过简单就是在为难我胖虎编译器。要是写成if end或者end if这样的形式就容易分析多了。
师傅:"哈哈毕竟编译器的工作就是为人类服务嘛。等一下,我感觉倒也不用灰心,我发现他的LR(0)分析表还挺有意思的。你看,歧义就只出现在(4,'else')这个格子里。
if
B
S
else
0
2
1
1
acc
2
3
3
2
4
4
5,r1
r1
5
2
6
6
r2
r2
你之前的LR系列文法出现冲突其实都是同样的问题对吧?那时候都觉得无法处理是因为我们不知道什么时候该走那条路,因为文法太抽象了。但对于if else,咱们可以引入一些规则来辅助判断呀!
比如,可以要求写代码的人记住'if只跟最近的else匹配'。一旦加上这条规则,意味着只要if看到后面有else,就决不会自绝于代码中,这样咱们直接就把这个格子中的r1去掉就好啦。别愣着了,规则是人定的,树挪死人挪活。"
师傅我突然想起九品芝麻官中那个歧义梗:"年租银两三十万不能转租别人"不是有两种解读方式嘛?现在看到这个语法树我才明白,这所谓文法的歧义就是指文法本身有问题,会导致一句话可以对应多棵语法树!
师傅点点头:"你这么一说,之前正则文法说是对应正则语言。那文法对应的也应该是语言。如果文法会导致歧义,那我估计有些语言天生就是带有歧义的,无论你用什么文法去描述都有歧义。
关键是,我感觉这歧义最麻烦的是没有通用算法直接判定,唯一能做的就是反证法,直接举个反例,说这里有个句子有多个语法树,然后这个文法就是带歧义了。
不管怎么说,咱们也算是把if else这个Boss给解决了!"
7.和LL文法的对比
"所以,你们搞出来的这个LR分析,到底比前面的LL强在了哪里?"老板还是有点没搞懂。
简单来讲,LL(k)即使k很大很大,那也是一个有限值。也就是说,它的目光是有限的。而LR分析技术,它就像个闷骚怪,先不声不响吸收了一大堆知识,等到他觉得可以做决定的时候再进行消化。这样他就具有了后手优势,总是会比先手的LL类要少出错。也就是说,它们的能力并不等价。
"但你说的这个出错我觉得核心在于你限制了它不能犯错吧?只要我允许它犯错并回溯,这样的话两者应该是完全等价的才对。"
您说得挺有道理,如果允许无限回溯,那么它们对上下文无关语言的识别上确实是等价的。经您这一提醒,我感觉LR有一个问题就是全部转成状态之后,代码太抽象太难调试了。如果可以让LL分析真的像DFS那样自顶向下一层一层递归解析,感觉写起来也不复杂呐。虽然说可能又让代码与逻辑灵肉纠缠了,但只要语言没有变态性的发育,这么点小修补应该还是能接受的。