Bend 编译与读回(Compilation and Readback)全解析:从 Lambda 项到 HVM 交互网节点
2026/9/13 22:36:36 网站建设 项目流程

Bend 编译与读回(Compilation and Readback)全解析:从 Lambda 项到 HVM 交互网节点

【免费下载链接】BendA massively parallel, high-level programming language项目地址: https://gitcode.com/GitHub_Trending/be/Bend

本文围绕 Bend(GitHub_Trending/be/Bend 仓库)中的 compilation-and-readback.md 展开,系统讲解 Bend 高阶函数式程序如何被编译为 HVM(Higher-order Virtual Machine)交互网(Interaction Net,IC)节点,以及计算结果如何被读回(readback)为可读的 Bend 项。你将掌握 7 种 HVM 节点类型与极性的对应关系、λ/应用/复制/叠加等核心项的编码规则、全部编译器 pass 的执行顺序,以及如何用-O选项控制每个 pass 的行为。

HVM 交互网:节点与端口的底层模型

HVM 交互网由大量节点(node)与连接节点的线(wire)构成。每个节点包含一个main端口(编号0)和两个auxiliary端口(编号12)。Bend 编译器把高阶 λ 演算程序"降级"(lower)成这样的图结构,再由 HVM 运行时(如 CUDA 后端)进行并行归约。

按 compilation-and-readback.md 的说明,共有 7 种节点:

节点种类含义
Eraser(擦除)表示被丢弃的值(*
Constructor(构造器,CON)承载 λ、应用、元组及其消解
Duplicator(复制器,DUP)承载复制(duplication)与叠加(superposition)
Reference(引用,REF)指向顶层函数的引用
Number(数字,NUM)用标签(label)存放数字本身
Operation(运算,OPR)用标签存放运算种类
Match(匹配,SWI)对数字的switch匹配

这一节点集合在源码中的NodeKind枚举里有直接对应:src/net/mod.rs 定义了RotEraOprSwiCon(Option<BendLab>)Tup(Option<BendLab>)Dup(BendLab)等种类。

同一个节点,不同语义:CON 与 DUP 的上下文歧义

交互网的一个精妙之处在于:节点种类相同,但通过"从哪个端口进入"来区分语义

λ 与应用共用 CON 节点

一个 λ 项λx x编译为一个 Constructor 节点,一个应用((λx x) (λx x))同样编译为 Constructor 节点。两者共享同一个 CON 节点结构:

0 - 指向 λ 出现的位置 0 - 指向函数 | | λ Lambda @ Application / \ / \ 1 2 - 指向 λ 体 1 2 - 指向应用出现的位置 | | 指向 λ 变量 指向实参

读回时(net_to_term)通过访问端口判断语义:从端口 0 进入 CON,则它是 λ(或元组);从端口 2 进入 CON,则它是应用;从端口 1 进入,则是变量。这一逻辑在 src/fun/net_to_term.rs 的read_con中实现:端口 0 分支再根据is_tup启发式区分元组与 λ,端口 2 分支读取函数与实参构造Term::App

复制与叠加共用 DUP 节点

let {a b} = x(复制)与叠加{a b}(superposition)都编译为 Duplicator 节点,差异同样来自上下文:

0 - 指向叠加出现的位置 0 - 指向被复制的值 | | # Superposition # Duplication / \ / \ 1 2 - 指向第二个值 1 2 - 指向第二个绑定 | | 指向第一个值 指向第一个绑定

读回时read_fan(src/fun/net_to_term.rs)处理 DUP/TUP 节点:从端口 0 进入表示叠加/元组值,从端口 1 或 2 进入表示发现了一个复制,将节点暂存入scope以便后续作为let展开(Splitinsert_split负责把分裂插入到变量使用的最低公共祖先处)。

Bend 核心项到 HVM 节点的完整映射

compilation-and-readback.md 给出了 Bend 核心项的直接编译规则,这里结合 src/fun/term_to_net.rs 的encode_term逐条展开:

Bend 核心项HVM 节点极性(polarization)
Application(应用)CON--+
Lambda(λ)CON++-
Duplication(复制let {a b} = xDUP-++
Superposition(叠加{a b}DUP+--
Pairs(元组)CON+--
Pair elimination(元组解构)CON-++
Erasure values(如λx *ERA+
Erased variables(如λ* xERA+
Numbers(数字)NUM恒为+
Switches(数字匹配)MAT(--+)+ 端口 1 上连接一个 CON(+--),分别指向0分支与>= 1分支
Numeric operations(数值运算)OPR(--+)+ 一个存放运算种类的 NUM(按 HVM2 论文约定)
References to top-level functions(顶层函数引用)REF+

从源码看,encode_term对每个分支的处理与表格一一对应:

  • Term::Lam→ 新建 CON 节点,端口 1 编码模式、端口 2 编码函数体(src/fun/term_to_net.rs);
  • Term::App→ 新建 CON 节点,端口 0 编码函数、端口 1 编码实参,并把"出现位置"接到端口 2(src/fun/term_to_net.rs);
  • Term::Swt→ 一次性分配一个Swi节点(其端口 1 上挂一个 CON,fst/snd 分别编码 0 分支与 succ 分支)与两个子节点(src/fun/term_to_net.rs);
  • Term::Oper→ 构造 OPR 节点,并用一个带运算标签的 NUM 挂在端口 1 上表示操作种类;当只部分应用一个参数时,编译器还会做"翻转(flip)"处理(如SUBFP_SUBDIVFP_DIVLTGT),见flip_sym(src/fun/term_to_net.rs);
  • Term::Ref→ 直接生成Tree::Ref,端口为+
  • Term::Era/ 无绑定名的变量模式 →Tree::Era
  • Term::LetTerm::Fan(含元组)→ 通过lets栈与make_node_list生成 DUP/TUP 节点链。

此外,term_to_net.rs 的term_to_hvm在编码完成后还会做一次"恶性环"(vicious cycle)检测:若created_nodes与网络中实际统计到的节点数不一致,则报错拒绝生成该网络——这是编码阶段保证结果可归约的防御性检查。

match表达式并不直接编译,而是根据adt-encoding选项先翻译为上述核心构造。以type Maybe = (Some val) | None为例(见 pattern-matching.md):

  • adt-num-scott(默认):Maybe/Some = λval λx (x 0 val)Maybe/None = λx (x 1)UnwrapOrZero变成对数字 tag 的switch
  • adt-scottMaybe/Some = λval λMaybe/Some λMaybe/None (Maybe/Some val),匹配变成纯 λ 应用(x λx.val x.val 0)

读回(Readback):从网络还原为 Bend 项

运行结果拿到的是归约后的交互网,需要net_to_term将其还原为可显示的 Bend 项。完整流程见 src/lib.rs 的readback_hvm_net

  1. hvm_to_net把 HVM 文本格式的网络解析为内部INet
  2. 调用 src/fun/net_to_term.rs 的net_to_term遍历网络,按节点种类与入口端口还原术语;
  3. succ分支可能出现的生成函数执行expand_generated
  4. 根据adt-encoding重新加糖(resugar_stringsresugar_lists),把编码后的字符串/列表还原为字符串字面量与[a, b, c]语法。

读回过程中的几个关键机制:

  • CON 节点语义判定read_con依据端口号 +is_tup启发式(端口 1 是否为"闭合树")区分 λ/应用/元组/元组解构;
  • 叠加与复制的配对解析:非线性读回(linear = false)时,dup_paths记录 DUP 标签对应的栈,用于把叠加值与复制变量正确配对(read_fan);
  • 数值运算还原read_opr从 OPR 节点的 NUM 标签中读取运算符号与数值类型(U24/I24/F24),还原出Term::Oper;遇到非法的数字匹配、非法运算、读到根节点或循环网络时,会记录ReadbackErrorInvalidNumericMatchInvalidNumericOpReachedRootCyclic)并给出中文可读的警告(src/fun/net_to_term.rs);
  • η 归约decay_or_get_ports在 CON/TUP/DUP 满足特定连接形态(端口 1、2 连接到同类节点的 1、2 端口)时直接读取对端端口 0,等价于对λa let (a,b) = a; (a,b)这类"解构后又重构"的网络做读回级 η 归约;
  • 无作用域变量恢复collect_unscoped+apply_unscoped把游离变量转换为Link/Chn形式,处理全局 λ 等无作用域绑定。

Bend 编译器 Pass 全清单

compilation-and-readback.md 列出了完整的 pass 序列,以下按文档顺序整理并标注对应源码:

Pass作用源码位置
encode_adt为构造器生成函数(按编码选项)encode_adts.rs
desugar_open把 open 项转换为 match 项desugar_open.rs
encode_builtins把内建类型的语法糖(list、string 等)转换为函数调用builtins.rs
desugar_match_def把等式风格的模式匹配函数转换为 match/switch 树desugar_match_defs.rs
fix_match_terms规范化所有 match 与 switch 项fix_match_terms.rs
lift_local_defs把局部def提升为顶层函数lift_local_defs.rs
desugar_bendbend项转换为顶层函数desugar_bend.rs
desugar_foldfold项转换为顶层函数desugar_fold.rs
desugar_with_blockswith<-(ask)转换为 monadic bind 与 wrapdesugar_with_blocks.rs
make_var_names_unique为每个函数中的每个变量生成唯一名unique_names.rs
desugar_use通过替换消解use别名(语法级复制)desugar_use.rs
linearize_matcheslinearize-matches选项线性化 match/switch 中的变量linearize_matches.rs
linearize_match_with线性化with子句中的变量(若尚未被上一步处理)linearize_matches.rs
type_check_book运行类型检查(仅推断/检查,不进行 elaboration)check/type_check.rs
encode_matchesadt-encoding把 match 项变换为 λ 演算形式encode_match_terms.rs
linearize_vars线性化变量出现:多用则复制、无用则擦除、仅出现一次的let直接内联linearize_vars.rs
float_combinators按源码中描述的尺寸启发式,把组合子提升为顶层函数float_combinators.rs
pruneprune选项删除未使用函数definition_pruning.rs
merge_definitions合并完全相同的顶层函数definition_merge.rs
expand_main解引用展开main,使其包含真实计算而非懒引用expand_main.rs
book_to_hvm降级到 HVM(即本文前半部分介绍的编码过程)term_to_net.rs
eta在 inet 层做 η 归约,但不归约两端为ERA/NUM的节点(逻辑等价但用户观感异常)eta_reduce.rs
check_cycles启发式检查可能引发 HVM 循环的互递归函数调用环mutual_recursion.rs
inline_hvm_book内联那些编译为 nullary 节点(REF/NUM/ERA)的 REFinline.rs
prune_hvm_book在 inet 层 η 归约后的额外剪枝层prune.rs
check_net_sizes确保生成的每个定义不会过大而无法在 CUDA 运行时上运行check_net_size.rs
add_recursive_priority在 inet 层为部分二元递归调用打标记,便于 GPU 运行时合理分配工作add_recursive_priority.rs

这些 Pass 在源码中的真实执行顺序

上述 pass 并非文档中的简单罗列,其真实执行流程体现在 src/lib.rs 的desugar_book与 src/lib.rs 的compile_book

  • desugar_book依次执行check_shared_namesset_entrypointencode_adtsfix_match_defsapply_argsdesugar_openencode_builtinsresolve_refsdesugar_match_defsfix_match_termslift_local_defsdesugar_benddesugar_folddesugar_with_blockscheck_unbound_vars→ 变量唯一化与desugar_use→ 按选项执行linearize_matches(含Alt变体)→linearize_match_with→ 类型检查 →encode_matches→ 再次检查未绑定变量 → 唯一化与desugar_uselinearize_vars→ 按选项float_combinators→ 未绑定引用检查 →prunemerge_definitionsexpand_main
  • 随后compile_book调用book_to_hvm生成 HVM 网络,再按选项依次执行eta(η 归约)、check_cycles、再次etainline_hvm_bookprune_hvm_bookcheck_net_sizesadd_recursive_priority

可以看到desugar_bookcompile_book之间有清晰的职责划分:前者完成项级(term-level)转换与优化,后者完成网络级(inet-level)优化。其中多个 pass 之间还穿插着"健全性检查"(sanity check),例如check_unbound_varscheck_unbound_refs,保证每个阶段产出的项都保持良好约束。

用 -O 选项控制编译 Pass

编译 pass 的行为由命令行选项控制,详见 compiler-options.md。下表整理了全部选项及其默认值:

选项默认值作用
-Oall关闭启用全部编译器 pass
-Ono-all关闭禁用全部编译器 pass
-Oeta/-Ono-eta关闭(eta 默认开启,见下方说明)对已定义函数做 η 归约
-Oprune/-Ono-prune关闭定义剪枝(删除未使用定义)
-Olinearize-matches/-Olinearize-matches-alt/-Ono-linearize-matches启用match 线性化
-Ofloat-combinators/-Ono-float-combinators启用浮出组合子
-Omerge/-Ono-merge关闭定义合并
-Oinline/-Ono-inline关闭内联 nullary 项
-Ocheck-net-size/-Ono-check-net-size关闭(编译时默认开启,见下方说明)网络尺寸检查(CUDA 限制 64 节点)
-Oadt-scott/-Oadt-num-scottadt-num-scottADT 编码方式
-Otype-check/-Ono-type-checktype-check类型检查

注意:表中"默认值"一列对应 compiler-options.md 的表格;而CompileOpts::default()的实现(src/lib.rs)实际默认开启了etalinearize_matchesfloat_combinatorscheck_net_sizetype_check。同时-Ono-all不会关闭type_check(见set_no_all,src/lib.rs),且严格模式(禁用float_combinators/linearize_matches)可能引发无限展开,编译器会打印相应警告(check_for_strict)。

几个与编译结果直接相关的选项行为:

  • η 归约id_id = λx (id x)-Oeta下变成id_id = id,在-Ono-eta下保持λz (id z)
  • 定义剪枝-Oprune移除Id2 = Id这类未被使用的定义;
  • 定义合并id = λx xalso_id = λx x-Omerge下合并为id_$_also_id,HVM 输出由两个@id/@also_id变为单个@a
  • 内联-Oinline@foo = 2内联进@main,HVM 输出中& @id ~ (@foo a)变为& @id ~ (2 a)
  • 网络尺寸检查-Ocheck-net-size要求每个函数编译后至多 64 个 HVM 节点(CUDA 运行时内存限制),radix_sort这类展开函数在开启时会报Definition is too large for hvm;非*-cu后端可关闭;
  • ADT 编码-Oadt-scott为每个构造器生成一个 λ;-Oadt-num-scott用数字 tag 指示构造器(Option/Some/tag = 0Option/None/tag = 1)。注意IO 仅在-Oadt-num-scott下可用
  • 类型检查:默认开启,def main() -> Bool: return 3会报Expected function type 'Bool' but found 'u24'-Ono-type-check下则正常编译并返回3

实战验证:编译 → 运行 → 读回的完整链路

将以上概念串起来,一条完整的执行链路是:

bend run <路径> [表达式形式的参数]...

以 cli-arguments.md 中的程序为例:

def main(x, y): return {x - y, y - x}
  • bend run <path> +5 +3→ 输出{+2 -2}
  • 只传一个参数bend run <path> +5→ 输出λa {(- a 5) (- a +5)}(缺参时结果为部分应用,读回为 λ);
  • 传三个参数+5 +3 +1→ 仍输出{+2 -2},多出的参数因交互规则被自然忽略。

运行机制见 src/lib.rs 的run_hvm:编译器把hvm_book以 pretty 形式写入.out.hvm,以子进程方式调用 HVM 二进制执行,再从输出中按Result:标记(HVM_OUTPUT_END_MARKER)截取结果网络(parse_hvm_output),最终交给读回模块还原为 Bend 项。这就是"编译 → 归约 → 读回"三个阶段的闭环。

仓库测试中,tests/golden_tests.rs 覆盖了desugar_file(查看各 desugar pass 输出)、compile_file(查看book_to_hvm后的 HVM 输出)、readback_hvm(网络读回)等场景,快照存放在 tests/snapshots 下,是观察每个 pass 实际产物最直接的素材:例如cli__compile_no_opts.bend.snapcli__compile_pre_reduce.bend.snap展示了不同优化开关下的 HVM 输出差异。

小结

Bend 的编译与读回是一套"项 → 图 → 项"的完整闭环:项级 pass 先把各类语法糖消解为 λ 演算核心项,book_to_hvm再按统一的 CON/DUP/ERA/NUM/OPR/SWI/REF 编码规则把核心项映射为 HVM 交互网;运行时归约完成后,读回模块依据入口端口与节点种类还原出可读的 Bend 项。理解这 7 种节点与极性映射,以及 20 余个 pass 的执行顺序,是调试编译输出、调优-O选项、甚至为 Bend 贡献新优化 pass 的基础。

【免费下载链接】BendA massively parallel, high-level programming language项目地址: https://gitcode.com/GitHub_Trending/be/Bend

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

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

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

立即咨询