dotnet / dotnet/fsharp

Redundant conjunctive patterns (`&`, `as`) may trigger spurious exhaustiveness warning

Open
#18,118 1 comment 1 reaction 0 assignees View on GitHub
Area-Compiler-PatternMatching Feature Improvement
Dominant language
F#
Stars
4.3k
Forks
876
Avg merge
4d 22h
Merged PRs (30d)
144

Description

Redundant conjunctive patterns (`&`, `as`) may trigger a spurious exhaustiveness warning in some scenarios.

## Repro steps

### Minimal repro

A normal exhaustive pattern match does not trigger an exhaustiveness warning, as expected:

```fsharp
type T = A | B | C

let f x =
match x with
| A | B -> ()
| C -> ()
```

However, given this example, adding a redundant match on case `C` with a conjunctive pattern like `&` or `as` will trigger a spurious exhaustiveness warning:

```fsharp
type T = A | B | C

let f x =
match x with
| A | B -> ()
| C & C -> ()

// match x with
// ----------^

//stdin(4,11): warning FS0025: Incomplete pattern matches on this expression. For example, the value 'A' may indicate a case not covered by the pattern(s).
```

### More realistic/useful repro

Another, perhaps more realistic and useful example is this, where you might want to bind the first two elements of a list as well as the tail of the original list (i.e., including the second element but not the first):

```fsharp
let f xs =
match xs with
| [] | [_] -> ()
| e1 :: e2 :: _ & _ :: tail -> ()

// match xs with
// ----------^^

//stdin(2,11): warning FS0025: Incomplete pattern matches on this expression. For example, the value '[]' may indicate a case not covered by the pattern(s).
```

It is impossible to make the compiler happy in that scenario — we cannot turn `e1 :: e2 :: _ & _ :: tail` into `(e1 :: e2 :: _ & _ :: tail | [])`, because we cannot bind `e1`, `e2`, or `tail` in the `[]` case which the compiler is (unnecessarily) asking us to add.

Note that no warning is emitted if one side of the conjunctive pattern is a simple named pattern, e.g.,

```fsharp
let f x =
match x with // No warning.
| A | B -> ()
| C & c -> ()
```

```fsharp
let f x =
match x with // No warning.
| A | B -> ()
| C as c -> ()
```

### Codegen

This even affects codegen — an unreachable `MatchFailureException` branch is emitted:

```fsharp
type T = A | B | C

let f x =
match x with // No warning.
| A | B -> true
| C -> false

let f' x =
match x with // Warning, different codegen.
| A | B -> true
| C & C -> false
```

[SharpLab](https://sharplab.io/#v2:DYLgZgzgPg2gPAOQPYGEkFsAOBDATgSwiQDsBuAAmQFEBHAV22HwBcBPCgEQFMxs7hmAQToBzdF2LNszfCQAUvYBC4BKAHwBdALAAoNpi7kAKuQC85QeSjkAQlfIpdu4F2bkw5AB5nd5P+XRpAGMACy9yAHcWEN9/a0trOwBaNXJmXDouWL9rFHIU90ZlbPJnV3cAcnDTEsDmUPCo5hidf3sE23zU9MyS3PIAMgcuwqUuIA=)

```csharp
public static bool f(T x)
{
switch (x._tag)
{
default:
return true;
case 2:
return false;
}
}

public static bool f'(T x)
{
switch (x._tag)
{
default:
return true;
case 2:
if (x._tag == 2)
{
return false;
}
throw new MatchFailureException("#mirrorsharp-virtual-fs\\f4765ba6-b1f5-44e9-b186-0abc7ae58e28\\_.fs", 10, 10);
}
}
```

## Expected behavior

No exhaustiveness warning should be emitted, and the codegen should be identical.

## Actual behavior

An exhaustiveness warning and an unreachable `MatchFailureException` branch are emitted.

## Known workarounds

N/A.

## Related information

This behavior appears to be old — it reproduces for me in .NET 3.1, and it is still present in the latest .NET 9 SDK.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.