Search code examples
macroscommon-lisplisp-macros

Common Lisp: How to use a macro within a macro?


I'm writing an application (a simple game) using cl-sdl2. cl-sdl2 includes a macro called WITH-EVENT-LOOP that can be used to start the SDL application with some event handlers.

Currently I use the macro like so:

(sdl2:with-event-loop (:method :poll)
  (:idle () (on-idle :renderer renderer
                     :tick-func tick-func
                     :render-func render-func))
  (:quit () t)

  ;; Input handlers below
  (:keydown (:keysym keysym)
            (funcall input-func :keydown :keysym keysym))
  (:keyup (:keysym keysym)
          (funcall input-func :keyup :keysym keysym)))

This code listing is small because I omitted several more input handlers. There are at least 6 more handlers defined following this same pattern, for handling mouse and game controller input events, so my actual code is longer and highly repetitive.

I can write a macro that generates these boilerplate forms:

(defmacro forward-evt-to (target evt &rest evt-args)
  `(,evt ,evt-args
         (funcall ,target ,evt ,@evt-args)))

This produces a list that can be understood by WITH-EVENT-LOOP:

> (macroexpand-1 '(forward-evt-to input-func :keydown :keysym keysym))
(:KEYDOWN (:KEYSYM KEYSYM) (FUNCALL INPUT-FUNC :KEYDOWN :KEYSYM KEYSYM))
T

But when I try to use that macro, I get a compiler error. For example this fails:

(sdl2:with-event-loop (:method :poll)
  (:idle () (on-idle :renderer renderer
                     :tick-func tick-func
                     :render-func render-func))
  (:quit () t)

  (forward-evt-to input-func :keydown :keysym keysym)
  (forward-evt-to input-func :keyup :keysym keysym))

WITH-EVENT-LOOP tries to compile the code without expanding it and gives the error

; The value
;      INPUT-FUNC
;    is not of type
;      LIST

Is there any way I can use my FORWARD-EVT-TO macro from inside the WITH-EVENT-LOOP macro? I can't/don't want to modify WITH-EVENT-LOOP directly.


Solution

  • One way would be to write a new macro, which expands into your target form.

    Sketch of an example:

    CL-USER 8 > (defmacro with-event-loop-1 (foo &body clauses &environment env)
                  `(with-event-loop ,foo
                                    ,(loop for clause in clauses
                                           when (keywordp (first clause))
                                             collect clause
                                           else
                                             collect (macroexpand-1 clause env))))
    WITH-EVENT-LOOP-1
    
    CL-USER 9 > (pprint
                 (macroexpand
                  '(with-event-loop-1 (:method :poll)
                     (:idle () (on-idle :renderer renderer
                                        :tick-func tick-func
                                        :render-func render-func))
                     (:quit () t)
                     
                     (forward-evt-to input-func :keydown :keysym keysym)
                     (forward-evt-to input-func :keyup :keysym keysym))))
    
    (WITH-EVENT-LOOP (:METHOD :POLL)
      ((:IDLE NIL (ON-IDLE :RENDERER RENDERER
                           :TICK-FUNC TICK-FUNC
                           :RENDER-FUNC RENDER-FUNC))
      (:QUIT NIL T)
      (:KEYDOWN (:KEYSYM KEYSYM) (FUNCALL INPUT-FUNC :KEYDOWN :KEYSYM KEYSYM))
      (:KEYUP (:KEYSYM KEYSYM) (FUNCALL INPUT-FUNC :KEYUP :KEYSYM KEYSYM))))