卷 I · 源CH 01深度 01/20

你每天在用它,却从没见过它

你写下的一行代码,在被运行之前,只是一段文本 —— 一串字符,死的。是有一个程序去它、给它赋予意义,它才动起来。你每天依赖这个程序,它却从不露面。这一章不写任何代码,只做一件事:把这个程序的全貌先摆给你看。看清了全貌,接下来十九章就是把它一格一格造出来。

解释器编译器流水线心智模型

先承认一件事:代码不会自己跑

你在编辑器里敲下这一行:

let price = base * (1 + taxRate);

然后你按下运行。它算出了结果。但你有没有停下来想过:中间到底发生了什么?

这一行,对你的硬盘来说,只是二十几个字符 —— let、空格、p…… 它和一句英文、一段乱码,在存储上没有任何区别。它不知道 * 是乘法,不知道 base 是个变量,不知道括号意味着「先算这里」。这些「意义」,字符本身一点都不携带。

可它偏偏就跑起来了,还算对了。那么,是谁在这堆死字符里,读出了「先乘、再加、赋值」这套意思?

答案是:有一个程序。它可能叫编译器(compiler),可能叫解释器(interpreter),可能藏在 node 里、藏在浏览器引擎里、藏在 JVM 里。它的工作,就是读你的代码,然后让它有意义。你写了很多年代码,用了它很多年 —— 但你大概从没见过它长什么样。

◆ 这本书要做的事

把这个你天天用、却从没见过的程序,亲手造一个出来。

不是照着抄一个庞然大物,是造一个小到能完全装进脑子、但五脏俱全、真的能跑的。造完之后,你再看任何一门语言,都会知道盖子底下是什么。

它不是魔法,是一条流水线

「读代码、赋予意义」听起来很玄。但真相非常朴素:它是一条流水线,原料从一头进去(你写的文本),成品从另一头出来(跑出的结果),中间经过几个工位,每个工位只干一件很具体的事。

就拿最简单的 1 + 2 * 3 来说,它要经过这么几站:

工位输入干的事输出
① 扫描
scan / lex
字符流
"1 + 2 * 3"
把字符切成一个个「词」记号流
1 + 2 * 3
② 解析
parse
记号流按语法把词搭成一棵树语法树
(+ 1 (* 2 3))
③ 求值
evaluate
语法树沿着树走一遍,算出来
7

就这三步。切词、搭树、走一遍树。你每天按下的那个「运行」,本质就是这条流水线跑了一遍。

说得再多,不如直接看它跑。下面这台机器,就是一条真的、完整的流水线 —— 它不是示意图,是本书接下来要造的那台解释器本人。左上角选一个例子,看同一段源码,在四个工位里分别变成什么:

盯着「语法树 AST」那一格看一会儿。那棵树,是整条流水线的枢纽。你写的文本是扁平的一行,但它表达的意思是有层次的(先算 2 * 3,再加 1)—— 这个层次,就藏在树的形状里。扫描器和解析器忙活半天,就是为了把那个藏起来的层次显式地搭出来;而求值器要做的,只是老实地沿着这棵树走一遍

「编译」和「解释」,差在最后一步

你可能听过「编译型语言」和「解释型语言」的争论。其实前两步(切词、搭树)它们一模一样,区别只在拿到语法树之后:

  • 解释器:直接走一遍树,边走边算,当场出结果。我们卷 IV 造的就是这个。简单、直接、启动快。
  • 编译器:不急着算,先把树翻译成另一种更低级的代码(机器码、字节码),之后再由别人去跑。我们卷 V 会尝一口这个。翻译一次、运行多次,所以快。

所以「编译 vs 解释」不是两种物种,是同一条流水线在最后一段的两个分岔。真实的语言常常两者都用:Java 先编译成字节码(编译),再由 JVM 解释 / 即时编译着跑(解释 + 编译)。你现在纠结的那个分类,学完这本书会自动溶解掉。

✎ 一个会让你舒服很多的事实

这条流水线的每一段,都是几十年前就被研究透了的、有名字的、有标准做法的。你不是在探索无人区。扫描器是一台状态机、解析器有一套叫「递归下降」的写法、求值器就是递归遍历一棵树 —— 每一段都能用一两百行普通代码写完。让编译器显得高深的,从来不是原理,是工业级实现里那些为了「快」和「全」而堆上去的东西。原理本身,朴素得很。

为什么值得亲手造一遍

你可能会问:我又不打算去写编译器,了解这个有什么用?

三个理由,一个比一个实在:

其一,你每天都在这条流水线的某一段上工作,只是没意识到。写正则、解析 JSON、配 Babel / KSP、看一个语法错误、调一个诡异的运算符优先级问题 —— 这些全是流水线上的活。造过一遍之后,这些事从「凭经验」变成「我知道它在干嘛」。

其二,这是「看穿一个系统」这件事最纯粹的一次训练。一门语言,是软件里最像魔法的东西之一。当你把这个魔法亲手拆开、重装一遍,你对「任何复杂系统底下都是一堆朴素机制」这件事,会有一种再也回不去的笃定。

其三,它非常好玩。没有多少编程任务,能像「造一门语言」这样,让你在一两百行之后,就得到一个会听你话的、活的东西。这本书的最后,你会在浏览器里敲下一段自己语言写的程序,按回车,看它跑出来。那个瞬间,值回票价。

回流

⟲ 回流 · 你今天就用过这条流水线

这一章还没写代码,但「流水线」这个模型,你已经能拿去用了。回想一下今天:

你看到过一个报错吗?下次注意看它是哪一段喊出来的。Unexpected token / 意外的记号 —— 那是解析器在说话(它拿到一个不该在这儿的词)。Unterminated string / 字符串未结束 —— 那是更早的扫描器undefined is not a function —— 那是最后的求值器(前两步都过了,跑起来才发现不对)。报错发生在哪一段,直接告诉你 bug 是哪种性质的。

你用过 JSON.parse 吗?那就是一条微缩流水线:把 JSON 文本切词、搭成对象树。你写过正则吗?正则引擎自己也要先「解析」你的正则表达式。「读文本、赋予结构」这件事,在你的日常里到处都是 —— 这本书只是把它最完整的那个版本,摊开给你看。

这一章的一句话

代码在跑起来之前只是文本,而让文本有意义的那个程序,是一条你能亲手造出来的流水线:切词、搭树、走一遍树。

下一章,我们先认识一下要造的那门小语言 —— 它叫 Lox,很小,但麻雀虽小五脏俱全。