前言
本文旨在用通俗的方式讲解如何手工构造LR(0)分析表,不涉及如何解决冲突问题
冲突问题解决见:SLR、LR(1) 分析
在阅读本文前你应该对 LR文法、移入/规约分析 有基本的了解。
1. 基本名词解释
1.1 项目
定义:在右部的某个地方加点的产生式。比如:
E -> A·bC
E -> Ab·C
... ...
这个概念的定义并不重要,因为你看了定义可能也不知道它的意义何在。那为什么要这么定义一个项目呢?为了符号化,看起来更直观。
比起你一大段一大段的文字,有时还不如简单的一个符号直观
因为在构造分析表前我们需要构造对应的状态自动机,那么如果我们能一眼看出某个项目对应什么状态那效率不就更高。举个例子:
E -> ·AbC
你一眼可以看出:当前刚刚开始分析这个文法,因为 · 在产生式的开始处。除此之外,你还能看出当遇到一个非终结符 A 时,这个项目将会进入下一个状态(项目):
E -> A·bC
换句话说:一个项目对应着LR分析过程中的某个状态。
根据点位置的不同,项目可以分为两大类:
- 待约项目:点不在末尾(等待规约)
E -> ·AbC
E -> A·bC
E -> Ab·C
- 规约项目:点在末尾
E -> AbC·
其中有两个特殊的项目:
- 初始项目:特殊的待约项目,点在开始符号右部产生式的开始(初始态,表示文法刚开始分析)
现有文法:
S -> E
E -> AbC
那么
S -> ·E
为初始项目,表示当前文法 S 开始分析;
而
E -> ·AbC
不能称为初始项目,只能称为待约项目。
因为 E 不是开始符号,此项目开始分析
时并不能表示 “文法 S 开始分析”
- 接收项目:特殊的规约项目,点在开始符号右部产生式的末尾(结束态,表示文法分析完毕)
现有文法:
S -> E
E -> AbC
那么
S -> E·
为接受项目,表示当前文法 S 分析完毕;
而
E -> AbC·
不能称为接受项目,只能称为规约项目。
因为 E 不是开始符号,此项目分析结束
后并不能表示 “文法 S 分析完毕”
1.2 项目集闭包
定义:某个项目与其所有等价项目的所构成的集合。显然:一个项目集闭包对应 LR自动机 的一个状态。
其求解方法为:
当 · 的后面的字符为
- 非终结符时,将后面的非终结符用对应的产生式进行展开,并且其点位于开始处
- 终结符时,结束。
【例】 现有文法:
S -> E
E -> Ab
A -> a
那么对于初始项目:
S -> ·E
由于 · 后的 E 是非终结符,那么用对应的产生式 “E -> Ab” 将其展开得:
E -> ·Ab
同理将 A 展开:
A -> ·b
由于 b 时终结符,结束。也就是说:
S -> ·E
E -> ·Ab
A -> ·a
为一个项目集闭包,他们之间都是等价项目(即对应同一状态)。
【附加】标识项目
该定义是为了后续表达的方便,我自己定义的
定义:用来唯一标识一个项目集闭包的项目。
因为在构造 LR 自动机的构建过程中很容易出现重复的情况。
那么用什么来标识呢?举个两个例子:
【例1】现有文法
S -> A
A -> a
它的初始项目集闭包为
(0) S -> ·A
(1) A -> ·a
那么用 (0) 来标识上述项目集闭包,因为 (1) 是由它推出来的。
【例2】现有文法
S -> A | B | c
A -> a
B -> b
初始项目集闭包为
(0) S -> ·A
(1) S -> ·B
(2) S -> ·c
(3) A -> ·a
(4) B -> ·b
那么它用 (0) (1) (2) 共同来标识,因为其他项目都是通过他们推导出来的,少一个就会不完整,多一个会有冗余。
可以认为:如果两个项目集闭包的标识项目一样,则认为这两个项目集闭包是一样的。
1.3 项目集规范族
定义:一个文法的所有项目集闭包所构成的集合。换句话说,项目集规范族是 一个LR自动机 的所有状态 的集合。
求解它无非就是求解多个项目集闭包,这并不难,难点在于容易漏掉。
一些教程采用先列出所有项目,在进行分类,我觉得这种方法比较麻烦,我采用另外一种方法:确定初始项目集闭包,依次推出其他所有项目集闭包
1.4 增广文法
给文法增加一个新的开始符号 S’ 替换掉源开始符号,并加入 S’ -> S。
为了书写方便,我用 T 作为 S’
为什么要这么做?这是为了确保只有一个接收项目集闭包。
如此可以提高分析效率
比如现有文法
S -> E | e
那么你会发现会有两个接收项目:
1. S -> E·
2. S -> e·
这就导致了有两个接收状态,因此引入增广文法,将上述文法改写为
T -> S
S -> E | e
那么现在的接收项目集闭包只有一个
T -> S·
下面以一个例题来讲解如何构造 LR 分析表
文法如下:
(0) S -> BB
(1) B -> aB
(2) B -> b
2. 构造LR自动机
2.1 增广文法
将上述文法改写为
(0) T -> S
(1) S -> BB
(2) B -> aB
(3) B -> b
2.2 构造 初始项目集闭包
首先,根据 (0) 产生式 (开始符号的产生式) 得出第一个初始项目
T -> ·S
根据此构造它的项目集闭包:
T -> ·S
S -> ·BB
B -> ·aB
B -> ·b
2.3 根据初始项目集闭包构建其余所有项目
算法:
- 找出每个项目集闭包的下一字符(也就是 · 之后的字符,无论是终结符还是非终结符)集合,看通过每一个字符后,该项目集闭包能到达哪一个项目集闭包,直到该项目集闭包中的所有下一字符都被遍历过;
- 如果下一字符集合为空,则跳过
- 重复上述直到所有项目集闭包都被遍历过。
为什么要记录 · 后的字符?先看一个定义:后继项目
B -> ·aB 的后继项目为 B -> a·B
直观地说,某个项目的后继项目就是
将该项目中的 · 后移一位得到的项目
那么我们记录一个项目集的所有下一字符,就能知道当此项目(状态)遇到某个字符时,它应该进入哪一个项目(状态)。
2.3.1 分析 A0
现在我们有初始项目集闭包,它对应初始状态,记作 A0(后续状态以此类推)
A0
(0) T -> ·S
(1) S -> ·BB
(2) B -> ·aB
(3) B -> ·b
则它的下一字符集为
{S, B, a, b}
-
通过字符 S
遍历 A0 中的所有项目,可以发现只有 A0(0) 满足
对应的后继项目为T -> S·
由于它为接收项目,没有等价项目,因此 A1 只有一个项目
A1 (0) T -> S·
因此
红色的项目表示它是此项目集闭包的标识项目
-
通过字符 B
只有 A0(1) 满足,对应的后继项目为S -> B·B
由于 · 之后是非终结符,因此有等价项目,所以下一状态 A2 为
A2 (0) S -> B·B (1) B -> ·aB (2) B -> ·b
故有
-
通过字符 a
只有 A0(2) 满足,它的后继项目为B -> a·B
同理得出下一状态 A3 为
A3 (0) B -> a·B (1) B -> ·aB (2) B -> ·b
故有
虽然 A2 与 A3 的 (1) (2) 一样,但是标识项目不同,所以 A2 与 A3 是不一样的状态
- 通过字符 b
只有 A0(3) 满足,对应的后继项目为
因此下一状态 A4 为B -> b·
故有A4 (0) B -> b·
现在 A0 的下一字符集被遍历完了,所以 A0 分析结束,进入 A1
2.3.2 分析 A1
A1
(0) T -> S·
显然它的下一字符集为空,故跳过
2.3.3 分析 A2
A2
(0) S -> B·B
(1) B -> ·aB
(2) B -> ·b
其下一字符集为
{B, a, b}
下面只针对有必要的地方给出解释,其余直接看图自行分析
- 通过 B
- 通过 a
只有 A1(1) 满足,其后继项目为
你会发现这与 A3 的标识项目一样,因此不产生新状态,此时进入的下一状态为 A3B -> a·B
- 通过 b
与 “通过 a” 同理,因此有
2.3.4 分析 A3
A3
(0) B -> a·B
(1) B -> ·aB
(2) B -> ·b
其中需要注意的是 通过字符 a 到达的是它自己 (A3)
剩下的状态的下一字符集都为空,故上图为最终结果
3. 构建 LR(0) 分析表
如何使用 LR(0) 分析表进行分析不在本文讨论范围,本文只讨论如何构建。
在此之前,你应知道如何使用 LR(0) 分析表进行语法分析。
LR(0)分析表分为两部分:
- action 表:针对终结符
- goto 表:针对非终结符
其中在 action 表中,有以下元素
- sn:表示 移入当前终结符,并进入n状态
比如 s1 表示移入当前终结符,进入 1 状态
- rn:表示 用 文法的 (n) 式来进行当前项目的规约
比如 r0 表示用 (0) 来进行规约
- acc:表示 接收成功 (accept)
- 空:表示 分析失败
在 goto 表中,它表示当某状态遇到非终结符时应进入哪一状态,因此它的元素只记录状态编号,空的地方表示分析失败
那么现在根据之前的分析开始构建分析表
- 文法
(0) T -> S (1) S -> BB (2) B -> aB (3) B -> b
- 自动机
3.1 构建 goto 表
goto表只与 自动机中的输入字符中的非终结符 (也就是箭头上的非终结符) 有关
{ B, S }
据自动机分析可知:
A0 --S--> A1
表示 A0 遇见 S 进入 A1
其余类推...
A0 --B--> A2
A2 --B--> A5
A3 --B--> A6
并且共有7个状态,因此有
在表中,状态我只用数字表示(“A0” 用 “0” 表示)
state | B | S |
---|---|---|
0 | 2 | 1 |
1 | ||
2 | 5 | |
3 | 6 | |
4 | ||
5 | ||
6 |
3.2 构建 action 表
action 表只与 自动机中的输入字符中的终结符 (也就是箭头上的终结符)有关
{ a, b, $ }
- 加入 ‘$’ 字符
- $ 表示结束字符,也就是说遇见 ‘$’ 说明当前输入中没有字符了
前面我们知道项目分两大类为:待约项目、规约项目,在 action 中满足:
-
对于待约项目使用移入动作
在之前的自动机中,待约项目有A0、A2、A3
其中有
A0 --a--> A3
所以对应的 action 表为
state a b $ 0 s3 表示 A0 遇见字符 a 时,将字符 a 移入栈内,并进入 A3 状态;遇见其他字符说明分析失败。
其余以此类推… … -
对于规约项目使用规约动作
一般规约项目,对于任何终结符都是采用对应的产生式进行规约,对于 接受项目,只有遇到 $ 时表示 acc,其余都表示分析失败。
在之前的自动机中,待约项目有A1、A4、A5、A6
其中你会发现待约项目没有出边(没有出去的箭头,只有指向它自己的箭头)
当然有时有的项目中既有待约项目也有规约项目,这就属于冲突,不在本文讨论范围
- 对于 A1
A1 (0) T -> S·
由于它是接受项目,因此
state a b $ 1 acc 表示 A1 遇见 $ 时分析成功,遇见其他字符都分析失败。
- 对于 A4
A4 (0) B -> b·
在之前的文法中
(0) T -> S (1) S -> BB (2) B -> aB (3) B -> b
A4 (B->b) 是 文法 (3) 式, 由于它是待约项目,因此
state a b $ 4 r3 r3 r3 表示 A4 遇见 a、b、$ 时用 文法的 (3) 产生式来进行规约。
其余以此类推… …
最后的 action 表为
state | a | b | $ |
---|---|---|---|
0 | s3 | s4 | |
1 | acc | ||
2 | s3 | s4 | |
3 | s3 | s4 | |
4 | r3 | r3 | r3 |
5 | r1 | r1 | r1 |
6 | r2 | r2 | r2 |
最后
如果你发现本文出现任何错误,欢迎指正。