Skip to content

Commit 09f3c90

Browse files
authored
Rollup merge of #70950 - nikomatsakis:leak-check-nll-2, r=matthewjasper
extend NLL checker to understand `'empty` combined with universes This PR extends the NLL region checker to understand `'empty` combined with universes. In particular, it means that the NLL region checker no longer considers `exists<R2> { forall<R1> { R1: R2 } }` to be provable. This is work towards #59490, but we're not all the way there. One thing in particular it does not address is error messages. The modifications to the NLL region inference code turned out to be simpler than expected. The main change is to require that if `R1: R2` then `universe(R1) <= universe(R2)`. This constraint follows from the region lattice (shown below), because we assume then that `R2` is "at least" `empty(Universe(R2))`, and hence if `R1: R2` (i.e., `R1 >= R2` on the lattice) then `R1` must be in some universe that can name `'empty(Universe(R2))`, which requires that `Universe(R1) <= Universe(R2)`. ``` static ----------+-----...------+ (greatest) | | | early-bound and | | free regions | | | | | scope regions | | | | | empty(root) placeholder(U1) | | / | | / placeholder(Un) empty(U1) -- / | / ... / | / empty(Un) -------- (smallest) ``` I also made what turned out to be a somewhat unrelated change to add a special region to represent `'empty(U0)`, which we use (somewhat hackily) to indicate well-formedness checks in some parts of the compiler. This fixes #68550. I did some investigation into fixing the error message situation. That's a bit trickier: the existing "nice region error" code around placeholders relies on having better error tracing than NLL currently provides, so that it knows (e.g.) that the constraint arose from applying a trait impl and things like that. I feel like I was hoping *not* to do such fine-grained tracing in NLL, and it seems like we...largely...got away with that. I'm not sure yet if we'll have to add more tracing information or if there is some sort of alternative. It's worth pointing out though that I've not kind of shifted my opinion on whose job it should be to enforce lifetimes: I tend to think we ought to be moving back towards *something like* the leak-check (just not the one we *had*). If we took that approach, it would actually resolve this aspect of the error message problem, because we would be resolving 'higher-ranked errors' in the trait solver itself, and hence we wouldn't have to thread as much causal information back to the region checker. I think it would also help us with removing the leak check while not breaking some of the existing crates out there. Regardless, I think it's worth landing this change, because it was relatively simple and it aligns the set of programs that NLL accepts with those that are accepted by the main region checker, and hence should at least *help* us in migration (though I guess we still also have to resolve the existing crates that rely on leak check for coherence). r? @matthewjasper
2 parents eece58a + cb9458d commit 09f3c90

20 files changed

+403
-223
lines changed

src/librustc_data_structures/graph/scc/mod.rs

+5
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ impl<N: Idx, S: Idx> Sccs<N, S> {
4747
}
4848

4949
/// Returns an iterator over the SCCs in the graph.
50+
///
51+
/// The SCCs will be iterated in **dependency order** (or **post order**),
52+
/// meaning that if `S1 -> S2`, we will visit `S2` first and `S1` after.
53+
/// This is convenient when the edges represent dependencies: when you visit
54+
/// `S1`, the value for `S2` will already have been computed.
5055
pub fn all_sccs(&self) -> impl Iterator<Item = S> {
5156
(0..self.scc_data.len()).map(S::new)
5257
}

src/librustc_infer/infer/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,9 @@ pub enum NLLRegionVariableOrigin {
472472
/// from a `for<'a> T` binder). Meant to represent "any region".
473473
Placeholder(ty::PlaceholderRegion),
474474

475+
/// The variable we create to represent `'empty(U0)`.
476+
RootEmptyRegion,
477+
475478
Existential {
476479
/// If this is true, then this variable was created to represent a lifetime
477480
/// bound in a `for` binder. For example, it might have been created to
@@ -493,6 +496,7 @@ impl NLLRegionVariableOrigin {
493496
NLLRegionVariableOrigin::FreeRegion => true,
494497
NLLRegionVariableOrigin::Placeholder(..) => true,
495498
NLLRegionVariableOrigin::Existential { .. } => false,
499+
NLLRegionVariableOrigin::RootEmptyRegion => false,
496500
}
497501
}
498502

src/librustc_mir/borrow_check/region_infer/mod.rs

+100-43
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use rustc_data_structures::frozen::Frozen;
66
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
77
use rustc_data_structures::graph::scc::Sccs;
88
use rustc_hir::def_id::DefId;
9-
use rustc_index::bit_set::BitSet;
109
use rustc_index::vec::IndexVec;
1110
use rustc_infer::infer::canonical::QueryOutlivesConstraint;
1211
use rustc_infer::infer::region_constraints::{GenericKind, VarInfos, VerifyBound};
@@ -315,16 +314,81 @@ impl<'tcx> RegionInferenceContext<'tcx> {
315314
/// SCC could have as well. This implies that the SCC must have
316315
/// the minimum, or narrowest, universe.
317316
fn compute_scc_universes(
318-
constraints_scc: &Sccs<RegionVid, ConstraintSccIndex>,
317+
constraint_sccs: &Sccs<RegionVid, ConstraintSccIndex>,
319318
definitions: &IndexVec<RegionVid, RegionDefinition<'tcx>>,
320319
) -> IndexVec<ConstraintSccIndex, ty::UniverseIndex> {
321-
let num_sccs = constraints_scc.num_sccs();
320+
let num_sccs = constraint_sccs.num_sccs();
322321
let mut scc_universes = IndexVec::from_elem_n(ty::UniverseIndex::MAX, num_sccs);
323322

323+
debug!("compute_scc_universes()");
324+
325+
// For each region R in universe U, ensure that the universe for the SCC
326+
// that contains R is "no bigger" than U. This effectively sets the universe
327+
// for each SCC to be the minimum of the regions within.
324328
for (region_vid, region_definition) in definitions.iter_enumerated() {
325-
let scc = constraints_scc.scc(region_vid);
329+
let scc = constraint_sccs.scc(region_vid);
326330
let scc_universe = &mut scc_universes[scc];
327-
*scc_universe = ::std::cmp::min(*scc_universe, region_definition.universe);
331+
let scc_min = std::cmp::min(region_definition.universe, *scc_universe);
332+
if scc_min != *scc_universe {
333+
*scc_universe = scc_min;
334+
debug!(
335+
"compute_scc_universes: lowered universe of {scc:?} to {scc_min:?} \
336+
because it contains {region_vid:?} in {region_universe:?}",
337+
scc = scc,
338+
scc_min = scc_min,
339+
region_vid = region_vid,
340+
region_universe = region_definition.universe,
341+
);
342+
}
343+
}
344+
345+
// Walk each SCC `A` and `B` such that `A: B`
346+
// and ensure that universe(A) can see universe(B).
347+
//
348+
// This serves to enforce the 'empty/placeholder' hierarchy
349+
// (described in more detail on `RegionKind`):
350+
//
351+
// ```
352+
// static -----+
353+
// | |
354+
// empty(U0) placeholder(U1)
355+
// | /
356+
// empty(U1)
357+
// ```
358+
//
359+
// In particular, imagine we have variables R0 in U0 and R1
360+
// created in U1, and constraints like this;
361+
//
362+
// ```
363+
// R1: !1 // R1 outlives the placeholder in U1
364+
// R1: R0 // R1 outlives R0
365+
// ```
366+
//
367+
// Here, we wish for R1 to be `'static`, because it
368+
// cannot outlive `placeholder(U1)` and `empty(U0)` any other way.
369+
//
370+
// Thanks to this loop, what happens is that the `R1: R0`
371+
// constraint lowers the universe of `R1` to `U0`, which in turn
372+
// means that the `R1: !1` constraint will (later) cause
373+
// `R1` to become `'static`.
374+
for scc_a in constraint_sccs.all_sccs() {
375+
for &scc_b in constraint_sccs.successors(scc_a) {
376+
let scc_universe_a = scc_universes[scc_a];
377+
let scc_universe_b = scc_universes[scc_b];
378+
let scc_universe_min = std::cmp::min(scc_universe_a, scc_universe_b);
379+
if scc_universe_a != scc_universe_min {
380+
scc_universes[scc_a] = scc_universe_min;
381+
382+
debug!(
383+
"compute_scc_universes: lowered universe of {scc_a:?} to {scc_universe_min:?} \
384+
because {scc_a:?}: {scc_b:?} and {scc_b:?} is in universe {scc_universe_b:?}",
385+
scc_a = scc_a,
386+
scc_b = scc_b,
387+
scc_universe_min = scc_universe_min,
388+
scc_universe_b = scc_universe_b
389+
);
390+
}
391+
}
328392
}
329393

330394
debug!("compute_scc_universes: scc_universe = {:#?}", scc_universes);
@@ -416,7 +480,8 @@ impl<'tcx> RegionInferenceContext<'tcx> {
416480
}
417481
}
418482

419-
NLLRegionVariableOrigin::Existential { .. } => {
483+
NLLRegionVariableOrigin::RootEmptyRegion
484+
| NLLRegionVariableOrigin::Existential { .. } => {
420485
// For existential, regions, nothing to do.
421486
}
422487
}
@@ -550,47 +615,27 @@ impl<'tcx> RegionInferenceContext<'tcx> {
550615
// SCC. For each SCC, we visit its successors and compute
551616
// their values, then we union all those values to get our
552617
// own.
553-
let visited = &mut BitSet::new_empty(self.constraint_sccs.num_sccs());
554-
for scc_index in self.constraint_sccs.all_sccs() {
555-
self.propagate_constraint_sccs_if_new(scc_index, visited);
618+
let constraint_sccs = self.constraint_sccs.clone();
619+
for scc in constraint_sccs.all_sccs() {
620+
self.compute_value_for_scc(scc);
556621
}
557622

558623
// Sort the applied member constraints so we can binary search
559624
// through them later.
560625
self.member_constraints_applied.sort_by_key(|applied| applied.member_region_scc);
561626
}
562627

563-
/// Computes the value of the SCC `scc_a` if it has not already
564-
/// been computed. The `visited` parameter is a bitset
565-
#[inline]
566-
fn propagate_constraint_sccs_if_new(
567-
&mut self,
568-
scc_a: ConstraintSccIndex,
569-
visited: &mut BitSet<ConstraintSccIndex>,
570-
) {
571-
if visited.insert(scc_a) {
572-
self.propagate_constraint_sccs_new(scc_a, visited);
573-
}
574-
}
575-
576628
/// Computes the value of the SCC `scc_a`, which has not yet been
577-
/// computed. This works by first computing all successors of the
578-
/// SCC (if they haven't been computed already) and then unioning
579-
/// together their elements.
580-
fn propagate_constraint_sccs_new(
581-
&mut self,
582-
scc_a: ConstraintSccIndex,
583-
visited: &mut BitSet<ConstraintSccIndex>,
584-
) {
629+
/// computed, by unioning the values of its successors.
630+
/// Assumes that all successors have been computed already
631+
/// (which is assured by iterating over SCCs in dependency order).
632+
fn compute_value_for_scc(&mut self, scc_a: ConstraintSccIndex) {
585633
let constraint_sccs = self.constraint_sccs.clone();
586634

587635
// Walk each SCC `B` such that `A: B`...
588636
for &scc_b in constraint_sccs.successors(scc_a) {
589637
debug!("propagate_constraint_sccs: scc_a = {:?} scc_b = {:?}", scc_a, scc_b);
590638

591-
// ...compute the value of `B`...
592-
self.propagate_constraint_sccs_if_new(scc_b, visited);
593-
594639
// ...and add elements from `B` into `A`. One complication
595640
// arises because of universes: If `B` contains something
596641
// that `A` cannot name, then `A` can only contain `B` if
@@ -1258,7 +1303,8 @@ impl<'tcx> RegionInferenceContext<'tcx> {
12581303
self.check_bound_universal_region(fr, placeholder, errors_buffer);
12591304
}
12601305

1261-
NLLRegionVariableOrigin::Existential { .. } => {
1306+
NLLRegionVariableOrigin::RootEmptyRegion
1307+
| NLLRegionVariableOrigin::Existential { .. } => {
12621308
// nothing to check here
12631309
}
12641310
}
@@ -1360,7 +1406,8 @@ impl<'tcx> RegionInferenceContext<'tcx> {
13601406
self.check_bound_universal_region(fr, placeholder, errors_buffer);
13611407
}
13621408

1363-
NLLRegionVariableOrigin::Existential { .. } => {
1409+
NLLRegionVariableOrigin::RootEmptyRegion
1410+
| NLLRegionVariableOrigin::Existential { .. } => {
13641411
// nothing to check here
13651412
}
13661413
}
@@ -1633,9 +1680,9 @@ impl<'tcx> RegionInferenceContext<'tcx> {
16331680
universe1.cannot_name(placeholder.universe)
16341681
}
16351682

1636-
NLLRegionVariableOrigin::FreeRegion | NLLRegionVariableOrigin::Existential { .. } => {
1637-
false
1638-
}
1683+
NLLRegionVariableOrigin::RootEmptyRegion
1684+
| NLLRegionVariableOrigin::FreeRegion
1685+
| NLLRegionVariableOrigin::Existential { .. } => false,
16391686
}
16401687
}
16411688

@@ -1773,6 +1820,12 @@ impl<'tcx> RegionInferenceContext<'tcx> {
17731820
/// Finds some region R such that `fr1: R` and `R` is live at `elem`.
17741821
crate fn find_sub_region_live_at(&self, fr1: RegionVid, elem: Location) -> RegionVid {
17751822
debug!("find_sub_region_live_at(fr1={:?}, elem={:?})", fr1, elem);
1823+
debug!("find_sub_region_live_at: {:?} is in scc {:?}", fr1, self.constraint_sccs.scc(fr1));
1824+
debug!(
1825+
"find_sub_region_live_at: {:?} is in universe {:?}",
1826+
fr1,
1827+
self.scc_universes[self.constraint_sccs.scc(fr1)]
1828+
);
17761829
self.find_constraint_paths_between_regions(fr1, |r| {
17771830
// First look for some `r` such that `fr1: r` and `r` is live at `elem`
17781831
debug!(
@@ -1794,13 +1847,16 @@ impl<'tcx> RegionInferenceContext<'tcx> {
17941847
.or_else(|| {
17951848
// If we fail to find THAT, it may be that `fr1` is a
17961849
// placeholder that cannot "fit" into its SCC. In that
1797-
// case, there should be some `r` where `fr1: r`, both
1798-
// `fr1` and `r` are in the same SCC, and `fr1` is a
1850+
// case, there should be some `r` where `fr1: r` and `fr1` is a
17991851
// placeholder that `r` cannot name. We can blame that
18001852
// edge.
1853+
//
1854+
// Remember that if `R1: R2`, then the universe of R1
1855+
// must be able to name the universe of R2, because R2 will
1856+
// be at least `'empty(Universe(R2))`, and `R1` must be at
1857+
// larger than that.
18011858
self.find_constraint_paths_between_regions(fr1, |r| {
1802-
self.constraint_sccs.scc(fr1) == self.constraint_sccs.scc(r)
1803-
&& self.cannot_name_placeholder(r, fr1)
1859+
self.cannot_name_placeholder(r, fr1)
18041860
})
18051861
})
18061862
.map(|(_path, r)| r)
@@ -1944,7 +2000,8 @@ impl<'tcx> RegionInferenceContext<'tcx> {
19442000
let blame_source = match from_region_origin {
19452001
NLLRegionVariableOrigin::FreeRegion
19462002
| NLLRegionVariableOrigin::Existential { from_forall: false } => true,
1947-
NLLRegionVariableOrigin::Placeholder(_)
2003+
NLLRegionVariableOrigin::RootEmptyRegion
2004+
| NLLRegionVariableOrigin::Placeholder(_)
19482005
| NLLRegionVariableOrigin::Existential { from_forall: true } => false,
19492006
};
19502007

src/librustc_mir/borrow_check/type_check/constraint_conversion.rs

-8
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,6 @@ impl<'a, 'b, 'tcx> TypeOutlivesDelegate<'tcx> for &'a mut ConstraintConversion<'
160160
a: ty::Region<'tcx>,
161161
b: ty::Region<'tcx>,
162162
) {
163-
// FIXME -- this is not the fix I would prefer
164-
if let ty::ReEmpty(ty::UniverseIndex::ROOT) = a {
165-
return;
166-
}
167163
let b = self.to_region_vid(b);
168164
let a = self.to_region_vid(a);
169165
self.add_outlives(b, a);
@@ -176,10 +172,6 @@ impl<'a, 'b, 'tcx> TypeOutlivesDelegate<'tcx> for &'a mut ConstraintConversion<'
176172
a: ty::Region<'tcx>,
177173
bound: VerifyBound<'tcx>,
178174
) {
179-
// FIXME: I'd prefer if NLL had a notion of empty
180-
if let ty::ReEmpty(ty::UniverseIndex::ROOT) = a {
181-
return;
182-
}
183175
let type_test = self.verify_to_type_test(kind, a, bound);
184176
self.add_type_test(type_test);
185177
}

src/librustc_mir/borrow_check/universal_regions.rs

+18-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ pub struct UniversalRegions<'tcx> {
5454
/// The total number of universal region variables instantiated.
5555
num_universals: usize,
5656

57+
/// A special region variable created for the `'empty(U0)` region.
58+
/// Note that this is **not** a "universal" region, as it doesn't
59+
/// represent a universally bound placeholder or any such thing.
60+
/// But we do create it here in this type because it's a useful region
61+
/// to have around in a few limited cases.
62+
pub root_empty: RegionVid,
63+
5764
/// The "defining" type for this function, with all universal
5865
/// regions instantiated. For a closure or generator, this is the
5966
/// closure type, but for a top-level function it's the `FnDef`.
@@ -317,7 +324,11 @@ impl<'tcx> UniversalRegions<'tcx> {
317324

318325
/// See `UniversalRegionIndices::to_region_vid`.
319326
pub fn to_region_vid(&self, r: ty::Region<'tcx>) -> RegionVid {
320-
self.indices.to_region_vid(r)
327+
if let ty::ReEmpty(ty::UniverseIndex::ROOT) = r {
328+
self.root_empty
329+
} else {
330+
self.indices.to_region_vid(r)
331+
}
321332
}
322333

323334
/// As part of the NLL unit tests, you can annotate a function with
@@ -473,10 +484,16 @@ impl<'cx, 'tcx> UniversalRegionsBuilder<'cx, 'tcx> {
473484
_ => None,
474485
};
475486

487+
let root_empty = self
488+
.infcx
489+
.next_nll_region_var(NLLRegionVariableOrigin::RootEmptyRegion)
490+
.to_region_vid();
491+
476492
UniversalRegions {
477493
indices,
478494
fr_static,
479495
fr_fn_body,
496+
root_empty,
480497
first_extern_index,
481498
first_local_index,
482499
num_universals,

src/test/mir-opt/nll/named-lifetimes-basic/rustc.use_x.nll.0.mir

+14-13
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,28 @@
1313
| '_#2r | U0 | {bb0[0..=1], '_#2r}
1414
| '_#3r | U0 | {bb0[0..=1], '_#3r}
1515
| '_#4r | U0 | {bb0[0..=1], '_#4r}
16-
| '_#5r | U0 | {bb0[0..=1], '_#1r}
17-
| '_#6r | U0 | {bb0[0..=1], '_#2r}
18-
| '_#7r | U0 | {bb0[0..=1], '_#1r}
19-
| '_#8r | U0 | {bb0[0..=1], '_#3r}
16+
| '_#5r | U0 | {}
17+
| '_#6r | U0 | {bb0[0..=1], '_#1r}
18+
| '_#7r | U0 | {bb0[0..=1], '_#2r}
19+
| '_#8r | U0 | {bb0[0..=1], '_#1r}
20+
| '_#9r | U0 | {bb0[0..=1], '_#3r}
2021
|
2122
| Inference Constraints
2223
| '_#0r live at {bb0[0..=1]}
2324
| '_#1r live at {bb0[0..=1]}
2425
| '_#2r live at {bb0[0..=1]}
2526
| '_#3r live at {bb0[0..=1]}
2627
| '_#4r live at {bb0[0..=1]}
27-
| '_#1r: '_#5r due to BoringNoLocation at All($DIR/named-lifetimes-basic.rs:12:26: 12:27)
28-
| '_#1r: '_#7r due to BoringNoLocation at All($DIR/named-lifetimes-basic.rs:12:54: 12:55)
29-
| '_#2r: '_#6r due to BoringNoLocation at All($DIR/named-lifetimes-basic.rs:12:42: 12:43)
30-
| '_#3r: '_#8r due to BoringNoLocation at All($DIR/named-lifetimes-basic.rs:12:66: 12:67)
31-
| '_#5r: '_#1r due to BoringNoLocation at All($DIR/named-lifetimes-basic.rs:12:26: 12:27)
32-
| '_#6r: '_#2r due to BoringNoLocation at All($DIR/named-lifetimes-basic.rs:12:42: 12:43)
33-
| '_#7r: '_#1r due to BoringNoLocation at All($DIR/named-lifetimes-basic.rs:12:54: 12:55)
34-
| '_#8r: '_#3r due to BoringNoLocation at All($DIR/named-lifetimes-basic.rs:12:66: 12:67)
28+
| '_#1r: '_#6r due to BoringNoLocation at All($DIR/named-lifetimes-basic.rs:12:26: 12:27)
29+
| '_#1r: '_#8r due to BoringNoLocation at All($DIR/named-lifetimes-basic.rs:12:54: 12:55)
30+
| '_#2r: '_#7r due to BoringNoLocation at All($DIR/named-lifetimes-basic.rs:12:42: 12:43)
31+
| '_#3r: '_#9r due to BoringNoLocation at All($DIR/named-lifetimes-basic.rs:12:66: 12:67)
32+
| '_#6r: '_#1r due to BoringNoLocation at All($DIR/named-lifetimes-basic.rs:12:26: 12:27)
33+
| '_#7r: '_#2r due to BoringNoLocation at All($DIR/named-lifetimes-basic.rs:12:42: 12:43)
34+
| '_#8r: '_#1r due to BoringNoLocation at All($DIR/named-lifetimes-basic.rs:12:54: 12:55)
35+
| '_#9r: '_#3r due to BoringNoLocation at All($DIR/named-lifetimes-basic.rs:12:66: 12:67)
3536
|
36-
fn use_x(_1: &'_#5r mut i32, _2: &'_#6r u32, _3: &'_#7r u32, _4: &'_#8r u32) -> bool {
37+
fn use_x(_1: &'_#6r mut i32, _2: &'_#7r u32, _3: &'_#8r u32, _4: &'_#9r u32) -> bool {
3738
debug w => _1; // in scope 0 at $DIR/named-lifetimes-basic.rs:12:26: 12:27
3839
debug x => _2; // in scope 0 at $DIR/named-lifetimes-basic.rs:12:42: 12:43
3940
debug y => _3; // in scope 0 at $DIR/named-lifetimes-basic.rs:12:54: 12:55

src/test/mir-opt/nll/region-subtyping-basic.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
#![allow(warnings)]
99

10-
fn use_x(_: usize) -> bool { true }
10+
fn use_x(_: usize) -> bool {
11+
true
12+
}
1113

1214
// EMIT_MIR_FOR_EACH_BIT_WIDTH
1315
// EMIT_MIR rustc.main.nll.0.mir

0 commit comments

Comments
 (0)