Skip to content

Commit 0542b0d

Browse files
committedMar 29, 2023
Freshen normalizes-to hack goal RHS in the evaluate loop
1 parent cf32b9d commit 0542b0d

File tree

2 files changed

+70
-43
lines changed

2 files changed

+70
-43
lines changed
 

‎compiler/rustc_trait_selection/src/solve/eval_ctxt.rs

+69-33
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,20 @@ pub(super) enum IsNormalizesToHack {
4848

4949
#[derive(Debug, Clone)]
5050
pub(super) struct NestedGoals<'tcx> {
51+
/// This normalizes-to goal that is treated specially during the evaluation
52+
/// loop. In each iteration we take the RHS of the projection, replace it with
53+
/// a fresh inference variable, and only after evaluating that goal do we
54+
/// equate the fresh inference variable with the actual RHS of the predicate.
55+
///
56+
/// This is both to improve caching, and to avoid using the RHS of the
57+
/// projection predicate to influence the normalizes-to candidate we select.
58+
///
59+
/// This is not a 'real' nested goal. We must not forget to replace the RHS
60+
/// with a fresh inference variable when we evaluate this goal. That can result
61+
/// in a trait solver cycle. This would currently result in overflow but can be
62+
/// can be unsound with more powerful coinduction in the future.
5163
pub(super) normalizes_to_hack_goal: Option<Goal<'tcx, ty::ProjectionPredicate<'tcx>>>,
64+
/// The rest of the goals which have not yet processed or remain ambiguous.
5265
pub(super) goals: Vec<Goal<'tcx, ty::Predicate<'tcx>>>,
5366
}
5467

@@ -163,6 +176,10 @@ impl<'a, 'tcx> EvalCtxt<'a, 'tcx> {
163176
canonical_response,
164177
)?;
165178

179+
if !has_changed && !nested_goals.is_empty() {
180+
bug!("an unchanged goal shouldn't have any side-effects on instantiation");
181+
}
182+
166183
// Check that rerunning this query with its inference constraints applied
167184
// doesn't result in new inference constraints and has the same result.
168185
//
@@ -180,9 +197,17 @@ impl<'a, 'tcx> EvalCtxt<'a, 'tcx> {
180197
let canonical_response =
181198
EvalCtxt::evaluate_canonical_goal(self.tcx(), self.search_graph, canonical_goal)?;
182199
if !canonical_response.value.var_values.is_identity() {
183-
bug!("unstable result: {goal:?} {canonical_goal:?} {canonical_response:?}");
200+
bug!(
201+
"unstable result: re-canonicalized goal={canonical_goal:#?} \
202+
response={canonical_response:#?}"
203+
);
204+
}
205+
if certainty != canonical_response.value.certainty {
206+
bug!(
207+
"unstable certainty: {certainty:#?} re-canonicalized goal={canonical_goal:#?} \
208+
response={canonical_response:#?}"
209+
);
184210
}
185-
assert_eq!(certainty, canonical_response.value.certainty);
186211
}
187212

188213
Ok((has_changed, certainty, nested_goals))
@@ -262,15 +287,44 @@ impl<'a, 'tcx> EvalCtxt<'a, 'tcx> {
262287
let mut has_changed = Err(Certainty::Yes);
263288

264289
if let Some(goal) = goals.normalizes_to_hack_goal.take() {
265-
let (_, certainty, nested_goals) = match this.evaluate_goal(
266-
IsNormalizesToHack::Yes,
267-
goal.with(this.tcx(), ty::Binder::dummy(goal.predicate)),
290+
// Replace the goal with an unconstrained infer var, so the
291+
// RHS does not affect projection candidate assembly.
292+
let unconstrained_rhs = this.next_term_infer_of_kind(goal.predicate.term);
293+
let unconstrained_goal = goal.with(
294+
this.tcx(),
295+
ty::Binder::dummy(ty::ProjectionPredicate {
296+
projection_ty: goal.predicate.projection_ty,
297+
term: unconstrained_rhs,
298+
}),
299+
);
300+
301+
let (_, certainty, instantiate_goals) =
302+
match this.evaluate_goal(IsNormalizesToHack::Yes, unconstrained_goal) {
303+
Ok(r) => r,
304+
Err(NoSolution) => return Some(Err(NoSolution)),
305+
};
306+
new_goals.goals.extend(instantiate_goals);
307+
308+
// Finally, equate the goal's RHS with the unconstrained var.
309+
// We put the nested goals from this into goals instead of
310+
// next_goals to avoid needing to process the loop one extra
311+
// time if this goal returns something -- I don't think this
312+
// matters in practice, though.
313+
match this.eq_and_get_goals(
314+
goal.param_env,
315+
goal.predicate.term,
316+
unconstrained_rhs,
268317
) {
269-
Ok(r) => r,
318+
Ok(eq_goals) => {
319+
goals.goals.extend(eq_goals);
320+
}
270321
Err(NoSolution) => return Some(Err(NoSolution)),
271322
};
272-
new_goals.goals.extend(nested_goals);
273323

324+
// We only look at the `projection_ty` part here rather than
325+
// looking at the "has changed" return from evaluate_goal,
326+
// because we expect the `unconstrained_rhs` part of the predicate
327+
// to have changed -- that means we actually normalized successfully!
274328
if goal.predicate.projection_ty
275329
!= this.resolve_vars_if_possible(goal.predicate.projection_ty)
276330
{
@@ -280,40 +334,22 @@ impl<'a, 'tcx> EvalCtxt<'a, 'tcx> {
280334
match certainty {
281335
Certainty::Yes => {}
282336
Certainty::Maybe(_) => {
283-
let goal = this.resolve_vars_if_possible(goal);
284-
285-
// The rhs of this `normalizes-to` must always be an unconstrained infer var as it is
286-
// the hack used by `normalizes-to` to ensure that every `normalizes-to` behaves the same
287-
// regardless of the rhs.
288-
//
289-
// However it is important not to unconditionally replace the rhs with a new infer var
290-
// as otherwise we may replace the original unconstrained infer var with a new infer var
291-
// and never propagate any constraints on the new var back to the original var.
292-
let term = this
293-
.term_is_fully_unconstrained(goal)
294-
.then_some(goal.predicate.term)
295-
.unwrap_or_else(|| {
296-
this.next_term_infer_of_kind(goal.predicate.term)
297-
});
298-
let projection_pred = ty::ProjectionPredicate {
299-
term,
300-
projection_ty: goal.predicate.projection_ty,
301-
};
337+
// We need to resolve vars here so that we correctly
338+
// deal with `has_changed` in the next iteration.
302339
new_goals.normalizes_to_hack_goal =
303-
Some(goal.with(this.tcx(), projection_pred));
304-
340+
Some(this.resolve_vars_if_possible(goal));
305341
has_changed = has_changed.map_err(|c| c.unify_and(certainty));
306342
}
307343
}
308344
}
309345

310-
for nested_goal in goals.goals.drain(..) {
311-
let (changed, certainty, nested_goals) =
312-
match this.evaluate_goal(IsNormalizesToHack::No, nested_goal) {
346+
for goal in goals.goals.drain(..) {
347+
let (changed, certainty, instantiate_goals) =
348+
match this.evaluate_goal(IsNormalizesToHack::No, goal) {
313349
Ok(result) => result,
314350
Err(NoSolution) => return Some(Err(NoSolution)),
315351
};
316-
new_goals.goals.extend(nested_goals);
352+
new_goals.goals.extend(instantiate_goals);
317353

318354
if changed {
319355
has_changed = Ok(());
@@ -322,7 +358,7 @@ impl<'a, 'tcx> EvalCtxt<'a, 'tcx> {
322358
match certainty {
323359
Certainty::Yes => {}
324360
Certainty::Maybe(_) => {
325-
new_goals.goals.push(nested_goal);
361+
new_goals.goals.push(goal);
326362
has_changed = has_changed.map_err(|c| c.unify_and(certainty));
327363
}
328364
}

‎compiler/rustc_trait_selection/src/solve/project_goals.rs

+1-10
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,7 @@ impl<'tcx> EvalCtxt<'_, 'tcx> {
3535
let candidates = self.assemble_and_evaluate_candidates(goal);
3636
self.merge_candidates(candidates)
3737
} else {
38-
let predicate = goal.predicate;
39-
let unconstrained_rhs = self.next_term_infer_of_kind(predicate.term);
40-
let unconstrained_predicate = ProjectionPredicate {
41-
projection_ty: goal.predicate.projection_ty,
42-
term: unconstrained_rhs,
43-
};
44-
45-
self.set_normalizes_to_hack_goal(goal.with(self.tcx(), unconstrained_predicate));
46-
self.try_evaluate_added_goals()?;
47-
self.eq(goal.param_env, unconstrained_rhs, predicate.term)?;
38+
self.set_normalizes_to_hack_goal(goal);
4839
self.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
4940
}
5041
}

0 commit comments

Comments
 (0)
Please sign in to comment.