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

unreachable pattern lint should report some (or all) of the earlier match arms that subsume the unreachable one #127870

Closed
pnkfelix opened this issue Jul 17, 2024 · 6 comments · Fixed by #128034
Assignees
Labels
A-diagnostics Area: Messages for errors, warnings, and lints A-exhaustiveness-checking Relating to exhaustiveness / usefulness checking of patterns A-lints Area: Lints (warnings about flaws in source code) such as unused_mut. A-patterns Relating to patterns and pattern matching D-terse Diagnostics: An error or lint that doesn't give enough information about the problem at hand. L-unreachable_patterns Lint: unreachable_patterns T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@pnkfelix
Copy link
Member

pnkfelix commented Jul 17, 2024

Code

enum Sym { A(i32, i32), B, C }

fn foo(s: Sym) -> &'static str {
    match s {
        Sym::B    => "B",
        Sym::A(_, 0) => "Am,0",
        Sym::A(0, _) => "A0,n",
        /* imagine a *lot* of other match arms inserted in here */
        Sym::C       => "C",
        Sym::A(0, 0) => "A0,0",
        Sym::A(_, _) => "Am,n",
    }
}

fn main() {
    foo(Sym::A(0, 0));
    foo(Sym::A(1, 1));
    foo(Sym::B);
    foo(Sym::C);
}

Current output

warning: unreachable pattern
  --> src/main.rs:10:9
   |
10 |         Sym::A(0, 0) => "A0,0",
   |         ^^^^^^^^^^^^
   |
   = note: `#[warn(unreachable_patterns)]` on by default

Desired output

warning: unreachable pattern
  --> src/main.rs:10:9
   |
10 |         Sym::A(0, 0) => "A0,0",
   |         ^^^^^^^^^^^^
   |
   = note: `#[warn(unreachable_patterns)]` on by default
   |
   = note: the first pattern that makes this pattern unreachable is:
  --> src/main.rs:6:9
   |
 6 |         Sym::A(_, 0) => "Am,0",
   |         ^^^^^^^^^^^^

Rationale and extra context

I was recently adding a new enum variant that had a ton of other enum variants. I had to update an associated match expression to include it. Then, at some later point, I forgot that I had updated that match expression, redundantly added new match arms for the same variant, and got the above diangostic (which was set to be denied in my project and so it was a hard error).

I spent a little while puzzling over how the pattern could possibly be unreachable before I realized my mistake, in part because the list was so long that I had inserted the new variants at different points in the (unsorted) series of match arms. It was relatively trivial for me to diagnose in my own case, but I think my example above helps illustrate hypothetical scenarios where the compiler could do this mechanical work for us.

(Another options might be to report all the arms that would each independently subsume the unreachable one, as I have two such arms in my example above. But I'm not sure it would be worth the effort of computing that set ... its probably good enough to give the user the first counter-example and let them iteratively invoke the compiler if that's the usage pattern they want to follow.)

Other cases

No response

Rust Version

playground nightly version: 1.81.0-nightly

(2024-07-12 c6727fc9b5c64cefa726)

Anything else?

No response

@pnkfelix pnkfelix added A-diagnostics Area: Messages for errors, warnings, and lints T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jul 17, 2024
@pnkfelix
Copy link
Member Author

pnkfelix commented Jul 17, 2024

In truth my original intent was to make an example that would motivate showing multiple arms that are collectively causing the unreachable arm to become unreachable. But I got the logic backward and gave an example that showed the opposite of that (i.e. multiple arms where each independently makes the problematic one unreachable).

Here's an example of what I mean by a collective set of arms making a later one unreachable (playground):

enum Sym { A(i32, i32), B, C }

fn foo(s: (Sym, bool)) -> &'static str {
    match s {
        (Sym::B, _)    => "B",
        (Sym::A(_, 0), true) => "Am,0",
        (Sym::A(0, _), false) => "A0,n",
        /* imagine a *lot* of other match arms inserted in here */
        (Sym::C, _)       => "C",
        (Sym::A(0, 0), _) => "A0,0",
        (Sym::A(_, _), _) => "Am,n",
    }
}

fn main() {
    foo((Sym::A(0, 0), true));
    foo((Sym::A(1, 1), false));
    foo((Sym::B, true));
    foo((Sym::C, false));
}

This is the scenario where I think the compiler can be most helpful in reporting multiple arms, because their cumulative effect is not trivial for a human to immediately see.

So I would like in this case to see output like:

warning: unreachable pattern
  --> src/main.rs:10:9
   |
10 |         (Sym::A(0, 0), _) => "A0,0",
   |         ^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(unreachable_patterns)]` on by default
   |
   = note: some patterns that collectively make this pattern unreachable are:
  --> src/main.rs:6:9
   |
 6 |         (Sym::A(_, 0), true) => "Am,0",
   |         ^^^^^^^^^^^^^^^^^^^^
 7 |         (Sym::A(0, _), false) => "A0,n",
   |         ^^^^^^^^^^^^^^^^^^^^^

@compiler-errors
Copy link
Member

@Nadrieril could probably weigh in on the feasibility of recording this information (or possibly re-deriving it on error). Seems hard to do the former, from scanning and being vaguely familiar with it from reviewing Nadrieril's changes some months ago, but perhaps it's easier to redo on the error path.

@Nadrieril
Copy link
Member

Nadrieril commented Jul 18, 2024

This sounds doable actually! We track intersection data today: we know for a given branch which branches above it match values in common. Unfortunately we only compute an underapproximation to prevent complexity blowups, but I'd guess it's good enough for diagnostics.

The report might look like "such and such branch intersect this one, they contribute to making it unreachable"

@estebank estebank added the D-terse Diagnostics: An error or lint that doesn't give enough information about the problem at hand. label Jul 19, 2024
@estebank
Copy link
Contributor

but I'd guess it's good enough for diagnostics.

Yeah, incomplete context is better than no context. All of the subtle cases I could think of are actually situations in which naïve output would be sufficient for the user to understand the issue.

bors added a commit to rust-lang-ci/rust that referenced this issue Jul 20, 2024
…try>

match exhaustiveness: Expand or-patterns as a separate step

To compute exhaustiveness, we must expand or-patterns. Previously, we expanded them at the same time that we pushed patterns into the matrix. This made it harder to track pattern reachability, because the or-pattern itself would never show up in the matrix so we had to recover missing information.

This PR changes that: we no longer expand or-patterns as we push them into the matrix. Instead, if we find an or-pattern in the matrix we expand them in a step very much like the specialization we already do. This simplifies a bunch of things, and will greatly simplify the implementation of rust-lang#127870.

r? `@compiler-errors`
@Nadrieril
Copy link
Member

Nadrieril commented Jul 21, 2024

It's easy to also compute an over-approximation of the intersections actually. The following is a case where we can't compute the exact set of intersections. If we take the over-approximation, we get (here everything that matches (true, true, _) is considered to potentially intersect each other):

match ... {
	(true, _, _) => {}
	//^ note: this may match some of the same values
	(_, true, 0..10) => {}
	//^ note: this may match some of the same values
	(_, true, 10..) => {}
	//^ note: this may match some of the same values
	(_, true, 3) => {}
	//^ unreachable pattern
}

If we take the under-approximation we get an empty set of intersections. Not sure which is best.

EDIT: I got confused somehow, we do find an intersection with the second arm which is exactly what we'd like to report. I have to think some more.
EDIT2: see below

@Nadrieril
Copy link
Member

Nadrieril commented Jul 21, 2024

After giving this more thought, it appears that the under-approximation is good enough that it still always contains a set of rows that cover the one we care about. Hence the lint is always able to list a set of arms that cause the unreachability.

@Nadrieril Nadrieril self-assigned this Jul 21, 2024
@Nadrieril Nadrieril added A-patterns Relating to patterns and pattern matching A-exhaustiveness-checking Relating to exhaustiveness / usefulness checking of patterns labels Jul 21, 2024
bors added a commit to rust-lang-ci/rust that referenced this issue Jul 21, 2024
exhaustiveness: Explain why a given pattern is considered unreachable

This PR tells the user why a given pattern is considered unreachable. I reused the intersection information we were already computing; even though it's incomplete I convinced myself that it is sufficient to always get a set of patterns that cover the unreachable one.

I'm not a fan of the diagnostic messages I came up with, I'm open to suggestions.

Fixes rust-lang#127870. This is also the other one of the two diagnostic improvements I wanted to do before rust-lang#122792.

Note: the first commit is rust-lang#128015, the second is an unrelated drive-by tweak.

r? `@compiler-errors`
@jieyouxu jieyouxu added L-unreachable_patterns Lint: unreachable_patterns A-lints Area: Lints (warnings about flaws in source code) such as unused_mut. labels Jul 21, 2024
bors added a commit to rust-lang-ci/rust that referenced this issue Jul 23, 2024
…ompiler-errors

match exhaustiveness: Expand or-patterns as a separate step

To compute exhaustiveness, we must expand or-patterns. Previously, we expanded them at the same time that we pushed patterns into the matrix. This made it harder to track pattern reachability, because the or-pattern itself would never show up in the matrix so we had to recover missing information.

This PR changes that: we no longer expand or-patterns as we push them into the matrix. Instead, if we find an or-pattern in the matrix we expand them in a step very much like the specialization we already do. This simplifies a bunch of things, and should greatly simplify the implementation of rust-lang#127870.

r? `@compiler-errors`
@bors bors closed this as completed in 355efac Jul 26, 2024
github-actions bot pushed a commit to rust-lang/miri that referenced this issue Jul 27, 2024
exhaustiveness: Explain why a given pattern is considered unreachable

This PR tells the user why a given pattern is considered unreachable. I reused the intersection information we were already computing; even though it's incomplete I convinced myself that it is sufficient to always get a set of patterns that cover the unreachable one.

I'm not a fan of the diagnostic messages I came up with, I'm open to suggestions.

Fixes rust-lang/rust#127870. This is also the other one of the two diagnostic improvements I wanted to do before rust-lang/rust#122792.

Note: the first commit is an unrelated drive-by tweak.

r? `@compiler-errors`
lnicola pushed a commit to lnicola/rust-analyzer that referenced this issue Jul 28, 2024
…rrors

match exhaustiveness: Expand or-patterns as a separate step

To compute exhaustiveness, we must expand or-patterns. Previously, we expanded them at the same time that we pushed patterns into the matrix. This made it harder to track pattern reachability, because the or-pattern itself would never show up in the matrix so we had to recover missing information.

This PR changes that: we no longer expand or-patterns as we push them into the matrix. Instead, if we find an or-pattern in the matrix we expand them in a step very much like the specialization we already do. This simplifies a bunch of things, and should greatly simplify the implementation of rust-lang/rust#127870.

r? `@compiler-errors`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-diagnostics Area: Messages for errors, warnings, and lints A-exhaustiveness-checking Relating to exhaustiveness / usefulness checking of patterns A-lints Area: Lints (warnings about flaws in source code) such as unused_mut. A-patterns Relating to patterns and pattern matching D-terse Diagnostics: An error or lint that doesn't give enough information about the problem at hand. L-unreachable_patterns Lint: unreachable_patterns T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants