// Act is the only entry point into the evaluator. It requires // that we already have a continuation. (I suggest cooking up one // that invokes Eval.) This whole function is one big loop. CPS // turns recursion into tail recursion, and our calling convention // turns tail recursion into iteration. BEHOLD! public static Datum Act(Action call) { while (call.target != Actor.Continue) { Shell.TraceAction("Entering", call); switch (call.target) { // Evaluate a single form. There's a case for each // sort of form. case Actor.Eval: { Datum exp = call.arg; Environment env = call.env; if (exp is Symbol) { // Symbol. Look up the value. call.target = Actor.Continue; call.arg = env.Lookup((Symbol)exp); goto NextCall; } if (exp is Pair) { // A list evaluates as a function call, unless // it's a special form. Pair form = (Pair)exp; if (form.car is Symbol) { string name = (form.car as Symbol).name; try { switch (name) { case "lambda": { // Evaluates to a closure. Pair formals = (Pair)form.Second; Datum body = form.Cdr.Cdr; call.target = Actor.Continue; call.arg = new Closure( env, formals, body); goto NextCall; } case "quote": { // Evaluates to its // (unevaluated) argument. call.target = Actor.Continue; call.arg = form.Second; goto NextCall; } case "begin": { // Evaluates to its last // argument, after evaluating // the others in sequence for // their side-effects. call.target = Actor.EvalSequence; call.arg = form.cdr; goto NextCall; } case "if": { // It looks exactly like every // other IF you've ever seen, // except that this one is CPS // so all we do here is enqueue // evaluation of the test // expression followed by an // actor which will branch to // the then or else clause. Datum test = form.Second; Datum conseq = form.Third; Datum alts = form.Cdr.Cdr.Cdr; call.target = Actor.Eval; call.arg = test; call.env = env; call.next = new Action( Actor.ExecuteIf, new Pair(conseq, alts), env, call.next); goto NextCall; } case "cond": { // COND is sneaky and will live // entirely in its own actor. Datum clauses = form.Cdr; call.target = Actor.ExecuteCond; call.arg = new Pair(clauses, null); call.env = env; goto NextCall; } case "set!": { // Assignment. Enqueue // evaluation of the value // expression, followed by a // helper actor that does the // assignment. Symbol what = (Symbol)form.Second; Datum val_exp = form.Third; call.target = Actor.Eval; call.arg = val_exp; call.next = new Action( Actor.ExecuteSetQ, what, env, call.next); goto NextCall; } case "define": { // Bind a new symbol in the // current environment. Has a // helper actor. Datum what = form.Second; Symbol who; if (what is Symbol) { who = (Symbol)what; Datum val_exp = form.Third; call.target = Actor.Eval; call.arg = val_exp; call.env = env; } else { who = (Symbol)what.Car; Pair formals = (Pair)what.Cdr; Datum body = form.Cdr.Cdr; call.target = Actor.Continue; call.arg = new Closure( env, formals, body); } call.next = new Action( Actor.ExecuteDefine, who, env, call.next); goto NextCall; } case "let": { // A macro. Perform the // transformation and evaluate // the resulting expression. A // beautiful case for tail // calls. call.target = Actor.Eval; call.arg = Transform.Let(form); goto NextCall; } case "let*": { // Another macro. call.target = Actor.Eval; call.arg = Transform.LetStar(form); goto NextCall; } } } catch (NullReferenceException) { throw new BadFormException(name); } catch (InvalidCastException) { throw new BadFormException(name); } } // If we've gotten here, we aren't a special // form, so our list is a function application. // We'll evaluate each element of the list and // then feed the cdr to the car. I've just // mentioned two actions, and sure enough, // we've about to enqueue them both... call.target = Actor.EvalList; call.arg = new Pair(form, null); call.env = env; call.next = new Action( Actor.ExecuteApply, null, env, call.next); goto NextCall; } else { // All other data types (strings, numbers, &c) // are self-evaluating. call.target = Actor.Continue; call.arg = exp; goto NextCall; } } // Evaluate a list of forms, in sequence. case Actor.EvalList: { Pair p = (Pair)call.arg; Pair exps = (Pair)p.car; Pair acc = (Pair)p.cdr; if (call.HasResult) acc = new Pair(call.Result, acc); if (exps == null) { call.target = Actor.Continue; call.arg = acc; goto NextCall; } else { call.target = Actor.Eval; call.arg = exps.car; call.next = new Action( Actor.EvalList, new Pair(exps.cdr, acc), call.env, call.next); goto NextCall; } } // Receive the product of EvalList and feed it to // Apply. We need this glue so that we can also invoke // Apply normally, without a result. case Actor.ExecuteApply: { call.target = Actor.Apply; call.arg = call.Result; goto NextCall; } // Apply a function to its arguments. Ugly kludge: the // argument list is backwards, with the function itself // last. Sorry. It was easier to generate this way. case Actor.Apply: { Environment env = call.env; Pair vals = (Pair)call.arg; // Juggle the backwards linked list. List<Datum> args = new List<Datum>(); while (vals != null) { args.Add(vals.car); vals = (Pair)vals.cdr; } args.Reverse(); Datum func = args[0]; args.RemoveAt(0); // Apply a primitive procedure: call the // implementation delegate. Primitive prim = func as Primitive; if (prim != null) { // Ouch. CALL/CC's implementation has to be // "inline" so that it won't use the CLR stack // and violate CPS. It's okay for all the // other primitives to do so, because they'll // return immediately and are "conceptually // inline", if you will. But since CALL/CC // may not return normally, it can't get away // with being on the CLR stack. // // For all that, the actual implementation is // trivial: package up the Action chain into a // Scheme Continuation object, and apply the // argument to it. if (prim.name == "call-with-current-continuation") { Continuation current = new Continuation( new Action( call.next.target, call.next.arg, call.next.env, call.next.next)); Shell.TraceAction( "Creating continuation which will", call.next); Datum recip = args[0]; call.target = Actor.Apply; call.arg = new Pair(current, new Pair(recip, null)); goto NextCall; } else { // If it's not CALL/CC, just call the // delegate, passing an implicit argument // if there is one. call.target = Actor.Continue; if (prim.magicEnvironment) { args.Insert(0, env); } call.arg = prim.implementation(args); goto NextCall; } } // Apply a continuation. Delightfully simple. Continuation cont = func as Continuation; if (cont != null) { Shell.TraceAction( "Invoking continuation which will", cont.call); call.target = Actor.Continue; call.arg = args[0]; // We cheerfully blow away the existing value // of call.next, because this is a Scheme goto. // This is the place where we "break the // chain". call.next = cont.call; goto NextCall; } // Apply a closure. If you've read the wizard // book, you can recite the mantra in your sleep: // extend the defining environment, bind the params // to the formals in the new environment, then // evaluate the body in the new environment... Closure closure = func as Closure; if (closure != null) { Environment new_env = new Environment(closure.env); Pair paramlist = closure.formals; foreach (Datum arg in args) { new_env.Bind((Symbol)paramlist.car, arg); paramlist = (Pair)paramlist.cdr; } call.target = Actor.EvalSequence; call.arg = closure.body; call.env = new_env; goto NextCall; } // Well, if we're still here, it means some jerk // has asked us to apply a non-function. throw new InapplicableException(func); } // Continue evaluating an IF. We end up here after // evaluating the test. We branch to the appropriate // result clause. case Actor.ExecuteIf: { Pair p = (Pair)call.arg; Datum conseq = p.car; Datum alts = p.cdr; if (!Datum.IsTrue(call.Result)) { call.target = Actor.EvalSequence; call.arg = alts; goto NextCall; } else { call.target = Actor.Eval; call.arg = conseq; goto NextCall; } } // Evaluate a COND. Just keep cdring down the list // until a clause matches. We'll do lots of two-action // calls to keep coming back here after evaluating a // test. If there's a result waiting for us, it means // this isn't our first trip through the loop and we // need to check if it's time yet to branch to the // consequent. case Actor.ExecuteCond: { Datum clauses = call.arg.Car; Datum thisConsequent = call.arg.Cdr; Environment env = call.env; // If we've just executed a successful test, // continue with the consequent. if (call.HasResult && Datum.IsTrue(call.Result)) { call.target = Actor.Eval; call.arg = thisConsequent; call.env = env; goto NextCall; } // Are we done? if (clauses == null) { // Didn't match any clauses. call.target = Actor.Continue; call.arg = new Unspecified(); goto NextCall; } Datum test = clauses.Car.First; Datum consequent = clauses.Car.Second; // Are we at an ELSE? Symbol symTest = test as Symbol; if (symTest != null && symTest.name == "else") { call.target = Actor.Eval; call.arg = consequent; call.env = env; goto NextCall; } // We've got more clauses. Do the test, and reenter. call.target = Actor.Eval; call.arg = test; call.env = env; call.next = new Action( Actor.ExecuteCond, new Pair(clauses.Cdr, consequent), env, call.next); goto NextCall; } // Continue assignment. We've just evaluated the // value, so assign it to the curried name. case Actor.ExecuteSetQ: { call.env.Set((Symbol)call.arg, call.Result); call.target = Actor.Continue; call.arg = new Unspecified(); goto NextCall; } // Similarly, continue definition by binding the // result expression to the curried name. case Actor.ExecuteDefine: { call.env.Bind((Symbol)call.arg, call.Result); call.target = Actor.Continue; call.arg = new Unspecified(); goto NextCall; } // Evaluate multiple expressions in order, discarding // all but the last result. case Actor.EvalSequence: { if (null == call.arg) { call.target = Actor.Continue; call.arg = new Pair(new Unspecified(), null); } Pair exps = (Pair)call.arg; Environment env = call.env; if (null == exps.cdr) { call.target = Actor.Eval; call.arg = exps.car; call.env = env; goto NextCall; } else { call.target = Actor.Eval; call.arg = exps.car; call.env = env; call.next = new Action( Actor.EvalSequence, exps.cdr, env, call.next); goto NextCall; } } } NextCall: // Ahh, the fabled land of NextCall, whither go all actors // when their part is played out. Here we process the // Continue actor by shuffling arg and result. We cdr down // the action chain and then finish the body of the while, // returning back to the very top of Evaluator.Act to begin // the cycle again. if (call.target != Actor.Continue) { call.ClearResult(); } if (call.target == Actor.Continue && call.next != null) { Shell.Trace("Continuing with result ", call.arg); call.next.Result = call.arg; call = call.next; } } // Well, we've managed to exit the great cosmic while loop. // Ths means that we hit an Actor.Continue token which didn't // have a next; in other words, the last actor told us to // continue but there's nothing to continue doing. This means // we've finally finished performing the act that we were // originally called for, "called" in (for once!) the prosaic // C# sense. And so... we return! Actually, honest-to-gosh // return, pop the CLR stack, bid a fond farewell to CPS-land. // Sayonara! Shell.Trace("Returning..."); return call.arg; }