On the implementation of unwind-protect (part 2) (2015 February 19 bis)

In a previous entry (see On the implementation of unwind-protect (2015 February 13)) I discussed the mechanism of unwind protection as implemented by Vicare, without showing many examples. This is to be remedied here. This section is about what’s currently in the master branch, not the latest Vicare release.

CONFESSION Some of this weblog’s entries are written by cheating: big portions of text and code are small reorganisations of Vicare’s documentation and test suite programs.

When using the mechanism:

If the bodacious answer to both the decisions is “We trust”: we can nest at will uses of the with-unwind-protection syntax:

(define (outer)
  (with-unwind-protection
      (lambda (why)
        (outer-unwind-handler))
    (lambda ()
      (do-something-useful)
      (inner))))

(define (inner)
  (with-unwind-protection
      (lambda (why)
        (inner-unwind-handler))
    (lambda ()
      (do-some-other-useful-thing))))

(outer)

beware: unwind handlers are evaluated in the dynamic environment of the use of with-unwind-protection.

Otherwise we must avoid error handlers to cause the execution flow to cross an unwind protection boundary; we can do it by using the standard guard syntax:

(define (outer)
  (push-compensation-thunk
    (lambda ()
      (outer-unwind-handler)))
  (do-something-useful)
  (inner))

(define (inner)
  (push-compensation-thunk
    (lambda ()
      (inner-unwind-handler)))
  (do-some-other-useful-thing))

(with-compensations
  (with-blocked-exceptions
    (lambda ()
      (guard (E ((type-one-error? E)
                 (handle E))
                (else
                 (do-something-dammit!!!)))
        (outer)))))

or by using the try syntax defined by Vicare:

(define (outer)
  (push-compensation-thunk
    (lambda ()
      (outer-unwind-handler)))
  (do-something-useful)
  (inner))

(define (inner)
  (push-compensation-thunk
    (lambda ()
      (inner-unwind-handler)))
  (do-some-other-useful-thing))

(with-compensations
  (with-blocked-exceptions
    (lambda ()
      (try
          (outer)
        (catch E
          ((&type-one)
           (handle E))
          (else
           (do-something-dammit!!!)))))))

beware: compensation handlers are evaluated in the dynamic environment of the use of with-compensations4.

Tracking the evaluation flow

To follow how the unwind protection mechanism intertwines itself with the protected code, we need a tracking mechanism. In this section we use the with-result api exported by the library (vicare checks); it works like this:

(import (vicare)
  (only (vicare checks)
        with-result
        add-result))

(with-result
  (add-result 1)
  (add-result 2)
  3)
⇒ (3 (1 2))

(with-result
  (add-result 1)
  (add-result 2)
  (values 3 4))
⇒ (3 4 (1 2))

we wrap the forms to track in a use of with-result, then, inside the form, we call add-result at “interesting points”; with-result returns a list whose head items represent the returned values and whose last item represents the FIFO queue of values to which add-result was applied. Very rough, but simple.

Prelude

In all the sample code we have to assume the following prelude:

(import (vicare)
  (only (vicare checks)
        with-result
        add-result))

and we refer to ?thunk and ?unwind-handler as with the following roles:

(with-unwind-protection ?unwind-handler    ?thunk)
(with-exception-handler ?exception-handler ?thunk)

Let’s get on with the examples.

Normal evaluation

When no exceptions are raised and no escape procedures are called, the evaluation just goes; the ?unwind-handler is evaluated after ?thunk.

(with-result
  (with-unwind-protection
      (lambda (why)
        (add-result 'out))
    (lambda ()
      (add-result 'in)
      1)))
⇒ (1 (in out))

If we call an escape procedure from ?thunk, this is what happens:

(with-result
  (call/cc
      (lambda (escape)
        (with-unwind-protection
            (lambda (why)
              (add-result 'unwind-handler))
          (lambda ()
            (add-result 'body)
            (escape 1))))))
⇒ (1 (body))

we see that the unwind protection handler is not called. This is correct! We escaped from the dynamic extent of the call to ?thunk by calling a raw escape procedure; not calling the unwind protection handler in this situation allows us to implement coroutines on top of continuations. Obviously, there is the downside that a raw escape might skip releasing resources; there is nothing we can do about it.

Reacting to non–continuable exceptions

Continuable exceptions are raised with the standard raise function. The only way to react to a raised non–continuable exception is to intercept it with an exception handler and, from its body, to call an escape procedure to exit the dynamic environment that caused the exception:

(call/cc
    (lambda (escape)
      (with-exception-handler
          (lambda (E)
            (escape E))
        (lambda ()
          (raise 1)))))
⇒ 1

(call/cc
    (lambda (escape)
      (with-exception-handler
          escape
        (lambda ()
          (raise 1)))))
⇒ 1

by tracking the evaluation flow we can verify that (as mandated by r6rs): raising an exception, by itself, does not cause an exit from the dynamic extent of the call to ?thunk, it is calling the escape procedure that causes the exit:

(with-result
  (call/cc
      (lambda (escape)
        (with-exception-handler
            (lambda (E)
              (add-result 'exception-handler)
              (escape E))
          (lambda ()
            (dynamic-wind
                (lambda ()
                  (add-result 'in-guard))
                (lambda ()
                  (add-result 'body)
                  (raise 1))
                (lambda ()
                  (add-result 'out-guard))))))))
⇒ (1 (in-guard body exception-handler out-guard))

saving a continuation and calling the escape procedure from the exception handler is what the standard guard syntax does; but guard does more than this.

So let’s add an unwind protection handler:

(with-result
  (call/cc
      (lambda (escape)
        (with-exception-handler
            (lambda (E)
              (add-result 'exception-handler)
              (escape E))
          (lambda ()
            (dynamic-wind
                (lambda ()
                  (add-result 'in-guard))
                (lambda ()
                  (with-unwind-protection
                      (lambda (why)
                        (add-result 'unwind-handler))
                    (lambda ()
                      (add-result 'body)
                      (raise 1))))
                (lambda ()
                  (add-result 'out-guard))))))))
⇒ (1 (in-guard body exception-handler out-guard))

we see that ?unwind-handler is not called; this is the same as the previous example of escaping from ?thunk. How do we cause a call to the unwind handler? By using the standard guard syntax, which is specially interfaced with the unwind protection mechanism:

(with-result
  (guard (E (else
             (add-result 'guard-else)
             E))
    (dynamic-wind
        (lambda ()
          (add-result 'in-guard))
        (lambda ()
          (with-unwind-protection
              (lambda (why)
                (add-result 'unwind-handler))
            (lambda ()
              (add-result 'body)
              (raise 1))))
        (lambda ()
          (add-result 'out-guard)))))
⇒ (1 (in-guard body out-guard
      in-guard unwind-handler out-guard
      guard-else))

this is what happens (brace yourself):

  1. The dynamic extent of the call to ?thunk is entered.
  2. The exception is raised.
  3. The exception handler installed by guard is applied to the raised object.
  4. The dynamic extent of the call to ?thunk is exited.
  5. It is determined by guard that the else clause must be evaluated.
  6. The dynamic extent of the call to ?thunk is re–entered.
  7. ?unwind-handler is called.
  8. The dynamic extent of the call to ?thunk is re–exited and terminated.
  9. The else clause is evaluated.

The following example has guard evaluate a non–else clause, which makes the mechanism even more clear:

(with-result
  (guard (E ((begin
               (add-result 'guard-test-1)
               (string? E))
             (add-result 'guard-expr-1)
             E)
            ((begin
               (add-result 'guard-test-2)
               (fixnum? E))
             (add-result 'guard-expr-2)
             E)
            (else
             (add-result 'guard-else)
             E))
    (dynamic-wind
        (lambda ()
          (add-result 'in-guard))
        (lambda ()
          (with-unwind-protection
              (lambda (why)
                (add-result 'unwind-handler))
            (lambda ()
              (add-result 'body)
              (raise 1))))
        (lambda ()
          (add-result 'out-guard)))))
⇒ (1 (in-guard body out-guard
      guard-test-1 guard-test-2
      in-guard unwind-handler out-guard
      guard-expr-2))

it is the non–false result from evaluating a guard test expression that causes the termination of the dynamic extent of the call to ?thunk.

Reacting to continuable exceptions

Everything we discussed for non–continuable exceptions applies unchanged to continuable exceptions when we do not actually continue. A continuable exception is raised with raise-continuable and it is a way to resume the execution of a chunk of code after raising an exception. This example shows the basic mechanism:

(with-exception-handler
    (lambda (E)
      (+ E 2))
  (lambda ()
    (raise-continuable 1)))
⇒ 3

if the exception handler returns to the caller, rather than calling an escape procedure: raise-continuable returns, too.

If we continue after raising a continuable exception, the evaluation flow is not different from a normal evaluation:

(with-result
  (with-exception-handler
      (lambda (E)
        (add-result 'exception-handler)
        (+ E 2))
    (lambda ()
      (with-unwind-protection
          (lambda (why)
            (add-result 'unwind-handler))
        (lambda ()
          (add-result 'thunk-in)
          (begin0
              (raise-continuable 1)
            (add-result 'thunk-out)))))))
⇒ (3 (thunk-in exception-handler thunk-out unwind-handler))

We can see a significant difference when we: raise a continuable exception in ?thunk; go through a guard with no else, which re–raises the continuable exception; execute an exception handler; return to ?thunk; perform normal a return:

(with-result
  (with-exception-handler
      (lambda (E)
        (add-result 'exception-handler)
        (+ 2 E))
    (lambda ()
      (guard (E ((begin
                   (add-result 'guard-test)
                   (error? E))
                 (add-result 'guard-error)
                 E))
        (with-unwind-protection
            (lambda (why)
              (add-result 'unwind-handler))
          (lambda ()
            (dynamic-wind
                (lambda ()
                  (add-result 'in-guard))
                (lambda ()
                  (add-result 'thunk-in)
                  (begin0
                      (raise-continuable 1)
                    (add-result 'thunk-out)))
                (lambda ()
                  (add-result 'out-guard)))))))))
⇒ (3 (in-guard thunk-in out-guard
      guard-test
      in-guard exception-handler
      thunk-out out-guard unwind-handler))

when a guard with no else clause is used and all the guard’s test expressions evaluate to #f: the exception is re–raised with raise-continue. ?unwind-handler is called only after the evaluation flow has re–entered ?thunk and performed a normal return.

Use with loop syntaxes

The unwind protection mechanism has special integration with the loop syntaxes defined by (vicare); if an unwind protection syntax is used in the body of a loop as defined by do, while, until, for and break or continue are used in the body forms: the unwind handlers are evaluated correctly.

We have to remember that break and continue are implemented by escaping continuations, so they have to be specially interfaced with unwind protection, otherwise the ?unwind-handler is not called.

This example shows breaking out of a while syntax; the body and the unwind handler are evaluated only once; exit is never called.

(define x 3)
(define y #f)
(while (positive? x)
  (with-unwind-protection
      (lambda (why)
        (set! y #t))
    (lambda ()
      (-- x)
      (break)
      (exit))))
x ⇒ 2
y ⇒ #t

This example shows using continue in a while syntax; the body and the unwind handler are evaluated 3 times; exit is never called.

(define x 3)
(define y 0)
(while (positive? x)
  (with-unwind-protection
      (lambda (why)
        (++ y))
    (lambda ()
      (-- x)
      (continue)
      (exit))))
x ⇒ 0
y ⇒ 3

Use with returnable bodies

The unwind protection mechanism has special integration with the returnable syntax defined by (vicare); if an unwind protection syntax is used in the body of returnable and return is used in the ?thunk: ?unwind-handler is evaluated correctly. We have to remember that return is implemented by an escaping continuation.

(define y #f)
(returnable
  (with-unwind-protection
      (lambda (why)
        (set! y #t))
    (lambda ()
      (return 1))))
⇒ 1
y ⇒ #t

Use with coroutines

Using the unwind protection mechanism with coroutines is fine; coroutines use yield to save the current continuation, give control to the next coroutine and come back later; this mechanism does not cause the ?unwind-handler evaluation.

The following sample code finishes with the given return values and lines printed:

(define (print template . args)
  (apply fprintf (current-error-port) template args)
  (yield))

(define a #f)
(define b #f)
(define c #f)

(concurrently
  (lambda ()
    (unwind-protect
        (begin
          (set! a 1.1)
          (print "unwind-protect sub 1.1: ~a\n" a)
          (set! a 1.2)
          (print "unwind-protect sub 1.2: ~a\n" a)
          (set! a 1.3)
          (print "unwind-protect sub 1.3: ~a\n" a))
      (set! a 1.4)))
  (lambda ()
    (unwind-protect
        (begin
          (set! b 2.1)
          (print "unwind-protect sub 2.1: ~a\n" b)
          (set! b 2.2)
          (print "unwind-protect sub 2.2: ~a\n" b)
          (set! b 2.3)
          (print "unwind-protect sub 2.3: ~a\n" b))
      (set! b 2.4)))
  (lambda ()
    (unwind-protect
        (begin
          (set! c 3.1)
          (print "unwind-protect sub 3.1: ~a\n" c)
          (set! c 3.2)
          (print "unwind-protect sub 3.2: ~a\n" c)
          (set! c 3.3)
          (print "unwind-protect sub 3.3: ~a\n" c))
      (set! c 3.4))))

(values a b c)
⇒ 1.4 2.4 3.4
-| unwind-protect sub 1.1: 1.1
-| unwind-protect sub 2.1: 2.1
-| unwind-protect sub 1.2: 1.2
-| unwind-protect sub 3.1: 3.1
-| unwind-protect sub 2.2: 2.2
-| unwind-protect sub 1.3: 1.3
-| unwind-protect sub 3.2: 3.2
-| unwind-protect sub 2.3: 2.3
-| unwind-protect sub 3.3: 3.3

Footnotes

(4)

Compensation handlers could be evaluated in the dynamic environment of the call to push-compensation-thunk, by using with-current-dynamic-environment. The current implementation of compensations does not save the dynamic environment, because doing it is more resource consuming.