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:
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})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:
(and true false)(and true (and false true))let x : bool = True; (and true (and false true))
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.