From 57b657b61727a1303eb458a7be929336004c6367 Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Sun, 6 Dec 2009 23:21:20 -0800 Subject: [PATCH] Broke the code into multiple files. el.sh runs a repl. --- TODO | 2 + el.js | 1123 +--------------------------------------------- el.sh | 3 + el/evaluator.js | 247 ++++++++++ el/init.js | 23 + el/jsExt.js | 60 +++ el/list.js | 103 +++++ el/parser.js | 221 +++++++++ el/primitives.js | 133 ++++++ el/repl.js | 51 +++ el/symtab.js | 78 ++++ el/types.js | 113 +++++ el/util.js | 143 ++++++ 13 files changed, 1191 insertions(+), 1109 deletions(-) create mode 100755 el.sh create mode 100644 el/evaluator.js create mode 100644 el/init.js create mode 100644 el/jsExt.js create mode 100644 el/list.js create mode 100644 el/parser.js create mode 100644 el/primitives.js create mode 100644 el/repl.js create mode 100644 el/symtab.js create mode 100644 el/types.js create mode 100644 el/util.js diff --git a/TODO b/TODO index 52e8792..17491e9 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,8 @@ TODO ==== + * TESTS + * implement all Emacs Lisp types/objects * relational operators: < > <= >= = not diff --git a/el.js b/el.js index d9722f6..39da85b 100644 --- a/el.js +++ b/el.js @@ -27,1116 +27,21 @@ // our namespace var EL = function(){}; -// Use initHook() to specify initialization routines at the very end -// of the file we call init when everything is defined, regardless of -// the order it appears in the file. The order of the hooks still -// matters though, it's not fool-proof. -EL._initHooks = []; -EL.initHook = function(hook) { - EL._initHooks.push(hook); -}; -EL.init = function() { - var i = 0, - n = EL._initHooks.length; - while (i < n) { - EL._initHooks[i++].call(); - } -}; - - -// Just a little sugar -EL._jsExt = function() { - Array.prototype.each = function(fn) { - var i = 0, - n = this.length; - while (i < n) { - fn(this[i], i); - ++i; - } - }; - - // Thanks Prototype - String.prototype.camelize = function() { - var oStringList = this.split('_'); - if (oStringList.length == 1) - return oStringList[0][0].toUpperCase() + oStringList[0].substring(1); - - var camelizedString = oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1); - - for (var i = 1, len = oStringList.length; i < len; i++) { - var s = oStringList[i]; - camelizedString += s.charAt(0).toUpperCase() + s.substring(1); - } - - return camelizedString; - }; - - // A typeOf function that distinguishes between objects, arrays, - // and null. - EL.typeOf = function(value) { - var s = typeof value; - if (s === 'object') { - if (value) { - if (typeof value.length === 'number' && - !(value.propertyIsEnumerable('length')) && - typeof value.splice === 'function') { - s = 'array'; - } - } else { - s = 'null'; - } - } - return s; - }; - - // TODO throw something more informative - EL.assert = function(condition, message) { - if (!condition()) { - throw("assertion failed: " + condition + " (" + message + ")"); - } - }; -}; -EL.initHook(EL._jsExt); - - -//**************************************************************** -// **** Lisp Support ********************************************** -// ****************************************************************/ - -// data types are simple tags -EL._defineTags = function() { - EL.tags = ['symbol', 'string', 'number', 'cons', 'lambda', 'regex']; - EL.tags.each = Array.prototype.each; - - // define constructors for the primitive types (box values) - // e.g. EL.symbol('foo') => ['symbol', 'foo'] - EL.tags.each(function(tag) { - // don't clobber custom constructors - if (EL[tag] === undefined) { - EL[tag] = function(value) { - return [tag, value]; - }; - } - // tag type tests - var isTag = function(expr) { - return (EL.tag(expr) == tag); - }; - EL['is' + tag.camelize()] = isTag; - }); -}; -EL.initHook(EL._defineTags); - -// shorthands to save my fingers -EL._defineConstants = function() { - EL.nil = EL.symbol('nil'); - EL.t = EL.symbol('t'); -}; -EL.initHook(EL._defineConstants); - -// retrieve the tag from a value -EL.tag = function(expr) { - EL.assert(function() { var f='tag'; return EL.typeOf(expr) == 'array'; }, expr); - return expr[0]; -}; - -// unbox a value -EL.val = function(expr) { - EL.assert(function() { var f='val'; return EL.typeOf(expr) == 'array'; }, expr); - return expr[1]; -}; - -EL.symbolName = function(symbol) { -// EL.Util.pp(symbol); - EL.assert(function(){ var f='symbolName'; return EL.isSymbol(symbol); }, symbol); - return EL.val(symbol); -}; - -EL.consPair = function(pair) { - var cons = ['cons', pair]; - cons.isList = true; - return cons; -}; - -EL.cons = function(car, cdr) { - return EL.consPair([car, cdr]); -}; - -EL.car = function(cons) { - return EL.isNil(cons) ? cons : EL.val(cons)[0]; -}; - -EL.cdr = function(cons) { - return EL.isNil(cons) ? cons : EL.val(cons)[1]; -}; - -EL.cadr = function(cons) { - return EL.car(EL.cdr(cons)); -}; - -EL.caddr = function(cons) { - return EL.car(EL.cdr(EL.cdr(cons))); -}; - -EL.cadddr = function(cons) { - return EL.car(EL.cdr(EL.cdr(EL.cdr(cons)))); -}; - -EL.listLength = function(cons) { - var n = 0; - while (!EL.isNil(cons)) { - cons = EL.cdr(cons); - ++n; - } - return n; -}; - -EL.listLast = function(cons) { - var last; - while (!EL.isNil(cons)) { - last = cons; - cons = EL.cdr(cons); - } - return EL.car(last); -}; - -EL.listMap = function(cons, fn) { - var list = [], - i = 0; - while (!EL.isNil(cons)) { - list.push(fn(EL.car(cons), i)); - cons = EL.cdr(cons); - ++i; - } - return list.length > 0 ? EL.list(list) : EL.nil; -}; - -EL.listReduce = function(fn, accum, cons) { - var i = 0, - n = EL.listLength(cons); - while (i < n) { - accum = fn(accum, EL.nth(i++, cons)); - } - return accum; -}; - -EL.idFunction = function(x){return x;}; - -EL.unlist = function(cons) { - return EL.listReduce(EL.idFunction, [], cons); -}; - -EL.nth = function(n, cons) { - var i = 0, - e; - while (i <= n && !EL.isNil(cons)) { - e = EL.car(cons); - cons = EL.cdr(cons); - ++i; - } - return n > --i ? EL.nil : e; -}; - -EL.nthcdr = function(n, cons) { - var e = EL.cdr(cons), - i = 0; - while (i < n && !EL.isNil(e)) { - e = EL.cdr(e); - ++i; - } - return n > i ? EL.nil : e; -}; - -EL.isNilSymbol = function(expr) { - return (EL.isSymbol(expr) && EL.symbolName(expr) == 'nil'); -}; - -EL.isNil = function(expr) { - return EL.isNilSymbol(expr); -}; - -EL.list = function(exprs) { - var list = EL.nil, - i = exprs.length; - while (--i >= 0) { - list = EL.cons(exprs[i], list); - } - list.isList = true; - return list; -}; - -EL.isList = function(expr) { - return (EL.isNil(expr) || EL.isCons(expr)); -}; - -EL.isAtom = function(expr) { - return EL.isString(expr) || EL.isNumber(expr) || EL.isRegex(expr); -}; - -EL.inferType = function(exprs) { - var type = 'number', - initial = 0, - i = exprs.length-1; - while(i >= 0) { - if (EL.isString(exprs[i--])) { - type = 'string'; - initial = ''; - break; - } - } - return EL[type](initial); -}; - -EL.isSpecialForm = function(name, expr) { - var tag = EL.tag(expr), - car = EL.typeOf(expr) == 'array' && EL.val(expr)[0], - thisName = car && EL.symbolName(car); - return (tag == 'cons' && thisName == name); -}; -EL.isQuote = function(expr){return EL.isSpecialForm('quote', expr);}; -EL.isDefVar = function(expr){return EL.isSpecialForm('defvar', expr);}; -EL.isDefFunc = function(expr){return EL.isSpecialForm('defun', expr);}; -EL.isSet = function(expr){return EL.isSpecialForm('set', expr);}; -EL.isSetq = function(expr){return EL.isSpecialForm('setq', expr);}; -EL.isCond = function(expr){return EL.isSpecialForm('cond', expr);}; -EL.isIf = function(expr){return EL.isSpecialForm('if', expr);}; - -EL.eval = function(exprs) { - var e = new EL.Evaluator(); - return e.evalExpressions(exprs); -}; - -EL.parse = function(string) { - var p = new EL.Parser(); - return p.parse(string); -}; - -EL.parseOne = function(string) { - return EL.parse(string)[0]; -}; -EL.read = EL.parseOne; - -EL.rep = function(string) { - EL.print(EL.eval(EL.parse(string))); -}; - -EL.repl = function() { - var p = new EL.Parser(), - e = new EL.Evaluator(); - while (true) { - print("elisp> "); // i don't want a newline, grrrr - try { - var line = readline(); - while (!line) { - line = readline(); - } - if (line.substring(0,1).toLowerCase() == 'q') return; - EL.print(e.eval(p.parseOne(line))); - } catch (x) { - if (x.evalError) { - print('[error] ' + x.message + ': ' + x.expression); - EL.print(x); - } - else throw(x); - } - } -}; - - -/**************************************************************** - ****************************************************************/ - - -/**************************************************************** - **** Parser **************************************************** - ****************************************************************/ - -EL.Parser = function(data) { - this.data = data || ''; -}; - -EL.Parser.Error = function(name, message) { - this.parserError = true; - this.name = name; - this.message = message; -}; - -EL.Parser.Error.messages = { - 'eof': "no more input" -}; - -EL.Parser.prototype.error = function(name) { - throw(new EL.Parser.Error(name, EL.Parser.Error.messages[name])); -}; - -EL.Parser.prototype.peek = function() { - return this.data[this.pos]; -}; - -EL.Parser.prototype.consumeChar = function() { - if (this.pos >= this.data.length) this.error('eof'); - return this.data[this.pos++]; -}; - -EL.Parser.prototype.consumeWhitespace = function() { - var c; - while ((c = this.peek()) && c.match(/[\s\n]/)) { - this.consumeChar(); - } -}; - -EL.Parser.prototype.rewind = function() { - this.pos = 0; -}; - -EL.Parser.prototype.rest = function() { - return this.data.substring(this.pos); -}; - -EL.Parser.prototype.moreInput = function() { - return (this.pos < this.data.length); -}; - -EL.Parser.prototype.parse = function(string) { - if (string) this.data = string; - this.rewind(); - var exprs = []; - while (this.moreInput()) { - try { - exprs.push(this.parseExpression()); - } catch (e) { - if (e.parserError && e.name == 'eof') { - print("error: " + e.message); - break; - } - else { - throw(e); - } - } - } - this.expressions = exprs; -// print(''); -// EL.Util.pp(exprs); -// print(''); - return exprs; -}; - -EL.Parser.prototype.parseOne = function(string) { - return this.parse(string)[0]; -}; - -EL.Parser.prototype.parseUntil = function(regex, initial, next, consumeTerminator) { - var c, - token = initial, - condition = function(c){ return c.match(regex) == null; }; - while ((c = this.peek()) && condition(c)) { - token = next(token, this.consumeChar()); - } - if (consumeTerminator && this.peek()) this.consumeChar(); - return token; -}; - -EL.Parser.prototype.parseList = function() { - var list = [], - expr; - // consume initial paren '(' - this.consumeChar(); - while ((expr = this.parseExpression()) && expr != ')') { - list.push(expr); - } - return list; -}; - -EL.Parser.prototype.parseCons = function() { - var cons = [], - expr; - // consume initial paren '(' - this.consumeChar(); - cons.push(this.parseExpression()); - // ignore . - this.parseExpression(); - cons.push(this.parseExpression()); - return cons; -}; - -EL.Parser.prototype.parseString = function() { - // consume initial quotation mark - this.consumeChar(); - var self = this; - return this.parseUntil(/"/, '', function(s,c){ - if (c == '\\') { - c = self.consumeChar(); - } - return s + c; - }, true /* consume terminator */); -}; - -EL.Parser.prototype.parseSymbol = function() { - var symbol = this.parseUntil(/[\s()]/, '', function(t,c){return t + c;}); - return symbol; -}; - -// Probably easy to break -EL.Parser.prototype.parseRegex = function() { - // consume initial slash - this.consumeChar(); - var self = this; - return new RegExp(this.parseUntil(/\//, '', function(s,c){ - if (c == '\\') { - c = self.consumeChar(); - } - return s + c; - }, true /* consume terminator */)); -}; - -EL.Parser.prototype.parseNumber = function() { - var sign = this.peek() == '-' ? this.consumeChar() : '+', - value = this.parseUntil(/[^\d]/, 0, function(n,c) { - return n*10 + parseInt(c); - }); - if (this.peek() == '.') { - this.consumeChar(); - var decimal = this.parseUntil(/[^\d]/, '', function(s,c) {return s + c;}); - value = parseFloat('' + value + '.' + decimal); - } - return sign == '-' ? -1*value : value; -}; - -EL.Parser.prototype.lookingAtNumber = function() { - var pos = this.pos, - rest = this.rest(), - match = rest.match(/^(-)?\d+(\.\d+)?[\s)\n]/) || rest.match(/^(-)?\d+(\.\d+)?$/); - return (match != null); -}; - -EL.Parser.prototype.lookingAtCons = function() { - var orig_pos = this.pos, - _ = this.consumeChar(), - _ = this.parseExpression(), - cdr = this.parseExpression(); - this.pos = orig_pos; // rewind, like it never happened. - return EL.typeOf(cdr) == 'array' && EL.isSymbol(cdr) && EL.val(cdr) == '.'; -}; - -EL.Parser.prototype.parseExpression = function() { - var value, - c = this.peek(); - if (c == '(' && this.lookingAtCons()) { - value = EL.consPair(this.parseCons()); - } - else if (c == '(') { - var list = this.parseList(); - value = (list.length > 0) ? EL.list(list) : EL.nil; - } - else if (c == ')') { - return this.consumeChar(); - } - else if (c == "'") { - this.consumeChar(); - value = EL.cons(EL.symbol('quote'), this.parseExpression()); - } - else if (c == '"') { - value = EL.string(this.parseString()); - } - else if (this.lookingAtNumber()) { - value = EL.number(this.parseNumber()); - } - else if (c) { - value = EL.symbol(this.parseSymbol()); - } - else { - if (this.pos == this.data.length) { - print('[error] no more input. unterminated string or list? (continuing anyway)'); - } - print('[warning] in EL.Parser.parseExpression: unrecognized char "' + c + '"'); - print('this.pos = ' + this.pos); - print('this.data.length = ' + this.data.length); - print('this.rest = ' + this.rest()); - } - this.consumeWhitespace(); - return value; -}; - -/**************************************************************** - ****************************************************************/ - - -/**************************************************************** - **** Symbol Table ********************************************** - ****************************************************************/ - -EL.SymbolTable = function(bindings) { - this.symbols = [[]]; - this.level = 0; - if (bindings) this.define(bindings); -}; - -EL.SymbolTable.prototype.lookupWithScope = function(name) { - var i = this.level, - symbol; - while (i >= 0) { - value = this.symbols[i][name]; - if (value) { - return [i, value]; - } - --i; - } - return null; -}; - -EL.SymbolTable.prototype.lookup = function(name) { - var pair = this.lookupWithScope(name); - return pair && pair[1]; -}; - -// store the given symbol/value pair in the symbol table at the current level. -EL.SymbolTable.prototype.define = function(name, value) { - if (value === undefined && EL.typeOf(name) == 'array') { - var bindings = name, - i = 0, - n = bindings.length, - scope = this.symbols[this.level]; - while (i < n) { - var name = bindings[i][0], - value = bindings[i][1]; -// print('name: ' + name); -// print('value: ' + value); - scope[name] = value; - ++i; - } - } - else { - this.symbols[this.level][name] = value; - } -}; - -EL.SymbolTable.prototype.pushScope = function(bindings) { -// print('>>> pushing scope <<<'); -// print('>>> level going from ' + this.level + ' to ' + (1+this.level)); -// print(bindings); - this.symbols[++this.level] = []; - if (bindings) this.define(bindings); -}; - -EL.SymbolTable.prototype.popScope = function() { - --this.level; -}; - -EL.SymbolTable.prototype.set = function(name, value) { - var pair = this.lookupWithScope(name), - level = pair[0]; - this.symbols[level][name] = value; -}; - -/**************************************************************** - ****************************************************************/ - - - -/**************************************************************** - **** Primitives ************************************************ - ****************************************************************/ - -EL.PrimitiveVariables = [ - ['t', { - type: 'variable', - value: ['symbol', 't'], - docstring: "true" - }], - ['nil', { - type: 'variable', - value: ['symbol', 'nil'], - docstring: "nil" - }] -]; - -// 'this' is bound to the EL.Evaluator object -EL.PrimitiveFunctions = [ - ['symbol-name', { - type: 'primitive', - name: 'symbol-name', - params: ['symbol'], - body: function(symbol) { return EL.string(EL.val(symbol)); }, - docstring: "Return a symbol's name, a string." - }], - - ['string-match', { - type: 'primitive', - name: 'string-match', - params: ['regex', 'string', '&optional', 'start'], - body: function(regex, string, start) { - var index = start ? EL.val(start) : 0, - s = EL.val(string).substring(index), - match = s.match(new RegExp(EL.val(regex))), - found = match ? EL.number(s.indexOf(match[0])) : EL.nil; - return found; - }, - docstring: "Return the index of the char matching regex in string, beginning from start if available." - }], - - ['+', { - type: 'primitive', - name: '+', - params: [/* ... */], - body: function() { - var args = EL.Util.shallowCopy(arguments), - initial = EL.inferType(args), - type = initial[0]; - return EL.Util.reduce(function(sum, n) { - return [type, EL.val(sum) + EL.val(n)]; - }, initial, args); - }, - docstring: "add two numbers" - }], - ['-', { - type: 'primitive', - name: '-', - params: [/* ... */], - body: function() { - return EL.Util.foldr(function(diff, n) { - return EL.number(EL.val(diff) - EL.val(n)); - }, EL.number(0), EL.Util.shallowCopy(arguments)); - }, - docstring: "subtract two numbers" - }], - ['*', { - type: 'primitive', - name: '*', - params: [/* ... */], - body: function() { - return EL.Util.reduce(function(prod, n) { - return EL.number(EL.val(prod) * EL.val(n)); - }, EL.number(1), EL.Util.shallowCopy(arguments)); - }, - docstring: "multiply two numbers" - }], - ['/', { - type: 'primitive', - name: '/', - params: [/* ... */], - body: function() { - return EL.Util.foldr(function(quot, n) { - return EL.number(EL.val(quot) / EL.val(n)); - }, EL.number(1), EL.Util.shallowCopy(arguments)); - }, - docstring: "divide two numbers" - }], - ['print', { - type: 'primitive', - name: 'print', - params: ['x'], - body: function(x, tostring) { - var buffer = "", - tag = EL.tag(x); - function p(s) { - if (tostring) buffer += s; - else print(s); - } - if (tag == 'number' || tag == 'symbol' || tag == 'string') { - p(EL.val(x)); - } - else if (tag == 'lambda') { - var fn = EL.val(x); - p('(lambda ' + fn.name + ' (' + fn.params + ')\n'); - p(fn.body); // TODO lisp pretty print - p(')'); - } - else if (tag == 'list') { - var recurse = arguments.callee; // far easier to remember than Y - print('(', El.val(x).map(function(e){return (recurse(e, true) + ' ');}), ")"); - } - else { - print('unknown type: ' + x); - } - return EL.nil; - }, - docstring: "print an expression" - }] -]; - - -/**************************************************************** - ****************************************************************/ - - -/**************************************************************** - **** Evaluation ************************************************ - ****************************************************************/ - -EL.Evaluator = function(exprs) { - this.expressions = exprs; - this.variables = new EL.SymbolTable(EL.PrimitiveVariables); - this.functions = new EL.SymbolTable(EL.PrimitiveFunctions); -}; - -EL.Evaluator.Error = function(name, message, expr) { - this.evalError = true; - this.name = name; - this.message = message; - this.expression = expr; -}; - -EL.Evaluator.Error.messages = { - 'not-expr': "not an expression", - 'undefined-func': "undefined function", - 'undefined-var': "variable not defined" -}; - -EL.Evaluator.prototype.error = function(name, expr) { - throw(new EL.Evaluator.Error(name, EL.Evaluator.Error.messages[name], expr)); -}; - -EL.Evaluator.prototype.evalExpressions = function(expressions) { - var exprs = expressions || this.expressions, - i = 0, - n = exprs.length, - result; - while (i < n) { - try { - result = this.eval(exprs[i++]); - } catch (e) { - if (e.evalError) { - print('[error] ' + e.message); - if (e.expression) { - print("got: " + e.expression); - } - result = EL.nil; - break; - } - else { - throw(e); - } - } - } -// EL.Util.pp(result); - return result; -}; - -EL.Evaluator.prototype.lookupVar = function(symbol) { - return this.variables.lookup(symbol); -}; - -EL.Evaluator.prototype.lookupFunc = function(symbol) { - return this.functions.lookup(symbol); -}; - -EL.Evaluator.prototype.defineVar = function(symbol, value, docstring) { - this.variables.define(symbol, { - type: 'variable', - value: value, - docstring: docstring || "(undocumented)" - }); -}; - -EL.Evaluator.prototype.setVar = function(symbol, value, create) { - var valueObject = this.lookupVar(symbol); - if (!valueObject) { - if (create) { - this.defineVar(symbol, EL.nil); - valueObject = this.lookupVar(symbol); - } - else { - this.error('undefined-var', symbol); - } - } - valueObject.value = value; - this.variables.set(symbol, valueObject); -}; - -EL.Evaluator.prototype.defineFunc = function(symbol, params, body, docstring) { - this.functions.define(symbol, { - type: 'lambda', - name: symbol, - params: params, - body: body, - docstring: docstring || "(undocumented)" - }); -}; - -EL.Evaluator.prototype.doIf = function(condition, trueBlock, nilBlock) { - return EL.isNil(condition) ? this.eval(nilBlock) : this.eval(trueBlock); -}; - -EL.Evaluator.prototype.doCond = function(exprs) { - print('----- COND (doCond) -----'); - EL.print(exprs); - return EL.nil; -}; - -EL.Evaluator.prototype.apply = function(func, args) { - var result; - if (func.type === 'primitive') { -// print('APPLY: '); -// EL.print(func); -// print('WITH: '); -// EL.print(args); -// print('------'); - result = func.body.apply(this, args); - } - else { - this.functions.pushScope(); - this.variables.pushScope(EL.listMap(func.params, function(e, i){ - var name = EL.symbolName(e), - value = { - type: 'variable', - value: this.eval(args[i]) - }; - return [name, value]; - })); - result = EL.listLast(EL.listMap(func.body, - function(e) {return this.eval(e); })); - this.functions.popScope(); - this.variables.popScope(); - } - return result; -}; - -EL.Evaluator.prototype.eval = function(expr) { -// print("EVAL: " + EL.typeOf(expr)); -// EL.print(expr); - var result, x, - tag = EL.tag(expr); - if (EL.isAtom(expr)) { - result = expr; - } - else if (EL.isSymbol(expr)) { - var name = EL.val(expr); - x = this.lookupVar(name); - if (x == null) this.error('undefined-var', name); - result = x.value; - } - - /////////////////// - // special forms // - /////////////////// - // (many could be in lisp when there are macros) // - - else if (EL.isQuote(expr)) { - result = EL.cdr(expr); - } - else if (EL.isDefVar(expr)) { - var name = EL.symbolName(EL.cadr(expr)), // 2nd param - value = this.eval(EL.caddr(expr)), // 3rd param - docstring = EL.cadddr(expr); // 4th param - // TODO check for re-definitions - this.defineVar(name, value, docstring); - result = EL.nil; - } - else if (EL.isDefFunc(expr)) { - var name = EL.symbolName(EL.nth(1, expr)), - params = EL.nth(2, expr), - d = EL.nth(3, expr), - docstring = EL.isString(d) && d, - body = EL.nthcdr(docstring ? 3 : 2, expr); - this.defineFunc(name, params, body, docstring); - result = EL.nil; - } - else if (EL.isSet(expr)) { - var val = EL.val(expr), - name = EL.symbolName(val[1]), - value = this.eval(val[2]); - this.setVar(name, value); - result = value; - } - else if (EL.isSetq(expr)) { - var i = 1, - n = EL.listLength(expr), - e; - while (i < n && EL.isSymbol((e=EL.nth(i,expr)))) { - var name = EL.symbolName(EL.nth(i, expr)), - value = this.eval(EL.nth(i+1, expr)); - this.setVar(name, value, true); - result = value; - i += 2; - } - } - else if (EL.isIf(expr)) { - var val = EL.val(expr), - condition = this.eval(val[1]), - trueBlock = val[2], - nilBlock = val[3]; - result = this.doIf(condition, trueBlock, nilBlock); - } - else if (EL.isCond(expr)) { - var val = EL.val(expr), - list = val[1], - condition = EL.car(list), - body = EL.cdr(list), - rest = val.slice(2); - result = this.doCond(exprs); - } - - // function application - else if (EL.isCons(expr)) { - var name = EL.car(expr), - rest = EL.cdr(expr), - func, args; - while (!EL.isSymbol(name)) { - name = this.eval(name); - } - if ((func = this.lookupFunc(EL.symbolName(name)))) { - var self = this; - args = EL.listReduce(function(a,e){ - a.push(self.eval(e)); - return a; - }, [], rest); - result = this.apply(func, args); - } - else { - this.error('undefined-func', name); - } - } - else { - this.error('not-expr', expr); - } -// print('RESULT: ' + result); - return result; -}; - - -/**************************************************************** - ****************************************************************/ - - - -/**************************************************************** - **** Utilities ************************************************* - ****************************************************************/ - -EL.Util = function(){}; - -// aka foldl -EL.Util.reduce = function(fn, accum, list) { - var i = 0, - n = list.length; - while (i < n) { - accum = fn(accum, list[i++]); - } - return accum; -}; - -EL.Util.foldr = function(fn, end, list) { - var i = list.length-1; - while (i >= 0) { - end = fn(list[i--], end); - } - return end; -}; - -EL.Util.shallowCopy = function(list) { - var i = 0, - n = list.length, - result = []; - while (i < n) { - result.push(list[i++]); - } - return result; -}; - - -// i'm sorry -EL.Util.pp = function(x, indent, key, noprint) { - if (indent === undefined) { - indent = 0; - } - - /* string to print at the end, the only way to print 2 strings in - * succession without a newline is to buffer them - */ - var buffer = ""; - var printB = function(s, newline) { - buffer += s; - if (newline) buffer += "\n"; - }; - var dumpBuffer = function(b) { - if (b === undefined) b = buffer; - if (buffer.length <= 72) { - print(buffer); - } - else { - var split = 72; - var c; - while ((c = b[split]) && c.match(/[a-zA-Z0-9"'\[\]_-]/)) --split; - print(b.substring(0, split)); - var next_chunk = b.substring(split); - if (next_chunk) dumpBuffer(next_chunk); - } - buffer = ""; - }; - - var space = ""; -// for (var j = 0; j < indent; j++) { -// space += " "; -// } - - switch (EL.typeOf(x)) { - case 'object': - if (key) { - printB(space + key + ': {'); - } - else { - printB(space + ' {'); - } - for (var a in x) { - printB(EL.Util.pp(x[a], 1+indent, a, true)); - printB(', '); - } - printB(space + "} "); - break; - - case 'string': - if (key) { - printB(space + key + ': "' + x + '"'); - } - else { - printB(space + '"' + x + '"'); - } - return buffer; - - case 'array': - // nil special case - if (x.length == 2 && EL.tag(x) == 'symbol' && EL.val(x) == 'nil') { - return EL.Util.pp(null, indent, key); - } - - if (key) { - printB(space + key + ': ['); - } - else { - printB(space + '['); - } - var n = x.length, i = 0; - while (i < n) { - if (i > 0) printB(', '); - printB(EL.Util.pp(x[i++], 1+indent, undefined, true)); - } - printB(space + ']'); - break; - - case 'null': - if (key) { - printB(space + key + ': (null)'); - } - else { - printB(space + '(null)'); - } - return buffer; - - default: - if (key) { - printB(space + key + ": " + x); - } - else { - printB(space + x); - } - return buffer; - } - if (noprint) return buffer; - else dumpBuffer(); -}; -EL.print = EL.Util.pp; +load('el/init.js'); // simple init system +load('el/jsExt.js'); // a few extensions to native types +load('el/util.js'); // utilities + +// main lisp system +load('el/types.js'); +load('el/list.js'); +load('el/symtab.js'); +load('el/parser.js'); +load('el/evaluator.js'); +load('el/primitives.js'); +load('el/repl.js'); // everything is defined, initialize EL.init(); -// spidermonkey doesn't like a C-style comment at the end either -spidermonkeykludge = 1; +// q to quit +EL.repl(); \ No newline at end of file diff --git a/el.sh b/el.sh new file mode 100755 index 0000000..f65829b --- /dev/null +++ b/el.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +rlwrap js el.js diff --git a/el/evaluator.js b/el/evaluator.js new file mode 100644 index 0000000..cf4d2a1 --- /dev/null +++ b/el/evaluator.js @@ -0,0 +1,247 @@ +//// +// Emacs Lisp implementation in JavaScript. +// +// Copyright (c) 2009 Sami Samhuri - sami.samhuri@gmail.com +// +// Released under the terms of the MIT license. See the included file +// LICENSE. + + +/**************************************************************** + **** Evaluation ************************************************ + ****************************************************************/ + +EL.Evaluator = function(exprs) { + this.expressions = exprs; + this.variables = new EL.SymbolTable(EL.PrimitiveVariables); + this.functions = new EL.SymbolTable(EL.PrimitiveFunctions); +}; + +EL.Evaluator.Error = function(name, message, expr) { + this.evalError = true; + this.name = name; + this.message = message; + this.expression = expr; +}; + +EL.Evaluator.Error.messages = { + 'not-expr': "not an expression", + 'undefined-func': "undefined function", + 'undefined-var': "variable not defined" +}; + +EL.Evaluator.prototype.error = function(name, expr) { + throw(new EL.Evaluator.Error(name, EL.Evaluator.Error.messages[name], expr)); +}; + +EL.Evaluator.prototype.evalExpressions = function(expressions) { + var exprs = expressions || this.expressions, + i = 0, + n = exprs.length, + result; + while (i < n) { + try { + result = this.eval(exprs[i++]); + } catch (e) { + if (e.evalError) { + print('[error] ' + e.message); + if (e.expression) { + print("got: " + e.expression); + } + result = EL.nil; + break; + } + else { + throw(e); + } + } + } +// EL.Util.pp(result); + return result; +}; + +EL.Evaluator.prototype.lookupVar = function(symbol) { + return this.variables.lookup(symbol); +}; + +EL.Evaluator.prototype.lookupFunc = function(symbol) { + return this.functions.lookup(symbol); +}; + +EL.Evaluator.prototype.apply = function(func, args) { + var result; + if (func.type === 'primitive') { +// print('APPLY: '); +// EL.print(func); +// print('WITH: '); +// EL.print(args); +// print('------'); + result = func.body.apply(this, args); + } + else { + this.functions.pushScope(); + this.variables.pushScope(EL.listMap(func.params, function(e, i){ + var name = EL.symbolName(e), + value = { + type: 'variable', + value: this.eval(args[i]) + }; + return [name, value]; + })); + result = EL.listLast(EL.listMap(func.body, + function(e) {return this.eval(e); })); + this.functions.popScope(); + this.variables.popScope(); + } + return result; +}; + +EL.Evaluator.prototype.eval = function(expr) { +// print("EVAL: " + EL.typeOf(expr)); +// EL.print(expr); + var result, x, + tag = EL.tag(expr); + if (EL.isAtom(expr)) { + result = expr; + } + else if (EL.isSymbol(expr)) { + var name = EL.val(expr); + x = this.lookupVar(name); + if (x == null) this.error('undefined-var', name); + result = x.value; + } + + /////////////////// + // special forms // + /////////////////// + // (many could be in lisp when there are macros) // + + else if (EL.isQuote(expr)) { + result = EL.cdr(expr); + } + else if (EL.isDefVar(expr)) { + var name = EL.symbolName(EL.cadr(expr)), // 2nd param + value = this.eval(EL.caddr(expr)), // 3rd param + docstring = EL.cadddr(expr); // 4th param + // TODO check for re-definitions + this.defineVar(name, value, docstring); + result = EL.nil; + } + else if (EL.isDefFunc(expr)) { + var name = EL.symbolName(EL.nth(1, expr)), + params = EL.nth(2, expr), + d = EL.nth(3, expr), + docstring = EL.isString(d) && d, + body = EL.nthcdr(docstring ? 3 : 2, expr); + this.defineFunc(name, params, body, docstring); + result = EL.nil; + } + else if (EL.isSet(expr)) { + var val = EL.val(expr), + name = EL.symbolName(val[1]), + value = this.eval(val[2]); + this.setVar(name, value); + result = value; + } + else if (EL.isSetq(expr)) { + var i = 1, + n = EL.listLength(expr), + e; + while (i < n && EL.isSymbol((e=EL.nth(i,expr)))) { + var name = EL.symbolName(EL.nth(i, expr)), + value = this.eval(EL.nth(i+1, expr)); + this.setVar(name, value, true); + result = value; + i += 2; + } + } + else if (EL.isIf(expr)) { + var val = EL.val(expr), + condition = this.eval(val[1]), + trueBlock = val[2], + nilBlock = val[3]; + result = this.doIf(condition, trueBlock, nilBlock); + } + else if (EL.isCond(expr)) { + var val = EL.val(expr), + list = val[1], + condition = EL.car(list), + body = EL.cdr(list), + rest = val.slice(2); + result = this.doCond(exprs); + } + + // function application + else if (EL.isCons(expr)) { + var name = EL.car(expr), + rest = EL.cdr(expr), + func, args; + while (!EL.isSymbol(name)) { + name = this.eval(name); + } + if ((func = this.lookupFunc(EL.symbolName(name)))) { + var self = this; + args = EL.listReduce(function(a,e){ + a.push(self.eval(e)); + return a; + }, [], rest); + result = this.apply(func, args); + } + else { + this.error('undefined-func', name); + } + } + else { + this.error('not-expr', expr); + } +// print('RESULT: ' + result); + return result; +}; + + +EL.Evaluator.prototype.defineVar = function(symbol, value, docstring) { + this.variables.define(symbol, { + type: 'variable', + value: value, + docstring: docstring || "(undocumented)" + }); +}; + +EL.Evaluator.prototype.setVar = function(symbol, value, create) { + var valueObject = this.lookupVar(symbol); + if (!valueObject) { + if (create) { + this.defineVar(symbol, EL.nil); + valueObject = this.lookupVar(symbol); + } + else { + this.error('undefined-var', symbol); + } + } + valueObject.value = value; + this.variables.set(symbol, valueObject); +}; + +EL.Evaluator.prototype.defineFunc = function(symbol, params, body, docstring) { + this.functions.define(symbol, { + type: 'lambda', + name: symbol, + params: params, + body: body, + docstring: docstring || "(undocumented)" + }); +}; + +EL.Evaluator.prototype.doIf = function(condition, trueBlock, nilBlock) { + return EL.isNil(condition) ? this.eval(nilBlock) : this.eval(trueBlock); +}; + +EL.Evaluator.prototype.doCond = function(exprs) { + print('----- COND (doCond) -----'); + EL.print(exprs); + return EL.nil; +}; + +/**************************************************************** + ****************************************************************/ + diff --git a/el/init.js b/el/init.js new file mode 100644 index 0000000..dacd70d --- /dev/null +++ b/el/init.js @@ -0,0 +1,23 @@ +//// +// Emacs Lisp implementation in JavaScript. +// +// Copyright (c) 2009 Sami Samhuri - sami.samhuri@gmail.com +// +// Released under the terms of the MIT license. See the included file +// LICENSE. + +// Use initHook() to specify initialization routines at the very end +// of the file we call init when everything is defined, regardless of +// the order it appears in the file. The order of the hooks still +// matters though, it's not fool-proof. +EL._initHooks = []; +EL.initHook = function(hook) { + EL._initHooks.push(hook); +}; +EL.init = function() { + var i = 0, + n = EL._initHooks.length; + while (i < n) { + EL._initHooks[i++].call(); + } +}; diff --git a/el/jsExt.js b/el/jsExt.js new file mode 100644 index 0000000..8fd5b58 --- /dev/null +++ b/el/jsExt.js @@ -0,0 +1,60 @@ +//// +// Emacs Lisp implementation in JavaScript. +// +// Copyright (c) 2009 Sami Samhuri - sami.samhuri@gmail.com +// +// Released under the terms of the MIT license. See the included file +// LICENSE. + +// Just a little sugar +EL.initHook(function() { + Array.prototype.each = function(fn) { + var i = 0, + n = this.length; + while (i < n) { + fn(this[i], i); + ++i; + } + }; + + // Thanks Prototype + String.prototype.camelize = function() { + var oStringList = this.split('_'); + if (oStringList.length == 1) + return oStringList[0][0].toUpperCase() + oStringList[0].substring(1); + + var camelizedString = oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1); + + for (var i = 1, len = oStringList.length; i < len; i++) { + var s = oStringList[i]; + camelizedString += s.charAt(0).toUpperCase() + s.substring(1); + } + + return camelizedString; + }; + + // A typeOf function that distinguishes between objects, arrays, + // and null. + EL.typeOf = function(value) { + var s = typeof value; + if (s === 'object') { + if (value) { + if (typeof value.length === 'number' && + !(value.propertyIsEnumerable('length')) && + typeof value.splice === 'function') { + s = 'array'; + } + } else { + s = 'null'; + } + } + return s; + }; + + // TODO throw something more informative + EL.assert = function(condition, message) { + if (!condition()) { + throw("assertion failed: " + condition + " (" + message + ")"); + } + }; +}); diff --git a/el/list.js b/el/list.js new file mode 100644 index 0000000..6641a56 --- /dev/null +++ b/el/list.js @@ -0,0 +1,103 @@ +//// +// Emacs Lisp implementation in JavaScript. +// +// Copyright (c) 2009 Sami Samhuri - sami.samhuri@gmail.com +// +// Released under the terms of the MIT license. See the included file +// LICENSE. + +EL.consPair = function(pair) { + var cons = ['cons', pair]; + cons.isList = true; + return cons; +}; + +EL.cons = function(car, cdr) { + return EL.consPair([car, cdr]); +}; + +EL.car = function(cons) { + return EL.isNil(cons) ? cons : EL.val(cons)[0]; +}; + +EL.cdr = function(cons) { + return EL.isNil(cons) ? cons : EL.val(cons)[1]; +}; + +EL.cadr = function(cons) { + return EL.car(EL.cdr(cons)); +}; + +EL.caddr = function(cons) { + return EL.car(EL.cdr(EL.cdr(cons))); +}; + +EL.cadddr = function(cons) { + return EL.car(EL.cdr(EL.cdr(EL.cdr(cons)))); +}; + +EL.listLength = function(cons) { + var n = 0; + while (!EL.isNil(cons)) { + cons = EL.cdr(cons); + ++n; + } + return n; +}; + +EL.listLast = function(cons) { + var last; + while (!EL.isNil(cons)) { + last = cons; + cons = EL.cdr(cons); + } + return EL.car(last); +}; + +EL.listMap = function(cons, fn) { + var list = [], + i = 0; + while (!EL.isNil(cons)) { + list.push(fn(EL.car(cons), i)); + cons = EL.cdr(cons); + ++i; + } + return list.length > 0 ? EL.list(list) : EL.nil; +}; + +EL.listReduce = function(fn, accum, cons) { + var i = 0, + n = EL.listLength(cons); + while (i < n) { + accum = fn(accum, EL.nth(i++, cons)); + } + return accum; +}; + +EL.idFunction = function(x){return x;}; + +EL.unlist = function(cons) { + return EL.listReduce(EL.idFunction, [], cons); +}; + +EL.nth = function(n, cons) { + var i = 0, + e; + while (i <= n && !EL.isNil(cons)) { + e = EL.car(cons); + cons = EL.cdr(cons); + ++i; + } + return n > --i ? EL.nil : e; +}; + +EL.nthcdr = function(n, cons) { + var e = EL.cdr(cons), + i = 0; + while (i < n && !EL.isNil(e)) { + e = EL.cdr(e); + ++i; + } + return n > i ? EL.nil : e; +}; + diff --git a/el/parser.js b/el/parser.js new file mode 100644 index 0000000..60f0f67 --- /dev/null +++ b/el/parser.js @@ -0,0 +1,221 @@ +//// +// Emacs Lisp implementation in JavaScript. +// +// Copyright (c) 2009 Sami Samhuri - sami.samhuri@gmail.com +// +// Released under the terms of the MIT license. See the included file +// LICENSE. + +/**************************************************************** + **** Parser **************************************************** + ****************************************************************/ + +EL.Parser = function(data) { + this.data = data || ''; +}; + +EL.Parser.Error = function(name, message) { + this.parserError = true; + this.name = name; + this.message = message; +}; + +EL.Parser.Error.messages = { + 'eof': "no more input" +}; + +EL.Parser.prototype.error = function(name) { + throw(new EL.Parser.Error(name, EL.Parser.Error.messages[name])); +}; + +EL.Parser.prototype.peek = function() { + return this.data[this.pos]; +}; + +EL.Parser.prototype.consumeChar = function() { + if (this.pos >= this.data.length) this.error('eof'); + return this.data[this.pos++]; +}; + +EL.Parser.prototype.consumeWhitespace = function() { + var c; + while ((c = this.peek()) && c.match(/[\s\n]/)) { + this.consumeChar(); + } +}; + +EL.Parser.prototype.rewind = function() { + this.pos = 0; +}; + +EL.Parser.prototype.rest = function() { + return this.data.substring(this.pos); +}; + +EL.Parser.prototype.moreInput = function() { + return (this.pos < this.data.length); +}; + +EL.Parser.prototype.parse = function(string) { + if (string) this.data = string; + this.rewind(); + var exprs = []; + while (this.moreInput()) { + try { + exprs.push(this.parseExpression()); + } catch (e) { + if (e.parserError && e.name == 'eof') { + print("error: " + e.message); + break; + } + else { + throw(e); + } + } + } + this.expressions = exprs; +// print(''); +// EL.Util.pp(exprs); +// print(''); + return exprs; +}; + +EL.Parser.prototype.parseOne = function(string) { + return this.parse(string)[0]; +}; + +EL.Parser.prototype.parseUntil = function(regex, initial, next, consumeTerminator) { + var c, + token = initial, + condition = function(c){ return c.match(regex) == null; }; + while ((c = this.peek()) && condition(c)) { + token = next(token, this.consumeChar()); + } + if (consumeTerminator && this.peek()) this.consumeChar(); + return token; +}; + +EL.Parser.prototype.parseList = function() { + var list = [], + expr; + // consume initial paren '(' + this.consumeChar(); + while ((expr = this.parseExpression()) && expr != ')') { + list.push(expr); + } + return list; +}; + +EL.Parser.prototype.parseCons = function() { + var cons = [], + expr; + // consume initial paren '(' + this.consumeChar(); + cons.push(this.parseExpression()); + // ignore . + this.parseExpression(); + cons.push(this.parseExpression()); + return cons; +}; + +EL.Parser.prototype.parseString = function() { + // consume initial quotation mark + this.consumeChar(); + var self = this; + return this.parseUntil(/"/, '', function(s,c){ + if (c == '\\') { + c = self.consumeChar(); + } + return s + c; + }, true /* consume terminator */); +}; + +EL.Parser.prototype.parseSymbol = function() { + var symbol = this.parseUntil(/[\s()]/, '', function(t,c){return t + c;}); + return symbol; +}; + +// Probably easy to break +EL.Parser.prototype.parseRegex = function() { + // consume initial slash + this.consumeChar(); + var self = this; + return new RegExp(this.parseUntil(/\//, '', function(s,c){ + if (c == '\\') { + c = self.consumeChar(); + } + return s + c; + }, true /* consume terminator */)); +}; + +EL.Parser.prototype.parseNumber = function() { + var sign = this.peek() == '-' ? this.consumeChar() : '+', + value = this.parseUntil(/[^\d]/, 0, function(n,c) { + return n*10 + parseInt(c); + }); + if (this.peek() == '.') { + this.consumeChar(); + var decimal = this.parseUntil(/[^\d]/, '', function(s,c) {return s + c;}); + value = parseFloat('' + value + '.' + decimal); + } + return sign == '-' ? -1*value : value; +}; + +EL.Parser.prototype.lookingAtNumber = function() { + var pos = this.pos, + rest = this.rest(), + match = rest.match(/^(-)?\d+(\.\d+)?[\s)\n]/) || rest.match(/^(-)?\d+(\.\d+)?$/); + return (match != null); +}; + +EL.Parser.prototype.lookingAtCons = function() { + var orig_pos = this.pos, + _ = this.consumeChar(), + _ = this.parseExpression(), + cdr = this.parseExpression(); + this.pos = orig_pos; // rewind, like it never happened. + return EL.typeOf(cdr) == 'array' && EL.isSymbol(cdr) && EL.val(cdr) == '.'; +}; + +EL.Parser.prototype.parseExpression = function() { + var value, + c = this.peek(); + if (c == '(' && this.lookingAtCons()) { + value = EL.consPair(this.parseCons()); + } + else if (c == '(') { + var list = this.parseList(); + value = (list.length > 0) ? EL.list(list) : EL.nil; + } + else if (c == ')') { + return this.consumeChar(); + } + else if (c == "'") { + this.consumeChar(); + value = EL.cons(EL.symbol('quote'), this.parseExpression()); + } + else if (c == '"') { + value = EL.string(this.parseString()); + } + else if (this.lookingAtNumber()) { + value = EL.number(this.parseNumber()); + } + else if (c) { + value = EL.symbol(this.parseSymbol()); + } + else { + if (this.pos == this.data.length) { + print('[error] no more input. unterminated string or list? (continuing anyway)'); + } + print('[warning] in EL.Parser.parseExpression: unrecognized char "' + c + '"'); + print('this.pos = ' + this.pos); + print('this.data.length = ' + this.data.length); + print('this.rest = ' + this.rest()); + } + this.consumeWhitespace(); + return value; +}; + +/**************************************************************** + ****************************************************************/ + diff --git a/el/primitives.js b/el/primitives.js new file mode 100644 index 0000000..a6d01d3 --- /dev/null +++ b/el/primitives.js @@ -0,0 +1,133 @@ +//// +// Emacs Lisp implementation in JavaScript. +// +// Copyright (c) 2009 Sami Samhuri - sami.samhuri@gmail.com +// +// Released under the terms of the MIT license. See the included file +// LICENSE. + + +/**************************************************************** + **** Primitives ************************************************ + ****************************************************************/ + +EL.PrimitiveVariables = [ + ['t', { + type: 'variable', + value: ['symbol', 't'], + docstring: "true" + }], + ['nil', { + type: 'variable', + value: ['symbol', 'nil'], + docstring: "nil" + }] +]; + +// 'this' is bound to the EL.Evaluator object +EL.PrimitiveFunctions = [ + ['symbol-name', { + type: 'primitive', + name: 'symbol-name', + params: ['symbol'], + body: function(symbol) { return EL.string(EL.val(symbol)); }, + docstring: "Return a symbol's name, a string." + }], + + ['string-match', { + type: 'primitive', + name: 'string-match', + params: ['regex', 'string', '&optional', 'start'], + body: function(regex, string, start) { + var index = start ? EL.val(start) : 0, + s = EL.val(string).substring(index), + match = s.match(new RegExp(EL.val(regex))), + found = match ? EL.number(s.indexOf(match[0])) : EL.nil; + return found; + }, + docstring: "Return the index of the char matching regex in string, beginning from start if available." + }], + + ['+', { + type: 'primitive', + name: '+', + params: [/* ... */], + body: function() { + var args = EL.Util.shallowCopy(arguments), + initial = EL.inferType(args), + type = initial[0]; + return EL.Util.reduce(function(sum, n) { + return [type, EL.val(sum) + EL.val(n)]; + }, initial, args); + }, + docstring: "add two numbers" + }], + ['-', { + type: 'primitive', + name: '-', + params: [/* ... */], + body: function() { + return EL.Util.foldr(function(diff, n) { + return EL.number(EL.val(diff) - EL.val(n)); + }, EL.number(0), EL.Util.shallowCopy(arguments)); + }, + docstring: "subtract two numbers" + }], + ['*', { + type: 'primitive', + name: '*', + params: [/* ... */], + body: function() { + return EL.Util.reduce(function(prod, n) { + return EL.number(EL.val(prod) * EL.val(n)); + }, EL.number(1), EL.Util.shallowCopy(arguments)); + }, + docstring: "multiply two numbers" + }], + ['/', { + type: 'primitive', + name: '/', + params: [/* ... */], + body: function() { + return EL.Util.foldr(function(quot, n) { + return EL.number(EL.val(quot) / EL.val(n)); + }, EL.number(1), EL.Util.shallowCopy(arguments)); + }, + docstring: "divide two numbers" + }], + ['print', { + type: 'primitive', + name: 'print', + params: ['x'], + body: function(x, tostring) { + var buffer = "", + tag = EL.tag(x); + function p(s) { + if (tostring) buffer += s; + else print(s); + } + if (tag == 'number' || tag == 'symbol' || tag == 'string') { + p(EL.val(x)); + } + else if (tag == 'lambda') { + var fn = EL.val(x); + p('(lambda ' + fn.name + ' (' + fn.params + ')\n'); + p(fn.body); // TODO lisp pretty print + p(')'); + } + else if (tag == 'list') { + var recurse = arguments.callee; // far easier to remember than Y + print('(', El.val(x).map(function(e){return (recurse(e, true) + ' ');}), ")"); + } + else { + print('unknown type: ' + x); + } + return EL.nil; + }, + docstring: "print an expression" + }] +]; + + +/**************************************************************** + ****************************************************************/ diff --git a/el/repl.js b/el/repl.js new file mode 100644 index 0000000..0a04f0c --- /dev/null +++ b/el/repl.js @@ -0,0 +1,51 @@ +//// +// Emacs Lisp implementation in JavaScript. +// +// Copyright (c) 2009 Sami Samhuri - sami.samhuri@gmail.com +// +// Released under the terms of the MIT license. See the included file +// LICENSE. + +EL.eval = function(exprs) { + var e = new EL.Evaluator(); + return e.evalExpressions(exprs); +}; + +EL.parse = function(string) { + var p = new EL.Parser(); + return p.parse(string); +}; + +EL.parseOne = function(string) { + return EL.parse(string)[0]; +}; + +EL.read = EL.parseOne; +EL.print = EL.Util.pp; + +EL.rep = function(string) { + EL.print(EL.eval(EL.parse(string))); +}; + +EL.repl = function() { + var p = new EL.Parser(), + e = new EL.Evaluator(); + while (true) { + print("elisp> "); // i don't want a newline, grrrr + try { + var line = readline(); + while (!line) { + line = readline(); + } + if (line.substring(0,1).toLowerCase() == 'q') return; + EL.print(e.eval(p.parseOne(line))); + } catch (x) { + if (x.evalError) { + print('[error] ' + x.message + ': ' + x.expression); + EL.print(x); + } + else throw(x); + } + } +}; + diff --git a/el/symtab.js b/el/symtab.js new file mode 100644 index 0000000..435bae6 --- /dev/null +++ b/el/symtab.js @@ -0,0 +1,78 @@ +//// +// Emacs Lisp implementation in JavaScript. +// +// Copyright (c) 2009 Sami Samhuri - sami.samhuri@gmail.com +// +// Released under the terms of the MIT license. See the included file +// LICENSE. + +/**************************************************************** + **** Symbol Table ********************************************** + ****************************************************************/ + +EL.SymbolTable = function(bindings) { + this.symbols = [[]]; + this.level = 0; + if (bindings) this.define(bindings); +}; + +EL.SymbolTable.prototype.lookupWithScope = function(name) { + var i = this.level, + symbol; + while (i >= 0) { + value = this.symbols[i][name]; + if (value) { + return [i, value]; + } + --i; + } + return null; +}; + +EL.SymbolTable.prototype.lookup = function(name) { + var pair = this.lookupWithScope(name); + return pair && pair[1]; +}; + +// store the given symbol/value pair in the symbol table at the current level. +EL.SymbolTable.prototype.define = function(name, value) { + if (value === undefined && EL.typeOf(name) == 'array') { + var bindings = name, + i = 0, + n = bindings.length, + scope = this.symbols[this.level]; + while (i < n) { + var name = bindings[i][0], + value = bindings[i][1]; +// print('name: ' + name); +// print('value: ' + value); + scope[name] = value; + ++i; + } + } + else { + this.symbols[this.level][name] = value; + } +}; + +EL.SymbolTable.prototype.pushScope = function(bindings) { +// print('>>> pushing scope <<<'); +// print('>>> level going from ' + this.level + ' to ' + (1+this.level)); +// print(bindings); + this.symbols[++this.level] = []; + if (bindings) this.define(bindings); +}; + +EL.SymbolTable.prototype.popScope = function() { + --this.level; +}; + +EL.SymbolTable.prototype.set = function(name, value) { + var pair = this.lookupWithScope(name), + level = pair[0]; + this.symbols[level][name] = value; +}; + +/**************************************************************** + ****************************************************************/ + diff --git a/el/types.js b/el/types.js new file mode 100644 index 0000000..dd02f0b --- /dev/null +++ b/el/types.js @@ -0,0 +1,113 @@ +//// +// Emacs Lisp implementation in JavaScript. +// +// Copyright (c) 2009 Sami Samhuri - sami.samhuri@gmail.com +// +// Released under the terms of the MIT license. See the included file +// LICENSE. + +// data types are simple tags +EL._defineTags = function() { + EL.tags = ['symbol', 'string', 'number', 'cons', 'lambda', 'regex']; + EL.tags.each = Array.prototype.each; + + // define constructors for the primitive types (box values) + // e.g. EL.symbol('foo') => ['symbol', 'foo'] + EL.tags.each(function(tag) { + // don't clobber custom constructors + if (EL[tag] === undefined) { + EL[tag] = function(value) { + return [tag, value]; + }; + } + // tag type tests + var isTag = function(expr) { + return (EL.tag(expr) == tag); + }; + EL['is' + tag.camelize()] = isTag; + }); +}; +EL.initHook(EL._defineTags); + +// shorthands to save my fingers +EL._defineConstants = function() { + EL.nil = EL.symbol('nil'); + EL.t = EL.symbol('t'); +}; +EL.initHook(EL._defineConstants); + + +// retrieve the tag from a value +EL.tag = function(expr) { + EL.assert(function() { var f='tag'; return EL.typeOf(expr) == 'array'; }, expr); + return expr[0]; +}; + +// unbox a value +EL.val = function(expr) { + EL.assert(function() { var f='val'; return EL.typeOf(expr) == 'array'; }, expr); + return expr[1]; +}; + +EL.symbolName = function(symbol) { +// EL.Util.pp(symbol); + EL.assert(function(){ var f='symbolName'; return EL.isSymbol(symbol); }, symbol); + return EL.val(symbol); +}; + +EL.isNilSymbol = function(expr) { + return (EL.isSymbol(expr) && EL.symbolName(expr) == 'nil'); +}; + +EL.isNil = function(expr) { + return EL.isNilSymbol(expr); +}; + +EL.list = function(exprs) { + var list = EL.nil, + i = exprs.length; + while (--i >= 0) { + list = EL.cons(exprs[i], list); + } + return list; +}; + +EL.isList = function(expr) { + return (EL.isNil(expr) || EL.isCons(expr)); +}; + +EL.isAtom = function(expr) { + return EL.isString(expr) || EL.isNumber(expr) || EL.isRegex(expr); +}; + +EL.inferType = function(exprs) { + var type = 'number', + initial = 0, + i = exprs.length-1; + while(i >= 0) { + if (EL.isString(exprs[i--])) { + type = 'string'; + initial = ''; + break; + } + } + return EL[type](initial); +}; + + +// special forms + +EL.isSpecialForm = function(name, expr) { + var tag = EL.tag(expr), + car = EL.typeOf(expr) == 'array' && EL.val(expr)[0], + thisName = car && EL.symbolName(car); + return (tag == 'cons' && thisName == name); +}; + +EL.isQuote = function(expr){return EL.isSpecialForm('quote', expr);}; +EL.isDefVar = function(expr){return EL.isSpecialForm('defvar', expr);}; +EL.isDefFunc = function(expr){return EL.isSpecialForm('defun', expr);}; +EL.isSet = function(expr){return EL.isSpecialForm('set', expr);}; +EL.isSetq = function(expr){return EL.isSpecialForm('setq', expr);}; +EL.isCond = function(expr){return EL.isSpecialForm('cond', expr);}; +EL.isIf = function(expr){return EL.isSpecialForm('if', expr);}; diff --git a/el/util.js b/el/util.js new file mode 100644 index 0000000..a0e1e96 --- /dev/null +++ b/el/util.js @@ -0,0 +1,143 @@ +//// +// Emacs Lisp implementation in JavaScript. +// +// Copyright (c) 2009 Sami Samhuri - sami.samhuri@gmail.com +// +// Released under the terms of the MIT license. See the included file +// LICENSE. + +/**************************************************************** + **** Utilities ************************************************* + ****************************************************************/ + +EL.Util = function(){}; + +// aka foldl +EL.Util.reduce = function(fn, accum, list) { + var i = 0, + n = list.length; + while (i < n) { + accum = fn(accum, list[i++]); + } + return accum; +}; + +EL.Util.foldr = function(fn, end, list) { + var i = list.length-1; + while (i >= 0) { + end = fn(list[i--], end); + } + return end; +}; + +EL.Util.shallowCopy = function(list) { + var i = 0, + n = list.length, + result = []; + while (i < n) { + result.push(list[i++]); + } + return result; +}; + + +// i'm sorry +EL.Util.pp = function(x, indent, key, noprint) { + if (indent === undefined) { + indent = 0; + } + + /* string to print at the end, the only way to print 2 strings in + * succession without a newline is to buffer them + */ + var buffer = ""; + var printB = function(s, newline) { + buffer += s; + if (newline) buffer += "\n"; + }; + var dumpBuffer = function(b) { + if (b === undefined) b = buffer; + if (buffer.length <= 72) { + print(buffer); + } + else { + var split = 72; + var c; + while ((c = b[split]) && c.match(/[a-zA-Z0-9"'\[\]_-]/)) --split; + print(b.substring(0, split)); + var next_chunk = b.substring(split); + if (next_chunk) dumpBuffer(next_chunk); + } + buffer = ""; + }; + + var space = ""; +// for (var j = 0; j < indent; j++) { +// space += " "; +// } + + switch (EL.typeOf(x)) { + case 'object': + if (key) { + printB(space + key + ': {'); + } + else { + printB(space + ' {'); + } + for (var a in x) { + printB(EL.Util.pp(x[a], 1+indent, a, true)); + printB(', '); + } + printB(space + "} "); + break; + + case 'string': + if (key) { + printB(space + key + ': "' + x + '"'); + } + else { + printB(space + '"' + x + '"'); + } + return buffer; + + case 'array': + // nil special case + if (x.length == 2 && EL.tag(x) == 'symbol' && EL.val(x) == 'nil') { + return EL.Util.pp(null, indent, key); + } + + if (key) { + printB(space + key + ': ['); + } + else { + printB(space + '['); + } + var n = x.length, i = 0; + while (i < n) { + if (i > 0) printB(', '); + printB(EL.Util.pp(x[i++], 1+indent, undefined, true)); + } + printB(space + ']'); + break; + + case 'null': + if (key) { + printB(space + key + ': (null)'); + } + else { + printB(space + '(null)'); + } + return buffer; + + default: + if (key) { + printB(space + key + ": " + x); + } + else { + printB(space + x); + } + return buffer; + } + if (noprint) return buffer; + else dumpBuffer(); +};