Skip to content

Commit 0492a30

Browse files
Backport "Fix a bundle of patmat issues" to 3.5.2 (#21478)
Backports #21000 to the 3.5.2 branch. PR submitted by the release tooling. [skip ci]
2 parents 4d402b6 + 40c60d8 commit 0492a30

22 files changed

+289
-38
lines changed

compiler/src/dotty/tools/dotc/config/Settings.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -411,9 +411,10 @@ object Settings:
411411
def PhasesSetting(category: SettingCategory, name: String, descr: String, default: String = "", aliases: List[String] = Nil, deprecation: Option[Deprecation] = None): Setting[List[String]] =
412412
publish(Setting(category, prependName(name), descr, if (default.isEmpty) Nil else List(default), aliases = aliases, deprecation = deprecation))
413413

414-
def PrefixSetting(category: SettingCategory, name: String, descr: String, deprecation: Option[Deprecation] = None): Setting[List[String]] =
414+
def PrefixSetting(category: SettingCategory, name0: String, descr: String, deprecation: Option[Deprecation] = None): Setting[List[String]] =
415+
val name = prependName(name0)
415416
val prefix = name.takeWhile(_ != '<')
416-
publish(Setting(category, "-" + name, descr, Nil, prefix = Some(prefix), deprecation = deprecation))
417+
publish(Setting(category, name, descr, Nil, prefix = Some(prefix), deprecation = deprecation))
417418

418419
def VersionSetting(category: SettingCategory, name: String, descr: String, default: ScalaVersion = NoScalaVersion, legacyArgs: Boolean = false, deprecation: Option[Deprecation] = None): Setting[ScalaVersion] =
419420
publish(Setting(category, prependName(name), descr, default, legacyArgs = legacyArgs, deprecation = deprecation))

compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala

+2-12
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,6 @@ class PatternMatcher extends MiniPhase {
3535

3636
override def runsAfter: Set[String] = Set(ElimRepeated.name)
3737

38-
private val InInlinedCode = new util.Property.Key[Boolean]
39-
private def inInlinedCode(using Context) = ctx.property(InInlinedCode).getOrElse(false)
40-
41-
override def prepareForInlined(tree: Inlined)(using Context): Context =
42-
if inInlinedCode then ctx
43-
else ctx.fresh.setProperty(InInlinedCode, true)
44-
4538
override def transformMatch(tree: Match)(using Context): Tree =
4639
if (tree.isInstanceOf[InlineMatch]) tree
4740
else {
@@ -53,13 +46,10 @@ class PatternMatcher extends MiniPhase {
5346
case rt => tree.tpe
5447
val translated = new Translator(matchType, this).translateMatch(tree)
5548

56-
if !inInlinedCode then
49+
// Skip analysis on inlined code (eg pos/i19157)
50+
if !tpd.enclosingInlineds.nonEmpty then
5751
// check exhaustivity and unreachability
5852
SpaceEngine.checkMatch(tree)
59-
else
60-
// only check exhaustivity, as inlining may generate unreachable code
61-
// like in i19157.scala
62-
SpaceEngine.checkMatchExhaustivityOnly(tree)
6353

6454
translated.ensureConforms(matchType)
6555
}

compiler/src/dotty/tools/dotc/transform/patmat/Space.scala

+60-24
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package dotc
33
package transform
44
package patmat
55

6-
import core.*, Constants.*, Contexts.*, Decorators.*, Flags.*, Names.*, NameOps.*, StdNames.*, Symbols.*, Types.*
6+
import core.*
7+
import Constants.*, Contexts.*, Decorators.*, Flags.*, NullOpsDecorator.*, Symbols.*, Types.*
8+
import Names.*, NameOps.*, StdNames.*
79
import ast.*, tpd.*
810
import config.Printers.*
911
import printing.{ Printer, * }, Texts.*
@@ -350,7 +352,7 @@ object SpaceEngine {
350352
val funRef = fun1.tpe.asInstanceOf[TermRef]
351353
if (fun.symbol.name == nme.unapplySeq)
352354
val (arity, elemTp, resultTp) = unapplySeqInfo(fun.tpe.widen.finalResultType, fun.srcPos)
353-
if (fun.symbol.owner == defn.SeqFactoryClass && defn.ListType.appliedTo(elemTp) <:< pat.tpe)
355+
if fun.symbol.owner == defn.SeqFactoryClass && pat.tpe.hasClassSymbol(defn.ListClass) then
354356
// The exhaustivity and reachability logic already handles decomposing sum types (into its subclasses)
355357
// and product types (into its components). To get better counter-examples for patterns that are of type
356358
// List (or a super-type of list, like LinearSeq) we project them into spaces that use `::` and Nil.
@@ -522,14 +524,26 @@ object SpaceEngine {
522524
val mt: MethodType = unapp.widen match {
523525
case mt: MethodType => mt
524526
case pt: PolyType =>
527+
val locked = ctx.typerState.ownedVars
525528
val tvars = constrained(pt)
526529
val mt = pt.instantiate(tvars).asInstanceOf[MethodType]
527530
scrutineeTp <:< mt.paramInfos(0)
528531
// force type inference to infer a narrower type: could be singleton
529532
// see tests/patmat/i4227.scala
530533
mt.paramInfos(0) <:< scrutineeTp
531-
instantiateSelected(mt, tvars)
532-
isFullyDefined(mt, ForceDegree.all)
534+
maximizeType(mt.paramInfos(0), Spans.NoSpan)
535+
if !(ctx.typerState.ownedVars -- locked).isEmpty then
536+
// constraining can create type vars out of wildcard types
537+
// (in legalBound, by using a LevelAvoidMap)
538+
// maximise will only do one pass at maximising the type vars in the target type
539+
// which means we can maximise to types that include other type vars
540+
// this fails TreeChecker's "non-empty constraint at end of $fusedPhase" check
541+
// e.g. run-macros/string-context-implicits
542+
// I can't prove that a second call won't also create type vars,
543+
// but I'd rather have an unassigned new-new type var, than an infinite loop.
544+
// After all, there's nothing strictly "wrong" with unassigned type vars,
545+
// it just fails TreeChecker's linting.
546+
maximizeType(mt.paramInfos(0), Spans.NoSpan)
533547
mt
534548
}
535549

@@ -543,7 +557,7 @@ object SpaceEngine {
543557
// Case unapplySeq:
544558
// 1. return the type `List[T]` where `T` is the element type of the unapplySeq return type `Seq[T]`
545559

546-
val resTp = ctx.typeAssigner.safeSubstMethodParams(mt, scrutineeTp :: Nil).finalResultType
560+
val resTp = wildApprox(ctx.typeAssigner.safeSubstMethodParams(mt, scrutineeTp :: Nil).finalResultType)
547561

548562
val sig =
549563
if (resTp.isRef(defn.BooleanClass))
@@ -564,20 +578,14 @@ object SpaceEngine {
564578
if (arity > 0)
565579
productSelectorTypes(resTp, unappSym.srcPos)
566580
else {
567-
val getTp = resTp.select(nme.get).finalResultType match
568-
case tp: TermRef if !tp.isOverloaded =>
569-
// Like widenTermRefExpr, except not recursively.
570-
// For example, in i17184 widen Option[foo.type]#get
571-
// to Option[foo.type] instead of Option[Int].
572-
tp.underlying.widenExpr
573-
case tp => tp
581+
val getTp = extractorMemberType(resTp, nme.get, unappSym.srcPos)
574582
if (argLen == 1) getTp :: Nil
575583
else productSelectorTypes(getTp, unappSym.srcPos)
576584
}
577585
}
578586
}
579587

580-
sig.map(_.annotatedToRepeated)
588+
sig.map { case tp: WildcardType => tp.bounds.hi case tp => tp }
581589
}
582590

583591
/** Whether the extractor covers the given type */
@@ -616,14 +624,36 @@ object SpaceEngine {
616624
case tp if tp.classSymbol.isAllOf(JavaEnum) => tp.classSymbol.children.map(_.termRef)
617625
// the class of a java enum value is the enum class, so this must follow SingletonType to not loop infinitely
618626

619-
case tp @ AppliedType(Parts(parts), targs) if tp.classSymbol.children.isEmpty =>
627+
case Childless(tp @ AppliedType(Parts(parts), targs)) =>
620628
// It might not obvious that it's OK to apply the type arguments of a parent type to child types.
621629
// But this is guarded by `tp.classSymbol.children.isEmpty`,
622630
// meaning we'll decompose to the same class, just not the same type.
623631
// For instance, from i15029, `decompose((X | Y).Field[T]) = [X.Field[T], Y.Field[T]]`.
624632
parts.map(tp.derivedAppliedType(_, targs))
625633

626-
case tp if tp.isDecomposableToChildren =>
634+
case tpOriginal if tpOriginal.isDecomposableToChildren =>
635+
// isDecomposableToChildren uses .classSymbol.is(Sealed)
636+
// But that classSymbol could be from an AppliedType
637+
// where the type constructor is a non-class type
638+
// E.g. t11620 where `?1.AA[X]` returns as "sealed"
639+
// but using that we're not going to infer A1[X] and A2[X]
640+
// but end up with A1[<?>] and A2[<?>].
641+
// So we widen (like AppliedType superType does) away
642+
// non-class type constructors.
643+
//
644+
// Can't use `tpOriginal.baseType(cls)` because it causes
645+
// i15893 to return exhaustivity warnings, because instead of:
646+
// <== refineUsingParent(N, class Succ, []) = Succ[<? <: NatT>]
647+
// <== isSub(Succ[<? <: NatT>] <:< Succ[Succ[<?>]]) = true
648+
// we get
649+
// <== refineUsingParent(NatT, class Succ, []) = Succ[NatT]
650+
// <== isSub(Succ[NatT] <:< Succ[Succ[<?>]]) = false
651+
def getAppliedClass(tp: Type): Type = tp match
652+
case tp @ AppliedType(_: HKTypeLambda, _) => tp
653+
case tp @ AppliedType(tycon: TypeRef, _) if tycon.symbol.isClass => tp
654+
case tp @ AppliedType(tycon: TypeProxy, _) => getAppliedClass(tycon.superType.applyIfParameterized(tp.args))
655+
case tp => tp
656+
val tp = getAppliedClass(tpOriginal)
627657
def getChildren(sym: Symbol): List[Symbol] =
628658
sym.children.flatMap { child =>
629659
if child eq sym then List(sym) // i3145: sealed trait Baz, val x = new Baz {}, Baz.children returns Baz...
@@ -676,6 +706,12 @@ object SpaceEngine {
676706
final class PartsExtractor(val get: List[Type]) extends AnyVal:
677707
def isEmpty: Boolean = get == ListOfNoType
678708

709+
object Childless:
710+
def unapply(tp: Type)(using Context): Result =
711+
Result(if tp.classSymbol.children.isEmpty then tp else NoType)
712+
class Result(val get: Type) extends AnyVal:
713+
def isEmpty: Boolean = !get.exists
714+
679715
/** Show friendly type name with current scope in mind
680716
*
681717
* E.g. C.this.B --> B if current owner is C
@@ -772,12 +808,15 @@ object SpaceEngine {
772808
doShow(s)
773809
}
774810

775-
private def exhaustivityCheckable(sel: Tree)(using Context): Boolean = {
811+
extension (self: Type) private def stripUnsafeNulls()(using Context): Type =
812+
if Nullables.unsafeNullsEnabled then self.stripNull() else self
813+
814+
private def exhaustivityCheckable(sel: Tree)(using Context): Boolean = trace(i"exhaustivityCheckable($sel ${sel.className})") {
776815
val seen = collection.mutable.Set.empty[Symbol]
777816

778817
// Possible to check everything, but be compatible with scalac by default
779-
def isCheckable(tp: Type): Boolean =
780-
val tpw = tp.widen.dealias
818+
def isCheckable(tp: Type): Boolean = trace(i"isCheckable($tp ${tp.className})"):
819+
val tpw = tp.widen.dealias.stripUnsafeNulls()
781820
val classSym = tpw.classSymbol
782821
classSym.is(Sealed) && !tpw.isLargeGenericTuple || // exclude large generic tuples from exhaustivity
783822
// requires an unknown number of changes to make work
@@ -812,7 +851,7 @@ object SpaceEngine {
812851
/** Return the underlying type of non-module, non-constant, non-enum case singleton types.
813852
* Also widen ExprType to its result type, and rewrap any annotation wrappers.
814853
* For example, with `val opt = None`, widen `opt.type` to `None.type`. */
815-
def toUnderlying(tp: Type)(using Context): Type = trace(i"toUnderlying($tp)")(tp match {
854+
def toUnderlying(tp: Type)(using Context): Type = trace(i"toUnderlying($tp ${tp.className})")(tp match {
816855
case _: ConstantType => tp
817856
case tp: TermRef if tp.symbol.is(Module) => tp
818857
case tp: TermRef if tp.symbol.isAllOf(EnumCase) => tp
@@ -823,7 +862,7 @@ object SpaceEngine {
823862
})
824863

825864
def checkExhaustivity(m: Match)(using Context): Unit = trace(i"checkExhaustivity($m)") {
826-
val selTyp = toUnderlying(m.selector.tpe).dealias
865+
val selTyp = toUnderlying(m.selector.tpe.stripUnsafeNulls()).dealias
827866
val targetSpace = trace(i"targetSpace($selTyp)")(project(selTyp))
828867

829868
val patternSpace = Or(m.cases.foldLeft(List.empty[Space]) { (acc, x) =>
@@ -901,9 +940,6 @@ object SpaceEngine {
901940
}
902941

903942
def checkMatch(m: Match)(using Context): Unit =
904-
checkMatchExhaustivityOnly(m)
905-
if reachabilityCheckable(m.selector) then checkReachability(m)
906-
907-
def checkMatchExhaustivityOnly(m: Match)(using Context): Unit =
908943
if exhaustivityCheckable(m.selector) then checkExhaustivity(m)
944+
if reachabilityCheckable(m.selector) then checkReachability(m)
909945
}

tests/warn/i15893.min.scala

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
sealed trait NatT
2+
case class Zero() extends NatT
3+
case class Succ[+N <: NatT](n: N) extends NatT
4+
5+
type Mod2[N <: NatT] <: NatT = N match
6+
case Zero => Zero
7+
case Succ[Zero] => Succ[Zero]
8+
case Succ[Succ[predPredN]] => Mod2[predPredN]
9+
10+
def dependentlyTypedMod2[N <: NatT](n: N): Mod2[N] = n match
11+
case Zero(): Zero => Zero() // warn
12+
case Succ(Zero()): Succ[Zero] => Succ(Zero()) // warn
13+
case Succ(Succ(predPredN)): Succ[Succ[?]] => dependentlyTypedMod2(predPredN) // warn

tests/warn/i20121.scala

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
sealed trait T_A[A, B]
2+
type X = T_A[Byte, Byte]
3+
4+
case class CC_B[A](a: A) extends T_A[A, X]
5+
6+
val v_a: T_A[X, X] = CC_B(null)
7+
val v_b = v_a match
8+
case CC_B(_) => 0 // warn: unreachable
9+
case _ => 1
10+
// for CC_B[A] to match T_A[X, X]
11+
// A := X
12+
// so require X, aka T_A[Byte, Byte]
13+
// which isn't instantiable, outside of null

tests/warn/i20122.scala

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
sealed trait T_B[C, D]
2+
3+
case class CC_A()
4+
case class CC_B[A, C](a: A) extends T_B[C, CC_A]
5+
case class CC_C[C, D](a: T_B[C, D]) extends T_B[Int, CC_A]
6+
case class CC_E(a: CC_C[Char, Byte])
7+
8+
val v_a: T_B[Int, CC_A] = CC_B(CC_E(CC_C(null)))
9+
val v_b = v_a match
10+
case CC_B(CC_E(CC_C(_))) => 0 // warn: unreachable
11+
case _ => 1
12+
// for CC_B[A, C] to match T_B[C, CC_A]
13+
// C <: Int, ok
14+
// A <: CC_E, ok
15+
// but you need a CC_C[Char, Byte]
16+
// which requires a T_B[Char, Byte]
17+
// which isn't instantiable, outside of null

tests/warn/i20123.scala

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
sealed trait T_A[A, B]
2+
sealed trait T_B[C]
3+
4+
case class CC_D[A, C]() extends T_A[A, C]
5+
case class CC_E() extends T_B[Nothing]
6+
case class CC_G[A, C](c: C) extends T_A[A, C]
7+
8+
val v_a: T_A[Boolean, T_B[Boolean]] = CC_G(null)
9+
val v_b = v_a match {
10+
case CC_D() => 0
11+
case CC_G(_) => 1 // warn: unreachable
12+
// for CC_G[A, C] to match T_A[Boolean, T_B[Boolean]]
13+
// A := Boolean, which is ok
14+
// C := T_B[Boolean],
15+
// which isn't instantiable, outside of null
16+
}

tests/warn/i20128.scala

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
sealed trait T_A[A]
2+
case class CC_B[A](a: T_A[A]) extends T_A[Byte]
3+
case class CC_E[A](b: T_A[A]) extends T_A[Byte]
4+
5+
val v_a: T_A[Byte] = CC_E(CC_B(null))
6+
val v_b: Int = v_a match { // warn: not exhaustive
7+
case CC_E(CC_E(_)) => 0
8+
case CC_B(_) => 1
9+
}

tests/warn/i20129.scala

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
sealed trait T_A[A]
2+
case class CC_B[A](a: T_A[A], c: T_A[A]) extends T_A[Char]
3+
case class CC_C[A]() extends T_A[A]
4+
case class CC_G() extends T_A[Char]
5+
6+
val v_a: T_A[Char] = CC_B(CC_G(), CC_C())
7+
val v_b: Int = v_a match { // warn: not exhaustive
8+
case CC_C() => 0
9+
case CC_G() => 1
10+
case CC_B(CC_B(_, _), CC_C()) => 2
11+
case CC_B(CC_C(), CC_C()) => 3
12+
case CC_B(_, CC_G()) => 4
13+
case CC_B(_, CC_B(_, _)) => 5
14+
}

tests/warn/i20130.scala

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
sealed trait T_A[B]
2+
sealed trait T_B[C]
3+
case class CC_B[C]() extends T_A[T_B[C]]
4+
case class CC_C[B, C](c: T_A[B], d: T_B[C]) extends T_B[C]
5+
case class CC_E[C]() extends T_B[C]
6+
7+
val v_a: T_B[Int] = CC_C(null, CC_E())
8+
val v_b: Int = v_a match { // warn: not exhaustive
9+
case CC_C(_, CC_C(_, _)) => 0
10+
case CC_E() => 5
11+
}

tests/warn/i20131.scala

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
sealed trait Foo
2+
case class Foo1() extends Foo
3+
case class Foo2[A, B]() extends Foo
4+
5+
sealed trait Bar[A, B]
6+
case class Bar1[A, C, D](a: Bar[C, D]) extends Bar[A, Bar[C, D]]
7+
case class Bar2[ C, D](b: Bar[C, D], c: Foo) extends Bar[Bar1[Int, Byte, Int], Bar[C, D]]
8+
9+
class Test:
10+
def m1(bar: Bar[Bar1[Int, Byte, Int], Bar[Char, Char]]): Int = bar match
11+
case Bar1(_) => 0
12+
case Bar2(_, Foo2()) => 1
13+
def t1 = m1(Bar2(null, Foo1()))
14+
// for Bar2[C, D] to match the scrutinee
15+
// C := Char and D := Char
16+
// which requires a Bar[Char, Char]
17+
// which isn't instantiable, outside of null

tests/warn/i20132.alt.scala

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
sealed trait Foo[A]
2+
case class Bar[C](x: Foo[C]) extends Foo[C]
3+
case class End[B]() extends Foo[B]
4+
class Test:
5+
def m1[M](foo: Foo[M]): Int = foo match // warn: not exhaustive
6+
case End() => 0
7+
case Bar(End()) => 1
8+
def t1 = m1[Int](Bar[Int](Bar[Int](End[Int]())))

tests/warn/i20132.future-Left.scala

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//> using options -Yexplicit-nulls -Yno-flexible-types
2+
3+
import scala.language.unsafeNulls
4+
5+
import java.util.concurrent.CompletableFuture
6+
import scala.jdk.CollectionConverters._
7+
8+
class Test1:
9+
def m1 =
10+
val fut: CompletableFuture[Either[String, List[String]]] = ???
11+
fut.thenApply:
12+
case Right(edits: List[String]) => edits.asJava
13+
case Left(error: String) => throw new Exception(error)

tests/warn/i20132.list-Seq.scala

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class D1
2+
class D2
3+
4+
class Test1:
5+
type Ds = List[D1] | List[D2]
6+
def m1(dss: List[Ds]) =
7+
dss.flatMap:
8+
case Seq(d) => Some(1)
9+
case Seq(head, tail*) => Some(2)
10+
case Seq() => None

tests/warn/i20132.scala

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
sealed trait Foo[A]
2+
case class Bar[C](x: Foo[C]) extends Foo[Int]
3+
case class End[B]() extends Foo[B]
4+
class Test:
5+
def m1[M](foo: Foo[M]): Int = foo match // warn: not exhaustive
6+
case End() => 0
7+
case Bar(End()) => 1
8+
def t1 = m1[Int](Bar[Int](Bar[Int](End[Int]())))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- [E029] Pattern Match Exhaustivity Warning: tests/warn/i20132.stream-Tuple2.safeNulls.scala:8:24 ---------------------
2+
8 | xs.asJava.forEach { case (a, b) => // warn
3+
| ^
4+
| match may not be exhaustive.
5+
|
6+
| It would fail on pattern case: _: Null
7+
|
8+
| longer explanation available when compiling with `-explain`

0 commit comments

Comments
 (0)