-
Notifications
You must be signed in to change notification settings - Fork 13.3k
add cycle detection for inferred outlives predicates #103309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,23 +8,24 @@ use rustc_span::Span; | |
use super::explicit::ExplicitPredicatesMap; | ||
use super::utils::*; | ||
|
||
pub(super) type GlobalInferredOutlives<'tcx> = | ||
FxHashMap<DefId, ty::EarlyBinder<RequiredPredicates<'tcx>>>; | ||
|
||
/// Infer predicates for the items in the crate. | ||
/// | ||
/// `global_inferred_outlives`: this is initially the empty map that | ||
/// was generated by walking the items in the crate. This will | ||
/// now be filled with inferred predicates. | ||
pub(super) fn infer_predicates<'tcx>( | ||
tcx: TyCtxt<'tcx>, | ||
) -> FxHashMap<DefId, ty::EarlyBinder<RequiredPredicates<'tcx>>> { | ||
debug!("infer_predicates"); | ||
|
||
pub(super) fn infer_predicates<'tcx>(tcx: TyCtxt<'tcx>) -> GlobalInferredOutlives<'tcx> { | ||
let mut explicit_map = ExplicitPredicatesMap::new(); | ||
|
||
let mut global_inferred_outlives = FxHashMap::default(); | ||
|
||
// If new predicates were added then we need to re-calculate | ||
// all crates since there could be new implied predicates. | ||
let mut round = 1; | ||
'outer: loop { | ||
debug!("infer_predicates: round {round}"); | ||
let mut predicates_added = false; | ||
|
||
// Visit all the crates and infer predicates | ||
|
@@ -50,6 +51,7 @@ pub(super) fn infer_predicates<'tcx>( | |
let field_span = tcx.def_span(field_def.did); | ||
insert_required_predicates_to_be_wf( | ||
tcx, | ||
adt_def.did(), | ||
field_ty, | ||
field_span, | ||
&global_inferred_outlives, | ||
|
@@ -72,6 +74,22 @@ pub(super) fn infer_predicates<'tcx>( | |
global_inferred_outlives.get(&item_did.to_def_id()).map_or(0, |p| p.0.len()); | ||
if item_required_predicates.len() > item_predicates_len { | ||
predicates_added = true; | ||
if tracing::enabled!(tracing::Level::DEBUG) { | ||
let def_id = item_did.to_def_id(); | ||
use std::collections::BTreeSet; | ||
let global_preds: BTreeSet<_> = | ||
global_inferred_outlives.get(&def_id).map_or_else(Default::default, |e| { | ||
e.0.iter().map(|(pred, _)| pred).collect() | ||
}); | ||
let computed_preds: BTreeSet<_> = | ||
item_required_predicates.iter().map(|(pred, _)| pred).collect(); | ||
let added = computed_preds.difference(&global_preds).collect::<BTreeSet<_>>(); | ||
debug!("global_inferred_outlives grew for {def_id:?}, added: {added:?}"); | ||
let removed = global_preds.difference(&computed_preds).collect::<BTreeSet<_>>(); | ||
if !removed.is_empty() { | ||
debug!("global_inferred_outlives lost predicates: {removed:?}") | ||
} | ||
} | ||
global_inferred_outlives | ||
.insert(item_did.to_def_id(), ty::EarlyBinder(item_required_predicates)); | ||
} | ||
|
@@ -80,16 +98,18 @@ pub(super) fn infer_predicates<'tcx>( | |
if !predicates_added { | ||
break 'outer; | ||
} | ||
round += 1; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just for sanity, maybe check the recursion limit here against |
||
} | ||
|
||
global_inferred_outlives | ||
} | ||
|
||
fn insert_required_predicates_to_be_wf<'tcx>( | ||
tcx: TyCtxt<'tcx>, | ||
self_did: DefId, | ||
field_ty: Ty<'tcx>, | ||
field_span: Span, | ||
global_inferred_outlives: &FxHashMap<DefId, ty::EarlyBinder<RequiredPredicates<'tcx>>>, | ||
global_inferred_outlives: &GlobalInferredOutlives<'tcx>, | ||
required_predicates: &mut RequiredPredicates<'tcx>, | ||
explicit_map: &mut ExplicitPredicatesMap<'tcx>, | ||
) { | ||
|
@@ -109,14 +129,22 @@ fn insert_required_predicates_to_be_wf<'tcx>( | |
// We also want to calculate potential predicates for the T | ||
ty::Ref(region, rty, _) => { | ||
debug!("Ref"); | ||
insert_outlives_predicate(tcx, rty.into(), region, field_span, required_predicates); | ||
insert_outlives_predicate( | ||
tcx, | ||
rty.into(), | ||
region, | ||
self_did, | ||
field_span, | ||
required_predicates, | ||
None, | ||
); | ||
} | ||
|
||
// For each Adt (struct/enum/union) type `Foo<'a, T>`, we | ||
// can load the current set of inferred and explicit | ||
// predicates from `global_inferred_outlives` and filter the | ||
// ones that are TypeOutlives. | ||
ty::Adt(def, substs) => { | ||
ty::Adt(adt, substs) => { | ||
// First check the inferred predicates | ||
// | ||
// Example 1: | ||
|
@@ -136,21 +164,86 @@ fn insert_required_predicates_to_be_wf<'tcx>( | |
// `['b => 'a, U => T]` and thus get the requirement that `T: | ||
// 'a` holds for `Foo`. | ||
debug!("Adt"); | ||
if let Some(unsubstituted_predicates) = global_inferred_outlives.get(&def.did()) { | ||
for (unsubstituted_predicate, &span) in &unsubstituted_predicates.0 { | ||
// `unsubstituted_predicate` is `U: 'b` in the | ||
// example above. So apply the substitution to | ||
// get `T: 'a` (or `predicate`): | ||
let predicate = unsubstituted_predicates | ||
.rebind(*unsubstituted_predicate) | ||
.subst(tcx, substs); | ||
insert_outlives_predicate( | ||
tcx, | ||
predicate.0, | ||
predicate.1, | ||
span, | ||
required_predicates, | ||
); | ||
if let Some(unsubstituted_predicates) = global_inferred_outlives.get(&adt.did()) { | ||
for (unsubstituted_predicate, stack) in &unsubstituted_predicates.0 { | ||
// We must detect cycles in the inference. If we don't, rustc can hang. | ||
// Cycles can be formed by associated types on traits when they are used like so: | ||
// | ||
// ``` | ||
// trait Trait<'a> { | ||
// type Assoc: 'a; | ||
// } | ||
// struct Node<'node, T: Trait<'node>> { | ||
// var: Var<'node, T::Assoc>, | ||
// } | ||
// struct Var<'var, R: 'var> { | ||
// node: Box<Node<'var, RGen<R>>>, | ||
// } | ||
// struct RGen<R>(std::marker::PhantomData<R>); | ||
// impl<'a, R: 'a> Trait<'a> for RGen<R> { | ||
// type Assoc = R; // works with () too | ||
// } | ||
// ``` | ||
// | ||
// Visiting Node, we walk the fields and find a Var. Var has an explicit | ||
// R : 'var. | ||
// Node finds this in check_explicit_predicates. | ||
// Node substitutes its type parameters for Var through, and gets an inferred | ||
// <T as Trait<'node>>::Assoc: 'node. | ||
// Visiting Var, we walk the fields and find Node, for which there us an | ||
// unsubstituted predicate in the global map. So Var gets | ||
// <RGen<R> as Trait<'var>>::Assoc: 'var | ||
// But Node contains a Var. So Node gets | ||
// <RGen<<T as Trait<'node>>::Assoc> as Trait<'node>>::Assoc 'node | ||
// Var gets | ||
// <RGen<<RGen<R> as Trait<'var>>::Assoc> as Trait<'var>>::Assoc: 'var | ||
// | ||
// Etc. This goes on forever. The fact that RGen<R>::Assoc *is* R is | ||
// irrelevant. It is simply that both types find a way to add a bigger | ||
// predicate each time. | ||
// | ||
// The outlives requirements propagate through the crate's types like a | ||
// graph walk, so to detect this cycle we can just track visited nodes in | ||
// our branch by pushing them on a stack when we move through the graph. | ||
// An edge move is when a type picks up a predicate from one of its fields | ||
// that it hasn't seen before. We store the stacks alongside the predicates, | ||
// so they carry their provenance with them through the current and | ||
// subsequent rounds of crate-wide inference. | ||
// | ||
// Try: RUSTC_LOG=rustc_hir_analysis::outlives=debug \ | ||
// rustc +stage1 src/test/ui/rfc-2093-infer-outlives/issue-102966.rs 2>&1 \ | ||
// | rg '(grew|cycle)' | ||
// | ||
// We do not treat a type with an explicit bound as the first in the visit | ||
// stack. So in the example, Node appears first in the stack, and each of | ||
// Node and Var will get a new bound constraining an ::Assoc projection | ||
// before the cycle is cut at Node. If Var were already in the visit stack, | ||
// it would not receive the projection bound, which is something it needs. | ||
Comment on lines
+217
to
+221
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the main thing I would like some eyes on. I think it's right for Var to get a substituted-through version of its own explicit bound here, because Var doesn't know what But I don't know and don't see much guidance in the RFC or its tests, as this situation wasn't really contemplated. Can anyone think of a test that might illuminate the correctness of this? Tangentially, this PR has more tracking of the source of inferred outlives than before. Are there error messages we can improve with this information? Like the example of a "long range error" message in the original RFC. |
||
|
||
if stack.iter().any(|&(did, _span)| did == self_did) { | ||
debug!( | ||
"detected cycle in inferred_outlives_predicates,\ | ||
for unsubstituted predicate {unsubstituted_predicate:?}:\ | ||
{self_did:?} found in {stack:?}" | ||
); | ||
} else { | ||
// `unsubstituted_predicate` is `U: 'b` in Example 1 above. | ||
// So apply the substitution to get `T: 'a` (or `predicate`): | ||
let predicate = unsubstituted_predicates | ||
.rebind(*unsubstituted_predicate) | ||
.subst(tcx, substs); | ||
|
||
insert_outlives_predicate( | ||
tcx, | ||
predicate.0, | ||
predicate.1, | ||
self_did, | ||
field_span, | ||
required_predicates, | ||
// a copy of this is made for the predicate and (self_did, field_span) is pushed. | ||
Some(stack), | ||
); | ||
} | ||
} | ||
} | ||
|
||
|
@@ -159,7 +252,8 @@ fn insert_required_predicates_to_be_wf<'tcx>( | |
// let _: () = substs.region_at(0); | ||
check_explicit_predicates( | ||
tcx, | ||
def.did(), | ||
self_did, | ||
adt.did(), | ||
substs, | ||
required_predicates, | ||
explicit_map, | ||
|
@@ -187,6 +281,7 @@ fn insert_required_predicates_to_be_wf<'tcx>( | |
ex_trait_ref.with_self_ty(tcx, tcx.types.usize).skip_binder().substs; | ||
check_explicit_predicates( | ||
tcx, | ||
self_did, | ||
ex_trait_ref.skip_binder().def_id, | ||
substs, | ||
required_predicates, | ||
|
@@ -202,6 +297,7 @@ fn insert_required_predicates_to_be_wf<'tcx>( | |
debug!("Projection"); | ||
check_explicit_predicates( | ||
tcx, | ||
self_did, | ||
tcx.parent(obj.item_def_id), | ||
obj.substs, | ||
required_predicates, | ||
|
@@ -232,23 +328,28 @@ fn insert_required_predicates_to_be_wf<'tcx>( | |
/// applying the substitution as above. | ||
fn check_explicit_predicates<'tcx>( | ||
tcx: TyCtxt<'tcx>, | ||
// i.e. Foo | ||
self_did: DefId, | ||
// i.e. Bar | ||
def_id: DefId, | ||
substs: &[GenericArg<'tcx>], | ||
required_predicates: &mut RequiredPredicates<'tcx>, | ||
explicit_map: &mut ExplicitPredicatesMap<'tcx>, | ||
ignored_self_ty: Option<Ty<'tcx>>, | ||
) { | ||
debug!( | ||
"check_explicit_predicates(def_id={:?}, \ | ||
"check_explicit_predicates(\ | ||
self_did={:?},\ | ||
def_id={:?}, \ | ||
substs={:?}, \ | ||
explicit_map={:?}, \ | ||
required_predicates={:?}, \ | ||
ignored_self_ty={:?})", | ||
def_id, substs, explicit_map, required_predicates, ignored_self_ty, | ||
self_did, def_id, substs, explicit_map, required_predicates, ignored_self_ty, | ||
); | ||
let explicit_predicates = explicit_map.explicit_predicates_of(tcx, def_id); | ||
let explicit_predicates = explicit_map.explicit_predicates_of(tcx, self_did, def_id); | ||
|
||
for (outlives_predicate, &span) in &explicit_predicates.0 { | ||
for (outlives_predicate, stack) in &explicit_predicates.0 { | ||
debug!("outlives_predicate = {:?}", &outlives_predicate); | ||
|
||
// Careful: If we are inferring the effects of a `dyn Trait<..>` | ||
|
@@ -293,8 +394,18 @@ fn check_explicit_predicates<'tcx>( | |
continue; | ||
} | ||
|
||
let &(_foo_did, span) = stack.last().unwrap(); | ||
let predicate = explicit_predicates.rebind(*outlives_predicate).subst(tcx, substs); | ||
debug!("predicate = {:?}", &predicate); | ||
insert_outlives_predicate(tcx, predicate.0, predicate.1, span, required_predicates); | ||
insert_outlives_predicate( | ||
tcx, | ||
predicate.0, | ||
predicate.1, | ||
// i.e. Foo, not the field ADT definition. | ||
self_did, | ||
span, | ||
required_predicates, | ||
None, | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.