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