字母表(alphabet):符号的有限集合 Σ
串:符号的有穷序列
语言:字母表上的一个串集
正规式 |
定义的语言 |
ε |
{ε} |
a |
{a} |
(r)|(s) |
L(r)∪L(s) |
(r)(s) |
L(r)L(s) |
(r)* |
(L(r))* |
(r) |
L(r) |
运算符优先级:*
> 连接运算 > |
- 状态集合 S
- 输入字母表 Σ
- 转换函数 move:S×Σ→S
- 唯一的初态 s∈S
- 终态集合 F
- 状态集合 S
- 输入字母表 Σ
- 转换函数 move:S×(Σ∪{ε})→P(S),其中 P(S) 是 S 的幂集
- 唯一的初态 s∈S
- 终态集合 F
- DFA 的一个状态,是 NFA 的一个状态集合
- 当读入了 a1,a2,…,an 后,若 NFA 能到达的所有状态为 s1,s2,…,sk,则 DFA 到达状态 {s1,s2,…,sk}
DFA 是否最简的判断依据:状态的可区分性.
对于 DFA M,如果存在串 w,使得 M 从状态 s 出发,在面临 w 时,最终停在一个接受状态,而从状态 t 出发,在面临 w 时,却最终停在一个非接受状态,则称串 w 可用来区别状态 s 和状态 t。
如果找不到任何的串来区别 s 和 t,则称状态 s 和状态 t 是不可区别的,反之则称状态 s 和状态 t 是可区别的。
DFA 化简步骤(要求 move 是全函数,为此可先给 DFA 增加死状态,化简后再去除):
- 构造状态集合的初始划分 Π:将状态集合划分为接受状态集合 F 和非接受状态集合 S−F。
- 反复应用以下过程,直到 Π 不再变化:
对于 Π 中的每个集合 G,把 Π 中的 G 替换为 G 的一个划分,该划分满足:对于 G 中的任意两个状态 s 和 t,划分后它们在同一个集合中,当且仅当对任意的符号 a, s 和 t 的 a 转换是到 Π 中的同一个集合中。
- 从 Π 中的每个集合中选一个状态来代表它,这些状态就是最简 DFA 的状态。若 s 是这样一个代表,则若在化简前的 DFA 中,状态 s 的 a 转换到状态 t,且 t 所在的集合的代表是 r,则在化简后的 DFA 中,s 的 a 转换到 r。
略
一个 0 型文法(短语文法) G 是一个四元组 (VT,VN,S,P),其中:
- VT 是非空有限的终结符集合;
- VN 是非空有限的非终结符集合;
- S 是一个非终结符,称作开始符号;
- P 是产生式的有限集合,每个产生式的形式是 α→β,其中 α∈(VT∪VN)∗VN(VT∪VN)∗,β∈(VT∪VN)∗。
0 型文法 G 是 1 型文法(上下文有关文法),如果 G 的任何产生式 α→β 或者满足 ∣α∣≤∣β∣,或者形如 S→ε,且 S 没有出现在任何产生式的右部;
0 型文法 G 是 2 型文法(上下文无关文法),如果 G 的任何产生式都形如 A→α,其中 A∈VN,α∈(VT∪VN)∗。
0 型文法 G 是 3 型文法(正规文法),如果 G 的任何产生式都形如 A→aB 或 A→a,其中 A,B∈VN,a∈VT。
后面均只涉及上下文无关文法,因此将其简称为文法。
定义见上文。
直接左递归 A→Aα∣β 可以用非左递归的
- A→βA′
- A′→αA′∣ε
来代替。
而非直接左递归
- S→Aa∣b
- A→Sd∣ε
则可先变换成直接左递归
- S→Aa∣b
- S→Aad∣bd∣ε
再消除左递归
- S→Aa∣b
- A→bdA′∣A′
- A′→adA′∣ε
文法 A→αβ1∣αβ2 可替换为
- A→αA′
- A′→β1∣β2
要求:
一个文法符号串α 的开始符号集合 FIRST(α) 是从 α 推导出的串的起始终结符的集合:
FIRST(α)={a∣α⇒∗a…,a∈VT}
特别地,当 α⇒∗ε 时,规定 ε∈FIRST(α)。
非终结符 A 的后继符号集合 FOLLOW(A) 是所有在句型中可以直接出现在 A 后面的终结符集合:
FOLLOW(A)={a∣S⇒∗…Aa…,a∈VT}
约定如果 A 是某个句型的最右符号(即 S⇒∗aA),那么 $ 也属于 FOLLOW(A)。
FIRST 集的计算方法
- 若 X→a…,则将终结符 a 加入 FIRST(X) 中;
- 若 X→ε,则将 ε 加入 FIRST(X) 中;
- 若 X→Y…,其中 Y 是非终结符,则将 FIRST(Y)\{ε} 加入到 FIRST(X) 中;
- 若 X→Y1Y2…Yk,其中 Y1Y2…Yi−1(2≤i≤k) 都是非终结符,且 Y1Y2…Yi−1 的 FIRST 集合中均包含 ε,则将 FIRST(Yj)(j=1,2,…,i) 中的所有非 ε 元素加入到 FIRST(X) 中,特别地,若 Y1,Y2,…,Yk 均有 ε 产生式,则将 ε 加入 FIRST(X) 中。
FOLLOW 集的计算方法
- 对于文法开始符号 S,将 $ 加入 FOLLOW(S) 中;
- 对产生式 A→αBβ,将 FIRST(β)\{ε} 中的元素加入 FOLLOW(B) 中;若 β→ε,则将 FOLLOW(A) 中的元素加入 FOLLOW(B) 中。
若文法 G 满足,对于文法的任何两个产生式 A→α∣β,都有
- FIRST(α)∩FIRST(β)=∅;
- 若 β⇒∗ε,则 FIRST(α)∩FOLLOW(A)=∅,
则称文法 G 是 LL(1) 文法。
计算 SELECT 集:给定上下文无关文法的产生式 A→α, 若 α 不能推导出 ε,则 SELECT(A→α)=FIRST(α);若 α 能推导出 ε,则 SELECT(A→α)=(FIRST(α)\{ε})∪FOLLOW(A)。
为每一个非终结符写一个分析过程。
表驱动的预测分析器有一个输入缓冲区、一个栈、一张分析表和一个输出流。输入缓冲区包含被分析的串 w,后面跟一个符号 $ 作为输入串的结束标记;栈中存放文法符号串,栈底符号是 $;分析表 M 是一个二维数组 M[A,a],A 是非终结符,a 是终结符或 $。
初始时,$S 在栈中,其中 S 是开始符号且在栈顶,w$ 在输入缓冲区中,输入指针指向 w$ 的第一个符号。
预测分析器根据当前的栈顶符号 X 和输入符号 a 来决定分析器的动作,有四种可能:
- 如果 X=a=$,分析器宣布分析完全成功而停机;
- 如果 X=a=$,分析器弹出栈顶符号 X,并推进输入指针使其指向下一个符号;
- 如果 X 是非终结符,且 M[X,a] 是 X 的产生式,即 M[X,a]=X→Y1Y2…Yk,则分析器弹出栈顶符号 X,并把 YkYk−1…Y1 依次压入栈,使 Y1 在栈顶;
- 其他情况,出错。
预测分析表 M 的构建
对于文法的每个产生式 A→α,执行:
- 对 FIRST(α) 的每个终结符 a,把 A→α 加入 M[A,a] 中;
- 如果 ε∈FIRST(α),对 FOLLOW(A) 中的每个终结符 b(包括 $),把 A→α 加入 M[A,b] 中。
这等价于:
对于文法的每个产生式 A→α,对所有的 a∈SELECT(A→α),把 A→α 加入 M[A,a] 中。
紧急方式的错误恢复:发现错误时,分析器每次抛弃一个输入记号,直到输入记号属于某个特定的同步记号集合为止。
右句型:最右推导可得到的句型
句柄:右句型 γ 的句柄是一个产生式 A→β 的右部 β 以及 γ 中的一个位置,在这个位置可找到串 β,且用 A 替换这里的 β 得到最右推导的前一个右句型。
- 句柄是句型的一个子串;
- 把句柄归约成非终结符代表了某一步最右推导的逆过程。
LR 分析器包含一个输入缓冲区、一个栈、一张分析表和一个输出流,其中:
- 输入缓冲区包含以 $ 结束的被分析的串 w$;
- 栈中存放过去的状态符号和文法符号(真正实现时文法符号不是必需的),栈底符号是 $;
- 分析表 M 由两部分组成:动作表 action 和转移表 goto。
LR 分析器根据栈顶的状态和当前的输入符号检索分析表 M 以决定接下来的动作。具体地将,分析器根据栈顶状态 s 和当前的输入符号 a 访问 action[s,a],并执行相应的动作:
- 若 action[s,a]= 移进 t,则分析器执行移进动作,将当前输入符号 a 和状态 t 压入栈,并移动输入指针使其指向下一个输入符号;
- 若 action[s,a]= 归约 A→β,则分析器执行归约动作,从栈中弹出 2r 个符号(其中 r 是 β 的长度),即弹出 r 个文法符号和 r 个状态符号(这 r 个文法符号恰好构成 β),此时栈顶暴露出过去的某个状态 t。分析器接下来访问 goto 表,把产生式左部的文法符号 A 和新状态 goto[t,A] 压入栈(注意当前输入符号没有改变)。
- 若 action[s,a]= 接受,则分析成功完成;
- 若 action[s,a]= 出错,则分析器发现错误。
LR 分析方法的特点:
- 栈中的文法符号总是形成一个活前缀;
- 分析表的转移表本质上是识别活前缀的 DFA。
项目:文法 G 的 LR(0) 项目(简称项目)是在其产生式右部的某个地方加点的产生式。如从产生式 A→XYZ 可得到如下四个项目:
A→⋅XYZA→X⋅YZA→XY⋅ZA→XYZ⋅
规定对于产生式 A→ε,只能得到项目 A→⋅。
拓广文法:如果文法 G 的开始符号是 S,那么 G 的拓广文法 G′ 是在 G 的基础上增加新的文法符号 S′ 和产生式 S′→S。
闭包函数 closure:如果 I 是文法 G 的一个项目集,那么 closure(I) 是应用如下两条规则从 I 构造的项目集:
- 初始时,将 I 的每个项目都加入 closure(I);
- 如果 A→α⋅Bβ 在 closure(I) 中,且 B→γ 是产生式,则将 B→⋅γ 加入 closure(I) 中。反复应用此规则,直到没有新的项目加入 closure(I) 为止。
转移函数 goto:如果 I 是文法 G 的一个项目集,X 是文法符号,goto(I,X) 是满足 A→α⋅Xβ 属于 I 的所有项目 A→αX⋅β 的集合的闭包。
项目集规范族:拓广文法 G′ 的 LR(0) 项目集规范族 C={I0,I1,…,In} 是项目集的集合,其构建方法如下:
- 将项目集 closure({S→⋅S}) 加入 C 中;
- 对于 C 中的每个项目集 I 和 G 的每个文法符号 X,如果 goto(I,X) 非空且不在 C 中,则将其加入 C 中。重复这一步直到没有新的项目集加入 C。
项目集规范族和 goto 函数构成了识别 G′ 的活前缀的 DFA。
构造拓广文法 G′ 的 SLR(1) 分析表
-
构造 G′ 的 LR(0) 项目集规范族 C={I0,I1,…,In}。
-
对于 C 中的每个项目集 I 构造一个状态 i,且 action 表的值按下列规则确定:
- 如果 A→α⋅aβ 在 Ii 中,且 goto(Ii,a)=Ij,则令 action[i,a]= 移进 j;
- 如果 S′→S⋅ 在 Ii 中,那么令 action[i,$]= 接受;
- 如果 A→α⋅ 在 Ii 中,那么对 FOLLOW(A) 中的所有 a,令 action[i,a]= 归约 A→α,此时 A 不能为 S′。
如果在应用上述规则时产生冲突,则该文法就不是 SLR(1) 的,算法流产,不产生分析器;
-
对所有的非终结符 A,如果 goto(Ii,A)=Ij,令 goto[i,A]=j;
-
不能由规则 2. 和 3. 定义的表项均为空白,表示出错;
-
分析器的初始状态是包含项目 S′→⋅S 的项目集 I0 所代表的状态。
LR(1) 项目:[A→α⋅β,a],其中 A→αβ 是产生式,A→α⋅β 称作项目的心,a 是终结符或 $,称作项目的搜索符。项目 [A→α⋅β,a] 和 [A→α⋅β,b] 可简写成 [A→α⋅β,a/b]
闭包函数 closure:
- 初始时,将 I 的每个项目都加入 closure(I);
- 如果 [A→α⋅Bβ,a] 在 closure(I) 中,且 B→γ 是产生式,则对于 FIRST(βa) 中的每个终结符 b,将 [B→⋅γ,b] 加入 closure(I) 中。反复应用此规则,直到没有新的项目加入 closure(I) 为止。
转移函数 goto:goto(I,X) 是满足 [A→α⋅Xβ,a] 属于 I 的所有项目 [A→αX⋅β,a] 的集合的闭包。
LR(1) 项目集规范族 C:
- 初始时将项目集 closure({[S′→⋅S,$]}) 加入 C 中;
- 对于 C 中的每个项目集 I 和 G 中的每个文法符号 X,如果 goto(I,X) 非空且不在 C 中,则将其加入 C 中。重复这一步直到没有新的项目集加入 C 中。
构造拓广文法 G′ 的规范的 LR 分析表
-
构造 G′ 的 LR(1) 项目集规范族 C={I0,I1,…,In}。
-
对于 C 中的每个项目集 I 构造一个状态 i,且 action 表的值按下列规则确定:
- 如果 [A→α⋅aβ,b] 在 Ii 中,且 goto(Ii,a)=Ij,则令 action[i,a]= 移进 j;
- 如果 [S′→S⋅,$] 在 Ii 中,那么令 action[i,$]= 接受;
- 如果 [A→α⋅,a] 在 Ii 中,令 action[i,a]= 归约 A→α,此时 A 不能为 S′。
如果在应用上述规则时产生冲突,则该文法就不是 LR(1) 的,算法流产,不产生分析器;
-
对所有的非终结符 A,如果 goto(Ii,A)=Ij,令 goto[i,A]=j;
-
不能由规则 2. 和 3. 定义的表项均为空白,表示出错;
-
分析器的初始状态是包含项目 [S′→⋅S,$] 的项目集 I0 所代表的状态。
同心的 LR(1) 项目集:略去搜索符后相同的项目集。
构造拓广文法 G′ 的 LALR 分析表
-
构造 G′ 的 LR(1) 项目集规范族 C={I0,I1,…,In}。
-
寻找 LR(1) 项目集规范族中同心的项目集,用它们的并集代替他们,构成 LALR(1) 项目集规范族 C′={J0,J1,…,Jm}。
-
在 C′ 上应用构造 LR(1) 分析表的算法构造 LALR 分析表。
-
LR 分析器在访问转移表时绝不会遇到出错条目,即决不会把不正确的后继移进栈;
-
规范的 LR 分析器甚至在报告错误之前决不做任何无效归约
紧急方式错误恢复
- 退栈,直至出现状态 s,它有预先确定的非终结符 A 的转移(即 goto[s,A] 有定义);
- 抛弃若干个输入符号,直到找到 a,它是 A 的合法后继;
- 把 A 和状态 goto[s,A] 压入栈,恢复正常分析。
A 的选择一般是代表主要语法构造的非终结符,如表达式、语句或程序块。
短语级恢复:对剩余输入作局部修正,如用分号代替逗号,删去多余的分号,或插入遗漏的分号等。
略
语法制导定义(Syntax-Directed Definition,SDD)是带属性和语义规则的上下文无关文法,其中每个文法符号有一组属性,每个产生式有一组语义规则,其中的文法成为基础文法,文法符号 X 的属性 a 记作 X.a。
在一个语法制导定义中,A→α 是文法产生式,b=f(c1,c2,…,ck) 是该产生式的一个语义规则,其中 f 是函数,b 和 c1,c2,…,ck 是该产生式的文法符号的属性,该产生式定义属性 b,则称属性 b 依赖于属性 c1,c2,…,ck,且:
-
如果 b 是 A 的属性,b 和 c1,c2,…,ck 是产生式右部文法符号的属性或 A 的其他属性,则称 b 为 A 的综合属性;
-
如果 b 是产生式右部某个文法符号 X 的属性,c1,c2,…,ck 是产生式右部文法符号的属性或 A 的属性,则称 b 为 X 的继承属性。
属性文法是指语义规则函数无副作用的语法制导定义。
仅使用综合属性的语法制导定义称为 S 属性定义。
每个结点的属性值都标注出来的分析树称作注释分析树,计算各结点的属性值的过程称作分析数的注释或修饰。
属性依赖图:分析树上每个结点的每个属性在依赖图上都有一个结点,如果属性 b 依赖于属性 c,则从 c 的结点到 b 的结点有一条有向边。
显然,依赖图的任何一个拓扑排序都是分析树中结点的属性计算的一个正确次序。
至此,由语法制导定义所规范的翻译可以准确地按照下述步骤完成:
- 根据基础文法构造输入的分析树;
- 构造属性依赖图;
- 对依赖图的结点进行拓扑排序;
- 按拓扑排序的次序计算属性。
这种计算方法称作分析树方法。
对语义规则进行分析,对每个产生式得到与其相关联的一组语义规则的计算次序,据此把计算次序在编译前就确定下来的方法称作基于规则的方法。
不是根据语义规则的特点来选取计算策略,而是根据事先确定的计算策略来限定编译器设计者所提供的语义规则的形式的计算方法称作忽略规则的方法。
仅使用综合属性的语法制导定义称为 S 属性定义。
语法树是分析树的浓缩表示,算符和关键字不是作为叶结点而是分支节点。
数据结构:语法树的结点可用有若干域的记录来实现,其中,对于算符结点,一个域存放算符,作为该节点的标记,其余的域存放指向运算对象的指针;对于基本运算对象结点,一个域存放其类型,另一个域存放其值。真正用于翻译时语法树的结点还可能有其他域来存储附加在该节点的其他属性值或指向其他属性值的指针。
分析器把文法符号的综合属性放在栈中,每当归约时,根据出现在栈顶的产生式右部符号的属性来计算左部符号的综合属性。
语法制导定义是 L 属性的,如果每个产生式 A→X1X2…Xn 的每条语义规则计算的是 A 的综合属性,或者计算的是 Xj 的继承属性(1≤j≤n)且它仅依赖(1)该产生式中 Xj 左边符号 X1,X2,…,Xj−1 的属性;和(2)A 的继承属性。
显然,S 属性定义是 L 属性定义。
语法制导翻译方案(Syntax-Directed Translation scheme,SDT)是语法制导定义的一种补充,区别是将语义动作插入在产生式右部的任何地方。这是一种分析和动作交错的方法,以表示动作的执行时机。
如 A→α{…}β 中的语义动作 {…} 应当在 α 的推导(或向 α 的归约)结束以后,β 的推导(或向 β 的归约)开始之前执行。
从语法制导翻译方案构造语法制导的递归下降预测翻译器
-
为每个非终结符 A 构造一个函数,A 的每个继承属性声明为该函数的一个形式参数,A 的综合属性作为该函数的返回值,对 A 的产生式的其他每个文法符号的每个属性,在该函数中声明一个对应的局部变量;
-
非终结符 A 对应的函数中,根据当前的输入符号决定使用哪一个产生式;
-
每个产生式对应的程序代码中,按照从左到右的次序,对于产生式右部的记号(终结符)、非终结符和语义动作分别作以下工作:
-
对于带有综合属性 x 的终结符 X(为于属性区分,这里终结符也用大写字母表示),把 x 的值存入为 X.x 设置的变量中,然后产生一个匹配 X 的调用,并推进输入指针;
-
对于非终结符 B,产生赋值语句 c=B(b1,b2,…,bn),其中,B 是与非终结符 B 相对应的函数,b1,b2,…,bn 是为非终结符 B 的继承属性设置的变量,c 是为非终结符 B 的综合属性设置的变量;
-
对于每个语义动作,把语义动作的代码复制到函数代码中,将其中对于属性的引用改为对函数中相应的局部变量的引用。
在自下而上分析的框架中实现 L 属性定义。
能实现任何基于 LL(1) 文法的 L 属性定义,也能实现许多(但不是全部)基于 LR(1) 文法的 L 属性定义。
为此,需要对翻译方案进行修改。要删除翻译方案中嵌入的动作,以使语义动作都出现在产生式右端,在归约时执行;还需要处理继承属性。
对于每个嵌入的语义动作,在文法中嵌入一个对应的推出空串的标记非终结符 M 来代表它,并将该语义动作加入到产生式 M→ε 的右端。
如翻译方案
E→TRR→+T {print(′+′);} R1 ∣ −T {print(′−′);} R1 ∣ εT→num {print(num.val);}
可变换成
E→TRR→+TMR1 ∣ −TNR1 ∣ εT→num {print(num.val);}M→ε {print(′+′);}N→ε {print(′−′);}
对于能够静态确定其所依赖的属性在分析栈中的位置的继承属性,直接增加对应的语义动作即可。
对于不能确定位置的属性,可尝试增加标记非终结符,使属性的位置可以静态确定。如对于语法制导定义
产生式S→aACS→bABCC→c语义规则C.i=A.sC.i=A.sC.s=g(C.i)
可变换为
产生式S→aACS→bABMCC→cM→ε语义规则C.i=A.sM.i=A.s, C.i=M.sC.s=g(C.i)M.s=M.i
从而对于非终结符 C,不论是在 aAC 还是 bABMC 的情况下使用 C→c 时,C.i 的值都可从 stack[top−1].val 找到。
对于需要计算的继承属性(即该继承属性是某个综合属性的一个函数),也可通过增加标记非终结符来模拟继承属性的计算。如对于语法制导定义
产生式S→aACC→c语义规则C.i=f(A.s)C.s=g(C.i)
可变换为
产生式S→aANCN→εC→c语义规则N.i=A.s, C.i=N.sN.s=f(N.i)C.s=g(C.i)
从而把 f(A.s) 的计算移到对标记非终结符 N 归约时(推导 C 之前)进行。
可以证明,对于基于 LL(1) 文法的 L 属性定义,总可以通过系统地引入标记非终结符,以在 LR 分析期间完成属性计算,且因为每个标记非终结符只有一个产生式,因此引入标记非终结符后的文法仍然是 LL(1) 的,进而是 LR(1) 的,从而不会引起 LR 分析动作的冲突。
但对于 LR(1) 文法的 L 属性定义,引入标记非终结符可能使文法不再是 LR(1) 的。
© 2019 倪可塑 保留所有权利。