好的,我来详细讲解LR(0)项目的概念和构造方法。
LR(0)项目是在某个产生式右部插入一个点号⋅后得到的表达式。设有产生式A→α,其对应的LR(0)项目形式是:
A→α1⋅α2
其中α1α2=α,点号⋅表示当前分析到的位置。比如对于产生式E→E+T,它可以产生以下四个LR(0)项目:
E→⋅E+T
E→E⋅+T
E→E+⋅T
E→E+T⋅
LR(0)项目的重要性质:
- 点号⋅在最左边,表示准备用这个产生式进行归约
- 点号⋅在最右边,表示已经完成了这个产生式的归约
- 点号⋅在中间,表示正在用这个产生式进行分析
为了构造LR分析器,我们需要计算LR(0)项目集的闭包。给定一个项目集I,它的闭包CLOSURE(I)的计算规则是:
- I中的所有项目都属于CLOSURE(I)
- 如果CLOSURE(I)中有项目A→α⋅Bβ,且有产生式B→γ,那么项目B→⋅γ也属于CLOSURE(I)
- 重复应用规则2,直到不能添加新的项目
例如,考虑文法:
SEETT→E→E+T→T→(E)→id
对于项目S→⋅E,它的闭包计算过程是:
- 初始项目:S→⋅E
- 因为点号后面是E,所以加入E的所有产生式:
- E→⋅E+T
- E→⋅T
- 因为新加入的项目中点号后面有T,继续加入T的产生式:
- T→⋅(E)
- T→⋅id
最终得到的闭包是:
CLOSURE({S→⋅E})={S→⋅E,E→⋅E+T,E→⋅T,T→⋅(E),T→⋅id}
另外,我们还需要定义GOTO函数,它计算项目集I对于文法符号X的后继项目集:
GOTO(I,X)=CLOSURE({A→αX⋅β∣A→α⋅Xβ∈I})
即把I中所有点号⋅后面是X的项目的点号向后移动一位,然后求闭包。
这些概念和操作是构造LR(0)分析器的基础,通过这些可以构造出完整的LR(0)状态集和状态转换图。这些项目集最终会用于构造LR分析表,指导语法分析过程。
让我先从拓广文法讲起,然后详细解释LR(0)项目集和项目集规范族。
一、拓广文法
对于任意文法G,其拓广文法G′通过在原文法基础上增加一个新的开始符号S′和一个新的产生式S′→S得到,其中S是G的开始符号。这样做的目的是:
- 保证分析过程只在最后一步完成归约
- 使新的开始符号S′只出现在一个产生式的左部
比如对于算术表达式文法:
ETF→E+T∣T→T∗F∣F→(E)∣id
其拓广文法为:
S′ETF→E→E+T∣T→T∗F∣F→(E)∣id
二、LR(0)项目集与规范族
LR(0)项目集是一个LR(0)项目的集合。LR(0)项目集规范族是一个文法的所有可能的项目集族,它包含了文法分析过程中可能出现的所有状态。我们通过以下步骤构造项目集规范族:
-
构造初始项目集I0
- 从拓广文法的开始产生式S′→⋅S构造
- 求其闭包CLOSURE({S′→⋅S})
-
构造后继项目集
- 对于每个项目集Ii
- 对于每个文法符号X
- 计算GOTO(Ii,X)
-
重复步骤2直到不能产生新的项目集
生成的所有项目集的集合就构成了这个文法的LR(0)项目集规范族,通常记作C={I0,I1,…,In}。
例如,考虑简单的表达式文法:
S′ET→E→E+T∣T→id
其项目集规范族的构造过程:
-
初始项目集I0:
S′→⋅EE→⋅E+TE→⋅TT→⋅id
-
从I0出发,对于终结符id
:
I1=GOTO(I0,id):
T→id⋅
-
从I0出发,对于非终结符T:
I2=GOTO(I0,T):
E→T⋅
-
从I0出发,对于非终结符E:
I3=GOTO(I0,E):
S′→E⋅E→E⋅+T
-
从I3出发,对于终结符+:
I4=GOTO(I3,+):
E→E+⋅TT→⋅id
-
从I4出发,对于非终结符T:
I5=GOTO(I4,T):
E→E+T⋅
这样我们得到了这个简单文法的完整LR(0)项目集规范族C={I0,I1,I2,I3,I4,I5}。每个项目集代表了分析过程中的一个状态,我们需要考虑:
- 项目中点号⋅后面的符号(可能的移进操作)
- 点号⋅在最右端的项目(可能的归约操作)
- 通过GOTO函数连接不同的项目集(状态转换关系)
这些项目集构成的规范族最终将用于构造LR(0)分析表,指导语法分析过程。如果一个文法是LR(0)文法,那么其规范族中的项目集在任何时候都不会出现移进-归约冲突或归约-归约冲突。通过项目集规范族,我们可以清晰地看到语法分析过程中所有可能的状态及其转换关系。
好的,我来讲解如何基于LR(0)项目集构造识别活前缀的DFA。
对于文法G,其LR(0)自动机是一个DFA M=(Q,Σ,δ,q0,F),其中:
-
状态集合Q
- 每个状态对应一个LR(0)项目集
- 初始状态q0对应初始项目集I0
- 项目集通过CLOSURE运算得到闭包
-
输入符号集Σ
- 包含文法中所有的终结符和非终结符
- Σ=VT∪VN
-
状态转移函数δ
- 基于GOTO函数定义
- 如果GOTO(Ii,X)=Ij,则δ(qi,X)=qj
让我用一个具体的例子来说明。考虑文法:
S′SC→S→CC→cC∣d
构造过程:
-
构造初始项目集I0:
I0=CLOSURE({S′→⋅S})={S′→⋅S,S→⋅CC,C→⋅cC,C→⋅d}
-
计算I0的后继状态:
对c:I1=GOTO(I0,c):
{C→c⋅C,C→⋅cC,C→⋅d}
对d:I2=GOTO(I0,d):
{C→d⋅}
对C:I3=GOTO(I0,C):
{S→C⋅C,C→⋅cC,C→⋅d}
对S:I4=GOTO(I0,S):
{S′→S⋅}
-
继续计算其他状态的后继:
从I1对c:GOTO(I1,c)=I1
从I1对d:GOTO(I1,d)=I2
从I1对C:I5=GOTO(I1,C):
{C→cC⋅}
从I3对C:I6=GOTO(I3,C):
{S→CC⋅}
从I3对c:GOTO(I3,c)=I1
从I3对d:GOTO(I3,d)=I2
这样就得到了完整的LR(0)自动机的状态集合和转移函数。可以用DOT语言表示这个DFA:
在这个DFA中,每个节点都以方框形式显示其完整的项目集内容,点号"·"表示分析位置,项目之间用换行符分隔,这样可以直观地看到每个状态包含的所有项目信息。
这个DFA可以用来:
- 识别文法的所有活前缀
- 指导LR(0)分析器的动作
- 作为构造LR(0)分析表的基础
通过这个DFA,我们可以:
- 判断某个状态是否有移进-归约冲突或归约-归约冲突
- 确定在每个状态下应该采取的分析动作
- 确定状态转换序列,指导语法分析过程
活前缀(viable prefix)是LR分析中的一个重要概念。让我来详细解释:
活前缀的定义:
- 如果右句型αβw中β是句柄
- 那么αβ的任何前缀都是一个活前缀
形式化定义:
如果存在推导S∗rmαAw⇒rmαβw,其中:
- αβ是句型的一个前缀
- β是句柄
- w是终结符串
- A→β是某个产生式
那么αβ的任何前缀都是活前缀。
让我来解释活前缀在LR(0)自动机中的体现。
LR(0)自动机通过其状态和转换来识别活前缀。具体来说,一个串是活前缀,当且仅当它能使LR(0)自动机从初始状态I0转换到某个有效状态。这个关系可以形式化地表示为:
对于文法G的一个活前缀γ,存在一个规范推导:
S′∗rmαAw⇒rmαβw
其中:
- γ是αβ的前缀
- A→β是产生式
- w是终结符串
在LR(0)自动机中,这个活前缀γ的识别过程体现为:
-
状态转换序列:
- 从初始状态I0开始
- 对γ中的每个符号X进行状态转换GOTO(Ii,X)
- 最终到达某个状态Ik
-
状态中的项目反映了可能的句柄:
- 如果γ是αβ的真前缀,那么到达的状态Ik中会包含形如A→δ⋅η的项目,其中δη=β
- 如果γ=αβ,那么状态Ik中会包含项目A→β⋅
让我们通过一个具体的例子来说明。考虑文法:
S′SC→S→CC→cC∣d
在分析输入串ccdd
(这是一个合法的句子,因为S⇒CC⇒Cd⇒cCd⇒ccCd⇒ccdd)的过程中:
步骤 |
状态栈 |
符号栈 |
输入串 |
动作 |
1 |
0 |
ϵ |
ccdd$ |
移进到I1 |
2 |
01 |
c |
cdd$ |
移进到I1 |
3 |
011 |
cc |
dd$ |
移进到I2 |
4 |
0112 |
ccd |
d$ |
按C→d归约 |
5 |
0115 |
ccC |
d$ |
按C→cC归约 |
6 |
015 |
cC |
d$ |
按C→cC归约 |
7 |
03 |
C |
d$ |
移进到I2 |
8 |
032 |
Cd |
$ |
按C→d归约 |
9 |
036 |
CC |
$ |
按S→CC归约 |
10 |
04 |
S |
$ |
按S′→S归约(接受) |
从这个分析过程我们可以看出活前缀在LR(0)自动机中的具体体现:
-
状态转换对应:
- 每个活前缀都对应一个有效的状态转换序列
- 比如活前缀
cc
对应状态转换序列[I0,I1,I1]
- 状态栈实际记录了识别活前缀的过程
-
句柄识别:
- 状态中的项目直接反映了句柄识别的进展
- 例如在状态I1中的项目C→c⋅C表示正在寻找可能的句柄
- 在状态I2中的项目C→d⋅表示已找到一个完整句柄
-
活前缀的累积性:
- 符号栈中出现的所有内容{ϵ,c,cc,ccd,ccC,cC,C,Cd,CC,S}都是活前缀
- 每个活前缀都能通过LR(0)自动机的状态转换得到确认
- 活前缀集合反映了规范规约过程中的所有有效中间状态
这种对应关系使得LR(0)自动机能够:
- 准确识别输入串中的句柄
- 正确指导移进-归约动作的选择
- 保证分析过程严格按照最右推导的逆过程进行
要找出所有仅由终结符组成的活前缀,最直接的方法是观察LR(0)自动机中的状态转换。
对于文法:
S′SC→S→CC→cC∣d
从LR(0)自动机可以看到,终结符的状态转换只有:
- 从I0可以读入
c
转到I1
- 从I1可以继续读入
c
保持在I1(自环)
- 从I0或I1可以读入
d
转到I2
因此所有由终结符构成的活前缀可以用正则表达式表示为:
c∗d?
这表示活前缀可以是:
好的,我来详细说明这个算法。
算法:构造活前缀正则表达式
输入:LR(0)自动机M=(Q,Σ,δ,q0,F)
输出:描述所有终结符活前缀的正则表达式R
第一步:构造可达状态集
-
定义可达状态集V:
- 初始时V={q0}
- 对V中的每个状态q和每个终结符a:
- 如果δ(q,a)=p存在,则将p加入V
- 重复直到V不再增大
-
构造状态转移图G=(V,E):
- 节点集合就是V
- 边集合E包含所有qap,其中:
- q,p∈V
- a是终结符
- δ(q,a)=p
第二步:构造正则表达式
-
对每个状态q∈V,构造从q0到q的路径正则表达式Rq:
- 初始时Rq0=ϵ
- 对其他状态初始化为∅
-
重复以下步骤直到所有Rq不再变化:
- 对每个边paq:
- 如果Rp=∅:
- 更新Rq=Rq∣Rpa
-
最终结果R是所有非空Rq的并:
R=∑q∈V,Rq=∅Rq
其中∑表示用选择运算符|
连接所有项。
让我们用之前的例子来说明:
S′SC→S→CC→cC∣d
-
构造可达状态集V:
- 从I0开始
- 通过
c
可达I1
- 通过
d
可达I2
- 从I1通过
c
可回到I1
- 从I1通过
d
可达I2
所以V={I0,I1,I2}
-
构造转移图:
-
构造正则表达式:
- RI0=ϵ
- 第一轮迭代(考虑所有可能的边):
- 从I0cI1得到c
- 从I1cI1得到cc
- 所以RI1=c∣cc
- 从I0dI2得到d
- 从I1dI2得到cd
- 所以RI2=d∣cd∣ccd
- 第二轮迭代:
- RI1=c∣cc∣ccc
- RI2=d∣cd∣ccd∣cccd
- 继续迭代...最终得到:
- RI1=c+
- RI2=d∣c+d=c∗d
-
最终结果:
R=ϵ∣c+∣c∗d=c∗∣c∗d=c∗(ϵ∣d)=c∗d?
这个算法的优点是:
- 系统性:保证找到所有可能的活前缀
- 可扩展性:适用于任意LR(0)文法
- 正确性:基于状态转换的完整性
缺点是:
- 可能需要多次迭代
- 正则表达式可能不是最简形式
- 对于复杂文法计算量较大
实际应用中,我们可以通过一些优化来改进这个算法:
- 使用动态规划减少重复计算
- 采用正则表达式化简技术
- 识别特殊模式直接生成对应表达式
事实上,我们还可以借助Arden引理来更系统地求解这个问题。Arden引理指出:如果P和Q是正则语言,且P不包含空串ϵ,那么方程X=PX+Q的唯一解是X=P∗Q。
考虑同样的例子,我们已经得到可达状态集V={I0,I1,I2}和对应的转移图。现在我们用Arden引理来求解:
-
首先写出状态转移关系表Rij:
|
I0 |
I1 |
I2 |
I0 |
∅ |
c |
d |
I1 |
∅ |
c |
d |
I2 |
∅ |
∅ |
∅ |
-
对每个状态i,设Xi表示从该状态开始的所有可能路径的标号构成的正则表达式。由于我们要求所有活前缀,所有状态都是接受状态,可以列出方程组:
X0X1X2=cX1+dX2+ϵ=cX1+dX2+ϵ=ϵ
-
从后向前求解:
-
X2=ϵ(已知)
-
代入第二个方程:
X1=cX1+dϵ+ϵ=cX1+d+ϵ
根据Arden引理:X1=c∗(d+ϵ)
-
代入第一个方程:
X0=cX1+dϵ+ϵ=c[c∗(d+ϵ)]+d+ϵ=cc∗(d+ϵ)+d+ϵ=c+(d+ϵ)+d+ϵ=(c+∣ϵ)(d+ϵ)=c∗d?
因为I0是初始状态,所以最终的正则表达式是c∗d?,这与我们之前通过转移图迭代构造得到的结果完全一致。
这种基于Arden引理的方法相比之前的迭代构造法有以下优点:
- 计算过程更加理论化和系统化
- 可以直接应用成熟的NFA→RE转换算法
- 求解步骤更加清晰,不容易出错
- 证明过程更加严谨,结果的正确性有理论保证
是的,你说得对。在实际分析中,通过观察状态转移图的结构,我们往往可以直观地写出路径正则表达式。对于这个例子:
直观上就能看出:
- 从I0到I1的路径:先读一个
c
,然后可以读任意多个c
,即c+
- 从I0到I2的路径:
- 要么直接读一个
d
- 要么先经过I1(读若干个
c
)再读一个d
- 合起来就是c∗d
所以最终的正则表达式就是c∗d?。
这种直观分析的方法在状态转移图结构简单时特别有效,主要有以下几种常见模式:
- 自环:表示a∗
- 两点间多条平行边:表示选择a∣b
- 串联路径:表示连接ab
- 菱形结构:表示可选a?
算法更多是作为一个理论保证和复杂情况下的系统性方法。
你说得对。LR(0)自动机确实和普通的DFA有着本质区别:
-
普通DFA的目的:
- 识别某种语言的字符串
- 需要明确的接受状态来判断字符串是否属于该语言
- 在处理完整个输入后,根据是否在接受状态来决定接受或拒绝
-
LR(0)自动机的目的:
- 指导语法分析过程中的移进-归约决策
- 识别活前缀,但不需要用"接受状态"来标记
- 任何可达状态都是有效的(能识别活前缀的状态)
虽然在形式化定义中把LR(0)自动机写作五元组M=(Q,Σ,δ,q0,F)是为了与DFA的形式保持一致,但实际上F(接受状态集合)在LR(0)分析中并不起实质性作用。重要的是:
-
状态集Q(项目集规范族)
- 每个状态包含的LR(0)项目反映了分析进展
- 指示当前可能的句柄位置
- 决定下一步应该采取的动作(移进或归约)
-
转移函数δ
- 定义状态间的转换关系
- 指导符号的移进操作
- 体现语法结构的识别过程
-
初始状态q0(I0)
所以我认为,在讨论LR(0)自动机时,完全可以省略接受状态这个概念,将其看作一个三元组(Q,Σ,δ)加上指定的初始状态q0就足够了。
我来详细讲解LR(0)文法的定义和LR(0)分析表的构造过程。
一、LR(0)文法定义
如果一个文法对应的LR(0)自动机满足下列条件,则称该文法是LR(0)文法:
-
对于任意项目集Ii,如果Ii含有项目A→α⋅,那么Ii中:
- 不能含有任何形如B→β⋅aγ的项目(a是终结符)
- 不能含有其他形如C→γ⋅的项目
-
换句话说,在任何状态下:
- 不能同时存在移进和归约动作(无移进-归约冲突)
- 不能同时存在多个归约动作(无归约-归约冲突)
二、LR(0)分析表的构造
在构造分析表时,我们把每个LR(0)项目集Ii视为一个状态(称为"状态i")。LR(0)分析表包含两个部分:动作表ACTION和转移表GOTO。构造规则如下:
-
ACTION表的构造规则:
对于状态i:
-
如果项目A→α⋅aβ在状态i中(a是终结符),且GOTO(Ii,a)=Ij,则:
ACTION[i,a]=shift j
-
如果项目A→α⋅在状态i中(A=S′),则对所有终结符a:
ACTION[i,a]=reduce A→α
-
如果项目S′→S⋅在状态i中,则:
ACTION[i,$]=accept
-
GOTO表的构造规则:
对于状态i和非终结符A:
- 如果GOTO(Ii,A)=Ij,则:
GOTO[i,A]=j
例如,考虑文法:
S′SA→S→aAd→bc(0)(1)(2)
构造步骤:
-
首先构造LR(0)项目集规范族:
I0:
S′→⋅SS→⋅aAd
I1(由I0经过a):
S→a⋅AdA→⋅bc
I2(由I1经过b):
A→b⋅c
I3(由I2经过c):
A→bc⋅
I4(由I1经过A):
S→aA⋅d
I5(由I4经过d):
S→aAd⋅
I6(由I0经过S):
S′→S⋅
-
然后对每个状态检查其中的项目,根据项目的形式填写分析表:
-
对于形如A→α⋅aβ的项目(点号后面是终结符):
填入移进动作sj,其中j是GOTO函数的目标状态
-
对于形如A→α⋅的项目(点号在最右端):
填入归约动作ri,其中i是使用的产生式编号
-
对于形如S′→S⋅的项目:
填入接受动作acc
-
对于形如A→α⋅Bβ的项目(点号后面是非终结符):
在GOTO表中填入目标状态号
例如对I0:
- 看到项目S→⋅aAd,点号后面是终结符a,且GOTO(I0,a)=I1
- 所以填入ACTION[0,a]=s1
- 看到项目S′→⋅S,点号后面是非终结符S,且GOTO(I0,S)=I6
- 所以填入GOTO[0,S]=6
最终得到的分析表:
状态 |
ACTION |
|
|
|
|
GOTO |
|
|
a |
b |
c |
d |
$ |
S |
A |
0 |
s1 |
|
|
|
|
6 |
|
1 |
|
s2 |
|
|
|
|
4 |
2 |
|
|
s3 |
|
|
|
|
3 |
r2 |
r2 |
r2 |
r2 |
r2 |
|
|
4 |
|
|
|
s5 |
|
|
|
5 |
r1 |
r1 |
r1 |
r1 |
r1 |
|
|
6 |
|
|
|
|
acc |
|
|
其中:
- sn表示移进到状态n
- rn表示用第n个产生式归约
- acc表示接受
- 空白表示报错
这是一个典型的LR(0)文法,因为:
- 每个状态下要么只能移进,要么只能归约
- 归约状态(状态3和状态5)中没有任何移进项目
- 每个状态的动作都是明确无冲突的
LR(0)分析器的工作过程:
- 维护状态栈和符号栈
- 根据当前状态和输入符号查表决定动作:
- 如果是移进,将状态和符号分别压入对应的栈
- 如果是归约,弹出相应数量的状态和符号,压入归约结果
- 如果是接受,分析成功结束
- 如果是空白,报告语法错误
LR(0)分析的局限性:
- 要求文法非常简单,实际中很少有文法直接满足LR(0)条件
- 不能处理需要向前看符号的情况
- 通常作为构造更强大的LR分析器(如SLR、LALR)的基础
让我详细讲解LR(0)分析的工作过程和实现方法。
LR(0)分析器使用两个栈:
- 状态栈:存放状态编号
- 符号栈:存放文法符号(终结符或非终结符)
分析步骤:
-
初始化:
- 状态栈压入初始状态0
- 符号栈为空
- 输入串末尾加上结束符号$
-
重复以下步骤直到接受或报错:
- 设当前状态为s(状态栈顶)
- 当前输入符号为a
- 查询ACTION[s,a]确定动作
-
根据ACTION表的内容执行相应动作:
-
若ACTION[s,a]=shift j:
- 将状态j压入状态栈
- 将输入符号a压入符号栈
- 指向下一个输入符号
-
若ACTION[s,a]=reduce A→β:
- 从状态栈弹出∣β∣个状态
- 从符号栈弹出∣β∣个符号
- 设当前状态为t(现在的状态栈顶)
- 查找GOTO[t,A]=g
- 将状态g压入状态栈
- 将非终结符A压入符号栈
-
若ACTION[s,a]=accept:
-
若ACTION[s,a]为空:
例如,考虑之前的文法:
S′SA→S→aAd→bc(0)(1)(2)
分析输入串abcd的过程:
步骤 |
状态栈 |
符号栈 |
输入串 |
动作 |
1 |
0 |
|
abcd$ |
移进,状态1 |
2 |
01 |
a |
bcd$ |
移进,状态2 |
3 |
012 |
ab |
cd$ |
移进,状态3 |
4 |
0123 |
abc |
d$ |
归约,A→bc |
5 |
014 |
aA |
d$ |
移进,状态5 |
6 |
0145 |
aAd |
$ |
归约,S→aAd |
7 |
06 |
S |
$ |
接受 |
详细说明每一步:
- 初始状态0,看到a,查表得s1,移进到状态1
- 状态1看到b,查表得s2,移进到状态2
- 状态2看到c,查表得s3,移进到状态3
- 状态3,查表得r2(对任何输入符号都是r2),使用A→bc归约:
- 弹出状态栈顶的2个状态
- 弹出符号栈顶的bc
- 当前状态为1,查GOTO表得GOTO[1,A]=4
- 压入非终结符A和状态4
- 状态4看到d,查表得s5,移进到状态5
- 状态5,查表得r1,使用S→aAd归约:
- 弹出状态栈顶的3个状态
- 弹出符号栈顶的aAd
- 当前状态为0,查GOTO表得GOTO[0,S]=6
- 压入非终结符S和状态6
- 状态6看到$,查表得acc,分析成功结束
我们可以写一个简单的LR(0)分析器算法:
算法: LR(0)语法分析
输入: 输入串w,ACTION表和GOTO表
输出: 如果w属于文法L(G)则接受,否则报错
- state_stack←[0]
- symbol_stack←[] (空栈)
- w←w$
- i←0 (输入指针)
- while true do
- s←state_stack.top()
- a←w[i]
- if ACTION[s,a]=empty then
- return "语法错误"
- if ACTION[s,a]=shift t then
- state_stack.push(t)
- symbol_stack.push(a)
- i←i+1
- else if ACTION[s,a]=reduce A→β then
- for j←1 to ∣β∣ do
- state_stack.pop()
- symbol_stack.pop()
- t←state_stack.top()
- g←GOTO[t,A]
- state_stack.push(g)
- symbol_stack.push(A)
- else if ACTION[s,a]=accept then
- return "接受"
LR(0)分析的优缺点:
优点:
- 自底向上分析,可以及时发现错误
- 不需要回溯,分析效率高
- 可以处理左递归文法
缺点:
- 要求文法必须是LR(0)文法,这个条件太严格
- 不能处理有冲突的情况
- 构造分析表比较复杂
为了解决这些限制,实际中常用的是SLR(1)、LALR(1)或LR(1)分析方法,它们都是在LR(0)的基础上改进得到的。
我来完整讲解SLR(1)分析,包括文法判定。
一、SLR(1)分析的基本思想
SLR(1)分析是在LR(0)基础上的改进,它通过引入向前看符号的FOLLOW集来解决一些LR(0)中的冲突。这样可以解决一些LR(0)中的移进-归约冲突。
二、SLR(1)文法的判定
一个文法是SLR(1)文法,当且仅当对于其LR(0)自动机的每个状态I:
-
对于一般形式的冲突判定:
假设状态I中包含如下形式的项目:
I={U1→x⋅b1y1,U2→x⋅b2y2,…,Um→x⋅bmym,V1→x⋅,V2→x⋅,…,Vn→x⋅}
其中:
- Ui,Vi都是非终结符
- x,yi是文法符号串(可以包含终结符和非终结符)
- bi是终结符
必须满足:集合{b1,b2,…,bm}与各个FOLLOW(Vi)两两互不相交
-
这个一般形式可以推导出两种具体情况:
-
对于移进-归约冲突的情况:
- 如果状态I中有项目A→α⋅且有项目B→β⋅aγ
- 则必须满足a∈/FOLLOW(A)
-
对于归约-归约冲突的情况:
- 如果状态I中有项目A→α⋅且有项目B→β⋅
- 则必须满足FOLLOW(A)∩FOLLOW(B)=∅
三、SLR(1)分析表的构造规则
对每个状态i:
-
移进动作:
- 同LR(0),如果项目A→α⋅aβ在状态i中(a是终结符),且GOTO(Ii,a)=Ij:
ACTION[i,a]=shift j
-
归约动作:
- 如果项目A→α⋅在状态i中:
- 对所有a∈FOLLOW(A):
ACTION[i,a]=reduce A→α
-
接受动作:
- 同LR(0),如果项目S′→S⋅在状态i中:
ACTION[i,$]=accept
-
GOTO动作:
- 同LR(0),如果GOTO(Ii,A)=Ij:
GOTO[i,A]=j
四、举例说明
考虑文法:
S′SSLR→S→L=R→R→id→L(0)(1)(2)(3)(4)
-
首先计算FOLLOW集:
FOLLOW(S)FOLLOW(L)FOLLOW(R)={$}={=,$}={$}
-
构造LR(0)项目集:
I0:
S′→⋅SS→⋅L=RS→⋅RL→⋅idR→⋅L
I1(由I0经过id):
L→id⋅
I2(由I0经过L):
S→L⋅=RR→L⋅
I3(由I0经过R):
S→R⋅
I4(由I2经过=):
S→L=⋅RR→⋅LL→⋅id
I5(由I4经过R):
S→L=R⋅
I6(由I4经过L):
R→L⋅
I7(由I0经过S):
S′→S⋅
-
检查是否是SLR(1)文法:
- 在状态I2中有潜在的移进-归约冲突:
- 移进项目:S→L⋅=R
- 归约项目:R→L⋅
- 但=∈/FOLLOW(R)={$},所以这不是真正的冲突
- 其他状态都没有冲突
- 因此这是一个SLR(1)文法
-
构造分析表:
状态 |
ACTION |
|
|
GOTO |
|
|
|
id |
= |
$ |
S |
L |
R |
0 |
s1 |
|
|
7 |
2 |
3 |
1 |
|
r3 |
r3 |
|
|
|
2 |
|
s4 |
r4 |
|
|
|
3 |
|
|
r2 |
|
|
|
4 |
s1 |
|
|
|
6 |
5 |
5 |
|
|
r1 |
|
|
|
6 |
|
|
r4 |
|
|
|
7 |
|
|
acc |
|
|
|
分析过程举例,分析输入串id=id:
步骤 |
状态栈 |
符号栈 |
输入串 |
动作 |
1 |
0 |
|
id=id$ |
移进,状态1 |
2 |
01 |
id |
=id$ |
归约,L→id |
3 |
02 |
L |
=id$ |
移进,状态4 |
4 |
024 |
L= |
id$ |
移进,状态1 |
5 |
0241 |
L=id |
$ |
归约,L→id |
6 |
0246 |
L=L |
$ |
归约,R→L |
7 |
0245 |
L=R |
$ |
归约,S→L=R |
8 |
07 |
S |
$ |
接受 |
SLR(1)的特点:
优点:
- 比LR(0)能处理更多的文法
- 构造过程相对简单,只需要额外计算FOLLOW集
- 分析表相对紧凑
缺点:
- 仍然不能处理所有的LR(1)文法
- 有时候FOLLOW集可能太大,导致不必要的归约冲突
- 不能处理需要更多上下文信息的情况
这就是为什么在实际中常常使用LALR(1)或完整的LR(1)分析方法。
从LR(0)自动机构造分析表
-
移进动作和GOTO表项的构造
- 遍历DFA中的每条边:
- 如果边上标记是终结符a,从状态i到状态j:
ACTION[i,a]=shift j
- 如果边上标记是非终结符A,从状态i到状态j:
GOTO[i,A]=j
-
归约动作的构造
- 对DFA的每个状态i:
- 如果状态包含项目A→α⋅
- 对于每个a∈FOLLOW(A):
ACTION[i,a]=reduce A→α
-
接受动作的构造
- 如果某个状态包含项目S′→S⋅:
ACTION[i,$]=accept
具体例子参见第七章作业前两题
让我们来分析if-else语句的匹配问题。对于下面这个描述if-else结构的文法:
S′SSS→S→iSeS→iS→a(0)(1)(2)(3)
其对应的LR(0)自动机如下:
请你根据这个DFA构造SLR(1)分析表,分析其中的冲突,并说明如何通过无二义性规则来消除这些冲突。
我来分析这个if-else问题。首先需要计算FOLLOW集,然后根据DFA构造分析表并找出冲突。
一、计算FOLLOW集:
- 由于S是开始符号,所以$∈FOLLOW(S)
- 从产生式S→iSeS可知,e∈FOLLOW(S)
- 继续迭代,最终得到:
FOLLOW(S)={e,$}
二、构造SLR(1)分析表:
状态I4存在移进-归约冲突:
- 包含项目S→iS⋅eS(指示移进e)
- 包含项目S→iS⋅(指示归约)
- 而e∈FOLLOW(S),因此在输入e时既可以移进也可以归约
完整的SLR(1)分析表如下:
状态 |
ACTION |
|
|
|
GOTO |
|
i |
e |
a |
$ |
S |
0 |
s2 |
|
s3 |
|
1 |
1 |
|
|
|
acc |
|
2 |
s2 |
|
s3 |
|
4 |
3 |
|
r3 |
|
r3 |
|
4 |
|
s5/r2 |
|
r2 |
|
5 |
s2 |
|
s3 |
|
6 |
6 |
|
r1 |
|
r1 |
|
在这个分析表中:
- sn表示移进到状态n
- rn表示用第n个产生式归约
- s5/r2表示存在冲突:可以移进到状态5,也可以用产生式2归约
这个冲突正是著名的if-else匹配问题。当分析器处于状态I4时,已经读入了形如iS的前缀,此时如果遇到输入符号e:
- 可以选择移进e,期待后面还有S,最终归约为S→iSeS
- 也可以选择立即用S→iS进行归约
这就是为什么这个文法不是SLR(1)文法,因为存在无法通过简单向前看符号解决的冲突。在实际编译器中,这个问题通常通过以下方式解决:
- 采用最近匹配原则:else总是和最近的未匹配的if配对
- 在语法分析阶段,优先选择移进而不是归约
- 或者修改文法,引入明确的配对结构
其中前两种方法实际上是统一的:采用最近匹配原则的具体体现就是在冲突时优先选择移进而不是归约。按照这个规则修改后的分析表如下:
状态 |
ACTION |
|
|
|
GOTO |
|
i |
e |
a |
$ |
S |
0 |
s2 |
|
s3 |
|
1 |
1 |
|
|
|
acc |
|
2 |
s2 |
|
s3 |
|
4 |
3 |
|
r3 |
|
r3 |
|
4 |
|
s5 |
|
r2 |
|
5 |
s2 |
|
s3 |
|
6 |
6 |
|
r1 |
|
r1 |
|
这样处理后,分析器在遇到else时总是优先选择将其与最近的未匹配的if进行配对,从而解决了if-else匹配的二义性问题。
关于运算符优先级和结合性规则如何消解LR分析中的冲突,可以参考第七章作业,其中包含了几个典型的算术表达式文法实例及其详细的冲突处理过程。
让我详细讲解LR(1)项目及其构造方法。
LR(1)项目引入了"向前看符号"的概念,每个项目都是一个二元组[A→α⋅β,a],其中:
- A→αβ是一个产生式
- 点号⋅表示当前分析位置
- a是向前看符号(终结符或$)
一、LR(1)项目的基本概念
-
规范LR(1)项目的记法:
[A→α⋅β,a]
这表示:
- 已经匹配了α
- 期望匹配β
- 在β匹配完成后,向前看符号应该是a
-
向前看符号的含义:
- 只有当下一个输入符号是a时,才能使用A→αβ进行归约
- 这比SLR(1)中使用FOLLOW集更精确
二、LR(1)项目集的闭包计算
给定一个LR(1)项目集I,其闭包CLOSURE(I)的计算规则:
-
I中的所有项目都属于CLOSURE(I)
-
如果[A→α⋅Bβ,a]∈CLOSURE(I),且B→γ是一个产生式:
- 对于每个终结符b∈FIRST(βa)
- [B→⋅γ,b]也属于CLOSURE(I)
-
重复步骤2直到不能添加新的项目
举例说明闭包计算:
考虑文法:
S′SC→S→CC→cC∣d
对于项目[S′→⋅S,$],其闭包计算过程:
-
初始项目:[S′→⋅S,$]
-
由于点号后面是S,看产生式S→CC:
- FIRST($)={$}
- 添加[S→⋅CC,$]
-
对于[S→⋅CC,$],点号后面是C:
- FIRST(C$)={c,d}
- 添加:
- [C→⋅cC,c]
- [C→⋅cC,d]
- [C→⋅d,c]
- [C→⋅d,d]
最终得到的闭包是:
CLOSURE({[S′→⋅S,$]})={[S′→⋅S,$],[S→⋅CC,$],[C→⋅cC,c],[C→⋅cC,d],[C→⋅d,c],[C→⋅d,d]}
三、LR(1)项目集的GOTO函数
对于LR(1)项目集I和文法符号X,GOTO(I,X)的计算规则:
-
首先找出I中所有形如[A→α⋅Xβ,a]的项目
-
将点号移到X之后,得到[A→αX⋅β,a]
-
计算这些新项目集合的闭包
即:
GOTO(I,X)=CLOSURE({[A→αX⋅β,a]∣[A→α⋅Xβ,a]∈I})
四、LR(1)项目集规范族的构造
构造步骤:
-
计算初始项目集I0=CLOSURE({[S′→⋅S,$]})
-
对于已经得到的项目集Ii:
- 对于每个文法符号X
- 如果GOTO(Ii,X)非空且不在已有的项目集中
- 将其作为新的项目集加入规范族
-
重复步骤2直到不能产生新的项目集
五、LR(1)项目的特点:
-
优点:
- 提供了最精确的向前看信息
- 能处理更多的文法
- 不会产生虚假的冲突
-
缺点:
- 项目集数量可能非常大
- 构造过程复杂
- 需要更多的存储空间
与其他分析方法的比较:
- 比LR(0)多了向前看符号
- 比SLR(1)的向前看信息更精确
- 是LALR(1)的基础
这种精确的向前看信息使得LR(1)分析器能够处理几乎所有的确定性上下文无关文法,但由于其项目集数量庞大,实际中更常用的是LALR(1)分析器,它在保持LR(1)大部分分析能力的同时,大大减少了状态数量。
好的,我来详细讲解LR(1)分析表的构造过程。
一、构造规则
对于每个LR(1)项目集Ii,根据其中的项目类型按以下规则构造分析表:
-
如果[A→α⋅aβ,b]∈Ii(a是终结符),且GOTO(Ii,a)=Ij:
ACTION[i,a]=shift j
-
如果[A→α⋅,a]∈Ii(A=S′):
ACTION[i,a]=reduce A→α
-
如果[S′→S⋅,$]∈Ii:
ACTION[i,$]=accept
-
如果GOTO(Ii,A)=Ij(A是非终结符):
GOTO[i,A]=j
二、举例说明
考虑文法:
S′SCC→S→CC→cC→d(0)(1)(2)(3)
-
首先构造LR(1)项目集:
I0:
[S′→⋅S,$][S→⋅CC,$][C→⋅cC,c][C→⋅cC,d][C→⋅d,c][C→⋅d,d]
I1(由I0经过c):
[C→c⋅C,c][C→c⋅C,d][C→⋅cC,c][C→⋅cC,d][C→⋅d,c][C→⋅d,d]
I2(由I0经过d):
[C→d⋅,c][C→d⋅,d]
I3(由I0经过C):
[S→C⋅C,$][C→⋅cC,$][C→⋅d,$]
I4(由I1经过C):
[C→cC⋅,c][C→cC⋅,d]
I5(由I3经过C):
[S→CC⋅,$]
I6(由I0经过S):
[S′→S⋅,$]
-
构造分析表:
状态 |
ACTION |
|
|
GOTO |
|
|
|
c |
d |
$ |
S |
C |
|
0 |
s1 |
s2 |
|
6 |
3 |
|
1 |
s1 |
s2 |
|
|
4 |
|
2 |
r3 |
r3 |
|
|
|
|
3 |
s1 |
s2 |
|
|
5 |
|
4 |
r2 |
r2 |
|
|
|
|
5 |
|
|
r1 |
|
|
|
6 |
|
|
acc |
|
|
|
三、分析过程示例
让我们分析输入串cdd:
步骤 |
状态栈 |
符号栈 |
输入串 |
动作 |
1 |
0 |
|
cdd$ |
移进,状态1 |
2 |
01 |
c |
dd$ |
移进,状态2 |
3 |
012 |
cd |
d$ |
归约,C→d |
4 |
014 |
cC |
d$ |
归约,C→cC |
5 |
03 |
C |
d$ |
移进,状态2 |
6 |
032 |
Cd |
$ |
归约,C→d |
7 |
035 |
CC |
$ |
归约,S→CC |
8 |
06 |
S |
$ |
接受 |
四、LR(1)分析的特点
-
优点:
- 能力最强,可以处理几乎所有的确定性上下文无关文法
- 分析精确,不会产生不必要的冲突
- 错误检测能力强
-
主要特征:
- 每个项目都带有精确的向前看符号
- 状态转换基于项目集和向前看符号
- 归约动作只对特定的向前看符号有效
-
与其他分析方法的比较:
- 比LR(0)多了向前看信息,能力更强
- 比SLR(1)使用更精确的向前看信息
- 生成的状态数通常比LALR(1)多
-
实现考虑:
- 状态数量可能很大,需要有效的存储方案
- 可以使用压缩技术减少分析表的大小
- 可以根据具体需求选择合适的实现方式
由于LR(1)分析表可能非常大,实际编译器通常使用LALR(1)分析器,它在保持大部分分析能力的同时,显著减少了状态数量。