** 16. Soundness ** ---------------------------------------------------------------------------- 16.1. We've defined typechecking rules, and evaluation rules. We'd like them to match up, in that if we successfully typecheck a program, then evaluation should always succeed (or at least run forever), without any "type errors" (defined somewhat cyclically as those run-time errors that we don't want to happen in semantically well-formed programs). We'd also like the result of evaluation to be a value of the type we expect. These properties can be formalized as theorems relating the static semantics (the typechecking rules) to the operational semantics. For the big-step operational semantics: Thm (Subject Reduction, aka Type Preservation): If Gamma |- e : tau and |- E : Gamma and E |- e => v then |- v : tau In other words, if an expression e typechecks in a type environment Gamma, yielding a type tau, and we're given a value environment E which binds identifiers to values compatible with the type environment Gamma, and e evaluates to some value v, then v has type tau. The analogous theorem for the small-step operational semantics: Thm (Subject Reduction, aka Type Preservation): If {} |- e : tau and e --> e' then {} |- e' : tau In other words, if closed expression e typechecks, yielding a type tau, and e evaluates in a single step to e', then the new expression e' also typechecks and yields the same type tau. In both of these cases, evaluation preserves typing, leading to the "type preservation" name. ---------------------------------------------------------------------------- 16.2. ** Details on helper relations for type preservation ** We can formalize the "|- E : Gamma" relation as follows, inductively on the bindings in Gamma: -------------------- |- E : {} E |- id => v |- v : tau |- E : Gamma -------------------- |- E : (Gamma,id:tau) The "|- v : tau" judgment is a little mini-type-inference, but only for closed values: -------------------- |- intconst : int -------------------- |- true : bool -------------------- |- false : bool (* the big-step semantics has closures as values: *) |- p => Gamma : tau1 gamma(E),Gamma |- e : tau2 -------------------- |- [(lambda p. e),E] : tau1->tau2 (* the small-step semantics has plain lambdas as values: *) |- p => Gamma : tau1 Gamma |- e : tau2 -------------------- |- (lambda p. e) : tau1->tau2 |- v1 : tau1 ... |- vN : tauN -------------------- |- {id1=v1, ..., idN=vN} : {id1:tau1, ..., idN:tauN} tau = [id1:tau1, ..., idN:tauN] |- vi : taui -------------------- |- [idi=vi]_tau : tau The gamma function takes a value environment E and returns the corresponding type environment Gamma: -------------------- gamma {} = {} gamma(E) = Gamma |- v : tau -------------------- gamma(E,id=v) = Gamma,id:tau ---------------------------------------------------------------------------- 16.3. In addition to preserving type, we also want to know that type-correct programs don't have run-time type errors. Such errors would show up by not being able to show that an expression evaluates successfully, i.e., that some expected evaluation judgment couldn't be proven. The natural thing to write down for the big-step semantics is the following theorem: Thm (Progress): If Gamma |- e : tau and |- E : Gamma then E |- e => v In other words, if an expression e typechecks in a type environment Gamma, yielding a type tau, and we're given a value environment E which binds identifiers to values compatible with the type environment Gamma, then we can always evaluate e successfully to a value v. Unfortunately, we can't prove this, because it's possible for e to run forever, never producing a value v. So we can't prove progress in this form for our given semantics, nor for any big-step semantics. We can state a progress theorem for small-step semantics, however: Thm (Progress): If {} |- e : tau then either 1) e is a value v or 2) e --> e' In other words, if a closed expression e typechecks, then either e is a value (and we're done evaluating), or we're always able to take a step, i.e., there are no run-time type-errors. We can put together preservation and progress for the small-step operational semantics to prove soundness: Thm (Soundness): If {} |- e : tau then either 1) e is a value v and |- v : tau or 2) e --> e' and {} |- e' : tau If this is true, then our static semantics is sound w.r.t. our dynamic (operational) semantics, i.e., well-typed programs evaluate successfully and preserve static typing. ---------------------------------------------------------------------------- 16.4. Some run-time errors aren't type errors, e.g. dividing by zero, or doing %id on a union whose tag isn't id at run-time, or having a refuted pattern-match. We don't want the presence of such features to prevent us from proving progress and soundness of our type system, so we treat such "run-time exceptions" as evaluation outcomes. E.g. idi != idj -------------------- (%idi [idj=vj]) fails e[p := v2] refuted -------------------- ((lambda p. e) v2) fails e2[p := v1] refuted -------------------- (let val p = v1 in e2) fails e1[p1 := v] refuted ... eN[pN := v] refuted -------------------- (case v of p1 => e1 ... pN => eN) fails Run-time errors that aren't type errors are treated as computing a special error value that doesn't support further evaluation. We also have to check for this error value at any other rule that evaluates a subexpression to a value, so that once error is produced, nothing else happens. E.g. e1 fails -------------------- (e1 e2) fails e2 fails -------------------- (v1 e2) fails e1 fails -------------------- (let val p = e1 in e2) fails and similarly for everything else with subexpressions, i.e. if, record and union construction, projection, and casing, and folding and unfolding. Then we extend our progress theorem to allow the error case: Thm (Progress): If {} |- e : tau then either 1) e is a value v or 2) e --> e' or 3) e fails Thm (Soundness): If {} |- e : tau then either 1) e is a value v and |- v : tau or 2) e --> e' and {} |- e' : tau or 3) e fails ---------------------------------------------------------------------------- 16.5. Let's prove preservation and progress for our small-step operational semantics, for a few cases at least. We do this by induction over the syntactic structure of e. (Almost all proofs about programs are either by induction over the syntactic structure of some piece of program, or by induction over the derivation of some judgment, either the type checking proof tree or the evaluation proof tree.) Thm (Soundness): If {} |- e : tau then either 1) e is a value v and |- v : tau or 2) e --> e' and {} |- e' : tau or 3) e fails Case 1: e = intconst tau must be int, by inspection of typing rules for intconst e is a value, and |- intconst : int. QED. [Cases for true, false, lambda, and other expressions that are values are similar] Case 2: e = (if e1 then e2 else e3) By assumption, we know: P1: {} |- (if e1 then e2 else e3) : tau Since the if is well-typed, we can deduce: P2: {} |- e1 : bool P3: {} |- e2 : tau P4: {} |- e3 : tau We know the if expression isn't a value, so we must have the second or third cases (step or fail). We look at the cases of small-step evaluation of an if. By our induction hypothesis, either e1 is a value, or it's an expression that takes a step, or its evaluation fails. Subcase 1: e1 is some value v1. e1 must be true or false (the only values of type bool). Subcase e1 = true: From the evaluation rule for (if true then e2 else e3), we know that e --> e2. Then P3 gives us what we want to prove. QED. Subcase false: Similar to true subcase. Subcase 2: e1 --> e1' From our induction hypothesis, we can assume: P5: {} |- e1' : bool We can conclude that the if expression successfully steps: P7: (if e1 then e2 else e3) --> (if e1' then e2 else e3) The typing rules for the new expression, given hypotheses P5, P3, and P4, gives us what we want to prove. QED. Subcase 3: e1 fails In this case, the if expression fails. QED. [Cases for other expressions containing subexpressions, such as record and union operations, are similar.] Case 3: e = (e1 e2) By assumption, we know: P1: {} |- (e1 e2) : tau Since the application is well-typed, we can deduce: P2: {} |- e1 : tau1->tau P3: {} |- e2 : tau1 We know the if expression isn't a value, so we must have the second or third cases (step or fail). We look at the cases of small-step evaluation of an application. For the cases where either e1 or e2 is not a value, or its evaluation fails, we do similarly to the non-value case of the if and related expressions. For the remaining case where e1 and e2 are values v1 and v2: Since v1 is well-typed with type tau1->tau, we deduce: v1 = (lambda p. e') P4: {} |- p => Gamma : tau1 P5: Gamma |- e' : tau By the two evaluation rules for applications, either the pattern-match of the formal succeeds, or it is refuted. (We need a lemma to show that these two cases are exhaustive.) Subcase success: We assume that e'[p := v2] --> e''. We need a lemma to let us deduce that {} |- e'' : tau from P4, P5, and the pattern-substitution rules. QED. Subcase failure: We assume that e'[p := v2] refuted. Then e fails. QED. Whew. Lots more of the same.