Skip to content

Explicit Tail Calls #3407

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 97 commits into
base: master
Choose a base branch
from
Open

Explicit Tail Calls #3407

wants to merge 97 commits into from

Conversation

phi-go
Copy link

@phi-go phi-go commented Apr 6, 2023

This RFC proposes a feature to provide a guarantee that function calls are tail-call eliminated via the become keyword. If this guarantee can not be provided an error is generated instead.

Rendered

For reference, previous RFCs #81 and #1888, as well as an earlier issue #271, and the currently active issue #2691.

Rust tracking issue: rust-lang/rust#112788

@ehuss ehuss added the T-lang Relevant to the language team, which will review and decide on the RFC. label Apr 6, 2023
@Robbepop
Copy link

Robbepop commented Apr 6, 2023

thanks a ton @phi-go for all the work you put into writing the RFC so far! Really appreciated! 🎉

@clarfonthey
Copy link

While I personally know the abbreviation TCO, I think that it would be helpful to expand the acronym in the issue title for folks who might not know it at first glance.

@phi-go phi-go changed the title Guaranteed TCO Guaranteed TCO (tail call optimization) Apr 6, 2023
@VitWW
Copy link

VitWW commented Apr 6, 2023

An alternative is to mark a function (like in OCaml), not a return keyword:

recursive fn x() {
    let a = Box::new(());
    let b = Box::new(());
    y(a);
}

@Robbepop
Copy link

Robbepop commented Apr 6, 2023

An alternative is to mark a function (like in OCaml), not a return keyword:

recursive fn x() {
    let a = Box::new(());
    let b = Box::new(());
    y(a);
}

The RFC already highlights an alternative design with markers on function declarations and states that tail calls are a property of the function call and not a property of a function declaration since there are use cases where the same function is used in a normal call and a tail call.

@digama0
Copy link
Contributor

digama0 commented Apr 6, 2023

Note: this may be suitable either as a comment on #2691 or here. I'm assuming interested parties are watching both anyway.

The restriction on caller and callee having the exact same signature sounds quite restrictive in practice. Comparing it with [Pre-RFC] Safe goto with value (which does a similar thing to the become keyword but for intra-function control flow instead of across functions), the reason that proposal doesn't have any requirements on labels having the same arguments is because all parameters in all labels are part of the same call frame, and when a local is used by different label than the current one it is still there in memory, just uninitialized.

If we translate it to the become approach, that basically means that each function involved in a mutual recursion would (internally) take the union of all the parameters of all of the functions, and any parameters not used by the current function would just be uninitialized. There are two limitations to this approach:

  • It changes the calling convention of the function (since you have to pad out the arguments with these uninitialized locals)
  • The calling convention depends on what other functions are called in the body

I don't think this should be handled by an actual calling convention label though. Calling these "rusttail" functions for discussion purposes, we have the following limitations:

  • You can't get a function pointer with "rusttail" convention
  • You can't use a "rusttail" function cross-crate

The user doesn't have to see either of these limitations directly though; the compiler can just generate a shim with the usual (or specified) calling convention which calls the "rusttail" function if required. Instead, they just observe the following restrictions, which are strictly weaker than the one in the RFC:

  • If you become a function in another crate, the arguments of caller and callee have to match exactly
  • If you only become a function in the same crate, there are no restrictions
  • (If f tail-calls a cross-crate function g and also an internal function h, then f,g,h must all have the same arguments)

@Robbepop
Copy link

Robbepop commented Jul 6, 2025

To me the become keyword makes a lot of sense if you think about it as follows: make the current function (frame) become this new one. That is also what is kind of happening under the hood. As a minor detail: become is already a keyword, reserved since 2014, by Graydon, for exactly this purpose.

@VitWW
Copy link

VitWW commented Jul 7, 2025

just for clarity: Replace be with become Accepted RFC:

A keyword needs to be reserved to support guaranteed tail calls in a backward-compatible way. Currently the keyword reserved for this purpose is be, but the become alternative was proposed... Rename the be reserved word to become.

So, become keyword is already reserved for tail calls in Rust

@blueglyph
Copy link

Interesting bits of history. :-)

In the mean time, someone pointed out to me there was a tailcall crate, which I didn't know. Shows that this choice of name was not entirely irrelevant.

I did see the meaning of become, but I just find it more remote from the programmer's perspective. Not a big deal; it'll be a welcome feature, no matter its name.

@phi-go
Copy link
Author

phi-go commented Jul 7, 2025

I think, in the end the language team will decide, probably after some crater runs. To be honest, to me the feature would also be welcome under any name.

(I see, that I forgot to update the RFC, I'll take some time soon to do that. Also I'm happy to see that the tracking issue seems to be nearing completion.)

Some wording updates and rewriting the section on local gotos.
@haberman
Copy link

Hello, I implemented [[clang::musttail]] in Clang, and wrote the blog post you linked describing how we use them at Google (https://blog.reverberate.org/2021/04/21/musttail-efficient-interpreters.html).

I would love to see guaranteed tail calls added to Rust. I hope this proposal lands at some point.

A few notes on the RFC:

GCC does not support a feature equivalent to Clang's musttail, there also does not seem to be a push to implement it (pipermail) (as of 2021).

This info is out of date -- GCC added support for musttail about a year ago. More info here: https://blog.reverberate.org/2025/02/10/tail-call-updates.html#gcc-support

Relaxing the Requirement of Strictly Matching Function Signatures [...]

I strongly support the goal of allowing function signatures to be different.

Since implementing [[clang::musttail]], I've become increasingly convinced that it was a mistake to require caller and callee to have perfectly matching function signatures. This requirement is both more strict than is necessary on many architectures and not strict enough to be fully portable to all architectures

Many architectures are perfectly capable of performing tail calls in cases where caller and callee do not match, for example: https://godbolt.org/z/hMn74o3nW

So why does Clang require the match? We inherited this requirement from LLVM, whose musttail attribute on LLVM IR predates the Clang attribute. I think the rationale of this LLVM constraint was to make musttail "portable", so that musttail calls will work on any architecture.

But this "portability" is an illusion. In practice, we've found that many LLVM backends fail to perform tail calls even when functions are strictly matching: https://github.com/protocolbuffers/protobuf/blob/bb454a777e6019f675ea2e0267f7b57b389c6720/src/google/protobuf/port_def.inc#L245-L250

Moreover, some platforms are fundamentally incompatible with tail calls, for example WebAssembly without the Tail Call Extension.

So in practice, the rule does not buy us much of a guarantee, but it also keeps us from using non-matching tail calls on platforms that support it.

A more useful semantic is "perform a tail call if supported on this platform, otherwise fail to compile." This has been proposed as [[clang::nonportable_musttail]], or we could just relax the constraints of [[clang::musttail]]. Discussion here: llvm/llvm-project#54964

Unfortunately this would require work in LLVM, and I have no idea what that would take to land.

@traviscross
Copy link
Contributor

Thanks @haberman for that experience report and helpful context. Many of us on the lang team would also like to see these land. That will, though, probably require us to confront those interesting and difficult questions that you raise.

@phi-go
Copy link
Author

phi-go commented Jul 10, 2025

Indeed, thank you @haberman!

To refresh some context. For this RFC, we expected that requiring matching function signatures will make it easier for backends to support musttail. We always needed to deal with the case that some backends will not have support for musttail. I think we already wanted to allow or disallow guaranteed tail calls based on choice of the backend (@WaffleLapkin please correct me here).

However, reading between the lines, do I understand correctly that even LLVM does not actually provide a guarantee to create tail calls for all matching signature calls for backends with support? Or in other words, do we need to let the backend decide for us on a per call basis, instead of allowing / disallowing on a per backend basis on the Rust side?

If we need to let the backend decide on a per call basis in any case, then I see no point to require matching signatures.

@bjorn3
Copy link
Member

bjorn3 commented Jul 10, 2025

But this "portability" is an illusion. In practice, we've found that many LLVM backends fail to perform tail calls even when functions are strictly matching: https://github.com/protocolbuffers/protobuf/blob/bb454a777e6019f675ea2e0267f7b57b389c6720/src/google/protobuf/port_def.inc#L245-L250

Is there any fundamental reason for backends other than the wasm backend? Or just not yet implemented. If the latter, I think we should get it implemented in LLVM before we stabilize explicit tail calls on the rust side.

@Robbepop
Copy link

Robbepop commented Jul 10, 2025

IIRC the decision to restrict tail calls to same signature was not just for portability.

  1. This restriction allows for a simpler implementation on the Rust side.
  2. There are some performance affecting considerations when signatures do not match. (I am not 100% certain here.)

Furthermore, introducing new features into Rust as MVP (minimum viable products) is a common pattern.

MVPs have higher chances to eventually land at all and with a bit of care it is also kind of easy or at least possible to extend the feature's capabilities later on.


Concerning portability, I was under the impression that matching signatures between caller and callee would allows us in more cases to simulate tailcall behavior even on backends that do not have support for them via loop transformations and trampolines instead of lowering to LLVM's musttail. I'd be glad for someone to shed light on this.

@haberman
Copy link

I haven't dug deep enough to determine whether some architectures have fundamental blockers to musttail (aside from WASM which does fundamentally prohibit it). I only know that in practice, backend support for musttail has been spotty in LLVM.

That said, things have improved a fair bit in the last few years. I just double checked on the bugs I linked above and several of them have been closed in the meantime. Hopefully this means that musttail in LLVM is getting most robust.

But that almost doesn't seem to matter, given that WASM fundamentally forbids tail calls. The existence of even a single such platform means that we can never expect backends to universally support tail calls.

This seems to leave three main options for become in Rust:

  1. Fully Portable (emulated as necessary): Rust specifies a set of rules for tail calls, and guarantees that calls following those rules will work on all platforms. On platforms where a tail call was not possible, the compiler will perform a transformation to simulate a true tail call. This transformation could feasibly be implemented either in the Rust frontend or in LLVM. The rules for tail calls have to be restrictive enough that such a transformation is always possible (I'm not personally familiar with how such a transformation would work, especially if many functions are calling each other through tail calls).
  2. Per-Backend Support: Rust specifies a set of rules for tail calls, and each platform either supports tail calls or it doesn't. The rules are written such that any platform that advertises support for tail calls must support any tail call that follows those rules. I think this is the approach that @phi-go was alluding to above. In this scenario, WASM would simply declare tail calls unsupported.
  3. Per-Call Support: There are no formal rules for caller and callee; any become that cannot be compiled into a true tail call on the current platform will just fail to compile.

Clang currently attempts to do (1), but has no emulation, so in practice it is not fully portable.

I believe that the proposed return goto feature in C does (3).

Personally my gut is that (3) is the best semantic for a low-level language like Rust, C, or C++. For a higher-level language maybe emulation would make sense, but for low-level applications like our tail-calling parsers you don't really want emulation. If tail calls are not available, we'd want to manually switch to a different strategy.

The user feedback I've seen also seems to support the idea that (3) is what users expect.

The main downside of (3) is that it seems to require changes in LLVM. The semantics for musttail in LLVM are designed for (1). To support (3), we'd need to either relax the signature-matching constraints around the attribute or introduce a new attribute with fewer constraints.

@Robbepop
Copy link

Robbepop commented Jul 10, 2025

@haberman thank you for the detailed outline for the different options. That really helps and clears things up.

My personal hope was for 1) since I find it rather inelegant to have a core language feature that depends on a compilation backend. But I can certainly see why people prefer other options and have no strong arguments for or against them. I clearly see a way forward for a future extension to add support for other options if necessary.

In the past there were discussions about people who strongly preferred one over the other options and there were syntax ideas that are similar to maybe_become in addition to become where maybe_become is similar to LLVM's weaker tail attribute. IIRC, we agreed to keep this RFC small and focused and have only a single keyword with the original musttail meaning as it is already hard enough to get this RFC implemented.

Note that WebAssembly has tail-calls since quite a while already. They are included in Wasm 2.0+. Furthermore, Wasm is not meant to be a modular specification like RISC-V, so over time, Wasm will (hopefully) pervasively implement tail-calls everywhere. Though, Wasm 1.0 will probably be kept around in some form and will cause problems forever.

@haberman
Copy link

@Robbepop If I'm understanding you correctly, maybe_become is a different semantic where compilation will always succeed, but the call may or may not be tail call optimized. I agree that this is a different feature that doesn't belong in this RFC.

In all of the options I outlined, the become keyword would guarantee that if compilation succeeds, then tail call optimization was performed. This is an important guarantee for the applications where we use [[clang::musttail]]. The maybe_become semantic wouldn't be safe for this use case, because if tail call optimization was not performed, then the program would be vulnerable to stack overflow. We would prefer to have the compile fail rather than have it succeed without the optimization.

The main difference between (1)-(3) is what circumstances would cause the compiler would throw an error. There are three general classes of error you could get from become:

a. Error: become applied to a call which [breaks general Rust rule about become, eg. caller and callee don't match]
b. Error: become is not supported on this platform
c. Error: become is not supported for this call on this platform

Option (1) would only ever throw error (a).

Option (2) could throw errors (a) or (b).

Option (3) could throw errors (b) or (c).

But with all of these options, become would guarantee that a successful compile performed tail-call optimization, which is different (I think) than maybe_become (or tail in LLVM).

@Robbepop
Copy link

Robbepop commented Jul 10, 2025

@Robbepop If I'm understanding you correctly, maybe_become is a different semantic where compilation will always succeed, but the call may or may not be tail call optimized. I agree that this is a different feature that doesn't belong in this RFC.

@haberman Yes you understood that correctly. Glad you agree.

Sorry for the confusion about the maybe_become that has been talked about in the past. Indeed, it has not much to do with the 3 options you presented. I just came up with it since it demonstrated yet even more different use cases that people have or had about tail calls.

@dlight
Copy link

dlight commented Jul 11, 2025

I haven't dug deep enough to determine whether some architectures have fundamental blockers to musttail (aside from WASM which does fundamentally prohibit it). I only know that in practice, backend support for musttail has been spotty in LLVM.

That said, things have improved a fair bit in the last few years. I just double checked on the bugs I linked above and several of them have been closed in the meantime. Hopefully this means that musttail in LLVM is getting most robust.

But that almost doesn't seem to matter, given that WASM fundamentally forbids tail calls. The existence of even a single such platform means that we can never expect backends to universally support tail calls.

This seems to leave three main options for become in Rust:

1. **Fully Portable (emulated as necessary)**: Rust specifies a set of rules for tail calls, and guarantees that calls following those rules will work on all platforms. On platforms where a tail call was not possible, the compiler will perform a transformation to simulate a true tail call. This transformation could feasibly be implemented either in the Rust frontend or in LLVM. The rules for tail calls have to be restrictive enough that such a transformation is always possible (I'm not personally familiar with how such a transformation would work, especially if many functions are calling each other through tail calls).

2. **Per-Backend Support**: Rust specifies a set of rules for tail calls, and each platform either supports tail calls or it doesn't. The rules are written such that any platform that advertises support for tail calls must support any tail call that follows those rules. I think this is the approach that @phi-go was alluding to above. In this scenario, WASM would simply declare tail calls unsupported.

3. **Per-Call Support**: There are no formal rules for caller and callee; any `become` that cannot be compiled into a true tail call on the current platform will just fail to compile.

Clang currently attempts to do (1), but has no emulation, so in practice it is not fully portable.

I believe that the proposed return goto feature in C does (3).

Personally my gut is that (3) is the best semantic for a low-level language like Rust, C, or C++. For a higher-level language maybe emulation would make sense, but for low-level applications like our tail-calling parsers you don't really want emulation. If tail calls are not available, we'd want to manually switch to a different strategy.

The user feedback I've seen also seems to support the idea that (3) is what users expect.

The main downside of (3) is that it seems to require changes in LLVM. The semantics for musttail in LLVM are designed for (1). To support (3), we'd need to either relax the signature-matching constraints around the attribute or introduce a new attribute with fewer constraints.

Note that if we have (1), a third party crate could provide (2)/(3) with a macro that essentially checked at compile time if the compilation target supports tail calls natively (with #[cfg] or something), and if it doesn't, just fail compilation with compile_error.

This means that it's okay for the MVP to offer only (1) and, if there is further demand, offer (3) in a later moment (or don't offer it at all)

Also I think (3) and (2) looks the same, only that (3) lacks documentation on what platform is not supported. What's important is that either a tail call is fully emulated without growing the stack (option (1)) or compilation fails (options (2) and (3)). It would be terrible if in unsupported platforms, become compiled into a regular call.

@haberman
Copy link

Note that if we have (1), a third party crate could provide (2)/(3) with a macro [...]

I don't think that's quite right, because the constraints imposed by (1) will probably be more restrictive than necessary on some architectures.

Take this example from Clang, which mostly does (1) now: https://godbolt.org/z/7dqqYTvEz

Without musttail, the compiler can trivially tail-call optimize this call. But if you use musttail, the example fails to compile because the call does not satisfy the "portable" rules. If we did (3) instead, this code could work on platforms that support it (like x86-64).

Also I think (3) and (2) looks the same, only that (3) lacks documentation on what platform is not supported.

I think (3) can better accommodate the fact that different platforms have different constraints on when a tail call is possible to perform.

For example, here is a variation of the previous example where we compile the code for both x86-64 and x86. For x86-64, the call can be tail-call optimized because that platform uses a register-based calling convention. On x86, the same code cannot be tail call optimized because arguments are passed on the stack: https://godbolt.org/z/xc4oPze4b

Since different platforms can have different levels of support for tail calls, it's not granular enough to make it a binary choice whether a platform supports tail calls or not.

It would be terrible if in unsupported platforms, become compiled into a regular call.

I fully agree on this.

@haberman
Copy link

Regarding emulation, I'm curious about two things:

Is there a discussion somewhere about the technical details of how this emulation would work? Is there prior art for it? Can tail calls be emulated in cases where the target function is not statically known?

Is there a clear use case where a user would benefit from this emulation? In all of the cases where I've used musttail, it's because we want the machine code to have true tail calls in it. If Rust were a LISP-like language where tail recursion was the primary idiomatic means of iteration, it would make more sense to me, but Rust has rich facilities for imperative iteration. In a low-level language like Rust, it seems beneficial to make the compiler bias towards being predictable; emulating a missing arch-specific feature seems somewhat against that spirit, at least from my perspective as a Rust outsider. I would hate to see a lot of work expended on emulation if few users want it in practice.

@Jules-Bertholet
Copy link
Contributor

Jules-Bertholet commented Jul 12, 2025

Whether tail calls can be performed depends on the function call ABI for the platform, and Rust’s default ABI is unstable. Whether tail calls can be performed on a particular platform must not depend on the unstable implementation details of extern "Rust".

I think, if we were to go down the “per-call-and-target support” route, it would need to apply only to stable, non-default ABIs like "C" and "system". The RFC also mentions the possibility of a dedicated tail-call ABI in its alternatives section.

@ssokolow
Copy link

Note that if we have (1), a third party crate could provide (2)/(3) with a macro [...]

I don't think that's quite right, because the constraints imposed by (1) will probably be more restrictive than necessary on some architectures.

(3), maybe, depending on how you interpret it... but (2) could work if the crate's purpose is to abstract over a big cfg expression of which platforms support musttail.

@DemiMarie
Copy link

I think the best approach is to make extern "rust" use a callee-pops calling convention for which LLVM always supports tail calls on all platforms. I believe this includs fastcall. This is the approach used by GHC, though they use a custom calling convention.


Since `#[track_caller]` adds an argument, `#[track_caller]` functions can only tail call other `#[track_caller]` functions. So the following example would not compile.
However, removing `#[track_caller]` should not be a breaking change, as [it is not part of the stable API surface](https://github.com/rust-lang/rust/issues/87401#issuecomment-910628405) for Rust.
For tail calls, however, removing an argument is a breaking change, so functions with `#[track_caller]` are **not** allowed to use `become`. ([Though, loosening this restriction might be possible.](https://github.com/rust-lang/rfcs/pull/3407/files/3304fee7542a56e3c671b8ce8a85070904cbf2ba#r2202286180)). Additionally, tail calling into a `#[track_caller]` function from a non-tracking function [might require growing the stack](https://github.com/rust-lang/rfcs/pull/3407/files/3304fee7542a56e3c671b8ce8a85070904cbf2ba#r2202286180), as this would defeat the purpose of tail calling, this is also **not** allowed.
Copy link
Contributor

@Jules-Bertholet Jules-Bertholet Jul 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding track_caller should also not be breaking. So tail-calling a track_caller fn from a non-track_caller should at most be a warning. (Note that the risk of unexpected stack growth is limited, because the track_caller callee cannot itself use become.)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make sure, you mean adding track_caller to a tail-called function should not be breaking? So the equivalent of adding track_caller?

If that is what you meant, I'm a bit confused. On the one hand I can see that we want track_caller to be usable transparently but on the other hand tail calls are pretty much incompatible with the idea of tracking the caller anyway. Ignoring the matching function signature issue, what caller would you want tracked for a tail called function? So should this just be added as another action that is not allowed? Or should we move track_caller into the "Unresolved questions" section as we need to figure out how this needs to work?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what caller would you want tracked for a tail called function

The call would have to go through the shim that is also used for function pointers, so no caller would be tracked.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I didn't know there was precedent for this. If we are just not tracking the caller then we can allow tail calls into caller tracking functions, other than how I understood it. I'll move this to the drawbacks section as it is a complication we need to implement.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated: 4dfb991

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what caller would you want tracked for a tail called function

The call would have to go through the shim that is also used for function pointers, so no caller would be tracked.

The shim however is not going to be tail-calling the actual fn implementation. This means that if I have a track-caller and a non-tracker-caller and have them mutually tail-call each other, the stack usage would grow.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nbdd0121 As I point out in the thread OP, the #[track_caller] function cannot itself use become, so mutual tail calling is impossible.

Copy link
Author

@phi-go phi-go Aug 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RFC was also updated earlier, let me know if anything is unclear.

rework track_caller section and move it into the drawbacks section
@phi-go
Copy link
Author

phi-go commented Aug 3, 2025

Based on the comments made by @haberman and following discussions, I think we need to consider the following open questions:

  • While for Clang matching function signatures do not guarantee portability, is this also an issue for Rust?
    • (Though, support seems to be improving in Clang / LLVM.)
    • Can we find cases for Rust code where tail calls of matching function signatures fail compilation or fail to actually tail call with LLVM?
    • What about other backends?
  • If matching function signatures cannot ensure portability, is there even any option other than to coordinate with the backend on a per-call basis?
  • Is there any push in LLVM to support [[clang::nonportable_musttail]]?
  • Is it possible to support a calling convention for per-call support?
    • A similar discussion was already had: here.
    • The choice of matching function signatures and matching calling conventions between caller and callee should allow simply substituting with new values, see discussion linked above. In theory, the calling convention should not matter in that regard (one of the reasons this RFC includes the matching signature restriction).
  • A design decision needs to be made. Should tail calls only provide the semantic guarantee (fully portable) or should a best effort for performance also be made (per-call)?
    • Note that, both can be supported independently.
    • Also note that, we will want to support the semantic guarantee (no stack overflows) in any case.
    • Caveats for fully portable (details above)
      • It might not actually be possible to be fully portable, even for matching function signatures.
      • Emulation will take extra effort to implement and it is not clear how to actually support dynamic calls using emulation.
      • Performance, a main motivation for this feature, will not be easily "controllable". Unexpected degradation is possible when switching from real tail calls to an emulated implementation. What assurances does Rust want to provide?
      • It is easier for users that just want the semantic guarantee of no stack overflows.
    • Caveats for per-call (details above)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.