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