Skip to content

Commit 9c18ee3

Browse files
committed
fix: preserve frontend fulfillment overflow behavior
Signed-off-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com>
1 parent 27665d9 commit 9c18ee3

3 files changed

Lines changed: 86 additions & 9 deletions

File tree

compiler/rustc_next_trait_solver/src/solve/fulfill.rs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,30 @@ pub enum NextSolverError<O> {
1515
Overflow(O),
1616
}
1717

18+
/// Controls when fulfillment detects recursion overflow.
19+
///
20+
/// rustc currently checks after a goal makes inference progress, while
21+
/// rust-analyzer checks before evaluating an obligation which has already
22+
/// reached the recursion limit.
23+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24+
pub enum FulfillmentOverflowMode {
25+
/// Check after a goal makes inference progress.
26+
AfterProgress,
27+
28+
/// Check before evaluating an obligation at the recursion limit.
29+
BeforeEvaluation,
30+
}
31+
32+
// FIXME: Do we need to use a `ThinVec` here?
1833
type PendingObligations<I, O> = ThinVec<(O, Option<GoalStalledOn<I>>)>;
1934

2035
#[derive(Debug)]
2136
struct ObligationStorage<I: Interner, O> {
2237
/// Obligations which resulted in overflow in fulfillment itself.
38+
///
39+
/// We cannot eagerly return these as errors, so we instead store them here
40+
/// to avoid recomputing them each time `try_evaluate_obligations` is called.
41+
/// This also allows the frontend to construct the correct error for them.
2342
overflowed: Vec<O>,
2443

2544
pending: PendingObligations<I, O>,
@@ -96,6 +115,14 @@ impl<I: Interner, O> ObligationStorage<I, O> {
96115
}
97116
}
98117

118+
/// A fulfillment engine using the new trait solver.
119+
///
120+
/// This is mostly identical to how `evaluate_all` works inside of the solver,
121+
/// except that it is possible to add new obligations later and the frontend
122+
/// needs to retain its obligation representation for diagnostics.
123+
///
124+
/// It is also likely that we want to use different data structures here, as
125+
/// fulfillment deals with far more root goals than `evaluate_all`.
99126
#[derive(Debug)]
100127
pub struct FulfillmentCtxt<I: Interner, O: FulfillmentObligation<I>> {
101128
obligations: ObligationStorage<I, O>,
@@ -119,6 +146,9 @@ impl<I: Interner, O: FulfillmentObligation<I>> FulfillmentCtxt<I, O> {
119146
if let Some(GoalEvaluation { certainty, stalled_on, .. }) =
120147
compute_goal_fast_path(delegate, obligation.as_goal(), obligation.span())
121148
{
149+
// If we can take the fast path, do not add a successful goal to
150+
// the pending obligations. For `Certainty::Maybe`, retain the
151+
// precise `stalled_on` information for later re-evaluation.
122152
match certainty {
123153
Certainty::Yes => {}
124154
Certainty::Maybe(_) => {
@@ -135,6 +165,11 @@ impl<I: Interner, O: FulfillmentObligation<I>> FulfillmentCtxt<I, O> {
135165
D: SolverDelegate<Interner = I>,
136166
{
137167
delegate.probe(|| {
168+
// IMPORTANT: we must not resolve any inference variables in the
169+
// obligations, as this is all happening inside of a probe. The
170+
// probe makes sure we collect every obligation involved in the
171+
// overflow. Conceptually, we check which goals would change if we
172+
// performed one more fulfillment iteration.
138173
let overflowed = self
139174
.obligations
140175
.pending
@@ -157,6 +192,7 @@ impl<I: Interner, O: FulfillmentObligation<I>> FulfillmentCtxt<I, O> {
157192
pub fn try_evaluate_obligations<D, Inspect, OnSuccess>(
158193
&mut self,
159194
delegate: &D,
195+
overflow_mode: FulfillmentOverflowMode,
160196
mut inspect: Inspect,
161197
mut on_success: OnSuccess,
162198
) -> Vec<NextSolverError<O>>
@@ -171,6 +207,13 @@ impl<I: Interner, O: FulfillmentObligation<I>> FulfillmentCtxt<I, O> {
171207
let mut any_changed = false;
172208

173209
for (mut obligation, stalled_on) in std::mem::take(&mut self.obligations.pending) {
210+
if overflow_mode == FulfillmentOverflowMode::BeforeEvaluation
211+
&& obligation.recursion_depth() >= delegate.cx().recursion_limit()
212+
{
213+
self.on_fulfillment_overflow(delegate);
214+
return errors;
215+
}
216+
174217
let goal = obligation.as_goal();
175218
let result = delegate.evaluate_root_goal(goal, obligation.span(), stalled_on);
176219

@@ -184,15 +227,26 @@ impl<I: Interner, O: FulfillmentObligation<I>> FulfillmentCtxt<I, O> {
184227
}
185228
};
186229

187-
// Reuse the eagerly resolved predicate during the next iteration.
230+
// We resolved the goal in `evaluate_root_goal`; retain the
231+
// eagerly resolved predicate to avoid repeating this work in
232+
// the next iteration. This does not resolve the inference
233+
// variables constrained by evaluating the goal.
188234
obligation.set_predicate(goal.predicate);
189235

190236
if has_changed == HasChanged::Yes {
237+
// Track the number of times this root goal has resulted in
238+
// inference progress. This does not precisely model the old
239+
// solver's recursion depth, as fulfillment only processes
240+
// root obligations, but it is a good approximation and
241+
// should only overflow in pathological cases.
191242
let depth = obligation.recursion_depth() + 1;
192243
obligation.set_recursion_depth(depth);
193244

194-
if depth > delegate.cx().recursion_limit() {
245+
if overflow_mode == FulfillmentOverflowMode::AfterProgress
246+
&& depth > delegate.cx().recursion_limit()
247+
{
195248
self.on_fulfillment_overflow(delegate);
249+
// Only return true errors accumulated while processing.
196250
return errors;
197251
}
198252

compiler/rustc_trait_selection/src/solve/rustc_fulfill.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ use rustc_infer::traits::{
77
};
88
use rustc_middle::ty::{self, TyCtxt, TypeVisitableExt, TypingMode};
99
use rustc_next_trait_solver::solve::fulfill::{
10-
FulfillmentCtxt as SolverFulfillmentCtxt, NextSolverError as SolverNextSolverError,
10+
FulfillmentCtxt as SolverFulfillmentCtxt, FulfillmentOverflowMode,
11+
NextSolverError as SolverNextSolverError,
1112
};
1213
use rustc_next_trait_solver::solve::{GoalEvaluation, MaybeInfo, StalledOnCoroutines};
1314
use tracing::instrument;
@@ -28,7 +29,8 @@ pub struct FulfillmentCtxt<'tcx, E: 'tcx> {
2829

2930
/// The snapshot in which this context was created. Using the context
3031
/// outside of this snapshot leads to subtle bugs if the snapshot
31-
/// gets rolled back.
32+
/// gets rolled back. Because of this we explicitly check that we only
33+
/// use the context in exactly this snapshot.
3234
usable_in_snapshot: usize,
3335
_errors: PhantomData<E>,
3436
}
@@ -100,13 +102,22 @@ where
100102
self.core
101103
.try_evaluate_obligations(
102104
delegate,
105+
FulfillmentOverflowMode::AfterProgress,
103106
|obligation, _, result| {
104107
Self::inspect_evaluated_obligation(infcx, obligation, result);
105108
},
106109
|obligation| {
107-
// Goals may depend on structural identity. Region
108-
// uniquification at the start of MIR borrowck may cause
109-
// things to no longer be structurally identical.
110+
// Goals may depend on structural identity. Region uniquification at the
111+
// start of MIR borrowck may cause things to no longer be so, potentially
112+
// causing an ICE.
113+
//
114+
// While we uniquify root goals in HIR this does not handle cases where
115+
// regions are hidden inside of a type or const inference variable.
116+
//
117+
// FIXME(-Znext-solver): This does not handle inference variables hidden
118+
// inside of an opaque type, e.g. if there's `Opaque = (?x, ?x)` in the
119+
// storage, we can also rely on structural identity of `?x` even if we
120+
// later uniquify it in MIR borrowck.
110121
if infcx.in_hir_typeck
111122
&& (obligation.has_non_region_infer() || obligation.has_free_regions())
112123
{
@@ -133,6 +144,7 @@ where
133144
infcx: &InferCtxt<'tcx>,
134145
vid: ty::TyVid,
135146
) -> PredicateObligations<'tcx> {
147+
// `-Zdisable-fast-paths`: same gate as the other new-solver fast paths.
136148
if infcx.tcx.disable_trait_solver_fast_paths() {
137149
return self.pending_obligations();
138150
}
@@ -142,6 +154,14 @@ where
142154
return true;
143155
};
144156

157+
// Don't reuse the sub-unification roots cached on `stalled_on`:
158+
// a later sub-unification merge can have changed which root
159+
// each stalled var belongs to, so the cached info can be stale.
160+
// Walk `stalled_vars` and recompute the current root instead.
161+
//
162+
// Conservative here: if a stalled var no longer resolves to an
163+
// infer var, some unification happened, so the goal is no longer
164+
// stalled. Include it to be re-evaluated downstream.
145165
stalled_on.stalled_vars.iter().filter_map(|arg| arg.as_type()).any(|ty| {
146166
match *infcx.shallow_resolve(ty).kind() {
147167
ty::Infer(ty::TyVar(tv)) => infcx.sub_unification_table_root_var(tv) == vid,
@@ -155,6 +175,7 @@ where
155175
&self,
156176
infcx: &InferCtxt<'tcx>,
157177
) -> PredicateObligations<'tcx> {
178+
// `-Zdisable-fast-paths`: same gate as the other new-solver fast paths.
158179
if infcx.tcx.disable_trait_solver_fast_paths() {
159180
return self.pending_obligations();
160181
}
@@ -164,6 +185,9 @@ where
164185
return true;
165186
};
166187

188+
// If the stalled vars don't have float infers, the nested goals
189+
// won't have them either. We only create float infers for
190+
// user-written literals.
167191
stalled_on
168192
.stalled_vars
169193
.iter()

compiler/rustc_type_ir/src/solve/fulfill.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ use crate::Interner;
33

44
/// An obligation that can be processed by the shared fulfillment engine.
55
///
6-
/// The concrete obligation and its diagnostic cause remain owned by the
7-
/// frontend. Fulfillment only needs the goal, span, and recursion depth.
6+
/// The shared engine only accesses the parts needed for fulfillment through this trait.
87
pub trait FulfillmentObligation<I: Interner>: Clone {
98
fn as_goal(&self) -> Goal<I, I::Predicate>;
109

0 commit comments

Comments
 (0)