about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMatthias Krüger <476013+matthiaskrgr@users.noreply.github.com>2025-03-17 16:34:48 +0100
committerGitHub <noreply@github.com>2025-03-17 16:34:48 +0100
commitfd4ad332422e7c7c2cb61cbb8cb63029367eb710 (patch)
treeca4f96eafb4aa6bc384b230e6743c56f8fcd9d25
parent9adf2189f55b6e20ab8b490cc75438ba5ef2e793 (diff)
parent5434242af764d1525bd6ddf6e53ee5567042e381 (diff)
downloadrust-fd4ad332422e7c7c2cb61cbb8cb63029367eb710.tar.gz
rust-fd4ad332422e7c7c2cb61cbb8cb63029367eb710.zip
Rollup merge of #137465 - Zalathar:visit-primary, r=oli-obk
mir_build: Avoid some useless work when visiting "primary" bindings

While looking over `visit_primary_bindings`, I noticed that it does a bunch of extra work to build up a collection of “user-type projections”, even though 2/3 of its call sites don't even use them. Those callers can get the same result via `thir::Pat::walk_always`.

(And it turns out that doing so also avoids creating some redundant user-type entries in MIR for some binding constructs.)

I also noticed that even when the user-type projections *are* used, the process of building them ends up eagerly cloning some nested vectors at every recursion step, even in cases where they won't be used because the current subpattern has no bindings. To avoid this, the visit method now assembles a linked list on the stack containing the information that *would* be needed to create projections, and only creates the concrete projections as needed when a primary binding is encountered.

Some relevant prior PRs:
- #55274
- https://github.com/rust-lang/rust/commit/0bfe184b1ad14db4b002c3a272adf44e1839822f in #55937

---

There should be no user-visible change in compiler output.
-rw-r--r--compiler/rustc_middle/src/mir/mod.rs85
-rw-r--r--compiler/rustc_middle/src/thir.rs4
-rw-r--r--compiler/rustc_mir_build/src/builder/block.rs46
-rw-r--r--compiler/rustc_mir_build/src/builder/matches/mod.rs117
-rw-r--r--compiler/rustc_mir_build/src/builder/matches/user_ty.rs140
-rw-r--r--tests/mir-opt/building/user_type_annotations.let_else.built.after.mir80
-rw-r--r--tests/mir-opt/building/user_type_annotations.let_else_bindless.built.after.mir62
-rw-r--r--tests/mir-opt/building/user_type_annotations.let_init.built.after.mir54
-rw-r--r--tests/mir-opt/building/user_type_annotations.let_init_bindless.built.after.mir39
-rw-r--r--tests/mir-opt/building/user_type_annotations.let_uninit.built.after.mir27
-rw-r--r--tests/mir-opt/building/user_type_annotations.let_uninit_bindless.built.after.mir15
-rw-r--r--tests/mir-opt/building/user_type_annotations.match_assoc_const.built.after.mir46
-rw-r--r--tests/mir-opt/building/user_type_annotations.match_assoc_const_range.built.after.mir74
-rw-r--r--tests/mir-opt/building/user_type_annotations.rs66
14 files changed, 699 insertions, 156 deletions
diff --git a/compiler/rustc_middle/src/mir/mod.rs b/compiler/rustc_middle/src/mir/mod.rs
index 7090e93549e..4dfb362f3a2 100644
--- a/compiler/rustc_middle/src/mir/mod.rs
+++ b/compiler/rustc_middle/src/mir/mod.rs
@@ -33,8 +33,8 @@ use crate::mir::interpret::{AllocRange, Scalar};
 use crate::ty::codec::{TyDecoder, TyEncoder};
 use crate::ty::print::{FmtPrinter, Printer, pretty_print_const, with_no_trimmed_paths};
 use crate::ty::{
-    self, AdtDef, GenericArg, GenericArgsRef, Instance, InstanceKind, List, Ty, TyCtxt,
-    TypeVisitableExt, TypingEnv, UserTypeAnnotationIndex,
+    self, GenericArg, GenericArgsRef, Instance, InstanceKind, List, Ty, TyCtxt, TypeVisitableExt,
+    TypingEnv, UserTypeAnnotationIndex,
 };
 
 mod basic_blocks;
@@ -1482,53 +1482,10 @@ pub struct UserTypeProjections {
     pub contents: Vec<UserTypeProjection>,
 }
 
-impl<'tcx> UserTypeProjections {
-    pub fn none() -> Self {
-        UserTypeProjections { contents: vec![] }
-    }
-
-    pub fn is_empty(&self) -> bool {
-        self.contents.is_empty()
-    }
-
+impl UserTypeProjections {
     pub fn projections(&self) -> impl Iterator<Item = &UserTypeProjection> + ExactSizeIterator {
         self.contents.iter()
     }
-
-    pub fn push_user_type(mut self, base_user_type: UserTypeAnnotationIndex) -> Self {
-        self.contents.push(UserTypeProjection { base: base_user_type, projs: vec![] });
-        self
-    }
-
-    fn map_projections(mut self, f: impl FnMut(UserTypeProjection) -> UserTypeProjection) -> Self {
-        self.contents = self.contents.into_iter().map(f).collect();
-        self
-    }
-
-    pub fn index(self) -> Self {
-        self.map_projections(|pat_ty_proj| pat_ty_proj.index())
-    }
-
-    pub fn subslice(self, from: u64, to: u64) -> Self {
-        self.map_projections(|pat_ty_proj| pat_ty_proj.subslice(from, to))
-    }
-
-    pub fn deref(self) -> Self {
-        self.map_projections(|pat_ty_proj| pat_ty_proj.deref())
-    }
-
-    pub fn leaf(self, field: FieldIdx) -> Self {
-        self.map_projections(|pat_ty_proj| pat_ty_proj.leaf(field))
-    }
-
-    pub fn variant(
-        self,
-        adt_def: AdtDef<'tcx>,
-        variant_index: VariantIdx,
-        field_index: FieldIdx,
-    ) -> Self {
-        self.map_projections(|pat_ty_proj| pat_ty_proj.variant(adt_def, variant_index, field_index))
-    }
 }
 
 /// Encodes the effect of a user-supplied type annotation on the
@@ -1553,42 +1510,6 @@ pub struct UserTypeProjection {
     pub projs: Vec<ProjectionKind>,
 }
 
-impl UserTypeProjection {
-    pub(crate) fn index(mut self) -> Self {
-        self.projs.push(ProjectionElem::Index(()));
-        self
-    }
-
-    pub(crate) fn subslice(mut self, from: u64, to: u64) -> Self {
-        self.projs.push(ProjectionElem::Subslice { from, to, from_end: true });
-        self
-    }
-
-    pub(crate) fn deref(mut self) -> Self {
-        self.projs.push(ProjectionElem::Deref);
-        self
-    }
-
-    pub(crate) fn leaf(mut self, field: FieldIdx) -> Self {
-        self.projs.push(ProjectionElem::Field(field, ()));
-        self
-    }
-
-    pub(crate) fn variant(
-        mut self,
-        adt_def: AdtDef<'_>,
-        variant_index: VariantIdx,
-        field_index: FieldIdx,
-    ) -> Self {
-        self.projs.push(ProjectionElem::Downcast(
-            Some(adt_def.variant(variant_index).name),
-            variant_index,
-        ));
-        self.projs.push(ProjectionElem::Field(field_index, ()));
-        self
-    }
-}
-
 rustc_index::newtype_index! {
     #[derive(HashStable)]
     #[encodable]
diff --git a/compiler/rustc_middle/src/thir.rs b/compiler/rustc_middle/src/thir.rs
index f7b98d935d4..1056644b813 100644
--- a/compiler/rustc_middle/src/thir.rs
+++ b/compiler/rustc_middle/src/thir.rs
@@ -783,8 +783,12 @@ pub enum PatKind<'tcx> {
         var: LocalVarId,
         ty: Ty<'tcx>,
         subpattern: Option<Box<Pat<'tcx>>>,
+
         /// Is this the leftmost occurrence of the binding, i.e., is `var` the
         /// `HirId` of this pattern?
+        ///
+        /// (The same binding can occur multiple times in different branches of
+        /// an or-pattern, but only one of them will be primary.)
         is_primary: bool,
     },
 
diff --git a/compiler/rustc_mir_build/src/builder/block.rs b/compiler/rustc_mir_build/src/builder/block.rs
index 7c76e02fcef..a71196f79d7 100644
--- a/compiler/rustc_mir_build/src/builder/block.rs
+++ b/compiler/rustc_mir_build/src/builder/block.rs
@@ -199,19 +199,15 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
                             None,
                             Some((Some(&destination), initializer_span)),
                         );
-                        this.visit_primary_bindings(
-                            pattern,
-                            UserTypeProjections::none(),
-                            &mut |this, _, _, node, span, _, _| {
-                                this.storage_live_binding(
-                                    block,
-                                    node,
-                                    span,
-                                    OutsideGuard,
-                                    ScheduleDrops::Yes,
-                                );
-                            },
-                        );
+                        this.visit_primary_bindings(pattern, &mut |this, node, span| {
+                            this.storage_live_binding(
+                                block,
+                                node,
+                                span,
+                                OutsideGuard,
+                                ScheduleDrops::Yes,
+                            );
+                        });
                         let else_block_span = this.thir[*else_block].span;
                         let (matching, failure) =
                             this.in_if_then_scope(last_remainder_scope, else_block_span, |this| {
@@ -295,20 +291,16 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
                         });
 
                         debug!("ast_block_stmts: pattern={:?}", pattern);
-                        this.visit_primary_bindings(
-                            pattern,
-                            UserTypeProjections::none(),
-                            &mut |this, _, _, node, span, _, _| {
-                                this.storage_live_binding(
-                                    block,
-                                    node,
-                                    span,
-                                    OutsideGuard,
-                                    ScheduleDrops::Yes,
-                                );
-                                this.schedule_drop_for_binding(node, span, OutsideGuard);
-                            },
-                        )
+                        this.visit_primary_bindings(pattern, &mut |this, node, span| {
+                            this.storage_live_binding(
+                                block,
+                                node,
+                                span,
+                                OutsideGuard,
+                                ScheduleDrops::Yes,
+                            );
+                            this.schedule_drop_for_binding(node, span, OutsideGuard);
+                        })
                     }
 
                     // Enter the visibility scope, after evaluating the initializer.
diff --git a/compiler/rustc_mir_build/src/builder/matches/mod.rs b/compiler/rustc_mir_build/src/builder/matches/mod.rs
index b05052a3455..ea341b604e0 100644
--- a/compiler/rustc_mir_build/src/builder/matches/mod.rs
+++ b/compiler/rustc_mir_build/src/builder/matches/mod.rs
@@ -5,6 +5,11 @@
 //! This also includes code for pattern bindings in `let` statements and
 //! function parameters.
 
+use std::assert_matches::assert_matches;
+use std::borrow::Borrow;
+use std::mem;
+use std::sync::Arc;
+
 use rustc_abi::VariantIdx;
 use rustc_data_structures::fx::FxIndexMap;
 use rustc_data_structures::stack::ensure_sufficient_stack;
@@ -19,6 +24,7 @@ use tracing::{debug, instrument};
 
 use crate::builder::ForGuard::{self, OutsideGuard, RefWithinGuard};
 use crate::builder::expr::as_place::PlaceBuilder;
+use crate::builder::matches::user_ty::ProjectedUserTypesNode;
 use crate::builder::scope::DropKind;
 use crate::builder::{
     BlockAnd, BlockAndExtension, Builder, GuardFrame, GuardFrameLocal, LocalsForNode,
@@ -27,13 +33,9 @@ use crate::builder::{
 // helper functions, broken out by category:
 mod match_pair;
 mod test;
+mod user_ty;
 mod util;
 
-use std::assert_matches::assert_matches;
-use std::borrow::Borrow;
-use std::mem;
-use std::sync::Arc;
-
 /// Arguments to [`Builder::then_else_break_inner`] that are usually forwarded
 /// to recursive invocations.
 #[derive(Clone, Copy)]
@@ -755,24 +757,23 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
         guard: Option<ExprId>,
         opt_match_place: Option<(Option<&Place<'tcx>>, Span)>,
     ) -> Option<SourceScope> {
-        self.visit_primary_bindings(
+        self.visit_primary_bindings_special(
             pattern,
-            UserTypeProjections::none(),
-            &mut |this, name, mode, var, span, ty, user_ty| {
-                if visibility_scope.is_none() {
-                    visibility_scope =
-                        Some(this.new_source_scope(scope_span, LintLevel::Inherited));
-                }
+            &ProjectedUserTypesNode::None,
+            &mut |this, name, mode, var, span, ty, user_tys| {
+                let vis_scope = *visibility_scope
+                    .get_or_insert_with(|| this.new_source_scope(scope_span, LintLevel::Inherited));
                 let source_info = SourceInfo { span, scope: this.source_scope };
-                let visibility_scope = visibility_scope.unwrap();
+                let user_tys = user_tys.build_user_type_projections();
+
                 this.declare_binding(
                     source_info,
-                    visibility_scope,
+                    vis_scope,
                     name,
                     mode,
                     var,
                     ty,
-                    user_ty,
+                    user_tys,
                     ArmHasGuard(guard.is_some()),
                     opt_match_place.map(|(x, y)| (x.cloned(), y)),
                     pattern.span,
@@ -848,13 +849,35 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
         }
     }
 
-    /// Visit all of the primary bindings in a patterns, that is, visit the
-    /// leftmost occurrence of each variable bound in a pattern. A variable
-    /// will occur more than once in an or-pattern.
+    /// Visits all of the "primary" bindings in a pattern, i.e. the leftmost
+    /// occurrence of each variable bound by the pattern.
+    /// See [`PatKind::Binding::is_primary`] for more context.
+    ///
+    /// This variant provides only the limited subset of binding data needed
+    /// by its callers, and should be a "pure" visit without side-effects.
     pub(super) fn visit_primary_bindings(
         &mut self,
         pattern: &Pat<'tcx>,
-        pattern_user_ty: UserTypeProjections,
+        f: &mut impl FnMut(&mut Self, LocalVarId, Span),
+    ) {
+        pattern.walk_always(|pat| {
+            if let PatKind::Binding { var, is_primary: true, .. } = pat.kind {
+                f(self, var, pat.span);
+            }
+        })
+    }
+
+    /// Visits all of the "primary" bindings in a pattern, while preparing
+    /// additional user-type-annotation data needed by `declare_bindings`.
+    ///
+    /// This also has the side-effect of pushing all user type annotations
+    /// onto `canonical_user_type_annotations`, so that they end up in MIR
+    /// even if they aren't associated with any bindings.
+    #[instrument(level = "debug", skip(self, f))]
+    fn visit_primary_bindings_special(
+        &mut self,
+        pattern: &Pat<'tcx>,
+        user_tys: &ProjectedUserTypesNode<'_>,
         f: &mut impl FnMut(
             &mut Self,
             Symbol,
@@ -862,20 +885,21 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
             LocalVarId,
             Span,
             Ty<'tcx>,
-            UserTypeProjections,
+            &ProjectedUserTypesNode<'_>,
         ),
     ) {
-        debug!(
-            "visit_primary_bindings: pattern={:?} pattern_user_ty={:?}",
-            pattern, pattern_user_ty
-        );
+        // Avoid having to write the full method name at each recursive call.
+        let visit_subpat = |this: &mut Self, subpat, user_tys: &_, f: &mut _| {
+            this.visit_primary_bindings_special(subpat, user_tys, f)
+        };
+
         match pattern.kind {
             PatKind::Binding { name, mode, var, ty, ref subpattern, is_primary, .. } => {
                 if is_primary {
-                    f(self, name, mode, var, pattern.span, ty, pattern_user_ty.clone());
+                    f(self, name, mode, var, pattern.span, ty, user_tys);
                 }
                 if let Some(subpattern) = subpattern.as_ref() {
-                    self.visit_primary_bindings(subpattern, pattern_user_ty, f);
+                    visit_subpat(self, subpattern, user_tys, f);
                 }
             }
 
@@ -884,17 +908,13 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
                 let from = u64::try_from(prefix.len()).unwrap();
                 let to = u64::try_from(suffix.len()).unwrap();
                 for subpattern in prefix.iter() {
-                    self.visit_primary_bindings(subpattern, pattern_user_ty.clone().index(), f);
+                    visit_subpat(self, subpattern, &user_tys.index(), f);
                 }
                 if let Some(subpattern) = slice {
-                    self.visit_primary_bindings(
-                        subpattern,
-                        pattern_user_ty.clone().subslice(from, to),
-                        f,
-                    );
+                    visit_subpat(self, subpattern, &user_tys.subslice(from, to), f);
                 }
                 for subpattern in suffix.iter() {
-                    self.visit_primary_bindings(subpattern, pattern_user_ty.clone().index(), f);
+                    visit_subpat(self, subpattern, &user_tys.index(), f);
                 }
             }
 
@@ -905,11 +925,11 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
             | PatKind::Error(_) => {}
 
             PatKind::Deref { ref subpattern } => {
-                self.visit_primary_bindings(subpattern, pattern_user_ty.deref(), f);
+                visit_subpat(self, subpattern, &user_tys.deref(), f);
             }
 
             PatKind::DerefPattern { ref subpattern, .. } => {
-                self.visit_primary_bindings(subpattern, UserTypeProjections::none(), f);
+                visit_subpat(self, subpattern, &ProjectedUserTypesNode::None, f);
             }
 
             PatKind::AscribeUserType {
@@ -925,28 +945,31 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
                 // Note that the variance doesn't apply here, as we are tracking the effect
                 // of `user_ty` on any bindings contained with subpattern.
 
+                // Caution: Pushing this user type here is load-bearing even for
+                // patterns containing no bindings, to ensure that the type ends
+                // up represented in MIR _somewhere_.
                 let base_user_ty = self.canonical_user_type_annotations.push(annotation.clone());
-                let subpattern_user_ty = pattern_user_ty.push_user_type(base_user_ty);
-                self.visit_primary_bindings(subpattern, subpattern_user_ty, f)
+                let subpattern_user_tys = user_tys.push_user_type(base_user_ty);
+                visit_subpat(self, subpattern, &subpattern_user_tys, f)
             }
 
             PatKind::ExpandedConstant { ref subpattern, .. } => {
-                self.visit_primary_bindings(subpattern, pattern_user_ty, f)
+                visit_subpat(self, subpattern, user_tys, f)
             }
 
             PatKind::Leaf { ref subpatterns } => {
                 for subpattern in subpatterns {
-                    let subpattern_user_ty = pattern_user_ty.clone().leaf(subpattern.field);
-                    debug!("visit_primary_bindings: subpattern_user_ty={:?}", subpattern_user_ty);
-                    self.visit_primary_bindings(&subpattern.pattern, subpattern_user_ty, f);
+                    let subpattern_user_tys = user_tys.leaf(subpattern.field);
+                    debug!("visit_primary_bindings: subpattern_user_tys={subpattern_user_tys:?}");
+                    visit_subpat(self, &subpattern.pattern, &subpattern_user_tys, f);
                 }
             }
 
             PatKind::Variant { adt_def, args: _, variant_index, ref subpatterns } => {
                 for subpattern in subpatterns {
-                    let subpattern_user_ty =
-                        pattern_user_ty.clone().variant(adt_def, variant_index, subpattern.field);
-                    self.visit_primary_bindings(&subpattern.pattern, subpattern_user_ty, f);
+                    let subpattern_user_tys =
+                        user_tys.variant(adt_def, variant_index, subpattern.field);
+                    visit_subpat(self, &subpattern.pattern, &subpattern_user_tys, f);
                 }
             }
             PatKind::Or { ref pats } => {
@@ -955,7 +978,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
                 // `let (x | y) = ...`, the primary binding of `y` occurs in
                 // the right subpattern
                 for subpattern in pats.iter() {
-                    self.visit_primary_bindings(subpattern, pattern_user_ty.clone(), f);
+                    visit_subpat(self, subpattern, user_tys, f);
                 }
             }
         }
@@ -2747,7 +2770,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
         mode: BindingMode,
         var_id: LocalVarId,
         var_ty: Ty<'tcx>,
-        user_ty: UserTypeProjections,
+        user_ty: Option<Box<UserTypeProjections>>,
         has_guard: ArmHasGuard,
         opt_match_place: Option<(Option<Place<'tcx>>, Span)>,
         pat_span: Span,
@@ -2757,7 +2780,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
         let local = LocalDecl {
             mutability: mode.1,
             ty: var_ty,
-            user_ty: if user_ty.is_empty() { None } else { Some(Box::new(user_ty)) },
+            user_ty,
             source_info,
             local_info: ClearCrossCrate::Set(Box::new(LocalInfo::User(BindingForm::Var(
                 VarBindingForm {
diff --git a/compiler/rustc_mir_build/src/builder/matches/user_ty.rs b/compiler/rustc_mir_build/src/builder/matches/user_ty.rs
new file mode 100644
index 00000000000..df9f93ac328
--- /dev/null
+++ b/compiler/rustc_mir_build/src/builder/matches/user_ty.rs
@@ -0,0 +1,140 @@
+//! Helper code for building a linked list of user-type projections on the
+//! stack while visiting a THIR pattern.
+//!
+//! This avoids having to repeatedly clone a partly-built [`UserTypeProjections`]
+//! at every step of the traversal, which is what the previous code was doing.
+
+use std::assert_matches::assert_matches;
+use std::iter;
+
+use rustc_abi::{FieldIdx, VariantIdx};
+use rustc_middle::mir::{ProjectionElem, UserTypeProjection, UserTypeProjections};
+use rustc_middle::ty::{AdtDef, UserTypeAnnotationIndex};
+use rustc_span::Symbol;
+
+/// One of a list of "operations" that can be used to lazily build projections
+/// of user-specified types.
+#[derive(Clone, Debug)]
+pub(crate) enum ProjectedUserTypesOp {
+    PushUserType { base: UserTypeAnnotationIndex },
+
+    Index,
+    Subslice { from: u64, to: u64 },
+    Deref,
+    Leaf { field: FieldIdx },
+    Variant { name: Symbol, variant: VariantIdx, field: FieldIdx },
+}
+
+#[derive(Debug)]
+pub(crate) enum ProjectedUserTypesNode<'a> {
+    None,
+    Chain { parent: &'a Self, op: ProjectedUserTypesOp },
+}
+
+impl<'a> ProjectedUserTypesNode<'a> {
+    pub(crate) fn push_user_type(&'a self, base: UserTypeAnnotationIndex) -> Self {
+        // Pushing a base user type always causes the chain to become non-empty.
+        Self::Chain { parent: self, op: ProjectedUserTypesOp::PushUserType { base } }
+    }
+
+    /// Push another projection op onto the chain, but only if it is already non-empty.
+    fn maybe_push(&'a self, op_fn: impl FnOnce() -> ProjectedUserTypesOp) -> Self {
+        match self {
+            Self::None => Self::None,
+            Self::Chain { .. } => Self::Chain { parent: self, op: op_fn() },
+        }
+    }
+
+    pub(crate) fn index(&'a self) -> Self {
+        self.maybe_push(|| ProjectedUserTypesOp::Index)
+    }
+
+    pub(crate) fn subslice(&'a self, from: u64, to: u64) -> Self {
+        self.maybe_push(|| ProjectedUserTypesOp::Subslice { from, to })
+    }
+
+    pub(crate) fn deref(&'a self) -> Self {
+        self.maybe_push(|| ProjectedUserTypesOp::Deref)
+    }
+
+    pub(crate) fn leaf(&'a self, field: FieldIdx) -> Self {
+        self.maybe_push(|| ProjectedUserTypesOp::Leaf { field })
+    }
+
+    pub(crate) fn variant(
+        &'a self,
+        adt_def: AdtDef<'_>,
+        variant: VariantIdx,
+        field: FieldIdx,
+    ) -> Self {
+        self.maybe_push(|| {
+            let name = adt_def.variant(variant).name;
+            ProjectedUserTypesOp::Variant { name, variant, field }
+        })
+    }
+
+    /// Traverses the chain of nodes to yield each op in the chain.
+    /// Because this walks from child node to parent node, the ops are
+    /// naturally yielded in "reverse" order.
+    fn iter_ops_reversed(&'a self) -> impl Iterator<Item = &'a ProjectedUserTypesOp> {
+        let mut next = self;
+        iter::from_fn(move || match next {
+            Self::None => None,
+            Self::Chain { parent, op } => {
+                next = parent;
+                Some(op)
+            }
+        })
+    }
+
+    /// Assembles this chain of user-type projections into a proper data structure.
+    pub(crate) fn build_user_type_projections(&self) -> Option<Box<UserTypeProjections>> {
+        // If we know there's nothing to do, just return None immediately.
+        if matches!(self, Self::None) {
+            return None;
+        }
+
+        let ops_reversed = self.iter_ops_reversed().cloned().collect::<Vec<_>>();
+        // The "first" op should always be `PushUserType`.
+        // Other projections are only added if there is at least one user type.
+        assert_matches!(ops_reversed.last(), Some(ProjectedUserTypesOp::PushUserType { .. }));
+
+        let mut projections = vec![];
+        for op in ops_reversed.into_iter().rev() {
+            match op {
+                ProjectedUserTypesOp::PushUserType { base } => {
+                    projections.push(UserTypeProjection { base, projs: vec![] })
+                }
+
+                ProjectedUserTypesOp::Index => {
+                    for p in &mut projections {
+                        p.projs.push(ProjectionElem::Index(()))
+                    }
+                }
+                ProjectedUserTypesOp::Subslice { from, to } => {
+                    for p in &mut projections {
+                        p.projs.push(ProjectionElem::Subslice { from, to, from_end: true })
+                    }
+                }
+                ProjectedUserTypesOp::Deref => {
+                    for p in &mut projections {
+                        p.projs.push(ProjectionElem::Deref)
+                    }
+                }
+                ProjectedUserTypesOp::Leaf { field } => {
+                    for p in &mut projections {
+                        p.projs.push(ProjectionElem::Field(field, ()))
+                    }
+                }
+                ProjectedUserTypesOp::Variant { name, variant, field } => {
+                    for p in &mut projections {
+                        p.projs.push(ProjectionElem::Downcast(Some(name), variant));
+                        p.projs.push(ProjectionElem::Field(field, ()));
+                    }
+                }
+            }
+        }
+
+        Some(Box::new(UserTypeProjections { contents: projections }))
+    }
+}
diff --git a/tests/mir-opt/building/user_type_annotations.let_else.built.after.mir b/tests/mir-opt/building/user_type_annotations.let_else.built.after.mir
new file mode 100644
index 00000000000..3a515787c10
--- /dev/null
+++ b/tests/mir-opt/building/user_type_annotations.let_else.built.after.mir
@@ -0,0 +1,80 @@
+// MIR for `let_else` after built
+
+| User Type Annotations
+| 0: user_ty: Canonical { value: Ty((u32, u64, &'static char)), max_universe: U0, variables: [] }, span: $DIR/user_type_annotations.rs:35:20: 35:45, inferred_ty: (u32, u64, &char)
+| 1: user_ty: Canonical { value: Ty((u32, u64, &'static char)), max_universe: U0, variables: [] }, span: $DIR/user_type_annotations.rs:35:20: 35:45, inferred_ty: (u32, u64, &char)
+|
+fn let_else() -> () {
+    let mut _0: ();
+    let mut _1: !;
+    let _2: u32 as UserTypeProjection { base: UserType(0), projs: [Field(0, ())] };
+    let _3: u64 as UserTypeProjection { base: UserType(0), projs: [Field(1, ())] };
+    let _4: &char as UserTypeProjection { base: UserType(0), projs: [Field(2, ())] };
+    let mut _5: (u32, u64, &char);
+    let mut _6: &char;
+    let _7: &char;
+    let _8: char;
+    scope 1 {
+        debug x => _2;
+        debug y => _3;
+        debug z => _4;
+    }
+
+    bb0: {
+        StorageLive(_2);
+        StorageLive(_3);
+        StorageLive(_4);
+        StorageLive(_5);
+        StorageLive(_6);
+        StorageLive(_7);
+        StorageLive(_8);
+        _8 = const 'u';
+        _7 = &_8;
+        _6 = &(*_7);
+        _5 = (const 7_u32, const 12_u64, move _6);
+        StorageDead(_6);
+        PlaceMention(_5);
+        falseEdge -> [real: bb4, imaginary: bb3];
+    }
+
+    bb1: {
+        _1 = core::panicking::panic(const "internal error: entered unreachable code") -> bb6;
+    }
+
+    bb2: {
+        unreachable;
+    }
+
+    bb3: {
+        goto -> bb5;
+    }
+
+    bb4: {
+        AscribeUserType(_5, +, UserTypeProjection { base: UserType(1), projs: [] });
+        _2 = copy (_5.0: u32);
+        _3 = copy (_5.1: u64);
+        _4 = copy (_5.2: &char);
+        StorageDead(_7);
+        StorageDead(_5);
+        _0 = const ();
+        StorageDead(_8);
+        StorageDead(_4);
+        StorageDead(_3);
+        StorageDead(_2);
+        return;
+    }
+
+    bb5: {
+        StorageDead(_7);
+        StorageDead(_5);
+        StorageDead(_8);
+        StorageDead(_4);
+        StorageDead(_3);
+        StorageDead(_2);
+        goto -> bb1;
+    }
+
+    bb6 (cleanup): {
+        resume;
+    }
+}
diff --git a/tests/mir-opt/building/user_type_annotations.let_else_bindless.built.after.mir b/tests/mir-opt/building/user_type_annotations.let_else_bindless.built.after.mir
new file mode 100644
index 00000000000..52a6d904d45
--- /dev/null
+++ b/tests/mir-opt/building/user_type_annotations.let_else_bindless.built.after.mir
@@ -0,0 +1,62 @@
+// MIR for `let_else_bindless` after built
+
+| User Type Annotations
+| 0: user_ty: Canonical { value: Ty((u32, u64, &'static char)), max_universe: U0, variables: [] }, span: $DIR/user_type_annotations.rs:40:20: 40:45, inferred_ty: (u32, u64, &char)
+| 1: user_ty: Canonical { value: Ty((u32, u64, &'static char)), max_universe: U0, variables: [] }, span: $DIR/user_type_annotations.rs:40:20: 40:45, inferred_ty: (u32, u64, &char)
+|
+fn let_else_bindless() -> () {
+    let mut _0: ();
+    let mut _1: !;
+    let mut _2: (u32, u64, &char);
+    let mut _3: &char;
+    let _4: &char;
+    let _5: char;
+    scope 1 {
+    }
+
+    bb0: {
+        StorageLive(_2);
+        StorageLive(_3);
+        StorageLive(_4);
+        StorageLive(_5);
+        _5 = const 'u';
+        _4 = &_5;
+        _3 = &(*_4);
+        _2 = (const 7_u32, const 12_u64, move _3);
+        StorageDead(_3);
+        PlaceMention(_2);
+        falseEdge -> [real: bb4, imaginary: bb3];
+    }
+
+    bb1: {
+        _1 = core::panicking::panic(const "internal error: entered unreachable code") -> bb6;
+    }
+
+    bb2: {
+        unreachable;
+    }
+
+    bb3: {
+        goto -> bb5;
+    }
+
+    bb4: {
+        AscribeUserType(_2, +, UserTypeProjection { base: UserType(1), projs: [] });
+        StorageDead(_4);
+        StorageDead(_2);
+        _0 = const ();
+        StorageDead(_5);
+        return;
+    }
+
+    bb5: {
+        StorageDead(_4);
+        StorageDead(_2);
+        StorageDead(_5);
+        goto -> bb1;
+    }
+
+    bb6 (cleanup): {
+        resume;
+    }
+}
diff --git a/tests/mir-opt/building/user_type_annotations.let_init.built.after.mir b/tests/mir-opt/building/user_type_annotations.let_init.built.after.mir
new file mode 100644
index 00000000000..d1b8f823e9b
--- /dev/null
+++ b/tests/mir-opt/building/user_type_annotations.let_init.built.after.mir
@@ -0,0 +1,54 @@
+// MIR for `let_init` after built
+
+| User Type Annotations
+| 0: user_ty: Canonical { value: Ty((u32, u64, &'static char)), max_universe: U0, variables: [] }, span: $DIR/user_type_annotations.rs:25:20: 25:45, inferred_ty: (u32, u64, &char)
+| 1: user_ty: Canonical { value: Ty((u32, u64, &'static char)), max_universe: U0, variables: [] }, span: $DIR/user_type_annotations.rs:25:20: 25:45, inferred_ty: (u32, u64, &char)
+|
+fn let_init() -> () {
+    let mut _0: ();
+    let _1: u32 as UserTypeProjection { base: UserType(0), projs: [Field(0, ())] };
+    let _2: u64 as UserTypeProjection { base: UserType(0), projs: [Field(1, ())] };
+    let _3: &char as UserTypeProjection { base: UserType(0), projs: [Field(2, ())] };
+    let mut _4: (u32, u64, &char);
+    let mut _5: &char;
+    let _6: &char;
+    let _7: char;
+    scope 1 {
+        debug x => _1;
+        debug y => _2;
+        debug z => _3;
+    }
+
+    bb0: {
+        StorageLive(_4);
+        StorageLive(_5);
+        StorageLive(_6);
+        StorageLive(_7);
+        _7 = const 'u';
+        _6 = &_7;
+        _5 = &(*_6);
+        _4 = (const 7_u32, const 12_u64, move _5);
+        StorageDead(_5);
+        PlaceMention(_4);
+        AscribeUserType(_4, +, UserTypeProjection { base: UserType(1), projs: [] });
+        StorageLive(_1);
+        _1 = copy (_4.0: u32);
+        StorageLive(_2);
+        _2 = copy (_4.1: u64);
+        StorageLive(_3);
+        _3 = copy (_4.2: &char);
+        StorageDead(_6);
+        StorageDead(_4);
+        _0 = const ();
+        StorageDead(_3);
+        StorageDead(_2);
+        StorageDead(_1);
+        StorageDead(_7);
+        return;
+    }
+
+    bb1: {
+        FakeRead(ForMatchedPlace(None), _4);
+        unreachable;
+    }
+}
diff --git a/tests/mir-opt/building/user_type_annotations.let_init_bindless.built.after.mir b/tests/mir-opt/building/user_type_annotations.let_init_bindless.built.after.mir
new file mode 100644
index 00000000000..6702f930060
--- /dev/null
+++ b/tests/mir-opt/building/user_type_annotations.let_init_bindless.built.after.mir
@@ -0,0 +1,39 @@
+// MIR for `let_init_bindless` after built
+
+| User Type Annotations
+| 0: user_ty: Canonical { value: Ty((u32, u64, &'static char)), max_universe: U0, variables: [] }, span: $DIR/user_type_annotations.rs:30:20: 30:45, inferred_ty: (u32, u64, &char)
+| 1: user_ty: Canonical { value: Ty((u32, u64, &'static char)), max_universe: U0, variables: [] }, span: $DIR/user_type_annotations.rs:30:20: 30:45, inferred_ty: (u32, u64, &char)
+|
+fn let_init_bindless() -> () {
+    let mut _0: ();
+    let mut _1: (u32, u64, &char);
+    let mut _2: &char;
+    let _3: &char;
+    let _4: char;
+    scope 1 {
+    }
+
+    bb0: {
+        StorageLive(_1);
+        StorageLive(_2);
+        StorageLive(_3);
+        StorageLive(_4);
+        _4 = const 'u';
+        _3 = &_4;
+        _2 = &(*_3);
+        _1 = (const 7_u32, const 12_u64, move _2);
+        StorageDead(_2);
+        PlaceMention(_1);
+        AscribeUserType(_1, +, UserTypeProjection { base: UserType(1), projs: [] });
+        StorageDead(_3);
+        StorageDead(_1);
+        _0 = const ();
+        StorageDead(_4);
+        return;
+    }
+
+    bb1: {
+        FakeRead(ForMatchedPlace(None), _1);
+        unreachable;
+    }
+}
diff --git a/tests/mir-opt/building/user_type_annotations.let_uninit.built.after.mir b/tests/mir-opt/building/user_type_annotations.let_uninit.built.after.mir
new file mode 100644
index 00000000000..76b5938b87d
--- /dev/null
+++ b/tests/mir-opt/building/user_type_annotations.let_uninit.built.after.mir
@@ -0,0 +1,27 @@
+// MIR for `let_uninit` after built
+
+| User Type Annotations
+| 0: user_ty: Canonical { value: Ty((u32, u64, &'static char)), max_universe: U0, variables: [] }, span: $DIR/user_type_annotations.rs:15:20: 15:45, inferred_ty: (u32, u64, &char)
+|
+fn let_uninit() -> () {
+    let mut _0: ();
+    let _1: u32 as UserTypeProjection { base: UserType(0), projs: [Field(0, ())] };
+    let _2: u64 as UserTypeProjection { base: UserType(0), projs: [Field(1, ())] };
+    let _3: &char as UserTypeProjection { base: UserType(0), projs: [Field(2, ())] };
+    scope 1 {
+        debug x => _1;
+        debug y => _2;
+        debug z => _3;
+    }
+
+    bb0: {
+        StorageLive(_1);
+        StorageLive(_2);
+        StorageLive(_3);
+        _0 = const ();
+        StorageDead(_3);
+        StorageDead(_2);
+        StorageDead(_1);
+        return;
+    }
+}
diff --git a/tests/mir-opt/building/user_type_annotations.let_uninit_bindless.built.after.mir b/tests/mir-opt/building/user_type_annotations.let_uninit_bindless.built.after.mir
new file mode 100644
index 00000000000..0cd12558771
--- /dev/null
+++ b/tests/mir-opt/building/user_type_annotations.let_uninit_bindless.built.after.mir
@@ -0,0 +1,15 @@
+// MIR for `let_uninit_bindless` after built
+
+| User Type Annotations
+| 0: user_ty: Canonical { value: Ty((u32, u64, &'static char)), max_universe: U0, variables: [] }, span: $DIR/user_type_annotations.rs:20:20: 20:45, inferred_ty: (u32, u64, &char)
+|
+fn let_uninit_bindless() -> () {
+    let mut _0: ();
+    scope 1 {
+    }
+
+    bb0: {
+        _0 = const ();
+        return;
+    }
+}
diff --git a/tests/mir-opt/building/user_type_annotations.match_assoc_const.built.after.mir b/tests/mir-opt/building/user_type_annotations.match_assoc_const.built.after.mir
new file mode 100644
index 00000000000..c0ce6f1d06b
--- /dev/null
+++ b/tests/mir-opt/building/user_type_annotations.match_assoc_const.built.after.mir
@@ -0,0 +1,46 @@
+// MIR for `match_assoc_const` after built
+
+| User Type Annotations
+| 0: user_ty: Canonical { value: TypeOf(DefId(0:11 ~ user_type_annotations[ee8e]::MyTrait::FOO), UserArgs { args: [MyStruct, 'static], user_self_ty: None }), max_universe: U0, variables: [] }, span: $DIR/user_type_annotations.rs:54:9: 54:44, inferred_ty: u32
+| 1: user_ty: Canonical { value: TypeOf(DefId(0:11 ~ user_type_annotations[ee8e]::MyTrait::FOO), UserArgs { args: [MyStruct, 'static], user_self_ty: None }), max_universe: U0, variables: [] }, span: $DIR/user_type_annotations.rs:54:9: 54:44, inferred_ty: u32
+|
+fn match_assoc_const() -> () {
+    let mut _0: ();
+    let mut _1: u32;
+
+    bb0: {
+        StorageLive(_1);
+        _1 = const 8_u32;
+        PlaceMention(_1);
+        switchInt(copy _1) -> [99: bb2, otherwise: bb1];
+    }
+
+    bb1: {
+        _0 = const ();
+        goto -> bb6;
+    }
+
+    bb2: {
+        falseEdge -> [real: bb5, imaginary: bb1];
+    }
+
+    bb3: {
+        goto -> bb1;
+    }
+
+    bb4: {
+        FakeRead(ForMatchedPlace(None), _1);
+        unreachable;
+    }
+
+    bb5: {
+        AscribeUserType(_1, -, UserTypeProjection { base: UserType(1), projs: [] });
+        _0 = const ();
+        goto -> bb6;
+    }
+
+    bb6: {
+        StorageDead(_1);
+        return;
+    }
+}
diff --git a/tests/mir-opt/building/user_type_annotations.match_assoc_const_range.built.after.mir b/tests/mir-opt/building/user_type_annotations.match_assoc_const_range.built.after.mir
new file mode 100644
index 00000000000..3a6aa5b7c2c
--- /dev/null
+++ b/tests/mir-opt/building/user_type_annotations.match_assoc_const_range.built.after.mir
@@ -0,0 +1,74 @@
+// MIR for `match_assoc_const_range` after built
+
+| User Type Annotations
+| 0: user_ty: Canonical { value: TypeOf(DefId(0:11 ~ user_type_annotations[ee8e]::MyTrait::FOO), UserArgs { args: [MyStruct, 'static], user_self_ty: None }), max_universe: U0, variables: [] }, span: $DIR/user_type_annotations.rs:62:11: 62:46, inferred_ty: u32
+| 1: user_ty: Canonical { value: TypeOf(DefId(0:11 ~ user_type_annotations[ee8e]::MyTrait::FOO), UserArgs { args: [MyStruct, 'static], user_self_ty: None }), max_universe: U0, variables: [] }, span: $DIR/user_type_annotations.rs:62:11: 62:46, inferred_ty: u32
+| 2: user_ty: Canonical { value: TypeOf(DefId(0:11 ~ user_type_annotations[ee8e]::MyTrait::FOO), UserArgs { args: [MyStruct, 'static], user_self_ty: None }), max_universe: U0, variables: [] }, span: $DIR/user_type_annotations.rs:63:9: 63:44, inferred_ty: u32
+| 3: user_ty: Canonical { value: TypeOf(DefId(0:11 ~ user_type_annotations[ee8e]::MyTrait::FOO), UserArgs { args: [MyStruct, 'static], user_self_ty: None }), max_universe: U0, variables: [] }, span: $DIR/user_type_annotations.rs:63:9: 63:44, inferred_ty: u32
+|
+fn match_assoc_const_range() -> () {
+    let mut _0: ();
+    let mut _1: u32;
+    let mut _2: bool;
+    let mut _3: bool;
+
+    bb0: {
+        StorageLive(_1);
+        _1 = const 8_u32;
+        PlaceMention(_1);
+        _3 = Lt(copy _1, const 99_u32);
+        switchInt(move _3) -> [0: bb4, otherwise: bb2];
+    }
+
+    bb1: {
+        _0 = const ();
+        goto -> bb11;
+    }
+
+    bb2: {
+        falseEdge -> [real: bb10, imaginary: bb4];
+    }
+
+    bb3: {
+        goto -> bb1;
+    }
+
+    bb4: {
+        _2 = Le(const 99_u32, copy _1);
+        switchInt(move _2) -> [0: bb5, otherwise: bb6];
+    }
+
+    bb5: {
+        goto -> bb1;
+    }
+
+    bb6: {
+        falseEdge -> [real: bb9, imaginary: bb1];
+    }
+
+    bb7: {
+        goto -> bb5;
+    }
+
+    bb8: {
+        FakeRead(ForMatchedPlace(None), _1);
+        unreachable;
+    }
+
+    bb9: {
+        AscribeUserType(_1, -, UserTypeProjection { base: UserType(3), projs: [] });
+        _0 = const ();
+        goto -> bb11;
+    }
+
+    bb10: {
+        AscribeUserType(_1, -, UserTypeProjection { base: UserType(1), projs: [] });
+        _0 = const ();
+        goto -> bb11;
+    }
+
+    bb11: {
+        StorageDead(_1);
+        return;
+    }
+}
diff --git a/tests/mir-opt/building/user_type_annotations.rs b/tests/mir-opt/building/user_type_annotations.rs
new file mode 100644
index 00000000000..d55c678d5ae
--- /dev/null
+++ b/tests/mir-opt/building/user_type_annotations.rs
@@ -0,0 +1,66 @@
+//@ edition: 2024
+// skip-filecheck
+
+// This test demonstrates how many user type annotations are recorded in MIR
+// for various binding constructs. In particular, this makes it possible to see
+// the number of duplicate user-type-annotation entries, and whether that
+// number has changed.
+//
+// Duplicates are mostly harmless, other than being inefficient.
+// "Unused" entries that are _not_ duplicates may nevertheless be necessary so
+// that they are seen by MIR lifetime checks.
+
+// EMIT_MIR user_type_annotations.let_uninit.built.after.mir
+fn let_uninit() {
+    let (x, y, z): (u32, u64, &'static char);
+}
+
+// EMIT_MIR user_type_annotations.let_uninit_bindless.built.after.mir
+fn let_uninit_bindless() {
+    let (_, _, _): (u32, u64, &'static char);
+}
+
+// EMIT_MIR user_type_annotations.let_init.built.after.mir
+fn let_init() {
+    let (x, y, z): (u32, u64, &'static char) = (7, 12, &'u');
+}
+
+// EMIT_MIR user_type_annotations.let_init_bindless.built.after.mir
+fn let_init_bindless() {
+    let (_, _, _): (u32, u64, &'static char) = (7, 12, &'u');
+}
+
+// EMIT_MIR user_type_annotations.let_else.built.after.mir
+fn let_else() {
+    let (x, y, z): (u32, u64, &'static char) = (7, 12, &'u') else { unreachable!() };
+}
+
+// EMIT_MIR user_type_annotations.let_else_bindless.built.after.mir
+fn let_else_bindless() {
+    let (_, _, _): (u32, u64, &'static char) = (7, 12, &'u') else { unreachable!() };
+}
+
+trait MyTrait<'a> {
+    const FOO: u32;
+}
+struct MyStruct {}
+impl MyTrait<'static> for MyStruct {
+    const FOO: u32 = 99;
+}
+
+// EMIT_MIR user_type_annotations.match_assoc_const.built.after.mir
+fn match_assoc_const() {
+    match 8 {
+        <MyStruct as MyTrait<'static>>::FOO => {}
+        _ => {}
+    }
+}
+
+// EMIT_MIR user_type_annotations.match_assoc_const_range.built.after.mir
+fn match_assoc_const_range() {
+    match 8 {
+        ..<MyStruct as MyTrait<'static>>::FOO => {}
+        <MyStruct as MyTrait<'static>>::FOO.. => {}
+        _ => {}
+    }
+}