在编译器中实现闭包



我正试图设计一个基本的编译器来伪汇编代码。然而,我不知道如何实现闭包。似乎我需要将特定的寄存器值与每个"子例程"关联起来。我曾考虑过使用堆叠,但再一次似乎还不够。似乎只有关联数组才能工作,但这或类似的事情怎么能在汇编中完成呢?

我选择的示例如下,为了简洁起见,以CoffeeScript的形式进行了交流。

((x) -> (y) -> x(y))((x) -> x)(2)

以下是我一直在尝试的总体结构。这是我正在编译的伪程序集的一个示例。

'((label lam1)   ;; (x) -> x
  (cp resp arg)
  (ret)
  (label lam2)   ;; (y) -> x(y)
  (jmp x)
  (label lam3)   ;; (x) -> [(y) -> ...]
  (cp x arg)     ;; this is the assignment intended for a closure
  (cp resp lam2) ;; this is a returned lambda, needing the closure
  (ret)
  (label main)
  (cp arg lam1)
  (call lam3)
  (set arg 2)
  (call resp))) 

这是有效的;然而,该值只是在名称x下设置,然后返回lambda,在执行lambda之前,x值可能很容易被污染。

《计算机程序的结构和解释》中对实现的描述如下,这对我来说在汇编中似乎是不可行的。我不知道他们还能用什么战术。

过程对象将在运行时通过将当前环境(定义点的环境)与编译过程的入口点(新生成的标签)相结合来构建。

总之,寄存器值如何与"子程序"关联?堆叠是否足够

堆栈不可能足够。。。考虑一个更简单的情况,他们进行

function bar(f) {
    alert(f());
}
function foo(x) {
    bar(function(){ return x; });
}
foo(42);

在上述情况下,闭包中的x在理论上可能存在于foo的堆栈帧中,因为闭包不会比其创建者foo更长寿。然而,有一个小小的变化:

function bar(f) {
    to_call_later.push(f);
}

则闭包将被存储起来,并且当CCD_ 6已经终止并且其激活记录的堆栈空间已经被回收时。显然,x不能在该堆栈区域中,因为它必须生存。

因此存在两个问题:

  1. 闭包必须有一些存储(环境)。当您认为两次调用foo传递两个不同的值应该为x创建两个独立的存储时,这一点就很明显了。如果闭包只是代码,那么这是不可能的,除非每次调用foo时都会生成不同的代码。

  2. 这个存储必须至少与闭包本身一样存在,而不仅仅是与创建闭包的人一样。

还要注意,如果您想对变量进行读/写关闭,则需要额外的间接级别,例如:

function bar(f) {
    alert(f());
}
function foo(x) {
    var c1 = function() { return ++x; };
    var c2 = function() { return x *= 2; };
    bar(c1);
    bar(c2);
}
foo(42);  // displays 42+1=43 and 43*2=86 (not 42*2=84!)

换句话说,您可以有几个不同的闭包共享相同的环境。

因此x不可能在foo激活记录的堆栈中,也不可能在闭包对象本身中。闭包对象必须有一个指向x所在位置的指针

在x86上实现这一点的一个可能的解决方案是:

  • 使用垃圾收集或引用计数的内存管理系统。到目前为止,堆垛还不足以处理封盖。

  • 每个闭包都是一个包含两个字段的对象:一个指向代码的指针和一个指向封闭变量("环境")的指针数组。

  • 当执行代码时,您有一个堆栈esp,例如esi指向闭包对象本身(因此(esi)是代码的地址,(esi+4)是第一个闭包变量的地址,而(esi+8)是第二个闭包变量,依此类推)。

  • 每个变量都是一个独立的堆分配对象,只要有闭包指向它,它就可以生存

这当然是一种非常粗糙的方法。例如,SBCL要智能得多,并且未捕获的变量仅在堆栈和/或寄存器上分配。这需要分析闭包是如何使用的。

编辑

假设你只考虑一个纯粹的函数设置(换句话说,函数/闭包的返回值只取决于传递的参数,并且闭包状态不能发生变化),那么事情就可以简化一点。

您可以做的是使闭包对象包含捕获的,而不是捕获的变量。同时使闭包本身成为可复制对象,理论上只能使用堆栈(除了闭包的大小可能因需要捕获的状态而异),因此,在这种情况下,至少对我来说,想象一个合理的仅基于堆栈的协议来传递参数和返回值并不容易。

通过使闭包成为固定大小的对象来消除可变大小的问题,您可以看到这个C程序如何仅使用堆栈实现闭包(注意没有malloc调用)

#include <stdio.h>
typedef struct TClosure {
    int (*code)(struct TClosure *env, int);
    int state;
} Closure;
int call(Closure *c, int x) {
    return c->code(c, x);
}
int adder_code(Closure *env, int x) {
    return env->state + x;
}
int multiplier_code(Closure *env, int x) {
    return env->state * x;
}
Closure make_closure(int op, int k) {
    Closure c;
    c.state = k;
    c.code = (op == '+' ? adder_code : multiplier_code);
    return c;
}
int main(int argc, const char *argv[]) {
    Closure c1 = make_closure('+', 10);
    Closure c2 = make_closure('*', 3);
    printf("c1(3) = %i, c2(3) = %in",
           call(&c1, 3), call(&c2, 3));
    return 0;
}

Closure结构可以在堆栈上传递、返回和存储,因为环境是只读的,所以您不存在生存期问题,因为可以在不影响语义的情况下复制不可变数据。

C编译器可以使用这样的方法来创建只能按值捕获变量的闭包,这确实是C++11 lambda所提供的(您也可以通过引用捕获,但这取决于程序员来确保捕获的变量的生存期足够长)。

最新更新