← Back to context

Comment by titzer

17 hours ago

> It is also true that loops mess up your stack traces

No, because an indirect tail-call eliminates the calling frame. After, it looks like the caller of that function called a function it did not call. In fact, with tail-calls a whole pile of steps gets telescoped down to nothing[1]. This is not the case with a loop, it doesn't jump to itself indirectly. Loops don't jump into the middle of other loops in other functions. We can still follow the chain of control flow from the start of the main function, through (non-tail) calls, i.e. the stack, through the start of the loop, and then from some backedge in the loop body we're looking at.

Tail-calls are absolutely harder to debug in general than loops.

[1] In the limit, if the entire program was transformed into CPS with tail-calls everywhere, the original execution stack is now strewn throughout the heap as a chain of closures.

I certainly can jump from one loop to the middle of another when writing Python code with Async and/or generators.

> Tail-calls are absolutely harder to debug in general than loops.

I like TCO because I often prefer the expressiveness of recursion over loops. When I need to debug, I turn off TCO. (I'm talking about Common Lisp.)

I agree TCO definitely makes the compiled code not look like the source, and this lossage doesn't happen with either regular recursion or with loops. But various other kinds of optimizations also have that problem.

If you're looking at the assembler output you'll see JMPs where there would have been JSRs without the optimization. So knowing that helps a bit. You just lose the automatic history preservation the stack gave you for free.

Time travel debugging might be an answer here but of course that can be much more expensive than simply keeping JSRs in place.