macroscommon-lispucb-logo

Can the inner workings of a Common Lisp macro be made evident with more than macroexpand(-1)?


I have this macro:

(defmacro if (test-form &body body)
  (let ((test (gensym)))
    `(let ((,test (cl:if (booleanp ,test-form)
                         (cl-truth ,test-form)
                         ,test-form)))
       (cl:if ,test
              ,@body))))

Using it results in this:

LO> (if (wordp (wordify ""))
        (print 1)
        (print 0))
NIL

(You can see, I work in a package lo that makes:use of common-lisp but cl:if is in the :shadow. -- The function wordify uses make-word to create an instance of the coresponding defstruct. The predicate booleanp checks, whether a predicate's output is => TRUE, t resp. => FALSE, nil. cl-truth returns the second value then.)

macroexpand-1 is not useful here. It returns => NIL, NIL

If I try to consider the possible output, I appreciate destructuring-bind as a helpful tool:

(destructuring-bind (test-form &body body)
    '((wordp (wordify "")) (print 1) (print 0))
  (let ((test (gensym)))
    `(let ((,test (cl:if (booleanp ,test-form)
                         (cl-truth ,test-form)
                         ,test-form)))
       (cl:if ,test
              ,@body))))

The output is:

(LET ((#:G698
       (COMMON-LISP:IF (BOOLEANP (WORDP (WORDIFY "")))
                       (CL-TRUTH
                         (WORDP (WORDIFY "")))
                       (WORDP (WORDIFY "")))))
  (COMMON-LISP:IF #:G698
                  (PRINT
                    1)
                  (PRINT
                    0)))

And the result is the desired:

1
; No value

(It's a custom print that uses princ with no return value.)

Can you tell me, why the macro behaves different?

Thank you so very much.

EDIT:

Just to add a maybe more appropriate version of the macro, but without the strange behaviour solved.

(defmacro if-c (test-form instructionlist1 &optional instructionlist2)
  (let ((test (gensym)))
    `(let (;; The general Logo testform will return => TRUE, T / FALSE, NIL
           (,test (destructuring-bind (logo &optional cl)
                      (multiple-value-list ,test-form)
                    (cl:if (booleanp logo)
                           cl
                           (error "if doesn't like ~a as input"
                                  logo)))))
       (cl:if ,test
              ,instructionlist1
              ,instructionlist2))))    

EDIT No. 2:

I forgot to mention: Yes, the strange behaviour is solved. Trivially, the macro above of package LC was not expanded in package LO but the one below instead:

(defmacro if (test-form &body body)
  ())

It was a left-over in LO where I was going to define it, before I decided to move it to LC.


Solution

  • MACROEXPAND-1 is actually sufficient to investigate macroexpansion in Common Lisp.

    There is a possibility to use *MACROEXPAND-HOOK* to let things happen during macroexpansion.

    I was thinking of TRACE for macros.

    It is possible to print out the macroexpansions happening during a macro call. (And also count the depth of the expansions.)

    (defun tracing-macroexpand-hook (expander form &optional env)
        (format t "[expanding] ~S~%" form)
        (let ((result (funcall expander form env)))
          (format t "=> ~S~%" result)
          result))
    
    ;; for convenience, let's define also a `with-macroexpansion-trace`
    (defmacro with-macroexpansion-trace (&body body)
      `(let ((*macroexpand-hook* #'tracing-macroexpand-hook))
         ,@body))
    

    Now, you can define some macros which are called in layers:

    (defmacro m1 (x)
      `(m2 (+ ,x 1)))
    
    (defmacro m2 (x)
      `(m3 (* ,x 2)))
    
    (defmacro m3 (x)
      `(+ ,x 42))
    

    And we can go:

    (with-macroexpansion-trace
      (macroexpand '(m1 5)))
    

    Resulting in the output:

    [expanding] (M1 5)
    => (M2 (+ 5 1))
    [expanding] (M2 (+ 5 1))
    => (M3 (* (+ 5 1) 2))
    [expanding] (M3 (* (+ 5 1) 2))
    => (+ (* (+ 5 1) 2) 42)
    (+ (* (+ 5 1) 2) 42) ;
    T
    

    You can now also define a with-macroexpansion-result which applies with-macroexpansion-trace but at the end presents you also the evaluated result after the macroexpansion - and counts the total macro calls and returns the result:

    (defmacro with-macroexpansion-result (form &key (evaluate nil))
      `(let ((*macroexpand-hook* #'tracing-macroexpand-hook))
         (let ((expanded (macroexpand ',form)))
           ,(if evaluate
                `(let ((result (eval expanded)))
                   (format t "~&[result] ~S~%" result)
                   result)
                'expanded))))
    

    Let's try it:

    (with-macroexpansion-result (m1 5) :evaluate t)
    
    ;; printing out:
    [expanding] (M1 5)
    => (M2 (+ 5 1))
    [expanding] (M2 (+ 5 1))
    => (M3 (* (+ 5 1) 2))
    [expanding] (M3 (* (+ 5 1) 2))
    => (+ (* (+ 5 1) 2) 42)
    [result] 54
    54
    
    
    (defmacro with-macroexpansion-result (form &key (evaluate nil))
      `(let ((macro-call-count 0))
         (flet ((hook (expander f &optional env)
                  (incf macro-call-count)
                  (format t "~&[expanding] ~S~%" f)
                  (let ((result (funcall expander f env)))
                    (format t "=> ~S~%" result)
                    result)))
           (let ((*macroexpand-hook* #'hook))
             (let ((expanded (macroexpand ',form)))
               (format t "~&[macro-calls-total] ~D~%" macro-call-count)
               ,(if evaluate
                    `(let ((result (eval expanded)))
                       (format t "~&[result] ~S~%" result)
                       result)
                    'expanded))))))
    
    (with-macroexpansion-result (m1 5) :evaluate t)
    [expanding] (M1 5)
    => (M2 (+ 5 1))
    [expanding] (M2 (+ 5 1))
    => (M3 (* (+ 5 1) 2))
    [expanding] (M3 (* (+ 5 1) 2))
    => (+ (* (+ 5 1) 2) 42)
    [macro-calls-total] 3
    [result] 54
    54
    
    

    I guess that is the tool you wanted to have, isn't it? Some kind of macro-trace.