Skip to content

Commit

Permalink
Improve HRTB error span when -Zno-leak-check is used
Browse files Browse the repository at this point in the history
As described in #57374, NLL currently produces unhelpful higher-ranked
trait bound (HRTB) errors when '-Zno-leak-check' is enabled.

This PR tackles one half of this issue - making the error message point
at the proper span. The error message itself is still the very generic
"higher-ranked subtype error", but this can be improved in a follow-up
PR.

The root cause of the bad spans lies in how NLL attempts to compute the
'blamed' region, for which it will retrieve a span for.
Consider the following code, which (correctly) does not compile:

```rust
let my_val: u8 = 25;
let a: &u8 = &my_val;
let b = a;
let c = b;
let d: &'static u8 = c;
```

This will cause NLL to generate the following subtype constraints:

d :< c
c :< b
b <: a

Since normal Rust lifetimes are covariant, this results in the following
region constraints (I'm using 'd to denote the lifetime of 'd',
'c to denote the lifetime of 'c, etc.):

'c: 'd
'b: 'c
'a: 'b

From this, we can derive that 'a: 'd holds, which implies that 'a: 'static
must hold. However, this is not the case, since 'a refers to 'my_val',
which does not outlive the current function.

When NLL attempts to infer regions for this code, it will see that the
region 'a has grown 'too large' - it will be inferred to outlive
'static, despite the fact that is not declared as outliving 'static
We can find the region responsible, 'd, by starting at the *end* of
the 'constraint chain' we generated above. This works because for normal
(non-higher-ranked) lifetimes, we generally build up a 'chain' of
lifetime constraints *away* from the original variable/lifetime.
That is, our original lifetime 'a is required to outlive progressively
more regions. If it ends up living for too long, we can look at the
'end' of this chain to determine the 'most recent' usage that caused
the lifetime to grow too large.

However, this logic does not work correctly when higher-ranked trait
bounds (HRTBs) come into play. This is because HRTBs have
*contravariance* with respect to their bound regions. For example,
this code snippet compiles:

```rust
let a: for<'a> fn(&'a ()) = |_| {};
let b: fn(&'static ()) = a;
```

Here, we require that 'a' is a subtype of 'b'. Because of
contravariance, we end up with the region constraint 'static: 'a,
*not* 'a: 'static

This means that our 'constraint chains' grow in the opposite direction
of 'normal lifetime' constraint chains. As we introduce subtypes, our
lifetime ends up being outlived by other lifetimes, rather than
outliving other lifetimes. Therefore, starting at the end of the
'constraint chain' will cause us to 'blame' a lifetime close to the original
definition of a variable, instead of close to where the bad lifetime
constraint is introduced.

This PR improves how we select the region to blame for 'too large'
universal lifetimes, when bound lifetimes are involved. If the region
we're checking is a 'placeholder' region (e.g. the region 'a' in
for<'a>, or the implicit region in fn(&())), we start traversing the
constraint chain from the beginning, rather than the end.

There are two (maybe more) different ways we generate region constraints for NLL:
requirements generated from trait queries, and requirements generated
from MIR subtype constraints. While the former always use explicit
placeholder regions, the latter is more tricky. In order to implement
contravariance for HRTBs, TypeRelating replaces placeholder regions with
existential regions. This requires us to keep track of whether or not an
existential region was originally a placeholder region. When we look for
a region to blame, we check if our starting region is either a
placeholder region or is an existential region created from a
placeholder region. If so, we start iterating from the beginning of the
constraint chain, rather than the end.
  • Loading branch information
Aaron1011 committed Oct 1, 2019
1 parent 702b45e commit 1245467
Show file tree
Hide file tree
Showing 12 changed files with 116 additions and 26 deletions.
6 changes: 4 additions & 2 deletions src/librustc/infer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,15 +418,17 @@ pub enum NLLRegionVariableOrigin {
/// from a `for<'a> T` binder). Meant to represent "any region".
Placeholder(ty::PlaceholderRegion),

Existential,
Existential {
was_placeholder: bool
},
}

impl NLLRegionVariableOrigin {
pub fn is_universal(self) -> bool {
match self {
NLLRegionVariableOrigin::FreeRegion => true,
NLLRegionVariableOrigin::Placeholder(..) => true,
NLLRegionVariableOrigin::Existential => false,
NLLRegionVariableOrigin::Existential{ .. } => false,
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/librustc/infer/nll_relate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ pub trait TypeRelatingDelegate<'tcx> {
/// we will invoke this method to instantiate `'a` with an
/// inference variable (though `'b` would be instantiated first,
/// as a placeholder).
fn next_existential_region_var(&mut self) -> ty::Region<'tcx>;
fn next_existential_region_var(&mut self, was_placeholder: bool) -> ty::Region<'tcx>;

/// Creates a new region variable representing a
/// higher-ranked region that is instantiated universally.
Expand Down Expand Up @@ -193,7 +193,7 @@ where
let placeholder = ty::PlaceholderRegion { universe, name: br };
delegate.next_placeholder_region(placeholder)
} else {
delegate.next_existential_region_var()
delegate.next_existential_region_var(true)
}
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,11 @@ impl<'tcx> RegionInferenceContext<'tcx> {
&self,
body: &Body<'tcx>,
from_region: RegionVid,
from_region_origin: NLLRegionVariableOrigin,
target_test: impl Fn(RegionVid) -> bool,
) -> (ConstraintCategory, bool, Span) {
debug!("best_blame_constraint(from_region={:?})", from_region);
debug!("best_blame_constraint(from_region={:?}, from_region_origin={:?})",
from_region, from_region_origin);

// Find all paths
let (path, target_region) =
Expand Down Expand Up @@ -153,19 +155,50 @@ impl<'tcx> RegionInferenceContext<'tcx> {
// we still want to screen for an "interesting" point to
// highlight (e.g., a call site or something).
let target_scc = self.constraint_sccs.scc(target_region);
let best_choice = (0..path.len()).rev().find(|&i| {
let constraint = path[i];
let mut range = 0..path.len();

let should_reverse = match from_region_origin {
NLLRegionVariableOrigin::FreeRegion
| NLLRegionVariableOrigin::Existential { was_placeholder: false } => {
true
}
NLLRegionVariableOrigin::Placeholder(_)
| NLLRegionVariableOrigin::Existential { was_placeholder: true } => {
false
}
};

let find_region = |i: &usize| {
let constraint = path[*i];

let constraint_sup_scc = self.constraint_sccs.scc(constraint.sup);

match categorized_path[i].0 {
ConstraintCategory::OpaqueType | ConstraintCategory::Boring |
ConstraintCategory::BoringNoLocation | ConstraintCategory::Internal => false,
ConstraintCategory::TypeAnnotation | ConstraintCategory::Return |
ConstraintCategory::Yield => true,
_ => constraint_sup_scc != target_scc,
if should_reverse {
match categorized_path[*i].0 {
ConstraintCategory::OpaqueType | ConstraintCategory::Boring |
ConstraintCategory::BoringNoLocation | ConstraintCategory::Internal => false,
ConstraintCategory::TypeAnnotation | ConstraintCategory::Return |
ConstraintCategory::Yield => true,
_ => constraint_sup_scc != target_scc,
}
} else {
match categorized_path[*i].0 {
ConstraintCategory::OpaqueType | ConstraintCategory::Boring |
ConstraintCategory::BoringNoLocation | ConstraintCategory::Internal => false,
_ => true
}
}
});
};

let best_choice = if should_reverse {
range.rev().find(find_region)
} else {
range.find(find_region)
};

debug!("best_blame_constraint: best_choice={:?} should_reverse={}",
best_choice, should_reverse);

if let Some(i) = best_choice {
if let Some(next) = categorized_path.get(i + 1) {
if categorized_path[i].0 == ConstraintCategory::Return
Expand Down Expand Up @@ -297,12 +330,13 @@ impl<'tcx> RegionInferenceContext<'tcx> {
infcx: &'a InferCtxt<'a, 'tcx>,
mir_def_id: DefId,
fr: RegionVid,
fr_origin: NLLRegionVariableOrigin,
outlived_fr: RegionVid,
renctx: &mut RegionErrorNamingCtx,
) -> DiagnosticBuilder<'a> {
debug!("report_error(fr={:?}, outlived_fr={:?})", fr, outlived_fr);

let (category, _, span) = self.best_blame_constraint(body, fr, |r| {
let (category, _, span) = self.best_blame_constraint(body, fr, fr_origin, |r| {
self.provides_universal_region(r, fr, outlived_fr)
});

Expand Down Expand Up @@ -709,6 +743,7 @@ impl<'tcx> RegionInferenceContext<'tcx> {
let (category, from_closure, span) = self.best_blame_constraint(
body,
borrow_region,
NLLRegionVariableOrigin::FreeRegion,
|r| self.provides_universal_region(r, borrow_region, outlived_region)
);

Expand Down Expand Up @@ -768,11 +803,13 @@ impl<'tcx> RegionInferenceContext<'tcx> {
&self,
body: &Body<'tcx>,
fr1: RegionVid,
fr1_origin: NLLRegionVariableOrigin,
fr2: RegionVid,
) -> (ConstraintCategory, Span) {
let (category, _, span) = self.best_blame_constraint(
body,
fr1,
fr1_origin,
|r| self.provides_universal_region(r, fr1, fr2),
);
(category, span)
Expand Down Expand Up @@ -825,7 +862,9 @@ impl<'tcx> RegionInferenceContext<'tcx> {
universe1.cannot_name(placeholder.universe)
}

NLLRegionVariableOrigin::FreeRegion | NLLRegionVariableOrigin::Existential => false,
NLLRegionVariableOrigin::FreeRegion | NLLRegionVariableOrigin::Existential { .. } => {
false
}
}
}
}
14 changes: 9 additions & 5 deletions src/librustc_mir/borrow_check/nll/region_infer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ impl<'tcx> RegionInferenceContext<'tcx> {
}
}

NLLRegionVariableOrigin::Existential => {
NLLRegionVariableOrigin::Existential { .. } => {
// For existential, regions, nothing to do.
}
}
Expand Down Expand Up @@ -1348,7 +1348,7 @@ impl<'tcx> RegionInferenceContext<'tcx> {
self.check_bound_universal_region(infcx, body, mir_def_id, fr, placeholder);
}

NLLRegionVariableOrigin::Existential => {
NLLRegionVariableOrigin::Existential { .. } => {
// nothing to check here
}
}
Expand Down Expand Up @@ -1461,7 +1461,8 @@ impl<'tcx> RegionInferenceContext<'tcx> {
debug!("check_universal_region: fr_minus={:?}", fr_minus);

let blame_span_category =
self.find_outlives_blame_span(body, longer_fr, shorter_fr);
self.find_outlives_blame_span(body, longer_fr,
NLLRegionVariableOrigin::FreeRegion,shorter_fr);

// Grow `shorter_fr` until we find some non-local regions. (We
// always will.) We'll call them `shorter_fr+` -- they're ever
Expand Down Expand Up @@ -1494,6 +1495,7 @@ impl<'tcx> RegionInferenceContext<'tcx> {
infcx,
mir_def_id,
longer_fr,
NLLRegionVariableOrigin::FreeRegion,
shorter_fr,
region_naming,
);
Expand Down Expand Up @@ -1547,7 +1549,9 @@ impl<'tcx> RegionInferenceContext<'tcx> {
};

// Find the code to blame for the fact that `longer_fr` outlives `error_fr`.
let (_, span) = self.find_outlives_blame_span(body, longer_fr, error_region);
let (_, span) = self.find_outlives_blame_span(
body, longer_fr, NLLRegionVariableOrigin::Placeholder(placeholder), error_region
);

// Obviously, this error message is far from satisfactory.
// At present, though, it only appears in unit tests --
Expand Down Expand Up @@ -1608,7 +1612,7 @@ impl<'tcx> RegionDefinition<'tcx> {

let origin = match rv_origin {
RegionVariableOrigin::NLL(origin) => origin,
_ => NLLRegionVariableOrigin::Existential,
_ => NLLRegionVariableOrigin::Existential { was_placeholder: false },
};

Self { origin, universe, external_name: None }
Expand Down
2 changes: 1 addition & 1 deletion src/librustc_mir/borrow_check/nll/renumber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ where
infcx
.tcx
.fold_regions(value, &mut false, |_region, _depth| {
let origin = NLLRegionVariableOrigin::Existential;
let origin = NLLRegionVariableOrigin::Existential { was_placeholder: false };
infcx.next_nll_region_var(origin)
})
}
Expand Down
8 changes: 5 additions & 3 deletions src/librustc_mir/borrow_check/nll/type_check/relate_tys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ impl TypeRelatingDelegate<'tcx> for NllTypeRelatingDelegate<'_, '_, 'tcx> {
self.infcx.create_next_universe()
}

fn next_existential_region_var(&mut self) -> ty::Region<'tcx> {
fn next_existential_region_var(&mut self, was_placeholder: bool) -> ty::Region<'tcx> {
if let Some(_) = &mut self.borrowck_context {
let origin = NLLRegionVariableOrigin::Existential;
let origin = NLLRegionVariableOrigin::Existential { was_placeholder };
self.infcx.next_nll_region_var(origin)
} else {
self.infcx.tcx.lifetimes.re_erased
Expand All @@ -88,7 +88,9 @@ impl TypeRelatingDelegate<'tcx> for NllTypeRelatingDelegate<'_, '_, 'tcx> {

fn generalize_existential(&mut self, universe: ty::UniverseIndex) -> ty::Region<'tcx> {
self.infcx
.next_nll_region_var_in_universe(NLLRegionVariableOrigin::Existential, universe)
.next_nll_region_var_in_universe(NLLRegionVariableOrigin::Existential {
was_placeholder: false
}, universe)
}

fn push_outlives(&mut self, sup: ty::Region<'tcx>, sub: ty::Region<'tcx>) {
Expand Down
2 changes: 1 addition & 1 deletion src/librustc_traits/chalk_context/unify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ impl TypeRelatingDelegate<'tcx> for &mut ChalkTypeRelatingDelegate<'_, 'tcx> {
self.infcx.create_next_universe()
}

fn next_existential_region_var(&mut self) -> ty::Region<'tcx> {
fn next_existential_region_var(&mut self, _was_placeholder: bool) -> ty::Region<'tcx> {
self.infcx.next_region_var(RegionVariableOrigin::MiscVariable(DUMMY_SP))
}

Expand Down
1 change: 1 addition & 0 deletions src/test/ui/hrtb/issue-30786.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,5 @@ fn main() {
//[nll]~^ ERROR higher-ranked subtype error
let count = filter.count(); // Assert that we still have a valid stream.
//[nll]~^ ERROR higher-ranked subtype error

}
10 changes: 10 additions & 0 deletions src/test/ui/nll/relate_tys/fn-subtype.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Test that NLL produces correct spans for higher-ranked subtyping errors.
//
// compile-flags:-Zno-leak-check

#![feature(nll)]

fn main() {
let x: fn(&'static ()) = |_| {};
let y: for<'a> fn(&'a ()) = x; //~ ERROR higher-ranked subtype error
}
8 changes: 8 additions & 0 deletions src/test/ui/nll/relate_tys/fn-subtype.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
error: higher-ranked subtype error
--> $DIR/fn-subtype.rs:9:33
|
LL | let y: for<'a> fn(&'a ()) = x;
| ^

error: aborting due to previous error

16 changes: 16 additions & 0 deletions src/test/ui/nll/relate_tys/trait-hrtb.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Test that NLL generates proper error spans for trait HRTB errors
//
// compile-flags:-Zno-leak-check

#![feature(nll)]

trait Foo<'a> {}

fn make_foo<'a>() -> Box<dyn Foo<'a>> {
panic!()
}

fn main() {
let x: Box<dyn Foo<'static>> = make_foo();
let y: Box<dyn for<'a> Foo<'a>> = x; //~ ERROR higher-ranked subtype error
}
8 changes: 8 additions & 0 deletions src/test/ui/nll/relate_tys/trait-hrtb.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
error: higher-ranked subtype error
--> $DIR/trait-hrtb.rs:15:39
|
LL | let y: Box<dyn for<'a> Foo<'a>> = x;
| ^

error: aborting due to previous error

0 comments on commit 1245467

Please sign in to comment.