Search code examples
loopslispcommon-lisplisp-macros

Using loop inside defmacro


I'm learning (common) Lisp, and as exercise, I want to implement 'xond', a cond macro, that transform this silly example:

(xond (= n 1) (setq x 2) (= n 2) (setq x 1))

into a if-else chain:

(if (= n 1) (setq x 2) (if (= n 2) (setq x 1)))

Currently, I have this macro:

(defmacro xond (&rest x) (if x (list 'progn (list 'if (pop x) (pop x)))))

that just expand the first two items in x:

(macroexpand '(xond (= x 1) (setq y 2)))

produce

(PROGN (IF (= X 1) (SETQ Y 2))) ;

Now I want to process all items in x, so I add a loop to produce a if-serie (a step toward if-else-version):

(defmacro xond (&rest x)
  (loop (if x
           (list 'progn (list 'if (pop x) (pop x)))
           (return t))))

but then macro seems to stop working:

(macroexpand '(xond (= x 1) (setq y 2)))
T ;

What I'm missing here?

Edition

verdammelt's answer put me in the right track, and coredump's made me change my approach to an iterative one.

Now I'll implement (xond test1 exp1 test2 exp2) as:

(block nil
   test1 (return exp1)
   test2 (return exp2)
)

which can be done by iteration.

I'm writing this for my minimal Lisp interpreter; I have only implemented the most basic functions.

This is what I wrote. I'm using la to accumulate the parts of the output.

(defmacro xond (&rest x) 
   (let ((la '())) 
      (loop 
         (if x (push (list 'if (pop x) (list 'return (pop x))) la) 
               (progn (push 'nil la)
                      (push 'block la)
                      (return la)
                )))))

with

(macroexpand '(xond (= x 1) (setq y 2) (= X 2) (setq y 1)))

result:

(BLOCK NIL 
    (IF (= X 2) (RETURN (SETQ Y 1)))
    (IF (= X 1) (RETURN (SETQ Y 2)))
) ;

Second edition

Add a label to block and change return to return-from, to avoid conflict with other return inside arguments. Also changed push for append to generate code in the same orden as the parameters.

(defmacro xond (&rest x) 
    (let ((label (gensym)) (la '()) (condition nil) (expresion nil)) 
        (setq la (append la (list 'block label)))
        (loop 
            (if x   
                (setq la (append la (list 
                   (list 'if (pop x) (list 'return-from label (pop x))))))
                 (return la)))))

So

(macroexpand '(xond (= x 1) (setq y 2) (= X 2) (setq y 1)))

now gives

(BLOCK #:G3187 (IF (= X 1) (RETURN-FROM #:G3187 (SETQ Y 2))) (IF (= X 2) (RETURN-FROM #:G3187 (SETQ Y 1))))

Solution

  • Some remarks

    • You do not need a progn when you only expand into a single if
    • The use of pop might be confusing for the reader (and the programmer too) since it mutates a place, maybe you want to start with a less imperative approach

    Also, in that case I don't think a loop approach is helpful, because you need to nest the expressions that come after in the body inside a previously built form, and even though it can be done, it is a bit more complex to do that simply a recursive function or a "recursive" macro.

    Here I explain both approach, starting with "recursive" macro (the quote here is because the macro does not call itself, but expands as call to itself).

    Macro expansion fixpoint

    If I had to implement xond, I would write a macro that expands into other calls to xond, until macroexpansion reaches a base case where there are no more xond:

    (defmacro xond (&rest body)
      (if (rest body)
          (destructuring-bind (test if-action . rest) body
            `(if ,test ,if-action (xond ,@rest)))
          (first body)))
    

    For example, this expression:

    (xond (= n 1) (setq x 2) (= n 2) (setq x 1))
    

    First macroexpands into:

    (if (= n 1)
        (setq x 2)
        (xond (= n 2) (setq x 1)))
    

    And eventually reaches a fixpoint with:

    (if (= n 1)
        (setq x 2)
        (if (= n 2)
            (setq x 1)
            nil))
    

    Be careful, you cannot directly use xond inside the definition of xond, what happens is that the macro expands as a call to xond, which Lisp then expands again. If you are not careful, you may end up with an infinite macroexpansion, that's why you need a base case where the macro does not expand into xond.

    Macro calling a recursive function

    Alternatively, you can call a recursive function inside your macro, and expand all the inner forms at once.

    With LABELS, you bind xond-expand to a recursive function. Here this is an actual recursive approach:

    (labels ((xond-expand (body)
               (if body
                   (list 'if
                         (pop body)
                         (pop body)
                         (xond-expand body))
                   nil)))
      (xond-expand '((= n 1) (setq x 2) (= n 2) (setq x 1))))
    
     ; => (IF (= N 1)
     ;    (SETQ X 2)
     ;    (IF (= N 2)
     ;        (SETQ X 1)
     ;        NIL))