Archipelago

Lec 2用正则表达式与上下文无关文法规范语言

讲义完成更新于 2026-09-16

Lec 02 用正则表达式与上下文无关文法规范语言

配套复习课:R1–R2(前端阶段) 参考:Cooper et al., Ch.2 Scanners;Ch.3 §3.1–3.2 Parsers

TL;DR

  • 词法层用正则表达式定义 token,再经结构归纳和子集构造转成 NFA / DFA 执行识别。
  • 嵌套结构超出正则语言的表达能力,因此语法层使用上下文无关文法(CFG)和语法树。
  • 结合性、优先级与悬挂 else 等歧义需通过改造具体文法解决,AST 则保留后续阶段真正需要的程序结构。

1. 语言定义问题(Language Definition Problem)

如何精确地定义一门语言?采用分层结构:

定义(语言的分层结构)

  • 字母表 (alphabet):语言中的字母集合 Σ\Sigma。

  • 词法结构 (lexical structure):识别语言中的"词 (words)",每个词是字母的序列。

  • 语法结构 (syntactic structure):识别语言中的"句子 (sentences)",每个句子是词的序列。

  • 语义 (semantics):程序的含义——对每个输入应产生什么结果。

本讲只关注词法与语法两层。

1.1 两种对偶视角

形式语言的规范有两种对偶 (dual) 思路,二者间可自动互转,这是 CS 的一大理论胜利:

  • 生成式 (generative):文法 (grammar) 或正则表达式 (regular expression),告诉你如何生成语言中所有串。
  • 识别式 (recognition):自动机 (automaton),告诉你如何判定某个串是否属于语言。

标准工程做法:用正则表达式/文法来定义语言(人友好),再自动翻译成自动机来实现(机器友好)。


2. 词法层:正则表达式(Regular Expressions)

2.1 构造规则

给定字母表 Σ\Sigma,正则表达式由以下构造归纳生成:

定义(正则表达式)

  • ε\varepsilon:空串(empty string)

  • 来自 Σ\Sigma 的任意字母

  • r1r2r_1 r_2:序列(sequence)——r1r_1 后接 r2r_2

  • r1∣r2r_1 \mid r_2:选择(choice)——r1r_1 或 r2r_2

  • r∗r^*:克莱尼星(Kleene star)——迭代序列与选择,即 ε∣r∣rr∣⋯\varepsilon \mid r \mid rr \mid \cdots

  • 括号用于分组/优先级 :::

2.2 生成语义:重写规则

把正则表达式不断重写,直到只剩一串字母,得到的串即被该表达式生成。重写的一般规则:

r1∣r2→r1,r1∣r2→r2,r∗→r r∗,r∗→εr_1 \mid r_2 \to r_1, \quad r_1 \mid r_2 \to r_2, \quad r^* \to r\,r^*, \quad r^* \to \varepsilon

重写类似"等式推理",但不同的规则应用次序产生不同结果——这是生成过程的非确定性 (nondeterminism)。

例题(用 (0∣1)∗.(0∣1)∗(0\mid1)^*.(0\mid1)^* 生成 1.0)

(0|1)*.(0|1)*
→ (0|1)(0|1)*.(0|1)*      [r* → r r*]
→ 1(0|1)*.(0|1)*          [选择取 1]
→ 1.(0|1)*                [r* → ε]
→ 1.(0|1)(0|1)*           [r* → r r*]
→ 1.(0|1)                 [r* → ε]
→ 1.0                     [选择取 0]

不同选择会生成不同串(如 0.1),所有可生成串的集合即为该正则表达式的语言 (language),可数无限。语言中的一个串常称为一个 token。

2.3 常用速记与示例

  • [0-9]=(0∣1∣⋯∣9)[0\text{-}9] = (0\mid1\mid\cdots\mid9),[a-z]=(a∣b∣⋯∣z)[a\text{-}z] = (a\mid b\mid\cdots\mid z)
  • (00)∗(00)^*:偶数长度的全 0 串
  • 1∗(01∗01∗)∗1^*(01^*01^*)^*:含偶数个 0 的串
  • (a∣b∣c)(a∣b∣c∣0∣1∣2)∗(a\mid b\mid c)(a\mid b\mid c\mid0\mid1\mid2)^*:字母数字标识符

3. 识别层:有限状态自动机(Finite-State Automata)

定义(有限状态自动机,形式化)

一个 FSA 是五元组 (S,A,F,s0,sF)(S, A, F, s_0, s_F):有限状态集 SS、有限字母表 AA、转移函数 F:S×A→SF: S \times A \to S、起始状态 s0s_0、接受状态集 sFs_F。自动机的语言是它接受的所有串的集合。

运行机制:从起始状态与输入首字母开始,每步用当前字母匹配一条同标签的转移;直到串结束或匹配失败。若结束时停在接受状态,则接受该串。

3.1 DFA vs. NFA

定义(DFA 与 NFA)

  • DFA(确定型):每个状态对每个字母至多一条转移;无 ε\varepsilon 转移。

  • NFA(非确定型):放宽两条限制——同一标签可有多条转移,且允许 ε\varepsilon(空串)转移。

NFA 的接受规则:只要存在一条执行路径接受,即接受(天使式非确定性 angelic nondeterminism——总能做出导向接受的选择)。


4. 从正则表达式到自动机(结构归纳构造)

用结构归纳 (structural induction) 把任意正则表达式转成 NFA:假设每个子表达式都能转成"单起始 + 单接受"的自动机,再给出每个构造子的拼装方式(均保持单起始 + 单接受)。

  • 基本:ε\varepsilon 用一条 ε\varepsilon 边;字母 aa 用一条 aa 边。
  • 序列 r1r2r_1 r_2:把 r1r_1 的旧接受态用 ε\varepsilon 连到 r2r_2 的旧起始态。
  • 选择 r1∣r2r_1 \mid r_2:新起始态用 ε\varepsilon 分别连到两支起始态,两支接受态再用 ε\varepsilon 汇入新接受态。
  • 克莱尼星 r∗r^*:新起始 →ε\to_\varepsilon 旧起始;旧接受 →ε\to_\varepsilon 旧起始(循环)与新接受;新起始 →ε\to_\varepsilon 新接受(跳过,对应零次)。

该构造产物总是 NFA(含 ε\varepsilon 边)。

4.1 NFA → DFA(子集构造,Subset Construction)

为简化识别算法,需转为 DFA:

定义(子集构造算法)

  • DFA 的每个状态对应 NFA 状态的一个子集。

  • DFA 起始态 = 从 NFA 起始态出发,沿 ε\varepsilon 边可达的状态集合(ε\varepsilon-closure)。

  • DFA 某状态为接受态 当且仅当 其集合中含有任一 NFA 接受态。

  • 对 DFA 状态 DD 与字母 aa:取 DD 对应的 NFA 状态集 NN,计算所有 n∈Nn\in N 读 aa 后(含后续 ε\varepsilon 可达)可能到达的状态并集 SS;若 SS 非空,则 D→aD \xrightarrow{a} 对应 SS 的 DFA 状态。 :::

代价:DFA 可能比 NFA 指数级变大(最坏 2∣S∣2^{|S|} 个状态)。


5. 编程语言的词法类别(Lexical Categories)

每门语言通常有若干词类,每类用一个正则表达式定义,扫描器为每个关键字/类别建一个词法类别:

IfKeyword    = if
WhileKeyword = while
Operator     = + | - | * | /
Integer      = [0-9][0-9]*
Float        = [0-9]*.[0-9]*
Identifier   = [a-z]([a-z]|[0-9])*

这些 token 类别将作为下一层(语法)的终结符。


6. 语法层:为何需要上下文无关文法

正则语言不足以描述编程语言语法——因为存在嵌套结构 (nested syntax):

  • (a+(b-c))*(d-(x-(y-z)))
  • if (x<y) if (y<z) a=5 else a=6 else a=7

正则语言缺少建模嵌套所需的"状态"——典型反例:带括号的表达式语言没有对应的正则表达式。

6.1 上下文无关文法(Context-Free Grammar, CFG)

CFG 是四元组 (T,NT,S,P)(T, NT, S, P):终结符集 TT(每个由正则表达式定义)、非终结符集 NTNT、起始非终结符 SS、产生式集 P:NT→(T∣NT)∗P: NT \to (T \mid NT)^*——左部是单个非终结符,右部是终结符与非终结符的任意序列。

示例文法:

Op    = + | - | * | /        Start → Expr
Int   = [0-9][0-9]*          Expr  → Expr Op Expr
Open  = <                    Expr  → Int
Close = >                    Expr  → Open Expr Close

6.2 推导与语法树(Derivation & Parse Tree)

生成游戏 (Production Game):从 Start 出发,反复选一个非终结符、选一条以它为左部的产生式、用右部替换它,直到无非终结符。不同选择产生不同串。

例

例题(推导 &lt;2-1&gt;+1)

Start
→ Expr
→ Expr Op Expr
→ Open Expr Close Op Expr
→ Open Expr Op Expr Close Op Expr
→ Open Int Op Expr Close Op Expr
→ Open Int Op Expr Close Op Int
→ Open Int Op Int Close Op Int
→  + 1

语法树 (parse tree):内部节点是非终结符,叶子是终结符,边从产生式左部连向右部各符号。它捕获了推导结构。


7. 文法歧义与消除(Ambiguity)

定义(歧义文法)

若某个串存在多个推导(即多棵语法树),则文法是歧义的 (ambiguous)。由于语法树通常反映程序语义,文法歧义往往意味着语义歧义——这是不可取的。

例

例题(2-1+1 的两棵树) 对 Expr → Expr Op Expr,2-1+1 可解析为 &lt;2-1&gt;+1(左结合)或 2-&lt;1+1&gt;(右结合),结果不同(前者 2,后者 0)。

7.1 消除歧义:改造文法("Hack the Grammar")

结合性:把运算符强制为左结合——

原文法                       改造后
Expr → Expr Op Expr          Expr → Expr Op Int
Expr → Int                   Expr → Int
Expr → Open Expr Close       Expr → Open Expr Close

此时 2-1+1 只剩一棵树。

优先级:上面改造仍违反 * 高于 + 的优先级(2-3*4 被当作 <2-3>*4)。解决办法是为每个优先级引入一个非终结符:

AddOp = + | -     Start → Expr
MulOp = * | /     Expr  → Expr AddOp Term      (加减层)
                  Expr  → Term
                  Term  → Term MulOp Num        (乘除层,绑定更强)
                  Term  → Num
                  Num   → Int
                  Num   → Open Expr Close

定理(优先级与结合性的通用处理)

将运算符按优先级分层,每层一个非终结符,绑定越强的层在文法中越靠"内层"(越接近叶子);在每层内部可通过左/右递归选择左/右结合。该方法可推广到任意多优先级层。

7.2 悬挂 else 问题(Dangling Else)

if e1 then if e2 then s1 else s2 中 else 归属哪个 if?歧义。改造文法,严格控制"无 else 的 if"只能出现在何处:

Goal     → Stat
Stat     → WithElse
Stat     → LastElse
WithElse → if Expr then WithElse else WithElse
WithElse → <不含 if-then / if-then-else 的语句>
LastElse → if Expr then Stat
LastElse → if Expr then WithElse else LastElse

即:无 else 的 if 只能位于语句最外层,或一串 if-then-else 的最末尾。


8. 抽象语法树 vs. 具体语法树

改造后的文法(消歧)解析出的具体语法树 (concrete parse tree) 很复杂、不直观。工程上常让解析器产出抽象语法树 (Abstract Syntax Tree, AST):

定义(抽象语法 vs . 具体语法)

  • 抽象语法 (abstract syntax):对应"直觉上"的程序结构,省略为消歧而引入的多余关键字/符号(如 Open/Close);本身可以是歧义的。

  • 具体语法 (concrete syntax):用于实际解析的完整(消歧)文法。

典型流程:直觉但歧义的文法 → 改造成消歧文法 → 解析得具体树 → 转换为更易程序操作的 AST。


9. 文法-自动机层级对应(Chomsky Hierarchy 视角)

定理(文法与自动机的对应)

文法自动机
正则文法 (Regular Grammar)有限状态自动机 (FSA)
上下文无关文法 (CFG)下推自动机 (PDA)
上下文相关文法 (Context-Sensitive)图灵机 (Turing Machine)
  • 正则文法:产生式形如 NT→T∣NT∣T NTNT \to T \mid NT \mid T\,NT。
  • 下推自动机 (PDA) = DFA + 一个栈,转移关系 F:S×(A∪{ε})×V→S×V∗F: S \times (A \cup \{\varepsilon\}) \times V \to S \times V^*(VV 为栈字母表)。CFG 与 PDA 等价——把 CFG 译成 PDA 再解析,是自底向上解析器生成器的基础。
  • 上下文相关文法:产生式允许使用上下文 (T.NT)+→(T.NT)∗(T.NT)^+ \to (T.NT)^*;对应图灵机(有限状态控制 + 双向纸带)。

10. 词汇表(Grammar Vocabulary)

  • 最左推导 (leftmost derivation):每步总是展开最左侧的非终结符;对称地有最右推导。
  • 句型 (sentential form):合法推导某一步得到的、部分或完全推导出的串(如 0 + Expr Op Expr)。

我的理解

正则表达式、CFG 和 AST 是三种不同职责的表示:前两者为「如何识别」服务,AST 为「后续如何理解和变换」服务。不应让解析器的便利性决定下游 IR 的形状。

11. 语言规范与自动机小结

  • 语言分层:字母表 → 词法(正则表达式/自动机)→ 语法(文法)→ 语义。
  • 生成式与识别式对偶且可自动互转;正则表达式 ↔ NFA ↔ DFA(子集构造,可能指数膨胀)。
  • 正则语言无法表达嵌套,故语法层用 CFG;推导/语法树捕获结构。
  • 歧义有害,通过"改造文法"处理结合性、优先级、悬挂 else;再用 AST 还原直觉结构。
  • 文法-自动机层级:正则↔FSA、CFG↔PDA、上下文相关↔图灵机。