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
36mod constraints;
37mod dump;
38pub(crate) mod legacy;
39mod liveness_constraints;
40
41use rustc_data_structures::fx::FxHashSet;
42use rustc_index::IndexVec;
43use rustc_index::bit_set::DenseBitSet;
44use rustc_middle::mir::{Body, Local};
45use rustc_middle::ty::RegionVid;
46use rustc_mir_dataflow::points::PointIndex;
47
48pub(self) use self::constraints::*;
49pub(crate) use self::dump::dump_polonius_mir;
50pub(crate) use self::liveness_constraints::record_live_region_variance;
51use crate::BorrowSet;
52use crate::constraints::OutlivesConstraint;
53use crate::dataflow::BorrowIndex;
54use crate::region_infer::values::LivenessValues;
55use crate::universal_regions::UniversalRegions;
56
57pub(crate) type LiveRegionVariances = IndexVec<RegionVid, Option<ConstraintDirection>>;
58
59#[derive(Clone)]
60pub(crate) struct LiveLoans {
61 num_points: usize,
62 // This matrix always has more rows (PointIndex) than columns (BorrowIndex),
63 // and the borrow dimension is usually very low (single digit in 90% of cases in our benchmark suite),
64 // so we store it packed in a single bitset. Rows are points, columns are borrows.
65 flat_matrix: DenseBitSet<usize>,
66}
67
68impl LiveLoans {
69 pub(crate) fn new(num_points: usize, num_borrows: usize) -> Self {
70 Self { num_points, flat_matrix: DenseBitSet::new_empty(num_points * num_borrows) }
71 }
72 pub(crate) fn insert(&mut self, row: PointIndex, col: BorrowIndex) {
73 let bit_index = row.index() + self.num_points * col.index();
74 self.flat_matrix.insert(bit_index);
75 }
76 pub(crate) fn contains(&self, row: PointIndex, col: BorrowIndex) -> bool {
77 let bit_index = row.index() + self.num_points * col.index();
78 self.flat_matrix.contains(bit_index)
79 }
80}
81
82/// This struct holds the necessary
83/// - liveness data, created during MIR typeck, and which will be used to lazily compute the
84/// polonius localized constraints, during NLL region inference as well as MIR dumping,
85/// - data needed by the borrowck error computation and diagnostics.
86#[derive(Default)]
87pub(crate) struct PoloniusContext {
88 /// The graph from which we extract the localized outlives constraints.
89 graph: Option<LocalizedConstraintGraph>,
90
91 /// The expected edge direction per live region: the kind of directed edge we'll create as
92 /// liveness constraints depends on the variance of types with respect to each contained region.
93 pub(crate) live_region_variances: LiveRegionVariances,
94
95 /// The regions that outlive free regions are used to distinguish relevant live locals from
96 /// boring locals. A boring local is one whose type contains only such regions. Polonius
97 /// currently has more boring locals than NLLs so we record the latter to use in errors and
98 /// diagnostics, to focus on the locals we consider relevant and match NLL diagnostics.
99 pub(crate) boring_nll_locals: FxHashSet<Local>,
100}
101
102/// The direction a constraint can flow into. Used to create liveness constraints according to
103/// variance.
104#[derive(Copy, Clone, PartialEq, Eq, Debug)]
105pub(crate) enum ConstraintDirection {
106 /// For covariant cases, we add a forward edge `O at P1 -> O at P2`.
107 Forward,
108
109 /// For contravariant cases, we add a backward edge `O at P2 -> O at P1`
110 Backward,
111
112 /// For invariant cases, we add both the forward and backward edges `O at P1 <-> O at P2`.
113 Bidirectional,
114}
115
116impl PoloniusContext {
117 /// Computes live loans using the set of loans model for `-Zpolonius=next`.
118 ///
119 /// First, creates a constraint graph combining regions and CFG points, by:
120 /// - converting NLL typeck constraints to be localized
121 /// - encoding liveness constraints
122 ///
123 /// Then, this graph is traversed, reachability is recorded as loan liveness, to be used by the
124 /// loan scope and active loans computations.
125 ///
126 /// The constraint data will be used to compute errors and diagnostics.
127 pub(crate) fn compute_loan_liveness<'tcx>(
128 &mut self,
129 liveness: &mut LivenessValues,
130 outlives_constraints: impl Iterator<Item = OutlivesConstraint<'tcx>>,
131 universal_regions: &UniversalRegions<'tcx>,
132 body: &Body<'tcx>,
133 borrow_set: &BorrowSet<'tcx>,
134 num_points: usize,
135 ) {
136 // We don't need to prepare the graph (index NLL constraints, etc.) if we have no loans to
137 // trace throughout localized constraints.
138 if borrow_set.len() > 0 {
139 // From the outlives constraints, liveness, and variances, we can compute reachability
140 // on the lazy localized constraint graph to trace the liveness of loans, for the next
141 // step in the chain (the NLL loan scope and active loans computations).
142 let graph = LocalizedConstraintGraph::new(liveness, outlives_constraints);
143
144 let mut live_loans = LiveLoans::new(num_points, borrow_set.len());
145 let mut visitor = LoanLivenessVisitor { liveness, live_loans: &mut live_loans };
146 graph.traverse(
147 body,
148 liveness,
149 &self.live_region_variances,
150 universal_regions,
151 borrow_set,
152 &mut visitor,
153 );
154 liveness.record_live_loans(live_loans);
155
156 // The graph can be traversed again during MIR dumping, so we store it here.
157 self.graph = Some(graph);
158 }
159 }
160}
161
162/// Visitor to record loan liveness when traversing the localized constraint graph.
163struct LoanLivenessVisitor<'a> {
164 liveness: &'a LivenessValues,
165 live_loans: &'a mut LiveLoans,
166}
167
168impl LocalizedConstraintGraphVisitor for LoanLivenessVisitor<'_> {
169 fn on_node_traversed(&mut self, loan: BorrowIndex, node: LocalizedNode) {
170 // Record the loan as being live on entry to this point if it reaches a live region
171 // there.
172 //
173 // This is an approximation of liveness (which is the thing we want), in that we're
174 // using a single notion of reachability to represent what used to be _two_ different
175 // transitive closures. It didn't seem impactful when coming up with the single-graph
176 // and reachability through space (regions) + time (CFG) concepts, but in practice the
177 // combination of time-traveling with kills is more impactful than initially
178 // anticipated.
179 //
180 // Kills should prevent a loan from reaching its successor points in the CFG, but not
181 // while time-traveling: we're not actually at that CFG point, but looking for
182 // predecessor regions that contain the loan. One of the two TCs we had pushed the
183 // transitive subset edges to each point instead of having backward edges, and the
184 // problem didn't exist before. In the abstract, naive reachability is not enough to
185 // model this, we'd need a slightly different solution. For example, maybe with a
186 // two-step traversal:
187 // - at each point we first traverse the subgraph (and possibly time-travel) looking for
188 // exit nodes while ignoring kills,
189 // - and then when we're back at the current point, we continue normally.
190 //
191 // Another (less annoying) subtlety is that kills and the loan use-map are
192 // flow-insensitive. Kills can actually appear in places before a loan is introduced, or
193 // at a location that is actually unreachable in the CFG from the introduction point,
194 // and these can also be encountered during time-traveling.
195 //
196 // The simplest change that made sense to "fix" the issues above is taking into account
197 // kills that are:
198 // - reachable from the introduction point
199 // - encountered during forward traversal. Note that this is not transitive like the
200 // two-step traversal described above: only kills encountered on exit via a backward
201 // edge are ignored.
202 //
203 // This version of the analysis, however, is enough in practice to pass the tests that
204 // we care about and NLLs reject, without regressions on crater, and is an actionable
205 // subset of the full analysis. It also naturally points to areas of improvement that we
206 // wish to explore later, namely handling kills appropriately during traversal, instead
207 // of continuing traversal to all the reachable nodes.
208 //
209 // FIXME: analyze potential unsoundness, possibly in concert with a borrowck
210 // implementation in a-mir-formality, fuzzing, or manually crafting counter-examples.
211 if self.liveness.is_live_at_point(node.region, node.point) {
212 self.live_loans.insert(node.point, loan);
213 }
214 }
215}