[refactor] Is this the functor/applicative/monadic life?
This is a big change. For chapter 5, I ripped out all line/column tracking and most error handling from the parser; it's now a plain ol' Lisp parser, and if it's not close to CL 22.1.1, it's a hell of a lot closer than it used to be. In doing so, I reduced the size of the parser by about 40 lines. TrackingReader takes every function in a Reader and puts that debugging information *back*. It turns out that all that information was prelude and postlude to the act of parsing; by wrapping each function in a decorator I was able to restore all that information, and I only had to get it right exactly *once*. In functional programming terms, this lifts: IO -> (atom | list) to: IO with tracking -> Node (atom | list) with tracking It's a totally free win without having to do much extra work. Now, this check-in isn't perfect. The tracking reader is still tossing on some things, and because I don't have a robust type system (it is Coffeescript, after all), I'm having to do the decorating and wrapping by hand. But I'm definitely on my way to understanding the issues, and having a grasp on functors and monoids.
This commit is contained in:
parent
981baec645
commit
db2e93b2f3
|
@ -1,6 +1,6 @@
|
|||
{car, cdr, cons, nil, nilp, pairp, vectorToList, list} = require 'cons-lists/lists'
|
||||
{inspect} = require "util"
|
||||
{Node, Comment, Symbol} = require "../chapter5/reader_types"
|
||||
{Comment, Symbol} = require "../chapter5/reader_types"
|
||||
|
||||
NEWLINES = ["\n", "\r", "\x0B", "\x0C"]
|
||||
WHITESPACE = [" ", "\t"].concat(NEWLINES)
|
||||
|
@ -8,6 +8,10 @@ WHITESPACE = [" ", "\t"].concat(NEWLINES)
|
|||
EOF = new (class Eof)()
|
||||
EOO = new (class Eoo)()
|
||||
|
||||
class ReadError extends Error
|
||||
name: 'LispInterpreterError'
|
||||
constructor: (@message) ->
|
||||
|
||||
class Source
|
||||
constructor: (@inStream) ->
|
||||
@index = 0
|
||||
|
@ -32,33 +36,6 @@ class Source
|
|||
skipWS = (inStream) ->
|
||||
while inStream.peek() in WHITESPACE then inStream.next()
|
||||
|
||||
# msg -> (IO -> IO, Node)
|
||||
handleError = (message) ->
|
||||
(line, column) -> new Node('error', message, line, column)
|
||||
|
||||
# IO -> (IO, Node)
|
||||
readComment = (inStream) ->
|
||||
[line, column] = inStream.position()
|
||||
r = (while inStream.peek() != "\n" and not inStream.done()
|
||||
inStream.next()).join("")
|
||||
if not inStream.done()
|
||||
inStream.next()
|
||||
new Node 'comment', (new Comment r), line, column
|
||||
|
||||
# IO -> (IO, Node) | 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()
|
||||
new Node 'string', (string.join ''), line, column
|
||||
|
||||
# (String) -> (Symbol | Number) | Nothing
|
||||
readMaybeNumber = (symbol) ->
|
||||
if symbol[0] == '+'
|
||||
return readMaybeNumber symbol.substr(1)
|
||||
|
@ -75,129 +52,116 @@ readMaybeNumber = (symbol) ->
|
|||
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 new Node 'number', number, line, column
|
||||
new Node (new Symbol symbol), line, column
|
||||
|
||||
|
||||
# (Delim, TypeName) -> IO -> (IO, Node) | Error
|
||||
# (Delim, TypeName) -> IO -> (IO, Node) | Errorfor
|
||||
makeReadPair = (delim, type) ->
|
||||
# IO -> (IO, Node) | Error
|
||||
(inStream) ->
|
||||
inStream.next()
|
||||
skipWS inStream
|
||||
[line, column] = inStream.position()
|
||||
if inStream.peek() == delim
|
||||
inStream.next()
|
||||
return new Node type, nil, line, column
|
||||
inStream.next() unless inStream.done()
|
||||
return if type then cons((new Symbol type), nil) else nil
|
||||
|
||||
# IO -> (IO, Node) | Error
|
||||
dotted = false
|
||||
readEachPair = (inStream) ->
|
||||
[line, column] = inStream.position()
|
||||
obj = read inStream, true, null, true
|
||||
readEachPair = (inStream) =>
|
||||
obj = @read inStream, true, null, true
|
||||
if inStream.peek() == delim
|
||||
if dotted then return obj
|
||||
return cons obj, nil
|
||||
if inStream.done() then return handleError("Unexpected end of input")(line, column)
|
||||
if dotted then return handleError("More than one symbol after dot")
|
||||
return obj if obj.type == 'error'
|
||||
if obj.type == 'symbol' and obj.value == '.'
|
||||
return obj if obj instanceof ReadError
|
||||
if inStream.done() then return new ReadError "Unexpected end of input"
|
||||
if dotted then return new ReadError "More than one symbol after dot in list"
|
||||
if obj instanceof Symbol and obj.name == '.'
|
||||
dotted = true
|
||||
return readEachPair inStream
|
||||
cons obj, readEachPair inStream
|
||||
|
||||
ret = new Node type, readEachPair(inStream), line, column
|
||||
obj = readEachPair(inStream)
|
||||
inStream.next()
|
||||
ret
|
||||
if type then cons((new Symbol type), obj) else obj
|
||||
|
||||
# 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 obj.type == 'error'
|
||||
new Node "list", cons((new Node("symbol", (new Symbol type), line1, column1)), cons(obj)), line, column
|
||||
return obj if obj instanceof ReadError
|
||||
cons((new Symbol type), obj)
|
||||
|
||||
# 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.
|
||||
class Reader
|
||||
"symbol": (inStream) ->
|
||||
symbol = (until (inStream.done() or @[inStream.peek()]? or inStream.peek() in WHITESPACE)
|
||||
inStream.next()).join ''
|
||||
number = readMaybeNumber symbol
|
||||
if number?
|
||||
return number
|
||||
new Symbol symbol
|
||||
|
||||
"read": (inStream, eofErrorP = false, eofError = EOF, recursiveP = false, keepComments = false) ->
|
||||
inStream = if inStream instanceof Source then inStream else new Source inStream
|
||||
|
||||
c = inStream.peek()
|
||||
|
||||
# (IO, Char) -> (IO, Node) | Error
|
||||
matcher = (inStream, c) =>
|
||||
if inStream.done()
|
||||
return if recursiveP then (new ReadError 'EOF while processing nested object') else nil
|
||||
if c in WHITESPACE
|
||||
inStream.next()
|
||||
return nil
|
||||
if c == ';'
|
||||
return readComment(inStream)
|
||||
ret = if @[c]? then @[c](inStream) else @symbol(inStream)
|
||||
skipWS inStream
|
||||
ret
|
||||
|
||||
while true
|
||||
form = matcher inStream, c
|
||||
skip = (not nilp form) and (form instanceof Comment) and not keepComments
|
||||
break if (not skip and not nilp form) or inStream.done()
|
||||
c = inStream.peek()
|
||||
null
|
||||
form
|
||||
|
||||
'(': makeReadPair ')', null
|
||||
|
||||
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 -> IO, Node
|
||||
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) ->
|
||||
inStream.next()
|
||||
s = until inStream.peek() == '"' or inStream.done()
|
||||
if inStream.peek() == '\\'
|
||||
inStream.next()
|
||||
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
|
||||
return (new ReadError "end of file seen before end of string") if inStream.done()
|
||||
inStream.next()
|
||||
s.join ''
|
||||
|
||||
while true
|
||||
form = matcher inStream, c
|
||||
skip = (not nilp form) and (form.type == 'comment') and not keepComments
|
||||
break if (not skip and not nilp form) or inStream.done()
|
||||
c = inStream.peek()
|
||||
null
|
||||
form
|
||||
')': (inStream) -> new ReadError "Closing paren encountered"
|
||||
|
||||
# readForms assumes that the string provided contains zero or more
|
||||
# forms. As such, it always returns a list of zero or more forms.
|
||||
']': (inStream) -> new ReadError "Closing bracket encountered"
|
||||
|
||||
# IO -> (IO, Nodes* | Error)
|
||||
readForms = (inStream) ->
|
||||
inStream = if inStream instanceof Source then inStream else new Source inStream
|
||||
return nil if inStream.done()
|
||||
'}': (inStream) -> new ReadError "Closing curly without corresponding opening."
|
||||
|
||||
# IO -> (IO, Nodes* | Error
|
||||
[line, column] = inStream.position()
|
||||
readEach = (inStream) ->
|
||||
obj = read inStream, true, null, false
|
||||
return nil if (nilp obj)
|
||||
return obj if obj.type == 'error'
|
||||
cons obj, readEach inStream
|
||||
"`": prefixReader 'back-quote'
|
||||
|
||||
obj = readEach inStream
|
||||
if obj.type == 'error' then obj else new Node "list", obj, line, column
|
||||
"'": prefixReader 'quote'
|
||||
|
||||
exports.read = read
|
||||
exports.readForms = readForms
|
||||
exports.Node = Node
|
||||
exports.Symbol = Symbol
|
||||
",": prefixReader 'unquote'
|
||||
|
||||
";": (inStream) ->
|
||||
r = (while inStream.peek() != "\n" and not inStream.done()
|
||||
inStream.next()).join("")
|
||||
inStream.next() if not inStream.done()
|
||||
new Comment r
|
||||
|
||||
exports.Source = Source
|
||||
exports.ReadError = ReadError
|
||||
exports.Reader = Reader
|
||||
reader = new Reader()
|
||||
exports.read = -> reader.read.apply(reader, arguments)
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
{car, cdr, cons, listp, nilp, nil,
|
||||
list, pairp, listToString} = require 'cons-lists/lists'
|
||||
|
||||
{Symbol, Comment} = require './reader_types'
|
||||
|
||||
exports.normalize = normalize = (form) ->
|
||||
return nil if nilp form
|
||||
|
||||
methods =
|
||||
'vector': (form) ->
|
||||
until (nilp form) then p = normalize(car form); form = cdr form; p
|
||||
|
||||
'record': (form) ->
|
||||
o = Object.create(null)
|
||||
until (nilp form)
|
||||
o[(normalize car form)] = (normalize car cdr form)
|
||||
form = cdr cdr form
|
||||
null
|
||||
o
|
||||
|
||||
if (listp form) and (car form) instanceof Symbol
|
||||
if (car form).name in ['vector', 'record']
|
||||
methods[(car form).name](cdr form)
|
||||
else
|
||||
cons (normalize car form), (normalize cdr form)
|
||||
else
|
||||
form
|
|
@ -0,0 +1,37 @@
|
|||
{car, cdr, cons, listp, nilp, nil,
|
||||
list, pairp, listToString} = require 'cons-lists/lists'
|
||||
|
||||
{Symbol, Comment} = require './reader_types'
|
||||
|
||||
exports.normalize = normalize = (form) ->
|
||||
_normalize = (form) ->
|
||||
return nil if nilp form.v
|
||||
|
||||
methods =
|
||||
'vector': (form) ->
|
||||
until (nilp form.v) then p = normalize(car form.v); form = cdr form.v; p
|
||||
|
||||
'record': (form) ->
|
||||
o = Object.create(null)
|
||||
until (nilp form.v)
|
||||
o[(normalize car form.v)] = (normalize car cdr form.v)
|
||||
form = cdr cdr form.v
|
||||
null
|
||||
o
|
||||
|
||||
'list': (form) ->
|
||||
handle = (form) ->
|
||||
return nil if (nilp form)
|
||||
return _normalize(form) if not (listp form)
|
||||
cons (_normalize car form), (handle cdr form)
|
||||
handle(form.v)
|
||||
|
||||
if (listp form.v)
|
||||
if (car form.v) instanceof Symbol and (car form.v).name in ['vector', 'record']
|
||||
methods[(car form.v).name](cdr form.v)
|
||||
else
|
||||
methods.list(form)
|
||||
else
|
||||
form.v
|
||||
|
||||
_normalize(form)
|
|
@ -0,0 +1,22 @@
|
|||
{Reader, ReadError, Source} = require './reader'
|
||||
{Node} = require './reader_types'
|
||||
|
||||
liftToTrack = (f) ->
|
||||
(ioStream) ->
|
||||
ioStream = if ioStream instanceof Source then ioStream else new Source ioStream
|
||||
[line, column] = ioStream.position()
|
||||
obj = f.apply(this, arguments)
|
||||
if obj instanceof ReadError
|
||||
obj['line'] = line
|
||||
obj['column'] = column
|
||||
return obj
|
||||
if obj instanceof Node then obj else new Node obj, line, column
|
||||
|
||||
TrackingReader = class
|
||||
for own key, func of Reader::
|
||||
TrackingReader::[key] = liftToTrack(func)
|
||||
|
||||
exports.ReadError = ReadError
|
||||
exports.Reader = TrackingReader
|
||||
exports.reader = reader = new TrackingReader()
|
||||
exports.read = -> reader.read.apply(reader, arguments)
|
|
@ -0,0 +1,22 @@
|
|||
{cons, nil} = require "cons-lists/lists"
|
||||
exports.samples = [
|
||||
['nil', nil]
|
||||
['0', 0]
|
||||
['1', 1]
|
||||
['500', 500]
|
||||
['0xdeadbeef', 3735928559]
|
||||
['"Foo"', 'Foo']
|
||||
['(1)', cons(1)]
|
||||
['(1 2)', cons(1, (cons 2))]
|
||||
['(1 2 )', cons(1, (cons 2))]
|
||||
['( 1 2 )', cons(1, (cons 2))]
|
||||
['( 1 2 )', cons(1, (cons 2))]
|
||||
['("a" "b")', cons("a", (cons "b"))]
|
||||
['("a" . "b")', cons("a", "b")]
|
||||
['[]', []]
|
||||
['{}', {}]
|
||||
['[1 2 3]', [1, 2, 3]]
|
||||
# ['(1 2 3', 'error']
|
||||
['{"foo" "bar"}', {foo: "bar"}]
|
||||
]
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
chai = require 'chai'
|
||||
chai.should()
|
||||
expect = chai.expect
|
||||
|
||||
{cons, nil, nilp} = require "cons-lists/lists"
|
||||
{read} = require '../chapter5/reader'
|
||||
{normalize} = require '../chapter5/reader_rawtoform'
|
||||
{samples} = require './reader5_samples'
|
||||
|
||||
describe "Lisp reader functions", ->
|
||||
for [t, v] in samples
|
||||
do (t, v) ->
|
||||
it "should interpret #{t} as #{v}", ->
|
||||
res = normalize read t
|
||||
expect(res).to.deep.equal(v)
|
|
@ -0,0 +1,15 @@
|
|||
chai = require 'chai'
|
||||
chai.should()
|
||||
expect = chai.expect
|
||||
|
||||
{cons, nil, nilp} = require "cons-lists/lists"
|
||||
{read} = require '../chapter5/tracking_reader'
|
||||
{normalize} = require '../chapter5/reader_tracktoform'
|
||||
{samples} = require './reader5_samples'
|
||||
|
||||
describe "Tracker reader functions", ->
|
||||
for [t, v] in samples
|
||||
do (t, v) ->
|
||||
it "should interpret #{t} as #{v}", ->
|
||||
res = normalize read t
|
||||
expect(res).to.deep.equal(v)
|
Loading…
Reference in New Issue