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.

  • lispm@alien.topB
    link
    fedilink
    English
    arrow-up
    1
    ·
    11 months ago

    If you compile code, then the compiler expands macro forms at compile-time. If you want to generate a variable number of let bindings, then the number needs to be known at compile-time.

    start and end thus can’t be variables at run-time.

    (let ((start 1))
      (with-free-ports start 3 port-1))
    

    If we compile above form, then at compile-time the value of start generally is unknown.

    • xhash101@alien.topOPB
      link
      fedilink
      English
      arrow-up
      1
      ·
      11 months ago

      Thank you for your reply.

      The compiler generally will not execute the LET and then compile the WITH-FREE-PORTS form with the new binding

      Here is an example when the compiler does exactly that:

      (defmacro with-free-ports (start end &body body)
        `(list ,start ,end ,@body))
      
      (let ((start 1)
            (end 3))      
        (with-free-ports start end NIL))
      

      So, why does it work?

      • lispm@alien.topB
        link
        fedilink
        English
        arrow-up
        1
        ·
        11 months ago

        No, the compiler does not do that. If you look into your macro here, it just puts the args start and end back in. The generated code then is executed. Remember: START and END are bound to the source forms, not computed values.

        In the original macro you had the form (loop for port from start to end ...), which you tried to compute at macro-expansion time. But the values of START and END are not necessary numbers, but source forms, like variable names.