Skip to content

Commit 04db456

Browse files
committed
Improve warning message and add doc page
Also, add back tests
1 parent 66adab2 commit 04db456

File tree

7 files changed

+147
-2
lines changed

7 files changed

+147
-2
lines changed

compiler/src/dotty/tools/dotc/typer/Checking.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,8 +1249,10 @@ trait Checking {
12491249

12501250
def checkMatchable(tp: Type, pos: SrcPos, pattern: Boolean)(using Context): Unit =
12511251
if !tp.derivesFrom(defn.MatchableClass) && sourceVersion.isAtLeast(`3.1-migration`) then
1252-
val kind = if pattern then "pattern selector " else ""
1253-
report.warning(em"${kind}type $tp should not be scrutinized", pos)
1252+
val kind = if pattern then "pattern selector" else "value"
1253+
report.warning(
1254+
em"""${kind} should be an instance of Matchable,
1255+
|but it has unmatchable type $tp instead""", pos)
12541256
}
12551257

12561258
trait ReChecking extends Checking {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
layout: doc-page
2+
title: The Matchable Trait
3+
---
4+
5+
A new trait `Matchable` controls the ability to pattern match.
6+
7+
### The Problem
8+
9+
The Scala 3 standard library has a type `IArray` for immutable
10+
arrays that is defined like this:
11+
12+
```scala
13+
opaque type IArray[+T] = Array[_ <: T]
14+
```
15+
The `IArray` type offers extension methods for `length` and `apply`, but not for `update`; hence it seems values of type `IArray` cannot be updated.
16+
17+
However, there is a potential hole due to pattern matching. Consider:
18+
```scala
19+
val imm: IArray[Int] = ...
20+
imm match
21+
case a: Array[Int] => a(0) = 1
22+
```
23+
The test will succeed at runtime since `IArray`s _are_ represented as
24+
`Array`s at runtime. But if we allowed it, it would break the fundamental abstraction of immutable arrays.
25+
26+
__Aside:__ One could also achieve the same by casting:
27+
```scala
28+
imm.asInstanceOf[Array[Int]](0) = 1
29+
```
30+
But that is not as much of a problem since in Scala `asInstanceOf` is understood to be low-level and unsafe. By contrast, a pattern match that compiles without warning or error should not break abstractions.
31+
32+
Note also that the problem is not tied to opaque types as match selectors. The following slight variant with a value of parametric
33+
type `T` as match selector leads to the same problem:
34+
35+
```scala
36+
def f[T](x: T) = x match
37+
case a: Array[Int] => a(0) = 0
38+
f(imm)
39+
```
40+
Finally, note that the problem is not linked to just opaque types. No unbounded type parameter or abstract type should be decomposable with a pattern match.
41+
42+
### The Solution
43+
44+
There is a new type `scala.Matchable` that controls pattern matching. When typing a pattern match of a constructor pattern `C(...)` or
45+
a type pattern `_: C` it is required that the selector type conforms
46+
to `Matchable`. If that's not the case a warning is issued. For instance when compiling the example at the start of this section we get:
47+
```
48+
> sc ../new/test.scala -source 3.1
49+
-- Warning: ../new/test.scala:4:12 ---------------------------------------------
50+
4 | case a: Array[Int] => a(0) = 0
51+
| ^^^^^^^^^^
52+
| pattern selector should be an instance of Matchable,
53+
| but it has unmatchable type IArray[Int] instead
54+
```
55+
To allow migration from Scala 2 and cross-compiling
56+
between Scala 2 and 3 the warning is turned on only for `-source 3.1-migration` or higher.
57+
58+
`Matchable` is a universal trait with `Any` as its parent class. It is
59+
extended by both `AnyVal` and `AnyRef`. Since `Matchable` is a supertype of every concrete value or reference class it means that instances of such classes can be matched as before. However, match selectors of the following types will produce a warning:
60+
61+
- Type `Any`: if pattern matching is required one should use `Matchable` instead.
62+
- Unbounded type parameters and abstract types: If pattern matching is required they should have an upper bound `Matchable`.
63+
- Type parameters and abstract types that are only bounded by some
64+
universal trait: Again, `Matchable` should be added as a bound.
65+
66+
`Matchable` is currently a marker trait without any methods. Over time
67+
we might migrate methods `getClass` and `isInstanceOf` to it, since these are closely related to pattern-matching.
68+

docs/sidebar.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ sidebar:
105105
url: docs/reference/other-new-features/parameter-untupling.html
106106
- title: Kind Polymorphism
107107
url: docs/reference/other-new-features/kind-polymorphism.html
108+
- title: Matchable Trait
109+
url: docs/reference/other-new-features/matchable.html
108110
- title: threadUnsafe Annotation
109111
url: docs/reference/other-new-features/threadUnsafe-annotation.html
110112
- title: targetName Annotation

tests/neg-custom-args/i7314.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@main def Test =
2+
// conversion out of the opaque type:
3+
val imm1 = IArray(1,2,3) // supposedly immutable
4+
println(imm1(0)) // 1
5+
imm1 match {
6+
case a: Array[Int] => // error: should not be scrutinized
7+
a(0) = 0
8+
}
9+
println(imm1(0)) // 0

tests/neg-custom-args/matchable.scala

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
def foo[T](x: T): Matchable =
2+
println(x.getClass()) // ok
3+
println(x.isInstanceOf[Int]) // ok
4+
x match
5+
case x: Int => // error: should not be scrutinized
6+
println("int")
7+
x
8+
case x: String => // error: should not be scrutinized
9+
println("string")
10+
x
11+
List(x) match
12+
case (x: Int) :: Nil => // error: should not be scrutinized
13+
println("int")
14+
x
15+
case List(x: String) => // error: should not be scrutinized
16+
println("string")
17+
x
18+
case List(y :: Nil) => // error: should not be scrutinized
19+
y :: Nil
20+
case _ =>
21+
x // error: should not be scrutinized
22+
23+
@main def Test =
24+
val x: Matchable = foo(1)
25+
val y: Matchable = foo("hello")
26+
assert(x != y)
27+

tests/run/matchable.check

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class java.lang.Integer
2+
true
3+
int
4+
int
5+
class java.lang.String
6+
false
7+
string
8+
string

tests/run/matchable.scala

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
def foo[T](x: T): Matchable =
2+
println(x.getClass())
3+
println(x.isInstanceOf[Int])
4+
x match
5+
case x: Int =>
6+
println("int")
7+
x
8+
case x: String =>
9+
println("string")
10+
x
11+
List(x) match
12+
case (x: Int) :: Nil =>
13+
println("int")
14+
x
15+
case List(x: String) =>
16+
println("string")
17+
x
18+
case List(y :: Nil) =>
19+
y :: Nil
20+
case _ =>
21+
g(x)
22+
23+
def g[T <: Matchable](x: T) = x
24+
25+
@main def Test =
26+
val x: Matchable = foo(1)
27+
val y: Matchable = foo("hello")
28+
assert(x != y)
29+

0 commit comments

Comments
 (0)