Lunar Programming Language

by David A. Moon
January 2017 - January 2018



Exception Handling

Exception handling is all about method selection based on the dynamic state of execution, not just on static state such as the type of a datum. When an exceptional condition occurs, the code that discovered the condition throws an exception datum of a specific type. The direct and indirect callers of that code catch that exception datum through a dynamically provided method.

Catch and Throw

The primitive mechanism for dynamic method selection is catch and throw.

Catch and throw are not a mechanism for early exit from execution, sometimes called "unwinding the stack." The primitive mechanism for that is exit functions; see Block Statement for details.

There is a sequence of catcher methods available at any given point in time. If there were threads, this sequence would be per-thread. A catch dynamically adds a catcher method to the front of this sequence for the duration of execution of an expression. When that execution ends, normally or by early exit, the method is dynamically removed.

The throw function (when called with one actual parameter) executes a method that was made dynamically available by catch. The method is not selected by the usual rules; instead throw invokes the first applicable method in the sequence of available catcher methods, with no consideration of method specificity or ambiguity.

As always, the result of the selected method is the result of throw, unless the selected method takes an early exit.

If throw does not find an applicable method, it calls missing_catch with the thrown datum as the sole parameter. Methods for missing_catch provide default handling of uncaught throws. Depending on the type of the thrown datum, the default could be a fatal error, or just to ignore it and return false as the result of throw.

A catcher method has two formal parameters, both required:

thrown is the datum that was thrown, the actual parameter to throw.

rethrow is a function that accepts one actual parameter and does the same thing as throw, except that it only considers catcher methods older than the current one. In other words, it only sees the portion of the sequence of available catcher methods after the portion that throw or an earlier rethrow already scanned for applicable methods.

The function available_catches takes no parameters and returns the sequence of available catcher methods. Its result type is sequence[method].

Catch Statement

A catch is represented as

defclass catch_expression(catcher expression, body expression) compound_expression
The result of catcher must be a method with two required formal parameters. During the execution of body, that method is in the sequence of available catcher methods and will be considered if throw is called. The result is the result of body.

As an optimization, the execution of catcher might be deferred until throw is called, so if no exception occurs catcher might never be executed. It should not have side-effects.

The usual syntax for a catch_expression is

$catch [ ^ ] { type_name [ "(" { destructuring & "," }* ")" ]
               [ description_string ] "=>" catcher_body & ^ }*
       ^^ "in:" body
Each type_name ... => catcher_body element defines one catcher method. The type of its first formal parameter thrown is type_name. The type of its second formal parameter rethrow is function. If description_string is present, it should be a string which is placed in the catcher method's description slot. The formal parameters thrown and rethrow are in scope in catcher_body. If destructurings are present, they are also in scope in catcher_body and their values come from destructuring thrown. See Destructuring .

Each catcher method passes the result of its catcher_body to an exit function that takes an early exit from the catch statement. Note that the stack is not unwound until the end of a catcher_body when it takes this early exit.

All catcher methods are available during the execution of body and of each catcher_body.

The result of the catch statement is the result of body unless a catcher method takes an early exit.

For example,

constant:
defclass mouse_click(button integer, x integer, y integer)

catch mouse_click(button, x, y) => do_mouse_command(button, x, y)
      error => false
  in: run_event_loop()

;;somewhere inside something called from run_event_loop
            throw mouse_click(mouse_button_pressed(), mouse_x(), mouse_y())

Exceptions

When an exception occurs during program execution it is useful to look up how to handle the exception dynamically, based on the current call stack. Lunar's catch and throw facility provides a mechanism for this.

The datum thrown to signal that an exception has occurred is always a member of the exception class. All direct and indirect members of the exception class have the automatic throw feature: Instead of producing the newly constructed datum as its result, the constructor calls the throw function, passing the newly constructed datum as the sole parameter, and the result of throw is the result of the constructor.

The exception class could have been defined by:

abstract: autothrow:
defclass exception

The error class is a subclass of exception used for exceptions that cannot be ignored. If throw yields a result instead of taking an early exit, another error will occur. No constructor for error or any subclass will ever yield a result.

The error class has one visible and destructurable slot, which contains an error message. This is a required formal parameter of the constructor.

The error class could have been defined by:

noreturn: constant:
defclass error(message string) exception
It inherits autothrow: from exception.

assert statement

$assert expression
causes an error to occur if the result of expression is false. It could have been defined by
defmacro assert expression =>
  def exp = string(expression)
  def msg = "The assertion \"$exp\" was violated."
  `if not $expression then assertion_violated_error($msg)`

defclass assertion_violated_error(message) error(message)

Exception Handlers

The catch statement can be used to establish an exception handler. For example,

catch error(message) => print("Fatal error: $message")
  in: do_something()
or
catch error(message) =>
          print("Fatal error: $message")
          rethrow(thrown)
  in: do_something()

Exception Restarts

Sometimes we want more sophisticated recovery from an exception than simply taking an early exit from a catch statement. Restart options can be made dynamically available based on the dynamic state of execution, using the same catch and throw mechanism. An exception handler, or a user acting through the debugger, selects an available restart and invokes it by constructing an instance of the restart's class, which automatically throws.

A catcher method that provides a restart option must be applicable to the class restart or a subclass, and should provide a method description. It should take an early exit from some appropriate block.

The restart class could have been defined by:

abstract: autothrow: noreturn:
defclass restart

A simple example:

defclass abort restart

catch abort "Abandon the whole thing" => false
  in:
    do_something()
    true

Another simple example:

defclass try_again restart

block exit: return
  while true
    catch try_again "Try it again" => 0
      in: return(do_something())

An example translated from the Common Lisp HyperSpec:

defclass my_restart(optional: value = false) restart

catch
  error => my_restart(7)
  my_restart(v) => v
  in: error("foo")
The result is 7. The body throws error, which is caught by the exception handler. The exception handler throws my_restart, which is caught by the restart handler. The restart handler takes an early exit from the catch statement and result is its value slot, in this case 7.

The function available_restarts takes no parameters and returns a keyed sequence of available restarts, filtered from the sequence of available catcher methods. Its result type is keyed_sequence[string, method]. The key is the description, defaulted to the restart class name if the catcher method has no description.

When there is a debugger, there will be a function interactive_restart that gives the debugger a way to get the slots for a restart through an interactive dialog with the user, get confirmation from the user, and then construct and throw the restart, or return false if cancelled. The details are TBD.

Pre-defined Exceptions

TODO list of built-in exceptions and their restarts


Previous page   Table of Contents   Next page



Creative Commons License
Lunar by David A. Moon is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Please inform me if you find this useful, or use any of the ideas embedded in it.
Comments and criticisms to dave underscore moon atsign alum dot mit dot edu.