diff --git a/Makefile b/Makefile index 7b69ed2..c290e79 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ lib/lisp_parser.js: lib/lisp_parser.peg node_modules/.bin/pegjs $< $@ -test: lib/lisp_parser.js +tests: lib/lisp_parser.js node_modules/.bin/coffee bin/lisp test.scm diff --git a/lib/reader.coffee b/lib/reader.coffee new file mode 100644 index 0000000..3c60dc7 --- /dev/null +++ b/lib/reader.coffee @@ -0,0 +1,197 @@ +{car, cdr, cons, nil, nilp, listp} = require './lists' + +NEWLINES = ["\n", "\r", "\x0B", "\x0C"] +WHITESPACE = [" ", "\t"].concat(NEWLINES) + +EOF = new (class) +EOO = new (class) + +class Source + constructor: (@inStream) -> + @index = 0 + @max = @inStream.length - 1 + @line = 0 + @column = 0 + + peek: -> @inStream[@index] + + position: -> [@line, @column] + + next: -> + c = @peek() + return EOF if @done() + @index++ + [@line, @column] = if @peek() in NEWLINES then [@line + 1, 0] else [@line, @column + 1] + c + + done: -> @index > @max + +# IO -> IO +skipWS = (inStream) -> + while inStream.peek() in WHITESPACE then inStream.next() + +# (type, value, line, column) -> (node {type, value, line, column)} +makeObj = (type, value, line, column) -> + cons type, (cons value, (cons line, (cons column))) + +# msg -> (IO -> Node => Error) +handleError = (message) -> + (line, column) -> makeObj('error', message, line, column) + +# IO -> Node => Comment +readComment = (inStream) -> + [line, column] = inStream.position() + r = (while inStream.peek() != "\n" and not inStream.done() + inStream.next()).join("") + if not inStream.done() + inStream.next() + makeObj 'comment', r, line, column + +# IO -> (Node => Literal => String) | Error +readString = (inStream) -> + [line, column] = inStream.position() + inStream.next() + string = until inStream.peek() == '"' or inStream.done() + if inStream.peek() == '\\' + inStream.next() + inStream.next() + if inStream.done() + return handleError("end of file seen before end of string.")(line, column) + inStream.next() + makeObj 'string', (string.join ''), line, column + +# (String) -> (Node => Literal => Number) | Nothing +readMaybeNumber = (symbol) -> + if symbol[0] == '+' + return readMaybeNumber symbol.substr(1) + if symbol[0] == '-' + ret = readMaybeNumber symbol.substr(1) + return if ret? then -1 * ret else undefined + if symbol.search(/^0x[0-9a-fA-F]+$/) > -1 + return parseInt(symbol, 16) + if symbol.search(/^0[0-9a-fA-F]+$/) > -1 + return parseInt(symbol, 8) + if symbol.search(/^[0-9]+$/) > -1 + return parseInt(symbol, 10) + if symbol.search(/^nil$/) > -1 + return nil + undefined + +# (IO, macros) -> (IO, Node => Number | Symbol) | Error +readSymbol = (inStream, tableKeys) -> + [line, column] = inStream.position() + symbol = (until (inStream.done() or inStream.peek() in tableKeys or inStream.peek() in WHITESPACE) + inStream.next()).join '' + number = readMaybeNumber symbol + if number? + return makeObj 'number', number, line, column + makeObj 'symbol', symbol, line, column + + +# (Delim, TypeName) -> IO -> (IO, node) | Error +makeReadPair = (delim, type) -> + # IO -> (IO, Node) | Error + (inStream) -> + inStream.next() + skipWS inStream + [line, column] = inStream.position() + if inStream.peek() == delim + inStream.next() + return makeObj(type, nil, line, column) + + # IO -> (IO, Node) | Error + readEachPair = (inStream) -> + [line, column] = inStream.position() + obj = read inStream, true, null, true + if inStream.peek() == delim then return cons obj, nil + if inStream.done() then return handleError("Unexpected end of input")(line, column) + return obj if (car obj) == 'error' + cons obj, readEachPair(inStream) + + ret = makeObj type, readEachPair(inStream), line, column + inStream.next() + ret + +# Type -> (IO -> (IO, Node)) +prefixReader = (type) -> + # IO -> (IO, Node) + (inStream) -> + [line, column] = inStream.position() + inStream.next() + [line1, column1] = inStream.position() + obj = read inStream, true, null, true + return obj if (car obj) == 'error' + makeObj "list", cons((makeObj("symbol", type, line1, column1)), cons(obj)), line, column + +# I really wanted to make anything more complex than a list (like an +# object or a vector) something handled by a read macro. Maybe in a +# future revision I can vertically de-integrate these. + +readMacros = + '"': readString + '(': makeReadPair ')', 'list' + ')': handleError "Closing paren encountered" + '[': makeReadPair ']', 'vector' + ']': handleError "Closing bracket encountered" + '{': makeReadPair('}', 'record', (res) -> + res.length % 2 == 0 and true or mkerr "record key without value") + '}': handleError "Closing curly without corresponding opening." + "`": prefixReader 'back-quote' + "'": prefixReader 'quote' + ",": prefixReader 'unquote' + ";": readComment + + +# Given a stream, reads from the stream until a single complete lisp +# object has been found and returns the object + +# IO -> Form +read = (inStream, eofErrorP = false, eofError = EOF, recursiveP = false, inReadMacros = null, keepComments = false) -> + inStream = if inStream instanceof Source then inStream else new Source inStream + inReadMacros = if InReadMacros? then inReadMacros else readMacros + inReadMacroKeys = (i for i of inReadMacros) + + c = inStream.peek() + + # (IO, Char) -> (IO, Node) | Error + matcher = (inStream, c) -> + if inStream.done() + return if recursiveP then handleError('EOF while processing nested object')(inStream) else nil + if c in WHITESPACE + inStream.next() + return nil + if c == ';' + return readComment(inStream) + ret = if c in inReadMacroKeys then inReadMacros[c](inStream) else readSymbol(inStream, inReadMacroKeys) + skipWS inStream + ret + + while true + form = matcher inStream, c + skip = (not nilp form) and (car form == 'comment') and not keepComments + break if (not skip and not nilp form) or inStream.done() + c = inStream.peek() + null + form + +# readForms assumes that the string provided contains zero or more +# forms. As such, it always returns a list of zero or more forms. + +# IO -> (Form* | Error) +readForms = (inStream) -> + inStream = if inStream instanceof Source then inStream else new Source inStream + return nil if inStream.done() + + # IO -> (FORM*, IO) | Error + [line, column] = inStream.position() + readEach = (inStream) -> + obj = read inStream, true, null, false + return nil if (nilp obj) + return obj if (car obj) == 'error' + cons obj, readEach inStream + + obj = readEach inStream + if (car obj) == 'error' then obj else makeObj "list", obj, line, column + +exports.read = read +exports.readForms = readForms diff --git a/lib/reduce.coffee b/lib/reduce.coffee new file mode 100644 index 0000000..3ed59e6 --- /dev/null +++ b/lib/reduce.coffee @@ -0,0 +1,50 @@ +{car, cdr, cons, listp, nilp, nil, list, listToString} = require './lists' + +reduce = (lst, iteratee, memo, context) -> + count = 0 + ptr = lst + while not nilp ptr + [item, ptr] = [(car ptr), (cdr ptr)] + memo = iteratee.call(context, memo, item, count, lst) + count++ + iteratee.call(context, memo, nil, count, lst) + +map = (lst, iteratee, context) -> + return nil if nilp lst + root = cons("") + + reducer = (memo, item, count) -> + next = cons(iteratee.call(context, item, count, lst)) + memo[1] = next + next + + reduce(lst, reducer, root, context) + (cdr root) + +rmap = (lst, iteratee, context) -> + reducer = (memo, item, count) -> + cons(iteratee.call(context, item, count, lst), memo) + reduce(lst, reducer, nil, context) + + +filter = (lst, iteratee, context) -> + return nil if nilp lst + root = cons("") + + reducer = (memo, item, count) -> + if iteratee.call(context, item, count, lst) + next = cons(item) + memo[1] = next + next + else + memo + + reduce(lst, reducer, root, context) + (cdr root) + +module.exports = + reduce: reduce + map: map + rmap: rmap + filter: filter + diff --git a/test/t.scm b/test/t.scm new file mode 100644 index 0000000..27ba77d --- /dev/null +++ b/test/t.scm @@ -0,0 +1 @@ +true