From f67991b605328bf0420382d9e24bdedf4ef0d33b Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Sat, 29 Aug 2026 15:20:23 -0300 Subject: [PATCH 1/4] 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 2923709432ee5..1e4e7572c8551 100644 --- a/compiler/rustc_type_ir/src/region_constraint.rs +++ b/compiler/rustc_type_ir/src/region_constraint.rs @@ -681,6 +681,10 @@ fn pull_region_outlives_constraints_out_of_universe< constraint } RegionOutlives(region_1, region_2, ()) => { + if region_1 == region_2 { + return RegionConstraint::new_true(); + } + 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 c42b5cd6c366ef4e86d2dfcb9c6c5ea965025ae3 Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Sat, 29 Aug 2026 21:04:16 -0300 Subject: [PATCH 2/4] 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 1e4e7572c8551..997aeb059b096 100644 --- a/compiler/rustc_type_ir/src/region_constraint.rs +++ b/compiler/rustc_type_ir/src/region_constraint.rs @@ -697,6 +697,14 @@ fn pull_region_outlives_constraints_out_of_universe< None => return RegionConstraint::Ambiguity(()), }; + // 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) { + return RegionConstraint::new_true(); + } + let mut candidates = vec![]; for ub in regions_outlived_by(region_1, assumptions).filter(|r| max_universe(infcx, *r) < u) 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 43023bee51adfc47791d5fe83361c80cf2484f46 Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Sat, 29 Aug 2026 21:08:04 -0300 Subject: [PATCH 3/4] 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 4224f8394ea4a..04a69b6d28a52 100644 --- a/compiler/rustc_hir_analysis/src/check/wfcheck.rs +++ b/compiler/rustc_hir_analysis/src/check/wfcheck.rs @@ -2395,8 +2395,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 a861b89cceb8e..2090bc1b2724e 100644 --- a/compiler/rustc_infer/src/infer/outlives/obligations.rs +++ b/compiler/rustc_infer/src/infer/outlives/obligations.rs @@ -224,8 +224,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); @@ -239,6 +253,7 @@ impl<'tcx> InferCtxt<'tcx> { region_outlives: TransitiveRelation, ) { let assumptions = rustc_type_ir::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 2790b563f1ea7..0a7579a90ba65 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 997aeb059b096..fb042cd7ae869 100644 --- a/compiler/rustc_type_ir/src/region_constraint.rs +++ b/compiler/rustc_type_ir/src/region_constraint.rs @@ -48,6 +48,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, FallibleTypeFolder, @@ -74,9 +75,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 70d280fb87854ac38e11c6c2e080f8466d7e458d Mon Sep 17 00:00:00 2001 From: Joao Roberto Date: Sat, 29 Aug 2026 21:10:50 -0300 Subject: [PATCH 4/4] Evaluate reflexive region constraints at the root --- .../rustc_type_ir/src/region_constraint.rs | 7 +++++ .../reflexive-outlives-in-root.rs | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+) 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 fb042cd7ae869..78e1fda2debca 100644 --- a/compiler/rustc_type_ir/src/region_constraint.rs +++ b/compiler/rustc_type_ir/src/region_constraint.rs @@ -628,6 +628,11 @@ pub fn evaluate_solver_constraint( ) -> RegionConstraint { use RegionConstraint::*; match constraint { + // 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 => RegionConstraint::new_true(), Ambiguity(_) | RegionOutlives(..) | AliasTyOutlivesViaEnv(..) @@ -723,6 +728,8 @@ fn pull_region_outlives_constraints_out_of_universe< constraint } 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 { return RegionConstraint::new_true(); } 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() {}