灯下 登录
计算机科学 / SICP / subsection 中英对照

5.5.3 Compiling Combinations

The essence of the compilation process is the compilation of procedure

applications. The code for a combination compiled with a given target and

linkage has the form

编译过程的核心在于对过程应用的编译。以给定目标和连接编译一个组合式所得的代码,具有如下形式

The registers env, proc, and argl may have to be saved and

restored during evaluation of the operator and operands. Note that this is the

only place in the compiler where a target other than val is specified.

在对运算符和运算对象求值的过程中,寄存器 env、proc 和 argl 可能需要被保存和恢复。注意,这是编译器中唯一指定 val 以外目标的地方。

The required code is generated by compile-application. This recursively

compiles the operator, to produce code that puts the procedure to be applied

into proc, and compiles the operands, to produce code that evaluates the

individual operands of the application. The instruction sequences for the

operands are combined (by construct-arglist) with code that constructs

the list of arguments in argl, and the resulting argument-list code is

combined with the procedure code and the code that performs the procedure call

(produced by compile-procedure-call). In appending the code sequences,

the env register must be preserved around the evaluation of the operator

(since evaluating the operator might modify env, which will be needed to

evaluate the operands), and the proc register must be preserved around

the construction of the argument list (since evaluating the operands might

modify proc, which will be needed for the actual procedure application).

Continue must also be preserved throughout, since it is needed for the

linkage in the procedure call.

所需代码由 compile-application 生成。它递归地编译运算符,产生将待应用过程置入 proc 的代码;并编译各个运算对象,产生对应用中各运算对象分别求值的代码。各运算对象的指令序列由 construct-arglist 组合,生成在 argl 中构造实参表的代码,所得实参表代码再与过程代码以及执行过程调用的代码(由 compile-procedure-call 生成)组合在一起。在追加代码序列时,求值运算符期间必须保护 env(因为对运算符求值可能修改 env,而 env 在对运算对象求值时还需用到),构造实参表期间必须保护 proc(因为对运算对象求值可能修改 proc,而 proc 在实际过程应用时还需用到)。在整个过程中还必须保护 continue,因为过程调用的连接需要它。

The code to construct the argument list will evaluate each operand into

val and then cons that value onto the argument list being

accumulated in argl. Since we cons the arguments onto

argl in sequence, we must start with the last argument and end with the

first, so that the arguments will appear in order from first to last in the

resulting list. Rather than waste an instruction by initializing argl

to the empty list to set up for this sequence of evaluations, we make the first

code sequence construct the initial argl. The general form of the

argument-list construction is thus as follows:

构造实参表的代码将依次对每个运算对象求值到 val,然后将该值 cons 到正在 argl 中积累的实参表上。由于我们是依次将实参 cons 到 argl 上,所以必须从最后一个实参开始,以第一个实参结束,从而使实参在最终的表中按从第一到最后的顺序排列。为避免用一条无谓的指令将 argl 初始化为空表来启动这一系列求值,我们让第一段代码序列负责构造 argl 的初始值。实参表的构造总体形式如下:

Argl must be preserved around each operand evaluation except the first

(so that arguments accumulated so far won’t be lost), and env must be

preserved around each operand evaluation except the last (for use by subsequent

operand evaluations).

除第一个运算对象外,每次对运算对象求值时都必须保护 argl(以免已积累的实参丢失);除最后一个运算对象外,每次对运算对象求值时都必须保护 env(供后续运算对象的求值使用)。

Compiling this argument code is a bit tricky, because of the special treatment

of the first operand to be evaluated and the need to preserve argl and

env in different places. The construct-arglist procedure takes

as arguments the code that evaluates the individual operands. If there are no

operands at all, it simply emits the instruction

编译这段实参代码稍有些棘手,原因在于需要对第一个待求值的运算对象做特殊处理,以及需要在不同位置保护 argl 和 env。construct-arglist 过程以各个运算对象的求值代码为实参。若没有任何运算对象,它直接生成指令

Otherwise, construct-arglist creates code that initializes argl

with the last argument, and appends code that evaluates the rest of the

arguments and adjoins them to argl in succession. In order to process

the arguments from last to first, we must reverse the list of operand code

sequences from the order supplied by compile-application.

否则,construct-arglist 创建以最后一个实参初始化 argl 的代码,并追加对其余实参求值并依次将其并入 argl 的代码。为了从最后一个到第一个依次处理实参,我们必须将 compile-application 所提供的运算对象代码序列的顺序颠倒。

Subheading: Applying procedures

小节标题:应用过程

After evaluating the elements of a combination, the compiled code must apply

the procedure in proc to the arguments in argl. The code

performs essentially the same dispatch as the apply procedure in the

metacircular evaluator of 4.1.1 or the apply-dispatch

entry point in the explicit-control evaluator of 5.4.1. It

checks whether the procedure to be applied is a primitive procedure or a

compiled procedure. For a primitive procedure, it uses

apply-primitive-procedure; we will see shortly how it handles compiled

procedures. The procedure-application code has the following form:

对组合式的各个元素求值完毕后,编译后的代码必须将 proc 中的过程应用于 argl 中的实参。该代码所执行的分派本质上与 4.1.1 的元循环求值器中的 apply 过程,或 5.4.1 的显式控制求值器中的 apply-dispatch 入口点相同。它检查待应用的过程是基本过程还是编译过程:对于基本过程,使用 apply-primitive-procedure;对编译过程的处理稍后会看到。过程应用代码具有以下形式:

Observe that the compiled branch must skip around the primitive branch.

Therefore, if the linkage for the original procedure call was next, the

compound branch must use a linkage that jumps to a label that is inserted after

the primitive branch. (This is similar to the linkage used for the true branch

in compile-if.)

注意,编译过程分支必须绕过基本过程分支。因此,若原过程调用的连接为 next,则复合过程分支必须使用一个跳转到基本过程分支之后所插入标号的连接。(这与 compile-if 中真分支所用的连接方式类似。)

The primitive and compound branches, like the true and false branches in

compile-if, are appended using parallel-instruction-sequences

rather than the ordinary append-instruction-sequences, because they will

not be executed sequentially.

Subheading: Applying compiled procedures

基本过程分支和复合过程分支与 compile-if 中的真分支和假分支一样,使用 parallel-instruction-sequences 而非普通的 append-instruction-sequences 来追加,因为它们不会被顺次执行。

小节标题:应用编译过程

The code that handles procedure application is the most subtle part of the

compiler, even though the instruction sequences it generates are very short. A

compiled procedure (as constructed by compile-lambda) has an entry

point, which is a label that designates where the code for the procedure

starts. The code at this entry point computes a result in val and

returns by executing the instruction (goto (reg continue)). Thus, we

might expect the code for a compiled-procedure application (to be generated by

compile-proc-appl) with a given target and linkage to look like this if

the linkage is a label

处理过程应用的代码是编译器中最微妙的部分,尽管它所生成的指令序列非常短。一个编译过程(由 compile-lambda 构造而成)有一个入口点,即指定过程代码起始位置的标号。该入口点处的代码在 val 中计算出结果,并通过执行指令 `(goto (reg continue))` 返回。因此,我们也许会预期,由 compile-proc-appl 生成的、以给定目标和连接对编译过程应用的代码,在连接为标号时如下所示

or like this if the linkage is return.

或在连接为 return 时如下所示。

This code sets up continue so that the procedure will return to a label

proc-return and jumps to the procedure’s entry point. The code at

proc-return transfers the procedure’s result from val to the

target register (if necessary) and then jumps to the location specified by the

linkage. (The linkage is always return or a label, because

compile-procedure-call replaces a next linkage for the

compound-procedure branch by an after-call label.)

这段代码将 continue 设置为使过程返回到标号 proc-return,并跳转到过程的入口点。proc-return 处的代码将过程结果从 val 转移到目标寄存器(若有必要),然后跳转到连接所指定的位置。(连接始终为 return 或某个标号,因为 compile-procedure-call 会将复合过程分支中的 next 连接替换为 after-call 标号。)

In fact, if the target is not val, that is exactly the code our compiler

will generate. Usually, however, the target is

val (the only time the compiler specifies a different register is when

targeting the evaluation of an operator to proc), so the procedure

result is put directly into the target register and there is no need to return

to a special location that copies it. Instead, we simplify the code by setting

up continue so that the procedure will “return” directly to the place

specified by the caller’s linkage:

实际上,若目标不是 val,编译器生成的正是上述代码。然而通常目标就是 val(编译器唯一指定其他寄存器的情形是将运算符求值的目标设为 proc),因此过程结果被直接置入目标寄存器,无需返回到一个专门的位置来复制它。于是,我们通过将 continue 设置为使过程"直接返回"到调用者连接所指定位置来简化代码:

If the linkage is a label, we set up continue so that the procedure will

return to that label. (That is, the (goto (reg continue)) the procedure

ends with becomes equivalent to the (goto (label ⟨linkage⟩)) at

proc-return above.)

若连接为标号,我们将 continue 设置为使过程返回到该标号。(即过程结尾处的 `(goto (reg continue))` 等价于上面 proc-return 处的 `(goto (label ⟨linkage⟩))`。)

If the linkage is return, we don’t need to set up continue at

all: It already holds the desired location. (That is, the (goto (reg

continue)) the procedure ends with goes directly to the place where the

(goto (reg continue)) at proc-return would have gone.)

若连接为 return,则根本无需设置 continue:它已经持有所需的位置。(即过程结尾处的 `(goto (reg continue))` 直接到达的地方,正是 proc-return 处的 `(goto (reg continue))` 本会到达的地方。)

With this implementation of the return linkage, the compiler generates

tail-recursive code. Calling a procedure as the final step in a procedure body

does a direct transfer, without saving any information on the stack.

通过这种 return 连接的实现方式,编译器生成了尾递归代码。以过程体的最后一步调用另一个过程,会直接转移控制权,而不在栈上保存任何信息。

Suppose instead that we had handled the case of a procedure call with a linkage

of return and a target of val as shown above for a non-val

target. This would destroy tail recursion. Our system would still give the

same value for any expression. But each time we called a procedure, we would

save continue and return after the call to undo the (useless) save.

These extra saves would accumulate during a nest of procedure

calls.

假设我们换一种方式来处理连接为 return、目标为 val 的过程调用,就像上面对非 val 目标的处理那样。这将破坏尾递归。系统对任何表达式仍会给出相同的值,但每次调用过程时,我们都会保存 continue,并在调用返回后执行撤销这次(毫无意义的)保存的操作。这些多余的保存会在嵌套的过程调用中不断积累。

Compile-proc-appl generates the above procedure-application code by

considering four cases, depending on whether the target for the call is

val and whether the linkage is return. Observe that the

instruction sequences are declared to modify all the registers, since executing

the procedure body can change the registers in arbitrary ways.

Also note that the code sequence for the case with target val and

linkage return is declared to need continue: Even though

continue is not explicitly used in the two-instruction sequence, we must

be sure that continue will have the correct value when we enter the

compiled procedure.

`compile-proc-appl` 通过考察四种情况来生成上述过程调用代码,具体取决于调用的目标是否为 val 以及连接是否为 return。注意,这些指令序列被声明为会修改所有寄存器,因为执行过程体可能以任意方式改变寄存器的内容。还需注意,目标为 val、连接为 return 的情况所对应的代码序列被声明为需要 continue:尽管在这两条指令的序列中并未显式用到 continue,但我们必须确保在进入已编译过程时,continue 已持有正确的值。

Racket #lang sicp
⟨compilation of operator,
 target proc, linkage next⟩
⟨evaluate operands and construct
 argument list in argl⟩
⟨compilation of procedure call
 with given target and linkage⟩
Racket #lang sicp
(define (compile-application
 exp target linkage)
 (let ((proc-code
 (compile (operator exp) 'proc 'next))
 (operand-codes
 (map (lambda (operand)
 (compile operand 'val 'next))
 (operands exp))))
 (preserving
 '(env continue)
 proc-code
 (preserving
 '(proc continue)
 (construct-arglist operand-codes)
 (compile-procedure-call
 target
 linkage)))))
Racket #lang sicp
⟨compilation of last operand, targeted to val⟩
(assign argl (op list) (reg val))
⟨compilation of next operand, targeted to val⟩
(assign argl (op cons) (reg val) (reg argl))
…
⟨compilation of first operand, targeted to val⟩
(assign argl (op cons) (reg val) (reg argl))
Racket #lang sicp
(assign argl (const ()))
Racket #lang sicp
(define (construct-arglist operand-codes)
 (let ((operand-codes
 (reverse operand-codes)))
 (if (null? operand-codes)
 (make-instruction-sequence
 '()
 '(argl)
 '((assign argl (const ()))))
 (let ((code-to-get-last-arg
 (append-instruction-sequences
 (car operand-codes)
 (make-instruction-sequence
 '(val)
 '(argl)
 '((assign argl
 (op list)
 (reg val)))))))
 (if (null? (cdr operand-codes))
 code-to-get-last-arg
 (preserving
 '(env)
 code-to-get-last-arg
 (code-to-get-rest-args
 (cdr operand-codes))))))))

(define (code-to-get-rest-args operand-codes)
 (let ((code-for-next-arg
 (preserving
 '(argl)
 (car operand-codes)
 (make-instruction-sequence
 '(val argl)
 '(argl)
 '((assign argl
 (op cons)
 (reg val)
 (reg argl)))))))
 (if (null? (cdr operand-codes))
 code-for-next-arg
 (preserving
 '(env)
 code-for-next-arg
 (code-to-get-rest-args
 (cdr operand-codes))))))
Racket #lang sicp
(test (op primitive-procedure?) (reg proc))
 (branch (label primitive-branch))
compiled-branch
 ⟨code to apply compiled procedure
 with given target and appropriate linkage⟩
primitive-branch
 (assign ⟨target⟩
 (op apply-primitive-procedure)
 (reg proc)
 (reg argl))
 ⟨linkage⟩
after-call
Racket #lang sicp
(define (compile-procedure-call
 target linkage)
 (let ((primitive-branch
 (make-label 'primitive-branch))
 (compiled-branch
 (make-label 'compiled-branch))
 (after-call
 (make-label 'after-call)))
 (let ((compiled-linkage
 (if (eq? linkage 'next)
 after-call
 linkage)))
 (append-instruction-sequences
 (make-instruction-sequence
 '(proc)
 '()
 `((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
 '(proc argl)
 (list target)
 `((assign
 ,target
 (op apply-primitive-procedure)
 (reg proc)
 (reg argl)))))))
 after-call))))
Racket #lang sicp
(assign continue
 (label proc-return))
 (assign val
 (op compiled-procedure-entry)
 (reg proc))
 (goto (reg val))
proc-return
 (assign ⟨target⟩
 (reg val)) ; included if target is not val
 (goto (label ⟨linkage⟩)) ; linkage code
Racket #lang sicp
(save continue)
 (assign continue
 (label proc-return))
 (assign val
 (op compiled-procedure-entry)
 (reg proc))
 (goto (reg val))
proc-return
 (assign ⟨target⟩
 (reg val)) ; included if target is not val
 (restore continue)
 (goto (reg continue)) ; linkage code
Racket #lang sicp
⟨set up continue for linkage⟩
(assign val
 (op compiled-procedure-entry)
 (reg proc))
(goto (reg val))
Racket #lang sicp
(assign continue
 (label ⟨linkage⟩))
(assign val
 (op compiled-procedure-entry)
 (reg proc))
(goto (reg val))
Racket #lang sicp
(assign val
 (op compiled-procedure-entry)
 (reg proc))
(goto (reg val))
Racket #lang sicp
(define (compile-proc-appl target linkage)
 (cond ((and (eq? target 'val)
 (not (eq? linkage 'return)))
 (make-instruction-sequence
 '(proc)
 all-regs
 `((assign continue (label ,linkage))
 (assign
 val
 (op compiled-procedure-entry)
 (reg proc))
 (goto (reg val)))))
 ((and (not (eq? target 'val))
 (not (eq? linkage 'return)))
 (let ((proc-return
 (make-label 'proc-return)))
 (make-instruction-sequence
 '(proc)
 all-regs
 `((assign continue
 (label ,proc-return))
 (assign
 val
 (op compiled-procedure-entry)
 (reg proc))
 (goto (reg val))
 ,proc-return
 (assign ,target (reg val))
 (goto (label ,linkage))))))
 ((and (eq? target 'val)
 (eq? linkage 'return))
 (make-instruction-sequence
 '(proc continue)
 all-regs
 '((assign
 val
 (op compiled-procedure-entry)
 (reg proc))
 (goto (reg val)))))
 ((and (not (eq? target 'val))
 (eq? linkage 'return))
 (error "return linkage,
 target not val: COMPILE"
 target))))