Skip to content

Commit 2e9084e

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. Further join refinements
1 parent 1c6e998 commit 2e9084e

File tree

2 files changed

+112
-21
lines changed

2 files changed

+112
-21
lines changed

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

+78-21
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,11 @@ 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`.
135+
*
136+
* Before we do that, we try to find a common non-class supertype of T1 | ... | Tn
137+
* in a "best effort", ad-hoc way by selectively widening types in `T1, ..., Tn`
138+
* and stopping if the resulting union simplifies to a type that is not a disjunction.
135139
*/
136140
def orDominator(tp: Type): Type = {
137141

@@ -188,29 +192,82 @@ trait TypeOps { this: Context => // TODO: Make standalone object.
188192
case _ => false
189193
}
190194

195+
// Step 1: Get RecTypes and ErrorTypes out of the way,
191196
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
197+
case tp1: RecType => return tp1.rebind(approximateOr(tp1.parent, tp2))
198+
case err: ErrorType => return err
198199
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-
}
213200
}
201+
tp2 match {
202+
case tp2: RecType => return tp2.rebind(approximateOr(tp1, tp2.parent))
203+
case err: ErrorType => return err
204+
case _ =>
205+
}
206+
207+
// Step 2: Try to widen either side. This is tricky and incomplete.
208+
// An illustration is in test pos/padTo.scala: Here we need to compute the join of
209+
//
210+
// `A | C` under the constraints `B >: A` and `C <: B`
211+
//
212+
// where `A, B, C` are type parameters.
213+
// Widening `A` to its upper bound would give `Any | C`, i.e. `Any`.
214+
// But widening `C` first would give `A | B` and then `B`.
215+
// So we need to widen `C` first. But how to decide this in general?
216+
// In the algorithm below, we try to widen both sides (once), and then proceed as follows:
217+
//
218+
// 0. If no widening succeeds, proceed with step 3.
219+
// 1. If only one widening succeeds, pick that one.
220+
// 2. If the two widened types are in a subtype relationship, pick the smaller one.
221+
// 3. If exactly one of the two types is a singleton type, pick that one.
222+
// 4. If the widened tp1 is a supertype of tp2, pick widened tp1.
223+
// 5. If the widened tp2 is a supertype of tp1, pick widened tp2.
224+
// 6. Otherwise, pick tp1
225+
//
226+
// At steps 4-6 we lose possible solutions, since we have to make an
227+
// arbitrary choice which side to widen. A better solution would look at
228+
// the constituents of each operand (if the operand is an OrType again) and
229+
// try to widen them selectively in turn. But this might lead to a combinatorial
230+
// explosion of possibilities.
231+
//
232+
// Another approach could be to store information contained in lower bounds
233+
// on both sides. So if `B >: A` we'd also record that `A <: B` and therefore
234+
// widening `A` would yield `B` instead of `Any`, so we'd still be on the right track.
235+
// This looks feasible if lower bounds are type parameters, but tricky if they
236+
// are something else. We'd have to extract the strongest possible
237+
// constraint over all type parameters that is implied by a lower bound.
238+
// This looks related to an algorithmic problem arising in GADT matching.
239+
//
240+
// However, this alone is still not enough. There are other sources of incompleteness,
241+
// for instance arising from mis-aligned refinements.
242+
val tp1w = tp1 match {
243+
case tp1: TypeProxy if !isClassRef(tp1) => tp1.superType.widenExpr
244+
case _ => tp1
245+
}
246+
val tp2w = tp2 match {
247+
case tp2: TypeProxy if !isClassRef(tp2) => tp2.superType.widenExpr
248+
case _ => tp2
249+
}
250+
if ((tp1w ne tp1) || (tp2w ne tp2)) {
251+
val isSingle1 = tp1.isInstanceOf[SingletonType]
252+
val isSingle2 = tp2.isInstanceOf[SingletonType]
253+
return {
254+
if (tp2w eq tp2) orDominator(tp1w | tp2)
255+
else if (tp1w eq tp1) orDominator(tp1 | tp2w)
256+
else if (tp1w frozen_<:< tp2w) orDominator(tp1w | tp2)
257+
else if (tp2w frozen_<:< tp1w) orDominator(tp1 | tp2w)
258+
else if (isSingle1 && !isSingle2) orDominator(tp1w | tp2)
259+
else if (isSingle2 && !isSingle1) orDominator(tp1 | tp2w)
260+
else if (tp1 frozen_<:< tp2w) tp2w
261+
else orDominator(tp1w | tp2)
262+
}
263+
}
264+
265+
// Step 3: Intersect base classes of both sides
266+
val commonBaseClasses = tp.mapReduceOr(_.baseClasses)(intersect)
267+
val doms = dominators(commonBaseClasses, Nil)
268+
def baseTp(cls: ClassSymbol): Type =
269+
tp.baseType(cls).mapReduceOr(identity)(mergeRefinedOrApplied)
270+
doms.map(baseTp).reduceLeft(AndType.apply)
214271
}
215272

216273
tp match {

tests/pos/padTo.scala

+34
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)