Skip to content

Commit 6faeb58

Browse files
committed
Fix #1754: Don't narrow GADTs to lower bounds
neg/i1754.scala succeeded because a GADT bound for `A` in `A <: B` was narrowed to the lower bound of `B` (which was nothing) instead of `B` itself. Fixing this uncovered several other problems that were hidden by the overly aggressive narrowing "feature".
1 parent 06903f3 commit 6faeb58

File tree

6 files changed

+51
-12
lines changed

6 files changed

+51
-12
lines changed

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

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import StdNames.{nme, tpnme}
88
import collection.mutable
99
import util.{Stats, DotClass}
1010
import config.Config
11-
import config.Printers.{typr, constr, subtyping, noPrinter}
11+
import config.Printers.{typr, constr, subtyping, gadts, noPrinter}
1212
import TypeErasure.{erasedLub, erasedGlb}
1313
import TypeApplications._
1414
import scala.util.control.NonFatal
@@ -362,9 +362,9 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling {
362362
narrowGADTBounds(tp2, tp1, isUpper = false)) &&
363363
GADTusage(tp2.symbol)
364364
}
365-
((frozenConstraint || !isCappable(tp1)) && isSubType(tp1, lo2) ||
366-
compareGADT ||
367-
fourthTry(tp1, tp2))
365+
val tryLowerFirst = frozenConstraint || !isCappable(tp1)
366+
if (tryLowerFirst) isSubType(tp1, lo2) || compareGADT || fourthTry(tp1, tp2)
367+
else compareGADT || fourthTry(tp1, tp2) || isSubType(tp1, lo2)
368368

369369
case _ =>
370370
val cls2 = tp2.symbol
@@ -1055,13 +1055,15 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling {
10551055
case _ => proto.isMatchedBy(tp)
10561056
}
10571057

1058-
/** Can type `tp` be constrained from above by adding a constraint to
1059-
* a typevar that it refers to? In that case we have to be careful not
1060-
* to approximate with the lower bound of a type in `thirdTry`. Instead,
1061-
* we should first unroll `tp1` until we hit the type variable and bind the
1062-
* type variable with (the corresponding type in) `tp2` instead.
1058+
/** Can type `tp` be constrained from above, either by adding a constraint to
1059+
* a typevar that it refers to, or by narrowing a GADT bound? In that case we have
1060+
* to be careful not to approximate with the lower bound of a type in `thirdTry`.
1061+
* Instead, we should first unroll `tp1` until we hit the type variable and bind the
1062+
* type variable with (the corresponding type in) `tp2` instead. Or, in the
1063+
* case of a GADT bounded typeref, we should narrow with `tp2` instead of its lower bound.
10631064
*/
10641065
private def isCappable(tp: Type): Boolean = tp match {
1066+
case tp: TypeRef => ctx.gadt.bounds.contains(tp.symbol)
10651067
case tp: TypeParamRef => constraint contains tp
10661068
case tp: TypeProxy => isCappable(tp.underlying)
10671069
case tp: AndOrType => isCappable(tp.tp1) || isCappable(tp.tp2)
@@ -1075,7 +1077,7 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling {
10751077
private def narrowGADTBounds(tr: NamedType, bound: Type, isUpper: Boolean): Boolean =
10761078
ctx.mode.is(Mode.GADTflexible) && !frozenConstraint && {
10771079
val tparam = tr.symbol
1078-
typr.println(i"narrow gadt bound of $tparam: ${tparam.info} from ${if (isUpper) "above" else "below"} to $bound ${bound.isRef(tparam)}")
1080+
gadts.println(i"narrow gadt bound of $tparam: ${tparam.info} from ${if (isUpper) "above" else "below"} to $bound ${bound.toString} ${bound.isRef(tparam)}")
10791081
if (bound.isRef(tparam)) false
10801082
else {
10811083
val oldBounds = ctx.gadt.bounds(tparam)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ class CompilationTests extends ParallelTesting {
175175
compileFilesInDir("../tests/neg", defaultOptions) +
176176
compileFilesInDir("../tests/neg-tailcall", defaultOptions) +
177177
compileFilesInDir("../tests/neg-no-optimise", defaultOptions) +
178+
compileFile("../tests/neg-custom-args/i1754.scala", allowDeepSubtypes) +
178179
compileFile("../tests/neg-custom-args/i3246.scala", scala2Mode) +
179180
compileFile("../tests/neg-custom-args/typers.scala", allowDoubleBindings) +
180181
compileFile("../tests/neg-custom-args/overrideClass.scala", scala2Mode) +

tests/neg-custom-args/i1754.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
case class One[T](fst: T)
2+
3+
object Test {
4+
def bad[T](e: One[T]) = e match {
5+
case foo: One[a] =>
6+
val t: T = e.fst
7+
val nok: Nothing = t // error
8+
}
9+
}

tests/neg/boundspropagation.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ object test4 {
3333
class Tree[-S, -T >: Option[S]]
3434

3535
def g(x: Any): Tree[_, _ <: Option[N]] = x match {
36-
case y: Tree[_, _] => y // works now (because of capture conversion?)
36+
case y: Tree[_, _] => y // error -- used to work (because of capture conversion?)
3737
}
3838
}
3939
}

tests/pos/boundspropagation.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ object test1 {
1717
}
1818
}
1919
}
20+
21+
/** Does not work:
2022
object test2 {
2123
class Tree[S, T <: S]
2224
2325
class Base {
2426
def g(x: Any): Tree[_, _ <: Int] = x match {
25-
case y: Tree[Int @unchecked, _] => y
27+
case y: Tree[Int @unchecked, t] => y
2628
}
2729
}
2830
}
31+
*/

tests/pos/i1754.scala

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
object Test {
2+
import java.util.{ concurrent => juc }
3+
import scala.collection.concurrent
4+
import scala.collection.convert.Wrappers._
5+
6+
/**
7+
* Implicitly converts a Java ConcurrentMap to a Scala mutable ConcurrentMap.
8+
* The returned Scala ConcurrentMap is backed by the provided Java
9+
* ConcurrentMap and any side-effects of using it via the Scala interface will
10+
* be visible via the Java interface and vice versa.
11+
*
12+
* If the Java ConcurrentMap was previously obtained from an implicit or
13+
* explicit call of `asConcurrentMap(scala.collection.mutable.ConcurrentMap)`
14+
* then the original Scala ConcurrentMap will be returned.
15+
*
16+
* @param m The ConcurrentMap to be converted.
17+
* @return A Scala mutable ConcurrentMap view of the argument.
18+
*/
19+
implicit def mapAsScalaConcurrentMap[A, B](m: juc.ConcurrentMap[A, B]): concurrent.Map[A, B] = m match {
20+
case null => null
21+
case cmw: ConcurrentMapWrapper[_, _] => cmw.underlying
22+
case _ => new JConcurrentMapWrapper(m)
23+
}
24+
}

0 commit comments

Comments
 (0)