Hello guys,
I have a question regarding the nested backquotes in macros. I wrote a macro, which creates lexical bindings for “port:ip” values:
(defun mkstr (&rest args)
(with-output-to-string (s)
(dolist (a args) (princ a s))))
(defun mksymb (&rest args)
(values (intern (string-upcase (apply #'mkstr args)))))
;; my macro
(defmacro with-free-ports (start end &body body)
(let ((range (loop for port from start to end collect (format NIL "127.0.0.1:~a" port)))
(n 0))
`(let ,(mapcar #'(lambda (p) `(,(mksymb "PORT-" (incf n)) ,p)) range)
(progn ,@body))))
One sets a range of ports on localhost and these ports are bound to symbols port-1, port-2, etc…
(with-free-ports 1 3 port-1) ;; => "127.0.0.1:1"
This works fine if the start
or end
parameters are given as values. But if they are variables. which must be evaluated, this macro doesn’t work:
(let ((start 1))
(with-free-ports start 3 port-1)) ;; error
In order to fix it, I made the let
- bindings a part of the macro-expansion:
(defmacro with-free-ports (start end &body body)
`(let ((range (loop for port from ,start to ,end collect (format NIL "127.0.0.1:~a" port)))
(n 0))
`(let ,(mapcar #'(lambda (p) `(,(mksymb "PORT-" (incf n)) ,p)) range)
(progn ,@body))))
but get a compilation warning that the body
is never used. I assume this is because of the inner backquote.
To evaluate ,@body
inside the inner backquote, I use one more comma, and the macro compiles without warnings:
(defmacro with-free-ports (start end &body body)
`(let ((range (loop for port from ,start to ,end collect (format NIL "127.0.0.1:~a" port)))
(n 0))
`(let ,(mapcar #'(lambda (p) `(,(mksymb "PORT-" (incf n)) ,p)) range)
(progn ,,@body)))) ;; one more comma here
But it doesn’t work:
(let ((start 1))
(with-free-ports start 3 port-1)) ;; error: port-1 is unbound
because with this ,,@body
I evaluate port-1: (progn ,port-1)
and this triggers the error.
I would appreciate if smbd can help me a bit and say what I am doing wrong.
Thank you.
Macros are expanded recursively, so if the result of expanding a macro is itself a macro then the evaluator or compiler will immediately expand it again. This is why we have
MACROEXPAND-1
which expands it once, andMACROEXPAND
which expands a form until it is no longer a macro form. I would suggest youMACROEXPAND-1
my answer, then repeat withMACROEXPAND
to see the non-macro code that actually ends up being compiled/evaluated.You may find Section 3 of the HyperSpec helpful. The spec goes into a fair amount of detail about the semantics of both evaluation and compilation which should help.