7

我在編譯Lisp代碼文件到字節碼或原始程序集(或者fasl文件)時讀取宏變得有點麻煩。或者,也許我確實瞭解它,但不知道。我真的很困惑。使用讀取宏編譯Lisp代碼

當您使用讀取宏時,您是否必須有可用的源代碼?

如果這樣做,那麼您必須執行構成讀取宏功能的源代碼。如果你不這樣做,那麼當你可以做像read-char這樣的東西時,它們如何工作?要做到這一點,如果你想讓讀取的宏使用上述變量,你必須執行所有的之前的代碼,所以這變成了運行時間,它弄糟了所有的東西。

如果您之前沒有運行代碼,那麼上面定義的代碼將不可用。

定義讀取宏的函數或編譯器宏如何?我會假設他們根本不會工作,除非你的requireload文件或者沒有被編譯的東西。但是如果它們被編譯,那麼它們將無法使用它們?

如果我的一些猜測是正確的,那麼這意味着「哪些數據可用於宏」和「哪些宏可用於函數」有很大差異,具體取決於您是否將整個文件編譯爲稍後運行或一次解釋文件一行(即讀取,編譯和評估一個表達式)。

簡而言之,它似乎是將一行編譯爲一個可以在沒有進一步宏處理的情況下執行的表單,或者其他任何行,您必須讀取,編譯和運行前面的行。

再次,這些問題適用於編譯口齒不清,無法解釋它,你可以,因爲它涉及運行在每一行記住。

對不起,我的散漫,但我是新來的口齒不清,想知道更多如何作品。

回答

1

宏(包括讀取宏)只不過是函數,它們的處理方式與所有其他函數一樣。一旦函數或宏被編譯完成,您就不需要保留源代碼。

許多Lisp實現根本不會做任何解釋。例如,默認情況下,SBCL只會編譯,即使對於eval,也不會切換到解釋模式。一個重要的細微差別是Common Lisp編譯是增量式的(與許多Scheme實現和C和Java等語言中常見的單獨編譯相反),它允許您編譯函數或宏並立即使用它,在相同的「編纂單位「。

+0

實際上,SBCL具有解釋模式,它只是默認關閉:http://www.sbcl.org/manual/Interpreter.html – 2012-07-19 05:48:26

+0

Common Lisp編譯是增量式的,但文件編譯的定義略有不同。 – 2012-07-19 05:55:39

+0

@Rainer能否詳細說明一下?我不熟悉它 – 2012-07-19 06:02:45

5

這實際上是一個有趣的問題,也是很多Lisp程序員開始苦苦掙扎的東西。這樣做的主要原因之一是,所有的工作都「按預期」工作,而當你開始使用Lisp的更高級功能時,你才真正開始考慮這些事情。

您的問題的簡短答案是,爲了使代碼正確編譯,必須先執行一些以前的代碼。注意一些字,這是關鍵。讓我們舉一個小例子。考慮一個文件,內容如下:

(print 'a) 

(defmacro bar (x) `(print ,x)) 

(bar 'b) 

正如你已經想通了,如果你對這個文件運行COMPILE-FILE,產生的.fasl文件將只包含以下代碼的編譯版本:

(print 'a) 
(print 'b) 

「但是」,你可能會問,「編譯期間爲什麼要執行DEFMACRO表單,但是PRINT表單不是?」。答案在Hyperspec部分3.2.3中解釋。它包含以下句子:

通常情況下,出現在與 編譯文件編譯的文件中的頂級形式,只有當編譯後的文件是 加載,而不是當文件被編譯評估。但是,通常需要在編譯 時評估文件中的某些表單,以便可以正確讀取和編譯文件的其餘部分。

有一種表單可以用來精確控制何時評估表單。您爲此使用EVAL-WHEN。實際上,這正是Lisp編譯器本身如何實現DEFMACRO。你可以看到你的Lisp如何通過鍵入從REPL以下實現它:

(macroexpand '(defmacro bar (x) `(print ,x))) 

顯然不同的Lisp實現將不同的方式實現這一點,但關鍵重要的是,它包裝的定義形式:(eval-when (:compile-toplevel :load-toplevel :execute) ...)。這告訴編譯器,應該在編譯文件時以及在文件加載時對錶單進行評估。如果它沒有這樣做,您將無法在定義的文件中使用該宏。如果僅在編譯文件時才評估表單,那麼在加載它之後,您將無法在其他文件中使用該宏。文件

+2

編譯的文件不僅包含兩個打印語句,它還包含宏定義。 – 2012-07-19 05:54:01

+1

在編譯時和運行時都運行編譯器宏實際上是關於我對編譯器宏的期望。但是,這並不(我看到)地址讀取宏。編譯器宏是簡單的函數(它們將實際對象作爲參數,因此理論上它們可以延遲擴展直到運行時),但讀取宏取決於文本。他們如何工作? – 2012-07-19 05:55:18

+0

@SethCarnegie編譯是增量式的。每個表格都是在讀取時處理的(在這種情況下處理意味着編譯或評估,或者兩者兼而有之)。這意味着第一種形式可以修改讀者的行爲,並且這種新行爲將隨後在閱讀時影響後續形式。 – 2012-07-20 01:42:09

4

編譯是Common Lisp中的定義:CLHS Section 3.2.3 File Compilation

雖然編譯:使用讀取宏使用的一種形式,你不得不做出這樣的閱讀提供給編譯器宏實現。

通常,這樣的依賴關係是通過defsystem工具來處理的,其中描述了系統各種文件(如項目)之間的依賴關係。爲了編譯某個文件,必須將另一個文件(最好是編譯後的版本)加載到編譯Lisp中。

現在,如果您想要定義讀取宏並在同一個文件中使用其表示法,那麼您需要確保編譯器知道讀取的宏及其實現。文件編譯器具有編譯環境。它不會將默認編譯的相同文件的函數加載到此環境中。

爲了讓編譯器知道文件中的某些代碼,它編譯Common Lisp提供了EVAL-WHEN

讓我們看一個讀宏示例:

(set-syntax-from-char #\] #\)) 

(defun reader-example (stream char) 
    (declare (ignore char)) 
    (let ((class (read stream t nil t)) 
     (args (read-delimited-list #\] stream t))) 
    (apply #'make-instance 
      class 
      args))) 

(set-macro-character #\[ 'reader-example) 

(defclass example() 
    ((name :initarg :name))) 

(defvar *examples* 
    (list [example :name e1] 
     [example :name e2] 
     [example :name e3])) 

如果加載上面的源代碼,一切都很好。但是如果我們使用文件編譯器,它不會在沒有首先加載的情況下編譯。例如文件編譯器通過調用具有路徑名的函數COMPILE-FILE來調用。

現在編譯文件:

(set-syntax-from-char #\] #\)) 

上面會不會在編譯時被執行。新的語法更改在編譯時不可用。

(defun reader-example (stream char) 
    (declare (ignore char)) 
    (let ((class (read stream t nil t)) 
     (args (read-delimited-list #\] stream t))) 
    (apply #'make-instance 
      class 
      args))) 

上述函數被編譯但未加載。它的實現在後面的步驟中不可用於編譯器。

(set-macro-character #\[ 'reader-example) 

再一次,上述表單不會被執行 - 只是它的代碼被生成。

(defclass example() 
    ((name :initarg :name))) 

編譯器記錄該類,但稍後不能創建它的實例。

(defvar *examples* 
    (list [example :name e1] 
     [example :name e2] 
     [example :name e3])) 

上面的代碼觸發錯誤,因爲讀取宏在編譯時不可用 - 除非它之前已經加載。

現在有兩個簡單的解決方案:

  • 把讀取宏的執行在一個單獨的文件,並確保它被編譯並使用讀取宏任何文件之前加載。

  • 把一個EVAL-WHEN在其周圍需要具有在編譯時效果的代碼:

實施例:

(EVAL-WHEN (:compile-toplevel :load-toplevel :execute) 
    (do-something-also-at-compile-time)) 

上面會被編譯器和可見也執行即可。現在,您必須確保代碼在編譯時具有所調用的所有內容(所有必需的定義)。不用說:儘可能減少這種編譯依賴性是一種很好的風格。通常將所需的功能放在單獨的文件中,並確保在編譯使用該文件的文件之前,將此文件編譯並加載到編譯Lisp中。