Skip to content

Commit 60fb562

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 d3a26e2 commit 60fb562

File tree

10 files changed

+104
-9
lines changed

10 files changed

+104
-9
lines changed

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -756,8 +756,13 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
756756
}
757757

758758
def tryBaseType(cls2: Symbol) = {
759+
val allowBaseType = caseLambda.eq(NoType) || (tp1 match {
760+
case tp: TypeRef if tp.symbol.isClass => true
761+
case AppliedType(tycon: TypeRef, _) if tycon.symbol.isClass => true
762+
case _ => false
763+
})
759764
val base = nonExprBaseType(tp1, cls2)
760-
if (base.exists && (base `ne` tp1))
765+
if (base.exists && base.ne(tp1) && allowBaseType)
761766
isSubType(base, tp2, if (tp1.isRef(cls2)) approx else approx.addLow) ||
762767
base.isInstanceOf[OrType] && fourthTry
763768
// if base is a disjunction, this might have come from a tp1 type that
@@ -776,7 +781,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
776781
|| narrowGADTBounds(tp1, tp2, approx, isUpper = true))
777782
&& (tp2.isAny || GADTusage(tp1.symbol))
778783

779-
isSubType(hi1, tp2, approx.addLow) || compareGADT || tryLiftedToThis1
784+
caseLambda.eq(NoType) && isSubType(hi1, tp2, approx.addLow) || compareGADT || tryLiftedToThis1
780785
case _ =>
781786
def isNullable(tp: Type): Boolean = tp.widenDealias match {
782787
case tp: TypeRef => tp.symbol.isNullableClass
@@ -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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1124,7 +1124,7 @@ trait Implicits:
11241124
/** The expected type where parameters and uninstantiated typevars are replaced by wildcard types */
11251125
val wildProto: Type =
11261126
if argument.isEmpty then wildApprox(pt)
1127-
else ViewProto(wildApprox(argument.tpe.widen), wildApprox(pt))
1127+
else ViewProto(wildApprox(argument.tpe.widen.normalized), wildApprox(pt))
11281128
// Not clear whether we need to drop the `.widen` here. All tests pass with it in place, though.
11291129

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ class CompilationTests {
193193
@Test def runAll: Unit = {
194194
implicit val testGroup: TestGroup = TestGroup("runAll")
195195
aggregateTests(
196+
compileFile("tests/run-custom-args/typeclass-derivation1.scala", defaultOptions.without(yCheckOptions: _*)),
196197
compileFile("tests/run-custom-args/tuple-cons.scala", allowDeepSubtypes),
197198
compileFile("tests/run-custom-args/i5256.scala", allowDeepSubtypes),
198199
compileFile("tests/run-custom-args/fors.scala", defaultOptions.and("-source", "future")),

tests/neg/11982.scala

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

Lines changed: 23 additions & 1 deletion
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

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

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

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

Lines changed: 2 additions & 1 deletion
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

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