From c8a6a8d3ccf5b31a42036f6949b7069053d63028 Mon Sep 17 00:00:00 2001 From: Zack Grannan Date: Tue, 23 Jun 2026 09:35:35 -0700 Subject: [PATCH 1/4] Add window to view unblock graph --- src/borrow_pcg/unblock_graph.rs | 4 ++ src/lib.rs | 46 ++++++++++++++- src/results/mod.rs | 4 +- src/visualization/mod.rs | 19 ++++++- test-files/227_axel.rs | 9 +++ visualization/src/components/PCGNavigator.tsx | 57 +++++++++++++++---- .../PcgStmtVisualizationData.ts | 2 +- 7 files changed, 127 insertions(+), 14 deletions(-) create mode 100644 test-files/227_axel.rs diff --git a/src/borrow_pcg/unblock_graph.rs b/src/borrow_pcg/unblock_graph.rs index 7c592bdc6..b745a43ee 100644 --- a/src/borrow_pcg/unblock_graph.rs +++ b/src/borrow_pcg/unblock_graph.rs @@ -115,6 +115,10 @@ impl<'tcx> UnblockGraph<'tcx> { self.edges.is_empty() } + pub(crate) fn edges(&self) -> impl Iterator> { + self.edges.iter() + } + fn add_dependency(&mut self, unblock_edge: UnblockEdge<'tcx>) -> bool { self.edges.insert(unblock_edge) } diff --git a/src/lib.rs b/src/lib.rs index 493241c71..eb0dbcb53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,7 +57,7 @@ pub mod visualization; use borrow_checker::BorrowCheckerInterface; use borrow_pcg::graph::borrows_imgcat_debug; -use pcg::{CapabilityKind, PcgEngine}; +use pcg::{CapabilityKind, EvalStmtPhase, PcgEngine}; use rustc_interface::{ borrowck::{self, BorrowSet, LocationTable, PoloniusInput, RegionInferenceContext}, dataflow::{AnalysisEngine, compute_fixpoint}, @@ -269,6 +269,7 @@ use utils::eval_stmt_data::EvalStmtData; struct PcgStmtVisualizationData { actions: EvalStmtData>, graphs: visualization::stmt_graphs::StmtGraphs, + return_unblock_graph: Option, } #[derive(Serialize)] @@ -546,6 +547,42 @@ pub fn run_pcg<'a, 'tcx>(pcg_ctxt: &'a PcgCtxt<'_, 'tcx>) -> PcgOutput<'a, 'tcx> (None, Vec::new()) }; + let terminator_stmt_index = body[block].statements.len(); + let return_unblock_graph = + if matches!(&body[block].terminator().kind, mir::TerminatorKind::Return) { + let filename = format!("{block:?}_remote_lifetime_projection_unblock.dot"); + let post_main = &pcg_block.terminator.states[EvalStmtPhase::PostMain]; + let remote_lifetime_projections = post_main + .borrow + .graph() + .nodes(pcg_ctxt.compiler_ctxt) + .into_iter() + .filter(|node| { + node.try_into_lifetime_projection().is_ok_and(|rp| { + rp.base() + .maybe_remote_current_place() + .is_some_and(|place| place.is_remote()) + }) + }) + .collect::>(); + let unblock_graph = borrow_pcg::unblock_graph::UnblockGraph::for_nodes( + remote_lifetime_projections, + &post_main.borrow, + pcg_ctxt.compiler_ctxt, + ); + let dot_graph = visualization::generate_unblock_dot_graph( + pcg_ctxt.compiler_ctxt, + &post_main.place_capabilities, + &unblock_graph, + ) + .expect("Failed to generate return unblock graph"); + std::fs::write(dir_path.join(&filename), dot_graph) + .expect("Failed to write return unblock graph"); + Some(filename) + } else { + None + }; + let statements = pcg_block .statements() .map(|stmt| { @@ -557,6 +594,13 @@ pub fn run_pcg<'a, 'tcx>(pcg_ctxt: &'a PcgCtxt<'_, 'tcx>) -> PcgOutput<'a, 'tcx> .get(stmt.location.statement_index) .cloned() .unwrap_or_default(), + return_unblock_graph: if stmt.location.statement_index + == terminator_stmt_index + { + return_unblock_graph.clone() + } else { + None + }, } }) .collect(); diff --git a/src/results/mod.rs b/src/results/mod.rs index 1ac55cc05..69ee1394c 100644 --- a/src/results/mod.rs +++ b/src/results/mod.rs @@ -127,6 +127,7 @@ impl<'a, 'tcx: 'a> PcgAnalysisResults<'a, 'tcx> { let state = self.cursor.get().expect_results_or_error()?; let from_pcg = &state.data.pcg; let from_post_main = from_pcg.states[EvalStmtPhase::PostMain].clone(); + let states = state.data.pcg.states.to_owned(); let self_abstraction_edges = from_post_main .borrow .graph() @@ -194,7 +195,7 @@ impl<'a, 'tcx: 'a> PcgAnalysisResults<'a, 'tcx> { )) }) .collect::, PcgError>>()?; - Ok(PcgTerminator { succs }) + Ok(PcgTerminator { succs, states }) } /// Obtains the results of the dataflow analysis for all blocks. @@ -455,4 +456,5 @@ impl<'a, 'tcx: 'a> PcgLocation<'a, 'tcx> { #[derive(Debug)] pub struct PcgTerminator<'a, 'tcx> { pub succs: Vec>, + pub(crate) states: DomainDataStates>, } diff --git a/src/visualization/mod.rs b/src/visualization/mod.rs index 73b1dbed0..c05ff7b67 100644 --- a/src/visualization/mod.rs +++ b/src/visualization/mod.rs @@ -25,7 +25,7 @@ pub use mir_graph::SourcePos; use crate::{ borrow_pcg::{ - edge::borrow_flow::BorrowFlowEdgeKind, graph::BorrowsGraph, + edge::borrow_flow::BorrowFlowEdgeKind, graph::BorrowsGraph, unblock_graph::UnblockGraph, validity_conditions::ValidityConditions, }, pcg::{CapabilityKind, PcgRef, place_capabilities::PlaceCapabilitiesReader}, @@ -425,6 +425,23 @@ pub(crate) fn generate_borrows_dot_graph<'a, 'tcx: 'a>( Ok(String::from_utf8(buf).unwrap()) } +pub(crate) fn generate_unblock_dot_graph<'a, 'tcx: 'a>( + ctxt: CompilerCtxt<'a, 'tcx>, + capabilities: &impl PlaceCapabilitiesReader<'tcx>, + unblock_graph: &UnblockGraph<'tcx>, +) -> io::Result { + let mut borrows_graph = BorrowsGraph::default(); + for edge in unblock_graph.edges() { + borrows_graph.insert(edge.clone(), ctxt.bc_ctxt()); + } + let constructor = BorrowsGraphConstructor::new(&borrows_graph, capabilities, ctxt.bc_ctxt()); + let graph = constructor.construct_graph(); + let mut buf = vec![]; + let drawer = GraphDrawer::new(&mut buf, None); + drawer.draw(&graph, ctxt)?; + Ok(String::from_utf8(buf).unwrap()) +} + pub(crate) fn generate_pcg_dot_graph<'a, 'tcx: 'a>( pcg: PcgRef<'a, 'tcx>, ctxt: impl HasBorrowCheckerCtxt<'a, 'tcx>, diff --git a/test-files/227_axel.rs b/test-files/227_axel.rs new file mode 100644 index 000000000..8c279b2da --- /dev/null +++ b/test-files/227_axel.rs @@ -0,0 +1,9 @@ +fn fn_arg_reborrow(x: &i32) -> &i32 { + let y = &(*x); + y // MIR: actually `result = &(*y); return` +} + +fn fn_arg_copy(x: &i32) -> &i32 { + let y = x; // MIR: y = copy x + y // MIR: actually `result = &(*y); return` +} diff --git a/visualization/src/components/PCGNavigator.tsx b/visualization/src/components/PCGNavigator.tsx index 23badc632..ab6ab754d 100644 --- a/visualization/src/components/PCGNavigator.tsx +++ b/visualization/src/components/PCGNavigator.tsx @@ -42,6 +42,17 @@ export const NAVIGATOR_MIN_WIDTH_NUM = 40; export const NAVIGATOR_MAX_WIDTH = "200px"; export const NAVIGATOR_MIN_WIDTH = "40px"; +const navigatorActionButtonStyle: React.CSSProperties = { + margin: "10px", + padding: "8px", + cursor: "pointer", + backgroundColor: "#007acc", + color: "white", + border: "none", + borderRadius: "4px", + fontSize: "12px", +}; + function DotGraphButton({ label, dotContent, @@ -257,6 +268,22 @@ const getPCGDotGraphFilename = ( return `data/${selectedFunction}/${filename}`; }; +const getReturnUnblockGraphFilename = ( + currentPoint: CurrentPoint, + selectedFunction: string, + graphs: PcgBlockVisualizationData +): string | null => { + if ( + currentPoint.type !== "stmt" || + graphs.statements.length <= currentPoint.stmt + ) { + return null; + } + + const filename = graphs.statements[currentPoint.stmt].return_unblock_graph; + return filename ? `data/${selectedFunction}/${filename}` : null; +}; + const formatCurrentPointTitle = (currentPoint: CurrentPoint): string => { if (currentPoint.type === "stmt") { const navPointName = @@ -345,6 +372,11 @@ export default function PCGNavigator({ }; const navigationItems = buildNavigationItems(); + const returnUnblockGraphFilename = getReturnUnblockGraphFilename( + currentPoint, + selectedFunction, + pcgData + ); // Resize handlers const handleResizeStart = (event: React.MouseEvent) => { @@ -626,16 +658,7 @@ export default function PCGNavigator({ {renderItems()} + {returnUnblockGraphFilename ? ( + + ) : null}
>, graphs: StmtGraphs, }; +export type PcgStmtVisualizationData = { actions: EvalStmtData>, graphs: StmtGraphs, return_unblock_graph: string | null, }; From 0c1a74da17325fbb4ad57f3b59ca24a4f5a2e143 Mon Sep 17 00:00:00 2001 From: Zack Grannan Date: Tue, 23 Jun 2026 09:42:10 -0700 Subject: [PATCH 2/4] add unblock actions --- src/lib.rs | 23 ++++- visualization/src/components/PCGNavigator.tsx | 21 ++++- visualization/src/dot_graph.ts | 89 +++++++++++++++++-- .../PcgStmtVisualizationData.ts | 2 +- 4 files changed, 124 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index eb0dbcb53..7b1e2574b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -270,6 +270,7 @@ struct PcgStmtVisualizationData { actions: EvalStmtData>, graphs: visualization::stmt_graphs::StmtGraphs, return_unblock_graph: Option, + return_unblock_actions: Option>, } #[derive(Serialize)] @@ -548,7 +549,7 @@ pub fn run_pcg<'a, 'tcx>(pcg_ctxt: &'a PcgCtxt<'_, 'tcx>) -> PcgOutput<'a, 'tcx> }; let terminator_stmt_index = body[block].statements.len(); - let return_unblock_graph = + let return_unblock = if matches!(&body[block].terminator().kind, mir::TerminatorKind::Return) { let filename = format!("{block:?}_remote_lifetime_projection_unblock.dot"); let post_main = &pcg_block.terminator.states[EvalStmtPhase::PostMain]; @@ -570,6 +571,13 @@ pub fn run_pcg<'a, 'tcx>(pcg_ctxt: &'a PcgCtxt<'_, 'tcx>) -> PcgOutput<'a, 'tcx> &post_main.borrow, pcg_ctxt.compiler_ctxt, ); + let unblock_actions = unblock_graph + .clone() + .actions(pcg_ctxt.compiler_ctxt) + .expect("Failed to generate return unblock actions") + .into_iter() + .map(|action| action.edge().display_string(pcg_ctxt.compiler_ctxt)) + .collect::>(); let dot_graph = visualization::generate_unblock_dot_graph( pcg_ctxt.compiler_ctxt, &post_main.place_capabilities, @@ -578,7 +586,7 @@ pub fn run_pcg<'a, 'tcx>(pcg_ctxt: &'a PcgCtxt<'_, 'tcx>) -> PcgOutput<'a, 'tcx> .expect("Failed to generate return unblock graph"); std::fs::write(dir_path.join(&filename), dot_graph) .expect("Failed to write return unblock graph"); - Some(filename) + Some((filename, unblock_actions)) } else { None }; @@ -597,7 +605,16 @@ pub fn run_pcg<'a, 'tcx>(pcg_ctxt: &'a PcgCtxt<'_, 'tcx>) -> PcgOutput<'a, 'tcx> return_unblock_graph: if stmt.location.statement_index == terminator_stmt_index { - return_unblock_graph.clone() + return_unblock + .as_ref() + .map(|(filename, _)| filename.clone()) + } else { + None + }, + return_unblock_actions: if stmt.location.statement_index + == terminator_stmt_index + { + return_unblock.as_ref().map(|(_, actions)| actions.clone()) } else { None }, diff --git a/visualization/src/components/PCGNavigator.tsx b/visualization/src/components/PCGNavigator.tsx index ab6ab754d..2ebdba500 100644 --- a/visualization/src/components/PCGNavigator.tsx +++ b/visualization/src/components/PCGNavigator.tsx @@ -284,6 +284,20 @@ const getReturnUnblockGraphFilename = ( return filename ? `data/${selectedFunction}/${filename}` : null; }; +const getReturnUnblockActions = ( + currentPoint: CurrentPoint, + graphs: PcgBlockVisualizationData +): string[] => { + if ( + currentPoint.type !== "stmt" || + graphs.statements.length <= currentPoint.stmt + ) { + return []; + } + + return graphs.statements[currentPoint.stmt].return_unblock_actions ?? []; +}; + const formatCurrentPointTitle = (currentPoint: CurrentPoint): string => { if (currentPoint.type === "stmt") { const navPointName = @@ -377,6 +391,7 @@ export default function PCGNavigator({ selectedFunction, pcgData ); + const returnUnblockActions = getReturnUnblockActions(currentPoint, pcgData); // Resize handlers const handleResizeStart = (event: React.MouseEvent) => { @@ -680,7 +695,11 @@ export default function PCGNavigator({ openDotGraphInNewWindow( api, returnUnblockGraphFilename, - "Remote Lifetime Projection Unblock Graph" + "Remote Lifetime Projection Unblock Graph", + { + title: "Unblock Actions", + items: returnUnblockActions, + } ); }} > diff --git a/visualization/src/dot_graph.ts b/visualization/src/dot_graph.ts index 610da1764..2c433fb84 100644 --- a/visualization/src/dot_graph.ts +++ b/visualization/src/dot_graph.ts @@ -1,7 +1,12 @@ import * as Viz from "@viz-js/viz"; import { Api } from "./api"; -function renderDotInPopup(dotData: string, title: string) { +export type DotGraphSidebar = { + title: string; + items: string[]; +}; + +function renderDotInPopup(dotData: string, title: string, sidebar?: DotGraphSidebar) { Viz.instance().then((viz) => { const svgElement = viz.renderSVGElement(dotData); const popup = window.open( @@ -16,21 +21,93 @@ function renderDotInPopup(dotData: string, title: string) { popup.document.head.innerHTML = ` ${title} `; - popup.document.body.appendChild(svgElement); + const container = document.createElement("div"); + container.className = "dot-popup"; + + if (sidebar) { + const sidebarElement = document.createElement("div"); + sidebarElement.className = "dot-sidebar"; + + const heading = document.createElement("h2"); + heading.textContent = sidebar.title; + sidebarElement.appendChild(heading); + + if (sidebar.items.length === 0) { + const empty = document.createElement("p"); + empty.textContent = "No actions."; + sidebarElement.appendChild(empty); + } else { + const list = document.createElement("ol"); + sidebar.items.forEach((item) => { + const listItem = document.createElement("li"); + listItem.textContent = item; + list.appendChild(listItem); + }); + sidebarElement.appendChild(list); + } + + container.appendChild(sidebarElement); + } + + const graphContainer = document.createElement("div"); + graphContainer.className = "dot-graph"; + graphContainer.appendChild(svgElement); + container.appendChild(graphContainer); + + popup.document.body.appendChild(container); }); } -export async function openDotGraphInNewWindow(api: Api, filename: string, title?: string) { +export async function openDotGraphInNewWindow( + api: Api, + filename: string, + title?: string, + sidebar?: DotGraphSidebar +) { const dotData = await api.fetchDotFile(filename); - renderDotInPopup(dotData, title || `Dot Graph - ${filename}`); + renderDotInPopup(dotData, title || `Dot Graph - ${filename}`, sidebar); } export function openDotStringInNewWindow(dotData: string, title?: string) { diff --git a/visualization/src/generated_types/PcgStmtVisualizationData.ts b/visualization/src/generated_types/PcgStmtVisualizationData.ts index 26158b4ec..228e408b3 100644 --- a/visualization/src/generated_types/PcgStmtVisualizationData.ts +++ b/visualization/src/generated_types/PcgStmtVisualizationData.ts @@ -3,4 +3,4 @@ import type { AppliedAction } from "./AppliedAction"; import type { EvalStmtData } from "./EvalStmtData"; import type { StmtGraphs } from "./StmtGraphs"; -export type PcgStmtVisualizationData = { actions: EvalStmtData>, graphs: StmtGraphs, return_unblock_graph: string | null, }; +export type PcgStmtVisualizationData = { actions: EvalStmtData>, graphs: StmtGraphs, return_unblock_graph: string | null, return_unblock_actions: Array | null, }; From fa132288c88c82244a608ffd1144f2c4ac28b022 Mon Sep 17 00:00:00 2001 From: Zack Grannan Date: Tue, 23 Jun 2026 09:46:53 -0700 Subject: [PATCH 3/4] Appease clippy --- src/lib.rs | 64 +++++++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7b1e2574b..9d31feeba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -549,11 +549,13 @@ pub fn run_pcg<'a, 'tcx>(pcg_ctxt: &'a PcgCtxt<'_, 'tcx>) -> PcgOutput<'a, 'tcx> }; let terminator_stmt_index = body[block].statements.len(); - let return_unblock = - if matches!(&body[block].terminator().kind, mir::TerminatorKind::Return) { - let filename = format!("{block:?}_remote_lifetime_projection_unblock.dot"); - let post_main = &pcg_block.terminator.states[EvalStmtPhase::PostMain]; - let remote_lifetime_projections = post_main + let return_unblock = if matches!( + &body[block].terminator().kind, + mir::TerminatorKind::Return + ) { + let filename = format!("{block:?}_remote_lifetime_projection_unblock.dot"); + let post_main = &pcg_block.terminator.states[EvalStmtPhase::PostMain]; + let remote_lifetime_projections = post_main .borrow .graph() .nodes(pcg_ctxt.compiler_ctxt) @@ -562,34 +564,36 @@ pub fn run_pcg<'a, 'tcx>(pcg_ctxt: &'a PcgCtxt<'_, 'tcx>) -> PcgOutput<'a, 'tcx> node.try_into_lifetime_projection().is_ok_and(|rp| { rp.base() .maybe_remote_current_place() - .is_some_and(|place| place.is_remote()) + .is_some_and( + borrow_pcg::graph::loop_abstraction::MaybeRemoteCurrentPlace::is_remote, + ) }) }) .collect::>(); - let unblock_graph = borrow_pcg::unblock_graph::UnblockGraph::for_nodes( - remote_lifetime_projections, - &post_main.borrow, - pcg_ctxt.compiler_ctxt, - ); - let unblock_actions = unblock_graph - .clone() - .actions(pcg_ctxt.compiler_ctxt) - .expect("Failed to generate return unblock actions") - .into_iter() - .map(|action| action.edge().display_string(pcg_ctxt.compiler_ctxt)) - .collect::>(); - let dot_graph = visualization::generate_unblock_dot_graph( - pcg_ctxt.compiler_ctxt, - &post_main.place_capabilities, - &unblock_graph, - ) - .expect("Failed to generate return unblock graph"); - std::fs::write(dir_path.join(&filename), dot_graph) - .expect("Failed to write return unblock graph"); - Some((filename, unblock_actions)) - } else { - None - }; + let unblock_graph = borrow_pcg::unblock_graph::UnblockGraph::for_nodes( + remote_lifetime_projections, + &post_main.borrow, + pcg_ctxt.compiler_ctxt, + ); + let unblock_actions = unblock_graph + .clone() + .actions(pcg_ctxt.compiler_ctxt) + .expect("Failed to generate return unblock actions") + .into_iter() + .map(|action| action.edge().display_string(pcg_ctxt.compiler_ctxt)) + .collect::>(); + let dot_graph = visualization::generate_unblock_dot_graph( + pcg_ctxt.compiler_ctxt, + &post_main.place_capabilities, + &unblock_graph, + ) + .expect("Failed to generate return unblock graph"); + std::fs::write(dir_path.join(&filename), dot_graph) + .expect("Failed to write return unblock graph"); + Some((filename, unblock_actions)) + } else { + None + }; let statements = pcg_block .statements() From 7fa45fc546abaf3ae0366dfe1e00736865098c39 Mon Sep 17 00:00:00 2001 From: Zack Grannan Date: Tue, 23 Jun 2026 10:31:58 -0700 Subject: [PATCH 4/4] Appease clippy --- src/lib.rs | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9d31feeba..613521ac2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,7 +56,7 @@ pub mod utils; pub mod visualization; use borrow_checker::BorrowCheckerInterface; -use borrow_pcg::graph::borrows_imgcat_debug; +use borrow_pcg::graph::{borrows_imgcat_debug, loop_abstraction::MaybeRemoteCurrentPlace}; use pcg::{CapabilityKind, EvalStmtPhase, PcgEngine}; use rustc_interface::{ borrowck::{self, BorrowSet, LocationTable, PoloniusInput, RegionInferenceContext}, @@ -549,27 +549,23 @@ pub fn run_pcg<'a, 'tcx>(pcg_ctxt: &'a PcgCtxt<'_, 'tcx>) -> PcgOutput<'a, 'tcx> }; let terminator_stmt_index = body[block].statements.len(); - let return_unblock = if matches!( - &body[block].terminator().kind, - mir::TerminatorKind::Return - ) { + let is_return = matches!(&body[block].terminator().kind, mir::TerminatorKind::Return); + let return_unblock = if is_return { let filename = format!("{block:?}_remote_lifetime_projection_unblock.dot"); let post_main = &pcg_block.terminator.states[EvalStmtPhase::PostMain]; let remote_lifetime_projections = post_main - .borrow - .graph() - .nodes(pcg_ctxt.compiler_ctxt) - .into_iter() - .filter(|node| { - node.try_into_lifetime_projection().is_ok_and(|rp| { - rp.base() - .maybe_remote_current_place() - .is_some_and( - borrow_pcg::graph::loop_abstraction::MaybeRemoteCurrentPlace::is_remote, - ) - }) + .borrow + .graph() + .nodes(pcg_ctxt.compiler_ctxt) + .into_iter() + .filter(|node| { + node.try_into_lifetime_projection().is_ok_and(|rp| { + rp.base() + .maybe_remote_current_place() + .is_some_and(MaybeRemoteCurrentPlace::is_remote) }) - .collect::>(); + }) + .collect::>(); let unblock_graph = borrow_pcg::unblock_graph::UnblockGraph::for_nodes( remote_lifetime_projections, &post_main.borrow,