rustc_borrowck/polonius/
loan_liveness.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
use std::collections::{BTreeMap, BTreeSet};

use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexSet};
use rustc_middle::mir::visit::Visitor;
use rustc_middle::mir::{
    Body, Local, Location, Place, Rvalue, Statement, StatementKind, Terminator, TerminatorKind,
};
use rustc_middle::ty::{RegionVid, TyCtxt};
use rustc_mir_dataflow::points::PointIndex;

use super::{LiveLoans, LocalizedOutlivesConstraintSet};
use crate::constraints::OutlivesConstraint;
use crate::dataflow::BorrowIndex;
use crate::region_infer::values::LivenessValues;
use crate::type_check::Locations;
use crate::{BorrowSet, PlaceConflictBias, places_conflict};

/// Compute loan reachability, stop at kills, and trace loan liveness throughout the CFG, by
/// traversing the full graph of constraints that combines:
/// - the localized constraints (the physical edges),
/// - with the constraints that hold at all points (the logical edges).
pub(super) fn compute_loan_liveness<'tcx>(
    tcx: TyCtxt<'tcx>,
    body: &Body<'tcx>,
    liveness: &LivenessValues,
    outlives_constraints: impl Iterator<Item = OutlivesConstraint<'tcx>>,
    borrow_set: &BorrowSet<'tcx>,
    localized_outlives_constraints: &LocalizedOutlivesConstraintSet,
) -> LiveLoans {
    let mut live_loans = LiveLoans::new(borrow_set.len());

    // FIXME: it may be preferable for kills to be encoded in the edges themselves, to simplify and
    // likely make traversal (and constraint generation) more efficient. We also display kills on
    // edges when visualizing the constraint graph anyways.
    let kills = collect_kills(body, tcx, borrow_set);

    // Create the full graph with the physical edges we've localized earlier, and the logical edges
    // of constraints that hold at all points.
    let logical_constraints =
        outlives_constraints.filter(|c| matches!(c.locations, Locations::All(_)));
    let graph = LocalizedConstraintGraph::new(&localized_outlives_constraints, logical_constraints);
    let mut visited = FxHashSet::default();
    let mut stack = Vec::new();

    // Compute reachability per loan by traversing each loan's subgraph starting from where it is
    // introduced.
    for (loan_idx, loan) in borrow_set.iter_enumerated() {
        visited.clear();
        stack.clear();

        let start_node = LocalizedNode {
            region: loan.region,
            point: liveness.point_from_location(loan.reserve_location),
        };
        stack.push(start_node);

        while let Some(node) = stack.pop() {
            if !visited.insert(node) {
                continue;
            }

            // Record the loan as being live on entry to this point.
            live_loans.insert(node.point, loan_idx);

            // Here, we have a conundrum. There's currently a weakness in our theory, in that
            // we're using a single notion of reachability to represent what used to be _two_
            // different transitive closures. It didn't seem impactful when coming up with the
            // single-graph and reachability through space (regions) + time (CFG) concepts, but in
            // practice the combination of time-traveling with kills is more impactful than
            // initially anticipated.
            //
            // Kills should prevent a loan from reaching its successor points in the CFG, but not
            // while time-traveling: we're not actually at that CFG point, but looking for
            // predecessor regions that contain the loan. One of the two TCs we had pushed the
            // transitive subset edges to each point instead of having backward edges, and the
            // problem didn't exist before. In the abstract, naive reachability is not enough to
            // model this, we'd need a slightly different solution. For example, maybe with a
            // two-step traversal:
            // - at each point we first traverse the subgraph (and possibly time-travel) looking for
            //   exit nodes while ignoring kills,
            // - and then when we're back at the current point, we continue normally.
            //
            // Another (less annoying) subtlety is that kills and the loan use-map are
            // flow-insensitive. Kills can actually appear in places before a loan is introduced, or
            // at a location that is actually unreachable in the CFG from the introduction point,
            // and these can also be encountered during time-traveling.
            //
            // The simplest change that made sense to "fix" the issues above is taking into
            // account kills that are:
            // - reachable from the introduction point
            // - encountered during forward traversal. Note that this is not transitive like the
            //   two-step traversal described above: only kills encountered on exit via a backward
            //   edge are ignored.
            //
            // In our test suite, there are a couple of cases where kills are encountered while
            // time-traveling, however as far as we can tell, always in cases where they would be
            // unreachable. We have reason to believe that this is a property of the single-graph
            // approach (but haven't proved it yet):
            // - reachable kills while time-traveling would also be encountered via regular
            //   traversal
            // - it makes _some_ sense to ignore unreachable kills, but subtleties around dead code
            //   in general need to be better thought through (like they were for NLLs).
            // - ignoring kills is a conservative approximation: the loan is still live and could
            //   cause false positive errors at another place access. Soundness issues in this
            //   domain should look more like the absence of reachability instead.
            //
            // This is enough in practice to pass tests, and therefore is what we have implemented
            // for now.
            //
            // FIXME: all of the above. Analyze potential unsoundness, possibly in concert with a
            // borrowck implementation in a-mir-formality, fuzzing, or manually crafting
            // counter-examples.

            // Continuing traversal will depend on whether the loan is killed at this point, and
            // whether we're time-traveling.
            let current_location = liveness.location_from_point(node.point);
            let is_loan_killed =
                kills.get(&current_location).is_some_and(|kills| kills.contains(&loan_idx));

            for succ in graph.outgoing_edges(node) {
                // If the loan is killed at this point, it is killed _on exit_. But only during
                // forward traversal.
                if is_loan_killed {
                    let destination = liveness.location_from_point(succ.point);
                    if current_location.is_predecessor_of(destination, body) {
                        continue;
                    }
                }
                stack.push(succ);
            }
        }
    }

    live_loans
}

/// The localized constraint graph indexes the physical and logical edges to compute a given node's
/// successors during traversal.
struct LocalizedConstraintGraph {
    /// The actual, physical, edges we have recorded for a given node.
    edges: FxHashMap<LocalizedNode, FxIndexSet<LocalizedNode>>,

    /// The logical edges representing the outlives constraints that hold at all points in the CFG,
    /// which we don't localize to avoid creating a lot of unnecessary edges in the graph. Some CFGs
    /// can be big, and we don't need to create such a physical edge for every point in the CFG.
    logical_edges: FxHashMap<RegionVid, FxIndexSet<RegionVid>>,
}

/// A node in the graph to be traversed, one of the two vertices of a localized outlives constraint.
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
struct LocalizedNode {
    region: RegionVid,
    point: PointIndex,
}

impl LocalizedConstraintGraph {
    /// Traverses the constraints and returns the indexed graph of edges per node.
    fn new<'tcx>(
        constraints: &LocalizedOutlivesConstraintSet,
        logical_constraints: impl Iterator<Item = OutlivesConstraint<'tcx>>,
    ) -> Self {
        let mut edges: FxHashMap<_, FxIndexSet<_>> = FxHashMap::default();
        for constraint in &constraints.outlives {
            let source = LocalizedNode { region: constraint.source, point: constraint.from };
            let target = LocalizedNode { region: constraint.target, point: constraint.to };
            edges.entry(source).or_default().insert(target);
        }

        let mut logical_edges: FxHashMap<_, FxIndexSet<_>> = FxHashMap::default();
        for constraint in logical_constraints {
            logical_edges.entry(constraint.sup).or_default().insert(constraint.sub);
        }

        LocalizedConstraintGraph { edges, logical_edges }
    }

    /// Returns the outgoing edges of a given node, not its transitive closure.
    fn outgoing_edges(&self, node: LocalizedNode) -> impl Iterator<Item = LocalizedNode> + use<'_> {
        // The outgoing edges are:
        // - the physical edges present at this node,
        // - the materialized logical edges that exist virtually at all points for this node's
        //   region, localized at this point.
        let physical_edges =
            self.edges.get(&node).into_iter().flat_map(|targets| targets.iter().copied());
        let materialized_edges =
            self.logical_edges.get(&node.region).into_iter().flat_map(move |targets| {
                targets
                    .iter()
                    .copied()
                    .map(move |target| LocalizedNode { point: node.point, region: target })
            });
        physical_edges.chain(materialized_edges)
    }
}

/// Traverses the MIR and collects kills.
fn collect_kills<'tcx>(
    body: &Body<'tcx>,
    tcx: TyCtxt<'tcx>,
    borrow_set: &BorrowSet<'tcx>,
) -> BTreeMap<Location, BTreeSet<BorrowIndex>> {
    let mut collector = KillsCollector { borrow_set, tcx, body, kills: BTreeMap::default() };
    for (block, data) in body.basic_blocks.iter_enumerated() {
        collector.visit_basic_block_data(block, data);
    }
    collector.kills
}

struct KillsCollector<'a, 'tcx> {
    body: &'a Body<'tcx>,
    tcx: TyCtxt<'tcx>,
    borrow_set: &'a BorrowSet<'tcx>,

    /// The set of loans killed at each location.
    kills: BTreeMap<Location, BTreeSet<BorrowIndex>>,
}

// This visitor has a similar structure to the `Borrows` dataflow computation with respect to kills,
// and the datalog polonius fact generation for the `loan_killed_at` relation.
impl<'tcx> KillsCollector<'_, 'tcx> {
    /// Records the borrows on the specified place as `killed`. For example, when assigning to a
    /// local, or on a call's return destination.
    fn record_killed_borrows_for_place(&mut self, place: Place<'tcx>, location: Location) {
        // For the reasons described in graph traversal, we also filter out kills
        // unreachable from the loan's introduction point, as they would stop traversal when
        // e.g. checking for reachability in the subset graph through invariance constraints
        // higher up.
        let filter_unreachable_kills = |loan| {
            let introduction = self.borrow_set[loan].reserve_location;
            let reachable = introduction.is_predecessor_of(location, self.body);
            reachable
        };

        let other_borrows_of_local = self
            .borrow_set
            .local_map
            .get(&place.local)
            .into_iter()
            .flat_map(|bs| bs.iter())
            .copied();

        // If the borrowed place is a local with no projections, all other borrows of this
        // local must conflict. This is purely an optimization so we don't have to call
        // `places_conflict` for every borrow.
        if place.projection.is_empty() {
            if !self.body.local_decls[place.local].is_ref_to_static() {
                self.kills
                    .entry(location)
                    .or_default()
                    .extend(other_borrows_of_local.filter(|&loan| filter_unreachable_kills(loan)));
            }
            return;
        }

        // By passing `PlaceConflictBias::NoOverlap`, we conservatively assume that any given
        // pair of array indices are not equal, so that when `places_conflict` returns true, we
        // will be assured that two places being compared definitely denotes the same sets of
        // locations.
        let definitely_conflicting_borrows = other_borrows_of_local
            .filter(|&i| {
                places_conflict(
                    self.tcx,
                    self.body,
                    self.borrow_set[i].borrowed_place,
                    place,
                    PlaceConflictBias::NoOverlap,
                )
            })
            .filter(|&loan| filter_unreachable_kills(loan));

        self.kills.entry(location).or_default().extend(definitely_conflicting_borrows);
    }

    /// Records the borrows on the specified local as `killed`.
    fn record_killed_borrows_for_local(&mut self, local: Local, location: Location) {
        if let Some(borrow_indices) = self.borrow_set.local_map.get(&local) {
            self.kills.entry(location).or_default().extend(borrow_indices.iter());
        }
    }
}

impl<'tcx> Visitor<'tcx> for KillsCollector<'_, 'tcx> {
    fn visit_statement(&mut self, statement: &Statement<'tcx>, location: Location) {
        // Make sure there are no remaining borrows for locals that have gone out of scope.
        if let StatementKind::StorageDead(local) = statement.kind {
            self.record_killed_borrows_for_local(local, location);
        }

        self.super_statement(statement, location);
    }

    fn visit_assign(&mut self, place: &Place<'tcx>, rvalue: &Rvalue<'tcx>, location: Location) {
        // When we see `X = ...`, then kill borrows of `(*X).foo` and so forth.
        self.record_killed_borrows_for_place(*place, location);
        self.super_assign(place, rvalue, location);
    }

    fn visit_terminator(&mut self, terminator: &Terminator<'tcx>, location: Location) {
        // A `Call` terminator's return value can be a local which has borrows, so we need to record
        // those as killed as well.
        if let TerminatorKind::Call { destination, .. } = terminator.kind {
            self.record_killed_borrows_for_place(destination, location);
        }

        self.super_terminator(terminator, location);
    }
}