Search code examples
ecmascript-6language-lawyertail-call-optimizationtail-call

Do bound functions support proper tail calls in ES6?


In the ECMAScript 2015 Language Specification, the definitions of Function.prototype.apply and Function.prototype.call both include "Perform PrepareForTailCall()" as one of their steps, so we know that these functions support proper tail calls (i.e., tail call optimisation).

The definition of [[Call]] on bound function objects, however, omits PrepareForTailCall(). Does this mean that bound functions do not support proper tail calls, and that a bound function calling itself recursively could blow up the stack?


Solution

  • The definition of [[Call]] on bound function objects omits PrepareForTailCall(). Does this mean that bound functions do not support proper tail calls, and that a bound function calling itself recursively could blow up the stack?

    No. The PrepareForTailCall happens in EvaluateDirectCall during the evaluation of the call expression, where it checks whether that expression is in a tail position. When the tail call is prepared, the current running execution context is dropped, before the function is called, dispatching on the respective [[Call]] internal method. The new running execution context is set up in PrepareForOrdinaryCall from the [[Call]] method of user-defined functions. The [[Call]] method of bound functions just introduces an extra level of indirection before that happens.

    In the ECMAScript 2015 Language Specification, the definitions of Function.prototype.apply and Function.prototype.call both include "Perform PrepareForTailCall()" as one of their steps, so we know that these functions support proper tail calls.

    Yes, this is necessary because the [[Call]] method of built-in functions sets up a new running execution context (for the "implementation-defined steps"). It is this "builtin context" that the PrepareForTailCall will drop before the actual function is called.

    The call and apply methods are functions calling functions, and when they are called there are two calls on the stack that need to be tail-call-optimised. (Contrast this to - for example - Array.prototype.map, which also calls other functions, but here the map execution context stays on the call stack. In call and apply, the Call() is indeed in a tail position of the algorithm).