Common Lisp

Table of Contents

ASDF

Defining systems

Dependencies are ordered

[2025-04-24 Thu] This is inferred from Daniel Kochmansky's comment here (ecl issue #555). That's the maintainer of ECL.

In that case: A package B forgot to declare UIOP as a dependency. When ASDF tries to load B as a dependency of A, UIOP isn't present. That's true even if UIOP is specified later in A's dependency list.

I must confirm:

  • whether this is intended ASDF behavior. I couldn't find any statements about it in the ASDF manual.
  • If not, whether only ECL behaves this way. SBCL can load the same systems without issue. Is that a result of
    1. UIOP being treated specially, system definitions aside, or
    2. SBCL processing dependencies "breadth-first", ECL "depth-first"?

Aside: I kind of shocked myself by finding this issue so quickly. The poster's problem matches mine in all important ways, and this was the first or second issue I viewed after searching just "uiop".

[2025-04-25 Fri] Indeed, in ECL, putting "asdf" as the first dependency in the ASD resolves the problem. Previously, "uiop" was last.

  • Consider trying:

    Including cl-str in the source registry directly. Then ASDF might be able to find the .asd.

Subsystems can be nested

I've tested this with ASDF via ql:quickload. With this file structure:

foo/
  foo.asd
  main.lisp
  bar/
    bar.asd
    main.lisp

ASDF finds bar if it finds foo, and I can load them independently.

This invites the use of Git submodules.

Controlling the source registry

I manage dependencies with Quicklisp. I also write small scripts in Common Lisp. I don't want to pay for loading Quicklisp every time I run a script. (On principle; the load time is short.)

I need to manually tell ASDF where to find the systems pulled by Quicklisp. See the ASDF manual on controlling the registry for basics.

I developed these examples to put the basics into practice.

With CL_SOURCE_REGISTRY

The SBCL invocation below foregoes Quicklisp. It configures ASDF's source registry entirely through the variable CL_SOURCE_REGISTRY, documented here.

Note:

  • Tilde characters are expanded by the shell, not necessarily Common Lisp.
  • SBCL's --script argument accepts input from stdin. So can ECL's --shell argument, but not when given a heredoc, for some reason.
  • The "//" in the value given to CL_SOURCE_REGISTRY means ASDF should recursively search the directory given.
CL_SOURCE_REGISTRY="~/src/dirmake/:~/quicklisp/dists/quicklisp/software//" sbcl --script <<EOF
(require 'asdf)
(asdf:load-system :dirmake)
(dirmake:main)
EOF

With initialize-source-registry

Run the following with sbcl --script:

(require 'asdf)
(asdf:initialize-source-registry
 '(:source-registry
   (:directory (:home "src/dirmake"))
   (:tree (:home "quicklisp/dists/quicklisp/software"))
   :ignore-inherited-configuration))
(asdf:load-system :dirmake)
(dirmake:main)

ASDF, or maybe Quicklisp, seems not to like systems with slashes / in their names

Bind

Supports mutual recursion in labels. I used this arbitrary example:

(bind (((:labels a (x))
        (if (evenp x) (mod x 3) (b x)))
       ((:labels b (x))
        (if (zerop (mod x 3)) x (a (+ 2 x)))))
  (a 25))

Defining classes

Each of a class's slot definitions is a list whose cdr is a property list.

Trivia (pattern matching library)

In Quicklisp.

Solved: Unbound pattern errors for alist, plist, property

Encountered for version trivia-20230618-git in the Quicklisp dist.

I was loading trivia but not use-ing it. The patterns listed above, unprefixed, would cause errors.

Two solutions:

  • (use-package :trivia) or equivalent, and don't namespace anything.
  • (trivia:match <...> ((trivia:property <...>)))

In short, patterns must be namespaced just like macros and functions. trivia's macros don't pull them in automatically.

match: _ default branch

Use the empty pattern _ for a default branch.

Written pre-2025:

As in CL's cond, use otherwise as a default last branch in a match or similar expression.

plist takes a fuzzy subsequence of the expected list

For example:

(ql:quickload :trivia)
(trivia:match '(:a 1 :b 2 :c 3)
  ((trivia:plist :c 3 :a 1) 'ok))
OK

Woo (web server)

System dependencies

  • libev: available as libev on Alpine 3.20.

Error handling: Conditions and restarts

Throw a condition, instantiated from a condition type, to signify an unusual situation.

Define a condition type like this:

(define-condition my-custom-error (error)
  ;; slot specs & options as in DEFCLASS
  )

Signal a condition with any of the following:

Function If condition not handled
ERROR Invoke debugger
WARN Print a warning
SIGNAL Return nil

Prefer error for errors because it ensures the condition is handled before computation continues.

A restart is a method of recovery from a signaled condition. Define a restart exactly where the flow of control should resume. Use restart-case.

Handle errors from higher up on the call stack using handler-bind. Functions bound by handler-bind can use invoke-restart to manually invoke any restart the programmer thinks might be available.

Example from Practical Common Lisp

 1  (define-condition malformed-log-entry-error (error)
 2    ((text :initarg :text :reader text)))
 3  
 4  (defun log-analyzer ()
 5    (handler-bind ((malformed-log-entry-error
 6                     #'(lambda (c)
 7                         (invoke-restart 'skip-log-entry))))
 8      (dolist (log (find-all-logs))
 9        (analyze-log log))))
10  
11  (defun analyze-log (log)
12    (dolist (entry (parse-log-file log))
13      (analyze-entry entry)))
14  
15  (defun parse-log-file (file)
16    (with-open-file (in file :direction :input)
17      (loop for text = (read-line in nil nil) while text
18            for entry = (restart-case (parse-log-entry text)
19                          (skip-log-entry () nil))
20            when entry collect it)))
21  
22  (defun parse-log-entry (text)
23    (if (well-formed-log-entry-p text)
24        (make-instance 'log-entry ...)
25        (restart-case (error 'malformed-log-entry-error :text text)
26          (use-value (value) value)
27          (reparse-entry (fixed-text) (parse-log-entry fixed-text)))))

Notice: Two functions establish restarts. Parse-log-file establishes one near line 18, and parse-log-entry establishes two near line 25.

The error call on line 25 is the only part of this code that can signal a condition. When that happens, all three restarts are available. The condition travels up the stack looking for a handler. If no function had one, we'd enter the debugger with at least our three custom restarts available. (Entering the debugger is a feature of ERROR. See WARN and SIGNAL.) But log-analyzer has a policy for a malformed-log-entry-error. It directly invokes the restart skip-log-entry. Handler-bind is used instead of handler-case; the latter would unwind the stack to log-analyzer. Control flow jumps to line 19, where the restart is defined. That expression returns nil, and the loop in parse-log-file continues as if nothing strange has happened.

Dynamic/special variables

Usually, variables are globally special or not special at all. A programmer must work hard to create exceptions.

Special denotes that, even when the variable is rebound in forms like let, any code run during the dynamic extent of that form can see the dynamic binding.

Defvar and defparameter both create dynamic variables. "Dynamic" and "special" are practically synonyms. Defparameter overwrites any existing value; defvar doesn't. That difference influences interactive development: A value assigned with defvar lasts an entire Lisp session. A value assigned with defparameter is overwritten when code is reloaded.

So, true to its name, defparameter is best suited for values which tweak the overall program behavior. Defvar is good for counters and other cumulative state.

What about runtime configuration variables like in Emacs? I'm thinking defvar. It sounds too jarring to reload those upon reloading a system.

Watch out: let vs let* matters a lot for dynamic variables

Consider this case I just ([2025-08-07 Thu]) encountered.

I want a test macro to use package-internal streams to accumulate text. I declare them as special variables with defvar and bind them to nil. In the body of the macro, I can bind these variables to fresh streams for the duration of the macro body's execution. Then the macro user can call presupplied functions that, under the hood, write text to the fresh streams. Part of the macro looks like this:

`(let ((pass-stream (make-string-output-stream))
       (fail-stream (make-string-output-stream))
       (,result (eval ,@body)))
   ;; ...
   )

,@body is supposed to be where the macro user can call the presupplied functions. But let binds values in parallel. Therefore, during eval, the values of both stream variables are still nil.

Simply changing let to let* fixes the problem.

Symbols

There's lots of weird (but powerful) stuff about how symbols are handled in Common Lisp. I've found this article useful.

Using uninterned (#:) symbols for package definitions

Say you're in CL-USER. Then this package definition:

(defpackage :foo
  (:use :cl)
  (:export :bar))

Interns :foo and :bar in CL-USER. That's probably not what you want. I'm not sure if :cl gets interned or if it already is. #: produces an uninterned symbol, so it's common to write:

(defpackage #:foo
  (:use :cl)
  (:export #:bar))

Implementation portability

ECL + Alpine Linux

Install ecl-dev. Otherwise, ECL complains that it's "Unable to find include directory" when

  1. Loading ~/quicklisp/setup.lisp or
  2. Attempting to load an ASDF package

Guessing (1) is caused by (2).

Environment variable CL_SOURCE_REGISTRY

Formatted like PATH. An empty string may appear once, either between colons or at the beginning or end of the string, to splice in inherited configuration (e.g. from Quicklisp). I haven't tested the splicing much.

This variable has worked on Alpine Linux with ECL 24.5.10 and SBCL 2.5.3.

Invoking restarts on ECL

ECL manual: Use :q at the debugger to return to the top-level loop. Use :rN to invoke restart N.

When in doubt, use the standard function invoke-restart, but remember: ECL keeps restart symbols in the SI package. So while the intuitive thing would be:

(invoke-restart 'restart-toplevel)

The correct invocation is:

(invoke-restart 'si::restart-toplevel)

Find correct symbols with, for example:

(mapcar #'restart-name (compute-restarts))

Generating executables

ECL

Many basics and good examples in ECL's manual.

Require ASDF before running c:build-program

A simple mistake I've made repeatedly.

Establish an entry point with c:build-program ... :epilogue-code ...

The manual doesn't describe perfectly what :epilogue-code does.

The easy path: asdf:make-build

See this example build script for Ostra, which places an executable in the current directory:

;; For use with ECL
(load "~/quicklisp/setup.lisp")
(asdf:make-build :ostra
                 :type :program
                 :move-here #P"./")