【免费下载链接】roc
A fast, friendly, functional language.
Roc 是一门快速、友好、函数式的编程语言(仓库自述 "A fast, friendly, functional language"),其编译器源码仓库以 Zig 实现,并配套了一套"快照测试(snapshot tests)"体系来锁定编译器的每一阶段行为。本文以仓库中的快照文件 test/snapshots/expr/two_arg_closure.md 为骨架,逐段拆解|_, _| 42这样一个双参数闭包从词法分析、语法解析、格式化、规范化到类型推断的完整编译流水线。读完本文,你将掌握 Roc 编译器快照文件的格式约定与读写方法,理解闭包与下划线占位参数在源码层面的表示形态,并能据此自行编写、更新和排查expr类快照测试。
快照测试是什么:为什么编译器需要它
Roc 编译器是一个多阶段流水线:源码先被词法分析为 token 流,再被语法分析为 AST,随后经过**规范化(canonicalize)**得到 CIR,最后交由类型检查与各后端(LLVM、Wasm、x86_64/aarch64 等)生成目标代码。任何一个阶段的输出发生变化,都可能引发难以察觉的回归。
快照测试的职责,正如 test/snapshots/README.md 所述:通过把每个编译阶段针对特定 Roc 代码样例的输出固化下来,检测编译器行为发生意外变化时的回归。每个快照文件同时记录"期望输出",一旦编译器输出与快照不符,测试即可精确定位到是哪个阶段出了问题。
值得强调的是,快照测试分为两类(见 test/snapshots/README.md 的 "Semantic diagnostics vs renderer output" 一节):
- 普通快照(
type=file、snippet、expr等):捕获诊断的语义。PROBLEMS段存放每个reporting.Report的规范化 S-expression 序列化(见src/reporting/report_sexpr.zig),不包含任何渲染器细节(无边框字符、ANSI 转义、换行或标记),NIL表示编译没有产生任何报告。它回答的问题是:编译器是否产生了正确的诊断? - 报告快照(
type=reporting,位于reporting/目录):固定渲染器的输出,逐节固化REPORT、CLI、MARKDOWN、HTML、LSP各格式的布局细节。
本文讨论的 two_arg_closure.md 属于普通快照中的expr类型。
快照文件的七段式结构
每个expr快照文件由若干以#开头的段落构成,two_arg_closure.md完整展示了全部七段。下表汇总了各段的作用:
| 段落 | 作用 | 本例内容 |
|---|---|---|
# META | INI 格式元信息,声明description与type | description=two_arg_closure,type=expr |
# SOURCE | 被编译的 Roc 源码片段 | \|_, _\| 42 |
# EXPECTED | 期望的诊断/错误(语义诊断的 S-expression) | NIL(无错误) |
# PROBLEMS | 实际产生的问题报告 | NIL(无问题) |
# TOKENS | 词法分析产出的 token 序列 | OpBar,Underscore,Comma,Underscore,OpBar,Int,EndOfFile |
# PARSE | 语法分析产出的 AST(Clojure 风格 S-expression) | (e-lambda ...) |
# FORMATTED | 格式化器的输出 | NO CHANGE(格式已规范) |
# CANONICALIZE | 规范化后的 CIR(含类型名解析后的常量) | (e-lambda (args (p-underscore) (p-underscore)) (e-num (value "42"))) |
# TYPES | 类型推断结果 | _arg, _arg2 -> a,带from_numeral约束 |
注:部分快照还可能出现
type=file(整个文件编译)、type=snippet(如 test/snapshots/001.md 同时带FORMATTED段)等变体;repl类型快照则额外支持--trace-eval解释器跟踪(见 test/snapshots/README.md)。
核心示例:|_, _| 42到底是什么
|_, _| 42是 Roc 中一个双参数闭包:|与|之间是参数列表,两个参数都用下划线_占位(即"忽略该参数"),函数体直接返回整数常量42。它不引用任何参数,因此是最简形式的闭包,非常适合用来观察编译流水线如何处理"参数 + 常量"结构。
逐段解读快照内容:
1. META 与 SOURCE
description=two_arg_closure type=expr|_, _| 42type=expr表示这是一个表达式级快照——编译单元是单个表达式而非整个文件(文件级样例可对比 test/snapshots/001.md 中type=snippet的foo = "one"声明)。EXPECTED与PROBLEMS均为NIL,说明该表达式合法且无任何诊断报告。
2. TOKENS:词法阶段
OpBar,Underscore,Comma,Underscore,OpBar,Int, EndOfFile,词法器把源码切成 7 个 token:两个OpBar(|)、两个Underscore(_)、一个Comma(,)、一个Int(42)以及文件结束符EndOfFile。注意两点:
_在 Roc 词法中是独立 tokenUnderscore,而不是标识符;42被识别为Int(整数字面量),token 定义可在 src/parse/tokenize.zig 中看到OpBar的枚举项。
3. PARSE:语法阶段生成 e-lambda AST
(e-lambda (args (p-underscore) (p-underscore)) (e-int (raw "42")))解析器把OpBar ... OpBar识别为 lambda 表达式节点e-lambda,其args子节点是两个下划线模式p-underscore(模式 kernel 的underscore变体),函数体是整数表达式e-int (raw "42")——raw保留源码中的原始文本"42"。
这个 AST 的 S-expression 序列化实现在 src/parse/AST.zig:e-lambda节点先 pushargs标签,再遍历patternSlice(a.args)逐个输出参数模式。而解析器识别OpBar的逻辑在 src/parse/Parser.zig:遇到OpBar后若紧跟着又一个OpBar(即||),说明是零参数 lambda;否则进入expr_lambda_args状态,把后续内容当作参数模式列表解析,直到闭合的OpBar。
4. FORMATTED:格式化器判定无需改动
NO CHANGE|_, _| 42已经符合 Roc 格式化器的规范排版(紧凑参数、单一空格间距),因此输出为NO CHANGE。对照 test/snapshots/001.md 的FORMATTED段可以直观看到"有改动"与"无改动"两种输出形态的差异。
5. CANONICALIZE:规范化阶段的语义变化
(e-lambda (args (p-underscore) (p-underscore)) (e-num (value "42")))这是最能体现"规范化"意义的一段:e-int (raw "42")变成了e-num (value "42")。raw字段消失,取而代之的是value——整数文本42被解析为数值内容。从源码看,规范化的数字字面量路径在 src/canonicalize/Can.zig:未带类型后缀的整数字面量被构造为CIR.Expr{ .e_num = .{ .value = value_content, .kind = .int_unbound } },即未绑定具体整数类型的数字节点,同时通过recordNumeralLiteral登记该字面量,供后续类型检查阶段做"数字字面量约束"处理。
参数模式则保持为p-underscore:规范化代码在 src/canonicalize/Can.zig 中把underscore模式原样转换为Pattern.underscore = {}(空结构体,不携带任何名字或区域信息),因为下划线本身就是"占位、无绑定"的语义。
6. TYPES:类型推断揭示闭包的完整签名
(expr (type "_arg, _arg2 -> a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]"))类型检查器为这个闭包推断出的类型是:
- 入参:
_arg与_arg2——两个由下划线占位符生成的不同类型变量(尽管源码都是_,每个占位符各引入一个独立类型变量); - 返回:类型变量
a; - 约束:
a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])——因为函数体42是未绑定类型的数字字面量,返回类型a必须满足"可以从Numeral转换而来"的约束,转换失败的错误类型为InvalidNumeral(Str)。
from_numeral正是 Roc 内建数字类型的统一入口:在 src/build/roc/Builtin.roc 附近可以看到num.from_numeral : Builtin.Num.Numeral -> Try(num, [InvalidNumeral(Str)])这类声明被注入到各数值类型(U8、I32、F64、Dec 等)的能力中;类型检查器在 src/check/Check.zig 处解析.from_numeral方法,并在 src/check/Check.zig 用专门的工作列表(open_literal_vars/open_numeral_literals)跟踪这些由字面量转换产生的柔性类型变量。这也是 Roc "数字字面量在未确定具体类型前保持多态"这一设计的关键实现。
纵向对比:闭包快照家族
把 two_arg_closure.md 与同目录下的其他闭包快照对比,能更清楚地理解参数形态对编译流水线的影响:
| 快照文件 | 源码 | PARSE 参数形态 | TYPES 结果 |
|---|---|---|---|
| lambda_simple.md | \|x\| x + 1 | (p-ident (raw "x")) | a -> a where [a.plus : a, b -> a, b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])] |
| lambda_with_args.md | \|x, y\| x + y | (p-ident (raw "x"))、(p-ident (raw "y")) | a, b -> a where [a.plus : a, b -> a] |
| two_arg_closure.md(本文) | \|_, _\| 42 | 两个(p-underscore) | _arg, _arg2 -> a where [a.from_numeral : ...] |
三个样例揭示了三条规律:
- 命名参数被解析为
p-ident,规范化后变为p-assign (ident ...)(绑定一个局部变量);下划线参数始终是p-underscore,规范化后依旧如此,因为它不产生任何绑定。 e-int/e-ident等"解析期节点"在规范化阶段统一转换为e-num/e-lookup-local等 CIR 节点,raw文本被替换为语义化value。- 类型变量命名:下划线占位符生成
_arg、_arg2这类匿名类型变量名,且每个_各自独立(两个_不共享同一类型变量)。
如何运行与维护这些快照
快照测试的命令全部集中在zig build的run-snapshot-tool目标(详见 test/snapshots/README.md 的 Usage 一节):
# 生成/校验所有快照 zig build run-snapshot-tool # 只处理指定快照文件 zig build run-snapshot-tool -- test/snapshots/expr/two_arg_closure.md # 用编译器当前输出更新快照中的 EXPECTED 段 zig build run-snapshot-tool -- test/snapshots/expr/two_arg_closure.md --update-expected # REPL 快照调试:启用解释器跟踪(仅 type=repl、单文件) zig build run-snapshot-tool -- test/snapshots/repl/repl_record_field_access.md --trace-eval补充两个使用要点:
- 若
SOURCE中需要嵌入回车符字节,可在META中添加source_escapes=true,并在SOURCE中把每个回车写作\r; --trace-eval要求使用 debug 构建(默认即开启跟踪),release 构建需通过-Dtrace-eval=true显式开启。
日常维护中,zig build run-snapshot-tool -- <file> --update-expected是把"行为变更"固化为"新期望"的标准流程:先人工确认新输出是正确的,再更新快照,避免用脚本盲改掩盖回归。测试与模糊测试基础设施同样围绕这些快照展开,例如 test/fuzzing/fuzz-tokenize.zig、test/fuzzing/fuzz-parse.zig 会以快照格式为基准做随机输入验证。
从快照反推编译流水线:一张全景图
综合本文所有源码证据,|_, _| 42的完整旅程可以概括为:
- 词法(src/parse/tokenize.zig):产出
OpBar, Underscore, Comma, Underscore, OpBar, Int, EndOfFile。 - 解析(src/parse/Parser.zig):
OpBar触发 lambda 参数解析状态机,参数作为模式列表存储,产出e-lambdaAST(序列化见 src/parse/AST.zig)。 - 格式化:对已符合规范排版的输入输出
NO CHANGE。 - 规范化(src/canonicalize/Can.zig):
e-int→e-num(kind = int_unbound)并登记数字字面量;下划线模式原样转为Pattern.underscore(src/canonicalize/Can.zig)。 - 类型检查(src/check/Check.zig):两个
_各自引入类型变量_arg、_arg2;数字字面量经from_numeral约束(src/build/roc/Builtin.roc)最终定型为多态返回类型a。
这条链路正是 Roc 编译器"快速、友好、函数式"承诺的底层支撑:语法简洁(|_, _| 42一行表达忽略双参的闭包)、类型安全(每个数字字面量的多态性都由from_numeral约束显式管理)、且每个阶段都可被快照测试精确观测。
结语
test/snapshots/expr/two_arg_closure.md 虽然只有几十行,却浓缩了 Roc 编译器从 token 到类型推断的完整流水线信息。通过读懂它的每一段、对照同目录的 lambda_simple.md 与 lambda_with_args.md、再深入 src/parse/Parser.zig、src/canonicalize/Can.zig 与 src/check/Check.zig 的实现,你既能掌握快照测试的读写与排查方法,也能从"表达式粒度"理解闭包、下划线占位符与数字字面量多态在 Roc 编译器内部的真实表示。
【免费下载链接】roc
A fast, friendly, functional language.
相关推荐
Roc 编译器快照测试深度解析:记录解构作为闭包参数的完整编译流水线
Roc 编译器快照测试深度解析:记录解构作为闭包参数的完整编译流水线 Roc 是一门追求快速、友好、函数式的编程语言(项目主页见 README.md https
Roc 编译器快照测试剖析:从 `(|x| x + 1)(2)` 看无捕获 Lambda 的完整编译流水线
Roc 编译器快照测试剖析:从 |x| x + 1 2 看无捕获 Lambda 的完整编译流水线 本篇技术指南以 Roc 编译器仓库中的快照测试 test/sn
Roc 编译器快照实战:从 `Some(42)` 看懂带负载标签(Tag with Payload)的完整编译管线
Roc 编译器快照实战:从 Some 42 看懂带负载标签(Tag with Payload)的完整编译管线 导读 本篇文章以 Roc 编译器测试套件中的快照文
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考