Lisp модульные тесты для соглашений и передовых методов макросов

Мне трудно рассуждать о макрорасширении и задавалось вопросом, какие лучшие методы были для их тестирования.

Итак, если у меня есть макрос, я могу выполнить один уровень расширения макроса через 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-выражениями?

Ответ 1

Обычно макросы тестирования не являются одной из сильных частей Lisp и Common Lisp. Общие диалоги Lisp (и Lisp в целом) используют процедурные макросы. Макросы могут зависеть от контекста среды выполнения, контекста времени компиляции, реализации и т.д. Они также могут иметь побочные эффекты (например, регистрировать вещи в среде компиляции, регистрировать вещи в среде разработки и т.д.).

Поэтому можно протестировать:

  • что правильный код генерируется
  • что сгенерированный код действительно делает правильную вещь
  • что сгенерированный код действительно работает в контекстах кода.
  • что аргументы макросов на самом деле правильно разбираются в случае сложных макросов. Подумайте loop, defstruct,... макросы.
  • что макрос обнаруживает неправильно сформированный код аргумента. Опять же, подумайте о макросах вроде loop и defstruct.
  • побочные эффекты

Из приведенного выше списка можно сделать вывод, что лучше всего минимизировать все эти проблемные области при разработке макроса. НО: там действительно очень сложные макросы. Действительно страшные. Особенно те, кто используется для внедрения новых доменных языков.

Использование чего-то типа 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| - это символ, который является неинтерминированным и вновь создан макросом.

Еще одна простая макроформа с сложным расширением будет (defstruct s b).

Таким образом, для сравнения разложений понадобился бы паттерн s-выражения для сравнения. Есть несколько доступных, и они будут полезны здесь. В тестовых шаблонах необходимо убедиться, что сгенерированные символы идентичны, если необходимо.

Существуют также инструменты сравнения s-expression. Например diff-sexp.

Ответ 2

Я согласен с ответом Райнера Йосвига; в общем, это очень трудная задача, потому что макросы могут делать много. Тем не менее, я хотел бы указать, что во многих случаях самый простой способ unit test ваших макросов - сделать макросы как можно меньше. Во многих случаях самая простая реализация макроса - это просто синтаксический сахар вокруг более простой функции. Например, существует типичный шаблон макросов с & hellip; в Common Lisp (например, с открытым файлом), где макрос просто инкапсулирует какой-либо шаблонный код:

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

Первые две функции здесь make-frob и cleanup-frob относительно просты к unit test. call-with-frob немного сложнее. Идея состоит в том, что она должна обрабатывать шаблонный код создания frob и гарантировать, что вызов очистки произойдет. Это немного сложнее проверить, но если шаблонный шаблон зависит только от некоторых четко определенных интерфейсов, то вы, вероятно, сможете создать макет frob, который сможет определить, правильно ли он очищен. Наконец, макрос с-frob настолько прост, что вы, вероятно, можете проверить его так, как вы рассматривали, то есть проверить его расширение. Или вы можете сказать, что это достаточно просто, что вам не нужно его проверять.

С другой стороны, если вы смотрите на гораздо более сложный макрос, например loop, который по сути является своего рода компилятором, вы почти наверняка уже собираетесь иметь логику расширения в некоторых отдельных функциях. Например, у вас может быть

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

в этом случае вам действительно не нужно тестировать цикл, вам нужно протестировать compile-loop, а затем вы вернетесь в область своего обычного модульное тестирование.

Ответ 3

Обычно я просто тестировал функциональность, а не форму расширения.

Да, есть всевозможные контексты и окружающая среда, которые могут повлиять на то, что происходит, но если вы полагаетесь на такие вещи, не должно быть проблем, чтобы установить их одинаково для вашего теста.

Некоторые распространенные случаи:

  • связывание макросов: проверьте, что переменные привязаны по назначению внутри и что любые скрытые внешние переменные не затронуты
  • Обматывающие оболочки для разматывания: вызывают нелокальный выход изнутри и проверяют, что очистка работает.
  • определение/регистрация: тест, который вы можете определить/зарегистрировать, что хотите, и использовать его впоследствии