aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Martin <github@martintribe.org>2014-11-08 16:56:36 -0600
committerJoel Martin <github@martintribe.org>2015-01-09 16:16:45 -0600
commit891c3f3b478292ad0bfca44b0dc098a2aecc9a5d (patch)
treed30c7923ffee7699cfca94af84f3be297448fff2
parent9b3362e86a57ed7f14c5fd018c37713185e0c154 (diff)
downloadmal-891c3f3b478292ad0bfca44b0dc098a2aecc9a5d.tar.gz
mal-891c3f3b478292ad0bfca44b0dc098a2aecc9a5d.zip
CoffeeScript: add all steps. Self-hosting.
-rw-r--r--.gitignore1
-rw-r--r--Makefile5
-rw-r--r--README.md11
-rw-r--r--coffee/Makefile15
-rw-r--r--coffee/core.coffee86
-rw-r--r--coffee/env.coffee24
-rw-r--r--coffee/package.json9
-rw-r--r--coffee/printer.coffee24
-rw-r--r--coffee/reader.coffee88
-rw-r--r--coffee/step0_repl.coffee20
-rw-r--r--coffee/step1_read_print.coffee27
-rw-r--r--coffee/step2_eval.coffee52
-rw-r--r--coffee/step3_env.coffee63
-rw-r--r--coffee/step4_if_fn_do.coffee76
-rw-r--r--coffee/step5_tco.coffee82
-rw-r--r--coffee/step6_file.coffee90
-rw-r--r--coffee/step7_quote.coffee106
-rw-r--r--coffee/step8_macros.coffee126
-rw-r--r--coffee/step9_try.coffee136
-rw-r--r--coffee/stepA_interop.coffee142
-rw-r--r--coffee/tests/stepA_interop.mal24
-rw-r--r--coffee/types.coffee115
-rw-r--r--tests/step8_macros.mal13
23 files changed, 1331 insertions, 4 deletions
diff --git a/.gitignore b/.gitignore
index 387be2b..f298b8c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ make/mal.mk
js/node_modules
js/mal.js
js/mal_web.js
+coffee/node_modules
bash/mal.sh
c/*.o
*.pyc
diff --git a/Makefile b/Makefile
index 860821a..3367582 100644
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,8 @@ PYTHON = python
# Settings
#
-IMPLS = bash c clojure cs go java js make mal perl php ps python r ruby rust
+IMPLS = bash c clojure coffee cs go java js make mal perl php ps \
+ python r ruby rust
step0 = step0_repl
step1 = step1_read_print
@@ -50,6 +51,7 @@ STEP_TEST_FILES = $(strip $(wildcard $(1)/tests/$($(2)).mal) $(wildcard tests/$(
bash_STEP_TO_PROG = bash/$($(1)).sh
c_STEP_TO_PROG = c/$($(1))
clojure_STEP_TO_PROG = clojure/src/$($(1)).clj
+coffee_STEP_TO_PROG = coffee/$($(1)).coffee
cs_STEP_TO_PROG = cs/$($(1)).exe
go_STEP_TO_PROG = go/$($(1))
java_STEP_TO_PROG = java/src/main/java/mal/$($(1)).java
@@ -68,6 +70,7 @@ rust_STEP_TO_PROG = rust/target/$($(1))
bash_RUNSTEP = bash ../$(2) $(3)
c_RUNSTEP = ../$(2) $(3)
clojure_RUNSTEP = lein with-profile +$(1) trampoline run $(3)
+coffee_RUNSTEP = coffee ../$(2) $(3)
cs_RUNSTEP = mono ../$(2) --raw $(3)
go_RUNSTEP = ../$(2) $(3)
java_RUNSTEP = mvn -quiet exec:java -Dexec.mainClass="mal.$($(1))" -Dexec.args="--raw$(if $(3), $(3),)"
diff --git a/README.md b/README.md
index 208380f..d7d5b8d 100644
--- a/README.md
+++ b/README.md
@@ -3,12 +3,13 @@
## Description
Mal is an interpreter for a subset of the Clojure programming
-language. Mal is implemented from scratch in 16 different languages:
+language. Mal is implemented from scratch in 17 different languages:
* Bash shell
* C
* C#
* Clojure
+* CoffeeScript
* Go
* Java
* Javascript ([Online Demo](http://kanaka.github.io/mal))
@@ -88,6 +89,14 @@ cd clojure
lein with-profile +stepX trampoline run
```
+### CoffeeScript
+
+```
+sudo npm install -g coffee-script
+cd coffee
+coffee ./stepX_YYY
+```
+
### Go
```
diff --git a/coffee/Makefile b/coffee/Makefile
new file mode 100644
index 0000000..d2212ed
--- /dev/null
+++ b/coffee/Makefile
@@ -0,0 +1,15 @@
+TESTS =
+
+SOURCES_BASE = node_readline.coffee types.coffee \
+ reader.coffee printer.coffee
+SOURCES_LISP = env.coffee core.coffee stepA_interop.coffee
+SOURCES = $(SOURCES_BASE) $(SOURCES_LISP)
+
+#all: mal.rb
+
+.PHONY: stats tests $(TESTS)
+
+stats: $(SOURCES)
+ @wc $^
+stats-lisp: $(SOURCES_LISP)
+ @wc $^
diff --git a/coffee/core.coffee b/coffee/core.coffee
new file mode 100644
index 0000000..d01e76f
--- /dev/null
+++ b/coffee/core.coffee
@@ -0,0 +1,86 @@
+readline = require "../js/node_readline"
+types = require "./types.coffee"
+reader = require "./reader.coffee"
+printer = require "./printer.coffee"
+[_pr_str, println] = [printer._pr_str, printer.println]
+
+# Sequence functions
+conj = (seq, args...) ->
+ switch types._obj_type(seq)
+ when 'list'
+ lst = types._clone(seq)
+ lst.unshift(x) for x in args
+ lst
+ when 'vector'
+ lst = types._clone(seq)
+ lst.push(args...)
+ types._vector(lst...)
+ else throw new Error "conj called on " + types._obj_type(seq)
+
+# Metadata functions
+with_meta = (obj,m) ->
+ new_obj = types._clone(obj)
+ new_obj.__meta__ = m
+ new_obj
+
+
+exports.ns = {
+ '=': (a,b) -> types._equal_Q(a,b),
+ 'throw': (a) -> throw a,
+ 'nil?': types._nil_Q,
+ 'true?': types._true_Q,
+ 'false?': types._false_Q,
+ 'symbol': types._symbol,
+ 'symbol?': types._symbol_Q,
+
+ 'pr-str': (a...) -> a.map((exp) -> _pr_str(exp,true)).join(" "),
+ 'str': (a...) -> a.map((exp) -> _pr_str(exp,false)).join(""),
+ 'prn': (a...) -> println(a.map((exp) -> _pr_str(exp,true))...),
+ 'println': (a...) -> println(a.map((exp) -> _pr_str(exp,false))...),
+ 'readline': readline.readline,
+ 'read-string': reader.read_str,
+ 'slurp': (a) -> require('fs').readFileSync(a, 'utf-8'),
+ '<': (a,b) -> a<b,
+ '<=': (a,b) -> a<=b,
+ '>': (a,b) -> a>b,
+ '>=': (a,b) -> a>=b,
+ '+': (a,b) -> a+b,
+ '-': (a,b) -> a-b,
+ '*': (a,b) -> a*b,
+ '/': (a,b) -> a/b,
+ 'time-ms': () -> new Date().getTime(),
+
+ 'list': (a...) -> a,
+ 'list?': types._list_Q,
+ 'vector': (a...) -> types._vector(a...),
+ 'vector?': types._vector_Q,
+ 'hash-map': (a...) -> types._hash_map(a...),
+ 'map?': types._hash_map_Q,
+ 'assoc': (a,b...) -> types._assoc_BANG(types._clone(a), b...),
+ 'dissoc': (a,b...) -> types._dissoc_BANG(types._clone(a), b...),
+ 'get': (a,b) -> if a != null and b of a then a[b] else null,
+ 'contains?': (a,b) -> b of a,
+ 'keys': (a) -> k for k of a,
+ 'vals': (a) -> v for k,v of a,
+
+ 'sequential?': types._sequential_Q,
+ 'cons': (a,b) -> [a].concat(b),
+ 'concat': (a=[],b...) -> a.concat(b...),
+ 'nth': (a,b) -> if a.length > b then a[b] else null,
+ 'first': (a) -> if a.length > 0 then a[0] else null,
+ 'rest': (a) -> a[1..],
+ 'empty?': (a) -> a.length == 0,
+ 'count': (a) -> a.length,
+ 'apply': (a,b...) -> a(b[0..-2].concat(b[b.length-1])...),
+ 'map': (a,b) -> b.map((x) -> a(x)),
+ 'conj': conj,
+
+ 'with-meta': with_meta,
+ 'meta': (a) -> a.__meta__ or null,
+ 'atom': types._atom,
+ 'atom?': types._atom_Q,
+ 'deref': (a) -> a.val,
+ 'reset!': (a,b) -> a.val = b,
+ 'swap!': (a,b,c...) -> a.val = b([a.val].concat(c)...), }
+
+# vim: ts=2:sw=2
diff --git a/coffee/env.coffee b/coffee/env.coffee
new file mode 100644
index 0000000..667e467
--- /dev/null
+++ b/coffee/env.coffee
@@ -0,0 +1,24 @@
+types = require "./types.coffee"
+
+# Env
+exports.Env = class Env
+ constructor: (@outer=null, @binds=[], @exprs=[]) ->
+ @data = {}
+ if @binds.length > 0
+ for b,i in @binds
+ if types._symbol_Q(b) && b.name == "&"
+ @data[@binds[i+1].name] = exprs[i..]
+ break
+ else
+ @data[b.name] = exprs[i]
+ find: (key) ->
+ if key of @data then @
+ else if @outer then @outer.find(key)
+ else null
+ set: (key, value) -> @data[key] = value
+ get: (key) ->
+ env = @find(key)
+ throw new Error("'" + key + "' not found") if !env
+ env.data[key]
+
+# vim: ts=2:sw=2
diff --git a/coffee/package.json b/coffee/package.json
new file mode 100644
index 0000000..859ec4a
--- /dev/null
+++ b/coffee/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "mal",
+ "version": "0.0.1",
+ "description": "Make a Lisp (mal) language implemented in CoffeeScript",
+ "dependencies": {
+ "ffi": "1.2.x",
+ "coffee-script": "~1.8"
+ }
+}
diff --git a/coffee/printer.coffee b/coffee/printer.coffee
new file mode 100644
index 0000000..7844416
--- /dev/null
+++ b/coffee/printer.coffee
@@ -0,0 +1,24 @@
+types = require "./types.coffee"
+
+exports.println = (args...) -> console.log(args.join(" ")) || null
+
+exports._pr_str = _pr_str = (obj, print_readably=true) ->
+ _r = print_readably
+ switch types._obj_type obj
+ when 'list' then '(' + obj.map((e) -> _pr_str(e,_r)).join(' ') + ')'
+ when 'vector' then '[' + obj.map((e) -> _pr_str(e,_r)).join(' ') + ']'
+ when 'hash-map'
+ ret = []
+ ret.push(_pr_str(k,_r), _pr_str(v,_r)) for k,v of obj
+ '{' + ret.join(' ') + '}'
+ when 'string'
+ if _r then '"' + (obj.replace(/\\/g, '\\\\')
+ .replace(/"/g, '\\"')
+ .replace(/\n/g, '\\n')) + '"'
+ else obj
+ when 'symbol' then obj.name
+ when 'nil' then 'nil'
+ when 'atom' then "(atom " + _pr_str(obj.val,_r) + ")"
+ else obj.toString()
+
+# vim: ts=2:sw=2
diff --git a/coffee/reader.coffee b/coffee/reader.coffee
new file mode 100644
index 0000000..5882b43
--- /dev/null
+++ b/coffee/reader.coffee
@@ -0,0 +1,88 @@
+types = require "./types.coffee"
+[_symbol, _vector, _hash_map] = [types._symbol,
+ types._vector,
+ types._hash_map]
+
+
+class Reader
+ constructor: (@tokens) -> @position = 0
+ next: -> @tokens[@position++]
+ peek: -> @tokens[@position]
+ skip: ->
+ @position++
+ @
+
+tokenize = (str) ->
+ re = /[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"|;.*|[^\s\[\]{}('"`,;)]*)/g
+ results = []
+ while (match = re.exec(str)[1]) != ""
+ continue if match[0] == ';'
+ results.push(match)
+ results
+
+read_atom = (rdr) ->
+ token = rdr.next()
+ if token.match /^-?[0-9]+$/ then parseInt token,10
+ else if token.match /^-?[0-9][0-9.]*$/ then parseFloat token,10
+ else if token[0] == '"'
+ token.slice(1, token.length-1)
+ .replace(/\\"/g, '"')
+ .replace(/\\n/g, "\n")
+ else if token == "nil" then null
+ else if token == "true" then true
+ else if token == "false" then false
+ else _symbol(token)
+
+read_list = (rdr, start='(', end=')') ->
+ ast = []
+ token = rdr.next()
+ throw new Error "expected '" + start + "'" if token != start
+ while (token = rdr.peek()) != end
+ throw new Error "expected '" + end + "', got EOF" if !token
+ ast.push read_form rdr
+ rdr.next()
+ ast
+
+read_vector = (rdr) ->
+ _vector(read_list(rdr, '[', ']')...)
+
+read_hash_map = (rdr) ->
+ _hash_map(read_list(rdr, '{', '}')...)
+
+read_form = (rdr) ->
+ token = rdr.peek()
+ switch token
+ when '\'' then [_symbol('quote'), read_form(rdr.skip())]
+ when '`' then [_symbol('quasiquote'), read_form(rdr.skip())]
+ when '~' then [_symbol('unquote'), read_form(rdr.skip())]
+ when '~@' then [_symbol('splice-unquote'), read_form(rdr.skip())]
+ when '^'
+ meta = read_form(rdr.skip())
+ [_symbol('with-meta'), read_form(rdr), meta]
+ when '@' then [_symbol('deref'), read_form(rdr.skip())]
+
+ # list
+ when ')' then throw new Error "unexpected ')'"
+ when '(' then read_list(rdr)
+ # vector
+ when ']' then throw new Error "unexpected ']'"
+ when '[' then read_vector(rdr)
+ # hash-map
+ when '}' then throw new Error "unexpected '}'"
+ when '{' then read_hash_map(rdr)
+ # atom
+ else read_atom(rdr)
+
+
+exports.BlankException = BlankException = (msg) -> null
+
+exports.read_str = read_str = (str) ->
+ tokens = tokenize(str)
+ throw new BlankException() if tokens.length == 0
+ read_form(new Reader(tokens))
+
+#console.log read_str "(1 \"two\" three)"
+#console.log read_str "[1 2 3]"
+#console.log read_str '{"abc" 123 "def" 456}'
+
+# vim: ts=2:sw=2
diff --git a/coffee/step0_repl.coffee b/coffee/step0_repl.coffee
new file mode 100644
index 0000000..4fa9e40
--- /dev/null
+++ b/coffee/step0_repl.coffee
@@ -0,0 +1,20 @@
+readline = require "./node_readline.coffee"
+
+# read
+READ = (str) -> str
+
+# eval
+EVAL = (ast, env) -> ast
+
+# print
+PRINT = (exp) -> exp
+
+# repl
+rep = (str) -> PRINT(EVAL(READ(str), {}))
+
+# repl loop
+while (line = readline.readline("user> ")) != null
+ continue if line == ""
+ console.log rep line
+
+# vim: ts=2:sw=2
diff --git a/coffee/step1_read_print.coffee b/coffee/step1_read_print.coffee
new file mode 100644
index 0000000..28edd1b
--- /dev/null
+++ b/coffee/step1_read_print.coffee
@@ -0,0 +1,27 @@
+readline = require "./node_readline.coffee"
+reader = require "./reader.coffee"
+printer = require "./printer.coffee"
+
+# read
+READ = (str) -> reader.read_str str
+
+# eval
+EVAL = (ast, env) -> ast
+
+# print
+PRINT = (exp) -> printer._pr_str exp, true
+
+# repl
+rep = (str) -> PRINT(EVAL(READ(str), {}))
+
+# repl loop
+while (line = readline.readline("user> ")) != null
+ continue if line == ""
+ try
+ console.log rep line
+ catch exc
+ continue if exc instanceof reader.BlankException
+ if exc.stack then console.log exc.stack
+ else console.log exc
+
+# vim: ts=2:sw=2
diff --git a/coffee/step2_eval.coffee b/coffee/step2_eval.coffee
new file mode 100644
index 0000000..1b3438e
--- /dev/null
+++ b/coffee/step2_eval.coffee
@@ -0,0 +1,52 @@
+readline = require "./node_readline.coffee"
+types = require "./types.coffee"
+reader = require "./reader.coffee"
+printer = require "./printer.coffee"
+
+# read
+READ = (str) -> reader.read_str str
+
+# eval
+eval_ast = (ast, env) ->
+ if types._symbol_Q(ast) then env[ast.name]
+ else if types._list_Q(ast) then ast.map((a) -> EVAL(a, env))
+ else if types._vector_Q(ast)
+ types._vector(ast.map((a) -> EVAL(a, env))...)
+ else if types._hash_map_Q(ast)
+ new_hm = {}
+ new_hm[k] = EVAL(ast[k],env) for k,v of ast
+ new_hm
+ else ast
+
+EVAL = (ast, env) ->
+ #console.log "EVAL:", printer._pr_str ast
+ if !types._list_Q ast then return eval_ast ast, env
+
+ # apply list
+ [f, args...] = eval_ast ast, env
+ f(args...)
+
+
+# print
+PRINT = (exp) -> printer._pr_str exp, true
+
+# repl
+repl_env = {}
+rep = (str) -> PRINT(EVAL(READ(str), repl_env))
+
+repl_env["+"] = (a,b) -> a+b
+repl_env["-"] = (a,b) -> a-b
+repl_env["*"] = (a,b) -> a*b
+repl_env["/"] = (a,b) -> a/b
+
+# repl loop
+while (line = readline.readline("user> ")) != null
+ continue if line == ""
+ try
+ console.log rep line
+ catch exc
+ continue if exc instanceof reader.BlankException
+ if exc.stack then console.log exc.stack
+ else console.log exc
+
+# vim: ts=2:sw=2
diff --git a/coffee/step3_env.coffee b/coffee/step3_env.coffee
new file mode 100644
index 0000000..a5398e7
--- /dev/null
+++ b/coffee/step3_env.coffee
@@ -0,0 +1,63 @@
+readline = require "./node_readline.coffee"
+types = require "./types.coffee"
+reader = require "./reader.coffee"
+printer = require "./printer.coffee"
+Env = require("./env.coffee").Env
+
+# read
+READ = (str) -> reader.read_str str
+
+# eval
+eval_ast = (ast, env) ->
+ if types._symbol_Q(ast) then env.get ast.name
+ else if types._list_Q(ast) then ast.map((a) -> EVAL(a, env))
+ else if types._vector_Q(ast)
+ types._vector(ast.map((a) -> EVAL(a, env))...)
+ else if types._hash_map_Q(ast)
+ new_hm = {}
+ new_hm[k] = EVAL(ast[k],env) for k,v of ast
+ new_hm
+ else ast
+
+EVAL = (ast, env) ->
+ #console.log "EVAL:", printer._pr_str ast
+ if !types._list_Q ast then return eval_ast ast, env
+
+ # apply list
+ [a0, a1, a2, a3] = ast
+ switch a0.name
+ when "def!"
+ env.set(a1.name, EVAL(a2, env))
+ when "let*"
+ let_env = new Env(env)
+ for k,i in a1 when i %% 2 == 0
+ let_env.set(a1[i].name, EVAL(a1[i+1], let_env))
+ EVAL(a2, let_env)
+ else
+ [f, args...] = eval_ast ast, env
+ f(args...)
+
+
+# print
+PRINT = (exp) -> printer._pr_str exp, true
+
+# repl
+repl_env = new Env()
+rep = (str) -> PRINT(EVAL(READ(str), repl_env))
+
+repl_env.set "+", (a,b) -> a+b
+repl_env.set "-", (a,b) -> a-b
+repl_env.set "*", (a,b) -> a*b
+repl_env.set "/", (a,b) -> a/b
+
+# repl loop
+while (line = readline.readline("user> ")) != null
+ continue if line == ""
+ try
+ console.log rep line
+ catch exc
+ continue if exc instanceof reader.BlankException
+ if exc.stack then console.log exc.stack
+ else console.log exc
+
+# vim: ts=2:sw=2
diff --git a/coffee/step4_if_fn_do.coffee b/coffee/step4_if_fn_do.coffee
new file mode 100644
index 0000000..037b58a
--- /dev/null
+++ b/coffee/step4_if_fn_do.coffee
@@ -0,0 +1,76 @@
+readline = require "./node_readline.coffee"
+types = require "./types.coffee"
+reader = require "./reader.coffee"
+printer = require "./printer.coffee"
+Env = require("./env.coffee").Env
+core = require("./core.coffee")
+
+# read
+READ = (str) -> reader.read_str str
+
+# eval
+eval_ast = (ast, env) ->
+ if types._symbol_Q(ast) then env.get ast.name
+ else if types._list_Q(ast) then ast.map((a) -> EVAL(a, env))
+ else if types._vector_Q(ast)
+ types._vector(ast.map((a) -> EVAL(a, env))...)
+ else if types._hash_map_Q(ast)
+ new_hm = {}
+ new_hm[k] = EVAL(ast[k],env) for k,v of ast
+ new_hm
+ else ast
+
+EVAL = (ast, env) ->
+ #console.log "EVAL:", printer._pr_str ast
+ if !types._list_Q ast then return eval_ast ast, env
+
+ # apply list
+ [a0, a1, a2, a3] = ast
+ switch a0.name
+ when "def!"
+ env.set(a1.name, EVAL(a2, env))
+ when "let*"
+ let_env = new Env(env)
+ for k,i in a1 when i %% 2 == 0
+ let_env.set(a1[i].name, EVAL(a1[i+1], let_env))
+ EVAL(a2, let_env)
+ when "do"
+ el = eval_ast(ast[1..], env)
+ el[el.length-1]
+ when "if"
+ cond = EVAL(a1, env)
+ if cond == null or cond == false
+ if a3? then EVAL(a3, env) else null
+ else
+ EVAL(a2, env)
+ when "fn*"
+ (args...) -> EVAL(a2, new Env(env, a1, args))
+ else
+ [f, args...] = eval_ast ast, env
+ f(args...)
+
+
+# print
+PRINT = (exp) -> printer._pr_str exp, true
+
+# repl
+repl_env = new Env()
+rep = (str) -> PRINT(EVAL(READ(str), repl_env))
+
+# core.coffee: defined using CoffeeScript
+repl_env.set k, v for k,v of core.ns
+
+# core.mal: defined using the language itself
+rep("(def! not (fn* (a) (if a false true)))");
+
+# repl loop
+while (line = readline.readline("user> ")) != null
+ continue if line == ""
+ try
+ console.log rep line
+ catch exc
+ continue if exc instanceof reader.BlankException
+ if exc.stack then console.log exc.stack
+ else console.log exc
+
+# vim: ts=2:sw=2
diff --git a/coffee/step5_tco.coffee b/coffee/step5_tco.coffee
new file mode 100644
index 0000000..4003e79
--- /dev/null
+++ b/coffee/step5_tco.coffee
@@ -0,0 +1,82 @@
+readline = require "./node_readline.coffee"
+types = require "./types.coffee"
+reader = require "./reader.coffee"
+printer = require "./printer.coffee"
+Env = require("./env.coffee").Env
+core = require("./core.coffee")
+
+# read
+READ = (str) -> reader.read_str str
+
+# eval
+eval_ast = (ast, env) ->
+ if types._symbol_Q(ast) then env.get ast.name
+ else if types._list_Q(ast) then ast.map((a) -> EVAL(a, env))
+ else if types._vector_Q(ast)
+ types._vector(ast.map((a) -> EVAL(a, env))...)
+ else if types._hash_map_Q(ast)
+ new_hm = {}
+ new_hm[k] = EVAL(ast[k],env) for k,v of ast
+ new_hm
+ else ast
+
+EVAL = (ast, env) ->
+ loop
+ #console.log "EVAL:", printer._pr_str ast
+ if !types._list_Q ast then return eval_ast ast, env
+
+ # apply list
+ [a0, a1, a2, a3] = ast
+ switch a0.name
+ when "def!"
+ return env.set(a1.name, EVAL(a2, env))
+ when "let*"
+ let_env = new Env(env)
+ for k,i in a1 when i %% 2 == 0
+ let_env.set(a1[i].name, EVAL(a1[i+1], let_env))
+ ast = a2
+ env = let_env
+ when "do"
+ eval_ast(ast[1..-2], env)
+ ast = ast[ast.length-1]
+ when "if"
+ cond = EVAL(a1, env)
+ if cond == null or cond == false
+ if a3? then ast = a3 else return null
+ else
+ ast = a2
+ when "fn*"
+ return types._function(EVAL, a2, env, a1)
+ else
+ [f, args...] = eval_ast ast, env
+ if types._function_Q(f)
+ ast = f.__ast__
+ env = f.__gen_env__(args)
+ else
+ return f(args...)
+
+
+# print
+PRINT = (exp) -> printer._pr_str exp, true
+
+# repl
+repl_env = new Env()
+rep = (str) -> PRINT(EVAL(READ(str), repl_env))
+
+# core.coffee: defined using CoffeeScript
+repl_env.set k, v for k,v of core.ns
+
+# core.mal: defined using the language itself
+rep("(def! not (fn* (a) (if a false true)))");
+
+# repl loop
+while (line = readline.readline("user> ")) != null
+ continue if line == ""
+ try
+ console.log rep line
+ catch exc
+ continue if exc instanceof reader.BlankException
+ if exc.stack then console.log exc.stack
+ else console.log exc
+
+# vim: ts=2:sw=2
diff --git a/coffee/step6_file.coffee b/coffee/step6_file.coffee
new file mode 100644
index 0000000..449b03e
--- /dev/null
+++ b/coffee/step6_file.coffee
@@ -0,0 +1,90 @@
+readline = require "./node_readline.coffee"
+types = require "./types.coffee"
+reader = require "./reader.coffee"
+printer = require "./printer.coffee"
+Env = require("./env.coffee").Env
+core = require("./core.coffee")
+
+# read
+READ = (str) -> reader.read_str str
+
+# eval
+eval_ast = (ast, env) ->
+ if types._symbol_Q(ast) then env.get ast.name
+ else if types._list_Q(ast) then ast.map((a) -> EVAL(a, env))
+ else if types._vector_Q(ast)
+ types._vector(ast.map((a) -> EVAL(a, env))...)
+ else if types._hash_map_Q(ast)
+ new_hm = {}
+ new_hm[k] = EVAL(ast[k],env) for k,v of ast
+ new_hm
+ else ast
+
+EVAL = (ast, env) ->
+ loop
+ #console.log "EVAL:", printer._pr_str ast
+ if !types._list_Q ast then return eval_ast ast, env
+
+ # apply list
+ [a0, a1, a2, a3] = ast
+ switch a0.name
+ when "def!"
+ return env.set(a1.name, EVAL(a2, env))
+ when "let*"
+ let_env = new Env(env)
+ for k,i in a1 when i %% 2 == 0
+ let_env.set(a1[i].name, EVAL(a1[i+1], let_env))
+ ast = a2
+ env = let_env
+ when "do"
+ eval_ast(ast[1..-2], env)
+ ast = ast[ast.length-1]
+ when "if"
+ cond = EVAL(a1, env)
+ if cond == null or cond == false
+ if a3? then ast = a3 else return null
+ else
+ ast = a2
+ when "fn*"
+ return types._function(EVAL, a2, env, a1)
+ else
+ [f, args...] = eval_ast ast, env
+ if types._function_Q(f)
+ ast = f.__ast__
+ env = f.__gen_env__(args)
+ else
+ return f(args...)
+
+
+# print
+PRINT = (exp) -> printer._pr_str exp, true
+
+# repl
+repl_env = new Env()
+rep = (str) -> PRINT(EVAL(READ(str), repl_env))
+
+# core.coffee: defined using CoffeeScript
+repl_env.set k, v for k,v of core.ns
+repl_env.set 'eval', (ast) -> EVAL(ast, repl_env)
+repl_env.set '*ARGV*', []
+
+# core.mal: defined using the language itself
+rep("(def! not (fn* (a) (if a false true)))");
+rep("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))");
+
+if process? && process.argv.length > 2
+ repl_env.set '*ARGV*', process.argv[3..]
+ rep('(load-file "' + process.argv[2] + '")')
+ process.exit 0
+
+# repl loop
+while (line = readline.readline("user> ")) != null
+ continue if line == ""
+ try
+ console.log rep line
+ catch exc
+ continue if exc instanceof reader.BlankException
+ if exc.stack then console.log exc.stack
+ else console.log exc
+
+# vim: ts=2:sw=2
diff --git a/coffee/step7_quote.coffee b/coffee/step7_quote.coffee
new file mode 100644
index 0000000..404e684
--- /dev/null
+++ b/coffee/step7_quote.coffee
@@ -0,0 +1,106 @@
+readline = require "./node_readline.coffee"
+types = require "./types.coffee"
+reader = require "./reader.coffee"
+printer = require "./printer.coffee"
+Env = require("./env.coffee").Env
+core = require("./core.coffee")
+
+# read
+READ = (str) -> reader.read_str str
+
+# eval
+is_pair = (x) -> types._sequential_Q(x) && x.length > 0
+
+quasiquote = (ast) ->
+ if !is_pair(ast) then [types._symbol('quote'), ast]
+ else if ast[0].name == 'unquote' then ast[1]
+ else if is_pair(ast[0]) && ast[0][0].name == 'splice-unquote'
+ [types._symbol('concat'), ast[0][1], quasiquote(ast[1..])]
+ else
+ [types._symbol('cons'), quasiquote(ast[0]), quasiquote(ast[1..])]
+
+
+
+eval_ast = (ast, env) ->
+ if types._symbol_Q(ast) then env.get ast.name
+ else if types._list_Q(ast) then ast.map((a) -> EVAL(a, env))
+ else if types._vector_Q(ast)
+ types._vector(ast.map((a) -> EVAL(a, env))...)
+ else if types._hash_map_Q(ast)
+ new_hm = {}
+ new_hm[k] = EVAL(ast[k],env) for k,v of ast
+ new_hm
+ else ast
+
+EVAL = (ast, env) ->
+ loop
+ #console.log "EVAL:", printer._pr_str ast
+ if !types._list_Q ast then return eval_ast ast, env
+
+ # apply list
+ [a0, a1, a2, a3] = ast
+ switch a0.name
+ when "def!"
+ return env.set(a1.name, EVAL(a2, env))
+ when "let*"
+ let_env = new Env(env)
+ for k,i in a1 when i %% 2 == 0
+ let_env.set(a1[i].name, EVAL(a1[i+1], let_env))
+ ast = a2
+ env = let_env
+ when "quote"
+ return a1
+ when "quasiquote"
+ ast = quasiquote(a1)
+ when "do"
+ eval_ast(ast[1..-2], env)
+ ast = ast[ast.length-1]
+ when "if"
+ cond = EVAL(a1, env)
+ if cond == null or cond == false
+ if a3? then ast = a3 else return null
+ else
+ ast = a2
+ when "fn*"
+ return types._function(EVAL, a2, env, a1)
+ else
+ [f, args...] = eval_ast ast, env
+ if types._function_Q(f)
+ ast = f.__ast__
+ env = f.__gen_env__(args)
+ else
+ return f(args...)
+
+
+# print
+PRINT = (exp) -> printer._pr_str exp, true
+
+# repl
+repl_env = new Env()
+rep = (str) -> PRINT(EVAL(READ(str), repl_env))
+
+# core.coffee: defined using CoffeeScript
+repl_env.set k, v for k,v of core.ns
+repl_env.set 'eval', (ast) -> EVAL(ast, repl_env)
+repl_env.set '*ARGV*', []
+
+# core.mal: defined using the language itself
+rep("(def! not (fn* (a) (if a false true)))");
+rep("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))");
+
+if process? && process.argv.length > 2
+ repl_env.set '*ARGV*', process.argv[3..]
+ rep('(load-file "' + process.argv[2] + '")')
+ process.exit 0
+
+# repl loop
+while (line = readline.readline("user> ")) != null
+ continue if line == ""
+ try
+ console.log rep line
+ catch exc
+ continue if exc instanceof reader.BlankException
+ if exc.stack then console.log exc.stack
+ else console.log exc
+
+# vim: ts=2:sw=2
diff --git a/coffee/step8_macros.coffee b/coffee/step8_macros.coffee
new file mode 100644
index 0000000..39404e7
--- /dev/null
+++ b/coffee/step8_macros.coffee
@@ -0,0 +1,126 @@
+readline = require "./node_readline.coffee"
+types = require "./types.coffee"
+reader = require "./reader.coffee"
+printer = require "./printer.coffee"
+Env = require("./env.coffee").Env
+core = require("./core.coffee")
+
+# read
+READ = (str) -> reader.read_str str
+
+# eval
+is_pair = (x) -> types._sequential_Q(x) && x.length > 0
+
+quasiquote = (ast) ->
+ if !is_pair(ast) then [types._symbol('quote'), ast]
+ else if ast[0].name == 'unquote' then ast[1]
+ else if is_pair(ast[0]) && ast[0][0].name == 'splice-unquote'
+ [types._symbol('concat'), ast[0][1], quasiquote(ast[1..])]
+ else
+ [types._symbol('cons'), quasiquote(ast[0]), quasiquote(ast[1..])]
+
+is_macro_call = (ast, env) ->
+ return types._list_Q(ast) && types._symbol_Q(ast[0]) &&
+ env.find(ast[0].name) && env.get(ast[0].name).__ismacro__
+
+macroexpand = (ast, env) ->
+ while is_macro_call(ast, env)
+ ast = env.get(ast[0].name)(ast[1..]...)
+ ast
+
+
+
+eval_ast = (ast, env) ->
+ if types._symbol_Q(ast) then env.get ast.name
+ else if types._list_Q(ast) then ast.map((a) -> EVAL(a, env))
+ else if types._vector_Q(ast)
+ types._vector(ast.map((a) -> EVAL(a, env))...)
+ else if types._hash_map_Q(ast)
+ new_hm = {}
+ new_hm[k] = EVAL(ast[k],env) for k,v of ast
+ new_hm
+ else ast
+
+EVAL = (ast, env) ->
+ loop
+ #console.log "EVAL:", printer._pr_str ast
+ if !types._list_Q ast then return eval_ast ast, env
+
+ # apply list
+ ast = macroexpand ast, env
+ if !types._list_Q ast then return ast
+
+ [a0, a1, a2, a3] = ast
+ switch a0.name
+ when "def!"
+ return env.set(a1.name, EVAL(a2, env))
+ when "let*"
+ let_env = new Env(env)
+ for k,i in a1 when i %% 2 == 0
+ let_env.set(a1[i].name, EVAL(a1[i+1], let_env))
+ ast = a2
+ env = let_env
+ when "quote"
+ return a1
+ when "quasiquote"
+ ast = quasiquote(a1)
+ when "defmacro!"
+ f = EVAL(a2, env)
+ f.__ismacro__ = true
+ return env.set(a1.name, f)
+ when "macroexpand"
+ return macroexpand(a1, env)
+ when "do"
+ eval_ast(ast[1..-2], env)
+ ast = ast[ast.length-1]
+ when "if"
+ cond = EVAL(a1, env)
+ if cond == null or cond == false
+ if a3? then ast = a3 else return null
+ else
+ ast = a2
+ when "fn*"
+ return types._function(EVAL, a2, env, a1)
+ else
+ [f, args...] = eval_ast ast, env
+ if types._function_Q(f)
+ ast = f.__ast__
+ env = f.__gen_env__(args)
+ else
+ return f(args...)
+
+
+# print
+PRINT = (exp) -> printer._pr_str exp, true
+
+# repl
+repl_env = new Env()
+rep = (str) -> PRINT(EVAL(READ(str), repl_env))
+
+# core.coffee: defined using CoffeeScript
+repl_env.set k, v for k,v of core.ns
+repl_env.set 'eval', (ast) -> EVAL(ast, repl_env)
+repl_env.set '*ARGV*', []
+
+# core.mal: defined using the language itself
+rep("(def! not (fn* (a) (if a false true)))");
+rep("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))");
+rep("(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw \"odd number of forms to cond\")) (cons 'cond (rest (rest xs)))))))")
+rep("(defmacro! or (fn* (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) `(let* (or_FIXME ~(first xs)) (if or_FIXME or_FIXME (or ~@(rest xs))))))))")
+
+if process? && process.argv.length > 2
+ repl_env.set '*ARGV*', process.argv[3..]
+ rep('(load-file "' + process.argv[2] + '")')
+ process.exit 0
+
+# repl loop
+while (line = readline.readline("user> ")) != null
+ continue if line == ""
+ try
+ console.log rep line
+ catch exc
+ continue if exc instanceof reader.BlankException
+ if exc.stack then console.log exc.stack
+ else console.log exc
+
+# vim: ts=2:sw=2
diff --git a/coffee/step9_try.coffee b/coffee/step9_try.coffee
new file mode 100644
index 0000000..d7919e7
--- /dev/null
+++ b/coffee/step9_try.coffee
@@ -0,0 +1,136 @@
+readline = require "./node_readline.coffee"
+types = require "./types.coffee"
+reader = require "./reader.coffee"
+printer = require "./printer.coffee"
+Env = require("./env.coffee").Env
+core = require("./core.coffee")
+
+# read
+READ = (str) -> reader.read_str str
+
+# eval
+is_pair = (x) -> types._sequential_Q(x) && x.length > 0
+
+quasiquote = (ast) ->
+ if !is_pair(ast) then [types._symbol('quote'), ast]
+ else if ast[0].name == 'unquote' then ast[1]
+ else if is_pair(ast[0]) && ast[0][0].name == 'splice-unquote'
+ [types._symbol('concat'), ast[0][1], quasiquote(ast[1..])]
+ else
+ [types._symbol('cons'), quasiquote(ast[0]), quasiquote(ast[1..])]
+
+is_macro_call = (ast, env) ->
+ return types._list_Q(ast) && types._symbol_Q(ast[0]) &&
+ env.find(ast[0].name) && env.get(ast[0].name).__ismacro__
+
+macroexpand = (ast, env) ->
+ while is_macro_call(ast, env)
+ ast = env.get(ast[0].name)(ast[1..]...)
+ ast
+
+
+
+eval_ast = (ast, env) ->
+ if types._symbol_Q(ast) then env.get ast.name
+ else if types._list_Q(ast) then ast.map((a) -> EVAL(a, env))
+ else if types._vector_Q(ast)
+ types._vector(ast.map((a) -> EVAL(a, env))...)
+ else if types._hash_map_Q(ast)
+ new_hm = {}
+ new_hm[k] = EVAL(ast[k],env) for k,v of ast
+ new_hm
+ else ast
+
+EVAL = (ast, env) ->
+ loop
+ #console.log "EVAL:", printer._pr_str ast
+ if !types._list_Q ast then return eval_ast ast, env
+
+ # apply list
+ ast = macroexpand ast, env
+ if !types._list_Q ast then return ast
+
+ [a0, a1, a2, a3] = ast
+ switch a0.name
+ when "def!"
+ return env.set(a1.name, EVAL(a2, env))
+ when "let*"
+ let_env = new Env(env)
+ for k,i in a1 when i %% 2 == 0
+ let_env.set(a1[i].name, EVAL(a1[i+1], let_env))
+ ast = a2
+ env = let_env
+ when "quote"
+ return a1
+ when "quasiquote"
+ ast = quasiquote(a1)
+ when "defmacro!"
+ f = EVAL(a2, env)
+ f.__ismacro__ = true
+ return env.set(a1.name, f)
+ when "macroexpand"
+ return macroexpand(a1, env)
+ when "try*"
+ try return EVAL(a1, env)
+ catch exc
+ if a2 && a2[0].name == "catch*"
+ if exc instanceof Error then exc = exc.message
+ return EVAL a2[2], new Env(env, [a2[1]], [exc])
+ else
+ throw exc
+ when "do"
+ eval_ast(ast[1..-2], env)
+ ast = ast[ast.length-1]
+ when "if"
+ cond = EVAL(a1, env)
+ if cond == null or cond == false
+ if a3? then ast = a3 else return null
+ else
+ ast = a2
+ when "fn*"
+ return types._function(EVAL, a2, env, a1)
+ else
+ [f, args...] = eval_ast ast, env
+ if types._function_Q(f)
+ ast = f.__ast__
+ env = f.__gen_env__(args)
+ else
+ return f(args...)
+
+
+# print
+PRINT = (exp) -> printer._pr_str exp, true
+
+# repl
+repl_env = new Env()
+rep = (str) -> PRINT(EVAL(READ(str), repl_env))
+
+# core.coffee: defined using CoffeeScript
+repl_env.set k, v for k,v of core.ns
+repl_env.set 'eval', (ast) -> EVAL(ast, repl_env)
+repl_env.set '*ARGV*', []
+
+# core.mal: defined using the language itself
+rep("(def! *host-language* \"CoffeeScript\")")
+rep("(def! not (fn* (a) (if a false true)))");
+rep("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))");
+rep("(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw \"odd number of forms to cond\")) (cons 'cond (rest (rest xs)))))))")
+rep("(defmacro! or (fn* (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) `(let* (or_FIXME ~(first xs)) (if or_FIXME or_FIXME (or ~@(rest xs))))))))")
+
+if process? && process.argv.length > 2
+ repl_env.set '*ARGV*', process.argv[3..]
+ rep('(load-file "' + process.argv[2] + '")')
+ process.exit 0
+
+# repl loop
+rep("(println (str \"Mal [\" *host-language* \"]\"))")
+while (line = readline.readline("user> ")) != null
+ continue if line == ""
+ try
+ console.log rep line
+ catch exc
+ continue if exc instanceof reader.BlankException
+ if exc.stack then console.log exc.stack
+ else console.log exc
+
+# vim: ts=2:sw=2
diff --git a/coffee/stepA_interop.coffee b/coffee/stepA_interop.coffee
new file mode 100644
index 0000000..aa9c5cc
--- /dev/null
+++ b/coffee/stepA_interop.coffee
@@ -0,0 +1,142 @@
+readline = require "./node_readline.coffee"
+types = require "./types.coffee"
+reader = require "./reader.coffee"
+printer = require "./printer.coffee"
+Env = require("./env.coffee").Env
+core = require("./core.coffee")
+
+# read
+READ = (str) -> reader.read_str str
+
+# eval
+is_pair = (x) -> types._sequential_Q(x) && x.length > 0
+
+quasiquote = (ast) ->
+ if !is_pair(ast) then [types._symbol('quote'), ast]
+ else if ast[0].name == 'unquote' then ast[1]
+ else if is_pair(ast[0]) && ast[0][0].name == 'splice-unquote'
+ [types._symbol('concat'), ast[0][1], quasiquote(ast[1..])]
+ else
+ [types._symbol('cons'), quasiquote(ast[0]), quasiquote(ast[1..])]
+
+is_macro_call = (ast, env) ->
+ return types._list_Q(ast) && types._symbol_Q(ast[0]) &&
+ env.find(ast[0].name) && env.get(ast[0].name).__ismacro__
+
+macroexpand = (ast, env) ->
+ while is_macro_call(ast, env)
+ ast = env.get(ast[0].name)(ast[1..]...)
+ ast
+
+
+
+eval_ast = (ast, env) ->
+ if types._symbol_Q(ast) then env.get ast.name
+ else if types._list_Q(ast) then ast.map((a) -> EVAL(a, env))
+ else if types._vector_Q(ast)
+ types._vector(ast.map((a) -> EVAL(a, env))...)
+ else if types._hash_map_Q(ast)
+ new_hm = {}
+ new_hm[k] = EVAL(ast[k],env) for k,v of ast
+ new_hm
+ else ast
+
+EVAL = (ast, env) ->
+ loop
+ #console.log "EVAL:", printer._pr_str ast
+ if !types._list_Q ast then return eval_ast ast, env
+
+ # apply list
+ ast = macroexpand ast, env
+ if !types._list_Q ast then return ast
+
+ [a0, a1, a2, a3] = ast
+ switch a0.name
+ when "def!"
+ return env.set(a1.name, EVAL(a2, env))
+ when "let*"
+ let_env = new Env(env)
+ for k,i in a1 when i %% 2 == 0
+ let_env.set(a1[i].name, EVAL(a1[i+1], let_env))
+ ast = a2
+ env = let_env
+ when "quote"
+ return a1
+ when "quasiquote"
+ ast = quasiquote(a1)
+ when "defmacro!"
+ f = EVAL(a2, env)
+ f.__ismacro__ = true
+ return env.set(a1.name, f)
+ when "macroexpand"
+ return macroexpand(a1, env)
+ when "try*"
+ try return EVAL(a1, env)
+ catch exc
+ if a2 && a2[0].name == "catch*"
+ if exc instanceof Error then exc = exc.message
+ return EVAL a2[2], new Env(env, [a2[1]], [exc])
+ else
+ throw exc
+ when "js*"
+ res = eval(a1.toString())
+ return if typeof(res) == 'undefined' then null else res
+ when "."
+ el = eval_ast(ast[2..], env)
+ return eval(a1.toString())(el...)
+ when "do"
+ eval_ast(ast[1..-2], env)
+ ast = ast[ast.length-1]
+ when "if"
+ cond = EVAL(a1, env)
+ if cond == null or cond == false
+ if a3? then ast = a3 else return null
+ else
+ ast = a2
+ when "fn*"
+ return types._function(EVAL, a2, env, a1)
+ else
+ [f, args...] = eval_ast ast, env
+ if types._function_Q(f)
+ ast = f.__ast__
+ env = f.__gen_env__(args)
+ else
+ return f(args...)
+
+
+# print
+PRINT = (exp) -> printer._pr_str exp, true
+
+# repl
+repl_env = new Env()
+rep = (str) -> PRINT(EVAL(READ(str), repl_env))
+
+# core.coffee: defined using CoffeeScript
+repl_env.set k, v for k,v of core.ns
+repl_env.set 'eval', (ast) -> EVAL(ast, repl_env)
+repl_env.set '*ARGV*', []
+
+# core.mal: defined using the language itself
+rep("(def! *host-language* \"CoffeeScript\")")
+rep("(def! not (fn* (a) (if a false true)))");
+rep("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))");
+rep("(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw \"odd number of forms to cond\")) (cons 'cond (rest (rest xs)))))))")
+rep("(defmacro! or (fn* (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) `(let* (or_FIXME ~(first xs)) (if or_FIXME or_FIXME (or ~@(rest xs))))))))")
+
+if process? && process.argv.length > 2
+ repl_env.set '*ARGV*', process.argv[3..]
+ rep('(load-file "' + process.argv[2] + '")')
+ process.exit 0
+
+# repl loop
+rep("(println (str \"Mal [\" *host-language* \"]\"))")
+while (line = readline.readline("user> ")) != null
+ continue if line == ""
+ try
+ console.log rep line
+ catch exc
+ continue if exc instanceof reader.BlankException
+ if exc.stack then console.log exc.stack
+ else console.log exc
+
+# vim: ts=2:sw=2
diff --git a/coffee/tests/stepA_interop.mal b/coffee/tests/stepA_interop.mal
new file mode 100644
index 0000000..f785292
--- /dev/null
+++ b/coffee/tests/stepA_interop.mal
@@ -0,0 +1,24 @@
+;; Testing basic bash interop
+
+(js* "7")
+;=>7
+
+(js* "'7'")
+;=>"7"
+
+(js* "[7,8,9]")
+;=>(7 8 9)
+
+(js* "console.log('hello');")
+; hello
+;=>nil
+
+(js* "foo=8;")
+(js* "foo;")
+;=>8
+
+(js* "['a','b','c'].map(function(x){return 'X'+x+'Y'}).join(' ')")
+;=>"XaY XbY XcY"
+
+(js* "[1,2,3].map(function(x){return 1+x})")
+;=>(2 3 4)
diff --git a/coffee/types.coffee b/coffee/types.coffee
new file mode 100644
index 0000000..8982120
--- /dev/null
+++ b/coffee/types.coffee
@@ -0,0 +1,115 @@
+Env = require("./env.coffee").Env
+
+E = exports
+
+# General functions
+E._obj_type = _obj_type = (obj) ->
+ if _symbol_Q(obj) then 'symbol'
+ else if _list_Q(obj) then 'list'
+ else if _vector_Q(obj) then 'vector'
+ else if _hash_map_Q(obj) then 'hash-map'
+ else if _nil_Q(obj) then 'nil'
+ else if _true_Q(obj) then 'true'
+ else if _false_Q(obj) then 'false'
+ else if _atom_Q(obj) then 'atom'
+ else
+ switch typeof obj
+ when 'number' then 'number'
+ when 'function' then 'function'
+ when 'string' then 'string'
+ else throw new Error "Unknown type '" + typeof(obj) + "'"
+
+E._sequential_Q = _sequential_Q = (o) -> _list_Q(o) or _vector_Q(o)
+
+E._equal_Q = _equal_Q = (a,b) ->
+ [ota, otb] = [_obj_type(a), _obj_type(b)]
+ if !(ota == otb or (_sequential_Q(a) && _sequential_Q(b)))
+ return false
+ switch (ota)
+ when 'symbol' then a.name == b.name
+ when 'list', 'vector'
+ return false if a.length != b.length
+ for av,i in a
+ return false if !_equal_Q(av, b[i])
+ true
+ when 'hash-map'
+ akeys = (key for key of a)
+ bkeys = (key for key of b)
+ return false if akeys.length != bkeys.length
+ for akey,i in akeys
+ bkey = bkeys[i]
+ return false if akey != bkey
+ return false if !_equal_Q(a[akey], b[bkey])
+ true
+ else a == b
+
+E._clone = _clone = (obj) ->
+ switch _obj_type(obj)
+ when 'list' then obj[0..-1]
+ when 'vector' then _vector(obj[0..-1]...)
+ when 'hash-map'
+ new_obj = {}
+ new_obj[k] = v for k,v of obj
+ new_obj
+ when 'function'
+ new_obj = (args...) -> obj(args...)
+ new_obj[k] = v for k,v of obj
+ new_obj
+ else throw new Error "clone called on non-collection" + _obj_type(obj)
+
+
+# Scalars
+E._nil_Q = _nil_Q = (o) -> o == null
+E._true_Q = _true_Q = (o) -> o == true
+E._false_Q = _false_Q = (o) -> o == false
+
+# Symbols
+class Symbol
+ constructor: (@name) ->
+E._symbol = (str) -> new Symbol str
+E._symbol_Q = _symbol_Q = (o) -> o instanceof Symbol
+
+# Functions
+E._function = (evalfn, ast, env, params) ->
+ fn = (args...) -> evalfn(ast, new Env(env, params, args))
+ fn.__ast__ = ast
+ fn.__gen_env__ = (args) -> new Env(env, params, args)
+ fn.__ismacro__ = false
+ fn
+E._function_Q = _function_Q = (o) -> !!o.__ast__
+
+# Lists
+E._list_Q = _list_Q = (o) -> Array.isArray(o) && !o.__isvector__
+
+# Vectors
+E._vector = _vector = (args...) ->
+ v = args
+ v.__isvector__ = true
+ v
+E._vector_Q = _vector_Q = (o) -> Array.isArray(o) && !!o.__isvector__
+
+# Hash Maps
+E._hash_map = (args...) ->
+ args = [{}].concat args
+ _assoc_BANG(args...)
+E._assoc_BANG = _assoc_BANG = (hm, args...) ->
+ if args.length %% 2 == 1
+ throw new Error "Odd number of hash map arguments"
+ hm[k] = args[i+1] for k, i in args when i %% 2 == 0
+ hm
+E._dissoc_BANG = (hm, args...) ->
+ delete hm[k] for k, i in args
+ hm
+E._hash_map_Q = _hash_map_Q = (o) ->
+ typeof o == "object" && !Array.isArray(o) &&
+ !(o == null) &&
+ !(o instanceof Atom)
+
+
+# Atoms
+class Atom
+ constructor: (@val) ->
+E._atom = (val) -> new Atom val
+E._atom_Q = _atom_Q = (o) -> o instanceof Atom
+
+# vim: ts=2:sw=2
diff --git a/tests/step8_macros.mal b/tests/step8_macros.mal
index f2f62dc..8773ba8 100644
--- a/tests/step8_macros.mal
+++ b/tests/step8_macros.mal
@@ -1,4 +1,14 @@
-;; Testing first function
+;; Testing nth, first and rest functions
+
+(nth '() 0)
+;=>nil
+(nth '(1) 0)
+;=>1
+(nth '(1 2) 1)
+;=>2
+(nth '(1 2) 2)
+;=>nil
+
(first '())
;=>nil
(first '(6))
@@ -6,7 +16,6 @@
(first '(7 8 9))
;=>7
-;; Testing rest function
(rest '())
;=>()
(rest '(6))