Roc 编译器 mono 快照测试解析:为什么顶级常量永远不会被 lambda 捕获
2026/9/18 14:01:34 网站建设 项目流程

Roc 编译器 mono 快照测试解析:为什么顶级常量永远不会被 lambda 捕获

【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc

导读

本文以 Roc 仓库中的 mono 快照测试 test/snapshots/mono_pure_lambda.md 为线索,逐区块拆解这份“纯 lambda”编译测试文件,说明它如何验证一个关键编译器行为:顶级(top-level)常量不会被 lambda 捕获,因此不会进入闭包的捕获集合。读完本文,你将掌握 Roc 快照测试文件的区块结构与阅读方法,理解e-closure/e-lookup-local等规范 IR(canonical IR)形态的区别,并能对照 src/snapshot_tool/main.zig 中的 mono 测试执行与校验流程,把这份快照当作调试编译器行为时的可复现样本。

一、快照文件整体结构与 mono 测试的定位

Roc 仓库的 test/snapshots/ 目录存放大量编译快照测试。每份快照是一个 Markdown 文件,用#标题将一次编译过程切成多个“观察点”,从源码到词法、语法、规范 IR、类型推断逐层记录结果。mono_pure_lambda.md属于mono类型测试:在 src/snapshot_tool/main.zig 的NodeType枚举中,monofilepackageplatformappsnippetreporting一样属于“完整模块”级测试(见 src/snapshot_tool/main.zig 的注释“Snippet, mono, and reporting tests are full modules”)。

整份文件共十个区块:

区块内容
META测试元信息:description一句话概括测试意图,type=mono标记测试类型
SOURCE被测的原始 Roc 源码
MONO单态化(monomorphize)后的类型化表示
FORMATTED格式化结果,NO CHANGE表示格式化前后无差异
EXPECTED期望输出,NIL表示无
PROBLEMS编译诊断问题列表,NIL表示无
TOKENS词法分析产出的 token 流
PARSE语法分析产出的 AST(s-expression 形式)
CANONICALIZE规范 IR(canon IR),即去糖、消解后的中间表示
TYPES类型推断结果(inferred types)

执行快照测试时,工具会按固定顺序输出区块;对 mono 测试,顺序是META, SOURCE, MONO, FORMATTED之后紧跟其余区块(见 src/snapshot_tool/main.zig)。

二、被测源码:一行常量加一个纯 lambda

SOURCE区块给出了三段极简的顶层声明:

one = 1 add_one = |x| x + one result = add_one(5)
  • one = 1顶级常量,位于模块顶层(type-mod),并非任何函数体内的局部变量;
  • add_one = |x| x + one:Roc 的 lambda 语法,|x|是参数列表,函数体引用x(自身参数)与one(外部名字);
  • result = add_one(5):对 lambda 应用整数5

这份测试的主题即description所述:“top-level constants are never captured by lambdas”(顶级常量永远不会被 lambda 捕获)。one虽然被 lambda 的函数体引用,但它属于模块作用域,而不是某个外层函数调用帧中的局部变量,因此编译器在规范 IR 中处理它的方式与处理“局部变量捕获”有本质区别——这正是后文CANONICALIZE对比的重点。

三、MONO 输出:单态化后的类型标注视图

one : Dec one = 1 add_one = |x| x + one result : Dec result = add_one(5)

MONO是 mono 测试特有的产物,它给出“单态化(monomorphized)后的类型字符串”。在 src/snapshot_tool/main.zig 的注释中明确写着“Get the defaulted (monomorphized) type string for an expression”。可以看到:

  • one : Dec:整数字面量1默认解析为Dec(十进制小数类型),所以顶级常量one被标注为Dec
  • add_one未附加类型标注:说明单态化后add_one仍是多态的(详见第六节类型推断);
  • result : Decadd_one(5)的应用结果单态化为Dec

值得强调的是,MONO输出并非随意打印,工具会对它做一次完整校验:validateMonoOutput会把 MONO 文本当作“无 header 的 type module”重新解析、规范化并类型检查(见 src/snapshot_tool/main.zig)。也就是说,这份带标注的代码必须本身是合法、可类型检查的 Roc 代码,测试才算通过。

四、词法与语法证据:TOKENS 与 PARSE

TOKENS区块用逗号分隔的 token 名记录了词法流:

LowerIdent,OpAssign,Int, LowerIdent,OpAssign,OpBar,LowerIdent,OpBar,LowerIdent,OpPlus,LowerIdent, LowerIdent,OpAssign,LowerIdent,NoSpaceOpenRound,Int,CloseRound, EndOfFile,

三行分别对应三条声明:

  1. LowerIdent(one) OpAssign(=) Int(1)
  2. LowerIdent(add_one) OpAssign(=) OpBar(|) LowerIdent(x) OpBar(|) LowerIdent(x) OpPlus(+) LowerIdent(one)——注意 lambda 的|x|由两个OpBar包裹参数名,函数体是x + one
  3. LowerIdent(result) OpAssign(=) LowerIdent(add_one) NoSpaceOpenRound(5) CloseRound——NoSpaceOpenRound表示调用括号紧跟被调用者,无空格(Roc 的调用语法)。

PARSE区块给出了等价的 AST:

(file (type-mod) (statements (s-decl (p-ident (raw "one")) (e-int (raw "1"))) (s-decl (p-ident (raw "add_one")) (e-lambda (args (p-ident (raw "x"))) (e-binop (op "+") (e-ident (raw "x")) (e-ident (raw "one"))))) (s-decl (p-ident (raw "result")) (e-apply (e-ident (raw "add_one")) (e-int (raw "5"))))))

关键结构一目了然:文件根节点带type-mod(类型模块);三条s-decl声明中,add_one对应e-lambda(参数x、函数体是e-binop "+"连接e-ident "x"e-ident "one"),result对应e-apply(调用add_one,实参为整数5)。语法层面此时并不区分“捕获”与“普通引用”,该区分发生在下一阶段的规范 IR 中。

五、核心主题:CANONICALIZE 证明顶级常量不进捕获集

规范 IR(CANONICALIZE区块)是这份快照真正想“说话”的地方:

(can-ir (d-let (p-assign (ident "one")) (e-num (value "1"))) (d-let (p-assign (ident "add_one")) (e-lambda (args (p-assign (ident "x"))) (e-dispatch-call (method "plus") (constraint-fn-var 222) (receiver (e-lookup-local (p-assign (ident "x")))) (args (e-lookup-local (p-assign (ident "one"))))))) (d-let (p-assign (ident "result")) (e-call (constraint-fn-var 234) (e-lookup-local (p-assign (ident "add_one"))) (e-num (value "5")))))

注意三点:

  1. 每条顶层声明变成d-let(let 绑定),one的值是e-num (value "1")
  2. add_one的 lambda 体把x + one编译成e-dispatch-call (method "plus")x是 receiver,one是参数,二者都以e-lookup-local方式按名查表;
  3. 关键差异:这里的 lambda 节点没有任何e-closure包装,也没有captures列表one作为顶级常量,在规范 IR 中被当作可直接按名查找(e-lookup-local)的全局定义,而不是需要随闭包对象携带的捕获值。

把它与同目录下的“局部变量捕获”快照对比,差异立刻显现。在 test/snapshots/mono_closure_single_capture.md 中,外层函数参数x被内层 lambda 引用,其规范 IR 出现:

(s-let (p-assign (ident "add_x")) (e-closure (captures (capture (ident "x"))) (e-lambda ...)))

内层 lambda 被显式包进e-closure,并列出捕获集合(capture (ident "x"))。多变量版本 test/snapshots/mono_closure_multiple_captures.md 同样出现(capture (ident "a")) (capture (ident "b"))

由此可以得出这条编译器行为链:函数参数、函数体内 let 绑定的局部变量被内层 lambda 引用时,编译器会构建e-closure并记录捕获列表;而模块顶层声明的常量被引用时,只生成e-lookup-local查找,永不进入任何闭包的捕获集合。从源码结构看,这与 Roc 的模块语义一致——顶级常量是全局可寻址的定义,其生命周期覆盖整个模块,无需闭包为其保留环境快照。这既是本快照description的验证目标,也是理解 Roc 闭包内存布局(捕获集大小即闭包负载)的起点。

六、TYPES:类型推断与约束求解

TYPES区块给出推断类型:

(inferred-types (defs (patt (type "Dec")) (patt (type "a -> a where [a.plus : a, Dec -> a]")) (patt (type "Dec"))) (expressions (expr (type "Dec")) (expr (type "a -> a where [a.plus : a, Dec -> a]")) (expr (type "Dec"))))
  • one : Dec:字面量1默认单态化为Dec
  • add_one : a -> a where [a.plus : a, Dec -> a]:由于oneDecx + one中的+(即plus方法)要求“a加上一个Dec仍得到a”,于是add_one保持多态a -> a,但携带约束a.plus : a, Dec -> a。也就是说,a在“存在与Dec相加能力”的数字类型范围内自由;
  • result : Dec:以5Dec)实例化add_one后得到Dec

对照 test/snapshots/mono_arithmetic.md 可以看到同样模式:sum = 1 + 2推断为Dec,规范 IR 中是e-dispatch-call (method "plus")作用于两个e-num。而涉及闭包的 test/snapshots/mono_closure_single_capture.md 里,类型约束还会出现from_numeral(数字字面量转换)约束,本测试因one已显式为Dec而省去了该约束的复杂度。

七、FORMATTED / EXPECTED / PROBLEMS:格式化与诊断基线

  • FORMATTED输出NO CHANGE:源码本身已符合 Roc 格式化规范,格式化前后无任何改动。快照测试会把源码交给格式化器,任何非规范书写(如多余空格)都会导致此区块出现改写后的代码,从而暴露格式化回归;
  • EXPECTED输出NIL:该测试没有独立的“期望输出”,mono 测试的断言对象主要是MONO/CANONICALIZE/TYPES等中间产物;
  • PROBLEMS输出NIL:编译全程未产生任何诊断问题(无类型错误、无解析错误),与第六节推断结果吻合。

八、背后的测试基础设施

这份快照不是孤立的文本,它在 src/snapshot_tool/main.zig 中被真实执行:

  • 类型识别# MONO\n~~~roc\n是区块标题常量(src/snapshot_tool/main.zig),遇到即标记为 mono 节点;
  • 执行路径:mono 测试走“完整模块”路径,先对SOURCE做解析、规范化和类型检查,再生成MONO区块,并对MONO文本调用validateMonoOutput重新校验(src/snapshot_tool/main.zig),确保单态化输出本身是合法可编译的 Roc;
  • 单态化取型:通过“Get the defaulted (monomorphized) type string”的接口从活动符号表中取出每个定义/表达式的单态化类型字符串(src/snapshot_tool/main.zig);
  • 已知限制:源码中以 TODO 注明“mono 测试暂未运行常量折叠(constant folding),待 zig-16 分支的 ComptimeEvaluator 就绪后补上”(src/snapshot_tool/main.zig),这说明当前MONO输出反映的是未做编译期求值的真实单态化视图。

九、如何利用这份快照做进一步验证

读者可以沿两条路径继续深入:

  1. 横向对比同一测试家族:把本文件与 test/snapshots/mono_closure_single_capture.md、test/snapshots/mono_closure_multiple_captures.md、test/snapshots/mono_nested_closures.md 并排阅读,可以系统观察“捕获数量从 0 → 1 → 多”时e-closure/captures的增长形态,从而理解 Roc 闭包表示与内存布局;
  2. 运行快照测试:在构建好编译器的环境下执行快照测试工具(参见仓库根目录 README.md 与构建文档 BUILDING_FROM_SOURCE.md),对本文件做增删捕获变量的改动后重新生成快照,即可亲手验证“顶级常量不进入捕获集”这一边界行为——注意仓库为只读,实验应在本地副本中进行。

总结

mono_pure_lambda.md用十一个区块、二十余行文本完整记录了一次“纯 lambda”编译过程:SOURCE定义顶级常量one与引用它的 lambda,MONO给出单态化类型标注,TOKENS/PARSE呈现词法与语法证据,CANONICALIZE以“无e-closure、仅e-lookup-local”的形态证明顶级常量不被捕获,TYPES展示约束求解结果。作为type=mono快照家族的一员,它与 test/snapshots/mono_closure_single_capture.md 等文件互为镜像,共同界定了 Roc 闭包捕获系统的语义边界:捕获只针对局部环境,全局常量永远按名解析

【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询