This note implements a prototype for a condition system with calling and exiting handlers. It is available as a package. The package requires the dynamic variable package.
Exiting handlers provide a structured exception handling mechanism
much like the one in Java. Calling handlers allow the error
option and the warning mechanism to be handled as part of the system,
and also allow for the creation of programmable recovery mechanisms.
Once loaded, the code in this package will take over handling of
errors, both those signaled with stop
and internal ones. This is
accomplished using some hooks that have been added to errors.c
for
this purpose. These hooks are temporary and will most likely be
removed once a new error handling system has been finalized. So this
code requires at least R 1.3, but may stop working (and no longer be
needed) with later versions of R.
There are a couple of rough edges. One is that internal errors in
this package could disable the new exception handling mechanism and
return to the standard one. If this happens, evaluating the
expression EnableErrorHooks()
should reinstate the new system.
To make it easier to identify when this has occurred, the new system
pre-pends an underscore to the error message, as in
> stop("A") _Error: AThe underscore can be turned off by setting the
add.error.underscore
option to false:
> options(add.error.underscore=FALSE) > stop("A") Error: AA proper internal implementation would avoid this issue, but it is too early for that. Adding another hook in the
longjmp
code would
also prevent this, but that seemed excessive.
A second issue is the integration with restart
and browser
.
For the most part these should now behave as onemight expect, (to the
extent that it is clear what is expected) since error-related jumps
are set to stop at intervening frames that have had restart
called
on them. But there may be some wrinkles here.
Conditions are unusual situations that might occur and should be addressed in some way. Errors are one example, situations that require a warning are another.
A condition system allows handlers for different kids of conditions to be registered. When an unusual situation occurs, code can signal an appropriate condition. The condition system is then responsible for finding and invoking an appropriate handler for the condition.
Condition handlers come in two flavors: exiting and calling. Exiting
handlers are like catch
clauses in a Java try
/catch
block:
dynamic state is unwound and control is transferred back to the
context where the handler was established (the try
/catch
block). Thus a non-local transfer of control (a longjmp
in C
terms) occurs before the handler code is executed. Calling handlers
are like UNIX signal handlers. They are called in the context of the
code that signaled the condition, much like an ordinary function call.
Most errors will eventually need to be handled by an exiting handler, since continuing after an error is usually not a good idea. Warnings on the other hand are often benign and should therefore typically use a calling handler. But even for errors calling handlers are useful: If you want to use a handler to enter the browser at the point where an error occurs, then a calling handler is needed.
Exiting handlers are primarily used for handling exceptions. The
mechanism implemented here is quite similar in many ways to Java's
mechanism. Exceptions are objects inheriting from the abstract class
exception
. The class simple.exception
is the class currently
used by stop
and all internal error signals. The constructor by
the same name takes a string describing the exception as argument and
an optional call and returns a simple.exception
object.
> simple.exception("bad foo") <simple.exception: bad foo> > simple.exception("bad foo", quote(foo())) <simple.exception in foo(): bad foo>
The function stop
has been modified to accept exception objects in
addition to strings as its argument:
> stop(simple.exception("bad foo")) _Error: bad foo > stop(simple.exception("bad foo", quote(foo()))) _Error in foo() : bad foo
The function try.catch
is used to establish exiting handlers. Its
usage is
try.catch(expr, ..., finally = NULL)It evaluates its expression argument in a context where the handlers provided in the
...
argument are available. Handlers are
specified as
name = funwhere
name
specifies an exception class and fun
is a function
of one argument, the condition that is to be handled. When an
exception is signaled, the most recently established handler that
matches the exception (for which the exception inherits from the
specified class) is chosen, control transfers back to the
try.catch
expression, the handler function is called, and the
value returned by the handler function is returned by the
try.catch
call.
As an example, here the handler catches the exception signaled with
stop
and returns it:
> try.catch(stop("A"), exception = function(e) e) <simple.exception in try.catch(stop("A"), exception = function(e) e): A>A
finally
clause, if provided, will be evaluated before the
try.catch
call returns:
> try.catch(stop(simple.exception("A")), exception = function(e) e, + finally = print("B")) [1] "B" <simple.exception: A> > try.catch("A", exception = function(e) e, finally = print("B")) [1] "B" [1] "A"
The handler calls and the finally
expression are evaluated in the
context in which try.catch
was called; that is, the handlers
supplied to the current try.catch
call are not active
during these evaluations.
Using try.catch
we can define a function ignore.errors
that is
essentially the equivalent of try
:
<establishing handlers>= (U->) [D->] ignore.errors <- function(expr) try.catch(expr, exception = function(e) e)
Definesignore.errors
(links are to index).
For example,
> ignore.errors(1+2) [1] 3 > ignore.errors(ts(1:2) + 1:3) <simple.exception in ts(1:2) + 1:3: time-series/vector length mismatch>Lazy evaluation is critical in making this simple definition work.
The exception hierarchy is contained in the condition hierarchy, which also includes warnings:
condition / | \ / | \ / | \ / | \ simple.condition exception warning | | simple.exception | | simple.warningThe
condition
class is analogous to the Java Throwable
class.
Exiting handlers can also be used with non-exception throwables, but calling handlers are probably more useful there. The calling handlers system is very close to the Common Lisp approach. The Dylan approach seemed a bit cleaner at first but there are too many problems with it, at least for an interactive language like R.
Conditions are signaled by signal.condition
, and calling handlers
get established by with.handlers
. A simple example:
<example>= [D->] f <- function() { n <- 0 h <- function(c) { n <<- n + 1 cat("handler call", n, "\n") } with.handlers(for (i in 1:5) signal.condition("a condition"), simple.condition = h) }
produces
> f() handler call 1 handler call 2 handler call 3 handler call 4 handler call 5
The internal warning mechanism has been modified to signal a
warning
condition, so we can define a function to suppress
warnings for a particular computation as
<establishing handlers>+= (U->) [<-D->] muffle.warnings <- function(expr) with.handlers(expr, warning = function(w) {})
Definesmuffle.warnings
(links are to index).
For example,
> { warning("A"); 1+2 } [1] 3 Warning message: A > muffle.warnings({ warning("A"); 1+2 }) [1] 3
We could also use a calling handler to enter the browser on warnings:
<browse on warnings>= browse.on.warnings <- function(expr) with.handlers(expr, warning=function(w) browser())
Browse[1]> browse.on.warnings({ warning("A"); 1+2 }) Called from: h$handler(c) Browse[2]> cont [1] 3Calling handlers are pushed on a stack as they are established. When a calling handler is invoked, the handler stack for the call of the handler is the portion of the handler stack below where the handler was found. A handler can therefore pass control to another handler established below it by re-signaling the condition. If a calling handler returns, then the returned value is returned from the signal call.
With calling handlers we can also make available a rich structure for programmable recovery mechanisms. These will be called resets. Dylan and Common Lisp use the term restarts, but for us that would cause some confusion. The mechanism described here is based on the Common Lisp mechanism.
One reset that will always be available is the abort
reset. This
reset can be invoked by calling abort()
. The default handler does
a jump to top level, but will stop at any intervening restarts. The
default error handlers call abort
; establishing a new abort
reset will therefore intercept their transfer of control.
The functions find.reset
and compute.resets
can be used to
locate available resets. find.reset
takes a reset name and an
optional condition as arguments. The first reset matching the name
and condition, if supplied, is returned. For example, we can find the
first available abort
reset with
> find.reset("abort") <reset: abort >
compute.resets
takes an optional condition and returns a list of
resets applicable to the condition (or all resets if no condition is
specified):
<example>+= [<-D->] > compute.resets() [[1]] <reset: abort >
compute.resets
could be used by browser or, in a GUI framework, by
a menu for choosing a reset to invoke.
Resets can be invoked with invoke.reset
. This takes a reset name
or a reset object, as returned by find.reset
for example, and any
other arguments needed by the reset handler, transfers control to the
point where the handler was established, and calls the handler with
the specified arguments. Thus the abort
function is just a
convenient shorthand for
invoke.reset("abort")or
invoke.reset(find.reset("abort"))
Resets are established using with.resets
. This is called as
with.resets(expr, name1 = spec1, name2 = spec2, ...)The
spec
values can take several forms. The can be a function of
any number of arguments, which is used as the handler. They can be a
string, which is used as a message along with a handler that ignores
its arguments and returns NULL
. Or they can be a list with any of
the following named fields:
handler
test
message
message
field of
resets returned by find.reset
and compute.resets
. The default
function returns TRUE
for all conditions.
restarts.honored
restart
called on them. The default is FALSE
, but it
is TRUE
for the default abort
reset.
interactive
invoke.restart
. This is used by
invoke.restart.interactively
that could be called from a menu of
available restarts in a GUI framework.
Here is an an outline of how this framework might be used. Suppose we are writing a function maximizer. To allow for flexible recovery when the function causes an error we might do something like this:
<resets example>= myopt <- function(x, fun) { repeat { with.resets(return(do.opt(x, fun)), restart.opt = function(new.x) x <<- new.x) } } do.opt <- function(x, fun) { ... fval <- with.resets(with.handlers(fun(x), exception = function(e) signal.condition(optfun.error(e, x))), use.value = function(val) val) ... }
With a call like
with.handlers(myopt(x, fun), optfun.error = function(e) browser())we would enter the browser when calling the function to be optimized creates an error. From the browser we could then interactively decide to return a particular value, say 3, from the call with
invoke.reset("use.value", 3)or we could restart the optimization at a new initial value with
invoke.reset("restart.opt", new.x = ...)Alternatively, this could be handled programatically with something like
with.handlers(myopt(x, fun), optfun.error = function(e) { if (e$x < 0) invoke.reset("use.value", 3) else invoke.reset("restart.opt", abs(rnorm(1))) })
browser
to take an
optional condition argument that represents the condition, if any,
that triggered the browser call.
y/n/c
options given by q()
, and the
internal exit code, could be handled with resets and conditions as
well.
with.calling.resets
as the interface.
abort
.
restart
Functionrestart
function is currently the primitive building block for
error handling mechanisms. When called, it marks the frame of its
function for intercepting certain transfers of control. [Currently
the frame that is restarted is the one where the restart
expression is evaluated. This is probably not what we want when a
restart
ends up in a promise.]
Conceptually there seem to be two ways to fit restart
into this
condition system:
restart
inserts an exiting exception handler into the
handler stack that swallows the exception. Thus every function can be
viewed as having a body that looks like this:
try.catch(body, exception = function(e) if (! restart.called) stop(e))
restart
inserts an abort
reset into the reset stack, so
the body of a function looks like
with.resets(body, abort = function() if (! restart.called) abort())
Currently, in both R and Splus the error
option is called even if
a restart
frame is on the stack---only the jump to top level is
affected. This seems consistent with the second approach. This
package is kind of in between. Following either would require an
internal implementation.
Implementing either approach will complicate fully integrating the
condition mechanism. The existence of restart
in its current form
also complicates the internal evaluation mechanism and makes byte code
compilation harder. Since anything that can be done with restart
can be done (better) with exception handling, it seems like a good
idea to consider eliminating restart
entirely.
To allow existing code using restart
to be easily converted, we
could provide a mechanism something like
<possible restart
changes>= [D->]
restartable <- function(expr) {
restart.called <- FALSE
assign("restart", function() { restart.called <<- TRUE },
env = parent.frame())
repeat
with.resets(return(eval(substitute(expr), env = parent.frame())),
abort = function() if (! restart.called) abort())
}
Definesrestartable
(links are to index).
An interpretation that makes restart
insert an exception handler
would use try.catch
instead of with.resets
. The
eval(substitute(...
construct is needed since the expression is
potentially evaluated more than once. Perhaps a test function should
be added that only makes the abort reset visible if it is active.
Using this mechanism, a function with a body that uses restart
could then be re-written as
function(...) restartable(body)For example,
<example>+= [<-D->] f<-function(x, y = TRUE) { restart() if (y) { y <- FALSE stop("A") } else x }
<example>+= [<-D] new.f<-function(x, y = TRUE) restartable({ restart() if (y) { y <- FALSE stop("A") } else x })
To insure that code using restart
is changed, we could define
restart
in the base package as
<possible restart
changes>+= [<-D]
restart <- function()
stop(paste("restart no longer supported.\n",
"convert to using the exception handling system or",
"use `restartable'")
Definesrestart
(links are to index).
<simpcond.R>= <global variables> <call with current continuation> <handler stack management> <invoking handlers> <signaling conditions> <establishing handlers> <condition objects> <default handlers> <internal error conversion> <resets> .First.lib <- function(lib, pkg) { library.dynam(pkg, pkg, lib) require(dynvars) <global variable initialization> EnableErrorHooks() }
<simpcond.c>= #include "Rinternals.h" <declarations for hooks in errors.c> <ReturnOrRestart
definition> <JumpToToplevel
definition> <EnableExceptionHooks
definition> <PrintDeferredWarnings
definition> <GetTraceback
definition> <SetErrmessage
definition> <InternalWarningCall
definition>
<NAMESPACE>= import(dynvars) export(default.handler, default.handler.warning, default.handler.exception) export(simple.exception, simple.condition, simple.warning) export(signal.condition, stop, warning) export(try.catch, with.handlers, muffle.warnings, ignore.errors) export(abort, with.resets, invoke.reset, find.reset, compute.resets)
callcc
. This function is called as callcc(fun)
where fun
is a function of one argument. callcc
calls this function with
one argument, an exit function. If the exit function is not used in
the body of fun
, then the result returned by callcc
is the
result returned by fun
. Calling the exit function has the effect
of returning immediately from the callcc
call with the argument to
the exit function as the return value of the callcc
call. This
implementation only allows the exit function to be used within the
body of fun
, which makes it like a Dylan block
; Scheme's call
with current continuation is quite a bit more general.
We can almost implement what we need in pure R code by using a combination of environments and lazy evaluation. A pure R implementation would look like this:
<pure R implementation of call with current continuation>= callcc <- function(fun) { make.thrower <- function(expr) function() expr value <- NULL; thrower <- make.thrower(return(value)) k <- function(v) { value <<- v thrower() } fun(k) }
Definescallcc
(links are to index).
Some examples:
> callcc(function(k) 1) [1] 1 > callcc(function(k) k(1)) [1] 1 > callcc(function(k) {k(1); 2}) [1] 1 > callcc(function(k) {on.exit(cat("A\n")); k(1); 2}) A [1] 1 > callcc(function(k) {try(k(1)); 2}) [1] 1
The final example illustrates a problem for using this pure R approach
for error handling: try
is implemented with restart
, and
restart
is supposed to catch errors but nor return
's. Since
we use return
to implement the jump, we jump straight through the
restart
frame.
There does not seem to be a pure R solution to this, so there is now a hook available (at lest temporarily) that will handle this. The hook is provided by a C function declared as
<declarations for hooks in errors.c>= (U->) [D->] void R_ReturnOrRestart(SEXP val, SEXP env, Rboolean restart);
DefinesR_ReturnOrRestart
(links are to index).
At the moment this declaration is not in any header files, so we need
to add it to our sources. This function takes the value to return and
the environment indicating the call to return from as arguments. If
the third argument is true, then the jump will stop at a restarted
call if there is one on the stack ahead of the target. Otherwise
restarted calls are ignored, as by return
. We can define
.Call
interfaces to these two settings:
<ReturnOrRestart
definition>= (U->)
SEXP DoReturnOrRestart(SEXP val, SEXP env)
{
R_ReturnOrRestart(val, env, TRUE);
return R_NilValue;
}
SEXP DoReturn(SEXP val, SEXP env)
{
R_ReturnOrRestart(val, env, FALSE);
return R_NilValue;
}
DefinesDoReturn
,DoReturnOrRestart
(links are to index).
Now we can modify callcc
to allow exit functions to take an
additional argument that specifies whether restarts on the stack are
to be honored or ignored:
<call with current continuation>= (<-U) callcc <- function(fun) { env <- environment() k <- function(v, restarts.honored = FALSE) { if (restarts.honored) .Call("DoReturnOrRestart", v, env) else .Call("DoReturn", v, env) } fun(k) }
Definescallcc
(links are to index).
Some examples:
> callcc(function(k) {try(k(1)); 2}) [1] 1 > callcc(function(k) {try(k(1, T)); 2}) [1] 2
handler.stack
.
<global variables>= (<-U) [D->] handler.stack <- NULL ## place holder for .First.lib
Defineshandler.stack
(links are to index).
<global variable initialization>= (<-U) [D->] handler.stack <<- dynamic.variable()
Defineshandler.stack
(links are to index).
The handler stack is managed as a linked list. An internal implementation could use one cons cell per handler.
<handler stack management>= (<-U) [D->] add.to.handler.stack <- function(handler, class, exit, stack) { list(handler = handler, class = class, exit = exit, next.handler = stack) }
Definesadd.to.handler.stack
(links are to index).
Default handlers can be added to the handler stack with
add.default.handler
.
<handler stack management>+= (<-U) [<-D] add.default.handler <- function(handler, class) handler.stack(add.to.handler.stack(handler, class, NULL, handler.stack()))
Definesadd.default.handler
(links are to index).
NULL
then the handler is a calling handler. It is called with the
handler stack bound to the rest of the handler stack below the handler
called. If the exit function is not NULL
then the handler is
exiting. The exit function is used to transfer control to the
try.catch
call where the handlers was established. Restarts on
the stack will be honored if the condition signaled is an exception
(this also includes stopping the transfer at a browser). For calling
handlers we must re-enable the internal error processing hooks just
before calling the handler. For exiting handlers the hooks should
ideally be re-enabled after the jump, but we need to do it here in
case the jump is intercepted by a restarted call. This minimizes the
chance of recursion; with an internal implementation this can be done
to eliminate the chance of recursion entirely.
<invoking handlers>= (<-U) [D->] handle.condition <- function(c) { h <- handler.stack() if (is.null(h)) FailsafeErrorHandler(c) while (! is.null(h)) if (inherits(c, h$class)) break else h <- h$next.handler if (is.null(h)) { EnableErrorHooks() my.stop(no.condition.handler.exception(c)) #**** } if (is.null(h$exit)) dynamic.bind({ EnableErrorHooks() h$handler(c) }, handler.stack = h$next.handler) else { restarts.honored <- inherits(c, "exception") result <- list(throw = TRUE, handler = h$handler, condition = c) EnableErrorHooks() h$exit(result, restarts.honored) } }
Defineshandle.condition
(links are to index).
The fail-safe error handler should ideally be implemented internally so
that transfer of control via an internal call to abort
is
guaranteed to happen. It will only be reached if the default
exception handler fails.
<invoking handlers>+= (<-U) [<-D] FailsafeErrorHandler <- function(c) { errcat("Error: error in default exception handler\n") EnableErrorHooks() abort() }
DefinesFailsafeErrorHandler
(links are to index).
<internal error conversion>= (<-U) [D->] errcat<- function(s) cat(s, file=stderr())
Defineserrcat
(links are to index).
<signaling conditions>= (<-U) [D->] signal.condition <- function(c) { if (! inherits(c, "condition")) c <- simple.condition(c) handle.condition(c) }
Definessignal.condition
(links are to index).
The stop
function needs to signal a condition but it must not
return. If the condition handler returns, we call abort
.
For now we'll define an internal version my.stop
as well
as redefining stop
.
<signaling conditions>+= (<-U) [<-D->] my.stop <- function(e, call. = TRUE) { if (! is.condition(e)) e <- simple.exception(e, if (call.) sys.call(1) else NULL) signal.condition(e) errcat("aborting ...\n") abort() } stop <- my.stop
Definesmy.stop
,stop
(links are to index).
The warning
function currently does not include a call.
argument (should it?) and seems to always include the call in its
message. Again, we'll define an internal version my.warning
and
use it to redefine warning
.
<signaling conditions>+= (<-U) [<-D] my.warning <- function(w) { if (! inherits(w, "warning")) w <- simple.warning(w, sys.call(1)) signal.condition(w) } warning <- my.warning
Definesmy.warning
,warning
(links are to index).
with.handlers
.
The definition is quite simple.
<establishing handlers>+= (<-U) [<-D->] with.handlers <- function(expr, ...) { stack <- handler.stack() handlers <- rev(list(...)) classes <- names(handlers) for (i in seq(along = handlers)) stack <- add.to.handler.stack(handlers[[i]], classes[i], NULL, stack) dynamic.bind(expr, handler.stack = stack) }
Defineswith.handlers
(links are to index).
Exiting handlers are established by try.catch
. A callcc
call
is used to obtain an exit function that will transfer control back to
the try.catch
call. Setting up the handlers is analogous to
with.handlers
. The result of the callcc
call will always be
wrapped in a list with a throw
element to distinguish a normal
return and a throw return. For an internal implementation this flag
could be passed as a (thread-local) global, a field in the context
structure, or the setjmp
return value. The rest of the result
list's fields depends on whether the result represents a normal return
or a throw to a handler. For a throw the result contains the handler
to call and the condition to call it with. The handler is called in
the handler context that exists outside the try.catch
call. The
finally
clause is handled by an on.exit
call (which will work
properly with recent changes to the R internals.
<establishing handlers>+= (<-U) [<-D] try.catch <- function(expr, ..., finally = NULL) { on.exit(finally) result <- callcc(function(k) { stack <- handler.stack() handlers <- rev(list(...)) classes <- names(handlers) for (i in seq(along = handlers)) stack <- add.to.handler.stack(handlers[[i]], classes[i], k, stack) dynamic.bind(list(throw = FALSE, value = expr), handler.stack = stack) }) if (result$throw) result$handler(result$condition) else result$value }
Definestry.catch
(links are to index).
Perhaps the finally
expression should be evaluated in a try
.
"condition"
.
<condition objects>= (<-U) [D->] is.condition <- function(c) inherits(c, "condition")
Definesis.condition
(links are to index).
Two generic functions are defined on condition objects.
condition.message
should return the message string associated with
a condition. condition.call
should return the call associated
with the condition, or NULL
if there is none. The print method
for conditions is defined in terms of these generic functions:
<condition objects>+= (<-U) [<-D->] print.condition <- function(c, ...) { msg <- condition.message(c) call <- condition.call(c) class <- class(c)[1] if (! is.null(call)) cat("<", class, " in ", deparse(call), ": ", msg, ">\n", sep="") else cat("<", class, ": ", msg, ">\n", sep="") } condition.message <- function(c) UseMethod("condition.message", c) condition.call <- function(c) UseMethod("condition.call", c) condition.message.condition <- function(c) c$message condition.call.condition <- function(c) c$call
Definescondition.call
,condition.call.condition
,condition.message
,condition.message.condition
,print.condition
(links are to index).
The signal.condition
function will convert non-condition arguments
to simple conditions by calling simple.condition
. Similarly,
stop
converts non-condition arguments to simple exceptions and
warning
makes simple warnings.
<condition objects>+= (<-U) [<-D->] simple.condition <- function(message, call = NULL) { class <- c("simple.condition", "condition") structure(list(message=as.character(message), call = call), class=class) } simple.exception <- function(message, call = NULL) { class <- c("simple.exception", "exception", "condition") structure(list(message=as.character(message), call = call), class=class) } simple.warning <- function(message, call = NULL) { class <- c("simple.warning", "warning", "condition") structure(list(message=as.character(message), call = call), class=class) }
Definessimple.condition
,simple.exception
,simple.warning
(links are to index).
The condition system uses one condition of its own, an exception for signaling unhandled conditions. This contains a field for recording the condition that did not have a matching handler.
<condition objects>+= (<-U) [<-D] no.condition.handler.exception <- function(c) structure(list(message = paste("no condition handler for", class(c)[1]), condition = c), class = c("no.condition.handler.exception", "exception", "condition"))
Defines"no.condition.handler.exception"
(links are to index).
default.handler
.
Defining methods for subtypes of exceptions and warnings allows the
default handling to be tuned somewhat.
<default handlers>= (<-U) [D->] default.handler <- function(e) { UseMethod("default.handler", e) }
Definesdefault.handler
(links are to index).
<global variable initialization>+= (<-U) [<-D->] add.default.handler(default.handler, "exception") add.default.handler(default.handler, "warning")
errorcall
and jump_to_toplevel
functions in errors.c
.
<default handlers>+= (<-U) [<-D->] default.handler.exception <- function(e) { call <- condition.call(e) message <- condition.message(e) op <- getOption("add.error.underscore") if (is.null(op) || op) us <- "_" else us <- "" if (is.null(call)) emsg <- paste(us, "Error: ", message, "\n", sep = "") else { dcall <- deparse(call) if (nchar(dcall) > 30) emsg <- paste(us, "Error in ", dcall[1], " :\n\t", message, "\n", sep = "") else emsg <- paste(us, "Error in ", dcall[1], " : ", message, "\n", sep = "") } seterrmessage(emsg) if (getOption("error.messages")) { errcat(emsg) PrintDeferredWarnings() } handler <- getOption("error") if (! is.null(handler)) eval(handler, R_GlobalEnv) else if (! interactive()) { errcat("Execution halted\n") q("no", 1, FALSE) # quit, no save, no .Last, status=1 } tb <- getTraceback() tb <- trim.traceback(tb) assign(".Traceback", tb, env = .GlobalEnv) abort() }
Definesdefault.handler.exception
(links are to index).
To make the traceback result a little cleaner we trim off some of the
leading stuff that represents the error handling code that is on the
stack. We trim down at least to the leading signal.condition
call. For calls generated by the internal error handling code we also
trim off the next two frames.
<default handlers>+= (<-U) [<-D->] trim.traceback <- function(t) { n <- length(t) pos <- NULL for (i in seq(along=t)) if (pmatch("signal.condition(", t[[i]], 0)) { pos <- i break } if (is.null(pos)) t else { if (pos < n - 1 && pmatch("my.stop(", t[[pos + 1]], 0) && pmatch("error.hook(", t[[pos + 2]], 0)) pos <- pos + 2 if (pos == n) NULL else t[(pos+1):n] } }
Definestrim.traceback
(links are to index).
Deferred warnings are printed by a hook into the internals provided in
errors.c
. This hook is temporary and hence not declared in the
header files, so we need to declare it here.
<default handlers>+= (<-U) [<-D->] PrintDeferredWarnings <- function() .Call("PrintDeferredWarnings")
DefinesPrintDeferredWarnings
(links are to index).
<declarations for hooks in errors.c>+= (U->) [<-D->] void R_PrintDeferredWarnings(void);
DefinesR_PrintDeferredWarnings
(links are to index).
<PrintDeferredWarnings
definition>= (U->)
SEXP PrintDeferredWarnings(void)
{
R_PrintDeferredWarnings();
return R_NilValue;
}
DefinesPrintDeferredWarnings
(links are to index).
The traceback is also generated by a hook function in errors.c
.
This hook allows us to exclude a specified number of frames on the top
of the stack, but it isn't clear if this is useful.
<default handlers>+= (<-U) [<-D->] getTraceback <- function(skip = 1) .Call("GetTraceback", as.integer(skip))
DefinesgetTraceback
(links are to index).
<declarations for hooks in errors.c>+= (U->) [<-D->] SEXP R_GetTraceback(int);
DefinesR_GetTraceback
(links are to index).
<GetTraceback
definition>= (U->)
SEXP GetTraceback(SEXP skip)
{
if (TYPEOF(skip) != INTSXP || LENGTH(skip) != 1)
error("bad skip argument");
return R_GetTraceback(INTEGER(skip)[0]);
}
DefinesGetTraceback
(links are to index).
Finally, the default handler needs to be able to place the error message in the internal error buffer (just for consistency with existing code---this can probably be dropped eventually, or at least it would need to be made thread-safe).
<default handlers>+= (<-U) [<-D->] seterrmessage <- function(s) .C("SetErrmessage", as.character(s))
Definesseterrmessage
(links are to index).
<declarations for hooks in errors.c>+= (U->) [<-D->] void R_SetErrmessage(char *s);
DefinesR_SetErrmessage
(links are to index).
<SetErrmessage
definition>= (U->)
void SetErrmessage(char **s)
{
R_SetErrmessage(*s);
}
DefinesSetErrmessage
(links are to index).
warningcall
to implement the default warning handler. We need to turn the hook
off around the call. If there is an error in the call, then the hooks
will be reset along with the error hook by the calls to
EnableErrorHooks
. There may be a flaw in this, but for now it
should do.
<default handlers>+= (<-U) [<-D] default.handler.warning <- function(w) { .Call("InternalWarningCall", condition.call(w), condition.message(w)) }
Definesdefault.handler.warning
(links are to index).
<InternalWarningCall
definition>= (U->)
SEXP InternalWarningCall(SEXP call, SEXP msg)
{
if (TYPEOF(msg) != STRSXP || LENGTH(msg) != 1)
error("invalid warning message");
R_SetWarningHook(NULL);
Rf_warningcall(call, "%s", CHAR(STRING_ELT(msg, 0)));
R_SetWarningHook(warnhook);
return R_NilValue;
}
DefinesInternalWarningCall
(links are to index).
EnableErrorHooks
.
<internal error conversion>+= (<-U) [<-D->] EnableErrorHooks <- function() { .Call("EnableExceptionHooks") }
DefinesEnableErrorHooks
(links are to index).
The hooks provided in errors.c
are declares as
<declarations for hooks in errors.c>+= (U->) [<-D->] void R_SetErrorHook(void (*hook)(SEXP, char *)); void R_SetWarningHook(void (*hook)(SEXP, char *));
DefinesR_SetErrorHook
,R_SetWarningHook
(links are to index).
Both hooks are installed by a common mechanism. They call back into R
using R functions called error.hook
and warning.hook
,
respectively.
<EnableExceptionHooks
definition>= (U->)
static void hook(SEXP fun, SEXP call, char *s)
{
SEXP expr, msg, qsym = install("quote");
PROTECT(msg = allocVector(STRSXP, 1));
SET_STRING_ELT(msg, 0, mkChar(s));
PROTECT(call = LCONS(qsym, LCONS(call, R_NilValue)));
expr = LCONS(msg, R_NilValue);
expr = LCONS(call, expr);
PROTECT(expr = LCONS(fun, expr));
eval(expr, R_GlobalEnv);
UNPROTECT(3);
}
static void errhook(SEXP call, char *s)
{
hook(install("error.hook"), call, s);
}
static void warnhook(SEXP call, char *s)
{
hook(install("warning.hook"), call, s);
}
SEXP EnableExceptionHooks(void)
{
R_SetErrorHook(errhook);
R_SetWarningHook(warnhook);
return R_NilValue;
}
DefinesEnableExceptionHooks
,errhook
,hook
,warnhook
(links are to index).
The R hook functions in turn just call my.stop
and my.warn
.
This is all quite a lot of overhead that could be avoided in an
internal implementation, but the only real issue is that it might
create problems if the error being signaled is about resource
exhaustion of some kind.
<internal error conversion>+= (<-U) [<-D] error.hook <- function(call, msg) my.stop(simple.exception(msg, call)) warning.hook <- function(call, msg) my.warning(simple.warning(msg, call))
Defineserror.hook
,warning.hook
(links are to index).
The reset mechanism needs to be able to jump to top level. The
.Call
interface for this is provided by JumpToToplevel
.
<declarations for hooks in errors.c>+= (U->) [<-D] void R_JumpToToplevel(Rboolean restart);
DefinesR_JumpToToplevel
(links are to index).
<JumpToToplevel
definition>= (U->)
SEXP JumpToToplevel(SEXP restart)
{
if (TYPEOF(restart) != LGLSXP || LENGTH(restart) != 1)
error("bad restarts.honored argument");
R_JumpToToplevel(LOGICAL(restart)[0]);
}
DefinesR_JumpToToplevel
(links are to index).
reset
with fields containing
all the settings for the reset.
<resets>= (<-U) [D->] make.reset <- function(name = "", handler = function(...) NULL, message = NULL, test = function(c) TRUE, interactive = function() NULL, restarts.honored = FALSE) { structure(list(name = name, handler = handler, message = message, test = test, interactive = interactive, restarts.honored = restarts.honored), class = "reset") } print.reset <- function(r) cat(paste("<reset:", r$name, ">\n")) is.reset <- function(x) inherits(x, "reset")
Definesis.reset
,make.reset
,print.reset
(links are to index).
Resets are maintained in a stack. The function add.to.reset.stack
creates the reset object, adds a name field to it, and also adds an
exit function. The reset is then linked on the front of the specified
stack.
<resets>+= (<-U) [<-D->] add.to.reset.stack <- function(spec, name, exit, stack) { if (is.function(spec)) reset <- make.reset(handler = spec) else if (is.character(spec)) reset <- make.reset(message = spec) else if (is.list(spec)) reset <- do.call("make.reset", spec) else stop("not a valid reset specification") reset$name <- name list(reset = reset, exit = exit, next.reset = stack) }
Definesadd.to.reset.stack
(links are to index).
The reset stack is maintained as a dynamic variable. The initial
stack contains a handler for abort
resets that jumps to top level,
but honors any restarts that might be on the stack.
<global variables>+= (<-U) [<-D] reset.stack <- NULL ## place holder for .First.lib
Definesreset.stack
(links are to index).
<global variable initialization>+= (<-U) [<-D] reset.stack <<- dynamic.variable( add.to.reset.stack(list(handler = function() {}, restarts.honored = TRUE), "abort", function(result, restarts.honored) .Call("JumpToToplevel", restarts.honored), NULL))
Definesreset.stack
(links are to index).
The function with.resets
for establishing (exiting) resets is
analogous to try.catch
. Since the number of arguments to the
handler is not known, we need to call it with do.call
. We have to
first store the handler in a variable, since do.call
does not
allow a computed function as its first argument---it has to be a
string.
<resets>+= (<-U) [<-D->] with.resets <- function(expr, ...) { result <- callcc(function(k) { stack <- reset.stack() specs <- rev(list(...)) names <- names(specs) for (i in seq(along = specs)) stack <- add.to.reset.stack(specs[[i]], names[i], k, stack) dynamic.bind(list(throw = FALSE, value = expr), reset.stack = stack) }) if (result$throw) { h <- result$handler do.call("h", result$args) } else result$value }
Defineswith.resets
(links are to index).
find.reset
walks down the reset stack looking for the first one
that matches the name and accepts the condition, if one is specified.
<resets>+= (<-U) [<-D->] find.reset <- function(name, cond = NULL) { r <- reset.stack() while (! is.null(r)) if (name == r$reset$name && (is.null(cond) || r$reset$test(cond))) { res <- r$reset res$exit <- r$exit return(res) } else r <- r$next.reset NULL }
Definesfind.reset
(links are to index).
Similarly, compute.restarts
walks down the sestart stack and
accumulates all elligible restarts into a list.
<resets>+= (<-U) [<-D->] compute.resets <- function(cond = NULL) { r <- reset.stack() val <- NULL while (! is.null(r)) { if (is.null(cond) || r$reset$test(cond)) { res <- r$reset res$exit <- r$exit val <- c(val, list(res)) } r <- r$next.reset } val }
Definescompute.resets
(links are to index).
invoke.restart
accpets either a string, which is passed to
find.restart
, or a reset as its first argument. The remaining
arguments, if any, are packed up as a list, along with the reset's
handler and a throw
flag, into a result list which is then passed
to the exit function stored in the reset object. The reset object's
restarts.honored
field determines whether the transfer of control
stops at intervening restarted call frames or not.
<resets>+= (<-U) [<-D->] invoke.reset <- function(r, ...) { if (! is.reset(r)) r <- find.reset(r) if (is.null(r$exit)) stop("calling resets not supported (yet)") result <- list(throw = TRUE, handler = r$handler, args = list(...)) r$exit(result, r$restarts.honored) }
Definesinvoke.reset
(links are to index).
The abort
function is just a simple shorthand for invoking an
abort
reset.
<resets>+= (<-U) [<-D->] abort <- function() invoke.reset("abort")
Definesabort
(links are to index).
The invoke.reset.interactively
function differs from
invoke.reset
only in the fact that it computes the arguments for
the reset handler by calling the reset's interactive
function.
<resets>+= (<-U) [<-D] invoke.reset.interactively <- function(r) { if (! is.reset(r)) r <- find.reset(r) if (is.null(r$exit)) stop("calling resets not supported (yet)") args <- r$interactive() result <- list(throw = TRUE, handler = r$handler, args = args) r$exit(result, r$restarts.honored) }
Definesinvoke.reset.interactively
(links are to index).
<tests>= .lib.loc <- c("lib",.lib.loc) library(simpcond) try.catch(1, finally=print("Hello")) e<-simple.exception("test exception") stop(e) try.catch(stop(e), finally=print("Hello")) try.catch(stop("fred"), finally=print("Hello")) try.catch(stop(e), exception = function(e) e, finally=print("Hello")) try.catch(stop("fred"), exception = function(e) e, finally=print("Hello")) muffle.warnings({my.warning("Hello"); 1})
Just to make sure I installed mindy
and ran this Dylan program:
<hello.dyl>= module: dylan-user define method main(name :: <string>, #rest arguments) let handler <error> = method (c, next) puts("handled the error\n"); error(c); end; error("an error"); end;
Compile and run gives:
luke@nokomis2 ~% mindycomp hello.dyl luke@nokomis2 ~% mindy -f hello.dbc handled the error handled the error ... handled the error Segmentation fault (core dumped)So their design really is hosed: if there is an error in a calling handler for <error> you blow out the top.
It looks like CL got this right (or at least more so than Dylan did).
EnableExceptionHooks
definition>: U1, D2
GetTraceback
definition>: U1, D2
InternalWarningCall
definition>: U1, D2
JumpToToplevel
definition>: U1, D2
restart
changes>: D1, D2
PrintDeferredWarnings
definition>: U1, D2
ReturnOrRestart
definition>: U1, D2
SetErrmessage
definition>: U1, D2