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}