Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Programmatic non-tail recursion elimination

I'm making a toy Lisp interpreter in JavaScript. JS has no tail recursion elimination (TRE), so I implemented TRE using while loop in JS (pseudocode):

function eval (exp, env)
  while true
    if exp is self evaluating
      return exp
    else if ...
      ...
    else if exp is a function call
      procedure = eval(car(exp), env)
      arguments = eval_operands(cdr(exp), env)
      exp = procedure.body
      env = extend_env(procedure.env, env)
      continue # tail call 

So I am happy, and tail-recursive functions like the following one do not run out of stack:

(define +
  (lambda (n m)
    (cond ((zero? n) m)
          (else (+ (sub1 n) (add1 m))))))

(+ 10000 1) ;=> 10001

However, functions that are not tail-recursive, run out of JS stack (because JS code recurs too much on eval_operands):

(define +
  (lambda (n m)
    (cond ((zero? n) m)
          (else (add1 (+ (sub1 n) m)))))  ; not proper tail-recursive

(+ 10000 1) ;=> JS: Maximum call stack size exceeded

How do I deal with non-tail-recursive functions? What are the options? What are good resources? I have read a bit about trampolining, stack externalising and continuation-passing style, but all I could find is how to write code in those styles, not how to use those techniques for writing interpreter.

like image 398
Vladimir Keleshev Avatar asked Jan 31 '13 19:01

Vladimir Keleshev


People also ask

What is non-tail recursion?

Non-Tail / Head Recursion A function is called the non-tail or head recursive if a function makes a recursive call itself, the recursive call will be the first statement in the function. It means there should be no statement or operation is called before the recursive calls.

Is tail recursion faster than non-tail?

As a rule of thumb; tail-recursive functions are faster if they don't need to reverse the result before returning it. That's because that requires another iteration over the whole list. Tail-recursive functions are usually faster at reducing lists, like our first example.

Why should we try to eliminate tail recursion?

Since each function call consumes both additional space and additional time, the removal of tail recursion by the optimizer increases the performance of a function significantly — and it eliminates the possibility that the recursive function could overflow memory as it tries to remember variable values in each ...

Are all recursive functions tail-recursive?

No, it is not possible to express all recursion as tail recursion unless you do supplement the tail recursion with other control flow mechanisms.


1 Answers

You can always turn calls into jumps if you are able to hold call frame information somewhere else. That is what "stack externalising" refers to.

For your interpreter, your call frame data needs to hold the non-tail call's continuation (which may itself hold further references, such as to any variables it needs access to). You will need one call frame per active non-tail call.

All this does, of course, is trade stack space for heap space. In the end, you don't really save any memory this way. :-)

like image 198
Chris Jester-Young Avatar answered Oct 19 '22 09:10

Chris Jester-Young