Next: , Up: stdlib exceptions   [Index]


5.7.1 Exceptions

This section describes Scheme’s exception–handling and exception–raising constructs provided by the (rnrs exceptions (6)) library.

Exception handlers are one–argument procedures that determine the action the program takes when an exceptional situation is signalled. The system implicitly maintains a current exception handler.

The program raises an exception by invoking the current exception handler, passing it an object encapsulating information about the exception. Any procedure accepting one argument may serve as an exception handler and any object may be used to represent an exception.

The system maintains the current exception handler as part of the dynamic environment of the program (see scheme basic dynamic extent).

When a program begins its execution, the current exception handler is expected to handle all &serious conditions by interrupting execution, reporting that an exception has been raised, and displaying information about the condition object that was provided. The handler may then exit, or may provide a choice of other options. Moreover, the exception handler is expected to return when passed any other non–&serious condition. Interpretation of these expectations necessarily depends upon the nature of the system in which programs are executed, but the intent is that users perceive the raising of an exception as a controlled escape from the situation that raised the exception, not as a crash.

Procedure: with-exception-handler handler thunk

handler must be a procedure and should accept one argument. thunk must be a procedure that accepts zero arguments. The with-exception-handler procedure returns the results of invoking thunk. handler is installed as the current exception handler for the dynamic extent (as determined by dynamic-wind) of the invocation of thunk.

Implementation responsibilities: The implementation must check the restrictions on handler to the extent performed by applying it as described when it is called as a result of a call to raise or raise-continuable. An implementation may check whether handler is an appropriate argument before applying it.

Syntax: guard (?variable ?cond-clause1 ?cond-clause2 …) ?body
Auxiliary Syntax: =>
Auxiliary Syntax: else

Each ?cond-clause is as in the specification of cond, baselib expressions derived cond. ‘=>’ and ‘else’ are the same as in the (rnrs base (6)) library.

Evaluating a guard form evaluates ?body with an exception handler that binds the raised object to ?variable and within the scope of that binding evaluates the clauses as if they were the clauses of a cond expression. That implicit cond expression is evaluated with the continuation and dynamic environment of the guard form. If every ?cond-clause’s ?test evaluates to #f and there is no else clause, then raise-continuable is re–invoked on the raised object within the dynamic environment of the original call to raise except that the current exception handler is that of the guard expression.

The final expression in a cond clause is in a tail context if the guard expression itself is.

Procedure: raise obj

Raise a non–continuable exception by invoking the current exception handler on obj. The handler is called with a continuation whose dynamic environment is that of the call to raise, except that the current exception handler is the one that was in place when the handler being called was installed. When the handler returns, a non–continuable exception with condition type &non-continuable is raised in the same dynamic environment as the handler.

Procedure: raise-continuable obj

Raise a continuable exception by invoking the current exception handler on obj. The handler is called with a continuation that is equivalent to the continuation of the call to raise-continuable, with these two exceptions:

  1. The current exception handler is the one that was in place when the handler being called was installed.
  2. If the handler being called returns, then it will again become the current exception handler.

If the handler returns, the values it returns become the values returned by the call to raise-continuable.

Miscellaneous examples:

(guard (con
         ((error? con)
          (if (message-condition? con)
              (display (condition-message con))
              (display "an error has occurred"))
          'error)
         ((violation? con)
          (if (message-condition? con)
              (display (condition-message con))
              (display "the program has a bug"))
          'violation))
  (raise
    (condition
      (make-error)
      (make-message-condition "I am an error"))))
-| I am an error
⇒ error

(guard (con
         ((error? con)
          (if (message-condition? con)
              (display (condition-message con))
              (display "an error has occurred"))
          'error))
  (raise
    (condition
      (make-violation)
      (make-message-condition "I am an error"))))
⇒ exception &violation

(guard (con
         ((error? con)
          (display "error opening file")
          #f))
  (call-with-input-file "foo.scm" read))
-| error opening file
⇒ #f

(with-exception-handler
  (lambda (con)
    (cond
      ((not (warning? con))
       (raise con))
      ((message-condition? con)
       (display (condition-message con)))
      (else
       (display "a warning has been issued")))
    42)
  (lambda ()
    (+ (raise-continuable
         (condition
           (make-warning)
           (make-message-condition
             "should be a number")))
       23)))
-| should be a number
⇒ 65

Discussion on a possible implementation

Given a single–thread program, the following implementation of with-exception-handler, raise and raise-continuable is compliant:

(import (except (rnrs)
                with-exception-handler
                raise
                raise-continuable
                guard))

(define default-error-port (current-error-port))

(define stack-of-handlers
  (list
   (lambda (raised-object)
     (display "unhandled exception:\n" default-error-port)
     (display raised-object default-error-port)
     (newline default-error-port)
     (when (serious-condition? raised-object)
       (exit -1)))
   (lambda args
     (exit -1))))

(define-syntax %with-popped-handler
  (syntax-rules ()
    ((_ ?handler . ?body)
     (let ((ell      stack-of-handlers)
           (?handler (car stack-of-handlers)))
       (dynamic-wind
           (lambda () (set! stack-of-handlers (cdr ell)))
           (lambda () . ?body)
           (lambda () (set! stack-of-handlers ell)))))))

(define (with-exception-handler handler thunk)
  (let ((ell stack-of-handlers))
    (dynamic-wind
        (lambda () (set! stack-of-handlers (cons handler ell)))
        thunk
        (lambda () (set! stack-of-handlers ell)))))

(define (raise-continuable raised-object)
  (%with-popped-handler handler
     (handler raised-object)))

(define (raise raised-object)
  (%with-popped-handler handler
     (handler raised-object)
     (raise
      (condition
       (make-non-continuable-violation)
       (make-message-condition
        "return from handler of non-continuable exception")))))

we see that no function rewinds the stack nor invokes a continuation; also, in raise, the recursive invocation raising a non–continuable violation can return to the caller if the handler returns.

The implementation of the guard syntax needs some reasoning to be understood; first, the skeleton of the transformation is a with-exception-handler syntax, the following form:

(guard (E ((alpha-condition? E)
           (do-alpha))
          ((beta-condition? E)
           (do-beta))
          ((condition-gamma-value E)
           => (lambda (value)
                (do-gamma value)))
          (else
           (do-else)))
  (form-a)
  (form-b))

is basically transformed to:

(with-exception-handler
    (lambda (E)
      (cond ((alpha-condition? E)
             (do-alpha))
            ((beta-condition? E)
             (do-beta))
            ((condition-gamma-value E)
             => (lambda (value)
                  (do-gamma value)))
            (else
             (do-else))))
  (lambda ()
    (form-a)
    (form-b)))

to make it compliant, we need to add to this transformation the correct handling of continuations and of the dynamic environment.

We need the mechanism explained step by step in what follows; given the form:

(alpha ... (beta ...))

in which the form beta is nested at some level in the form alpha, we want to answer the question: how do we evaluate the form beta so that it has the same continuation and dynamic environment of alpha?

We begin by understanding that this chunk of code:

(begin
  (form-a)
  (form-b))

can be rewritten in the following equivalent form, in which an anonymous function is created and just evaluated in place:

((lambda ()
   (begin
     (form-a)
     (form-b))))

or more explicitly:

(apply (lambda ()
         (begin
           (form-a)
           (form-b))))

we should understand that, from the point of view of the executed computation, the apply and begin forms have the same continuation.

The last version can be rewritten as:

(apply (call/cc
          (lambda (kont)
            (lambda () ;body
              (begin
                (form-a)
                (form-b))))))

in which the call/cc form evaluates to the anonymous function commented as ‘body’ which is then evaluated by apply; so we can say again that, from the point of view of the executed computation, the apply and begin forms have the same continuation. We notice that the escape procedure kont is not used.

Let’s consider the following transformation of the previous example and understand why it is equivalent:

(apply (call/cc
          (lambda (kont)
            (lambda () ;shell
              (kont (lambda () ;body
                      (begin
                        (form-a)
                        (form-b))))))))

the call/cc form evaluates to the anonymous function commented as ‘shell’ which is evaluated by apply; the kont escape procedure jumps back to the continuation of call/cc which is again apply and its return value is the anonymous function commented as ‘body’ which is evaluated by apply. So once again we can say that, from the point of view of the executed computation, the apply and begin forms have the same continuation.

As a final consideration, let’s modify the form as follows:

(apply (call/cc
          (lambda (kont)
            (lambda () ;shell
              (dynamic-wind
                  (lambda ()
                    (in-guard))
                  (lambda ()
                    (kont (lambda () ;body
                            (begin
                              (form-a)
                              (form-b)))))
                  (lambda ()
                    (out-guard)))))))

when the shell function is evaluated, the dynamic environment may be modified by the dynamic-wind form’s in-guard and out-guard, but the begin form is still evaluated with the same continuation and the same dynamic environment of apply: both in-guard and out-guard are evaluated before the body function is evaluated.

Now we can go back to the guard implementation and consider a guard form with else clause:

(guard (E ((alpha-condition? E)
           (do-alpha))
          (else
           (do-else)))
  (dynamic-wind
    (lambda () (in-guard))
    (lambda () (form-a))
    (lambda () (out-guard))))

we observed that the skeleton expansion of guard is:

(with-exception-handler
     (lambda (E)
       (cond ((alpha-condition? E)
              (do-alpha))
             (else
              (do-else))))
  (lambda ()
    (dynamic-wind
      (lambda () (in-guard))
      (lambda () (form-a))
      (lambda () (out-guard)))))

the report establishes that:

That implicit cond expression is evaluated with the continuation and dynamic environment of the guard form.

using the mechanism described above, we can expand the guard form as follows:

((call/cc
     (lambda (guard-kont)
       (lambda ()
         (with-exception-handler
             (lambda (E) ;shell
               (guard-kont
                (lambda () ;handler
                  (cond ((alpha-condition? E)
                         (do-alpha))
                        (else
                         (do-else))))))
           (lambda () ;body
             (dynamic-wind
                 (lambda () (in-guard))
                 (lambda () (form-a))
                 (lambda () (out-guard)))))))))

if the anonymous function commented as body raises an exception, the handler function commented as shell is applied to the raised object; the guard-kont escape procedure is used to jump to the continuation of call/cc and then the cond form is evaluated. This expansion is compliant. We notice explicitly that:

Now let’s consider a guard form without else clause:

(guard (E ((alpha-condition? E)
           (do-alpha))
          ((beta-condition? E)
           (do-beta)))
  (dynamic-wind
    (lambda () (in-guard))
    (lambda () (form-a))
    (lambda () (out-guard))))

applying the same expansion described above yields:

(apply
 (call/cc
     (lambda (guard-kont)
       (lambda ()
         (with-exception-handler
             (lambda (E) ;shell
               (guard-kont
                (lambda () ;handler
                  (cond ((alpha-condition? E)
                         (do-alpha))
                        ((beta-condition? E)
                         (do-beta))))))
           (lambda () ;body
             (dynamic-wind
                 (lambda () (in-guard))
                 (lambda () (form-a))
                 (lambda () (out-guard)))))))))

but the report establishes that:

If every ?cond-clause’s ?test evaluates to #f and there is no else clause, then raise-continuable is re–invoked on the raised object within the dynamic environment of the original call to raise except that the current exception handler is that of the guard expression.

so we use again the same mechanism and expand as follows:

(apply ;external apply
 (call/cc
     (lambda (guard-kont)
       (lambda ()
         (with-exception-handler
             (lambda (E) ;shell
               (apply ;internal apply
                 (call/cc
                     (lambda (reraise-kont)
                       (guard-kont
                         (lambda () ;handler
                           (cond ((alpha-condition? E)
                                  (do-alpha))
                                 ((beta-condition? E)
                                  (do-beta))
                                 (else
                                  (reraise-kont
                                   (lambda () ;reraise
                                     (raise-continuable E))))))
                       )))))
           (lambda () ;body
             (dynamic-wind
                 (lambda () (in-guard))
                 (lambda () (form-a))
                 (lambda () (out-guard)))))))))

if the body function raises an exception and one of the non–else clauses matches, everything works as above and the cond form is evaluated with the continuation of guard; but if the clause which matches is else: the reraise-kont escape procedure is used to jump back to the continuation of the internal call/cc which has a dynamic environment equal to the one of the original raise invocation; the return value of reraise-kont is the anonymous function commented as reraise, which is evaluated in place by the internal apply; the return value of raise-continuable becomes the argument of guard-kont.

We notice explicitly that:

As the last touch, let’s introduce a binding for the raised object as follows:

(apply ;external apply
 (call/cc
     (lambda (guard-kont)
       (lambda ()
         (with-exception-handler
             (lambda (D) ;shell
               (apply ;internal apply
                 (call/cc
                     (lambda (reraise-kont)
                       (guard-kont
                         (lambda () ;handler
                           (let ((E D))
                             (cond ((alpha-condition? E)
                                    (do-alpha))
                                   ((beta-condition? E)
                                    (do-beta))
                                   (else
                                    (reraise-kont
                                     (lambda () ;reraise
                                       (raise-continuable D)))))))
                         )))))
           (lambda () ;body
             (dynamic-wind
                 (lambda () (in-guard))
                 (lambda () (form-a))
                 (lambda () (out-guard)))))))))

this way we are sure to reraise the original raised object in the else clause even though the tests in the clauses may have modified the biding of E or its value.


Next: , Up: stdlib exceptions   [Index]