Search code examples
macroscommon-lispclisp

Is there any way to see the implementations of built-in macros in Common Lisp?


Common Lisp built-in functions are probably implemented in C. But I imagine macros are implemented in lisp (sorry if I'm wrong about any of two sentences). Is there any way (through some function or some macro) to see the implementations of built-in macros in Common Lisp? I'm using CLisp.


Solution

  • The ability to inspect function and macro definitions is a feature of your development environment. These days it is typical to use SLIME or SLY with emacs as the basis of a Lisp development environment. I personally use SLIME, but I have heard good things about SLY, too.

    In SLIME you can invoke slime-edit-definition (either by keying M-x slime-edit-definition or by using the keybinding M-.) to visit a definition for the symbol under the cursor in a source file. This works both when editing in a source file, or from the REPL. This feature is extremely useful when you want to inspect some library code you are working with, but you can also view a lot of built-in definitions this way. You can even jump to a new definition from a new symbol found in whatever definition you are currently inspecting.

    After you are done looking at a definition, you can use M-x slime-pop-find-definition-stack, or the easier to remember keybinding M-, (M-* will also work), to back out through the previously viewed definitions, eventually returning to your starting point.

    Here is an example, in SBCL:

    CL-USER> with-open-file[press M-.]
    

    (Note that the "[press M-.]" above is not typed, but only meant to remind what action is taken here). With the cursor on or right after the symbol with-open-file, press M-. to see the definition:

    (sb-xc:defmacro with-open-file ((stream filespec &rest options)
                                    &body body)
      (multiple-value-bind (forms decls) (parse-body body nil)
        (let ((abortp (gensym)))
          `(let ((,stream (open ,filespec ,@options))
                 (,abortp t))
             ,@decls
             (unwind-protect
                  (multiple-value-prog1
                      (progn ,@forms)
                    (setq ,abortp nil))
               (when ,stream
                 (close ,stream :abort ,abortp)))))))
    

    This time after keying M-. SLIME gives a choice of definitions to view:

    CL-USER> and[press M-.]
    

    Displayed in an emacs buffer:

    /path-to-source/sbcl-2.0.4/src/code/macros.lisp
      (DEFMACRO AND)
    /path-to-source/sbcl-2.0.4/src/pcl/ctypes.lisp
      (DEFINE-METHOD-COMBINATION AND)
    

    We want to see the macro definition, so move the cursor to the line showing (DEFMACRO AND), and the following definition is displayed:

    ;; AND and OR are defined in terms of IF.
    (sb-xc:defmacro and (&rest forms)
      (named-let expand-forms ((nested nil) (forms forms) (ignore-last nil))
        (cond ((endp forms) t)
              ((endp (rest forms))
               (let ((car (car forms)))
                 (cond (nested
                        car)
                       (t
                        ;; Preserve non-toplevelness of the form!
                        `(the t ,car)))))
              ((and ignore-last
                    (endp (cddr forms)))
               (car forms))
              ;; Better code that way, since the result will only have two
              ;; values, NIL or the last form, and the precedeing tests
              ;; will only be used for jumps
              ((and (not nested) (cddr forms))
               `(if ,(expand-forms t forms t)
                    ,@(last forms)))
              (t
               `(if ,(first forms)
                    ,(expand-forms t (rest forms) ignore-last))))))
    

    There is more stuff here, since you are now actually in the source file that contains the definition for and; if you scroll down a bit you can also find the definition for or.

    A lot of SBCL functions are written in Lisp; SBCL has a very high-quality compiler, so a lot of stuff that you might otherwise expect to be written in C can be written in Lisp without loss of performance. Here is the definition for the function list-length:

    CL-USER> list-length[press M-.]
    
    (defun list-length (list)
      "Return the length of the given List, or Nil if the List is circular."
      (do ((n 0 (+ n 2))
           (y list (cddr y))
           (z list (cdr z)))
          (())
        (declare (type fixnum n)
                 (type list y z))
        (when (endp y) (return n))
        (when (endp (cdr y)) (return (+ n 1)))
        (when (and (eq y z) (> n 0)) (return nil))))
    

    The same thing can be done when using CLISP with SLIME. Here is with-open-file as defined in CLISP:

    CL-USER> with-open-file[press M-.]
    
    (defmacro with-open-file ((stream &rest options) &body body)
      (multiple-value-bind (body-rest declarations) (SYSTEM::PARSE-BODY body)
        `(LET ((,stream (OPEN ,@options)))
           (DECLARE (READ-ONLY ,stream) ,@declarations)
           (UNWIND-PROTECT
             (MULTIPLE-VALUE-PROG1
               (PROGN ,@body-rest)
               ;; Why do we do a first CLOSE invocation inside the protected form?
               ;; For reliability: Because the stream may be a buffered file stream,
               ;; therefore (CLOSE ,stream) may produce a disk-full error while
               ;; writing the last block of the file. In this case, we need to erase
               ;; the file again, through a (CLOSE ,stream :ABORT T) invocation.
               (WHEN ,stream (CLOSE ,stream)))
             (WHEN ,stream (CLOSE ,stream :ABORT T))))))
    

    But, many CLISP functions are written in C, and those definitions are not available to inspect in the same way as before:

    CL-USER> list-length[press M-.]
    
    No known definition for: list-length (in COMMON-LISP-USER)