
Roc 格式化器如何保持###分隔注释原样快照测试用例hash_separator_comment_formatting.md深度解析【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc导读Roc 编译器内置的格式化器roc fmt在重排源码时会自动为#注释补一个空格但这一规则不能无差别套用到##文档注释与###分隔注释上。本文以仓库中的快照测试用例 test/snapshots/hash_separator_comment_formatting.md 为骨架逐段拆解该用例的八个阶段输出并结合src/fmt/fmt.zig、src/parse/tokenize.zig、src/snapshot_tool/main.zig等源码讲清「三井号分隔注释为什么不会被插入空格」的完整机制同时给出快照测试的生成、更新与校验命令帮助你掌握 Roc 编译器快照测试体系的读法与用法。一、用例文件总览一条注释引发的格式化回归测试hash_separator_comment_formatting.md是 Roc 编译器test/snapshots/目录下的一个普通快照测试ordinary snapshot。这类测试的核心思想在 test/snapshots/README.md 中有明确说明通过捕获源码在词法分析、解析、规范化、类型检查等各个编译阶段的输出对编译器行为进行全流水线验证一旦行为发生意外变化即可检出回归。本用例的META段直接点明了被测行为descriptionTriple hash ### separators should not have space inserted typefile:Foo.rocdescription描述被测语义——「三井号###分隔注释不应被插入空格」typefile:Foo.roc被测单元是一个名为Foo.roc的整文件。整个用例的测试源码SOURCE段极为精简只有两行### This is a separator comment Foo : [A]第一行是以###开头的「分隔注释」separator comment第二行是一个 Roc 顶层类型别名声明Foo : [A]一个只含标签A的标签联合类型。本文要回答的核心问题就是格式化器处理这段代码后###前面不能多出一个空格变成### This is...之外的形式确切地说不能把###变成## #或在#后插入空格破坏三井号序列。二、逐段拆解快照文件的八个阶段快照文件的章节顺序由快照工具固定。在 src/snapshot_tool/main.zig 中可以看到标准顺序META → SOURCE → EXPECTED → PROBLEMS → TOKENS → PARSE → FORMATTED → CANONICALIZE → TYPEStypemono的用例顺序略有不同MONO与FORMATTED紧跟SOURCE见 main.zig#L1330-L1339。各章节标题常量定义在 main.zig#L1597-L1610。2.1 EXPECTED 与 PROBLEMS没有诊断报告# EXPECTED NIL # PROBLEMS NIL这两段都输出NIL表示这段源码在编译过程中没有产生任何诊断报告错误、警告均为零。根据 test/snapshots/README.md 的说明普通快照的PROBLEMS段保存的是每个reporting.Report的规范化 S-表达式序列化见src/reporting/report_sexpr.zig包含严重级别、标题、源码区域及完整的文档结构NIL即代表编译未产生任何报告。EXPECTED与PROBLEMS之所以分开是为了让「语义变化」与「呈现变化」互不干扰普通快照只钉住诊断的语义而渲染细节框线字符、ANSI 转义、换行包裹等由typereporting的快照在reporting/目录单独钉住。2.2 TOKENS词法层面注释被完全吞掉# TOKENS ~~~zig UpperIdent,OpColonEqual,OpenSquare,UpperIdent,CloseSquare, EndOfFile, ~~~这是词法分析tokenization阶段的输出。值得注意### This is a separator comment这一整行没有产生任何 token。原因是词法分析器在chompTrivia吞掉「琐碎内容」时把注释与空白一并跳过见 src/parse/tokenize.zig#L785-L814} else if (b #) { self.pos 1; while (self.pos self.buf.len and self.pos ! \n and self.pos ! \r) { self.pos 1; } }只要遇到#游标就一路前进到行尾整条注释无论一个#、两个##还是三个###都不进入 token 流。因此剩余的真实 token 只有Token对应源码UpperIdentFooOpColonEqual:OpenSquare[UpperIdentACloseSquare]EndOfFile文件结束2.3 PARSE注释不参与 AST# PARSE ~~~clojure (file (type-mod) (statements (s-type-decl (header (name Foo) (args)) (ty-tag-union (tags (ty (name A))))))) ~~~解析阶段的 S-表达式确认了注释在语法树中同样「隐形」根节点file下只有一个类型声明节点s-type-decl其头部是名为Foo的header类型本体是包含单个标签A的标签联合ty-tag-union。分隔注释既不影响type-mod模块类型头也不作为 AST 节点出现——它只作为源码「夹缝」中的文本被保留供格式化阶段重新排版。2.4 FORMATTED核心断言——「NO CHANGE」# FORMATTED ~~~roc NO CHANGE ~~~这是整个用例的灵魂。NO CHANGE表示格式化器输出的结果与SOURCE完全一致### This is a separator comment原样保留Foo : [A]原样保留。这正是META中description所声明的行为——###分隔注释不会被插入空格。要理解这条断言为何重要需要看格式化器的注释刷新逻辑见 src/fmt/fmt.zig#L3733-L3739try fmt.push(#); const comment_text between_text[comment_start..comment_end]; // Add space after # unless next char is space or # (preserves ## doc comments and ### separators) if (!isShebang(start_offset i, comment_text) and comment_text.len 0 and comment_text[0] ! and comment_text[0] ! #) { try fmt.push( ); } try fmt.pushAll(comment_text);规则非常明确格式化器在输出#之后默认会补一个空格把#foo规范化为# foo但有两个例外注释文本下一个字符已经是空格# foo保持# foo注释文本下一个字符是#——即##文档注释与###分隔注释此时不插入空格从而保住两井号/三井号序列的完整性。同样的逻辑也出现在文件末尾注释处理函数flushCommentsEOF中fmt.zig#L3655-L3661并附有完全一致的注释说明。这两处共同保证了无论注释出现在语句之间还是文件末尾###分隔符都不会被「美化」成带空格的形式。此外flushCommentsfmt.zig#L3687 起还负责处理##文档注释的特殊排版——若文档注释紧跟代码行仅隔一个换行且前一个 token 不是注释会在其上方补一个空行见 fmt.zig#L3719-L3726保证文档注释与代码之间视觉分隔清晰。还有一处需要留意的例外是 shebang若注释位于文件最开头且内容是#!格式化器通过isShebangfmt.zig#L3677-L3684识别并跳过空格插入否则会破坏可执行 Roc 脚本的 shebang 行。由此可以总结出 Roc 格式化器对三类#注释的完整处理矩阵注释形态类型格式化行为# comment普通注释#后若紧跟非空格非#字符则插入一个空格## comment文档注释doc comment#后不插空格保持##必要时在文档注释与代码之间补空行### comment分隔注释separator comment#后不插空格保持###原样#! ...文件首行shebang完全跳过格式化处理关于##文档注释可在 src/docs/extract.zig#L66-L113 看到模块级文档注释提取逻辑##行位于文件开头且连续时会被收集为模块文档。这也解释了为什么格式化器必须对##格外小心——它承载着可被文档系统提取的语义信息。2.5 CANONICALIZE 与 TYPES语义阶段的等价性确认# CANONICALIZE ~~~clojure (can-ir (s-nominal-decl (ty-header (name Foo)) (ty-tag-union (ty-tag-name (name A))))) ~~~ # TYPES ~~~clojure (inferred-types (defs) (type_decls (nominal (type Foo) (ty-header (name Foo)))) (expressions)) ~~~CANONICALIZE是规范化阶段canonicalization产生的 CIRCanonical IRS-表达式TYPES是类型推断阶段的输出。两者都只包含Foo这一个名义类型声明defs与expressions为空进一步确认分隔注释对语义层毫无影响编译流水线的「真正产物」只有类型Foo。三、快照测试如何生成与更新test/snapshots/下的每个.md文件都可由快照工具重新生成。工具入口是 src/snapshot_tool/main.zig构建目标名snapshot见 build.zig#L4231-L4246通过 Zig Build 步骤暴露给开发者命令汇总如下命令作用zig build run-snapshot-tool生成/刷新全部快照文件zig build run-snapshot-tool -- file_path只处理指定快照文件如zig build run-snapshot-tool -- test/snapshots/hash_separator_comment_formatting.mdzig build run-snapshot-tool -- file_path --update-expected用实际输出更新EXPECTED/PROBLEMS等期望段zig build run-snapshot-tool -- repl_snapshot.md --trace-eval对 REPL 快照开启解释器追踪调试仅typerepl、单文件zig build run-check-snapshots重新生成快照若与已跟踪文件有差异则失败zig build check-snapshot-diff仅做差异检查git diff --exit-code test/snapshots其中run-check-snapshots与check-snapshot-diff的定义可分别在 build.zig#L2996-L3029 与 build.zig#L1323-L1393 找到——后者通过git diff --exit-code test/snapshots保证「已跟踪快照与重新生成结果完全一致」任何未提交的格式化器行为变化都会让 CI 失败这正是快照测试的核心价值把格式化器、词法、解析、类型检查的每一次细微行为变化显式暴露在 diff 中。一个细节为什么快照输出不随编译器版本漂移格式化器在 src/fmt/fmt.zig#L40-L50 定义了Options.compiler_version字段当它为null时格式化不会重写文件头中roc: ...的版本钉扎。该字段的注释明确写道「快照工具、playground 以及格式化器自身的 round-trip 测试」必须让输出不随构建它的编译器变化——因此快照测试在调用格式化时不会传入版本号保证快照文件在任何 nightly 下重新生成都逐字节稳定。四、在命令行中亲手验证快照测试之外的日常验证路径是roc fmt子命令。其参数解析在 src/cli/cli_args.zig#L827-L864支持roc fmt [DIRECTORY_OR_FILES]格式化指定文件或目录缺省时格式化当前目录下的main.rocroc fmt --check只检查不写回若有文件需要格式化则返回非零退出码roc fmt --stdin从 stdin 读入源码、把格式化结果写到 stdout。格式化命令的实现位于 src/cli/main.zig#L16225-L16284--check模式会汇总所有未格式化文件并打印「The following file(s) failedroc fmt --check: ...」全部通过时打印All formatting valid.普通模式则打印格式化成功的文件数。你可以用下面的命令亲手验证本用例的行为# 准备一个与 SOURCE 段相同的文件 printf ### This is a separator comment\nFoo : [A]\n Foo.roc # 从 stdin 格式化观察 ### 是否保持原样 roc fmt --stdin Foo.roc # 期望输出 # ### This is a separator comment # Foo : [A] # 再验证普通注释会被补空格 printf #bar\nFoo : [A]\n | roc fmt --stdin # 期望输出 # # bar # Foo : [A]对比两个输出即可直观看到#bar会被规范化为# bar而### This is a separator comment因为第三个字符是#而逃过空格插入——与快照用例FORMATTED: NO CHANGE的断言完全吻合。五、小结一条快照用例的完整价值链条hash_separator_comment_formatting.md虽只有 52 行却串起了 Roc 编译器测试体系的一条完整价值链条词法层tokenize.zig###注释作为 trivia 被跳过不产生 token解析层Parser.zig注释不进 ASTFoo : [A]解析为类型声明节点格式化层fmt.zigflushComments在补空格时对#后接#的情况放行保证###分隔注释原样输出语义层canonicalize / types注释对 CIR 与类型推断零影响CI 层build.zig snapshot_tool/main.zig任何阶段的输出漂移都会在run-check-snapshots中暴露为 diff。换句话说这一条「小小的」格式化快照用例实际上同时守护了词法、解析、格式化、规范化、类型检查五个阶段的行为契约——这正是 Roc 快照测试体系「以极简源码覆盖全流水线」的设计精髓。【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考