public static void InitEnv(StepFunction step, Env env, string[]?cmdArgs = null) { env.Set(new Symbol("*ARGV*", NilV), new List((cmdArgs ?? new string[0]).Skip(1).Select(arg => new Str(arg) as MalType).ToLList(), ListType.List, NilV)); env.Set(new Symbol("eval", NilV), new Fn(args => args switch { (var Mal, null) => Eval(Mal, env), _ => throw new Exception($"'eval' function requires one argument, but got {args.JoinMalTypes(", ")}") }, NilV));
static Env CreateTopLevelEnv() { Env env = new Env(); env.Set(Reader.AddSymbol("+") as Symbol, new Func_Add()); env.Set(Reader.AddSymbol("-") as Symbol, new Func_Sub()); env.Set(Reader.AddSymbol("*") as Symbol, new Func_Mul()); env.Set(Reader.AddSymbol("/") as Symbol, new Func_Div()); return(env); }
// Setup the REPL. public MalRepl() { // Load built-in functions into the initial eval environment. try { myEnv = new Env(null); // Arithmetic. myEnv.Set("+", BuiltInAdd); myEnv.Set("*", BuiltInMultiply); myEnv.Set("-", BuiltInSubtract); myEnv.Set("/", BuiltInDivide); // List manipulation. myEnv.Set("list", BuiltinList); myEnv.Set("list?", BuiltinIsList); myEnv.Set("empty?", BuiltinEmpty); myEnv.Set("count", BuiltinCount); // Functions defined using Mal itself. EVAL(READ("(def not (fn (a) (if a false true)))"), myEnv); } catch (MalEvalError e) { Console.WriteLine(e.Message); } }
static int Main(string[] args) { Env env = CreateTopLevelEnv(); List <Value> malArgs = new List <Value>(); for (int i = 1; i < args.Length; i++) { malArgs.Add(new Str(args[i])); } env.Set(Reader.AddSymbol("*ARGV*") as Symbol, new List(malArgs)); if (args.Length == 0) { return(Repl(env)); } #if NATIVE_LOAD_FILE FileInfo fi = new FileInfo(args[0]); if (fi.Exists) { using (Stream stream = fi.OpenRead()) LoadStream(stream, env); } else { Console.Error.WriteLine("ERROR: unable open file '{0}'", args[0]); return(1); } #else List loaderArgs = new List(new List <Value>() { new Str(args[0]) }); try { var val = env.Get(Reader.Load_file); if (val is Closure cls) { EVAL(cls.Body, cls.CreateEnv(loaderArgs)); } else if (val is Func_Native fn) { fn.Apply(loaderArgs); } else { throw new MalException("unknown function to evaluate file"); } } catch (MalException ex) { Console.Error.WriteLine("ERROR: {0}", ex.Message); return(1); } #endif return(0); }
// Setup the REPL. public MalRepl() { // Load built-in functions into the initial eval environment. try { myEnv = new Env(null); // Load core functions. foreach (var thing in MalNameSpace) { myEnv.Set(new MalSym(thing.Key), thing.Value); } // Load the special eval core function. We have to do it here to access the REPL env. myEnv.Set(new MalSym("eval"), new MalFunc(args => { return(EVAL(args[0], myEnv)); })); // some convenient test files // "F:\Mal-development\mal-tests\mal-step6-test-01.txt" // "F:\Mal-development\mal-tests\mal-code-01.txt" // (load-file "F:\Mal-development\mal-tests\mal-code-01.txt") // (eval (read-string (slurp "F:\Mal-development\mal-tests\mal-code-01.txt"))) // Add 'core' functions defined using Mal itself. EVAL(READ("(def not (fn (a) (if a false true)))"), myEnv); // TODO -number of brackets is wrong and returns nil. EVAL(READ("(def load-file (fn (f) (eval (read-string (str \"(do \" (slurp f) \"))\")))))"), myEnv); // Add some of the test functions for convenience. EVAL(READ("(def sumdown (fn (N) (if (> N 0) (+ N (sumdown (- N 1))) 0)))"), myEnv); } catch (MalLookupError e) { Console.WriteLine(e.Message); } catch (MalEvalError e) { Console.WriteLine(e.Message); } }
static MalVal EvalDef(MalList astList, Env env) { // 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"); } MalVal result = EVAL(astList[2], env); env.Set(symbol.getName(), result); return(result); }
static MalVal EvalLet(MalList astList, Env env) { // 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. // Confirm the structure of the let* form. 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. Env LetEnv = 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 is the value of the key symbol in Let's environment, E.g. (+ 2 3) MalVal val = EVAL(bindingsList[i + 1], LetEnv); // This expression can refer to earlier let bindings. // Now, store the new value in the environment. LetEnv.Set(bindingKey.getName(), val); // Note - this can silently redefine built-in functions e.g. if something like '+' is used. } // Using the populated Let* environment, evaluate and return the result form. // The Let* environment itself is discarded, unshadowing symbols in outer environments. return(EVAL(astList[2], LetEnv)); }
// Setup the REPL. public MalRepl() { // Load built-in functions into the initial eval environment. try { myEnv = new Env(null); // Arithmetic. myEnv.Set("+", BuiltInAdd); myEnv.Set("*", BuiltInMultiply); myEnv.Set("-", BuiltInSubtract); myEnv.Set("/", BuiltInDivide); // List manipulation. myEnv.Set("list", BuiltinList); myEnv.Set("list?", BuiltinIsList); myEnv.Set("empty?", BuiltinEmpty); myEnv.Set("count", BuiltinCount); } catch (MalEvalError e) { Console.WriteLine(e.Message); } }
// Setup the REPL. public MalRepl() { // Load built-in functions into the initial eval environment. try { myEnv = new Env(null); // Load core functions. foreach (var thing in MalNameSpace) { myEnv.Set(new MalSym(thing.Key), thing.Value); } // Add functions defined using Mal itself. EVAL(READ("(def not (fn (a) (if a false true)))"), myEnv); EVAL(READ("(def sumdown (fn (N) (if (> N 0) (+ N (sumdown (- N 1))) 0)))"), myEnv); } catch (MalEvalError e) { Console.WriteLine(e.Message); } }
static Value EVAL(Value arg, Env env) { if (arg is List lst) { if (lst.IsEmpty()) { return(arg); } Value val = lst.First(); if (val == Reader.Def) { if (lst.Count() != 3) { throw new MalException(Reader.Def.Name + " - syntax error"); } val = lst.Nth(1); if (val is Symbol sym) { val = EVAL(lst.Nth(2), env); env.Set(sym, val); return(val); } else { throw new MalException(Reader.Def.Name + " - symbol expected"); } } else if (val == Reader.Let) { if (lst.Count() != 3) { throw new MalException(Reader.Let.Name + " - syntax error"); } Env newEnv = new Env(env); Value val_1 = lst.Nth(1); if (val_1 is Sequence syms && syms.Count() % 2 == 0) { int cnt = syms.Count(); for (int i = 0; i < cnt; i += 2) { if (syms.Nth(i) is Symbol sym) { newEnv.Set(sym, EVAL(syms.Nth(i + 1), newEnv)); } else { throw new MalException(Reader.Let.Name + " - bindings - symbol is expected"); } } return(EVAL(lst.Nth(2), newEnv)); } else { throw new MalException(Reader.Let.Name + " - bad bindings"); } } else if (val == Reader.If) { int cnt = lst.Count(); if (cnt != 3 && cnt != 4) { throw new MalException(Reader.If.Name + " - syntax error"); } Value tst = EVAL(lst.Nth(1), env); if (tst == null || tst == Reader.Nil || tst == Reader.False) { return(cnt == 3 ? Reader.Nil : EVAL(lst.Nth(3), env)); } else { return(EVAL(lst.Nth(2), env)); } } else if (val == Reader.Do) { if (lst.Count() < 2) { throw new MalException(Reader.Do.Name + " - at least one argument is required"); } Value res = null; int cnt = lst.Count(); for (int i = 1; i < cnt; i++) { res = EVAL(lst.Nth(i), env); } return(res); } else if (val == Reader.Fn) { if (lst.Count() != 3) { throw new MalException(Reader.Fn.Name + " - syntax error"); } if (lst.Nth(1) is Sequence seq) { int cnt = seq.Count(); for (int i = 0; i < cnt; i++) { if (!(seq.Nth(i) is Symbol)) { throw new MalException(Reader.Fn.Name + " - symbol is expected for argument"); } } } else { throw new MalException(Reader.Fn.Name + " - expected sequence of arguments"); } if (lst.Nth(2) == null) { throw new MalException(Reader.Fn.Name + " - function body is expected"); } return(new Closure(lst.Nth(1) as Sequence, lst.Nth(2), env)); } else { Value ast = Eval_ast(arg, env); val = (ast as List).First(); if (val is Func_Native fn) { return(fn.Apply((ast as List).Rest())); } else if (val is Closure cls) { return(EVAL(cls.Body, cls.CreateEnv((ast as List).Rest()))); } else { throw new MalException("function is expected"); } } }
// 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)); } } }
// Setup the REPL. public MalRepl() { // Load built-in functions into the initial eval environment. try { myEnv = new Env(null); // Arithmetic. myEnv.Set("+", BuiltInAdd); myEnv.Set("*", BuiltInMultiply); myEnv.Set("-", BuiltInSubtract); myEnv.Set("/", BuiltInDivide); // List manipulation. myEnv.Set("list", BuiltinList); myEnv.Set("list?", BuiltinIsList); myEnv.Set("empty?", BuiltinEmpty); myEnv.Set("count", BuiltinCount); // Equality and friends. myEnv.Set("=", BuiltinEQ); myEnv.Set("<", BuiltinLT); myEnv.Set("<=", BuiltinLToE); myEnv.Set(">", BuiltinGT); myEnv.Set(">=", BuiltinGToE); // Printing myEnv.Set("pr-str", BuiltinPrStr); myEnv.Set("str", BuiltinStr); myEnv.Set("prn", BuiltinPrn); myEnv.Set("println", BuiltinPrintLn); // Functions defined using Mal itself. EVAL(READ("(def not (fn (a) (if a false true)))"), myEnv); } catch (MalEvalError e) { Console.WriteLine(e.Message); } }
// Evaluate the ast in the supplied environment. This is a bit of a monster but // has been left as per the guide to simplify TCO in step 5. // I've taken off the '*' and '!' chars from the special forms. static MalVal EVAL(MalVal ast, Env env) { if (ast is MalList astList) { if (astList.Count() <= 0) { // Empty list, return unchanged. return(ast); } else { // ast is a non-empty list. [0] should be a special form or a function name. if (!(astList[0] is MalSym sym)) { // Something like ([ 1 2]) perhaps. throw new MalEvalError("Expected function name or special form but got '" + astList[0] + "'"); } switch (sym.getName()) { 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"); } MalVal result = EVAL(astList[2], env); env.Set(symbol.getName(), 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.getName(), val); } // Using the populated Let environment, evaluate and return the result form. return(EVAL(astList[2], TempLetEnv)); case "do": // Evaluate all elements of list using eval_ast, retun the final eval'd element. MalList el = (MalList)eval_ast(astList.Rest(), env); return((el.Count() > 0) ? el[el.Count() - 1] : malNil); 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. Eval the 'false' branch, if any. if (astList.Count() == 4) { return(EVAL(astList[3], env)); } else { return(malNil); } } else { // Eval the 'true' branch. return(EVAL(astList[2], env)); } case "fn": // e.g. (def a (fn (i) (* i 2))) if (!(astList[1] is MalList a1f)) { throw new MalEvalError("Expected arg list but got: '" + astList[1].ToString(true) + "'"); } // Note - these functions can create Mal runtime errors - e.g. if undefined symbols are used. MalVal a2f = astList[2]; Env cur_env = env; return(new MalFunc(a2f, env, a1f, args => EVAL(a2f, new Env(cur_env, a1f, args)))); default: // Ast isn't a special form, so it should be a function. // Try to evaluate an ast as a MalFunc. MalList evaledList = (MalList)eval_ast(ast, env); MalVal listFirst = evaledList.First(); if (listFirst is MalFunc func) { // Ast *is* a function, so call it. return(func.Apply(evaledList.Rest())); } else { throw new MalEvalError("Can't use '" + listFirst.ToString(true) + "' as a function."); } } } } 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)); } }
static Value EVAL(Value arg, Env env) { for (;;) { arg = MACROEXPAND(arg, env); if (arg is List lst) { if (lst.IsEmpty()) { return(arg); } Value val = lst.First(); if (val == Reader.Def) { if (lst.Count() != 3) { throw new MalException(Reader.Def.Name + " - syntax error"); } val = lst.Nth(1); if (val is Symbol sym) { val = EVAL(lst.Nth(2), env); if (val is Closure cls && cls.IsMacro) { cls = cls.Clone(null) as Closure; cls.IsMacro = false; val = cls; } env.Set(sym, val); return(val); } else { throw new MalException(Reader.Def.Name + " - symbol expected"); } } else if (val == Reader.Let) { if (lst.Count() != 3) { throw new MalException(Reader.Let.Name + " - syntax error"); } Env newEnv = new Env(env); Value val_1 = lst.Nth(1); if (val_1 is Sequence syms && syms.Count() % 2 == 0) { int cnt = syms.Count(); for (int i = 0; i < cnt; i += 2) { if (syms.Nth(i) is Symbol sym) { newEnv.Set(sym, EVAL(syms.Nth(i + 1), newEnv)); } else { throw new MalException(Reader.Let.Name + " - bindings - symbol is expected"); } } arg = lst.Nth(2); env = newEnv; continue; } else { throw new MalException(Reader.Let.Name + " - bad bindings"); } } else if (val == Reader.If) { int cnt = lst.Count(); if (cnt != 3 && cnt != 4) { throw new MalException(Reader.If.Name + " - syntax error"); } Value tst = EVAL(lst.Nth(1), env); if (tst == null || tst == Reader.Nil || tst == Reader.False) { if (cnt == 3) { return(Reader.Nil); } arg = lst.Nth(3); continue; } else { arg = lst.Nth(2); continue; } } else if (val == Reader.Do) { if (lst.Count() < 2) { throw new MalException(Reader.Do.Name + " - at least one argument is required"); } int cnt = lst.Count() - 1; for (int i = 1; i < cnt; i++) { EVAL(lst.Nth(i), env); } arg = lst.Nth(cnt); continue; } else if (val == Reader.Fn) { if (lst.Count() != 3) { throw new MalException(Reader.Fn.Name + " - syntax error"); } if (lst.Nth(1) is Sequence seq) { int cnt = seq.Count(); for (int i = 0; i < cnt; i++) { if (!(seq.Nth(i) is Symbol)) { throw new MalException(Reader.Fn.Name + " - symbol is expected for argument"); } } } else { throw new MalException(Reader.Fn.Name + " - expected sequence of arguments"); } if (lst.Nth(2) == null) { throw new MalException(Reader.Fn.Name + " - function body is expected"); } return(new Closure(lst.Nth(1) as Sequence, lst.Nth(2), env)); } else if (val == Reader.Quote) { if (lst.Count() != 2) { throw new MalException(Reader.Quote.Name + " - syntax error"); } return(lst.Nth(1)); } else if (val == Reader.Quasiquote) { if (lst.Count() != 2) { throw new MalException(Reader.Quasiquote.Name + " - syntax error"); } return(QQuote(lst.Nth(1), env)); } else if (val == Reader.Defmacro) { if (lst.Count() != 3) { throw new MalException(Reader.Defmacro.Name + " - syntax error"); } val = lst.Nth(1); if (val is Symbol sym) { val = EVAL(lst.Nth(2), env); if (val is Closure cls) { cls = cls.Clone(null) as Closure; cls.IsMacro = true; env.Set(sym, cls); return(val); } else { throw new MalException(Reader.Defmacro.Name + " - closure is expected"); } } else { throw new MalException(Reader.Defmacro.Name + " - symbol expected"); } } else if (val == Reader.Macroexpand) { if (lst.Count() != 2) { throw new MalException(Reader.Macroexpand.Name + " - syntax error"); } return(MACROEXPAND(lst.Nth(1), env)); } else if (val == Reader.Try) { int cnt = lst.Count(); if (cnt != 2 && cnt != 3) { throw new MalException(Reader.Try.Name + " - syntax error"); } Symbol var = null; Value exp = null; if (cnt == 3) { if (lst.Nth(2) is List clst && clst.Count() == 3) { if (clst.Nth(1) is Symbol sym) { var = sym; } else { throw new MalException(Reader.Catch.Name + " - symbol is expected"); } exp = clst.Nth(2); } else { throw new MalException(Reader.Catch.Name + " - syntax error"); } } Value res; try { res = EVAL(lst.Nth(1), env); } catch (MalException ex) { if (var != null && exp != null) { Env newEnv = new Env(env); if (ex.Val != null) { newEnv.Set(var, ex.Val); } else { newEnv.Set(var, new Str(ex.Message)); } res = EVAL(exp, newEnv); } else { throw ex; } } return(res); }
static Value EVAL(Value arg, Env env) { for (;;) { arg = MACROEXPAND(arg, env); if (arg is List lst) { if (lst.IsEmpty()) { return(arg); } Value val = lst.First(); if (val == Reader.Def) { if (lst.Count() != 3) { throw new MalException(Reader.Def.Name + " - syntax error"); } val = lst.Nth(1); if (val is Symbol sym) { val = EVAL(lst.Nth(2), env); if (val is Closure cls && cls.IsMacro) { cls = cls.Clone(null) as Closure; cls.IsMacro = false; val = cls; } env.Set(sym, val); return(val); } else { throw new MalException(Reader.Def.Name + " - symbol expected"); } } else if (val == Reader.Let) { if (lst.Count() != 3) { throw new MalException(Reader.Let.Name + " - syntax error"); } Env newEnv = new Env(env); Value val_1 = lst.Nth(1); if (val_1 is Sequence syms && syms.Count() % 2 == 0) { int cnt = syms.Count(); for (int i = 0; i < cnt; i += 2) { if (syms.Nth(i) is Symbol sym) { newEnv.Set(sym, EVAL(syms.Nth(i + 1), newEnv)); } else { throw new MalException(Reader.Let.Name + " - bindings - symbol is expected"); } } arg = lst.Nth(2); env = newEnv; continue; } else { throw new MalException(Reader.Let.Name + " - bad bindings"); } } else if (val == Reader.If) { int cnt = lst.Count(); if (cnt != 3 && cnt != 4) { throw new MalException(Reader.If.Name + " - syntax error"); } Value tst = EVAL(lst.Nth(1), env); if (tst == null || tst == Reader.Nil || tst == Reader.False) { if (cnt == 3) { return(Reader.Nil); } arg = lst.Nth(3); continue; } else { arg = lst.Nth(2); continue; } } else if (val == Reader.Do) { if (lst.Count() < 2) { throw new MalException(Reader.Do.Name + " - at least one argument is required"); } int cnt = lst.Count() - 1; for (int i = 1; i < cnt; i++) { EVAL(lst.Nth(i), env); } arg = lst.Nth(cnt); continue; } else if (val == Reader.Fn) { if (lst.Count() != 3) { throw new MalException(Reader.Fn.Name + " - syntax error"); } if (lst.Nth(1) is Sequence seq) { int cnt = seq.Count(); for (int i = 0; i < cnt; i++) { if (!(seq.Nth(i) is Symbol)) { throw new MalException(Reader.Fn.Name + " - symbol is expected for argument"); } } } else { throw new MalException(Reader.Fn.Name + " - expected sequence of arguments"); } if (lst.Nth(2) == null) { throw new MalException(Reader.Fn.Name + " - function body is expected"); } return(new Closure(lst.Nth(1) as Sequence, lst.Nth(2), env)); } else if (val == Reader.Quote) { if (lst.Count() != 2) { throw new MalException(Reader.Quote.Name + " - syntax error"); } return(lst.Nth(1)); } else if (val == Reader.Quasiquote) { if (lst.Count() != 2) { throw new MalException(Reader.Quasiquote.Name + " - syntax error"); } return(QQuote(lst.Nth(1), env)); } else if (val == Reader.Defmacro) { if (lst.Count() != 3) { throw new MalException(Reader.Defmacro.Name + " - syntax error"); } val = lst.Nth(1); if (val is Symbol sym) { val = EVAL(lst.Nth(2), env); if (val is Closure cls) { cls = cls.Clone(null) as Closure; cls.IsMacro = true; env.Set(sym, cls); return(val); } else { throw new MalException(Reader.Defmacro.Name + " - closure is expected"); } } else { throw new MalException(Reader.Defmacro.Name + " - symbol expected"); } } else if (val == Reader.Macroexpand) { if (lst.Count() != 2) { throw new MalException(Reader.Macroexpand.Name + " - syntax error"); } return(MACROEXPAND(lst.Nth(1), env)); } else { Value ast = Eval_ast(arg, env); val = (ast as List).First(); if (val is Func_Native fn) { return(fn.Apply((ast as List).Rest())); } else if (val is Closure cls) { if (cls.IsMacro) { arg = EVAL(cls.Body, cls.CreateEnv((ast as List).Rest())); } else { arg = cls.Body; env = cls.CreateEnv((ast as List).Rest()); } continue; } else { throw new MalException("function is expected"); } } }
// Setup the REPL. public MalRepl() { // Load built-in functions into the initial eval environment. try { myEnv = new Env(null); // Load core functions. foreach (var thing in MalNameSpace) { MalSym mSym = new MalSym(thing.Key); if (myEnv.Find(mSym) == null) { myEnv.Set(mSym, thing.Value); } else { // The namespace itself may already have thrown an error but if not. throw new MalInternalError("Attempt to refine symbol: '" + mSym.ToString(true) + "' with '" + thing.Value.ToString(true) + "'"); } } // Load the special eval core function. We have to do it here to access the REPL env. myEnv.Set(new MalSym("eval"), new MalFunc(args => { return(EVAL(args[0], myEnv)); })); // some convenient test files // "F:\Mal-development\mal-tests\mal-step6-test-01.txt" // "F:\Mal-development\mal-tests\mal-code-01.txt" // (load-file "F:\Mal-development\mal-tests\mal-code-01.txt") // (eval (read-string (slurp "F:\Mal-development\mal-tests\mal-code-01.txt"))) // Add 'core' functions defined using Mal itself. EVAL(READ("(def not (fn (a) (if a false true)))"), myEnv); // Establish a gensym mechanism EVAL(READ("(def *gensym-counter* (atom 0))"), myEnv); EVAL(READ("(def gensym (fn [] (symbol (str \"G__\" (swap *gensym-counter* (fn [x] (+ 1 x)))))))"), myEnv); EVAL(READ("(def load-file (fn (f) (eval (read-string (str \"(do \" (slurp f) \"))\")))))"), myEnv); EVAL(READ("(defmacro cond (fn (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw \"odd number of forms to cond\")) (cons 'cond (rest (rest xs)))))))"), myEnv); EVAL(READ("(defmacro or (fn (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) (let (condvar (gensym)) `(let (~condvar ~(first xs)) (if ~condvar ~condvar (or ~@(rest xs)))))))))"), myEnv); } catch (System.TypeInitializationException e) { // Typically happens if there is a duplicate symbol in the namespace list Console.WriteLine("Unrecoverable error: " + e.InnerException.Message); } catch (MalLookupError e) { Console.WriteLine(e.Message); } catch (MalEvalError e) { Console.WriteLine(e.Message); } catch (MalParseError e) { Console.WriteLine(e.Message); } }
static Value EVAL(Value arg, Env env) { if (arg is List lst) { if (lst.IsEmpty()) { return(arg); } Value val = lst.First(); if (val == Reader.Def) { if (lst.Count() != 3) { throw new MalException(Reader.Def.Name + " - syntax error"); } val = lst.Nth(1); if (val is Symbol sym) { val = EVAL(lst.Nth(2), env); env.Set(sym, val); return(val); } else { throw new MalException(Reader.Def.Name + " - symbol expected"); } } else if (val == Reader.Let) { if (lst.Count() != 3) { throw new MalException(Reader.Let.Name + " - syntax error"); } Env newEnv = new Env(env); Value val_1 = lst.Nth(1); if (val_1 is Sequence syms && syms.Count() % 2 == 0) { int cnt = syms.Count(); for (int i = 0; i < cnt; i += 2) { if (syms.Nth(i) is Symbol sym) { newEnv.Set(sym, EVAL(syms.Nth(i + 1), newEnv)); } else { throw new MalException(Reader.Let.Name + " - bindings - symbol is expected"); } } return(EVAL(lst.Nth(2), newEnv)); } else { throw new MalException(Reader.Let.Name + " - bad bindings"); } } else { Value ast = Eval_ast(arg, env); val = (ast as List).First(); if (val is Func_Native fn) { return(fn.Apply((ast as List).Rest())); } else { throw new MalException("function is expected"); } } }