Lisp单元测试宏约定和最佳实践

Gre*_*bet 13 lisp macros unit-testing common-lisp

我发现很难推断宏观扩张,并且想知道测试它们的最佳实践是什么.

所以如果我有一个宏,我可以通过执行一个级别的宏扩展macroexpand-1.

(defmacro incf-twice (n)
  `(progn
     (incf ,n)
     (incf ,n)))
Run Code Online (Sandbox Code Playgroud)

例如

(macroexpand-1 '(incf-twice n))
Run Code Online (Sandbox Code Playgroud)

评估为

(PROGN (INCF N) (INCF N))
Run Code Online (Sandbox Code Playgroud)

将它变成对宏的测试似乎很简单.

(equalp (macroexpand-1 '(incf-twice n))
  '(progn (incf n) (incf n)))
Run Code Online (Sandbox Code Playgroud)

是否有用于组织宏测试的既定惯例?还有,是否有一个库来总结s表达式之间的差异?

Rai*_*wig 7

通常,测试宏不是Lisp和Common Lisp的强大部分之一.Common Lisp(和Lisp方言一般)使用过程宏.宏可以依赖于运行时上下文,编译时上下文,实现等.它们也可能有副作用(比如在编译时环境中注册事物,在开发环境中注册事物等等).

所以有人可能想测试一下:

  • 生成正确的代码
  • 生成的代码实际上做了正确的事情
  • 生成的代码实际上在代码上下文中有效
  • 在复杂的宏的情况下,宏参数实际上是正确解析的.想想loop,defstruct...宏.
  • 宏检测到错误形成的参数代码.再次,想想宏loopdefstruct.
  • 副作用

从上面的列表可以推断,在开发宏时最好最小化所有这些问题区域.但是:那里真的有非常复杂的宏.真可怕的.特别是那些习惯于实现新域特定语言的人.

使用equalp比较代码之类的东西仅适用于相对简单的宏.宏通常会引入新的,未加工的和唯一的符号.因此equalp将无法与那些人合作.

示例:(rotatef a b)看起来很简单,但扩展实际上很复杂:

CL-USER 28 > (pprint (macroexpand-1 '(rotatef a b)))

(PROGN
  (LET* ()
    (LET ((#:|Store-Var-1234| A))
      (LET* ()
        (LET ((#:|Store-Var-1233| B))
          (PROGN
            (SETQ A #:|Store-Var-1233|)
            (SETQ B #:|Store-Var-1234|))))))
  NIL)
Run Code Online (Sandbox Code Playgroud)

#:|Store-Var-1233| 是一个符号,它是未处理的并由宏新创建的.

另一个具有复杂扩展的简单宏形式将是(defstruct s b).

因此,需要一个s表达式模式匹配器来比较扩展.有一些可用,它们在这里很有用.需要在测试模式中确保生成的符号在需要时是相同的.

还有s-expression diff工具.例如diff-sexp.


Jos*_*lor 5

我同意Rainer Joswig 的回答;一般来说,这是一个非常难解决的任务,因为宏可以做很多事情。但是,我要指出的是,在许多情况下,对宏进行单元测试的最简单方法是让宏尽可能少地执行操作。在许多情况下,宏的最简单实现只是围绕更简单函数的语法糖。例如,在 Common Lisp 中有一种典型的with -...宏模式(例如with-open-file),其中宏简单地封装了一些样板代码:

(defun make-frob (frob-args)
  ;; do something and return the resulting frob
  (list 'frob frob-args))

(defun cleanup-frob (frob)
  (declare (ignore frob))
  ;; release the resources associated with the frob
  )

(defun call-with-frob (frob-args function)
  (let ((frob (apply 'make-frob frob-args)))
    (unwind-protect (funcall function frob)
      (cleanup-frob frob))))

(defmacro with-frob ((var &rest frob-args) &body body)
  `(call-with-frob
    (list ,@frob-args)
    (lambda (,var)
      ,@body)))
Run Code Online (Sandbox Code Playgroud)

这里的前两个函数make-frobcleanup-frob对于单元测试来说相对简单。该呼叫与-FROB是有点困难。这个想法是它应该处理创建 frob 并确保清理调用发生的样板代码。这有点难以检查,但如果样板仅依赖于一些定义良好的接口,那么您可能能够创建一个可以检测是否正确清理的 frob 模型。最后,with-frob宏非常简单,您可能可以按照您一直在考虑的方式对其进行测试,即检查其扩展。或者你可能会说它很简单,你不需要测试它。

另一方面,如果您正在查看一个更复杂的宏,例如loop,它本身就是一种编译器,您几乎可以肯定已经在一些单独的函数中拥有扩展逻辑。例如,你可能有

(defmacro loop (&body body)
  (compile-loop body))
Run Code Online (Sandbox Code Playgroud)

在这种情况下,您真的不需要测试loop,您需要测试compile-loop,然后您又回到了通常的单元测试领域。