Rollup merge of #145194 - compiler-errors:coro-witness-re, r=lcnr

Ignore coroutine witness type region args in auto trait confirmation

## The problem

Consider code like:

```
async fn process<'a>() {
    Box::pin(process()).await;
}

fn require_send(_: impl Send) {}

fn main() {
    require_send(process());
}
```

When proving that the coroutine `{coroutine@process}::<'?0>: Send`, we end up instantiating a nested goal `{witness@process}::<'?0>: Send` by synthesizing a witness type from the coroutine's args:

Proving a coroutine witness type implements an auto trait requires looking up the coroutine's witness types. The witness types are a binder that look like `for<'r> { Pin<Box<{coroutine@process}::<'r>>> }`. We instantiate this binder with placeholders and prove `Send` on the witness types. This ends up eventually needing to prove something like `{coroutine@process}::<'!1>: Send`. Repeat this process, and we end up in an overflow during fulfillment, since fulfillment does not use freshening.

This can be visualized with a trait stack that ends up looking like:
* `{coroutine@process}::<'?0>: Send`
  * `{witness@process}::<'?0>: Send`
    * `Pin<Box<{coroutine@process}::<'!1>>>: Send`
      * `{coroutine@process}::<'!1>: Send`
        * ...
          * `{coroutine@process}::<'!2>: Send`
            * `{witness@process}::<'!2>: Send`
              * ...
                * overflow!

The problem here specifically comes from the first step: synthesizing a witness type from the coroutine's args.

## Why wasn't this an issue before?

Specifically, before 63f6845e57, this wasn't an issue because we were instead extracting the witness from the coroutine type itself. It turns out that given some `{coroutine@process}::<'?0>`, the witness type was actually something like `{witness@process}::<'erased>`!

So why do we end up with a witness type with `'erased` in its args? This is due to the fact that opaque type inference erases all regions from the witness. This is actually explicitly part of opaque type inference -- changing this to actually visit the witness types actually replicates this overflow even with 63f6845e57 reverted:

ca77504943/compiler/rustc_borrowck/src/type_check/opaque_types.rs (L303-L313)

To better understand this difference and how it avoids a cycle, if you look at the trait stack before 63f6845e57, we end up with something like:

* `{coroutine@process}::<'?0>: Send`
  * `{witness@process}::<'erased>: Send` **<-- THIS CHANGED**
    * `Pin<Box<{coroutine@process}::<'!1>>>: Send`
      * `{coroutine@process}::<'!1>: Send`
        * ...
          * `{coroutine@process}::<'erased>: Send` **<-- THIS CHANGED**
            * `{witness@process}::<'erased>: Send` **<-- THIS CHANGED**
              * coinductive cycle! 🎉

## So what's the fix?

This hack replicates the behavior in opaque type inference to erase regions from the witness type, but instead erasing the regions during auto trait confirmation. This is kinda a hack, but is sound. It does not need to be replicated in the new trait solver, of course.

---

I hope this explanation makes sense.

We could beta backport this instead of the revert https://github.com/rust-lang/rust/pull/145193, but then I'd like to un-revert that on master in this PR along with landing this this hack. Thoughts?

r? lcnr
This commit is contained in:
Stuart Cook
2025-08-11 18:22:33 +10:00
committed by GitHub
2 changed files with 29 additions and 2 deletions

View File

@@ -2333,10 +2333,23 @@ impl<'tcx> SelectionContext<'_, 'tcx> {
ty::Coroutine(def_id, args) => { ty::Coroutine(def_id, args) => {
let ty = self.infcx.shallow_resolve(args.as_coroutine().tupled_upvars_ty()); let ty = self.infcx.shallow_resolve(args.as_coroutine().tupled_upvars_ty());
let tcx = self.tcx();
let witness = Ty::new_coroutine_witness( let witness = Ty::new_coroutine_witness(
self.tcx(), tcx,
def_id, def_id,
self.tcx().mk_args(args.as_coroutine().parent_args()), ty::GenericArgs::for_item(tcx, def_id, |def, _| match def.kind {
// HACK: Coroutine witnesse types are lifetime erased, so they
// never reference any lifetime args from the coroutine. We erase
// the regions here since we may get into situations where a
// coroutine is recursively contained within itself, leading to
// witness types that differ by region args. This means that
// cycle detection in fulfillment will not kick in, which leads
// to unnecessary overflows in async code. See the issue:
// <https://github.com/rust-lang/rust/issues/145151>.
ty::GenericParamDefKind::Lifetime => tcx.lifetimes.re_erased.into(),
ty::GenericParamDefKind::Type { .. }
| ty::GenericParamDefKind::Const { .. } => args[def.index as usize],
}),
); );
ty::Binder::dummy(AutoImplConstituents { ty::Binder::dummy(AutoImplConstituents {
types: [ty].into_iter().chain(iter::once(witness)).collect(), types: [ty].into_iter().chain(iter::once(witness)).collect(),

View File

@@ -0,0 +1,14 @@
// Regression test for <https://github.com/rust-lang/rust/issues/145151>.
//@ edition: 2024
//@ check-pass
async fn process<'a>() {
Box::pin(process()).await;
}
fn require_send(_: impl Send) {}
fn main() {
require_send(process());
}