explicit tail calls: support indirect arguments#151143
Merged
rust-bors[bot] merged 2 commits intorust-lang:mainfrom Feb 28, 2026
Merged
explicit tail calls: support indirect arguments#151143rust-bors[bot] merged 2 commits intorust-lang:mainfrom
rust-bors[bot] merged 2 commits intorust-lang:mainfrom
Conversation
8a1cd78 to
6f7eede
Compare
folkertdev
commented
Jan 14, 2026
Comment on lines
+1270
to
+1279
| PassMode::Indirect { on_stack: true, .. } => { | ||
| // FIXME: some LLVM backends (notably x86) do not correctly pass byval | ||
| // arguments to tail calls (as of LLVM 21). See also: | ||
| // | ||
| // - https://github.com/rust-lang/rust/pull/144232#discussion_r2218543841 | ||
| // - https://github.com/rust-lang/rust/issues/144855 | ||
| span_bug!( | ||
| fn_span, | ||
| "arguments using PassMode::Indirect {{ on_stack: true, .. }} are currently not supported for tail calls" | ||
| ) |
Contributor
Author
There was a problem hiding this comment.
I've kept the span_bug! for now.
It looks like a fix for x86 might get cherry-picked into LLVM 22. If so, I think that is enough support to allow this variant too. At that point riscv would be the next most commonly used target that would miscompile.
This comment has been minimized.
This comment has been minimized.
6f7eede to
a904f16
Compare
This comment has been minimized.
This comment has been minimized.
a904f16 to
fc79542
Compare
This comment has been minimized.
This comment has been minimized.
029786e to
414b969
Compare
Contributor
Author
|
@bors try=x86_64-msvc-1,x86_64-msvc-2 |
Contributor
|
Unknown command "try". Run |
Contributor
Author
|
@bors try job=x86_64-msvc-1,x86_64-msvc-2 |
This comment has been minimized.
This comment has been minimized.
rust-bors Bot
pushed a commit
that referenced
this pull request
Jan 15, 2026
explicit tail calls: support indirect arguments try-job: x86_64-msvc-1 try-job: x86_64-msvc-2
This comment has been minimized.
This comment has been minimized.
Contributor
|
💔 Test for e8cb9bb failed: CI. Failed job:
|
414b969 to
33942c6
Compare
Contributor
Author
|
@bors try job=x86_64-msvc-1,x86_64-msvc-2 |
This comment has been minimized.
This comment has been minimized.
rust-bors Bot
pushed a commit
that referenced
this pull request
Jan 15, 2026
explicit tail calls: support indirect arguments try-job: x86_64-msvc-1 try-job: x86_64-msvc-2
Contributor
33942c6 to
2e004e1
Compare
Contributor
Author
|
This seems to work, though we should probably run a bunch of try jobs once the code looks good. |
Collaborator
|
|
This comment has been minimized.
This comment has been minimized.
rust-bors Bot
pushed a commit
that referenced
this pull request
Feb 27, 2026
explicit tail calls: support indirect arguments try-job: test-various
This comment has been minimized.
This comment has been minimized.
106ccdb to
e6cf5a2
Compare
Contributor
Author
|
@bors try job=test-various |
This comment has been minimized.
This comment has been minimized.
rust-bors Bot
pushed a commit
that referenced
this pull request
Feb 27, 2026
explicit tail calls: support indirect arguments try-job: test-various
Contributor
Contributor
Author
|
@bors r=WaffleLapkin |
Contributor
JonathanBrouwer
added a commit
to JonathanBrouwer/rust
that referenced
this pull request
Feb 27, 2026
…affleLapkin explicit tail calls: support indirect arguments tracking issue: rust-lang#112788 After discussion in rust-lang#144855, I was wrong and it is actually possible to support tail calls with `PassMode::Indirect { on_stack: false, .. }` arguments. Normally an indirect argument with `on_stack: false` would be passed as a pointer into the caller's stack frame. For tail calls, that would be unsound, because the caller's stack frame is overwritten by the callee's stack frame. Therefore we store the argument for the callee in the corresponding caller's slot. Because guaranteed tail calls demand that the caller's signature matches the callee's, the corresponding slot has the correct type. To handle cases like the one below, the tail call arguments must first be copied to a temporary, and can only then be copied to the caller's argument slots. ```rust // A struct big enough that it is not passed via registers. pub struct Big([u64; 4]); fn swapper(a: Big, b: Big) -> (Big, Big) { become swapper_helper(b, a); } ``` --- I'm not really familiar with MIR and what tricks/helpers we have, so I'm just cobbling this together. Hopefully we can arrive at something more elegant.
This was referenced Feb 27, 2026
rust-bors Bot
pushed a commit
that referenced
this pull request
Feb 27, 2026
…uwer Rollup of 12 pull requests Successful merges: - #151143 (explicit tail calls: support indirect arguments) - #153012 (Stop using `LinkedGraph` in `lexical_region_resolve`) - #153175 (Clarify a confusing green-path function) - #153179 (Force a CI LLVM stamp bump) - #150828 (Improved security section in rustdoc for `current_exe`) - #152673 (rustc_public: rewrite `bridge_impl` to reduce boilerplate) - #152674 (rustc_public: remove the `CrateDefItems` trait) - #153073 (Fix mem::conjure_zst panic message to use any::type_name instead) - #153117 (Remove mutation from macro path URL construction) - #153128 (Recover feature lang_items for emscripten) - #153138 (Print path root when printing path) - #153159 (Work around a false `err.emit();` type error in rust-analyzer)
rust-bors Bot
pushed a commit
that referenced
this pull request
Feb 27, 2026
…uwer Rollup of 12 pull requests Successful merges: - #151143 (explicit tail calls: support indirect arguments) - #153012 (Stop using `LinkedGraph` in `lexical_region_resolve`) - #153175 (Clarify a confusing green-path function) - #153179 (Force a CI LLVM stamp bump) - #150828 (Improved security section in rustdoc for `current_exe`) - #152673 (rustc_public: rewrite `bridge_impl` to reduce boilerplate) - #152674 (rustc_public: remove the `CrateDefItems` trait) - #153073 (Fix mem::conjure_zst panic message to use any::type_name instead) - #153117 (Remove mutation from macro path URL construction) - #153128 (Recover feature lang_items for emscripten) - #153138 (Print path root when printing path) - #153159 (Work around a false `err.emit();` type error in rust-analyzer)
rust-timer
added a commit
that referenced
this pull request
Feb 28, 2026
Rollup merge of #151143 - folkertdev:tail-call-indirect, r=WaffleLapkin explicit tail calls: support indirect arguments tracking issue: #112788 After discussion in #144855, I was wrong and it is actually possible to support tail calls with `PassMode::Indirect { on_stack: false, .. }` arguments. Normally an indirect argument with `on_stack: false` would be passed as a pointer into the caller's stack frame. For tail calls, that would be unsound, because the caller's stack frame is overwritten by the callee's stack frame. Therefore we store the argument for the callee in the corresponding caller's slot. Because guaranteed tail calls demand that the caller's signature matches the callee's, the corresponding slot has the correct type. To handle cases like the one below, the tail call arguments must first be copied to a temporary, and can only then be copied to the caller's argument slots. ```rust // A struct big enough that it is not passed via registers. pub struct Big([u64; 4]); fn swapper(a: Big, b: Big) -> (Big, Big) { become swapper_helper(b, a); } ``` --- I'm not really familiar with MIR and what tricks/helpers we have, so I'm just cobbling this together. Hopefully we can arrive at something more elegant.
makai410
pushed a commit
to makai410/rustc_public
that referenced
this pull request
Mar 19, 2026
…uwer Rollup of 12 pull requests Successful merges: - rust-lang/rust#151143 (explicit tail calls: support indirect arguments) - rust-lang/rust#153012 (Stop using `LinkedGraph` in `lexical_region_resolve`) - rust-lang/rust#153175 (Clarify a confusing green-path function) - rust-lang/rust#153179 (Force a CI LLVM stamp bump) - rust-lang/rust#150828 (Improved security section in rustdoc for `current_exe`) - rust-lang/rust#152673 (rustc_public: rewrite `bridge_impl` to reduce boilerplate) - rust-lang/rust#152674 (rustc_public: remove the `CrateDefItems` trait) - rust-lang/rust#153073 (Fix mem::conjure_zst panic message to use any::type_name instead) - rust-lang/rust#153117 (Remove mutation from macro path URL construction) - rust-lang/rust#153128 (Recover feature lang_items for emscripten) - rust-lang/rust#153138 (Print path root when printing path) - rust-lang/rust#153159 (Work around a false `err.emit();` type error in rust-analyzer)
makai410
pushed a commit
to makai410/rustc_public
that referenced
this pull request
Mar 19, 2026
…uwer Rollup of 12 pull requests Successful merges: - rust-lang/rust#151143 (explicit tail calls: support indirect arguments) - rust-lang/rust#153012 (Stop using `LinkedGraph` in `lexical_region_resolve`) - rust-lang/rust#153175 (Clarify a confusing green-path function) - rust-lang/rust#153179 (Force a CI LLVM stamp bump) - rust-lang/rust#150828 (Improved security section in rustdoc for `current_exe`) - rust-lang/rust#152673 (rustc_public: rewrite `bridge_impl` to reduce boilerplate) - rust-lang/rust#152674 (rustc_public: remove the `CrateDefItems` trait) - rust-lang/rust#153073 (Fix mem::conjure_zst panic message to use any::type_name instead) - rust-lang/rust#153117 (Remove mutation from macro path URL construction) - rust-lang/rust#153128 (Recover feature lang_items for emscripten) - rust-lang/rust#153138 (Print path root when printing path) - rust-lang/rust#153159 (Work around a false `err.emit();` type error in rust-analyzer)
makai410
pushed a commit
to makai410/rustc_public
that referenced
this pull request
Mar 27, 2026
…uwer Rollup of 12 pull requests Successful merges: - rust-lang/rust#151143 (explicit tail calls: support indirect arguments) - rust-lang/rust#153012 (Stop using `LinkedGraph` in `lexical_region_resolve`) - rust-lang/rust#153175 (Clarify a confusing green-path function) - rust-lang/rust#153179 (Force a CI LLVM stamp bump) - rust-lang/rust#150828 (Improved security section in rustdoc for `current_exe`) - rust-lang/rust#152673 (rustc_public: rewrite `bridge_impl` to reduce boilerplate) - rust-lang/rust#152674 (rustc_public: remove the `CrateDefItems` trait) - rust-lang/rust#153073 (Fix mem::conjure_zst panic message to use any::type_name instead) - rust-lang/rust#153117 (Remove mutation from macro path URL construction) - rust-lang/rust#153128 (Recover feature lang_items for emscripten) - rust-lang/rust#153138 (Print path root when printing path) - rust-lang/rust#153159 (Work around a false `err.emit();` type error in rust-analyzer)
rust-bors Bot
pushed a commit
that referenced
this pull request
Apr 22, 2026
codegen: Copy to an alloca when the argument is neither by-val nor by-move for indirect pointer. Fixes #155241. When a value is passed via an indirect pointer, the value needs to be copied to a new alloca. For x86_64-unknown-linux-gnu, `Thing` is the case: ```rust #[derive(Clone, Copy)] struct Thing(usize, usize, usize); pub fn foo() { let thing = Thing(0, 0, 0); bar(thing); assert_eq!(thing.0, 0); } #[inline(never)] #[unsafe(no_mangle)] pub fn bar(mut thing: Thing) { thing.0 = 1; } ``` Before passing the thing to the bar function, the thing needs to be copied to an alloca that is passed to bar. ```llvm %0 = alloca [24 x i8], align 8 call void @llvm.memcpy.p0.p0.i64(ptr align 8 %0, ptr align 8 %thing, i64 24, i1 false) call void @bar(ptr %0) ``` This patch applies the rule to the untupled arguments as well. ```rust #![feature(fn_traits)] #[derive(Clone, Copy)] struct Thing(usize, usize, usize); #[inline(never)] #[unsafe(no_mangle)] pub fn foo() { let thing = (Thing(0, 0, 0),); (|mut thing: Thing| { thing.0 = 1; }).call(thing); assert_eq!(thing.0.0, 0); } ``` For this case, this patch changes from ```llvm ; call example::foo::{closure#0} call void @_RNCNvCs15qdZVLwHPA_7example3foo0B3_(ptr ..., ptr %thing) ``` to ```llvm %0 = alloca [24 x i8], align 8 call void @llvm.memcpy.p0.p0.i64(ptr align 8 %0, ptr align 8 %thing, i64 24, i1 false) ; call example::foo::{closure#0} call void @_RNCNvCs15qdZVLwHPA_7example3foo0B3_(ptr ..., ptr %0) ``` However, the same rule cannot be applied to tail calls that would be unsound, because the caller's stack frame is overwritten by the callee's stack frame. Fortunately, #151143 has already handled the special case. We must not copy again. No copy is needed for by-move arguments, because the argument is passed to the called "in-place". No copy is also needed for by-val arguments, because the attribute implies that a hidden copy of the pointee is made between the caller and the callee. NOTE: The patch has a trick for tail calls that we pass by-move. We can choose to copy an alloca even for by-move arguments, but tail calls require MUST-by-move.
pull Bot
pushed a commit
to LeeeeeeM/miri
that referenced
this pull request
Apr 24, 2026
codegen: Copy to an alloca when the argument is neither by-val nor by-move for indirect pointer. Fixes rust-lang/rust#155241. When a value is passed via an indirect pointer, the value needs to be copied to a new alloca. For x86_64-unknown-linux-gnu, `Thing` is the case: ```rust #[derive(Clone, Copy)] struct Thing(usize, usize, usize); pub fn foo() { let thing = Thing(0, 0, 0); bar(thing); assert_eq!(thing.0, 0); } #[inline(never)] #[unsafe(no_mangle)] pub fn bar(mut thing: Thing) { thing.0 = 1; } ``` Before passing the thing to the bar function, the thing needs to be copied to an alloca that is passed to bar. ```llvm %0 = alloca [24 x i8], align 8 call void @llvm.memcpy.p0.p0.i64(ptr align 8 %0, ptr align 8 %thing, i64 24, i1 false) call void @bar(ptr %0) ``` This patch applies the rule to the untupled arguments as well. ```rust #![feature(fn_traits)] #[derive(Clone, Copy)] struct Thing(usize, usize, usize); #[inline(never)] #[unsafe(no_mangle)] pub fn foo() { let thing = (Thing(0, 0, 0),); (|mut thing: Thing| { thing.0 = 1; }).call(thing); assert_eq!(thing.0.0, 0); } ``` For this case, this patch changes from ```llvm ; call example::foo::{closure#0} call void @_RNCNvCs15qdZVLwHPA_7example3foo0B3_(ptr ..., ptr %thing) ``` to ```llvm %0 = alloca [24 x i8], align 8 call void @llvm.memcpy.p0.p0.i64(ptr align 8 %0, ptr align 8 %thing, i64 24, i1 false) ; call example::foo::{closure#0} call void @_RNCNvCs15qdZVLwHPA_7example3foo0B3_(ptr ..., ptr %0) ``` However, the same rule cannot be applied to tail calls that would be unsound, because the caller's stack frame is overwritten by the callee's stack frame. Fortunately, rust-lang/rust#151143 has already handled the special case. We must not copy again. No copy is needed for by-move arguments, because the argument is passed to the called "in-place". No copy is also needed for by-val arguments, because the attribute implies that a hidden copy of the pointee is made between the caller and the callee. NOTE: The patch has a trick for tail calls that we pass by-move. We can choose to copy an alloca even for by-move arguments, but tail calls require MUST-by-move.
github-actions Bot
pushed a commit
to rust-lang/rustc-dev-guide
that referenced
this pull request
Apr 24, 2026
codegen: Copy to an alloca when the argument is neither by-val nor by-move for indirect pointer. Fixes rust-lang/rust#155241. When a value is passed via an indirect pointer, the value needs to be copied to a new alloca. For x86_64-unknown-linux-gnu, `Thing` is the case: ```rust #[derive(Clone, Copy)] struct Thing(usize, usize, usize); pub fn foo() { let thing = Thing(0, 0, 0); bar(thing); assert_eq!(thing.0, 0); } #[inline(never)] #[unsafe(no_mangle)] pub fn bar(mut thing: Thing) { thing.0 = 1; } ``` Before passing the thing to the bar function, the thing needs to be copied to an alloca that is passed to bar. ```llvm %0 = alloca [24 x i8], align 8 call void @llvm.memcpy.p0.p0.i64(ptr align 8 %0, ptr align 8 %thing, i64 24, i1 false) call void @bar(ptr %0) ``` This patch applies the rule to the untupled arguments as well. ```rust #![feature(fn_traits)] #[derive(Clone, Copy)] struct Thing(usize, usize, usize); #[inline(never)] #[unsafe(no_mangle)] pub fn foo() { let thing = (Thing(0, 0, 0),); (|mut thing: Thing| { thing.0 = 1; }).call(thing); assert_eq!(thing.0.0, 0); } ``` For this case, this patch changes from ```llvm ; call example::foo::{closure#0} call void @_RNCNvCs15qdZVLwHPA_7example3foo0B3_(ptr ..., ptr %thing) ``` to ```llvm %0 = alloca [24 x i8], align 8 call void @llvm.memcpy.p0.p0.i64(ptr align 8 %0, ptr align 8 %thing, i64 24, i1 false) ; call example::foo::{closure#0} call void @_RNCNvCs15qdZVLwHPA_7example3foo0B3_(ptr ..., ptr %0) ``` However, the same rule cannot be applied to tail calls that would be unsound, because the caller's stack frame is overwritten by the callee's stack frame. Fortunately, rust-lang/rust#151143 has already handled the special case. We must not copy again. No copy is needed for by-move arguments, because the argument is passed to the called "in-place". No copy is also needed for by-val arguments, because the attribute implies that a hidden copy of the pointee is made between the caller and the callee. NOTE: The patch has a trick for tail calls that we pass by-move. We can choose to copy an alloca even for by-move arguments, but tail calls require MUST-by-move.
github-actions Bot
pushed a commit
to rust-lang/stdarch
that referenced
this pull request
May 4, 2026
codegen: Copy to an alloca when the argument is neither by-val nor by-move for indirect pointer. Fixes rust-lang/rust#155241. When a value is passed via an indirect pointer, the value needs to be copied to a new alloca. For x86_64-unknown-linux-gnu, `Thing` is the case: ```rust #[derive(Clone, Copy)] struct Thing(usize, usize, usize); pub fn foo() { let thing = Thing(0, 0, 0); bar(thing); assert_eq!(thing.0, 0); } #[inline(never)] #[unsafe(no_mangle)] pub fn bar(mut thing: Thing) { thing.0 = 1; } ``` Before passing the thing to the bar function, the thing needs to be copied to an alloca that is passed to bar. ```llvm %0 = alloca [24 x i8], align 8 call void @llvm.memcpy.p0.p0.i64(ptr align 8 %0, ptr align 8 %thing, i64 24, i1 false) call void @bar(ptr %0) ``` This patch applies the rule to the untupled arguments as well. ```rust #![feature(fn_traits)] #[derive(Clone, Copy)] struct Thing(usize, usize, usize); #[inline(never)] #[unsafe(no_mangle)] pub fn foo() { let thing = (Thing(0, 0, 0),); (|mut thing: Thing| { thing.0 = 1; }).call(thing); assert_eq!(thing.0.0, 0); } ``` For this case, this patch changes from ```llvm ; call example::foo::{closure#0} call void @_RNCNvCs15qdZVLwHPA_7example3foo0B3_(ptr ..., ptr %thing) ``` to ```llvm %0 = alloca [24 x i8], align 8 call void @llvm.memcpy.p0.p0.i64(ptr align 8 %0, ptr align 8 %thing, i64 24, i1 false) ; call example::foo::{closure#0} call void @_RNCNvCs15qdZVLwHPA_7example3foo0B3_(ptr ..., ptr %0) ``` However, the same rule cannot be applied to tail calls that would be unsound, because the caller's stack frame is overwritten by the callee's stack frame. Fortunately, rust-lang/rust#151143 has already handled the special case. We must not copy again. No copy is needed for by-move arguments, because the argument is passed to the called "in-place". No copy is also needed for by-val arguments, because the attribute implies that a hidden copy of the pointee is made between the caller and the callee. NOTE: The patch has a trick for tail calls that we pass by-move. We can choose to copy an alloca even for by-move arguments, but tail calls require MUST-by-move.
pull Bot
pushed a commit
to Kokoro2336/rust-analyzer
that referenced
this pull request
May 4, 2026
codegen: Copy to an alloca when the argument is neither by-val nor by-move for indirect pointer. Fixes rust-lang/rust#155241. When a value is passed via an indirect pointer, the value needs to be copied to a new alloca. For x86_64-unknown-linux-gnu, `Thing` is the case: ```rust #[derive(Clone, Copy)] struct Thing(usize, usize, usize); pub fn foo() { let thing = Thing(0, 0, 0); bar(thing); assert_eq!(thing.0, 0); } #[inline(never)] #[unsafe(no_mangle)] pub fn bar(mut thing: Thing) { thing.0 = 1; } ``` Before passing the thing to the bar function, the thing needs to be copied to an alloca that is passed to bar. ```llvm %0 = alloca [24 x i8], align 8 call void @llvm.memcpy.p0.p0.i64(ptr align 8 %0, ptr align 8 %thing, i64 24, i1 false) call void @bar(ptr %0) ``` This patch applies the rule to the untupled arguments as well. ```rust #![feature(fn_traits)] #[derive(Clone, Copy)] struct Thing(usize, usize, usize); #[inline(never)] #[unsafe(no_mangle)] pub fn foo() { let thing = (Thing(0, 0, 0),); (|mut thing: Thing| { thing.0 = 1; }).call(thing); assert_eq!(thing.0.0, 0); } ``` For this case, this patch changes from ```llvm ; call example::foo::{closure#0} call void @_RNCNvCs15qdZVLwHPA_7example3foo0B3_(ptr ..., ptr %thing) ``` to ```llvm %0 = alloca [24 x i8], align 8 call void @llvm.memcpy.p0.p0.i64(ptr align 8 %0, ptr align 8 %thing, i64 24, i1 false) ; call example::foo::{closure#0} call void @_RNCNvCs15qdZVLwHPA_7example3foo0B3_(ptr ..., ptr %0) ``` However, the same rule cannot be applied to tail calls that would be unsound, because the caller's stack frame is overwritten by the callee's stack frame. Fortunately, rust-lang/rust#151143 has already handled the special case. We must not copy again. No copy is needed for by-move arguments, because the argument is passed to the called "in-place". No copy is also needed for by-val arguments, because the attribute implies that a hidden copy of the pointee is made between the caller and the callee. NOTE: The patch has a trick for tail calls that we pass by-move. We can choose to copy an alloca even for by-move arguments, but tail calls require MUST-by-move.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
View all comments
tracking issue: #112788
After discussion in #144855, I was wrong and it is actually possible to support tail calls with
PassMode::Indirect { on_stack: false, .. }arguments.Normally an indirect argument with
on_stack: falsewould be passed as a pointer into the caller's stack frame. For tail calls, that would be unsound, because the caller's stack frame is overwritten by the callee's stack frame.Therefore we store the argument for the callee in the corresponding caller's slot. Because guaranteed tail calls demand that the caller's signature matches the callee's, the corresponding slot has the correct type.
To handle cases like the one below, the tail call arguments must first be copied to a temporary, and can only then be copied to the caller's argument slots.
I'm not really familiar with MIR and what tricks/helpers we have, so I'm just cobbling this together. Hopefully we can arrive at something more elegant.