Skip to content

WF checks on closure arguments and improved type-test promotion. - #151510

Open
LorrensP-2158466 wants to merge 3 commits into
rust-lang:mainfrom
LorrensP-2158466:wf-closure-arg
Open

LorrensP-2158466 wants to merge 3 commits into
rust-lang:mainfrom
LorrensP-2158466:wf-closure-arg

Conversation

@LorrensP-2158466

@LorrensP-2158466 LorrensP-2158466 commented Jan 22, 2026 •

Copy link
Copy Markdown
Contributor

View all comments

Fixes #104478.
Fixes #154267.

Enables WF checks on the arguments of a closure and improves type-test promotion.

The now removed function ascribe_user_type_skip_wf mentioned that skipping WF was done due to backwards compatibility reasons.

FCP below explains the type-test changes.

FCP: WF checks on closure arguments and better type-test propagation

Summary

On stable, we only perform WF-checks on closure arguments when that argument is actually used. This causes non WF code to still compile.

This was necessary as properly checking them for WF caused breakage due to a weakness of closure requirement propagation #104477, see #104478. As we've fixed #104477 in #148329 and in this PR, the remaining breakage is now all intended.

This change cause compilation errors when a used type is not WF. This is an intentional breakaging change.

Example

Consider the example from the first linked issue of this PR.

struct MyTy<T: Trait>(T);
trait Trait {}
impl Trait for &'static str {}
fn wf<T>(_: T) {}

fn test() {
    let _: for<'x> fn(MyTy<&'x str>) = |_| {};
    
    let _: for<'x> fn(MyTy<&'x str>) = |x| wf(x);
}

Currently, on stable, this fails only because of the second closure:

error[E0521]: borrowed data escapes outside of closure
 --> src/lib.rs:9:44
  |
9 |     let _: for<'x> fn(MyTy<&'x str>) = |x| wf(x);
  |                                         -  ^^^^^
  |                                         |  |
  |                                         |  `x` escapes the closure body here
  |                                         |  argument requires that `'1` must outlive `'static`
  |                                         `x` is a reference that is only valid in the closure body
  |                                         has type `MyTy<&'1 str>`

We don't emit an error for the first closure because the argument is not used in the closure body, even though it is also not WF.

With this PR we now error for both closures:

error: lifetime may not live long enough
  --> src/lib.rs:7:44
   |
LL |     let _: for<'x> fn(MyTy<&'x str>) = |_| {};
   |                                         ^
   |                                         |
   |                                         has type `MyTy<&'1 str>`
   |                                         requires that `'1` must outlive `'static`

error: lifetime may not live long enough
  --> src/lib.rs:9:44
   |
LL |     let _: for<'x> fn(MyTy<&'x str>) = |x| wf(x);
   |                                         ^
   |                                         |
   |                                         has type `MyTy<&'1 str>`
   |                                         requires that `'1` must outlive `'static`

Improved Type Test Promotion

When only enabling WF-checks on closure arguments, a regression was observed when compiling jobsteal: See the crater triage by @theemathas #151510 (comment).

This regression and possible others can be resolved by immproving type-test promotion, this is the second part of the PR. We've already changed closure requirement propagation for region outlive constraints, see #148329 for background here.

For a constraint like T: 'a, T is promoted first, replacing all free regions in T with 'static or equal region parameters, producing a constraint subject, free of closure-local regions - failing if any region has no such replacement.'a is promoted to the caller's context, we then find non-local universal regions that 'a outlives. On stable, we propagate T: 'ub for all non-local upper bounds of 'a.

These constraints are too conservative. We find some universal regions 'ur in the SCC of 'a, then for each of those 'ur we find their non-local upper bounds and propagate T: 'ub. Though, we actually need only one T: 'ub to prove T: 'ur. However, the compiler does not support OR constraints (e.g. T: 'ub1 OR T: 'ub2), so we require them all.

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 'a and we minimize the amount of OR requirements we propagate to the closure creator.

Example for Universal Regions Minization

Consider this program:

struct Arg<'a: 'c, 'b: 'c, 'c, T> {
    field: *mut (&'a (), &'b (), &'c (), T),
}

impl<'a, 'b, 'c, T> Arg<'a, 'b, 'c, T> {
    fn constrain(self)
    where
        T: 'a,
    {
    }
}

fn takes_closure<'a, 'b, T>(
    _: impl for<'c> FnOnce(Arg<'a, 'b, 'c, T>)
) {}

fn error<'a, 'b, T: 'a>() {
    takes_closure::<'a, 'b, T>(|arg| arg.constrain());
}

It currently fails to compile and gives the following error:

error[E0309]: the parameter type `T` may not live long enough
  --> src/lib.rs:20:38
   |
19 | fn error<'a, 'b, T: 'a>() {
   |              -- the parameter type `T` must be valid for the lifetime `'b` as defined here...
20 |     takes_closure::<'a, 'b, T>(|arg| arg.constrain());
   |                                      ^^^^^^^^^^^^^^^ ...so that the type `T` will meet its required lifetime bounds
   |
help: consider adding an explicit lifetime bound
   |
19 | fn error<'a, 'b, T: 'a + 'b>() {
   |                        ++++

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:

let r_scc = self.constraint_sccs.scc(lower_bound);
// ...
for ur in self.scc_values.universal_regions_outlived_by(r_scc) {

These are 'a and 'c, then for each of those regions, we find their non-local upper bounds 'ub and propagate T: 'ub:

  • for 'a, this is just 'a -> T: 'a
  • for 'c, these are 'a and 'b -> T: 'a OR T: 'b

T: 'a holds in the parent. We also propagate T: 'b on stable, resulting in an error.

This P 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 propagating T: 'b.

The reason we can do this "minimization" is somewhat trivial. If we need to prove T: '1 and T: '2, but '1: '2 holds, there is no reason to prove T: '2 as well.

Example for OR Requirement minimization

Say we introduce another type test on the first example:

struct Arg<'a: 'c, 'b: 'c, 'c, T> {
    field: *mut (&'a (), &'b (), &'c (), T),
}

impl<'a, 'b, 'c, T> Arg<'a, 'b, 'c, T> {
    fn constrain(self)
    where
        T: 'a,
        T: 'c,
    {
    }
}

fn takes_closure<'a, 'b, T>(_: impl for<'c> FnOnce(Arg<'a, 'b, 'c, T>)) {}

// We have `'a: 'c` and `'b: 'c`, requiring `T: 'a` in `constrain` should not need
// `T: 'b` here.
fn error<'a, 'b, T: 'a>() {
    takes_closure::<'a, 'b, T>(|arg| arg.constrain());
}

This T: 'c type-test now introduces an implicit OR requirement: the non-local upper bounds of 'c are ['a, 'b], thus propagating T: 'a OR T: 'b as two standalone requirements, both of which should hold. Even though our example shows that constrain already requires T: 'a, making T: 'b redundant.

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: 'a from type test T: 'a.
  • R2: T: 'a OR T: 'b from type test T: 'c.

With full requirement R1 AND R2 represented 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 removing R2, because it contains R1.

Note that we can be even smarter during this removal process, say we had following OR requirements:

  • R1: T: 'a
  • R2: T: 'b OR T: 'c

With full requirement R1 AND R2 and assumption 'a: 'b. This assumption would allow us to remove R2, because T: 'a implies T: 'b through the assumption.

Reference

The specifics of WF checking is not documented in the 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.

Coverage

See the tests added in this PR for things which now (don't) compile.

Outstanding bugs WF

There is a related unsoundness in #151637, which is not fixed by this PR. This is due to closures using implied bounds only involving external regions and needs to be fixed separately. See the WIP work in #153027.

Outstanding bugs type-test Promotion

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.

Breaking changes

This change is intentionally breaking: (TODO after crater).

History

WF checks on closure arguments was left as future work in #101947 and tracked in #104478.

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.

Open items

Rustc Dev Guide should be updated: type-outlive constraints.

r? @lcnr

@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 Jan 22, 2026
Comment on lines +10 to +19
error: lifetime may not live long enough
--> $DIR/check-wf-of-closure-args.rs:13:41
|
LL | let _: for<'x> fn(MyTy<&'x str>) = |x| wf(x);
| ^
| |
| has type `MyTy<&'1 str>`
| requires that `'1` must outlive `'static`

error: aborting due to 2 previous errors

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.

I find that the error message is a bit worse now, compared to the old:

error[E0521]: borrowed data escapes outside of closure
  --> src/lib.rs:10:44
   |
10 |     let _: for<'x> fn(MyTy<&'x str>) = |x| wf(x); // FAIL
   |                                         -  ^^^^^
   |                                         |  |
   |                                         |  `x` escapes the closure body here
   |                                         |  argument requires that `'1` must outlive `'static`
   |                                         `x` is a reference that is only valid in the closure body
   |                                         has type `MyTy<&'1 str>`

Comment thread tests/ui/wf/check-wf-of-closure-args-complex.rs Outdated
@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@LorrensP-2158466

Copy link
Copy Markdown
Contributor Author

CI x86_64-gnu-tools failed with an error I don't understand:

..............................F................... (50/146)
.................................................. (100/146)
..............................................     (146/146)
======== tests/rustdoc-gui/globals.goml ========
[ERROR] line 14: The following errors happened: [Property named `"searchIndex"` doesn't exist]: for command `assert-window-property-false: {"searchIndex": null}`
    at <file:///checkout/obj/build/x86_64-unknown-linux-gnu/test/rustdoc-gui/doc/test_docs/index.html?search=Foo>
======== tests/rustdoc-gui/search-result-display.goml ========
[WARNING] line 39: Delta is 0 for "x", maybe try to use `compare-elements-position` instead?

@lcnr

lcnr commented Jan 24, 2026

Copy link
Copy Markdown
Contributor

@bors try

@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Jan 24, 2026
@rust-bors

rust-bors Bot commented Jan 25, 2026

Copy link
Copy Markdown
Contributor

☀️ Try build successful (CI)
Build commit: 1baf923 (1baf923b9c3a455162afe43e18647f494c1a4b73, parent: 021fc25b7a48f6051bee1e1f06c7a277e4de1cc9)

@lcnr

lcnr commented Jan 26, 2026

Copy link
Copy Markdown
Contributor

@craterbot check

@craterbot

Copy link
Copy Markdown
Collaborator

👌 Experiment pr-151510 created and queued.
🤖 Automatically detected try build 1baf923
🔍 You can check out the queue and this experiment's details.

ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more

@craterbot craterbot added S-waiting-on-crater Status: Waiting on a crater run to be completed. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jan 26, 2026
@rust-bors

This comment has been minimized.

@craterbot

Copy link
Copy Markdown
Collaborator

🚧 Experiment pr-151510 is now running

ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more

@craterbot

Copy link
Copy Markdown
Collaborator

🎉 Experiment pr-151510 is completed!
📊 17 regressed and 6 fixed (795068 total)
📊 2202 spurious results on the retry-regressed-list.txt, consider a retry1 if this is a significant amount.
📰 Open the summary report.

⚠️ If you notice any spurious failure please add them to the denylist!
ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more

Footnotes

  1. re-run the experiment with crates=https://crater-reports.s3.amazonaws.com/pr-151510/retry-regressed-list.txt ↩

@craterbot craterbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-crater Status: Waiting on a crater run to be completed. labels Feb 3, 2026
@theemathas

Copy link
Copy Markdown
Contributor

@theemathas

theemathas commented Feb 3, 2026 •

Copy link
Copy Markdown
Contributor

Crater results:

The jobsteal regression seems to not directly involve closure arguments, which is strange. And the errors for the cw-storage-plus regression mention a lifetime '1 without pointing out where that lifetime is, which is confusing.

@lcnr

lcnr commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

@LorrensP-2158466 can you look into how to fix jobsteal without opening a PR yet. Also, I would like to FCP merge this change, are you interested in writing the FCP proposal here?

@LorrensP-2158466

Copy link
Copy Markdown
Contributor Author

@lcnr Will do, both!

Comment thread tests/ui/wf/check-wf-of-closure-args-complex.rs Outdated
Comment thread tests/ui/wf/check-wf-of-closure-args-complex.rs Outdated
@lcnr

This comment was marked as outdated.

@lcnr lcnr added the S-waiting-on-t-types Status: Awaiting decision from T-types label Jul 29, 2026
@LorrensP-2158466

LorrensP-2158466 commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor Author

does the following also break?

trait Bound<'a> {}
impl<'a> Bound<'a> for &'a str {}

struct Container<'a, K>
where
   K: Bound<'a>,
{
   _d: &'a [u8],
   _k: std::marker::PhantomData<K>,
}

fn main() {
   let _closure = |_: Container<&str>| {};
}

It does :(

error: lifetime may not live long enough
  --> lcnr_test.rs:13:21
   |
13 |     let _closure = |_: Container<&str>| {};
   |                     ^            - let's call the lifetime of this reference `'2`
   |                     |
   |                     has type `Container<'1, &str>`
   |                     requires that `'1` must outlive `'2`

error: lifetime may not live long enough
  --> lcnr_test.rs:13:21
   |
13 |     let _closure = |_: Container<&str>| {};
   |                     ^            - let's call the lifetime of this reference `'2`
   |                     |
   |                     has type `Container<'1, &str>`
   |                     requires that `'2` must outlive `'1`

error: aborting due to 2 previous errors

damn, this sucks xd

it does indeed.

@lcnr lcnr removed the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Jul 29, 2026
@LorrensP-2158466

This comment was marked as off-topic.

@rust-bors

This comment has been minimized.

@rustbot

This comment has been minimized.

@LorrensP-2158466

Copy link
Copy Markdown
Contributor Author

force-pushed because i had to rebase.

r? @oli-obk

Some things that have changed (i'll update the description once we have something concrete here):

WF checks

The first commit is still the introduction of WF checks on closure argument and return types, but now with all the "machinery" of collecting any errors produced by this WF checks and emitting them as FCW's instead. As discussed in #t-types/nominated > #151510: WF checks on closure arguments and improved type-t….

The gist of it is: we run borrowck twice, once with the WF checks enabled, if there are errors, we run it again without them enabled. We can then diff the errors and act accordingly.

Oh no, large diff alert

Because we run borrowck twice, the #[rustc_regions] and co are outputted twice as well. i kept them in as asked by @oli-obk.

improved type-test closure promotion

The second and third commit are still the same as the previous state of this pr and the work of #154271.

@rustbot rustbot assigned oli-obk and unassigned lcnr Sep 25, 2026
@rust-log-analyzer

This comment has been minimized.

@rust-bors

This comment has been minimized.

…m as FCW.

Runs borrowck in 2 modes to capture errors emitted due to WF checks on closure arg and return types, these are then emitted as FCWs.

We first run borrowck in the Yes mode and remember the emitted errors, then we run it again with WF checks turned off. We take the diff
off these errors and report them as FCW, the rest are the same and emitted as is.

+ add tests of issue
+ bless/change other tests
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.

+ filter redundant requirements through unit propagation
& bless test
@rustbot

rustbot commented Sep 28, 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.

@LorrensP-2158466

Copy link
Copy Markdown
Contributor Author

Had to rebase due to conflicts and failing tests with next-solver=globally. There are now some tests that have duplicate diagnostics because of this. I have a feeling its because of the same reason as #151510 (comment). (for example: tests/ui/unsized/issue-115203.rs)

@oli-obk

oli-obk commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

@rfcbot fcp cancel (not a breaking change anymore, though has an FCW, also gonna wait for next solver to be enabled on stable before merging this)

@rust-rfcbot

Copy link
Copy Markdown
Collaborator

@oli-obk proposal cancelled.

@rust-rfcbot rust-rfcbot removed proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. labels Oct 1, 2026
@theemathas theemathas added S-blocked Status: Blocked on something else such as an RFC or other implementation work. and removed S-waiting-on-t-types Status: Awaiting decision from T-types labels Oct 1, 2026
options: ConsumerOptions,
) -> FxHashMap<LocalDefId, BodyWithBorrowckFacts<'_>> {
let tainted_by_errors = Default::default();
// FIXME: run with `WfCheckClosures::FCW` as well?

@oli-obk oli-obk Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nah. custom driver users explicitly using this can just get the new experience 😆

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.

alright, thats clear 😆

}

Some(_) => root_cx.finalize(),
None => unreachable!("`WfCheckClosures::Yes` always returns `Some(errors)`"),

@oli-obk oli-obk Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think at that point you can just not return an Option, but just always return the errors list in both modes and avoid the match here

View changes since the review

fcw_reason: FutureIncompatibilityReason,
current_edition: Edition,
) {
self.level = Level::Warning(None);

@oli-obk oli-obk Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

assert that it was an error and mention so in the doc comment

View changes since the review

@rustbot rustbot added the S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. label Oct 1, 2026
@rust-bors

rust-bors Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

☔ The latest upstream changes (presumably #163609) made this pull request unmergeable. Please resolve the merge conflicts by rebasing.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

I-types-nominated Nominated for discussion during a types team meeting. S-blocked Status: Blocked on something else such as an RFC or other implementation work. S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-types Relevant to the types team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

unnecessary closure constraint propagation V2 closures accept ill-formed inputs overly restrictive closure borrow-checking

10 participants