CS312 Lecture 23: An evaluator for ML with side effects

CS312 Lecture 23: An evaluator for ML with side effects

References review

Consider
val a = ref 5;
val b = ref 5;

We know that the expression a = b; evaluates to false. Because ref v is not equal to ref v. They are different locations in memory, each of which happen to contain 5.

Now if we do

a := !a + 1

We have increased the value at the location pointed to by a, but not by b. To do sharing, we need to have said

val b = a;

After this, a=b is true, and changing a changes b as well. Also note that in := the left hand side must be a ref.

The use of references introduces into our language a new idea of time and sharing. Before, we could consider a binding as assigning a value to a name and expect this value to be constant in time. Each time we created a new binding, we created a new copy of the value, having not possible to share the same structure in memory by two different bindings.

Now if we bind references to values, we can keep a same binding to a reference, but change the pointed value later in time. This introduces to the language a higher complexity that we will need to handle carefully.

Interpreter with side effects

To extend our interpreter to have side effects, we need to extend our Abstract Syntax Tree, and the possible values of the environment to include references. Then we also need to introduce operators that behave similar to ref, !, :=.
  datatype typ = Int_t
               | Real_t
               ...
               | Ref_t     of typ                (* reference type *)

  datatype binop = Plus
               | Times
               ...
               | Assign                          (* :=       *)

  datatype unop = Neg                            (* ~        *)
               |  Not                            (* not      *)
               |  Ref                            (* ref      *)
               |  Deref                          (* !        *)

  datatype value = Int_v      of int
                 | Real_v     of real
					  ...
                 | Ptr_v      of int                (* address in memory    *)
                 | Tag_v      of objectType
The := is a binary operator and needs to have on the left hand side a Pointer, so we extend evaluateBinop as follows:
evaluateBinop (bop: binop, (v1,t1): value*typ, (v2,t2): value*typ)
    case (bop, v1, v2) of
	 ...
    | (Assign, Ptr_v i, _)                => (replaceObjectAt (i, v2);
                                              (Tuple_v [], Tuple_t []))
    | (Assign, _, _)                      => err "type error (:=)" 
ref is a unary operator, that takes a value and creates a pointer to this value. ! is an unary operator that "dereferences" the value stored at the location pointed by a specific reference. For this two operators, we extend the definition of evaluateUnop as follows:
evaluateUnop (uop: unop, (v, t): value * typ): value * typ =
    case (uop, v) of
	 ...
    | (Ref, _)                            => (Ptr_v (storeObject v), Ref_t t)
    | (Deref, Ptr_v i)                    => (case t of
                                                Ref_t t' => (loadObject i, t')
                                              | _ => err "type error (!)")
    | (Deref, _)                          => err "type error (!)"

To do a functional version of this we basically just need a heap structure, which maps addresses to values. This doesn't actually require side effects; we can simply pass it around and update it when needed. The advantage of the evaluator is that it showcases a model that is closer to Intel's.


CS312  © 2002 Cornell University Computer Science