From 4ee3a2a2056ebf777cccba3d4e5452b862ca56b6 Mon Sep 17 00:00:00 2001 From: Makro Date: Sun, 26 Jul 2026 05:57:06 +0000 Subject: [PATCH 1/2] Roll back partial hidden type equations in relate_opaques Equating a hidden type in insert_hidden_type registers region constraints on the inference context eagerly and returns only the non-region goals. When the equation fails partway, those partial region constraints stayed pending on the inference context: nothing drained them until the next query type op scraped and attributed them to itself, whichever op that happened to be. Roll them back instead. This makes it an invariant that no region state is pending on the inference context between type ops. Co-Authored-By: Claude Fable 5 --- compiler/rustc_borrowck/src/type_check/relate_tys.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/compiler/rustc_borrowck/src/type_check/relate_tys.rs b/compiler/rustc_borrowck/src/type_check/relate_tys.rs index 24383eea3600f..f7c032066e208 100644 --- a/compiler/rustc_borrowck/src/type_check/relate_tys.rs +++ b/compiler/rustc_borrowck/src/type_check/relate_tys.rs @@ -160,7 +160,15 @@ impl<'a, 'b, 'tcx> NllTypeRelating<'a, 'b, 'tcx> { "expected at least one opaque type in `relate_opaques`, got {a} and {b}." ), }; - self.register_goals(infcx.handle_opaque_type(a, b, self.span(), self.param_env())?); + // Equating the hidden type registers region constraints on the + // inference context eagerly, before its non-region goals reach + // `InstantiateOpaqueType` below. If the equation fails partway, roll + // those back rather than leaving them pending: nothing would scrape + // them until an unrelated later type op, and query type op fast + // paths rely on no region state being pending between type ops. + let goals = infcx + .commit_if_ok(|_| infcx.handle_opaque_type(a, b, self.span(), self.param_env()))?; + self.register_goals(goals); Ok(()) } From 03969d2ec9391b041f38a194df64c0eea0864a97 Mon Sep 17 00:00:00 2001 From: Makro Date: Sun, 26 Jul 2026 05:57:07 +0000 Subject: [PATCH 2/2] Return type op fast path results without the full fulfillment machinery Query type ops check their fast paths (trivial sizedness proofs, alias free normalizations) inside the closure passed to scrape_region_constraints. Every fast path hit therefore still paid for an inference snapshot, an ObligationCtxt with a boxed fulfillment engine, RefCell take and reset cycles for region constraints, and the region constraint scraping itself, all over empty data. MIR type check issues several type ops per statement, and the overwhelming majority of these hit a fast path. Fast path results are final: they register no obligations and produce no region constraints, so the fast path is now checked once at the top of fully_perform and the result returned directly. The two inner fast path checks become dead and are removed. resolve_vars_if_possible is kept on the returned value, matching what scrape_region_constraints did before. Skipping the machinery also skips its role as the unconditional drain point for region state pending on the inference context. With the hidden type equation rollback in the previous commit, nothing registers such state outside a type op; the fast path debug-asserts that invariant (and not-in-snapshot, matching the machinery's own assertions) via the new InferCtxt::has_pending_region_state helper, which is documented to stay in sync with what scrape_region_constraints drains. Borrowck's leftover-constraints debug check polices the same invariant after every op. Co-Authored-By: Claude Fable 5 --- .../rustc_infer/src/infer/outlives/mod.rs | 16 ++++++ .../src/traits/query/type_op/custom.rs | 4 ++ .../src/traits/query/type_op/mod.rs | 50 +++++++++++++------ 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/compiler/rustc_infer/src/infer/outlives/mod.rs b/compiler/rustc_infer/src/infer/outlives/mod.rs index 92b47295ade88..228aa3d8e8f55 100644 --- a/compiler/rustc_infer/src/infer/outlives/mod.rs +++ b/compiler/rustc_infer/src/infer/outlives/mod.rs @@ -119,6 +119,22 @@ impl<'tcx> InferCtxt<'tcx> { /// region constraints as normal, but then we take them and /// translate them into the form that the NLL solver /// understands. See the NLL module for mode details. + /// Returns `true` if any region obligations, region assumptions, or + /// region constraints are currently registered on this inference + /// context. Nothing may be pending here outside a type op: + /// `scrape_region_constraints` drains this state when each op + /// completes, and type op fast paths debug-assert emptiness before + /// skipping that machinery. + /// + /// N.B.: the checks here must stay in sync with the kinds of pending + /// region state drained by `scrape_region_constraints`. + pub fn has_pending_region_state(&self) -> bool { + let mut inner = self.inner.borrow_mut(); + !inner.region_obligations.is_empty() + || !inner.region_assumptions.is_empty() + || !inner.unwrap_region_constraints().data().is_empty() + } + pub fn take_and_reset_region_constraints(&self) -> RegionConstraintData<'tcx> { assert!( self.inner.borrow().region_obligations.is_empty(), diff --git a/compiler/rustc_trait_selection/src/traits/query/type_op/custom.rs b/compiler/rustc_trait_selection/src/traits/query/type_op/custom.rs index 812e087a0cf69..18321667f67cf 100644 --- a/compiler/rustc_trait_selection/src/traits/query/type_op/custom.rs +++ b/compiler/rustc_trait_selection/src/traits/query/type_op/custom.rs @@ -62,6 +62,10 @@ impl fmt::Debug for CustomTypeOp { /// Executes `op` and then scrapes out all the "old style" region /// constraints that result, creating query-region-constraints. +/// +/// N.B.: the kinds of pending region state drained here must stay in sync +/// with `InferCtxt::has_pending_region_state`, which type op fast paths +/// debug-assert to be empty when skipping this function. pub fn scrape_region_constraints<'tcx, Op, R>( infcx: &InferCtxt<'tcx>, root_def_id: LocalDefId, diff --git a/compiler/rustc_trait_selection/src/traits/query/type_op/mod.rs b/compiler/rustc_trait_selection/src/traits/query/type_op/mod.rs index 3a8a3fc66e663..a42f9ade9ec5c 100644 --- a/compiler/rustc_trait_selection/src/traits/query/type_op/mod.rs +++ b/compiler/rustc_trait_selection/src/traits/query/type_op/mod.rs @@ -110,12 +110,6 @@ pub trait QueryTypeOp<'tcx>: fmt::Debug + Copy + TypeFoldable> + 't ), NoSolution, > { - if !infcx.disable_trait_solver_fast_paths() - && let Some(result) = QueryTypeOp::try_fast_path(infcx.tcx, &query_key) - { - return Ok((result, None, PredicateObligations::new(), Certainty::Proven)); - } - let mut canonical_var_values = OriginalQueryValues::default(); let old_param_env = query_key.param_env; let canonical_self = infcx.canonicalize_query(query_key, &mut canonical_var_values); @@ -147,6 +141,41 @@ where root_def_id: LocalDefId, span: Span, ) -> Result, ErrorGuaranteed> { + // Fast-path results are final: they never register obligations or + // produce region constraints, so we can skip opening a snapshot, + // building a fulfillment context, and scraping region constraints. + // The overwhelming majority of type ops performed during MIR type + // check take this path. + // + // This is only sound while the inference context has no pending + // region state: `scrape_region_constraints` unconditionally drains + // region constraints registered on the inference context and + // attributes them to the current type op, and some of that state is + // registered outside any type op. In particular, when equating + // hidden types, `insert_hidden_type` eagerly registers region + // constraints on the inference context and its non-region goals may + // never reach `InstantiateOpaqueType` if the equation fails. If + // anything is pending, fall through to the full machinery, which + // scrapes (and asserts) exactly as before. + if !infcx.disable_trait_solver_fast_paths() + && let Some(output) = QueryTypeOp::try_fast_path(infcx.tcx, &self) + { + // Skipping the machinery relies on no region state being pending + // on the inference context: `scrape_region_constraints` drains + // whatever is registered during an op once it completes, and + // nothing may register region state outside an op. These + // assertions and borrowck's leftover-constraints check enforce + // that invariant, like the machinery's own assertions did here + // before the fast path was hoisted out of it. + debug_assert!( + !infcx.has_pending_region_state(), + "region state pending at a query type op fast path" + ); + debug_assert!(!infcx.in_snapshot(), "query type op performed inside a snapshot"); + let output = infcx.resolve_vars_if_possible(output); + return Ok(TypeOpOutput { output, constraints: None, error_info: None }); + } + // In the new trait solver, query type ops are performed locally. This // is because query type ops currently use the old canonicalizer, and // that doesn't preserve things like opaques which have been registered @@ -160,14 +189,7 @@ where root_def_id, "query type op", span, - |ocx| { - if !infcx.disable_trait_solver_fast_paths() - && let Some(result) = QueryTypeOp::try_fast_path(infcx.tcx, &self) - { - return Ok(result); - } - QueryTypeOp::perform_locally_with_next_solver(ocx, self, span) - }, + |ocx| QueryTypeOp::perform_locally_with_next_solver(ocx, self, span), )? .0); }