unit-testing macros lisp common-lisp

unit testing - La unidad Lisp prueba las convenciones de macros y las mejores prácticas



unit-testing common-lisp (3)

Me resulta difícil razonar sobre la macro-expansión y me preguntaba cuáles eran las mejores prácticas para probarlas.

Entonces, si tengo una macro, puedo realizar un nivel de expansión de macros a través de macroexpand-1 .

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

por ejemplo

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

evalúa a

(PROGN (INCF N) (INCF N))

Parece lo suficientemente simple como para convertir esto en una prueba para la macro.

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

¿Existe una convención establecida para organizar pruebas para macros? Además, ¿hay una biblioteca para resumir las diferencias entre las expresiones s?


Por lo general, solo probaba la funcionalidad, no la forma de la expansión.

Sí, hay todo tipo de contextos y entornos que pueden influir en lo que sucede, pero si confía en tales cosas, no debería ser un problema configurarlos para su prueba.

Algunos casos comunes:

  • macros vinculantes: compruebe que las variables están vinculadas como se desea dentro y que las variables externas sombreadas no se ven afectadas
  • envolturas de protección contra deshechos: provoque una salida no local desde el interior y verifique que la limpieza esté funcionando
  • definición / registro: pruebe que puede definir / registrar lo que quiere y usarlo luego

Generalmente, las pruebas de macros no son una de las partes fuertes de Lisp y Common Lisp. Common Lisp (y dialectos Lisp en general) utiliza macros de procedimiento. Las macros pueden depender del contexto de tiempo de ejecución, el contexto de tiempo de compilación, la implementación y más. También pueden tener efectos secundarios (como registrar cosas en el entorno de tiempo de compilación, registrar cosas en el entorno de desarrollo y más).

Entonces uno podría querer probar:

  • que se genera el código correcto
  • que el código generado realmente hace lo correcto
  • que el código generado realmente funciona en contextos de código
  • que los macro argumentos se analizan correctamente en el caso de macros complejas. Piensa en loop , defstruct , ... macros.
  • que la macro detecta código de argumento formado incorrectamente. Nuevamente, piense en macros como loop y defstruct .
  • los efectos secundarios

De la lista anterior podemos inferir que es mejor minimizar todas estas áreas problemáticas al desarrollar una macro. PERO: en realidad hay macros realmente complejos. Realmente terroríficos. Especialmente aquellos que están acostumbrados a implementar nuevos lenguajes específicos de dominio.

Usar algo como equalp para comparar código funciona solo para macros relativamente simples. Las macros a menudo introducen símbolos nuevos, no identificados y únicos. Por equalp tanto, equalp no funcionará con esos.

Ejemplo: (rotatef ab) parece simple, pero la expansión es realmente complicada:

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| es un símbolo, que no se ha iniciado y que ha sido creado recientemente por la macro.

Otra forma de macro simple con una expansión compleja sería (defstruct sb) .

Por lo tanto, uno necesitaría un patrón de s-expresión para comparar las expansiones. Hay algunos disponibles y serían útiles aquí. Uno necesita asegurarse de que en los patrones de prueba que los símbolos generados sean idénticos, donde sea necesario.

También hay herramientas s-expression diff. Por ejemplo, diff-sexp .


Estoy de acuerdo con la respuesta de Rainer Joswig ; en general, esta es una tarea muy difícil de resolver porque las macros pueden hacer mucho. Sin embargo, quisiera señalar que, en muchos casos, la manera más fácil de probar sus macros es haciendo que las macros hagan lo menos posible. En muchos casos, la implementación más fácil de una macro es simplemente azúcar sintáctica en torno a una función más simple. Por ejemplo, hay un patrón típico de - ... macros en Common Lisp (por ejemplo, con-open-file ), donde la macro simplemente encapsula un código repetitivo:

(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)))

Las primeras dos funciones aquí, make-frob y clean-frob son relativamente sencillas para la prueba unitaria. El call-with-frob es un poco más difícil. La idea es que se supone que maneja el código repetitivo de crear el frob y garantizar que se realice la llamada de limpieza. Eso es un poco más difícil de verificar, pero si la repetición solo depende de algunas interfaces bien definidas, entonces probablemente será capaz de crear una imagen falsa que pueda detectar si se limpió correctamente. Por último, la macro de "no tener nada" es tan simple que probablemente puedas probarla de la manera que has estado considerando, es decir, verificar su expansión. O puede decir que es lo suficientemente simple como para no tener que probarlo.

Por otro lado, si está mirando una macro mucho más compleja, como loop , que en realidad es una especie de compilador, es casi seguro que ya tendrá la lógica de expansión en algunas funciones separadas. Por ejemplo, podrías tener

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

en cuyo caso, realmente no necesita probar el bucle , debe probar el bucle de compilación , y luego volverá al ámbito de las pruebas unitarias habituales.