banner
NEWS LETTER

语法分析

Scroll down

语法分析是编译过程的核心阶段。它在词法分析器生成的记号(Token)序列的基础上,根据源语言的语法规则(通常由上下文无关文法描述),检查记号序列是否在结构上构成了合法的程序(即“句子”),并构造出程序的层次结构,即分析树

1. 语法分析基础概念 (附录重点)

在深入分析方法前,必须掌握以下基本概念:

  • 推导 (Derivation):从文法开始符号 出发,应用产生式(如 )将非终结符替换为其产生式右部的过程,记为
    • 最左推导 (Leftmost Derivation, ):每一步推导都替换句型中最左边的非终结符。
    • 最右推导 (Rightmost 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...stmtif E1 then if E2 then S1 else S2 有二义性。这通常通过“else 与最近的 then 匹配”的规则来解决。
  • 短语、直接短语、句柄 (Phrase, Direct Phrase, Handle)
    • 这些是自底向上分析的核心概念。
    • 短语:在句型 中,若 ,则 称为句型 相对于 的短语。
    • 直接短语:如果 (一步推导),则 称为直接短语
    • 句柄:一个句型的最左直接短语称为该句型的句柄

2. 语法分析的两大方法

语法分析器主要分为两类,它们构造分析树的方向相反:

  1. 自顶向下 (Top-Down):从根结点(开始符号)向叶结点(输入串)构造分析树。它试图为输入串找到一个最左推导
  2. 自底向上 (Bottom-Up):从叶结点(输入串)向根结点(开始符号)构造分析树。它实际上是最右推导的逆过程(称为规范归约)。

3. 语法错误处理

一个好的语法分析器必须能处理错误。
* 目标:清楚报告错误位置和性质、能从错误中恢复、且不影响正确程序的效率。
* 紧急恢复 (Panic Mode):最简单的策略。一旦发现错误,分析器就抛弃输入记号,直到找到一个指定的同步记号(如 ;end)为止。


4. 自顶向下分析 (Top-Down Analysis)

4.1 递归下降分析 (Recursive Descent)

  • 思想:为每个非终结符编写一个递归过程(函数)。
  • 试探性 (Backtracking):这是一种“试探性”的分析方法。当一个非终结符 有多个候选产生式时(例如 ),分析器会按顺序尝试每一种可能:
    1. 试探:分析器选择第一个产生式 ,并假设它是正确的。
    2. 匹配:它尝试用 去匹配接下来的输入符号串。
    3. 成功:如果 成功匹配了输入串的一部分,分析器就继续前进。
    4. 失败:如果在匹配 的过程中发现不一致,分析器就必须回溯 (Backtracking)
  • 回溯的代价
    • 分析器将输入指针重置回尝试 之前的原始位置。
    • 它抛弃在尝试 期间所做的所有分析。
    • 它返回到非终结符 的地方,尝试下一个候选式
  • 两大问题
    1. 左递归 (Left Recursion):产生式如 会导致递归函数无限调用,陷入死循环。
    2. 回溯 (Backtracking):重复工作多,效率极低,是一种穷尽一切可能的试探法。

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)
      1. = 栈顶符号; = 当前输入符号。
      2. if ()接受,分析成功。
      3. if ()弹出 ,输入指针 前移。
      4. if ( 是终结符但 )错误
      5. if ( 是非终结符)
        • 查询 M[X, a]
        • if ()弹出 ,然后将 (反序) 压入栈。输出该产生式。
        • if ( = 空白)错误

4.4 构造 LL(1) 分析表

构造 M[A, a] 的关键是计算 FIRSTFOLLOW 集合。

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:递归文法(表达式文法)

这是一个更复杂的例子,它展示了为什么我们通常从“最底层”的非终结符开始计算。

考虑文法:

  1. :( 不依赖别的)

    • :根据规则 3,看 。根据规则 1,。加入 (

    • :根据规则 3,看 。根据规则 1,。加入 id

    • 结果:

  2. :( 不依赖别的)

    • :根据规则 3 和 1,加入 *

    • :根据规则 2,加入

    • 结果:

  3. :(依赖

    • :根据规则 3,看

    • 我们将 加入

    • 不在其中。我们加入 (id

    • 吗?不是。

    • 我们停止(不需要看 )。

    • 结果:

  4. :( 不依赖别的)

    • :根据规则 3 和 1,加入 +

    • :根据规则 2,加入

    • 结果:

  5. :(依赖

    • :根据规则 3,看

    • 我们将 加入

    • 不在其中。我们加入 (id

    • D ma? 不是。

    • 我们停止(不需要看 )。

    • 结果:

2. FOLLOW 集合
* :在所有句型中,可能紧跟在 之后出现的终结符号(或 $)的集合。
* 如果 可能是某个句型的最右符号,则 $ (输入结束符)
* 计算规则
1. 将 $ 放入 (S是开始符号)。
2. 若有产生式 ,则 (除 外) 加入
3. 若有产生式 ,则 中的所有元素都加入
4. 重复 2, 3 直到集合不再变化。

示例 1:简单情况(规则 1, 2)

文法:

(开始符号是 )

FIRST 集 (已计算):

计算 FOLLOW

  1. 初始化

    • (根据规则 1)

  2. 第 1 轮迭代

    • 分析产生式

      • 这符合规则 2 (),其中 , , ,

      • 应用规则 2:将 加入

      • 。因此,将 加入

      • 应用规则 3:检查 (即 ) 是否能推导出 。因此规则 3 不适用于此。

    • 变为

  3. 第 2 轮迭代

    • 再次检查所有产生式,没有发现新的终结符可以被加入任何 FOLLOW 集合。
  4. 最终结果


示例 2:链式 nullable(规则 2, 3 结合)

文法:

(开始符号是 )

FIRST 集 (已计算):

计算 FOLLOW

  1. 初始化

    • (规则 1)

  2. 第 1 轮迭代

    • 分析产生式

      • 对于 ():

        • 规则 2:将 加入

        • (因为 )

        • 加入

        • 规则 3 吗?不,因为

      • 对于 ():d

        • 规则 2:将 加入

        • 。将 加入

        • 规则 3 吗?不。

      • 对于 (,即 形式):

        • 规则 3:将 加入

        • 。将 加入

  3. 第 2 轮迭代

    • 没有新符号加入。
  4. 最终结果


示例 3:完全 nullable(规则 3 的深入应用)

文法:

(开始符号是 )

FIRST 集 (已计算):

计算 FOLLOW

  1. 初始化

    • (规则 1)

  2. 第 1 轮迭代

    • 分析产生式

      • 对于 ():

        • 规则 2:将 加入 ,所以没有符号加入。

        • 规则 3 吗?是的。

        • 因此,将 加入

        • 。将 加入

      • 对于 (,即 形式):

        • 规则 3:将 加入

        • 。将 加入

  3. 第 2 轮迭代

    • 没有新符号加入。
  4. 最终结果


示例 4:递归文法(表达式文法)

这是最典型、最重要的例子,它需要多次迭代才能稳定。

文法:

(开始符号是 )

FIRST 集 (已计算):

计算 FOLLOW

  1. 初始化

    • (规则 1)

  2. 第 1 轮迭代

    • :

      • 对于 ():

        • 规则 2:将 加入

        • 规则 3:因为 ,将 加入

        • 变为

      • 对于 ():

        • 规则 3:将 加入

        • 变为

    • :

      • 对于 ():

        • 规则 2:将 加入 (已存在)。

        • 规则 3:因为 ,将 加入

        • 。将 加入 (已存在)。

    • :

      • 对于 ():

        • 规则 2:将 加入

        • 规则 3:因为 ,将 加入

        • 变为

      • 对于 ():

        • 规则 3:将 加入

    • :

      • 对于 ():

        • 规则 2:将 加入 (已存在)。

        • 规则 3:因为 ,将 加入

        • 。将 加入 (已存在)。

    • :

      • 对于 ( ):

        • 规则 2:将 加入

        • 变为

    • (此时集合为)

  3. 第 2 轮迭代 (集合发生了变化,必须重新迭代):

    • :

      • 对于 ():

        • 规则 3:将 加入

        • 。将 ) 加入 (新)。

        • 变为

      • 对于 ():

        • 规则 3:将 加入

        • 。将 ) 加入 (新)。

        • 变为

    • :

      • 对于 ():

        • 规则 3:将 加入

        • (这是新值)。将 ) 加入 (已存在)。

    • :

      • 对于 ():

        • 规则 3:将 加入

        • (这是新值)。将 ) 加入 (新)。

        • 变为

      • 对于 ():

        • 规则 3:将 加入

        • (这是新值)。将 ) 加入 (新)。

        • 变为

    • :

      • 对于 ():

        • 规则 3:将 加入

        • (这是新值)。将 ) 加入 (已存在)。

  4. 第 3 轮迭代

    • 再次检查所有产生式,没有发现新的终结符可以被加入任何 FOLLOW 集合。集合已稳定。
  5. 最终结果

3. 构造分析表 (算法 4.2)
* 对文法 G 的每个产生式 执行:
1. For ( 中的每个终结符 )
*
2. If ( )
* For ( 中的每个终结符 或 $ )
*
* 所有其他 均填 “error”。

LL(1) 文法:如果构造的分析表 不含多重定义的表项(即没有冲突),则该文法是 文法。

4.5 详细示例:LL(1) 分析的全过程

让我们以一个表达式文法为例,完整地走一遍 LL(1) 的所有步骤。

目标:为文法 构造一个 LL(1) 分析器,并分析输入串 num * num $
原始文法 G


第1步:检查 LL(1) 条件并改写文法

  • 检查:原始文法 G 不是 LL(1) 文法
    • 的产生式 存在左递归
    • 存在左公因子(虽然在此例中,消除左递归已顺带解决)。
    • LL(1) 文法要求对于 ,必须满足 。原始文法不满足。
  • 改写文法 G G’(消除左递归)
    • 对于 E:


    • 对于 T:


    • 对于 F: (无变化)
  • 最终文法 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] 弹出E, 压入
2 $ E’ T’ num * num $ M[T, num] 弹出T, 压入
3 $ E’ T’ F num * num $ M[F, num] 弹出F, 压入 num
4 $ E’ T’ num num * num $ X=a (匹配!), 弹出 num, ip前移
5 $ E’ T’ * num $ M[T', *] 弹出T’, 压入
6 $ E’ T’ F * * num $ X=a (匹配!), 弹出 *, ip前移
7 $ E’ T’ F num $ M[F, num] 弹出F, 压入 num
8 $ E’ T’ num num $ X=a (匹配!), 弹出 num, ip前移
9 $ E’ T’ $ M[T', $] 弹出T’, 压入
10 $ E’ $ M[E', $] 弹出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[状态, 非终结符]:指定归约后要转移到的新状态
  • 工作过程 (算法 4.3)
    1. = 栈顶状态; = 当前输入。
    2. if (ACTION[S, a] = Shift ):将 压入栈, 前移。
    3. if (ACTION[S, a] = Reduce ) (假设 ):
      • 从栈顶弹出 个状态(和符号)。
      • = 此时的栈顶状态。
      • = GOTO[S’, A]
      • 压入栈。
    4. if (ACTION[S, a] = Accept):分析成功。
    5. 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)
    1. 初态
    2. C = { I_0 }
    3. 循环:对 中每个 和每个文法符号 ,计算 。如果 不为空且不在 中,将其加入
    4. 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) 通过在项目中携带向前看符号 (lookahead) 来精确化归约时机。
* 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) SLR(1) LALR(1) LR(1)
* LL(1) LALR(1)
* 所有这些都是无二义性文法的子集。


7. 巩固习题

  1. (文法改造)
    考虑文法 G[bexpr]
    bexpr → bexpr or bterm | bterm
    bterm → bterm and bfactor | bfactor
    1. 这个文法有什么问题,会导致自顶向下分析失败?
    2. 请消除这些问题,将其改写为适合 LL(1) 分析的等价文法。
  2. (LL(1) 分析)
    考虑文法 G[S]
    S → (L) | a
    L → SL'
    L' → ,SL' | ε
    1. 计算所有非终结符 S, L, L'FIRSTFOLLOW 集合。
    2. 构造该文法的 LL(1) 预测分析表。
    3. 模拟 LL(1) 分析过程(栈、输入、动作),分析输入串 (a,a)$
  3. (Shift-Reduce 分析)
    考虑文法:






    1. 请模拟“移进-归约”分析过程,分析输入串 id * id + id。请在每一步中明确指出句柄是什么(如果归约的话)。
  4. (SLR(1) 冲突)
    考虑 PPT 中文法 4.7:
    S → L=R | R
    L → *R | id
    R → L
    1. 构造该文法的 项目集 (提示:PPT中已给出 )。
    2. 计算非终结符 集合。
    3. 解释为什么 状态在 分析表中会产生移进-归约冲突
  5. (LR(1) 与 LALR(1) 对比)
    1. LALR(1) 分析表是如何通过 LR(1) 分析表构造的?
    2. 这种构造方法(合并同心集)的主要优点是什么?
    3. 这种构造方法可能引入什么新类型的冲突(如果原 LR(1) 文法无冲突)?

希望这份笔记能帮助你全面掌握语法分析!

你希望我为你详细解答这些习题,还是开始总结下一章的内容(如果已提供)?

如果您喜欢我的文章,可以考虑打赏以支持我继续创作.

其他文章
目录导航 置顶
  1. 1. 1. 语法分析基础概念 (附录重点)
  2. 2. 2. 语法分析的两大方法
  3. 3. 3. 语法错误处理
  4. 4. 4. 自顶向下分析 (Top-Down Analysis)
  5. 5. 示例 1:简单情况(规则 1, 2)
  6. 6. 示例 2:链式 nullable(规则 2, 3 结合)
  7. 7. 示例 3:完全 nullable(规则 3 的深入应用)
  8. 8. 示例 4:递归文法(表达式文法)
  9. 9. 5. 自底向上分析 (Bottom-Up Analysis)
  10. 10. 6. 文法层级总结
  11. 11. 7. 巩固习题
请输入关键词进行搜索