From 6b7c35a227ff6c2982dfd01d1808077556a5f6af Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 21 Aug 2026 18:25:50 +0000 Subject: [PATCH] Skip next-solver fulfillment when no type var was instantiated Unknown-unknown equate and sub-unify do not make single-var stalled goals progress. Track instantiations in the infer undo log and skip the pending-queue walk when every pending goal is type-var-only with at most one stalled vid. This removes the remaining quadratic rescan on rustc#159933: each `push(Default::default())` equates a fresh vid with the vec element vid, so goals stay stalled until a later instantiate. Two-var goals still take the existing path. --- compiler/rustc_infer/src/infer/mod.rs | 16 +++ .../src/infer/opaque_types/table.rs | 7 + .../rustc_infer/src/infer/type_variable.rs | 33 ++++- .../src/solve/fulfill.rs | 129 +++++++++++++++++- .../fulfillment-skip-equate-two-var.rs | 23 ++++ .../fulfillment-skip-equate-unknowns.rs | 36 +++++ 6 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 tests/ui/traits/next-solver/fulfillment-skip-equate-two-var.rs create mode 100644 tests/ui/traits/next-solver/fulfillment-skip-equate-unknowns.rs diff --git a/compiler/rustc_infer/src/infer/mod.rs b/compiler/rustc_infer/src/infer/mod.rs index 583fb1d7db21a..3a9a1fb94f443 100644 --- a/compiler/rustc_infer/src/infer/mod.rs +++ b/compiler/rustc_infer/src/infer/mod.rs @@ -1613,6 +1613,22 @@ impl<'tcx> InferCtxt<'tcx> { } } + /// Whether any type vid was instantiated with a known type since the last + /// [`Self::reset_ty_instantiated`]. + #[inline] + pub fn ty_was_instantiated(&self) -> bool { + self.inner.borrow().type_variable_storage.ty_instantiated() + } + + pub fn reset_ty_instantiated(&self) { + self.inner.borrow_mut().type_variables().reset_ty_instantiated(); + } + + #[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..6da8376e2ed2e 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::ty_instantiated`]. + TyInstantiated(bool), } /// 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::TyInstantiated(prev) => self.ty_instantiated = prev, } } } @@ -83,6 +86,14 @@ 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, + /// Whether any type vid was instantiated with a known type since the last + /// [`TypeVariableTable::reset_ty_instantiated`]. + /// + /// Equating two unknown vids does not set this. Next-solver fulfillment + /// uses that to skip the pending-queue walk when every pending goal is + /// stalled on at most one type var: unknown-unknown unification cannot + /// make those goals progress (see rustc#159933). + ty_instantiated: bool, } pub(crate) struct TypeVariableTable<'a, 'tcx> { @@ -161,6 +172,11 @@ impl<'tcx> TypeVariableStorage<'tcx> { pub(crate) fn sub_unification_table_ref(&self) -> &ut::UnificationTableStorage { &self.sub_unification_table } + + #[inline] + pub(crate) fn ty_instantiated(&self) -> bool { + self.ty_instantiated + } } impl<'tcx> TypeVariableTable<'_, 'tcx> { @@ -172,6 +188,20 @@ impl<'tcx> TypeVariableTable<'_, 'tcx> { self.storage.values[vid].origin } + fn note_ty_instantiated(&mut self) { + if !self.storage.ty_instantiated { + self.undo_log.push(UndoLog::TyInstantiated(false)); + self.storage.ty_instantiated = true; + } + } + + pub(crate) fn reset_ty_instantiated(&mut self) { + if self.storage.ty_instantiated { + self.undo_log.push(UndoLog::TyInstantiated(true)); + self.storage.ty_instantiated = false; + } + } + /// Records that `a == b`. /// /// Precondition: neither `a` nor `b` are known. @@ -204,6 +234,7 @@ impl<'tcx> TypeVariableTable<'_, 'tcx> { "instantiating type variable `{vid:?}` twice: new-value = {ty:?}, old-value={:?}", self.eq_relations().probe_value(vid) ); + self.note_ty_instantiated(); 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..af7e127ce25ef 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, + /// Whether every trackable pending goal is stalled on at most one type var. + /// Goals stalled on two type vars can progress when those vars are equated. + all_pending_single_ty_stall: bool, + /// 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,82 @@ impl<'tcx, E: 'tcx> FulfillmentCtxt<'tcx, E> { FulfillmentCtxt { obligations: Default::default(), usable_in_snapshot: infcx.num_open_snapshots(), + all_pending_trackable: true, + all_pending_single_ty_stall: true, + stalled_opaque_count: None, _errors: PhantomData, } } + fn reset_tracking(&mut self) { + self.all_pending_trackable = true; + self.all_pending_single_ty_stall = true; + 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.stalled_opaque_count, + &mut self.all_pending_single_ty_stall, + ) { + self.all_pending_trackable = false; + } + } + + fn recompute_tracking(&mut self) { + let mut stalled_opaque_count = None; + let mut all_pending_trackable = true; + let mut all_pending_single_ty_stall = true; + for (_, stalled_on) in &self.obligations.pending { + match stalled_on { + Some(stalled_on) => { + if !record_trackable_stalled_on( + stalled_on, + &mut stalled_opaque_count, + &mut all_pending_single_ty_stall, + ) { + all_pending_trackable = false; + break; + } + } + None => { + all_pending_trackable = false; + break; + } + } + } + self.all_pending_trackable = all_pending_trackable; + self.all_pending_single_ty_stall = all_pending_single_ty_stall; + self.stalled_opaque_count = stalled_opaque_count; + } + + /// Skip the pending-queue walk when no pending goal can have made progress: + /// every goal is type-var-only with at most one stalled vid, opaque storage + /// is unchanged, and no type vid was instantiated. + fn can_skip_fulfillment(&self, infcx: &InferCtxt<'tcx>) -> bool { + if infcx.disable_trait_solver_fast_paths() + || !self.all_pending_trackable + || !self.all_pending_single_ty_stall + || infcx.ty_was_instantiated() + { + return false; + } + if let Some(n) = self.stalled_opaque_count + && infcx.opaque_type_count() != n + { + return false; + } + true + } + fn inspect_evaluated_obligation( infcx: &InferCtxt<'tcx>, obligation: &PredicateObligation<'tcx>, @@ -151,6 +233,38 @@ impl<'tcx, E: 'tcx> FulfillmentCtxt<'tcx, E> { } } +/// Returns `false` if this stalled goal cannot participate in the +/// "no type-var instantiate" fulfillment skip. +fn record_trackable_stalled_on<'tcx>( + stalled_on: &GoalStalledOn>, + stalled_opaque_count: &mut Option, + all_pending_single_ty_stall: &mut bool, +) -> bool { + let mut ty_stalls = 0usize; + for var in &stalled_on.stalled_vars { + match *var { + TyOrConstInferVar::Ty(_) => ty_stalls += 1, + TyOrConstInferVar::TyInt(_) + | TyOrConstInferVar::TyFloat(_) + | TyOrConstInferVar::Const(_) => return false, + } + } + if ty_stalls > 1 { + *all_pending_single_ty_stall = false; + } + 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> TraitEngine<'tcx, E> for FulfillmentCtxt<'tcx, E> where E: FromSolverError<'tcx, NextSolverError<'tcx>>, @@ -172,10 +286,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 +309,14 @@ 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(); + infcx.reset_ty_instantiated(); + 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 +409,7 @@ where }); if overflowed { self.obligations.on_fulfillment_overflow(infcx); + self.all_pending_trackable = false; // Only return true errors that we have accumulated while processing. return errors; } @@ -294,6 +419,8 @@ where } } + infcx.reset_ty_instantiated(); + self.recompute_tracking(); errors } diff --git a/tests/ui/traits/next-solver/fulfillment-skip-equate-two-var.rs b/tests/ui/traits/next-solver/fulfillment-skip-equate-two-var.rs new file mode 100644 index 0000000000000..ec127520db8d5 --- /dev/null +++ b/tests/ui/traits/next-solver/fulfillment-skip-equate-two-var.rs @@ -0,0 +1,23 @@ +//@ check-pass +// +// Unifying two unknown vids can make a two-var goal progress +// (`T: Mirror`). Fulfillment must still walk that goal; it +// must not treat unknown-unknown equate as a skip. + +trait Mirror {} +impl Mirror for T {} + +fn assert_mirror, U>(_: T, _: U) {} + +fn unify(x: T, y: T) {} + +pub fn check() { + let a = None; + let b = None; + assert_mirror(a, b); + unify(a, b); +} + +fn main() { + check(); +} diff --git a/tests/ui/traits/next-solver/fulfillment-skip-equate-unknowns.rs b/tests/ui/traits/next-solver/fulfillment-skip-equate-unknowns.rs new file mode 100644 index 0000000000000..b46c1e7c66b7e --- /dev/null +++ b/tests/ui/traits/next-solver/fulfillment-skip-equate-unknowns.rs @@ -0,0 +1,36 @@ +//@ check-pass +// +// Each `push(Default::default())` equates a fresh infer vid with the +// vec element vid. That is unknown-unknown unification, not instantiate. +// Next-solver fulfillment may skip the pending-queue walk for single-var +// stalls in that case (rustc#159933). Instantiating with `u8` must still +// resolve the stalled `Default` goals. + +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(); +}