On May 18, 2021, at 12:33 PM, Guy Steele <guy.ste...@oracle.com<mailto:guy.ste...@oracle.com>> wrote:
If only we had tail calls. Then instead of writing (int q, int r) = quotientAndRemainder(m, n); whatever we could write (using my preferred syntax for a mandatory tail call in Java) goto quotientAndRemainder(m, n, (int q, int r) -> whatever); (the method quotientAndRemainder would of course tail-call its third argument), with no need of tuples or even records to get multiple values out of a method, and then everyone would be happy, right? RIGHT? :-) :-/ :-P As it happens, this week I have been enjoying “Compiling without Continuations” by Maurer at al [1]. Their work is in Haskell which already has tail calls, but needs (for some key optimizations) a primitive which is a non-local jump with arguments. (They mention Rabbit but not Lambda the Ultimate Goto. :-) It basically throws, not an exception but an argument bundle, to a statically (not dynamically) determined catch point; it compiles to a goto, assuming it the special catch and jump points are only introduced after inlining. Using their jump primitive, the Q&R call looks something like this: QR: join(int q, int r) { quotientAndRemainder(m, n, QR); } and AlwaysThrows quotientAndRemainder(int m, int n, BiConsumer<int,int> QR) { int q = m/n, r = m-n*q; throw QR.apply(q, r); } If it could compile to JVM-level LET-JUMP-POINT (“join”) and GOTO-JUMP-POINT (“jump”) IR commands, then you could get the right kind of “ultimate goto” by the route of throws instead of tail calls. Threading the types through exceptions is very icky though. Also, tail calls, proper, allow you to jump to any dynamically specified next-callee, not just a selection of catch points already on the stack. Still, in this example, a statically present catch point is enough. — John [1]: https://www.microsoft.com/en-us/research/wp-content/uploads/2016/11/compiling-without-continuations.pdf