Skip to content

Introduce asMatchable escape hatch #11904

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)