Skip to content

Commit 263b07f

Browse files
committed
Auto merge of rust-lang#113218 - lqd:polonius-scopes, r=<try>
Compute NLL loan scopes using the polonius model Draft PR to guide discussion for this monday's types team meeting, where the plan is to read this and review as a group. For a *location-insensitive* analysis (that is, without expressiveness improvements for users yet), this PR implements loans going out of scope using reachability and liveness, rather than checking if the issuing region's values contain a given CFG point. This is equivalent to NLL scopes and computes the same data. There are a few fixmes left, and I'll still work on them before the meeting. A couple of notes: - I started prototyping this a while ago so it follows the structure of the _previous_ NLL `OutOfScopePrecomputer`, before the couple recent refactors. They probably could/should look the same instead. - there are some assumptions about placeholders, free regions, and member constraints that I think hold, and they're documented in the code - this passes all the UI tests with `-Zpolonius=next` -- though the additional work done can be felt on the huge numeric/float tests, but the perf is [not terrible](rust-lang#112432 (comment)) and there are a bunch of ways to improve it in the future. (cc `@matthewjasper` since you asked to be pinged about it) r? `@ghost`
2 parents 90f3a6f + 51ed991 commit 263b07f

File tree

10 files changed

+413
-37
lines changed

10 files changed

+413
-37
lines changed

compiler/rustc_borrowck/src/dataflow.rs

+193-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#![deny(rustc::untranslatable_diagnostic)]
22
#![deny(rustc::diagnostic_outside_of_impl)]
33
use rustc_data_structures::fx::FxIndexMap;
4+
use rustc_data_structures::graph::WithSuccessors;
45
use rustc_index::bit_set::BitSet;
56
use rustc_middle::mir::{
67
self, BasicBlock, Body, CallReturnPlaces, Location, Place, TerminatorEdges,
@@ -122,7 +123,6 @@ rustc_index::newtype_index! {
122123
pub struct Borrows<'a, 'tcx> {
123124
tcx: TyCtxt<'tcx>,
124125
body: &'a Body<'tcx>,
125-
126126
borrow_set: &'a BorrowSet<'tcx>,
127127
borrows_out_of_scope_at_location: FxIndexMap<Location, Vec<BorrowIndex>>,
128128
}
@@ -222,6 +222,7 @@ impl<'tcx> OutOfScopePrecomputer<'_, 'tcx> {
222222
}
223223
}
224224

225+
// This is `pub` because it's used by unstable external borrowck data users, see `consumers.rs`.
225226
pub fn calculate_borrows_out_of_scope_at_location<'tcx>(
226227
body: &Body<'tcx>,
227228
regioncx: &RegionInferenceContext<'tcx>,
@@ -238,15 +239,196 @@ pub fn calculate_borrows_out_of_scope_at_location<'tcx>(
238239
prec.borrows_out_of_scope_at_location
239240
}
240241

242+
struct PoloniusOutOfScopePrecomputer<'a, 'tcx> {
243+
visited: BitSet<mir::BasicBlock>,
244+
visit_stack: Vec<mir::BasicBlock>,
245+
body: &'a Body<'tcx>,
246+
regioncx: &'a RegionInferenceContext<'tcx>,
247+
248+
loans_out_of_scope_at_location: FxIndexMap<Location, Vec<BorrowIndex>>,
249+
}
250+
251+
impl<'a, 'tcx> PoloniusOutOfScopePrecomputer<'a, 'tcx> {
252+
fn new(body: &'a Body<'tcx>, regioncx: &'a RegionInferenceContext<'tcx>) -> Self {
253+
Self {
254+
visited: BitSet::new_empty(body.basic_blocks.len()),
255+
visit_stack: vec![],
256+
body,
257+
regioncx,
258+
loans_out_of_scope_at_location: FxIndexMap::default(),
259+
}
260+
}
261+
}
262+
263+
impl<'tcx> PoloniusOutOfScopePrecomputer<'_, 'tcx> {
264+
/// Loans are in scope while they are live: whether they are contained within any live region.
265+
/// In the location-insensitive analysis, a loan will be contained in a region if the issuing
266+
/// region can reach it in the subset graph. So this is a reachability problem.
267+
fn precompute_loans_out_of_scope(
268+
&mut self,
269+
loan_idx: BorrowIndex,
270+
issuing_region: RegionVid,
271+
loan_issued_at: Location,
272+
) {
273+
let sccs = self.regioncx.constraint_sccs();
274+
let issuing_region_scc = sccs.scc(issuing_region);
275+
276+
// We first handle the cases where the loan doesn't go out of scope, depending on the issuing
277+
// region's successors.
278+
for scc in sccs.depth_first_search(issuing_region_scc) {
279+
// 1. Via member constraints
280+
//
281+
// The issuing region can flow into the choice regions, and they are either:
282+
// - placeholders or free regions themselves,
283+
// - or also transitively outlive a free region.
284+
//
285+
// That is to say, if there are member constraints here, the loan escapes the function
286+
// and cannot go out of scope. We can early return.
287+
if self.regioncx.scc_has_member_constraints(scc) {
288+
return;
289+
}
290+
291+
// 2. Via regions that are live at all points: placeholders and free regions.
292+
//
293+
// If the issuing region outlives such a region, its loan escapes the function and
294+
// cannot go out of scope. We can early return.
295+
if self.regioncx.scc_is_live_at_all_points(scc) {
296+
return;
297+
}
298+
}
299+
300+
let first_block = loan_issued_at.block;
301+
let first_bb_data = &self.body.basic_blocks[first_block];
302+
303+
// The first block we visit is the one where the loan is issued, starting from the statement
304+
// where the loan is issued: at `loan_issued_at`.
305+
let first_lo = loan_issued_at.statement_index;
306+
let first_hi = first_bb_data.statements.len();
307+
308+
if let Some(kill_location) =
309+
self.loan_kill_location(loan_idx, loan_issued_at, first_block, first_lo, first_hi)
310+
{
311+
debug!("loan {:?} gets killed at {:?}", loan_idx, kill_location);
312+
self.loans_out_of_scope_at_location.entry(kill_location).or_default().push(loan_idx);
313+
314+
// The loan dies within the first block, we're done and can early return.
315+
return;
316+
}
317+
318+
// The loan is not dead. Add successor BBs to the work list, if necessary.
319+
for succ_bb in first_bb_data.terminator().successors() {
320+
if self.visited.insert(succ_bb) {
321+
self.visit_stack.push(succ_bb);
322+
}
323+
}
324+
325+
// We may end up visiting `first_block` again. This is not an issue: we know at this point
326+
// that the loan is not killed in the `first_lo..=first_hi` range, so checking the
327+
// `0..first_lo` range and the `0..first_hi` range gives the same result.
328+
while let Some(block) = self.visit_stack.pop() {
329+
let bb_data = &self.body[block];
330+
let num_stmts = bb_data.statements.len();
331+
if let Some(kill_location) =
332+
self.loan_kill_location(loan_idx, loan_issued_at, block, 0, num_stmts)
333+
{
334+
debug!("loan {:?} gets killed at {:?}", loan_idx, kill_location);
335+
self.loans_out_of_scope_at_location
336+
.entry(kill_location)
337+
.or_default()
338+
.push(loan_idx);
339+
340+
// The loan dies within this block, so we don't need to visit its successors.
341+
continue;
342+
}
343+
344+
// Add successor BBs to the work list, if necessary.
345+
for succ_bb in bb_data.terminator().successors() {
346+
if self.visited.insert(succ_bb) {
347+
self.visit_stack.push(succ_bb);
348+
}
349+
}
350+
}
351+
352+
self.visited.clear();
353+
assert!(self.visit_stack.is_empty(), "visit stack should be empty");
354+
}
355+
356+
/// Returns the lowest statement in `start..=end`, where the loan goes out of scope, if any.
357+
/// This is the statement where the issuing region can't reach any of the regions that are live
358+
/// at this point.
359+
fn loan_kill_location(
360+
&self,
361+
loan_idx: BorrowIndex,
362+
loan_issued_at: Location,
363+
block: BasicBlock,
364+
start: usize,
365+
end: usize,
366+
) -> Option<Location> {
367+
for statement_index in start..=end {
368+
let location = Location { block, statement_index };
369+
370+
// Check whether the issuing region can reach local regions that are live at this point:
371+
// - a loan is always live at its issuing location because it can reach the issuing
372+
// region, which is always live at this location.
373+
if location == loan_issued_at {
374+
continue;
375+
}
376+
377+
// - the loan goes out of scope at `location` if it's not contained within any regions
378+
// live at this point.
379+
//
380+
// FIXME: if the issuing region `i` can reach a live region `r` at point `p`, and `r` is
381+
// live at point `q`, then it's guaranteed that `i` would reach `r` at point `q`.
382+
// Reachability is location-insensitive, and we could take advantage of that, by jumping
383+
// to a further point than just the next statement: we can jump to the furthest point
384+
// within the block where `r` is live.
385+
if self.regioncx.is_loan_live_at(loan_idx, location) {
386+
continue;
387+
}
388+
389+
// No live region is reachable from the issuing region: the loan is killed at this
390+
// point.
391+
return Some(location);
392+
}
393+
394+
None
395+
}
396+
}
397+
241398
impl<'a, 'tcx> Borrows<'a, 'tcx> {
242399
pub fn new(
243400
tcx: TyCtxt<'tcx>,
244401
body: &'a Body<'tcx>,
245-
nonlexical_regioncx: &'a RegionInferenceContext<'tcx>,
402+
regioncx: &'a RegionInferenceContext<'tcx>,
246403
borrow_set: &'a BorrowSet<'tcx>,
247404
) -> Self {
248-
let borrows_out_of_scope_at_location =
249-
calculate_borrows_out_of_scope_at_location(body, nonlexical_regioncx, borrow_set);
405+
let mut borrows_out_of_scope_at_location =
406+
calculate_borrows_out_of_scope_at_location(body, regioncx, borrow_set);
407+
408+
// The in-tree polonius analysis computes loans going out of scope using the set-of-loans
409+
// model, and makes sure they're identical to the existing computation of the set-of-points
410+
// model.
411+
if tcx.sess.opts.unstable_opts.polonius.is_next_enabled() {
412+
let mut polonius_prec = PoloniusOutOfScopePrecomputer::new(body, regioncx);
413+
for (loan_idx, loan_data) in borrow_set.iter_enumerated() {
414+
let issuing_region = loan_data.region;
415+
let issued_location = loan_data.reserve_location;
416+
417+
polonius_prec.precompute_loans_out_of_scope(
418+
loan_idx,
419+
issuing_region,
420+
issued_location,
421+
);
422+
}
423+
424+
assert_eq!(
425+
borrows_out_of_scope_at_location, polonius_prec.loans_out_of_scope_at_location,
426+
"the loans out of scope must be the same as the borrows out of scope"
427+
);
428+
429+
borrows_out_of_scope_at_location = polonius_prec.loans_out_of_scope_at_location;
430+
}
431+
250432
Borrows { tcx, body, borrow_set, borrows_out_of_scope_at_location }
251433
}
252434

@@ -333,6 +515,13 @@ impl<'tcx> rustc_mir_dataflow::AnalysisDomain<'tcx> for Borrows<'_, 'tcx> {
333515
}
334516
}
335517

518+
/// Forward dataflow computation of the set of borrows that are in scope at a particular location.
519+
/// - we gen the introduced loans
520+
/// - we kill loans on locals going out of (regular) scope
521+
/// - we kill the loans going out of their region's NLL scope: in NLL terms, the frontier where a
522+
/// region stops containing the CFG points reachable from the issuing location.
523+
/// - we also kill loans of conflicting places when overwriting a shared path: e.g. borrows of
524+
/// `a.b.c` when `a` is overwritten.
336525
impl<'tcx> rustc_mir_dataflow::GenKillAnalysis<'tcx> for Borrows<'_, 'tcx> {
337526
type Idx = BorrowIndex;
338527

compiler/rustc_borrowck/src/facts.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ pub(crate) trait AllFactsExt {
4141
impl AllFactsExt for AllFacts {
4242
/// Return
4343
fn enabled(tcx: TyCtxt<'_>) -> bool {
44-
tcx.sess.opts.unstable_opts.nll_facts || tcx.sess.opts.unstable_opts.polonius
44+
tcx.sess.opts.unstable_opts.nll_facts
45+
|| tcx.sess.opts.unstable_opts.polonius.is_legacy_enabled()
4546
}
4647

4748
fn write_to_dir(

compiler/rustc_borrowck/src/nll.rs

+24-18
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,11 @@ pub(crate) fn compute_regions<'cx, 'tcx>(
169169
upvars: &[Upvar<'tcx>],
170170
consumer_options: Option<ConsumerOptions>,
171171
) -> NllOutput<'tcx> {
172+
let is_polonius_legacy_enabled = infcx.tcx.sess.opts.unstable_opts.polonius.is_legacy_enabled();
172173
let polonius_input = consumer_options.map(|c| c.polonius_input()).unwrap_or_default()
173-
|| infcx.tcx.sess.opts.unstable_opts.polonius;
174+
|| is_polonius_legacy_enabled;
174175
let polonius_output = consumer_options.map(|c| c.polonius_output()).unwrap_or_default()
175-
|| infcx.tcx.sess.opts.unstable_opts.polonius;
176+
|| is_polonius_legacy_enabled;
176177
let mut all_facts =
177178
(polonius_input || AllFacts::enabled(infcx.tcx)).then_some(AllFacts::default());
178179

@@ -181,22 +182,26 @@ pub(crate) fn compute_regions<'cx, 'tcx>(
181182
let elements = &Rc::new(RegionValueElements::new(&body));
182183

183184
// Run the MIR type-checker.
184-
let MirTypeckResults { constraints, universal_region_relations, opaque_type_values } =
185-
type_check::type_check(
186-
infcx,
187-
param_env,
188-
body,
189-
promoted,
190-
&universal_regions,
191-
location_table,
192-
borrow_set,
193-
&mut all_facts,
194-
flow_inits,
195-
move_data,
196-
elements,
197-
upvars,
198-
polonius_input,
199-
);
185+
let MirTypeckResults {
186+
constraints,
187+
universal_region_relations,
188+
opaque_type_values,
189+
live_loans,
190+
} = type_check::type_check(
191+
infcx,
192+
param_env,
193+
body,
194+
promoted,
195+
&universal_regions,
196+
location_table,
197+
borrow_set,
198+
&mut all_facts,
199+
flow_inits,
200+
move_data,
201+
elements,
202+
upvars,
203+
polonius_input,
204+
);
200205

201206
if let Some(all_facts) = &mut all_facts {
202207
let _prof_timer = infcx.tcx.prof.generic_activity("polonius_fact_generation");
@@ -274,6 +279,7 @@ pub(crate) fn compute_regions<'cx, 'tcx>(
274279
type_tests,
275280
liveness_constraints,
276281
elements,
282+
live_loans,
277283
);
278284

279285
// Generate various additional constraints.

0 commit comments

Comments
 (0)