Skip to content

Commit

Permalink
Merge pull request #11904 from dotty-staging/fix-10930-alt
Browse files Browse the repository at this point in the history
Introduce `asMatchable` escape hatch
  • Loading branch information
odersky authored Mar 29, 2021
2 parents 83bd356 + e54f668 commit 0982f66
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 4 deletions.
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ enum ErrorMessageID extends java.lang.Enum[ErrorMessageID] {
AlreadyDefinedID,
CaseClassInInlinedCodeID,
OverrideTypeMismatchErrorID,
OverrideErrorID
OverrideErrorID,
MatchableWarningID

def errorNumber = ordinal - 2
}
26 changes: 26 additions & 0 deletions compiler/src/dotty/tools/dotc/reporting/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
4 changes: 1 addition & 3 deletions compiler/src/dotty/tools/dotc/typer/Checking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions library/src/scala/compiletime/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]

13 changes: 13 additions & 0 deletions tests/neg-custom-args/fatal-warnings/i10930.scala
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions tests/run/i10930.scala
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 0982f66

Please sign in to comment.