Skip to content

Commit d1957e5

Browse files
Don't follow BaseType of abstract binders in MT reduction
Fix #11982 and the associated soundness problem. The issue with the behavior on master arises from the fact that type binder of match types might change as context gets more precise, which results in a single match type reducing in two different ways. This issue comes from the fact that subtyping looks into base types, and is thus able to match a type such as `T <: Tuple2[Int, Int]` against a pattern `case Tuple2[a, b]`, even if the best solutions for `a` and `b` in the current context are not guaranteed to be the best solution in more precise contexts (such as at call site in the added test case).
1 parent ee6733a commit d1957e5

File tree

10 files changed

+104
-9
lines changed

10 files changed

+104
-9
lines changed

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

+8-3
Original file line numberDiff line numberDiff line change
@@ -744,8 +744,13 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
744744
}
745745

746746
def tryBaseType(cls2: Symbol) = {
747+
val allowBaseType = caseLambda.eq(NoType) || (tp1 match {
748+
case tp: TypeRef if tp.symbol.isClass => true
749+
case AppliedType(tycon: TypeRef, _) if tycon.symbol.isClass => true
750+
case _ => false
751+
})
747752
val base = nonExprBaseType(tp1, cls2)
748-
if (base.exists && (base `ne` tp1))
753+
if (base.exists && base.ne(tp1) && allowBaseType)
749754
isSubType(base, tp2, if (tp1.isRef(cls2)) approx else approx.addLow) ||
750755
base.isInstanceOf[OrType] && fourthTry
751756
// if base is a disjunction, this might have come from a tp1 type that
@@ -764,7 +769,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
764769
|| narrowGADTBounds(tp1, tp2, approx, isUpper = true))
765770
&& (tp2.isAny || GADTusage(tp1.symbol))
766771

767-
isSubType(hi1, tp2, approx.addLow) || compareGADT || tryLiftedToThis1
772+
caseLambda.eq(NoType) && isSubType(hi1, tp2, approx.addLow) || compareGADT || tryLiftedToThis1
768773
case _ =>
769774
// `Mode.RelaxedOverriding` is only enabled when checking Java overriding
770775
// in explicit nulls, and `Null` becomes a bottom type, which allows
@@ -2536,7 +2541,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
25362541
override def apply(x: Boolean, t: Type) =
25372542
x && {
25382543
t match {
2539-
case tp: TypeRef if tp.symbol.isAbstractOrParamType => false
2544+
case tp: TypeRef if !tp.symbol.isClass => false
25402545
case _: SkolemType | _: TypeVar | _: TypeParamRef => false
25412546
case _ => foldOver(x, t)
25422547
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -1137,7 +1137,7 @@ trait Implicits:
11371137
/** The expected type where parameters and uninstantiated typevars are replaced by wildcard types */
11381138
val wildProto: Type =
11391139
if argument.isEmpty then wildApprox(pt)
1140-
else ViewProto(wildApprox(argument.tpe.widen), wildApprox(pt))
1140+
else ViewProto(wildApprox(argument.tpe.widen.normalized), wildApprox(pt))
11411141
// Not clear whether we need to drop the `.widen` here. All tests pass with it in place, though.
11421142

11431143
val isNotGiven: Boolean = wildProto.classSymbol == defn.NotGivenClass

compiler/test/dotty/tools/dotc/CompilationTests.scala

+1
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ class CompilationTests {
198198
@Test def runAll: Unit = {
199199
implicit val testGroup: TestGroup = TestGroup("runAll")
200200
aggregateTests(
201+
compileFile("tests/run-custom-args/typeclass-derivation1.scala", defaultOptions.without(yCheckOptions: _*)),
201202
compileFile("tests/run-custom-args/tuple-cons.scala", allowDeepSubtypes),
202203
compileFile("tests/run-custom-args/i5256.scala", allowDeepSubtypes),
203204
compileFile("tests/run-custom-args/fors.scala", defaultOptions.and("-source", "future")),

tests/neg/11982.scala

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// testCompilation 11982.scala
2+
type Head[X] = X match {
3+
case Tuple2[a, b] => a
4+
}
5+
6+
object Unpair {
7+
def unpair[X <: Tuple2[Any, Any]]: Head[X] = 1 // error
8+
unpair[Tuple2["msg", 42]]: "msg"
9+
}
10+
11+
12+
type Head2[X] = X match {
13+
case Tuple2[Tuple2[a, b], Tuple2[c, d]] => a
14+
}
15+
16+
object Unpair2 {
17+
def unpair[X <: Tuple2[Tuple2[Any, Any], Tuple2[Any, Any]]]: Head2[X] = 1 // error
18+
unpair[Tuple2[Tuple2["msg", 42], Tuple2[41, 40]]]: "msg"
19+
}
20+
21+
22+
type Head3[X] = X match {
23+
case Tuple2[a, b] => a
24+
}
25+
26+
object Unpair3 {
27+
def unpair[X <: Tuple2[Any, Any]]: Head3[Tuple2[X, X]] = (1, 2) // error
28+
unpair[Tuple2["msg", 42]]: ("msg", 42)
29+
}
30+
31+
trait Foo[+A, +B]
32+
33+
type Head4[X] = X match {
34+
case Foo[Foo[a, b], Foo[c, d]] => a
35+
}
36+
37+
object Unpair4 {
38+
def unpair[X <: Foo[Any, Any]]: Head4[Foo[X, X]] = 1 // error
39+
unpair[Foo["msg", 42]]: "msg"
40+
}

tests/neg/6570-1.scala

+23-1
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,32 @@ trait Root[A] {
2121

2222
class Asploder extends Root[Cov[Box[Int & String]]] {
2323
def thing = new Trait1 {} // error
24+
// ^
25+
// Found: Object with Trait1 {...}
26+
// Required: N[Box[Int & String]]
27+
//
28+
// Note: a match type could not be fully reduced:
29+
//
30+
// trying to reduce N[Box[Int & String]]
31+
// failed since selector Box[Int & String]
32+
// is uninhabited (there are no values of that type).
2433
}
2534

2635
object Main {
27-
def foo[T <: Cov[Box[Int]]](c: Root[T]): Trait2 = c.thing
36+
def foo[T <: Cov[Box[Int]]](c: Root[T]): Trait2 = c.thing // error
37+
// ^^^^^^^
38+
// Found: M[T]
39+
// Required: Trait2
40+
//
41+
// where: T is a type in method foo with bounds <: Cov[Box[Int]]
42+
//
43+
//
44+
// Note: a match type could not be fully reduced:
45+
//
46+
// trying to reduce M[T]
47+
// failed since selector T
48+
// does not match case Cov[x] => N[x]
49+
// and cannot be shown to be disjoint from it either.
2850

2951
def explode = foo(new Asploder)
3052

tests/neg/6570.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ object UpperBoundParametricVariant {
2121
trait Child[A <: Cov[Int]] extends Root[A]
2222

2323
// we reduce `M[T]` to `Trait2`, even though we cannot be certain of that
24-
def foo[T <: Cov[Int]](c: Child[T]): Trait2 = c.thing
24+
def foo[T <: Cov[Int]](c: Child[T]): Trait2 = c.thing // error
2525

2626
class Asploder extends Child[Cov[String & Int]] {
2727
def thing = new Trait1 {} // error
@@ -42,7 +42,7 @@ object InheritanceVariant {
4242

4343
trait Child extends Root { type B <: { type A <: Int } }
4444

45-
def foo(c: Child): Trait2 = c.thing
45+
def foo(c: Child): Trait2 = c.thing // error
4646

4747
class Asploder extends Child {
4848
type B = { type A = String & Int }
@@ -98,7 +98,7 @@ object UpperBoundVariant {
9898

9999
trait Child extends Root { type A <: Cov[Int] }
100100

101-
def foo(c: Child): Trait2 = c.thing
101+
def foo(c: Child): Trait2 = c.thing // error
102102

103103
class Asploder extends Child {
104104
type A = Cov[String & Int]

tests/pos/11982-a/119_1.scala

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
object Unpair {
2+
class Inv[T]
3+
4+
type Head[X] = X match {
5+
case Tuple2[a, b] => a
6+
}
7+
8+
def unpair[X <: Tuple2[Any, Any]]: Head[X] = ???
9+
}

tests/pos/11982-a/119_2.scala

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object UnpairApp {
2+
import Unpair._
3+
4+
val x: String = unpair[("msg", 42)]
5+
}

tests/pos/13491.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ object Rule {
8686
type RuleN[+L <: HList] = Rule[HNil, L]
8787

8888
def rule[I <: HList, O <: HList](r: Rule[I, O]): Rule[I, O] = ???
89-
implicit def valueMap[T](m: Map[String, T])(implicit h: HListable[T]): RuleN[h.Out] = ???
89+
90+
implicit def valueMap[T, Out0 <: HList](m: Map[String, T])(implicit h: HListable[T] { type Out = Out0 }): RuleN[Out0] = ???
9091
}
9192

9293
object Test {

tests/run/typeclass-derivation1.scala renamed to tests/run-custom-args/typeclass-derivation1.scala

+12
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,15 @@ object Test extends App {
9696
assert(!eq2.equals(yss, xss))
9797
assert(eq2.equals(yss, yss))
9898
}
99+
100+
// -Ycheck failure minimized to:
101+
// import scala.compiletime.*
102+
// object Eq {
103+
// inline def deriveForProduct[Elems <: Tuple](xs: Elems): Boolean = inline erasedValue[Elems] match {
104+
// case _: (elem *: elems1) =>
105+
// val xs1 = xs.asInstanceOf[elem *: elems1]
106+
// deriveForProduct(xs1.tail)
107+
// case _: EmptyTuple =>
108+
// true
109+
// }
110+
// }

0 commit comments

Comments
 (0)