Skip to content

Commit 537d61a

Browse files
committed
Check that patterns in val/var defs are irrefutable
... unless followed by `: @unchecked`. For the moment, this requires -strict mode. We plan to turn this on by default once bootstrap compiler supports `: @unchecked` in patterns (i.e. once this PR is part of the bootstrap compiler).
1 parent 7512f58 commit 537d61a

File tree

6 files changed

+71
-28
lines changed

6 files changed

+71
-28
lines changed

compiler/src/dotty/tools/dotc/core/Types.scala

+9-5
Original file line numberDiff line numberDiff line change
@@ -1485,13 +1485,17 @@ object Types {
14851485
*/
14861486
def signature(implicit ctx: Context): Signature = Signature.NotAMethod
14871487

1488-
def dropRepeatedAnnot(implicit ctx: Context): Type = this match {
1489-
case AnnotatedType(parent, annot) if annot.symbol eq defn.RepeatedAnnot => parent
1490-
case tp @ AnnotatedType(parent, annot) =>
1491-
tp.derivedAnnotatedType(parent.dropRepeatedAnnot, annot)
1492-
case tp => tp
1488+
/** Drop annotation of given `cls` from this type */
1489+
def dropAnnot(cls: Symbol)(implicit ctx: Context): Type = stripTypeVar match {
1490+
case self @ AnnotatedType(pre, annot) =>
1491+
if (annot.symbol eq cls) pre
1492+
else self.derivedAnnotatedType(pre.dropAnnot(cls), annot)
1493+
case _ =>
1494+
this
14931495
}
14941496

1497+
def dropRepeatedAnnot(implicit ctx: Context): Type = dropAnnot(defn.RepeatedAnnot)
1498+
14951499
def annotatedToRepeated(implicit ctx: Context): Type = this match {
14961500
case tp @ ExprType(tp1) => tp.derivedExprType(tp1.annotatedToRepeated)
14971501
case AnnotatedType(tp, annot) if annot matches defn.RepeatedAnnot =>

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -1105,7 +1105,7 @@ trait Applications extends Compatibility { self: Typer with Dynamic =>
11051105
if (selType <:< unapplyArgType) {
11061106
unapp.println(i"case 1 $unapplyArgType ${ctx.typerState.constraint}")
11071107
fullyDefinedType(unapplyArgType, "pattern selector", tree.span)
1108-
selType
1108+
selType.dropAnnot(defn.UncheckedAnnot) // need to drop @unchecked. Just because the selector is @unchecked, the pattern isn't.
11091109
} else if (isSubTypeOfParent(unapplyArgType, selType)(ctx.addMode(Mode.GADTflexible))) {
11101110
val patternBound = maximizeType(unapplyArgType, tree.span, fromScala2x)
11111111
if (patternBound.nonEmpty) unapplyFn = addBinders(unapplyFn, patternBound)

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

+39-15
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import Decorators._
2525
import ErrorReporting.{err, errorType}
2626
import config.Printers.typr
2727
import NameKinds.DefaultGetterName
28+
import Applications.unapplyArgs
2829

2930
import collection.mutable
3031
import SymDenotations.{NoCompleter, NoDenotation}
@@ -596,23 +597,46 @@ trait Checking {
596597
ctx.error(ex"$cls cannot be instantiated since it${rstatus.msg}", pos)
597598
}
598599

599-
/** Check that pattern definition is either marked @unchecked or has a right
600-
* hand side with a type that conforms to the pattern's type.
600+
/** Check that pattern `pat` is irrefutable for scrutinee tye `pt`.
601+
* This means `pat` is either marked @unchecked or `pt` conforms to the
602+
* pattern's type. If pattern is an UnApply, do the check recursively.
601603
*/
602-
def checkPatDefMatch(tree: Tree, pt: Type)(implicit ctx: Context): Unit = tree match {
603-
case Match(_, CaseDef(pat, _, _) :: _)
604-
if !pat.tpe.widen.hasAnnotation(defn.UncheckedAnnot) && !(pt <:< pat.tpe) =>
605-
val pt1 = pt match {
606-
case AnnotatedType(pt1, annot) if annot.matches(defn.UncheckedAnnot) => pt1
607-
case _ => pt
604+
def checkIrrefutable(pat: Tree, pt: Type)(implicit ctx: Context): Boolean = {
605+
patmatch.println(i"check irrefutable $pat: ${pat.tpe} against $pt")
606+
607+
def check(pat: Tree, pt: Type): Boolean = {
608+
if (pt <:< pat.tpe)
609+
true
610+
else {
611+
ctx.errorOrMigrationWarning(
612+
ex"""pattern's type ${pat.tpe} is more specialized than the right hand side expression's type ${pt.dropAnnot(defn.UncheckedAnnot)}
613+
|
614+
|If the narrowing is intentional, this can be communicated by writing `: @unchecked` after the full pattern.${err.rewriteNotice}""",
615+
pat.sourcePos)
616+
false
617+
}
618+
}
619+
620+
!ctx.settings.strict.value || // only in -strict mode for now since mitigations work only after this PR
621+
pat.tpe.widen.hasAnnotation(defn.UncheckedAnnot) || {
622+
pat match {
623+
case Bind(_, pat1) =>
624+
checkIrrefutable(pat1, pt)
625+
case UnApply(fn, _, pats) =>
626+
check(pat, pt) && {
627+
val argPts = unapplyArgs(fn.tpe.finalResultType, fn, pats, pat.sourcePos)
628+
pats.corresponds(argPts)(checkIrrefutable)
629+
}
630+
case Alternative(pats) =>
631+
pats.forall(checkIrrefutable(_, pt))
632+
case Typed(arg, tpt) =>
633+
check(pat, pt) && checkIrrefutable(arg, pt)
634+
case Ident(nme.WILDCARD) =>
635+
true
636+
case _ =>
637+
check(pat, pt)
608638
}
609-
ctx.errorOrMigrationWarning(
610-
ex"""pattern's type ${pat.tpe.widen} is more specialized than the right hand side expression's type $pt1
611-
|
612-
|If the narrowing is intentional, this can be communicated by writing `: @unchecked` after the pattern.${err.rewriteNotice}""",
613-
pat.sourcePos)
614-
if (ctx.scala2Mode) patch(Span(pat.span.end), ": @unchecked")
615-
case _ =>
639+
}
616640
}
617641

618642
/** Check that `path` is a legal prefix for an import or export clause */

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

+14-6
Original file line numberDiff line numberDiff line change
@@ -1036,10 +1036,15 @@ class Typer extends Namer
10361036
if (tree.isInline) checkInInlineContext("inline match", tree.posd)
10371037
val sel1 = typedExpr(tree.selector)
10381038
val selType = fullyDefinedType(sel1.tpe, "pattern selector", tree.span).widen
1039-
val res = typedMatchFinish(tree, sel1, selType, tree.cases, pt)
1040-
if (tree.selector.removeAttachment(desugar.PatDefMatch).isDefined)
1041-
checkPatDefMatch(res, sel1.tpe)
1042-
res
1039+
val result = typedMatchFinish(tree, sel1, selType, tree.cases, pt)
1040+
result match {
1041+
case Match(sel, CaseDef(pat, _, _) :: _)
1042+
if (tree.selector.removeAttachment(desugar.PatDefMatch).isDefined) =>
1043+
if (!checkIrrefutable(pat, sel.tpe) && ctx.scala2Mode)
1044+
patch(Span(pat.span.end), ": @unchecked")
1045+
case _ =>
1046+
}
1047+
result
10431048
}
10441049
}
10451050

@@ -1820,8 +1825,11 @@ class Typer extends Namer
18201825
}
18211826
case _ => arg1
18221827
}
1823-
val tpt = TypeTree(AnnotatedType(arg1.tpe.widenIfUnstable, Annotation(annot1)))
1824-
assignType(cpy.Typed(tree)(arg2, tpt), tpt)
1828+
val argType =
1829+
if (arg1.isInstanceOf[Bind]) arg1.tpe.widen // bound symbol is not accessible outside of Bind node
1830+
else arg1.tpe.widenIfUnstable
1831+
val annotatedTpt = TypeTree(AnnotatedType(argType, Annotation(annot1)))
1832+
assignType(cpy.Typed(tree)(arg2, annotatedTpt), annotatedTpt)
18251833
}
18261834
}
18271835

tests/neg/unchecked-patterns.scala

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,9 @@ object Test {
22
val (y1: Some[Int] @unchecked) = Some(1): Option[Int] // OK
33
val y2: Some[Int] @unchecked = Some(1): Option[Int] // error
44

5-
val x :: xs = List(1, 2, 3) // error
5+
val x :: xs = List(1, 2, 3) // error
6+
val (1, c) = (1, 2) // error
7+
val 1 *: cs = 1 *: () // error
8+
9+
val (_: Int | _: Any) = ??? : Any // error
610
}

tests/run/unchecked-patterns.scala

+3
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@ object Test extends App {
33
val (y1: Some[Int] @unchecked) = Some(1): Option[Int]
44

55
val a :: as: @unchecked = List(1, 2, 3)
6+
val lst @ b :: bs: @unchecked = List(1, 2, 3)
7+
val (1, c): @unchecked = (1, 2)
8+
val 1 *: cs: @unchecked = 1 *: () // error
69
}

0 commit comments

Comments
 (0)