Skip to main content

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