Bend 语言 Dups 与 Sups 完全指南:手工复制与超级位置叠加的原理与实践
【免费下载链接】BendA massively parallel, high-level programming language项目地址: https://gitcode.com/GitHub_Trending/be/Bend
导读
dup(duplication,复制)与sup(superposition,叠加/超级位置)是 Bend 这一大规模并行高级语言中一对互为镜像的核心机制:dup负责把一个项按需复制成多份,sup则把一个"可能值"叠加起来参与后续所有计算。本文以 docs/dups-and-sups.md 为主干,结合 src/fun/parser.rs、src/fun/transform/linearize_vars.rs 等源码,系统讲解let手工复制语法、{...}叠加语法、二者配对时的"构造/析构"行为,以及两个dup相互干扰导致结果错误这一关键限制,帮助读者在编写高阶函数与并行程序时规避这一类陷阱。
一、术语背景:Bend 中的复制与叠加
Bend 建立在交互组合子(Interaction Combinators)模型之上,程序会被编译成由节点与端口构成的交互网络(interaction net),这一转换由 src/fun/term_to_net.rs 负责。在 HVM(Higher-order Virtual Machine)层面,dup与sup最终都体现为带标签的扇出节点(fan node),即 src/fun/mod.rs 中定义的Term::Fan:
/// Either a tuple or a superposition Fan { fan: FanKind, // FanKind::Dup 或 FanKind::Tup tag: Tag, els: Vec<Term>, },理解这一底层表示有助于解释本文后面涉及的种种行为:
dup(复制):把某个值拆成多份副本分别供多个使用者使用。使用次数超过一次的变量,编译器会自动复制;同时语言也提供了let语法进行手工复制。sup(叠加):把两个(或多个)值"叠加"成一个不确定项,是所有可能值的并行替身。任何操作作用在叠加态上,都会在每种可能性上分别执行,天然映射为并行分支。dup与sup的对偶性:一次复制正好对应一次叠加的"消耗"。当一个dup遇到一个sup时,二者发生交互,等价于把叠加的各个分支分别分配给复制的各个变量,即"构造"与"析构"一对值。
二、手工复制:用let编写dup
2.1 基本语法
当一个变量在函数体中被使用多次时,Bend 会自动完成复制,无需任何特殊标记。但也可以使用let语句手动把一项复制成多份。语法上,dup表现为let {a1 a2} = value; ...的形式,花括号内给出要绑定到的多个新变量:
# 使用 let 手工复制,丘奇编码的 2 ch2 = λf λx let {f1 f2} = f; (f1 (f2 x)) # 丘奇编码的 3,需要连续两次复制 ch3 = λf λx let {f0 f1} = f; let {f2 f3} = f0; (f1 (f2 (f3 x)))上面的ch2把函数f复制为f1、f2两份,再依次应用两次,得到丘奇数 2;ch3则先复制出f0、f1,再对f0做第二次复制得到f2、f3,从而获得三次应用。这正体现了dup的语义:一个值,多个副本,互不共享。
2.2 编译器如何实现自动复制
即使不写任何let,只要某个变量在作用域内被引用多次,编译器也会自动生成对应的dup。这一逻辑位于 src/fun/transform/linearize_vars.rs,其核心是"按引用次数线性化":
- 若某个绑定被引用
0次,该绑定被擦除(Erase binding); - 若被引用
1次,保持原样(Keep as-is); - 若被引用超过
1次,则自动插入Term::Let,其模式为duplicate_pat(nam, uses)——即构造一个Pattern::Fan(FanKind::Dup, Tag::Auto, ...),把原变量复制成与使用次数相等的若干份。
fn duplicate_pat(nam: &Name, uses: u64) -> Box<Pattern> { Box::new(Pattern::Fan( FanKind::Dup, Tag::Auto, (1..uses + 1).map(|i| Pattern::Var(Some(dup_name(nam, i)))).collect(), )) }可见,手写let {f1 f2} = f与编译器自动插入的复制在语义上完全等价,都是往 AST 里注入一个FanKind::Dup模式。这也是为什么文档示例中List/map里对参数f的两次使用会被编译器"展开"成显式的{f1 f2} = f。
三、叠加态:用{}定义sup
3.1 基本语法
sup使用花括号写出,花括号内包含两个(或多个)可能值:
sup = {3 7}{3 7}表示一个"同时等于 3 或 7"的叠加值。从解析器实现看,{开头的项在 src/fun/parser.rs 中被解析为:
// Sup if self.starts_with("{") { let els = self.list_like(|p| p.parse_term(), "{", "}", ",", false, 2)?; return Ok(Term::Fan { fan: FanKind::Dup, tag: tag.unwrap_or(Tag::Auto), els }); }即:sup在语法树里同样被表示为Term::Fan,与dup共用同一节点类型,只是它出现在"值"的位置上;而let {x1 x2} = ...形式的dup出现在"模式(pattern)"的位置上(见 src/fun/parser.rs)。二者一个构造、一个析构,恰好构成一对。
3.2 叠加参与计算的传播规则
叠加态可以出现在任何期望普通值的位置。任何运算与叠加态交互时,都会对每个可能值分别执行一次,结果仍然是叠加态:
mul = λa λb (* a b) result = (mul 2 5) # 返回 10 result_sup = (mul 2 {5 7}) # 返回 {10 14} multi_sup = (mul {2 3} {5 7}) # 返回 {{10 14} {15 21}}- 单个叠加参数
{5 7}:乘法分别在5、7上执行,得到{10 14}; - 两个叠加参数
{2 3}与{5 7}:结果成为两层的叠加,{{10 14} {15 21}},覆盖全部四种组合。
这一"笛卡尔积式"的传播就是叠加并行性的来源:在一次计算中同时探索所有可能性,而不是串行遍历。对应的实际用例可见测试 tests/golden_tests/run_file/sup_app.bend:
main = ({(λx x) (λx x)} 3)这里把两个恒等函数叠加后应用于3,测试验证了叠加应用于函数时的行为。
3.3 叠加态的读取
在交互网络上运行结束后,结果中的sup需要被读回(readback)为语法层面的项。这一过程由 src/fun/net_to_term.rs 的read_fan完成:如果叠加节点在读取路径上遇到配对的dup,就按dup的各个分支把叠加值拆开(FanKind::Dup分支);如果没有配对的dup,则原样保留为叠加项(Term::Fan)。也就是说,未配对的sup会作为一等值存活在结果中。
四、dup与sup配对:等价于元组的构造与析构
当把一个叠加值与一个复制配对时,二者恰好构成"构造/析构"的逆运算:
# 每个 dup 变量现在都各自拿到 {1 2} 叠加中的一份 let {x1 x2} = {1 2}语义上,{1 2}构造了一个"同时是 1 和 2"的值,而let {x1 x2} = ...将其析构:x1拿到1,x2拿到2。二者合起来等价于一个元组(1, 2)的打包与解包。
从交互组合子的角度解释:sup生成一个扇出(fan-out)节点,dup生成一个扇入(fan-in)节点,二者相遇即发生交互(annihilation/commutation 规则),把两端的值一一配对。这正是 Bend 中实现"一个函数返回多个值""复制与叠加互相抵消"等模式的底层机制。
五、关键限制:两个dup相互干扰会破坏结果
5.1 破坏性干涉的产生
由于复制在编译后的交互网络中是以特定方式排列的,当两个dup相遇时,它们会互相产生破坏性干涉(destructive interference)。此时的结果虽然在 HVM 层是良定义的(well defined),但在 λ 演算语义层面却是错误的:
因此,正确的 Bend 程序必须满足一条强约束:一个变量不应复制另一个本身也在复制变量的变量。
换句话说,同一段计算路径上只能存在"单一来源"的复制。若出现嵌套/重叠的双重复制,各dup的端口配对会被打乱,导致变量绑定错位。
5.2 高阶函数中的典型踩坑示例
下面这段来自原文档的示例,展示了在使用高阶函数时双重复制如何导致错误:
def List/map(xs: List(A), f: A -> B) -> List(B): fold xs: case List/Nil: return List/Nil case List/Cons: # 'f' 在这里被复制 return List/Cons(f(xs.head), List/map(xs.tail, f)) # 上面这行会被编译器转换为对 'f' 的显式复制: # {f1 f2} = f # return List/Cons(f1(xs.head), List/map(xs.tail, f2)) def main() -> _: # 这个 lambda 复制了 `x`,同时又因为 List/map 而自身被复制。 # 这会导致错误行为。 # 在当前这个具体例子中,运行时能捕获到并报错, # 但目前并非总是如此。 return List/map([1, 2, 3], lambda x: (+ x x))逐步拆解这里的双重复制来源:
List/map的case List/Cons分支中,参数f被使用两次(f(xs.head)与递归调用List/map(xs.tail, f)),因此编译器自动把f复制为f1、f2(对应 src/fun/transform/linearize_vars.rs 中的duplicate_pat逻辑),这就是第一层dup。- 传入的 lambda
λx (+ x x)内部复制了变量x(x被使用两次),这构成第二层dup。 - 当
List/map复制f时,被复制的对象本身还含有一个内部dup(x的复制)。两个dup叠加在一起,产生破坏性干涉,最终得到错误结果。
在文档给出的这个具体例子中,运行时恰好能够检测到这种异常并报错;但文档明确强调,并非所有情况都能被运行时捕获,因此不能依赖运行时兜底,而应在编写时就避免双重复制。
5.3 解决方案:只保留一个复制来源
要修复这类程序,必须保证复制来源唯一,二选一即可:
- 让
List/map保持线性:f在List/map内部不被复制(例如改为对f的引用只出现一次,或者使用use等机制避免复制); - 让传入的函数保持线性:传入的 lambda 内部不复制任何变量,即
λx (+ x x)改为不重复使用x的函数。
只要满足其中一条,复制来源就只有一个,dup与dup不会相遇,结果保持正确。这是编写高阶、递归函数时必须牢记的 Bend 特有约束。
六、实践要点与使用建议
综合上述原理与限制,在实际编写 Bend 程序时可遵循以下要点:
| 场景 | 推荐写法 | 说明 |
|---|---|---|
| 手动复制一个项 | let {a b} = value; ... | 等价于编译器对多引用变量的自动复制 |
| 表示多个可能值并行计算 | {v1 v2} | 运算结果自动成为各可能值的叠加 |
| 叠加参与多次运算 | (f {a b} {c d}) | 结果按笛卡尔积展开为多层叠加 |
| 构造/析构配对 | let {x1 x2} = {1 2} | 等价于元组的打包与解包 |
| 高阶函数传参 | 传入线性函数,或保证高阶函数不复制参数 | 避免两个dup相遇产生破坏性干涉 |
几点补充说明:
- 叠加与类型检查:
sup属于未类型化(untyped)特性的范畴。在类型检查开启时,函数体内的Term::Fan { fan: FanKind::Dup, .. }会触发错误 "Superposition term in type-checked function",见 src/fun/check/check_untyped.rs。也就是说,叠加主要用于未类型化的快速原型与并行探索场景。 - 显式与隐式复制并存:手写
let {f1 f2} = f与依赖编译器自动复制(src/fun/transform/linearize_vars.rs)在语义上一致,二者可以混用。 - 结果读取:运行结果中的叠加在读取回语法树时会保留(src/fun/net_to_term.rs),因此可以直接在终端观察叠加分支展开后的完整结构。
- 测试佐证:仓库的 golden 测试(tests/golden_tests/run_file/sup_app.bend 与快照
run_file__sup_app.bend.snap)覆盖了叠加应用于函数的运行行为,可作为验证叠加语义的最小样例。
总结
dup与sup是 Bend 并行模型在语言层最直接的体现:dup用let {a b} = x手工复制项,sup用{a b}叠加可能值,二者配对时等价于元组的构造与析构,叠加在参与任何运算时都会按分支并行展开。与此同时,Bend 对复制来源有严格限制——一个变量不能复制另一个自身也在复制的变量,否则两个dup的破坏性干涉会产生 λ 层语义错误。理解并遵守这一约束,是写出正确高阶函数与并行程序的前提。
【免费下载链接】BendA massively parallel, high-level programming language项目地址: https://gitcode.com/GitHub_Trending/be/Bend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考