about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--compiler/rustc_lexer/src/lib.rs34
-rw-r--r--compiler/rustc_mir_transform/src/coverage/expansion.rs127
-rw-r--r--compiler/rustc_mir_transform/src/coverage/mod.rs1
-rw-r--r--compiler/rustc_mir_transform/src/coverage/spans.rs142
-rw-r--r--compiler/rustc_mir_transform/src/coverage/spans/from_mir.rs34
-rw-r--r--compiler/rustc_mir_transform/src/coverage/unexpand.rs48
-rw-r--r--compiler/rustc_mir_transform/src/lib.rs1
-rw-r--r--library/coretests/tests/floats/f128.rs64
-rw-r--r--library/coretests/tests/floats/f16.rs60
-rw-r--r--library/coretests/tests/floats/f32.rs64
-rw-r--r--library/coretests/tests/floats/f64.rs58
-rw-r--r--library/coretests/tests/floats/mod.rs102
-rw-r--r--src/tools/compiletest/src/executor.rs13
-rw-r--r--src/tools/compiletest/src/lib.rs3
-rw-r--r--src/tools/compiletest/src/panic_hook.rs136
-rw-r--r--src/tools/nix-dev-shell/shell.nix3
-rw-r--r--tests/coverage/closure.cov-map35
-rw-r--r--tests/coverage/closure.coverage6
-rw-r--r--tests/coverage/macro_in_closure.cov-map7
-rw-r--r--tests/coverage/rustfmt-skip.cov-map12
-rw-r--r--tests/coverage/rustfmt-skip.coverage18
-rw-r--r--tests/coverage/rustfmt-skip.rs17
-rw-r--r--tests/ui/frontmatter/frontmatter-whitespace-1.rs2
-rw-r--r--tests/ui/frontmatter/frontmatter-whitespace-1.stderr18
-rw-r--r--tests/ui/frontmatter/frontmatter-whitespace-2.rs5
-rw-r--r--tests/ui/frontmatter/frontmatter-whitespace-2.stderr38
-rw-r--r--tests/ui/frontmatter/multifrontmatter-2.rs6
-rw-r--r--tests/ui/frontmatter/multifrontmatter-2.stderr22
28 files changed, 597 insertions, 479 deletions
diff --git a/compiler/rustc_lexer/src/lib.rs b/compiler/rustc_lexer/src/lib.rs
index 483cc3e93dc..d10b1920343 100644
--- a/compiler/rustc_lexer/src/lib.rs
+++ b/compiler/rustc_lexer/src/lib.rs
@@ -550,28 +550,20 @@ impl Cursor<'_> {
         self.eat_while(|ch| ch != '\n' && is_whitespace(ch));
         let invalid_infostring = self.first() != '\n';
 
-        let mut s = self.as_str();
         let mut found = false;
-        let mut size = 0;
-        while let Some(closing) = s.find(&"-".repeat(length_opening as usize)) {
-            let preceding_chars_start = s[..closing].rfind("\n").map_or(0, |i| i + 1);
-            if s[preceding_chars_start..closing].chars().all(is_whitespace) {
-                // candidate found
-                self.bump_bytes(size + closing);
-                // in case like
-                // ---cargo
-                // --- blahblah
-                // or
-                // ---cargo
-                // ----
-                // combine those stuff into this frontmatter token such that it gets detected later.
-                self.eat_until(b'\n');
-                found = true;
-                break;
-            } else {
-                s = &s[closing + length_opening as usize..];
-                size += closing + length_opening as usize;
-            }
+        let nl_fence_pattern = format!("\n{:-<1$}", "", length_opening as usize);
+        if let Some(closing) = self.as_str().find(&nl_fence_pattern) {
+            // candidate found
+            self.bump_bytes(closing + nl_fence_pattern.len());
+            // in case like
+            // ---cargo
+            // --- blahblah
+            // or
+            // ---cargo
+            // ----
+            // combine those stuff into this frontmatter token such that it gets detected later.
+            self.eat_until(b'\n');
+            found = true;
         }
 
         if !found {
diff --git a/compiler/rustc_mir_transform/src/coverage/expansion.rs b/compiler/rustc_mir_transform/src/coverage/expansion.rs
new file mode 100644
index 00000000000..91e0528f52f
--- /dev/null
+++ b/compiler/rustc_mir_transform/src/coverage/expansion.rs
@@ -0,0 +1,127 @@
+use rustc_data_structures::fx::{FxIndexMap, FxIndexSet, IndexEntry};
+use rustc_middle::mir::coverage::BasicCoverageBlock;
+use rustc_span::{ExpnId, ExpnKind, Span};
+
+#[derive(Clone, Copy, Debug)]
+pub(crate) struct SpanWithBcb {
+    pub(crate) span: Span,
+    pub(crate) bcb: BasicCoverageBlock,
+}
+
+#[derive(Debug)]
+pub(crate) struct ExpnTree {
+    nodes: FxIndexMap<ExpnId, ExpnNode>,
+}
+
+impl ExpnTree {
+    pub(crate) fn get(&self, expn_id: ExpnId) -> Option<&ExpnNode> {
+        self.nodes.get(&expn_id)
+    }
+
+    /// Yields the tree node for the given expansion ID (if present), followed
+    /// by the nodes of all of its descendants in depth-first order.
+    pub(crate) fn iter_node_and_descendants(
+        &self,
+        root_expn_id: ExpnId,
+    ) -> impl Iterator<Item = &ExpnNode> {
+        gen move {
+            let Some(root_node) = self.get(root_expn_id) else { return };
+            yield root_node;
+
+            // Stack of child-node-ID iterators that drives the depth-first traversal.
+            let mut iter_stack = vec![root_node.child_expn_ids.iter()];
+
+            while let Some(curr_iter) = iter_stack.last_mut() {
+                // Pull the next ID from the top of the stack.
+                let Some(&curr_id) = curr_iter.next() else {
+                    iter_stack.pop();
+                    continue;
+                };
+
+                // Yield this node.
+                let Some(node) = self.get(curr_id) else { continue };
+                yield node;
+
+                // Push the node's children, to be traversed next.
+                if !node.child_expn_ids.is_empty() {
+                    iter_stack.push(node.child_expn_ids.iter());
+                }
+            }
+        }
+    }
+}
+
+#[derive(Debug)]
+pub(crate) struct ExpnNode {
+    /// Storing the expansion ID in its own node is not strictly necessary,
+    /// but is helpful for debugging and might be useful later.
+    #[expect(dead_code)]
+    pub(crate) expn_id: ExpnId,
+
+    // Useful info extracted from `ExpnData`.
+    pub(crate) expn_kind: ExpnKind,
+    /// Non-dummy `ExpnData::call_site` span.
+    pub(crate) call_site: Option<Span>,
+    /// Expansion ID of `call_site`, if present.
+    /// This links an expansion node to its parent in the tree.
+    pub(crate) call_site_expn_id: Option<ExpnId>,
+
+    /// Spans (and their associated BCBs) belonging to this expansion.
+    pub(crate) spans: Vec<SpanWithBcb>,
+    /// Expansions whose call-site is in this expansion.
+    pub(crate) child_expn_ids: FxIndexSet<ExpnId>,
+}
+
+impl ExpnNode {
+    fn new(expn_id: ExpnId) -> Self {
+        let expn_data = expn_id.expn_data();
+
+        let call_site = Some(expn_data.call_site).filter(|sp| !sp.is_dummy());
+        let call_site_expn_id = try { call_site?.ctxt().outer_expn() };
+
+        Self {
+            expn_id,
+
+            expn_kind: expn_data.kind.clone(),
+            call_site,
+            call_site_expn_id,
+
+            spans: vec![],
+            child_expn_ids: FxIndexSet::default(),
+        }
+    }
+}
+
+/// Given a collection of span/BCB pairs from potentially-different syntax contexts,
+/// arranges them into an "expansion tree" based on their expansion call-sites.
+pub(crate) fn build_expn_tree(spans: impl IntoIterator<Item = SpanWithBcb>) -> ExpnTree {
+    let mut nodes = FxIndexMap::default();
+    let new_node = |&expn_id: &ExpnId| ExpnNode::new(expn_id);
+
+    for span_with_bcb in spans {
+        // Create a node for this span's enclosing expansion, and add the span to it.
+        let expn_id = span_with_bcb.span.ctxt().outer_expn();
+        let node = nodes.entry(expn_id).or_insert_with_key(new_node);
+        node.spans.push(span_with_bcb);
+
+        // Now walk up the expansion call-site chain, creating nodes and registering children.
+        let mut prev = expn_id;
+        let mut curr_expn_id = node.call_site_expn_id;
+        while let Some(expn_id) = curr_expn_id {
+            let entry = nodes.entry(expn_id);
+            let node_existed = matches!(entry, IndexEntry::Occupied(_));
+
+            let node = entry.or_insert_with_key(new_node);
+            node.child_expn_ids.insert(prev);
+
+            if node_existed {
+                break;
+            }
+
+            prev = expn_id;
+            curr_expn_id = node.call_site_expn_id;
+        }
+    }
+
+    ExpnTree { nodes }
+}
diff --git a/compiler/rustc_mir_transform/src/coverage/mod.rs b/compiler/rustc_mir_transform/src/coverage/mod.rs
index c5fef299244..08c7d346009 100644
--- a/compiler/rustc_mir_transform/src/coverage/mod.rs
+++ b/compiler/rustc_mir_transform/src/coverage/mod.rs
@@ -8,6 +8,7 @@ use crate::coverage::graph::CoverageGraph;
 use crate::coverage::mappings::ExtractedMappings;
 
 mod counters;
+mod expansion;
 mod graph;
 mod hir_info;
 mod mappings;
diff --git a/compiler/rustc_mir_transform/src/coverage/spans.rs b/compiler/rustc_mir_transform/src/coverage/spans.rs
index d1b04c8f587..325935ee846 100644
--- a/compiler/rustc_mir_transform/src/coverage/spans.rs
+++ b/compiler/rustc_mir_transform/src/coverage/spans.rs
@@ -1,15 +1,14 @@
-use rustc_data_structures::fx::FxHashSet;
 use rustc_middle::mir;
 use rustc_middle::mir::coverage::{Mapping, MappingKind, START_BCB};
 use rustc_middle::ty::TyCtxt;
 use rustc_span::source_map::SourceMap;
-use rustc_span::{BytePos, DesugaringKind, ExpnKind, MacroKind, Span};
+use rustc_span::{BytePos, DesugaringKind, ExpnId, ExpnKind, MacroKind, Span};
 use tracing::instrument;
 
+use crate::coverage::expansion::{self, ExpnTree, SpanWithBcb};
 use crate::coverage::graph::{BasicCoverageBlock, CoverageGraph};
 use crate::coverage::hir_info::ExtractedHirInfo;
-use crate::coverage::spans::from_mir::{Hole, RawSpanFromMir, SpanFromMir};
-use crate::coverage::unexpand;
+use crate::coverage::spans::from_mir::{Hole, RawSpanFromMir};
 
 mod from_mir;
 
@@ -34,19 +33,51 @@ pub(super) fn extract_refined_covspans<'tcx>(
     let &ExtractedHirInfo { body_span, .. } = hir_info;
 
     let raw_spans = from_mir::extract_raw_spans_from_mir(mir_body, graph);
-    let mut covspans = raw_spans
-        .into_iter()
-        .filter_map(|RawSpanFromMir { raw_span, bcb }| try {
-            let (span, expn_kind) =
-                unexpand::unexpand_into_body_span_with_expn_kind(raw_span, body_span)?;
-            // Discard any spans that fill the entire body, because they tend
-            // to represent compiler-inserted code, e.g. implicitly returning `()`.
-            if span.source_equal(body_span) {
-                return None;
-            };
-            SpanFromMir { span, expn_kind, bcb }
-        })
-        .collect::<Vec<_>>();
+    // Use the raw spans to build a tree of expansions for this function.
+    let expn_tree = expansion::build_expn_tree(
+        raw_spans
+            .into_iter()
+            .map(|RawSpanFromMir { raw_span, bcb }| SpanWithBcb { span: raw_span, bcb }),
+    );
+
+    let mut covspans = vec![];
+    let mut push_covspan = |covspan: Covspan| {
+        let covspan_span = covspan.span;
+        // Discard any spans not contained within the function body span.
+        // Also discard any spans that fill the entire body, because they tend
+        // to represent compiler-inserted code, e.g. implicitly returning `()`.
+        if !body_span.contains(covspan_span) || body_span.source_equal(covspan_span) {
+            return;
+        }
+
+        // Each pushed covspan should have the same context as the body span.
+        // If it somehow doesn't, discard the covspan, or panic in debug builds.
+        if !body_span.eq_ctxt(covspan_span) {
+            debug_assert!(
+                false,
+                "span context mismatch: body_span={body_span:?}, covspan.span={covspan_span:?}"
+            );
+            return;
+        }
+
+        covspans.push(covspan);
+    };
+
+    if let Some(node) = expn_tree.get(body_span.ctxt().outer_expn()) {
+        for &SpanWithBcb { span, bcb } in &node.spans {
+            push_covspan(Covspan { span, bcb });
+        }
+
+        // For each expansion with its call-site in the body span, try to
+        // distill a corresponding covspan.
+        for &child_expn_id in &node.child_expn_ids {
+            if let Some(covspan) =
+                single_covspan_for_child_expn(tcx, graph, &expn_tree, child_expn_id)
+            {
+                push_covspan(covspan);
+            }
+        }
+    }
 
     // Only proceed if we found at least one usable span.
     if covspans.is_empty() {
@@ -57,17 +88,10 @@ pub(super) fn extract_refined_covspans<'tcx>(
     // Otherwise, add a fake span at the start of the body, to avoid an ugly
     // gap between the start of the body and the first real span.
     // FIXME: Find a more principled way to solve this problem.
-    covspans.push(SpanFromMir::for_fn_sig(
-        hir_info.fn_sig_span.unwrap_or_else(|| body_span.shrink_to_lo()),
-    ));
-
-    // First, perform the passes that need macro information.
-    covspans.sort_by(|a, b| graph.cmp_in_dominator_order(a.bcb, b.bcb));
-    remove_unwanted_expansion_spans(&mut covspans);
-    shrink_visible_macro_spans(tcx, &mut covspans);
-
-    // We no longer need the extra information in `SpanFromMir`, so convert to `Covspan`.
-    let mut covspans = covspans.into_iter().map(SpanFromMir::into_covspan).collect::<Vec<_>>();
+    covspans.push(Covspan {
+        span: hir_info.fn_sig_span.unwrap_or_else(|| body_span.shrink_to_lo()),
+        bcb: START_BCB,
+    });
 
     let compare_covspans = |a: &Covspan, b: &Covspan| {
         compare_spans(a.span, b.span)
@@ -117,43 +141,37 @@ pub(super) fn extract_refined_covspans<'tcx>(
     }));
 }
 
-/// Macros that expand into branches (e.g. `assert!`, `trace!`) tend to generate
-/// multiple condition/consequent blocks that have the span of the whole macro
-/// invocation, which is unhelpful. Keeping only the first such span seems to
-/// give better mappings, so remove the others.
-///
-/// Similarly, `await` expands to a branch on the discriminant of `Poll`, which
-/// leads to incorrect coverage if the `Future` is immediately ready (#98712).
-///
-/// (The input spans should be sorted in BCB dominator order, so that the
-/// retained "first" span is likely to dominate the others.)
-fn remove_unwanted_expansion_spans(covspans: &mut Vec<SpanFromMir>) {
-    let mut deduplicated_spans = FxHashSet::default();
-
-    covspans.retain(|covspan| {
-        match covspan.expn_kind {
-            // Retain only the first await-related or macro-expanded covspan with this span.
-            Some(ExpnKind::Desugaring(DesugaringKind::Await)) => {
-                deduplicated_spans.insert(covspan.span)
-            }
-            Some(ExpnKind::Macro(MacroKind::Bang, _)) => deduplicated_spans.insert(covspan.span),
-            // Ignore (retain) other spans.
-            _ => true,
+/// For a single child expansion, try to distill it into a single span+BCB mapping.
+fn single_covspan_for_child_expn(
+    tcx: TyCtxt<'_>,
+    graph: &CoverageGraph,
+    expn_tree: &ExpnTree,
+    expn_id: ExpnId,
+) -> Option<Covspan> {
+    let node = expn_tree.get(expn_id)?;
+
+    let bcbs =
+        expn_tree.iter_node_and_descendants(expn_id).flat_map(|n| n.spans.iter().map(|s| s.bcb));
+
+    let bcb = match node.expn_kind {
+        // For bang-macros (e.g. `assert!`, `trace!`) and for `await`, taking
+        // the "first" BCB in dominator order seems to give good results.
+        ExpnKind::Macro(MacroKind::Bang, _) | ExpnKind::Desugaring(DesugaringKind::Await) => {
+            bcbs.min_by(|&a, &b| graph.cmp_in_dominator_order(a, b))?
         }
-    });
-}
-
-/// When a span corresponds to a macro invocation that is visible from the
-/// function body, truncate it to just the macro name plus `!`.
-/// This seems to give better results for code that uses macros.
-fn shrink_visible_macro_spans(tcx: TyCtxt<'_>, covspans: &mut Vec<SpanFromMir>) {
-    let source_map = tcx.sess.source_map();
+        // For other kinds of expansion, taking the "last" (most-dominated) BCB
+        // seems to give good results.
+        _ => bcbs.max_by(|&a, &b| graph.cmp_in_dominator_order(a, b))?,
+    };
 
-    for covspan in covspans {
-        if matches!(covspan.expn_kind, Some(ExpnKind::Macro(MacroKind::Bang, _))) {
-            covspan.span = source_map.span_through_char(covspan.span, '!');
-        }
+    // For bang-macro expansions, limit the call-site span to just the macro
+    // name plus `!`, excluding the macro arguments.
+    let mut span = node.call_site?;
+    if matches!(node.expn_kind, ExpnKind::Macro(MacroKind::Bang, _)) {
+        span = tcx.sess.source_map().span_through_char(span, '!');
     }
+
+    Some(Covspan { span, bcb })
 }
 
 /// Discard all covspans that overlap a hole.
diff --git a/compiler/rustc_mir_transform/src/coverage/spans/from_mir.rs b/compiler/rustc_mir_transform/src/coverage/spans/from_mir.rs
index 7985e1c0798..dfeaa90dc2e 100644
--- a/compiler/rustc_mir_transform/src/coverage/spans/from_mir.rs
+++ b/compiler/rustc_mir_transform/src/coverage/spans/from_mir.rs
@@ -5,10 +5,9 @@ use rustc_middle::mir::coverage::CoverageKind;
 use rustc_middle::mir::{
     self, FakeReadCause, Statement, StatementKind, Terminator, TerminatorKind,
 };
-use rustc_span::{ExpnKind, Span};
+use rustc_span::Span;
 
-use crate::coverage::graph::{BasicCoverageBlock, CoverageGraph, START_BCB};
-use crate::coverage::spans::Covspan;
+use crate::coverage::graph::{BasicCoverageBlock, CoverageGraph};
 
 #[derive(Debug)]
 pub(crate) struct RawSpanFromMir {
@@ -160,32 +159,3 @@ impl Hole {
         true
     }
 }
-
-#[derive(Debug)]
-pub(crate) struct SpanFromMir {
-    /// A span that has been extracted from MIR and then "un-expanded" back to
-    /// within the current function's `body_span`. After various intermediate
-    /// processing steps, this span is emitted as part of the final coverage
-    /// mappings.
-    ///
-    /// With the exception of `fn_sig_span`, this should always be contained
-    /// within `body_span`.
-    pub(crate) span: Span,
-    pub(crate) expn_kind: Option<ExpnKind>,
-    pub(crate) bcb: BasicCoverageBlock,
-}
-
-impl SpanFromMir {
-    pub(crate) fn for_fn_sig(fn_sig_span: Span) -> Self {
-        Self::new(fn_sig_span, None, START_BCB)
-    }
-
-    pub(crate) fn new(span: Span, expn_kind: Option<ExpnKind>, bcb: BasicCoverageBlock) -> Self {
-        Self { span, expn_kind, bcb }
-    }
-
-    pub(crate) fn into_covspan(self) -> Covspan {
-        let Self { span, expn_kind: _, bcb } = self;
-        Covspan { span, bcb }
-    }
-}
diff --git a/compiler/rustc_mir_transform/src/coverage/unexpand.rs b/compiler/rustc_mir_transform/src/coverage/unexpand.rs
index cb861544736..922edd3cc4f 100644
--- a/compiler/rustc_mir_transform/src/coverage/unexpand.rs
+++ b/compiler/rustc_mir_transform/src/coverage/unexpand.rs
@@ -1,4 +1,4 @@
-use rustc_span::{ExpnKind, Span};
+use rustc_span::Span;
 
 /// Walks through the expansion ancestors of `original_span` to find a span that
 /// is contained in `body_span` and has the same [syntax context] as `body_span`.
@@ -7,49 +7,3 @@ pub(crate) fn unexpand_into_body_span(original_span: Span, body_span: Span) -> O
     // we can just delegate directly to `find_ancestor_inside_same_ctxt`.
     original_span.find_ancestor_inside_same_ctxt(body_span)
 }
-
-/// Walks through the expansion ancestors of `original_span` to find a span that
-/// is contained in `body_span` and has the same [syntax context] as `body_span`.
-///
-/// If the returned span represents a bang-macro invocation (e.g. `foo!(..)`),
-/// the returned symbol will be the name of that macro (e.g. `foo`).
-pub(crate) fn unexpand_into_body_span_with_expn_kind(
-    original_span: Span,
-    body_span: Span,
-) -> Option<(Span, Option<ExpnKind>)> {
-    let (span, prev) = unexpand_into_body_span_with_prev(original_span, body_span)?;
-
-    let expn_kind = prev.map(|prev| prev.ctxt().outer_expn_data().kind);
-
-    Some((span, expn_kind))
-}
-
-/// Walks through the expansion ancestors of `original_span` to find a span that
-/// is contained in `body_span` and has the same [syntax context] as `body_span`.
-/// The ancestor that was traversed just before the matching span (if any) is
-/// also returned.
-///
-/// For example, a return value of `Some((ancestor, Some(prev)))` means that:
-/// - `ancestor == original_span.find_ancestor_inside_same_ctxt(body_span)`
-/// - `prev.parent_callsite() == ancestor`
-///
-/// [syntax context]: rustc_span::SyntaxContext
-fn unexpand_into_body_span_with_prev(
-    original_span: Span,
-    body_span: Span,
-) -> Option<(Span, Option<Span>)> {
-    let mut prev = None;
-    let mut curr = original_span;
-
-    while !body_span.contains(curr) || !curr.eq_ctxt(body_span) {
-        prev = Some(curr);
-        curr = curr.parent_callsite()?;
-    }
-
-    debug_assert_eq!(Some(curr), original_span.find_ancestor_inside_same_ctxt(body_span));
-    if let Some(prev) = prev {
-        debug_assert_eq!(Some(curr), prev.parent_callsite());
-    }
-
-    Some((curr, prev))
-}
diff --git a/compiler/rustc_mir_transform/src/lib.rs b/compiler/rustc_mir_transform/src/lib.rs
index 08f25276cec..8f319e64916 100644
--- a/compiler/rustc_mir_transform/src/lib.rs
+++ b/compiler/rustc_mir_transform/src/lib.rs
@@ -5,6 +5,7 @@
 #![feature(const_type_name)]
 #![feature(cow_is_borrowed)]
 #![feature(file_buffered)]
+#![feature(gen_blocks)]
 #![feature(if_let_guard)]
 #![feature(impl_trait_in_assoc_type)]
 #![feature(try_blocks)]
diff --git a/library/coretests/tests/floats/f128.rs b/library/coretests/tests/floats/f128.rs
index ac4a2066530..c173d7f0ae0 100644
--- a/library/coretests/tests/floats/f128.rs
+++ b/library/coretests/tests/floats/f128.rs
@@ -1,18 +1,18 @@
 // FIXME(f16_f128): only tested on platforms that have symbols and aren't buggy
 #![cfg(target_has_reliable_f128)]
 
-use std::f128::consts;
-
 use super::{assert_approx_eq, assert_biteq};
 
 // Note these tolerances make sense around zero, but not for more extreme exponents.
 
 /// Default tolerances. Works for values that should be near precise but not exact. Roughly
 /// the precision carried by `100 * 100`.
+#[allow(unused)]
 const TOL: f128 = 1e-12;
 
 /// For operations that are near exact, usually not involving math of different
 /// signs.
+#[allow(unused)]
 const TOL_PRECISE: f128 = 1e-28;
 
 /// First pattern over the mantissa
@@ -44,70 +44,12 @@ fn test_mul_add() {
 
 #[test]
 #[cfg(any(miri, target_has_reliable_f128_math))]
-fn test_recip() {
-    let nan: f128 = f128::NAN;
-    let inf: f128 = f128::INFINITY;
-    let neg_inf: f128 = f128::NEG_INFINITY;
-    assert_biteq!(1.0f128.recip(), 1.0);
-    assert_biteq!(2.0f128.recip(), 0.5);
-    assert_biteq!((-0.4f128).recip(), -2.5);
-    assert_biteq!(0.0f128.recip(), inf);
+fn test_max_recip() {
     assert_approx_eq!(
         f128::MAX.recip(),
         8.40525785778023376565669454330438228902076605e-4933,
         1e-4900
     );
-    assert!(nan.recip().is_nan());
-    assert_biteq!(inf.recip(), 0.0);
-    assert_biteq!(neg_inf.recip(), -0.0);
-}
-
-#[test]
-#[cfg(not(miri))]
-#[cfg(target_has_reliable_f128_math)]
-fn test_powi() {
-    let nan: f128 = f128::NAN;
-    let inf: f128 = f128::INFINITY;
-    let neg_inf: f128 = f128::NEG_INFINITY;
-    assert_biteq!(1.0f128.powi(1), 1.0);
-    assert_approx_eq!((-3.1f128).powi(2), 9.6100000000000005506706202140776519387, TOL);
-    assert_approx_eq!(5.9f128.powi(-2), 0.028727377190462507313100483690639638451, TOL);
-    assert_biteq!(8.3f128.powi(0), 1.0);
-    assert!(nan.powi(2).is_nan());
-    assert_biteq!(inf.powi(3), inf);
-    assert_biteq!(neg_inf.powi(2), inf);
-}
-
-#[test]
-fn test_to_degrees() {
-    let pi: f128 = consts::PI;
-    let nan: f128 = f128::NAN;
-    let inf: f128 = f128::INFINITY;
-    let neg_inf: f128 = f128::NEG_INFINITY;
-    assert_biteq!(0.0f128.to_degrees(), 0.0);
-    assert_approx_eq!((-5.8f128).to_degrees(), -332.31552117587745090765431723855668471, TOL);
-    assert_approx_eq!(pi.to_degrees(), 180.0, TOL);
-    assert!(nan.to_degrees().is_nan());
-    assert_biteq!(inf.to_degrees(), inf);
-    assert_biteq!(neg_inf.to_degrees(), neg_inf);
-    assert_biteq!(1_f128.to_degrees(), 57.2957795130823208767981548141051703);
-}
-
-#[test]
-fn test_to_radians() {
-    let pi: f128 = consts::PI;
-    let nan: f128 = f128::NAN;
-    let inf: f128 = f128::INFINITY;
-    let neg_inf: f128 = f128::NEG_INFINITY;
-    assert_biteq!(0.0f128.to_radians(), 0.0);
-    assert_approx_eq!(154.6f128.to_radians(), 2.6982790235832334267135442069489767804, TOL);
-    assert_approx_eq!((-332.31f128).to_radians(), -5.7999036373023566567593094812182763013, TOL);
-    // check approx rather than exact because round trip for pi doesn't fall on an exactly
-    // representable value (unlike `f32` and `f64`).
-    assert_approx_eq!(180.0f128.to_radians(), pi, TOL_PRECISE);
-    assert!(nan.to_radians().is_nan());
-    assert_biteq!(inf.to_radians(), inf);
-    assert_biteq!(neg_inf.to_radians(), neg_inf);
 }
 
 #[test]
diff --git a/library/coretests/tests/floats/f16.rs b/library/coretests/tests/floats/f16.rs
index bb9c8a002fe..c12de7221ba 100644
--- a/library/coretests/tests/floats/f16.rs
+++ b/library/coretests/tests/floats/f16.rs
@@ -1,8 +1,6 @@
 // FIXME(f16_f128): only tested on platforms that have symbols and aren't buggy
 #![cfg(target_has_reliable_f16)]
 
-use std::f16::consts;
-
 use super::{assert_approx_eq, assert_biteq};
 
 /// Tolerance for results on the order of 10.0e-2
@@ -50,64 +48,8 @@ fn test_mul_add() {
 
 #[test]
 #[cfg(any(miri, target_has_reliable_f16_math))]
-fn test_recip() {
-    let nan: f16 = f16::NAN;
-    let inf: f16 = f16::INFINITY;
-    let neg_inf: f16 = f16::NEG_INFINITY;
-    assert_biteq!(1.0f16.recip(), 1.0);
-    assert_biteq!(2.0f16.recip(), 0.5);
-    assert_biteq!((-0.4f16).recip(), -2.5);
-    assert_biteq!(0.0f16.recip(), inf);
+fn test_max_recip() {
     assert_approx_eq!(f16::MAX.recip(), 1.526624e-5f16, 1e-4);
-    assert!(nan.recip().is_nan());
-    assert_biteq!(inf.recip(), 0.0);
-    assert_biteq!(neg_inf.recip(), -0.0);
-}
-
-#[test]
-#[cfg(not(miri))]
-#[cfg(target_has_reliable_f16_math)]
-fn test_powi() {
-    let nan: f16 = f16::NAN;
-    let inf: f16 = f16::INFINITY;
-    let neg_inf: f16 = f16::NEG_INFINITY;
-    assert_biteq!(1.0f16.powi(1), 1.0);
-    assert_approx_eq!((-3.1f16).powi(2), 9.61, TOL_0);
-    assert_approx_eq!(5.9f16.powi(-2), 0.028727, TOL_N2);
-    assert_biteq!(8.3f16.powi(0), 1.0);
-    assert!(nan.powi(2).is_nan());
-    assert_biteq!(inf.powi(3), inf);
-    assert_biteq!(neg_inf.powi(2), inf);
-}
-
-#[test]
-fn test_to_degrees() {
-    let pi: f16 = consts::PI;
-    let nan: f16 = f16::NAN;
-    let inf: f16 = f16::INFINITY;
-    let neg_inf: f16 = f16::NEG_INFINITY;
-    assert_biteq!(0.0f16.to_degrees(), 0.0);
-    assert_approx_eq!((-5.8f16).to_degrees(), -332.315521, TOL_P2);
-    assert_approx_eq!(pi.to_degrees(), 180.0, TOL_P2);
-    assert!(nan.to_degrees().is_nan());
-    assert_biteq!(inf.to_degrees(), inf);
-    assert_biteq!(neg_inf.to_degrees(), neg_inf);
-    assert_biteq!(1_f16.to_degrees(), 57.2957795130823208767981548141051703);
-}
-
-#[test]
-fn test_to_radians() {
-    let pi: f16 = consts::PI;
-    let nan: f16 = f16::NAN;
-    let inf: f16 = f16::INFINITY;
-    let neg_inf: f16 = f16::NEG_INFINITY;
-    assert_biteq!(0.0f16.to_radians(), 0.0);
-    assert_approx_eq!(154.6f16.to_radians(), 2.698279, TOL_0);
-    assert_approx_eq!((-332.31f16).to_radians(), -5.799903, TOL_0);
-    assert_approx_eq!(180.0f16.to_radians(), pi, TOL_0);
-    assert!(nan.to_radians().is_nan());
-    assert_biteq!(inf.to_radians(), inf);
-    assert_biteq!(neg_inf.to_radians(), neg_inf);
 }
 
 #[test]
diff --git a/library/coretests/tests/floats/f32.rs b/library/coretests/tests/floats/f32.rs
index e77e44655dc..b79295f470d 100644
--- a/library/coretests/tests/floats/f32.rs
+++ b/library/coretests/tests/floats/f32.rs
@@ -1,5 +1,4 @@
 use core::f32;
-use core::f32::consts;
 
 use super::{assert_approx_eq, assert_biteq};
 
@@ -9,11 +8,6 @@ const NAN_MASK1: u32 = 0x002a_aaaa;
 /// Second pattern over the mantissa
 const NAN_MASK2: u32 = 0x0055_5555;
 
-/// Miri adds some extra errors to float functions; make sure the tests still pass.
-/// These values are purely used as a canary to test against and are thus not a stable guarantee Rust provides.
-/// They serve as a way to get an idea of the real precision of floating point operations on different platforms.
-const APPROX_DELTA: f32 = if cfg!(miri) { 1e-4 } else { 1e-6 };
-
 // FIXME(#140515): mingw has an incorrect fma https://sourceforge.net/p/mingw-w64/bugs/848/
 #[cfg_attr(all(target_os = "windows", target_env = "gnu", not(target_abi = "llvm")), ignore)]
 #[test]
@@ -33,64 +27,6 @@ fn test_mul_add() {
 }
 
 #[test]
-fn test_recip() {
-    let nan: f32 = f32::NAN;
-    let inf: f32 = f32::INFINITY;
-    let neg_inf: f32 = f32::NEG_INFINITY;
-    assert_biteq!(1.0f32.recip(), 1.0);
-    assert_biteq!(2.0f32.recip(), 0.5);
-    assert_biteq!((-0.4f32).recip(), -2.5);
-    assert_biteq!(0.0f32.recip(), inf);
-    assert!(nan.recip().is_nan());
-    assert_biteq!(inf.recip(), 0.0);
-    assert_biteq!(neg_inf.recip(), -0.0);
-}
-
-#[test]
-fn test_powi() {
-    let nan: f32 = f32::NAN;
-    let inf: f32 = f32::INFINITY;
-    let neg_inf: f32 = f32::NEG_INFINITY;
-    assert_approx_eq!(1.0f32.powi(1), 1.0);
-    assert_approx_eq!((-3.1f32).powi(2), 9.61, APPROX_DELTA);
-    assert_approx_eq!(5.9f32.powi(-2), 0.028727);
-    assert_biteq!(8.3f32.powi(0), 1.0);
-    assert!(nan.powi(2).is_nan());
-    assert_biteq!(inf.powi(3), inf);
-    assert_biteq!(neg_inf.powi(2), inf);
-}
-
-#[test]
-fn test_to_degrees() {
-    let pi: f32 = consts::PI;
-    let nan: f32 = f32::NAN;
-    let inf: f32 = f32::INFINITY;
-    let neg_inf: f32 = f32::NEG_INFINITY;
-    assert_biteq!(0.0f32.to_degrees(), 0.0);
-    assert_approx_eq!((-5.8f32).to_degrees(), -332.315521);
-    assert_biteq!(pi.to_degrees(), 180.0);
-    assert!(nan.to_degrees().is_nan());
-    assert_biteq!(inf.to_degrees(), inf);
-    assert_biteq!(neg_inf.to_degrees(), neg_inf);
-    assert_biteq!(1_f32.to_degrees(), 57.2957795130823208767981548141051703);
-}
-
-#[test]
-fn test_to_radians() {
-    let pi: f32 = consts::PI;
-    let nan: f32 = f32::NAN;
-    let inf: f32 = f32::INFINITY;
-    let neg_inf: f32 = f32::NEG_INFINITY;
-    assert_biteq!(0.0f32.to_radians(), 0.0);
-    assert_approx_eq!(154.6f32.to_radians(), 2.698279);
-    assert_approx_eq!((-332.31f32).to_radians(), -5.799903);
-    assert_biteq!(180.0f32.to_radians(), pi);
-    assert!(nan.to_radians().is_nan());
-    assert_biteq!(inf.to_radians(), inf);
-    assert_biteq!(neg_inf.to_radians(), neg_inf);
-}
-
-#[test]
 fn test_float_bits_conv() {
     assert_eq!((1f32).to_bits(), 0x3f800000);
     assert_eq!((12.5f32).to_bits(), 0x41480000);
diff --git a/library/coretests/tests/floats/f64.rs b/library/coretests/tests/floats/f64.rs
index fea9cc19b39..a2540586328 100644
--- a/library/coretests/tests/floats/f64.rs
+++ b/library/coretests/tests/floats/f64.rs
@@ -1,5 +1,4 @@
 use core::f64;
-use core::f64::consts;
 
 use super::{assert_approx_eq, assert_biteq};
 
@@ -28,63 +27,6 @@ fn test_mul_add() {
 }
 
 #[test]
-fn test_recip() {
-    let nan: f64 = f64::NAN;
-    let inf: f64 = f64::INFINITY;
-    let neg_inf: f64 = f64::NEG_INFINITY;
-    assert_biteq!(1.0f64.recip(), 1.0);
-    assert_biteq!(2.0f64.recip(), 0.5);
-    assert_biteq!((-0.4f64).recip(), -2.5);
-    assert_biteq!(0.0f64.recip(), inf);
-    assert!(nan.recip().is_nan());
-    assert_biteq!(inf.recip(), 0.0);
-    assert_biteq!(neg_inf.recip(), -0.0);
-}
-
-#[test]
-fn test_powi() {
-    let nan: f64 = f64::NAN;
-    let inf: f64 = f64::INFINITY;
-    let neg_inf: f64 = f64::NEG_INFINITY;
-    assert_approx_eq!(1.0f64.powi(1), 1.0);
-    assert_approx_eq!((-3.1f64).powi(2), 9.61);
-    assert_approx_eq!(5.9f64.powi(-2), 0.028727);
-    assert_biteq!(8.3f64.powi(0), 1.0);
-    assert!(nan.powi(2).is_nan());
-    assert_biteq!(inf.powi(3), inf);
-    assert_biteq!(neg_inf.powi(2), inf);
-}
-
-#[test]
-fn test_to_degrees() {
-    let pi: f64 = consts::PI;
-    let nan: f64 = f64::NAN;
-    let inf: f64 = f64::INFINITY;
-    let neg_inf: f64 = f64::NEG_INFINITY;
-    assert_biteq!(0.0f64.to_degrees(), 0.0);
-    assert_approx_eq!((-5.8f64).to_degrees(), -332.315521);
-    assert_biteq!(pi.to_degrees(), 180.0);
-    assert!(nan.to_degrees().is_nan());
-    assert_biteq!(inf.to_degrees(), inf);
-    assert_biteq!(neg_inf.to_degrees(), neg_inf);
-}
-
-#[test]
-fn test_to_radians() {
-    let pi: f64 = consts::PI;
-    let nan: f64 = f64::NAN;
-    let inf: f64 = f64::INFINITY;
-    let neg_inf: f64 = f64::NEG_INFINITY;
-    assert_biteq!(0.0f64.to_radians(), 0.0);
-    assert_approx_eq!(154.6f64.to_radians(), 2.698279);
-    assert_approx_eq!((-332.31f64).to_radians(), -5.799903);
-    assert_biteq!(180.0f64.to_radians(), pi);
-    assert!(nan.to_radians().is_nan());
-    assert_biteq!(inf.to_radians(), inf);
-    assert_biteq!(neg_inf.to_radians(), neg_inf);
-}
-
-#[test]
 fn test_float_bits_conv() {
     assert_eq!((1f64).to_bits(), 0x3ff0000000000000);
     assert_eq!((12.5f64).to_bits(), 0x4029000000000000);
diff --git a/library/coretests/tests/floats/mod.rs b/library/coretests/tests/floats/mod.rs
index 2c2a07920d0..5f59cb9cce3 100644
--- a/library/coretests/tests/floats/mod.rs
+++ b/library/coretests/tests/floats/mod.rs
@@ -1,13 +1,20 @@
 use std::num::FpCategory as Fp;
 use std::ops::{Add, Div, Mul, Rem, Sub};
 
-trait TestableFloat {
+trait TestableFloat: Sized {
     /// Unsigned int with the same size, for converting to/from bits.
     type Int;
     /// Set the default tolerance for float comparison based on the type.
     const APPROX: Self;
+    /// Allow looser tolerance for f32 on miri
+    const POWI_APPROX: Self = Self::APPROX;
+    /// Allow looser tolerance for f16
+    const _180_TO_RADIANS_APPROX: Self = Self::APPROX;
+    /// Allow for looser tolerance for f16
+    const PI_TO_DEGREES_APPROX: Self = Self::APPROX;
     const ZERO: Self;
     const ONE: Self;
+    const PI: Self;
     const MIN_POSITIVE_NORMAL: Self;
     const MAX_SUBNORMAL: Self;
     /// Smallest number
@@ -25,8 +32,11 @@ trait TestableFloat {
 impl TestableFloat for f16 {
     type Int = u16;
     const APPROX: Self = 1e-3;
+    const _180_TO_RADIANS_APPROX: Self = 1e-2;
+    const PI_TO_DEGREES_APPROX: Self = 0.125;
     const ZERO: Self = 0.0;
     const ONE: Self = 1.0;
+    const PI: Self = std::f16::consts::PI;
     const MIN_POSITIVE_NORMAL: Self = Self::MIN_POSITIVE;
     const MAX_SUBNORMAL: Self = Self::MIN_POSITIVE.next_down();
     const TINY: Self = Self::from_bits(0x1);
@@ -39,8 +49,13 @@ impl TestableFloat for f16 {
 impl TestableFloat for f32 {
     type Int = u32;
     const APPROX: Self = 1e-6;
+    /// Miri adds some extra errors to float functions; make sure the tests still pass.
+    /// These values are purely used as a canary to test against and are thus not a stable guarantee Rust provides.
+    /// They serve as a way to get an idea of the real precision of floating point operations on different platforms.
+    const POWI_APPROX: Self = if cfg!(miri) { 1e-4 } else { Self::APPROX };
     const ZERO: Self = 0.0;
     const ONE: Self = 1.0;
+    const PI: Self = std::f32::consts::PI;
     const MIN_POSITIVE_NORMAL: Self = Self::MIN_POSITIVE;
     const MAX_SUBNORMAL: Self = Self::MIN_POSITIVE.next_down();
     const TINY: Self = Self::from_bits(0x1);
@@ -55,6 +70,7 @@ impl TestableFloat for f64 {
     const APPROX: Self = 1e-6;
     const ZERO: Self = 0.0;
     const ONE: Self = 1.0;
+    const PI: Self = std::f64::consts::PI;
     const MIN_POSITIVE_NORMAL: Self = Self::MIN_POSITIVE;
     const MAX_SUBNORMAL: Self = Self::MIN_POSITIVE.next_down();
     const TINY: Self = Self::from_bits(0x1);
@@ -69,6 +85,7 @@ impl TestableFloat for f128 {
     const APPROX: Self = 1e-9;
     const ZERO: Self = 0.0;
     const ONE: Self = 1.0;
+    const PI: Self = std::f128::consts::PI;
     const MIN_POSITIVE_NORMAL: Self = Self::MIN_POSITIVE;
     const MAX_SUBNORMAL: Self = Self::MIN_POSITIVE.next_down();
     const TINY: Self = Self::from_bits(0x1);
@@ -1340,3 +1357,86 @@ float_test! {
         assert_eq!(Ordering::Less, Float::total_cmp(&-s_nan(), &s_nan()));
     }
 }
+
+float_test! {
+    name: recip,
+    attrs: {
+        f16: #[cfg(any(miri, target_has_reliable_f16_math))],
+        f128: #[cfg(any(miri, target_has_reliable_f128_math))],
+    },
+    test<Float> {
+        let nan: Float = Float::NAN;
+        let inf: Float = Float::INFINITY;
+        let neg_inf: Float = Float::NEG_INFINITY;
+        assert_biteq!((1.0 as Float).recip(), 1.0);
+        assert_biteq!((2.0 as Float).recip(), 0.5);
+        assert_biteq!((-0.4 as Float).recip(), -2.5);
+        assert_biteq!((0.0 as Float).recip(), inf);
+        assert!(nan.recip().is_nan());
+        assert_biteq!(inf.recip(), 0.0);
+        assert_biteq!(neg_inf.recip(), -0.0);
+    }
+}
+
+float_test! {
+    name: powi,
+    attrs: {
+        const: #[cfg(false)],
+        f16: #[cfg(all(not(miri), target_has_reliable_f16_math))],
+        f128: #[cfg(all(not(miri), target_has_reliable_f128_math))],
+    },
+    test<Float> {
+        let nan: Float = Float::NAN;
+        let inf: Float = Float::INFINITY;
+        let neg_inf: Float = Float::NEG_INFINITY;
+        assert_approx_eq!(Float::ONE.powi(1), Float::ONE);
+        assert_approx_eq!((-3.1 as Float).powi(2), 9.6100000000000005506706202140776519387, Float::POWI_APPROX);
+        assert_approx_eq!((5.9 as Float).powi(-2), 0.028727377190462507313100483690639638451);
+        assert_biteq!((8.3 as Float).powi(0), Float::ONE);
+        assert!(nan.powi(2).is_nan());
+        assert_biteq!(inf.powi(3), inf);
+        assert_biteq!(neg_inf.powi(2), inf);
+    }
+}
+
+float_test! {
+    name: to_degrees,
+    attrs: {
+        f16: #[cfg(target_has_reliable_f16)],
+        f128: #[cfg(target_has_reliable_f128)],
+    },
+    test<Float> {
+        let pi: Float = Float::PI;
+        let nan: Float = Float::NAN;
+        let inf: Float = Float::INFINITY;
+        let neg_inf: Float = Float::NEG_INFINITY;
+        assert_biteq!((0.0 as Float).to_degrees(), 0.0);
+        assert_approx_eq!((-5.8 as Float).to_degrees(), -332.31552117587745090765431723855668471);
+        assert_approx_eq!(pi.to_degrees(), 180.0, Float::PI_TO_DEGREES_APPROX);
+        assert!(nan.to_degrees().is_nan());
+        assert_biteq!(inf.to_degrees(), inf);
+        assert_biteq!(neg_inf.to_degrees(), neg_inf);
+        assert_biteq!((1.0 as Float).to_degrees(), 57.2957795130823208767981548141051703);
+    }
+}
+
+float_test! {
+    name: to_radians,
+    attrs: {
+        f16: #[cfg(target_has_reliable_f16)],
+        f128: #[cfg(target_has_reliable_f128)],
+    },
+    test<Float> {
+        let pi: Float = Float::PI;
+        let nan: Float = Float::NAN;
+        let inf: Float = Float::INFINITY;
+        let neg_inf: Float = Float::NEG_INFINITY;
+        assert_biteq!((0.0 as Float).to_radians(), 0.0);
+        assert_approx_eq!((154.6 as Float).to_radians(), 2.6982790235832334267135442069489767804);
+        assert_approx_eq!((-332.31 as Float).to_radians(), -5.7999036373023566567593094812182763013);
+        assert_approx_eq!((180.0 as Float).to_radians(), pi, Float::_180_TO_RADIANS_APPROX);
+        assert!(nan.to_radians().is_nan());
+        assert_biteq!(inf.to_radians(), inf);
+        assert_biteq!(neg_inf.to_radians(), neg_inf);
+    }
+}
diff --git a/src/tools/compiletest/src/executor.rs b/src/tools/compiletest/src/executor.rs
index fdd7155c21f..5519ef1af1f 100644
--- a/src/tools/compiletest/src/executor.rs
+++ b/src/tools/compiletest/src/executor.rs
@@ -13,6 +13,7 @@ use std::sync::{Arc, Mutex, mpsc};
 use std::{env, hint, io, mem, panic, thread};
 
 use crate::common::{Config, TestPaths};
+use crate::panic_hook;
 
 mod deadline;
 mod json;
@@ -120,6 +121,11 @@ fn run_test_inner(
     completion_sender: mpsc::Sender<TestCompletion>,
 ) {
     let is_capture = !runnable_test.config.nocapture;
+
+    // Install a panic-capture buffer for use by the custom panic hook.
+    if is_capture {
+        panic_hook::set_capture_buf(Default::default());
+    }
     let capture_buf = is_capture.then(|| Arc::new(Mutex::new(vec![])));
 
     if let Some(capture_buf) = &capture_buf {
@@ -128,6 +134,13 @@ fn run_test_inner(
 
     let panic_payload = panic::catch_unwind(move || runnable_test.run()).err();
 
+    if let Some(panic_buf) = panic_hook::take_capture_buf() {
+        let panic_buf = panic_buf.lock().unwrap_or_else(|e| e.into_inner());
+        // For now, forward any captured panic message to (captured) stderr.
+        // FIXME(Zalathar): Once we have our own output-capture buffer for
+        // non-panic output, append the panic message to that buffer instead.
+        eprint!("{panic_buf}");
+    }
     if is_capture {
         io::set_output_capture(None);
     }
diff --git a/src/tools/compiletest/src/lib.rs b/src/tools/compiletest/src/lib.rs
index 8737fec80bb..fa84691a46f 100644
--- a/src/tools/compiletest/src/lib.rs
+++ b/src/tools/compiletest/src/lib.rs
@@ -15,6 +15,7 @@ pub mod directives;
 pub mod errors;
 mod executor;
 mod json;
+mod panic_hook;
 mod raise_fd_limit;
 mod read2;
 pub mod runtest;
@@ -493,6 +494,8 @@ pub fn opt_str2(maybestr: Option<String>) -> String {
 pub fn run_tests(config: Arc<Config>) {
     debug!(?config, "run_tests");
 
+    panic_hook::install_panic_hook();
+
     // If we want to collect rustfix coverage information,
     // we first make sure that the coverage file does not exist.
     // It will be created later on.
diff --git a/src/tools/compiletest/src/panic_hook.rs b/src/tools/compiletest/src/panic_hook.rs
new file mode 100644
index 00000000000..1661ca6dabe
--- /dev/null
+++ b/src/tools/compiletest/src/panic_hook.rs
@@ -0,0 +1,136 @@
+use std::backtrace::{Backtrace, BacktraceStatus};
+use std::cell::Cell;
+use std::fmt::{Display, Write};
+use std::panic::PanicHookInfo;
+use std::sync::{Arc, LazyLock, Mutex};
+use std::{env, mem, panic, thread};
+
+type PanicHook = Box<dyn Fn(&PanicHookInfo<'_>) + Sync + Send + 'static>;
+type CaptureBuf = Arc<Mutex<String>>;
+
+thread_local!(
+    static CAPTURE_BUF: Cell<Option<CaptureBuf>> = const { Cell::new(None) };
+);
+
+/// Installs a custom panic hook that will divert panic output to a thread-local
+/// capture buffer, but only for threads that have a capture buffer set.
+///
+/// Otherwise, the custom hook delegates to a copy of the default panic hook.
+pub(crate) fn install_panic_hook() {
+    let default_hook = panic::take_hook();
+    panic::set_hook(Box::new(move |info| custom_panic_hook(&default_hook, info)));
+}
+
+pub(crate) fn set_capture_buf(buf: CaptureBuf) {
+    CAPTURE_BUF.set(Some(buf));
+}
+
+pub(crate) fn take_capture_buf() -> Option<CaptureBuf> {
+    CAPTURE_BUF.take()
+}
+
+fn custom_panic_hook(default_hook: &PanicHook, info: &panic::PanicHookInfo<'_>) {
+    // Temporarily taking the capture buffer means that if a panic occurs in
+    // the subsequent code, that panic will fall back to the default hook.
+    let Some(buf) = take_capture_buf() else {
+        // There was no capture buffer, so delegate to the default hook.
+        default_hook(info);
+        return;
+    };
+
+    let mut out = buf.lock().unwrap_or_else(|e| e.into_inner());
+
+    let thread = thread::current().name().unwrap_or("(test runner)").to_owned();
+    let location = get_location(info);
+    let payload = payload_as_str(info).unwrap_or("Box<dyn Any>");
+    let backtrace = Backtrace::capture();
+
+    writeln!(out, "\nthread '{thread}' panicked at {location}:\n{payload}").unwrap();
+    match backtrace.status() {
+        BacktraceStatus::Captured => {
+            let bt = trim_backtrace(backtrace.to_string());
+            write!(out, "stack backtrace:\n{bt}",).unwrap();
+        }
+        BacktraceStatus::Disabled => {
+            writeln!(
+                out,
+                "note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace",
+            )
+            .unwrap();
+        }
+        _ => {}
+    }
+
+    drop(out);
+    set_capture_buf(buf);
+}
+
+fn get_location<'a>(info: &'a PanicHookInfo<'_>) -> &'a dyn Display {
+    match info.location() {
+        Some(location) => location,
+        None => &"(unknown)",
+    }
+}
+
+/// FIXME(Zalathar): Replace with `PanicHookInfo::payload_as_str` when that's
+/// stable in beta.
+fn payload_as_str<'a>(info: &'a PanicHookInfo<'_>) -> Option<&'a str> {
+    let payload = info.payload();
+    if let Some(s) = payload.downcast_ref::<&str>() {
+        Some(s)
+    } else if let Some(s) = payload.downcast_ref::<String>() {
+        Some(s)
+    } else {
+        None
+    }
+}
+
+fn rust_backtrace_full() -> bool {
+    static RUST_BACKTRACE_FULL: LazyLock<bool> =
+        LazyLock::new(|| matches!(env::var("RUST_BACKTRACE").as_deref(), Ok("full")));
+    *RUST_BACKTRACE_FULL
+}
+
+/// On stable, short backtraces are only available to the default panic hook,
+/// so if we want something similar we have to resort to string processing.
+fn trim_backtrace(full_backtrace: String) -> String {
+    if rust_backtrace_full() {
+        return full_backtrace;
+    }
+
+    let mut buf = String::with_capacity(full_backtrace.len());
+    // Don't print any frames until after the first `__rust_end_short_backtrace`.
+    let mut on = false;
+    // After the short-backtrace state is toggled, skip its associated "at" if present.
+    let mut skip_next_at = false;
+
+    let mut lines = full_backtrace.lines();
+    while let Some(line) = lines.next() {
+        if mem::replace(&mut skip_next_at, false) && line.trim_start().starts_with("at ") {
+            continue;
+        }
+
+        if line.contains("__rust_end_short_backtrace") {
+            on = true;
+            skip_next_at = true;
+            continue;
+        }
+        if line.contains("__rust_begin_short_backtrace") {
+            on = false;
+            skip_next_at = true;
+            continue;
+        }
+
+        if on {
+            writeln!(buf, "{line}").unwrap();
+        }
+    }
+
+    writeln!(
+        buf,
+        "note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace."
+    )
+    .unwrap();
+
+    buf
+}
diff --git a/src/tools/nix-dev-shell/shell.nix b/src/tools/nix-dev-shell/shell.nix
index ad33b121f97..6ca8a7c4652 100644
--- a/src/tools/nix-dev-shell/shell.nix
+++ b/src/tools/nix-dev-shell/shell.nix
@@ -14,6 +14,7 @@ pkgs.mkShell {
   packages = [
     pkgs.git
     pkgs.nix
+    pkgs.glibc.out
     pkgs.glibc.static
     x
     # Get the runtime deps of the x wrapper
@@ -23,5 +24,7 @@ pkgs.mkShell {
     # Avoid creating text files for ICEs.
     RUSTC_ICE = 0;
     SSL_CERT_FILE = cacert;
+    # cargo seems to dlopen libcurl, so we need it in the ld library path
+    LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath [pkgs.stdenv.cc.cc.lib pkgs.curl]}";
   };
 }
diff --git a/tests/coverage/closure.cov-map b/tests/coverage/closure.cov-map
index 5f7e4ce58e9..ee8934f0a84 100644
--- a/tests/coverage/closure.cov-map
+++ b/tests/coverage/closure.cov-map
@@ -118,21 +118,23 @@ Number of file 0 mappings: 4
 Highest counter ID seen: (none)
 
 Function name: closure::main::{closure#12} (unused)
-Raw bytes (10): 0x[01, 01, 00, 01, 00, a7, 01, 0a, 00, 16]
+Raw bytes (15): 0x[01, 01, 00, 02, 00, a7, 01, 01, 00, 09, 00, 00, 0a, 00, 16]
 Number of files: 1
 - file 0 => $DIR/closure.rs
 Number of expressions: 0
-Number of file 0 mappings: 1
-- Code(Zero) at (prev + 167, 10) to (start + 0, 22)
+Number of file 0 mappings: 2
+- Code(Zero) at (prev + 167, 1) to (start + 0, 9)
+- Code(Zero) at (prev + 0, 10) to (start + 0, 22)
 Highest counter ID seen: (none)
 
 Function name: closure::main::{closure#13} (unused)
-Raw bytes (10): 0x[01, 01, 00, 01, 00, ad, 01, 11, 00, 1d]
+Raw bytes (15): 0x[01, 01, 00, 02, 00, ac, 01, 0d, 00, 15, 00, 01, 11, 00, 1d]
 Number of files: 1
 - file 0 => $DIR/closure.rs
 Number of expressions: 0
-Number of file 0 mappings: 1
-- Code(Zero) at (prev + 173, 17) to (start + 0, 29)
+Number of file 0 mappings: 2
+- Code(Zero) at (prev + 172, 13) to (start + 0, 21)
+- Code(Zero) at (prev + 1, 17) to (start + 0, 29)
 Highest counter ID seen: (none)
 
 Function name: closure::main::{closure#14}
@@ -289,30 +291,33 @@ Number of file 0 mappings: 7
 Highest counter ID seen: (none)
 
 Function name: closure::main::{closure#5}
-Raw bytes (10): 0x[01, 01, 00, 01, 01, 8c, 01, 46, 00, 4e]
+Raw bytes (15): 0x[01, 01, 00, 02, 01, 8c, 01, 3d, 00, 45, 01, 00, 46, 00, 4e]
 Number of files: 1
 - file 0 => $DIR/closure.rs
 Number of expressions: 0
-Number of file 0 mappings: 1
-- Code(Counter(0)) at (prev + 140, 70) to (start + 0, 78)
+Number of file 0 mappings: 2
+- Code(Counter(0)) at (prev + 140, 61) to (start + 0, 69)
+- Code(Counter(0)) at (prev + 0, 70) to (start + 0, 78)
 Highest counter ID seen: c0
 
 Function name: closure::main::{closure#6}
-Raw bytes (10): 0x[01, 01, 00, 01, 01, 8d, 01, 4a, 00, 56]
+Raw bytes (15): 0x[01, 01, 00, 02, 01, 8d, 01, 41, 00, 49, 01, 00, 4a, 00, 56]
 Number of files: 1
 - file 0 => $DIR/closure.rs
 Number of expressions: 0
-Number of file 0 mappings: 1
-- Code(Counter(0)) at (prev + 141, 74) to (start + 0, 86)
+Number of file 0 mappings: 2
+- Code(Counter(0)) at (prev + 141, 65) to (start + 0, 73)
+- Code(Counter(0)) at (prev + 0, 74) to (start + 0, 86)
 Highest counter ID seen: c0
 
 Function name: closure::main::{closure#7} (unused)
-Raw bytes (10): 0x[01, 01, 00, 01, 00, 8e, 01, 44, 00, 50]
+Raw bytes (15): 0x[01, 01, 00, 02, 00, 8e, 01, 3b, 00, 43, 00, 00, 44, 00, 50]
 Number of files: 1
 - file 0 => $DIR/closure.rs
 Number of expressions: 0
-Number of file 0 mappings: 1
-- Code(Zero) at (prev + 142, 68) to (start + 0, 80)
+Number of file 0 mappings: 2
+- Code(Zero) at (prev + 142, 59) to (start + 0, 67)
+- Code(Zero) at (prev + 0, 68) to (start + 0, 80)
 Highest counter ID seen: (none)
 
 Function name: closure::main::{closure#8} (unused)
diff --git a/tests/coverage/closure.coverage b/tests/coverage/closure.coverage
index 14285269228..d44ecf5a69e 100644
--- a/tests/coverage/closure.coverage
+++ b/tests/coverage/closure.coverage
@@ -139,9 +139,9 @@
    LL|       |
    LL|      1|    let short_used_covered_closure_macro = | used_arg: u8 | println!("called");
    LL|      1|    let short_used_not_covered_closure_macro = | used_arg: u8 | println!("not called");
-                                                                                       ^0
+                                                                              ^0       ^0
    LL|      1|    let _short_unused_closure_macro = | _unused_arg: u8 | println!("not called");
-                                                                                 ^0
+                                                                        ^0       ^0
    LL|       |
    LL|       |
    LL|       |
@@ -173,7 +173,7 @@
    LL|       |
    LL|      1|    let _short_unused_closure_line_break_no_block2 =
    LL|       |        | _unused_arg: u8 |
-   LL|       |            println!(
+   LL|      0|            println!(
    LL|      0|                "not called"
    LL|       |            )
    LL|       |    ;
diff --git a/tests/coverage/macro_in_closure.cov-map b/tests/coverage/macro_in_closure.cov-map
index 4544aa50143..3529d0c4c32 100644
--- a/tests/coverage/macro_in_closure.cov-map
+++ b/tests/coverage/macro_in_closure.cov-map
@@ -1,10 +1,11 @@
 Function name: macro_in_closure::NO_BLOCK::{closure#0}
-Raw bytes (9): 0x[01, 01, 00, 01, 01, 07, 25, 00, 2c]
+Raw bytes (14): 0x[01, 01, 00, 02, 01, 07, 1c, 00, 24, 01, 00, 25, 00, 2c]
 Number of files: 1
 - file 0 => $DIR/macro_in_closure.rs
 Number of expressions: 0
-Number of file 0 mappings: 1
-- Code(Counter(0)) at (prev + 7, 37) to (start + 0, 44)
+Number of file 0 mappings: 2
+- Code(Counter(0)) at (prev + 7, 28) to (start + 0, 36)
+- Code(Counter(0)) at (prev + 0, 37) to (start + 0, 44)
 Highest counter ID seen: c0
 
 Function name: macro_in_closure::WITH_BLOCK::{closure#0}
diff --git a/tests/coverage/rustfmt-skip.cov-map b/tests/coverage/rustfmt-skip.cov-map
new file mode 100644
index 00000000000..bb673a411bf
--- /dev/null
+++ b/tests/coverage/rustfmt-skip.cov-map
@@ -0,0 +1,12 @@
+Function name: rustfmt_skip::main
+Raw bytes (24): 0x[01, 01, 00, 04, 01, 0a, 01, 00, 0a, 01, 02, 05, 00, 0d, 01, 03, 09, 00, 10, 01, 02, 01, 00, 02]
+Number of files: 1
+- file 0 => $DIR/rustfmt-skip.rs
+Number of expressions: 0
+Number of file 0 mappings: 4
+- Code(Counter(0)) at (prev + 10, 1) to (start + 0, 10)
+- Code(Counter(0)) at (prev + 2, 5) to (start + 0, 13)
+- Code(Counter(0)) at (prev + 3, 9) to (start + 0, 16)
+- Code(Counter(0)) at (prev + 2, 1) to (start + 0, 2)
+Highest counter ID seen: c0
+
diff --git a/tests/coverage/rustfmt-skip.coverage b/tests/coverage/rustfmt-skip.coverage
new file mode 100644
index 00000000000..b7276cf0ee8
--- /dev/null
+++ b/tests/coverage/rustfmt-skip.coverage
@@ -0,0 +1,18 @@
+   LL|       |//@ edition: 2024
+   LL|       |
+   LL|       |// The presence of `#[rustfmt::skip]` on a function should not cause macros
+   LL|       |// within that function to mysteriously not be instrumented.
+   LL|       |//
+   LL|       |// This test detects problems that can occur when building an expansion tree
+   LL|       |// based on `ExpnData::parent` instead of `ExpnData::call_site`, for example.
+   LL|       |
+   LL|       |#[rustfmt::skip]
+   LL|      1|fn main() {
+   LL|       |    // Ensure a gap between the body start and the first statement.
+   LL|      1|    println!(
+   LL|       |        // Keep this on a separate line, to distinguish instrumentation of
+   LL|       |        // `println!` from instrumentation of its arguments.
+   LL|      1|        "hello"
+   LL|       |    );
+   LL|      1|}
+
diff --git a/tests/coverage/rustfmt-skip.rs b/tests/coverage/rustfmt-skip.rs
new file mode 100644
index 00000000000..6f6874c9aa0
--- /dev/null
+++ b/tests/coverage/rustfmt-skip.rs
@@ -0,0 +1,17 @@
+//@ edition: 2024
+
+// The presence of `#[rustfmt::skip]` on a function should not cause macros
+// within that function to mysteriously not be instrumented.
+//
+// This test detects problems that can occur when building an expansion tree
+// based on `ExpnData::parent` instead of `ExpnData::call_site`, for example.
+
+#[rustfmt::skip]
+fn main() {
+    // Ensure a gap between the body start and the first statement.
+    println!(
+        // Keep this on a separate line, to distinguish instrumentation of
+        // `println!` from instrumentation of its arguments.
+        "hello"
+    );
+}
diff --git a/tests/ui/frontmatter/frontmatter-whitespace-1.rs b/tests/ui/frontmatter/frontmatter-whitespace-1.rs
index 8b6e2d1af84..3b7f762d26e 100644
--- a/tests/ui/frontmatter/frontmatter-whitespace-1.rs
+++ b/tests/ui/frontmatter/frontmatter-whitespace-1.rs
@@ -1,7 +1,7 @@
   ---
 //~^ ERROR: invalid preceding whitespace for frontmatter opening
+//~^^ ERROR: unclosed frontmatter
   ---
-//~^ ERROR: invalid preceding whitespace for frontmatter close
 
 #![feature(frontmatter)]
 
diff --git a/tests/ui/frontmatter/frontmatter-whitespace-1.stderr b/tests/ui/frontmatter/frontmatter-whitespace-1.stderr
index 37ece27acb2..f16788fa399 100644
--- a/tests/ui/frontmatter/frontmatter-whitespace-1.stderr
+++ b/tests/ui/frontmatter/frontmatter-whitespace-1.stderr
@@ -10,17 +10,21 @@ note: frontmatter opening should not be preceded by whitespace
 LL |   ---
    | ^^
 
-error: invalid preceding whitespace for frontmatter close
-  --> $DIR/frontmatter-whitespace-1.rs:3:1
+error: unclosed frontmatter
+  --> $DIR/frontmatter-whitespace-1.rs:1:3
    |
-LL |   ---
-   | ^^^^^
+LL | /   ---
+LL | |
+LL | |
+LL | |   ---
+LL | |
+   | |_^
    |
-note: frontmatter close should not be preceded by whitespace
-  --> $DIR/frontmatter-whitespace-1.rs:3:1
+note: frontmatter opening here was not closed
+  --> $DIR/frontmatter-whitespace-1.rs:1:3
    |
 LL |   ---
-   | ^^
+   |   ^^^
 
 error: aborting due to 2 previous errors
 
diff --git a/tests/ui/frontmatter/frontmatter-whitespace-2.rs b/tests/ui/frontmatter/frontmatter-whitespace-2.rs
index e8c100849b4..7a28e5c1b85 100644
--- a/tests/ui/frontmatter/frontmatter-whitespace-2.rs
+++ b/tests/ui/frontmatter/frontmatter-whitespace-2.rs
@@ -1,4 +1,5 @@
 ---cargo
+//~^ ERROR: unclosed frontmatter
 
 //@ compile-flags: --crate-type lib
 
@@ -6,10 +7,8 @@
 
 fn foo(x: i32) -> i32 {
     ---x
-    //~^ ERROR: invalid preceding whitespace for frontmatter close
-    //~| ERROR: extra characters after frontmatter close are not allowed
+     //~^ WARNING: use of a double negation [double_negations]
 }
-//~^ ERROR: unexpected closing delimiter: `}`
 
 // this test is for the weird case that valid Rust code can have three dashes
 // within them and get treated as a frontmatter close.
diff --git a/tests/ui/frontmatter/frontmatter-whitespace-2.stderr b/tests/ui/frontmatter/frontmatter-whitespace-2.stderr
index ada6af0ec04..2ae63cdc6fe 100644
--- a/tests/ui/frontmatter/frontmatter-whitespace-2.stderr
+++ b/tests/ui/frontmatter/frontmatter-whitespace-2.stderr
@@ -1,26 +1,30 @@
-error: invalid preceding whitespace for frontmatter close
-  --> $DIR/frontmatter-whitespace-2.rs:8:1
+error: unclosed frontmatter
+  --> $DIR/frontmatter-whitespace-2.rs:1:1
    |
-LL |     ---x
-   | ^^^^^^^^
+LL | / ---cargo
+...  |
+LL | |
+   | |_^
    |
-note: frontmatter close should not be preceded by whitespace
-  --> $DIR/frontmatter-whitespace-2.rs:8:1
+note: frontmatter opening here was not closed
+  --> $DIR/frontmatter-whitespace-2.rs:1:1
    |
-LL |     ---x
-   | ^^^^
+LL | ---cargo
+   | ^^^
 
-error: extra characters after frontmatter close are not allowed
-  --> $DIR/frontmatter-whitespace-2.rs:8:1
+warning: use of a double negation
+  --> $DIR/frontmatter-whitespace-2.rs:9:6
    |
 LL |     ---x
-   | ^^^^^^^^
-
-error: unexpected closing delimiter: `}`
-  --> $DIR/frontmatter-whitespace-2.rs:11:1
+   |      ^^^
+   |
+   = note: the prefix `--` could be misinterpreted as a decrement operator which exists in other languages
+   = note: use `-= 1` if you meant to decrement the value
+   = note: `#[warn(double_negations)]` on by default
+help: add parentheses for clarity
    |
-LL | }
-   | ^ unexpected closing delimiter
+LL |     --(-x)
+   |       +  +
 
-error: aborting due to 3 previous errors
+error: aborting due to 1 previous error; 1 warning emitted
 
diff --git a/tests/ui/frontmatter/multifrontmatter-2.rs b/tests/ui/frontmatter/multifrontmatter-2.rs
index 33cc30cb465..8e5b45a0bf7 100644
--- a/tests/ui/frontmatter/multifrontmatter-2.rs
+++ b/tests/ui/frontmatter/multifrontmatter-2.rs
@@ -1,12 +1,12 @@
 ---
  ---
-//~^ ERROR: invalid preceding whitespace for frontmatter close
 
  ---
-//~^ ERROR: expected item, found `-`
-// FIXME(frontmatter): make this diagnostic better
 ---
 
+// hyphens only need to be escaped when at the start of a line
+//@ check-pass
+
 #![feature(frontmatter)]
 
 fn main() {}
diff --git a/tests/ui/frontmatter/multifrontmatter-2.stderr b/tests/ui/frontmatter/multifrontmatter-2.stderr
deleted file mode 100644
index ed9ac4029e2..00000000000
--- a/tests/ui/frontmatter/multifrontmatter-2.stderr
+++ /dev/null
@@ -1,22 +0,0 @@
-error: invalid preceding whitespace for frontmatter close
-  --> $DIR/multifrontmatter-2.rs:2:1
-   |
-LL |  ---
-   | ^^^^
-   |
-note: frontmatter close should not be preceded by whitespace
-  --> $DIR/multifrontmatter-2.rs:2:1
-   |
-LL |  ---
-   | ^
-
-error: expected item, found `-`
-  --> $DIR/multifrontmatter-2.rs:5:2
-   |
-LL |  ---
-   |  ^ expected item
-   |
-   = note: for a full list of items that can appear in modules, see <https://doc.rust-lang.org/reference/items.html>
-
-error: aborting due to 2 previous errors
-