Auto merge of #151065 - nagisa:add-preserve-none-abi, r=petrochenkov

abi: add a rust-preserve-none calling convention

This is the conceptual opposite of the rust-cold calling convention and is particularly useful in combination with the new `explicit_tail_calls` feature.

For relatively tight loops implemented with tail calling (`become`) each of the function with the regular calling convention is still responsible for restoring the initial value of the preserved registers. So it is not unusual to end up with a situation where each step in the tail call loop is spilling and reloading registers, along the lines of:

    foo:
        push r12
        ; do things
        pop r12
        jmp next_step

This adds up quickly, especially when most of the clobberable registers are already used to pass arguments or other uses.

I was thinking of making the name of this ABI a little less LLVM-derived and more like a conceptual inverse of `rust-cold`, but could not come with a great name (`rust-cold` is itself not a great name: cold in what context? from which perspective? is it supposed to mean that the function is rarely called?)
This commit is contained in:
bors 2026-01-25 02:49:32 +00:00
commit 75963ce795
26 changed files with 241 additions and 5 deletions

View file

@ -27,6 +27,7 @@ pub enum CanonAbi {
C,
Rust,
RustCold,
RustPreserveNone,
/// An ABI that rustc does not know how to call or define.
Custom,
@ -54,7 +55,7 @@ pub enum CanonAbi {
impl CanonAbi {
pub fn is_rustic_abi(self) -> bool {
match self {
CanonAbi::Rust | CanonAbi::RustCold => true,
CanonAbi::Rust | CanonAbi::RustCold | CanonAbi::RustPreserveNone => true,
CanonAbi::C
| CanonAbi::Custom
| CanonAbi::Arm(_)
@ -74,6 +75,7 @@ impl fmt::Display for CanonAbi {
CanonAbi::C => ExternAbi::C { unwind: false },
CanonAbi::Rust => ExternAbi::Rust,
CanonAbi::RustCold => ExternAbi::RustCold,
CanonAbi::RustPreserveNone => ExternAbi::RustPreserveNone,
CanonAbi::Custom => ExternAbi::Custom,
CanonAbi::Arm(arm_call) => match arm_call {
ArmCall::Aapcs => ExternAbi::Aapcs { unwind: false },

View file

@ -42,6 +42,13 @@ pub enum ExternAbi {
/// in a platform-agnostic way.
RustInvalid,
/// Preserves no registers.
///
/// Note, that this ABI is not stable in the registers it uses, is intended as an optimization
/// and may fall-back to a more conservative calling convention if the backend does not support
/// forcing callers to save all registers.
RustPreserveNone,
/// Unstable impl detail that directly uses Rust types to describe the ABI to LLVM.
/// Even normally-compatible Rust types can become ABI-incompatible with this ABI!
Unadjusted,
@ -163,6 +170,7 @@ abi_impls! {
RustCall =><= "rust-call",
RustCold =><= "rust-cold",
RustInvalid =><= "rust-invalid",
RustPreserveNone =><= "rust-preserve-none",
Stdcall { unwind: false } =><= "stdcall",
Stdcall { unwind: true } =><= "stdcall-unwind",
System { unwind: false } =><= "system",
@ -243,7 +251,7 @@ impl ExternAbi {
/// - are subject to change between compiler versions
pub fn is_rustic_abi(self) -> bool {
use ExternAbi::*;
matches!(self, Rust | RustCall | RustCold)
matches!(self, Rust | RustCall | RustCold | RustPreserveNone)
}
/// Returns whether the ABI supports C variadics. This only controls whether we allow *imports*
@ -315,7 +323,8 @@ impl ExternAbi {
| Self::Thiscall { .. }
| Self::Vectorcall { .. }
| Self::SysV64 { .. }
| Self::Win64 { .. } => true,
| Self::Win64 { .. }
| Self::RustPreserveNone => true,
}
}
}

View file

@ -95,6 +95,11 @@ pub fn extern_abi_stability(abi: ExternAbi) -> Result<(), UnstableAbi> {
ExternAbi::RustCold => {
Err(UnstableAbi { abi, feature: sym::rust_cold_cc, explain: GateReason::Experimental })
}
ExternAbi::RustPreserveNone => Err(UnstableAbi {
abi,
feature: sym::rust_preserve_none_cc,
explain: GateReason::Experimental,
}),
ExternAbi::RustInvalid => {
Err(UnstableAbi { abi, feature: sym::rustc_attrs, explain: GateReason::ImplDetail })
}

View file

@ -400,6 +400,7 @@ impl<'a> AstValidator<'a> {
CanonAbi::C
| CanonAbi::Rust
| CanonAbi::RustCold
| CanonAbi::RustPreserveNone
| CanonAbi::Arm(_)
| CanonAbi::X86(_) => { /* nothing to check */ }

View file

@ -56,6 +56,9 @@ pub(crate) fn conv_to_call_conv(
CanonAbi::Rust | CanonAbi::C => default_call_conv,
CanonAbi::RustCold => CallConv::Cold,
// Cranelift doesn't currently have anything for this.
CanonAbi::RustPreserveNone => default_call_conv,
// Functions with this calling convention can only be called from assembly, but it is
// possible to declare an `extern "custom"` block, so the backend still needs a calling
// convention for declaring foreign functions.

View file

@ -243,6 +243,8 @@ impl<'gcc, 'tcx> FnAbiGccExt<'gcc, 'tcx> for FnAbi<'tcx, Ty<'tcx>> {
pub fn conv_to_fn_attribute<'gcc>(conv: CanonAbi, arch: &Arch) -> Option<FnAttribute<'gcc>> {
let attribute = match conv {
CanonAbi::C | CanonAbi::Rust => return None,
// gcc/gccjit does not have anything for this.
CanonAbi::RustPreserveNone => return None,
CanonAbi::RustCold => FnAttribute::Cold,
// Functions with this calling convention can only be called from assembly, but it is
// possible to declare an `extern "custom"` block, so the backend still needs a calling

View file

@ -694,6 +694,10 @@ pub(crate) fn to_llvm_calling_convention(sess: &Session, abi: CanonAbi) -> llvm:
match abi {
CanonAbi::C | CanonAbi::Rust => llvm::CCallConv,
CanonAbi::RustCold => llvm::PreserveMost,
CanonAbi::RustPreserveNone => match &sess.target.arch {
Arch::X86_64 | Arch::AArch64 => llvm::PreserveNone,
_ => llvm::CCallConv,
},
// Functions with this calling convention can only be called from assembly, but it is
// possible to declare an `extern "custom"` block, so the backend still needs a calling
// convention for declaring foreign functions.

View file

@ -370,7 +370,7 @@ fn create_alloc_family_attr(llcx: &llvm::Context) -> &llvm::Attribute {
llvm::CreateAttrStringValue(llcx, "alloc-family", "__rust_alloc")
}
/// Helper for `FnAbi::apply_attrs_llfn`:
/// Helper for `FnAbiLlvmExt::apply_attrs_llfn`:
/// Composite function which sets LLVM attributes for function depending on its AST (`#[attribute]`)
/// attributes.
pub(crate) fn llfn_attrs_from_instance<'ll, 'tcx>(

View file

@ -167,6 +167,7 @@ pub(crate) enum CallConv {
PreserveMost = 14,
PreserveAll = 15,
Tail = 18,
PreserveNone = 21,
X86StdcallCallConv = 64,
X86FastcallCallConv = 65,
ArmAapcsCallConv = 67,

View file

@ -634,6 +634,8 @@ declare_features! (
(unstable, rtm_target_feature, "1.35.0", Some(150258)),
/// Allows `extern "rust-cold"`.
(unstable, rust_cold_cc, "1.63.0", Some(97544)),
/// Allows `extern "rust-preserve-none"`.
(unstable, rust_preserve_none_cc, "CURRENT_RUSTC_VERSION", Some(151401)),
/// Target features on s390x.
(unstable, s390x_target_feature, "1.82.0", Some(150259)),
/// Allows the use of the `sanitize` attribute.

View file

@ -188,6 +188,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
CanonAbi::C
| CanonAbi::Rust
| CanonAbi::RustCold
| CanonAbi::RustPreserveNone
| CanonAbi::Arm(_)
| CanonAbi::X86(_) => {}
}

View file

@ -1288,7 +1288,7 @@ pub fn fn_can_unwind(tcx: TyCtxt<'_>, fn_def_id: Option<DefId>, abi: ExternAbi)
| RiscvInterruptS
| RustInvalid
| Unadjusted => false,
Rust | RustCall | RustCold => tcx.sess.panic_strategy().unwinds(),
Rust | RustCall | RustCold | RustPreserveNone => tcx.sess.panic_strategy().unwinds(),
}
}

View file

@ -432,6 +432,7 @@ pub enum CallConvention {
Cold,
PreserveMost,
PreserveAll,
PreserveNone,
Custom,

View file

@ -1139,6 +1139,7 @@ pub enum Abi {
RustCold,
RiscvInterruptM,
RiscvInterruptS,
RustPreserveNone,
RustInvalid,
Custom,
}

View file

@ -615,6 +615,7 @@ impl RustcInternal for Abi {
Abi::RustInvalid => rustc_abi::ExternAbi::RustInvalid,
Abi::RiscvInterruptM => rustc_abi::ExternAbi::RiscvInterruptM,
Abi::RiscvInterruptS => rustc_abi::ExternAbi::RiscvInterruptS,
Abi::RustPreserveNone => rustc_abi::ExternAbi::RustPreserveNone,
Abi::Custom => rustc_abi::ExternAbi::Custom,
}
}

View file

@ -123,6 +123,7 @@ impl<'tcx> Stable<'tcx> for CanonAbi {
CanonAbi::C => CallConvention::C,
CanonAbi::Rust => CallConvention::Rust,
CanonAbi::RustCold => CallConvention::Cold,
CanonAbi::RustPreserveNone => CallConvention::PreserveNone,
CanonAbi::Custom => CallConvention::Custom,
CanonAbi::Arm(arm_call) => match arm_call {
ArmCall::Aapcs => CallConvention::ArmAapcs,

View file

@ -1020,6 +1020,7 @@ impl<'tcx> Stable<'tcx> for rustc_abi::ExternAbi {
ExternAbi::RustCall => Abi::RustCall,
ExternAbi::Unadjusted => Abi::Unadjusted,
ExternAbi::RustCold => Abi::RustCold,
ExternAbi::RustPreserveNone => Abi::RustPreserveNone,
ExternAbi::RustInvalid => Abi::RustInvalid,
ExternAbi::RiscvInterruptM => Abi::RiscvInterruptM,
ExternAbi::RiscvInterruptS => Abi::RiscvInterruptS,

View file

@ -1918,6 +1918,7 @@ symbols! {
rust_future,
rust_logo,
rust_out,
rust_preserve_none_cc,
rustc,
rustc_abi,
// FIXME(#82232, #143834): temporary name to mitigate `#[align]` nameres ambiguity

View file

@ -88,6 +88,7 @@ impl AbiMap {
(ExternAbi::RustCold, _) if self.os == OsKind::Windows => CanonAbi::Rust,
(ExternAbi::RustCold, _) => CanonAbi::RustCold,
(ExternAbi::RustPreserveNone, _) => CanonAbi::RustPreserveNone,
(ExternAbi::Custom, _) => CanonAbi::Custom,

View file

@ -230,6 +230,7 @@ pub enum FnAbi {
Win64,
Win64Unwind,
X86Interrupt,
RustPreserveNone,
Unknown,
}
@ -271,6 +272,7 @@ impl FnAbi {
s if *s == sym::riscv_dash_interrupt_dash_s => FnAbi::RiscvInterruptS,
s if *s == sym::rust_dash_call => FnAbi::RustCall,
s if *s == sym::rust_dash_cold => FnAbi::RustCold,
s if *s == sym::rust_dash_preserve_dash_none => FnAbi::RustPreserveNone,
s if *s == sym::rust_dash_intrinsic => FnAbi::RustIntrinsic,
s if *s == sym::Rust => FnAbi::Rust,
s if *s == sym::stdcall_dash_unwind => FnAbi::StdcallUnwind,
@ -314,6 +316,7 @@ impl FnAbi {
FnAbi::Rust => "Rust",
FnAbi::RustCall => "rust-call",
FnAbi::RustCold => "rust-cold",
FnAbi::RustPreserveNone => "rust-preserve-none",
FnAbi::RustIntrinsic => "rust-intrinsic",
FnAbi::Stdcall => "stdcall",
FnAbi::StdcallUnwind => "stdcall-unwind",

View file

@ -109,6 +109,7 @@ define_symbols! {
vectorcall_dash_unwind = "vectorcall-unwind",
win64_dash_unwind = "win64-unwind",
x86_dash_interrupt = "x86-interrupt",
rust_dash_preserve_dash_none = "preserve-none",
@PLAIN:
__ra_fixup,

View file

@ -0,0 +1,33 @@
//@ add-minicore
//@ revisions: X86 AARCH64 UNSUPPORTED
//@ [X86] compile-flags: -C no-prepopulate-passes --target=x86_64-unknown-linux-gnu
//@ [X86] needs-llvm-components: x86
//@ [AARCH64] compile-flags: -C no-prepopulate-passes --target=aarch64-unknown-linux-gnu
//@ [AARCH64] needs-llvm-components: aarch64
//@ [UNSUPPORTED] compile-flags: -C no-prepopulate-passes --target=i686-unknown-linux-gnu
//@ [UNSUPPORTED] needs-llvm-components: x86
#![crate_type = "lib"]
#![feature(rust_preserve_none_cc)]
#![feature(no_core, lang_items)]
#![no_core]
extern crate minicore;
// X86: define{{( dso_local)?}} preserve_nonecc void @peach(i16
// AARCH64: define{{( dso_local)?}} preserve_nonecc void @peach(i16
// UNSUPPORTED: define{{( dso_local)?}} void @peach(i16
#[no_mangle]
#[inline(never)]
pub extern "rust-preserve-none" fn peach(x: u16) {
loop {}
}
// X86: call preserve_nonecc void @peach(i16
// AARCH64: call preserve_nonecc void @peach(i16
// UNSUPPORTED: call void @peach(i16
pub fn quince(x: u16) {
if let 12345u16 = x {
peach(54321);
}
}

View file

@ -0,0 +1,67 @@
//@ run-pass
//@ needs-unwind
#![feature(rust_preserve_none_cc)]
struct CrateOf<'a> {
mcintosh: f64,
golden_delicious: u64,
jonagold: Option<&'a u64>,
rome: [u64; 12],
}
#[inline(never)]
extern "rust-preserve-none" fn oven_explosion() {
panic!("bad time");
}
#[inline(never)]
fn bite_into(yummy: u64) -> u64 {
let did_it_actually = std::panic::catch_unwind(move || {
oven_explosion()
});
assert!(did_it_actually.is_err());
yummy - 25
}
#[inline(never)]
extern "rust-preserve-none" fn lotsa_apples(
honeycrisp: u64,
gala: u32,
fuji: f64,
granny_smith: &[u64],
pink_lady: (),
and_a: CrateOf<'static>,
cosmic_crisp: u64,
ambrosia: f64,
winesap: &[u64],
) -> (u64, f64, u64, u64) {
assert_eq!(honeycrisp, 220);
assert_eq!(gala, 140);
assert_eq!(fuji, 210.54201234);
assert_eq!(granny_smith, &[180, 210]);
assert_eq!(pink_lady, ());
assert_eq!(and_a.mcintosh, 150.0);
assert_eq!(and_a.golden_delicious, 185);
assert_eq!(and_a.jonagold, None); // my scales can't weight these gargantuans.
assert_eq!(and_a.rome, [180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202]);
assert_eq!(cosmic_crisp, 270);
assert_eq!(ambrosia, 193.1);
assert_eq!(winesap, &[]);
(
and_a.rome.iter().sum(),
fuji + ambrosia,
cosmic_crisp - honeycrisp,
bite_into(and_a.golden_delicious)
)
}
fn main() {
let pie = lotsa_apples(220, 140, 210.54201234, &[180, 210], (), CrateOf {
mcintosh: 150.0,
golden_delicious: 185,
jonagold: None,
rome: [180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202]
}, 270, 193.1, &[]);
assert_eq!(pie, (2292, 403.64201234, 50, 160));
}

View file

@ -0,0 +1,21 @@
#![crate_type = "lib"]
extern "rust-preserve-none" fn apple() {} //~ ERROR "rust-preserve-none" ABI is experimental
trait T {
extern "rust-preserve-none" fn banana(); //~ ERROR "rust-preserve-none" ABI is experimental
extern "rust-preserve-none" fn citrus() {} //~ ERROR "rust-preserve-none" ABI is experimental
}
struct S;
impl T for S {
extern "rust-preserve-none" fn banana() {} //~ ERROR "rust-preserve-none" ABI is experimental
}
impl S {
extern "rust-preserve-none" fn durian() {} //~ ERROR "rust-preserve-none" ABI is experimental
}
type Fig = extern "rust-preserve-none" fn(); //~ ERROR "rust-preserve-none" ABI is experimental
extern "rust-preserve-none" {} //~ ERROR "rust-preserve-none" ABI is experimental

View file

@ -0,0 +1,73 @@
error[E0658]: the extern "rust-preserve-none" ABI is experimental and subject to change
--> $DIR/feature-gate-rust-preserve-none-cc.rs:3:8
|
LL | extern "rust-preserve-none" fn apple() {}
| ^^^^^^^^^^^^^^^^^^^^
|
= note: see issue #151401 <https://github.com/rust-lang/rust/issues/151401> for more information
= help: add `#![feature(rust_preserve_none_cc)]` to the crate attributes to enable
= note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
error[E0658]: the extern "rust-preserve-none" ABI is experimental and subject to change
--> $DIR/feature-gate-rust-preserve-none-cc.rs:6:12
|
LL | extern "rust-preserve-none" fn banana();
| ^^^^^^^^^^^^^^^^^^^^
|
= note: see issue #151401 <https://github.com/rust-lang/rust/issues/151401> for more information
= help: add `#![feature(rust_preserve_none_cc)]` to the crate attributes to enable
= note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
error[E0658]: the extern "rust-preserve-none" ABI is experimental and subject to change
--> $DIR/feature-gate-rust-preserve-none-cc.rs:7:12
|
LL | extern "rust-preserve-none" fn citrus() {}
| ^^^^^^^^^^^^^^^^^^^^
|
= note: see issue #151401 <https://github.com/rust-lang/rust/issues/151401> for more information
= help: add `#![feature(rust_preserve_none_cc)]` to the crate attributes to enable
= note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
error[E0658]: the extern "rust-preserve-none" ABI is experimental and subject to change
--> $DIR/feature-gate-rust-preserve-none-cc.rs:12:12
|
LL | extern "rust-preserve-none" fn banana() {}
| ^^^^^^^^^^^^^^^^^^^^
|
= note: see issue #151401 <https://github.com/rust-lang/rust/issues/151401> for more information
= help: add `#![feature(rust_preserve_none_cc)]` to the crate attributes to enable
= note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
error[E0658]: the extern "rust-preserve-none" ABI is experimental and subject to change
--> $DIR/feature-gate-rust-preserve-none-cc.rs:16:12
|
LL | extern "rust-preserve-none" fn durian() {}
| ^^^^^^^^^^^^^^^^^^^^
|
= note: see issue #151401 <https://github.com/rust-lang/rust/issues/151401> for more information
= help: add `#![feature(rust_preserve_none_cc)]` to the crate attributes to enable
= note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
error[E0658]: the extern "rust-preserve-none" ABI is experimental and subject to change
--> $DIR/feature-gate-rust-preserve-none-cc.rs:19:19
|
LL | type Fig = extern "rust-preserve-none" fn();
| ^^^^^^^^^^^^^^^^^^^^
|
= note: see issue #151401 <https://github.com/rust-lang/rust/issues/151401> for more information
= help: add `#![feature(rust_preserve_none_cc)]` to the crate attributes to enable
= note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
error[E0658]: the extern "rust-preserve-none" ABI is experimental and subject to change
--> $DIR/feature-gate-rust-preserve-none-cc.rs:21:8
|
LL | extern "rust-preserve-none" {}
| ^^^^^^^^^^^^^^^^^^^^
|
= note: see issue #151401 <https://github.com/rust-lang/rust/issues/151401> for more information
= help: add `#![feature(rust_preserve_none_cc)]` to the crate attributes to enable
= note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
error: aborting due to 7 previous errors
For more information about this error, try `rustc --explain E0658`.

View file

@ -21,6 +21,7 @@ riscv-interrupt-s
rust-call
rust-cold
rust-invalid
rust-preserve-none
stdcall
stdcall-unwind
system