In the HyperSpec entry for HANDLER-BIND
, it says that a handler can decline to handle a signal.
However, the linked glossary entry for decline to handle a signal is not very enlightening:
decline v. (of a handler) to return normally without having handled the condition being signaled, permitting the signaling process to continue as if the handler had not been present.
This definition begs and does not answer the question of what constitutes not returning normally?
Is there a full list of actions that constitute "handling" a signal?
I know from hands-on experience that INVOKE-RESTART
appears to fit this criterion. But is that the only way for a handler to "handle" a signal, or are there others?
I think to understand the true meaning of “handle”, you should consider how the condition system works under the hood. Kent Pitman's sample implementation, written during the standardization process, is a good place to start (even though it has kludgy stuff like essentially implementing an entire object system, since CLOS was not yet part of the language).
Roughly speaking, the action of handler-bind
is to set up a special variable, which we will call *handler-clusters*
, so that it holds lists of lists of (type . function)
pairs, corresponding to the list of bindings. A possible definition is
(defmacro handler-bind (bindings &body forms)
`(let ((*handler-clusters* (cons (list (mapcar #'(lambda (x) `(cons ',(car x) ,(cadr x)))
bindings))
*handler-clusters*)))
,@forms))
The signal
function, then, goes through the clusters; if it finds one for the correct condition type, it calls the associated function. Definition:
(defun signal (datum &rest arguments)
(let ((condition (coerce-to-condition datum arguments :default 'simple-condition))
(*handler-clusters* *handler-clusters*)) ; save current value
(when (typep condition *break-on-signals*)
(with-simple-restart (continue "Continue the signaling process")
(break "Break caused by *BREAK-ON-SIGNALS*")))
(loop for cluster := (pop *handler-clusters*) do
(loop for binding in cluster do
(when (typep condition (car binding))
(funcall (cdr binding) condition)))))
nil)
where coerce-to-condition
is a function that deals with “condition designators”. A subtle point is that we can't simply (loop for cluster in *handler-clusters* do …)
, because if, during the call of a handler, a condition of the same type as that which is being handled is signaled, the handler would be called recursively, which is probably not desirable. Thus the previous value is saved and we destructively pop
the cluster off.
Now, remember that Common Lisp allows closures over block
names and tagbody
tags. That is, after the definitions
(defvar *transfer-control*)
(defun weird (function)
(tagbody
(go :start)
:tag
(print 'transferred)
(return-from weird)
:start
(setf *transfer-control* (lambda () ; captures the tag :tag
(go :tag)))
(funcall function)))
a form like
(weird (lambda () (funcall *transfer-control*)))
is allowed, and will print 'transferred
. The control transfer happens, in a sense, outside of the lexical scope of the tagbody
; the ability to (go :tag)
has “escaped” its enclosing scope. (It would be an error to funcall *transfer-control*
after weird
has returned, because the dynamic extent of the tagbody
has been exited.)
All this is to say that calling an ordinary Common Lisp function can cause a transfer of control, instead of returning a value. Calling *transfer-control*
does nothing but unwind the dynamic environment up to the point of the tagbody
, and then jumps to :tag
. The function doesn't “return” in the usual sense of the term, because the evaluation of the expression in which it is embedded will be abruptly stopped, never to resume. (With weird
and *transfer-control*
, we've defined a primitive substitute for catch
and throw
that simply transfers control but doesn't convey a value at the same time. To see definitions of tagbody
, block
, and catch
in terms of each other, see Henry Baker's “Metacircular Semantics for Common Lisp Special Forms”.)
Therefore, when signal
calls the handler for a condition, two things can happen:
signal
and unwinding the stack to the place to which control was transferred.signal
will continue looking for a handler until reaching the end of *handler-clusters*
or until another handler transfers control. This is called “declining”.(In a way, it can also do neither or both, by, for instance, calling signal
on another condition. The specification calls this deferring.)
For example, the hyperspec gives a sample expansion for handler-case
. The form
(handler-case form
(type1 (var1) . body1)
(type2 (var2) . body2) ...)
becomes (ignoring problems of variable capture)
(block return-point
(let ((condition nil))
(tagbody
(handler-bind ((type1 #'(lambda (temp)
(setq condition temp)
(go :handler-tag-1)))
(type2 #'(lambda (temp)
(setq condition temp)
(go :handler-tag-2)))
...)
(return-from return-point form))
:handler-tag-1
(return-from return-point (let ((var1 condition)) . body1))
:handler-tag-2
(return-from return-point (let ((var2 condition)) . body2))
...)))
(I've rewritten the hyperspec's code to be more readable, although unhygienic, and I also fixed an error in the original.)
As you can see, the handlers established by handler-case
unconditionally transfer control if called. Thus handler-case
handlers definitely “handle” conditions.
Restarts are implemented in a very similar way, by restart-bind
setting up the dynamic environment and invoke-restart
using it to call a function. Because restarts are just functions, they need not transfer control, and so calling invoke-restart
is not always an act of “handling”, although it is if the restart was established by restart-case
or with-simple-restart
—or, of course, if the restart transfers control.