After looking at evaluating statements,
we will look at methods and programs building on the definition of Prog and Method as defined in
earlier.
Moreover will assuming that method bodies use the version of Expr and Stmt from the
previous section
but extended with global contexts and method calls.
Dealing with memory is tricky and we will do that last, in the next section.
Global Closures and Local Environments
Given that we have both global and local environments, we need to keep track of those as well. We will do so by defining an Idris record to keep the two definitions on one structure.
We must remember,
however,
that functions are closures and they can be defined in much the same way as Value was.
Instead of typing based on expression types,
we do so using Method types and we must capture the global environment from time of the method’s declaration.
data Closure : (type : MTy) -> Type where
C : (meth : Method global type)
-> (env : All Closure global)
-> Closure type
Using this we can define our evaluation environment as:
record Env (global : SnocList MTy)
(local : SnocList Ty)
where
constructor MkEnv
decls : Stack global
stack : Stack local
where:
decls- extracts the global environment
stack- extracts the local environment
Mutual Definitions
As statements, expressions, and methods all depend on each other, we need to mutually define them. Rather than using Idris’ mutual blocks, we instead use ‘early function declarations’ to enable bodies to depend on things yet defined.
namespace Expr
export
eval : (env : Env globals locals)
-> (expr : Expr globals locals type)
-> Value type)
namespace Stmt
export
eval : (env : Env globals locals)
-> (met : Stmt ret globals locals type)
-> (Maybe (Value type))
namespace Method
export
eval : (env : Env globals args)
-> (met : Method globals (METH args type))
-> Value type
namespace Expr
eval env expr = ?expr_eval_rhs
namespace Stmt
eval env stmt = ?stmt_eval_rhs
namespace Method
eval env meth = ?meth_eval_rhs
Incorporate you previous attempts at evaluating Expr and Stmt into these mutual blocks.
Evaluating Method Calls
Evaluating expressions has not changed from earlier. We need, however, to incorporate evaluation of method calls. Specifically evaluation of the arguments.
Let us first recall their dynamics:
lookup(mref,g) = C (\{ xref_{0} : t_{0},..., xref_{n} : t_{n}} => s) g'
g;l |- {e_{0},...,e_{n}} ~>* {v_{0},...,v_{n}}
g', {v_{0},...,v_{n}} |- s ~>* v
---- [ Method Call]
g;l |- mref({e_{0},...,e_{n}}))
~>*
v
and the encoding of Call in Idris.
Call : (idx : Var (M tyArgs tyRet) global)
-> (args : All (Expr global local) tyArgs)
-> Expr global local tyRet
We can look the closure from the environment using index,
much like resolving references for let-binders.
For evaluating arguments we need a function.
local to the Exprs namespace and before our Expr.eval body,
that iterates over args evaluating them to get a list of values.
namespace Expr
evalArgs : (env : Env globals locals)
-> (args : All (Expr globals localss) types)
-> All Value types
A call with no arguments will return an empty list:
evalArgs env [<] = [<]
A call with at least one argument evaluates the front of the list, before evaluating the last argument.
evalArgs env (sx :< x)
= evalArgs env sx :< Expr.eval env x
You would be right in thinking that we could use a functor
i.e. map,
here.
We are just mapping over the list afterall.
However,
we have yet to address memory,
and having the explicit function will be useful.
Evaluation of method calls can then proceed as described in the dynamics. First we lookup the closure, evaluate the arguments, and pass them to a call to evaluating methods with the correct environment.
namespace Exprs
eval env (Call idx args)
= let C m clos = lookup (decls env) idx
in let vs = evalArgs env args
in Method.eval (MkEnv clos vs) m
eval env rest = ?seeBefore
Evaluating Methods
Evaluating methods requires evaluating the method body with the correct environment, which we build when calling the method.
When pattern matching on the result of evaluating the body, Idris tells us that we may have a result or nothing. We know, however, that method bodies must return some thing. But we do not know a priori how statements will be executed at runtime. So even though we have an advanced type system, somethings cannot be statically known.
namespace Method
eval env (M body)
= do res <- Stmt.eval env body
case res of
Just v => v
Nothing => ?rhs
We could,
instead,
write a separate version of Stmt.eval which must return,
but that is duplicating our work.
As we know the language is type-safe we can leave the hole.
Evaluating Programs
We end by looking at the evaluation of programs. As with our other evaluation functions, we evaluate against an environment with is closed against local variables. (We populate the stack when executing methods.) As our main method return’s UNIT, we must return value of that type.
namespace Prog
export
eval : (env : Env gs Lin)
-> (met : Prog gs)
-> Value UNIT
We examine each term in turn:
- Main methods are evaluated as methods, unlike method calls we do not need to augment the environment.
eval env (Main x)
= Method.eval env x
- For each method declaration, we extend the global stack with the closure.
eval env (Decl ds rest)
= do let e = extendDecls env (C ds env.decls)
eval e rest
Finally,
we create a wrapper function run to make executing programs that little bit easier by inserting an empty environment.
run : Prog Lin -> IO ()
run p = do R h U p <- Prog.exec (MkEnv Lin Lin) p
pure ()
Exercises
For the sample programs in
walk-through for Olaf
evaluate them using run.
Many imperative languages have main methods that take in system arguments and return values of type int.
Rewrite Prog to support evaluation of such main methods.