语法分析是编译过程的核心阶段。它在词法分析器生成的记号(Token)序列的基础上,根据源语言的语法规则(通常由上下文无关文法描述),检查记号序列是否在结构上构成了合法的程序(即“句子”),并构造出程序的层次结构,即分析树。
1. 语法分析基础概念 (附录重点)
在深入分析方法前,必须掌握以下基本概念:
- 推导 (Derivation):从文法开始符号
出发,应用产生式(如 )将非终结符替换为其产生式右部的过程,记为 。 - 最左推导 (Leftmost Derivation,
):每一步推导都替换句型中最左边的非终结符。
- 最右推导 (Rightmost Derivation,
):每一步推导都替换句型中最右边的非终结符。这也称为规范推导。
- 最左推导 (Leftmost Derivation,
- 句型、句子 (Sentential Form, Sentence):
- 句型:从开始符号
经过0步或多步推导得到的任何文法符号串 ( )。
- 句子:一个只包含终结符号的句型。
- 句型:从开始符号
- 文法二义性 (Ambiguity):
- 如果一个文法可以为某个句子生成多棵分析树(或多个最左推导/最右推导),则该文法是二义性的。
- 示例1 (表达式):文法
E → E+E | E*E | id对句子id+id*id有两棵分析树。
- 示例2 (悬挂else):文法
stmt → if...then...stmt | if...then...stmt else...stmt对if E1 then if E2 then S1 else S2有二义性。这通常通过“else与最近的then匹配”的规则来解决。
- 如果一个文法可以为某个句子生成多棵分析树(或多个最左推导/最右推导),则该文法是二义性的。
- 短语、直接短语、句柄 (Phrase, Direct Phrase,
Handle):
- 这些是自底向上分析的核心概念。
- 短语:在句型
中,若 且 ,则 称为句型 相对于 的短语。
- 直接短语:如果
(一步推导),则 称为直接短语。
- 句柄:一个句型的最左直接短语称为该句型的句柄。
- 这些是自底向上分析的核心概念。
2. 语法分析的两大方法
语法分析器主要分为两类,它们构造分析树的方向相反:
- 自顶向下
(Top-Down):从根结点(开始符号)向叶结点(输入串)构造分析树。它试图为输入串找到一个最左推导。
- 自底向上 (Bottom-Up):从叶结点(输入串)向根结点(开始符号)构造分析树。它实际上是最右推导的逆过程(称为规范归约)。
3. 语法错误处理
一个好的语法分析器必须能处理错误。
*
目标:清楚报告错误位置和性质、能从错误中恢复、且不影响正确程序的效率。
* 紧急恢复 (Panic
Mode):最简单的策略。一旦发现错误,分析器就抛弃输入记号,直到找到一个指定的同步记号(如
; 或 end)为止。
4. 自顶向下分析 (Top-Down Analysis)
4.1 递归下降分析 (Recursive Descent)
- 思想:为每个非终结符编写一个递归过程(函数)。
- 试探性
(Backtracking):这是一种“试探性”的分析方法。当一个非终结符
有多个候选产生式时(例如 ),分析器会按顺序尝试每一种可能: - 试探:分析器选择第一个产生式
,并假设它是正确的。
- 匹配:它尝试用
去匹配接下来的输入符号串。
- 成功:如果
成功匹配了输入串的一部分,分析器就继续前进。
- 失败:如果在匹配
的过程中发现不一致,分析器就必须回溯 (Backtracking)。
- 试探:分析器选择第一个产生式
- 回溯的代价:
- 分析器将输入指针重置回尝试
之前的原始位置。
- 它抛弃在尝试
期间所做的所有分析。
- 它返回到非终结符
的地方,尝试下一个候选式 。
- 分析器将输入指针重置回尝试
- 两大问题:
- 左递归 (Left Recursion):产生式如
会导致递归函数无限调用,陷入死循环。
- 回溯 (Backtracking):重复工作多,效率极低,是一种穷尽一切可能的试探法。
- 左递归 (Left Recursion):产生式如
4.2 文法改造 (Top-Down 的前提)
为了实现高效的(无回溯的)自顶向下分析,必须改造文法:
1. 消除左递归
* 直接左递归:
* 消除方法:替换为:
*
*
* 示例:E → E+T | T
* 改写为:E → TE'
* E' → +TE' | ε
2. 提取左公因子
* 问题:
* 提取方法:替换为:
*
*
*
示例:stmt → if...then...stmt | if...then...stmt else...
* 改写为:stmt → if...then...stmt S'
* S' → else stmt | ε
4.3 预测分析 (Predictive Analysis)
这是一种不带回溯的递归下降分析。它能根据当前向前看的输入符号,确切地知道应选择哪个产生式。
- 递归调用预测分析:
- 为改造后(无左递归、无左公因子)的文法,每个非终结符编写一个递归过程。
- 函数内根据当前输入符号(
char)来决定调用哪个其他过程或匹配哪个终结符。
- 示例:
procE()先调用procT(),然后检查是否有+,如果有,就消耗+并再次调用procE()(或procT(),取决于消除左递归后的文法E' → +TE' | ε)。
- 为改造后(无左递归、无左公因子)的文法,每个非终结符编写一个递归过程。
- 非递归预测分析 (LL(1)分析法):
- 这是最重要的一种自顶向下方法。
- LL(1) 含义:L (Left-to-right scan)
从左到右扫描输入;L (Leftmost derivation)
构造最左推导;1 (1 lookahead) 向前看1个记号。
- 模型:由一个输入缓冲区、一个符号栈、一个预测分析表M和一个控制程序组成。
- 工作过程 (算法 4.1):
= 栈顶符号; = 当前输入符号。
- if (
):接受,分析成功。
- if (
):弹出 ,输入指针 前移。
- if (
是终结符但 ):错误。
- if (
是非终结符): - 查询
M[X, a]。
- if (
):弹出 ,然后将 (反序) 压入栈。输出该产生式。
- if (
= 空白):错误。
- 查询
- 这是最重要的一种自顶向下方法。
4.4 构造 LL(1) 分析表
构造 M[A, a] 的关键是计算 FIRST 和
FOLLOW 集合。
1. FIRST 集合
*
* 如果
* 计算规则:
1. 若
2. 若
3. 若
* 将
* 如果
* … 以此类推。如果所有
示例 1:简单情况(规则 1, 2, 3)
考虑文法:
: 看
:根据规则 3,我们看 。根据规则 1, 。我们将 加入 。 看
:根据规则 2,我们将 加入 。 结果:
: 看
:根据规则 3,我们先看 。 我们将
加入 。 是 ,所以我们加入 。 现在
。 规则 3 接着问:
吗?是的。 因此,我们继续看
。我们将 加入 。 根据规则 1,
。我们将 加入 。 现在
。 规则 3 接着问:
吗?不是。 我们停止。
结果:
示例 2:链式 nullable(规则 3 的深入应用)
考虑文法:
: 看
:根据规则 3 和 1, 。 结果:
: 看
:加入 。 看
:根据规则 2,加入 。 结果:
: 看
:加入 。 看
:根据规则 2,加入 。 结果:
: 看
:根据规则 3,我们看 。 我们将
加入 。加入 。 。 吗?是的。 我们继续看
。我们将 加入 。加入 。 。 吗?是的。 我们继续看
。我们将 加入 。加入 。 。 吗?不是。 我们停止。
结果:
示例 3:完全 nullable(规则 3 的最后部分)
考虑文法:
: 看
:根据规则 2,加入 。 结果:
: 看
:根据规则 2,加入 。 结果:
: 看
:根据规则 3,我们看 。 我们将
加入 。(什么也没加入)。 。 吗?是的。 我们继续看
。我们将 加入 。(什么也没加入)。 。 吗?是的。 我们已经检查了产生式中的所有符号(
和 ),并且它们都包含 。 根据规则 3 的最后一条:“…如果所有
都含 ,则 加入 。” 因此,我们将
加入 。 结果:
示例 4:递归文法(表达式文法)
这是一个更复杂的例子,它展示了为什么我们通常从“最底层”的非终结符开始计算。
考虑文法:
:( 不依赖别的) 看
:根据规则 3,看 。根据规则 1, 。加入 (。看
:根据规则 3,看 。根据规则 1, 。加入 id。结果:
:( 不依赖别的) 看
:根据规则 3 和 1,加入 *。看
:根据规则 2,加入 。 结果:
:(依赖 ) 看
:根据规则 3,看 。 我们将
加入 。 是 。 不在其中。我们加入 (和id。吗?不是。 我们停止(不需要看
)。 结果:
:( 不依赖别的) 看
:根据规则 3 和 1,加入 +。看
:根据规则 2,加入 。 结果:
:(依赖 ) 看
:根据规则 3,看 。 我们将
加入 。 是 。 不在其中。我们加入 (和id。D ma? 不是。 我们停止(不需要看
)。 结果:
2. FOLLOW 集合
* $)的集合。
* 如果
* 计算规则:
1. 将 $ 放入
2. 若有产生式
3. 若有产生式
4. 重复 2, 3 直到集合不再变化。
示例 1:简单情况(规则 1, 2)
文法:
(开始符号是
FIRST 集 (已计算):
计算 FOLLOW 集:
初始化:
(根据规则 1)
第 1 轮迭代:
分析产生式
: 这符合规则 2 (
),其中 , , , 。 应用规则 2:将
加入 。 。因此,将 加入 。 应用规则 3:检查
(即 ) 是否能推导出 ? 。因此规则 3 不适用于此。
变为 。
第 2 轮迭代:
- 再次检查所有产生式,没有发现新的终结符可以被加入任何
FOLLOW集合。
- 再次检查所有产生式,没有发现新的终结符可以被加入任何
最终结果:
示例 2:链式 nullable(规则 2, 3 结合)
文法:
(开始符号是
FIRST 集 (已计算):
计算 FOLLOW 集:
初始化:
(规则 1)
第 1 轮迭代:
分析产生式
: 对于
( ): 规则 2:将
加入 。 (因为 ) 。 将
加入 。 规则 3:
吗?不,因为 。
对于
( ):d 规则 2:将
加入 。 。将 加入 。 规则 3:
吗?不。
对于
( ,即 形式): 规则 3:将
加入 。 。将 加入 。
第 2 轮迭代:
- 没有新符号加入。
最终结果:
示例 3:完全 nullable(规则 3 的深入应用)
文法:
(开始符号是
FIRST 集 (已计算):
计算 FOLLOW 集:
初始化:
(规则 1)
第 1 轮迭代:
分析产生式
: 对于
( ): 规则 2:将
加入 。 ,所以没有符号加入。 规则 3:
吗?是的。 因此,将
加入 。 。将 加入 。
对于
( ,即 形式): 规则 3:将
加入 。 。将 加入 。
第 2 轮迭代:
- 没有新符号加入。
最终结果:
示例 4:递归文法(表达式文法)
这是最典型、最重要的例子,它需要多次迭代才能稳定。
文法:
(开始符号是
FIRST 集 (已计算):
计算 FOLLOW 集:
初始化:
(规则 1)
第 1 轮迭代:
: 对于
( ): 规则 2:将
加入 。 规则 3:因为
,将 加入 。 变为 。
对于
( ): 规则 3:将
加入 。 变为 。
: 对于
( ): 规则 2:将
加入 (已存在)。 规则 3:因为
,将 加入 。 。将 加入 (已存在)。
: 对于
( ): 规则 2:将
加入 。 规则 3:因为
,将 加入 。 。 变为 。
对于
( ): 规则 3:将
加入 。 。
: 对于
( ): 规则 2:将
加入 (已存在)。 规则 3:因为
,将 加入 。 。将 加入 (已存在)。
: 对于
( ): 规则 2:将
加入 。 变为 。
(此时集合为):
第 2 轮迭代 (集合发生了变化,必须重新迭代):
: 对于
( ): 规则 3:将
加入 。 。将 )加入(新)。 变为 。
对于
( ): 规则 3:将
加入 。 。将 )加入(新)。 变为 。
: 对于
( ): 规则 3:将
加入 。 (这是新值)。将 )加入(已存在)。
: 对于
( ): 规则 3:将
加入 。 (这是新值)。将 )加入(新)。 变为 。
对于
( ): 规则 3:将
加入 。 (这是新值)。将 )加入(新)。 变为 。
: 对于
( ): 规则 3:将
加入 。 (这是新值)。将 )加入(已存在)。
第 3 轮迭代:
- 再次检查所有产生式,没有发现新的终结符可以被加入任何
FOLLOW集合。集合已稳定。
- 再次检查所有产生式,没有发现新的终结符可以被加入任何
最终结果:
3. 构造分析表 (算法 4.2)
* 对文法 G 的每个产生式
1. For (
*
2. If (
* For (
*
* 所有其他
LL(1) 文法:如果构造的分析表
4.5 详细示例:LL(1) 分析的全过程
让我们以一个表达式文法为例,完整地走一遍 LL(1) 的所有步骤。
目标:为文法 num * num $。
原始文法 G:
第1步:检查 LL(1) 条件并改写文法
- 检查:原始文法 G 不是 LL(1) 文法。
的产生式 和 存在左递归。
存在左公因子(虽然在此例中,消除左递归已顺带解决)。
- LL(1) 文法要求对于
,必须满足 。原始文法不满足。
- 改写文法 G
G’(消除左递归): - 对于 E:
- 对于 T:
- 对于 F: (无变化)
- 对于 E:
- 最终文法 G’:
第2步:计算 FIRST 和 FOLLOW 集
- FIRST 集:
- FOLLOW 集:
( $是开始符号,)来自)
(来自 )
(来自 )
- 根据公式计算SELECT集
第3步:构造预测分析表 M
| num | + | - | * | / | ( | ) | $ | |
|---|---|---|---|---|---|---|---|---|
| E | ||||||||
| E’ | ||||||||
| T | ||||||||
| T’ | ||||||||
| F |
- 解释 M[T’, +]:
是一个产生式。 包含 。因此,我们需要 集合。 。所以 , , , 都填 。
第4步:模拟分析过程
- 分析输入串:
num * num $
| 步骤 | 分析栈 (底 |
输入 | 动作 (X, a) | 输出 |
|---|---|---|---|---|
| 1 | $ E | num * num $ | M[E, num] |
|
| 2 | $ E’ T’ | num * num $ | M[T, num] |
|
| 3 | $ E’ T’ F | num * num $ | M[F, num] num |
|
| 4 | $ E’ T’ num | num * num $ | X=a (匹配!), 弹出
num, ip前移 |
|
| 5 | $ E’ T’ | * num $ | M[T', *] |
|
| 6 | $ E’ T’ F * | * num $ | X=a (匹配!), 弹出
*, ip前移 |
|
| 7 | $ E’ T’ F | num $ | M[F, num] num |
|
| 8 | $ E’ T’ num | num $ | X=a (匹配!), 弹出
num, ip前移 |
|
| 9 | $ E’ T’ | $ | M[T', $] |
|
| 10 | $ E’ | $ | M[E', $] |
|
| 11 | $ | ** |
接受 |
5. 自底向上分析 (Bottom-Up Analysis)
5.1 “移进-归约”分析 (Shift-Reduce)
这是自底向上分析的通用方法。它使用一个符号栈。
*
核心:在分析过程中,栈顶的符号串始终是规范句型的一个活前缀
(Viable Prefix)。
*
活前缀:一个规范句型(最右推导得到的句型)的前缀,且不包含句柄之后的任何符号。
* 四种动作:
1. 移进 (Shift):将下一个输入符号压入栈顶。
2. 归约 (Reduce):当栈顶形成一个句柄
3. 接受 (Accept):当栈中只剩开始符号
4. 错误 (Error):无法执行上述动作。
*
冲突:该方法的难点在于如何准确找到句柄并决定何时移进、何时归约。
* 移进-归约冲突 (Shift-Reduce Conflict)
* 归约-归约冲突 (Reduce-Reduce Conflict)
5.2 LR 分析法
LR分析法是目前最强大、最高效的自底向上分析技术,它能确定性地解决移进-归约冲突。
- LR(k) 含义:L (Left-to-right scan)
从左到右扫描,R (Rightmost derivation)
构造最右推导的逆过程,k (k个向前看符号)。
- LR 分析模型:
- 一个栈,但栈中存放的是状态 (state)
和文法符号 。栈顶状态 隐含了已识别的活前缀信息。
- 一个LR分析表,分为两部分:
- ACTION[状态, 终结符]:指定 (shift, reduce, accept,
error)。
- GOTO[状态,
非终结符]:指定归约后要转移到的新状态。
- ACTION[状态, 终结符]:指定 (shift, reduce, accept,
error)。
- 一个栈,但栈中存放的是状态 (state)
- 工作过程 (算法 4.3):
= 栈顶状态; = 当前输入。
- if (ACTION[S, a] = Shift
):将 压入栈, 前移。
- if (ACTION[S, a] = Reduce
) (假设 ): - 从栈顶弹出
个状态(和符号)。
= 此时的栈顶状态。
= GOTO[S’, A]。
- 将
压入栈。
- 从栈顶弹出
- if (ACTION[S, a] = Accept):分析成功。
- if (ACTION[S, a] = Error):调用错误处理。
5.3 构造 LR 分析表
构造LR分析表的核心是构造一个识别文法所有活前缀的DFA。DFA的每个状态就是一个LR项目集。
- 拓广文法 (Augmented
Grammar):在原文法G中加入一个新产生式
,其中 是新的开始符号。这用于产生唯一的“接受”状态。
- LR(0)
项目:一个文法产生式,其右部某位置加了一个圆点
•。(待约项目,期待 )
(移进项目,已看到 ,期待 )
(归约项目,已看到 ,准备归约)
- 核心操作1:
closure(I)(闭包) (算法 4.4)- 思想:如果一个项目
在项目集 中(意味着我们正在期待 ),那么 的所有产生式(如 )也必须被加入到 中,因为我们 也 在期待 。
- 重复此过程直到
不再增大。
- 思想:如果一个项目
- 核心操作2:
go(I, X)(转移函数)- 思想:
go(I, X)模拟DFA在读入符号后的状态转移。
go(I, X)=closure( {所有这样的项目 | 在 中 } )。
- 思想:
- DFA 构造 (LR(0)项目集规范族) (算法 4.5)
- 初态
。
C = { I_0 }。
- 循环:对
中每个 和每个文法符号 ,计算 。如果 不为空且不在 中,将其加入 。
- DFA的状态就是
。
- 初态
LR分析表的本质:构造识别活前缀的NFA之后,将NFA转化为DFA得到的结果。这里省略了NFA-DFA的转化,通过一个特殊的算法,可以直接省略掉这一步。将构造子集,计算闭包的过程直接纳入算法。
5.4 三种 LR 分析表
1. SLR(1) (Simple LR)
* 使用 LR(0) 项目集 和 FOLLOW 集
来构造。
* SLR(1) 表构造 (算法 4.6):
* ACTION - Shift:如果 ACTION[i, a] = Shift j。
* ACTION - Reduce:如果
* For (
*
ACTION[i, a] = Reduce A \rightarrow \alpha。
* ACTION - Accept:如果 ACTION[i, $] = Accept。
* GOTO:如果 GOTO[i, A] = j。
* 冲突:如果 ACTION[i, a] 既是 Shift
又是 Reduce,产生移进-归约冲突。
2. LR(1) (Canonical LR)
* SLR(1) 的
* LR(1) 项目:形式为
*
* closure(I) (算法 4.7):
* 如果
* LR(1) 表构造 (算法 4.9):
* ACTION - Shift:如果 ACTION[i, a] = Shift j。
* ACTION - Reduce:如果
*
ACTION[i, a] = Reduce A \rightarrow \alpha。
(注意:只在
* 优点:最强大的分析方法。
* 缺点:状态集(DFA)非常庞大,分析表可能非常大。
3. LALR(1) (Look-Ahead LR)
* 一种折中方案:状态数与 SLR(1) 相同,分析能力接近
LR(1)。
* 构造思想 (算法 4.10):
1. 先构造出完整的 LR(1) 项目集规范族。
2. 找出所有同心集 (core)
并合并它们。
* 同心集:指
* 示例:
3. 根据合并后的新项目集
*
冲突:合并同心集不会产生新的“移进-归约”冲突,但可能会产生“归约-归约”冲突。
6. 文法层级总结
PPT 第94页总结了各类文法的关系:
* LR(0)
* LL(1)
* 所有这些都是无二义性文法的子集。
7. 巩固习题
- (文法改造)
考虑文法G[bexpr]:
bexpr → bexpr or bterm | bterm
bterm → bterm and bfactor | bfactor- 这个文法有什么问题,会导致自顶向下分析失败?
- 请消除这些问题,将其改写为适合 LL(1) 分析的等价文法。
- 这个文法有什么问题,会导致自顶向下分析失败?
- (LL(1) 分析)
考虑文法G[S]:
S → (L) | a
L → SL'
L' → ,SL' | ε- 计算所有非终结符
S,L,L'的FIRST和FOLLOW集合。
- 构造该文法的
LL(1)预测分析表。
- 模拟 LL(1) 分析过程(栈、输入、动作),分析输入串
(a,a)$。
- 计算所有非终结符
- (Shift-Reduce 分析)
考虑文法:
请模拟“移进-归约”分析过程,分析输入串id * id + id。请在每一步中明确指出句柄是什么(如果归约的话)。
- (SLR(1) 冲突)
考虑 PPT 中文法 4.7:
S → L=R | R
L → *R | id
R → L- 构造该文法的
项目集 (提示:PPT中已给出 )。
- 计算非终结符
的 集合。
- 解释为什么
状态在 分析表中会产生移进-归约冲突。
- 构造该文法的
- (LR(1) 与 LALR(1) 对比)
- LALR(1) 分析表是如何通过 LR(1) 分析表构造的?
- 这种构造方法(合并同心集)的主要优点是什么?
- 这种构造方法可能引入什么新类型的冲突(如果原 LR(1) 文法无冲突)?
- LALR(1) 分析表是如何通过 LR(1) 分析表构造的?
希望这份笔记能帮助你全面掌握语法分析!
你希望我为你详细解答这些习题,还是开始总结下一章的内容(如果已提供)?
如果您喜欢我的文章,可以考虑打赏以支持我继续创作.