コード例 #1
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));
                }
            }
        }
コード例 #2
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));
                }
            }
        }