-
-
Notifications
You must be signed in to change notification settings - Fork 15.5k
trait solver: Handle reflexive region constraints #161988
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c98b786
f88e474
5cbee6e
fdc2acb
4b73b07
ca6e6f0
b532a13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -165,6 +165,13 @@ impl<I: Interner, S: Clone + std::fmt::Debug + Eq + std::hash::Hash> 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 | ||
| /// | ||
| /// FIXME(-Zassumptions-on-binders): We should consider a more general canonical form which also | ||
| /// drops any AND that is a superset of another AND. Proving the superset requires strictly more | ||
| /// than proving the subset, so it can never be the candidate which makes the OR hold. E.g. | ||
| /// `OR(AND('a: 'b), AND('a: 'b, 'b: 'c))` really ought to just be `OR(AND('a: 'b))`. Only keeping | ||
| /// an empty AND is the degenerate case of that rule. | ||
| pub struct Or<I: Interner, S: Clone + std::fmt::Debug = ()>(pub Box<[And<I, S>]>); | ||
| impl<I: Interner> Or<I> { | ||
| pub fn with_spans<S: Clone + std::fmt::Debug + Eq + std::hash::Hash>( | ||
|
|
@@ -204,6 +211,17 @@ impl<I: Interner, S: Clone + std::hash::Hash + std::fmt::Debug + Eq> Or<I, S> { | |
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here :3
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| // 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(); | ||
| } | ||
|
|
||
| // FIXME(-Zassumptions-on-binders): We only discard an AND which is equivalent to one | ||
| // we already have. More generally we should discard any AND which is a superset of | ||
| // another, as it requires strictly more to hold. E.g. the second AND in | ||
| // `OR(AND('a: 'b), AND('a: 'b, 'b: 'c))` is never the one which makes the OR true. | ||
| if new_ands.iter().all(|c| !c.is_and_equivalent_to(&and)) { | ||
| new_ands.push(and) | ||
| } | ||
|
|
@@ -217,7 +235,7 @@ impl<I: Interner, S: Clone + std::hash::Hash + std::fmt::Debug + Eq> Or<I, S> { | |
| } | ||
|
|
||
| pub fn new_leaf(l: LeafRegionConstraint<I, S>) -> Self { | ||
| Or(Box::new([And(Box::new([l]))])) | ||
| Or::new([And::new([l])]) | ||
| } | ||
|
|
||
| pub fn build_and(a: Or<I, S>, b: Or<I, S>) -> Self { | ||
|
|
@@ -249,6 +267,7 @@ impl<I: Interner, S: Clone + std::hash::Hash + std::fmt::Debug + Eq> Or<I, S> { | |
| #[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<I: Interner, S: Clone + std::fmt::Debug = ()>(pub Box<[LeafRegionConstraint<I, S>]>); | ||
| impl<I: Interner> And<I> { | ||
| pub fn with_spans<S: Clone + std::fmt::Debug + Eq + std::hash::Hash>( | ||
|
|
@@ -264,6 +283,15 @@ impl<I: Interner, S: Clone + std::hash::Hash + std::fmt::Debug + Eq> And<I, S> { | |
| 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 +437,7 @@ impl<I: Interner, S: Clone + std::fmt::Debug + Eq + std::hash::Hash> RegionConst | |
| } | ||
|
|
||
| pub fn new_leaf(l: LeafRegionConstraint<I, S>) -> Self { | ||
| RegionConstraint { and_constraint: And(Box::new([l])), or_constraint: Or::new_true() } | ||
| RegionConstraint { and_constraint: And::new([l]), or_constraint: Or::new_true() } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -661,6 +689,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) | ||
|
|
@@ -992,7 +1028,6 @@ pub fn regions_outlived_by<I: Interner>( | |
| r: Region<I>, | ||
| assumptions: &Assumptions<I>, | ||
| ) -> impl Iterator<Item = Region<I>> { | ||
| // FIXME(-Zassumptions-on-binders): do we need to be adding the reflexive edge here? | ||
| assumptions.region_outlives.reachable_from(r).into_iter().chain([r]) | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| //@ check-pass | ||
| //@ compile-flags: -Zassumptions-on-binders | ||
|
|
||
| #![feature(test_binder_constraints, non_lifetime_binders)] | ||
| #![expect(incomplete_features)] | ||
|
|
||
| // 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. 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 | ||
| } | ||
| } | ||
|
|
||
| fn main() {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() {} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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 secondANDView changes since the review
There was a problem hiding this comment.
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 toAND('a: 'b)is just more for regionck to walk through for nothing.