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:

  1. 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
  1. 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.