跳到正文
cajulang

Caju 的工作原理

从一个 .caju 文件到运行中的程序:编译器的各个阶段、值在内存中的布局、垃圾回收器、调用与 panic,以及编译器如何在你的浏览器中运行。这里展示的一切都来自真实的编译器。

从源码到可执行文件

编译器由一连串阶段组成,每个阶段都用 Caju 编写(compiler/*.caju)。跟随一个小程序走完这些阶段:选择一个阶段,看看它产出什么。

02

词法分析器

lexer.caju 把文本切分成词法单元:关键字、名称、运算符、数字和文本。每个词法单元都记录所在的行和列,之后的每个错误正是靠它指向正确的位置。换行会变成 NEWLINE 词法单元,用来结束语句。

示例程序 · main.caju
fn add(a: int, b: int) -> int {
return a + b
}
print(add(2, 3))
该阶段的产出: 词法单元
  1. fnKEYWORD 1:1
  2. addNAME 1:4
  3. (OP 1:7
  4. aNAME 1:8
  5. :OP 1:9
  6. intNAME 1:11
  7. ,OP 1:14
  8. bNAME 1:16
  9. :OP 1:17
  10. intNAME 1:19
  11. )OP 1:22
  12. ->OP 1:24
  13. intNAME 1:27
  14. {OP 1:31
  15. ↵NEWLINE 1:32
  16. returnKEYWORD 2:3
  17. aNAME 2:10
  18. +OP 2:12
  19. bNAME 2:14
  20. ↵NEWLINE 2:15
  21. }OP 3:1
  22. ↵NEWLINE 3:2
  23. printNAME 5:1
  24. (OP 5:6
  25. addNAME 5:7
  26. (OP 5:10
  27. 2NUMBER 5:11
  28. ,OP 5:12
  29. 3NUMBER 5:14
  30. )OP 5:15
  31. )OP 5:16
  32. ↵NEWLINE 5:17
  33. ∎EOF 6:1

能编译自己的编译器

Caju 编译器用 Caju 编写。为了得到第一个编译器,需要借助一个用 JavaScript 编写的小型编译器,且只用一次;此后,编译器就由它自己编译自己的源码。

  1. 1

    第 0 阶段

    bootstrap/*.js:最初的 JavaScript 编译器,由 Node 运行。

  2. 2

    第 1 阶段

    第 0 阶段编译 compiler/main.caju:得到一个由 JavaScript 编译器生成的原生 caju。

  3. 3

    第 2 阶段

    第 1 阶段编译同一份源码:这次得到的是由 Caju 编译器生成的编译器。

  4. 4

    第 3 阶段

    第 2 阶段再编译一次。第 2 阶段和第 3 阶段的产物必须逐字节一致:编译器能复现自身。

  5. 5

    caju.wasm

    使用 --target wasm 时,编译器把自己编译成 WebAssembly;caju.wasm 再编译自己,也会得到完全相同的字节。

cmp build/caju2 build/caju3 && echo okstage 2 == stage 3

除测试套件外,编译器的每次改动都要经过这项检查:如果第 2 阶段和第 3 阶段的产物不同,说明编译器的某处改动改变了编译器自身的含义。

内存中的值

每个值都能放进一个 64 位字。数字和布尔值就是这个字本身;其他一切都是指向运行时创建的对象的指针。

s := "caju"
s
●
str
len
4
byte
c
byte
a
byte
j
byte
u
NUL
0

指向 [长度][UTF-8 字节][0] 的指针。文本永不改变:+ 会生成一个新文本,相同的字面量在整个程序中共享。

垃圾回收器

运行时以 64 KB 的页分配对象,并用标记-清除算法回收,回收器用 Caju 编写(rt/gc.caju)。试试看:根是程序的栈和全局变量。

每个对象要么能从某个根到达,要么不能。点击“标记”。

根堆stack: liststack: userglobal: cache[]intarray*User"Ana"mapmap"old"structclosure
  • 尚未访问
  • 已标记(可达)
  • 垃圾
  • 已释放

回收器是保守式的:栈上或对象中任何看起来像指向某个对象的指针的字,都会让该对象保持存活。正因如此,在调用期间指针绝不能只存在于寄存器中,WebAssembly 版本也因此把每个需要跨调用存活的值都保存在内存中的栈上。当自上次回收以来分配的内存超过上次回收后存活的内存(至少 8 MB)时,就会开始新一轮回收。

调用、defer 与 panic

每次调用都把参数压入栈,每个函数都把局部变量保存在一个栈帧中。defer、panic 和 recover 是运行时的一部分。

↑ 调用方
局部变量
sum
list ●
参数
a = 2
b = 3
栈帧 ($fp) →
↓ 被调用方
局部变量
x
y ●
临时值
t ●
栈指针 ($sp) →

一次调用

调用方压入参数(如果是函数值,还会压入闭包)然后发起调用;被调用方在固定偏移处找到 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. 1

    发布包

    每个 GitHub 发布版本都包含 caju-wasm.tar.gz:编译器 caju.wasm、以文本形式提供的标准库和运行时,以及一个小型 JavaScript 宿主。

  2. 2

    Worker

    页面启动一个 Web Worker,加载 caju.wasm 和一个装有标准库的内存文件系统。

  3. 3

    编译

    你的文件被放入这个文件系统,caju build --target wasm 在 caju.wasm 内部运行,就像在 Linux 上一样:宿主负责响应它的系统调用(open、read、write……)。

  4. 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 代码,并获得结构化结果。