On the implementation of unwind-protect (2015 February 13)

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

The macro unwind-protect has the purpose of releasing resources that must be allocated synchronously with respect to a chunk of code; its syntax is:

(unwind-protect
    ?body
  ?clean-up0
  ?clean-up
  ...)

and it can be used as:

#!vicare
(let ((port (open-file-output-port "file.ext")))
  (unwind-protect
      (put-bytevector port '#ve(ascii "ciao"))
    (close-output-port port)))

first we allocate a resource (in this case the port); then we use it in the ?body form; finally we release it in the ?clean-up forms. The ?clean-up forms are executed whether ?body performs a normal return or raises an exception. When ?body returns: the return value of unwind-protect is the return value of ?body.

It is clear that unwind-protect is a very useful operator: in a way or the other, a language must provide a way to release synchronous resources. We might be tempted to rely on the garbage collector finalisers (under Vicare: guardians), but:

There have been plenty of discussions about how to implement unwind-protect in Scheme and some proposals; none of them provided a definitive solution (where definitive means: problem solved and syntax included in the standard language). At present, Vicare’s master branch implements an unwind–protection mechanism that, at its core, is available through the built–in macro with-unwind-protection exported by the library (vicare); the syntax is:

(with-unwind-protection ?clean-up ?thunk)

where ?thunk performs the job and the procedure ?clean-up releases resources.

The above example would be implemented as:

#!vicare
(let ((port (open-file-output-port "file.ext")))
  (with-unwind-protection
      (lambda (why)
        (close-output-port port))
    (lambda ()
      (put-bytevector port '#ve(ascii "ciao")))))

The built–in macro unwind-protect is still available as simple wrapper around with-undind-protection:

(unwind-protect ?body ?cleanup0 ?clean-up ...)
→ (with-unwind-protection
        (lambda (dummy) ?cleanup0 ?clean-up ...)
      (lambda () ?body))

the built–in macros try and with-compensations are also implemented on top of with-unwind-protection.

The mechanism

So, how does it work? The idea is to define the concept of dynamic extent termination of a call to function; this concept is defined by Vicare’s unwind–protection mechanism and it is not a r6rs concept. To understand the unwind–protection mechanism we must understand the concepts “dynamic extent of a function call” and “dynamic environment”.

The procedure ?clean-up is called when the dynamic extent of the invocation of ?thunk terminates; dynamic extent termination is different from dynamic extent exiting as determined by dynamic-wind. When the execution flow exits the dynamic extent of a function call: such extent might also terminate, but not all the exits are also terminations.

In this discussion, we consider the syntax use:

(with-unwind-protection ?clean-up ?thunk)

the dynamic extent of a call to ?thunk is terminated, and so ?clean-up is invoked, when:

The dynamic extent of a call to ?thunk is not terminated, and so ?clean-up is not invoked, when:

About the termination of the dynamic extent of ?thunk, we must acknowledge that:

Things to notice:

The problem of the second exception does not haunt only Vicare; if I understand correctly, unwind-protect as defined by Common Lisp also presents cases where the clean–up forms may not be called.

How do we manage resources then?

The unwind–protection mechanism has the purpose of evaluating code when the dynamic extent of a function call terminates; it can be used to release synchronous resources: resources whose life must terminate when the dynamic extent of a function call terminates. Whenever possible, we must take care of handling exceptions with guard before they cross the unwind–protection boundary. So, with compensations, we might do:

(with-compensations
  (guard (E (?test ?expr)
            ...)
    (define ?resource-reference
      (compensate
          ?alloc
        (with
          ?release)))
    ...
    ?body0
    ?body
    ...))

or, if we like try:

(with-compensations
  (try
      (letrec
          ((?resource-reference (compensate
                                    ?alloc
                                  (with
                                    ?release)))
            ...)
        ?body0
        ?body
        ...)
    (catch E
      ((?condition-type)
        ?expr)
      ...)))