Join the discussion

Write your take first — we'll ask for email only when you're ready to publish.

  • Hacker News
  • > In 2001 Mark Probst implemented tail-call optimization in GCC

    MSVC didn't add tail-call optimisation until sometime in the 2010s, IIRC.

    I distinctly remember sending a tail-recursive C++ program to someone who developed on Windows, and it crashing, in the late mid-to-late 2000s.

  • MSVC stands for MicroSoft Visual C++ compiler AFAIK.

    It famously doesn’t support a few features of C99.

    They don’t really seem to care much about regular C support (non-C++).

  • >That quote is the article, and it's a little surprising that it's buried so far into the content

    Is it really surprising in 2026? Today's online writing style is not primarily designed to communicate. It's designed to keep the reader 'engaged' for as long as possible. The reader's time is a resource to be extracted.

    I'm absolutely not poking this author individually. It's the writing style of the net.

  • I was wondering what this was in reference to; it's not in reference to TFA here. It's a quote from https://bytecode.news/posts/2026/08/because-it-s-not-fun-eno..., so presumably you meant to post this over at https://news.ycombinator.com/item?id=49242245.
  • > In 2001 Mark Probst implemented tail-call optimization in GCC with a separate calling convention; he lists the limitations of the then-existing tail-call optimization in GCC in section 6.4, among them: "It cannot handle indirect calls" (which would have been used in tail calls for interpreter dispatch).

    Relatively recent being a quarter of century? Or at least a fifth of a century for indirect calls[1] (GCC 3.4.6 is the earliest I see on Compiler Explorer, released March 2006).

    [1]: https://godbolt.org/z/vvcnn54oM

  • Given that GCC was first released in 1987, that would mean that tail call optimization, including of indirect calls, has been around for more than half of GCC's lifetime. So it's indeed fair for the parent article to say that "[GCC has] had tail-call optimizations for most of [its] existence".
  • For people who passed their 30s, everything that happened after their 20th birthday is recent. For me, September 11 is recent memory, as well as the 2008 great recession.
  • GCC has had TCO since the 1980s I'm pretty sure. Since then it's been extended to work in more contexts.
  • I recently played around with what I call "manual tail-call optimization": transform a tail call to a goto to the beginning of the function. Check it out: https://godbolt.org/z/3fY1v1oeW

      int factorial_loop_iterative(int n, int a){
        while(n > 0){
          a = a * n;
          n = n - 1;
        }
        return a;
      }
      
      int factorial_loop_recursive(int n, int a){
        if(n > 0){
          return factorial_loop_recursive(n - 1, a * n);
        }else{
          return a;
        }
      }
      
      int factorial_loop_manual(int n, int a){
      tailcall:
        if(n > 0){
          a = a * n;
          n = n - 1;
          goto tailcall;
        }else{
          return a;
        }
      }
      
      int (*factorial_loop)(int n, int a) = factorial_loop_manual;
      
      int factorial(int n){
        return factorial_loop(n, 0);
      }
    
    I recommend against, of course! Incorrectly sequencing the manual version results in bugs (swap the assignment for n and a), which the recursive version doesn't need to care about.
  • Seems like a complex way to write a normal looped version. Apart from factorial_loop_manual() being one in design, its name even says as much.
  • TFA assumes pre-C89 C, I think:

    > The caller could see the declaration int f();, the actual call could have n>0 arguments, and the actual function could have m≤n parameters.

    Certainly if `f()` were `int f(void);` then that wouldn't be the case. But even for `int f();` C17 6.5.2.2p6 says that "If the number of arguments does not equal the number of parameters, the behavior is undefined." Near as I can tell that was made UB in C89. So TFA is a) right about K&R C, b) just wrong for pretty much all post-K&R C. C23 makes `int f();` be the same as `int f(void);`.

    That calling a non-variadic function with more / fewer arguments than expected by its definition is UB is enough to make TCO possible for that function's body.

    The point about K&R C is well taken though: to turn a tail call into a jump, the caller needs to know how much to pop off the stack.

    For variadic if you `va_start()`, `va_arg()` as needed, then `va_end()` with no `va_copy()` left alive then you can still tail-call out correctly, otherwise you can't.

    For non-variadic functions post K&R C TCO should always be possible and not UB, provided you're not triggering UB to begin with by using the incorrect number of arguments.

  • What practical patterns are enabled by TCO in C? My impression is that every tail call can written as a loop much more naturally. Tail calls are important in functional languages where you don't have mutable loop variables.

    And imo they are an ugly hack even there - one of the few core constructs where its readily apparent you're not programming an abstract machine but a real, and limited computer. For example the most natural way to write factorial:

          let rec factorial n = if n <= 1 then 1 else n * factorial (n - 1)
    
    is not tail recursive, and will overflow if the compiler fails to optimize.
  • It is simple to convert factorial to tail recursive form. In lua, which has tco:

        local factorial do
          local function impl(n, acc)
            if n == 1 then
              return acc
            else
              return impl(n - 1, acc * n)
            end
          end
          factorial = function(n)
            if n < 0 then
              error("factorial input is negative")
            elseif n <= 1 then
              return 1
            else
              return impl(n - 1, n)
            end
          end
        end
    
    You could replace impl with an imperative loop:

        local acc = 1
        repeat
          acc = acc * n
          n = n - 1
        until n == 1
        return acc
    
    Personally, I find this ugly compared to the tail recursive solution. The loop version only seems more natural if you primarily think in loops. Tail recursion is strictly more powerful than looping since every imperative loop can trivially be converted to a tail recursive function, but the reverse is not true.
  • > My impression is that every tail call can written as a loop much more naturally.

    Which is more natural? (please just assume my wonky pseudo code syntax makes sense)

       printall(List) -> 
          foreach item in List {
             print_item(item)
         }.
    
       printall([Head | Tail]) ->
           print_item(Head),
           printall(Tail);
       printall([]) -> ok.
    
        
    IMHO, both of these need to be taught, neither is particularly more natural. In addition, as others have described, TCO makes a lot of sense for interpreters and state machines.
  • > What practical patterns are enabled by TCO in C?

    It's important in interpreters. Here's an example: https://blog.reverberate.org/2021/04/21/musttail-efficient-i...

  • > What practical patterns are enabled by TCO in C?

    Continuation Passing Style - an important construction for interpreters, but which is also useful for compilers as it's a nice way to do control flow analysis, data flow analysis and more.

    The missing feature is closures - functions which capture values from their static environment, which are basically needed to make CPS useful. GCC has nested functions, but they cannot capture without making the stack executable, which is terrible. There's a proposal[1] to get closures into C, but at present you need to simulate the capturing yourself, which is cumbersome, but can be done efficiently.

    [1]:https://thephd.dev/_vendor/future_cxx/papers/C%20-%20Functio...

  • Not every tail call is for a loop.

    You can have a set of mutually recursive functions, which tail call each other.

    In C you can write state machines using "goto" (the implementations with "switch" are typically much more inefficient), but in languages with guaranteed tail call optimizations you can write a state machine where each state is a function.

    In general, it is frequent enough to call another function as the last step of a function, even when there is no recursion involved. It is quite stupid for a compiler to use a CALL in such instances, instead of using a JMP. The only problem is that the function calling convention must be compatible with this optimization, while traditionally the C language used an inefficient calling convention that is not compatible with optimizations. That convention is a residue of the time when functions could be used without being declared and it should never be used by modern compilers.

  • and TCO was added then removed from js! https://stackoverflow.com/a/54721813

    This leads to fun stack-overflow bugs too in a lot of js code (one solution is to flatten: https://joshua.hu/javascript-infinite-tail-call-recursion-st...)

    by mmsc
  • Js really should have it. I think the shift in style from functional and manual prototype chains to Java classes is quite disappointing.
  • Lack of TCO is also a common footgun for Scheme programmers using Common Lisp.
  • I think Anton is replying to me in that LWN article IIRC. I personally didn't know C only had tail calls that late and learnt something new there!

    On the other hand, I am pretty new to the compiler space myself, and I count early 2000s as a pretty long time ago, though again it is not that far back considering how long other language implementations had tail calls like in ML or variants since 1980-90s.

  • I think Anton is wrong. Since C89 and before C23 calling an `int f();` function with arguments not matching the definition's actuals is UB. In C23 `int f();` became the same as `int f(void);`, so calling that function with any arguments is a compile-time error.

    For variadic functions, if you use `va_start()`/`va_arg()`/`va_end()` to consume all the arguments, and leave no `va_list` alive, then the compiler can correctly generate a tail call from such functions.

  • It still doesn't, this is a compiler specific language extension.

    You won't find anything on ISO/IEC 9899:2024 about tail calls, like it happens on Scheme.

    https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf

    Section 3.5 of R7RS.

    https://standards.scheme.org/official/r7rs.pdf

  • Unless the language can guarantee TCO, I don’t feel comfortable writing tail recursive code and being at the compiler’s/interpreter’s mercy.

    I think the framing of TCO as an optimization has been very unfortunate.

  • Indeed. I guess this is why [[gnu::musttail]] and [[clang::musttail]] exist.

    https://gcc.gnu.org/onlinedocs/gcc-15.1.0/gcc/Statement-Attr...

  • Someone on here had the neat idea of a “become” keyword replacing “return” when TCO is desired. I thought it was the obvious route forward and remain confused why I still haven’t seen it adopted.
  • You can rely on it now in gcc and clang, in the sense that they support a [[musttail]] attribute that tells the compiler to report an error if a call can't be TCO'd. The language doesn't guarantee TCO but can implement it at its option. If your program uses the attribute and still compiles, it means it has compiled with proper TCO.
  • Some languages have TCO annotation, it throws compiler error if TCO fails. You want stronger type system, not smart compiler guarantees or promises!
  • GCC has `[[gnu::musttail]] return`.

    But yes, framing TCO as an optimization is unfortunate.

  • C# is an interesting case because it shares a common runtime with F#, and F# guarantees TCO in most circumstances ( try / catch can stop it ) .

    There is a "tail" prefix in the intermediate language (IL) bytecode that F# uses but Roslyn, the C# compiler, never emits.

    So unlike F#, whether the same algorithm written in C# becomes a loop depends on JIT behaviour. This means if you're coming to a function cold in C# you can overflow the stack, while if you enter the same function fresh after it's been warmed up, it may have been optimised away by RyuJIT and if so you are able to call it safely for what would be large numbers of recursions.