Search code examples
macroslispcommon-lisp

How can macro variable capture happen with a gensym symbol?


I'm learning common lisp. I have written a version of the once-only macro, which suffers from an unusual variable capture problem.

My macro is this:

(defmacro my-once-only (names &body body)
  (let ((syms (mapcar #'(lambda (x) (gensym))
                      names)))
    ``(let (,,@(mapcar #'(lambda (sym name) ``(,',sym ,,name))
                      syms names))
        ,(let (,@(mapcar #'(lambda (name sym) `(,name ',sym))
                      names syms))
           ,@body))))

The canonical version of only-once is this:

(defmacro once-only ((&rest names) &body body)
  (let ((gensyms (loop for n in names collect (gensym))))
    `(let (,@(loop for g in gensyms collect `(,g (gensym))))
      `(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
        ,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))
           ,@body)))))

The difference, as far as I can tell, is that the canonical version generates new symbols for every expansion of the macro using only-once. For example:

CL-USER> (macroexpand-1 '(once-only (foo) foo))
(LET ((#:G824 (GENSYM)))
  `(LET (,`(,#:G824 ,FOO))
     ,(LET ((FOO #:G824))
        FOO)))
T
CL-USER> (macroexpand-1 '(my-once-only (foo) foo))
`(LET (,`(,'#:G825 ,FOO))
   ,(LET ((FOO '#:G825))
      FOO))
T

The variable my macro uses to store the value of foo is the same for every expansion of this form, in this case it would be #:G825. This is akin to defining a macro like the following:

(defmacro identity-except-for-bar (foo)
  `(let ((bar 2))
     ,foo))

This macro captures bar, and this capture manifests when bar is passed to it, like so:

CL-USER> (let ((bar 1))
           (identity-except-for-bar bar))
2

However, I cannot think of any way to pass #:G825 to a macro that uses my-only-once so that it breaks like this, because the symbols gensym returns are unique, and I cannot create a second copy of it outside of the macro. I assume that capturing it is unwanted, otherwise the canonical version wouldn't bother adding the additional layer of gensym. How could capturing a symbol like #:G826 be a problem? Please provide an example where this capture manifests.


Solution

  • We can demonstrate a behavioral difference between my-once-only and once-only:

    Let's store our test form in a variable.

    (defvar *form* '(lexalias a 0 (lexalias b (1+ a) (list a b))))
    

    This test form exercises a macro called lexalias, which we will define in two ways. First with once-only:

    (defmacro lexalias (var value &body body)
      (once-only (value)
        `(symbol-macrolet ((,var ,value))
           ,@body)))
    
    (eval *form*) -> (0 1)
    

    Then with my-once-only:

    (defmacro lexalias (var value &body body)
      (my-once-only (value)
        `(symbol-macrolet ((,var ,value))
           ,@body)))
    
    (eval *form*) -> (1 1)
    

    Oops! The problem is that under my-once-only, both a and b end up being symbol-macrolet aliases for exactly the same gensym; the returned expression (list a b) ends up being something like (list #:g0025 #:g0025).

    If you're writing a macro-writing helper that implements once-only evaluation, you have no idea how the symbol is going to be used by the code which calls the macro, whose author uses your once-only tool. There are two big unknowns: the nature of the macro and of its use.

    As you can see, if you don't make fresh gensyms, it will not work correctly in all conceivable scenarios.