Rendered at 16:50:22 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
drdexebtjl 2 hours ago [-]
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.
kevincox 1 hours ago [-]
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.
steveklabnik 52 minutes ago [-]
> It's hard to argue that it isn't an optimization, because it doesn't affect the semantics of the program.
Depends on the semantics of the programming language itself. For some languages, it is truly an optimization, for some, it is required, and does meaningfully change observed semantics.
lilbigdoot 8 minutes ago [-]
If my program crashes without it, that's a semantic difference no?
jmalicki 47 minutes ago [-]
JVM does a lot of escape analysis to turn heap allocated memory into stack local variables.
It doesn't matter if it's local since it's a VM, it's doing it at runtime and can change an entire call stack of non local code for an optimization.
tialaramex 22 minutes ago [-]
std::vector<bool> is just a terrible specialisation, it isn't an optimisation.
If std::vector<bool> was an optimisation we couldn't write C++ which blows up because it's actually a bitset, it would be semantically transparent - but that's easy to do even by accident because it's not transparent at all.
In fact the existing std::vector<bool> should just be named std::growable_bitset or something and then std::vector<bool> would make what you actually wanted like Rust's Vec<bool> does.
drdexebtjl 30 minutes ago [-]
The main difference is not that it affects memory usage, imo.
It’s that it affects memory usage _asymptotically_.
Most optimizations only affect constant factors.
LukeShu 2 hours ago [-]
GCC has `[[gnu::musttail]] return`.
But yes, framing TCO as an optimization is unfortunate.
vinkelhake 27 minutes ago [-]
And there's also [[clang::musttail]] and [[msvc::musttail]].
Some languages have TCO annotation, it throws compiler error if TCO fails. You want stronger type system, not smart compiler guarantees or promises!
kenjin4096 4 hours ago [-]
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.
pjmlp 4 hours ago [-]
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.
Woops, I meant to write tail call optimization not tail calls. Shouldn't have used the term interchangeably. Yeah I'm aware that C doesn't have proper tail calls. Thanks for the correction though.
wahern 3 hours ago [-]
A formal technical specification (TS) extension is already being drafted: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3582.pdf That's a step up from the usual proposals. I'm not sure what criteria is used to decide whether to first create a TS vs just incorporating a change into the working draft of the next standard.[1] _Defer also seems to be taking the TS route.[2]
Well, even if that lands on the official standard, that means C29 as probable release year for C2y, plus adoption of the exact form across compilers to be able to rely on it being available.
Someone 3 hours ago [-]
Plus the fact that an implementation that recognizes the new syntax and produces an error whenever it encounters it will be conforming.
On such an implementation, the feature is available but useless.
Lack of TCO is also a common footgun for Scheme programmers using Common Lisp.
guenthert 4 hours ago [-]
Only if they are using an insufficiently smart compiler. SBCL handles TCO just fine, as do a number of other implementations, see : https://0branch.com/notes/tco-cl.html
pfdietz 4 hours ago [-]
Even SBCL doesn't do TCO at all times. Compiling at (debug 3) means no TCO.
Another related footgun is deep recursion of other kinds, for example when recursively traversing down lists. For long lists it's easy to exceed the stack size limit. The common idiom is to recur on list elements, but iterate or map to go along a list.
guenthert 4 hours ago [-]
> Even SBCL doesn't do TCO at all times. Compiling at (debug 3) means no TCO.
Presumably one intends to debug the code, when setting (debug 3). Then it'll be helpful to see the stack, no?
> Another related footgun is deep recursion of other kinds, for example when recursively traversing down lists. For long lists it's easy to exceed the stack size limit. The common idiom is to recur on list elements, but iterate or map to go along a list.
Not going to argue with seasoned lispers here, but IMHO recursive code makes most sense when accessing recursive data structures.
pfdietz 3 hours ago [-]
One place where this shows up is in parse trees. The grammar for a list of things may involve productions that look like list constructors. This, directly translated into a data structure, would give a very long chain of parse tree nodes dangling off to the right. It's a recursive data structure, but a very deep one for large lists, and traversing it recursively can use a lot of stack.
This can also be seen as an argument against building parse trees that way. Instead, have a node with an unbounded number of children, the elements of the list.
tialaramex 4 hours ago [-]
This footgun is the reason I'm so enthusiastic about the Rust `become` keyword.
This proposal would give Rust a specific keyword which says that you intend TCO and so two things happen: 1. The compiler goes to more length to deliver TCO even where it wouldn't "just work" and 2. If it cannot deliver TCO your code doesn't compile, because you asked for TCO.
chriswarbo 4 hours ago [-]
Sounds similar to @tailrec in Scala
I personally use the phrase "tail call elimination" when it's a requirement that can be relied on; and "tail call optimisation" when it might be implementation-dependent, context-dependent, limited (e.g. to immediate self-calls), etc.
tialaramex 2 hours ago [-]
I am definitely not a Scala expert.
As I wrote in a sibling comment, the key benefit here is the extra work from the compiler to deliver what you wanted, on top of the diagnostic if it can't.
I don't know if Scala has the problem that `become` addresses (C++ calls this RAII, but I have no idea what Scala would call it if they have the same idea)
However in my brief attempt to validate what Scala does do here, I found discussion of "alway" optimising to a loop which is a bad sign. Tail recursion is an elegant way to write some loops but that's not the only thing it's useful for, and it seems as though Scala just doesn't care about other cases, at least for @tailrec
One thing you want TCO for in a language like Rust with lots of monomorphisation is to avoid function call overhead for the deliberately out-of-line slow path in some code. So in this case there was never an implied loop and we're not averting a stack overflow, we wanted to do a single instruction pointer change instead of an expensive function call wrapper. Seems like @tailrec isn't for that.
chriswarbo 35 seconds ago [-]
I jsut did some digging and it seems you're right, it's only for methods which call themselves (which indeed get compiled into a loop, as an entirely local transformation). So not hugely useful.
Apologies, I've not written Scala for many years; I just recalled that there was a way to annotate tail calls which the compiler checks. I didn't realise it was so limited!
jmalicki 46 minutes ago [-]
Does 1 really happen? I would never trust a compiler where 1 was a possibility. If it can work it should.
StilesCrisis 4 hours ago [-]
Sounds like clang::must_tail?
tialaramex 3 hours ago [-]
I am not a Clang expert, but first, obviously that's a C++ attribute and so while Clang can decide what it means in Clang in the programming language itself it has no semantic weight because the ISO document says attributes are always ignorable.
Secondly however in these languages you often won't naively get TCO because you have at least one local variable which C++ would say has a "non-trivial destructor" or Rust would say "implements Drop". These both mean that naively the "tail call" wasn't actually the last thing to happen, the destructor / Drop::drop happen at the end of the function, after the tail call.
The proposed become keyword tries to core::mem::drop any such variables, if it succeeds now that tail call is last and we can do TCO, if it fails [e.g. because the variables it wants to drop are needed for the tail call] we can diagnose the problem. I believe the Clang attribute doesn't have this behaviour.
StilesCrisis 2 hours ago [-]
Clang tail-calls aren't guaranteed to work with all C++ code. If you have a non-trivial constructor, as you mention, it will tell you this and fail instead of silently letting you believe you have tail-calls when you don't.
im3w1l 2 hours ago [-]
Reordering destructors is not safe in C++, as it's fairly common to rely on objects being destroyed in reverse order and doing stuff like
A a;
B b(&a);
In rust the borrow checker would guard against reordering such things, but a caveat is that there might be unsafe code relying on drop-order which the borrow checker would be oblivious to. There could also potentially be objects representing external resources like a temp file where dropping them out of order leads to issues.
steveklabnik 48 minutes ago [-]
> In rust the borrow checker would guard against reordering such things
It doesn't even get that far: Rust guarantees that things drop in reverse order of declaration, full stop.
One interesting wrinkle here: for struct members, Rust does the opposite of what C++ does. We debated changing it to match, but
> there might be unsafe code relying on drop-order which the borrow checker would be oblivious to.
There was no super real compelling argument to choose one direction over the other in the abstract, and "be the same as C++" was not considered important enough to risk breaking unsafe code that relied on the (what was at the time) implementation defined behavior.
tialaramex 1 hours ago [-]
That Rust was in fact always unsound if it would cause problems to core::mem::drop(a); and the `become` call just drops things so it's the same.
Safe-but-undesirable outcomes are acceptable. For example maybe our tail call ends up reverting a database transaction and we wish it were otherwise. But if the code did compile but wasn't memory safe as a result of this new drop then it was always unsound and shouldn't have existed.
Just as the guts of some STL classes are very complicated in order to deliver the promised exception safety promises, the guts of unsafe Rust code are often tricky for similar reasons, you are mandated to deliver safety, it's not up to you to say "That's stupid, don't do that" either ensure it won't compile or safely cope.
pjmlp 3 hours ago [-]
Mostly because they forget Scheme is one of the few languages where TCO is part of the language standard, making it a required feature for any compliant implementation.
This has always been an issue regarding TCO support across programming languages.
pfdietz 3 hours ago [-]
Well, and also because of the "I've been told in Scheme you should do it this way, so by gum I'm going to do it this way!"
groundzeros2015 4 hours ago [-]
Js really should have it. I think the shift in style from functional and manual prototype chains to Java classes is quite disappointing.
chuckadams 1 hours ago [-]
Technically TCO is still in the spec, TC39 deadlocked over modifying it. TC39 really does not fill me with confidence in general.
nyeah 4 hours ago [-]
>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.
"If you really need either of the following.....then we recommend that you consider using a different compiler such as Intel or gcc (short-term) and/or pressure your standards committee representatives to have ISO C++ include more of the C standard (longer-term)."
Which is kind of why nowadays clang is part of Visual Studio as well.
However, after Satya got into the whole Microsoft <3 FOSS, this changed a bit,
There are a few blogs after that, so at least up to C17 minus the optional parts from C11, the support is there.
It remains to be seen if anything C23 or later will ever come into MSVC, and then again, clang is part of VS installer.
throw-qqqqq 48 minutes ago [-]
TIL, thanks for updating me on this!
I read Herb Sutter’s post many years ago, but didn’t know they had picked up the work again.
I see that VLAs are still not supported, which is a shame IMO, but the C-support seems much better than it used to be at least.
4 hours ago [-]
steveklabnik 57 minutes ago [-]
(2025)
hnfvovpje4 3 hours ago [-]
Clear, useful, done
messe 4 hours ago [-]
> 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).
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.
derdi 3 hours ago [-]
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".
I think the framing of TCO as an optimization has been very unfortunate.
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.
Depends on the semantics of the programming language itself. For some languages, it is truly an optimization, for some, it is required, and does meaningfully change observed semantics.
It doesn't matter if it's local since it's a VM, it's doing it at runtime and can change an entire call stack of non local code for an optimization.
If std::vector<bool> was an optimisation we couldn't write C++ which blows up because it's actually a bitset, it would be semantically transparent - but that's easy to do even by accident because it's not transparent at all.
In fact the existing std::vector<bool> should just be named std::growable_bitset or something and then std::vector<bool> would make what you actually wanted like Rust's Vec<bool> does.
It’s that it affects memory usage _asymptotically_.
Most optimizations only affect constant factors.
But yes, framing TCO as an optimization is unfortunate.
As well as an effort to get it standardized: https://isocpp.org/files/papers/D3939R0.html (in C++).
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.
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
1. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3886.pdf
2. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3928.pdf
On such an implementation, the feature is available but useless.
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...)
Another related footgun is deep recursion of other kinds, for example when recursively traversing down lists. For long lists it's easy to exceed the stack size limit. The common idiom is to recur on list elements, but iterate or map to go along a list.
Presumably one intends to debug the code, when setting (debug 3). Then it'll be helpful to see the stack, no?
> Another related footgun is deep recursion of other kinds, for example when recursively traversing down lists. For long lists it's easy to exceed the stack size limit. The common idiom is to recur on list elements, but iterate or map to go along a list.
Not going to argue with seasoned lispers here, but IMHO recursive code makes most sense when accessing recursive data structures.
This can also be seen as an argument against building parse trees that way. Instead, have a node with an unbounded number of children, the elements of the list.
This proposal would give Rust a specific keyword which says that you intend TCO and so two things happen: 1. The compiler goes to more length to deliver TCO even where it wouldn't "just work" and 2. If it cannot deliver TCO your code doesn't compile, because you asked for TCO.
I personally use the phrase "tail call elimination" when it's a requirement that can be relied on; and "tail call optimisation" when it might be implementation-dependent, context-dependent, limited (e.g. to immediate self-calls), etc.
As I wrote in a sibling comment, the key benefit here is the extra work from the compiler to deliver what you wanted, on top of the diagnostic if it can't.
I don't know if Scala has the problem that `become` addresses (C++ calls this RAII, but I have no idea what Scala would call it if they have the same idea)
However in my brief attempt to validate what Scala does do here, I found discussion of "alway" optimising to a loop which is a bad sign. Tail recursion is an elegant way to write some loops but that's not the only thing it's useful for, and it seems as though Scala just doesn't care about other cases, at least for @tailrec
One thing you want TCO for in a language like Rust with lots of monomorphisation is to avoid function call overhead for the deliberately out-of-line slow path in some code. So in this case there was never an implied loop and we're not averting a stack overflow, we wanted to do a single instruction pointer change instead of an expensive function call wrapper. Seems like @tailrec isn't for that.
Apologies, I've not written Scala for many years; I just recalled that there was a way to annotate tail calls which the compiler checks. I didn't realise it was so limited!
Secondly however in these languages you often won't naively get TCO because you have at least one local variable which C++ would say has a "non-trivial destructor" or Rust would say "implements Drop". These both mean that naively the "tail call" wasn't actually the last thing to happen, the destructor / Drop::drop happen at the end of the function, after the tail call.
The proposed become keyword tries to core::mem::drop any such variables, if it succeeds now that tail call is last and we can do TCO, if it fails [e.g. because the variables it wants to drop are needed for the tail call] we can diagnose the problem. I believe the Clang attribute doesn't have this behaviour.
It doesn't even get that far: Rust guarantees that things drop in reverse order of declaration, full stop.
One interesting wrinkle here: for struct members, Rust does the opposite of what C++ does. We debated changing it to match, but
> there might be unsafe code relying on drop-order which the borrow checker would be oblivious to.
There was no super real compelling argument to choose one direction over the other in the abstract, and "be the same as C++" was not considered important enough to risk breaking unsafe code that relied on the (what was at the time) implementation defined behavior.
Safe-but-undesirable outcomes are acceptable. For example maybe our tail call ends up reverting a database transaction and we wish it were otherwise. But if the code did compile but wasn't memory safe as a result of this new drop then it was always unsound and shouldn't have existed.
Just as the guts of some STL classes are very complicated in order to deliver the promised exception safety promises, the guts of unsafe Rust code are often tricky for similar reasons, you are mandated to deliver safety, it's not up to you to say "That's stupid, don't do that" either ensure it won't compile or safely cope.
This has always been an issue regarding TCO support across programming languages.
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.
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.
It famously doesn’t support a few features of C99.
They don’t really seem to care much about regular C support (non-C++).
https://herbsutter.com/2012/05/03/reader-qa-what-about-vc-an...
Note,
"If you really need either of the following.....then we recommend that you consider using a different compiler such as Intel or gcc (short-term) and/or pressure your standards committee representatives to have ISO C++ include more of the C standard (longer-term)."
Which is kind of why nowadays clang is part of Visual Studio as well.
However, after Satya got into the whole Microsoft <3 FOSS, this changed a bit,
https://devblogs.microsoft.com/cppblog/c11-and-c17-standard-...
There are a few blogs after that, so at least up to C17 minus the optional parts from C11, the support is there.
It remains to be seen if anything C23 or later will ever come into MSVC, and then again, clang is part of VS installer.
I read Herb Sutter’s post many years ago, but didn’t know they had picked up the work again.
I see that VLAs are still not supported, which is a shame IMO, but the C-support seems much better than it used to be at least.
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