LR分析器
LR(0),SLR,LR(1),LALR
- 首先,LR(0)很简单,就是将文法构造成一个nfa,然后再转成dfa就行。在这个过程中,它可能出现移进-规约冲突和归约归约冲突。一旦出现冲突,则说明该文法无法使用LR(0)。
- 但对于一类移进-归约冲突,我们实际上可以在原LR(0)的基础上解决掉,如下图
可以发现,如果下一个终结符在 { a 1 , . . . , a m } \{a_1,...,a_m\} {a1,...,am}中,就移进;如果在 f o l l o w ( B i ) follow(B_i) follow(Bi)中,就归约。只要这些集合不相交即可。这就是SLR。 - 但是,上面的有待改进,看下图:
我们发现在 I 2 I_2 I2中出现了移进-归约冲突。这个如何解决?我们发现,对于 I 2 I_2 I2,它的困境是:遇到L时不知道是该归约成R还是移进=,而由于 I 2 I_2 I2是 I 0 I_0 I0的后继,说明此时在句首,那么可知,当遇到=时说明该使用1),遇到#时该使用2),因此需要通过后面的符号来进行展望,因此,LR(1)加入了1位展望符。
- LALR解决的问题是:LR(1)太大了,每次展望符不同都会产生不同的项目集,但是展望符只在归约时使用,那么为什么不能删去移进时的展望符呢?
我们可以发现,它生成的和SLR一样啊,那这样不会错误的归约吗?
事实上,它也会报错,但是会推迟报错。
比如id=id=id,对于LR(1),识别到id=时就到 I 6 I_6 I6了,此后发现输入id到 I 1 2 I_12 I12时,展望符不对,这是就会报错。
但是对于LALR,它输入id到 I 5 I_5 I5,依旧会归约出L,然后 I 6 I_6 I6遇到L归约出R, I 6 I_6 I6遇到R到了 I 9 I_9 I9,这时候发现,后面展望符不对,此时才会报错。
LALR分析能力小于LR(1),为什么?同时为什么LALR会引入归约归约冲突而不会引入移进归约冲突
LR的本质
编译原理书中已经提到了,LR(0)实质上是对一个NFA的改进(加了个栈来储存以前的信息),话不多说,直接放图:
但对于LR(1),书中却没有提它与NFA的关系,我认为它也是一个NFA,只不过是多加了一个展望符。我自己试验了一个例子,得出的结果与书中一致,因此,我认为,这个结论是正确的。(至于证明我不会,先用再说)
对于LALR,它的想法是合并LR(1)中的一些项目集,其实,合并的项目集都是点在产生式中间或前面的,因为对于这些产生式来说,展望符并不起作用,那么可以知道合并了也不会出现移进归约冲突。
既然我们知道了它们与NFA的关系,那么我们实际上没必要从头开始编写LR分析器,我们可以通过给定一个文法,计/算出所有的项目集,然后再通过项目集以及之间的关系,直接生成LR分析器,这样一来,我们实际上并不需要关心first集(LR(1)需要关心,它需要用first集来计算展望符,参考下面的问题”展望符计算“)和follow集,然后就可以生成分析器。
我的问题:1、LALR和SLR不知道如何通过NFA来实现。2、为什么既然可以在nfa的基础上实现,在书上还有专门介绍不用nfa,而直接生成下推自动机的方法,换句话说,两者有什么区别。
LR如何考虑优先级和结合性
对于一个项目集(状态),如 E − > E + E ∙ E->E+E \bullet E−>E+E∙ E − > E ∙ ∗ E E->E\bullet *E E−>E∙∗E E − > E ∙ + E E->E\bullet+E E−>E∙+E经典的LR分析器是无法处理这些情况的。我们如果想要处理这些二义性文法,就必须得引入一些规则,即优先级和结合性。
对于+和*,显然* 的优先级高,那么在这个状态接收一个*时,它会比较需要归约的产生式中的+,发现比+的优先级高,那么就先执行移进,而不是归约;在接收一个+时,它与需要归约的产生式中的+比较,发现两者优先级一样,那么就查看结合性,如为左结合,那么就优先归约,如为右结合,那么就优先移进。
对应于LR分析表的dfa,即该状态的*边得以保留,而+边被删去。(因为要优先归约,所以事实上不可能移进了)
我的问题:对于该状态,我们很好判断是要与+号比较,但是对于一个普通的情况,是否是跟产生式中的最后一个终结符比较
如何计算first集和follow集
计算first集和follow集的标准算法在编译原理的书上都有,也可以在网上迅速找到,但是,缺点显而易见:它判断是否结束循环的标准是该次循环是否改变了目前所有元素的first集和follow集,如果没有,就重新搜索一遍。可是,对于一些已经计算完成的元素,这么做无疑是浪费时间。
截止目前,我没有查到一个公认的更有效的算法。(可能是大佬觉得这个问题太简单,或者是我查的范围不够广)
我的想法是:我们通过spfa算法的思想,每次如果有元素更新,那么就入队,再通过队列里面记录的元素来更新与它相关的新元素,这样就可以实现对现有算法的优化了。
LR(1)中“……,a”中的a(展望符)如何计算
首先明确一点,比如对于一个项目:
E
−
>
T
R
∙
,
a
E->TR \bullet ,a
E−>TR∙,aa是属于E的follow集中的元素,它的意思是,如果后面跟着a,那么就选该产生式(这个情况只在点在产生式末尾时有效,否则就应该选点后面的元素)。
显然,在SLR中我们发现,仅简单判断该字符是不是属于follow集,然后再进行选择的方式对于一些情况也不适合,那么一个比较直接的想法就是:我们再将follow集进行细分,对于不同情况选取不同产生式,这个想法的体现就是展望符。
因此,它的计算方法与follow集有关。我们考虑如下文法:
S
′
−
>
S
S'->S
S′−>S
S
−
>
B
B
S->BB
S−>BB
B
−
>
a
B
B->aB
B−>aB
B
−
>
b
B->b
B−>b
显然有
1
、
S
′
−
>
∙
S
,
#
1、S'->\bullet S,\#
1、S′−>∙S,#之所以是#,我的理解是S’的follow集只有这个,只能填这个。(想不出更好的解释了)
它等价于下列式子
2
、
S
−
>
∙
B
B
,
#
2、S->\bullet BB,\#
2、S−>∙BB,#
3
、
S
−
>
B
∙
B
,
#
3、S->B\bullet B,\#
3、S−>B∙B,#
4
、
S
−
>
B
B
∙
,
#
4、S->BB\bullet ,\#
4、S−>BB∙,#,这些式子中的#是由于1中的S后面是#,所以,它们期望后面跟一个#。
然后2等价于
5
、
B
−
>
∙
a
B
,
a
/
b
5、B->\bullet aB,a/b
5、B−>∙aB,a/b
6
、
B
−
>
∙
b
,
a
/
b
6、B->\bullet b,a/b
6、B−>∙b,a/b因为2中B后面跟着B,那么在在接受第一个B后,它们期望后面跟的符号是第二个B的fiest集,即a和b。
3等价于
7
、
B
−
>
∙
a
B
,
#
7、B->\bullet aB,\#
7、B−>∙aB,#
8
、
B
−
>
∙
b
,
#
8、B->\bullet b,\#
8、B−>∙b,#因为3中第二个B后面没有符号,那么在接受第二个B后,它们期望的符号应该是3的展望符(因为3的展望符代表接受S后面期望的符号,自然就可以推出此处期望的也是)。
然后以此类推。
在该文法中没有给出一个特殊情况:如果B的first中包含
ε
\varepsilon
ε怎么办,那2等价于什么,解决方法是:去掉
ε
\varepsilon
ε,然后再加上下一个元素的first集。
LR如何处理 ε \varepsilon ε产生式
诸如:
S
−
>
ε
S->\varepsilon
S−>ε
由于它对应着一个NFA,那么通过NFA中的方法,就可以处理它。
终结符如何识别
遇到终结符,我们通过LR分析表,将能够通过该终结符转移的状态压入状态栈,当遇到一个接受状态时,设该接受状态中产生式右部的个数a,产生式左部符号为X,我们就可以删除状态栈里面的a个状态,然后设此时状态栈栈顶状态为n,我们再查找LR分析表,找到n能通过X转移到的状态,再加入状态栈。
因为LR分析表是一个dfa,因此,每条边都对应着一个元素,那么我们每转移一个元素,就压入一个状态,也就记录了这个元素。而当能够归约时,如果用符号栈(即为人手动模拟),我们需要删掉a个元素,对应状态栈也就是删掉a个,不过事实上删掉的元素不是对应状态,而是两相邻状态之间的边。
接受状态
如果文法给力,不出现移进归约冲突和归约归约冲突,那么一个接受状态中只能包括一个产生式,它的点在最后面。
如果出现移进归约冲突,那么一个接受状态就可以出现多个产生式,其中它有一个产生式点在最后面,其他的点在中间。
如果出现归约归约冲突,那么一个接受状态就会出现多个产生式,它们的点都在最后面。
对于移进归约冲突,我们可以通过增加LR(k)中k,即展望符的个数、定义优先级和结合性来尝试解决;对于归约归约冲突,似乎除了直接修改文法外没有好的解决办法。(还有一种可能的方法:如LR(0)分析器中发现状态n有归约归约冲突,但是如果前面的状态有移进归约冲突,而换了SLR后修改发现无法走到状态n了,这也可以解决,但是我感觉这个可能性太小)也可能是我太菜,没找到。
先LR(1)后LALR能否改变
我的问题:既然一个状态是同心的项目集,就可以被合并,那么可不可以在构造nfa时就给出提示,而不是弄出LR(1)后再减状态
目前想法:既然LR(1)中的非接受状态项目集不考虑展望符,那么能不能再处理完后,构建nfa前,将它们的展望符删去,然后再构建nfa,得到LALR。
select集能否代替first集,它与follow集的关系
这个问题实际上并没有必要,因为编译原理一书中并没有出现select集,但是在哈工大的mooc上老师介绍了select集,如果我没有记错的话,mooc中的LL分析器需要使用的就是select集。
select集实际上是比较简单的。
如果first(E)不包括
ε
\varepsilon
ε,则select(E)=first(E)。
如果包括,则select(E)=first(E)+follow(E)。
解释:
设符号栈栈顶为非终结符E,select集来判断终结符a是否能位于符号栈栈头。如果a能被E归约,那它显然可以,如果不能被E归约,但是E后面还可以跟非终结符R,而R可以产生a,同时E能产生
ε
\varepsilon
ε,那它也可以。
first集和follow集结合显然可以计算出select集,而在LL中first集和follow集的作用就是来计算select集,然后应用。但是问题在于,它在LR中没啥用,所以它没啥前途。
移进归约冲突能否通过延后来解决(先读入字符,然后判断执行归约或移进)
这个问题属于我初学时的,现在看有点脑残。
事实上,有延后来解决移进归约冲突的技巧,它叫LR(k),而就算遇到一个冲突的状态,此时不执行移进或归约,而是再读入字符,再进行判断也是没用的。
对于该状态:
E
−
>
E
+
E
∙
E->E+E \bullet
E−>E+E∙
E
−
>
E
∙
∗
E
E->E\bullet *E
E−>E∙∗E
E
−
>
E
∙
+
E
E->E\bullet+E
E−>E∙+E无论此时的输入是1+2还是1+2*3,只要不指定优先级,都没法分析下去。
在构造时,如果展望符为/不为集合,nfa转dfa最后会得到LR(1)吗?
此处我认为是会得到,因为在nfa转dfa时,会经过hopcorft算法。
对于该式
E
−
>
∙
a
B
,
a
/
b
E-> \bullet aB,a/b
E−>∙aB,a/b构造时,它实际上是两条:
E
−
>
∙
a
B
,
a
E-> \bullet aB,a
E−>∙aB,a
E
−
>
∙
a
B
,
b
E-> \bullet aB,b
E−>∙aB,b此时,它对应于nfa中的两个状态。
而两者除展望符外的形式相同,所以,通过这两个状态转移到的新状态必然也只有展望符不同,甚至是完全相同,那么在hopcroft算法时,它们就会被认为是无法识别的一个整体,那么最后就会被合并成一个状态,即为最终的状态。