Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

TCO should be table stakes. An interpreter can do it by noticing that the next thing is a call to itself (the interpreter), a compiler by reorganising the stack frame before the jump.

Lots of stuff gets in the way. Destructors, varying type signatures, calling conventions. But for the case where it can happen, it should be considered an implementation error that it does not. Very like a space leak.

edit: this ^ was evidently unclear - tail call support in an interpreter means recognising when the interpreter is about to call the interpreter and jumping there, e.g. in an eval-apply setup, you use one frame for the eval-apply-eval-apply skeleton and only spawn more for evaluation of function arguments.

Tail call support in an compiler involves changing the calling convention to clean up before jump. At that point it's all jumps, whether back to the top of a loop or to some other basic block, because all the world is SSA to a pretty good approximation.

Neither of those makes any meaningful distinction between calls-to-self, calls-to-sibling, calls-to-indirect and so forth. The TCO is easier on self calls idea comes from languages where it is hacked before the compiler backend in a setting where goto isn't allowed to cross function boundaries. Some C code will have a label at the top named "self" or similar and use `goto self;` to force the equivalent of a tail call, some compiler stacks implement TCO by a mechanical version of the same hack and thus can't deal with it crossing between different functions.

edit2: If you're compiling to a target with restrictions on calling convention that preclude cleanup before jump (and doesn't sort this out itself) you are out of luck. Pick a better target or live in the doomed world you've created. See e.g. https://v8.dev/blog/wasm-tail-call for some stuff on wasm trying to fix itself, I haven't kept up to date with whether they did or not.



TCO is about more than self-recursive calls -- you should be able to handle mutual recursion:

    f = x => x > 0 ? g(x - 1) : 0;
    g = x => x > 0 ? f(x - 1) : 0;
(These functions don't do anything other than stress-test the stack, then return 0.)

Note that if I changed this to, say, 1 + g(x - 1), this would no longer fall under the purview of tail recursion, where the last instruction before the return is a recursive call.

The usual workaround I learned in my SICP days was to add a second variable for accumulating any results. And, indeed, that's how the blog post rewrites the recursive function.

(The recursive one-liner is still wildly inefficient because JS doesn't implement arrays as linked lists, so the spread operator results in a copy for each recursive call, resulting in a quadratic solution. It could be made more efficient by using Array.prototype.push, but then it's no longer a pure function.)


I don’t think they picked the best example to showcase this, most JavaScript developers would write this simply as:

    Array.from({ length: n }, (_, i) => i + 1)
But this example does get the point across, so I think it is fine. I do agree though, while reading it the inefficientness was screaming at me.


TCO is broader than tail calls to the same function, but tail calls to the same function would certainly be a start.


Implicit tail-call elimination outside the context of tail-recursive algorithms causes behavior that is surprising to programmers working in languages where stack traces are used for debugging (i.e. it removes frames from the stack and obscures the path of execution).




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: