Skip to content

Commit 1a5bff6

Browse files
Backport "Fix a bundle of patmat issues" to 3.3.4 (#21549)
Backports #21000 and #21532 to 3.3.4-RC2. Fixes regression introduced in RC1
2 parents 419cfe6 + 610988c commit 1a5bff6

17 files changed

+234
-30
lines changed

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

Lines changed: 2 additions & 12 deletions
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

Lines changed: 60 additions & 18 deletions
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.*
@@ -359,7 +361,7 @@ object SpaceEngine {
359361
val funRef = fun1.tpe.asInstanceOf[TermRef]
360362
if (fun.symbol.name == nme.unapplySeq)
361363
val (arity, elemTp, resultTp) = unapplySeqInfo(fun.tpe.widen.finalResultType, fun.srcPos)
362-
if (fun.symbol.owner == defn.SeqFactoryClass && defn.ListType.appliedTo(elemTp) <:< pat.tpe)
364+
if fun.symbol.owner == defn.SeqFactoryClass && pat.tpe.hasClassSymbol(defn.ListClass) then
363365
// The exhaustivity and reachability logic already handles decomposing sum types (into its subclasses)
364366
// and product types (into its components). To get better counter-examples for patterns that are of type
365367
// List (or a super-type of list, like LinearSeq) we project them into spaces that use `::` and Nil.
@@ -531,14 +533,26 @@ object SpaceEngine {
531533
val mt: MethodType = unapp.widen match {
532534
case mt: MethodType => mt
533535
case pt: PolyType =>
536+
val locked = ctx.typerState.ownedVars
534537
val tvars = constrained(pt)
535538
val mt = pt.instantiate(tvars).asInstanceOf[MethodType]
536539
scrutineeTp <:< mt.paramInfos(0)
537540
// force type inference to infer a narrower type: could be singleton
538541
// see tests/patmat/i4227.scala
539542
mt.paramInfos(0) <:< scrutineeTp
540-
instantiateSelected(mt, tvars)
541-
isFullyDefined(mt, ForceDegree.all)
543+
maximizeType(mt.paramInfos(0), Spans.NoSpan)
544+
if !(ctx.typerState.ownedVars -- locked).isEmpty then
545+
// constraining can create type vars out of wildcard types
546+
// (in legalBound, by using a LevelAvoidMap)
547+
// maximise will only do one pass at maximising the type vars in the target type
548+
// which means we can maximise to types that include other type vars
549+
// this fails TreeChecker's "non-empty constraint at end of $fusedPhase" check
550+
// e.g. run-macros/string-context-implicits
551+
// I can't prove that a second call won't also create type vars,
552+
// but I'd rather have an unassigned new-new type var, than an infinite loop.
553+
// After all, there's nothing strictly "wrong" with unassigned type vars,
554+
// it just fails TreeChecker's linting.
555+
maximizeType(mt.paramInfos(0), Spans.NoSpan)
542556
mt
543557
}
544558

@@ -552,7 +566,7 @@ object SpaceEngine {
552566
// Case unapplySeq:
553567
// 1. return the type `List[T]` where `T` is the element type of the unapplySeq return type `Seq[T]`
554568

555-
val resTp = ctx.typeAssigner.safeSubstMethodParams(mt, scrutineeTp :: Nil).finalResultType
569+
val resTp = wildApprox(ctx.typeAssigner.safeSubstMethodParams(mt, scrutineeTp :: Nil).finalResultType)
556570

557571
val sig =
558572
if (resTp.isRef(defn.BooleanClass))
@@ -573,14 +587,14 @@ object SpaceEngine {
573587
if (arity > 0)
574588
productSelectorTypes(resTp, unappSym.srcPos)
575589
else {
576-
val getTp = resTp.select(nme.get).finalResultType.widenTermRefExpr
590+
val getTp = extractorMemberType(resTp, nme.get, unappSym.srcPos)
577591
if (argLen == 1) getTp :: Nil
578592
else productSelectorTypes(getTp, unappSym.srcPos)
579593
}
580594
}
581595
}
582596

583-
sig.map(_.annotatedToRepeated)
597+
sig.map { case tp: WildcardType => tp.bounds.hi case tp => tp }
584598
}
585599

586600
/** Whether the extractor covers the given type */
@@ -618,14 +632,36 @@ object SpaceEngine {
618632
case tp if tp.classSymbol.isAllOf(JavaEnumTrait) => tp.classSymbol.children.map(_.termRef)
619633
// the class of a java enum value is the enum class, so this must follow SingletonType to not loop infinitely
620634

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

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

717+
object Childless:
718+
def unapply(tp: Type)(using Context): Result =
719+
Result(if tp.classSymbol.children.isEmpty then tp else NoType)
720+
class Result(val get: Type) extends AnyVal:
721+
def isEmpty: Boolean = !get.exists
722+
681723
/** Show friendly type name with current scope in mind
682724
*
683725
* E.g. C.this.B --> B if current owner is C
@@ -774,12 +816,15 @@ object SpaceEngine {
774816
doShow(s)
775817
}
776818

777-
private def exhaustivityCheckable(sel: Tree)(using Context): Boolean = {
819+
extension (self: Type) private def stripUnsafeNulls()(using Context): Type =
820+
if Nullables.unsafeNullsEnabled then self.stripNull else self
821+
822+
private def exhaustivityCheckable(sel: Tree)(using Context): Boolean = trace(i"exhaustivityCheckable($sel ${sel.className})") {
778823
val seen = collection.mutable.Set.empty[Symbol]
779824

780825
// Possible to check everything, but be compatible with scalac by default
781-
def isCheckable(tp: Type): Boolean =
782-
val tpw = tp.widen.dealias
826+
def isCheckable(tp: Type): Boolean = trace(i"isCheckable($tp ${tp.className})"):
827+
val tpw = tp.widen.dealias.stripUnsafeNulls()
783828
val classSym = tpw.classSymbol
784829
classSym.is(Sealed) && !tpw.isLargeGenericTuple || // exclude large generic tuples from exhaustivity
785830
// requires an unknown number of changes to make work
@@ -814,7 +859,7 @@ object SpaceEngine {
814859
/** Return the underlying type of non-module, non-constant, non-enum case singleton types.
815860
* Also widen ExprType to its result type, and rewrap any annotation wrappers.
816861
* For example, with `val opt = None`, widen `opt.type` to `None.type`. */
817-
def toUnderlying(tp: Type)(using Context): Type = trace(i"toUnderlying($tp)")(tp match {
862+
def toUnderlying(tp: Type)(using Context): Type = trace(i"toUnderlying($tp ${tp.className})")(tp match {
818863
case _: ConstantType => tp
819864
case tp: TermRef if tp.symbol.is(Module) => tp
820865
case tp: TermRef if tp.symbol.isAllOf(EnumCase) => tp
@@ -825,7 +870,7 @@ object SpaceEngine {
825870
})
826871

827872
def checkExhaustivity(m: Match)(using Context): Unit = trace(i"checkExhaustivity($m)") {
828-
val selTyp = toUnderlying(m.selector.tpe).dealias
873+
val selTyp = toUnderlying(m.selector.tpe.stripUnsafeNulls()).dealias
829874
val targetSpace = trace(i"targetSpace($selTyp)")(project(selTyp))
830875

831876
val patternSpace = Or(m.cases.foldLeft(List.empty[Space]) { (acc, x) =>
@@ -903,9 +948,6 @@ object SpaceEngine {
903948
}
904949

905950
def checkMatch(m: Match)(using Context): Unit =
906-
checkMatchExhaustivityOnly(m)
907-
if reachabilityCheckable(m.selector) then checkReachability(m)
908-
909-
def checkMatchExhaustivityOnly(m: Match)(using Context): Unit =
910951
if exhaustivityCheckable(m.selector) then checkExhaustivity(m)
952+
if reachabilityCheckable(m.selector) then checkReachability(m)
911953
}

tests/warn/i15893.min.scala

Lines changed: 13 additions & 0 deletions
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

Lines changed: 13 additions & 0 deletions
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

Lines changed: 17 additions & 0 deletions
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

Lines changed: 16 additions & 0 deletions
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

Lines changed: 9 additions & 0 deletions
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

Lines changed: 14 additions & 0 deletions
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

Lines changed: 11 additions & 0 deletions
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

Lines changed: 17 additions & 0 deletions
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

Lines changed: 8 additions & 0 deletions
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.list-Seq.scala

Lines changed: 10 additions & 0 deletions
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

Lines changed: 8 additions & 0 deletions
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]())))

tests/warn/i20132.wo.scala

Lines changed: 8 additions & 0 deletions
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
6+
case End() => 0
7+
case Bar(_) => 1
8+
def t1 = m1[Int](Bar[Int](Bar[Int](End[Int]())))

tests/warn/i21218.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
def Test[U, A](thisElem: A, thatElem: U) = {
2+
case object passedEnd
3+
val any: Seq[Any] = ???
4+
any.zip(any)
5+
.map {
6+
case (`passedEnd`, r: U @unchecked) => (thisElem, r)
7+
case (l: A @unchecked, `passedEnd`) => (l, thatElem)
8+
case t: (A, U) @unchecked => t // false-positive warning
9+
}
10+
}

tests/warn/i5422.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
sealed trait Foo[A[_]]
2+
3+
case class Bar[C[_], X](x: C[X]) extends Foo[C]
4+
case class End[B[_]]() extends Foo[B]
5+
6+
class Test:
7+
def foo[M[_]](foo: Foo[M]): Int = foo match
8+
case End() => 0
9+
case Bar(_) => 1

tests/warn/t11620.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
sealed trait A[+T0]
2+
case class A1[+T1](t1: T1) extends A[T1]
3+
case class A2[+T2](t2: T2) extends A[T2]
4+
sealed trait B[+T3] { type AA[+U] <: A[U] ; def a: AA[T3] }
5+
object B { def unapply[T4](b: B[T4]): Some[b.AA[T4]] = Some(b.a) }
6+
class Test:
7+
def m1[X](b: B[X]): X = b match
8+
case B(A1(v1)) => v1
9+
case B(A2(v2)) => v2

0 commit comments

Comments
 (0)