// Evaluate the ast in the supplied environment. This is a bit of a monster but // has been left as per the guide to simplify Tail Call Optimisation (TCO) static MalVal EVAL(MalVal InitialAst, Env IntialEnv) { // The ast and env to be EVAL'd. Initially set to those passed to EVAL but // may be updated if there is a TCO loop. MalVal ast = InitialAst; Env env = IntialEnv; // Some eval cases will return out of the function, others keep looping (as per TCO) with // an updated ast / Env. The alternative is to call EVAL recursively, which has simpler // logic but which can blow up the stack. while (true) { if (ast is MalList astList) { // Console.WriteLine("EVAL: ast is " + astList.ToString(true)); if (astList.Count() <= 0) { // Empty list, return unchanged. return(ast); } else { // I used to check astList[0] was a symbol but this dissallows forms whose first el is an anon (fn..) special form. string symbolZero = astList[0] is MalSym ? ((MalSym)astList[0]).getName() : "__<*fn*>__"; switch (symbolZero) { case "def": // Evaluate all elements of list using eval_ast, retun the final eval'd element. // Should be something like (def a b). Set() symbol a to be the result of evaluating b. if (astList.Count() != 3 || !(astList[1] is MalSym symbol)) { throw new MalEvalError("'def' should be followed by a symbol and a value"); } // Must add the result of the EVAL to the environment so can't TCO loop here. MalVal result = EVAL(astList[2], env); env.Set(symbol, result); return(result); case "let": // Let syntax is (let <bindings-list> <result>), e.g. (let (p ( + 2 3) q (+ 2 p)) (+ p q)). // Bindings can refer to earlier bindings in the new let environment or to symbols // in the outer environment. Let defs hide hide same-named symbols in the outer scope. if (astList.Count() != 3) { throw new MalEvalError("'let' should have two arguments (bindings-list and result), but instead had " + (astList.Count() - 1)); } // Extract the first parameter - the bindings list. E.g. (p (+ 2 3) q (+ 2 p)) if (!(astList[1] is MalList bindingsList)) { throw new MalEvalError("'let' should be followed by a non-empty bindings list and a result form"); } if (bindingsList.Count() <= 1 || bindingsList.Count() % 2 != 0) { throw new MalEvalError("'let' bindings list should have an even number of entries"); } // Create a new Env - the scope of the Let form. It's discarded when done. Env TempLetEnv = new Env(env); // Process each pair of entries in the bindings list. for (int i = 0; i < bindingsList.Count(); i += 2) { // The first element should be a 'key' symbol. E.g. 'p'. if (!(bindingsList[i] is MalSym bindingKey)) { throw new MalEvalError("'let' expected symbol but got: '" + bindingsList[i].ToString(true) + "'"); } // The second element (e.g. (+ 2 3)) is the value of the key symbol in Let's environment MalVal val = EVAL(bindingsList[i + 1], TempLetEnv); // Store the new value in the environment. TempLetEnv.Set(bindingKey, val); } // TCO loop instead of EVAL(astList[2], TempLetEnv) ast = astList[2]; env = TempLetEnv; continue; case "do": // Something like (do <form> <form>) MalList formsToDo = astList.Rest(); int formsToDoCount = formsToDo.Count(); if (formsToDoCount == 0) { // Empty (do ) return(malNil); } else { // EVAL all forms in the (do ) body. // up to but not including the last one. if (formsToDoCount > 1) { eval_ast(formsToDo.GetRange(0, formsToDoCount - 2), env); } // EVAL the last form in the (do ) body via TCO loop, keeping the same env. ast = formsToDo[formsToDoCount - 1]; // TCO loop. continue; } case "if": // If has the syntax (if <cond> <true-branch> <optional-false-branch>) if (astList.Count() < 3 || astList.Count() > 4) { throw new MalEvalError("'if' should have a condition, true branch and optional false branch"); } // Evaluate the Cond part of the if. MalVal cond = EVAL(astList[1], env); if (cond is MalNil || cond is MalFalse) { // Cond is nil or false. if (astList.Count() == 4) { // Eval the 'false' branch via TCO loop. ast = astList[3]; continue; } else { // No 'false' branch. return(malNil); } } else { // Eval the 'true' branch via TCO loop. ast = astList[2]; continue; } case "fn": // e.g. (def a (fn (i) (* i 2))) or (def b (fn () (prn 1) (prn 2))) if (astList.Count() < 3) { throw new MalEvalError("fn must have an arg list and at least one body form"); } if (!(astList[1] is MalList FnArgList)) { // The 2nd element must be an arg list (possibly empty). throw new MalEvalError("Expected arg list but got: '" + astList[1].ToString(true) + "'"); } MalVal FnBodyExprs; // Unlike the Mal reference, this handles fns that have multiple expr forms. if (astList.Count() == 3) { FnBodyExprs = astList[2]; } else { // Either I've misunderstood something or the Reference doesn't handle some functions correctly. FnBodyExprs = astList.GetRange(2, astList.Count() - 1); } Env cur_env = env; // This is different from C#-Mal but their version doesn't work. return(new MalFunc(FnBodyExprs, env, FnArgList, args => EVAL(FnBodyExprs, new Env(cur_env, FnArgList, args)))); default: // Ast isn't a special form, so try to evaluate it as a function. MalList evaledList = (MalList)eval_ast(ast, env); MalVal listFirst = evaledList.First(); if (listFirst is MalFunc func) { if (func.IsCore) { // Can't TCO a core function so directly apply func. return(func.Apply(evaledList.Rest())); } else { // Non-ore function. // The ast is the one stored by the func. ast = func.ast; // Create a new env using func's env and params as the outer and binds arguments // and the rest of the current ast as the exprs argument. env = new Env(func.env, func.fparams, evaledList.Rest()); // and now 'apply' via the TCO loop. continue; } } else { return(listFirst); } } } } else { // If ast is not a list (e.g. a vector), return the result of calling eval_ast on it. return(eval_ast(ast, env)); } } }
// EVALuate the ast in the supplied env. EVAL directly handles special forms // and functions (core or otherwise) and passes symbols and sequences to its helper // function eval_ast (with which it is mutually recursive). // Conceptually, EVAL invokes itself recursively to evaluate some forms, sometimes in their // own new environment (e.g. (let)). In this implementation, Tail Call Optimisation (TCO) // is used to avoid recursion if possible. Specifically, the TCO version EVAL has ast and env // variables. It sets these to those passed in by the caller and then starts a while(true) // loop. If the last act of a pass through EVAL would have been to invoke EVAL recursively and // return that value, the TCO version sets the ast and env to whatever would have been // passed recursively, and goes back to the beginning of its loop, thus replacing // recursion with (stack efficient) iteration. Recursion is still used in cases where // the recursive call isn't the last thing EVAL does before returning the value, e.g. in // (def ) clause where the returned value must be stored in the env. // The downside of TCO is the increased complexity of the EVAL function itself, which // is now harder to refactor into helper-functions for the various special forms. static MalVal EVAL(MalVal InitialAst, Env IntialEnv) { // The ast and env to be EVAL'd (and which might be reset in a TCO loop). MalVal ast = InitialAst; Env env = IntialEnv; // The TCO loop. while (true) { if (ast is MalList astList) { // Console.WriteLine("EVAL: ast is " + astList.ToString(true)); if (astList.Count() <= 0) { // Empty list, return unchanged. return(ast); } else { // See if we have a special form or something else. string astListHead = astList[0] is MalSym ? ((MalSym)astList[0]).getName() : "__<*fn*>__"; switch (astListHead) { // Look for cases where the head of the ast list is a symbol (e.g. let, do, if) associated // with a special form. If it is, we can discard th head symbol, and the remaining elements of the // astList are the forms in the body of the special form. What happens to these depends on the form // that has been encountered, case "def": // Should be something like (def a b). Set() symbol a to be the result of evaluating b // and store the result in the env. // Evaluate all elements of list using eval_ast, retun the final eval'd element. if (astList.Count() != 3 || !(astList[1] is MalSym symbol)) { throw new MalEvalError("'def' should be followed by a symbol and a value"); } // Can't TCO loop because we must store the result of the EVAL before returning it. MalVal result = EVAL(astList[2], env); env.Set(symbol, result); return(result); case "let": // Let syntax is (let <bindings-list> <result>), e.g. (let (p ( + 2 3) q (+ 2 p)) (+ p q)). // Bindings can refer to earlier bindings in the new let environment or to symbols // in the outer environment. Let defs can hide same-named symbols in the outer scope. if (astList.Count() != 3) { throw new MalEvalError("'let' should have two arguments (bindings-list and result), but instead had " + (astList.Count() - 1)); } // Extract the first parameter - the bindings list. E.g. (p (+ 2 3) q (+ 2 p)) if (!(astList[1] is MalList bindingsList)) { throw new MalEvalError("'let' should be followed by a non-empty bindings list and a result form"); } if (bindingsList.Count() <= 1 || bindingsList.Count() % 2 != 0) { throw new MalEvalError("'let' bindings list should have an even number of entries"); } // Create a new Env - the scope of the Let form. It's discarded when done. Env TempLetEnv = new Env(env); // Process each pair of entries in the bindings list. for (int i = 0; i < bindingsList.Count(); i += 2) { // The first element should be a 'key' symbol. E.g. 'p'. if (!(bindingsList[i] is MalSym bindingKey)) { throw new MalEvalError("'let' expected symbol but got: '" + bindingsList[i].ToString(true) + "'"); } // The second element (e.g. (+ 2 3)) is the value of the key symbol in Let's environment MalVal val = EVAL(bindingsList[i + 1], TempLetEnv); // Store the new value in the environment. TempLetEnv.Set(bindingKey, val); } // TCO loop instead of EVAL(astList[2], TempLetEnv) ast = astList[2]; env = TempLetEnv; continue; case "do": // Something like (do <form> <form>). EVAL all forms in the (do ) body in current environment. // Some vars that make meaning clearer. MalList formsInDoBody = astList.Rest(); int formsInDoBodyCount = formsInDoBody.Count(); if (formsInDoBodyCount == 0) { // Empty (do ) return(malNil); } else { // Use eval_ast to evaluate the DoBody forms up to but NOT including the last one. if (formsInDoBodyCount > 1) { // If there is just one DoBody form, this won't get called. eval_ast(formsInDoBody.GetRange(0, formsInDoBodyCount - 2), env); } // Prep for the TCO by using the last DoBody form as the new ast. ast = formsInDoBody[formsInDoBodyCount - 1]; // TCO loop to process the final DoBody form. continue; } case "if": // If has the syntax (if <cond> <true-branch> <optional-false-branch>) if (astList.Count() < 3 || astList.Count() > 4) { throw new MalEvalError("'if' should have a condition, true branch and optional false branch"); } // Evaluate the Cond part of the if. MalVal cond = EVAL(astList[1], env); if (cond is MalNil || cond is MalFalse) { // Cond is nil or false. if (astList.Count() == 4) { // Eval the 'false' branch via TCO loop. ast = astList[3]; continue; } else { // No 'false' branch. return(malNil); } } else { // Eval the 'true' branch via TCO loop. ast = astList[2]; continue; } case "fn": // e.g. (def a (fn (i) (* i 2))) or (def b (fn () (prn 1) (prn 2))) if (astList.Count() < 3) { throw new MalEvalError("fn must have an arg list and at least one body form"); } if (!(astList[1] is MalList FnParamsList)) { // The 2nd element must be an arg list (possibly empty). throw new MalEvalError("Expected arg list but got: '" + astList[1].ToString(true) + "'"); } MalVal FnBodyForms; // Make a list of the forms in the funcion body (following the fn name and paramd list). if (astList.Count() == 3) { FnBodyForms = astList[2]; } else { // Either I've misunderstood something or the Reference doesn't handle some functions correctly. FnBodyForms = astList.GetRange(2, astList.Count() - 1); } Env cur_env = env; // This is different from C#-Mal but their version doesn't compile. Perhaps it's a C# version thing? return(new MalFunc(FnBodyForms, env, FnParamsList, args => EVAL(FnBodyForms, new Env(cur_env, FnParamsList, args)))); case "quote": if (astList.Count() == 2) { // Return the quoted thing. return(astList[1]); } throw new MalEvalError("quote expected 1 arg but had " + (astList.Count() - 1).ToString()); case "quasiquote": if (astList.Count() == 2) { // Return the result of processing the quasiquote. ast = ProcessQuasiquote(astList[1]); continue; } throw new MalEvalError("quasiquotequote expected 1 arg but had " + (astList.Count() - 1).ToString()); default: // Ast isn't a special form (it mey be a symbol or a function). // eval_ast the ast MalList evaledList = (MalList)eval_ast(ast, env); // Look at the head of the result. MalVal listFirst = evaledList.First(); if (listFirst is MalFunc func) { // It's a mal func. if (func.IsCore) { // Core funcs can be immediately applied and the value returned. return(func.Apply(evaledList.Rest())); } else { // Non-core function, i.e. one created by (fn...). // Use the Fn's ast and env info to set up a new EVAL (via TCO). // The ast to EVAL is the func's stored ast. ast = func.ast; // The new EVAL Env is made as follows... // The outer env is the func's env // The 'binds' are the func's params // The exprs are are the rest of the current ast. env = new Env(func.env, func.fparams, evaledList.Rest()); // Apply = EVAL the function indirectly via the TCO loop. continue; } } else { // This is typically the value of a symbol looked up by eval_ast. return(listFirst); } } } } else { // If ast is not a list (e.g. a vector), return the result of calling eval_ast on it. return(eval_ast(ast, env)); } } }