From bd8e3db94d6547829cdfd60c9dfdb6bdcdd262c3 Mon Sep 17 00:00:00 2001 From: Zalathar Date: Sun, 17 Aug 2025 13:41:33 +1000 Subject: [PATCH 1/9] coverage: Add a specific test for `#[rustfmt::skip]` --- tests/coverage/rustfmt-skip.cov-map | 12 ++++++++++++ tests/coverage/rustfmt-skip.coverage | 18 ++++++++++++++++++ tests/coverage/rustfmt-skip.rs | 17 +++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 tests/coverage/rustfmt-skip.cov-map create mode 100644 tests/coverage/rustfmt-skip.coverage create mode 100644 tests/coverage/rustfmt-skip.rs diff --git a/tests/coverage/rustfmt-skip.cov-map b/tests/coverage/rustfmt-skip.cov-map new file mode 100644 index 0000000000000..bb673a411bfdd --- /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 0000000000000..b7276cf0ee8b8 --- /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 0000000000000..6f6874c9aa0a5 --- /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" + ); +} From c2eb45b4a176dba37a0bce8fa9c4d46cf0d55e24 Mon Sep 17 00:00:00 2001 From: Zalathar Date: Sat, 16 Aug 2025 17:46:05 +1000 Subject: [PATCH 2/9] coverage: Build an "expansion tree" and use it to unexpand raw spans --- .../src/coverage/expansion.rs | 127 ++++++++++++++++ .../rustc_mir_transform/src/coverage/mod.rs | 1 + .../rustc_mir_transform/src/coverage/spans.rs | 142 ++++++++++-------- .../src/coverage/spans/from_mir.rs | 34 +---- .../src/coverage/unexpand.rs | 48 +----- compiler/rustc_mir_transform/src/lib.rs | 1 + tests/coverage/closure.cov-map | 35 +++-- tests/coverage/closure.coverage | 6 +- tests/coverage/macro_in_closure.cov-map | 7 +- 9 files changed, 239 insertions(+), 162 deletions(-) create mode 100644 compiler/rustc_mir_transform/src/coverage/expansion.rs 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 0000000000000..91e0528f52f94 --- /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, +} + +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 { + 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, + /// 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, + + /// Spans (and their associated BCBs) belonging to this expansion. + pub(crate) spans: Vec, + /// Expansions whose call-site is in this expansion. + pub(crate) child_expn_ids: FxIndexSet, +} + +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) -> 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 c5fef29924478..08c7d346009c6 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 d1b04c8f5877f..325935ee84682 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::>(); + // 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::>(); + 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) { - 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 { + 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) { - 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 7985e1c079881..dfeaa90dc2e22 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, - 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, 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 cb8615447362b..922edd3cc4fee 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)> { - 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)> { - 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 08f25276cecc1..8f319e6491660 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/tests/coverage/closure.cov-map b/tests/coverage/closure.cov-map index 5f7e4ce58e97b..ee8934f0a846f 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 1428526922894..d44ecf5a69e2b 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 4544aa50143eb..3529d0c4c321b 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} From 142e25e35621b7043cc86bdc3d74f463ca2032d1 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 22 Aug 2025 10:46:47 -0500 Subject: [PATCH 3/9] fix(lexer): Don't require frontmatters to be escaped with indented fences The RFC only limits hyphens at the beginning of lines and not if they are indented or embedded in other content. Sticking to that approach was confirmed by the T-lang liason at https://github.com/rust-lang/rust/issues/141367#issuecomment-3202217544 There is a regression in error message quality which I'm leaving for someone if they feel this needs improving. --- compiler/rustc_lexer/src/lib.rs | 34 +++++++---------- .../frontmatter/frontmatter-whitespace-1.rs | 2 +- .../frontmatter-whitespace-1.stderr | 18 +++++---- .../frontmatter/frontmatter-whitespace-2.rs | 5 +-- .../frontmatter-whitespace-2.stderr | 38 ++++++++++--------- tests/ui/frontmatter/multifrontmatter-2.rs | 6 +-- .../ui/frontmatter/multifrontmatter-2.stderr | 22 ----------- 7 files changed, 51 insertions(+), 74 deletions(-) delete mode 100644 tests/ui/frontmatter/multifrontmatter-2.stderr diff --git a/compiler/rustc_lexer/src/lib.rs b/compiler/rustc_lexer/src/lib.rs index 483cc3e93dc20..d10b192034354 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/tests/ui/frontmatter/frontmatter-whitespace-1.rs b/tests/ui/frontmatter/frontmatter-whitespace-1.rs index 8b6e2d1af8499..3b7f762d26ea3 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 37ece27acb225..f16788fa39929 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 e8c100849b407..7a28e5c1b85a6 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 ada6af0ec04ce..2ae63cdc6fe48 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 33cc30cb46554..8e5b45a0bf75f 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 ed9ac4029e277..0000000000000 --- 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 - -error: aborting due to 2 previous errors - From 6ebd009d470b504a411e47b24b5c99ec17d9af08 Mon Sep 17 00:00:00 2001 From: Karol Zwolak Date: Fri, 15 Aug 2025 09:54:22 +0200 Subject: [PATCH 4/9] dedup recip float test I left the additional asserts on {f16, f128}::MAX.recip() in a new test_max_recip tests. --- library/coretests/tests/floats/f128.rs | 12 +----------- library/coretests/tests/floats/f16.rs | 12 +----------- library/coretests/tests/floats/f32.rs | 14 -------------- library/coretests/tests/floats/f64.rs | 14 -------------- library/coretests/tests/floats/mod.rs | 20 ++++++++++++++++++++ 5 files changed, 22 insertions(+), 50 deletions(-) diff --git a/library/coretests/tests/floats/f128.rs b/library/coretests/tests/floats/f128.rs index ac4a20665305c..f718cfa6edc6d 100644 --- a/library/coretests/tests/floats/f128.rs +++ b/library/coretests/tests/floats/f128.rs @@ -44,22 +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] diff --git a/library/coretests/tests/floats/f16.rs b/library/coretests/tests/floats/f16.rs index bb9c8a002fe88..244b9c3fd69c0 100644 --- a/library/coretests/tests/floats/f16.rs +++ b/library/coretests/tests/floats/f16.rs @@ -50,18 +50,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] diff --git a/library/coretests/tests/floats/f32.rs b/library/coretests/tests/floats/f32.rs index e77e44655dc88..8c7cc97329ca5 100644 --- a/library/coretests/tests/floats/f32.rs +++ b/library/coretests/tests/floats/f32.rs @@ -32,20 +32,6 @@ fn test_mul_add() { assert_biteq!(f32::math::mul_add(-3.2f32, 2.4, neg_inf), neg_inf); } -#[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; diff --git a/library/coretests/tests/floats/f64.rs b/library/coretests/tests/floats/f64.rs index fea9cc19b39fa..7738fc1c47047 100644 --- a/library/coretests/tests/floats/f64.rs +++ b/library/coretests/tests/floats/f64.rs @@ -27,20 +27,6 @@ fn test_mul_add() { assert_biteq!((-3.2f64).mul_add(2.4, neg_inf), neg_inf); } -#[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; diff --git a/library/coretests/tests/floats/mod.rs b/library/coretests/tests/floats/mod.rs index 2c2a07920d06c..d5b05c45ddfdb 100644 --- a/library/coretests/tests/floats/mod.rs +++ b/library/coretests/tests/floats/mod.rs @@ -1340,3 +1340,23 @@ 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 { + 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); + } +} From e10e6d78ac671ad4df7f658e8a90dadb7a67b246 Mon Sep 17 00:00:00 2001 From: Karol Zwolak Date: Fri, 15 Aug 2025 10:10:54 +0200 Subject: [PATCH 5/9] dedup powi float test --- library/coretests/tests/floats/f128.rs | 16 -------------- library/coretests/tests/floats/f16.rs | 16 -------------- library/coretests/tests/floats/f32.rs | 19 ----------------- library/coretests/tests/floats/f64.rs | 14 ------------- library/coretests/tests/floats/mod.rs | 29 +++++++++++++++++++++++++- 5 files changed, 28 insertions(+), 66 deletions(-) diff --git a/library/coretests/tests/floats/f128.rs b/library/coretests/tests/floats/f128.rs index f718cfa6edc6d..be10a1503f3a1 100644 --- a/library/coretests/tests/floats/f128.rs +++ b/library/coretests/tests/floats/f128.rs @@ -52,22 +52,6 @@ fn test_max_recip() { ); } -#[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; diff --git a/library/coretests/tests/floats/f16.rs b/library/coretests/tests/floats/f16.rs index 244b9c3fd69c0..f5cb3184fb7fe 100644 --- a/library/coretests/tests/floats/f16.rs +++ b/library/coretests/tests/floats/f16.rs @@ -54,22 +54,6 @@ fn test_max_recip() { assert_approx_eq!(f16::MAX.recip(), 1.526624e-5f16, 1e-4); } -#[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; diff --git a/library/coretests/tests/floats/f32.rs b/library/coretests/tests/floats/f32.rs index 8c7cc97329ca5..06bc889fd55a7 100644 --- a/library/coretests/tests/floats/f32.rs +++ b/library/coretests/tests/floats/f32.rs @@ -9,11 +9,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] @@ -32,20 +27,6 @@ fn test_mul_add() { assert_biteq!(f32::math::mul_add(-3.2f32, 2.4, neg_inf), neg_inf); } -#[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; diff --git a/library/coretests/tests/floats/f64.rs b/library/coretests/tests/floats/f64.rs index 7738fc1c47047..34f56ba0f53ff 100644 --- a/library/coretests/tests/floats/f64.rs +++ b/library/coretests/tests/floats/f64.rs @@ -27,20 +27,6 @@ fn test_mul_add() { assert_biteq!((-3.2f64).mul_add(2.4, neg_inf), neg_inf); } -#[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; diff --git a/library/coretests/tests/floats/mod.rs b/library/coretests/tests/floats/mod.rs index d5b05c45ddfdb..1a7dd4d05540d 100644 --- a/library/coretests/tests/floats/mod.rs +++ b/library/coretests/tests/floats/mod.rs @@ -1,11 +1,13 @@ 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; const ZERO: Self; const ONE: Self; const MIN_POSITIVE_NORMAL: Self; @@ -39,6 +41,10 @@ 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 MIN_POSITIVE_NORMAL: Self = Self::MIN_POSITIVE; @@ -1360,3 +1366,24 @@ float_test! { 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 { + 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); + } +} From 43e535947b9ad8d951cde1bed626883cdd00bdf1 Mon Sep 17 00:00:00 2001 From: Waffle Lapkin Date: Sat, 30 Aug 2025 22:33:42 +0200 Subject: [PATCH 6/9] fixup nix dev shell again --- src/tools/nix-dev-shell/shell.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tools/nix-dev-shell/shell.nix b/src/tools/nix-dev-shell/shell.nix index ad33b121f979a..6ca8a7c4652d3 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]}"; }; } From c81a8a89eebf5683a9be09e3160c956f77bb405e Mon Sep 17 00:00:00 2001 From: Karol Zwolak Date: Fri, 15 Aug 2025 10:33:11 +0200 Subject: [PATCH 7/9] dedup to_degrees float test --- library/coretests/tests/floats/f128.rs | 15 ------------- library/coretests/tests/floats/f16.rs | 15 ------------- library/coretests/tests/floats/f32.rs | 15 ------------- library/coretests/tests/floats/f64.rs | 14 ------------- library/coretests/tests/floats/mod.rs | 29 ++++++++++++++++++++++++++ 5 files changed, 29 insertions(+), 59 deletions(-) diff --git a/library/coretests/tests/floats/f128.rs b/library/coretests/tests/floats/f128.rs index be10a1503f3a1..5e62eb7b7c59d 100644 --- a/library/coretests/tests/floats/f128.rs +++ b/library/coretests/tests/floats/f128.rs @@ -52,21 +52,6 @@ fn test_max_recip() { ); } -#[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; diff --git a/library/coretests/tests/floats/f16.rs b/library/coretests/tests/floats/f16.rs index f5cb3184fb7fe..f172e9cb508c9 100644 --- a/library/coretests/tests/floats/f16.rs +++ b/library/coretests/tests/floats/f16.rs @@ -54,21 +54,6 @@ fn test_max_recip() { assert_approx_eq!(f16::MAX.recip(), 1.526624e-5f16, 1e-4); } -#[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; diff --git a/library/coretests/tests/floats/f32.rs b/library/coretests/tests/floats/f32.rs index 06bc889fd55a7..c439dfed33caa 100644 --- a/library/coretests/tests/floats/f32.rs +++ b/library/coretests/tests/floats/f32.rs @@ -27,21 +27,6 @@ fn test_mul_add() { assert_biteq!(f32::math::mul_add(-3.2f32, 2.4, neg_inf), neg_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; diff --git a/library/coretests/tests/floats/f64.rs b/library/coretests/tests/floats/f64.rs index 34f56ba0f53ff..10ea901e930c1 100644 --- a/library/coretests/tests/floats/f64.rs +++ b/library/coretests/tests/floats/f64.rs @@ -27,20 +27,6 @@ fn test_mul_add() { assert_biteq!((-3.2f64).mul_add(2.4, neg_inf), neg_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; diff --git a/library/coretests/tests/floats/mod.rs b/library/coretests/tests/floats/mod.rs index 1a7dd4d05540d..8c939f8037dd5 100644 --- a/library/coretests/tests/floats/mod.rs +++ b/library/coretests/tests/floats/mod.rs @@ -8,8 +8,11 @@ trait TestableFloat: Sized { const APPROX: Self; /// Allow looser tolerance for f32 on miri const POWI_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 @@ -27,8 +30,10 @@ trait TestableFloat: Sized { impl TestableFloat for f16 { type Int = u16; const APPROX: Self = 1e-3; + 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); @@ -47,6 +52,7 @@ impl TestableFloat for f32 { 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); @@ -61,6 +67,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); @@ -75,6 +82,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); @@ -1387,3 +1395,24 @@ float_test! { 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 { + 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); + } +} From 9028efcf2e67f5fb7a890b64866760c26c6c0270 Mon Sep 17 00:00:00 2001 From: Karol Zwolak Date: Sun, 31 Aug 2025 18:19:06 +0200 Subject: [PATCH 8/9] dedup to_radians float test --- library/coretests/tests/floats/f128.rs | 21 ++------------------- library/coretests/tests/floats/f16.rs | 17 ----------------- library/coretests/tests/floats/f32.rs | 16 ---------------- library/coretests/tests/floats/f64.rs | 16 ---------------- library/coretests/tests/floats/mod.rs | 24 ++++++++++++++++++++++++ 5 files changed, 26 insertions(+), 68 deletions(-) diff --git a/library/coretests/tests/floats/f128.rs b/library/coretests/tests/floats/f128.rs index 5e62eb7b7c59d..c173d7f0ae01f 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 @@ -52,23 +52,6 @@ fn test_max_recip() { ); } -#[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] fn test_float_bits_conv() { assert_eq!((1f128).to_bits(), 0x3fff0000000000000000000000000000); diff --git a/library/coretests/tests/floats/f16.rs b/library/coretests/tests/floats/f16.rs index f172e9cb508c9..c12de7221baa7 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 @@ -54,21 +52,6 @@ fn test_max_recip() { assert_approx_eq!(f16::MAX.recip(), 1.526624e-5f16, 1e-4); } -#[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] fn test_float_bits_conv() { assert_eq!((1f16).to_bits(), 0x3c00); diff --git a/library/coretests/tests/floats/f32.rs b/library/coretests/tests/floats/f32.rs index c439dfed33caa..b79295f470def 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}; @@ -27,21 +26,6 @@ fn test_mul_add() { assert_biteq!(f32::math::mul_add(-3.2f32, 2.4, neg_inf), neg_inf); } -#[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); diff --git a/library/coretests/tests/floats/f64.rs b/library/coretests/tests/floats/f64.rs index 10ea901e930c1..a254058632867 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}; @@ -27,21 +26,6 @@ fn test_mul_add() { assert_biteq!((-3.2f64).mul_add(2.4, neg_inf), 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); diff --git a/library/coretests/tests/floats/mod.rs b/library/coretests/tests/floats/mod.rs index 8c939f8037dd5..5f59cb9cce379 100644 --- a/library/coretests/tests/floats/mod.rs +++ b/library/coretests/tests/floats/mod.rs @@ -8,6 +8,8 @@ trait TestableFloat: Sized { 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; @@ -30,6 +32,7 @@ trait TestableFloat: Sized { 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; @@ -1416,3 +1419,24 @@ float_test! { 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 { + 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); + } +} From e7519c63b0ace45a374290c451db87a5e5f6578d Mon Sep 17 00:00:00 2001 From: Zalathar Date: Sun, 31 Aug 2025 14:35:02 +1000 Subject: [PATCH 9/9] Capture panic messages via a custom panic hook --- src/tools/compiletest/src/executor.rs | 13 +++ src/tools/compiletest/src/lib.rs | 3 + src/tools/compiletest/src/panic_hook.rs | 136 ++++++++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 src/tools/compiletest/src/panic_hook.rs diff --git a/src/tools/compiletest/src/executor.rs b/src/tools/compiletest/src/executor.rs index fdd7155c21ffe..5519ef1af1fad 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, ) { 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 8737fec80bb39..fa84691a46f41 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 { pub fn run_tests(config: Arc) { 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 0000000000000..1661ca6dabe8a --- /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) + Sync + Send + 'static>; +type CaptureBuf = Arc>; + +thread_local!( + static CAPTURE_BUF: Cell> = 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 { + 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"); + 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::() { + Some(s) + } else { + None + } +} + +fn rust_backtrace_full() -> bool { + static RUST_BACKTRACE_FULL: LazyLock = + 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 +}