diff --git a/compiler/rustc_ast/src/ast.rs b/compiler/rustc_ast/src/ast.rs
index a121b5a9bed..3d59c034210 100644
--- a/compiler/rustc_ast/src/ast.rs
+++ b/compiler/rustc_ast/src/ast.rs
@@ -286,41 +286,16 @@ impl ParenthesizedArgs {
 
 pub use crate::node_id::{NodeId, CRATE_NODE_ID, DUMMY_NODE_ID};
 
-/// A modifier on a bound, e.g., `?Trait` or `~const Trait`.
-///
-/// Negative bounds should also be handled here.
+/// Modifiers on a trait bound like `~const`, `?` and `!`.
 #[derive(Copy, Clone, PartialEq, Eq, Encodable, Decodable, Debug)]
-pub enum TraitBoundModifier {
-    /// No modifiers
-    None,
-
-    /// `!Trait`
-    Negative,
-
-    /// `?Trait`
-    Maybe,
-
-    /// `~const Trait`
-    MaybeConst(Span),
-
-    /// `~const !Trait`
-    //
-    // This parses but will be rejected during AST validation.
-    MaybeConstNegative,
-
-    /// `~const ?Trait`
-    //
-    // This parses but will be rejected during AST validation.
-    MaybeConstMaybe,
+pub struct TraitBoundModifiers {
+    pub constness: BoundConstness,
+    pub polarity: BoundPolarity,
 }
 
-impl TraitBoundModifier {
-    pub fn to_constness(self) -> Const {
-        match self {
-            Self::MaybeConst(span) => Const::Yes(span),
-            _ => Const::No,
-        }
-    }
+impl TraitBoundModifiers {
+    pub const NONE: Self =
+        Self { constness: BoundConstness::Never, polarity: BoundPolarity::Positive };
 }
 
 /// The AST represents all type param bounds as types.
@@ -329,7 +304,7 @@ impl TraitBoundModifier {
 /// detects `Copy`, `Send` and `Sync`.
 #[derive(Clone, Encodable, Decodable, Debug)]
 pub enum GenericBound {
-    Trait(PolyTraitRef, TraitBoundModifier),
+    Trait(PolyTraitRef, TraitBoundModifiers),
     Outlives(Lifetime),
 }
 
@@ -1193,7 +1168,7 @@ impl Expr {
         match &self.kind {
             ExprKind::Path(None, path) => Some(GenericBound::Trait(
                 PolyTraitRef::new(ThinVec::new(), path.clone(), self.span),
-                TraitBoundModifier::None,
+                TraitBoundModifiers::NONE,
             )),
             _ => None,
         }
@@ -2491,6 +2466,15 @@ pub enum Const {
     No,
 }
 
+impl From<BoundConstness> for Const {
+    fn from(constness: BoundConstness) -> Self {
+        match constness {
+            BoundConstness::Maybe(span) => Self::Yes(span),
+            BoundConstness::Never => Self::No,
+        }
+    }
+}
+
 /// Item defaultness.
 /// For details see the [RFC #2532](https://github.com/rust-lang/rfcs/pull/2532).
 #[derive(Copy, Clone, PartialEq, Encodable, Decodable, Debug, HashStable_Generic)]
@@ -2516,7 +2500,9 @@ impl fmt::Debug for ImplPolarity {
     }
 }
 
-#[derive(Copy, Clone, PartialEq, Encodable, Decodable, HashStable_Generic)]
+/// The polarity of a trait bound.
+#[derive(Copy, Clone, PartialEq, Eq, Encodable, Decodable, Debug)]
+#[derive(HashStable_Generic)]
 pub enum BoundPolarity {
     /// `Type: Trait`
     Positive,
@@ -2526,6 +2512,35 @@ pub enum BoundPolarity {
     Maybe(Span),
 }
 
+impl BoundPolarity {
+    pub fn as_str(self) -> &'static str {
+        match self {
+            Self::Positive => "",
+            Self::Negative(_) => "!",
+            Self::Maybe(_) => "?",
+        }
+    }
+}
+
+/// The constness of a trait bound.
+#[derive(Copy, Clone, PartialEq, Eq, Encodable, Decodable, Debug)]
+#[derive(HashStable_Generic)]
+pub enum BoundConstness {
+    /// `Type: Trait`
+    Never,
+    /// `Type: ~const Trait`
+    Maybe(Span),
+}
+
+impl BoundConstness {
+    pub fn as_str(self) -> &'static str {
+        match self {
+            Self::Never => "",
+            Self::Maybe(_) => "~const",
+        }
+    }
+}
+
 #[derive(Clone, Encodable, Decodable, Debug)]
 pub enum FnRetTy {
     /// Returns type is not specified.
@@ -3259,7 +3274,7 @@ mod size_asserts {
     static_assert_size!(ForeignItem, 96);
     static_assert_size!(ForeignItemKind, 24);
     static_assert_size!(GenericArg, 24);
-    static_assert_size!(GenericBound, 64);
+    static_assert_size!(GenericBound, 72);
     static_assert_size!(Generics, 40);
     static_assert_size!(Impl, 136);
     static_assert_size!(Item, 136);
diff --git a/compiler/rustc_ast_lowering/src/item.rs b/compiler/rustc_ast_lowering/src/item.rs
index 5bddbe5f417..9c990cb4619 100644
--- a/compiler/rustc_ast_lowering/src/item.rs
+++ b/compiler/rustc_ast_lowering/src/item.rs
@@ -1372,7 +1372,13 @@ impl<'hir> LoweringContext<'_, 'hir> {
             // need to compute this at all unless there is a Maybe bound.
             let mut is_param: Option<bool> = None;
             for bound in &bound_pred.bounds {
-                if !matches!(*bound, GenericBound::Trait(_, TraitBoundModifier::Maybe)) {
+                if !matches!(
+                    *bound,
+                    GenericBound::Trait(
+                        _,
+                        TraitBoundModifiers { polarity: BoundPolarity::Maybe(_), .. }
+                    )
+                ) {
                     continue;
                 }
                 let is_param = *is_param.get_or_insert_with(compute_is_param);
diff --git a/compiler/rustc_ast_lowering/src/lib.rs b/compiler/rustc_ast_lowering/src/lib.rs
index 47b92981626..ed033d86008 100644
--- a/compiler/rustc_ast_lowering/src/lib.rs
+++ b/compiler/rustc_ast_lowering/src/lib.rs
@@ -1425,19 +1425,16 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
                         this.arena.alloc_from_iter(bounds.iter().filter_map(|bound| match bound {
                             GenericBound::Trait(
                                 ty,
-                                modifier @ (TraitBoundModifier::None
-                                | TraitBoundModifier::MaybeConst(_)
-                                | TraitBoundModifier::Negative),
-                            ) => {
-                                Some(this.lower_poly_trait_ref(ty, itctx, modifier.to_constness()))
-                            }
-                            // `~const ?Bound` will cause an error during AST validation
-                            // anyways, so treat it like `?Bound` as compilation proceeds.
+                                TraitBoundModifiers {
+                                    polarity: BoundPolarity::Positive | BoundPolarity::Negative(_),
+                                    constness,
+                                },
+                            ) => Some(this.lower_poly_trait_ref(ty, itctx, (*constness).into())),
+                            // We can safely ignore constness here, since AST validation
+                            // will take care of invalid modifier combinations.
                             GenericBound::Trait(
                                 _,
-                                TraitBoundModifier::Maybe
-                                | TraitBoundModifier::MaybeConstMaybe
-                                | TraitBoundModifier::MaybeConstNegative,
+                                TraitBoundModifiers { polarity: BoundPolarity::Maybe(_), .. },
                             ) => None,
                             GenericBound::Outlives(lifetime) => {
                                 if lifetime_bound.is_none() {
@@ -2028,9 +2025,9 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
         itctx: &ImplTraitContext,
     ) -> hir::GenericBound<'hir> {
         match tpb {
-            GenericBound::Trait(p, modifier) => hir::GenericBound::Trait(
-                self.lower_poly_trait_ref(p, itctx, modifier.to_constness()),
-                self.lower_trait_bound_modifier(*modifier),
+            GenericBound::Trait(p, modifiers) => hir::GenericBound::Trait(
+                self.lower_poly_trait_ref(p, itctx, modifiers.constness.into()),
+                self.lower_trait_bound_modifiers(*modifiers),
             ),
             GenericBound::Outlives(lifetime) => {
                 hir::GenericBound::Outlives(self.lower_lifetime(lifetime))
@@ -2316,25 +2313,29 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
         }
     }
 
-    fn lower_trait_bound_modifier(&mut self, f: TraitBoundModifier) -> hir::TraitBoundModifier {
-        match f {
-            TraitBoundModifier::None => hir::TraitBoundModifier::None,
-            TraitBoundModifier::MaybeConst(_) => hir::TraitBoundModifier::MaybeConst,
-
-            TraitBoundModifier::Negative => {
+    fn lower_trait_bound_modifiers(
+        &mut self,
+        modifiers: TraitBoundModifiers,
+    ) -> hir::TraitBoundModifier {
+        match (modifiers.constness, modifiers.polarity) {
+            (BoundConstness::Never, BoundPolarity::Positive) => hir::TraitBoundModifier::None,
+            (BoundConstness::Never, BoundPolarity::Maybe(_)) => hir::TraitBoundModifier::Maybe,
+            (BoundConstness::Never, BoundPolarity::Negative(_)) => {
                 if self.tcx.features().negative_bounds {
                     hir::TraitBoundModifier::Negative
                 } else {
                     hir::TraitBoundModifier::None
                 }
             }
-
-            // `MaybeConstMaybe` will cause an error during AST validation, but we need to pick a
-            // placeholder for compilation to proceed.
-            TraitBoundModifier::MaybeConstMaybe | TraitBoundModifier::Maybe => {
-                hir::TraitBoundModifier::Maybe
+            (BoundConstness::Maybe(_), BoundPolarity::Positive) => {
+                hir::TraitBoundModifier::MaybeConst
+            }
+            // Invalid modifier combinations will cause an error during AST validation.
+            // Arbitrarily pick a placeholder for compilation to proceed.
+            (BoundConstness::Maybe(_), BoundPolarity::Maybe(_)) => hir::TraitBoundModifier::Maybe,
+            (BoundConstness::Maybe(_), BoundPolarity::Negative(_)) => {
+                hir::TraitBoundModifier::MaybeConst
             }
-            TraitBoundModifier::MaybeConstNegative => hir::TraitBoundModifier::MaybeConst,
         }
     }
 
diff --git a/compiler/rustc_ast_passes/messages.ftl b/compiler/rustc_ast_passes/messages.ftl
index 1c5ad820bd6..ea3cd3e4bee 100644
--- a/compiler/rustc_ast_passes/messages.ftl
+++ b/compiler/rustc_ast_passes/messages.ftl
@@ -152,6 +152,8 @@ ast_passes_impl_trait_path = `impl Trait` is not allowed in path parameters
 ast_passes_incompatible_features = `{$f1}` and `{$f2}` are incompatible, using them at the same time is not allowed
     .help = remove one of these features
 
+ast_passes_incompatible_trait_bound_modifiers = `{$left}` and `{$right}` are mutually exclusive
+
 ast_passes_inherent_cannot_be = inherent impls cannot be {$annotation}
     .because = {$annotation} because of this
     .type = inherent impl for this type
@@ -195,8 +197,6 @@ ast_passes_nomangle_ascii = `#[no_mangle]` requires ASCII identifier
 ast_passes_obsolete_auto = `impl Trait for .. {"{}"}` is an obsolete syntax
     .help = use `auto trait Trait {"{}"}` instead
 
-ast_passes_optional_const_exclusive = `~const` and `{$modifier}` are mutually exclusive
-
 ast_passes_optional_trait_object = `?Trait` is not permitted in trait object types
 
 ast_passes_optional_trait_supertrait = `?Trait` is not permitted in supertraits
diff --git a/compiler/rustc_ast_passes/src/ast_validation.rs b/compiler/rustc_ast_passes/src/ast_validation.rs
index 887cb434a60..23a45749455 100644
--- a/compiler/rustc_ast_passes/src/ast_validation.rs
+++ b/compiler/rustc_ast_passes/src/ast_validation.rs
@@ -1196,18 +1196,18 @@ impl<'a> Visitor<'a> for AstValidator<'a> {
     }
 
     fn visit_param_bound(&mut self, bound: &'a GenericBound, ctxt: BoundKind) {
-        if let GenericBound::Trait(poly, modify) = bound {
-            match (ctxt, modify) {
-                (BoundKind::SuperTraits, TraitBoundModifier::Maybe) => {
+        if let GenericBound::Trait(poly, modifiers) = bound {
+            match (ctxt, modifiers.constness, modifiers.polarity) {
+                (BoundKind::SuperTraits, BoundConstness::Never, BoundPolarity::Maybe(_)) => {
                     self.dcx().emit_err(errors::OptionalTraitSupertrait {
                         span: poly.span,
                         path_str: pprust::path_to_string(&poly.trait_ref.path),
                     });
                 }
-                (BoundKind::TraitObject, TraitBoundModifier::Maybe) => {
+                (BoundKind::TraitObject, BoundConstness::Never, BoundPolarity::Maybe(_)) => {
                     self.dcx().emit_err(errors::OptionalTraitObject { span: poly.span });
                 }
-                (_, &TraitBoundModifier::MaybeConst(span))
+                (_, BoundConstness::Maybe(span), BoundPolarity::Positive)
                     if let Some(reason) = &self.disallow_tilde_const =>
                 {
                     let reason = match reason {
@@ -1235,16 +1235,15 @@ impl<'a> Visitor<'a> for AstValidator<'a> {
                     };
                     self.dcx().emit_err(errors::TildeConstDisallowed { span, reason });
                 }
-                (_, TraitBoundModifier::MaybeConstMaybe) => {
-                    self.dcx().emit_err(errors::OptionalConstExclusive {
+                (
+                    _,
+                    BoundConstness::Maybe(_),
+                    BoundPolarity::Maybe(_) | BoundPolarity::Negative(_),
+                ) => {
+                    self.dcx().emit_err(errors::IncompatibleTraitBoundModifiers {
                         span: bound.span(),
-                        modifier: "?",
-                    });
-                }
-                (_, TraitBoundModifier::MaybeConstNegative) => {
-                    self.dcx().emit_err(errors::OptionalConstExclusive {
-                        span: bound.span(),
-                        modifier: "!",
+                        left: modifiers.constness.as_str(),
+                        right: modifiers.polarity.as_str(),
                     });
                 }
                 _ => {}
@@ -1252,7 +1251,8 @@ impl<'a> Visitor<'a> for AstValidator<'a> {
         }
 
         // Negative trait bounds are not allowed to have associated constraints
-        if let GenericBound::Trait(trait_ref, TraitBoundModifier::Negative) = bound
+        if let GenericBound::Trait(trait_ref, modifiers) = bound
+            && let BoundPolarity::Negative(_) = modifiers.polarity
             && let Some(segment) = trait_ref.trait_ref.path.segments.last()
             && let Some(ast::GenericArgs::AngleBracketed(args)) = segment.args.as_deref()
         {
@@ -1494,7 +1494,8 @@ fn deny_equality_constraints(
             for param in &generics.params {
                 if param.ident == potential_param.ident {
                     for bound in &param.bounds {
-                        if let ast::GenericBound::Trait(trait_ref, TraitBoundModifier::None) = bound
+                        if let ast::GenericBound::Trait(trait_ref, TraitBoundModifiers::NONE) =
+                            bound
                         {
                             if let [trait_segment] = &trait_ref.trait_ref.path.segments[..] {
                                 let assoc = pprust::path_to_string(&ast::Path::from_ident(
diff --git a/compiler/rustc_ast_passes/src/errors.rs b/compiler/rustc_ast_passes/src/errors.rs
index 304c5c1bde9..a5b842b320e 100644
--- a/compiler/rustc_ast_passes/src/errors.rs
+++ b/compiler/rustc_ast_passes/src/errors.rs
@@ -580,11 +580,12 @@ pub enum TildeConstReason {
 }
 
 #[derive(Diagnostic)]
-#[diag(ast_passes_optional_const_exclusive)]
-pub struct OptionalConstExclusive {
+#[diag(ast_passes_incompatible_trait_bound_modifiers)]
+pub struct IncompatibleTraitBoundModifiers {
     #[primary_span]
     pub span: Span,
-    pub modifier: &'static str,
+    pub left: &'static str,
+    pub right: &'static str,
 }
 
 #[derive(Diagnostic)]
diff --git a/compiler/rustc_ast_pretty/src/lib.rs b/compiler/rustc_ast_pretty/src/lib.rs
index 670f2a45835..100b2988982 100644
--- a/compiler/rustc_ast_pretty/src/lib.rs
+++ b/compiler/rustc_ast_pretty/src/lib.rs
@@ -4,6 +4,7 @@
 #![deny(rustc::untranslatable_diagnostic)]
 #![deny(rustc::diagnostic_outside_of_impl)]
 #![feature(box_patterns)]
+#![feature(let_chains)]
 #![recursion_limit = "256"]
 
 mod helpers;
diff --git a/compiler/rustc_ast_pretty/src/pprust/state.rs b/compiler/rustc_ast_pretty/src/pprust/state.rs
index d6c15ec35b6..2ad8aa38bcc 100644
--- a/compiler/rustc_ast_pretty/src/pprust/state.rs
+++ b/compiler/rustc_ast_pretty/src/pprust/state.rs
@@ -17,7 +17,7 @@ use rustc_ast::util::comments::{gather_comments, Comment, CommentStyle};
 use rustc_ast::util::parser;
 use rustc_ast::{self as ast, AttrArgs, AttrArgsEq, BlockCheckMode, PatKind};
 use rustc_ast::{attr, BindingAnnotation, ByRef, DelimArgs, RangeEnd, RangeSyntax, Term};
-use rustc_ast::{GenericArg, GenericBound, SelfKind, TraitBoundModifier};
+use rustc_ast::{GenericArg, GenericBound, SelfKind};
 use rustc_ast::{InlineAsmOperand, InlineAsmRegOrRegClass};
 use rustc_ast::{InlineAsmOptions, InlineAsmTemplatePiece};
 use rustc_span::edition::Edition;
@@ -1559,26 +1559,20 @@ impl<'a> State<'a> {
 
             match bound {
                 GenericBound::Trait(tref, modifier) => {
-                    match modifier {
-                        TraitBoundModifier::None => {}
-                        TraitBoundModifier::Negative => {
-                            self.word("!");
-                        }
-                        TraitBoundModifier::Maybe => {
-                            self.word("?");
-                        }
-                        TraitBoundModifier::MaybeConst(_) => {
-                            self.word_space("~const");
-                        }
-                        TraitBoundModifier::MaybeConstNegative => {
-                            self.word_space("~const");
-                            self.word("!");
-                        }
-                        TraitBoundModifier::MaybeConstMaybe => {
-                            self.word_space("~const");
-                            self.word("?");
+                    match modifier.constness {
+                        ast::BoundConstness::Never => {}
+                        ast::BoundConstness::Maybe(_) => {
+                            self.word_space(modifier.constness.as_str());
                         }
                     }
+
+                    match modifier.polarity {
+                        ast::BoundPolarity::Positive => {}
+                        ast::BoundPolarity::Negative(_) | ast::BoundPolarity::Maybe(_) => {
+                            self.word(modifier.polarity.as_str());
+                        }
+                    }
+
                     self.print_poly_trait_ref(tref);
                 }
                 GenericBound::Outlives(lt) => self.print_lifetime(*lt),
diff --git a/compiler/rustc_ast_pretty/src/pprust/state/item.rs b/compiler/rustc_ast_pretty/src/pprust/state/item.rs
index 405ccc722d4..247061c5ca7 100644
--- a/compiler/rustc_ast_pretty/src/pprust/state/item.rs
+++ b/compiler/rustc_ast_pretty/src/pprust/state/item.rs
@@ -339,13 +339,15 @@ impl<'a> State<'a> {
                 self.print_ident(item.ident);
                 self.print_generic_params(&generics.params);
                 let mut real_bounds = Vec::with_capacity(bounds.len());
-                for b in bounds.iter() {
-                    if let GenericBound::Trait(ptr, ast::TraitBoundModifier::Maybe) = b {
+                for bound in bounds.iter() {
+                    if let GenericBound::Trait(ptr, modifiers) = bound
+                        && let ast::BoundPolarity::Maybe(_) = modifiers.polarity
+                    {
                         self.space();
                         self.word_space("for ?");
                         self.print_trait_ref(&ptr.trait_ref);
                     } else {
-                        real_bounds.push(b.clone());
+                        real_bounds.push(bound.clone());
                     }
                 }
                 if !real_bounds.is_empty() {
diff --git a/compiler/rustc_expand/src/build.rs b/compiler/rustc_expand/src/build.rs
index 86f555fa08b..f9eddfeeaa8 100644
--- a/compiler/rustc_expand/src/build.rs
+++ b/compiler/rustc_expand/src/build.rs
@@ -134,10 +134,13 @@ impl<'a> ExtCtxt<'a> {
     pub fn trait_bound(&self, path: ast::Path, is_const: bool) -> ast::GenericBound {
         ast::GenericBound::Trait(
             self.poly_trait_ref(path.span, path),
-            if is_const {
-                ast::TraitBoundModifier::MaybeConst(DUMMY_SP)
-            } else {
-                ast::TraitBoundModifier::None
+            ast::TraitBoundModifiers {
+                polarity: ast::BoundPolarity::Positive,
+                constness: if is_const {
+                    ast::BoundConstness::Maybe(DUMMY_SP)
+                } else {
+                    ast::BoundConstness::Never
+                },
             },
         )
     }
diff --git a/compiler/rustc_hir/src/hir.rs b/compiler/rustc_hir/src/hir.rs
index 3179fd73604..26430dcf965 100644
--- a/compiler/rustc_hir/src/hir.rs
+++ b/compiler/rustc_hir/src/hir.rs
@@ -417,8 +417,7 @@ pub enum GenericArgsParentheses {
     ParenSugar,
 }
 
-/// A modifier on a bound, currently this is only used for `?Sized`, where the
-/// modifier is `Maybe`. Negative bounds should also be handled here.
+/// A modifier on a trait bound.
 #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, HashStable_Generic)]
 pub enum TraitBoundModifier {
     None,
diff --git a/compiler/rustc_parse/src/parser/path.rs b/compiler/rustc_parse/src/parser/path.rs
index 8970a99f1b7..405531c1e41 100644
--- a/compiler/rustc_parse/src/parser/path.rs
+++ b/compiler/rustc_parse/src/parser/path.rs
@@ -840,7 +840,7 @@ impl<'a> Parser<'a> {
             {
                 return Ok((false, seg.ident, seg.args.as_deref().cloned()));
             } else if let ast::TyKind::TraitObject(bounds, ast::TraitObjectSyntax::None) = &ty.kind
-                && let [ast::GenericBound::Trait(trait_ref, ast::TraitBoundModifier::None)] =
+                && let [ast::GenericBound::Trait(trait_ref, ast::TraitBoundModifiers::NONE)] =
                     bounds.as_slice()
                 && let [seg] = trait_ref.trait_ref.path.segments.as_slice()
             {
diff --git a/compiler/rustc_parse/src/parser/ty.rs b/compiler/rustc_parse/src/parser/ty.rs
index da8cc05ff66..f89d6d1d965 100644
--- a/compiler/rustc_parse/src/parser/ty.rs
+++ b/compiler/rustc_parse/src/parser/ty.rs
@@ -13,37 +13,15 @@ use rustc_ast::ptr::P;
 use rustc_ast::token::{self, Delimiter, Token, TokenKind};
 use rustc_ast::util::case::Case;
 use rustc_ast::{
-    self as ast, BareFnTy, BoundPolarity, FnRetTy, GenericBound, GenericBounds, GenericParam,
-    Generics, Lifetime, MacCall, MutTy, Mutability, PolyTraitRef, TraitBoundModifier,
-    TraitObjectSyntax, Ty, TyKind,
+    self as ast, BareFnTy, BoundConstness, BoundPolarity, FnRetTy, GenericBound, GenericBounds,
+    GenericParam, Generics, Lifetime, MacCall, MutTy, Mutability, PolyTraitRef,
+    TraitBoundModifiers, TraitObjectSyntax, Ty, TyKind,
 };
 use rustc_errors::{Applicability, PResult};
 use rustc_span::symbol::{kw, sym, Ident};
 use rustc_span::{Span, Symbol};
 use thin_vec::{thin_vec, ThinVec};
 
-/// Any `?`, `!`, or `~const` modifiers that appear at the start of a bound.
-struct BoundModifiers {
-    /// `?Trait`.
-    bound_polarity: BoundPolarity,
-
-    /// `~const Trait`.
-    maybe_const: Option<Span>,
-}
-
-impl BoundModifiers {
-    fn to_trait_bound_modifier(&self) -> TraitBoundModifier {
-        match (self.bound_polarity, self.maybe_const) {
-            (BoundPolarity::Positive, None) => TraitBoundModifier::None,
-            (BoundPolarity::Negative(_), None) => TraitBoundModifier::Negative,
-            (BoundPolarity::Maybe(_), None) => TraitBoundModifier::Maybe,
-            (BoundPolarity::Positive, Some(sp)) => TraitBoundModifier::MaybeConst(sp),
-            (BoundPolarity::Negative(_), Some(_)) => TraitBoundModifier::MaybeConstNegative,
-            (BoundPolarity::Maybe(_), Some(_)) => TraitBoundModifier::MaybeConstMaybe,
-        }
-    }
-}
-
 #[derive(Copy, Clone, PartialEq)]
 pub(super) enum AllowPlus {
     Yes,
@@ -461,7 +439,7 @@ impl<'a> Parser<'a> {
         parse_plus: bool,
     ) -> PResult<'a, TyKind> {
         let poly_trait_ref = PolyTraitRef::new(generic_params, path, lo.to(self.prev_token.span));
-        let bounds = vec![GenericBound::Trait(poly_trait_ref, TraitBoundModifier::None)];
+        let bounds = vec![GenericBound::Trait(poly_trait_ref, TraitBoundModifiers::NONE)];
         self.parse_remaining_bounds(bounds, parse_plus)
     }
 
@@ -800,7 +778,7 @@ impl<'a> Parser<'a> {
         let has_parens = self.eat(&token::OpenDelim(Delimiter::Parenthesis));
         let inner_lo = self.token.span;
 
-        let modifiers = self.parse_ty_bound_modifiers()?;
+        let modifiers = self.parse_trait_bound_modifiers()?;
         let bound = if self.token.is_lifetime() {
             self.error_lt_bound_with_modifiers(modifiers);
             self.parse_generic_lt_bound(lo, inner_lo, has_parens)?
@@ -831,18 +809,21 @@ impl<'a> Parser<'a> {
     }
 
     /// Emits an error if any trait bound modifiers were present.
-    fn error_lt_bound_with_modifiers(&self, modifiers: BoundModifiers) {
-        if let Some(span) = modifiers.maybe_const {
-            self.sess.emit_err(errors::TildeConstLifetime { span });
+    fn error_lt_bound_with_modifiers(&self, modifiers: TraitBoundModifiers) {
+        match modifiers.constness {
+            BoundConstness::Never => {}
+            BoundConstness::Maybe(span) => {
+                self.sess.emit_err(errors::TildeConstLifetime { span });
+            }
         }
 
-        match modifiers.bound_polarity {
+        match modifiers.polarity {
             BoundPolarity::Positive => {}
-            BoundPolarity::Negative(span) => {
-                self.sess.emit_err(errors::ModifierLifetime { span, sigil: "!" });
-            }
-            BoundPolarity::Maybe(span) => {
-                self.sess.emit_err(errors::ModifierLifetime { span, sigil: "?" });
+            BoundPolarity::Negative(span) | BoundPolarity::Maybe(span) => {
+                self.sess.emit_err(errors::ModifierLifetime {
+                    span,
+                    sigil: modifiers.polarity.as_str(),
+                });
             }
         }
     }
@@ -867,26 +848,26 @@ impl<'a> Parser<'a> {
     /// If no modifiers are present, this does not consume any tokens.
     ///
     /// ```ebnf
-    /// TY_BOUND_MODIFIERS = ["~const"] ["?"]
+    /// TRAIT_BOUND_MODIFIERS = ["~const"] ["?" | "!"]
     /// ```
-    fn parse_ty_bound_modifiers(&mut self) -> PResult<'a, BoundModifiers> {
-        let maybe_const = if self.eat(&token::Tilde) {
+    fn parse_trait_bound_modifiers(&mut self) -> PResult<'a, TraitBoundModifiers> {
+        let constness = if self.eat(&token::Tilde) {
             let tilde = self.prev_token.span;
             self.expect_keyword(kw::Const)?;
             let span = tilde.to(self.prev_token.span);
             self.sess.gated_spans.gate(sym::const_trait_impl, span);
-            Some(span)
+            BoundConstness::Maybe(span)
         } else if self.eat_keyword(kw::Const) {
             let span = self.prev_token.span;
             self.sess.gated_spans.gate(sym::const_trait_impl, span);
             self.sess.emit_err(errors::ConstMissingTilde { span, start: span.shrink_to_lo() });
 
-            Some(span)
+            BoundConstness::Maybe(span)
         } else {
-            None
+            BoundConstness::Never
         };
 
-        let bound_polarity = if self.eat(&token::Question) {
+        let polarity = if self.eat(&token::Question) {
             BoundPolarity::Maybe(self.prev_token.span)
         } else if self.eat(&token::Not) {
             self.sess.gated_spans.gate(sym::negative_bounds, self.prev_token.span);
@@ -895,13 +876,13 @@ impl<'a> Parser<'a> {
             BoundPolarity::Positive
         };
 
-        Ok(BoundModifiers { bound_polarity, maybe_const })
+        Ok(TraitBoundModifiers { constness, polarity })
     }
 
     /// Parses a type bound according to:
     /// ```ebnf
     /// TY_BOUND = TY_BOUND_NOPAREN | (TY_BOUND_NOPAREN)
-    /// TY_BOUND_NOPAREN = [TY_BOUND_MODIFIERS] [for<LT_PARAM_DEFS>] SIMPLE_PATH
+    /// TY_BOUND_NOPAREN = [TRAIT_BOUND_MODIFIERS] [for<LT_PARAM_DEFS>] SIMPLE_PATH
     /// ```
     ///
     /// For example, this grammar accepts `~const ?for<'a: 'b> m::Trait<'a>`.
@@ -909,7 +890,7 @@ impl<'a> Parser<'a> {
         &mut self,
         lo: Span,
         has_parens: bool,
-        modifiers: BoundModifiers,
+        modifiers: TraitBoundModifiers,
         leading_token: &Token,
     ) -> PResult<'a, GenericBound> {
         let mut lifetime_defs = self.parse_late_bound_lifetime_defs()?;
@@ -991,9 +972,8 @@ impl<'a> Parser<'a> {
             }
         }
 
-        let modifier = modifiers.to_trait_bound_modifier();
         let poly_trait = PolyTraitRef::new(lifetime_defs, path, lo.to(self.prev_token.span));
-        Ok(GenericBound::Trait(poly_trait, modifier))
+        Ok(GenericBound::Trait(poly_trait, modifiers))
     }
 
     // recovers a `Fn(..)` parenthesized-style path from `fn(..)`
diff --git a/compiler/rustc_resolve/src/late/diagnostics.rs b/compiler/rustc_resolve/src/late/diagnostics.rs
index d767ed74139..376ccfba9d8 100644
--- a/compiler/rustc_resolve/src/late/diagnostics.rs
+++ b/compiler/rustc_resolve/src/late/diagnostics.rs
@@ -520,7 +520,7 @@ impl<'a: 'ast, 'ast, 'tcx> LateResolutionVisitor<'a, '_, 'ast, 'tcx> {
                     continue;
                 };
                 for bound in bounds {
-                    let ast::GenericBound::Trait(trait_ref, ast::TraitBoundModifier::None) = bound
+                    let ast::GenericBound::Trait(trait_ref, ast::TraitBoundModifiers::NONE) = bound
                     else {
                         continue;
                     };
@@ -1242,7 +1242,7 @@ impl<'a: 'ast, 'ast, 'tcx> LateResolutionVisitor<'a, '_, 'ast, 'tcx> {
             }
             if let (
                 [ast::PathSegment { args: None, .. }],
-                [ast::GenericBound::Trait(poly_trait_ref, ast::TraitBoundModifier::None)],
+                [ast::GenericBound::Trait(poly_trait_ref, ast::TraitBoundModifiers::NONE)],
             ) = (&type_param_path.segments[..], &bounds[..])
             {
                 if let [ast::PathSegment { ident, args: None, .. }] =
@@ -3276,7 +3276,7 @@ fn mk_where_bound_predicate(
                 },
                 span: DUMMY_SP,
             },
-            ast::TraitBoundModifier::None,
+            ast::TraitBoundModifiers::NONE,
         )],
     };
 
diff --git a/src/tools/rustfmt/src/types.rs b/src/tools/rustfmt/src/types.rs
index a5a4244903c..cd2582e66be 100644
--- a/src/tools/rustfmt/src/types.rs
+++ b/src/tools/rustfmt/src/types.rs
@@ -537,28 +537,19 @@ impl Rewrite for ast::Lifetime {
 impl Rewrite for ast::GenericBound {
     fn rewrite(&self, context: &RewriteContext<'_>, shape: Shape) -> Option<String> {
         match *self {
-            ast::GenericBound::Trait(ref poly_trait_ref, trait_bound_modifier) => {
+            ast::GenericBound::Trait(ref poly_trait_ref, modifiers) => {
                 let snippet = context.snippet(self.span());
                 let has_paren = snippet.starts_with('(') && snippet.ends_with(')');
-                let rewrite = match trait_bound_modifier {
-                    ast::TraitBoundModifier::None => poly_trait_ref.rewrite(context, shape),
-                    ast::TraitBoundModifier::Maybe => poly_trait_ref
-                        .rewrite(context, shape.offset_left(1)?)
-                        .map(|s| format!("?{}", s)),
-                    ast::TraitBoundModifier::MaybeConst(_) => poly_trait_ref
-                        .rewrite(context, shape.offset_left(7)?)
-                        .map(|s| format!("~const {}", s)),
-                    ast::TraitBoundModifier::MaybeConstMaybe => poly_trait_ref
-                        .rewrite(context, shape.offset_left(8)?)
-                        .map(|s| format!("~const ?{}", s)),
-                    ast::TraitBoundModifier::Negative => poly_trait_ref
-                        .rewrite(context, shape.offset_left(1)?)
-                        .map(|s| format!("!{}", s)),
-                    ast::TraitBoundModifier::MaybeConstNegative => poly_trait_ref
-                        .rewrite(context, shape.offset_left(8)?)
-                        .map(|s| format!("~const !{}", s)),
-                };
-                rewrite.map(|s| if has_paren { format!("({})", s) } else { s })
+                let mut constness = modifiers.constness.as_str().to_string();
+                if !constness.is_empty() {
+                    constness.push(' ');
+                }
+                let polarity = modifiers.polarity.as_str();
+                let shape = shape.offset_left(constness.len() + polarity.len())?;
+                poly_trait_ref
+                    .rewrite(context, shape)
+                    .map(|s| format!("{constness}{polarity}{s}"))
+                    .map(|s| if has_paren { format!("({})", s) } else { s })
             }
             ast::GenericBound::Outlives(ref lifetime) => lifetime.rewrite(context, shape),
         }
diff --git a/tests/ui/stats/hir-stats.stderr b/tests/ui/stats/hir-stats.stderr
index e6da83296ce..070dbbb10bb 100644
--- a/tests/ui/stats/hir-stats.stderr
+++ b/tests/ui/stats/hir-stats.stderr
@@ -15,20 +15,20 @@ ast-stats-1 Arm                       96 ( 1.5%)             2            48
 ast-stats-1 ForeignItem               96 ( 1.5%)             1            96
 ast-stats-1 - Fn                        96 ( 1.5%)             1
 ast-stats-1 FnDecl                   120 ( 1.8%)             5            24
-ast-stats-1 FieldDef                 160 ( 2.5%)             2            80
-ast-stats-1 Stmt                     160 ( 2.5%)             5            32
+ast-stats-1 FieldDef                 160 ( 2.4%)             2            80
+ast-stats-1 Stmt                     160 ( 2.4%)             5            32
 ast-stats-1 - Local                     32 ( 0.5%)             1
 ast-stats-1 - MacCall                   32 ( 0.5%)             1
 ast-stats-1 - Expr                      96 ( 1.5%)             3
-ast-stats-1 Param                    160 ( 2.5%)             4            40
+ast-stats-1 Param                    160 ( 2.4%)             4            40
 ast-stats-1 Block                    192 ( 2.9%)             6            32
 ast-stats-1 Variant                  208 ( 3.2%)             2           104
-ast-stats-1 GenericBound             256 ( 3.9%)             4            64
-ast-stats-1 - Trait                    256 ( 3.9%)             4
+ast-stats-1 GenericBound             288 ( 4.4%)             4            72
+ast-stats-1 - Trait                    288 ( 4.4%)             4
 ast-stats-1 AssocItem                352 ( 5.4%)             4            88
 ast-stats-1 - Type                     176 ( 2.7%)             2
 ast-stats-1 - Fn                       176 ( 2.7%)             2
-ast-stats-1 GenericParam             480 ( 7.4%)             5            96
+ast-stats-1 GenericParam             480 ( 7.3%)             5            96
 ast-stats-1 Pat                      504 ( 7.7%)             7            72
 ast-stats-1 - Struct                    72 ( 1.1%)             1
 ast-stats-1 - Wild                      72 ( 1.1%)             1
@@ -45,15 +45,15 @@ ast-stats-1 - Ptr                       64 ( 1.0%)             1
 ast-stats-1 - Ref                       64 ( 1.0%)             1
 ast-stats-1 - ImplicitSelf             128 ( 2.0%)             2
 ast-stats-1 - Path                     640 ( 9.8%)            10
-ast-stats-1 Item                   1_224 (18.8%)             9           136
+ast-stats-1 Item                   1_224 (18.7%)             9           136
 ast-stats-1 - Trait                    136 ( 2.1%)             1
 ast-stats-1 - Enum                     136 ( 2.1%)             1
 ast-stats-1 - ForeignMod               136 ( 2.1%)             1
 ast-stats-1 - Impl                     136 ( 2.1%)             1
 ast-stats-1 - Fn                       272 ( 4.2%)             2
-ast-stats-1 - Use                      408 ( 6.3%)             3
+ast-stats-1 - Use                      408 ( 6.2%)             3
 ast-stats-1 ----------------------------------------------------------------
-ast-stats-1 Total                  6_520
+ast-stats-1 Total                  6_552
 ast-stats-1
 ast-stats-2 POST EXPANSION AST STATS
 ast-stats-2 Name                Accumulated Size         Count     Item Size
@@ -81,16 +81,16 @@ ast-stats-2 - Expr                      96 ( 1.3%)             3
 ast-stats-2 Param                    160 ( 2.2%)             4            40
 ast-stats-2 Block                    192 ( 2.7%)             6            32
 ast-stats-2 Variant                  208 ( 2.9%)             2           104
-ast-stats-2 GenericBound             256 ( 3.6%)             4            64
-ast-stats-2 - Trait                    256 ( 3.6%)             4
+ast-stats-2 GenericBound             288 ( 4.0%)             4            72
+ast-stats-2 - Trait                    288 ( 4.0%)             4
 ast-stats-2 AssocItem                352 ( 4.9%)             4            88
 ast-stats-2 - Type                     176 ( 2.5%)             2
 ast-stats-2 - Fn                       176 ( 2.5%)             2
 ast-stats-2 GenericParam             480 ( 6.7%)             5            96
-ast-stats-2 Pat                      504 ( 7.1%)             7            72
+ast-stats-2 Pat                      504 ( 7.0%)             7            72
 ast-stats-2 - Struct                    72 ( 1.0%)             1
 ast-stats-2 - Wild                      72 ( 1.0%)             1
-ast-stats-2 - Ident                    360 ( 5.1%)             5
+ast-stats-2 - Ident                    360 ( 5.0%)             5
 ast-stats-2 Expr                     648 ( 9.1%)             9            72
 ast-stats-2 - Path                      72 ( 1.0%)             1
 ast-stats-2 - Match                     72 ( 1.0%)             1
@@ -99,12 +99,12 @@ ast-stats-2 - InlineAsm                 72 ( 1.0%)             1
 ast-stats-2 - Lit                      144 ( 2.0%)             2
 ast-stats-2 - Block                    216 ( 3.0%)             3
 ast-stats-2 PathSegment              792 (11.1%)            33            24
-ast-stats-2 Ty                       896 (12.6%)            14            64
+ast-stats-2 Ty                       896 (12.5%)            14            64
 ast-stats-2 - Ptr                       64 ( 0.9%)             1
 ast-stats-2 - Ref                       64 ( 0.9%)             1
 ast-stats-2 - ImplicitSelf             128 ( 1.8%)             2
-ast-stats-2 - Path                     640 ( 9.0%)            10
-ast-stats-2 Item                   1_496 (21.0%)            11           136
+ast-stats-2 - Path                     640 ( 8.9%)            10
+ast-stats-2 Item                   1_496 (20.9%)            11           136
 ast-stats-2 - Trait                    136 ( 1.9%)             1
 ast-stats-2 - Enum                     136 ( 1.9%)             1
 ast-stats-2 - ExternCrate              136 ( 1.9%)             1
@@ -113,7 +113,7 @@ ast-stats-2 - Impl                     136 ( 1.9%)             1
 ast-stats-2 - Fn                       272 ( 3.8%)             2
 ast-stats-2 - Use                      544 ( 7.6%)             4
 ast-stats-2 ----------------------------------------------------------------
-ast-stats-2 Total                  7_120
+ast-stats-2 Total                  7_152
 ast-stats-2
 hir-stats HIR STATS
 hir-stats Name                Accumulated Size         Count     Item Size