一门叫 Lox 的小语言
要造一门语言,先得知道要造成什么样。这一章把 Lox 认全 —— 它小到你花五分钟就能读完全部语法,却大到能写递归、能写闭包。你会发现它长得像一个被削瘦的 JavaScript。最后我会解释一个刻意的选择:为什么用别人的语言,而不是自己发明一门。
五分钟看完一门语言
先别管怎么实现,先看它长什么样。这是一段完整的、能跑的 Lox:
// 这是注释
var name = "世界"; // 变量,动态类型
print "你好," + name; // 输出:你好,世界
var a = 1 + 2 * 3; // 算术,优先级和你想的一样
print a; // 7
if (a > 5) {
print "大";
} else {
print "小";
}
var i = 0;
while (i < 3) {
print i; // 0 1 2
i = i + 1;
}
for (var j = 0; j < 3; j = j + 1) {
print j; // 0 1 2
}
如果你写过任何一门 C 系语言(C、Java、JavaScript、Kotlin…),上面这段你一行都不用学。这就是重点:Lox 故意长成你已经熟悉的样子,好让你把注意力全放在「它是怎么被实现的」上,而不是「它的语法有什么怪癖」。
它有的东西
Lox 是一门动态类型的语言。值有这么几种:
// 数字:只有一种,双精度浮点(没有 int / float 之分) print 42; print 3.14; // 字符串 print "hello"; // 布尔 print true; print false; // 空值 print nil;
有函数,用 fun 声明,用 return 返回:
fun add(a, b) {
return a + b;
}
print add(3, 4); // 7
函数是头等公民 —— 可以赋值给变量、当参数传、当返回值。而一旦函数能返回函数,你就自动得到了闭包:
fun makeCounter() {
var count = 0;
fun tick() {
count = count + 1; // 记住了外面那个 count
return count;
}
return tick;
}
var c = makeCounter();
print c(); // 1
print c(); // 2
print c(); // 3
这段代码运行的输出,确实是 1、2、3:
1 2 3
这个例子,是整本书的一个小北极星。那个 tick 函数被 return 出来之后,外面的 makeCounter 明明已经结束了,可 tick 居然还记得、还能改那个 count。这件事一点都不显然 —— 它需要你在实现里做对好几件事。等我们在卷 IV 亲手把它跑通(第 15 章),你会对「闭包」这个词有一种别处得不到的理解:因为你造过它。
数字 · 字符串 · 布尔 · nil · 变量 · 算术与比较 · and / or · if / else · while · for · print · 函数 · 递归 · 闭包。
就这些。但这些足够图灵完备 —— 你能用它写任何计算。少即是多:正因为清单短,你才可能在一本小书里把每一样都亲手实现一遍,而不是永远停在「看懂了」。
它没有的东西(以及为什么现在不要)
完整的 Lox(CI 里那个)还有类和继承。这本书刻意把类砍掉了 —— 不是因为它难,是因为它对「领略这条流水线的魅力」不是必需的,而且会让书变长、让你更晚尝到甜头。有了函数和闭包,你已经拥有了「造一门语言」最核心、最漂亮的那部分。类怎么加,CI 讲得很清楚,等你造完这一版、有了手感,那一章会读得很轻松。
它也没有:模块、标准库、异常语法、数组 / 字典这些内置数据结构。这些不是原理,是「装修」。原理你在这本书里学完,装修你随手就能往上加 —— 事实上,「给自己的语言加一个新特性」会变成一件让你上瘾的事。
为什么用 Lox,而不是自己发明一门
造语言的人,十有八九会忍不住自己设计语法。我们不这么干,有三个明确的理由:
其一,也是最重要的:这本书是《Crafting Interpreters》的引子。CI 用的就是 Lox。你在这里造一遍 jlox(树遍历版),等于给 CI 上半本做了一次快速预演 —— 语法你熟了、jlox 的整体骨架你在脑子里搭过了。那本厚书,会因此从「不知从何读起」变成「哦,这段我懂,往下看它怎么做得更严谨」。我们用同一门语言,是为了让你的学习无缝衔接。
其二,设计语言会分散注意力。一旦开始纠结「三元运算符要不要」「分号能不能省」,你就掉进了语言设计的兔子洞,而这本书要教的是实现,不是设计。用一门定好的语言,你能一门心思扑在流水线上。
其三,Lox 的设计本身很克制、很经典。它是 Bob Nystrom 为了教学精心裁剪的,每一个特性都「刚好够用来演示一类实现技术」。跟着它走,你不会遇到「这个语法特性纯粹是设计者的个人趣味」这种噪音。
可以。翻到第 15 章,那里有一个完整的 Lox REPL(就是本书造的解释器本人),你可以把上面任何一段粘进去、改一改、按运行。不过别急 —— 那台 REPL,是我们卷 II 到卷 IV 一章一章造出来的。先看它怎么造,再看它怎么跑,那个「跑起来」才有分量。
回流
把上面那些 Lox 片段,和你每天写的语言并排放:
JavaScript:几乎一模一样。var、fun(对应 function)、C 系的 if / while / for、动态类型、闭包 —— Lox 就像是 JS 去掉了原型链、去掉了一堆历史包袱之后剩下的核。你在 JS 里用得最顺的那些东西,Lox 都有,而且你即将知道它们是怎么实现的。
Kotlin:Lox 的动态类型和 Kotlin 的静态类型是它们最大的分野 —— 而「类型检查在哪一步做」恰恰是一个精彩的问题(Lox 不做,所以 1 + "a" 要等到运行时才报错;Kotlin 在编译时就拦下了)。等你造完求值器,会特别能体会到:类型系统,本质上是在流水线里加了一个「不真正运行、但假装运行一遍来挑错」的工位。这也是 CI 之后、你若想深入的一个大方向。
这一章的一句话
Lox 是一门被精心削瘦的语言:小到每一样都能亲手实现,全到足以写出闭包 —— 而我们用它,是为了让你顺着它走进 Crafting Interpreters。
下一章,我们把这台机器的骨架搭出来 —— 四个工位怎么串成一条线。