我以为编译器很可怕,于是我做了 Sauce。
Source: Dev.to

我已经写代码多年了。我敲下 cargo run 或 npm start,按下 Enter,然后有意义的事情就会发生。但如果你问我在按下 Enter 与看到 “Hello World” 之间到底发生了什么,我可能会含糊其辞地说“机器码”,然后转移话题。
这让我很困扰。我每天都依赖这些工具,却并不真正了解它们。
于是我开始用 Rust 构建 Sauce,我的个人编程语言。并不是因为世界需要另一种语言,而是因为我需要停止把编译器当作黑盒子。
事实证明,语言并不是魔法。它只是一条流水线。
为什么我们认为这很难
我们通常把编译器看成一个庞大、可怕的大脑,它会评判我们的代码。你把文本喂给它,它要么给你一个可运行的程序,要么用错误信息大声斥责你。
我花了多年时间认为要构建编译器必须是数学天才。我错了。你只需要把它拆分成小步骤。
Sauce 实际上是什么
去掉炒作,编程语言不过是一种移动数据的方式。
Sauce 是一种 静态类型 的语言,却像简单脚本一样。 我想要一种清晰诚实的语言。核心理念很简单:
- 管道 (
|>) 为默认 – 数据显式地从一步流向下一步,像工厂流水线一样。 - 副作用是显式的 (
toss) – 代码中没有隐藏的惊喜或秘密跳转。
但要实现这一点,我必须先构建引擎。多亏了 Rust,我发现这个引擎其实相当酷。
架构:它只是一条装配线
我曾经以为编译是一个巨大的、混乱的函数。实际上,它是一个有纪律的过程。我遵循一种严格的 Sauce 架构,将 “理解代码”(前端)与 “运行代码”(后端)分离。
下面就是 Sauce 在底层的工作方式。上面的图不仅仅是草图;它是我正在构建的地图。整个过程分为两个主要阶段。
阶段 1:前端(大脑)
此阶段全部关于 理解 你写的代码。它还不运行任何东西,只是读取并检查。
Lexer (Logos) – 切块机
- 职责: 计算机不读取单词,它们读取字符。lexer 将字符分组为有意义的块,称为 tokens(标记)。
- 通俗解释: 想象读取没有空格的句子:
thequickbrownfox。这很困难。lexer 会添加空格并给每个词贴标签。它把grab x = 10转换为列表:
[Keyword(grab), Ident(x), Symbol(=), Int(10)]。 - 工具: 我使用了 Rust crate Logos。它快得惊人,但我也学到了一个硬教训:计算机很笨。如果你不明确告诉它们
grab是特殊关键字,它们会把它当作普通标识符(比如green)来处理。必须设定严格的规则。
Parser (Chumsky) – 语法警察
- 职责: 手里有了 token 列表后,需要检查它们是否构成合法的句子。parser 将平坦的列表组织成结构化的树,称为 AST(抽象语法树)。
- 通俗解释: 像
[10, =, x, grab]这样的列表包含合法的单词,却毫无意义。parser 确保顺序正确(grab x = 10),并构建层级结构:“这是一次变量赋值。变量名是x,值是10。” - 工具: 我使用 Chumsky,它让你像搭 LEGO 积木一样构建逻辑。你写一个读取数字的小函数,另一个读取变量的函数,然后把它们粘合在一起。
- 恍然大悟的时刻: 将语法拆分为小的、可组合的块,使语言的扩展和推理变得容易得多。这不是魔法,只是对数据的组织。
Type Checking – 逻辑检查
- 职责: 语法正确的句子不一定有意义。“三明治吃了星期二”在句法上是合法的,但在语义上是胡说。类型检查器捕捉这些逻辑错误。
- 通俗解释: 如果你写
grab x = "hello" + 5,parser 会说“看起来是合法的数学运算!”,但类型检查器会介入并说:“等等。你不能把字符串和数字相加。这是非法的。”Sauce 目前拥有一个小而明确的系统,在代码真正运行之前捕获这些基本错误。
阶段 2:后端(肌肉)
当前端给出 “批准” 以后,我们进入后端。此阶段是 让代码真正运行。
Codegen (Inkwell/LLVM) – 翻译器
- 职责: 这里我们离开 “变量” 与 “管道” 的高级世界,进入 CPU 指令的低级世界。我们把 AST 翻译成 LLVM IR(中间表示)。
- 通俗解释: Sauce 像是给出指令的高级经理(“计算这个管道”),CPU 是只懂基本任务的工人(“把数字移动到寄存器 A”, “把寄存器 A 与 B 相加”)。LLVM 就是把经理的指令翻译成工人检查清单的翻译器。
- 为什么选 LLVM? 它是同样用于 Rust、Swift 和 C++ 的工业级机器。使用它,Sauce 可以免费获得数十年的优化成果。一旦你弄清楚如何让 LLVM “打印一个数字”,其余的工作就会变得相对轻松。
一切就位。
感觉如此恐怖
Native Binary: 最终产物
- 任务:
最后一步是将所有 CPU 指令打包成一个独立的文件(比如 Windows 上的.exe或 Linux 上的二进制文件)。 - 通俗解释:
这就是让你可以把程序发送给朋友的方式。他们不需要安装 Sauce、Rust 或其他任何东西。他们只需双击该文件,即可运行。(目前,这仅适用于简单的、无副作用的程序。)
现在可用的功能 (v0.1.0)
Sauce 不再只是一个想法——核心编译器管线已经可以运行。
- 管线(Pipelines):
你可以写grab x = 10 |> _,它能完美理解。数据从左到右流动,就像阅读一句话一样。 - 真实输出(Real Output):
你可以输入真实的.sauce源代码,它会将其解析为类型安全的语法树。 - 显式副作用(Explicit Effects):
你可以使用toss来标记副作用。这目前在解释器中可用,而 LLVM 后端暂时会拒绝副作用。
前进的路线
我对接下来的发展有明确的计划。由于核心架构已经稳定,接下来的更新将侧重于提升可用性。
- v0.1.x (UX):
目前错误信息有点晦涩。我将加入一个名为 Ariadne 的工具,以提供漂亮且有帮助的错误报告(类似 Rust 的做法)。 - v0.2.0 (Effects):
关键更新。我会完善 “Effects” 的工作方式——定义在错误后何时可以恢复程序以及何时必须中止的规则。 - v0.3.0 (Runtime):
将解释器和 LLVM 的实现合并,使它们的行为完全一致,并添加标准库,让你能够做的不止是打印数字。
为什么你应该尝试这个
我多年里一直回避构建语言,因为我觉得自己不够聪明。
但构建 Sauce 让我明白,根本没有魔法。它只是数据结构:
- 词法分析器不过是正则表达式。
- 语法解析器不过是树构建器。
- 解释器不过是遍历这棵树的函数。
如果你真的想了解代码是如何运行的,别只读书。动手做一个小而不完整的编译器。创建一个词法分析器。定义一棵简单的语法树。解析 1 + 1。
在一个周末里与语法错误搏斗,你学到的东西会比整整一年只会 cargo run 多得多。
查看 Sauce on GitHub。它小巧、诚实,而且我们已经正式“烹饪”。
