├── README.md ├── eval.tl ├── sqrt.tl └── tiddlylisp.py /README.md: -------------------------------------------------------------------------------- 1 | # tiddlylisp 2 | 3 | Tiddlylisp is a toy Lisp interpreter, written in Python, and intended 4 | to accompany the essay 5 | [Lisp as the Maxwell's equations of software](http://michaelnielsen.org/ddi/lisp-as-the-maxwells-equations-of-software/). 6 | 7 | The repository contains the following files: 8 | 9 | `tiddlylisp.py`: A simple interpreter for a subset of Lisp. 10 | Tiddlylisp is adapted from and closely based on Peter Norvig's 11 | [lispy interpreter](http://norvig.com/lispy.html). 12 | 13 | `sqrt.tl`: An example tiddlylisp program, for computing square roots. 14 | Adapted from an example in the book 15 | [The Structure and Interpretation of Computer Programs](http://mitpress.mit.edu/sicp/), 16 | by Harold Abelson and Gerald Jay Sussman with Julie Sussmann. 17 | 18 | `eval.tl`: A tiddylisp program defining a function `eval` which can 19 | evaluate any Lisp expression, albeit, for an even smaller subset of 20 | Lisp than tiddlylisp. Based on the 21 | [LISP 1.5 Programmer's Manual](http://www.softwarepreservation.org/projects/LISP/book/LISP%201.5%20Programmers%20Manual.pdf) 22 | and the essay 23 | [The Roots of Lisp](http://lib.store.yahoo.net/lib/paulgraham/jmc.ps) 24 | (postscript) by Paul Graham. 25 | 26 | See 27 | [the essay](http://michaelnielsen.org/ddi/lisp-as-the-maxwells-equations-of-software/) 28 | for more details. 29 | 30 | Notifications of bugs (and bug fixes) are welcome, but I am not adding 31 | new features to tiddlylisp, as its main purpose is as a complement to 32 | the essay. 33 | 34 | Note added (June 3, 2023): Switched to Python 3. 35 | -------------------------------------------------------------------------------- /eval.tl: -------------------------------------------------------------------------------- 1 | (define caar (lambda (x) (car (car x)))) 2 | (define cadr (lambda (x) (car (cdr x)))) 3 | (define cadar (lambda (x) (cadr (car x)))) 4 | (define caddr (lambda (x) (cadr (cdr x)))) 5 | (define caddar (lambda (x) (caddr (car x)))) 6 | 7 | (define not (lambda (x) (if x False True))) 8 | 9 | (define append (lambda (x y) 10 | (if (null? x) y (cons (car x) (append (cdr x) y))))) 11 | 12 | (define pair (lambda (x y) (cons x (cons y (q ()) )))) 13 | 14 | (define pairlis 15 | (lambda (x y) 16 | (if (null? x) 17 | (q ()) 18 | (cons (pair (car x) (car y)) (pairlis (cdr x) (cdr y)))))) 19 | 20 | (define assoc (lambda (x y) 21 | (if (eq? (caar y) x) (cadar y) (assoc x (cdr y))))) 22 | 23 | (define eval 24 | (lambda (e a) 25 | (cond 26 | ((atom? e) (assoc e a)) 27 | ((atom? (car e)) 28 | (cond 29 | ((eq? (car e) (q car)) (car (eval (cadr e) a))) 30 | ((eq? (car e) (q cdr)) (cdr (eval (cadr e) a))) 31 | ((eq? (car e) (q cons)) (cons (eval (cadr e) a) (eval (caddr e) a))) 32 | ((eq? (car e) (q atom?)) (atom? (eval (cadr e) a))) 33 | ((eq? (car e) (q eq?)) (eq? (eval (cadr e) a) (eval (caddr e) a))) 34 | ((eq? (car e) (q quote)) (cadr e)) 35 | ((eq? (car e) (q q)) (cadr e)) 36 | ((eq? (car e) (q cond)) (evcon (cdr e) a)) 37 | (True (eval (cons (assoc (car e) a) (cdr e)) a)))) 38 | ((eq? (caar e) (q lambda)) 39 | (eval (caddar e) (append (pairlis (cadar e) (evlis (cdr e) a)) a)))))) 40 | 41 | (define evcon 42 | (lambda (c a) 43 | (cond ((eval (caar c) a) (eval (cadar c) a)) 44 | (True (evcon (cdr c) a))))) 45 | 46 | (define evlis 47 | (lambda (m a) 48 | (cond ((null? m) (q ())) 49 | (True (cons (eval (car m) a) (evlis (cdr m) a)))))) 50 | 51 | 52 | (define assert-equal (lambda (x y) (= x y))) 53 | 54 | (define assert-not-equal (lambda (x y) (not (assert-equal x y)))) 55 | 56 | (assert-equal (eval (q x) (q ((x test-value)))) 57 | (q test-value)) 58 | (assert-equal (eval (q y) (q ((y (1 2 3))))) 59 | (q (1 2 3))) 60 | (assert-not-equal (eval (q z) (q ((z ((1) 2 3))))) 61 | (q (1 2 3))) 62 | (assert-equal (eval (q (quote 7)) (q ())) 63 | (q 7)) 64 | (assert-equal (eval (q (atom? (q (1 2)))) (q ())) 65 | False) 66 | (assert-equal (eval (q (eq? 1 1)) (q ((1 1)))) 67 | True) 68 | (assert-equal (eval (q (eq? 1 2)) (q ((1 1) (2 2)))) 69 | False) 70 | (assert-equal (eval (q (eq? 1 1)) (q ((1 1)))) 71 | True) 72 | (assert-equal (eval (q (car (q (3 2)))) (q ())) 73 | (q 3)) 74 | (assert-equal (eval (q (cdr (q (1 2 3)))) (q ())) 75 | (q (2 3))) 76 | (assert-not-equal (eval (q (cdr (q (1 (2 3) 4)))) (q ())) 77 | (q (2 3 4))) 78 | (assert-equal (eval (q (cons 1 (q (2 3)))) (q ((1 1)(2 2)(3 3)))) 79 | (q (1 2 3))) 80 | (assert-equal (eval (q (cond ((atom? x) (q x-atomic)) 81 | ((atom? y) (q y-atomic)) 82 | ((q True) (q nonatomic)))) 83 | (q ((x 1)(y (3 4))))) 84 | (q x-atomic)) 85 | (assert-equal (eval (q (cond ((atom? x) (q x-atomic)) 86 | ((atom? y) (q y-atomic)) 87 | ((q True) (q nonatomic)))) 88 | (q ((x (1 2))(y 3)))) 89 | (q y-atomic)) 90 | (assert-equal (eval (q (cond ((atom? x) (q x-atomic)) 91 | ((atom? y) (q y-atomic)) 92 | ((q True) (q nonatomic)))) 93 | (q ((x (1 2))(y (3 4))))) 94 | (q nonatomic)) 95 | (assert-equal (eval (q ((lambda (x) (car (cdr x))) (q (1 2 3 4)))) (q ())) 96 | 2) 97 | -------------------------------------------------------------------------------- /sqrt.tl: -------------------------------------------------------------------------------- 1 | (define sqrt (lambda (x) (sqrt-iter 1.0 x))) 2 | (define sqrt-iter 3 | (lambda (guess x) 4 | (if (good-enough? guess x) guess (sqrt-iter (improve guess x) x)))) 5 | (define good-enough? 6 | (lambda (guess x) (< (abs (- x (square guess))) 0.00001))) 7 | (define abs (lambda (x) (if (< 0 x) x (- 0 x)))) 8 | (define square (lambda (x) (* x x))) 9 | (define improve (lambda (guess x) (average guess (/ x guess)))) 10 | (define average (lambda (x y) (* 0.5 (+ x y)))) 11 | -------------------------------------------------------------------------------- /tiddlylisp.py: -------------------------------------------------------------------------------- 1 | #### tiddlylisp.py 2 | # 3 | # Based on Peter Norvig's lispy (http://norvig.com/lispy.html), 4 | # copyright by Peter Norvig, 2010. 5 | # 6 | # Adaptations by Michael Nielsen. See 7 | # http://michaelnielsen.org/ddi/lisp-as-the-maxwells-equations-of-software/ 8 | 9 | import sys 10 | import traceback 11 | 12 | #### Symbol, Env classes 13 | 14 | Symbol = str 15 | 16 | class Env(dict): 17 | "An environment: a dict of {'var':val} pairs, with an outer Env." 18 | 19 | def __init__(self, params=(), args=(), outer=None): 20 | self.update(list(zip(params, args))) 21 | self.outer = outer 22 | 23 | def find(self, var): 24 | "Find the innermost Env where var appears." 25 | return self if var in self else self.outer.find(var) 26 | 27 | def add_globals(env): 28 | "Add some built-in procedures and variables to the environment." 29 | import operator 30 | env.update( 31 | {'+': operator.add, 32 | '-': operator.sub, 33 | '*': operator.mul, 34 | '/': operator.truediv, 35 | '>': operator.gt, 36 | '<': operator.lt, 37 | '>=': operator.ge, 38 | '<=': operator.le, 39 | '=': operator.eq 40 | }) 41 | env.update({'True': True, 'False': False}) 42 | return env 43 | 44 | global_env = add_globals(Env()) 45 | 46 | isa = isinstance 47 | 48 | #### eval 49 | 50 | def eval(x, env=global_env): 51 | "Evaluate an expression in an environment." 52 | if isa(x, Symbol): # variable reference 53 | return env.find(x)[x] 54 | elif not isa(x, list): # constant literal 55 | return x 56 | elif x[0] == 'quote' or x[0] == 'q': # (quote exp), or (q exp) 57 | (_, exp) = x 58 | return exp 59 | elif x[0] == 'atom?': # (atom? exp) 60 | (_, exp) = x 61 | return not isa(eval(exp, env), list) 62 | elif x[0] == 'eq?': # (eq? exp1 exp2) 63 | (_, exp1, exp2) = x 64 | v1, v2 = eval(exp1, env), eval(exp2, env) 65 | return (not isa(v1, list)) and (v1 == v2) 66 | elif x[0] == 'car': # (car exp) 67 | (_, exp) = x 68 | return eval(exp, env)[0] 69 | elif x[0] == 'cdr': # (cdr exp) 70 | (_, exp) = x 71 | return eval(exp, env)[1:] 72 | elif x[0] == 'cons': # (cons exp1 exp2) 73 | (_, exp1, exp2) = x 74 | return [eval(exp1, env)]+eval(exp2,env) 75 | elif x[0] == 'cond': # (cond (p1 e1) ... (pn en)) 76 | for (p, e) in x[1:]: 77 | if eval(p, env): 78 | return eval(e, env) 79 | elif x[0] == 'null?': # (null? exp) 80 | (_, exp) = x 81 | return eval(exp,env) == [] 82 | elif x[0] == 'if': # (if test conseq alt) 83 | (_, test, conseq, alt) = x 84 | return eval((conseq if eval(test, env) else alt), env) 85 | elif x[0] == 'set!': # (set! var exp) 86 | (_, var, exp) = x 87 | env.find(var)[var] = eval(exp, env) 88 | elif x[0] == 'define': # (define var exp) 89 | (_, var, exp) = x 90 | env[var] = eval(exp, env) 91 | elif x[0] == 'lambda': # (lambda (var*) exp) 92 | (_, vars, exp) = x 93 | return lambda *args: eval(exp, Env(vars, args, env)) 94 | elif x[0] == 'begin': # (begin exp*) 95 | for exp in x[1:]: 96 | val = eval(exp, env) 97 | return val 98 | else: # (proc exp*) 99 | exps = [eval(exp, env) for exp in x] 100 | proc = exps.pop(0) 101 | return proc(*exps) 102 | 103 | #### parsing 104 | 105 | def parse(s): 106 | "Parse a Lisp expression from a string." 107 | return read_from(tokenize(s)) 108 | 109 | def tokenize(s): 110 | "Convert a string into a list of tokens." 111 | return s.replace("(", " ( ").replace(")", " ) ").split() 112 | 113 | def read_from(tokens): 114 | "Read an expression from a sequence of tokens." 115 | if len(tokens) == 0: 116 | raise SyntaxError('unexpected EOF while reading') 117 | token = tokens.pop(0) 118 | if '(' == token: 119 | L = [] 120 | while tokens[0] != ')': 121 | L.append(read_from(tokens)) 122 | tokens.pop(0) # pop off ')' 123 | return L 124 | elif ')' == token: 125 | raise SyntaxError('unexpected )') 126 | else: 127 | return atom(token) 128 | 129 | def atom(token): 130 | "Numbers become numbers; every other token is a symbol." 131 | try: return int(token) 132 | except ValueError: 133 | try: return float(token) 134 | except ValueError: 135 | return Symbol(token) 136 | 137 | def to_string(exp): 138 | "Convert a Python object back into a Lisp-readable string." 139 | if not isa(exp, list): 140 | return str(exp) 141 | else: 142 | return '('+' '.join(map(to_string, exp))+')' 143 | 144 | #### Load from a file and run 145 | 146 | def load(filename): 147 | """ 148 | Load the tiddlylisp program in filename, execute it, and start the 149 | repl. If an error occurs, execution stops, and we are left in the 150 | repl. Note that load copes with multi-line tiddlylisp code by 151 | merging lines until the number of opening and closing parentheses 152 | match. 153 | """ 154 | print("Loading and executing %s" % filename) 155 | f = open(filename, "r") 156 | program = f.readlines() 157 | f.close() 158 | rps = running_paren_sums(program) 159 | full_line = "" 160 | for (paren_sum, program_line) in zip(rps, program): 161 | program_line = program_line.strip() 162 | full_line += program_line+" " 163 | if paren_sum == 0 and full_line.strip() != "": 164 | try: 165 | val = eval(parse(full_line)) 166 | if val is not None: print(to_string(val)) 167 | except: 168 | handle_error() 169 | print("\nThe line in which the error occurred:\n%s" % full_line) 170 | break 171 | full_line = "" 172 | repl() 173 | 174 | def running_paren_sums(program): 175 | """ 176 | Map the lines in the list program to a list whose entries contain 177 | a running sum of the per-line difference between the number of '(' 178 | parentheses and the number of ')' parentheses. 179 | """ 180 | count_open_parens = lambda line: line.count("(")-line.count(")") 181 | paren_counts = list(map(count_open_parens, program)) 182 | rps = [] 183 | total = 0 184 | for paren_count in paren_counts: 185 | total += paren_count 186 | rps.append(total) 187 | return rps 188 | 189 | #### repl 190 | 191 | def repl(prompt='tiddlylisp> '): 192 | "A prompt-read-eval-print loop." 193 | while True: 194 | try: 195 | val = eval(parse(input(prompt))) 196 | if val is not None: print(to_string(val)) 197 | except KeyboardInterrupt: 198 | print("\nExiting tiddlylisp\n") 199 | sys.exit() 200 | except: 201 | handle_error() 202 | 203 | #### error handling 204 | 205 | def handle_error(): 206 | """ 207 | Simple error handling for both the repl and load. 208 | """ 209 | print("An error occurred. Here's the Python stack trace:\n") 210 | traceback.print_exc() 211 | 212 | #### on startup from the command line 213 | 214 | if __name__ == "__main__": 215 | if len(sys.argv) > 1: 216 | load(sys.argv[1]) 217 | else: 218 | repl() 219 | --------------------------------------------------------------------------------