screwlisp proposes a toast

A little ansi common lisp condition ls example

| link | Last. 20260309T210612550Z |

Unlike any other language - in the top 25 hot languages of 2025, at least - lisp is in its 30s. The language now is the only popular language that was standardised more than thirty years ago. Look at languages struggling to draft algebraic effects in the past year - but lisp has The Lisp Condition System (local restarts from external condition signal handling) in its standard, and has for more than 30 years (now, in 2026) of which lisp users are rightly proud. You can get this age number for any language by looking at wikipedia.org - it is the number labelled “stable release”

Stable release

3.14.3[3] Edit this on Wikidata / 3 February 2026; 33 days ago

Stable release

Fortran 2023 (ISO/IEC 1539:2023) / November 17, 2023; 2 years ago

Stable release

Java SE 25[2] Edit this on Wikidata / 16 September 2025; 5 months ago

standard document ANSI INCITS 226-1994 (S2018)

1994 (32 years ago) for ANSI Common Lisp

one of these things is not like the others. Do not be mystified by claims that the name ‘javascript’ was used back in the 90s: Javascript (wikipedia) used now is “June 2024; 21 months ago”.

It-not-being-a-baby adds a certain weight to common lisp the language. In other languages, programming libraries and programs are seen to be more stable than the language itself which is always trying to keep up with the Jones’. This is sort of true in common lisp- if you consider lisp’s modern, lazy, optimizing infinite series macro package hails from 1978 and the US Department of Energy’s Maxima=Macsyma from 1982/1968

Stable release

5.49.0[1] Edit this on Wikidata / 18 December 2025; 2 months ago

though we can see Maxima’s stable release is a baby, unlike the ansi common lisp standard.

We can clearly rely on writing ansi common lisp code as per the standard, portable between its many famous free and commercial compilers more than we can rely on any particular software subject to a software lifecycle, even an immortal jellyfish like Maxima.

Common lisp comes with a facility for modernising programs without changing them which is what the algebraic effects scientists are trying to copy into their languages this year. This is the ANSI common lisp condition system.

ls

A non-condition lisp example

(defun %ls
    (&optional (dir #p"./*.*") (rel #p"../")
     &aux (rel-dir (probe-file rel)))
  (dolist
      (file (directory dir))
    (print (enough-namestring file rel-dir))))

showing some of my blog and temporary files from emacs

CL-USER> (%ls)

"md/.#Section1.fragment.md" 
"md/Section1.fragment.md" 
"md/Section1.fragment.md~" 
NIL

we can see this makes sense as a program for listing contents of directories.

This is great and all but I would actually like to argue that this program is not very future-proof. The implication is that I would change the program if I wanted to do something else. Like common lisp itself, I would like my programs to exhibit forward-compatibility, not just backwards-compatibility. Actually, I am just recently trying to fully get my head around this myself.

Let us try phrasing our program differently.

Using conditions

Note I added a done signal though we do not use it - similar to an :after method-qualifier I guess.

(define-condition ls () ())
(define-condition want ()
  ((target :initarg :target :accessor target)))
(define-condition done ()
  ((result :initarg :result :accessor result)))
(define-condition want-ls (want ls) ())
(define-condition done-ls (done ls) ())

(defun %%ls
    (target &rest other-keys &key &allow-other-keys)
  (restart-case
      (restart-case
	  (apply 'signal 'want-ls
		 :target target
		 other-keys)
	(dirs
	    (c)
	  (setf (target c) (directory (target c)))
	  (signal c)))
    (prints
	(results)
      (let ((res (map 'list 'print results)))
	(signal 'done-ls
		:result res)))))

But that is, as Kent tells us, a half-program. It signals a want-ls with our arguement, and has tiered restarts for retrieving directory contents, and printing a sequence.

(defun targets-dir (c)
  (when (and (slot-boundp c 'target)
	     (typep (target c) '(or string pathname)))
    (let ((r (find-restart 'dirs c)))
      (invoke-restart r c))))

(defun prints-target (c)
  (when (and (slot-boundp c 'target)
	   (listp (target c)))
      (let ((r (find-restart 'prints c)))
	(when r (invoke-restart r (target c))))))

As a first pass we specified two handlers that use the nested restarts in an obvious way. Note that we cannot restart uphill. And we bind those handlers at run-time:

(handler-bind
    ((want-ls #'targets-dir)
     (want-ls #'prints-target))
  (%%ls "*.*"))

e.g.:

CL-USER> (handler-bind
    ((want-ls #'targets-dir)
     (want-ls #'prints-target))
  (%%ls "*.*"))

#P"/home/screwlisp/screwlisps-kitten/fundamental/a-little-ansi-common-lisp/md/#Section1.fragment.md#" 
#P"/home/screwlisp/screwlisps-kitten/fundamental/a-little-ansi-common-lisp/md/.#Section1.fragment.md" 
#P"/home/screwlisp/screwlisps-kitten/fundamental/a-little-ansi-common-lisp/md/Section1.fragment.md" 
#P"/home/screwlisp/screwlisps-kitten/fundamental/a-little-ansi-common-lisp/md/Section1.fragment.md~" 
NIL

though I really want relative paths again. In our lisp condition system style, we do not have to and in fact should not touch our previous program code. Here the path kind of splits into three modestly different ways to extend the program without modifying it.

  1. The handler modifies data before invoking prints
  2. The handler signals to handlers in some different handler-bind that eventually restart - the handler-bind this handler was in is deactivated to give other handlers a chance and stop infinite handling loops.
  3. The handler restarts to an outer restart that calls some different function/args, still in the scope of this handler-bind.
(defun list-rel-to (c &aux (rel #p"../"))
  (when (and (slot-boundp c 'target)
	     (listp (target c))
	     (pathnamep (car (target c)))
	     (not (member :relative
			  (pathname-directory
			   (car (target c))))))
    (invoke-restart
     'prints
     (mapcar (lambda (x) (enough-namestring x (probe-file rel)))
	     (target c)))))

(defun fun-again (c &aux (rel #p"../"))
  (when (and (slot-boundp c 'target) ;; no work?
	     (listp (target c))
	     (pathnamep (car (target c)))
	     (not (member :relative
			  (pathname-directory
			   (car (target c))))))
    (let ((res (mapcar (lambda (x)
			 (enough-namestring x (probe-file rel)))
		       (target c))))
      (setf (target c) res)
      (handler-bind ((want-ls #'prints-target))
	(signal c)))))

(defun fun-retry (c &aux (rel #p"../"))
  (when (and (slot-boundp c 'target)
	     (listp (target c))
	     (pathnamep (car (target c)))
	     (not (member :relative
			  (pathname-directory
			   (car (target c))))))
    (invoke-restart
     'retry
     (mapcar (lambda (x)
	       (enough-namestring x (probe-file rel)))
	     (target c)))))

No matter which of these three is uncommented, the result is the same:

(handler-bind
    ((want-ls #'targets-dir)
     (want-ls #'fun-retry)
     (want-ls #'fun-again)
     (want-ls #'list-rel-to)
     (want-ls #'prints-target))
  (restart-case
      (%%ls "*.*")
    (retry (target)
      (%%ls target))))

Results:

CL-USER> (handler-bind
    ((want-ls #'targets-dir)
     (want-ls #'fun-retry)
     ;;(want-ls #'fun-again)
     ;;(want-ls #'list-rel-to)
     (want-ls #'prints-target))
  (restart-case
      (%%ls "*.*")
    (retry (target)
      (%%ls target))))

"md/#Section1.fragment.md#" 
"md/.#Section1.fragment.md" 
"md/Section1.fragment.md" 
"md/Section1.fragment.md~" 
NIL
CL-USER> (handler-bind
    ((want-ls #'targets-dir)
     ;;(want-ls #'fun-retry)
     (want-ls #'fun-again)
     ;;(want-ls #'list-rel-to)
     (want-ls #'prints-target))
  (restart-case
      (%%ls "*.*")
    (retry (target)
      (%%ls target))))

"md/#Section1.fragment.md#" 
"md/.#Section1.fragment.md" 
"md/Section1.fragment.md" 
"md/Section1.fragment.md~" 
NIL
CL-USER> (handler-bind
    ((want-ls #'targets-dir)
     ;;(want-ls #'fun-retry)
     ;;(want-ls #'fun-again)
     (want-ls #'list-rel-to)
     (want-ls #'prints-target))
  (restart-case
      (%%ls "*.*")
    (retry (target)
      (%%ls target))))

"md/#Section1.fragment.md#" 
"md/.#Section1.fragment.md" 
"md/Section1.fragment.md" 
"md/Section1.fragment.md~" 
NIL
CL-USER> (handler-bind
    ((want-ls #'targets-dir)
     (want-ls #'fun-retry)
     (want-ls #'fun-again)
     (want-ls #'list-rel-to)
     (want-ls #'prints-target))
  (restart-case
      (%%ls "*.*")
    (retry (target)
      (%%ls target))))

"md/#Section1.fragment.md#" 
"md/.#Section1.fragment.md" 
"md/Section1.fragment.md" 
"md/Section1.fragment.md~" 
NIL

Note we should have used find-restart to check if the restarts were available on the condition before invoking them, but in this example we knew they were there.

The Delete Problem

In the 70s, Sandewall coined the delete problem about reasoning structures like this. When your program works but uses non-trivial condition handling, meaning here that more than one signal can happen - removing a condition or restart might drastically change the program’s behaviour (or not).

To put it pithily, emergent behaviours are chaotic. While we are purely good-old-fashioned-ai (list processing!) here, the delete problem has far-ranging implications about censorship in today’s large language models - doctoring the training data to remove something bad that is real could be expected to derange the model.

Conclusions

We saw that the ansi common lisp standard - the only widely used programming language whose current definition is in its 30s (in 2026) - defines a condition system which is coming into vogue in other languages today under the name algebraic effects.

In particular, we saw that run-time handler-bind handlers are able to locally handle and invoke-restart inside a pre-existingly programmed program, which lets us customize and extend programs without changing them. This is of fundamental importance, because repeatedly changing the source of a widely used program is known in software engineering to be a disaster. We would like the program to stay intact and reliable while we choose how we interact with that program in our own context at runtime.

This is an example of John Mashey’s (unix/bell labs) Small-is-beautiful design principle. https://www.usenix.org/legacy/events/bsdcon/mashey_small/

Sandewall’s delete problem shows that changing a program has chaotic effects. So we would like to extend and customize what our program does at run-time without changing the program’s definition for everyone, everything and everywhere else. This is a fundamental programming style.

screwlisp proposes a toast