Compare commits
71 Commits
coffeescri
...
master
Author | SHA1 | Date |
---|---|---|
Elf M. Sternberg | ec9cdfb4a1 | |
Elf M. Sternberg | 65476fecaf | |
Elf M. Sternberg | 356d4561b2 | |
Elf M. Sternberg | e0b6b44178 | |
Elf M. Sternberg | db2e93b2f3 | |
Elf M. Sternberg | 981baec645 | |
Elf M. Sternberg | 00fbe22583 | |
Elf M. Sternberg | d49f07911c | |
Ken Elf Mathieu Sternberg | ea522f6cf6 | |
Elf M. Sternberg | 1e38327b2a | |
Elf M. Sternberg | 73be7dee59 | |
Elf M. Sternberg | bf7068d0ad | |
Elf M. Sternberg | d26c572ba2 | |
Elf M. Sternberg | 38fa5ae125 | |
Elf M. Sternberg | edf8cd2c3c | |
Elf M. Sternberg | 32edb45f84 | |
Elf M. Sternberg | 3e17e69746 | |
Elf M. Sternberg | 675577431d | |
Elf M. Sternberg | 501ac5fe72 | |
Elf M. Sternberg | 746f92fcdb | |
Elf M. Sternberg | 5d9703aa33 | |
Elf M. Sternberg | 02f79c4255 | |
Elf M. Sternberg | 5bba101ee2 | |
Elf M. Sternberg | bb0c06b073 | |
Elf M. Sternberg | e6b4a73559 | |
Elf M. Sternberg | 39f6a09d51 | |
Elf M. Sternberg | c816fa9eb8 | |
Elf M. Sternberg | 1c113a2f7a | |
Elf M. Sternberg | 5e8172d233 | |
Elf M. Sternberg | 07a800cfbf | |
Elf M. Sternberg | abf6c4ec50 | |
Elf M. Sternberg | 111ad5d8dc | |
Elf M. Sternberg | de3aa61e22 | |
Elf M. Sternberg | 8cf6e4fb5b | |
Elf M. Sternberg | b9a60e3fed | |
Elf M. Sternberg | 1a777acb4c | |
Elf M. Sternberg | 7bd66b6080 | |
Elf M. Sternberg | 983f29c1eb | |
Elf M. Sternberg | b4f5add0b8 | |
Elf M. Sternberg | 1676584db2 | |
Elf M. Sternberg | fb7dab6b33 | |
Elf M. Sternberg | bc857b19f1 | |
Elf M. Sternberg | 560bcd4dda | |
Elf M. Sternberg | 8572d84817 | |
Ken Elf Mathieu Sternberg | 1c4975067d | |
Ken Elf Mathieu Sternberg | 254c1c0f60 | |
Elf M. Sternberg | f711432626 | |
Elf M. Sternberg | c2ff0a3d88 | |
Ken Elf Mathieu Sternberg | 51ae30e31f | |
Ken Elf Mathieu Sternberg | 368abbf827 | |
Ken Elf Mathieu Sternberg | bd9cb09298 | |
Ken Elf Mathieu Sternberg | fc0ad0c05a | |
Ken Elf Mathieu Sternberg | cbd2c168da | |
Ken Elf Mathieu Sternberg | abe220ac1f | |
Ken Elf Mathieu Sternberg | 291f9771f5 | |
Elf M. Sternberg | 9d9624632a | |
Elf M. Sternberg | df6f3f17ae | |
Elf M. Sternberg | b8a19d5c79 | |
Elf M. Sternberg | a5e4558df9 | |
Elf M. Sternberg | 8c9f6622d4 | |
Elf M. Sternberg | b8909fcfd1 | |
Elf M. Sternberg | 1127498546 | |
Elf M. Sternberg | 3238844835 | |
Elf M. Sternberg | b8aa463993 | |
Elf M. Sternberg | cff2d5cb97 | |
Elf M. Sternberg | f17e74207e | |
Elf M. Sternberg | 647dfbbc14 | |
Elf M. Sternberg | c0bcc268a0 | |
Elf M. Sternberg | 74579b9fa0 | |
Elf M. Sternberg | 40a4d5ca19 | |
Elf M. Sternberg | 2bc6312415 |
|
@ -6,4 +6,11 @@ npm-debug.log
|
||||||
package.yml
|
package.yml
|
||||||
node_modules/*
|
node_modules/*
|
||||||
tmp/
|
tmp/
|
||||||
test/
|
bin/_mocha
|
||||||
|
bin/mocha
|
||||||
|
bin/coffee
|
||||||
|
bin/cake
|
||||||
|
bin/coffeelint
|
||||||
|
test-reports.xml
|
||||||
|
LisperatorLanguage
|
||||||
|
chapter?/test.coffee
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
.PHONY: test
|
||||||
|
|
||||||
|
# docs: $(patsubst %.md,%.html,$(wildcard *.md))
|
||||||
|
|
||||||
|
SOURCES= chapter1/*.coffee chapter3/*.coffee chapter-lambda-1/*.coffee test/*.coffee
|
||||||
|
|
||||||
|
%.js: src/%.coffee
|
||||||
|
node_modules/.bin/mocha coffee -c -o . $<
|
||||||
|
|
||||||
|
%.html: %.md header.html footer.html
|
||||||
|
cat header.html > $@
|
||||||
|
pandoc $< >> $@
|
||||||
|
cat footer.html >> $@
|
||||||
|
|
||||||
|
node_modules: package.json
|
||||||
|
mkdir -p node_modules
|
||||||
|
npm install
|
||||||
|
|
||||||
|
lint:
|
||||||
|
coffeelint $(SOURCES)
|
||||||
|
|
||||||
|
test: clean node_modules
|
||||||
|
@JUNIT_REPORT_PATH=test-reports.xml JUNIT_REPORT_STACK=1 ./node_modules/.bin/mocha \
|
||||||
|
--reporter mocha-jenkins-reporter --compilers coffee:coffee-script/register || true
|
||||||
|
|
||||||
|
ltest: node_modules
|
||||||
|
node_modules/.bin/mocha --compilers coffee:coffee-script/register
|
||||||
|
|
||||||
|
watch:
|
||||||
|
while inotifywait $(SOURCES) ; do make test; done
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f report.xml test-reports.xml
|
39
README.md
39
README.md
|
@ -1,2 +1,37 @@
|
||||||
Not much to see here. Just practicing a few things. If you're
|
# A Collection of Interpreters from Lisp In Small Pieces, written in Coffeescript
|
||||||
expecting miracles, this isn't the place for it.
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
I don't know Lisp, so I figured the correct place to start was to write
|
||||||
|
my own interpreter. After buying five different textbooks (*The
|
||||||
|
Structure and Interpretation of Computer Programs*, aka "The Wizard
|
||||||
|
Book", Friedman's *The Essentials of Programming Languages*, *Let over
|
||||||
|
Lambda*, *On Lisp*, and one more) I decided Christian Quinnec's *Lisp In
|
||||||
|
Small Pieces* gave the clearest step-by-step introduction.
|
||||||
|
|
||||||
|
Since I didn't know Lisp, my task was to translate what Quiennec wrote
|
||||||
|
in his book into a language I *did* know: Javascript. Well,
|
||||||
|
Coffeescript, which is basically Javascript with a lot of the
|
||||||
|
syntactical noise removed, which is why I liked it.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
I don't know if you're going to get much out of it, but the reader
|
||||||
|
(which I had to write by hand, seeing as I didn't *have* a native Lisp
|
||||||
|
reader on hand in my Javascripty environment), and each interpreter has
|
||||||
|
a fairly standard test case that demonstrates that each language does
|
||||||
|
what it says it does: you can do math, set variables, name and create
|
||||||
|
functions, and even do recursion.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
chapter-lambda-1 is not from Lisp In Small Pieces. It is a primitive
|
||||||
|
CPS interpreter built on top of the interpreter from LiSP Chapter 1,
|
||||||
|
using techniques derived from a fairly facile reading of
|
||||||
|
<a href="http://lisperator.net/pltut/">Lisperator's "Implement A
|
||||||
|
Programming Language in Javascript."</a> But it was fun.
|
||||||
|
|
||||||
|
## LICENSE AND COPYRIGHT NOTICE: NO WARRANTY GRANTED OR IMPLIED
|
||||||
|
|
||||||
|
See the LICENSE file.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,195 @@
|
||||||
|
{listToString, listToVector, pairp, cons, car, cdr, caar, cddr, cdar,
|
||||||
|
cadr, caadr, cadar, caddr, nilp, nil, setcdr, metacadr} = require "cons-lists/lists"
|
||||||
|
{Node} = require "../chapter1/reader_types"
|
||||||
|
|
||||||
|
class LispInterpreterError extends Error
|
||||||
|
name: 'LispInterpreterError'
|
||||||
|
constructor: (@message) ->
|
||||||
|
|
||||||
|
env_init = nil
|
||||||
|
env_global = env_init
|
||||||
|
|
||||||
|
definitial = (name, value = nil) ->
|
||||||
|
env_global = (cons (cons name, value), env_global)
|
||||||
|
name
|
||||||
|
|
||||||
|
defprimitive = (name, nativ, arity) ->
|
||||||
|
definitial name, ((args, callback) ->
|
||||||
|
vmargs = listToVector(args)
|
||||||
|
if (vmargs.length == arity)
|
||||||
|
callback nativ.apply null, vmargs
|
||||||
|
else
|
||||||
|
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
|
||||||
|
|
||||||
|
the_false_value = (cons "false", "boolean")
|
||||||
|
|
||||||
|
definitial "#t", true
|
||||||
|
definitial "#f", the_false_value
|
||||||
|
definitial "nil", nil
|
||||||
|
definitial "foo"
|
||||||
|
definitial "bar"
|
||||||
|
definitial "fib"
|
||||||
|
definitial "fact"
|
||||||
|
|
||||||
|
defprimitive "cons", cons, 2
|
||||||
|
defprimitive "car", car, 2
|
||||||
|
defprimitive "set-cdr!", setcdr, 2
|
||||||
|
defprimitive "+", ((a, b) -> a + b), 2
|
||||||
|
defprimitive "*", ((a, b) -> a * b), 2
|
||||||
|
defprimitive "-", ((a, b) -> a - b), 2
|
||||||
|
defprimitive "/", ((a, b) -> a / b), 2
|
||||||
|
defpredicate "lt", ((a, b) -> a < b), 2
|
||||||
|
defpredicate "eq?", ((a, b) -> a == b), 2
|
||||||
|
|
||||||
|
# MISTAKE: Variables are always of type Symbol. This is probably a
|
||||||
|
# mistake.
|
||||||
|
|
||||||
|
extend = (env, variables, values) ->
|
||||||
|
if (pairp variables)
|
||||||
|
if (pairp values)
|
||||||
|
(cons (cons (car variables), (car values)),
|
||||||
|
(extend env, (cdr variables), (cdr values)))
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Too few values"
|
||||||
|
else if (nilp variables)
|
||||||
|
if (nilp values) then env else throw new LispInterpreterError "Too many values"
|
||||||
|
else
|
||||||
|
if (variables.type == 'symbol')
|
||||||
|
(cons (cons variables, values), env)
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
|
||||||
|
make_function = (variables, body, env, callback) ->
|
||||||
|
callback (values, cb) -> eprogn body, (extend env, variables, values), cb
|
||||||
|
|
||||||
|
invoke = (fn, args, callback) ->
|
||||||
|
fn args, callback
|
||||||
|
|
||||||
|
# Takes a list of nodes and calls evaluate on each one, returning the
|
||||||
|
# last one as the value of the total expression. In this example, we
|
||||||
|
# are hard-coding what ought to be a macro, namely the threading
|
||||||
|
# macros, "->"
|
||||||
|
|
||||||
|
eprogn = (exps, env, callback) ->
|
||||||
|
if (pairp exps)
|
||||||
|
if pairp (cdr exps)
|
||||||
|
evaluate (car exps), env, (next) ->
|
||||||
|
eprogn (cdr exps), env, callback
|
||||||
|
else
|
||||||
|
evaluate (car exps), env, callback
|
||||||
|
else
|
||||||
|
callback nil
|
||||||
|
|
||||||
|
evlis = (exps, env, callback) ->
|
||||||
|
if (pairp exps)
|
||||||
|
evlis (cdr exps), env, (rest) ->
|
||||||
|
evaluate (car exps), env, (calc) ->
|
||||||
|
callback cons calc, rest
|
||||||
|
else
|
||||||
|
callback nil
|
||||||
|
|
||||||
|
lookup = (id, env) ->
|
||||||
|
if (pairp env)
|
||||||
|
if (caar env) == id
|
||||||
|
cdar env
|
||||||
|
else
|
||||||
|
lookup id, (cdr env)
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
|
||||||
|
update = (id, env, value, callback) ->
|
||||||
|
if (pairp env)
|
||||||
|
if (caar env) == id
|
||||||
|
setcdr value, (car env)
|
||||||
|
callback value
|
||||||
|
else
|
||||||
|
update id, (cdr env), value, callback
|
||||||
|
else
|
||||||
|
callback nil
|
||||||
|
|
||||||
|
# This really ought to be the only place where the AST meets the
|
||||||
|
# interpreter core. I can't help but think that this design precludes
|
||||||
|
# pluggable interpreter core.
|
||||||
|
|
||||||
|
# TODO: Reengineer this with a call to normalize
|
||||||
|
|
||||||
|
astSymbolsToLispSymbols = (node) ->
|
||||||
|
return nil if nilp node
|
||||||
|
throw (new LispInterpreterError "Not a list of variable names") if not node.type == 'list'
|
||||||
|
handler = (cell) ->
|
||||||
|
return nil if nilp cell
|
||||||
|
cons (car cell).value, (handler cdr cell)
|
||||||
|
handler node.value
|
||||||
|
|
||||||
|
# Takes an AST node and evaluates it and its contents. A node may be
|
||||||
|
# ("list" (... contents ...)) or ("number" 42) or ("symbol" x), etc.
|
||||||
|
|
||||||
|
cadddr = metacadr('cadddr')
|
||||||
|
|
||||||
|
metadata_evaluation =
|
||||||
|
listp: (node) -> node.type == 'list'
|
||||||
|
symbolp: (node) -> node.type == 'symbol'
|
||||||
|
numberp: (node) -> node.type == 'number'
|
||||||
|
stringp: (node) -> node.type == 'string'
|
||||||
|
commentp: (node) -> node.type == 'comment'
|
||||||
|
nvalu: (node) -> node.value
|
||||||
|
mksymbols: (list) -> astSymbolsToLispSymbols(list)
|
||||||
|
|
||||||
|
straight_evaluation =
|
||||||
|
listp: (cell) -> cell.__type == 'list'
|
||||||
|
symbolp: (cell) -> typeof cell == 'string' and cell.length > 0 and cell[0] not in ["\"", ";"]
|
||||||
|
commentp: (cell) -> typeof cell == 'string' and cell.length > 0 and cell[0] == ";"
|
||||||
|
numberp: (cell) -> typeof cell == 'number'
|
||||||
|
stringp: (cell) -> typeof cell == 'string' and cell.length > 0 and cell[0] == "\""
|
||||||
|
boolp: (cell) -> typeof cell == 'boolean'
|
||||||
|
nullp: (cell) -> cell == null
|
||||||
|
vectorp: (cell) -> (not straight_evaluation.listp cell) and toString.call(cell) == '[object Array]'
|
||||||
|
recordp: (cell) -> (not cell._prototype?) and toSTring.call(cell) == '[object Object]'
|
||||||
|
objectp: (cell) -> (cell._prototype?) and toString.call(cell) == '[object Object]'
|
||||||
|
nilp: (cell) -> nilp(cell)
|
||||||
|
nvalu: (cell) -> cell
|
||||||
|
mksymbols: (cell) -> cell
|
||||||
|
|
||||||
|
makeEvaluator = (ix = straight_evaluation, ty="straight") ->
|
||||||
|
(exp, env, callback) ->
|
||||||
|
if ix.symbolp exp
|
||||||
|
return callback lookup (ix.nvalu exp), env
|
||||||
|
else if ([ix.numberp, ix.stringp].filter (i) -> i(exp)).length > 0
|
||||||
|
return callback ix.nvalu exp
|
||||||
|
else if ix.listp exp
|
||||||
|
body = ix.nvalu exp
|
||||||
|
head = car body
|
||||||
|
if ix.symbolp head
|
||||||
|
return switch (ix.nvalu head)
|
||||||
|
when "quote" then callback cdr body
|
||||||
|
when "if"
|
||||||
|
evaluate (cadr body), env, (res) ->
|
||||||
|
w = unless res == the_false_value then caddr else cadddr
|
||||||
|
evaluate (w body), env, callback
|
||||||
|
when "begin" then eprogn (cdr body), env, callback
|
||||||
|
when "set!"
|
||||||
|
evaluate (caddr body), env, (newvalue) ->
|
||||||
|
update (ix.nvalu cadr body), env, newvalue, callback
|
||||||
|
when "lambda"
|
||||||
|
make_function (ix.mksymbols cadr body), (cddr body), env, callback
|
||||||
|
else
|
||||||
|
evaluate (car body), env, (fn) ->
|
||||||
|
evlis (cdr body), env, (args) ->
|
||||||
|
invoke fn, args, callback
|
||||||
|
else
|
||||||
|
evaluate (car body), env, (fn) ->
|
||||||
|
evlis (cdr body), env, (args) ->
|
||||||
|
invoke fn, args, callback
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError ("Can't handle a #{type}")
|
||||||
|
|
||||||
|
nodeEval = makeEvaluator(metadata_evaluation, "node")
|
||||||
|
lispEval = makeEvaluator(straight_evaluation, "lisp")
|
||||||
|
|
||||||
|
evaluate = (exp, env, cb) ->
|
||||||
|
(if exp? and (exp instanceof Node) then nodeEval else lispEval)(exp, env, cb)
|
||||||
|
|
||||||
|
module.exports = (c, cb) -> evaluate c, env_global, cb
|
|
@ -0,0 +1,10 @@
|
||||||
|
{car, cdr, listp} = require 'cons-lists/lists'
|
||||||
|
{Node, Symbol} = require "./reader_types"
|
||||||
|
|
||||||
|
module.exports = ops =
|
||||||
|
astObject: (form) -> form instanceof Node
|
||||||
|
aValue: (form) -> form.value
|
||||||
|
aSymbol: (form) -> form.value
|
||||||
|
isAList: (form) -> ops.astObject(form) and form.type == 'list'
|
||||||
|
isARecord: (form) -> ops.astObject(form) and form.type == 'record'
|
||||||
|
isAVector: (form) -> ops.astObject(form) and form.type == 'vector'
|
|
@ -0,0 +1,56 @@
|
||||||
|
{car, cdr, cons, listp, nilp, nil,
|
||||||
|
list, pairp, listToString} = require 'cons-lists/lists'
|
||||||
|
{astObject} = require './astAccessors'
|
||||||
|
{Symbol} = require './reader_types'
|
||||||
|
|
||||||
|
# RICH_AST -> LISP_AST
|
||||||
|
|
||||||
|
normalizeForm = (form) ->
|
||||||
|
|
||||||
|
listToRecord1 = (l) ->
|
||||||
|
o = Object.create(null)
|
||||||
|
while(l != nil)
|
||||||
|
o[normalizeForm(car l)] = normalizeForm(car cdr l)
|
||||||
|
l = cdr cdr l
|
||||||
|
null
|
||||||
|
o
|
||||||
|
|
||||||
|
listToVector1 = (l) ->
|
||||||
|
while(l != nil) then p = normalizeForm(car l); l = cdr l; p
|
||||||
|
|
||||||
|
id = (a) -> a
|
||||||
|
|
||||||
|
methods =
|
||||||
|
'list': normalizeForms
|
||||||
|
'vector': (atom) -> listToVector1(atom)
|
||||||
|
'record': (atom) -> listToRecord1(atom)
|
||||||
|
|
||||||
|
# Basic native types. Meh.
|
||||||
|
'symbol': (id) -> new Symbol(id.name)
|
||||||
|
'number': id
|
||||||
|
'string': id
|
||||||
|
'nil': (atom) -> nil
|
||||||
|
|
||||||
|
# Values inherited from the VM.
|
||||||
|
'true': (atom) -> true
|
||||||
|
'false': (atom) -> false
|
||||||
|
'null': (atom) -> null
|
||||||
|
'undefined': (atom) -> undefined
|
||||||
|
|
||||||
|
methods[form.type](form.value)
|
||||||
|
|
||||||
|
|
||||||
|
normalizeForms = (forms) ->
|
||||||
|
# Yes, this reifies the expectation than an empty list and 'nil' are
|
||||||
|
# the same.
|
||||||
|
return nil if nilp forms
|
||||||
|
|
||||||
|
# Handle dotted list.
|
||||||
|
if (astObject forms)
|
||||||
|
return normalizeForm(forms)
|
||||||
|
cons(normalizeForm(car forms), normalizeForms(cdr forms))
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
normalizeForm: normalizeForm
|
||||||
|
normalizeForms: normalizeForms
|
||||||
|
|
|
@ -0,0 +1,185 @@
|
||||||
|
{listToString, listToVector, pairp, cons, car, cdr, caar, cddr,
|
||||||
|
cdar, cadr, caadr, cadar, caddr, nilp, nil, setcdr, metacadr} = require "cons-lists/lists"
|
||||||
|
{Node} = require "./reader_types"
|
||||||
|
|
||||||
|
class LispInterpreterError extends Error
|
||||||
|
name: 'LispInterpreterError'
|
||||||
|
constructor: (@message, position = null) ->
|
||||||
|
|
||||||
|
env_init = nil
|
||||||
|
env_global = env_init
|
||||||
|
|
||||||
|
definitial = (name, value = nil) ->
|
||||||
|
env_global = (cons (cons name, value), env_global)
|
||||||
|
name
|
||||||
|
|
||||||
|
defprimitive = (name, nativ, arity) ->
|
||||||
|
definitial name, ((args) ->
|
||||||
|
vmargs = listToVector(args)
|
||||||
|
if (vmargs.length == arity)
|
||||||
|
nativ.apply null, vmargs
|
||||||
|
else
|
||||||
|
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
|
||||||
|
|
||||||
|
the_false_value = (cons "false", "boolean")
|
||||||
|
|
||||||
|
definitial "#t", true
|
||||||
|
definitial "#f", the_false_value
|
||||||
|
definitial "nil", nil
|
||||||
|
definitial "foo"
|
||||||
|
definitial "bar"
|
||||||
|
definitial "fib"
|
||||||
|
definitial "fact"
|
||||||
|
|
||||||
|
defprimitive "cons", cons, 2
|
||||||
|
defprimitive "car", car, 2
|
||||||
|
defprimitive "set-cdr!", setcdr, 2
|
||||||
|
defprimitive "+", ((a, b) -> a + b), 2
|
||||||
|
defprimitive "*", ((a, b) -> a * b), 2
|
||||||
|
defprimitive "-", ((a, b) -> a - b), 2
|
||||||
|
defprimitive "/", ((a, b) -> a / b), 2
|
||||||
|
defpredicate "lt", ((a, b) -> a < b), 2
|
||||||
|
defpredicate "eq?", ((a, b) -> a == b), 2
|
||||||
|
|
||||||
|
extend = (env, variables, values) ->
|
||||||
|
if (pairp variables)
|
||||||
|
if (pairp values)
|
||||||
|
(cons (cons (car variables), (car values)),
|
||||||
|
(extend env, (cdr variables), (cdr values)))
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Too few values"
|
||||||
|
else if (nilp variables)
|
||||||
|
if (nilp values) then env else throw new LispInterpreterError "Too many values"
|
||||||
|
else
|
||||||
|
if (variables.type == 'symbol')
|
||||||
|
(cons (cons variables, values), env)
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
|
||||||
|
make_function = (variables, body, env) ->
|
||||||
|
(values) -> eprogn body, (extend env, variables, values)
|
||||||
|
|
||||||
|
invoke = (fn, args) ->
|
||||||
|
(fn args)
|
||||||
|
|
||||||
|
# Takes a list of nodes and calls evaluate on each one, returning the
|
||||||
|
# last one as the value of the total expression. In this example, we
|
||||||
|
# are hard-coding what ought to be a macro, namely the threading
|
||||||
|
# macros, "->"
|
||||||
|
|
||||||
|
eprogn = (exps, env) ->
|
||||||
|
if (pairp exps)
|
||||||
|
if pairp (cdr exps)
|
||||||
|
evaluate (car exps), env
|
||||||
|
eprogn (cdr exps), env
|
||||||
|
else
|
||||||
|
evaluate (car exps), env
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
|
||||||
|
evlis = (exps, env) ->
|
||||||
|
if (pairp exps)
|
||||||
|
(cons (evaluate (car exps), env), (evlis (cdr exps), env))
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
|
||||||
|
lookup = (id, env) ->
|
||||||
|
if (pairp env)
|
||||||
|
if (caar env) == id
|
||||||
|
cdar env
|
||||||
|
else
|
||||||
|
lookup id, (cdr env)
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
|
||||||
|
update = (id, env, value) ->
|
||||||
|
if (pairp env)
|
||||||
|
if (caar env) == id
|
||||||
|
setcdr value, (car env)
|
||||||
|
value
|
||||||
|
else
|
||||||
|
update id, (cdr env), value
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
|
||||||
|
# TODO: Reengineer this with a call to normalize
|
||||||
|
|
||||||
|
tap = (i) -> console.log(i) ; i
|
||||||
|
|
||||||
|
astSymbolsToLispSymbols = (node) ->
|
||||||
|
return nil if nilp node
|
||||||
|
throw (new LispInterpreterError "Not a list of variable names") if not node.type == 'list'
|
||||||
|
handler = (cell) ->
|
||||||
|
return nil if nilp cell
|
||||||
|
cons (car cell).value, (handler cdr cell)
|
||||||
|
handler node.value
|
||||||
|
|
||||||
|
cadddr = metacadr('cadddr')
|
||||||
|
|
||||||
|
# This is really the only thing that changes behavior between "reader
|
||||||
|
# nodes" (nodes loaded with debugging metadata) and a standard cons
|
||||||
|
# object. TODO: astSymbolsToLispSymbols should be deprecated in
|
||||||
|
# favor of normalizeForm (s?) and Symbol extraction
|
||||||
|
|
||||||
|
metadata_evaluation =
|
||||||
|
listp: (node) -> node.type == 'list'
|
||||||
|
symbolp: (node) -> node.type == 'symbol'
|
||||||
|
numberp: (node) -> node.type == 'number'
|
||||||
|
stringp: (node) -> node.type == 'string'
|
||||||
|
commentp: (node) -> node.type == 'comment'
|
||||||
|
nvalu: (node) -> node.value
|
||||||
|
mksymbols: (list) -> astSymbolsToLispSymbols(list)
|
||||||
|
|
||||||
|
straight_evaluation =
|
||||||
|
listp: (cell) -> cell.__type == 'list'
|
||||||
|
symbolp: (cell) -> typeof cell == 'string' and cell.length > 0 and cell[0] not in ["\"", ";"]
|
||||||
|
commentp: (cell) -> typeof cell == 'string' and cell.length > 0 and cell[0] == ";"
|
||||||
|
numberp: (cell) -> typeof cell == 'number'
|
||||||
|
stringp: (cell) -> typeof cell == 'string' and cell.length > 0 and cell[0] == "\""
|
||||||
|
boolp: (cell) -> typeof cell == 'boolean'
|
||||||
|
nullp: (cell) -> cell == null
|
||||||
|
vectorp: (cell) -> (not straight_evaluation.listp cell) and toString.call(cell) == '[object Array]'
|
||||||
|
recordp: (cell) -> (not cell._prototype?) and toSTring.call(cell) == '[object Object]'
|
||||||
|
objectp: (cell) -> (cell._prototype?) and toString.call(cell) == '[object Object]'
|
||||||
|
nilp: (cell) -> nilp(cell)
|
||||||
|
nvalu: (cell) -> cell
|
||||||
|
mksymbols: (cell) -> cell
|
||||||
|
|
||||||
|
makeEvaluator = (ix = straight_evaluation, ty="straight") ->
|
||||||
|
(exp, env) ->
|
||||||
|
# Takes an AST node and evaluates it and its contents. A node may be
|
||||||
|
# ("list" (... contents ...)) or ("number" 42) or ("symbol" x), etc.
|
||||||
|
if ix.symbolp(exp)
|
||||||
|
return lookup (ix.nvalu exp), env
|
||||||
|
else if ([ix.numberp, ix.stringp].filter (i) -> i(exp)).length > 0
|
||||||
|
return ix.nvalu exp
|
||||||
|
else if ix.listp(exp)
|
||||||
|
body = ix.nvalu exp
|
||||||
|
head = car body
|
||||||
|
if ix.symbolp(head)
|
||||||
|
switch (ix.nvalu head)
|
||||||
|
when "quote" then cdr body
|
||||||
|
when "if"
|
||||||
|
unless (evaluate (cadr body), env) == the_false_value
|
||||||
|
evaluate (caddr body), env
|
||||||
|
else
|
||||||
|
evaluate (cadddr body), env
|
||||||
|
when "begin" then eprogn (cdr body), env
|
||||||
|
when "set!" then update (ix.nvalu cadr body), env, (evaluate (caddr body), env)
|
||||||
|
when "lambda" then make_function (ix.mksymbols cadr body), (cddr body), env
|
||||||
|
else invoke (evaluate (car body), env), (evlis (cdr body), env)
|
||||||
|
else
|
||||||
|
invoke (evaluate (car body), env), (evlis (cdr body), env)
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Can't handle a #{exp.type}"
|
||||||
|
|
||||||
|
nodeEval = makeEvaluator(metadata_evaluation, "node")
|
||||||
|
lispEval = makeEvaluator(straight_evaluation, "lisp")
|
||||||
|
|
||||||
|
evaluate = (exp, env) ->
|
||||||
|
(if exp? and (exp instanceof Node) then nodeEval else lispEval)(exp, env)
|
||||||
|
|
||||||
|
module.exports = (c) -> evaluate c, env_global
|
|
@ -1,10 +1,12 @@
|
||||||
{car, cdr, cons, nil, nilp, pairp, vectorToList} = require 'cons-lists/lists'
|
{car, cdr, cons, nil, nilp, pairp, vectorToList, list} = require 'cons-lists/lists'
|
||||||
|
{inspect} = require "util"
|
||||||
|
{Node, Comment} = require "./reader_types"
|
||||||
|
|
||||||
NEWLINES = ["\n", "\r", "\x0B", "\x0C"]
|
NEWLINES = ["\n", "\r", "\x0B", "\x0C"]
|
||||||
WHITESPACE = [" ", "\t"].concat(NEWLINES)
|
WHITESPACE = [" ", "\t"].concat(NEWLINES)
|
||||||
|
|
||||||
EOF = new (class)
|
EOF = new (class Eof)()
|
||||||
EOO = new (class)
|
EOO = new (class Eoo)()
|
||||||
|
|
||||||
class Source
|
class Source
|
||||||
constructor: (@inStream) ->
|
constructor: (@inStream) ->
|
||||||
|
@ -27,16 +29,12 @@ class Source
|
||||||
done: -> @index > @max
|
done: -> @index > @max
|
||||||
|
|
||||||
# IO -> IO
|
# IO -> IO
|
||||||
skipWS = (inStream) ->
|
skipWS = (inStream) ->
|
||||||
while inStream.peek() in WHITESPACE then inStream.next()
|
while inStream.peek() in WHITESPACE then inStream.next()
|
||||||
|
|
||||||
# (type, value, line, column) -> (node {type, value, line, column)}
|
|
||||||
makeObj = (type, value, line, column) ->
|
|
||||||
cons(type, cons(value, cons(line, cons(column, nil))))
|
|
||||||
|
|
||||||
# msg -> (IO -> Node => Error)
|
# msg -> (IO -> Node => Error)
|
||||||
handleError = (message) ->
|
handleError = (message) ->
|
||||||
(line, column) -> makeObj('error', message, line, column)
|
(line, column) -> new Node('error', message, line, column)
|
||||||
|
|
||||||
# IO -> Node => Comment
|
# IO -> Node => Comment
|
||||||
readComment = (inStream) ->
|
readComment = (inStream) ->
|
||||||
|
@ -45,7 +43,7 @@ readComment = (inStream) ->
|
||||||
inStream.next()).join("")
|
inStream.next()).join("")
|
||||||
if not inStream.done()
|
if not inStream.done()
|
||||||
inStream.next()
|
inStream.next()
|
||||||
makeObj 'comment', r, line, column
|
new Node 'comment', (new Comment r), line, column
|
||||||
|
|
||||||
# IO -> (Node => Literal => String) | Error
|
# IO -> (Node => Literal => String) | Error
|
||||||
readString = (inStream) ->
|
readString = (inStream) ->
|
||||||
|
@ -58,7 +56,7 @@ readString = (inStream) ->
|
||||||
if inStream.done()
|
if inStream.done()
|
||||||
return handleError("end of file seen before end of string.")(line, column)
|
return handleError("end of file seen before end of string.")(line, column)
|
||||||
inStream.next()
|
inStream.next()
|
||||||
makeObj 'string', (string.join ''), line, column
|
new Node 'string', (string.join ''), line, column
|
||||||
|
|
||||||
# (String) -> (Node => Literal => Number) | Nothing
|
# (String) -> (Node => Literal => Number) | Nothing
|
||||||
readMaybeNumber = (symbol) ->
|
readMaybeNumber = (symbol) ->
|
||||||
|
@ -84,9 +82,9 @@ readSymbol = (inStream, tableKeys) ->
|
||||||
inStream.next()).join ''
|
inStream.next()).join ''
|
||||||
number = readMaybeNumber symbol
|
number = readMaybeNumber symbol
|
||||||
if number?
|
if number?
|
||||||
return makeObj 'number', number, line, column
|
return new Node 'number', number, line, column
|
||||||
makeObj 'symbol', symbol, line, column
|
new Node 'symbol', symbol, line, column
|
||||||
|
|
||||||
|
|
||||||
# (Delim, TypeName) -> IO -> (IO, node) | Error
|
# (Delim, TypeName) -> IO -> (IO, node) | Error
|
||||||
makeReadPair = (delim, type) ->
|
makeReadPair = (delim, type) ->
|
||||||
|
@ -97,18 +95,25 @@ makeReadPair = (delim, type) ->
|
||||||
[line, column] = inStream.position()
|
[line, column] = inStream.position()
|
||||||
if inStream.peek() == delim
|
if inStream.peek() == delim
|
||||||
inStream.next()
|
inStream.next()
|
||||||
return makeObj(type, nil, line, column)
|
return new Node type, nil, line, column
|
||||||
|
|
||||||
# IO -> (IO, Node) | Error
|
# IO -> (IO, Node) | Error
|
||||||
|
dotted = false
|
||||||
readEachPair = (inStream) ->
|
readEachPair = (inStream) ->
|
||||||
[line, column] = inStream.position()
|
[line, column] = inStream.position()
|
||||||
obj = read inStream, true, null, true
|
obj = read inStream, true, null, true
|
||||||
if inStream.peek() == delim then return cons obj, nil
|
if inStream.peek() == delim
|
||||||
|
if dotted then return obj
|
||||||
|
return cons obj, nil
|
||||||
if inStream.done() then return handleError("Unexpected end of input")(line, column)
|
if inStream.done() then return handleError("Unexpected end of input")(line, column)
|
||||||
return obj if (car obj) == 'error'
|
if dotted then return handleError("More than one symbol after dot")
|
||||||
cons obj, readEachPair(inStream)
|
return obj if obj.type == 'error'
|
||||||
|
if obj.type == 'symbol' and obj.value == '.'
|
||||||
|
dotted = true
|
||||||
|
return readEachPair inStream
|
||||||
|
cons obj, readEachPair inStream
|
||||||
|
|
||||||
ret = makeObj type, readEachPair(inStream), line, column
|
ret = new Node type, readEachPair(inStream), line, column
|
||||||
inStream.next()
|
inStream.next()
|
||||||
ret
|
ret
|
||||||
|
|
||||||
|
@ -120,8 +125,8 @@ prefixReader = (type) ->
|
||||||
inStream.next()
|
inStream.next()
|
||||||
[line1, column1] = inStream.position()
|
[line1, column1] = inStream.position()
|
||||||
obj = read inStream, true, null, true
|
obj = read inStream, true, null, true
|
||||||
return obj if (car obj) == 'error'
|
return obj if obj.type == 'error'
|
||||||
makeObj "list", cons((makeObj("symbol", type, line1, column1)), cons(obj)), line, column
|
new Node "list", cons((new Node("symbol", type, line1, column1)), cons(obj)), line, column
|
||||||
|
|
||||||
# I really wanted to make anything more complex than a list (like an
|
# I really wanted to make anything more complex than a list (like an
|
||||||
# object or a vector) something handled by a read macro. Maybe in a
|
# object or a vector) something handled by a read macro. Maybe in a
|
||||||
|
@ -134,7 +139,7 @@ readMacros =
|
||||||
'[': makeReadPair ']', 'vector'
|
'[': makeReadPair ']', 'vector'
|
||||||
']': handleError "Closing bracket encountered"
|
']': handleError "Closing bracket encountered"
|
||||||
'{': makeReadPair('}', 'record', (res) ->
|
'{': makeReadPair('}', 'record', (res) ->
|
||||||
res.length % 2 == 0 and true or mkerr "record key without value")
|
res.length % 2 == 0 and true or mkerr "record key without value")
|
||||||
'}': handleError "Closing curly without corresponding opening."
|
'}': handleError "Closing curly without corresponding opening."
|
||||||
"`": prefixReader 'back-quote'
|
"`": prefixReader 'back-quote'
|
||||||
"'": prefixReader 'quote'
|
"'": prefixReader 'quote'
|
||||||
|
@ -168,7 +173,7 @@ read = (inStream, eofErrorP = false, eofError = EOF, recursiveP = false, inReadM
|
||||||
|
|
||||||
while true
|
while true
|
||||||
form = matcher inStream, c
|
form = matcher inStream, c
|
||||||
skip = (not nilp form) and (car form == 'comment') and not keepComments
|
skip = (not nilp form) and (form.type == 'comment') and not keepComments
|
||||||
break if (not skip and not nilp form) or inStream.done()
|
break if (not skip and not nilp form) or inStream.done()
|
||||||
c = inStream.peek()
|
c = inStream.peek()
|
||||||
null
|
null
|
||||||
|
@ -179,19 +184,21 @@ read = (inStream, eofErrorP = false, eofError = EOF, recursiveP = false, inReadM
|
||||||
|
|
||||||
# IO -> (Form* | Error)
|
# IO -> (Form* | Error)
|
||||||
readForms = (inStream) ->
|
readForms = (inStream) ->
|
||||||
inStream = if inStream instanceof Source then inStream else new Source inStream
|
inStream = if inStream instanceof Source then inStream else new Source inStream
|
||||||
return nil if inStream.done()
|
return nil if inStream.done()
|
||||||
|
|
||||||
# IO -> (FORM*, IO) | Error
|
# IO -> (FORM*, IO) | Error
|
||||||
[line, column] = inStream.position()
|
[line, column] = inStream.position()
|
||||||
readEach = (inStream) ->
|
readEach = (inStream) ->
|
||||||
obj = read inStream, true, null, false
|
obj = read inStream, true, null, false
|
||||||
return nil if (nilp obj)
|
return nil if (nilp obj)
|
||||||
return obj if (car obj) == 'error'
|
return obj if obj.type == 'error'
|
||||||
cons obj, readEach inStream
|
cons obj, readEach inStream
|
||||||
|
|
||||||
obj = readEach inStream
|
obj = readEach inStream
|
||||||
if (car obj) == 'error' then obj else makeObj "list", obj, line, column
|
if obj.type == 'error' then obj else new Node "list", obj, line, column
|
||||||
|
|
||||||
exports.read = read
|
exports.read = read
|
||||||
exports.readForms = readForms
|
exports.readForms = readForms
|
||||||
|
exports.Node = Node
|
||||||
|
exports.Symbol = Symbol
|
|
@ -0,0 +1,11 @@
|
||||||
|
exports.Node = class
|
||||||
|
constructor: (@type, @value, @line, @column) ->
|
||||||
|
|
||||||
|
exports.Symbol = class
|
||||||
|
constructor: (@name) ->
|
||||||
|
|
||||||
|
exports.Comment = class
|
||||||
|
constructor: (@text) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,448 @@
|
||||||
|
{listToString, listToVector, pairp, cons, car, cdr, caar, cddr, cdar,
|
||||||
|
cadr, caadr, cadar, caddr, nilp, nil, setcdr,
|
||||||
|
metacadr, setcar} = require "cons-lists/lists"
|
||||||
|
{normalizeForms, normalizeForm} = require "../chapter1/astToList"
|
||||||
|
{Node, Symbol} = require '../chapter1/reader_types'
|
||||||
|
|
||||||
|
class LispInterpreterError extends Error
|
||||||
|
name: 'LispInterpreterError'
|
||||||
|
constructor: (@message) ->
|
||||||
|
|
||||||
|
the_false_value = (cons "false", "boolean")
|
||||||
|
|
||||||
|
# Base class that represents a value. Base class representing a LiSP
|
||||||
|
# value, a primitive, or a function
|
||||||
|
|
||||||
|
eq = (id1, id2) ->
|
||||||
|
if id1 instanceof Symbol and id2 instanceof Symbol
|
||||||
|
return id1.name == id2.name
|
||||||
|
id1 == id2
|
||||||
|
|
||||||
|
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...
|
||||||
|
|
||||||
|
class Continuation
|
||||||
|
constructor: (@kont) ->
|
||||||
|
invoke: (value, env, kont) ->
|
||||||
|
if nilp cdr value
|
||||||
|
@kont.resume (car value)
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Continuations expect one argument"
|
||||||
|
unwind: (value, ktarget) ->
|
||||||
|
if (@kont == ktarget) then (@kont.resume value) else (@kont.unwind value, ktarget)
|
||||||
|
catchLookup: (tag, kk) ->
|
||||||
|
@kont.catchLookup tag, kk
|
||||||
|
|
||||||
|
# Abstract class representing the environment
|
||||||
|
|
||||||
|
class Environment
|
||||||
|
lookup: -> throw new LispInterpreterError "Nonspecific invocation"
|
||||||
|
update: -> throw new LispInterpreterError "Nonspecific invocation"
|
||||||
|
blockLookup: -> throw new LispInterpreterError "Not an Environment"
|
||||||
|
|
||||||
|
# Base of the environment stack. If you hit this, your variable was
|
||||||
|
# never found for lookup/update. Note that at this time in the
|
||||||
|
# class, you have not
|
||||||
|
|
||||||
|
class NullEnv extends Environment
|
||||||
|
lookup: (e) -> throw new LispInterpreterError "Unknown variable #{e}"
|
||||||
|
update: (e) -> throw new LispInterpreterError "Unknown variable #{e}"
|
||||||
|
blockLookup: (name) -> throw new LispInterpreterError "Unknown block label #{name}"
|
||||||
|
|
||||||
|
# This appears to be an easy and vaguely abstract handle to the
|
||||||
|
# environment. The book is not clear on the distinction between the
|
||||||
|
# FullEnv and the VariableEnv.
|
||||||
|
|
||||||
|
class FullEnv extends Environment
|
||||||
|
constructor: (@others, @name) ->
|
||||||
|
@_type = "FullEnv"
|
||||||
|
lookup: (name, kont) ->
|
||||||
|
@others.lookup name, kont
|
||||||
|
update: (name, kont, value) ->
|
||||||
|
@others.update name, kont, value
|
||||||
|
blockLookup: (name, kont, value) ->
|
||||||
|
@others.blockLookup(name, kont, value)
|
||||||
|
|
||||||
|
# This is the classic environment pair; either it's *this*
|
||||||
|
# environment, or it's a parent environment, until you hit the
|
||||||
|
# NullEnv. Once the name has been found, the continuation is called
|
||||||
|
# with the found value.
|
||||||
|
|
||||||
|
class VariableEnv extends FullEnv
|
||||||
|
constructor: (@others, @name, @value) ->
|
||||||
|
@_type = "VariableEnv"
|
||||||
|
lookup: (name, kont) ->
|
||||||
|
if name == @name
|
||||||
|
kont.resume @value
|
||||||
|
else
|
||||||
|
@others.lookup name, kont
|
||||||
|
update: (name, kont, value) ->
|
||||||
|
if name == @name
|
||||||
|
@value = value
|
||||||
|
kont.resume value
|
||||||
|
else
|
||||||
|
@others.update name, kont, value
|
||||||
|
|
||||||
|
# "Renders the quote term to the current continuation"; in a more
|
||||||
|
# familiar parlance, calls resume in the current context with the
|
||||||
|
# quoted term uninterpreted.
|
||||||
|
|
||||||
|
evaluateQuote = (v, env, kont) ->
|
||||||
|
kont.resume normalizeForms v
|
||||||
|
|
||||||
|
# Evaluates the conditional expression, creating a continuation with
|
||||||
|
# the current environment that, when resumed, evaluates either the
|
||||||
|
# true or false branch, again in the current enviornment.
|
||||||
|
|
||||||
|
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"
|
||||||
|
resume: (value) ->
|
||||||
|
evaluate (if value == the_false_value then @iff else @ift), @env, @kont
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
evaluateBegin = (exps, env, kont) ->
|
||||||
|
if (pairp exps)
|
||||||
|
if pairp (cdr exps)
|
||||||
|
evaluate (car exps), env, (new BeginCont kont, exps, env)
|
||||||
|
else
|
||||||
|
evaluate (car exps), env, kont
|
||||||
|
else
|
||||||
|
kont.resume("Begin empty value")
|
||||||
|
|
||||||
|
class BeginCont extends Continuation
|
||||||
|
constructor: (@kont, @exps, @env) ->
|
||||||
|
@_type = "BeginCont"
|
||||||
|
resume: (v) -> evaluateBegin (cdr @exps), @env, @kont
|
||||||
|
|
||||||
|
# In this continuation, we simply pass the continuation and the name
|
||||||
|
# to the environment to look up. The environment knows to call the
|
||||||
|
# continuation with the value.
|
||||||
|
|
||||||
|
evaluateVariable = (name, env, kont) ->
|
||||||
|
env.lookup(name, kont)
|
||||||
|
|
||||||
|
# This is the same dance as lookup, only with the continuation being
|
||||||
|
# called after an update has been performed.
|
||||||
|
|
||||||
|
evaluateSet = (name, exp, env, kont) ->
|
||||||
|
evaluate exp, env, (new SetCont(kont, name, env))
|
||||||
|
|
||||||
|
class SetCont extends Continuation
|
||||||
|
constructor: (@kont, @name, @env) ->
|
||||||
|
@_type = "SetCont"
|
||||||
|
resume: (value) ->
|
||||||
|
@env.update @name, @kont, value
|
||||||
|
|
||||||
|
# Calls the current contunation, passing it a new function wrapper.
|
||||||
|
|
||||||
|
evaluateLambda = (names, exp, env, kont) ->
|
||||||
|
kont.resume new Function names, exp, env
|
||||||
|
|
||||||
|
# Upon invocation, evaluates the body with a new environment that
|
||||||
|
# consists of the original names, their current values as called, and
|
||||||
|
# the continuation an the moment of invocation, which will continue
|
||||||
|
# (resume) execution once the function is finished.
|
||||||
|
#
|
||||||
|
# By the way: this is pretty much the whole the point.
|
||||||
|
|
||||||
|
class Function extends Value
|
||||||
|
constructor: (@variables, @body, @env) ->
|
||||||
|
@_type = "Function"
|
||||||
|
invoke: (values, env, kont) ->
|
||||||
|
evaluateBegin @body, (extend @env, @variables, values), kont
|
||||||
|
|
||||||
|
# Helper function to build name/value pairs for the current execution
|
||||||
|
# context.
|
||||||
|
|
||||||
|
extend = (env, names, values) ->
|
||||||
|
if (pairp names) and (pairp values)
|
||||||
|
new VariableEnv (extend env, (cdr names), (cdr values)), (car names), (car values)
|
||||||
|
else if (nilp names)
|
||||||
|
if (nilp values) then env else throw new LispInterpreterError "Arity mismatch"
|
||||||
|
else
|
||||||
|
new VariableEnv 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.
|
||||||
|
|
||||||
|
evaluateApplication = (exp, exps, env, kont) ->
|
||||||
|
evaluate exp, env, (new EvFunCont kont, exps, env)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
evaluateArguments = (exp, env, kont) ->
|
||||||
|
if (pairp exp)
|
||||||
|
evaluate (car exp), env, (new ArgumentCont kont, exp, env)
|
||||||
|
else
|
||||||
|
kont.resume(nil)
|
||||||
|
|
||||||
|
class ArgumentCont extends Continuation
|
||||||
|
constructor: (@kont, @exp, @env) ->
|
||||||
|
@_type = "ArgumentCont"
|
||||||
|
resume: (v) ->
|
||||||
|
evaluateArguments (cdr @exp), @env, (new GatherCont @kont, v)
|
||||||
|
|
||||||
|
# Gather the arguments as each ArgumentCont is resumed into a list to
|
||||||
|
# be passed to our next step.
|
||||||
|
|
||||||
|
class GatherCont extends Continuation
|
||||||
|
constructor: (@kont, @value) ->
|
||||||
|
@_type = "GatherCont"
|
||||||
|
resume: (value) ->
|
||||||
|
@kont.resume (cons @value, value)
|
||||||
|
|
||||||
|
# Upon resumption, invoke the function.
|
||||||
|
|
||||||
|
class ApplyCont extends Continuation
|
||||||
|
constructor: (@kont, @fn, @env) ->
|
||||||
|
@_type = "ApplyCont"
|
||||||
|
resume: (value) ->
|
||||||
|
@fn.invoke value, @env, @kont
|
||||||
|
|
||||||
|
# A special continuation that represents what we want the interpreter
|
||||||
|
# to do when it's done processing.
|
||||||
|
|
||||||
|
class BottomCont extends Continuation
|
||||||
|
constructor: (@kont, @func) ->
|
||||||
|
@_type = "BottomCont"
|
||||||
|
resume: (value) ->
|
||||||
|
@func(value)
|
||||||
|
unwind: (value, ktarget) ->
|
||||||
|
throw new LispInterpreterError "Obsolete continuation"
|
||||||
|
catchLookup: (tag, kk) ->
|
||||||
|
throw new LispInterpreterError "No associated catch"
|
||||||
|
|
||||||
|
evaluateBlock = (label, body, env, kont) ->
|
||||||
|
k = new BlockCont(kont, label)
|
||||||
|
evaluateBegin body, (new BlockEnv env, label, kont), k
|
||||||
|
|
||||||
|
class BlockCont extends Continuation
|
||||||
|
constructor: (@kont, @label) ->
|
||||||
|
@_type = "BlockCont"
|
||||||
|
resume: (value) ->
|
||||||
|
@kont.resume value
|
||||||
|
|
||||||
|
class BlockEnv extends FullEnv
|
||||||
|
constructor: (@others, @name, @kont) ->
|
||||||
|
blockLookup: (name, kont, value) ->
|
||||||
|
if (name == @name)
|
||||||
|
kont.unwind value, @kont
|
||||||
|
else
|
||||||
|
@others.blockLookup(name, kont, value)
|
||||||
|
|
||||||
|
evaluateReturnFrom = (label, form, env, kont) ->
|
||||||
|
evaluate form, env, (new ReturnFromCont kont, env, label)
|
||||||
|
|
||||||
|
class ReturnFromCont extends Continuation
|
||||||
|
constructor: (@kont, @env, @label) ->
|
||||||
|
resume: (v) ->
|
||||||
|
@env.blockLookup @label, @kont, v
|
||||||
|
|
||||||
|
evaluateCatch = (tag, body, env, kont) ->
|
||||||
|
evaluate tag, env, (new CatchCont kont, body, env)
|
||||||
|
|
||||||
|
class CatchCont extends Continuation
|
||||||
|
constructor: (@kont, @body, @env) ->
|
||||||
|
resume: (value) ->
|
||||||
|
evaluateBegin @body, @env, (new LabeledCont @kont, value)
|
||||||
|
|
||||||
|
class LabeledCont extends Continuation
|
||||||
|
constructor: (@kont, @tag) ->
|
||||||
|
resume: (value) ->
|
||||||
|
@kont.resume value
|
||||||
|
catchLookup: (tag, kk) ->
|
||||||
|
if eq tag, @tag
|
||||||
|
evaluate kk.form, kk.env, (new ThrowingCont kk, tag, this)
|
||||||
|
else
|
||||||
|
@kont.catchLookup tag, kk
|
||||||
|
|
||||||
|
class ThrowCont extends Continuation
|
||||||
|
constructor: (@kont, @form, @env) ->
|
||||||
|
resume: (value) ->
|
||||||
|
@catchLookup value, @
|
||||||
|
|
||||||
|
evaluateThrow = (tag, form, env, kont) ->
|
||||||
|
evaluate tag, env, (new ThrowCont kont, form, env)
|
||||||
|
|
||||||
|
class ThrowingCont extends Continuation
|
||||||
|
constructor: (@kont, @tag, @resumecont) ->
|
||||||
|
resume: (value) ->
|
||||||
|
@resumecont.resume value
|
||||||
|
|
||||||
|
evaluateUnwindProtect = (form, cleanup, env, kont) ->
|
||||||
|
evaluate form, env, (new UnwindProtectCont kont, cleanup, env)
|
||||||
|
|
||||||
|
class UnwindProtectCont extends Continuation
|
||||||
|
constructor: (@kont, @cleanup, @env) ->
|
||||||
|
resume: (value) ->
|
||||||
|
evaluateBegin @cleanup, @env, (new ProtectReturnCont @kont, value)
|
||||||
|
|
||||||
|
class Primitive extends Value
|
||||||
|
constructor: (@name, @nativ) ->
|
||||||
|
@_type = "Primitive"
|
||||||
|
invoke: (args, env, kont) ->
|
||||||
|
@nativ.apply null, [args, env, kont]
|
||||||
|
|
||||||
|
env_init = new NullEnv()
|
||||||
|
|
||||||
|
definitial = (name, value = nil) ->
|
||||||
|
env_init = new VariableEnv env_init, name, value
|
||||||
|
name
|
||||||
|
|
||||||
|
defprimitive = (name, nativ, arity) ->
|
||||||
|
definitial name, new Primitive name, (args, env, kont) ->
|
||||||
|
vmargs = listToVector(args)
|
||||||
|
if (vmargs.length == arity)
|
||||||
|
kont.resume (nativ.apply null, vmargs)
|
||||||
|
else
|
||||||
|
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
|
||||||
|
|
||||||
|
definitial "#t", true
|
||||||
|
definitial "#f", the_false_value
|
||||||
|
definitial "nil", nil
|
||||||
|
|
||||||
|
# FIXME: All of these things dereference to the same value!!!!
|
||||||
|
|
||||||
|
for i in [
|
||||||
|
"x", "y", "z", "a", "b", "c", "foo", "bar", "hux",
|
||||||
|
"fib", "fact", "visit", "primes", "length"]
|
||||||
|
definitial i
|
||||||
|
|
||||||
|
defprimitive "cons", cons, 2
|
||||||
|
defprimitive "car", car, 2
|
||||||
|
defprimitive "cdr", cdr, 2
|
||||||
|
defprimitive "set-cdr!", setcdr, 2
|
||||||
|
defprimitive "set-car!", setcar, 2
|
||||||
|
defprimitive "+", ((a, b) -> a + b), 2
|
||||||
|
defprimitive "*", ((a, b) -> a * b), 2
|
||||||
|
defprimitive "-", ((a, b) -> a - b), 2
|
||||||
|
defprimitive "/", ((a, b) -> a / b), 2
|
||||||
|
defpredicate "lt", ((a, b) -> a < b), 2
|
||||||
|
defpredicate "gt", ((a, b) -> a > b), 2
|
||||||
|
defpredicate "lte", ((a, b) -> a <= b), 2
|
||||||
|
defpredicate "gte", ((a, b) -> a >= b), 2
|
||||||
|
defpredicate "eq?", ((a, b) -> a == b), 2
|
||||||
|
defpredicate "pair?", ((a) -> pairp a), 1
|
||||||
|
defpredicate "nil?", ((a) -> nilp a), 1
|
||||||
|
defpredicate "symbol?", ((a) -> /\-?[0-9]+$/.test(a) == false), 1
|
||||||
|
|
||||||
|
definitial "call/cc", new Primitive "call/cc", (values, env, kont) ->
|
||||||
|
if nilp cdr values
|
||||||
|
(car values).invoke (cons kont), env, kont
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Incorrect arity for call/cc"
|
||||||
|
|
||||||
|
definitial "apply", new Primitive "apply", (values, env, kont) ->
|
||||||
|
if pairp cdr values
|
||||||
|
f = car values
|
||||||
|
args = (() ->
|
||||||
|
(flat = (args) ->
|
||||||
|
if nilp (cdr args) then (car args) else (cons (car args), (flat cdr args)))(cdr values))()
|
||||||
|
f.invoke args, env, kont
|
||||||
|
|
||||||
|
definitial "funcall", new Primitive "funcall", (args, env, kont) ->
|
||||||
|
if not nilp cdr args
|
||||||
|
@kont.invoke (car args), (cdr args)
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Invoke requires a function name and arguments"
|
||||||
|
|
||||||
|
definitial "list", new Primitive "list", (values, env, kont) ->
|
||||||
|
(values, env, kont) -> kont.resume(values)
|
||||||
|
|
||||||
|
# Only called in rich node mode...
|
||||||
|
|
||||||
|
astSymbolsToLispSymbols = (node) ->
|
||||||
|
return nil if nilp node
|
||||||
|
throw (new LispInterpreterError "Not a list of variable names") if not node.type == 'list'
|
||||||
|
handler = (cell) ->
|
||||||
|
return nil if nilp cell
|
||||||
|
cons (car cell).value, (handler cdr cell)
|
||||||
|
handler node.value
|
||||||
|
|
||||||
|
metadata_evaluation =
|
||||||
|
listp: (node) -> node.type == 'list'
|
||||||
|
symbolp: (node) -> node.type == 'symbol'
|
||||||
|
numberp: (node) -> node.type == 'number'
|
||||||
|
stringp: (node) -> node.type == 'string'
|
||||||
|
commentp: (node) -> node.type == 'comment'
|
||||||
|
nvalu: (node) -> node.value
|
||||||
|
mksymbols: (list) -> astSymbolsToLispSymbols(list)
|
||||||
|
|
||||||
|
straight_evaluation =
|
||||||
|
listp: (cell) -> cell.__type == 'list'
|
||||||
|
symbolp: (cell) -> typeof cell == 'string' and cell.length > 0 and cell[0] not in ["\"", ";"]
|
||||||
|
commentp: (cell) -> typeof cell == 'string' and cell.length > 0 and cell[0] == ";"
|
||||||
|
numberp: (cell) -> typeof cell == 'number'
|
||||||
|
stringp: (cell) -> typeof cell == 'string' and cell.length > 0 and cell[0] == "\""
|
||||||
|
boolp: (cell) -> typeof cell == 'boolean'
|
||||||
|
nullp: (cell) -> cell == null
|
||||||
|
vectorp: (cell) -> (not straight_evaluation.listp cell) and toString.call(cell) == '[object Array]'
|
||||||
|
recordp: (cell) -> (not cell._prototype?) and toSTring.call(cell) == '[object Object]'
|
||||||
|
objectp: (cell) -> (cell._prototype?) and toString.call(cell) == '[object Object]'
|
||||||
|
nilp: (cell) -> nilp(cell)
|
||||||
|
nvalu: (cell) -> cell
|
||||||
|
mksymbols: (cell) -> cell
|
||||||
|
|
||||||
|
makeEvaluator = (ix = straight_evaluation, ty="straight") ->
|
||||||
|
(exp, env, kont) ->
|
||||||
|
if ix.symbolp exp
|
||||||
|
return evaluateVariable (ix.nvalu exp), env, kont
|
||||||
|
else if ([ix.numberp, ix.stringp].filter (i) -> i(exp)).length > 0
|
||||||
|
return kont.resume ix.nvalu exp
|
||||||
|
else if ix.listp exp
|
||||||
|
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
|
||||||
|
else
|
||||||
|
evaluateApplication (car body), (cdr body), env, kont
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError("Can't handle a '#{type}'")
|
||||||
|
|
||||||
|
nodeEval = makeEvaluator(metadata_evaluation, "node")
|
||||||
|
lispEval = makeEvaluator(straight_evaluation, "lisp")
|
||||||
|
|
||||||
|
evaluate = (exp, env, kont) ->
|
||||||
|
(if exp? and (exp instanceof Node) then nodeEval else lispEval)(exp, env, kont)
|
||||||
|
|
||||||
|
interpreter = (ast, kont) ->
|
||||||
|
evaluate ast, env_init, new BottomCont null, kont
|
||||||
|
|
||||||
|
module.exports = interpreter
|
|
@ -0,0 +1,549 @@
|
||||||
|
{listToString, listToVector, pairp, cons, car, cdr, caar, cddr, cdar,
|
||||||
|
cadr, caadr, cadar, caddr, nilp, nil, setcdr,
|
||||||
|
metacadr, setcar} = require "cons-lists/lists"
|
||||||
|
{normalizeForms, normalizeForm} = require "../chapter1/astToList"
|
||||||
|
{Node, Symbol} = require '../chapter1/reader_types'
|
||||||
|
|
||||||
|
class LispInterpreterError extends Error
|
||||||
|
name: 'LispInterpreterError'
|
||||||
|
constructor: (@message) ->
|
||||||
|
|
||||||
|
the_false_value = (cons "false", "boolean")
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
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}"
|
||||||
|
|
||||||
|
# Abstract class representing the environment
|
||||||
|
|
||||||
|
class Environment
|
||||||
|
lookup: -> throw new LispInterpreterError "Nonspecific invocation"
|
||||||
|
update: -> throw new LispInterpreterError "Nonspecific invocation"
|
||||||
|
blockLookup: -> throw new LispInterpreterError "Not an Environment"
|
||||||
|
|
||||||
|
# Base of the environment stack. If you hit this, your variable was
|
||||||
|
# never found for lookup/update. Note that at this time in the
|
||||||
|
# class, you have not
|
||||||
|
|
||||||
|
class NullEnv extends Environment
|
||||||
|
lookup: (e) -> throw new LispInterpreterError "Unknown variable #{e}"
|
||||||
|
update: (e) -> throw new LispInterpreterError "Unknown variable #{e}"
|
||||||
|
blockLookup: (name) -> throw new LispInterpreterError "Unknown block label #{name}"
|
||||||
|
|
||||||
|
# This appears to be an easy and vaguely abstract handle to the
|
||||||
|
# environment. The book is not clear on the distinction between the
|
||||||
|
# FullEnv and the VariableEnv.
|
||||||
|
|
||||||
|
class FullEnv extends Environment
|
||||||
|
constructor: (@others, @name) ->
|
||||||
|
@_type = "FullEnv"
|
||||||
|
lookup: (name, kont) ->
|
||||||
|
@others.lookup name, kont
|
||||||
|
update: (name, kont, value) ->
|
||||||
|
@others.update name, kont, value
|
||||||
|
blockLookup: (name, kont, value) ->
|
||||||
|
@others.blockLookup(name, kont, value)
|
||||||
|
|
||||||
|
# This is the classic environment pair; either it's *this*
|
||||||
|
# environment, or it's a parent environment, until you hit the
|
||||||
|
# NullEnv. Once the name has been found, the continuation is called
|
||||||
|
# with the found value.
|
||||||
|
|
||||||
|
class VariableEnv extends FullEnv
|
||||||
|
constructor: (@others, @name, @value) ->
|
||||||
|
@_type = "VariableEnv"
|
||||||
|
lookup: (name, kont) ->
|
||||||
|
if name == @name
|
||||||
|
kont.resume @value
|
||||||
|
else
|
||||||
|
@others.lookup name, kont
|
||||||
|
update: (name, kont, value) ->
|
||||||
|
if name == @name
|
||||||
|
@value = value
|
||||||
|
kont.resume value
|
||||||
|
else
|
||||||
|
@others.update name, kont, value
|
||||||
|
|
||||||
|
# "Renders the quote term to the current continuation"; in a more
|
||||||
|
# familiar parlance, calls resume in the current context with the
|
||||||
|
# quoted term uninterpreted.
|
||||||
|
|
||||||
|
evaluateQuote = (v, env, kont) ->
|
||||||
|
kont.resume normalizeForms v
|
||||||
|
|
||||||
|
# Evaluates the conditional expression, creating a continuation with
|
||||||
|
# the current environment that, when resumed, evaluates either the
|
||||||
|
# true or false branch, again in the current enviornment.
|
||||||
|
|
||||||
|
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"
|
||||||
|
resume: (value) ->
|
||||||
|
evaluate (if value == the_false_value then @iff else @ift), @env, @kont
|
||||||
|
|
||||||
|
# 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. 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)
|
||||||
|
if pairp (cdr exps)
|
||||||
|
evaluate (car exps), env, (new BeginCont kont, exps, env)
|
||||||
|
else
|
||||||
|
evaluate (car exps), env, kont
|
||||||
|
else
|
||||||
|
kont.resume("Begin empty value")
|
||||||
|
|
||||||
|
class BeginCont extends Continuation
|
||||||
|
constructor: (@kont, @exps, @env) ->
|
||||||
|
@_type = "BeginCont"
|
||||||
|
resume: (v) -> evaluateBegin (cdr @exps), @env, @kont
|
||||||
|
|
||||||
|
# In this continuation, we simply pass the continuation and the name
|
||||||
|
# to the environment to look up. The environment knows to call the
|
||||||
|
# continuation with the value.
|
||||||
|
|
||||||
|
evaluateVariable = (name, env, kont) ->
|
||||||
|
env.lookup(name, kont)
|
||||||
|
|
||||||
|
# This is the same dance as lookup, only with the continuation being
|
||||||
|
# called after an update has been performed.
|
||||||
|
|
||||||
|
evaluateSet = (name, exp, env, kont) ->
|
||||||
|
evaluate exp, env, (new SetCont(kont, name, env))
|
||||||
|
|
||||||
|
class SetCont extends Continuation
|
||||||
|
constructor: (@kont, @name, @env) ->
|
||||||
|
@_type = "SetCont"
|
||||||
|
resume: (value) ->
|
||||||
|
@env.update @name, @kont, value
|
||||||
|
|
||||||
|
# Calls the current contunation, passing it a new function wrapper.
|
||||||
|
|
||||||
|
evaluateLambda = (names, exp, env, kont) ->
|
||||||
|
kont.resume new Function names, exp, env
|
||||||
|
|
||||||
|
# Upon invocation, evaluates the body with a new environment that
|
||||||
|
# consists of the original names, their current values as called, and
|
||||||
|
# the continuation an the moment of invocation, which will continue
|
||||||
|
# (resume) execution once the function is finished.
|
||||||
|
#
|
||||||
|
# By the way: this is pretty much the whole the point.
|
||||||
|
|
||||||
|
class Function extends Value
|
||||||
|
constructor: (@variables, @body, @env) ->
|
||||||
|
@_type = "Function"
|
||||||
|
invoke: (values, env, kont) ->
|
||||||
|
evaluateBegin @body, (extend @env, @variables, values), kont
|
||||||
|
|
||||||
|
# Helper function to build name/value pairs for the current execution
|
||||||
|
# context.
|
||||||
|
|
||||||
|
extend = (env, names, values) ->
|
||||||
|
if (pairp names) and (pairp values)
|
||||||
|
new VariableEnv (extend env, (cdr names), (cdr values)), (car names), (car values)
|
||||||
|
else if (nilp names)
|
||||||
|
if (nilp values) then env else throw new LispInterpreterError "Arity mismatch"
|
||||||
|
else
|
||||||
|
new VariableEnv env, names, values
|
||||||
|
|
||||||
|
# Now we start the invocation: this is applying the function. Let's
|
||||||
|
# take it stepwise.
|
||||||
|
|
||||||
|
# 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 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)
|
||||||
|
evaluate (car exp), env, (new ArgumentCont kont, exp, env)
|
||||||
|
else
|
||||||
|
kont.resume(nil)
|
||||||
|
|
||||||
|
class ArgumentCont extends Continuation
|
||||||
|
constructor: (@kont, @exp, @env) ->
|
||||||
|
@_type = "ArgumentCont"
|
||||||
|
resume: (v) ->
|
||||||
|
evaluateArguments (cdr @exp), @env, (new GatherCont @kont, v)
|
||||||
|
|
||||||
|
# Gather the arguments as each ArgumentCont is resumed into a list to
|
||||||
|
# be passed to our next step.
|
||||||
|
|
||||||
|
class GatherCont extends Continuation
|
||||||
|
constructor: (@kont, @value) ->
|
||||||
|
@_type = "GatherCont"
|
||||||
|
resume: (value) ->
|
||||||
|
@kont.resume (cons @value, value)
|
||||||
|
|
||||||
|
# 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) ->
|
||||||
|
@_type = "ApplyCont"
|
||||||
|
resume: (value) ->
|
||||||
|
@fn.invoke value, @env, @kont
|
||||||
|
|
||||||
|
# A special continuation that represents what we want the interpreter
|
||||||
|
# to do when it's done processing.
|
||||||
|
|
||||||
|
class BottomCont extends Continuation
|
||||||
|
constructor: (@kont, @func) ->
|
||||||
|
@_type = "BottomCont"
|
||||||
|
resume: (value) ->
|
||||||
|
@func(value)
|
||||||
|
unwind: (value, ktarget) ->
|
||||||
|
throw new LispInterpreterError "Obsolete 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
|
||||||
|
|
||||||
|
class BlockCont extends Continuation
|
||||||
|
constructor: (@kont, @label) ->
|
||||||
|
@_type = "BlockCont"
|
||||||
|
resume: (value) ->
|
||||||
|
@kont.resume value
|
||||||
|
|
||||||
|
class BlockEnv extends FullEnv
|
||||||
|
constructor: (@others, @name, @kont) ->
|
||||||
|
blockLookup: (name, kont, value) ->
|
||||||
|
if (name == @name)
|
||||||
|
kont.unwind value, @kont
|
||||||
|
else
|
||||||
|
@others.blockLookup(name, kont, value)
|
||||||
|
|
||||||
|
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"
|
||||||
|
resume: (v) ->
|
||||||
|
@env.blockLookup @label, @kont, v
|
||||||
|
|
||||||
|
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 an object
|
||||||
|
# to be evaluated with its current environment, then continue with
|
||||||
|
# *this* as the continuation passed to throwing-continuation, which
|
||||||
|
# resumes the catchLookup until the stack is exhausted.
|
||||||
|
|
||||||
|
class LabeledCont extends Continuation
|
||||||
|
constructor: (@kont, @tag) ->
|
||||||
|
@_type = "LabeledFromCont"
|
||||||
|
resume: (value) ->
|
||||||
|
@kont.resume value
|
||||||
|
catchLookup: (tag, kk) ->
|
||||||
|
if eq tag, @tag
|
||||||
|
evaluate kk.form, kk.env, (new ThrowingCont kk, tag, this)
|
||||||
|
else
|
||||||
|
@kont.catchLookup tag, kk
|
||||||
|
|
||||||
|
class ThrowCont extends Continuation
|
||||||
|
constructor: (@kont, @form, @env) ->
|
||||||
|
@_type = "ThrowCont"
|
||||||
|
resume: (value) ->
|
||||||
|
@catchLookup value, @
|
||||||
|
|
||||||
|
evaluateThrow = (tag, form, env, kont) ->
|
||||||
|
evaluate tag, env, (new ThrowCont kont, form, env)
|
||||||
|
|
||||||
|
class UnwindCont extends Continuation
|
||||||
|
constructor: (@kont, @value, @target) ->
|
||||||
|
resume: (value) ->
|
||||||
|
@kont.unwind @value, @target
|
||||||
|
|
||||||
|
evaluateUnwindProtect = (form, cleanup, env, kont) ->
|
||||||
|
evaluate form, env, (new UnwindProtectCont kont, cleanup, env)
|
||||||
|
|
||||||
|
# If the continuation is "resumed," it works like normal; but if its
|
||||||
|
# "unwound," it works its way up the unwind stack looking for the
|
||||||
|
# target continuation to which to deliver the value.
|
||||||
|
|
||||||
|
class UnwindProtectCont extends Continuation
|
||||||
|
constructor: (@kont, @cleanup, @env) ->
|
||||||
|
@_type = "UnwindProtectCont"
|
||||||
|
resume: (value) ->
|
||||||
|
evaluateBegin @cleanup, @env, (new ProtectReturnCont @kont, value)
|
||||||
|
unwind: (value, target) ->
|
||||||
|
evaluateBegin @cleanup, @env, (new UnwindCont @kont, 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: the throwing-continuation was
|
||||||
|
# constructed with the catch continuation itself as the address of the
|
||||||
|
# resumecont.
|
||||||
|
|
||||||
|
class ThrowingCont extends Continuation
|
||||||
|
constructor: (@kont, @tag, @resumecont) ->
|
||||||
|
@_type = "ThrowingCont"
|
||||||
|
resume: (value) ->
|
||||||
|
@kont.unwind value, @resumecont
|
||||||
|
|
||||||
|
# Note that this behavior basically much like throwing-continuation,
|
||||||
|
# except that it's the resumption (the next continuation), rather than
|
||||||
|
# the rewind.
|
||||||
|
|
||||||
|
class ProtectReturnCont extends Continuation
|
||||||
|
constructor: (@kont, @value) ->
|
||||||
|
@_type = "ProtectReturnCont"
|
||||||
|
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"
|
||||||
|
invoke: (args, env, kont) ->
|
||||||
|
@nativ.apply null, [args, env, kont]
|
||||||
|
|
||||||
|
env_init = new NullEnv()
|
||||||
|
|
||||||
|
definitial = (name, value = nil) ->
|
||||||
|
env_init = new VariableEnv env_init, name, value
|
||||||
|
name
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
defpredicate = (name, nativ, arity) ->
|
||||||
|
defprimitive name, ((a, b) -> if nativ.call(null, a, b) then true else the_false_value), arity
|
||||||
|
|
||||||
|
definitial "#t", true
|
||||||
|
definitial "#f", the_false_value
|
||||||
|
definitial "nil", nil
|
||||||
|
|
||||||
|
# FIXME: All of these things dereference to the same value!!!!
|
||||||
|
|
||||||
|
for i in [
|
||||||
|
"x", "y", "z", "a", "b", "c", "foo", "bar", "hux",
|
||||||
|
"fib", "fact", "visit", "primes", "length"]
|
||||||
|
definitial i
|
||||||
|
|
||||||
|
defprimitive "cons", cons, 2
|
||||||
|
defprimitive "car", car, 2
|
||||||
|
defprimitive "cdr", cdr, 2
|
||||||
|
defprimitive "set-cdr!", setcdr, 2
|
||||||
|
defprimitive "set-car!", setcar, 2
|
||||||
|
defprimitive "+", ((a, b) -> a + b), 2
|
||||||
|
defprimitive "*", ((a, b) -> a * b), 2
|
||||||
|
defprimitive "-", ((a, b) -> a - b), 2
|
||||||
|
defprimitive "/", ((a, b) -> a / b), 2
|
||||||
|
defpredicate "lt", ((a, b) -> a < b), 2
|
||||||
|
defpredicate "gt", ((a, b) -> a > b), 2
|
||||||
|
defpredicate "lte", ((a, b) -> a <= b), 2
|
||||||
|
defpredicate "gte", ((a, b) -> a >= b), 2
|
||||||
|
defpredicate "eq?", ((a, b) -> a == b), 2
|
||||||
|
defpredicate "pair?", ((a) -> pairp a), 1
|
||||||
|
defpredicate "nil?", ((a) -> nilp a), 1
|
||||||
|
defpredicate "symbol?", ((a) -> /\-?[0-9]+$/.test(a) == false), 1
|
||||||
|
|
||||||
|
definitial "call/cc", new Primitive "call/cc", (values, env, kont) ->
|
||||||
|
if nilp cdr values
|
||||||
|
(car values).invoke (cons kont), env, kont
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Incorrect arity for call/cc"
|
||||||
|
|
||||||
|
definitial "apply", new Primitive "apply", (values, env, kont) ->
|
||||||
|
if pairp cdr values
|
||||||
|
f = car values
|
||||||
|
args = (() ->
|
||||||
|
(flat = (args) ->
|
||||||
|
if nilp (cdr args) then (car args) else (cons (car args), (flat cdr args)))(cdr values))()
|
||||||
|
f.invoke args, env, kont
|
||||||
|
|
||||||
|
definitial "funcall", new Primitive "funcall", (args, env, kont) ->
|
||||||
|
if not nilp cdr args
|
||||||
|
kont.invoke (env.lookup (car args)), (cdr args)
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Invoke requires a function name and arguments"
|
||||||
|
|
||||||
|
definitial "list", new Primitive "list", (values, env, kont) ->
|
||||||
|
(values, env, kont) -> kont.resume(values)
|
||||||
|
|
||||||
|
# Only called in rich node mode...
|
||||||
|
|
||||||
|
astSymbolsToLispSymbols = (node) ->
|
||||||
|
return nil if nilp node
|
||||||
|
throw (new LispInterpreterError "Not a list of variable names") if not node.type == 'list'
|
||||||
|
handler = (cell) ->
|
||||||
|
return nil if nilp cell
|
||||||
|
cons (car cell).value, (handler cdr cell)
|
||||||
|
handler node.value
|
||||||
|
|
||||||
|
metadata_evaluation =
|
||||||
|
listp: (node) -> node.type == 'list'
|
||||||
|
symbolp: (node) -> node.type == 'symbol'
|
||||||
|
numberp: (node) -> node.type == 'number'
|
||||||
|
stringp: (node) -> node.type == 'string'
|
||||||
|
commentp: (node) -> node.type == 'comment'
|
||||||
|
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 ["\"", ";"]
|
||||||
|
commentp: (cell) -> typeof cell == 'string' and cell.length > 0 and cell[0] == ";"
|
||||||
|
numberp: (cell) -> typeof cell == 'number'
|
||||||
|
stringp: (cell) -> typeof cell == 'string' and cell.length > 0 and cell[0] == "\""
|
||||||
|
boolp: (cell) -> typeof cell == 'boolean'
|
||||||
|
nullp: (cell) -> cell == null
|
||||||
|
vectorp: (cell) -> (not straight_evaluation.listp cell) and toString.call(cell) == '[object Array]'
|
||||||
|
recordp: (cell) -> (not cell._prototype?) and toSTring.call(cell) == '[object Object]'
|
||||||
|
objectp: (cell) -> (cell._prototype?) and toString.call(cell) == '[object Object]'
|
||||||
|
nilp: (cell) -> nilp(cell)
|
||||||
|
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
|
||||||
|
return evaluateVariable (ix.nvalu exp), env, kont
|
||||||
|
else if ([ix.numberp, ix.stringp].filter (i) -> i(exp)).length > 0
|
||||||
|
return kont.resume ix.nvalu exp
|
||||||
|
else if ix.listp exp
|
||||||
|
body = ix.nvalu exp
|
||||||
|
head = car body
|
||||||
|
if ix.symbolp head
|
||||||
|
# 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
|
||||||
|
throw new LispInterpreterError("Can't handle a '#{type}'")
|
||||||
|
|
||||||
|
nodeEval = makeEvaluator(metadata_evaluation, "node")
|
||||||
|
lispEval = makeEvaluator(straight_evaluation, "lisp")
|
||||||
|
|
||||||
|
evaluate = (exp, env, kont) ->
|
||||||
|
(if exp? and (exp instanceof Node) then nodeEval else lispEval)(exp, env, kont)
|
||||||
|
|
||||||
|
interpreter = (ast, kont) ->
|
||||||
|
evaluate ast, env_init, new BottomCont null, kont
|
||||||
|
|
||||||
|
module.exports = interpreter
|
|
@ -0,0 +1,12 @@
|
||||||
|
olisp = require '../chapter3g/interpreter'
|
||||||
|
{read, readForms} = require '../chapter1/reader'
|
||||||
|
{inspect} = require 'util'
|
||||||
|
|
||||||
|
lisp = (ast) ->
|
||||||
|
ret = undefined
|
||||||
|
olisp ast, (i) -> ret = i
|
||||||
|
return ret
|
||||||
|
|
||||||
|
# console.log lisp read "(catch 2 (* 7 (catch 1 (* 3 (catch 2 (throw 1 (throw 2 5)) )) )))"
|
||||||
|
|
||||||
|
console.log lisp read "((lambda (c) (catch 111 (* 2 (unwind-protect (* 3 (throw 111 5)) (set! c 1) ))) ) 0)"
|
|
@ -0,0 +1,479 @@
|
||||||
|
{listToString, listToVector, pairp, cons, car, cdr, caar, cddr, cdar,
|
||||||
|
cadr, caadr, cadar, caddr, nilp, nil, setcdr,
|
||||||
|
metacadr, setcar} = require "cons-lists/lists"
|
||||||
|
{length} = require "cons-lists/reduce"
|
||||||
|
{normalizeForms, normalizeForm} = require "../chapter1/astToList"
|
||||||
|
{Node, Comment, Symbol} = require '../chapter1/reader_types'
|
||||||
|
{inspect} = require 'util'
|
||||||
|
|
||||||
|
itap = (a) -> return inspect a, true, null, false
|
||||||
|
|
||||||
|
class LispInterpreterError extends Error
|
||||||
|
name: 'LispInterpreterError'
|
||||||
|
constructor: (@message) ->
|
||||||
|
|
||||||
|
the_false_value = (cons "false", "boolean")
|
||||||
|
|
||||||
|
eq = (id1, id2) ->
|
||||||
|
if id1 instanceof Symbol and id2 instanceof Symbol
|
||||||
|
return id1.name == id2.name
|
||||||
|
id1 == id2
|
||||||
|
|
||||||
|
# Only called in rich node mode...
|
||||||
|
|
||||||
|
astSymbolsToLispSymbols = (node) ->
|
||||||
|
return nil if nilp node
|
||||||
|
throw (new LispInterpreterError "Not a list of variable names") if not node.type == 'list'
|
||||||
|
handler = (cell) ->
|
||||||
|
return nil if nilp cell
|
||||||
|
cons (car cell).value, (handler cdr cell)
|
||||||
|
handler node.value
|
||||||
|
|
||||||
|
cadddr = metacadr('cadddr')
|
||||||
|
|
||||||
|
intlistp = (node) -> node.type == 'list'
|
||||||
|
intpairp = (node) -> node.type == 'list' and ((node.value.length < 2) or node.value[1].node.type != 'list')
|
||||||
|
intsymbolp = (node) -> node.type == 'symbol' or node instanceof Symbol
|
||||||
|
intnumberp = (node) -> node.type == 'number'
|
||||||
|
intstringp = (node) -> node.type == 'string'
|
||||||
|
intcommentp = (node) -> node.type == 'comment'
|
||||||
|
intnvalu = (node) -> node.value
|
||||||
|
intatomp = (node) -> node.type in ['symbol', 'number', 'string']
|
||||||
|
intnullp = (node) -> node.type == 'symbol' and node.value.name == 'null'
|
||||||
|
intmksymbols = (list) -> astSymbolsToLispSymbols(list)
|
||||||
|
|
||||||
|
# The hairness of this makes me doubt the wisdom of using Javascript.
|
||||||
|
|
||||||
|
sBehavior = new Symbol 'behavior'
|
||||||
|
sBoolean = new Symbol 'boolean'
|
||||||
|
sBoolify = new Symbol 'boolify'
|
||||||
|
sFunction = new Symbol 'function'
|
||||||
|
sSymbol = new Symbol 'symbol'
|
||||||
|
sString = new Symbol 'string'
|
||||||
|
sValue = new Symbol 'chars'
|
||||||
|
sName = new Symbol 'name'
|
||||||
|
sNumber = new Symbol 'number'
|
||||||
|
sNull = new Symbol 'null'
|
||||||
|
sTag = new Symbol 'tag'
|
||||||
|
sType = new Symbol 'type'
|
||||||
|
sValue = new Symbol 'value'
|
||||||
|
sPair = new Symbol 'pair'
|
||||||
|
sCar = new Symbol 'car'
|
||||||
|
sCdr = new Symbol 'cdr'
|
||||||
|
sSetCar = new Symbol 'setcar'
|
||||||
|
sSetCdr = new Symbol 'setcdr'
|
||||||
|
|
||||||
|
prox =
|
||||||
|
"quote": (body, env, mem, kont) -> evaluateQuote (cadr body), env, mem, kont
|
||||||
|
"if": (body, env, mem, kont) -> evaluateIf (cadr body), (caddr body), (cadddr body), env, mem, kont
|
||||||
|
"begin": (body, env, mem, kont) -> evaluateBegin (cdr body), env, mem, kont
|
||||||
|
"set!": (body, env, mem, kont) -> evaluateSet (intnvalu cadr body), (caddr body), env, mem, kont
|
||||||
|
"lambda": (body, env, mem, kont) -> evaluateLambda (intmksymbols cadr body), (cddr body), env, mem, kont
|
||||||
|
"or": (body, env, mem, kont) -> evaluateOr (cadr body), (caddr body), env, mem, kont
|
||||||
|
|
||||||
|
# ___ _ _
|
||||||
|
# | __|_ ____ _| |_ _ __ _| |_ ___ _ _
|
||||||
|
# | _|\ V / _` | | || / _` | _/ _ \ '_|
|
||||||
|
# |___|\_/\__,_|_|\_,_\__,_|\__\___/_|
|
||||||
|
#
|
||||||
|
|
||||||
|
transcode = (value, mem, qont) ->
|
||||||
|
forms = [
|
||||||
|
[intnullp, -> qont theEmptyList, mem],
|
||||||
|
[((v) -> intsymbolp(v) and v in ['#t', '#f']), (-> qont (createBoolean value), mem)]
|
||||||
|
[intsymbolp, (-> qont (createSymbol value), mem)]
|
||||||
|
[intnumberp, (-> qont (createNumber value), mem)]
|
||||||
|
[intstringp, (-> qont (createString value), mem)]
|
||||||
|
[intlistp, (-> transcode (car intnvalu value), mem, (addr, mem2) ->
|
||||||
|
(transcode (cdr intvalu value), mem2, (d, mem3) ->
|
||||||
|
(allocatePair addr, d, mem3, qont)))]
|
||||||
|
]
|
||||||
|
found = (form[1] for form in forms when form[0](value))
|
||||||
|
if found.length != 1
|
||||||
|
throw new LispInterpreterError "Bad transcode match for #{value}"
|
||||||
|
found[0]()
|
||||||
|
|
||||||
|
transcode2 = (value, mem, qont) ->
|
||||||
|
forms = [
|
||||||
|
[((v) -> v instanceof Symbol and v.name == 'null'), (-> qont theEmptyList, mem)],
|
||||||
|
[((v) -> v instanceof Symbol and v.name in ['#t', '#f']), (-> qont (createBoolean value), mem)]
|
||||||
|
[((v) -> v instanceof Symbol), (-> qont (createSymbol value), mem)]
|
||||||
|
[((v) -> typeof v == 'string'), (-> qont (createString value), mem)]
|
||||||
|
[((v) -> typeof v == 'number'), (-> qont (createNumber value), mem)]
|
||||||
|
[((v) -> v.__type == 'list'), (-> transcode (car value), mem, (addr, mem2) ->
|
||||||
|
(transcode (cdr value), mem2, (d, mem3) ->
|
||||||
|
(allocatePair addr, d, mem3, qont)))]
|
||||||
|
]
|
||||||
|
found = (form[1] for form in forms when form[0](value))
|
||||||
|
if found.length < 1
|
||||||
|
throw new LispInterpreterError "Bad transcode match for #{value}"
|
||||||
|
found[0]()
|
||||||
|
|
||||||
|
|
||||||
|
transcodeBack = (value, mem) ->
|
||||||
|
forms = [
|
||||||
|
[sBoolean, ((v) -> ((v sBoolify) true, false))]
|
||||||
|
[sSymbol, ((v) -> (v sName))]
|
||||||
|
[sString, ((v) -> (v sValue))]
|
||||||
|
[sNumber, ((v) -> (v sValue))]
|
||||||
|
[sPair, ((v) ->
|
||||||
|
cons (transcodeBack (mem (v sCar)), mem), (transcodeBack (mem (v sCdr)), mem))]
|
||||||
|
[sFunction, (v) -> v]
|
||||||
|
]
|
||||||
|
found = (form[1] for form in forms when (eq (value sType), form[0]))
|
||||||
|
if found.length != 1
|
||||||
|
throw new LispInterpreterError "Bad transcode-back match for #{value}"
|
||||||
|
found[0](value)
|
||||||
|
|
||||||
|
evaluate = (exp, env, mem, kont) ->
|
||||||
|
if intatomp exp
|
||||||
|
if intsymbolp exp
|
||||||
|
evaluateVariable (intnvalu exp), env, mem, kont
|
||||||
|
else
|
||||||
|
evaluateQuote exp, env, mem, kont
|
||||||
|
else
|
||||||
|
body = intnvalu exp
|
||||||
|
head = car body
|
||||||
|
pname = (intnvalu head)
|
||||||
|
if pname instanceof Symbol and prox[pname.name]?
|
||||||
|
prox[pname.name](body, env, mem, kont)
|
||||||
|
else
|
||||||
|
evaluateApplication head, (cdr body), env, mem, kont
|
||||||
|
|
||||||
|
env_init = (id) ->
|
||||||
|
throw new LispInterpreterError "No binding for " + id
|
||||||
|
|
||||||
|
# This is basically the core definition of 'mem': it returns a
|
||||||
|
# function enclosing the address (a monotomically increasing number as
|
||||||
|
# memory is allocated) and the value. Update is passed the current
|
||||||
|
# memory, the address, and the value; it returns a function that says
|
||||||
|
# "If the requested address is my address, return my value, otherwise
|
||||||
|
# I'll call the memory handed to me at creation time with the address,
|
||||||
|
# and it'll go down the line." Update basically adds to a 'stack'
|
||||||
|
# built entirely out of pointers to the base mem.
|
||||||
|
|
||||||
|
update = (mem, addr, value) ->
|
||||||
|
(addra) -> if (eq addra, addr) then value else (mem addra)
|
||||||
|
|
||||||
|
updates = (mem, addrs, values) ->
|
||||||
|
if (pairp addrs)
|
||||||
|
updates (update mem, (car addrs), (car values)), (cdr addrs), (cdr values)
|
||||||
|
else
|
||||||
|
mem
|
||||||
|
|
||||||
|
# Memory location zero contains the position of the stack.
|
||||||
|
|
||||||
|
expandStore = (highLocation, mem) ->
|
||||||
|
update mem, 0, highLocation
|
||||||
|
|
||||||
|
mem_init = expandStore 0, (a) ->
|
||||||
|
throw new LispInterpreterError "No such address #{a}"
|
||||||
|
|
||||||
|
newLocation = (mem) ->
|
||||||
|
(mem 0) + 1
|
||||||
|
|
||||||
|
evaluateVariable = (name, env, mem, kont) ->
|
||||||
|
kont (mem (env name)), mem
|
||||||
|
|
||||||
|
evaluateSet = (name, exp, env, mem, kont) ->
|
||||||
|
evaluate exp, env, mem, (value, mem2) ->
|
||||||
|
kont value, (update mem2, (env name), value)
|
||||||
|
|
||||||
|
evaluateApplication = (exp, exprs, env, mem, kont) ->
|
||||||
|
|
||||||
|
# In chapter 3, this was a series of jumping continuations chasing
|
||||||
|
# each other. Here, all of the continuations are kept in one place,
|
||||||
|
# and the argument list is built by tail-calls to evaluateArguments
|
||||||
|
# until the list is exhausted, at which point the continuation is
|
||||||
|
# called. The continuation is built in the second paragraph below.
|
||||||
|
|
||||||
|
evaluateArguments = (exprs, env, mem, kont) ->
|
||||||
|
if (pairp exprs)
|
||||||
|
evaluate (car exprs), env, mem, (value, mem2) ->
|
||||||
|
evaluateArguments (cdr exprs), env, mem2, (value2, mem3) ->
|
||||||
|
kont (cons value, value2), mem3
|
||||||
|
else
|
||||||
|
kont cons(), mem
|
||||||
|
|
||||||
|
evaluate exp, env, mem, (fun, mem2) ->
|
||||||
|
evaluateArguments exprs, env, mem2, (value2, mem3) ->
|
||||||
|
if eq (fun sType), sFunction
|
||||||
|
(fun sBehavior) value2, mem3, kont
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Not a function #{(car value2)}"
|
||||||
|
|
||||||
|
# Creates a memory address for the function, then creates a new memory
|
||||||
|
# address for each argument, then evaluates the expressions in the
|
||||||
|
# lambda, returning the value of the last one.
|
||||||
|
|
||||||
|
evaluateLambda = (names, exprs, env, mem, kont) ->
|
||||||
|
allocate 1, mem, (addrs, mem2) ->
|
||||||
|
kont (createFunction (car addrs), (values, mem, kont) ->
|
||||||
|
if eq (length names), (length values)
|
||||||
|
allocate (length names), mem, (addrs, mem2) ->
|
||||||
|
evaluateBegin exprs, (updates env, names, addrs), (updates mem2, addrs, values), kont
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Incorrect Arrity"), mem2
|
||||||
|
|
||||||
|
evaluateIf = (expc, expt, expf, env, mem, kont) ->
|
||||||
|
evaluate expc, env, mem, (env, mems) ->
|
||||||
|
evaluate ((env sBoolify) expt, expf), env, mems, kont
|
||||||
|
|
||||||
|
evaluateQuote = (c, env, mem, kont) ->
|
||||||
|
transcode2 (normalizeForm c), mem, kont
|
||||||
|
|
||||||
|
# By starting over "from here," we undo all side-effect assignments
|
||||||
|
# that were effected by expression 1
|
||||||
|
|
||||||
|
evaluateOr = (exp1, exp2, env, mem, kont) ->
|
||||||
|
evaluate exp1, env, mem, (value, mem2) ->
|
||||||
|
((value sBoolify) (-> kont value, mem2), (-> evaluate exp2, env, mem, kont))()
|
||||||
|
|
||||||
|
# I like how, in this version, we explicitly throw away the meaning of
|
||||||
|
# all but the last statement in evaluateBegin.
|
||||||
|
evaluateBegin = (exps, env, mem, kont) ->
|
||||||
|
if pairp (cdr exps)
|
||||||
|
evaluate (car exps), env, mem, (_, mems) ->
|
||||||
|
evaluateBegin (cdr exps), env, mems, kont
|
||||||
|
else
|
||||||
|
evaluate (car exps), env, mem, kont
|
||||||
|
|
||||||
|
theEmptyList = (msg) ->
|
||||||
|
switch msg
|
||||||
|
when sType then sNull
|
||||||
|
when sBoolify then (x, y) -> x
|
||||||
|
|
||||||
|
createBoolean = (value) ->
|
||||||
|
combinator = if value then ((x, y) -> x) else ((x, y) -> y)
|
||||||
|
(msg) ->
|
||||||
|
switch msg
|
||||||
|
when sType then sBoolean
|
||||||
|
when sBoolify then combinator
|
||||||
|
|
||||||
|
createSymbol = (value) ->
|
||||||
|
(msg) ->
|
||||||
|
switch msg
|
||||||
|
when sType then sSymbol
|
||||||
|
when sName then value
|
||||||
|
when sBoolify then (x, y) -> x
|
||||||
|
|
||||||
|
createNumber = (value) ->
|
||||||
|
(msg) ->
|
||||||
|
switch msg
|
||||||
|
when sType then sNumber
|
||||||
|
when sValue then value
|
||||||
|
when sBoolify then (x, y) -> x
|
||||||
|
|
||||||
|
createString = (value) ->
|
||||||
|
(msg) ->
|
||||||
|
switch msg
|
||||||
|
when sType then sString
|
||||||
|
when sValue then value
|
||||||
|
when sBoolify then (x, y) -> x
|
||||||
|
|
||||||
|
createFunction = (tag, behavior) ->
|
||||||
|
(msg) ->
|
||||||
|
switch msg
|
||||||
|
when sType then sFunction
|
||||||
|
when sBoolify then (x, y) -> x
|
||||||
|
when sTag then tag
|
||||||
|
when sBehavior then behavior
|
||||||
|
|
||||||
|
# I'm not sure I get the difference between allocate and update.
|
||||||
|
# Update appears to have the power to append to the memory list
|
||||||
|
# without updating highLocation. If I'm reading this correct, then
|
||||||
|
# what we're actually looking at is a simulation of a memory
|
||||||
|
# subsystem, with expandStore/newLocation/allocate taking on the duty
|
||||||
|
# of "managing" our stack, and update actually just doing the managing
|
||||||
|
# the stack, and letting the garbage collector do its thing when a
|
||||||
|
# pointer to memory function goes out of scope. In short: the
|
||||||
|
# allocate collection of functions is "going through the motions" of
|
||||||
|
# managing memory; had this been a real memory manager, you'd have
|
||||||
|
# a lot more work to do.
|
||||||
|
|
||||||
|
allocate = (num, mem, q) ->
|
||||||
|
if (num > 0)
|
||||||
|
do ->
|
||||||
|
addr = newLocation mem
|
||||||
|
allocate (num - 1), (expandStore addr, mem), (addrs, mem2) ->
|
||||||
|
q (cons addr, addrs), mem2
|
||||||
|
else
|
||||||
|
q cons(), mem
|
||||||
|
|
||||||
|
allocateList = (values, mem, q) ->
|
||||||
|
consify = (values, q) ->
|
||||||
|
if (pairp values)
|
||||||
|
consify (cdr values), (value, mem2) ->
|
||||||
|
allocatePair (car values), value, mem2, q
|
||||||
|
else
|
||||||
|
q theEmptyList, mem
|
||||||
|
consify values, q
|
||||||
|
|
||||||
|
allocatePair = (addr, d, mem, q) ->
|
||||||
|
allocate 2, mem, (addrs, mem2) ->
|
||||||
|
q (createPair (car addrs), (cadr addrs)), (update (update mem2, (car addrs), addr), (cadr addrs), d)
|
||||||
|
|
||||||
|
createPair = (a, d) ->
|
||||||
|
(msg) ->
|
||||||
|
switch msg
|
||||||
|
when sType then sPair
|
||||||
|
when sBoolify then (x, y) -> x
|
||||||
|
when sSetCar then (mem, val) -> update mem, a, val
|
||||||
|
when sSetCdr then (mem, val) -> update mem, d, val
|
||||||
|
when sCar then a
|
||||||
|
when sCdr then d
|
||||||
|
|
||||||
|
env_global = env_init
|
||||||
|
mem_global = mem_init
|
||||||
|
|
||||||
|
# The name is pushed onto the global environment, with a corresponding
|
||||||
|
# address. The address is pushed onto the current memory, with the
|
||||||
|
# corresponding boxed value.
|
||||||
|
|
||||||
|
defInitial = (name, value) ->
|
||||||
|
if typeof name == 'string'
|
||||||
|
name = new Symbol name
|
||||||
|
allocate 1, mem_global, (addrs, mem2) ->
|
||||||
|
env_global = update env_global, name, (car addrs)
|
||||||
|
mem_global = update mem2, (car addrs), value
|
||||||
|
|
||||||
|
defPrimitive = (name, arity, value) ->
|
||||||
|
defInitial name, allocate 1, mem_global, (addrs, mem2) ->
|
||||||
|
mem_global = expandStore (car addrs), mem2
|
||||||
|
createFunction (car addrs), (values, mem, kont) ->
|
||||||
|
if (eq arity, (length values))
|
||||||
|
value values, mem, kont
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Wrong arity for #{name}"
|
||||||
|
|
||||||
|
# ___ _ _ _ _ _ _ _
|
||||||
|
# |_ _|_ _ (_) |_(_) (_)_____ _| |_(_)___ _ _
|
||||||
|
# | || ' \| | _| | | |_ / _` | _| / _ \ ' \
|
||||||
|
# |___|_||_|_|\__|_|_|_/__\__,_|\__|_\___/_||_|
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
defInitial "#t", createBoolean true
|
||||||
|
defInitial "#f", createBoolean false
|
||||||
|
defInitial "nil", null
|
||||||
|
|
||||||
|
defPrimitive "<=", 2, (values, mem, kont) ->
|
||||||
|
if (eq ((car values) sType), sNumber) and (eq ((cadr values) sName), sNumber)
|
||||||
|
kont (createBoolean (((car values) sValue) <= ((cadr values) sValue))), mem
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Comparison requires numbers"
|
||||||
|
|
||||||
|
defPrimitive "<", 2, (values, mem, kont) ->
|
||||||
|
if (eq ((car values) sType), sNumber) and (eq ((cadr values) sName), sNumber)
|
||||||
|
kont (createBoolean (((car values) sValue) < ((cadr values) sValue))), mem
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Comparison requires numbers"
|
||||||
|
|
||||||
|
defPrimitive ">=", 2, (values, mem, kont) ->
|
||||||
|
if (eq ((car values) sType), sNumber) and (eq ((cadr values) sName), sNumber)
|
||||||
|
kont (createBoolean (((car values) sValue) >= ((cadr values) sValue))), mem
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Comparison requires numbers"
|
||||||
|
|
||||||
|
defPrimitive ">", 2, (values, mem, kont) ->
|
||||||
|
if (eq ((car values) sType), sNumber) and (eq ((cadr values) sName), sNumber)
|
||||||
|
kont (createBoolean (((car values) sValue) > ((cadr values) sValue))), mem
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Comparison requires numbers"
|
||||||
|
|
||||||
|
defPrimitive "=", 2, (values, mem, kont) ->
|
||||||
|
if (eq ((car values) sType), sNumber) and (eq ((cadr values) sType), sNumber)
|
||||||
|
kont (createBoolean (((car values) sValue) == ((cadr values) sValue))), mem
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Comparison requires numbers"
|
||||||
|
|
||||||
|
defPrimitive "*", 2, (values, mem, kont) ->
|
||||||
|
if (eq ((car values) sType), sNumber) and (eq ((cadr values) sName), sNumber)
|
||||||
|
kont (createNumber (((car values) sValue) * ((cadr values) sValue))), mem
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Multiplication requires numbers"
|
||||||
|
|
||||||
|
defPrimitive "+", 2, (values, mem, kont) ->
|
||||||
|
if (eq ((car values) sType), sNumber) and (eq ((cadr values) sType), sNumber)
|
||||||
|
kont (createNumber (((car values) sValue) + ((cadr values) sValue))), mem
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Addition requires numbers"
|
||||||
|
|
||||||
|
defPrimitive "/", 2, (values, mem, kont) ->
|
||||||
|
if (eq ((car values) sType), sNumber) and (eq ((cadr values) sName), sNumber)
|
||||||
|
kont (createNumber (((car values) sValue) / ((cadr values) sValue))), mem
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Division requires numbers"
|
||||||
|
|
||||||
|
defPrimitive "*", 2, (values, mem, kont) ->
|
||||||
|
if (eq ((car values) sType), sNumber) and (eq ((cadr values) sName), sNumber)
|
||||||
|
kont (createNumber (((car values) sValue) - ((cadr values) sValue))), mem
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Subtraction requires numbers"
|
||||||
|
|
||||||
|
defPrimitive "cons", 2, (values, mem, kont) ->
|
||||||
|
allocatePair (car values), (cadr values), mem, kont
|
||||||
|
|
||||||
|
defPrimitive "car", 1, (values, mem, kont) ->
|
||||||
|
if (eq ((car values) sType) sPair)
|
||||||
|
kont (mem ((car values) sCar)), mem
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Not a pair"
|
||||||
|
|
||||||
|
defPrimitive "cdr", 1, (values, mem, kont) ->
|
||||||
|
if (eq ((car values) sType) sPair)
|
||||||
|
kont (mem ((car values) sCdr)), mem
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Not a pair"
|
||||||
|
|
||||||
|
defPrimitive "setcdr", 2, (values, mem, kont) ->
|
||||||
|
if (eq ((car values) sType) sPair)
|
||||||
|
pair = (car values)
|
||||||
|
kont pair, ((pair sSetCdr) mem, (cadr values))
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Not a pair"
|
||||||
|
|
||||||
|
defPrimitive "setcar", 2, (values, mem, kont) ->
|
||||||
|
if (eq ((car values) sType) sPair)
|
||||||
|
pair = (car values)
|
||||||
|
kont pair, ((pair sSetCar) mem, (cadr values))
|
||||||
|
else
|
||||||
|
throw new LispInterpreterError "Not a pair"
|
||||||
|
|
||||||
|
defPrimitive "eq?", 2, (values, mem, kont) ->
|
||||||
|
kont createBoolean (
|
||||||
|
if (eq ((car values) sType), ((cadr values) sType))
|
||||||
|
switch ((car values) sType)
|
||||||
|
when sBoolean
|
||||||
|
((car values) sBoolify) (((cadr values) sBoolify) true, false), (((cadr values) sBoolify) false, true)
|
||||||
|
when sSymbol
|
||||||
|
eq ((car values) sName), ((cadr values) sName)
|
||||||
|
when sPair
|
||||||
|
(((car values) sCar) == ((cadr values) sCar) and
|
||||||
|
((car values) sCdr) == ((cadr values) sCdr))
|
||||||
|
when sFunction
|
||||||
|
((car values) sTag) == ((cadr values) sTag)
|
||||||
|
else false
|
||||||
|
else false)
|
||||||
|
|
||||||
|
defPrimitive "eqv?", 2, (values, mem, kont) ->
|
||||||
|
kont createBoolean (
|
||||||
|
if (eq ((car values) sType), ((cadr values) sType))
|
||||||
|
switch ((car values) sType)
|
||||||
|
when sBoolean
|
||||||
|
((car values) sBoolify) (((cadr values) sBoolify) true, false), (((cadr values) sBoolify) false, true)
|
||||||
|
when sSymbol
|
||||||
|
eq ((car values) sName), ((cadr values) sName)
|
||||||
|
when sNumber
|
||||||
|
((car values) sValue) == ((cadr values) sValue)
|
||||||
|
when sPair
|
||||||
|
(((car values) sCar) == ((cadr values) sCar) and
|
||||||
|
((car values) sCdr) == ((cadr values) sCdr))
|
||||||
|
when sFunction
|
||||||
|
((car values) sTag) == ((cadr values) sTag)
|
||||||
|
else false
|
||||||
|
else false)
|
||||||
|
|
||||||
|
module.exports = (ast, kont) ->
|
||||||
|
evaluate ast, env_global, mem_global, (value, mem) ->
|
||||||
|
kont (transcodeBack value, mem)
|
|
@ -0,0 +1,204 @@
|
||||||
|
{car, cdr, cons, nil, nilp, pairp, vectorToList, list} = require 'cons-lists/lists'
|
||||||
|
{inspect} = require "util"
|
||||||
|
{Node, Comment, Symbol} = require "../chapter1/reader_types"
|
||||||
|
|
||||||
|
NEWLINES = ["\n", "\r", "\x0B", "\x0C"]
|
||||||
|
WHITESPACE = [" ", "\t"].concat(NEWLINES)
|
||||||
|
|
||||||
|
EOF = new (class Eof)()
|
||||||
|
EOO = new (class Eoo)()
|
||||||
|
|
||||||
|
class Source
|
||||||
|
constructor: (@inStream) ->
|
||||||
|
@index = 0
|
||||||
|
@max = @inStream.length - 1
|
||||||
|
@line = 0
|
||||||
|
@column = 0
|
||||||
|
|
||||||
|
peek: -> @inStream[@index]
|
||||||
|
|
||||||
|
position: -> [@line, @column]
|
||||||
|
|
||||||
|
next: ->
|
||||||
|
c = @peek()
|
||||||
|
return EOF if @done()
|
||||||
|
@index++
|
||||||
|
[@line, @column] = if @peek() in NEWLINES then [@line + 1, 0] else [@line, @column + 1]
|
||||||
|
c
|
||||||
|
|
||||||
|
done: -> @index > @max
|
||||||
|
|
||||||
|
# IO -> IO
|
||||||
|
skipWS = (inStream) ->
|
||||||
|
while inStream.peek() in WHITESPACE then inStream.next()
|
||||||
|
|
||||||
|
# msg -> (IO -> Node => Error)
|
||||||
|
handleError = (message) ->
|
||||||
|
(line, column) -> new Node('error', message, line, column)
|
||||||
|
|
||||||
|
# IO -> Node => Comment
|
||||||
|
readComment = (inStream) ->
|
||||||
|
[line, column] = inStream.position()
|
||||||
|
r = (while inStream.peek() != "\n" and not inStream.done()
|
||||||
|
inStream.next()).join("")
|
||||||
|
if not inStream.done()
|
||||||
|
inStream.next()
|
||||||
|
new Node 'comment', (new Comment r), line, column
|
||||||
|
|
||||||
|
# IO -> (Node => Literal => String) | Error
|
||||||
|
readString = (inStream) ->
|
||||||
|
[line, column] = inStream.position()
|
||||||
|
inStream.next()
|
||||||
|
string = until inStream.peek() == '"' or inStream.done()
|
||||||
|
if inStream.peek() == '\\'
|
||||||
|
inStream.next()
|
||||||
|
inStream.next()
|
||||||
|
if inStream.done()
|
||||||
|
return handleError("end of file seen before end of string.")(line, column)
|
||||||
|
inStream.next()
|
||||||
|
new Node 'string', (string.join ''), line, column
|
||||||
|
|
||||||
|
# (String) -> (Node => Literal => Number) | Nothing
|
||||||
|
readMaybeNumber = (symbol) ->
|
||||||
|
if symbol[0] == '+'
|
||||||
|
return readMaybeNumber symbol.substr(1)
|
||||||
|
if symbol[0] == '-'
|
||||||
|
ret = readMaybeNumber symbol.substr(1)
|
||||||
|
return if ret? then -1 * ret else undefined
|
||||||
|
if symbol.search(/^0x[0-9a-fA-F]+$/) > -1
|
||||||
|
return parseInt(symbol, 16)
|
||||||
|
if symbol.search(/^0[0-9a-fA-F]+$/) > -1
|
||||||
|
return parseInt(symbol, 8)
|
||||||
|
if symbol.search(/^[0-9]+$/) > -1
|
||||||
|
return parseInt(symbol, 10)
|
||||||
|
if symbol.search(/^nil$/) > -1
|
||||||
|
return nil
|
||||||
|
undefined
|
||||||
|
|
||||||
|
# (IO, macros) -> (IO, Node => Number | Symbol) | Error
|
||||||
|
readSymbol = (inStream, tableKeys) ->
|
||||||
|
[line, column] = inStream.position()
|
||||||
|
symbol = (until (inStream.done() or inStream.peek() in tableKeys or inStream.peek() in WHITESPACE)
|
||||||
|
inStream.next()).join ''
|
||||||
|
number = readMaybeNumber symbol
|
||||||
|
if number?
|
||||||
|
return new Node 'number', number, line, column
|
||||||
|
new Node 'symbol', (new Symbol symbol), line, column
|
||||||
|
|
||||||
|
|
||||||
|
# (Delim, TypeName) -> IO -> (IO, node) | Error
|
||||||
|
makeReadPair = (delim, type) ->
|
||||||
|
# IO -> (IO, Node) | Error
|
||||||
|
(inStream) ->
|
||||||
|
inStream.next()
|
||||||
|
skipWS inStream
|
||||||
|
[line, column] = inStream.position()
|
||||||
|
if inStream.peek() == delim
|
||||||
|
inStream.next()
|
||||||
|
return new Node type, nil, line, column
|
||||||
|
|
||||||
|
# IO -> (IO, Node) | Error
|
||||||
|
dotted = false
|
||||||
|
readEachPair = (inStream) ->
|
||||||
|
[line, column] = inStream.position()
|
||||||
|
obj = read inStream, true, null, true
|
||||||
|
if inStream.peek() == delim
|
||||||
|
if dotted then return obj
|
||||||
|
return cons obj, nil
|
||||||
|
if inStream.done() then return handleError("Unexpected end of input")(line, column)
|
||||||
|
if dotted then return handleError("More than one symbol after dot")
|
||||||
|
return obj if obj.type == 'error'
|
||||||
|
if obj.type == 'symbol' and obj.value == '.'
|
||||||
|
dotted = true
|
||||||
|
return readEachPair inStream
|
||||||
|
cons obj, readEachPair inStream
|
||||||
|
|
||||||
|
ret = new Node type, readEachPair(inStream), line, column
|
||||||
|
inStream.next()
|
||||||
|
ret
|
||||||
|
|
||||||
|
# Type -> (IO -> (IO, Node))
|
||||||
|
prefixReader = (type) ->
|
||||||
|
# IO -> (IO, Node)
|
||||||
|
(inStream) ->
|
||||||
|
[line, column] = inStream.position()
|
||||||
|
inStream.next()
|
||||||
|
[line1, column1] = inStream.position()
|
||||||
|
obj = read inStream, true, null, true
|
||||||
|
return obj if obj.type == 'error'
|
||||||
|
new Node "list", cons((new Node("symbol", (new Symbol type), line1, column1)), cons(obj)), line, column
|
||||||
|
|
||||||
|
# I really wanted to make anything more complex than a list (like an
|
||||||
|
# object or a vector) something handled by a read macro. Maybe in a
|
||||||
|
# future revision I can vertically de-integrate these.
|
||||||
|
|
||||||
|
readMacros =
|
||||||
|
'"': readString
|
||||||
|
'(': makeReadPair ')', 'list'
|
||||||
|
')': handleError "Closing paren encountered"
|
||||||
|
'[': makeReadPair ']', 'vector'
|
||||||
|
']': handleError "Closing bracket encountered"
|
||||||
|
'{': makeReadPair('}', 'record', (res) ->
|
||||||
|
res.length % 2 == 0 and true or mkerr "record key without value")
|
||||||
|
'}': handleError "Closing curly without corresponding opening."
|
||||||
|
"`": prefixReader 'back-quote'
|
||||||
|
"'": prefixReader 'quote'
|
||||||
|
",": prefixReader 'unquote'
|
||||||
|
";": readComment
|
||||||
|
|
||||||
|
|
||||||
|
# Given a stream, reads from the stream until a single complete lisp
|
||||||
|
# object has been found and returns the object
|
||||||
|
|
||||||
|
# IO -> Form
|
||||||
|
read = (inStream, eofErrorP = false, eofError = EOF, recursiveP = false, inReadMacros = null, keepComments = false) ->
|
||||||
|
inStream = if inStream instanceof Source then inStream else new Source inStream
|
||||||
|
inReadMacros = if InReadMacros? then inReadMacros else readMacros
|
||||||
|
inReadMacroKeys = (i for i of inReadMacros)
|
||||||
|
|
||||||
|
c = inStream.peek()
|
||||||
|
|
||||||
|
# (IO, Char) -> (IO, Node) | Error
|
||||||
|
matcher = (inStream, c) ->
|
||||||
|
if inStream.done()
|
||||||
|
return if recursiveP then handleError('EOF while processing nested object')(inStream) else nil
|
||||||
|
if c in WHITESPACE
|
||||||
|
inStream.next()
|
||||||
|
return nil
|
||||||
|
if c == ';'
|
||||||
|
return readComment(inStream)
|
||||||
|
ret = if c in inReadMacroKeys then inReadMacros[c](inStream) else readSymbol(inStream, inReadMacroKeys)
|
||||||
|
skipWS inStream
|
||||||
|
ret
|
||||||
|
|
||||||
|
while true
|
||||||
|
form = matcher inStream, c
|
||||||
|
skip = (not nilp form) and (form.type == 'comment') and not keepComments
|
||||||
|
break if (not skip and not nilp form) or inStream.done()
|
||||||
|
c = inStream.peek()
|
||||||
|
null
|
||||||
|
form
|
||||||
|
|
||||||
|
# readForms assumes that the string provided contains zero or more
|
||||||
|
# forms. As such, it always returns a list of zero or more forms.
|
||||||
|
|
||||||
|
# IO -> (Form* | Error)
|
||||||
|
readForms = (inStream) ->
|
||||||
|
inStream = if inStream instanceof Source then inStream else new Source inStream
|
||||||
|
return nil if inStream.done()
|
||||||
|
|
||||||
|
# IO -> (FORM*, IO) | Error
|
||||||
|
[line, column] = inStream.position()
|
||||||
|
readEach = (inStream) ->
|
||||||
|
obj = read inStream, true, null, false
|
||||||
|
return nil if (nilp obj)
|
||||||
|
return obj if obj.type == 'error'
|
||||||
|
cons obj, readEach inStream
|
||||||
|
|
||||||
|
obj = readEach inStream
|
||||||
|
if obj.type == 'error' then obj else new Node "list", obj, line, column
|
||||||
|
|
||||||
|
exports.read = read
|
||||||
|
exports.readForms = readForms
|
||||||
|
exports.Node = Node
|
||||||
|
exports.Symbol = Symbol
|
|
@ -0,0 +1,374 @@
|
||||||
|
{listToString, listToVector, pairp, cons, car, cdr, caar, cddr, cdar,
|
||||||
|
cadr, caadr, cadar, caddr, nilp, nil, setcdr,
|
||||||
|
metacadr, setcar} = require "cons-lists/lists"
|
||||||
|
{map} = require "cons-lists/reduce"
|
||||||
|
{length} = require "cons-lists/reduce"
|
||||||
|
{Node, Comment, Symbol} = require '../chapter5/reader_types'
|
||||||
|
{inspect} = require 'util'
|
||||||
|
|
||||||
|
itap = (a) -> console.log inspect a, true, null, false; a
|
||||||
|
ftap = (a) -> console.log Function.prototype.toString.call(a); a
|
||||||
|
|
||||||
|
|
||||||
|
class Value
|
||||||
|
vpos = 0
|
||||||
|
constructor: (@v) ->
|
||||||
|
vpos = vpos + 1
|
||||||
|
|
||||||
|
inValue = (f) ->
|
||||||
|
new Value(f)
|
||||||
|
|
||||||
|
class LispInterpreterError extends Error
|
||||||
|
name: 'LispInterpreterError'
|
||||||
|
constructor: (@message) ->
|
||||||
|
|
||||||
|
the_false_value = (cons "false", "boolean")
|
||||||
|
|
||||||
|
eq = (id1, id2) ->
|
||||||
|
if id1 instanceof Symbol and id2 instanceof Symbol
|
||||||
|
return id1.name == id2.name
|
||||||
|
id1 == id2
|
||||||
|
|
||||||
|
cadddr = metacadr('cadddr')
|
||||||
|
|
||||||
|
# Hack
|
||||||
|
gsym = (x) -> if (x instanceof Symbol) then x.name else x
|
||||||
|
|
||||||
|
consp = (e) ->
|
||||||
|
((pairp e) and (typeof (car e) == 'number') and
|
||||||
|
((car e) > 0) and (pairp cdr e) and (typeof (cadr e) == 'number') and
|
||||||
|
((cadr e) > 0) and (nilp cddr e))
|
||||||
|
|
||||||
|
convert = (exp, store) ->
|
||||||
|
conv = (e) ->
|
||||||
|
if consp e
|
||||||
|
cons (conv (store (car e)).v), (conv (store (cadr e)).v)
|
||||||
|
else
|
||||||
|
if symbolp e then e.name
|
||||||
|
else if stringp e then '"' + e | '"'
|
||||||
|
else e
|
||||||
|
conv exp.v
|
||||||
|
|
||||||
|
# 5.2.4
|
||||||
|
# f[y → z] = λx . if y = x then z else f(x) endif
|
||||||
|
#
|
||||||
|
# Accepts a parent function, and ID and a value. Returns a function
|
||||||
|
# that takes a request ID. If the request ID equals the ID above,
|
||||||
|
# return the value, else call the parent function with the request
|
||||||
|
# ID.
|
||||||
|
#
|
||||||
|
# Calls allocate
|
||||||
|
|
||||||
|
extend = (next, id, value) ->
|
||||||
|
(x) -> if (eq x, id) then value else (next x)
|
||||||
|
|
||||||
|
# f[y* → z*] = if #y>0 then f[y*†1 → z*†1][y*↓1 → z*↓1] else f endif
|
||||||
|
#
|
||||||
|
# Helper. Builds a stack of extend() functions, at tail of which it
|
||||||
|
# appends the parent function.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
lextends = (fn, ids, values) ->
|
||||||
|
if (pairp ids)
|
||||||
|
extend (lextends fn, (cdr ids), (cdr values)), (car ids), (car values)
|
||||||
|
else
|
||||||
|
fn
|
||||||
|
|
||||||
|
translate = (exp, store, qont) ->
|
||||||
|
if (pairp exp)
|
||||||
|
translate (car exp), store, (val1, store1) ->
|
||||||
|
translate (cdr exp), store1, (val2, store2) ->
|
||||||
|
allocate store2, 2, (store, addrs) ->
|
||||||
|
qont (inValue addrs), (extend (extend store, (car addrs), val1), (cadr addrs), val2)
|
||||||
|
else
|
||||||
|
qont (inValue exp), store
|
||||||
|
|
||||||
|
# Allocate is a function that takes a store, a number of addresses to
|
||||||
|
# allocate within that store, and a continuation; at the end, it calls
|
||||||
|
# the continuation with the store object and the new addresses.
|
||||||
|
|
||||||
|
allocate = (->
|
||||||
|
loc = 0
|
||||||
|
(store, num, qont) ->
|
||||||
|
aloop = (n, a) ->
|
||||||
|
if (n > 0)
|
||||||
|
loc = loc - 1
|
||||||
|
aloop (n - 1), (cons loc, a)
|
||||||
|
else
|
||||||
|
qont store, a
|
||||||
|
aloop(num, cons()))()
|
||||||
|
|
||||||
|
|
||||||
|
listp = (cell) -> cell.__type == 'list'
|
||||||
|
atomp = (cell) -> not (cell.__type?) or (not cell.__type == 'list')
|
||||||
|
symbolp = (cell) -> cell instanceof Symbol
|
||||||
|
commentp = (cell) -> typeof cell == 'string' and cell.length > 0 and cell[0] == ";"
|
||||||
|
numberp = (cell) -> typeof cell == 'number'
|
||||||
|
stringp = (cell) -> typeof cell == 'string' and cell.length > 0 and cell[0] == "\""
|
||||||
|
boolp = (cell) -> typeof cell == 'boolean'
|
||||||
|
nullp = (cell) -> cell == null
|
||||||
|
vectorp = (cell) -> (not straight_evaluation.listp cell) and toString.call(cell) == '[object Array]'
|
||||||
|
recordp = (cell) -> (not cell._prototype?) and toSTring.call(cell) == '[object Object]'
|
||||||
|
objectp = (cell) -> (cell._prototype?) and toString.call(cell) == '[object Object]'
|
||||||
|
nilp = (cell) -> nilp(cell)
|
||||||
|
nvalu = (cell) -> cell
|
||||||
|
mksymbols = (cell) -> cell
|
||||||
|
|
||||||
|
sBehavior = new Symbol 'behavior'
|
||||||
|
sBoolean = new Symbol 'boolean'
|
||||||
|
sBoolify = new Symbol 'boolify'
|
||||||
|
sFunction = new Symbol 'function'
|
||||||
|
sSymbol = new Symbol 'symbol'
|
||||||
|
sString = new Symbol 'string'
|
||||||
|
sQuote = new Symbol 'quote'
|
||||||
|
sLambda = new Symbol 'lambda'
|
||||||
|
sIf = new Symbol 'if'
|
||||||
|
sValue = new Symbol 'value'
|
||||||
|
sChars = new Symbol 'chars'
|
||||||
|
sBegin = new Symbol 'begin'
|
||||||
|
sName = new Symbol 'name'
|
||||||
|
sNumber = new Symbol 'number'
|
||||||
|
sNull = new Symbol 'null'
|
||||||
|
sTag = new Symbol 'tag'
|
||||||
|
sSet = new Symbol 'set'
|
||||||
|
sType = new Symbol 'type'
|
||||||
|
sValue = new Symbol 'value'
|
||||||
|
sPair = new Symbol 'pair'
|
||||||
|
sCar = new Symbol 'car'
|
||||||
|
sCdr = new Symbol 'cdr'
|
||||||
|
sSetCar = new Symbol 'setcar'
|
||||||
|
sSetCdr = new Symbol 'setcdr'
|
||||||
|
|
||||||
|
ValueToFunction = (e) ->
|
||||||
|
c = e.v
|
||||||
|
if (typeof c == 'function') then c else throw new LispInterpreterError("Not a function: " + Object.toString(c))
|
||||||
|
|
||||||
|
ValueToPair = (e) ->
|
||||||
|
c = e.v
|
||||||
|
if pairp c then c else throw new LispInterpreterError("Not a pair: " + Object.toString(c))
|
||||||
|
|
||||||
|
ValueToNumber = (e) ->
|
||||||
|
c = parseInt(e.v, 10)
|
||||||
|
if (typeof c == 'number') then c else throw new LispInterpreterError("Not a number: " + Object.toString(c))
|
||||||
|
|
||||||
|
ValueToPrimitive = (e) ->
|
||||||
|
return e.v
|
||||||
|
|
||||||
|
store_init = (a) -> throw new LispInterpreterError "No such address: #{a}"
|
||||||
|
env_init = (a) -> throw new LispInterpreterError "No such variable: #{a}"
|
||||||
|
|
||||||
|
class Interpreter
|
||||||
|
constructor: ->
|
||||||
|
arity_check = (name, arity, fn) =>
|
||||||
|
(values, kont, store) =>
|
||||||
|
if not eq (length values), arity
|
||||||
|
throw new LispInterpreterError "Incorrect Arity for #{name}"
|
||||||
|
fn(values, kont, store)
|
||||||
|
|
||||||
|
@definitial "cons", inValue arity_check "cons", 2, (values, kont, store) =>
|
||||||
|
allocate store, 2, (store, addrs) =>
|
||||||
|
kont (inValue (cons (car addrs), (cadr addrs))), (lextends store, addrs, values)
|
||||||
|
|
||||||
|
@definitial "car", inValue arity_check "car", 1, (values, kont, store) =>
|
||||||
|
kont (store car @valueToPair (car values)), store
|
||||||
|
|
||||||
|
@definitial "cdr", inValue arity_check "car", 1, (values, kont, store) =>
|
||||||
|
kont (store cadr @valueToPair (car values)), store
|
||||||
|
|
||||||
|
@defprimitive "pair?", ((v) -> inValue (consp v.v)), 1
|
||||||
|
@defprimitive "eq?", ((v1, v2) -> inValue (eq v1.v, v2.v)), 2
|
||||||
|
@defprimitive "symbol?", ((v) -> inValue (symbolp v.v)), 1
|
||||||
|
|
||||||
|
@definitial "set-car!", inValue arity_check, "set-car!", 2, (values, kont, store) ->
|
||||||
|
kont (car values), (extend store, (car (ValueToPair (car values))), (cadr values))
|
||||||
|
|
||||||
|
@definitial "set-cdr!", inValue arity_check, "set-cdr!", 2, (values, kont, store) ->
|
||||||
|
kont (car values), (extend store, (cadr (ValueToPair (car values))), (cadr values))
|
||||||
|
|
||||||
|
@defarithmetic "+", ((x, y) -> x + y), 2
|
||||||
|
@defarithmetic "-", ((x, y) -> x - y), 2
|
||||||
|
@defarithmetic "*", ((x, y) -> x * y), 2
|
||||||
|
@defarithmetic "/", ((x, y) -> x / y), 2
|
||||||
|
@defarithmetic "<", ((x, y) -> x < y), 2
|
||||||
|
@defarithmetic ">", ((x, y) -> x > y), 2
|
||||||
|
@defarithmetic "=", ((x, y) -> x == y), 2
|
||||||
|
@defarithmetic "<=", ((x, y) -> x <= y), 2
|
||||||
|
@defarithmetic ">=", ((x, y) -> x >= y), 2
|
||||||
|
@defarithmetic "%", ((x, y) -> x % y), 2
|
||||||
|
|
||||||
|
@definitial "apply", arity_check "apply", 2, inValue (values, kont, store) ->
|
||||||
|
flat = (v) ->
|
||||||
|
if pairp v.v
|
||||||
|
cons (store (car (ValueToPair v))), (flat (store (cadr (ValueToPair v))))
|
||||||
|
else
|
||||||
|
cons()
|
||||||
|
|
||||||
|
collect = (values) ->
|
||||||
|
if nullp cdr values
|
||||||
|
flat car values
|
||||||
|
else
|
||||||
|
cons (car values), (collect cdr values)
|
||||||
|
|
||||||
|
(ValueToFunction (car values)) (collect (cdr values)), kont, store
|
||||||
|
|
||||||
|
@definitial '#t', (inValue true)
|
||||||
|
@definitial '#f', (inValue false)
|
||||||
|
@definitial 'nil', (inValue cons())
|
||||||
|
|
||||||
|
@definitial "x", inValue (new Object(null))
|
||||||
|
@definitial "y", inValue (new Object(null))
|
||||||
|
@definitial "z", inValue (new Object(null))
|
||||||
|
@definitial "a", inValue (new Object(null))
|
||||||
|
@definitial "b", inValue (new Object(null))
|
||||||
|
@definitial "c", inValue (new Object(null))
|
||||||
|
@definitial "foo", inValue (new Object(null))
|
||||||
|
@definitial "bar", inValue (new Object(null))
|
||||||
|
@definitial "hux", inValue (new Object(null))
|
||||||
|
@definitial "fib", inValue (new Object(null))
|
||||||
|
@definitial "fact", inValue (new Object(null))
|
||||||
|
@definitial "visit", inValue (new Object(null))
|
||||||
|
@definitial "length", inValue (new Object(null))
|
||||||
|
@definitial "filter", inValue (new Object(null))
|
||||||
|
@definitial "primes", inValue (new Object(null))
|
||||||
|
|
||||||
|
meaning: (e) ->
|
||||||
|
meaningTable =
|
||||||
|
"quote": ((e) => @meaningQuotation (cadr e))
|
||||||
|
'lambda': ((e) => @meaningAbstraction (cadr e), (cddr e))
|
||||||
|
'if': ((e) => @meaningAlternative (cadr e), (caddr e), (cadddr e))
|
||||||
|
'begin': ((e) => @meaningSequence (cdr e))
|
||||||
|
'set!': ((e) => @meaningAssignment (cadr e), (caddr e))
|
||||||
|
|
||||||
|
if (atomp e)
|
||||||
|
return if (symbolp e) then (@meaningReference gsym(e)) else (@meaningQuotation e)
|
||||||
|
n = gsym(car e)
|
||||||
|
if meaningTable[n]?
|
||||||
|
meaningTable[n](e)
|
||||||
|
else
|
||||||
|
@meaningApplication (car e), (cdr e)
|
||||||
|
|
||||||
|
meaningSequence: (exps) =>
|
||||||
|
(env, kont, store) =>
|
||||||
|
(@meaningsSequence exps) env, kont, store
|
||||||
|
|
||||||
|
meaningsMultipleSequence: (exp, exps) ->
|
||||||
|
(env, kont, store) =>
|
||||||
|
hkont = (values, store1) =>
|
||||||
|
(@meaningsSequence exps) env, kont, store1
|
||||||
|
(@meaning exp) env, hkont, store
|
||||||
|
|
||||||
|
meaningsSingleSequence: (exp) ->
|
||||||
|
(env, kont, store) =>
|
||||||
|
(@meaning exp) env, kont, store
|
||||||
|
|
||||||
|
meaningsSequence: (exps) ->
|
||||||
|
if not (pairp exps)
|
||||||
|
throw new LispInterpreterError("Illegal Syntax")
|
||||||
|
if pairp cdr exps
|
||||||
|
@meaningsMultipleSequence (car exps), (cdr exps)
|
||||||
|
else
|
||||||
|
@meaningsSingleSequence (car exps)
|
||||||
|
|
||||||
|
meaningQuotation: (val) ->
|
||||||
|
(env, kont, store) ->
|
||||||
|
(translate val, store, kont)
|
||||||
|
|
||||||
|
meaningReference: (name) ->
|
||||||
|
(env, kont, store) ->
|
||||||
|
kont (store (env name)), store
|
||||||
|
|
||||||
|
# Extensional alternative
|
||||||
|
|
||||||
|
meaningAlternative: (exp1, exp2, exp3) ->
|
||||||
|
boolify = (value) ->
|
||||||
|
if (eq? value (inValue false)) then ((x, y) -> y) else ((x, y) -> x)
|
||||||
|
|
||||||
|
ef = (val, val1, val2) ->
|
||||||
|
val val1, val2
|
||||||
|
|
||||||
|
(env, kont, store) =>
|
||||||
|
hkont = (val, store1) =>
|
||||||
|
ef (boolify val), ((@meaning exp2) env, kont, store1), ((@meaning exp3) env, kont, store1)
|
||||||
|
(@meaning exp1)(env, hkont, store)
|
||||||
|
|
||||||
|
# Assignment
|
||||||
|
|
||||||
|
meaningAssignment: (name, exp) ->
|
||||||
|
name = if name instanceof Symbol then name.name else name
|
||||||
|
console.log(name)
|
||||||
|
(env, kont, store) =>
|
||||||
|
hkont = (val, store1) ->
|
||||||
|
kont val, (extend store1, (env name), val)
|
||||||
|
(@meaning exp) env, hkont, store
|
||||||
|
|
||||||
|
# Abstraction (keeps a lambda)
|
||||||
|
|
||||||
|
meaningAbstraction: (names, exps) ->
|
||||||
|
(env, kont, store) =>
|
||||||
|
funcrep = (vals, kont1, store1) =>
|
||||||
|
if not (eq (length vals), (length names))
|
||||||
|
throw new LispInterpreterError("Incorrect Arity.")
|
||||||
|
argnamestostore = (store2, addrs) =>
|
||||||
|
(@meaningsSequence exps) (lextends env, names, addrs), kont1, (lextends store2, addrs, vals)
|
||||||
|
allocate store1, (length names), argnamestostore
|
||||||
|
kont (inValue funcrep), store
|
||||||
|
|
||||||
|
meaningVariable: (name) ->
|
||||||
|
(m) ->
|
||||||
|
(vals, env, kont, store) ->
|
||||||
|
allocate store, 1, (store, addrs) ->
|
||||||
|
addr = (car addrs)
|
||||||
|
m (cdr vals), (extend env, names, addr), kont, (extend store, addr, (car vals))
|
||||||
|
|
||||||
|
meaningApplication: (exp, exps) ->
|
||||||
|
(env, kont, store) =>
|
||||||
|
hkont = (func, store1) =>
|
||||||
|
kont2 = (values, store2) ->
|
||||||
|
(ValueToFunction func) values, kont, store2
|
||||||
|
(@meanings exps) env, kont2, store1
|
||||||
|
(@meaning exp) env, hkont, store
|
||||||
|
|
||||||
|
meanings: (exps) =>
|
||||||
|
meaningSomeArguments = (exp, exps) =>
|
||||||
|
(env, kont, store) =>
|
||||||
|
hkont = (value, store1) =>
|
||||||
|
hkont2 = (values, store2) ->
|
||||||
|
kont (cons value, values), store2
|
||||||
|
(@meanings exps) env, hkont2, store1
|
||||||
|
(@meaning exp) env, hkont, store
|
||||||
|
|
||||||
|
meaningNoArguments = ->
|
||||||
|
(env, kont, store) ->
|
||||||
|
kont (cons()), store
|
||||||
|
|
||||||
|
if pairp exps
|
||||||
|
meaningSomeArguments (car exps), (cdr exps)
|
||||||
|
else
|
||||||
|
meaningNoArguments()
|
||||||
|
|
||||||
|
definitial: (name, value) ->
|
||||||
|
allocate store_init, 1, (store, addrs) ->
|
||||||
|
env_init = extend env_init, name, (car addrs)
|
||||||
|
store_init = extend store, (car addrs), value
|
||||||
|
name
|
||||||
|
|
||||||
|
defprimitive: (name, value, arity) ->
|
||||||
|
callable = (values, kont, store) =>
|
||||||
|
if not eq arity, (length values)
|
||||||
|
throw new LispInterpreterError "Incorrect Arity for #{name}"
|
||||||
|
kont (value.apply(null, listToVector(values))), store
|
||||||
|
@definitial name, (inValue callable)
|
||||||
|
|
||||||
|
defarithmetic: (name, value, arity) ->
|
||||||
|
callable = (values, kont, store) ->
|
||||||
|
if not eq arity, (length values)
|
||||||
|
throw new LispInterpreterError "Incorrect Arity for #{name}"
|
||||||
|
kont (inValue (value.apply(null, listToVector(map values, ValueToNumber)))), store
|
||||||
|
@definitial name, (inValue callable)
|
||||||
|
|
||||||
|
module.exports = (ast, kont) ->
|
||||||
|
interpreter = new Interpreter()
|
||||||
|
store_current = store_init
|
||||||
|
(interpreter.meaning ast)(env_init,
|
||||||
|
((value, store_final) -> kont (convert value, store_final)), store_current)
|
||||||
|
|
|
@ -0,0 +1,170 @@
|
||||||
|
{car, cdr, cons, nil, nilp, pairp, vectorToList, list} = require 'cons-lists/lists'
|
||||||
|
{inspect} = require "util"
|
||||||
|
{Comment, Symbol} = require "../chapter5/reader_types"
|
||||||
|
|
||||||
|
NEWLINES = ["\n", "\r", "\x0B", "\x0C"]
|
||||||
|
WHITESPACE = [" ", "\t"].concat(NEWLINES)
|
||||||
|
|
||||||
|
EOF = new (class Eof)()
|
||||||
|
EOO = new (class Eoo)()
|
||||||
|
|
||||||
|
class ReadError extends Error
|
||||||
|
name: 'LispInterpreterError'
|
||||||
|
constructor: (@message) ->
|
||||||
|
|
||||||
|
class Source
|
||||||
|
constructor: (@inStream) ->
|
||||||
|
@index = 0
|
||||||
|
@max = @inStream.length - 1
|
||||||
|
@line = 0
|
||||||
|
@column = 0
|
||||||
|
|
||||||
|
peek: -> @inStream[@index]
|
||||||
|
|
||||||
|
position: -> [@line, @column]
|
||||||
|
|
||||||
|
next: ->
|
||||||
|
c = @peek()
|
||||||
|
return EOF if @done()
|
||||||
|
@index++
|
||||||
|
[@line, @column] = if @peek() in NEWLINES then [@line + 1, 0] else [@line, @column + 1]
|
||||||
|
c
|
||||||
|
|
||||||
|
done: -> @index > @max
|
||||||
|
|
||||||
|
# IO -> IO
|
||||||
|
skipWS = (inStream) ->
|
||||||
|
while inStream.peek() in WHITESPACE then inStream.next()
|
||||||
|
|
||||||
|
readMaybeNumber = (symbol) ->
|
||||||
|
if symbol[0] == '+'
|
||||||
|
return readMaybeNumber symbol.substr(1)
|
||||||
|
if symbol[0] == '-'
|
||||||
|
ret = readMaybeNumber symbol.substr(1)
|
||||||
|
return if ret? then -1 * ret else undefined
|
||||||
|
if symbol.search(/^0x[0-9a-fA-F]+$/) > -1
|
||||||
|
return parseInt(symbol, 16)
|
||||||
|
if symbol.search(/^0[0-9a-fA-F]+$/) > -1
|
||||||
|
return parseInt(symbol, 8)
|
||||||
|
if symbol.search(/^[0-9]+$/) > -1
|
||||||
|
return parseInt(symbol, 10)
|
||||||
|
if symbol.search(/^nil$/) > -1
|
||||||
|
return nil
|
||||||
|
undefined
|
||||||
|
|
||||||
|
# (Delim, TypeName) -> IO -> (IO, Node) | Errorfor
|
||||||
|
makeReadPair = (delim, type) ->
|
||||||
|
# IO -> (IO, Node) | Error
|
||||||
|
(inStream) ->
|
||||||
|
inStream.next()
|
||||||
|
skipWS inStream
|
||||||
|
if inStream.peek() == delim
|
||||||
|
inStream.next() unless inStream.done()
|
||||||
|
return if type then cons((new Symbol type), nil) else nil
|
||||||
|
|
||||||
|
# IO -> (IO, Node) | Error
|
||||||
|
dotted = false
|
||||||
|
readEachPair = (inStream) =>
|
||||||
|
obj = @read inStream, true, null, true
|
||||||
|
if inStream.peek() == delim
|
||||||
|
if dotted then return obj
|
||||||
|
return cons obj, nil
|
||||||
|
return obj if obj instanceof ReadError
|
||||||
|
if inStream.done() then return new ReadError "Unexpected end of input"
|
||||||
|
if dotted then return new ReadError "More than one symbol after dot in list"
|
||||||
|
if @acc(obj) instanceof Symbol and @acc(obj).name == '.'
|
||||||
|
dotted = true
|
||||||
|
return readEachPair inStream
|
||||||
|
cons obj, readEachPair inStream
|
||||||
|
|
||||||
|
obj = readEachPair(inStream)
|
||||||
|
inStream.next()
|
||||||
|
if type then cons((new Symbol type), obj) else obj
|
||||||
|
|
||||||
|
# Type -> IO -> IO, Node
|
||||||
|
|
||||||
|
class Reader
|
||||||
|
prefixReader = (type) ->
|
||||||
|
# IO -> IO, Node
|
||||||
|
(inStream) ->
|
||||||
|
inStream.next()
|
||||||
|
obj = @read inStream, true, null, true
|
||||||
|
return obj if obj instanceof ReadError
|
||||||
|
list((new Symbol type), obj)
|
||||||
|
|
||||||
|
"acc": (obj) -> obj
|
||||||
|
|
||||||
|
"symbol": (inStream) ->
|
||||||
|
symbol = (until (inStream.done() or @[inStream.peek()]? or inStream.peek() in WHITESPACE)
|
||||||
|
inStream.next()).join ''
|
||||||
|
number = readMaybeNumber symbol
|
||||||
|
if number?
|
||||||
|
return number
|
||||||
|
new Symbol symbol
|
||||||
|
|
||||||
|
"read": (inStream, eofErrorP = false, eofError = EOF, recursiveP = false, keepComments = false) ->
|
||||||
|
inStream = if inStream instanceof Source then inStream else new Source inStream
|
||||||
|
|
||||||
|
c = inStream.peek()
|
||||||
|
|
||||||
|
# (IO, Char) -> (IO, Node) | Error
|
||||||
|
matcher = (inStream, c) =>
|
||||||
|
if inStream.done()
|
||||||
|
return if recursiveP then (new ReadError 'EOF while processing nested object') else nil
|
||||||
|
if c in WHITESPACE
|
||||||
|
inStream.next()
|
||||||
|
return nil
|
||||||
|
if c == ';'
|
||||||
|
return readComment(inStream)
|
||||||
|
ret = if @[c]? then @[c](inStream) else @symbol(inStream)
|
||||||
|
skipWS inStream
|
||||||
|
ret
|
||||||
|
|
||||||
|
while true
|
||||||
|
form = matcher inStream, c
|
||||||
|
skip = (not nilp form) and (form instanceof Comment) and not keepComments
|
||||||
|
break if (not skip and not nilp form) or inStream.done()
|
||||||
|
c = inStream.peek()
|
||||||
|
null
|
||||||
|
form
|
||||||
|
|
||||||
|
'(': makeReadPair ')', null
|
||||||
|
|
||||||
|
'[': makeReadPair ']', 'vector'
|
||||||
|
|
||||||
|
'{': makeReadPair('}', 'record', (res) ->
|
||||||
|
res.length % 2 == 0 and true or mkerr "record key without value")
|
||||||
|
|
||||||
|
'"': (inStream) ->
|
||||||
|
inStream.next()
|
||||||
|
s = until inStream.peek() == '"' or inStream.done()
|
||||||
|
if inStream.peek() == '\\'
|
||||||
|
inStream.next()
|
||||||
|
inStream.next()
|
||||||
|
return (new ReadError "end of file seen before end of string") if inStream.done()
|
||||||
|
inStream.next()
|
||||||
|
s.join ''
|
||||||
|
|
||||||
|
')': (inStream) -> new ReadError "Closing paren encountered"
|
||||||
|
|
||||||
|
']': (inStream) -> new ReadError "Closing bracket encountered"
|
||||||
|
|
||||||
|
'}': (inStream) -> new ReadError "Closing curly without corresponding opening."
|
||||||
|
|
||||||
|
"`": prefixReader 'back-quote'
|
||||||
|
|
||||||
|
"'": prefixReader 'quote'
|
||||||
|
|
||||||
|
",": prefixReader 'unquote'
|
||||||
|
|
||||||
|
";": (inStream) ->
|
||||||
|
r = (while inStream.peek() != "\n" and not inStream.done()
|
||||||
|
inStream.next()).join("")
|
||||||
|
inStream.next() if not inStream.done()
|
||||||
|
new Comment r
|
||||||
|
|
||||||
|
exports.Source = Source
|
||||||
|
exports.ReadError = ReadError
|
||||||
|
exports.Reader = Reader
|
||||||
|
reader = new Reader()
|
||||||
|
exports.read = -> reader.read.apply(reader, arguments)
|
|
@ -0,0 +1,39 @@
|
||||||
|
{car, cdr, cons, listp, nilp, nil,
|
||||||
|
list, pairp, listToString} = require 'cons-lists/lists'
|
||||||
|
|
||||||
|
{Symbol, Comment} = require './reader_types'
|
||||||
|
|
||||||
|
class Normalize
|
||||||
|
normalize: (form) ->
|
||||||
|
return nil if nilp form
|
||||||
|
|
||||||
|
if (pairp form)
|
||||||
|
if (car form) instanceof Symbol and (car form).name in ['vector', 'record']
|
||||||
|
@[(car form).name](cdr form)
|
||||||
|
else
|
||||||
|
@list form
|
||||||
|
else
|
||||||
|
form
|
||||||
|
|
||||||
|
list: (form) ->
|
||||||
|
handle = (form) =>
|
||||||
|
return nil if nilp form
|
||||||
|
if not pairp form
|
||||||
|
return @normalize form
|
||||||
|
cons (@normalize car form), (handle cdr form)
|
||||||
|
handle form
|
||||||
|
|
||||||
|
vector: (form) ->
|
||||||
|
until (nilp form) then p = @normalize(car form); form = cdr form; p
|
||||||
|
|
||||||
|
record: (form) ->
|
||||||
|
o = Object.create(null)
|
||||||
|
until (nilp form)
|
||||||
|
o[(@normalize car form)] = (@normalize car cdr form)
|
||||||
|
form = cdr cdr form
|
||||||
|
null
|
||||||
|
o
|
||||||
|
|
||||||
|
exports.Normalize = Normalize
|
||||||
|
normalize = new Normalize()
|
||||||
|
exports.normalize = -> normalize.normalize.apply(normalize, arguments)
|
|
@ -0,0 +1,14 @@
|
||||||
|
{Node} = require './reader_types'
|
||||||
|
{Normalize} = require './reader_rawtoform'
|
||||||
|
|
||||||
|
liftToNode = (f) ->
|
||||||
|
(form) ->
|
||||||
|
return f.call this, (if (form instanceof Node) then form.v else form)
|
||||||
|
|
||||||
|
NodeNormalize = class
|
||||||
|
for own key, func of Normalize::
|
||||||
|
NodeNormalize::[key] = liftToNode(func)
|
||||||
|
|
||||||
|
exports.Normalize = NodeNormalize
|
||||||
|
normalize = new NodeNormalize()
|
||||||
|
exports.normalize = -> normalize.normalize.apply(normalize, arguments)
|
|
@ -0,0 +1,11 @@
|
||||||
|
exports.Node = class
|
||||||
|
constructor: (@v, @line, @column) ->
|
||||||
|
|
||||||
|
exports.Symbol = class
|
||||||
|
constructor: (@name) ->
|
||||||
|
|
||||||
|
exports.Comment = class
|
||||||
|
constructor: (@text) ->
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
{Reader, ReadError, Source} = require './reader'
|
||||||
|
{Node} = require './reader_types'
|
||||||
|
|
||||||
|
liftToTrack = (f) ->
|
||||||
|
(ioStream) ->
|
||||||
|
ioStream = if ioStream instanceof Source then ioStream else new Source ioStream
|
||||||
|
[line, column] = ioStream.position()
|
||||||
|
obj = f.apply(this, arguments)
|
||||||
|
if obj instanceof ReadError
|
||||||
|
obj['line'] = line
|
||||||
|
obj['column'] = column
|
||||||
|
return obj
|
||||||
|
if obj instanceof Node then obj else new Node obj, line, column
|
||||||
|
|
||||||
|
TrackingReader = class
|
||||||
|
|
||||||
|
for own key, func of Reader::
|
||||||
|
TrackingReader::[key] = liftToTrack(func)
|
||||||
|
TrackingReader::acc = (obj) -> obj.v
|
||||||
|
|
||||||
|
exports.ReadError = ReadError
|
||||||
|
exports.Reader = TrackingReader
|
||||||
|
exports.reader = reader = new TrackingReader()
|
||||||
|
exports.read = -> reader.read.apply(reader, arguments)
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
{
|
||||||
|
"arrow_spacing": {
|
||||||
|
"level": "ignore"
|
||||||
|
},
|
||||||
|
"braces_spacing": {
|
||||||
|
"level": "ignore",
|
||||||
|
"spaces": 0,
|
||||||
|
"empty_object_spaces": 0
|
||||||
|
},
|
||||||
|
"camel_case_classes": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"coffeescript_error": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"colon_assignment_spacing": {
|
||||||
|
"level": "ignore",
|
||||||
|
"spacing": {
|
||||||
|
"left": 0,
|
||||||
|
"right": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cyclomatic_complexity": {
|
||||||
|
"value": 10,
|
||||||
|
"level": "ignore"
|
||||||
|
},
|
||||||
|
"duplicate_key": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"empty_constructor_needs_parens": {
|
||||||
|
"level": "ignore"
|
||||||
|
},
|
||||||
|
"ensure_comprehensions": {
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
|
"eol_last": {
|
||||||
|
"level": "ignore"
|
||||||
|
},
|
||||||
|
"indentation": {
|
||||||
|
"value": 2,
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"line_endings": {
|
||||||
|
"level": "ignore",
|
||||||
|
"value": "unix"
|
||||||
|
},
|
||||||
|
"max_line_length": {
|
||||||
|
"value": 120,
|
||||||
|
"level": "error",
|
||||||
|
"limitComments": true
|
||||||
|
},
|
||||||
|
"missing_fat_arrows": {
|
||||||
|
"level": "ignore",
|
||||||
|
"is_strict": false
|
||||||
|
},
|
||||||
|
"newlines_after_classes": {
|
||||||
|
"value": 3,
|
||||||
|
"level": "ignore"
|
||||||
|
},
|
||||||
|
"no_backticks": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"no_debugger": {
|
||||||
|
"level": "warn",
|
||||||
|
"console": false
|
||||||
|
},
|
||||||
|
"no_empty_functions": {
|
||||||
|
"level": "ignore"
|
||||||
|
},
|
||||||
|
"no_empty_param_list": {
|
||||||
|
"level": "ignore"
|
||||||
|
},
|
||||||
|
"no_implicit_braces": {
|
||||||
|
"level": "ignore",
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"no_implicit_parens": {
|
||||||
|
"strict": true,
|
||||||
|
"level": "ignore"
|
||||||
|
},
|
||||||
|
"no_interpolation_in_single_quotes": {
|
||||||
|
"level": "ignore"
|
||||||
|
},
|
||||||
|
"no_plusplus": {
|
||||||
|
"level": "ignore"
|
||||||
|
},
|
||||||
|
"no_stand_alone_at": {
|
||||||
|
"level": "ignore"
|
||||||
|
},
|
||||||
|
"no_tabs": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"no_this": {
|
||||||
|
"level": "ignore"
|
||||||
|
},
|
||||||
|
"no_throwing_strings": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"no_trailing_semicolons": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"no_trailing_whitespace": {
|
||||||
|
"level": "error",
|
||||||
|
"allowed_in_comments": false,
|
||||||
|
"allowed_in_empty_lines": true
|
||||||
|
},
|
||||||
|
"no_unnecessary_double_quotes": {
|
||||||
|
"level": "ignore"
|
||||||
|
},
|
||||||
|
"no_unnecessary_fat_arrows": {
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
|
"non_empty_constructor_needs_parens": {
|
||||||
|
"level": "ignore"
|
||||||
|
},
|
||||||
|
"prefer_english_operator": {
|
||||||
|
"level": "ignore",
|
||||||
|
"doubleNotLevel": "ignore"
|
||||||
|
},
|
||||||
|
"space_operators": {
|
||||||
|
"level": "ignore"
|
||||||
|
},
|
||||||
|
"spacing_after_comma": {
|
||||||
|
"level": "ignore"
|
||||||
|
},
|
||||||
|
"transform_messes_up_line_numbers": {
|
||||||
|
"level": "warn"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
I've been working my way through a Lisp textbook, *Lisp In Small
|
||||||
|
Pieces*, by Christian Quinnec. It was originally written in French and
|
||||||
|
is not that well known among English-speaking Lisperati, not in
|
||||||
|
comparison to the Wizard book or Paul Graham's *On Lisp*, but what
|
||||||
|
caught my attention was how it really was in *small* pieces. Each
|
||||||
|
chapter ended with an interpreter described, sometimes in code,
|
||||||
|
sometimes in text; if you were smart enough, you could actually piece
|
||||||
|
the whole thing together and see how it worked.
|
||||||
|
|
||||||
|
I decided to make things hard for myself. Since I'm *not* a Lisperati
|
||||||
|
(although I may well and truly be seduced by Hy), I decided to make
|
||||||
|
things hard for myself by writing the interpreter in Coffeescript. Most
|
||||||
|
Lisp books assume you have a Lisp handy, and Quinnec's examples are fine
|
||||||
|
and dandy on many variants of Scheme, but for a fun time I decided to
|
||||||
|
write it in something else. Raganwald claims Javascript "is a Lisp,"
|
||||||
|
and if that's so it ought to be good enough to write a Lisp in it.
|
||||||
|
|
||||||
|
I mean, it's obviously been done before. I tried once before but got
|
||||||
|
lost. *LiSP* does me the favor of keeping me on track.
|
||||||
|
|
||||||
|
You can see all my sourcecode at <a
|
||||||
|
href="https://github.com/elfsternberg/LispInSmallPieces">Github: Lisp In
|
||||||
|
Small Pieces</a>.
|
||||||
|
|
||||||
|
Chapter 1 contains the base interpreter. It also contains a
|
||||||
|
hand-written Lisp reader, and refers to another project I have on
|
||||||
|
GitHub, <a
|
||||||
|
href="https://github.com/elfsternberg/cons-lists">cons-lists</a>, which
|
||||||
|
is exactly what it sounds like, a singly-linked list implementation in
|
||||||
|
Javascript, using nested Javascript arrays as the base. The base
|
||||||
|
interpreter is very primitive-- you can't even create new variable names
|
||||||
|
in the global namespace! Although you can shadow them using lambdas, so
|
||||||
|
it's pretty much bog standard Lambda Calculus.
|
||||||
|
|
||||||
|
Chapter "Lambda 1" contains a continuation-passing variant of the
|
||||||
|
interpreter from Chapter 1. It's basically a facile reading of
|
||||||
|
Lisperator's λ-language intepreter, with my own parser front-end and
|
||||||
|
some CPS style. It passes all the tests, but it's a distraction.
|
||||||
|
|
||||||
|
Chapter 3 contains the same interpreter, only using the architecture
|
||||||
|
Quinnec describes in Chapter 3 of his book.
|
||||||
|
|
||||||
|
Chapter 2 describes a number of different methodologies for binding,
|
||||||
|
scoping, and namespaces. The material is interesting but I didn't
|
||||||
|
pursue writing the various interpreters. I "got" what Quinnec was
|
||||||
|
saying, and if I'm ever interested in writing something with scoping
|
||||||
|
rules outside of the lexical scopes with which I'm familiar, I might
|
||||||
|
revisit the material.
|
||||||
|
|
||||||
|
The next step will be to add functions to the Chapter 3 interpreter to
|
||||||
|
do the various continuation management games, like call/cc, throw/catch,
|
||||||
|
$and so forth. Because *those*, I feel I need to understand.
|
||||||
|
|
||||||
|
How far will I take this project? I’m not sure. Chapter 4 is
|
||||||
|
"Assignment and Side Effects," so I’ll do that. Chapter 5 is theory,
|
||||||
|
and 6 implementation, of a "fast interpreter" of the kind French
|
||||||
|
programming language guys apparently love to study. I’ll read them, but
|
||||||
|
I’m not sure what code I’ll generate out of that. Chapter 7,
|
||||||
|
"Compilation," is interesting in that he starts by defining a VM that on
|
||||||
|
top of which our bytecode will run, and implement both the VM and the
|
||||||
|
compiler in Scheme. I think I want to do that chapter, and then
|
||||||
|
re-write the compiler to create LLVM-compatible code instead, just to
|
||||||
|
learn LLVM. Chapter 8 implements EVAL, chapter 9 has Macros, and
|
||||||
|
chapter 10 has Object-Oriented Lisp. So I’ll probably do those as well.
|
||||||
|
|
||||||
|
And then... we’ll see. I surprised myself by doing Chapter 3 in less
|
||||||
|
than two weeks.
|
|
@ -0,0 +1,12 @@
|
||||||
|
This doesn't really look like the read/analyze/compile passes that one
|
||||||
|
expects of a modern Lisp.
|
||||||
|
|
||||||
|
Reading converts the source code into a list of immutable values in the
|
||||||
|
low-level AST of the system. Reading and analysis must be combined if
|
||||||
|
there are to be reader macros (which I want to support).
|
||||||
|
|
||||||
|
... and then a miracle occurs ...
|
||||||
|
|
||||||
|
Compilation is the process of turning the AST into javascript.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<project>
|
||||||
|
<actions/>
|
||||||
|
<description>Building in the chapters of Lisp In Small Pieces.</description>
|
||||||
|
<logRotator class="hudson.tasks.LogRotator">
|
||||||
|
<daysToKeep>-1</daysToKeep>
|
||||||
|
<numToKeep>10</numToKeep>
|
||||||
|
<artifactDaysToKeep>-1</artifactDaysToKeep>
|
||||||
|
<artifactNumToKeep>-1</artifactNumToKeep>
|
||||||
|
</logRotator>
|
||||||
|
<keepDependencies>false</keepDependencies>
|
||||||
|
<properties/>
|
||||||
|
<scm class="hudson.plugins.git.GitSCM" plugin="git@2.3.4">
|
||||||
|
<configVersion>2</configVersion>
|
||||||
|
<userRemoteConfigs>
|
||||||
|
<hudson.plugins.git.UserRemoteConfig>
|
||||||
|
<url>file:///home/elf/Projects/LispInSmallPieces</url>
|
||||||
|
</hudson.plugins.git.UserRemoteConfig>
|
||||||
|
</userRemoteConfigs>
|
||||||
|
<branches>
|
||||||
|
<hudson.plugins.git.BranchSpec>
|
||||||
|
<name>*/master</name>
|
||||||
|
</hudson.plugins.git.BranchSpec>
|
||||||
|
</branches>
|
||||||
|
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
|
||||||
|
<submoduleCfg class="list"/>
|
||||||
|
<extensions/>
|
||||||
|
</scm>
|
||||||
|
<canRoam>true</canRoam>
|
||||||
|
<disabled>false</disabled>
|
||||||
|
<blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
|
||||||
|
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
|
||||||
|
<triggers>
|
||||||
|
<org.jenkinsci.plugins.fstrigger.triggers.FolderContentTrigger plugin="fstrigger@0.39">
|
||||||
|
<spec></spec>
|
||||||
|
<path>file:///home/elf/Projects/LispInSmallPieces</path>
|
||||||
|
<excludeCheckLastModificationDate>false</excludeCheckLastModificationDate>
|
||||||
|
<excludeCheckContent>false</excludeCheckContent>
|
||||||
|
<excludeCheckFewerOrMoreFiles>false</excludeCheckFewerOrMoreFiles>
|
||||||
|
</org.jenkinsci.plugins.fstrigger.triggers.FolderContentTrigger>
|
||||||
|
</triggers>
|
||||||
|
<concurrentBuild>false</concurrentBuild>
|
||||||
|
<builders>
|
||||||
|
<hudson.tasks.Shell>
|
||||||
|
<command>make node_modules test</command>
|
||||||
|
</hudson.tasks.Shell>
|
||||||
|
</builders>
|
||||||
|
<publishers>
|
||||||
|
<hudson.tasks.junit.JUnitResultArchiver plugin="junit@1.2-beta-4">
|
||||||
|
<testResults>test-reports.xml</testResults>
|
||||||
|
<keepLongStdio>false</keepLongStdio>
|
||||||
|
<testDataPublishers/>
|
||||||
|
<healthScaleFactor>1.0</healthScaleFactor>
|
||||||
|
</hudson.tasks.junit.JUnitResultArchiver>
|
||||||
|
</publishers>
|
||||||
|
<buildWrappers/>
|
||||||
|
</project>
|
14
package.json
14
package.json
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "CoffeeLisp",
|
"name": "LispInSmallPieces",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "A Coffeescript rendition of Lisp In Small Pieces",
|
"description": "A Coffeescript rendition of Lisp In Small Pieces",
|
||||||
"main": "bin/lisp",
|
"main": "bin/lisp",
|
||||||
|
@ -8,13 +8,21 @@
|
||||||
"cons-lists": "git+https://github.com/elfsternberg/cons-lists.git"
|
"cons-lists": "git+https://github.com/elfsternberg/cons-lists.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"coffeelint": "~1.10.0",
|
||||||
"chai": "^2.0.0",
|
"chai": "^2.0.0",
|
||||||
"mocha": "^2.1.0"
|
"mocha": "^2.1.0",
|
||||||
|
"mocha-jenkins-reporter": "^0.1.7"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "make test",
|
||||||
|
"build": "make node_modules"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "ssh://elfsternberg@elfsternberg.com/home/elfsternberg/repos/LispInSmallPieces"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
"lisp",
|
||||||
"scheme",
|
"scheme",
|
||||||
"practice",
|
"practice",
|
||||||
"interpreter",
|
"interpreter",
|
||||||
|
|
192
racket/chap1.rkt
192
racket/chap1.rkt
|
@ -1,192 +0,0 @@
|
||||||
; Lisp In Small Pieces, chapter 1 Simple Lambda Calculus interpreter
|
|
||||||
; with global environment, simple lexical closures enforced by singly
|
|
||||||
; linked lists.
|
|
||||||
|
|
||||||
; This covers only severals exercise: I added 'exit as an exit value;
|
|
||||||
; I fixed the definition of '<', and then applied the exercise out of
|
|
||||||
; the book.
|
|
||||||
|
|
||||||
; Any of the exercises that needed call/cc I've avoided for the simple
|
|
||||||
; reason that I started this to learn lisp, I'm a raw beginner, and
|
|
||||||
; call/cc is definitely advance estorics.
|
|
||||||
|
|
||||||
; Needed for 'wrong-syntax', which is Racket's version of the "wrong"
|
|
||||||
; exception tosser.
|
|
||||||
|
|
||||||
(require racket/syntax)
|
|
||||||
|
|
||||||
; LISP requires a mutatable environment, so using mcons/mpair for
|
|
||||||
; that.
|
|
||||||
|
|
||||||
(require scheme/mpair)
|
|
||||||
|
|
||||||
; Weird; racket needs this as a patch. I would have expected it as
|
|
||||||
; present in the default list of functions!
|
|
||||||
|
|
||||||
(define (atom? x)
|
|
||||||
(and (not (null? x))
|
|
||||||
(not (pair? x))))
|
|
||||||
|
|
||||||
(define env_init '())
|
|
||||||
|
|
||||||
(define env_global env_init)
|
|
||||||
|
|
||||||
; So, this macro places *into the current scope* (i.e. no building of
|
|
||||||
; a new scope that gets reaped upon exit) the names of variables and
|
|
||||||
; potential initial values.
|
|
||||||
|
|
||||||
(define-syntax definitial
|
|
||||||
(syntax-rules ()
|
|
||||||
((definitial name)
|
|
||||||
(begin (set! env_global (mcons (mcons 'name 'void) env_global)) 'name))
|
|
||||||
((definitial name value)
|
|
||||||
(begin (set! env_global (mcons (mcons 'name value) env_global)) 'name))))
|
|
||||||
|
|
||||||
; Oh! This macro (same scope thing again) associates named things with
|
|
||||||
; values in the target environment (the host language), along with
|
|
||||||
; arity checking. (which it doesn't do for 'if', for example)
|
|
||||||
|
|
||||||
(define-syntax defprimitive
|
|
||||||
(syntax-rules ()
|
|
||||||
((defprimitive name value arity)
|
|
||||||
(definitial name
|
|
||||||
(lambda (values)
|
|
||||||
(if (= arity (length values))
|
|
||||||
(apply value values)
|
|
||||||
(wrong-syntax #'here "Incorrect arity ~s" (list 'name values))))))))
|
|
||||||
|
|
||||||
; Sometimes, you do have to define something before you use it. Lesson
|
|
||||||
; learned.
|
|
||||||
|
|
||||||
(define the-false-value (cons "false" "boolean"))
|
|
||||||
|
|
||||||
(definitial t #t)
|
|
||||||
(definitial f the-false-value)
|
|
||||||
(definitial nil '())
|
|
||||||
(definitial foo)
|
|
||||||
(definitial bar)
|
|
||||||
(definitial fib)
|
|
||||||
(definitial fact)
|
|
||||||
|
|
||||||
(define-syntax defpredicate
|
|
||||||
(syntax-rules ()
|
|
||||||
((_ name native arity)
|
|
||||||
(defprimitive name (lambda args (or (apply native args) the-false-value)) arity))))
|
|
||||||
|
|
||||||
(defprimitive cons cons 2)
|
|
||||||
(defprimitive car car 1)
|
|
||||||
(defprimitive set-cdr! set-mcdr! 2)
|
|
||||||
(defprimitive + + 2)
|
|
||||||
(defprimitive - - 2)
|
|
||||||
(defprimitive * * 2)
|
|
||||||
(defpredicate lt < 2)
|
|
||||||
(defpredicate eq? eq? 2)
|
|
||||||
|
|
||||||
; This function extends the environment so that *at this moment of
|
|
||||||
; extension* the conslist head points to the old environment, then
|
|
||||||
; when it's done it points to the new environment. What's interesting
|
|
||||||
; is that the conslist head points to the last object initialized, not
|
|
||||||
; the first.
|
|
||||||
|
|
||||||
(define (extend env variables values)
|
|
||||||
(cond ((pair? variables)
|
|
||||||
(if (pair? values)
|
|
||||||
(mcons (mcons (car variables) (car values))
|
|
||||||
(extend env (cdr variables) (cdr values)))
|
|
||||||
(wrong-syntax #'here "Too few values")))
|
|
||||||
((null? variables)
|
|
||||||
(if (null? values)
|
|
||||||
env
|
|
||||||
(wrong-syntax #'here "Too many values")))
|
|
||||||
((symbol? variables) (mcons (mcons variables values) env))))
|
|
||||||
|
|
||||||
; Already we're starting to get some scope here. Note that
|
|
||||||
; make-function provides the environment, not the invoke. This makes
|
|
||||||
; this a lexically scoped interpreter.
|
|
||||||
|
|
||||||
(define (make-function variables body env)
|
|
||||||
(lambda (values)
|
|
||||||
(eprogn body (extend env variables values))))
|
|
||||||
|
|
||||||
; if it's a function, invoke it. Wow. Much complex. Very interpret.
|
|
||||||
|
|
||||||
(define (invoke fn args)
|
|
||||||
(if (procedure? fn)
|
|
||||||
(fn args)
|
|
||||||
(wrong-syntax #'here "Not an function ~s" fn)))
|
|
||||||
|
|
||||||
; Iterate through the exps, return the value of the last one.
|
|
||||||
|
|
||||||
(define (eprogn exps env)
|
|
||||||
(if (pair? exps)
|
|
||||||
(if (pair? (cdr exps))
|
|
||||||
(begin (evaluate (car exps) env)
|
|
||||||
(eprogn (cdr exps) env))
|
|
||||||
(evaluate (car exps) env))
|
|
||||||
'()))
|
|
||||||
|
|
||||||
; Iterate through the exps, return a list of the values of the
|
|
||||||
; evaluated expressions
|
|
||||||
|
|
||||||
(define (evlis exps env)
|
|
||||||
(if (pair? exps)
|
|
||||||
(cons (evaluate (car exps) env)
|
|
||||||
(evlis (cdr exps) env))
|
|
||||||
'()))
|
|
||||||
|
|
||||||
; silly patch because of the mutatable lists
|
|
||||||
|
|
||||||
(define-syntax mcaar (syntax-rules () ((_ e) (mcar (mcar e)))))
|
|
||||||
(define-syntax mcdar (syntax-rules () ((_ e) (mcdr (mcar e)))))
|
|
||||||
|
|
||||||
; Iterate through the environment, find an ID, return its associated
|
|
||||||
; value.
|
|
||||||
|
|
||||||
(define (lookup id env)
|
|
||||||
(if (mpair? env)
|
|
||||||
(if (eq? (mcaar env) id)
|
|
||||||
(mcdar env)
|
|
||||||
(lookup id (mcdr env)))
|
|
||||||
(wrong-syntax #'here "No such binding ~s" id)))
|
|
||||||
|
|
||||||
; Iterate through the environment, find an ID, and change its value to
|
|
||||||
; the new value. Again, purely global environment. Really starting
|
|
||||||
; to grok how the environment "stack" empowers modern runtimes.
|
|
||||||
|
|
||||||
(define (update! id env value)
|
|
||||||
(if (mpair? env)
|
|
||||||
(if (eq? (mcaar env) id)
|
|
||||||
(begin (set-mcdr! (mcar env) value) value)
|
|
||||||
(update! id (mcdr env) value))
|
|
||||||
(wrong-syntax #'here "No such binding ~s" id)))
|
|
||||||
|
|
||||||
; Core evaluation rules.
|
|
||||||
|
|
||||||
(define (evaluate exp env)
|
|
||||||
(if (atom? exp)
|
|
||||||
(cond
|
|
||||||
((symbol? exp) (lookup exp env))
|
|
||||||
((or (number? exp) (string? exp) (char? exp) (boolean? exp) (vector? exp)) exp)
|
|
||||||
(else (wrong-syntax #'here "Cannot evaluate")))
|
|
||||||
(case (car exp)
|
|
||||||
((quote) (cadr exp))
|
|
||||||
; Note: No checks that the statement even vaguely resembles the rules.
|
|
||||||
((if) (if (not (eq? (evaluate (cadr exp) env) the-false-value))
|
|
||||||
(evaluate (caddr exp) env)
|
|
||||||
(evaluate (cadddr exp) env)))
|
|
||||||
((begin) (eprogn (cdr exp) env))
|
|
||||||
((set!) (update! (cadr exp) env (evaluate (caddr exp) env)))
|
|
||||||
((lambda) (make-function (cadr exp) (cddr exp) env))
|
|
||||||
(else (invoke (evaluate (car exp) env) (evlis (cdr exp) env))))))
|
|
||||||
|
|
||||||
; Run it. Note that the function toplevel is self-referential.
|
|
||||||
|
|
||||||
(define (chapter1-scheme)
|
|
||||||
(define (toplevel)
|
|
||||||
(let ((result (evaluate (read) env_global)))
|
|
||||||
(if (not (eq? result 'exit))
|
|
||||||
(begin (display result) (toplevel))
|
|
||||||
#f)))
|
|
||||||
(toplevel))
|
|
||||||
|
|
||||||
; (set! fact (lambda (x) (if (eq? x 0) 1 (* x (fact (- x 1))))))
|
|
|
@ -1,10 +0,0 @@
|
||||||
(define (find-symbol id tree)
|
|
||||||
(call/cc
|
|
||||||
(lambda (exit)
|
|
||||||
(define (find tree)
|
|
||||||
(if (pair? tree)
|
|
||||||
(or (find (car tree)) (find (cdr tree)))
|
|
||||||
(if (eq? tree id) (exit #t) #f)))
|
|
||||||
(find tree))))
|
|
||||||
|
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
lookup = require './lookup'
|
|
||||||
{car, cdr, cadr, caadr, cdadr} = require './lists'
|
|
||||||
|
|
||||||
lispeval = (element, scope) ->
|
|
||||||
|
|
||||||
switch (car element)
|
|
||||||
when 'number' then parseInt (cadr element), 10
|
|
||||||
when 'string' then (cadr element)
|
|
||||||
when 'symbol' then lookup scope, (cadr element)
|
|
||||||
when 'list'
|
|
||||||
proc = lispeval (caadr element), scope
|
|
||||||
args = cdadr element
|
|
||||||
proc args, scope
|
|
||||||
else throw new Error ("Unrecognized type in parse: #{(car element)}")
|
|
||||||
|
|
||||||
module.exports = lispeval
|
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
lispeval = require './eval'
|
|
||||||
{cons, nil, nilp, car, cdr, listToVector} = require './lists'
|
|
||||||
|
|
||||||
module.exports =
|
|
||||||
create_vm_expression_evaluator: (defining_scope, params, body) ->
|
|
||||||
(cells, scope) ->
|
|
||||||
args = (amap = (cells, accum) ->
|
|
||||||
return accum if nilp cells
|
|
||||||
amap((cdr cells), accum.concat(lispeval (car cells), scope)))(cells, [])
|
|
||||||
body.apply null, args
|
|
||||||
|
|
||||||
create_lisp_expression_evaluator: (defining_scope, params, body) ->
|
|
||||||
(cells, scope) ->
|
|
||||||
|
|
||||||
# Takes the current scope, which has been passed in during the
|
|
||||||
# execution phase, and evaluate the contents of the parameters
|
|
||||||
# in the context in which this call is made (i.e. when the
|
|
||||||
# function is *called*, rather than defined.
|
|
||||||
|
|
||||||
new_scope = (cmap = (cells, params, nscope) ->
|
|
||||||
return nscope if (nilp cells) or (nilp params)
|
|
||||||
nscope[(car params)] = lispeval (car cells), scope
|
|
||||||
cmap((cdr cells), (cdr params), nscope))(cells, params, {})
|
|
||||||
|
|
||||||
# Execute and evaluate the body, creating an inner scope that
|
|
||||||
# consists of: (1) the bound variables (the parameters)
|
|
||||||
# evaluated in the context of the function call, because that's
|
|
||||||
# where they were encountered (2) the free variables evaluated
|
|
||||||
# in the context of the defining scope, because that's where
|
|
||||||
# *they* were encountered.
|
|
||||||
#
|
|
||||||
# While this inspiration comes from Coglan, the clearest
|
|
||||||
# explanation is from Lisperator's 'make_lambda' paragraph at
|
|
||||||
# http://lisperator.net/pltut/eval1/
|
|
||||||
|
|
||||||
inner_scope = cons(new_scope, defining_scope)
|
|
||||||
(nval = (body, memo) ->
|
|
||||||
return memo if nilp body
|
|
||||||
nval((cdr body), lispeval((car body), inner_scope)))(body)
|
|
||||||
|
|
||||||
create_special_form_evaluator: (defining_scope, params, body) ->
|
|
||||||
(cells, scope) -> body(cells, scope)
|
|
||||||
|
|
|
@ -1,152 +0,0 @@
|
||||||
{listToString, listToVector, pairp, cons, car, cdr, caar, cddr, cdar, cadr, caadr, cadar, caddr, nilp, nil, setcdr, metacadr} = require "cons-lists/lists"
|
|
||||||
readline = require "readline"
|
|
||||||
{inspect} = require "util"
|
|
||||||
print = require "./print"
|
|
||||||
|
|
||||||
|
|
||||||
env_init = nil
|
|
||||||
env_global = env_init
|
|
||||||
|
|
||||||
ntype = (node) -> car node
|
|
||||||
nvalu = (node) -> cadr node
|
|
||||||
|
|
||||||
definitial = (name, value = nil) ->
|
|
||||||
env_global = (cons (cons name, value), env_global)
|
|
||||||
name
|
|
||||||
|
|
||||||
defprimitive = (name, nativ, arity) ->
|
|
||||||
definitial name, ((args) ->
|
|
||||||
vmargs = listToVector(args)
|
|
||||||
if (vmargs.length == arity)
|
|
||||||
nativ.apply null, vmargs
|
|
||||||
else
|
|
||||||
throw "Incorrect arity")
|
|
||||||
|
|
||||||
the_false_value = (cons "false", "boolean")
|
|
||||||
|
|
||||||
definitial "#t", true
|
|
||||||
definitial "#f", the_false_value
|
|
||||||
definitial "nil", nil
|
|
||||||
definitial "foo"
|
|
||||||
definitial "bar"
|
|
||||||
definitial "fib"
|
|
||||||
definitial "fact"
|
|
||||||
|
|
||||||
defpredicate = (name, nativ, arity) ->
|
|
||||||
defprimitive name, ((a, b) -> if nativ.call(null, a, b) then true else the_false_value), arity
|
|
||||||
|
|
||||||
defprimitive "cons", cons, 2
|
|
||||||
defprimitive "car", car, 2
|
|
||||||
defprimitive "set-cdr!", setcdr, 2
|
|
||||||
defprimitive "+", ((a, b) -> a + b), 2
|
|
||||||
defprimitive "*", ((a, b) -> a * b), 2
|
|
||||||
defprimitive "-", ((a, b) -> a - b), 2
|
|
||||||
defprimitive "/", ((a, b) -> a / b), 2
|
|
||||||
defpredicate "lt", ((a, b) -> a < b), 2
|
|
||||||
defpredicate "eq?", ((a, b) -> a == b), 2
|
|
||||||
|
|
||||||
extend = (env, variables, values) ->
|
|
||||||
if (pairp variables)
|
|
||||||
if (pairp values)
|
|
||||||
(cons (cons (car variables), (car values)),
|
|
||||||
(extend env, (cdr variables), (cdr values)))
|
|
||||||
else
|
|
||||||
throw "Too few values"
|
|
||||||
else if (nilp variables)
|
|
||||||
if (nilp values) then env else throw "Too many values"
|
|
||||||
else
|
|
||||||
if (symbolp variables)
|
|
||||||
(cons (cons variables, values), env)
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
|
|
||||||
make_function = (variables, body, env) ->
|
|
||||||
(values) -> eprogn body, (extend env, variables, values)
|
|
||||||
|
|
||||||
invoke = (fn, args) ->
|
|
||||||
(fn args)
|
|
||||||
|
|
||||||
# Takes a list of nodes and calls evaluate on each one, returning the
|
|
||||||
# last one as the value of the total expression. In this example, we
|
|
||||||
# are hard-coding what ought to be a macro, namely the threading
|
|
||||||
# macros, "->"
|
|
||||||
|
|
||||||
eprogn = (exps, env) ->
|
|
||||||
if (pairp exps)
|
|
||||||
if pairp (cdr exps)
|
|
||||||
evaluate (car exps), env
|
|
||||||
eprogn (cdr exps), env
|
|
||||||
else
|
|
||||||
evaluate (car exps), env
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
|
|
||||||
evlis = (exps, env) ->
|
|
||||||
if (pairp exps)
|
|
||||||
(cons (evaluate (car exps), env), (evlis (cdr exps), env))
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
|
|
||||||
lookup = (id, env) ->
|
|
||||||
if (pairp env)
|
|
||||||
if (caar env) == id
|
|
||||||
cdar env
|
|
||||||
else
|
|
||||||
lookup id, (cdr env)
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
|
|
||||||
update = (id, env, value) ->
|
|
||||||
if (pairp env)
|
|
||||||
if (caar env) == id
|
|
||||||
setcdr value, (car env)
|
|
||||||
value
|
|
||||||
else
|
|
||||||
update id, (cdr env), value
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
|
|
||||||
# This really ought to be the only place where the AST meets the
|
|
||||||
# interpreter core. I can't help but think that this design precludes
|
|
||||||
# pluggable interpreter core.
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
# Takes an AST node and evaluates it and its contents. A node may be
|
|
||||||
# ("list" (... contents ...)) or ("number" 42) or ("symbol" x), etc.
|
|
||||||
|
|
||||||
cadddr = metacadr('cadddr')
|
|
||||||
|
|
||||||
evaluate = (e, env) ->
|
|
||||||
[type, exp] = [(ntype e), (nvalu e)]
|
|
||||||
if type == "symbol"
|
|
||||||
return lookup exp, env
|
|
||||||
else if type in ["number", "string", "boolean", "vector"]
|
|
||||||
return exp
|
|
||||||
else if type == "list"
|
|
||||||
head = car exp
|
|
||||||
if (ntype head) == 'symbol'
|
|
||||||
switch (nvalu head)
|
|
||||||
when "quote" then cdr exp
|
|
||||||
when "if"
|
|
||||||
unless (evaluate (cadr exp), env) == the_false_value
|
|
||||||
evaluate (caddr exp), env
|
|
||||||
else
|
|
||||||
evaluate (cadddr exp), env
|
|
||||||
when "begin" then eprogn (cdr exp), env
|
|
||||||
when "set!" then update (nvalu cadr exp), env, (evaluate (caddr exp), env)
|
|
||||||
when "lambda" then make_function (astSymbolsToLispSymbols cadr exp), (cddr exp), env
|
|
||||||
else invoke (evaluate (car exp), env), (evlis (cdr exp), env)
|
|
||||||
else
|
|
||||||
invoke (evaluate (car exp), env), (evlis (cdr exp), env)
|
|
||||||
else
|
|
||||||
throw new Error("Can't handle a #{type}")
|
|
||||||
|
|
||||||
module.exports = (c) -> evaluate c, env_global
|
|
|
@ -1,21 +0,0 @@
|
||||||
{listToString, listToVector, pairp, cons, car, cdr, caar, cddr, cdar, cadr, caadr, cadar, caddr, nilp, nil, setcdr, metacadr} = require "cons-lists/lists"
|
|
||||||
|
|
||||||
ntype = (node) -> car node
|
|
||||||
nvalu = (node) -> cadr node
|
|
||||||
|
|
||||||
evlis = (exps, d) ->
|
|
||||||
if (pairp exps) then evaluate((car exps), d) + " " + evlis((cdr exps), d) else ""
|
|
||||||
|
|
||||||
indent = (d) ->
|
|
||||||
([0..d].map () -> " ").join('')
|
|
||||||
|
|
||||||
evaluate = (e, d = 0) ->
|
|
||||||
[type, exp] = [(ntype e), (nvalu e)]
|
|
||||||
if type == "symbol" then exp
|
|
||||||
else if type in ["number", "boolean"] then exp
|
|
||||||
else if type == "string" then '"' + exp + '"'
|
|
||||||
else if type == "list" then "\n" + indent(d) + "(" + evlis(exp, d + 2) + ")"
|
|
||||||
else throw "Don't recognize a #{type}"
|
|
||||||
|
|
||||||
module.exports = (c) -> evaluate c, 0
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
lispeval = require './eval'
|
|
||||||
{cons, car, cdr, nilp, nil, cadar, cadr, caddr} = require './lists'
|
|
||||||
{create_lisp_expression_evaluator, create_vm_expression_evaluator, create_special_form_evaluator} = require './fn'
|
|
||||||
|
|
||||||
scope = cons
|
|
||||||
'+': create_vm_expression_evaluator scope, [], (a, b) -> a + b
|
|
||||||
'-': create_vm_expression_evaluator scope, [], (a, b) -> a - b
|
|
||||||
'*': create_vm_expression_evaluator scope, [], (a, b) -> a * b
|
|
||||||
'/': create_vm_expression_evaluator scope, [], (a, b) -> a / b
|
|
||||||
'==': create_vm_expression_evaluator scope, [], (a, b) -> a == b
|
|
||||||
'#t': true
|
|
||||||
'#f': false
|
|
||||||
|
|
||||||
'define': create_special_form_evaluator scope, [], (nodes, scope) ->
|
|
||||||
current = (car scope)
|
|
||||||
current[(cadar nodes)] = lispeval((cadr nodes), scope)
|
|
||||||
|
|
||||||
'lambda': create_special_form_evaluator scope, [], (nodes, scope) ->
|
|
||||||
param_nodes = cadar nodes
|
|
||||||
reducer = (l) ->
|
|
||||||
if (nilp l) then nil else cons (cadar l), reducer(cdr l)
|
|
||||||
param_names = reducer(param_nodes)
|
|
||||||
create_lisp_expression_evaluator scope, param_names, (cdr nodes)
|
|
||||||
|
|
||||||
'if': create_special_form_evaluator scope, [], (nodes, scope) ->
|
|
||||||
if lispeval (car nodes), scope
|
|
||||||
lispeval (cadr nodes), scope
|
|
||||||
else
|
|
||||||
lispeval (caddr nodes), scope
|
|
||||||
|
|
||||||
module.exports = scope
|
|
|
@ -1,13 +0,0 @@
|
||||||
lisp = require './lisp_ch1'
|
|
||||||
{read, readForms} = require './reader'
|
|
||||||
{inspect} = require 'util'
|
|
||||||
ast = read("(begin (set! fact (lambda (x) (if (eq? x 0) 1 (* x (fact (- x 1)))))) (fact 5))")
|
|
||||||
|
|
||||||
# ast = read("(begin (if (lt 4 2) (+ 4 1) (+ 2 1)))")
|
|
||||||
# ast = read("(begin (set! fact 4) fact)")
|
|
||||||
# ast = read("(begin ((lambda (t) (if (lt t 2) (+ 4 1) (+ 2 1))) 1))")
|
|
||||||
|
|
||||||
# ast = read("(begin (set! fact (lambda (x) (+ x x))) (fact 5))")
|
|
||||||
# ast = read("(begin (set! fact (lambda (x) (- x 4))) (fact 5))")
|
|
||||||
|
|
||||||
console.log "Result:", (lisp ast)
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
{cons, nil} = require "cons-lists/lists"
|
||||||
|
exports.samples = [
|
||||||
|
['nil', nil]
|
||||||
|
['0', 0]
|
||||||
|
['1', 1]
|
||||||
|
['500', 500]
|
||||||
|
['0xdeadbeef', 3735928559]
|
||||||
|
['"Foo"', 'Foo']
|
||||||
|
['(1)', cons(1)]
|
||||||
|
['(1 2)', cons(1, (cons 2))]
|
||||||
|
['(1 2 )', cons(1, (cons 2))]
|
||||||
|
['( 1 2 )', cons(1, (cons 2))]
|
||||||
|
['(1 (2 3) 4)', cons(1, cons(cons(2, cons(3)), cons(4)))]
|
||||||
|
['( 1 2 )', cons(1, (cons 2))]
|
||||||
|
['("a" "b")', cons("a", (cons "b"))]
|
||||||
|
['("a" . "b")', cons("a", "b")]
|
||||||
|
['[]', []]
|
||||||
|
['{}', {}]
|
||||||
|
['{"a" [1 2 3] "b" {"c" "d"} "c" ("a" "b" . "c")}', {"a": [1,2,3], "b":{"c": "d"}, "c": cons("a", cons("b", "c"))}]
|
||||||
|
['[1 2 3]', [1, 2, 3]]
|
||||||
|
['[1 2 [3 4] 5]', [1, 2, [3, 4], 5]]
|
||||||
|
# ['(1 2 3', 'error']
|
||||||
|
['{"foo" "bar"}', {foo: "bar"}]
|
||||||
|
]
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
chai = require 'chai'
|
||||||
|
chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
|
||||||
|
{cons} = require "cons-lists/lists"
|
||||||
|
olisp = require '../chapter-lambda-1/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)
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
chai = require 'chai'
|
||||||
|
chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
|
||||||
|
{cons} = require "cons-lists/lists"
|
||||||
|
lisp = require '../chapter1/interpreter'
|
||||||
|
{read, readForms} = require '../chapter1/reader'
|
||||||
|
|
||||||
|
the_false_value = (cons "false", "boolean")
|
||||||
|
|
||||||
|
describe "Core interpreter #1", ->
|
||||||
|
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)
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
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: Blocks", ->
|
||||||
|
it "Should handle simple blocks", ->
|
||||||
|
expect(lisp read "(block foo 33)").to.equal(33)
|
||||||
|
it "Should handle the last blocks", ->
|
||||||
|
expect(lisp read "(block foo 1 2 3)").to.equal(3)
|
||||||
|
it "Should handle expressive blocks", ->
|
||||||
|
expect(lisp read "(block foo (+ 5 5))").to.equal(10)
|
||||||
|
it "Should handle basic returns blocks", ->
|
||||||
|
expect(lisp read "(block foo (+ 1 (return-from foo 2)))").to.equal(2)
|
||||||
|
it "Should handle complex returns blocks", ->
|
||||||
|
code = "(block foo ((lambda (exit)(* 2 (block foo (* 3 (exit 5)) )) ) (lambda (x) (return-from foo x)) ) )"
|
||||||
|
expect(lisp read code).to.equal(5)
|
||||||
|
it "Expects an uninitialized return-from to fail", ->
|
||||||
|
expect(-> lisp read "(return-from foo 3)").to.throw("Unknown block label foo")
|
||||||
|
it "Expects to see an obsolete block when called late", ->
|
||||||
|
expect(-> lisp read "((block foo (lambda (x) (return-from foo x))) 3 )")
|
||||||
|
.to.throw("Obsolete continuation")
|
||||||
|
it "Expects to see an obsolete block when called late", ->
|
||||||
|
blocka = "((block a (* 2 (block b (return-from a (lambda (x) (return-from b x))))) )3 )"
|
||||||
|
expect(-> lisp read blocka).to.throw("Obsolete continuation")
|
||||||
|
it "Expects to see an obsolete block when called late", ->
|
||||||
|
blockb = "((block a (* 2 (block b (return-from a (lambda (x) (return-from a x))))) ) 3 )"
|
||||||
|
expect(-> lisp read blockb).to.throw("Obsolete continuation")
|
||||||
|
|
||||||
|
describe "Core interpreter #3: Try/Catch", ->
|
||||||
|
it "doesn't change a simple value", ->
|
||||||
|
expect(lisp read "(catch 'bar 1)").to.equal(1)
|
||||||
|
it "doesn't interfere with standard behavior", ->
|
||||||
|
expect(lisp read "(catch 'bar 1 2 3)").to.equal(3)
|
||||||
|
it "bails at the top level when no catch", ->
|
||||||
|
expect(-> lisp read "(throw 'bar 33)").to.throw("No associated catch")
|
||||||
|
it "catches the throws value", ->
|
||||||
|
expect(lisp read "(catch 'bar (throw 'bar 11))").to.equal(11)
|
||||||
|
it "catches before the evaluation happens", ->
|
||||||
|
expect(lisp read "(catch 'bar (* 2 (throw 'bar 5)))").to.equal(5)
|
||||||
|
it "unrolls through multiple layers of the stack", ->
|
||||||
|
expect(lisp read "((lambda (f) (catch 'bar (* 2 (f 5))) ) (lambda (x) (throw 'bar x)))").to.equal(5)
|
||||||
|
it "continues at the right location", ->
|
||||||
|
expect(lisp read "((lambda (f) (catch 'bar (* 2 (catch 'bar (* 3 (f 5))))) ) (lambda (x) (throw 'bar x)))").to.equal(10)
|
||||||
|
it "throw/catch happens with literalally catches", ->
|
||||||
|
expect(lisp read "(catch 2 (* 7 (catch 1 (* 3 (catch 2 (throw 1 (throw 2 5)) )) )))").to.equal(105)
|
||||||
|
it "bails at top level when there aren't enough catches", ->
|
||||||
|
expect(-> lisp read "(catch 2 (* 7 (throw 1 (throw 2 3))))").to.throw("No associated catch")
|
||||||
|
|
||||||
|
#describe "Core interpreter #3: Unwind-Protect", ->
|
||||||
|
# it "protects the value correctly", ->
|
||||||
|
# expect(lisp read "(unwind-protect 1 2").to.equal(1)
|
||||||
|
# it "", ->
|
||||||
|
# expect(lisp read "((lambda (c) (unwind-protect 1 (set! c 2)) c ) 0 ").to.equal(2)
|
||||||
|
# it "", ->
|
||||||
|
# expect(lisp read "((lambda (c) (catch 111 (* 2 (unwind-protect (* 3 (throw 111 5)) (set! c 1) ))) ) 0 ").to.equal(5)
|
||||||
|
# it "", ->
|
||||||
|
# expect(lisp read "((lambda (c) (catch 111 (* 2 (unwind-protect (* 3 (throw 111 5)) (set! c 1) ))) c ) 0 ").to.equal(1)
|
||||||
|
# it "", ->
|
||||||
|
# expect(lisp read "((lambda (c) (block A (* 2 (unwind-protect (* 3 (return-from A 5)) (set! c 1) ))) ) 0 ").to.equal(5)
|
||||||
|
# it "", ->
|
||||||
|
# expect(lisp read "((lambda (c) (block A (* 2 (unwind-protect (* 3 (return-from A 5)) (set! c 1) ))) c ) 0 ").to.equal(1)
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# describe "Core interpreter #3: Try/Catch with Throw as a function", ->
|
||||||
|
# contain = (fcall) ->
|
||||||
|
# return "(begin ((lambda () (begin (set! funcall (lambda (g . args) (apply g args))) #{fcall}))))"
|
||||||
|
#
|
||||||
|
# it "", ->
|
||||||
|
# expect(-> lisp read "(funcall throw 'bar 33").to.throw("bar")
|
||||||
|
# it "", ->
|
||||||
|
# expect(lisp read "(catch 'bar (funcall throw 'bar 11))").to.equal(11)
|
||||||
|
# it "", ->
|
||||||
|
# expect(lisp read "(catch 'bar (* 2 (funcall throw 'bar 5)))").to.equal(5)
|
||||||
|
# it "", ->in
|
||||||
|
# expect(lisp read "((lambda (f) (catch 'bar (* 2 (f 5))) ) (lambda (x) (funcall throw 'bar x))) ").to.equal(5)
|
||||||
|
# it "", ->
|
||||||
|
# expect(lisp read "((lambda (f) (catch 'bar (* 2 (catch 'bar (* 3 (f 5))))) ) (lambda (x) (funcall throw 'bar x)))) ").to.equal(10)
|
||||||
|
# it "", ->
|
||||||
|
# expect(lisp read "(catch 2 (* 7 (catch 1 (* 3 (catch 2 (funcall throw 1 (funcall throw 2 5)) )) ))) ").to.equal(105)
|
||||||
|
# it "", ->
|
||||||
|
# expect(lisp read "(catch 2 (* 7 (funcall throw 1 (funcall throw 2 3))))").to.equal(3)
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
chai = require 'chai'
|
||||||
|
chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
|
||||||
|
{cons} = require "cons-lists/lists"
|
||||||
|
olisp = require '../chapter3g/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: Protect", ->
|
||||||
|
it "unwinds but returns the value of the form", ->
|
||||||
|
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) (protect 1 (set! c 2)) c ) 0 ").to.equal(2)
|
||||||
|
it "Unwinds inside an unevaluated definition", ->
|
||||||
|
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 (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 (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 (protect (* 3 (return A 5)) (set! c 1) ))) c ) 0)").to.equal(1)
|
||||||
|
|
||||||
|
|
||||||
|
#describe "Core interpreter #3: Try/Catch with Throw as a function", ->
|
||||||
|
# contain = (fcall) ->
|
||||||
|
# return "(begin ((lambda () (begin (set! funcall (lambda (g . args) (apply g args))) #{fcall}))))"
|
||||||
|
#
|
||||||
|
# it "", ->
|
||||||
|
# expect(-> lisp read "(funcall throw 'bar 33").to.throw("bar")
|
||||||
|
# it "", ->
|
||||||
|
# expect(lisp read "(catch 'bar (funcall throw 'bar 11))").to.equal(11)
|
||||||
|
# it "", ->
|
||||||
|
# expect(lisp read "(catch 'bar (* 2 (funcall throw 'bar 5)))").to.equal(5)
|
||||||
|
# it "", ->
|
||||||
|
# expect(lisp read "((lambda (f) (catch 'bar (* 2 (f 5))) ) (lambda (x) (funcall throw 'bar x))) ").to.equal(5)
|
||||||
|
# it "", ->
|
||||||
|
# expect(lisp read "((lambda (f) (catch 'bar (* 2 (catch 'bar (* 3 (f 5))))) ) (lambda (x) (funcall throw 'bar x)))) ").to.equal(10)
|
||||||
|
# it "", ->
|
||||||
|
# expect(lisp read "(catch 2 (* 7 (catch 1 (* 3 (catch 2 (funcall throw 1 (funcall throw 2 5)) )) ))) ").to.equal(105)
|
||||||
|
# it "", ->
|
||||||
|
# expect(lisp read "(catch 2 (* 7 (funcall throw 1 (funcall throw 2 3))))").to.equal(3)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
chai = require 'chai'
|
||||||
|
chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
|
||||||
|
{cons} = require "cons-lists/lists"
|
||||||
|
olisp = require '../chapter4/interpreter'
|
||||||
|
{read, readForms} = require '../chapter4/reader'
|
||||||
|
|
||||||
|
the_false_value = (cons "false", "boolean")
|
||||||
|
|
||||||
|
lisp = (ast) ->
|
||||||
|
ret = undefined
|
||||||
|
olisp ast, (i) -> ret = i
|
||||||
|
return ret
|
||||||
|
|
||||||
|
describe "Core interpreter #4: Pure Lambda Memory", ->
|
||||||
|
it "Understands symbol equality", ->
|
||||||
|
expect(lisp read "(eq? 'a 'b)").to.equal(false)
|
||||||
|
expect(lisp read "(eq? 'a 'a)").to.equal(true)
|
||||||
|
it "Understands separate allocation inequality", ->
|
||||||
|
expect(lisp read "(eq? (cons 1 2) (cons 1 2))").to.equal(false)
|
||||||
|
it "Understands address equality", ->
|
||||||
|
expect(lisp read "((lambda (a) (eq? a a)) (cons 1 2))").to.equal(true)
|
||||||
|
expect(lisp read "((lambda (a) (eq? a a)) (lambda (x) x))").to.equal(true)
|
||||||
|
it "Understands function inequality", ->
|
||||||
|
expect(lisp read "(eq? (lambda (x) 1) (lambda (x y) 2))").to.equal(false)
|
||||||
|
|
||||||
|
it "Understands equivalence", ->
|
||||||
|
expect(lisp read "(eqv? '1 '2)").to.equal(false)
|
||||||
|
expect(lisp read "(eqv? 1 1)").to.equal(true)
|
||||||
|
expect(lisp read "(eqv? 'a 'b)").to.equal(false)
|
||||||
|
expect(lisp read "(eqv? 'a 'a)").to.equal(true)
|
||||||
|
expect(lisp read "(eqv? (cons 1 2) (cons 1 2))").to.equal(false)
|
||||||
|
expect(lisp read "((lambda (a) (eqv? a a)) (cons 1 2))").to.equal(true)
|
||||||
|
expect(lisp read "((lambda (a) (eqv? a a)) (lambda (x) x))").to.equal(true)
|
||||||
|
expect(lisp read "(eqv? (lambda (x) 1) (lambda (x y) 2))").to.equal(false)
|
||||||
|
|
||||||
|
it "Does special OR (backtracking without side-effect)", ->
|
||||||
|
expr1 = "((lambda (x) (or (begin (set! x (+ x 1)) #f) (if (= x 1) \"OK\" \"KO\"))) 1)"
|
||||||
|
expect(lisp read expr1).to.equal("OK")
|
||||||
|
expr2 = "((lambda (x) (or (begin (set! x (+ x 1)) #f) (if (= x 1) (begin (set! x 3) x) \"KO\"))) 1)"
|
||||||
|
expect(lisp read expr2).to.equal(3)
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
chai = require 'chai'
|
||||||
|
chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
|
||||||
|
olisp = require '../chapter5/interpreter5a'
|
||||||
|
{read} = require '../chapter5/reader'
|
||||||
|
|
||||||
|
lisp = (ast) ->
|
||||||
|
ret = undefined
|
||||||
|
olisp ast, (i) -> ret = i
|
||||||
|
return ret
|
||||||
|
|
||||||
|
describe "Core interpreter #5: Now with more λ!", ->
|
||||||
|
it "Understands symbol inequality", ->
|
||||||
|
expect(lisp read "(eq? 'a 'b)").to.equal(false)
|
||||||
|
it "Understands symbol equality", ->
|
||||||
|
expect(lisp read "(eq? 'a 'a)").to.equal(true)
|
||||||
|
it "Understands separate allocation inequality", ->
|
||||||
|
expect(lisp read "(eq? (cons 1 2) (cons 1 2))").to.equal(false)
|
||||||
|
it "Understands address equality of values", ->
|
||||||
|
expect(lisp read "((lambda (a) (eq? a a)) (cons 1 2))").to.equal(true)
|
||||||
|
it "Understands address equality of functions", ->
|
||||||
|
expect(lisp read "((lambda (a) (eq? a a)) (lambda (x) x))").to.equal(true)
|
||||||
|
it "Understands function inequality", ->
|
||||||
|
expect(lisp read "(eq? (lambda (x) 1) (lambda (x y) 2))").to.equal(false)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
chai = require 'chai'
|
||||||
|
chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
|
||||||
|
{cons, nil, nilp} = require "cons-lists/lists"
|
||||||
|
{read, readForms} = require '../chapter1/reader'
|
||||||
|
{normalizeForm} = require '../chapter1/astToList'
|
||||||
|
|
||||||
|
describe "Core reader functions", ->
|
||||||
|
samples = [
|
||||||
|
['nil', nil]
|
||||||
|
['0', 0]
|
||||||
|
['1', 1]
|
||||||
|
['500', 500]
|
||||||
|
['0xdeadbeef', 3735928559]
|
||||||
|
['"Foo"', 'Foo']
|
||||||
|
['(1)', cons(1)]
|
||||||
|
['(1 2)', cons(1, (cons 2))]
|
||||||
|
['(1 2 )', cons(1, (cons 2))]
|
||||||
|
['( 1 2 )', cons(1, (cons 2))]
|
||||||
|
['( 1 2 )', cons(1, (cons 2))]
|
||||||
|
['("a" "b")', cons("a", (cons "b"))]
|
||||||
|
['("a" . "b")', cons("a", "b")]
|
||||||
|
['[]', []]
|
||||||
|
['{}', {}]
|
||||||
|
['[1 2 3]', [1, 2, 3]]
|
||||||
|
# ['(1 2 3', 'error']
|
||||||
|
['{"foo" "bar"}', {foo: "bar"}]
|
||||||
|
]
|
||||||
|
|
||||||
|
for [t, v] in samples
|
||||||
|
do (t, v) ->
|
||||||
|
it "should interpret #{t} as #{v}", ->
|
||||||
|
res = normalizeForm read t
|
||||||
|
expect(res).to.deep.equal(v)
|
|
@ -0,0 +1,15 @@
|
||||||
|
chai = require 'chai'
|
||||||
|
chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
|
||||||
|
{cons, nil, nilp} = require "cons-lists/lists"
|
||||||
|
{read} = require '../chapter5/reader'
|
||||||
|
{normalize} = require '../chapter5/reader_rawtoform'
|
||||||
|
{samples} = require './reader5_samples'
|
||||||
|
|
||||||
|
describe "Lisp reader functions", ->
|
||||||
|
for [t, v] in samples
|
||||||
|
do (t, v) ->
|
||||||
|
it "should interpret #{t} as #{v}", ->
|
||||||
|
res = normalize read t
|
||||||
|
expect(res).to.deep.equal(v)
|
|
@ -0,0 +1,15 @@
|
||||||
|
chai = require 'chai'
|
||||||
|
chai.should()
|
||||||
|
expect = chai.expect
|
||||||
|
|
||||||
|
{cons, nil, nilp} = require "cons-lists/lists"
|
||||||
|
{read} = require '../chapter5/tracking_reader'
|
||||||
|
{normalize} = require '../chapter5/reader_tracktoform'
|
||||||
|
{samples} = require './reader5_samples'
|
||||||
|
|
||||||
|
describe "Tracker reader functions", ->
|
||||||
|
for [t, v] in samples
|
||||||
|
do (t, v) ->
|
||||||
|
it "should interpret #{t} as #{v}", ->
|
||||||
|
res = normalize read t
|
||||||
|
expect(res).to.deep.equal(v)
|
Loading…
Reference in New Issue