5.1. 引言
- “移进-归约”法基本思想
- 算法优先分析技术及优先函数
- LR 系列分析技术(LR(0), SLR(1), LR(1), LALR(1) 等)
- 二义性文法的应用
- 语法分析器的自动生成器 YACC
由于“移进-归约”法的基本思想,这些“归约”都是最左归约。因为输入串中的符号是从左向右逐个移进栈,这样最左边的可归约子串先被归约掉。
5.2. 自底向上的语法分析面临的问题
设有文法 G[S]
SA→cAd→a∣Aa
对于输入串 #caad#
,采用自底向上分析技术,根据“移进-归约”思想,有以下序列
其中,面临两个问题:
- 如何寻找“可归约子串”?如何判定使用 A→a 还是 A→Aa?
- 可归约子串被归约到到一个非终结符?
可归约子串总是在栈顶产生,不会在栈内部产生。另外,在自底向上语法分析中,只有 4 个动作:移进、归约、接受、报错。
5.3. 算符优先分析技术
5.3.1. 算符优先关系的定义
很好理解,就例如乘法的优先级要高于加法。对于一个输入串 #2*(3+4)#
可以画出语法分析树,最终归约到一个结果。
由图中,非终结符代表的是值,终结符都是运算符。因此在自底向上的算符优先分析中,运算符是指文法中的所有终结符,运算数(运算对象)是所有非终结符。归约的先后次序隐含了计算的先后次序。
算符优先分析技术对文法有一定的要求,如两个操作数之间至少有一个运算符。也就是说,若用非终结符表示操作数,则两个非终结符号之间至少要有一个或一个以上的运算符。
定义 5.1 若文法 G 中不存在形式 A→⋯UV⋯,其中 A,U,V 均为非终结符,则称该文法是算符文法。通常,算符文法中也不包含 A→ε,即不包含空规则。
由定义可知,算符文法产生的所有句型都满足:两个非终结符之间至少有一个或以上的终结符(运算符)
若文法 G 不是算符文法,则不能采用算符优先分析法。若是,则可以尝试对运算符进行优先关系分析。
在表达式 #2+3*4+5*6#
中只要通过相邻两个运算符之间的优先关系(如 2+3*4
中 +
和 *
相邻),就可以进行正确运算了。通过考察相邻运算符之间的优先关系,也许可以进行语法分析。由此引出“相邻”的概念
定义 5.2 若有 ab 或 aWb,其中 a,b∈VT,W∈VN,则称运算符 a,b 相邻
Warning
运算符 a 和 b 相邻,要求 a 在左边,b 在右边,先后顺序不能颠倒,且 a 与 b 之间要么直接依靠在一起,要么最多有一个非终结符。显然,运算符 a,b 相邻不一定有运算符 b,a 相邻成立。
定义 5.3 设文法 G 是一个算符文法,对文法 G 中任何一对终结符 a 和 b,定义:
- a≐b 当且仅当文法 G 中存在规则 A→⋯ab⋯ 或 A→⋯aRb⋯,其中 a,b∈VT;R∈VN
- a⋖b 当且仅当文法 G 中存在规则 A→⋯aR⋯ 并且 R⇒+b⋯ 或 R⇒+Qb⋯,其中 a,b∈VT;Q,R∈VN
- a⋗b 当且仅当文法 G 中存在规则 A→⋯Rb⋯ 并且 R⇒+⋯a 或 R⇒+⋯aQ,其中 a,b∈VT;Q,R∈VN
书上的 ≐ 符号好像点应该是夹在在等号两线中间,但是好像没找到这个符号,最像的只有 ≖,=⋅ 写出来又太丑
对于定义的理解
对于 1,产生式右部包含了 a,b,那么这两个算符必定同时能够被归约,即对应了算符的优先级相等
对于 2 和 3,R 能够推导出算符 b,那么在归约过程中,b 应当先被归约,而 aR 应当后被归约,也就对应着,先被归约的优先级更高,后被归约的优先级更低。
a,b 之间可能不存在优先关系,有可能是上述 3 种之一,也有可能是 3 种中的两种或多种。但对我们有用的是不存在或存在一种
定义 5.4 若一个算符文法 G 种任何一对终结符号 a,b 之间最多存在上述中的一种,则称文法 G 是一个算符优先文法。
显然,若一对终结符号 a,b 之间不存在优先关系,表明 a,b 不可能相邻
注意:不要混淆算符文法和算符优先文法
为方便处理,我们约定:
- #⋖ 任何终结符号
- 任何终结符号 ⋗#
- # 与 # 之间不存在优先关系
对于句型 AaBbCcDd,有产生式规则 U→BbCcD,则可以看作以下形式
可归约子串特点:左边 ⋖,中间 ≐,右边 ⋗
而对于一般形式的句型,可归约子串的结构与其形式类似。
算符优先分析法首先定义终结符之间的优先关系,然后通过该优先关系按图所示方式,寻找或确定可归约子串。由此,引出素短语的定义
定义 5.5 算符优先文法的句型中具有上图结构特征且至少含有一个终结符的子串称为该句型的一个素短语(质短语)
一个素短语不可能包含其他的素短语,素短语是归约的最小单位
算符文法不允许两个相邻非终结符或者只有单个非终结符在规则的右部
例 5.3 试分析下图终结符之间的优先关系
解答
- 有 A→CcdD,故 c≐d
- d 与 a 相邻, CcdD 先被归约,因此 d⋗a;或由 Z→Aa⋯,A⇒+⋯dD,d⋗a
- a 与 e 相邻,eE 先被归约,因此 a⋖e
句型 #CcdDaeE 的素短语有两个 CcdD 和 eE
定义 5.6 算符优先文法的一个句型中最左边的素短语称为最左素短语
5.3.2. 算符优先关系表的生成
定义 5.7 对文法中每一个非终结符 P,定义
- FirstVT(P)={a∣P⇒+a⋯ or P⇒+Qa⋯,a∈VT,Q∈VN}
- LastVT(P)={a∣P⇒+⋯a or P⇒+⋯aQ,a∈VT,Q∈VN}
基于这两个集合,可以给出如下表述 ⋖,⋗,≐ 的等价定义
定义 5.8 设文法 G 是一个算符文法,对文法 G 中任何一对终结符 a,b,定义
- a≐b 当且仅当文法 G 中存在规则 A→⋯ab⋯ or A→⋯aRb⋯,其中 a,b∈VT,R∈VN
- a⋖b 当且仅当文法 G 中存在规则 A→⋯aR⋯ and b∈FirstVT(R),其中 a,b∈VT,R∈VN
- a⋗b 当且仅当文法 G 中存在规则 A→⋯Rb⋯ and a∈LastVT(R),其中 a,b∈VT,R∈VN
要找出一个句型中的最左素短语,需要知道任何一对终结符之间的优先关系。为此用二维表登记所有的终结符之间的优先关系。
None | a | b |
---|
a | ⋗ | ⋖ |
b | ⋗ | ⋗ |
表格行中的终结符出现在左边,列中的终结符出现在右边。如表格 a 行 b 列中填写 ⋖,含义为 a⋖b,即 a 出现在左边,b 出现在右边相邻时,a 的优先关系小于 b
1. 求 FirstVT(P) 的算法
- 若有规则 P→a⋯ or P→Qa⋯,则 a∈FirstVT(P)
- 若有规则 P→Q⋯,则 FirstVT(Q) 全部加入 FirstVT(P)
- 反复利用上述两条规则,直到所有的非终结符的 FirstVT 集不再增大为止
2. 求 LastVT(P) 的算法
- 若有 P→⋯a or P→⋯aQ,则 a∈LastVT(P)
- 若有 P→⋯Q,则 LastVT(Q) 全部加入 LastVT(P)
- 反复利用上述两条规则,直到所有的非终结符的 LastVT 集不再增大为止
以求 FirstVT 集为例,具体实现时,可以定义一个布尔数组 FirstVT[P,a],FirstVT[P,a]=true 表示 a∈FirstVT(P)
对 FirstVT[P,a] 赋初值,再利用一个栈,将所有初始 FirstVT[P,a]=true 的符号对 (P,a) 入栈,然后对栈施加一个循环,用于完成 2 和 3 的任务
例 5.4 设有文法 G[S]
SAA→cAd→a→Aa
求出 FirstVT 集和 LastVT 集
解答
| FirstVT | LastVT |
---|
S | c | d |
A | a | a |
对每一个规则进行分析
-
有式子 cAd 所以 c≐d ;同时 c⋖FirstVT(A) 即 c⋖a ;有 LastVT(A)⋗d 即 a⋗d
-
对于第三条式子,LastVT(A)⋗a 即 a⋗a
| a | c | d |
---|
a | ⋗ | | ⋗ |
c | ⋖ | | ≐ |
d | | | |
由于表中没有多重定义,即一个单元格内没有同时存在多种关系,因此是算符优先文法。
5.3.3. 算符优先分析总控程序
有了算符优先关系表,就可以在“移进-归约”过程中,在栈内部寻找最左素短语,一旦找到,就进行归约。
例 5.6 还是拿最熟悉的文法 G
ETF→E+T∣T→T∗F∣F→(E)∣i
尝试分析句子 #i+i∗i#
- 当栈顶运算符优先关系为 a⋖b 或 a≐b 时,将 b 入栈
- 当栈顶运算符优先关系为 a⋗b 时,此时 b 不进栈,已经找到了最左素短语的尾部(右部),然后在栈内由栈顶向底搜索第一个出现 ⋖ 的运算符,此时找到最左素短语的头部,头尾之间的子串即为最左素短语,然后进行归约。
注意到,我们在过程中直接将 F∗F 归约为 T,将 T+F 归约为 E,其实分别是用 T→TF 和 E→E+T 来完成的。由此可见,算符优先分析法与规范归约的不同点在于:在前者中,由于非终结符代表的是值(运算对象),因而我们不必关心非终结符之间在名字上的区别,不论他的名字是 E 还是 F,所有的非终结符,都只是栈中作为相应属性值的一个占位符,名字无关紧要。因此最左短语 F+F 与规则 E→E+T 的右部是相匹配的。
由于非终结符对归约没有实质性影响,因此非终结符其实可以不用进栈
归纳起来,算符优先分析法的总控程序工作过程有:
5.3.4. 优先函数及其生成
在实际使用算符优先分析技术时,若有 n 个终结符,则优先关系表的大小为 n×n,此时我们需要减小表的大小,来进行优化。
基本想法:每个运算符(终结符)披上两个数。当终结符 a 在左边出现,配 f(a);当终结符 a 出现在右边,配 g(a). 如果每个终结符都能如愿配上两个数,则优先关系表的大小就可以缩减为 2n
定义 5.9 算符优先文法 G 中每个终结符 a 对应着两个数 f(a) 和 g(a),满足
- 若 a⋖b 则 f(a)<g(a)
- 若 a⋗b 则 f(a)>g(a)
- 若 a≐b 则 f(a)=g(a)
则称 f 和 g 为算符优先文法 G 的优先函数
每个算符优先文法,都存在一张优先关系表,但并不总是存在优先函数。如果存在优先函数,实际上也就存在无穷多个优先函数。因为只要把一个优先函数的每一个数加上一个 k,就可以形成一个优先函数。
下面介绍一些构造优先函数的方法
1. Bell 有向图法
基本步骤:
- 对每个终结符 a1,a2,⋯,an(包含 #),上排画 n 个结点,标记为 f(ai),i=1,2,⋯,n;下排画 n 个结点,标记为 g(aj),j=1,2,⋯,n
- 画有向边
- 若 ai⋗aj,则画 f(ai)→g(aj)
- 若 ai⋖aj,则画 g(aj)→f(ai)
- 若 ai≐aj,则画 f(ai)⇌g(aj)
- 配数。每个结点(无论 f 还是 g)都配上一个数 x,它等于从该结点出发沿有向边所能到达的结点总数(结点自身也算)
- 检验。检验这样构造出来的优先函数是否与优先关系表一致。若一致,则即为所求,否则该优先关系表不存在优先函数。
可以证明,这样构造出来的函数正是所要求的优先函数,也就是说,函数满足:
- 若 a≐b,则 f(a)=g(b)
- 若 a⋗b,则 f(a)>g(b)
- 若 a⋖b,则 f(a)<g(b)
一些说明
当 a⋗b,从 f(a) 发出有向边到 g(b),因此有 f(a)⩾g(b)。而对于等号成立条件来说,不妨假设有两者相等,则一定存在以下有向边构成的环路:
fa→gb→fq1→gq1→fq2→gq2→⋯→gqm→fa这表示有下面的优先关系存在
a⋗b,p1⩽⋅b,p1⋅⩾q1,⋯,pm⋅⩾qm,a⩽⋅qm而对于任何优先函数 f 和 g,按优先级定义,对于上述描述,有
f(a)>g(b)⩾f(p1)⩾g(q1)⩾⋯⩾f(pm)⩾g(qm)⩾f(a)由此得到矛盾。这一矛盾表明,当 a⋗b时,如果 f(a)=g(b),则不存在优先函数。
这样带有环路的,是不存在优先函数的,所以此处的等号应当不成立。
例 5.7 对表中的优先关系矩阵,用 Bell 有向图法构造优先函数
| a | b |
---|
a | ≐ | ≐ |
b | ≐ | ⋗ |
解答
表不一致,不存在优先函数。
2. 逐次加一法(Floyd 法)
- 初始所有终结符 f(a)=g(a)=1
- 对优先关系表中每一对终结符 (a,b),分情况讨论
- 若 a⋖b 但 f(a)⩾g(b),令 g(b)=f(a)+1
- 若 a⋗b 但 f(a)⩽g(b),令 f(a)=g(b)+1
- 若 a≐b 但 f(a)=g(b),令 f(a)=g(b)=max{f(a),g(b)}
- 反复执行 2,直到以下两种情况
- f(a),g(b) 都不再变化,此时 f,g 为所求的优先函数
- 每个 f,g 都大于 2n,则优先函数不存在
逐次加一法需要从优先关系矩阵从上到下,从左到右一一进行
例 5.8 对上面的例子使用逐次加一法构造优先函数
解答
初始化 f(a)=g(a)=1,f(b)=g(b)=1,而后逐步分析
Step | f(a) | g(a) | f(b) | g(b) |
---|
a≐a | 1 | 1 | 1 | 1 |
a≐b | 1 | 1 | 1 | 1 |
b≐a | 1 | 1 | 1 | 1 |
b⋗b | 1 | 1 | 2 | 1 |
a≐a | 1 | 1 | 2 | 1 |
a≐b | 1 | 1 | 2 | 1 |
b≐a | 1 | 2 | 2 | 1 |
b⋗b | 1 | 2 | 2 | 1 |
a≐a | 2 | 2 | 2 | 1 |
a≐b | 2 | 2 | 2 | 2 |
b≐a | 2 | 2 | 2 | 2 |
b⋗b | 2 | 2 | 3 | 2 |
由此可见,在 b⋗b 发生了循环,经过若干步后一定会得到 f,g>2n,故优先函数不存在。
3. Martin 算法
Bell 有向图法本身不能发现是否存在优先函数,需要找到函数后再进行检验。Martin 算法也是基于有向图的,但算法本身能发现是否存在优先函数,不需要再检验。
- 对每个终结符 a1,a2,⋯,an 包含 #,上排画 n 个结点,标记为 f(ai),i=1,2,⋯,n;下排画 n 个结点,标记为 g(aj),j=1,2,⋯,n
- 对所有的 ⋖,⋗ 画有向边,即 ai⋖aj 则画 f(ai)←g(aj),ai⋗aj 则画 f(ai)→g(aj)
- 对 ≐ 反复画有向边。若 ai≐aj,则画 f(ai) 向 g(aj) 的所有直接后继结点,以及则画 g(ai) 向 f(aj) 的所有直接后继结点。反复执行,直至没有新的有向边添加到图中。
- 配数。若该有向图有环路,则不存在优先函数。否则,每个结点都配数 x,它等于从该结点出发可达的所有结点的数量(不包含自己)。
例 5.9 对上表使用 Martin 算法来求优先函数
解答
对于 b⋗b,画 f(b)→g(b)2. 由于 a≐b,从 g(a) 出发画到 f(b) 的直接后继结点 g(b)
3. 由于 a≐a,从 f(a) 出发画到 g(a) 的直接后继结点 g(b)
4. f(a) 的 a 与 g(b) 的 b 优先级相同,那么要从 g(b) 出发画到 f(a) 的直接后继 g(b) 此时生成了环
引入优先函数的利弊
- 利:优先关系的存储空间从 n×n 减小到 2n
- 弊: 原本在优先关系表中,不存在优先关系的终结符对 (a,b),现在通过优先函数,反而可以比较优先关系了。原本通过优先关系表能立即发现错误,现在多做了许多“移进-归约”动作,直到找出来的最左短语不能和任何规则的右部匹配时,才能出现报错,即推迟了错误的发现。