CS312 Lecture 16: Modifying the Evaluator

Administrivia

Special Forms

One of the major limitations of ML is that it is impossible to add new special forms. What do I mean by a special form? A function that controls the evaluation of its arguments.

In ML, functions evaluate their arguments (be sure you know this fact). As a consequence, it is impossible to write a function ifnot such that

ifnot(x,y,z) = if (not x) then y else z

Why? Consider

fun ifnot(x:bool, y:int, z:int):int = if (not x) then y else z
Let's try it:
3+ifnot(1 > 2, 39, 42)
Þ
42
So it works. But, what about ifnot(1 > 2, 25, raise Fail "can't get here")? the function will raise the exception regardless of the value of the boolean expression!. So in the SML implementation we have, we are stuck; there is no way to write a new special form. The special forms are fixed; we're stuck with the ones that happen to be built into the language.

But what about Mini-ML? A different story! We can modify the interpreter by adding new special forms to it. Here are the examples we will focus on:

To be able to add special forms, we need to study the evaluator:

Evaluator Overview

A critical function to consider is evaluate(ex: exp, en: env): value * typ

  fun evaluate (ex: exp, en: env): value * typ =
    case ex of
      Int_c i               => (Int_v i,    Int_t)
    | Real_c r              => (Real_v r,   Real_t)
    | Bool_c b              => (Bool_v b,   Bool_t)
    | Char_c c              => (Char_v c,   Char_t)
    | String_c s            => (String_v s, String_t)
    | Id_e id               => (case lookupBinding (id, en) of
                                 NONE     => err ("unbound variable " ^ id)
                               | SOME v   => v)
    | If_e (test, e1, e2)   =>
         (case evaluate (test, en) of
            (Bool_v b, Bool_t)=> evaluate(if b then e1 else e2, en)
          | _               => err ("'If' condition must be boolean."))
    | Let_e   (dlist, ex)   => evaluate      (ex, evaluateDeclare (dlist, en))
    | Apply_e (e1, e2)      => evaluateApply (e1, e2, en)
    | Unop_e  (uop, ex)     => evaluateUnop  (uop, evaluate (ex, en))
    | Binop_e (e1, bop, e2) => evaluateBinop (bop, evaluate (e1, en),
                                                   evaluate (e2, en))
    | Tuple_e elist         =>
         let
           val (v, t) = foldr (fn ((v, t), (vl, tl)) => (v::vl, t::tl))
                              ([],[])
                              (map (fn(e) => evaluate (e, en)) elist)
         in
          (Tuple_v v, Tuple_t t)
         end
    | Ith_e (i, ex)         =>
         (case evaluate (ex, en) of
           (Tuple_v vs, Tuple_t tlst)  => (List.nth (vs, i), List.nth (tlst, i))
         | _                           => err "Projection from non-tuple.")
    | List_e elist          =>
         let
           val (v, t) = foldr (fn ((v, t), (vl, tl)) => (v::vl, t::tl))
                              ([],[])
                              (map (fn(e) => evaluate (e, en)) elist)
         in
          (*
             The empty list has a dummy type that can later be instantiated
             using context information.
          *)
          if null(v) then (List_v [], List_t (Undef_t))
          else (List_v v, List_t (hd t))
         end
    | Fn_e (args, t, body)  => (Fn_v(args, en, body, NONE), t)

An exp is the representation of an expression, returned by the parser. The top-level REPL loops calling evaluate in a special enviroment (env) called empty, which contains a number of special bindings for various builtin functions (hd, tl, null, implode, explode), as well as the names of all special forms. An environment is represented as a list of bindings, each of which has a name, a value and a type spec (saying what it returns).

(If you type declarations into the REPL, it adds these to "empty", but there is no way to add new built-ins or special forms to the evaluator on the fly. Maybe someday?)

evaluate simply does a dispatch on the type of its argument, using the rules of evaluation.

To add special forms, we need to look further into the evaluator. Here are some changes that you could try to do in the evaluator. Originally we have evaluateApply defined as follows:

  and evaluateApply (e1: exp, e2: exp, en: env): value * typ =
    let
      val (fcn, fcnt)  = evaluate (e1, en)
      val aa           = evaluate (e2, en)
    in
      case fcn of
       Predef_v(name)              => predefined(name, aa)
      | Fn_v([],    en, body, name) =>
             evaluate(body,
                      (case name of
                         NONE    => en
                       | SOME(s) => insertBinding(s, (fcn, fcnt), en)))
...

This function defines how the evaluator handles the application of functions and predefined values. Then to add the special forms, we could do the following changes to the evaluator:

update enviroment.sml:
  val empty  = Env([
                ("if3",     SpecForm("if3"),     Fn_t(Tuple_t([Bool_t,
                                                               Undef_t,
                                                               Undef_t]),
                                                      Undef_t)),
                ("ifnot",     SpecForm("ifnot"),Fn_t(Tuple_t([Bool_t,
                                                              Undef_t,
                                                              Undef_t]),
                                                     Undef_t)),
                ("ifmaybe",     SpecForm("ifmaybe"),Fn_t(Tuple_t([Undef_t,
                                                                  Undef_t]),
                                                         Undef_t)),
                ("hd",      Predef_v("hd"),      Undef_t),
                ("tl",      Predef_v("tl"),      Undef_t),
                ("null",    Predef_v("null"),    Fn_t(List_t(Undef_t), Bool_t)),
                ("explode", Predef_v("explode"), Fn_t(String_t,Char_t)),
                ("implode", Predef_v("implode"), Fn_t(Char_t, String_t))
               ])

update evaluator.sml:
  and evaluateApply (e1: exp, e2: exp, en: env): value * typ =
    let
      val (fcn, fcnt)  = evaluate (e1, en)
      val aa           = evaluate (e2, en)
    in
      case fcn of
        SpecForm(name)              => specialForm(name, e2, en)
       |Predef_v(name)              => predefined(name, aa)
    ...

  fun specialForm (name: string, e2: exp, en: env): value * typ =
    case name of
      (*
         If3 evaluates its first argument, which must result in a boolean value.
         If the result is true, the second argument is evaluated and returned.
         If the result is false, the third argument is evaluated and returned.

         Note that in the dynamic typechecking setting that we employ, it is
         not possible to determine whether the 'then' and 'else' branches
         return values of the same type without evaluating both. Thus in this
         version of Mini-SML we can legally write statements like this:

        >> if3(2 = 2, "true", 1/0)
        "true": string
      *)
      "if3" => (case e2 of
                  Tuple_e([cond, thenE, elseE]) =>
                   (case evaluate(cond, en) of
                      (Bool_v(true),  Bool_t) => (* evaluate 'then' branch *)
                        evaluate(thenE, en)
                    | (Bool_v(false), Bool_t) => (* evaluate 'else' branch *)
                        evaluate(elseE, en)
                    | _ => err "first argument of if3 must be boolean")
                | _ => err "incorrect argument number for if3; should be 3")
    | "ifnot" =>
        (case e2 of
           Tuple_e([cond, thenE, elseE]) =>
             (case evaluate(cond, en) of
                (Bool_v(false),  Bool_t) => (* evaluate 'then' branch *)
                  evaluate(thenE, en)
              | (Bool_v(true), Bool_t) => (* evaluate 'else' branch *)
                  evaluate(elseE, en)
              | _ => err "first argument of ifnot must be boolean")
         | _ => err "incorrect argument number for ifnot; should be 3")
    | "ifmaybe" =>
        (case e2 of
           Tuple_e([thenE, elseE]) =>
             (case flip() of
                0 => (* evaluate 'then' branch *)
                  evaluate(thenE, en)
              | 1 => (* evaluate 'else' branch *)
                  evaluate(elseE, en)
              | _ => err "first argument of ifmaybe must be boolean")
         | _ => err "incorrect argument number for ifmaybe; should be 2")
    | _     => err "internal error [09]"


CS312  © 2002 Cornell University Computer Science