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

WF checks on closure arguments and improved type-test promotion.#151510
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.

@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Jul 6, 2026
@rust-bors

rust-bors Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

☀️ Try build successful (CI)
Build commit: 02e1f20 (02e1f2052e11b05cb84518f9ef6209c23f3b3a00)
Base parent: 36714a9 (36714a9983d6ba11203d8bb87a1b372247fbcf06)

@theemathas

Copy link
Copy Markdown
Contributor

@craterbot check

@craterbot

Copy link
Copy Markdown
Collaborator

👌 Experiment pr-151510-1 created and queued.
🤖 Automatically detected try build 02e1f20
🔍 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 Jul 7, 2026
@lcnr

lcnr commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

@LorrensP-2158466 please update the FCP proposal for this PR (could wait until after the crater run, but don't expect anything surprising here)

@LorrensP-2158466

Copy link
Copy Markdown
Contributor Author

That will be a rather big FCP. I'll try to merge the 2 as best as I can, if you find the need to delete some details, go ahead!.

@LorrensP-2158466 LorrensP-2158466 changed the title WF checks on closure arguments. WF checks on closure arguments and improved type-test promotion. Jul 13, 2026
@craterbot

Copy link
Copy Markdown
Collaborator

🚧 Experiment pr-151510-1 is now running

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

@rust-bors

This comment has been minimized.

Done by removing the function that skipped the WF check on user types (used to skip it on closure args) and using the default one.

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
@LorrensP-2158466

Copy link
Copy Markdown
Contributor Author

rebased to fix conflict and updated FCP. Now waiting on crater so that i can finish the FCP.

@rustbot

rustbot commented Jul 20, 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.

@craterbot

Copy link
Copy Markdown
Collaborator

🎉 Experiment pr-151510-1 is completed!
📊 9 regressed and 0 fixed (1013799 total)
📊 3315 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-1/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 Jul 22, 2026
@LorrensP-2158466

Copy link
Copy Markdown
Contributor Author

Obligatory, never did a crater triage, so excuse me if this is done wrong 😅, below is the detailed triage, I'll give a reducer that reports the same kind of error.

So the root 9 regressions seem to be expected, they are a result of 2 crates that fail to compile because of the following:

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>| {};
}

With this pr, it fails like so:

error: lifetime may not live long enough
  --> repro.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
  --> repro.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

While it succeeds on nightly: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=7ad2eb0611bb068c3df9db34a9fa7660

I don't know enough of this part of the compiler to correctly say this should compile or not. What do you say @lcnr?

Detailed Triage (Click to expand)
  • root ghostscope-dwarf-0.1.5 fails because of this code:

    let mut collect_symbol = |symbol: object::Symbol<'_, '_, &[u8]>| {
        if symbol.kind() != SymbolKind::Text {
            return;
        }
    
        let Ok(name) = symbol.name() else {
            return;
        };
        by_name
            .entry(name.to_string())
            .or_default()
            .push(symbol.address());
    };

    Which fails like so:

    error: lifetime may not live long enough
        --> src/objfile/loading.rs:535:35
         |
     535 |         let mut collect_symbol = |symbol: object::Symbol<'_, '_, &[u8]>| {
         |                                   ^^^^^^                         - let's call the lifetime of this reference '2
         |                                   |
         |                                   has type object::Symbol<'1, '_, &[u8]>
         |                                   requires that '1 must outlive '2
     
     
     error: lifetime may not live long enough
        --> src/objfile/loading.rs:535:35
         |
     535 |         let mut collect_symbol = |symbol: object::Symbol<'_, '_, &[u8]>| {
         |                                   ^^^^^^                         - let's call the lifetime of this reference `'2`
         |                                   |
         |                                   has type `object::Symbol<'1, '_, &[u8]>`
         |                                   requires that `'2` must outlive `'1` 
    

    It looks like a valid error if I look at the object::Symbol struct:

    pub struct Symbol<'data, 'file, R = &'data [u8]>
    where
       R: ReadRef<'data>,
    { /* private fields */ }

    This seems like the user not adding explicit lifetimes to the closure:

    let mut collect_symbol = |symbol: object::Symbol<'d, '_, &'d [u8]>| { .. }
    // or even:
    let mut collect_symbol = |symbol: object::Symbol<'_, '_>| { .. }

    But to be honest, I do not know enough about this to be certain.

  • The 8 others fails because of the same kind of closure with a type that contains a lifetime parameter and a data field that contains that lifetime. This code in crate https://github.com/listeven989/cw-plus-for-pass/tree/master/packages/storage-plus:

     let name_count = |map: &IndexedMap<&[u8], Data, DataIndexes>,
                       store: &MemoryStorage,
                       name: &str| -> usize {
       map.idx
           .name
           .prefix(name.as_bytes().to_vec())
           .keys(store, None, None, Order::Ascending)
           .count()
    };

    Same kind of error as the first one:

    error: lifetime may not live long enough
       --> packages/storage-plus/src/indexed_map.rs:824:27
        |
    824 |         let name_count = |map: &IndexedMap<&str, Data, DataIndexes>,
        |                           ^^^              - let's call the lifetime of this reference `'2`
        |                           |
        |                           has type `&indexed_map::IndexedMap<'1, &str, indexed_map::test::Data, indexed_map::test::DataIndexes<'_>>`
        |                           requires that `'1` must outlive `'2`
    
    
    error: lifetime may not live long enough
       --> packages/storage-plus/src/indexed_map.rs:824:27
        |
    824 |         let name_count = |map: &IndexedMap<&str, Data, DataIndexes>,
        |                           ^^^              - let's call the lifetime of this reference `'2`
        |                           |
        |                           has type `&indexed_map::IndexedMap<'1, &str, indexed_map::test::Data, indexed_map::test::DataIndexes<'_>>`
        |                           requires that `'2` must outlive `'1`
    

    IndexedMap looks like this:

     pub struct IndexedMap<'a, K, T, I>
     where
         K: PrimaryKey<'a>,
         T: Serialize + DeserializeOwned + Clone,
         I: IndexList<T>,
     {
         pk_namespace: &'a [u8],
         primary: Map<'a, K, T>,
         /// This is meant to be read directly to get the proper types, like:
         /// map.idx.owner.items(...)
         pub idx: I,
     }
     
     // And `PrimaryKey<'a> and its implementors:
     pub trait PrimaryKey<'a>: Clone {
        type Prefix: Prefixer<'a>;
        type SubPrefix: Prefixer<'a>;
        // ...
     }
     
     impl<'a> PrimaryKey<'a> for &'a [u8] { ... }
     impl<'a> PrimaryKey<'a> for &'a str { ... }
     ```
     
     So again, the lifetimes should be explicit.

@LorrensP-2158466

Copy link
Copy Markdown
Contributor Author

So yeah, the compiler creates 3 regions:

    let _closure = |_: &'1 Container<'2, &'3 str>| {};

And does not "infer" that '2 and '3 should be the same so it reports 2 errors, saying that both '2: '3 and '3: '2 should be true.

Though, I don't know if the compiler should infer it, and I don't know how to make it compile by adding explicit lifetimes.

@LorrensP-2158466

LorrensP-2158466 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Also, with #![feature(closure_lifetime_binder)] #97362, we could create the closure like so (with everything explicit unfortunately:

fn main() {
    let _closure = for<'a, 'd> |_: &'a Container<'d, &'d str>| -> () {};
}

@lcnr

lcnr commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

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>| {};
}

And does not "infer" that '2 and '3 should be the same so it reports 2 errors, saying that both '2: '3 and '3: '2 should be true.

There is no real implied bound that'd make that compile :/ because the fact that &'a str only impls Bound<'a> is not guaranteed in the definition of Container 🤔

trait Bound<'a> {}
impl<'a> Bound<'a> for &'a str {}
struct Container<'a, K: Bound<'a>>(std::marker::PhantomData<(&'a (), K)>);
fn main() {
    let _ = |x: Container<'_, &str>| -> Container<'_, &str> { x };
}

already errors with the following on stable

error: lifetime may not live long enough
 --> src/main.rs:5:63
  |
5 |     let _ = |x: Container<'_, &str>| -> Container<'_, &str> { x };
  |              -                          -------------------   ^ returning this value requires that `'1` must outlive `'2`
  |              |                          |
  |              |                          return type of closure is Container<'2, &str>
  |              has type `Container<'1, &str>`

so does this snippet if we make the lifetimes invariant

trait Bound<'a> {}
impl<'a> Bound<'a> for &'a str {}
struct Container<'a, K: Bound<'a>>(std::marker::PhantomData<*mut (&'a (), K)>);
fn main() {
    let _ = |x: Container<'_, &str>| drop(x);
}
error: lifetime may not live long enough
 --> src/main.rs:5:38
  |
5 |     let _ = |x: Container<'_, &str>| drop(x);
  |              -                -      ^^^^^^^ argument requires that `'1` must outlive `'2`
  |              |                |
  |              |                let's call the lifetime of this reference `'2`
  |              has type `Container<'1, &str>`
  |
  = note: requirement occurs because of the type `Container<'_, &str>`, which makes the generic argument `'_` invariant
  = note: the struct `Container<'a, K>` is invariant over the parameter `'a`
  = help: see <https://doc.rust-lang.org/nomicon/subtyping.html> for more information about variance

damn, this sucks xd

@lcnr

lcnr commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

you can work around this with deduce_closure_signature but I don't want to force users to do so.

trait Bound<'a> {}
impl<'a> Bound<'a> for &'a str {}
struct Container<'a, K: Bound<'a>>(std::marker::PhantomData<*mut (&'a (), K)>);
fn id<F: for<'a> FnOnce(Container<'a, &'a str>)>(f: F) -> F { f }
fn main() {
    let _ = id(|x: Container<'_, &str>| drop(x)); // compiles
}

@lcnr lcnr added I-types-nominated Nominated for discussion during a types team meeting. S-waiting-on-t-types Status: Awaiting decision from T-types labels 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. I-types-nominated Nominated for discussion during a types team meeting. proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. S-waiting-on-t-types Status: Awaiting decision from T-types 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

9 participants