1. 程式人生 > >5.4.4 執行直譯器

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
的程式碼能被檢查條件程式碼,並且如果有必要的話,去報錯。但是構建這個結構和讓它工作。
這是一個主要的工程。