Skip to content

Commit 6ce1ac7

Browse files
committed
Fix #2998: Constrain type from above before interpolating variables
When interpolating type variables we should use all available information at this point, including the expected result type. One situation where this makes a difference is if the expected type is a singleton type, because type variables are instantiated to singleton types only if their upper bounds are singleton types. Also fixes #2997.
1 parent 7e402a7 commit 6ce1ac7

File tree

5 files changed

+121
-69
lines changed

5 files changed

+121
-69
lines changed

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

Lines changed: 81 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -222,72 +222,6 @@ object Inferencing {
222222
case _ => NoType
223223
}
224224

225-
/** Interpolate those undetermined type variables in the widened type of this tree
226-
* which are introduced by type application contained in the tree.
227-
* If such a variable appears covariantly in type `tp` or does not appear at all,
228-
* approximate it by its lower bound. Otherwise, if it appears contravariantly
229-
* in type `tp` approximate it by its upper bound.
230-
* @param ownedBy if it is different from NoSymbol, all type variables owned by
231-
* `ownedBy` qualify, independent of position.
232-
* Without that second condition, it can be that certain variables escape
233-
* interpolation, for instance when their tree was eta-lifted, so
234-
* the typechecked tree is no longer the tree in which the variable
235-
* was declared. A concrete example of this phenomenon can be
236-
* observed when compiling core.TypeOps#asSeenFrom.
237-
*/
238-
def interpolateUndetVars(tree: Tree, ownedBy: Symbol)(implicit ctx: Context): Unit = {
239-
val constraint = ctx.typerState.constraint
240-
val qualifies = (tvar: TypeVar) =>
241-
(tree contains tvar.bindingTree) || ownedBy.exists && tvar.owner == ownedBy
242-
def interpolate() = Stats.track("interpolateUndetVars") {
243-
val tp = tree.tpe.widen
244-
constr.println(s"interpolate undet vars in ${tp.show}, pos = ${tree.pos}, mode = ${ctx.mode}, undets = ${constraint.uninstVars map (tvar => s"${tvar.show}@${tvar.bindingTree.pos}")}")
245-
constr.println(s"qualifying undet vars: ${constraint.uninstVars filter qualifies map (tvar => s"$tvar / ${tvar.show}")}, constraint: ${constraint.show}")
246-
247-
val vs = variances(tp, qualifies)
248-
val hasUnreportedErrors = ctx.typerState.reporter match {
249-
case r: StoreReporter if r.hasErrors => true
250-
case _ => false
251-
}
252-
// Avoid interpolating variables if typerstate has unreported errors.
253-
// Reason: The errors might reflect unsatisfiable constraints. In that
254-
// case interpolating without taking account the constraints risks producing
255-
// nonsensical types that then in turn produce incomprehensible errors.
256-
// An example is in neg/i1240.scala. Without the condition in the next code line
257-
// we get for
258-
//
259-
// val y: List[List[String]] = List(List(1))
260-
//
261-
// i1430.scala:5: error: type mismatch:
262-
// found : Int(1)
263-
// required: Nothing
264-
// val y: List[List[String]] = List(List(1))
265-
// ^
266-
// With the condition, we get the much more sensical:
267-
//
268-
// i1430.scala:5: error: type mismatch:
269-
// found : Int(1)
270-
// required: String
271-
// val y: List[List[String]] = List(List(1))
272-
if (!hasUnreportedErrors)
273-
vs foreachBinding { (tvar, v) =>
274-
if (v != 0 && ctx.typerState.constraint.contains(tvar)) {
275-
// previous interpolations could have already instantiated `tvar`
276-
// through unification, that's why we have to check again whether `tvar`
277-
// is contained in the current constraint.
278-
typr.println(s"interpolate ${if (v == 1) "co" else "contra"}variant ${tvar.show} in ${tp.show}")
279-
tvar.instantiate(fromBelow = v == 1)
280-
}
281-
}
282-
for (tvar <- constraint.uninstVars)
283-
if (!(vs contains tvar) && qualifies(tvar)) {
284-
typr.println(s"instantiating non-occurring ${tvar.show} in ${tp.show} / $tp")
285-
tvar.instantiate(fromBelow = true)
286-
}
287-
}
288-
if (constraint.uninstVars exists qualifies) interpolate()
289-
}
290-
291225
/** Instantiate undetermined type variables to that type `tp` is
292226
* maximized and return None. If this is not possible, because a non-variant
293227
* typevar is not uniquely determined, return that typevar in a Some.
@@ -375,6 +309,87 @@ object Inferencing {
375309
}
376310
}
377311

312+
trait Inferencing { this: Typer =>
313+
import Inferencing._
314+
import tpd._
315+
316+
/** Interpolate those undetermined type variables in the widened type of this tree
317+
* which are introduced by type application contained in the tree.
318+
* If such a variable appears covariantly in type `tp` or does not appear at all,
319+
* approximate it by its lower bound. Otherwise, if it appears contravariantly
320+
* in type `tp` approximate it by its upper bound.
321+
* @param ownedBy if it is different from NoSymbol, all type variables owned by
322+
* `ownedBy` qualify, independent of position.
323+
* Without that second condition, it can be that certain variables escape
324+
* interpolation, for instance when their tree was eta-lifted, so
325+
* the typechecked tree is no longer the tree in which the variable
326+
* was declared. A concrete example of this phenomenon can be
327+
* observed when compiling core.TypeOps#asSeenFrom.
328+
*/
329+
def interpolateUndetVars(tree: Tree, ownedBy: Symbol, pt: Type)(implicit ctx: Context): Unit = {
330+
val constraint = ctx.typerState.constraint
331+
val qualifies = (tvar: TypeVar) =>
332+
(tree contains tvar.bindingTree) || ownedBy.exists && tvar.owner == ownedBy
333+
def interpolate() = Stats.track("interpolateUndetVars") {
334+
val tp = tree.tpe.widen
335+
constr.println(s"interpolate undet vars in ${tp.show}, pos = ${tree.pos}, mode = ${ctx.mode}, undets = ${constraint.uninstVars map (tvar => s"${tvar.show}@${tvar.bindingTree.pos}")}")
336+
constr.println(s"qualifying undet vars: ${constraint.uninstVars filter qualifies map (tvar => s"$tvar / ${tvar.show}")}, constraint: ${constraint.show}")
337+
338+
val vs = variances(tp, qualifies)
339+
val hasUnreportedErrors = ctx.typerState.reporter match {
340+
case r: StoreReporter if r.hasErrors => true
341+
case _ => false
342+
}
343+
344+
var isConstrained = false
345+
def ensureConstrained() =
346+
if (!isConstrained) {
347+
isConstrained = true
348+
constrainResult(tree.tpe, pt)
349+
}
350+
351+
// Avoid interpolating variables if typerstate has unreported errors.
352+
// Reason: The errors might reflect unsatisfiable constraints. In that
353+
// case interpolating without taking account the constraints risks producing
354+
// nonsensical types that then in turn produce incomprehensible errors.
355+
// An example is in neg/i1240.scala. Without the condition in the next code line
356+
// we get for
357+
//
358+
// val y: List[List[String]] = List(List(1))
359+
//
360+
// i1430.scala:5: error: type mismatch:
361+
// found : Int(1)
362+
// required: Nothing
363+
// val y: List[List[String]] = List(List(1))
364+
// ^
365+
// With the condition, we get the much more sensical:
366+
//
367+
// i1430.scala:5: error: type mismatch:
368+
// found : Int(1)
369+
// required: String
370+
// val y: List[List[String]] = List(List(1))
371+
if (!hasUnreportedErrors)
372+
vs foreachBinding { (tvar, v) =>
373+
if (v != 0 && ctx.typerState.constraint.contains(tvar)) {
374+
// previous interpolations could have already instantiated `tvar`
375+
// through unification, that's why we have to check again whether `tvar`
376+
// is contained in the current constraint.
377+
typr.println(s"interpolate ${if (v == 1) "co" else "contra"}variant ${tvar.show} in ${tp.show}")
378+
ensureConstrained()
379+
tvar.instantiate(fromBelow = v == 1)
380+
}
381+
}
382+
for (tvar <- constraint.uninstVars)
383+
if (!(vs contains tvar) && qualifies(tvar)) {
384+
typr.println(s"instantiating non-occurring ${tvar.show} in ${tp.show} / $tp")
385+
ensureConstrained()
386+
tvar.instantiate(fromBelow = true)
387+
}
388+
}
389+
if (constraint.uninstVars exists qualifies) interpolate()
390+
}
391+
}
392+
378393
/** An enumeration controlling the degree of forcing in "is-dully-defined" checks. */
379394
@sharable object ForceDegree {
380395
class Value(val appliesTo: TypeVar => Boolean, val minimizeAll: Boolean)

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,14 @@ object Typer {
6666
private val InsertedApply = new Property.Key[Unit]
6767
}
6868

69-
class Typer extends Namer with TypeAssigner with Applications with Implicits with Dynamic with Checking with Docstrings {
69+
class Typer extends Namer
70+
with TypeAssigner
71+
with Applications
72+
with Implicits
73+
with Inferencing
74+
with Dynamic
75+
with Checking
76+
with Docstrings {
7077

7178
import Typer._
7279
import tpd.{cpy => _, _}
@@ -1929,8 +1936,8 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit
19291936
/*>|>*/ trace(i"adapting $tree of type ${tree.tpe} to $pt", typr, show = true) /*<|<*/ {
19301937
if (!tree.denot.isOverloaded) {
19311938
// for overloaded trees: resolve overloading before simplifying
1932-
if (tree.isDef) interpolateUndetVars(tree, tree.symbol)
1933-
else if (!tree.tpe.widen.isInstanceOf[LambdaType]) interpolateUndetVars(tree, NoSymbol)
1939+
if (tree.isDef) interpolateUndetVars(tree, tree.symbol, pt)
1940+
else if (!tree.tpe.widen.isInstanceOf[LambdaType]) interpolateUndetVars(tree, NoSymbol, pt)
19341941
tree.overwriteType(tree.tpe.simplified)
19351942
}
19361943
adaptInterpolated(tree, pt)

tests/neg/i2997.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
case class Foo[T <: Int with Singleton](t : T)
2+
3+
object Test {
4+
val one = 1
5+
final val final_one = 1
6+
val a : 1 = Foo(1).t
7+
val b : Int = Foo(one).t // error: does not conform to upper bound Int & Singleton
8+
val c : 1 = Foo(final_one).t
9+
}

tests/pos/i2997.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
case class Foo[T <: Int with Singleton](t : T)
2+
3+
object Test {
4+
val one = 1
5+
final val final_one = 1
6+
val a : 1 = Foo(1).t
7+
val c : 1 = Foo(final_one).t
8+
}

tests/pos/i2998.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class Foo[T] {
2+
def zero: T = ???
3+
def one(): T = ???
4+
val two: T = ???
5+
}
6+
7+
object Test {
8+
def foo[T](x: T): Foo[T] = new Foo
9+
10+
val b: 1 = foo(1).one() // OK
11+
val a: 1 = foo(1).zero // Fails: Found: Int, required: Int(1)
12+
val c: 1 = foo(1).two // Fails: Found: Int, required: Int(1)
13+
}

0 commit comments

Comments
 (0)