[feat] The CPS-based interpreter from Chapter 3 of LiSP, with tests.

This passes all the basic tests provided from Lisp In Small Pieces,
chapter 3.  This is a functional LiSP interpreter with limited ability
and very little bug handling, but it's a solid implementation that
matches the specification and passes the tests provided for the CPS
interpreter.

This commit does *not* provide any of the continuation variants
described in the book; it is only the base interpreter.
This commit is contained in:
Elf M. Sternberg 2015-07-07 19:57:35 -07:00
parent 560bcd4dda
commit bc857b19f1
2 changed files with 119 additions and 43 deletions

View File

@ -1,11 +1,12 @@
{listToString, listToVector, pairp, cons, car, cdr, caar, cddr, cdar, cadr, caadr, cadar, caddr, nilp, nil, setcdr, metacadr, setcar} = require "cons-lists/lists" {listToString, listToVector, pairp, cons, car, cdr, caar, cddr, cdar, cadr, caadr, cadar, caddr, nilp, nil, setcdr, metacadr, setcar} = require "cons-lists/lists"
readline = require "readline" readline = require "readline"
{inspect} = require "util" {inspect} = require "util"
print = require "./print"
ntype = (node) -> car node ntype = (node) -> car node
nvalu = (node) -> cadr node nvalu = (node) -> cadr node
the_false_value = (cons "false", "boolean")
class Value class Value
# Represents the base class of a continuation. Calls to invoke resume # Represents the base class of a continuation. Calls to invoke resume
@ -18,7 +19,7 @@ class Continuation
if nilp cdr v if nilp cdr v
@k.resume (car v) @k.resume (car v)
else else
throw "Continuations expect one argument", [v, env, kont] throw "Continuations expect one argument"
# Abstract class representing the environment # Abstract class representing the environment
@ -31,8 +32,8 @@ class Environment
# class, you have not # class, you have not
class NullEnv extends Environment class NullEnv extends Environment
lookup: -> throw "Unknown variable" lookup: (e) -> throw "Unknown variable #{e}"
update: -> throw "Unknown variable" update: (e) -> throw "Unknown variable #{e}"
# This appears to be an easy and vaguely abstract handle to the # This appears to be an easy and vaguely abstract handle to the
# environment. The book is not clear on the distinction between the # environment. The book is not clear on the distinction between the
@ -40,6 +41,7 @@ class NullEnv extends Environment
class FullEnv extends Environment class FullEnv extends Environment
constructor: (@others, @name) -> constructor: (@others, @name) ->
@_type = "FullEnv"
lookup: (name, kont) -> lookup: (name, kont) ->
@others.lookup name, kont @others.lookup name, kont
update: (name, kont, value) -> update: (name, kont, value) ->
@ -52,17 +54,18 @@ class FullEnv extends Environment
class VariableEnv extends FullEnv class VariableEnv extends FullEnv
constructor: (@others, @name, @value) -> constructor: (@others, @name, @value) ->
lookup: (name, kont) -> @_type = "VariableEnv"
if name == @name lookup: (name, kont) ->
kont.resume @value if name == @name
else kont.resume @value
@others.lookup name, kont else
update: (nam, kont, value) -> @others.lookup name, kont
if name == @name update: (name, kont, value) ->
@value = value if name == @name
kont.resume value @value = value
else kont.resume value
@others.update name, kont, value else
@others.update name, kont, value
# "Renders the quote term to the current continuation"; in a more # "Renders the quote term to the current continuation"; in a more
# familiar parlance, calls resume in the current context with the # familiar parlance, calls resume in the current context with the
@ -76,11 +79,13 @@ evaluateQuote = (v, env, kont) ->
# true or false branch, again in the current enviornment. # true or false branch, again in the current enviornment.
evaluateIf = (exps, env, kont) -> evaluateIf = (exps, env, kont) ->
evaluate (car e), env, new IfCont(kont, (cadr e), (caddr e), env) evaluate (car exps), env, new IfCont(kont, (cadr exps), (caddr exps), env)
class IfCont extends Continuation class IfCont extends Continuation
constructor: (@k, @ift, @iff, @env) -> constructor: (@k, @ift, @iff, @env) ->
resume: (v) -> evaluate (if v then @ift else @iff), @env, @k @_type = "IfCont"
resume: (value) ->
evaluate (if value == the_false_value then @iff else @ift), @env, @k
# Sequences: evaluates the current expression with a continuation that # Sequences: evaluates the current expression with a continuation that
# represents "the next expression" in the sequence. Upon resumption, # represents "the next expression" in the sequence. Upon resumption,
@ -97,6 +102,7 @@ evaluateBegin = (exps, env, kont) ->
class BeginCont extends Continuation class BeginCont extends Continuation
constructor: (@k, @exps, @env) -> constructor: (@k, @exps, @env) ->
@_type = "BeginCont"
resume: (v) -> evaluateBegin (cdr @exps), @env, @k resume: (v) -> evaluateBegin (cdr @exps), @env, @k
# In this continuation, we simply pass the continuation and the name # In this continuation, we simply pass the continuation and the name
@ -110,12 +116,13 @@ evaluateVariable = (name, env, kont) ->
# called after an update has been performed. # called after an update has been performed.
evaluateSet = (name, exp, env, kont) -> evaluateSet = (name, exp, env, kont) ->
evaluate exp, env, (new setCont(kont, name, env)) evaluate exp, env, (new SetCont(kont, name, env))
class SetCont extend Continuation class SetCont extends Continuation
constructor: (@k, @name, @env) -> constructor: (@k, @name, @env) ->
@_type = "SetCont"
resume: (value) -> resume: (value) ->
update @env, @name, @k, value @env.update @name, @k, value
# Calls the current contunation, passing it a new function wrapper. # Calls the current contunation, passing it a new function wrapper.
@ -131,6 +138,7 @@ evaluateLambda = (names, exp, env, kont) ->
class Function extends Value class Function extends Value
constructor: (@variables, @body, @env) -> constructor: (@variables, @body, @env) ->
@_type = "Function"
invoke: (values, env, kont) -> invoke: (values, env, kont) ->
evaluateBegin @body, (extend @env, @variables, values), kont evaluateBegin @body, (extend @env, @variables, values), kont
@ -139,7 +147,7 @@ class Function extends Value
extend = (env, names, values) -> extend = (env, names, values) ->
if (pairp names) and (pairp values) if (pairp names) and (pairp values)
new VariableEnv (extend env (cdr names) (cdr values)), (car names), (car values) new VariableEnv (extend env, (cdr names), (cdr values)), (car names), (car values)
else if (nilp names) else if (nilp names)
if (nilp values) then env else throw "Arity mismatch" if (nilp values) then env else throw "Arity mismatch"
else else
@ -158,8 +166,9 @@ evaluateApplication = (exp, exps, env, kont) ->
class EvFunCont extends Continuation class EvFunCont extends Continuation
constructor: (@k, @exp, @env) -> constructor: (@k, @exp, @env) ->
@_type = "EvFunCont"
resume: (f) -> resume: (f) ->
evaluateArguments (@exp, @k, new ApplyCont @k, f, @env) evaluateArguments @exp, @env, (new ApplyCont(@k, f, @env))
# Evaluate the first list, creating a new list of the arguments. Upon # Evaluate the first list, creating a new list of the arguments. Upon
# completion, resume the continuation with the gather phase # completion, resume the continuation with the gather phase
@ -168,47 +177,61 @@ evaluateArguments = (exp, env, kont) ->
if (pairp exp) if (pairp exp)
evaluate (car exp), env, (new ArgumentCont kont, exp, env) evaluate (car exp), env, (new ArgumentCont kont, exp, env)
else else
kont.resume("No more arguments") kont.resume(nil)
class ArgumentCont extends Continuation class ArgumentCont extends Continuation
constructor: (@k, @exp, @env) -> constructor: (@k, @exp, @env) ->
@_type = "ArgumentCont"
resume: (v) -> resume: (v) ->
evaluateArguments (cdr @env, @env, new GatherCont @k, v) evaluateArguments (cdr @exp), @env, (new GatherCont @k, v)
# Gather the arguments as each ArgumentCont is resumed into a list to # Gather the arguments as each ArgumentCont is resumed into a list to
# be passed to our next step. # be passed to our next step.
class GatherCont extends Continuation class GatherCont extends Continuation
constructor: (@k, @v) -> constructor: (@k, @v) ->
@_type = "GatherCont"
resume: (v) -> resume: (v) ->
@k.resume (cons @v, v) @k.resume (cons @v, v)
# Upon resumption, invoke the function. # Upon resumption, invoke the function.
class ApplyCont extends Continuation class ApplyCont extends Continuation
constructor: (@k, @fn, @env) -> constructor: (@k, @fn, @env) ->
@_type = "ApplyCont"
resume: (v) -> resume: (v) ->
invoke @fn, v, @env, @k console.log
@fn.invoke v, @env, @k
# A special continuation that represents what we want the interpreter # A special continuation that represents what we want the interpreter
# to do when it's done processing. # to do when it's done processing.
class BottomCont extends Continuation class BottomCont extends Continuation
constructor: (@k, @f) -> constructor: (@k, @f) ->
@_type = "BottomCont"
resume: (v) -> resume: (v) ->
@f(v) @f(v)
class Primitive extends Value class Primitive extends Value
constructor: (@name, @nativ) -> constructor: (@name, @nativ) ->
@_type = "Primitive"
invoke: (args, env, kont) -> invoke: (args, env, kont) ->
@nativ.apply null, (listToVector args), env, kont @nativ.apply null, [args, env, kont]
astSymbolsToLispSymbols = (node) ->
return nil if nilp node
throw "Not a list of variable names" if not (ntype(node) is 'list')
handler = (node) ->
return nil if nilp node
cons (nvalu car node), (handler cdr node)
handler(nvalu node)
evaluate = (e, env, kont) -> evaluate = (e, env, kont) ->
[type, exp] = [(ntype e), (nvalu e)] [type, exp] = [(ntype e), (nvalu e)]
if type == "symbol" if type == "symbol"
return evaluateVariable exp, env, kont return evaluateVariable exp, env, kont
if type in ["number", "string", "boolean", "vector"] if type in ["number", "string", "boolean", "vector"]
return exp return kont.resume exp
if type == "list" if type == "list"
head = car exp head = car exp
if (ntype head) == 'symbol' if (ntype head) == 'symbol'
@ -216,13 +239,15 @@ evaluate = (e, env, kont) ->
when "quote" then evaluateQuote (cdr exp), env, kont when "quote" then evaluateQuote (cdr exp), env, kont
when "if" then evaluateIf (cdr exp), env, kont when "if" then evaluateIf (cdr exp), env, kont
when "begin" then evaluateBegin (cdr exp), env, kont when "begin" then evaluateBegin (cdr exp), env, kont
when "set!" then evaluateSet (nvalu cadr exp), (nvalu caddr exp), env, kont when "set!" then evaluateSet (nvalu cadr exp), (caddr exp), env, kont
when "lambda" then evaluateLambda (astSymbolsToLispSymbols cadr exp), (cddr exp), env, kont when "lambda" then evaluateLambda (astSymbolsToLispSymbols cadr exp), (cddr exp), env, kont
evaluateApplication (car exp), (cdr exp), env, cont else evaluateApplication (car exp), (cdr exp), env, kont
else else
evaluateApplication (car exp), (cdr exp), env, cont evaluateApplication (car exp), (cdr exp), env, kont
else else
throw new Error("Can't handle a #{type}") throw new Error("Can't handle a '#{type}'")
env_init = new NullEnv()
definitial = (name, value = nil) -> definitial = (name, value = nil) ->
env_init = new VariableEnv env_init, name, value env_init = new VariableEnv env_init, name, value
@ -234,13 +259,11 @@ defprimitive = (name, nativ, arity) ->
if (vmargs.length == arity) if (vmargs.length == arity)
kont.resume (nativ.apply null, vmargs) kont.resume (nativ.apply null, vmargs)
else else
throw "Incorrect arity") throw "Incorrect arity"
defpredicate = (name, nativ, arity) -> defpredicate = (name, nativ, arity) ->
defprimitive name, ((a, b) -> if nativ.call(null, a, b) then true else the_false_value), arity defprimitive name, ((a, b) -> if nativ.call(null, a, b) then true else the_false_value), arity
the_false_value = (cons "false", "boolean")
definitial "#t", true definitial "#t", true
definitial "#f", the_false_value definitial "#f", the_false_value
definitial "nil", nil definitial "nil", nil
@ -263,30 +286,28 @@ defpredicate "gt", ((a, b) -> a > b), 2
defpredicate "lte", ((a, b) -> a <= b), 2 defpredicate "lte", ((a, b) -> a <= b), 2
defpredicate "gte", ((a, b) -> a >= b), 2 defpredicate "gte", ((a, b) -> a >= b), 2
defpredicate "eq?", ((a, b) -> a == b), 2 defpredicate "eq?", ((a, b) -> a == b), 2
defpredicate "pair?" ((a) -> pairp a), 1 defpredicate "pair?", ((a) -> pairp a), 1
defpredicate "nil?" ((a) -> nilp a), 1 defpredicate "nil?", ((a) -> nilp a), 1
defpredicate "symbol?" ((a) -> /\-?[0-9]+$/.test(a) == false), 1 defpredicate "symbol?", ((a) -> /\-?[0-9]+$/.test(a) == false), 1
definitial "call/cc", new Primitive "call/cc", (values, env, kont) -> definitial "call/cc", new Primitive "call/cc", (values, env, kont) ->
if nilp cdr values if nilp cdr values
(car values).invoke (cons kont), env, kont (car values).invoke (cons kont), env, kont
else else
throw "Incorrect arity for call/cc", [r, k] throw ["Incorrect arity for call/cc", [r, k]]
definitial "apply", new Primitive "apply", (values, env, kont) -> definitial "apply", new Primitive "apply", (values, env, kont) ->
if pairp cdr values if pairp cdr values
f = car values f = car values
args = (() -> args = (() ->
(flat = (args) -> (flat = (args) ->
if nilp cdr args then (car args) else (cons (car args), (flat cdr args)))(cdr values))() if nilp (cdr args) then (car args) else (cons (car args), (flat cdr args)))(cdr values))()
f.invoke args, env, kont f.invoke args, env, kont
definitial "list", new Primitive "list", (values, env, kont) -> definitial "list", new Primitive "list", (values, env, kont) ->
(values, env, kont) -> kont.resume(values) (values, env, kont) -> kont.resume(values)
env_init = new NullEnv()
interpreter = (ast, kont) -> interpreter = (ast, kont) ->
evaluate ast, env_init, new BottomCont null, kont evaluate ast, env_init, new BottomCont null, kont
module.exports = intepreter module.exports = interpreter

55
test/test_chapter3.coffee Normal file
View File

@ -0,0 +1,55 @@
chai = require 'chai'
chai.should()
expect = chai.expect
{cons} = require "cons-lists/lists"
olisp = require '../chapter3/interpreter'
{read, readForms} = require '../chapter1/reader'
the_false_value = (cons "false", "boolean")
lisp = (ast) ->
ret = undefined
olisp ast, (i) -> ret = i
return ret
describe "Core interpreter #3", ->
it "Should handle true statements", ->
expect(lisp read "(begin (if (lt 0 1) #t #f))").to.equal(true)
it "Should handle false statements", ->
expect(lisp read "(begin (if (lt 1 0) #t #f))").to.deep.equal(the_false_value)
it "Should handle return strings", ->
expect(lisp read '(begin (if (lt 0 1) "y" "n"))').to.equal("y")
it "Should handle return strings when false", ->
expect(lisp read '(begin (if (lt 1 0) "y" "n"))').to.equal("n")
it "Should handle equivalent objects that are not intrinsically truthy", ->
expect(lisp read '(begin (if (eq? "y" "y") "y" "n"))').to.equal("y")
it "Should handle inequivalent objects that are not intrinsically truthy", ->
expect(lisp read '(begin (if (eq? "y" "x") "y" "n"))').to.equal("n")
it "Should handle basic arithmetic", ->
expect(lisp read '(begin (+ 5 5))').to.equal(10)
expect(lisp read '(begin (* 5 5))').to.equal(25)
expect(lisp read '(begin (/ 5 5))').to.equal(1)
expect(lisp read '(begin (- 9 5))').to.equal(4)
it "Should handle some algebra", ->
expect(lisp read '(begin (* (+ 5 5) (* 2 3)))').to.equal(60)
it "Should handle a basic setting", ->
expect(lisp read '(begin (set! fact 4) fact)').to.equal(4)
it "Should handle a zero arity thunk", ->
expect(lisp read '(begin (set! fact (lambda () (+ 5 5))) (fact))').to.equal(10)
it "Should handle a two arity thunk", ->
expect(lisp read '(begin (set! fact (lambda (a b) (+ a b))) (fact 4 6))').to.equal(10)
it "Should handle a recursive function", ->
expect(lisp read '(begin (set! fact (lambda (x) (if (eq? x 0) 1 (* x (fact (- x 1)))))) (fact 5))').to.equal(120)
it "Should handle an IIFE", ->
expect(lisp read '(begin ((lambda () (+ 5 5))))').to.equal(10)