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

Edition 2024 guide for temporary lifetime changes #318

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,5 @@
- [Rustdoc combined tests](rust-2024/rustdoc-doctests.md)
- [Rustdoc nested `include!` change](rust-2024/rustdoc-nested-includes.md)
- [Reserved syntax](rust-2024/reserved-syntax.md)
- [`if let` temporary scope](rust-2024/temporary-if-let-scope.md)
- [Tail expression temporary scope](rust-2024/temporary-tail-expr-scope.md)
100 changes: 100 additions & 0 deletions src/rust-2024/temporary-if-let-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# `if let` temporary scope

🚧 The 2024 Edition has not yet been released and hence this section is still "under construction".
More information may be found in the tracking issue at <https://github.com/rust-lang/rust/issues/124085>.

## Summary

- In an `if let $pat = $expr { .. } else { .. }` expression, the temporary values generated from evaluating `$expr` will be dropped before the program enters the `else` branch instead of after.

## Details

The 2024 Edition changes the drop scope of [temporary values] in the scrutinee[^scrutinee] of an `if let` expression. This is intended to help reduce the potentially unexpected behavior involved with the temporary living for too long.

Before 2024, the temporaries could be extended beyond the `if let` expression itself. For example:

```rust,edition2021
// Before 2024
# use std::sync::RwLock;

fn f(value: &RwLock<Option<bool>>) {
if let Some(x) = *value.read().unwrap() {
println!("value is {x}");
} else {
let mut v = value.write().unwrap();
if v.is_none() {
*v = Some(true);
}
}
// <--- Read lock is dropped here in 2021
}
```

In this example, the temporary read lock generated by the call to `value.read()` will not be dropped until after the `if let` expression (that is, after the `else` block). In the case where the `else` block is executed, this causes a deadlock when it attempts to acquire a write lock.

The 2024 Edition shortens the lifetime of the temporaries to the point where the then-block is completely evaluated or the program control enters the `else` block.

<!-- TODO: edition2024 -->
```rust
// Starting with 2024
# use std::sync::RwLock;

fn f(value: &RwLock<Option<bool>>) {
if let Some(x) = *value.read().unwrap() {
println!("value is {x}");
}
// <--- Read lock is dropped here in 2024
else {
let mut s = value.write().unwrap();
if s.is_none() {
*s = Some(true);
}
}
}
```

See the [temporary scope rules] for more information about how temporary scopes are extended. See the [tail expression temporary scope] chapter for a similar change made to tail expressions.

[^scrutinee]: The [scrutinee] is the expression being matched on in the `if let` expression.

[scrutinee]: ../../reference/glossary.html#scrutinee
[temporary values]: ../../reference/expressions.html#temporaries
[temporary scope rules]: ../../reference/destructors.html#temporary-scopes
[tail expression temporary scope]: temporary-tail-expr-scope.md

## Migration

It is always safe to rewrite `if let` with a `match`. The temporaries of the `match` scrutinee are extended past the end of the `match` expression (typically to the end of the statement), which is the same as the 2021 behavior of `if let`.

The [`if_let_rescope`] lint suggests a fix when a lifetime issue arises due to this change or the lint detects that a temporary value with a custom, non-trivial `Drop` destructor is generated from the scrutinee of the `if let`. For instance, the earlier example may be rewritten into the following when the suggestion from `cargo fix` is accepted:

```rust
# use std::sync::RwLock;
fn f(value: &RwLock<Option<bool>>) {
match *value.read().unwrap() {
Some(x) => {
println!("value is {x}");
}
_ => {
let mut s = value.write().unwrap();
if s.is_none() {
*s = Some(true);
}
}
}
// <--- Read lock is dropped here in both 2021 and 2024
}
```

In this particular example, that's probably not what you want due to the aforementioned deadlock! However, some scenarios may be assuming that the temporaries are held past the `else` clause, in which case you may want to retain the old behavior.

The `if_let_rescope` lint cannot deduce with complete confidence that the program semantics are preserved when the lifetime of such temporary values are shortened. For this reason, the suggestion from this lint is *not* automatically applied when running `cargo fix --edition`. It is recommended to manually inspect the warnings emitted when running `cargo fix --edition` and determine whether or not you need to apply the suggestion.

If you want to manually inspect these warnings without performing the edition migration, you can enable the lint with:

```rust
// Add this to the root of your crate to do a manual migration.
#![warn(if_let_rescope)]
```

[`if_let_rescope`]: ../../rustc/lints/listing/allowed-by-default.html#if-let-rescope
77 changes: 77 additions & 0 deletions src/rust-2024/temporary-tail-expr-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Tail expression temporary scope

🚧 The 2024 Edition has not yet been released and hence this section is still "under construction".
More information may be found in the tracking issue at <https://github.com/rust-lang/rust/issues/123739>.

## Summary

- Temporary values generated in evaluation of the tail expression of a [function] or closure body, or a [block] are now dropped before local variables.

[function]: ../../reference/items/functions.html
[block]: ../../reference/expressions/block-expr.html

## Details

The 2024 Edition changes the drop order of [temporary values] in tail expressions. It often comes as a surprise that, before the 2024 Edition, temporary values in tail expressions are dropped later than the local variable bindings, as in the following example:

[temporary values]: ../../reference/expressions.html#temporaries

```rust,edition2021,compile_fail,E0597
// Before 2024
# use std::cell::RefCell;
fn f() -> usize {
let c = RefCell::new("..");
c.borrow().len() // error[E0597]: `c` does not live long enough
}
```

This yields the following error with the 2021 Edition:

```text
error[E0597]: `c` does not live long enough
--> src/lib.rs:4:5
|
3 | let c = RefCell::new("..");
| - binding `c` declared here
4 | c.borrow().len() // error[E0597]: `c` does not live long enough
| ^---------
| |
| borrowed value does not live long enough
| a temporary with access to the borrow is created here ...
5 | }
| -
| |
| `c` dropped here while still borrowed
| ... and the borrow might be used here, when that temporary is dropped and runs the destructor for type `Ref<'_, &str>`
|
= note: the temporary is part of an expression at the end of a block;
consider forcing this temporary to be dropped sooner, before the block's local variables are dropped
help: for example, you could save the expression's value in a new local variable `x` and then make `x` be the expression at the end of the block
|
4 | let x = c.borrow().len(); x // error[E0597]: `c` does not live long enough
| +++++++ +++

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

In 2021 the local variable `c` is dropped before the temporary created by `c.borrow()`. The 2024 Edition changes this so that the temporary value `c.borrow()` is dropped first, followed by dropping the local variable `c`, allowing the code to compile as expected.

See the [temporary scope rules] for more information about how temporary scopes are extended. See the [`if let` temporary scope] chapter for a similar change made to `if let` expressions.

[`if let` temporary scope]: temporary-if-let-scope.md
[temporary scope rules]: ../../reference/destructors.html#temporary-scopes

## Migration

Unfortunately, there are no semantics-preserving rewrites to shorten the lifetime for temporary values in tail expressions[^RFC3606]. The [`tail_expr_drop_order`] lint detects if a temporary value with a custom, non-trivial `Drop` destructor is generated in a tail expression. Warnings from this lint will appear when running `cargo fix --edition`, but will otherwise not automatically make any changes. It is recommended to manually inspect the warnings and determine whether or not you need to make any adjustments.

If you want to manually inspect these warnings without performing the edition migration, you can enable the lint with:

```rust
// Add this to the root of your crate to do a manual migration.
#![warn(tail_expr_drop_order)]
```

[^RFC3606]: Details are documented at [RFC 3606](https://github.com/rust-lang/rfcs/pull/3606)

[`tail_expr_drop_order`]: ../../rustc/lints/listing/allowed-by-default.html#tail-expr-drop-order