5.5.3 编译组合子
编译过程的本质是程序应用的编译。一个被编译的组合子带有一个给定的目标和一个连接符
的代码有如下的形式:
<compilation of operator, target proc, linkage next>
<evaluate operands and construct argument list in argl>
<compilation of procedure call with given target and linkage>
在操作符与操作数的解释期间,寄存器env,proc,arg1可能不得不被保存和恢复。
注意的是这是在编译器中的仅的一个地方它的目标不是val.
需要的代码被compile-application生成。这递归地编译了操作符,来生成代码,把
被应用的程序放入proc,编译操作数,来生成解释应用的单独的操作数的代码。操作数
的指令序列被合并(被construct-arglist)。这合并是由组装实际参数的列表的代码和
结果性的实际参数的列表。这个实参的列表由程序代码和执行程序调用的代码合并而成。
在合并的代码序列中,env proc continue这三个寄存器被保留。
(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)))))
组装实际参数列表的代码将解释每个操作数,放入val,然后把值累加到实际参数的列表,放入arg1.
因为我们把实际参数顺序地组装到arg1,我们必须以最后一个参数开始以第一个参数结束,为了
在结果的列表中参数的出现能按照第一个到最后一个的顺序出现。而不是通过初始化arg1为一个
空列表,来安装解释器的序列,而浪费了一个指令,我们让第一个代码序列组装 初始化的arg1。
实际参数的列表的通用的形式是如下的:
<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))
arg1必须被保留 在每个操作数的解释前后,除了第一个操作数,
env必须被保留 在每个操作数的解释前后,除了最后一个操作数。
编译这个实际参数的代码有一点复杂,因为第一个操作数的特殊的处理,和
保留arg1 和env在不同的地方。construct-arglist程序以解释单独的操作数
的代码为参数。如果根本没有参数,它简单地忽略指令。
(assign argl (const ()))
否则,construct-arglist创建代码,以最后一个参数初始化arg1,合并解释
其它参数的代码,并且连续地把它们加到arg1。为了处理从最后一个到第一个
参数,我们必须操作数的代码的序列的列表进行逆序,以compile-application程序
提供的顺序。
(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))))))
* 应用程序
解释了组合体的元素之后,编译的代码必须把程序应用到它的参数上。代码必须的执行
与4.1.1部分中的元循环的解释器中的apply程序中的相同的分发,也与5.4.1部分中的
显式控制的解释器中的apply-dispatch入口点是一样的。它能检查被应用的程序是一
个原生的程序还是一个编译过的程序。对于一个原生的程序,它使用 apply-primitive
-procedure程序;我们将大略地看一下它是如何处理编译过的程序。程序应用的代码
有如下的形式:
(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
注意的是编译的分支必须跳过原生程序的分支。因此,如果原来的程序调用的连接符
是next,复合的分支必须使用一个连接符跳到一个标签,这个标签被插入到原生
程序分支的后面。(这与编译条件表达式中的对真值分支的使用的连接符是相似的)
(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))))
原生的和复合的分支,与条件表达式中的真值和假值分支是相似的,使用
parallel-instruction-sequences合并,而不是普通的 append-instruction-sequences
因为它们不是顺序地执行。
* 应用编译过的程序
处理程序应用的代码是编译器中的最巧妙的部分,即使它生成的指令序列是非常短的。
一个编译过的程序(由compile-lambda组装的)有一个入口点,它是一个指向程序的
代码从哪开始的标签。在这个入口点的代码计算一个结果并且通过执行指令(goto (reg 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
(goto (label <linkage>)) ; linkage code
或者如果连接符是返回的话像这样:
(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
这个代码设置了continue,是为了程序将返回到一个标签proc-return和
跳转到程序的入口点。proc-return的代码把程序的结构从val转移到目标
寄存器并且然后跳转到连接符指定的位置。(连接符总是返回或者是一个标签
因为通过一个after-call标签,为了复合的程序分支, compile-procedure-call
程序把一个next连接符给替换了)
在事实上,如果目标不是val,那是我们的编译将生成精确的代码。通常,然而,
目标是val(仅有一次编译器指定了另一个寄存器是当一个操作符的解释目标为proc)
,所以程序的结果被直接放在了目标寄存器和不需要返回一个特定的位置。代替的是,我们简化
了设置continue的代码,所以程序将直接返回调用者的连接符指定的位置。
<set up continue for linkage>
(assign val (op compiled-procedure-entry) (reg proc))
(goto (reg val))
如果连接符是一个标签,我们设置continue为了程序返回到那个标签。(也就是
程序以(goto (reg continue)) 结尾变成了等价于在如上的proc-return程序中的
(goto (label <linkage>)) )。
(assign continue (label <linkage>))
(assign val (op compiled-procedure-entry) (reg proc))
(goto (reg val))
如果连接符是返回,我们不需要设置继续寄存器。它已保存了期望的位置。
(也就是程序以(goto (reg continue)) 结尾能直接到proc-return程序中的
(goto (label <linkage>)) 的位置。)
(assign val (op compiled-procedure-entry) (reg proc))
(goto (reg val))
有了这个返回连接符的实现,编译器生成了尾递归的代码。调用一个程序,
作为最后一步,在一个程序体中做一个直接的回忆的我,没有在栈上保存任何信息。
假定除了我们已经处理的有一个返回的连接符的程序调用的情况和如上显示的val
作为目标对于非val的目标。这将破坏尾递归。我们的系统将仍然为任何的表达式
给出相同的值。但是每次我们调用一个程序,我们将保存继续的寄存器和调用之后返回
恢复保存。这些额外的保存在一个程序调用的嵌套中会被累加的。
compile-proc-appl生成了如上的程序应用的代码,通过考虑四种情况,依赖于目标
是否是val,连接符是否是返回.注意的是指令序列被声明修改所有的寄存器,因为执行程序体
能以随意的方式修改寄存器。也注意的是代码序列为了目标是val,连接符是返回的情况,被声明
需要继续的寄存器:即使这个寄存器没有显式的使用在两个指令的序列中,我们必须确定
当我们进入到被编译的程序中时继续寄存器将有正确的值。
(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))))