about summary refs log tree commit diff
path: root/compiler/rustc_const_eval/src/util
diff options
context:
space:
mode:
Diffstat (limited to 'compiler/rustc_const_eval/src/util')
-rw-r--r--compiler/rustc_const_eval/src/util/alignment.rs73
-rw-r--r--compiler/rustc_const_eval/src/util/caller_location.rs73
-rw-r--r--compiler/rustc_const_eval/src/util/check_validity_requirement.rs160
-rw-r--r--compiler/rustc_const_eval/src/util/compare_types.rs58
-rw-r--r--compiler/rustc_const_eval/src/util/mod.rs39
-rw-r--r--compiler/rustc_const_eval/src/util/type_name.rs186
6 files changed, 589 insertions, 0 deletions
diff --git a/compiler/rustc_const_eval/src/util/alignment.rs b/compiler/rustc_const_eval/src/util/alignment.rs
new file mode 100644
index 00000000000..528274e6aba
--- /dev/null
+++ b/compiler/rustc_const_eval/src/util/alignment.rs
@@ -0,0 +1,73 @@
+use rustc_middle::mir::*;
+use rustc_middle::ty::{self, TyCtxt};
+use rustc_target::abi::Align;
+use tracing::debug;
+
+/// Returns `true` if this place is allowed to be less aligned
+/// than its containing struct (because it is within a packed
+/// struct).
+pub fn is_disaligned<'tcx, L>(
+    tcx: TyCtxt<'tcx>,
+    local_decls: &L,
+    param_env: ty::ParamEnv<'tcx>,
+    place: Place<'tcx>,
+) -> bool
+where
+    L: HasLocalDecls<'tcx>,
+{
+    debug!("is_disaligned({:?})", place);
+    let Some(pack) = is_within_packed(tcx, local_decls, place) else {
+        debug!("is_disaligned({:?}) - not within packed", place);
+        return false;
+    };
+
+    let ty = place.ty(local_decls, tcx).ty;
+    let unsized_tail = || tcx.struct_tail_with_normalize(ty, |ty| ty, || {});
+    match tcx.layout_of(param_env.and(ty)) {
+        Ok(layout)
+            if layout.align.abi <= pack
+                && (layout.is_sized()
+                    || matches!(unsized_tail().kind(), ty::Slice(..) | ty::Str)) =>
+        {
+            // If the packed alignment is greater or equal to the field alignment, the type won't be
+            // further disaligned.
+            // However we need to ensure the field is sized; for unsized fields, `layout.align` is
+            // just an approximation -- except when the unsized tail is a slice, where the alignment
+            // is fully determined by the type.
+            debug!(
+                "is_disaligned({:?}) - align = {}, packed = {}; not disaligned",
+                place,
+                layout.align.abi.bytes(),
+                pack.bytes()
+            );
+            false
+        }
+        _ => {
+            // We cannot figure out the layout. Conservatively assume that this is disaligned.
+            debug!("is_disaligned({:?}) - true", place);
+            true
+        }
+    }
+}
+
+pub fn is_within_packed<'tcx, L>(
+    tcx: TyCtxt<'tcx>,
+    local_decls: &L,
+    place: Place<'tcx>,
+) -> Option<Align>
+where
+    L: HasLocalDecls<'tcx>,
+{
+    place
+        .iter_projections()
+        .rev()
+        // Stop at `Deref`; standard ABI alignment applies there.
+        .take_while(|(_base, elem)| !matches!(elem, ProjectionElem::Deref))
+        // Consider the packed alignments at play here...
+        .filter_map(|(base, _elem)| {
+            base.ty(local_decls, tcx).ty.ty_adt_def().and_then(|adt| adt.repr().pack)
+        })
+        // ... and compute their minimum.
+        // The overall smallest alignment is what matters.
+        .min()
+}
diff --git a/compiler/rustc_const_eval/src/util/caller_location.rs b/compiler/rustc_const_eval/src/util/caller_location.rs
new file mode 100644
index 00000000000..3b07bee2d9c
--- /dev/null
+++ b/compiler/rustc_const_eval/src/util/caller_location.rs
@@ -0,0 +1,73 @@
+use rustc_hir::LangItem;
+use rustc_middle::bug;
+use rustc_middle::mir;
+use rustc_middle::query::TyCtxtAt;
+use rustc_middle::ty::layout::LayoutOf;
+use rustc_middle::ty::{self, Mutability};
+use rustc_span::symbol::Symbol;
+use tracing::trace;
+
+use crate::const_eval::{mk_eval_cx_to_read_const_val, CanAccessMutGlobal, CompileTimeInterpCx};
+use crate::interpret::*;
+
+/// Allocate a `const core::panic::Location` with the provided filename and line/column numbers.
+fn alloc_caller_location<'tcx>(
+    ecx: &mut CompileTimeInterpCx<'tcx>,
+    filename: Symbol,
+    line: u32,
+    col: u32,
+) -> MPlaceTy<'tcx> {
+    let loc_details = ecx.tcx.sess.opts.unstable_opts.location_detail;
+    // This can fail if rustc runs out of memory right here. Trying to emit an error would be
+    // pointless, since that would require allocating more memory than these short strings.
+    let file = if loc_details.file {
+        ecx.allocate_str(filename.as_str(), MemoryKind::CallerLocation, Mutability::Not).unwrap()
+    } else {
+        // FIXME: This creates a new allocation each time. It might be preferable to
+        // perform this allocation only once, and re-use the `MPlaceTy`.
+        // See https://github.com/rust-lang/rust/pull/89920#discussion_r730012398
+        ecx.allocate_str("<redacted>", MemoryKind::CallerLocation, Mutability::Not).unwrap()
+    };
+    let file = file.map_provenance(CtfeProvenance::as_immutable);
+    let line = if loc_details.line { Scalar::from_u32(line) } else { Scalar::from_u32(0) };
+    let col = if loc_details.column { Scalar::from_u32(col) } else { Scalar::from_u32(0) };
+
+    // Allocate memory for `CallerLocation` struct.
+    let loc_ty = ecx
+        .tcx
+        .type_of(ecx.tcx.require_lang_item(LangItem::PanicLocation, None))
+        .instantiate(*ecx.tcx, ecx.tcx.mk_args(&[ecx.tcx.lifetimes.re_erased.into()]));
+    let loc_layout = ecx.layout_of(loc_ty).unwrap();
+    let location = ecx.allocate(loc_layout, MemoryKind::CallerLocation).unwrap();
+
+    // Initialize fields.
+    ecx.write_immediate(file.to_ref(ecx), &ecx.project_field(&location, 0).unwrap())
+        .expect("writing to memory we just allocated cannot fail");
+    ecx.write_scalar(line, &ecx.project_field(&location, 1).unwrap())
+        .expect("writing to memory we just allocated cannot fail");
+    ecx.write_scalar(col, &ecx.project_field(&location, 2).unwrap())
+        .expect("writing to memory we just allocated cannot fail");
+
+    location
+}
+
+pub(crate) fn const_caller_location_provider(
+    tcx: TyCtxtAt<'_>,
+    file: Symbol,
+    line: u32,
+    col: u32,
+) -> mir::ConstValue<'_> {
+    trace!("const_caller_location: {}:{}:{}", file, line, col);
+    let mut ecx = mk_eval_cx_to_read_const_val(
+        tcx.tcx,
+        tcx.span,
+        ty::ParamEnv::reveal_all(),
+        CanAccessMutGlobal::No,
+    );
+
+    let loc_place = alloc_caller_location(&mut ecx, file, line, col);
+    if intern_const_alloc_recursive(&mut ecx, InternKind::Constant, &loc_place).is_err() {
+        bug!("intern_const_alloc_recursive should not error in this case")
+    }
+    mir::ConstValue::Scalar(Scalar::from_maybe_pointer(loc_place.ptr(), &tcx))
+}
diff --git a/compiler/rustc_const_eval/src/util/check_validity_requirement.rs b/compiler/rustc_const_eval/src/util/check_validity_requirement.rs
new file mode 100644
index 00000000000..daf57285ebe
--- /dev/null
+++ b/compiler/rustc_const_eval/src/util/check_validity_requirement.rs
@@ -0,0 +1,160 @@
+use rustc_middle::bug;
+use rustc_middle::ty::layout::{LayoutCx, LayoutError, LayoutOf, TyAndLayout, ValidityRequirement};
+use rustc_middle::ty::{ParamEnv, ParamEnvAnd, Ty, TyCtxt};
+use rustc_target::abi::{Abi, FieldsShape, Scalar, Variants};
+
+use crate::const_eval::{CanAccessMutGlobal, CheckAlignment, CompileTimeMachine};
+use crate::interpret::{InterpCx, MemoryKind, OpTy};
+
+/// Determines if this type permits "raw" initialization by just transmuting some memory into an
+/// instance of `T`.
+///
+/// `init_kind` indicates if the memory is zero-initialized or left uninitialized. We assume
+/// uninitialized memory is mitigated by filling it with 0x01, which reduces the chance of causing
+/// LLVM UB.
+///
+/// By default we check whether that operation would cause *LLVM UB*, i.e., whether the LLVM IR we
+/// generate has UB or not. This is a mitigation strategy, which is why we are okay with accepting
+/// Rust UB as long as there is no risk of miscompilations. The `strict_init_checks` can be set to
+/// do a full check against Rust UB instead (in which case we will also ignore the 0x01-filling and
+/// to the full uninit check).
+pub fn check_validity_requirement<'tcx>(
+    tcx: TyCtxt<'tcx>,
+    kind: ValidityRequirement,
+    param_env_and_ty: ParamEnvAnd<'tcx, Ty<'tcx>>,
+) -> Result<bool, &'tcx LayoutError<'tcx>> {
+    let layout = tcx.layout_of(param_env_and_ty)?;
+
+    // There is nothing strict or lax about inhabitedness.
+    if kind == ValidityRequirement::Inhabited {
+        return Ok(!layout.abi.is_uninhabited());
+    }
+
+    if kind == ValidityRequirement::Uninit || tcx.sess.opts.unstable_opts.strict_init_checks {
+        might_permit_raw_init_strict(layout, tcx, kind)
+    } else {
+        let layout_cx = LayoutCx { tcx, param_env: param_env_and_ty.param_env };
+        might_permit_raw_init_lax(layout, &layout_cx, kind)
+    }
+}
+
+/// Implements the 'strict' version of the `might_permit_raw_init` checks; see that function for
+/// details.
+fn might_permit_raw_init_strict<'tcx>(
+    ty: TyAndLayout<'tcx>,
+    tcx: TyCtxt<'tcx>,
+    kind: ValidityRequirement,
+) -> Result<bool, &'tcx LayoutError<'tcx>> {
+    let machine = CompileTimeMachine::new(CanAccessMutGlobal::No, CheckAlignment::Error);
+
+    let mut cx = InterpCx::new(tcx, rustc_span::DUMMY_SP, ParamEnv::reveal_all(), machine);
+
+    let allocated = cx
+        .allocate(ty, MemoryKind::Machine(crate::const_eval::MemoryKind::Heap))
+        .expect("OOM: failed to allocate for uninit check");
+
+    if kind == ValidityRequirement::Zero {
+        cx.write_bytes_ptr(
+            allocated.ptr(),
+            std::iter::repeat(0_u8).take(ty.layout.size().bytes_usize()),
+        )
+        .expect("failed to write bytes for zero valid check");
+    }
+
+    let ot: OpTy<'_, _> = allocated.into();
+
+    // Assume that if it failed, it's a validation failure.
+    // This does *not* actually check that references are dereferenceable, but since all types that
+    // require dereferenceability also require non-null, we don't actually get any false negatives
+    // due to this.
+    Ok(cx.validate_operand(&ot).is_ok())
+}
+
+/// Implements the 'lax' (default) version of the `might_permit_raw_init` checks; see that function for
+/// details.
+fn might_permit_raw_init_lax<'tcx>(
+    this: TyAndLayout<'tcx>,
+    cx: &LayoutCx<'tcx, TyCtxt<'tcx>>,
+    init_kind: ValidityRequirement,
+) -> Result<bool, &'tcx LayoutError<'tcx>> {
+    let scalar_allows_raw_init = move |s: Scalar| -> bool {
+        match init_kind {
+            ValidityRequirement::Inhabited => {
+                bug!("ValidityRequirement::Inhabited should have been handled above")
+            }
+            ValidityRequirement::Zero => {
+                // The range must contain 0.
+                s.valid_range(cx).contains(0)
+            }
+            ValidityRequirement::UninitMitigated0x01Fill => {
+                // The range must include an 0x01-filled buffer.
+                let mut val: u128 = 0x01;
+                for _ in 1..s.size(cx).bytes() {
+                    // For sizes >1, repeat the 0x01.
+                    val = (val << 8) | 0x01;
+                }
+                s.valid_range(cx).contains(val)
+            }
+            ValidityRequirement::Uninit => {
+                bug!("ValidityRequirement::Uninit should have been handled above")
+            }
+        }
+    };
+
+    // Check the ABI.
+    let valid = match this.abi {
+        Abi::Uninhabited => false, // definitely UB
+        Abi::Scalar(s) => scalar_allows_raw_init(s),
+        Abi::ScalarPair(s1, s2) => scalar_allows_raw_init(s1) && scalar_allows_raw_init(s2),
+        Abi::Vector { element: s, count } => count == 0 || scalar_allows_raw_init(s),
+        Abi::Aggregate { .. } => true, // Fields are checked below.
+    };
+    if !valid {
+        // This is definitely not okay.
+        return Ok(false);
+    }
+
+    // Special magic check for references and boxes (i.e., special pointer types).
+    if let Some(pointee) = this.ty.builtin_deref(false) {
+        let pointee = cx.layout_of(pointee)?;
+        // We need to ensure that the LLVM attributes `aligned` and `dereferenceable(size)` are satisfied.
+        if pointee.align.abi.bytes() > 1 {
+            // 0x01-filling is not aligned.
+            return Ok(false);
+        }
+        if pointee.size.bytes() > 0 {
+            // A 'fake' integer pointer is not sufficiently dereferenceable.
+            return Ok(false);
+        }
+    }
+
+    // If we have not found an error yet, we need to recursively descend into fields.
+    match &this.fields {
+        FieldsShape::Primitive | FieldsShape::Union { .. } => {}
+        FieldsShape::Array { .. } => {
+            // Arrays never have scalar layout in LLVM, so if the array is not actually
+            // accessed, there is no LLVM UB -- therefore we can skip this.
+        }
+        FieldsShape::Arbitrary { offsets, .. } => {
+            for idx in 0..offsets.len() {
+                if !might_permit_raw_init_lax(this.field(cx, idx), cx, init_kind)? {
+                    // We found a field that is unhappy with this kind of initialization.
+                    return Ok(false);
+                }
+            }
+        }
+    }
+
+    match &this.variants {
+        Variants::Single { .. } => {
+            // All fields of this single variant have already been checked above, there is nothing
+            // else to do.
+        }
+        Variants::Multiple { .. } => {
+            // We cannot tell LLVM anything about the details of this multi-variant layout, so
+            // invalid values "hidden" inside the variant cannot cause LLVM trouble.
+        }
+    }
+
+    Ok(true)
+}
diff --git a/compiler/rustc_const_eval/src/util/compare_types.rs b/compiler/rustc_const_eval/src/util/compare_types.rs
new file mode 100644
index 00000000000..3ea54146fc7
--- /dev/null
+++ b/compiler/rustc_const_eval/src/util/compare_types.rs
@@ -0,0 +1,58 @@
+//! Routines to check for relations between fully inferred types.
+//!
+//! FIXME: Move this to a more general place. The utility of this extends to
+//! other areas of the compiler as well.
+
+use rustc_infer::infer::TyCtxtInferExt;
+use rustc_middle::traits::ObligationCause;
+use rustc_middle::ty::{ParamEnv, Ty, TyCtxt, Variance};
+use rustc_trait_selection::traits::ObligationCtxt;
+
+/// Returns whether the two types are equal up to subtyping.
+///
+/// This is used in case we don't know the expected subtyping direction
+/// and still want to check whether anything is broken.
+pub fn is_equal_up_to_subtyping<'tcx>(
+    tcx: TyCtxt<'tcx>,
+    param_env: ParamEnv<'tcx>,
+    src: Ty<'tcx>,
+    dest: Ty<'tcx>,
+) -> bool {
+    // Fast path.
+    if src == dest {
+        return true;
+    }
+
+    // Check for subtyping in either direction.
+    relate_types(tcx, param_env, Variance::Covariant, src, dest)
+        || relate_types(tcx, param_env, Variance::Covariant, dest, src)
+}
+
+/// Returns whether `src` is a subtype of `dest`, i.e. `src <: dest`.
+///
+/// When validating assignments, the variance should be `Covariant`. When checking
+/// during `MirPhase` >= `MirPhase::Runtime(RuntimePhase::Initial)` variance should be `Invariant`
+/// because we want to check for type equality.
+pub fn relate_types<'tcx>(
+    tcx: TyCtxt<'tcx>,
+    param_env: ParamEnv<'tcx>,
+    variance: Variance,
+    src: Ty<'tcx>,
+    dest: Ty<'tcx>,
+) -> bool {
+    if src == dest {
+        return true;
+    }
+
+    let mut builder = tcx.infer_ctxt().ignoring_regions();
+    let infcx = builder.build();
+    let ocx = ObligationCtxt::new(&infcx);
+    let cause = ObligationCause::dummy();
+    let src = ocx.normalize(&cause, param_env, src);
+    let dest = ocx.normalize(&cause, param_env, dest);
+    match ocx.relate(&cause, param_env, variance, src, dest) {
+        Ok(()) => {}
+        Err(_) => return false,
+    };
+    ocx.select_all_or_error().is_empty()
+}
diff --git a/compiler/rustc_const_eval/src/util/mod.rs b/compiler/rustc_const_eval/src/util/mod.rs
new file mode 100644
index 00000000000..66a1addfb52
--- /dev/null
+++ b/compiler/rustc_const_eval/src/util/mod.rs
@@ -0,0 +1,39 @@
+use rustc_middle::mir;
+
+mod alignment;
+pub(crate) mod caller_location;
+mod check_validity_requirement;
+mod compare_types;
+mod type_name;
+
+pub use self::alignment::{is_disaligned, is_within_packed};
+pub use self::check_validity_requirement::check_validity_requirement;
+pub use self::compare_types::{is_equal_up_to_subtyping, relate_types};
+pub use self::type_name::type_name;
+
+/// Classify whether an operator is "left-homogeneous", i.e., the LHS has the
+/// same type as the result.
+#[inline]
+pub fn binop_left_homogeneous(op: mir::BinOp) -> bool {
+    use rustc_middle::mir::BinOp::*;
+    match op {
+        Add | AddUnchecked | Sub | SubUnchecked | Mul | MulUnchecked | Div | Rem | BitXor
+        | BitAnd | BitOr | Offset | Shl | ShlUnchecked | Shr | ShrUnchecked => true,
+        AddWithOverflow | SubWithOverflow | MulWithOverflow | Eq | Ne | Lt | Le | Gt | Ge | Cmp => {
+            false
+        }
+    }
+}
+
+/// Classify whether an operator is "right-homogeneous", i.e., the RHS has the
+/// same type as the LHS.
+#[inline]
+pub fn binop_right_homogeneous(op: mir::BinOp) -> bool {
+    use rustc_middle::mir::BinOp::*;
+    match op {
+        Add | AddUnchecked | AddWithOverflow | Sub | SubUnchecked | SubWithOverflow | Mul
+        | MulUnchecked | MulWithOverflow | Div | Rem | BitXor | BitAnd | BitOr | Eq | Ne | Lt
+        | Le | Gt | Ge | Cmp => true,
+        Offset | Shl | ShlUnchecked | Shr | ShrUnchecked => false,
+    }
+}
diff --git a/compiler/rustc_const_eval/src/util/type_name.rs b/compiler/rustc_const_eval/src/util/type_name.rs
new file mode 100644
index 00000000000..01e517250f7
--- /dev/null
+++ b/compiler/rustc_const_eval/src/util/type_name.rs
@@ -0,0 +1,186 @@
+use rustc_data_structures::intern::Interned;
+use rustc_hir::def_id::CrateNum;
+use rustc_hir::definitions::DisambiguatedDefPathData;
+use rustc_middle::bug;
+use rustc_middle::ty::{
+    self,
+    print::{PrettyPrinter, Print, PrintError, Printer},
+    GenericArg, GenericArgKind, Ty, TyCtxt,
+};
+use std::fmt::Write;
+
+struct AbsolutePathPrinter<'tcx> {
+    tcx: TyCtxt<'tcx>,
+    path: String,
+}
+
+impl<'tcx> Printer<'tcx> for AbsolutePathPrinter<'tcx> {
+    fn tcx(&self) -> TyCtxt<'tcx> {
+        self.tcx
+    }
+
+    fn print_region(&mut self, _region: ty::Region<'_>) -> Result<(), PrintError> {
+        Ok(())
+    }
+
+    fn print_type(&mut self, ty: Ty<'tcx>) -> Result<(), PrintError> {
+        match *ty.kind() {
+            // Types without identity.
+            ty::Bool
+            | ty::Char
+            | ty::Int(_)
+            | ty::Uint(_)
+            | ty::Float(_)
+            | ty::Str
+            | ty::Pat(_, _)
+            | ty::Array(_, _)
+            | ty::Slice(_)
+            | ty::RawPtr(_, _)
+            | ty::Ref(_, _, _)
+            | ty::FnPtr(_)
+            | ty::Never
+            | ty::Tuple(_)
+            | ty::Dynamic(_, _, _) => self.pretty_print_type(ty),
+
+            // Placeholders (all printed as `_` to uniformize them).
+            ty::Param(_) | ty::Bound(..) | ty::Placeholder(_) | ty::Infer(_) | ty::Error(_) => {
+                write!(self, "_")?;
+                Ok(())
+            }
+
+            // Types with identity (print the module path).
+            ty::Adt(ty::AdtDef(Interned(&ty::AdtDefData { did: def_id, .. }, _)), args)
+            | ty::FnDef(def_id, args)
+            | ty::Alias(ty::Projection | ty::Opaque, ty::AliasTy { def_id, args, .. })
+            | ty::Closure(def_id, args)
+            | ty::CoroutineClosure(def_id, args)
+            | ty::Coroutine(def_id, args) => self.print_def_path(def_id, args),
+            ty::Foreign(def_id) => self.print_def_path(def_id, &[]),
+
+            ty::Alias(ty::Weak, _) => bug!("type_name: unexpected weak projection"),
+            ty::Alias(ty::Inherent, _) => bug!("type_name: unexpected inherent projection"),
+            ty::CoroutineWitness(..) => bug!("type_name: unexpected `CoroutineWitness`"),
+        }
+    }
+
+    fn print_const(&mut self, ct: ty::Const<'tcx>) -> Result<(), PrintError> {
+        self.pretty_print_const(ct, false)
+    }
+
+    fn print_dyn_existential(
+        &mut self,
+        predicates: &'tcx ty::List<ty::PolyExistentialPredicate<'tcx>>,
+    ) -> Result<(), PrintError> {
+        self.pretty_print_dyn_existential(predicates)
+    }
+
+    fn path_crate(&mut self, cnum: CrateNum) -> Result<(), PrintError> {
+        self.path.push_str(self.tcx.crate_name(cnum).as_str());
+        Ok(())
+    }
+
+    fn path_qualified(
+        &mut self,
+        self_ty: Ty<'tcx>,
+        trait_ref: Option<ty::TraitRef<'tcx>>,
+    ) -> Result<(), PrintError> {
+        self.pretty_path_qualified(self_ty, trait_ref)
+    }
+
+    fn path_append_impl(
+        &mut self,
+        print_prefix: impl FnOnce(&mut Self) -> Result<(), PrintError>,
+        _disambiguated_data: &DisambiguatedDefPathData,
+        self_ty: Ty<'tcx>,
+        trait_ref: Option<ty::TraitRef<'tcx>>,
+    ) -> Result<(), PrintError> {
+        self.pretty_path_append_impl(
+            |cx| {
+                print_prefix(cx)?;
+
+                cx.path.push_str("::");
+
+                Ok(())
+            },
+            self_ty,
+            trait_ref,
+        )
+    }
+
+    fn path_append(
+        &mut self,
+        print_prefix: impl FnOnce(&mut Self) -> Result<(), PrintError>,
+        disambiguated_data: &DisambiguatedDefPathData,
+    ) -> Result<(), PrintError> {
+        print_prefix(self)?;
+
+        write!(self.path, "::{}", disambiguated_data.data).unwrap();
+
+        Ok(())
+    }
+
+    fn path_generic_args(
+        &mut self,
+        print_prefix: impl FnOnce(&mut Self) -> Result<(), PrintError>,
+        args: &[GenericArg<'tcx>],
+    ) -> Result<(), PrintError> {
+        print_prefix(self)?;
+        let args =
+            args.iter().cloned().filter(|arg| !matches!(arg.unpack(), GenericArgKind::Lifetime(_)));
+        if args.clone().next().is_some() {
+            self.generic_delimiters(|cx| cx.comma_sep(args))
+        } else {
+            Ok(())
+        }
+    }
+}
+
+impl<'tcx> PrettyPrinter<'tcx> for AbsolutePathPrinter<'tcx> {
+    fn should_print_region(&self, _region: ty::Region<'_>) -> bool {
+        false
+    }
+    fn comma_sep<T>(&mut self, mut elems: impl Iterator<Item = T>) -> Result<(), PrintError>
+    where
+        T: Print<'tcx, Self>,
+    {
+        if let Some(first) = elems.next() {
+            first.print(self)?;
+            for elem in elems {
+                self.path.push_str(", ");
+                elem.print(self)?;
+            }
+        }
+        Ok(())
+    }
+
+    fn generic_delimiters(
+        &mut self,
+        f: impl FnOnce(&mut Self) -> Result<(), PrintError>,
+    ) -> Result<(), PrintError> {
+        write!(self, "<")?;
+
+        f(self)?;
+
+        write!(self, ">")?;
+
+        Ok(())
+    }
+
+    fn should_print_verbose(&self) -> bool {
+        // `std::any::type_name` should never print verbose type names
+        false
+    }
+}
+
+impl Write for AbsolutePathPrinter<'_> {
+    fn write_str(&mut self, s: &str) -> std::fmt::Result {
+        self.path.push_str(s);
+        Ok(())
+    }
+}
+
+pub fn type_name<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> String {
+    let mut printer = AbsolutePathPrinter { tcx, path: String::new() };
+    printer.print_type(ty).unwrap();
+    printer.path
+}