lispcommon-lispquicklisp

Why does Lisp code execute if entered line by line in the REPL but not if compiled in the buffer?


I am a noob Lisper and am attempting Adam Tornhill's Lisp for the Web tutorial using Portacle Emacs.

I would very much appreciate an explanation of why the set-up code below will run happily and change the REPL cursor to RETRO-GAMES> if entered line by line in the REPL but does not seem to work if compiled in the buffer. (The first time it compiles it often throws an error saying it can't find the quickload packages; the REPL cursor never changes.)

If anyone can shed light on what this implies for compiled programs, it would be great. I'm struggling to understand the value of programming directly into a REPL rather than a saved file. If there are any noob-level resources on what a fully compiled program "looks like" in terms of whether it includes the type of loady code below, a link would make my day. Thank you.

(load "C:/Users/USER/portacle/all/quicklisp/quicklisp/setup.lisp")

(ql:quickload :cl-who)
(ql:quickload :hunchentoot)
(ql:quickload :parenscript)

(defpackage :retro-games
  (:use :cl :cl-who :hunchentoot :parenscript))
(in-package :retro-games)
(defclass game ()
  ((name :initarg :name)
   (votes :initform 0)))

Solution

  • To see why this code is a problem for compilation, consider what the compiler must do when compiling a file containing it, in a 'cold' Lisp image, where 'cold' here specifically means 'without Quicklisp loaded'. To see this, divide it into chunks. For the sake of argument we'll imagine that the way the file compiler works is to read a form from the file, compile it, and write it out. It does have to work in a way which is equivalent to this for our purposes so that's OK here.

    First chunk

    (load "C:/Users/USER/portacle/all/quicklisp/quicklisp/setup.lisp")
    

    load is a normal function and the call to it occurs at top level in the file: what the compiler will do is to compile code so that, when the file is loaded this load is called. The compiler will not itself execute load, and so Quicklisp will not be loaded at compile time.

    Second chunk

    (ql:quickload :cl-who)
    (ql:quickload :hunchentoot)
    (ql:quickload :parenscript)
    

    This code has two problems. In a cold lisp the QL package does not exist, because the previous load was not executed at compile-time, so the reader will raise an exception when reading these forms. Compilation will stop here.

    If the Lisp is not cold and in particular if the QL package does exist (so, if the Lisp image which is doing the compilation does have Quicklisp loaded into it) then the compiler will simply write code which causes these three ql:quickload calls to take place at load-time, and will not load these three QL systems now.

    In a cold lisp you will not get beyond this step. In a Lisp with Quicklisp loaded at compile time you will.

    Third chunk

    (defpackage :retro-games
      (:use :cl :cl-who :hunchentoot :parenscript))
    

    This chunk will only be reached in a Lisp where Quicklisp is loaded at compile time. This is a package definition, and it has effect at compile time, as it must do. But, at compile time, the calls in the second chunk have not yet taken place: this package definition refers to packages which do not exist. So this is a compile-time error and compilation will stop here.

    If the Lisp doing the compilation not only has Quicklisp loadad but also has the three QL systems loaded, this will succeed.

    Fourth chunk

    (in-package :retro-games)
    

    This will only be reached in a lisp which, at compile time, has both Quicklisp and the three QL systems loaded into it. In that case it tells the compiler that the following forms are to be read in the retro-games package. Otherwise it is never reached.

    Fifth chunk

    (defclass game ()
      ((name :initarg :name)
       (votes :initform 0)))
    

    In the unlikely event the compiler gets this far, this defines a class called retro-games::game.

    What the problem is

    The problem is that the compiler is, well, a compiler: what it does is compile code for later execution. But the later parts of your source depend on the earlier parts of your source having been loaded.

    There are two ways to deal with this.

    If you are only writing a very small amount of code, then you can tell the compiler that some things must happen at compile time as well as at load time. The way you do this is by eval-when, and the particular incantation you want is something like this, for chunks one and two:

    (eval-when (:compile-toplevel :load-toplevel :execute)
      (load "C:/Users/USER/portacle/all/quicklisp/quicklisp/setup.lisp"))
    
    (eval-when (:compile-toplevel :load-toplevel :execute)
      (ql:quickload :cl-who)
      (ql:quickload :hunchentoot)
      (ql:quickload :parenscript))
    

    Note you must have two separate incantations of eval-when because the QL package needs to be brought into being by the first so that the second can be read.

    The semantics of eval-when are mildly hairy, but this incantation will do what you want: it will tell the compiler to load these things and make sure that they are also run at load time.

    The second way of dealing with this, which is appropriate for larger systems, is to have one or more files which build the compile-time environment for later files, and which are compiled and loaded before the later files, under the control of something like ASDF. In this case you typically want some minimal bootstrap file, which perhaps simply needs to make sure that Quicklisp is loaded and then ask Quicklisp (and hence ASDF) to compile and load everything else, with dependency management looked after by those tools, so there would be no ql:quickloads at all. How to set up something like this is beyond this answer however!