From 8a7ec3449948fad3153bb42a555bc699c738cffa Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Wed, 29 Jul 2026 02:16:53 +0300 Subject: [PATCH 01/33] Stabilize `windows_process_extensions_main_thread_handle` Giving access to `std::os::windows::process::ChildExt::main_thread_handle()`. --- library/std/src/os/windows/process.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/library/std/src/os/windows/process.rs b/library/std/src/os/windows/process.rs index 41dcb70c59c9f..93d3be1838264 100644 --- a/library/std/src/os/windows/process.rs +++ b/library/std/src/os/windows/process.rs @@ -437,14 +437,23 @@ impl CommandExt for process::Command { } } -#[unstable(feature = "windows_process_extensions_main_thread_handle", issue = "96723")] +#[stable( + feature = "windows_process_extensions_main_thread_handle", + since = "CURRENT_RUSTC_VERSION" +)] pub impl(self) trait ChildExt { /// Extracts the main thread raw handle, without taking ownership - #[unstable(feature = "windows_process_extensions_main_thread_handle", issue = "96723")] + #[stable( + feature = "windows_process_extensions_main_thread_handle", + since = "CURRENT_RUSTC_VERSION" + )] fn main_thread_handle(&self) -> BorrowedHandle<'_>; } -#[unstable(feature = "windows_process_extensions_main_thread_handle", issue = "96723")] +#[stable( + feature = "windows_process_extensions_main_thread_handle", + since = "CURRENT_RUSTC_VERSION" +)] impl ChildExt for process::Child { fn main_thread_handle(&self) -> BorrowedHandle<'_> { self.handle.main_thread_handle() From c317698ed6bba628d6438be820544e4829087f98 Mon Sep 17 00:00:00 2001 From: Alice Ryhl Date: Mon, 7 Sep 2026 09:09:09 +0000 Subject: [PATCH 02/33] Make Receiver #[rustc_dyn_incompatible_trait] --- library/core/src/ops/deref.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/library/core/src/ops/deref.rs b/library/core/src/ops/deref.rs index 58bf0e2d73b97..1003edad24484 100644 --- a/library/core/src/ops/deref.rs +++ b/library/core/src/ops/deref.rs @@ -367,6 +367,7 @@ unsafe impl DerefPure for &mut T {} /// ``` #[lang = "receiver"] #[unstable(feature = "arbitrary_self_types", issue = "44874")] +#[rustc_dyn_incompatible_trait] pub trait Receiver: PointeeSized { /// The target type on which the method may be called. #[rustc_diagnostic_item = "receiver_target"] From 40bbaeaed1017cbb9f8cabcbc4ec69fc915cb070 Mon Sep 17 00:00:00 2001 From: Alice Ryhl Date: Mon, 7 Sep 2026 13:56:42 +0000 Subject: [PATCH 03/33] Fix test --- .../self/arbitrary-self-types-dyn-receiver.rs | 8 +- .../arbitrary-self-types-dyn-receiver.stderr | 85 +++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 tests/ui/self/arbitrary-self-types-dyn-receiver.stderr diff --git a/tests/ui/self/arbitrary-self-types-dyn-receiver.rs b/tests/ui/self/arbitrary-self-types-dyn-receiver.rs index fe128301d497a..3be08611c9e22 100644 --- a/tests/ui/self/arbitrary-self-types-dyn-receiver.rs +++ b/tests/ui/self/arbitrary-self-types-dyn-receiver.rs @@ -1,15 +1,20 @@ -//@ run-pass +//@ check-fail #![feature(arbitrary_self_types)] use std::ops::Receiver; trait Trait { fn foo(self: &dyn Receiver); + //~^ ERROR: the trait `std::ops::Receiver` is not dyn compatible + //~| ERROR: the trait `std::ops::Receiver` is not dyn compatible } struct Thing; impl Trait for Thing { fn foo(self: &dyn Receiver) { + //~^ ERROR: the trait `std::ops::Receiver` is not dyn compatible + //~| ERROR: the trait `std::ops::Receiver` is not dyn compatible + //~| ERROR: the trait `std::ops::Receiver` is not dyn compatible println!("huh???"); } } @@ -17,5 +22,6 @@ impl Trait for Thing { fn main() { let x = Box::new(Thing); let y: &dyn Receiver = &x; + //~^ ERROR: the trait `std::ops::Receiver` is not dyn compatible y.foo(); } diff --git a/tests/ui/self/arbitrary-self-types-dyn-receiver.stderr b/tests/ui/self/arbitrary-self-types-dyn-receiver.stderr new file mode 100644 index 0000000000000..24acb56ffb1b3 --- /dev/null +++ b/tests/ui/self/arbitrary-self-types-dyn-receiver.stderr @@ -0,0 +1,85 @@ +error[E0038]: the trait `std::ops::Receiver` is not dyn compatible + --> $DIR/arbitrary-self-types-dyn-receiver.rs:14:5 + | +LL | fn foo(self: &dyn Receiver) { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `std::ops::Receiver` is not dyn compatible + | +note: for a trait to be dyn compatible it needs to allow building a vtable + for more information, visit + --> $SRC_DIR/core/src/ops/deref.rs:LL:COL + | + = note: the trait is not dyn compatible because it opted out of dyn-compatibility + +error[E0038]: the trait `std::ops::Receiver` is not dyn compatible + --> $DIR/arbitrary-self-types-dyn-receiver.rs:14:18 + | +LL | fn foo(self: &dyn Receiver) { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ `std::ops::Receiver` is not dyn compatible + | +note: for a trait to be dyn compatible it needs to allow building a vtable + for more information, visit + --> $SRC_DIR/core/src/ops/deref.rs:LL:COL + | + = note: the trait is not dyn compatible because it opted out of dyn-compatibility + +error[E0038]: the trait `std::ops::Receiver` is not dyn compatible + --> $DIR/arbitrary-self-types-dyn-receiver.rs:14:19 + | +LL | fn foo(self: &dyn Receiver) { + | ^^^^^^^^^^^^^^^^^^^^^^^^^ `std::ops::Receiver` is not dyn compatible + | +note: for a trait to be dyn compatible it needs to allow building a vtable + for more information, visit + --> $SRC_DIR/core/src/ops/deref.rs:LL:COL + | + = note: the trait is not dyn compatible because it opted out of dyn-compatibility +help: you might have meant to use `Self` to refer to the implementing type + | +LL - fn foo(self: &dyn Receiver) { +LL + fn foo(self: &Self) { + | + +error[E0038]: the trait `std::ops::Receiver` is not dyn compatible + --> $DIR/arbitrary-self-types-dyn-receiver.rs:7:18 + | +LL | fn foo(self: &dyn Receiver); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ `std::ops::Receiver` is not dyn compatible + | +note: for a trait to be dyn compatible it needs to allow building a vtable + for more information, visit + --> $SRC_DIR/core/src/ops/deref.rs:LL:COL + | + = note: the trait is not dyn compatible because it opted out of dyn-compatibility + +error[E0038]: the trait `std::ops::Receiver` is not dyn compatible + --> $DIR/arbitrary-self-types-dyn-receiver.rs:7:19 + | +LL | fn foo(self: &dyn Receiver); + | ^^^^^^^^^^^^^^^^^^^^^^^^^ `std::ops::Receiver` is not dyn compatible + | +note: for a trait to be dyn compatible it needs to allow building a vtable + for more information, visit + --> $SRC_DIR/core/src/ops/deref.rs:LL:COL + | + = note: the trait is not dyn compatible because it opted out of dyn-compatibility +help: you might have meant to use `Self` to refer to the implementing type + | +LL - fn foo(self: &dyn Receiver); +LL + fn foo(self: &Self); + | + +error[E0038]: the trait `std::ops::Receiver` is not dyn compatible + --> $DIR/arbitrary-self-types-dyn-receiver.rs:24:17 + | +LL | let y: &dyn Receiver = &x; + | ^^^^^^^^^^^^^^^^^^^^^^ `std::ops::Receiver` is not dyn compatible + | +note: for a trait to be dyn compatible it needs to allow building a vtable + for more information, visit + --> $SRC_DIR/core/src/ops/deref.rs:LL:COL + | + = note: the trait is not dyn compatible because it opted out of dyn-compatibility + +error: aborting due to 6 previous errors + +For more information about this error, try `rustc --explain E0038`. From 58c9d2ff397941f13095a2d5a20d92e6de238348 Mon Sep 17 00:00:00 2001 From: Jonathan Keller Date: Mon, 14 Sep 2026 16:40:15 +0000 Subject: [PATCH 04/33] core: Rewrite docs for try_as_dyn --- library/core/src/any.rs | 171 ++++++++++++++++++++++++++++++++-------- 1 file changed, 140 insertions(+), 31 deletions(-) diff --git a/library/core/src/any.rs b/library/core/src/any.rs index 85ff2fe1dd6ee..8c049b118d703 100644 --- a/library/core/src/any.rs +++ b/library/core/src/any.rs @@ -964,43 +964,86 @@ pub trait TryAsDynCompatible<'a>: ptr::Pointee /// Returns `Some(&U)` if `T` can be coerced to the dyn trait type `U`. Otherwise, it returns `None`. /// -/// # Run-time failures +///
/// -/// There are multiple ways to get a `None`, and you need to manually analyze which one it is, as the -/// compiler does not provide any help here. +/// This function is implemented on a best-effort basis. It is not always possible to determine +/// whether a generic type implements a trait; thus, this function may produce false negatives, +/// returning `None` even when `T` implements the requested trait. /// -/// * `T` does not implement `Trait` at all, -/// * `T`'s impl for `Trait` is not fully generic, +/// `try_as_dyn` is guaranteed to return `None` if `T` does *not* implement the requested trait, but +/// it is never guaranteed to return `Some`. It is intended to be used for performance +/// optimizations and debugging, and `try_as_dyn` succeeding for a particular type should never be +/// relied upon for correctness (i.e. callers must behave correctly even if `try_as_dyn` spuriously +/// returns `None`). +/// +///
+/// +/// # Examples of false negatives +/// +/// Some examples of situations where `try_as_dyn::` returns `None` in practice even +/// when `T` implements `Trait`: +/// * `T`'s impl for `Trait` is lifetime-dependent /// * `T`'s impl for `Trait` is a builtin impl (e.g. `dyn Debug` implements `Debug`) +/// * `T`'s impl for `Trait` has a trait bound which requires transitively reasoning about +/// lifetime-dependent or builtin impls /// -/// There is some detailed documentation about this feature at -/// -/// But the gist is summarized below: +/// This list is not exhaustive. There is some detailed documentation about these limitations at +/// But the gist is +/// summarized below: /// -/// ## Lifetime-independent impls +/// ## Lifetime-dependent impls /// /// `try_as_dyn` does not have access to lifetime information, thus it cannot differentiate between -/// `'static`, other lifetimes, and can't reason about outlives bounds on impls. Thus we can only accept -/// impls that do not have `'static` lifetimes, or outlives bounds of any kind. You can have simple -/// trait bounds, and the compiler will transitively only use impls of those simple trait bounds that satisfy -/// the same rules as the main trait you're converting to. +/// `'static` and other lifetimes and cannot reason about outlives bounds on impls. Thus it cannot +/// reason about impls that have `'static` lifetimes or outlives bounds of any kind. +/// +/// The following impls are lifetime-dependent and produce false negatives when used with +/// `try_as_dyn`: +/// +/// ```rust +/// # trait Trait<'a, T> {} +/// # struct Type<'b, U>(&'b U); +/// # use std::fmt::{Debug, Display}; +/// // impl mentions a 'static lifetime +/// impl<'a, T: Debug, U: Display> Trait<'a, T> for Type<'static, U> {} +/// ``` +/// +/// ``` +/// # trait Trait<'a, T> {} +/// # struct Type<'b, U>(&'b U); +/// # use std::fmt::{Debug, Display}; +/// // impl contains an outlives bound +/// impl<'a, 'b, T: Debug, U: Display> Trait<'a, T> for Type<'b, U> +/// where 'b: 'a {} +/// ``` /// -/// An example of a legal impl is: +/// Impls that mention a generic parameter more than once are lifetime-dependent and produce false +/// negatives, even if they don't expressly mention any lifetimes: /// /// ```rust +/// # trait Trait {} +/// // impl mentions T more than once, creating an implied lifetime dependence +/// impl Trait for T {} +/// ``` +/// +/// The following impl is lifetime-**independent**, because even though it *mentions* lifetimes, +/// implementation of the trait is not *conditional* over the lifetimes: +/// ```rust /// # trait Trait<'a, T> {} /// # struct Type<'b, U>(&'b U); /// # use std::fmt::{Debug, Display}; /// impl<'a, 'b, T: Debug, U: Display> Trait<'a, T> for Type<'b, U> {} /// ``` /// -/// Impls without generic parameters at all are also legal, as long as they contain no `'static` lifetimes. +/// Impls without generic parameters at all are also lifetime-independent, as long as they contain +/// no `'static` lifetimes. /// /// ## Builtin impls /// -/// Builtin impls (like `impl Debug for dyn Debug`) have various obscure rules and often are not fully generic. -/// To simplify reasoning about what is allowed and what not, all builtin impls are rejected and will neither -/// directly nor indirectly contribute to a `Some` result. +/// Builtin impls (like `impl Debug for dyn Debug`, or automatic implementations of `Send` and +/// `Sync`) have various obscure rules and often are not fully generic. To simplify reasoning about +/// what is allowed and what not, all builtin impls are rejected and will neither directly nor +/// indirectly contribute to a `Some` result. /// /// # Compile-time failures /// Determining whether `T` can be coerced to the dyn trait type `U` requires compiler trait resolution. @@ -1014,30 +1057,96 @@ pub trait TryAsDynCompatible<'a>: ptr::Pointee /// /// # Examples /// +/// Using `try_as_dyn` to use bytewise comparison instead of PartialEq for certain types, similar to +/// the standard library's optimization for slices: +/// /// ```rust /// #![feature(try_as_dyn)] /// /// use core::any::try_as_dyn; /// -/// trait Animal { -/// fn speak(&self) -> &'static str; +/// /// Compares two objects for equality, +/// fn eq(x: &T, y: &T) -> bool { +/// if try_as_dyn::(&x).is_some() { +/// // T implements BytewiseEq, so we cast the slices to u8 and compare their bytes +/// // instead of calling PartialEq on each individual element. +/// unsafe { +/// // SAFETY: x and y are valid for reads of size_of::() bytes +/// // BytewiseEq trait guarantees we can interperet these bytes as u8's +/// // and compare them for equality +/// let x = &*core::ptr::slice_from_raw_parts( +/// (&raw const *x).cast::(), +/// core::mem::size_of_val(x), +/// ); +/// let y = &*core::ptr::slice_from_raw_parts( +/// (&raw const *y).cast::(), +/// core::mem::size_of_val(y), +/// ); +/// +/// x == y +/// } +/// } else { +/// // T does not implement BytewiseEq, or try_as_dyn returned a false negative. +/// // Fallback to PartialEq. +/// // +/// // BytewiseEq guarantees bytewise comparison and PartialEq will produce the same +/// // results, so our code behaves correctly if try_as_dyn produces false negatives. +/// x == y +/// } /// } /// -/// struct Dog; -/// impl Animal for Dog { -/// fn speak(&self) -> &'static str { "woof" } +/// /// Marker trait for types that can be compared for equality +/// /// using a bytewise comparison (i.e. memcmp). +/// /// +/// /// Implementations must ensure the type contains no uninitialized bytes, +/// /// and that a bytewise comparison will produce the same result as PartialEq. +/// unsafe trait BytewiseEq {} +/// +/// unsafe impl BytewiseEq for u8 {} +/// unsafe impl BytewiseEq for u16 {} +/// unsafe impl BytewiseEq for u32 {} +/// +/// // u16 implements BytewiseEq, so eq:: will use bytewise comparison +/// // (unless try_as_dyn returns a false negative) +/// assert!(eq(&5u16, &5u16)); +/// +/// // f32 does not implement BytewiseEq, so eq:: will use element-wise comparison +/// assert!(eq(&5f32, &5f32)); +/// ``` +/// +/// Using `try_as_dyn` for debugging: +/// +/// ```rust +/// #![feature(try_as_dyn)] +/// +/// use core::any::{try_as_dyn, type_name}; +/// use core::fmt::Debug; +/// +/// /// Prints a value of type T, attempting to use its Debug implementation with try_as_dyn. +/// fn debug_println(x: &T) { +/// if let Some(debug) = try_as_dyn::(x) { +/// println!("{:?}", debug); +/// } else { +/// // T does not implement Debug, or try_as_dyn returned a false negative. +/// // Print the name of the type instead. +/// // +/// // We're not relying on this for correctness; it's just for debugging, +/// // so we can tolerate false negatives. +/// println!("<{}>", type_name::()); +/// } /// } /// -/// struct Rock; // does not implement Animal +/// /// This type does not implement Debug. +/// struct NoDebug; /// -/// let dog = Dog; -/// let rock = Rock; +/// // Prints "Hello, world!" unless try_as_dyn returns a false negative. +/// debug_println(&"Hello, world!"); /// -/// let as_animal: Option<&dyn Animal> = try_as_dyn::(&dog); -/// assert_eq!(as_animal.unwrap().speak(), "woof"); +/// // Prints the name of the type, since it does not have a Debug implementation. +/// debug_println(&NoDebug); /// -/// let not_an_animal: Option<&dyn Animal> = try_as_dyn::(&rock); -/// assert!(not_an_animal.is_none()); +/// // The current implementation of try_as_dyn gives a false positive in this case! +/// debug_println(&"Hello, world!" as &dyn Debug); /// ``` #[must_use] #[unstable(feature = "try_as_dyn", issue = "144361")] @@ -1062,7 +1171,7 @@ pub const fn try_as_dyn<'a, T: ?Sized + 'a, U: TryAsDynCompatible<'a> + ?Sized>( } } -/// Returns `Some(&mut U)` if `T` can be coerced to the trait object type `U`. Otherwise, it returns `None`. +/// Returns `Some(&mut U)` if `T` can be coerced to the dyn trait type `U`. Otherwise, it returns `None`. /// /// See documentation of [try_as_dyn] for details about the behaviour and limitations. #[must_use] From 324f136401b4edc1500fc2dfca75ca0de8d511f6 Mon Sep 17 00:00:00 2001 From: Ben Kimock Date: Mon, 14 Sep 2026 18:17:10 -0400 Subject: [PATCH 05/33] Fix a bug in MatchBranchSimplification --- .../rustc_mir_transform/src/match_branches.rs | 19 ++- ...sing_locals.MatchBranchSimplification.diff | 36 ++++++ .../match_branch_simplification_aliasing.rs | 116 ++++++++++++++++++ ...nion_fields.MatchBranchSimplification.diff | 34 +++++ 4 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 tests/mir-opt/match_branch_simplification_aliasing.aliasing_locals.MatchBranchSimplification.diff create mode 100644 tests/mir-opt/match_branch_simplification_aliasing.rs create mode 100644 tests/mir-opt/match_branch_simplification_aliasing.union_fields.MatchBranchSimplification.diff diff --git a/compiler/rustc_mir_transform/src/match_branches.rs b/compiler/rustc_mir_transform/src/match_branches.rs index 894f209f9b473..48a0caa4979bb 100644 --- a/compiler/rustc_mir_transform/src/match_branches.rs +++ b/compiler/rustc_mir_transform/src/match_branches.rs @@ -1,5 +1,6 @@ use rustc_abi::Integer; use rustc_const_eval::const_eval::mk_eval_cx_for_const_val; +use rustc_index::bit_set::DenseBitSet; use rustc_middle::mir::*; use rustc_middle::ty::layout::{IntegerExt, TyAndLayout}; use rustc_middle::ty::util::Discr; @@ -44,6 +45,7 @@ struct SimplifyMatch<'tcx, 'a> { discr: &'a Operand<'tcx>, discr_local: Option, discr_ty: Ty<'tcx>, + borrowed_locals: Option>, } impl<'tcx, 'a> SimplifyMatch<'tcx, 'a> { @@ -228,7 +230,7 @@ impl<'tcx, 'a> SimplifyMatch<'tcx, 'a> { /// ``` /// This will simplify into a copy statement. fn unify_by_copy( - &self, + &mut self, dest: Place<'tcx>, rvals: &[(u128, &Rvalue<'tcx>)], ) -> Option> { @@ -258,6 +260,20 @@ impl<'tcx, 'a> SimplifyMatch<'tcx, 'a> { return None; }; + if copy_src_place.is_indirect() { + // If the src place is indirect, only permit generating the copy when the dest place is + // never borrowed. + let borrowed_locals = self + .borrowed_locals + .get_or_insert_with(|| rustc_mir_dataflow::impls::borrowed_locals(self.body)); + if borrowed_locals.contains(dest.local) { + return None; + } + } else if copy_src_place.local == dest.local { + // Also forbid the case where the source and dest are fields of the same local + return None; + } + for &(case, rvalue) in rvals.iter() { match rvalue { // Check if `_3 = const Foo::B` can be transformed to `_3 = copy *_1`. @@ -385,6 +401,7 @@ fn simplify_match<'tcx>( discr, discr_local: None, discr_ty: discr.ty(body.local_decls(), tcx), + borrowed_locals: None, }; let reachable_cases: Vec<_> = targets.iter().filter(|&(_, bb)| !body.basic_blocks[bb].is_empty_unreachable()).collect(); diff --git a/tests/mir-opt/match_branch_simplification_aliasing.aliasing_locals.MatchBranchSimplification.diff b/tests/mir-opt/match_branch_simplification_aliasing.aliasing_locals.MatchBranchSimplification.diff new file mode 100644 index 0000000000000..ccaa78736afe7 --- /dev/null +++ b/tests/mir-opt/match_branch_simplification_aliasing.aliasing_locals.MatchBranchSimplification.diff @@ -0,0 +1,36 @@ +- // MIR for `aliasing_locals` before MatchBranchSimplification ++ // MIR for `aliasing_locals` after MatchBranchSimplification + + fn aliasing_locals(_1: Foo) -> Foo { + let mut _0: Foo; + let mut _2: Foo; + let mut _3: *const Foo; + let mut _4: u8; + + bb0: { + _2 = copy _1; + _3 = &raw const _2; + _4 = discriminant((*_3)); + switchInt(copy _4) -> [0: bb2, 1: bb3, otherwise: bb1]; + } + + bb1: { + unreachable; + } + + bb2: { + _2 = Foo::A; + goto -> bb4; + } + + bb3: { + _2 = Foo::B; + goto -> bb4; + } + + bb4: { + _0 = copy _2; + return; + } + } + diff --git a/tests/mir-opt/match_branch_simplification_aliasing.rs b/tests/mir-opt/match_branch_simplification_aliasing.rs new file mode 100644 index 0000000000000..1c81d2efa74b0 --- /dev/null +++ b/tests/mir-opt/match_branch_simplification_aliasing.rs @@ -0,0 +1,116 @@ +//@ test-mir-pass: MatchBranchSimplification + +#![feature(custom_mir, core_intrinsics)] +#![allow(internal_features)] + +use std::intrinsics::mir::*; + +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq)] +enum Foo { + A, + B, + // This variant is not used, but makes the enum BackendRepr::Memory. Without it, the enum is a + // scalar and overlapping copies of it are permitted. + C(u32), +} + +// EMIT_MIR match_branch_simplification_aliasing.aliasing_locals.MatchBranchSimplification.diff +#[inline(never)] +#[custom_mir(dialect = "runtime")] +fn aliasing_locals(init: Foo) -> Foo { + // CHECK-LABEL: fn aliasing_locals(_1 + // CHECK: _2 = copy _1; + // CHECK: _3 = &raw const _2; + // CHECK: _4 = discriminant((*_3)); + // CHECK-NOT: copy (*_3); + // CHECK: switchInt + // CHECK: _2 = Foo::A; + // CHECK: _2 = Foo::B; + // CHECK: _0 = copy _2; + mir! { + let x: Foo; + let p: *const Foo; + let d: u8; + { + x = init; + p = core::ptr::addr_of!(x); + d = Discriminant(*p); + match d { + 0 => bb_a, + 1 => bb_b, + _ => bb_unreachable, + } + } + bb_unreachable = { + Unreachable() + } + bb_a = { + x = Foo::A; + Goto(bb_join) + } + bb_b = { + x = Foo::B; + Goto(bb_join) + } + bb_join = { + RET = x; + Return() + } + } +} + +union U { + a: Foo, + b: Foo, +} + +// EMIT_MIR match_branch_simplification_aliasing.union_fields.MatchBranchSimplification.diff +#[inline(never)] +#[custom_mir(dialect = "runtime")] +fn union_fields(init: Foo) -> Foo { + // CHECK-LABEL: fn union_fields(_1 + // CHECK: (_2.1: Foo) = copy _1; + // CHECK: _3 = discriminant((_2.1: Foo)); + // CHECK-NOT: copy(_2.1: Foo); + // CHECK: switchInt + // CHECK: (_2.0: Foo) = Foo::A; + // CHECK: (_2.0: Foo) = Foo::B; + // CHECK: _0 = copy (_2.0: Foo); + mir! { + let u: U; + let d: u8; + { + u.b = init; + d = Discriminant(u.b); + match d { + 0 => bb_a, + 1 => bb_b, + _ => bb_unreachable, + } + } + bb_unreachable = { + Unreachable() + } + bb_a = { + u.a = Foo::A; + Goto(bb_join) + } + bb_b = { + u.a = Foo::B; + Goto(bb_join) + } + bb_join = { + RET = u.a; + Return() + } + } +} + +fn main() { + let r = aliasing_locals(std::hint::black_box(Foo::B)); + assert!(r == Foo::B); + + let r = union_fields(std::hint::black_box(Foo::B)); + assert!(r == Foo::B); +} diff --git a/tests/mir-opt/match_branch_simplification_aliasing.union_fields.MatchBranchSimplification.diff b/tests/mir-opt/match_branch_simplification_aliasing.union_fields.MatchBranchSimplification.diff new file mode 100644 index 0000000000000..15092edd549ef --- /dev/null +++ b/tests/mir-opt/match_branch_simplification_aliasing.union_fields.MatchBranchSimplification.diff @@ -0,0 +1,34 @@ +- // MIR for `union_fields` before MatchBranchSimplification ++ // MIR for `union_fields` after MatchBranchSimplification + + fn union_fields(_1: Foo) -> Foo { + let mut _0: Foo; + let mut _2: U; + let mut _3: u8; + + bb0: { + (_2.1: Foo) = copy _1; + _3 = discriminant((_2.1: Foo)); + switchInt(copy _3) -> [0: bb2, 1: bb3, otherwise: bb1]; + } + + bb1: { + unreachable; + } + + bb2: { + (_2.0: Foo) = Foo::A; + goto -> bb4; + } + + bb3: { + (_2.0: Foo) = Foo::B; + goto -> bb4; + } + + bb4: { + _0 = copy (_2.0: Foo); + return; + } + } + From 6401a6e516abd00f5e34987e50f6212e34bca31e Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Tue, 15 Sep 2026 07:54:35 +0200 Subject: [PATCH 06/33] libtest: do not early exit from test runners --- .../rustc_builtin_macros/src/test_harness.rs | 47 +++++++++---------- library/test/src/lib.rs | 28 ++++------- .../src/items_after_test_module.rs | 6 +-- tests/pretty/tests-are-sorted.pp | 4 +- 4 files changed, 38 insertions(+), 47 deletions(-) diff --git a/compiler/rustc_builtin_macros/src/test_harness.rs b/compiler/rustc_builtin_macros/src/test_harness.rs index 972a4cc8de83e..677db66530ecb 100644 --- a/compiler/rustc_builtin_macros/src/test_harness.rs +++ b/compiler/rustc_builtin_macros/src/test_harness.rs @@ -110,8 +110,7 @@ impl TestHarnessGenerator<'_> { Some(node_id), ); for test in &mut tests { - // See the comment on `mk_main` for why we're using - // `apply_mark` directly. + // See the comment on `add_main` for why we're using `apply_mark` directly. test.ident.span = test.ident.span.apply_mark(expn_id.to_expn_id(), Transparency::Opaque); } @@ -127,7 +126,7 @@ impl<'a> MutVisitor for TestHarnessGenerator<'a> { self.add_test_cases(ast::CRATE_NODE_ID, c.spans.inner_span, prev_tests); // Create a main function to run our tests - c.items.push(mk_main(&mut self.cx)); + add_main(&mut self.cx, c); } fn visit_item(&mut self, item: &mut ast::Item) { @@ -288,16 +287,20 @@ fn generate_test_harness( /// [`TestCtxt::reexport_test_harness_main`] provides a different name for the `main` /// function and [`TestCtxt::test_runner`] provides a path that replaces /// `test::test_main_env_args`. -fn mk_main(cx: &mut TestCtxt<'_>) -> Box { +fn add_main(cx: &mut TestCtxt<'_>, c: &mut ast::Crate) { let sp = cx.def_site; let ecx = &cx.ext_cx; + // `sp` has def-site hygiene so should not clash with user-defined names. let test_ident = Ident::new(sym::test, sp); - let runner_name = - if cx.panic_strategy.unwinds() { "test_main_env_args" } else { "test_main_env_args_abort" }; - // test::test_main_env_args(...) let mut test_runner = cx.test_runner.clone().unwrap_or_else(|| { + // Built-in runner name depends on panic strategy. + let runner_name = if cx.panic_strategy.unwinds() { + "test_main_env_args" + } else { + "test_main_env_args_abort" + }; ecx.path(sp, vec![test_ident, Ident::from_str_and_span(runner_name, sp)]) }); @@ -308,10 +311,8 @@ fn mk_main(cx: &mut TestCtxt<'_>) -> Box { let call_test_main = ecx.stmt_expr(call_test_main); // extern crate test - let test_extern_stmt = ecx.stmt_item( - sp, - ecx.item(sp, ast::AttrVec::new(), ast::ItemKind::ExternCrate(None, test_ident)), - ); + let test_extern_stmt = + ecx.item(sp, ast::AttrVec::new(), ast::ItemKind::ExternCrate(None, test_ident)); // #[rustc_main] let main_attr = ecx.attr_word(sym::rustc_main, sp); @@ -320,20 +321,18 @@ fn mk_main(cx: &mut TestCtxt<'_>) -> Box { // #[doc(hidden)] let doc_hidden_attr = ecx.attr_nested_word(sym::doc, sym::hidden, sp); - // pub fn main() { ... } - // FIXME: it would be nice if we could use `std::process::ExitCode` as return type here, and - // remove all early-exit from libtest itself. Or rather, it should be `test::ExitCode` so we - // don't depend on whatever `std` may be. This needs the `extern crate test` to be *outside* - // `main`. But naively moving it out causes ICEs that give no hint as to what is wrong. - let main_ret_ty = ecx.ty(sp, ast::TyKind::Tup(ThinVec::new())); - - // If no test runner is provided we need to import the test crate - let main_body = if cx.test_runner.is_none() { - ecx.block(sp, thin_vec![test_extern_stmt, call_test_main]) + // pub fn main() -> ExitCode { ... } + let main_ret_ty = if cx.test_runner.is_none() { + // Built-in runner has return type `ExitCode`. + let exit_code_path = vec![test_ident, Ident::from_str_and_span("ExitCode", sp)]; + ecx.ty(sp, ast::TyKind::Path(None, ecx.path(sp, exit_code_path))) } else { - ecx.block(sp, thin_vec![call_test_main]) + // User-defined runners have return type `()`. + ecx.ty(sp, ast::TyKind::Tup(ThinVec::new())) }; + let main_body = ecx.block(sp, thin_vec![call_test_main]); + let decl = ecx.fn_decl(ThinVec::new(), ast::FnRetTy::Ty(main_ret_ty)); let sig = ast::FnSig { decl, header: ast::FnHeader::default(), span: sp }; let defaultness = ast::Defaultness::Implicit; @@ -365,8 +364,8 @@ fn mk_main(cx: &mut TestCtxt<'_>) -> Box { }); // Integrate the new item into existing module structures. - let main = AstFragment::Items(smallvec![main]); - cx.ext_cx.monotonic_expander().fully_expand_fragment(main).make_items().pop().unwrap() + let items = AstFragment::Items(smallvec![test_extern_stmt, main]); + c.items.extend(cx.ext_cx.monotonic_expander().fully_expand_fragment(items).make_items()); } /// Creates a slice containing every test like so: diff --git a/library/test/src/lib.rs b/library/test/src/lib.rs index 25886d295b3bc..2031bd041eecb 100644 --- a/library/test/src/lib.rs +++ b/library/test/src/lib.rs @@ -18,7 +18,6 @@ #![doc(test(attr(deny(warnings))))] #![doc(rust_logo)] #![feature(rustdoc_internals)] -#![feature(exitcode_exit_method)] #![feature(file_buffered)] #![feature(internal_output_capture)] #![feature(io_const_error)] @@ -31,6 +30,8 @@ #![warn(rustdoc::unescaped_backticks)] #![warn(unreachable_pub)] +pub use std::process::ExitCode; // used by rustc-generated test harness + pub use cli::TestOpts; pub use self::ColorConfig::*; @@ -40,7 +41,7 @@ pub use self::options::{ColorConfig, Options, OutputFormat, RunIgnored, ShouldPa pub use self::types::TestName::*; pub use self::types::*; -// Module to be used by rustc to compile tests in libtest +// Module to be used by rustc to compile tests in libtest itself pub mod test { pub use crate::bench::Bencher; pub use crate::cli::{TestOpts, parse_opts}; @@ -59,7 +60,7 @@ use std::collections::VecDeque; use std::io::prelude::Write; use std::mem::ManuallyDrop; use std::panic::{self, AssertUnwindSafe, PanicHookInfo, catch_unwind}; -use std::process::{self, Command, ExitCode, Termination}; +use std::process::{self, Command, Termination}; use std::sync::mpsc::{Sender, channel}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; @@ -170,34 +171,28 @@ fn test_main_inner(args: &[String], tests: TestList<'_>, options: Option ExitCode { // This is supposed to be reasonably fast even in Miri. In particular, when invoked via `--exact // test`, we want the entire invocation to be `O(log n)` in the number of tests: never iterate // the entire test list (as that list could be big)! let args = env::args().collect::>(); // Tests are sorted by name at compile time by mk_tests_slice. let tests = TestList::new(tests, TestListOrder::Sorted); - let exit = test_main_inner(&args, tests, None); - // We do *not* want to exit here on success, that breaks coverage tracking on Windows. - if exit != std::process::ExitCode::SUCCESS { - exit.exit_process(); - } + test_main_inner(&args, tests, None) } -/// A variant that takes the arguments from the command line. Exits the process if there -/// was an error, returns on success. +/// A variant that takes the arguments from the command line. /// /// Runs tests in panic=abort mode, which involves spawning subprocesses for /// tests. If we are invoked as subprocess, this function does not return. /// /// This is the entry point for the main function generated by `rustc --test` /// when panic=abort. -pub fn test_main_env_args_abort(tests: &[&TestDescAndFn]) { +pub fn test_main_env_args_abort(tests: &[&TestDescAndFn]) -> ExitCode { // If we're being run in SpawnedSecondary mode, run the test here. run_test // will then exit the process. if let Ok(name) = env::var(SECONDARY_TEST_INVOKER_VAR) { @@ -246,10 +241,7 @@ pub fn test_main_env_args_abort(tests: &[&TestDescAndFn]) { let args = env::args().collect::>(); // Tests are sorted by name at compile time by mk_tests_slice. let tests = TestList::new(tests, TestListOrder::Sorted); - let exit = test_main_inner(&args, tests, Some(Options::new().panic_abort(true))); - if exit != std::process::ExitCode::SUCCESS { - exit.exit_process(); - } + test_main_inner(&args, tests, Some(Options::new().panic_abort(true))) } /// Public API used by rustdoc to display the `total` and `compilation` times in the expected diff --git a/src/tools/clippy/clippy_lints/src/items_after_test_module.rs b/src/tools/clippy/clippy_lints/src/items_after_test_module.rs index 9cd71c62eb32a..dac7a24bf2a8d 100644 --- a/src/tools/clippy/clippy_lints/src/items_after_test_module.rs +++ b/src/tools/clippy/clippy_lints/src/items_after_test_module.rs @@ -65,9 +65,9 @@ impl LateLintPass<'_> for ItemsAfterTestModule { let after: Vec<_> = items .filter(|item| { - // Ignore the generated test main function - if let ItemKind::Fn { ident, .. } = item.kind - && ident.name == sym::main + // Ignore the generated test main function and `extern crate test` + if (matches!(item.kind, ItemKind::Fn { ident, .. } if ident.name == sym::main) + || matches!(item.kind, ItemKind::ExternCrate(None, ident) if ident.name == sym::test)) && item.span.ctxt().outer_expn_data().kind == ExpnKind::AstPass(AstPass::TestHarness) { false diff --git a/tests/pretty/tests-are-sorted.pp b/tests/pretty/tests-are-sorted.pp index 0002189b48c04..e3edb7e22a0b5 100644 --- a/tests/pretty/tests-are-sorted.pp +++ b/tests/pretty/tests-are-sorted.pp @@ -80,10 +80,10 @@ test::assert_test_result(a_test())), }; fn a_test() {} +extern crate test; #[rustc_main] #[coverage(off)] #[doc(hidden)] -pub fn main() -> () { - extern crate test; +pub fn main() -> test::ExitCode { test::test_main_env_args(&[&a_test, &m_test, &z_test]) } From 5fd598e8340f813dcdc2b0d0ea23692d806a5858 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Tue, 15 Sep 2026 08:06:41 +0200 Subject: [PATCH 07/33] clean up libtest re-exports a bit It does not look like these items are actually used by rustc. --- library/test/src/lib.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/library/test/src/lib.rs b/library/test/src/lib.rs index 2031bd041eecb..7eb632ba69882 100644 --- a/library/test/src/lib.rs +++ b/library/test/src/lib.rs @@ -38,10 +38,9 @@ pub use self::ColorConfig::*; pub use self::bench::{Bencher, black_box}; pub use self::console::run_tests_console; pub use self::options::{ColorConfig, Options, OutputFormat, RunIgnored, ShouldPanic}; -pub use self::types::TestName::*; pub use self::types::*; -// Module to be used by rustc to compile tests in libtest itself +// Make some items publicly available for our own tests. pub mod test { pub use crate::bench::Bencher; pub use crate::cli::{TestOpts, parse_opts}; @@ -49,11 +48,6 @@ pub mod test { pub use crate::options::{Options, RunIgnored, RunStrategy, ShouldPanic}; pub use crate::test_result::{TestResult, TrFailed, TrFailedMsg, TrIgnored, TrOk}; pub use crate::time::{TestExecTime, TestTimeOptions}; - pub use crate::types::{ - DynTestFn, DynTestName, StaticBenchFn, StaticTestFn, StaticTestName, TestDesc, - TestDescAndFn, TestId, TestList, TestListOrder, TestName, TestType, - }; - pub use crate::{assert_test_result, filter_tests, run_test, test_main, test_main_env_args}; } use std::collections::VecDeque; From f05d37a0dcf99f95993bf190c551491e57a59f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Rakic?= Date: Fri, 4 Sep 2026 15:36:59 +0200 Subject: [PATCH 08/33] extract hardcoded polonius MIR dump into HTML template This is to make it easy to update the static template, like its skeleton or style, and add features there, instead of doing it all with rust code. The dynamic sections are marked as dummy tokens and are replaced when dumping the MIR. --- compiler/rustc_borrowck/src/polonius/dump.rs | 130 +++++++++--------- .../dump/polonius-mir-dump.template.html | 39 ++++++ 2 files changed, 103 insertions(+), 66 deletions(-) create mode 100644 compiler/rustc_borrowck/src/polonius/dump/polonius-mir-dump.template.html diff --git a/compiler/rustc_borrowck/src/polonius/dump.rs b/compiler/rustc_borrowck/src/polonius/dump.rs index 5285f724b02ec..1d4e342ab0771 100644 --- a/compiler/rustc_borrowck/src/polonius/dump.rs +++ b/compiler/rustc_borrowck/src/polonius/dump.rs @@ -15,6 +15,10 @@ use crate::region_infer::values::LivenessValues; use crate::type_check::Locations; use crate::{BorrowckInferCtxt, ClosureRegionRequirements, RegionInferenceContext}; +/// The polonius MIR dump template: a regular HTML file for easy editing, with special dummy +/// sections to be replaced by real contents. +const TEMPLATE: &str = include_str!("./dump/polonius-mir-dump.template.html"); + /// `-Zdump-mir=polonius` dumps MIR annotated with NLL and polonius specific information. pub(crate) fn dump_polonius_mir<'tcx>( infcx: &BorrowckInferCtxt<'tcx>, @@ -114,72 +118,66 @@ fn emit_polonius_dump<'tcx>( localized_outlives_constraints: &[LocalizedOutlivesConstraint], out: &mut dyn io::Write, ) -> io::Result<()> { - // Prepare the HTML dump file prologue. - writeln!(out, "")?; - writeln!(out, "")?; - writeln!(out, "Polonius MIR dump")?; - writeln!(out, "")?; - - // Section 1: the NLL + Polonius MIR. - writeln!(out, "
")?; - writeln!(out, "Raw MIR dump")?; - writeln!(out, "
")?;
-    emit_html_mir(dumper, body, out)?;
-    writeln!(out, "
")?; - writeln!(out, "
")?; - - // Section 2: mermaid visualization of the polonius constraint graph. - writeln!(out, "
")?; - writeln!(out, "Polonius constraint graph")?; - writeln!(out, "
")?;
-    let edge_count = emit_mermaid_constraint_graph(
-        borrow_set,
-        regioncx.liveness_constraints(),
-        &localized_outlives_constraints,
-        out,
-    )?;
-    writeln!(out, "
")?; - writeln!(out, "
")?; - - // Section 3: mermaid visualization of the CFG. - writeln!(out, "
")?; - writeln!(out, "Control-flow graph")?; - writeln!(out, "
")?;
-    emit_mermaid_cfg(body, out)?;
-    writeln!(out, "
")?; - writeln!(out, "
")?; - - // Section 4: mermaid visualization of the NLL region graph. - writeln!(out, "
")?; - writeln!(out, "NLL regions")?; - writeln!(out, "
")?;
-    emit_mermaid_nll_regions(dumper.tcx(), regioncx, out)?;
-    writeln!(out, "
")?; - writeln!(out, "
")?; - - // Section 5: mermaid visualization of the NLL SCC graph. - writeln!(out, "
")?; - writeln!(out, "NLL SCCs")?; - writeln!(out, "
")?;
-    emit_mermaid_nll_sccs(dumper.tcx(), regioncx, out)?;
-    writeln!(out, "
")?; - writeln!(out, "
")?; - - // Finalize the dump with the HTML epilogue. - writeln!( - out, - "" - )?; - writeln!(out, "")?; - writeln!(out, "")?; - writeln!(out, "")?; + let mut edge_count = 0; + + // We replace the dummy $SECTION tokens from the HTML polonius dump template, and emit the + // result into the given writer. + for chunk in TEMPLATE.split("$SECTION") { + match chunk.strip_prefix("_") { + None => { + // We're at the beginning of the template: this is the prologue to emit as-is. + writeln!(out, "{}", chunk)?; + } + Some(section) => { + // This is the start of a prefixed section, we look for its identifier. + let dummy_section_end = section + .find("<") + .expect("the template section end boundary needs to be present"); + let section_identifier = section[..dummy_section_end].trim(); + + // Emit the real section instead of the dummy token. + match section_identifier { + "MIR" => { + emit_html_mir(dumper, body, out)?; + } + "POLONIUS_CONSTRAINTS" => { + edge_count = emit_mermaid_constraint_graph( + borrow_set, + regioncx.liveness_constraints(), + &localized_outlives_constraints, + out, + )?; + } + "CFG" => { + emit_mermaid_cfg(body, out)?; + } + "NLL_CONSTRAINTS" => { + emit_mermaid_nll_regions(dumper.tcx(), regioncx, out)?; + } + "NLL_SCCS" => { + emit_mermaid_nll_sccs(dumper.tcx(), regioncx, out)?; + } + "INITIALIZATION" => { + writeln!(out, "")?; + } + + _ => { + unreachable!("unexpected dummy section identifier {:?}", section_identifier) + } + } + + // And finally, emit the contents that followed the dummy token. + writeln!(out, "{}", §ion[dummy_section_end..])?; + } + } + } Ok(()) } diff --git a/compiler/rustc_borrowck/src/polonius/dump/polonius-mir-dump.template.html b/compiler/rustc_borrowck/src/polonius/dump/polonius-mir-dump.template.html new file mode 100644 index 0000000000000..05c5278f4c5ca --- /dev/null +++ b/compiler/rustc_borrowck/src/polonius/dump/polonius-mir-dump.template.html @@ -0,0 +1,39 @@ + + +Polonius MIR dump + + + +
+ Raw MIR dump +
$SECTION_MIR
+
+ + +
+ Polonius constraint graph +
$SECTION_POLONIUS_CONSTRAINTS
+
+ + +
+ Control-flow graph +
$SECTION_CFG
+
+ + +
+ NLL regions +
$SECTION_NLL_CONSTRAINTS
+
+ + +
+ NLL SCCs +
$SECTION_NLL_SCCS
+
+ + +$SECTION_INITIALIZATION + + From 6f49dfe7c1af10fb88d2d63aa00e077e09520305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Rakic?= Date: Tue, 15 Sep 2026 17:04:55 +0200 Subject: [PATCH 09/33] add loan reachability to the polonius mir dump display a list of all the nodes each loan can reach (and whether the node's region is live at the node's point) --- compiler/rustc_borrowck/src/polonius/dump.rs | 104 +++++++++++++++--- .../dump/polonius-mir-dump.template.html | 16 ++- 2 files changed, 97 insertions(+), 23 deletions(-) diff --git a/compiler/rustc_borrowck/src/polonius/dump.rs b/compiler/rustc_borrowck/src/polonius/dump.rs index 1d4e342ab0771..3fb9a0926e9ce 100644 --- a/compiler/rustc_borrowck/src/polonius/dump.rs +++ b/compiler/rustc_borrowck/src/polonius/dump.rs @@ -10,6 +10,7 @@ use rustc_session::config::MirIncludeSpans; use crate::borrow_set::BorrowSet; use crate::constraints::OutlivesConstraint; +use crate::dataflow::BorrowIndex; use crate::polonius::{LocalizedConstraintGraphVisitor, LocalizedNode, PoloniusContext}; use crate::region_infer::values::LivenessValues; use crate::type_check::Locations; @@ -40,7 +41,7 @@ pub(crate) fn dump_polonius_mir<'tcx>( // If we have a polonius graph to dump along the rest of the MIR and NLL info, we extract its // constraints here. - let mut collector = LocalizedOutlivesConstraintCollector { constraints: Vec::new() }; + let mut collector = MirDumpCollector::default(); if let Some(graph) = &polonius_context.graph { graph.traverse( body, @@ -76,7 +77,7 @@ pub(crate) fn dump_polonius_mir<'tcx>( let _ = try { let mut file = dumper.create_dump_file("html", body)?; - emit_polonius_dump(&dumper, body, regioncx, borrow_set, &collector.constraints, &mut file)?; + emit_polonius_dump(&dumper, body, regioncx, borrow_set, &collector, &mut file)?; }; } @@ -88,12 +89,19 @@ struct LocalizedOutlivesConstraint { to: PointIndex, } -/// Visitor to record constraints encountered when traversing the localized constraint graph. -struct LocalizedOutlivesConstraintCollector { +/// Visitor to record constraints encountered when traversing the localized constraint graph, as +/// well as the reachability of each loan. +#[derive(Default)] +struct MirDumpCollector { constraints: Vec, + reachability: FxIndexMap>, } -impl LocalizedConstraintGraphVisitor for LocalizedOutlivesConstraintCollector { +impl LocalizedConstraintGraphVisitor for MirDumpCollector { + fn on_node_traversed(&mut self, loan: BorrowIndex, node: LocalizedNode) { + self.reachability.entry(loan).or_default().push(node); + } + fn on_successor_discovered(&mut self, current_node: LocalizedNode, successor: LocalizedNode) { self.constraints.push(LocalizedOutlivesConstraint { source: current_node.region, @@ -115,7 +123,7 @@ fn emit_polonius_dump<'tcx>( body: &Body<'tcx>, regioncx: &RegionInferenceContext<'tcx>, borrow_set: &BorrowSet<'tcx>, - localized_outlives_constraints: &[LocalizedOutlivesConstraint], + collector: &MirDumpCollector, out: &mut dyn io::Write, ) -> io::Result<()> { let mut edge_count = 0; @@ -144,7 +152,15 @@ fn emit_polonius_dump<'tcx>( edge_count = emit_mermaid_constraint_graph( borrow_set, regioncx.liveness_constraints(), - &localized_outlives_constraints, + &collector.constraints, + out, + )?; + } + "POLONIUS_REACHABILITY" => { + emit_loan_reachability( + borrow_set, + regioncx.liveness_constraints(), + &collector.reachability, out, )?; } @@ -429,15 +445,9 @@ fn emit_mermaid_constraint_graph<'tcx>( localized_outlives_constraints: &[LocalizedOutlivesConstraint], out: &mut dyn io::Write, ) -> io::Result { - let location_name = |location: Location| { - // A MIR location looks like `bb5[2]`. As that is not a syntactically valid mermaid node id, - // transform it into `BB5_2`. - format!("BB{}_{}", location.block.index(), location.statement_index) - }; - let region_name = |region: RegionVid| format!("'{}", region.index()); - let node_name = |region: RegionVid, point: PointIndex| { + let node_label = |region: RegionVid, point: PointIndex| { let location = liveness.location_from_point(point); - format!("{}_{}", region_name(region), location_name(location)) + node_name(region, location) }; // The mermaid chart type: a top-down flowchart, which supports subgraphs. @@ -472,7 +482,7 @@ fn emit_mermaid_constraint_graph<'tcx>( for (region, points) in points_per_region { writeln!(out, " subgraph \"{}\"", region_name(region))?; for point in points { - writeln!(out, " {}", node_name(region, point))?; + writeln!(out, " {}", node_label(region, point))?; } writeln!(out, " end\n")?; } @@ -483,8 +493,8 @@ fn emit_mermaid_constraint_graph<'tcx>( writeln!( out, " {} --> {}", - node_name(constraint.source, constraint.from), - node_name(constraint.target, constraint.to), + node_label(constraint.source, constraint.from), + node_label(constraint.target, constraint.to), )?; } @@ -493,3 +503,61 @@ fn emit_mermaid_constraint_graph<'tcx>( let edge_count = borrow_set.len() + localized_outlives_constraints.len(); Ok(edge_count) } + +/// Emits the reachability of loans: a list of all nodes reached while traversing the polonius +/// constraint graph. +fn emit_loan_reachability( + borrow_set: &BorrowSet<'_>, + liveness: &LivenessValues, + reachability: &FxIndexMap>, + out: &mut dyn io::Write, +) -> io::Result<()> { + for (loan, _) in borrow_set.iter_enumerated() { + let Some(reachability) = reachability.get(&loan) else { + continue; + }; + let loan = format!("L{}", loan.index()); + writeln!(out, "
")?; + writeln!(out, "
Trace for loan {loan}
")?; + writeln!(out, "
    ")?; + for (idx, node) in reachability.iter().enumerate() { + writeln!(out, "
  • ")?; + + let location = liveness.location_from_point(node.point); + let kind = if idx == 0 { "starts in" } else { "reaches" }; + writeln!( + out, + "{loan} {kind} {}", + node_name(node.region, location), + )?; + + // It's useful to know whether the region we're reaching is live at this point. + let node_liveness = + if liveness.is_live_at(node.region, location) { "live" } else { "not live" }; + writeln!( + out, + "/ at {:?}: '{} is {}", + location, + node.region.index(), + node_liveness, + )?; + writeln!(out, "
  • ")?; + } + writeln!(out, "
")?; + writeln!(out, "
")?; + } + + Ok(()) +} + +fn region_name(region: RegionVid) -> String { + format!("'{}", region.index()) +} +/// A MIR location looks like `bb5[2]`. As that is not a syntactically valid mermaid node id, +/// transform it into `BB5_2`. +fn location_name(location: Location) -> String { + format!("BB{}_{}", location.block.index(), location.statement_index) +} +fn node_name(region: RegionVid, location: Location) -> String { + format!("{}_{}", region_name(region), location_name(location)) +} diff --git a/compiler/rustc_borrowck/src/polonius/dump/polonius-mir-dump.template.html b/compiler/rustc_borrowck/src/polonius/dump/polonius-mir-dump.template.html index 05c5278f4c5ca..4ee6e21165d41 100644 --- a/compiler/rustc_borrowck/src/polonius/dump/polonius-mir-dump.template.html +++ b/compiler/rustc_borrowck/src/polonius/dump/polonius-mir-dump.template.html @@ -3,31 +3,37 @@ Polonius MIR dump - +
Raw MIR dump
$SECTION_MIR
- +
Polonius constraint graph
$SECTION_POLONIUS_CONSTRAINTS
- + +
+ Loan Traces + $SECTION_POLONIUS_REACHABILITY +
+ +
Control-flow graph
$SECTION_CFG
- +
NLL regions
$SECTION_NLL_CONSTRAINTS
- +
NLL SCCs
$SECTION_NLL_SCCS
From 5e715e0f20c16ef95920f8234669e94d9182fd11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Rakic?= Date: Tue, 15 Sep 2026 18:01:32 +0200 Subject: [PATCH 10/33] make loan traces opt in Loan traces can be big and numerous, so we hide them by default. We instead use a button to show a loan's trace. --- compiler/rustc_borrowck/src/polonius/dump.rs | 8 +++++- .../dump/polonius-mir-dump.template.html | 26 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/compiler/rustc_borrowck/src/polonius/dump.rs b/compiler/rustc_borrowck/src/polonius/dump.rs index 3fb9a0926e9ce..884d10354997d 100644 --- a/compiler/rustc_borrowck/src/polonius/dump.rs +++ b/compiler/rustc_borrowck/src/polonius/dump.rs @@ -517,7 +517,13 @@ fn emit_loan_reachability( continue; }; let loan = format!("L{}", loan.index()); - writeln!(out, "
")?; + + // The button to display the loan trace. The javascript event listener is hooked up in the + // template itself. + writeln!(out, "
")?; + + // The actual trace contents, hidden by default. + writeln!(out, " -
+
Loan Traces $SECTION_POLONIUS_REACHABILITY
@@ -41,5 +49,19 @@ $SECTION_INITIALIZATION + From 2cd2f3d3e8e39bc80e68822d720f52cb3b49ae20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Rakic?= Date: Tue, 15 Sep 2026 18:32:32 +0200 Subject: [PATCH 11/33] improve visuals margins and spacing, section separators, reachability layout, etc. --- compiler/rustc_borrowck/src/polonius/dump.rs | 7 ++- .../dump/polonius-mir-dump.template.html | 43 +++++++++++++------ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/compiler/rustc_borrowck/src/polonius/dump.rs b/compiler/rustc_borrowck/src/polonius/dump.rs index 884d10354997d..e0f4c9ff98eca 100644 --- a/compiler/rustc_borrowck/src/polonius/dump.rs +++ b/compiler/rustc_borrowck/src/polonius/dump.rs @@ -520,10 +520,13 @@ fn emit_loan_reachability( // The button to display the loan trace. The javascript event listener is hooked up in the // template itself. - writeln!(out, "
")?; + writeln!( + out, + "
" + )?; // The actual trace contents, hidden by default. - writeln!(out, "