** 14. Operational Semantics ** ---------------------------------------------------------------------------- 14.1. We've formally defined how typechecking is done, i.e., which syntactically well-formed expressions are semantically well-formed in a type environment with a particular result type. But we haven't defined how expressions evaluate. Let's do that now. The most natural way to do this is to define evaluation judgments of the following form: E |- e => v meaning that, in an environment E mapping identifiers to values, the (semantically well-formed) expression e evaluates to value v. Values are a subset of expressions that can't evaluate any more. They basically are the introduction forms, where any *evaluated* subexpressions must be values as well: v ::= intconst | true | false | [(lambda p. e), E] | {id=v, ..., id=v} | [id=v] In the above, note that function *closures* are values, not functions themselves. Here are the rules: (* constants evaluate to themselves *) -------------------- E |- intconst => intconst -------------------- E |- true => true -------------------- E |- false => false (* if's only evaluate one of the then or else, depending on what the test evaluates to *) E |- e1 => true E |- e2 => v -------------------- E |- (if e1 then e2 else e3) => v E |- e1 => false E |- e3 => v -------------------- E |- (if e1 then e2 else e3) => v (* lambdas evaluate to closures *) -------------------- E |- (lambda p. e) => [(lambda p. e), E] (* applications evaluate the function and the argument to values, then evaluate the function body in the function's static environment extended with bindings from the function's formal parameter *) E |- e1 => [(lambda p. e), E'] E |- e2 => v2 |- p v2 => E'' E',E'' |- e => v -------------------- E |- (e1 e2) => v (* identifier references simply look things up in the environment *) -------------------- E,id=v |- id => v id != id' E |- id => v -------------------- E,id'=v' |- id => v E |- id => v -------------------- E,{} |- id => v (* let bindings *) E |- e1 => v1 |- p v1 => E' E,E' |- e2 => v2 -------------------- E |- (let val p = e1 in e2) => v2 (* Record operations *) E |- e1 => v1 ... E |- eN => vN -------------------- E |- {id1=e1, ..., idN=eN} => {id1=v1, ..., idN=vN} E |- e => {id1=v1, ..., idN=vN} -------------------- E |- (#idi e) => vi (* Union operations *) E |- e => v -------------------- E |- [id=e] => [id=v] (* two cases for union testing *) E |- e => [idi=vi] -------------------- E |- (?idi e) => true E |- e => [idj=vj] idj != idi -------------------- E |- (?idi e) => false (* leave undefined the case where the tag doesn't match; such programs can't evaluate to anything, which is how we say they have run-time errors *) E |- e => [idi=vi] -------------------- E |- (%idi e) => vi (* case evaluates patterns until it finds one that isn't refuted, evaluating the rhs of that case *) E |- e => [id=v] |- p1 v refuted ... |- pi v refuted (0 <= i < N) |- p(i+1) v => E' E,E' |- e(i+1) => v' -------------------- E |- (case e of p1 => e1 ... pN => eN) => v' (* fold and unfold have no effect at run-time *) E |- e => v -------------------- E |- (fold e as tau) => v E |- e => v -------------------- E |- (unfold e) => v (* recursive values can reference themselves; this is tricky to implement! *) E,id=v |- e => v -------------------- E |- (rec id:tau = e) => v ---------------------------------------------------------------------------- 14.2. The pattern evaluation judgments are as follows: (* bind an identifier *) -------------------- |- (id:tau) v => ({},id=v) (* do nothing for a wildcard *) -------------------- |- (_:tau) v => {} (* recursively bind components of the record. refute the record if any of the components refuted *) |- p1 v1 => E1 ... |- pN vN => EN -------------------- |- {id1=p1,...,idN=pN} {id1=v1,...,idN=vN} => (E1,...,EN) |- pi vi refuted -------------------- |- {id1=p1,...,idN=pN} {id1=v1,...,idN=vN} refuted (* for a union pattern, succeed if we've matched the right tag, fail otherwise *) |- pi vi => Ei -------------------- |- ([idi=pi]_tau) [idi=vi] => Ei |- pi vi refuted -------------------- |- ([idi=pi]_tau) [idi=vi] refuted idi != idj -------------------- |- ([idi=pi]_tau) [idj=vj] refuted (* patterns on values of recursive type just ignore the recursion *) |- p v => E -------------------- |- (fold p as tau) v => E |- p v refuted -------------------- |- (fold p as tau) v refuted ---------------------------------------------------------------------------- 14.3. The above rules don't handle references and side-effects. What we need is something in our semantics to track the state of memory, which is updated by := operations and examined by ! operations. We can't just use the environment, because we save environments for lambdas and restore them when calling a lambda, but we don't want to restore the state of memory when lambdas are called. Also, we throw away the extended environment after the lambda returns, which would throw away any updates to the state of memory. So what we need is a separate environment (which we'll call a store) mapping reference values (which we'll call locations) to their contents. We'll keep passing around a single store through all our evaluation rules, in contrast to how environments are handled. To let us talk about reference operations, we'll treat them as explicit expressions rather than predefined functions: e ::= ... | ref e | !e | e := e The result of the ref expression will be a new location value: v ::= ... | l l in Location Location is an infinite set of locations, and the only property of a location is that it can be distinguished from other locations. The store will be Sigma, a map from locations l to values v. The result of := is () in ML. We can either add () as a new kind of expression and value, or we can pick a different result value that's already in Core ML, e.g. returning the rhs value as a C assignment expression does. The latter requires less change to our formalism, so we'll do that. Our new evaluation judgments will be of the following form: E;Sigma |- e => v; Sigma' meaning that, in an environment E mapping identifiers to values and a store Sigma mapping location values to values, the (semantically well-formed) expression e evaluates to value v and new store Sigma'. Here are the rules for the new expressions manipulating references: (* ref allocations a new location value l and binds it in the store *) E;Sigma |- e => v; Sigma' l not in dom(Sigma') -------------------- E;Sigma |- (ref e) => l; (Sigma',l=v) (* ! looks up the contents of the given location in the current store *) E;Sigma |- e => l; Sigma' (l=v) in Sigma' -------------------- E;Sigma |- (! e) => v; Sigma' (* := updates a location in the store, returning the rhs value (unlike real ML) *) E;Sigma |- e1 => l; Sigma' E;Sigma' |- e2 => v; Sigma'' -------------------- E;Sigma |- (e1 := e2) => v; (Sigma'',l=v) Note that the Sigma' resulting from the first expression evaluation is passed in as the initial store for evaluating the second expression, and that the store resulting from that is then updated and "returned" from the assignment expression evaluation. This "single-threading" of the store, where at any given time there's only one store live, and that store threads its way through the rules, is standard, and all the other evaluation rules will follow the same pattern. I give all these updated rules below. (* constants evaluate to themselves *) -------------------- E;Sigma |- intconst => intconst; Sigma -------------------- E;Sigma |- true => true; Sigma -------------------- E;Sigma |- false => false; Sigma (* if's only evaluate one of the then or else, depending on what the test evaluates to *) E;Sigma |- e1 => true; Sigma' E;Sigma' |- e2 => v; Sigma'' -------------------- E;Sigma |- (if e1 then e2 else e3) => v; Sigma'' E;Sigma |- e1 => false; Sigma' E;Sigma' |- e3 => v; Sigma'' -------------------- E;Sigma |- (if e1 then e2 else e3) => v; Sigma'' (* lambdas evaluate to closures *) -------------------- E;Sigma |- (lambda p. e) => [(lambda p. e), E]; Sigma (* applications evaluate the function and the argument to values, then evaluate the function body in the function's static environment extended with bindings from the function's formal parameter *) E;Sigma |- e1 => [(lambda p. e), E']; Sigma' E;Sigma' |- e2 => v2; Sigma'' |- p v2 => E'' (E',E'');Sigma'' |- e => v; Sigma''' -------------------- E;Sigma |- (e1 e2) => v; Sigma''' (* pattern matching doesn't involve any reference reads or updates, so we don't need to pass the store to the pattern-matching judgments *) (* identifier references simply look things up in the environment *) -------------------- (E,id=v);Sigma |- id => v; Sigma id != id' E;Sigma |- id => v; Sigma -------------------- (E,id'=v');Sigma |- id => v; Sigma E;Sigma |- id => v; Sigma -------------------- (E,{});Sigma |- id => v; Sigma (* let bindings *) E;Sigma |- e1 => v1; Sigma' |- p v1 => E' (E,E');Sigma' |- e2 => v2; Sigma'' -------------------- E;Sigma |- (let val p = e1 in e2) => v2; Sigma'' (* Record operations *) E;Sigma |- e1 => v1; Sigma1 E;Sigma1 |- e2 => v2; Sigma2 ... E;Sigma(N-1) |- eN => vN; SigmaN -------------------- E;Sigma |- {id1=e1, ..., idN=eN} => {id1=v1, ..., idN=vN}; SigmaN E;Sigma |- e => {id1=v1, ..., idN=vN}; Sigma' -------------------- E;Sigma |- (#idi e) => vi; Sigma' (* Union operations *) E;Sigma |- e => v; Sigma' -------------------- E;Sigma |- [id=e] => [id=v]; Sigma' (* two cases for union testing *) E;Sigma |- e => [idi=vi]; Sigma' -------------------- E;Sigma |- (?idi e) => true; Sigma' E;Sigma |- e => [idj=vj]; Sigma' idj != idi -------------------- E;Sigma |- (?idi e) => false; Sigma' (* leave undefined the case where the tag doesn't match; such programs can't evaluate to anything, which is how we say they have run-time errors *) E;Sigma |- e => [idi=vi]; Sigma' -------------------- E;Sigma |- (%idi e) => vi; Sigma' (* case evaluates patterns until it finds one that isn't refuted, evaluating the rhs of that case *) E;Sigma |- e => [id=v]; Sigma' |- p1 v refuted ... |- pi v refuted (0 <= i < N) |- p(i+1) v => E' (E,E');Sigma' |- e(i+1) => v'; Sigma'' -------------------- E;Sigma |- (case e of p1 => e1 ... pN => eN) => v'; Sigma'' (* fold and unfold have no effect at run-time *) E;Sigma |- e => v; Sigma' -------------------- E;Sigma |- (fold e as tau) => v; Sigma' E;Sigma |- e => v; Sigma' -------------------- E;Sigma |- (unfold e) => v; Sigma' (* recursive values can reference themselves; this is tricky to implement! *) (E,id=v);Sigma |- e => v; Sigma' -------------------- E;Sigma |- (rec id:tau = e) => v; Sigma'