Skip to content
14 changes: 13 additions & 1 deletion compiler/rustc_next_trait_solver/src/canonical/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use std::iter;
use canonicalizer::Canonicalizer;
use rustc_index::IndexVec;
use rustc_type_ir::inherent::*;
use rustc_type_ir::region_constraint::RegionConstraint as SolverRegionConstraint;
use rustc_type_ir::relate::{
self, Relate, RelateResult, TypeRelation, VarianceDiagInfo, relate_args_invariantly,
};
Expand Down Expand Up @@ -380,7 +381,18 @@ where

#[instrument(skip(self), level = "trace")]
fn regions(&mut self, a: Region<I>, b: Region<I>) -> RelateResult<I, Region<I>> {
self.infcx.equate_regions(a, b, VisibleForLeakCheck::Yes, self.span);
if self.cx().assumptions_on_binders() {
if a != b {
self.infcx.register_solver_region_constraint(
SolverRegionConstraint::RegionOutlives(a, b),
);
self.infcx.register_solver_region_constraint(
SolverRegionConstraint::RegionOutlives(b, a),
);
}
} else {
self.infcx.equate_regions(a, b, VisibleForLeakCheck::Yes, self.span);
}

Ok(a)
}
Expand Down
155 changes: 155 additions & 0 deletions compiler/rustc_type_ir/src/region_constraint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,10 @@ pub fn eagerly_handle_placeholders_in_universe<Infcx: InferCtxtLike<Interner = I

let assumptions = infcx.get_placeholder_assumptions(u);

// Do this before rewriting type outlives constraints: alias/env matching below needs to
// see placeholders equated with current-universe region variables in the same `And` branch.
let constraint = normalize_equated_region_vars(infcx, constraint, u);

@BoxyUwU BoxyUwU Jul 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im having a hard time understanding what motivated this change 🤔 can you give an example with:

  • an alias outlives assumption
  • the alias being matched against it
  • what the region constraints we get are
  • why those region constraints are mishandled in some way if we dont do this line of code
  • what region constraints we get instead if we do this line of code

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi I checked this with a local trace on tests/ui/assumptions_on_binders/alias_outlives.rs, and I think I have a better handle on what is happening now.

The passing case has this env assumption:

<T as AliasHaver>::Assoc: 'static

To prove T::Assoc: for<'a> Trait<'a>, we enter the binder and prove T::Assoc: Trait<'!a_u1>. Selecting the blanket impl introduces an impl lifetime var, '?0 in my trace.

Before this normalization, the relevant constraint was:

RegionOutlives('!1_...ReqTrait::'a, '?0)
RegionOutlives('?0, '!1_...ReqTrait::'a)
AliasTyOutlivesViaEnv(<T as AliasHaver>::Assoc: '?0)
PlaceholderTyOutlives(!0, '?0)

So the alias being matched is <T as AliasHaver>::Assoc, but its outlives target is '?0, not the placeholder directly.

After normalization, the same constraint becomes:

RegionOutlives('!1_...ReqTrait::'a, '!1_...ReqTrait::'a)
RegionOutlives('!1_...ReqTrait::'a, '!1_...ReqTrait::'a)
AliasTyOutlivesViaEnv(<T as AliasHaver>::Assoc: '!1_...ReqTrait::'a)
PlaceholderTyOutlives(!0, '!1_...ReqTrait::'a)

Then matching against <T as AliasHaver>::Assoc: 'static gives:

RegionOutlives('static, '!1_...ReqTrait::'a)

For the failing case, where the env only has <T as AliasHaver>::Assoc: 'a, it gives:

RegionOutlives('a/#0, '!1_...ReqTrait::'a)

and that stays unsatisfied, which is what we want.

Without this normalization, the trace keeps the alias target as '?0, and REGIONCK_ENV_PASS fails with:

error[E0283]: type annotations needed: cannot satisfy `for<'a> <T as AliasHaver>::Assoc: Trait<'a>`

My read is that alias/env matching gets here before the later region closure pass. We already know '?0 == '!a_u1 in the same And branch, but unless we make that visible first, the alias outlives path sees a current-universe var instead of the placeholder shape it needs.

idk if this is the nicest layer for it, but the trace made the motivation a lot clearer to me. ltm if you want the raw trace bits and I can paste them too.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What actually goes wrong when we have these constraints:

RegionOutlives('!1_...ReqTrait::'a, '?0)
RegionOutlives('?0, '!1_...ReqTrait::'a)
AliasTyOutlivesViaEnv(<T as AliasHaver>::Assoc: '?0)
PlaceholderTyOutlives(!0, '?0)

im having a hard time understanding how we go from that to an Ambiguity somewhere. I'm assuming that this is happening in alias_outlives_candidates_from_assumptions when trying to rewrite AliasTyOutlivesViaEnv? I would expect that we get back a RegionOutlives('static, '?0) constraint from that, i don't understand why that would go on to cause problems :3

@Dnreikronos Dnreikronos Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, your expectation would be right if <T as AliasHaver>::Assoc: 'static was in the assumptions available here. fyi i traced this again and that's the bit i was missing.

while handling U1, i found that get_placeholder_assumptions(U1) has type_outlives: []. the 'static assumption comes from the root param env, so alias_outlives_candidates_from_assumptions gives Or([]) here instead of RegionOutlives('static, '?0).

the rewrite then tries to move AliasTyOutlivesViaEnv(...: '?0) out of U1, but PlaceholderReplacer only replaces placeholders, not revars. '?0 stays in U1, the max_universe(candidate) < u check fails, and that adds Ambiguity. btw, there are still candidates that could work once this gets back to the root, but evaluate_solver_constraint sees the ambiguous branch and nothing that's already true in U1, so the whole Or becomes ambiguous before any of them get there.

normalization changes '?0 to the placeholder it's equated with first. PlaceholderReplacer can bind that, which lets the alias-outlives candidate escape U1. once it gets back to the root, the full env is there and the 'static assumption can be matched.

so imo your reasoning is right. if RegionOutlives('static, '?0) existed here, the later closure should handle it. that edge just never gets produced at this point because the root assumption isn't available yet.

idk if normalization is the nicest layer for this tbh, it does feel a little blunt to me. imo doing it before the rewrite still makes sense though, since that's where the equality and the type-outlives constraint are together in the same And. ltm if you want the raw trace bits :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the max_universe(candidate) < u check fails, and that adds Ambiguity

what do you mean by this? can you link me to the place that adds the ambiguity? as far as I'm aware we only add ambiguity when we have None assumptions for a binder (or when handling AliasTyOutlivesViaEnv during coherence)

evaluate_solver_constraint sees the ambiguous branch and nothing that's already true in U1, so the whole Or becomes ambiguous before any of them get there.

this also seems problematic to me 😅 I don't think that evaluating OR(ambig, 'a: 'b) should result in ambig that feels wrong 🤔 I would probably expect us to just keep that as is. I guess this isn't technically wrong but it does make us wildly conservative wrt considering things to be ambiguous when they might still be fine...

I would probably like us to fix this too 🤔

the rewrite then tries to move AliasTyOutlivesViaEnv(...: '?0) out of U1, but PlaceholderReplacer only replaces placeholders, not revars

I think this is something we should "just" fix 🤔 we should be rewriting <T as Trait>::Assoc: '?x_u1 to for<'a> <T as Trait>::Assoc: 'a when rewriting things in u1. Or more generally, we should never be rewriting type outlives constraints to different type outlives in the same universe. The max_universe should either be lowered or they should now be region outlives constraints

as a fix this is slightly conservative 🤔 which is unfortunate, but I would need to spend a lot more time thinking about this before adding some form of "resolving regions" (or normalization as you've called it).

@BoxyUwU BoxyUwU Jul 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx for your comment by the way it's cleared up a lot to me about things here (so much stuff is scuffed from my original impl of -Zassumptions-on-binders! lol!)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, this is the Ambiguity I meant in my previous comment: it's added here. PlaceholderReplacer doesn't touch '?0_u1, so the candidate is still in u1, the < u check fails, and that else pushes Ambiguity. fyi I wasn't saying alias_outlives_candidates_from_assumptions itself adds it, my wording there was a bit imprecise.

and yeah, I think you're right about the evaluator too. The rewrite also produces an AliasTyOutlivesViaEnv(...: 'static) candidate, which could work once it reaches the root and the full env is available. But evaluate_solver_constraint turns Or(Ambiguity, remaining_candidate) into Ambiguity before that happens. imo that's too eager, keeping the mixed Or around makes more sense so the other candidate still gets a chance at the root.

btw I agree that handling the current-universe revar in PlaceholderReplacer sounds cleaner than the normalization I added. tbh I like that quite a bit more since it directly rewrites <T as Trait>::Assoc: '?x_u1 into for<'a> <T as Trait>::Assoc: 'a and keeps the universe-escaping logic local. The normalization works for this case, but idk, walking the whole And and replacing equated vars feels a bit too broad.

would you prefer if I drop the normalization and make PlaceholderReplacer rewrite current-universe revars to fresh bound regions instead? also, should the mixed Or evaluation be fixed in this PR, or would you rather keep that as a follow-up? ltm what you think :)


// 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`
//
Expand Down Expand Up @@ -496,6 +500,152 @@ pub fn eagerly_handle_placeholders_in_universe<Infcx: InferCtxtLike<Interner = I
evaluate_solver_constraint(&constraint)
}

fn normalize_equated_region_vars<Infcx: InferCtxtLike<Interner = I>, I: Interner>(
infcx: &Infcx,
constraint: RegionConstraint<I>,
u: UniverseIndex,
) -> RegionConstraint<I> {
use RegionConstraint::*;

match constraint {
Ambiguity | RegionOutlives(..) | PlaceholderTyOutlives(..) | AliasTyOutlivesViaEnv(..) => {
constraint
}
Or(constraints) => Or(constraints
.into_iter()
.map(|constraint| normalize_equated_region_vars(infcx, constraint, u))
.collect()),
And(constraints) => {
let constraint = And(constraints
.into_iter()
.map(|constraint| normalize_equated_region_vars(infcx, constraint, u))
.collect());

let mut region_outlives = vec![];
collect_conjunctive_region_outlives(&constraint, &mut region_outlives);
let replacements = compute_equated_region_var_replacements(infcx, &region_outlives, u);

if replacements.is_empty() {
constraint
} else {
constraint.fold_with(&mut EquatedRegionVarReplacer { cx: infcx.cx(), replacements })
}
}
}
}

fn compute_equated_region_var_replacements<Infcx: InferCtxtLike<Interner = I>, I: Interner>(
infcx: &Infcx,
region_outlives: &[(Region<I>, Region<I>)],
u: UniverseIndex,
) -> Vec<(Region<I>, Region<I>)> {
compute_equated_region_var_replacements_from(
region_outlives,
|r| is_current_universe_region_var(infcx, r, u),
is_region_var::<I>,
)
}

fn compute_equated_region_var_replacements_from<R>(
region_outlives: &[(R, R)],
mut is_current_universe_region_var: impl FnMut(R) -> bool,
mut is_region_var: impl FnMut(R) -> bool,
) -> Vec<(R, R)>
where
R: Copy + Eq + std::hash::Hash,
{
let mut equated_regions_builder = TransitiveRelationBuilder::default();
let mut has_equated_regions = false;
for (r1, r2) in region_outlives.iter().copied() {
// Paired outlives constraints represent region equality. Build a transitive relation so
// current-universe variables equated through other variables still find a non-var partner.
if has_reverse_region_outlives_edge(region_outlives, r1, r2) {
equated_regions_builder.add(r1, r2);
equated_regions_builder.add(r2, r1);
has_equated_regions = true;
}
}

if !has_equated_regions {
return vec![];
}

let equated_regions = equated_regions_builder.freeze();
let mut candidates = IndexSet::new();
for (r1, r2) in region_outlives.iter().copied() {
if is_current_universe_region_var(r1) {
candidates.insert(r1);
}

if is_current_universe_region_var(r2) {
candidates.insert(r2);
}
}

candidates
.into_iter()
.filter_map(|candidate| {
std::iter::once(candidate)
.chain(equated_regions.reachable_from(candidate))
.find(|r| !is_region_var(*r))
.map(|partner| (candidate, partner))
})
.collect()
}

fn has_reverse_region_outlives_edge<R: Eq>(region_outlives: &[(R, R)], r1: R, r2: R) -> bool {
region_outlives.iter().any(|(outlives, outlived)| outlives == &r2 && outlived == &r1)
}

fn collect_conjunctive_region_outlives<I: Interner>(
constraint: &RegionConstraint<I>,
out: &mut Vec<(Region<I>, Region<I>)>,
) {
use RegionConstraint::*;

match constraint {
RegionOutlives(r1, r2) => out.push((*r1, *r2)),
And(constraints) => {
for constraint in constraints.iter() {
collect_conjunctive_region_outlives(constraint, out);
}
}
Ambiguity | PlaceholderTyOutlives(..) | AliasTyOutlivesViaEnv(..) | Or(..) => {}
}
}

fn is_current_universe_region_var<Infcx: InferCtxtLike<Interner = I>, I: Interner>(
infcx: &Infcx,
region: Region<I>,
u: UniverseIndex,
) -> bool {
is_region_var::<I>(region) && max_universe(infcx, region) == u
}

fn is_region_var<I: Interner>(region: Region<I>) -> bool {
matches!(region.kind(), RegionKind::ReVar(_))
}

struct EquatedRegionVarReplacer<I: Interner> {
cx: I,
replacements: Vec<(Region<I>, Region<I>)>,
}

impl<I: Interner> TypeFolder<I> for EquatedRegionVarReplacer<I> {
fn cx(&self) -> I {
self.cx
}

fn fold_region(&mut self, r: Region<I>) -> Region<I> {
// If a region variable has multiple non-var partners, the remaining folded
// constraints still relate those partners, so first-match only affects representation.
self.replacements.iter().find_map(|(from, to)| (*from == r).then_some(*to)).unwrap_or(r)
}
}

#[cfg(test)]
mod tests;

/// 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
Expand Down Expand Up @@ -657,6 +807,11 @@ fn pull_region_outlives_constraints_out_of_universe<
constraint
}
RegionOutlives(region_1, region_2) => {
if region_1 == region_2 {
// Reflexive constraints are always satisfied, even if the region is from `u`.
return RegionConstraint::new_true();
}

let region_1_u = max_universe(infcx, region_1);
let region_2_u = max_universe(infcx, region_2);

Expand Down
19 changes: 19 additions & 0 deletions compiler/rustc_type_ir/src/region_constraint/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use super::compute_equated_region_var_replacements_from;

#[test]
fn equated_region_var_replacements_follow_transitive_region_var_chains() {
const REVAR_1: u8 = 1;
const REVAR_2: u8 = 2;
const PLACEHOLDER: u8 = 3;

let region_outlives =
[(REVAR_1, REVAR_2), (REVAR_2, REVAR_1), (REVAR_2, PLACEHOLDER), (PLACEHOLDER, REVAR_2)];

let replacements = compute_equated_region_var_replacements_from(
&region_outlives,
|r| matches!(r, REVAR_1 | REVAR_2),
|r| matches!(r, REVAR_1 | REVAR_2),
);

assert_eq!(replacements, vec![(REVAR_1, PLACEHOLDER), (REVAR_2, PLACEHOLDER)]);
}
43 changes: 35 additions & 8 deletions compiler/rustc_type_ir/src/relate/solver_relating.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use tracing::{debug, instrument};

use self::combine::{PredicateEmittingRelation, super_combine_consts, super_combine_tys};
use crate::data_structures::DelayedSet;
use crate::region_constraint::RegionConstraint;
use crate::relate::combine::combine_ty_args;
pub use crate::relate::*;
use crate::solve::{Goal, VisibleForLeakCheck};
Expand Down Expand Up @@ -218,14 +219,40 @@ where

#[instrument(skip(self), level = "trace")]
fn regions(&mut self, a: Region<I>, b: Region<I>) -> RelateResult<I, Region<I>> {
match self.ambient_variance {
// Subtype(&'a u8, &'b u8) => Outlives('a: 'b) => SubRegion('b, 'a)
ty::Covariant => self.infcx.sub_regions(b, a, VisibleForLeakCheck::Yes, self.span),
// Suptype(&'a u8, &'b u8) => Outlives('b: 'a) => SubRegion('a, 'b)
ty::Contravariant => self.infcx.sub_regions(a, b, VisibleForLeakCheck::Yes, self.span),
ty::Invariant => self.infcx.equate_regions(a, b, VisibleForLeakCheck::Yes, self.span),
ty::Bivariant => {
unreachable!("Expected bivariance to be handled in relate_with_variance")
if self.cx().assumptions_on_binders() {
if a == b {
return Ok(a);
}

match self.ambient_variance {
ty::Covariant => self
.infcx
.register_solver_region_constraint(RegionConstraint::RegionOutlives(a, b)),
ty::Contravariant => self
.infcx
.register_solver_region_constraint(RegionConstraint::RegionOutlives(b, a)),
ty::Invariant => {
self.infcx
.register_solver_region_constraint(RegionConstraint::RegionOutlives(a, b));
self.infcx
.register_solver_region_constraint(RegionConstraint::RegionOutlives(b, a));
}
ty::Bivariant => {
unreachable!("Expected bivariance to be handled in relate_with_variance")
}
}
} else {
match self.ambient_variance {
ty::Covariant => self.infcx.sub_regions(b, a, VisibleForLeakCheck::Yes, self.span),
ty::Contravariant => {
self.infcx.sub_regions(a, b, VisibleForLeakCheck::Yes, self.span)
}
ty::Invariant => {
self.infcx.equate_regions(a, b, VisibleForLeakCheck::Yes, self.span)
}
ty::Bivariant => {
unreachable!("Expected bivariance to be handled in relate_with_variance")
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//@compile-flags: -Zassumptions-on-binders -Znext-solver=globally
//@ dont-require-annotations: ERROR

trait Super<U> {
fn a(&self) {
let a: &dyn Sub = &();
let b: &dyn Super<for<'a> fn(&'a ())> = a;
}
}

impl<T> Super<T> for () {}

trait Sub: Super<fn(&'static ())> {}

impl Sub for () {}

fn main() {
let a: &dyn Sub = &();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
error[E0277]: the trait bound `&dyn Sub: CoerceUnsized<&dyn Super<for<'a> fn(&'a ())>>` is not satisfied
--> $DIR/principal-upcast-region-eq-issue-157859.rs:7:49
|
LL | let b: &dyn Super<for<'a> fn(&'a ())> = a;
| ^ the nightly-only, unstable trait `Unsize<dyn Super<for<'a> fn(&'a ())>>` is not implemented for `dyn Sub`
|
= note: all implementations of `Unsize` are provided automatically by the compiler, see <https://doc.rust-lang.org/stable/std/marker/trait.Unsize.html> for more information
= note: required for `&dyn Sub` to implement `CoerceUnsized<&dyn Super<for<'a> fn(&'a ())>>`
= note: required for the cast from `&dyn Sub` to `&dyn Super<for<'a> fn(&'a ())>`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0277`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//@ compile-flags: -Zassumptions-on-binders -Znext-solver=globally

trait Super {
type Assoc;

fn a(&self) {
let a: &dyn Sub = &();
let b: &dyn Super<Assoc = for<'a> fn(&'a ())> = a;
//~^ ERROR the trait bound `&dyn Sub: CoerceUnsized<&dyn Super<Assoc = for<'a> fn(&'a ())>>` is not satisfied
}
}

impl Super for () {
type Assoc = fn(&'static ());
}

trait Sub: Super<Assoc = fn(&'static ())> {}

impl Sub for () {}

fn main() {
let a: &dyn Sub = &();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
error[E0277]: the trait bound `&dyn Sub: CoerceUnsized<&dyn Super<Assoc = for<'a> fn(&'a ())>>` is not satisfied
--> $DIR/trait-upcast-projection-region-eq.rs:8:57
|
LL | let b: &dyn Super<Assoc = for<'a> fn(&'a ())> = a;
| ^ the nightly-only, unstable trait `Unsize<dyn Super<Assoc = for<'a> fn(&'a ())>>` is not implemented for `dyn Sub`
|
= note: all implementations of `Unsize` are provided automatically by the compiler, see <https://doc.rust-lang.org/stable/std/marker/trait.Unsize.html> for more information
= note: required for `&dyn Sub` to implement `CoerceUnsized<&dyn Super<Assoc = for<'a> fn(&'a ())>>`
= note: required for the cast from `&dyn Sub` to `&dyn Super<Assoc = for<'a> fn(&'a ())>`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0277`.
Loading