- Feature Name: proper_tail_calls
- Start Date: (fill me in with today's date, YYYY-MM-DD)
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)
This RFC adds explicit proper tail calls to Rust via the become
keyword.
It also specifies the semantics of proper tail calls and their interaction
with destructors.
Proper tail calls are commonly found in functional languages, such as OCaml, SML, Haskell, Scheme, and F#. They also can be used to easily encode state machines and jump tables. Furthermore, they allow for code to be written in continuation-passing style, which may be useful for some functional programming patterns.
A proper tail call is written using the become
keyword. This is already
reserved, so there is no backwards-compatibility break.
The become
keyword must be followed by a function or method calls.
Invocations of overloaded operators with at least one non-primitive argument
were considered as valid targets, but were rejected on grounds of being too
error-prone. In any case, these can still be called as methods.
A future RFC may allow become
in more places.
A become
statement is type-checked exactly like a return
statement. In the
current implementation, the syntactic restrictions on become
noted above are
enforced during this phase. Additionally, the callee must use either the
rust
calling convention or the rust-call
calling convention.
A become
expression acts as if the following events occurred in-order:
- All variables that are being passed by-value are moved to temporary storage.
- All local variables in the caller are destroyed according to usual Rust semantics. Destructors are called where necessary. Note that values moved from in step 1 are not dropped.
- The caller's stack frame is removed from the stack.
- Control is transferred to the callee's entry point.
This implies that it is invalid for any references into the caller's stack frame to outlive the call.
The borrow checker ensures that none of the above steps will result in the use of a value that has gone out of scope.
An implementation is required to support an unlimited number of proper tail calls without exhausting any resources.
A current, mostly-functioning implementation can be found at DemiMarie/rust/tree/explicit-tailcalls.
The parser parses become
exactly how it parses the return
keyword. The
difference in semantics is handled later.
During type checking, the following are checked:
- The target of the tail call is, in fact, a call.
- The target of the tail call has the proper ABI.
Later phases in the compiler assert that these requirements are met.
New nodes are added in HIR and HAIR to correspond to become
. In MIR, however,
a new flag is added to the TerminatorKind::Call
variant. This flag is only
allowed to be set if all of the following are true:
- The destination is
RETURN_POINTER
. - There are no cleanups.
- The basic block being branched into has length zero.
- The basic block being branched into terminates with a return.
Trans will assert that the above are in fact true.
Finally, the use of proper tail calls must be propogated to LLVM. This is done in two ways:
-
Turn on tail call optimization. This is done by setting
Options.GuaranteedTailCallOpt
in PassWrapper.cpp. -
Make the actual call a tail call. This is done by means of the following function, added to RustWrapper.cpp:
extern "C" void LLVMRustSetTailCall(LLVMValueRef Instr) { CallInst *Call = cast<CallInst>(unwrap<Instruction>(Instr)); Call->setTailCall(); }
Tail calls are essentially disciplined cross-function goto
– a unidirectional
transfer of control. They are a wholly new pattern for Rust, and make others
possible, such as continuation-passing style.
Nevertheless, tail calls are an advanced feature that can produce code which is difficult to debug. As such, they should only be used when other techniques are not suitable.
I believe that the first three paragraphs under #detailed-design could be added to the Rust Reference with only minor changes. New material containing some use cases would need to be added to The Rust Programming Language and Rust by Example.
One major drawback of proper tail calls is that their current implementation in LLVM is not zero-cost: it forces a callee-pops calling convention, and thus causes a stack adjustment after each non-tail call. This could be a performance penalty.
An even greater drawback of proper tail calls is lack of cross-platform support: LLVM does not support proper tail calls when targeting MIPS or WebAssembly, and a compiler that generated C code would be hard-pressed to support them. While relying on sibling call optimization in the C compiler might be possible with whole-program compilation, it would still be tricky. WebAssembly does not support tail calls at all yet, so stablization of this feature will need to wait until this changes, which could take years.
In fact, this is such a drawback that I (Demi Marie Obenour) considered not making this RFC at all. Rust language features (as opposed to library features) should work everywhere. That this does not is unfortunate.
Proper tail calls can make debugging difficult, by overwriting stack frames.
Proper tail calls are not necessary. Rust has done fine without them, and will continue to do so if this RFC is not accepted.
Is there a way to emulate proper tail calls when compiling to targets that don't support them? Is it possible to eliminate the overhead imposed on every non-tail call?
This RFC is licensed under the same terms as Rust itself.