很多人把"编译原理"当成劝退课。但把它拆成一条流水线后,你会发现它只做一件事:把源代码一层层降维,翻译成等价的另一种程序 。 本文用一句
x = 1 + 2串完整个流程,最后对照 V 编译器的真实目录,看看这套理论是怎么落地的。
一、先记住一句话
编译的本质是 分阶段降级抽象:把给人看的高级文本,一步步变成给机器执行的低级指令。
每个阶段只操心一件事,做完就把结果交给下一棒。这样既降低了单次思考的复杂度,也让中间产物可以复用------N 种语言的前端可以共用同一个后端,一个前端也能对接多个后端(x86 / ARM / WASM)。
二、六个阶段:让 x = 1 + 2 走一遍
| 阶段 | 输入 → 输出 | 干什么 |
|---|---|---|
| 词法分析 | 字符流 → Token 流 | 扫掉空格和注释,识别出 x、=、1、+、2 |
| 语法分析 | Token 流 → AST | 按语法规则搭树;括号不匹配、缺分号在这里报错 |
| 语义分析 | AST → 带类型的 AST | 查 x 声明过没、类型对不对、1+2 能不能赋给 x |
| 中间表示 / 优化 | AST → IR | 常量折叠成 x = 3、删掉没用到的函数、内联 |
| 代码生成 | IR → 目标代码 | 生成机器码、C 代码、字节码...... |
| 汇编链接 | 目标代码 → 可执行文件 | 地址重定位、链接库 |
三、每一步具体长什么样
1. 词法分析(Lexical Analysis)
编译器拿到的第一手材料只是一串字符。词法分析器(Scanner)把它切成有含义的最小单元 Token,顺便扔掉空格、换行和注释:
text
x = 1 + 2
↓
IDENT(x) ASSIGN(=) INT(1) PLUS(+) INT(2)
2. 语法分析(Parsing)
Token 是平的,程序是嵌套的。语法分析器(Parser)按语法规则把 Token 组装成一棵树------AST(抽象语法树)。这棵树直接表达了"谁先算、谁包着谁":
text
Assign
├── target: x
└── value: Binary
├── left: 1
├── op: +
└── right: 2
注意:括号、分号这类纯语法问题在这一层就暴露了,和"类型对不对"完全解耦。
3. 语义分析(Semantic Analysis)
语法对了不代表意思对。这一层负责:变量有没有声明(查符号表)、类型是否匹配、1 + 2 的结果类型是什么、重载该选哪一个。产出是带类型标注的 AST。
4. 中间表示与优化(IR + Optimization)
AST 仍然是语言相关的,换成更贴近机器的 IR(中间表示)才好做优化:常量折叠、死代码消除、循环不变量外提、函数内联......这些优化与源语言、目标平台都无关,所以能写一次到处复用。
5-6. 代码生成与链接
最后把 IR 降级成目标代码,再交给汇编器和链接器变成可执行文件。这里最硬的活是指令选择 和寄存器分配。
四、对照 V 编译器:理论怎么落地
V 语言的编译由 v.builder 编排,入口在 cmd/v/v.v,流水线非常标准:
text
v.scanner → v.parser → v.checker → v.transformer → v.markused → v.gen.c
| 阶段 | 模块 | 职责 |
|---|---|---|
| 词法 | vlib/v/scanner/ |
字符 → Token |
| 语法 | vlib/v/parser/ |
Token → AST(AST 定义在 vlib/v/ast/) |
| 语义 | vlib/v/checker/ |
类型检查、符号解析 |
| 优化 | vlib/v/transformer/ |
常量折叠、表达式简化 |
| 裁剪 | vlib/v/markused/ |
死代码消除,只保留真正用到的函数 |
| 后端 | vlib/v/gen/c/ |
生成 C 源码 |
V 的独门选择:转译到 C
V 不做传统意义上的后端(指令选择、寄存器分配都不做),而是把 AST 翻译成 C 代码,再交给系统里的 gcc / clang / tcc 去汇编和链接。
好处很直接:
- 编译极快:重活外包给 C 编译器
- 可移植性白嫖:C 能跑的地方 V 就能跑,生态直接复用
- 调试直观 :加
-keepc就能看到人类可读的 C 代码
代价也很实在:
- 语义受 C 约束,某些语言特性不好表达
- 必须依赖外部 C 编译器
想亲眼看这条流水线?在本地 V 仓库里两条命令就有答案:
bash
./v -keepc -o ./vnew cmd/v # 构建编译器,保留生成的 C
./vnew -keepc run file.v # 看你的代码变成了什么 C
除了 C,V 还有 gen/js/、gen/native/(直接产出机器码 ELF / Mach-O)、gen/wasm/,但 C 后端最成熟。
五、一句话总结
编译 = 分阶段降级抽象。 前端理解人写了什么,中端把它改得更好,后端把它翻译成机器能跑的东西。搞懂这条流水线,再去看任何一个编译器的源码,你都能立刻定位到"我现在在哪一层"。