编译原理-算符优先分析法
花了不少时间终于把算符优先分析法大体上看明白了。写一篇文章来记录一下分析的过程。面向做题的,细节很全。
算符优先分析法是一种比较古老的自下而上的语法分析方法,很容易手动推导,但是会有一些问题,现在已经很少使用。现在主流的语法分析技术是 LR 分析法。
已知某个文法,进行算符优先分析需要几个步骤:
- 根据文法构建每个非终结符的 FIRSTVT 集合和 LASTVT 集合。
- 使用 FIRSTVT 和 LASTVT 推导算符的优先关系,构建算符优先表。
- (可选)根据算符优先表,构建优先函数,以替代算符优先表(适合程序实现)。
- 使用算符优先表或优先函数对给定的输入串执行算符优先分析。这一部分实际上是给出了针对输入串的自下而上的归约过程。
下面我们依次说明这几个步骤。
(另外,笔者使用的课本是《编译原理》陈火旺 第三版)
本文约定:
- \(G\) 用来表示一个文法
- 大写字母表示非终结符,如 A,B 等;
- 小写字母表示终结符,如 a,b 等;
- \(\cdots\) 表示由任意终结符和非终结符组成的序列。
基础知识
算符文法
如果一个文法的任意产生式右部都不含两个并列的非终结符,即不含如下的产生式右部:
那么我们称这个文法是一个算符文法。
终结符的优先关系
两个相继出现的终结符之间的优先关系有3种:
- a < b a 的优先级低于 b
- a = b a 的优先级等于 b
- a > b a 的优先级高于 b
(由于那种带点的符号暂时找不到 latex 公式,就用普通的大于、小于、等于号来代替。但一定要清楚:这种优先关系符号和普通的大于、小于、等于不是同一个东西)
注意:有 a < b 不代表就有 b > a,a = b 不代表就有 b = a
这里的优先关系是仅针对终结符而言的,非终结符没有优先关系。两个终结符之间可以夹着一个非终结符,也算相继出现(算符文法的定义限制了两个终结符之间最多夹着一个非终结符,不能更多)
算符优先文法
假定 \(G\) 是一个不含 \(\epsilon\)-产生式的算符文法。如果 \(G\) 中的任何终结符号对 \((a,b)\) 至多只满足下面三种关系之一:
那么就说 \(G\) 是一个算符优先文法。
构建 FIRSTVT 集合和 LASTVT 集合
这两个集合主要是用来得出后面的优先表的。
FIRSTVT 和 LASTVT 可以和 FIRST 和 FOLLOW 两个集合类比。它们都是非终结符专有的集合,终结符没有。
先来看它们的形式化定义,\(P\) 是任意非终结符。
有些抽象!下面直接给出求 FIRSTVT 和 LASTVT 的方法。假设我们的构造对象都是非终结符 P。
FIRSTVT
找到文法中以 P 为左部的产生式,看产生式的右部。
- 如果右部是以终结符开头的,就把这个终结符加入 FIRSTVT(P)。
- 如果右部是以非终结符开头的,那么这个非终结符之后跟的一定是一个终结符,否则文法就不是算符文法。不妨设为 \(Qa\cdots\),把终结符 \(a\) 加入 FIRSTVT(P),同时把 FRISTVT(Q) 中的所有元素也加入 FIRSTVT(P)。
第二条规则实际上是一个递归。也就是说,总会有一个非终结符,它的 FIRSTVT 集合中不含任何来自其他 FIRSTVT 集合的元素。我们最好是从这个非终结符开始求解,然后逐层向上。
LASTVT
和上面一样,找到文法中以 P 为左部的产生式,看产生式的右部。
- 如果右部是以终结符结尾的,就把这个终结符加入 LASTVT(P)。
- 如果右部是以非终结符结尾的,那么这个非终结符之前跟的一定是一个终结符,理由同上。不妨设为 \(\cdots aQ\),把终结符 \(a\) 加入 LASTVT(P),同时把 LASTVT(Q) 中的所有元素也加入 LASTVT(P)。
和前面一样,第二条规则实际上是递归。
步骤就是这些,和 FIRST、FOLLOW 相比还算是比较简单的。
课本上还介绍了一种程序化的方法,要用到布尔数组和栈,考虑到算符优先分析法在实际应用中已经基本被 LR 分析法淘汰,这里就不写程序化方法了,会手动推导就可以。
推导终结符之间的优先关系
逐条遍历文法 \(G\) 的产生式右部,结果不外乎以下4种情况:(记得算符文法要求非终结符不能连续出现)
- \(\cdots ab\cdots\) 的情况,这时令 \(a=b\)。
- \(\cdots aBc\cdots\) 的情况,这时令 \(a=c\)。
- \(\cdots aB\cdots\) 的情况,这时令 \(a\) < FIRSTVT(B)中的每个元素(产生若干个二元关系)
- \(\cdots Ab\cdots\) 的情况,这时令 LASTVT(A)中的每个元素 > \(b\)(同样产生若干个二元关系)
注意上面的几种情况不是互斥的,比如3和4实际上是2的一部分。
为了方便后面的分析,我们需要在文法中添加一个#号,并将其也看做是一个终结符(实际上不是)。在推导优先关系这一步,我们要额外对一个特定的式子 \(\#S\#\) 进行推导,其中 \(S\) 是文法的开始符号。这个式子可以对应到上面的234三种情况,都要进行推导。
如此操作下来,我们就得到了一堆终结符(含#在内)之间的二元关系。我们接下来要构建一张优先关系表。
构造优先关系表
有了前一步推导的二元关系,构造优先关系表是十分容易的,就是简单的填表过程。假设我们有 n 个终结符(含#在内),那么优先关系表就是 n*n 的。将这 n 个终结符依次填入表的首行和首列,作为表头。之后根据已经得到的关系依次填表。
做题时,为了和标准答案尽可能接近,最好按照这些终结符在文法中出现的先后顺序来填表头。先出现的先填,后出现的后填。# 一般在最后一个。
需要注意的一点是,对于一个关系对 \((a,b)\),其对应的二元关系应该填入表格的第 \(a\) 行、第 \(b\) 列。也就是先看行再看列。
有的格子里可能没有填入任何内容。这是正常现象。这表示两个终结符之间没有优先关系。
构造优先函数(可选)
优先函数是优先关系表的(近似)等价表示。优点在于方便比较、节省空间。缺点在于原表中不存在优先关系的终结符在优先函数中变得可以比较了,这可能会掩盖一些潜在的错误。
有优先关系表的文法不一定就有对应的优先函数,但一般都会有的。
如果一个文法有优先函数,那么它的优先函数一定有无穷多对。但我们只要找到一对就行了。
这里介绍2种由优先关系表构造优先函数的方法,一种是课本的有向图法,一种是方便程序化的迭代法。
(待补)
算符优先分析过程
这是最后一步了。我们已经有了对于文法 \(G\) 的优先关系表(或与其对应的优先函数),根据这个表(函数),对于一个特定的输入串,我们就可以执行自下而上的算符优先归约过程。
我们使用一个符号栈 \(S\),既用它寄存终结符,也用它寄存非终结符。
- 初始化:清空栈,填入#号。在输入串最右边也添加一个#号。
- 从输入串中读入下一个符号。输入串中一定都是终结符,所以读入的符号也一定是终结符。
- 查看栈顶。如果栈顶符号是非终结符,那么就跳过这个符号,再向下看。算符文法的性质规定了此种情况下,下面的符号一定是终结符。
- 比较栈顶符号(准确的说,是栈中最靠近栈顶的终结符,下同)和刚刚读入的符号的优先级。
- 如果栈顶符号的优先级 < 或 = 读入的符号,就执行移进操作,将读入的符号放进栈里。
- 如果栈顶符号的优先级 > 读入的符号,就进入一个循环。找一个临时的变量,用来寄存栈顶的符号(注意如果栈顶是非终结符,就跳过它,找下面的终结符),不妨设变量为Q。向栈的深处查找,每次深入一个符号,如果是非终结符,就跳过。找到第一个优先级 < Q 的符号,停止循环。循环中要同步地更新 Q,即向下深入之前要先将现在位置的符号保存到 Q 中。
- 找到一条合适的产生式,把从找到的优先级 < Q 的那个符号开始直到栈顶的符号串归约为该产生式的左部。注意最下面的符号(也就是最后找到的符号)不参与归约。无论栈顶是终结符还是非终结符,都归约栈顶。归约后的产生式左部留在栈中。
- 回到6开头的逻辑,如果栈顶符号和读入符号之间没有优先关系(这在优先关系表上表现为空的格子),那么出错。
- 重复 2-9 步,直到输入串只剩下一个#号。归约完毕。最后符号栈应该只剩下一个#号和文法的开始符号。
(上面的内容是我根据课本的伪代码翻译过来的,可能不太准确,之后我会把伪代码也一并贴上来)