From 6bd1b4d27ffe6a0310012023d4b980a9be3a9b4c Mon Sep 17 00:00:00 2001 From: "Elf M. Sternberg" Date: Mon, 16 Mar 2015 10:17:51 -0700 Subject: [PATCH] Updated to make the internals more 'lispy.' I've introduced a lisp-like singly-linked list handler, and removed all of the object-oriented materials. There are still *records*, but those are different from scoped objects in the given context. This has really helped me understand what's going on with the difference between the dynamic and lexical scopes, which has been one of my biggest headaches. --- .gitignore | 1 + lib/eval.coffee | 6 ++-- lib/fn.coffee | 31 ++++++++++----------- lib/lisp.coffee | 4 +-- lib/lists.coffee | 71 +++++++++++++++++++++++++++++++++++++++++++++++ lib/lookup.coffee | 8 ++++++ lib/scope.coffee | 64 ++++++++++++++---------------------------- 7 files changed, 122 insertions(+), 63 deletions(-) create mode 100644 lib/lists.coffee create mode 100644 lib/lookup.coffee diff --git a/.gitignore b/.gitignore index e7ee1d2..19cc150 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ *.orig npm-debug.log node_modules/* +lib/lisp_parser.js tmp/ diff --git a/lib/eval.coffee b/lib/eval.coffee index 6988489..405a6e4 100644 --- a/lib/eval.coffee +++ b/lib/eval.coffee @@ -1,13 +1,15 @@ +lookup = require './lookup' + lispeval = (element, scope) -> switch element.type when 'boolean' then element.value == '#t' when 'number' then parseInt(element.value, 10) when 'symbol' - scope.lookup(element.value) + lookup(scope, element.value) when 'list' proc = lispeval(element.value[0], scope) args = element.value.slice(1) - proc.apply(args, scope) + proc args, scope else throw new Error ("Unrecognized type in parse") diff --git a/lib/fn.coffee b/lib/fn.coffee index a6ff487..230d559 100644 --- a/lib/fn.coffee +++ b/lib/fn.coffee @@ -1,21 +1,20 @@ lispeval = require './eval' +{cons} = require './lists' -class Proc - constructor: (@scope, @params, @body) -> - apply: (cells, scope) -> - args = (cells.map (i) -> lispeval(i, scope)) - if @body instanceof Function - @body.apply(this, args) +module.exports = + create_expression_evaluator: (defining_scope, params, body) -> + if body instanceof Function + (cells, scope) -> + args = cells.map (i) -> lispeval i, scope + body.apply null, args else - inner = @scope.fork() - @params.forEach((name, i) -> inner.set(name, args[i])) - @body.map((e) -> lispeval(e, inner)).pop() + (cells, scope) -> + args = cells.map (i) -> lispeval i, scope + new_scope = {} + params.forEach (name, i) -> new_scope[name] = args[i] + inner = cons(new_scope, defining_scope) + body.map((i) -> lispeval i, inner).pop() -class Syntax extends Proc - apply: (cells, scope) -> - return @body(cells, scope) - + create_special_form_evaluator: (defining_scope, params, body) -> + (cells, scope) -> body(cells, scope) -module.exports = - Proc: Proc - Syntax: Syntax diff --git a/lib/lisp.coffee b/lib/lisp.coffee index 69e84c3..e041ba1 100644 --- a/lib/lisp.coffee +++ b/lib/lisp.coffee @@ -1,12 +1,12 @@ fs = require 'fs' {parse} = require './lisp_parser' lisp = require './parser' -Scope = require './scope' +{scope} = require './scope' {inspect} = require 'util' module.exports = run: (pathname) -> text = fs.readFileSync(pathname, 'utf8') ast = parse(text) - return lisp(ast, new Scope.Toplevel()) + return lisp(ast, scope) diff --git a/lib/lists.coffee b/lib/lists.coffee new file mode 100644 index 0000000..fa5b879 --- /dev/null +++ b/lib/lists.coffee @@ -0,0 +1,71 @@ +vectorp = (a) -> toString.call(a) == '[object Array]' + +listp = (a) -> vectorp(a) and a.__list == true + +recordp = (a) -> Object.prototype.toString.call(a) == '[object Object]' + +cons = (a, b = nil) -> + # Supporting an identity + l = if not (a?) then [] else if (nilp a) then b else [a, b] + l.__list = true + l + +nil = cons() + +car = (a) -> a[0] + +cdr = (a) -> a[1] + +nilp = (a) -> listp(a) and a.length == 0 + +vectorToList = (v, p) -> + p = if p? then p else 0 + if p >= v.length then return nil + # Annoying, but since lists are represented as nested arrays, they + # have to be intercepted first. The use of duck-typing here is + # frustrating, but I suppose the eventual runtime will be doing + # something like this anyway for base types. + item = if listp(v[p]) then v[p] else if vectorp(v[p]) then vectorToList(v[p]) else v[p] + cons(item, vectorToList(v, p + 1)) + +list = (v...) -> vectorToList v + +listToVector = (l, v = []) -> + return v if nilp l + v.push if listp (car l) then listToVector(car l) else (car l) + listToVector (cdr l), v + +# This is the simplified version. It can't be used stock with reader, +# because read() returns a rich version decorated with extra +# information. + +listToString = (l) -> + return "" if nilp l + if listp (car l) + "(" + (listToString(car l)).replace(/\ *$/, "") + ") " + listToString(cdr l) + else + p = if typeof (car l) == 'string' then '"' else '' + p + (car l).toString() + p + ' ' + listToString(cdr l) + +module.exports = + cons: cons + nil: nil + car: car + cdr: cdr + list: list + nilp: nilp + listp: listp + vectorp: vectorp + recordp: recordp + vectorToList: vectorToList + listToVector: listToVector + listToString: listToString + cadr: (l) -> car (cdr l) + cddr: (l) -> cdr (cdr l) + cdar: (l) -> cdr (car l) + caddr: (l) -> car (cdr (cdr l)) + cdddr: (l) -> cdr (cdr (cdr l)) + cadar: (l) -> car (cdr (car l)) + cddar: (l) -> cdr (cdr (car l)) + caadr: (l) -> car (car (cdr l)) + cdadr: (l) -> cdr (car (cdr l)) diff --git a/lib/lookup.coffee b/lib/lookup.coffee new file mode 100644 index 0000000..3200ddf --- /dev/null +++ b/lib/lookup.coffee @@ -0,0 +1,8 @@ +{nilp, car, cdr} = require './lists' + +module.exports = lookup = (scopes, name) -> + throw new Error "Unknown variable '#{name}'" if nilp scopes + scope = car scopes + return scope[name] if scope[name]? + lookup((cdr scopes), name) + diff --git a/lib/scope.coffee b/lib/scope.coffee index 29cbb90..416802e 100644 --- a/lib/scope.coffee +++ b/lib/scope.coffee @@ -1,50 +1,28 @@ parser = require './parser' lispeval = require './eval' +{cons, car, cdr, nilp} = require './lists' +{create_expression_evaluator, create_special_form_evaluator} = require './fn' +lookup = require './lookup' -{Proc, Syntax} = require './fn' +scope = cons + '+': create_expression_evaluator scope, [], (a, b) -> a + b + '-': create_expression_evaluator scope, [], (a, b) -> a - b + '*': create_expression_evaluator scope, [], (a, b) -> a * b + '/': create_expression_evaluator scope, [], (a, b) -> a / b + '==': create_expression_evaluator scope, [], (a, b) -> a == b -class Scope - constructor: (@parent) -> - @_symbols = {} + 'define': create_special_form_evaluator scope, [], (list, scope) -> + current = (car scope) + current[list[0].value] = lispeval(list[1], scope) - lookup: (name) -> - if @_symbols[name]? - return @_symbols[name] + 'lambda': create_special_form_evaluator scope, [], (list, scope) -> + params = list[0].value.map (n) -> return n.value + create_expression_evaluator(scope, params, list.slice(1)) + + 'if': create_special_form_evaluator scope, [], (list, scope) -> + lispeval(list[if lispeval(list[0], scope) then 1 else 2], scope) - if @parent - return @parent.lookup(name) - throw new Error "Unknown variable '#{name}'" - - define: (name, body) -> - @set name, (new Proc(this, [], body)) - - syntax: (name, body) -> - @set name, (new Syntax(this, [], body)) - - fork: -> new Scope(@) - - set: (name, value) -> - @_symbols[name] = value - -class Toplevel extends Scope - constructor: (@parent = null) -> - super - @define '+', (a, b) -> a + b - @define '-', (a, b) -> a - b - @define '*', (a, b) -> a * b - @define '==', (a, b) -> a == b - - @syntax 'define', (list, scope) -> - scope.set(list[0].value, lispeval(list[1], scope)) - - @syntax 'lambda', (list, scope) -> - params = list[0].value.map (n) -> return n.value - new Proc(scope, params, list.slice(1)) - - @syntax 'if', (list, scope) -> - lispeval(list[if lispeval(list[0], scope) then 1 else 2], scope) - -Scope.Toplevel = Toplevel - -module.exports = Scope +module.exports = + lookup: lookup + scope: scope