From 2aa27f4ef69c90c5a3237fd53fefa9ae0ec558bf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 21 Aug 2026 17:01:44 +0000 Subject: [PATCH 1/2] Skip redundant next-solver fulfillment scans When every pending goal is stalled only on type inference variables older than any vid changed since the last fulfillment pass, skip walking the pending queue. This removes the quadratic rescan from large typeck bodies (rustc#159933) without tracking a set of vids. Int/float/const stalls and opaque-storage count mismatches still take the existing retain_mut path. -Zdisable-fast-paths disables the skip. --- compiler/rustc_infer/src/infer/mod.rs | 17 +++ .../src/infer/opaque_types/table.rs | 7 + .../rustc_infer/src/infer/type_variable.rs | 45 +++++- .../src/solve/fulfill.rs | 133 +++++++++++++++++- .../fulfillment-skip-unrelated-infer.rs | 35 +++++ 5 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 tests/ui/traits/next-solver/fulfillment-skip-unrelated-infer.rs diff --git a/compiler/rustc_infer/src/infer/mod.rs b/compiler/rustc_infer/src/infer/mod.rs index 583fb1d7db21a..62fb3370aa9b8 100644 --- a/compiler/rustc_infer/src/infer/mod.rs +++ b/compiler/rustc_infer/src/infer/mod.rs @@ -1613,6 +1613,23 @@ impl<'tcx> InferCtxt<'tcx> { } } + /// Smallest type vid equated, sub-unified, or instantiated since the last + /// [`Self::reset_min_changed_ty_vid`]. Used by next-solver fulfillment to + /// skip walking pending goals stalled only on older vids. + #[inline] + pub fn min_changed_ty_vid(&self) -> Option { + self.inner.borrow().type_variable_storage.min_changed_ty_vid() + } + + pub fn reset_min_changed_ty_vid(&self) { + self.inner.borrow_mut().type_variables().reset_min_changed_ty_vid(); + } + + #[inline] + pub fn opaque_type_count(&self) -> usize { + self.inner.borrow().opaque_type_storage.num_entries().num_opaque_types() + } + /// `ty_or_const_infer_var_changed` is equivalent to one of these two: /// * `shallow_resolve(ty) != ty` (where `ty.kind = ty::Infer(_)`) /// * `shallow_resolve(ct) != ct` (where `ct.kind = ty::ConstKind::Infer(_)`) diff --git a/compiler/rustc_infer/src/infer/opaque_types/table.rs b/compiler/rustc_infer/src/infer/opaque_types/table.rs index 066d12be320a2..ff2835baa9d20 100644 --- a/compiler/rustc_infer/src/infer/opaque_types/table.rs +++ b/compiler/rustc_infer/src/infer/opaque_types/table.rs @@ -30,6 +30,13 @@ impl rustc_type_ir::inherent::OpaqueTypeStorageEntries for OpaqueTypeStorageEntr } } +impl OpaqueTypeStorageEntries { + #[inline] + pub fn num_opaque_types(self) -> usize { + self.opaque_types + } +} + impl<'tcx> OpaqueTypeStorage<'tcx> { #[instrument(level = "debug")] pub(crate) fn remove( diff --git a/compiler/rustc_infer/src/infer/type_variable.rs b/compiler/rustc_infer/src/infer/type_variable.rs index be0ac72d85675..24afec443d2a1 100644 --- a/compiler/rustc_infer/src/infer/type_variable.rs +++ b/compiler/rustc_infer/src/infer/type_variable.rs @@ -2,7 +2,7 @@ use std::cmp; use std::marker::PhantomData; use std::ops::Range; -use rustc_data_structures::undo_log::Rollback; +use rustc_data_structures::undo_log::{Rollback, UndoLogs}; use rustc_data_structures::{snapshot_vec as sv, unify as ut}; use rustc_hir::HirId; use rustc_hir::def_id::DefId; @@ -19,6 +19,8 @@ use crate::infer::InferCtxtUndoLogs; pub(crate) enum UndoLog<'tcx> { EqRelation(sv::UndoLog>>), SubRelation(sv::UndoLog>), + /// Previous value of [`TypeVariableStorage::min_changed_ty_vid`]. + MinChangedTyVid(Option), } /// Convert from a specific kind of undo to the more general UndoLog @@ -52,6 +54,7 @@ impl<'tcx> Rollback> for TypeVariableStorage<'tcx> { match undo { UndoLog::EqRelation(undo) => self.eq_relations.reverse(undo), UndoLog::SubRelation(undo) => self.sub_unification_table.reverse(undo), + UndoLog::MinChangedTyVid(prev) => self.min_changed_ty_vid = prev, } } } @@ -83,6 +86,12 @@ pub(crate) struct TypeVariableStorage<'tcx> { /// type of `x` is only a supertype of the argument of `returns_arg`. We /// still want to suggest specifying the type of the argument. sub_unification_table: ut::UnificationTableStorage, + /// Smallest type-inference vid that was equated, sub-unified, or + /// instantiated since the last [`TypeVariableTable::reset_min_changed_ty_vid`]. + /// + /// Next-solver fulfillment uses this to skip walking pending goals that + /// can only be unstalled by changes to older vids (see rustc#159933). + min_changed_ty_vid: Option, } pub(crate) struct TypeVariableTable<'a, 'tcx> { @@ -161,6 +170,11 @@ impl<'tcx> TypeVariableStorage<'tcx> { pub(crate) fn sub_unification_table_ref(&self) -> &ut::UnificationTableStorage { &self.sub_unification_table } + + #[inline] + pub(crate) fn min_changed_ty_vid(&self) -> Option { + self.min_changed_ty_vid + } } impl<'tcx> TypeVariableTable<'_, 'tcx> { @@ -172,12 +186,37 @@ impl<'tcx> TypeVariableTable<'_, 'tcx> { self.storage.values[vid].origin } + fn note_ty_infer_change(&mut self, vid: ty::TyVid) { + let idx = vid.as_u32(); + let old = self.storage.min_changed_ty_vid; + let new = Some(old.map_or(idx, |m| m.min(idx))); + if old != new { + self.undo_log.push(UndoLog::MinChangedTyVid(old)); + self.storage.min_changed_ty_vid = new; + } + } + + pub(crate) fn reset_min_changed_ty_vid(&mut self) { + let old = self.storage.min_changed_ty_vid; + if old.is_some() { + self.undo_log.push(UndoLog::MinChangedTyVid(old)); + self.storage.min_changed_ty_vid = None; + } + } + /// Records that `a == b`. /// /// Precondition: neither `a` nor `b` are known. pub(crate) fn equate(&mut self, a: ty::TyVid, b: ty::TyVid) { debug_assert!(self.probe(a).is_unknown()); debug_assert!(self.probe(b).is_unknown()); + let ra = self.root_var(a); + let rb = self.root_var(b); + let sa = self.sub_unification_table_root_var(a); + let sb = self.sub_unification_table_root_var(b); + self.note_ty_infer_change(ty::TyVid::from_u32( + ra.as_u32().min(rb.as_u32()).min(sa.as_u32()).min(sb.as_u32()), + )); self.eq_relations().union(a, b); self.sub_unification_table().union(a, b); } @@ -189,6 +228,9 @@ impl<'tcx> TypeVariableTable<'_, 'tcx> { pub(crate) fn sub_unify(&mut self, a: ty::TyVid, b: ty::TyVid) { debug_assert!(self.probe(a).is_unknown()); debug_assert!(self.probe(b).is_unknown()); + let sa = self.sub_unification_table_root_var(a); + let sb = self.sub_unification_table_root_var(b); + self.note_ty_infer_change(if sa.as_u32() < sb.as_u32() { sa } else { sb }); self.sub_unification_table().union(a, b); } @@ -204,6 +246,7 @@ impl<'tcx> TypeVariableTable<'_, 'tcx> { "instantiating type variable `{vid:?}` twice: new-value = {ty:?}, old-value={:?}", self.eq_relations().probe_value(vid) ); + self.note_ty_infer_change(vid); self.eq_relations().union_value(vid, TypeVariableValue::Known { value: ty }); } diff --git a/compiler/rustc_trait_selection/src/solve/fulfill.rs b/compiler/rustc_trait_selection/src/solve/fulfill.rs index 549dcc24cfa9a..96e6c5f182f5b 100644 --- a/compiler/rustc_trait_selection/src/solve/fulfill.rs +++ b/compiler/rustc_trait_selection/src/solve/fulfill.rs @@ -9,7 +9,8 @@ use rustc_infer::traits::{ use rustc_middle::ty::{self, TyCtxt, TypeVisitableExt, TypingMode}; use rustc_next_trait_solver::solve::fast_path::compute_goal_fast_path; use rustc_next_trait_solver::solve::{ - GoalEvaluation, GoalStalledOn, HasChanged, SolverDelegateEvalExt as _, StalledOnCoroutines, + GoalEvaluation, GoalStalledOn, GoalStalledOnOpaques, HasChanged, SolverDelegateEvalExt as _, + StalledOnCoroutines, TyOrConstInferVar, }; use thin_vec::ThinVec; use tracing::instrument; @@ -44,6 +45,15 @@ pub struct FulfillmentCtxt<'tcx, E: 'tcx> { /// gets rolled back. Because of this we explicitly check that we only /// use the context in exactly this snapshot. usable_in_snapshot: usize, + /// Whether every pending goal has precise, type-var-only stall info. + /// Int/float/const stalls and opaque-count mismatches force a full scan. + all_pending_trackable: bool, + /// Maximum type vid among pending `stalled_vars` / `sub_roots`. + /// Changes to strictly newer vids cannot unstall these goals. + max_stalled_ty_vid: Option, + /// Shared `GoalStalledOnOpaques::Yes` storage count, if any pending goal + /// recorded one. `None` means no pending goal depends on opaques. + stalled_opaque_count: Option, _errors: PhantomData, } @@ -132,10 +142,118 @@ impl<'tcx, E: 'tcx> FulfillmentCtxt<'tcx, E> { FulfillmentCtxt { obligations: Default::default(), usable_in_snapshot: infcx.num_open_snapshots(), + all_pending_trackable: true, + max_stalled_ty_vid: None, + stalled_opaque_count: None, _errors: PhantomData, } } + fn reset_tracking(&mut self) { + self.all_pending_trackable = true; + self.max_stalled_ty_vid = None; + self.stalled_opaque_count = None; + } + + fn note_registered_stalled_on(&mut self, stalled_on: Option<&GoalStalledOn>>) { + if !self.all_pending_trackable { + return; + } + let Some(stalled_on) = stalled_on else { + self.all_pending_trackable = false; + return; + }; + if !record_trackable_stalled_on( + stalled_on, + &mut self.max_stalled_ty_vid, + &mut self.stalled_opaque_count, + ) { + self.all_pending_trackable = false; + } + } + + fn recompute_tracking(&mut self) { + let mut max_stalled_ty_vid = None; + let mut stalled_opaque_count = None; + let mut all_pending_trackable = true; + for (_, stalled_on) in &self.obligations.pending { + match stalled_on { + Some(stalled_on) => { + if !record_trackable_stalled_on( + stalled_on, + &mut max_stalled_ty_vid, + &mut stalled_opaque_count, + ) { + all_pending_trackable = false; + break; + } + } + None => { + all_pending_trackable = false; + break; + } + } + } + self.all_pending_trackable = all_pending_trackable; + self.max_stalled_ty_vid = max_stalled_ty_vid; + self.stalled_opaque_count = stalled_opaque_count; + } + + /// Skip the pending-queue walk when no pending goal can have been + /// unstalled: every goal is trackable, opaque storage is unchanged, + /// and every type-infer change is to a newer vid than any stalled vid. + fn can_skip_fulfillment(&self, infcx: &InferCtxt<'tcx>) -> bool { + if infcx.disable_trait_solver_fast_paths() || !self.all_pending_trackable { + return false; + } + if let Some(n) = self.stalled_opaque_count + && infcx.opaque_type_count() != n + { + return false; + } + match (infcx.min_changed_ty_vid(), self.max_stalled_ty_vid) { + (None, _) | (Some(_), None) => true, + (Some(changed), Some(stalled)) => changed > stalled, + } + } +} + +/// Returns `false` if this stalled goal cannot participate in the +/// "no relevant type-infer change" fulfillment skip. +fn record_trackable_stalled_on<'tcx>( + stalled_on: &GoalStalledOn>, + max_stalled_ty_vid: &mut Option, + stalled_opaque_count: &mut Option, +) -> bool { + for var in &stalled_on.stalled_vars { + match *var { + TyOrConstInferVar::Ty(vid) => { + let idx = vid.as_u32(); + *max_stalled_ty_vid = Some(max_stalled_ty_vid.map_or(idx, |m| m.max(idx))); + } + TyOrConstInferVar::TyInt(_) + | TyOrConstInferVar::TyFloat(_) + | TyOrConstInferVar::Const(_) => return false, + } + } + for &vid in &stalled_on.sub_roots { + let idx = vid.as_u32(); + *max_stalled_ty_vid = Some(max_stalled_ty_vid.map_or(idx, |m| m.max(idx))); + } + match stalled_on.opaques { + GoalStalledOnOpaques::No => true, + GoalStalledOnOpaques::Yes { num_opaques_in_storage, .. } => match stalled_opaque_count { + None => { + *stalled_opaque_count = Some(num_opaques_in_storage); + true + } + Some(n) if *n == num_opaques_in_storage => true, + Some(_) => false, + }, + } +} + +impl<'tcx, E: 'tcx> FulfillmentCtxt<'tcx, E> { fn inspect_evaluated_obligation( infcx: &InferCtxt<'tcx>, obligation: &PredicateObligation<'tcx>, @@ -172,10 +290,12 @@ where match certainty { Certainty::Yes => {} Certainty::Maybe(_) => { + self.note_registered_stalled_on(stalled_on.as_ref()); self.obligations.register(obligation, stalled_on); } } } else { + self.note_registered_stalled_on(None); self.obligations.register(obligation, None); } } @@ -193,6 +313,13 @@ where fn try_evaluate_obligations(&mut self, infcx: &InferCtxt<'tcx>) -> TraitErrors { assert_eq!(self.usable_in_snapshot, infcx.num_open_snapshots()); + if self.obligations.pending.is_empty() { + self.reset_tracking(); + return TraitErrors::NoErrors; + } + if self.can_skip_fulfillment(infcx) { + return TraitErrors::NoErrors; + } let mut errors = TraitErrors::NoErrors; let delegate = <&SolverDelegate<'tcx>>::from(infcx); loop { @@ -285,6 +412,8 @@ where }); if overflowed { self.obligations.on_fulfillment_overflow(infcx); + // Remaining pending goals may be a mix; do not take the skip path. + self.all_pending_trackable = false; // Only return true errors that we have accumulated while processing. return errors; } @@ -294,6 +423,8 @@ where } } + infcx.reset_min_changed_ty_vid(); + self.recompute_tracking(); errors } diff --git a/tests/ui/traits/next-solver/fulfillment-skip-unrelated-infer.rs b/tests/ui/traits/next-solver/fulfillment-skip-unrelated-infer.rs new file mode 100644 index 0000000000000..50387bc8e6327 --- /dev/null +++ b/tests/ui/traits/next-solver/fulfillment-skip-unrelated-infer.rs @@ -0,0 +1,35 @@ +//@ check-pass +// +// Typeck of a long chain of stalled `Default` obligations, then a +// constraint that resolves them. Next-solver fulfillment may skip +// walking the pending queue when only newer, unrelated infer vids +// changed (rustc#159933). This must still notice the final `u8`. + +pub fn big() { + let mut v = Vec::new(); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(Default::default()); + v.push(0u8); +} + +fn main() { + big(); +} From 18ea5192e88761906396bbfd18426bc7234933d7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 21 Aug 2026 17:56:04 +0000 Subject: [PATCH 2/2] Avoid 'unstall' in comments for tidy spellcheck typos already allowlists 'unstalled'; the verb form trips CI. --- compiler/rustc_infer/src/infer/type_variable.rs | 2 +- compiler/rustc_trait_selection/src/solve/fulfill.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/rustc_infer/src/infer/type_variable.rs b/compiler/rustc_infer/src/infer/type_variable.rs index 24afec443d2a1..c00bff1451845 100644 --- a/compiler/rustc_infer/src/infer/type_variable.rs +++ b/compiler/rustc_infer/src/infer/type_variable.rs @@ -90,7 +90,7 @@ pub(crate) struct TypeVariableStorage<'tcx> { /// instantiated since the last [`TypeVariableTable::reset_min_changed_ty_vid`]. /// /// Next-solver fulfillment uses this to skip walking pending goals that - /// can only be unstalled by changes to older vids (see rustc#159933). + /// can only become unstalled by changes to older vids (see rustc#159933). min_changed_ty_vid: Option, } diff --git a/compiler/rustc_trait_selection/src/solve/fulfill.rs b/compiler/rustc_trait_selection/src/solve/fulfill.rs index 96e6c5f182f5b..c52214d251443 100644 --- a/compiler/rustc_trait_selection/src/solve/fulfill.rs +++ b/compiler/rustc_trait_selection/src/solve/fulfill.rs @@ -49,7 +49,7 @@ pub struct FulfillmentCtxt<'tcx, E: 'tcx> { /// Int/float/const stalls and opaque-count mismatches force a full scan. all_pending_trackable: bool, /// Maximum type vid among pending `stalled_vars` / `sub_roots`. - /// Changes to strictly newer vids cannot unstall these goals. + /// Changes to strictly newer vids cannot make these goals unstalled. max_stalled_ty_vid: Option, /// Shared `GoalStalledOnOpaques::Yes` storage count, if any pending goal /// recorded one. `None` means no pending goal depends on opaques.