Skip to content

Commit 4673f77

Browse files
authored
Update unreducible match types error reporting (#19954)
Match type reduction can fail for any of the following reasons: - EmptyScrutinee: would be unsound to reduce - Stuck: selector does not match a case and is not provably disjoint from it either - NoInstance: selector does not uniquely determine params captures in pattern - NoMatches: selector matches none of the cases - LegacyPattern: match type contains an illegal case and sourceVersion >= 3.4 Out of those, only Stuck and NoInstance, *could* get reduced in a refined context. ## Status quo The match reducer returns: - `ErrorType` for NoMatches and LegacyPattern, - `NoType`, which implies the match type is left unreduced, in all other cases. In addition, the implementation has an issue where the `ErrorType`s can be left unreported, then entering the flexible type logic, thereby conforming to anything. ## Proposed changes In addition to fixing the aforementioned bug, this PR proposes to leave all unreducible match types as unreduced. Of course the reduction may be needed at a later point for conformance, in which case the error message will still contain the same explanations from the `MatchTypeTrace`. Fixes #19949 Fixes #19950 ## Discussion All cases of failed match type reductions which we know will never reduce, even with refined scrutinee, should have a consistent behaviour. So NoMatches and EmptyScrutinee should either both be an error or both be left unreduced. The current implementation attempts to do the former approach (but only for NoMatches), which has some limitations as discussed below (I'm not saying I can do better, hence the latter approach). ### Undesirable errors We dont always want an error for a NoMatches failed reduction, for example if we just need `Nothing` to conform to it: ```scala 3 trait TupleWrap[T <: Tuple]: def head: Tuple.Head[T] object EmptyTupleWrap extends TupleWrap[EmptyTuple]: def head = throw NoSuchElementException() // Error: // | ^ // | Match type reduction failed since selector EmptyTuple // | matches none of the cases ``` But we could do `def head: Nothing = ...` to avoid the error here. Generally speaking, places where the bounds of the match type suffice can still get a reduction error, and adding an ascription to avoid an inferred match type doesn't always do the trick. Another refused example could be: ```scala 3 type Default[N <: Int] = N match case 0 => 'a' | 'c' case 1 => 'b' | 'd' def default(n: Int): Option[Default[n.type]] = n match case _: (0 | 1) => Some[Default[n.type]]: n match case _: 0 => 'a' case _: 1 => 'b' case _ => None default(2): Option[Char] // Error // | ^ // | Match type reduction failed since selector (2 : Int) // | matches none of the cases ``` even though the function looks reasonable and type-checking would be sound. ### Missed errors Also note in the `EmptyTupleWrap` example, we get a reduction error from a match type application which does not appear in the source code. A valid question might be when and for what exactly these conditions are checked ? The goal is to report a type error early on for a NoMatches application right, but we are actually only doing so if we happen to do `tryNormalize` and end up in the `MatchReducer`. Here is an example where were a match type with NoMatches is accepted ```scala 3 trait A: type X type R = X match case 0 => 'a' case 1 => 'b' trait B extends A: type S = 2 type R1 = B#R // no error ``` Generally speaking, the NoMatches error can be circumvented with: ```scala 3 type AllowNoMatchesM[X] = { type X1 = X type R = X1 match case 0 => 'a' case 1 => 'b' }#R type R2 = AllowNoMatchesM[2] // no error ``` Also note the projections are used in the examples for simplicity but are not necessary, `R` *can be* used within `B` as unreduced without a reported error. See #19799 for another example of inconsistent errors
2 parents 6a40dd5 + 2beb67e commit 4673f77

File tree

12 files changed

+124
-114
lines changed

12 files changed

+124
-114
lines changed

compiler/src/dotty/tools/dotc/core/MatchTypeTrace.scala

+13-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ object MatchTypeTrace:
1212

1313
private enum TraceEntry:
1414
case TryReduce(scrut: Type)
15+
case NoMatches(scrut: Type, cases: List[MatchTypeCaseSpec])
1516
case Stuck(scrut: Type, stuckCase: MatchTypeCaseSpec, otherCases: List[MatchTypeCaseSpec])
1617
case NoInstance(scrut: Type, stuckCase: MatchTypeCaseSpec, fails: List[(Name, TypeBounds)])
1718
case EmptyScrutinee(scrut: Type)
@@ -50,6 +51,12 @@ object MatchTypeTrace:
5051
case _ =>
5152
case _ =>
5253

54+
/** Record a failure that scrutinee `scrut` does not match any case in `cases`.
55+
* Only the first failure is recorded.
56+
*/
57+
def noMatches(scrut: Type, cases: List[MatchTypeCaseSpec])(using Context) =
58+
matchTypeFail(NoMatches(scrut, cases))
59+
5360
/** Record a failure that scrutinee `scrut` does not match `stuckCase` but is
5461
* not disjoint from it either, which means that the remaining cases `otherCases`
5562
* cannot be visited. Only the first failure is recorded.
@@ -71,7 +78,7 @@ object MatchTypeTrace:
7178
*/
7279
def recurseWith(scrut: Type)(op: => Type)(using Context): Type =
7380
ctx.property(MatchTrace) match
74-
case Some(trace) =>
81+
case Some(trace) if !trace.entries.contains(TryReduce(scrut)) =>
7582
val prev = trace.entries
7683
trace.entries = TryReduce(scrut) :: prev
7784
val res = op
@@ -95,6 +102,11 @@ object MatchTypeTrace:
95102
private def explainEntry(entry: TraceEntry)(using Context): String = entry match
96103
case TryReduce(scrut: Type) =>
97104
i" trying to reduce $scrut"
105+
case NoMatches(scrut, cases) =>
106+
i""" failed since selector $scrut
107+
| matches none of the cases
108+
|
109+
| ${casesText(cases)}"""
98110
case EmptyScrutinee(scrut) =>
99111
i""" failed since selector $scrut
100112
| is uninhabited (there are no values of that type)."""

compiler/src/dotty/tools/dotc/core/TypeComparer.scala

+8-17
Original file line numberDiff line numberDiff line change
@@ -3634,23 +3634,14 @@ class MatchReducer(initctx: Context) extends TypeComparer(initctx) {
36343634
MatchTypeTrace.emptyScrutinee(scrut)
36353635
NoType
36363636
case Nil =>
3637-
val casesText = MatchTypeTrace.noMatchesText(scrut, cases)
3638-
ErrorType(reporting.MatchTypeNoCases(casesText))
3639-
3640-
inFrozenConstraint {
3641-
if scrut.isError then
3642-
// if the scrutinee is an error type
3643-
// then just return that as the result
3644-
// not doing so will result in the first type case matching
3645-
// because ErrorType (as a FlexType) is <:< any type case
3646-
// this situation can arise from any kind of nesting of match types,
3647-
// e.g. neg/i12049 `Tuple.Concat[Reverse[ts], (t2, t1)]`
3648-
// if Reverse[ts] fails with no matches,
3649-
// the error type should be the reduction of the Concat too
3650-
scrut
3651-
else
3652-
recur(cases)
3653-
}
3637+
/* TODO warn ? then re-enable warn/12974.scala:26
3638+
val noCasesText = MatchTypeTrace.noMatchesText(scrut, cases)
3639+
report.warning(reporting.MatchTypeNoCases(noCasesText), pos = ???)
3640+
*/
3641+
MatchTypeTrace.noMatches(scrut, cases)
3642+
NoType
3643+
3644+
inFrozenConstraint(recur(cases))
36543645
}
36553646
}
36563647

tests/neg-macros/toexproftuple.scala

-12
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,10 @@
11
import scala.quoted._, scala.deriving.*
22

33
inline def mcr: Any = ${mcrImpl}
4-
54
def mcrImpl(using ctx: Quotes): Expr[Any] = {
6-
75
val tpl: (Expr[1], Expr[2], Expr[3]) = ('{1}, '{2}, '{3})
86
'{val res: (1, 3, 3) = ${Expr.ofTuple(tpl)}; res} // error
9-
// ^^^^^^^^^^^^^^^^^
10-
// Found: quoted.Expr[(1 : Int) *: (2 : Int) *: (3 : Int) *: EmptyTuple]
11-
// Required: quoted.Expr[((1 : Int), (3 : Int), (3 : Int))]
127

138
val tpl2: (Expr[1], 2, Expr[3]) = ('{1}, 2, '{3})
149
'{val res = ${Expr.ofTuple(tpl2)}; res} // error
15-
// ^
16-
// Cannot prove that (quoted.Expr[(1 : Int)], (2 : Int), quoted.Expr[(3 : Int)]) =:= scala.Tuple.Map[
17-
// scala.Tuple.InverseMap[
18-
// (quoted.Expr[(1 : Int)], (2 : Int), quoted.Expr[(3 : Int)])
19-
// , quoted.Expr]
20-
// , quoted.Expr].
21-
2210
}

tests/neg/10349.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ object Firsts:
44
case Map[_, v] => First[Option[v]]
55

66
def first[X](x: X): First[X] = x match
7-
case x: Map[_, _] => first(x.values.headOption) // error
7+
case x: Map[_, _] => first(x.values.headOption)
88

99
@main
1010
def runFirsts2(): Unit =

tests/neg/10747.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ type Foo[A] = A match {
22
case Int => String
33
}
44

5-
type B = Foo[Boolean] // error
5+
type B = Foo[Boolean]
6+
val _: B = "hello" // error

tests/neg/i12049.check

+52-24
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,39 @@
1515
| case B => String
1616
|
1717
| longer explanation available when compiling with `-explain`
18-
-- [E184] Type Error: tests/neg/i12049.scala:14:23 ---------------------------------------------------------------------
18+
-- [E007] Type Mismatch Error: tests/neg/i12049.scala:14:17 ------------------------------------------------------------
1919
14 |val y3: String = ??? : Last[Int *: Int *: Boolean *: String *: EmptyTuple] // error
20-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
21-
| Match type reduction failed since selector EmptyTuple
22-
| matches none of the cases
20+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
21+
| Found: Last[EmptyTuple]
22+
| Required: String
2323
|
24-
| case _ *: _ *: t => Last[t]
25-
| case t *: EmptyTuple => t
26-
-- [E184] Type Error: tests/neg/i12049.scala:22:26 ---------------------------------------------------------------------
24+
| Note: a match type could not be fully reduced:
25+
|
26+
| trying to reduce Last[EmptyTuple]
27+
| failed since selector EmptyTuple
28+
| matches none of the cases
29+
|
30+
| case _ *: _ *: t => Last[t]
31+
| case t *: EmptyTuple => t
32+
|
33+
| longer explanation available when compiling with `-explain`
34+
-- [E007] Type Mismatch Error: tests/neg/i12049.scala:22:20 ------------------------------------------------------------
2735
22 |val z3: (A, B, A) = ??? : Reverse[(A, B, A)] // error
28-
| ^^^^^^^^^^^^^^^^^^
29-
| Match type reduction failed since selector A *: EmptyTuple.type
30-
| matches none of the cases
36+
| ^^^^^^^^^^^^^^^^^^^^^^^^
37+
| Found: Tuple.Concat[Reverse[A *: EmptyTuple.type], (B, A)]
38+
| Required: (A, B, A)
39+
|
40+
| Note: a match type could not be fully reduced:
41+
|
42+
| trying to reduce Tuple.Concat[Reverse[A *: EmptyTuple.type], (B, A)]
43+
| trying to reduce Reverse[A *: EmptyTuple.type]
44+
| failed since selector A *: EmptyTuple.type
45+
| matches none of the cases
3146
|
32-
| case t1 *: t2 *: ts => Tuple.Concat[Reverse[ts], (t2, t1)]
33-
| case EmptyTuple => EmptyTuple
47+
| case t1 *: t2 *: ts => Tuple.Concat[Reverse[ts], (t2, t1)]
48+
| case EmptyTuple => EmptyTuple
49+
|
50+
| longer explanation available when compiling with `-explain`
3451
-- [E172] Type Error: tests/neg/i12049.scala:24:20 ---------------------------------------------------------------------
3552
24 |val _ = summon[M[B]] // error
3653
| ^
@@ -45,22 +62,33 @@
4562
| Therefore, reduction cannot advance to the remaining case
4663
|
4764
| case B => String
48-
-- [E184] Type Error: tests/neg/i12049.scala:25:26 ---------------------------------------------------------------------
65+
-- [E172] Type Error: tests/neg/i12049.scala:25:78 ---------------------------------------------------------------------
4966
25 |val _ = summon[String =:= Last[Int *: Int *: Boolean *: String *: EmptyTuple]] // error
50-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
51-
| Match type reduction failed since selector EmptyTuple
52-
| matches none of the cases
67+
| ^
68+
| Cannot prove that String =:= Last[EmptyTuple].
69+
|
70+
| Note: a match type could not be fully reduced:
71+
|
72+
| trying to reduce Last[EmptyTuple]
73+
| failed since selector EmptyTuple
74+
| matches none of the cases
5375
|
54-
| case _ *: _ *: t => Last[t]
55-
| case t *: EmptyTuple => t
56-
-- [E184] Type Error: tests/neg/i12049.scala:26:29 ---------------------------------------------------------------------
76+
| case _ *: _ *: t => Last[t]
77+
| case t *: EmptyTuple => t
78+
-- [E172] Type Error: tests/neg/i12049.scala:26:48 ---------------------------------------------------------------------
5779
26 |val _ = summon[(A, B, A) =:= Reverse[(A, B, A)]] // error
58-
| ^^^^^^^^^^^^^^^^^^
59-
| Match type reduction failed since selector A *: EmptyTuple.type
60-
| matches none of the cases
80+
| ^
81+
| Cannot prove that (A, B, A) =:= Tuple.Concat[Reverse[A *: EmptyTuple.type], (B, A)].
82+
|
83+
| Note: a match type could not be fully reduced:
84+
|
85+
| trying to reduce Tuple.Concat[Reverse[A *: EmptyTuple.type], (B, A)]
86+
| trying to reduce Reverse[A *: EmptyTuple.type]
87+
| failed since selector A *: EmptyTuple.type
88+
| matches none of the cases
6189
|
62-
| case t1 *: t2 *: ts => Tuple.Concat[Reverse[ts], (t2, t1)]
63-
| case EmptyTuple => EmptyTuple
90+
| case t1 *: t2 *: ts => Tuple.Concat[Reverse[ts], (t2, t1)]
91+
| case EmptyTuple => EmptyTuple
6492
-- [E008] Not Found Error: tests/neg/i12049.scala:28:21 ----------------------------------------------------------------
6593
28 |val _ = (??? : M[B]).length // error
6694
| ^^^^^^^^^^^^^^^^^^^

tests/neg/i17944.check

-30
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,3 @@
1414
| Therefore, reduction cannot advance to the remaining case
1515
|
1616
| case _ *: t => test.FindField0[t, ("i" : String), scala.compiletime.ops.int.S[(0 : Int)]]
17-
| trying to reduce test.FindField[(("s" : String) ->> String, ("i" : String) ->> Int), ("i" : String)]
18-
| trying to reduce test.FindField0[(("s" : String) ->> String, ("i" : String) ->> Int), ("i" : String), (0 : Int)]
19-
| failed since selector (("s" : String) ->> String, ("i" : String) ->> Int)
20-
| does not match case (("i" : String) ->> f) *: _ => (f, (0 : Int))
21-
| and cannot be shown to be disjoint from it either.
22-
| Therefore, reduction cannot advance to the remaining case
23-
|
24-
| case _ *: t => test.FindField0[t, ("i" : String), scala.compiletime.ops.int.S[(0 : Int)]]
25-
| trying to reduce test.FindField0[(("s" : String) ->> String, ("i" : String) ->> Int), ("i" : String), (0 : Int)]
26-
| failed since selector (("s" : String) ->> String, ("i" : String) ->> Int)
27-
| does not match case (("i" : String) ->> f) *: _ => (f, (0 : Int))
28-
| and cannot be shown to be disjoint from it either.
29-
| Therefore, reduction cannot advance to the remaining case
30-
|
31-
| case _ *: t => test.FindField0[t, ("i" : String), scala.compiletime.ops.int.S[(0 : Int)]]
32-
| trying to reduce test.FindField[(("s" : String) ->> String, ("i" : String) ->> Int), ("i" : String)]
33-
| trying to reduce test.FindField0[(("s" : String) ->> String, ("i" : String) ->> Int), ("i" : String), (0 : Int)]
34-
| failed since selector (("s" : String) ->> String, ("i" : String) ->> Int)
35-
| does not match case (("i" : String) ->> f) *: _ => (f, (0 : Int))
36-
| and cannot be shown to be disjoint from it either.
37-
| Therefore, reduction cannot advance to the remaining case
38-
|
39-
| case _ *: t => test.FindField0[t, ("i" : String), scala.compiletime.ops.int.S[(0 : Int)]]
40-
| trying to reduce test.FindField0[(("s" : String) ->> String, ("i" : String) ->> Int), ("i" : String), (0 : Int)]
41-
| failed since selector (("s" : String) ->> String, ("i" : String) ->> Int)
42-
| does not match case (("i" : String) ->> f) *: _ => (f, (0 : Int))
43-
| and cannot be shown to be disjoint from it either.
44-
| Therefore, reduction cannot advance to the remaining case
45-
|
46-
| case _ *: t => test.FindField0[t, ("i" : String), scala.compiletime.ops.int.S[(0 : Int)]]

tests/neg/i19949.scala

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
trait T[N]:
3+
type M = N match
4+
case 0 => Any
5+
6+
val t: T[Double] = new T[Double] {}
7+
val x: t.M = "hello" // error
8+
9+
val z: T[Double]#M = "hello" // error

tests/neg/matchtype-seq.check

+28-12
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,35 @@
1-
-- [E184] Type Error: tests/neg/matchtype-seq.scala:9:11 ---------------------------------------------------------------
1+
-- [E007] Type Mismatch Error: tests/neg/matchtype-seq.scala:9:18 ------------------------------------------------------
22
9 | identity[T1[3]]("") // error
3-
| ^^^^^
4-
| Match type reduction failed since selector (3 : Int)
5-
| matches none of the cases
3+
| ^^
4+
| Found: ("" : String)
5+
| Required: Test.T1[(3 : Int)]
66
|
7-
| case (1 : Int) => Int
8-
| case (2 : Int) => String
9-
-- [E184] Type Error: tests/neg/matchtype-seq.scala:10:11 --------------------------------------------------------------
7+
| Note: a match type could not be fully reduced:
8+
|
9+
| trying to reduce Test.T1[(3 : Int)]
10+
| failed since selector (3 : Int)
11+
| matches none of the cases
12+
|
13+
| case (1 : Int) => Int
14+
| case (2 : Int) => String
15+
|
16+
| longer explanation available when compiling with `-explain`
17+
-- [E007] Type Mismatch Error: tests/neg/matchtype-seq.scala:10:18 -----------------------------------------------------
1018
10 | identity[T1[3]](1) // error
11-
| ^^^^^
12-
| Match type reduction failed since selector (3 : Int)
13-
| matches none of the cases
19+
| ^
20+
| Found: (1 : Int)
21+
| Required: Test.T1[(3 : Int)]
1422
|
15-
| case (1 : Int) => Int
16-
| case (2 : Int) => String
23+
| Note: a match type could not be fully reduced:
24+
|
25+
| trying to reduce Test.T1[(3 : Int)]
26+
| failed since selector (3 : Int)
27+
| matches none of the cases
28+
|
29+
| case (1 : Int) => Int
30+
| case (2 : Int) => String
31+
|
32+
| longer explanation available when compiling with `-explain`
1733
-- [E007] Type Mismatch Error: tests/neg/matchtype-seq.scala:11:20 -----------------------------------------------------
1834
11 | identity[T1[Int]]("") // error
1935
| ^^

tests/pos/i18488.scala

-15
This file was deleted.

tests/pos/i19950.scala

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
trait Apply[F[_]]:
3+
extension [T <: NonEmptyTuple](tuple: T)(using toMap: Tuple.IsMappedBy[F][T])
4+
def mapN[B](f: Tuple.InverseMap[T, F] => B): F[B] = ???
5+
6+
given Apply[Option] = ???
7+
given Apply[List] = ???
8+
given Apply[util.Try] = ???
9+
10+
@main def Repro = (Option(1), Option(2), Option(3)).mapN(_ + _ + _)

tests/neg/12974.scala renamed to tests/warn/12974.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ object RecMap {
2323
def main(args: Array[String]) =
2424
import Record._
2525

26-
val foo: Any = Rec.empty.fetch("foo") // error
26+
val foo: Any = Rec.empty.fetch("foo") // TODO
2727
// ^
2828
// Match type reduction failed since selector EmptyTuple.type
2929
// matches none of the cases

0 commit comments

Comments
 (0)