0%

编译技术-语法理论

一、直观理解

1.1 语法分析的目的

语法分析是在进行完词法分析后进行的步骤,词法分析会将一个个的字母拆解成不同的符号,这些符号会被组成一个线性的数组交给语法分析部分,语法分析不会会将这个线性的数组重新组织成一个语法树,交给后面的语义分析部分。

至于为什么要组织成一个树形结构?其实也并不是一个必选项,其实本质是这个线性数组可以被语法规则完全的接受。只不过是因为特定的语法规则刚好可以被组织成一个语法树的形式(语法树可以看做是语法分析的“历史记录”),而且语法树的结构又被后面的语义分析部分所需要,所以我们才恰好需要这棵语法树。

1.2 编译中的矛盾

1.2.1 推导和规约

推导(derivation)和规约(reducation)是针对语法规则进行的两个相反方向的过程,如下图所示

image-20230209160923676

如果结合语法树来看,推导是有一种让语法树“向下遍历”的趋势的(越来越具体),规约是有一种让语法树“向上遍历”的趋势的(越来越抽象)。两者的本质都是依赖于语法规则。

那么到底我们平时是用哪一种呢?这其实对应了 LL 和 LR 两种语法设计思路,在 LL 中采用的是推导的方式,而在 LR 中采用的是规约的方式。具体的对比需要之后说,这里只是一个铺垫。

1.2.2 规范规约

不可否认,无论是规约还是推导,都可以有多种思路的,先推左边的,后推右边的,想怎么推怎么推,想怎么规约就怎么规约。

在众多的方式中,有一种推导被我们称为了规范推导,也就是这样这样的

在这个推导中,我们进行了这样的 $U \rightarrow u$,这个的最关键的地方是,$y \in V_t^*$ 这个条件,这说明 $U$ 后面的东西都推导完了,才可以推导 $U$ 。更加直观的说,按照这个方法推导,一个句子的最右侧会最先诞生(也就是终结符会从最右侧诞生),然后直到整个句子全部变成终结符。

这是一个相当反直觉的事情,因为正常人都是从左往右去看一个字符串的,当你尝试推导的时候,一定是从左侧开始尝试,比如说 $FIRST$ 集就是一个看左侧是啥字符来判断推导哪个语法规则的辅助工具,没有人会从后面开始推。所以应该这样理解,这个东西本身就是不自然的,那么为什么一个不自然的东西会被称为“规范”呢?那是因为它是“规范规约”的逆过程,而规范规约才是真正自然的事情。

规范规约描述的是这样的过程,当我们进行规约的时候,优先规约当前规约的句柄,也就是优先规约最左简单短语。这就是一个很自然的事情,因为我们依然是从左向右读取字符串,所以读一点就规约一点,就是一个十分自然的事情。

再说得武断一些,“规范”概念的提出,就是为了服务“自底向上”的语法分析方法,也就是建立在规约基础上的分析方法。

1.2.3 句柄

可以首先解释一下对于“柄(handler)”的概念,这个东西如果直译是“把手”的意思,我对其的理解是,一个复杂的物体,我们没办法直面它,所以只能先用一个它的简单的“把柄”来操控它。这个理解相对于网上常见的“柄是一个东西的代表”增加了“复杂和简单”的概念,同时模糊了“柄和它所指代的物品之间的同质性”,是我比较满意的地方。

比如说在 C 语言中,有 File Handler 的东西,这是因为实际上我们并没有办法直接操作文件,所以我们只能借由一个文件句柄来操作它。又或者方向盘也可以看做是车的 handler,我们没有办法直接操控车的轮子和引擎,但是我们可以操纵车的方向盘,但是我们并不可以说方向盘是车的代表。

之所以要强调这个,是因为在语法分析中,句柄相对于句型或者句子(这取决于这个句型是否全是终结符),大致与上面探讨的关系类似,句柄可以“暗示”句型的语法树的部分特征,“指导”部分的规约的进行。

之所以有这个效果,是因为句柄的定义是“最左简单短语”,“简单”的深层次理解是这样的,它代表了一种“一个语法规则”的应用,因为显然一个语法规则对应着一个高度为 2 的语法子树,这和简单短语的定义有着一定的相似性,换句话说,简单短语限制了规约“必须是一步一步的进行”的这个特点,而“最左”则限制了规约发生的位置。

1.2.4 LL 和 LR

教材中这个部分的内容十分的分散,我将其整合起来,希望得到一定好的对比

entry LL LR
基础 推导(derivation) 规约(reducation)
产物 最左推导 最左规约(而不是所谓的“规范推导”)
方向 自顶向下 自底向上
树遍历 前序遍历 后序遍历
动作 前瞻(Prediction)、匹配(Match) 移进(Shift)、规约(Reducation)
前瞻目的 确定推导的方向 确定动作是移进还是规约
典型 递归下降法、LL(1) 算符优先法,SLR,LR(1),LALR
使用范围

关于为啥 LR 的使用范围比同级(也就是前瞻相同的符号个数)的 LL 要强,可能在于他们前瞻的目的是不同的,LL 依赖与前瞻去决定推导方向,这是重中之重,但是 LR 仅仅是用于解决动作的冲突。这可能是由于 LR 的前期预处理工作做得更好,对于语法的分析更加细致导致的。

1.3 多种概念的直观理解

每个句型对应一个语法树

短语的全称:

  • 属于某个句型
  • 相对于某个非终结符

当我们找短语的时候,首先先根据句型画出语法树,然后遍历语法树的子树(这个子树包括语法树本身,其实就是遍历非叶子节点)(不遍历叶子节点的原因是叶子节点没有推导过程,而定义中要求最少经过一步推导,所以叶子节点一定不是),非叶子节点的叶子子孙(也就是不必须是子代节点)组成了短语,这个非叶子节点就是“相对于某个非终结符”的那个“非终结符”。如果叶子节点都是这个节点的子代,那么就被称为简单短语。

不过我们一般不强调“相对于某个非终结符”这个属性,所以说到短语,直说他是某个句型的短语。

$E$

image-20221116104329690

它没有非叶子节点,所以就没有短语,所以啥都没有

$T$

image-20221116104454079

非叶子节点 短语 简单短语 句柄 素短语
E T T T

$i$

image-20221116105548980

非叶子节点 短语 简单短语 句柄 素短语
E i i
T i i
F i i i i

$T * F$

image-20221116110349286

非叶子节点 短语 简单短语 句柄 素短语
E T * F T * F
T T * F T * F T * F T * F

$F * F$

image-20221116110507990

非叶子节点 短语 简单短语 句柄 素短语
E F * F F * F
T1 F * F F * F
T2 F F F

$i * F$

image-20221116110634029

非叶子节点 短语 简单短语 句柄 素短语
E i * F
T1 i * F
T2 i i
F2 i i i i

$F * i$

image-20221116112037159

非叶子节点 短语 简单短语 句柄 素短语
E F * i
T1 F * i
T2 F F F
F1 i i i

$F + F + F$

image-20221116112811168

非叶子节点 短语 简单短语 句柄 素短语
E1 F + F + F
E2 F + F F + F
T1 F F
E3 F
T2 F F
T3 F F F

不得不说,这种列表法是有一定问题的,对于短语和简单短语,确实是可以每个非叶子节点都立即得出的,而对于句柄,其实是在得到所有的简单短语后,挑选最左面的简单短语获得的;而对于素短语,则是得到所有的短语后,从中挑出含有非终结符的最短短语。

1.4 语法范围

如图所示

有如下规律:

  • LR 一定包括同级的 LL
  • LR(1) 的功能最强,适应范围最广,但是实现的代价最大
  • LALR(1) 可用于大多数程序设计语言,用途最广
  • SLR 最容易实现,但是功能最弱

二、自顶向下方法

2.1 递归下降法

2.1.1 直观理解

递归下降法当然很好理解了,但是递归的简洁性掩盖了递归下降法和 LL(1) 的相似性,其实它俩基本上就是一个方法,递归下降法也可以用栈来实现,栈里的内容刚好是 LL(1) 中栈的内容,越靠近栈底,层次越高。

当然递归下降法还是和 LL(1) 有所区别的,因为 LL(1) 相当于仅仅是利用 $FIRST$ 集合,如果是 LL(2) 则是利用了 $FIRST, SECOND$ 集合,而递归下降在使用的时候则没有那么多的讲究,是一个相当工程化的方法。

斗胆猜测,LL 方法正式递归下降法的形式化表述。

2.1.2 消除左递归

首先是对于直接左递归的消除,核心在于这条规则,如果有左递归式呈现这种样式

那么消除左递归后为

这个转换分为两个部分 $(a \space|\space b \space|\space c \space|\space d \space|\space …)$ 和 $Au$ 。第一个只重复一遍,后面的重复 0 到多次。这个在教材中被称为“规则2”,那么规则 1 是什么?是为了应付这种情况

规则 1 为提取公因式,说的是这种现象

只要利用这个规则,就可以将上面的式子整理成了

然后利用规则 2,消除左递归

对于间接左递归,需要先对语法规则进行排序,然后依次进行消除左递归和带入。

排序的规则是后面规则的右部中包含前面规则左部的非终结符。其本质是让通过带入法,让间接左递归转变成直接左递归。这个规则的本质是一种“有序”的“一遍带入”。

以题为例:

需要注意,排序的时候只需要保证紧挨着的俩有上面的性质即可。所以非常容易,排序后

对于 $S ::= Qc \space | \space c$ 没有直接左递归,所以带入下一个式子,得到 $R : Qca \space|\space ca \space|\space a$

依然没有直接左递归,所以带入下一个式子,得到 $Q ::= Qcab \space|\space cab \space|\space ab \space | \space b$,消除左递归后得到 $Q ::= (cab \space|\space ab \space | \space b)\{cab\}$ 。

此外,如果是考虑压缩文法,其实把 $S$ 放在最下面比较好,因为 $S$ 是起始符号。之所以这样操作,是因为为了说明这种排序规则的结果并不具有唯一性,多种结果都满足这种排序规则,就好像在二元一次方程组中,用 $x$ 可以表示 $y$ ,也可以用 $y$ 表示 $x$。

2.2 LL(1) 分析法

2.2.1 FIRST 和 FOLLOW

正如前所述,LL 分析法需要利用前瞻(Prediction)去判断到底要匹配哪一条推导规则。这正是 $FIRST$ 和 $FOLLOW$ 的由来。$FIRST$ 很好理解,对于一个

我们确定使用这个规则去推导,是因为我们前瞻的结果刚好属于 $FIRST(\alpha)$ ,不然我们就不用这条规则去推导了,但是有一条规则是例外的,就是当

的时候,那么回到 $FIRST(\alpha)$ 为空,但是有的时候确实需要应用这条规则,其情况就是当遇到 $FOLLOW(\alpha)$ 中元素的时候,就需要应用这条 $A \rightarrow \varepsilon$ 的规则。

2.2.2 FIRST 的求解

$FIRST$ 的求解有如下规则:

  • 当 $A \rightarrow t\beta$ 其中 $t$ 是终结符的时候,可以推断出 $t \in FIRST(A)$ 。这个就是 $FIRST$ 最符合常理的定义,即第一个终结符。
  • 当 $A \rightarrow \varepsilon$ 时,可以推断出 $\varepsilon \in FIRST(A)$,这个其实并不会在填写 LL 表的时候用到,但是一但出现 $\varepsilon \in FIRST(A)$ ,这就会导致一些特殊的性质(在第三条)
  • 当 $A \rightarrow B_1B_2\dots B_n$,令 $i = 1$,进行如下循环:
    • 当 $\varepsilon \notin FIRST(B_i)$ 时,将 $FIRST(B_i)$ 加入 $FIRST(A)$ 中,终止循环。
    • 当 $\varepsilon \in FIRST(B_i)$ 时,将 $FIRST(B_i)$ 加入 $FIRST(A)$ 中,让 $i++$ 。

第三条规则演示了右部有非终结符的时候的做法,其实很容易理解,就是一个个的去看,如果存在 $\varepsilon$ 那么就需要再往后看一个非终结符(严谨的说是字符串)。

因为第三条规则的存在,导致一个非终结符的 $FIRST$ 是可能依赖于其他非终结符(要看具体的语法规则),而语法规则会导致一种图结构,没有办法只进行一遍遍历就完成所有的 $FIRST$ 的求解(起码是非常困难的),所以这是一个不动点算法

有文法 $G[S]$

最后的求解是

非终结符 $FIRST$
$S$ $a, \epsilon$
$A$ $\epsilon, a, d$
$B$ $a, \epsilon, e$
$C$ $a, \epsilon, g, f$

这里有两个在做题中容易出现的错误,一个是在

中,$\varepsilon \in FIRST(A)$ ,所以将 $FIRST(S)$ 加入 $FIRST(A)$ 中,但是却忘记考虑 $\varepsilon \in FIRST(S)$ 这件事情,导致没有将 $d$ 加入 $FIRST(A)$ 中,这是由于对于规则 3 的把握不熟练造成的。

另一个是对于这种

左递归形式,其实就是如果被递归的 $FIRST$ 集中没有 $\varepsilon$ ,那么就不用考虑了,如果有的话,那么就考虑下面的,没有什么过于稀奇的(依然是有一些稀奇的)。

2.2.3 FOLLOW 的求解

image-20221108190515286

非终结符 $FIRST$ $FOLLOW$
$S$ $a, \epsilon$ $d, a, f, #$
$A$ $a, d, \epsilon$ $a, e, b, d$
$B$ $a, e, \epsilon$ $b$
$C$ $a, \epsilon, g, f$ $g, b$

2.2.4 LL(1) 表的构建

LL(1) 表是一个二维表,这个表的每一列的列首都是一个终结符或者是 # ,每一行的行首都是一个非终结符,我们的目的是利用 first 和 follow 填写这个二维表。填写的方式如下:

遍历每一条语法规则 $A \rightarrow \alpha$,如果有:

  • 对 $FIRST(\alpha)$ 中的每一个终结符 $t$,置 $(A, t)$ 为 $A \rightarrow \alpha$ 。
  • 对 $\varepsilon \in FIRST(\alpha)$ ,则对 $FOLLOW(\alpha)$ 中的每个终结符 $t$ ,置 $(A, t)$ 为 $A \rightarrow \varepsilon$ 。

三、自底向上方法

3.1 算符优先分析法

3.1.1 基础知识

这里细致讨论一下算法优先算法的意义,还是以这个简单的表达式文法举例

对于 i + i i 这种东西,我们希望先算 i i (也就是先规约它),然后再规约 i * T 之类的。

那么最直观的办法就是加括号,只要将其变成 $((i) + ((i)*(i)))$ ,规约顺序就一目了然了。

所以为了构造这种括号,我们发明了算法优先分析法,通过终结符的优先级,构造出这种括号,就像这样

$< + <*>$ 其中的 $<, >$ 符号就是根据终结符的优先级表生成的。

为了构造这种优先级顺序,我们需要考察规约的优先级,而规约的优先级,本质上是语法导出的,而语法中优先级的判断,其实是判断语法产生式的某种“深度”,比如说 法优先级高于 + 法,是因为先推出 +,后推出

EqExp -> RelExp -> AddExp -> MulExp -> UnaryExp -> PrimaryExp

这就导致 ==, != 的优先级最低,而 !, - 的优先级最高。

然后介绍一下各种定义:

  • 算符文法(Operator Grammer,OG):文法中没有 $U::=…VW…$ 的规则,就被称为算符文法,这个的意思是说,非终结符间一定由终结符连接,就好像 $A + B * C$ 一样
  • 算符优先关系(Operator Priority):
    • a == b 当且仅当有 $U::=\dots ab \dots$ 或者 $U::= \dots aVb \dots$ 。这个很好理解,比如说 $A + B - C$ ,那么加减法就是相等的,$((A + B) * C)$ 两个 $($ 的优先级就是相等的。
    • a < b 当且仅当有 $U::= \dots aW \dots$ 且 $W \Rightarrow b \dots, W \Rightarrow Vb\dots $ 。这个同样好理解,说白了就是 $+A*B$ 或者 $+-A$ ,显然 $+$ 的优先级要低于乘法和负号。
    • a > b 当且仅当有 $U::=\dots Wb \dots$ 且 $W \Rightarrow \dots a, W \Rightarrow \dots aV$ 。和上面相同理解
    • 此外,最重要的一点,这里的比较关系并没有可逆性,也就是说,即使有 a < b 也不一定有 b > a 所以 ab 的关系需要考量两次。
  • 算符优先文法(Operator Priority Grammer,OPG),任意两个终结符之间,在 $=, <, >$ 间只有至多一种关系成立,则成为算法优先文法。说白了就是和表达式很像的文法,各个子部件的优先级是稳定的。

我们有了算符优先关系,就可以进行算符优先分析了。至于怎么构造,其实是一个很浅显的方法,首先构造两个集合:

  • FIRSTVT
  • LASTVT

定义繁多,就不敲了,这两个集合都是相对于某个非终结符而言的,说白了,都是这个非终结符中的运算符,比如说 Exp 中就有 $\{+, -, \times, \div, !\}$ ,而 item 里只有 $\{\times, \div, !\}$ 。有了这些,如果某个终结符和非终结符平级,那么就说明它的优先级是比里面的元素低的。

如果再说的详细一些,FIRSTVT 指的是当前非终结符可以推出的第一个终结符,比如说对于 AddExp,可以由 i + i 推出 +,也可以由 i i + 1 推出 ,还可以由 (i + i) 推出 (,而LASTVT 则是指由当前非终结符可以推出的最后一个非终结符,比如说由 i + i - i 推出 -,由 i - i / i 推出 /,这一类的操作。

至于算法,又是一个不动点问题,虽然用栈包装了一下,但是依然很难。可以考虑先把同级的写出来,然后在拓展(注意是逆向拓展),比如说对于

$AddExp -> MulExp -> PrimaryExp$

可以有

第一次 第二次
AddExp +, - +, -, *, /, !
MulExp *, / *, /, !
PrimaryExp ! !

最后总结一下

  • 首先求 FIRSTTK 和 LASTTK,求解的方法可以列表,因为要迭代,不要列表的话,可能不方便,注意这里要把所有的 FISRT 写在一起,LAST 写在一起
  • 然后根据语法规则开始填表(这个表是一个行首为终结符,列首也为终结符的二维表):
    • 首先扫描同级的,把 = 写好,注意,对于 $(Exp)$ 这种东西,只有 $( = )$ ,没有 $) = ($
    • 然后扫描发现这样 $+ E$ 这种结构,然后让 $+ < FIRSTTK(E)$,此时应该是在 + 这一行填写
    • 然后扫描发现这样 $E+$ 这种结构,然后让 $LASTRK(E) > +$ ,此时应该在 + 这一列填写
    • 对于 # 符号,可以将其视为最大的大括号,让 # < $FIRST(S)$ 和 $LAST(S)$ > # 即可。

3.1.2 例题

用算法优先分析法做题本身很简单,但是如果想要快速的做题,大概可以从两点优化:

  • 改变填写表格的顺序
  • 对于比较典型的表达式,不用查表即可给出优先关系

对于第一点,表头应当是“步骤,符号栈,输入字符串,优先关系,动作”。因为只有符号栈和输入字符串确定了,优先关系比较的是符号栈的栈顶和输入字符串的头部元素,当优先关系确定后,才可以确定具体要进行的动作,而当动作确定后,才可以确定下一行的符号栈和输入字符串。

对于第二点,其实只要牢记,当 > 出现的时候,就需要规约了,然后用意识辅助判断即可

以 $i * i + i$ 为例

步骤 符号栈 输入串 优先关系 动作
1 # i * i + i # # < i 移进
2 # i * i + i # i > * 规约
3 # E * i + i # # < * 移进
4 # E * i + i # * < i 移进
5 # E * i + i # i > + 规约
6 # E * E + i # * > + 规约
7 # E + i # # < + 移进
8 # E + i # + < i 移进
9 # E + i # i > # 规约
10 # E + E # + > # 规约
11 # E # 接受

3.1.3 素短语与规约

首先声明,素短语的概念和实际的解题并没有关系,素短语的提出是为了在理论上更好的解释算符优先分析法。

我们是这样描述的,在算符优先分析法中,每次规约的都是最左素短语

素短语简单短语有一定的相似性,比如说素短语强调了一定的最小性,素短语内部是不可以嵌套素短语的,这就使得素短语是一个“较小”的短语,同时素短语也要求必须含有至少一个终结符,这是其特性。

在与 LR 的对比中,可以发现,因为素短语并不一定是简单短语,所以规约是有可能是利用了多个语法规则的,比如说下面的语法树,按照算符优先分析,应当如此递归

image-20230210150129057

会发现在第二次递归的时候直接递归了两步,这是一个十分神奇而且我觉得其实是不严谨的地方。在某种意义上说,这意味着我们并没有按照语法规则去递归建树,我们将语法规则翻译成算符优先表之后,就彻底抛弃了原来的语法规则。

3.2 SLR

3.2.1 活前缀和下推自动机

活前缀指的是在一个句型中,不包括“句柄之后的符号”的其他部分组成的前缀。

那么活前缀代表着什么?LR 有两个基本动作“移进”和“规约”,如果一个活前缀不包括句柄,那么它就可以通过不断的移进操作增加它的长度,直到包括了句柄为止,而当一个活前缀包括句柄时,就可以通过规约操作改变活前缀,然后继续重复“移进-规约”的循环。这个过程似乎是可以用一个 DFA 来描述的,因为 DFA 会有一种记录自己状态的感觉,当前状态到起始状态的连线就是当前的活前缀。

然后我们来考虑一下正则语法是怎样和 DFA 联系在一起的呢,对于左线性正则来说,每一步都是规约,所以只需要一直规约到起始符即可,而对于右线性正则来说,每一步都是推导,所以只要一直移入到终止符即可。但是对于更加常见的二型文法来说,“移进-规约”是一个循环,也就是说,在发生移进的时候,可以进行类似于“一步一步走”的操作,但是在发生规约的时候,会导致之前的活前缀发生大量变化(不只是减少,还有改变),这就导致在状态图上可能需要后退,甚至跳转到某一个不太相关的状态。也就是说,DFA 那种“只记录当前状态“ 的思路是没有办法支持二型文法的。

为了解决这个问题,我们提出了下推自动机,相比于 DFA 只记录当前状态的特性,下推自动机拥有一个栈结构,在进行状态转移的同时,也会进行压栈或者弹栈操作,同时,栈顶的元素也会参与状态转移的决策。相比于 DFA,下推自动机拥有了记忆功能,所以功能更为强大。在 LR 分析中,栈中会有状态和符号,但是仔细观察就可以得知,符号是并不参与 LR 分析流程的,他们的存在只是为了让语法分析的结果更加的漂亮(将符号移到一个新开的栈中也是可以的),所以栈里最本质的东西就是状态,换句话说,LR 的下推自动机唯一强于 DFA 就是他用栈结构存储了状态,方便他进行“历史状态的撤销和修改”。

那么如果用下推自动机的角度考虑算符优先分析法,可以发现基本上一个算符就代表了一种状态,但是如果要具体解释,就显得有些牵强,所以并不在此胡诌了。

3.2.2 不同的 LR

​ 那时,我在想,为什么会发明这么多的分析算法? 我为什么不递归下降一把梭解决所有问题呢? 而当我学习了LR后,才发现这些算法其实是一脉相承,每一个都是针对上一个的痛点作出改进而诞生:

  • SLR 使用 follow 集来减少LR(0)的规约/移入冲突问题
  • LR 通过展望符(搜索符)预测来解决 Follow 集预测不准确的问题
  • 而 LR 又存在将状态划分得过细,导致状态数过多的问题,因此又诞生了 LALR,该算法剪除了 LR 中相同性质的状态,缩减了状态数,但是又带来了延迟报错的问题.……

现在我们尝试用一个例子来解释一下 LR 面对的问题(其本质是规约和规约的冲突,规约和移入的冲突)

img

构建这个图很简单,就是不断的移动那个点就好了,然后求出一个闭包即可。

我们来看一下一个移进过程具体是怎样的,对于 $id * id$ 这个输入来说,首先看到的是 $id$,那么移进就是在当前状态和输入考虑状态转移,所以我们考虑转移到 $I_5$ ,然后发现 $I_5$ 里的项目有一个特征

它的 . 是在最后的,这是要发生规约的标志,所以我们考虑发生规约,发生规约后我们可以利用栈结构重新回到 $I_0$,然后根据 $I_0$ 和规约出的 $F$,判断出要转到 $I_3$ ,$I_3$ 中也是发生规约,所以又转到 $I_2$,然后就发生了有趣的事情

这个里面有两个项目,那么我们是要按照第一个进行规约呢?还是要按照后一个进行移进呢?这就是一个冲突,SLR 第一次尝试回应这个冲突。

我们规定当前字符是是 $c$ ,然后考虑是否可以规约,比如说对于

一旦规约了,那么就意味着出现了 $Ec$ 这种结构,所以也就是说 $c \in FOLLOW(E)$ ,这正是判断能够进行规约的标准,如果 $c \notin FOLLOW(E)$ ,那么就是无法进行规约的。

对于移进,也是同理,如果想要转移,那么当前状态 $I_{cur}$ 必须有一条边通向 $I_{next}$ ,这条边恰好是 $c$ ,所以如果没有这条边的,那么就是不能发生移进的。

但是可以想见,这些要求会指导一定的动作冲突,但是还是有可能导致冲突,这也正式 SLR 无力的地方,在某种意义上,SLR 将“可规约”的情况预测多了,所以我们才有了 LR(1) 来减少这件事情。

3.2.3 例子

构建 LR(0) 项目集,构建 SLR 表

本来应该是两个事情,但是考虑到项目集状态图太大了,所以其实可以先用 GOTO 图去记录状态图的边,然后将 GOTO 图改造成 SLR 表,示例如下

构造出的 SR 分析表如图:

+ * $\uparrow$ i ( ) # S E T P F
0 S5 S6 1 2 3 4
1 S7 acc
2 r1 S8 r1 r1
3 r3 r3 r3 r3
4 r5 r5 S9 r5 r5
5 r7 r7 r7 r7 r7
6 S5 S6 10 2 3 4
7 S5 S6 11 3 4
8 S5 S6 12 4
9 S5 S6 13 4
10 S7 S14
11 r2 S8 r2 r2
12 r4 r4 r4 r4
13 r6 r6 r6 r6
14 r8 r8 r8 r8 r8

在构建的时候需要求出 follow 集判定是否可以进行递归,follow 集如下

符号 FOLLOW
S #
E +, ), #
T *, +, ), #
P *, +, ), #
F $\uparrow$ , *, +, ), #

SLR 使用

$i + i \uparrow (i * i)$

步骤 状态栈 输入串 动作
1 #0 i + i ^ (i * i)# S5
2 #0i5 + i ^ (i * i)# r7
3 #0F4 + i ^ (i * i)# r5
4 #0P3 + i ^ (i * i)# r3
5 #0T2 + i ^ (i * i)# r1
6 #0E1 + i ^ (i * i)# S7
7 #0E1+7 i ^ (i * i)# S5
8 #0E1+7i5 ^ (i * i)# r7
9 #0E1+7F4 ^ (i * i)# S9
10 #0E1+7F4^9 (i * i)# S6
11 #0E1+7F4^9(6 i * i)# S5
12 #0E1+7F4^9(6i5 * i)# r7
13 #0E1+7F4^9(6F4 * i)# r5
14 #0E1+7F4^9(6P3 * i)# r3
15 #0E1+7F4^9(6T2 * i)# S8
16 #0E1+7F4^9(6T2*8 i)# S5
17 #0E1+7F4^9(6T2*8i5 )# r7
18 #0E1+7F4^9(6T2*8F4 )# r5
19 #0E1+7F4^9(6T2*8P12 )# r4
20 #0E1+7F4^9(6T2 )# r1
21 #0E1+7F4^9(6E10 )# S14
22 #0E1+7F4^9(6E10)14 # r8
23 #0E1+7F4^9F4 # r5
24 #0E1+7F4^9P13 # r6
25 #0E1+7P3 # r3
26 #0E1+7T11 # r2
27 #0E1 # acc

$(i * i) \uparrow (i + i)$

步骤 状态栈 输入串 动作
#0 (i * i) ^ (i + i)# S6
#0(6 i * i) ^ (i + i)# S5
#0(6i5 * i) ^ (i + i)# r7
#0(6F4 * i) ^ (i + i)# r5
#0(6P3 * i) ^ (i + i)# r3
#0(6T2 * i) ^ (i + i)# S8
#0(6T2*8 i) ^ (i + i)# S5
#0(6T2*8i5 ) ^ (i + i)# r7
#0(6T2*8F4 ) ^ (i + i)# r5
#0(6T2*8P12 ) ^ (i + i)# r4
#0(6T2 ) ^ (i + i)# r1
#0(6E10 ) ^ (i + i)# S14
#0(6E10)14 ^ (i + i)# r8
#0F4 ^ (i + i)# S9
#0F4^9 (i + i)# S6
#0F4^9(6 i + i)# S5
#0F4^9(6i5 + i)# r7
#0F4^9(6F4 + i)# r5
#0F4^9(6P3 + i)# r3
#0F4^9(6T2 + i)# r1
#0F4^9(6E10 + i)# S7
#0F4^9(6E10+7 i)# S5
#0F4^9(6E10+7i5 )# r7
#0F4^9(6E10+7F4 )# r5
#0F4^9(6E10+7P3 )# r3
#0F4^9(6E10+7T11 )# r2
#0F4^9(6E10 )# S14
#0F4^9(6E10)14 # r8
#0F4^9F4 # r5
#0F4^9P13 # r6
#0P3 # r3
#0T2 # r1
#0E1 # acc