Exceptions by gjjur4356


                           Brigitte Pientka (based on CH 14 in Pierce)

                                           October 28, 2010

     Previously, we have seen how to extend our language with mutable references and considered
its impact on the typing rules and various theoretical properties such as type safety. In these notes,
we consider a different form of side-effect: exceptions
     Exceptions are important in practice, since they allow us to signal exceptional conditions. Some-
times, we can signal these conditions by returning an optional value. If all goes well, we return
SOME v otherwise NONE. In this set-up, NONE signals that things went wrong. However, there are
many situations where we may prefer to directly defer control to an exception handler which will
safely abort the program. Exceptions are also useful to take care of run-time exceptions such as
divison by zero.

1    Raising exceptions
We concentrate here on the fragment of MiniML which contains variables, functions and function
application, and extend it with an error construct.

                             Expressions e ::= x | λx : T.e | e1 e2 | error
                             Value v       ::= λx:T.e
   The term error when evaluated will completely abort evaluation of the term in which it ap-
pears. We hence add the following evaluation rules (we omit the premise and do not write the line
separating the premises from the conclusion when we have no premises):

       error e2    −→     error    E - APP- ERR 1       e1 e2 −→ e1 e2             v1 e2 −→ v1 e2
                                                                       E - APP 1                  E - APP 2
       v1 error    −→     error    E - APP- ERR 2          e1 −→ e1                   e2 −→ e2
       (λx.e) v2   −→     [v2 /x]e E - BETA

     The main design question is how to formally treat “abnormal termination”. Here, we decide
to follow a very simple idea: let error itself be the result of a program that aborts. The rules
E - APP- ERR 1 and E - APP- ERR 2 capture this behavior.
     E - APP- ERR 1 says that if we encounter the term error while trying to reduce the left-hand side of
an application to a value, we immediately yield error as the result. Similarly, E - APP- ERR 2 says that
if we encounter and error while we are working on reducing the argument of an application to a
value, we should abandon work on the application and immediately yield error .
     Note, we have deliberately not added error to the values, but only to the terms. This guarantees
that there will be no overlap between the left-hand sides of the E - BETA and the E - APP- ERR 2 rule,
i.e. there is no ambiguity.

    For example, to evaluate (λx : int.0) error , we can only use the rule E - APP- ERR 2 and step to
error . Since error is not a value, the rule E - BETA does not apply.
    Similarly, to evaluate ((λx : int.λy : int.x + y) 0) error means we will wait and first evaluate
(λx : int.λy : int.x + y) 0 to λy : int.0 + y, and then subsequently evaluate (λy : int.0 + y) error
which will return error .
    Note, that some computation may diverge before producing an error. For example, (fix f.f) error
will diverge instead of aborting. Or more generally, let foo= fix f.λx:int.f x and we are trying to
evaluate foo error = (fix f.λx:int.f x) error . This will diverge.
    These conditions ensure that evaluation will remain deterministic.
    Finally, let us consider the typing rule for error . Since, we may want to raise an exception in
any context, the term error is allowed to have any type.
                                                        T- ERROR
                                        Γ   error : T
    For example in the expression λx:bool.x) error , the sub-expression error has type bool. But in
the expressions (λx:bool.x) (error true), the sub-expressions error will have type bool → bool.
    This flexibility comes with a price. we will loose the property that every well-typed term has a
unique type. Clearly, error can have many types.
    Previously, we resolved this issue by giving a type annotation; so one may think this is a possible
way to re-establish type uniqueness. Unfortunately, this is not as simple. Annotating error with
a type will break type preservation. Let us write error [T ] for annotating error with type T . The
intention is that error [T ] has the specified type T . Consider the following expression:

                                (λx:int.x) ((λy:bool.5) (error [bool]))
    The sub-expression ((λy:bool.5) (error [bool])) is well-typed and has type int. However, it will
evaluate to error [bool] which has type bool! Hence, types are not preserved.
    There are two solutions to re-cover type uniqueness: the first is to add subtyping where we
assign error the type bot which can be promoted to any other type if necessary. The other option is
to extend our language with parametric polymorphism. In such a language, we can give error the
type ∀α.α which can be instantiated to any other type. Both of these tricks allow infinitely many
possible types for error to be represented compactly by a single type.
    Although our language does not have type uniqueness, we can prove type preservation as usual.
The progress is a little bit more challenging: progress states that whenever e is well-typed, the either
e is a value or there is another step we can take. Clearly, with our new error construct there is a
third possibility: we may encounter error . We hence must generalize our progress statement as
Theorem 1.1 (Progress).
If Γ e : T then either e is a value or e = error or there exists an expression e s.t. e −→ e .

2    Handling exceptions
The evaluation rules for error can be thought of as unwinding the call stack where we abandon
pending function calls until the error is propagated all the way to the top-level.
     In most languages, it is not only possible to raise an error explicitly, but also to defer control,
i.e. what happens when an error occurred. We will add the construct try e1 with e2 . It will allow

us to catch an error. The idea is if evaluating e1 produces an error, we continue with the expression
e2 . Otherwise evaluating e1 produces a value v1 , and we simply return v1 .

                                                                             e1 −→ e1
                                  E -TRY-V                                                       E -TRY
         try v1 with e2 −→ v1                                   try e1 with e2 −→ try e1 with e2
                                     E -TRY- ERROR
         try error with e2 −→ e2

    The rule E -TRY says evaluate e1 until we have either reached a value v1 or an error . If we
have try error with e2 proceed with evaluating e2 . If we have try v1 with e2 , we simply succeed
returning v1 .
    The typing rule for try e1 with e2 simply follows its operational semantics:

                                         Γ      e1 : T Γ e2 : T
                                         Γ      try e1 with e2 : T
    The type safety property remains unchanged.

3    Exceptions carrying values
A well-known mechanism from SML or OCaml are exceptions which propagate a value. This allows
us to propagate an error together with for example a string which describes the error; or we may
want to propagate some partial result we have produced when the error occurred.
    Hence, we will replace the error construct with a more general construct raise e. Note, in
ML-like languages we also have the ability to give exceptions different names. Here we ignore the
name issue and simply have one kind of exception, which is raised using the construct raise e.
    Let us revisit the small-step semantics for value-carrying exceptions.

                                         E - APP- RAISE 1                    e1 e2 −→ e1 e2
           (raise v1 ) e2 −→ raise v1                                                       E - APP 1
                                                                                e1 −→ e1
                                         E - APP- RAISE 2
           v1 (raise v2 ) −→ raise v2                                        v1 e2 −→ v1 e2
                                                                                            E - APP 2
                                          E - RAISE - RAISE                     e2 −→ e2
         raise (raise v1 ) −→ raise v1
                                             E - BETA
                                                                                 e −→ e       E - RAISE
                  (λx.e) v2 −→ [v2 /x]e                                    raise e −→ raise e

                                          E -TRY-V
                  try v1 with e2 −→ v1
                                                                                  e1 −→ e1
                                               E -TRY- RAISE                                          E -TRY
        try (raise v1 ) with e2 −→ e2 v1                             try e1 with e2 −→ try e1 with e2

    We mostly replace error with raise e or raise v. The value propagating happens in the rule
              when evaluating try (raise v1 ) with e2 , we apply e2 to the value v1 . Hence, we
think of the expression e2 as a function which is waiting for a result v1 to be passed to in order to

continue. Note that we only propagate value not expressions with an exceptions. If we attempt to
evaluate a try raise e with e , there rules will force us to evaluate e before we pass this information
along to the expression e .
   Finally, let us revisit the typing rules:

                                Γ    e : Texn     Γ   e1 : T Γ e2 : Texn → T
                            Γ       raise e : T       Γ try e1 with e2 : T
    Here we think of e2 occurring in try e1 with e2 as a function which is waiting for a value of type
Texn before it will produce a value of type T . We have left the type Texn deliberately unspecified.
Restricting Texn to int will essentially force as to propagate only integers as error values. This
corresponds for example to the errno convention used by the Unix operating system: each system
call returns a numeric ”error code” with 0 signaling success and other values reporting various
exceptional conditions.
    We can Texn also to be string. This avoids looking up error numbers in tables and allows us to
propagate a more declarative error message describing the the error. This extra flexibility comes
with a cost: error handlers may now have to parse these strings to find out what happened.
    We can keep the ability to pass more information, while avoiding string parsing if we define
Texn to be a variant type (i.e. a data-type). For example,
Texn = < divideByZero : Unit ,
             overflow:      Unit,
             fileNotFound: String,
             non-exhaustive match : String,
    This allows us to distinguish between different error handlers using case-expressions. In addi-
tion, different exceptions can carry different types of additional information.
    The problem with this alternative is that it is rather inflexible: it requires us to fix the set of
exceptions in advance. This leaves no room for the programmer to add user-defined exceptions.
    This idea can be however refined to allow for extra flexibility using an extensible variant type.
ML uses this idea, providing a single extensible variant type called exn.
    In ML, we declare an exceptions as follows:
  exception    Error of string
    This introduces a tag Error together with what value the exception will be propagating; in
the example, it propagates strings. More generally, exception l of T is the syntax for exceptions
where l is a tag and T is the type of value to be propagated.
    We can raise an exception in ML using the tag l and an expression e: raise l(e) .
    This can be easily understood as a series of tagging operations by simply looking up the declared
type of the exception tag l.
raise l(t)    =   raise (<l = t > as Texn
   Similarly, the ML try construct can be desugared using our simple try construct together with
try e with l(x) ⇒ e’
   is desugared to

try t with λ e:Texn . case e of <l = x > ⇒ e’
                              | _        ⇒ raise e

     Exceptions carrying values of type int
tp: type .
nat: tp.
arr: tp → tp → tp.
term: type .
app: term → term → term.
lam: tp → (term → term) → term.
raise : term → term.
trywith: term → (term → term) → term.
value: term → type .
v_lam: value (lam T M).

% ---------------------------------------------------------- %
% Small-step operational semantics
step: term → term → type .
step_app_1      : step (app E1 E2) (app E1’ E2)
                   <- step E1 E1’.
step_app_2      : step (app E1 E2) (app E1 E2’)
                   <- value E1
                   <- step E2 E2’.
step_app_beta : step (app (lam T E ) E2) (E E2)
                 <- value E2.
step_app_err1 : value V
                → step (app (raise V) E2) (raise V).
step_app_err2 : value V1 → value V2 →
                step (app V1 (raise V2)) (raise V2).
step_try       : step E1 E1’
               → step (trywith E1 E2) (trywith E1’ E2).
step_try_v      : value V
                  → step (trywith V E2) V.
step_try_error: value V → step (trywith (raise V) E2) (E2 V).
% ---------------------------------------------------------- %
oft: term → tp → type .
t_lam : ({x:term} oft x T → oft (E x) S)
        → oft (lam T E) (arr T S).
t_app: oft E1 (arr T S) → oft E2 T
        → oft (app E1 E2) S.
t_trywith: oft E1 T → ({x:term} oft x nat → oft (E2 x) T)
           → oft (trywith E1 E2) T.
t_error : oft E nat → oft (raise E) T.
% ---------------------------------------------------------- %

rec tps: (oft M T)[] → (step M N)[] → (oft N T)[] =
fn d ⇒ fn s ⇒ case s of
| [] step_app_1 S1 ⇒
  let [] t_app D1 D2 = d in
  let [] F1 = tps ([] D1) ([] S1) in
     [] t_app F1 D2
| [] step_app_2 S2 _ ⇒
  let [] t_app D1 D2 = d in
  let [] F2 = tps ([] D2) ([] S2) in
     [] t_app D1 F2
| [] step_app_beta V ⇒
  let [] t_app (t_lam D) D2 = d in
     [] D _ D2
| []    step_app_err1 _   ⇒
  let   [] t_app (t_error D) _ = d in [] t_error D
| []    step_app_err2 _ _ ⇒
  let   [] t_app _ (t_error D) = d in [] t_error D
| [] step_try S1 ⇒
  let [] t_trywith D1 D2 = d in
  let [] F1 = tps ([] D1) ([] S1) in
     [] t_trywith F1 D2
| [] step_try_v V ⇒
  let [] t_trywith D1 D2 = d in [] D1
| [] step_try_error _ ⇒
  let [] t_trywith (t_error D1) D2 = d in [] D2 _ D1


To top