518 lines
17 KiB
Python
518 lines
17 KiB
Python
#!/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 '#<apply>'
|
||
|
||
class CallCcClass:
|
||
def __str__(self):
|
||
return '#<call/cc>'
|
||
|
||
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()
|