Problem EC (200pts): Tail Recursion

Hint1: The staff will prioritize helping students with required questions.

Hint2: If you get stuck, try to take a look at the debugging guide.

Why current interpreter cannot properly process tail call?

Consider such scheme code:

(define (factor n acc)
  (if (= n 0)
      acc
      (factor (- n 1) (* acc n))))

(factor 10 1)

The current evaluation process is:

     scheme_eval(`(factor 10 1))
===> return scheme_eval(`(factor 9 10))
===> return (return scheme_eval(`(factor 8 90)))
...

The problem is python interpreter does not properly perform tail call.

There're mainly three way to solve this problem:

  • transform the scheme interpreter to an non-recursive interpreter, i.e., eliminate all recursive call to scheme_eval. That can be accomplished by
    • rewrite this interpreter to an CEK interpreter. ('C' means control, 'E' means environment, 'K' means continuation). See here and Chap. 5 of ESSENTIALS OF PROGRAMMING LANGUAGES.
    • rewrite this interpreter to an trampolined interpreter. We use this approach.
  • transform the source scheme code, e.g. Continuation-Passing Style transformation.

The trampoline technique is the simplest one. So we use this approach.

Trampoline

The trampoline technique is a simple technique to perform proper tail call in a "bad" language (e.g. Python, C, C++, Java, ...).

For example, we can define a sum recursive function in python:

def sum(n, acc):
    if n == 0:
        return acc
    else:
        return sum(n - 1, acc + n)

And sum(10000, 0) will produce a StackOverflow error. Now we "trampoline" such function:

class Unevaluated:
    def __init__(self, n, acc):
        self.n = n
        self.acc = acc

def sum_tram(n, acc):
    if n == 0:
        return acc
    else:
        return Unevaluated(n - 1, acc + n)

def sum(n, acc):
    # use do-while here is better, but Python does not support that
    res = sum_tram(n, acc)
    while isinstance(res, Unevaluated):
        res = sum_tram(res.n, res.acc)
    return res

Now sum(10000, 0) will properly works. This can be generalized to transform all recursive functions to loops, see here. But the above form is enough for our problem.

The Tasks

Complete the function optimize_tail_calls in scheme_eval_apply.py. It returns an alternative to scheme_eval that is properly tail recursive. That is, the interpreter will allow an unbounded number of active tail calls in constant space. It has a third argument tail that indicates whether the expression to be evaluated is in a tail context.

The Unevaluated class represents an expression that needs to be evaluated in an environment. When optimized_eval receives a non-atomic expression in a tail context, it returns an Unevaluated instance. Otherwise, it should repeatedly call original_scheme_eval until the result is a value, rather than an Unevaluated.

A successful implementation will require changes to several other functions, including some functions that we provided for you. All expressions throughout your interpreter that are in a tail context should be evaluated by calling scheme_eval with True as the third argument (now called tail). Your goal is to determine which expressions are in a tail context throughout your code and change calls to scheme_eval as needed.

Once you finish, uncomment the following line in scheme_eval_apply.py to use your implementation:

scheme_eval = optimize_tail_calls(scheme_eval)

Use Ok to test your code:

$ python ok -q EC