#!/usr/bin/env python """ A Little Scheme in Python 2.7/3.8, v3.2 H31.01.13/R02.04.09 by SUZUKI Hisao """ from __future__ import print_function from sys import argv, exit try: from sys import intern # for Python 3 raw_input = input # for Python 3 long = int # for Python 3 except ImportError: pass class List (object): "Empty list" __slots__ = () def __iter__(self): return iter(()) def __len__(self): n = 0 for e in self: n += 1 return n NIL = List() class Cell (List): "Cons cell" __slots__ = ('car', 'cdr') def __init__(self, car, cdr): self.car, self.cdr = car, cdr def __iter__(self): "Yield car, cadr, caddr and so on." j = self while isinstance(j, Cell): yield j.car j = j.cdr if j is not NIL: raise ImproperListError(j) class ImproperListError (Exception): pass QUOTE = intern('quote') # Use an interned string as a symbol. IF = intern('if') BEGIN = intern('begin') LAMBDA = intern('lambda') DEFINE = intern('define') DEFINE_RECORD_TYPE = intern('define-record-type') SETQ = intern('set!') APPLY = intern('apply') CALLCC = intern('call/cc') NOCONT = () # NOCONT means there is no continuation. # Continuation operators THEN = intern('then') APPLY_FUN = intern('aplly-fun') EVAL_ARG = intern('eval-arg') CONS_ARGS = intern('cons-args') RESTORE_ENV = intern('restore-env') class ApplyClass: def __str__(self): return '#' class CallCcClass: def __str__(self): return '#' APPLY_OBJ = ApplyClass() CALLCC_OBJ = CallCcClass() class SchemeString: "String in Scheme" def __init__(self, string): self.string = string def __repr__(self): return '"' + self.string + '"' class Environment (object): "Linked list of bindings mapping symbols to values" __slots__ = ('sym', 'val', 'next') def __init__(self, sym, val, next): "(env.sym is None) means the env is the frame top. " self.sym, self.val, self.next = sym, val, next def __iter__(self): "Yield each binding in the linked list." env = self while env is not None: yield env env = env.next def look_for(self, symbol): "Search the bindings for a symbol." for env in self: if env.sym is symbol: return env raise NameError(symbol) def prepend_defs(self, symbols, data): "Build an environment prepending the bindings of symbols and data." if symbols is NIL: if data is not NIL: raise TypeError('surplus arg: ' + stringify(data)) return self else: if data is NIL: raise TypeError('surplus param: ' + stringify(symbols)) return Environment(symbols.car, data.car, self.prepend_defs(symbols.cdr, data.cdr)) class Closure (object): "Lambda expression with its environment" __slots__ = ('params', 'body', 'env') def __init__(self, params, body, env): self.params, self.body, self.env = params, body, env class Intrinsic (object): "Built-in function" __slots__ = ('name', 'arity', 'fun') def __init__(self, name, arity, fun): self.name, self.arity, self.fun = name, arity, fun def __repr__(self): return '#<%s:%d>' % (self.name, self.arity) def stringify(exp, quote=True): "Convert an expression to a string." if exp is True: return '#t' elif exp is False: return '#f' elif isinstance(exp, List): ss = [] try: for element in exp: ss.append(stringify(element, quote)) except ImproperListError as ex: ss.append('.') ss.append(stringify(ex.args[0], quote)) return '(' + ' '.join(ss) + ')' elif isinstance(exp, Environment): ss = [] for env in exp: if env is GLOBAL_ENV: ss.append('GlobalEnv') break elif env.sym is None: # marker of the frame top ss.append('|') else: ss.append(env.sym) return '#<' + ' '.join(ss) + '>' elif isinstance(exp, Closure): p, b, e = [stringify(x) for x in (exp.params, exp.body, exp.env)] return '#<' + p + ':' + b + ':' + e + '>' elif isinstance(exp, tuple) and len(exp) == 3: p, v, k = [stringify(x) for x in exp] return '#<' + p + ':' + v + ':\n ' + k + '>' elif isinstance(exp, SchemeString) and not quote: return exp.string else: return str(exp) def _globals(x): "Return a list of keys of the global environment." j, env = NIL, GLOBAL_ENV.next # Take next to skip the marker. for e in env: j = Cell(e.sym, j) return j def _error(x): "Based on SRFI-23" raise ErrorException("Error: %s: %s" % (stringify(x.car, False), stringify(x.cdr.car))) class ErrorException (Exception): pass _ = lambda n, a, f, next: Environment(intern(n), Intrinsic(n, a, f), next) GLOBAL_ENV = ( _('+', 2, lambda x: x.car + x.cdr.car, _('-', 2, lambda x: x.car - x.cdr.car, _('*', 2, lambda x: x.car * x.cdr.car, _('/', 2, lambda x: x.car / x.cdr.car, _('>', 2, lambda x: x.car > x.cdr.car, _('>=', 2, lambda x: x.car >= x.cdr.car, _('<', 2, lambda x: x.car < x.cdr.car, _('<=', 2, lambda x: x.car <= x.cdr.car, _('=', 2, lambda x: x.car == x.cdr.car, _('and', 2, lambda x: x.car and x.cdr.car, _('number?', 1, lambda x: isinstance(x.car, (int, float, long)), _('error', 2, _error, _('globals', 0, _globals, None)))))))))))))) # My custom helpers from datetime import datetime GLOBAL_ENV = ( _('now', 0, lambda x: datetime.now(), _('concat', -1, lambda x: ''.join((stringify(x, False) for x in x)), _('exit', 0, lambda x: exit(), GLOBAL_ENV)))) # records = {} # def make_record_type(name, params): # # {n: None for n in x.cdr} # # exp, k = kdr.cdr.car, (SETQ, env.look_for(kdr.car), k) # # def first(cons, res=()): # # print(cons, 'res', res) # # if cons is NIL: # # return res # # return first(cons.cdr, (*res, cons.car)) # # print('params:', first(params)) # # print('x:', params.car.car) # records[name] = tuple(x for x in params.car) # print(records) # return params # Records GLOBAL_ENV = ( _('record?', 1, lambda x: x.car.string in records.keys(), GLOBAL_ENV)) GLOBAL_ENV = ( _('display', 1, lambda x: print(stringify(x.car, False), end=''), _('newline', 0, lambda x: print(), _('read', 0, lambda x: read_expression('', ''), _('eof-object?', 1, lambda x: isinstance(x.car, EOFError), _('symbol?', 1, lambda x: isinstance(x.car, str), Environment(CALLCC, CALLCC_OBJ, Environment(APPLY, APPLY_OBJ, GLOBAL_ENV)))))))) GLOBAL_ENV = Environment( None, None, # marker of the frame top _('car', 1, lambda x: x.car.car, _('cdr', 1, lambda x: x.car.cdr, _('cons', 2, lambda x: Cell(x.car, x.cdr.car), _('eq?', 2, lambda x: x.car is x.cdr.car, _('pair?', 1, lambda x: isinstance(x.car, Cell), _('null?', 1, lambda x: x.car is NIL, _('not', 1, lambda x: x.car is False, _('list', -1, lambda x: x, GLOBAL_ENV))))))))) def evaluate(exp, env=GLOBAL_ENV): "Evaluate an expression in an environment." k = NOCONT try: while True: while True: if isinstance(exp, Cell): kar, kdr = exp.car, exp.cdr if kar is QUOTE: # (quote e) exp = kdr.car break elif kar is IF: # (if e1 e2 e3) or (if e1 e2) exp, k = kdr.car, (THEN, kdr.cdr, k) elif kar is BEGIN: # (begin e...) exp = kdr.car if kdr.cdr is not NIL: k = (BEGIN, kdr.cdr, k) elif kar is LAMBDA: # (lambda (v...) e...) exp = Closure(kdr.car, kdr.cdr, env) break elif kar is DEFINE: # (define v e) v = kdr.car assert isinstance(v, str), v exp, k = kdr.cdr.car, (DEFINE, v, k) elif kar is DEFINE_RECORD_TYPE: # x = v print(kar, kdr.car, kdr.cdr.car.car) exp, k = kdr.cdr.car, (DEFINE_RECORD_TYPE, kdr.car, kdr.cdr.car) exp = None elif kar is SETQ: # (set! v e) exp, k = kdr.cdr.car, (SETQ, env.look_for(kdr.car), k) else: exp, k = kar, (APPLY, kdr, k) elif isinstance(exp, str): exp = env.look_for(exp).val break else: # as a number, #t, #f etc. break while True: if k is NOCONT: return exp op, x, k = k if op is THEN: # x = (e2 e3) if exp is False: if x.cdr is NIL: exp = None else: exp = x.cdr.car # e3 break else: exp = x.car # e2 break elif op is BEGIN: # x = (e...) if x.cdr is not NIL: # unless tail call... k = (BEGIN, x.cdr, k) exp = x.car break elif op is DEFINE: # x = v assert env.sym is None # Check for the marker. env.next = Environment(x, exp, env.next) exp = None elif op is DEFINE_RECORD_TYPE: # x = v assert env.sym is None # Check for the marker. # for x, exp in (): # env.next = Environment(x, exp, env.next) print('OP, x, k', op, x, k, exp) # DEFINE: # 1. x? # 2. x-a, x-b, x-c exit() env.next = Environment(x, exp, env.next) exp = None elif op is SETQ: # x = Environment(v, e, next) x.val = exp exp = None elif op is APPLY: # x = args; exp = fun if x is NIL: exp, k, env = apply_function(exp, NIL, k, env) else: k = (APPLY_FUN, exp, k) while x.cdr is not NIL: k = (EVAL_ARG, x.car, k) x = x.cdr exp = x.car k = (CONS_ARGS, NIL, k) break elif op is CONS_ARGS: # x = evaluated args args = Cell(exp, x) op, exp, k = k if op is EVAL_ARG: # exp = the next arg k = (CONS_ARGS, args, k) break elif op is APPLY_FUN: # exp = evaluated fun exp, k, env = apply_function(exp, args, k, env) else: raise RuntimeError('unexpected op: %s: %s' % (stringify(op), stringify(exp))) elif op is RESTORE_ENV: # x = env env = x else: raise RuntimeError('bad op: %s: %s' % (stringify(op), stringify(x))) except ErrorException: raise except Exception as ex: msg = type(ex).__name__ + ': ' + str(ex) if k is not NOCONT: msg += '\n ' + stringify(k) raise Exception(msg) def apply_function(fun, arg, k, env): """Apply a function to arguments with a continuation. It returns (result, continuation, environment). """ while True: if fun is CALLCC_OBJ: k = _push_RESTORE_ENV(k, env) fun, arg = arg.car, Cell(k, NIL) elif fun is APPLY_OBJ: fun, arg = arg.car, arg.cdr.car else: break if isinstance(fun, Intrinsic): if fun.arity >= 0: if len(arg) != fun.arity: raise TypeError('arity not matched: ' + str(fun) + ' and ' + stringify(arg)) return fun.fun(arg), k, env elif isinstance(fun, Closure): k = _push_RESTORE_ENV(k, env) k = (BEGIN, fun.body, k) env = Environment(None, None, # marker of the frame top fun.env.prepend_defs(fun.params, arg)) return None, k, env elif isinstance(fun, tuple): # as a continuation return arg.car, fun, env else: raise TypeError('not a function: ' + stringify(fun) + ' with ' + stringify(arg)) def _push_RESTORE_ENV(k, env): if k is NOCONT or k[0] is not RESTORE_ENV: # unless tail call... k = (RESTORE_ENV, env, k) return k def split_string_into_tokens(source_string): "split_string_into_tokens('(a 1)') => ['(', 'a', '1', ')']" result = [] for line in source_string.split('\n'): ss, x = [], [] for i, e in enumerate(line.split('"')): if i % 2 == 0: x.append(e) else: ss.append('"' + e) # Append a string literal. x.append('#s') s = ' '.join(x).split(';')[0] # Ignore ;-commment. s = s.replace("'", " ' ").replace(')', ' ) ').replace('(', ' ( ') x = s.split() result.extend([(ss.pop(0) if e == '#s' else e) for e in x]) assert not ss return result def read_from_tokens(tokens): """Read an expression from a list of token strings. The list will be left with the rest of token strings, if any. """ token = tokens.pop(0) if token == '(': y = z = Cell(NIL, NIL) while tokens[0] != ')': if tokens[0] == '.': tokens.pop(0) y.cdr = read_from_tokens(tokens) if tokens[0] != ')': raise SyntaxError(') is expected') break e = read_from_tokens(tokens) y.cdr = Cell(e, NIL) y = y.cdr tokens.pop(0) return z.cdr elif token == ')': raise SyntaxError('unexpected )') elif token == "'": e = read_from_tokens(tokens) return Cell(QUOTE, Cell(e, NIL)) # 'e => (quote e) elif token == '#f': return False elif token == '#t': return True elif token[0] == '"': return SchemeString(token[1:]) else: try: return int(token) except ValueError: try: return float(token) except ValueError: return intern(token) # as a symbol def eval(code, env=GLOBAL_ENV): tokens = split_string_into_tokens(code) resp = None while tokens: exp = read_from_tokens(tokens) resp = evaluate(exp, env) return resp def load(file_name, env=GLOBAL_ENV): "Load a source code from a file." with open(file_name) as rf: source_string = rf.read() return eval(source_string, env) TOKENS = [] def read_expression(prompt1='> ', prompt2='| '): "Read an expression." while True: old = TOKENS[:] try: return read_from_tokens(TOKENS) except IndexError: # tokens have run out unexpectedly. try: source_string = raw_input(prompt2 if old else prompt1) except EOFError as ex: return ex TOKENS[:] = old TOKENS.extend(split_string_into_tokens(source_string)) except SyntaxError: del TOKENS[:] # Discard the erroneous tokens. raise def read_eval_print_loop(): "Repeat read-eval-print until End-of-File." while True: try: exp = read_expression() if isinstance(exp, EOFError): print('Goodbye') return result = evaluate(exp) if result is not None: print(stringify(result, True)) except Exception as ex: print(ex) # import os # print(load(os.path.join(os.path.dirname(__file__), 'init.scm'))) if __name__ == '__main__': if argv[1:2]: load(argv[1]) if argv[2:3] != ['-']: exit(0) read_eval_print_loop()