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.
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.
GCC has `[[gnu::musttail]] return`.
But yes, framing TCO as an optimization is unfortunate.
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.
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.
Some languages have TCO annotation, it throws compiler error if TCO fails. You want stronger type system, not smart compiler guarantees or promises!
It's hard to argue that it isn't an optimization, because it doesn't affect the semantics of the program. However most optimizations are very hard to observe. The vast majority of optimizations only affect code size and runtime. TCO is one of the few exceptions. It affects memory usage, and more sensitive stack memory at that. This is why a missed optimization can be so much more catastrophic and it is worth considering things like `musttail` attributes so that the code fails to compile rather than misses the optimization.
I can only think of a few other optimizations that affect memory usage. Register spilling (arguably not really an optimization but a necessity), Rust's niche filling for enum discriminants and C++'s std::vec<bool> (a language-level optimization, arguably a different thing entirely).
I often think about how few memory optimizations we have. The reason is most likely that they tend to be non-local so are much harder to apply than CPU optimizations that generally have no effect outside of the function they are in.