diff --git a/src/librustdoc/clean/inline.rs b/src/librustdoc/clean/inline.rs
index e56a715e85780..6bbb82897f306 100644
--- a/src/librustdoc/clean/inline.rs
+++ b/src/librustdoc/clean/inline.rs
@@ -596,6 +596,8 @@ fn build_module_items(
 }
 
 pub(crate) fn print_inlined_const(tcx: TyCtxt<'_>, did: DefId) -> String {
+    // FIXME: Both branches rely on HIR pretty-printing which
+    //        leaks private and doc(hidden) struct fields.
     if let Some(did) = did.as_local() {
         let hir_id = tcx.hir().local_def_id_to_hir_id(did);
         rustc_hir_pretty::id_to_string(&tcx.hir(), hir_id)
diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs
index 1a4786c9b0664..ef7fbe871fc08 100644
--- a/src/librustdoc/clean/types.rs
+++ b/src/librustdoc/clean/types.rs
@@ -37,11 +37,11 @@ use crate::clean::cfg::Cfg;
 use crate::clean::clean_visibility;
 use crate::clean::external_path;
 use crate::clean::inline::{self, print_inlined_const};
-use crate::clean::utils::{is_literal_expr, print_const_expr, print_evaluated_const};
+use crate::clean::utils::is_literal_expr;
 use crate::core::DocContext;
 use crate::formats::cache::Cache;
 use crate::formats::item_type::ItemType;
-use crate::html::render::Context;
+use crate::html::render::{constant::Renderer as ConstantRenderer, Context};
 use crate::passes::collect_intra_doc_links::UrlFragment;
 
 pub(crate) use self::FnRetTy::*;
@@ -2292,8 +2292,8 @@ impl Constant {
         self.kind.expr(tcx)
     }
 
-    pub(crate) fn value(&self, tcx: TyCtxt<'_>) -> Option<String> {
-        self.kind.value(tcx)
+    pub(crate) fn eval_and_render(&self, renderer: &ConstantRenderer<'_, '_>) -> Option<String> {
+        self.kind.eval_and_render(renderer)
     }
 
     pub(crate) fn is_literal(&self, tcx: TyCtxt<'_>) -> bool {
@@ -2307,16 +2307,16 @@ impl ConstantKind {
             ConstantKind::TyConst { ref expr } => expr.clone(),
             ConstantKind::Extern { def_id } => print_inlined_const(tcx, def_id),
             ConstantKind::Local { body, .. } | ConstantKind::Anonymous { body } => {
-                print_const_expr(tcx, body)
+                super::utils::print_const_expr(tcx, body)
             }
         }
     }
 
-    pub(crate) fn value(&self, tcx: TyCtxt<'_>) -> Option<String> {
+    pub(crate) fn eval_and_render(&self, renderer: &ConstantRenderer<'_, '_>) -> Option<String> {
         match *self {
             ConstantKind::TyConst { .. } | ConstantKind::Anonymous { .. } => None,
             ConstantKind::Extern { def_id } | ConstantKind::Local { def_id, .. } => {
-                print_evaluated_const(tcx, def_id)
+                crate::html::render::eval_and_render_const(def_id, renderer)
             }
         }
     }
diff --git a/src/librustdoc/clean/utils.rs b/src/librustdoc/clean/utils.rs
index 718cbbd2b8374..b3f3f56e226d0 100644
--- a/src/librustdoc/clean/utils.rs
+++ b/src/librustdoc/clean/utils.rs
@@ -16,17 +16,12 @@ use rustc_data_structures::thin_vec::ThinVec;
 use rustc_hir as hir;
 use rustc_hir::def::{DefKind, Res};
 use rustc_hir::def_id::{DefId, LOCAL_CRATE};
-use rustc_middle::mir;
-use rustc_middle::mir::interpret::ConstValue;
 use rustc_middle::ty::subst::{GenericArgKind, SubstsRef};
 use rustc_middle::ty::{self, DefIdTree, TyCtxt};
 use rustc_span::symbol::{kw, sym, Symbol};
 use std::fmt::Write as _;
 use std::mem;
 
-#[cfg(test)]
-mod tests;
-
 pub(crate) fn krate(cx: &mut DocContext<'_>) -> Crate {
     let module = crate::visit_ast::RustdocVisitor::new(cx).visit();
 
@@ -238,6 +233,8 @@ pub(crate) fn print_const(cx: &DocContext<'_>, n: ty::Const<'_>) -> String {
             let mut s = if let Some(def) = def.as_local() {
                 print_const_expr(cx.tcx, cx.tcx.hir().body_owned_by(def.did))
             } else {
+                // FIXME: This relies on the HIR pretty-printer which leaks private and
+                //        doc(hidden) struct fields.
                 inline::print_inlined_const(cx.tcx, def.did)
             };
             if let Some(promoted) = promoted {
@@ -261,69 +258,6 @@ pub(crate) fn print_const(cx: &DocContext<'_>, n: ty::Const<'_>) -> String {
     }
 }
 
-pub(crate) fn print_evaluated_const(tcx: TyCtxt<'_>, def_id: DefId) -> Option<String> {
-    tcx.const_eval_poly(def_id).ok().and_then(|val| {
-        let ty = tcx.type_of(def_id);
-        match (val, ty.kind()) {
-            (_, &ty::Ref(..)) => None,
-            (ConstValue::Scalar(_), &ty::Adt(_, _)) => None,
-            (ConstValue::Scalar(_), _) => {
-                let const_ = mir::ConstantKind::from_value(val, ty);
-                Some(print_const_with_custom_print_scalar(tcx, const_))
-            }
-            _ => None,
-        }
-    })
-}
-
-fn format_integer_with_underscore_sep(num: &str) -> String {
-    let num_chars: Vec<_> = num.chars().collect();
-    let mut num_start_index = if num_chars.get(0) == Some(&'-') { 1 } else { 0 };
-    let chunk_size = match num[num_start_index..].as_bytes() {
-        [b'0', b'b' | b'x', ..] => {
-            num_start_index += 2;
-            4
-        }
-        [b'0', b'o', ..] => {
-            num_start_index += 2;
-            let remaining_chars = num_chars.len() - num_start_index;
-            if remaining_chars <= 6 {
-                // don't add underscores to Unix permissions like 0755 or 100755
-                return num.to_string();
-            }
-            3
-        }
-        _ => 3,
-    };
-
-    num_chars[..num_start_index]
-        .iter()
-        .chain(num_chars[num_start_index..].rchunks(chunk_size).rev().intersperse(&['_']).flatten())
-        .collect()
-}
-
-fn print_const_with_custom_print_scalar(tcx: TyCtxt<'_>, ct: mir::ConstantKind<'_>) -> String {
-    // Use a slightly different format for integer types which always shows the actual value.
-    // For all other types, fallback to the original `pretty_print_const`.
-    match (ct, ct.ty().kind()) {
-        (mir::ConstantKind::Val(ConstValue::Scalar(int), _), ty::Uint(ui)) => {
-            format!("{}{}", format_integer_with_underscore_sep(&int.to_string()), ui.name_str())
-        }
-        (mir::ConstantKind::Val(ConstValue::Scalar(int), _), ty::Int(i)) => {
-            let ty = tcx.lift(ct.ty()).unwrap();
-            let size = tcx.layout_of(ty::ParamEnv::empty().and(ty)).unwrap().size;
-            let data = int.assert_bits(size);
-            let sign_extended_data = size.sign_extend(data) as i128;
-            format!(
-                "{}{}",
-                format_integer_with_underscore_sep(&sign_extended_data.to_string()),
-                i.name_str()
-            )
-        }
-        _ => ct.to_string(),
-    }
-}
-
 pub(crate) fn is_literal_expr(tcx: TyCtxt<'_>, hir_id: hir::HirId) -> bool {
     if let hir::Node::Expr(expr) = tcx.hir().get(hir_id) {
         if let hir::ExprKind::Lit(_) = &expr.kind {
diff --git a/src/librustdoc/clean/utils/tests.rs b/src/librustdoc/clean/utils/tests.rs
deleted file mode 100644
index ebf4b49548394..0000000000000
--- a/src/librustdoc/clean/utils/tests.rs
+++ /dev/null
@@ -1,41 +0,0 @@
-use super::*;
-
-#[test]
-fn int_format_decimal() {
-    assert_eq!(format_integer_with_underscore_sep("12345678"), "12_345_678");
-    assert_eq!(format_integer_with_underscore_sep("123"), "123");
-    assert_eq!(format_integer_with_underscore_sep("123459"), "123_459");
-    assert_eq!(format_integer_with_underscore_sep("-12345678"), "-12_345_678");
-    assert_eq!(format_integer_with_underscore_sep("-123"), "-123");
-    assert_eq!(format_integer_with_underscore_sep("-123459"), "-123_459");
-}
-
-#[test]
-fn int_format_hex() {
-    assert_eq!(format_integer_with_underscore_sep("0xab3"), "0xab3");
-    assert_eq!(format_integer_with_underscore_sep("0xa2345b"), "0xa2_345b");
-    assert_eq!(format_integer_with_underscore_sep("0xa2e6345b"), "0xa2e6_345b");
-    assert_eq!(format_integer_with_underscore_sep("-0xab3"), "-0xab3");
-    assert_eq!(format_integer_with_underscore_sep("-0xa2345b"), "-0xa2_345b");
-    assert_eq!(format_integer_with_underscore_sep("-0xa2e6345b"), "-0xa2e6_345b");
-}
-
-#[test]
-fn int_format_binary() {
-    assert_eq!(format_integer_with_underscore_sep("0o12345671"), "0o12_345_671");
-    assert_eq!(format_integer_with_underscore_sep("0o123"), "0o123");
-    assert_eq!(format_integer_with_underscore_sep("0o123451"), "0o123451");
-    assert_eq!(format_integer_with_underscore_sep("-0o12345671"), "-0o12_345_671");
-    assert_eq!(format_integer_with_underscore_sep("-0o123"), "-0o123");
-    assert_eq!(format_integer_with_underscore_sep("-0o123451"), "-0o123451");
-}
-
-#[test]
-fn int_format_octal() {
-    assert_eq!(format_integer_with_underscore_sep("0b101"), "0b101");
-    assert_eq!(format_integer_with_underscore_sep("0b101101011"), "0b1_0110_1011");
-    assert_eq!(format_integer_with_underscore_sep("0b01101011"), "0b0110_1011");
-    assert_eq!(format_integer_with_underscore_sep("-0b101"), "-0b101");
-    assert_eq!(format_integer_with_underscore_sep("-0b101101011"), "-0b1_0110_1011");
-    assert_eq!(format_integer_with_underscore_sep("-0b01101011"), "-0b0110_1011");
-}
diff --git a/src/librustdoc/core.rs b/src/librustdoc/core.rs
index c48b25aea4a37..3e9b49ae245c0 100644
--- a/src/librustdoc/core.rs
+++ b/src/librustdoc/core.rs
@@ -379,7 +379,11 @@ pub(crate) fn run_global_ctxt(
         impl_trait_bounds: Default::default(),
         generated_synthetics: Default::default(),
         auto_traits,
-        cache: Cache::new(access_levels, render_options.document_private),
+        cache: Cache::new(
+            access_levels,
+            render_options.document_private,
+            render_options.document_hidden,
+        ),
         inlined: FxHashSet::default(),
         output_format,
         render_options,
diff --git a/src/librustdoc/formats/cache.rs b/src/librustdoc/formats/cache.rs
index 2b2691e53bbcc..7670dd3f5e67d 100644
--- a/src/librustdoc/formats/cache.rs
+++ b/src/librustdoc/formats/cache.rs
@@ -87,6 +87,10 @@ pub(crate) struct Cache {
     /// This is stored in `Cache` so it doesn't need to be passed through all rustdoc functions.
     pub(crate) document_private: bool,
 
+    /// Whether to document `#[doc(hidden)]` items.
+    /// This is stored in `Cache` so it doesn't need to be passed through all rustdoc functions.
+    pub(crate) document_hidden: bool,
+
     /// Crates marked with [`#[doc(masked)]`][doc_masked].
     ///
     /// [doc_masked]: https://doc.rust-lang.org/nightly/unstable-book/language-features/doc-masked.html
@@ -132,8 +136,12 @@ struct CacheBuilder<'a, 'tcx> {
 }
 
 impl Cache {
-    pub(crate) fn new(access_levels: AccessLevels<DefId>, document_private: bool) -> Self {
-        Cache { access_levels, document_private, ..Cache::default() }
+    pub(crate) fn new(
+        access_levels: AccessLevels<DefId>,
+        document_private: bool,
+        document_hidden: bool,
+    ) -> Self {
+        Cache { access_levels, document_private, document_hidden, ..Cache::default() }
     }
 
     /// Populates the `Cache` with more data. The returned `Crate` will be missing some data that was
diff --git a/src/librustdoc/html/format.rs b/src/librustdoc/html/format.rs
index 3dee4d1acc819..24202ef956aca 100644
--- a/src/librustdoc/html/format.rs
+++ b/src/librustdoc/html/format.rs
@@ -654,6 +654,13 @@ pub(crate) fn href_with_root_path(
             // documented on their parent's page
             tcx.parent(did)
         }
+        DefKind::Field => {
+            let parent_id = tcx.parent(did);
+            match tcx.def_kind(parent_id) {
+                DefKind::Variant => tcx.parent(parent_id),
+                _ => parent_id,
+            }
+        }
         _ => did,
     };
     let cache = cx.cache();
diff --git a/src/librustdoc/html/render/constant.rs b/src/librustdoc/html/render/constant.rs
new file mode 100644
index 0000000000000..e6b6f2ab70557
--- /dev/null
+++ b/src/librustdoc/html/render/constant.rs
@@ -0,0 +1,486 @@
+use crate::clean::{inline::print_inlined_const, utils::print_const_expr};
+use crate::formats::{cache::Cache, item_type::ItemType};
+use crate::html::{escape::Escape, format::Buffer};
+use rustc_hir::def::{CtorKind, DefKind};
+use rustc_hir::def_id::DefId;
+use rustc_middle::mir::interpret::{AllocRange, ConstValue, Scalar};
+use rustc_middle::mir::ConstantKind;
+use rustc_middle::ty::{self, util::is_doc_hidden};
+use rustc_middle::ty::{Const, ConstInt, DefIdTree, FieldDef, ParamConst, ScalarInt};
+use rustc_middle::ty::{Ty, TyCtxt, TypeVisitable, Visibility};
+use rustc_span::sym;
+use rustc_target::abi::Size;
+use std::fmt::Write;
+
+/// Try to evaluate the given constant expression and render it as HTML code or as plain text.
+///
+/// `None` is returned if the expression is “too generic”.
+///
+/// This textual representation may not actually be lossless or even valid
+/// Rust syntax since
+///
+/// * overly long arrays & strings and deeply-nested subexpressions are replaced
+///   with ellipses
+/// * private and `#[doc(hidden)]` struct fields are omitted
+/// * `..` is added to (tuple) struct literals if any fields are omitted
+///   or if the type is `#[non_exhaustive]`
+/// * `_` is used in place of “unsupported” expressions (e.g. pointers)
+pub(crate) fn eval_and_render_const(def_id: DefId, renderer: &Renderer<'_, '_>) -> Option<String> {
+    let value = renderer.tcx().const_eval_poly(def_id).ok()?;
+    let mut buffer = renderer.buffer();
+    render_const_value(&mut buffer, value, renderer.tcx().type_of(def_id), renderer, 0);
+    Some(buffer.into_inner())
+}
+
+pub(crate) enum Renderer<'cx, 'tcx> {
+    PlainText(crate::json::Context<'tcx>),
+    Html(&'cx super::Context<'tcx>),
+}
+
+impl<'cx, 'tcx> Renderer<'cx, 'tcx> {
+    fn buffer(&self) -> Buffer {
+        match self {
+            Self::PlainText(_) => Buffer::new(),
+            Self::Html(_) => Buffer::html(),
+        }
+    }
+
+    fn tcx(&self) -> TyCtxt<'tcx> {
+        match self {
+            Self::PlainText(cx) => cx.tcx,
+            Self::Html(cx) => cx.tcx(),
+        }
+    }
+
+    fn cache(&self) -> &Cache {
+        match self {
+            Self::PlainText(cx) => &cx.cache,
+            Self::Html(cx) => &cx.shared.cache,
+        }
+    }
+}
+
+const DEPTH_LIMIT: u32 = 2;
+const STRING_LENGTH_LIMIT: usize = 80;
+const ARRAY_LENGTH_LIMIT: usize = 12;
+
+const DEPTH_ELLIPSIS: &str = "…";
+const LENGTH_ELLIPSIS: &str = "………";
+
+fn render_constant_kind<'tcx>(
+    buffer: &mut Buffer,
+    ct: ConstantKind<'tcx>,
+    renderer: &Renderer<'_, 'tcx>,
+    depth: u32,
+) {
+    if depth > DEPTH_LIMIT {
+        render_ellipsis(buffer, DEPTH_ELLIPSIS);
+        return;
+    }
+
+    match ct {
+        ConstantKind::Ty(ct) => render_const(buffer, ct, renderer),
+        ConstantKind::Val(ct, ty) => render_const_value(buffer, ct, ty, renderer, depth),
+    }
+}
+
+fn render_const<'tcx>(buffer: &mut Buffer, ct: Const<'tcx>, renderer: &Renderer<'_, 'tcx>) {
+    let tcx = renderer.tcx();
+
+    match ct.kind() {
+        ty::ConstKind::Unevaluated(ty::Unevaluated { def, promoted: Some(promoted), .. }) => {
+            render_path(buffer, def.did, renderer);
+            write!(buffer, "::{:?}", promoted);
+        }
+        ty::ConstKind::Unevaluated(ty::Unevaluated { def, promoted: None, .. }) => {
+            match tcx.def_kind(def.did) {
+                DefKind::Static(..) | DefKind::Const | DefKind::AssocConst => {
+                    render_path(buffer, def.did, renderer)
+                }
+                _ => {
+                    let expr = match def.as_local() {
+                        Some(def) => print_const_expr(tcx, tcx.hir().body_owned_by(def.did)),
+                        None => print_inlined_const(tcx, def.did),
+                    };
+
+                    if buffer.is_for_html() {
+                        write!(buffer, "{}", Escape(&expr));
+                    } else {
+                        write!(buffer, "{expr}");
+                    }
+                }
+            }
+        }
+        ty::ConstKind::Param(ParamConst { name, .. }) => write!(buffer, "{name}"),
+        ty::ConstKind::Value(value) => render_valtree(buffer, value, ct.ty()),
+        ty::ConstKind::Infer(_)
+        | ty::ConstKind::Bound(..)
+        | ty::ConstKind::Placeholder(_)
+        | ty::ConstKind::Error(_) => write!(buffer, "_"),
+    }
+}
+
+fn render_const_value<'tcx>(
+    buffer: &mut Buffer,
+    ct: ConstValue<'tcx>,
+    ty: Ty<'tcx>,
+    renderer: &Renderer<'_, 'tcx>,
+    depth: u32,
+) {
+    let tcx = renderer.tcx();
+    // FIXME: The code inside `rustc_middle::mir::pretty_print_const` does this.
+    //        Do we need to do this, too? Why (not)?
+    // let ct = tcx.lift(ct).unwrap();
+    // let ty = tcx.lift(ty).unwrap();
+    let u8_type = tcx.types.u8;
+
+    match (ct, ty.kind()) {
+        (ConstValue::Slice { data, start, end }, ty::Ref(_, inner, _)) => {
+            match inner.kind() {
+                ty::Slice(t) if *t == u8_type => {
+                    // The `inspect` here is okay since we checked the bounds, and there are
+                    // no relocations (we have an active slice reference here). We don't use
+                    // this result to affect interpreter execution.
+                    let byte_str =
+                        data.inner().inspect_with_uninit_and_ptr_outside_interpreter(start..end);
+                    render_byte_str(buffer, byte_str);
+                }
+                ty::Str => {
+                    // The `inspect` here is okay since we checked the bounds, and there are no
+                    // relocations (we have an active `str` reference here). We don't use this
+                    // result to affect interpreter execution.
+                    let slice =
+                        data.inner().inspect_with_uninit_and_ptr_outside_interpreter(start..end);
+
+                    // FIXME: Make the limit depend on the `depth` (inversely proportionally)
+                    if slice.len() > STRING_LENGTH_LIMIT {
+                        write!(buffer, "\"");
+                        render_ellipsis(buffer, LENGTH_ELLIPSIS);
+                        write!(buffer, "\"");
+                    } else {
+                        let slice = format!("{:?}", String::from_utf8_lossy(slice));
+
+                        if buffer.is_for_html() {
+                            write!(buffer, "{}", Escape(&slice));
+                        } else {
+                            write!(buffer, "{slice}");
+                        }
+                    }
+                }
+                // `ConstValue::Slice` is only used for `&[u8]` and `&str`.
+                _ => unreachable!(),
+            }
+        }
+        (ConstValue::ByRef { alloc, offset }, ty::Array(t, n)) if *t == u8_type => {
+            let n = n.kind().try_to_bits(tcx.data_layout.pointer_size).unwrap();
+            // cast is ok because we already checked for pointer size (32 or 64 bit) above
+            let range = AllocRange { start: offset, size: Size::from_bytes(n) };
+            let byte_str = alloc.inner().get_bytes(&tcx, range).unwrap();
+            write!(buffer, "*");
+            render_byte_str(buffer, byte_str);
+        }
+        // Aggregates.
+        //
+        // NB: the `has_param_types_or_consts` check ensures that we can use
+        // the `destructure_const` query with an empty `ty::ParamEnv` without
+        // introducing ICEs (e.g. via `layout_of`) from missing bounds.
+        // E.g. `transmute([0usize; 2]): (u8, *mut T)` needs to know `T: Sized`
+        // to be able to destructure the tuple into `(0u8, *mut T)
+        (_, ty::Array(..) | ty::Tuple(..) | ty::Adt(..)) if !ty.has_param_types_or_consts() => {
+            // FIXME: The code inside `rustc_middle::mir::pretty_print_const` does this.
+            //        Do we need to do this, too? Why (not)?
+            // let ct = tcx.lift(ct).unwrap();
+            // let ty = tcx.lift(ty).unwrap();
+            let Some(contents) = tcx.try_destructure_mir_constant(
+                ty::ParamEnv::reveal_all().and(ConstantKind::Val(ct, ty))
+            ) else {
+                return write!(buffer, "_");
+            };
+
+            let mut fields = contents.fields.iter().copied();
+
+            // FIXME: Should we try to print larger structs etc. across multiple lines?
+            match *ty.kind() {
+                ty::Array(..) => {
+                    write!(buffer, "[");
+
+                    // FIXME: Make the limit depend on the `depth` (inversely proportionally)
+                    if contents.fields.len() > ARRAY_LENGTH_LIMIT {
+                        render_ellipsis(buffer, LENGTH_ELLIPSIS);
+                    } else if let Some(first) = fields.next() {
+                        render_constant_kind(buffer, first, renderer, depth + 1);
+                        for field in fields {
+                            buffer.write_str(", ");
+                            render_constant_kind(buffer, field, renderer, depth + 1);
+                        }
+                    }
+
+                    write!(buffer, "]");
+                }
+                ty::Tuple(..) => {
+                    write!(buffer, "(");
+
+                    if let Some(first) = fields.next() {
+                        render_constant_kind(buffer, first, renderer, depth + 1);
+                        for field in fields {
+                            buffer.write_str(", ");
+                            render_constant_kind(buffer, field, renderer, depth + 1);
+                        }
+                    }
+                    if contents.fields.len() == 1 {
+                        write!(buffer, ",");
+                    }
+
+                    write!(buffer, ")");
+                }
+                ty::Adt(def, _) if !def.variants().is_empty() => {
+                    let should_hide = |field: &FieldDef| {
+                        // FIXME: Should I use `cache.access_levels.is_public(did)` here instead?
+                        is_doc_hidden(tcx, field.did)
+                            && !(renderer.cache().document_hidden && field.did.is_local())
+                            || field.vis != Visibility::Public
+                                && !(renderer.cache().document_private && field.did.is_local())
+                    };
+
+                    let is_non_exhaustive = tcx.has_attr(def.did(), sym::non_exhaustive);
+
+                    let variant_idx =
+                        contents.variant.expect("destructed const of adt without variant idx");
+                    let variant_def = &def.variant(variant_idx);
+                    render_path(buffer, variant_def.def_id, renderer);
+
+                    match variant_def.ctor_kind {
+                        CtorKind::Const => {
+                            if is_non_exhaustive {
+                                write!(buffer, " {{ .. }}");
+                            }
+                        }
+                        CtorKind::Fn => {
+                            write!(buffer, "(");
+
+                            let mut first = true;
+                            for (field_def, field) in std::iter::zip(&variant_def.fields, fields) {
+                                if !first {
+                                    write!(buffer, ", ");
+                                }
+                                first = false;
+
+                                if should_hide(field_def) {
+                                    write!(buffer, "_");
+                                    continue;
+                                }
+
+                                render_constant_kind(buffer, field, renderer, depth + 1);
+                            }
+
+                            if is_non_exhaustive {
+                                if !first {
+                                    write!(buffer, ", ");
+                                }
+
+                                // Using `..` (borrowed from patterns) to mark non-exhaustive tuple
+                                // structs in our pseudo-Rust expression syntax is sadly not without
+                                // caveats since in real-Rust expressions, it denotes full ranges
+                                // (`std::ops::RangeFull`) which may appear as arguments to tuple
+                                // struct constructors (albeit incredibly uncommonly) and thus
+                                // it may lead to confusion.
+                                // NB: Actually we literally render full ranges as `RangeFull`.
+                                //     Still, that does not help that much.
+                                // If this issue turns out to be significant, we can change the
+                                // output to e.g. `_ @ ..` (borrowed from slice patterns) which is
+                                // not a valid Rust expression at the time of this writing but
+                                // it's quite cryptic.
+                                write!(buffer, "..");
+                            }
+
+                            write!(buffer, ")");
+                        }
+                        CtorKind::Fictive => {
+                            write!(buffer, " {{ ");
+
+                            let mut first = true;
+                            let mut did_hide_fields = false;
+                            for (field_def, field) in std::iter::zip(&variant_def.fields, fields) {
+                                if should_hide(field_def) {
+                                    did_hide_fields = true;
+                                    continue;
+                                }
+
+                                if !first {
+                                    write!(buffer, ", ");
+                                }
+                                first = false;
+
+                                render_field_name(buffer, field_def, renderer);
+                                write!(buffer, ": ");
+                                render_constant_kind(buffer, field, renderer, depth + 1);
+                            }
+
+                            if did_hide_fields || is_non_exhaustive {
+                                if !first {
+                                    write!(buffer, ", ");
+                                }
+                                write!(buffer, "..");
+                            }
+
+                            write!(buffer, " }}");
+                        }
+                    }
+                }
+                _ => unreachable!(),
+            }
+        }
+        (ConstValue::Scalar(Scalar::Int(int)), _) => render_const_scalar_int(buffer, int, ty),
+        // FIXME: Support `&[_]`: `(ByRef { .. }, ty::Ref(_, ty, Not)) if let ty::Slice(_) = ty`
+        //        Blocker: try_destructure_mir_constant does not support slices.
+        _ => write!(buffer, "_"),
+    }
+}
+
+fn render_valtree<'tcx>(buffer: &mut Buffer, _valtree: ty::ValTree<'tcx>, _ty: Ty<'tcx>) {
+    // FIXME: If this case is actually reachable, adopt the code from
+    //        rustc_middle::ty::print::pretty::PrettyPrinter::pretty_print_const_valtree
+    write!(buffer, "_");
+}
+
+fn render_path<'tcx>(buffer: &mut Buffer, def_id: DefId, renderer: &Renderer<'_, 'tcx>) {
+    let tcx = renderer.tcx();
+    let name = tcx.item_name(def_id);
+
+    match renderer {
+        Renderer::PlainText(_) => write!(buffer, "{name}"),
+        Renderer::Html(cx) => {
+            if let Ok((mut url, item_type, path)) = super::href(def_id, cx) {
+                let mut needs_fragment = true;
+                let item_type = match tcx.def_kind(def_id) {
+                    DefKind::AssocFn => {
+                        if tcx.associated_item(def_id).defaultness(tcx).has_value() {
+                            ItemType::Method
+                        } else {
+                            ItemType::TyMethod
+                        }
+                    }
+                    DefKind::AssocTy => ItemType::AssocType,
+                    DefKind::AssocConst => ItemType::AssocConst,
+                    DefKind::Variant => ItemType::Variant,
+                    _ => {
+                        needs_fragment = false;
+                        item_type
+                    }
+                };
+
+                let mut path = super::join_with_double_colon(&path);
+
+                if needs_fragment {
+                    write!(url, "#{item_type}.{name}").unwrap();
+                    write!(path, "::{name}").unwrap();
+                }
+
+                write!(
+                    buffer,
+                    r#"<a class="{item_type}" href="{url}" title="{item_type} {path}">{name}</a>"#,
+                );
+            } else {
+                write!(buffer, "{name}");
+            }
+        }
+    }
+}
+
+fn render_byte_str(buffer: &mut Buffer, byte_str: &[u8]) {
+    buffer.write_str("b\"");
+
+    // FIXME: Make the limit depend on the `depth` (inversely proportionally)
+    if byte_str.len() > STRING_LENGTH_LIMIT {
+        render_ellipsis(buffer, LENGTH_ELLIPSIS);
+    } else {
+        for &char in byte_str {
+            for char in std::ascii::escape_default(char) {
+                let char = char::from(char).to_string();
+
+                if buffer.is_for_html() {
+                    write!(buffer, "{}", Escape(&char));
+                } else {
+                    write!(buffer, "{char}");
+                }
+            }
+        }
+    }
+
+    buffer.write_str("\"");
+}
+
+fn render_const_scalar_int<'tcx>(buffer: &mut Buffer, int: ScalarInt, ty: Ty<'tcx>) {
+    extern crate rustc_apfloat;
+    use rustc_apfloat::ieee::{Double, Single};
+
+    match ty.kind() {
+        ty::Bool if int == ScalarInt::FALSE => write!(buffer, "false"),
+        ty::Bool if int == ScalarInt::TRUE => write!(buffer, "true"),
+
+        ty::Float(ty::FloatTy::F32) => {
+            write!(buffer, "{}", Single::try_from(int).unwrap());
+        }
+        ty::Float(ty::FloatTy::F64) => {
+            write!(buffer, "{}", Double::try_from(int).unwrap());
+        }
+
+        ty::Uint(_) | ty::Int(_) => {
+            let int =
+                ConstInt::new(int, matches!(ty.kind(), ty::Int(_)), ty.is_ptr_sized_integral());
+            // FIXME: We probably shouldn't use the *Debug* impl for *user-facing output*.
+            //        However, it looks really nice and its implementation is non-trivial.
+            //        Should we modify rustc_middle and make it a *Display* impl?
+            write!(buffer, "{int:?}");
+        }
+        ty::Char if char::try_from(int).is_ok() => {
+            // FIXME: We probably shouldn't use the *Debug* impl here (see fixme above).
+            write!(buffer, "{:?}", char::try_from(int).unwrap());
+        }
+        _ => write!(buffer, "_"),
+    }
+}
+
+fn render_field_name(buffer: &mut Buffer, field_def: &FieldDef, renderer: &Renderer<'_, '_>) {
+    let tcx = renderer.tcx();
+
+    match renderer {
+        Renderer::PlainText(_) => write!(buffer, "{}", field_def.name),
+        Renderer::Html(cx) => match super::href(field_def.did, cx) {
+            Ok((mut url, ..)) => {
+                write!(url, "#").unwrap();
+                let parent_id = tcx.parent(field_def.did);
+                if tcx.def_kind(parent_id) == DefKind::Variant {
+                    write!(url, "{}.{}.field", ItemType::Variant, tcx.item_name(parent_id))
+                } else {
+                    write!(url, "{}", ItemType::StructField)
+                }
+                .unwrap();
+
+                write!(url, ".{}", field_def.name).unwrap();
+
+                write!(
+                    buffer,
+                    r#"<a class="{}" href="{}" title="field {}">{}</a>"#,
+                    ItemType::StructField,
+                    url,
+                    field_def.name,
+                    field_def.name,
+                );
+            }
+            Err(_) => write!(buffer, "{}", field_def.name),
+        },
+    }
+}
+
+fn render_ellipsis(buffer: &mut Buffer, ellipsis: &str) {
+    if buffer.is_for_html() {
+        write!(buffer, r#"<span class="ellipsis">"#);
+    }
+
+    write!(buffer, "{ellipsis}");
+
+    if buffer.is_for_html() {
+        write!(buffer, "</span>");
+    }
+}
diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs
index 5ed5299e09bc0..58b73a61cf470 100644
--- a/src/librustdoc/html/render/mod.rs
+++ b/src/librustdoc/html/render/mod.rs
@@ -28,11 +28,13 @@ pub(crate) mod search_index;
 #[cfg(test)]
 mod tests;
 
+pub(crate) mod constant;
 mod context;
 mod print_item;
 mod span_map;
 mod write_shared;
 
+pub(crate) use self::constant::eval_and_render_const;
 pub(crate) use self::context::*;
 pub(crate) use self::span_map::{collect_spans_and_sources, LinkFromSrc};
 
@@ -715,14 +717,13 @@ fn assoc_const(
         ty = ty.print(cx),
     );
     if let Some(default) = default {
-        write!(w, " = ");
-
-        // FIXME: `.value()` uses `clean::utils::format_integer_with_underscore_sep` under the
-        //        hood which adds noisy underscores and a type suffix to number literals.
-        //        This hurts readability in this context especially when more complex expressions
-        //        are involved and it doesn't add much of value.
-        //        Find a way to print constants here without all that jazz.
-        write!(w, "{}", Escape(&default.value(cx.tcx()).unwrap_or_else(|| default.expr(cx.tcx()))));
+        write!(
+            w,
+            " = {}",
+            default
+                .eval_and_render(&constant::Renderer::Html(cx))
+                .unwrap_or_else(|| default.expr(cx.tcx())),
+        );
     }
 }
 
diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs
index 6d0a825fec866..7881361a03b26 100644
--- a/src/librustdoc/html/render/print_item.rs
+++ b/src/librustdoc/html/render/print_item.rs
@@ -1377,36 +1377,13 @@ fn item_constant(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, c: &cle
                 typ = c.type_.print(cx),
             );
 
-            // FIXME: The code below now prints
-            //            ` = _; // 100i32`
-            //        if the expression is
-            //            `50 + 50`
-            //        which looks just wrong.
-            //        Should we print
-            //            ` = 100i32;`
-            //        instead?
-
-            let value = c.value(cx.tcx());
-            let is_literal = c.is_literal(cx.tcx());
-            let expr = c.expr(cx.tcx());
-            if value.is_some() || is_literal {
-                write!(w, " = {expr};", expr = Escape(&expr));
-            } else {
-                w.write_str(";");
+            // FIXME: (Maybe) preserve hexadecimal literals like on master.
+            // FIXME: (Maybe) add numeric underscores like on master.
+            if let Some(value) = c.eval_and_render(&super::constant::Renderer::Html(cx)) {
+                write!(w, " = {}", value);
             }
 
-            if !is_literal {
-                if let Some(value) = &value {
-                    let value_lowercase = value.to_lowercase();
-                    let expr_lowercase = expr.to_lowercase();
-
-                    if value_lowercase != expr_lowercase
-                        && value_lowercase.trim_end_matches("i32") != expr_lowercase
-                    {
-                        write!(w, " // {value}", value = Escape(value));
-                    }
-                }
-            }
+            w.write_str(";");
         });
     });
 
diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css
index 710ca3ee7c7e7..3bcd9bec6c6bd 100644
--- a/src/librustdoc/html/static/css/rustdoc.css
+++ b/src/librustdoc/html/static/css/rustdoc.css
@@ -828,6 +828,12 @@ pre, .rustdoc.source .example-wrap {
 	margin-left: 0;
 }
 
+.content .ellipsis {
+	border: 1px solid var(--main-color);
+	border-radius: 4px;
+	opacity: 0.5;
+}
+
 nav.sub {
 	flex-grow: 1;
 	margin-bottom: 25px;
diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs
index 5f3793ead42ba..c66c1e760bb8e 100644
--- a/src/librustdoc/json/conversions.rs
+++ b/src/librustdoc/json/conversions.rs
@@ -6,6 +6,7 @@
 
 use std::convert::From;
 use std::fmt;
+use std::rc::Rc;
 
 use rustc_ast::ast;
 use rustc_hir::{def::CtorKind, def_id::DefId};
@@ -17,7 +18,8 @@ use rustdoc_json_types::*;
 
 use crate::clean::utils::print_const_expr;
 use crate::clean::{self, ItemId};
-use crate::formats::item_type::ItemType;
+use crate::formats::{cache::Cache, item_type::ItemType};
+use crate::html::render::constant::Renderer as ConstantRenderer;
 use crate::json::JsonRenderer;
 
 impl JsonRenderer<'_> {
@@ -49,11 +51,13 @@ impl JsonRenderer<'_> {
                     // We document non-empty stripped modules as with `Module::is_stripped` set to
                     // `true`, to prevent contained items from being orphaned for downstream users,
                     // as JSON does no inlining.
-                    clean::ModuleItem(m) if !m.items.is_empty() => from_clean_item(item, self.tcx),
+                    clean::ModuleItem(m) if !m.items.is_empty() => {
+                        from_clean_item(item, &Context::new(self.tcx, self.cache.clone()))
+                    }
                     _ => return None,
                 }
             }
-            _ => from_clean_item(item, self.tcx),
+            _ => from_clean_item(item, &Context::new(self.tcx, self.cache.clone())),
         };
         Some(Item {
             id: from_item_id_with_name(item_id, self.tcx, name),
@@ -102,30 +106,42 @@ impl JsonRenderer<'_> {
     }
 }
 
-pub(crate) trait FromWithTcx<T> {
-    fn from_tcx(f: T, tcx: TyCtxt<'_>) -> Self;
+#[derive(Clone)]
+pub(crate) struct Context<'tcx> {
+    pub(crate) tcx: TyCtxt<'tcx>,
+    pub(crate) cache: Rc<Cache>,
 }
 
-pub(crate) trait IntoWithTcx<T> {
-    fn into_tcx(self, tcx: TyCtxt<'_>) -> T;
+impl<'tcx> Context<'tcx> {
+    pub(crate) fn new(tcx: TyCtxt<'tcx>, cache: Rc<Cache>) -> Self {
+        Self { tcx, cache }
+    }
+}
+
+pub(crate) trait FromWithCx<T> {
+    fn from_cx(f: T, cx: &Context<'_>) -> Self;
 }
 
-impl<T, U> IntoWithTcx<U> for T
+pub(crate) trait IntoWithCx<T> {
+    fn into_cx(self, cx: &Context<'_>) -> T;
+}
+
+impl<T, U> IntoWithCx<U> for T
 where
-    U: FromWithTcx<T>,
+    U: FromWithCx<T>,
 {
-    fn into_tcx(self, tcx: TyCtxt<'_>) -> U {
-        U::from_tcx(self, tcx)
+    fn into_cx(self, cx: &Context<'_>) -> U {
+        U::from_cx(self, cx)
     }
 }
 
-impl<I, T, U> FromWithTcx<I> for Vec<U>
+impl<I, T, U> FromWithCx<I> for Vec<U>
 where
     I: IntoIterator<Item = T>,
-    U: FromWithTcx<T>,
+    U: FromWithCx<T>,
 {
-    fn from_tcx(f: I, tcx: TyCtxt<'_>) -> Vec<U> {
-        f.into_iter().map(|x| x.into_tcx(tcx)).collect()
+    fn from_cx(f: I, cx: &Context<'_>) -> Vec<U> {
+        f.into_iter().map(|x| x.into_cx(cx)).collect()
     }
 }
 
@@ -135,59 +151,60 @@ pub(crate) fn from_deprecation(deprecation: rustc_attr::Deprecation) -> Deprecat
     Deprecation { since: since.map(|s| s.to_string()), note: note.map(|s| s.to_string()) }
 }
 
-impl FromWithTcx<clean::GenericArgs> for GenericArgs {
-    fn from_tcx(args: clean::GenericArgs, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::GenericArgs> for GenericArgs {
+    fn from_cx(args: clean::GenericArgs, cx: &Context<'_>) -> Self {
         use clean::GenericArgs::*;
         match args {
             AngleBracketed { args, bindings } => GenericArgs::AngleBracketed {
-                args: args.into_vec().into_tcx(tcx),
-                bindings: bindings.into_tcx(tcx),
+                args: args.into_vec().into_cx(cx),
+                bindings: bindings.into_cx(cx),
             },
             Parenthesized { inputs, output } => GenericArgs::Parenthesized {
-                inputs: inputs.into_vec().into_tcx(tcx),
-                output: output.map(|a| (*a).into_tcx(tcx)),
+                inputs: inputs.into_vec().into_cx(cx),
+                output: output.map(|a| (*a).into_cx(cx)),
             },
         }
     }
 }
 
-impl FromWithTcx<clean::GenericArg> for GenericArg {
-    fn from_tcx(arg: clean::GenericArg, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::GenericArg> for GenericArg {
+    fn from_cx(arg: clean::GenericArg, cx: &Context<'_>) -> Self {
         use clean::GenericArg::*;
         match arg {
             Lifetime(l) => GenericArg::Lifetime(convert_lifetime(l)),
-            Type(t) => GenericArg::Type(t.into_tcx(tcx)),
-            Const(box c) => GenericArg::Const(c.into_tcx(tcx)),
+            Type(t) => GenericArg::Type(t.into_cx(cx)),
+            Const(box c) => GenericArg::Const(c.into_cx(cx)),
             Infer => GenericArg::Infer,
         }
     }
 }
 
-impl FromWithTcx<clean::Constant> for Constant {
-    fn from_tcx(constant: clean::Constant, tcx: TyCtxt<'_>) -> Self {
-        let expr = constant.expr(tcx);
-        let value = constant.value(tcx);
-        let is_literal = constant.is_literal(tcx);
-        Constant { type_: constant.type_.into_tcx(tcx), expr, value, is_literal }
+impl FromWithCx<clean::Constant> for Constant {
+    fn from_cx(constant: clean::Constant, cx: &Context<'_>) -> Self {
+        let expr = constant.expr(cx.tcx);
+        // FIXME: Should we “disable” depth and length limits for the JSON backend?
+        let value = constant.eval_and_render(&ConstantRenderer::PlainText(cx.clone()));
+        let is_literal = constant.is_literal(cx.tcx);
+        Constant { type_: constant.type_.into_cx(cx), expr, value, is_literal }
     }
 }
 
-impl FromWithTcx<clean::TypeBinding> for TypeBinding {
-    fn from_tcx(binding: clean::TypeBinding, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::TypeBinding> for TypeBinding {
+    fn from_cx(binding: clean::TypeBinding, cx: &Context<'_>) -> Self {
         TypeBinding {
             name: binding.assoc.name.to_string(),
-            args: binding.assoc.args.into_tcx(tcx),
-            binding: binding.kind.into_tcx(tcx),
+            args: binding.assoc.args.into_cx(cx),
+            binding: binding.kind.into_cx(cx),
         }
     }
 }
 
-impl FromWithTcx<clean::TypeBindingKind> for TypeBindingKind {
-    fn from_tcx(kind: clean::TypeBindingKind, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::TypeBindingKind> for TypeBindingKind {
+    fn from_cx(kind: clean::TypeBindingKind, cx: &Context<'_>) -> Self {
         use clean::TypeBindingKind::*;
         match kind {
-            Equality { term } => TypeBindingKind::Equality(term.into_tcx(tcx)),
-            Constraint { bounds } => TypeBindingKind::Constraint(bounds.into_tcx(tcx)),
+            Equality { term } => TypeBindingKind::Equality(term.into_cx(cx)),
+            Constraint { bounds } => TypeBindingKind::Constraint(bounds.into_cx(cx)),
         }
     }
 }
@@ -230,51 +247,55 @@ pub(crate) fn from_item_id_with_name(item_id: ItemId, tcx: TyCtxt<'_>, name: Opt
     }
 }
 
-fn from_clean_item(item: clean::Item, tcx: TyCtxt<'_>) -> ItemEnum {
+fn from_clean_item(item: clean::Item, cx: &Context<'_>) -> ItemEnum {
     use clean::ItemKind::*;
     let name = item.name;
     let is_crate = item.is_crate();
-    let header = item.fn_header(tcx);
+    let header = item.fn_header(cx.tcx);
 
     match *item.kind {
         ModuleItem(m) => {
-            ItemEnum::Module(Module { is_crate, items: ids(m.items, tcx), is_stripped: false })
-        }
-        ImportItem(i) => ItemEnum::Import(i.into_tcx(tcx)),
-        StructItem(s) => ItemEnum::Struct(s.into_tcx(tcx)),
-        UnionItem(u) => ItemEnum::Union(u.into_tcx(tcx)),
-        StructFieldItem(f) => ItemEnum::StructField(f.into_tcx(tcx)),
-        EnumItem(e) => ItemEnum::Enum(e.into_tcx(tcx)),
-        VariantItem(v) => ItemEnum::Variant(v.into_tcx(tcx)),
-        FunctionItem(f) => ItemEnum::Function(from_function(f, header.unwrap(), tcx)),
-        ForeignFunctionItem(f) => ItemEnum::Function(from_function(f, header.unwrap(), tcx)),
-        TraitItem(t) => ItemEnum::Trait(t.into_tcx(tcx)),
-        TraitAliasItem(t) => ItemEnum::TraitAlias(t.into_tcx(tcx)),
-        MethodItem(m, _) => ItemEnum::Method(from_function_method(m, true, header.unwrap(), tcx)),
-        TyMethodItem(m) => ItemEnum::Method(from_function_method(m, false, header.unwrap(), tcx)),
-        ImplItem(i) => ItemEnum::Impl((*i).into_tcx(tcx)),
-        StaticItem(s) => ItemEnum::Static(s.into_tcx(tcx)),
-        ForeignStaticItem(s) => ItemEnum::Static(s.into_tcx(tcx)),
+            ItemEnum::Module(Module { is_crate, items: ids(m.items, cx.tcx), is_stripped: false })
+        }
+        ImportItem(i) => ItemEnum::Import(i.into_cx(cx)),
+        StructItem(s) => ItemEnum::Struct(s.into_cx(cx)),
+        UnionItem(u) => ItemEnum::Union(u.into_cx(cx)),
+        StructFieldItem(f) => ItemEnum::StructField(f.into_cx(cx)),
+        EnumItem(e) => ItemEnum::Enum(e.into_cx(cx)),
+        VariantItem(v) => ItemEnum::Variant(v.into_cx(cx)),
+        FunctionItem(f) => ItemEnum::Function(from_function(f, header.unwrap(), cx)),
+        ForeignFunctionItem(f) => ItemEnum::Function(from_function(f, header.unwrap(), cx)),
+        TraitItem(t) => ItemEnum::Trait(t.into_cx(cx)),
+        TraitAliasItem(t) => ItemEnum::TraitAlias(t.into_cx(cx)),
+        MethodItem(m, _) => ItemEnum::Method(from_function_method(m, true, header.unwrap(), cx)),
+        TyMethodItem(m) => ItemEnum::Method(from_function_method(m, false, header.unwrap(), cx)),
+        ImplItem(i) => ItemEnum::Impl((*i).into_cx(cx)),
+        StaticItem(s) => ItemEnum::Static(s.into_cx(cx)),
+        ForeignStaticItem(s) => ItemEnum::Static(s.into_cx(cx)),
         ForeignTypeItem => ItemEnum::ForeignType,
-        TypedefItem(t) => ItemEnum::Typedef(t.into_tcx(tcx)),
-        OpaqueTyItem(t) => ItemEnum::OpaqueTy(t.into_tcx(tcx)),
-        ConstantItem(c) => ItemEnum::Constant(c.into_tcx(tcx)),
+        TypedefItem(t) => ItemEnum::Typedef(t.into_cx(cx)),
+        OpaqueTyItem(t) => ItemEnum::OpaqueTy(t.into_cx(cx)),
+        ConstantItem(c) => ItemEnum::Constant(c.into_cx(cx)),
         MacroItem(m) => ItemEnum::Macro(m.source),
-        ProcMacroItem(m) => ItemEnum::ProcMacro(m.into_tcx(tcx)),
+        ProcMacroItem(m) => ItemEnum::ProcMacro(m.into_cx(cx)),
         PrimitiveItem(p) => ItemEnum::PrimitiveType(p.as_sym().to_string()),
-        TyAssocConstItem(ty) => ItemEnum::AssocConst { type_: ty.into_tcx(tcx), default: None },
-        AssocConstItem(ty, default) => {
-            ItemEnum::AssocConst { type_: ty.into_tcx(tcx), default: Some(default.expr(tcx)) }
-        }
-        TyAssocTypeItem(g, b) => ItemEnum::AssocType {
-            generics: (*g).into_tcx(tcx),
-            bounds: b.into_tcx(tcx),
-            default: None,
+        TyAssocConstItem(ty) => ItemEnum::AssocConst { type_: ty.into_cx(cx), default: None },
+        AssocConstItem(ty, default) => ItemEnum::AssocConst {
+            type_: ty.into_cx(cx),
+            // FIXME: Should we “disable” depth and length limits for the JSON backend?
+            default: Some(
+                default
+                    .eval_and_render(&ConstantRenderer::PlainText(cx.clone()))
+                    .unwrap_or_else(|| default.expr(cx.tcx)),
+            ),
         },
+        TyAssocTypeItem(g, b) => {
+            ItemEnum::AssocType { generics: (*g).into_cx(cx), bounds: b.into_cx(cx), default: None }
+        }
         AssocTypeItem(t, b) => ItemEnum::AssocType {
-            generics: t.generics.into_tcx(tcx),
-            bounds: b.into_tcx(tcx),
-            default: Some(t.item_type.unwrap_or(t.type_).into_tcx(tcx)),
+            generics: t.generics.into_cx(cx),
+            bounds: b.into_cx(cx),
+            default: Some(t.item_type.unwrap_or(t.type_).into_cx(cx)),
         },
         // `convert_item` early returns `None` for stripped items and keywords.
         KeywordItem => unreachable!(),
@@ -282,7 +303,7 @@ fn from_clean_item(item: clean::Item, tcx: TyCtxt<'_>) -> ItemEnum {
             match *inner {
                 ModuleItem(m) => ItemEnum::Module(Module {
                     is_crate,
-                    items: ids(m.items, tcx),
+                    items: ids(m.items, cx.tcx),
                     is_stripped: true,
                 }),
                 // `convert_item` early returns `None` for stripped items we're not including
@@ -296,28 +317,28 @@ fn from_clean_item(item: clean::Item, tcx: TyCtxt<'_>) -> ItemEnum {
     }
 }
 
-impl FromWithTcx<clean::Struct> for Struct {
-    fn from_tcx(struct_: clean::Struct, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::Struct> for Struct {
+    fn from_cx(struct_: clean::Struct, cx: &Context<'_>) -> Self {
         let fields_stripped = struct_.has_stripped_entries();
         let clean::Struct { struct_type, generics, fields } = struct_;
         Struct {
             struct_type: from_ctor_kind(struct_type),
-            generics: generics.into_tcx(tcx),
+            generics: generics.into_cx(cx),
             fields_stripped,
-            fields: ids(fields, tcx),
+            fields: ids(fields, cx.tcx),
             impls: Vec::new(), // Added in JsonRenderer::item
         }
     }
 }
 
-impl FromWithTcx<clean::Union> for Union {
-    fn from_tcx(union_: clean::Union, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::Union> for Union {
+    fn from_cx(union_: clean::Union, cx: &Context<'_>) -> Self {
         let fields_stripped = union_.has_stripped_entries();
         let clean::Union { generics, fields } = union_;
         Union {
-            generics: generics.into_tcx(tcx),
+            generics: generics.into_cx(cx),
             fields_stripped,
-            fields: ids(fields, tcx),
+            fields: ids(fields, cx.tcx),
             impls: Vec::new(), // Added in JsonRenderer::item
         }
     }
@@ -359,51 +380,51 @@ fn convert_lifetime(l: clean::Lifetime) -> String {
     l.0.to_string()
 }
 
-impl FromWithTcx<clean::Generics> for Generics {
-    fn from_tcx(generics: clean::Generics, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::Generics> for Generics {
+    fn from_cx(generics: clean::Generics, cx: &Context<'_>) -> Self {
         Generics {
-            params: generics.params.into_tcx(tcx),
-            where_predicates: generics.where_predicates.into_tcx(tcx),
+            params: generics.params.into_cx(cx),
+            where_predicates: generics.where_predicates.into_cx(cx),
         }
     }
 }
 
-impl FromWithTcx<clean::GenericParamDef> for GenericParamDef {
-    fn from_tcx(generic_param: clean::GenericParamDef, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::GenericParamDef> for GenericParamDef {
+    fn from_cx(generic_param: clean::GenericParamDef, cx: &Context<'_>) -> Self {
         GenericParamDef {
             name: generic_param.name.to_string(),
-            kind: generic_param.kind.into_tcx(tcx),
+            kind: generic_param.kind.into_cx(cx),
         }
     }
 }
 
-impl FromWithTcx<clean::GenericParamDefKind> for GenericParamDefKind {
-    fn from_tcx(kind: clean::GenericParamDefKind, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::GenericParamDefKind> for GenericParamDefKind {
+    fn from_cx(kind: clean::GenericParamDefKind, cx: &Context<'_>) -> Self {
         use clean::GenericParamDefKind::*;
         match kind {
             Lifetime { outlives } => GenericParamDefKind::Lifetime {
                 outlives: outlives.into_iter().map(convert_lifetime).collect(),
             },
             Type { did: _, bounds, default, synthetic } => GenericParamDefKind::Type {
-                bounds: bounds.into_tcx(tcx),
-                default: default.map(|x| (*x).into_tcx(tcx)),
+                bounds: bounds.into_cx(cx),
+                default: default.map(|x| (*x).into_cx(cx)),
                 synthetic,
             },
             Const { did: _, ty, default } => GenericParamDefKind::Const {
-                type_: (*ty).into_tcx(tcx),
+                type_: (*ty).into_cx(cx),
                 default: default.map(|x| *x),
             },
         }
     }
 }
 
-impl FromWithTcx<clean::WherePredicate> for WherePredicate {
-    fn from_tcx(predicate: clean::WherePredicate, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::WherePredicate> for WherePredicate {
+    fn from_cx(predicate: clean::WherePredicate, cx: &Context<'_>) -> Self {
         use clean::WherePredicate::*;
         match predicate {
             BoundPredicate { ty, bounds, bound_params } => WherePredicate::BoundPredicate {
-                type_: ty.into_tcx(tcx),
-                bounds: bounds.into_tcx(tcx),
+                type_: ty.into_cx(cx),
+                bounds: bounds.into_cx(cx),
                 generic_params: bound_params
                     .into_iter()
                     .map(|x| GenericParamDef {
@@ -414,23 +435,23 @@ impl FromWithTcx<clean::WherePredicate> for WherePredicate {
             },
             RegionPredicate { lifetime, bounds } => WherePredicate::RegionPredicate {
                 lifetime: convert_lifetime(lifetime),
-                bounds: bounds.into_tcx(tcx),
+                bounds: bounds.into_cx(cx),
             },
             EqPredicate { lhs, rhs } => {
-                WherePredicate::EqPredicate { lhs: lhs.into_tcx(tcx), rhs: rhs.into_tcx(tcx) }
+                WherePredicate::EqPredicate { lhs: lhs.into_cx(cx), rhs: rhs.into_cx(cx) }
             }
         }
     }
 }
 
-impl FromWithTcx<clean::GenericBound> for GenericBound {
-    fn from_tcx(bound: clean::GenericBound, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::GenericBound> for GenericBound {
+    fn from_cx(bound: clean::GenericBound, cx: &Context<'_>) -> Self {
         use clean::GenericBound::*;
         match bound {
             TraitBound(clean::PolyTrait { trait_, generic_params }, modifier) => {
                 GenericBound::TraitBound {
-                    trait_: trait_.into_tcx(tcx),
-                    generic_params: generic_params.into_tcx(tcx),
+                    trait_: trait_.into_cx(cx),
+                    generic_params: generic_params.into_cx(cx),
                     modifier: from_trait_bound_modifier(modifier),
                 }
             }
@@ -450,67 +471,67 @@ pub(crate) fn from_trait_bound_modifier(
     }
 }
 
-impl FromWithTcx<clean::Type> for Type {
-    fn from_tcx(ty: clean::Type, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::Type> for Type {
+    fn from_cx(ty: clean::Type, cx: &Context<'_>) -> Self {
         use clean::Type::{
             Array, BareFunction, BorrowedRef, Generic, ImplTrait, Infer, Primitive, QPath,
             RawPointer, Slice, Tuple,
         };
 
         match ty {
-            clean::Type::Path { path } => Type::ResolvedPath(path.into_tcx(tcx)),
+            clean::Type::Path { path } => Type::ResolvedPath(path.into_cx(cx)),
             clean::Type::DynTrait(bounds, lt) => Type::DynTrait(DynTrait {
                 lifetime: lt.map(convert_lifetime),
-                traits: bounds.into_tcx(tcx),
+                traits: bounds.into_cx(cx),
             }),
             Generic(s) => Type::Generic(s.to_string()),
             Primitive(p) => Type::Primitive(p.as_sym().to_string()),
-            BareFunction(f) => Type::FunctionPointer(Box::new((*f).into_tcx(tcx))),
-            Tuple(t) => Type::Tuple(t.into_tcx(tcx)),
-            Slice(t) => Type::Slice(Box::new((*t).into_tcx(tcx))),
-            Array(t, s) => Type::Array { type_: Box::new((*t).into_tcx(tcx)), len: s },
-            ImplTrait(g) => Type::ImplTrait(g.into_tcx(tcx)),
+            BareFunction(f) => Type::FunctionPointer(Box::new((*f).into_cx(cx))),
+            Tuple(t) => Type::Tuple(t.into_cx(cx)),
+            Slice(t) => Type::Slice(Box::new((*t).into_cx(cx))),
+            Array(t, s) => Type::Array { type_: Box::new((*t).into_cx(cx)), len: s },
+            ImplTrait(g) => Type::ImplTrait(g.into_cx(cx)),
             Infer => Type::Infer,
             RawPointer(mutability, type_) => Type::RawPointer {
                 mutable: mutability == ast::Mutability::Mut,
-                type_: Box::new((*type_).into_tcx(tcx)),
+                type_: Box::new((*type_).into_cx(cx)),
             },
             BorrowedRef { lifetime, mutability, type_ } => Type::BorrowedRef {
                 lifetime: lifetime.map(convert_lifetime),
                 mutable: mutability == ast::Mutability::Mut,
-                type_: Box::new((*type_).into_tcx(tcx)),
+                type_: Box::new((*type_).into_cx(cx)),
             },
             QPath { assoc, self_type, trait_, .. } => Type::QualifiedPath {
                 name: assoc.name.to_string(),
-                args: Box::new(assoc.args.clone().into_tcx(tcx)),
-                self_type: Box::new((*self_type).into_tcx(tcx)),
-                trait_: trait_.into_tcx(tcx),
+                args: Box::new(assoc.args.clone().into_cx(cx)),
+                self_type: Box::new((*self_type).into_cx(cx)),
+                trait_: trait_.into_cx(cx),
             },
         }
     }
 }
 
-impl FromWithTcx<clean::Path> for Path {
-    fn from_tcx(path: clean::Path, tcx: TyCtxt<'_>) -> Path {
+impl FromWithCx<clean::Path> for Path {
+    fn from_cx(path: clean::Path, cx: &Context<'_>) -> Path {
         Path {
             name: path.whole_name(),
-            id: from_item_id(path.def_id().into(), tcx),
-            args: path.segments.last().map(|args| Box::new(args.clone().args.into_tcx(tcx))),
+            id: from_item_id(path.def_id().into(), cx.tcx),
+            args: path.segments.last().map(|args| Box::new(args.clone().args.into_cx(cx))),
         }
     }
 }
 
-impl FromWithTcx<clean::Term> for Term {
-    fn from_tcx(term: clean::Term, tcx: TyCtxt<'_>) -> Term {
+impl FromWithCx<clean::Term> for Term {
+    fn from_cx(term: clean::Term, cx: &Context<'_>) -> Term {
         match term {
-            clean::Term::Type(ty) => Term::Type(FromWithTcx::from_tcx(ty, tcx)),
-            clean::Term::Constant(c) => Term::Constant(FromWithTcx::from_tcx(c, tcx)),
+            clean::Term::Type(ty) => Term::Type(FromWithCx::from_cx(ty, cx)),
+            clean::Term::Constant(c) => Term::Constant(FromWithCx::from_cx(c, cx)),
         }
     }
 }
 
-impl FromWithTcx<clean::BareFunctionDecl> for FunctionPointer {
-    fn from_tcx(bare_decl: clean::BareFunctionDecl, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::BareFunctionDecl> for FunctionPointer {
+    fn from_cx(bare_decl: clean::BareFunctionDecl, cx: &Context<'_>) -> Self {
         let clean::BareFunctionDecl { unsafety, generic_params, decl, abi } = bare_decl;
         FunctionPointer {
             header: Header {
@@ -519,23 +540,23 @@ impl FromWithTcx<clean::BareFunctionDecl> for FunctionPointer {
                 async_: false,
                 abi: convert_abi(abi),
             },
-            generic_params: generic_params.into_tcx(tcx),
-            decl: decl.into_tcx(tcx),
+            generic_params: generic_params.into_cx(cx),
+            decl: decl.into_cx(cx),
         }
     }
 }
 
-impl FromWithTcx<clean::FnDecl> for FnDecl {
-    fn from_tcx(decl: clean::FnDecl, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::FnDecl> for FnDecl {
+    fn from_cx(decl: clean::FnDecl, cx: &Context<'_>) -> Self {
         let clean::FnDecl { inputs, output, c_variadic } = decl;
         FnDecl {
             inputs: inputs
                 .values
                 .into_iter()
-                .map(|arg| (arg.name.to_string(), arg.type_.into_tcx(tcx)))
+                .map(|arg| (arg.name.to_string(), arg.type_.into_cx(cx)))
                 .collect(),
             output: match output {
-                clean::FnRetTy::Return(t) => Some(t.into_tcx(tcx)),
+                clean::FnRetTy::Return(t) => Some(t.into_cx(cx)),
                 clean::FnRetTy::DefaultReturn => None,
             },
             c_variadic,
@@ -543,34 +564,34 @@ impl FromWithTcx<clean::FnDecl> for FnDecl {
     }
 }
 
-impl FromWithTcx<clean::Trait> for Trait {
-    fn from_tcx(trait_: clean::Trait, tcx: TyCtxt<'_>) -> Self {
-        let is_auto = trait_.is_auto(tcx);
-        let is_unsafe = trait_.unsafety(tcx) == rustc_hir::Unsafety::Unsafe;
+impl FromWithCx<clean::Trait> for Trait {
+    fn from_cx(trait_: clean::Trait, cx: &Context<'_>) -> Self {
+        let is_auto = trait_.is_auto(cx.tcx);
+        let is_unsafe = trait_.unsafety(cx.tcx) == rustc_hir::Unsafety::Unsafe;
         let clean::Trait { items, generics, bounds, .. } = trait_;
         Trait {
             is_auto,
             is_unsafe,
-            items: ids(items, tcx),
-            generics: generics.into_tcx(tcx),
-            bounds: bounds.into_tcx(tcx),
+            items: ids(items, cx.tcx),
+            generics: generics.into_cx(cx),
+            bounds: bounds.into_cx(cx),
             implementations: Vec::new(), // Added in JsonRenderer::item
         }
     }
 }
 
-impl FromWithTcx<clean::PolyTrait> for PolyTrait {
-    fn from_tcx(
+impl FromWithCx<clean::PolyTrait> for PolyTrait {
+    fn from_cx(
         clean::PolyTrait { trait_, generic_params }: clean::PolyTrait,
-        tcx: TyCtxt<'_>,
+        cx: &Context<'_>,
     ) -> Self {
-        PolyTrait { trait_: trait_.into_tcx(tcx), generic_params: generic_params.into_tcx(tcx) }
+        PolyTrait { trait_: trait_.into_cx(cx), generic_params: generic_params.into_cx(cx) }
     }
 }
 
-impl FromWithTcx<clean::Impl> for Impl {
-    fn from_tcx(impl_: clean::Impl, tcx: TyCtxt<'_>) -> Self {
-        let provided_trait_methods = impl_.provided_trait_methods(tcx);
+impl FromWithCx<clean::Impl> for Impl {
+    fn from_cx(impl_: clean::Impl, cx: &Context<'_>) -> Self {
+        let provided_trait_methods = impl_.provided_trait_methods(cx.tcx);
         let clean::Impl { unsafety, generics, trait_, for_, items, polarity, kind } = impl_;
         // FIXME: use something like ImplKind in JSON?
         let (synthetic, blanket_impl) = match kind {
@@ -584,17 +605,17 @@ impl FromWithTcx<clean::Impl> for Impl {
         };
         Impl {
             is_unsafe: unsafety == rustc_hir::Unsafety::Unsafe,
-            generics: generics.into_tcx(tcx),
+            generics: generics.into_cx(cx),
             provided_trait_methods: provided_trait_methods
                 .into_iter()
                 .map(|x| x.to_string())
                 .collect(),
-            trait_: trait_.map(|path| path.into_tcx(tcx)),
-            for_: for_.into_tcx(tcx),
-            items: ids(items, tcx),
+            trait_: trait_.map(|path| path.into_cx(cx)),
+            for_: for_.into_cx(cx),
+            items: ids(items, cx.tcx),
             negative: negative_polarity,
             synthetic,
-            blanket_impl: blanket_impl.map(|x| x.into_tcx(tcx)),
+            blanket_impl: blanket_impl.map(|x| x.into_cx(cx)),
         }
     }
 }
@@ -602,12 +623,12 @@ impl FromWithTcx<clean::Impl> for Impl {
 pub(crate) fn from_function(
     function: Box<clean::Function>,
     header: rustc_hir::FnHeader,
-    tcx: TyCtxt<'_>,
+    cx: &Context<'_>,
 ) -> Function {
     let clean::Function { decl, generics } = *function;
     Function {
-        decl: decl.into_tcx(tcx),
-        generics: generics.into_tcx(tcx),
+        decl: decl.into_cx(cx),
+        generics: generics.into_cx(cx),
         header: from_fn_header(&header),
     }
 }
@@ -616,46 +637,46 @@ pub(crate) fn from_function_method(
     function: Box<clean::Function>,
     has_body: bool,
     header: rustc_hir::FnHeader,
-    tcx: TyCtxt<'_>,
+    cx: &Context<'_>,
 ) -> Method {
     let clean::Function { decl, generics } = *function;
     Method {
-        decl: decl.into_tcx(tcx),
-        generics: generics.into_tcx(tcx),
+        decl: decl.into_cx(cx),
+        generics: generics.into_cx(cx),
         header: from_fn_header(&header),
         has_body,
     }
 }
 
-impl FromWithTcx<clean::Enum> for Enum {
-    fn from_tcx(enum_: clean::Enum, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::Enum> for Enum {
+    fn from_cx(enum_: clean::Enum, cx: &Context<'_>) -> Self {
         let variants_stripped = enum_.has_stripped_entries();
         let clean::Enum { variants, generics } = enum_;
         Enum {
-            generics: generics.into_tcx(tcx),
+            generics: generics.into_cx(cx),
             variants_stripped,
-            variants: ids(variants, tcx),
+            variants: ids(variants, cx.tcx),
             impls: Vec::new(), // Added in JsonRenderer::item
         }
     }
 }
 
-impl FromWithTcx<clean::VariantStruct> for Struct {
-    fn from_tcx(struct_: clean::VariantStruct, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::VariantStruct> for Struct {
+    fn from_cx(struct_: clean::VariantStruct, cx: &Context<'_>) -> Self {
         let fields_stripped = struct_.has_stripped_entries();
         let clean::VariantStruct { struct_type, fields } = struct_;
         Struct {
             struct_type: from_ctor_kind(struct_type),
             generics: Generics { params: vec![], where_predicates: vec![] },
             fields_stripped,
-            fields: ids(fields, tcx),
+            fields: ids(fields, cx.tcx),
             impls: Vec::new(),
         }
     }
 }
 
-impl FromWithTcx<clean::Variant> for Variant {
-    fn from_tcx(variant: clean::Variant, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::Variant> for Variant {
+    fn from_cx(variant: clean::Variant, cx: &Context<'_>) -> Self {
         use clean::Variant::*;
         match variant {
             CLike => Variant::Plain,
@@ -664,20 +685,20 @@ impl FromWithTcx<clean::Variant> for Variant {
                     .into_iter()
                     .map(|f| {
                         if let clean::StructFieldItem(ty) = *f.kind {
-                            ty.into_tcx(tcx)
+                            ty.into_cx(cx)
                         } else {
                             unreachable!()
                         }
                     })
                     .collect(),
             ),
-            Struct(s) => Variant::Struct(ids(s.fields, tcx)),
+            Struct(s) => Variant::Struct(ids(s.fields, cx.tcx)),
         }
     }
 }
 
-impl FromWithTcx<clean::Import> for Import {
-    fn from_tcx(import: clean::Import, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::Import> for Import {
+    fn from_cx(import: clean::Import, cx: &Context<'_>) -> Self {
         use clean::ImportKind::*;
         let (name, glob) = match import.kind {
             Simple(s) => (s.to_string(), false),
@@ -689,14 +710,14 @@ impl FromWithTcx<clean::Import> for Import {
         Import {
             source: import.source.path.whole_name(),
             name,
-            id: import.source.did.map(ItemId::from).map(|i| from_item_id(i, tcx)),
+            id: import.source.did.map(ItemId::from).map(|i| from_item_id(i, cx.tcx)),
             glob,
         }
     }
 }
 
-impl FromWithTcx<clean::ProcMacro> for ProcMacro {
-    fn from_tcx(mac: clean::ProcMacro, _tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::ProcMacro> for ProcMacro {
+    fn from_cx(mac: clean::ProcMacro, _cx: &Context<'_>) -> Self {
         ProcMacro {
             kind: from_macro_kind(mac.kind),
             helpers: mac.helpers.iter().map(|x| x.to_string()).collect(),
@@ -713,37 +734,38 @@ pub(crate) fn from_macro_kind(kind: rustc_span::hygiene::MacroKind) -> MacroKind
     }
 }
 
-impl FromWithTcx<Box<clean::Typedef>> for Typedef {
-    fn from_tcx(typedef: Box<clean::Typedef>, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<Box<clean::Typedef>> for Typedef {
+    fn from_cx(typedef: Box<clean::Typedef>, cx: &Context<'_>) -> Self {
         let clean::Typedef { type_, generics, item_type: _ } = *typedef;
-        Typedef { type_: type_.into_tcx(tcx), generics: generics.into_tcx(tcx) }
+        Typedef { type_: type_.into_cx(cx), generics: generics.into_cx(cx) }
     }
 }
 
-impl FromWithTcx<clean::OpaqueTy> for OpaqueTy {
-    fn from_tcx(opaque: clean::OpaqueTy, tcx: TyCtxt<'_>) -> Self {
-        OpaqueTy { bounds: opaque.bounds.into_tcx(tcx), generics: opaque.generics.into_tcx(tcx) }
+impl FromWithCx<clean::OpaqueTy> for OpaqueTy {
+    fn from_cx(opaque: clean::OpaqueTy, cx: &Context<'_>) -> Self {
+        OpaqueTy { bounds: opaque.bounds.into_cx(cx), generics: opaque.generics.into_cx(cx) }
     }
 }
 
-impl FromWithTcx<clean::Static> for Static {
-    fn from_tcx(stat: clean::Static, tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<clean::Static> for Static {
+    fn from_cx(stat: clean::Static, cx: &Context<'_>) -> Self {
         Static {
-            type_: stat.type_.into_tcx(tcx),
+            type_: stat.type_.into_cx(cx),
             mutable: stat.mutability == ast::Mutability::Mut,
-            expr: stat.expr.map(|e| print_const_expr(tcx, e)).unwrap_or_default(),
+            // FIXME: Consider using `eval_and_render` here instead (despite the name of the field)
+            expr: stat.expr.map(|e| print_const_expr(cx.tcx, e)).unwrap_or_default(),
         }
     }
 }
 
-impl FromWithTcx<clean::TraitAlias> for TraitAlias {
-    fn from_tcx(alias: clean::TraitAlias, tcx: TyCtxt<'_>) -> Self {
-        TraitAlias { generics: alias.generics.into_tcx(tcx), params: alias.bounds.into_tcx(tcx) }
+impl FromWithCx<clean::TraitAlias> for TraitAlias {
+    fn from_cx(alias: clean::TraitAlias, cx: &Context<'_>) -> Self {
+        TraitAlias { generics: alias.generics.into_cx(cx), params: alias.bounds.into_cx(cx) }
     }
 }
 
-impl FromWithTcx<ItemType> for ItemKind {
-    fn from_tcx(kind: ItemType, _tcx: TyCtxt<'_>) -> Self {
+impl FromWithCx<ItemType> for ItemKind {
+    fn from_cx(kind: ItemType, _cx: &Context<'_>) -> Self {
         use ItemType::*;
         match kind {
             Module => ItemKind::Module,
diff --git a/src/librustdoc/json/mod.rs b/src/librustdoc/json/mod.rs
index 6364d00d0624e..bf514e02aa853 100644
--- a/src/librustdoc/json/mod.rs
+++ b/src/librustdoc/json/mod.rs
@@ -27,9 +27,11 @@ use crate::docfs::PathError;
 use crate::error::Error;
 use crate::formats::cache::Cache;
 use crate::formats::FormatRenderer;
-use crate::json::conversions::{from_item_id, from_item_id_with_name, IntoWithTcx};
+use crate::json::conversions::{from_item_id, from_item_id_with_name, IntoWithCx};
 use crate::{clean, try_err};
 
+pub(crate) use crate::json::conversions::Context;
+
 #[derive(Clone)]
 pub(crate) struct JsonRenderer<'tcx> {
     tcx: TyCtxt<'tcx>,
@@ -127,7 +129,11 @@ impl<'tcx> JsonRenderer<'tcx> {
                                 .last()
                                 .map(|s| s.to_string()),
                             visibility: types::Visibility::Public,
-                            inner: types::ItemEnum::Trait(trait_item.clone().into_tcx(self.tcx)),
+                            inner: types::ItemEnum::Trait(
+                                trait_item
+                                    .clone()
+                                    .into_cx(&Context::new(self.tcx, self.cache.clone())),
+                            ),
                             span: None,
                             docs: Default::default(),
                             links: Default::default(),
@@ -283,7 +289,7 @@ impl<'tcx> FormatRenderer<'tcx> for JsonRenderer<'tcx> {
                         types::ItemSummary {
                             crate_id: k.krate.as_u32(),
                             path: path.iter().map(|s| s.to_string()).collect(),
-                            kind: kind.into_tcx(self.tcx),
+                            kind: kind.into_cx(&Context::new(self.tcx, self.cache.clone())),
                         },
                     )
                 })
diff --git a/src/rustdoc-json-types/lib.rs b/src/rustdoc-json-types/lib.rs
index 7dcad66b1f992..f9a65b7a863a6 100644
--- a/src/rustdoc-json-types/lib.rs
+++ b/src/rustdoc-json-types/lib.rs
@@ -166,7 +166,26 @@ pub enum GenericArg {
 pub struct Constant {
     #[serde(rename = "type")]
     pub type_: Type,
+    /// The *unevaluated* constant expression assigned to this constant item.
+    ///
+    /// This textual representation may not actually be what the user wrote
+    /// or be valid Rust syntax since it may contain underscores in place of
+    /// overly big or complex expressions.
     pub expr: String,
+    /// The (constant) value of this constant item if the evaluation succeeded.
+    ///
+    /// It is the result of the *evaluation* of the constant expression assigned
+    /// to this constant item or `None` if the expression is “too generic”.
+    ///
+    /// This textual representation may not actually be lossless or even valid
+    /// Rust syntax since
+    ///
+    /// * overly long arrays & strings and deeply-nested subexpressions are replaced
+    ///   with ellipses
+    /// * private and `#[doc(hidden)]` struct fields are omitted
+    /// * `..` is added to (tuple) struct literals if any fields are omitted
+    ///   or if the type is `#[non_exhaustive]`
+    /// * `_` is used in place of “unsupported” expressions (e.g. pointers)
     pub value: Option<String>,
     pub is_literal: bool,
 }
@@ -259,7 +278,19 @@ pub enum ItemEnum {
     AssocConst {
         #[serde(rename = "type")]
         type_: Type,
-        /// e.g. `const X: usize = 5;`
+        /// The default (constant) value of this associated constant if available.
+        ///
+        /// E.g. the `5` in `const X: usize = 5;`.
+        ///
+        /// This textual representation may not actually be lossless or even valid
+        /// Rust syntax since
+        ///
+        /// * overly long arrays & strings and deeply-nested subexpressions are replaced
+        ///   with ellipses
+        /// * private and `#[doc(hidden)]` struct fields are omitted
+        /// * `..` is added to (tuple) struct literals if any fields are omitted
+        ///   or if the type is `#[non_exhaustive]`
+        /// * `_` is used in place of “unsupported” expressions (e.g. pointers)
         default: Option<String>,
     },
     AssocType {
diff --git a/src/test/rustdoc-json/auxiliary/data.rs b/src/test/rustdoc-json/auxiliary/data.rs
new file mode 100644
index 0000000000000..b4b2efb53c4f2
--- /dev/null
+++ b/src/test/rustdoc-json/auxiliary/data.rs
@@ -0,0 +1,26 @@
+use std::cell::Cell;
+
+pub struct Data {
+    pub open: (i8, i8, i8),
+    closed: bool,
+    #[doc(hidden)]
+    pub internal: Cell<u64>,
+}
+
+impl Data {
+    pub const fn new(value: (i8, i8, i8)) -> Self {
+        Self {
+            open: value,
+            closed: false,
+            internal: Cell::new(0),
+        }
+    }
+}
+
+pub struct Opaque(u32);
+
+impl Opaque {
+    pub const fn new(value: u32) -> Self {
+        Self(value)
+    }
+}
diff --git a/src/test/rustdoc-json/const_value.rs b/src/test/rustdoc-json/const_value.rs
new file mode 100644
index 0000000000000..6b554576797fd
--- /dev/null
+++ b/src/test/rustdoc-json/const_value.rs
@@ -0,0 +1,283 @@
+// Testing the formatting of constant values (i.e. evaluated constant expressions)
+// where the specific format was first proposed in issue #98929.
+
+// ignore-tidy-linelength
+// edition:2021
+// aux-crate:data=data.rs
+
+// @has const_value.json
+
+// Check that constant expressions are printed in their evaluated form.
+//
+// @is - "$.index[*][?(@.name=='HOUR_IN_SECONDS')].kind" \"constant\"
+// @is - "$.index[*][?(@.name=='HOUR_IN_SECONDS')].inner.value" \"3600\"
+pub const HOUR_IN_SECONDS: u64 = 60 * 60;
+
+// @is - "$.index[*][?(@.name=='NEGATIVE')].kind" \"constant\"
+// @is - "$.index[*][?(@.name=='NEGATIVE')].inner.value" \"-3600\"
+pub const NEGATIVE: i64 = -60 * 60;
+
+// @is - "$.index[*][?(@.name=='CONCATENATED')].kind" \"constant\"
+// @is - "$.index[*][?(@.name=='CONCATENATED')].inner.value" '"\"[0, +∞)\""'
+pub const CONCATENATED: &str = concat!("[", stringify!(0), ", ", "+∞", ")");
+
+pub struct Record<'r> {
+    pub one: &'r str,
+    pub two: (i32,),
+}
+
+// Test that structs whose fields are all public and 1-tuples are displayed correctly.
+// Furthermore, the struct fields should appear in definition order.
+//
+// @is - "$.index[*][?(@.name=='REC')].kind" \"constant\"
+// @is - "$.index[*][?(@.name=='REC')].inner.value" '"Record { one: \"thriving\", two: (180,) }"'
+pub const REC: Record<'_> = {
+    assert!(true);
+
+    let auxiliary = 90 * "||".len() as i32;
+    Record {
+        two: (
+            auxiliary,
+            #[cfg(FALSE)]
+            "vanished",
+        ),
+        one: "thriving",
+    }
+};
+
+// Check that private and doc(hidden) struct fields are not displayed.
+// Instead, an ellipsis (namely `..`) should be printed.
+//
+// @is - "$.index[*][?(@.name=='STRUCT')].kind" \"constant\"
+// @is - "$.index[*][?(@.name=='STRUCT')].inner.value" '"Struct { public: (), .. }"'
+pub const STRUCT: Struct = Struct {
+    private : /* SourceMap::span_to_snippet trap */ (),
+    public: { 1 + 3; },
+    hidden: ()
+};
+
+// Test that enum variants, 2-tuples, bools and structs (with private and doc(hidden) fields) nested
+// within are rendered correctly. Further, check that there is a maximum depth.
+//
+// @is - "$.index[*][?(@.name=='NESTED')].kind" \"constant\"
+// @is - "$.index[*][?(@.name=='NESTED')].inner.value" '"Some((Struct { public: …, .. }, false))"'
+pub const NESTED: Option<(Struct, bool)> = Some((
+    Struct {
+        public: (),
+        private: (),
+        hidden: (),
+    },
+    false,
+));
+
+use std::sync::atomic::AtomicBool;
+
+pub struct Struct {
+    private: (),
+    pub public: (),
+    #[doc(hidden)]
+    pub hidden: (),
+}
+
+impl Struct {
+    // Check that even inside inherent impl blocks private and doc(hidden) struct fields
+    // are not displayed.
+    //
+    // @is - "$.index[*][?(@.name=='SELF')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='SELF')].inner.default" '"Struct { public: (), .. }"'
+    pub const SELF: Self = Self {
+        private: (),
+        public: match () {
+            () => {}
+        },
+        hidden: (),
+    };
+
+    // Verify that private and doc(hidden) *tuple* struct fields are not shown.
+    // In their place, an underscore should be rendered.
+    //
+    // @is - "$.index[*][?(@.name=='TUP_STRUCT')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='TUP_STRUCT')].inner.default" '"TupStruct(_, -45, _, _)"'
+    pub const TUP_STRUCT: TupStruct = TupStruct((), -45, (), false);
+
+    // Check that structs whose fields are all doc(hidden) are rendered correctly.
+    //
+    // @is - "$.index[*][?(@.name=='SEALED0')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='SEALED0')].inner.default" '"Container0 { .. }"'
+    pub const SEALED0: Container0 = Container0 { hack: () };
+
+    // Check that *tuple* structs whose fields are all private are rendered correctly.
+    //
+    // @is - "$.index[*][?(@.name=='SEALED1')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='SEALED1')].inner.default" '"Container1(_)"'
+    pub const SEALED1: Container1 = Container1(None);
+
+    // Verify that cross-crate structs are displayed correctly and that their fields
+    // are not leaked.
+    //
+    // @is - "$.index[*][?(@.name=='SEALED2')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='SEALED2')].inner.default" '"AtomicBool { .. }"'
+    pub const SEALED2: AtomicBool = AtomicBool::new(true);
+
+    // Test that (local) *unit* enum variants are rendered properly.
+    //
+    // @is - "$.index[*][?(@.name=='SUM0')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='SUM0')].inner.default" '"Uninhabited"'
+    pub const SUM0: Size = self::Size::Uninhabited;
+
+    // Test that (local) *struct* enum variants are rendered properly.
+    //
+    // @is - "$.index[*][?(@.name=='SUM1')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='SUM1')].inner.default" '"Inhabited { inhabitants: 9000 }"'
+    pub const SUM1: Size = AdtSize::Inhabited { inhabitants: 9_000 };
+
+    // Test that (local) *tuple* enum variants are rendered properly.
+    //
+    // @is - "$.index[*][?(@.name=='SUM2')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='SUM2')].inner.default" '"Unknown(Reason)"'
+    pub const SUM2: Size = Size::Unknown(Reason);
+
+    // @is - "$.index[*][?(@.name=='INT')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='INT')].inner.default" '"2368"'
+    pub const INT: i64 = 2345 + 23;
+
+    // @is - "$.index[*][?(@.name=='STR0')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='STR0')].inner.default" '"\"hello friends >.<\""'
+    pub const STR0: &'static str = "hello friends >.<";
+
+    // @is - "$.index[*][?(@.name=='FLOAT0')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='FLOAT0')].inner.default" '"2930.21997"'
+    pub const FLOAT0: f32 = 2930.21997;
+
+    // @is - "$.index[*][?(@.name=='FLOAT1')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='FLOAT1')].inner.default" '"-3.42E+21"'
+    pub const FLOAT1: f64 = -3.42e+21;
+
+    // FIXME: Should we attempt more sophisticated formatting for references?
+    //
+    // @is - "$.index[*][?(@.name=='REF')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='REF')].inner.default" '"_"'
+    pub const REF: &'static i32 = &234;
+
+    // FIXME: Should we attempt more sophisticated formatting for raw pointers?
+    //
+    // @is - "$.index[*][?(@.name=='PTR')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='PTR')].inner.default" '"_"'
+    pub const PTR: *const u16 = &90;
+
+    // @is - "$.index[*][?(@.name=='ARR0')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='ARR0')].inner.default" '"[1080, 1080, 1080, 1080, 1080, 1080, 1080, 1080]"'
+    pub const ARR0: [u16; 8] = [12 * 90; 8];
+
+    // Check that after a certain unspecified size threshold, array elements
+    // won't be displayed anymore and that instead a series of ellipses is shown.
+    //
+    // @is - "$.index[*][?(@.name=='ARR1')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='ARR1')].inner.default" '"[………]"'
+    pub const ARR1: [u16; 100] = [12; 52 + 50 - 2];
+
+    // FIXME: We actually want to print the contents of slices!
+    // @is - "$.index[*][?(@.name=='SLICE0')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='SLICE0')].inner.default" '"_"'
+    pub const SLICE0: &'static [bool] = &[false, !true, true];
+
+    //
+    // Make sure that we don't leak private and doc(hidden) struct fields
+    // of cross-crate structs (i.e. structs from external crates).
+    //
+
+    // @is - "$.index[*][?(@.name=='DATA')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='DATA')].inner.default" '"Data { open: (0, 0, 1), .. }"'
+    pub const DATA: data::Data = data::Data::new((0, 0, 1));
+
+    // @is - "$.index[*][?(@.name=='OPAQ')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='OPAQ')].inner.default" '"Opaque(_)"'
+    pub const OPAQ: data::Opaque = data::Opaque::new(0xff00);
+}
+
+pub struct TupStruct(#[doc(hidden)] pub (), pub i32, (), #[doc(hidden)] pub bool);
+
+pub struct Container0 {
+    #[doc(hidden)]
+    pub hack: (),
+}
+
+pub struct Container1(Option<std::cell::Cell<u8>>);
+
+pub type AdtSize = Size;
+
+pub enum Size {
+    Inhabited { inhabitants: u128 },
+    Uninhabited,
+    Unknown(Reason),
+}
+
+pub struct Reason;
+
+use std::cmp::Ordering;
+
+pub trait Protocol {
+    // Make sure that this formatting also applies to const exprs inside of trait items, not just
+    // inside of inherent impl blocks or free constants.
+
+    // @is - "$.index[*][?(@.name=='MATCH')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='MATCH')].inner.default" '"99"'
+    const MATCH: u64 = match 1 + 4 {
+        SUPPORT => 99,
+        _ => 0,
+    };
+
+    // @is - "$.index[*][?(@.name=='OPT')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='OPT')].inner.default" '"Some(Some(Equal))"'
+    const OPT: Option<Option<Ordering>> = Some(Some(Ordering::Equal));
+
+    // Test that there is a depth limit. Subexpressions exceeding the maximum depth are
+    // rendered as ellipses.
+    //
+    // @is - "$.index[*][?(@.name=='DEEP0')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='DEEP0')].inner.default" '"Some(Some(Some(…)))"'
+    const DEEP0: Option<Option<Option<Ordering>>> = Some(Some(Some(Ordering::Equal)));
+
+    // FIXME: Add more depth tests
+
+    // Check that after a certain unspecified size threshold, the string contents
+    // won't be displayed anymore and that instead a series of ellipses is shown.
+    //
+    // @is - "$.index[*][?(@.name=='STR1')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='STR1')].inner.default" '"\"………\""'
+    const STR1: &'static str = "\
+        This is the start of a relatively long text. \
+        I might as well throw some more words into it. \
+        Informative content? Never heard of it! \
+        That's probably one of the reasons why I shouldn't be included \
+        into the generated documentation, don't you think so, too?\
+    ";
+
+    // @is - "$.index[*][?(@.name=='BYTE_STR0')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='BYTE_STR0')].inner.default" '"b\"Stuck in the days of yore! >.<\""'
+    const BYTE_STR0: &'static [u8] = b"Stuck in the days of yore! >.<";
+
+    // Check that after a certain unspecified size threshold, the byte string contents
+    // won't be displayed anymore and that instead a series of ellipses is shown.
+    //
+    // @is - "$.index[*][?(@.name=='BYTE_STR1')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='BYTE_STR1')].inner.default" '"b\"………\""'
+    const BYTE_STR1: &'static [u8] = b"\
+        AGTC CCTG GAAT TACC AAAA AACA TCCA AGTC CTCT \
+        AGTC CCTG TCCA AGTC CTCT GAAT TACC AAAA AACA \
+        AGTC CCTG GAAT TACC AAAA GGGG GGGG AGTC GTTT \
+        GGGG AACA TCCA AGTC CTCT AGTC CCTG GAAT TACC \
+        AGTC AAAA GAAT TACC CGAG AACA TCCA AGTC CTCT \
+        AGTC CCTG GAAT TACC TTCC AACA TCCA AGTC CTCT\
+    ";
+
+    // @is - "$.index[*][?(@.name=='BYTE_ARR0')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='BYTE_ARR0')].inner.default" '"*b\"DEREFERENCED\""'
+    const BYTE_ARR0: [u8; 12] = *b"DEREFERENCED";
+
+    // @is - "$.index[*][?(@.name=='BYTE_ARR1')].kind" \"assoc_const\"
+    // @is - "$.index[*][?(@.name=='BYTE_ARR1')].inner.default" '"*b\"MINCED\\x00\""'
+    const BYTE_ARR1: [u8; 7] = [b'M', b'I', b'N', b'C', b'E', b'D', b'\0'];
+}
+
+const SUPPORT: i32 = 5;
diff --git a/src/test/rustdoc/anchors.no_const_anchor2.html b/src/test/rustdoc/anchors.no_const_anchor2.html
index 6d37e8e5eee5e..95607679af2f9 100644
--- a/src/test/rustdoc/anchors.no_const_anchor2.html
+++ b/src/test/rustdoc/anchors.no_const_anchor2.html
@@ -1 +1 @@
-<section id="associatedconstant.X" class="associatedconstant has-srclink"><span class="rightside"><a class="srclink" href="../src/foo/anchors.rs.html#42">source</a></span><h4 class="code-header">pub const <a href="#associatedconstant.X" class="constant">X</a>: <a class="primitive" href="{{channel}}/std/primitive.i32.html">i32</a> = 0i32</h4></section>
+<section id="associatedconstant.X" class="associatedconstant has-srclink"><span class="rightside"><a class="srclink" href="../src/foo/anchors.rs.html#42">source</a></span><h4 class="code-header">pub const <a href="#associatedconstant.X" class="constant">X</a>: <a class="primitive" href="{{channel}}/std/primitive.i32.html">i32</a> = 0</h4></section>
diff --git a/src/test/rustdoc/assoc-consts.rs b/src/test/rustdoc/assoc-consts.rs
index 97b7739b4c975..46345c7a5bb72 100644
--- a/src/test/rustdoc/assoc-consts.rs
+++ b/src/test/rustdoc/assoc-consts.rs
@@ -1,6 +1,6 @@
 pub trait Foo {
     // @has assoc_consts/trait.Foo.html '//*[@class="rust trait"]' \
-    //      'const FOO: usize = 13usize;'
+    //      'const FOO: usize = 13;'
     // @has - '//*[@id="associatedconstant.FOO"]' 'const FOO: usize'
     const FOO: usize = 12 + 1;
     // @has - '//*[@id="associatedconstant.FOO_NO_DEFAULT"]' 'const FOO_NO_DEFAULT: bool'
diff --git a/src/test/rustdoc/auxiliary/const-value.rs b/src/test/rustdoc/auxiliary/const-value.rs
new file mode 100644
index 0000000000000..b4b2efb53c4f2
--- /dev/null
+++ b/src/test/rustdoc/auxiliary/const-value.rs
@@ -0,0 +1,26 @@
+use std::cell::Cell;
+
+pub struct Data {
+    pub open: (i8, i8, i8),
+    closed: bool,
+    #[doc(hidden)]
+    pub internal: Cell<u64>,
+}
+
+impl Data {
+    pub const fn new(value: (i8, i8, i8)) -> Self {
+        Self {
+            open: value,
+            closed: false,
+            internal: Cell::new(0),
+        }
+    }
+}
+
+pub struct Opaque(u32);
+
+impl Opaque {
+    pub const fn new(value: u32) -> Self {
+        Self(value)
+    }
+}
diff --git a/src/test/rustdoc/const-value-dead-links.data.html b/src/test/rustdoc/const-value-dead-links.data.html
new file mode 100644
index 0000000000000..4d47a5e6d4abd
--- /dev/null
+++ b/src/test/rustdoc/const-value-dead-links.data.html
@@ -0,0 +1 @@
+<code>pub const DATA: Data = Data { open: (0, 0, 1), .. };</code>
\ No newline at end of file
diff --git a/src/test/rustdoc/const-value-dead-links.opaq.html b/src/test/rustdoc/const-value-dead-links.opaq.html
new file mode 100644
index 0000000000000..65b96790bc74b
--- /dev/null
+++ b/src/test/rustdoc/const-value-dead-links.opaq.html
@@ -0,0 +1 @@
+<code>pub const OPAQ: Opaque = Opaque(_);</code>
\ No newline at end of file
diff --git a/src/test/rustdoc/const-value-dead-links.rs b/src/test/rustdoc/const-value-dead-links.rs
new file mode 100644
index 0000000000000..670ec77bfcb85
--- /dev/null
+++ b/src/test/rustdoc/const-value-dead-links.rs
@@ -0,0 +1,22 @@
+// aux-crate:aux=const-value.rs
+// edition:2021
+#![crate_name = "consts"]
+
+// Test that *no* hyperlink anchors are created for the structs here since their
+// documentation wasn't built (the dependency crate was only *compiled*).
+// Re snapshots: Check that this is indeed the case.
+//
+// NB: The corresponding test cases where the docs of the dependency *were* built
+//     can be found in `./const-value.rs` (as `Struct::{DATA, OPAQ}`).
+
+// @has 'consts/constant.DATA.html'
+// @has - '//*[@class="docblock item-decl"]//code' \
+//        'const DATA: Data = Data { open: (0, 0, 1), .. }'
+// @snapshot data - '//*[@class="docblock item-decl"]//code'
+pub const DATA: aux::Data = aux::Data::new((0, 0, 1));
+
+// @has 'consts/constant.OPAQ.html'
+// @has - '//*[@class="docblock item-decl"]//code' \
+//        'const OPAQ: Opaque = Opaque(_)'
+// @snapshot opaq - '//*[@class="docblock item-decl"]//code'
+pub const OPAQ: aux::Opaque = aux::Opaque::new(0xff00);
diff --git a/src/test/rustdoc/const-value-display.rs b/src/test/rustdoc/const-value-display.rs
deleted file mode 100644
index 5b2f3c48d57fa..0000000000000
--- a/src/test/rustdoc/const-value-display.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-#![crate_name = "foo"]
-
-// @has 'foo/constant.HOUR_IN_SECONDS.html'
-// @has - '//*[@class="docblock item-decl"]//code' 'pub const HOUR_IN_SECONDS: u64 = _; // 3_600u64'
-pub const HOUR_IN_SECONDS: u64 = 60 * 60;
-
-// @has 'foo/constant.NEGATIVE.html'
-// @has - '//*[@class="docblock item-decl"]//code' 'pub const NEGATIVE: i64 = _; // -3_600i64'
-pub const NEGATIVE: i64 = -60 * 60;
diff --git a/src/test/rustdoc/const-value-document-hidden.rs b/src/test/rustdoc/const-value-document-hidden.rs
new file mode 100644
index 0000000000000..13ac2231dc58e
--- /dev/null
+++ b/src/test/rustdoc/const-value-document-hidden.rs
@@ -0,0 +1,27 @@
+// aux-crate:aux=const-value.rs
+// compile-flags: -Zunstable-options --document-hidden-items
+
+// edition:2021
+#![crate_name = "consts"]
+
+// @has 'consts/struct.Context.html'
+pub struct Context {
+    yi: i32,
+    pub er: bool,
+    #[doc(hidden)]
+    pub san: aux::Data,
+}
+
+impl Context {
+    // Test that with `--document-hidden-items`, the hidden fields of the *local* type `Context`
+    // show up in the documentation but
+    // the hidden field `internal` of the *non-local* type `aux::Data` does *not*.
+    //
+    // @has - '//*[@id="associatedconstant.DUMMY"]' \
+    //        'const DUMMY: Context = Context { er: false, san: Data { open: (…, …, …), .. }, .. }'
+    pub const DUMMY: Context = Context {
+        yi: 0xFFFFFF,
+        er: false,
+        san: aux::Data::new((2, 0, -1)),
+    };
+}
diff --git a/src/test/rustdoc/const-value-document-private.rs b/src/test/rustdoc/const-value-document-private.rs
new file mode 100644
index 0000000000000..4c30aacf41233
--- /dev/null
+++ b/src/test/rustdoc/const-value-document-private.rs
@@ -0,0 +1,31 @@
+// aux-crate:aux=const-value.rs
+// compile-flags: --document-private-items
+
+// edition:2021
+#![crate_name = "consts"]
+
+// ignore-tidy-linelength
+
+// @has 'consts/struct.Context.html'
+pub struct Context {
+    yi: i32,
+    pub er: bool,
+    san: aux::Data,
+    #[doc(hidden)]
+    pub si: (),
+}
+
+impl Context {
+    // Test that with `--document-private-items`, the private fields of the *local* type `Context`
+    // show up in the documentation but
+    // the private field `closed` of the *non-local* type `aux::Data` does *not*.
+    //
+    // @has - '//*[@id="associatedconstant.DUMMY"]' \
+    //        'const DUMMY: Context = Context { yi: 16777215, er: false, san: Data { open: (…, …, …), .. }, .. }'
+    pub const DUMMY: Context = Context {
+        yi: 0xFFFFFF,
+        er: false,
+        san: aux::Data::new((2, 0, -1)),
+        si: (),
+    };
+}
diff --git a/src/test/rustdoc/const-value.arr1.html b/src/test/rustdoc/const-value.arr1.html
new file mode 100644
index 0000000000000..20b3c3b510d13
--- /dev/null
+++ b/src/test/rustdoc/const-value.arr1.html
@@ -0,0 +1 @@
+<h4 class="code-header">pub const <a href="#associatedconstant.ARR1" class="constant">ARR1</a>: <a class="primitive" href="{{channel}}/std/primitive.array.html">[</a><a class="primitive" href="{{channel}}/std/primitive.u16.html">u16</a><a class="primitive" href="{{channel}}/std/primitive.array.html">; 100]</a> = [<span class="ellipsis">&#8230;&#8230;&#8230;</span>]</h4>
\ No newline at end of file
diff --git a/src/test/rustdoc/const-value.byte-str1.html b/src/test/rustdoc/const-value.byte-str1.html
new file mode 100644
index 0000000000000..bdb4205ba46ab
--- /dev/null
+++ b/src/test/rustdoc/const-value.byte-str1.html
@@ -0,0 +1 @@
+<h4 class="code-header">const <a href="#associatedconstant.BYTE_STR1" class="constant">BYTE_STR1</a>: &amp;'static [<a class="primitive" href="{{channel}}/std/primitive.u8.html">u8</a>] = b"<span class="ellipsis">&#8230;&#8230;&#8230;</span>"</h4>
\ No newline at end of file
diff --git a/src/test/rustdoc/const-value.data.html b/src/test/rustdoc/const-value.data.html
new file mode 100644
index 0000000000000..1f62dea211b68
--- /dev/null
+++ b/src/test/rustdoc/const-value.data.html
@@ -0,0 +1 @@
+<h4 class="code-header">pub const <a href="#associatedconstant.DATA" class="constant">DATA</a>: <a class="struct" href="../const_value/struct.Data.html" title="struct const_value::Data">Data</a> = <a class="struct" href="../const_value/struct.Data.html" title="struct const_value::Data">Data</a> { <a class="structfield" href="../const_value/struct.Data.html#structfield.open" title="field open">open</a>: (0, 0, 1), .. }</h4>
\ No newline at end of file
diff --git a/src/test/rustdoc/const-value.deep0.html b/src/test/rustdoc/const-value.deep0.html
new file mode 100644
index 0000000000000..6d8315112fcd5
--- /dev/null
+++ b/src/test/rustdoc/const-value.deep0.html
@@ -0,0 +1 @@
+<h4 class="code-header">const <a href="#associatedconstant.DEEP0" class="constant">DEEP0</a>: <a class="enum" href="{{channel}}/core/option/enum.Option.html" title="enum core::option::Option">Option</a>&lt;<a class="enum" href="{{channel}}/core/option/enum.Option.html" title="enum core::option::Option">Option</a>&lt;<a class="enum" href="{{channel}}/core/option/enum.Option.html" title="enum core::option::Option">Option</a>&lt;<a class="enum" href="{{channel}}/core/cmp/enum.Ordering.html" title="enum core::cmp::Ordering">Ordering</a>&gt;&gt;&gt; = <a class="variant" href="{{channel}}/core/option/enum.Option.html#variant.Some" title="variant core::option::Option::Some">Some</a>(<a class="variant" href="{{channel}}/core/option/enum.Option.html#variant.Some" title="variant core::option::Option::Some">Some</a>(<a class="variant" href="{{channel}}/core/option/enum.Option.html#variant.Some" title="variant core::option::Option::Some">Some</a>(<span class="ellipsis">&#8230;</span>)))</h4>
\ No newline at end of file
diff --git a/src/test/rustdoc/const-value.nested.html b/src/test/rustdoc/const-value.nested.html
new file mode 100644
index 0000000000000..a3916bdc566fe
--- /dev/null
+++ b/src/test/rustdoc/const-value.nested.html
@@ -0,0 +1 @@
+<code>pub const NESTED: <a class="enum" href="{{channel}}/core/option/enum.Option.html" title="enum core::option::Option">Option</a>&lt;(<a class="struct" href="struct.Struct.html" title="struct consts::Struct">Struct</a>, <a class="primitive" href="{{channel}}/std/primitive.bool.html">bool</a>)&gt; = <a class="variant" href="{{channel}}/core/option/enum.Option.html#variant.Some" title="variant core::option::Option::Some">Some</a>((<a class="struct" href="struct.Struct.html" title="struct consts::Struct">Struct</a> { <a class="structfield" href="struct.Struct.html#structfield.public" title="field public">public</a>: <span class="ellipsis">&#8230;</span>, .. }, false));</code>
\ No newline at end of file
diff --git a/src/test/rustdoc/const-value.opaq.html b/src/test/rustdoc/const-value.opaq.html
new file mode 100644
index 0000000000000..761c6a5beab15
--- /dev/null
+++ b/src/test/rustdoc/const-value.opaq.html
@@ -0,0 +1 @@
+<h4 class="code-header">pub const <a href="#associatedconstant.OPAQ" class="constant">OPAQ</a>: <a class="struct" href="../const_value/struct.Opaque.html" title="struct const_value::Opaque">Opaque</a> = <a class="struct" href="../const_value/struct.Opaque.html" title="struct const_value::Opaque">Opaque</a>(_)</h4>
\ No newline at end of file
diff --git a/src/test/rustdoc/const-value.opt.html b/src/test/rustdoc/const-value.opt.html
new file mode 100644
index 0000000000000..b1da5dd7178d7
--- /dev/null
+++ b/src/test/rustdoc/const-value.opt.html
@@ -0,0 +1 @@
+<h4 class="code-header">const <a href="#associatedconstant.OPT" class="constant">OPT</a>: <a class="enum" href="{{channel}}/core/option/enum.Option.html" title="enum core::option::Option">Option</a>&lt;<a class="enum" href="{{channel}}/core/option/enum.Option.html" title="enum core::option::Option">Option</a>&lt;<a class="enum" href="{{channel}}/core/cmp/enum.Ordering.html" title="enum core::cmp::Ordering">Ordering</a>&gt;&gt; = <a class="variant" href="{{channel}}/core/option/enum.Option.html#variant.Some" title="variant core::option::Option::Some">Some</a>(<a class="variant" href="{{channel}}/core/option/enum.Option.html#variant.Some" title="variant core::option::Option::Some">Some</a>(<a class="variant" href="{{channel}}/core/cmp/enum.Ordering.html#variant.Equal" title="variant core::cmp::Ordering::Equal">Equal</a>))</h4>
\ No newline at end of file
diff --git a/src/test/rustdoc/const-value.rec.html b/src/test/rustdoc/const-value.rec.html
new file mode 100644
index 0000000000000..6490f697b948f
--- /dev/null
+++ b/src/test/rustdoc/const-value.rec.html
@@ -0,0 +1 @@
+<code>pub const REC: <a class="struct" href="struct.Record.html" title="struct consts::Record">Record</a>&lt;'static&gt; = <a class="struct" href="struct.Record.html" title="struct consts::Record">Record</a> { <a class="structfield" href="struct.Record.html#structfield.one" title="field one">one</a>: "thriving", <a class="structfield" href="struct.Record.html#structfield.two" title="field two">two</a>: (180,) };</code>
\ No newline at end of file
diff --git a/src/test/rustdoc/const-value.rs b/src/test/rustdoc/const-value.rs
new file mode 100644
index 0000000000000..0f63de4a5b5d8
--- /dev/null
+++ b/src/test/rustdoc/const-value.rs
@@ -0,0 +1,378 @@
+// Testing the formatting of constant values (i.e. evaluated constant expressions)
+// where the specific format was first proposed in issue #98929.
+
+// edition:2021
+#![crate_name = "consts"]
+
+// aux-build:const-value.rs
+// build-aux-docs
+// ignore-cross-compile
+extern crate const_value as aux;
+
+// ignore-tidy-linelength
+
+// FIXME: Some tests in here might be redundant and already present in other test files.
+// FIXME: Test restricted visibilities (e.g. `pub(super)`, `pub(in crate::some::thing)`).
+
+// Check that constant expressions are printed in their evaluated form.
+//
+// @has 'consts/constant.HOUR_IN_SECONDS.html'
+// @has - '//*[@class="docblock item-decl"]//code' 'pub const HOUR_IN_SECONDS: u64 = 3600;'
+pub const HOUR_IN_SECONDS: u64 = 60 * 60;
+
+// @has 'consts/constant.NEGATIVE.html'
+// @has - '//*[@class="docblock item-decl"]//code' 'pub const NEGATIVE: i64 = -3600;'
+pub const NEGATIVE: i64 = -60 * 60;
+
+// @has 'consts/constant.CONCATENATED.html'
+// @has - '//*[@class="docblock item-decl"]//code' \
+//        "pub const CONCATENATED: &'static str = \"[0, +∞)\";"
+pub const CONCATENATED: &str = concat!("[", stringify!(0), ", ", "+∞", ")");
+
+// @has 'consts/struct.Record.html'
+pub struct Record<'r> {
+    pub one: &'r str,
+    pub two: (i32,),
+}
+
+// Test that structs whose fields are all public and 1-tuples are displayed correctly.
+// Furthermore, the struct fields should appear in definition order.
+// Re snapshot: Check that hyperlinks are generated for the struct name and fields.
+//
+// @has 'consts/constant.REC.html'
+// @has - '//*[@class="docblock item-decl"]//code' \
+//        "const REC: Record<'static> = Record { one: \"thriving\", two: (180,) }"
+// @snapshot rec - '//*[@class="docblock item-decl"]//code'
+pub const REC: Record<'_> = {
+    assert!(true);
+
+    let auxiliary = 90 * "||".len() as i32;
+    Record {
+        two: (
+            auxiliary,
+            #[cfg(FALSE)]
+            "vanished",
+        ),
+        one: "thriving",
+    }
+};
+
+// Check that private and doc(hidden) struct fields are not displayed.
+// Instead, an ellipsis (namely `..`) should be printed.
+// Re snapshot: Check that hyperlinks are generated for the struct name and the public struct field.
+//
+// @has 'consts/constant.STRUCT.html'
+// @has - '//*[@class="docblock item-decl"]//code' \
+//        'const STRUCT: Struct = Struct { public: (), .. }'
+// @snapshot struct - '//*[@class="docblock item-decl"]//code'
+pub const STRUCT: Struct = Struct {
+    private : /* SourceMap::span_to_snippet trap */ (),
+    public: { 1 + 3; },
+    hidden: ()
+};
+
+// Test that enum variants, 2-tuples, bools and structs (with private and doc(hidden) fields) nested
+// within are rendered correctly. Further, check that there is a maximum depth.
+// Re snapshot: Test the hyperlinks are generated for the cross-crate enum variant etc.
+//
+// @has 'consts/constant.NESTED.html'
+// @has - '//*[@class="docblock item-decl"]//code' \
+//        'const NESTED: Option<(Struct, bool)> = Some((Struct { public: …, .. }, false))'
+// @snapshot nested - '//*[@class="docblock item-decl"]//code'
+pub const NESTED: Option<(Struct, bool)> = Some((
+    Struct {
+        public: (),
+        private: (),
+        hidden: (),
+    },
+    false,
+));
+
+use std::sync::atomic::AtomicBool;
+
+// @has 'consts/struct.Struct.html'
+pub struct Struct {
+    private: (),
+    pub public: (),
+    #[doc(hidden)]
+    pub hidden: (),
+}
+
+impl Struct {
+    // Check that even inside inherent impl blocks private and doc(hidden) struct fields
+    // are not displayed.
+    // Re snapshot: Check that hyperlinks are generated for the struct name and
+    // the public struct field.
+    //
+    // @has - '//*[@id="associatedconstant.SELF"]' \
+    //        'const SELF: Self = Struct { public: (), .. }'
+    // @snapshot self - '//*[@id="associatedconstant.SELF"]//*[@class="code-header"]'
+    pub const SELF: Self = Self {
+        private: (),
+        public: match () {
+            () => {}
+        },
+        hidden: (),
+    };
+
+    // Verify that private and doc(hidden) *tuple* struct fields are not shown.
+    // In their place, an underscore should be rendered.
+    // Re snapshot: Check that a hyperlink is generated for the tuple struct name.
+    //
+    // @has - '//*[@id="associatedconstant.TUP_STRUCT"]' \
+    //        'const TUP_STRUCT: TupStruct = TupStruct(_, -45, _, _)'
+    // @snapshot tup-struct - '//*[@id="associatedconstant.TUP_STRUCT"]//*[@class="code-header"]'
+    pub const TUP_STRUCT: TupStruct = TupStruct((), -45, (), false);
+
+    // Check that structs whose fields are all doc(hidden) are rendered correctly.
+    //
+    // @has - '//*[@id="associatedconstant.SEALED0"]' \
+    //        'const SEALED0: Container0 = Container0 { .. }'
+    pub const SEALED0: Container0 = Container0 { hack: () };
+
+    // Check that *tuple* structs whose fields are all private are rendered correctly.
+    //
+    // @has - '//*[@id="associatedconstant.SEALED1"]' \
+    //        'const SEALED1: Container1 = Container1(_)'
+    pub const SEALED1: Container1 = Container1(None);
+
+    // Verify that cross-crate structs are displayed correctly and that their fields
+    // are not leaked.
+    // Re snapshot: Check that a hyperlink is generated for the name of the cross-crate struct.
+    //
+    // @has - '//*[@id="associatedconstant.SEALED2"]' \
+    //        'const SEALED2: AtomicBool = AtomicBool { .. }'
+    // @snapshot sealed2 - '//*[@id="associatedconstant.SEALED2"]//*[@class="code-header"]'
+    pub const SEALED2: AtomicBool = AtomicBool::new(true);
+
+    // Test that (local) *unit* enum variants are rendered properly.
+    // Re snapshot: Test that a hyperlink is generated for the variant.
+    //
+    // @has - '//*[@id="associatedconstant.SUM0"]' \
+    //        'const SUM0: Size = Uninhabited'
+    // @snapshot sum0 - '//*[@id="associatedconstant.SUM0"]//*[@class="code-header"]'
+    pub const SUM0: Size = self::Size::Uninhabited;
+
+    // Test that (local) *struct* enum variants are rendered properly.
+    // Re snapshot: Test that a hyperlink is generated for the variant.
+    //
+    // @has - '//*[@id="associatedconstant.SUM1"]' \
+    //        'const SUM1: Size = Inhabited { inhabitants: 9000 }'
+    // @snapshot sum1 - '//*[@id="associatedconstant.SUM1"]//*[@class="code-header"]'
+    pub const SUM1: Size = AdtSize::Inhabited { inhabitants: 9_000 };
+
+    // Test that (local) *tuple* enum variants are rendered properly.
+    // Re snapshot: Test that a hyperlink is generated for the variant.
+    //
+    // @has - '//*[@id="associatedconstant.SUM2"]' \
+    //        'const SUM2: Size = Unknown(Reason)'
+    // @snapshot sum2 - '//*[@id="associatedconstant.SUM2"]//*[@class="code-header"]'
+    pub const SUM2: Size = Size::Unknown(Reason);
+
+    // @has - '//*[@id="associatedconstant.INT"]' \
+    //        'const INT: i64 = 2368'
+    pub const INT: i64 = 2345 + 23;
+
+    // @has - '//*[@id="associatedconstant.STR"]' \
+    //        "const STR: &'static str = \"hello friends\""
+    pub const STR: &'static str = "hello friends";
+
+    // @has - '//*[@id="associatedconstant.FLOAT0"]' \
+    //        'const FLOAT0: f32 = 2930.21997'
+    pub const FLOAT0: f32 = 2930.21997;
+
+    // @has - '//*[@id="associatedconstant.FLOAT1"]' \
+    //        'const FLOAT1: f64 = -3.42E+21'
+    pub const FLOAT1: f64 = -3.42e+21;
+
+    // @has - '//*[@id="associatedconstant.REF"]' \
+    //        "const REF: &'static i32 = _"
+    pub const REF: &'static i32 = &234;
+
+    // @has - '//*[@id="associatedconstant.PTR"]' \
+    //        'const PTR: *const u16 = _'
+    pub const PTR: *const u16 = &90;
+
+    // @has - '//*[@id="associatedconstant.ARR0"]' \
+    //        'const ARR0: [u16; 8] = [1080, 1080, 1080, 1080, 1080, 1080, 1080, 1080]'
+    pub const ARR0: [u16; 8] = [12 * 90; 8];
+
+    // Check that after a certain unspecified size threshold, array elements
+    // won't be displayed anymore and that instead a *styled* series of ellipses is shown.
+    // Re snapshot: Check that the series of ellipses is styled (has a certain CSS class).
+    //
+    // @has - '//*[@id="associatedconstant.ARR1"]' \
+    //        'const ARR1: [u16; 100] = [………]'
+    // @snapshot arr1 - '//*[@id="associatedconstant.ARR1"]//*[@class="code-header"]'
+    pub const ARR1: [u16; 100] = [12; 52 + 50 - 2];
+
+    // FIXME: We actually want to print the contents of slices!
+    // @has - '//*[@id="associatedconstant.SLICE0"]' \
+    //        "const SLICE0: &'static [bool] = _"
+    pub const SLICE0: &'static [bool] = &[false, !true, true];
+
+    //
+    // The following two test cases are regression tests for issue #99630:
+    // Make sure that we don't leak private and doc(hidden) struct fields
+    // of cross-crate structs (i.e. structs from external crates).
+    //
+
+    // @has - '//*[@id="associatedconstant.DATA"]' \
+    //        'const DATA: Data = Data { open: (0, 0, 1), .. }'
+    // @snapshot data - '//*[@id="associatedconstant.DATA"]//*[@class="code-header"]'
+    pub const DATA: aux::Data = aux::Data::new((0, 0, 1));
+
+    // @has - '//*[@id="associatedconstant.OPAQ"]' \
+    //        'const OPAQ: Opaque = Opaque(_)'
+    // @snapshot opaq - '//*[@id="associatedconstant.OPAQ"]//*[@class="code-header"]'
+    pub const OPAQ: aux::Opaque = aux::Opaque::new(0xff00);
+}
+
+pub struct TupStruct(#[doc(hidden)] pub (), pub i32, (), #[doc(hidden)] pub bool);
+
+pub struct Container0 {
+    #[doc(hidden)]
+    pub hack: (),
+}
+
+pub struct Container1(Option<std::cell::Cell<u8>>);
+
+pub type AdtSize = Size;
+
+pub enum Size {
+    Inhabited { inhabitants: u128 },
+    Uninhabited,
+    Unknown(Reason),
+}
+
+pub struct Reason;
+
+use std::cmp::Ordering;
+
+// @has 'consts/trait.Protocol.html'
+pub trait Protocol {
+    // Make sure that this formatting also applies to const exprs inside of trait items, not just
+    // inside of inherent impl blocks or free constants.
+
+    // @has - '//*[@id="associatedconstant.MATCH"]' \
+    //        'const MATCH: u64 = 99'
+    const MATCH: u64 = match 1 + 4 {
+        SUPPORT => 99,
+        _ => 0,
+    };
+
+    // Re snapshot: Verify that hyperlinks are created.
+    //
+    // @has - '//*[@id="associatedconstant.OPT"]' \
+    //        'const OPT: Option<Option<Ordering>> = Some(Some(Equal))'
+    // @snapshot opt - '//*[@id="associatedconstant.OPT"]//*[@class="code-header"]'
+    const OPT: Option<Option<Ordering>> = Some(Some(Ordering::Equal));
+
+    // Test that there is a depth limit. Subexpressions exceeding the maximum depth are
+    // rendered as *styled* ellipses.
+    // Re snapshot: Check that the ellipses are styled (have a certain CSS class).
+    //
+    // @has - '//*[@id="associatedconstant.DEEP0"]' \
+    //        'const DEEP0: Option<Option<Option<Ordering>>> = Some(Some(Some(…)))'
+    // @snapshot deep0 - '//*[@id="associatedconstant.DEEP0"]//*[@class="code-header"]'
+    const DEEP0: Option<Option<Option<Ordering>>> = Some(Some(Some(Ordering::Equal)));
+
+    // FIXME: Add more depth tests
+
+    // @has - '//*[@id="associatedconstant.STR0"]' \
+    //        "const STR0: &'static str = \"I want to <em>escape</em>!\""
+    const STR0: &'static str = "I want to <em>escape</em>!";
+
+    // Check that after a certain unspecified size threshold, the string contents
+    // won't be displayed anymore and that instead a *styled* series of ellipses is shown.
+    // Re snapshot: Check that the series of ellipses is styled (has a certain CSS class).
+    //
+    // @has - '//*[@id="associatedconstant.STR1"]' \
+    //        "const STR1: &'static str = \"………\""
+    // @snapshot str1 - '//*[@id="associatedconstant.STR1"]//*[@class="code-header"]'
+    const STR1: &'static str = "\
+        This is the start of a relatively long text. \
+        I might as well throw some more words into it. \
+        Informative content? Never heard of it! \
+        That's probably one of the reasons why I shouldn't be included \
+        into the generated documentation, don't you think so, too?\
+    ";
+
+    // @has - '//*[@id="associatedconstant.BYTE_STR0"]' \
+    //        "const BYTE_STR0: &'static [u8] = b\"I want to <em>escape</em>!\""
+    const BYTE_STR0: &'static [u8] = b"I want to <em>escape</em>!";
+
+    // Check that after a certain unspecified size threshold, the byte string contents
+    // won't be displayed anymore and that instead a *styled* series of ellipses is shown.
+    // Re snapshot: Check that the series of ellipses is styled (has a certain CSS class).
+    //
+    // @has - '//*[@id="associatedconstant.BYTE_STR1"]' \
+    //        "const BYTE_STR1: &'static [u8] = b\"………\""
+    // @snapshot byte-str1 - '//*[@id="associatedconstant.BYTE_STR1"]//*[@class="code-header"]'
+    const BYTE_STR1: &'static [u8] = b"\
+        AGTC CCTG GAAT TACC AAAA AACA TCCA AGTC CTCT \
+        AGTC CCTG TCCA AGTC CTCT GAAT TACC AAAA AACA \
+        AGTC CCTG GAAT TACC AAAA GGGG GGGG AGTC GTTT \
+        GGGG AACA TCCA AGTC CTCT AGTC CCTG GAAT TACC \
+        AGTC AAAA GAAT TACC CGAG AACA TCCA AGTC CTCT \
+        AGTC CCTG GAAT TACC TTCC AACA TCCA AGTC CTCT\
+    ";
+
+    // @has - '//*[@id="associatedconstant.BYTE_ARR0"]' \
+    //        'const BYTE_ARR0: [u8; 12] = *b"DEREFERENCED"'
+    const BYTE_ARR0: [u8; 12] = *b"DEREFERENCED";
+
+    // @has - '//*[@id="associatedconstant.BYTE_ARR1"]' \
+    //        "const BYTE_ARR1: [u8; 7] = *b\"MINCED\\x00\""
+    const BYTE_ARR1: [u8; 7] = [b'M', b'I', b'N', b'C', b'E', b'D', b'\0'];
+}
+
+const SUPPORT: i32 = 5;
+
+pub mod exhaustiveness {
+    // @has 'consts/exhaustiveness/constant.EXHAUSTIVE_UNIT_STRUCT.html'
+    // @has - '//*[@class="docblock item-decl"]//code' \
+    //        'const EXHAUSTIVE_UNIT_STRUCT: ExhaustiveUnitStruct = ExhaustiveUnitStruct'
+    pub const EXHAUSTIVE_UNIT_STRUCT: ExhaustiveUnitStruct = ExhaustiveUnitStruct;
+
+    // @has 'consts/exhaustiveness/constant.EXHAUSTIVE_TUPLE_STRUCT.html'
+    // @has - '//*[@class="docblock item-decl"]//code' \
+    //        'const EXHAUSTIVE_TUPLE_STRUCT: ExhaustiveTupleStruct = ExhaustiveTupleStruct(())'
+    pub const EXHAUSTIVE_TUPLE_STRUCT: ExhaustiveTupleStruct = ExhaustiveTupleStruct(());
+
+    // @has 'consts/exhaustiveness/constant.EXHAUSTIVE_STRUCT.html'
+    // @has - '//*[@class="docblock item-decl"]//code' \
+    //        'const EXHAUSTIVE_STRUCT: ExhaustiveStruct = ExhaustiveStruct { inner: () }'
+    pub const EXHAUSTIVE_STRUCT: ExhaustiveStruct = ExhaustiveStruct { inner: () };
+
+    // @has 'consts/exhaustiveness/constant.NON_EXHAUSTIVE_UNIT_STRUCT.html'
+    // @has - '//*[@class="docblock item-decl"]//code' \
+    //        'const NON_EXHAUSTIVE_UNIT_STRUCT: NonExhaustiveUnitStruct = NonExhaustiveUnitStruct { .. }'
+    pub const NON_EXHAUSTIVE_UNIT_STRUCT: NonExhaustiveUnitStruct = NonExhaustiveUnitStruct;
+
+    // @has 'consts/exhaustiveness/constant.NON_EXHAUSTIVE_TUPLE_STRUCT.html'
+    // @has - '//*[@class="docblock item-decl"]//code' \
+    //        'const NON_EXHAUSTIVE_TUPLE_STRUCT: NonExhaustiveTupleStruct = NonExhaustiveTupleStruct((), ..)'
+    pub const NON_EXHAUSTIVE_TUPLE_STRUCT: NonExhaustiveTupleStruct = NonExhaustiveTupleStruct(());
+
+    // @has 'consts/exhaustiveness/constant.NON_EXHAUSTIVE_STRUCT.html'
+    // @has - '//*[@class="docblock item-decl"]//code' \
+    //        'const NON_EXHAUSTIVE_STRUCT: NonExhaustiveStruct = NonExhaustiveStruct { inner: (), .. }'
+    pub const NON_EXHAUSTIVE_STRUCT: NonExhaustiveStruct = NonExhaustiveStruct { inner: () };
+
+    // Assert that full ranges are literally rendered as `RangeFull` and not `..` to make sure that
+    // `..` unambiguously means “omitted fields” in our pseudo-Rust expression syntax.
+    // See the comment in `render_const_value` for more details.
+    //
+    // @has 'consts/exhaustiveness/constant.RANGE_FULL.html'
+    // @has - '//*[@class="docblock item-decl"]//code' \
+    //        'const RANGE_FULL: RangeFull = RangeFull'
+    pub const RANGE_FULL: std::ops::RangeFull = ..;
+
+    pub struct ExhaustiveUnitStruct;
+    pub struct ExhaustiveTupleStruct(pub ());
+    pub struct ExhaustiveStruct { pub inner: () }
+    #[non_exhaustive] pub struct NonExhaustiveUnitStruct;
+    #[non_exhaustive] pub struct NonExhaustiveTupleStruct(pub ());
+    #[non_exhaustive] pub struct NonExhaustiveStruct { pub inner: () }
+}
diff --git a/src/test/rustdoc/const-value.sealed2.html b/src/test/rustdoc/const-value.sealed2.html
new file mode 100644
index 0000000000000..3c1c7033c0a1a
--- /dev/null
+++ b/src/test/rustdoc/const-value.sealed2.html
@@ -0,0 +1 @@
+<h4 class="code-header">pub const <a href="#associatedconstant.SEALED2" class="constant">SEALED2</a>: <a class="struct" href="{{channel}}/core/sync/atomic/struct.AtomicBool.html" title="struct core::sync::atomic::AtomicBool">AtomicBool</a> = <a class="struct" href="{{channel}}/core/sync/atomic/struct.AtomicBool.html" title="struct core::sync::atomic::AtomicBool">AtomicBool</a> { .. }</h4>
\ No newline at end of file
diff --git a/src/test/rustdoc/const-value.self.html b/src/test/rustdoc/const-value.self.html
new file mode 100644
index 0000000000000..c74e3363c4960
--- /dev/null
+++ b/src/test/rustdoc/const-value.self.html
@@ -0,0 +1 @@
+<h4 class="code-header">pub const <a href="#associatedconstant.SELF" class="constant">SELF</a>: Self = <a class="struct" href="struct.Struct.html" title="struct consts::Struct">Struct</a> { <a class="structfield" href="struct.Struct.html#structfield.public" title="field public">public</a>: (), .. }</h4>
\ No newline at end of file
diff --git a/src/test/rustdoc/const-value.str1.html b/src/test/rustdoc/const-value.str1.html
new file mode 100644
index 0000000000000..a381fdead9727
--- /dev/null
+++ b/src/test/rustdoc/const-value.str1.html
@@ -0,0 +1 @@
+<h4 class="code-header">const <a href="#associatedconstant.STR1" class="constant">STR1</a>: &amp;'static <a class="primitive" href="{{channel}}/std/primitive.str.html">str</a> = "<span class="ellipsis">&#8230;&#8230;&#8230;</span>"</h4>
\ No newline at end of file
diff --git a/src/test/rustdoc/const-value.struct.html b/src/test/rustdoc/const-value.struct.html
new file mode 100644
index 0000000000000..830247e35a63c
--- /dev/null
+++ b/src/test/rustdoc/const-value.struct.html
@@ -0,0 +1 @@
+<code>pub const STRUCT: <a class="struct" href="struct.Struct.html" title="struct consts::Struct">Struct</a> = <a class="struct" href="struct.Struct.html" title="struct consts::Struct">Struct</a> { <a class="structfield" href="struct.Struct.html#structfield.public" title="field public">public</a>: (), .. };</code>
\ No newline at end of file
diff --git a/src/test/rustdoc/const-value.sum0.html b/src/test/rustdoc/const-value.sum0.html
new file mode 100644
index 0000000000000..a60193c6bd71b
--- /dev/null
+++ b/src/test/rustdoc/const-value.sum0.html
@@ -0,0 +1 @@
+<h4 class="code-header">pub const <a href="#associatedconstant.SUM0" class="constant">SUM0</a>: <a class="enum" href="enum.Size.html" title="enum consts::Size">Size</a> = <a class="variant" href="enum.Size.html#variant.Uninhabited" title="variant consts::Size::Uninhabited">Uninhabited</a></h4>
\ No newline at end of file
diff --git a/src/test/rustdoc/const-value.sum1.html b/src/test/rustdoc/const-value.sum1.html
new file mode 100644
index 0000000000000..24abc72d545e0
--- /dev/null
+++ b/src/test/rustdoc/const-value.sum1.html
@@ -0,0 +1 @@
+<h4 class="code-header">pub const <a href="#associatedconstant.SUM1" class="constant">SUM1</a>: <a class="enum" href="enum.Size.html" title="enum consts::Size">Size</a> = <a class="variant" href="enum.Size.html#variant.Inhabited" title="variant consts::Size::Inhabited">Inhabited</a> { <a class="structfield" href="enum.Size.html#variant.Inhabited.field.inhabitants" title="field inhabitants">inhabitants</a>: 9000 }</h4>
\ No newline at end of file
diff --git a/src/test/rustdoc/const-value.sum2.html b/src/test/rustdoc/const-value.sum2.html
new file mode 100644
index 0000000000000..fda05bd3c6299
--- /dev/null
+++ b/src/test/rustdoc/const-value.sum2.html
@@ -0,0 +1 @@
+<h4 class="code-header">pub const <a href="#associatedconstant.SUM2" class="constant">SUM2</a>: <a class="enum" href="enum.Size.html" title="enum consts::Size">Size</a> = <a class="variant" href="enum.Size.html#variant.Unknown" title="variant consts::Size::Unknown">Unknown</a>(<a class="struct" href="struct.Reason.html" title="struct consts::Reason">Reason</a>)</h4>
\ No newline at end of file
diff --git a/src/test/rustdoc/const-value.tup-struct.html b/src/test/rustdoc/const-value.tup-struct.html
new file mode 100644
index 0000000000000..8ad6b4068239b
--- /dev/null
+++ b/src/test/rustdoc/const-value.tup-struct.html
@@ -0,0 +1 @@
+<h4 class="code-header">pub const <a href="#associatedconstant.TUP_STRUCT" class="constant">TUP_STRUCT</a>: <a class="struct" href="struct.TupStruct.html" title="struct consts::TupStruct">TupStruct</a> = <a class="struct" href="struct.TupStruct.html" title="struct consts::TupStruct">TupStruct</a>(_, -45, _, _)</h4>
\ No newline at end of file
diff --git a/src/test/rustdoc/hide-complex-unevaluated-consts.rs b/src/test/rustdoc/hide-complex-unevaluated-consts.rs
index ba623246a01e0..7089532f4bb38 100644
--- a/src/test/rustdoc/hide-complex-unevaluated-consts.rs
+++ b/src/test/rustdoc/hide-complex-unevaluated-consts.rs
@@ -7,6 +7,8 @@
 // Read the documentation of `rustdoc::clean::utils::print_const_expr`
 // for further details.
 
+// FIXME: This test file now partially overlaps with `const-value-display.rs`.
+
 // @has hide_complex_unevaluated_consts/trait.Container.html
 pub trait Container {
     // A helper constant that prevents const expressions containing it
@@ -18,20 +20,15 @@ pub trait Container {
     // Ensure that the private field does not get leaked:
     //
     // @has - '//*[@id="associatedconstant.STRUCT0"]' \
-    //        'const STRUCT0: Struct = _'
+    //        'const STRUCT0: Struct = Struct { .. }'
     const STRUCT0: Struct = Struct { private: () };
 
     // @has - '//*[@id="associatedconstant.STRUCT1"]' \
-    //        'const STRUCT1: (Struct,) = _'
+    //        'const STRUCT1: (Struct,) = (Struct { .. },)'
     const STRUCT1: (Struct,) = (Struct{private: /**/()},);
 
-    // Although the struct field is public here, check that it is not
-    // displayed. In a future version of rustdoc, we definitely want to
-    // show it. However for the time being, the printing logic is a bit
-    // conservative.
-    //
     // @has - '//*[@id="associatedconstant.STRUCT2"]' \
-    //        'const STRUCT2: Record = _'
+    //        'const STRUCT2: Record = Record { public: 5 }'
     const STRUCT2: Record = Record { public: 5 };
 
     // Test that we do not show the incredibly verbose match expr:
diff --git a/src/test/rustdoc/show-const-contents.rs b/src/test/rustdoc/show-const-contents.rs
index 69e742ee74739..c2f11424481c5 100644
--- a/src/test/rustdoc/show-const-contents.rs
+++ b/src/test/rustdoc/show-const-contents.rs
@@ -1,6 +1,15 @@
 // Test that the contents of constants are displayed as part of the
 // documentation.
 
+// FIXME: This test file now partially overlaps with `const-value-display.rs`.
+// FIXME: I (temporarily?) removed this “split view” for const items.
+//        The RHS used to be `<LITERAL_CONST_EXPR>; // <CONST_VALUE>` when
+//        the LITERAL_CONST_EXPR was a “literal” to
+//        preserve hexadecimal notation and numeric underscores.
+//        Personally, I've never come to like that special treatment
+//        but I can add it back in. Let me just say that this old system
+//        is quite inflexible and it doesn't scale to more complex expressions.
+
 // @hasraw show_const_contents/constant.CONST_S.html 'show this'
 // @!hasraw show_const_contents/constant.CONST_S.html '; //'
 pub const CONST_S: &'static str = "show this";
@@ -9,7 +18,7 @@ pub const CONST_S: &'static str = "show this";
 // @!hasraw show_const_contents/constant.CONST_I32.html '; //'
 pub const CONST_I32: i32 = 42;
 
-// @hasraw show_const_contents/constant.CONST_I32_HEX.html '= 0x42;'
+// @hasraw show_const_contents/constant.CONST_I32_HEX.html '= 66;'
 // @!hasraw show_const_contents/constant.CONST_I32_HEX.html '; //'
 pub const CONST_I32_HEX: i32 = 0x42;
 
@@ -17,21 +26,23 @@ pub const CONST_I32_HEX: i32 = 0x42;
 // @!hasraw show_const_contents/constant.CONST_NEG_I32.html '; //'
 pub const CONST_NEG_I32: i32 = -42;
 
-// @hasraw show_const_contents/constant.CONST_EQ_TO_VALUE_I32.html '= 42i32;'
-// @!hasraw show_const_contents/constant.CONST_EQ_TO_VALUE_I32.html '// 42i32'
+// @hasraw show_const_contents/constant.CONST_EQ_TO_VALUE_I32.html '= 42;'
+// @!hasraw show_const_contents/constant.CONST_EQ_TO_VALUE_I32.html '; //'
 pub const CONST_EQ_TO_VALUE_I32: i32 = 42i32;
 
-// @hasraw show_const_contents/constant.CONST_CALC_I32.html '= _; // 43i32'
+// @hasraw show_const_contents/constant.CONST_CALC_I32.html '= 43;'
+// @!hasraw show_const_contents/constant.CONST_CALC_I32.html '; //'
 pub const CONST_CALC_I32: i32 = 42 + 1;
 
 // @!hasraw show_const_contents/constant.CONST_REF_I32.html '= &42;'
 // @!hasraw show_const_contents/constant.CONST_REF_I32.html '; //'
 pub const CONST_REF_I32: &'static i32 = &42;
 
-// @hasraw show_const_contents/constant.CONST_I32_MAX.html '= i32::MAX; // 2_147_483_647i32'
+// @hasraw show_const_contents/constant.CONST_I32_MAX.html '= i32::MAX;'
+// @!hasraw show_const_contents/constant.CONST_REF_I32.html '; //'
 pub const CONST_I32_MAX: i32 = i32::MAX;
 
-// @!hasraw show_const_contents/constant.UNIT.html '= ();'
+// @hasraw show_const_contents/constant.UNIT.html '= ();'
 // @!hasraw show_const_contents/constant.UNIT.html '; //'
 pub const UNIT: () = ();
 
@@ -47,11 +58,14 @@ pub struct MyTypeWithStr(&'static str);
 // @!hasraw show_const_contents/constant.MY_TYPE_WITH_STR.html '; //'
 pub const MY_TYPE_WITH_STR: MyTypeWithStr = MyTypeWithStr("show this");
 
-// @hasraw show_const_contents/constant.PI.html '= 3.14159265358979323846264338327950288f32;'
-// @hasraw show_const_contents/constant.PI.html '; // 3.14159274f32'
+// FIXME: Hmm, that's bothersome :(
+// @hasraw show_const_contents/constant.PI.html '= 3.14159274;'
+// @!hasraw show_const_contents/constant.PI.html '; //'
 pub use std::f32::consts::PI;
 
-// @hasraw show_const_contents/constant.MAX.html '= i32::MAX; // 2_147_483_647i32'
+// FIXME: This is also quite sad (concrete value not shown anymore).
+// @hasraw show_const_contents/constant.MAX.html '= i32::MAX;'
+// @!hasraw show_const_contents/constant.PI.html '; //'
 #[allow(deprecated, deprecated_in_future)]
 pub use std::i32::MAX;
 
@@ -61,8 +75,9 @@ macro_rules! int_module {
     )
 }
 
-// @hasraw show_const_contents/constant.MIN.html '= i16::MIN; // -32_768i16'
+// @hasraw show_const_contents/constant.MIN.html '= i16::MIN;'
+// @!hasraw show_const_contents/constant.MIN.html '; //'
 int_module!(i16);
 
-// @has show_const_contents/constant.ESCAPE.html //pre '= r#"<script>alert("ESCAPE");</script>"#;'
+// @has show_const_contents/constant.ESCAPE.html //code '= "<script>alert(\"ESCAPE\");</script>";'
 pub const ESCAPE: &str = r#"<script>alert("ESCAPE");</script>"#;