8 comments

  • fsckboy1 day ago
    the &quot;lambda the ultimate&quot; papers and the birth of scheme was a loong time ago, so it grates on my ears to hear this topic presented as &quot;an optimization&quot;. Yes, it is sometimes an optimization a compiler can make, but the idea is much better presented as a useful semantic of a language.<p>in the same way that passing parameters to a subfunction &quot;creates&quot; a special set of local variables for the subfunction, the tail recursion semantic updates this set of local variables in an especially clean way for loop semantics, allowing &quot;simultaneous assignment&quot; from old values to new ones.<p>(yes, it would be confusing with side effected C&#x2F;C++ operators like ++ because then you&#x27;d need to know order of evaluation or know not to do that, but those are already issues in those languages quite apart from tail recursion)<p>because it&#x27;s the way I learned it, I tend to call the semantic &quot;tail recursion&quot; and the optimization &quot;tail call elimination&quot;, but since other people don&#x27;t do the same it&#x27;s somewhat pointless; but I do like to crusade for awareness of the semantic beyond the optimization. If it&#x27;s an optimization, you can&#x27;t rely on it because you could blow the stack on large loops. If it&#x27;s a semantic, you can rely on it.<p>(the semantic is not entirely &quot;clean&quot; either. it&#x27;s a bit of a subtle point that you need to return straightaway the return value of the tail call or it&#x27;s not a tail call. fibonacci is the sum of the current with the next so it&#x27;s not a tail call unless you somewhat carefully arrange the values you pass&#x2F;keep around. also worth pointing out that all &quot;tail calls&quot; are up for consideration, not just recursive ones)
    • ekimekim1 day ago
      In a weird way it kinda reminds me of `exec` in sh (which replaces the current process instead of creating a child process). Practically, there&#x27;s little difference between these two scripts:<p><pre><code> #!&#x2F;bin&#x2F;sh foo bar </code></pre> vs<p><pre><code> #!&#x2F;bin&#x2F;sh foo exec bar </code></pre> And you could perhaps imagine a shell that does &quot;tail process elimination&quot; to automatically perform the latter when you write the former.<p>But the distinction <i>can be</i> important due to a variety of side effects and if you could only achieve it through carefully following a pattern that the shell might or might not recognize, that would be very limiting.
      • nagaiaida1 day ago
        this is pretty much exactly how my &quot;forth&quot; handles tail call elimination, and it&#x27;s the main thing that&#x27;s added the quotes so far since it shifts the mental burden to being aware of this when writing code to manipulate the return stack.<p>as you imply towards the end, i&#x27;m not confident this is a trick you can get away with as easily without the constraints of concatenative programming to railroad you into it being an easily recognizable pattern for both the human and the interpreter.
    • LeFantome1 day ago
      One of the issues with Java is that it is two levels of language. You compile Java into Java Byte code which is further compiled into native machine code. There is no concept of tail call recursion in Java Byte code. So, it is difficult to propagate the semantics. So it really has to be a programmer or compiler optimization to implement the tail call optimization into the generated intermediate bytecode before that is further compiled.<p>.NET is an interesting contrast. The equivalent of Java Bytecode in .NET (CIL) does have the concept of tail calls. This allows a functional language like F# to be compiled to the intermediate form without losing the tail call concept. It is still up to the first level compiler though. C# for example does not support tail calls even though it’s intermediate target (CIL) does.
    • ghoul21 day ago
      Sigh. I have been kicking this horse forever as well: an &quot;optimization&quot; implies just a performance improvement.<p>Tail call elimination, if it exists in a language, allows coding certain (even infinite) loops as recursion - making loop data flow explicit, easier to analyze, and at least in theory, easier to vectorize&#x2F;parallelize, etc<p>But if a language&#x2F;runtime doesn&#x27;t do tail call elimination, then you CAN&#x27;T code up loops as recursion, as you would be destroying you stack. So the WAY you code, structure it, must be different.<p>Its NOT an optimization.<p>I have no idea who even came up with that expression.
    • ameliaquining1 day ago
      I mean, in the particular case demonstrated in this blog post it can only be an optimization, because semantically guaranteeing it would require language features that Java doesn&#x27;t have.
  • bradley132 days ago
    Every compiler should recognize and optimize for tail recursion. It&#x27;s not any harder than most other optimizations, and some algorithms are far better expressed recursively.<p>Why is this not done?
    • SkiFire132 days ago
      In general, tail recursion destroys stacktrace information, e.g. if f calls g which tail calls h, and h crashes, you won&#x27;t see g in the stacktrace, and this is bad for debuggability.<p>In lower level languages there are also a bunch of other issues:<p>- RAII can easily make functions that appear in a tail position not actually tail calls, due to destructors implicitly running after the call;<p>- there can be issues when reusing the stack frame of the caller, especially with caller-cleanup calling conventions;<p>- the compiler needs to prove that no pointers to the stack frame of the function being optimized have escaped, otherwise it would be reusing the memory of live variables which is illegal.
      • chowells1 day ago
        I&#x27;ll believe destroying stacktrace information is a valid complaint when people start complaining that for loops destroy the entire history of previous values the loop variables have had. Tail recursion is equivalent to looping. People should stop complaining when it gives them the same information as looping.
        • roenxi1 day ago
          &gt; I&#x27;ll believe destroying stacktrace information is a valid complaint when people start complaining that for loops destroy the entire history of previous values the loop variables have had.<p>That is a common criticism. You&#x27;re referring to the functional programmers. They would typically argue that building up state based on transient loop variables is a mistake. The body of a loop ideally should be (at the time any stack trace gets thrown) a pure function of constant values and a range that is being iterated over while being preserved. That makes debugging easier.
        • ameliaquining1 day ago
          I mean, if I were doing an ordinary non-recursive function call that just happened to be in tail position, and it got eliminated, and this caused me to not be able to get the full stack trace while debugging, I might be annoyed.<p>In a couple languages I&#x27;ve seen proposals to solve this problem with a syntactic opt-in for tail call elimination, though I&#x27;m not sure whether any mainstream language has actually implemented this.
          • chowells1 day ago
            Language designers could keep taking ideas from Haskell, and allow functions to opt in to appearing in stack traces. Give the programmer control, and all.
          • SamLL1 day ago
            Kotlin has a syntactic opt-in for tail call elimination (the &quot;tailrec&quot; modifier).
          • michaelmrose1 day ago
            <a href="https:&#x2F;&#x2F;clojuredocs.org&#x2F;clojure.core&#x2F;recur" rel="nofollow">https:&#x2F;&#x2F;clojuredocs.org&#x2F;clojure.core&#x2F;recur</a>
      • vbezhenar1 day ago
        Some of the issues partially alleviated by using limited part of tail recursion optimization. You mark some function with tailrec keyword, and compiler verifies that this function calls itself as the last statement. You also wouldn&#x27;t expect complete stack trace from that function. At the same time it probably helps with 90% of recursive algorithms which would benefit from the tail recursion.
        • LeFantome1 day ago
          That is what Clojure does I believe.
      • hyperbrainer2 days ago
        AFAIK Zig is the only somewhat-big and known low-level language with TCO. Obviously, Haskell&#x2F;Ocaml and the like support and it are decently fast too, but system programming languages they are not.
        • vlovich1232 days ago
          For guarantee:<p><a href="https:&#x2F;&#x2F;crates.io&#x2F;crates&#x2F;tiny_tco" rel="nofollow">https:&#x2F;&#x2F;crates.io&#x2F;crates&#x2F;tiny_tco</a><p><a href="https:&#x2F;&#x2F;crates.io&#x2F;crates&#x2F;tco" rel="nofollow">https:&#x2F;&#x2F;crates.io&#x2F;crates&#x2F;tco</a><p>As an optimization my understanding is that GCC and LLVM implement it so Rust, C, and C++ also have it implicitly as optimizations that may or may not apply to your code.<p>But yes, zig does have a formal language syntax for guaranteeing tail calls to happen at the language level (which I agree with as the right way to expose this optimization).
        • SkiFire132 days ago
          Zig&#x27;s tco support is not much different than Clang&#x27;s `[[clang::musttail]]` in C++. Both have the big restriction that the two functions involved are required to have the same signature.
          • hyperbrainer1 day ago
            &gt; Both have the big restriction that the two functions involved are required to have the same signature.<p>I did not know that! But I am a bit confused, since I don&#x27;t really program in either language. Where exactly in the documentation could I read more about this? Or see more examples?<p>The language reference for @call[0] was quite unhelpful for my untrained eye.<p>[0] <a href="https:&#x2F;&#x2F;ziglang.org&#x2F;documentation&#x2F;master&#x2F;#call" rel="nofollow">https:&#x2F;&#x2F;ziglang.org&#x2F;documentation&#x2F;master&#x2F;#call</a>
            • SkiFire131 day ago
              Generally I also find Zig&#x27;s documentation pretty lacking, instead I try looking for the relevant issues&#x2F;prs. In this case I found comments on this issues [1] which seem to still hold true. That same issue also links to the relevant LLVM&#x2F;Clang issue [2], and the same restriction is also being proposed for Rust [3]. This is were I first learned about it and prompted me to investigate whether Zig also suffers from the same issue.<p>[1]: <a href="https:&#x2F;&#x2F;github.com&#x2F;ziglang&#x2F;zig&#x2F;issues&#x2F;694#issuecomment-1567447672" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;ziglang&#x2F;zig&#x2F;issues&#x2F;694#issuecomment-15674...</a> [2]: <a href="https:&#x2F;&#x2F;github.com&#x2F;llvm&#x2F;llvm-project&#x2F;issues&#x2F;54964" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;llvm&#x2F;llvm-project&#x2F;issues&#x2F;54964</a> [3]: <a href="https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;pull&#x2F;3407" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rfcs&#x2F;pull&#x2F;3407</a>
            • ufo1 day ago
              This limitation is to ensure that the two functions use the exact same calling convention (input &amp; output registers, and values passed via stack). It can depend on the particular architecture.
        • Thorrez2 days ago
          C++:<p>&gt; All current mainstream compilers perform tail call optimisation fairly well (and have done for more than a decade)<p><a href="https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;34125&#x2F;which-if-any-c-compilers-do-tail-recursion-optimization" rel="nofollow">https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;34125&#x2F;which-if-any-c-com...</a> (2008)
          • hyperbrainer1 day ago
            I couldn&#x27;t actually figure out whether this TCO being done &quot;fairly well&quot; was a guarantee or simply like Rust (I am referring to the native support of the language, not what crates allow)
            • Thorrez3 hours ago
              When that SO answer was written, it was not a guarantee.<p>You can now get a guarantee by using non-standard compiler attributes:<p><a href="https:&#x2F;&#x2F;clang.llvm.org&#x2F;docs&#x2F;AttributeReference.html#musttail" rel="nofollow">https:&#x2F;&#x2F;clang.llvm.org&#x2F;docs&#x2F;AttributeReference.html#musttail</a><p><a href="https:&#x2F;&#x2F;gcc.gnu.org&#x2F;onlinedocs&#x2F;gcc&#x2F;Statement-Attributes.html#index-musttail-statement-attribute" rel="nofollow">https:&#x2F;&#x2F;gcc.gnu.org&#x2F;onlinedocs&#x2F;gcc&#x2F;Statement-Attributes.html...</a>
        • johnisgood1 day ago
          Depends on what you mean by &quot;systems programming&quot;, you can definitely do that in OCaml.
        • pjmlp1 day ago
          &quot;Unix system programming in OCaml&quot;<p><a href="https:&#x2F;&#x2F;ocaml.github.io&#x2F;ocamlunix&#x2F;ocamlunix.html" rel="nofollow">https:&#x2F;&#x2F;ocaml.github.io&#x2F;ocamlunix&#x2F;ocamlunix.html</a><p>MirageOS<p><a href="https:&#x2F;&#x2F;mirage.io&#x2F;" rel="nofollow">https:&#x2F;&#x2F;mirage.io&#x2F;</a><p>House OS,<p><a href="https:&#x2F;&#x2F;programatica.cs.pdx.edu&#x2F;House&#x2F;" rel="nofollow">https:&#x2F;&#x2F;programatica.cs.pdx.edu&#x2F;House&#x2F;</a><p>Just saying.
          • hyperbrainer1 day ago
            I know of these. Almost added a disclaimer too -- that was not my point, as I am sure, you understand. Also Ocaml has a GC, unsuitable for many applications common to systems programming.
    • vlovich1232 days ago
      My bigger issue with tail call optimization is that you really want it to be enforceable since if you accidentally deoptimize it for some reason then you can end up blowing up your stack at runtime. Usually failure to optimize some pattern doesn’t have such a drastic effect - normally code just runs more slowly. So tail call is one of those special optimizations you want a language annotation for so that if it fails you get a compiler error (and similarly you may want it applied even in debug builds).
    • _old_dude_2 days ago
      Parroting something i have heard at a Java conference several years ago, tail recursion remove stack frames but the security model is based on stack frames, so it has to be a JVM optimization, not a compiler optimization.<p>I&#x27;ve no idea if this fact still holds when the security manager will be removed.
      • smarks2 days ago
        The security manager was removed (well, “permanently disabled”) in Java 24. As you note, the permissions available at any given point can depend on the permissions of the code on the stack, and TCO affects this. Removal of the SM thus removes one impediment to TCO.<p>However, there are other things still in the platform for which stack frames are significant. These are referred to as “caller sensitive” methods. An example is Class.forName(). This looks up the given name in the classloader of the class that contains the calling code. If the stack frames were shifted around by TCO, this might cause Class.forName() to use the wrong classloader.<p>No doubt there are ways to overcome this — the JVM does inlining after all — but there’s work to be done and problems to be solved.
        • thfuran1 day ago
          Is there? As you say, there&#x27;s already inlining, and I don&#x27;t see how tco presents a harder case for that.
          • smarks19 hours ago
            There are similarities in the problems, but there are also fundamental differences. With inlining, the JVM can always decide to deoptimize and back out the inlining without affecting the correctness of the result. But it can&#x27;t do that with tail calls without exposting the program to a risk of StackOverflowError.<p>We&#x27;ve been using TCO here (&quot;tail call optimization&quot;) but I recall Guy Steele advocating for calling this feature TCE (&quot;elimination&quot;) because programs can rely on TCE for correctness.
    • javier21 day ago
      In theory, if all you do is implement algorithms, this sounds fine. But most apps implement horrible business processes, so what would one do with missing stacktraces? Maybe in languages that can mark functions as pure.
  • cempaka2 days ago
    Very nice article demonstrating a neat use of ASM bytecode. The Java language devs are also working on Project Babylon (code reflection), which will bring additional techniques to manipulate the output from the Java compiler: <a href="https:&#x2F;&#x2F;openjdk.org&#x2F;projects&#x2F;babylon&#x2F;articles&#x2F;code-models" rel="nofollow">https:&#x2F;&#x2F;openjdk.org&#x2F;projects&#x2F;babylon&#x2F;articles&#x2F;code-models</a>
    • gavinray2 days ago
      This was delivered in JDK 24 as the &quot;Class-File API&quot;<p><a href="https:&#x2F;&#x2F;openjdk.org&#x2F;jeps&#x2F;484" rel="nofollow">https:&#x2F;&#x2F;openjdk.org&#x2F;jeps&#x2F;484</a>
      • algo_trader1 day ago
        Can this improve&#x2F;replace AspectJ and similar instrumentations? We do lots of instruction level modifications
  • 19328122672 days ago
    Scala has been using this technique for years with its scala.annotation.tailrec annotation. Regardless, it&#x27;s cool to see this implemented as a bytecode pass.
    • gavinray2 days ago
      Kotlin as well, with the &quot;tailrec&quot; keyword, e.g. &quot;tailrec fun fibonacci()&quot;<p><a href="https:&#x2F;&#x2F;kotlinlang.org&#x2F;docs&#x2F;functions.html#tail-recursive-functions" rel="nofollow">https:&#x2F;&#x2F;kotlinlang.org&#x2F;docs&#x2F;functions.html#tail-recursive-fu...</a><p>Kotlin also has a neat other tool, &quot;DeepRecursiveFunction&lt;T, R&gt;&quot; that allows defining deep recursion that is not necessarily tail-recursive.<p>Really useful if you wind up a problem that is most cleanly solved with mutual recursion or similar:<p><a href="https:&#x2F;&#x2F;kotlinlang.org&#x2F;api&#x2F;core&#x2F;kotlin-stdlib&#x2F;kotlin&#x2F;-deep-recursive-function&#x2F;" rel="nofollow">https:&#x2F;&#x2F;kotlinlang.org&#x2F;api&#x2F;core&#x2F;kotlin-stdlib&#x2F;kotlin&#x2F;-deep-r...</a>
      • deepsun2 days ago
        Interesting, does it depend on Kotlin compiler or it can be implemented in Java as well?
        • gavinray1 day ago
          The &quot;DeepRecursiveFunction&lt;T,R&gt;&quot; could be implemented in Java. The Kotlin implementation leverages Kotlin&#x27;s native coroutines and uses continuations.<p>It&#x27;d require a bit of engineering to get something working in native Java I&#x27;d imagine, even with the new JDK Structured Concurrency API offering you a coroutines alternative.<p>On the other hand, &quot;tailrec&quot; is a keyword and implemented as a compiler optimization.<p>The closest I&#x27;ve seen in Java is a neat IntelliJ plugin that has a transformation to convert recursive method calls into imperative loops with a stack frame.<p>This transformation and resulting tool was the result of someone&#x27;s thesis, it&#x27;s pretty cool:<p><a href="https:&#x2F;&#x2F;github.com&#x2F;andreisilviudragnea&#x2F;remove-recursion-inspection" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;andreisilviudragnea&#x2F;remove-recursion-insp...</a>
  • ncruces1 day ago
    It&#x27;s been a long time since I&#x27;ve messed with Java bytecode [1], but shouldn&#x27;t the private method call use INVOKESPECIAL?<p>In general I don&#x27;t think you can do this to INVOKEVIRTUAL (or INVOKEINTERFACE) as it covers cases where your target is not statically resolved (virtual&#x2F;interface calls). This transformation should be limited to INVOKESTATIC and INVOKESPECIAL.<p>You also need lots more checks to make sure you can apply the transformations, like ensure the call site is not covered by a try block, otherwise this is not semantics preserving.<p>1: <a href="https:&#x2F;&#x2F;jauvm.blogspot.com&#x2F;" rel="nofollow">https:&#x2F;&#x2F;jauvm.blogspot.com&#x2F;</a>
  • lukaslalinsky1 day ago
    I never understood the need for tail recursion optimization in imperative languages. Sure, you need it in FP if you don&#x27;t have loops and recursion is you only option, but what is the benefit of recursive algorithms, that could benefit from tail optimization (i.e recursive loops), in a language like Java?
  • droideqa2 days ago
    Cool, now ABCL can have TCO!
    • 19328122672 days ago
      This isn&#x27;t a _general_ tail call optimization--just tail recursion. The issue is that this won&#x27;t support mutual tail recursion.<p>e.g.:<p>(defun func-a (x) (func-b (- x 34)) (defun func-b (x) (cond ((&lt;= 0 x) x) (&#x27;t (func-a (-x 3))))<p>Because func-a and func-b are different (JVM) functions, you&#x27;d need an inter-procedural goto (i.e. a tail call) in order to natively implement this.<p>As an alternative, some implementations will use a trampoline. func-a and func-b return a _value_ which says what function to call (and what arguments) for the next step of the computation. The trampoline then calls the appropriate function. Because func-a and func-b _return_ instead of actually calling their sibling, the stack depth is always constant, and the trampoline takes care of the dispatch.
      • knome1 day ago
        Sounds like a manual form of clojures recur function.<p><a href="https:&#x2F;&#x2F;clojuredocs.org&#x2F;clojure.core&#x2F;recur" rel="nofollow">https:&#x2F;&#x2F;clojuredocs.org&#x2F;clojure.core&#x2F;recur</a>
        • 19328122671 day ago
          Clojure&#x27;s loop&#x2F;recur is specifically tail recursion like scala&#x27;s tailrec or the optimization described in the blogpost. It doesn&#x27;t use trampolines to enable tail calls that aren&#x27;t tail recursion.
    • dapperdrake2 days ago
      Finally.<p>The ANTLR guys went through terrible contortions for their parsers.<p>Never felt like working those details out for ABCL.
  • curtisszmania2 days ago
    [dead]