diff --git a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala index 2fba7ef447b5..518be160763e 100644 --- a/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala +++ b/compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala @@ -172,7 +172,8 @@ enum ErrorMessageID extends java.lang.Enum[ErrorMessageID] { AlreadyDefinedID, CaseClassInInlinedCodeID, OverrideTypeMismatchErrorID, - OverrideErrorID + OverrideErrorID, + MatchableWarningID def errorNumber = ordinal - 2 } diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 2d9d75b24f56..eb3e2b22781c 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -838,6 +838,32 @@ import transform.SymUtils._ def explain = "" } + class MatchableWarning(tp: Type, pattern: Boolean)(using Context) + extends TypeMsg(MatchableWarningID) { + def msg = + val kind = if pattern then "pattern selector" else "value" + em"""${kind} should be an instance of Matchable,, + |but it has unmatchable type $tp instead""" + + def explain = + if pattern then + em"""A value of type $tp cannot be the selector of a match expression + |since it is not constrained to be `Matchable`. Matching on unconstrained + |values is disallowed since it can uncover implementation details that + |were intended to be hidden and thereby can violate paramtetricity laws + |for reasoning about programs. + | + |The restriction can be overridden by appending `.asMatchable` to + |the selector value. `asMatchable` needs to be imported from + |scala.compiletime. Example: + | + | import compiletime.asMatchable + | def f[X](x: X) = x.asMatchable match { ... }""" + else + em"""The value can be converted to a `Matchable` by appending `.asMatchable`. + |`asMatchable` needs to be imported from scala.compiletime.""" + } + class SeqWildcardPatternPos()(using Context) extends SyntaxMsg(SeqWildcardPatternPosID) { def msg = em"""${hl("*")} can be used only for last argument""" diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 2c9d48537f09..cd0effedafcd 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -1280,9 +1280,7 @@ trait Checking { def checkMatchable(tp: Type, pos: SrcPos, pattern: Boolean)(using Context): Unit = if !tp.derivesFrom(defn.MatchableClass) && sourceVersion.isAtLeast(`future-migration`) then val kind = if pattern then "pattern selector" else "value" - report.warning( - em"""${kind} should be an instance of Matchable, - |but it has unmatchable type $tp instead""", pos) + report.warning(MatchableWarning(tp, pattern), pos) } trait ReChecking extends Checking { diff --git a/library/src/scala/compiletime/package.scala b/library/src/scala/compiletime/package.scala index 080ef29e0fa2..84ac4fde4e29 100644 --- a/library/src/scala/compiletime/package.scala +++ b/library/src/scala/compiletime/package.scala @@ -158,3 +158,14 @@ end summonAll /** Assertion that an argument is by-name. Used for nullability checking. */ def byName[T](x: => T): T = x + +/** Casts a value to be `Matchable`. This is needed if the value's type is an unconstrained + * type parameter and the value is the scrutinee of a match expression. + * This is normally disallowed since it violates parametricity and allows + * to uncover implementation details that were intended to be hidden. + * The `asMatchable` escape hatch should be used sparingly. It's usually + * better to constrain the scrutinee type to be `Matchable` in the first place. + */ +extension [T](x: T) + transparent inline def asMatchable: x.type & Matchable = x.asInstanceOf[x.type & Matchable] + diff --git a/tests/neg-custom-args/fatal-warnings/i10930.scala b/tests/neg-custom-args/fatal-warnings/i10930.scala new file mode 100644 index 000000000000..d1fbdde10574 --- /dev/null +++ b/tests/neg-custom-args/fatal-warnings/i10930.scala @@ -0,0 +1,13 @@ +import language.future +@main def Test = + type LeafElem[X] = X match + case String => Char + case Array[t] => LeafElem[t] + case Iterable[t] => LeafElem[t] + case AnyVal => X + + def leafElem[X](x: X): LeafElem[X] = x match + case x: String => x.charAt(0) // error + case x: Array[t] => leafElem(x(1)) // error + case x: Iterable[t] => leafElem(x.head) // error + case x: AnyVal => x // error diff --git a/tests/run/i10930.scala b/tests/run/i10930.scala new file mode 100644 index 000000000000..87aaf148417c --- /dev/null +++ b/tests/run/i10930.scala @@ -0,0 +1,33 @@ +import language.future +import compiletime.asMatchable + +@main def Test = + type LeafElem[X] = X match + case String => Char + case Array[t] => LeafElem[t] + case Iterable[t] => LeafElem[t] + case AnyVal => X + + def leafElem[X](x: X): LeafElem[X] = x.asMatchable match + case x: String => x.charAt(0) + case x: Array[t] => leafElem(x(1)) + case x: Iterable[t] => leafElem(x.head) + case x: AnyVal => x + + def f[X](x: X) = x + + def leafElem2[X](x: X): LeafElem[X] = f(x).asMatchable match + case x: String => x.charAt(0) + case x: Array[t] => leafElem(x(1)) + case x: Iterable[t] => leafElem(x.head) + case x: AnyVal => x + + val x1: Char = leafElem("a") + assert(x1 == 'a') + val x2: Char = leafElem(Array("a", "b")) + assert(x2 == 'b') + val x3: Char = leafElem(List(Array("a", "b"), Array(""))) + assert(x3 == 'b') + val x4: Int = leafElem(3) + assert(x4 == 3) +