by dapperdrake | 2025-07-13 17:02:36+00:00
(Not answerer) Slight clarification, because I had the same questions and the previous answer is already really good:

Rainer Joswig pointed out on StackOverflow that the evaluation order is as follows (even though CLHS seems non-explicit about this):

For a compiler, for every "lisp form" (see below):

1. Read time

2. Macroexpansion time

3. Compile time

4. Load time

5 Run time

Read time maps strings to symbolic expressions (lists and atoms). A Common Lisp form is a combination of lists and atoms that is valid Common Lisp, for example

(+ 1 2)

or

(format t "Hello World")

Note, that the following is a list but not valid code, a.k.a. a form:

("abc" 2 thingamagig)

Macroexpansion is when user-defined forms are turned into Lisp forms. This is how WITH-OPEN-FILE can be defined by a user or non-language-implementor. It cannot be a function, because it doesn’t really evaluate its first argument, the stream variable name (a symbol) and the arguments to open. Furthermore, it also cleans up the file handle with UNWIND-PROTECT.

Special forms and macros are "unlike function calls". The difference is, that special forms are provided by the language and macros can be provided by the user. The conditional "if" is a special form for this reason. Haskell sidesteps this particular problem by making "if" (and others) evaluate lazily. In lisp you can get a similar effect by wrapping stuff in lambdas with zero arguments as something called a thunk and funcalling that later.

Compile time takes lisp forms and turns them into "machine code" whatever that happens to be.

Compile time and load time are more easily understood when you look at parenscript. Valid parenscript forms are not always valid lisp forms and vice versa. But they are both symbolic expressions, so basically lisp lists of lists and atoms. Parenscript compiles to JavaScript strings. Those get loaded by the browser when it loads the HTML or JS page that contains the JavaScript strings. Then the browser runs that code.

Look at the differences between PS:defpsmacro and CL:defmacro and PS:defmacro+ps .

In Common Lisp:

Read macros are executed at read time. Macros are evaluated at macroexpansion time. Compiler macros are evaluated at compile time. Load-time-value is evaluated at load time. Funcall, apply, lambda, and technically eval are evaluated at run time.

The slight wrinkle is that eval applies all if these rules recursively. So LIST is a lisp run-time function, but you can also call it to create the output in a DEFMACRO form. All run-time functions already defined can be used for all later forms (and strings) when they are being processed. Doug Hoyte called this "run-time at read-time" [1]. And by extensions it is also "run-time at macroexpansion time" and "run-time at compile time" and "run-time at load time" and "run-time at run time". (This last one is functions as first-class values and having access to a compiler and EVAL.)

LOAD-TIME-VALUE is executed at load time. Technically, the following top-level form is, too:

(Eval-when (:load-toplevel) …)

For ideas on read macros look at Doug Hoyte's [1] SHARP-DOUBLE-QUOTE #" and SHARP-GREATER-THAN #> .

[1] https://letoverlambda.com/index.cl/guest/chap4.html