Skip to content

Fix #8976: Do not instantiate type variables outside bounds #9010

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions compiler/src/dotty/tools/dotc/core/TypeErrors.scala
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,11 @@ class MergeError(val sym1: Symbol, val sym2: Symbol, val tp1: Type, val tp2: Typ
"""
}
}

class NoInstance(param: TypeParamRef, val inst: Type, bounds: TypeBounds) extends TypeError:
override def produceMessage(using Context): Message =
i"""Type variable $param cannot be instantiated.
|Its attempted instantiation type $inst
|does not conform to its bounds $bounds}"""


53 changes: 42 additions & 11 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4029,6 +4029,13 @@ object Types {
case bound: OrType => occursIn(bound.tp1, fromBelow) || occursIn(bound.tp2, fromBelow)
case _ => false
}

/** Is `inst` a legal instance type for this type parameter?
* TODO: Check whether we can always check both bounds even if
* one check would be redundant. But maybe that's cheap enough.
*/
def canInstantiateWith(inst: Type, fromBelow: Boolean)(using Context): Boolean =
if fromBelow then inst frozen_<:< this else this frozen_<:< inst
}

private final class TypeParamRefImpl(binder: TypeLambda, paramNum: Int) extends TypeParamRef(binder, paramNum)
Expand Down Expand Up @@ -4176,7 +4183,7 @@ object Types {
def msg = i"Inaccessible variables captured in instantation of type variable $this.\n$tp was fixed to $atp"
typr.println(msg)
val bound = ctx.typeComparer.fullUpperBound(origin)
if !(atp <:< bound) then
if !(atp <:< bound) then // TODO: compare with param instead
throw new TypeError(s"$msg,\nbut the latter type does not conform to the upper bound $bound")
atp
// AVOIDANCE TODO: This really works well only if variables are instantiated from below
Expand All @@ -4187,24 +4194,48 @@ object Types {
// To do this, we need first test cases for that situation.

/** Instantiate variable with given type */
def instantiateWith(tp: Type)(implicit ctx: Context): Type = {
def instantiateWith(tp: Type)(implicit ctx: Context): Unit =
assert(tp ne this, s"self instantiation of ${tp.show}, constraint = ${ctx.typerState.constraint.show}")
typr.println(s"instantiating ${this.show} with ${tp.show}")
if ((ctx.typerState eq owningState.get) && !ctx.typeComparer.subtypeCheckInProgress)
if (ctx.typerState eq owningState.get) && !ctx.typeComparer.subtypeCheckInProgress then
inst = tp
ctx.typerState.constraint = ctx.typerState.constraint.replace(origin, tp)
tp
}

/** The type this type variable should be instantiated with.
* This is the type-comparer's instance type with the added tweak that
* captured for more deeply nested symbols are avoided.
*/
def instanceType(fromBelow: Boolean)(using Context): Type =
avoidCaptures(ctx.typeComparer.instanceType(origin, fromBelow))

/** Instantiate variable from the constraints over its `origin`.
* If `fromBelow` is true, the variable is instantiated to the lub
* of its lower bounds in the current constraint; otherwise it is
* instantiated to the glb of its upper bounds. However, a lower bound
* instantiation can be a singleton type only if the upper bound
* is also a singleton type.
* If `fromBelow` is true, the variable's instance type is computed from
* the lub of its lower bounds in the current constraint; otherwise it is
* computed from the glb of its upper bounds.
* After that, there are some further transformations and checks:
*
* - Singletons and union types are sometimes widened according to
* `ConstraintHandling.widenInferred`.
* - References to more neeply nested symbols are avoided
* according to `avoidCaptures`
* - It is checked that the resulting type still fits within
* bounds. If not, a `NoInstance` exception is thrown,
* but only after instantiating the variable anyway because
* this avoids follow-on errors.
Comment on lines +4221 to +4224
Copy link
Member

@smarter smarter May 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced this is needed:

  • avoidCaptures already checks that the approximation is within bound and throws an exception if it isn't
  • widenInferred will not do any widening if that would be out of bounds

The only problematic piece is approximation in ConstraintHandling which will replace recursive occurences of the type variable being instantiated by TypeBounds.empty, which can indeed be unsound. Here's an alternative PR which tries to fix that specific problem: #9012

*/
def instantiate(fromBelow: Boolean)(implicit ctx: Context): Type =
instantiateWith(avoidCaptures(ctx.typeComparer.instanceType(origin, fromBelow)))
val inst = instanceType(fromBelow)
val instOK = origin.canInstantiateWith(inst, fromBelow)
if instOK then
instantiateWith(inst)
else
typr.println(i"failure to instantiate $this with $inst")
val bounds = ctx.typeComparer.fullBounds(origin)
instantiateWith(inst) // instantiate anyway to prevent subsequent instantiation attempts and errors
val noInstance = NoInstance(origin, inst, bounds)
if ctx.settings.Ydebug.value then noInstance.printStackTrace()
throw noInstance
inst

/** For uninstantiated type variables: Is the lower bound different from Nothing? */
def hasLowerBound(implicit ctx: Context): Boolean =
Expand Down
29 changes: 20 additions & 9 deletions compiler/src/dotty/tools/dotc/typer/Inferencing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,17 @@ object Inferencing {
private class IsFullyDefinedAccumulator(force: ForceDegree.Value, minimizeSelected: Boolean = false)
(using Context) extends TypeAccumulator[Boolean] {

private def instantiate(tvar: TypeVar, fromBelow: Boolean): Type = {
val inst = tvar.instantiate(fromBelow)
typr.println(i"forced instantiation of ${tvar.origin} = $inst")
private var instancesOK = true

private def instantiate(tvar: TypeVar, fromBelow: Boolean): Type =
val inst = tvar.instanceType(fromBelow)
if tvar.origin.canInstantiateWith(inst, fromBelow) then
tvar.instantiateWith(inst)
typr.println(i"forced instantiation of ${tvar.origin} = $inst")
else
typr.println(i"failed forced instantiation of ${tvar.origin} = $inst")
instancesOK = false
inst
}

private var toMaximize: List[TypeVar] = Nil

Expand Down Expand Up @@ -147,6 +153,7 @@ object Inferencing {
}

def process(tp: Type): Boolean =
instancesOK = true
// Maximize type vars in the order they were visited before */
def maximize(tvars: List[TypeVar]): Unit = tvars match
case tvar :: tvars1 =>
Expand All @@ -158,10 +165,11 @@ object Inferencing {
&& (
toMaximize.isEmpty
|| { maximize(toMaximize)
toMaximize = Nil // Do another round since the maximixing instances
process(tp) // might have type uninstantiated variables themselves.
}
toMaximize = Nil // Do another round since the maximixing instances
process(tp) // might have type uninstantiated variables themselves.
}
)
&& instancesOK
}

def approximateGADT(tp: Type)(implicit ctx: Context): Type = {
Expand Down Expand Up @@ -233,8 +241,11 @@ object Inferencing {
case TypeBounds(lo, hi)
if (hi frozen_<:< lo) =>
val inst = accCtx.typeComparer.approximation(param, fromBelow = true)
typr.println(i"replace singleton $param := $inst")
accCtx.typerState.constraint = constraint.replace(param, inst)
if param.canInstantiateWith(inst, fromBelow = true) then
typr.println(i"replace singleton $param := $inst")
accCtx.typerState.constraint = constraint.replace(param, inst)
else
typr.println(i"failed to replace singleton $param $inst")
case _ =>
}
case _ =>
Expand Down
5 changes: 5 additions & 0 deletions tests/neg/i8976.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
trait Cons[X, Y]

def solve[X, Y](using Cons[X, Y] =:= Cons[1, Cons[2, Y]]) = ()

@main def main = solve // error: Type variable Y cannot be instantiated.