Skip to content

Commit 0982f66

Browse files
authored
Merge pull request scala#11904 from dotty-staging/fix-10930-alt
Introduce `asMatchable` escape hatch
2 parents 83bd356 + e54f668 commit 0982f66

File tree

6 files changed

+86
-4
lines changed

6 files changed

+86
-4
lines changed

compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ enum ErrorMessageID extends java.lang.Enum[ErrorMessageID] {
172172
AlreadyDefinedID,
173173
CaseClassInInlinedCodeID,
174174
OverrideTypeMismatchErrorID,
175-
OverrideErrorID
175+
OverrideErrorID,
176+
MatchableWarningID
176177

177178
def errorNumber = ordinal - 2
178179
}

compiler/src/dotty/tools/dotc/reporting/messages.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,32 @@ import transform.SymUtils._
838838
def explain = ""
839839
}
840840

841+
class MatchableWarning(tp: Type, pattern: Boolean)(using Context)
842+
extends TypeMsg(MatchableWarningID) {
843+
def msg =
844+
val kind = if pattern then "pattern selector" else "value"
845+
em"""${kind} should be an instance of Matchable,,
846+
|but it has unmatchable type $tp instead"""
847+
848+
def explain =
849+
if pattern then
850+
em"""A value of type $tp cannot be the selector of a match expression
851+
|since it is not constrained to be `Matchable`. Matching on unconstrained
852+
|values is disallowed since it can uncover implementation details that
853+
|were intended to be hidden and thereby can violate paramtetricity laws
854+
|for reasoning about programs.
855+
|
856+
|The restriction can be overridden by appending `.asMatchable` to
857+
|the selector value. `asMatchable` needs to be imported from
858+
|scala.compiletime. Example:
859+
|
860+
| import compiletime.asMatchable
861+
| def f[X](x: X) = x.asMatchable match { ... }"""
862+
else
863+
em"""The value can be converted to a `Matchable` by appending `.asMatchable`.
864+
|`asMatchable` needs to be imported from scala.compiletime."""
865+
}
866+
841867
class SeqWildcardPatternPos()(using Context)
842868
extends SyntaxMsg(SeqWildcardPatternPosID) {
843869
def msg = em"""${hl("*")} can be used only for last argument"""

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,9 +1280,7 @@ trait Checking {
12801280
def checkMatchable(tp: Type, pos: SrcPos, pattern: Boolean)(using Context): Unit =
12811281
if !tp.derivesFrom(defn.MatchableClass) && sourceVersion.isAtLeast(`future-migration`) then
12821282
val kind = if pattern then "pattern selector" else "value"
1283-
report.warning(
1284-
em"""${kind} should be an instance of Matchable,
1285-
|but it has unmatchable type $tp instead""", pos)
1283+
report.warning(MatchableWarning(tp, pattern), pos)
12861284
}
12871285

12881286
trait ReChecking extends Checking {

library/src/scala/compiletime/package.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,14 @@ end summonAll
158158

159159
/** Assertion that an argument is by-name. Used for nullability checking. */
160160
def byName[T](x: => T): T = x
161+
162+
/** Casts a value to be `Matchable`. This is needed if the value's type is an unconstrained
163+
* type parameter and the value is the scrutinee of a match expression.
164+
* This is normally disallowed since it violates parametricity and allows
165+
* to uncover implementation details that were intended to be hidden.
166+
* The `asMatchable` escape hatch should be used sparingly. It's usually
167+
* better to constrain the scrutinee type to be `Matchable` in the first place.
168+
*/
169+
extension [T](x: T)
170+
transparent inline def asMatchable: x.type & Matchable = x.asInstanceOf[x.type & Matchable]
171+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import language.future
2+
@main def Test =
3+
type LeafElem[X] = X match
4+
case String => Char
5+
case Array[t] => LeafElem[t]
6+
case Iterable[t] => LeafElem[t]
7+
case AnyVal => X
8+
9+
def leafElem[X](x: X): LeafElem[X] = x match
10+
case x: String => x.charAt(0) // error
11+
case x: Array[t] => leafElem(x(1)) // error
12+
case x: Iterable[t] => leafElem(x.head) // error
13+
case x: AnyVal => x // error

tests/run/i10930.scala

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import language.future
2+
import compiletime.asMatchable
3+
4+
@main def Test =
5+
type LeafElem[X] = X match
6+
case String => Char
7+
case Array[t] => LeafElem[t]
8+
case Iterable[t] => LeafElem[t]
9+
case AnyVal => X
10+
11+
def leafElem[X](x: X): LeafElem[X] = x.asMatchable match
12+
case x: String => x.charAt(0)
13+
case x: Array[t] => leafElem(x(1))
14+
case x: Iterable[t] => leafElem(x.head)
15+
case x: AnyVal => x
16+
17+
def f[X](x: X) = x
18+
19+
def leafElem2[X](x: X): LeafElem[X] = f(x).asMatchable match
20+
case x: String => x.charAt(0)
21+
case x: Array[t] => leafElem(x(1))
22+
case x: Iterable[t] => leafElem(x.head)
23+
case x: AnyVal => x
24+
25+
val x1: Char = leafElem("a")
26+
assert(x1 == 'a')
27+
val x2: Char = leafElem(Array("a", "b"))
28+
assert(x2 == 'b')
29+
val x3: Char = leafElem(List(Array("a", "b"), Array("")))
30+
assert(x3 == 'b')
31+
val x4: Int = leafElem(3)
32+
assert(x4 == 3)
33+

0 commit comments

Comments
 (0)