Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Properly mark loop as diverging if it has no breaks #128443

Merged
merged 1 commit into from
Aug 1, 2024

Conversation

compiler-errors
Copy link
Member

@compiler-errors compiler-errors commented Jul 31, 2024

Due to specifics about the desugaring of the .await operator, HIR typeck doesn't recognize that .awaiting an impl Future<Output = !> will diverge in the same way as calling a fn() -> !.

This is because the await operator desugars to approximately:

loop {
    match future.poll(...) {
        Poll::Ready(x) => break x,
        Poll::Pending => {}
    }
}

We know that the value of x is !, however since break is a coercion site, we coerce ! to some ?0 (the type of the loop expression). Then since the type of the loop {...} expression is ?0, we will not detect the loop as diverging like we do with other expressions that evaluate to !:

// Any expression that produces a value of type `!` must have diverged
if ty.is_never() {
self.diverges.set(self.diverges.get() | Diverges::always(expr.span));
}

We can technically fix this in two ways:

  1. Make coercion of loop exprs more eagerly result in a type of ! when the only break expressions have type !.
  2. Make loops understand that if they have only diverging break values, then the loop diverges as well.

(1.) likely has negative effects on inference, and seems like a weird special case to drill into coercion. However, it turns out that (2.) is very easy to implement, we already record whether a loop has any break expressions, and when we do so, we actually skip over any break expressions with diverging values!:

// If we encountered a `break`, then (no surprise) it may be possible to break from the
// loop... unless the value being returned from the loop diverges itself, e.g.
// `break return 5` or `break loop {}`.
ctxt.may_break |= !self.diverges.get().is_always();

Thus, we can consider the loop as diverging if we see that it has no breaks, which is the change implemented in this PR.

This is not usually a problem in regular code for two reasons:

  1. In regular code, we already mark break diverging() as unreachable if diverging() is unreachable. We don't do this for .await, since we suppress unreachable errors within .await (Silence unreachable code lint from await desugaring #64930). Un-suppressing this code will result in spurious unreachable expression errors pointing to internal await machinery.
  2. In loops that truly have no breaks (e.g. loop {}), we already evaluate the type of the loop to !, so this special case is kinda moot. This only affects loops that have breaks with values of type !.

Thus, this seems like a change that may affect more code than just .await, but it likely does not in meaningful ways; if it does, it's certainly correct to apply.

Fixes #128434

@rustbot
Copy link
Collaborator

rustbot commented Jul 31, 2024

r? @fmease

rustbot has assigned @fmease.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@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 Jul 31, 2024
@@ -48,30 +48,42 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
/// Produces warning on the given node, if the current point in the
/// function is unreachable, and there hasn't been another warning.
pub(crate) fn warn_if_unreachable(&self, id: HirId, span: Span, kind: &str) {
Copy link
Member Author

@compiler-errors compiler-errors Jul 31, 2024

Choose a reason for hiding this comment

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

I rewrote this method to split up the 3 special cases and properly document them. No functional changes other than:

orig_span.is_desugaring(DesugaringKind::Await)

to

span.is_desugaring(DesugaringKind::Await)

Since the former suppresses any unreachable warnings originating from an await, but we actually want to suppress any unreachable warnings that occur within an await.

Copy link
Member

@fmease fmease left a comment

Choose a reason for hiding this comment

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

Very well-written explanation, thanks!

@fmease
Copy link
Member

fmease commented Jul 31, 2024

@bors r+

@bors
Copy link
Contributor

bors commented Jul 31, 2024

📌 Commit 74754b8 has been approved by fmease

It is now in the queue for this repository.

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 31, 2024
@compiler-errors
Copy link
Member Author

compiler-errors commented Jul 31, 2024

I consider this to be strictly a bugfix, and therefore deserves no team approval, but I do think that it's worth mentioning in the relnotes just in case a crate maintainer notices that f().await now marks subsequent statements as unused due to more accurately tracking divergence.

@rustbot label +relnotes

For relnotes: The compiler now triggers the unreachable code warning properly for async functions that don't return/are -> !.

@rustbot rustbot added the relnotes Marks issues that should be documented in the release notes of the next release. label Jul 31, 2024
tgross35 added a commit to tgross35/rust that referenced this pull request Jul 31, 2024
… r=fmease

Properly mark loop as diverging if it has no breaks

Due to specifics about the desugaring of the `.await` operator, HIR typeck doesn't recognize that `.await`ing an `impl Future<Output = !>` will diverge in the same way as calling a `fn() -> !`.

This is because the await operator desugars to approximately:

```rust
loop {
    match future.poll(...) {
        Poll::Ready(x) => break x,
        Poll::Pending => {}
    }
}
```

We know that the value of `x` is `!`, however since `break` is a coercion site, we coerce `!` to some `?0` (the type of the loop expression). Then since the type of the `loop {...}` expression is `?0`, we will not detect the loop as diverging like we do with other expressions that evaluate to `!`:

https://github.com/rust-lang/rust/blob/0b5eb7ba7bd796fb39c8bb6acd9ef6c140f28b65/compiler/rustc_hir_typeck/src/expr.rs#L240-L243

We can technically fix this in two ways:
1. Make coercion of loop exprs more eagerly result in a type of `!` when the only break expressions have type `!`.
2. Make loops understand that all of that if they have only diverging break values, then the loop diverges as well.

(1.) likely has negative effects on inference, and seems like a weird special case to drill into coercion. However, it turns out that (2.) is very easy to implement, we already record whether a loop has any break expressions, and when we do so, we actually skip over any break expressions with diverging values!:

https://github.com/rust-lang/rust/blob/0b5eb7ba7bd796fb39c8bb6acd9ef6c140f28b65/compiler/rustc_hir_typeck/src/expr.rs#L713-L716

Thus, we can consider the loop as diverging if we see that it has no breaks, which is the change implemented in this PR.

This is not usually a problem in regular code for two reasons:
1. In regular code, we already mark `break diverging()` as unreachable if `diverging()` is unreachable. We don't do this for `.await`, since we suppress unreachable errors within `.await` (rust-lang#64930). Un-suppressing this code will result in spurious unreachable expression errors pointing to internal await machinery.
3. In loops that truly have no breaks (e.g. `loop {}`), we already evaluate the type of the loop to `!`, so this special case is kinda moot. This only affects loops that have `break`s with values of type `!`.

Thus, this seems like a change that may affect more code than just `.await`, but it likely does not in meaningful ways; if it does, it's certainly correct to apply.

Fixes rust-lang#128434
bors added a commit to rust-lang-ci/rust that referenced this pull request Jul 31, 2024
Rollup of 8 pull requests

Successful merges:

 - rust-lang#127567 (std: implement the `once_wait` feature)
 - rust-lang#127624 (Migrate and rename `issue-47551`, `issue-35164` and `issue-69368` `run-make` tests to rmake)
 - rust-lang#128162 (Cleanup sys module to match house style)
 - rust-lang#128437 (improve bootstrap to allow selecting llvm tools individually)
 - rust-lang#128443 (Properly mark loop as diverging if it has no breaks)
 - rust-lang#128449 (Temporarily switch `ambiguous_negative_literals` lint to allow)
 - rust-lang#128450 (Create COFF archives for non-LLVM backends)
 - rust-lang#128452 (derive(SmartPointer): require pointee to be maybe sized)

r? `@ghost`
`@rustbot` modify labels: rollup
bors added a commit to rust-lang-ci/rust that referenced this pull request Aug 1, 2024
…iaskrgr

Rollup of 6 pull requests

Successful merges:

 - rust-lang#127567 (std: implement the `once_wait` feature)
 - rust-lang#128162 (Cleanup sys module to match house style)
 - rust-lang#128296 (Update target-spec metadata for loongarch64 targets)
 - rust-lang#128443 (Properly mark loop as diverging if it has no breaks)
 - rust-lang#128449 (Temporarily switch `ambiguous_negative_literals` lint to allow)
 - rust-lang#128452 (derive(SmartPointer): require pointee to be maybe sized)

r? `@ghost`
`@rustbot` modify labels: rollup
@bors bors merged commit ac67d10 into rust-lang:master Aug 1, 2024
6 checks passed
@rustbot rustbot added this to the 1.82.0 milestone Aug 1, 2024
rust-timer added a commit to rust-lang-ci/rust that referenced this pull request Aug 1, 2024
Rollup merge of rust-lang#128443 - compiler-errors:async-unreachable, r=fmease

Properly mark loop as diverging if it has no breaks

Due to specifics about the desugaring of the `.await` operator, HIR typeck doesn't recognize that `.await`ing an `impl Future<Output = !>` will diverge in the same way as calling a `fn() -> !`.

This is because the await operator desugars to approximately:

```rust
loop {
    match future.poll(...) {
        Poll::Ready(x) => break x,
        Poll::Pending => {}
    }
}
```

We know that the value of `x` is `!`, however since `break` is a coercion site, we coerce `!` to some `?0` (the type of the loop expression). Then since the type of the `loop {...}` expression is `?0`, we will not detect the loop as diverging like we do with other expressions that evaluate to `!`:

https://github.com/rust-lang/rust/blob/0b5eb7ba7bd796fb39c8bb6acd9ef6c140f28b65/compiler/rustc_hir_typeck/src/expr.rs#L240-L243

We can technically fix this in two ways:
1. Make coercion of loop exprs more eagerly result in a type of `!` when the only break expressions have type `!`.
2. Make loops understand that all of that if they have only diverging break values, then the loop diverges as well.

(1.) likely has negative effects on inference, and seems like a weird special case to drill into coercion. However, it turns out that (2.) is very easy to implement, we already record whether a loop has any break expressions, and when we do so, we actually skip over any break expressions with diverging values!:

https://github.com/rust-lang/rust/blob/0b5eb7ba7bd796fb39c8bb6acd9ef6c140f28b65/compiler/rustc_hir_typeck/src/expr.rs#L713-L716

Thus, we can consider the loop as diverging if we see that it has no breaks, which is the change implemented in this PR.

This is not usually a problem in regular code for two reasons:
1. In regular code, we already mark `break diverging()` as unreachable if `diverging()` is unreachable. We don't do this for `.await`, since we suppress unreachable errors within `.await` (rust-lang#64930). Un-suppressing this code will result in spurious unreachable expression errors pointing to internal await machinery.
3. In loops that truly have no breaks (e.g. `loop {}`), we already evaluate the type of the loop to `!`, so this special case is kinda moot. This only affects loops that have `break`s with values of type `!`.

Thus, this seems like a change that may affect more code than just `.await`, but it likely does not in meaningful ways; if it does, it's certainly correct to apply.

Fixes rust-lang#128434
@Kobzol
Copy link
Contributor

Kobzol commented Aug 2, 2024

@rust-timer build ffc0e2e

@rust-timer

This comment has been minimized.

@rust-timer
Copy link
Collaborator

Finished benchmarking commit (ffc0e2e): comparison URL.

Overall result: ❌ regressions - ACTION NEEDED

Benchmarking this pull request likely means that it is perf-sensitive, so we're automatically marking it as not fit for rolling up. While you can manually mark this PR as fit for rollup, we strongly recommend not doing so since this PR may lead to changes in compiler perf.

Next Steps: If you can justify the regressions found in this try perf run, please indicate this with @rustbot label: +perf-regression-triaged along with sufficient written justification. If you cannot justify the regressions please fix the regressions and do another perf run. If the next run shows neutral or positive results, the label will be automatically removed.

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

Instruction count

This is a highly reliable metric that was used to determine the overall result at the top of this comment.

mean range count
Regressions ❌
(primary)
0.4% [0.2%, 0.7%] 20
Regressions ❌
(secondary)
1.0% [0.2%, 2.1%] 36
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
- - 0
All ❌✅ (primary) 0.4% [0.2%, 0.7%] 20

Max RSS (memory usage)

Results (primary -3.3%, secondary 2.9%)

This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.

mean range count
Regressions ❌
(primary)
- - 0
Regressions ❌
(secondary)
2.9% [2.9%, 2.9%] 1
Improvements ✅
(primary)
-3.3% [-3.3%, -3.3%] 1
Improvements ✅
(secondary)
- - 0
All ❌✅ (primary) -3.3% [-3.3%, -3.3%] 1

Cycles

Results (primary 2.7%, secondary 3.3%)

This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.

mean range count
Regressions ❌
(primary)
2.7% [1.4%, 8.1%] 7
Regressions ❌
(secondary)
3.3% [3.2%, 3.4%] 2
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
- - 0
All ❌✅ (primary) 2.7% [1.4%, 8.1%] 7

Binary size

This benchmark run did not return any relevant results for this metric.

Bootstrap: 757.595s -> 759.601s (0.26%)
Artifact size: 336.69 MiB -> 336.74 MiB (0.02%)

@rustbot rustbot added the perf-regression Performance regression. label Aug 2, 2024
@compiler-errors
Copy link
Member Author

?????????????????

@compiler-errors
Copy link
Member Author

Oh wait, I probably know what caused it. I changed the warn_if_unreachable function.

@Kobzol
Copy link
Contributor

Kobzol commented Aug 2, 2024

Seems like a bit more time is spent in typeck. Not sure if this was expected, but this seems like a correctness fix, and the regression in primary benchmarks is only in check builds, so nothing that terrible. If you have any ideas how to fix, would be nice to send a PR, otherwise we can probably let it slide.

bors added a commit to rust-lang-ci/rust that referenced this pull request Aug 2, 2024
…able, r=<try>

Check divergence value first before doing span operations in `warn_if_unreachable`

It's more expensive to extract the span's desugaring first rather than check the value of the divergence enum. For some reason I inverted these checks, probably for readability, but as a consequence I regressed perf:

rust-lang#128443 (comment)

r? fmease
@compiler-errors compiler-errors deleted the async-unreachable branch August 3, 2024 14:10
bors added a commit to rust-lang-ci/rust that referenced this pull request Aug 4, 2024
…able, r=fmease

Check divergence value first before doing span operations in `warn_if_unreachable`

It's more expensive to extract the span's desugaring first rather than check the value of the divergence enum. For some reason I inverted these checks, probably for readability, but as a consequence I regressed perf:

rust-lang#128443 (comment)

r? fmease
RalfJung pushed a commit to RalfJung/miri that referenced this pull request Aug 5, 2024
…mease

Check divergence value first before doing span operations in `warn_if_unreachable`

It's more expensive to extract the span's desugaring first rather than check the value of the divergence enum. For some reason I inverted these checks, probably for readability, but as a consequence I regressed perf:

rust-lang/rust#128443 (comment)

r? fmease
@Mark-Simulacrum Mark-Simulacrum added relnotes Marks issues that should be documented in the release notes of the next release. and removed relnotes Marks issues that should be documented in the release notes of the next release. labels Aug 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
perf-regression Performance regression. relnotes Marks issues that should be documented in the release notes of the next release. S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Awaiting a function returning a never type does not warn for unreachable code after the await
7 participants