From dbb76aa8344d51835ee9e8ff219982da70a6339e Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Sat, 29 Aug 2026 15:20:23 -0300 Subject: [PATCH 1/9] Handle reflexive solver region constraints --- compiler/rustc_type_ir/src/region_constraint.rs | 4 ++++ .../reflexive-placeholder-outlives.rs | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 tests/ui/assumptions_on_binders/reflexive-placeholder-outlives.rs diff --git a/compiler/rustc_type_ir/src/region_constraint.rs b/compiler/rustc_type_ir/src/region_constraint.rs index 9a643b538d93f..9c6fd993b180a 100644 --- a/compiler/rustc_type_ir/src/region_constraint.rs +++ b/compiler/rustc_type_ir/src/region_constraint.rs @@ -645,6 +645,10 @@ fn pull_region_outlives_constraints_out_of_universe< pulled_constraints.push(Or::new_leaf(c.clone())); } RegionOutlives(region_1, region_2, ()) => { + if region_1 == region_2 { + continue; + } + let region_1_u = max_universe(infcx, region_1); let region_2_u = max_universe(infcx, region_2); diff --git a/tests/ui/assumptions_on_binders/reflexive-placeholder-outlives.rs b/tests/ui/assumptions_on_binders/reflexive-placeholder-outlives.rs new file mode 100644 index 0000000000000..5ae97af1198d0 --- /dev/null +++ b/tests/ui/assumptions_on_binders/reflexive-placeholder-outlives.rs @@ -0,0 +1,17 @@ +//@ check-pass +//@ compile-flags: -Zassumptions-on-binders + +#![feature(test_binder_constraints, non_lifetime_binders)] +#![expect(incomplete_features)] + +// Regression test for rust-lang/project-assumptions-on-binders#19. +core::test_binder_constraints! { + impl<> { + forall<'a> { + 'a: 'a + } expect { + } + } +} + +fn main() {} From fa9c3bec2d5a740d98de738c8566a744eb3ad699 Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Sat, 29 Aug 2026 21:04:16 -0300 Subject: [PATCH 2/9] Discharge region constraints entailed by assumptions --- .../rustc_type_ir/src/region_constraint.rs | 8 +++ .../reflexive-placeholder-outlives.rs | 17 ----- .../same-universe-placeholder-outlives.rs | 64 +++++++++++++++++++ 3 files changed, 72 insertions(+), 17 deletions(-) delete mode 100644 tests/ui/assumptions_on_binders/reflexive-placeholder-outlives.rs create mode 100644 tests/ui/assumptions_on_binders/same-universe-placeholder-outlives.rs diff --git a/compiler/rustc_type_ir/src/region_constraint.rs b/compiler/rustc_type_ir/src/region_constraint.rs index 9c6fd993b180a..7b661c400b1ea 100644 --- a/compiler/rustc_type_ir/src/region_constraint.rs +++ b/compiler/rustc_type_ir/src/region_constraint.rs @@ -665,6 +665,14 @@ fn pull_region_outlives_constraints_out_of_universe< } }; + // The constraint may already be entailed by the assumptions of the binder we are + // leaving, e.g. `for<'a, 'b> where 'b: 'a { 'b: 'a }`. There is nothing to lift into + // a smaller universe in that case, and looking for lower universe candidates would + // wrongly result in `Or([])` whenever the placeholders have no lower universe bounds. + if regions_outlived_by(region_1, assumptions).any(|r| r == region_2) { + continue; + } + let mut candidates = vec![]; for ub in regions_outlived_by(region_1, assumptions) diff --git a/tests/ui/assumptions_on_binders/reflexive-placeholder-outlives.rs b/tests/ui/assumptions_on_binders/reflexive-placeholder-outlives.rs deleted file mode 100644 index 5ae97af1198d0..0000000000000 --- a/tests/ui/assumptions_on_binders/reflexive-placeholder-outlives.rs +++ /dev/null @@ -1,17 +0,0 @@ -//@ check-pass -//@ compile-flags: -Zassumptions-on-binders - -#![feature(test_binder_constraints, non_lifetime_binders)] -#![expect(incomplete_features)] - -// Regression test for rust-lang/project-assumptions-on-binders#19. -core::test_binder_constraints! { - impl<> { - forall<'a> { - 'a: 'a - } expect { - } - } -} - -fn main() {} diff --git a/tests/ui/assumptions_on_binders/same-universe-placeholder-outlives.rs b/tests/ui/assumptions_on_binders/same-universe-placeholder-outlives.rs new file mode 100644 index 0000000000000..6d292416cd4cf --- /dev/null +++ b/tests/ui/assumptions_on_binders/same-universe-placeholder-outlives.rs @@ -0,0 +1,64 @@ +//@ check-pass +//@ compile-flags: -Zassumptions-on-binders + +#![feature(test_binder_constraints, non_lifetime_binders)] +#![expect(incomplete_features)] + +// Regression test for rust-lang/project-assumptions-on-binders#19. +// +// When leaving a binder we lift its region constraints into a smaller universe. Constraints +// which already hold inside of the binder have no lower universe candidates to be lifted to, +// so they used to turn into `Or([])`, i.e. `false`. They have to be discharged instead. + +// Outlives is reflexive. +core::test_binder_constraints! { + impl<> { + forall<'a> { + 'a: 'a + } expect { + } + } +} + +// Directly entailed by an assumption of the binder we're leaving. +core::test_binder_constraints! { + impl<> { + forall<'a, 'b> where 'b: 'a { + 'b: 'a + } expect { + } + } +} + +// Transitively entailed by the assumptions of the binder we're leaving. +core::test_binder_constraints! { + impl<> { + forall<'a, 'b, 'c> where 'c: 'b, 'b: 'a { + 'c: 'a + } expect { + } + } +} + +// Discharging entailed constraints must not swallow the ones which still have to be lifted +// into the outer universe. Here `'a: 'a` and `'b: 'a` are discharged inside the binder while +// `'c: 'a` is lifted, as `'c` outlives every lower universe region that `'a` outlives. +// +// FIXME(-Zassumptions-on-binders): this should be `impl<'b, 'c: 'b>`, not +// `impl<'b, 'c: 'b + 'static>`, but OR isn't actually implemented yet +core::test_binder_constraints! { + impl<'b, 'c: 'b + 'static> { + forall<'a> where 'b: 'a { + 'a: 'a, + 'b: 'a, + 'c: 'a, + } expect { + or { + 'c: 'b, + 'c: 'static, + } + } + } +} + +fn main() {} From c516fc060c059c973f27a6fdf3b6f2f8edceaa88 Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Sat, 29 Aug 2026 21:08:04 -0300 Subject: [PATCH 3/9] Include implied regions in outlives assumptions --- .../rustc_hir_analysis/src/check/wfcheck.rs | 7 +++- .../src/infer/outlives/obligations.rs | 17 +++++++- .../eval_ctxt/solver_region_constraints.rs | 5 ++- .../rustc_type_ir/src/region_constraint.rs | 42 +++++++++++++++++++ .../same-universe-placeholder-outlives.rs | 22 ++++++++++ 5 files changed, 88 insertions(+), 5 deletions(-) diff --git a/compiler/rustc_hir_analysis/src/check/wfcheck.rs b/compiler/rustc_hir_analysis/src/check/wfcheck.rs index 0dee9690737df..34f8ae70abac4 100644 --- a/compiler/rustc_hir_analysis/src/check/wfcheck.rs +++ b/compiler/rustc_hir_analysis/src/check/wfcheck.rs @@ -2406,8 +2406,11 @@ impl<'tcx> WfCheckingCtxt<'_, 'tcx> { for &(r1, r2) in &body.region_outlives { builder.add(r1, r2); } - let assumptions = - ty::region_constraint::Assumptions::new(body.type_outlives, builder.freeze()); + let assumptions = ty::region_constraint::Assumptions::new( + self.tcx(), + body.type_outlives, + builder.freeze(), + ); self.infcx.insert_placeholder_assumptions(u, Some(assumptions)); self.check_test_binder_body(body.value); let solver_region_constraint = self.infcx.get_solver_region_constraint(); diff --git a/compiler/rustc_infer/src/infer/outlives/obligations.rs b/compiler/rustc_infer/src/infer/outlives/obligations.rs index cbbf5e3c91c42..23eb48d43dfb7 100644 --- a/compiler/rustc_infer/src/infer/outlives/obligations.rs +++ b/compiler/rustc_infer/src/infer/outlives/obligations.rs @@ -234,8 +234,22 @@ impl<'tcx> InferCtxt<'tcx> { &self, outlives_env: &OutlivesEnvironment<'tcx>, ) { + // `known_type_outlives` only contains the explicit `Ty: 'a` where clauses. The implied + // bounds, e.g. `T: 'a` from a `&'a T` argument, are only tracked in `region_bound_pairs` + // so we have to pull them in separately. Without them we'd fail to prove `T: 'a` for a + // `&'a T` argument whenever the only explicit bound on `T` mentions a different region. + let mut known_type_outlives = outlives_env.known_type_outlives().to_vec(); + for &ty::OutlivesClause(kind, r) in outlives_env.region_bound_pairs() { + let ty = match kind { + GenericKind::Param(p) => Ty::new_param(self.tcx, p.index, p.name), + GenericKind::Placeholder(p) => Ty::new_placeholder(self.tcx, p), + GenericKind::Alias(alias) => alias.to_ty(self.tcx, ty::IsRigid::Yes), + }; + known_type_outlives.push(ty::Binder::dummy(ty::OutlivesClause(ty, r))); + } let assumptions = rustc_type_ir::region_constraint::Assumptions::new( - outlives_env.known_type_outlives().into_iter().cloned().collect(), + self.tcx, + known_type_outlives, outlives_env.free_region_map().relation.clone(), ); self.destructure_solver_region_constraints(assumptions, self); @@ -249,6 +263,7 @@ impl<'tcx> InferCtxt<'tcx> { region_outlives: TransitiveRelation, ) { let assumptions = region_constraint::Assumptions::new( + self.tcx, known_type_outlives.into_iter().cloned().collect(), region_outlives.maybe_map(|r| Some(Region::new_var(self.tcx, r))).unwrap(), ); 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..d67e4cf0275bb 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 @@ -102,8 +102,9 @@ where // FIXME(-Zassumptions-on-binders): we need to normalize here/somewhere // as we assume the type outlives assumptions only have rigid types :> + let cx = self.cx(); let clauses = rustc_type_ir::elaborate::elaborate( - self.cx(), + cx, reqs.into_iter().filter_map(|goal| goal.predicate.as_clause()), ); @@ -120,7 +121,7 @@ where }, ); - Some(Assumptions::new(type_outlives, region_outlives_builder.freeze())) + Some(Assumptions::new(cx, type_outlives, region_outlives_builder.freeze())) } #[instrument(level = "debug", skip(self), ret)] diff --git a/compiler/rustc_type_ir/src/region_constraint.rs b/compiler/rustc_type_ir/src/region_constraint.rs index 7b661c400b1ea..b0e931f5ab6bf 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, @@ -73,9 +74,50 @@ impl Assumptions { } pub fn new( + cx: I, type_outlives: Vec>>, region_outlives: TransitiveRelation>, ) -> Self { + // A `Ty: 'a` assumption also tells us that every region component of `Ty` outlives `'a`, + // e.g. `&'b u8: 'a` implies `'b: 'a`. Callers do not necessarily hand us an elaborated set + // of assumptions so we destructure them here, otherwise we'd fail to prove `'b: 'a` when + // leaving the binder these assumptions belong to. + // + // The type outlives assumptions are still kept around as they are required for proving + // placeholder and alias outlives. + // + // This mirrors `elaborate`, in particular in how it deals with binders: they're simply + // skipped, so `for<'c> Foo<'a, 'c>: 'b` still gives us `'a: 'b`. + let mut implied_region_outlives = vec![]; + for clause in &type_outlives { + let OutlivesClause(ty, r) = clause.clone().skip_binder(); + // Ignore `for<'a> Ty: 'a`. We could treat this as evidence for `Ty: 'static` but + // `elaborate` conservatively doesn't, so neither do we. + if r.is_bound() { + continue; + } + let mut components = Default::default(); + push_outlives_components(cx, ty, &mut components); + implied_region_outlives.extend(components.into_iter().filter_map(|c| match c { + // Regions bound *inside* of `ty` don't relate to anything in scope here. + Component::Region(c_r) if !c_r.is_bound() => Some((c_r, r)), + _ => None, + })); + } + + let region_outlives = if implied_region_outlives.is_empty() { + region_outlives + } else { + let mut builder = TransitiveRelationBuilder::default(); + for (r1, r2) in region_outlives.base_edges() { + builder.add(r1, r2); + } + for (r1, r2) in implied_region_outlives { + builder.add(r1, r2); + } + builder.freeze() + }; + Self { inverse_region_outlives: { let mut builder = TransitiveRelationBuilder::default(); diff --git a/tests/ui/assumptions_on_binders/same-universe-placeholder-outlives.rs b/tests/ui/assumptions_on_binders/same-universe-placeholder-outlives.rs index 6d292416cd4cf..56bbf8eacbbcd 100644 --- a/tests/ui/assumptions_on_binders/same-universe-placeholder-outlives.rs +++ b/tests/ui/assumptions_on_binders/same-universe-placeholder-outlives.rs @@ -40,6 +40,28 @@ core::test_binder_constraints! { } } +// `&'b u8: 'a` implies `'b: 'a`, so type outlives assumptions have to be destructured into +// region outlives assumptions. +core::test_binder_constraints! { + impl<> { + forall<'a, 'b> where &'b u8: 'a { + 'b: 'a + } expect { + } + } +} + +// Binders in a type outlives assumption are skipped rather than bailed on, so +// `for<'c> fn(&'c (), &'b u8): 'a` still gives us `'b: 'a`. +core::test_binder_constraints! { + impl<> { + forall<'a, 'b> where for<'c> fn(&'c (), &'b u8): 'a { + 'b: 'a + } expect { + } + } +} + // Discharging entailed constraints must not swallow the ones which still have to be lifted // into the outer universe. Here `'a: 'a` and `'b: 'a` are discharged inside the binder while // `'c: 'a` is lifted, as `'c` outlives every lower universe region that `'a` outlives. From 96373ce7212c4452b5eb832024bbfd777d937bc2 Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Sat, 29 Aug 2026 21:10:50 -0300 Subject: [PATCH 4/9] Evaluate reflexive region constraints at the root --- .../rustc_type_ir/src/region_constraint.rs | 27 +++++++++++++----- .../reflexive-outlives-in-root.rs | 28 +++++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 tests/ui/assumptions_on_binders/reflexive-outlives-in-root.rs diff --git a/compiler/rustc_type_ir/src/region_constraint.rs b/compiler/rustc_type_ir/src/region_constraint.rs index b0e931f5ab6bf..d0de852f60b3b 100644 --- a/compiler/rustc_type_ir/src/region_constraint.rs +++ b/compiler/rustc_type_ir/src/region_constraint.rs @@ -687,6 +687,8 @@ fn pull_region_outlives_constraints_out_of_universe< pulled_constraints.push(Or::new_leaf(c.clone())); } RegionOutlives(region_1, region_2, ()) => { + // Outlives is reflexive, so this holds no matter which universes are involved + // and regardless of whether we know the assumptions for `u`. if region_1 == region_2 { continue; } @@ -771,16 +773,27 @@ pub fn destructure_type_outlives_constraints_in_root< let mut destructured_constraints = Vec::new(); for c in &and.0 { match c { + // Outlives is reflexive. Discharging this here and not just when leaving a universe + // matters as constraints are also destructured in the root, where reflexive candidates + // are the whole reason an OR is satisfiable. E.g. `!T: 'a` with a `!T: 'a` assumption + // ends up as `Or([.., RegionOutlives('a, 'a)])`. + RegionOutlives(r1, r2, _) if r1 == r2 => {} Ambiguity(_) | RegionOutlives(..) => { destructured_constraints.push(Or::new_leaf(c.clone())) } - PlaceholderTyOutlives(ty, r, span) => destructured_constraints.push(Or::new( - regions_outlived_by_placeholder(*ty, assumptions, infcx.cx()).map( - move |assumption_r| { - And::new([RegionOutlives(assumption_r, *r, span.clone())]) - }, - ), - )), + PlaceholderTyOutlives(ty, r, span) => { + let candidates = + regions_outlived_by_placeholder(*ty, assumptions, infcx.cx()).collect::>(); + if candidates.contains(r) { + destructured_constraints.push(Or::new_true()); + } else { + destructured_constraints.push(Or::new(candidates.into_iter().map( + move |assumption_r| { + And::new([RegionOutlives(assumption_r, *r, span.clone())]) + }, + ))); + } + } AliasTyOutlivesViaEnv(bound_outlives, span) => { destructured_constraints.push( alias_outlives_candidates_from_assumptions( diff --git a/tests/ui/assumptions_on_binders/reflexive-outlives-in-root.rs b/tests/ui/assumptions_on_binders/reflexive-outlives-in-root.rs new file mode 100644 index 0000000000000..c6e7f025219dc --- /dev/null +++ b/tests/ui/assumptions_on_binders/reflexive-outlives-in-root.rs @@ -0,0 +1,28 @@ +//@ check-pass +//@ compile-flags: -Zassumptions-on-binders + +// Regression test for rust-lang/project-assumptions-on-binders#19, minimized from `syn`. +// +// Proving `I: '_` for the `&'_ self` receiver destructures to an OR over every region which +// `I` is known to outlive. Two things are needed for that OR to be satisfiable: +// +// - the implied bound `I: '_` from `&'_ self` has to be part of the root assumptions, not just +// the explicit `I: 'a` where clause, as `'a: '_` does not hold +// - the resulting `RegionOutlives('_, '_)` candidate has to be discharged, which happens when +// evaluating the constraint rather than when leaving a universe + +trait IterTrait<'a, T: 'a>: Iterator { + fn clone_box(&self) -> Box + 'a>; +} + +impl<'a, T, I> IterTrait<'a, T> for I +where + T: 'a, + I: Iterator + Clone + 'a, +{ + fn clone_box(&self) -> Box + 'a> { + Box::new(self.clone()) + } +} + +fn main() {} From b4c1f95f09ca1e9e945b6fd408eb794a5778d06e Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Thu, 3 Sep 2026 10:11:05 -0300 Subject: [PATCH 5/9] Keep reflexive constraint handling self-contained --- .../rustc_hir_analysis/src/check/wfcheck.rs | 7 +-- .../src/infer/outlives/obligations.rs | 17 +----- .../eval_ctxt/solver_region_constraints.rs | 5 +- .../rustc_type_ir/src/region_constraint.rs | 60 ++----------------- .../reflexive-outlives-in-root.rs | 29 +++------ .../same-universe-placeholder-outlives.rs | 22 ------- 6 files changed, 19 insertions(+), 121 deletions(-) diff --git a/compiler/rustc_hir_analysis/src/check/wfcheck.rs b/compiler/rustc_hir_analysis/src/check/wfcheck.rs index 34f8ae70abac4..0dee9690737df 100644 --- a/compiler/rustc_hir_analysis/src/check/wfcheck.rs +++ b/compiler/rustc_hir_analysis/src/check/wfcheck.rs @@ -2406,11 +2406,8 @@ impl<'tcx> WfCheckingCtxt<'_, 'tcx> { for &(r1, r2) in &body.region_outlives { builder.add(r1, r2); } - let assumptions = ty::region_constraint::Assumptions::new( - self.tcx(), - body.type_outlives, - builder.freeze(), - ); + let assumptions = + ty::region_constraint::Assumptions::new(body.type_outlives, builder.freeze()); self.infcx.insert_placeholder_assumptions(u, Some(assumptions)); self.check_test_binder_body(body.value); let solver_region_constraint = self.infcx.get_solver_region_constraint(); diff --git a/compiler/rustc_infer/src/infer/outlives/obligations.rs b/compiler/rustc_infer/src/infer/outlives/obligations.rs index 23eb48d43dfb7..cbbf5e3c91c42 100644 --- a/compiler/rustc_infer/src/infer/outlives/obligations.rs +++ b/compiler/rustc_infer/src/infer/outlives/obligations.rs @@ -234,22 +234,8 @@ impl<'tcx> InferCtxt<'tcx> { &self, outlives_env: &OutlivesEnvironment<'tcx>, ) { - // `known_type_outlives` only contains the explicit `Ty: 'a` where clauses. The implied - // bounds, e.g. `T: 'a` from a `&'a T` argument, are only tracked in `region_bound_pairs` - // so we have to pull them in separately. Without them we'd fail to prove `T: 'a` for a - // `&'a T` argument whenever the only explicit bound on `T` mentions a different region. - let mut known_type_outlives = outlives_env.known_type_outlives().to_vec(); - for &ty::OutlivesClause(kind, r) in outlives_env.region_bound_pairs() { - let ty = match kind { - GenericKind::Param(p) => Ty::new_param(self.tcx, p.index, p.name), - GenericKind::Placeholder(p) => Ty::new_placeholder(self.tcx, p), - GenericKind::Alias(alias) => alias.to_ty(self.tcx, ty::IsRigid::Yes), - }; - known_type_outlives.push(ty::Binder::dummy(ty::OutlivesClause(ty, r))); - } let assumptions = rustc_type_ir::region_constraint::Assumptions::new( - self.tcx, - known_type_outlives, + outlives_env.known_type_outlives().into_iter().cloned().collect(), outlives_env.free_region_map().relation.clone(), ); self.destructure_solver_region_constraints(assumptions, self); @@ -263,7 +249,6 @@ impl<'tcx> InferCtxt<'tcx> { region_outlives: TransitiveRelation, ) { let assumptions = region_constraint::Assumptions::new( - self.tcx, known_type_outlives.into_iter().cloned().collect(), region_outlives.maybe_map(|r| Some(Region::new_var(self.tcx, r))).unwrap(), ); 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 d67e4cf0275bb..5a4daa5e44fc5 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 @@ -102,9 +102,8 @@ where // FIXME(-Zassumptions-on-binders): we need to normalize here/somewhere // as we assume the type outlives assumptions only have rigid types :> - let cx = self.cx(); let clauses = rustc_type_ir::elaborate::elaborate( - cx, + self.cx(), reqs.into_iter().filter_map(|goal| goal.predicate.as_clause()), ); @@ -121,7 +120,7 @@ where }, ); - Some(Assumptions::new(cx, type_outlives, region_outlives_builder.freeze())) + Some(Assumptions::new(type_outlives, region_outlives_builder.freeze())) } #[instrument(level = "debug", skip(self), ret)] diff --git a/compiler/rustc_type_ir/src/region_constraint.rs b/compiler/rustc_type_ir/src/region_constraint.rs index d0de852f60b3b..ff7b00fdbbd98 100644 --- a/compiler/rustc_type_ir/src/region_constraint.rs +++ b/compiler/rustc_type_ir/src/region_constraint.rs @@ -49,7 +49,6 @@ 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, @@ -74,50 +73,9 @@ impl Assumptions { } pub fn new( - cx: I, type_outlives: Vec>>, region_outlives: TransitiveRelation>, ) -> Self { - // A `Ty: 'a` assumption also tells us that every region component of `Ty` outlives `'a`, - // e.g. `&'b u8: 'a` implies `'b: 'a`. Callers do not necessarily hand us an elaborated set - // of assumptions so we destructure them here, otherwise we'd fail to prove `'b: 'a` when - // leaving the binder these assumptions belong to. - // - // The type outlives assumptions are still kept around as they are required for proving - // placeholder and alias outlives. - // - // This mirrors `elaborate`, in particular in how it deals with binders: they're simply - // skipped, so `for<'c> Foo<'a, 'c>: 'b` still gives us `'a: 'b`. - let mut implied_region_outlives = vec![]; - for clause in &type_outlives { - let OutlivesClause(ty, r) = clause.clone().skip_binder(); - // Ignore `for<'a> Ty: 'a`. We could treat this as evidence for `Ty: 'static` but - // `elaborate` conservatively doesn't, so neither do we. - if r.is_bound() { - continue; - } - let mut components = Default::default(); - push_outlives_components(cx, ty, &mut components); - implied_region_outlives.extend(components.into_iter().filter_map(|c| match c { - // Regions bound *inside* of `ty` don't relate to anything in scope here. - Component::Region(c_r) if !c_r.is_bound() => Some((c_r, r)), - _ => None, - })); - } - - let region_outlives = if implied_region_outlives.is_empty() { - region_outlives - } else { - let mut builder = TransitiveRelationBuilder::default(); - for (r1, r2) in region_outlives.base_edges() { - builder.add(r1, r2); - } - for (r1, r2) in implied_region_outlives { - builder.add(r1, r2); - } - builder.freeze() - }; - Self { inverse_region_outlives: { let mut builder = TransitiveRelationBuilder::default(); @@ -687,12 +645,6 @@ fn pull_region_outlives_constraints_out_of_universe< pulled_constraints.push(Or::new_leaf(c.clone())); } RegionOutlives(region_1, region_2, ()) => { - // Outlives is reflexive, so this holds no matter which universes are involved - // and regardless of whether we know the assumptions for `u`. - if region_1 == region_2 { - continue; - } - let region_1_u = max_universe(infcx, region_1); let region_2_u = max_universe(infcx, region_2); @@ -773,17 +725,16 @@ pub fn destructure_type_outlives_constraints_in_root< let mut destructured_constraints = Vec::new(); for c in &and.0 { match c { - // Outlives is reflexive. Discharging this here and not just when leaving a universe - // matters as constraints are also destructured in the root, where reflexive candidates - // are the whole reason an OR is satisfiable. E.g. `!T: 'a` with a `!T: 'a` assumption - // ends up as `Or([.., RegionOutlives('a, 'a)])`. + // Root constraints never go through `pull_region_outlives_constraints_out_of_universe`. + // A reflexive leaf may be the candidate which makes a root OR true, so discharge it here + // instead of requiring the remaining candidates to hold. RegionOutlives(r1, r2, _) if r1 == r2 => {} Ambiguity(_) | RegionOutlives(..) => { destructured_constraints.push(Or::new_leaf(c.clone())) } PlaceholderTyOutlives(ty, r, span) => { - let candidates = - regions_outlived_by_placeholder(*ty, assumptions, infcx.cx()).collect::>(); + let candidates = regions_outlived_by_placeholder(*ty, assumptions, infcx.cx()) + .collect::>(); if candidates.contains(r) { destructured_constraints.push(Or::new_true()); } else { @@ -1059,7 +1010,6 @@ pub fn regions_outlived_by( r: Region, assumptions: &Assumptions, ) -> impl Iterator> { - // FIXME(-Zassumptions-on-binders): do we need to be adding the reflexive edge here? assumptions.region_outlives.reachable_from(r).into_iter().chain([r]) } diff --git a/tests/ui/assumptions_on_binders/reflexive-outlives-in-root.rs b/tests/ui/assumptions_on_binders/reflexive-outlives-in-root.rs index c6e7f025219dc..9cac9e0eaeed3 100644 --- a/tests/ui/assumptions_on_binders/reflexive-outlives-in-root.rs +++ b/tests/ui/assumptions_on_binders/reflexive-outlives-in-root.rs @@ -1,27 +1,16 @@ //@ check-pass //@ compile-flags: -Zassumptions-on-binders -// Regression test for rust-lang/project-assumptions-on-binders#19, minimized from `syn`. -// -// Proving `I: '_` for the `&'_ self` receiver destructures to an OR over every region which -// `I` is known to outlive. Two things are needed for that OR to be satisfiable: -// -// - the implied bound `I: '_` from `&'_ self` has to be part of the root assumptions, not just -// the explicit `I: 'a` where clause, as `'a: '_` does not hold -// - the resulting `RegionOutlives('_, '_)` candidate has to be discharged, which happens when -// evaluating the constraint rather than when leaving a universe +#![feature(test_binder_constraints, non_lifetime_binders)] +#![expect(incomplete_features)] -trait IterTrait<'a, T: 'a>: Iterator { - fn clone_box(&self) -> Box + 'a>; -} - -impl<'a, T, I> IterTrait<'a, T> for I -where - T: 'a, - I: Iterator + Clone + 'a, -{ - fn clone_box(&self) -> Box + 'a> { - Box::new(self.clone()) +// Root type outlives constraints are destructured into an OR over every region the type is known +// to outlive. The reflexive `'b: 'b` candidate makes this OR true even though the unrelated +// `'a: 'b` candidate does not hold. Root constraints never go through the universe-pulling code, +// so evaluation has to recognize the reflexive candidate itself. +core::test_binder_constraints! { + impl<'a, 'b, T: 'a + 'b> { + T: 'b } } diff --git a/tests/ui/assumptions_on_binders/same-universe-placeholder-outlives.rs b/tests/ui/assumptions_on_binders/same-universe-placeholder-outlives.rs index 56bbf8eacbbcd..6d292416cd4cf 100644 --- a/tests/ui/assumptions_on_binders/same-universe-placeholder-outlives.rs +++ b/tests/ui/assumptions_on_binders/same-universe-placeholder-outlives.rs @@ -40,28 +40,6 @@ core::test_binder_constraints! { } } -// `&'b u8: 'a` implies `'b: 'a`, so type outlives assumptions have to be destructured into -// region outlives assumptions. -core::test_binder_constraints! { - impl<> { - forall<'a, 'b> where &'b u8: 'a { - 'b: 'a - } expect { - } - } -} - -// Binders in a type outlives assumption are skipped rather than bailed on, so -// `for<'c> fn(&'c (), &'b u8): 'a` still gives us `'b: 'a`. -core::test_binder_constraints! { - impl<> { - forall<'a, 'b> where for<'c> fn(&'c (), &'b u8): 'a { - 'b: 'a - } expect { - } - } -} - // Discharging entailed constraints must not swallow the ones which still have to be lifted // into the outer universe. Here `'a: 'a` and `'b: 'a` are discharged inside the binder while // `'c: 'a` is lifted, as `'c` outlives every lower universe region that `'a` outlives. From ba8b0fca9dbea2463304b0ba43752d5131947818 Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Mon, 7 Sep 2026 10:07:09 -0300 Subject: [PATCH 6/9] Drop trivially true region constraints when canonicalizing A reflexive `'a: 'a` leaf is always satisfied, so filter it out in `And::new` instead of pattern matching for it in the places which happen to build such a constraint. An AND which ends up empty is trivially true, which makes the OR containing it true. This is how a reflexive candidate discharges a root type outlives constraint, so `destructure_type_outlives_constraints_in_root` no longer has to look at region outlives leaves at all. --- .../rustc_type_ir/src/region_constraint.rs | 46 +++++++++++-------- .../reflexive-outlives-in-root.rs | 4 +- .../test-infra-fails-properly.rs | 2 +- .../test-infra-fails-properly.stderr | 4 +- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/compiler/rustc_type_ir/src/region_constraint.rs b/compiler/rustc_type_ir/src/region_constraint.rs index ff7b00fdbbd98..fcea7db55df1a 100644 --- a/compiler/rustc_type_ir/src/region_constraint.rs +++ b/compiler/rustc_type_ir/src/region_constraint.rs @@ -165,6 +165,7 @@ impl LeafRegionC /// An OR of AND of LEAF constraints. Always in "canonical form" meaning: /// - No two ANDs are equivalent /// - All ANDs are in canonical form +/// - If any AND is empty, i.e. trivially true, it is the only AND pub struct Or(pub Box<[And]>); impl Or { pub fn with_spans( @@ -204,6 +205,13 @@ impl Or { let mut new_ands: Vec> = Vec::new(); for and in ands { + // An empty AND is trivially true, which makes the whole OR true no matter what the + // other candidates are. `And::new` discards leaf constraints which are trivially + // true, so this is how e.g. a reflexive `'a: 'a` candidate discharges an OR. + if and.0.is_empty() { + return Self::new_true(); + } + if new_ands.iter().all(|c| !c.is_and_equivalent_to(&and)) { new_ands.push(and) } @@ -217,7 +225,7 @@ impl Or { } pub fn new_leaf(l: LeafRegionConstraint) -> Self { - Or(Box::new([And(Box::new([l]))])) + Or::new([And::new([l])]) } pub fn build_and(a: Or, b: Or) -> Self { @@ -249,6 +257,7 @@ impl Or { #[cfg_attr(feature = "nightly", derive(StableHash_NoContext))] /// An AND of leaf constraints. Always in "canonical form", meaning: /// - No leaf constraints are present twice in this AND +/// - No leaf constraint is trivially true, i.e. a reflexive `'a: 'a` pub struct And(pub Box<[LeafRegionConstraint]>); impl And { pub fn with_spans( @@ -264,6 +273,15 @@ impl And { And(i .into_iter() .filter(|leaf| { + // Outlives is reflexive so a `'a: 'a` leaf is always true and carries no + // information. Dropping it here keeps the rest of the code from having to special + // case it, and is what lets an OR with a reflexive candidate be recognized as true. + if let LeafRegionConstraint::RegionOutlives(r1, r2, _) = leaf + && r1 == r2 + { + return false; + } + if seen.contains(&leaf.clone().without_span()) { false } else { @@ -409,7 +427,7 @@ impl RegionConst } pub fn new_leaf(l: LeafRegionConstraint) -> Self { - RegionConstraint { and_constraint: And(Box::new([l])), or_constraint: Or::new_true() } + RegionConstraint { and_constraint: And::new([l]), or_constraint: Or::new_true() } } } @@ -725,26 +743,16 @@ pub fn destructure_type_outlives_constraints_in_root< let mut destructured_constraints = Vec::new(); for c in &and.0 { match c { - // Root constraints never go through `pull_region_outlives_constraints_out_of_universe`. - // A reflexive leaf may be the candidate which makes a root OR true, so discharge it here - // instead of requiring the remaining candidates to hold. - RegionOutlives(r1, r2, _) if r1 == r2 => {} Ambiguity(_) | RegionOutlives(..) => { destructured_constraints.push(Or::new_leaf(c.clone())) } - PlaceholderTyOutlives(ty, r, span) => { - let candidates = regions_outlived_by_placeholder(*ty, assumptions, infcx.cx()) - .collect::>(); - if candidates.contains(r) { - destructured_constraints.push(Or::new_true()); - } else { - destructured_constraints.push(Or::new(candidates.into_iter().map( - move |assumption_r| { - And::new([RegionOutlives(assumption_r, *r, span.clone())]) - }, - ))); - } - } + PlaceholderTyOutlives(ty, r, span) => destructured_constraints.push(Or::new( + regions_outlived_by_placeholder(*ty, assumptions, infcx.cx()).map( + move |assumption_r| { + And::new([RegionOutlives(assumption_r, *r, span.clone())]) + }, + ), + )), AliasTyOutlivesViaEnv(bound_outlives, span) => { destructured_constraints.push( alias_outlives_candidates_from_assumptions( diff --git a/tests/ui/assumptions_on_binders/reflexive-outlives-in-root.rs b/tests/ui/assumptions_on_binders/reflexive-outlives-in-root.rs index 9cac9e0eaeed3..641ac3951d76a 100644 --- a/tests/ui/assumptions_on_binders/reflexive-outlives-in-root.rs +++ b/tests/ui/assumptions_on_binders/reflexive-outlives-in-root.rs @@ -6,8 +6,8 @@ // Root type outlives constraints are destructured into an OR over every region the type is known // to outlive. The reflexive `'b: 'b` candidate makes this OR true even though the unrelated -// `'a: 'b` candidate does not hold. Root constraints never go through the universe-pulling code, -// so evaluation has to recognize the reflexive candidate itself. +// `'a: 'b` candidate does not hold. Reflexive leaves are dropped when building an AND, which +// leaves an empty, i.e. trivially true, AND as one of the candidates of the OR. core::test_binder_constraints! { impl<'a, 'b, T: 'a + 'b> { T: 'b diff --git a/tests/ui/assumptions_on_binders/test-infra-fails-properly.rs b/tests/ui/assumptions_on_binders/test-infra-fails-properly.rs index 240b64e770f51..97629627ad556 100644 --- a/tests/ui/assumptions_on_binders/test-infra-fails-properly.rs +++ b/tests/ui/assumptions_on_binders/test-infra-fails-properly.rs @@ -60,7 +60,7 @@ core::test_binder_constraints! { } expect { or { 'c: 'b, - 'c: 'c, + 'b: 'c, //~^ ERROR forall expect clause failed } } diff --git a/tests/ui/assumptions_on_binders/test-infra-fails-properly.stderr b/tests/ui/assumptions_on_binders/test-infra-fails-properly.stderr index 2931a37c0f340..ab025b0e0b9d6 100644 --- a/tests/ui/assumptions_on_binders/test-infra-fails-properly.stderr +++ b/tests/ui/assumptions_on_binders/test-infra-fails-properly.stderr @@ -53,7 +53,7 @@ LL | T: 'a, error: forall expect clause failed --> $DIR/test-infra-fails-properly.rs:63:17 | -LL | 'c: 'c, +LL | 'b: 'c, | ^^^^^^ | note: constraint from here @@ -62,7 +62,7 @@ note: constraint from here LL | forall<'a> where 'b: 'a { | ^^^^^^ = note: expected: RegionOutlives( - 'c/#1, + 'b/#0, 'c/#1, $DIR/test-infra-fails-properly.rs:63:17: 63:23 (#0), ) From 1e3e8f66e0dc904c74107c417c1a888baed71801 Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Mon, 7 Sep 2026 15:07:49 -0300 Subject: [PATCH 7/9] Preserve concrete candidates in ambiguous ORs --- .../rustc_type_ir/src/region_constraint.rs | 24 +++---- ...mixed-ambiguous-alias-outlives.fail.stderr | 13 ++++ .../mixed-ambiguous-alias-outlives.rs | 71 +++++++++++++++++++ 3 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 tests/ui/assumptions_on_binders/mixed-ambiguous-alias-outlives.fail.stderr create mode 100644 tests/ui/assumptions_on_binders/mixed-ambiguous-alias-outlives.rs diff --git a/compiler/rustc_type_ir/src/region_constraint.rs b/compiler/rustc_type_ir/src/region_constraint.rs index fcea7db55df1a..e845c8ac1d591 100644 --- a/compiler/rustc_type_ir/src/region_constraint.rs +++ b/compiler/rustc_type_ir/src/region_constraint.rs @@ -594,20 +594,16 @@ pub fn propagate_ambiguity $DIR/mixed-ambiguous-alias-outlives.rs:35:9 + | +LL | forall<'a, U> { + | ^^^^^^ + +error: unable to satisfy constraints involving placeholders due to unknown implied bounds + --> $DIR/mixed-ambiguous-alias-outlives.rs:63:9 + | +LL | forall<'a, U> { + | ^^^^^^ + +error: aborting due to 2 previous errors diff --git a/tests/ui/assumptions_on_binders/mixed-ambiguous-alias-outlives.rs b/tests/ui/assumptions_on_binders/mixed-ambiguous-alias-outlives.rs new file mode 100644 index 0000000000000..e2b36df9f6ba7 --- /dev/null +++ b/tests/ui/assumptions_on_binders/mixed-ambiguous-alias-outlives.rs @@ -0,0 +1,71 @@ +//@ revisions: pass fail +//@[pass] check-pass +//@ compile-flags: -Zassumptions-on-binders + +#![feature(test_binder_constraints, non_lifetime_binders)] +#![expect(incomplete_features)] + +trait Project { + type Assoc; +} + +impl Project for T { + type Assoc = T; +} + +// The alias involving U cannot leave its non-lifetime binder and becomes ambiguous. +// Keep the other alias candidate until the root, where its outlives assumption is known. +#[cfg(pass)] +core::test_binder_constraints! { + impl> { + forall<'a, U> { + or { + for<> ::Assoc: 'a, + for<> T::Assoc: 'a, + } + } + } +} + +// If the concrete candidate is rejected at the root, the ambiguous alternative must +// still cause an error rather than disappearing with the rejected candidate. +#[cfg(fail)] +core::test_binder_constraints! { + impl { + forall<'a, U> { + //[fail]~^ ERROR unable to satisfy constraints involving placeholders + or { + for<> ::Assoc: 'a, + for<> T::Assoc: 'a, + } + } + } +} + +// The order of the alternatives must not change whether the bound can be proved. +#[cfg(pass)] +core::test_binder_constraints! { + impl> { + forall<'a, U> { + or { + for<> T::Assoc: 'a, + for<> ::Assoc: 'a, + } + } + } +} + +// A required ambiguous constraint still makes the whole AND ambiguous, even when +// its sibling is known to hold. +#[cfg(fail)] +core::test_binder_constraints! { + impl> { + forall<'a, U> { + //[fail]~^ ERROR unable to satisfy constraints involving placeholders + for<> ::Assoc: 'a, + for<> T::Assoc: 'a, + } + } +} + +fn main() {} From da7324eef4675caee8a4ce7c6063e7ed2cc21e0e Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Mon, 7 Sep 2026 15:56:24 -0300 Subject: [PATCH 8/9] Match mixed OR stderr fixture --- .../mixed-ambiguous-alias-outlives.fail.stderr | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ui/assumptions_on_binders/mixed-ambiguous-alias-outlives.fail.stderr b/tests/ui/assumptions_on_binders/mixed-ambiguous-alias-outlives.fail.stderr index 7cea92483b265..a07be490ba91c 100644 --- a/tests/ui/assumptions_on_binders/mixed-ambiguous-alias-outlives.fail.stderr +++ b/tests/ui/assumptions_on_binders/mixed-ambiguous-alias-outlives.fail.stderr @@ -11,3 +11,4 @@ LL | forall<'a, U> { | ^^^^^^ error: aborting due to 2 previous errors + From 215aa48532f4574b44bbde5735043846af20b744 Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Tue, 8 Sep 2026 15:20:16 -0300 Subject: [PATCH 9/9] Remove redundant OR ambiguity propagation Shared ambiguous leaves already move into the outer AND during canonicalization. Keep the AND check and cover an OR whose alternatives both become ambiguous. --- compiler/rustc_type_ir/src/region_constraint.rs | 12 ------------ .../mixed-ambiguous-alias-outlives.fail.stderr | 8 +++++++- .../mixed-ambiguous-alias-outlives.rs | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/compiler/rustc_type_ir/src/region_constraint.rs b/compiler/rustc_type_ir/src/region_constraint.rs index e845c8ac1d591..090799d19b61b 100644 --- a/compiler/rustc_type_ir/src/region_constraint.rs +++ b/compiler/rustc_type_ir/src/region_constraint.rs @@ -594,18 +594,6 @@ pub fn propagate_ambiguity { | ^^^^^^ -error: aborting due to 2 previous errors +error: unable to satisfy constraints involving placeholders due to unknown implied bounds + --> $DIR/mixed-ambiguous-alias-outlives.rs:75:9 + | +LL | forall<'a, U, V> { + | ^^^^^^ + +error: aborting due to 3 previous errors diff --git a/tests/ui/assumptions_on_binders/mixed-ambiguous-alias-outlives.rs b/tests/ui/assumptions_on_binders/mixed-ambiguous-alias-outlives.rs index e2b36df9f6ba7..6f05e57fc5b2f 100644 --- a/tests/ui/assumptions_on_binders/mixed-ambiguous-alias-outlives.rs +++ b/tests/ui/assumptions_on_binders/mixed-ambiguous-alias-outlives.rs @@ -68,4 +68,18 @@ core::test_binder_constraints! { } } +// Ambiguity shared by every OR alternative is still required after canonicalization. +#[cfg(fail)] +core::test_binder_constraints! { + impl { + forall<'a, U, V> { + //[fail]~^ ERROR unable to satisfy constraints involving placeholders + or { + for<> ::Assoc: 'a, + for<> ::Assoc: 'a, + } + } + } +} + fn main() {}