├── .gitignore ├── AUTHORS ├── CHANGES.txt ├── CNAME ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── guidelines.txt ├── pyclojure ├── __init__.py ├── core.py ├── lexer.py ├── parser.py ├── repl.py └── test_lisp.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | parsetab.py 3 | .DS_Store 4 | .externalToolBuilders/* 5 | .project 6 | .pydevproject 7 | parser.out 8 | build 9 | dist 10 | *.egg-info 11 | *.swp 12 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Author/originator: 2 | John Jacobsen http://johnj.com 3 | github.com/eigenhombre 4 | 5 | Contributors: 6 | Laurie Clark-Michalek github.com/bluepeppers 7 | Adam Charnock github.com/adamcharnock 8 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | For changes, see commit history at github.com/eigenhombre/PyClojure. 2 | 3 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | pyclojure.com 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | We still have to figure out what license to use.... 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.rst 3 | recursive-include docs * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyClojure 2 | 3 | This project consists of the first steps of an implementation of 4 | the Clojure language in Python. At the moment it is merely interpreted 5 | and is incomplete (see file test_lisp.py for examples). 6 | 7 | **Note**: the [clojure-py](http://github.com/halgari/clojure-py) project is much further along at this point; however, 8 | I'm keeping this code around as an example of using the Ply tools. 9 | 10 | #### What works: 11 | 12 | - REPL with history and readline support 13 | - Parsing and storage of trees of lists, vectors, atoms, integers, floats 14 | - 'def' and simple evaluation 15 | - A few builtin functions and access to functions in Python's main namespace 16 | - setup.py packaging 17 | 18 | ## Dependencies 19 | 20 | - [PLY](http://www.dabeaz.com/ply/), for lexing and parsing. It turns 21 | out Python is actually quite serviceable for writing compilers, 22 | particularly for prototyping -- one of the reasons for this project 23 | was to see how far I could push this. 24 | - [Nose](http://readthedocs.org/docs/nose/en/latest/), to run unit tests. 25 | - Python 2.6+ 26 | 27 | ## Why Clojure-in-Python? 28 | 29 | The long JVM startup time makes use of Clojure for scripting common 30 | system tasks somewhat unworkable, or at least irritating. 31 | [NewLISP](http://www.newlisp.org/) would be an option, but Clojure is 32 | such a nice Lisp; this particular implementation is meant to 33 | eventually be syntax-compatible with Clojure (as currently implemented 34 | on the JVM). The goal is to provide a real Lisp with macros, and 35 | Clojure's syntactic goodness, as well as immutable data structures, 36 | which can coexist with Python and make use of its extensive, 37 | 'batteries included' libraries. 38 | 39 | There has been [some 40 | discussion](http://groups.google.com/group/clojure/browse_thread/thread/d910983a4c9ed3f5) 41 | as well about getting Clojure running under PyPy, something which I 42 | think would be quite interesting. 43 | 44 | Feel free to join in! For next steps, see Issues list on GitHub. 45 | Please see the file guidelines.txt for contribution guidelines. 46 | 47 | ## Author 48 | 49 | Primary contact: John Jacobsen, NPX Designs, Inc. john@mail.npxdesigns.com 50 | (See AUTHORS file for complete list of contributors) 51 | -------------------------------------------------------------------------------- /guidelines.txt: -------------------------------------------------------------------------------- 1 | Unit testing can be done by typing 'nosetests'. You'll need to install 2 | nose first (e.g. 'pip install nose'). 3 | 4 | I'm striving for 100% test coverage. Please add test cases for any 5 | new contributions whenever possible. Ideally, write only enough test 6 | code to fail, then write only enough production code to make the tests 7 | pass. Iterate as needed.[1] 8 | 9 | Thanks in advance, 10 | John 11 | 12 | [1] http://www.apress.com/9781590599815/ is a good reference which has 13 | more details on the subject of test-driven development in Python. 14 | 15 | -------------------------------------------------------------------------------- /pyclojure/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # It must be possible to import this file with 3 | # none of the package's dependencies installed 4 | 5 | __version__ = '0.1.0' 6 | -------------------------------------------------------------------------------- /pyclojure/core.py: -------------------------------------------------------------------------------- 1 | import operator 2 | from funktown import ImmutableDict, ImmutableVector, ImmutableList 3 | 4 | 5 | class ComparableExpr(object): 6 | def __eq__(self, other): 7 | return (isinstance(other, self.__class__) 8 | and self.__dict__ == other.__dict__) 9 | 10 | def __ne__(self, other): 11 | return not (self == other) 12 | 13 | 14 | class Map(ComparableExpr, ImmutableDict): 15 | def __init__(self, *args, **kwargs): 16 | if not kwargs: 17 | if len(args) == 1: 18 | ImmutableDict.__init__(self, args[0]) 19 | else: 20 | ImmutableDict.__init__(self) 21 | else: 22 | ImmutableDict.__init__(self, kwargs) 23 | 24 | def __eq__(self, other): 25 | return ImmutableDict.__eq__(self, other) 26 | 27 | def __repr__(self): 28 | return 'MAP(%s)' % (str(self)) 29 | 30 | class Atom(ComparableExpr): 31 | def __init__(self, name=None, value=None): 32 | self.__name = name 33 | 34 | def name(self): 35 | return self.__name 36 | 37 | def __repr__(self): 38 | return "ATOM(%s)" % (self.__name) 39 | 40 | 41 | class ComparableIter(ComparableExpr): 42 | def __eq__(self, other): 43 | try: 44 | if len(self) != len(other): 45 | return False 46 | for a, b in zip(self, other): 47 | if a != b: 48 | return False 49 | except: 50 | return False 51 | else: 52 | return True 53 | 54 | 55 | class List(ComparableIter, ImmutableList): 56 | def __init__(self, *args): 57 | ImmutableList.__init__(self, args) 58 | 59 | def __repr__(self): 60 | return "%s(%s)" % (self.__class__.__name__.upper(), 61 | ','.join([str(el) for el in self])) 62 | 63 | 64 | class Vector(ComparableIter, ImmutableVector): 65 | def __init__(self, *args): 66 | ImmutableVector.__init__(self, args) 67 | 68 | def __repr__(self): 69 | return "%s(%s)" % (self.__class__.__name__.upper(), 70 | str(self)) 71 | 72 | 73 | class Keyword(ComparableExpr): 74 | def __init__(self, name): 75 | self.name = name 76 | def __repr__(self): 77 | return ":"+self.name 78 | 79 | 80 | class Function(ComparableExpr): 81 | pass 82 | 83 | 84 | class Scope(dict): 85 | pass 86 | 87 | 88 | class GlobalScope(Scope): 89 | def __init__(self, *args, **kwargs): 90 | Scope.__init__(self, *args, **kwargs) 91 | # Get all builtin python functions 92 | python_callables = [(name, obj) for name, obj\ 93 | in __builtins__.items() if\ 94 | callable(obj)] 95 | self.update(python_callables) 96 | 97 | # These functions take a variable number of arguments 98 | variadic_operators = {'+': ('add', 0), 99 | '-': ('sub', 0), 100 | '*': ('mul', 1), 101 | '/': ('div', 1)} 102 | def variadic_generator(fname, default): 103 | func = getattr(operator, fname) 104 | ret = (lambda *args: reduce(func, args) if args else default) 105 | # For string representation; otherwise just get 'lambda': 106 | ret.__name__ = fname 107 | return ret 108 | 109 | for name, info in variadic_operators.items(): 110 | self[name] = variadic_generator(*info) 111 | 112 | non_variadic_operators = { 113 | '!': operator.inv, 114 | '==': operator.eq, 115 | } 116 | self.update((name, func) for name, func in\ 117 | non_variadic_operators.items()) 118 | 119 | 120 | class UnknownVariable(Exception): 121 | pass 122 | 123 | 124 | BUILTIN_FUNCTIONS = {} 125 | 126 | def register_builtin(name): 127 | """ 128 | A decorator that registers built in functions. 129 | 130 | @register_builtin("def") 131 | def def(args, scopes): 132 | implementation here 133 | """ 134 | def inner(func): 135 | BUILTIN_FUNCTIONS[name] = func 136 | return func 137 | return inner 138 | 139 | @register_builtin("def") 140 | def def_(args, scopes): 141 | if len(args) != 2: 142 | raise TypeError("def takes two arguments") 143 | atom = args.first() 144 | rhs = args.second() 145 | if type(atom) is not Atom: 146 | raise TypeError("First argument to def must be atom") 147 | scopes[-1][atom.name()] = evaluate(rhs, scopes) 148 | 149 | 150 | def find_in_scopechain(scopes, name): 151 | for scope in reversed(scopes): 152 | try: 153 | return scope[name] 154 | except KeyError: 155 | pass 156 | 157 | 158 | def tostring(x): 159 | if x is None: 160 | return 'nil' 161 | elif type(x) in (int, float): 162 | return str(x) 163 | elif type(x) is Atom: 164 | return x.name() 165 | elif type(x) is Keyword: 166 | return ":"+x.name 167 | elif x.__class__.__name__ in ['function', 'builtin_function_or_method']: 168 | return str(x) 169 | elif type(x) is List: 170 | inner = ' '.join([tostring(x) for x in x]) 171 | return '(%s)' % inner 172 | elif type(x) is Vector: 173 | inner = ' '.join([tostring(x) for x in x]) 174 | return '[%s]' % inner 175 | elif type(x) is Map: 176 | inner = ', '.join(['%s %s' % (k, v) for k,v in x.items()]) 177 | return '{%s}' % inner 178 | else: 179 | raise TypeError('%s is unknown!' % x) 180 | 181 | 182 | def evaluate(x, scopes): 183 | if type(x) in (int, float): 184 | return x 185 | elif type(x) is Atom: 186 | val = find_in_scopechain(scopes, x.name()) 187 | if not val: 188 | raise UnknownVariable("Unknown variable: %s" % x.name()) 189 | else: 190 | return val 191 | elif type(x) is Keyword: 192 | return x 193 | elif type(x) is Vector: 194 | return apply(Vector, [evaluate(el, scopes) for el in x]) 195 | elif type(x) is Map: 196 | return Map(dict([(evaluate(k, scopes), evaluate(v, scopes)) 197 | for k, v in x.items()])) 198 | elif type(x) is List: 199 | contents = x 200 | return eval_list(contents, scopes) 201 | return x 202 | 203 | 204 | def eval_list(contents, scopes): 205 | if contents.empty(): 206 | return List() # () 207 | first = contents.first() 208 | rest = contents.rest() 209 | if type(first) is Map: 210 | if not rest.rest().empty(): 211 | raise TypeError("Map lookup takes one argument") 212 | return evaluate(first, scopes)[evaluate(rest.first(), scopes)] 213 | elif type(first) is Atom: 214 | name = first.name() 215 | if name in BUILTIN_FUNCTIONS: 216 | func = BUILTIN_FUNCTIONS[name] 217 | return func(rest, scopes) 218 | else: 219 | val = find_in_scopechain(scopes, name) 220 | if not val: 221 | raise UnknownVariable("Function %s is unknown" % name) 222 | if callable(val): 223 | args = map((lambda obj: evaluate(obj, scopes)), rest) 224 | return val(*args) 225 | else: 226 | raise TypeError("%s is not callable" % name) 227 | -------------------------------------------------------------------------------- /pyclojure/lexer.py: -------------------------------------------------------------------------------- 1 | import ply.lex as lex 2 | 3 | class PyClojureLex(object): 4 | def build(self,**kwargs): 5 | self.lexer = lex.lex(module=self, **kwargs) 6 | return self.lexer 7 | 8 | reserved = {'nil': 'NIL'} 9 | 10 | tokens = ['ATOM', 'KEYWORD', 11 | 'NUMBER', 'READMACRO', 12 | 'LBRACKET', 'RBRACKET', 13 | 'LBRACE', 'RBRACE', 14 | 'LPAREN', 'RPAREN'] + list(reserved.values()) 15 | 16 | t_LPAREN = r'\(' 17 | t_RPAREN = r'\)' 18 | t_LBRACKET = r'\[' 19 | t_RBRACKET = r'\]' 20 | t_LBRACE = r'\{' 21 | t_RBRACE = r'\}' 22 | t_ignore = ' ,\t\r' 23 | t_ignore_COMMENT = r'\;.*' 24 | 25 | def t_KEYWORD(self, t): 26 | r'\:[a-zA-Z_-]+' 27 | t.value = t.value[1:] 28 | return t 29 | 30 | def t_NUMBER(self, t): 31 | r'[+-]?((\d+(\.\d+)?([eE][+-]?\d+)?)|(\.\d+([eE][+-]?\d+)?))' 32 | val = t.value 33 | if '.' in val or 'e' in val.lower(): 34 | t.type = 'FLOAT' 35 | else: 36 | t.type = 'INTEGER' 37 | return t 38 | 39 | def t_ATOM(self, t): 40 | r'[\*\+\!\-\_a-zA-Z_-]+' 41 | t.type = self.reserved.get(t.value, 'ATOM') 42 | return t 43 | 44 | def t_READMACRO(self, t): 45 | r'[@\'#^`\\.]+' 46 | # All the possible reader macro chars 47 | return t 48 | 49 | def t_newline(self, t): 50 | r'\n+' 51 | t.lexer.lineno += len(t.value) 52 | 53 | def t_error(self, t): 54 | print "Illegal character '%s'" % t.value[0] 55 | t.lexer.skip(1) 56 | -------------------------------------------------------------------------------- /pyclojure/parser.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import re 3 | import ply.yacc as yacc 4 | from pyclojure.lexer import PyClojureLex 5 | from pyclojure.core import Atom, Keyword, List, Vector, Map 6 | 7 | # BNF grammar for 'lisp' 8 | # sexpr : atom 9 | # | readmacro sexpr 10 | # | keyword 11 | # | float 12 | # | integer 13 | # | list 14 | # | vector 15 | # | map 16 | # | nil 17 | # sexprs : sexpr 18 | # | sexprs sexpr 19 | # list : ( sexprs ) 20 | # | ( ) 21 | 22 | _quiet = True 23 | 24 | class LispLogger(yacc.PlyLogger): 25 | def debug(self, *args, **kwargs): 26 | if not _quiet: 27 | super(type(self), self).debug(*args, **kwargs) 28 | 29 | def make_map(args): 30 | m = {} 31 | kvlist = [(args[i], args[i+1]) for i in range(0, len(args), 2)] 32 | for k, v in kvlist: 33 | m[k] = v 34 | return Map(m) 35 | 36 | def quote_expr(raw): 37 | return List(Atom('quote'), raw) 38 | 39 | def deref_expr(raw): 40 | return List(Atom('deref'), raw) 41 | 42 | def init_type(raw): 43 | # Due to how python types are initialized, we can just treat them 44 | # as function calls. 45 | return raw 46 | 47 | # Map from the regex that matches the atom to the function that takes 48 | # in an ast, and modifies it as specified by the macro 49 | READER_MACROS = { 50 | r'@': deref_expr, 51 | r'\'': quote_expr, 52 | r'\.': init_type, 53 | } 54 | 55 | class PyClojureParse(object): 56 | def build(self): 57 | return yacc.yacc(module=self, errorlog=LispLogger(sys.stderr)) 58 | 59 | tokens = PyClojureLex.tokens 60 | tokens.remove('NUMBER') 61 | tokens.extend(('FLOAT', 'INTEGER')) 62 | 63 | def p_sexpr_nil(self, p): 64 | 'sexpr : NIL' 65 | p[0] = None 66 | 67 | def p_sexpr_atom(self, p): 68 | 'sexpr : ATOM' 69 | p[0] = Atom(p[1]) 70 | 71 | def p_sexpr_readmacro(self, p): 72 | 'sexpr : READMACRO sexpr' 73 | for regex, func in READER_MACROS.items(): 74 | if re.match(regex, p[1]): 75 | p[0] = func(p[2]) 76 | return 77 | 78 | def p_keyword(self, p): 79 | 'sexpr : KEYWORD' 80 | p[0] = Keyword(p[1]) 81 | 82 | def p_sexpr_float(self, p): 83 | 'sexpr : FLOAT' 84 | p[0] = float(p[1]) 85 | 86 | def p_sexpr_integer(self, p): 87 | 'sexpr : INTEGER' 88 | p[0] = int(p[1]) 89 | 90 | def p_sexpr_seq(self, p): 91 | ''' 92 | sexpr : list 93 | | vector 94 | | map 95 | ''' 96 | p[0] = p[1] 97 | 98 | def p_sexprs_sexpr(self, p): 99 | 'sexprs : sexpr' 100 | p[0] = p[1] 101 | 102 | def p_sexprs_sexprs_sexpr(self, p): 103 | 'sexprs : sexprs sexpr' 104 | #p[0] = ', '.join((p[1], p[2])) 105 | if type(p[1]) is list: 106 | p[0] = p[1] 107 | p[0].append(p[2]) 108 | else: 109 | p[0] = [p[1], p[2]] 110 | 111 | def p_list(self, p): 112 | 'list : LPAREN sexprs RPAREN' 113 | try: 114 | p[0] = apply(List, p[2]) 115 | except TypeError: 116 | p[0] = List(p[2]) 117 | 118 | def p_empty_list(self, p): 119 | 'list : LPAREN RPAREN' 120 | p[0] = List() 121 | 122 | def p_vector(self, p): 123 | 'vector : LBRACKET sexprs RBRACKET' 124 | try: 125 | p[0] = apply(Vector, p[2]) 126 | except TypeError: 127 | p[0] = Vector(p[2]) 128 | 129 | def p_empty_vector(self, p): 130 | 'vector : LBRACKET RBRACKET' 131 | p[0] = Vector() 132 | 133 | def p_map(self, p): 134 | 'map : LBRACE sexprs RBRACE' 135 | p[0] = make_map(p[2]) 136 | 137 | def p_empty_map(self, p): 138 | 'map : LBRACE RBRACE' 139 | p[0] = Map() 140 | 141 | def p_error(self, p): 142 | if p: 143 | print(p.lineno, "Syntax error in input at token '%s'" % p.value) 144 | else: 145 | print("EOF","Syntax error. No more input.") 146 | -------------------------------------------------------------------------------- /pyclojure/repl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import re 4 | import sys 5 | 6 | from pyclojure.lexer import PyClojureLex 7 | from pyclojure.parser import PyClojureParse 8 | from pyclojure.core import evaluate, tostring, GlobalScope 9 | 10 | try: 11 | import readline 12 | except ImportError: 13 | pass 14 | else: 15 | import os 16 | histfile = os.path.join(os.path.expanduser("~"), ".pyclojurehist") 17 | try: 18 | readline.read_history_file(histfile) 19 | except IOError: 20 | # Pass here as there isn't any history file, so one will be 21 | # written by atexit 22 | pass 23 | import atexit 24 | atexit.register(readline.write_history_file, histfile) 25 | 26 | lexer = PyClojureLex().build() 27 | parser = PyClojureParse().build() 28 | 29 | def main(): 30 | global_scope = GlobalScope() 31 | scopechain = [global_scope] 32 | while True: 33 | try: 34 | txt = raw_input("pyclojure> ") 35 | if re.search('^\s*$', txt): 36 | continue 37 | else: 38 | print(tostring(evaluate( 39 | parser.parse(txt, lexer=lexer), scopechain))) 40 | except EOFError: 41 | break 42 | except KeyboardInterrupt: 43 | print # Give user a newline after Cntrl-C for readability 44 | break 45 | except Exception, e: 46 | print e 47 | # return 1 <-- for now, we assume interactive session at REPL. 48 | # later/soon, we should handle source files as well. 49 | 50 | 51 | if __name__ == "__main__": 52 | exit_code = main() 53 | if exit_code: 54 | sys.exit(exit_code) 55 | -------------------------------------------------------------------------------- /pyclojure/test_lisp.py: -------------------------------------------------------------------------------- 1 | from pyclojure.lexer import PyClojureLex # Need tokens for parser 2 | from pyclojure.parser import PyClojureParse 3 | from pyclojure.core import (Atom, Keyword, Vector, List, Map, Scope, evaluate, 4 | tostring, UnknownVariable, GlobalScope) 5 | 6 | 7 | def test_lexer(): 8 | lexer = PyClojureLex().build() 9 | lexer.input("""(a 10 | (nested) list (of 534 atoms [and :symbols] 11 | (and lists))) ;; with comments 12 | """) 13 | assert 20 == len([tok for tok in lexer]) 14 | lexer.input("") 15 | assert [tok for tok in lexer] == [] 16 | 17 | 18 | def test_parser(): 19 | parse = PyClojureParse().build().parse 20 | assert parse("an_atom") == Atom('an_atom') 21 | assert parse("(simple_list)") == List(Atom('simple_list')) 22 | assert parse('(two elements)') == List(Atom('two'), 23 | Atom('elements')) 24 | assert (parse("(three element list)") == 25 | List(Atom('three'), Atom('element'), Atom('list'))) 26 | assert parse('666') == 666 27 | assert (parse('(a (nested (list)))') == 28 | List(Atom('a'), List(Atom('nested'), List(Atom('list'))))) 29 | assert parse('()') == List() 30 | 31 | 32 | def test_reader_macros(): 33 | parse = PyClojureParse().build().parse 34 | assert parse("@a") == parse("(deref a)") 35 | assert parse("'a") == parse("(quote a)") 36 | assert parse("(.float 3)") == parse("(float 3)") 37 | assert parse("'(1 2 3)") == parse("(quote (1 2 3))") 38 | 39 | 40 | def test_core(): 41 | Atom() 42 | Atom('a') 43 | Atom(name='a', value=6) 44 | List() 45 | List(Atom('car')) 46 | List(Atom('car'), Atom('cadr'), 666) 47 | List(List()) 48 | List(List('car')) 49 | Vector() 50 | Vector(1, 2, 3) 51 | Keyword("a") 52 | assert Atom() == Atom() 53 | assert List() == List() 54 | assert List(1) == List(1) 55 | assert List(2) != List(1) 56 | assert List(1, 2) != List(2, 1) 57 | assert List(1, 2) == List(1, 2) 58 | assert List(Atom()) == List(Atom()) 59 | assert List(Atom('a')) == List(Atom('a')) 60 | assert List(Atom('b')) != List(Atom('a')) 61 | assert Vector(1, 2) != Vector(2, 1) 62 | assert Vector(1, 2) == Vector(1, 2) 63 | assert Vector(1, 2) == List(1, 2) 64 | assert Keyword("a") == Keyword("a") 65 | assert Keyword("a") != Keyword("b") 66 | Map() 67 | Map(x=1) 68 | assert Map(x=1).keys() == ['x'] 69 | assert Map(x=1) == Map(x=1) 70 | assert Map(x=1) != Map(x=2) 71 | assert Map(x=1) != Map(x=1, a=3) 72 | assert Map(x=1)["x"] == 1 73 | 74 | 75 | def test_python_compat(): 76 | assert List(1, 2, 3) == [1, 2, 3] 77 | assert Map() == {} 78 | assert Map(a=3) == {'a': 3} 79 | assert Map(a=3) != ['a', 3] 80 | assert Vector(*range(10)) == range(10) 81 | assert map(abs, List(-1, -2, -3)) == List(1, 2, 3) 82 | def infinite_gen(): 83 | x = 1 84 | while 1: 85 | x += 1 86 | yield x 87 | assert List(1, 2, 3) != infinite_gen() 88 | assert List(1, 2) != List(1, 2, 3) 89 | 90 | 91 | def evalparser(): 92 | parse = PyClojureParse().build().parse 93 | scopechain = [GlobalScope()] 94 | def evalparse(x): 95 | return evaluate(parse(x), scopechain) 96 | return evalparse 97 | 98 | def test_eval(): 99 | evalparse = evalparser() 100 | assert evalparse("666") == 666 101 | assert evalparse("6.66") == 6.66 102 | assert evalparse("nil") == None 103 | assert evalparse("()") == List() 104 | assert evalparse("[]") == Vector() 105 | assert evalparse("[1 2 3]") == Vector(1, 2, 3) 106 | assert evalparse("{}") == Map() 107 | m = Map({1:2}) 108 | assert evalparse("{1 2}") == m 109 | m = Map({1:2, 3:4}) 110 | assert evalparse("{1 2, 3 4}") == m 111 | 112 | try: 113 | evalparse("a") 114 | assert False, "UnknownVariable exception not raised!" 115 | except UnknownVariable: 116 | pass 117 | try: 118 | evalparse("(x)") 119 | assert False, "UnknownVariable exception not raised!" 120 | except UnknownVariable: 121 | pass 122 | evalparse("(def a 777)") 123 | assert evalparse("a") == 777 124 | assert evalparse("a") == 777 125 | evalparse("(def a 666)") 126 | assert evalparse("a") == 666 127 | assert evalparse("[1 a]") == Vector(1, 666) 128 | assert evalparse(":a") == Keyword("a") 129 | assert evalparse("(+ 2 2)") == 4 130 | assert evalparse("(+)") == 0 131 | assert evalparse("(+ 1 2 3 4)") == 10 132 | assert evalparse("(*)") == 1 133 | assert evalparse("(* 1 2 3 4 5)") == 120 134 | assert evalparse("(+ 2 (+ 2 3))") == 7 135 | assert evalparse("{}") == Map() 136 | assert evalparse("{1 2}") == Map({1: 2}) 137 | assert evalparse("({1 2} 1)") == 2 138 | assert evalparse("({a 1} 666)") == 1 139 | assert evalparse("({666 1} a)") == 1 140 | assert evalparse("({a 2 3 a} a)") == 2 141 | assert evalparse("({a 2 3 a} 3)") == 666 142 | 143 | 144 | def test_function_calling(): 145 | ''' 146 | Test builtin function calling 147 | ''' 148 | evalparse = evalparser() 149 | assert evalparse("(abs (- 0 100))") == 100 150 | assert evalparse("(round 3.3)") == 3.0 151 | evalparse("(def a 3)") 152 | assert evalparse("a") == 3 153 | try: 154 | evalparse("(def a 3 2)") 155 | assert False, "TypeError expected" 156 | except TypeError: 157 | pass 158 | try: 159 | evalparse("(def 3 a)") 160 | assert False, "TypeError expected" 161 | except TypeError: 162 | pass 163 | 164 | def test_float_parsing(): 165 | ''' 166 | Test builtin python function calling 167 | ''' 168 | evalparse = evalparser() 169 | assert evalparse("1") == 1 170 | assert evalparse("1.2") == 1.2 171 | assert evalparse(".12") == .12 172 | assert evalparse("0.12") == .12 173 | assert evalparse("0.12E2") == 12 174 | assert evalparse("-0.12E+02") == -12 175 | assert evalparse("-0.12E-2") == -.0012 176 | assert evalparse("(.float 3)") == 3.0 177 | assert 'function abs' in str(evalparse("abs")) 178 | assert 'function add' in str(evalparse("+")) 179 | 180 | 181 | def test_to_string(): 182 | parse = PyClojureParse().build().parse 183 | assert tostring(parse("nil")) =="nil" 184 | assert tostring(parse("666")) =="666" 185 | assert tostring(parse("6.66")) == "6.66" 186 | assert tostring(parse("666e-2")) == "6.66" 187 | assert tostring(parse("-666")) =="-666" 188 | assert tostring(parse("-6.66")) == "-6.66" 189 | assert tostring(parse("-666e-2")) == "-6.66" 190 | assert tostring(parse("()")) == "()" 191 | assert tostring(parse("(a)")) == "(a)" 192 | assert tostring(parse("(a b)")) == "(a b)" 193 | assert tostring(parse("(a (b c))")) == "(a (b c))" 194 | assert tostring(parse("[]")) == "[]" 195 | assert tostring(parse(":a")) == ":a" 196 | assert tostring(parse("{}")) == "{}" 197 | assert tostring(parse("{1 2}")) == "{1 2}" 198 | assert tostring(parse("{1 2 3 4}")) == "{1 2, 3 4}" 199 | 200 | 201 | def test_scope(): 202 | ''' 203 | Fixme - eventually add tests for nested scope, lexical scope, etc. 204 | ''' 205 | s = Scope() 206 | s["a"] = 666 207 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from os.path import exists 3 | try: 4 | # Use setup() from setuptools(/distribute) if available 5 | from setuptools import setup 6 | except ImportError: 7 | from distutils.core import setup 8 | 9 | from pyclojure import __version__ 10 | 11 | setup( 12 | name='PyClojure', 13 | version=__version__, 14 | # Your name & email here 15 | author='John Jacobsen', 16 | author_email='john@mail.npxdesigns.com', 17 | # If you had pyclojure.tests, you would also include that in this list 18 | packages=['pyclojure'], 19 | # Any executable scripts, typically in 'bin'. E.g 'bin/do-something.py' 20 | scripts=[], 21 | url='https://github.com/eigenhombre/PyClojure', 22 | license='', 23 | description='Clojure implemented on top of Python', 24 | long_description=open('README').read() if exists("README") else "", 25 | entry_points=dict(console_scripts=['pyclojure=pyclojure.repl:main']), 26 | install_requires=[ 27 | 'ply>=3.4', 28 | 'funktown>=0.4.6' 29 | ], 30 | ) 31 | --------------------------------------------------------------------------------