Better closure requirement propagation V2#154271
Conversation
try_promote_type_test_subject + add a test.
Yeah, let me write down my thoughts here. There are actually multiple things here. We can prove What we currently do on stable and what I didn't even think of, is that if we have Separately, if we have Now, if we know that we'll separately propagate We do have access to Separately, we could handle type tests as a two step pass. First collect all type tests which have a single possible candidate, then go through all the ones with multiple options and check whether one of the options is already part of the list of definitely required type tests. Could also do that separately/not handle that part for now 🤷 |
|
Thanks for the explanation, i understand now.
I am with you here, that's what this PR is currently doing right?
Yes right, I was thinking in the same direction, this is the part where i am stuck because a clean solution didn't come to me. I only came up with the following myself:
But this seems a bit unnatural to me, i prefer the other one:
I find that this equates to finding the "minimal required" type tests. Which we seem to resort to in these closure propagation pr's.
I will try both solutions anyway, you never know :). i'll create separate branches in my fork and create a channel on zullip to discuss them. If you want it done in another way, lmk! |
This comment has been minimized.
This comment has been minimized.
05a43d7 to
657862c
Compare
Fix in question: We only propagate `T: 'ub` requirements when 'ub actually outlives the lower bounds of the type test. If none of the non-local upper bounds outlive it, then we propagate all of them.
This comment has been minimized.
This comment has been minimized.
657862c to
d27d602
Compare
Yeah, nevermind, the 2-phase way is much easier to implement and is more logical (at least to me :)). When trying to define the problem it reminded me of Unit Propagation. Basically given a boolean expression in conjunctive normal form (i.e boolean expressions with AND operators between them), try to simplify the expressions by "propagating unit expressions". Filtering these OR requirements in conjunctive normal form can be reduced to this Unit Propagation problem, but it is simpler, the subexpressions only contain OR operators, so the full expression is true if at least one unit is true. Thus, Given your example:
Then the full requirement (expression) is This can be done in 1 pass, as the code does. And it compiles your harder example successfully. @rustbot ready |
|
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. |
This comment has been minimized.
This comment has been minimized.
| or_requirement.len() == 1 | ||
| || !or_requirement.iter().any(|r| { | ||
| let key = requirement_key(*r); | ||
| units.contains(&key) |
There was a problem hiding this comment.
could go even stronger, if you have any unit clause T: 'a and later have some T: 'b || T: 'c with an assumption 'a: 'b we can also drop that requirement, might be cool to write a test for that
There was a problem hiding this comment.
Oh yeah, that's true.
Implementing it is straightforward (i think for now), but actually creating a test is quite hard. Just extending your example didn't produce errors that i need, i am still bad at this 😅 .
There was a problem hiding this comment.
Nevermind, I think I got it, just had to draw it out first, that helped.
I made an extra commit with the change and test.
|
@bors try @rust-timer queue |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…=<try> Better closure requirement propagation V2
This comment has been minimized.
This comment has been minimized.
|
Finished benchmarking commit (8d9f715): comparison URL. Overall result: no relevant changes - no action neededBenchmarking means the PR may be perf-sensitive. Consider adding rollup=never if this change is not fit for rolling up. @rustbot label: -S-waiting-on-perf -perf-regression Instruction countThis perf run didn't have relevant results for this metric. Max RSS (memory usage)Results (secondary -7.6%)A less reliable metric. May be of interest, but not used to determine the overall result above.
CyclesResults (secondary 0.2%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Binary sizeResults (secondary -0.1%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Bootstrap: 487.764s -> 489.197s (0.29%) |
|
Gladly! |
|
With an FCP proposal written by @LorrensP-2158466 @rfcbot fcp merge types |
|
@lcnr has proposed to merge this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See this document for info about what commands tagged team members can give me. |
View all comments
Fixes #154267
Unblocks #151510.
Summary
Background
While we borrowck closures and their parent body in the same query to handle opaque types, each body is otherwise handled separately. When checking closures (and const blocks), we don't know the region constraints of the parent body, so we don't know anything about non-late bound regions of the closure.
These regions are represented as external universal regions of the closure, e.g. in
When borrow checking the closure, it's signature is
for<'anon> fn(&'anon &'ext0 str) -> &'ext1 str)and it has an upvar of type&'ext2 str.Because of its signature it can assume that
'ext0: 'anonholds. It does not know anything else about its external regions. Notably even though the expected signature will use'atwice, we still use two separate external regions here.Borrow checking the closure now requires
'ext0: 'ext1because of the return statement and'ext2: 'ext0because of the assignment. We don't know whether these outlive requirements hold and we propagate them to the parent function instead.If both regions we have to propagate are already external, that's easy. However, we may also have to handle constraints which reference regions internal to the closure. In this case, we can't propagate the constraint to the caller, as the caller cannot name these regions of the closure. In these cases we propagate constrains which imply the constraint we actually have to prove.
E.g. in the above example if the closure ended up requiring
'ext2: 'anonwe can't propagate this to the caller directly. However, as we know that'ext0: 'anonholds, we can instead propagate'ext2: 'ext0which then transitively implies'ext2: 'anon.Given
longer_fr: shorter_frwe need to propagate at least one constraintfr-: shorter_fr+for which the closure knows thatlonger_fr: longer_fr-andshorter_fr+: 'shorter_frhold.The behavior on stable
For a constraint like
T: 'a,Tis promoted first, replacing all free regions inTwith'staticor equal region parameters, producing a constraint subject, free of closure-local regions - failing if any region has no such replacement.'ais promoted to the caller's context, we then find non-local universal regions that'aoutlives. Then for each such region'ub, we propagateT: 'ub.The behavior of this PR
There are 2 changes made in this PR relevant to the current behaviour on stable: We minimize the amount of universal regions found for
'aand we minimize the amount of OR requirements we propagate to the closure creator.Example for Universal Regions Minization
Consider this program:
It currently fails to compile and gives the following error:
The closure tries to prove
T: 'a, but it can't, so it tries to promote it to the creator.On stable, fn try_promote_type_test finds all universal regions in the constraint graph for the lower bound
'a:These are
'aand'c, then for each of those regions, we find their non-local upper bounds'uband propagateT: 'ub:'a, this is just'a->T: 'a'c, these are'aand'b->T: 'aORT: 'bClearly, there is no outlives relation between
'aand'bthat requiresT: 'bto proveT: 'a.This pr first minimizes the regions returned by
self.scc_values.universal_regions_outlived_by(r_scc)by removing any region already outlived by another region in the set. For the example above we would get['a, 'c], which we would be minimized to['a], because'c: 'a. Thus never propagatingT: 'b.The reason we can do this "minimization" is somewhat trivial. If we need to prove
T: '1andT: '2, but'1: '2holds, there is no reason to proveT: '2as well.Example for OR Requirement minimization
Say we introduce another type test on the first example:
This
T: 'ctype-test now introduces an implicit OR requirement: the non-local upper bounds of'care['a, 'b], thus propagatingT: 'a OR T: 'bas two standalone requirements, both of which should hold. Even though our example shows thatconstrainalready requiresT: 'a, makingT: 'bredundant.This PR changes the way these requirements are propagated, essentially propagating a list of requirements for each univeral regions. This list is seen as a collection of OR requirements and if we have multiple such lists, we require all of them to hold. In our above example this would result in 2 OR requirements:
R1:T: 'afrom type testT: 'a.R2:T: 'a OR T: 'bfrom type testT: 'c.With full requirement
R1 AND R2represented by[R1, R2]. We then loop over these OR requirements and collect all "unit" requirements (R1 above). We then loop again over the OR requirements and remove every OR requirement which contains at least one unit requirement. For our example, this would result in removingR2, because it containsR1.Note that we can be even smarter during this removal process, say we had following OR requirements:
R1:T: 'aR2:T: 'b OR T: 'cWith full requirement
R1 AND R2and assumption'a: 'b. This assumption would allow us to removeR2, becauseT: 'aimpliesT: 'bthrough the assumption.Design
Reference
We currently don't document borrowck in the reference. Once we do this, this is PR requires a localized change to how we propagate requirements from nested bodies.
Other
RFC history, Answers to unresolved questions, Post-RFC changes, Key points, Nightly extensions, Doors closed, Feedback, Call for testing, Nightly use: n/a
Implementation n/a see summary
Major parts n/a
Coverage
All examples shown above are covered as tests in this PR.
Outstanding bugs
We sometimes still propagate multiple constraints even though a single one would be sufficient, potentially causing unnecessary errors.
To avoid any such errors here, we'd probably have to support OR constraints. There may also be some future improvements to closure constraint propagation, but 🤷 we want to avoid introducing non-determinstic or surprising behavior here.
Outstanding FIXMEs n/a
Tool changes n/a
Breaking changes n/a
Type system, opsem
Compile-time checks n/a
Type system rules n/a
Sound by default? n/a
Breaks the AM? n/a
Common interactions
Temporaries n/a
Drop order n/a
Pre-expansion / post-expansion n/a
Edition hygiene n/a
SemVer implications n/a
Exposing other features n/a
History
Propagating closure requirements was originally implemented by @nikomatsakis in #46509 and then improved by @matthewjasper in #58347.
It was then improved by @LorrensP-2158466 in #148329.
Acknowledgments
Open items
Rustc Dev Guide should be updated: type-outlive constraints.
r? @lcnr