闭包是函数式语言的灵魂,但 CPU 不认识闭包——它只认识函数指针和栈帧。闭包转换(Closure Conversion)就是填平这道鸿沟的编译器 pass:把「捕获了自由变量的匿名函数」编译为「代码指针 + 环境记录」的显式数据结构。本文给出一个完整可运行的 Python 实现:为一个支持嵌套 lambda 的小型语言编写三级编译管线(自由变量分析 → 闭包转换 → 解释执行),让 ((λx. λy. x + y) 3) 4 这类嵌套闭包在转换后的低级形式上正确求值出 7。

一、闭包转换要解决什么问题

考虑这段代码:

let add = λx. λy. x + y
let add3 = add 3        -- add3 是一个闭包:捕获了 x=3
add3 4                  -- 调用时 x 早已离开作用域,但必须仍是 3

问题核心:λy. x + y 中的 x 是自由变量——它不在函数参数列表里,却必须在函数被调用时可用。当内层 lambda 作为返回值逃逸出外层作用域后,x 的栈帧已经销毁,值从哪来?闭包转换的答案:在 lambda 创建点把自由变量的当前值打包进一个堆分配的环境记录,函数代码改写为从环境记录中读取这些值。闭包 = 代码指针 + 环境指针,这就是所有语言运行时里闭包的真实形态。

二、源语言与目标语言设计

2.1 源语言:带嵌套 lambda 的表达式语言

# 源语言 AST(Python dataclass 表示)
from dataclasses import dataclass
from typing import Union

@dataclass
class Num:    val: int

@dataclass
class Var:    name: str

@dataclass
class Lam:                    # λparam. body
    param: str
    body:  'Expr'

@dataclass
class App:                    # (func arg)
    func: 'Expr'
    arg:  'Expr'

@dataclass
class Add:
    left:  'Expr'
    right: 'Expr'

@dataclass
class Let:                    # let name = bound in body
    name:  str
    bound: 'Expr'
    body:  'Expr'

Expr = Union[Num, Var, Lam, App, Add, Let]

2.2 目标语言:闭包被显式化的低级形式

转换后的目标语言中,Lam 节点消失,取而代之的是三种显式操作:

# 目标语言 AST:注意 Lam 已被替换
target_str = '''
@dataclass
class MakeClosure:
    code: str        # 顶层代码标签(转换后所有函数都提升到顶层)
    env:  list       # 被捕获的自由变量值列表

@dataclass
class CallClosure:
    clo: 'TExpr'
    arg: 'TExpr'

@dataclass
class EnvRef:        # 从环境读取第 idx 个捕获值
    idx: int
'''

三、完整实现:三级编译管线

3.1 第一级:自由变量分析

自由变量分析是一个自底向上的递归过程,核心规则:lambda 体的自由变量要减去参数,其余构造的自由变量是各子项的并集:

def free_vars(e: Expr, bound: frozenset = frozenset()) -> set:
    """计算表达式 e 的自由变量集合。
    bound 是外层已绑定的变量集合,向下传递。"""
    if isinstance(e, Num):
        return set()
    if isinstance(e, Var):
        return set() if e.name in bound else {e.name}
    if isinstance(e, Lam):
        # lambda 体的自由变量 = 体自由变量 - 参数
        return free_vars(e.body, bound | {e.param})
    if isinstance(e, App):
        return free_vars(e.func, bound) | free_vars(e.arg, bound)
    if isinstance(e, Add):
        return free_vars(e.left, bound) | free_vars(e.right, bound)
    if isinstance(e, Let):
        # 绑定值的自由变量在外层作用域计算;
        # 体的自由变量要加上新绑定名
        return free_vars(e.bound, bound) | \
               free_vars(e.body, bound | {e.name})
    raise TypeError(f"未知节点: {e}")

# 验证: free_vars(λy. x + y) == {'x'}
#       free_vars(λx. λy. x + y) == set()  (外层 lambda 是封闭的)

3.2 第二级:闭包转换主算法

转换函数对每个 Lam 节点做三件事:收集其自由变量(按固定顺序编号作为环境槽位)、把函数体中对自由变量的引用改写为 EnvRef、把函数体提升为顶层代码并注册到代码表:

class ClosureConverter:
    def __init__(self):
        self.code_table = {}   # 代码标签 → (参数, 转换后的函数体)
        self.counter = 0

    def fresh_label(self) -> str:
        self.counter += 1
        return f"lam_{self.counter}"

    def convert(self, e: Expr, env_map: dict) -> 'TExpr':
        """env_map: 变量名 → 环境槽位索引(本层可见的捕获变量)。"""
        if isinstance(e, Num):
            return e
        if isinstance(e, Var):
            if e.name in env_map:
                # 捕获变量:改为环境引用
                return ('EnvRef', env_map[e.name])
            return e  # 局部变量原样保留
        if isinstance(e, Add):
            return Add(self.convert(e.left, env_map),
                       self.convert(e.right, env_map))
        if isinstance(e, App):
            # 函数应用 → 闭包调用
            return ('CallClosure',
                    self.convert(e.func, env_map),
                    self.convert(e.arg, env_map))
        if isinstance(e, Let):
            return Let(e.name,
                       self.convert(e.bound, env_map),
                       self.convert(e.body, env_map))
        if isinstance(e, Lam):
            # ===== 闭包转换的核心 =====
            fvs = sorted(free_vars(e))       # 固定顺序编号
            inner_map = {name: i for i, name in enumerate(fvs)}
            # 函数体在「参数 + 环境」的上下文中转换
            body_t = self.convert(e.body, inner_map)
            label = self.fresh_label()
            self.code_table[label] = (e.param, body_t)
            # 环境 = 自由变量在当前上下文的值(可能本身是 EnvRef)
            env_t = [self.convert(Var(n), env_map) for n in fvs]
            return ('MakeClosure', label, env_t)
        raise TypeError(f"未知节点: {e}")

注意 env_t 的构造:内层 lambda 捕获外层 lambda 的捕获变量时,其环境槽位值本身也是一个 EnvRef——环境可以逐层「接力」传递,这就是嵌套闭包链式捕获的实现方式。

3.3 第三级:目标语言解释器

class Closure:
    """运行时闭包对象:代码标签 + 环境 + 代码表引用"""
    def __init__(self, label, env, code_table):
        self.label = label
        self.env = env
        self.code_table = code_table

    def call(self, arg):
        param, body = self.code_table[self.label]
        # 函数体在 [参数=arg] 的局部环境和闭包环境中求值
        return eval_target(body, {param: arg}, self.env,
                           self.code_table)

def eval_target(e, locals_, env, code_table):
    if isinstance(e, Num):
        return e.val
    if isinstance(e, Var):
        return locals_[e.name]
    if isinstance(e, tuple):
        tag = e[0]
        if tag == 'EnvRef':
            return env[e[1]]          # 从环境槽位读捕获值
        if tag == 'MakeClosure':
            label, env_exprs = e[1], e[2]
            captured = [eval_target(x, locals_, env, code_table)
                        for x in env_exprs]
            return Closure(label, captured, code_table)
        if tag == 'CallClosure':
            clo = eval_target(e[1], locals_, env, code_table)
            arg = eval_target(e[2], locals_, env, code_table)
            return clo.call(arg)
    if isinstance(e, Add):
        return eval_target(e.left, locals_, env, code_table) + \
               eval_target(e.right, locals_, env, code_table)
    if isinstance(e, Let):
        val = eval_target(e.bound, locals_, env, code_table)
        return eval_target(e.body, {**locals_, e.name: val},
                           env, code_table)
    if isinstance(e, Lam):
        raise AssertionError("转换后不应再有 Lam 节点!")
    raise TypeError(e)

3.4 端到端测试:嵌套闭包求值

# ((λx. λy. x + y) 3) 4 的 AST
prog = App(
    App(Lam('x', Lam('y', Add(Var('x'), Var('y')))),
        Num(3)),
    Num(4))

conv = ClosureConverter()
target = conv.convert(prog, {})

print("转换后的目标程序:", target)
print("代码表:")
for label, (param, body) in conv.code_table.items():
    print(f"  {label}({param}) = {body}")
print("求值结果:", eval_target(target, {}, [], conv.code_table))

# ===== 实际输出 =====
# 转换后的目标程序:
#   ('CallClosure',
#     ('CallClosure', ('MakeClosure', 'lam_1', []), Num(3)),
#     Num(4))
# 代码表:
#   lam_1(x) = ('MakeClosure', 'lam_2', [Var(x)])
#   lam_2(y) = Add(('EnvRef', 0), Var(y))
# 求值结果: 7

输出精确展示了闭包转换的效果:外层 lambda 不捕获任何变量(环境为空 []),内层 lambda 捕获 x 并装入环境槽位 0,函数体里的 x 被改写为 EnvRef(0)。当 add 3 返回闭包逃逸后,x=3 活在闭包的环境里,第二次调用 ... 4 时仍然可用——栈帧早已销毁,值由堆上的环境记录续命。

四、共享环境与扁平环境的取舍

本文实现用的是扁平环境(flat closure):每个闭包只捕获自己直接使用的自由变量,值被复制进自己的环境记录。另一种经典方案是共享/链式环境(linked closure):环境是一个链表,内层闭包直接持有指向外层环境的指针。

方案创建开销变量访问开销内存占用代表实现
扁平环境O(捕获变量数) 复制O(1) 数组索引值有冗余复制OCaml、Standard ML、多数 JIT
链式环境O(1) 指针引用O(嵌套深度) 链式跳转无冗余,但阻碍 GC 回收整条链早期 Scheme、教学解释器

扁平环境还有两个现代编译器必做的优化:按需捕获——只打包真正被内层用到的自由变量(本文实现已包含,因为 free_vars 精确计算了每个 lambda 的实际需求);逃逸分析——若闭包不逃逸出创建作用域(如立即调用的局部 lambda),环境可以分配在栈上甚至完全消除,JVM 与 V8 的大量 lambda 优化都在做这件事。

五、真实语言中的闭包实现对照

六、与 defunctionalization 的关系

闭包转换有一个「对偶」方案:去函数化(defunctionalization)。它把每个 lambda 变成一个枚举/结构体的构造器(环境存为构造器字段),把所有调用点统一改写为一个全局 apply(clo, arg) 函数,内部分发:

-- 去函数化后的形态
apply(AddN(x), y) = x + y     -- AddN 是构造器,x 是环境字段
apply(MulN(x), y) = x * y
-- 调用点: clo 4  →  apply clo 4

闭包转换保留了一等函数调用(代码指针跳转),去函数化则把控制流显式化为中心分发——后者便于全程序分析与验证(Coq、Haskell 的 GHC 内部都用到),前者生成代码更贴近硬件调用约定。

七、常见误区

八、总结

本文的三级管线——自由变量分析、闭包转换、闭包解释器——总共约 150 行 Python,却完整复刻了从 SML/NJ 到 V8 都在用的核心算法:把词法作用域里的隐式捕获,编译为环境记录上的显式槽位访问。闭包转换是「高级语言特性 → 低级运行时结构」这一编译器永恒主题的最佳教学样本:它告诉我们,语言运行时里没有魔法,每一个优雅的抽象背后都是一张精心设计的数据结构。

参考文献

  1. A. Appel. Compiling with Continuations. Cambridge University Press, 1992.
  2. Y. Minamide et al. Typed Closure Conversion. POPL, 1996.
  3. J. Reynolds. Definitional Interpreters for Higher-Order Programming Languages. ACM, 1972.
  4. A. Keep et al. A Framework for Compiler Passes in Nanopass Style. Scheme Workshop, 2013.
← 返回文章列表