计算机程序的构造与解释第5.5.1至5.5.4节翻译

代序

原书此章节翻译词不达意,重译以缀补,冀以补其漏。

名词表

compiled code 汇编指令
linkage descriptor 链接符
target 目标,目标寄存器
code generator 指令生成器
instruction sequence 指令序列,汇编指令序列

5.5.1 编译器的结构

本书4.1.7节修改了原始的元循环求值器,将分析与执行过程分离。修改后,表达式被逐一分析,生成对应的scheme过程,此过程以环境指针作为唯一参数,所执行的操作与原表达式一致。编译器编译时,也要做相同的分析工作,但最终将生成一系列可以在寄存器机器上运行的指令序列,而非scheme过程。

compile过程在编译器中做最顶层的分派工作,对应于4.1.1节中的eval过程,4.1.7节中的analyze过程,以及5.4.1节显式控制求值器的eval-dispatch入口点。编译器也将使用4.1.2节中定义的表达式语法检测过程,将待编译表达式按词法类型分派给各自的“指令生成器”:

(define (compile exp target linkage)
  (cond ((self-evaluating? exp) (compile-self-evaluating exp target linkage))
        ((quoted? exp) (compile-quoted exp target linkage))
        ((variable? exp) (compile-variable exp target linkage))
        ((assignment? exp) (compile-assignment exp target linkage))
        ((definition? exp) (compile-definition exp target linkage))
        ((if? exp) (compile-if exp target linkage))
        ((lambda? exp) (compile-lambda exp target linkage))
        ((begin? exp) (compile-sequence (begin-actions exp) target linkage))
        ((cond? exp) (compile (cond->if exp) target linkage))
        ((application? exp) (compile-application exp target linkage))
        (else (error "未知表达式类型 -- COMPILE" exp))))

目标寄存器和链接符

compile及指令生成器除了待编译表达式外,还需要两个参数,其一为目标寄存器,指定表达式对应汇编指令存放表达式返回值的位置。其二为链接符,描述表达式对应汇编指令执行完成后如何继续执行后续指令。链接符有以下三种取值:

  • 'next:继续执行下一指令
  • 'return:从当前过程中返回
  • '<some-label>:跳转到<some-label>继续执行

例如,编译达式5,如果以val为目标寄存器,'next为链接符,将得到指令

(assign val (const 5))

如果以'return为链接符,将得到

(assign val (const 5))
(goto (reg continue))

于前者,表达式对应汇编指令执行完后,将继续执行指令序列中的下一指令。于后者,执行完原表达式汇编指令后,将执行过程返回指令。二者都将结果放在val中。

指令序列和栈的使用

每个指令生成器都返回一个指令序列,包含所编译表达式的目标代码。编译复合表达式时,指令生成器先编译各个成分表达式,再将这些编译结果组合起来,恰似(元循环求值器中)组合表达式的求值是求值各成分表达式一样。

最简单的组合指令序列的过程是append-instruction-sequences。此过程的参数为任意数量的指令序列,这些指令序列之间为顺序执行。此过程依次将它们组成一个指令序列。例如,<seq1>和<seq2>是两个顺序执行的指令序列,那么调用

(append-instruction-sequences <seq1> <seq2>)

将得到

<seq1>
<seq2>

当被组合的令序列执行前后需要保存和恢复寄存器时,指令生成器会调用preserving来执行这一精细的指令组合操作。preserving需要三个参数,分别是需要关注的寄存器集合,以及两个被组合的指令序列。对preserving来说,如果第二个指令序列需要用到需关注寄存器集合中的寄存器,那么,在组合指令序列时,第一个指令序列的前后就会分别加上保存和恢复这些被用到的寄存器的指令。换句话说,如果第一个指令序列修改了某个寄存器,而第二个指令序列恰好需要这个寄存器的原始内容,那么preserving就会在第一个指令序列之前加入save,之后加入restore来执行保存和恢复对应寄存器。若非如此,preserving就只简单的把两段指令序列拼接起来。例如,

(preserving (list <reg1> <reg2>) <seq1> <seq2>)

根据<seq1>和<seq2>对寄存器的使用情况,将产生以下四种指令列:

<seq1>
<seq2>
(save <reg1>)
<seq1>
(restore <reg1>)
<seq2>
(save <reg2>)
<seq1>
(restore <reg2>)
<seq2>
(save <reg2>)
(save <reg1>)
<seq1>
(restore <reg1>)
(restore <reg2>)
<seq2>

通过使用preserving来组合指令序列,编译器就可以减少不必要的栈操作。上述方法将寄存器的保存和恢复局限在preserving中,在构造指令生成器时,就不用再考虑这一问题了。本书中展示的指令生成器也确实没有主动生成相关的save和restore操作。

指令序列大体上可以简单表示为一个指令的列表,append-instruction-sequences可以通过常规的拼接操作来连接指令序列,但preserving不能样做,因为它必须分析序列中的每条指令来判断这些指令如何使用寄存器。甚至于,即使被分析的指令序列本身是通过另一次preserving构造起来的,preserving也要去逐一分析,这一操作将十分复杂且低效。为了避免这样重复的分析过程,每个指令序列都将关联其寄存器使用信息。构造单条指令时,寄存器使用信息将被显式提供,这样在组合指令序列时,组合指令的寄存器使用信息就可以通过其组成指令序列的寄存器使用信息推导出来。

一个指令序列包含以下三类信息:

  • 在指令序列执行前必须初始化好的寄存器集合。(指令序列需要用到的寄存器集合)
  • 执令执行时将会修改的寄存器集合
  • 指令列表
    本书使用包含上述三个部分的列表来表示指令序列,其构造函数是:
(define (make-instruction-sequence needs modifies statements)
  (list needs modifies statements))

例如,一个含有两条指令的序列,第一条从环境中查找变量x的值,写入val寄存器,第二条指令执行函数返回操作。因此该指令序列需要env和continue寄存器初始化好,执行中将修改val,所以该指令序列可以用以下调用构造:

(make-instruction-sequence
 '(env continue)
 '(val)
 '((assign val (op lookup-variable-value) (const x) (reg env))
   (goto (reg continue))))

当需要构造空指令序列时,可以用以下方法:

(define (empty-instruction-sequence)
  (make-instruction-sequence '() '() '()))

用于组合指令序列的过程在5.5.4节中讨论。

5.5.2 编译表达式

本节与下一节实现compile中的指令生成器。

编译链接符

一般情况下,指令生成器在指令结束后的链接指令由compile-linkage生成。如果链接符是return,那么必须在指令序列最后添加指令(goto (reg continue)),这条指令执行前需要continue寄存器初始化好,不修改其它寄存器。如果链接符是next,则不需要添加任体指令。此外,链接符是标号时,需要附加一条goto指令到对应标号,不用修改其它寄存器。

(define (compile-linkage linkage)
 (cond ((eq? linkage 'return) (make-instruction-sequence '(continue) '() '((goto (reg continue)))))
       ((eq? linkage 'next) (empty-instruction-sequence))
       (else (make-instruction-sequence '() '() `((goto (label ,linkage)))))))

由链接符生成的指令将附加在被编译指令序列最后,并要求保留continue寄存器。这是因为return链接符需要它。如果表达式的指令序列修改了continue,而链接指令又需要它,continue就会在表达式指令序列执行前被保存,执行后再恢复。

(define (end-with-linkage linkage instruction-sequence)
 (preserving '(continue)
             instruction-sequence
             (compile-linkage linkage)))

编译简单表达式

对于自我求值,引用,变量表达式,指令生成器将构造指令,将结果放在目标寄存器中,然后附加上链接符生成的指令。

(define (compile-self-evaluating exp target linkage)
 (end-with-linkage linkage (make-instruction-sequence '() (list target) `((assign ,target (const ,exp))))))

(define (compile-quoted exp target linkage)
 (end-with-linkage linkage (make-instruction-sequence '() (list target) `((assign ,target (const ,(text-of-quotation exp)))))))

(define (compile-variable exp target linkage)
 (end-with-linkage linkage
                   (make-instruction-sequence '(env)
                                              (list target)
                                              `((assign ,target (op lookup-variable-value) (const ,exp) (reg env))))))

上述三种表达式的指令序列都需要修改target寄存器,此外变量表达式需要env寄存器。

赋值和定义表达式的处理跟解释器中的做法类似,首先递归生成计算要赋予或定义的实际值的指令序列,再在其后附加设置或定义对应变量为该值的指令,最后将'ok做为整个表达式的结果,写入target寄存器。递归计算实际值的部分需要使用val做为target,用'next做为链接符,这样当实际值计算完成后,可以自然进入到后续的赋值指令中。连接计算实际值的指令和赋值指令时,需要保存env寄存器,因为执行赋值或定义时需要使用env,而计算实际值的指令很可能修改它。

(define (compile-assignment exp target linkage)
 (let ((var (assignment-variable exp))
       (get-value-code (compile (assignment-value exp) 'val 'next)))
  (end-with-linkage linkage
                    (preserving '(env)
                                get-value-code
                                (make-instruction-sequence '(env val) (list target)
                                                           `((perform (op set-variable-value!)
                                                             (const ,var)
                                                             (reg val)
                                                             (reg env))
                                                             (assign ,target (const ok))))))))

(define (compile-definition exp target linkage)
 (let ((var (definition-variable exp))
       (get-value-code (compile (definition-value exp) 'val 'next)))
  (end-with-linkage linkage
                    (preserving '(env)
                                get-value-code
                                (make-instruction-sequence '(env val) (list target)
                                                           `((perform (op define-variable!)
                                                             (const ,var)
                                                             (reg val)
                                                             (reg env))
                                                             (assign ,target (const ok))))))))

附加在最后的两条指令需要使用val和env,另外还会修改target寄存器,此外在连接指令序列时,只保留了env,这是因为get-value-code部分会将计算结果放在val中,供后面的序列使用。(保留 val反而会导致错误,因为preserving会恢复原始的val值,后续指令就无法取到计算结果了)

编译条件表达式

具有特定目标寄存器和链接符的if表达式的编译结果具有以下形式:

 ⟨compilation of predicate, target val, linkage next⟩
(test (op false?) (reg val))
    (branch (label false-branch))
true-branch
    ⟨compilation of consequent with given target and given linkage or after-if⟩
false-branch
    ⟨compilation of alternative with given target and linkage⟩
after-if

为了生成这样的指令序列,需要分别编译if表达式的谓词部分,真分支和假分支,然后将它们与测试谓词的指令序列组合,同时要在真分支开始,假分支开始和指令序列结束部分放置对应的标签用于跳转。在这种安排下,如果谓词部分为假,就必须跳转到假分支部分执行。此外,如果执行到真分支,那么真分支执行完后,也需要执行链接符指令。如果链接符是'return或者标签,那么无论真假分支都将使用相同的链接符指令。如果链接符是'next那么真分支就必须在执行完成后跳转到if表达式指令序列结束的地方。

(define (compile-if exp target linkage)
 (let ((t-branch (make-label 'true-branch))
       (f-branch (make-label 'false-branch))
       (after-if (make-label 'after-if)))
  (let ((consequent-linkage (if (eq? linkage 'next) after-if linkage)))
   (let ((p-code (compile (if-predicate exp) 'val 'next))
         (c-code (compile (if-consequent exp) target consequent-linkage))
         (a-code (compile (if-alternative exp) target linkage)))
    (preserving '(env continue)
                p-code
                (append-instruction-sequences
                 (make-instruction-sequence '(val) '() `((test (op false?) (reg val)) (branch (label ,f-branch))))
                (parallel-instruction-sequences
                 (append-instruction-sequences t-branch c-code)
                 (append-instruction-sequences f-branch a-code))
                after-if))))))

因为谓词部分指令可能会修改env,而真假分支的指令序列中的链接相关指令可能会修改continue,所以要保存这两个寄存器。真假分支的指令序列因为不是顺序执行的,所以要用特殊的方法parallel-instruction-sequences来组合,该方法在5.5.4节讨论。

另外,cond表达式可以被派生为多层if表达式,因此编译cond表达式时,编译器只需要先用conf->if将其转换为if表达式,再对派生的if表达式进行编译。

编译序列表达式

过程体中的表达式序列或者由begin引起的表达式序列中,各个表达式的求值是不相关的。编译时,序列中每个表达式都被编译成对应的汇编指令,除了最后一个表达式使用整个表达式序列的链接符进行编译外,其它的表达式都用'next做为链接符进行编译。再将各表达式的汇编指令逐一组合成最终的汇编指令序列,在组合时,每条指令序列执行前都将保留env和continue寄存器。保留env是因为序列中后面的表达式可能会用到env,保留continue是因为最后一条指令可能需要用continue做跳转。

(define (compile-sequence seq target linkage)
 (if (last-exp? seq)
     (compile (first-exp seq) target linkage)
     (preserving
      '(env continue)
      (compile (first-exp seq) target 'next)
      (compile-sequence (rest-exps seq) target linkage))))

编译lambda表达式

lambda表达式用于构造函数,其编译结果具有以下形式:

⟨construct procedure object and assign it to target register⟩
⟨linkage⟩

编译lambda表达式时,也需要为过程体生成指令序列。尽管过程体在lambda定义时并不执行,但将其汇编指令直接放在lambda定义的位置后仍然可以会后面带来方便。如果编译lambda表达式所用的链接符是标签或'return,就没什么特殊之处,但如果是'next,则汇编指令序列最后必须跳过过程体,这需要在过程体结束位置放置一个标签,并用goto跳转。因此,lambda表达式的指令序列应具有以下形式

    ⟨construct procedure object and assign it to target register⟩ ;; 一般是将lambda的入口地址和环境指针赋予某个寄存器
    ⟨code for given linkage⟩ or (goto (label after-lambda)) ;; 跳转到特定地方
    ⟨compilation of procedure body ⟩ ;; lambda过程体指令,包含过程体的入口标签
after-lambda ;; 结尾标记,第二行指令有可能会跳转到这里

compile-lambda用来编译lambda表达式,它先生成用于构造过程对像的指令序列,然后后面接着过程体的指令序列。过程对像本身,是在指令运行时,通过将lambda定义时的环境,与lambda入口标签组合起来得到。

(define (compile-lambda exp target linkage)
 (let ((proc-entry (make-label 'entry))
       (after-lambda (make-label 'after-lambda)))
  (let ((lambda-linkage
         (if (eq? linkage 'next) after-lambda linkage)))
   (append-instruction-sequences
    (tack-on-instruction-sequence
     (end-with-linkage lambda-linkage
      (make-instruction-sequence '(env) (list target)
                                 `((assign ,target (op make-compiled-procedure) (label ,proc-entry) (reg env)))))
     (compile-lambda-body exp proc-entry))
    after-lambda))))

compile-lambda使用tack-on-instruction-sequence这一特殊过程来将过程体指令序列与lambda表达式本身的指令序列组合起来,而非之前的append-instruction-sequences,这是因为过程体不是表达式指令序列的一部分,当组合后的指令序列运行时,过程体部分不会执行。将它组合进这里,仅仅是出于方便。

lambda表达式的过程体指令序列由compile-lambda-body生成,该序列由一个入口标签作为起始,接下来是将运行时的环境切换为正确环境的指令,也就是将环境切换为lambda定义时的环境,再加上lambda表达式形参与实参的绑定关系。接下来就是lambda过程体的指令序列。过程体的表达式编译时的链接符是'return,并且将结果写入val。

(define (compile-lambda-body exp proc-entry)
  (let ((formals (lambda-parameters exp)))
    (append-instruction-sequences
     (make-instruction-sequence '(env proc argl) '(env)
                                `(,proc-entry
                                  (assign env (op compiled-procedure-env) (reg proc)) ;; 切换环境
                                  ;; 扩展实参与形参对应关系
                                  (assign env
                                          (op extend-environment)
                                          (const ,formals)
                                          (reg argl)
                                          (reg env))))
     (compile-sequence (lambda-body exp) 'val 'return))))

编译复合表达式

编译最本质的地方就在于编译过程应用。对于给定目标和链接符的复合表达式,其指令序列是以下形式

⟨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和argl寄存器,需要做好保留和恢复操作。这也是整个编译器中,唯一一处目标寄存器不是val的地方。

复合表达式的指令序列由compile-application生成。它先编译运算符部分,将结果存入proc寄存器,然后编译参数部分,逐一产生出每个参数的计算指令序列,然后将这些序列,通过将累积参数到argl寄存器的指令序列组合在一起。实现累积的指令序列由construct-arglist生成。累积的结果再与过程体指令序列和调用过程体的指令序列组合在一起。组合过程中,env寄存器在求值运算符前后被保留和恢复(因为求值运算符时可能会修改env,而求值参数时又要用到),此外proc寄存器在构造参数列表指令序列前后也要保留和恢复(因为求值参数时可能会用到proc,而运算符的结果恰好保留在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中,再通过cons将值与argl中的列表组合起来。因为累积参数是通过cons来完成,所以必须倒过来处理参数列表,最后一个参数第一个被累积进argl。累积第一个参数的指令序列还需要负责初始化argl寄存器,这样就不需要预先把argl初始化成'()了。构造参数列表的指令序列的一般形式是:

⟨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))

除了累积第一个参数外,其它累积参数的指令前后,都要保存和恢复argl寄存器,而除了最后一个参数外,其它参数的累积指令前后都需要保存和恢复env寄存器。

因为上述的处理第一个和最后一个参数时的特殊性,构造参数列表的指令生成器比较复杂。construct-arglist使用和过程调用一样的参数列表,如果发现过程调用没有参数,就生成指令

(assign argl (const ()))

否则,construct-arglist生成用最后一个参数初始化argl寄存器的指令序列,然后再生成剩余参数的指令序列并附加到前面的指令序列后面,再加上将参数值累积到argl中的指令。为了从后往前处理参数列表,必须先将参数列表逆序。

(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))))))

过程应用

对复合表达式编译后,编译器需要产生用argl中的参数来调用proc中过程的指令。这一过程与4.1.1节元循环求值器过程应用的方式或5.4.1节显示控制求值器的过程应用方式很像,它首先检查被应用的过程是原始过程还是复合过程,对于原始过程,调用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

由于compiled-branch后的指令序列与primitive-branch并非先后执行,而是并列的,因此,若原始表达式编译时的链接符是'next,compound-branch就必须在其指令序列最后跳到after-all标签处,就像compile-if里那样。

(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))))

就像compile-if中真假分支一样,这里的原生和复合分支也是用parallel-instruction-sequences进行连接的,因为它们是并列关系而非顺序执行。

应用过程体

虽然最终应用过程体的指令序列很短,但它的生成过程仍然是整个编译器中最精妙的部分。一个编译后的过程体(例如compile-lambda生成的指令序列)都有一个入口点,用于标识过程体的开始。此入口点之后的指令序列会进行计算,然后将结果放入val寄存器,再执行(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))  ; 如果目标寄存器不是 val
 (goto (label ⟨linkage⟩))  ; 链接符

如果链接符是'return,那么

 (save continue)
 (assign continue (label proc-return))
 (assign val (op compiled-procedure-entry) (reg proc))
 (goto (reg val))
proc-return
 (assign ⟨target⟩ (reg val)) ; 如果目标寄存器不是 val
 (restore continue)
 (goto (reg continue))   ; 链接符

指令序列先设置好continue寄存器,以便过程体结束后可以返回proc-return标签,然后就跳转到过程体入口标签。proc-return标签处的指令在必要时,会将过程体的结果从val中移动到编译过程调用时指令的寄存器,然后跳转到过程应用编译时指定的链接符去。(链接符一定是'return或标签,因为compile-procedure-call对于复合过程,会把'next链接符替换成after-call标签。
如果被编译的过程应用的表达式的目标寄存器不是val,那么编译结果就是上述的形式。否则,只要将结果放进val中就行,不需要再从其它地方复制到val中,这样,指令序列只需要将continue设置成过程调用编译时的链接符,这样过程调用完成后,就会直接返回到过程调用表达式之后。

⟨set up continue for linkage⟩
(assign val (op compiled-procedure-entry) (reg proc))
(goto (reg val))

如果过程调用表达式编译链接符是标签,那么continue将被设置为返回那个标签。

(assign continue (label ⟨linkage⟩))
(assign val (op compiled-procedure-entry) (reg proc))
(goto (reg val))

如果过程调用表达式的编译链接符是'return,那么continue无须进行设置(也就是不用先返回到proc-return再执行一次跳转。proc-return单纯就是为了处理结果而存在的,如果结果不需要处理,就不需要proc-return),过程调用最后的返回指令会自然的返回。
采用上述方式实现'return链接符,将会产生尾递归优化的指令序列。在表达式最后进行过程调用将直接进行跳转,不需要额外保存调用的返回信息。
假设对于使用'return做为链接符,val做为目标寄存器的过程调用,编译器也将其当做目标寄存器为非val的表达式对待,那么尾递归优化就会被破坏。虽然编译后运行结果是一样的,但是每次过程调用都必须保存和恢复continue寄存器,且对于递归调用来说,这一过程将反复进行并积累在栈上。(编译器进行尾递归优化似乎是一件很自然的事,但大多数语言的编译器,像C,Pascal,都不会这样做。这是因为这些编译器同时用栈来存储过程调用的参数和返回值,这样可以不用进行专门的垃圾回收,通常这样效率也更高。而scheme的参数保存在内存中,这样做是为了便于进行垃圾回收。)

compile-proc-appl生成过程调用指令时,会同时考虑上述4种情况。需要注意的要点有:1,过程体的指令序列可能会修改所有寄存器,因此编译时要指定保留所有寄存器。2,当目标寄存器为val,链接符为'return时,即使continue没有显式的用到,也需要指明需要continue,因为过程体最终返回时需要continue。

(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))))

5.5.4 组合指令序列

本节描述指令序列的表示与组合。5.5.1节已经说过指令序列列表由所需寄存器列表,修改寄存器列表,以及指令列表构成,标签也被做为一种特殊的指令,不需要任何寄存器,也不修改任何寄存器。基于此结构,从指令序列中解出所需寄存器和修改寄存器将使用以下选择方法

(define (registers-needed s)
  (if (symbol? s) '() (car s)))
(define (registers-modified s)
  (if (symbol? s) '() (cadr s)))
(define (statements s)
  (if (symbol? s) (list s) (caddr s)))

确定某个指令序列是否需要或修改某个寄存器,需要使用以下判断方法

(define (needs-register? seq reg)
  (memq reg (registers-needed seq)))
(define (modifies-register? seq reg)
  (memq reg (registers-modified seq)))

通过以上选择方法和判断方法,就可以实现各种指令序列的组合方法了。

最基本的组合方法是append-instruction-sequences,此方法使用若干序顺执行的指令序列做为参数,返回一个新的指令序列,其指令列表是所有参数指令序列的组合。一个略微复杂的问题在于确定组后后序列所需要的寄存器和要修改寄存器。该序列所要修改的寄存器就是被组合的所有序列所要修改的寄存器的并集。而它所需要的寄存器,则是第一个序列所需的寄存器,与后续每个序列所需要,且没有被此后续序列之前的任意序列所修改的寄存器。

指令序列的拼接,通过反复调用append-2-sequences方法完成,每次调用将前两个序列拼成一个新序列。该方法接收seq1和seq2两个序列,返回的新序列中,被修改寄存器就是seq1和seq2所修改寄存器的并集,而所需要寄存器是seq1所需要的寄存器,外加seq2所需要且没有被seq1修改的寄存器。(用集合表示:组合序列所需要的寄存器 = seq1所需寄存器 + (seq2所需寄存器 - seq1要修改寄存器))完整的append-instruction-sequences如下

(define (append-instruction-sequences . seqs)
  (define (append-2-sequences seq1 seq2)
    (make-instruction-sequence
     (list-union (registers-needed seq1)
                 (list-difference (registers-needed seq2)
                                  (registers-needed seq1)))
     (list-union (registers-modified seq1)
                 (registers-modified seq2))
     (append (statements seq1) (statements seq2))))
  (define (append-seq-list seqs)
    (if (null? seqs)
        (empty-instruction-sequence)
        (append-2-sequences (car seqs)
                            (append-seq-list (cdr seqs)))))
  (append-seq-list seqs))

此方法使用的集合运算在2.3.3节中有描述

(define (list-union s1 s2)
  (cond ((null? s1) s2)
        ((memq (car s1) s2) (list-union (cdr s1) s2))
        (else (cons (car s1) (list-union (cdr s1) s2)))))

(define (list-difference s1 s2)
  (cond ((null? s1) '())
        ((memq (car s1) s2) (list-difference (cdr s1) s2))
        (else (cons (car s1)
                    (list-difference (cdr s1) s2)))))

preserving是第二个用于组合序列的重要方法,接收一个寄存器列表regs,两个指令序列列表seq1和seq2,返回一个新的指令序列,包含seq1和seq2的指令,些外还会在原seq1指令的前后加入合适的save和restore来保护regs中被seq2需要,且被seq1修改的寄存器。为了达成这一功能,preserving道先创建一个扩充序列,包含所需的save指令,然后加入seq1中的指令,再加上所需的restore指令。此序列需要的寄存器就是需要保存和恢复的寄存器,外加被seq1所需要的寄存器。要修改的寄存器是seq1所要修改,且没有被额外保存和恢复的寄存器。这一扩充序列再与seq2按照常规的方法(append-instruction-sequences)进行组合。以下方法通过递归策略遍历要保存的寄存器列表来实现preserving方法

(define (preserving regs seq1 seq2)
  (if (null? regs)
      (append-instruction-sequences seq1 seq2)
      (let ((first-reg (car regs)))
        (if (and (needs-register? seq1 first-reg)
                 (modifies-register? seq1 first-reg))
            (preserving (cdr regs)
                        (make-instruction-sequence
                         (list-union (list first-reg)
                                     (registers-needed seq1))
                         (list-difference (registers-modified seq1)
                                          (list first-reg))
                         (append `((save ,fist-reg))
                                 (statements seq1)
                                 `((restore ,first-reg))))
                        seq2)
            (preserving (cdr regs) seq1 seq2)))))

另一个序列组合方法是tack-on-instruction-sequence,它由compile-lambda调用,将过程体组合到另一个序列上。因为被组合的过程体不是在组合的地方立即执行的,所以其所需寄存器与另一被组合序列是否需要或修改没有关系。因此,在组合时,可以安全的忽略过程体中指令所需要和所修改的寄存器。

(define (tack-on-instruction-sequence seq body-seq)
  (make-instruction-sequence
   (registers-needed seq)
   (registers-modified seq)
   (append (statemens seq) (statemens body-seq))))

compile-if和compile-procedure-call使用特殊的组合方法parallel-instruction-sequences,此方法在测试指令后添加两个分支,两个分支是并列关系,不会顺序执行,而是根据测试指令的结果,选择其中之一。因此,无论第一个序列是否修改了这些寄存器,后一个序列所需要的所有寄存器都会被组合后的指令序列需要

(define (parallel-instruction-sequences seq1 seq2)
  (make-instruction-sequence
   (list-union (registers-needed seq1)
               (registers-needed seq2))
   (list-union (registers-modified seq1)
               (registers-modified seq2))
   (append (statements seq1) (statemens seq2))))
posted @ 2021-05-05 22:43  戊戌乡人  阅读(173)  评论(0)    收藏  举报