【问题标题】:Compiling Lisp code with read macros使用读取宏编译 Lisp 代码
【发布时间】:2012-07-18 21:43:33
【问题描述】:

在将 lisp 代码文件编译为字节码或原始程序集(或 fasl 文件)时,我在理解读取宏的内容时遇到了一些麻烦。或者也许我确实明白但不知道。我真的很困惑。

当您使用读取宏时,您不必提供可用的源代码吗?

如果您这样做,那么您必须执行构成读取宏功能的源代码。如果您不这样做,那么当您可以执行read-char 之类的操作时,它们如何工作?

为此,如果你想让 read 宏使用预先定义的变量,你必须在它之前执行 all 代码,所以这变成了运行时,会搞砸一切。

如果你不运行它之前的代码,那么上面定义的东西将不可用。

定义读取宏的函数或编译器宏呢?我会假设它们根本不起作用,除非你 requireload 一个文件或未编译的东西。但是如果编译了,就不能用了?

如果我的一些猜测是正确的,那么这意味着“哪些数据可用于宏”和“哪些宏可用于函数”之间存在很大差异,具体取决于您是否将整个文件编译为稍后运行或一次解释一行文件(即读取、编译和评估一个又一个表达式)。

简而言之,似乎要将一行编译成无需进一步宏处理或其他任何操作即可执行的形式,您必须阅读、编译和运行前面的行。

再次记住,这些问题适用于编译 lisp,而不是解释它在哪里可以运行每一行。

很抱歉我的漫无边际,但我是 lisp 的新手,想了解更多它是如何工作的。

【问题讨论】:

    标签: compiler-construction lisp common-lisp interpreter reader-macro


    【解决方案1】:

    这实际上是一个有趣的问题,也是很多 Lisp 初学者都在纠结的问题。造成这种情况的主要原因之一是一切都“按预期”运行,当您开始使用 Lisp 的更高级功能时,您才真正开始考虑这些事情。

    您的问题的简短回答是,是的,为了正确编译代码,必须执行之前的一些代码。注意一些这个词,这是关键。让我们举一个小例子。考虑一个包含以下内容的文件:

    (print 'a)
    
    (defmacro bar (x) `(print ,x))
    
    (bar 'b)
    

    如您所见,如果您在此文件上运行 COMPILE-FILE,则生成的 .fasl 文件将仅包含以下代码的编译版本:

    (print 'a)
    (print 'b)
    

    “但是”,您可能会问,“为什么在编译期间执行了DEFMACRO 表单,但没有执行PRINT 表单?”。答案在 Hyperspec 部分3.2.3 中进行了解释。它包含以下句子:

    通常,顶级表单出现在使用编译的文件中 compile-file 仅在生成的编译文件为 加载,而不是在编译文件时。然而,它通常是 文件中的某些表单需要在编译时评估的情况 时间,以便可以正确读取和编译文件的其余部分。

    有一个表单可以用来精确控制何时评估表单。为此,您使用EVAL-WHEN。事实上,这正是 Lisp 编译器自己实现DEFMACRO 的方式。您可以通过在 REPL 中键入以下内容来查看您的 Lisp 是如何实现它的:

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

    显然不同的 Lisp 实现会以不同的方式实现这一点,但关键重要的是它将定义包装在一个表单中:(eval-when (:compile-toplevel :load-toplevel :execute) ...)。这告诉编译器应该在编译文件时以及加载文件时评估表单。如果它不这样做,您将无法在定义的同一文件中使用该宏。如果仅在编译文件时评估表单,则加载后将无法在其他文件中使用宏。

    【讨论】:

    • 编译后的文件不仅包含两个打印语句,还包含宏定义。
    • 在编译时和运行时运行编译器宏实际上是我对编译器宏的期望。但是,这并没有(我看到的)地址读取宏。编译器宏是简单的函数(它们将真实对象作为参数,因此理论上它们可以将扩展延迟到运行时),但读取宏取决于文本。它们是如何工作的?
    • @SethCarnegie 编译是增量的。每个表单在读取时都会被处理(在此上下文中处理意味着编译或评估,或两者兼而有之)。这意味着第一个表单可以修改阅读器的行为,而这个新的行为会影响后续表单的阅读。
    • 非顶级宏有例外吗?
    【解决方案2】:

    文件的编译在 Common Lisp 中定义:CLHS Section 3.2.3 File Compilation

    编译时:要使用带有读取宏的表单,您必须使该读取宏实现对编译器可用。

    通常使用defsystem 工具处理此类依赖关系,其中描述了系统(类似于项目)的各种文件之间的依赖关系。为了编译某个文件,必须将另一个文件(最好是编译后的版本)加载到编译的 Lisp 中。

    现在,如果您想在同一个文件中定义 read 宏并使用其表示法创建表单,那么您再次需要确保编译器知道 read 宏及其实现。文件编译器有编译环境。默认不加载同一个文件的编译函数到这个环境中。

    为了让编译器知道它编译的文件中的某些代码,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]))
    

    上面的代码会触发错误,因为 read 宏在编译时不可用 - 除非它之前已经加载过。

    现在有两个简单的解决方案:

    • 将读取宏的实现放在一个单独的文件中,并确保在使用读取宏的任何文件之前编译和加载它。

    • 在需要在编译时生效的代码周围加上EVAL-WHEN

    例子:

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

    编译器会看到上面的内容,然后也会执行。现在您必须确保代码在编译时具有它调用的所有内容(所有需要的定义)。

    不用说:尽可能减少这种编译依赖是一种很好的风格。通常将所需的功能放在一个单独的文件中,并确保在编译使用它的文件之前将该文件编译并加载到编译 Lisp 中。

    【讨论】:

      【解决方案3】:

      宏(包括读取宏)只不过是函数,它们的处理方式与所有其他函数相同。一旦编译了函数或宏,您就不需要保留源代码。

      许多 Lisp 实现根本不会做任何解释。例如,默认情况下 SBCL 只会编译,即使 eval 也不会切换到解释模式。一个重要的细微差别是 Common Lisp 编译是增量式的(与单独编译相反,在许多 Scheme 实现和 C 和 Java 等语言中很常见),它允许您编译一个函数或宏并立即使用它,在同一“编译单元”。

      【讨论】:

      • 其实SBCL有解释模式,只是默认关闭:sbcl.org/manual/Interpreter.html
      • Common Lisp 编译是增量的,但文件的编译定义略有不同。
      • @Rainer 你能详细说明一下吗?我不熟悉它
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-06-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多