hakkoso/scm.py

518 lines
17 KiB
Python
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

#!/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()