Wednesday, May 24, 2006

Better handling of undefined words; restarts

Everybody who tries Factor has run into this situation:
  \ layout see
An unhandled error was caught:

Parsing <interactive>:1
\ layout see
^
"Not a number"

:s :r :c show stacks at time of error
:get ( var -- value ) accesses variables at time of error
:error starts the inspector with the error
:cc starts the inspector with the error continuation

Oops! You forgot to use the right vocabulary -- if you know which one, otherwise its another call to apropos:
  USE: gadgets-layouts
\ layout see
IN: gadgets-layouts : layout
dup gadget-relayout? [
f over set-gadget-relayout? dup layout* dup
layout-children
] when drop ;

This is somewhat irritating, and the error message ("Not a number") is not helpful at all. It arises because of how the Factor parser works; first it looks up each token in the vocabulary search path, and if it does not find a word named by this token, it attempts to parse it as a number.

Now after my latest changes, this situation is handled more gracefully:
  \ layout see
An unhandled error was caught:

Parsing :1
\ layout see
^
"No word named layout found in current vocabulary search path"

The following restarts are available:
0 :res Use the word IN: gadgets-layouts : layout
:s :r :c show stacks at time of error
:get ( var -- value ) accesses variables at time of error
:error starts the inspector with the error
:cc starts the inspector with the error continuation
0 :res
IN: gadgets-layouts : layout
dup gadget-relayout? [
f over set-gadget-relayout? dup layout* dup
layout-children
] when drop ;

After the restart is invoked, the relevant vocabulary is automatically added to the search path, and parsing continues. If more than one word with the given name is defined in the dictionary, a number of restarts are offered, one for each possible vocabulary.

Here is how it works behind the scenes:

The condition ( error restarts -- restart ) word signals a recoverable error. The error parameter is the actual error message; it is wrapped inside a special condition object, together with the restarts parameter (more on it in a second) and the current continuation. The restarts parameter is an association list, associating a string description of a restart to an object. If the user chooses to restart from this error, the object associated with the chosen restart will be pushed on the stack, and execution will resume after the point where condition was called.

Here is an example:
: restart-test
"This is broken" {
{ "Indeed" t }
{ "I disagree" f }
} condition
"You choose " write . ;
restart-test

Running this example will yield the following error:

An unhandled error was caught:

"This is broken"

The following restarts are available:
0 :res Indeed
1 :res I disagree
:s :r :c show stacks at time of error
:get ( var -- value ) accesses variables at time of error
:error starts the inspector with the error
:cc starts the inspector with the error continuation

Invoking either restart using 0 :res or 1 :res will rewind execution and push a boolean true or false on the stack. The portion of the word definition after the call to condition now executes; it prints the received boolean on the stack.
  0 :res
You choose t

The problem with this approach is that any exception handlers further up the chain might have already closed streams and other external resources before the restart is invoked. In this case the restart will not function properly. What CL does is not execute any clean up handlers before the user picks a restart; this involves two trips up the catch stack, one to collect restarts and possibly present a debugger, and another one to call the relevant cleanup hooks, if any, depending on what restart is chosen. I'll investigate this further and pick a good solution. In any case I don't intend to use this feature as extensively as CL uses restarts, so I hope to avoid hairy situations.

1 comment:

Anonymous said...

I think what CL does is find the condition handler (which is probably something like a Lisp special/global variable, dynamically scoped), and invoke it. So all this happens *on top* of the current stack and execution.

Whatever the invoked restart returns is used in the program where the exception happened. The default condition handler is the debugger, so if nothing else is registered, the system will call the debugger to return a value. That value is then used. It's interesting that all this doesn't require any stack magic (like longjmp() or throw), but only a function call in case an error happens.

So I think you could also just lookup some condition handler (or the debugger) and have it return a value to the error-throwing continuation.