自己造一门语言:一个学习项目的开篇
0x01 一个一直没解决的问题
我写过不少代码。每次敲下 if,我大概知道它会编译成一条比较指令和一次跳转。但若有人问它是如何变成这两条指令的,我答不上来。
这个空白我填过很多次:看视频、读《编译原理》、翻 LLVM 的文档。每次都看懂了,不到半年却忘干净。
后来我找到了原因:我一直在读别人的做法,从未自己动手做过一次。
看懂和做出来是两回事。看书、看视频,是别人把理解讲给你听;自己动手,才是把理解拿去给现实检验。检验一定会失败,而失败的地方,恰恰是真正学到东西的地方。
所以这次我换了条路:不学编译原理,而是造一门语言。
这个系列写完时,它能跑成这样:
$ varde run hello.vd
hello, varde
$ varde build hello.vd && ls -lh hello
-rwxrwxrwx 1 root root 16K Sep 30 10:34 hello
0x02 为什么选“造语言”作为学习载体
学习编译原理大致有三条路:看书、看视频、读别人的编译器源码。它们都有用,却共享同一个缺口:没有必须做决定的时刻。
而造一门语言,会在每个环节逼你做决定。举三个例子。
例 1 · 词法分析
书上的“正规式 → NFA → DFA”是一套算法,三页纸讲完。轮到动手写,你要回答:
>和>=怎么区分?(最长匹配)//是注释,/是除号,扫描器怎么知道该看第二个字符?- 源码里有中文时,报错该报第几列?字节偏移与字符列号是两回事。
/* /* */ */这种嵌套注释要不要支持?
这些问题书上都不讲,因为它们不是算法问题,是工程问题。工程问题只能自己撞一次才会记住。
例 2 · 类型系统
类型系统的教材定义只有一句:“静态类型 = 编译期检查类型”。真到自己写,问题就来了:
let j: i64 = i;(i是i32)该不该放过?let f: f64 = i;呢?- 函数体里的类型标注能省吗?函数签名上的呢?
- 泛型要单态化,还是用虚表动态派发?
- 若子类型只有“具体类型 →
dyn Trait”这一来源,那Vec<Dog>能不能赋给Vec<Animal>?
每一个问题都没有标准答案,只有取舍。做过取舍的人,才看得懂别人的取舍。
例 3 · 内存管理
“引用计数”在书里是一个概念,配一张计数增减的示意图。把它真正实现出来时,你躲不开这些问题:
- 赋值要不要拷贝对象?传参呢?
let b = a;之后改a,b该不该跟着变?- 两个对象互相引用,计数永远不归零,怎么办?
- 析构函数在什么时刻执行?抛异常时还执行吗?
你必须把答案写进代码,然后承担它的全部后果。
看书是输入,动手是输出。只有真正输出一次,你才知道自己懂没懂。
0x03 为什么是“自己造”,而不是“跟着抄”
跟着《Crafting Interpreters》抄一个 Lox,不失为一种有效的入门方式。很多人就是这么入门的,我自己也读过。
但抄有一个代价:你没有决策时刻。
抄的过程中,所有“为什么这样设计”的问题,答案都是“书里就这么写的”。抄完,你获得的是流程知识,不是判断力。换一个需求,你仍然不知道从何下手。
自己造,差别就在这里:每一个设计决策都要自己拍板,而决策过程本身就是内容。
设计语言时,每个决定都要回答:为什么这么选、放弃了什么、什么时候会咬人。这些“为什么”在任何教程里都找不到答案,因为那是你自己的取舍。
0x04 这个项目是什么
一个教学用的玩具语言 varde。它存在的唯一目的,就是让我(和跟着做的你)把编译原理和 Rust 两件事真正学透。
项目用 Rust 编写,代码总量不到 1 万行,编译后是单个可执行文件。
还有一句话提前说:这门语言不会有用户。它的用户只有我和你。
0x05 你跟着学完会得到什么
你能得到:
- 从字符流到可执行文件的完整能力,不是片段知识
- 一个真实项目的全部决策过程和踩坑记录
- Rust 与编译原理的贯通
- 一套可迁移的方法论:怎么用“造东西”来学习任何系统性知识
你得不到:
- 一门生产可用的语言(varde 只是个玩具)
- 速成。这是个以年计的项目
如果你看到这里,还愿意跟下去,那这个系列就是为你写的。
0x06 出发
回到开头那个问题:if 是怎么被执行的?
现在你大概知道了这条路要走哪些站——从字符流开始,切词、建树、查类型、降成中间表示、生成代码,最后落到机器上。中间每一站都有具体要做的决定,我会一站一站写下去。
设计和实现都从零开始,正好从头记起。
学一门语言最好的方式,是用它造一个东西;而造一个东西最好的方式,是造一门语言。这句话听着绕,但这个系列就是来把它跑通的。