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端口(编号1、2)。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 定义了Rot、Era、Opr、Swi、Con(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展开(Split与insert_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} = x) | DUP | -++ |
Superposition(叠加{a b}) | DUP | +-- |
| Pairs(元组) | CON | +-- |
| Pair elimination(元组解构) | CON | -++ |
Erasure values(如λx *) | ERA | + |
Erased variables(如λ* x) | ERA | + |
| 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)"处理(如SUB↔FP_SUB、DIV↔FP_DIV、LT↔GT),见flip_sym(src/fun/term_to_net.rs);Term::Ref→ 直接生成Tree::Ref,端口为+;Term::Era/ 无绑定名的变量模式 →Tree::Era;Term::Let、Term::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-scott:Maybe/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:
- 用
hvm_to_net把 HVM 文本格式的网络解析为内部INet; - 调用 src/fun/net_to_term.rs 的
net_to_term遍历网络,按节点种类与入口端口还原术语; - 对
succ分支可能出现的生成函数执行expand_generated; - 根据
adt-encoding重新加糖(resugar_strings、resugar_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;遇到非法的数字匹配、非法运算、读到根节点或循环网络时,会记录ReadbackError(InvalidNumericMatch、InvalidNumericOp、ReachedRoot、Cyclic)并给出中文可读的警告(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_bend | 把bend项转换为顶层函数 | desugar_bend.rs |
desugar_fold | 把fold项转换为顶层函数 | desugar_fold.rs |
desugar_with_blocks | 把with与<-(ask)转换为 monadic bind 与 wrap | desugar_with_blocks.rs |
make_var_names_unique | 为每个函数中的每个变量生成唯一名 | unique_names.rs |
desugar_use | 通过替换消解use别名(语法级复制) | desugar_use.rs |
linearize_matches | 按linearize-matches选项线性化 match/switch 中的变量 | linearize_matches.rs |
linearize_match_with | 线性化with子句中的变量(若尚未被上一步处理) | linearize_matches.rs |
type_check_book | 运行类型检查(仅推断/检查,不进行 elaboration) | check/type_check.rs |
encode_matches | 按adt-encoding把 match 项变换为 λ 演算形式 | encode_match_terms.rs |
linearize_vars | 线性化变量出现:多用则复制、无用则擦除、仅出现一次的let直接内联 | linearize_vars.rs |
float_combinators | 按源码中描述的尺寸启发式,把组合子提升为顶层函数 | float_combinators.rs |
prune | 按prune选项删除未使用函数 | 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)的 REF | inline.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_names→set_entrypoint→encode_adts→fix_match_defs→apply_args→desugar_open→encode_builtins→resolve_refs→desugar_match_defs→fix_match_terms→lift_local_defs→desugar_bend→desugar_fold→desugar_with_blocks→check_unbound_vars→ 变量唯一化与desugar_use→ 按选项执行linearize_matches(含Alt变体)→linearize_match_with→ 类型检查 →encode_matches→ 再次检查未绑定变量 → 唯一化与desugar_use→linearize_vars→ 按选项float_combinators→ 未绑定引用检查 →prune→merge_definitions→expand_main;- 随后
compile_book调用book_to_hvm生成 HVM 网络,再按选项依次执行eta(η 归约)、check_cycles、再次eta、inline_hvm_book、prune_hvm_book、check_net_sizes与add_recursive_priority。
可以看到desugar_book与compile_book之间有清晰的职责划分:前者完成项级(term-level)转换与优化,后者完成网络级(inet-level)优化。其中多个 pass 之间还穿插着"健全性检查"(sanity check),例如check_unbound_vars与check_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-scott | adt-num-scott | ADT 编码方式 |
-Otype-check/-Ono-type-check | type-check | 类型检查 |
注意:表中"默认值"一列对应 compiler-options.md 的表格;而CompileOpts::default()的实现(src/lib.rs)实际默认开启了eta、linearize_matches、float_combinators、check_net_size与type_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 x与also_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 = 0、Option/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.snap、cli__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),仅供参考