Repository: adamhaney/lispy (language: python)

Issue:
Integrating norvig's lispy2 implementation 
This closes #16 



Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 497f137..8757e01 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,4 +37,6 @@ nosetests.xml
 *~
 venv/
 \#*
-.\#*
\ No newline at end of file
+.\#*
+
+originals/
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index 97ccff1..2503404 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,7 +3,9 @@ python:
   - "2.7"
   - "3.2"
   - "3.3"
+
 script:
+  - python setup.py install
   - pep8 .
-  - pyflakes .
+#  - pyflakes .
   - python setup.py nosetests
\ No newline at end of file
diff --git a/README.md b/README.md
index 33bd923..9c01f36 100644
--- a/README.md
+++ b/README.md
@@ -115,4 +115,15 @@ theory and complexity of language design. My roadmap includes several
 additions to lispy to make it a toolkit for python developers who
 might be interested in dipping their toe in LISP. For functionality
 that's left to be implemented please check the issue tracker for the
-project at https://github.com/adamhaney/lispy/issues
\ No newline at end of file
+project at https://github.com/adamhaney/lispy/issues
+
+Anti-Goals
+----------
+
+There are a few things this project is not aiming to achieve. While I
+do want lispy to be able to run in python 2.x and 3.x I do not intend
+to mask some of the differences between 2.x and 3.x namely the
+different ways the two python branches handle math (in 2.x integer
+division returns a truncated value, in 3.x it returns a float). Trying
+to translate between these two design decisions is outside the scope
+of this project.
diff --git a/lispy/__init__.py b/lispy/__init__.py
index f0481ac..8072eb0 100644
--- a/lispy/__init__.py
+++ b/lispy/__init__.py
@@ -12,6 +12,7 @@
 
 from .runtime import Runtime
 
+
 def cli():
     """
     The function that console_scripts points to for command line
diff --git a/lispy/dialects/haney/__init__.py b/lispy/dialects/haney/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/lispy/dialects/haney/special_forms.py b/lispy/dialects/haney/special_forms.py
deleted file mode 100644
index 93d8f34..0000000
--- a/lispy/dialects/haney/special_forms.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from lispy.dialects.norvig.special_forms import SPECIAL_FORMS as NORVIG_FORMS
-
-SPECIAL_FORMS = NORVIG_FORMS
diff --git a/lispy/dialects/norvig/__init__.py b/lispy/dialects/norvig/__init__.py
index 7d22537..2f4050a 100644
--- a/lispy/dialects/norvig/__init__.py
+++ b/lispy/dialects/norvig/__init__.py
@@ -1,70 +1,216 @@
-"""
-Lispy: Scheme Interpreter in Python
+############### Scheme Interpreter in Python
 
-(c) Peter Norvig, 2010; See http://norvig.com/lispy.html
+## (c) Peter Norvig, 2010; See http://norvig.com/lispy2.html
 
-"""
+################ Symbol, Procedure, classes
 
 from __future__ import division
+import re
+import sys
+from io import StringIO
 
-from .scope import Scope
+import six
 
-Symbol = str
+from .scope import Scope, add_globals
+from .symbols import Symbol, SYMBOLS, EOF_OBJECT
+from .special_forms import is_pair, read, cons
+from .parse import to_string
 
-def eval(x, env):
+TOKEN_REGEX = r"""\s*(,@|[('`,)]|"(?:[\\].|[^\\"])*"|;.*|[^\s('"`,;)]*)(.*)"""
+
+
+class Procedure(object):
+    "A user-defined Scheme procedure."
+
+    def __init__(self, parms, exp, env):
+        self.parms, self.exp, self.env = parms, exp, env
+
+    def __call__(self, *args):
+        return eval(self.exp, Scope(self.parms, args, self.env))
+
+################ parse, read, and user interaction
+
+
+def parse(inport):
+    "Parse a program: read and expand/error-check it."
+    # Backwards compatibility: given a str, convert it to an InPort
+    if isinstance(inport, str):
+        inport = InPort(StringIO(six.u(inport)))
+
+    return expand(read(inport), toplevel=True)
+
+
+class InPort(object):
+    "An input port. Retains a line of chars."
+    tokenizer = TOKEN_REGEX
+
+    def __init__(self, file):
+        self.file = file
+        self.line = ''
+
+    def next_token(self):
+        "Return the next token, reading new text into line buffer if needed."
+        while True:
+            if self.line == '':
+                self.line = self.file.readline()
+            if self.line == '':
+                return EOF_OBJECT
+            token, self.line = re.match(InPort.tokenizer, self.line).groups()
+            if token != '' and not token.startswith(';'):
+                return token
+
+
+################ Environment class
+
+global_env = add_globals(Scope())
+
+################ eval (tail recursive)
+
+
+def eval(x, env=global_env):
     "Evaluate an expression in an environment."
-    if isinstance(x, Symbol):             # variable reference
-        return env.find(x)[x]
-    elif not isinstance(x, list):         # constant literal
+    while True:
+        if isinstance(x, Symbol):       # variable reference
+            return env.find(x)[x]
+        elif not isinstance(x, list):   # constant literal
+            return x
+        elif x[0] is SYMBOLS["QUOTE"]:     # (quote exp)
+            (_, exp) = x
+            return exp
+        elif x[0] is SYMBOLS["IF"]:        # (if test conseq alt)
+            (_, test, conseq, alt) = x
+            x = (conseq if eval(test, env) else alt)
+        elif x[0] is SYMBOLS["SET!"]:       # (set! var exp)
+            (_, var, exp) = x
+            env.find(var)[var] = eval(exp, env)
+            return None
+        elif x[0] is SYMBOLS["DEFINE"]:    # (define var exp)
+            (_, var, exp) = x
+            env[var] = eval(exp, env)
+            return None
+        elif x[0] is SYMBOLS["LAMBDA"]:    # (lambda (var*) exp)
+            (_, vars, exp) = x
+            return Procedure(vars, exp, env)
+        elif x[0] is SYMBOLS["BEGIN"]:     # (begin exp+)
+            for exp in x[1:-1]:
+                eval(exp, env)
+            x = x[-1]
+        else:                    # (proc exp*)
+            exps = [eval(exp, env) for exp in x]
+            proc = exps.pop(0)
+            if isinstance(proc, Procedure):
+                x = proc.exp
+                env = Scope(proc.parms, exps, proc.env)
+            else:
+                return proc(*exps)
+
+################ expand
+
+
+def expand(x, toplevel=False):
+    "Walk tree of x, making optimizations/fixes, and signaling SyntaxError."
+    require(x, x != [])                    # () => Error
+    if not isinstance(x, list):                 # constant => unchanged
+        return x
+    elif x[0] is SYMBOLS["QUOTE"]:                 # (quote exp)
+        require(x, len(x) == 2)
         return x
-    elif x[0] == 'quote':          # (quote exp)
-        (_, exp) = x
-        return exp
-    elif x[0] == 'if':             # (if test conseq alt)
-        (_, test, conseq, alt) = x
-        return eval((conseq if eval(test, env) else alt), env)
-    elif x[0] == 'set!':           # (set! var exp)
-        (_, var, exp) = x
-        env.find(var)[var] = eval(exp, env)
-    elif x[0] == 'define':         # (define var exp)
-        (_, var, exp) = x
-        env[var] = eval(exp, env)
-    elif x[0] == 'lambda':         # (lambda (var*) exp)
-        (_, vars, exp) = x
-        return lambda *args: eval(exp, Scope(vars, args, env))
-    elif x[0] == 'begin':          # (begin exp*)
-        for exp in x[1:]:
-            val = eval(exp, env)
-        return val
-    else:                          # (proc exp*)
-        exps = [eval(exp, env) for exp in x]
-        proc = exps.pop(0)
-        return proc(*exps)
-
-def tokenize(s):
-    "Convert a string into a list of tokens."
-    return s.replace('(',' ( ').replace(')',' ) ').split()
-
-def read_from(tokens):
-    "Read an expression from a sequence of tokens."
-    if len(tokens) == 0:
-        raise SyntaxError('unexpected EOF while reading')
-    token = tokens.pop(0)
-    if '(' == token:
-        L = []
-        while tokens[0] != ')':
-            L.append(read_from(tokens))
-        tokens.pop(0) # pop off ')'
-        return L
-    elif ')' == token:
-        raise SyntaxError('unexpected )')
+    elif x[0] is SYMBOLS["IF"]:
+        if len(x) == 3:
+            x = x + [None]     # (if t c) => (if t c None)
+        require(x, len(x) == 4)
+        return list(map(expand, x))
+    elif x[0] is SYMBOLS["SET!"]:
+        require(x, len(x) == 3)
+        var = x[1]                       # (set! non-var exp) => Error
+        require(x, isinstance(var, Symbol), "can set! only a symbol")
+        return [SYMBOLS["SET!"], var, expand(x[2])]
+    elif x[0] is SYMBOLS["DEFINE"] or x[0] is SYMBOLS["DEFINE-MACRO"]:
+        require(x, len(x) >= 3)
+        _def, v, body = x[0], x[1], x[2:]
+        if isinstance(v, list) and v:           # (define (f args) body)
+            f, args = v[0], v[1:]        # => (define f (lambda (args) body))
+            return expand([_def, f, [SYMBOLS["LAMBDA"], args]+body])
+        else:
+            require(x, len(x) == 3)        # (define non-var/list exp) => Error
+            require(x, isinstance(v, Symbol), "can define only a symbol")
+            exp = expand(x[2])
+            if _def is SYMBOLS["DEFINE-MACRO"]:
+                require(x, toplevel, "define-macro only allowed at top level")
+                proc = eval(exp)
+                require(x, callable(proc), "macro must be a procedure")
+                macro_table[v] = proc    # (define-macro v proc)
+                return None              # => None; add v:proc to macro_table
+            return [SYMBOLS["DEFINE"], v, exp]
+    elif x[0] is SYMBOLS["BEGIN"]:
+        if len(x) == 1:
+            return None        # (begin) => None
+        else:
+            return [expand(xi, toplevel) for xi in x]
+    elif x[0] is SYMBOLS["LAMBDA"]:                # (lambda (x) e1 e2)
+        require(x, len(x) >= 3)            # => (lambda (x) (begin e1 e2))
+        vars, body = x[1], x[2:]
+        require(x,
+                (isinstance(vars, list)
+                 and all(isinstance(v, Symbol) for v in vars))
+                or isinstance(vars, Symbol), "illegal lambda argument list")
+        exp = body[0] if len(body) == 1 else [SYMBOLS["BEGIN"]] + body
+        return [SYMBOLS["LAMBDA"], vars, expand(exp)]
+    elif x[0] is SYMBOLS["QUASIQUOTE"]:            # `x => expand_quasiquote(x)
+        require(x, len(x) == 2)
+        return expand_quasiquote(x[1])
+    elif isinstance(x[0], Symbol) and x[0] in macro_table:
+        return expand(macro_table[x[0]](*x[1:]), toplevel)  # (m arg...)
+    else:                                # => macroexpand if m isinstance macro
+        return list(map(expand, x))            # (f arg...) => expand each
+
+
+def require(x, predicate, msg="wrong length"):
+    "Signal a syntax error if predicate is false."
+    if not predicate:
+        raise SyntaxError(to_string(x)+': '+msg)
+
+
+def expand_quasiquote(x):
+    """Expand `x => 'x; `,x => x; `(,@x y) => (append x y) """
+    if not is_pair(x):
+        return [SYMBOLS["QUOTE"], x]
+    require(x, x[0] is not SYMBOLS["UNQUOTE-SPLICING"], "can't splice here")
+    if x[0] is SYMBOLS["UNQUOTE"]:
+        require(x, len(x) == 2)
+        return x[1]
+    elif is_pair(x[0]) and x[0][0] is SYMBOLS["UNQUOTE-SPLICING"]:
+        require(x[0], len(x[0]) == 2)
+        return [SYMBOLS["APPEND"], x[0][1], expand_quasiquote(x[1:])]
     else:
-        return atom(token)
-
-def atom(token):
-    "Numbers become numbers; every other token is a symbol."
-    try: return int(token)
-    except ValueError:
-        try: return float(token)
-        except ValueError:
-            return Symbol(token)
+        return [
+            SYMBOLS["CONS"],
+            expand_quasiquote(x[0]),
+            expand_quasiquote(x[1:])
+        ]
+
+
+def let(*args):
+    args = list(args)
+    x = cons(SYMBOLS["LET"], args)
+    require(x, len(args) > 1)
+    bindings, body = args[0], args[1:]
+    require(x, all(
+            isinstance(b, list) and len(b) == 2 and isinstance(b[0], Symbol)
+            for b in bindings), "illegal binding list")
+    vars, vals = zip(*bindings)
+    return [
+        [SYMBOLS["LAMBDA"],
+         list(vars)] + list(map(expand, body))] + list(map(expand, vals))
+
+macro_table = {SYMBOLS["LET"]: let}  # More macros can go here
+
+eval(parse("""(begin
+(define-macro and (lambda args
+   (if (null? args) #t
+       (if (= (length args) 1) (car args)
+           `(if ,(car args) (and ,@(cdr args)) #f)))))
+
+;; More macros can also go here
+
+)"""))
diff --git a/lispy/dialects/norvig/parse.py b/lispy/dialects/norvig/parse.py
new file mode 100644
index 0000000..c002d59
--- /dev/null
+++ b/lispy/dialects/norvig/parse.py
@@ -0,0 +1,19 @@
+from .symbols import Symbol
+
+
+def to_string(x):
+    "Convert a Python object back into a Lisp-readable string."
+    if x is True:
+        return "#t"
+    elif x is False:
+        return "#f"
+    elif isinstance(x, Symbol):
+        return x
+    elif isinstance(x, str):
+        return '"%s"' % x.encode('string_escape').replace('"', r'\"')
+    elif isinstance(x, list):
+        return '('+' '.join(list(map(to_string, x)))+')'
+    elif isinstance(x, complex):
+        return str(x).replace('j', 'i')
+    else:
+        return str(x)
diff --git a/lispy/dialects/norvig/scope.py b/lispy/dialects/norvig/scope.py
index 8f28ef2..32a180d 100644
--- a/lispy/dialects/norvig/scope.py
+++ b/lispy/dialects/norvig/scope.py
@@ -1,39 +1,56 @@
 import importlib
 
+from .symbols import Symbol
+from .special_forms import SPECIAL_FORMS
+from .parse import to_string
+
+
 class Scope(dict):
-    "A scope: a dict of {'var':val} pairs, with an outer Scope."
+    "An environment: a dict of {'var':val} pairs, with an outer Env."
     def __init__(self, parms=(), args=(), outer=None):
-        self.update(zip(parms,args))
+        # Bind parm list to corresponding args, or single parm to list of args
         self.outer = outer
+        if isinstance(parms, Symbol):
+            self.update({parms: list(args)})
+        else:
+            if len(args) != len(parms):
+                raise TypeError('expected %s, given %s, '
+                                % (to_string(parms), to_string(args)))
+            self.update(zip(parms, args))
 
     def find(self, var):
-        "Find the innermost Scope where var appears."
-        # If ':' in var add module to environment
-        if ":" in var:
-
-            namespace, attribute = var.split(":")
-
-            if "py" == namespace:
-                return {"py:{}".format(attribute): eval(attribute)}
-
-            else:
-                module = importlib.import_module(namespace)
-
-            module_funcs = {
-                "{}:{}".format(namespace, k): attr
-                for k, attr
-                in vars(module).items()
-            }
-            self.update(module_funcs)
-        try:
-            return self if var in self else self.outer.find(var)
-        except AttributeError:
-            raise NameError("name '{}' is not defined".format(var))
-
-def add_globals(env, special_forms=None):
-    "Add some Scheme standard procedures to an environment."
+        "Find the innermost Env where var appears."
+        if var in self:
+            return self
+        elif self.outer is None:
+            # Check for outside modules in this conditional since we
+            #  know it's the top level and we've already not found the
+            #  variable
+            if ":" in var:
+                namespace, attribute = var.split(":")
+
+                if "py" == namespace:
+                    return {"py:{}".format(attribute): eval(attribute)}
+
+                else:
+                    module = importlib.import_module(namespace)
+
+                module_funcs = {
+                    "{}:{}".format(namespace, k): attr
+                    for k, attr
+                    in vars(module).items()
+                }
+                self.update(module_funcs)
+                return self
+
+            raise LookupError(var)
+        else:
+            return self.outer.find(var)
+
+
+def add_globals(self, special_forms=None):
+    "Add some Scheme standard procedures."
     if special_forms is None:
-        special_forms = {}
-
-    env.update(special_forms)
-    return env
+        special_forms = SPECIAL_FORMS
+    self.update(special_forms)
+    return self
diff --git a/lispy/dialects/norvig/special_forms.py b/lispy/dialects/norvig/special_forms.py
index 4ea534d..1c719e8 100644
--- a/lispy/dialects/norvig/special_forms.py
+++ b/lispy/dialects/norvig/special_forms.py
@@ -1,7 +1,9 @@
-from __future__ import print_function
-
 import sys
 import operator
+from functools import reduce
+
+from .symbols import QUOTES, EOF_OBJECT, atom, Symbol
+from .parse import to_string
 
 # Python 3 gives us more fine grained division operators
 if sys.version_info[0] > 2:
@@ -9,30 +11,106 @@
 else:
     division_function = operator.div
 
+
+def readchar(inport):
+    "Read the next character from an input port."
+    if inport.line != '':
+        ch, inport.line = inport.line[0], inport.line[1:]
+        return ch
+    else:
+        return inport.file.read(1) or EOF_OBJECT
+
+
+def cons(x, y):
+    return [x]+y
+
+
+def is_pair(x):
+    return x != [] and isinstance(x, list)
+
+
+def callcc(proc):
+    "Call proc with current continuation; escape only"
+    ball = RuntimeWarning("Sorry, can't continue this continuation any longer")
+
+    def throw(retval):
+        ball.retval = retval
+        raise ball
+
+    try:
+        return proc(throw)
+    except RuntimeWarning as w:
+        if w is ball:
+            return ball.retval
+        else:
+            raise w
+
+
+def read(inport):
+    "Read a Scheme expression from an input port."
+    def read_ahead(token):
+        if '(' == token:
+            L = []
+            while True:
+                token = inport.next_token()
+                if token == ')':
+                    return L
+                else:
+                    L.append(read_ahead(token))
+        elif ')' == token:
+            raise SyntaxError('unexpected )')
+        elif token in QUOTES:
+            return [QUOTES[token], read(inport)]
+        elif token is EOF_OBJECT:
+            raise SyntaxError('unexpected EOF in list')
+        else:
+            return atom(token)
+    # body of read:
+    token1 = inport.next_token()
+    return EOF_OBJECT if token1 is EOF_OBJECT else read_ahead(token1)
+
+
+def display(x, port=sys.stdout):
+    return port.write(x if isinstance(x, str) else to_string(x) + "\n")
+
 SPECIAL_FORMS = {
     '+': lambda *args: reduce(operator.add, args),
-    '-':operator.sub,
+    '+': operator.add,
+    '-': operator.sub,
     '*': lambda *args: reduce(operator.mul, args),
-    '/':division_function,
-    'not':operator.not_,
-    '>':operator.gt,
-    '<':operator.lt,
-    '>=':operator.ge,
-    '<=':operator.le,
-    '=':operator.eq,
-    'equal?':operator.eq,
-    'eq?':operator.is_,
-    'length':len,
-    'cons':lambda x,y:[x]+y,
-    'car':lambda x:x[0],
-    'cdr':lambda x:x[1:],
-    'append':operator.add,
-    'list':lambda *x:list(x),
-    'list?': lambda x:isa(x,list),
-    'null?':lambda x:x==[],
-    'symbol?':lambda x: isa(x, Symbol),
-    'true': True,
-    'false': False,
-    'exit': exit,
-    'print': print
+    '/': division_function,
+    'not': operator.not_,
+    '>': operator.gt,
+    '<': operator.lt,
+    '>=': operator.ge,
+    '<=': operator.le,
+    '=': operator.eq,
+    'equal?': operator.eq,
+    'eq?': operator.is_,
+    'length': len,
+    'cons': cons,
+    'car': lambda x: x[0],
+    'cdr': lambda x: x[1:],
+    'append': operator.add,
+    'list': lambda *x: list(x),
+    'list?': lambda x: isinstance(x, list),
+    'null?': lambda x: x == [],
+    'symbol?': lambda x: isinstance(x, Symbol),
+    'boolean?': lambda x: isinstance(x, bool),
+    'pair?': is_pair,
+    'port?': lambda x: isinstance(x, file),
+    'apply': lambda proc, l: proc(*l),
+    'eval': lambda x: eval(expand(x)),
+    'load': lambda fn: load(fn),
+    'call/cc': callcc,
+    'open-input-file': open,
+    'close-input-port': lambda p:  p.file.close(),
+    'open-output-file': lambda f: open(f, 'w'),
+    'close-output-port': lambda p:  p.close(),
+    'eof-object?': lambda x: x is EOF_OBJECT,
+    'read-char': readchar,
+    'read': read,
+    'write': lambda x, port=sys.stdout: port.write(to_string(x)),
+    'display': display,
+    'exit':  exit
 }
diff --git a/lispy/dialects/norvig/symbols.py b/lispy/dialects/norvig/symbols.py
new file mode 100644
index 0000000..a6a7967
--- /dev/null
+++ b/lispy/dialects/norvig/symbols.py
@@ -0,0 +1,67 @@
+import six
+
+
+class Symbol(str):
+    pass
+
+
+def Sym(s, symbol_table={}):
+    "Find or create unique Symbol entry for str s in symbol table."
+    if s not in symbol_table:
+        symbol_table[s] = Symbol(s)
+    return symbol_table[s]
+
+EOF_OBJECT = Symbol('#<eof-object>')  # Note: uninterned; can't be read
+
+SYMBOLS = {
+    str_.upper(): Sym(str_)
+    for str_
+    in [
+        "quote",
+        "if",
+        "set!",
+        "define",
+        "lambda",
+        "begin",
+        "define-macro",
+        "quasiquote",
+        "unquote",
+        "unquote-splicing",
+        "append",
+        "cons",
+        "let"
+    ]
+}
+
+QUOTES = {
+    "'": SYMBOLS["QUOTE"],
+    "`": SYMBOLS["QUASIQUOTE"],
+    ",": SYMBOLS["UNQUOTE"],
+    ",@": SYMBOLS["UNQUOTE-SPLICING"]
+}
+
+
+def atom(token):
+    """
+    Numbers become numbers; #t and #f are booleans; "..." string;
+    otherwise Symbol.
+    """
+    if token == '#t':
+        return True
+    elif token == '#f':
+        return False
+    elif token[0] == '"':
+        if six.PY3:
+            return token[1:-1]
+        else:
+            return token[1:-1].decode('string_escape')
+    try:
+        return int(token)
+    except ValueError:
+        try:
+            return float(token)
+        except ValueError:
+            try:
+                return complex(token.replace('i', 'j', 1))
+            except ValueError:
+                return Sym(token)
diff --git a/lispy/runtime.py b/lispy/runtime.py
index 7391dd9..79678a4 100644
--- a/lispy/runtime.py
+++ b/lispy/runtime.py
@@ -6,26 +6,16 @@
 
 from __future__ import print_function
 
-import re
 import os
 import sys
 import cmd
 import traceback
-import argparse
+from io import StringIO
 
-import lispy.dialects.haney.special_forms
-from lispy.dialects.norvig import read_from, tokenize, eval
-from lispy.dialects.norvig.scope import Scope, add_globals
-
-
-def read(s):
-    "Read a Scheme expression from a string."
-    return read_from(tokenize(s))
-
-
-def to_string(exp):
-    "Convert a Python object back into a Lisp-readable string."
-    return '('+' '.join(map(to_string, exp))+')' if isinstance(exp, list) else str(exp)
+from .dialects.norvig.scope import Scope, add_globals
+from .dialects.norvig import eval, InPort, parse
+from .dialects.norvig import EOF_OBJECT
+from .dialects.norvig.parse import to_string
 
 
 class Repl(cmd.Cmd):
@@ -52,47 +42,83 @@ def __init__(self, special_forms=None):
         """
         Initialize a runtime context.
 
-        special_forms: should be a class that inherits from the dict module (or just be a dict)
+        special_forms: should be a class that inherits from the dict
+        module (or just be a dict)
 
         """
 
         # spcial forms may be passed in, or read from the environment,
-        # by default they're the Haney combination of default dialects
+        # by default they're the norvig combination of default dialects
         if special_forms is None:
             special_forms = os.environ.get("LISPY_SPECIAL_FORMS_CLASS")
             if special_forms is None:
-                special_forms = lispy.dialects.haney.special_forms.SPECIAL_FORMS
+                from .dialects.norvig.special_forms import SPECIAL_FORMS
+                special_forms = SPECIAL_FORMS
 
         self.special_forms = special_forms
 
         self.global_env = add_globals(Scope(), special_forms=special_forms)
 
-    def repl(self, prompt='lis.py> '):
-        "A prompt-read-eval-print loop"
-        Repl(runtime=self).cmdloop()
+    def repl(
+        self,
+        prompt='lispy> ',
+        inport=InPort(sys.stdin),
+        out=sys.stdout,
+        err=sys.stderr,
+        return_value=False,
+        catch_exceptions=True
+    ):
+        "A prompt-read-eval-print loop."
+        if out is None:
+            out = StringIO()
+
+        if err is None:
+            err = StringIO()
+
+        while True:
+            try:
+                if prompt:
+                    sys.stderr.write(prompt)
+                x = parse(inport)
+                if x is EOF_OBJECT:
+                    return
+                val = eval(x)
+                if val is not None and out and return_value is False:
+                    err.write(to_string(val) + "\n")
+                    err.flush()
+                elif return_value:
+                    return val
+            except Exception as e:
+                if catch_exceptions:
+                    exc_type, exc_value, exc_traceback = sys.exc_info()
+                    traceback.print_exception(
+                        exc_type,
+                        exc_value,
+                        exc_traceback
+                    )
+                else:
+                    raise e
 
     def read_file(self, file):
         """
         grab the individual pieces of code from a file (the complete
         s-expressions) and evaluate them syncronously
         """
-        return self.eval(file.read())
-
-    def eval(self, str_):
-        ## preprocess
-
-        # Remove hash comments so we can support hashbangs
-        str_ = re.sub("\#.*", "\n", str_)
+        self.repl(None, InPort(file), None)
 
-        # Remove lisp style comments
-        str_ = re.sub(";.*", "\n", str_)
-
-        # wrap everything in an implicit begin statement
-        str_ = "(begin {})".format(str_)
-
-
-        try:
-            return eval(read(str_), env=self.global_env)
-        except Exception as e:
-            exc_type, exc_value, exc_traceback = sys.exc_info()
-            traceback.print_exception(exc_type, exc_value, exc_traceback)
+    def eval(self, expression, out=None, err=None):
+        """
+        Evaluate a string as a lispy program and return its value
+        """
+        # conditionally unicode the expression for python 3 compatibility
+        if sys.version_info[0] < 3:
+            expression = unicode(expression)
+
+        return self.repl(
+            None,
+            InPort(StringIO(expression)),
+            out,
+            err,
+            return_value=True,
+            catch_exceptions=False
+        )
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 30b0a3e..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,1 +0,0 @@
-nose==1.3.0
diff --git a/setup.py b/setup.py
index c8a7231..e4e4ab3 100644
--- a/setup.py
+++ b/setup.py
@@ -9,11 +9,12 @@
     author_email='adam.haney@akimbo.io',
     download_url='https://github.com/adamhaney/lispy',
     url='https://github.com/adamhaney/lispy',
-    tests_require=[
+    install_requires=[
         'pep8',
         'pyflakes',
         'nose==1.3.0',
-        'sh==1.08'
+        'sh==1.08',
+        'six'
     ],
     test_suite='lispy.tests.tests',
     include_package_data=True,
@@ -24,5 +25,5 @@
         'Topic :: Software Development :: Libraries :: Python Modules',
     ],
     entry_points={'console_scripts': ['lispy=lispy:cli']},
-    packages=['lispy']
+    packages=find_packages()
 )


Test patch:
diff --git a/lispy/tests/code/basic_syntax.lispy b/lispy/tests/code/basic_syntax.lispy
index 3632e17..0f94625 100755
--- a/lispy/tests/code/basic_syntax.lispy
+++ b/lispy/tests/code/basic_syntax.lispy
@@ -1,5 +1,5 @@
 #!/usr/bin/env lispy
 (begin
-        (print (+ 2 2))
-        (print (* 6 8))
-        (print os:environ))
+        (display (+ 2 2))
+        (display (* 6 8))
+        (display os:environ))
diff --git a/lispy/tests/code/sicp/chapters/1.1.1.lispy b/lispy/tests/code/sicp/chapters/1.1.1.lispy
index ef86465..7f7a9df 100644
--- a/lispy/tests/code/sicp/chapters/1.1.1.lispy
+++ b/lispy/tests/code/sicp/chapters/1.1.1.lispy
@@ -1,28 +1,28 @@
-(print (+ 137 349))
+(display (+ 137 349))
 ;; 486
 
-(print (- 1000 334))
+(display (- 1000 334))
 ;; 666
 
-(print (* 5 99))
+(display (* 5 99))
 ;; 495
 
-(print (/ 10 5))
-;; 2
+(display (/ 10.0 5))
+;; 2.0
 
-(print (+ 2.7 10))
+(display (+ 2.7 10))
 ;; 12.7
 
-(print (+ 21 35 12 7))
+(display (+ (+ 21 35) (+ 12 7)))
 ;; 75
 
-(print (* 25 4 12))
+(display (* 25 4 12))
 ;; 1200
 
-(print (+ (* 3 5) (- 10 6)))
+(display (+ (* 3 5) (- 10 6)))
 ;; 19
 
-(print
+(display
     (+ (* 3
          (+ (* 2 4)
             (+ 3 5)))
diff --git a/lispy/tests/code/sicp/chapters/1.1.2.lispy b/lispy/tests/code/sicp/chapters/1.1.2.lispy
index 8e9f369..bda2021 100644
--- a/lispy/tests/code/sicp/chapters/1.1.2.lispy
+++ b/lispy/tests/code/sicp/chapters/1.1.2.lispy
@@ -1,15 +1,15 @@
 (define size 2)
-(print size)
+(display size)
 ;; 2
 
-(print (* 5 size))
+(display (* 5 size))
 ;; 10
 
 (define pi 3.14159)
 (define radius 10)
-(print (* pi (* radius radius)))
+(display (* pi (* radius radius)))
 ;; 314.159
 
 (define circumference (* 2 pi radius))
-(print circumference)
+(display circumference)
 ;; 62.8318
\ No newline at end of file
diff --git a/lispy/tests/code/sicp/chapters/1.1.3.lispy b/lispy/tests/code/sicp/chapters/1.1.3.lispy
index c6bca65..c47ed18 100644
--- a/lispy/tests/code/sicp/chapters/1.1.3.lispy
+++ b/lispy/tests/code/sicp/chapters/1.1.3.lispy
@@ -1,3 +1,3 @@
-(print (* (+ 2 (* 4 6))
-   (+ 3 5 7))))
-;; 390
\ No newline at end of file
+(display (* (+ 2 (* 4 6))
+   (+ 3 (+ 5 7))))
+;; 390
diff --git a/lispy/tests/code/sicp/chapters/1.1.4.lispy b/lispy/tests/code/sicp/chapters/1.1.4.lispy
index 6776d09..e33153e 100644
--- a/lispy/tests/code/sicp/chapters/1.1.4.lispy
+++ b/lispy/tests/code/sicp/chapters/1.1.4.lispy
@@ -1,30 +1,30 @@
 (define square (lambda (x) (* x x)))
-(print (square 3))
+(display (square 3))
 ;; 9
 
-(print (square 21))
+(display (square 21))
 ;; 441
 
-(print (square (+ 2 5)))
+(display (square (+ 2 5)))
 ;; 49
 
-(print (square (square 3)))
+(display (square (square 3)))
 ;; 81
 
 (define x 2)
 (define y 3)
 
-(print (+ (square x) (square y)))
+(display (+ (square x) (square y)))
 ;; 13
 
 (define sum-of-squares (lambda (x y)
         (+ (square x) (square y))))
 
-(print (sum-of-squares 3 4))
+(display (sum-of-squares 3 4))
 ;; 25
 
 (define f (lambda (a)
         (sum-of-squares (+ a 1) (* a 2))))
 
-(print (f 5))
+(display (f 5))
 ;; 136
\ No newline at end of file
diff --git a/lispy/tests/code/sicp/chapters/1.1.6.lispy b/lispy/tests/code/sicp/chapters/1.1.6.lispy
index 80e3e15..a39e89e 100644
--- a/lispy/tests/code/sicp/chapters/1.1.6.lispy
+++ b/lispy/tests/code/sicp/chapters/1.1.6.lispy
@@ -1,11 +1,11 @@
 (define abs (lambda (x)
               (if (< x 0) (* -1 x) x)))
 
-(print (abs -7))
+(display (abs -7))
 ;; 7
 
-(print (abs 0))
+(display (abs 0))
 ;; 0
 
-(print (abs 2))
+(display (abs 2))
 ;; 2
\ No newline at end of file
diff --git a/lispy/tests/code/sicp/chapters/1.1.7.lispy b/lispy/tests/code/sicp/chapters/1.1.7.lispy
index 27996de..2d29db1 100644
--- a/lispy/tests/code/sicp/chapters/1.1.7.lispy
+++ b/lispy/tests/code/sicp/chapters/1.1.7.lispy
@@ -21,35 +21,32 @@
 
 (define sqrt (lambda (x) (sqrt-iter 1.0 x)))
 
-(print (square 3))
+(display (square 3))
 ;; 9
 
-(print (abs -3))
+(display (abs -3))
 ;; 3
 
-(print (abs 0))
+(display (abs 0))
 ;; 0
 
-(print (abs 2))
+(display (abs 2))
 ;; 2
 
-(print (error 4 3))
+(display (error 4 3))
 ;; 1
 
-(print (error 3 4))
+(display (error 3 4))
 ;; 1
 
-(print (error 3 3.0001))
-;; 0.0001
+(display (good-enough? 4 3))
+;; #f
 
-(print (good-enough? 4 3))
-;; False
+(display (good-enough? 3 9.0001))
+;; #t
 
-(print (good-enough? 3 9.0001))
-;; True
+(display (average 3.0 9))
+;; 6.0
 
-(print (average 3 9))
-;; 6
-
-(print (sqrt 9))
-;; 3.00009155413
\ No newline at end of file
+(display (py:int (sqrt 9)))
+;; 3
diff --git a/lispy/tests/code/sicp/chapters/1.1.8.lispy b/lispy/tests/code/sicp/chapters/1.1.8.lispy
new file mode 100644
index 0000000..28d8643
--- /dev/null
+++ b/lispy/tests/code/sicp/chapters/1.1.8.lispy
@@ -0,0 +1,15 @@
+(define (sqrt x)
+      (define (square x) (* x x))
+      (define average (lambda (x y) (/ (+ x y) 2)))
+      (define abs (lambda (x) (if (< x 0) (* -1 x) x)))
+      (define (good-enough? guess)
+        (< (abs (- (square guess) x)) 0.001))
+      (define (improve guess)
+        (average guess (/ x guess)))
+      (define (sqrt-iter guess)
+        (if (good-enough? guess)
+            guess
+            (sqrt-iter (improve guess))))
+      (sqrt-iter 1.0))
+(display (py:int (sqrt 9)))
+;; 3
diff --git a/lispy/tests/code/sicp/chapters/1.2.1.lispy b/lispy/tests/code/sicp/chapters/1.2.1.lispy
new file mode 100644
index 0000000..4a618dd
--- /dev/null
+++ b/lispy/tests/code/sicp/chapters/1.2.1.lispy
@@ -0,0 +1,18 @@
+(define (factorial n)
+  (if (= n 1)
+      1
+      (* n (factorial (- n 1)))))
+
+(display (factorial 4))
+;; 24
+
+(define (factorial n)
+  (define iter product counter)
+  (if (> counter n)
+      product
+      (iter (* counter product)
+            (+ counter 1)))
+  (iter 1 1))
+
+(display (factorial 4))
+;; 24
diff --git a/lispy/tests/code/sicp/chapters/1.2.2.lispy b/lispy/tests/code/sicp/chapters/1.2.2.lispy
new file mode 100644
index 0000000..e045568
--- /dev/null
+++ b/lispy/tests/code/sicp/chapters/1.2.2.lispy
@@ -0,0 +1,8 @@
+(define (fib n)
+  (if (= n 0) 0
+      (if (= n 1)
+          1
+          (+ (fib (- n 1))
+                 (fib (- n 2))))))
+(display (fib 18))
+;; 2584
diff --git a/lispy/tests/code/sicp/chapters/1.2.3.lispy b/lispy/tests/code/sicp/chapters/1.2.3.lispy
new file mode 100644
index 0000000..deddcb6
--- /dev/null
+++ b/lispy/tests/code/sicp/chapters/1.2.3.lispy
@@ -0,0 +1,4 @@
+(define (cube x) (* x x x))
+
+(display (cube 3))
+;; 27
\ No newline at end of file
diff --git a/lispy/tests/code/sicp/chapters/1.2.5.lispy b/lispy/tests/code/sicp/chapters/1.2.5.lispy
new file mode 100644
index 0000000..00c1a80
--- /dev/null
+++ b/lispy/tests/code/sicp/chapters/1.2.5.lispy
@@ -0,0 +1,7 @@
+(define (gcd a b)
+      (if (= b 0)
+          a
+          (gcd b (operator:mod a b))))
+
+(display (gcd 6 12))
+;; 6
\ No newline at end of file
diff --git a/lispy/tests/tests.py b/lispy/tests/tests.py
index 559b759..e11fbe0 100644
--- a/lispy/tests/tests.py
+++ b/lispy/tests/tests.py
@@ -1,12 +1,17 @@
+from __future__ import print_function
+
 import re
 import os
-import doctest
 from unittest import TestCase
+from inspect import isclass
 
-import sh
-from sh import lispy, grep
+from nose.tools import raises
 
 from lispy.runtime import Runtime
+from lispy.dialects.norvig import to_string
+
+from sh import lispy
+
 
 class RuntimeTest(TestCase):
     def setUp(self):
@@ -23,9 +28,9 @@ def test_simple_addition(self):
         )
 
     def test_simple_module_access(self):
-        import os
         self.assertEquals(
-            self.runtime.eval("(list os:environ)"), [os.environ]
+            list(self.runtime.eval("(list os:environ)")),
+            [os.environ]
         )
 
 
@@ -36,6 +41,7 @@ def test_py_builtin_abs(self):
             self.runtime.eval("(py:abs -7)"), 7
         )
 
+
 class CommandlineTest(RuntimeTest):
     """
     Test the options provided by lispy's cli using python's sh module
@@ -47,9 +53,12 @@ def test_passed_as_string(self):
         """
         self.assertEquals(lispy("-c (+ 2 2)"), "4\n")
 
-def check_expected(expected, actual):
+
+def check_expected(filename, expected, actual):
+    print(filename)
     assert expected == actual, "{} != {}".format(expected, actual)
 
+
 def test_sicp_examples():
     """
     Test all the files in the SICP code directory
@@ -61,7 +70,179 @@ def test_sicp_examples():
         abs_script_path = "{}/{}".format(chapters_dir, lispy_file)
         output = lispy("{}".format(abs_script_path))
         comments = re.findall(";;.*", open(abs_script_path).read())
-        expected = map(lambda line: line.replace(";;", ""), comments)
+        expected = list(map(lambda line: line.replace(";;", ""), comments))
 
         for output_line, expected in zip(output.split('\n'), expected):
-            yield check_expected, expected.strip(), output_line
+            yield check_expected, lispy_file, expected.strip(), output_line
+
+################ Tests for lis.py and lispy.py
+
+lis_tests = [
+    ("(+ 2 2)", 4),
+    ("(+ (* 2 100) (* 1 10))", 210),
+    ("(if (> 6 5) (+ 1 1) (+ 2 2))", 2),
+    ("(if (< 6 5) (+ 1 1) (+ 2 2))", 4),
+    ("(quote (testing 1 (2.0) -3.14e159))", ['testing', 1, [2.0], -3.14e159]),
+    ("(define x 3)", None), ("x", 3), ("(+ x x)", 6),
+    ("(begin (define x 1) (set! x (+ x 1)) (+ x 1))", 3),
+    ("((lambda (x) (+ x x)) 5)", 10),
+    ("(define twice (lambda (x) (* 2 x)))", None), ("(twice 5)", 10),
+    ("(define compose (lambda (f g) (lambda (x) (f (g x)))))", None),
+    ("((compose list twice) 5)", [10]),
+    ("(define repeat (lambda (f) (compose f f)))", None),
+    ("((repeat twice) 5)", 20), ("((repeat (repeat twice)) 5)", 80),
+    ("(define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1))))))", None),
+    ("(fact 3)", 6),
+    ("(fact 50)",
+     30414093201713378043612608166064768844377641568960512000000000000),
+    ("(define abs (lambda (n) ((if (> n 0) + -) 0 n)))", None),
+    ("(list (abs -3) (abs 0) (abs 3))", [3, 0, 3]),
+    ("""(define combine (lambda (f)
+    (lambda (x y)
+      (if (null? x) (quote ())
+          (f (list (car x) (car y))
+             ((combine f) (cdr x) (cdr y)))))))""", None),
+    ("(define zip (combine cons))", None),
+    ("(zip (list 1 2 3 4) (list 5 6 7 8))", [[1, 5], [2, 6], [3, 7], [4, 8]]),
+    ("""(define riff-shuffle (lambda (deck) (begin
+    (define take
+      (lambda (n seq)
+        (if (<= n 0)
+          (quote ()) (cons (car seq) (take (- n 1) (cdr seq))))))
+    (define drop (lambda (n seq) (if (<= n 0) seq (drop (- n 1) (cdr seq)))))
+    (define mid (lambda (seq) (/ (length seq) 2)))
+    ((combine append) (take (mid deck) deck) (drop (mid deck) deck)))))""",
+     None),
+    ("(riff-shuffle (list 1 2 3 4 5 6 7 8))", [1, 5, 2, 6, 3, 7, 4, 8]),
+    ("((repeat riff-shuffle) (list 1 2 3 4 5 6 7 8))",
+     [1, 3, 5, 7, 2, 4, 6, 8]),
+    ("(riff-shuffle (riff-shuffle (riff-shuffle (list 1 2 3 4 5 6 7 8))))",
+     [1, 2, 3, 4, 5, 6, 7, 8]),
+]
+
+lispy_tests = [
+    ("(define (twice x) (* 2 x))", None),
+    ("(twice 2)", 4),
+    ("(define lyst (lambda items items))", None),
+    ("(lyst 1 2 3 (+ 2 2))", [1, 2, 3, 4]),
+    ("(if 1 2)", 2),
+    ("(if (= 3 4) 2)", None),
+    ("(define ((account bal) amt) (set! bal (+ bal amt)) bal)", None),
+    ("(define a1 (account 100))", None),
+    ("(a1 0)", 100), ("(a1 10)", 110), ("(a1 10)", 120),
+    ("""(define (newton guess function derivative epsilon)
+    (define guess2 (- guess (/ (function guess) (derivative guess))))
+    (if (< (abs (- guess guess2)) epsilon) guess2
+        (newton guess2 function derivative epsilon)))""", None),
+    ("""(define (square-root a)
+    (newton 1 (lambda (x) (- (* x x) a)) (lambda (x) (* 2 x)) 1e-8))""", None),
+    ("(> (square-root 200.) 14.14213)", True),
+    ("(< (square-root 200.) 14.14215)", True),
+    ("(= (square-root 200.) (math:sqrt 200.))", True),
+    ("""(define (sum-squares-range start end)
+         (define (sumsq-acc start end acc)
+            (if
+              (> start end)
+                acc
+                (sumsq-acc (+ start 1) end (+ (* start start) acc))))
+         (sumsq-acc start end 0))""", None),
+    ("(sum-squares-range 1 3000)", 9004500500),  # Tests tail recursion
+    ("(call/cc (lambda (throw) (+ 5 (* 10 (throw 1))))) ;; throw", 1),
+    ("(call/cc (lambda (throw) (+ 5 (* 10 1)))) ;; do not throw", 15),
+    ("""(call/cc (lambda (throw)
+         (+ 5 (* 10 (call/cc
+                      (lambda (escape)
+                        (* 100 (escape 3)))))))) ; 1 level""", 35),
+    ("""(call/cc (lambda (throw)
+         (+ 5 (* 10 (call/cc
+                      (lambda (escape) (* 100 (throw 3)))))))) ; 2 levels""",
+     3),
+    ("""(call/cc (lambda (throw)
+         (+ 5 (* 10 (call/cc (lambda (escape) (* 100 1))))))) ; 0 levels""",
+     1005),
+    ("(* 1i 1i)", -1), ("(cmath:sqrt -1)", 1j),
+    ("(let ((a 1) (b 2)) (+ a b))", 3),
+    ("(and 1 2 3)", 3), ("(and (> 2 1) 2 3)", 3), ("(and)", True),
+    ("(and (> 2 1) (> 2 3))", False),
+    ("""(define-macro unless
+          (lambda args
+            `(if (not ,(car args)) (begin ,@(cdr args))))) ; test `""",
+     None),
+    ("(unless (= 2 (+ 1 1)) (display 2) 3 4)", None),
+    (r'(unless (= 4 (+ 1 1)) (display 2) (display "\n") 3 4)', 4),
+    ("(quote x)", 'x'),
+    ("(quote (1 2 three))", [1, 2, 'three']),
+    ("'x", 'x'),
+    ("'(one 2 3)", ['one', 2, 3]),
+    ("(define L (list 1 2 3))", None),
+    ("`(testing ,@L testing)", ['testing', 1, 2, 3, 'testing']),
+    ("`(testing ,L testing)", ['testing', [1, 2, 3], 'testing']),
+    ("""'(1 ;test comments '
+     ;skip this line
+     2 ; more ; comments ; ) )
+     3) ; final comment""", [1, 2, 3]),
+]
+
+syntax_errors = [
+    ("`,@L", SyntaxError),
+    ("(let ((a 1) (b 2 3)) (+ a b))", SyntaxError),
+    ("()", SyntaxError),
+    ("(set! x)", SyntaxError),
+    ("(define 3 4)", SyntaxError),
+    ("(quote 1 2)", SyntaxError),
+    ("(if 1 2 3 4)", SyntaxError),
+    ("(lambda 3 3)", SyntaxError),
+    ("(lambda (x))", SyntaxError),
+    ("""(if (= 1 2) (define-macro a 'a)
+     (define-macro a 'b))""", SyntaxError),
+]
+
+type_errors = [
+    ("(twice 2 2)", TypeError),
+]
+
+
+def check_norvig_expected(x, expected, rt):
+    try:
+        result = rt.eval(x)
+        print(x, '=>', to_string(result))
+        if isclass(expected) and not issubclass(expected, Exception):
+            assert result == expected
+    except Exception as e:
+        assert issubclass(expected, Exception)
+        assert isinstance(e, expected)
+
+
+def norvig_suite_test():
+    tests = lis_tests + lispy_tests
+
+    "For each (exp, expected) test case, see if eval(parse(exp)) == expected."
+    fails = 0
+    rt = Runtime()
+
+    for (x, expected) in tests:
+        yield check_norvig_expected, x, expected, rt
+
+
+@raises(SyntaxError)
+def check_for_syntax_error(expression, rt):
+    val = rt.eval(expression)
+
+
+def norvig_test_syntax_errors():
+    rt = Runtime()
+
+    for syntax_error_case in syntax_errors:
+        yield check_for_syntax_error, syntax_error_case[0], rt
+
+
+@raises(TypeError)
+def check_for_type_error(expression, rt):
+    val = rt.eval(expression)
+
+
+def norvig_test_type_errors():
+    rt = Runtime()
+
+    for type_error_case in type_errors:
+        yield check_for_type_error, type_error_case[0], rt