diff --git a/chapter3g/interpreter.coffee b/chapter3g/interpreter.coffee index 690773a..aae2fa9 100644 --- a/chapter3g/interpreter.coffee +++ b/chapter3g/interpreter.coffee @@ -10,31 +10,47 @@ class LispInterpreterError extends Error the_false_value = (cons "false", "boolean") -# Base class that represents a value. Base class representing a LiSP -# value, a primitive, or a function +# An equality function that compares two values. This is necessary +# because object comparison in Javascipt is by reference, not by value. +# I want them to be by value, and this accomplishes that in this one +# special case. eq = (id1, id2) -> if id1 instanceof Symbol and id2 instanceof Symbol return id1.name == id2.name id1 == id2 +# Base class that represents a value. Base class representing a LiSP +# value, a primitive, or a function + class Value -# Represents the base class of a continuation. Calls to invoke resume -# the contained continuation, which is typecast to one of the specific -# continuation needs of conditional, sequence, etc... +# Represents the base class of a continuation. class Continuation + # Takes an existing continuation, which represents what to do when + # this continuation is invoked. constructor: (@kont) -> + + # Near as I can tell, this exists strictly to support call/cc invoke: (value, env, kont) -> if nilp cdr value @kont.resume (car value) else throw new LispInterpreterError "Continuations expect one argument" + + # As we're unwinding the stack, when we receive a new ktarget we've + # "breached" where this protection was created and need to resume the + # continuation passed there. unwind: (value, ktarget) -> if (@ == ktarget) then (@kont.resume value) else (@kont.unwind value, ktarget) + + # When a throw happens, we need to proceed down the stack looking + # for a CatchContinuation. This supports that for all continuations. catchLookup: (tag, kk) -> @kont.catchLookup tag, kk + + # Resume is literally the "What to do next." resume: (value) -> throw new LispInterpreterError "Wrong continuation for #{@_type}" @@ -102,7 +118,6 @@ evaluateQuote = (v, env, kont) -> evaluateIf = (exps, env, kont) -> evaluate (car exps), env, new IfCont(kont, (cadr exps), (caddr exps), env) - class IfCont extends Continuation constructor: (@kont, @ift, @iff, @env) -> @_type = "IfCont" @@ -111,7 +126,10 @@ class IfCont extends Continuation # Sequences: evaluates the current expression with a continuation that # represents "the next expression" in the sequence. Upon resumption, -# calls this function with that next expression. +# calls this function with that next expression. You can begin to +# note how the "what to do next" gets wrapped in deeper and deeper +# layers of context until the current needs are resolved and we +# finally reach that final expression. evaluateBegin = (exps, env, kont) -> if (pairp exps) @@ -178,22 +196,30 @@ extend = (env, names, values) -> # Now we start the invocation: this is applying the function. Let's # take it stepwise. -# Create a function environment. Calls the evaluateArguments(), which -# in turns goes down the list of arguments and creates a new -# environment, and then the continuation is to actually appy the nev -# environment to the existing function. +# Evaluate the application of a function call. The first step is to +# evaluate the first element of the function call, assuming it is or +# will resolve to a function (something of type * -> *). The +# continuation we create is to evaluate-function-cont. evaluateApplication = (exp, exps, env, kont) -> evaluate exp, env, (new EvFunCont kont, exps, env) +# After the function reference is finally generated, the resume() +# function here takes that reference and creates an +# apply-continuation, then calls evaluateArguments. See that. + class EvFunCont extends Continuation constructor: (@kont, @exp, @env) -> @_type = "EvFunCont" resume: (f) -> evaluateArguments @exp, @env, (new ApplyCont(@kont, f, @env)) -# Evaluate the first list, creating a new list of the arguments. Upon -# completion, resume the continuation with the gather phase +# Evaluate the argument list, creating a new list of the arguments. +# For each argument pair, in calls the gather-cont, which creates the +# actual pair and resumes by processing the next pair, building the +# new environment. When the list is exhausted, the EvFunCont's built +# ApplyCont() is called with the generated new environment and its +# associated thunk. evaluateArguments = (exp, env, kont) -> if (pairp exp) @@ -216,7 +242,9 @@ class GatherCont extends Continuation resume: (value) -> @kont.resume (cons @value, value) -# Upon resumption, invoke the function. +# Called with the new environment, and the orginal continuation that +# says what to do with the value generated by this function, now that +# it's actually been invoke. class ApplyCont extends Continuation constructor: (@kont, @fn, @env) -> @@ -237,6 +265,11 @@ class BottomCont extends Continuation catchLookup: (tag, kk) -> throw new LispInterpreterError "No associated catch" +# A block is an implicit begin. So we evaluate it's contents with a +# new block-environment, which will automatically unwind any contents +# found within by traversing up the environment stack looking for +# labels that match the one with which this block was created. + evaluateBlock = (label, body, env, kont) -> k = new BlockCont(kont, label) evaluateBegin body, (new BlockEnv env, label, k), k @@ -258,6 +291,13 @@ class BlockEnv extends FullEnv evaluateReturnFrom = (label, form, env, kont) -> evaluate form, env, (new ReturnFromCont kont, env, label) +# Note that when return-from-cont's body has been evaluated, we then +# unwind up the environment stack until we find the first block that +# has the same label and call the continuation saved there. Note that +# this is the *first* time that continuation and executable is stored +# on the environment, and isn't implicitly part of the continuation +# stack. + class ReturnFromCont extends Continuation constructor: (@kont, @env, @label) -> @_type = "ReturnFromCont" @@ -267,12 +307,24 @@ class ReturnFromCont extends Continuation evaluateCatch = (tag, body, env, kont) -> evaluate tag, env, (new CatchCont kont, body, env) +# catch-continuation receives (from evaluate) the processed value of a +# tag, the current environment, and what should happen after the +# context containing the catch is complete (the passed in 'kont' to +# evaluateCatch). That processed value becomes the label of the new +# labeled-continuation. + class CatchCont extends Continuation constructor: (@kont, @body, @env) -> @_type = "CatchFromCont" resume: (value) -> evaluateBegin @body, @env, (new LabeledCont @kont, value) +# Resume here does just that; it just resumes with the continuation +# passed in above. But should catch be *triggered* by a throw (and +# the throw-continuation), we get the contents of throw as a thing to +# be evaluated with its current environment, then continue with *this* +# as the continuation passed to throwing-continuation. + class LabeledCont extends Continuation constructor: (@kont, @tag) -> @_type = "LabeledFromCont" @@ -298,6 +350,12 @@ class UnwindCont extends Continuation resume: (value) -> @kont.unwind @value, @target +# Works its way through the stack environment stack, looking for +# ("breaching") protected blocks to unwind, and processing them as +# necessary. One of those will by definition be the continuation +# passed to the catch continuation, as the throwing-continuation is +# constructed with it as the address of the resumecont. + class ThrowingCont extends Continuation constructor: (@kont, @tag, @resumecont) -> @_type = "ThrowingCont" @@ -321,6 +379,8 @@ class ProtectReturnCont extends Continuation resume: (value) -> @kont.resume @value +# The bottom of the function pile, where native code is invoked. + class Primitive extends Value constructor: (@name, @nativ) -> @_type = "Primitive" @@ -337,9 +397,11 @@ defprimitive = (name, nativ, arity) -> definitial name, new Primitive name, (args, env, kont) -> vmargs = listToVector(args) if (vmargs.length == arity) + # Note that native.apply(ctx, vmargs) is expected to return a + # singleton, like all evaluate() passes. kont.resume (nativ.apply null, vmargs) else - throw new LispInterpreterError "Incorrect arity" + throw new LispInterpreterError "Incorrect arity" defpredicate = (name, nativ, arity) -> defprimitive name, ((a, b) -> if nativ.call(null, a, b) then true else the_false_value), arity @@ -415,6 +477,8 @@ metadata_evaluation = nvalu: (node) -> node.value mksymbols: (list) -> astSymbolsToLispSymbols(list) +# The hairness of this makes me doubt the wisdom of using Javascript. + straight_evaluation = listp: (cell) -> cell.__type == 'list' symbolp: (cell) -> typeof cell == 'string' and cell.length > 0 and cell[0] not in ["\"", ";"] @@ -430,6 +494,19 @@ straight_evaluation = nvalu: (cell) -> cell mksymbols: (cell) -> cell + +prox = + "quote": (body, env, kont, ix) -> evaluateQuote (cadr body), env, kont + "if": (body, env, kont, ix) -> evaluateIf (cdr body), env, kont + "begin": (body, env, kont, ix) -> evaluateBegin (cdr body), env, kont + "set!": (body, env, kont, ix) -> evaluateSet (ix.nvalu cadr body), (caddr body), env, kont + "lambda": (body, env, kont, ix) -> evaluateLambda (ix.mksymbols cadr body), (cddr body), env, kont + "block": (body, env, kont, ix) -> evaluateBlock (ix.nvalu cadr body), (cddr body), env, kont + "return": (body, env, kont, ix) -> evaluateReturnFrom (ix.nvalu cadr body), (caddr body), env, kont + "catch": (body, env, kont, ix) -> evaluateCatch (cadr body), (cddr body), env, kont + "throw": (body, env, kont, ix) -> evaluateThrow (cadr body), (caddr body), env, kont + "protect": (body, env, kont, ix) -> evaluateUnwindProtect (cadr body), (cddr body), env, kont + makeEvaluator = (ix = straight_evaluation, ty="straight") -> (exp, env, kont) -> if ix.symbolp exp @@ -440,18 +517,11 @@ makeEvaluator = (ix = straight_evaluation, ty="straight") -> body = ix.nvalu exp head = car body if ix.symbolp head - switch (ix.nvalu head) - when "quote" then evaluateQuote (cadr body), env, kont - when "if" then evaluateIf (cdr body), env, kont - when "begin" then evaluateBegin (cdr body), env, kont - when "set!" then evaluateSet (ix.nvalu cadr body), (caddr body), env, kont - when "lambda" then evaluateLambda (ix.mksymbols cadr body), (cddr body), env, kont - when "block" then evaluateBlock (ix.nvalu cadr body), (cddr body), env, kont - when "return-from" then evaluateReturnFrom (ix.nvalu cadr body), (caddr body), env, kont - when "catch" then evaluateCatch (cadr body), (cddr body), env, kont - when "throw" then evaluateThrow (cadr body), (caddr body), env, kont - when "unwind-protect" then evaluateUnwindProtect (cadr body), (cddr body), env, kont - else evaluateApplication (car body), (cdr body), env, kont + # Every call is boiled down to body/env/kont (with ix.nvalu tossed in for fun) + # It should be possible to move natives into an address space + if prox[(ix.nvalu head)]? + prox[(ix.nvalu head)](body, env, kont, ix) + else evaluateApplication (car body), (cdr body), env, kont else evaluateApplication (car body), (cdr body), env, kont else diff --git a/test/test_chapter3g.coffee b/test/test_chapter3g.coffee index 05a6c6f..5beb042 100644 --- a/test/test_chapter3g.coffee +++ b/test/test_chapter3g.coffee @@ -13,19 +13,19 @@ lisp = (ast) -> olisp ast, (i) -> ret = i return ret -describe "Core interpreter #3: Unwind-Protect", -> +describe "Core interpreter #3: Protect", -> it "unwinds but returns the value of the form", -> - expect(lisp read "(unwind-protect 1 2").to.equal(1) + expect(lisp read "(protect 1 2").to.equal(1) it "unwinds within an iffe to correctly evaluate the side-effect", -> - expect(lisp read "((lambda (c) (unwind-protect 1 (set! c 2)) c ) 0 ").to.equal(2) + expect(lisp read "((lambda (c) (protect 1 (set! c 2)) c ) 0 ").to.equal(2) it "Unwinds inside an unevaluated definition", -> - expect(lisp read "((lambda (c) (catch 111 (* 2 (unwind-protect (* 3 (throw 111 5)) (set! c 1) ))) ) 0)").to.equal(5) + expect(lisp read "((lambda (c) (catch 111 (* 2 (protect (* 3 (throw 111 5)) (set! c 1) ))) ) 0)").to.equal(5) it "Unwinds inside the evaluated definition, triggering the side effect", -> - expect(lisp read "((lambda (c) (catch 111 (* 2 (unwind-protect (* 3 (throw 111 5)) (set! c 1) ))) c ) 0)").to.equal(1) + expect(lisp read "((lambda (c) (catch 111 (* 2 (protect (* 3 (throw 111 5)) (set! c 1) ))) c ) 0)").to.equal(1) it "Same story, using block/return", -> - expect(lisp read "((lambda (c) (block A (* 2 (unwind-protect (* 3 (return-from A 5)) (set! c 1) ))) ) 0)").to.equal(5) + expect(lisp read "((lambda (c) (block A (* 2 (protect (* 3 (return A 5)) (set! c 1) ))) ) 0)").to.equal(5) it "Same story, using block/return with a triggered side-effect", -> - expect(lisp read "((lambda (c) (block A (* 2 (unwind-protect (* 3 (return-from A 5)) (set! c 1) ))) c ) 0)").to.equal(1) + expect(lisp read "((lambda (c) (block A (* 2 (protect (* 3 (return A 5)) (set! c 1) ))) c ) 0)").to.equal(1) #describe "Core interpreter #3: Try/Catch with Throw as a function", ->