5.4.4 运行解释器

5.4.4 运行解释器
对显式控制的解释器的实现,我们已经来到了开发的末尾了,在第一章开始时,
我们已经连续探索了解释过程的更加精确的模型。我们开始于相对地非正式的
替换模型,然后在第三章中扩展了它,成为了环境模型,它让我们有能力处理
状态和改变。在第四章的元循环的解释器中,在表达式的解释期间,为了做
更显式的环境结构的组装,我们使用了scheme本身作为一个语言。现在,
用寄存器机器,我们能更贴近地观察解释器的机制,例如存储管理,实际参数
的传递,和控制。在描述的每一个新的层级,我们都不得不引出问题,解决在
之前不是很明显的麻烦,解释的更少的精确性的处理。为了理解显式控制的
解释器的行为,我们能模拟它,监控它的性能。

在我们的解释器机器中,我们将安装一个驱动循环。这扮演着4.1.4部分中的
driver-loop程序的角色。解释器将重复打印一个提示,读取一个表达式,通过
使用eval-dispatch,解释表达式,再打印结果。如下的指令形成了显式控制的
解释器的控制器序列的开头:

read-eval-print-loop
  (perform (op initialize-stack))
  (perform
   (op prompt-for-input) (const ";;; EC-Eval input:"))
  (assign exp (op read))
  (assign env (op get-global-environment))
  (assign continue (label print-result))
  (goto (label eval-dispatch))
print-result
  (perform
   (op announce-output) (const ";;; EC-Eval value:"))
  (perform (op user-print) (reg val))
  (goto (label read-eval-print-loop))

在一个程序中,当我们遇到了一个错误时,(例如“未知的程序类型的错误”
显示在apply-dispatch),我们打印一个错误的消息并且返回到驱动循环。

unknown-expression-type
  (assign val (const unknown-expression-type-error))
  (goto (label signal-error))
unknown-procedure-type
  (restore continue)    ; clean up stack (from apply-dispatch)
  (assign val (const unknown-procedure-type-error))
  (goto (label signal-error))
signal-error
  (perform (op user-print) (reg val))
  (goto (label read-eval-print-loop))

为了模拟的目的,在驱动循环之中,每次我们初始化栈,因为在一个错误
(例如一个未定义的变量)中断了一个解释后,栈可能是不空的。

如果我们组合了显示在5.4.1到5.4.4部分的所有的代码片段,我们能创建一
个解释器机器模型,我们能运行它,使用5.2部分中的寄存器机器的模拟器。

(define eceval
 (make-machine
   '(exp env val proc argl continue unev)
   eceval-operations
  '(
    read-eval-print-loop
      <entire machine controller as given above>
   )))

我们必须定义scheme程序来模拟被解释器作为原生程序的操作。这些程序是相同的程序,
与我们为了在4.1部分中的元循环解释器所使用的程序,带有小量的追加的定义,在5.4
部分的注脚里。

(define eceval-operations
  (list (list 'self-evaluating? self-evaluating)
        <complete list of operations for eceval machine>))

最后,我们能够初始化全局的环境变量和运行解释器:

(define the-global-environment (setup-environment))
(start eceval)
;;; EC-Eval input:
(define (append x y)
  (if (null? x)
      y
      (cons (car x)
            (append (cdr x) y))))
;;; EC-Eval value:
ok
;;; EC-Eval input:
(append '(a b c) '(d e f))
;;; EC-Eval value:
(a b c d e f)

当然了,解释表达式以这种方式,将花更长的时间,比我们直接把它们运行在scheme中。
因为包括了模拟的多个层级。我们的表达式被解释,通过显式控制的解释器的机器,这个机器
被一个scheme程序所模拟,这个scheme程序本身被scheme解释器所解释。

*  监控解释器的性能
模拟能成为强有力的工具来指导解释器的实现。模拟让它变得容易,不仅为了探索
寄存器机器的设计的演变,而且监控被模拟的解释器的性能。例如,在性能方面
一个重要的因素是解释器如何有效地使用栈。我们能注意到解释各种表达式要求的
栈操作的次数,通过定义解释器寄存器机器带有模拟器的版本,这个模拟器收集
在栈使用方面的统计信息,并且在解释器的打印结果的入口点上加一个指令来打印
统计信息:

print-result
  (perform (op print-stack-statistics)); added instruction
  (perform
   (op announce-output) (const ";;; EC-Eval value:"))
  ... ; same as before

与解释器的交互现在看起来像这样了:

;;; EC-Eval input:
(define (factorial n)
  (if (= n 1)
      1
      (* (factorial (- n 1)) n)))
(total-pushes = 3 maximum-depth = 3)
;;; EC-Eval value:
ok
;;; EC-Eval input:
(factorial 5)
(total-pushes = 144 maximum-depth = 28)
;;; EC-Eval value:
120

附记的是解释器的驱动循环在每次交互的开始时重新初始化了栈,为了被打印
的统计信息将仅记录解释之前的表达式时使用的栈操作。

练习5.26
使用带监控的栈来探索解释器的尾递归属性(5.4.2部分)。启动解释器
并且定义一个迭代的阶乘程序,它来自于1.2.1部分:

(define (factorial n)
  (define (iter product counter)
    (if (> counter n)
        product
        (iter (* counter product)
              (+ counter 1))))
  (iter 1 1))

以一些小的数n运行程序。记录这些数中的每一次的最大的栈深度和
计算n的阶乘需要的压栈次数。

a.你能发现解释n的阶乘需要的最大深度独立于n,那个深度是什么?
 
b.对于任意的n,它是大于1的数,在解释n的阶乘时,使用的压栈的总次数的
用n为自变量的公式,请确定一下这个公式是什么?注意的是操作的次数是
n的一个线性的函数,因为它由两个常数来确定。

练习5.27 
为了与练习5.26进行比较,探索如下的递归的计算阶乘的程序的行为:

(define (factorial n)
  (if (= n 1)
      1
      (* (factorial (- n 1)) n)))

通过用有监控能力的栈运行这个程序,以一个n的函数,来确定计算阶乘的过程中
栈的最大深度和压栈的总的操作次数。(这也是线性的函数)总结你的经验,并且
填写如下的表格,用n的合适的表达式:

——————————————————————
|                  |     最大深度  |     压栈次数                   |                   
——————————————————————
|   递归        |                    |                                      |
——————————————————————
|   迭代        |                    |                                      |
——————————————————————  
          
在执行计算的过程中,最大深度是解释器所使用的空间的数量的度量,并且压栈的次数
很好地与消耗的时间相关联。

练习5.28
通过修改在5.4.2部分中描述的eval-sequence程序,修改解释器的定义,为了让解释器
不再是尾递归的。重新运行你的练习5.26和练习5.27中的实验,演示你的阶乘程序的两个
版本,现在需要的空间是随着输入的变化而线性增长的。

练习5.29
在树形递归的斐波那些数的计算中,监控栈的操作:

(define (fib n)
  (if (< n 2)
      n
      (+ (fib (- n 1)) (fib (- n 2)))))

a. 给出一个n的公式来计算斐波那些数的需要的栈的最大深度。提示:在1.2.2部分中
   我们讨论了这种过程的增长是n的线性函数所使用的空间。 
b. 给出一个n的公式来计算斐波那些数的需要的栈的压栈总次数。你应该发现压栈的次数是
指数级增长的。提示:让S(n)是压栈的总次数,你应该能讨论表示S(n)的公式是就S(n-1)和
S(n-2)和一些独立于n的常数k。给出公式,说出k是什么,然后,显示S(n)为a Fib(n+1)+b
再给出a和b的值。

练习5.30 
我们的解释器现在只是捕捉和报出了两种错误消息,未知的表达式类型和未知的程序类型。
其它的错误将让我们跳出了解释器的错解释打印的循环。当我们用寄存器机器的模拟器运行
解释器时,这些错误被底层的scheme系统捕捉到了。这与当一个用户的程序生成了一个错误
导致计算机受到冲击是相似的。为了让一个真正的错误系统有效的工作是一个大工程,但是
在这里理解它包括什么的努力是很有价值的。

a.  在解释的过程中发生的错误,例如,读取一个未绑定的变量的操作,通过修改查找操作来
让它返回一个可区别的条件代码,来捕捉到这个错误,这个代码不能是任何的用户变量可能使用的值。解释器能测试这个条件代码,然后所做的是有必要去执行报错的操作。找到在解释器中所有的这样的地方,这样的修改是必要的,修正它们。这是很大量的工作。
 
b.  更糟糕的是处理应用原生的程序时报出的错误的问题,例如除以0的错误或者是抽取一个符号的头部的错误。在一个专业人的写出的高质量的系统中,任何一个原生的程序为了安全做检查
成为了原生的程序的一部分。例如,对取头部的操作的每一次调用首先检查它的实际参数是不是一个数对。如果实际参数不是一个数对,程序应该返回一个可区别的条件代码给解释器,由解释器来报错。在我们的寄存器机器的模拟器中,我们能为这做了安排,通过让每个原生的程序检查它的应用性和返回一个合适的可区别的为错误而定的条件代码。然后在解释器中primitive-code
的代码能被检查条件代码,并且如果有必要的话,去报错。但是构建这个结构和让它工作。
这是一个主要的工程。

猜你喜欢

转载自blog.csdn.net/gggwfn1982/article/details/83504379
今日推荐