Concepts of Program Design Type safety and exception handling
Gabriele Keller Ron Vanderfeesten Overview
higher & first-order syntax inference rules, induction
tools to talk about languages
abstract machines big step and small step operational semantics
value & type environments parametric polymorphism/ generics type safety control stacks error/exception handling partial application/function closures semantic features
functional (algebraic) data types static & dynamic static & dynamic scoping typing language concepts
explicit & implicit procedural/imperative typing Types in programming languages
statically vs dynamically typed
type safe vs non type safe strongly vs weakly typed Static and Dynamic Semantics
• MinHs is a type-safe (or strongly typed) language
• What exactly do we mean by this?
- these terms are used by different authors to mean different things
- in general, it refers to guarantees about the run-time behaviour derived from static properties of the program
- Robin Milner: “Well typed programs never go wrong”
‣ do not exhibit undefined behaviour
- we define type safety to be the following two properties:
‣ preservation
‣ progress
- we look at both preservation and progress in turn Preservation and Progress
• Preservation:
- Idea: evaluation does not change the type of an expression
- Formally: If ⊢ e : τ and e ↦ e’ , then ⊢ e’ : τ
• Progress:
- Idea: a well-defined program can not get stuck
- Formally: If e is well typed, then either e is a final state, or there exists e’, with e ↦ e’
e1 : τ ↦ e2 : τ ↦ e3 : τ ↦ …
• Together is means that a program will either evaluate to a value of the promised type, or run forever Type Safety
• For any language to be type safe, the progress and preservation properties need to hold!
• Strictly speaking, the term type safety only makes sense in the context of a formal static and dynamic semantics
• This is one reason why formal methods in programming languages are essential
• The more expressive a type system is, the more information and assertions the type checker can derive at compile type
- type systems usually should be decidable
- but there are exceptions
• MinHs is type safe
- we can show that progress and preservation hold!
- but what if the language contains partial operations, like division? Run-time Errors and Safety
• Stuck states: in a type safe language language, stuck states correspond to ill- defined programs, e.g.,
- use (+) on values of function type, for example
- treat an integer value as a pointer
- use an integer as function
let x = 1 in x 5
• Unsafe languages/operations do not get stuck
- something happens, but its not predictable and/or portable:
void boom () { void (f*)(void) = 0xdeadbeef; f (); } Type safe languages
• Which of these languages are type safe?
- C
- C++
- C#
- Haskell
- Python
- Rust Run-time Errors and Safety
• How can we deal with partial functions, for example division by zero?
Γ ⊢ t1 : Int Γ ⊢ t2 : Int
Γ ⊢ Div t1 t2 : Int
Problem: the expression 5/0 is well-typed, but does not evaluate to a value.
• There are two alternatives:
(1) Change static semantics: can we enhance the type system to check for division by zero?
‣ in general, such a type system would not be decidable ‣ there exist systems that approximate this
(2) Change dynamic semantics: can we modify the semantics such that the devision by zero does not lead to a stuck state
‣ this approach is widely used for type safe languages Run-time Errors and Safety
• Application of a partial function can yield Error
Div v (Num 0) ↦ Error
• An Error interrupts any computation
Plus Error e ↦ Error
Plus e Error ↦ Error
If Error e1 e2 ↦ Error
and so on..... Run-time errors and Safety
• Typing the Error value:
- a run-time error can have any type
Γ ⊢ Error : ∀ τ. τ
• What type of situations lead to checked run-time errors in Haskell? Exceptions
• Error handling so far: - The Error expression to handle run-time errors deterministically aborts the whole program - For many applications, this is not the appropriate behaviour - Exceptions permit a more fine grained response to run-time errors
• Error: - result of a programming error (e.g., preconditions of a function not met), can be fixed by fixing the program
• Exception: - result of expected, but irregular occurrence, can possibly be handled in the program Exceptions in C#
• Language constructs to handle exceptions:
- try: execute a code block which may trigger an exception, has to be followed by at least one
- catch block, which contains the exception handling code, which may be followed by a code block preceded by
- finally, which is executed whether or not
- throw, which triggers an exception, was called in the try block
• Predefined exception classes contain:
- Message: human readable text that describes the exception
- StackTrace: lists all the called methods and line numbers
• Exception class can be extended by user defined exceptions Exceptions in C#
try { // code block which may trigger // an exception through … throw … } catch (ExceptionName e1 ) { // error handling code } catch (ExceptionName e2 ) { // error handling code } catch (ExceptionName eN ) { // error handling code } finally { // code block which will always be // executed } Exceptions in C#
namespace SaveDivision { class Division { int result;
Division() { result = 0; }
public void div(int num1, int num2) { try { result = num1 / num2; } catch (DivideByZeroException e) { Console.WriteLine("Exception caught: {0}", e.Message); Console.WriteLine(“Stack trace: {0}", e.StackTrace); } finally { Console.WriteLine("Result: {0}", result); } } … } } Exceptions
• Exceptions in MinHs: (1) raising (or throwing) an exception: raise e ‣ e : information about handling of exception
(2) catching an exception: try e1 handle x => e2
‣ catch expression raised in e1
‣ exception handler is e2 ‣ access to information about handling exception via x Exceptions
• Abstract Syntax
‣ raise e Raise e
‣ try e1 handle x => e2 Try e1 (x.e2)
• Informal evaluation rules: on try e1 handle x => e2
‣ evaluate e1, and
‣ if (Raise v) is encountered during e1, bind x to v and then evaluate e2 Exceptions
• Example:
try if (y <= 0) then raise -1 else x/y handle err => ....
• try expressions can be nested ‣ innermost try expression catches ‣ handler may re-raise an exception Exceptions
• Observations:
- type of exception values (second argument of raise)
• in many programming languages, this is a fixed type τexc
• may simply be a string or integer (exception code)
• e.g., subclass Throwable in Java, Exception in C# Exceptions - Static Semantics
• Typing rules
Γ ⊢ e : τexc
Γ ⊢ Raise e: ∀ τ. τ
Γ ∪ {x : τexc} ⊢ e2 :τ Γ ⊢ e1: τ
Γ ⊢ Try e1, (x.e2): τ Exceptions - Dynamic Semantics
• We introduce a new machines state
s ⪻ (Raise v) the machine raises an exception with the exception value v
• First approach:
on s ⪻ (Raise v)
‣ propagate exception upwards in the control stack s
‣ use first handler encountered Exceptions - Dynamic Semantics
• Entering a try block
s ≻ Try e1(x.e2) ↦C (Try ☐ (x.e2) ▷ s)≻ e1 • Returning to a try block
(Try ☐ (x.e2) ▷ s ≻ v1 ↦C s ≺ v1 • Evaluating a raise expression
s ≻ Raise e ↦C (Raise ☐) ▷ s ≻ e
• Raising an exception
(Raise ☐) ▷s ≻ v ↦C s ⪻ Raise v
• Catching an exception
Try ☐ x.e2 ▷ s ⪻ Raise v ↦C s ≻ e2 [x:=v] • Propagating an exception
f ▷ s ⪻ Raise v ↦C s ⪻ Raise v Exceptions - Dynamic Semantics
• What is the problem here?
- efficiency: the frames are popped one by one when an exception is raised
• Second approach
- how can we jump directly to the appropriate handler?
- we use an extra handler stack h
- a handler frame contains
‣ a copy of the control stack
‣ the handler expression Exceptions - Dynamic Semantics
• Entering a try block
(h, k) ≻ Try e1 (x.e2) ↦C
(Handle k (x.e2) ▷ h,(Try ☐ )▷ k) ≻ e1 • Returning to a try block
(Handle k (x.e2) ▷ h,(Try ☐) ▷ k) ≺ v1 ↦C
(h, k) ≺ v1
• Evaluating a raise expression
(h, k) ≻(Raise e) ↦C
( h, (Raise ☐) ▷ k) ≻ e • Raising an exception
( h, (Raise ☐) ▷ k) ≻ v ↦C
(h, k) ⪻ (Raise v) • Catching an exception
(handle k’ (x.e2))▷ h, k) ⪻ (Raise v) ↦C
(h, k’) ≻ e2 [x:=v]