Wednesday, April 6, 2011

exception handling with defmacro

I've started work on a coroutine/thread scheduler, code named "Doom".  Doing some socket I/O has finally  snapped my 'missing feature' threshold - I really need exception handling to do it right.

Here's my first attempt. The basic syntax is (try <body> except <exception-patterns>).

If this works, it'll be a great example of the huge advantages of the lisp syntax and macros - no changes made to the compiler to support it!

;; -*- Mode: Irken -*-

(include "lib/core.scm")
(include "lib/random.scm")

;; can we implement an exception system using the macro system?
;; XXX eventually we'll need to change call/cc to swap out exception
;;  handlers, which probably means making a 'modern' call/cc with
;;  dynamic-wind, etc.

(define (base-exception-handler exn)
  (error1 "uncaught exception" exn))

(define *the-exception-handler* base-exception-handler)

(define (raise exn)
  (*the-exception-handler* exn)

;; what kind of values do we want for exceptions, and how they're caught/compared?
;; python: started with strings, went to objects of the Exception class.
;; scheme: SRFI-12 includes something that looks like property lists?
;; sml: string refs? (in sml/nj according to CwC)
;; ocaml: declared exception datatypes.
;; [alternate for ocaml:
;;  sounds a little like my 'fourth idea']

;; my first temptation is to just use symbols, and stay simple

;; second idea is to use a record, which theoretically gives you the
;;   ability to attach other kinds of data, which could be very
;;   convenient. Example: (raise BadFD) => {exception='BadFD ...}

;; third idea: hardcore - define a global exception type, force the user to
;;   extend it.  [could be made easier by compiler hacks that allow you to
;;   extend the type anywhere in the source?].  Code would have to wildcard
;;   exceptions it doesn't know about?  [or is that the definition of handing
;;   it upstream?]

;; fourth idea: use records (or polymorphic variants) to define unique
;;   exception names *and* types.  For example: (raise (BadFD
;;   current-fd)) would type as {BadFD=int ...}  this would make it
;;   more like the SRFI-12 property-list thing (unless I misunderstand
;;   what SRFI-12 is all about).  Hmmm... maybe we could extend the
;;   exception handler by extending the record?

;; It'd be nice if the type system can tell us what exceptions are possible
;;   at any given point in the code... is this maybe impossible due to the
;;   dynamic (vs static/lexical) nature of exception handling?

;; the more I think about the 'fourth idea' the less reason I see to
;;  restrict the type of exceptions.  Syntax-wise, the following macro
;;  does not enforce any restrictions.  It's possible though that only
;;  the polymorphic-variant-scheme will survive type checking.

;; this is the first time I've tried to 'accumulate' pieces in a macro.
;; it's possible that there's a better idiom that I just haven't discovered yet.
;; should really look to see how the scheme folks do this (or do they just avoid
;; it all by wrapping everything in verbose s-expressions).

(defmacro try
  ;; done accumulating body parts, finish up.
  (try (begin body0 ...) <except> exn-match ...)
  -> (let (($old-hand *the-exception-handler*))
        (lambda ($exn)
          (set! *the-exception-handler* $old-hand)
          (match $exn with
            exn-match ...
            _ -> (raise $exn))))
       (let (($result (begin body0 ...)))
         (set! *the-exception-handler* $old-hand)
  ;; accumulating body parts...
  (try (begin body0 ...) body1 body2 ...) -> (try (begin body0 ... body1) body2 ...)
  ;; begin to accumulate...
  (try body0 body1 ...)                   -> (try (begin body0) body1 ...)

(define (random-barf)
  (if (= (logand (random) 1) 1)
      (raise (:OtherException 99))

(define (thing)
   (let loop ((n 0))
     (if (= n 100)
         (raise (:MyException 12))
           (loop (+ n 1)))))
   (:MyException value) -> value
   (:OtherException _)  -> 9


Here's what the macro expansion looks like for thing:

(define (thing)
  (let (($old-hand *the-exception-handler*))
       (lambda ($exn)
           (set! *the-exception-handler* $old-hand)
             (MyException OtherException)
             (1 1)
               ((m9 (%nvget (:MyException 0 1) $exn)))
               (let_subst (value m9) value))
             (%match-error #f))
            (raise $exn)))))
      (let (($result
                   (function loop (n) (if (= n 100) (raise (:MyException 12)) (loop (+ n 1))))))
               (loop 0))))
        (begin (set! *the-exception-handler* $old-hand) $result)))))


  1. A good history of exception handling in lisp:

  2. And of course that code doesn't actually *work* as an exception handler because it doesn't capture the continuation of the handler. 8^) details!

  3. Having trouble with the type safety of call/cc etc. This paper looks relevant:
    "A generalization of exceptions and control in ML-like languages"

  4. And the 'delimcc' library for OCaml talks about how to implement the operators described in that paper. I'm still trying to figure out why they need 'new_prompt' in addition to 'set x in ...'. Why not combine new_prompt and 'set x in ...'?

  5. The "catch me if you can" ocaml library has an interesting approach, that I could probably emulate with Irken. One aspect gives me pause: the type of exception-handling functions becomes annotated with the exceptions that it handles. Good/Bad/Ugly, this makes me nervous. Also, if you want to read that link you probably need to turn off javascript, otherwise the source boxes are unreadable. [tried 3 browsers].