コード例 #1
0
ファイル: MalRepl.cs プロジェクト: KineticLensman/JK-s-Lisp
        static MalVal EvalIf(MalList astList, Env env)
        {
            // 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(new MalNil());
                }
            }
            else
            {
                // Eval the 'true' branch.
                return(EVAL(astList[2], env));
            }
        }
コード例 #2
0
        static protected MalNum ProcessNumbers(MalList args, NumericOp op)
        {
            double result = 0;

            // Initiate the calculation with the first arg.
            switch (args[0])
            {
            case MalNum num:
                result = (double)num;
                // Handle the special case of (- 1).
                if (args.Count() == 1 && op == NumericOp.Subtract)
                {
                    double negative = (double)num;
                    return(new MalNum(-negative));
                }
                break;

            default:
                // Should have already detected this but just in case.
                throw new MalInternalError("Non-number while calculating numbers: '" + args[0].ToString(true));
            }
            double divTest = 0;

            // Now apply the op to the remaining args.
            for (var i = 1; i < args.Count(); i++)
            {
                switch (args[i])
                {
                case MalNum num:
                    switch (op)
                    {
                    case NumericOp.Plus:
                        result += (double)num;
                        break;

                    case NumericOp.Multiply:
                        result *= (double)num;
                        break;

                    case NumericOp.Subtract:
                        result -= (double)num;
                        break;

                    case NumericOp.Divide:
                        divTest = (double)num;
                        if (i > 0 && divTest == 0)
                        {
                            throw new MalEvalError("Can't divide by zero");
                        }
                        result /= divTest;
                        break;
                    }
                    break;

                default:
                    throw new MalInternalError("Non-number while calculating numbers: '" + args[i].ToString(true));
                }
            }
            return(new MalNum(result));
        }
コード例 #3
0
ファイル: MalRepl.cs プロジェクト: KineticLensman/JK-s-Lisp
        static MalVal EvalDo(MalList astList, Env env)
        {
            // Evaluate all elements of list using eval_ast, retun the final eval'd element.
            // I peeked at the reference to get this right although in my defence I added error checking
            MalList el = (MalList)eval_ast(astList.Rest(), env);

            return((el.Count() > 0) ? el[el.Count() - 1] : new MalNil());
        }
コード例 #4
0
ファイル: Core.cs プロジェクト: KineticLensman/JK-s-Lisp
 //------------------ General utilities -------------------------------------------
 // Generic error check for core fns.
 public static void CheckArgCount(MalList args, int expectedCount, string callerName)
 {
     if (args.Count() != expectedCount)
     {
         throw new MalEvalError("'" + callerName + "' expected " + expectedCount + " arg(s) but got " + args.Count());
     }
 }
コード例 #5
0
ファイル: Env.cs プロジェクト: KineticLensman/JK-s-Lisp
        public Env(Env outer, MalList binds, MalList exprs)
        {
            // A key-MalVal dictionary.
            data = new Dictionary <string, MalVal>();

            // The scope we are created in. Null is taken to mean the REPL itself.
            this.outer = outer;

            if (binds.Count() != exprs.Count())
            {
                throw new MalEvalError("Incorrect number of fn arguments?");
            }
            // Bind (set) each element (symbol) to the respective elements of the exprs list
            for (var i = 0; i < binds.Count(); i++)
            {
                if (binds[i] is MalSym symbol)
                {
                    Set(symbol, exprs[i]);
                }
            }
        }
コード例 #6
0
ファイル: MalRepl.cs プロジェクト: KineticLensman/JK-s-Lisp
        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);
        }
コード例 #7
0
ファイル: Core.cs プロジェクト: KineticLensman/JK-s-Lisp
        // Mixing floats and integers introduces loads of special cases. I decided to
        // move the things that are type-dependent into helper functions to keep the
        // numeric Builtins themselves cleaner.
        static protected MalInt ProcessIntegers(MalList args, NumericOp op)
        {
            int result  = (int)(MalInt)args[0];
            int divTest = 0;

            // Handle the special case of (- 1).
            if (args.Count() == 1 && op == NumericOp.Subtract)
            {
                return(new MalInt(-result));
            }
            for (var i = 1; i < args.Count(); i++)
            {
                switch (op)
                {
                case NumericOp.Plus:
                    result += (int)(MalInt)args[i];
                    break;

                case NumericOp.Multiply:
                    result *= (int)(MalInt)args[i];
                    break;

                case NumericOp.Subtract:
                    result -= (int)(MalInt)args[i];
                    break;

                case NumericOp.Divide:
                    divTest = (int)(MalInt)args[i];
                    if (i > 0 && divTest == 0)
                    {
                        throw new MalEvalError("Can't divide by zero");
                    }
                    result /= divTest;
                    break;
                }
            }
            return(new MalInt(result));
        }
コード例 #8
0
ファイル: MalRepl.cs プロジェクト: KineticLensman/JK-s-Lisp
        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));
        }
コード例 #9
0
ファイル: Core.cs プロジェクト: KineticLensman/JK-s-Lisp
        //------------------ Utilities for Numbers and numeric ops -------------------------------------------
        // C#-Mal only allows integers and thus avoids a lot of complexity. I allow ints and floats
        // although the implementation is more complex.
        // Also unlike C#-Mal, numeric args can have arg lists of variable  length, e.g. (+ 1 2 3).

        // Decide whether to use int or float arithmetic and check for non-numerics. Applies
        // float contaigon if an expression has a mix of ints and floats.
        protected static bool AllIntegers(MalList args)
        {
            bool allInts = true;

            for (var index = 0; index < args.Count(); index++)
            {
                switch (args[index])
                {
                case MalInt i:
                    break;

                case MalFloat f:
                    allInts = false;
                    // Don't return at this point in case there are lurking non-numbers.
                    break;

                default:
                    throw new MalEvalError("non-number in numeric expression: '" + args[index].ToString(true));
                }
            }
            return(allInts);
        }
コード例 #10
0
ファイル: MalRepl.cs プロジェクト: KineticLensman/JK-s-Lisp
        // 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));
                }
            }
        }
コード例 #11
0
ファイル: MalRepl.cs プロジェクト: KineticLensman/JK-s-Lisp
        // 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));
                }
            }
        }
コード例 #12
0
        // 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));
            }
        }