Debugging Lisp Part 4: Restarts

This is part four of Debugging Lisp. Here are the previous parts on recompilation, inspecting, and class redefinition. The next post on miscellaneous debugging techniques can be found here.

Many languages provide error handling as two distinct parts, throw and catch. Throw is the part that detects something has gone wrong and in some way signals that an error has occurred. In the process, throw creates an exception object which contains information about the problem. The other part, catch, takes the exception object signaled by throw and attempts to recover from the error.

The issue with throw/catch is that throw acts like an unconditional goto to the catch part. Because of this, all of the state information that is available when throw is used that is not given to the exception object is lost. This becomes problematic if the code that catches the error wants to use some information about what happened when the error occurred in order to recover.

As an example, lets say you are implementing a library which takes several files and parses a list of numbers from each one. One way to implement this library is as two functions. The first function, read-file, will read the contents of a single file and return a list of the results. The second, read-files, will take a list of files and return a list of the contents of each one. Here is what the code for those two functions might look like if they did not have any error handling:

(defun read-file (file)
  (with-open-file (in file :direction :input)
    (loop for line = (read-line in nil in)
          until (eq in line)
          collect (parse-integer line))))
 
(defun read-files (files)
  (loop for file in files
        collect (read-file file)))

To test the library you have two files. The first file contains the numbers 5, 10, 15, 20, 25 and the second contains 5, 10, 15, 20, a, 30, 40. In order to make sure your library handles errors properly, you decided to put a line which is just a in the second file. As it stands, parse-integer will signal an error when it comes across this line. To make testing the library easy, you have stored a list containing the pathnames of the two files in the variable *files*. Here is what happens when you try running the library on the two files:

(read-files *files*)
 
=> ERROR

An error occurred due to the a in the second file. As the designer of the library you have to decide what should happen when a situation like this one comes up. Below are several different options you could choose from if your language only provided catch/throw.

Your first option is to just skip the entry that caused the error. To do this, you could use handler-case, Common Lisps version of catch:

(defun read-file (file)
  (with-open-file (in file :direction :input)
    (loop for line = (read-line in nil in)
          until (eq in line)
          when (handler-case (parse-integer line)
                 ;; C is the name being used to
                 ;; refer to the exception object.
                 (error (c)
                   (declare (ignore c))
                   nil))
          collect it)))
 
(read-files *files*)
 
=> ((5 10 15 20 25) (5 10 15 20 30 40))

Another option is to provide a dynamic variable1 which the user of the library can use to specify a value to be used in place of the malformed entry:

(defvar *malformed-value* nil)
 
(defun read-file (file)
  (with-open-file (in file :direction :input)
    (loop for line = (read-line in nil in)
          until (eq in line)
          when (handler-case (parse-integer line)
                 (error (c)
                   (declare (ignore c))
                   *malformed-value*))
          collect it)))
 
(let ((*malformed-value* :malformed))
  (read-files *files*))
 
=> ((5 10 15 20 25) (5 10 15 20 :MALFORMED 30 40))

A third option is to have read-files catch the error and skip the entire file with the malformed entry:

(defun read-files (files)
  (loop for file in files
        when (handler-case (read-file file)
               (error (c)
                 (declare (ignore c))
                 nil))
        collect it))
 
(read-files *files*)
 
=> ((5 10 15 20 25))

Your last option is to let the user of the library handle the exception themselves:

(handler-case (read-files *files*)
  (error (c) (do-something)))

To the user, this last option is somewhat useful because it gives them some flexibility into how the error is handled. As mentioned above, the problem with doing this is that it becomes difficult for the user to properly recover from the error. If the user just wanted to skip the one corrupted file, there is no easy way to for them to do that due to the fact that by the time their error handling code is ran, execution would have left read-files. This means all of the state information, such as the remaining files that need to be read from, is completely lost by the time their code catches the exception.

Another problem with catch/throw is that of the four possible ways above you could handle the problem, you only get to choose one of them. Any one of them is in conflict with all of the others. Again, this is because throw acts like goto. Once you decide where you are jumping to, you have no control over what happens next. And, if you let the user handle the error themselves, they have no easy way to handle the error gracefully since all of the state information is lost.

This is where restarts come in. In Common Lisp, catch is provided as two separate pieces: handlers and restarts. A handler is bound by the user of the library in order to specify what should happen when an exception is thrown and a restart is defined by the library in order to provide a recovery option to the user. If you are using a language that supports restarts, you could implement the first three options above as restarts. Then when a user is using the library, they will get to select which of those restarts they want to have run when an error occurs. If they do not want to use any of the restarts, they can run their own code instead. Here is the code for the file reading library, but reimplemented to support three different restarts, one for each of the first three ways to handle errors.

(defun ask (string)
  (princ string *query-io*)
  (read *query-io*))
 
(defun read-file (file)
  (with-open-file (in file :direction :input)
    (loop for line = (read-line in nil in)
          until (eq in line)
          when (restart-case (parse-integer line)
                 (use-value (value)
                   :report "Use a new value."
                   :interactive (lambda ()
                                  (list (ask "Value: ")))
                   value)
                 (skip-entry ()
                   :report "Skip the entry."
                   nil))
          collect it)))
 
(defun read-files (files)
  (loop for file in files
        when (restart-case (read-file file)
               (skip-file ()
                 :report "Skip the entire file."
                 nil))
        collect it))
 
;;; The three functions below are predefined
;;; handlers for the most common ways the user
;;; will interact with the restarts.
(defun skip-entry (c)
  (declare (ignore c))
  (invoke-restart 'skip-entry))
 
(defun skip-file  (c)
  (declare (ignore c))
  (invoke-restart 'skip-file))
 
(defun use-value-handler (value)
  (lambda (c)
    (declare (ignore c))
    (invoke-restart 'use-value value)))

A restart is defined with the macro restart-case, and invoked by the function invoke-restart. This is a bit of a simplification, but invoking a restart is effectively equivalent to jumping to the body of the restart from where the error was signaled. This means that all of the state stored on the stack before the restart was established is still available when the restart is invoked. This gives the user of the library much finer grained control over what happens when an error is thrown.

To specify what should happen, all the user needs to do is use the macro handler-bind. Handler-bind takes an error type and a handler (which should be a function) to call when an error of that type is thrown. The handler can then call invoke-restart in order to invoke one of the restarts provided by the library. As part of the library, there is one handler per restart provided, since those are the most common kinds of handlers. Here is what happens when each of the handlers are used when running the library on the two test files:

(handler-bind ((error #'skip-entry))
  (read-files files*))
 
=> ((5 10 15 20 25) (5 10 15 20 30 40))
 
(handler-bind ((error #'skip-file))
  (read-files files*))
 
=> ((5 10 15 20 25))
 
(handler-bind ((error (use-value-handler 0)))
  (read-files files*))
 
=> ((5 10 15 20 25) (5 10 15 20 0 30 40))

The really cool thing about restarts is what happens when the user doesnt handle the error. When this happens they will enter the Slime Debugger. From there they will be given a list of the restarts that are available to them and they will be able to invoke them as if the error had been handled in the first place! Here is what happens when a user doesnt handle the error, and then invokes the skip-entry restart on the fly:

 

 

Whats really cool about this is that this interactive restarting can use it to implement breakpoints! As I said in Part 1, Common Lisp provides breakpoints as a function break instead of as a feature of the editor. Here is code that could be used to implement break:

(defun break (&optional (format-control "Break")
              &rest format-arguments)
   (with-simple-restart (continue "Return from BREAK.")
     (let ((*debugger-hook* nil))
       (invoke-debugger
         (make-condition 'simple-condition
           :format-control   format-control
           :format-arguments format-arguments))))
   nil)

The code for break works by signalling an error while providing a continue restart. This means that as soon as the function break is called, you will enter the debugger with a restart available which will continue normal execution. Exactly what a breakpoint actually is.

Restarts are another fantastic part of debugging Common Lisp. They give you better control over what happens when an error occurs. And, if your code doesnt handle the error itself, you can still recover the process by using an interactive restart.

  1. A dynamic variable is basically a global variable that can be shadowed. When a dynamic variable is shadowed, any reference to it refers to the new binding. Once execution leaves the form that shadowed the dynamic variable, the dynamic variable reverts back to its previous binding.

Leave a Reply

Your email address will not be published. Required fields are marked *