1. 自下而上分析法思路
归约:根据文法的产生式规则,把产生的右部替换成左部符号
采用一个寄存符号的先进后出的栈,把输入符号一个一个地移动进栈里,把栈顶形成某个产生式的候选式时,即把栈顶的这一部分替换成该产生式的左部符号
2. 自上而下分析的基本问题
识别可归约串
当栈顶的符号串可以匹配多个候选式的时候,选择哪一个候选式进行归约成为了该分析法的一个基本问题
3. 短语、直接短语、句柄
对于一个文法 G , S 是一个文法的开始符号,假定 αβδ 是文法 G 的一个句型,如果有 S⇒∗αAδ,A⇒+β ,则称 β 是句型 αβδ 相对于非终结符 A 的短语
特别的,如果有 A⇒β ,则称 β 是句型 αβδ 相对于规则 A→β 的直接短语
一个句型的最左直接短语称为该句型的句柄
以某非终结符为根的两代以上的子树的所有末端结点从左到右排列就是相对于该非终结符的一个短语
例子:文法 G(E)
E→T∣E+TT→F∣T∗FF→(E)∣i
短语: i1,i2,i3,i1∗i2,i1∗i2+i3
直接短语: i1,i2,i3
句柄: i1
4. 规范归约
定义:假设 α 是文法 G 的一个句子,我们称序列 αn,αn−1,…,α0 是 α 的一个规范归约,当且仅当:
αn=α
α0 为文法的开始符号,即 α0=S
对于任何 i , 0≤i≤n , αi−1 是从 αi 经把句柄替换成相应产生式左部符号而得到的
4.1 规范归约与规范推导
4.2 符号栈的使用
E→T∣E+TT→F∣T∗FF→(E)∣i 输入串: i∗i+i
归,用 F→i
归,用 T→F
归,用 F→i
归,用 T→T∗F
归,用 E→T
归,用 F→i
归,用 T→F
归,用 E→E+T
上述方法采用人工判断句柄,然后进行归约的过程
5. 算符优先分析算法
对于一个文法,如果它的句子可能有好几种不同的规范归约,如果规定算符的优先次序,并按照这个规定进行归约,则归约过程是唯一的
5.1 算符优先分析基本思想
起决定作用的是相邻的两个算符(终结符)之间的优先关系
所谓算符优先分析法就是定义算符(终结符)之间的某种优先关系,借助于这种关系寻找“可归约串”和进行归约
5.2 优先关系
对于任何两个相继出现的终结符 a 与 b 的三种优先关系
a⋖b ,a 的优先级低于 b
a≖b ,a 的优先级等于 b
a⋗b ,a 的优先级高于 b
5.3 算符优先文法
算符文法:
一个文法,如果它的任一产生式的右部都不含两个相继(并列)的非终结符,即不含有如下形式的生成式右部: ⋯QR⋯ ,则我们称该文法为算符文法
约定:
⋯ 代表由终结符和非终结符组成的任意序列、包括空字
算符优先文法:
假定 G 是一个不含 ϵ− 产生式的算符文法。对于任何一对终结符a、b ,我们定义:
a≖b 当且仅当文法 G 中含有形如 P→…ab… 或 P→…aQb… 的产生式
a⋖b 当且仅当文法 G 中含有形如 P→…aR… 的产生式,且 R⇒+b… 或 R⇒+Qb…
a⋗b 当且仅当文法 G 中含有形如 P→…Rb… 的产生式,且 R⇒+…a 或 R⇒+aQ…
如果一个算符文法 G 中任何终结符对( a、b )至多只满足下述三关系之一: a≖b,a⋖b,a⋗b 则称 G 是一个算符优先文法
5.4 构造优先关系表算法
通过检查 G 的每个产生式的每个候选式,可找出所有满足 a≖b 的终结符对
确定满足关系 a⋖b,a⋗b 的所有终结符对
首先需要对 G 的每个非终结符 P 构造两个集合 FIRSTVT(P)、LASTVT(P)
FIRSTVT(P)={a∣P⇒+a…,或P⇒+Qa…,a∈VT,Q∈VN}
LASTVT(P)={a∣P⇒+…a,或P⇒+…aQ,a∈VT,Q∈VN}
有了这两个集合后,就可以通过检查每个产生式的候选式确定满足关系 a⋖b,a⋗b 的所有终结符对
假定有个产生式的一个候选形为 …aP… ,那么,对任何 b∈FIRSTVT(P) ,有 a⋖b
假定有个产生式的一个候选形为 …Pb… ,那么,对任何 a∈LASTVT(P) ,有 a⋗b
5.5 构造集合
FIRSTVT(P) 的算法
反复使用下面两条规则构造集合
若有产生式 P→a… 或 P→Qa… ,则 a∈FTRSTVT(P)
若 a∈FTRSTVT(Q) ,且有产生式 P→Q… ,则 a∈FIRSTVT(P)
数据结构
布尔数组 F[P,a] ,使得 F[P,a] 为真的条件是,当且仅当 a∈FTRSTVT(P) 。开始时,按上述规则(1)对每个数组元素 F[P,a] 赋初始值
栈 STACK ,把所有初值为真的数组元素 F[P,a] 的符号对 (P,a)全部放在 STACK 之中
运算
如果栈 STACK 不空,就将栈顶逐出,记此项为 (Q,a)。对于每个形如 P→Q… 的产生式,若 F[P,a] 为假,则变其值为真且将(P,a)推进 STACK 栈
上述过程必须一直重复,直至栈 STACK 拆空为止
程序
BEGIN
FOR 每个非终结符 P 和终结符 a DO
F[P,a]:=FALSE;
FOR 每个形如 P→a…或P→Qa… 的产生式 DO
INSERT(P,a);
WHILE STACK 非空 DO
BEGIN
把 STACK 的栈顶,记为(Q,a),上托出去;
FOR 每条形如 P→Q… 的产生式 DO
INSERT(P,a);
END OF WHILE
END
5.6 构造集合
LASTVT(P) 的算法
反复使用下面两条规则构造集合
若有产生式 P→…a 或 P→…Qa ,则 a∈LASTVT(P)
若 a∈LASTVT(Q) ,且有产生式 P→…Q ,则 a∈LASTVT(P)
5.7 构造集合示例
文法 G(E) :
E→E+T∣TT→T∗F∣FF→P↑F∣PP→(E)∣i
FIRSTVT 集合:
LASTVT 集合:
5.8 构造优先关系表算法程序实现
概念请看 5.4 部分
FOR 每条产生式 P→X1X2…Xn DO
FOR i:= 1 TO n-1 DO
BEGIN
IF Xi 和 Xi+1 均为终结符 THEN 置 Xi≖Xi+1
IF i≤n−2 且 Xi 和 Xi+2 均为终结符,但 Xi+1 为非终结符 THEN Xi≖Xi+2
IF Xi 为终结符而 Xi+1 为非终结符 THEN
FOR FIRSTVT(Xi+1) 中每个 a DO 置 Xi⋖a
IF Xi 为f非终结符而 Xi+1 为终结符 THEN
FOR LASTVT(Xi) 中每个 a DO 置 a⋗Xi+1
END
5.9 示例
接着 5.7 的示例
G 的算符优先关系表
5.10 算符优先分析算法实现
素短语:如果一个短语中,至少包含一个终结符,并且,除了它自身外不再含有任何更小的素短语
最左素短语:处于句型最左边的那个素短语
例子:文法 G(E)
E→E+T∣TT→T∗F∣FF→P↑F∣PF→(E)∣i
短语: T、F、P、i、F∗P、T+F∗P、T+F∗P+i
直接短语: T、F、P、i
句柄: T
素短语: F∗P、i
最左素短语: F∗P
上面的例子是人工手动找最左素短语,下面介绍编程自己找最左素短语
算法优先文法句型(包括两个 # 之间)的一般形式写成:#N1a1N2a2…NnanNn+1# ,其中每个 ai 都是终结符, Ni 是可有可无的非终结符
定理:一个算符优先文法 G 的任何句型的最左素短语是满足如下条件的最左子串 Njaj…NiaiNi+1
aj−1⋖aj
aj≖aj+1,…,ai−1≖ai
ai⋗ai+1
算法:
k := 1;
S[k] := '#';
REPEAT
把下一个输入符号读进 a 中;
IF S[k] ∈VT THEN j := k ELSE j := k - 1;
WHILE S[j] ⋗ a DO
BEGIN
REPEAT
Q := S[j];
IF S[j - 1] ∈VT THEN j := j - 1 ELSE j := j - 2;
UNTIL S[j] ⋖Q
把 S[j + 1] ... S[k] 归约为某个 N;
k := j + 1;
S[k] := N;
END OF WHILE;
IF S[j] ⋖ a OR S[j] ≖ a THEN
BEGIN k := k + 1; S[k] := a END
ELSE ERROR /* 调用出错诊断程序 */
UNTIL a='#'
5.11 优先函数
每个终结符 θ 与两个自然数 f(θ) 与 g(θ) 相对应,使得:
若 θ1⋖θ2 ,则 f(θ1)<g(θ2)
若 θ1≖θ2 ,则 f(θ1)=g(θ2)
若 θ1⋗θ2 ,则 f(θ1)>g(θ2)
优点:便于比较、节省空间
缺点:原本不需要比较优先关系的两个终结符,由于自然数的对应,变得可以比较,需要进行一些判断
构造优先函数
画图:对于每个终结符 a ,令其对应两个符号 fa 和 ga ,画一条所有符号和为结点的方向图。如果 a⋗≖b ,则从 fa 画一条弧至 ga ;如果 a⋖≖b ,则从 ga 画一条弧至 fa
数数:对每个结点都赋予一个数,次数等于从该结点出发所能到达的结点(包括出发点自身)。赋予 fa 的数作为 f(a) ,赋予 ga 的数作为 g(a)
验证:检查所构造出来的函数 f 和 g 是否与原来的关系矛盾。若没有矛盾,则 f 和 g 就是要求的优先函数;若有矛盾,则不存在优先函数
注意:
就算构造出来了优先函数,也可能存在不矛盾,所以必须检查优先函数是否满足要求
6. LR 分析法
上述算符优先分析算法采用素短语来实现栈顶归约;而 LR 分析法只用利用句柄来进行归约
LR 分析法的思路:
产生分析表
文法 ⟶ 分析表产生器 ⟶ 分析表
LR 分析器工作
输入 ⟶ LR 分析总控程序(根据分析表) ⟶ 输出
6.1 总控程序(LR 分析器)的工作原理
实际上 LR 分析法是一种规范归约,关键问题在于寻找句柄
LR 分析法:把“历史”及“展望”综合抽象成状态;由栈顶的状态和现行的输入符号唯一确定每一步的工作
LR 分析器的核心就是一张分析表
ACTION[s,a] :当状态 s 面临输入符号 a 时,应采取什么动作
GOTO[s,X] :状态 s 面对文法符号 X 时,下一状态是什么
移进(shift):把(s,a)的下一个状态 s' 和 输入符号 a 推进栈,下一符号变为现行输入符号
归约(reduce):用某产生式 A→β 进行归约,假若 β 的长度为 r,归约作为是,去除栈顶 r 个项,使状态 sm−r 变为栈顶,然后把 (sm−r,A) 的下一状态 s′=GOTO[sm−r,A] 和文法符号 A 推进栈
状态 已归约串 输入串
(s0s1…sm,#X1…Xm,aiai+1…an#)
分析器根据 ACTION[sm,ai] 确定下一动作
若 ACTION[sm,ai] 为移进,且 s 为下一状态,则三元式格局变为: (s0s1…sms,#X1…Xmai,ai+1…an#)
若 ACTION[sm,ai] 为按 A→β 归约 ,三元式变为: (s0s1…sm−rs,#X1…Xm−rA,aiai+1…an#)
此处, s=GOTO(sm−r,A) ,r 为 β 的长度, β=Xm−r+1…Xm
若 ACTION[sm,ai] 为“接受”,则三元式不再变化,变化过程终止,宣布分析成功
若 ACTION[sm,ai] 为“报错”,则三元式变化过程终止,报告错误
示例:
文法 G(E):
1.E→E+T2.E→T3.T→T∗F4.T→F5.F→(E)6.F→i
6.2 LR 文法
对于一个文法,如果能够构造一张分析表,使得它的每个入口均是唯一确定的,则这个文法就称为 LR 文法
一个文法,如果能用一个每步顶多向前检查 k 个输入符号的 LR 分析器进行分析,则这个文法就称为 LR(k) 文法
6.3 LR(0) 项目集规范族的构造
字的前缀:指字的任意首部,如字 abc 的前缀有 ϵ,a,ab,abc
活前缀:不含句柄之后的任何符号的前缀。对于规范句型 αβγ , β 为句柄,如果 αβ=u1u2…ur ,则符号串 u1u2…ui(1≤i≤r) 是 αβγ 的活前缀( γ 必定为终结符串)
规范归约的过程中,保证分析栈中的总是活前缀,就说明分析采用的移进 / 归约动作是正确的
项目概念:文法 G 的每个产生式的右部添加一个圆点称为 G 的 LR(0) 项目
如: A→XYZ 有四个项目: A→⋅XYZ;A→X⋅YZ;A→XY⋅Z;A→XYZ⋅
A→α⋅ 称为 “归约项目”
归约项目 S′→α⋅ 称为 “接受项目”
A→α⋅aβ(a∈VT) 称为 “移进项目”
A→α⋅Bβ(B∈VN) 称为 “待约项目”
构造识别文法所有活前缀的 NFA 方法
若状态 i 为 X→X1⋯Xi+1⋅Xi⋯Xn ,状态 j 为 X→X1⋯Xi−1Xi⋅Xi+1⋯Xn ,则从状态 i 画一条标志为 Xi 的有向边到状态 j
若状态 i 为 X→α⋅Aβ ,A 为非终结符,则从状态 i 画一条 ϵ 边到所有状态 A→⋅γ
例:文法 G(S′)
S′→EE→aA∣bBA→cA∣dB→cB∣d
该文法的项目有:
1.S′→⋅E2.S′→E⋅ 3.E→⋅aA4.E→a⋅A5.E→aA⋅ 6.E→⋅bB7.E→b⋅B8.E→bB⋅ 9.A→⋅cA10.A→c⋅A11.A→cA⋅
12.A→⋅d13.A→d⋅ 14.B→⋅cB15.B→c⋅B16.B→cB⋅ 17.B→⋅d18.B→d⋅
识别活前缀的 NFA
识别活前缀 DFA
识别构成一个文法活前缀的 DFA 的项目集(状态)的全体称为文法的一个 LR(0) 项目集规范族
有效项目
我们说项目 A→β1⋅β2 对活前缀 αβ1 是有效的,其条件是存在规范推导 S′⇒∗αAω⇒αβ1β2ω
拓广文法
构造一个G',它包含了整个 G,但引进了一个不出现在 G 中的非终结符 S',并加进一个新的产生式 S′→S ,S' 是 G' 的开始符号
G' 唯一的接受态:仅含项目 S′→S⋅ 的状态
闭包 CLOSURE
假设 I 是文法 G' 的任一项目集,定义和构造 I 的闭包 CLOSURE(I) 如下:
若 A→α⋅Bβ 属于 CLOSURE(I),那么对任何关于 B 的产生式 B→γ ,项目 B→⋅γ 也属于CLOSURE(I)
重复执行上述两个步骤直至 CLOSURE(I) 不再增大为止
GO函数
为了识别活前缀,我们定义一个状态转换函数 GO 的一个状态转换函数。I 是一个项目集,X 是一个文法符号。函数值 GO(I, X) 定义为: GO(I,X)=CLOSURE(J) 其中 J={任何形如A→αX⋅β的项目∣A→α⋅Xβ∈I}
直观上说,若 I 是对某个活前缀 γ 有效的项目集,那么,GO(I, X) 便是对 γX 有效的项目集
例:文法 G(S′)
S′→EE→aA∣bBA→cA∣dB→cB∣d
I0={S′→⋅E,E→⋅aA,E→⋅bB}
GO(I0,E)=closure(J)==closure({S′→E⋅}){S′→E⋅}=I1
GO(I0,a)=closure(J)==closure({E→a⋅A}){E→a⋅A,A→⋅cA,A→⋅d}=I2
GO(I0,b)=closure(J)==closure({E→b⋅B}){E→b⋅B,B→⋅cB,B→⋅d}=I3
通过 6.2 和 6.3 我们学了两种构造识别活前缀的 DFA 的方法
项目→NFA→DFA
Closure→GO→DFA
6.4 LR(0) 分析表
LR(0) 文法
假设一个文法 G 的拓广文法 G' 的活前缀识别自动机中的每个状态(项目集)不存在下列情况:
则称 G 是一个 LR(0) 文法
构造 LR(0) 分析表的算法
令每个项目集 Ik 的下标 k 作为分析器的状态,包含项目 S′→⋅S 的集合 Ik 的下标 k 的分析器的初态
LR(0)分析表的 ACTION 和 GOTO 子表构造
若项目 A→α⋅aβ 属于 Ik 且 GO(Ik,a)=Ij , a 为终结符,则置 ACTION[k,a] 为 sj
若项目 A→α⋅ 属于 Ik ,那么,对任何终结符 a (或结束符 #),置 ACTION[k,a] 为 rj (假定产生式 A→α 是文法 G' 的第 j 个产生式)
若项目 S′→S⋅ 属于 Ik ,则置 ACTION[k,#] 为 acc
若 GO(Ik,A)=Ij ,A 为非终结符,则置 GOTO[k,A]=j
分析表中凡不能用规则 1 至 4 填入信息的空白格均置入“报错信息”
接着 6.3 中的例子,构造分析表
6.5 SLR 分析表
LR(0) 文法太简单,没有实用价值。一个项目集里可能既存在移进项目,也存在归约项目,会产生冲突
消除冲突:引入 FOLLOW 集
假定一个 LR(0) 规范族中含有如下的一个项目集(状态) I={X→α⋅bβ,A→α⋅,B→α⋅} 。 FOLLOW(A) 和 FOLLOW(B) 的交集为 ϕ ,且不包含 b ,那么,当状态 I 面临任何输入符号 a 时,可以:
若 a∈FOLLOW(A) ,用产生式 A→α 进行归约
若 a∈FOLLOW(B) ,用产生式 B→α 进行归约
一般情况:
假定 LR(0) 规范族的一个项目集 I={A1→α⋅a1β1,A2→α⋅a2β2,⋯,Am→α⋅amβm,B1→α⋅,B2→α⋅,⋯,Bn→α⋅,}
如果集合 {a1,⋯,am},FOLLOW(B1),⋯,FOLLOW(Bn) 两两不相交(包括不得有两个 FOLLOW 集合有 #),则:
若 a 是某个 ai,i=1,2,⋯,m ,则移进
若 a∈FOLLOW(Bi),i=1,2,⋯,n ,则用产生式 Bi→α 进行归约
6.6 构造 SLR(1) 分析表方法
首先把 G 拓广为 G',对 G' 构造 LR(0) 项目集规范族 C 和活前缀识别自动机的状态 转换函数 GO
然后使用 C 和 GO ,按照下面的算法构造 SLR 分析表
令每个项目集 Ik 的下标 k 作为分析器的状态,包含项目 S′→⋅S 的集合 Ik 的下标 k 的分析器的初态
SLR(1)分析表的 ACTION 和 GOTO 子表构造
若项目 A→α⋅aβ 属于 Ik 且 GO(Ik,a)=Ij , a 为终结符,则置 ACTION[k,a] 为 sj
若项目 A→α⋅ 属于 Ik ,那么,对任何终结符 a , a∈FOLLOW(A) ,置 ACTION[k,a] 为 rj (假定产生式 A→α 是文法 G' 的第 j 个产生式)
若项目 S′→S⋅ 属于 Ik ,则置 ACTION[k,#] 为 acc
若 GO(Ik,A)=Ij ,A 为非终结符,则置 GOTO[k,A]=j
分析表中凡不能用规则 1 至 4 填入信息的空白格均置入“报错信息”
SLR(1) 和 LR(0) 构造的区别:只有第二步不同
接着 6.3 中的例子,构造分析表
6.7 LR(1) 分析表的构造
SLR冲突消解存在的问题:计算 FOLLOW 集合所得到的超前符号集合可能大于实际能出现的超前符号集
SLR 在方法中,如果项目集 Ii 含项目 A→α⋅ ,而且下一个输入符号 a∈FOLLOW(A) ,则状态 i 面临 a 时,可选用 “用 A→α 归约” 动作
但在有些情况下,当状态 i 显示在栈顶时,栈里的活前缀未必允许把 α 归约成 A,因为可能根本就不存在一个形如 βAα 的规范句型
在这种情况下,用 A→α 归约不一定合适
我们需要重新定义项目,使得每个项目都附带 k 个终结符
每个项目的一般形式是 [A→α⋅β,a1a2⋯ak] ,这样的一个项目称为一个 LR(k) 项目。项目中的 a1a2⋯ak 称为它的向前搜索符串 (展望串)
向前搜索符串 仅对 归约项目 [A→α⋅,a1a2⋯ak] 有意义
对于任何移进或待约项目 [A→α⋅β,a1a2⋯ak],β=ϵ ,搜索符串 a1a2⋯ak 没有直接作用
我们研究当 k=1 的情况
有效项目
形式上我们说一个 LR(1)项目 [A→α⋅β,a] 对活前缀 γ 是有效的,如果存在规范推导 S′⇒∗δAω⇒δαβω
其中 γ=δα
a 是 ω 的第一个符号,或者 a 为 # 而 ω 为 ϵ
LR(1) 项目集规范族
需要两个函数 CLOSURE 和 GO
[A→α⋅Bβ,a] 对活前缀 γ=δα 是有效的,则对于每个形如 B=ξ 的产生式,对任何 b∈FIRST(βa),[B→⋅ξ,b] 对 γ 也是有效的
项目集的闭包 CLOSURE(I)
若项目 [A→α⋅Bβ,a] 属于 CLOSURE(I), B=ξ 是一个产生式,那么,对于 FIRST(βa) 的每个终结符 b,如果 [B→⋅ξ,b] 原来不在 CLOSURE(I) 中,则把它加进去
重复步骤 2 ,直至 LCLOSURE(I) 不再增大为止
项目集的装换函数 GO
令I 是一个项目集,X 是一个文法符号,函数 GO(I, X) 定义为: GO(I,X)=CLOSURE(J) 其中 J={任何形如[A→αX⋅β,a]的项目∣[A→α⋅Xβ,a]∈I}
构造 LR(1) 分析表的算法
令每个项目集 Ik 的下标 k 作为分析器的状态,包含项目 [S′→⋅S,#] 的集合 Ik 的下标 k 的分析器的初态
LR(0)分析表的 ACTION 和 GOTO 子表构造
若项目 [A→α⋅aβ,b] 属于 Ik 且 GO(Ik,a)=Ij , a 为终结符,则置 ACTION[k,a] 为 sj
若项目 [A→α⋅,a] 属于 Ik ,则置 ACTION[k,a] 为 rj (假定产生式 A→α 是文法 G' 的第 j 个产生式)
若项目 [S′→⋅S,#] 属于 Ik ,则置 ACTION[k,#] 为 acc
若 GO(Ik,A)=Ij ,A 为非终结符,则置 GOTO[k,A]=j
分析表中凡不能用规则 1 至 4 填入信息的空白格均置入“报错信息”
示例:拓广文法 G(S')
0.S′→S1.S→BB2.B→aB3.B→b