about summary refs log tree commit diff
diff options
context:
space:
mode:
authorRalf Jung <post@ralfj.de>2025-05-23 08:07:42 +0200
committerRalf Jung <post@ralfj.de>2025-06-19 09:44:01 +0900
commitcd08652faa37a9119c2cf535b927129b1c4438b7 (patch)
treef81da7726f370e8b3a36b0016145c1062a9c5f9d
parentd70ec32ea7e03e3a082a45eeba6c8aa4c653efb4 (diff)
downloadrust-cd08652faa37a9119c2cf535b927129b1c4438b7.tar.gz
rust-cd08652faa37a9119c2cf535b927129b1c4438b7.zip
move -Ctarget-feature handling into shared code
-rw-r--r--compiler/rustc_codegen_gcc/messages.ftl16
-rw-r--r--compiler/rustc_codegen_gcc/src/errors.rs26
-rw-r--r--compiler/rustc_codegen_gcc/src/gcc_util.rs124
-rw-r--r--compiler/rustc_codegen_llvm/messages.ftl10
-rw-r--r--compiler/rustc_codegen_llvm/src/errors.rs26
-rw-r--r--compiler/rustc_codegen_llvm/src/llvm_util.rs160
-rw-r--r--compiler/rustc_codegen_ssa/messages.ftl19
-rw-r--r--compiler/rustc_codegen_ssa/src/codegen_attrs.rs26
-rw-r--r--compiler/rustc_codegen_ssa/src/errors.rs89
-rw-r--r--compiler/rustc_codegen_ssa/src/target_features.rs161
-rw-r--r--compiler/rustc_session/messages.ftl8
-rw-r--r--compiler/rustc_session/src/errors.rs17
-rw-r--r--compiler/rustc_session/src/features.rs59
-rw-r--r--compiler/rustc_session/src/lib.rs1
-rw-r--r--compiler/rustc_session/src/options.rs8
-rw-r--r--compiler/rustc_target/src/target_features.rs30
-rw-r--r--src/librustdoc/json/mod.rs5
-rw-r--r--tests/ui/check-cfg/target_feature.stderr3
18 files changed, 326 insertions, 462 deletions
diff --git a/compiler/rustc_codegen_gcc/messages.ftl b/compiler/rustc_codegen_gcc/messages.ftl
index 18a8a5a1e04..55a28bc9493 100644
--- a/compiler/rustc_codegen_gcc/messages.ftl
+++ b/compiler/rustc_codegen_gcc/messages.ftl
@@ -1,7 +1,3 @@
-codegen_gcc_unknown_ctarget_feature_prefix =
-    unknown feature specified for `-Ctarget-feature`: `{$feature}`
-    .note = features must begin with a `+` to enable or `-` to disable it
-
 codegen_gcc_unwinding_inline_asm =
     GCC backend does not support unwinding from inline asm
 
@@ -16,15 +12,3 @@ codegen_gcc_lto_disallowed = lto can only be run for executables, cdylibs and st
 codegen_gcc_lto_dylib = lto cannot be used for `dylib` crate type without `-Zdylib-lto`
 
 codegen_gcc_lto_bitcode_from_rlib = failed to get bitcode from object file for LTO ({$gcc_err})
-
-codegen_gcc_unknown_ctarget_feature =
-    unknown and unstable feature specified for `-Ctarget-feature`: `{$feature}`
-    .note = it is still passed through to the codegen backend, but use of this feature might be unsound and the behavior of this feature can change in the future
-    .possible_feature = you might have meant: `{$rust_feature}`
-    .consider_filing_feature_request = consider filing a feature request
-
-codegen_gcc_missing_features =
-    add the missing features in a `target_feature` attribute
-
-codegen_gcc_target_feature_disable_or_enable =
-    the target features {$features} must all be either enabled or disabled together
diff --git a/compiler/rustc_codegen_gcc/src/errors.rs b/compiler/rustc_codegen_gcc/src/errors.rs
index 7786be9ae5d..b7e7343460f 100644
--- a/compiler/rustc_codegen_gcc/src/errors.rs
+++ b/compiler/rustc_codegen_gcc/src/errors.rs
@@ -1,31 +1,7 @@
-use rustc_macros::{Diagnostic, Subdiagnostic};
+use rustc_macros::Diagnostic;
 use rustc_span::Span;
 
 #[derive(Diagnostic)]
-#[diag(codegen_gcc_unknown_ctarget_feature_prefix)]
-#[note]
-pub(crate) struct UnknownCTargetFeaturePrefix<'a> {
-    pub feature: &'a str,
-}
-
-#[derive(Diagnostic)]
-#[diag(codegen_gcc_unknown_ctarget_feature)]
-#[note]
-pub(crate) struct UnknownCTargetFeature<'a> {
-    pub feature: &'a str,
-    #[subdiagnostic]
-    pub rust_feature: PossibleFeature<'a>,
-}
-
-#[derive(Subdiagnostic)]
-pub(crate) enum PossibleFeature<'a> {
-    #[help(codegen_gcc_possible_feature)]
-    Some { rust_feature: &'a str },
-    #[help(codegen_gcc_consider_filing_feature_request)]
-    None,
-}
-
-#[derive(Diagnostic)]
 #[diag(codegen_gcc_unwinding_inline_asm)]
 pub(crate) struct UnwindingInlineAsm {
     #[primary_span]
diff --git a/compiler/rustc_codegen_gcc/src/gcc_util.rs b/compiler/rustc_codegen_gcc/src/gcc_util.rs
index 2e00d5fcb61..562f4124882 100644
--- a/compiler/rustc_codegen_gcc/src/gcc_util.rs
+++ b/compiler/rustc_codegen_gcc/src/gcc_util.rs
@@ -1,20 +1,11 @@
 #[cfg(feature = "master")]
 use gccjit::Context;
-use rustc_codegen_ssa::codegen_attrs::check_tied_features;
-use rustc_codegen_ssa::errors::TargetFeatureDisableOrEnable;
-use rustc_data_structures::fx::FxHashMap;
-use rustc_data_structures::unord::UnordSet;
+use rustc_codegen_ssa::target_features;
 use rustc_session::Session;
-use rustc_session::features::{StabilityExt, retpoline_features_by_flags};
-use rustc_target::target_features::RUSTC_SPECIFIC_FEATURES;
 use smallvec::{SmallVec, smallvec};
 
-use crate::errors::{PossibleFeature, UnknownCTargetFeature, UnknownCTargetFeaturePrefix};
-
-fn gcc_features_by_flags(sess: &Session) -> Vec<&str> {
-    let mut features: Vec<&str> = Vec::new();
-    retpoline_features_by_flags(sess, &mut features);
-    features
+fn gcc_features_by_flags(sess: &Session, features: &mut Vec<String>) {
+    target_features::retpoline_features_by_flags(sess, features);
 }
 
 /// The list of GCC features computed from CLI flags (`-Ctarget-cpu`, `-Ctarget-feature`,
@@ -44,98 +35,31 @@ pub(crate) fn global_gcc_features(sess: &Session, diagnostics: bool) -> Vec<Stri
     features.extend(sess.target.features.split(',').filter(|v| !v.is_empty()).map(String::from));
 
     // -Ctarget-features
-    let known_features = sess.target.rust_target_features();
-    let mut featsmap = FxHashMap::default();
-
-    // Compute implied features
-    let mut all_rust_features = vec![];
-    for feature in sess.opts.cg.target_feature.split(',').chain(gcc_features_by_flags(sess)) {
-        if let Some(feature) = feature.strip_prefix('+') {
-            all_rust_features.extend(
-                UnordSet::from(sess.target.implied_target_features(feature))
-                    .to_sorted_stable_ord()
-                    .iter()
-                    .map(|&&s| (true, s)),
-            )
-        } else if let Some(feature) = feature.strip_prefix('-') {
-            // FIXME: Why do we not remove implied features on "-" here?
-            // We do the equivalent above in `target_config`.
-            // See <https://github.com/rust-lang/rust/issues/134792>.
-            all_rust_features.push((false, feature));
-        } else if !feature.is_empty() && diagnostics {
-            sess.dcx().emit_warn(UnknownCTargetFeaturePrefix { feature });
-        }
-    }
-    // Remove features that are meant for rustc, not codegen.
-    all_rust_features.retain(|&(_, feature)| {
-        // Retain if it is not a rustc feature
-        !RUSTC_SPECIFIC_FEATURES.contains(&feature)
-    });
-
-    // Check feature validity.
-    if diagnostics {
-        for &(enable, feature) in &all_rust_features {
-            let feature_state = known_features.iter().find(|&&(v, _, _)| v == feature);
-            match feature_state {
-                None => {
-                    let rust_feature = known_features.iter().find_map(|&(rust_feature, _, _)| {
-                        let gcc_features = to_gcc_features(sess, rust_feature);
-                        if gcc_features.contains(&feature) && !gcc_features.contains(&rust_feature)
-                        {
-                            Some(rust_feature)
-                        } else {
-                            None
-                        }
-                    });
-                    let unknown_feature = if let Some(rust_feature) = rust_feature {
-                        UnknownCTargetFeature {
-                            feature,
-                            rust_feature: PossibleFeature::Some { rust_feature },
-                        }
-                    } else {
-                        UnknownCTargetFeature { feature, rust_feature: PossibleFeature::None }
-                    };
-                    sess.dcx().emit_warn(unknown_feature);
-                }
-                Some(&(_, stability, _)) => {
-                    stability.verify_feature_enabled_by_flag(sess, enable, feature);
-                }
-            }
-
-            // FIXME(nagisa): figure out how to not allocate a full hashset here.
-            featsmap.insert(feature, enable);
-        }
-    }
-
-    // Translate this into GCC features.
-    let feats =
-        all_rust_features.iter().flat_map(|&(enable, feature)| {
-            let enable_disable = if enable { '+' } else { '-' };
+    target_features::flag_to_backend_features(
+        sess,
+        diagnostics,
+        |feature| to_gcc_features(sess, feature),
+        |feature, enable| {
             // We run through `to_gcc_features` when
             // passing requests down to GCC. This means that all in-language
             // features also work on the command line instead of having two
             // different names when the GCC name and the Rust name differ.
-            to_gcc_features(sess, feature)
-                .iter()
-                .flat_map(|feat| to_gcc_features(sess, feat).into_iter())
-                .map(|feature| {
-                    if enable_disable == '-' {
-                        format!("-{}", feature)
-                    } else {
-                        feature.to_string()
-                    }
-                })
-                .collect::<Vec<_>>()
-        });
-    features.extend(feats);
-
-    if diagnostics && let Some(f) = check_tied_features(sess, &featsmap) {
-        sess.dcx().emit_err(TargetFeatureDisableOrEnable {
-            features: f,
-            span: None,
-            missing_features: None,
-        });
-    }
+            features.extend(
+                to_gcc_features(sess, feature)
+                    .iter()
+                    .flat_map(|feat| to_gcc_features(sess, feat).into_iter())
+                    .map(
+                        |feature| {
+                            if !enable { format!("-{}", feature) } else { feature.to_string() }
+                        },
+                    ),
+            );
+        },
+    );
+
+    gcc_features_by_flags(sess, &mut features);
+
+    // FIXME: LLVM also sets +reserve-x18 here under some conditions.
 
     features
 }
diff --git a/compiler/rustc_codegen_llvm/messages.ftl b/compiler/rustc_codegen_llvm/messages.ftl
index 3faeb9b3b22..3885f18271f 100644
--- a/compiler/rustc_codegen_llvm/messages.ftl
+++ b/compiler/rustc_codegen_llvm/messages.ftl
@@ -59,16 +59,6 @@ codegen_llvm_symbol_already_defined =
 codegen_llvm_target_machine = could not create LLVM TargetMachine for triple: {$triple}
 codegen_llvm_target_machine_with_llvm_err = could not create LLVM TargetMachine for triple: {$triple}: {$llvm_err}
 
-codegen_llvm_unknown_ctarget_feature =
-    unknown and unstable feature specified for `-Ctarget-feature`: `{$feature}`
-    .note = it is still passed through to the codegen backend, but use of this feature might be unsound and the behavior of this feature can change in the future
-    .possible_feature = you might have meant: `{$rust_feature}`
-    .consider_filing_feature_request = consider filing a feature request
-
-codegen_llvm_unknown_ctarget_feature_prefix =
-    unknown feature specified for `-Ctarget-feature`: `{$feature}`
-    .note = features must begin with a `+` to enable or `-` to disable it
-
 codegen_llvm_unknown_debuginfo_compression = unknown debuginfo compression algorithm {$algorithm} - will fall back to uncompressed debuginfo
 
 codegen_llvm_write_bytecode = failed to write bytecode to {$path}: {$err}
diff --git a/compiler/rustc_codegen_llvm/src/errors.rs b/compiler/rustc_codegen_llvm/src/errors.rs
index 8bc74fbec7e..d50ad8a1a9c 100644
--- a/compiler/rustc_codegen_llvm/src/errors.rs
+++ b/compiler/rustc_codegen_llvm/src/errors.rs
@@ -3,36 +3,12 @@ use std::path::Path;
 
 use rustc_data_structures::small_c_str::SmallCStr;
 use rustc_errors::{Diag, DiagCtxtHandle, Diagnostic, EmissionGuarantee, Level};
-use rustc_macros::{Diagnostic, Subdiagnostic};
+use rustc_macros::Diagnostic;
 use rustc_span::Span;
 
 use crate::fluent_generated as fluent;
 
 #[derive(Diagnostic)]
-#[diag(codegen_llvm_unknown_ctarget_feature_prefix)]
-#[note]
-pub(crate) struct UnknownCTargetFeaturePrefix<'a> {
-    pub feature: &'a str,
-}
-
-#[derive(Diagnostic)]
-#[diag(codegen_llvm_unknown_ctarget_feature)]
-#[note]
-pub(crate) struct UnknownCTargetFeature<'a> {
-    pub feature: &'a str,
-    #[subdiagnostic]
-    pub rust_feature: PossibleFeature<'a>,
-}
-
-#[derive(Subdiagnostic)]
-pub(crate) enum PossibleFeature<'a> {
-    #[help(codegen_llvm_possible_feature)]
-    Some { rust_feature: &'a str },
-    #[help(codegen_llvm_consider_filing_feature_request)]
-    None,
-}
-
-#[derive(Diagnostic)]
 #[diag(codegen_llvm_symbol_already_defined)]
 pub(crate) struct SymbolAlreadyDefined<'a> {
     #[primary_span]
diff --git a/compiler/rustc_codegen_llvm/src/llvm_util.rs b/compiler/rustc_codegen_llvm/src/llvm_util.rs
index f9ebf861634..ee78c310b0d 100644
--- a/compiler/rustc_codegen_llvm/src/llvm_util.rs
+++ b/compiler/rustc_codegen_llvm/src/llvm_util.rs
@@ -7,25 +7,18 @@ use std::{ptr, slice, str};
 
 use libc::c_int;
 use rustc_codegen_ssa::base::wants_wasm_eh;
-use rustc_codegen_ssa::codegen_attrs::check_tied_features;
 use rustc_codegen_ssa::{TargetConfig, target_features};
-use rustc_data_structures::fx::{FxHashMap, FxHashSet};
+use rustc_data_structures::fx::FxHashSet;
 use rustc_data_structures::small_c_str::SmallCStr;
-use rustc_data_structures::unord::UnordSet;
 use rustc_fs_util::path_to_c_string;
 use rustc_middle::bug;
 use rustc_session::Session;
 use rustc_session::config::{PrintKind, PrintRequest};
-use rustc_session::features::{StabilityExt, retpoline_features_by_flags};
 use rustc_target::spec::{MergeFunctions, PanicStrategy, SmallDataThresholdSupport};
-use rustc_target::target_features::RUSTC_SPECIFIC_FEATURES;
 use smallvec::{SmallVec, smallvec};
 
 use crate::back::write::create_informational_target_machine;
-use crate::errors::{
-    FixedX18InvalidArch, PossibleFeature, UnknownCTargetFeature, UnknownCTargetFeaturePrefix,
-};
-use crate::llvm;
+use crate::{errors, llvm};
 
 static INIT: Once = Once::new();
 
@@ -194,15 +187,6 @@ impl<'a> LLVMFeature<'a> {
     ) -> Self {
         Self { llvm_feature_name, dependencies }
     }
-
-    fn contains(&'a self, feat: &str) -> bool {
-        self.iter().any(|dep| dep == feat)
-    }
-
-    fn iter(&'a self) -> impl Iterator<Item = &'a str> {
-        let dependencies = self.dependencies.iter().map(|feat| feat.as_str());
-        std::iter::once(self.llvm_feature_name).chain(dependencies)
-    }
 }
 
 impl<'a> IntoIterator for LLVMFeature<'a> {
@@ -215,18 +199,22 @@ impl<'a> IntoIterator for LLVMFeature<'a> {
     }
 }
 
-// WARNING: the features after applying `to_llvm_features` must be known
-// to LLVM or the feature detection code will walk past the end of the feature
-// array, leading to crashes.
-//
-// To find a list of LLVM's names, see llvm-project/llvm/lib/Target/{ARCH}/*.td
-// where `{ARCH}` is the architecture name. Look for instances of `SubtargetFeature`.
-//
-// Check the current rustc fork of LLVM in the repo at https://github.com/rust-lang/llvm-project/.
-// The commit in use can be found via the `llvm-project` submodule in
-// https://github.com/rust-lang/rust/tree/master/src Though note that Rust can also be build with
-// an external precompiled version of LLVM which might lead to failures if the oldest tested /
-// supported LLVM version doesn't yet support the relevant intrinsics.
+/// Convert a Rust feature name to an LLVM feature name. Returning `None` means the
+/// feature should be skipped, usually because it is not supported by the current
+/// LLVM version.
+///
+/// WARNING: the features after applying `to_llvm_features` must be known
+/// to LLVM or the feature detection code will walk past the end of the feature
+/// array, leading to crashes.
+///
+/// To find a list of LLVM's names, see llvm-project/llvm/lib/Target/{ARCH}/*.td
+/// where `{ARCH}` is the architecture name. Look for instances of `SubtargetFeature`.
+///
+/// Check the current rustc fork of LLVM in the repo at <https://github.com/rust-lang/llvm-project/>.
+/// The commit in use can be found via the `llvm-project` submodule in
+/// <https://github.com/rust-lang/rust/tree/master/src> Though note that Rust can also be build with
+/// an external precompiled version of LLVM which might lead to failures if the oldest tested /
+/// supported LLVM version doesn't yet support the relevant intrinsics.
 pub(crate) fn to_llvm_features<'a>(sess: &Session, s: &'a str) -> Option<LLVMFeature<'a>> {
     let arch = if sess.target.arch == "x86_64" {
         "x86"
@@ -634,10 +622,9 @@ pub(crate) fn target_cpu(sess: &Session) -> &str {
     handle_native(cpu_name)
 }
 
-fn llvm_features_by_flags(sess: &Session) -> Vec<&str> {
-    let mut features: Vec<&str> = Vec::new();
-    retpoline_features_by_flags(sess, &mut features);
-    features
+/// The target features for compiler flags other than `-Ctarget-features`.
+fn llvm_features_by_flags(sess: &Session, features: &mut Vec<String>) {
+    target_features::retpoline_features_by_flags(sess, features);
 }
 
 /// The list of LLVM features computed from CLI flags (`-Ctarget-cpu`, `-Ctarget-feature`,
@@ -704,6 +691,8 @@ pub(crate) fn global_llvm_features(
             .split(',')
             .filter(|v| !v.is_empty())
             // Drop +v8plus feature introduced in LLVM 20.
+            // (Hard-coded target features do not go through `to_llvm_feature` since they already
+            // are LLVM feature names, hence we need a special case here.)
             .filter(|v| *v != "+v8plus" || get_version() >= (20, 0, 0))
             .map(String::from),
     );
@@ -714,86 +703,23 @@ pub(crate) fn global_llvm_features(
 
     // -Ctarget-features
     if !only_base_features {
-        let known_features = sess.target.rust_target_features();
-        // Will only be filled when `diagnostics` is set!
-        let mut featsmap = FxHashMap::default();
-
-        // Compute implied features
-        let mut all_rust_features = vec![];
-        for feature in sess.opts.cg.target_feature.split(',').chain(llvm_features_by_flags(sess)) {
-            if let Some(feature) = feature.strip_prefix('+') {
-                all_rust_features.extend(
-                    UnordSet::from(sess.target.implied_target_features(feature))
-                        .to_sorted_stable_ord()
-                        .iter()
-                        .map(|&&s| (true, s)),
-                )
-            } else if let Some(feature) = feature.strip_prefix('-') {
-                // FIXME: Why do we not remove implied features on "-" here?
-                // We do the equivalent above in `target_config`.
-                // See <https://github.com/rust-lang/rust/issues/134792>.
-                all_rust_features.push((false, feature));
-            } else if !feature.is_empty() {
-                if diagnostics {
-                    sess.dcx().emit_warn(UnknownCTargetFeaturePrefix { feature });
-                }
-            }
-        }
-        // Remove features that are meant for rustc, not LLVM.
-        all_rust_features.retain(|(_, feature)| {
-            // Retain if it is not a rustc feature
-            !RUSTC_SPECIFIC_FEATURES.contains(feature)
-        });
-
-        // Check feature validity.
-        if diagnostics {
-            for &(enable, feature) in &all_rust_features {
-                let feature_state = known_features.iter().find(|&&(v, _, _)| v == feature);
-                match feature_state {
-                    None => {
-                        let rust_feature =
-                            known_features.iter().find_map(|&(rust_feature, _, _)| {
-                                let llvm_features = to_llvm_features(sess, rust_feature)?;
-                                if llvm_features.contains(feature)
-                                    && !llvm_features.contains(rust_feature)
-                                {
-                                    Some(rust_feature)
-                                } else {
-                                    None
-                                }
-                            });
-                        let unknown_feature = if let Some(rust_feature) = rust_feature {
-                            UnknownCTargetFeature {
-                                feature,
-                                rust_feature: PossibleFeature::Some { rust_feature },
-                            }
-                        } else {
-                            UnknownCTargetFeature { feature, rust_feature: PossibleFeature::None }
-                        };
-                        sess.dcx().emit_warn(unknown_feature);
-                    }
-                    Some((_, stability, _)) => {
-                        stability.verify_feature_enabled_by_flag(sess, enable, feature);
-                    }
-                }
-
-                // FIXME(nagisa): figure out how to not allocate a full hashset here.
-                featsmap.insert(feature, enable);
-            }
-        }
-
-        // Translate this into LLVM features.
-        let feats = all_rust_features
-            .iter()
-            .filter_map(|&(enable, feature)| {
+        target_features::flag_to_backend_features(
+            sess,
+            diagnostics,
+            |feature| {
+                to_llvm_features(sess, feature)
+                    .map(|f| SmallVec::<[&str; 2]>::from_iter(f.into_iter()))
+                    .unwrap_or_default()
+            },
+            |feature, enable| {
                 let enable_disable = if enable { '+' } else { '-' };
                 // We run through `to_llvm_features` when
                 // passing requests down to LLVM. This means that all in-language
                 // features also work on the command line instead of having two
                 // different names when the LLVM name and the Rust name differ.
-                let llvm_feature = to_llvm_features(sess, feature)?;
+                let Some(llvm_feature) = to_llvm_features(sess, feature) else { return };
 
-                Some(
+                features.extend(
                     std::iter::once(format!(
                         "{}{}",
                         enable_disable, llvm_feature.llvm_feature_name
@@ -808,23 +734,17 @@ pub(crate) fn global_llvm_features(
                         },
                     )),
                 )
-            })
-            .flatten();
-        features.extend(feats);
-
-        if diagnostics && let Some(f) = check_tied_features(sess, &featsmap) {
-            sess.dcx().emit_err(rustc_codegen_ssa::errors::TargetFeatureDisableOrEnable {
-                features: f,
-                span: None,
-                missing_features: None,
-            });
-        }
+            },
+        );
+
+        llvm_features_by_flags(sess, &mut features);
     }
 
     // -Zfixed-x18
+    // FIXME: merge with `llvm_features_by_flags`.
     if sess.opts.unstable_opts.fixed_x18 {
         if sess.target.arch != "aarch64" {
-            sess.dcx().emit_fatal(FixedX18InvalidArch { arch: &sess.target.arch });
+            sess.dcx().emit_fatal(errors::FixedX18InvalidArch { arch: &sess.target.arch });
         } else {
             features.push("+reserve-x18".into());
         }
diff --git a/compiler/rustc_codegen_ssa/messages.ftl b/compiler/rustc_codegen_ssa/messages.ftl
index 5322fe58cf3..ffcce8b2ff4 100644
--- a/compiler/rustc_codegen_ssa/messages.ftl
+++ b/compiler/rustc_codegen_ssa/messages.ftl
@@ -68,6 +68,11 @@ codegen_ssa_failed_to_write = failed to write {$path}: {$error}
 
 codegen_ssa_field_associated_value_expected = associated value expected for `{$name}`
 
+codegen_ssa_forbidden_ctarget_feature =
+    target feature `{$feature}` cannot be {$enabled} with `-Ctarget-feature`: {$reason}
+    .note = this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
+codegen_ssa_forbidden_ctarget_feature_issue = for more information, see issue #116344 <https://github.com/rust-lang/rust/issues/116344>
+
 codegen_ssa_forbidden_target_feature_attr =
     target feature `{$feature}` cannot be enabled with `#[target_feature]`: {$reason}
 
@@ -368,8 +373,22 @@ codegen_ssa_unexpected_parameter_name = unexpected parameter name
 codegen_ssa_unknown_archive_kind =
     Don't know how to build archive of type: {$kind}
 
+codegen_ssa_unknown_ctarget_feature =
+    unknown and unstable feature specified for `-Ctarget-feature`: `{$feature}`
+    .note = it is still passed through to the codegen backend, but use of this feature might be unsound and the behavior of this feature can change in the future
+    .possible_feature = you might have meant: `{$rust_feature}`
+    .consider_filing_feature_request = consider filing a feature request
+
+codegen_ssa_unknown_ctarget_feature_prefix =
+    unknown feature specified for `-Ctarget-feature`: `{$feature}`
+    .note = features must begin with a `+` to enable or `-` to disable it
+
 codegen_ssa_unknown_reuse_kind = unknown cgu-reuse-kind `{$kind}` specified
 
+codegen_ssa_unstable_ctarget_feature =
+    unstable feature specified for `-Ctarget-feature`: `{$feature}`
+    .note = this feature is not stably supported; its behavior can change in the future
+
 codegen_ssa_unsupported_instruction_set = target does not support `#[instruction_set]`
 
 codegen_ssa_unsupported_link_self_contained = option `-C link-self-contained` is not supported on this target
diff --git a/compiler/rustc_codegen_ssa/src/codegen_attrs.rs b/compiler/rustc_codegen_ssa/src/codegen_attrs.rs
index 188a9a98ce7..78c8264eeb0 100644
--- a/compiler/rustc_codegen_ssa/src/codegen_attrs.rs
+++ b/compiler/rustc_codegen_ssa/src/codegen_attrs.rs
@@ -7,7 +7,6 @@ use rustc_attr_data_structures::ReprAttr::ReprAlign;
 use rustc_attr_data_structures::{
     AttributeKind, InlineAttr, InstructionSetAttr, OptimizeAttr, find_attr,
 };
-use rustc_data_structures::fx::FxHashMap;
 use rustc_hir::def::DefKind;
 use rustc_hir::def_id::{DefId, LOCAL_CRATE, LocalDefId};
 use rustc_hir::weak_lang_items::WEAK_LANG_ITEMS;
@@ -19,13 +18,15 @@ use rustc_middle::mir::mono::Linkage;
 use rustc_middle::query::Providers;
 use rustc_middle::span_bug;
 use rustc_middle::ty::{self as ty, TyCtxt};
+use rustc_session::lint;
 use rustc_session::parse::feature_err;
-use rustc_session::{Session, lint};
 use rustc_span::{Ident, Span, sym};
 use rustc_target::spec::SanitizerSet;
 
 use crate::errors;
-use crate::target_features::{check_target_feature_trait_unsafe, from_target_feature_attr};
+use crate::target_features::{
+    check_target_feature_trait_unsafe, check_tied_features, from_target_feature_attr,
+};
 
 fn linkage_by_name(tcx: TyCtxt<'_>, def_id: LocalDefId, name: &str) -> Linkage {
     use rustc_middle::mir::mono::Linkage::*;
@@ -625,25 +626,6 @@ fn codegen_fn_attrs(tcx: TyCtxt<'_>, did: LocalDefId) -> CodegenFnAttrs {
     codegen_fn_attrs
 }
 
-/// Given a map from target_features to whether they are enabled or disabled, ensure only valid
-/// combinations are allowed.
-pub fn check_tied_features(
-    sess: &Session,
-    features: &FxHashMap<&str, bool>,
-) -> Option<&'static [&'static str]> {
-    if !features.is_empty() {
-        for tied in sess.target.tied_target_features() {
-            // Tied features must be set to the same value, or not set at all
-            let mut tied_iter = tied.iter();
-            let enabled = features.get(tied_iter.next().unwrap());
-            if tied_iter.any(|f| enabled != features.get(f)) {
-                return Some(tied);
-            }
-        }
-    }
-    None
-}
-
 /// Checks if the provided DefId is a method in a trait impl for a trait which has track_caller
 /// applied to the method prototype.
 fn should_inherit_track_caller(tcx: TyCtxt<'_>, def_id: DefId) -> bool {
diff --git a/compiler/rustc_codegen_ssa/src/errors.rs b/compiler/rustc_codegen_ssa/src/errors.rs
index 5387b2a7f81..7c5d615aaff 100644
--- a/compiler/rustc_codegen_ssa/src/errors.rs
+++ b/compiler/rustc_codegen_ssa/src/errors.rs
@@ -1217,30 +1217,6 @@ pub(crate) struct ErrorCreatingImportLibrary<'a> {
     pub error: String,
 }
 
-pub struct TargetFeatureDisableOrEnable<'a> {
-    pub features: &'a [&'a str],
-    pub span: Option<Span>,
-    pub missing_features: Option<MissingFeatures>,
-}
-
-#[derive(Subdiagnostic)]
-#[help(codegen_ssa_missing_features)]
-pub struct MissingFeatures;
-
-impl<G: EmissionGuarantee> Diagnostic<'_, G> for TargetFeatureDisableOrEnable<'_> {
-    fn into_diag(self, dcx: DiagCtxtHandle<'_>, level: Level) -> Diag<'_, G> {
-        let mut diag = Diag::new(dcx, level, fluent::codegen_ssa_target_feature_disable_or_enable);
-        if let Some(span) = self.span {
-            diag.span(span);
-        };
-        if let Some(missing_features) = self.missing_features {
-            diag.subdiagnostic(missing_features);
-        }
-        diag.arg("features", self.features.join(", "));
-        diag
-    }
-}
-
 #[derive(Diagnostic)]
 #[diag(codegen_ssa_aix_strip_not_used)]
 pub(crate) struct AixStripNotUsed;
@@ -1283,3 +1259,68 @@ pub(crate) struct XcrunSdkPathWarning {
 #[derive(LintDiagnostic)]
 #[diag(codegen_ssa_aarch64_softfloat_neon)]
 pub(crate) struct Aarch64SoftfloatNeon;
+
+#[derive(Diagnostic)]
+#[diag(codegen_ssa_unknown_ctarget_feature_prefix)]
+#[note]
+pub(crate) struct UnknownCTargetFeaturePrefix<'a> {
+    pub feature: &'a str,
+}
+
+#[derive(Subdiagnostic)]
+pub(crate) enum PossibleFeature<'a> {
+    #[help(codegen_ssa_possible_feature)]
+    Some { rust_feature: &'a str },
+    #[help(codegen_ssa_consider_filing_feature_request)]
+    None,
+}
+
+#[derive(Diagnostic)]
+#[diag(codegen_ssa_unknown_ctarget_feature)]
+#[note]
+pub(crate) struct UnknownCTargetFeature<'a> {
+    pub feature: &'a str,
+    #[subdiagnostic]
+    pub rust_feature: PossibleFeature<'a>,
+}
+
+#[derive(Diagnostic)]
+#[diag(codegen_ssa_unstable_ctarget_feature)]
+#[note]
+pub(crate) struct UnstableCTargetFeature<'a> {
+    pub feature: &'a str,
+}
+
+#[derive(Diagnostic)]
+#[diag(codegen_ssa_forbidden_ctarget_feature)]
+#[note]
+#[note(codegen_ssa_forbidden_ctarget_feature_issue)]
+pub(crate) struct ForbiddenCTargetFeature<'a> {
+    pub feature: &'a str,
+    pub enabled: &'a str,
+    pub reason: &'a str,
+}
+
+pub struct TargetFeatureDisableOrEnable<'a> {
+    pub features: &'a [&'a str],
+    pub span: Option<Span>,
+    pub missing_features: Option<MissingFeatures>,
+}
+
+#[derive(Subdiagnostic)]
+#[help(codegen_ssa_missing_features)]
+pub struct MissingFeatures;
+
+impl<G: EmissionGuarantee> Diagnostic<'_, G> for TargetFeatureDisableOrEnable<'_> {
+    fn into_diag(self, dcx: DiagCtxtHandle<'_>, level: Level) -> Diag<'_, G> {
+        let mut diag = Diag::new(dcx, level, fluent::codegen_ssa_target_feature_disable_or_enable);
+        if let Some(span) = self.span {
+            diag.span(span);
+        };
+        if let Some(missing_features) = self.missing_features {
+            diag.subdiagnostic(missing_features);
+        }
+        diag.arg("features", self.features.join(", "));
+        diag
+    }
+}
diff --git a/compiler/rustc_codegen_ssa/src/target_features.rs b/compiler/rustc_codegen_ssa/src/target_features.rs
index 476671d6855..9cca6a3c72b 100644
--- a/compiler/rustc_codegen_ssa/src/target_features.rs
+++ b/compiler/rustc_codegen_ssa/src/target_features.rs
@@ -1,5 +1,5 @@
 use rustc_attr_data_structures::InstructionSetAttr;
-use rustc_data_structures::fx::{FxHashSet, FxIndexSet};
+use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexSet};
 use rustc_data_structures::unord::{UnordMap, UnordSet};
 use rustc_errors::Applicability;
 use rustc_hir as hir;
@@ -9,11 +9,13 @@ use rustc_middle::middle::codegen_fn_attrs::TargetFeature;
 use rustc_middle::query::Providers;
 use rustc_middle::ty::TyCtxt;
 use rustc_session::Session;
-use rustc_session::features::StabilityExt;
 use rustc_session::lint::builtin::AARCH64_SOFTFLOAT_NEON;
 use rustc_session::parse::feature_err;
 use rustc_span::{Span, Symbol, sym};
-use rustc_target::target_features::{self, RUSTC_SPECIAL_FEATURES, Stability};
+use rustc_target::target_features::{
+    self, RUSTC_SPECIAL_FEATURES, RUSTC_SPECIFIC_FEATURES, Stability,
+};
+use smallvec::SmallVec;
 
 use crate::errors;
 
@@ -68,7 +70,7 @@ pub(crate) fn from_target_feature_attr(
 
             // Only allow target features whose feature gates have been enabled
             // and which are permitted to be toggled.
-            if let Err(reason) = stability.is_toggle_permitted(tcx.sess) {
+            if let Err(reason) = stability.toggle_allowed() {
                 tcx.dcx().emit_err(errors::ForbiddenTargetFeatureAttr {
                     span: item.span(),
                     feature,
@@ -89,7 +91,7 @@ pub(crate) fn from_target_feature_attr(
                 let feature_sym = Symbol::intern(feature);
                 for &name in tcx.implied_target_features(feature_sym) {
                     // But ensure the ABI does not forbid enabling this.
-                    // Here we do assume that LLVM doesn't add even more implied features
+                    // Here we do assume that the backend doesn't add even more implied features
                     // we don't know about, at least no features that would have ABI effects!
                     // We skip this logic in rustdoc, where we want to allow all target features of
                     // all targets, so we can't check their ABI compatibility and anyway we are not
@@ -251,6 +253,155 @@ pub fn cfg_target_feature(
     (f(true), f(false))
 }
 
+/// Given a map from target_features to whether they are enabled or disabled, ensure only valid
+/// combinations are allowed.
+pub fn check_tied_features(
+    sess: &Session,
+    features: &FxHashMap<&str, bool>,
+) -> Option<&'static [&'static str]> {
+    if !features.is_empty() {
+        for tied in sess.target.tied_target_features() {
+            // Tied features must be set to the same value, or not set at all
+            let mut tied_iter = tied.iter();
+            let enabled = features.get(tied_iter.next().unwrap());
+            if tied_iter.any(|f| enabled != features.get(f)) {
+                return Some(tied);
+            }
+        }
+    }
+    None
+}
+
+/// Translates the `-Ctarget-feature` flag into a backend target feature list.
+///
+/// `to_backend_features` converts a Rust feature name into a list of backend feature names; this is
+/// used for diagnostic purposes only.
+///
+/// `extend_backend_features` extends the set of backend features (assumed to be in mutable state
+/// accessible by that closure) to enable/disable the given Rust feature name.
+pub fn flag_to_backend_features<'a, const N: usize>(
+    sess: &'a Session,
+    diagnostics: bool,
+    to_backend_features: impl Fn(&'a str) -> SmallVec<[&'a str; N]>,
+    mut extend_backend_features: impl FnMut(&'a str, /* enable */ bool),
+) {
+    let known_features = sess.target.rust_target_features();
+
+    // Compute implied features
+    let mut rust_features = vec![];
+    for feature in sess.opts.cg.target_feature.split(',') {
+        if let Some(feature) = feature.strip_prefix('+') {
+            rust_features.extend(
+                UnordSet::from(sess.target.implied_target_features(feature))
+                    .to_sorted_stable_ord()
+                    .iter()
+                    .map(|&&s| (true, s)),
+            )
+        } else if let Some(feature) = feature.strip_prefix('-') {
+            // FIXME: Why do we not remove implied features on "-" here?
+            // We do the equivalent above in `target_config`.
+            // See <https://github.com/rust-lang/rust/issues/134792>.
+            rust_features.push((false, feature));
+        } else if !feature.is_empty() {
+            if diagnostics {
+                sess.dcx().emit_warn(errors::UnknownCTargetFeaturePrefix { feature });
+            }
+        }
+    }
+    // Remove features that are meant for rustc, not the backend.
+    rust_features.retain(|(_, feature)| {
+        // Retain if it is not a rustc feature
+        !RUSTC_SPECIFIC_FEATURES.contains(feature)
+    });
+
+    // Check feature validity.
+    if diagnostics {
+        let mut featsmap = FxHashMap::default();
+
+        for &(enable, feature) in &rust_features {
+            let feature_state = known_features.iter().find(|&&(v, _, _)| v == feature);
+            match feature_state {
+                None => {
+                    // This is definitely not a valid Rust feature name. Maybe it is a backend feature name?
+                    // If so, give a better error message.
+                    let rust_feature = known_features.iter().find_map(|&(rust_feature, _, _)| {
+                        let backend_features = to_backend_features(rust_feature);
+                        if backend_features.contains(&feature)
+                            && !backend_features.contains(&rust_feature)
+                        {
+                            Some(rust_feature)
+                        } else {
+                            None
+                        }
+                    });
+                    let unknown_feature = if let Some(rust_feature) = rust_feature {
+                        errors::UnknownCTargetFeature {
+                            feature,
+                            rust_feature: errors::PossibleFeature::Some { rust_feature },
+                        }
+                    } else {
+                        errors::UnknownCTargetFeature {
+                            feature,
+                            rust_feature: errors::PossibleFeature::None,
+                        }
+                    };
+                    sess.dcx().emit_warn(unknown_feature);
+                }
+                Some((_, stability, _)) => {
+                    if let Err(reason) = stability.toggle_allowed() {
+                        sess.dcx().emit_warn(errors::ForbiddenCTargetFeature {
+                            feature,
+                            enabled: if enable { "enabled" } else { "disabled" },
+                            reason,
+                        });
+                    } else if stability.requires_nightly().is_some() {
+                        // An unstable feature. Warn about using it. It makes little sense
+                        // to hard-error here since we just warn about fully unknown
+                        // features above.
+                        sess.dcx().emit_warn(errors::UnstableCTargetFeature { feature });
+                    }
+                }
+            }
+
+            // FIXME(nagisa): figure out how to not allocate a full hashset here.
+            featsmap.insert(feature, enable);
+        }
+
+        if let Some(f) = check_tied_features(sess, &featsmap) {
+            sess.dcx().emit_err(errors::TargetFeatureDisableOrEnable {
+                features: f,
+                span: None,
+                missing_features: None,
+            });
+        }
+    }
+
+    // Add this to the backend features.
+    for (enable, feature) in rust_features {
+        extend_backend_features(feature, enable);
+    }
+}
+
+/// Computes the backend target features to be added to account for retpoline flags.
+/// Used by both LLVM and GCC since their target features are, conveniently, the same.
+pub fn retpoline_features_by_flags(sess: &Session, features: &mut Vec<String>) {
+    // -Zretpoline without -Zretpoline-external-thunk enables
+    // retpoline-indirect-branches and retpoline-indirect-calls target features
+    let unstable_opts = &sess.opts.unstable_opts;
+    if unstable_opts.retpoline && !unstable_opts.retpoline_external_thunk {
+        features.push("+retpoline-indirect-branches".into());
+        features.push("+retpoline-indirect-calls".into());
+    }
+    // -Zretpoline-external-thunk (maybe, with -Zretpoline too) enables
+    // retpoline-external-thunk, retpoline-indirect-branches and
+    // retpoline-indirect-calls target features
+    if unstable_opts.retpoline_external_thunk {
+        features.push("+retpoline-external-thunk".into());
+        features.push("+retpoline-indirect-branches".into());
+        features.push("+retpoline-indirect-calls".into());
+    }
+}
+
 pub(crate) fn provide(providers: &mut Providers) {
     *providers = Providers {
         rust_target_features: |tcx, cnum| {
diff --git a/compiler/rustc_session/messages.ftl b/compiler/rustc_session/messages.ftl
index 61953614c77..528c52eace7 100644
--- a/compiler/rustc_session/messages.ftl
+++ b/compiler/rustc_session/messages.ftl
@@ -40,11 +40,6 @@ session_file_is_not_writeable = output file {$file} is not writeable -- check it
 
 session_file_write_fail = failed to write `{$path}` due to error `{$err}`
 
-session_forbidden_ctarget_feature =
-    target feature `{$feature}` cannot be {$enabled} with `-Ctarget-feature`: {$reason}
-    .note = this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
-session_forbidden_ctarget_feature_issue = for more information, see issue #116344 <https://github.com/rust-lang/rust/issues/116344>
-
 session_function_return_requires_x86_or_x86_64 = `-Zfunction-return` (except `keep`) is only supported on x86 and x86_64
 
 session_function_return_thunk_extern_requires_non_large_code_model = `-Zfunction-return=thunk-extern` is only supported on non-large code models
@@ -137,9 +132,6 @@ session_target_stack_protector_not_supported = `-Z stack-protector={$stack_prote
 session_unleashed_feature_help_named = skipping check for `{$gate}` feature
 session_unleashed_feature_help_unnamed = skipping check that does not even have a feature gate
 
-session_unstable_ctarget_feature =
-    unstable feature specified for `-Ctarget-feature`: `{$feature}`
-    .note = this feature is not stably supported; its behavior can change in the future
 session_unstable_virtual_function_elimination = `-Zvirtual-function-elimination` requires `-Clto`
 
 session_unsupported_crate_type_for_target =
diff --git a/compiler/rustc_session/src/errors.rs b/compiler/rustc_session/src/errors.rs
index 9c591dcf619..bf95014843d 100644
--- a/compiler/rustc_session/src/errors.rs
+++ b/compiler/rustc_session/src/errors.rs
@@ -501,20 +501,3 @@ pub(crate) struct SoftFloatIgnored;
 #[note]
 #[note(session_soft_float_deprecated_issue)]
 pub(crate) struct SoftFloatDeprecated;
-
-#[derive(Diagnostic)]
-#[diag(session_forbidden_ctarget_feature)]
-#[note]
-#[note(session_forbidden_ctarget_feature_issue)]
-pub(crate) struct ForbiddenCTargetFeature<'a> {
-    pub feature: &'a str,
-    pub enabled: &'a str,
-    pub reason: &'a str,
-}
-
-#[derive(Diagnostic)]
-#[diag(session_unstable_ctarget_feature)]
-#[note]
-pub(crate) struct UnstableCTargetFeature<'a> {
-    pub feature: &'a str,
-}
diff --git a/compiler/rustc_session/src/features.rs b/compiler/rustc_session/src/features.rs
deleted file mode 100644
index 70a088a236f..00000000000
--- a/compiler/rustc_session/src/features.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-use rustc_target::target_features::Stability;
-
-use crate::Session;
-use crate::errors::{ForbiddenCTargetFeature, UnstableCTargetFeature};
-
-pub trait StabilityExt {
-    /// Returns whether the feature may be toggled via `#[target_feature]` or `-Ctarget-feature`.
-    /// Otherwise, some features also may only be enabled by flag (target modifier).
-    /// (It might still be nightly-only even if this returns `true`, so make sure to also check
-    /// `requires_nightly`.)
-    fn is_toggle_permitted(&self, sess: &Session) -> Result<(), &'static str>;
-
-    /// Check that feature is correctly enabled/disabled by command line flag (emits warnings)
-    fn verify_feature_enabled_by_flag(&self, sess: &Session, enable: bool, feature: &str);
-}
-
-impl StabilityExt for Stability {
-    fn is_toggle_permitted(&self, sess: &Session) -> Result<(), &'static str> {
-        match self {
-            Stability::Forbidden { reason } => Err(reason),
-            Stability::TargetModifierOnly { reason, flag } => {
-                if !sess.opts.target_feature_flag_enabled(*flag) { Err(reason) } else { Ok(()) }
-            }
-            _ => Ok(()),
-        }
-    }
-    fn verify_feature_enabled_by_flag(&self, sess: &Session, enable: bool, feature: &str) {
-        if let Err(reason) = self.is_toggle_permitted(sess) {
-            sess.dcx().emit_warn(ForbiddenCTargetFeature {
-                feature,
-                enabled: if enable { "enabled" } else { "disabled" },
-                reason,
-            });
-        } else if self.requires_nightly().is_some() {
-            // An unstable feature. Warn about using it. It makes little sense
-            // to hard-error here since we just warn about fully unknown
-            // features above.
-            sess.dcx().emit_warn(UnstableCTargetFeature { feature });
-        }
-    }
-}
-
-pub fn retpoline_features_by_flags(sess: &Session, features: &mut Vec<&str>) {
-    // -Zretpoline without -Zretpoline-external-thunk enables
-    // retpoline-indirect-branches and retpoline-indirect-calls target features
-    let unstable_opts = &sess.opts.unstable_opts;
-    if unstable_opts.retpoline && !unstable_opts.retpoline_external_thunk {
-        features.push("+retpoline-indirect-branches");
-        features.push("+retpoline-indirect-calls");
-    }
-    // -Zretpoline-external-thunk (maybe, with -Zretpoline too) enables
-    // retpoline-external-thunk, retpoline-indirect-branches and
-    // retpoline-indirect-calls target features
-    if unstable_opts.retpoline_external_thunk {
-        features.push("+retpoline-external-thunk");
-        features.push("+retpoline-indirect-branches");
-        features.push("+retpoline-indirect-calls");
-    }
-}
diff --git a/compiler/rustc_session/src/lib.rs b/compiler/rustc_session/src/lib.rs
index 4added19e56..5e5872ee068 100644
--- a/compiler/rustc_session/src/lib.rs
+++ b/compiler/rustc_session/src/lib.rs
@@ -29,7 +29,6 @@ pub use session::*;
 pub mod output;
 
 pub use getopts;
-pub mod features;
 
 rustc_fluent_macro::fluent_messages! { "../messages.ftl" }
 
diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs
index 31b4237d3b2..7fef942525b 100644
--- a/compiler/rustc_session/src/options.rs
+++ b/compiler/rustc_session/src/options.rs
@@ -290,14 +290,6 @@ macro_rules! top_level_options {
                 mods.sort_by(|a, b| a.opt.cmp(&b.opt));
                 mods
             }
-
-            pub fn target_feature_flag_enabled(&self, flag: &str) -> bool {
-                match flag {
-                    "retpoline" => self.unstable_opts.retpoline,
-                    "retpoline-external-thunk" => self.unstable_opts.retpoline_external_thunk,
-                    _ => false,
-                }
-            }
         }
     );
 }
diff --git a/compiler/rustc_target/src/target_features.rs b/compiler/rustc_target/src/target_features.rs
index a1eac1fba25..c18a2c1ebef 100644
--- a/compiler/rustc_target/src/target_features.rs
+++ b/compiler/rustc_target/src/target_features.rs
@@ -34,9 +34,6 @@ pub enum Stability {
     /// particular for features are actually ABI configuration flags (not all targets are as nice as
     /// RISC-V and have an explicit way to set the ABI separate from target features).
     Forbidden { reason: &'static str },
-    /// This feature can not be set via `-Ctarget-feature` or `#[target_feature]`, it can only be set
-    /// by target modifier flag. Target modifier flags are tracked to be consistent in linked modules.
-    TargetModifierOnly { reason: &'static str, flag: &'static str },
 }
 use Stability::*;
 
@@ -52,7 +49,6 @@ impl<CTX> HashStable<CTX> for Stability {
             Stability::Forbidden { reason } => {
                 reason.hash_stable(hcx, hasher);
             }
-            Stability::TargetModifierOnly { .. } => {}
         }
     }
 }
@@ -62,7 +58,7 @@ impl Stability {
     /// (It might still be nightly-only even if this returns `true`, so make sure to also check
     /// `requires_nightly`.)
     pub fn in_cfg(&self) -> bool {
-        !matches!(self, Stability::Forbidden { .. })
+        matches!(self, Stability::Stable | Stability::Unstable { .. })
     }
 
     /// Returns the nightly feature that is required to toggle this target feature via
@@ -78,7 +74,16 @@ impl Stability {
             Stability::Unstable(nightly_feature) => Some(nightly_feature),
             Stability::Stable { .. } => None,
             Stability::Forbidden { .. } => panic!("forbidden features should not reach this far"),
-            Stability::TargetModifierOnly { .. } => None,
+        }
+    }
+
+    /// Returns whether the feature may be toggled via `#[target_feature]` or `-Ctarget-feature`.
+    /// (It might still be nightly-only even if this returns `true`, so make sure to also check
+    /// `requires_nightly`.)
+    pub fn toggle_allowed(&self) -> Result<(), &'static str> {
+        match self {
+            Stability::Unstable(_) | Stability::Stable { .. } => Ok(()),
+            Stability::Forbidden { reason } => Err(reason),
         }
     }
 }
@@ -450,26 +455,19 @@ static X86_FEATURES: &[(&str, Stability, ImpliedFeatures)] = &[
     ("rdseed", Stable, &[]),
     (
         "retpoline-external-thunk",
-        Stability::TargetModifierOnly {
+        Stability::Forbidden {
             reason: "use `retpoline-external-thunk` target modifier flag instead",
-            flag: "retpoline-external-thunk",
         },
         &[],
     ),
     (
         "retpoline-indirect-branches",
-        Stability::TargetModifierOnly {
-            reason: "use `retpoline` target modifier flag instead",
-            flag: "retpoline",
-        },
+        Stability::Forbidden { reason: "use `retpoline` target modifier flag instead" },
         &[],
     ),
     (
         "retpoline-indirect-calls",
-        Stability::TargetModifierOnly {
-            reason: "use `retpoline` target modifier flag instead",
-            flag: "retpoline",
-        },
+        Stability::Forbidden { reason: "use `retpoline` target modifier flag instead" },
         &[],
     ),
     ("rtm", Unstable(sym::rtm_target_feature), &[]),
diff --git a/src/librustdoc/json/mod.rs b/src/librustdoc/json/mod.rs
index 2feadce26d0..29c63a391e2 100644
--- a/src/librustdoc/json/mod.rs
+++ b/src/librustdoc/json/mod.rs
@@ -18,7 +18,6 @@ use rustc_data_structures::fx::FxHashSet;
 use rustc_hir::def_id::{DefId, DefIdSet};
 use rustc_middle::ty::TyCtxt;
 use rustc_session::Session;
-use rustc_session::features::StabilityExt;
 use rustc_span::def_id::LOCAL_CRATE;
 use rustdoc_json_types as types;
 // It's important to use the FxHashMap from rustdoc_json_types here, instead of
@@ -148,7 +147,7 @@ fn target(sess: &rustc_session::Session) -> types::Target {
             .copied()
             .filter(|(_, stability, _)| {
                 // Describe only target features which the user can toggle
-                stability.is_toggle_permitted(sess).is_ok()
+                stability.toggle_allowed().is_ok()
             })
             .map(|(name, stability, implied_features)| {
                 types::TargetFeature {
@@ -164,7 +163,7 @@ fn target(sess: &rustc_session::Session) -> types::Target {
                             // Imply only target features which the user can toggle
                             feature_stability
                                 .get(name)
-                                .map(|stability| stability.is_toggle_permitted(sess).is_ok())
+                                .map(|stability| stability.toggle_allowed().is_ok())
                                 .unwrap_or(false)
                         })
                         .map(String::from)
diff --git a/tests/ui/check-cfg/target_feature.stderr b/tests/ui/check-cfg/target_feature.stderr
index f29a41d6a8e..ec81ba2e3d8 100644
--- a/tests/ui/check-cfg/target_feature.stderr
+++ b/tests/ui/check-cfg/target_feature.stderr
@@ -212,9 +212,6 @@ LL |     cfg!(target_feature = "_UNEXPECTED_VALUE");
 `relax`
 `relaxed-simd`
 `reserve-x18`
-`retpoline-external-thunk`
-`retpoline-indirect-branches`
-`retpoline-indirect-calls`
 `rtm`
 `sb`
 `scq`