Extending Your Interpreter
Exercises to grow lisp.py toward a real Lisp:
Exercises to grow lisp.py toward a real Lisp:
Strings and comments: extend the tokenizer to handle "string literals" and ; comments.
Multi-expression bodies and let: desugar (let ((x 1) (y 2)) body...) into ((lambda (x y) body...) 1 2).
cond, and, or: add special forms (they must short-circuit, so they can't be plain functions).
Tail-call optimization: turn evaluate into a loop: for if, begin, and procedure calls in tail position, update x and env and continue instead of recursing. Then (define loop (lambda (n) (if (= n 0) 0 (loop (- n 1))))) works for a million iterations.
Quasiquote and macros: add quasiquote/unquote/unquote-splicing, then define-macro: store a macro as a procedure that receives unevaluated arguments; at evaluation time, call it and evaluate the returned expression.
Proper booleans and the empty list: add #t/#f literals and decide on truthiness rules.
Error handling: add a try/raise form using Python exceptions, or implement call/cc with exceptions for escape-only continuations.
A standard prelude: write map, filter, reduce, append, and reverse in your Lisp itself and load them at startup.
Read files: (load "file.lisp"), and support multiple top-level expressions per file.
Performance: compile expressions to Python closures once ("closure compilation") instead of re-walking the tree on every call.
Further projects: Make a Lisp (mal) (a step-by-step guide to implementing Lisp in 80+ languages), *Build Your Own Lisp* (in C), and *Lisp in Small Pieces* (Christian Queinnec) for implementation theory.
# Sketch: tail-call-optimized evaluate (replace the recursive version)
def evaluate(x, env=GLOBAL_ENV):
while True:
if isinstance(x, str):
return env.find(x)[x]
if not isinstance(x, list):
return x
head = x[0]
if head == 'quote':
return x[1]
if head == 'if':
_, test, conseq, alt = x
x = conseq if evaluate(test, env) else alt # loop instead of recursing
continue
if head == 'define':
_, name, expr = x
env[name] = evaluate(expr, env)
return None
if head == 'lambda':
_, params, body = x
return Procedure(params, body, env)
if head == 'begin':
for expr in x[1:-1]:
evaluate(expr, env)
x = x[-1]
continue
proc = evaluate(head, env)
args = [evaluate(a, env) for a in x[1:]]
if isinstance(proc, Procedure):
x, env = proc.body, Env(proc.params, args, proc.env) # tail call: reuse the loop
continue
return proc(*args)