From c606c55b3fa3d2744af7618c0a81919a0efbe8de Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Tue, 1 Sep 2026 18:09:27 -0300 Subject: [PATCH 1/6] Add minimal coroutine binder assumption mode --- compiler/rustc_interface/src/tests.rs | 30 ++++++++++------ compiler/rustc_middle/src/ty/context.rs | 6 +++- .../src/ty/context/impl_interner.rs | 4 +++ compiler/rustc_session/src/config.rs | 35 +++++++++++++++---- compiler/rustc_session/src/options.rs | 22 ++++++++++-- compiler/rustc_type_ir/src/interner.rs | 2 ++ 6 files changed, 78 insertions(+), 21 deletions(-) diff --git a/compiler/rustc_interface/src/tests.rs b/compiler/rustc_interface/src/tests.rs index 41ce4fb759dac..6f80c09bd76ed 100644 --- a/compiler/rustc_interface/src/tests.rs +++ b/compiler/rustc_interface/src/tests.rs @@ -10,14 +10,15 @@ use rustc_errors::ColorConfig; use rustc_errors::emitter::HumanReadableErrorType; use rustc_lint_defs::Level; use rustc_session::config::{ - AnnotateMoves, AutoDiff, BranchProtection, CFGuard, Cfg, CodegenRetagOptions, CoverageLevel, - CoverageOptions, DebugInfo, DumpMonoStatsFormat, ErrorOutputType, ExternEntry, ExternLocation, - Externs, FmtDebug, FunctionReturn, IncrementalStateAssertion, InliningThreshold, Input, - InstrumentCoverage, InstrumentMcount, InstrumentMcountOpts, InstrumentXRay, LinkSelfContained, - LinkerPluginLto, LocationDetail, LtoCli, MirIncludeSpans, NextSolverConfig, Offload, Options, - OutFileName, OutputType, OutputTypes, PAuthKey, PacRet, Passes, PatchableFunctionEntry, - Polonius, ProcMacroExecutionStrategy, Strip, SwitchWithOptPath, SymbolManglingVersion, - WasiExecModel, build_session_options, rustc_optgroups, + AnnotateMoves, AssumptionsOnBinders, AutoDiff, BranchProtection, CFGuard, Cfg, + CodegenRetagOptions, CoverageLevel, CoverageOptions, DebugInfo, DumpMonoStatsFormat, + ErrorOutputType, ExternEntry, ExternLocation, Externs, FmtDebug, FunctionReturn, + IncrementalStateAssertion, InliningThreshold, Input, InstrumentCoverage, InstrumentMcount, + InstrumentMcountOpts, InstrumentXRay, LinkSelfContained, LinkerPluginLto, LocationDetail, + LtoCli, MirIncludeSpans, NextSolverConfig, Offload, Options, OutFileName, OutputType, + OutputTypes, PAuthKey, PacRet, Passes, PatchableFunctionEntry, Polonius, + ProcMacroExecutionStrategy, Strip, SwitchWithOptPath, SymbolManglingVersion, WasiExecModel, + build_session_options, rustc_optgroups, }; use rustc_session::search_paths::SearchPath; use rustc_session::utils::{CanonicalizedPath, NativeLib}; @@ -950,7 +951,14 @@ fn test_assumptions_on_binders_enables_next_solver_globally() { // `-Zassumptions-on-binders` alone enables the next solver globally. let matches = optgroups().parse(&["-Zassumptions-on-binders".to_string()]).unwrap(); let opts = build_session_options(&mut early_dcx, &matches); - assert!(opts.unstable_opts.assumptions_on_binders); + assert_eq!(opts.unstable_opts.assumptions_on_binders, AssumptionsOnBinders::All); + assert_eq!(opts.unstable_opts.next_solver, globally); + + // The minimal coroutine mode also requires the next solver globally. + let matches = + optgroups().parse(&["-Zassumptions-on-binders=min_coroutines".to_string()]).unwrap(); + let opts = build_session_options(&mut early_dcx, &matches); + assert_eq!(opts.unstable_opts.assumptions_on_binders, AssumptionsOnBinders::MinCoroutines); assert_eq!(opts.unstable_opts.next_solver, globally); // Flag order must not matter when both `-Zassumptions-on-binders` and `-Znext-solver` @@ -961,7 +969,7 @@ fn test_assumptions_on_binders_enables_next_solver_globally() { ] { let matches = optgroups().parse(&args).unwrap(); let opts = build_session_options(&mut early_dcx, &matches); - assert!(opts.unstable_opts.assumptions_on_binders); + assert_eq!(opts.unstable_opts.assumptions_on_binders, AssumptionsOnBinders::All); assert_eq!(opts.unstable_opts.next_solver, globally); } @@ -974,7 +982,7 @@ fn test_assumptions_on_binders_enables_next_solver_globally() { ] { let matches = optgroups().parse(&args).unwrap(); let opts = build_session_options(&mut early_dcx, &matches); - assert!(opts.unstable_opts.assumptions_on_binders); + assert_eq!(opts.unstable_opts.assumptions_on_binders, AssumptionsOnBinders::All); assert_eq!(opts.unstable_opts.next_solver, globally); } } diff --git a/compiler/rustc_middle/src/ty/context.rs b/compiler/rustc_middle/src/ty/context.rs index 5ff5c05de734a..440ab0dcc2a90 100644 --- a/compiler/rustc_middle/src/ty/context.rs +++ b/compiler/rustc_middle/src/ty/context.rs @@ -2828,7 +2828,11 @@ impl<'tcx> TyCtxt<'tcx> { } pub fn assumptions_on_binders(self) -> bool { - self.sess.opts.unstable_opts.assumptions_on_binders + self.sess.opts.unstable_opts.assumptions_on_binders.is_enabled() + } + + pub fn assumptions_on_binders_min_coroutines(self) -> bool { + self.sess.opts.unstable_opts.assumptions_on_binders.is_min_coroutines() } pub fn is_impl_trait_in_trait(self, def_id: DefId) -> bool { diff --git a/compiler/rustc_middle/src/ty/context/impl_interner.rs b/compiler/rustc_middle/src/ty/context/impl_interner.rs index 202991d3f0ada..9bba7eeed366e 100644 --- a/compiler/rustc_middle/src/ty/context/impl_interner.rs +++ b/compiler/rustc_middle/src/ty/context/impl_interner.rs @@ -383,6 +383,10 @@ impl<'tcx> Interner for TyCtxt<'tcx> { self.assumptions_on_binders() } + fn assumptions_on_binders_min_coroutines(self) -> bool { + self.assumptions_on_binders_min_coroutines() + } + fn renormalize_rigid_aliases(self) -> bool { self.renormalize_rigid_aliases() } diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs index d30cc29e66298..590f95d6015d6 100644 --- a/compiler/rustc_session/src/config.rs +++ b/compiler/rustc_session/src/config.rs @@ -1023,6 +1023,28 @@ impl ExternEntry { } } +/// The behavior selected by `-Zassumptions-on-binders`. +#[derive(Debug, Copy, Clone, Default, Hash, PartialEq, Eq)] +pub enum AssumptionsOnBinders { + /// Do not deduce outlives assumptions when entering binders. + #[default] + Disabled, + /// Deduce outlives assumptions from every binder. + All, + /// Deduce outlives assumptions only from coroutine-witness binders. + MinCoroutines, +} + +impl AssumptionsOnBinders { + pub fn is_enabled(self) -> bool { + self != AssumptionsOnBinders::Disabled + } + + pub fn is_min_coroutines(self) -> bool { + self == AssumptionsOnBinders::MinCoroutines + } +} + #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] pub struct NextSolverConfig { /// Whether the new trait solver should be enabled in coherence. @@ -2701,7 +2723,7 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M // `-Zassumptions-on-binders` requires the next trait solver globally. Normalize after // parsing so the effective config is independent of flag order and so consumers that // read `next_solver.globally` directly (e.g. feature-gate checks) see the right value. - if unstable_opts.assumptions_on_binders { + if unstable_opts.assumptions_on_binders.is_enabled() { // `NextSolverConfig::default()` has `coherence: true`; the only way `coherence` is // false here is an explicit `-Znext-solver=no`. if !unstable_opts.next_solver.coherence { @@ -3330,11 +3352,11 @@ pub(crate) mod dep_tracking { }; use super::{ - AnnotateMoves, AutoDiff, BranchProtection, CFGuard, CFProtection, CodegenRetagOptions, - CoverageOptions, CrateType, DebugInfo, DebugInfoCompression, ErrorOutputType, FmtDebug, - FunctionReturn, InliningThreshold, InstrumentCoverage, InstrumentMcount, - InstrumentMcountOpts, InstrumentXRay, LinkerPluginLto, LocationDetail, LtoCli, - MirStripDebugInfo, NextSolverConfig, Offload, OptLevel, OutFileName, OutputType, + AnnotateMoves, AssumptionsOnBinders, AutoDiff, BranchProtection, CFGuard, CFProtection, + CodegenRetagOptions, CoverageOptions, CrateType, DebugInfo, DebugInfoCompression, + ErrorOutputType, FmtDebug, FunctionReturn, InliningThreshold, InstrumentCoverage, + InstrumentMcount, InstrumentMcountOpts, InstrumentXRay, LinkerPluginLto, LocationDetail, + LtoCli, MirStripDebugInfo, NextSolverConfig, Offload, OptLevel, OutFileName, OutputType, OutputTypes, PatchableFunctionEntry, PointerAuthOption, Polonius, ResolveDocLinks, SourceFileHashAlgorithm, SplitDwarfKind, SwitchWithOptPath, SymbolManglingVersion, WasiExecModel, @@ -3381,6 +3403,7 @@ pub(crate) mod dep_tracking { impl_dep_tracking_hash_via_hash!( (), AnnotateMoves, + AssumptionsOnBinders, AutoDiff, Offload, bool, diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs index 8fd9c4da967dc..1f114d65f94e5 100644 --- a/compiler/rustc_session/src/options.rs +++ b/compiler/rustc_session/src/options.rs @@ -855,6 +855,8 @@ mod desc { pub(crate) const parse_instrument_xray: &str = "either a boolean (`yes`, `no`, `on`, `off`, etc), or a comma separated list of settings: `always` or `never` (mutually exclusive), `ignore-loops`, `instruction-threshold=N`, `skip-entry`, `skip-exit`"; pub(crate) const parse_unpretty: &str = "`string` or `string=string`"; pub(crate) const parse_treat_err_as_bug: &str = "either no value or a non-negative number"; + pub(crate) const parse_assumptions_on_binders: &str = + "either a boolean (`yes`, `no`, `on`, `off`, etc), or `min_coroutines`"; pub(crate) const parse_next_solver_config: &str = "either `globally` (when used without an argument), `coherence` (default) or `no`"; pub(crate) const parse_lto: &str = @@ -959,6 +961,19 @@ pub mod parse { } } + pub(crate) fn parse_assumptions_on_binders( + slot: &mut AssumptionsOnBinders, + v: Option<&str>, + ) -> bool { + *slot = match v { + Some("y") | Some("yes") | Some("on") | Some("true") | None => AssumptionsOnBinders::All, + Some("n") | Some("no") | Some("off") | Some("false") => AssumptionsOnBinders::Disabled, + Some("min_coroutines") => AssumptionsOnBinders::MinCoroutines, + Some(_) => return false, + }; + true + } + /// Use this for any boolean option that lacks a static default. (The /// actions taken when such an option is not specified will depend on /// other factors, such as other options, or target options.) @@ -2378,9 +2393,10 @@ options! { either `loaded` or `not-loaded`."), assume_incomplete_release: bool = (false, parse_bool, [TRACKED], "make cfg(version) treat the current version as incomplete (default: no)"), - assumptions_on_binders: bool = (false, parse_bool, [TRACKED], - "allow deducing higher-ranked outlives assumptions from all binders (`for<'a>`); \ - implies `-Znext-solver=globally`"), + assumptions_on_binders: AssumptionsOnBinders = (AssumptionsOnBinders::Disabled, + parse_assumptions_on_binders, [TRACKED], + "allow deducing higher-ranked outlives assumptions from all binders (`for<'a>`), or only \ + coroutine-witness binders with `min_coroutines`; implies `-Znext-solver=globally`"), autodiff: Vec = (Vec::new(), parse_autodiff, [TRACKED], "a list of autodiff flags to enable Mandatory setting: diff --git a/compiler/rustc_type_ir/src/interner.rs b/compiler/rustc_type_ir/src/interner.rs index 31a027c15fd01..2568b228a147f 100644 --- a/compiler/rustc_type_ir/src/interner.rs +++ b/compiler/rustc_type_ir/src/interner.rs @@ -355,6 +355,8 @@ pub trait Interner: fn assumptions_on_binders(self) -> bool; + fn assumptions_on_binders_min_coroutines(self) -> bool; + fn renormalize_rigid_aliases(self) -> bool; fn coroutine_hidden_types( From 30060f661ce6c17e843f082c732b4b49db7aa6be Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Tue, 1 Sep 2026 18:13:03 -0300 Subject: [PATCH 2/6] Implement minimal coroutine binder assumptions Keep type-outlives constraints intact while leaving a minimal-mode binder, then remove only leaves proven by that binder. Ordinary binders continue through the normal eager leak check. --- .../rustc_hir_analysis/src/check/wfcheck.rs | 1 + compiler/rustc_infer/src/infer/context.rs | 6 +- .../src/infer/outlives/obligations.rs | 2 +- .../src/canonical/mod.rs | 36 +-- .../src/solve/effect_goals.rs | 32 +-- .../src/solve/eval_ctxt/mod.rs | 24 +- .../eval_ctxt/solver_region_constraints.rs | 70 +----- .../rustc_next_trait_solver/src/solve/mod.rs | 14 +- .../src/solve/trait_goals.rs | 8 +- .../rustc_type_ir/src/region_constraint.rs | 207 +++++++++++++++++- 10 files changed, 283 insertions(+), 117 deletions(-) diff --git a/compiler/rustc_hir_analysis/src/check/wfcheck.rs b/compiler/rustc_hir_analysis/src/check/wfcheck.rs index 0dee9690737df..10596caccc94c 100644 --- a/compiler/rustc_hir_analysis/src/check/wfcheck.rs +++ b/compiler/rustc_hir_analysis/src/check/wfcheck.rs @@ -2369,6 +2369,7 @@ impl<'tcx> WfCheckingCtxt<'_, 'tcx> { match c { LeafRegionConstraint::Ambiguity(_) | LeafRegionConstraint::RegionOutlives(..) + | LeafRegionConstraint::TypeOutlives(..) | LeafRegionConstraint::AliasTyOutlivesViaEnv(..) => (), // OK LeafRegionConstraint::PlaceholderTyOutlives(ty, _, span) => { // we can't check this during lowering, because the ty is a ty::Bound that gets diff --git a/compiler/rustc_infer/src/infer/context.rs b/compiler/rustc_infer/src/infer/context.rs index f9b08efad88cf..3a54f8baf17c2 100644 --- a/compiler/rustc_infer/src/infer/context.rs +++ b/compiler/rustc_infer/src/infer/context.rs @@ -190,9 +190,9 @@ impl<'tcx> rustc_type_ir::InferCtxtLike for InferCtxt<'tcx> { ) -> U { self.enter_forall(value, |value| { let u = self.universe(); - self.placeholder_assumptions_for_next_solver - .borrow_mut() - .insert(u, Some(rustc_type_ir::region_constraint::Assumptions::empty())); + let assumptions = (!self.tcx.assumptions_on_binders_min_coroutines()) + .then(rustc_type_ir::region_constraint::Assumptions::empty); + self.placeholder_assumptions_for_next_solver.borrow_mut().insert(u, assumptions); f(value) }) } diff --git a/compiler/rustc_infer/src/infer/outlives/obligations.rs b/compiler/rustc_infer/src/infer/outlives/obligations.rs index cbbf5e3c91c42..ad87eb4b8ce0a 100644 --- a/compiler/rustc_infer/src/infer/outlives/obligations.rs +++ b/compiler/rustc_infer/src/infer/outlives/obligations.rs @@ -302,7 +302,7 @@ impl<'tcx> InferCtxt<'tcx> { b, a, category, ); } - AliasTyOutlivesViaEnv(..) | PlaceholderTyOutlives(..) => { + TypeOutlives(..) | AliasTyOutlivesViaEnv(..) | PlaceholderTyOutlives(..) => { unreachable!() } } diff --git a/compiler/rustc_next_trait_solver/src/canonical/mod.rs b/compiler/rustc_next_trait_solver/src/canonical/mod.rs index 0d8620c3614a2..804d3ad125bae 100644 --- a/compiler/rustc_next_trait_solver/src/canonical/mod.rs +++ b/compiler/rustc_next_trait_solver/src/canonical/mod.rs @@ -163,20 +163,7 @@ where let prev_universe = delegate.universe(); let universes_created_in_query = response.max_universe.index(); for _ in 0..universes_created_in_query { - let new_universe = delegate.create_next_universe(); - if delegate.cx().assumptions_on_binders() { - // FIXME(-Zassumptions-on-binders): Remove this temporary workaround once - // opaque types no longer escape query responses with query-created placeholders. - // Region constraints involving query-created placeholders were handled inside - // the query. However, the placeholders can still escape in other response - // fields, such as opaque type constraints. To avoid triggering - // assertions, we explicitly insert empty assumptions for the - // recreated universes here. - delegate.insert_placeholder_assumptions( - new_universe, - Some(rustc_type_ir::region_constraint::Assumptions::empty()), - ); - } + create_next_universe_with_placeholder_assumptions(delegate); } compute_query_response_instantiation_values_in_universe( @@ -188,6 +175,25 @@ where ) } +fn create_next_universe_with_placeholder_assumptions(delegate: &D) +where + D: SolverDelegate, + I: Interner, +{ + let new_universe = delegate.create_next_universe(); + if delegate.cx().assumptions_on_binders() { + // FIXME(-Zassumptions-on-binders): Remove this temporary workaround once opaque types no + // longer escape query responses with query-created placeholders. Region constraints + // involving query-created placeholders were handled inside the query, but placeholders can + // still escape in other response fields. These contextless universes use empty assumptions: + // they cannot discharge constraints, but allow them to propagate back to their source. + delegate.insert_placeholder_assumptions( + new_universe, + Some(rustc_type_ir::region_constraint::Assumptions::empty()), + ); + } +} + fn compute_query_response_instantiation_values_in_universe( delegate: &D, original_values: &[I::GenericArg], @@ -591,7 +597,7 @@ where // and the previous instantiation, extend `orig_values` for it. let max_universe = prev_universe + state.max_universe.index(); while delegate.universe() < max_universe { - delegate.create_next_universe(); + create_next_universe_with_placeholder_assumptions(delegate); } orig_values.extend( state.value.var_values.var_values.as_slice()[orig_values.len()..] diff --git a/compiler/rustc_next_trait_solver/src/solve/effect_goals.rs b/compiler/rustc_next_trait_solver/src/solve/effect_goals.rs index 04d1376d20b9f..ea9d88df19438 100644 --- a/compiler/rustc_next_trait_solver/src/solve/effect_goals.rs +++ b/compiler/rustc_next_trait_solver/src/solve/effect_goals.rs @@ -14,6 +14,7 @@ use tracing::instrument; use super::assembly::{Candidate, structural_traits}; use crate::delegate::SolverDelegate; +use crate::solve::eval_ctxt::ForallBinderKind; use crate::solve::{ BuiltinImplSource, CandidateSource, Certainty, EvalCtxt, Goal, GoalSource, NoSolution, assembly, }; @@ -267,19 +268,24 @@ where structural_traits::instantiate_constituent_tys_for_copy_clone_trait(ecx, self_ty)?; ecx.probe_builtin_trait_candidate(BuiltinImplSource::Misc).enter(|ecx| { - ecx.enter_forall_with_assumptions(constituent_tys, goal.param_env, |ecx, tys| { - ecx.add_goals( - GoalSource::ImplWhereBound, - tys.into_iter().map(|ty| { - goal.with( - cx, - ty::ClauseKind::HostEffect( - goal.predicate.with_replaced_self_ty(cx, ty), - ), - ) - }), - ) - })?; + ecx.enter_forall_with_assumptions( + constituent_tys, + goal.param_env, + ForallBinderKind::for_self_ty::(self_ty), + |ecx, tys| { + ecx.add_goals( + GoalSource::ImplWhereBound, + tys.into_iter().map(|ty| { + goal.with( + cx, + ty::ClauseKind::HostEffect( + goal.predicate.with_replaced_self_ty(cx, ty), + ), + ) + }), + ) + }, + )?; ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes) }) diff --git a/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/mod.rs b/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/mod.rs index 60813f00dd4c9..1fbe7773b4716 100644 --- a/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/mod.rs +++ b/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/mod.rs @@ -51,6 +51,22 @@ pub mod fast_path; mod probe; mod solver_region_constraints; +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(super) enum ForallBinderKind { + Other, + CoroutineWitness, +} + +impl ForallBinderKind { + pub(super) fn for_self_ty(self_ty: I::Ty) -> Self { + if matches!(self_ty.kind(), ty::CoroutineWitness(..)) { + Self::CoroutineWitness + } else { + Self::Other + } + } +} + /// The kind of goal we're currently proving. /// /// This has effects on cycle handling handling and on how we compute @@ -886,7 +902,7 @@ where ) -> QueryResultOrRerunNonErased { let Goal { param_env, predicate } = goal; let kind = predicate.kind(); - self.enter_forall_with_assumptions(kind, param_env, |ecx, kind| { + self.enter_forall_with_assumptions(kind, param_env, ForallBinderKind::Other, |ecx, kind| { Ok(match kind { ty::PredicateKind::Clause(ty::ClauseKind::Trait(predicate)) => { ecx.compute_trait_goal(Goal { param_env, predicate }).map(|(r, _via)| r)? @@ -1285,11 +1301,15 @@ where &mut self, value: ty::Binder, param_env: I::ParamEnv, + binder_kind: ForallBinderKind, f: impl FnOnce(&mut Self, T) -> U, ) -> U { self.delegate.enter_forall_without_assumptions(value, |value| { let u = self.delegate.universe(); - let assumptions = if self.cx().assumptions_on_binders() { + let assumptions = if self.cx().assumptions_on_binders() + && (!self.cx().assumptions_on_binders_min_coroutines() + || binder_kind == ForallBinderKind::CoroutineWitness) + { self.region_assumptions_for_placeholders_in_universe(value.clone(), u, param_env) } else { None diff --git a/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/solver_region_constraints.rs b/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/solver_region_constraints.rs index 5a4daa5e44fc5..48484e0fa5e79 100644 --- a/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/solver_region_constraints.rs +++ b/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/solver_region_constraints.rs @@ -4,16 +4,14 @@ use rustc_data_structures::transitive_relation::TransitiveRelationBuilder; use rustc_type_ir::ClauseKind::*; use rustc_type_ir::inherent::*; -use rustc_type_ir::outlives::{Component, push_outlives_components}; #[cfg(not(feature = "nightly"))] use rustc_type_ir::region_constraint::TransitiveRelationBuilder; use rustc_type_ir::region_constraint::{ - And, Assumptions, LeafRegionConstraint, Or, eagerly_handle_placeholders_in_universe, - propagate_ambiguity, + Assumptions, eagerly_handle_placeholders_in_universe, propagate_ambiguity, }; use rustc_type_ir::{ - AliasTy, Binder, ClauseKind, InferCtxtLike, Interner, OutlivesClause, Region, TypeVisitable, - TypeVisitableExt, TypeVisitor, UniverseIndex, max_universe, + ClauseKind, InferCtxtLike, Interner, OutlivesClause, TypeVisitable, TypeVisitableExt, + TypeVisitor, UniverseIndex, max_universe, }; use tracing::{debug, instrument}; @@ -150,66 +148,4 @@ where Ok(Certainty::Yes) } } - - /// Convert a type outlives constraint into a set of region outlives constraints and - /// type outlives constraints between the "components" of the type. E.g. `Foo: 'b` - /// will be turned into `T: 'b, 'a: 'b` - #[instrument(level = "debug", skip(self), ret)] - pub(in crate::solve) fn destructure_type_outlives(&mut self, ty: I::Ty, r: Region) -> Or { - let mut components = Default::default(); - push_outlives_components(self.cx(), ty, &mut components); - self.destructure_components(&components, r) - } - - fn destructure_components(&mut self, components: &[Component], r: Region) -> Or { - components - .into_iter() - .fold(Or::new_true(), |acc, c| Or::build_and(acc, self.destructure_component(c, r))) - } - - fn destructure_component(&mut self, c: &Component, r: Region) -> Or { - use Component::*; - use LeafRegionConstraint::*; - match c { - Region(c_r) => Or::new_leaf(RegionOutlives(*c_r, r, ())), - Placeholder(p) => { - Or::new_leaf(PlaceholderTyOutlives(Ty::new_placeholder(self.cx(), *p), r, ())) - } - Alias(_, alias) => self.destructure_alias_outlives(*alias, r), - UnresolvedInferenceVariable(_) => Or::new_ambig(()), - Param(_) => panic!("Params should have been canonicalized to placeholders"), - EscapingAlias(components) => self.destructure_components(components, r), - } - } - - /// Convert an alias outlives constraint into an OR constraint of any number of three - /// separate classes of candidates: - /// 1. component outlives. we turn `Alias: 'b` into `T: 'b, 'a: 'b`. - /// 2. item bounds. we turn `Alias: 'b` into `'c: 'b` if `Alias` is - /// defined as `type Alias: 'c` - /// 3. env assumptions. we defer handling `Alias: 'b` via where clauses until - /// when exiting the current binder. See [`LeafRegionConstraint::AliasTyOutlivesViaEnv`]. - #[instrument(level = "debug", skip(self), ret)] - fn destructure_alias_outlives(&mut self, alias: AliasTy, r: Region) -> Or { - use LeafRegionConstraint::*; - - let item_bounds = - rustc_type_ir::outlives::declared_bounds_from_definition(self.cx(), alias) - .map(|bound| And::new([RegionOutlives(bound, r, ())])); - let item_bound_outlives = Or::new(item_bounds); - - let where_clause_outlives = - Or::new_leaf(AliasTyOutlivesViaEnv(Binder::dummy((alias, r)), ())); - - let mut components = Default::default(); - rustc_type_ir::outlives::compute_alias_components_recursive( - self.cx(), - alias, - &mut components, - ); - let components_outlives = self.destructure_components(&components, r); - - let assumption_outlives = Or::build_or(item_bound_outlives, where_clause_outlives); - Or::build_or(assumption_outlives, components_outlives) - } } diff --git a/compiler/rustc_next_trait_solver/src/solve/mod.rs b/compiler/rustc_next_trait_solver/src/solve/mod.rs index 8d20bcf4c7a6d..bf05c30102d6c 100644 --- a/compiler/rustc_next_trait_solver/src/solve/mod.rs +++ b/compiler/rustc_next_trait_solver/src/solve/mod.rs @@ -93,10 +93,16 @@ where let ty = self.normalize(GoalSource::Misc, goal.param_env, ty::Unnormalized::new_wip(ty))?; if self.cx().assumptions_on_binders() { - use rustc_type_ir::region_constraint::RegionConstraint; - - let constraint = self.destructure_type_outlives(ty, lt); - self.register_solver_region_constraint(RegionConstraint::new_from_or(constraint)); + use rustc_type_ir::region_constraint::{ + LeafRegionConstraint, RegionConstraint, destructure_type_outlives, + }; + + let constraint = if self.cx().assumptions_on_binders_min_coroutines() { + RegionConstraint::new_leaf(LeafRegionConstraint::TypeOutlives(ty, lt, ())) + } else { + RegionConstraint::new_from_or(destructure_type_outlives(self.cx(), ty, lt, ())) + }; + self.register_solver_region_constraint(constraint); } else { self.register_ty_outlives(ty, lt); } diff --git a/compiler/rustc_next_trait_solver/src/solve/trait_goals.rs b/compiler/rustc_next_trait_solver/src/solve/trait_goals.rs index 51ec0bcbf5ec2..2aa8a589ba60c 100644 --- a/compiler/rustc_next_trait_solver/src/solve/trait_goals.rs +++ b/compiler/rustc_next_trait_solver/src/solve/trait_goals.rs @@ -21,6 +21,7 @@ use crate::solve::assembly::structural_traits::{self, AsyncCallableRelevantTypes use crate::solve::assembly::{ self, AllowInferenceConstraints, AssembleCandidatesFrom, Candidate, FailedCandidateInfo, }; +use crate::solve::eval_ctxt::ForallBinderKind; use crate::solve::inspect::ProbeKind; use crate::solve::{ BuiltinImplSource, CandidateSource, Certainty, EvalCtxt, Goal, GoalSource, MaybeCause, @@ -1118,6 +1119,7 @@ where ecx.enter_forall_with_assumptions( target_projection, param_env, + ForallBinderKind::Other, |ecx, target_projection| { let source_projection = ecx.instantiate_binder_with_infer(source_projection); @@ -1145,6 +1147,7 @@ where ecx.enter_forall_with_assumptions( target_principal, param_env, + ForallBinderKind::Other, |ecx, target_principal| { let source_principal = ecx.instantiate_binder_with_infer(source_principal); @@ -1180,6 +1183,7 @@ where ecx.enter_forall_with_assumptions( target_projection, param_env, + ForallBinderKind::Other, |ecx, target_projection| { let source_projection = ecx.instantiate_binder_with_infer(matching); ecx.eq(param_env, source_projection, target_projection)?; @@ -1411,9 +1415,11 @@ where ) -> Result>, NoSolution>, ) -> Result, NoSolutionOrRerunNonErased> { self.probe_trait_candidate(source).enter(|ecx| { + let self_ty = goal.predicate.self_ty(); let goals = ecx.enter_forall_with_assumptions( - constituent_tys(ecx, goal.predicate.self_ty())?, + constituent_tys(ecx, self_ty)?, goal.param_env, + ForallBinderKind::for_self_ty::(self_ty), |ecx, tys| { tys.into_iter() .map(|ty| { diff --git a/compiler/rustc_type_ir/src/region_constraint.rs b/compiler/rustc_type_ir/src/region_constraint.rs index 9a643b538d93f..648a68b9e23c1 100644 --- a/compiler/rustc_type_ir/src/region_constraint.rs +++ b/compiler/rustc_type_ir/src/region_constraint.rs @@ -49,6 +49,7 @@ impl Default for TransitiveRelationBuilder { use crate::data_structures::IndexMap; use crate::fold::TypeSuperFoldable; use crate::inherent::*; +use crate::outlives::{Component, push_outlives_components}; use crate::relate::{Relate, RelateResult, TypeRelation, VarianceDiagInfo}; use crate::{ AliasTy, Binder, BoundRegion, BoundVar, BoundVariableKind, DebruijnIndex, InferCtxtLike, @@ -96,6 +97,11 @@ impl Assumptions { pub enum LeafRegionConstraint { Ambiguity(S), RegionOutlives(Region, Region, S), + /// A type-outlives constraint which has not yet been decomposed into its constituent parts. + /// + /// The minimal coroutine mode keeps these intact until region checking so that enabling the + /// mode does not strengthen the eager leak check. + TypeOutlives(I::Ty, Region, S), /// Requirement that a (potentially higher ranked) alias outlives some (potentially higher ranked) /// region due to an assumption in the environment. This cannot be satisfied via component outlives /// or item bounds. @@ -128,6 +134,7 @@ impl LeafRegionConstraint { match self { Ambiguity(()) => Ambiguity(span), RegionOutlives(r1, r2, ()) => RegionOutlives(r1, r2, span), + TypeOutlives(ty, r, ()) => TypeOutlives(ty, r, span), AliasTyOutlivesViaEnv(bound_outlives, ()) => { AliasTyOutlivesViaEnv(bound_outlives, span) } @@ -143,6 +150,7 @@ impl LeafRegionC match self { Ambiguity(_) => Ambiguity(()), RegionOutlives(r1, r2, _) => RegionOutlives(r1, r2, ()), + TypeOutlives(ty, r, _) => TypeOutlives(ty, r, ()), AliasTyOutlivesViaEnv(bound_outlives, _) => AliasTyOutlivesViaEnv(bound_outlives, ()), PlaceholderTyOutlives(ty, r, _) => PlaceholderTyOutlives(ty, r, ()), } @@ -153,6 +161,7 @@ impl LeafRegionC let (Ambiguity(s) | RegionOutlives(_, _, s) + | TypeOutlives(_, _, s) | AliasTyOutlivesViaEnv(_, s) | PlaceholderTyOutlives(_, _, s)) = self; s.clone() @@ -426,7 +435,7 @@ impl LeafRegionConstraint { } /// Takes any constraints involving placeholders from the current universe and eagerly checks them. -/// This can be done a few ways: +/// Full assumptions-on-binders mode can do this a few ways: /// - There's an assumption on the binder introducing the placeholder which means the constraint is satisfied (true) /// - There's assumptions on the binder introducing the placeholder which allow us to rewrite the constraint in /// terms of lower universe variables. For example given `for<'a> where('b: 'a) { prove(T: '!a_u1) }` we can @@ -438,6 +447,10 @@ impl LeafRegionConstraint { /// propagating true/false/ambiguity as close to the root of the constraint as we can. The returned constraint should /// be checked for whether it is true/false/ambiguous as that should affect the result of whatever operation required /// entering the binder corresponding to `u`. +/// +/// For universes with explicit assumptions, minimal coroutine mode only removes constraints +/// directly implied by them. It leaves every other constraint unchanged so it can be checked in +/// the root inference context. Universes without assumptions use the ordinary eager leak check. #[instrument(level = "debug", skip(infcx), ret)] pub fn eagerly_handle_placeholders_in_universe, I: Interner>( infcx: &Infcx, @@ -446,6 +459,12 @@ pub fn eagerly_handle_placeholders_in_universe RegionConstraint { let assumptions = infcx.get_placeholder_assumptions(u); + if infcx.cx().assumptions_on_binders_min_coroutines() + && let Some(assumptions) = assumptions.as_ref() + { + return drop_constraints_satisfied_by_assumptions(infcx, constraint, u, assumptions); + } + // 1. rewrite type outlives constraints involving things from `u` into either region constraints // involving things from `u` or type outlives constraints not involving things from `u` // @@ -470,6 +489,56 @@ pub fn eagerly_handle_placeholders_in_universe, I: Interner>( + infcx: &Infcx, + constraint: RegionConstraint, + u: UniverseIndex, + assumptions: &Assumptions, +) -> RegionConstraint { + use LeafRegionConstraint::*; + + let region_outlives = |r1, r2| regions_outlived_by(r1, assumptions).any(|r| r == r2); + let type_outlives = |ty, r| { + assumptions.type_outlives.iter().any(|assumption| { + let Some(OutlivesClause(assumed_ty, assumed_r)) = assumption.no_bound_vars() else { + return false; + }; + assumed_ty == ty && region_outlives(assumed_r, r) + }) + }; + let is_satisfied = |constraint: &LeafRegionConstraint| { + // Constraints retained while leaving an inner universe may still mention that universe. + // The assumptions for `u` cannot be used to discharge those constraints. + if max_universe(infcx, constraint.clone()) != u { + return false; + } + + match constraint { + RegionOutlives(r1, r2, ()) => region_outlives(*r1, *r2), + TypeOutlives(ty, r, ()) | PlaceholderTyOutlives(ty, r, ()) => type_outlives(*ty, *r), + Ambiguity(()) | AliasTyOutlivesViaEnv(..) => false, + } + }; + + let has_satisfied_constraint = constraint + .and_constraint + .0 + .iter() + .chain(constraint.or_constraint.0.iter().flat_map(|and| and.0.iter())) + .any(is_satisfied); + if !has_satisfied_constraint { + return constraint; + } + + let filter_and = |and: And| And::new(and.0.into_iter().filter(|c| !is_satisfied(c))); + let and_constraint = filter_and(constraint.and_constraint); + let or_ands: Vec<_> = constraint.or_constraint.0.into_iter().map(filter_and).collect(); + let or_constraint = + if or_ands.iter().any(|and| and.0.is_empty()) { Or::new_true() } else { Or::new(or_ands) }; + + RegionConstraint::new_from_or(Or::build_and(Or::new([and_constraint]), or_constraint)) +} + /// Filter our region constraints to not include constraints between region variables from `u` and /// other regions as those are always satisfied. This requires some care to handle correctly for example: /// `'!a_u1: '?x_u1: '!b_u1` should result in us requiring `'!a_u1: '!b_u1` rather than dropping the two @@ -492,9 +561,10 @@ fn compute_new_region_constraints, I: Interne and: &And| { for c in &and.0 { match c { - Ambiguity(()) | PlaceholderTyOutlives(..) | AliasTyOutlivesViaEnv(..) => { - constraints.push(c.clone()) - } + Ambiguity(()) + | TypeOutlives(..) + | PlaceholderTyOutlives(..) + | AliasTyOutlivesViaEnv(..) => constraints.push(c.clone()), RegionOutlives(r1, r2, ()) => { regions.insert(*r1); regions.insert(*r2); @@ -640,7 +710,10 @@ fn pull_region_outlives_constraints_out_of_universe< let mut pulled_constraints = Vec::new(); for c in and.0 { match c { - Ambiguity(()) | PlaceholderTyOutlives(..) | AliasTyOutlivesViaEnv(..) => { + Ambiguity(()) + | TypeOutlives(..) + | PlaceholderTyOutlives(..) + | AliasTyOutlivesViaEnv(..) => { assert!(max_universe(infcx, c.clone()) < u); pulled_constraints.push(Or::new_leaf(c.clone())); } @@ -696,10 +769,83 @@ fn pull_region_outlives_constraints_out_of_universe< RegionConstraint::new_from_or(Or::build_and(and_constraint, or_constraint)) } -/// Converts type outlives constraints into region outlives constraints. This assumes the *complete* set of -/// assumptions are known. This should not be called until the end of type checking. -/// -/// The returned region constraint will not have *any* PlaceholderTyOutlives or AliasTyOutlivesViaEnv constraints. +/// Converts a type-outlives constraint into constraints for the components of the type. +#[instrument(level = "debug", skip(cx), ret)] +pub fn destructure_type_outlives( + cx: I, + ty: I::Ty, + r: Region, + span: S, +) -> Or +where + S: Clone + std::fmt::Debug + Eq + std::hash::Hash, +{ + let mut components = Default::default(); + push_outlives_components(cx, ty, &mut components); + destructure_type_outlives_components(cx, &components, r, span) +} + +fn destructure_type_outlives_components( + cx: I, + components: &[Component], + r: Region, + span: S, +) -> Or +where + S: Clone + std::fmt::Debug + Eq + std::hash::Hash, +{ + components.into_iter().fold(Or::new_true(), |acc, component| { + Or::build_and(acc, destructure_type_outlives_component(cx, component, r, span.clone())) + }) +} + +fn destructure_type_outlives_component( + cx: I, + component: &Component, + r: Region, + span: S, +) -> Or +where + S: Clone + std::fmt::Debug + Eq + std::hash::Hash, +{ + use LeafRegionConstraint::*; + + match component { + Component::Region(component_r) => Or::new_leaf(RegionOutlives(*component_r, r, span)), + Component::Param(param) => { + Or::new_leaf(PlaceholderTyOutlives(Ty::new_param(cx, *param), r, span)) + } + Component::Placeholder(placeholder) => { + Or::new_leaf(PlaceholderTyOutlives(Ty::new_placeholder(cx, *placeholder), r, span)) + } + Component::Alias(_, alias) => { + let item_bound_outlives = Or::new( + crate::outlives::declared_bounds_from_definition(cx, *alias) + .map(|bound| And::new([RegionOutlives(bound, r, span.clone())])), + ); + let where_clause_outlives = + Or::new_leaf(AliasTyOutlivesViaEnv(Binder::dummy((*alias, r)), span.clone())); + + let mut components = Default::default(); + crate::outlives::compute_alias_components_recursive(cx, *alias, &mut components); + let components_outlives = + destructure_type_outlives_components(cx, &components, r, span); + + Or::build_or( + Or::build_or(item_bound_outlives, where_clause_outlives), + components_outlives, + ) + } + Component::UnresolvedInferenceVariable(_) => Or::new_ambig(span), + Component::EscapingAlias(components) => { + destructure_type_outlives_components(cx, components, r, span) + } + } +} + +/// Converts all type-outlives constraints at the end of type checking, once the complete set of +/// assumptions is known. The returned constraint has no `TypeOutlives`, +/// `PlaceholderTyOutlives`, or `AliasTyOutlivesViaEnv` leaves. #[instrument(level = "debug", skip(infcx), ret)] pub fn destructure_type_outlives_constraints_in_root< Infcx: InferCtxtLike, @@ -720,6 +866,22 @@ pub fn destructure_type_outlives_constraints_in_root< Ambiguity(_) | RegionOutlives(..) => { destructured_constraints.push(Or::new_leaf(c.clone())) } + TypeOutlives(ty, r, span) => { + let constraint = RegionConstraint::new_from_or(destructure_type_outlives( + infcx.cx(), + *ty, + *r, + span.clone(), + )); + destructured_constraints.push( + destructure_type_outlives_constraints_in_root( + infcx, + constraint, + assumptions, + ) + .splatted_and_constraints(), + ); + } PlaceholderTyOutlives(ty, r, span) => destructured_constraints.push(Or::new( regions_outlived_by_placeholder(*ty, assumptions, infcx.cx()).map( move |assumption_r| { @@ -791,6 +953,23 @@ fn rewrite_type_outlives_constraints_in_universe_for_eager_placeholder_handling< for c in and.0 { match c { Ambiguity(()) | RegionOutlives(..) => rewritten_constraints.push(Or::new_leaf(c)), + TypeOutlives(ty, region, ()) => { + let constraint = RegionConstraint::new_from_or(destructure_type_outlives( + infcx.cx(), + ty, + region, + (), + )); + rewritten_constraints.push( + rewrite_type_outlives_constraints_in_universe_for_eager_placeholder_handling( + infcx, + constraint, + u, + assumptions, + ) + .splatted_and_constraints(), + ); + } PlaceholderTyOutlives(ty, region, ()) => { rewritten_constraints.push(rewrite_placeholder_ty_outlives_constraints_in_universe_for_eager_placeholder_handling(infcx, ty, region, u, assumptions)); } @@ -1175,14 +1354,20 @@ impl<'a, Infcx: InferCtxtLike, I: Interner> TypeRelation { self.infcx.enter_forall_with_empty_assumptions(a, |a| { let u = self.infcx.universe(); - self.infcx.insert_placeholder_assumptions(u, Some(Assumptions::empty())); + self.infcx.insert_placeholder_assumptions( + u, + (!self.cx().assumptions_on_binders_min_coroutines()).then(Assumptions::empty), + ); let b = self.infcx.instantiate_binder_with_infer(b); self.relate(a, b) })?; self.infcx.enter_forall_with_empty_assumptions(b, |b| { let u = self.infcx.universe(); - self.infcx.insert_placeholder_assumptions(u, Some(Assumptions::empty())); + self.infcx.insert_placeholder_assumptions( + u, + (!self.cx().assumptions_on_binders_min_coroutines()).then(Assumptions::empty), + ); let a = self.infcx.instantiate_binder_with_infer(a); self.relate(a, b) })?; From 9ba3c794a8372c00327a8d93ddbd719a491d919a Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Tue, 1 Sep 2026 18:14:13 -0300 Subject: [PATCH 3/6] Add minimal coroutine binder regressions --- .../min-coroutines-only-witness-binders.rs | 26 ++++++++++++++++++ ...min-coroutines-only-witness-binders.stderr | 27 +++++++++++++++++++ ...outines-retains-unsatisfied-constraints.rs | 19 +++++++++++++ ...nes-retains-unsatisfied-constraints.stderr | 7 +++++ .../test-infra-works.rs | 16 ++++++++++- ...-ranked-auto-trait-1.no_assumptions.stderr | 12 ++++----- .../async-await/higher-ranked-auto-trait-1.rs | 4 ++- ...er-ranked-auto-trait-10.assumptions.stderr | 4 +-- ...ranked-auto-trait-10.no_assumptions.stderr | 4 +-- .../higher-ranked-auto-trait-10.rs | 4 ++- ...-ranked-auto-trait-5.no_assumptions.stderr | 2 +- .../async-await/higher-ranked-auto-trait-5.rs | 4 ++- ...-ranked-auto-trait-8.no_assumptions.stderr | 2 +- .../async-await/higher-ranked-auto-trait-8.rs | 4 ++- 14 files changed, 118 insertions(+), 17 deletions(-) create mode 100644 tests/ui/assumptions_on_binders/min-coroutines-only-witness-binders.rs create mode 100644 tests/ui/assumptions_on_binders/min-coroutines-only-witness-binders.stderr create mode 100644 tests/ui/assumptions_on_binders/min-coroutines-retains-unsatisfied-constraints.rs create mode 100644 tests/ui/assumptions_on_binders/min-coroutines-retains-unsatisfied-constraints.stderr diff --git a/tests/ui/assumptions_on_binders/min-coroutines-only-witness-binders.rs b/tests/ui/assumptions_on_binders/min-coroutines-only-witness-binders.rs new file mode 100644 index 0000000000000..706dd8db35ff1 --- /dev/null +++ b/tests/ui/assumptions_on_binders/min-coroutines-only-witness-binders.rs @@ -0,0 +1,26 @@ +//@ compile-flags: -Zassumptions-on-binders=min_coroutines + +use std::marker::PhantomData; + +struct WellFormed<'a, T: 'a>(PhantomData<&'a T>); + +trait Trait {} + +impl<'a, 'b> Trait for WellFormed<'a, &'b ()> +where + &'b (): 'a, +{ +} + +fn require() +where + for<'a, 'b> WellFormed<'a, &'b ()>: Trait, +{ +} + +fn check() { + require(); + //~^ ERROR type annotations needed: cannot satisfy +} + +fn main() {} diff --git a/tests/ui/assumptions_on_binders/min-coroutines-only-witness-binders.stderr b/tests/ui/assumptions_on_binders/min-coroutines-only-witness-binders.stderr new file mode 100644 index 0000000000000..98abf4674a9a1 --- /dev/null +++ b/tests/ui/assumptions_on_binders/min-coroutines-only-witness-binders.stderr @@ -0,0 +1,27 @@ +error[E0283]: type annotations needed: cannot satisfy `for<'a, 'b> WellFormed<'a, &'b ()>: Trait` + --> $DIR/min-coroutines-only-witness-binders.rs:22:5 + | +LL | require(); + | ^^^^^^^^^ + | + = note: cannot satisfy `for<'a, 'b> WellFormed<'a, &'b ()>: Trait` +help: the trait `Trait` is not implemented for `WellFormed<'a, &'b ()>` + but it is implemented for `WellFormed<'_, &()>` + --> $DIR/min-coroutines-only-witness-binders.rs:9:1 + | +LL | / impl<'a, 'b> Trait for WellFormed<'a, &'b ()> +LL | | where +LL | | &'b (): 'a, + | |_______________^ +note: required by a bound in `require` + --> $DIR/min-coroutines-only-witness-binders.rs:17:41 + | +LL | fn require() + | ------- required by a bound in this function +LL | where +LL | for<'a, 'b> WellFormed<'a, &'b ()>: Trait, + | ^^^^^ required by this bound in `require` + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0283`. diff --git a/tests/ui/assumptions_on_binders/min-coroutines-retains-unsatisfied-constraints.rs b/tests/ui/assumptions_on_binders/min-coroutines-retains-unsatisfied-constraints.rs new file mode 100644 index 0000000000000..9f8fc1c1b3814 --- /dev/null +++ b/tests/ui/assumptions_on_binders/min-coroutines-retains-unsatisfied-constraints.rs @@ -0,0 +1,19 @@ +//@ compile-flags: -Zassumptions-on-binders=min_coroutines +//@ normalize-stderr: "\n\n$" -> "\n" + +#![feature(test_binder_constraints)] +#![allow(internal_features)] + +core::test_binder_constraints! { + impl<'a, 'b> { + forall<'w> where 'b: 'w { + //~^ ERROR higher-ranked lifetime bound could not be satisfied + 'b: 'w, + 'a: 'b, + } expect { + 'a: 'b, + } + } +} + +fn main() {} diff --git a/tests/ui/assumptions_on_binders/min-coroutines-retains-unsatisfied-constraints.stderr b/tests/ui/assumptions_on_binders/min-coroutines-retains-unsatisfied-constraints.stderr new file mode 100644 index 0000000000000..b0c8cb3f8a743 --- /dev/null +++ b/tests/ui/assumptions_on_binders/min-coroutines-retains-unsatisfied-constraints.stderr @@ -0,0 +1,7 @@ +error: higher-ranked lifetime bound could not be satisfied + --> $DIR/min-coroutines-retains-unsatisfied-constraints.rs:9:9 + | +LL | forall<'w> where 'b: 'w { + | ^^^^^^ + +error: aborting due to 1 previous error diff --git a/tests/ui/assumptions_on_binders/test-infra-works.rs b/tests/ui/assumptions_on_binders/test-infra-works.rs index d8d64d1aac255..d9f4ed2179aff 100644 --- a/tests/ui/assumptions_on_binders/test-infra-works.rs +++ b/tests/ui/assumptions_on_binders/test-infra-works.rs @@ -1,5 +1,7 @@ //@ check-pass -//@ compile-flags: -Zassumptions-on-binders +//@ revisions: assumptions min_coroutines +//@[assumptions] compile-flags: -Zassumptions-on-binders +//@[min_coroutines] compile-flags: -Zassumptions-on-binders=min_coroutines #![feature(test_binder_constraints, non_lifetime_binders)] #![expect(incomplete_features)] @@ -19,6 +21,7 @@ core::test_binder_constraints! { // FIXME(-Zassumptions-on-binders): this should be `impl<'b, 'c: 'b>`, not // `impl<'b, 'c: 'b + 'static>`, but OR isn't actually implemented yet +#[cfg(assumptions)] core::test_binder_constraints! { impl<'b, 'c: 'b + 'static> { forall<'a> where 'b: 'a { @@ -82,4 +85,15 @@ core::test_binder_constraints! { } } +#[cfg(min_coroutines)] +core::test_binder_constraints! { + impl { + // Minimal mode directly discharges constraints proven by the current binder without + // rewriting either placeholder into a lower universe. + forall<'a, 'b> where 'b: 'a { + 'b: 'a, + } expect {} + } +} + fn main() {} diff --git a/tests/ui/async-await/higher-ranked-auto-trait-1.no_assumptions.stderr b/tests/ui/async-await/higher-ranked-auto-trait-1.no_assumptions.stderr index b298a3bf2153a..9b03bda6b5e53 100644 --- a/tests/ui/async-await/higher-ranked-auto-trait-1.no_assumptions.stderr +++ b/tests/ui/async-await/higher-ranked-auto-trait-1.no_assumptions.stderr @@ -1,5 +1,5 @@ error[E0308]: mismatched types - --> $DIR/higher-ranked-auto-trait-1.rs:37:5 + --> $DIR/higher-ranked-auto-trait-1.rs:39:5 | LL | / async { LL | | let _y = &(); @@ -10,13 +10,13 @@ LL | | drop(_x); LL | | } | |_____^ one type is more general than the other | - = note: expected `async` block `{async block@$DIR/higher-ranked-auto-trait-1.rs:40:19: 40:29}` - found `async` block `{async block@$DIR/higher-ranked-auto-trait-1.rs:40:19: 40:29}` + = note: expected `async` block `{async block@$DIR/higher-ranked-auto-trait-1.rs:42:19: 42:29}` + found `async` block `{async block@$DIR/higher-ranked-auto-trait-1.rs:42:19: 42:29}` = note: no two async blocks, even if identical, have the same type = help: consider pinning your async block and casting it to a trait object error[E0308]: mismatched types - --> $DIR/higher-ranked-auto-trait-1.rs:37:5 + --> $DIR/higher-ranked-auto-trait-1.rs:39:5 | LL | / async { LL | | let _y = &(); @@ -27,8 +27,8 @@ LL | | drop(_x); LL | | } | |_____^ one type is more general than the other | - = note: expected `async` block `{async block@$DIR/higher-ranked-auto-trait-1.rs:40:19: 40:29}` - found `async` block `{async block@$DIR/higher-ranked-auto-trait-1.rs:40:19: 40:29}` + = note: expected `async` block `{async block@$DIR/higher-ranked-auto-trait-1.rs:42:19: 42:29}` + found `async` block `{async block@$DIR/higher-ranked-auto-trait-1.rs:42:19: 42:29}` = note: no two async blocks, even if identical, have the same type = help: consider pinning your async block and casting it to a trait object = note: duplicate diagnostic emitted due to `-Z deduplicate-diagnostics=no` diff --git a/tests/ui/async-await/higher-ranked-auto-trait-1.rs b/tests/ui/async-await/higher-ranked-auto-trait-1.rs index 740f7e2924545..7e714fc9cac18 100644 --- a/tests/ui/async-await/higher-ranked-auto-trait-1.rs +++ b/tests/ui/async-await/higher-ranked-auto-trait-1.rs @@ -1,8 +1,10 @@ // Repro for . //@ edition: 2021 -//@ revisions: assumptions no_assumptions +//@ revisions: assumptions min_coroutines no_assumptions //@[assumptions] compile-flags: -Zhigher-ranked-assumptions //@[assumptions] check-pass +//@[min_coroutines] compile-flags: -Zassumptions-on-binders=min_coroutines +//@[min_coroutines] check-pass //@[no_assumptions] known-bug: #110338 use std::future::Future; diff --git a/tests/ui/async-await/higher-ranked-auto-trait-10.assumptions.stderr b/tests/ui/async-await/higher-ranked-auto-trait-10.assumptions.stderr index 6fcf1b1eac176..e2990ad69f2c9 100644 --- a/tests/ui/async-await/higher-ranked-auto-trait-10.assumptions.stderr +++ b/tests/ui/async-await/higher-ranked-auto-trait-10.assumptions.stderr @@ -1,5 +1,5 @@ error: implementation of `Foo` is not general enough - --> $DIR/higher-ranked-auto-trait-10.rs:32:5 + --> $DIR/higher-ranked-auto-trait-10.rs:34:5 | LL | Box::new(async move { get_foo(x).await }) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ implementation of `Foo` is not general enough @@ -8,7 +8,7 @@ LL | Box::new(async move { get_foo(x).await }) = note: ...but `Foo<'2>` is actually implemented for the type `&'2 str`, for some specific lifetime `'2` error: implementation of `Foo` is not general enough - --> $DIR/higher-ranked-auto-trait-10.rs:32:5 + --> $DIR/higher-ranked-auto-trait-10.rs:34:5 | LL | Box::new(async move { get_foo(x).await }) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ implementation of `Foo` is not general enough diff --git a/tests/ui/async-await/higher-ranked-auto-trait-10.no_assumptions.stderr b/tests/ui/async-await/higher-ranked-auto-trait-10.no_assumptions.stderr index 6fcf1b1eac176..e2990ad69f2c9 100644 --- a/tests/ui/async-await/higher-ranked-auto-trait-10.no_assumptions.stderr +++ b/tests/ui/async-await/higher-ranked-auto-trait-10.no_assumptions.stderr @@ -1,5 +1,5 @@ error: implementation of `Foo` is not general enough - --> $DIR/higher-ranked-auto-trait-10.rs:32:5 + --> $DIR/higher-ranked-auto-trait-10.rs:34:5 | LL | Box::new(async move { get_foo(x).await }) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ implementation of `Foo` is not general enough @@ -8,7 +8,7 @@ LL | Box::new(async move { get_foo(x).await }) = note: ...but `Foo<'2>` is actually implemented for the type `&'2 str`, for some specific lifetime `'2` error: implementation of `Foo` is not general enough - --> $DIR/higher-ranked-auto-trait-10.rs:32:5 + --> $DIR/higher-ranked-auto-trait-10.rs:34:5 | LL | Box::new(async move { get_foo(x).await }) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ implementation of `Foo` is not general enough diff --git a/tests/ui/async-await/higher-ranked-auto-trait-10.rs b/tests/ui/async-await/higher-ranked-auto-trait-10.rs index 4bfa27961abd0..e49aeff5c3394 100644 --- a/tests/ui/async-await/higher-ranked-auto-trait-10.rs +++ b/tests/ui/async-await/higher-ranked-auto-trait-10.rs @@ -1,8 +1,10 @@ // Repro for . //@ edition: 2021 -//@ revisions: assumptions no_assumptions +//@ revisions: assumptions min_coroutines no_assumptions //@[assumptions] compile-flags: -Zhigher-ranked-assumptions //@[assumptions] known-bug: unknown +//@[min_coroutines] compile-flags: -Zassumptions-on-binders=min_coroutines +//@[min_coroutines] check-pass //@[no_assumptions] known-bug: #110338 use std::any::Any; diff --git a/tests/ui/async-await/higher-ranked-auto-trait-5.no_assumptions.stderr b/tests/ui/async-await/higher-ranked-auto-trait-5.no_assumptions.stderr index 8fa3c7483c89d..98d37a55b4a15 100644 --- a/tests/ui/async-await/higher-ranked-auto-trait-5.no_assumptions.stderr +++ b/tests/ui/async-await/higher-ranked-auto-trait-5.no_assumptions.stderr @@ -1,5 +1,5 @@ error: implementation of `Send` is not general enough - --> $DIR/higher-ranked-auto-trait-5.rs:13:5 + --> $DIR/higher-ranked-auto-trait-5.rs:15:5 | LL | / assert_send(async { LL | | call_me.call().await; diff --git a/tests/ui/async-await/higher-ranked-auto-trait-5.rs b/tests/ui/async-await/higher-ranked-auto-trait-5.rs index 9a8b3f4357c05..ef21c15334531 100644 --- a/tests/ui/async-await/higher-ranked-auto-trait-5.rs +++ b/tests/ui/async-await/higher-ranked-auto-trait-5.rs @@ -1,8 +1,10 @@ // Repro for . //@ edition: 2021 -//@ revisions: assumptions no_assumptions +//@ revisions: assumptions min_coroutines no_assumptions //@[assumptions] compile-flags: -Zhigher-ranked-assumptions //@[assumptions] check-pass +//@[min_coroutines] compile-flags: -Zassumptions-on-binders=min_coroutines +//@[min_coroutines] check-pass //@[no_assumptions] known-bug: #110338 use std::future::Future; diff --git a/tests/ui/async-await/higher-ranked-auto-trait-8.no_assumptions.stderr b/tests/ui/async-await/higher-ranked-auto-trait-8.no_assumptions.stderr index 6208675117b74..ea9a622dfaf7c 100644 --- a/tests/ui/async-await/higher-ranked-auto-trait-8.no_assumptions.stderr +++ b/tests/ui/async-await/higher-ranked-auto-trait-8.no_assumptions.stderr @@ -1,5 +1,5 @@ error: higher-ranked lifetime error - --> $DIR/higher-ranked-auto-trait-8.rs:26:5 + --> $DIR/higher-ranked-auto-trait-8.rs:28:5 | LL | needs_send(use_my_struct(second_struct)); // ERROR | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/ui/async-await/higher-ranked-auto-trait-8.rs b/tests/ui/async-await/higher-ranked-auto-trait-8.rs index 91cef204e44b9..4546e3990fcc9 100644 --- a/tests/ui/async-await/higher-ranked-auto-trait-8.rs +++ b/tests/ui/async-await/higher-ranked-auto-trait-8.rs @@ -1,8 +1,10 @@ // Repro for . //@ edition: 2021 -//@ revisions: assumptions no_assumptions +//@ revisions: assumptions min_coroutines no_assumptions //@[assumptions] compile-flags: -Zhigher-ranked-assumptions //@[assumptions] check-pass +//@[min_coroutines] compile-flags: -Zassumptions-on-binders=min_coroutines +//@[min_coroutines] check-pass //@[no_assumptions] known-bug: #110338 fn needs_send(_val: T) {} From 91b9d1257bafcfcbb7ef62b335bd644eefa5d0f3 Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Sat, 5 Sep 2026 14:23:53 -0300 Subject: [PATCH 4/6] Name the assumptions-on-binders accessors by mode The predicate was called `assumptions_on_binders`, which read as "the flag is on" and gave no way to ask which mode is active. Callers that gate the shared machinery want "any mode", while callers that gate the eager placeholder rewriting want "the full mode" specifically. Split it into `any_is_enabled`, `is_full` and `is_min_coroutines` on the option, and expose all three through `TyCtxt` and `Interner`. --- compiler/rustc_borrowck/src/type_check/mod.rs | 2 +- .../rustc_infer/src/infer/outlives/obligations.rs | 6 +++--- compiler/rustc_middle/src/ty/context.rs | 14 ++++++++++++-- .../rustc_middle/src/ty/context/impl_interner.rs | 8 ++++++-- .../rustc_next_trait_solver/src/canonical/mod.rs | 2 +- .../rustc_next_trait_solver/src/placeholder.rs | 2 +- .../src/solve/eval_ctxt/mod.rs | 10 +++++----- .../solve/eval_ctxt/solver_region_constraints.rs | 2 +- compiler/rustc_next_trait_solver/src/solve/mod.rs | 4 ++-- compiler/rustc_session/src/config.rs | 11 +++++++++-- .../rustc_trait_selection/src/solve/delegate.rs | 2 +- .../src/solve/fulfill/derive_errors.rs | 2 +- compiler/rustc_type_ir/src/interner.rs | 7 ++++++- compiler/rustc_type_ir/src/solve/mod.rs | 2 +- 14 files changed, 50 insertions(+), 24 deletions(-) diff --git a/compiler/rustc_borrowck/src/type_check/mod.rs b/compiler/rustc_borrowck/src/type_check/mod.rs index 9f23a0d5ab631..4599c3b8e9fcf 100644 --- a/compiler/rustc_borrowck/src/type_check/mod.rs +++ b/compiler/rustc_borrowck/src/type_check/mod.rs @@ -173,7 +173,7 @@ pub(crate) fn type_check<'tcx>( let polonius_context = typeck.polonius_context; - if infcx.tcx.assumptions_on_binders() { + if infcx.tcx.assumptions_on_binders_any() { let mut converter = constraint_conversion::ConstraintConversion::new( typeck.infcx, typeck.universal_regions, diff --git a/compiler/rustc_infer/src/infer/outlives/obligations.rs b/compiler/rustc_infer/src/infer/outlives/obligations.rs index ad87eb4b8ce0a..b52bd86624679 100644 --- a/compiler/rustc_infer/src/infer/outlives/obligations.rs +++ b/compiler/rustc_infer/src/infer/outlives/obligations.rs @@ -163,7 +163,7 @@ impl<'tcx> InferCtxt<'tcx> { sub_region: Region<'tcx>, cause: &ObligationCause<'tcx>, ) { - assert!(!self.tcx.assumptions_on_binders()); + assert!(!self.tcx.assumptions_on_binders_any()); // `is_global` means the type has no params, infer, placeholder, or non-`'static` // free regions. If the type has none of these things, then we can skip registering @@ -261,7 +261,7 @@ impl<'tcx> InferCtxt<'tcx> { assumptions: rustc_type_ir::region_constraint::Assumptions>, mut conversion: impl TypeOutlivesDelegate<'tcx>, ) { - assert!(self.tcx.assumptions_on_binders()); + assert!(self.tcx.assumptions_on_binders_any()); assert!(self.next_trait_solver()); let constraint = self.inner.borrow().solver_region_constraint_storage.get_constraint(); @@ -326,7 +326,7 @@ impl<'tcx> InferCtxt<'tcx> { ) { assert!(!self.in_snapshot(), "cannot process registered region obligations in a snapshot"); - if self.tcx.assumptions_on_binders() { + if self.tcx.assumptions_on_binders_any() { self.destructure_solver_region_constraints_for_regionck(outlives_env); } diff --git a/compiler/rustc_middle/src/ty/context.rs b/compiler/rustc_middle/src/ty/context.rs index 440ab0dcc2a90..75622a8284d96 100644 --- a/compiler/rustc_middle/src/ty/context.rs +++ b/compiler/rustc_middle/src/ty/context.rs @@ -2827,10 +2827,20 @@ impl<'tcx> TyCtxt<'tcx> { || self.sess.opts.unstable_opts.typing_mode_post_typeck_until_borrowck } - pub fn assumptions_on_binders(self) -> bool { - self.sess.opts.unstable_opts.assumptions_on_binders.is_enabled() + /// Whether any `-Zassumptions-on-binders` mode is enabled. Use this to gate + /// the shared machinery, e.g. tracking region constraints in the solver. + pub fn assumptions_on_binders_any(self) -> bool { + self.sess.opts.unstable_opts.assumptions_on_binders.any_is_enabled() } + /// Whether the full `-Zassumptions-on-binders` mode is enabled, deducing + /// assumptions from every binder. + pub fn assumptions_on_binders_full(self) -> bool { + self.sess.opts.unstable_opts.assumptions_on_binders.is_full() + } + + /// Whether `-Zassumptions-on-binders=min_coroutines` is enabled, deducing + /// assumptions only from coroutine-witness binders. pub fn assumptions_on_binders_min_coroutines(self) -> bool { self.sess.opts.unstable_opts.assumptions_on_binders.is_min_coroutines() } diff --git a/compiler/rustc_middle/src/ty/context/impl_interner.rs b/compiler/rustc_middle/src/ty/context/impl_interner.rs index 9bba7eeed366e..56d16d9e75043 100644 --- a/compiler/rustc_middle/src/ty/context/impl_interner.rs +++ b/compiler/rustc_middle/src/ty/context/impl_interner.rs @@ -379,8 +379,12 @@ impl<'tcx> Interner for TyCtxt<'tcx> { self.features() } - fn assumptions_on_binders(self) -> bool { - self.assumptions_on_binders() + fn assumptions_on_binders_any(self) -> bool { + self.assumptions_on_binders_any() + } + + fn assumptions_on_binders_full(self) -> bool { + self.assumptions_on_binders_full() } fn assumptions_on_binders_min_coroutines(self) -> bool { diff --git a/compiler/rustc_next_trait_solver/src/canonical/mod.rs b/compiler/rustc_next_trait_solver/src/canonical/mod.rs index 804d3ad125bae..2c8c784be76a4 100644 --- a/compiler/rustc_next_trait_solver/src/canonical/mod.rs +++ b/compiler/rustc_next_trait_solver/src/canonical/mod.rs @@ -181,7 +181,7 @@ where I: Interner, { let new_universe = delegate.create_next_universe(); - if delegate.cx().assumptions_on_binders() { + if delegate.cx().assumptions_on_binders_any() { // FIXME(-Zassumptions-on-binders): Remove this temporary workaround once opaque types no // longer escape query responses with query-created placeholders. Region constraints // involving query-created placeholders were handled inside the query, but placeholders can diff --git a/compiler/rustc_next_trait_solver/src/placeholder.rs b/compiler/rustc_next_trait_solver/src/placeholder.rs index 83b2eb6ac6295..94a34208657fb 100644 --- a/compiler/rustc_next_trait_solver/src/placeholder.rs +++ b/compiler/rustc_next_trait_solver/src/placeholder.rs @@ -67,7 +67,7 @@ where current_index: _, } = replacer; - if infcx.cx().assumptions_on_binders() { + if infcx.cx().assumptions_on_binders_any() { for (old, new) in old_universes.into_iter().zip(universe_indices.iter()) { if let (None, Some(new)) = (old, new) { // FIXME(-Zassumptions-on-binders): `replace_bound_vars` does not have enough diff --git a/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/mod.rs b/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/mod.rs index 1fbe7773b4716..ce05d5e52f815 100644 --- a/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/mod.rs +++ b/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/mod.rs @@ -1306,9 +1306,9 @@ where ) -> U { self.delegate.enter_forall_without_assumptions(value, |value| { let u = self.delegate.universe(); - let assumptions = if self.cx().assumptions_on_binders() - && (!self.cx().assumptions_on_binders_min_coroutines() - || binder_kind == ForallBinderKind::CoroutineWitness) + let assumptions = if self.cx().assumptions_on_binders_full() + || (self.cx().assumptions_on_binders_min_coroutines() + && binder_kind == ForallBinderKind::CoroutineWitness) { self.region_assumptions_for_placeholders_in_universe(value.clone(), u, param_env) } else { @@ -1553,7 +1553,7 @@ where previous call to `try_evaluate_added_goals!`" ); - let goals_certainty = match self.delegate.cx().assumptions_on_binders() { + let goals_certainty = match self.delegate.cx().assumptions_on_binders_any() { true => { let certainty = self.eagerly_handle_placeholders()?; certainty.and(goals_certainty) @@ -1682,7 +1682,7 @@ where // region constraints from an ambiguous nested goal. This is tested in both // `tests/ui/higher-ranked/leak-check/leak-check-in-selection-5-ambig.rs` and // `tests/ui/higher-ranked/leak-check/leak-check-in-selection-6-ambig-unify.rs`. - let region_constraints = if self.cx().assumptions_on_binders() { + let region_constraints = if self.cx().assumptions_on_binders_any() { ExternalRegionConstraints::NextGen(if let Certainty::Yes = certainty { let constraint = self.delegate.get_solver_region_constraint(); debug_assert_eq!( diff --git a/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/solver_region_constraints.rs b/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/solver_region_constraints.rs index 48484e0fa5e79..84609fe78d53d 100644 --- a/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/solver_region_constraints.rs +++ b/compiler/rustc_next_trait_solver/src/solve/eval_ctxt/solver_region_constraints.rs @@ -34,7 +34,7 @@ where u: UniverseIndex, param_env: I::ParamEnv, ) -> Option> { - assert!(self.cx().assumptions_on_binders()); + assert!(self.cx().assumptions_on_binders_any()); struct RawAssumptions<'a, 'b, D: SolverDelegate, I: Interner> { ecx: &'a mut EvalCtxt<'b, D, I>, diff --git a/compiler/rustc_next_trait_solver/src/solve/mod.rs b/compiler/rustc_next_trait_solver/src/solve/mod.rs index bf05c30102d6c..451224a627d36 100644 --- a/compiler/rustc_next_trait_solver/src/solve/mod.rs +++ b/compiler/rustc_next_trait_solver/src/solve/mod.rs @@ -92,7 +92,7 @@ where let ty::OutlivesClause(ty, lt) = goal.predicate; let ty = self.normalize(GoalSource::Misc, goal.param_env, ty::Unnormalized::new_wip(ty))?; - if self.cx().assumptions_on_binders() { + if self.cx().assumptions_on_binders_any() { use rustc_type_ir::region_constraint::{ LeafRegionConstraint, RegionConstraint, destructure_type_outlives, }; @@ -126,7 +126,7 @@ where ) -> QueryResultOrRerunNonErased { let ty::OutlivesClause(a, b) = goal.predicate; - if self.cx().assumptions_on_binders() { + if self.cx().assumptions_on_binders_any() { use rustc_type_ir::region_constraint::{LeafRegionConstraint, RegionConstraint}; let constraint = diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs index 590f95d6015d6..ca8e05b1035da 100644 --- a/compiler/rustc_session/src/config.rs +++ b/compiler/rustc_session/src/config.rs @@ -1036,10 +1036,17 @@ pub enum AssumptionsOnBinders { } impl AssumptionsOnBinders { - pub fn is_enabled(self) -> bool { + /// Whether any kind of assumptions-on-binders handling is enabled. This is + /// `true` for both the full mode and the minimal coroutine mode. + pub fn any_is_enabled(self) -> bool { self != AssumptionsOnBinders::Disabled } + /// Whether the full mode is enabled, deducing assumptions from every binder. + pub fn is_full(self) -> bool { + self == AssumptionsOnBinders::All + } + pub fn is_min_coroutines(self) -> bool { self == AssumptionsOnBinders::MinCoroutines } @@ -2723,7 +2730,7 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M // `-Zassumptions-on-binders` requires the next trait solver globally. Normalize after // parsing so the effective config is independent of flag order and so consumers that // read `next_solver.globally` directly (e.g. feature-gate checks) see the right value. - if unstable_opts.assumptions_on_binders.is_enabled() { + if unstable_opts.assumptions_on_binders.any_is_enabled() { // `NextSolverConfig::default()` has `coherence: true`; the only way `coherence` is // false here is an explicit `-Znext-solver=no`. if !unstable_opts.next_solver.coherence { diff --git a/compiler/rustc_trait_selection/src/solve/delegate.rs b/compiler/rustc_trait_selection/src/solve/delegate.rs index 818d8e1a4e0c3..4f8c27cc213c1 100644 --- a/compiler/rustc_trait_selection/src/solve/delegate.rs +++ b/compiler/rustc_trait_selection/src/solve/delegate.rs @@ -155,7 +155,7 @@ impl<'tcx> rustc_next_trait_solver::delegate::SolverDelegate for SolverDelegate< use ComputeGoalFastPathOutcome as Outcome; // FIXME(-Zassumptions-on-binders): actually handle fast path - if self.tcx.assumptions_on_binders() { + if self.tcx.assumptions_on_binders_any() { return Outcome::NoFastPath; } diff --git a/compiler/rustc_trait_selection/src/solve/fulfill/derive_errors.rs b/compiler/rustc_trait_selection/src/solve/fulfill/derive_errors.rs index 59336396ab11c..f1e794e08c0e3 100644 --- a/compiler/rustc_trait_selection/src/solve/fulfill/derive_errors.rs +++ b/compiler/rustc_trait_selection/src/solve/fulfill/derive_errors.rs @@ -68,7 +68,7 @@ pub(super) fn fulfillment_error_for_no_solution<'tcx>( } ty::PredicateKind::Clause( ty::ClauseKind::RegionOutlives(_) | ty::ClauseKind::TypeOutlives(_), - ) if infcx.tcx.assumptions_on_binders() => FulfillmentErrorCode::Outlives, + ) if infcx.tcx.assumptions_on_binders_any() => FulfillmentErrorCode::Outlives, ty::PredicateKind::Clause(_) | ty::PredicateKind::DynCompatible(_) | ty::PredicateKind::Ambiguous => { diff --git a/compiler/rustc_type_ir/src/interner.rs b/compiler/rustc_type_ir/src/interner.rs index 2568b228a147f..71f9dd5fd7a11 100644 --- a/compiler/rustc_type_ir/src/interner.rs +++ b/compiler/rustc_type_ir/src/interner.rs @@ -353,8 +353,13 @@ pub trait Interner: type Features: Features; fn features(self) -> Self::Features; - fn assumptions_on_binders(self) -> bool; + /// Whether any `-Zassumptions-on-binders` mode is enabled. + fn assumptions_on_binders_any(self) -> bool; + /// Whether the full `-Zassumptions-on-binders` mode is enabled. + fn assumptions_on_binders_full(self) -> bool; + + /// Whether `-Zassumptions-on-binders=min_coroutines` is enabled. fn assumptions_on_binders_min_coroutines(self) -> bool; fn renormalize_rigid_aliases(self) -> bool; diff --git a/compiler/rustc_type_ir/src/solve/mod.rs b/compiler/rustc_type_ir/src/solve/mod.rs index d1a24e0054115..a273b1a187dd8 100644 --- a/compiler/rustc_type_ir/src/solve/mod.rs +++ b/compiler/rustc_type_ir/src/solve/mod.rs @@ -638,7 +638,7 @@ impl Eq for ExternalConstraintsData {} impl ExternalConstraintsData { pub fn new(cx: I) -> Self { - let region_constraints = match cx.assumptions_on_binders() { + let region_constraints = match cx.assumptions_on_binders_any() { true => ExternalRegionConstraints::NextGen(RegionConstraint::new_true()), false => ExternalRegionConstraints::Old(vec![]), }; From e038e65053d06351a0bd5b5a35a0ce56ecc1e792 Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Sat, 5 Sep 2026 14:28:02 -0300 Subject: [PATCH 5/6] Install empty placeholder assumptions only in the full mode Entering a binder with no assumptions recorded an empty assumption set whenever the minimal coroutine mode was off, which includes the case where assumptions on binders is disabled entirely. Storing `None` there instead keeps the map meaningful: a universe has an entry only when some mode actually computed one for it. --- compiler/rustc_infer/src/infer/context.rs | 4 +++- compiler/rustc_type_ir/src/region_constraint.rs | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/compiler/rustc_infer/src/infer/context.rs b/compiler/rustc_infer/src/infer/context.rs index 3a54f8baf17c2..08d7b287ab21e 100644 --- a/compiler/rustc_infer/src/infer/context.rs +++ b/compiler/rustc_infer/src/infer/context.rs @@ -190,7 +190,9 @@ impl<'tcx> rustc_type_ir::InferCtxtLike for InferCtxt<'tcx> { ) -> U { self.enter_forall(value, |value| { let u = self.universe(); - let assumptions = (!self.tcx.assumptions_on_binders_min_coroutines()) + let assumptions = self + .tcx + .assumptions_on_binders_full() .then(rustc_type_ir::region_constraint::Assumptions::empty); self.placeholder_assumptions_for_next_solver.borrow_mut().insert(u, assumptions); f(value) diff --git a/compiler/rustc_type_ir/src/region_constraint.rs b/compiler/rustc_type_ir/src/region_constraint.rs index 648a68b9e23c1..78544f7d174cf 100644 --- a/compiler/rustc_type_ir/src/region_constraint.rs +++ b/compiler/rustc_type_ir/src/region_constraint.rs @@ -1356,7 +1356,7 @@ impl<'a, Infcx: InferCtxtLike, I: Interner> TypeRelation let u = self.infcx.universe(); self.infcx.insert_placeholder_assumptions( u, - (!self.cx().assumptions_on_binders_min_coroutines()).then(Assumptions::empty), + self.cx().assumptions_on_binders_full().then(Assumptions::empty), ); let b = self.infcx.instantiate_binder_with_infer(b); self.relate(a, b) @@ -1366,7 +1366,7 @@ impl<'a, Infcx: InferCtxtLike, I: Interner> TypeRelation let u = self.infcx.universe(); self.infcx.insert_placeholder_assumptions( u, - (!self.cx().assumptions_on_binders_min_coroutines()).then(Assumptions::empty), + self.cx().assumptions_on_binders_full().then(Assumptions::empty), ); let a = self.infcx.instantiate_binder_with_infer(a); self.relate(a, b) From bed5ff582863385538b39154b3ee54d20235cdb5 Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Sat, 5 Sep 2026 14:28:16 -0300 Subject: [PATCH 6/6] Cover type outlives handling in the minimal coroutine mode The shared binder tests only exercised region outlives constraints, and the two alias cases assert on the rewrite that the full mode performs, which the minimal mode deliberately skips. Add a case where an assumption names the component while the goal names the composite. Keeping the constraint whole leaves it for the root, and destructuring it would reduce it to the component and discharge it, so the two representations disagree. Also record that an assumption naming an alias exactly fails to discharge it, because assumptions are lowered without normalization and so compare unequal to the normalized goal. --- .../min-coroutines-alias-outlives.rs | 47 ++++++++++++++ .../min-coroutines-alias-outlives.stderr | 61 +++++++++++++++++++ .../test-infra-works.rs | 21 +++++++ 3 files changed, 129 insertions(+) create mode 100644 tests/ui/assumptions_on_binders/min-coroutines-alias-outlives.rs create mode 100644 tests/ui/assumptions_on_binders/min-coroutines-alias-outlives.stderr diff --git a/tests/ui/assumptions_on_binders/min-coroutines-alias-outlives.rs b/tests/ui/assumptions_on_binders/min-coroutines-alias-outlives.rs new file mode 100644 index 0000000000000..3ada2d4545412 --- /dev/null +++ b/tests/ui/assumptions_on_binders/min-coroutines-alias-outlives.rs @@ -0,0 +1,47 @@ +//@ compile-flags: -Zassumptions-on-binders=min_coroutines +//@ normalize-stderr: "\[[0-9a-f]{4}\]" -> "[HASH]" + +#![feature(test_binder_constraints)] +#![allow(internal_features)] + +trait Trait { + type Assoc; +} + +// Minimal mode keeps type outlives constraints intact instead of destructuring them into their +// components, so these constraints are retained and left for the root inference context. +// +// The `actual` constraint in the expected output is the point of these tests, so do not normalize +// it away: it is what distinguishes the retained `TypeOutlives` leaf from the OR of item bounds, +// env assumptions and components that eager destructuring would produce. + +// The assumption names the component `T` while the goal names the composite `(T,)`. Destructuring +// eagerly would reduce the goal to its component and discharge it against the assumption, which is +// exactly the strengthening of the eager leak check that this mode avoids. Keeping the constraint +// whole means it is retained instead, so this `expect` clause fails. +core::test_binder_constraints! { + impl { + forall<'a> where T: 'a { + //~^ ERROR forall expect clause failed + where (T,): 'a + } expect {} + } +} + +// FIXME(-Zassumptions-on-binders): the assumption on the binder names the very same alias, so this +// ought to be discharged and the `expect` clause ought to hold. It is not, because the assumption +// is lowered without being normalized and so carries a non-rigid alias, while the goal is +// normalized to a rigid one, and the two do not compare equal. See the FIXME about normalizing +// assumptions in `region_assumptions_for_placeholders_in_universe`. Destructuring the constraint +// eagerly would lose the `TypeOutlives` leaf that this matching needs, which is why minimal mode +// keeps it. +core::test_binder_constraints! { + impl { + forall<'a> where T::Assoc: 'a { + //~^ ERROR forall expect clause failed + where T::Assoc: 'a + } expect {} + } +} + +fn main() {} diff --git a/tests/ui/assumptions_on_binders/min-coroutines-alias-outlives.stderr b/tests/ui/assumptions_on_binders/min-coroutines-alias-outlives.stderr new file mode 100644 index 0000000000000..aa955d52f2398 --- /dev/null +++ b/tests/ui/assumptions_on_binders/min-coroutines-alias-outlives.stderr @@ -0,0 +1,61 @@ +error: forall expect clause failed + --> $DIR/min-coroutines-alias-outlives.rs:24:9 + | +LL | forall<'a> where T: 'a { + | ^^^^^^ + | +note: constraint from here + --> $DIR/min-coroutines-alias-outlives.rs:24:9 + | +LL | forall<'a> where T: 'a { + | ^^^^^^ + = note: expected: And( + [], + ) + = note: actual: And( + [ + TypeOutlives( + (T/#0,), + '!1_0.Named(DefId(0:8 ~ min_coroutines_alias_outlives[HASH]::{test_binder_constraints#0}::'a)), + $DIR/min-coroutines-alias-outlives.rs:24:9: 24:15 (#0), + ), + ], + ) + +error: forall expect clause failed + --> $DIR/min-coroutines-alias-outlives.rs:40:9 + | +LL | forall<'a> where T::Assoc: 'a { + | ^^^^^^ + | +note: constraint from here + --> $DIR/min-coroutines-alias-outlives.rs:40:9 + | +LL | forall<'a> where T::Assoc: 'a { + | ^^^^^^ + = note: expected: And( + [], + ) + = note: actual: And( + [ + TypeOutlives( + Alias( + Yes, + Alias { + kind: Projection { + def_id: DefId(0:4 ~ min_coroutines_alias_outlives[HASH]::Trait::Assoc), + }, + args: [ + T/#0, + ], + .. + }, + ), + '!1_0.Named(DefId(0:11 ~ min_coroutines_alias_outlives[HASH]::{test_binder_constraints#1}::'a)), + $DIR/min-coroutines-alias-outlives.rs:40:9: 40:15 (#0), + ), + ], + ) + +error: aborting due to 2 previous errors + diff --git a/tests/ui/assumptions_on_binders/test-infra-works.rs b/tests/ui/assumptions_on_binders/test-infra-works.rs index d9f4ed2179aff..66ec09639659e 100644 --- a/tests/ui/assumptions_on_binders/test-infra-works.rs +++ b/tests/ui/assumptions_on_binders/test-infra-works.rs @@ -52,7 +52,15 @@ trait Trait { // `impl` should fail because the constraints asserted in `expect` should fail to prove true. Might // be https://github.com/rust-lang/project-assumptions-on-binders/issues/26 // +// The `expect` clauses of this and the next test assert on the full mode's rewrite of alias +// outlives constraints into lower universes. `min_coroutines` deliberately does not rewrite, it +// only drops constraints directly implied by the binder's assumptions and keeps the rest as they +// are, so the rewritten form is specific to `assumptions`. The retained form cannot be spelled in +// an `expect` clause because it still mentions the binder's own lifetime, so `min_coroutines` +// coverage for aliases lives in `min-coroutines-alias-outlives.rs` instead. +// // for<> syntax does direct insert into constraint storage +#[cfg(assumptions)] core::test_binder_constraints! { impl { forall<'a> { @@ -71,6 +79,7 @@ core::test_binder_constraints! { // be https://github.com/rust-lang/project-assumptions-on-binders/issues/26 // // `where` syntax goes through the full clause destructuring and register_obligation pipeline +#[cfg(assumptions)] core::test_binder_constraints! { impl { forall<'a> { @@ -96,4 +105,16 @@ core::test_binder_constraints! { } } +// Minimal mode discharges a type outlives goal when an assumption names the same type. Note that +// this case alone does not pin down whether the constraint was kept whole or destructured, since a +// bare param is its own only component either way; `min-coroutines-alias-outlives.rs` covers that. +#[cfg(min_coroutines)] +core::test_binder_constraints! { + impl { + forall<'a> where T: 'a { + where T: 'a + } expect {} + } +} + fn main() {}