编译的本质,是把不确定的、模糊的人类表达,转换为确定的、可执行的机器结构。
这一过程解决三个根本问题:
所有编译器设计,无论语言、工具、平台如何变化,都围绕这三点展开。
stateDiagram-v2
源代码 --> 词法分析
state 前端 {
词法分析 --> 语法分析
语法分析 --> 语义分析
}
语义分析 --> 中间表示(IR)
state 后端 {
中间表示(IR) --> 优化
优化 --> 目标代码
}
分层不是实现习惯,而是复杂性控制手段。
每一层只解决一个不可回避的本质问题,并向下游提供稳定输出。
| 阶段 | 输入 | 输出 | 不变量(稳定知识) |
|---|---|---|---|
| 词法分析 | 字符流 | Token 流 | 正则语言 / 有限自动机 |
| 语法分析 | Token 流 | AST | 上下文无关文法 |
| 语义分析 | AST | 注解 AST / IR | 类型系统、作用域 |
| 中间表示 | AST | IR | 语言无关抽象 |
| 优化 | IR | IR | 语义等价 |
| 代码生成 | IR | 目标代码 | 指令模型 |
这张表是高度稳定的知识资产,几乎不随语言或工具变化。
问题本质:字符本身没有语义,必须先被分组成“符号”。
词法分析的目标不是“理解程序”,而是:
因此:
词法分析是一个纯粹的状态迁移问题。
age >= 45stateDiagram-v2
初始 --> GT: >
GT --> GE: =
初始 --> ID: 字母
ID --> ID: 字母或数字
初始 --> INT: 数字
INT --> INT: 数字
这里不存在“上下文”“嵌套”“优先级”等复杂问题。
Id : [a-zA-Z_] ([a-zA-Z_] | [0-9])*
IntLiteral : [0-9]+
GE : '>='
正则 ≠ 实现方式,只是规则描述语言。
Token 序列本身仍然是线性的,而程序结构是层次化的。
语法分析解决的问题是:
这些符号是否能组成合法的结构?
上下文无关的含义是:
一个产生式的使用,不依赖它所处的位置。
add ::= mul | add + mul
mul ::= pri | mul * pri
pri ::= Id | Num | (add)
CFG 是结构化语言的最低必要表达能力。
以 2 + 3 * 5 为例:
additive
├── Int(2)
└── multiplicative
├── Int(3)
└── Int(5)
AST 不关心如何写,只关心是什么结构。
递归下降的本质:
文法结构 = 函数调用结构
左递归问题不是语法问题,而是实现问题。
add -> mul add'
add' -> + mul add' | ε
EBNF 本质是对 CFG 的一种工程友好扩展。
语法正确 ≠ 语义正确。
int a = "hello"; // 语法正确,语义错误
语义分析关注:
语义分析通常产生:
IR 是前端与后端的解耦边界。
IR 是稳定核心。
LLVM IR 是经典实践,但 IR 这一思想本身是稳定知识。
优化的唯一约束:
语义等价
优化不是“更快”,而是:
ANTLR ≠ 编译原理本身,而是:
形式化文法 + 自动代码生成
Yacc / Bison / ANTLR 本质相同。
都属于:
工程实现示例(不稳定知识)
理解思想即可,无需长期记忆细节。
编译原理不是“写语言”的专利。
| 场景 | 编译思想 |
|---|---|
| SQL | 查询 → 执行计划 |
| 前端 | AST → 转译 / 优化 |
| DSL | 规则 → 执行模型 |
| 正则 | 模式 → 自动机 |
凡是“表达 → 结构 → 执行”的系统,都是编译问题。
学会编译原理,真正得到的是:
如何把混乱的问题,拆解为可验证、可演进、可执行的结构。