Show HN: Tail Recursion Optimization for the JVM
github.com
github.com
[1] Using the word 'optimization' suggests that eliminating tail calls is optional, but for it to be really useful it has to be guaranteed. If you're writing in recursive style, your program becomes instantly incorrect if TCE goes away because many acceptable inputs will start causing stack overflows.
[2] See this answer: https://softwareengineering.stackexchange.com/a/272086
That's... lame. BTW, tail code elimination could keep a count of tail calls for this. It wouldn't work so well for mutual recursion, but maybe that would suffice. In any case, nothing should be so dependent on stack traces!
I thought stack inspection was a bad idea anyway, but you could implement it together with TCO.
[edited because truncated by misclick]
At first it was - why such requirement? Then it hit me - this is not bad - it's actually enforcing that either you would succeed doing that, and the compiler would also do it, or not. So there is a strong guarantee there.
It’s a weird but helpful annotation. For example, Scala won’t optimize methods that can be overridden (non-final and public/protected), which is easy to forget. So the annotation is a nice check/confirmation that the compiler is doing what you expect.
[1]. https://clojure.org/about/functional_programming#_recursive_...
This optimization messes with the stack trace, so if you throw an exception in one of the recursively called methods, it will only show up only once in the stack trace. This may very well cause confusion in cases where the developer is not aware that such optimizations have been performed.
However, you can still perform the optimization on your Java bytecode classes, and then pass them to Graal. (If I understand Graal correctly.)
http://cesquivias.github.io/blog/2015/01/15/writing-a-langua...
The point is that when your program throws an exception, and prints the stacktrace, you will only see a single stack frame for your method. Even if it recursively descended into itself multiple times. This is counter-intuitive as you don't see the evidence of the recursive calls in the stack trace. If you're unaware that tail recursion optimization was performed, you won't know why it seems like your method was only called once.
The program flow stays the same, only the diagnostic information of the program changes.
The whole idea with tail call elimination is that a stack trace shouldn't be able to see the frames for the tail calls, because the tail call reuses the frame. To add support for tracking the tail call stack frames, you would need to modify the compiler to output code that specifically keeps track of "elided tail call frames", and you would need to update the stack trace traversal code to be able to recognize the extra debug information.
Sure, it's something you could build into a new toolchain, but adding something like this to the JVM would be harder due to the constraints already placed on the JVM. Furthermore I don't know of such support in any toolchains, not even for Lua or Scheme, both of which guarantee tail call optimization. (If anybody has an example, please share!)
> There are some non-standard ECMAScript features in JavaScript that work differently in the presence of PTC.
Java has a strict spec, and the relevant methods which would break aren’t non-standard like they are in JavaScript.
If you’re willing to change the spec (I think Loom does) then yeah, but you can’t implement it as an ‘optimisation’ until then, because it’s not an optimisation if it changes behaviour.
$ cat tailrec.lua
function f(g,n)
if n == 0 then
error("oh no")
end
return g(g,n-1)
end
f(f,5)
$ lua tailrec.lua
lua: tailrec.lua:3: oh no
stack traceback:
[C]: in function 'error'
tailrec.lua:3: in function 'f'
(...tail calls...)
tailrec.lua:7: in main chunk
[C]: in ?So we’d need to reconstruct some information about the activation. How are you proposing we do that? From what information?
Are you suggesting we could store some metadata perhaps? Can you suggest a mechanism for the metadata we should use? There’s a tricky constraint before you suggest an idea! It has to use constant storage, because making storage not grow with the number of calls is the whole point in the first place.
JSC's call stack is the C stack, and this stack has real TCO. There is a separate, heap-allocated "shadow" stack which records calls (but not returns). A single recycled C-stack frame may correspond to multiple shadow-stack calls, but between the two, a debugger can recover many tail calls.
It's best effort. The GC may collect frames if it chooses to. Highly optimized code won't touch the shadow stack. etc. But it's good enough for a debugger.
It's named "Shadow Chicken" because it's a shadow stack inspired by CHICKEN scheme, which uses the legendary Cheney-on-the-MTA strategy.