【问题标题】:Lisp unit tests for macros conventions and best practices用于宏约定和最佳实践的 Lisp 单元测试
【发布时间】:2016-04-06 21:06:28
【问题描述】:

我发现很难对宏扩展进行推理,并且想知道测试它们的最佳实践是什么。

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

(defmacro incf-twice (n)
  `(progn
     (incf ,n)
     (incf ,n)))

例如

(macroexpand-1 '(incf-twice n))

评估为

(PROGN (INCF N) (INCF N))

把它变成宏的测试似乎很简单。

(equalp (macroexpand-1 '(incf-twice n))
  '(progn (incf n) (incf n)))

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

【问题讨论】:

  • 我会测试宏的最终效果,而不是中间扩展。很好的问题,我会期待答案。

标签: unit-testing macros lisp common-lisp


【解决方案1】:

通常测试宏不是 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)

#:|Store-Var-1233| 是一个符号,它是由宏新创建的uninterned。

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

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

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

【讨论】:

    【解决方案2】:

    我同意Rainer Joswig's answer;一般来说,这是一项非常难以解决的任务,因为宏可以做很多事情。但是,我要指出,在许多情况下,对宏进行单元测试的最简单方法是使宏尽可能少地执行。在许多情况下,宏的最简单实现只是围绕更简单函数的语法糖。例如,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)))
    

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

    另一方面,如果您正在查看一个更复杂的宏,例如 loop,它本身确实是一种编译器,您几乎可以肯定已经开始了在一些单独的功能中具有扩展逻辑。例如,您可能有

    (defmacro loop (&body body)
      (compile-loop body))
    

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

    【讨论】:

      【解决方案3】:

      我通常只测试功能,而不是扩展的形状。

      是的,有各种各样的上下文和环境可能会影响发生的事情,但是如果您依赖这些东西,那么为您的测试设置相同的设置应该没有问题。

      一些常见情况:

      • 绑定宏:测试变量是否按预期在内部绑定,并且任何隐藏的外部变量不受影响
      • unwind-protect wrappers:从内部引发非本地退出并检查清理工作是否正常
      • 定义/注册:测试您是否可以定义/注册您想要的内容并在之后使用它

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-09-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-09-08
        • 2016-02-20
        • 1970-01-01
        相关资源
        最近更新 更多