Why does this Lisp macro as a whole work, even though each piece doesn't work?
Asked Answered
D

2

6

I'm reading/working through Practical Common Lisp. I'm on the chapter about building a test framework in Lisp.

I have the function "test-+" implemented as below, and it works:

(defun test-+ ()
  (check
    (= (+ 1 2) 3)
    (= (+ 5 6) 11)
    (= (+ -1 -6) -7)))

Remember, I said, it works, which is why what follows is so baffling....

Here is some code that "test-+" refers to:

(defmacro check (&body forms)
  `(combine-results
    ,@(loop for f in forms collect `(report-result ,f ',f))))

(defmacro combine-results (&body forms)
  (with-gensyms (result)
    `(let ((,result t))
       ,@(loop for f in forms collect `(unless ,f (setf ,result nil)))
       ,result)))

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

(defun report-result (value form)
  (format t "~:[FAIL~;pass~] ... ~a~%" value form)
  value)

Now, what I've been doing is using Slime to macro-expand these, step by step (using ctrl-c RET, which is mapped to macroexpand-1).

So, the "check" call of "test-+" expands to this:

(COMBINE-RESULTS
  (REPORT-RESULT (= (+ 1 2) 3) '(= (+ 1 2) 3))
  (REPORT-RESULT (= (+ 5 6) 11) '(= (+ 5 6) 11))
  (REPORT-RESULT (= (+ -1 -6) -7) '(= (+ -1 -6) -7)))

And then that macro-expands to this:

(LET ((#:G2867 T))
  (UNLESS (REPORT-RESULT (= (+ 1 2) 3) '(= (+ 1 2) 3)) (SETF #:G2867 NIL))
  (UNLESS (REPORT-RESULT (= (+ 5 6) 11) '(= (+ 5 6) 11)) (SETF #:G2867 NIL))
  (UNLESS (REPORT-RESULT (= (+ -1 -6) -7) '(= (+ -1 -6) -7))
    (SETF #:G2867 NIL))
  #:G2867)

And it is THAT code, directly above this sentence, which doesn't work. If I paste that into the REPL, I get the following error (I'm using Clozure Common Lisp):

Unbound variable: #:G2867 [Condition of type UNBOUND-VARIABLE]

Now, if I take that same code, replace the gensym with a variable name such as "x", it works just fine.

So, how can we explain the following surprises:

  1. The "test-+" macro, which calls all of this, works fine.

  2. The macro-expansion of the "combine-results" macro does not run.

  3. If I remove the gensym from the macro-expansion of "combine-results", it does work.

The only thing I can speculate is that you cannot use code the contains literal usages of gensyms. If so, why not, and how does one work around that? And if that is not the explanation, what is?

Thanks.

Damara answered 29/9, 2012 at 23:43 Comment(0)
T
11

The code, after being printed and read back, is no longer the same code. In particular, the two instances of #:G2867 in the printed representation would be read back as two separated symbols (albeit sharing the same name), while they should be the same in the original internal representation.

Try setting *PRINT-CIRCLE* to T to preserve the identity in the printed representation of the macro-expanded code.

Threadfin answered 30/9, 2012 at 0:37 Comment(0)
L
13

GENSYM creates uninterned symbols. When the macro runs normally, this isn't a problem, because the same uninterned symbol is being substituted throughout the expression.

But when you copy and paste the expression into the REPL, this doesn't happen. #: tells the reader to return an uninterned symbol. As a result, each occurrence of #:G2867 is a different symbol, and you get the unbound variable warning.

If you do (setq *print-circle* t) before doing the MACROEXPAND it will use #n= and #n# notation to link the identical symbols together.

Lens answered 30/9, 2012 at 0:37 Comment(1)
There's a section of Let Over Lambda that discusses this behavior, and why gensyms work this way, and how it relates to variable capture, etc. Here: letoverlambda.com/index.cl/guest/chap3.html#sec_5Cameo
T
11

The code, after being printed and read back, is no longer the same code. In particular, the two instances of #:G2867 in the printed representation would be read back as two separated symbols (albeit sharing the same name), while they should be the same in the original internal representation.

Try setting *PRINT-CIRCLE* to T to preserve the identity in the printed representation of the macro-expanded code.

Threadfin answered 30/9, 2012 at 0:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.