【免费下载链接】roc
A fast, friendly, functional language.
本篇文章以 Roc 编译器仓库中的快照测试文件 string_multiline_formatting_(due_to_templating_not_multiline_string_literal)_3.md_3.md) 为核心研究对象,深入讲解快照(Snapshot)测试的完整文件结构、字符串插值模板中函数调用与注释的多行格式化规则,以及从词法分析、语法解析、格式化、规范化(Canonicalize)到类型检查的完整编译管线在测试中的呈现方式。读完本文,你将掌握如何阅读与维护 Roc 编译器的表达式级快照测试,并能准确理解FORMATTED: NO CHANGE这类输出所代表的格式化稳定性语义。
一、什么是 Roc 快照测试:一份表达式在编译管线中的“全息照片”
Roc 编译器的测试体系里,test/snapshots/目录下存放着大量 Markdown 格式的快照文件。根据 test/snapshots/README.md 的说明,快照测试通过捕获特定 Roc 代码示例在编译各阶段的输出,来验证编译管线的正确性——源码经过词法分析(tokenization)、语法解析(parsing)、规范化(canonicalization)、类型检查(type checking)等阶段时,每一阶段的输出都被固化在快照文件中,一旦编译器行为发生意外变化,快照对比就能立刻暴露回归(regression)。
快照文件通常包含以下核心区块:
| 区块 | 含义 |
|---|---|
META | 元信息,用description描述测试场景,用type声明快照类型(如expr、snippet、file、reporting、repl) |
SOURCE | 被测的原始 Roc 源码 |
EXPECTED | 期望的诊断摘要(行号 + 诊断名),NIL表示无预期诊断 |
PROBLEMS | 诊断的规范 S 表达式序列化(见src/reporting/report_sexpr.zig),NIL表示编译未产生任何报告 |
TOKENS | 词法分析阶段产出的 token 流 |
PARSE | 语法解析阶段产出的 AST(S 表达式形式) |
FORMATTED | 格式化器(src/fmt/fmt.zig)处理后的源码,NO CHANGE表示输入已符合格式规范 |
CANONICALIZE | 规范化(desugar)阶段产出的中间表示 |
TYPES | 类型检查阶段推断出的表达式类型 |
我们研究的这份快照type=expr,即“表达式级”快照:被测内容是一个字符串表达式,通过它验证一个非常刁钻的场景——多行格式化并非来自多行字符串字面量(multiline string literal),而是来自字符串模板(templating)内部嵌入的函数调用。
二、被测源码:模板插值内嵌带注释的多行函数调用
快照的SOURCE区块给出了被测表达式:
"This is a string with ${ some_func( a, # This is a comment b, ) } lines of text due to the template parts"这段代码本身就是一个完整的 Roc 字符串表达式,其特征如下:
- 整个字符串使用双引号包围,是单行字符串字面量而非
"""多行字符串; - 字符串内部通过
${ ... }进行插值(interpolation / templating),插值区域内是一个函数调用some_func(a, b); - 调用内部携带了行内注释
# This is a comment,且参数a与注释在同一行; - 由于插值区域内出现了换行与缩进,整个字符串表达式在源码层面呈现为多行。
这正是快照文件名中点名的关键区别:due_to_templating_not_multiline_string_literal(因模板而非多行字符串字面量所致)。也就是说,此处“多行”的成因是模板插值区内的结构(函数调用跨行),而非字符串字面量本身支持换行。
对比同系列的另一份快照 string_multiline_formatting_(due_to_templating_not_multiline_string_literal)_1.md_1.md),其SOURCE是同一场景的“单行压缩版”:
"This is a string with ${some_func(a, #This is a comment b)} lines of text due to the template parts"两份快照一个测“输入已多行、格式化后保持不变”,一个测“输入是压缩单行、格式化器会将其展开为规范多行”。二者配合,完整锁定了格式化器在该场景下的行为边界。
三、TOKENS:词法阶段如何切分插值字符串
快照的TOKENS区块记录了词法分析器的输出:
StringStart,StringPart,OpenStringInterpolation, LowerIdent,NoSpaceOpenRound, LowerIdent,Comma, LowerIdent,Comma, CloseRound, CloseStringInterpolation,StringPart,StringEnd, EndOfFile,对照 src/parse/tokenize.zig 中的 token 定义(如OpenStringInterpolation、CloseStringInterpolation位于 token 枚举中,相关处理逻辑可参见 src/parse/tokenize.zig 及 src/parse/tokenize.zig、src/parse/tokenize.zig 附近的 push 逻辑),可以还原词法分析的切分过程:
StringStart:字符串字面量开始;StringPart:普通字符串片段"This is a string with ";OpenStringInterpolation:遇到${,进入插值区;LowerIdent、NoSpaceOpenRound:函数名some_func与紧跟其后的左括号(NoSpaceOpenRound表示左括号与标识符之间无空格);LowerIdent,Comma:参数a及其后的逗号;LowerIdent,Comma:参数b及其后的逗号(注意这里的注释# This is a comment在 token 流中被过滤,属于注释的常规处理——注释不影响 token 序列,但会被 AST 保留以便格式化器还原);CloseRound:右括号);CloseStringInterpolation:遇到}结束插值区;StringPart,StringEnd:尾部普通片段" lines of text due to the template parts"与字符串结束;EndOfFile:文件结束。
一个关键细节是:插值区内的内容并非字符串 token,而是按普通 Roc 表达式进行词法分析(LowerIdent、NoSpaceOpenRound、Comma、CloseRound都是表达式语法 token)。这从词法层面印证了“插值区域 = 嵌入的表达式”这一设计:字符串外壳由StringStart/StringPart/StringEnd包裹,OpenStringInterpolation/CloseStringInterpolation则作为表达式区的边界标记。
四、PARSE:AST 中字符串由“片段 + 表达式”拼装而成
词法完成后进入语法解析,快照的PARSE区块展示了 AST:
(e-string (e-string-part (raw "This is a string with ")) (e-apply (e-ident (raw "some_func")) (e-ident (raw "a")) (e-ident (raw "b"))) (e-string-part (raw " lines of text due to the template parts")))从 AST 结构可以清晰看到 Roc 如何表示插值字符串:
e-string是字符串表达式的根节点;e-string-part表示普通文本片段,共两段:"This is a string with "与" lines of text due to the template parts";- 中间的
e-apply是插值区内解析出的函数调用表达式:e-ident some_func作为被调函数,e-ident a与e-ident b作为两个参数。
也就是说,${ ... }中的内容被当作一个完整的表达式(这里是e-apply)解析,字符串本身则是“文本片段序列 + 表达式序列”的交错组合。注释# This is a comment在 AST 的 S 表达式输出中不出现,因为它属于附加在节点上的注释信息(供格式化器使用),而非表达式结构的一部分。
五、FORMATTED:NO CHANGE背后的格式化稳定性承诺
本快照的FORMATTED区块输出是:
NO CHANGE这表示:将SOURCE交给格式化器(src/fmt/fmt.zig)处理后,输出与输入完全一致。SOURCE已经是规范化格式:
- 插值区
${后换行、缩进一层(tab); - 函数调用的参数每个占一行,
a,与注释# This is a comment同行,注释前有一个空格; - 每个参数后都有尾随逗号(trailing comma),
b,后跟)前的换行; }与收尾的字符串片段lines of text due to the template parts位于)之后、缩进对齐。
与之对照,_1.md快照的SOURCE是压缩的单行形式,其FORMATTED区块展示的正是展开后的多行规范格式——与本快照的SOURCE逐字一致:
"This is a string with ${ some_func( a, # This is a comment b, ) } lines of text due to the template parts"把两份快照连起来读,可以得到一个完整结论:格式化器在展开插值区内的函数调用时,会统一采用“参数逐行 + 尾随逗号 + 注释保留在首个参数行”的规范布局;若源码已是该布局,格式化器保持原样(NO CHANGE)。这验证了格式化器的幂等性(idempotence)——对已格式化代码再次格式化不产生任何变化,这是 src/fmt/fmt.zig 作为确定性格式化器的核心保证,也是_1与_3两份快照互为镜像、相互锁定的意义所在。
六、CANONICALIZE:模板插值如何 desugar 为局部绑定
快照的CANONICALIZE区块展示了规范化(desugar)阶段的中间表示:
(e-block (s-let (p-assign (ident "#interp_0")) (e-call (e-runtime-error (tag "ident_not_in_scope")) (e-runtime-error (tag "ident_not_in_scope")) (e-runtime-error (tag "ident_not_in_scope")))) (e-interpolation (first (e-literal (string "This is a string with "))) (parts (e-lookup-local (p-assign (ident "#interp_0"))) (e-literal (string " lines of text due to the template parts")))))这是本文技术含量最高的部分,可以拆解出 Roc 编译器的若干内部机制:
- 插值表达式被提为局部绑定:
some_func(a, b)这个插值内的表达式被提取出来,绑定到自动生成的局部变量#interp_0(内部标识符,以#开头以避免与用户标识符冲突)。这正是 src/canonicalize/Can.zig 中ident_not_in_scope诊断(参见 src/canonicalize/Diagnostic.zig)所在机制的表现——它出现在插值/表达式 desugar 过程中。 e-interpolation的first与parts:first是字符串字面量首片段"This is a string with ",parts依次为e-lookup-local #interp_0(引用被提取的表达式结果)与字符串尾片段" lines of text due to the template parts"。这证实了运行时求值顺序:先计算插值表达式,再把结果拼接到字符串首尾片段之间。ident_not_in_scope的注入:e-call的三个实参都被替换为(e-runtime-error (tag "ident_not_in_scope"))。这是因为some_func、a、b在快照上下文中并未定义(快照只测表达式本身,没有前置的绑定声明),规范器无法解析这些标识符,因此以“运行时错误”节点占位并产生ident_not_in_scope诊断。这是编译器对“未在作用域内的标识符”的标准降级处理路径,也是PROBLEMS区块语义的来源之一。
七、PROBLEMS 与 TYPES:诊断语义与类型结论
本快照的PROBLEMS区块为NIL,而EXPECTED同样为NIL。这里需要说明二者的微妙关系:根据 test/snapshots/README.md 的约定,EXPECTED是“期望的诊断行摘要”,PROBLEMS是诊断的完整 S 表达式序列化。本快照中两者都是NIL,意味着该测试用例不关注诊断输出——尽管规范化阶段注入了ident_not_in_scope运行时错误占位,但本快照的断言重点是 token、AST、格式化与规范化的结构,而非错误报告本身。
TYPES区块则给出了类型检查的结论:
(expr (type "Error"))表达式最终被推断为类型Error。结合 CANONICALIZE 阶段的e-runtime-error占位可以理解:由于some_func、a、b均未绑定,整个插值表达式被降级为错误表达式,类型检查自然得到Error。这展示了 Roc 的类型系统在“表达式已含错误占位”时如何继续推进类型推导——用统一的Error类型收束,避免类型检查在中途崩溃。
八、如何运行与维护这类快照测试
快照测试的构建与维护方式记录在 test/snapshots/README.md 中,核心命令如下:
# 生成/更新所有快照 zig build run-snapshot-tool # 仅更新指定快照文件 zig build run-snapshot-tool -- test/snapshots/string_multiline_formatting_(due_to_templating_not_multiline_string_literal)_3.md # 依据诊断(problems)更新期望输出 zig build run-snapshot-tool -- <file_path> --update-expected # 调试 REPL 快照时启用解释器追踪 zig build run-snapshot-tool -- <repl_snapshot.md> --trace-eval对表达式级快照而言,读者可以在 src/parse/tokenize.zig 中追踪OpenStringInterpolation/CloseStringInterpolation的 push 逻辑,在 src/fmt/fmt.zig 中查看多行布局的判定(如nodeWillBeMultiline相关逻辑),在 src/canonicalize/Can.zig 中检索ident_not_in_scope与插值 desugar 的实现,从而形成“快照输出 → 源码实现”的闭环印证。
结语:一份快照,一次完整的编译管线缩影
string_multiline_formatting_(due_to_templating_not_multiline_string_literal)_3.md虽然只有 63 行,却完整覆盖了 Roc 编译器从词法、解析、格式化、规范化到类型检查的全部关键阶段,并精确刻画了一个易被忽视的行为边界:插值模板内部的函数调用会触发多行格式化,而这一行为与多行字符串字面量无关。与同系列的_1.md快照互为镜像,它们共同固化了格式化器在“注释 + 参数逐行 + 尾随逗号”场景下的规范输出,是理解 Roc 字符串插值实现与快照测试方法论的一手资料。
【免费下载链接】roc
A fast, friendly, functional language.
相关推荐
Roc 语言数值字符串解析:从 `from_str` 快照测试看 F32、I64、U128、I128 的边界行为
Roc 语言数值字符串解析:从 from_str 快照测试看 F32、I64、U128、I128 的边界行为 本篇文章以 Roc 编译器仓库中的 REPL 快照
Roc 字符串插值中的多行格式化:从快照测试透视编译流水线(templating 触发而非多行字符串字面量)
Roc 字符串插值中的多行格式化:从快照测试透视编译流水线(templating 触发而非多行字符串字面量) 本文以 Roc 编译器的快照测试文件 string
Roc 语言多行列表格式化深度解析:以 `multiline_list_formatting_11` 快照测试为例
Roc 语言多行列表格式化深度解析:以 multiline_list_formatting_11 快照测试为例 Roc 是一门快速、友好、函数式的编程语言,其编
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考