├── .gitignore ├── LICENSE ├── README.md └── src ├── core.lisp ├── py ├── atom.py ├── env.py ├── error.py ├── fun.py ├── interface.py ├── lisp.py ├── lithp.py ├── number.py ├── reader.py ├── seq.py └── tests │ ├── __init__.py │ └── test_atoms.py └── rb ├── env.rb ├── fun.rb ├── lisp.rb ├── lithp.rb └── reader.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .svn 2 | *.pyc 3 | *#* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2013 Michael Fogus 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lithp is a(nother) McCarthy Lisp interpreter (with macros implemented in Lithp) written in the Python Programming Language. 2 | 3 | *note: currently only the Python port is fully operational* 4 | 5 | RIP John McCarthy 1927.09.04 - 2011.10.23 6 | 7 | RIP Timothy Hart 1939.09.18 - 2014.01.20 8 | 9 | 10 | What The?!? 11 | =========== 12 | 13 | My last true exposure to Lisp was during my college years. Since then I have hacked away at a bit of ELisp, but even that was done in the spirit of immediacy. With the resurgence of Lisp, thanks to the advocacy of [Mr. Paul Graham](http://www.paulgraham.com), I feel it is once again time to (re)learn the language. However, times have changed; I have written my recursive algorithms, I have explored the beauty of closures, and I have touched on functional programming with the grace and emotion of a lover. However, I fear that if I simply take up the task of (re)learning Lisp then I will take these notions for granted and not appreciate them fully as they relate to Lisp itself. Therefore, I feel that my best chance for truly absorbing Lisp is the invent Lisp. While the leg-work has already been done by such luminaries as [Mr. McCarthy][jmc], [Mr. Steele][steele], and [Mr. Sussman][sussman], it is my intention to approach their works as if they are newly minted and implement them within the Lithp interpreter. 14 | 15 | [steele]: http://research.sun.com/people/mybio.php?uid=25706 16 | [jmc]: http://www-formal.stanford.edu/jmc 17 | [sussman]: http://swiss.csail.mit.edu/~gjs 18 | 19 | Features 20 | ======== 21 | The Lithp interpreter provides the absolute core functions of McCarthy's original as outlined in his classical paper. That is, there are only seven functions and two special forms. 22 | 23 | Seven Functions 24 | --------------- 25 | 1. `atom` 26 | 2. `car` 27 | 3. `cdr` 28 | 4. `cond` 29 | 5. `cons` 30 | 6. `eq` 31 | 7. `quote` 32 | 33 | Two Special Forms 34 | ------------------ 35 | 1. `label` 36 | 2. `lambda` 37 | 38 | Running 39 | ======= 40 | 41 | cd src/py 42 | python lithp.py 43 | 44 | License 45 | ======= 46 | 47 | This software is provided as-is under the [MIT license](http://opensource.org/licenses/MIT). 48 | -------------------------------------------------------------------------------- /src/core.lisp: -------------------------------------------------------------------------------- 1 | (label t (quote t)) 2 | (label nil (quote ())) 3 | 4 | (label and (lambda (and_x and_y) 5 | (cond (and_x 6 | (cond (and_y t) 7 | (t nil))) 8 | (t nil)))) 9 | 10 | (label not (lambda (not_x) 11 | (cond (not_x nil) 12 | (t t)))) 13 | 14 | (label nand (lambda (nand_x nand_y) 15 | (not (and nand_x nand_y)))) 16 | 17 | (label or (lambda (or_x or_y) 18 | (nand 19 | (nand or_x or_x) 20 | (nand or_y or_y)))) 21 | 22 | (label nor (lambda (nor_x nor_y) 23 | (not (or nor_x nor_y)))) 24 | 25 | (label xor (lambda (xor_x xor_y) 26 | (or 27 | (and xor_x (not xor_y)) 28 | (and (not xor_x) xor_y)))) 29 | 30 | (label and_ (lambda (x y) (cond (x (cond (y t) (t nil))) (t nil)))) 31 | (label not_ (lambda (x) (cond (x nil) (t t)))) 32 | (label nand_ (lambda (x y) (not_ (and_ x y)))) 33 | (label or_ (lambda (x y) (nand_ (nand_ x x) (nand_ y y)))) 34 | (label xor_ (lambda (x y) (or_ (and_ x (not_ y)) (and_ (not_ x) y)))) 35 | 36 | (label pair (lambda (x y) 37 | (cons x (cons y nil)))) 38 | 39 | (label zip (lambda (x y) 40 | (cond ((and (null x) (null y)) nil) 41 | ((and (not (atom x)) (not (atom y))) 42 | (cons (pair (car x) (car y)) 43 | (zip (cdr x) (cdr y))))))) 44 | 45 | (label lookup (lambda (name context) 46 | (cond ((eq (car (car context)) name) (car (cdr (car context)))) 47 | (t (lookup name (cdr context)))))) 48 | 49 | (label caar (lambda (x) (car (car x)))) 50 | (label cadr (lambda (x) (car (cdr x)))) 51 | (label caddr (lambda (x) (car (cdr (cdr x))))) 52 | (label cadar (lambda (x) (car (cdr (car x))))) 53 | (label caddar (lambda (x) (car (cdr (cdr (car x)))))) 54 | (label cadddar (lambda (x) (car (cdr (cdr (cdr (car x))))))) 55 | 56 | (label null (lambda (null_x) 57 | (eq null_x nil))) 58 | 59 | (label mapcar (lambda (mapcar_f mapcar_lst) 60 | (cond 61 | ((null mapcar_lst) mapcar_lst) 62 | (t (cons 63 | (mapcar_f (car mapcar_lst)) 64 | (mapcar mapcar_f (cdr mapcar_lst))))))) 65 | 66 | (label reduce (lambda (reduce_f reduce_lst) 67 | (cond 68 | ((null reduce_lst) (reduce_f)) 69 | (t (reduce_f (car reduce_lst) 70 | (reduce reduce_f (cdr reduce_lst))))))) 71 | 72 | (label append (lambda (append_x append_y) 73 | (cond ((null append_x) append_y) 74 | (t (cons (car append_x) 75 | (append (cdr append_x) append_y)))))) 76 | 77 | (label filter (lambda (filter_f filter_lst) 78 | (cond 79 | ((null filter_lst) filter_lst) 80 | (t (cond 81 | ((filter_f (car filter_lst)) (cons 82 | (car filter_lst) 83 | (filter filter_f (cdr filter_lst)))) 84 | (t (filter filter_f (cdr filter_lst)))))))) 85 | 86 | (label env' (pair (pair (quote t) (quote t)) 87 | (pair (quote nil) nil))) 88 | 89 | (label quote' (lambda (qexpr) 90 | (car (cdr qexpr)))) 91 | 92 | (label atom' (lambda (aexpr abinds) 93 | (atom (eval (car (cdr aexpr)) abinds)))) 94 | 95 | (label eq' (lambda (eexpr ebinds) 96 | (eq (eval (car (cdr eexpr)) ebinds) 97 | (eval (car (cdr (cdr eexpr))) ebinds)))) 98 | 99 | (label car' (lambda (caexpr cabinds) 100 | (car (eval (car (cdr caexpr)) cabinds)))) 101 | 102 | (label cdr' (lambda (cdexpr cdbinds) 103 | (cdr (eval (car (cdr cdexpr)) cdbinds)))) 104 | 105 | (label cons' (lambda (coexpr cobinds) 106 | (cons (eval (car (cdr coexpr)) cobinds) 107 | (eval (car (cdr (cdr coexpr))) cobinds)))) 108 | 109 | (label eval-cond (lambda (condition condbinds) 110 | (cond ((eval (car (car condition)) condbinds) 111 | (eval (car (cdr (car condition))) condbinds)) 112 | (t (eval-cond (cdr condition) condbinds))))) 113 | 114 | (label cond' (lambda (cndexpr cndbinds) 115 | (eval-cond (cdr cndexpr) cndbinds))) 116 | 117 | (label rewrite (lambda (rexpr rbinds) 118 | (cons (lookup (car rexpr) rbinds) 119 | (cdr rexpr)))) 120 | 121 | (label eval (lambda (expr binds) 122 | (cond 123 | ((atom expr) (lookup expr binds)) 124 | ((atom (car expr)) 125 | (cond 126 | ((eq (car expr) (quote quote)) (quote' expr)) 127 | ((eq (car expr) (quote atom)) (atom' expr binds)) 128 | ((eq (car expr) (quote eq)) (eq' expr binds)) 129 | ((eq (car expr) (quote car)) (car' expr binds)) 130 | ((eq (car expr) (quote cdr)) (cdr' expr binds)) 131 | ((eq (car expr) (quote cons)) (cons' expr binds)) 132 | ((eq (car expr) (quote cond)) (cond' expr binds)) 133 | (t (eval (rewrite expr binds) binds)))) 134 | ((eq (car (car expr)) (quote label)) 135 | (eval (cons (car (cdr (cdr (car expr)))) (cdr expr)) 136 | (cons (pair (car (cdr (car expr))) (car expr)) binds))) 137 | ((eq (caar expr) (quote lambda)) 138 | (eval (caddar expr) 139 | (append (zip (cadar expr) (eval-args (cdr expr) binds)) 140 | binds))) 141 | ((eq (caar expr) (quote macro)) 142 | (cond 143 | ((eq (cadar expr) (quote lambda)) 144 | (eval (eval (car (cdddar expr)) 145 | (cons (pair (car (caddar expr)) 146 | (cadr expr)) 147 | binds)) 148 | binds))))))) 149 | 150 | (label eval-args (lambda (eval-args_m eval-args_a) 151 | (cond ((null eval-args_m) nil) 152 | (t (cons (eval (car eval-args_m) eval-args_a) 153 | (eval-args (cdr eval-args_m) eval-args_a)))))) 154 | 155 | (label apply (lambda (apply_name apply_args) 156 | ((pair apply_name (pair (quote quote) apply_args))))) 157 | 158 | 159 | (label zero (lambda (s z) z)) 160 | (label one (lambda (s z) (s z))) 161 | (label plus (lambda (m n) (lambda (f x) (m f (n f x))))) 162 | -------------------------------------------------------------------------------- /src/py/atom.py: -------------------------------------------------------------------------------- 1 | from interface import Eval, Egal 2 | from seq import Seq, List 3 | from error import UnimplementedFunctionError 4 | 5 | #### Atoms 6 | 7 | # McCarthy's Lisp defined two fundamental types: lists and atoms. The class `Atom` 8 | # represents that latter type. Originally an atom was defined as simply something 9 | # immutable and unique. 10 | # 11 | # There is currently a disparity in the implementation of Lithp in that atoms are created 12 | # and stored within the contextual environment and therefore their uniqueness cannot 13 | # be guaranteed. This is an artifact of implementation and not a problem in emulating 14 | # McCarthy's Lisp. 15 | # 16 | # One point of note is that in the original there were **no numbers**. Instead, numbers 17 | # had to be represented as lists of atoms, proving to be quite slow. (McCarthy 1979) Numbers 18 | # were not implemented until after Lisp 1.5 (**TODO** what version?) 19 | class Atom(Eval, Egal): 20 | def __init__(self, d): 21 | self.data = d 22 | self.hint = "atom" 23 | 24 | def __eq__(self, rhs): 25 | if isinstance(rhs, Atom): 26 | return (self.data == rhs.data) 27 | else: 28 | return False 29 | 30 | #### Symbols 31 | 32 | # The symbol was the basic atom in Lisp 1 and served as the basic unit of data. In his early 33 | # papers McCarthy freely mixes the terms atom and symbol. 34 | class Symbol(Atom): 35 | def __init__(self, sym): 36 | Atom.__init__(self, sym) 37 | 38 | def __repr__(self): 39 | return self.data 40 | 41 | def __hash__(self): 42 | return hash(self.data) 43 | 44 | def eval(self, env, args=None): 45 | return env.get(self.data) 46 | 47 | #### Truth 48 | 49 | # The first symbol created is `t`, corresponding to logical true. It's a little unclear to me 50 | # how this operated in the original Lisp. That is, was the symbol `t` meant as logical truth or 51 | # were symbols true by default? I suppose I will have to dig deeper for an answer. 52 | TRUE = Symbol("t") 53 | 54 | # Logical false is easy -- the empty list 55 | FALSE = List() 56 | 57 | #### Strings 58 | 59 | # In McCarthy's original paper (McCarthy 1960) he uses the term *string* to mean symbols, but later on 60 | # he mentions them in a different context reagrding their role in something called *linear Lisp*. I started 61 | # down the path of implementing linear Lisp also, but got sidetracked. Perhaps I will find time to complete it 62 | # sometime in the future. In the meantime strings are provided, but are not compliant with the Lisp 1 63 | # formalization. 64 | # 65 | # The first point of note is that the `String` class implements the `Seq` abstraction. This is needed by the 66 | # definition of linear Lisp that defines three functions of strings: `first`, `rest`, and `combine`. If you play 67 | # around with strings in the Lithp REPL you'll see that they conform to the linear Lisp formalism. 68 | # 69 | # This class will likely change in the future. 70 | class String(Atom, Seq): 71 | def __init__(self, str): 72 | Atom.__init__(self, str) 73 | 74 | def __repr__(self): 75 | return repr(self.data) 76 | 77 | def eval(self, env, args=None): 78 | return self 79 | 80 | # The `cons` behavior is (roughly) the same as the `combine` behavior defined in linear Lisp 81 | # Instead of returning a list however, the string `cons` returns another string. 82 | # I originally added the ability to `combine` strings and symbols, but I might pull that back. 83 | def cons(self, e): 84 | if e.__class__ != self.__class__ and e.__class__ != Symbol.__class__: 85 | raise UnimplementedFunctionError("Cannot cons a string and a ", e.__class__.__name__) 86 | 87 | return String(e.data + self.data) 88 | 89 | # `car` is roughly the same as `first` in linear Lisp 90 | def car(self): 91 | return Symbol(self.data[0]) 92 | 93 | # `cdr` is roughly the same as `rest` in linear Lisp 94 | def cdr(self): 95 | return String(self.data[1:]) 96 | -------------------------------------------------------------------------------- /src/py/env.py: -------------------------------------------------------------------------------- 1 | # The `Environment` class represents the dynamic environment of McCarthy's original Lisp. The creation of 2 | # this class is actually an interesting story. As many of you probably know, [Paul Graham wrote a paper and 3 | # code for McCarthy's original Lisp](http://www.paulgraham.com/rootsoflisp.html) and it was my first exposure to 4 | # the stark simplicity of the language. The simplicity is breath-taking! 5 | # 6 | # However, while playing around with the code I found that in using the core functions (i.e. `null.`, `not.`, etc.) 7 | # I was not experiencing the full effect of the original. That is, the original Lisp was dynamically scoped, but 8 | # the Common Lisp used to implement and run (CLisp in the latter case) Graham's code was lexically scoped. Therefore, 9 | # by attempting to write high-level functions using only the magnificent 7 and Graham's core functions in the Common Lisp 10 | # I was taking advantage of lexical scope; something not available to McCarthy and company. Of course, the whole reason 11 | # that Graham wrote `eval.` was to enforce dynamic scoping (he used a list of symbol-value pairs where the dynamic variables 12 | # were added to its front when introduced). However, that was extremely cumbersome to use: 13 | # 14 | # (eval. 'a '((a 1) (a 2))) 15 | # ;=> 1 16 | # 17 | # So I then implemented a simple REPL in Common Lisp that fed input into `eval.` and maintained the current environment list. 18 | # That was fun, but I wasn't sure that I was learning anything at all. Therefore, years later I came across the simple 19 | # REPL and decided to try to implement my own core environment for the magnificent 7 to truly get a feel for what it took 20 | # to build a simple language up from scratch. I suppose if I were a real manly guy then I would have found an IBM 704, but 21 | # that would be totally insane. (email me if you have one that you'd like to sell for cheap) 22 | # 23 | # Anyway, the point of this is that I needed to start with creating an `Environment` that provided dynamic scoping, and the 24 | # result is this. 25 | class Environment: 26 | # The binding are stored in a simple dict and the stack discipline is emulated through the `parent` link 27 | def __init__(self, par=None, bnd=None): 28 | if bnd: 29 | self.binds = bnd 30 | else: 31 | self.binds = {} 32 | 33 | self.parent = par 34 | 35 | if par: 36 | self.level = self.parent.level + 1 37 | else: 38 | self.level = 0 39 | 40 | # Getting a binding potentially requires the traversal of the parent link 41 | def get(self, key): 42 | if key in self.binds: 43 | return self.binds[key] 44 | elif self.parent: 45 | return self.parent.get(key) 46 | else: 47 | raise ValueError("Invalid symbol " + key) 48 | 49 | # Setting a binding is symmetric to getting 50 | def set(self, key, value): 51 | if key in self.binds: 52 | self.binds[key] = value 53 | elif self.parent: 54 | self.parent.set(key,value) 55 | else: 56 | self.binds[key] = value 57 | 58 | def definedp(self, key): 59 | if key in self.binds.keys(): 60 | return True 61 | 62 | return False 63 | 64 | # Push a new binding by creating a new Env 65 | # 66 | # Dynamic scope works like a stack. Whenever a variable is created it's binding is pushed onto a 67 | # global stack. In this case, the stack is simulated through a chain of parent links. So if you were to 68 | # create the following: 69 | # 70 | # (label a nil) 71 | # (label frobnicate (lambda () (cons a nil))) 72 | # 73 | # ((lambda (a) 74 | # (frobnicate)) 75 | # (quote x)) 76 | # 77 | # Then the stack would look like the figure below within the body of `frobnicate`: 78 | # 79 | # | | 80 | # | | 81 | # | a = 'x | 82 | # | ------- | 83 | # | a = nil | 84 | # +---------+ 85 | # 86 | # Meaning that when accessing `a`, `frobnicate` will get the binding at the top of the stack, producing the result `(x)`. This push/pop 87 | # can become difficult, so people have to do all kinds of tricks to avoid confusion (i.e. pseudo-namespace via variable naming schemes). 88 | # 89 | def push(self, bnd=None): 90 | return Environment(self, bnd) 91 | 92 | def pop(self): 93 | return self.parent 94 | 95 | def __repr__( self): 96 | ret = "\nEnvironment %s:\n" % self.level 97 | keys = [i for i in self.binds.keys() if not i[:2] == "__"] 98 | 99 | for key in keys: 100 | ret = ret + " %5s: %s\n" % (key, self.binds[key]) 101 | 102 | return ret 103 | -------------------------------------------------------------------------------- /src/py/error.py: -------------------------------------------------------------------------------- 1 | # I one day plan to create a whole battery of errors so that the REPL provides a detailed report whenever 2 | # something goes wrong. That day is not now. 3 | 4 | class Error(Exception): 5 | """Base class for exceptions in this module.""" 6 | pass 7 | 8 | class UnimplementedFunctionError(Error): 9 | def __init__(self, message, thing): 10 | self.thing = thing 11 | self.message = message 12 | 13 | def __str__(self): 14 | return self.message + repr(self.thing) 15 | 16 | class EvaluationError(Error): 17 | def __init__(self, env, args, message): 18 | self.env = env 19 | self.args = args 20 | self.message = message 21 | 22 | def __str__(self): 23 | return self.message + ", " + repr(self.args) + " in environment " + self.env.level 24 | -------------------------------------------------------------------------------- /src/py/fun.py: -------------------------------------------------------------------------------- 1 | from interface import Eval 2 | from atom import FALSE 3 | 4 | # Functions 5 | 6 | # As you might have imagined, McCarthy's Lisp derives much of its power from the function. The `Function` 7 | # class is used exclusively for *builtin* functions (i.e. the magnificent seven). Each core function is 8 | # implemented as a regular Python method, each taking an `Environment` and its arguments. 9 | class Function(Eval): 10 | def __init__(self, fn): 11 | self.fn = fn 12 | self.hint = "fun" 13 | 14 | def __repr__( self): 15 | return "" % id(self.fn) 16 | 17 | # Evaluation just delegates out to the builtin. 18 | def eval(self, env, args): 19 | return self.fn(env, args) 20 | 21 | # λ λ λ 22 | 23 | # The real power of McCarthy's Lisp srpings from Alonzo Chruch's λ-calculus. 24 | class Lambda(Eval): 25 | def __init__(self, n, b): 26 | # The names that occur in the arg list of the lambda are bound (or dummy) variables 27 | self.names = n 28 | # Unlike the builtin functions, lambdas have arbitrary bodies 29 | self.body = b 30 | 31 | def __repr__(self): 32 | return "" % id(self) 33 | 34 | # Every invocation of a lambda causes its bound variables to be pushed onto the 35 | # dynamic bindings stack. McCarthy only touches briefly on the idea that combining functions 36 | # built from lambda is problemmatic. In almost a throw-away sentence he states, "different bound 37 | # variables may be represented by the same symbol. This is called collision of bound variables." If you 38 | # take the time to explore [core.lisp](core.html) then you will see what this means in practice. 39 | # The reason for these difficulties is a direct result of dynamic scoping. McCarthy suggests that 40 | # a way to avoid these issues is to use point-free combinators to eliminate the need for variables 41 | # entirely. This approach is a book unto itself -- which is likely the reason that McCarthy skips it. 42 | def push_bindings(self, containing_env, values): 43 | containing_env.push() 44 | 45 | self.set_bindings(containing_env, values) 46 | 47 | # The bindings are set one by one corresponding to the input values. 48 | def set_bindings(self, containing_env, values): 49 | for i in range(len(values)): 50 | containing_env.environment.binds[self.names[i].data] = values[i].eval(containing_env.environment) 51 | 52 | # The evaluation of a lambda is not much more complicated than a builtin function, except that it will 53 | # establish bindings in the root context. Additionally, the root context will hold all bindings, so free 54 | # variables will also be in play. 55 | def eval(self, env, args): 56 | values = [a for a in args] 57 | 58 | if len(values) != len(self.names): 59 | raise ValueError("Wrong number of arguments, expected {0}, got {1}".format(len(self.names), len(args))) 60 | 61 | # Dynamic scope requires that names be bound on the global environment stack ... 62 | LITHP = env.get("__lithp__") 63 | 64 | # ... so I do just that. 65 | self.push_bindings(LITHP, values) 66 | 67 | # Now each form in the body is evaluated one by one, and the last determines the return value 68 | ret = FALSE 69 | for form in self.body: 70 | ret = form.eval(LITHP.environment) 71 | 72 | # Finally, the bindings established by the lambda are popped off of the dynamic stack 73 | LITHP.pop() 74 | return ret 75 | 76 | # Closures 77 | 78 | # McCarthy's Lisp does not define closures and in fact the precense of closures in the context of a pervasive dynamic 79 | # scope is problemmatic. However, this fact was academic to me and didn't really map conceptually to anything that I 80 | # had experienced in the normal course of my programming life. Therefore, I added closures to see what would happen. 81 | # It turns out that if you thought that bound variables caused issues then your head will explode to find out what 82 | # closures do. Buyer beware. However, closures are disabled by default. 83 | class Closure(Lambda): 84 | def __init__(self, e, n, b): 85 | Lambda.__init__(self, n, b) 86 | self.env = e 87 | 88 | def __repr__(self): 89 | return "" % id(self) 90 | 91 | # It's hard to imagine that this is the only difference between dynamic and lexical scope. That is, whereas the 92 | # latter established bindings in the root context, the former does so only at the most immediate. Of course, there 93 | # is no way to know this, so I had to make sure that the right context was passed within [lithp.py](index.html). 94 | def push_bindings(self, containing_env, values): 95 | containing_env.push(self.env.binds) 96 | 97 | self.set_bindings(containing_env, values) 98 | 99 | 100 | import lithp 101 | -------------------------------------------------------------------------------- /src/py/interface.py: -------------------------------------------------------------------------------- 1 | # I guess my background as a Java programmer compels me to create pseudo-interfaces. Is there no hope for the likes of me? 2 | 3 | from error import UnimplementedFunctionError, EvaluationError 4 | 5 | # Every form in Lithp is evalable 6 | class Eval: 7 | def eval(self, environment, args=None): 8 | raise EvaluationError(environment, args, "Evaluation error") 9 | 10 | # I read Henry Baker's paper *Equal Rights for Functional Objects or, The More Things Change, The More They Are the Same* 11 | # and got a wild hair about `egal`. However, it turns out that in McCarthy's Lisp the idea is trivial to the extreme. Oh well... 12 | # it's still a great paper. [Clojure](http://clojure.org)'s creator Rich Hickey summarizes `egal` much more succinctly than I ever could: 13 | # 14 | # > ... the only things you can really compare for equality are immutable things, because if you compare two things for equality that 15 | # > are mutable, and ever say true, and they're ever not the same thing, you are wrong. Or you will become wrong at some point in the future. 16 | # 17 | # Pretty cool huh? 18 | class Egal: 19 | def __eq__(self, rhs): 20 | raise UnimplementedFunctionError("Function not yet implemented", rhs) 21 | 22 | -------------------------------------------------------------------------------- /src/py/lisp.py: -------------------------------------------------------------------------------- 1 | # RIP John McCarthy 1927.09.04 - 2011.10.23 2 | 3 | from atom import TRUE 4 | from atom import FALSE 5 | from atom import Symbol 6 | from seq import Seq 7 | from fun import Lambda 8 | 9 | # The original Lisp described by McCarthy in his 1960 paper describes the following function set: 10 | # 11 | # 1. `atom` 12 | # 2. `car` 13 | # 3. `cdr` 14 | # 4. `cond` 15 | # 5. `cons` 16 | # 6. `eq` 17 | # 7. `quote` 18 | # 19 | # Plus two special forms: 20 | # 21 | # 1. `lambda` *(defined in [lithp.py](index.html))* 22 | # 2. `label` 23 | # 24 | # 25 | # 26 | 27 | # The `Lisp` class defines the magnificent seven in terms of the runtime environment built 28 | # thus far (i.e. dynamic scope, lambda, etc.). 29 | # 30 | class Lisp: 31 | SPECIAL = "()" 32 | 33 | # The magnificent seven are tainted by a pair of useful, but ugly functions, `dummy` and `println` 34 | # purely for practical matters. 35 | def dummy(self, env, args): 36 | print("I do nothing, but you gave me: ") 37 | self.println(env, args) 38 | 39 | def println(self, env, args): 40 | for a in args: 41 | result = a.eval(env) 42 | self.stdout.write( "%s " % str( result)) 43 | 44 | self.stdout.write( "\n") 45 | return TRUE 46 | 47 | #### `cond` 48 | 49 | # Did you know that McCarthy discovered conditionals? This is only partially true. That is, 50 | # Stephen Kleene defined the notion of a *primitive recursive function* and McCarthy built on 51 | # that by defining the conditional as a way to simplify the definition of recursive functions. 52 | # How would you define a recursive function without the use of a conditional in the terminating condition? 53 | # It turns out that you *can* define recursive functions this way (see fixed point combinators), but the 54 | # use of the conditional vastly simplifies the matter. 55 | # 56 | # We take conditionals for granted these days so it's difficult to imagine writing programs that 57 | # were not able to use them, or used a subset of their functionality. 58 | # 59 | # The `cond` form is used as follows: 60 | # 61 | # (cond ((atom (quote (a b))) (quote foo)) 62 | # ((atom (quote a)) (quote bar)) 63 | # (t (quote baz))) 64 | # 65 | # ;=> bar 66 | # 67 | def cond(self, env, args): 68 | for test in args: 69 | result = test.car().eval(env) 70 | 71 | if result == TRUE: 72 | return test.data[1].eval(env) 73 | 74 | return FALSE 75 | 76 | #### `eq` 77 | 78 | # Equality is delegated out to the objects being tested, so I will not discuss the mechanics here. 79 | # However, examples of usage are as follows: 80 | # 81 | # (eq nil (quote ())) 82 | # ;=> t 83 | # 84 | # (eq (quote (a b)) (quote (a b))) 85 | # ;=> t 86 | # 87 | # (eq (quote a) (quote b)) 88 | # ;=> () 89 | # 90 | def eq(self, env, args): 91 | if len(args) > 2: 92 | raise ValueError("Wrong number of arguments, expected {0}, got {1}".format(2, len(args))) 93 | 94 | if args[0].eval(env) == args[1].eval(env): 95 | return TRUE 96 | 97 | return FALSE 98 | 99 | #### `quote` 100 | 101 | # The `quote` builtin does one thing -- it returns exactly what was given to it without evaluation: 102 | # 103 | # (quote a) 104 | # ;=> a 105 | # 106 | # (quote (car (quote (a b c)))) 107 | # ;=> (car (quote (a b c))) 108 | # 109 | # Of course, you can evaluate the thing that `quote` returns: 110 | # 111 | # (eval (quote (car (quote (a b c)))) (quote ())) 112 | # ;=> a 113 | # 114 | def quote(self, env, args): 115 | if(len(args) > 1): 116 | raise ValueError("Wrong number of arguments, expected {0}, got {1}".format(1, len(args))) 117 | 118 | return args[0] 119 | 120 | #### `car` 121 | 122 | # The original Lisp implementation was written for the IBM 704 by Steve Russell (a genius of the highest 123 | # order -- also the creator/discoverer of [Spacewar!](http://pdp-1.computerhistory.org/pdp-1/?f=theme&s=4&ss=3) 124 | # and continuations). The somewhat obtuse name for a function that returns the first element of an s-expression 125 | # derives from the idiosyncracies of the IBM 704 on which Lisp was first implemented. The `car` function was 126 | # thus a shortening of the term "Contents of the Address part of Register number" that in itself has a very interesting 127 | # explanation. That is, `car` was used to refer to the first half of the wordsize addressed by the IBM 704. In this 128 | # particular machine (and many others at that time and since) the wordsize could address more than twice of the 129 | # actual physical memory. Taking this particular nuance of the IBM 704 into account, programmers were able to 130 | # efficiently create stacks by using the address of the stack's top in one half-word and the negative of the 131 | # allocated size in the other (the "Contents of Decrement part of Register number"), like so: 132 | # 133 | # +----------+----------+ 134 | # | top | -size | 135 | # +----------+----------+ 136 | # | | size goes toward zero 137 | # | | | 138 | # | | | 139 | # | | v 140 | # | | | | 141 | # 4 | | | | 142 | # | V | | 143 | # 3 | elem3 | | 144 | # | | | ^ 145 | # 2 | elem2 | | | 146 | # | | | | 147 | # 1 | elem1 |<-----+ stack grows up 148 | # | | 149 | # 0 | elem0 | 150 | # +--------+ 151 | # 152 | # Whenever something was pushed onto the stack the number `1` was added to both half-words. If the decrement 153 | # part of the word became zero then that signalled a stack-overflow, that was checked on each push or pop 154 | # instruction. However, the use of the car/cdr half-words was used quite differently (McCarthy 1962). That is, 155 | # The contents part contained a pointer to the memory location of the actual cons cell (see the documentation for 156 | # the next function `cdr` for more information) element, and the decrement part contained a pointer to the 157 | # next cell: 158 | # 159 | # +----------+----------+ +----------+----------+ 160 | # | car | cdr |--->| car | cdr | ... 161 | # +----------+----------+ +----------+----------+ 162 | # 163 | # The Lisp garbage collector used this structure to facilitate garbage collection by marking referenced chains of 164 | # cells as negative (sign bit), thus causing them to be ignored when performing memory reclamation. 165 | # 166 | # The `car` function works as follows: 167 | # 168 | # (car (quote (a b c))) 169 | # ;=> a 170 | # 171 | # The car of an empty list is an error (TODO: check if this is the case in McCarthy's Lisp) 172 | # 173 | def car(self, env, args): 174 | if(len(args) > 1): 175 | raise ValueError("Wrong number of arguments, expected {0}, got {1}".format(1, len(args))) 176 | 177 | # Of course, I do not use pointer arithmetic to implement cons cells... 178 | cell = args[0].eval(env) 179 | 180 | # ... instead I define it in terms of a sequence abstraction. This is a side-effect of originally 181 | # hoping to go further with this implementation (e.g. into linear Lisp), but as of now it's a bit heavy-weight 182 | # for what is actually needed. But I wouldn't be a programmer if I didn't needlessly abstract. 183 | if not isinstance(cell, Seq): 184 | raise ValueError("Function car not valid on non-sequence type: %s" % cell.data) 185 | 186 | return cell.car() 187 | 188 | #### `cdr` 189 | 190 | # In the previous function definition (`car`) I used the term cons-cell to describe the primitive structure underlying a 191 | # Lisp list. If you allow me, let me spend a few moments describing this elegant structure, and why it's such an important 192 | # abstract data type (ADT). 193 | # 194 | # Lisp from the beginning was built with the philosophy that lists should be a first-class citizen of the language; not only in 195 | # the realm of execution, but also generation and manipulation. If you look at my implementation of `List` in [seq.py](seq.html) 196 | # you'll notice that it's pretty standard fare. That is, it, like most lisp implementations is backed by a boring sequential store 197 | # where one element conceptually points to the next and blah blah blah. **Boring**. Where the cons-cell shines is that it is a 198 | # very general purpose ADT that can be used in a number of ways, but primary among them is the ability to represent the list. 199 | # 200 | # Lists in the early Lisp was precisely a chain of cons cells and the operators `car` and `cdr` pointed to very 201 | # specific implementation details that over time became generalized to mean "the first thing" and "the rest of the things" 202 | # respectively. But the fact remains that the cons cell solves a problem that is often difficult to do properly. That is, 203 | # how could Lisp represent a container that solved a number of requirements: 204 | # 205 | # * Represents a list 206 | # * Represents a pair 207 | # * Implementation efficiency 208 | # * Heterogeneous 209 | # 210 | # It would be interesting to learn the precise genesis of the idea behind the cons cell, but I imagine that it must have provoked 211 | # a eureka moment. 212 | # 213 | # I've already discussed how the IBM 704 hardware was especially ammenable to solving this problem efficiently, but the other points 214 | # bear further consideration. Lisp popularly stands for "LISt Processing language" but as I explained, the basic unit of data was 215 | # instead the cons cell structure. The fact of the matter is that the cons cell serves as both the implementation detail for lists 216 | # **and** the abstraction of a pair, all named oddly as if the implementation mattered. If Lisp had originally gone whole hog into the 217 | # abstraction game, then `car` and `cdr` would have been `first` and `rest` and would have spared the world decades of whining. 218 | # 219 | # Modern Lisps like Common Lisp rarely implement lists as chains of cons cells. Instead, it's preferred to create proper lists 220 | # with the `list` or `list*` functions and access them via `first` or `rest` (`cons` still persists thanks to its more general 221 | # meaning of "construct") and to only use `car` and `cdr` when dealing with cons cells. You can probably tell a lot about the 222 | # level of knowledge for a Lisp programmer by the way that they construct and access lists. For example, a programmer like 223 | # myself whose exposure to Common Lisp has been entirely academic, you will probably see a propensity toward the use of `car` and 224 | # `cdr` instead of leveraging the more expressive sequence abstractions. 225 | # 226 | # The `cdr` function works as follows: 227 | # 228 | # (cdr (quote (a b c))) 229 | # ;=> (b c) 230 | # 231 | # The cdr of an empty list is an empty list (TODO: check if this is the case in McCarthy's Lisp) 232 | # 233 | def cdr(self, env, args): 234 | if(len(args) > 1): 235 | raise ValueError("Wrong number of arguments, expected {0}, got {1}".format(1, len(args))) 236 | 237 | cell = args[0].eval(env) 238 | 239 | if not isinstance(cell, Seq): 240 | raise ValueError("Function cdr not valid on non-sequence type: %s" % cell.data) 241 | 242 | return cell.cdr() 243 | 244 | #### cons 245 | 246 | # So if Common Lisp has a more general sequence abstraction, then why would we still want to keep the cons cell? The reason is 247 | # that the cons cell is more flexible than a sequence and allows for a more intuitive way to build things like trees, pairs, and 248 | # to represent code structure. 249 | # 250 | # This function simply delegates the matter of consing to the target object. 251 | # 252 | # The `cons` function works as follows: 253 | # 254 | # (cons (quote a) nil) 255 | # ;=> (a) 256 | # 257 | # (cons (quote a) (quote (b c))) 258 | # ;=> (a b c) 259 | # 260 | # (cons (quote a) (quote b)) 261 | # ;=> Error 262 | # 263 | # I've agonized long and hard over wheter or not to implement McCarthy Lisp as the language described in *Recursive functions...* 264 | # as the anecdotal version only partially described in the *LISP 1.5 Programmer's Manual* and in most cases the former was my 265 | # choice. The creation of "dotted pairs" (I believe) was not an aspect of the original description and therefore is not represented 266 | # in Lithp. Sadly, I think that in some cases these version are mixed because I originally went down the path of creating a version of 267 | # Litho compatible with linear Lisp and Lisp 1.5, so this is a product of some pollution in the varying ideas. 268 | # 269 | def cons(self, env, args): 270 | if(len(args) > 2): 271 | raise ValueError("Wrong number of arguments, expected {0}, got {1}".format(2, len(args))) 272 | 273 | first = args[0].eval(env) 274 | second = args[1].eval(env) 275 | 276 | return second.cons(first) 277 | 278 | #### atom 279 | 280 | # Checks if a function is an atom; returns truthy if so. One thing to note is that the empty 281 | # list `()` is considered an atom because it cannot be deconstructed further. 282 | # 283 | # The `atom` function works as follows: 284 | # 285 | # (atom (quote a)) 286 | # ;=> t 287 | # 288 | # (atom nil) 289 | # ;=> t 290 | # 291 | # (atom (quote (a b c))) 292 | # ;=> () 293 | # 294 | # Recall that the empty list is falsity. 295 | # 296 | def atom(self, env, args): 297 | if(len(args) > 1): 298 | raise ValueError("Wrong number of arguments, expected {0}, got {1}".format(1, len(args))) 299 | 300 | first = args[0].eval(env) 301 | 302 | if first == FALSE: 303 | return TRUE 304 | elif isinstance(first, Symbol): 305 | return TRUE 306 | 307 | return FALSE 308 | 309 | #### label 310 | 311 | # Defines a named binding in the dynamic environment. 312 | def label(self, env, args): 313 | if(len(args) != 2): 314 | raise ValueError("Wrong number of arguments, expected {0}, got {1}".format(2, len(args))) 315 | 316 | # Notice that the first argument to `label` (a symbol) is **not** evaluated. This is the key difference between 317 | # a Lisp function and a special form (and macro, but I will not talk about those here). That is, in *all* 318 | # cases the arguments to a function are evaluated from left to right before being passed into the function. 319 | # Conversely, special forms have special semantics for evaluation that cannot be directly emulated or implemented 320 | # using functions. 321 | env.set(args[0].data, args[1].eval(env)) 322 | return env.get(args[0].data) 323 | -------------------------------------------------------------------------------- /src/py/lithp.py: -------------------------------------------------------------------------------- 1 | # Lithp - A interpreter for John McCarthy's original Lisp. 2 | # 3 | # The heavily documented code for [Lithp can be found on Github](http://github.com/fogus/lithp). 4 | # 5 | # It wasn't enough to write the Lisp interpreter -- I also wanted to share what I learned with *you*. Reading 6 | # this source code provides a snapshot into the mind of John McCarthy, Steve Russell, Timothy P. Hart, and Mike Levin and 7 | # as an added bonus, myself. The following source files are available for your reading: 8 | # 9 | # - [atom.py](atom.html) 10 | # - [env.py](env.html) 11 | # - [error.py](error.html) 12 | # - [fun.py](fun.html) 13 | # - [interface.py](interface.html) 14 | # - [lisp.py](lisp.html) 15 | # - [lithp.py](index.html) *(this file)* 16 | # - [number.py](number.html) 17 | # - [reader.py](reader.html) 18 | # - [seq.py](seq.html) 19 | # - [core.lisp](core.html) 20 | # 21 | # The Lithp interpreter requires Python 2.6.1+ to function. 22 | # please add comments, report errors, annecdotes, etc. to the [Lithp Github project page](http://github.com/fogus/lithp) 23 | # 24 | import pdb 25 | import getopt, sys, io 26 | from env import Environment 27 | from fun import Function 28 | from atom import TRUE 29 | from atom import FALSE 30 | from lisp import Lisp 31 | from reader import Reader 32 | from error import Error 33 | from fun import Lambda 34 | from fun import Closure 35 | 36 | NAME = "Lithp" 37 | VERSION = "v1.1" 38 | WWW = "http://fogus.me/fun/lithp/" 39 | PROMPT = "lithp" 40 | DEPTH_MARK = "." 41 | 42 | class Lithp(Lisp): 43 | """ The Lithper class is the interpreter driver. It does the following: 44 | 1. Initialize the global environment 45 | 2. Parse the cl arguments and act on them as appropriate 46 | 3. Initialize the base Lisp functions 47 | 4. Read input 48 | 5. Evaluate 49 | 6. Print 50 | 7. Loop back to #4 51 | """ 52 | def __init__( self): 53 | iostreams=(sys.stdin, sys.stdout, sys.stderr) 54 | (self.stdin, self.stdout, self.stderr) = iostreams 55 | 56 | self.debug = False 57 | self.verbose = True 58 | self.core = True 59 | self.closures = True 60 | 61 | self.rdr = Reader() 62 | self.environment = Environment() 63 | 64 | self.init() 65 | 66 | def init(self): 67 | # Define core functions 68 | self.environment.set("eq", Function(self.eq)) 69 | self.environment.set("quote", Function(self.quote)) 70 | self.environment.set("car", Function(self.car)) 71 | self.environment.set("cdr", Function(self.cdr)) 72 | self.environment.set("cons", Function(self.cons)) 73 | self.environment.set("atom", Function(self.atom)) 74 | self.environment.set("cond", Function(self.cond)) 75 | 76 | # Define utility function 77 | self.environment.set("print", Function(self.println)) 78 | 79 | # Special forms 80 | self.environment.set("lambda", Function(self.lambda_)) 81 | self.environment.set("label", Function(self.label)) 82 | 83 | # Define meta-elements 84 | self.environment.set("__lithp__", self) 85 | self.environment.set("__global__", self.environment) 86 | 87 | def usage(self): 88 | self.print_banner() 89 | print() 90 | print(NAME.lower(), " [lithp files]\n") 91 | 92 | def print_banner(self): 93 | print("The", NAME, "programming shell", VERSION) 94 | print(" by Fogus,", WWW) 95 | print(" Type :help for more information") 96 | print() 97 | 98 | def print_help(self): 99 | print("Help for Lithp v", VERSION) 100 | print(" Type :help for more information") 101 | print(" Type :env to see the bindings in the current environment") 102 | print(" Type :load followed by one or more filenames to load source files") 103 | print(" Type :quit to exit the interpreter") 104 | 105 | def push(self, env=None): 106 | if env: 107 | self.environment = self.environment.push(env) 108 | else: 109 | self.environment = self.environment.push() 110 | 111 | def pop(self): 112 | self.environment = self.environment.pop() 113 | 114 | def repl(self): 115 | while True: 116 | # Stealing the s-expression parsing approach from [CLIPS](http://clipsrules.sourceforge.net/) 117 | source = self.get_complete_command() 118 | 119 | # Check for any REPL directives 120 | try: 121 | if source in [":quit"]: 122 | break 123 | elif source in [":help"]: 124 | self.print_help() 125 | elif source.startswith(":load"): 126 | files = source.split(" ")[1:] 127 | self.process_files(files) 128 | elif source in [":env"]: 129 | print(self.environment) 130 | else: 131 | self.process(source) 132 | except AttributeError as ae: 133 | print("Could not process command: ", source) 134 | raise ae 135 | 136 | 137 | # Source is processed one s-expression at a time. 138 | def process(self, source): 139 | sexpr = self.rdr.get_sexpr(source) 140 | 141 | while sexpr: 142 | result = None 143 | 144 | try: 145 | result = self.eval(sexpr) 146 | except Error as err: 147 | print(err) 148 | 149 | if self.verbose: 150 | self.stdout.write(" %s\n" % result) 151 | 152 | sexpr = self.rdr.get_sexpr() 153 | 154 | # In the process of living my life I had always heard that closures and dynamic scope 155 | # cannot co-exist. As a thought-experiment I can visualize why this is the case. That is, 156 | # while a closure captures the contextual binding of a variable, lookups in dynamic scoping 157 | # occur on the dynamic stack. This means that you may be able to close over a variable as 158 | # long as it's unique, but the moment someone else defines a variable of the same name 159 | # and attempt to look up the closed variable will resolve to the top-most binding on the 160 | # dynamic stack. This assumes the the lookup occurs before the variable of the same name 161 | # is popped. While this is conceptually easy to grasp, I still wanted to see what would 162 | # happen in practice -- and it wasn't pretty. 163 | def lambda_(self, env, args): 164 | if self.environment != env.get("__global__") and self.closures: 165 | return Closure(env, args[0], args[1:]) 166 | else: 167 | return Lambda(args[0], args[1:]) 168 | 169 | # Delegate evaluation to the form. 170 | def eval(self, sexpr): 171 | try: 172 | return sexpr.eval(self.environment) 173 | except ValueError as err: 174 | print(err) 175 | return FALSE 176 | 177 | # A complete command is defined as a complete s-expression. Simply put, this would be any 178 | # atom or any list with a balanced set of parentheses. 179 | def get_complete_command(self, line="", depth=0): 180 | if line != "": 181 | line = line + " " 182 | 183 | if self.environment.level != 0: 184 | prompt = PROMPT + " %i%s " % (self.environment.level, DEPTH_MARK * (depth+1)) 185 | else: 186 | if depth == 0: 187 | prompt = PROMPT + "> " 188 | else: 189 | prompt = PROMPT + "%s " % (DEPTH_MARK * (depth+1)) 190 | 191 | line = line + self.read_line(prompt) 192 | 193 | # Used to balance the parens 194 | balance = 0 195 | for ch in line: 196 | if ch == "(": 197 | # This is not perfect, but will do for now 198 | balance = balance + 1 199 | elif ch == ")": 200 | # Too many right parens is a problem 201 | balance = balance - 1 202 | if balance > 0: 203 | # Balanced parens gives zero 204 | return self.get_complete_command(line, depth+1) 205 | elif balance < 0: 206 | raise ValueError("Invalid paren pattern") 207 | else: 208 | return line 209 | 210 | def read_line(self, prompt) : 211 | if prompt and self.verbose: 212 | self.stdout.write("%s" % prompt) 213 | self.stdout.flush() 214 | 215 | line = self.stdin.readline() 216 | 217 | if(len(line) == 0): 218 | return "EOF" 219 | 220 | if line[-1] == "\n": 221 | line = line[:-1] 222 | 223 | return line 224 | 225 | # Lithp also processes files using the reader plumbing. 226 | def process_files(self, files): 227 | self.verbose = False 228 | 229 | for filename in files: 230 | infile = open( filename, 'r') 231 | self.stdin = infile 232 | 233 | source = self.get_complete_command() 234 | while(source not in ["EOF"]): 235 | self.process(source) 236 | 237 | source = self.get_complete_command() 238 | 239 | infile.close() 240 | self.stdin = sys.stdin 241 | 242 | self.verbose = True 243 | 244 | if __name__ == '__main__': 245 | lithp = Lithp() 246 | 247 | try: 248 | opts, files = getopt.getopt(sys.argv[1:], "hd", ["help", "debug", "no-core", "no-closures"]) 249 | except getopt.GetoptError as err: 250 | # Print help information and exit: 251 | print(str( err)) # will print something like "option -a not recognized" 252 | lithp.usage() 253 | sys.exit(1) 254 | 255 | for opt,arg in opts: 256 | if opt in ("--help", "-h"): 257 | lithp.usage() 258 | sys.exit(0) 259 | elif opt in ("--debug", "-d"): 260 | lithp.verbose = True 261 | elif opt in ("--no-core"): 262 | lithp.core = False 263 | elif opt in ("--no-closures"): 264 | lithp.closures = False 265 | else: 266 | print("unknown option " + opt) 267 | 268 | # Process the core lisp functions, if applicable 269 | if lithp.core: 270 | print("Loading core.lisp...") 271 | lithp.process_files(["../core.lisp"]) 272 | 273 | if len(files) > 0: 274 | lithp.process_files(files) 275 | 276 | lithp.print_banner() 277 | lithp.repl() 278 | 279 | 280 | #### References 281 | 282 | # - (McCarthy 1979) *History of Lisp* by John MaCarthy 283 | # - (McCarthy 1960) *Recursive functions of symbolic expressions and their computation by machine, part I* by John McCarthy 284 | # - (Church 1941) *The Calculi of Lambda-Conversion* by Alonzo Church 285 | # - (Baker 1993) *Equal Rights for Functional Objects or, The More Things Change, The More They Are the Same* by Henry Baker 286 | # - (Kleene 1952) *Introduction of Meta-Mathematics* by Stephen Kleene 287 | # - (McCarthy 1962) *LISP 1.5 Programmer's Manual* by John McCarthy, Daniel Edwards, Timothy Hart, and Michael Levin 288 | # - (IBM 1955) *IBM 704 Manual of Operation* [here](http://www.cs.virginia.edu/brochure/images/manuals/IBM_704/IBM_704.html) 289 | # - (Hart 1963) *AIM-57: MACRO Definitions for LISP* by Timothy P. Hart 290 | -------------------------------------------------------------------------------- /src/py/number.py: -------------------------------------------------------------------------------- 1 | from interface import Eval 2 | import re 3 | import types 4 | 5 | 6 | class Number(Eval): 7 | def __init__( self, v): 8 | self.data = v 9 | 10 | def __repr__( self): 11 | return repr(self.data) 12 | 13 | def eval( self, env, args=None): 14 | return self 15 | 16 | def __eq__(self, rhs): 17 | if isinstance(rhs, Number): 18 | return (self.data == rhs.data) 19 | else: 20 | return False 21 | 22 | class Integral(Number): 23 | REGEX = re.compile(r'^[+-]?\d+$') 24 | 25 | def __init__( self, v): 26 | Number.__init__(self, v) 27 | 28 | class LongInt(Number): 29 | REGEX = re.compile(r'^[+-]?\d+[lL]$') 30 | 31 | def __init__( self, v): 32 | Number.__init__(self, v) 33 | 34 | class Float(Number): 35 | REGEX = re.compile(r'^[+-]?(\d+\.\d*$|\d*\.\d+$)') 36 | 37 | def __init__( self, v): 38 | Number.__init__(self, v) 39 | 40 | -------------------------------------------------------------------------------- /src/py/reader.py: -------------------------------------------------------------------------------- 1 | import string 2 | import re 3 | 4 | from atom import Symbol, String 5 | from number import Number, Integral, LongInt, Float 6 | from lisp import Lisp 7 | from seq import List 8 | 9 | DELIM = string.whitespace + Lisp.SPECIAL 10 | 11 | class Reader: 12 | def __init__(self, str=None): 13 | self.raw_source = str 14 | self.index = 0 15 | self.length = 0 16 | self.sexpr = [] 17 | 18 | if str: 19 | self.sexpr = self.get_sexpr() 20 | 21 | def get_sexpr(self, source=None): 22 | if source: 23 | self.raw_source = source 24 | self.length = len(self.raw_source) 25 | self.index = 0 26 | 27 | token = self.get_token() 28 | expr = None 29 | 30 | if token == ')': 31 | raise ValueError("Unexpected right paren") 32 | elif token == '(': 33 | expr = [] 34 | token = self.get_token() 35 | 36 | while token != ')': 37 | if token == '(': 38 | # Start parsing again. 39 | self.prev() 40 | expr.append(self.get_sexpr()) 41 | elif token == None: 42 | raise ValueError("Invalid end of expression: ", self.raw_source) 43 | else: 44 | expr.append(token) 45 | 46 | token = self.get_token() 47 | 48 | return List(expr) 49 | else: 50 | return token 51 | 52 | 53 | def get_token(self): 54 | if self.index >= self.length: 55 | return None 56 | 57 | # Kill whitespace 58 | while self.index < self.length and self.current() in string.whitespace: 59 | self.next() 60 | 61 | # Check if we had a string of whitespace 62 | if self.index == self.length: 63 | return None 64 | 65 | if self.current() in Lisp.SPECIAL: 66 | self.next() 67 | 68 | return self.previous() 69 | # As mentioned in [atom.py](atom.html), I started down the path of implementing linear Lisp. 70 | # However, that work was never completed, but the reading of strings (surrounded by `"`) still remains 71 | # This may change in the future. 72 | elif self.current() == '"': 73 | # Parse a string. 74 | str = "" 75 | self.next() 76 | 77 | while self.current() != '"' and self.index < self.length: 78 | str = str + self.current() 79 | self.next() 80 | 81 | self.next() 82 | return String(str) 83 | else: 84 | token_str = "" 85 | 86 | # Build the token string 87 | while self.index < self.length - 1: 88 | if self.current() in DELIM: 89 | break 90 | else: 91 | token_str = token_str + self.current() 92 | self.next() 93 | 94 | if not self.current() in DELIM: 95 | token_str = token_str + self.current() 96 | self.next() 97 | 98 | if Integral.REGEX.match(token_str): 99 | return Integral(int(token_str)) 100 | elif Float.REGEX.match(token_str): 101 | return Float(float(token_str)) 102 | elif LongInt.REGEX.match(token_str): 103 | return LongInt(int(token_str)) 104 | else: 105 | return Symbol(token_str) 106 | 107 | return None 108 | 109 | def next(self): 110 | self.index = self.index + 1 111 | 112 | def prev(self): 113 | self.index = self.index - 1 114 | 115 | def current(self): 116 | return self.raw_source[self.index] 117 | 118 | def previous(self): 119 | return self.raw_source[self.index - 1] 120 | -------------------------------------------------------------------------------- /src/py/seq.py: -------------------------------------------------------------------------------- 1 | from interface import Eval, Egal 2 | from error import UnimplementedFunctionError 3 | 4 | class Seq(Eval, Egal): 5 | def __init__(self): 6 | self.data = None 7 | 8 | def car(self): 9 | if self.data: 10 | return self.data[0] 11 | 12 | return self 13 | 14 | def cdr(self): 15 | raise UnimplementedFunctionError("Function not yet implemented for ", self.__class__.__name__) 16 | 17 | def cons(self, e): 18 | raise UnimplementedFunctionError("Function not yet implemented for ", self.__class__.__name__) 19 | 20 | # The following four functions needed for iterability 21 | def __iter__(self): 22 | return self.data.__iter__() 23 | 24 | def __len__(self): 25 | return len(self.data) 26 | 27 | def __contains__(self, e): 28 | return e in self.data 29 | 30 | def __getitem__(self, e): 31 | return self.data[e] 32 | 33 | def __eq__(self, rhs): 34 | if not isinstance(rhs, Seq): 35 | return False 36 | 37 | if len(self) != len(rhs): 38 | return False 39 | 40 | for i in range(len(self.data)): 41 | if not self.data[i] == rhs.data[i]: 42 | return False 43 | 44 | return True 45 | 46 | 47 | class List(Seq): 48 | def __init__(self, l=None): 49 | Seq.__init__(self) 50 | 51 | if l is None: 52 | self.data = [] 53 | else: 54 | self.data = l 55 | 56 | def cdr(self): 57 | try: 58 | return List(self.data[1:]) 59 | except: 60 | return List([]) 61 | 62 | def cons(self, e): 63 | ret = List(self.data[:]) # bugfix 1234977437 64 | ret.data.insert(0, e) 65 | return ret 66 | 67 | def eval(self, env, args=None): 68 | form = self.car() 69 | 70 | if form is self: 71 | return self 72 | else: 73 | form = form.eval(env) 74 | 75 | return form.eval(env, self.cdr()) 76 | 77 | def __repr__(self): 78 | if self.data == []: 79 | return "()" 80 | 81 | ret = "(%s" % self.data[0] 82 | for e in self.data[1:]: 83 | ret = ret + " %s" % e 84 | 85 | return ret + ")" 86 | -------------------------------------------------------------------------------- /src/py/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Testing Lithp""" 2 | -------------------------------------------------------------------------------- /src/py/tests/test_atoms.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from atom import TRUE, FALSE 3 | from atom import Atom 4 | from atom import Symbol 5 | from seq import List 6 | from env import Environment 7 | 8 | class AtomTests(TestCase): 9 | def test_truthiness(self): 10 | self.assertEquals(TRUE, Symbol("t")) 11 | 12 | def test_falsiness(self): 13 | self.assertEquals(FALSE, List()) 14 | 15 | def test_atomness(self): 16 | foo = Atom("foo") 17 | another_foo = Atom("foo") 18 | bar = Atom("bar") 19 | baz = Atom("baz") 20 | 21 | self.assertTrue(foo == foo) 22 | self.assertTrue(foo == another_foo) 23 | self.assertTrue(foo != bar) 24 | self.assertTrue(baz != bar) 25 | self.assertTrue(foo != bar != baz) 26 | 27 | def test_symbolness(self): 28 | foo = Symbol("foo") 29 | another_foo = Symbol("foo") 30 | bar = Symbol("bar") 31 | e = Environment(None, {"foo":foo}) 32 | 33 | self.assertTrue(foo != bar) 34 | self.assertTrue(foo == another_foo) 35 | self.assertTrue(another_foo == foo) 36 | self.assertTrue(foo.__hash__() == another_foo.__hash__()) 37 | self.assertTrue(foo.eval(e) == foo) 38 | -------------------------------------------------------------------------------- /src/rb/env.rb: -------------------------------------------------------------------------------- 1 | module Fogus 2 | class Env 3 | attr_reader :level 4 | 5 | def initialize(p=nil,b=nil) 6 | if b 7 | @bindings = b 8 | else 9 | @bindings = Hash.new 10 | end 11 | 12 | @parent = p 13 | 14 | if @parent 15 | @level = @parent.level + 1 16 | else 17 | @level = 0 18 | end 19 | end 20 | 21 | def get(key) 22 | if @bindings.has_key? key 23 | return @bindings[key] 24 | else 25 | return @parent.get(key) rescue raise "Unknown binding for #{key} in context #{@bindings.inspect}" 26 | end 27 | end 28 | 29 | def set(key, val) 30 | if @bindings.has_key? key 31 | @bindings[key] = val 32 | elsif @parent 33 | @parent.set(key, val) 34 | else 35 | @bindings[key] = val 36 | end 37 | end 38 | 39 | def def?(key) 40 | @bindings.has_key? key 41 | end 42 | 43 | def push(context=nil) 44 | Env.new(self, context) 45 | end 46 | 47 | def pop() 48 | @parent 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /src/rb/fun.rb: -------------------------------------------------------------------------------- 1 | module Fogus 2 | class Fun 3 | attr_reader :fun 4 | 5 | def initialize(f) 6 | @fun = f 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /src/rb/lisp.rb: -------------------------------------------------------------------------------- 1 | module Fogus 2 | class Lithp 3 | @@eq = lambda { |lhs, rhs| 4 | false 5 | } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /src/rb/lithp.rb: -------------------------------------------------------------------------------- 1 | # The Lithp interpreter 2 | # For more information: 3 | # http://www.slideshare.net/jza/python-3000 4 | # http://www.ibm.com/developerworks/linux/library/l-python3-1/ 5 | # http://www.python.org/dev/peps/pep-3000/ 6 | # http://www.python.org/dev/peps/ 7 | # http://www.python.org/dev/peps/pep-0008/ 8 | 9 | require 'reader' 10 | require 'env' 11 | require 'lisp' 12 | require 'fun' 13 | 14 | NAME = 'Lithp' 15 | VER = '0.0.1' 16 | WWW = 'http://github.com/fogus/lithp' 17 | PROMPT = 'lithp' 18 | DEPTH_MARK = '.' 19 | 20 | module Fogus 21 | class Lithp 22 | attr_reader :stdin, :stdout, :stderr 23 | attr_reader :core, :closures 24 | attr_reader :rdr, :global 25 | 26 | def print_banner 27 | puts "The #{NAME} programming shell v#{VER}" 28 | puts " by Fogus, #{WWW}" 29 | puts " Type :help for more information" 30 | puts "" 31 | end 32 | 33 | def initialize 34 | @stdin = STDIN 35 | @stdout = STDOUT 36 | @stderr = STDERR 37 | @rdr = Reader.new 38 | @global = Env.new 39 | @core, @closures = true, false 40 | 41 | bootstrap 42 | end 43 | 44 | def bootstrap 45 | @global.set('eq', Fun.new(@@eq)) 46 | end 47 | 48 | def read_line(prompt) 49 | @stdout.write(prompt) 50 | line = @stdin.gets.strip 51 | 52 | if line.empty? 53 | :done 54 | end 55 | 56 | line 57 | end 58 | 59 | def get_complete_expr(line='',depth=0) 60 | line += ' ' unless line.empty? 61 | 62 | if @global.level != 0 63 | prompt = "#{PROMPT} #{@global.level}#{DEPTH_MARK * (depth + 1)}" 64 | else 65 | if depth == 0 66 | prompt = "#{PROMPT}> " 67 | else 68 | prompt = "#{PROMPT}#{DEPTH_MARK * (depth + 1)}" 69 | end 70 | 71 | line += read_line(prompt) 72 | end 73 | 74 | balance = 0; 75 | line.each_char {|c| 76 | if c.eql?('(') 77 | balance += 1 78 | elsif c.eql?(')') 79 | balance -= 1 80 | end 81 | } 82 | 83 | if balance > 0 84 | get_complete_expr(line, depth+1) 85 | elsif balance < 0 86 | raise "Unmatched parenthesis #{balance}" 87 | end 88 | 89 | line 90 | end 91 | 92 | def process(expr) 93 | puts 'this is where i will get the sexpr from the rdr' 94 | puts "for the expr #{expr}" 95 | end 96 | 97 | def go 98 | while true 99 | expr = get_complete_expr 100 | 101 | if expr.eql? ':quit' 102 | puts 'bye now' 103 | break 104 | elsif expr.eql? ':help' 105 | puts 'help is on its way' 106 | elsif expr == :done 107 | puts 'end of file encountered' 108 | break 109 | elsif expr.eql? ':env' 110 | puts 'todo - print env' 111 | else 112 | process(expr) 113 | end 114 | 115 | puts expr 116 | end 117 | end 118 | end 119 | end 120 | 121 | repl = Fogus::Lithp.new 122 | repl.print_banner 123 | repl.go 124 | 125 | -------------------------------------------------------------------------------- /src/rb/reader.rb: -------------------------------------------------------------------------------- 1 | module Fogus 2 | class Reader 3 | end 4 | end 5 | --------------------------------------------------------------------------------