Roc 语言 match 表达式嵌套模式解析:从快照测试看 tag、record 与 list 的组合模式
2026/9/18 21:30:50 网站建设 项目流程

Roc 语言 match 表达式嵌套模式解析:从快照测试看 tag、record 与 list 的组合模式

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

导读

本文以 Roc 编译器仓库中的表达式快照测试test/snapshots/match_expr/nested_patterns.md为主线,深入剖析 Roc 语言match表达式中嵌套模式的完整形态:tag 内嵌 record、record 内嵌 list、list 中使用.. as rest列表 rest 模式,以及它们与表达式主体的组合。读完本文,你将掌握嵌套模式的语法规则、旧语法迁移要点、从词法到类型推断的完整编译流水线,以及如何通过zig build run-snapshot-tool复现与维护此类快照测试。

一、快照文件结构:一份完整的编译流水线记录

test/snapshots/match_expr/nested_patterns.md是 Roc 编译器快照测试体系(见 test/snapshots/README.md)中的一份普通快照(type=expr)。快照测试通过捕获源代码经过每个编译阶段的输出,来验证编译器行为并检测回归。该文件由以下区块构成,每个区块对应一条编译流水线阶段:

区块含义
META元信息:description描述用例,type=expr声明快照类型为表达式
SOURCE被编译的 Roc 源代码
EXPECTED/PROBLEMS预期结果与诊断报告;NIL表示无报告产生
TOKENS词法分析(tokenization)输出
PARSE语法分析(parse)输出的 S-expression 语法树
FORMATTED格式化器输出(验证格式化不改变语义)
CANONICALIZE规范化阶段输出(含作用域解析后的表达式树)
TYPES类型检查(type checking)得到的表达式类型

本文件的PROBLEMSNIL,说明该示例是合法、无任何编译诊断的完整程序片段。快照类型expr在 src/snapshot_tool/main.zig 中定义,其驱动逻辑会解析SOURCE、运行各编译阶段并对比预期输出。

二、核心示例:四层嵌套模式逐行剖析

SOURCE中的完整表达式如下(与FORMATTED区块完全一致,说明格式化前后 AST 语义不变):

match data { Container({ items: [First(x), .. as rest] }) => x + List.len(rest) Container({ items: [] }) => 0 Wrapper([Tag(value), Other(y)]) => value + y Simple(x) => x }

这是对一个名为data的联合类型(tag union)值进行模式匹配,四个分支依次覆盖不同的形状:

  1. Container({ items: [First(x), .. as rest] })——三层嵌套:Container是 tag,其载荷是一个 record(字段items),而items的值是一个 list。list 模式[First(x), .. as rest]表示"首元素必须是First(x),剩余元素绑定到变量rest"。分支主体x + List.len(rest)+把首元素值与rest的长度相加。从PARSE树可见p-list-rest (name "rest")节点与e-apply(调用List.len)的对应关系。

  2. Container({ items: [] })——匹配同一个Containertag 但items为空列表的穷尽情形,主体返回整数0。注意PARSE中此分支的 record 字段仍是(field (name "items") (rest false) (p-list)),与分支 1 共享同一 record 形状但内层 list 为空。

  3. Wrapper([Tag(value), Other(y)])——Wrappertag 的载荷是一个二元 list,两个元素分别匹配 tag 模式Tag(value)Other(y),即"list 的每个元素本身也是带载荷的 tag"。主体value + y取出两个载荷值相加。

  4. Simple(x) => x——最简情形:无嵌套,tag 载荷直接绑定到变量x并原样返回。

整个表达式推断出的类型为U64(见TYPES区块(expr (type "U64"))),因此分支 1、3 的xvalueyList.len(rest)都是整数类型,分支 2 的0与之保持一致。

三、列表 rest 模式.. as rest:语法与底层实现

本例最值得关注的是分支 1 中的[First(x), .. as rest]TOKENS区块可以清楚看到其词法序列:DoubleDot..)后紧跟KwAsas),再跟LowerIdentrest)。这一语法对应 Roc 当前的列表 rest 模式:.. as namename可选

从 src/parse/Parser.zig 的解析器实现可以确认:

  • pattern_list_next状态遇到DoubleDot后,若下一个 token 是KwAs,则解析as之后可选的LowerIdentNamedUnderscore作为 rest 名称;
  • DoubleDot后直接跟标识符(旧语法..rest),解析器仍会接受,但会推送pattern_list_rest_old_syntax诊断;
  • 最终生成AST.Pattern.Idx.list_rest节点(nameregion字段,见 src/parse/AST.zig),并在语法树序列化时输出为p-list-rest(src/parse/AST.zig)。

pattern_list_rest_old_syntax诊断在 src/parse/AST.zig 中的文案为:"List rest patterns now use.. as name. The name is optional, but if it is present it must come afteras.",并给出示例[first, .. as rest]。也就是说,本项目中的.. as rest是推荐写法,旧式[First(x), ..rest]会触发兼容性警告但仍可解析。

此外,Parser.zig中处理.list_rest模式时会将其绑定的名称(p.name)加入声明作用域(约 L656),因此rest在分支主体中可直接使用。这正是CANONICALIZE区块中e-lookup-local (p-assign (ident "rest"))能够成功解析的前提。

四、PARSE 到 CANONICALIZE:嵌套模式的编译期流转

PARSE区块以 S-expression 形式给出了嵌套模式的语法树,例如分支 1:

(p-tag (raw "Container") (p-record (field (name "items") (rest false) (p-list (p-tag (raw "First") (p-ident (raw "x"))) (p-list-rest (name "rest"))))))

可以看出模式节点采用"组合子"式嵌套:p-tag包裹p-recordp-record包裹p-listp-list内并列p-tag元素与p-list-rest。这种树状结构直观反映了模式类型的递归定义——模式可以无限嵌套,只要每一层都符合各自的语法约束。

进入CANONICALIZE阶段后,模式被解析为带作用域信息的内部表示。以分支 1 为例:

(pattern (degenerate false) (p-applied-tag)) (value (e-dispatch-call (method "plus") (constraint-fn-var 255) (receiver (e-lookup-local (p-assign (ident "x")))) (args (e-call (constraint-fn-var 254) (e-lookup-external (builtin)) (e-lookup-local (p-assign (ident "rest")))))))

关键变化包括:

  • 模式被归一为p-applied-tag("已应用的 tag"),degenerate false标记该分支模式非退化;
  • 变量xrest通过p-assign建立局部绑定,并通过e-lookup-local引用——即作用域解析已完成;
  • List.len被解析为外部内建函数(e-lookup-external (builtin))的调用;
  • x + List.len(rest)被表示为e-dispatch-call(方法分发调用,method "plus"),体现了 Roc 中+运算符作为约束方法分派的实现方式。

分支 3 的value + y同样编译为e-dispatch-call (method "plus"),而分支 2 的0编译为e-num (value "0"),分支 4 则直接e-lookup-local返回x。可见 CANONICALIZE 阶段的输出把嵌套模式"扁平化"为每个分支独立的绑定与值表达式,供后续类型检查使用。

五、类型检查结果:整体表达式的类型

TYPES区块只有一行:

(expr (type "U64"))

这说明整个match表达式被类型检查器推断为U64类型。由此可以反推:data是一个各分支载荷统一的联合类型,其中First(x)List.len(rest)都是U64WrapperTag(value)Other(y)的载荷同样是U64,四个分支的值类型全部收敛为U64,从而保证match的所有分支类型一致——这是 Roc 对match表达式的基本类型约束。

六、运行与维护:如何复现这份快照

快照文件本身既是文档也是测试预期。若要复现或更新输出,可按 test/snapshots/README.md 的方式执行:

# 运行快照工具处理本文件(校验各阶段输出是否与文件一致) zig build run-snapshot-tool -- test/snapshots/match_expr/nested_patterns.md # 若编译器行为有意的改变导致输出不同,更新预期 zig build run-snapshot-tool -- test/snapshots/match_expr/nested_patterns.md --update-expected

仓库的 build.zig 中定义了build-snapshot-toolrun-snapshot-tool两个构建步骤,快照工具入口为 src/snapshot_tool/main.zig。另外run-check-snapshots步骤会重新生成全部快照并与已跟踪文件做git diff对比(build.zig),任何未提交的快照漂移都会导致构建失败,以此确保编译器各阶段行为始终与快照锁定一致。

七、延伸阅读:match 模式家族的其他快照

nested_patterns.md并非孤立用例,test/snapshots/match_expr/目录下还有一系列覆盖 match 模式各个维度的兄弟快照,可作为组合阅读材料:

  • nested_record_patterns.md:深层 record 嵌套解构(address: { city, country }),并演示match ...触发 "Unconditional Condition" 警告的场景;
  • list_patterns.md 与 list_rest_scoping.md:list 模式与 rest 变量的作用域规则;
  • complex_list_tags.md:list 中 tag 模式的复杂组合;
  • record_destructure.md:record 解构基础;
  • tag_with_payload.md:带载荷 tag 的基础用法;
  • pattern_as_nested.md 与 pattern_alternatives_mixed.md:as绑定与模式备选(alternatives)在嵌套场景中的行为。

结语

通过test/snapshots/match_expr/nested_patterns.md这一份快照,我们可以完整观察到 Roc 语言嵌套模式匹配从源码到类型推断的全过程:Container({ items: [First(x), .. as rest] })这类三层嵌套模式在语法树上由p-tag/p-record/p-list/p-list-rest组合子递归构造;.. as rest是当前推荐的列表 rest 语法(旧式..rest会触发兼容性诊断);规范化阶段完成变量绑定与作用域解析后,分支值被编译为约束方法分派与内建函数调用;最终整个表达式被推断为U64。掌握快照文件的结构与编译阶段的对应关系,不仅能读懂这份文档,也能举一反三地阅读test/snapshots/match_expr/下其余三十余份模式相关快照,进而深入理解 Roc 编译器管线的设计与行为。

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

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

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

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

立即咨询