scheme_primitives.py (plain text)
"""This module implements the primitives of the Scheme language."""
import math
import operator
import sys
import numbers
import re
try:
import turtle
except:
print("warning: could not import the turtle module.", file=sys.stderr)
#############################
# Errors and Error Checking #
#############################
class SchemeError(Exception):
"""Exception indicating an error in a Scheme program."""
def bad_type(val, k, name):
"""Raise a SchemeError using "argument K of NAME" to describe the
offending value (VAL)."""
msg = "argument {0} of {1} has wrong type ({2})"
raise SchemeError(msg.format(k, name, type(val).__name__))
def check_type(val, predicate, k, name):
"""Returns VAL. Raises a SchemeError if not PREDICATE(VAL)
using "argument K of NAME" to describe the offending value."""
if not predicate(val):
bad_type(val, k, name)
return val
#################
# Scheme Values #
#################
# Every value used in the interpreter to represent a Scheme value "is a"
# SchemeValue. Thus, if x is such a value, isinstance(x, SchemeValue).
# In proper object-oriented fashion, however, you will only need to use
# this test in 'assert' statements that catch bugs in your interpreter.
# The SchemeValue class has methods for testing for Scheme integers,
# procedures, etc. The SchemeValue class documents these methods by
# giving default 'def's for all of them. The various subclasses of
# SchemeValue, which stand for Scheme integers, Scheme pairs, etc., override
# these methods with appropriate methods of their own.
# As an example, consider the method pairp (returns a true Scheme value iff
# its argument is a pair). The default definition (in SchemeValue) returns
# scheme_false (which represents #f). Most subtypes of SchemeValue simply
# inherit this definition, and so also return #f. The type Pair, however,
# overrides pairp to return scheme_true (which represents #t). As a result,
# No 'if' statements are necessary to check for pairs.
# In other examples, such as the length method, the default implementation
# causes an error, since length is defined only for proper lists (so it
# is overridden in the classes Pair and Nil).
class SchemeValue:
"""The parent class of all Scheme values manipulated by the interpreter.
The methods here give default implementations, and are overridden in the
subclasses of SchemeValue."""
def __bool__(self):
"""True if I am supposed to count as a "true value" in Python. This
is the method used by Python's conditionals (if, and, or, not, while)
to determine whether some value is to be treated as meaning 'true'.
As a result, any SchemeValue that Scheme considers to represent true
also means true to Python. It is therefore unnecessary in Python to
write things like 'if EXPRESSION is not scheme_false: ...'"""
return True
def print_repr(self):
"""The string printed for me by the Scheme 'print' procedure."""
return str(self)
# The following methods return SchemeValues. Those ending in 'p' (for
# 'predicate') return scheme_false to mean 'false', and some other
# SchemeValue (often scheme_true) to mean 'true'.
def booleanp(self):
return scheme_false
def notp(self):
return scbool(not self)
def eqp(self, y):
return scbool(self is y)
def eqvp(self, y):
"""True iff I am equivalent to Y (same object, same numeric or boolean
value."""
return scbool(self is y)
def equalp(self, y):
"""True iff I am equal in value to Y."""
return scbool(self == y)
def atomp(self):
return scheme_true
def pairp(self):
return scheme_false
def nullp(self):
return scheme_false
def listp(self):
return scheme_false
def length(self):
bad_type(self, 0, "length")
def neg(self):
"""Unary negation (as in -x), returning a SchemeValue."""
bad_type(self, 0, "sub")
def quo(self, y):
bad_type(self, 0, "quotient")
def modulo(self, y):
bad_type(self, 0, "modulo")
def rem(self, y):
bad_type(self, 0, "remainder")
def floor(self):
bad_type(self, 0, "floor")
def ceil(self):
bad_type(self, 0, "ceil")
def eq(self, y):
bad_type(self, 0, "=")
def ltp(self, y):
bad_type(self, 0, "<")
def gtp(self, y):
bad_type(self, 0, ">")
def lep(self, y):
bad_type(self, 0, "<=")
def gep(self, y):
bad_type(self, 0, ">=")
def evenp(self):
bad_type(self, 0, "even?")
def oddp(self):
bad_type(self, 0, "odd?")
def zerop(self):
bad_type(self, 0, "zero?")
def cons(self, y):
return Pair(self, y)
def append(self, y):
bad_type(self, 0, "append")
def car(self):
bad_type(self, 0, "car")
def cdr(self):
bad_type(self, 0, "cdr")
def stringp(self):
return scheme_false
def symbolp(self):
return scheme_false
def numberp(self):
return scheme_false
def integerp(self):
return scheme_false
def evaluate_arguments(self, arg_list, env):
"""Evaluate the expressions in ARG_LIST in ENV to produce
arguments for this procedure. Returns an iterable of the results."""
msg = "attempt to call something of non-function type ({0})"
raise SchemeError(msg.format(type(self).__name__))
def apply(self, args, env):
"""Apply me to a Scheme list of ARGS in environment ENV. Returns
either
A. A tuple (val, None), where val is the value this procedure
returns when given ARGS and called in environment ENV, or
B. A tuple (expr1, env1), where evaluating the Scheme
expression expr1 in environment env1 will yield the same result
and the same effects as evaluating ARGS in ENV.
This method is overridden in procedure types. THis default
implementation causes an error.
NOTE: Until you complete extra-credit question 22, this method
will always return (val, None)."""
msg = "attempt to call something of non-function type ({0})"
raise SchemeError(msg.format(type(self).__name__))
def get_actual_value(self):
"""Returns any desired transformation of a value newly fetched
from a symbol before it is used. The default implementation
is simply the identity. [This method is useful for the call-by-name
extension in extra-credit Problem 23.]"""
return self
def scheme_coerce(x):
"""Returns the Scheme value corresponding to X. Converts numbers to
SchemeNumbers and strings to interned symbols. SchemeValues are unchanged.
Other values raise a TypeError."""
if isinstance(x, SchemeValue):
return x
elif isinstance(x, numbers.Number):
return scnum(x)
elif isinstance(x, str):
return intern(x)
else:
raise TypeError("cannot convert type {} to SchemeValue".format(type(x)))
########
# okay #
########
class okay(SchemeValue):
"""Signifies an undefined value."""
def __repr__(self):
return "okay"
okay = okay() # Assignment hides the okay class; there is only one instance
############
# Booleans #
############
class scheme_true(SchemeValue):
def booleanp(self):
return scheme_true
def __repr__(self):
return 'scheme_true'
def __str__(self):
return "#t"
class scheme_false(SchemeValue):
def __bool__(self):
return False
def booleanp(self):
return scheme_true
def __repr__(self):
return 'scheme_false'
def __str__(self):
return "#f"
# scheme_true and scheme_false hide the classes scheme_true and scheme_false.
# Thus they are the only instances of those classes.
scheme_true = scheme_true()
scheme_false = scheme_false()
def scbool(x):
"""The Scheme boolean value (#t or #f) that corresponds to the Python value
X. True Python values yield scheme_true, and false values yield
scheme_false."""
return scheme_true if x else scheme_false
###########
# Numbers #
###########
def _check_num(x, name):
"""If X is a number, return it. Otherwise, indicate a type error in
argument 1 of operation NAME."""
return check_type(x, scheme_numberp, 1, name)
class SchemeNumber(SchemeValue):
"""The parent class of all Scheme numeric types."""
def numberp(self):
return scheme_true
def __repr__(self):
return "scnum({})".format(self)
# Tests
def eq(self, y):
return scbool(self == _check_num(y, "="))
def ltp(self, y):
return scbool(self < _check_num(y, "<"))
def gtp(self, y):
return scbool(self > _check_num(y, ">"))
def lep(self, y):
return scbool(self <= _check_num(y, "<="))
def gep(self, y):
return scbool(self >= _check_num(y, ">="))
def zerop(self):
return scbool(self == 0)
# Types SchemeInt and SchemeFloat illustrate *multiple inheritance*.
# That is, they inherits methods from both SchemeValue and int or float.
# Thus, SchemeInts print as integers, and respond to all the
# same operators (+, -, etc.) as integers. To convert an ordinary Python
# integer, x, into a SchemeInt, use SchemeInt(x); likewise for SchemeFloat.
class SchemeInt(SchemeNumber, int):
def integerp(self):
return scheme_true
def neg(self):
return SchemeInt(-self)
# Scheme quotient rounds toward 0; Pythons rounds toward negative infinity.
def quo(self, y):
check_type(y, scheme_integerp, 1, "quotient")
try:
if (y < 0) != (self < 0):
return SchemeInt(- (abs(self) // abs(y)))
else:
return SchemeInt(self // y)
except ZeroDivisionError as err:
raise SchemeError(err)
def modulo(self, y):
check_type(y, scheme_integerp, 1, "modulo")
try:
return SchemeInt(self % y)
except ZeroDivisionError as err:
raise SchemeError(err)
def rem(self, y):
q = self.quo(y)
return SchemeInt(self - q * y)
def floor(self):
return self
def ceil(self):
return self
def eqvp(self, y):
return scbool(self == y)
def evenp(self):
return scbool(self % 2 == 0)
def oddp(self):
return scbool(self % 2 == 1)
class SchemeFloat(SchemeNumber, float):
def neg(self):
return SchemeFloat(-self)
def floor(self):
return SchemeInt(int(math.floor(self)))
def ceil(self):
return SchemeInt(int(math.ceil(self)))
def eqvp(self, y):
return scbool(self == y)
# Shorthand for numeric types.
scint = SchemeInt
scfloat = SchemeFloat
def scnum(num):
r = round(num)
if r == num:
return scint(r)
else:
return scfloat(num)
###########
# Symbols #
###########
class SchemeSymbol(SchemeValue):
"""Represents a Scheme symbol. Normally, one creates new symbols with
the static intern factory method. Each distinct symbol name is associated
with exactly one symbol. Symbol equality is simply object identity, so
that SchemeSymbols are hashable (so may be used as dictionary keys and
in sets)."""
def __init__(self, name):
"""A new symbol whose name is NAME. Normally, you should create new
symbols with intern (below). This function is for internal use."""
assert type(name) is str, \
"invalid type of symbol name: {}".format(type(name))
self.name = name
def symbolp(self):
return scheme_true
def __repr__(self):
if self is _all_symbols.get(self.name, None):
return "intern('{}')".format(self.name)
else:
return "SchemeSymbol('{}')".format(self.name)
def __str__(self):
return self.name
# The symbols corresponding to each unique symbol name.
_all_symbols = {}
def intern(name):
"""If NAME is a string, the canonical symbol named NAME. If NAME is a
symbol, a canonical symbol with that name."""
if isinstance(name, SchemeSymbol):
if str(name) in _all_symbols:
return _all_symbols[str(name)]
else:
_all_symbols[str(name)] = name
return name
if name in _all_symbols:
return _all_symbols[name]
else:
sym = SchemeSymbol(name)
_all_symbols[name] = sym
return sym
###########
# Strings #
###########
class SchemeStr(SchemeValue, str):
def stringp(self):
return scheme_true
def __repr__(self):
return "scstr({!r})".format(str(self))
_escape = re.compile(r'''"|\.''')
def print_repr(self):
s = str.__repr__(self)
if s[0] == "'":
s = s[1:-1]
s = SchemeStr._escape.sub(lambda x: (r'\"' if x == '"'
else "'" if x == r"\'"
else x), s)
s = '"' + s + '"'
return s
scstr = SchemeStr
##########################
# Pairs and Scheme lists #
##########################
class Pair(SchemeValue):
"""A pair has two instance attributes: first and second. For a Pair to be
a well-formed list, second is either a well-formed list or nil. Some
methods only apply to well-formed lists. As a convenience, the constructor
for Pair converts arguments that are numbers into SchemeNumbers and
arguments that are strings into SchemeStrs (NOT symbols, which the caller
must convert explictly, generally using intern). Likewise, the __repr__
function will abbreviate scnum(x) to x and scstr(x) to x.
>>> s = Pair(1, Pair(2, nil))
>>> s
Pair(1, Pair(2, nil))
>>> print(s)
(1 2)
>>> len(s)
2
>>> s[1]
scnum(2)
>>> print(s.map(lambda x: x+4))
(5 6)
"""
def __init__(self, first, second):
"""The pair (FIRST . SECOND). As a convenience, FIRST and SECOND
are coerced from Python numbers to SchemeNumbers or from Python
strings to SchemeStrs."""
first = scheme_coerce(first)
second = scheme_coerce(second)
assert isinstance(first, SchemeValue) and \
isinstance(second, SchemeValue), \
"bad types to Pair: {}, {}".format(type(first), type(second))
self.first = first
self.second = second
def atomp(self):
return scheme_false
def pairp(self):
return scheme_true
def car(self):
return self.first
def cdr(self):
return self.second
def set_car(self, v):
self.first = v
return okay
def set_cdr(self, v):
self.second = v
return okay
def length(self):
return SchemeInt(self.__len__())
def equalp(self, y):
return scbool(self == y)
def listp(self):
return self._list_end().nullp()
def _list_end(self):
"""The 'end' of the list of which I am the head. This is nil if
I am a normal list. If I am a dot list---e.g. (1 2 . 3)---, then
the atom after the dot. If the list is circular, then the first
repeated pair in the list."""
p0 = self
p1 = self.second
while p1 is not p0 and p1.pairp():
p1 = p1.second
if p1 is p0 or not p1.pairp():
break
p0 = p0.second
return p1
def __repr__(self):
def uncoerce(x):
if scheme_numberp(x):
return x + 0
elif scheme_symbolp(x):
return str(x)
else:
return x
return "Pair({0!r}, {1!r})".format(uncoerce(self.first),
uncoerce(self.second))
def __str__(self):
s = "(" + str(self.first)
second = self.second
while second.pairp():
s += " " + str(second.car())
second = second.cdr()
if not second.nullp():
s += " . " + str(second)
return s + ")"
def __len__(self):
if not self._list_end().nullp():
raise SchemeError("length attempted on improper list")
n, second = 1, self.second
while second.pairp():
n += 1
second = second.second
return n
def __getitem__(self, k):
if k < 0:
raise IndexError("negative index into list")
y = self
for _ in range(k):
if y.second is nil:
raise IndexError("list index out of bounds")
elif not isinstance(y.second, Pair):
raise SchemeError("ill-formed list")
y = y.second
return y.first
def __eq__(self, p):
if not isinstance(p, Pair):
return False
return bool(self.first.equalp(p.first) and self.second.equalp(p.second))
def map(self, fn):
"""Return a Scheme list after mapping Python function FN to SELF."""
mapped = fn(self.first)
if self.second.nullp() or self.second.pairp():
return Pair(mapped, self.second.map(fn))
else:
raise SchemeError("ill-formed list")
def append(self, y):
if not self.listp():
raise SchemeError("attempt to append to improper list")
result = last = Pair(self.first, y)
p = self.second
while p is not nil:
last.second = Pair(p.first, y)
last = last.second
p = p.second
return result
class nil(SchemeValue):
"""The empty list"""
def listp(self):
return scheme_true
def nullp(self):
return scheme_true
def length(self):
return SchemeInt(0)
def __repr__(self):
return "nil"
def __str__(self):
return "()"
def __len__(self):
return 0
def __getitem__(self, k):
if k < 0:
raise IndexError("negative index into list")
raise IndexError("list index out of bounds")
def append(self, y):
return y
def map(self, fn):
return self
nil = nil() # Assignment hides the nil class; there is only one instance
########################
# Primitive Operations #
########################
_PRIMITIVES = []
def primitive(*names):
"""An annotation to record a Python function as a primitive procedure.
NAMES is the set of names to use in the Scheme global environment for
the function."""
def add(fn):
_PRIMITIVES.append((names, fn))
return fn
return add
def get_primitive_bindings():
"""The list of all (names, function) bindings recorded by @primitive
annotations."""
return _PRIMITIVES
@primitive("boolean?")
def scheme_booleanp(x):
"""True iff X is #t or #f."""
return x.booleanp()
@primitive("not")
def scheme_not(x):
"""True iff X is #f."""
return x.notp()
@primitive("eq?")
def scheme_eqp(x, y):
return x.eqp(y)
@primitive("eqv?")
def scheme_eqvp(x, y):
return x.eqvp(y)
@primitive("equal?")
def scheme_equalp(x, y):
return x.equalp(y)
@primitive("pair?")
def scheme_pairp(x):
return x.pairp()
@primitive("null?")
def scheme_nullp(x):
return x.nullp()
@primitive("list?")
def scheme_listp(x):
return x.listp()
@primitive("length")
def scheme_length(x):
return x.length()
@primitive("cons")
def scheme_cons(x, y):
return x.cons(y)
@primitive("car")
def scheme_car(x):
return x.car()
@primitive("cdr")
def scheme_cdr(x):
return x.cdr()
@primitive("list")
def scheme_list(*vals):
result = nil
for i in range(len(vals)-1, -1, -1):
result = scheme_cons(vals[i], result)
return result
@primitive("append")
def scheme_append(*vals):
if len(vals) == 0:
return nil
result = vals[-1]
for i in range(len(vals)-2, -1, -1):
result = vals[i].append(result)
return result
@primitive("string?")
def scheme_stringp(x):
return x.stringp()
@primitive("symbol?")
def scheme_symbolp(x):
return x.symbolp()
@primitive("number?")
def scheme_numberp(x):
return x.numberp()
@primitive("integer?")
def scheme_integerp(x):
return x.integerp()
def _check_nums(*vals):
"""Check that all arguments in VALS are numbers."""
for i, v in enumerate(vals):
if not scheme_numberp(v):
msg = "operand {0} ({1}) is not a number"
raise SchemeError(msg.format(i, v))
def _arith(fn, init, vals):
"""Perform the fn operation on the number values of VALS, with INIT as
the value when VALS is empty. Returns the result as a Scheme value."""
_check_nums(*vals)
s = init
for val in vals:
s = fn(s, val)
if round(s) == s:
return SchemeInt(round(s))
else:
return SchemeFloat(s)
@primitive("+")
def scheme_add(*vals):
return _arith(operator.add, 0, vals)
@primitive("-")
def scheme_sub(val0, *vals):
if len(vals) == 0:
return val0.neg()
return _arith(operator.sub, val0, vals)
@primitive("*")
def scheme_mul(*vals):
return _arith(operator.mul, 1, vals)
@primitive("/")
def scheme_div(*vals):
try:
if len(vals) == 1:
return _arith(operator.truediv, scnum(1), vals)
elif len(vals) == 0:
raise SchemeError("/ takes at least one argument")
return _arith(operator.truediv, vals[0], vals[1:])
except ZeroDivisionError as err:
raise SchemeError(err)
@primitive("quotient")
def scheme_quo(val0, val1):
return val0.quo(val1)
@primitive("modulo")
def scheme_modulo(val0, val1):
return val0.modulo(val1)
@primitive("remainder")
def scheme_rem(val0, val1):
return val0.rem(val1)
@primitive("floor")
def scheme_floor(val):
return val.floor()
@primitive("ceil")
def scheme_ceil(val):
return val.ceil()
@primitive("=")
def scheme_eq(x, y):
return x.eq(y)
@primitive("<")
def scheme_lt(x, y):
return x.ltp(y)
@primitive(">")
def scheme_gt(x, y):
return x.gtp(y)
@primitive("<=")
def scheme_le(x, y):
return x.lep(y)
@primitive(">=")
def scheme_ge(x, y):
return x.gep(y)
@primitive("even?")
def scheme_evenp(x):
return x.evenp()
@primitive("odd?")
def scheme_oddp(x):
return x.oddp()
@primitive("zero?")
def scheme_zerop(x):
return x.zerop()
##
## Other operations
##
# atom? is not standard.
@primitive("atom?")
def scheme_atomp(x):
return x.atomp()
@primitive("display")
def scheme_display(val):
print(str(val), end="")
return okay
@primitive("print")
def scheme_print(val):
print(val.print_repr())
return okay
@primitive("newline")
def scheme_newline():
print()
sys.stdout.flush()
return okay
@primitive("error")
def scheme_error(msg = None):
msg = "" if msg is None else str(msg)
raise SchemeError(msg)
@primitive("exit")
def scheme_exit():
raise EOFError
##
## Turtle graphics (non-standard)
##
_turtle_screen_on = False
def turtle_screen_on():
return _turtle_screen_on
def _tscheme_prep():
global _turtle_screen_on
if not _turtle_screen_on:
_turtle_screen_on = True
turtle.title("Scheme Turtles")
turtle.mode('logo')
@primitive("forward", "fd")
def tscheme_forward(n):
"""Move the turtle forward a distance N units on the current heading."""
_check_nums(n)
_tscheme_prep()
turtle.forward(n)
return okay
@primitive("backward", "back", "bk")
def tscheme_backward(n):
"""Move the turtle backward a distance N units on the current heading,
without changing direction."""
_check_nums(n)
_tscheme_prep()
turtle.backward(n)
return okay
@primitive("left", "lt")
def tscheme_left(n):
"""Rotate the turtle's heading N degrees counterclockwise."""
_check_nums(n)
_tscheme_prep()
turtle.left(n)
return okay
@primitive("right", "rt")
def tscheme_right(n):
"""Rotate the turtle's heading N degrees clockwise."""
_check_nums(n)
_tscheme_prep()
turtle.right(n)
return okay
@primitive("circle")
def tscheme_circle(r, extent = None):
"""Draw a circle with center R units to the left of the turtle (i.e.,
right if N is negative. If EXTENT is not None, then draw EXTENT degrees
of the circle only. Draws in the clockwise direction if R is negative,
and otherwise counterclockwise, leaving the turtle facing along the
arc at its end."""
if extent is None:
_check_nums(r)
else:
_check_nums(r, extent)
_tscheme_prep()
turtle.circle(r, extent and extent)
return okay
@primitive("setposition", "setpos", "goto")
def tscheme_setposition(x, y):
"""Set turtle's position to (X,Y), heading unchanged."""
_check_nums(x, y)
_tscheme_prep()
turtle.setposition(x, y)
return okay
@primitive("setheading", "seth")
def tscheme_setheading(h):
"""Set the turtle's heading H degrees clockwise from north (up)."""
_check_nums(h)
_tscheme_prep()
turtle.setheading(h)
return okay
@primitive("penup", "pu")
def tscheme_penup():
"""Raise the pen, so that the turtle does not draw."""
_tscheme_prep()
turtle.penup()
return okay
@primitive("pendown", "pd")
def tscheme_pendown():
"""Lower the pen, so that the turtle starts drawing."""
_tscheme_prep()
turtle.pendown()
return okay
@primitive("showturtle", "st")
def tscheme_showturtle():
"""Make turtle visible."""
_tscheme_prep()
turtle.showturtle()
return okay
@primitive("hideturtle", "ht")
def tscheme_hideturtle():
"""Make turtle visible."""
_tscheme_prep()
turtle.hideturtle()
return okay
@primitive("clear")
def tscheme_clear():
"""Clear the drawing, leaving the turtle unchanged."""
_tscheme_prep()
turtle.clear()
return okay
@primitive("color")
def tscheme_color(c):
"""Set the color to C, a string such as '"red"' or '"#ffc0c0"' (representing
hexadecimal red, green, and blue values."""
_tscheme_prep()
check_type(c, scheme_stringp, 0, "color")
turtle.color(eval(c))
return okay
@primitive("begin_fill")
def tscheme_begin_fill():
"""Start a sequence of moves that outline a shape to be filled."""
_tscheme_prep()
turtle.begin_fill()
return okay
@primitive("end_fill")
def tscheme_end_fill():
"""Fill in shape drawn since last begin_fill."""
_tscheme_prep()
turtle.end_fill()
return okay
@primitive("exitonclick")
def tscheme_exitonclick():
"""Wait for a click on the turtle window, and then close it."""
global _turtle_screen_on
if _turtle_screen_on:
print("Close or click on turtle window to complete exit")
turtle.exitonclick()
_turtle_screen_on = False
return okay
@primitive("speed")
def tscheme_speed(s):
"""Set the turtle's animation speed as indicated by S (an integer in
0-10, with 0 indicating no animation (lines draw instantly), and 1-10
indicating faster and faster movement."""
check_type(s, scheme_integerp, 0, "speed")
_tscheme_prep()
turtle.speed(s)
return okay