“返回的是携带 x 的函数值。把内层改成 后, 不能只返回 的代码地址;未来调用者只给 y,代码地址无法区分 make(4) 与 make(9)。可以返回“代码、已供 x”的部分应用记录,但它…”
形式陈述
输入、输出与保持的行为
输入是一门从左到右按值求值的小语言:整数、变量、非递归 let、单参数函数、调用、二元组,以及 ref e、!e、e1 := e2。加法按先左后右求值;e1;e2 先丢弃 e1 的结果再计算 e2;fst/snd 投影二元组。三元组 (a,b,c) 是 (a,(b,c)) 的书写糖。按可变存储语义,绑定本身不可变,赋值只改变显式引用单元;两个变量可以保存同一位置。整数按数学整数处理,或要求本例运算落在目标整数范围内。下面没有并发、地址算术、弱引用和终结器。
名字先按绑定身份解析。两个都写成 x 的声明分别得到 x₁、x₂,不会因文本相同而共用槽。输出 IR 只含顶层代码标签、记录分配、字段读写与显式调用;闭包值表示为 Closure(code, env),环境字段保存被捕获的值。若那个值是引用,它保存的是位置,不是该位置里的当前整数。
目标观察是返回的整数或元组、可变单元的读写结果,以及正常终止或发散。增加的环境分配不是源程序可见事件;有限堆上的分配失败与空间代价另列,不能仅凭结果保持就断言资源保持。此处不承诺异常栈、调试变量或函数地址的外观相同。
一套可以执行的转换
编译器维护映射 A,把每个解析后的变量身份映到目标访问表达式。局部变量对应局部临时量;捕获变量对应 env.field。入口 A 将所有外部已解析绑定映到明确的目标参数或全局访问,闭合程序的顶层 A 为空。局部 let x=e1 in e2 先用旧 A 转换 e1 并求值到新鲜临时量 t,再用 A[x↦t] 转换 e2,离开作用域后恢复旧 A。既有自由变量规则沿用,在新构造上增加:非递归 let x=e1 in e2 的捕获集合是 FV(e1) ∪ (FV(e2)−{x});引用创建、读取不引入绑定;写入及二元组取其子表达式集合的并。
转换一个函数 fun y -> e 时,按固定的绑定身份顺序排列 FV(e)−{y},得到 x₁,…,xₖ。建立顶层代码 L(env,y),将这些变量的访问映到 env[1],…,env[k],将形参 y 的身份映到目标形参 y,再递归转换函数体;其他外部普通绑定只有进入捕获字段后才能在此代码体访问。原函数表达式替换为先分配环境 E = Env(A(x₁),…,A(xₖ)),再构造 Closure(L,E)。代码体此后只引用参数、局部量、显式环境和全局代码标签,没有未解释的词法自由变量。
转换应用 e1 e2 时,必须先把转换后的 e1 求值到新临时量 c,再把 e2 求值到新临时量 v,最后执行 call c.code(c.env,v)。不能直接重复展开 e1 来分别取 code 和 env;若 e1 创建引用或修改计数器,重复求值已经改变行为。其余构造逐项保持从左到右顺序。
这是一个语法变换算法,不是运行时每次重新寻找自由变量。若共有 n 个语法节点、K 个实际输出的捕获字段,用哈希集合合并逐节点自由变量集合的直接实现可能需要二次工作;不应笼统写成 O(n)。已给定捕获集合与身份索引后,发出目标代码及字段访问的工作为 O(n+K)。运行时创建捕获 k 项的闭包需 O(k+1) 时间与空间,调用增加常数次字段访问;被捕获对象的整个可达子图不会被复制。
直觉
函数被返回以后,它使用的局部名字去了哪里?闭包给出语义答案:代码带着定义时的环境。闭包转换则给出编译步骤:把环境变成普通记录,把自由变量读取变成字段读取,把函数调用变成“代码地址加环境参数”的调用。
例子与边界
手算:两份环境,一处状态
make(seed) =
let x = ref seed in
let inc = fun d -> (x := !x + d; !x) in
let peek = fun u -> !x in
(inc, peek)
let p = make(4) in
let a = fst p in
let b = snd p in
let x = 100 in
let first = a(3) in
(first, b(0), x)
inc 的形参是 d,自由变量只有 make 内的 x₁;peek 的形参 u 未被使用,自由变量也是 x₁。最后的 x₂=100 不在任何捕获集合里。为使布局直观,本例为两个函数分别建立一字段环境;它们可以共享环境记录,但没有必要这样优化。
inc_code(E, d):
C = E.cell
old = load C.value
store C.value = old + d
return load C.value
peek_code(E, u):
return load E.cell.value
make_code(E_unused, seed):
C = alloc Cell(seed)
Ei = alloc Env(C)
A = alloc Closure(inc_code, Ei)
Ep = alloc Env(C)
B = alloc Closure(peek_code, Ep)
return alloc Pair(A, B)
make(4) 返回时,C.value=4,Ei.cell=Ep.cell=C。调用 A.code(A.env,3) 把同一个 C.value 改为 7;再调用 B.code(B.env,0) 沿 B→Ep→C 读到 7。外层调用点的 x₂ 仍为 100,所以结果是 (7,7,100)。make 的调用帧已经退出,但这些堆记录的生命期没有随它结束。
一个真正的错误变体是把 Env(C) 改成 Env(load C.value),再让每个函数更新各自环境中的数值。此时 inc(3) 返回 7,peek(0) 仍返回 4,结果变为 (7,4,100)。这不是无害的布局优化,而是把一处共享状态拆成了两处独立状态。
递归初始化是额外一步
若扩展到 let rec f = fun y -> e in body,函数体里对 f 的调用需要找到同一闭包。一个明确的实现是:先创建包含 self=null 的环境,再创建闭包 F=Closure(L,E),最后写入 E.self=F,函数体把 f 读作 E.self。环境与闭包形成一个允许自引用的循环。
分配期间若可能收集,尚未完成的 E、F 必须保存在已登记的根槽;未初始化指针字段必须先是合法空值。最后写入 self 的短序列不得在无有效根的状态触发收集。此规则只覆盖“递归右侧是函数”的受限 let rec;任意递归值需要另定初始化或拒绝规则。本页主例不依赖递归扩展。
推论与应用
为什么转换保持结果
建立源位置到目标 Cell 的一一对应 h。源整数与同值目标整数对应;源引用 ℓ 与 h(ℓ) 对应;源闭包与一个目标闭包对应,当且仅当目标代码是该函数的转换,并且每个捕获字段对应源定义环境中的同一绑定值。特别地,若两个源引用相等,它们都映到同一个 h(ℓ)。
对求值推导归纳:常量与局部读取直接保持对应;创建引用扩展 h;读写通过同一个 h(ℓ) 保持存储关系;函数创建按捕获集合建立闭包关系;调用先对应地计算函数与实参,再用环境字段和形参进入对应函数体。因此每次写后,所有指向同一单元的别名仍观察到相同值。元组按字段逐项对应即可。
这是对本页终止求值的证明结构。若要同时得到发散保持,须把构造过程表为逐步模拟,证明有限管理步骤不会自行引入无限循环;若要得到空间界,还须分析环境保留了哪些对象。共享一个含无用大对象的外层环境,可能延长大对象的可达生命期。文献中的 safe-for-space 定理有自己的语言、成本语义和转换,不能直接当作本页任何环境优化的保证。[2]
迁移练习
把 make 改成返回 inc、peek、reset 三个闭包,reset(v) 写 x:=v。执行 inc(3); reset(10); peek(0) 应得到 10;三个环境必须指向同一个单元。若另调用一次 make(4),第二次分配得到新单元,两个计数器组不能互相影响。验收时分别写出“组内别名相同”和“组间位置不同”,这比只比较一次输出更能检查转换。
为了检验运行时收集,可以在 inc 写入后插入一个源语言不可见的 gc_poll。本页这段 IR 尚未插点;根映射页给出插点后的完整片段,明确丢弃旧 C 并在返回前从已更新环境重新取 C,不能把本页未重载的片段直接接到移动收集器上。
接下来,调用约定与栈帧决定显式环境参数放在哪里;逃逸分析解释哪些对象不能随帧释放;复制收集则检验搬家以后这些别名是否仍相同。
Lambda 提升对已知直接调用的局部函数增加普通参数,可以不为这些调用创建环境记录;传递需求还须沿调用图闭合。返回函数值不能只返回提升后的代码地址,因为未来调用者未必持有捕获值,所以本页的返回闭包与共享单元仍由显式环境表示承担。前端可先用声明身份解析固定捕获对象,再经ANF把有副作用的求值次序显式化。
参考资料
[1] Andrew Myers,Cornell CS 4120/5120,Compiling first-class functions,2021,§2 “Closures and closure conversion”、§§3–4 “Escaping variables / Static link chains”。用于核对闭包实现接口;本页程序与转换轨迹为独立教学例。
[2] Zoe Paraskevopoulou 与 Andrew W. Appel,Closure Conversion Is Safe for Space,ICFP 2019,作者机构摘要限定的 flat-environment CPS 转换。该文的资源定理未在本页重证,也未外推到任意环境共享方式。