Make operational semantics of pattern matching independent of crate and module
The question of "when does matching an enum against a pattern of one of its variants read its discriminant" is currently an underspecified part of the language, causing weird behavior around borrowck, drop order, and UB.
Of course, in the common cases, the discriminant must be read to distinguish the variant of the enum, but currently the following exceptions are implemented:
1. If the enum has only one variant, we currently skip the discriminant read.
- This has the advantage that single-variant enums behave the same way as structs in this regard.
- However, it means that if the discriminant exists in the layout, we can't say that this discriminant being invalid is UB. This makes me particularly uneasy in its interactions with niches – consider the following example ([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=5904a6155cbdd39af4a2e7b1d32a9b1a)), where miri currently doesn't detect any UB (because the semantics don't specify any):
<details><summary>Example 1</summary>
```rust
#![allow(dead_code)]
use core::mem::{size_of, transmute};
#[repr(u8)]
enum Inner {
X(u8),
}
enum Outer {
A(Inner),
B(u8),
}
fn f(x: &Inner) {
match x {
Inner::X(v) => {
println!("{v}");
}
}
}
fn main() {
assert_eq!(size_of::<Inner>(), 2);
assert_eq!(size_of::<Outer>(), 2);
let x = Outer::B(42);
let y = &x;
f(unsafe { transmute(y) });
}
```
</details>
2. For the purpose of the above, enums with marked with `#[non_exhaustive]` are always considered to have multiple variants when observed from foreign crates, but the actual number of variants is considered in the current crate.
- This means that whether code has UB can depend on which crate it is in: https://github.com/rust-lang/rust/issues/147722
- In another case of `#[non_exhaustive]` affecting the runtime semantics, its presence or absence can change what gets captured by a closure, and by extension, the drop order: https://github.com/rust-lang/rust/issues/147722#issuecomment-3674554872
- Also at the above link, there is an example where removing `#[non_exhaustive]` can cause borrowck to suddenly start failing in another crate.
3. Moreover, we currently make a more specific check: we only read the discriminant if there is more than one *inhabited* variant in the enum.
- This means that the semantics can differ between `foo<!>`, and a copy of `foo` where `T` was manually replaced with `!`: rust-lang/rust#146803
- Moreover, due to the privacy rules for inhabitedness, it means that the semantics of code can depend on the *module* in which it is located.
- Additionally, this inhabitedness rule is even uglier due to the fact that closure capture analysis needs to happen before we can determine whether types are uninhabited, which means that whether the discriminant read happens has a different answer specifically for capture analysis.
- For the two above points, see the following example ([playground](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=a07d8a3ec0b31953942e96e2130476d9)):
<details><summary>Example 2</summary>
```rust
#![allow(unused)]
mod foo {
enum Never {}
struct PrivatelyUninhabited(Never);
pub enum A {
V(String, String),
Y(PrivatelyUninhabited),
}
fn works(mut x: A) {
let a = match x {
A::V(ref mut a, _) => a,
_ => unreachable!(),
};
let b = match x {
A::V(_, ref mut b) => b,
_ => unreachable!(),
};
a.len(); b.len();
}
fn fails(mut x: A) {
let mut f = || match x {
A::V(ref mut a, _) => (),
_ => unreachable!(),
};
let mut g = || match x {
A::V(_, ref mut b) => (),
_ => unreachable!(),
};
f(); g();
}
}
use foo::A;
fn fails(mut x: A) {
let a = match x {
A::V(ref mut a, _) => a,
_ => unreachable!(),
};
let b = match x {
A::V(_, ref mut b) => b,
_ => unreachable!(),
};
a.len(); b.len();
}
fn fails2(mut x: A) {
let mut f = || match x {
A::V(ref mut a, _) => (),
_ => unreachable!(),
};
let mut g = || match x {
A::V(_, ref mut b) => (),
_ => unreachable!(),
};
f(); g();
}
```
</details>
In light of the above, and following the discussion at rust-lang/rust#138961 and rust-lang/rust#147722, this PR ~~makes it so that, operationally, matching on an enum *always* reads its discriminant.~~ introduces the following changes to this behavior:
- matching on a `#[non_exhaustive]` enum will always introduce a discriminant read, regardless of whether the enum is from an external crate
- uninhabited variants now count just like normal ones, and don't get skipped in the checks
As per the discussion below, the resolution for point (1) above is that it should land as part of a separate PR, so that the subtler decision can be more carefully considered.
Note that this is a breaking change, due to the aforementioned changes in borrow checking behavior, new UB (or at least UB newly detected by miri), as well as drop order around closure captures. However, it seems to me that the combination of this PR with rust-lang/rust#138961 should have smaller real-world impact than rust-lang/rust#138961 by itself.
Fixesrust-lang/rust#142394Fixesrust-lang/rust#146590Fixesrust-lang/rust#146803 (though already marked as duplicate)
Fixes parts of rust-lang/rust#147722Fixesrust-lang/miri#4778
r? @Nadrieril @RalfJung
@rustbot label +A-closures +A-patterns +T-opsem +T-lang
If late lifetime resolution fails for whatever reason, forward to RBV
the guarantee that an error was emitted - thereby eliminating the need
for a "hack" to suppress subsequent/superfluous error diagnostics.
Provide more context on trait bounds being unmet due to imperfect derive
When encountering a value that has a borrow checker error where the type was previously moved, when suggesting cloning verify that it is not already being derived. If it is, explain why the `derive(Clone)` doesn't apply:
```
note: if `TypedAddress<T>` implemented `Clone`, you could clone the value
--> $DIR/derive-clone-implicit-bound.rs:6:1
|
LL | #[derive(Clone, Copy)]
| ----- derived `Clone` adds implicit bounds on type parameters
LL | pub struct TypedAddress<T>{
| ^^^^^^^^^^^^^^^^^^^^^^^^-^
| | |
| | introduces an implicit `T: Clone` bound
| consider manually implementing `Clone` for this type
...
LL | let old = self.return_value(offset);
| ------ you could clone this value
```
When encountering a bound coming from a derive macro, suggest manual impl of the trait.
Use the span for the specific param when adding bounds in builtin derive macros, so the diagnostic will point at them as well as the derive macro itself.
```
note: required for `Id<SomeNode>` to implement `PartialEq`
--> $DIR/derive-implicit-bound.rs:5:10
|
LL | #[derive(PartialEq, Eq)]
| ^^^^^^^^^
LL | pub struct Id<T>(PhantomData<T>);
| - unsatisfied trait bound introduced in this `derive` macro
= help: consider manually implementing `PartialEq` to avoid undesired bounds
```
Mention that the trait could be manually implemented in E0599.
Fixrust-lang/rust#108894. Address rust-lang/rust#143714. Address #rust-lang/rust#146515 (but ideally would also suggest constraining the fn bound correctly as well).
Preliminary match/capture test cleanup for PR 150681
Review for rust-lang/rust#150681 requested that this cleanup gets extracted to a separate PR.
r? @Zalathar
When a closure has an inferred parameter type like `|ch|` and the
expected type differs in borrowing (e.g., `char` vs `&char`), the
suggestion code would incorrectly suggest `|char|` instead of the
valid `|ch: char|`.
This happened because the code couldn't walk explicit `&` references
in the HIR when the type is inferred, and fell back to replacing the
entire parameter span with the expected type name.
Fix by only emitting the suggestion when we can properly identify the
`&` syntax to remove.
add comment to closure-move-use-after-move-diagnostic.rs
add comment to missing-operator-after-float.rs
add comment to closure-array-break-length.rs
add comment to box-lifetime-argument-not-allowed.rs
add comment to const-return-outside-fn.rs
add comment to drop-conflicting-impls.rs
add comment to unbalanced-doublequote-2.rs
add comment to borrow-immutable-deref-box.rs
add comment to for-in-const-eval.rs
add comment to borrowck-annotated-static-lifetime.rs
cleaned up cast-rfc0401.rs
add comment to nll-anon-to-static.rs
add comment to cast-to-dyn-any.rs
add comment to missing-associated-items.rs
add comment to enum-discriminant-missing-variant.rs
The split between walk_pat and maybe_read_scrutinee has now become
redundant.
Due to this change, one testcase within the testsuite has become similar
enough to a known ICE to also break. I am leaving this as future work,
as it requires feature(type_alias_impl_trait)
Tidying up tests/ui/issues 33 tests [4/N]
> [!NOTE]
> Intermediate commits are intended to help review, but will be squashed add comment commit prior to merge.
part of rust-lang/rust#133895
`tests/ui/compile-flags` split it into `tests/ui/compile-flags/invalid/` and `tests/ui/compile-flags/run-pass/`
r? Kivooeo
Merged tests/ui/typeck/non-function-call-error-2 with
tests/ui/typeck/non-function-call-error
Add comment to
tests/ui/traits/normalize-associated-type-in-where-clause.rs
Merged tests/ui/privacy/private-item-simple-2.rs with
tests/ui/privacy/private-item-simple.rs
Merged tests/ui/str/str-add-operator-2.rs with
tests/ui/str/str-add-operator.rs
Add comment to tests/ui/imports/duplicate-empty-imports.rs
Add comment to tests/ui/for-loop-while/nested-loop-break-unit.rs
Add comment to tests/ui/match/match-ref-option-pattern.rs
Add comment to tests/ui/closures/simple-capture-and-call.rs
Add comment to tests/ui/type/never-type-inference-fail.rs
Add comment to tests/ui/match/match-stack-overflow-72933.rs
Gate tests with the right edition
This PR guarantees that `./x test --test-args="--edition XXXX" ui` runs correctly with the 2015, 2018 and 2021 editions.
I don't expect this PR to hold up over time but it helps to submit further updates to the `//@ edition` directives of tests where we can use the new range syntax to have a more robust testing across different editions
r? `@fmease`
---
try-job: aarch64-gnu
try-job: aarch64-apple
try-job: x86_64-msvc-1
try-job: i686-msvc-1
try-job: x86_64-mingw-1
try-job: test-various
try-job: armhf-gnu
Relocate issues/issue-51154.rs to closures/box-generic-closure.rs
Relocate issues/issue-51515.rs to
borrowck/assignment-to-immutable-ref.rs
Relocate issues/issue-53348.rs to mismatched_types/deref-string-assign.rs
Relocate issues/issue-52717.rs to
pattern/match-enum-struct-variant-field-missing.rs
Relocate issues/issue-53300.rs to
type/cannot-find-wrapper-with-impl-trait.rs
Provide more context when mutably borrowing an imutably borrowed value
Point at statics and consts being mutable borrowed or written to:
```
error[E0594]: cannot assign to immutable static item `NUM`
--> $DIR/E0594.rs:4:5
|
LL | static NUM: i32 = 18;
| --------------- this `static` cannot be written to
...
LL | NUM = 20;
| ^^^^^^^^ cannot assign
```
Point at the expression that couldn't be mutably borrowed from a pattern:
```
error[E0596]: cannot borrow data in a `&` reference as mutable
--> $DIR/mut-pattern-of-immutable-borrow.rs:19:14
|
LL | match &arg.field {
| ---------- this cannot be borrowed as mutable
LL | Some(ref mut s) => s.push('a'),
| ^^^^^^^^^ cannot borrow as mutable
```
Partially address rust-lang/rust#74617.
Point at statics and consts being mutable borrowed or written to:
```
error[E0594]: cannot assign to immutable static item `NUM`
--> $DIR/E0594.rs:4:5
|
LL | static NUM: i32 = 18;
| --------------- this `static` cannot be written to
...
LL | NUM = 20;
| ^^^^^^^^ cannot assign
```
Point at the expression that couldn't be mutably borrowed from a pattern:
```
error[E0596]: cannot borrow data in a `&` reference as mutable
--> $DIR/mut-pattern-of-immutable-borrow.rs:19:14
|
LL | match &arg.field {
| ---------- this cannot be borrowed as mutable
LL | Some(ref mut s) => s.push('a'),
| ^^^^^^^^^ cannot borrow as mutable
```
When modifying a binding from inside of an `Fn` closure, point at the binding definition and suggest using an `std::sync` type that would allow the code to compile.
```
error[E0594]: cannot assign to `counter`, as it is a captured variable in a `Fn` closure
--> f703.rs:6:9
|
4 | let mut counter = 0;
| ----------- `counter` declared here, outside the closure
5 | let x: Box<dyn Fn()> = Box::new(|| {
| -- in this closure
6 | counter += 1;
| ^^^^^^^^^^^^ cannot assign
```
Perform unused assignment and unused variables lints on MIR.
Rebase of https://github.com/rust-lang/rust/pull/101500
Fixes https://github.com/rust-lang/rust/issues/51003.
The first commit moves detection of uninhabited types from the current liveness pass to MIR building.
In order to keep the same level of diagnostics, I had to instrument MIR a little more:
- keep for which original local a guard local is created;
- store in the `VarBindingForm` the list of introducer places and whether this was a shorthand pattern.
I am not very proud of the handling of self-assignments. The proposed scheme is in two parts: first detect probable self-assignments, by pattern matching on MIR, and second treat them specially during dataflow analysis. I welcome ideas.
Please review carefully the changes in tests. There are many small changes to behaviour, and I'm not sure all of them are desirable.
Rehome 30 `tests/ui/issues/` tests to other subdirectories under `tests/ui/` [#4 of Batch #2]
Part of rust-lang/rust#133895
Methodology:
1. Refer to the previously written `tests/ui/SUMMARY.md`
2. Find an appropriate category for the test, using the original issue thread and the test contents.
3. Add the issue URL at the bottom (not at the top, as that would mess up stderr line numbers)
4. Rename the tests to make their purpose clearer
Inspired by the methodology that `@Kivooeo` was using.
r? `@jieyouxu`