解释器总览
写一行 let x = 1 + 2 出来 x == 3,看起来一行代码的事——背后是这门语言运行时几千行代码的协作:字符串变 token、token 变 AST、AST 走求值或编译到字节码、字节码进 VM 调度循环,中间还要处理作用域、闭包、错误回滚、GC 暂停。这套机制几乎每门动态语言都长一样(就像 OS 都有进程 / 内存 / 调度),所以亲手写一遍解释器,等于一次性拆掉 JS / Python / Lua / Ruby 共用的那层抽象——之后你读 V8 / CPython 源码会快一个量级,写 DSL / 模板引擎 / 规则引擎时,第一反应是「先定义文法」,而不是堆一堆正则和 if/else 硬拼。
一句话先记住:解释器不是黑魔法,是几千行代码的事——本系列陪你从 0 写一门叫 Mochi 的小语言,从树遍历版本一路推到字节码 VM,核心算法每个 30-80 行 TS 就能跑,30 篇打完你应该能在白板上讲清楚 V8 / CPython / Lua 的核心实现差异。
一、为什么写解释器是元能力
应用工程师 99% 的时间在「用语言」,而不是「看语言怎么实现」。结果是:
- 看到「闭包捕获 by reference」只会背规则,不知道实现层为什么必须 by reference(否则递归引用环建不起来,closure 之间互相调用就崩)
- 写 React
useCallback反复抓 bug,因为不知道 V8 把 closure 编译成了什么样、什么时候会 deopt - 写配置 DSL,第一反应是写正则,正则没法处理嵌套就硬塞
eval,留下注入风险 - 看 CPython 源码,看到
Python/ceval.c那一万行 switch 直接关掉——因为不知道那就是字节码 VM 的调度循环
解释器是 OS 之外、应用工程师最值得拆的一层抽象——和 OS 不同的是,几千行代码就能写完一个能用的玩具版(Lox 3000 行,Monkey 2500 行,Mochi 目标 3500 行)。拆完之后,几个看似神秘的东西会瞬间塌缩:
| 之前的感觉 | 拆完之后 |
|---|---|
| 闭包是"魔法" | 闭包就是一个函数对象 + 一个环境指针 |
| GC 是"黑盒" | mark-sweep 三色标记 50 行能写出来 |
| AST 是"理论东西" | AST 就是一个 sum type / discriminated union |
| 字节码是"汇编近亲" | 字节码就是一个 Uint8Array,VM 是一个 while + switch |
| 异常是"语言特性" | 异常就是带 try 栈的非局部跳转 |
二、三种姿态:解释器 / 编译器 / JIT
「语言怎么跑」其实有三种基础姿态——一门真实语言往往是几种的组合。
| 维度 | 解释器(Interpreter) | 编译器(Compiler) | JIT |
|---|---|---|---|
| 何时翻译 | 边读边执行 | 一次性翻成机器码 | 跑起来后边跑边翻 |
| 输出物 | 无(直接执行) | 可执行文件 | 内存里的机器码 page |
| 启动 | 快 | 慢(要先编译) | 快(冷代码走解释器) |
| 峰值性能 | 慢 | 快 | 接近编译器(热代码) |
| 调试体验 | 好(可中断、可 REPL) | 一般 | 痛苦(代码动态生成、栈帧奇怪) |
| 典型代表 | 早期 Python / Bash / tree-walk Lox | C / Rust / Go(AOT) | V8 / HotSpot / LuaJIT / PyPy |
但现实里很少是"纯解释器"——CPython 不是 tree walk,它先把 .py 编译成 .pyc(字节码),再用解释器跑字节码;V8 干脆是「字节码解释器 Ignition + JIT TurboFan + Maglev + Sparkplug」四个执行引擎接力。所以更准确的分法是「翻译停在哪一层」:
源码
├─ 一路走到底 → 机器码 (C / Rust / Go AOT)
├─ 中间停在 AST → 直接求值 (tree-walk 解释器,本系列 01-19)
├─ 中间停在字节码 → VM 执行字节码 (CPython / Lua / JVM,本系列 20-25)
└─ 字节码热路径 → 翻成机器码 (V8 / HotSpot / PyPy JIT)「编译」和「解释」是连续光谱,不是非黑即白——停在哪一层,就是性能 vs 复杂度的取舍。
三、不是黑魔法:数代码量
很多人觉得「写解释器 = 啃龙书 = 半年起步」,这是被大学编译原理课劝退的副作用。真实的玩具/嵌入式解释器数据:
| 项目 | 类型 | 代码量 | 宿主语言 | 状态 |
|---|---|---|---|---|
| Lox / jlox(《Crafting Interpreters》) | tree-walk | ~2200 行 | Java | 教科书 |
| Lox / clox(《Crafting Interpreters》) | 字节码 VM | ~3500 行 | C | 教科书 |
| Monkey(《Writing An Interpreter in Go》) | tree-walk | ~2500 行 | Go | 教科书 |
| Wren | 字节码 VM | ~9000 行 | C | 嵌入式可用 |
| Lua 5.4 内核 | 字节码 VM | ~25000 行 | C | 生产语言 |
| 本系列 Mochi 目标 | tree-walk → 字节码 | ~3500 行 | TypeScript | 玩具 |
Lua 是反例里最猛的——一门嵌进无数游戏引擎、Redis、Nginx、OpenResty 的生产可用语言,核心代码 25k 行 C。魔兽世界、Roblox 都跑在它上面。
结论:不要被「编译原理」四个字吓到。解释器这一层是「少量代码 + 大量精妙抽象」——这恰好是它适合作为元能力训练的原因:动手成本低,认知收益超高。
四、心智图:源码到结果的五个站点
不管什么语言,从你按下回车到屏幕上出现结果,中间这条路上最多 5 个站点——记住下面这张图,30 篇都在填它的细节:
┌─────────────┐
源码字符流 ────→ │ Lexer │ ── tokens ──→
"1 + 2" │ 词法分析 │ [NUM 1] [PLUS] [NUM 2]
└─────────────┘
┌─────────────┐
tokens ───→ │ Parser │ ── AST ──→
│ 语法分析 │ BinaryExpr(+)
└─────────────┘ ├─ NumLit(1)
└─ NumLit(2)
┌─── 路线 A:树遍历 ──┐ ┌─── 路线 B:字节码 ───┐
│ │ │ │
↓ │ ↓ │
┌────────────┐ │ ┌────────────┐ │
│ Evaluator │ │ │ Compiler │ │
│ 走 AST │ │ │ AST→bytecode│ │
└────────────┘ │ └────────────┘ │
│ │ │ │
↓ │ ↓ │
结果 3 │ [CONST 1] [CONST 2] │
│ [ADD] [RETURN] │
│ │ │
│ ↓ │
│ ┌────────────┐ │
│ │ VM │ │
│ │ 调度循环 │ │
│ └────────────┘ │
│ │ │
│ ↓ │
│ 结果 3 │
└───────────────────────────┘两条路线的取舍:
- 路线 A(树遍历):简单,几百行能写完;慢——每次执行都要重新遍历树并做类型派发,cache 不友好
- 路线 B(字节码):多一个编译步骤,但运行时只剩一个紧凑的
while + switch;实测比 A 快 5-10 倍
本系列 01-19 篇走 A,把 Mochi 写出来能跑;20-25 篇切换到 B,让同一份测试用例跑得快好几倍——加速比实测出来本身就是这层最爽的反馈。
五、本系列要产出什么
不是 30 篇空文,三件实物:
- Mochi 语言本身——动态类型、一等公民函数、闭包、类、模块、try/catch,能跑一段含这些特性的脚本
- GitHub 仓库
mochi-lang——按篇分 tag,每篇对应一个可运行版本,pnpm test全过才能打勾 - 「从 Mochi 到真实源码」的索引——CPython 的
ceval.c在哪一行做调度循环、V8 的 Ignition 在哪个目录、Lua 的lvm.c怎么实现OP_ADD,你的下一站直接落地
宿主语言锁定 TypeScript(读者基数广)+ Rust(关键算法贴近工业级)。不用 Python 写解释器——后面 VM 篇章要测「树遍历 vs 字节码」的实际加速比,宿主太慢会让结论失去说服力。
六、立场
- 不啃龙书——不证明文法等价性,工程师视角够用就行
- 不抄《Crafting Interpreters》——那本书在那儿,本系列补它没讲透的「为什么」和「工业级是怎么做的」
- Mochi 是玩具——所有"为了简单而省略"的地方都明确标注「Mochi 这里偷懒了 / 真实语言会怎么做」,确保你知道边界在哪
- 每篇独立可读,但 Mochi 仓库一路叠加——不是孤立的 demo,是一门越长越大的真实语言
下一篇:02-一段代码怎么跑起来.md,把 print(1 + 2) 这一行 Mochi 代码从字符串变成屏幕上的 3,每一步贴一个数据结构 snapshot——让你看清楚字符流、tokens、AST、字节码 这四种物理形态各是什么,以及为什么 CPython / V8 走的也是同一条路。