Skip to content

Commit c24ece5

Browse files
committed
Prune constraints that could turn into cycles
Fixes #864. Review by @smarter.
1 parent 9d8c92d commit c24ece5

File tree

3 files changed

+65
-3
lines changed

3 files changed

+65
-3
lines changed

src/dotty/tools/dotc/core/ConstraintHandling.scala

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ trait ConstraintHandling {
3535

3636
private def addOneBound(param: PolyParam, bound: Type, isUpper: Boolean): Boolean =
3737
!constraint.contains(param) || {
38+
def occursIn(bound: Type): Boolean = {
39+
val b = bound.dealias
40+
(b eq param) || {
41+
b match {
42+
case b: AndOrType => occursIn(b.tp1) || occursIn(b.tp2)
43+
case b: TypeVar => occursIn(b.origin)
44+
case _ => false
45+
}
46+
}
47+
}
48+
if (Config.checkConstraintsNonCyclicTrans)
49+
assert(!occursIn(bound), s"$param occurs in $bound")
3850
val c1 = constraint.narrowBound(param, bound, isUpper)
3951
(c1 eq constraint) || {
4052
constraint = c1
@@ -222,11 +234,46 @@ trait ConstraintHandling {
222234
//checkPropagated(s"adding $description")(true) // DEBUG in case following fails
223235
checkPropagated(s"added $description") {
224236
addConstraintInvocations += 1
237+
238+
/** Drop all constrained parameters that occur at the toplevel in bound.
239+
* The preconditions make sure that such parameters occur only
240+
* in one of two ways:
241+
*
242+
* 1.
243+
*
244+
* P <: Ts1 | ... | Tsm (m > 0)
245+
* Tsi = T1 & ... Tn (n >= 0)
246+
* Some of the Ti are constrained parameters
247+
*
248+
* 2.
249+
*
250+
* Ts1 & ... & Tsm <: P (m > 0)
251+
* Tsi = T1 | ... | Tn (n >= 0)
252+
* Some of the Ti are constrained parameters
253+
*
254+
* In each case we cannot record the relationship as an isLess, because
255+
* of he outer |/&. But we should not leave it in the constraint either
256+
* because that would risk making a parameter a subtype or supertype of a bound
257+
* where the parameter occurs again at toplevel, which leads to cycles
258+
* in the subtyping test. So we intentionally loosen the constraint in order
259+
* to keep it safe. A test case that demonstrates the problem is i864.scala.
260+
*/
261+
def prune(bound: Type): Type = bound match {
262+
case bound: AndOrType =>
263+
bound.derivedAndOrType(prune(bound.tp1), prune(bound.tp2))
264+
case bound: TypeVar if constraint contains bound.origin =>
265+
prune(bound.underlying)
266+
case bound: PolyParam if constraint contains bound =>
267+
if (fromBelow) defn.NothingType else defn.AnyType
268+
case _ =>
269+
bound
270+
}
225271
try bound match {
226272
case bound: PolyParam if constraint contains bound =>
227273
if (fromBelow) addLess(bound, param) else addLess(param, bound)
228274
case _ =>
229-
if (fromBelow) addLowerBound(param, bound) else addUpperBound(param, bound)
275+
val pbound = prune(bound)
276+
if (fromBelow) addLowerBound(param, pbound) else addUpperBound(param, pbound)
230277
}
231278
finally addConstraintInvocations -= 1
232279
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,10 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling {
124124
pendingSubTypes = new mutable.HashSet[(Type, Type)]
125125
ctx.log(s"!!! deep subtype recursion involving ${tp1.show} <:< ${tp2.show}, constraint = ${state.constraint.show}")
126126
ctx.log(s"!!! constraint = ${constraint.show}")
127-
assert(!ctx.settings.YnoDeepSubtypes.value) //throw new Error("deep subtype")
127+
//if (ctx.settings.YnoDeepSubtypes.value) {
128+
// new Error("deep subtype").printStackTrace()
129+
//}
130+
assert(!ctx.settings.YnoDeepSubtypes.value)
128131
if (Config.traceDeepSubTypeRecursions && !this.isInstanceOf[ExplainingTypeComparer])
129132
ctx.log(TypeComparer.explained(implicit ctx => ctx.typeComparer.isSubType(tp1, tp2)))
130133
}
@@ -1047,7 +1050,9 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling {
10471050
else hkCombine(tp1, tp2, tparams1, tparams2, op)
10481051
}
10491052

1050-
/** Try to distribute `&` inside type, detect and handle conflicts */
1053+
/** Try to distribute `&` inside type, detect and handle conflicts
1054+
* @pre !(tp1 <: tp2) && !(tp2 <:< tp1) -- these cases were handled before
1055+
*/
10511056
private def distributeAnd(tp1: Type, tp2: Type): Type = tp1 match {
10521057
// opportunistically merge same-named refinements
10531058
// this does not change anything semantically (i.e. merging or not merging

tests/pos/i864.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
object C {
2+
val a: Int = 1
3+
val b: Int = 2
4+
val c: Int = 2
5+
6+
trait X[T]
7+
implicit def u[A, B]: X[A | B] = new X[A | B] {}
8+
def y[T](implicit x: X[T]): T = ???
9+
val x: a.type & b.type | b.type & c.type = y
10+
}

0 commit comments

Comments
 (0)