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/16] 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 fd4d2e62cdaebddaeb8c2072676e25b34eecb82d Mon Sep 17 00:00:00 2001 From: Orson Peters Date: Sat, 20 Jun 2026 21:03:37 +0200 Subject: [PATCH 02/16] Guarantee 8 bytes of alignment of RawWakerVTable --- library/core/src/task/wake.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/core/src/task/wake.rs b/library/core/src/task/wake.rs index 473b185d24652..1dbd11426e57e 100644 --- a/library/core/src/task/wake.rs +++ b/library/core/src/task/wake.rs @@ -90,6 +90,8 @@ impl RawWaker { /// pointers to *different* functions can compare equal (since identical functions can be /// deduplicated within a codegen unit). /// +/// This struct is guaranteed to be aligned to at least 8 bytes. +/// /// # Thread safety /// If the [`RawWaker`] will be used to construct a [`Waker`] then /// these functions must all be thread-safe (even though [`RawWaker`] is @@ -106,6 +108,8 @@ impl RawWaker { #[stable(feature = "futures_api", since = "1.36.0")] #[allow(unpredictable_function_pointer_comparisons)] #[derive(PartialEq, Copy, Clone, Debug)] +// For bit-stuffing pointers we guarantee align >= 8. +#[repr(align(8))] pub struct RawWakerVTable { /// This function will be called when the [`RawWaker`] gets cloned, e.g. when /// the [`Waker`] in which the [`RawWaker`] is stored gets cloned. From 6401a6e516abd00f5e34987e50f6212e34bca31e Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Tue, 15 Sep 2026 07:54:35 +0200 Subject: [PATCH 03/16] 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 04/16] 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 05/16] 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 06/16] 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 07/16] 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 08/16] 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, "