1
votes

Consider the following code:

CL-USER> (defmacro sum (a b)
           (+ a b))
SUM
CL-USER> (let ((alpha 3) (beta -1))
           (sum alpha beta))
; in: LET ((ALPHA 3) (BETA -1))
;     (SUM ALPHA BETA)
; 
; caught ERROR:       
;   during macroexpansion of (SUM ALPHA BETA). Use *BREAK-ON-SIGNALS* to intercept.
;   
;    Argument X is not a NUMBER: ALPHA

;     (LET ((ALPHA 3) (BETA -1))
;       (SUM ALPHA BETA))
; 
; caught STYLE-WARNING:
;   The variable ALPHA is defined but never used.
; 
; caught STYLE-WARNING:
;   The variable BETA is defined but never used.
; 
; compilation unit finished
;   caught 1 ERROR condition
;   caught 2 STYLE-WARNING conditions
; Evaluation aborted on #<SB-INT:COMPILED-PROGRAM-ERROR {10030E4223}>.

There are basically two reasons (that I could think of), which contribute to the failure of this code:
1. The macro sum is first evaluated upon two variables alpha and beta, which are sent as symbols into the macro. So, the code to be evaluated inside the macro is:

(+ 'alpha 'beta)      

Which won't work, because we can't add two symbols.
2. The variables alpha and beta are lexically bound, because of which, the code of the macro can't access the symbolic values of alpha and beta.
Thus, redefining sum:

CL-USER> (defmacro sum (a b)
           (+ (symbol-value a) (symbol-value b)))
WARNING: redefining COMMON-LISP-USER::SUM in DEFMACRO
SUM

Here, the macro sum will evaluate the value of the symbols provided to it. It can only do it, if it is in the scope of the symbols. So, in order to do that, we can make alpha and beta dynamically bound.
Furthermore, in order to check if the dynamic binding is working, we can make a function dynamic-checker, which is defined below:

CL-USER> (defun dynamic-checker ()
           (+ alpha beta))

; in: DEFUN DYNAMIC-CHECKER
;     (+ ALPHA BETA)
; 
; caught WARNING:
;   undefined variable: ALPHA
; 
; caught WARNING:
;   undefined variable: BETA
; 
; compilation unit finished
;   Undefined variables:
;     ALPHA BETA
;   caught 2 WARNING conditions
DYNAMIC-CHECKER

And, finally we can evaluate this code in the REPL:

CL-USER> (let ((alpha 3) (beta -1))
           (declare (special alpha))
           (declare (special beta))
           (print (dynamic-checker))
           (sum alpha beta))

Which gives us the error:

; in: LET ((ALPHA 3) (BETA -1))
;     (SUM ALPHA BETA)
; 
; caught ERROR:
;   during macroexpansion of (SUM ALPHA BETA). Use *BREAK-ON-SIGNALS* to intercept.
;   
;    The variable ALPHA is unbound.
; 
; compilation unit finished
;   caught 1 ERROR condition

2 ; Evaluation aborted on #<SB-INT:COMPILED-PROGRAM-ERROR {1003F23AD3}>.
CL-USER>     

Note the 2 in the end of the error code. This is returned by the function dynamic-checker, which adds alpha and beta, even though they aren't it's parameters, which proves that the variables alpha and beta can be accessed dynamically by the members of let.
Therefore, the macro sum should've worked now, because both the problems that occurred earlier are resolved.
But this clearly isn't the case, and I'm missing something.
Any help appreciated.

2
The values of the parameters A and B in the macro are the symbols ALPHA and BETA. Usually macroexpansion will happen at compile time, long before the LET is evaluated and the bindings established, so the code cannot work. If you use the interpreted mode you might be able to sum the SYMBOL-VALUEs, but you shouldn't rely on that.jkiiski

2 Answers

4
votes

Interpreter and Compiler vs. interactivity

Common Lisp allows both interpreters and compilers. That you can interactively use a read-eval-print-loop does not mean that the implementation uses an interpreter. It could just incrementally compile the code and then invoke the compiled code. A Lisp interpreter runs the code from the Lisp representation. SBCL doesn't use an interpreter by default. It uses a compiler.

Using an Interpreter

LispWorks has an interpreter. Let's use it:

CL-USER 8 > (defun test ()
              (let ((alpha 3) (beta -1))
                (declare (special alpha))
                (declare (special beta))
                (print (dynamic-checker))
                (sum alpha beta)))
TEST

CL-USER 9 > (test)

2 
2

Thus the code works, since the Lisp interpreter executes the forms and when it sees a macro, then it expands it on the go. The bindings are available.

Let's use the LispWorks stepper, which uses the interpreter. :s is the step command.

(step (test))

(TEST) -> :s
   (LET ((ALPHA 3) (BETA -1))
     (DECLARE (SPECIAL ALPHA))
     (DECLARE (SPECIAL BETA))
     (PRINT (DYNAMIC-CHECKER))
     (SUM ALPHA BETA)) -> :s
      3 -> :s
      3 
      -1 -> :s
      -1 
      (PRINT (DYNAMIC-CHECKER)) -> :s
         (DYNAMIC-CHECKER) -> :s
            (+ ALPHA BETA) -> :s
               ALPHA -> :s
               3 
               BETA -> :s
               -1 
            2 
         2 
2                                ; <- output
      2 
      (SUM ALPHA BETA) <=> 2     ; <- macro expansion to 2
      2 -> :s                    
      2                          ; 2 evaluates to itself
   2 
2 
2

Compilation fails

But we can't compile your code:

CL-USER 10 > (compile 'test)

Error: The variable ALPHA is unbound.
  1 (continue) Try evaluating ALPHA again.
  2 Return the value of :ALPHA instead.
  3 Specify a value to use this time instead of evaluating ALPHA.
  4 Specify a value to set ALPHA to.
  5 (abort) Return to level 0.
  6 Return to top loop level 0.

Type :b for backtrace or :c <option number> to proceed.
Type :bug-form "<subject>" for a bug report template or :? for other options.

The compiler tries to expand the macro, but it does not run the code. Since the LET form isn't executed (the compiler only compiles it to something else, but does not execute it) there is no binding for alpha.

Style

Generally it is a good idea to avoid writing macros which need runtime state from the enclosing code. Such macros would only be executable by an interpreter.

2
votes

As per user jkiiski's comment, I get the feeling you need to understand when the two operations (macro operations and program/function application) are taking place in typically compiled code. Additionally, i wouldn't recommend writing the macros you've defined, but will work through them for the purpose of education.

Lets start by explicitly declaring two symbols to be special, and bind them to some values:

CL-USER> (defvar alpha 6)
6
CL-USER> (defvar beta 5)
5

This not only establishes two variables as special/dynamic, but also gives them bindings to the values 6 and 5 respectively.

More importantly, by doing this we establish the variables and bindings before the examples in your macro or your code will be evaluated.

Now we run your final form and we should get the following:

CL-USER> (let ((alpha 3) (beta -1))
           (declare (special alpha))
           (declare (special beta))
           (print (dynamic-checker))
           (sum alpha beta))

2
11

The first value of 2 is printed due to the printing of (dynamic-checker). The value of 11 is being returned because the macro sum is resolving to the value 11, which then effectively takes the place of the form (sum alpha beta) and is returned in the actual evaluation of your (let ...) code.

The important thing to realise is that the macro is being evaluated at compile-time: that is to say, before the final form of (let ...) is compiled and run. Because it is evaluated before your (let ...) form, your let form does not establish the bindings of 3 and -1 to the symbols alpha and beta when the macro is resolving. I've been cheeky and made explicit bindings to the values 6 and 5 before this, and that's what the macro is seeing.

After the macro is successfully resolved, your let form effectively becomes:

(let ((alpha 3) (beta -1))
  (declare (special alpha))
  (declare (special beta))
  (print (dynamic-checker))
  11)

And that explains why we now get no errors, what caused the errors in your previous example, and why we got the output from my additions specified above, and we can explicitly see when the macro/let form are being resolved.