Next section: Specification
OverviewThe Dylan exception facility is object-based. It uses calling semantics but also provides terminating handlers. It provides formal recovery.
Conditions are a dynamically bound, type-based mechanism for finding and invoking functions. When an exceptional situation occurs, it is indicated to the calling code by signaling a condition. The condition facility locates an applicable handler and calls it. An applicable handler is one that matches the signaled condition by type and by (an optional) test function associated with the handler. The condition system is simply a way for a signaler and a handler to be introduced to each other. Once they have met, they communicate through an ordinary function call.
Like any function, the called handler either returns some values or takes a non-local exit. Either way, the handler has handled the condition, and the act of signaling is completed. A dynamic handler also has the option of declining to handle the condition, passing the buck to the next applicable handler, by calling a next-handler function passed to the dynamic handler as an argument. A default handler cannot decline because it is always the last applicable handler. Every signaled condition is handled, because the system ensures that there is always an applicable default handler.
The following terms describe portions of the dynamic state of execution. These terms come from the stack model of function calling.
- outside stack
- The state existing just before the handler was established
- signaling unit
- The conceptual program component that includes the form that signaled the condition and does not include the form that established the handler. This informal concept provides a notion of where the interface boundary between the signaler and the handler lies.
- middle stack
- The state existing just before the signaling unit was called, minus the outside stack
The simplest way to handle a condition is to terminate the signaling unit by taking a non-local exit established in the outside stack. The convenient exception clause of the block statement is provided for this purpose.
- inside stack
- The state existing just before signaling occurred, minus the middle stack and outside stack
A less common handling style is to terminate the signaling unit by taking a non-local exit established in the middle stack, leaving the handler in force.
Instead of terminating, a handler can recover, returning control to the signaling unit. This can be done either by returning values that the signaling unit will understand or by taking a non-local exit established in the inside stack. Returning or exiting can be done directly, or a more formal recovery mechanism called restarting can be used. The advantage of the formal mechanism is the advantage of any formal interface: it provides more assurance that the handler and the signaling unit agree on the meaning of what they are doing and provides some isolation of the handler from names and data representations internal to the signaling unit.
Returning values or non-locally exiting directly, rather than restarting, is a less formal mechanism with an increased likelihood of accidentally violating the protocol or contract between a signaler and a handler. Nonetheless it is allowed, in the spirit of freedom and because it is the primitive upon which restarting is built. When the direct mechanism is used, it should be used with due care.
A handler restarts by signaling a restart. This is the formal recovery mechanism. Any values needed for recovery are passed in the restart (that is, in initialization arguments that the restart remembers either in slots or in any other way it chooses). The restart is handled by a restart handler which either returns or takes a non-local exit. If the restart handler returns some values, signal returns those values and the handler that called signal also returns them. The call to signal from the signaling unit that signaled the original condition returns the same values, and the signaling unit recovers as directed by those values. If the restart handler was established by the signaling unit, then it was established in the inside stack and typically takes a non-local exit in the inside stack. It is just as valid to invoke a restart handler established in the outside or middle stack by a program component other than the signaling unit.
For every condition class there should be a "recovery protocol" that defines the meaning of handling by returning, the meaning of the values returned, and which restart handlers are supposed to be established by the signaling unit. The recovery protocol tells the handler what to expect from the signaler. For many condition classes, this is the empty protocol: handling by returning isn't allowed, and no particular restart handlers are provided. In this case only handling by terminating is possible. (Terminating might be accomplished by signaling a restart whose handler was established in the outside or middle stack, or by an ordinary non-local exit.) The recovery protocol for a subclass should be compatible with the recovery protocol of a superclass. That is, a handler that applies the superclass's recovery protocol, when the condition actually signaled was a direct instance of the subclass, should operate correctly.
An example recovery protocol for a hypothetical <unbound-slot> condition could be: returning a value uses that value as if it had been the contents of the slot; a restart handler for <new-value> is available; <new-value> has initialization arguments value:, the value to use, and permanent:, #t to store the value into the slot or #f (the default) to leave the slot unbound.
At present, no formal mechanism is provided for describing recovery protocols; they are left to the documentation of a condition class. Introspective functions are provided for discovering which recovery facilities are actually available, but this is different from (and sometimes is a superset of) the recovery facilities guaranteed by a recovery protocol always to be available.
The debugger is the condition handler of last resort which receives control if no program-provided handler handles a serious condition. (This is true even if the "debugger" provided cannot analyze or intervene in the execution of programs but can only abort or restart them. The debugger might be merely a "core dumper," a "bomb box," or something similar.) An interactive debugger ought to offer the user the ability to signal any restart for which a restart handler is applicable and to return if the condition's recovery protocol allows it. This could, for example, be done with a menu titled "Recovery."