VIEWS: 4 PAGES: 7 POSTED ON: 3/22/2011 Public Domain
Exceptions 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. 1 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 ﬁrst 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, (ﬁx f.f) error will diverge instead of aborting. Or more generally, let foo= ﬁx f.λx:int.f x and we are trying to evaluate foo error = (ﬁx 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 ﬂexibility 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 speciﬁed 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 ﬁrst 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 inﬁnitely 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 follows: 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 2 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 T-TRY Γ 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 E -TRY- RAISE : think of the expression e2 as a function which is waiting for a result v1 to be passed to in order to 3 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 unspeciﬁed. 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 ﬂexibility comes with a cost: error handlers may now have to parse these strings to ﬁnd out what happened. We can keep the ability to pass more information, while avoiding string parsing if we deﬁne 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 inﬂexible: it requires us to ﬁx the set of exceptions in advance. This leaves no room for the programmer to add user-deﬁned exceptions. This idea can be however reﬁned to allow for extra ﬂexibility 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 case. try e with l(x) ⇒ e’ is desugared to 4 try t with λ e:Texn . case e of <l = x > ⇒ e’ | _ ⇒ raise e 5 %{ 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. % ---------------------------------------------------------- % 6 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 ; 7