diff --git a/compiler/rustc_borrowck/src/diagnostics/bound_region_errors.rs b/compiler/rustc_borrowck/src/diagnostics/bound_region_errors.rs
index 538c5887118e6..d7fe7ddf261ed 100644
--- a/compiler/rustc_borrowck/src/diagnostics/bound_region_errors.rs
+++ b/compiler/rustc_borrowck/src/diagnostics/bound_region_errors.rs
@@ -9,12 +9,12 @@ use rustc_infer::infer::{
 };
 use rustc_infer::traits::ObligationCause;
 use rustc_infer::traits::query::{
-    CanonicalTypeOpAscribeUserTypeGoal, CanonicalTypeOpDeeplyNormalizeGoal,
-    CanonicalTypeOpNormalizeGoal, CanonicalTypeOpProvePredicateGoal,
+    CanonicalTypeOpAscribeUserTypeGoal, CanonicalTypeOpNormalizeGoal,
+    CanonicalTypeOpProvePredicateGoal,
 };
 use rustc_middle::ty::error::TypeError;
 use rustc_middle::ty::{
-    self, RePlaceholder, Region, RegionVid, Ty, TyCtxt, TypeFoldable, UniverseIndex, Unnormalized,
+    self, RePlaceholder, Region, RegionVid, Ty, TyCtxt, TypeFoldable, UniverseIndex,
 };
 use rustc_span::Span;
 use rustc_trait_selection::error_reporting::InferCtxtErrorExt;
@@ -109,14 +109,6 @@ impl<'tcx, T: Copy + fmt::Display + TypeFoldable<TyCtxt<'tcx>> + 'tcx> ToUnivers
     }
 }
 
-impl<'tcx, T: Copy + fmt::Display + TypeFoldable<TyCtxt<'tcx>> + 'tcx> ToUniverseInfo<'tcx>
-    for CanonicalTypeOpDeeplyNormalizeGoal<'tcx, T>
-{
-    fn to_universe_info(self, base_universe: ty::UniverseIndex) -> UniverseInfo<'tcx> {
-        UniverseInfo::TypeOp(Rc::new(DeeplyNormalizeQuery { canonical_query: self, base_universe }))
-    }
-}
-
 impl<'tcx> ToUniverseInfo<'tcx> for CanonicalTypeOpAscribeUserTypeGoal<'tcx> {
     fn to_universe_info(self, base_universe: ty::UniverseIndex) -> UniverseInfo<'tcx> {
         UniverseInfo::TypeOp(Rc::new(AscribeUserTypeQuery { canonical_query: self, base_universe }))
@@ -247,7 +239,14 @@ where
     fn fallback_error(&self, tcx: TyCtxt<'tcx>, span: Span) -> Diag<'tcx> {
         tcx.dcx().create_err(HigherRankedLifetimeError {
             cause: Some(HigherRankedErrorCause::CouldNotNormalize {
-                value: self.canonical_query.canonical.value.value.value.to_string(),
+                value: self
+                    .canonical_query
+                    .canonical
+                    .value
+                    .value
+                    .value
+                    .skip_normalization()
+                    .to_string(),
             }),
             span,
         })
@@ -275,54 +274,7 @@ where
         // the former fails to normalize the `nll/relate_tys/impl-fn-ignore-binder-via-bottom.rs`
         // test. Check after #85499 lands to see if its fixes have erased this difference.
         let ty::ParamEnvAnd { param_env, value } = key;
-        let _ = ocx.normalize(&cause, param_env, Unnormalized::new_wip(value.value));
-
-        let diag = try_extract_error_from_fulfill_cx(
-            &ocx,
-            mbcx.mir_def_id(),
-            placeholder_region,
-            error_region,
-        )?
-        .with_dcx(mbcx.dcx());
-        Some(diag)
-    }
-}
-
-struct DeeplyNormalizeQuery<'tcx, T> {
-    canonical_query: CanonicalTypeOpDeeplyNormalizeGoal<'tcx, T>,
-    base_universe: ty::UniverseIndex,
-}
-
-impl<'tcx, T> TypeOpInfo<'tcx> for DeeplyNormalizeQuery<'tcx, T>
-where
-    T: Copy + fmt::Display + TypeFoldable<TyCtxt<'tcx>> + 'tcx,
-{
-    fn fallback_error(&self, tcx: TyCtxt<'tcx>, span: Span) -> Diag<'tcx> {
-        tcx.dcx().create_err(HigherRankedLifetimeError {
-            cause: Some(HigherRankedErrorCause::CouldNotNormalize {
-                value: self.canonical_query.canonical.value.value.value.to_string(),
-            }),
-            span,
-        })
-    }
-
-    fn base_universe(&self) -> ty::UniverseIndex {
-        self.base_universe
-    }
-
-    fn nice_error<'infcx>(
-        &self,
-        mbcx: &mut MirBorrowckCtxt<'_, 'infcx, 'tcx>,
-        cause: ObligationCause<'tcx>,
-        placeholder_region: ty::Region<'tcx>,
-        error_region: Option<ty::Region<'tcx>>,
-    ) -> Option<Diag<'infcx>> {
-        let (infcx, key, _) =
-            mbcx.infcx.tcx.infer_ctxt().build_with_canonical(cause.span, &self.canonical_query);
-        let ocx = ObligationCtxt::new(&infcx);
-
-        let ty::ParamEnvAnd { param_env, value } = key;
-        let _ = ocx.deeply_normalize(&cause, param_env, Unnormalized::new_wip(value.value));
+        let _ = ocx.normalize(&cause, param_env, value.value);
 
         let diag = try_extract_error_from_fulfill_cx(
             &ocx,
diff --git a/compiler/rustc_borrowck/src/type_check/canonical.rs b/compiler/rustc_borrowck/src/type_check/canonical.rs
index f4e345dbba9bc..08b0ec8edbca2 100644
--- a/compiler/rustc_borrowck/src/type_check/canonical.rs
+++ b/compiler/rustc_borrowck/src/type_check/canonical.rs
@@ -196,16 +196,19 @@ impl<'a, 'tcx> TypeChecker<'a, 'tcx> {
         self.normalize_with_category(value, location, ConstraintCategory::Boring)
     }
 
-    pub(super) fn deeply_normalize<T>(&mut self, value: T, location: impl NormalizeLocation) -> T
+    pub(super) fn deeply_normalize<T>(
+        &mut self,
+        value: Unnormalized<'tcx, T>,
+        location: impl NormalizeLocation,
+    ) -> Result<T, ErrorGuaranteed>
     where
         T: type_op::normalize::Normalizable<'tcx> + fmt::Display + Copy + 'tcx,
     {
-        let result: Result<_, ErrorGuaranteed> = self.fully_perform_op(
+        self.fully_perform_op(
             location.to_locations(),
             ConstraintCategory::Boring,
-            self.infcx.param_env.and(type_op::normalize::DeeplyNormalize { value }),
-        );
-        result.unwrap_or(value)
+            self.infcx.param_env.and(type_op::normalize::Normalize { value }),
+        )
     }
 
     #[instrument(skip(self), level = "debug")]
@@ -218,14 +221,13 @@ impl<'a, 'tcx> TypeChecker<'a, 'tcx> {
     where
         T: type_op::normalize::Normalizable<'tcx> + fmt::Display + Copy + 'tcx,
     {
-        let value = value.skip_normalization();
         let param_env = self.infcx.param_env;
         let result: Result<_, ErrorGuaranteed> = self.fully_perform_op(
             location.to_locations(),
             category,
             param_env.and(type_op::normalize::Normalize { value }),
         );
-        result.unwrap_or(value)
+        result.unwrap_or(value.skip_norm_wip())
     }
 
     #[instrument(skip(self), level = "debug")]
diff --git a/compiler/rustc_borrowck/src/type_check/constraint_conversion.rs b/compiler/rustc_borrowck/src/type_check/constraint_conversion.rs
index 82e623bb42535..17d75986d28e7 100644
--- a/compiler/rustc_borrowck/src/type_check/constraint_conversion.rs
+++ b/compiler/rustc_borrowck/src/type_check/constraint_conversion.rs
@@ -5,7 +5,7 @@ use rustc_infer::infer::canonical::{QueryRegionConstraint, QueryRegionConstraint
 use rustc_infer::infer::outlives::env::RegionBoundPairs;
 use rustc_infer::infer::outlives::obligations::{TypeOutlives, TypeOutlivesDelegate};
 use rustc_infer::infer::region_constraints::{GenericKind, VerifyBound};
-use rustc_infer::traits::query::type_op::DeeplyNormalize;
+use rustc_infer::traits::query::type_op::Normalize;
 use rustc_middle::bug;
 use rustc_middle::ty::{
     self, GenericArgKind, Ty, TyCtxt, TypeFoldable, TypeVisitableExt, elaborate, fold_regions,
@@ -183,7 +183,7 @@ impl<'a, 'tcx> ConstraintConversion<'a, 'tcx> {
                         // in the new trait solver.
                         if infcx.next_trait_solver() {
                             t1 = self.normalize_and_add_type_outlives_constraints(
-                                t1,
+                                ty::Unnormalized::new_wip(t1),
                                 &mut next_outlives_predicates,
                             );
                         }
@@ -279,15 +279,17 @@ impl<'a, 'tcx> ConstraintConversion<'a, 'tcx> {
         self.constraints.type_tests.push(type_test);
     }
 
+    // FIXME(trait-refactor-initiative#260): This function needs to
+    // be removed.
     fn normalize_and_add_type_outlives_constraints(
         &self,
-        ty: Ty<'tcx>,
+        ty: ty::Unnormalized<'tcx, Ty<'tcx>>,
         next_outlives_predicates: &mut Vec<(
             ty::ArgOutlivesPredicate<'tcx>,
             ConstraintCategory<'tcx>,
         )>,
     ) -> Ty<'tcx> {
-        match self.infcx.fully_perform(DeeplyNormalize { value: ty }, self.span) {
+        match self.infcx.fully_perform(Normalize { value: ty }, self.span) {
             Ok(TypeOpOutput { output: ty, constraints, .. }) => {
                 // FIXME(higher_ranked_auto): What should we do with the assumptions here?
                 if let Some(QueryRegionConstraints { constraints, assumptions: _ }) = constraints {
@@ -299,7 +301,7 @@ impl<'a, 'tcx> ConstraintConversion<'a, 'tcx> {
                 }
                 ty
             }
-            Err(_) => ty,
+            Err(_) => ty.skip_norm_wip(),
         }
     }
 }
diff --git a/compiler/rustc_borrowck/src/type_check/free_region_relations.rs b/compiler/rustc_borrowck/src/type_check/free_region_relations.rs
index 62becc8e298f4..a89d45c4b0b2a 100644
--- a/compiler/rustc_borrowck/src/type_check/free_region_relations.rs
+++ b/compiler/rustc_borrowck/src/type_check/free_region_relations.rs
@@ -5,7 +5,7 @@ use rustc_infer::infer::canonical::QueryRegionConstraints;
 use rustc_infer::infer::outlives;
 use rustc_infer::infer::outlives::env::RegionBoundPairs;
 use rustc_infer::infer::region_constraints::GenericKind;
-use rustc_infer::traits::query::type_op::DeeplyNormalize;
+use rustc_infer::traits::query::type_op::Normalize;
 use rustc_middle::mir::ConstraintCategory;
 use rustc_middle::traits::query::OutlivesBound;
 use rustc_middle::ty::{self, RegionVid, Ty, TypeVisitableExt};
@@ -244,7 +244,7 @@ impl<'tcx> UniversalRegionRelationsBuilder<'_, 'tcx> {
             }
             let TypeOpOutput { output: norm_ty, constraints: constraints_normalize, .. } = self
                 .infcx
-                .fully_perform(DeeplyNormalize { value: ty }, span)
+                .fully_perform(Normalize { value: ty::Unnormalized::new_wip(ty) }, span)
                 .unwrap_or_else(|guar| TypeOpOutput {
                     output: Ty::new_error(self.infcx.tcx, guar),
                     constraints: None,
@@ -298,8 +298,9 @@ impl<'tcx> UniversalRegionRelationsBuilder<'_, 'tcx> {
         if matches!(tcx.def_kind(defining_ty_def_id), DefKind::AssocFn | DefKind::AssocConst { .. })
         {
             for &(ty, _) in tcx.assumed_wf_types(tcx.local_parent(defining_ty_def_id)) {
-                let result: Result<_, ErrorGuaranteed> =
-                    self.infcx.fully_perform(DeeplyNormalize { value: ty }, span);
+                let result: Result<_, ErrorGuaranteed> = self
+                    .infcx
+                    .fully_perform(Normalize { value: ty::Unnormalized::new_wip(ty) }, span);
                 let Ok(TypeOpOutput { output: norm_ty, constraints: c, .. }) = result else {
                     continue;
                 };
@@ -348,18 +349,24 @@ impl<'tcx> UniversalRegionRelationsBuilder<'_, 'tcx> {
     ) {
         // In the new solver, normalize the type-outlives obligation assumptions.
         if self.infcx.next_trait_solver() {
-            let Ok(TypeOpOutput {
-                output: normalized_outlives,
-                constraints: constraints_normalize,
-                error_info: _,
-            }) = self.infcx.fully_perform(DeeplyNormalize { value: outlives }, span)
-            else {
-                self.infcx.dcx().delayed_bug(format!("could not normalize {outlives:?}"));
-                return;
-            };
-            outlives = normalized_outlives;
-            if let Some(c) = constraints_normalize {
-                constraints.push(c);
+            match self
+                .infcx
+                .fully_perform(Normalize { value: ty::Unnormalized::new_wip(outlives) }, span)
+            {
+                Ok(TypeOpOutput {
+                    output: normalized_outlives,
+                    constraints: constraints_normalize,
+                    error_info: _,
+                }) => {
+                    outlives = normalized_outlives;
+                    if let Some(c) = constraints_normalize {
+                        constraints.push(c);
+                    }
+                }
+                Err(guar) => {
+                    let _: ErrorGuaranteed = guar;
+                    return;
+                }
             }
         }
 
diff --git a/compiler/rustc_borrowck/src/type_check/mod.rs b/compiler/rustc_borrowck/src/type_check/mod.rs
index 9c4b1f998dcee..aa68b878a938e 100644
--- a/compiler/rustc_borrowck/src/type_check/mod.rs
+++ b/compiler/rustc_borrowck/src/type_check/mod.rs
@@ -828,7 +828,15 @@ impl<'a, 'tcx> Visitor<'tcx> for TypeChecker<'a, 'tcx> {
                     ConstraintCategory::Boring,
                 );
 
-                let sig = self.deeply_normalize(unnormalized_sig, term_location);
+                let sig = match self
+                    .deeply_normalize(ty::Unnormalized::new_wip(unnormalized_sig), term_location)
+                {
+                    Ok(sig) => sig,
+                    Err(guar) => {
+                        let _: ErrorGuaranteed = guar;
+                        return;
+                    }
+                };
                 // HACK(#114936): `WF(sig)` does not imply `WF(normalized(sig))`
                 // with built-in `Fn` implementations, since the impl may not be
                 // well-formed itself.
diff --git a/compiler/rustc_infer/src/infer/mod.rs b/compiler/rustc_infer/src/infer/mod.rs
index aa2fe2ee21dfa..4e04a42cccb72 100644
--- a/compiler/rustc_infer/src/infer/mod.rs
+++ b/compiler/rustc_infer/src/infer/mod.rs
@@ -1394,24 +1394,17 @@ impl<'tcx> InferCtxt<'tcx> {
         }
     }
 
-    // Instantiates the bound variables in a given binder with fresh inference
-    // variables in the current universe.
-    //
-    // Use this method if you'd like to find some generic parameters of the binder's
-    // variables (e.g. during a method call). If there isn't a [`BoundRegionConversionTime`]
-    // that corresponds to your use case, consider whether or not you should
-    // use [`InferCtxt::enter_forall`] instead.
-    pub fn instantiate_binder_with_fresh_vars<T>(
+    pub fn instantiate_binder_with_fresh_vars_unnormalized<T>(
         &self,
         span: Span,
         lbrct: BoundRegionConversionTime,
         value: ty::Binder<'tcx, T>,
-    ) -> T
+    ) -> ty::Unnormalized<'tcx, T>
     where
         T: TypeFoldable<TyCtxt<'tcx>> + Copy,
     {
         if let Some(inner) = value.no_bound_vars() {
-            return inner;
+            return ty::Unnormalized::new(inner);
         }
 
         let bound_vars = value.bound_vars();
@@ -1444,7 +1437,26 @@ impl<'tcx> InferCtxt<'tcx> {
             }
         }
         let delegate = ToFreshVars { args };
-        self.tcx.replace_bound_vars_uncached(value, delegate)
+        ty::Unnormalized::new(self.tcx.replace_bound_vars_uncached(value, delegate))
+    }
+
+    // Instantiates the bound variables in a given binder with fresh inference
+    // variables in the current universe.
+    //
+    // Use this method if you'd like to find some generic parameters of the binder's
+    // variables (e.g. during a method call). If there isn't a [`BoundRegionConversionTime`]
+    // that corresponds to your use case, consider whether or not you should
+    // use [`InferCtxt::enter_forall`] instead.
+    pub fn instantiate_binder_with_fresh_vars<T>(
+        &self,
+        span: Span,
+        lbrct: BoundRegionConversionTime,
+        value: ty::Binder<'tcx, T>,
+    ) -> T
+    where
+        T: TypeFoldable<TyCtxt<'tcx>> + Copy,
+    {
+        self.instantiate_binder_with_fresh_vars_unnormalized(span, lbrct, value).skip_norm_wip()
     }
 
     /// See the [`region_constraints::RegionConstraintCollector::verify_generic_bound`] method.
diff --git a/compiler/rustc_middle/src/queries.rs b/compiler/rustc_middle/src/queries.rs
index 05eb8727bafbd..64adb9b8fc41d 100644
--- a/compiler/rustc_middle/src/queries.rs
+++ b/compiler/rustc_middle/src/queries.rs
@@ -2541,7 +2541,7 @@ rustc_queries! {
         &'tcx Canonical<'tcx, canonical::QueryResponse<'tcx, Ty<'tcx>>>,
         NoSolution,
     > {
-        desc { "normalizing `{}`", goal.canonical.value.value.value }
+        desc { "normalizing `{}`", goal.canonical.value.value.value.skip_normalization() }
     }
 
     /// Do not call this query directly: part of the `Normalize` type-op
@@ -2551,7 +2551,7 @@ rustc_queries! {
         &'tcx Canonical<'tcx, canonical::QueryResponse<'tcx, ty::Clause<'tcx>>>,
         NoSolution,
     > {
-        desc { "normalizing `{:?}`", goal.canonical.value.value.value }
+        desc { "normalizing `{:?}`", goal.canonical.value.value.value.skip_normalization() }
     }
 
     /// Do not call this query directly: part of the `Normalize` type-op
@@ -2561,7 +2561,7 @@ rustc_queries! {
         &'tcx Canonical<'tcx, canonical::QueryResponse<'tcx, ty::PolyFnSig<'tcx>>>,
         NoSolution,
     > {
-        desc { "normalizing `{:?}`", goal.canonical.value.value.value }
+        desc { "normalizing `{:?}`", goal.canonical.value.value.value.skip_normalization() }
     }
 
     /// Do not call this query directly: part of the `Normalize` type-op
@@ -2571,7 +2571,7 @@ rustc_queries! {
         &'tcx Canonical<'tcx, canonical::QueryResponse<'tcx, ty::FnSig<'tcx>>>,
         NoSolution,
     > {
-        desc { "normalizing `{:?}`", goal.canonical.value.value.value }
+        desc { "normalizing `{:?}`", goal.canonical.value.value.value.skip_normalization() }
     }
 
     query instantiate_and_check_impossible_predicates(key: (DefId, GenericArgsRef<'tcx>)) -> bool {
diff --git a/compiler/rustc_middle/src/traits/query.rs b/compiler/rustc_middle/src/traits/query.rs
index d6e6c56c00312..92aa6fe47a7e4 100644
--- a/compiler/rustc_middle/src/traits/query.rs
+++ b/compiler/rustc_middle/src/traits/query.rs
@@ -17,7 +17,7 @@ use crate::ty::{self, GenericArg, Ty, TyCtxt};
 pub mod type_op {
     use rustc_macros::{StableHash, TypeFoldable, TypeVisitable};
 
-    use crate::ty::{Predicate, Ty, UserType};
+    use crate::ty::{Predicate, Ty, Unnormalized, UserType};
 
     #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, StableHash, TypeFoldable, TypeVisitable)]
     pub struct AscribeUserType<'tcx> {
@@ -42,16 +42,10 @@ pub mod type_op {
         pub predicate: Predicate<'tcx>,
     }
 
-    /// Normalizes, but not in the new solver.
+    /// Normalizes a value that may contain unnormalized aliases.
     #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, StableHash, TypeFoldable, TypeVisitable)]
-    pub struct Normalize<T> {
-        pub value: T,
-    }
-
-    /// Normalizes, and deeply normalizes in the new solver.
-    #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, StableHash, TypeFoldable, TypeVisitable)]
-    pub struct DeeplyNormalize<T> {
-        pub value: T,
+    pub struct Normalize<'tcx, T> {
+        pub value: Unnormalized<'tcx, T>,
     }
 
     #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, StableHash, TypeFoldable, TypeVisitable)]
@@ -89,10 +83,7 @@ pub type CanonicalTypeOpProvePredicateGoal<'tcx> =
     CanonicalQueryInput<'tcx, ty::ParamEnvAnd<'tcx, type_op::ProvePredicate<'tcx>>>;
 
 pub type CanonicalTypeOpNormalizeGoal<'tcx, T> =
-    CanonicalQueryInput<'tcx, ty::ParamEnvAnd<'tcx, type_op::Normalize<T>>>;
-
-pub type CanonicalTypeOpDeeplyNormalizeGoal<'tcx, T> =
-    CanonicalQueryInput<'tcx, ty::ParamEnvAnd<'tcx, type_op::DeeplyNormalize<T>>>;
+    CanonicalQueryInput<'tcx, ty::ParamEnvAnd<'tcx, type_op::Normalize<'tcx, T>>>;
 
 pub type CanonicalImpliedOutlivesBoundsGoal<'tcx> =
     CanonicalQueryInput<'tcx, ty::ParamEnvAnd<'tcx, type_op::ImpliedOutlivesBounds<'tcx>>>;
diff --git a/compiler/rustc_middle/src/ty/sty.rs b/compiler/rustc_middle/src/ty/sty.rs
index bb3a4f4fadee0..1c84b269541a4 100644
--- a/compiler/rustc_middle/src/ty/sty.rs
+++ b/compiler/rustc_middle/src/ty/sty.rs
@@ -1631,6 +1631,11 @@ impl<'tcx> Ty<'tcx> {
         self.kind().fn_sig(tcx)
     }
 
+    #[tracing::instrument(level = "trace", skip(tcx))]
+    pub fn unnormalized_fn_sig(self, tcx: TyCtxt<'tcx>) -> ty::Unnormalized<'tcx, PolyFnSig<'tcx>> {
+        self.kind().unnormalized_fn_sig(tcx)
+    }
+
     #[inline]
     pub fn is_fn(self) -> bool {
         matches!(self.kind(), FnDef(..) | FnPtr(..))
diff --git a/compiler/rustc_trait_selection/src/traits/effects.rs b/compiler/rustc_trait_selection/src/traits/effects.rs
index 1d5c657cb6723..567845a34bd8b 100644
--- a/compiler/rustc_trait_selection/src/traits/effects.rs
+++ b/compiler/rustc_trait_selection/src/traits/effects.rs
@@ -104,7 +104,7 @@ fn match_candidate<'tcx>(
             obligation.param_env,
             obligation.cause.clone(),
             obligation.recursion_depth,
-            candidate,
+            Unnormalized::new_wip(candidate),
             &mut nested,
         );
     }
@@ -245,7 +245,7 @@ fn evaluate_host_effect_from_conditionally_const_item_bounds<'tcx>(
                         obligation.param_env,
                         obligation.cause.clone(),
                         obligation.recursion_depth,
-                        trait_ref.skip_norm_wip(),
+                        trait_ref,
                         nested,
                     );
                     (trait_ref, span)
diff --git a/compiler/rustc_trait_selection/src/traits/fulfill.rs b/compiler/rustc_trait_selection/src/traits/fulfill.rs
index aa963b1bca749..95f0c3dc9881d 100644
--- a/compiler/rustc_trait_selection/src/traits/fulfill.rs
+++ b/compiler/rustc_trait_selection/src/traits/fulfill.rs
@@ -406,7 +406,7 @@ impl<'a, 'tcx> ObligationProcessor for FulfillProcessor<'a, 'tcx> {
                 obligation.param_env,
                 obligation.cause.clone(),
                 obligation.recursion_depth + 1,
-                obligation.predicate,
+                ty::Unnormalized::new_wip(obligation.predicate),
                 &mut obligations,
             );
             if predicate != obligation.predicate {
diff --git a/compiler/rustc_trait_selection/src/traits/normalize.rs b/compiler/rustc_trait_selection/src/traits/normalize.rs
index 2a28fab776a12..adb2e7a7060a5 100644
--- a/compiler/rustc_trait_selection/src/traits/normalize.rs
+++ b/compiler/rustc_trait_selection/src/traits/normalize.rs
@@ -35,7 +35,6 @@ impl<'tcx> At<'_, 'tcx> {
             let Normalized { value, obligations } = crate::solve::normalize(*self, value);
             InferOk { value, obligations }
         } else {
-            let value = value.skip_normalization();
             let mut selcx = SelectionContext::new(self.infcx);
             let Normalized { value, obligations } =
                 normalize_with_depth(&mut selcx, self.param_env, self.cause.clone(), 0, value);
@@ -99,7 +98,7 @@ pub(crate) fn normalize_with_depth<'a, 'b, 'tcx, T>(
     param_env: ty::ParamEnv<'tcx>,
     cause: ObligationCause<'tcx>,
     depth: usize,
-    value: T,
+    value: Unnormalized<'tcx, T>,
 ) -> Normalized<'tcx, T>
 where
     T: TypeFoldable<TyCtxt<'tcx>>,
@@ -115,7 +114,7 @@ pub(crate) fn normalize_with_depth_to<'a, 'b, 'tcx, T>(
     param_env: ty::ParamEnv<'tcx>,
     cause: ObligationCause<'tcx>,
     depth: usize,
-    value: T,
+    value: Unnormalized<'tcx, T>,
     obligations: &mut PredicateObligations<'tcx>,
 ) -> T
 where
@@ -123,7 +122,9 @@ where
 {
     debug!(obligations.len = obligations.len());
     let mut normalizer = AssocTypeNormalizer::new(selcx, param_env, cause, depth, obligations);
-    let result = ensure_sufficient_stack(|| AssocTypeNormalizer::fold(&mut normalizer, value));
+    let result = ensure_sufficient_stack(|| {
+        AssocTypeNormalizer::fold(&mut normalizer, value.skip_normalization())
+    });
     debug!(?result, obligations.len = normalizer.obligations.len());
     debug!(?normalizer.obligations,);
     result
diff --git a/compiler/rustc_trait_selection/src/traits/project.rs b/compiler/rustc_trait_selection/src/traits/project.rs
index 6c6ab430126f8..492ead34aee53 100644
--- a/compiler/rustc_trait_selection/src/traits/project.rs
+++ b/compiler/rustc_trait_selection/src/traits/project.rs
@@ -401,7 +401,10 @@ pub(super) fn opt_normalize_projection_term<'a, 'b, 'tcx>(
 
                 Normalized { value: normalized_ty, obligations: projected_obligations }
             } else {
-                Normalized { value: projected_term, obligations: projected_obligations }
+                Normalized {
+                    value: projected_term.skip_normalization(),
+                    obligations: projected_obligations,
+                }
             };
 
             let mut deduped = SsoHashSet::with_capacity(result.obligations.len());
@@ -551,7 +554,7 @@ pub fn normalize_inherent_projection<'a, 'b, 'tcx>(
             param_env,
             cause.clone(),
             depth + 1,
-            predicate.skip_norm_wip(),
+            predicate,
             obligations,
         );
 
@@ -574,10 +577,17 @@ pub fn normalize_inherent_projection<'a, 'b, 'tcx>(
         ));
     }
 
-    let term: Term<'tcx> = if alias_term.kind.is_type() {
-        tcx.type_of(def_id).instantiate(tcx, args).skip_norm_wip().into()
+    let term: ty::Unnormalized<'tcx, Term<'tcx>> = if alias_term.kind.is_type() {
+        tcx.type_of(def_id).instantiate(tcx, args).map(Into::into)
+    } else {
+        tcx.const_of_item(def_id).instantiate(tcx, args).map(Into::into)
+    };
+
+    let term = selcx.infcx.resolve_vars_if_possible(term);
+    let term = if term.has_aliases() {
+        normalize_with_depth_to(selcx, param_env, cause.clone(), depth + 1, term, obligations)
     } else {
-        tcx.const_of_item(def_id).instantiate(tcx, args).skip_norm_wip().into()
+        term.skip_normalization()
     };
 
     push_const_arg_has_type_obligation(
@@ -591,12 +601,6 @@ pub fn normalize_inherent_projection<'a, 'b, 'tcx>(
         args,
     );
 
-    let mut term = selcx.infcx.resolve_vars_if_possible(term);
-    if term.has_aliases() {
-        term =
-            normalize_with_depth_to(selcx, param_env, cause.clone(), depth + 1, term, obligations);
-    }
-
     term
 }
 
@@ -615,31 +619,21 @@ pub fn compute_inherent_assoc_term_args<'a, 'b, 'tcx>(
     let impl_def_id = tcx.parent(alias_def_id);
     let impl_args = selcx.infcx.fresh_args_for_item(cause.span, impl_def_id);
 
-    let mut impl_ty = tcx.type_of(impl_def_id).instantiate(tcx, impl_args).skip_norm_wip();
-    if !selcx.infcx.next_trait_solver() {
-        impl_ty = normalize_with_depth_to(
-            selcx,
-            param_env,
-            cause.clone(),
-            depth + 1,
-            impl_ty,
-            obligations,
-        );
-    }
+    let impl_ty = tcx.type_of(impl_def_id).instantiate(tcx, impl_args);
+    let impl_ty = if !selcx.infcx.next_trait_solver() {
+        normalize_with_depth_to(selcx, param_env, cause.clone(), depth + 1, impl_ty, obligations)
+    } else {
+        impl_ty.skip_normalization()
+    };
 
     // Infer the generic parameters of the impl by unifying the
     // impl type with the self type of the projection.
-    let mut self_ty = alias_term.self_ty();
-    if !selcx.infcx.next_trait_solver() {
-        self_ty = normalize_with_depth_to(
-            selcx,
-            param_env,
-            cause.clone(),
-            depth + 1,
-            self_ty,
-            obligations,
-        );
-    }
+    let self_ty = ty::Unnormalized::dummy(alias_term.self_ty());
+    let self_ty = if !selcx.infcx.next_trait_solver() {
+        normalize_with_depth_to(selcx, param_env, cause.clone(), depth + 1, self_ty, obligations)
+    } else {
+        self_ty.skip_normalization()
+    };
 
     match selcx.infcx.at(&cause, param_env).eq(DefineOpaqueTypes::Yes, impl_ty, self_ty) {
         Ok(mut ok) => obligations.append(&mut ok.obligations),
@@ -660,7 +654,7 @@ enum Projected<'tcx> {
 }
 
 struct Progress<'tcx> {
-    term: ty::Term<'tcx>,
+    term: ty::Unnormalized<'tcx, ty::Term<'tcx>>,
     obligations: PredicateObligations<'tcx>,
 }
 
@@ -675,7 +669,10 @@ impl<'tcx> Progress<'tcx> {
         } else {
             ty::Const::new_error(tcx, guar).into()
         };
-        Progress { term: err_term, obligations: PredicateObligations::new() }
+        Progress {
+            term: ty::Unnormalized::dummy(err_term),
+            obligations: PredicateObligations::new(),
+        }
     }
 
     fn with_addl_obligations(mut self, mut obligations: PredicateObligations<'tcx>) -> Self {
@@ -1103,14 +1100,15 @@ fn assemble_candidates_from_impls<'cx, 'tcx>(
                             |ty| {
                                 // We throw away any obligations we get from this, since we normalize
                                 // and confirm these obligations once again during confirmation
-                                normalize_with_depth(
+                                let mut obligations = PredicateObligations::new();
+                                normalize_with_depth_to(
                                     selcx,
                                     obligation.param_env,
                                     obligation.cause.clone(),
                                     obligation.recursion_depth + 1,
-                                    ty.skip_norm_wip(),
+                                    ty,
+                                    &mut obligations,
                                 )
-                                .value
                             },
                             || {},
                         );
@@ -1346,7 +1344,7 @@ fn confirm_coroutine_candidate<'cx, 'tcx>(
             "expected coroutine self type for built-in coroutine candidate, found {self_ty}"
         )
     };
-    let coroutine_sig = args.as_coroutine().sig();
+    let coroutine_sig = args.as_coroutine().unnormalized_sig();
     let Normalized { value: coroutine_sig, obligations } = normalize_with_depth(
         selcx,
         obligation.param_env,
@@ -1402,7 +1400,7 @@ fn confirm_future_candidate<'cx, 'tcx>(
             "expected coroutine self type for built-in async future candidate, found {self_ty}"
         )
     };
-    let coroutine_sig = args.as_coroutine().sig();
+    let coroutine_sig = args.as_coroutine().unnormalized_sig();
     let Normalized { value: coroutine_sig, obligations } = normalize_with_depth(
         selcx,
         obligation.param_env,
@@ -1447,7 +1445,7 @@ fn confirm_iterator_candidate<'cx, 'tcx>(
     let ty::Coroutine(_, args) = self_ty.kind() else {
         unreachable!("expected coroutine self type for built-in gen candidate, found {self_ty}")
     };
-    let gen_sig = args.as_coroutine().sig();
+    let gen_sig = args.as_coroutine().unnormalized_sig();
     let Normalized { value: gen_sig, obligations } = normalize_with_depth(
         selcx,
         obligation.param_env,
@@ -1492,7 +1490,7 @@ fn confirm_async_iterator_candidate<'cx, 'tcx>(
     else {
         unreachable!()
     };
-    let gen_sig = args.as_coroutine().sig();
+    let gen_sig = args.as_coroutine().unnormalized_sig();
     let Normalized { value: gen_sig, obligations } = normalize_with_depth(
         selcx,
         obligation.param_env,
@@ -1563,7 +1561,7 @@ fn confirm_builtin_candidate<'cx, 'tcx>(
                 obligation.param_env,
                 obligation.cause.clone(),
                 obligation.recursion_depth + 1,
-                ty.skip_norm_wip(),
+                ty,
                 &mut obligations,
             )
         };
@@ -1626,7 +1624,7 @@ fn confirm_fn_pointer_candidate<'cx, 'tcx>(
 ) -> Progress<'tcx> {
     let tcx = selcx.tcx();
     let fn_type = selcx.infcx.shallow_resolve(obligation.predicate.self_ty());
-    let sig = fn_type.fn_sig(tcx);
+    let sig = fn_type.unnormalized_fn_sig(tcx);
     let Normalized { value: sig, obligations } = normalize_with_depth(
         selcx,
         obligation.param_env,
@@ -1648,7 +1646,7 @@ fn confirm_closure_candidate<'cx, 'tcx>(
     let tcx = selcx.tcx();
     let self_ty = selcx.infcx.shallow_resolve(obligation.predicate.self_ty());
     let closure_sig = match *self_ty.kind() {
-        ty::Closure(_, args) => args.as_closure().sig(),
+        ty::Closure(_, args) => args.as_closure().unnormalized_sig(),
 
         // Construct a "normal" `FnOnce` signature for coroutine-closure. This is
         // basically duplicated with the `AsyncFnOnce::CallOnce` confirmation, but
@@ -1656,49 +1654,51 @@ fn confirm_closure_candidate<'cx, 'tcx>(
         ty::CoroutineClosure(def_id, args) => {
             let args = args.as_coroutine_closure();
             let kind_ty = args.kind_ty();
-            args.coroutine_closure_sig().map_bound(|sig| {
-                // If we know the kind and upvars, use that directly.
-                // Otherwise, defer to `AsyncFnKindHelper::Upvars` to delay
-                // the projection, like the `AsyncFn*` traits do.
-                let output_ty = if let Some(_) = kind_ty.to_opt_closure_kind()
+            args.unnormalized_coroutine_closure_sig().map(|sig| {
+                sig.map_bound(|sig| {
+                    // If we know the kind and upvars, use that directly.
+                    // Otherwise, defer to `AsyncFnKindHelper::Upvars` to delay
+                    // the projection, like the `AsyncFn*` traits do.
+                    let output_ty = if let Some(_) = kind_ty.to_opt_closure_kind()
                     // Fall back to projection if upvars aren't constrained
                     && !args.tupled_upvars_ty().is_ty_var()
-                {
-                    sig.to_coroutine_given_kind_and_upvars(
-                        tcx,
-                        args.parent_args(),
-                        tcx.coroutine_for_closure(def_id),
-                        ty::ClosureKind::FnOnce,
-                        tcx.lifetimes.re_static,
-                        args.tupled_upvars_ty(),
-                        args.coroutine_captures_by_ref_ty(),
-                    )
-                } else {
-                    let upvars_projection_def_id =
-                        tcx.require_lang_item(LangItem::AsyncFnKindUpvars, obligation.cause.span);
-                    let tupled_upvars_ty = Ty::new_projection(
-                        tcx,
-                        ty::IsRigid::No,
-                        upvars_projection_def_id,
-                        [
-                            ty::GenericArg::from(kind_ty),
-                            Ty::from_closure_kind(tcx, ty::ClosureKind::FnOnce).into(),
-                            tcx.lifetimes.re_static.into(),
-                            sig.tupled_inputs_ty.into(),
-                            args.tupled_upvars_ty().into(),
-                            args.coroutine_captures_by_ref_ty().into(),
-                        ],
-                    );
-                    sig.to_coroutine(
-                        tcx,
-                        args.parent_args(),
-                        Ty::from_closure_kind(tcx, ty::ClosureKind::FnOnce),
-                        tcx.coroutine_for_closure(def_id),
-                        tupled_upvars_ty,
-                    )
-                };
-
-                tcx.mk_fn_sig([sig.tupled_inputs_ty], output_ty, sig.fn_sig_kind)
+                    {
+                        sig.to_coroutine_given_kind_and_upvars(
+                            tcx,
+                            args.parent_args(),
+                            tcx.coroutine_for_closure(def_id),
+                            ty::ClosureKind::FnOnce,
+                            tcx.lifetimes.re_static,
+                            args.tupled_upvars_ty(),
+                            args.coroutine_captures_by_ref_ty(),
+                        )
+                    } else {
+                        let upvars_projection_def_id = tcx
+                            .require_lang_item(LangItem::AsyncFnKindUpvars, obligation.cause.span);
+                        let tupled_upvars_ty = Ty::new_projection(
+                            tcx,
+                            ty::IsRigid::No,
+                            upvars_projection_def_id,
+                            [
+                                ty::GenericArg::from(kind_ty),
+                                Ty::from_closure_kind(tcx, ty::ClosureKind::FnOnce).into(),
+                                tcx.lifetimes.re_static.into(),
+                                sig.tupled_inputs_ty.into(),
+                                args.tupled_upvars_ty().into(),
+                                args.coroutine_captures_by_ref_ty().into(),
+                            ],
+                        );
+                        sig.to_coroutine(
+                            tcx,
+                            args.parent_args(),
+                            Ty::from_closure_kind(tcx, ty::ClosureKind::FnOnce),
+                            tcx.coroutine_for_closure(def_id),
+                            tupled_upvars_ty,
+                        )
+                    };
+
+                    tcx.mk_fn_sig([sig.tupled_inputs_ty], output_ty, sig.fn_sig_kind)
+                })
             })
         }
 
@@ -1962,13 +1962,13 @@ fn confirm_param_env_candidate<'cx, 'tcx>(
     let cause = &obligation.cause;
     let param_env = obligation.param_env;
 
-    let cache_entry = infcx.instantiate_binder_with_fresh_vars(
+    let cache_entry = infcx.instantiate_binder_with_fresh_vars_unnormalized(
         cause.span,
         BoundRegionConversionTime::HigherRankedType,
         poly_cache_entry,
     );
 
-    let cache_projection = cache_entry.projection_term;
+    let cache_projection = cache_entry.map(|entry| entry.projection_term);
     let mut nested_obligations = PredicateObligations::new();
     let obligation_projection = obligation.predicate;
     let obligation_projection = ensure_sufficient_stack(|| {
@@ -1977,7 +1977,7 @@ fn confirm_param_env_candidate<'cx, 'tcx>(
             obligation.param_env,
             obligation.cause.clone(),
             obligation.recursion_depth + 1,
-            obligation_projection,
+            ty::Unnormalized::new_wip(obligation_projection),
             &mut nested_obligations,
         )
     });
@@ -1993,7 +1993,7 @@ fn confirm_param_env_candidate<'cx, 'tcx>(
             )
         })
     } else {
-        cache_projection
+        cache_projection.skip_normalization()
     };
 
     debug!(?cache_projection, ?obligation_projection);
@@ -2006,7 +2006,7 @@ fn confirm_param_env_candidate<'cx, 'tcx>(
         Ok(InferOk { value: _, obligations }) => {
             nested_obligations.extend(obligations);
             assoc_term_own_obligations(selcx, obligation, &mut nested_obligations);
-            Progress { term: cache_entry.term, obligations: nested_obligations }
+            Progress { term: cache_entry.map(|entry| entry.term), obligations: nested_obligations }
         }
         Err(e) => {
             let msg = format!(
@@ -2014,7 +2014,10 @@ fn confirm_param_env_candidate<'cx, 'tcx>(
             );
             debug!("confirm_param_env_candidate: {}", msg);
             let err = Ty::new_error_with_message(infcx.tcx, obligation.cause.span, msg);
-            Progress { term: err.into(), obligations: PredicateObligations::new() }
+            Progress {
+                term: ty::Unnormalized::dummy(err.into()),
+                obligations: PredicateObligations::new(),
+            }
         }
     }
 }
@@ -2063,11 +2066,11 @@ fn confirm_impl_candidate<'cx, 'tcx>(
             return Ok(Projected::NoProgress(obligation.predicate.to_term(tcx, ty::IsRigid::No)));
         } else {
             return Ok(Projected::Progress(Progress {
-                term: if obligation.predicate.kind.is_type() {
+                term: ty::Unnormalized::dummy(if obligation.predicate.kind.is_type() {
                     Ty::new_misc_error(tcx).into()
                 } else {
                     ty::Const::new_misc_error(tcx).into()
-                },
+                }),
                 obligations: nested,
             }));
         }
@@ -2096,17 +2099,18 @@ fn confirm_impl_candidate<'cx, 'tcx>(
         } else {
             ty::Const::new_error_with_message(tcx, span, msg).into()
         };
-        Progress { term: err, obligations: nested }
+        Progress { term: ty::Unnormalized::dummy(err), obligations: nested }
     } else {
         assoc_term_own_obligations(selcx, obligation, &mut nested);
-        let instantiated_term: Term<'tcx> = term.instantiate(tcx, args).skip_norm_wip();
+        let instantiated_term = term.instantiate(tcx, args);
+        let term_for_obligation = instantiated_term.skip_normalization();
         push_const_arg_has_type_obligation(
             tcx,
             &mut nested,
             &obligation.cause,
             obligation.recursion_depth + 1,
             obligation.param_env,
-            instantiated_term,
+            term_for_obligation,
             assoc_term.item.def_id,
             args,
         );
@@ -2135,7 +2139,7 @@ fn assoc_term_own_obligations<'cx, 'tcx>(
             obligation.param_env,
             obligation.cause.clone(),
             obligation.recursion_depth + 1,
-            predicate.skip_norm_wip(),
+            predicate,
             nested,
         );
 
diff --git a/compiler/rustc_trait_selection/src/traits/query/type_op/normalize.rs b/compiler/rustc_trait_selection/src/traits/query/type_op/normalize.rs
index 577a5564c6312..1eef77b436ac6 100644
--- a/compiler/rustc_trait_selection/src/traits/query/type_op/normalize.rs
+++ b/compiler/rustc_trait_selection/src/traits/query/type_op/normalize.rs
@@ -2,23 +2,25 @@ use std::fmt;
 
 use rustc_middle::traits::ObligationCause;
 use rustc_middle::traits::query::NoSolution;
-pub use rustc_middle::traits::query::type_op::{DeeplyNormalize, Normalize};
-use rustc_middle::ty::{
-    self, Lift, ParamEnvAnd, Ty, TyCtxt, TypeFoldable, TypeVisitableExt, Unnormalized,
-};
+pub use rustc_middle::traits::query::type_op::Normalize;
+use rustc_middle::ty::{self, Lift, ParamEnvAnd, Ty, TyCtxt, TypeFoldable, TypeVisitableExt};
 use rustc_span::Span;
 
 use crate::infer::canonical::{CanonicalQueryInput, CanonicalQueryResponse};
 use crate::traits::ObligationCtxt;
 
-impl<'tcx, T> super::QueryTypeOp<'tcx> for Normalize<T>
+impl<'tcx, T> super::QueryTypeOp<'tcx> for Normalize<'tcx, T>
 where
     T: Normalizable<'tcx> + 'tcx,
 {
     type QueryResponse = T;
 
     fn try_fast_path(_tcx: TyCtxt<'tcx>, key: &ParamEnvAnd<'tcx, Self>) -> Option<T> {
-        if !key.value.value.has_aliases() { Some(key.value.value) } else { None }
+        if !key.value.value.skip_normalization().has_aliases() {
+            Some(key.value.value.skip_normalization())
+        } else {
+            None
+        }
     }
 
     fn perform_query(
@@ -36,49 +38,7 @@ where
         ocx.deeply_normalize(
             &ObligationCause::dummy_with_span(span),
             key.param_env,
-            Unnormalized::new_wip(key.value.value),
-        )
-        .map_err(|_| NoSolution)
-    }
-}
-
-impl<'tcx, T> super::QueryTypeOp<'tcx> for DeeplyNormalize<T>
-where
-    T: Normalizable<'tcx> + 'tcx,
-{
-    type QueryResponse = T;
-
-    fn try_fast_path(_tcx: TyCtxt<'tcx>, key: &ParamEnvAnd<'tcx, Self>) -> Option<T> {
-        if !key.value.value.has_aliases() { Some(key.value.value) } else { None }
-    }
-
-    fn perform_query(
-        tcx: TyCtxt<'tcx>,
-        canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Self>>,
-    ) -> Result<CanonicalQueryResponse<'tcx, Self::QueryResponse>, NoSolution> {
-        T::type_op_method(
-            tcx,
-            CanonicalQueryInput {
-                typing_mode: canonicalized.typing_mode,
-                canonical: canonicalized.canonical.unchecked_map(
-                    |ty::ParamEnvAnd { param_env, value }| ty::ParamEnvAnd {
-                        param_env,
-                        value: Normalize { value: value.value },
-                    },
-                ),
-            },
-        )
-    }
-
-    fn perform_locally_with_next_solver(
-        ocx: &ObligationCtxt<'_, 'tcx>,
-        key: ParamEnvAnd<'tcx, Self>,
-        span: Span,
-    ) -> Result<Self::QueryResponse, NoSolution> {
-        ocx.deeply_normalize(
-            &ObligationCause::dummy_with_span(span),
-            key.param_env,
-            Unnormalized::new_wip(key.value.value),
+            key.value.value,
         )
         .map_err(|_| NoSolution)
     }
@@ -89,14 +49,14 @@ pub trait Normalizable<'tcx>:
 {
     fn type_op_method(
         tcx: TyCtxt<'tcx>,
-        canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<Self>>>,
+        canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<'tcx, Self>>>,
     ) -> Result<CanonicalQueryResponse<'tcx, Self>, NoSolution>;
 }
 
 impl<'tcx> Normalizable<'tcx> for Ty<'tcx> {
     fn type_op_method(
         tcx: TyCtxt<'tcx>,
-        canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<Self>>>,
+        canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<'tcx, Self>>>,
     ) -> Result<CanonicalQueryResponse<'tcx, Self>, NoSolution> {
         tcx.type_op_normalize_ty(canonicalized)
     }
@@ -105,7 +65,7 @@ impl<'tcx> Normalizable<'tcx> for Ty<'tcx> {
 impl<'tcx> Normalizable<'tcx> for ty::Clause<'tcx> {
     fn type_op_method(
         tcx: TyCtxt<'tcx>,
-        canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<Self>>>,
+        canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<'tcx, Self>>>,
     ) -> Result<CanonicalQueryResponse<'tcx, Self>, NoSolution> {
         tcx.type_op_normalize_clause(canonicalized)
     }
@@ -114,7 +74,7 @@ impl<'tcx> Normalizable<'tcx> for ty::Clause<'tcx> {
 impl<'tcx> Normalizable<'tcx> for ty::PolyFnSig<'tcx> {
     fn type_op_method(
         tcx: TyCtxt<'tcx>,
-        canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<Self>>>,
+        canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<'tcx, Self>>>,
     ) -> Result<CanonicalQueryResponse<'tcx, Self>, NoSolution> {
         tcx.type_op_normalize_poly_fn_sig(canonicalized)
     }
@@ -123,7 +83,7 @@ impl<'tcx> Normalizable<'tcx> for ty::PolyFnSig<'tcx> {
 impl<'tcx> Normalizable<'tcx> for ty::FnSig<'tcx> {
     fn type_op_method(
         tcx: TyCtxt<'tcx>,
-        canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<Self>>>,
+        canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<'tcx, Self>>>,
     ) -> Result<CanonicalQueryResponse<'tcx, Self>, NoSolution> {
         tcx.type_op_normalize_fn_sig(canonicalized)
     }
@@ -134,7 +94,7 @@ impl<'tcx> Normalizable<'tcx> for ty::FnSig<'tcx> {
 impl<'tcx> Normalizable<'tcx> for ty::PolyTypeOutlivesPredicate<'tcx> {
     fn type_op_method(
         _tcx: TyCtxt<'tcx>,
-        _canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<Self>>>,
+        _canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<'tcx, Self>>>,
     ) -> Result<CanonicalQueryResponse<'tcx, Self>, NoSolution> {
         unreachable!("we never normalize PolyTypeOutlivesPredicate")
     }
diff --git a/compiler/rustc_trait_selection/src/traits/select/candidate_assembly.rs b/compiler/rustc_trait_selection/src/traits/select/candidate_assembly.rs
index 6c7a0f3550b99..a4f751e23d799 100644
--- a/compiler/rustc_trait_selection/src/traits/select/candidate_assembly.rs
+++ b/compiler/rustc_trait_selection/src/traits/select/candidate_assembly.rs
@@ -1059,7 +1059,7 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
                             if upcast_trait_ref.def_id() == target_trait_did
                                 && let Ok(nested) = self.match_upcast_principal(
                                     obligation,
-                                    upcast_trait_ref,
+                                    ty::Unnormalized::new_wip(upcast_trait_ref),
                                     a_data,
                                     b_data,
                                     a_region,
diff --git a/compiler/rustc_trait_selection/src/traits/select/confirmation.rs b/compiler/rustc_trait_selection/src/traits/select/confirmation.rs
index 11ce6235eb7ff..1fad988a7f85c 100644
--- a/compiler/rustc_trait_selection/src/traits/select/confirmation.rs
+++ b/compiler/rustc_trait_selection/src/traits/select/confirmation.rs
@@ -171,7 +171,7 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
 
         let candidate = candidate_predicate.map_bound(|t| t.trait_ref);
 
-        let candidate = self.infcx.instantiate_binder_with_fresh_vars(
+        let candidate = self.infcx.instantiate_binder_with_fresh_vars_unnormalized(
             obligation.cause.span,
             BoundRegionConversionTime::HigherRankedType,
             candidate,
@@ -416,7 +416,7 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
                         obligation.param_env,
                         cause.clone(),
                         obligation.recursion_depth + 1,
-                        assumption,
+                        Unnormalized::new_wip(assumption),
                         &mut obligations,
                     );
                     self.infcx.register_region_assumption(assumption);
@@ -512,7 +512,7 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
         let unnormalized_upcast_trait_ref =
             supertraits.nth(index).expect("supertraits iterator no longer has as many elements");
 
-        let upcast_trait_ref = self.infcx.instantiate_binder_with_fresh_vars(
+        let upcast_trait_ref = self.infcx.instantiate_binder_with_fresh_vars_unnormalized(
             obligation.cause.span,
             BoundRegionConversionTime::HigherRankedType,
             unnormalized_upcast_trait_ref,
@@ -536,10 +536,10 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
 
         // Check supertraits hold. This is so that their associated type bounds
         // will be checked in the code below.
-        for (supertrait, _) in tcx
+        for supertrait in tcx
             .explicit_super_predicates_of(trait_predicate.def_id())
             .iter_instantiated_copied(tcx, trait_predicate.trait_ref.args)
-            .map(Unnormalized::skip_norm_wip)
+            .map(|pred| pred.unzip().0)
         {
             let normalized_supertrait = normalize_with_depth_to(
                 self,
@@ -581,7 +581,7 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
                     obligation.param_env,
                     obligation.cause.clone(),
                     obligation.recursion_depth + 1,
-                    bound.instantiate(tcx, trait_predicate.trait_ref.args).skip_norm_wip(),
+                    bound.instantiate(tcx, trait_predicate.trait_ref.args),
                     &mut nested,
                 );
                 nested.push(obligation.with(tcx, normalized_bound));
@@ -602,7 +602,9 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
         let self_ty = self.infcx.shallow_resolve(placeholder_predicate.self_ty());
 
         let tcx = self.tcx();
-        let sig = self_ty.fn_sig(tcx);
+        let sig = self_ty.unnormalized_fn_sig(tcx);
+        let output_ty = sig.map(|sig| self.infcx.enter_forall_and_leak_universe(sig.output()));
+        let sig = sig.skip_norm_wip();
         let trait_ref = closure_trait_ref_and_return_type(
             tcx,
             obligation.predicate.def_id(),
@@ -617,7 +619,6 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
         let cause = obligation.derived_cause(ObligationCauseCode::BuiltinDerived);
 
         // Confirm the `type Output: Sized;` bound that is present on `FnOnce`
-        let output_ty = self.infcx.enter_forall_and_leak_universe(sig.output());
         let output_ty = normalize_with_depth_to(
             self,
             obligation.param_env,
@@ -975,7 +976,7 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
                     obligation.param_env,
                     obligation.cause.clone(),
                     obligation.recursion_depth + 1,
-                    (obligation.predicate.trait_ref, found_trait_ref),
+                    Unnormalized::new_wip((obligation.predicate.trait_ref, found_trait_ref)),
                 )
             });
 
@@ -1018,7 +1019,7 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
 
         let source_principal = a_data.principal().unwrap().with_self_ty(tcx, a_ty);
         let unnormalized_upcast_principal =
-            util::supertraits(tcx, source_principal).nth(idx).unwrap();
+            ty::Unnormalized::new_wip(util::supertraits(tcx, source_principal).nth(idx).unwrap());
 
         let nested = self
             .match_upcast_principal(
@@ -1182,7 +1183,7 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
                     obligation.param_env,
                     obligation.cause.clone(),
                     obligation.recursion_depth + 1,
-                    tail_field_ty.instantiate(tcx, args_a).skip_norm_wip(),
+                    tail_field_ty.instantiate(tcx, args_a),
                     &mut nested,
                 );
                 let target_tail = normalize_with_depth_to(
@@ -1190,7 +1191,7 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
                     obligation.param_env,
                     obligation.cause.clone(),
                     obligation.recursion_depth + 1,
-                    tail_field_ty.instantiate(tcx, args_b).skip_norm_wip(),
+                    tail_field_ty.instantiate(tcx, args_b),
                     &mut nested,
                 );
 
diff --git a/compiler/rustc_trait_selection/src/traits/select/mod.rs b/compiler/rustc_trait_selection/src/traits/select/mod.rs
index 440ed2b46320b..0ee701e0a45be 100644
--- a/compiler/rustc_trait_selection/src/traits/select/mod.rs
+++ b/compiler/rustc_trait_selection/src/traits/select/mod.rs
@@ -1083,7 +1083,7 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
                     param_env,
                     obligation.cause.clone(),
                     obligation.recursion_depth + 1,
-                    obligation.predicate,
+                    Unnormalized::new_wip(obligation.predicate),
                     &mut nested_obligations,
                 );
                 if predicate != obligation.predicate {
@@ -1719,7 +1719,7 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
             return Err(());
         }
 
-        let trait_bound = self.infcx.instantiate_binder_with_fresh_vars(
+        let trait_bound = self.infcx.instantiate_binder_with_fresh_vars_unnormalized(
             obligation.cause.span,
             HigherRankedType,
             trait_bound,
@@ -1777,11 +1777,12 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
         debug_assert_eq!(def_id, env_predicate.item_def_id());
 
         let mut nested_obligations = PredicateObligations::new();
-        let infer_predicate = self.infcx.instantiate_binder_with_fresh_vars(
+        let infer_predicate = self.infcx.instantiate_binder_with_fresh_vars_unnormalized(
             obligation.cause.span,
             BoundRegionConversionTime::HigherRankedType,
             env_predicate,
         );
+        let infer_projection = infer_predicate.map(|predicate| predicate.projection_term);
         let infer_projection = if potentially_unnormalized_candidates {
             ensure_sufficient_stack(|| {
                 normalize_with_depth_to(
@@ -1789,12 +1790,12 @@ impl<'cx, 'tcx> SelectionContext<'cx, 'tcx> {
                     obligation.param_env,
                     obligation.cause.clone(),
                     obligation.recursion_depth + 1,
-                    infer_predicate.projection_term,
+                    infer_projection,
                     &mut nested_obligations,
                 )
             })
         } else {
-            infer_predicate.projection_term
+            infer_projection.skip_normalization()
         };
 
         let is_match = self
@@ -2475,7 +2476,7 @@ impl<'tcx> SelectionContext<'_, 'tcx> {
                             param_env,
                             cause.clone(),
                             recursion_depth,
-                            placeholder_ty,
+                            Unnormalized::new_wip(placeholder_ty),
                         )
                     });
 
@@ -2538,20 +2539,20 @@ impl<'tcx> SelectionContext<'_, 'tcx> {
 
         let impl_args = self.infcx.fresh_args_for_item(obligation.cause.span, impl_def_id);
 
-        let trait_ref =
-            impl_trait_header.trait_ref.instantiate(self.tcx(), impl_args).skip_norm_wip();
+        let trait_ref = impl_trait_header.trait_ref.instantiate(self.tcx(), impl_args);
         debug!(?impl_trait_header);
 
-        let Normalized { value: impl_trait_ref, obligations: mut nested_obligations } =
-            ensure_sufficient_stack(|| {
-                normalize_with_depth(
-                    self,
-                    obligation.param_env,
-                    obligation.cause.clone(),
-                    obligation.recursion_depth + 1,
-                    trait_ref,
-                )
-            });
+        let mut nested_obligations = PredicateObligations::new();
+        let impl_trait_ref = ensure_sufficient_stack(|| {
+            normalize_with_depth_to(
+                self,
+                obligation.param_env,
+                obligation.cause.clone(),
+                obligation.recursion_depth + 1,
+                trait_ref,
+                &mut nested_obligations,
+            )
+        });
 
         debug!(?impl_trait_ref, ?placeholder_obligation_trait_ref);
 
@@ -2583,7 +2584,7 @@ impl<'tcx> SelectionContext<'_, 'tcx> {
     fn match_upcast_principal(
         &mut self,
         obligation: &PolyTraitObligation<'tcx>,
-        unnormalized_upcast_principal: ty::PolyTraitRef<'tcx>,
+        unnormalized_upcast_principal: ty::Unnormalized<'tcx, ty::PolyTraitRef<'tcx>>,
         a_data: &'tcx ty::List<ty::PolyExistentialPredicate<'tcx>>,
         b_data: &'tcx ty::List<ty::PolyExistentialPredicate<'tcx>>,
         a_region: ty::Region<'tcx>,
@@ -2876,7 +2877,7 @@ impl<'tcx> SelectionContext<'_, 'tcx> {
                 param_env,
                 cause.clone(),
                 recursion_depth,
-                predicate.skip_norm_wip(),
+                predicate,
                 &mut obligations,
             );
             obligations.push(Obligation {
@@ -2889,11 +2890,7 @@ impl<'tcx> SelectionContext<'_, 'tcx> {
 
         // Register any outlives obligations from the trait here, cc #124336.
         if tcx.def_kind(def_id) == (DefKind::Impl { of_trait: true }) {
-            for clause in tcx
-                .impl_super_outlives(def_id)
-                .iter_instantiated(tcx, args)
-                .map(Unnormalized::skip_norm_wip)
-            {
+            for clause in tcx.impl_super_outlives(def_id).iter_instantiated(tcx, args) {
                 let clause = normalize_with_depth_to(
                     self,
                     param_env,
diff --git a/compiler/rustc_trait_selection/src/traits/wf.rs b/compiler/rustc_trait_selection/src/traits/wf.rs
index aaed02a8f2e96..95043385cf17a 100644
--- a/compiler/rustc_trait_selection/src/traits/wf.rs
+++ b/compiler/rustc_trait_selection/src/traits/wf.rs
@@ -362,7 +362,7 @@ impl<'a, 'tcx> WfPredicates<'a, 'tcx> {
                 param_env,
                 cause.clone(),
                 self.recursion_depth,
-                obligation.predicate,
+                ty::Unnormalized::new_wip(obligation.predicate),
                 &mut obligations,
             );
             obligation.predicate = normalized_predicate;
diff --git a/compiler/rustc_traits/src/type_op.rs b/compiler/rustc_traits/src/type_op.rs
index f77e1994cf472..510226cf141b0 100644
--- a/compiler/rustc_traits/src/type_op.rs
+++ b/compiler/rustc_traits/src/type_op.rs
@@ -38,42 +38,44 @@ fn type_op_ascribe_user_type<'tcx>(
 
 fn type_op_normalize<'tcx, T>(
     ocx: &ObligationCtxt<'_, 'tcx>,
-    key: ParamEnvAnd<'tcx, Normalize<T>>,
+    key: ParamEnvAnd<'tcx, Normalize<'tcx, T>>,
 ) -> Result<T, NoSolution>
 where
     T: fmt::Debug + TypeFoldable<TyCtxt<'tcx>>,
 {
     let ParamEnvAnd { param_env, value: Normalize { value } } = key;
-    let Normalized { value, obligations } =
-        ocx.infcx.at(&ObligationCause::dummy(), param_env).query_normalize(value)?;
+    let Normalized { value, obligations } = ocx
+        .infcx
+        .at(&ObligationCause::dummy(), param_env)
+        .query_normalize(value.skip_normalization())?;
     ocx.register_obligations(obligations);
     Ok(value)
 }
 
 fn type_op_normalize_ty<'tcx>(
     tcx: TyCtxt<'tcx>,
-    canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<Ty<'tcx>>>>,
+    canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<'tcx, Ty<'tcx>>>>,
 ) -> Result<&'tcx Canonical<'tcx, QueryResponse<'tcx, Ty<'tcx>>>, NoSolution> {
     tcx.infer_ctxt().enter_canonical_trait_query(&canonicalized, type_op_normalize)
 }
 
 fn type_op_normalize_clause<'tcx>(
     tcx: TyCtxt<'tcx>,
-    canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<Clause<'tcx>>>>,
+    canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<'tcx, Clause<'tcx>>>>,
 ) -> Result<&'tcx Canonical<'tcx, QueryResponse<'tcx, Clause<'tcx>>>, NoSolution> {
     tcx.infer_ctxt().enter_canonical_trait_query(&canonicalized, type_op_normalize)
 }
 
 fn type_op_normalize_fn_sig<'tcx>(
     tcx: TyCtxt<'tcx>,
-    canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<FnSig<'tcx>>>>,
+    canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<'tcx, FnSig<'tcx>>>>,
 ) -> Result<&'tcx Canonical<'tcx, QueryResponse<'tcx, FnSig<'tcx>>>, NoSolution> {
     tcx.infer_ctxt().enter_canonical_trait_query(&canonicalized, type_op_normalize)
 }
 
 fn type_op_normalize_poly_fn_sig<'tcx>(
     tcx: TyCtxt<'tcx>,
-    canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<PolyFnSig<'tcx>>>>,
+    canonicalized: CanonicalQueryInput<'tcx, ParamEnvAnd<'tcx, Normalize<'tcx, PolyFnSig<'tcx>>>>,
 ) -> Result<&'tcx Canonical<'tcx, QueryResponse<'tcx, PolyFnSig<'tcx>>>, NoSolution> {
     tcx.infer_ctxt().enter_canonical_trait_query(&canonicalized, type_op_normalize)
 }
diff --git a/compiler/rustc_type_ir/src/ty_kind.rs b/compiler/rustc_type_ir/src/ty_kind.rs
index 68eebb1122928..475828d134bb8 100644
--- a/compiler/rustc_type_ir/src/ty_kind.rs
+++ b/compiler/rustc_type_ir/src/ty_kind.rs
@@ -22,7 +22,7 @@ use crate::ty::AliasTy;
 use crate::visit::TypeVisitable;
 use crate::{
     self as ty, BoundVarIndexKind, FloatTy, FreeAliasTy, InherentAliasTy, IntTy, Interner,
-    OpaqueAliasTy, ProjectionAliasTy, UintTy,
+    OpaqueAliasTy, ProjectionAliasTy, UintTy, Unnormalized,
 };
 
 mod closure;
@@ -352,15 +352,18 @@ impl<I: Interner> Eq for TyKind<I> {}
 
 impl<I: Interner> TyKind<I> {
     pub fn fn_sig(self, interner: I) -> ty::Binder<I, ty::FnSig<I>> {
+        self.unnormalized_fn_sig(interner).skip_normalization()
+    }
+
+    pub fn unnormalized_fn_sig(self, interner: I) -> Unnormalized<I, ty::Binder<I, ty::FnSig<I>>> {
         match self {
-            ty::FnPtr(sig_tys, hdr) => sig_tys.with(hdr),
-            ty::FnDef(def_id, args) => interner
-                .fn_sig(def_id)
-                .instantiate(interner, args.no_bound_vars().unwrap())
-                .skip_norm_wip(),
+            ty::FnPtr(sig_tys, hdr) => Unnormalized::new(sig_tys.with(hdr)),
+            ty::FnDef(def_id, args) => {
+                interner.fn_sig(def_id).instantiate(interner, args.no_bound_vars().unwrap())
+            }
             ty::Error(_) => {
                 // ignore errors (#54954)
-                ty::Binder::dummy(ty::FnSig::dummy())
+                Unnormalized::dummy(ty::Binder::dummy(ty::FnSig::dummy()))
             }
             ty::Closure(..) => panic!(
                 "to get the signature of a closure, use `args.as_closure().sig()` not `fn_sig()`",
diff --git a/compiler/rustc_type_ir/src/ty_kind/closure.rs b/compiler/rustc_type_ir/src/ty_kind/closure.rs
index 9453d30ce91c9..9da8fc7d1ee8f 100644
--- a/compiler/rustc_type_ir/src/ty_kind/closure.rs
+++ b/compiler/rustc_type_ir/src/ty_kind/closure.rs
@@ -9,7 +9,7 @@ use crate::data_structures::DelayedMap;
 use crate::fold::{TypeFoldable, TypeFolder, TypeSuperFoldable, shift_region};
 use crate::inherent::*;
 use crate::visit::{TypeSuperVisitable, TypeVisitable, TypeVisitableExt, TypeVisitor};
-use crate::{self as ty, FnSigKind, Interner};
+use crate::{self as ty, FnSigKind, Interner, Unnormalized};
 
 /// A closure can be modeled as a struct that looks like:
 /// ```ignore (illustrative)
@@ -200,8 +200,13 @@ impl<I: Interner> ClosureArgs<I> {
 
     /// Extracts the signature from the closure.
     pub fn sig(self) -> ty::Binder<I, ty::FnSig<I>> {
+        self.unnormalized_sig().skip_norm_wip()
+    }
+
+    /// Extracts the signature from the closure without assuming it has been normalized.
+    pub fn unnormalized_sig(self) -> Unnormalized<I, ty::Binder<I, ty::FnSig<I>>> {
         match self.sig_as_fn_ptr_ty().kind() {
-            ty::FnPtr(sig_tys, hdr) => sig_tys.with(hdr),
+            ty::FnPtr(sig_tys, hdr) => Unnormalized::new(sig_tys.with(hdr)),
             ty => panic!("closure_sig_as_fn_ptr_ty is not a fn-ptr: {ty:?}"),
         }
     }
@@ -294,8 +299,14 @@ impl<I: Interner> CoroutineClosureArgs<I> {
     }
 
     pub fn coroutine_closure_sig(self) -> ty::Binder<I, CoroutineClosureSignature<I>> {
+        self.unnormalized_coroutine_closure_sig().skip_norm_wip()
+    }
+
+    pub fn unnormalized_coroutine_closure_sig(
+        self,
+    ) -> Unnormalized<I, ty::Binder<I, CoroutineClosureSignature<I>>> {
         let ty::FnPtr(sig_tys, hdr) = self.signature_parts_ty().kind() else { panic!() };
-        sig_tys.map_bound(|sig_tys| {
+        Unnormalized::new(sig_tys.map_bound(|sig_tys| {
             let [resume_ty, tupled_inputs_ty] = *sig_tys.inputs().as_slice() else {
                 panic!();
             };
@@ -309,7 +320,7 @@ impl<I: Interner> CoroutineClosureArgs<I> {
                 return_ty,
                 fn_sig_kind: hdr.fn_sig_kind,
             }
-        })
+        }))
     }
 
     pub fn coroutine_captures_by_ref_ty(self) -> I::Ty {
@@ -661,7 +672,16 @@ impl<I: Interner> CoroutineArgs<I> {
     /// Returns the "coroutine signature", which consists of its resume, yield
     /// and return types.
     pub fn sig(self) -> GenSig<I> {
+        self.unnormalized_sig().skip_norm_wip()
+    }
+
+    /// Returns the "coroutine signature" without assuming it has been normalized.
+    pub fn unnormalized_sig(self) -> Unnormalized<I, GenSig<I>> {
         let parts = self.split();
-        GenSig { resume_ty: parts.resume_ty, yield_ty: parts.yield_ty, return_ty: parts.return_ty }
+        Unnormalized::new(GenSig {
+            resume_ty: parts.resume_ty,
+            yield_ty: parts.yield_ty,
+            return_ty: parts.return_ty,
+        })
     }
 }
diff --git a/compiler/rustc_type_ir/src/unnormalized.rs b/compiler/rustc_type_ir/src/unnormalized.rs
index 0981a8a7e0e18..a685f32bced2c 100644
--- a/compiler/rustc_type_ir/src/unnormalized.rs
+++ b/compiler/rustc_type_ir/src/unnormalized.rs
@@ -1,8 +1,11 @@
 use std::marker::PhantomData;
 
 use derive_where::derive_where;
+#[cfg(feature = "nightly")]
+use rustc_macros::StableHash_NoContext;
 use rustc_type_ir_macros::TypeVisitable_Generic;
 
+use crate::fold::{FallibleTypeFolder, TypeFoldable, TypeFolder};
 use crate::inherent::*;
 use crate::upcast::Upcast;
 use crate::{
@@ -25,7 +28,8 @@ use crate::{
 /// e.g., `Unnormalized<I, I::Clause>`.
 ///
 /// [here]: https://rust-lang.zulipchat.com/#narrow/channel/364551-t-types.2Ftrait-system-refactor/topic/Eager.20normalization.2C.20ahoy.21/with/582996293
-#[derive_where(Clone, Copy, PartialOrd, PartialEq, Debug; T)]
+#[derive_where(Clone, Copy, PartialOrd, PartialEq, Eq, Hash, Debug; T)]
+#[cfg_attr(feature = "nightly", derive(StableHash_NoContext))]
 #[derive(TypeVisitable_Generic)]
 pub struct Unnormalized<I: Interner, T> {
     value: T,
@@ -87,6 +91,16 @@ impl<I: Interner, T> Unnormalized<I, T> {
     }
 }
 
+impl<I: Interner, T: TypeFoldable<I>> TypeFoldable<I> for Unnormalized<I, T> {
+    fn try_fold_with<F: FallibleTypeFolder<I>>(self, folder: &mut F) -> Result<Self, F::Error> {
+        Ok(Unnormalized::new(self.value.try_fold_with(folder)?))
+    }
+
+    fn fold_with<F: TypeFolder<I>>(self, folder: &mut F) -> Self {
+        Unnormalized::new(self.value.fold_with(folder))
+    }
+}
+
 impl<I: Interner, T, U> Unnormalized<I, (T, U)> {
     pub fn unzip(self) -> (Unnormalized<I, T>, Unnormalized<I, U>) {
         (Unnormalized::new(self.value.0), Unnormalized::new(self.value.1))
