# 5.5.3编译组合

> 译者： [https://sicp.comp.nus.edu.sg/chapters/117](https://sicp.comp.nus.edu.sg/chapters/117)

编译过程的实质是功能应用程序的编译。 使用给定目标和链接编译的组合的代码形式为`![%5Clangle%20%5Ctextit%7Bcompilation%20of%20operator%2C%20target%7D](img/62da6ada4d2e989c4ef54b860bea9009.jpg) proc, ![%5Ctextit%7Blinkage%7D](img/ffd62ac7a75206143172e825f4a5ca94.jpg) next![%5Crangle](img/4e5a6afd1302745fb047196f1d72ec8e.jpg) ![%5Clangle%20%5Ctextit%7Bevaluate%20operands%20and%20construct%20argument%20list%20in%7D](img/a5e3bbb04e941e23a32eea371499571c.jpg) argl![%5Crangle](img/4e5a6afd1302745fb047196f1d72ec8e.jpg) ![%5Clangle%20%5Ctextit%7Bcompilation%20of%20procedure%20call%20with%20given%20target%20and%20linkage%7D%5Crangle](img/cb063b64af031d27d594576e6ee040c0.jpg)`

在评估运算符和操作数期间，可能必须保存和恢复寄存器`env`，`proc`和`argl`。 请注意，这是编译器中唯一指定了`val`以外的目标的地方。

所需代码由`compile_application`生成。 这将递归编译运算符，以生成将要应用的功能放入`proc`的代码，并编译操作数，以生成评估应用程序各个操作数的代码。 将操作数的指令序列（由`construct_arglist`）与构成`argl`中参数列表的代码组合，然后将得到的参数列表代码与功能代码和执行函数调用的代码组合（产生 通过`compile_procedure_call`）。 在追加代码序列时，必须在`env`寄存器的周围保留运算符的值（因为评估运算符可能会修改`env`，这将需要评估操作数），并且`proc`寄存器必须保留 围绕参数列表的构造（因为评估操作数可能会修改`proc`，这对于实际的函数应用程序将是必需的）。 `Continue`也必须始终保留，因为函数调用中的链接需要它。

```js
function compile_application(exp, target, linkage) {
    const proc_code = compile(operator(exp), "proc", "next"); 
    const operand_codes = map(operand => compile(operand, "val", "next"), operands(exp));

    return preserving(list("env", "continue"), 
                      preserving(list("proc", "continue"), 
                                 construct_arglist(operand_codes),
                                 compile_procedure_call(target, linkage)));
}
```

构造参数列表的代码将把每个操作数求值到`val`中，然后将`pair`的值赋值到`argl`中累积的参数列表中。 由于我们将`pair`的参数依次排列在`argl`上，因此我们必须从最后一个参数开始，以第一个参数结束，以便这些参数在结果列表中从头到尾的顺序显示。 与其通过将`argl`初始化为用于此评估序列的空白列表来浪费指令，我们使第一个代码序列构造了初始的`argl`。 因此，参数列表构造的一般形式如下：`![%5Clangle%20%5Ctextit%7Bcompilation%20of%20last%20operand%2C%20targeted%20to%7D](img/d19b585db50dde555251bbcf65a66d1a.jpg) val![%5Crangle](img/4e5a6afd1302745fb047196f1d72ec8e.jpg) assign("argl", list(op("list"), reg("val"))); ![%5Clangle%20%5Ctextit%7Bcompilation%20of%20next%20operand%2C%20targeted%20to%7D](img/8521def6df0c4be256175801478dd8af.jpg) val![%5Crangle](img/4e5a6afd1302745fb047196f1d72ec8e.jpg) assign("argl", list(op("pair"), reg("val"), reg("argl"))); ![%5Cldots](img/9f57fa7487fb5cc87b777f74773ffb74.jpg) ![%5Clangle%20%5Ctextit%7Bcompilation%20of%20first%20operand%2C%20targeted%20to%7D](img/533cf512d0191a68e9df86e59d62c1f4.jpg) val![%5Crangle](img/4e5a6afd1302745fb047196f1d72ec8e.jpg) assign("argl", list(op("pair"), reg("val"), reg("argl")));`

除第一个操作数之外，`Argl`必须保留在每个操作数评估周围（这样就不会丢失到目前为止累积的参数），除最后一个（每个操作数评估）必须保留`env`（供后续操作数评估使用）。

编译此自变量代码有些棘手，因为对要评估的第一个操作数进行了特殊处理，并且需要将`argl`和`env`保存在不同的位置。 `construct_arglist`函数将评估各个操作数的代码作为参数。 如果根本没有操作数，它只会发出指令

```js
assign(argl, list(constant("null")));
```

否则，`construct_arglist`将创建使用最后一个参数初始化`argl`的代码，并附加对其余参数进行求值的代码，并将它们依次附加到`argl`。 为了从最后到第一处理参数，我们必须按照`compile_application`提供的顺序反转操作数代码序列的列表。

```js
function construct_arglist(operand_codes) {
    const operand_codes = reverse(operand_codes);

    if (is_null(operand_codes)) {
        return make_instruction_sequence(
            null, 
            list("argl"),
            list(assign("argl", list(constant("null")))));
    } else {
        const code_to_get_last_arg =
              append_instruction_sequences(
                  head(operand_codes),
                  make_instruction_sequence(
                      list("val"),
                      list("argl"),
                      list(assign("argl", list(op("list"), reg("val"))))));

        return is_null(tail(operand_codes))
            ? code_to_get_last_arg
            : preserving(
                list("env"),
                code_to_get_last_arg,
                code_to_get_rest_args(tail(operand_codes)));
    }
}

function code_to_get_rest_args(operand_codes) {
    const code_for_next_arg = preserving(
        list("argl"), 
        head(operand_codes),
        make_instruction_sequence(
            list("val", "argl"), 
            list("argl"),
            list(assign("argl", list(op("cons"), reg("val"), reg("argl"))))));

    return is_null(tail(operand_codes))
        ? code_for_next_arg
        : preserving(list("env"),
                     code_for_next_arg,
                     code_to_get_rest_args(tail(operand_codes)));
}
```

## 应用功能

在评估组合的元素之后，编译后的代码必须将`proc`中的函数应用于`argl`中的参数。 该代码与 [4.1.1](74)[5.4.1](110) 部分。 它检查要应用的功能是原始功能还是编译功能。 对于原始函数，它使用`apply_primitive_function`； 我们很快将看到它如何处理编译函数。 功能应用程序代码具有以下形式：`test(op("primitive-procedure"), reg("proc")), branch(label("primitive_branch")), "compiled-branch", ![%5Clangle%20%5Ctextit%7Bcode%20to%20apply%20compiled%20procedure%20with%20given%20target%20and%20appropriate%20linkage%7D%20%5Crangle](img/1c896e83b1af450bb756a7955d918686.jpg) "primitive-branch", assign(![%5Clangle%5Ctextit%7Btarget%7D%5Crangle](img/4d7a465bc2a089151a70413887ebd43f.jpg), list(op("apply_primitive_procedure"), reg("proc"), reg("argl"))) ![%5Clangle%5Ctextit%7Blinkage%7D%5Crangle](img/30b6e84425fafd6e6390e890a9f8b063.jpg) "after_call",`

请注意，已编译的分支必须跳过原始分支。 因此，如果原始函数调用的链接为`next`，则复合分支必须使用跳转到原始分支之后插入的标签的链接。 （这类似于`compile_if`中用于true分支的链接。）

```js
function compile_procedure_call(target, linkage) {
    const primitive_branch = make_label("primitive-branch");
    const compiled_branch = make_label("compiled-branch");
    const after_call = make_label("after-call");

    const compiled_linkage = linkage === "next" ? after_call : linkage;

    return append_instruction_sequences(
        make_instruction_sequence(
            list("proc"),
            list(),
            list(test(op("primitive_procedure"), reg("proc"), branch(label(primitive_branch))))), 
        parallel_instruction_sequences(
            append_instruction_sequences(
                compiled_branch,
                compile_proc_appl(target, compiled_linkage)),
            append_instruction_sequences(
                primitive_branch,
                end_with_linkage(
                    linkage,
                    make_instruction_sequence(list("proc", "argl"),
                                              list(target),
                                              assign(target, list(op("apply_primitive_procedure"), reg("proc"), reg("argl"))))))),
        "after_call");
}
```

基本和复合分支（如`compile_if`中的正确和错误分支）是使用`parallel_instruction_sequences`而不是普通的`append_instruction_sequences`附加的，因为它们不会顺序执行。

## 应用编译函数

处理函数应用程序的代码是编译器中最微妙的部分，即使它生成的指令序列很短。 编译后的函数（由`compile_function_definition_expression`构造）具有入口点，该入口点是一个标签，用于指定该函数的代码在何处开始。 此入口点处的代码在`val`中计算结果，并通过执行指令`go_to(reg("continue"))`返回。 因此，我们可能期望具有给定目标和链接的编译功能应用程序（由`compile_proc_appl`生成）的代码在链接为标签`assign("continue", list(label("proc_return")),) assign("val", list(op("compiled_procedure_entry"), reg("proc"))), go_to(reg("val")), "proc_return", assign(![%5Clangle%20target%5Crangle](img/173d44c95e38e26fe6a33fb373cf84da.jpg), list(reg("val"))), // included if target is not val go_to(label(![%5Clangle%20linkage%5Crangle](img/52327f2016f818cf0249291ad0f4b2a5.jpg))), // linkage code`的情况下看起来像这样，或者在链接为`return`的情况下看起来像这样 ]。 `save("continue"), assign("continue", list(label("proc_return"))), assign("val", list(op("compiled_procedure_entry"), reg("proc"))), go_to(reg("val")), "proc_return", assign(![%5Clangle%20target%5Crangle](img/173d44c95e38e26fe6a33fb373cf84da.jpg), list(reg("val"))), // included if target is not val restore("continue"), go_to(reg("continue")), // linkage code`

该代码设置了`continue`，以便该功能返回到标签`proc_return`，并跳转到该功能的入口点。 `proc_return`处的代码将函数的结果从`val`传输到目标寄存器（如有必要），然后跳转到链接指定的位置。 （链接始终是`return`或标签，因为`compile_procedure_call`用`after_call`标签替换了复合过程分支的`next`链接。）

实际上，如果目标不是`val`，那正是我们的编译器将生成的代码。 [[1]](117#footnote-1) 但是，通常目标是`val`（编译器唯一指定不同寄存器的时间是将运算符的评估对象定为`proc`时），因此将函数结果直接放入 到目标寄存器中，无需返回到复制它的特殊位置。 相反，我们通过设置`continue`简化了代码，以便该函数将&lt;quote&gt;直接将&lt;/quote&gt;返回到调用者的链接所指定的位置：`![%5Clangle%20%5Ctextit%7Bset~up%7D%5Crangle](img/413847d6b44920401ae7c5df8bae8b23.jpg) continue ![%5Clangle%5Ctextit%7Bfor~linkage%7D%5Crangle](img/2f9034a2bd6c8613cc9cdedffbc98528.jpg) assign("val", list(op("compiled_procedure_entry"), reg("proc"))), go_to(reg("val")),`

如果链接是标签，则设置`continue`，以便功能返回该标签。 （也就是说，功能终止的`go_to(reg("continue"))`等同于上述`proc_return`的`go_to(label(` ![%5Clangle%20linkage%5Crangle](img/52327f2016f818cf0249291ad0f4b2a5.jpg) `))`。）`assign("continue", list(label(![%5Clangle%20linkage%5Crangle](img/52327f2016f818cf0249291ad0f4b2a5.jpg)))), assign("val", list(op("compiled_procedure_entry"), reg("proc"))), go_to(reg("val")),`

如果链接为`return`，那么我们根本不需要设置`continue`：它已经拥有所需的位置。 （也就是说，函数`go_to(reg("continue"))`的结尾直接移到`proc_return`处的`go_to(reg("continue"))`消失的位置。）

```js
assign("val", list(op("compiled_procedure_entry"), reg("proc"))),
      go_to(reg("val")),
```

通过`return`链接的这种实现，编译器生成尾递归代码。 调用函数作为函数主体中的最后一步将直接进行传输，而无需在堆栈上保存任何信息。

假设我们已经处理了带有`return`和目标`val`的链接的函数调用的情况，如上针对非`val`目标所示。 这将破坏尾递归。 对于任何表达式，我们的系统仍然会提供相同的值。 但是每次调用函数时，我们都会保存`continue`并在调用后返回以撤消（无用）保存。 这些额外的节省将在嵌套函数调用期间累积。 [[2]](117#footnote-2)

`Compile_proc_appl`通过考虑四种情况来生成上述功能应用代码，具体取决于调用的目标是否为`val`和链接是否为`return`。 观察到声明了指令序列以修改所有寄存器，因为执行功能体可以任意方式更改寄存器。 [[3]](117#footnote-3) 另请注意，具有目标`val`和链接`return`的情况的代码序列被声明为需要`continue`：即使在两个指令中未明确使用`continue` 序列，我们必须确保在进入编译函数时`continue`的值正确。

```js
function compile_proc_appl(target, linkage) {
    if (target === "val"  linkage !== "return") {
        return make_instruction_sequence(
            list("proc"),
            all_regs,
            list(
                assign("continue", list(label(linkage))),
                assign("val", list(op("compiled_procedure_entry"), reg("proc"))),
                go_to(reg("val"))));
    } else if (target !== "val"  linkage !== "return") {
        const proc_return = make_label("proc_return");

        return make_instruction_sequence(
            list("proc"),
            all_regs,
            list(
                assign("continue", list(label(proc_return))),
                assign("val", list(op("compiled_procedure_entry"), reg("proc"))),
                go_to(reg("val")),
                proc_return,
                assign(target, list(reg("val"))),
                go_to(label(linkage))));
    } else if (target === "val"  linkage === "return") {
        return make_instruction_sequence(
            list("proc", "continue"),
            all_regs,
            list(
                assign("val", list(op("compiled_procedure_entry"), reg("proc"))),
                go_to(reg("val"))));
    } else if (target !== "val"  linkage === "return") {
        error(target, "return linkage, target not val - - COMPILE");
    }
}
```

* * *

[[1]](117#footnote-link-1) Actually, we signal an error when the target is not `val` and the linkage is `return`, since the only place we request `return` linkages is in compiling functions, and our convention is that functions return their values in `val`.

[[2]](117#footnote-link-2) Making a compiler generate tail-recursive code might seem like a straightforward idea. But most compilers for common languages, including C and Pascal, do not do this, and therefore these languages cannot represent iterative processes in terms of function call alone. The difficulty with tail recursion in these languages is that their implementations use the stack to store function arguments and local variables as well as return addresses. The Scheme implementations described in this book store arguments and variables in memory to be garbage-collected. The reason for using the stack for variables and arguments is that it avoids the need for garbage collection in languages that would not otherwise require it, and is generally believed to be more efficient. Sophisticated JavaScript compilers can, in fact, use the stack for arguments without destroying tail recursion. (See <citation>Hanson 1990</citation> for a description.) There is also some debate about whether stack allocation is actually more efficient than garbage collection in the first place, but the details seem to hinge on fine points of computer architecture. (See <citation>Appel 1987</citation> and Miller and Rozas 1994 for opposing views on this issue.)

[[3]](117#footnote-link-3) The variable `all_regs` is bound to the list of names of all the registers:

```js
const all_regs = list("env", "proc", "val", "argl", "continue");
```

