// L7c-eval: CPS version of L5 with concrete data-structure continuations
// and registerization

import { map, repeat, zipWith, join, identity } from "ramda";
import { AppExp, CExp, DefineExp, Exp, IfExp, LetrecExp, LetExp,
         Program, SetExp, VarDecl, makeProgram } from '../L5/L5-ast';
import { isBoolExp, isLitExp, isNumExp, isPrimOp, isStrExp, isVarRef } from "../L5/L5-ast";
import { parseL5Exp, unparse } from "../L5/L5-ast";
import { isAppExp, isDefineExp, isIfExp, isLetrecExp, isLetExp,
         isProcExp, isSetExp } from "../L5/L5-ast";
import { applyEnv, applyEnvBdg, globalEnvAddBinding, makeExtEnv, setFBinding,
         theGlobalEnv, Env, ExtEnv } from "../L5/L5-env";
import { valueToString } from '../L5/L5-value';
import { isClosure, makeClosure, Value } from "../L5/L5-value";
import { isEmpty, first, rest } from '../shared/list';
import { Result, either, makeOk, bind, makeFailure } from "../shared/result";
import { parse as p } from "../shared/parser";
import { applyPrimitive } from "../L5/evalPrimitive";

// ========================================================
// Concrete Continuation datatype
// type Cont = (res: Value | Error) => Value | Error;
// type ContArray = (results: Array<Value | Error>) => Value | Error;
export type Cont = IfCont | FirstCont | SetCont | AppCont1 | ExpsCont1 | DefCont | TopCont;
export type ContArray = LetCont | LetrecCont | AppCont2 | ExpsCont2;

export const isCont = (x: any): x is Cont =>
    isIfCont(x) || isFirstCont(x) || isSetCont(x) ||
    isAppCont1(x) || isExpsCont1(x) || isDefCont(x) || isTopCont(x);
export const isContArray = (x: any): x is ContArray =>
    isLetCont(x) || isLetrecCont(x) || isAppCont2(x) || isExpsCont2(x);

export interface TopCont {tag: "TopCont"}
export const makeTopCont = (): TopCont => ({tag: "TopCont"});
export const isTopCont = (x: any): x is TopCont => x.tag === "TopCont";

export interface IfCont {tag: "IfCont", exp: IfExp, env: Env, cont: Cont}
export const makeIfCont = (exp: IfExp, env: Env, cont: Cont): IfCont => ({tag: "IfCont", env: env, exp: exp, cont: cont});
export const isIfCont = (x: any): x is IfCont => x.tag === "IfCont";

export interface FirstCont {tag: "FirstCont", exps: Exp[], env: Env, cont: Cont}
export const makeFirstCont = (exps: Exp[], env: Env, cont: Cont): FirstCont => ({tag: "FirstCont", env: env, exps: exps, cont: cont});
export const isFirstCont = (x: any): x is FirstCont => x.tag === "FirstCont";

export interface SetCont {tag: "SetCont", exp: SetExp, env: Env, cont: Cont}
export const makeSetCont = (exp: SetExp, env: Env, cont: Cont): SetCont => ({tag: "SetCont", env: env, exp: exp, cont: cont});
export const isSetCont = (x: any): x is SetCont => x.tag === "SetCont";

export interface AppCont1 {tag: "AppCont1", exp: AppExp, env: Env, cont: Cont}
export const makeAppCont1 = (exp: AppExp, env: Env, cont: Cont): AppCont1 => ({tag: "AppCont1", env: env, exp: exp, cont: cont});
export const isAppCont1 = (x: any): x is AppCont1 => x.tag === "AppCont1";

export interface ExpsCont1 {tag: "ExpsCont1", exps: Exp[], env: Env, cont: ContArray}
export const makeExpsCont1 = (exps: Exp[], env: Env, cont: ContArray): ExpsCont1 => ({tag: "ExpsCont1", env: env, exps: exps, cont: cont});
export const isExpsCont1 = (x: any): x is ExpsCont1 => x.tag === "ExpsCont1";

export interface LetCont {tag: "LetCont", exp: LetExp, env: Env, cont: Cont}
export const makeLetCont = (exp: LetExp, env: Env, cont: Cont): LetCont => ({tag: "LetCont", env: env, exp: exp, cont: cont});
export const isLetCont = (x: any): x is LetCont => x.tag === "LetCont";

export interface LetrecCont {tag: "LetrecCont", exp: LetrecExp, env: ExtEnv, cont: Cont}
export const makeLetrecCont = (exp: LetrecExp, env: ExtEnv, cont: Cont): LetrecCont => ({tag: "LetrecCont", env: env, exp: exp, cont: cont});
export const isLetrecCont = (x: any): x is LetrecCont => x.tag === "LetrecCont";

export interface AppCont2 {tag: "AppCont2", proc: Result<Value>, env: Env, cont: Cont}
export const makeAppCont2 = (proc: Result<Value>, env: Env, cont: Cont): AppCont2 => ({tag: "AppCont2", proc: proc, env: env, cont: cont});
export const isAppCont2 = (x: any): x is AppCont2 => x.tag === "AppCont2";

export interface ExpsCont2 {tag: "ExpsCont2", firstVal: Value, cont: ContArray}
export const makeExpsCont2 = (firstVal: Value, cont: ContArray): ExpsCont2 => ({tag: "ExpsCont2", firstVal: firstVal, cont: cont});
export const isExpsCont2 = (x: any): x is ExpsCont2 => x.tag === "ExpsCont2";

export interface DefCont {tag: "DefCont", exp: DefineExp, exps: Exp[], cont: Cont}
export const makeDefCont = (exp: DefineExp, exps: Exp[], cont: Cont): DefCont => ({tag: "DefCont", exp: exp, exps: exps, cont: cont});
export const isDefCont = (x: any): x is DefCont => x.tag === "DefCont";

export type InstructionSet =
    'applyCont' | 'applyContArray' | 'halt' |
    'applyIfCont' | 'applyFirstCont' | 'applySetCont' |
    'applyLetCont' | 'applyLetrecCont' | 'applyAppCont2' | 'applyExpsCont2' |
    'applyAppCont1' | 'applyExpsCont1' | 'applyDefCont' |
    'applyTopCont' | 'evalCont' | 'evalIf' | 'evalProc' | 'evalApp' | 'evalSet' |
    'evalLet' | 'evalLetrec' | 'evalSequence' | 'evalSequenceFR' |
    'evalExps' | 'evalExpsFR' |
    'evalDefineExps' |
    'applyProcedure' | 'applyClosure';

// ============================================================================
// REGISTERS
// Define global variable for registers - one for all the parameters of the functions
// involved in the interpreter.
export let contREG: Cont;
export let contArrayREG: ContArray | undefined;
export let valREG: Result<Value> | undefined;
export let valsREG: Result<Value[]> | undefined;
export let expREG: Exp;
export let expsREG: Exp[];
export let envREG: Env;
export let pcREG: InstructionSet;

export let TRACE = false;
export let MAXCOUNT = 10000000;

// ============= Debug utilities ===================
// Safe ways to print values, expressions, continuations registries
// taking into account undefined, Error and circular structures.

const up = (exp: Exp | undefined): string =>
    exp ? either(unparse(exp), identity, identity) : "undefined";

const pes = (es: Exp[]): string =>
    `[${join(", ", map(up, es))}]`;

const pc = (c: Cont | ContArray | undefined): string =>
    c === undefined ? "undefined" : c.tag;

const pv = (v: Result<Value> | undefined): string =>
    v ? either(v, valueToString, identity) : "undefined";

const pvs = (vs: Result<Value[]> | undefined): string =>
    vs ? either(vs, values => `[${join(", ", map(pv, map(makeOk, values)))}]`, identity) : "undefined";

export const dumpREG = () => {
    if (TRACE) {
        console.log(`In ${pcREG}:`);
        // console.log(valREG);
        console.log(`valREG = ${pv(valREG)}`);
        console.log(`valsREG = ${pvs(valsREG)}`);
        console.log(`expREG = ${up(expREG)}`);
        console.log(`expsREG = ${pes(expsREG)}`);
        console.log(`contREG = ${pc(contREG)}`);
        console.log(`contArrayREG = ${pc(contArrayREG)}`);
        console.log(`\n`);
    }
}

// type Cont = IfCont | FirstCont | SetCont | AppCont1 | ExpsCont1 | DefCont | TopCont;
// const applyContVal = (cont: ContVal, val: Value | Error): Value | Error =>
export const applyCont = (): void => {
    dumpREG();
    pcREG =
    isIfCont(contREG) ? 'applyIfCont' :
    isFirstCont(contREG) ? 'applyFirstCont' :
    isSetCont(contREG) ? 'applySetCont' :
    isAppCont1(contREG) ? 'applyAppCont1' :
    isExpsCont1(contREG) ? 'applyExpsCont1' :
    isDefCont(contREG) ? 'applyDefCont' :
    isTopCont(contREG) ? 'applyTopCont' :
    contREG;
}

// type ContArray = LetCont | LetrecCont | AppCont2 | ExpsCont2;
// const applyContArray = (cont: ContArray, vals: Array<Value | Error>): Value | Error =>
export const applyContArray = (): void => {
    dumpREG();
    pcREG =
    isLetCont(contArrayREG) ? 'applyLetCont' :
    isLetrecCont(contArrayREG) ? 'applyLetrecCont' :
    isAppCont2(contArrayREG) ? 'applyAppCont2' :
    isExpsCont2(contArrayREG) ? 'applyExpsCont2' :
    (console.error(`2 Unknown contArray ${JSON.stringify(contArrayREG)}`), 'halt');
}

// const applyTopCont = (cont: TopCont, val: Value | Error): Value | Error => {
export const applyTopCont = (): void => {
    // console.log(parsedToString(valREG));
    pcREG = 'halt';
}

// const applyIfCont = (cont: IfCont, val: Value | Error): Value | Error =>
export const applyIfCont = (): void => {
    dumpREG();
    if (isIfCont(contREG)) {
        const cont = contREG;
        either(valREG !== undefined ? valREG : makeOk(undefined),
               val => {
                   if (isTrueValue(val)) {
                       expREG = cont.exp.then;
                       envREG = cont.env;
                       contREG = cont.cont;
                       pcREG = "evalCont";
                   } else {
                       expREG = cont.exp.alt;
                       envREG = cont.env;
                       contREG = cont.cont;
                       pcREG = 'evalCont';
                   }
               },
               _ => {
                   contREG = cont.cont;
                   pcREG = "applyCont";
               });
    } else {
        console.error(`3 Unknown cont ${JSON.stringify(contREG)}`);
        pcREG = 'halt';
    }
}

// const applyLetCont = (cont: LetCont, vals: Array<Value | Error>): Value | Error =>
export const applyLetCont = (): void => {
    dumpREG();
    if (isLetCont(contArrayREG)) {
        const cont = contArrayREG;
        if (valsREG === undefined) {
            console.log(`Empty valsREG in applyLetCont`);
            return;
        }
        either(valsREG,
               vals => {
                   expsREG = cont.exp.body;
                   envREG = makeExtEnv(letVars(cont.exp), vals, cont.env);
                   contREG = cont.cont;
                   pcREG = "evalSequence";
               },
               message => {
                   valREG = makeFailure(message);
                   contREG = cont.cont;
                   pcREG = "applyCont";
               });
    } else {
        console.error(`Unknown cont ${JSON.stringify(contArrayREG)}`);
        pcREG = 'halt';
    }
}

// export const applyFirstCont = (cont: FirstCont, val: Value | Error): Value | Error =>
export const applyFirstCont = (): void => {
    dumpREG();
    if (isFirstCont(contREG)) {
        const cont = contREG;
        either(valREG !== undefined ? valREG : makeOk(undefined),
               () => {
                   expsREG = cont.exps;
                   envREG = cont.env;
                   contREG = cont.cont;
                   pcREG = "evalSequence";
               },
               _ => {
                   contREG = cont.cont;
                   pcREG = "applyCont";
               });
    } else {
        console.error(`Unknown cont ${JSON.stringify(contREG)}`);
        pcREG = 'halt';
    }
}

// export const applyLetrecCont = (cont: LetrecCont, vals: Array<Value | Error>): Value | Error => {
export const applyLetrecCont = (): void => {
    dumpREG();
    if (isLetrecCont(contArrayREG)) {
        const cont = contArrayREG;
        if (valsREG === undefined) {
            console.log(`Empty valsREG in applyLetrecCont`);
            return;
        }

        either(valsREG,
               vals => {
                   zipWith((bdg, cval) => setFBinding(bdg, cval), cont.env.frame.fbindings, vals);
                   expsREG = cont.exp.body;
                   envREG = cont.env;
                   contREG = cont.cont;
                   pcREG = "evalSequence";
               },
               message => {
                   valREG = makeFailure(message);
                   contREG = cont.cont;
                   pcREG = "applyCont";
               });
    } else {
        console.error(`Unknown cont ${JSON.stringify(contArrayREG)}`);
        pcREG = 'halt';
    }
}

// export const applySetCont = (cont: SetCont, val: Value | Error): Value | Error => {
export const applySetCont = (): void => {
    dumpREG();
    if (isSetCont(contREG)) {
        const cont = contREG;
        either(valREG !== undefined ? valREG : makeOk(undefined),
               val => {
                   const v = cont.exp.var.var;
                   const bdgResult = applyEnvBdg(cont.env, v);
                   either(bdgResult,
                          bdg => {
                              setFBinding(bdg, val);
                              valREG = undefined;
                              contREG = cont.cont;
                              pcREG = "applyCont";
                          },
                          _ => {
                              valREG = makeFailure(`var not found: ${JSON.stringify(v, null, 2)}`);
                              contREG = cont.cont;
                              pcREG = "applyCont";
                          });
               },
               _ => {
                   contREG = cont.cont;
                   pcREG = "applyCont";
               });
    } else {
        console.error(`Unknown cont ${JSON.stringify(contREG)}`);
        pcREG = 'halt';
    }
}

// export const applyAppCont1 = (cont: AppCont1, val: Value | Error): Value | Error =>
export const applyAppCont1 = (): void => {
    dumpREG();
    if (isAppCont1(contREG)) {
        if (valREG === undefined) {
            console.error("valREG undefined in applyAppCont1");
            return;
        }
        expsREG = contREG.exp.rands;
        envREG = contREG.env;
        contArrayREG = makeAppCont2(valREG, contREG.env, contREG.cont)
        pcREG = 'evalExps';
    } else {
        console.error(`Unknown cont ${JSON.stringify(contREG)}`);
        pcREG = 'halt';
    }
}

// export const applyAppCont2 = (cont: AppCont2, vals: Array<Value | Error>): Value | Error =>
export const applyAppCont2 = (): void => {
    dumpREG();
    if (isAppCont2(contArrayREG)) {
        valREG = contArrayREG.proc;
        contREG = contArrayREG.cont;
        pcREG = 'applyProcedure';
    } else {
        console.error(`Unknown cont ${JSON.stringify(contArrayREG)}`);
        pcREG = 'halt';
    }
}

// export const applyExpsCont1 = (cont: ExpsCont1, val: Value | Error): Value | Error =>
export const applyExpsCont1 = (): void => {
    dumpREG();
    if (isExpsCont1(contREG)) {
        const cont = contREG;
        either(valREG !== undefined ? valREG : makeOk(undefined),
               val => {
                   expsREG = cont.exps;
                   envREG = cont.env;
                   contArrayREG = makeExpsCont2(val, cont.cont);
                   pcREG = "evalExps";
               },
               message => {
                   contArrayREG = cont.cont;
                   valsREG = makeFailure(message);
                   pcREG = "applyContArray";
               });
    } else {
        console.error(`Unknown cont ${JSON.stringify(contREG)}`);
        pcREG = 'halt';
    }
}

// export const applyExpsCont2 = (cont: ExpsCont2, vals: Array<Value | Error>): Value | Error =>
export const applyExpsCont2 = (): void => {
    dumpREG();
    if (valsREG === undefined) {
        console.log(`Empty valsREG in applyExpsCont2`);
        return;
    }
    if (isExpsCont2(contArrayREG)) {
        const cont = contArrayREG;
        either(valsREG,
               vals => {
                   valsREG = makeOk([cont.firstVal, ...vals]);
                   contArrayREG = cont.cont;
                   pcREG = "applyContArray";
               },
               message => {
                   valsREG = makeFailure(message);
                   contArrayREG = cont.cont;
                   pcREG = "applyContArray";
               });
    } else {
        console.error(`Unknown cont ${JSON.stringify(contArrayREG)}`);
        pcREG = 'halt';
    }
}

// export const applyDefCont = (cont: DefCont, val: Value | Error): Value | Error => {
export const applyDefCont = (): void => {
    dumpREG();
    if (isDefCont(contREG)) {
        const cont = contREG;
        either(valREG !== undefined ? valREG : makeOk(undefined),
               val => {
                   globalEnvAddBinding(cont.exp.var.var, val);
                   expsREG = cont.exps;
                   envREG = theGlobalEnv;
                   contREG = cont.cont;
                   pcREG = "evalSequence";
               },
               _ => {
                   contREG = cont.cont;
                   pcREG = "applyCont";
               });
    } else {
        console.error(`Unknown cont ${JSON.stringify(contREG)}`);
        pcREG = 'halt';
    }
}

// ========================================================
// Eval functions

// export const evalCont = (exp: CExp | Error, env: Env, cont: Cont): Value | Error =>
export const evalCont = (): void => {
    dumpREG();
    if (isNumExp(expREG)) {
        valREG = makeOk(expREG.val);
        pcREG = 'applyCont';
    } else if (isBoolExp(expREG)) {
        valREG = makeOk(expREG.val);
        pcREG = 'applyCont';
    } else if (isStrExp(expREG)) {
        valREG = makeOk(expREG.val);
        pcREG = 'applyCont';
    } else if (isPrimOp(expREG)) {
        valREG = makeOk(expREG);
        pcREG = 'applyCont';
    } else if (isVarRef(expREG)) {
        valREG = applyEnv(envREG, expREG.var);
        pcREG = 'applyCont';
    } else if (isLitExp(expREG)) {
        valREG = makeOk(expREG.val);
        pcREG = 'applyCont';
    } else if (isIfExp(expREG)) {
        pcREG = 'evalIf';
    } else if (isProcExp(expREG)) {
        pcREG = 'evalProc';
    } else if (isLetExp(expREG)) {
        pcREG = 'evalLet';
    } else if (isLetrecExp(expREG)) {
        pcREG = 'evalLetrec';
    } else if (isSetExp(expREG)) {
        pcREG = 'evalSet';
    } else if (isAppExp(expREG)) {
        pcREG = 'evalApp';
    } else {
        valREG = makeFailure(`Bad L5 AST ${JSON.stringify(expREG, null, 2)}`);
        pcREG = 'applyCont';
    }
}

export const isTrueValue = (x: Value): boolean =>
    ! (x === false);

// const evalIf = (exp: IfExp, env: Env, cont: Cont): Value | Error =>
export const evalIf = (): void => {
    dumpREG();
    if (isIfExp(expREG)) {
        contREG = makeIfCont(expREG, envREG, contREG);
        expREG = expREG.test;
        pcREG = 'evalCont';
    } else {
        valREG = makeFailure(`Bad expREG in evalIf ${JSON.stringify(expREG, null, 2)}`);
        pcREG = 'halt';
    }
}

// const evalProc = (exp: ProcExp, env: Env, cont: Cont): Value | Error =>
export const evalProc = (): void => {
    dumpREG();
    if (isProcExp(expREG)) {
        valREG = makeOk(makeClosure(expREG.args, expREG.body, envREG));
        pcREG = 'applyCont';
    } else {
        valREG = makeFailure(`Bad expREG in evalProc ${JSON.stringify(expREG, null, 2)}`);
        pcREG = 'halt';
    }
}

// Return the vals (rhs) of the bindings of a let expression
const letVals = (exp: LetExp | LetrecExp): CExp[] =>
        map((b) => b.val, exp.bindings);

// Return the vars (lhs) of the bindings of a let expression
const letVars = (exp: LetExp | LetrecExp): string[] =>
        map((b) => b.var.var, exp.bindings);

// LET: Direct evaluation rule without syntax expansion
// compute the values, extend the env, eval the body.
// const evalLet = (exp: LetExp, env: Env, cont: Cont): Value | Error =>
export const evalLet = (): void => {
    dumpREG();
    if (isLetExp(expREG)) {
        expsREG = letVals(expREG);
        contArrayREG = makeLetCont(expREG, envREG, contREG);
        pcREG = 'evalExps';
    } else {
        valREG = makeFailure(`Bad expREG in evalLet ${JSON.stringify(expREG, null, 2)}`);
        pcREG = 'halt';
    }
}

// Evaluate an array of expressions in sequence - pass the result of the last element to cont
// @Pre: exps is not empty
// export const evalSequence = (exps: Exp[], env: Env, cont: Cont): Value | Error =>
export const evalSequence = (): void => {
    dumpREG();
    if (isEmpty(expsREG)) {
        valREG = makeFailure("Empty Sequence");
        pcREG = 'applyCont';
    } else {
        expREG = first(expsREG);
        expsREG = rest(expsREG);
        pcREG = 'evalSequenceFR';
    }
}

// const evalSequenceFR = (exp: Exp, exps: Exp[], env: Env, cont: Cont): Value | Error =>
const evalSequenceFR = (): void => {
    dumpREG();
    if (isDefineExp(expREG)) {
        pcREG = 'evalDefineExps';
    } else if (isEmpty(expsREG)) {
        pcREG = 'evalCont';
    } else {
        contREG = makeFirstCont(expsREG, envREG, contREG);
        pcREG = 'evalCont';
    }
}

// LETREC: Direct evaluation rule without syntax expansion
// 1. extend the env with vars initialized to void (temporary value)
// 2. compute the vals in the new extended env
// 3. update the bindings of the vars to the computed vals
// 4. compute body in extended env
// export const evalLetrec = (exp: LetrecExp, env: Env, cont: Cont): Value | Error => {
export const evalLetrec = (): void => {
    dumpREG();
    if (isLetrecExp(expREG)) {
        const vars = letVars(expREG);
        const vals = letVals(expREG);
        const extEnv = makeExtEnv(vars, repeat(undefined, vars.length), envREG);
        // Compute the vals in the extended env
        expsREG = vals;
        envREG = extEnv;
        contArrayREG = makeLetrecCont(expREG, extEnv, contREG);
        pcREG = 'evalExps';
    } else {
        valREG = makeFailure(`Bad expREG in evalLetrec ${JSON.stringify(expREG, null, 2)}`);
        pcREG = 'halt';
    }
}

// Handling of mutation with set!
// export const evalSet = (exp: SetExp, env: Env, cont: Cont): Value | Error =>
export const evalSet = (): void => {
    dumpREG();
    if (isSetExp(expREG)) {
        contREG = makeSetCont(expREG, envREG, contREG);
        expREG = expREG.val;
        pcREG = 'evalCont';
    } else {
        valREG = makeFailure(`Bad expREG in evalSet ${JSON.stringify(expREG, null, 2)}`);
        pcREG = 'halt';
    }
}

// export const evalApp = (exp: AppExp, env: Env, cont: Cont): Value | Error =>
export const evalApp = (): void => {
    dumpREG();
    if (isAppExp(expREG)) {
        contREG = makeAppCont1(expREG, envREG, contREG);
        expREG = expREG.rator;
        pcREG = 'evalCont';
    } else {
        valREG = makeFailure(`Bad expREG in evalApp ${JSON.stringify(expREG, null, 2)}`);
        pcREG = 'halt';
    }
}

// export const applyProcedure = (proc: Value | Error, args: Array<Value | Error>, cont: Cont): Value | Error =>
// Arguments in valREG, valsREG, contREG
export const applyProcedure = (): void => {
    dumpREG();
    if (valsREG === undefined) {
        console.log(`Empty valsREG in applyProcedure`);
        return;
    }

    either(valsREG,
           vals => {
               if (valREG === undefined) {
                   valREG = makeFailure(`"undefined" procedure in applyProcedure`);
                   pcREG = "applyCont";
                   return;
               }
               either(valREG,
                      val => {
                          if (isPrimOp(val)) {
                              valREG = applyPrimitive(val, vals);
                              pcREG = "applyCont";
                          } else if (isClosure(val)) {
                              pcREG = "applyClosure";
                          } else {
                              valREG = makeFailure(`Bad procedure: ${JSON.stringify(val, null, 2)}`);
                              pcREG = "applyCont";
                          }
                      },
                      _ => {
                          pcREG = "applyCont";
                      });
           },
           _ => {
               valREG = makeFailure(`Bad argument: ${JSON.stringify(valsREG, null, 2)}`);
               pcREG = "applyCont";
           });
}

// export const applyClosure = (proc: Closure, args: Value[], cont: Cont): Value | Error => {
// Parameters in proc = valREG, args = valsREG
export const applyClosure = (): void => {
    dumpREG();
    if (valREG) {
        either(valREG, 
               value => {
                   if (isClosure(value)) {
                       const vars = map((v: VarDecl) => v.var, value.params);
                       expsREG = value.body;
                       if (valsREG === undefined) {
                           console.error("Empty valsREG in applyClosure");
                           return;
                       }
                       either(valsREG,
                              vals => {
                                  envREG = makeExtEnv(vars, vals, value.env);
                                  pcREG = "evalSequence";
                              },
                              message => {
                                  valREG = makeFailure(message);
                                  pcREG = "applyCont";
                              });
                   }
               },
               _ => {
                   valREG = makeFailure(`Bad expREG in evalApp ${JSON.stringify(expREG, null, 2)}`);
                   pcREG = "halt";
               });
    } else {
        valREG = makeFailure(`valREG is undefined`)
        pcREG = "halt";
    }
}

// Evaluate an array of expressions - pass the result as an array to the continuation
// export const evalExps = (exps: Exp[], env: Env, cont: ContArray): Value | Error =>
export const evalExps = (): void => {
    dumpREG();
    if (isEmpty(expsREG)) {
        valsREG = makeOk([]);
        pcREG = 'applyContArray';
    } else {
        expREG = first(expsREG);
        expsREG = rest(expsREG);
        pcREG = 'evalExpsFR';
    }
}

// const evalExpsFR = (exp: Exp, exps: Exp[], env: Env, cont: ContArray): Value | Error =>
const evalExpsFR = (): void => {
    dumpREG();
    if (isDefineExp(expREG)) {
        valsREG = bind(unparse(expREG), e => makeFailure(`Unexpected define: ${JSON.stringify(e, null, 2)}`));
        pcREG = 'applyContArray';
    } else {
        if (contArrayREG === undefined) {
            console.error(`undefined contArrayREG`);
            return;
        }
        contREG = makeExpsCont1(expsREG, envREG, contArrayREG);
        pcREG = 'evalCont';
    }
}

// define always updates theGlobalEnv
// We only expect defineExps at the top level.
// const evalDefineExps = (exp: DefineExp, exps: Exp[], cont: Cont): Value | Error =>
export const evalDefineExps = (): void => {
    dumpREG();
    if (isDefineExp(expREG)) {
        contREG = makeDefCont(expREG, expsREG, contREG);
        expREG = expREG.val;
        envREG = theGlobalEnv;
        pcREG = 'evalCont';
    } else {
        valREG = makeFailure(`Bad expREG in evalDefine ${JSON.stringify(expREG, null, 2)}`);
        pcREG = 'halt';
    }
}

// Evaluate a program
// Main program - EVALUATION LOOP of the Virtual Machine
export const evalProgram = (program: Program): Result<Value> => {
    valREG = undefined;
    valsREG = undefined;
    expsREG = program.exps;
    envREG = theGlobalEnv;
    contREG = makeTopCont();
    contArrayREG = undefined;
    pcREG = 'evalSequence';
    let count = 0;
    while (true) {
        count++;
        if (pcREG === 'evalSequence') evalSequence();
        else if (pcREG === 'evalSequenceFR') evalSequenceFR();
        else if (pcREG === 'halt') break;
        else if (pcREG === 'applyCont') applyCont();
        else if (pcREG === 'applyContArray') applyContArray();
        else if (pcREG === 'applyIfCont') applyIfCont();
        else if (pcREG === 'applyFirstCont') applyFirstCont();
        else if (pcREG === 'applySetCont') applySetCont();
        else if (pcREG === 'applyLetCont') applyLetCont();
        else if (pcREG === 'applyLetrecCont') applyLetrecCont();
        else if (pcREG === 'applyAppCont2') applyAppCont2();
        else if (pcREG === 'applyExpsCont2') applyExpsCont2();
        else if (pcREG === 'applyAppCont1') applyAppCont1();
        else if (pcREG === 'applyExpsCont1') applyExpsCont1();
        else if (pcREG === 'applyDefCont') applyDefCont();
        else if (pcREG === 'applyTopCont') applyTopCont();
        else if (pcREG === 'evalCont') evalCont();
        else if (pcREG === 'evalIf') evalIf();
        else if (pcREG === 'evalApp') evalApp();
        else if (pcREG === 'evalProc') evalProc();
        else if (pcREG === 'evalSet') evalSet();
        else if (pcREG === 'evalLet') evalLet();
        else if (pcREG === 'evalLetrec') evalLetrec();
        else if (pcREG === 'evalExps') evalExps();
        else if (pcREG === 'evalExpsFR') evalExpsFR();
        else if (pcREG === 'evalDefineExps') evalDefineExps();
        else if (pcREG === 'applyProcedure') applyProcedure();
        else if (pcREG === 'applyClosure') applyClosure();
        else {
            console.error(`Bad instruction: ${pcREG}`);
            break;
        }
        if (count > MAXCOUNT) {
            console.error(`STOP: ${count} instructions`);
            dumpREG();
            break;
        }
    }
    return valREG ? valREG : makeOk(undefined);
}

export const evalParse = (s: string): Result<Value> =>
    bind(bind(p(s), parseL5Exp), (exp: Exp) => evalProgram(makeProgram([exp])));