Skip to content

Commit 78939ca

Browse files
committed
Document capture conversion in TypeComparer and Typer
Test case pos/capture.scala contains an explanation why a different scheme we tried does not work.
1 parent abb8146 commit 78939ca

File tree

6 files changed

+131
-18
lines changed

6 files changed

+131
-18
lines changed

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

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,14 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] {
130130
}
131131
}
132132

133+
/** The current approximation state. See `ApproxState`. */
133134
private[this] var approx: ApproxState = FreshApprox
134135
protected def approxState: ApproxState = approx
135136

137+
/** The original left-hand type of the comparison. Gets reset
138+
* everytime we compare components of the previous pair of types.
139+
* This type is used for capture conversion in `isSubArgs`.
140+
*/
136141
private [this] var leftRoot: Type = null
137142

138143
protected def isSubType(tp1: Type, tp2: Type, a: ApproxState): Boolean = {
@@ -155,6 +160,16 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] {
155160

156161
def isSubType(tp1: Type, tp2: Type)(implicit nc: AbsentContext): Boolean = isSubType(tp1, tp2, FreshApprox)
157162

163+
/** The inner loop of the isSubType comparison.
164+
* Recursive calls from recur should go to recur directly if the two types
165+
* compared in the callee are essentially the same as the types compared in the
166+
* caller. "The same" means: represent essentially the same sets of values.
167+
* `recur` should not be used to compare components of types. In this case
168+
* one should use `isSubType(_, _)`.
169+
* `recur` should also not be used to compare approximated versions of the original
170+
* types (as when we go from an abstract type to one of its bounds). In that case
171+
* one should use `isSubType(_, _, a)` where `a` defines the kind of approximation
172+
*/
158173
protected def recur(tp1: Type, tp2: Type): Boolean = trace(s"isSubType ${traceInfo(tp1, tp2)} $approx", subtyping) {
159174

160175
def monitoredIsSubType = {
@@ -1017,15 +1032,48 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] {
10171032
*/
10181033
def isSubArgs(args1: List[Type], args2: List[Type], tp1: Type, tparams2: List[ParamInfo]): Boolean = {
10191034

1035+
/** The bounds of parameter `tparam`, where all references to type paramneters
1036+
* are replaced by corresponding arguments (or their approximations in the case of
1037+
* wildcard arguments).
1038+
*/
10201039
def paramBounds(tparam: Symbol): TypeBounds =
10211040
tparam.info.substApprox(tparams2.asInstanceOf[List[Symbol]], args2).bounds
10221041

1023-
def recur(args1: List[Type], args2: List[Type], tparams2: List[ParamInfo]): Boolean =
1042+
def recurArgs(args1: List[Type], args2: List[Type], tparams2: List[ParamInfo]): Boolean =
10241043
if (args1.isEmpty) args2.isEmpty
10251044
else args2.nonEmpty && {
10261045
val tparam = tparams2.head
10271046
val v = tparam.paramVariance
10281047

1048+
/** Try a capture conversion:
1049+
* If the original left-hand type `leftRoot` is a path `p.type`,
1050+
* and the current widened left type is an application with wildcard arguments
1051+
* such as `C[_]`, where `X` is `C`'s type parameter corresponding to the `_` argument,
1052+
* compare with `C[p.X]` instead. Otherwise return `false`.
1053+
* Also do a capture conversion in either of the following cases:
1054+
*
1055+
* - If we are after typer. We generally relax soundness requirements then.
1056+
* We need the relaxed condition to correctly compute overriding relationships.
1057+
* Missing this case led to AbstractMethod errors in the bootstrap.
1058+
*
1059+
* - If we are in mode TypevarsMissContext, which means we test implicits
1060+
* for eligibility. In this case, we can be more permissive, since it's
1061+
* just a pre-check. This relaxation is needed since the full
1062+
* implicit typing might perform an adaptation that skolemizes the
1063+
* type of a synthesized tree before comparing it with an expected type.
1064+
* But no such adaptation is applied for implicit eligibility
1065+
* testing, so we have to compensate.
1066+
*/
1067+
def compareCaptured(arg1: TypeBounds, arg2: Type) = tparam match {
1068+
case tparam: Symbol
1069+
if leftRoot.isStable || ctx.isAfterTyper || ctx.mode.is(Mode.TypevarsMissContext) =>
1070+
val captured = TypeRef(leftRoot, tparam)
1071+
assert(captured.exists, i"$leftRoot has no member $tparam in isSubArgs($args1, $args2, $tp1, $tparams2)")
1072+
isSubArg(captured, arg2)
1073+
case _ =>
1074+
false
1075+
}
1076+
10291077
def isSubArg(arg1: Type, arg2: Type): Boolean = arg2 match {
10301078
case arg2: TypeBounds =>
10311079
val arg1norm = arg1 match {
@@ -1040,13 +1088,7 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] {
10401088
case _ =>
10411089
arg1 match {
10421090
case arg1: TypeBounds =>
1043-
tparam match {
1044-
case tparam: Symbol if leftRoot.isStable || ctx.isAfterTyper =>
1045-
val captured = TypeRef(leftRoot, tparam)
1046-
isSubArg(captured, arg2)
1047-
case _ =>
1048-
false
1049-
}
1091+
compareCaptured(arg1, arg2)
10501092
case _ =>
10511093
(v > 0 || isSubType(arg2, arg1)) &&
10521094
(v < 0 || isSubType(arg1, arg2))
@@ -1061,9 +1103,9 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] {
10611103
val adapted2 = arg2.adaptHkVariances(tparam.paramInfo)
10621104
adapted2.ne(arg2) && isSubArg(arg1, adapted2)
10631105
}
1064-
} && recur(args1.tail, args2.tail, tparams2.tail)
1106+
} && recurArgs(args1.tail, args2.tail, tparams2.tail)
10651107

1066-
recur(args1, args2, tparams2)
1108+
recurArgs(args1, args2, tparams2)
10671109
}
10681110

10691111
/** Test whether `tp1` has a base type of the form `B[T1, ..., Tn]` where
@@ -1826,8 +1868,6 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] {
18261868

18271869
object TypeComparer {
18281870

1829-
val oldScheme = true
1830-
18311871
/** Class for unification variables used in `natValue`. */
18321872
private class AnyConstantType extends UncachedGroundType with ValueType {
18331873
var tpe: Type = NoType
@@ -1841,6 +1881,12 @@ object TypeComparer {
18411881
private val LoApprox = 1
18421882
private val HiApprox = 2
18431883

1884+
/** The approximation state indicates how the pair of types currently compared
1885+
* relates to the types compared originally.
1886+
* - `NoApprox`: They are still the same types
1887+
* - `LoApprox`: The left type is approximated (i.e widened)"
1888+
* - `HiApprox`: The right type is approximated (i.e narrowed)"
1889+
*/
18441890
class ApproxState(private val bits: Int) extends AnyVal {
18451891
override def toString: String = {
18461892
val lo = if ((bits & LoApprox) != 0) "LoApprox" else ""
@@ -1854,6 +1900,11 @@ object TypeComparer {
18541900
}
18551901

18561902
val NoApprox: ApproxState = new ApproxState(0)
1903+
1904+
/** A special approximation state to indicate that this is the first time we
1905+
* compare (approximations of) this pair of types. It's converted to `NoApprox`
1906+
* in `isSubType`, but also leads to `leftRoot` being set there.
1907+
*/
18571908
val FreshApprox: ApproxState = new ApproxState(4)
18581909

18591910
/** Show trace of comparison operations when performing `op` as result string */

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,8 +1443,8 @@ object Types {
14431443
final def substSym(from: List[Symbol], to: List[Symbol])(implicit ctx: Context): Type =
14441444
ctx.substSym(this, from, to, null)
14451445

1446-
/** Substitute all occurrences of symbols in `from` by corresponding types in a`to`.
1447-
* Unlike `subst`, the `to` types here can be type bounds. A TypeBounds target
1446+
/** Substitute all occurrences of symbols in `from` by corresponding types in `to`.
1447+
* Unlike for `subst`, the `to` types can be type bounds. A TypeBounds target
14481448
* will be replaced by range that gets absorbed in an approximating type map.
14491449
*/
14501450
final def substApprox(from: List[Symbol], to: List[Type])(implicit ctx: Context): Type =

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2699,8 +2699,10 @@ class Typer extends Namer
26992699
}
27002700

27012701
/** Replace every top-level occurrence of a wildcard type argument by
2702-
* a skolem type
2703-
*/
2702+
* a fresh skolem type. The skolem types are of the form $i.CAP, where
2703+
* $i is a skolem of type `scala.internal.TypeBox`, and `CAP` is its
2704+
* type member.
2705+
*/
27042706
def captureWildcards(tp: Type)(implicit ctx: Context): Type = tp match {
27052707
case tp: AndOrType => tp.derivedAndOrType(captureWildcards(tp.tp1), captureWildcards(tp.tp2))
27062708
case tp: RefinedType => tp.derivedRefinedType(captureWildcards(tp.parent), tp.refinedName, tp.refinedInfo)
@@ -2733,11 +2735,13 @@ class Typer extends Namer
27332735
def adaptToSubType(wtp: Type): Tree = {
27342736
// try converting a constant to the target type
27352737
val folded = ConstFold(tree, pt)
2736-
if (folded ne tree) return adaptConstant(folded, folded.tpe.asInstanceOf[ConstantType])
2738+
if (folded ne tree)
2739+
return adaptConstant(folded, folded.tpe.asInstanceOf[ConstantType])
27372740

27382741
// Try to capture wildcards in type
27392742
val captured = captureWildcards(wtp)
2740-
if (captured `ne` wtp) return readapt(tree.cast(captured))
2743+
if (captured `ne` wtp)
2744+
return readapt(tree.cast(captured))
27412745

27422746
// drop type if prototype is Unit
27432747
if (pt isRef defn.UnitClass) {

tests/neg/capture1.scala

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Test shows that Java soundness hole does not apply in Dotty
2+
import collection.mutable
3+
object Test extends App {
4+
5+
val l: mutable.Seq[String] = mutable.ArrayBuffer()
6+
7+
def (xs: List[T]) emap[T, U] (f: T => U): List[U] = xs.map(f)
8+
9+
def (xs: List[T]) ereduce[T] (f: (T, T) => T): T = xs.reduceLeft(f)
10+
11+
def (xs: mutable.Seq[T]) append[T] (ys: mutable.Seq[T]): mutable.Seq[T] = xs ++ ys
12+
13+
List(l, mutable.ArrayBuffer(1))
14+
.emap(list => list)
15+
.ereduce((xs, ys) => xs `append` ys) // error
16+
17+
}

tests/neg/i4376.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
object App {
2+
type Id[A] >: A <: A
3+
4+
val a: Array[_ >: Id[_ <: Int]] =
5+
(Array.ofDim[String](1) : Array[_ >: Id[Nothing]]) // error
6+
}

tests/pos/capture.scala

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// A condensation of shonan-hmm/Lifters.scala that shows why the approach
2+
// of just skolemizing a tree as a hole to prepare it for capture conversion
3+
// does not work.
4+
// I tried two different stratgegies to adapt a tree whose widened type has wildcard
5+
// arguments. Say the tree is `t` and the widened type is `C[_]`.
6+
//
7+
// skolemization-as-a-whole:
8+
//
9+
// Convert `t` to `t.cast[$i]` where `$i` is a skolem of type C[_]
10+
// This then relies on capture conversion for singleton types to do the rest.
11+
//
12+
// skolemization-of-each-param:
13+
//
14+
// Convert `t` to `t.cast[C[$j.CAP]]` where `$j` is a skolem of type `TypeBox[Nothing, Any`]
15+
// (or more generally, `TypeBox[L, U]`) wgere `L` and `U` are the bounds of the wildcard).
16+
//
17+
// skolemization-of-ewach-param is more robust since it is stable under widening.
18+
class Test {
19+
20+
abstract class Liftable[T]
21+
22+
implicit def ClassIsLiftable[T]: Liftable[Class[T]] = new Liftable[Class[T]] {}
23+
24+
class Expr[+T]
25+
26+
implicit class LiftExprOps[T](val x: T) {
27+
def toExpr(implicit ev: Liftable[T]): Expr[T] = ???
28+
}
29+
30+
def runtimeClass: Class[_] = ???
31+
32+
runtimeClass.toExpr(ClassIsLiftable) // OK for skolemization-as-a-whole and skolemization-of-each-param
33+
34+
runtimeClass.toExpr // only works with skolemization-of-each-param
35+
}

0 commit comments

Comments
 (0)