上下文无关文法 (CFG) 基础
上下文无关文法(Context-Free Grammar, CFG)是形式语言理论的核心概念,也是编译器前端设计的基础工具。
A context-free grammar (CFG) is G = (V, Σ, R, S),其中:
V 是有限个变量(非终结符)的集合。
Σ 是有限个终结符的集合,且 Σ 与 V 不相交。
R 是有限个产生式规则,形式为 A → α,其中 A ∈ V 且 α ∈ (V ∪ Σ)*。
S ∈ V 是开始符号。
示例:回文串语言
如何用 CFG 来描述所有回文串?回顾一下,字符串 w 是回文当且仅当 w = w^R(即正读反读都一样)。
文法 G_pal = ({S}, {0,1}, R, S) 定义了这个集合。规则集 R 简洁地表示为:
S → ε | 0 | 1 | 0S0 | 1S1
你看,通过在变量 S 两侧对称地添加 0 或 1,这个递归规则巧妙地捕捉了回文结构的本质。
CFG 的语言:推导与正确性证明
推导的形式化定义
为了从文法生成字符串,我们从开始符号 S 出发,不断用规则的右部替换当前的变量,直到最终只剩下终结符。这个过程就是推导。
对于 G = (V, Σ, R, S):
- 若
A → γ 是规则,则记 αAβ ⇒_G αγβ,表示一步推导。
- 若经过零步或多步推导,则记为
α ⇒_G^* β。
文法 G 的语言 L(G) 定义为所有能被推导出的纯终结符串的集合:
L(G) = { w ∈ Σ* | S ⇒_G^* w }。如果存在某个 CFG G 能生成语言 L,则 L 是上下文无关语言。
证明回文文法的正确性:L(G_pal) = L_pal
任何形式化的定义都需要证明其合理性。我们通过双向包含来严格证明 G_pal 确实精确描述了回文语言 L_pal。

第一部分:证明 L_pal ⊆ L(G_pal)
思路是对回文串 w 的长度进行归纳。基础情况(|w| = 0 或 1)显然成立,因为规则直接包含 S → ε | 0 | 1。归纳步骤的关键在于:任何长度大于等于 2 的回文串,其首尾字符必然相同。假设 w = 0x0 且 x 也是回文,根据归纳假设 S ⇒* x,再利用规则 S → 0S0,我们就能得到 S ⇒ 0S0 ⇒* 0x0 = w。

第二部分:证明 L(G_pal) ⊆ L_pal
这次我们对推导的步数进行归纳。基础情况:一步推导生成的 ε、0、1 显然都是回文。归纳步骤:任何多步推导的形态必然是 S ⇒ 0S0 ⇒* 0x0 或 S ⇒ 1S1 ⇒* 1x1。根据归纳假设,x 已是回文,那么在它两边加上相同字符得到的 0x0 或 1x1 自然也是回文。
至此,双向包含得证,我们确信这个精巧的文法没有任何“漏洞”。
语法分析树 (Parse Trees)
推导过程本质上是线性的,而语法分析树则为我们提供了字符串句法结构的二维层次化视图,这在编译器中至关重要。
对于 CFG G = (V, Σ, R, S),一棵合法的语法树需满足:
- 每个内部节点标记为
V 中的变量。
- 叶节点可以是变量、终结符或
ε;标为 ε 的叶子必须是其父节点的唯一孩子。
- 若内部节点
A 的子节点自左向右为 X_1, X_2, ..., X_k,则 A → X_1 X_2 ... X_k 必须是文法中的规则。
- 这棵树的产出 (Yield),是将所有叶节点标签从左到右连接而成的字符串。

推导与语法树的等价性
一个核心定理是:对于 CFG 中的变量 A,A ⇒*_G w (存在推导)当且仅当 存在一棵根为 A、产出为 w 的语法树。我们分两步证明。
(⇒) 从推导构造语法树
通过对推导步数进行归纳。关键步骤是:假设推导在最后一步应用了规则 X → X_1...X_n = γ。根据归纳假设,我们已经有一棵以 A 为根、产出包含 X 的树。此时,只需在 X 节点下添加代表 X_1,...,X_n 的子节点,新树的产出即是我们期望的最终字符串。

(⇐) 从语法树构造推导
通过对树中内部节点的数量进行归纳。
基础情况:如果树只有一个内部节点(根),那么该树的结构就如右图所示,产出 α = X_1...X_n。根据树的合法性,A → α 必然是一条规则,因此我们可以一步推出 A ⇒ α。

归纳步骤:假设树有 k+1 个内部节点。设根 A 的子节点为 X_1, X_2, ..., X_n,每个子树以 X_i 为根、产出记为 α_i。那么整棵树的产出就是 α = α_1 α_2 ... α_n。

由于每个子树最多有 k 个节点,根据归纳假设,我们有 X_i ⇒* α_i。于是,从 A 开始,我们可以这样构造推导:
A ⇒ X_1 X_2 ... X_n (应用规则) ⇒* α_1 X_2 ... X_n ⇒* α_1 α_2 ... X_n ⇒* ... ⇒* α_1 α_2 ... α_n = α。

小结与上下文无关性
综上所述,推导与语法树是描述 CFG 语言的两种等价视角。这引出了 CFG 一个极其重要的性质——上下文无关性:如果 X ⇒*_G γ,那么在任意上下文 α...β 中,我们都可以独立地将 X 替换为 γ,即 αXβ ⇒*_G αγβ。这个性质是语法分析算法能够模块化工作的理论基石。

实例:英语句子与算术表达式
语法树的概念不仅适用于形式语言,也能很直观地解释自然语言和编程语言的语法。
例如,英语句子 “the girl hits the boy with the bat” 就能产生两棵不同的分析树(见下文歧义部分)。

在程序设计语言中,考虑一个包含整数和标识符、仅支持 + 和 * 运算的算术表达式语言。我们可以定义文法 G_exp,其中规则 R 如下:
E → I | N | -N | E + E | E * E | (E)
I → a | b | Ia | Ib
N → 0 | 1 | N0 | N1

即便是简单的表达式 a + b * a,在 G_exp 中也会生成两棵不同的语法树:一棵对应 (a + b) * a,另一棵对应 a + (b * a)。这直接导致了歧义性问题。

文法歧义性与消除方法
什么是歧义性 (Ambiguity)?
一个文法是歧义的 (Ambiguous),当且仅当对于语言中的同一个句子,存在两棵结构不同的语法分析树。注意,这里强调的是“语法树不同”,而不仅仅是“推导过程不同”。有多种推导方式指向同一棵语法树并不代表文法有歧义。

如何消除歧义?
我们可以通过两种主要方式来消除文法歧义:
- 利用语义约束:比如在自然语言处理中,上下文的语义信息(“谁拿着球棒”)能帮助我们选择唯一正确的句法树。
- 强制优先级与结合性:在程序语言设计中,我们规定
* 的优先级高于 +,或者 else 应与最近的 if 结合。这种约定可以体现在文法规则中。
以下就是一个改造后的无歧义算术表达式文法 G'exp。我们通过引入 F(因子)、T(项)、E(表达式)三个层次,强制实现了运算的优先级:
I → a | b | Ia | Ib
N → 0 | 1 | N0 | N1
F → I | N | -N | (E)
T → F | T * F
E → T | E + T
现在,* 处于比 + 更“深”的 T 层,解析时将自然形成更紧密的结合。

来自计算理论的坏消息
然而,歧义性问题远比想象中棘手。理论计算机科学告诉我们:
- 消歧问题:不存在一个通用算法,能够接收一个任意 CFG 并为其生成一个等价的无歧义文法。
- 歧义判定问题:甚至,不存在一个通用算法能判定任意给定的 CFG 是否是歧义的。此问题是不可判定的 (undecidable)。
固有歧义性
更“悲观”的事实是,有些上下文无关语言是固有歧义的 (Inherently Ambiguous)。这意味着,不论我们尝试什么方法,生成这种语言的所有文法都必然存在歧义。

经典例子是语言 L = {a^i b^j c^k | i = j 或 j = k}。对于任何生成 L 的文法,字符串 a^n b^n c^n 总是会产生两棵语法树:一棵用来检查 a 的数量是否等于 b 的数量,另一棵用来检查 b 的数量是否等于 c 的数量。这是一种结构上的、无法克服的歧义。

本文梳理了 CFG 与语法分析树的核心理论,从基础定义、推导证明到令人困扰的歧义性难题。理解这些底层原理,对于深入学习编译技术乃至计算理论都大有裨益。在云栈社区,你可以找到更多关于编译原理、操作系统等计算机基础技术的高质量讨论和资料。