https://github.com/xianjianlf2/mini-compiler
手写 the-super-tiny-compiler:词法 / 语法 / 转换 / 代码生成(mini-* 源码学习系列,按 Git 提交历史一步步搭建)
https://github.com/xianjianlf2/mini-compiler
ast compiler learning parser tokenizer typescript
Last synced: about 8 hours ago
JSON representation
手写 the-super-tiny-compiler:词法 / 语法 / 转换 / 代码生成(mini-* 源码学习系列,按 Git 提交历史一步步搭建)
- Host: GitHub
- URL: https://github.com/xianjianlf2/mini-compiler
- Owner: xianjianlf2
- Created: 2022-08-23T08:42:45.000Z (almost 4 years ago)
- Default Branch: main
- Last Pushed: 2026-06-24T11:44:37.000Z (about 24 hours ago)
- Last Synced: 2026-06-24T12:09:52.474Z (about 23 hours ago)
- Topics: ast, compiler, learning, parser, tokenizer, typescript
- Language: TypeScript
- Size: 10.7 KB
- Stars: 2
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# mini-compiler
这个仓库会通过 Git 提交历史,一步步把 mini-compiler 搭出来。
目标不是一次写完一个复杂编译器,而是按最适合理解编译流程的顺序,让同一个小编译器慢慢长出来。
换句话说,这个仓库更在意“为什么下一步应该是它”,而不是“还能不能再多加一个功能”。
## 怎么看这条历史
每一条提交只做一件事,所以这条历史本身就是学习路线。
你可以先看完整代码,再按顺序回看提交,观察同一条编译链路是怎么一步步长出来的。
常用方式:
```bash
git log --oneline --reverse
```
如果你想看某一步的具体变化,可以继续用:
```bash
git show
```
如果你想按顺序体验每一步,可以从最早的提交开始,一步步切过去看。
## 为什么要按这个顺序
如果目标是学编译器的核心,而不是尽快做一个真正的语言,那顺序非常重要。
最容易走偏的方式,是太早去做这些东西:
- 先做变量作用域
- 先做类型检查
- 先做优化
- 先做复杂语法
这样虽然看起来“功能更多”,但会把编译器最核心的那条主线打散。
这个项目真正最值得先弄明白的,是这条链:
1. 源码字符串怎么拆开
2. 拆开的片段怎么变成树
3. 树怎么被稳定遍历
4. 源语言的树怎么变成目标语言的树
5. 目标语言的树怎么打印成代码
6. 这些步骤怎么串成一个 compiler
只有这条主线通了,后面的作用域、类型系统、优化、报错提示,才知道该挂在哪、为什么能挂上去。
## 推荐学习顺序
| 步骤 | 当前问题 | 新增能力 | 为什么重要 |
| --- | --- | --- | --- |
| 1 | 源码是一整段字符串 | 分词 | 先得到最小片段 |
| 2 | tokens 还是平铺的 | 解析成语法树 | 保存嵌套关系 |
| 3 | 树需要被稳定访问 | 遍历器 | 把“走树”和“处理节点”分开 |
| 4 | 源语言和目标语言结构不同 | 转换语法树 | 让结构靠近目标代码 |
| 5 | 树不能直接运行 | 生成代码 | 得到最终字符串 |
| 6 | 步骤太散 | compiler 总入口 | 串起完整流程 |
## 整体流程
```mermaid
flowchart LR
A["源码字符串"] --> B["tokenizer"]
B --> C["parser"]
C --> D["traverser"]
D --> E["transformer"]
E --> F["codegen"]
F --> G["目标代码"]
```
## 第一步
先把源码拆成一串 tokens。
这一步只回答一个问题:编译器拿到字符串以后,第一眼应该怎么把它切成更小、更容易理解的片段。
> **为什么下一步不是直接生成代码?**
反例是:如果连 `add`、`2`、`(`、`)` 这些最小片段都没分清楚,后面就没法判断谁是谁的参数。
## 第二步
把 tokens 组织成语法树。
这一版解决的是“括号嵌套关系怎么保存”的问题。
`(add 2 (subtract 4 2))` 不是一串平铺的词,而是 `add` 里面又包含了一个 `subtract`。语法树就是用来保存这种层级关系的。
> **为什么下一步不是直接改成目标代码?**
反例是:如果每次改代码都手写一堆针对某种节点的判断,逻辑会散在很多地方。先有统一遍历方式,后面转换会更清楚。
## 第三步
给语法树加一个统一遍历器。
这一版解决的是“怎么稳定走完整棵树”的问题。
遍历器会先进入外层节点,再进入里面的节点,最后再从里面退出来。这样后面想对某种节点做处理,只需要告诉遍历器“遇到它时做什么”。
> **为什么下一步才开始转换?**
反例是:如果没有遍历器,转换逻辑既要负责走树,又要负责改树,两件事混在一起会很难读。
## 第四步
把源语言语法树转换成目标语法树。
这一版解决的是“源语言结构和目标语言结构不一样”的问题。
源语言是 `(add 2 4)`,目标语言更像 `add(2, 4)`。它们表达的是同一件事,但树的形状不同,所以需要先转换结构。
> **为什么下一步还不能直接结束?**
反例是:转换后的树仍然只是对象结构,人和机器都不能直接把它当成最终代码运行。还需要把树重新打印成字符串。
## 第五步
把目标语法树生成 JavaScript 代码。
这一版解决的是“树怎么变回字符串”的问题。
转换后的树已经很接近 JavaScript,但还不是最终代码。代码生成器会按节点类型,把函数调用、数字、字符串这些节点打印出来。
> **为什么最后还要有 compiler?**
反例是:如果使用者每次都要手动调用分词、解析、转换、生成代码四个步骤,使用体验会很碎,也不利于看清完整流程。
## 第六步
把所有阶段串成一个 compiler。
这一版解决的是“怎么从源码一步到最终代码”的问题。
最终入口只做一件事:把源码依次交给分词、解析、转换和代码生成,最后返回目标代码。
> **为什么到这里先停住?**
反例是:如果刚跑通主线就加入变量、作用域、类型检查,学习者会分不清哪些是编译器主流程,哪些是更高级的语言能力。
## 最终能做什么
现在这个 mini-compiler 可以把这种 Lisp 风格的调用:
```text
(add 2 (subtract 4 2))
```
编译成 JavaScript 风格的调用:
```text
add(2, subtract(4, 2));
```
它也支持字符串参数:
```text
(print "hello")
```
会变成:
```text
print("hello");
```
## 怎么验证
运行:
```bash
npm test
```
它会检查每一个阶段,也会检查完整 compiler 的最终输出。
---
## `mini-*` 源码学习系列
手写主流框架 / 工具的最小可运行实现,每个仓库只追「核心主线」,不堆功能。
| 仓库 | 内容 |
| --- | --- |
| [mini-vue](https://github.com/xianjianlf2/mini-vue) | 手写 Vue3:响应式 / runtime / 编译器 |
| [mini-react](https://github.com/xianjianlf2/mini-react) | 手写 React:Fiber / reconciliation / Hooks |
| [mini-koa](https://github.com/xianjianlf2/mini-koa) | 手写 Koa:中间件洋葱模型 / context |
| [mini-webpack](https://github.com/xianjianlf2/mini-webpack) | 手写 webpack:依赖图 / loader / plugin |
| **mini-compiler**(本仓库) | 手写 the-super-tiny-compiler:词法 / 语法 / 转换 / 生成 |
| [ts-axios](https://github.com/xianjianlf2/ts-axios) | 手写 axios(TypeScript 版) |
> Talk is cheap. Read the code.