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 "always" 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.
This is different from Scheme or the MLs (there as a quality-of-implementation feature) where tail calls into arbitrary functions are expected not to lead to space leaks.
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.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.
The drops happen (if implemented) in the same order, but in a different place, half the point of become is to put any needed drops first before the call, as otherwise it's not in tail position and we can't do the optimisation.
So the borrowck can become involved if our become foo(bar, &baz) borrows baz but baz's type impl Drop - the diagnostics aren't great today, but then the feature isn't finished so it's not a priority.
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.
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.
You don't necessarily need to give up TCO to do that though. You just do some bookkeeping and synthesize virtual stack frames. DWARF has native facilities to handle this.
CL goes the route it does mostly out of history, which includes the fact it has its own debugging ecosystem, more than any fundamental technical reason. There are technical hurdles with doing this in an image-based dynamic compilation model, but it's very far from intractable. Especially if you just do what GHC did and add a DWARF workflow. Most CL users wouldn't ever touch it though, because that's a drastically different debugging model that costs them a lot of ergonomic power, which may even be the reason they're working in CL to begin with.
This has always been an issue regarding TCO support across programming languages.