Restarts in Common Lisp

Posted on 2020-04-01

Errata: An earlier version of this post was misrepresenting conditions as exceptions, which has been addressed.

I have been reading Practical Common Lisp by Peter Seibel over the weekend, which is an excellent introduction to Common Lisp, showcasing its power by writing real programs. If you are interested in Lisp or programming languages at all, I recommend at least skimming it, it is free to read online.

Writing a Lisp-descended language professionally, and also living inside Emacs, I had dabbled in Common Lisp before, but I still found something I was not aware of, restarts. I do not think that this is a particularly well known feature outside the Lisp world, so I would like to spread awareness, as I think it is a particularly interesting take on error handling.

The book explains restarts using a mocked parser, which I will slightly modify for my example. Imagine you are writing an interpreter/compiler for a language. On the lowest level you are parsing lines to some internal representation:

(define-condition invalid-line-error (error)
  ((line :initarg :line :reader line)))

(defun parse-line (line)
  (if (valid-line-p line)
      (to-ir line)
    (error 'invalid-line-error :line line)))

We define a condition, which is similar to an exception object with metadata in other languages A "condition" in Common Lisp, as has been explained to me by Michał "phoe" Herda, is a way of signalling arbitrary events up the stack to allow running of additional code, not just signalling errors. They're comparable to hooks in Emacs, but dynamically scoped to the current call stack. , and a function which attempts to parse a single line. This is assuming of course that a line always represents a complete parsable entity, but this is only an example after all. If it turns out that the line is invalid, it signals a condition up the stack. We attach the line encountered, in case we want to use it for error reporting.

Now imagine your parser is used in two situations: there is a compiler, and a REPL. For the compiler, you would like to abort at the first invalid line you encounter, which is what we are currently set up to do. But for the REPL, you would like to ignore the line and just continue with the next line. I'm not saying that is necessarily a good idea, but it is something some REPLs do, for example some Clojure REPLs.

To ignore a line, we would have to either do it on a low-level, return nil instead of signalling and filter out nil values up the stack. Handling the condition will not help us a lot, because at that point we have lost our position in the file already, or have we?

The next layer up is parsing a collection of lines:

(defun parse-lines (lines)
  (loop for line in lines
        for entry = (restart-case
                     (parse-line line)
                     (skip-line () nil))
        when entry collect it))

This is where the magic begins. The loop construct just loops over the lines, applies parse-line to every element of the list, and returns a list containing all results which are not nil. The feature I am showcasing in this post is restart-case. Think of it this way: it does not handle a condition, but when the stack starts unwinding Technically not unwinding yet, at least not in Common Lisp. because we signalled a condition in parse-line, it registers a possible restart-position. If the condition is handled at some point, If it isn't caught, you will get dropped into the debugger, which also gives you the option to restart. the signal handler can choose to restart at any restart-point that has been registered down the stack.

Now let us have a look at the callers:

(defun parse-compile (lines)
      (parse-lines lines)
    (invalid-line-error (e)
                        (print-error e))))

(defun parse-repl (lines)
  (handler-bind ((invalid-line-error
                  #'(lambda (e)
                      (invoke-restart 'skip-line))))
    (parse-lines lines)))

There is a lot to unpack here. The compiler code is using handler-case, which is comparable to catch in other languages. It unwinds the stack to the current point and runs the signal handling code, in this case print-error.

Because we do not actually want to unwind the stack all the way, but resume execution inside the loop in parse-lines, we use a different construct, handler-bind, which automatically handles invalid-line-error and invokes the skip-line restart. If you scroll up to parse-lines now, you will see that the restart clause says, if we restart here, just return nil, and nil will be filtered on the very next line by when entry.

The elegance here is the split of signal handling code, and decisions about which signal handling approach to take. You can register a lot of different restart-case statements throughout the stack, and let the caller decide if some signals are okay to ignore, without the caller having to have intricate knowledge of the lower-level code. It does need to know about the registered restart-case statements though, at least by name.

If you want to learn more about this, make sure to have a look at the book, it goes into much more detail than I did here.