Skip to content

Commit 437920f

Browse files
committed
Refine join algorithm
This was detected when failing to compile the 2.13 library. The problem is minimized in test pos/padTo.scala. The failure arose because with the introduction of singletons in unions we get more "interesting" union types to approximate. But it can be also produced without singleton types.
1 parent c1db1b0 commit 437920f

File tree

2 files changed

+94
-21
lines changed

2 files changed

+94
-21
lines changed

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

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ trait TypeOps { this: Context => // TODO: Make standalone object.
131131
* class A extends C[A] with D
132132
* class B extends C[B] with D with E
133133
*
134-
* we approximate `A | B` by `C[A | B] with D`
134+
* we approximate `A | B` by `C[A | B] with D`.
135135
*/
136136
def orDominator(tp: Type): Type = {
137137

@@ -188,29 +188,68 @@ trait TypeOps { this: Context => // TODO: Make standalone object.
188188
case _ => false
189189
}
190190

191+
// Step 1: Get RecTypes and ErrorTypes out of the way,
191192
tp1 match {
192-
case tp1: RecType =>
193-
tp1.rebind(approximateOr(tp1.parent, tp2))
194-
case tp1: TypeProxy if !isClassRef(tp1) =>
195-
orDominator(tp1.superType | tp2)
196-
case err: ErrorType =>
197-
err
193+
case tp1: RecType => return tp1.rebind(approximateOr(tp1.parent, tp2))
194+
case err: ErrorType => return err
198195
case _ =>
199-
tp2 match {
200-
case tp2: RecType =>
201-
tp2.rebind(approximateOr(tp1, tp2.parent))
202-
case tp2: TypeProxy if !isClassRef(tp2) =>
203-
orDominator(tp1 | tp2.superType)
204-
case err: ErrorType =>
205-
err
206-
case _ =>
207-
val commonBaseClasses = tp.mapReduceOr(_.baseClasses)(intersect)
208-
val doms = dominators(commonBaseClasses, Nil)
209-
def baseTp(cls: ClassSymbol): Type =
210-
tp.baseType(cls).mapReduceOr(identity)(mergeRefinedOrApplied)
211-
doms.map(baseTp).reduceLeft(AndType.apply)
212-
}
213196
}
197+
tp2 match {
198+
case tp2: RecType => return tp2.rebind(approximateOr(tp1, tp2.parent))
199+
case err: ErrorType => return err
200+
case _ =>
201+
}
202+
203+
// Step 2: Try to widen either side. This is tricky and currently incomplete.
204+
// An illustration is in test pos/padTo.scala: Here we nee to compute the join of
205+
//
206+
// `A | C` under the constraints `B >: A` and `C <: B`
207+
//
208+
// where `A, B, C` are type parameters.
209+
// Widening `A` to its upper bound would give `Any | C`, i.e. `Any`.
210+
// But widening `C` first would give `A | B` and then `B`.
211+
// So we need to widen `C` first. But how to decide this in general?
212+
// In the algorithm below, we widen both sides, and then check whether
213+
// one widened type is a supertype of the other original type, in which
214+
// case we can immediately pick that widened type as the join.
215+
// If that yields no result, we pick the second widened type if it is
216+
// a subtype of the first widened type and the first widened type otherwise.
217+
// At this last step we lose possible solutions, since we have to make an
218+
// arbitrary choice which side to widen. A better solution would look at
219+
// the constituents of each operand (if the operand is an OrType again) and
220+
// try to widen them selectively in turn. But this might lead to a combinatorial
221+
// explosion of possibilities.
222+
//
223+
// Another approach could be to store information contained in lower bounds
224+
// on both sides. So if `B >: A` we'd also record that `A <: B` and therefore
225+
// widening `A` would yield `B` instead of `Any`, so we'd still be on the right track.
226+
// This looks feasible if lower bounds are type parameters, but tricky if they
227+
// are something else. We'd have to extract the strongest possible
228+
// constraint over all type parameters that is implied by a lower bound.
229+
// This looks related to an algorithmic problem arising in GADT matching.
230+
val tp1w = tp1 match {
231+
case tp1: TypeProxy if !isClassRef(tp1) => tp1.superType
232+
case _ => tp1
233+
}
234+
val tp2w = tp2 match {
235+
case tp2: TypeProxy if !isClassRef(tp2) => tp2.superType
236+
case _ => tp1
237+
}
238+
if ((tp1w ne tp1) || (tp2w ne tp2)) {
239+
return {
240+
if (tp1 frozen_<:< tp2w) tp2w
241+
else if (tp2 frozen_<:< tp1w) tp1w
242+
else if (tp2w frozen_<:< tp1w) orDominator(tp1 | tp2w)
243+
else orDominator(tp1w | tp2)
244+
}
245+
}
246+
247+
// Step 3: Intersect base classes of both sides
248+
val commonBaseClasses = tp.mapReduceOr(_.baseClasses)(intersect)
249+
val doms = dominators(commonBaseClasses, Nil)
250+
def baseTp(cls: ClassSymbol): Type =
251+
tp.baseType(cls).mapReduceOr(identity)(mergeRefinedOrApplied)
252+
doms.map(baseTp).reduceLeft(AndType.apply)
214253
}
215254

216255
tp match {

tests/pos/padTo.scala

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
object Test {
2+
abstract class AbstractIterator[A] extends Iterator[A] {
3+
override def padTo[B >: A](len: Int, elem: B): Iterator[B] = {
4+
val it = this
5+
new AbstractIterator[B] {
6+
private[this] var i = 0
7+
8+
// This illustrates a tricky situation for joins
9+
// The RHS of `val b` has type `A | elem.type` where `elem: B`
10+
// If we widen `A` first in the join we get a RHS of `Any` and a subsequent
11+
// type error. The right thing to do is to widen `elem.type` to `B` first.
12+
def next(): B = {
13+
val b =
14+
if (it.hasNext) it.next()
15+
else if (i < len) elem
16+
else Iterator.empty.next()
17+
i += 1
18+
b
19+
}
20+
21+
// Same problem, but without singleton types.
22+
// This one fails to compile in Scala 2.
23+
def f[C <: B](c: () => C): B = {
24+
val b =
25+
if (it.hasNext) it.next()
26+
else c()
27+
b
28+
}
29+
30+
def hasNext: Boolean = it.hasNext || i < len
31+
}
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)