如何在Lisp编译器中编译宏?

apg*_*apg 21 lisp compiler-construction macros common-lisp

在Lisp解释器中,可以很容易地创建一个eval可以扩展宏的分支,并且在扩展它的过程中,调用函数来构建扩展表达式.我在使用低级宏之前已经完成了这个,很容易让人感到满意.

但是,在编译器中没有任何函数可以调用来构建扩展代码:在以下示例中可以非常简单地看到该问题:

(defmacro cube (n)
    (let ((x (gensym)))
      `(let ((,x ,n))
          (* ,x ,x ,x))))
Run Code Online (Sandbox Code Playgroud)

当解析器扩展宏时,它会调用gensym并执行您期望的操作.当由编译器扩展,你会生成代码的let结合x,以(gensym)但gensymmed符号,只需要编译器做正确的事.因为gensym在编译宏之前实际上没有调用它,所以它不是很有用.

当宏建立一个列表用作扩展使用map或时,这对我来说更加奇怪filter.

那么这是如何工作的呢?当然,编译后的代码不会被编译,(eval *macro-code*)因为它的效率非常低.有一个写得很好的Lisp编译器吗?

Rai*_*wig 22

各种Lisp方言的工作方式有很大不同.对于Common Lisp,它在ANSI Common Lisp标准中是标准化的,并且各种Common Lisp实现主要区别于它们是使用编译器,解释器还是两者.

以下假定Common Lisp.

EVAL不是翻译.EVAL可以用编译器实现.一些Common Lisp实现甚至没有解释器.然后EVAL调用编译器来编译代码,然后调用编译后的代码.这些实施方式使用一个增量编译器,其也可以编译等简单的表达式2,(+ 2 3),(gensym),等.

宏扩展是通过函数MACROEXPANDMACROEXPAND-1.

Common Lisp中的一个宏是一个需要某些表单并返回另一个表单的函数.DEFMACRO将此功能注册为宏.

你的宏

(defmacro cube (n)
  (let ((x (gensym)))
    `(let ((,x ,n))
        (* ,x ,x ,x))))
Run Code Online (Sandbox Code Playgroud)

只是一个Lisp函数,它被注册为一个宏.

效果类似于:

(defun cube-internal (form environment)
  (destructuring-bind (name n) form   ; the name would be CUBE
    (let ((x (gensym)))
      `(let ((,x ,n))
         (* ,x ,x ,x)))))

(setf (macro-function 'my-cube) #'cube-internal)
Run Code Online (Sandbox Code Playgroud)

在真正的CL实现中,DEFMACRO扩展方式不同,并且不使用类似的名称CUBE-INTERNAL.但从概念上讲,它定义了一个宏函数并对其进行了注册.

当Lisp编译器看到宏定义时,它通常编译宏函数并将其存储在当前所谓的环境中.如果环境是运行时环境,则会在运行时记住它.如果在编译文件时环境是编译器环境,则在编译文件后会忘记宏.需要加载编译的文件,以便Lisp知道宏.

因此,定义宏并对其进行编译会产生副作用.编译器会记住编译的宏并存储其代码.

当编译器现在看到一些使用宏的代码时,(cube 10)编译器只调用存储在名称下的当前环境中的CUBE宏函数,调用此宏函数10作为参数,然后编译生成的表单.如上所述,它不是直接完成的,而是通过MACROEXPAND函数完成的.

这是宏定义:

CL-USER 5 > (defmacro cube (n)
              (let ((x (gensym)))
                `(let ((,x ,n))
                   (* ,x ,x ,x))))
CUBE
Run Code Online (Sandbox Code Playgroud)

我们编译宏:

CL-USER 6 > (compile 'cube)
CUBE
NIL
NIL
Run Code Online (Sandbox Code Playgroud)

MACRO-FUNCTION返回宏的函数.我们可以像任何其他函数一样调用它FUNCALL.它需要两个参数:整个形式(cube 10)和环境(这里NIL).

CL-USER 7 > (funcall (macro-function 'cube) '(cube 10) nil)
(LET ((#:G2251 10)) (* #:G2251 #:G2251 #:G2251))
Run Code Online (Sandbox Code Playgroud)

也可以采用一个函数(它接受两个参数:一个表单和一个环境)并使用SETF作为宏函数存储它.

摘要

当Common Lisp编译器运行时,它只知道宏函数并在必要时调用它们以通过内置宏扩展器扩展代码.宏函数本身就是Lisp代码.当Lisp编译器看到宏定义时,它编译宏函数,将其存储在当前环境中并使用它来扩展宏的后续使用.

注意:这使得在Common Lisp中必须在编译器使用之前定义宏.


Eli*_*lay 6

有很多方法可以解决这个问题.一个极端是被称为"FEXPER"的东西,它们是宏观的东西,基本上在每次评估时都会重新扩展.它们在过去的某个时刻引起了很多噪音,但几乎完全消失了.(有一些人仍然做类似的事情,newlisp可能是最受欢迎的例子.)

所以FEXPERs倾向于支持宏,这些宏在某种程度上更"表现良好".你基本上做了一次宏扩展,并编译生成的代码.像往常一样,这里有一些策略,可能导致不同的结果.例如,"展开一次"未指定何时展开.这可以在读取代码时发生,或者(通常)在编译时发生,或者甚至在第一次运行时发生.

这里的另一个问题 - 这基本上就是你的立场 - 是在你评估宏代码的环境中.在大多数Lisps中,一切都发生在同一个快乐的全球环境中.宏可以自由访问函数,这可能会导致一些微妙的问题.这样做的一个结果是,许多商业Common Lisp实现为您提供了一个开发环境,您可以在其中完成大部分工作并进行编译 - 这使得两个级别上的相同环境都可用.(实际上,由于宏可以使用宏,因此这里有任意数量的级别.)要部署应用程序,您将获得一个没有编译器(即compile函数)的受限环境,因为如果部署使用它的代码,您的代码本质上是一个CL编译器.因此,我们的想法是在完整实现上编译代码,并扩展所有宏,这意味着编译后的代码不再使用宏.

但当然,这可能导致我谈到的那些微妙的问题.例如,某些副作用可能导致加载顺序混乱,您需要按特定顺序加载代码.更糟糕的是,你可能陷入一个陷阱,代码为你运行一种方式,另一种方式是编译 - 因为已编译的代码已经预先扩展了所有宏(以及它们所做的调用).这些有一些hackish解决方案,比如eval-when指定评估某些代码的某些条件.还有一些用于CL的软件包系统,您可以在其中指定加载顺序(如asdf).尽管如此,那里还没有真正强大的解决方案,你仍然可以陷入这些陷阱(例如参见这个扩展的咆哮).

当然还有其他选择.最值得注意的是,Racket使用其模块系统.模块可以多次"实例化",并且状态对于每个实例都是唯一的.现在,当在宏和运行时使用某个模块时,这个模块的两个实例是不同的,这意味着编译总是可靠的,并且没有上述令人头疼的问题.在Scheme世界中,这被称为"单独阶段",其中每个阶段(运行时,编译时和更高级别与宏使用宏)具有单独的模块实例.有关这方面的详细介绍和详尽解释,请阅读Matthew FlattComposable和Compilable宏.您还可以查看Racket文档,例如" 编译和运行时阶段"部分.