你每天在用它,却从没见过它
你写下的一行代码,在被运行之前,只是一段文本 —— 一串字符,死的。是有一个程序去读它、给它赋予意义,它才动起来。你每天依赖这个程序,它却从不露面。这一章不写任何代码,只做一件事:把这个程序的全貌先摆给你看。看清了全貌,接下来十九章就是把它一格一格造出来。
先承认一件事:代码不会自己跑
你在编辑器里敲下这一行:
let price = base * (1 + taxRate);
然后你按下运行。它算出了结果。但你有没有停下来想过:中间到底发生了什么?
这一行,对你的硬盘来说,只是二十几个字符 —— l、e、t、空格、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,很小,但麻雀虽小五脏俱全。