本章核心知识点
- 上下文无关文法(context free grammar)
- parse tree 语法分析树
- ambiguity 二义性
- 由文法确认language
- 由language构造grammar
- 文法简化
- 构造无
产生式的文法
- TOP DOWN Parsing 自定向下的文法分析
- LL(1)
- 消除二义性
- 消除左递归
- 提取左公因子
- First()
- Follow()
- 构造预测表 predictive table
- 递归下降分析
- Bottom-Up Parsing 自底向上分析
- 移入和规约技术
- LR(0)
- SLR(0)
- LR(1)
- 二义性文法使用
一.context-free grammar
1.derivation 推导
最左推导和最右规约, 对应CH3中的推导
2.Ambiguity 二义性
一个语法能够产生多棵语法树
3.grammar to language
特例
4.language to grammar
语言构造语法,如上例,可以考虑4种方法:
- 分而治之
- 递归归纳构造(一定要有归纳基础,及出口)
- 后缀构造
- 前缀构造
上面这题
分治 | |
递归 | |
后缀 | |
前缀 |
例题:the number of a in a sentence of S is odd, the number of b in a sentence of S is even.
方法1: 要考虑所有情况,生成的产生式较为复杂。
方法2: 递归分解
下面的图可以这样理解,出口一定经过奇数次a边和偶数次b边,(注意设计的完备性,如过设置出口为红色节点,那么所有的语言都是由a结束的,明显不正确)
5.simplify grammar 文法简化
把文法中不能正常工作的产生式消去。
(1)删除形如P→P的产生式(废话)
(2)删除永不被使用的产生式,即由文法的开始符号无法推导出其左部。(不干活的)
(3)删除不能从中导出终结符串的产生式。(不出活的)(始终有非终结符,或不能推导出终结符串)
(4)整理产生式 文法的简化只是抛弃无效产生式,并不改变文法的对错。
例子:
6.构造无𝜖产生式的上下文无关文法
定义:
首先我们通过两个例题直观的理解一下:
具体流程:
用自己的话说,消除所有产生式中的,就是要将所有情况带入递归的带入依赖产生式,这么做就是为了消除最大头,然后再调整起始符号,就构造结束
例题:
二.Top-Down Parsing 自顶向下分析
top-down用的是最左推导,最主要的问题在于:如何避免试错
两种方法:
1.回溯法,遇到错误就回退,但是效率不高
2.非回溯语法分析器--预测
2.1 LL(1)语法分析
2.1.1LL(1)文法必须满足的性质
以下是性质及其背后的原因和问题分析:
1. FIRST(α) ∩ FIRST(β) = ∅
解释:
- 对于产生式 A→α ∣ β,FIRST 集表示可能从 α或 β 开始的终结符集合。
- 如果 FIRST(α)∩FIRST(β)≠∅,解析器在遇到这些终结符时无法决定应该选择 α还是 β。
问题: 例如:
A→aB∣aC
FIRST(aB)=FIRST(aC)={a}。在遇到输入 a 时,解析器无法确定选择 aB还是 aC。这会导致歧义,破坏 LL(1) 文法的可预测性。
2. 最多一个产生式可以派生空串
解释:
- 如果 α→∗ϵ且 β→∗ϵ,则两个产生式都能生成空串。当输入为空时,解析器会面临歧义,不知道选择 α 还是 β。
问题: 例如:
A→ϵ ∣ ϵ
在解析 A 时,不管解析器选择哪一条产生式,结果都一样,但这种文法是歧义的,无法直接被 LL(1) 解析器处理。
3. 如果 β ⇒ ϵ, 则 FIRST(α) ∩ FOLLOW(A) = ∅*
解释:
- FOLLOW 集表示非终结符A 在某种句型中可能的后继符号集合。如果 β→∗ϵ,则 β 消失后,输入可能直接与 FOLLOW(A) 中的符号匹配。
- 如果 FIRST(α)∩FOLLOW(A)≠∅,解析器在看到一个符号时无法确定它是属于 α 还是 FOLLOW(A),导致歧义。
问题: 例如:
A→α ∣ ϵ
假设 FIRST(α)={a} 且 FOLLOW(A)={a}。当输入为 a 时,解析器无法确定 a 是属于 α 的前缀还是 FOLLOW(A) 中的符号。
总结
如果 LL(1) 文法不满足上述性质,解析器会出现以下问题:
- 歧义:解析器在某些情况下无法明确选择哪条产生式。
- 回溯:需要尝试不同路径进行解析,这破坏了 LL(1) 的线性效率。
- 解析失败:解析器可能陷入死循环或无法继续。
这些性质本质上是为了保证 LL(1) 文法的预测性,使得解析器可以通过当前符号(及最多一个前瞻符号)唯一决定解析路径,从而高效、无歧义地完成解析。
所以由上面的性质,我们可以知道,哪些因素会影响我们预测:
- Common preceding symbols derived by alternative productions of the same non-terminal symbol (左公因子)
- Left-recursive grammar (左递归)
- Ambiguity grammar (二义性)
而我们要做的,就是消除上面这三样东西,来构造LL(1)文法
2.1.2消除二义性
消除二义性的唯一办法:重新设计文法
2.1.3提取左公因子
简单来说,就是找到最长公共前缀,提取然后用一个新的产生式表示不同的后缀
例子:前提取,深提取
2.1.4 消除左递归
消除直接左递归
消除直接左递归的方式很简单,但是更重要的是原理是什么,
直观的来说:左递归是从后往前构建语言,右递归是从前往后更加符合我们写字的习惯
如果要使用LL(1)语法分析,就意味着我们是从前往后的去读和预测,这时候如果文法是左递归的就不适用。所以我们需要消除左递归!
所以流程如下:
已知左递归是从后向前构建语言的,所以没有起始产生式没有左递归的部分一定是在语言的头部,也就是左递归的出口,现在我们修改让它编程右递归,也就是要让所有出口成为头部,在构建一个新的产生式来对应之前左递归的部分,同时加上作为出口。
例子:
消除间接左递归
但是我们都知道,通常情况下,左递归是间接发生的,这时候我们应该如何做呢
我们渐进地替换和消除直接左递归
来看算法:
是不是感觉有点懵,没错我也是这么感觉的
首先, 然后代入所有的
,p是产生式,然后消除直接左递归,循环,直到结束。
举个例子:
例题:顺序不同解答不同
2.2FIRST & FOLLOW
2.2.1FIRST
对于First(X)简单直接理解就是X中可能会出现的第一个非终结符或是的集合。
如 : FIRST(aS) = {a},
又如:对于 , FIRST(S) = {a, b,
}, $代表endmarker
但是存在一种情况,FIRST(S) & FIRST(A)形成循环依赖我们应该怎么办呢?
所以,在实现的时候,我们应该避免使用递归,而是循环增量的求解
具体算法:
例子:
2.2.2FOLLOW
Follow 集合的定义:
对于文法中的一个非终结符 A,Follow(A) 的定义是:
- 如果一个字符串的形式是 S⇒αAβ,那么 Follow(A)包含 First(β)(去掉其中的 ε)。
- 如果 S⇒αA是一个推导,或者 S⇒αAβ且 β 的 First(β) 包含 ε,那么 Follow(A) 包含 Follow(父非终结符)。▽▽▽疑问
- Follow(开始符号)包含文件结束符号(通常记为 $)。
看到第二条定义的时候或许会产生一些疑问,为什么FOLLOW(A)包含FOLLOW(父非终结符),
容易理解:其实和first一样,当自己后面全是逃兵的时候,自然而然可能的非终结符就续上了。
简化后的理解
- Follow 集合可以看作在计算 First 的基础上,加上了额外的“上下文传播规则”。这些规则正是你提到的两条约束:
- 如果一个非终结符后面没有内容(或后面的内容可以推导出 ε),则它继承父非终结符的 Follow 集合。
- 开始符号的 Follow 集合必须包含 $。
这两条约束让 Follow 的计算比 First 更复杂,因为 Follow 集合之间会产生依赖关系,需要多次迭代传播才能完成。
具体流程
规则传播
按照以下三条规则迭代更新每个非终结符的 FOLLOW 集合,直到所有集合不再变化。
规则 1:产生式右部中非终结符后跟终结符
对于产生式 A→αBβ:
- 如果 β的第一个符号是终结符 b,将 b 加入 FOLLOW(B)。
规则 2:产生式右部中非终结符后跟非终结符
对于产生式 A→αBβ:
- 如果 β的第一个符号是非终结符 C,将 FIRST(β)的非空符号加入 FOLLOW(B)。
- 如果 ε∈FIRST(β),还需要将 FOLLOW(A) 加入 FOLLOW(B)。
规则 3:产生式右部中的最后一个非终结符
对于产生式 A→αB或 A→αBβ,且 β→ε:
- 将 FOLLOW(A) 加入 FOLLOW(B)。
流程步骤
Step 1: 初始扫描
- 遍历所有产生式规则,应用规则 1 和规则 2,将初步的信息加入到每个非终结符的 FOLLOW 集合中。
Step 2: 递推更新
- 持续重复遍历文法规则,使用规则 3,将 FOLLOW 信息从左侧符号向右传播,直到 FOLLOW 集合在一次完整遍历后不再发生变化(即收敛)。
Step 3: 结果输出
- 在所有规则遍历完成后,输出每个非终结符的 FOLLOW 集合。
2.3LL(1)语法分析器
2.3.1构造预测表
就是要通过每一个非终结符的FIRST & FOLLOW 构建预测表
具体流程如上面所说,
对于每一个在FIRST中的非终结符 ,我们将对应的产生式填写在预测表中,
当FISRST中存在的时候,这时候我们就需要用到FOLLOW来决定是否使用该产生式
接着对于FOLLOW中的每一个终结符,我们将对应的产生式填写在预测表中。
直到所有的非终结符都填写完毕。
算法结束。
例子:
另一个我有点疑问的例子:
为什么, 看图!!!,注意FOLLOW计算的两个规则,请看前文的FOLLOW计算算法介绍
2.3.2 递归下降解析
三.buttom-up parsing
定义:
LR解析的优点:
- It can recognize virtually all programming language constructs for which context-free grammars can be written.
- It is the most general nonbacktracking shift-reduce parsing method.
- It can parse more grammars than predictive parsers.
LR解析的缺点:
- It is too much work to construct an LR parser by hand for a typical programming language.
- LR parser generators are needed.
- YACC
规约——一个与某产生式体相匹配的特定字串被替换为干产生式头部的非终结符。
3.1移入-规约语法分析技术
3.2LR(0)
3.2.1项 item
可以理解为:用一个黑点来表示当前移动到的位置,以及下一个输入应该是在右边
3.2.2项集--Closure of Item Sets
在一个状态内由kernel items推出non-kernel items,什么是kernel items和non-kernel items呢
- Kernel item:由●移动,即状态变迁所得
- Nonkernel Item:为实现Kernel item的等价状态
3.2.3LR(0)-parsing table
表格最左边代表状态图的状态号,ACTION部分代表终结符,GOTO 中的内容代表状态号,意味着GOTO[i, non-terminal] 在状态i下遇到非终结符应该去哪一个状态
表示转移到
状态,
表示使用i号产生式进行规约
3.2.4LR(0)可能会遇到的冲突
1. 移入-规约冲突(Shift-Reduce Conflict)
定义:
- 当同一个状态下,对于某个输入符号,既可以选择 移入(shift) 操作,又可以选择 规约(reduce) 操作时,发生移入-规约冲突。
发生原因:
- FIRST 和 FOLLOW 集合重叠:
- 在 LR(0) 分析中,FOLLOW 集合不会被明确区分,因此可能导致移入与规约的选择混淆。
- 文法的二义性:
- 如果文法存在二义性(例如经典的 if-else 嵌套问题),LR(0) 无法通过单一的状态决定操作。
示例:
考虑以下文法:
对应的项目集构造过程中,会出现如下冲突:
状态 I3:
在状态 I3,当输入符号为 + 时:
- 项目 E→E·+E 表示应 移入 +。
- 项目 E→E·表示应 规约到 E。
解决办法:
- 修改文法:通过消除二义性、提取左公因子或消除左递归。
- 使用更强的分析器:如 SLR、LR(1) 或 LALR,可以通过向前看符号(lookahead)避免冲突。
2. 规约-规约冲突(Reduce-Reduce Conflict)
定义:
- 当同一个状态下,对于某个输入符号,有两个或更多的产生式都可以执行规约操作时,发生规约-规约冲突。
发生原因:
- 非确定性文法:
- 某些文法可能导致一个输入符号匹配多个规约规则。
- LR(0) 缺乏向前看符号:
- LR(0) 分析不使用向前看符号来区分可能的规约操作。
示例:
考虑以下文法:
对应的项目集可能出现:
状态 I2:
在状态I2,对于任何后续输入符号(如 $),两个产生式都可以规约,发生规约-规约冲突。
3.3SLR(1)-simple LR(1)
SLR(1)(Simple LR(1))是一种改进的 LR 语法分析方法,比 LR(0) 更强大,但比完全的 LR(1) 简单得多,因此被称为 简单的 LR(1)。
SLR(1) 在 LR(0) 分析器的基础上,引入了 向前看符号 的概念,通过利用 FOLLOW 集合 来解决部分 LR(0) 的冲突问题,从而可以处理更复杂的文法。
1. SLR(1) 的特点
- SLR(1) 是基于 LR(0):
- 它首先构造 LR(0) 项目集(也称为项目集规范族)和状态转移。
- 在此基础上,改进 ACTION 表的构造方式。
- 使用 FOLLOW 集合作为向前看符号:
- 通过 FOLLOW 集合来决定规约操作的合法性,从而避免一些 LR(0) 的冲突。
- 适合处理非二义性文法:
- 如果文法可以通过 SLR(1) 分析,称其为 SLR(1) 文法。
- SLR(1) 能处理的文法范围介于 LR(0) 和 LR(1) 之间。
2. SLR(1) 分析的核心改进
与 LR(0) 的主要区别
- LR(0) 决定规约的条件:
- 在 LR(0) 中,只要项目为 A→α⋅(点在产生式末尾),就会直接规约,无论输入符号是什么。
- SLR(1) 决定规约的条件:
- 在 SLR(1) 中,项目为 A→α⋅时,只有当输入符号属于 FOLLOW(A)时才执行规约。
SLR(1) 的改进逻辑:
- 对于 ACTION 表中的每个规约项 A→α,只在输入符号属于 FOLLOW(A)的时候添加规约操作。
- 通过引入 FOLLOW 集合,避免了一些 LR(0) 分析中出现的 移入-规约冲突 和 规约-规约冲突。
3. 构造 SLR(1) 分析表的步骤
SLR(1) 分析表的构造包括以下几个步骤:
(1) 构造 LR(0) 项目集
- 从文法的增广文法出发,构造 LR(0) 项目集规范族和状态转移。
(2) 构造 ACTION 表
- 对于状态 I 中的项目 A→α⋅aβ,如果点后是终结符 a:
- 添加 ACTION[I,a]=shift(j),其中 j 是转移后的状态。
- 对于状态 I 中的项目 A→α⋅:
- 对 FOLLOW 集合中的每个符号 b,添加 ACTION[I,b]=reduce(A→α)。
- 如果某状态包含增广文法的项目 S′→S⋅,且点在末尾:
- 添加 ACTION[I,$]=accept。
(3) 构造 GOTO 表
- 与 LR(0) 分析相同,针对非终结符的转移填充 GOTO 表。
(4) 检查冲突
- 如果 ACTION 表中有同一个状态对某输入符号既有移入又有规约,或者有多个规约,则存在冲突,说明该文法不是 SLR(1)。
构建结果
不再是无脑规约
4. SLR(1) 的优势与不足
优势
- 比 LR(0) 更强大:通过引入 FOLLOW 集合,解决了一部分 LR(0) 分析中常见的冲突。
- 构造简单:相比完全的 LR(1),SLR(1) 不需要为每个项目存储具体的向前看符号,因此构造的表较小。
不足
- 处理能力有限:SLR(1) 无法处理所有 LR(1) 文法,因为它只考虑 FOLLOW 集合,而没有逐项检查每个项目的具体向前看符号。
- 可能出现误判:一些文法的 FOLLOW 集合可能过大,导致误判为冲突文法。
3.4 LR(1)
为什么SLR(1)不行,因为SLR(1)使用的是全局FOLLOW,在某些特殊的情况下,下一个要输入的符号可能导致不正确的行为。
对于文法:
为什么 SLR(1) 无法解决?
SLR(1) 的问题在于它使用了 全局 FOLLOW 集合 来决定规约的合法性,而没有为每个项目引入特定的 向前看符号。在这个例子中:
- FOLLOW(R) 包含了不应该触发规约的符号 =,因为这里的 = 应该用于触发移入操作 L=R。
如果采用更精确的 LR(1) 分析方法,可以为项目 R→L单独记录向前看符号,而不会误将 = 判定为规约触发条件。
LR(1)
那么如何计算LR(1)呢
需要注意的点:
- 计算项集闭包时要计算
, 其中a是内核项的follow,beta是非终结符B的follow
例子:跟着例子走解决问题
构造LR(1)表
总结
分析方法 | 文法要求 | 特点 | 优点 | 缺点 |
---|---|---|---|---|
LL(1) | 1. 无左递归。 看上面的2.1.1 | - 从左到右扫描输入,使用递归下降的方式进行分析。 - 非常适合递归下降分析器的实现。 | - 易于理解和实现。 - 对于适合的文法非常高效。 | |
LR(0) | 1. 文法没有二义性。 2. 对于每个状态,不能有多个归约操作或者移入操作。 3. 每个产生式的后缀都不能与其他产生式产生歧义。 | - 从左到右扫描输入,进行自底向上的分析。 - 最基本的 LR 分析。 | - 适用于大部分文法,能够处理较为复杂的文法结构。 | - 无法处理某些需要区分移入和规约的冲突(如移入-规约冲突)。 - 能处理的文法较为有限。 |
SLR(1) | 1. 无左递归。 2. 对于每个文法的产生式,使用 FOLLOW 集合来判断是否规约。 3. FOLLOW 集合不重叠:对于每个状态,输入符号和 FOLLOW 集合应当一致,以避免移入-规约冲突。 | - 改进了 LR(0),通过使用 FOLLOW 集合解决了部分 LR(0) 的冲突。 - 比 LR(0) 强,但比 LR(1) 简单。 | - 可以处理比 LR(0) 更复杂的文法。 - 构造较为简单,表格大小较小。 | - 依赖 FOLLOW 集合,可能无法区分一些需要具体向前看符号的情况,无法处理所有 LR(1) 文法。 |
LR(1) | 1. 无左递归。 2. 对于每个状态和每个输入符号,唯一确定是移入还是规约。 3. 每个项目集中的项目能够通过单一的向前看符号区分规约操作。 4. FIRST 集合和 FOLLOW 集合不重叠,同时可以使用向前看符号来精确决定动作。 | - 最强大的分析方法,能够处理几乎所有的上下文无关文法。 - 每个项目都能够通过一个 向前看符号 来明确判断移入或规约操作。 | - 能处理所有 LR(0) 和 SLR(1) 可以处理的文法,且能够处理更复杂的文法。 - 适合处理复杂文法。 | - 表格较大,构造复杂。 - 需要更多的内存和时间来处理。 |
-
LL(1):
- 要求文法没有左递归,且 FIRST 集合和 FOLLOW 集合必须互不重叠。
- 适合递归下降分析器,但对于复杂文法或含有左递归的文法无法处理。
-
LR(0):
- 是最基本的自底向上的分析方法,能够处理大部分无二义性的文法,但可能出现移入-规约冲突,无法处理某些复杂文法。
-
SLR(1):
- 相较于 LR(0),引入了 FOLLOW 集合来减少冲突。能够处理比 LR(0) 更复杂的文法,但仍然无法处理所有 LR(1) 文法。
-
LR(1):
- 是最强的分析方法,可以处理几乎所有的上下文无关文法。它通过引入向前看符号来解决移入和规约的冲突,适用于复杂的编译器设计,但代价是更大的内存消耗和更复杂的分析表。
4.使用二义性文法
对于二义性文法
这个文法是二义性的因为没有指明+和*的优先级和结合性,这时候使用二义性可以方便的改变+和*的优先级和结合性
LR(0)项集图
可以看到在和
产生了冲突,假设我们使用SLR(1)方法来看,E的follow是+和*,这时候按理来说应该进行规约,但是又可以移进,于是产生了规约和移进冲突。
考虑定义优先级和结合性(左结合还是右结合)