Caju 的工作原理
从一个 .caju 文件到运行中的程序:编译器的各个阶段、值在内存中的布局、垃圾回收器、调用与 panic,以及编译器如何在你的浏览器中运行。这里展示的一切都来自真实的编译器。
从源码到可执行文件
编译器由一连串阶段组成,每个阶段都用 Caju 编写(compiler/*.caju)。跟随一个小程序走完这些阶段:选择一个阶段,看看它产出什么。
词法分析器
lexer.caju 把文本切分成词法单元:关键字、名称、运算符、数字和文本。每个词法单元都记录所在的行和列,之后的每个错误正是靠它指向正确的位置。换行会变成 NEWLINE 词法单元,用来结束语句。
fn add(a: int, b: int) -> int { return a + b} print(add(2, 3))- fnKEYWORD 1:1
- addNAME 1:4
- (OP 1:7
- aNAME 1:8
- :OP 1:9
- intNAME 1:11
- ,OP 1:14
- bNAME 1:16
- :OP 1:17
- intNAME 1:19
- )OP 1:22
- ->OP 1:24
- intNAME 1:27
- {OP 1:31
- ↵NEWLINE 1:32
- returnKEYWORD 2:3
- aNAME 2:10
- +OP 2:12
- bNAME 2:14
- ↵NEWLINE 2:15
- }OP 3:1
- ↵NEWLINE 3:2
- printNAME 5:1
- (OP 5:6
- addNAME 5:7
- (OP 5:10
- 2NUMBER 5:11
- ,OP 5:12
- 3NUMBER 5:14
- )OP 5:15
- )OP 5:16
- ↵NEWLINE 5:17
- ∎EOF 6:1
能编译自己的编译器
Caju 编译器用 Caju 编写。为了得到第一个编译器,需要借助一个用 JavaScript 编写的小型编译器,且只用一次;此后,编译器就由它自己编译自己的源码。
- 1
第 0 阶段
bootstrap/*.js:最初的 JavaScript 编译器,由 Node 运行。 - 2
第 1 阶段
第 0 阶段编译
compiler/main.caju:得到一个由 JavaScript 编译器生成的原生caju。 - 3
第 2 阶段
第 1 阶段编译同一份源码:这次得到的是由 Caju 编译器生成的编译器。
- 4
第 3 阶段
第 2 阶段再编译一次。第 2 阶段和第 3 阶段的产物必须逐字节一致:编译器能复现自身。
- 5
caju.wasm
使用
--target wasm时,编译器把自己编译成 WebAssembly;caju.wasm再编译自己,也会得到完全相同的字节。
除测试套件外,编译器的每次改动都要经过这项检查:如果第 2 阶段和第 3 阶段的产物不同,说明编译器的某处改动改变了编译器自身的含义。
内存中的值
每个值都能放进一个 64 位字。数字和布尔值就是这个字本身;其他一切都是指向运行时创建的对象的指针。
s := "caju"指向 [长度][UTF-8 字节][0] 的指针。文本永不改变:+ 会生成一个新文本,相同的字面量在整个程序中共享。
垃圾回收器
运行时以 64 KB 的页分配对象,并用标记-清除算法回收,回收器用 Caju 编写(rt/gc.caju)。试试看:根是程序的栈和全局变量。
每个对象要么能从某个根到达,要么不能。点击“标记”。
- 尚未访问
- 已标记(可达)
- 垃圾
- 已释放
回收器是保守式的:栈上或对象中任何看起来像指向某个对象的指针的字,都会让该对象保持存活。正因如此,在调用期间指针绝不能只存在于寄存器中,WebAssembly 版本也因此把每个需要跨调用存活的值都保存在内存中的栈上。当自上次回收以来分配的内存超过上次回收后存活的内存(至少 8 MB)时,就会开始新一轮回收。
调用、defer 与 panic
每次调用都把参数压入栈,每个函数都把局部变量保存在一个栈帧中。defer、panic 和 recover 是运行时的一部分。
一次调用
调用方压入参数(如果是函数值,还会压入闭包)然后发起调用;被调用方在固定偏移处找到 n 个参数中的第 i 个,并为局部变量腾出空间。第 2、3……个返回值通过 g.ret 传递。
深度限制
一个剩余调用次数的计数器从 10,000 开始,每次调用减一:第 10,001 层嵌套调用会以 "Too much recursion" 终止,而不是崩溃。
defer
含有 defer 的函数会把一条记录链入一个链表(rt_dtop);每个 defer 添加一个节点,保存函数及其已求值的参数。函数返回时,它们按从后到前的顺序执行。
panic 与 recover
panic 从最新的记录到最旧的记录依次执行被推迟的调用。如果其中某个调用了 recover(),执行会跳回推迟它的那个函数,该函数正常返回。在 WebAssembly 中,这次跳转是一个 wasm 异常。
浏览器中的 Caju
在线运行页面运行的是编译成 WebAssembly 的真实编译器,无需服务器。
- 1
发布包
每个 GitHub 发布版本都包含
caju-wasm.tar.gz:编译器caju.wasm、以文本形式提供的标准库和运行时,以及一个小型 JavaScript 宿主。 - 2
Worker
页面启动一个 Web Worker,加载
caju.wasm和一个装有标准库的内存文件系统。 - 3
编译
你的文件被放入这个文件系统,
caju build --target wasm在caju.wasm内部运行,就像在 Linux 上一样:宿主负责响应它的系统调用(open、read、write……)。 - 4
运行
新模块在同一个 worker 中运行;它写出的内容会实时传回页面。点击“停止”会终止 worker。
编译器周边
caju test
查找 *_test.caju 文件并运行:使用 std/testing 中的 describe、it 和 expect。
caju pkg
包就是 GitHub 仓库;采用最小版本选择,并用带 SHA-256 哈希的 caju.lock 锁定版本。
caju mcp
一个 Model Context Protocol 服务器:AI 助手可以检查、运行和测试 Caju 代码,并获得结构化结果。