aboutsummaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
Diffstat (limited to 'js')
-rw-r--r--js/Makefile29
-rw-r--r--js/josh_readline.js402
-rw-r--r--js/node_readline.js38
-rw-r--r--js/package.json8
-rw-r--r--js/reader.js127
-rw-r--r--js/step0_repl.js42
-rw-r--r--js/step1_read_print.js47
-rw-r--r--js/step2_eval.js83
-rw-r--r--js/step3_env.js97
-rw-r--r--js/step4_if_fn_do.js112
-rw-r--r--js/step5_tco.js119
-rw-r--r--js/step6_file.js133
-rw-r--r--js/step7_quote.js154
-rw-r--r--js/step8_macros.js178
-rw-r--r--js/step9_interop.js184
-rw-r--r--js/stepA_more.js198
-rw-r--r--js/tests/common.js15
l---------js/tests/node_modules1
-rw-r--r--js/tests/reader.js68
-rw-r--r--js/tests/step5_tco.js22
-rw-r--r--js/tests/types.js94
-rw-r--r--js/types.js429
22 files changed, 2580 insertions, 0 deletions
diff --git a/js/Makefile b/js/Makefile
new file mode 100644
index 0000000..cb57644
--- /dev/null
+++ b/js/Makefile
@@ -0,0 +1,29 @@
+
+TESTS = tests/types.js tests/reader.js tests/step5_tco.js
+
+SOURCES = node_readline.js types.js reader.js stepA_more.js
+WEB_SOURCES = $(SOURCES:node_readline.js=josh_readline.js)
+
+all: mal.js mal_web.js
+
+mal.js: $(SOURCES)
+ echo "#!/usr/bin/env node" > $@
+ cat $+ | grep -v "= *require('./" >> $@
+ chmod +x $@
+
+mal_web.js: $(WEB_SOURCES)
+ cat $+ | grep -v "= *require('./" > $@
+
+clean:
+ rm -f mal.js mal_web.js
+
+.PHONY: stats tests $(TESTS)
+
+stats: $(SOURCES)
+ @wc $^
+
+tests: $(TESTS)
+
+$(TESTS):
+ @echo "Running $@"; \
+ node $@ || exit 1; \
diff --git a/js/josh_readline.js b/js/josh_readline.js
new file mode 100644
index 0000000..ff4d201
--- /dev/null
+++ b/js/josh_readline.js
@@ -0,0 +1,402 @@
+/* ------------------------------------------------------------------------*
+ * Copyright 2013 Arne F. Claassen
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *-------------------------------------------------------------------------*/
+
+var Josh = Josh || {};
+(function(root, $, _) {
+ Josh.Shell = function(config) {
+ config = config || {};
+
+ // instance fields
+ var _console = config.console || (Josh.Debug && root.console ? root.console : {
+ log: function() {
+ }
+ });
+ var _prompt = config.prompt || 'jsh$';
+ var _action = config.action || function(str) {
+ return "<div>No action defined for: " + str + "</div>";
+ };
+ var _shell_view_id = config.shell_view_id || 'shell-view';
+ var _shell_panel_id = config.shell_panel_id || 'shell-panel';
+ var _input_id = config.input_id || 'shell-cli';
+ var _blinktime = config.blinktime || 500;
+ var _history = config.history || new Josh.History();
+ var _readline = config.readline || new Josh.ReadLine({history: _history, console: _console});
+ var _active = false;
+ var _cursor_visible = false;
+ var _activationHandler;
+ var _deactivationHandler;
+ var _cmdHandlers = {
+ clear: {
+ exec: function(cmd, args, callback) {
+ $(id(_input_id)).parent().empty();
+ callback();
+ }
+ },
+ help: {
+ exec: function(cmd, args, callback) {
+ callback(self.templates.help({commands: commands()}));
+ }
+ },
+ history: {
+ exec: function(cmd, args, callback) {
+ if(args[0] == "-c") {
+ _history.clear();
+ callback();
+ return;
+ }
+ callback(self.templates.history({items: _history.items()}));
+ }
+ },
+ _default: {
+ exec: function(cmd, args, callback) {
+ callback(self.templates.bad_command({cmd: cmd}));
+ },
+ completion: function(cmd, arg, line, callback) {
+ if(!arg) {
+ arg = cmd;
+ }
+ return callback(self.bestMatch(arg, self.commands()))
+ }
+ }
+ };
+ var _line = {
+ text: '',
+ cursor: 0
+ };
+ var _searchMatch = '';
+ var _view, _panel;
+ var _promptHandler;
+ var _initializationHandler;
+ var _initialized;
+
+ // public methods
+ var self = {
+ commands: commands,
+ templates: {
+ history: _.template("<div><% _.each(items, function(cmd, i) { %><div><%- i %>&nbsp;<%- cmd %></div><% }); %></div>"),
+ help: _.template("<div><div><strong>Commands:</strong></div><% _.each(commands, function(cmd) { %><div>&nbsp;<%- cmd %></div><% }); %></div>"),
+ bad_command: _.template('<div><strong>Unrecognized command:&nbsp;</strong><%=cmd%></div>'),
+ input_cmd: _.template('<div id="<%- id %>"><span class="prompt"></span>&nbsp;<span class="input"><span class="left"/><span class="cursor"/><span class="right"/></span></div>'),
+ input_search: _.template('<div id="<%- id %>">(reverse-i-search)`<span class="searchterm"></span>\':&nbsp;<span class="input"><span class="left"/><span class="cursor"/><span class="right"/></span></div>'),
+ suggest: _.template("<div><% _.each(suggestions, function(suggestion) { %><div><%- suggestion %></div><% }); %></div>")
+ },
+ isActive: function() {
+ return _readline.isActive();
+ },
+ activate: function() {
+ if($(id(_shell_view_id)).length == 0) {
+ _active = false;
+ return;
+ }
+ _readline.activate();
+ },
+ deactivate: function() {
+ _console.log("deactivating");
+ _active = false;
+ _readline.deactivate();
+ },
+ setCommandHandler: function(cmd, cmdHandler) {
+ _cmdHandlers[cmd] = cmdHandler;
+ },
+ getCommandHandler: function(cmd) {
+ return _cmdHandlers[cmd];
+ },
+ setPrompt: function(prompt) {
+ _prompt = prompt;
+ if(!_active) {
+ return;
+ }
+ self.refresh();
+ },
+ onEOT: function(completionHandler) {
+ _readline.onEOT(completionHandler);
+ },
+ onCancel: function(completionHandler) {
+ _readline.onCancel(completionHandler);
+ },
+ onInitialize: function(completionHandler) {
+ _initializationHandler = completionHandler;
+ },
+ onActivate: function(completionHandler) {
+ _activationHandler = completionHandler;
+ },
+ onDeactivate: function(completionHandler) {
+ _deactivationHandler = completionHandler;
+ },
+ onNewPrompt: function(completionHandler) {
+ _promptHandler = completionHandler;
+ },
+ render: function() {
+ var text = _line.text || '';
+ var cursorIdx = _line.cursor || 0;
+ if(_searchMatch) {
+ cursorIdx = _searchMatch.cursoridx || 0;
+ text = _searchMatch.text || '';
+ $(id(_input_id) + ' .searchterm').text(_searchMatch.term);
+ }
+ var left = _.escape(text.substr(0, cursorIdx)).replace(/ /g, '&nbsp;');
+ var cursor = text.substr(cursorIdx, 1);
+ var right = _.escape(text.substr(cursorIdx + 1)).replace(/ /g, '&nbsp;');
+ $(id(_input_id) + ' .prompt').html(_prompt);
+ $(id(_input_id) + ' .input .left').html(left);
+ if(!cursor) {
+ $(id(_input_id) + ' .input .cursor').html('&nbsp;').css('textDecoration', 'underline');
+ } else {
+ $(id(_input_id) + ' .input .cursor').text(cursor).css('textDecoration', 'underline');
+ }
+ $(id(_input_id) + ' .input .right').html(right);
+ _cursor_visible = true;
+ self.scrollToBottom();
+ _console.log('rendered "' + text + '" w/ cursor at ' + cursorIdx);
+ },
+ refresh: function() {
+ $(id(_input_id)).replaceWith(self.templates.input_cmd({id:_input_id}));
+ self.render();
+ _console.log('refreshed ' + _input_id);
+
+ },
+ scrollToBottom: function() {
+ _panel.animate({scrollTop: _view.height()}, 0);
+ },
+ bestMatch: function(partial, possible) {
+ _console.log("bestMatch on partial '" + partial + "'");
+ var result = {
+ completion: null,
+ suggestions: []
+ };
+ if(!possible || possible.length == 0) {
+ return result;
+ }
+ var common = '';
+ if(!partial) {
+ if(possible.length == 1) {
+ result.completion = possible[0];
+ result.suggestions = possible;
+ return result;
+ }
+ if(!_.every(possible, function(x) {
+ return possible[0][0] == x[0]
+ })) {
+ result.suggestions = possible;
+ return result;
+ }
+ }
+ for(var i = 0; i < possible.length; i++) {
+ var option = possible[i];
+ if(option.slice(0, partial.length) == partial) {
+ result.suggestions.push(option);
+ if(!common) {
+ common = option;
+ _console.log("initial common:" + common);
+ } else if(option.slice(0, common.length) != common) {
+ _console.log("find common stem for '" + common + "' and '" + option + "'");
+ var j = partial.length;
+ while(j < common.length && j < option.length) {
+ if(common[j] != option[j]) {
+ common = common.substr(0, j);
+ break;
+ }
+ j++;
+ }
+ }
+ }
+ }
+ result.completion = common.substr(partial.length);
+ return result;
+ }
+ };
+
+ function id(id) {
+ return "#"+id;
+ }
+
+ function commands() {
+ return _.chain(_cmdHandlers).keys().filter(function(x) {
+ return x[0] != "_"
+ }).value();
+ }
+
+ function blinkCursor() {
+ if(!_active) {
+ return;
+ }
+ root.setTimeout(function() {
+ if(!_active) {
+ return;
+ }
+ _cursor_visible = !_cursor_visible;
+ if(_cursor_visible) {
+ $(id(_input_id) + ' .input .cursor').css('textDecoration', 'underline');
+ } else {
+ $(id(_input_id) + ' .input .cursor').css('textDecoration', '');
+ }
+ blinkCursor();
+ }, _blinktime);
+ }
+
+ function split(str) {
+ return _.filter(str.split(/\s+/), function(x) {
+ return x;
+ });
+ }
+
+ function getHandler(cmd) {
+ return _cmdHandlers[cmd] || _cmdHandlers._default;
+ }
+
+ function renderOutput(output, callback) {
+ if(output) {
+ $(id(_input_id)).after(output);
+ }
+ $(id(_input_id) + ' .input .cursor').css('textDecoration', '');
+ $(id(_input_id)).removeAttr('id');
+ $(id(_shell_view_id)).append(self.templates.input_cmd({id:_input_id}));
+ if(_promptHandler) {
+ return _promptHandler(function(prompt) {
+ self.setPrompt(prompt);
+ return callback();
+ });
+ }
+ return callback();
+ }
+
+ function activate() {
+ _console.log("activating shell");
+ if(!_view) {
+ _view = $(id(_shell_view_id));
+ }
+ if(!_panel) {
+ _panel = $(id(_shell_panel_id));
+ }
+ if($(id(_input_id)).length == 0) {
+ _view.append(self.templates.input_cmd({id:_input_id}));
+ }
+ self.refresh();
+ _active = true;
+ blinkCursor();
+ if(_promptHandler) {
+ _promptHandler(function(prompt) {
+ self.setPrompt(prompt);
+ })
+ }
+ if(_activationHandler) {
+ _activationHandler();
+ }
+ }
+
+ // init
+ _readline.onActivate(function() {
+ if(!_initialized) {
+ _initialized = true;
+ if(_initializationHandler) {
+ return _initializationHandler(activate);
+ }
+ }
+ return activate();
+ });
+ _readline.onDeactivate(function() {
+ if(_deactivationHandler) {
+ _deactivationHandler();
+ }
+ });
+ _readline.onChange(function(line) {
+ _line = line;
+ self.render();
+ });
+ _readline.onClear(function() {
+ _cmdHandlers.clear.exec(null, null, function() {
+ renderOutput(null, function() {
+ });
+ });
+ });
+ _readline.onSearchStart(function() {
+ $(id(_input_id)).replaceWith(self.templates.input_search({id:_input_id}));
+ _console.log('started search');
+ });
+ _readline.onSearchEnd(function() {
+ $(id(_input_id)).replaceWith(self.templates.input_cmd({id:_input_id}));
+ _searchMatch = null;
+ self.render();
+ _console.log("ended search");
+ });
+ _readline.onSearchChange(function(match) {
+ _searchMatch = match;
+ self.render();
+ });
+ _readline.onEnter(function(cmdtext, callback) {
+ _console.log("got command: " + cmdtext);
+ var result;
+ try {
+ result = "<div>" + _action(cmdtext) + "</div>";
+ } catch (e) {
+ result = "<div>" + e.stack + "</div>";
+ }
+ renderOutput(result, function() {
+ callback("");
+ });
+ });
+ _readline.onCompletion(function(line, callback) {
+ if(!line) {
+ return callback();
+ }
+ var text = line.text.substr(0, line.cursor);
+ var parts = split(text);
+
+ var cmd = parts.shift() || '';
+ var arg = parts.pop() || '';
+ _console.log("getting completion handler for " + cmd);
+ var handler = getHandler(cmd);
+ if(handler != _cmdHandlers._default && cmd && cmd == text) {
+
+ _console.log("valid cmd, no args: append space");
+ // the text to complete is just a valid command, append a space
+ return callback(' ');
+ }
+ if(!handler.completion) {
+ // handler has no completion function, so we can't complete
+ return callback();
+ }
+ _console.log("calling completion handler for " + cmd);
+ return handler.completion(cmd, arg, line, function(match) {
+ _console.log("completion: " + JSON.stringify(match));
+ if(!match) {
+ return callback();
+ }
+ if(match.suggestions && match.suggestions.length > 1) {
+ return renderOutput(self.templates.suggest({suggestions: match.suggestions}), function() {
+ callback(match.completion);
+ });
+ }
+ return callback(match.completion);
+ });
+ });
+ return self;
+ }
+})(this, $, _);
+
+var readline = {};
+readline.rlwrap = function(action) {
+ var history = new Josh.History({ key: 'josh.helloworld'});
+ var shell = Josh.Shell({history: history,
+ action: action});
+ var promptCounter = 0;
+ shell.onNewPrompt(function(callback) {
+ promptCounter++;
+ //callback("[" + promptCounter + "] $");
+ callback("user>");
+ });
+ shell.activate();
+}
diff --git a/js/node_readline.js b/js/node_readline.js
new file mode 100644
index 0000000..bfd1982
--- /dev/null
+++ b/js/node_readline.js
@@ -0,0 +1,38 @@
+// IMPORTANT: choose one
+var RL_LIB = "libreadline"; // NOTE: libreadline is GPL
+//var RL_LIB = "libedit";
+
+var HISTORY_FILE = require('path').join(process.env.HOME, '.mal-history');
+
+var rlwrap = {}; // namespace for this module in web context
+
+var ffi = require('ffi'),
+ fs = require('fs');
+
+var rllib = ffi.Library(RL_LIB, {
+ 'readline': [ 'string', [ 'string' ] ],
+ 'add_history': [ 'int', [ 'string' ] ]});
+
+var rl_history_loaded = false;
+
+exports.readline = rlwrap.readline = function(prompt) {
+ prompt = prompt || "user> ";
+
+ if (!rl_history_loaded) {
+ rl_history_loaded = true;
+ var lines = fs.readFileSync(HISTORY_FILE).toString().split("\n");
+ // Max of 2000 lines
+ lines = lines.slice(Math.max(lines.length - 2000, 0));
+ for (var i=0; i<lines.length; i++) {
+ if (lines[i]) { rllib.add_history(lines[i]); }
+ }
+ }
+
+ var line = rllib.readline(prompt);
+ if (line) {
+ rllib.add_history(line);
+ fs.appendFileSync(HISTORY_FILE, line + "\n");
+ }
+
+ return line;
+}
diff --git a/js/package.json b/js/package.json
new file mode 100644
index 0000000..976e6ae
--- /dev/null
+++ b/js/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "mal",
+ "version": "0.0.1",
+ "description": "Make a Lisp (mal) language implemented in Javascript",
+ "dependencies": {
+ "ffi": "1.2.x"
+ }
+}
diff --git a/js/reader.js b/js/reader.js
new file mode 100644
index 0000000..da51088
--- /dev/null
+++ b/js/reader.js
@@ -0,0 +1,127 @@
+// Node vs browser behavior
+var reader = {};
+if (typeof module !== 'undefined') {
+ var types = require('./types');
+} else {
+ var exports = reader;
+}
+
+function Reader(tokens) {
+ // copy
+ this.tokens = tokens.map(function (a) { return a; });
+ this.position = 0;
+}
+Reader.prototype.next = function() { return this.tokens[this.position++]; }
+Reader.prototype.peek = function() { return this.tokens[this.position]; }
+
+function tokenize(str) {
+ var re = /[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"|;.*|[^\s\[\]{}('"`,;)]*)/g;
+ var results = [];
+ while ((match = re.exec(str)[1]) != '') {
+ if (match[0] === ';') { continue; }
+ results.push(match);
+ }
+ return results;
+}
+
+function read_atom (reader) {
+ var token = reader.next();
+ //console.log("read_atom:", token);
+ if (token.match(/^-?[0-9]+$/)) {
+ return parseInt(token,10) // integer
+ } else if (token.match(/^-?[0-9][0-9.]*$/)) {
+ return parseFloat(token,10); // float
+ } else if (token[0] === "\"") {
+ return token.slice(1,token.length-1).replace(/\\"/g, '"'); // string
+ } else if (token === "nil") {
+ return null;
+ } else if (token === "true") {
+ return true;
+ } else if (token === "false") {
+ return false;
+ } else {
+ return types.symbol(token); // symbol
+ }
+}
+
+// read list of tokens
+function read_list(reader, start, end) {
+ start = start || '(';
+ end = end || ')';
+ var ast = [];
+ var token = reader.next();
+ if (token !== start) {
+ throw new Error("expected '" + start + "'");
+ }
+ while ((token = reader.peek()) !== end) {
+ if (!token) {
+ throw new Error("expected '" + end + "', got EOF");
+ }
+ ast.push(read_form(reader));
+ }
+ reader.next();
+ return ast;
+}
+
+// read vector of tokens
+function read_vector(reader) {
+ var lst = read_list(reader, '[', ']');
+ return types.vector.apply(types.vector, lst);
+}
+
+// read hash-map key/value pairs
+function read_hash_map(reader) {
+ var lst = read_list(reader, '{', '}');
+ return types.hash_map.apply(types.hash_map, lst);
+}
+
+function read_form(reader) {
+ var token = reader.peek();
+ switch (token) {
+ // reader macros/transforms
+ case ';': return null; // Ignore comments
+ case '\'': reader.next();
+ return [types.symbol('quote'), read_form(reader)];
+ case '`': reader.next();
+ return [types.symbol('quasiquote'), read_form(reader)];
+ case '~': reader.next();
+ return [types.symbol('unquote'), read_form(reader)];
+ case '~@': reader.next();
+ return [types.symbol('splice-unquote'), read_form(reader)];
+ case '^': reader.next();
+ var meta = read_form(reader);
+ return [types.symbol('with-meta'), read_form(reader), meta];
+ case '@': reader.next();
+ return [types.symbol('deref'), read_form(reader)];
+
+ // list
+ case ')': throw new Error("unexpected ')'");
+ case '(': return read_list(reader);
+
+ // vector
+ case ']': throw new Error("unexpected ']'");
+ case '[': return read_vector(reader);
+
+ // hash-map
+ case '}': throw new Error("unexpected '}'");
+ case '{': return read_hash_map(reader);
+
+ // atom
+ default: return read_atom(reader);
+ }
+}
+
+function BlankException(msg) {
+}
+
+function read_str(str) {
+ var tokens = tokenize(str);
+ if (tokens.length === 0) { throw new BlankException(); }
+ return read_form(new Reader(tokens))
+}
+
+exports.Reader = reader.Reader = Reader;
+exports.BlankException = reader.BlankException = BlankException;
+exports.tokenize = reader.tokenize = tokenize;
+exports.read_form = reader.read_form = read_form;
+exports.read_str = reader.read_str = read_str;
diff --git a/js/step0_repl.js b/js/step0_repl.js
new file mode 100644
index 0000000..5fa10f2
--- /dev/null
+++ b/js/step0_repl.js
@@ -0,0 +1,42 @@
+if (typeof module !== 'undefined') {
+ var readline = require('./node_readline');
+}
+
+// read
+function READ(str) {
+ return str;
+}
+
+// eval
+function EVAL(ast, env) {
+ return eval(ast);
+}
+
+// print
+function PRINT(exp) {
+ return exp;
+}
+
+// repl
+var rep = function(str) { return PRINT(EVAL(READ(str), {})); };
+
+if (typeof require === 'undefined') {
+ // Asynchronous browser mode
+ readline.rlwrap(function(line) { return rep(line); },
+ function(exc) {
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ });
+} else if (require.main === module) {
+ // Synchronous node.js commandline mode
+ while (true) {
+ var line = readline.readline("user> ");
+ if (line === null) { break; }
+ try {
+ if (line) { console.log(rep(line)); }
+ } catch (exc) {
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ }
+ }
+} else {
+ exports.rep = rep;
+}
diff --git a/js/step1_read_print.js b/js/step1_read_print.js
new file mode 100644
index 0000000..ee027d7
--- /dev/null
+++ b/js/step1_read_print.js
@@ -0,0 +1,47 @@
+var types = require('./types');
+var reader = require('./reader');
+if (typeof module !== 'undefined') {
+ var readline = require('./node_readline');
+}
+
+// read
+function READ(str) {
+ return reader.read_str(str);
+}
+
+// eval
+function EVAL(ast, env) {
+ return ast;
+}
+
+// print
+function PRINT(exp) {
+ return types._pr_str(exp, true);
+}
+
+// repl
+var re = function(str) { return EVAL(READ(str), {}); };
+var rep = function(str) { return PRINT(EVAL(READ(str), {})); };
+
+if (typeof require === 'undefined') {
+ // Asynchronous browser mode
+ readline.rlwrap(function(line) { return rep(line); },
+ function(exc) {
+ if (exc instanceof reader.BlankException) { return; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ });
+} else if (require.main === module) {
+ // Synchronous node.js commandline mode
+ while (true) {
+ var line = readline.readline("user> ");
+ if (line === null) { break; }
+ try {
+ if (line) { console.log(rep(line)); }
+ } catch (exc) {
+ if (exc instanceof reader.BlankException) { continue; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ }
+ }
+} else {
+ exports.rep = rep;
+}
diff --git a/js/step2_eval.js b/js/step2_eval.js
new file mode 100644
index 0000000..f2cb8b1
--- /dev/null
+++ b/js/step2_eval.js
@@ -0,0 +1,83 @@
+var types = require('./types');
+var reader = require('./reader');
+if (typeof module !== 'undefined') {
+ var readline = require('./node_readline');
+}
+
+// read
+function READ(str) {
+ return reader.read_str(str);
+}
+
+// eval
+function eval_ast(ast, env) {
+ if (types.symbol_Q(ast)) {
+ return env[ast];
+ } else if (types.list_Q(ast)) {
+ return ast.map(function(a) { return EVAL(a, env); });
+ } else if (types.vector_Q(ast)) {
+ var v = ast.map(function(a) { return EVAL(a, env); });
+ v.__isvector__ = true;
+ return v;
+ } else if (types.hash_map_Q(ast)) {
+ var new_hm = {};
+ for (k in ast) {
+ new_hm[EVAL(k, env)] = EVAL(ast[k], env);
+ }
+ return new_hm;
+ } else {
+ return ast;
+ }
+}
+
+function _EVAL(ast, env) {
+ if (!types.list_Q(ast)) {
+ return eval_ast(ast, env);
+ }
+
+ // apply list
+ var el = eval_ast(ast, env), f = el[0];
+ return f.apply(f, el.slice(1));
+}
+
+function EVAL(ast, env) {
+ var result = _EVAL(ast, env);
+ return (typeof result !== "undefined") ? result : null;
+}
+
+// print
+function PRINT(exp) {
+ return types._pr_str(exp, true);
+}
+
+// repl
+repl_env = {};
+var rep = function(str) { return PRINT(EVAL(READ(str), repl_env)); };
+
+repl_env['+'] = function(a,b){return a+b;};
+repl_env['-'] = function(a,b){return a-b;};
+repl_env['*'] = function(a,b){return a*b;};
+repl_env['/'] = function(a,b){return a/b;};
+
+if (typeof require === 'undefined') {
+ // Asynchronous browser mode
+ readline.rlwrap(function(line) { return rep(line); },
+ function(exc) {
+ if (exc instanceof reader.BlankException) { return; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ });
+} else if (require.main === module) {
+ // Synchronous node.js commandline mode
+ while (true) {
+ var line = readline.readline("user> ");
+ if (line === null) { break; }
+ try {
+ if (line) { console.log(rep(line)); }
+ } catch (exc) {
+ if (exc instanceof reader.BlankException) { continue; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ }
+ }
+} else {
+ exports.rep = rep;
+}
diff --git a/js/step3_env.js b/js/step3_env.js
new file mode 100644
index 0000000..5b6e802
--- /dev/null
+++ b/js/step3_env.js
@@ -0,0 +1,97 @@
+var types = require('./types');
+var reader = require('./reader');
+if (typeof module !== 'undefined') {
+ var readline = require('./node_readline');
+}
+
+// read
+function READ(str) {
+ return reader.read_str(str);
+}
+
+// eval
+function eval_ast(ast, env) {
+ if (types.symbol_Q(ast)) {
+ return env.get(ast);
+ } else if (types.list_Q(ast)) {
+ return ast.map(function(a) { return EVAL(a, env); });
+ } else if (types.vector_Q(ast)) {
+ var v = ast.map(function(a) { return EVAL(a, env); });
+ v.__isvector__ = true;
+ return v;
+ } else if (types.hash_map_Q(ast)) {
+ var new_hm = {};
+ for (k in ast) {
+ new_hm[EVAL(k, env)] = EVAL(ast[k], env);
+ }
+ return new_hm;
+ } else {
+ return ast;
+ }
+}
+
+function _EVAL(ast, env) {
+ if (!types.list_Q(ast)) {
+ return eval_ast(ast, env);
+ }
+
+ // apply list
+ var a0 = ast[0], a1 = ast[1], a2 = ast[2], a3 = ast[3];
+ switch (a0.value) {
+ case "def!":
+ var res = EVAL(a2, env);
+ return env.set(a1, res);
+ case "let*":
+ var let_env = new types.Env(env);
+ for (var i=0; i < a1.length; i+=2) {
+ let_env.set(a1[i].value, EVAL(a1[i+1], let_env));
+ }
+ return EVAL(a2, let_env);
+ default:
+ var el = eval_ast(ast, env), f = el[0];
+ return f.apply(f, el.slice(1));
+ }
+}
+
+function EVAL(ast, env) {
+ var result = _EVAL(ast, env);
+ return (typeof result !== "undefined") ? result : null;
+}
+
+// print
+function PRINT(exp) {
+ return types._pr_str(exp, true);
+}
+
+// repl
+var repl_env = new types.Env();
+var rep = function(str) { return PRINT(EVAL(READ(str), repl_env)); };
+_ref = function (k,v) { repl_env.set(k, v); }
+
+_ref('+', function(a,b){return a+b;});
+_ref('-', function(a,b){return a-b;});
+_ref('*', function(a,b){return a*b;});
+_ref('/', function(a,b){return a/b;});
+
+if (typeof require === 'undefined') {
+ // Asynchronous browser mode
+ readline.rlwrap(function(line) { return rep(line); },
+ function(exc) {
+ if (exc instanceof reader.BlankException) { return; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ });
+} else if (require.main === module) {
+ // Synchronous node.js commandline mode
+ while (true) {
+ var line = readline.readline("user> ");
+ if (line === null) { break; }
+ try {
+ if (line) { console.log(rep(line)); }
+ } catch (exc) {
+ if (exc instanceof reader.BlankException) { continue; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ }
+ }
+} else {
+ exports.rep = rep;
+}
diff --git a/js/step4_if_fn_do.js b/js/step4_if_fn_do.js
new file mode 100644
index 0000000..d33ec04
--- /dev/null
+++ b/js/step4_if_fn_do.js
@@ -0,0 +1,112 @@
+var types = require('./types');
+var reader = require('./reader');
+if (typeof module !== 'undefined') {
+ var readline = require('./node_readline');
+}
+
+// read
+function READ(str) {
+ return reader.read_str(str);
+}
+
+// eval
+function eval_ast(ast, env) {
+ if (types.symbol_Q(ast)) {
+ return env.get(ast);
+ } else if (types.list_Q(ast)) {
+ return ast.map(function(a) { return EVAL(a, env); });
+ } else if (types.vector_Q(ast)) {
+ var v = ast.map(function(a) { return EVAL(a, env); });
+ v.__isvector__ = true;
+ return v;
+ } else if (types.hash_map_Q(ast)) {
+ var new_hm = {};
+ for (k in ast) {
+ new_hm[EVAL(k, env)] = EVAL(ast[k], env);
+ }
+ return new_hm;
+ } else {
+ return ast;
+ }
+}
+
+function _EVAL(ast, env) {
+ if (!types.list_Q(ast)) {
+ return eval_ast(ast, env);
+ }
+
+ // apply list
+ var a0 = ast[0], a1 = ast[1], a2 = ast[2], a3 = ast[3];
+ switch (a0.value) {
+ case "def!":
+ var res = EVAL(a2, env);
+ return env.set(a1, res);
+ case "let*":
+ var let_env = new types.Env(env);
+ for (var i=0; i < a1.length; i+=2) {
+ let_env.set(a1[i].value, EVAL(a1[i+1], let_env));
+ }
+ return EVAL(a2, let_env);
+ case "do":
+ var el = eval_ast(ast.slice(1), env);
+ return el[el.length-1];
+ case "if":
+ var cond = EVAL(a1, env);
+ if (cond === null || cond === false) {
+ return typeof a3 !== "undefined" ? EVAL(a3, env) : null;
+ } else {
+ return EVAL(a2, env);
+ }
+ case "fn*":
+ return function() {
+ return EVAL(a2, new types.Env(env, a1, arguments));
+ };
+ default:
+ var el = eval_ast(ast, env), f = el[0];
+ return f.apply(f, el.slice(1));
+ }
+}
+
+function EVAL(ast, env) {
+ var result = _EVAL(ast, env);
+ return (typeof result !== "undefined") ? result : null;
+}
+
+// print
+function PRINT(exp) {
+ return types._pr_str(exp, true);
+}
+
+// repl
+var repl_env = new types.Env();
+var rep = function(str) { return PRINT(EVAL(READ(str), repl_env)); };
+_ref = function (k,v) { repl_env.set(k, v); }
+
+// Import types functions
+for (var n in types.ns) { repl_env.set(n, types.ns[n]); }
+
+// Defined using the language itself
+rep("(def! not (fn* (a) (if a false true)))");
+
+if (typeof require === 'undefined') {
+ // Asynchronous browser mode
+ readline.rlwrap(function(line) { return rep(line); },
+ function(exc) {
+ if (exc instanceof reader.BlankException) { return; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ });
+} else if (require.main === module) {
+ // Synchronous node.js commandline mode
+ while (true) {
+ var line = readline.readline("user> ");
+ if (line === null) { break; }
+ try {
+ if (line) { console.log(rep(line)); }
+ } catch (exc) {
+ if (exc instanceof reader.BlankException) { continue; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ }
+ }
+} else {
+ exports.rep = rep;
+}
diff --git a/js/step5_tco.js b/js/step5_tco.js
new file mode 100644
index 0000000..20a9583
--- /dev/null
+++ b/js/step5_tco.js
@@ -0,0 +1,119 @@
+var types = require('./types');
+var reader = require('./reader');
+if (typeof module !== 'undefined') {
+ var readline = require('./node_readline');
+}
+
+// read
+function READ(str) {
+ return reader.read_str(str);
+}
+
+// eval
+function eval_ast(ast, env) {
+ if (types.symbol_Q(ast)) {
+ return env.get(ast);
+ } else if (types.list_Q(ast)) {
+ return ast.map(function(a) { return EVAL(a, env); });
+ } else if (types.vector_Q(ast)) {
+ var v = ast.map(function(a) { return EVAL(a, env); });
+ v.__isvector__ = true;
+ return v;
+ } else if (types.hash_map_Q(ast)) {
+ var new_hm = {};
+ for (k in ast) {
+ new_hm[EVAL(k, env)] = EVAL(ast[k], env);
+ }
+ return new_hm;
+ } else {
+ return ast;
+ }
+}
+
+function _EVAL(ast, env) {
+ while (true) {
+ if (!types.list_Q(ast)) {
+ return eval_ast(ast, env);
+ }
+
+ // apply list
+ var a0 = ast[0], a1 = ast[1], a2 = ast[2], a3 = ast[3];
+ switch (a0.value) {
+ case "def!":
+ var res = EVAL(a2, env);
+ return env.set(a1, res);
+ case "let*":
+ var let_env = new types.Env(env);
+ for (var i=0; i < a1.length; i+=2) {
+ let_env.set(a1[i].value, EVAL(a1[i+1], let_env));
+ }
+ return EVAL(a2, let_env);
+ case "do":
+ eval_ast(ast.slice(1, -1), env);
+ ast = ast[ast.length-1];
+ break;
+ case "if":
+ var cond = EVAL(a1, env);
+ if (cond === null || cond === false) {
+ ast = (typeof a3 !== "undefined") ? a3 : null;
+ } else {
+ ast = a2;
+ }
+ break;
+ case "fn*":
+ return types.new_function(EVAL, a2, env, a1);
+ default:
+ var el = eval_ast(ast, env), f = el[0], meta = f.__meta__;
+ if (meta && meta.exp) {
+ ast = meta.exp;
+ env = new types.Env(meta.env, meta.params, el.slice(1));
+ } else {
+ return f.apply(f, el.slice(1));
+ }
+ }
+ }
+}
+
+function EVAL(ast, env) {
+ var result = _EVAL(ast, env);
+ return (typeof result !== "undefined") ? result : null;
+}
+
+// print
+function PRINT(exp) {
+ return types._pr_str(exp, true);
+}
+
+// repl
+var repl_env = new types.Env();
+var rep = function(str) { return PRINT(EVAL(READ(str), repl_env)); };
+_ref = function (k,v) { repl_env.set(k, v); }
+
+// Import types functions
+for (var n in types.ns) { repl_env.set(n, types.ns[n]); }
+
+// Defined using the language itself
+rep("(def! not (fn* (a) (if a false true)))");
+
+if (typeof require === 'undefined') {
+ // Asynchronous browser mode
+ readline.rlwrap(function(line) { return rep(line); },
+ function(exc) {
+ if (exc instanceof reader.BlankException) { return; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ });
+} else if (require.main === module) {
+ // Synchronous node.js commandline mode
+ while (true) {
+ var line = readline.readline("user> ");
+ if (line === null) { break; }
+ try {
+ if (line) { console.log(rep(line)); }
+ } catch (exc) {
+ if (exc instanceof reader.BlankException) { continue; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ }
+ }
+} else {
+ exports.rep = rep;
+}
diff --git a/js/step6_file.js b/js/step6_file.js
new file mode 100644
index 0000000..b9ec187
--- /dev/null
+++ b/js/step6_file.js
@@ -0,0 +1,133 @@
+var types = require('./types');
+var reader = require('./reader');
+if (typeof module !== 'undefined') {
+ var readline = require('./node_readline');
+}
+
+// read
+function READ(str) {
+ return reader.read_str(str);
+}
+
+// eval
+function eval_ast(ast, env) {
+ if (types.symbol_Q(ast)) {
+ return env.get(ast);
+ } else if (types.list_Q(ast)) {
+ return ast.map(function(a) { return EVAL(a, env); });
+ } else if (types.vector_Q(ast)) {
+ var v = ast.map(function(a) { return EVAL(a, env); });
+ v.__isvector__ = true;
+ return v;
+ } else if (types.hash_map_Q(ast)) {
+ var new_hm = {};
+ for (k in ast) {
+ new_hm[EVAL(k, env)] = EVAL(ast[k], env);
+ }
+ return new_hm;
+ } else {
+ return ast;
+ }
+}
+
+function _EVAL(ast, env) {
+ while (true) {
+ if (!types.list_Q(ast)) {
+ return eval_ast(ast, env);
+ }
+
+ // apply list
+ var a0 = ast[0], a1 = ast[1], a2 = ast[2], a3 = ast[3];
+ switch (a0.value) {
+ case "def!":
+ var res = EVAL(a2, env);
+ return env.set(a1, res);
+ case "let*":
+ var let_env = new types.Env(env);
+ for (var i=0; i < a1.length; i+=2) {
+ let_env.set(a1[i].value, EVAL(a1[i+1], let_env));
+ }
+ return EVAL(a2, let_env);
+ case "do":
+ eval_ast(ast.slice(1, -1), env);
+ ast = ast[ast.length-1];
+ break;
+ case "if":
+ var cond = EVAL(a1, env);
+ if (cond === null || cond === false) {
+ ast = (typeof a3 !== "undefined") ? a3 : null;
+ } else {
+ ast = a2;
+ }
+ break;
+ case "fn*":
+ return types.new_function(EVAL, a2, env, a1);
+ default:
+ var el = eval_ast(ast, env), f = el[0], meta = f.__meta__;
+ if (meta && meta.exp) {
+ ast = meta.exp;
+ env = new types.Env(meta.env, meta.params, el.slice(1));
+ } else {
+ return f.apply(f, el.slice(1));
+ }
+ }
+ }
+}
+
+function EVAL(ast, env) {
+ var result = _EVAL(ast, env);
+ return (typeof result !== "undefined") ? result : null;
+}
+
+// print
+function PRINT(exp) {
+ return types._pr_str(exp, true);
+}
+
+// repl
+var repl_env = new types.Env();
+var rep = function(str) { return PRINT(EVAL(READ(str), repl_env)); };
+_ref = function (k,v) { repl_env.set(k, v); }
+
+// Import types functions
+for (var n in types.ns) { repl_env.set(n, types.ns[n]); }
+
+_ref('read-string', reader.read_str);
+_ref('eval', function(ast) { return EVAL(ast, repl_env); });
+_ref('slurp', function(f) {
+ return require('fs').readFileSync(f, 'utf-8');
+});
+_ref('slurp-do', function(f) {
+ return '(do ' + require('fs').readFileSync(f, 'utf-8') + ')';
+});
+
+// Defined using the language itself
+rep("(def! not (fn* (a) (if a false true)))");
+rep("(def! load-file (fn* (f) (eval (read-string (slurp-do f)))))");
+
+if (typeof process !== 'undefined' && process.argv.length > 2) {
+ for (var i=2; i < process.argv.length; i++) {
+ rep('(load-file "' + process.argv[i] + '")');
+ }
+} else if (typeof require === 'undefined') {
+ // Asynchronous browser mode
+ readline.rlwrap(function(line) { return rep(line); },
+ function(exc) {
+ if (exc instanceof reader.BlankException) { return; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ });
+} else if (require.main === module) {
+ // Synchronous node.js commandline mode
+ while (true) {
+ var line = readline.readline("user> ");
+ if (line === null) { break; }
+ try {
+ if (line) { console.log(rep(line)); }
+ } catch (exc) {
+ if (exc instanceof reader.BlankException) { continue; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ }
+ }
+} else {
+ exports.rep = rep;
+}
diff --git a/js/step7_quote.js b/js/step7_quote.js
new file mode 100644
index 0000000..832e47b
--- /dev/null
+++ b/js/step7_quote.js
@@ -0,0 +1,154 @@
+var types = require('./types');
+var reader = require('./reader');
+if (typeof module !== 'undefined') {
+ var readline = require('./node_readline');
+}
+
+// read
+function READ(str) {
+ return reader.read_str(str);
+}
+
+// eval
+function is_pair(x) {
+ return types.sequential_Q(x) && x.length > 0;
+}
+
+function quasiquote(ast) {
+ if (!is_pair(ast)) {
+ return [types.symbol("quote"), ast];
+ } else if (ast[0].value === 'unquote') {
+ return ast[1];
+ } else if (is_pair(ast[0]) && ast[0][0].value === 'splice-unquote') {
+ return [types.symbol("concat"), ast[0][1], quasiquote(ast.slice(1))];
+ } else {
+ return [types.symbol("cons"), quasiquote(ast[0]), quasiquote(ast.slice(1))];
+ }
+}
+
+function eval_ast(ast, env) {
+ if (types.symbol_Q(ast)) {
+ return env.get(ast);
+ } else if (types.list_Q(ast)) {
+ return ast.map(function(a) { return EVAL(a, env); });
+ } else if (types.vector_Q(ast)) {
+ var v = ast.map(function(a) { return EVAL(a, env); });
+ v.__isvector__ = true;
+ return v;
+ } else if (types.hash_map_Q(ast)) {
+ var new_hm = {};
+ for (k in ast) {
+ new_hm[EVAL(k, env)] = EVAL(ast[k], env);
+ }
+ return new_hm;
+ } else {
+ return ast;
+ }
+}
+
+function _EVAL(ast, env) {
+ while (true) {
+ //console.log("EVAL:", types._pr_str(ast, true));
+ if (!types.list_Q(ast)) {
+ return eval_ast(ast, env);
+ }
+
+ // apply list
+ var a0 = ast[0], a1 = ast[1], a2 = ast[2], a3 = ast[3];
+ switch (a0.value) {
+ case "def!":
+ var res = EVAL(a2, env);
+ return env.set(a1, res);
+ case "let*":
+ var let_env = new types.Env(env);
+ for (var i=0; i < a1.length; i+=2) {
+ let_env.set(a1[i].value, EVAL(a1[i+1], let_env));
+ }
+ return EVAL(a2, let_env);
+ case "quote":
+ return a1;
+ case "quasiquote":
+ return EVAL(quasiquote(a1), env);
+ case "do":
+ eval_ast(ast.slice(1, -1), env);
+ ast = ast[ast.length-1];
+ break;
+ case "if":
+ var cond = EVAL(a1, env);
+ if (cond === null || cond === false) {
+ ast = (typeof a3 !== "undefined") ? a3 : null;
+ } else {
+ ast = a2;
+ }
+ break;
+ case "fn*":
+ return types.new_function(EVAL, a2, env, a1);
+ default:
+ var el = eval_ast(ast, env), f = el[0], meta = f.__meta__;
+ if (meta && meta.exp) {
+ ast = meta.exp;
+ env = new types.Env(meta.env, meta.params, el.slice(1));
+ } else {
+ return f.apply(f, el.slice(1));
+ }
+ }
+ }
+}
+
+function EVAL(ast, env) {
+ var result = _EVAL(ast, env);
+ return (typeof result !== "undefined") ? result : null;
+}
+
+// print
+function PRINT(exp) {
+ return types._pr_str(exp, true);
+}
+
+// repl
+var repl_env = new types.Env();
+var rep = function(str) { return PRINT(EVAL(READ(str), repl_env)); };
+_ref = function (k,v) { repl_env.set(k, v); }
+
+// Import types functions
+for (var n in types.ns) { repl_env.set(n, types.ns[n]); }
+
+_ref('read-string', reader.read_str);
+_ref('eval', function(ast) { return EVAL(ast, repl_env); });
+_ref('slurp', function(f) {
+ return require('fs').readFileSync(f, 'utf-8');
+});
+_ref('slurp-do', function(f) {
+ return '(do ' + require('fs').readFileSync(f, 'utf-8') + ')';
+});
+
+// Defined using the language itself
+rep("(def! not (fn* (a) (if a false true)))");
+rep("(def! load-file (fn* (f) (eval (read-string (slurp-do f)))))");
+
+if (typeof process !== 'undefined' && process.argv.length > 2) {
+ for (var i=2; i < process.argv.length; i++) {
+ rep('(load-file "' + process.argv[i] + '")');
+ }
+} else if (typeof require === 'undefined') {
+ // Asynchronous browser mode
+ readline.rlwrap(function(line) { return rep(line); },
+ function(exc) {
+ if (exc instanceof reader.BlankException) { return; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ });
+} else if (require.main === module) {
+ // Synchronous node.js commandline mode
+ while (true) {
+ var line = readline.readline("user> ");
+ if (line === null) { break; }
+ try {
+ if (line) { console.log(rep(line)); }
+ } catch (exc) {
+ if (exc instanceof reader.BlankException) { continue; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ }
+ }
+} else {
+ exports.rep = rep;
+}
diff --git a/js/step8_macros.js b/js/step8_macros.js
new file mode 100644
index 0000000..766f750
--- /dev/null
+++ b/js/step8_macros.js
@@ -0,0 +1,178 @@
+var types = require('./types');
+var reader = require('./reader');
+if (typeof module !== 'undefined') {
+ var readline = require('./node_readline');
+}
+
+// read
+function READ(str) {
+ return reader.read_str(str);
+}
+
+// eval
+function is_pair(x) {
+ return types.sequential_Q(x) && x.length > 0;
+}
+
+function quasiquote(ast) {
+ if (!is_pair(ast)) {
+ return [types.symbol("quote"), ast];
+ } else if (ast[0].value === 'unquote') {
+ return ast[1];
+ } else if (is_pair(ast[0]) && ast[0][0].value === 'splice-unquote') {
+ return [types.symbol("concat"), ast[0][1], quasiquote(ast.slice(1))];
+ } else {
+ return [types.symbol("cons"), quasiquote(ast[0]), quasiquote(ast.slice(1))];
+ }
+}
+
+function is_macro_call(ast, env) {
+ return types.list_Q(ast) &&
+ types.symbol_Q(ast[0]) &&
+ env.find(ast[0].value) &&
+ env.get(ast[0].value)._ismacro_;
+}
+
+function macroexpand(ast, env) {
+ while (is_macro_call(ast, env)) {
+ var mac = env.get(ast[0]);
+ ast = mac.apply(mac, ast.slice(1));
+ }
+ return ast;
+}
+
+function eval_ast(ast, env) {
+ if (types.symbol_Q(ast)) {
+ return env.get(ast);
+ } else if (types.list_Q(ast)) {
+ return ast.map(function(a) { return EVAL(a, env); });
+ } else if (types.vector_Q(ast)) {
+ var v = ast.map(function(a) { return EVAL(a, env); });
+ v.__isvector__ = true;
+ return v;
+ } else if (types.hash_map_Q(ast)) {
+ var new_hm = {};
+ for (k in ast) {
+ new_hm[EVAL(k, env)] = EVAL(ast[k], env);
+ }
+ return new_hm;
+ } else {
+ return ast;
+ }
+}
+
+function _EVAL(ast, env) {
+ while (true) {
+ //console.log("EVAL:", types._pr_str(ast, true));
+ if (!types.list_Q(ast)) {
+ return eval_ast(ast, env);
+ }
+
+ // apply list
+ ast = macroexpand(ast, env);
+ if (!types.list_Q(ast)) { return ast; }
+
+ var a0 = ast[0], a1 = ast[1], a2 = ast[2], a3 = ast[3];
+ switch (a0.value) {
+ case "def!":
+ var res = EVAL(a2, env);
+ return env.set(a1, res);
+ case "let*":
+ var let_env = new types.Env(env);
+ for (var i=0; i < a1.length; i+=2) {
+ let_env.set(a1[i].value, EVAL(a1[i+1], let_env));
+ }
+ return EVAL(a2, let_env);
+ case "quote":
+ return a1;
+ case "quasiquote":
+ return EVAL(quasiquote(a1), env);
+ case 'defmacro!':
+ var func = EVAL(a2, env);
+ func._ismacro_ = true;
+ return env.set(a1, func);
+ case 'macroexpand':
+ return macroexpand(a1, env);
+ case "do":
+ eval_ast(ast.slice(1, -1), env);
+ ast = ast[ast.length-1];
+ break;
+ case "if":
+ var cond = EVAL(a1, env);
+ if (cond === null || cond === false) {
+ ast = (typeof a3 !== "undefined") ? a3 : null;
+ } else {
+ ast = a2;
+ }
+ break;
+ case "fn*":
+ return types.new_function(EVAL, a2, env, a1);
+ default:
+ var el = eval_ast(ast, env), f = el[0], meta = f.__meta__;
+ if (meta && meta.exp) {
+ ast = meta.exp;
+ env = new types.Env(meta.env, meta.params, el.slice(1));
+ } else {
+ return f.apply(f, el.slice(1));
+ }
+ }
+ }
+}
+
+function EVAL(ast, env) {
+ var result = _EVAL(ast, env);
+ return (typeof result !== "undefined") ? result : null;
+}
+
+// print
+function PRINT(exp) {
+ return types._pr_str(exp, true);
+}
+
+// repl
+var repl_env = new types.Env();
+var rep = function(str) { return PRINT(EVAL(READ(str), repl_env)); };
+_ref = function (k,v) { repl_env.set(k, v); }
+
+// Import types functions
+for (var n in types.ns) { repl_env.set(n, types.ns[n]); }
+
+_ref('read-string', reader.read_str);
+_ref('eval', function(ast) { return EVAL(ast, repl_env); });
+_ref('slurp', function(f) {
+ return require('fs').readFileSync(f, 'utf-8');
+});
+_ref('slurp-do', function(f) {
+ return '(do ' + require('fs').readFileSync(f, 'utf-8') + ')';
+});
+
+// Defined using the language itself
+rep("(def! not (fn* (a) (if a false true)))");
+rep("(def! load-file (fn* (f) (eval (read-string (slurp-do f)))))");
+
+if (typeof process !== 'undefined' && process.argv.length > 2) {
+ for (var i=2; i < process.argv.length; i++) {
+ rep('(load-file "' + process.argv[i] + '")');
+ }
+} else if (typeof require === 'undefined') {
+ // Asynchronous browser mode
+ readline.rlwrap(function(line) { return rep(line); },
+ function(exc) {
+ if (exc instanceof reader.BlankException) { return; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ });
+} else if (require.main === module) {
+ // Synchronous node.js commandline mode
+ while (true) {
+ var line = readline.readline("user> ");
+ if (line === null) { break; }
+ try {
+ if (line) { console.log(rep(line)); }
+ } catch (exc) {
+ if (exc instanceof reader.BlankException) { continue; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ }
+ }
+} else {
+ exports.rep = rep;
+}
diff --git a/js/step9_interop.js b/js/step9_interop.js
new file mode 100644
index 0000000..a811c52
--- /dev/null
+++ b/js/step9_interop.js
@@ -0,0 +1,184 @@
+var types = require('./types');
+var reader = require('./reader');
+if (typeof module !== 'undefined') {
+ var readline = require('./node_readline');
+}
+
+// read
+function READ(str) {
+ return reader.read_str(str);
+}
+
+// eval
+function is_pair(x) {
+ return types.sequential_Q(x) && x.length > 0;
+}
+
+function quasiquote(ast) {
+ if (!is_pair(ast)) {
+ return [types.symbol("quote"), ast];
+ } else if (ast[0].value === 'unquote') {
+ return ast[1];
+ } else if (is_pair(ast[0]) && ast[0][0].value === 'splice-unquote') {
+ return [types.symbol("concat"), ast[0][1], quasiquote(ast.slice(1))];
+ } else {
+ return [types.symbol("cons"), quasiquote(ast[0]), quasiquote(ast.slice(1))];
+ }
+}
+
+function is_macro_call(ast, env) {
+ return types.list_Q(ast) &&
+ types.symbol_Q(ast[0]) &&
+ env.find(ast[0].value) &&
+ env.get(ast[0].value)._ismacro_;
+}
+
+function macroexpand(ast, env) {
+ while (is_macro_call(ast, env)) {
+ var mac = env.get(ast[0]);
+ ast = mac.apply(mac, ast.slice(1));
+ }
+ return ast;
+}
+
+function eval_ast(ast, env) {
+ if (types.symbol_Q(ast)) {
+ return env.get(ast);
+ } else if (types.list_Q(ast)) {
+ return ast.map(function(a) { return EVAL(a, env); });
+ } else if (types.vector_Q(ast)) {
+ var v = ast.map(function(a) { return EVAL(a, env); });
+ v.__isvector__ = true;
+ return v;
+ } else if (types.hash_map_Q(ast)) {
+ var new_hm = {};
+ for (k in ast) {
+ new_hm[EVAL(k, env)] = EVAL(ast[k], env);
+ }
+ return new_hm;
+ } else {
+ return ast;
+ }
+}
+
+function _EVAL(ast, env) {
+ while (true) {
+ //console.log("EVAL:", types._pr_str(ast, true));
+ if (!types.list_Q(ast)) {
+ return eval_ast(ast, env);
+ }
+
+ // apply list
+ ast = macroexpand(ast, env);
+ if (!types.list_Q(ast)) { return ast; }
+
+ var a0 = ast[0], a1 = ast[1], a2 = ast[2], a3 = ast[3];
+ switch (a0.value) {
+ case "def!":
+ var res = EVAL(a2, env);
+ return env.set(a1, res);
+ case "let*":
+ var let_env = new types.Env(env);
+ for (var i=0; i < a1.length; i+=2) {
+ let_env.set(a1[i].value, EVAL(a1[i+1], let_env));
+ }
+ return EVAL(a2, let_env);
+ case "quote":
+ return a1;
+ case "quasiquote":
+ return EVAL(quasiquote(a1), env);
+ case 'defmacro!':
+ var func = EVAL(a2, env);
+ func._ismacro_ = true;
+ return env.set(a1, func);
+ case 'macroexpand':
+ return macroexpand(a1, env);
+ case "js*":
+ return eval(a1.toString());
+ case ".":
+ var el = eval_ast(ast.slice(2), env),
+ f = eval(a1.toString());
+ return f.apply(f, el);
+ case "do":
+ eval_ast(ast.slice(1, -1), env);
+ ast = ast[ast.length-1];
+ break;
+ case "if":
+ var cond = EVAL(a1, env);
+ if (cond === null || cond === false) {
+ ast = (typeof a3 !== "undefined") ? a3 : null;
+ } else {
+ ast = a2;
+ }
+ break;
+ case "fn*":
+ return types.new_function(EVAL, a2, env, a1);
+ default:
+ var el = eval_ast(ast, env), f = el[0], meta = f.__meta__;
+ if (meta && meta.exp) {
+ ast = meta.exp;
+ env = new types.Env(meta.env, meta.params, el.slice(1));
+ } else {
+ return f.apply(f, el.slice(1));
+ }
+ }
+ }
+}
+
+function EVAL(ast, env) {
+ var result = _EVAL(ast, env);
+ return (typeof result !== "undefined") ? result : null;
+}
+
+// print
+function PRINT(exp) {
+ return types._pr_str(exp, true);
+}
+
+// repl
+var repl_env = new types.Env();
+var rep = function(str) { return PRINT(EVAL(READ(str), repl_env)); };
+_ref = function (k,v) { repl_env.set(k, v); }
+
+// Import types functions
+for (var n in types.ns) { repl_env.set(n, types.ns[n]); }
+
+_ref('read-string', reader.read_str);
+_ref('eval', function(ast) { return EVAL(ast, repl_env); });
+_ref('slurp', function(f) {
+ return require('fs').readFileSync(f, 'utf-8');
+});
+_ref('slurp-do', function(f) {
+ return '(do ' + require('fs').readFileSync(f, 'utf-8') + ')';
+});
+
+// Defined using the language itself
+rep("(def! not (fn* (a) (if a false true)))");
+rep("(def! load-file (fn* (f) (eval (read-string (slurp-do f)))))");
+
+if (typeof process !== 'undefined' && process.argv.length > 2) {
+ for (var i=2; i < process.argv.length; i++) {
+ rep('(load-file "' + process.argv[i] + '")');
+ }
+} else if (typeof require === 'undefined') {
+ // Asynchronous browser mode
+ readline.rlwrap(function(line) { return rep(line); },
+ function(exc) {
+ if (exc instanceof reader.BlankException) { return; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ });
+} else if (require.main === module) {
+ // Synchronous node.js commandline mode
+ while (true) {
+ var line = readline.readline("user> ");
+ if (line === null) { break; }
+ try {
+ if (line) { console.log(rep(line)); }
+ } catch (exc) {
+ if (exc instanceof reader.BlankException) { continue; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ }
+ }
+} else {
+ exports.rep = rep;
+}
diff --git a/js/stepA_more.js b/js/stepA_more.js
new file mode 100644
index 0000000..fca3744
--- /dev/null
+++ b/js/stepA_more.js
@@ -0,0 +1,198 @@
+var types = require('./types');
+var reader = require('./reader');
+if (typeof module !== 'undefined') {
+ var readline = require('./node_readline');
+}
+
+// read
+function READ(str) {
+ return reader.read_str(str);
+}
+
+// eval
+function is_pair(x) {
+ return types.sequential_Q(x) && x.length > 0;
+}
+
+function quasiquote(ast) {
+ if (!is_pair(ast)) {
+ return [types.symbol("quote"), ast];
+ } else if (ast[0].value === 'unquote') {
+ return ast[1];
+ } else if (is_pair(ast[0]) && ast[0][0].value === 'splice-unquote') {
+ return [types.symbol("concat"), ast[0][1], quasiquote(ast.slice(1))];
+ } else {
+ return [types.symbol("cons"), quasiquote(ast[0]), quasiquote(ast.slice(1))];
+ }
+}
+
+function is_macro_call(ast, env) {
+ return types.list_Q(ast) &&
+ types.symbol_Q(ast[0]) &&
+ env.find(ast[0].value) &&
+ env.get(ast[0].value)._ismacro_;
+}
+
+function macroexpand(ast, env) {
+ while (is_macro_call(ast, env)) {
+ var mac = env.get(ast[0]);
+ ast = mac.apply(mac, ast.slice(1));
+ }
+ return ast;
+}
+
+function eval_ast(ast, env) {
+ if (types.symbol_Q(ast)) {
+ return env.get(ast);
+ } else if (types.list_Q(ast)) {
+ return ast.map(function(a) { return EVAL(a, env); });
+ } else if (types.vector_Q(ast)) {
+ var v = ast.map(function(a) { return EVAL(a, env); });
+ v.__isvector__ = true;
+ return v;
+ } else if (types.hash_map_Q(ast)) {
+ var new_hm = {};
+ for (k in ast) {
+ new_hm[EVAL(k, env)] = EVAL(ast[k], env);
+ }
+ return new_hm;
+ } else {
+ return ast;
+ }
+}
+
+function _EVAL(ast, env) {
+ while (true) {
+ //console.log("EVAL:", types._pr_str(ast, true));
+ if (!types.list_Q(ast)) {
+ return eval_ast(ast, env);
+ }
+
+ // apply list
+ ast = macroexpand(ast, env);
+ if (!types.list_Q(ast)) { return ast; }
+
+ var a0 = ast[0], a1 = ast[1], a2 = ast[2], a3 = ast[3];
+ switch (a0.value) {
+ case "def!":
+ var res = EVAL(a2, env);
+ return env.set(a1, res);
+ case "let*":
+ var let_env = new types.Env(env);
+ for (var i=0; i < a1.length; i+=2) {
+ let_env.set(a1[i].value, EVAL(a1[i+1], let_env));
+ }
+ return EVAL(a2, let_env);
+ case "quote":
+ return a1;
+ case "quasiquote":
+ return EVAL(quasiquote(a1), env);
+ case 'defmacro!':
+ var func = EVAL(a2, env);
+ func._ismacro_ = true;
+ return env.set(a1, func);
+ case 'macroexpand':
+ return macroexpand(a1, env);
+ case "js*":
+ return eval(a1.toString());
+ case ".":
+ var el = eval_ast(ast.slice(2), env),
+ f = eval(a1.toString());
+ return f.apply(f, el);
+ case "try*":
+ try {
+ return EVAL(a1, env);
+ } catch (exc) {
+ if (a2 && a2[0].value === "catch*") {
+ if (exc instanceof Error) { exc = exc.message; }
+ return EVAL(a2[2], new types.Env(env, [a2[1]], [exc]));
+ } else {
+ throw exc;
+ }
+ }
+ case "do":
+ eval_ast(ast.slice(1, -1), env);
+ ast = ast[ast.length-1];
+ break;
+ case "if":
+ var cond = EVAL(a1, env);
+ if (cond === null || cond === false) {
+ ast = (typeof a3 !== "undefined") ? a3 : null;
+ } else {
+ ast = a2;
+ }
+ break;
+ case "fn*":
+ return types.new_function(EVAL, a2, env, a1);
+ default:
+ var el = eval_ast(ast, env), f = el[0], meta = f.__meta__;
+ if (meta && meta.exp) {
+ ast = meta.exp;
+ env = new types.Env(meta.env, meta.params, el.slice(1));
+ } else {
+ return f.apply(f, el.slice(1));
+ }
+ }
+ }
+}
+
+function EVAL(ast, env) {
+ var result = _EVAL(ast, env);
+ return (typeof result !== "undefined") ? result : null;
+}
+
+// print
+function PRINT(exp) {
+ return types._pr_str(exp, true);
+}
+
+// repl
+var repl_env = new types.Env();
+var rep = function(str) { return PRINT(EVAL(READ(str), repl_env)); };
+_ref = function (k,v) { repl_env.set(k, v); }
+
+// Import types functions
+for (var n in types.ns) { repl_env.set(n, types.ns[n]); }
+
+_ref('readline', readline.readline)
+_ref('read-string', reader.read_str);
+_ref('eval', function(ast) { return EVAL(ast, repl_env); });
+_ref('slurp', function(f) {
+ return require('fs').readFileSync(f, 'utf-8');
+});
+_ref('slurp-do', function(f) {
+ return '(do ' + require('fs').readFileSync(f, 'utf-8') + ')';
+});
+
+// Defined using the language itself
+rep("(def! not (fn* (a) (if a false true)))");
+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))))))))");
+rep("(def! load-file (fn* (f) (eval (read-string (slurp-do f)))))");
+
+if (typeof process !== 'undefined' && process.argv.length > 2) {
+ for (var i=2; i < process.argv.length; i++) {
+ rep('(load-file "' + process.argv[i] + '")');
+ }
+} else if (typeof require === 'undefined') {
+ // Asynchronous browser mode
+ readline.rlwrap(function(line) { return rep(line); },
+ function(exc) {
+ if (exc instanceof reader.BlankException) { return; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ });
+} else if (require.main === module) {
+ // Synchronous node.js commandline mode
+ while (true) {
+ var line = readline.readline("user> ");
+ if (line === null) { break; }
+ try {
+ if (line) { console.log(rep(line)); }
+ } catch (exc) {
+ if (exc instanceof reader.BlankException) { continue; }
+ if (exc.stack) { console.log(exc.stack); } else { console.log(exc); }
+ }
+ }
+} else {
+ exports.rep = rep;
+}
diff --git a/js/tests/common.js b/js/tests/common.js
new file mode 100644
index 0000000..a95d79b
--- /dev/null
+++ b/js/tests/common.js
@@ -0,0 +1,15 @@
+fs = require('fs');
+assert = require('assert');
+
+function assert_eq(a, b) {
+ GLOBAL.assert.deepEqual(a, b, a + " !== " + b);
+}
+
+function load(file) {
+ console.log(process.cwd());
+ //process.chdir('../');
+ eval(fs.readFileSync(file,'utf8'));
+}
+
+exports.assert_eq = assert_eq;
+exports.load = load;
diff --git a/js/tests/node_modules b/js/tests/node_modules
new file mode 120000
index 0000000..b870225
--- /dev/null
+++ b/js/tests/node_modules
@@ -0,0 +1 @@
+../ \ No newline at end of file
diff --git a/js/tests/reader.js b/js/tests/reader.js
new file mode 100644
index 0000000..2aa81c6
--- /dev/null
+++ b/js/tests/reader.js
@@ -0,0 +1,68 @@
+common = require('./common.js');
+types = require('../types');
+reader = require('../reader');
+var assert_eq = common.assert_eq,
+ read_str = reader.read_str,
+ nth = types.ns.nth;
+
+console.log("Testing read of constants/strings");
+assert_eq(2,read_str('2'));
+assert_eq(12345,read_str('12345'));
+assert_eq(12345,read_str('12345 "abc"'));
+assert_eq('abc',read_str('"abc"'));
+assert_eq('a string (with parens)',read_str('"a string (with parens)"'));
+
+console.log("Testing read of symbols");
+assert(types.symbol_Q(read_str('abc')));
+assert_eq('abc',read_str('abc').value);
+assert_eq('.',read_str('.').value);
+
+console.log("Testing READ_STR of strings");
+assert_eq('a string',read_str('"a string"'));
+assert_eq('a string (with parens)',read_str('"a string (with parens)"'));
+assert_eq('a string',read_str('"a string"()'));
+assert_eq('a string',read_str('"a string"123'));
+assert_eq('a string',read_str('"a string"abc'));
+assert_eq('',read_str('""'));
+assert_eq('abc ',read_str('"abc "'));
+assert_eq(' abc',read_str('" abc"'));
+assert_eq('$abc',read_str('"$abc"'));
+assert_eq('abc$()',read_str('"abc$()"'));
+assert_eq('"xyz"',read_str('"\\"xyz\\""'));
+
+
+console.log("Testing READ_STR of lists");
+assert_eq(2,types.ns.count(read_str('(2 3)')));
+assert_eq(2,types.ns.first(read_str('(2 3)')));
+assert_eq(3,types.ns.first(types.ns.rest(read_str('(2 3)'))));
+L = read_str('(+ 1 2 "str1" "string (with parens) and \'single quotes\'")');
+assert_eq(5,types.ns.count(L));
+assert_eq('str1',nth(L,3));
+assert_eq('string (with parens) and \'single quotes\'',nth(L,4));
+assert_eq([2,3],read_str('(2 3)'));
+assert_eq([2,3, 'string (with parens)'],read_str('(2 3 "string (with parens)")'));
+
+
+console.log("Testing READ_STR of quote/quasiquote");
+assert_eq('quote',nth(read_str('\'1'),0).value);
+assert_eq(1,nth(read_str('\'1'),1));
+assert_eq('quote',nth(read_str('\'(1 2 3)'),0).value);
+assert_eq(3,nth(nth(read_str('\'(1 2 3)'),1),2));
+
+assert_eq('quasiquote',nth(read_str('`1'),0).value);
+assert_eq(1,nth(read_str('`1'),1));
+assert_eq('quasiquote',nth(read_str('`(1 2 3)'),0).value);
+assert_eq(3,nth(nth(read_str('`(1 2 3)'),1),2));
+
+assert_eq('unquote',nth(read_str('~1'),0).value);
+assert_eq(1,nth(read_str('~1'),1));
+assert_eq('unquote',nth(read_str('~(1 2 3)'),0).value);
+assert_eq(3,nth(nth(read_str('~(1 2 3)'),1),2));
+
+assert_eq('splice-unquote',nth(read_str('~@1'),0).value);
+assert_eq(1,nth(read_str('~@1'),1));
+assert_eq('splice-unquote',nth(read_str('~@(1 2 3)'),0).value);
+assert_eq(3,nth(nth(read_str('~@(1 2 3)'),1),2));
+
+
+console.log("All tests completed");
diff --git a/js/tests/step5_tco.js b/js/tests/step5_tco.js
new file mode 100644
index 0000000..60c0576
--- /dev/null
+++ b/js/tests/step5_tco.js
@@ -0,0 +1,22 @@
+common = require('./common.js');
+var assert_eq = common.assert_eq;
+var rep = require('../step5_tco.js').rep;
+
+console.log("Testing Stack Exhaustion Function");
+rep('(def! sum-to (fn* (n) (if (= n 0) 0 (+ n (sum-to (- n 1))))))');
+try {
+ rep('(sum-to 10000)');
+ throw new Error("Did not get expected stack exhaustion");
+} catch (e) {
+ if (e.toString().match(/RangeError/)) {
+ console.log("Got expected stack exhaustion");
+ } else {
+ throw new Error("Unexpected error: " + e);
+ }
+}
+
+console.log("Testing Tail Call Optimization/Elimination");
+rep('(def! sum2 (fn* (n acc) (if (= n 0) acc (sum2 (- n 1) (+ n acc)))))');
+rep('(sum2 10000 0)');
+
+console.log("All tests completed");
diff --git a/js/tests/types.js b/js/tests/types.js
new file mode 100644
index 0000000..71b276f
--- /dev/null
+++ b/js/tests/types.js
@@ -0,0 +1,94 @@
+common = require('./common.js');
+var assert_eq = common.assert_eq;
+var types = require('../types.js');
+var symbol = types.symbol,
+ hash_map = types.ns['hash-map'],
+ hash_map_Q = types.ns['map?'],
+ assoc = types.ns['assoc'],
+ dissoc = types.ns['dissoc'],
+ get = types.ns['get'],
+ contains_Q = types.ns['contains?'],
+ count = types.ns['count'],
+ equal_Q = types.ns['='];
+
+
+console.log("Testing hash_maps");
+X = hash_map();
+assert_eq(true, hash_map_Q(X));
+
+assert_eq(null, get(X,'a'));
+assert_eq(false, contains_Q(X, 'a'));
+X1 = assoc(X, 'a', "value of X a");
+assert_eq(null, get(X,'a'));
+assert_eq(false, contains_Q(X, 'a'));
+assert_eq("value of X a", get(X1, 'a'));
+assert_eq(true, contains_Q(X1, 'a'));
+
+Y = hash_map();
+assert_eq(0, count(Y));
+Y1 = assoc(Y, 'a', "value of Y a");
+assert_eq(1, count(Y1));
+Y2 = assoc(Y1, 'b', "value of Y b");
+assert_eq(2, count(Y2));
+assert_eq("value of Y a", get(Y2, 'a'));
+assert_eq("value of Y b", get(Y2, 'b'));
+
+X2 = assoc(X1, 'b', Y2);
+assert_eq(2, count(Y2));
+
+assert_eq(true, hash_map_Q(get(X2,'b')));
+
+assert_eq('value of Y a', get(get(X2,'b'),'a'));
+assert_eq('value of Y b', get(get(X2,'b'),'b'));
+
+Y3 = dissoc(Y2, 'a');
+assert_eq(2, count(Y2));
+assert_eq(1, count(Y3));
+assert_eq(null, get(Y3, 'a'));
+Y4 = dissoc(Y3, 'b');
+assert_eq(0, count(Y4));
+assert_eq(null, get(Y4, 'b'));
+
+
+console.log("Testing equal? function");
+assert_eq(true, equal_Q(2,2));
+assert_eq(false, equal_Q(2,3));
+assert_eq(false, equal_Q(2,3));
+assert_eq(true, equal_Q("abc","abc"));
+assert_eq(false, equal_Q("abc","abz"));
+assert_eq(false, equal_Q("zbc","abc"));
+assert_eq(true, equal_Q(symbol("abc"),symbol("abc")));
+assert_eq(false, equal_Q(symbol("abc"),symbol("abz")));
+assert_eq(false, equal_Q(symbol("zbc"),symbol("abc")));
+L6 = [1, 2, 3];
+L7 = [1, 2, 3];
+L8 = [1, 2, "Z"];
+L9 = ["Z", 2, 3];
+L10 = [1, 2];
+assert_eq(true, equal_Q(L6, L7));
+assert_eq(false, equal_Q(L6, L8));
+assert_eq(false, equal_Q(L6, L9));
+assert_eq(false, equal_Q(L6, L10));
+assert_eq(false, equal_Q(L10, L6));
+
+
+console.log("Testing ENV (1 level)")
+env1 = new types.Env();
+assert_eq('val_a',env1.set('a','val_a'));
+assert_eq('val_b',env1.set('b','val_b'));
+assert_eq('val_eq',env1.set('=','val_eq'));
+assert_eq('val_a',env1.get('a'));
+assert_eq('val_b',env1.get('b'));
+assert_eq('val_eq',env1.get('='));
+
+console.log("Testing ENV (2 levels)");
+env2 = new types.Env(env1);
+assert_eq('val_b2',env2.set('b','val_b2'));
+assert_eq('val_c',env2.set('c','val_c'));
+assert_eq(env1,env2.find('a'));
+assert_eq(env2,env2.find('b'));
+assert_eq(env2,env2.find('c'));
+assert_eq('val_a', env2.get('a'));
+assert_eq('val_b2',env2.get('b'));
+assert_eq('val_c', env2.get('c'));
+
diff --git a/js/types.js b/js/types.js
new file mode 100644
index 0000000..062b0dd
--- /dev/null
+++ b/js/types.js
@@ -0,0 +1,429 @@
+// Node vs browser behavior
+var types = {};
+if (typeof module === 'undefined') {
+ var exports = types;
+}
+
+// General utility functions
+
+// Clone a function
+Function.prototype.clone = function() {
+ var that = this;
+ var temp = function () { return that.apply(this, arguments); };
+ for( key in this ) {
+ temp[key] = this[key];
+ }
+ return temp;
+};
+
+function _clone (obj) {
+ var new_obj;
+ switch (obj_type(obj)) {
+ case 'list':
+ new_obj = obj.slice(0);
+ break;
+ case 'vector':
+ new_obj = obj.slice(0);
+ new_obj.__isvector__ = true;
+ break;
+ case 'hash-map':
+ new_obj = {};
+ for (var k in obj) {
+ if (obj.hasOwnProperty(k)) { new_obj[k] = obj[k]; }
+ }
+ break;
+ case 'function':
+ new_obj = obj.clone();
+ break;
+ default:
+ throw new Error("clone of non-collection: " + obj_type(obj));
+ }
+ return new_obj;
+}
+
+
+
+
+function nil_Q(a) { return a === null ? true : false; }
+function true_Q(a) { return a === true ? true : false; }
+function false_Q(a) { return a === false ? true : false; }
+
+function obj_type(obj) {
+ if (symbol_Q(obj)) { return 'symbol'; }
+ else if (list_Q(obj)) { return 'list'; }
+ else if (vector_Q(obj)) { return 'vector'; }
+ else if (hash_map_Q(obj)) { return 'hash-map'; }
+ else if (nil_Q(obj)) { return 'nil'; }
+ else if (true_Q(obj)) { return 'true'; }
+ else if (false_Q(obj)) { return 'false'; }
+ else if (atom_Q(obj)) { return 'atom'; }
+ else {
+ switch (typeof(obj)) {
+ case 'number': return 'number';
+ case 'function': return 'function';
+ case 'string': return 'string';
+ default: throw new Error("Unknown type '" + typeof(obj) + "'");
+ }
+ }
+}
+
+function _pr_str(obj, print_readably) {
+ if (typeof print_readably === 'undefined') { print_readably = true; }
+ var _r = print_readably;
+ var ot = obj_type(obj);
+ switch (ot) {
+ case 'list':
+ var ret = obj.map(function(e) { return _pr_str(e,_r); });
+ return "(" + ret.join(' ') + ")";
+ case 'vector':
+ var ret = obj.map(function(e) { return _pr_str(e,_r); });
+ return "[" + ret.join(' ') + "]";
+ case 'hash-map':
+ var ret = [];
+ for (var k in obj) {
+ ret.push(_pr_str(k,_r), _pr_str(obj[k],_r));
+ }
+ return "{" + ret.join(' ') + "}";
+ case 'string':
+ if (print_readably) {
+ return '"' + obj.replace(/\\/, "\\\\").replace(/"/g, '\\"') + '"';
+ } else {
+ return obj;
+ }
+ case 'nil':
+ return "nil";
+ case 'atom':
+ return "(atom " + _pr_str(obj.val,_r) + ")";
+ default:
+ return obj.toString();
+ }
+}
+
+function pr_str() {
+ return Array.prototype.map.call(arguments,function(exp) {
+ return _pr_str(exp, true);
+ }).join(" ");
+}
+
+function str() {
+ return Array.prototype.map.call(arguments,function(exp) {
+ return _pr_str(exp, false);
+ }).join("");
+}
+
+function prn() {
+ console.log.apply(console, Array.prototype.map.call(arguments,function(exp) {
+ return _pr_str(exp, true);
+ }));
+}
+
+function println() {
+ console.log.apply(console, Array.prototype.map.call(arguments,function(exp) {
+ return _pr_str(exp, false);
+ }));
+}
+
+function with_meta(obj, m) {
+ var new_obj = _clone(obj);
+ new_obj.__meta__ = m;
+ return new_obj;
+}
+
+function meta(obj) {
+ // TODO: support symbols and atoms
+ if ((!sequential_Q(obj)) &&
+ (!(hash_map_Q(obj))) &&
+ (!(function_Q(obj)))) {
+ throw new Error("attempt to get metadata from: " + obj_type(obj));
+ }
+ return obj.__meta__;
+}
+
+
+function equal_Q (a, b) {
+ var ota = obj_type(a), otb = obj_type(b);
+ if (!(ota === otb || (sequential_Q(a) && sequential_Q(b)))) {
+ return false;
+ }
+ switch (ota) {
+ case 'symbol': return a.value === b.value;
+ case 'list':
+ case 'vector':
+ if (a.length !== b.length) { return false; }
+ for (var i=0; i<a.length; i++) {
+ if (! equal_Q(a[i], b[i])) { return false; }
+ }
+ return true;
+ case 'hash-map':
+ var akeys = Object.keys(a).sort(),
+ bkeys = Object.keys(b).sort();
+ if (akeys.length !== bkeys.length) { return false; }
+ for (var i=0; i<akeys.length; i++) {
+ if (akeys[i] !== bkeys[i]) { return false; }
+ if (! equal_Q(a[akeys[i]], b[bkeys[i]])) { return false; }
+ }
+ return true;
+ default:
+ return a === b;
+ }
+}
+
+
+
+// Symbols
+function Symbol(name) {
+ this.value = name;
+ return this;
+}
+Symbol.prototype.toString = function() { return this.value; }
+
+function symbol(name) { return new Symbol(name); }
+
+function symbol_Q(obj) { return obj instanceof Symbol; }
+
+
+// Functions
+function new_function(func, exp, env, params) {
+ var f = function() {
+ // TODO: figure out why this throws with 'and' macro
+ //throw new Error("Attempt to invoke mal function directly");
+ return func(exp, new Env(env, params, arguments));
+ };
+ f.__meta__ = {exp: exp, env: env, params: params};
+ return f;
+
+}
+function function_Q(f) { return typeof f == "function"; }
+
+
+
+// Errors/Exceptions
+function mal_throw(exc) { throw exc; }
+
+
+// Vectors
+function vector() {
+ var v = Array.prototype.slice.call(arguments, 0);
+ v.__isvector__ = true;
+ return v;
+}
+
+function vector_Q(v) { return Array.isArray(v) && v.__isvector__; }
+
+
+// Lists
+
+function list() {
+ return Array.prototype.slice.call(arguments, 0);
+}
+
+function list_Q(lst) { return Array.isArray(lst) && !lst.__isvector__; }
+
+
+// Hash Maps
+
+function hash_map() {
+ if (arguments.length % 2 === 1) {
+ throw new Error("Odd number of hash map arguments");
+ }
+ var args = [{}].concat(Array.prototype.slice.call(arguments, 0));
+ return assoc_BANG.apply(null, args);
+}
+
+function hash_map_Q(hm) {
+ return typeof hm === "object" &&
+ !Array.isArray(hm) &&
+ !(hm === null) &&
+ !(hm instanceof Atom);
+}
+
+function assoc_BANG(hm) {
+ if (arguments.length % 2 !== 1) {
+ throw new Error("Odd number of assoc arguments");
+ }
+ for (var i=1; i<arguments.length; i+=2) {
+ var ktoken = arguments[i],
+ vtoken = arguments[i+1];
+ // TODO: support more than string keys
+ //if (list_Q(ktoken) && hash_map_Q(ktoken)) {
+ // throw new Error("expected hash-map key atom, got collection");
+ //}
+ if (typeof ktoken !== "string") {
+ throw new Error("expected hash-map key string, got: " + (typeof ktoken));
+ }
+ hm[ktoken] = vtoken;
+ }
+ return hm;
+}
+
+function assoc(src_hm) {
+ var hm = _clone(src_hm);
+ var args = [hm].concat(Array.prototype.slice.call(arguments, 1));
+ return assoc_BANG.apply(null, args);
+}
+
+function dissoc_BANG(hm) {
+ for (var i=1; i<arguments.length; i++) {
+ var ktoken = arguments[i];
+ delete hm[ktoken];
+ }
+ return hm;
+}
+
+function dissoc(src_hm) {
+ var hm = _clone(src_hm);
+ var args = [hm].concat(Array.prototype.slice.call(arguments, 1));
+ return dissoc_BANG.apply(null, args);
+}
+
+function get(hm, key) {
+ if (key in hm) {
+ return hm[key];
+ } else {
+ return null;
+ }
+}
+
+function contains_Q(hm, key) {
+ if (key in hm) { return true; } else { return false; }
+}
+
+function keys(hm) { return Object.keys(hm); }
+function vals(hm) { return Object.keys(hm).map(function(k) { return hm[k]; }); }
+
+
+// Atoms
+function Atom(val) { this.val = val; }
+function atom(val) { return new Atom(val); }
+function atom_Q(atm) { return atm instanceof Atom; }
+function deref(atm) { return atm.val; }
+function reset_BANG(atm, val) { return atm.val = val; }
+function swap_BANG(atm, f) {
+ var args = [atm.val].concat(Array.prototype.slice.call(arguments, 2));
+ atm.val = f.apply(f, args);
+ return atm.val;
+}
+
+
+// Sequence operations
+function sequential_Q(lst) { return list_Q(lst) || vector_Q(lst); }
+
+function nth(lst, idx) { return lst[idx]; }
+
+function count(s) {
+ if (Array.isArray(s)) { return s.length; }
+ else { return Object.keys(s).length; }
+}
+
+function empty_Q(lst) { return lst.length === 0; }
+
+function cons(a, b) { return [a].concat(b); }
+
+function concat(lst) {
+ lst = lst || [];
+ return lst.concat.apply(lst, Array.prototype.slice.call(arguments, 1));
+}
+
+function conj(lst) {
+ return lst.concat(Array.prototype.slice.call(arguments, 1));
+}
+
+function first(lst) { return lst[0]; }
+
+function rest(lst) { return lst.slice(1); }
+
+
+
+// General list related functions
+function apply(f) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ return f.apply(f, args.slice(0, args.length-1).concat(args[args.length-1]));
+}
+
+function map(f, lst) {
+ return lst.map(function(el){ return f(el); });
+}
+
+
+// Env implementation
+function Env(outer, binds, exprs) {
+ this.data = {};
+ this.outer = outer || null;
+
+ if (binds && exprs) {
+ // Returns a new Env with symbols in binds bound to
+ // corresponding values in exprs
+ // TODO: check types of binds and exprs and compare lengths
+ for (var i=0; i<binds.length;i++) {
+ if (binds[i].value === "&") {
+ // variable length arguments
+ this.data[binds[i+1].value] = Array.prototype.slice.call(exprs, i);
+ break;
+ } else {
+ this.data[binds[i].value] = exprs[i];
+ }
+ }
+ }
+ return this;
+}
+Env.prototype.find = function (key) {
+ if (key in this.data) { return this; }
+ else if (this.outer) { return this.outer.find(key); }
+ else { return null; }
+};
+Env.prototype.set = function(key, value) { this.data[key] = value; return value; },
+Env.prototype.get = function(key) {
+ var env = this.find(key);
+ if (!env) { throw new Error("'" + key + "' not found"); }
+ return env.data[key];
+};
+
+// types.ns is namespace of type functions
+var ns = {'pr-str': pr_str, 'str': str, 'prn': prn, 'println': println,
+ 'with-meta': with_meta, 'meta': meta,
+ type: obj_type, '=': equal_Q,
+ symbol: symbol, 'symbol?': symbol_Q,
+ 'nil?': nil_Q, 'true?': true_Q, 'false?': false_Q,
+ '<' : function(a,b){return a<b;},
+ '<=' : function(a,b){return a<=b;},
+ '>' : function(a,b){return a>b;},
+ '>=' : function(a,b){return a>=b;},
+ '+' : function(a,b){return a+b;},
+ '-' : function(a,b){return a-b;},
+ '*' : function(a,b){return a*b;},
+ '/' : function(a,b){return a/b;},
+ 'throw': mal_throw,
+ 'list': list, 'list?': list_Q,
+ 'vector': vector, 'vector?': vector_Q,
+ 'hash-map': hash_map, 'map?': hash_map_Q,
+ 'assoc': assoc, 'dissoc': dissoc, 'get': get,
+ 'contains?': contains_Q, 'keys': keys, 'vals': vals,
+ 'atom': atom, 'atom?': atom_Q,
+ "deref": deref, "reset!": reset_BANG, "swap!": swap_BANG,
+ 'sequential?': sequential_Q, 'cons': cons, 'nth': nth,
+ 'empty?': empty_Q, 'count': count, 'concat': concat,
+ 'conj': conj, 'first': first, 'rest': rest,
+ 'apply': apply, 'map': map};
+
+exports.ns = types.ns = ns;
+exports._pr_str = types._pr_str = _pr_str;
+exports.prn = types.prn = prn;
+exports.Env = types.Env = Env;
+
+exports.symbol = types.symbol = symbol;
+exports.symbol_Q = types.symbol_Q = symbol_Q;
+exports.hash_map = types.hash_map = hash_map;
+exports.hash_map_Q = types.hash_map_Q = hash_map_Q;
+exports.new_function = types.new_function = new_function;
+exports.list = types.list = list;
+exports.list_Q = types.list_Q = list_Q;
+exports.vector = types.vector = vector;
+exports.vector_Q = types.vector_Q = vector_Q;
+
+exports.sequential_Q = types.sequential_Q = sequential_Q;
+exports.cons = types.cons = cons;
+exports.concat = types.concat = concat;
+exports.first = types.first = first;
+exports.rest = types.rest = rest;
+exports.apply = types.apply = apply;
+exports.map = types.map = map;