We begin our evaluation story by looking at how we can evaluate expressions. We will do so be examining the evaluation of the small expression language we created when introducing nameless variable representations.

data Expr : (ctxt : SnocList Ty)
         -> (type : Ty)
                 -> Type
  where
    B : (b : Bool) -> Expr ctxt BOOL
    And : (l : Expr ctxt BOOL)
       -> (r : Expr ctxt BOOL)
            -> Expr ctxt BOOL

    Var : (idx : Elem type ctxt)
              -> Expr ctxt type

    Let : (this : Expr  ctxt    typeA)
       -> (body : Expr (ctxt :< typeA) typeB)
               -> Expr  ctxt           typeB

Intrinsically-Typed Values

Our expression language will reduce down to values, we can encode these values in much the same way we do the terms themselves. With Expr our values will be boolean constants.

data Value : (type : Ty) -> Type where
  VB : (b : Bool) -> Value BOOL

That is it!

Our values are fully reduced terms, as such we do not need to have a typing context.

The exercises from nameless variable representations extended Expr with integers and integer addition. Use that version of Expr and add integers to Value.

Well-Typed Environments

With variables we need to keep track of bound variables and their values. For intrinsically-typed values we need to collect them. When looking at typing method calls we saw use of the All quantifier for collecting method call arguments. We can do so here for defining our environment.

To help with understanding the link between the dynamic semantics and our implementation, We will use a type-synonym to rename All to Stack:

Stack : (sx : SnocList Ty) -> Type
Stack = All Value

We can now specify our evaluation function as one that takes in an environment and expression, and returns a value of the type specified in the expression itself.

eval : (env  : Stack ctxt)
    -> (expr : Expr  ctxt type)
            -> Value      type

Our evaluation environment has the same index as expressions, meaning that it will grow and shrink as dictated by our expressions. Further, as the type of the output value is the same as the input expression we know that typing has been preserved, and as we are purely returning a value we know that evaluation will make progress towards a value.

Before we can write our evaluation function, however, we need to be able to lookup values from our context.

Here is where the link between how we encoded variables and the environment starts to come together.

Recall that Var, AtIndex, and Elem are all propositions that state we know that a value resides in the list and where that value is. As our nameless representations indexes the typing context we can use this proof to safely index the environment.

To do so we need to write an function to safely index environments. Fortunately, we can make it generic for all instances of All.

The type signature for index takes, as input, a proof that we know where in the environment we are going to and the environment itself. As we know what we are looking for, index will return the value at the specificed location. That is, as our context is full of, at the type-level, types we can use that to ensure our value has the correct type.

index : (idx  : Var   x sx)
     -> (ctxt : All p   sx)
             ->     p x

The two cases for indexing environments follow the two data constructors for AtIndex. Here enables the safe extraction, and There means we have to travel further backwards into the context.

index (V 0 Here) (sx :< x)
  = x

index (V (S idx) (There ltr)) (sx :< y)
  = index (V idx ltr) sx

We can now write our evaluation function.

When dealing with heaps, the environment will need to be updated. Given the following type signature for update, can you complete the implementation:

update : (idx  : Var   x sx)
      -> (new  :     p x   )
      -> (ctxt : All p   sx)
              -> All p   sx

Evaluating Expressions

Small-step operational semantics means evaluating expressions in steps. By nature of how we write functions, we do not need to describe each individual step for each term, we can do it in a one.

Recalling our evaluation function’s type signature:

eval : (env  : Stack  ctxt)
    -> (expr : Expr  ctxt type)
            -> Value      type

Boolean Constants

Boolean Constants are just values:

eval env (B b) = VB b

And Operations

The formal reduction rules for And are:

  1. To simplify the expressions to values:

    g |- l ~>* v_{l}
    g |- r ~>* v_{r}
    
    ---- [ Bool Simplify ]
    
    g |- and(l,r) ~>* and(v_{l},v_{r})
  2. To reduce/resolve the values:

    ---- [ Bool Resolve ]
    
    g |- and(v_{l},v_{r}) ~> v_{l} && v_{r}

We simplify the operands to values by performing a recursive call,, and we get the final value by reducing the values by performing Idris’ own and operation (&&) on the results.

eval env (And l r)
  =  let B l = eval env l
  in let B r = eval env r
  in VB (l && r)

We can safely pattern match on each operand as each call of eval is well-typed.

When writing eval for And try putting in typed-holes and see what idris itself tells you about variables.

Variable Lookup

We resolve variables in Olaf by lookup.


v = lookup(xref,l)

---- [ Let ]

l |- xref -> v

When encountering variables we can use the instance of Var to safely index env:

eval env (Var idx)
  = index idx env

Extending Contexts

The final term to evaluate is Let. The reduction rule for Let we will use is:

l          |- e ~>* v
l,(xref,v) |- s ~>* s'

---- [ Let ]

l |- let xref : t = e; s ~>* s'

Which we realise by extending env with the evaluated expression.

eval env (Let expr body)
  =  let v = eval env expr
  in eval (env :< v ) body

Evaluate the following terms:

Extend the definition of Expr to include support for integers and their addition, and extend eval as required.

For Expr to represent Ola expressions, you will have to remove Let.