I decided to learn more about how interpreters work and made a simple Scheme interpreter in Python. I'd like to hear some advice on how I could have written this better, or on the mistakes I've made.
Parser.py (I thought about putting the Tokenizer and Atom classes in different files, but they are too small for that. Maybe I should somehow rename the file to get rid of the confusion?):
import Types class Parser: def __init__(self) -> None: pass def parse(self, tokens : list) -> Types.Exp: if len(tokens) == 0: raise SyntaxError('Unexpected EOF.') token = tokens.pop(0) if token == '(': L = [] while tokens[0] != ')': L.append(self.parse(tokens)) tokens.pop(0) # pop off ')' return L elif token == ')': raise SyntaxError('Unexpected syntax.') else: atom = Atom(token) return atom.value class Tokenizer: def tokenize(self, program : str) -> list: return program.replace('(', ' ( ').replace(')', ' ) ').split() class Atom: def __init__(self, value) -> None: self.value = self.__set__value(value) def __set__value(self, value): try: return int(value) except ValueError: try: return float(value) except ValueError: return Types.Symbol(value)
Types.py:
Symbol = str List = list Number = (int, float) Atom = (Symbol, Number) Exp = (Atom, List)
Eval.py (I thought it was not good to implement the Procedure class here and it would be more natural to put it in Types.py, but I could not solve the problem with the two-way import. I need to import Types.py into Eval.py and Eval.py into Types.py while I use eval() in Procedure.__call__):
import Types from Environment import Environment global_env = Environment() def eval(exp : Types.Exp, e = global_env): if isinstance(exp, Types.Symbol): return e.env[exp] elif isinstance(exp, Types.Number): return exp op = exp[0] args = exp[1:] if op == 'quote': return args[0] elif op == "if": (syntax_if, test, conseq, alt) = exp exp = (conseq if eval(test, e) else alt) return eval(exp, e) elif op == "define": (_, symbol, exp) = exp e.env[symbol] = eval(exp, e) elif op == "set!": (symbol, value) = args e.env[symbol] = eval(value, e) elif op == "lambda": (parms, body) = args return Procedure(parms, body, e) else: proc = eval(op, e) args = [eval(arg, e) for arg in args] if proc is None and args is not None: for arg in args: if arg is not None: return arg return proc(*args) class Procedure: def __init__(self, parms, body, env): self.parms, self.body, self.env = parms, body, env def __call__(self, *args): function_env = Environment(self.parms, args, self.env) return eval(self.body, function_env)
Environment.py (I implemented memory using dictionary):
import math import operator as op import Types class Environment: def __init__(self, parms=(), args=(), outer = None): self.env = {} if outer is not None: self.env = outer.env else: self.__set_default_env() self.env.update(zip(parms, args)) self.outer = outer def find(self, key): if key in self.env: return self.env[key] elif self.outer is not None: return self.outer.find(key) return None def update(self, parms=(), args=()): self.env.update(zip(parms, args)) def __set_default_env(self): self.env.update(vars(math)) self.env.update({ '+':op.add, '-':op.sub, '*':op.mul, '/':op.truediv, '>':op.gt, '<':op.lt, '>=':op.ge, '<=':op.le, '=':op.eq, 'abs': abs, 'append': op.add, 'apply': lambda proc, args: proc(*args), 'begin': lambda *x: x[-1], 'car': lambda x: x[0], 'cdr': lambda x: x[1:], 'cons': lambda x,y: [x] + y, 'eq?': op.is_, 'expt': pow, 'equal?': op.eq, 'length': len, 'list': lambda *x: Types.List(x), 'list?': lambda x: isinstance(x, Types.List), 'map': map, 'max': max, 'min': min, 'not': op.not_, 'null?': lambda x: x == [], 'number?': lambda x: isinstance(x, Types.Number), 'print': print, 'procedure?': callable, 'round': round, 'symbol?': lambda x: isinstance(x, Types.Symbol), })
I also made some tests (link on github repository)
eval
shadows a Python built-in, you might want to rename the function.\$\endgroup\$