编译器常用的语法分析方法有自上而下和自下而上两种。正如它们的名字所示,自上而下分析器按从根结点到叶结点的次序来建立分析树,而自下而上分析器恰好相反。它们的共同点是从左向右地扫描输入,每次一个符号。
最有效的自上而下和自下而上的分析法都只能处理上下文无关文法的子类。这些子类足以描述编程语言的大多数构造和它们的语法特征,其中L文法的分析器通常用手工实现,而LR文法的分析器通常利用自动工具构造。
很多较复杂的语言不能使用正规式表达,所以需要定义描述功能比正规式更强的上下文无关的文法。
终结符: 即记号名。
非终结符: 非终结符用来帮助定义由文法决定的语言,一个非终结符定义终结符串的一个集合。非终结符还在语言中强加了层次结构,这种层次结构对语法分析和翻译都是有用的。
推导: 从开始符号开始,通过使用产生式的右部取代左部,最终能产生语言的一个句子的过程。其中, → \rightarrow → 表示单步推导, ⇒ ∗ \Rightarrow^* ⇒∗ 表示经过任意步推导, ⇒ + \Rightarrow^+ ⇒+ 表示经过至少一步推导。
文法表示的规定:
(1)下列符号是终结符:
- 字母表前面的小写字母,如 a a a, b b b, c c c;
- 黑体串,如 id \textbf{id} id 或 while \textbf{while} while;
- 数字 0 0 0, . . . ... ..., 9 9 9;
- 标点符号,如分号、逗号等;
- 运算符号,如 + + +、 − - − 等。
(2)下列符号是非终结符:
- 字母表前面的大写字母,如 A A A, B B B, C C C;
- 字母 S S S,并且它通常代表开始符号;
- 小写字母组成的名字,如 e x p r expr expr 和 s t m t stmt stmt。
(3)字母表后面的大写字母,如 X X X, Y Y Y 和 Z Z Z,代表文法符号,即非终结符或终结符。
(4)字母表后面的小写字母,主要是 u u u, v v v, . . . ... ..., z z z,代表终结符号串。
(5)小写希腊字母,例如 α \alpha α, β \beta β 和 γ \gamma γ,代表文法的符号串。
(6) A → α 1 A\rightarrow \alpha_1 A→α1, A → α 2 A\rightarrow \alpha_2 A→α2, . . . ... ..., A → α k A\rightarrow \alpha_k A→αk ⇔ \Leftrightarrow ⇔ A → α 1 ∣ α 2 ∣ . . . ∣ α k A\rightarrow \alpha_1\space|\space \alpha_2 \space | \space ...\space |\space \alpha_k A→α1 ∣ α2 ∣ ... ∣ αk,称这些产生式的右部 α 1 \alpha_1 α1、 α 2 \alpha_2 α2、 . . . ... ...、 α k \alpha_k αk 是 A A A 的选择。
文法的句型: 如果 S ⇒ ∗ α S\Rightarrow^*\alpha S⇒∗α, α \alpha α 可能含有非终结符或终结符,则把 α \alpha α 称为文法的句型。
文法的句子: 只含终结符的句型。
最左推导: 从开始符号进行推导,每一步都是替换句型中最左边非终结符的推导,这样的推导称最左推导。用符号 ⇒ l m \Rightarrow_{lm} ⇒lm 表示。
最右推导: 类似地定义,从开始符号进行推导,每一步都是替换句型中最右边非终结符的推导,这样的推导称最左推导。用符号 ⇒ r m \Rightarrow_{rm} ⇒rm 表示。最右推导又称为规范推导,经过规范推导得到的句型称为规范句型(又是也称为右句型)。
分析树是推导的图形表示。分析树上的每个分支结点都由非终结符标记,它的子结点由该非终结符本次推导所用产生式的右部的各符号从左到右依次来标记。分析树的叶结点由非终结符或终结符标记,所有这些标记从左到右构成一个句型。
以算术表达式文法 E → E + E ∣ E ∗ E ∣ ( E ) ∣ − E ∣ id E\rightarrow E+E\space|\space E*E\space|\space(E)\space|-E\space|\space \textbf{id} E→E+E ∣ E∗E ∣ (E) ∣−E ∣ id 为例,表达式 − ( id + id ) -(\textbf{id}+\textbf{id}) −(id+id) 最左推导的分析树(包括推导过程中的分析树)如下图所示。

显然,该表达式的最左推导和最右推导的最终分析树是一样的。
有些文法的一些句子存在不止一棵分析树,或者说这些句子存在不止一种最左(最右)推导,那么称该文法是二义的。也可以这么说,二义文法是至少存在一个句子有不止一个最左(最右)推导的文法。
以上面的算术表达式文法为例,对于句子 id ∗ id + id \textbf{id}*\textbf{id}+\textbf{id} id∗id+id 存在两种不同的最左推导过程如下。

因而有两棵不同的分析树。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xubbVsph-1656145792541)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220614162759947.png)]](https://1000bd.com/contentImg/2022/06/27/204108358.png)
上图右边的分析树反映了 + + + 和 ∗ * ∗ 通常的优先关系,而左边的分析树则不是。也就是,习惯上 ∗ * ∗ 的优先级高于 + + +,因而表达式 a ∗ b + c a * b+c a∗b+c 看成 ( a ∗ b ) + c ( a* b)+c (a∗b)+c,而不是 a ∗ ( b + c ) a * (b+c) a∗(b+c)。
有些类型的分析器,它希望处理的文法是无二义的,否则它不能唯一确定对某个句子应选择哪棵分析树。出于某些需要,也可以构造允许二义文法的分析器,不过这样的文法要附带消除二义性的规则,以便分析器扔掉不希望的分析树,为每个句子只留一棵分析树。
注意,文法二义并不代表语言一定是二义的。只有当产生一个语言的所有文法都是二义时,这个语言才称为二义的。
这部分用于补充理解,可以跳过。只要知道可以通过重写文法来消除二义性即可。
一个句子的不同分析树体现了不同的算符优先关系和算符结合性。下面构造非二义的有 + + + 和 ∗ * ∗ 运算的表达式文法,该文法和通常的算符优先关系和算符结合性相对应。
设置两个非终结符
e
x
p
r
expr
expr 和
t
e
r
m
term
term(
e
x
p
r
expr
expr 是开始符号),用以表示不同层次的表达式和子表达式,再用非终结符
f
a
c
t
o
r
factor
factor 来产生表达式的基本单位。基本单位有
id
\textbf{id}
id 和外加括号的表达式,即
f
a
c
t
o
r
→
id
∣
(
e
x
p
r
)
factor\rightarrow \textbf{id} \space|\space (expr)
factor→id ∣ (expr)
然后考虑二元算符
∗
*
∗ ,它有较高的优先级,又是左结合的算符,因而产生式如下:
t
e
r
m
→
t
e
r
m
∗
f
a
c
t
o
r
∣
f
a
c
t
o
r
term\rightarrow term\space *\space factor\space |\space factor
term→term ∗ factor ∣ factor
同样地,
e
x
p
r
expr
expr 产生由加法算符隔开的、左结合的
t
e
r
m
term
term 表,其产生式如下:
e
x
p
r
→
e
x
p
r
+
t
e
r
m
∣
t
e
r
m
expr\rightarrow expr\space + \space term \space | \space term
expr→expr + term ∣ term
这个表达式文法是无二义的。句子
id
∗
id
∗
id
\textbf{id} * \textbf{id} * \textbf{id}
id∗id∗id 和
id
+
id
∗
id
\textbf{id} + \textbf{id} * \textbf{id}
id+id∗id 的分析树如下图所示。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o6oeBDGB-1656145792541)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220614164320196.png)]](https://1000bd.com/contentImg/2022/06/27/204108676.png)
上面两棵分析树所表现出的算符优先关系和结合性与通常的规定是一致的。可以看出,如果语言语义所规定的算符优先关系和结合性不是这样的话,则文法可能需要重新设计,否则所得到的分析树就不能很方便地用于语义分析和中间代码生成等阶段。例如,如果规定
∗
*
∗ 和
+
+
+ 是右结合的运算,那么文法应该如下。
e
x
p
r
→
t
e
r
m
+
e
x
p
r
∣
t
e
r
m
t
e
r
m
→
f
a
c
t
o
r
∗
t
e
r
m
∣
f
a
c
t
o
r
f
a
c
t
o
r
→
id
∣
(
e
x
p
r
)
expr\rightarrow term\space + \space expr \space | \space term \\ term\rightarrow factor\space *\space term\space |\space factor \\ factor\rightarrow \textbf{id} \space|\space (expr)
expr→term + expr ∣ termterm→factor ∗ term ∣ factorfactor→id ∣ (expr)
对应的最左推导和最右推导的分析树如下。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xNwwIEzu-1656145792541)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220614164558753.png)]](https://1000bd.com/contentImg/2022/06/27/204108876.png)
只有在多个同优先级的算符参与的计算中,结合性才起作用。
比如:
+ + + 具有左结合性,所以在计算 a + b + c a+b+c a+b+c 时需要先计算 a + b a+b a+b,再计算 ( a + b ) + c (a+b)+c (a+b)+c;
= = = 具有右结合性,所以在进行连续赋值时, a = b = 2 a=b=2 a=b=2需要先对 b = 2 b=2 b=2 进行赋值,再 a = ( b = 2 ) a=(b=2) a=(b=2)。
大多数编程语言都不用无二义的文法,而采用二义的文法。这是因为,非二义的文法失去了简洁性。定义语言语法的文法有二义性并不可怕,只要有消除二义性的规则就可以了。
※ 分析树与推导过程的区别与联系:(理解)
由于分析树中每个非终结符使用产生式进行扩展的先后顺序并不唯一(分析树无法明确地体现每个非终结符扩展的顺序,即分析树忽略了不同的推导次序),所以一棵分析树可以对应多个推导过程(又可称为方式),对应的推导过程可以是最左推导、最右推导,又或者是其他推导方式)。但是,一棵采用已知文法建立的分析树,所表示的句子的最左推导和最右推导的过程是唯一的。但是,反过来,文法所能表示的一个句子对应的通过最左或最右推导建立的分析树却不一定是唯一的(即不止一种最左和最右推导方式),也就是文法是二义的。另外,如果文法是无二义性的,那么任意一种推导方式(不局限于最左和最右推导)都仅产生唯一的一棵分析树,此时,最左和最右推导对应的分析树相同。
※ 将语法分析器和词法分析器分离的理由:
从软件工程的角度来看,将其分离有如下好处:
※ 自上而下语法分析与 LL(1) 文法的关系:
当且仅当一个文法是LL(1)文法,才能对文法的句子进行确定的自上而下的语法分析。
※ 自上而下语法分析与预测分析(器)的关系:
自上而下语法分析是一种抽象的思想,即按从根结点到叶结点的次序来建立分析树,以实现对语法句子的分析。预测分析则是这种思想的具体实现方案,比如采用什么样的数据结构和算法等。根据采用算法(是否采用递归的方法)的不同,又可以将预测分析分成”采用递归下降技术的预测分析“和“采用非递归算法的预测分析表驱动的预测分析”。分析器是预测分析更具体的体现,即一段实现预测分析的程序。
先在前面说清楚,之后看完详细知识点后可以再回头看看。
很多资料上都并没有给出这些内容之间的关系,导致大家感觉虽然每一部分都学的差不多了,但是整体上却无法理解。
一个文法是左递归的,如果它有非终结符 A A A,对某个串 α α α,存在推导 A ⇒ ∗ A α A\Rightarrow^*Aα A⇒∗Aα。自上而下的分析方法不能用于左递归文法,因此需要消除左递归。由形式为 A → A α A→A\alpha A→Aα 的产生式引起的左递归称为直接左递归。经过两步或者多步推导形成的左递归称为间接左递归。
消除直接左递归
左递归产生式
A
→
A
+
T
∣
T
A\rightarrow A+T\space|\space T
A→A+T ∣ T,可以产生语言
L
(
T
(
+
T
)
∗
)
L(\space T(+T)^*\space)
L( T(+T)∗ )。引入新的非终结符
A
′
A'
A′。
A
→
T
A
′
A
′
→
+
T
A
′
∣
ε
A\rightarrow TA' \\ A'\rightarrow +TA'\space|\space \varepsilon\\
A→TA′A′→+TA′ ∣ ε
这个两个产生式所表达同样的语言。
不管有多少
A
A
A 产生式,都可以用下面的技术消除直接左递归。首先把A产生式组合在一起:
A
→
A
α
1
∣
A
α
2
.
.
.
∣
A
α
m
∣
β
1
∣
β
2
∣
.
.
.
∣
β
n
A\rightarrow A\alpha_1\space | \space A\alpha_2 \space ...\space |\space A\alpha_m \space |\space \beta_1\space | \space \beta_2\space | \space ...\space | \space \beta_n
A→Aα1 ∣ Aα2 ... ∣ Aαm ∣ β1 ∣ β2 ∣ ... ∣ βn
其中
β
i
\beta_i
βi (可以为
ε
\varepsilon
ε)都不以
A
A
A 开始,
α
i
α_i
αi 都非空,然后用
A
→
β
1
A
′
∣
β
2
A
′
∣
.
.
.
∣
β
n
A
′
A
′
→
α
1
A
′
∣
α
2
A
′
∣
.
.
.
∣
α
m
A
′
∣
ε
A\rightarrow \beta_1 A'\space | \space \beta_2A'\space | \space ... \space |\space \beta_n A' \\ A'\rightarrow \alpha_1A'\space | \space \alpha_2 A' \space | \space ... \space | \space \alpha_mA'\space | \space \varepsilon \\
A→β1A′ ∣ β2A′ ∣ ... ∣ βnA′A′→α1A′ ∣ α2A′ ∣ ... ∣ αmA′ ∣ ε
代替
A
A
A 产生式。这些产生式和前面的产生式产生一样的串集,但是不再有左递归。
消除间接左递归
例如,考虑文法
S
→
A
a
∣
b
A
→
S
d
∣
ε
S→Aa \space|\space b\\ A→ Sd \space | \space \varepsilon
S→Aa ∣ bA→Sd ∣ ε
其中非终结符
S
S
S 是左递归的,因为
S
⇒
A
a
⇒
S
d
a
S\Rightarrow Aa \Rightarrow Sda
S⇒Aa⇒Sda,但它不是直接左递归的。用
S
S
S 产生式代换
A
→
S
d
A→Sd
A→Sd 中的
S
S
S,可以得到下面的文法:
S
→
A
a
∣
b
A
→
A
a
d
∣
b
d
∣
ε
S \rightarrow Aa \space|\space b \\ A →Aad \space |\space bd \space |\space \varepsilon
S→Aa ∣ bA→Aad ∣ bd ∣ ε
删除其中的直接左递归,得到如下的文法:
S
→
A
a
∣
b
A
→
b
d
A
′
∣
A
′
A
′
→
a
d
A
′
∣
ε
S→ Aa \space|\space b\\ A \rightarrow bdA' \space|\space A' \\ A'\rightarrow adA' \space |\space \varepsilon
S→Aa ∣ bA→bdA′ ∣ A′A′→adA′ ∣ ε
感觉嵌套层次加深或者产生式数量比较多时,很难手工处理。
提左因子也是一种文法变换,它用于产生适合于自上而下分析的文法。在自上而下的分析中,当不清楚应该用非终结符 A A A 的哪个选择来替换它时,可以通过重写 A A A 产生式来推迟这种决定,推迟到看见足够多的输入,能帮助正确决定所需选择为止。
例如,条件语句有两个产生式:
s
t
m
t
→
if
e
x
p
r
then
s
t
m
t
else
s
t
m
t
∣
if
e
x
p
r
then
s
t
m
t
stmt\rightarrow \textbf{if} \space\space expr\space \space \textbf{then} \space\space stmt \space\space \textbf{else} \space\space stmt \\ | \space \space \textbf{if} \space\space expr\space \space \textbf{then} \space\space stmt
stmt→if expr then stmt else stmt∣ if expr then stmt
当看见输入记号
if
\textbf{if}
if 时,不能马上确定用哪个产生式来扩展
s
t
m
t
stmt
stmt。
一般来说,如果
A
→
a
β
1
∣
α
β
2
A →a\beta_1\space|\space \alpha\beta_2
A→aβ1 ∣ αβ2 是
A
A
A 的两个产生式,输入串的前缀是从
α
α
α 推导出的非空串时,则不知道是用
a
β
1
aβ_1
aβ1 还是用
α
β
2
α\beta_2
αβ2 来扩展
A
A
A。但是可以通过先扩展
A
A
A 到
a
A
′
aA'
aA′ 来推迟这个决定。然后,看完了从
α
α
α 推出的输入后,再扩展
A
′
A'
A′ 到
β
1
β_1
β1 或
β
2
\beta_2
β2。这就是提左因子,原来的产生式成为:
A
→
α
A
′
A
′
→
β
1
∣
β
2
A →α A'\\ A'→\beta_1\space| \spaceβ_2
A→αA′A′→β1 ∣ β2
自上而下分析的宗旨是,对任何输入串,试图用一切可能的办法,从文法开始符号(根结点)出发,自上而下,从左到右地为输入串建立分析树。或者说,为输入串寻找最左推导。这种分析过程本质上是一种试探过程,是反复使用不同的产生式谋求匹配输人串的过程。
自上而下分析的举例。若有文法
S
→
a
C
b
C
→
c
d
∣
c
S\rightarrow aCb\\ C\rightarrow cd\space | \space c \\
S→aCbC→cd ∣ c
为了自上而下地为输入串
w
=
a
c
b
w = acb
w=acb 建立分析树,首先建立只有标记为
S
S
S 的单个结点树,输入指针指向
w
w
w 第一个符号
a
a
a。然后用
S
S
S 的第一个产生式来扩展该树,得到的树如图所示。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Un5xjUnJ-1656145792542)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220614185647521.png)]](https://1000bd.com/contentImg/2022/06/27/204109038.png)
树中最左边的叶子标记为 a a a,匹配 w w w的第一个符号。于是,推进输入指针到 w w w 的第二个符号 c c c,并考虑分析树中下一个叶子 C C C,它是非终结符。用 C C C 的第一个选择来扩展 C C C,得到下图所示的树。现在第二个输入符号 c c c 能匹配,再推进输入指针到 b b b,把它和分析树中的下一个叶子 d d d 比较。因为 b b b 和 d d d 不匹配,回到 C C C,看它是否还有别的选择尚未尝试。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-30c2vV8n-1656145792542)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220614190002055.png)]](https://1000bd.com/contentImg/2022/06/27/204109217.png)
在回到 C C C 时,必须让输入指针重新指向第二个符号,和第一次进入 C C C 时的位置一致。现在尝试 C C C 的第二个选择,得到下图所示的分析树。叶子 c c c 匹配 w w w 的第二个符号,叶子 b b b 匹配 w w w 的第三个符号。这就得到 w w w 的分析树,表明分析完全成功。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bpWUr6Vo-1656145792543)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220614190012822.png)]](https://1000bd.com/contentImg/2022/06/27/204109388.png)
自上而下分析面临的问题
左递归
如果存在 A ⇒ + A a A\Rightarrow^+Aa A⇒+Aa 这样的左递归,当试图用 A A A 去匹配输入串时有可能使分析过程陷人无限循环(若输人串不是一个句子,则一定陷人无限循环),因为可能在没有吃进任何输入符号的情况下,又得要求用下一个A去进行新的匹配。因此,使用自上而下分析法时,文法应该没有左递归。
使用产生式 A → A + T ∣ T A\rightarrow A+T\space|\space T A→A+T ∣ T 去匹配 A A A,如果选择 A + T A+T A+T,则得到 A ⇒ A + T A\Rightarrow A+T A⇒A+T,再次选择 A + T A+T A+T 得到 A + T + T A+T+T A+T+T,如此下去,最终会得到 A + T + . . . + T A+T+...+T A+T+...+T,显然陷入了无限循环。因此,使用自上而下分析法必须消除文法的左递归性。
回溯
当非终结符用某个选择匹配成功时,这种成功可能仅是暂时的。由于这种虚假现象,需要使用复杂的回溯技术。试探与回溯是一种穷尽一切可能的办法,效率低、代价高,它只有理论意义,在实践中价值不大。因此解决回溯问题同样是自上而下分析的关键。
我当时疑惑提左因子的意义,向老师询问后得知,左因子和左递归一样都存在直接的和间接的。直接的左因子和左递归我们一般能轻易地处理掉,但是对于一些复杂的间接左因子和左递归,我们往往难以发现并且难以处理。如果将全部的左因子都提出来,并且将全部的左递归都消除,那么就可以保证不存在回溯,这就是我们提左因子和消除左递归的意义。可是,往往我们并不能发现复杂的左因子和左递归的情况,所以上面处理左因子和左递归的方法比较局限。
LL(1)的第一个“L”代表从左向右地扫描输入,第二个“L”表示产生最左推导,“1”代表在决定分析器的每步动作时需要向前查看下一个输入符号(即输人指针所指向的符号)。有时省略“(1)”,LL文法也同样表示的是LL(1)文法。
LL(1)文法可以使用自上而下的分析,且保证无回溯。为了构造LL(1)文法,首先要消除文法的左递归,这部分上面我们已经讲解了消除左递归的方法。下面讨论如何避免回溯。
对文法的任何非终结符,当要用它去匹配输入串时,如果能够根据所面临的输入符号准确地指派它的一个选择去执行任务,那么就肯定能消除回溯。这个“准确”是指:若此选择匹配成功,那么这种匹配绝不是虚假的;若此选择无法完成匹配任务,则任何其他的选择也肯定无法完成。
F I R S T FIRST FIRST 集
一个文法的符号串
α
α
α 的开始符号集合
F
I
R
S
T
(
α
)
FIRST(α)
FIRST(α) 是
F
I
R
S
T
(
α
)
=
{
a
∣
α
⇒
∗
a
.
.
.
,
a
是
终
结
符
}
FIRST(\alpha)=\{a\space|\space \alpha\Rightarrow^*a\space...,\space a是终结符\}
FIRST(α)={a ∣ α⇒∗a ..., a是终结符}
特别是,
α
⇒
∗
ε
\alpha\Rightarrow^*\varepsilon
α⇒∗ε 时,规定
ε
∈
F
I
R
S
T
(
α
)
\varepsilon ∈ FIRST ( α)
ε∈FIRST(α)。
构造 F I R S T FIRST FIRST 集的算法
不断应用下列规则,直到没有新的终结符或 ε \varepsilon ε 可以被加入到任何 F I R S T FIRST FIRST 集合中为止。
F O L L O W FOLLOW FOLLOW 集
一个文法的非终结符
A
A
A 的后继符号集合
F
O
L
L
O
W
(
A
)
FOLLOW(A)
FOLLOW(A) 是可能在某个句型中紧跟在
A
A
A 后边的终结符
a
a
a 的集合
F
O
L
L
O
W
(
A
)
=
{
a
∣
S
⇒
∗
.
.
.
A
a
.
.
.
,
a
为
终
结
符
}
FOLLOW(A)=\{a\space|\space S\Rightarrow^* ...Aa...\space,a为终结符\}
FOLLOW(A)={a ∣ S⇒∗...Aa... ,a为终结符}
如果
A
A
A 是某个句型的最右符号,则将结束符 “$” 添加到
F
O
L
L
O
W
(
A
)
FOLLOW(A)
FOLLOW(A) 中。
句型的最右符号:存在 S ⇒ ∗ . . . A S\Rightarrow^*...A S⇒∗...A,则 A A A 为该句型的最右符号。特别地,开始符号 S S S 也是一个句型,所以 S S S 也是句型的最右符号。
我们手工构建 F O L L O W FOLLOW FOLLOW 集时,最初一般只向开始符号 S S S 的 F O L L O W FOLLOW FOLLOW 集中添加 “$”,因为根据构造算法, "$"最终会加到其他句型的最右符号的 F O L L O W FOLLOW FOLLOW 集中。
构造 F O L L O W FOLLOW FOLLOW 集的算法
不断应用下列规则,直到没有新的终结符可以被加入到任何 F O L L O W FOLLOW FOLLOW 集合中为止。
注意:
- F I R S T FIRST FIRST 集中可能存在 ε \varepsilon ε,但是 F O L L O W FOLLOW FOLLOW 集中不可能存在 ε \varepsilon ε。
- F I R S T FIRST FIRST 集和 F O L L O W FOLLOW FOLLOW 集是针对非终结符而言的,而 S E L E C T SELECT SELECT 集是针对产生式而言的。
具体例题讲解建议观看MOOC哈工大编译原理视频 4-4 FIRST集和FOLLOW集的计算
* S E L E C T SELECT SELECT 集
产生式 A → α A\rightarrow \alpha A→α 的可选集合 S E L E C T ( A → α ) SELECT(A\rightarrow \alpha) SELECT(A→α) ,由终结符构成。非终结符 A A A 遇到输入为 S E L E C T ( A → α ) SELECT(A\rightarrow \alpha) SELECT(A→α) 中的终结符时,可以使用产生式 A → α A\rightarrow \alpha A→α 进行推导,即用 α \alpha α 替换 A A A。
* 构造 S E L E C T SELECT SELECT 集的算法
LL(1)文法具有的性质
文法为LL(1)文法的充分必要条件的形式化表示
文法 G G G 是 LL(1) 的,当且仅当 G G G 的任意两个具有相同左部的产生式 A → α ∣ β A\rightarrow \alpha \space| \space \beta A→α ∣ β 满足下面的条件:
不存在终结符 a a a 使得 α \alpha α 和 β \beta β 能够推导出以 a a a 开头的串
α \alpha α 和 β \beta β 至多存在一个能推导出 ε \varepsilon ε
如果 β ⇒ ∗ ε \beta\Rightarrow ^*\varepsilon β⇒∗ε ,则 F I R S T ( α ) ∩ F O L L O W ( A ) = ∅ FIRST(\alpha)\space ∩\space FOLLOW(A)=\varnothing FIRST(α) ∩ FOLLOW(A)=∅
如果 α ⇒ ∗ ε \alpha\Rightarrow ^*\varepsilon α⇒∗ε ,则 F I R S T ( β ) ∩ F O L L O W ( A ) = ∅ FIRST(\beta)\space ∩\space FOLLOW(A)=\varnothing FIRST(β) ∩ FOLLOW(A)=∅
以上三个条件就是为了保证同一个非终结符的各个产生式的可选集互不相交。
预测分析法的工作过程:
从文法开始符号除法,在每一步推导过程中根据当前句型的最左非终结符 A A A 和当前输入符号 a a a,选择正确的 A − A- A− 产生式。为保证分析的正确性,选出的候选式必须是唯一的。LL(1)文法是满足这个要求的。
递归下降的预测分析是指为每一个非终结符写一个分析过程(函数),由于文法的定义是递归的,因此这些过程也是递归的。在处理输人串时,首先执行的是开始符号所对应的过程,然后根据产生式右部出现的非终结符,依次调用相应的过程,这种逐步下降的过程调用序列隐含地建立了输入的分析树。
文法
t
y
p
e
→
s
i
m
p
l
e
∣
↑
id
∣
array
[
s
i
m
p
l
e
]
of
t
y
p
e
s
i
m
p
l
e
→
integer
∣
char
∣
num
dotdot
num
\left.
显然,该文法是LL(1)的。
下图是上面类型定义文法的递归下降预测分析器。这个分析器包括处理非终结符
t
y
p
e
type
type 和
s
i
m
p
l
e
simple
simple 的过程以及附加的过程 match()。使用 match() 是为了简化 type() 和 simple() 的代码,如果它的参数匹配当前面临的符号,它就调用函数 nextToken(),取下一个记号,并更新变量 lookahead 的值。

预测分析器
非递归的预测分析不需要为每个非终结符编写递归下降过程,而是根据预测分析表构造一个自动机,也叫表驱动的预测分析。
表驱动的预测分析器(下推自动机)有一个输入缓冲区、一个栈(下推存储器)、一张分析表和一个输出流,如下图所示。

输入缓冲区包含被分析的串,后面跟一个符号
构建预测分析表
对文法的每个产生式 A → α A\rightarrow \alpha A→α ,执行下面的操作:
预测分析表 M M M 剩下的条目没有定义,都是出错体条目,通常用空白表示。
考虑文法
E
→
T
E
′
E
′
→
+
T
E
′
∣
ε
T
→
F
T
′
T
′
→
∗
F
T
′
∣
ε
F
→
(
E
)
∣
id
E\rightarrow TE'\\ E'\rightarrow +TE'\space |\space \varepsilon\\ T\rightarrow FT' \\ T'\rightarrow*FT'\space | \space \varepsilon\\ F\rightarrow (E) \space |\space \textbf{id}\\
E→TE′E′→+TE′ ∣ εT→FT′T′→∗FT′ ∣ εF→(E) ∣ id
可求得非终结符的
F
I
R
S
T
FIRST
FIRST 集和
F
O
L
L
O
W
FOLLOW
FOLLOW 如下。
F I R S T ( E ) = F I R S T ( T ) = F I R S T ( F ) = { ( , id } F I R S T ( E ′ ) = { + , ε } F I R S T ( T ′ ) = { ∗ , ε } F O L L O W ( E ) = F O L L O W ( E ′ ) = { ) , $ } F O L L O W ( T ) = F O L L O W ( T ′ ) = { + , ) , $ } F O L L O W ( F ) = { + , ∗ , ) , $ } FIRST(E)=FIRST(T)=FIRST(F)=\{\space(,\textbf{id}\space\}\\ FIRST(E')=\{\space +,\varepsilon\space\}\\\\ FIRST(T') = \{\space*,\varepsilon\space\}\\ FOLLOW(E)=FOLLOW(E')=\{\space),\$\}\\ FOLLOW(T)=FOLLOW(T')=\{\space +, ) , \$ \space\}\\ FOLLOW(F)=\{\space +,*,),\$ \space\} FIRST(E)=FIRST(T)=FIRST(F)={ (,id }FIRST(E′)={ +,ε }FIRST(T′)={ ∗,ε }FOLLOW(E)=FOLLOW(E′)={ ),$}FOLLOW(T)=FOLLOW(T′)={ +,),$ }FOLLOW(F)={ +,∗,),$ }
构建该文法预测分析表的部分部分过程:
因为 F I R S T ( T E ′ ) = F I R S T ( T ) = { ( , id } FIRST(TE')=FIRST(T)=\{\space(,\textbf{id}\space\} FIRST(TE′)=FIRST(T)={ (,id },因此产生式 E → T E ′ E\rightarrow TE' E→TE′ 加入 M [ E , ( ] M[E, (] M[E,(] 和 M [ E , id ] M[E, \textbf{id}] M[E,id]。
产生式 E ′ → + T E ′ E'\rightarrow +TE' E′→+TE′ 加入 M [ E ′ , + ] M[E',+] M[E′,+] 是显然的。因为 ε ∈ F I R S T ( E ′ ) \varepsilon\in FIRST(E') ε∈FIRST(E′) ,并且 F O L L O W ( E ′ ) = { ) , $ } FOLLOW(E')=\{\space ), \$\space\} FOLLOW(E′)={ ),$ } ,因此产生式 E ′ → ε E'\rightarrow \varepsilon E′→ε 加入 M [ E ′ , ) ] M[E',)] M[E′,)] 和 $M[E’, $]$。
最终得到的预测分析表如下。

上述算法可以用于任何文法 G G G 来产生分析表 M M M。然而对某些文法, M M M 可能含有一些多重定义的条目。例如 G G G 是左递归或二义的,则 M M M 至少含一个多重定义的条目。
一个文法的预测分析表没有多重定义的条目,当且仅当该文法是LL(1)的。
上述算法为LL(1)文法 G G G 产生的分析表能分析 L ( G ) L(G) L(G) 的所有句子,也仅能分析 L ( G ) L(G) L(G) 的句子。
预测分析算法
输入:串 w w w 和文法 G G G 的分析表 M M M。
输出:如果 w w w 属于 L ( G ) L(G) L(G),则输出 w w w 的最左推导,否则报告错误。
方法:
初始时分析器的格局是:$$\space\space S$ 在栈里,其中 S S S 是开始符号并且在栈顶;$w\space\space$$ 在输入缓冲区中,下图是用预测分析表 M M M 对输入串进行分析的程序。

举例:如果输入是 i d ∗ i d + i d id * id+id id∗id+id,分析过程中各部分的变化见下图。输人指针指在输入串最左边的符号。仔细观察分析器的输出动作可知,分析器跟踪的是输入的最左推导,也就是输出最左推导的那些产生式。已匹配的输入符号加上栈中的文法符号(从顶到底),构成最左推导的句型。

预测分析发实现的完整步骤
移进-归约分析是自下而上分析的一般风格,编译器常用的移进-归约分析方法称为LR分析(包括LR(0)分析、SLR(1)分析、LALR(1)分析、LR(1)分析等),下面将讨论移进-归约分析。
自下向上的语法分析是指从分析树的底部(叶节点)向顶部(根节点)方向构造分析树。可以看成是将输入串 w w w 归约为文法开始符号 S S S 的过程。在每一步归约中,一个子串和某个产生式的右部匹配,然后用该产生式的左部符号代替这个子串。如果每步都能恰当地选择子串,那么它实际跟踪的是最右推导过程的逆过程。
自上而下的语法分析采用最左推导方式。
自下向上的语法分析采用最左归约方式(反向构造最右推导)(最右推导又称规范推导)
引入几个概念以方便后续的讲解。
考虑文法:
E → E + E E → E ∗ E E → − E E → ( E ) E → i d \left.\right. E→E+EE→E∗EE→−EE→(E)E→id
描述的句型 − ( E + E ) -(E+E) −(E+E) 的分析树如下:
分析树的边缘: 分析树的叶节点既可以是非终结符,也可以是终结符。从左到右排列叶节点得到的符号串称为这棵树的边缘。可见,分析树的边缘是文法的一个句型。对于上面的分析树而言,分析树的边缘就是其对应的句型 − ( E + E ) -(E+E) −(E+E) 。
(句型的)短语: 给定一个句型,其分析树中的每一棵子树的边缘称为该句型的一个短语。对于上面的分析树而言,其深度为2的子树对应的短语有 E + E E+E E+E ,深度为3的子树对应的短语有 ( E + E ) (E+E) (E+E),深度为4的子树就是整棵分析树。特别地,如果子树只有父子两代节点(即深度为2),那么这棵子树的边缘称为该句型的直接短语。对于该树而言,直接短语就是 E + E E+E E+E 。
不难总结出,直接短语一定是某个产生式的右部,但是产生式的右部不一定是直接短语。
句柄: 句型的最左直接短语,即在句型中起始位置最靠左的直接短语。
非形式定义,句柄是和某个产生式右部匹配的字符串,把句柄归约成产生式左部的非终结符,可以得到最右推导的逆过程的一步;
形式化定义,右句型(最右推导可得到的句型) γ \gamma γ 的句柄是一个产生式的右部 β β β 以及 γ \gamma γ 中的一个位置,在这个位置可找到串 β β β,用 A A A 代替 β β β (有产生式 A → β A→β A→β)得到最右推导的前一个右句型。即,如果 S ⇒ r m ∗ α A w ⇒ r m α β w S \Rightarrow^*_{rm} \alpha Aw \Rightarrow_{rm} \alpha\beta w S⇒rm∗αAw⇒rmαβw,那么在 α α α 后的 β β β 是 α β w α\beta w αβw 的句柄。句柄右边的 w w w 仅含终结符。注意,如果文法二义,那么句柄可能不唯一,因为一个句子可能不止一个最右推导。只有文法无二义时,它的每个右句型才有唯一的句柄。
**活前缀:**前缀是指一个句型的从头开始连续的若干个文法符号构成的串。而文法符号栈(下面会讲到)中保留的总是一个右句型的前缀,称为活前缀。
句柄举例:
E − > E + E ∣ E ∗ E ∣ − E ∣ ( E ) ∣ id E->E+E|E*E|-E|(E)|\textbf{id} E−>E+E∣E∗E∣−E∣(E)∣id,对于 id + id ∗ id \textbf{id}+\textbf{id}*\textbf{id} id+id∗id,其中一个最右推导为 E → E + E → E + E ∗ E → E + E ∗ id → E + i d ∗ id → id + id ∗ id E\rightarrow E+E\rightarrow E+E*E\rightarrow E+E*\textbf{id}\rightarrow E+{id}*\textbf{id}\rightarrow \textbf{id}+\textbf{id}*\textbf{id} E→E+E→E+E∗E→E+E∗id→E+id∗id→id+id∗id。在 id + id ∗ id \textbf{id}+\textbf{id}*\textbf{id} id+id∗id 归约成 E + i d ∗ i d E+id*id E+id∗id 的过程中,最左边的 i d id id 是句柄。 E + id ∗ id E+\textbf{id}*\textbf{id} E+id∗id 归约成 E + E ∗ id E+E*\textbf{id} E+E∗id 时,最左边的 id \textbf{id} id 是句柄,把 E + E ∗ id E+E*\textbf{id} E+E∗id 归约成 E + E ∗ E E+E*E E+E∗E 时, id \textbf{id} id 是句柄。把 E + E ∗ E E+E*E E+E∗E 归约成 E + E E+E E+E 时 E ∗ E E*E E∗E 是句柄。 E + E E+E E+E 归约成 E E E 时, E + E E+E E+E 是句柄。
LR(k)分析技术是一种高效的、自下而上的语法分析技术,它能适用于一大类上下文无关文法的分析。L是指从左向右扫描输入,R是指构造最右推导的逆,k是指在决定分析动作时向前查看的符号个数。(k)省略时,表示k是1。
下面将详细讲解四种构造LR分析表的技术。第一种方法称为LR(0)方法,最容易实现,但功能最弱,实用性不高。第二种方法称为简单的LR方法(简称SLR),它比LR(0)难实现一点,功能也强一点。对某些文法,用另外两种方法能成功地产生分析表,但用前两种却失败。第三种方法称为规范的LR方法,它功能最强,但代价也最大,我们一般只考虑LR(1)。第四种方法称为向前搜索的LR方法(简称LALR),它的功能和代价处于SLR和规范的LR之间。LALR 方法可用于大多数编程语言的文法,可以高效地实现。
LR分析器的模型见下图,它包括输入、输出、栈(我习惯理解为两个栈,一个文法符号栈,一个状态栈,但实际实现中只存在状态栈)、驱动程序和含动作和转移两部分的分析表。驱动程序对所有的LR分析方法都一样,用不同的分析方法构造的分析表不同。驱动程序每次从输入缓冲区读一个符号,它使用状态栈存储形式为 s 0 s 1 … s m s_0s_1…s_m s0s1…sm 的串, s m s_m sm 在栈顶,文法符号栈存储形式为 X 0 X 1 . . . X m X_0X_1...X_m X0X1...Xm, X m X_m Xm 在栈顶。 X i X_i Xi 是文法符号, s i s_i si 是叫做状态的符号,状态符号概括了栈中它下面部分所含的信息。栈顶的状态符号和当前的输入符号用来检索分析表,以决定移进-归约分析的动作。

四种分析方法对应的分析表都包括两个“子表”, a c t i o n action action 表和 g o t o goto goto 表。
两个表都是能够由行标和列标唯一确定一个表中条目,且二者的行标都是状态(项目集)编号,两个表的不同之处在于 a c t i o n action action 表的列标为终结符和 $$$,而 g o t o goto goto 标的列标为非终结符。
对于 a c t i o n action action 表中的条目,总是以 s ? s? s?、 r ? r? r? 、 a c c acc acc 或空白的形式填写。其中 s s s 是 S h i f t Shift Shift 的简写,表示“移进”,其后面的 ? ? ? 表示要移进的状态(项目集)编号; r r r 是 R e d u c e Reduce Reduce 的简写,表示“归约”,其后面的 ? ? ? 表示归约采用的产生式的编号; a c c acc acc 是 a c c e p t accept accept 的简写,表示到达接受状态,程序结束。
对于 g o t o goto goto 表中的条目,总是以数字或者空白的形式填写,数字表示的是状态(项目集)编号。
**四种分析方法的驱动程序(LR分析算法)**是相同的,如下:
输入:输入串 w w w 和文法 G G G 的 L R LR LR 分析表。
输出:若 w ∈ L ( G ) w\in L( G) w∈L(G),则输出 w w w 自下而上分析的归约步骤,否则报错。
方法:分析器初始情况是:初始状态 s 0 s_0 s0 在分析器的栈顶,$w\space$$ 在输人缓冲区。然后分析器执行下图的程序。

既然已经认识了移进-归约分析的栈,那么就来详细说说栈中的句柄和活前缀。
如果文法二义,那么句柄可能不唯一,因为一个句子可能不止一个最右推导。只有文法无二义时,它的每个右句型才有唯一的句柄。
文法符号栈中句柄总是位于栈顶。
文法符号栈中的文法符号和输入缓冲区中的剩余符号构成一个右句型。
文法符号栈中的串就是一个活前缀,但是栈中的句柄不一定是从栈底的符号开始的。
文法
G
G
G 的LR(0)项目(简称项目)是在其右部的的某个地方加点的产生式。如从产生式
A
→
X
Y
Z
A→XYZ
A→XYZ 可得到如下四个项目:
A
→
⋅
X
Y
Z
A
→
X
⋅
Y
Z
A
→
X
Y
⋅
Z
A
→
X
Y
Z
⋅
A \rightarrow ·XYZ\\ A \rightarrow X·YZ\\ A \rightarrow XY·Z\\ A \rightarrow XYZ·\\
A→⋅XYZA→X⋅YZA→XY⋅ZA→XYZ⋅
特别地,对于产生式
A
→
E
A→E
A→E,只能得到一个项目
A
→
⋅
A→ ·
A→⋅。
直观地讲,项目表示在分析过程的某一点,已经看见了产生式的多大部分(点的左边部分)和下面期望看见的部分(点的右边部分)。例如, A → ⋅ X Y Z A →·XYZ A→⋅XYZ 表示期望下一步从输人中看见由 X Y Z XYZ XYZ 推出的串, A → X ⋅ Y Z A →X·YZ A→X⋅YZ 表示刚从输人中看见了由 X X X 推出的串,下面期望看见由 Y Z YZ YZ 推出的串。也可以这么说,点的左边代表历史信息,而右边代表展望信息。
例如: S → b B B S\rightarrow bBB S→bBB
S → ⋅ b B B S\rightarrow ·bBB S→⋅bBB 为移进项目;
S → b ⋅ B B S\rightarrow b·BB S→b⋅BB 和 S → b B ⋅ B S\rightarrow bB·B S→bB⋅B 为待约项目;
S → b B B ⋅ S\rightarrow bBB· S→bBB⋅ 为归约项目
增广文法(又称拓广文法): 如果 G G G 是一个以 S S S 为开始符号的文法,则 G G G 的增广文法 G ′ G' G′ 就是在 G G G 中加上新开始符号 S ′ S' S′ 和产生式 S ′ → S S'→S S′→S 而得到的文法。引入这个新的开始产生式的目的是使得文法开始符号仅出现在一个产生式的左边,从而使得分析器只有一个接受状态。
对于LR分析表,我们一般先建立自动机,再根据自动机构建分析表。
考虑文法
S
→
B
B
B
→
a
B
B
→
b
S\rightarrow BB \\ B\rightarrow aB \\ B\rightarrow b \\
S→BBB→aBB→b
首先得到其增广文法:
(
0
)
S
′
→
S
(
1
)
S
→
B
B
(
2
)
B
→
a
B
(
3
)
B
→
b
\left.
文法中的项目:
( 0 ) S ′ → ⋅ S (0)\space\space S'\rightarrow ·S (0) S′→⋅S、 ( 1 ) S ′ → S ⋅ (1)\space\space S'\rightarrow S· (1) S′→S⋅
( 2 ) S → ⋅ B B (2)\space\space S\rightarrow ·BB (2) S→⋅BB、 ( 3 ) S → B ⋅ B (3)\space\space S\rightarrow B·B (3) S→B⋅B、 ( 4 ) S → B B ⋅ (4)\space\space S\rightarrow BB· (4) S→BB⋅
( 5 ) B → ⋅ a B (5)\space\space B\rightarrow ·aB (5) B→⋅aB、 ( 6 ) B → a ⋅ B (6)\space\space B\rightarrow a·B (6) B→a⋅B、 ( 7 ) B → a B ⋅ (7)\space\space B\rightarrow aB· (7) B→aB⋅
( 8 ) B → ⋅ b (8)\space\space B\rightarrow ·b (8) B→⋅b、 ( 9 ) B → b ⋅ (9)\space\space B\rightarrow b· (9) B→b⋅
其中, ( 1 ) (1) (1) 是 ( 0 ) (0) (0) 的后继项目, ( 3 ) (3) (3) 是 ( 2 ) (2) (2) 的后继项目, ( 4 ) (4) (4) 是 ( 3 ) (3) (3) 的后继项目,……。
点位于产生式右部开头位置的项目(除 S ′ → S S'\rightarrow S S′→S 外)称为非核心项目,一般这样的项目都是通过等价关系得到的;而点不位于产生式右部的开头或者是 S ′ → S S'\rightarrow S S′→S 项目称为核心项目,一般这样的项目用于产生闭包中的其他项目。
存在某些项目是“等价”的。(类似于NFA转化为DFA中的“等价”思想)
对于 ( 0 ) (0) (0) 而言,点后面是非终结符 S S S,所以此时在等待 S S S;再看 ( 2 ) (2) (2),等待 S S S 就相当于在等待 B B BB BB,因为此时如果点在产生式左部的 S S S 前面,那么就可能在 B B BB BB 前面。因此, ( 0 ) (0) (0) 和 ( 2 ) (2) (2) 是等价的。
可以把所有等价的项目组成一个项目集( I I I),称为项目集闭包,每个项目集闭包对应着自动机的一个状态。
注意:只有当点位于项目中非终结符前面时,该项目才存在等价项目。
这和“NFA转化为DFA”也太像了吧!
构建状态转换图:
先把初始项目放入项目集 I 0 I_0 I0 中

再为其构建项目集闭包。观察 S ′ → ⋅ S S'\rightarrow ·S S′→⋅S,点位于 S S S 前,那么点也就可以在 ( 1 ) (1) (1) 产生式右部的最前面。同理 ( 1 ) (1) (1) 产生式右部的 B B B 前面有点,那么点也可以在 ( 2 ) (2) (2) 和 ( 3 ) (3) (3) 产生式的最前面,所以就得到了 I 0 I_0 I0 中的四个项目。

生成 I 0 I_0 I0 的后继项目集。在 I 0 I_0 I0 中,点现在位于 S S S、 B B B、 a a a 和 b b b 前面,因此如果 I 0 I_0 I0 遇到这四个文法符号,则进入其他项目集。
S ′ → ⋅ S S'\rightarrow ·S S′→⋅S 的后继项目为 S ′ → S ⋅ S'\rightarrow S· S′→S⋅,即当遇到 S S S 后得到项目 S ′ → S ⋅ S'\rightarrow S· S′→S⋅,我们将这个项目加入到新的项目集 I 1 I_1 I1 中。由于 S ′ → S ⋅ S'\rightarrow S· S′→S⋅ 的点后面没有非终结符,所以也就没有等价项目,因此该项目自己形成闭包,构成项目集 I 1 I_1 I1。
类似地, S → ⋅ B B S\rightarrow ·BB S→⋅BB 遇到 B B B 得到后继项目 S → B ⋅ B S\rightarrow B·B S→B⋅B,该项目存在等价项目,所以形成闭包应该还包括 B → ⋅ a B B\rightarrow ·aB B→⋅aB 和 B → ⋅ b B\rightarrow ·b B→⋅b 。
遇到剩下两个终结符构造新项目集的方法一样。

最终可以得到完整的状态转换图。

转换图中边上的终结符表示边起点对应的项目集(状态)遇到该终结符时要将边终点对应的项目集(状态)的编号压入状态栈中,因此需要将 a c t i o n [ 边 起 点 对 应 的 项 目 集 编 号 , 终 结 符 ] action[边起点对应的项目集编号,终结符] action[边起点对应的项目集编号,终结符] 条目填上 边 终 点 对 应 的 项 目 集 的 编 号 边终点对应的项目集的编号 边终点对应的项目集的编号,表示如果前一个状态遇到该终结符需要进行移进操作到达后有个状态。
转换图中边上的非终结符用于填写 g o t o goto goto 表,即 g o t o [ 边 起 点 对 应 的 项 目 集 编 号 , 非 终 结 符 ] = 边 终 点 对 应 的 项 目 集 的 编 号 goto[边起点对应的项目集编号,非终结符] = 边终点对应的项目集的编号 goto[边起点对应的项目集编号,非终结符]=边终点对应的项目集的编号。
对于那些存在归约项目的项目集,LR(0)分析表要求,填写这些状态对应的 a c t i o n action action 表中每一个终结符和 $ 对应条目为 r 归 约 项 目 对 应 的 产 生 式 编 号 r归约项目对应的产生式编号 r归约项目对应的产生式编号 。
特别地,要将填写 a c t i o n [ 包 含 S ′ → S ⋅ 的 项 目 集 的 编 号 , $ ] action[包含 S'\rightarrow S· 的项目集的编号,\$] action[包含S′→S⋅的项目集的编号,$] 为 a c c acc acc,表示到达接受状态。
之所以标红,是因为这四种建立转换表的方法的唯一区别就在于填写归约条目。(严谨地说,规范的LR(1)和LALR(1)其实是相同的,区别在于别处)
最终构造的分析表如下。

SLR方法的主要思想是首先从文法构造识别文法活前缀的 DFA。
提出SLR是为了解决LR(0)分析表中存在冲突的问题,即一个条目可能既包括归约又包括移进。但是,SLR也只能解决一部分分析表中出现移进-归约的问题。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tYTJO1pY-1656172667606)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220615212611631.png)]](https://1000bd.com/contentImg/2022/06/27/204112368.png)
对于上图的状态转换建立LR(0)分析表如下:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eaAZ7GCJ-1656172667606)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220615212715781.png)]](https://1000bd.com/contentImg/2022/06/27/204112633.png)
很显然转换图和分析表中的红色部分表示存在移进-归约冲突。
考虑 I 2 I_2 I2,事实上 E E E 的 F O L L O W FOLLOW FOLLOW 集中并没有 ∗ * ∗ ,这说明就算我们使用了 E → T E\rightarrow T E→T 进行归约,也不可能出现后面紧接着 ∗ * ∗ 的情况。因此如果遇到的输入是 ∗ * ∗,很显然不能进行归约,而应该进行移入,这是LR(0)分析没有考虑到的。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nHQSRANk-1656172667606)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220615213036247.png)]](https://1000bd.com/contentImg/2022/06/27/204112821.png)
SLR分析法的基本思想:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eZGkBeBI-1656172667607)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220615213338040.png)]](https://1000bd.com/contentImg/2022/06/27/204113087.png)
保证满足上面的基本思想后建立SLR分析表:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mx7dZBEe-1656172667607)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220615214114929.png)]](https://1000bd.com/contentImg/2022/06/27/204113337.png)
上面我们见到了移进-归约冲突。下面来讲解一下归约归约冲突。
考虑文法
( 0 ) S ′ → T ( 1 ) T → a B d ( 2 ) T → ε ( 3 ) B → T b ( 4 ) B → ε \left.\right. (0) S′→T(1) T→aBd(2) T→ε(3) B→Tb(4) B→ε
对应的LR(0)自动机如下。
I 0 I_0 I0 项目集存在两个归约项目 B → ⋅ B\rightarrow · B→⋅ 和 T → ⋅ T\rightarrow · T→⋅ ,分别表示把栈顶的空串归约成 B B B 和 T T T,因此当栈顶为空串空串时,不能确定采用哪个产生式进行归约,这就是归约-归约冲突。
与建立LR(0)转换图完全一样。
转换图中边上的终结符表示边起点对应的项目集(状态)遇到该终结符时要将边终点对应的项目集(状态)的编号压入状态栈中,因此需要将 a c t i o n [ 边 起 点 对 应 的 项 目 集 编 号 , 终 结 符 ] action[边起点对应的项目集编号,终结符] action[边起点对应的项目集编号,终结符] 条目填上 边 终 点 对 应 的 项 目 集 的 编 号 边终点对应的项目集的编号 边终点对应的项目集的编号,表示如果前一个状态遇到该终结符需要进行移进操作到达后有个状态。
转换图中边上的非终结符用于填写 g o t o goto goto 表,即 g o t o [ 边 起 点 对 应 的 项 目 集 编 号 , 非 终 结 符 ] = 边 终 点 对 应 的 项 目 集 的 编 号 goto[边起点对应的项目集编号,非终结符] = 边终点对应的项目集的编号 goto[边起点对应的项目集编号,非终结符]=边终点对应的项目集的编号。
对于那些存在归约项目的项目集,如果满足SLR分析法的基本思想,那么SLR分析表要求,对于其中的归约项目 A → α ⋅ A\rightarrow \alpha· A→α⋅,当该状态遇到 F O L L O W ( A ) FOLLOW(A) FOLLOW(A) 中的终结符或 $ 时,采用该项目对应的产生式进行归约,即 a c t i o n [ 该 状 态 编 号 , F O L L O W ( A ) ] = r 该 产 生 式 编 号 action[该状态编号,FOLLOW(A)]=r该产生式编号 action[该状态编号,FOLLOW(A)]=r该产生式编号 。
特别地,要将填写 a c t i o n [ 包 含 S ′ → S ⋅ 的 项 目 集 的 编 号 , $ ] action[包含 S'\rightarrow S· 的项目集的编号,\$] action[包含S′→S⋅的项目集的编号,$] 为 a c c acc acc,表示到达接受状态。
另外,当然也可以构造出NFA,,详见:
SLR分析存在的问题:
SLR只是简单地考察下一个输入符号 b b b 是否属于与归约项目 A → α A→\alpha A→α 相关联的 F O L L O W ( A ) FOLLOW(A) FOLLOW(A),但 b ∈ F O L L O W ( A ) b∈FOLLOW(A) b∈FOLLOW(A) 只是归约 α \alpha α 的一个必要条件,而非充分条件。
考虑下面的转换图中的 I 2 I_2 I2 , F O L L O W ( R ) FOLLOW(R) FOLLOW(R) 中包括 = = =,所以这不满足建立SLR分析表的要求。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hwl6jeyr-1656172667608)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220615214754053.png)]](https://1000bd.com/contentImg/2022/06/27/204113774.png)
下面是某个句型的分析树。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6Za94542-1656172667608)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220615215537479.png)]](https://1000bd.com/contentImg/2022/06/27/204114368.png)
但是根据 1 ) 1) 1) 产生式可以知道,该文法所表示的句型中最多存在一个 = = =,且 R R R 作为 S → L = R S\rightarrow L=R S→L=R 右部的最后一个符号,而 S S S 的后面只能连接结束符 $,因此分析树从上往下数第二层的 R R R 的后继符号只能是 $;而第三层的 R R R 的后继符号只能是 = = =。可见,在特定位置, A A A 的后继符号集合是 F O L L O W ( A ) FOLLOW(A) FOLLOW(A) 的子集。
将一般形式为 [ A → α ⋅ β , a ] [A→\alpha·\beta, a] [A→α⋅β,a] 的项称为LR(1)项,其中 A → α β A→\alphaβ A→αβ 是一个产生式, a a a 是一个终结符(这里将 $ 视为一个特殊的终结符)它表示在当前状态下, A A A 后面必须紧跟终结符,称为该项的展望符(或搜索符)。
注意:
格外注意,展望符只有在归约时起作用。在分析表中同样不影响LR(0)、SLR表中的移进操作,变的只是归约操作。
由于引入了展望符的概念,所以转换图中状态的个数和项目的表示都会发生变化。
另外说明,两个LR(1)项目相同是指项目的第一个分量和第二个分量都相同。
如何确定一个项目的展望符?
S ′ → ⋅ S S'\rightarrow ·S S′→⋅S 的展望符为 $
核心项目(除 S ′ → ⋅ S S'\rightarrow ·S S′→⋅S 外)的展望符由输入文法符号前的状态中的项目继承而来
对于某个项目(可以是核心也可以是非核心)的等价项目,要在生成项目集的过程中同时确定。假设存在产生式 B → γ B\rightarrow \gamma B→γ,则项目 [ A → α ⋅ B β , a ] [A\rightarrow \alpha·B\beta, a] [A→α⋅Bβ,a] 的等价项目为 [ B → ⋅ γ ] , F I R S T ( β a ) [B\rightarrow ·\gamma], FIRST(\beta a) [B→⋅γ],FIRST(βa),即如果 β ≠ ε \beta≠\varepsilon β=ε ,则等价项目为 [ B → ⋅ r , F I R S T ( β ) ] [B\rightarrow ·r,FIRST(\beta)] [B→⋅r,FIRST(β)] ,否则为 [ B → ⋅ r , a ] [B\rightarrow ·r,a] [B→⋅r,a]
构建如下增广文法的转换图。
首先 S ′ → ⋅ S S'\rightarrow ·S S′→⋅S 的展望符为 $,将项目加入到 I 0 I_0 I0。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rcJeem2y-1656172667609)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220615222119365.png)]](https://1000bd.com/contentImg/2022/06/27/204114524.png)
由于 S ′ → ⋅ S S'\rightarrow ·S S′→⋅S 中 S S S 后面为空串,所以其等价项目的展望符应该都继承自其展望符 $。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t6cmVdzF-1656172667609)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220615222326209.png)]](https://1000bd.com/contentImg/2022/06/27/204114727.png)
但是对于新生成的等价项目 [ S → ⋅ L = R , $ ] [S\rightarrow ·L=R,\$] [S→⋅L=R,$],其点后面的非终结符 L L L 后面不是空串,所以该项目的等价项目的展望符应该为 = = =。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SbROoOD9-1656172667609)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220615222523574.png)]](https://1000bd.com/contentImg/2022/06/27/204114915.png)
继续按照上面的规则得到完整的 I 0 I_0 I0。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F0Dmr57x-1656172667610)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220615222548923.png)]](https://1000bd.com/contentImg/2022/06/27/204115196.png)
遇到输入符号 S S S 进入新的状态 I 0 I_0 I0,先把后继项目原封不动地加入到 I 1 I_1 I1 项目集中(包括展望符也原封不动),发现其自己形成闭包。

其他项目集同理。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HorNtMPs-1656172667610)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220615223524062.png)]](https://1000bd.com/contentImg/2022/06/27/204115805.png)
转换图中边上的终结符表示边起点对应的项目集(状态)遇到该终结符时要将边终点对应的项目集(状态)的编号压入状态栈中,因此需要将 a c t i o n [ 边 起 点 对 应 的 项 目 集 编 号 , 终 结 符 ] action[边起点对应的项目集编号,终结符] action[边起点对应的项目集编号,终结符] 条目填上 边 终 点 对 应 的 项 目 集 的 编 号 边终点对应的项目集的编号 边终点对应的项目集的编号,表示如果前一个状态遇到该终结符需要进行移进操作到达后有个状态。
转换图中边上的非终结符用于填写 g o t o goto goto 表,即 g o t o [ 边 起 点 对 应 的 项 目 集 编 号 , 非 终 结 符 ] = 边 终 点 对 应 的 项 目 集 的 编 号 goto[边起点对应的项目集编号,非终结符] = 边终点对应的项目集的编号 goto[边起点对应的项目集编号,非终结符]=边终点对应的项目集的编号。
对于那些存在归约项目的项目集,对于其中的归约项目 A → α ⋅ A\rightarrow \alpha· A→α⋅,当该状态遇到展望符中的终结符或 $ 时,采用该项目对应的产生式进行归约,即 a c t i o n [ 该 状 态 编 号 , 展 望 符 ] = r 该 产 生 式 编 号 action[该状态编号,展望符]=r该产生式编号 action[该状态编号,展望符]=r该产生式编号 。
特别地,要将填写 a c t i o n [ 包 含 S ′ → S ⋅ 的 项 目 集 的 编 号 , $ ] action[包含 S'\rightarrow S· 的项目集的编号,\$] action[包含S′→S⋅的项目集的编号,$] 为 a c c acc acc,表示到达接受状态。
最终构造的分析表如下。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DJMcqBSw-1656172667611)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220615223906404.png)]](https://1000bd.com/contentImg/2022/06/27/204116024.png)
引入LALR的原因:
由LALR分析法产生的分析表比规范LR的分析表要小得多,而且对大多数一般编程语言来说,其语法构造都能方便地由LALR文法表示。
就分析器的大小而言,SLR和LALR 的分析表对同一个文法有同样多的状态,而规范LR分析表要大得多。所以,使用SLR和 LALR的分析表比使用规范LR分析表要经济得多。
LALR分析的基本思想: 寻找具有相同核心的LR(1)项集,并将这些项集合并为一个项集。所谓项集的核心就是其第一分量的集合。然后根据合并后得到的项集族构造语法分析表。如果分析表中没有语法分析动作冲突,给定的文法就称为LALR (1)文法,就可以根据该分析表进行语法分析。
为什么说合并同心项目集(由规范LR(1)自动机生成LALR自动机)不会产生新的移进-归约冲突?
同心项目集的第一个分量是相同的,所以本质上合并的是对应项的展望符集合,而展望符直在归约的时候起作用,移进时无作用,因此合并同心集前如果没有移进-归约冲突,那么合并后也不会引入。
但是合并同心项目集可能会产生归约-归约冲突,也可能推迟错误的发现。
当输入串有错误时,LALR分析可能比LR饭呢西多做一些不必要的的归约,但LALR分析绝不会比LR分析移进更多的符号。
特点:
我们手工计算的思路是,先构造规范 LR(1) 转换图,再将同心项目集合并。
但实际实现时,如果按照这个方式构造,显然构造LALR分析表要比构造LR(1)分析表速度更慢,这与我们的目标相违背,显然实际实现时不会采用这种方法,存在其他高效的方法。
对上面的规范LR自动机进行合并同心项目集得到如下LALR自动机。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T5pUFxGP-1656172667611)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220616063723008.png)]](https://1000bd.com/contentImg/2022/06/27/204116243.png)
与构造规范 LR(1) 分析表方法相同。
最终构造的分析表如下。

※ 所谓文法是XXX的(XXX为LR(0)、SLR、LR(1)或LALR(1)),其含义为该文法可以使用XXX分析方法进行语法分析,当且仅当依据该文法构建的XXX分析表全部条目内容唯一,即无冲突,那么我们就称该文法是XXX的。
同时应该理解,对于任何一种文法都可以按照上面的XXX分析算法构建对应的分析表,只不过构建出来的分析表可能存在冲突。如果存在冲突显然该文法就不能采用XXX分析法,反之可以。
四种方法不同之处在于分析表的建立方法,相同之处在于使用分析表的方法,即驱动程序。
在LR分析中,可按如下方法实现紧急方式的错误恢复:从栈顶开始退栈,直至出现状态 s s s,它有预先确定的非终结符 A A A 的转移;然后抛弃若干个(可以是零个)输人符号,直至找到符号 a a a,它能合法地跟随 A A A。分析器再把 A A A 和状态 g o t o [ s , A ] goto[ s,A] goto[s,A] 压进栈,恢复正常分析。 A A A 的选择可能不唯一,一般来说 A A A 应是代表主要语法构造的非终结符,如表达式、语句或程序块。例如,若 A A A 是非终结符 s t m t stmt stmt ,那么 a a a 可以是分号或 } \} },后者标记一个语句序列的结束。
这种错误恢复方法的实质是试图忽略含语法错误的短语。分析器认为由 A A A 推出的串含有一个错误,这个串的一部分已经处理,该处理的结果是若干个状态已压到栈顶。这个串的其余部分仍在剩余输人中。分析器试图跳过这个串的其余部分,在剩余输人中找到一个符号,它能合法地跟随 A A A 。通过从栈中弹出一些状态,跳过若干输入符号,把 g o t o [ s , A ] goto[ s,A] goto[s,A] 推进栈,分析器装扮成已发现了 A A A 的一个实例,并恢复正常分析。
错误恢复的另一种方式叫做短语级恢复。当发现错误时,分析器对剩余输入作局部纠正,它用可以使分析器继续分析的串来代替剩余输入的前缀。典型的局部纠正有:用分号代替逗号,删除多余的分号,或插入遗漏的分号等。编译器的设计者必须仔细选择替换的串,以免引起死循环。死循环是可能的,例如,总是在当前输人符号的前面插入一些东西。
这种替换可以纠正任何输入串,它的主要缺点是很难应付实际错误出现在诊断点以前的情况。
对LR分析来说,短语级恢复的实现过程是,考察LR分析表的每一个空白条目并根据语言的使用情况,决定最可能进入该条目的输入错误,然后为该条目编一个适当的错误恢复过程。
用Yacc写的规范由三部分组成:
声明
%%
翻译规则
%%
用C语言编写的支持例程
用Yacc建立翻译器的过程:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bf3bL28U-1656172667611)(C:\Users\23343\AppData\Roaming\Typora\typora-user-images\image-20220616074455019.png)]](https://1000bd.com/contentImg/2022/06/27/204116930.png)
Yacc规范的第二部分位于第一个%%后面,该部分放置翻译规则,每条规则由一个文法产生式及和它相联系的语义动作组成。产生式集合
左部→选择1|选择2 |…|选择n
在Yacc中写成
左部 :选择1{语义动作1}
|选择2{语义动作2}
...
|选择n{语义动作n}
;
Yacc规范的第三部分是一些用C语言写的支持例程。必须提供名字为yylex()的词法分析器(当然也可以用Lex来产生 yylex()),其他的过程,如错误恢复例程,需要的话也可以加上。
词法分析器yylex()返回记号,它由记号名和属性值两部分组成。如果返回的是 DIGIT \textbf{DIGIT} DIGIT 这样的记号名,该名字还必须声明在Yacc规范第一部分的第一节中,给它一个区分于其他记号的值。属性值是通过Yacc定义的变量yylval传给分析器的。
除非另有说明,否则Yace按下面两条规则解决分析动作的冲突。
(1)对于归约-归约冲突,选择在Yacc规范中最先出现的那个冲突产生式。
(2)对于移进-归约冲突,优先移进。(这条规则正确解决了悬空else的移进-归约冲突)
对于采用 % r i g h t \%right %right 等方式规定的优先级和结合性,Yacc不用报告用这些优先级和结合性能解决的移进-归约冲突。