尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Bend 模式匹配完全指南:switch、match 与等式模式匹配函数的编译原理

Bend 模式匹配完全指南:switch、match 与等式模式匹配函数的编译原理 Bend 模式匹配完全指南switch、match 与等式模式匹配函数的编译原理【免费下载链接】BendA massively parallel, high-level programming language项目地址: https://gitcode.com/GitHub_Trending/be/Bend模式匹配是 Bend一种大规模并行的函数式编程语言中最重要的语法设施之一。本指南以 docs/pattern-matching.md 为核心系统讲解数字switch、ADTmatch与等式风格模式匹配函数三大形式各自的语义、编译产物与使用注意事项并深入 Bend 编译器源码src/fun/transform/说明这些语法糖是如何被降级desugar成最终的核心项的。读完本文你将理解模式匹配在两种 ADT 编码adt-scott/adt-num-scott下的差异、switch级联的优化策略、以及为什么严格的求值模式要求对 match 分支做线性化和组合子浮升。三种模式匹配形式总览Bend 中的模式匹配共有三种写法分别面向不同的场景语法面向对象编译方式switch n { ... }数字u24编译为简单switch表达式的级联cascadematch x { ... }ADT 构造子依据 ADT 编码adt-scott或adt-num-scott编译为不同表达式等式模式匹配函数任意数字、构造子、字符串、列表先转换成match与switch的树再按上述规则编译三种形式在语义上是递进关系等式模式匹配函数在编译期被转换为match/switch项的嵌套树对应 desugar_match_defs.rs 中的desugar_match_defs流程而后两者再由 encode_match_terms.rs 编码为最终的核心项。下面逐一深入。switch数字匹配被编译为简单 switch 的级联Bend 对多个连续数字的switch采用朴素但可靠的策略编译成一系列只判断 0 与“其余”的简单 switch 表达式后一个 switch 通过减 1 得到前一个数字的前驱。# 以下两种写法等价 switch n { 0: A 1: B 2: C _: (D n-3) } switch n { 0: A _: switch n-1 n-1 { 0: B _: switch n-2 n-1-1 { 0: C _: use n-3 n-2-1; (D n-3) } } }注意上述展开中每个默认分支里通过n-1、n-2、n-3逐步递减来重建“当前数字的前驱”这正是switch允许你在默认分支中直接访问前驱pred变量的方式你可以在switch中显式写出switch n-1 n-1 { ... }这样的绑定也可以依赖switch n隐式得到的前驱变量。从源码看这段级联正是 encode_match_terms.rs 中encode_switch函数的产物它把若干分支拆成“最外层 switch 判 0、其余分支包成内层递归 switch”并用%x作为中间变量依次递减// switch n {0: A; 1: B; _: (C n-2)} converted to // switch n {0: A; _: %x match %x {0: B; _: n-2 (C n-2)}}可见switch匹配的分支数量越多生成的嵌套 switch 就越深这属于编译期展开运行期每个switch仍然只做一次 0/非 0 的数值判断。matchADT 构造子匹配与两种编码对 ADT 构造子的match编译结果取决于当前选择的 ADT 编码方式。Bend 提供两种编码可通过 CLI 选项切换详见 docs/compiler-options.md-Oadt-scott标准 Scott 编码每个构造子对应一个 lambda-Oadt-num-scott默认Scott 编码的变体用数字 tag 标识构造子每个构造子还会生成类型/构造子/tag定义。例如对如下定义type Maybe (Some val) | None UnwrapOrZero x match x { Maybe/Some: x.val Maybe/None: 0 }在adt-num-scott编码下构造子与匹配函数会被编译为Maybe/Some λval λx (x 0 val) Maybe/None λx (x 1) UnwrapOrZero x (x λtag switch tag { 0: λx.val x.val _: λ* 0 })Some先接收字段val再接收一个“接收 tag 的函数”UnwrapOrZero把自身作为函数传给xx内部先对数字 tag 做switch0 表示Some其余表示None。而在adt-scott编码下构造子被实现为接收其他构造子作为续延continuation的 lambda匹配则直接应用参数Maybe/Some λval λMaybe/Some λMaybe/None (Maybe/Some val) Maybe/None λMaybe/Some λMaybe/None Maybe/None UnwrapOrZero x (x λx.val x.val 0)源码中的两种编码实现构造子编码逻辑位于 encode_adts.rs编译器遍历每个 ADT 的每个构造子按AdtEncoding选择编码函数——encode_ctr_scottencode_adts.rs生成λa1..λan λCtr1..λCtrN (Ctr a1..an)形式的标准 Scott 编码encode_ctr_num_scottencode_adts.rs生成λa1..λan λx (x TAG a1..an)其中TAG引用由encode_num_scott_tagencode_adts.rs生成的构造子/tag数字定义——tag 按构造子声明顺序从 0 开始编号。匹配项的编码则在 encode_match_terms.rs 的encode_match中Scott编码直接Term::call(arg, arms)即把匹配对象作为函数应用到各分支上NumScott编码生成λ%tag switch %tag { 0: 分支0; _: ... }的嵌套 switch 树先取数字 tag 再分发。编码选择的重要限制使用adt-num-scott时需要注意IO 功能仅在-Oadt-num-scott下可用见 docs/compiler-options.md。这是因为 IO 需要把内建函数编码为可识别的 tag。adt-scott与adt-num-scott的切换还影响列表与字符串的还原resugar方式对应 resugar_list.rs 与 resugar_string.rs 中按编码分别处理的实现。等式模式匹配函数最强大也最复杂的形式除match与switch之外Bend 还支持等式风格equational-style的模式匹配函数And Bool/True b b And Bool/False * Bool/False这类写法的优缺点非常鲜明优点支持更高级的模式匹配能力变量、通配符、构造子、数字、字符串、列表的任意组合并且会自动负责变量的线性化linearizing确保在严格求值模式下递归定义也能正确工作缺点你无法控制模式匹配的具体实现方式某些情况下资源开销会略高。从等式到 match/switch 树从左到右的转换等式模式匹配函数会被转换成一颗match和switch组成的树从第一个参数开始、从左到右依次展开。下面这组规则(Foo 0 Bool/False (List/Cons h1 (List/Cons h2 t))) (Bar h1 h2 t) (Foo 0 * *) Baz (Foo n Bool/False *) n (Foo n Bool/True *) 0等价于如下嵌套的switch/matchBool、List均为标准 ADTFoo λarg1 λarg2 λarg3 (switch arg1 { 0: λarg2 λarg3 match arg2 { Bool/True: λarg3 Baz Bool/False: λarg3 match arg3 { List/Cons: (match arg3.tail { List/Cons: λarg3.head (Bar arg3.head arg3.tail.head arg3.tail.tail) List/Nil: λarg3.head Baz } arg3.head) List/Nil: Baz } } _: λarg2 λarg3 (match arg2 { Bool/True: λarg1 0 Bool/False: λarg1 arg1 } arg1) } arg2 arg3)这个例子还展示了两个关键编译行为参数被推入 match 内部外层switch arg1的分支里出现了λarg2 λarg3 ...意味着arg2、arg3被下推到需要它们的 match 分支中嵌套构造子模式被拆开(List/Cons h1 (List/Cons h2 t))先匹配最外层List/Cons再对arg3.tail进行二次match取出的字段通过arg3.head、arg3.tail.head、arg3.tail.tail重组。严格模式下的线性化与组合子化在严格求值模式下默认会对 match 内使用的变量做线性化linearize为每个分支加一个 lambda并在 match 末尾用一次应用把变量的值传进去。这是-Olinearize-matches选项的行为见 docs/compiler-options.md# 线性化前 a b switch a { 0: (Foo b) _: (Bar a-1 b) } # 线性化后 a b (switch a { 0: b (Foo b) _: b (Bar a-1 b) } b)而为了确保递归模式匹配函数在严格模式下不陷入死循环还必须把 match 分支变成组合子combinator闭包项被提取为独立的顶层定义match 分支中只保留惰性引用。这正是-Ofloat-combinators选项默认开启所做的见 docs/compiler-options.md以及 lazy-definitions.md 的说明。以上面的Foo为例启用-Olinearize-matches与-Ofloat-combinators严格模式默认后实际编译产物为# 主函数 (Foo) λa λb λc (switch a { 0: Foo__C8; _: Foo__C9; } b c) # 分支 0第一个参数为 0 (Foo__C8) λa λb (a Foo__C5 b) # Foo.case_0 (Foo__C5) λa switch a { 0: λ* Baz; _: Foo__C4; } # Foo.case_0.case_true (Foo__C4) λ* λa (a Foo__C3) # Foo.case_0.case_false (Foo__C3) λa switch a { 0: Baz; _: Foo__C2; } # Foo.case_0.case_false_cons (Foo__C2) λ* λa λb (b Foo__C1 a) # Foo.case_0.case_false_cons_cons (Foo__C1) λa switch a { 0: λ* Baz; _: Foo__C0; } # Foo.case_0.case_false_cons_nil (Foo__C0) λ* λa λb λc (Bar c a b) # Foo.case_0.case_false_nil # 分支非 0第一个参数非 0 (Foo__C9) λa λb λc (b Foo__C7 c a) # Foo.case_ (Foo__C7) λa switch a { 0: λ* λ* 0; _: Foo__C6; } # Foo.case_.case_false (Foo__C6) λ* λ* λa ( a 1) # Foo.case_.case_true每一个Foo__C*都是一个被提取到顶层的组合子对应源文件注释中的Foo.case_0、Foo.case_0.case_true等语义位置match 分支只引用这些名字从而避免在严格求值时无限展开。注意普通用户无法写出名字中带__的函数——这个序列被编译器保留给自动生成的项使用。这段管线的执行顺序可以在 src/lib.rs 的desugar_book中看到先encode_adts编码构造子L95再desugar_match_defs把等式规则转成 match 树L107随后按linearize_matches选项做自动线性化L123-L127再encode_matches编码 match/switchL135最后按float_combinators选项浮升组合子L149-L151。每步之间还有check_unbound_vars、check_unbound_refs等健全性检查。匹配非连续数字距离计算的优化与switch不同等式模式匹配函数允许直接匹配非连续的数字或字符。下面的解析函数用字符字面量做模式Parse ( Token.LParenthesis Parse ) Token.RParenthesis Parse λ Token.Lambda Parse n (Token.Name n)编译器会把它变换为一串经过优化的switch级联每个 switch 都计算当前值与最小字符之间的距离从而高效地逐一测试每个分支Parse λarg0 switch matched (- arg0 () { 0: Token.LParenthesis # ) 1 - ( 在编译期被解析为常量 _: switch matched (- matched-1 ( ) - 1 - ( )) { 0: Token.RParenthesis _: switch matched (- matched-1 ( λ - 1 - ) )) { 0: Token.Lambda _: use n ( 1 matched-1); (Token.Name n) } } }与switch直接提供“前驱变量”不同等式模式匹配函数中你无法直接访问被匹配值的前驱只能匹配一个变量变量会被绑定为基于匹配值计算出的表达式。例如上例中n被绑定为( 1 matched-1)即通过累计距离把原始字符重建出来。这一行为在源码中体现为 desugar_match_defs.rs 的num_rule函数它对数字列做两件事——按数字模式出现的实际值nums逐个生成分支每个分支用(- arg num_i)计算距离最外层用(- arg num_0)判断是否命中第一个数字见 L397-L415 的级联构造默认分支里把通配变量恢复为( 1 pred_var)其中pred_var名为%arg-1并通过fast_pred_accessdesugar_match_defs.rs优化若函数体中出现了形如(- var cur_num)的表达式则直接替换为前驱变量避免多余的减法计算。同时num_rule强制要求数字匹配必须有默认分支L318-L321由于数字本质上拥有 2^60 个“构造子”无法穷举因此必须提供通配符兜底否则报NumMissingDefault错误。规则顺序与通配符覆盖等式模式匹配的规则是从上到下依次尝试的通配符*可以覆盖多个具体分支。以下定义是合法的pred_if Bool/False * if_false if_false pred_if Bool/True p * (- p 1) pred_if Bool/True 0 * 0第一条规则的*同时覆盖了后两条规则中第二个参数为p任意数字和0的情况因此只有当第一个参数是Bool/True时才会轮到后面的规则——第一条中的通配符只影响Bool/False情形。编译器在将规则矩阵转换成 match 树时会为每个构造子建立分支desugar_match_defs.rs 的switch_rule并把“变量模式”所在的分支改写为匹配该构造子的所有字段再用use var (Ctr 字段0 .. 字段M)重建原变量L524-L542。如果某个构造子没有任何规则覆盖则报“非穷尽匹配”AdtNotExhaustive错误。字符串与列表模式统一 desugar 为 Cons/Nil字符串与列表的模式匹配会被去糖desugar为对 Cons 和 Nil 的匹配Hi hi 1 Hi _ 0 Foo [] 0 Foo [x] x Foo _ 3 # 实际变成 Hi (String/Cons h (String/Cons i String/Nil)) 2 Hi _ 0 Foo List/Nil 0 Foo (List/Cons x List/Nil) x Foo _ 3注意Hi hi的i字符原本是String/Cons i String/Nildesugar 后直接以字面字符形式呈现最终仍编码为String/Cons。在编译期字符串与列表模式分别被 desugar_match_defs.rs 的Pattern::to_type识别为内建STRING与LISTADT从而走构造子匹配的同一套编译路径。编译期错误与警告等式模式匹配函数的降级过程会进行多项完整性检查不满足时报错或告警错误消息模板见 desugar_match_defs.rs诊断类型触发条件级别AdtNotExhaustiveADT 的某个构造子没有任何规则覆盖错误NumMissingDefault数字模式缺少默认通配符分支错误TypeMismatch同一列出现不同 ADT 类型的构造子错误RepeatedBind同一规则内同名变量重复绑定警告UnreachableRule某条规则永远不可能被匹配到警告其中“类型不匹配”由 desugar_match_defs.rs 的infer_from_def_arg在逐列推断模式类型时检测列的类型从Any出发一旦发现两个互不兼容的具体类型即报错。这些检查保证了模式矩阵是良构的也解释了为什么通配符列Type::Any可以与任意类型共存。用 golden 测试验证编译行为Bend 仓库用 golden 测试固化了上述编译行为可直接在 tests/golden_tests/encode_pattern_match/ 中查看输入与预期输出and3.bend、bool.bend布尔构造子的 Scott/NumScott 编码产物match_num_adt_tup_parser.bend数字、ADT、元组混合的模式匹配编码non_matching_fst_arg.bend首个参数不匹配时的规则过滤pattern_match_encoding.bend、match_auto_linearization.bendmatch 自动线性化的产物switch_in_switch_arg.bendswitch 嵌套在 switch 参数位置的场景。这些测试与 docs/pattern-matching.md 中的例子互为印证文档解释语义与设计意图测试则锁定具体编码输出防止编译器改动破坏既定行为。小结Bend 的模式匹配体系可以概括为一条清晰的降级链等式模式匹配函数 →desugar_match_defs.rs→match/switch树 →encode_match_terms.rs、encode_adts.rs→ 核心项。实践中最需要记住的几点switch只针对数字多个分支会被编译为逐层减 1 的简单 switch 级联默认分支里可直接访问前驱变量match的编译结果取决于 ADT 编码默认adt-num-scott用数字 tag 分发并支持 IOadt-scott则完全基于 lambda 续延等式模式匹配函数能力最强但实现细节由编译器决定严格模式下默认开启的-Olinearize-matches与-Ofloat-combinators会生成Foo__C*形式的组合子保证递归定义不会在严格求值时无限展开数字与字符模式匹配采用“距离计算”优化且必须提供默认分支字符串与列表模式统一 desugar 为Cons/Nil匹配编译器会在降级阶段诊断非穷尽、类型不匹配、重复绑定与不可达规则等问题帮助你在编译期尽早发现模式矩阵的错误。【免费下载链接】BendA massively parallel, high-level programming language项目地址: https://gitcode.com/GitHub_Trending/be/Bend创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表