Skip to content

Commit 4f24531

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 7801c57 commit 4f24531

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
@@ -383,9 +383,9 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling {
383383
narrowGADTBounds(tp2, tp1, isUpper = false)) &&
384384
GADTusage(tp2.symbol)
385385
}
386-
((frozenConstraint || !isCappable(tp1)) && isSubType(tp1, lo2) ||
387-
compareGADT ||
388-
fourthTry(tp1, tp2))
386+
val tryLowerFirst = frozenConstraint || !isCappable(tp1)
387+
if (tryLowerFirst) isSubType(tp1, lo2) || compareGADT || fourthTry(tp1, tp2)
388+
else compareGADT || fourthTry(tp1, tp2) || isSubType(tp1, lo2)
389389

390390
case _ =>
391391
val cls2 = tp2.symbol
@@ -1076,13 +1076,15 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling {
10761076
case _ => proto.isMatchedBy(tp)
10771077
}
10781078

1079-
/** Can type `tp` be constrained from above by adding a constraint to
1080-
* a typevar that it refers to? In that case we have to be careful not
1081-
* to approximate with the lower bound of a type in `thirdTry`. Instead,
1082-
* we should first unroll `tp1` until we hit the type variable and bind the
1083-
* type variable with (the corresponding type in) `tp2` instead.
1079+
/** Can type `tp` be constrained from above, either by adding a constraint to
1080+
* a typevar that it refers to, or by narrowing a GADT bound? In that case we have
1081+
* to be careful not to approximate with the lower bound of a type in `thirdTry`.
1082+
* Instead, we should first unroll `tp1` until we hit the type variable and bind the
1083+
* type variable with (the corresponding type in) `tp2` instead. Or, in the
1084+
* case of a GADT bounded typeref, we should narrow with `tp2` instead of its lower bound.
10841085
*/
10851086
private def isCappable(tp: Type): Boolean = tp match {
1087+
case tp: TypeRef => ctx.gadt.bounds.contains(tp.symbol)
10861088
case tp: TypeParamRef => constraint contains tp
10871089
case tp: TypeProxy => isCappable(tp.underlying)
10881090
case tp: AndOrType => isCappable(tp.tp1) || isCappable(tp.tp2)
@@ -1096,7 +1098,7 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling {
10961098
private def narrowGADTBounds(tr: NamedType, bound: Type, isUpper: Boolean): Boolean =
10971099
ctx.mode.is(Mode.GADTflexible) && !frozenConstraint && {
10981100
val tparam = tr.symbol
1099-
typr.println(i"narrow gadt bound of $tparam: ${tparam.info} from ${if (isUpper) "above" else "below"} to $bound ${bound.isRef(tparam)}")
1101+
gadts.println(i"narrow gadt bound of $tparam: ${tparam.info} from ${if (isUpper) "above" else "below"} to $bound ${bound.toString} ${bound.isRef(tparam)}")
11001102
if (bound.isRef(tparam)) false
11011103
else {
11021104
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/i3627.scala", allowDeepSubtypes) +
180181
compileFile("../tests/neg-custom-args/typers.scala", allowDoubleBindings) +

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)