Skip to content

trait solver: Handle reflexive region constraints - #161988

Open
Dnreikronos wants to merge 7 commits into
rust-lang:mainfrom
Dnreikronos:trait_solver/reflexive_region_constraints
Open

trait solver: Handle reflexive region constraints#161988
Dnreikronos wants to merge 7 commits into
rust-lang:mainfrom
Dnreikronos:trait_solver/reflexive_region_constraints

Conversation

@Dnreikronos

@Dnreikronos Dnreikronos commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

View all comments

Fixes rust-lang/project-assumptions-on-binders#19

A reflexive placeholder constraint like 'a: 'a can show up after we compute transitive region constraints. We then try to pull it out of the current universe by looking for lower-universe candidates. With none to choose from it becomes false, even though 'a: 'a was true the whole time.

I first had this buried in the binder region work, which made a green test pretty meaningless because too much else was changing around it. Split out on its own, the fix is just to accept equal regions before doing the universe rewrite. I think that is the sensible place to stop: it handles the direct case and the reflexive edges made by the transitive pass. I added a regression for the direct case.

cc/ @BoxyUwU o/

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Aug 29, 2026
@rustbot

rustbot commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

r? @JohnTitor

rustbot has assigned @JohnTitor.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: compiler
  • compiler expanded to 75 candidates
  • Random selection from 21 candidates

@Dnreikronos
Dnreikronos marked this pull request as draft August 29, 2026 20:12
@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver) and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Aug 29, 2026
@Dnreikronos

Dnreikronos commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Sup @BoxyUwU and @adwinwhite o/
I noticed #161963 has the same reflexive check as the first commit in this PR, so that part overlaps. I pushed a few more changes here because I don't think that check covers the whole problem in rust-lang/project-assumptions-on-binders#19.

The first check handles 'a: 'a while we're leaving the placeholder's universe. That check is still right, it just fires too early for the other case I was looking at. In the small syn repro, the type outlives constraint only gets taken apart once we're back at the root, so the earlier check never sees the reflexive region constraint.

When I followed that path, some information was missing from the assumptions used at the root. The implied I: '_ from &'_ self was in region_bound_pairs, but it wasn't included when we destructured the solver constraints. An assumption such as &'b u8: 'a also didn't always give this code the 'b: 'a relation.

There's a similar problem while leaving a binder. If the binder's own assumptions already prove 'b: 'a, we still try to rewrite it into a lower universe. When there are no lower-universe candidates, that turns a true constraint into Or([]). The new code checks whether the assumptions already prove it before doing that rewrite.

At the root, a RegionOutlives('a, 'a) produced by destructuring is now treated as true too. That's the part that gets the syn minimization through.

I added tests for direct and transitive assumptions, type outlives assumptions, and the root case. The full assumptions_on_binders UI directory passes locally, 17 tests, and tidy passes too. I haven't tested every affected case listed in #19 yet, so I don't want to claim more than I've checked.

My take is that #161963 is a good small fix for #161733, while this PR now deals with the wider path behind #19. I would keep the extra work here, but I'm not sure where you want the PR boundary. Would you rather keep it this way or move the extra changes to a follow-up on #161963?

@BoxyUwU BoxyUwU assigned BoxyUwU and unassigned JohnTitor Aug 30, 2026
@Dnreikronos
Dnreikronos marked this pull request as ready for review August 31, 2026 22:40
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Aug 31, 2026
Comment thread compiler/rustc_infer/src/infer/outlives/obligations.rs Outdated
Comment thread compiler/rustc_type_ir/src/region_constraint.rs Outdated

@BoxyUwU BoxyUwU left a comment

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.

// 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(),

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.

Do one of your tests cover needing this? It's not clear to me why we need this special case of exactly 'a: 'a in evaluate when we also have the more general fix in pull_region_outlives_constraints

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.

Yep, there is a test for this now. I replaced the old syn case since that also needed the assumption work moved to #162238.

The new test is a small root case with T: 'a + 'b and a T: 'b constraint. Root destructuring makes an OR with an unrelated 'a: 'b candidate and the reflexive 'b: 'b candidate. The general check in pull_region_outlives_constraints_out_of_universe never gets a chance to handle this because the root does not leave a universe. Regionck also still flattens that OR like an AND, so evaluate has to notice 'b: 'b and make the OR true before that happens.

I removed the evaluator branch once to check the test, and it fails with the lifetime bound error. So I kept this bit, but rewrote the test and comment around the root case. In my opinion it still fits here: same reflexive constraint bug, just a path that does not call the general helper.

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.

ah right lol. that makes sense

Comment thread compiler/rustc_type_ir/src/region_constraint.rs Outdated
@rustbot

This comment has been minimized.

// 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 => {}

@BoxyUwU BoxyUwU Sep 4, 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.

Want to instead move this into construction of And, i.e. filter out any leaf constraints of the form 'a: 'a 🤔 Then you can drop all of the changes to this function

Feels more appropriately placed than handling region outlives in the type outlives handling function. And I do think in general we probably just don't want to be having 'a: 'a constraints littered all over the place since they're kind useless

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.

Yep, done. And::new drops any 'a: 'a leaf now, and all the changes to destructure_type_outlives_constraints_in_root are gone, it's back to what it was.

I did have to add one more thing for that to actually work. Or::new returns true as soon as it sees an empty And. Without it the root test still fails, because the reflexive candidate becomes And([]) and you end up with Or([And([]), And([X])]), which isn't is_true(). Regionck still flattens ORs like ANDs, so it would go and ask for X anyway. Seems like the right canonical form regardless, an empty AND is true so the OR is true.

I also pointed Or::new_leaf and RegionConstraint::new_leaf at And::new/Or::new. They were building the boxes by hand, so a reflexive leaf could slip past the filter there.

I like this better than what I had. The special casing in the root function never really felt like it belonged in the type outlives arm, and having one rule about what an AND is allowed to contain is easier to keep in my head.

There's one small side effect. test-infra-fails-properly was using 'c: 'c as the deliberately wrong candidate in its expect clause, and that canonicalizes to true now, so the error changed shape. I swapped it for 'b: 'c so the test still shows a mismatched candidate instead of true vs false. If you'd rather just bless the new output and leave the test alone, say so and I'll flip it.

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.
@Dnreikronos
Dnreikronos force-pushed the trait_solver/reflexive_region_constraints branch from 6709071 to ca6e6f0 Compare September 7, 2026 13:40
@rustbot

rustbot commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

/// 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

@BoxyUwU BoxyUwU Sep 8, 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.

Can you add a FIXME(-Zassumptions-on-binders) that we should consider doing something more general and dropping ands which are supersets of other ands. E.g. OR(AND('a: 'b), AND('a: 'b, 'b: 'c)) really ought to just drop the second AND

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.

Added, right under the canonical form list.

I wrote it so the empty AND rule reads as the small case of the same idea. An empty AND is a subset of everything, so a general superset check gives you the "if any AND is empty it is the only AND" line for free. Felt odd listing them as two separate rules once I noticed that.

Agree on the general point though. Keeping AND('a: 'b, 'b: 'c) around next to AND('a: 'b) is just more for regionck to walk through for nothing.

let mut new_ands: Vec<And<I, S>> = Vec::new();

for and in ands {
// An empty AND is trivially true, which makes the whole OR true no matter what the

@BoxyUwU BoxyUwU Sep 8, 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.

@Dnreikronos Dnreikronos Sep 8, 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.

Added, on the dedup check since that's the code you'd actually touch to do it.

Funny thing I ran into while writing it: the subset check is already half of is_and_equivalent_to. That does containment both ways, so "is a superset of" is just dropping one of the two all calls. So it's not a cost thing.

The reason I didn't just do it is that it isn't only a filter. Right now I skip an AND if it matches one I already pushed. For the general rule I'd also have to go back and drop ANDs I already pushed when a smaller one turns up later, since iteration order decides which one you see first. That's a real change to the loop and I'd rather not sneak it into a PR about reflexive constraints.

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.

Yeah good to do as a separate PR. cc rust-lang/project-assumptions-on-binders#47

@BoxyUwU

BoxyUwU commented Sep 8, 2026

Copy link
Copy Markdown
Member

@rustbot author

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Sep 8, 2026
@rustbot

rustbot commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Reminder, once the PR becomes ready for a review, use @rustbot ready.

@Dnreikronos

Copy link
Copy Markdown
Contributor Author

@rustbot ready

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Sep 8, 2026
@BoxyUwU

BoxyUwU commented Sep 8, 2026

Copy link
Copy Markdown
Member

sick, I'll r+ this once CI is green.

JonathanBrouwer added a commit to JonathanBrouwer/rust that referenced this pull request Sep 8, 2026
…utlives_assumptions, r=BoxyUwU

trait solver: Include implied outlives assumptions

Part of rust-lang/project-assumptions-on-binders#19

Split out of rust-lang#161988 after @BoxyUwU pointed out that these are about which assumptions we keep, not really about reflexive region constraints.

I went back through where each piece comes from and found two gaps. Inside a binder we kept `Ty: 'a`, but the region relation only knew about explicit region clauses. That means something like `&'b T: 'a` did not also give us `'b: 'a`. At the root it was a slightly different version of the same problem: `known_type_outlives` has the explicit where clauses, while implied bounds from things like `&'b self` live in `region_bound_pairs`, so constraint destructuring never saw them.

`Assumptions::new` now pulls the free region components out of type outlives clauses and adds those edges to the region relation. I think doing it there is the cleanest spot. All callers get the same view of an assumption, and the original type clauses stay around for placeholder and alias cases. Regions bound inside the type are ignored because they do not name anything we can use outside that binder.

The root path now adds its implied type bounds to the same assumption set before destructuring. The regression uses an implied `I: 'b` from a receiver and a separate `'b: 'a` relation, so it covers this without leaning on the reflexive fix from rust-lang#161988. There are also binder checks for a reference and a higher-ranked function type. Those caught an easy testing trap here: a green direct constraint could have depended on the other PR, so the checks look at the lifted candidates instead.

Personally, I think splitting this was the right call. It is really a change to how assumption data is built, and that is easier to reason about on its own than under the reflexive constraint fix.

cc @BoxyUwU, this is the pair of changes you asked me to pull out.
JonathanBrouwer added a commit to JonathanBrouwer/rust that referenced this pull request Sep 8, 2026
…utlives_assumptions, r=BoxyUwU

trait solver: Include implied outlives assumptions

Part of rust-lang/project-assumptions-on-binders#19

Split out of rust-lang#161988 after @BoxyUwU pointed out that these are about which assumptions we keep, not really about reflexive region constraints.

I went back through where each piece comes from and found two gaps. Inside a binder we kept `Ty: 'a`, but the region relation only knew about explicit region clauses. That means something like `&'b T: 'a` did not also give us `'b: 'a`. At the root it was a slightly different version of the same problem: `known_type_outlives` has the explicit where clauses, while implied bounds from things like `&'b self` live in `region_bound_pairs`, so constraint destructuring never saw them.

`Assumptions::new` now pulls the free region components out of type outlives clauses and adds those edges to the region relation. I think doing it there is the cleanest spot. All callers get the same view of an assumption, and the original type clauses stay around for placeholder and alias cases. Regions bound inside the type are ignored because they do not name anything we can use outside that binder.

The root path now adds its implied type bounds to the same assumption set before destructuring. The regression uses an implied `I: 'b` from a receiver and a separate `'b: 'a` relation, so it covers this without leaning on the reflexive fix from rust-lang#161988. There are also binder checks for a reference and a higher-ranked function type. Those caught an easy testing trap here: a green direct constraint could have depended on the other PR, so the checks look at the lifted candidates instead.

Personally, I think splitting this was the right call. It is really a change to how assumption data is built, and that is easier to reason about on its own than under the reflexive constraint fix.

cc @BoxyUwU, this is the pair of changes you asked me to pull out.
rust-bors Bot pushed a commit that referenced this pull request Sep 9, 2026
Rollup merge of #162238 - Dnreikronos:trait_solver/implied_outlives_assumptions, r=BoxyUwU

trait solver: Include implied outlives assumptions

Part of rust-lang/project-assumptions-on-binders#19

Split out of #161988 after @BoxyUwU pointed out that these are about which assumptions we keep, not really about reflexive region constraints.

I went back through where each piece comes from and found two gaps. Inside a binder we kept `Ty: 'a`, but the region relation only knew about explicit region clauses. That means something like `&'b T: 'a` did not also give us `'b: 'a`. At the root it was a slightly different version of the same problem: `known_type_outlives` has the explicit where clauses, while implied bounds from things like `&'b self` live in `region_bound_pairs`, so constraint destructuring never saw them.

`Assumptions::new` now pulls the free region components out of type outlives clauses and adds those edges to the region relation. I think doing it there is the cleanest spot. All callers get the same view of an assumption, and the original type clauses stay around for placeholder and alias cases. Regions bound inside the type are ignored because they do not name anything we can use outside that binder.

The root path now adds its implied type bounds to the same assumption set before destructuring. The regression uses an implied `I: 'b` from a receiver and a separate `'b: 'a` relation, so it covers this without leaning on the reflexive fix from #161988. There are also binder checks for a reference and a higher-ranked function type. Those caught an easy testing trap here: a green direct constraint could have depended on the other PR, so the checks look at the lifted candidates instead.

Personally, I think splitting this was the right call. It is really a change to how assumption data is built, and that is easier to reason about on its own than under the reflexive constraint fix.

cc @BoxyUwU, this is the pair of changes you asked me to pull out.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

we dont handle 'a: 'a eagerly in some cases (?)

4 participants