Skip to content

Desugar async blocks in HIR instead of MIR#157309

Open
cjgillot wants to merge 16 commits into
rust-lang:mainfrom
cjgillot:coroutine-hir-desugar
Open

Desugar async blocks in HIR instead of MIR#157309
cjgillot wants to merge 16 commits into
rust-lang:mainfrom
cjgillot:coroutine-hir-desugar

Conversation

@cjgillot

@cjgillot cjgillot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

View all comments

Implements MCP rust-lang/compiler-team#997
Based on #157166

In the current implementation, gen/async/async gen blocks and closures have type Coroutine(..) and CoroutineClosure(..). Those types implement Iterator, Future or AsyncIterator depending on the initial desugaring.

This creates a lot of complexity:

  • trait solvers must check which kind of coroutine each time;
  • MIR StateTransform needs to fixup types depending on the coroutine kind.

I propose to change the desugaring for coroutines to:

  • gen { .. } becomes CoroutineIterator::from_coroutine(#[coroutine] { .. });
  • async { .. } becomes CoroutineFuture::from_coroutine(#[coroutine] { .. });
  • async gen { .. } becomes CoroutineAsyncIterator::from_coroutine(#[coroutine] { .. }).

This way, all coroutines implement std::ops::Coroutine and core is responsible for translating this to user-friendly traits. All the complexity is pushed to error-reporting code, which is not soundness-critical.

Coroutine closures are a little more complex, as we need to keep the CoroutineClosure type for borrow-checking.

Main design point: I create two methods on TyCtxt that are meant to do the back-and-forth between wrapped and unwrapped coroutines. coroutine_desugared_type wraps a coroutine inside the adapter struct. try_unwrap_desugared_coroutine unwraps it.

r? @oli-obk
cc @lcnr @RalfJung
cc @estebank as I modify quite a lot of diagnostic code

@rustbot rustbot added PG-exploit-mitigations Project group: Exploit mitigations 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-libs Relevant to the library team, which will review and decide on the PR/issue. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver) labels Jun 2, 2026
@rust-log-analyzer

This comment has been minimized.

@rustbot rustbot added the T-clippy Relevant to the Clippy team. label Jun 2, 2026
@Noratrieb

Copy link
Copy Markdown
Member

cc @Swatinem as you have done work on this in the past

@cjgillot cjgillot force-pushed the coroutine-hir-desugar branch from 3aee865 to d17eeb1 Compare June 2, 2026 15:13
@cjgillot

cjgillot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@bors try @rust-timer queue

@rust-timer

This comment has been minimized.

@rustbot rustbot added the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Jun 2, 2026
@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Jun 2, 2026
Desugar async blocks in HIR instead of MIR
@rust-log-analyzer

This comment has been minimized.

@rust-bors

rust-bors Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

☀️ Try build successful (CI)
Build commit: ec00386 (ec003861e71dff1f92a4835792378f93fac0d0d7, parent: 48f976c7131e76b6a1ba6ba316c90d97ffdfe184)

@rust-timer

This comment has been minimized.

@rust-timer

Copy link
Copy Markdown
Collaborator

Finished benchmarking commit (ec00386): comparison URL.

Overall result: ❌✅ regressions and improvements - please read:

Benchmarking means the PR may be perf-sensitive. It's automatically marked not fit for rolling up. Overriding is possible but disadvised: it risks changing compiler perf.

Next, please: If you can, justify the regressions found in this try perf run in writing along with @rustbot label: +perf-regression-triaged. If not, fix the regressions and do another perf run. Neutral or positive results will clear the label automatically.

@bors rollup=never
@rustbot label: -S-waiting-on-perf +perf-regression

Instruction count

Our most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.

mean range count
Regressions ❌
(primary)
0.3% [0.3%, 0.3%] 2
Regressions ❌
(secondary)
6.3% [0.1%, 25.0%] 32
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-0.2% [-0.3%, -0.0%] 14
All ❌✅ (primary) 0.3% [0.3%, 0.3%] 2

Max RSS (memory usage)

Results (primary -0.2%, secondary 0.9%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
4.8% [3.0%, 6.6%] 2
Regressions ❌
(secondary)
3.3% [1.5%, 7.6%] 9
Improvements ✅
(primary)
-3.5% [-8.0%, -0.6%] 3
Improvements ✅
(secondary)
-3.4% [-5.2%, -1.0%] 5
All ❌✅ (primary) -0.2% [-8.0%, 6.6%] 5

Cycles

Results (secondary 6.3%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
- - 0
Regressions ❌
(secondary)
8.1% [2.4%, 23.5%] 23
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-4.4% [-8.8%, -1.9%] 4
All ❌✅ (primary) - - 0

Binary size

Results (primary 0.1%, secondary 6.4%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
0.1% [0.0%, 0.2%] 13
Regressions ❌
(secondary)
6.4% [0.0%, 15.0%] 26
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
- - 0
All ❌✅ (primary) 0.1% [0.0%, 0.2%] 13

Bootstrap: 510.698s -> 509.663s (-0.20%)
Artifact size: 400.57 MiB -> 400.84 MiB (0.07%)

@rustbot rustbot added perf-regression Performance regression. and removed S-waiting-on-perf Status: Waiting on a perf run to be completed. labels Jun 2, 2026

@estebank estebank left a comment

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.

Just had a fast scan, have a bunch of nitpicks.

View changes since this review

Comment on lines +21 to +31
--> $DIR/issue-76168-hr-outlives-3.rs:13:1
|
LL | / {
LL | |
LL | |
LL | | let mut i = 41;
LL | | &mut i;
LL | | }
| |_^ expected an `FnOnce(&'a mut i32)` closure, found `i32`
|
= help: the trait `for<'a> FnOnce(&'a mut i32)` is not implemented for `i32`

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.

Not sure why the later error isn't marked as duplicate automatically. I don't see any visual difference between the two. It'd be a shame to start producing redundant errors here. (but so far things look reasonable in general.)

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 suspect that deduplication sees different SyntaxContexts because spans are marked as desugared. This should be fixed in a separate PR.

Comment on lines 10 to +19
error[E0038]: the trait `Foo` is not dyn compatible
--> $DIR/inference_var_self_argument.rs:5:33
--> $DIR/inference_var_self_argument.rs:5:34
|
LL | async fn foo(self: &dyn Foo) {
| ^ `Foo` is not dyn compatible
LL | async fn foo(self: &dyn Foo) {
| __________________________________^
LL | |
LL | |
LL | | todo!()
LL | | }
| |_____^ `Foo` is not dyn compatible

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.

We should have a better span (ideally pointing at dyn Foo). Errors that point at the whole body are bad when presenting information in IDEs, as then you end up with a whole lot of text with a red underline, generally masking other errors.

Comment on lines +51 to +52
= note: no two async blocks, even if identical, have the same type
= help: consider pinning your async block and casting it to a trait object

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.

These two are redundant with the ones immediately above.

| ----- ^^^^^^^^
| | |
| | expected `async` block, found a different `async` block
| | arguments to this function are incorrect

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.

We need to customize the E0308 logic for function calls to account for this new desugaring, as talking about "arguments to this function" when pointing at an async block are less than ideal. Just not including the label would be enough., but ideally we'd make the previous output whihch points at fun still work even with the additional level of indirection.

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 don't think I know how to do that.
IIUC, the error arises while checking call to to future_from_coroutine in the second argument. The first argument provides an expected type, which propagates to future_from_coroutine's own argument, causing a mismatch there. Fixing this would mean tracking the origin of the expectation in typeck.

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 believe you might be able to accomplish that just with using an appropriate DesugaringKind on the Span.

Comment thread tests/ui/async-await/issues/issue-63388-1.stderr

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.

This is a problem: we are no longer pointing at user code in the recursion error. There's no way a normal user can figure out what caused there to be a problem with the new output.

@rust-bors

This comment has been minimized.

@cjgillot cjgillot force-pushed the coroutine-hir-desugar branch from d17eeb1 to d3fbd81 Compare June 3, 2026 22:03
@rust-log-analyzer

This comment has been minimized.

@cjgillot cjgillot force-pushed the coroutine-hir-desugar branch from d3fbd81 to c1714a2 Compare June 4, 2026 12:11
@rust-log-analyzer

This comment has been minimized.

@rust-bors

This comment has been minimized.

@cjgillot

cjgillot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

Analysis of perf report:

  • icount change is very small on primary benchmarks, including hyper which is the only one using async fn;
  • large icount regressions on 3 secondary benchmarks: await-call-tree, deeply-nested-multi and issue-88862, which are 3 stress tests with deep await chains, so regression is expected and happens mostly in typeck;
  • same thing with binary size, which was also to be expected.

I'm tempted to consider these results as the cost for -573 lines of code.

@cjgillot cjgillot force-pushed the coroutine-hir-desugar branch from c1714a2 to f4e0c0b Compare June 5, 2026 09:53
@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Jun 14, 2026
Desugar async blocks in HIR instead of MIR



Implements MCP rust-lang/compiler-team#997
Based on #157166

In the current implementation, `gen`/`async`/`async gen` blocks and closures have type `Coroutine(..)` and `CoroutineClosure(..)`. Those types implement `Iterator`, `Future` or `AsyncIterator` depending on the initial desugaring.

This creates a lot of complexity:
- trait solvers must check which kind of coroutine each time;
- MIR StateTransform needs to fixup types depending on the coroutine kind.

I propose to change the desugaring for coroutines to:
- `gen { .. }` becomes `CoroutineIterator::from_coroutine(#[coroutine] { .. })`;
- `async { .. }` becomes `CoroutineFuture::from_coroutine(#[coroutine] { .. })`;
- `async gen { .. }` becomes `CoroutineAsyncIterator::from_coroutine(#[coroutine] { .. })`.

This way, all coroutines implement `std::ops::Coroutine` and `core` is responsible for translating this to user-friendly traits. All the complexity is pushed to error-reporting code, which is not soundness-critical.

Coroutine closures are a little more complex, as we need to keep the `CoroutineClosure` type for borrow-checking.

Main design point: I create two methods on `TyCtxt` that are meant to do the back-and-forth between wrapped and unwrapped coroutines. `coroutine_desugared_type` wraps a coroutine inside the adapter struct. `try_unwrap_desugared_coroutine` unwraps it.

r? @oli-obk 
cc @lcnr @RalfJung 
cc @estebank as I modify quite a lot of diagnostic code
@rust-log-analyzer

This comment has been minimized.

@rust-bors rust-bors Bot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. labels Jun 15, 2026
@rust-bors

rust-bors Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

💔 Test for e72d173 failed: CI. Failed job:

@cjgillot cjgillot force-pushed the coroutine-hir-desugar branch from 19482b7 to 0534a7b Compare June 16, 2026 00:22
@rust-bors rust-bors Bot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jun 16, 2026
@rustbot

This comment has been minimized.

@rust-bors

This comment has been minimized.

cjgillot added 16 commits June 16, 2026 22:55
Debug-printing MIR is not readable. We have dedicated infra to dump MIR,
use it.

Added benefit, this avoid hardcoding that `AddMovesForPackedDrops` is the
first pass in make_shim (it is not).
Closures, coroutines, coroutine witnesses and coroutine closures use the
same syntax. This used to be copy-paste but gradually departed.
@cjgillot cjgillot force-pushed the coroutine-hir-desugar branch from 0534a7b to 3b65567 Compare June 16, 2026 22:58
@rustbot

rustbot commented Jun 16, 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.

@rust-log-analyzer

Copy link
Copy Markdown
Collaborator

The job x86_64-gnu-pre-stabilization failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
---- [ui] tests/ui/traits/next-solver/async.rs#fail stdout ----
Saved the actual stderr to `/checkout/obj/build/x86_64-unknown-linux-gnu/test/ui/traits/next-solver/async.fail/async.fail.stderr`
diff of stderr:

- error[E0271]: expected `{async block@$DIR/async.rs:12:17: 12:22}` to be a future that resolves to `i32`, but it resolves to `()`
+ error[E0271]: expected `{async block@async.rs:12:17}` to be a future that resolves to `i32`, but it resolves to `()`
2   --> $DIR/async.rs:12:17
3    |
4 LL |     needs_async(async {});


The actual stderr differed from the expected stderr
To update references, rerun the tests and pass the `--bless` flag
To only update this specific test, also pass `--test-args traits/next-solver/async.rs`

error in revision `fail`: 1 errors occurred comparing output.
status: exit status: 1
command: env -u RUSTC_LOG_COLOR RUSTC_ICE="0" RUST_BACKTRACE="short" "/checkout/obj/build/x86_64-unknown-linux-gnu/stage1/bin/rustc" "/checkout/tests/ui/traits/next-solver/async.rs" "-Zthreads=1" "-Zsimulate-remapped-rust-src-base=/rustc/FAKE_PREFIX" "-Ztranslate-remapped-path-to-local-path=no" "-Z" "ignore-directory-in-diagnostics-source-blocks=/cargo" "-Z" "ignore-directory-in-diagnostics-source-blocks=/checkout/vendor" "--sysroot" "/checkout/obj/build/x86_64-unknown-linux-gnu/stage1" "--target=x86_64-unknown-linux-gnu" "--cfg" "fail" "--check-cfg" "cfg(test,FALSE,pass,fail)" "--error-format" "json" "--json" "future-incompat" "-Ccodegen-units=1" "-Zui-testing" "-Zdeduplicate-diagnostics=no" "-Zwrite-long-types-to-disk=no" "-Cstrip=debuginfo" "--emit" "metadata" "-C" "prefer-dynamic" "--out-dir" "/checkout/obj/build/x86_64-unknown-linux-gnu/test/ui/traits/next-solver/async.fail" "-A" "unused" "-W" "unused_attributes" "-A" "internal_features" "-A" "incomplete_features" "-A" "unused_parens" "-A" "unused_braces" "-Crpath" "-Cdebuginfo=0" "-Lnative=/checkout/obj/build/x86_64-unknown-linux-gnu/native/rust-test-helpers" "--edition=2021" "-Znext-solver"
stdout: none
--- stderr -------------------------------
error[E0271]: expected `{async block@async.rs:12:17}` to be a future that resolves to `i32`, but it resolves to `()`
##[error]  --> /checkout/tests/ui/traits/next-solver/async.rs:12:17
   |
LL |     needs_async(async {});
   |     ----------- ^^^^^^^^ expected `i32`, found `()`
   |     |
   |     required by a bound introduced by this call
   |
note: required by a bound in `needs_async`
  --> /checkout/tests/ui/traits/next-solver/async.rs:8:31
   |
LL | fn needs_async(_: impl Future<Output = i32>) {}
   |                               ^^^^^^^^^^^^ required by this bound in `needs_async`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0271`.

For more information how to resolve CI failures of this job, visit this link.

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

Labels

perf-regression Performance regression. PG-exploit-mitigations Project group: Exploit mitigations S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-clippy Relevant to the Clippy team. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants