第2章 词法分析
词法分析(Lexical Analysis / Scanning)将源程序字符流转换为记号(token)流,是编译器前端的第一步。
2.1 扫描处理
记号(Token)
扫描程序生成的逻辑单元,表示源程序中的信息单元。每个记号有一个类型和可选的属性值。
- 关键字:if, while, for 等固定串
- 标识符:用户定义的名字(字母开头,字母数字组成)
- 特殊符号:+, *, >=, <> 等单字符或多字符符号
- 数字常量:整型、浮点型
- 字符串文字
缓冲区与先行
- 使用双缓冲区处理大源程序
- 单符号先行(single symbol lookahead):一次只生成一个记号
- 最长子串原则:任何时候能构成记号的最长字符串被视为下一个记号
- 遇到分隔符时需回退(backing up)或提前查看(lookahead)
保留字 vs 标识符
- 方法一:先识别为标识符,再查保留字表(TINY 使用此法)
- 方法二:在 DFA 中直接区分
- 保留字查找:线性搜索 → 二分搜索 → 杂凑表 → 最小完美杂凑函数
2.2 正则表达式
正则表达式表示字符串的模式。一个正则表达式完全由它匹配的串的集合定义,该集合称为语言,写作 L(r)。
基本运算
| 运算 | 符号 | 示例 |
|---|---|---|
| 并置 | 相邻 | ab 表示 a 后跟 b |
| 选择 | ` | ` |
| 闭包 | * |
a* 表示 0 个或多个 a |
| 正闭包 | + |
a+ 表示 1 个或多个 a |
| 可选 | ? |
a? 表示 0 或 1 个 a |
| 字符类 | [abc] |
等价于 `a |
常用正则表达式示例
标识符: letter (letter | digit)*
数字常量: digit+ (. digit+)? (E (+|-)? digit+)?
注释: /* ([^*] | *+[^/])* */
代数定律
r | s = s | r(交换律)r | (s | t) = (r | s) | t(结合律)r (s t) = (r s) t(并置结合律)r (s | t) = r s | r t(分配律)r ε = ε r = rr** = r*
扩展表示法
r+:一个或多个重复.:匹配任意字符[0-9]、[a-zA-Z]:字符范围~a:不在给定集合中的字符r?:可选子表达式
2.3 有穷自动机
DFA(确定的有穷自动机)
每个状态对每个输入符号有唯一确定的转换。
![[编译原理/attachments/figures/ch02_s32_2.3.1 Definite of Deterministic finite a.png]]
图中英文:letter=字母,digit=数字,other=其他,始态→接受态表示标识符识别过程
![[编译原理/attachments/figures/ch02_s36_2.3.1 Definite of Deterministic finite a.png]]
图中英文:start=初始状态,accept=接受状态,a, b=输入字符
![[编译原理/attachments/figures/ch02_s41_Deterministic finite automation (DFA).png]]
图中英文:无特殊术语,为 DFA 状态转换示例
五元组:(S, Σ, δ, s0, F)
- S:状态集合
- Σ:输入字母表
- δ:转移函数 S × Σ → S
- s0:初始状态
- F:接受状态集合
NFA(非确定的有穷自动机)
![[编译原理/attachments/figures/ch02_s51_NFA (Nondeterministic Finite Automaton).png]]
图中英文:NFA=Nondeterministic Finite Automaton(非确定有穷自动机),input=输入,state=状态,ε=空串转移
- 一个状态对同一输入可能有多个转换
- 允许 ε-转换(不消费字符的转移)
- NFA 通常比 DFA 更紧凑
NFA → DFA 转换(子集构造法)
算法要点:
1. NFA 每个状态子集 → DFA 的一个状态
2. ε-闭包(s):从 s 出发仅通过 ε 转移能到达的所有状态
3. 对于每个子集 T 和每个输入符号 a:
DFAstate(T, a) = ε-闭包(所有从 T 中状态通过 a 到达的状态)
ε-闭包计算
ε-闭包(T):
将 T 中所有状态入栈
栈非空时:
出栈 → t
对 t 的每条 ε 转移到达 u:
若 u 不在闭包中:
添加 u, u 入栈
返回闭包集合
DFA 状态最小化(划分法)
1. 初始分两组: 接受状态组 F, 非接受状态组 S-F
2. 重复:
对每组 G, 若存在字符 a 使组内状态经 a 转移
到不同的组 → 拆分 G
3. 直到无法继续拆分
2.4 从正则表达式到 DFA
正则表达式 →[Thompson 构造]→ NFA →[子集构造法]→ DFA →[最小化]→ 最小 DFA
Thompson 构造(正则 → NFA)
| 正则结构 | NFA 构造 |
|---|---|
| ε | 两个状态,ε 转移 |
| a(单字符) | 两个状态,a 转移 |
| r | s(选择) | 新增始态和接受态,ε 转移到两个子 NFA |
| r s(并置) | r 的接受态与 s 的始态合并 |
| r*(闭包) | 新增始态和接受态,ε 回路 |
2.5 TINY 扫描程序的实现
![[编译原理/attachments/figures/ch02_s87_FA of TINY.png]]
图中英文:letter=字母,digit=数字,other=其他,TINY DFA 中标识符和数字的识别路径
- 用 DFA 实现(双重嵌套 case 语句)
- 主过程:
getToken()— 根据当前状态和输入字符进行状态转换 - 保留字表:TINY 只有 8 个保留字(if, then, else, end, repeat, until, read, write)
- 使用
ungetNextChar()处理超前读取后的回退
2.6 Lex 扫描程序生成器
- Lex 输入文件结构:
定义部分 %% 规则部分 %% 用户子程序 - 规则格式:
模式 { 动作 } - Lex 用正则表达式描述模式,自动构造 DFA
- 生成的扫描程序输出为 C 代码
考试重点
★★★ 第一梯队(必考)
| 考点 | 典型题型 |
|---|---|
| 正则表达式 → NFA(Thompson 构造) | 构造题 |
| NFA → DFA(子集构造法) | 计算题 |
| DFA 状态最小化(划分法) | 计算题 |
| ε-闭包计算 | 计算题 |
★★☆ 第二梯队
| 考点 | 典型题型 |
|---|---|
| 正则表达式定义与运算 | 简答/选择 |
| DFA 与 NFA 的区别 | 对比题 |
★☆☆ 第三梯队
| 考点 | 典型题型 |
|---|---|
| 保留字处理策略 | 简答 |
| Lex 的使用 | 概念题 |