執拗な再帰マクロ展開

処理系に依るかも知れないが、macroexpandは全てのマクロを展開してくれない。

(defmacro macro (&body body)
  `(progn (foo) ,@body (bar)))

(defmacro macro2 (&body body)
  `(prog1 ,@body (buzz)))

(defmacro macro3 ()
  ''fizz)

(macroexpand '(macro (macro2 (macro3))))
|
(progn (foo) (macro2 (macro3)) (bar))

次のように、全部展開してくれた方がうれしい。

(exhausted-recursive-macroexpand '(macro (macro2 (macro3))))
|
(progn (foo) (let ((#1=#:result 'fizz)) (progn (buzz)) #1#) (bar))

;整形(これはまた別の問題)
(progn
  (foo)
  (let ((#1=#:result 'fizz))
    (progn (buzz))
    #1#)
  (bar))

上の定義と形が異なる気がするが、ご存じのようにprog1もマクロなので、それも展開された形になっている。というわけで、そんな関数を作ってみた。

(defun erm (form)
  (cond
   ((atom form) form)
   ((eq (car form) 'lambda) form)
   (t (multiple-value-bind (form trail)
          (macroexpand-1 (rerm form))
        (if trail (erm form) form)))))

(defun rerm (form)
  (if (atom form) form
    (append (list (erm (car form))) (rerm (cdr form)))))

;補題
(macroexpand-1 '(lambda () 'hoge))
|
#'(lambda nil 'hoge)
t

簡単に動作説明をしてみる。macroexpand-1は、展開できたら2番目の戻り値にtを返すので、これ以上展開できないところまで繰り返す。その動作をS式の内側から実行していく。一回展開したら、また中身を見直さないといけないのでなかなか面倒である。


また、(lambda 〜)の形のときだけは展開しないようにしている。これは、(lambda)が(function (lambda))に展開されるので、繰り返し展開すると無限ループになってしまうからである。


その他にも、無限ループに陥る可能性がある。defmacro中に同じ名前を使うのは論外(展開が終わらない)だが、&wholeを用いたマクロはその内部に自分と同じものが現れる。本当は、これを回避しなくてはならない。macro-functionで&wholeを用いているか確認したり、一度展開したかどうか記憶したりする必要があるが、割と面倒なのでここでは手抜きをした*1

*1:&wholeの挙動をよく知らないので、それを調べるのが面倒だった