rustc_borrowck/polonius/mod.rs
1//! Polonius analysis and support code:
2//! - dedicated constraints
3//! - conversion from NLL constraints
4//! - debugging utilities
5//! - etc.
6//!
7//! The current implementation models the flow-sensitive borrow-checking concerns as a graph
8//! containing both information about regions and information about the control flow.
9//!
10//! Loan propagation is seen as a reachability problem (with some subtleties) between where the loan
11//! is introduced and a given point.
12//!
13//! Constraints arising from type-checking allow loans to flow from region to region at the same CFG
14//! point. Constraints arising from liveness allow loans to flow within from point to point, between
15//! live regions at these points.
16//!
17//! Edges can be bidirectional to encode invariant relationships, and loans can flow "back in time"
18//! to traverse these constraints arising earlier in the CFG.
19//!
20//! When incorporating kills in the traversal, the loans reaching a given point are considered live.
21//!
22//! After this, the usual NLL process happens. These live loans are fed into a dataflow analysis
23//! combining them with the points where loans go out of NLL scope (the frontier where they stop
24//! propagating to a live region), to yield the "loans in scope" or "active loans", at a given
25//! point.
26//!
27//! Illegal accesses are still computed by checking whether one of these resulting loans is
28//! invalidated.
29//!
30//! More information on this simple approach can be found in the following links, and in the future
31//! in the rustc dev guide:
32//! - <https://smallcultfollowing.com/babysteps/blog/2023/09/22/polonius-part-1/>
33//! - <https://smallcultfollowing.com/babysteps/blog/2023/09/29/polonius-part-2/>
34//!
35//!
36//! Data flows like this:
37//! 1) during MIR typeck, record liveness data needed later: live region variances, as well as the
38//! usual NLL liveness data (just computed on more locals). That's the [PoloniusLivenessContext].
39//! 2) once that is done, variance data is transferred, and the NLL region liveness is converted to
40//! the polonius shape. That's the main [PoloniusContext].
41//! 3) during region inference, that data and the NLL outlives constraints are used to create the
42//! localized outlives constraints, as described above. That's the [PoloniusDiagnosticsContext].
43//! 4) transfer this back to the main borrowck procedure: it handles computing errors and
44//! diagnostics, debugging and MIR dumping concerns.
45
46mod constraints;
47mod dump;
48pub(crate) mod legacy;
49mod liveness_constraints;
50mod loan_liveness;
51mod typeck_constraints;
52
53use std::collections::BTreeMap;
54
55use rustc_data_structures::fx::FxHashSet;
56use rustc_index::bit_set::SparseBitMatrix;
57use rustc_index::interval::SparseIntervalMatrix;
58use rustc_middle::mir::{Body, Local};
59use rustc_middle::ty::{RegionVid, TyCtxt};
60use rustc_mir_dataflow::points::PointIndex;
61
62pub(crate) use self::constraints::*;
63pub(crate) use self::dump::dump_polonius_mir;
64use self::liveness_constraints::create_liveness_constraints;
65use self::loan_liveness::compute_loan_liveness;
66use self::typeck_constraints::convert_typeck_constraints;
67use crate::dataflow::BorrowIndex;
68use crate::{BorrowSet, RegionInferenceContext};
69
70pub(crate) type LiveLoans = SparseBitMatrix<PointIndex, BorrowIndex>;
71
72/// This struct holds the liveness data created during MIR typeck, and which will be used later in
73/// the process, to compute the polonius localized constraints.
74#[derive(Default)]
75pub(crate) struct PoloniusLivenessContext {
76 /// The expected edge direction per live region: the kind of directed edge we'll create as
77 /// liveness constraints depends on the variance of types with respect to each contained region.
78 live_region_variances: BTreeMap<RegionVid, ConstraintDirection>,
79
80 /// The regions that outlive free regions are used to distinguish relevant live locals from
81 /// boring locals. A boring local is one whose type contains only such regions. Polonius
82 /// currently has more boring locals than NLLs so we record the latter to use in errors and
83 /// diagnostics, to focus on the locals we consider relevant and match NLL diagnostics.
84 pub(crate) boring_nll_locals: FxHashSet<Local>,
85}
86
87/// This struct holds the data needed to create the Polonius localized constraints. Its data is
88/// transferred and converted from the [PoloniusLivenessContext] at the end of MIR typeck.
89pub(crate) struct PoloniusContext {
90 /// The liveness data we recorded during MIR typeck.
91 liveness_context: PoloniusLivenessContext,
92
93 /// The set of regions that are live at a given point in the CFG, used to create localized
94 /// outlives constraints between regions that are live at connected points in the CFG.
95 live_regions: SparseBitMatrix<PointIndex, RegionVid>,
96}
97
98/// This struct holds the data needed by the borrowck error computation and diagnostics. Its data is
99/// computed from the [PoloniusContext] when computing NLL regions.
100pub(crate) struct PoloniusDiagnosticsContext {
101 /// The localized outlives constraints that were computed in the main analysis.
102 localized_outlives_constraints: LocalizedOutlivesConstraintSet,
103
104 /// The liveness data computed during MIR typeck: [PoloniusLivenessContext::boring_nll_locals].
105 pub(crate) boring_nll_locals: FxHashSet<Local>,
106}
107
108/// The direction a constraint can flow into. Used to create liveness constraints according to
109/// variance.
110#[derive(Copy, Clone, PartialEq, Eq, Debug)]
111enum ConstraintDirection {
112 /// For covariant cases, we add a forward edge `O at P1 -> O at P2`.
113 Forward,
114
115 /// For contravariant cases, we add a backward edge `O at P2 -> O at P1`
116 Backward,
117
118 /// For invariant cases, we add both the forward and backward edges `O at P1 <-> O at P2`.
119 Bidirectional,
120}
121
122impl PoloniusContext {
123 /// Unlike NLLs, in polonius we traverse the cfg to look for regions live across an edge, so we
124 /// need to transpose the "points where each region is live" matrix to a "live regions per point"
125 /// matrix.
126 // FIXME: avoid this conversion by always storing liveness data in this shape in the rest of
127 // borrowck.
128 pub(crate) fn create_from_liveness(
129 liveness_context: PoloniusLivenessContext,
130 num_regions: usize,
131 points_per_live_region: &SparseIntervalMatrix<RegionVid, PointIndex>,
132 ) -> PoloniusContext {
133 let mut live_regions_per_point = SparseBitMatrix::new(num_regions);
134 for region in points_per_live_region.rows() {
135 for point in points_per_live_region.row(region).unwrap().iter() {
136 live_regions_per_point.insert(point, region);
137 }
138 }
139
140 PoloniusContext { live_regions: live_regions_per_point, liveness_context }
141 }
142
143 /// Computes live loans using the set of loans model for `-Zpolonius=next`.
144 ///
145 /// First, creates a constraint graph combining regions and CFG points, by:
146 /// - converting NLL typeck constraints to be localized
147 /// - encoding liveness constraints
148 ///
149 /// Then, this graph is traversed, and combined with kills, reachability is recorded as loan
150 /// liveness, to be used by the loan scope and active loans computations.
151 ///
152 /// The constraint data will be used to compute errors and diagnostics.
153 pub(crate) fn compute_loan_liveness<'tcx>(
154 self,
155 tcx: TyCtxt<'tcx>,
156 regioncx: &mut RegionInferenceContext<'tcx>,
157 body: &Body<'tcx>,
158 borrow_set: &BorrowSet<'tcx>,
159 ) -> PoloniusDiagnosticsContext {
160 let PoloniusLivenessContext { live_region_variances, boring_nll_locals } =
161 self.liveness_context;
162
163 let mut localized_outlives_constraints = LocalizedOutlivesConstraintSet::default();
164 convert_typeck_constraints(
165 tcx,
166 body,
167 regioncx.liveness_constraints(),
168 regioncx.outlives_constraints(),
169 regioncx.universal_regions(),
170 &mut localized_outlives_constraints,
171 );
172
173 create_liveness_constraints(
174 body,
175 regioncx.liveness_constraints(),
176 &self.live_regions,
177 &live_region_variances,
178 regioncx.universal_regions(),
179 &mut localized_outlives_constraints,
180 );
181
182 // Now that we have a complete graph, we can compute reachability to trace the liveness of
183 // loans for the next step in the chain, the NLL loan scope and active loans computations.
184 let live_loans = compute_loan_liveness(
185 tcx,
186 body,
187 regioncx.liveness_constraints(),
188 regioncx.outlives_constraints(),
189 borrow_set,
190 &localized_outlives_constraints,
191 );
192 regioncx.record_live_loans(live_loans);
193
194 PoloniusDiagnosticsContext { localized_outlives_constraints, boring_nll_locals }
195 }
196}