package dotty.tools
package dotc
package core
import Types._, Contexts._, Symbols._
import Decorators._
import config.Config
import config.Printers.{constr, typr}
import TypeApplications.EtaExpansion
import collection.mutable
/** Methods for adding constraints and solving them.
*
* What goes into a Constraint as opposed to a ConstrainHandler?
*
* Constraint code is purely functional: Operations get constraints and produce new ones.
* Constraint code does not have access to a type-comparer. Anything regarding lubs and glbs has to be done
* elsewhere.
*
* By comparison: Constraint handlers are parts of type comparers and can use their functionality.
* Constraint handlers update the current constraint as a side effect.
*/
trait ConstraintHandling {
implicit val ctx: Context
protected def isSubType(tp1: Type, tp2: Type): Boolean
protected def isSameType(tp1: Type, tp2: Type): Boolean
val state: TyperState
import state.constraint
private var addConstraintInvocations = 0
/** If the constraint is frozen we cannot add new bounds to the constraint. */
protected var frozenConstraint = false
/** If set, align arguments `S1`, `S2`when taking the glb
* `T1 { X = S1 } & T2 { X = S2 }` of a constraint upper bound for some type parameter.
* Aligning means computing `S1 =:= S2` which may change the current constraint.
* See note in TypeComparer#distributeAnd.
*/
protected var homogenizeArgs = false
/** We are currently comparing type lambdas. Used as a flag for
* optimization: when `false`, no need to do an expensive `pruneLambdaParams`
*/
protected var comparedTypeLambdas: Set[TypeLambda] = Set.empty
private def addOneBound(param: TypeParamRef, bound: Type, isUpper: Boolean): Boolean =
!constraint.contains(param) || {
def occursIn(bound: Type): Boolean = {
val b = bound.dealias
(b eq param) || {
b match {
case b: AndOrType => occursIn(b.tp1) || occursIn(b.tp2)
case b: TypeVar => occursIn(b.origin)
case b: TermRef => occursIn(b.underlying)
case _ => false
}
}
}
if (Config.checkConstraintsSeparated)
assert(!occursIn(bound), s"$param occurs in $bound")
val newBound = narrowedBound(param, bound, isUpper)
val c1 = constraint.updateEntry(param, newBound)
(c1 eq constraint) || {
constraint = c1
val TypeBounds(lo, hi) = constraint.entry(param)
isSubType(lo, hi)
}
}
/** Narrow one of the bounds of type parameter `param`
* If `isUpper` is true, ensure that `param <: `bound`, otherwise ensure
* that `param >: bound`.
*/
def narrowedBound(param: TypeParamRef, bound: Type, isUpper: Boolean)(implicit ctx: Context): TypeBounds = {
val oldBounds @ TypeBounds(lo, hi) = constraint.nonParamBounds(param)
val saved = homogenizeArgs
homogenizeArgs = Config.alignArgsInAnd
try
if (isUpper) oldBounds.derivedTypeBounds(lo, hi & bound)
else oldBounds.derivedTypeBounds(lo | bound, hi)
finally homogenizeArgs = saved
}
protected def addUpperBound(param: TypeParamRef, bound: Type): Boolean = {
def description = i"constraint $param <: $bound to\n$constraint"
if (bound.isRef(defn.NothingClass) && ctx.typerState.isGlobalCommittable) {
def msg = s"!!! instantiated to Nothing: $param, constraint = ${constraint.show}"
if (Config.failOnInstantiationToNothing) assert(false, msg)
else ctx.log(msg)
}
constr.println(i"adding $description in ${ctx.typerState.hashesStr}")
val lower = constraint.lower(param)
val res =
addOneBound(param, bound, isUpper = true) &&
lower.forall(addOneBound(_, bound, isUpper = true))
constr.println(i"added $description = $res in ${ctx.typerState.hashesStr}")
res
}
protected def addLowerBound(param: TypeParamRef, bound: Type): Boolean = {
def description = i"constraint $param >: $bound to\n$constraint"
constr.println(i"adding $description")
val upper = constraint.upper(param)
val res =
addOneBound(param, bound, isUpper = false) &&
upper.forall(addOneBound(_, bound, isUpper = false))
constr.println(i"added $description = $res in ${ctx.typerState.hashesStr}")
res
}
protected def addLess(p1: TypeParamRef, p2: TypeParamRef): Boolean = {
def description = i"ordering $p1 <: $p2 to\n$constraint"
val res =
if (constraint.isLess(p2, p1)) unify(p2, p1)
else {
val down1 = p1 :: constraint.exclusiveLower(p1, p2)
val up2 = p2 :: constraint.exclusiveUpper(p2, p1)
val lo1 = constraint.nonParamBounds(p1).lo
val hi2 = constraint.nonParamBounds(p2).hi
constr.println(i"adding $description down1 = $down1, up2 = $up2 ${ctx.typerState.hashesStr}")
constraint = constraint.addLess(p1, p2)
down1.forall(addOneBound(_, hi2, isUpper = true)) &&
up2.forall(addOneBound(_, lo1, isUpper = false))
}
constr.println(i"added $description = $res ${ctx.typerState.hashesStr}")
res
}
/** Make p2 = p1, transfer all bounds of p2 to p1
* @pre less(p1)(p2)
*/
private def unify(p1: TypeParamRef, p2: TypeParamRef): Boolean = {
constr.println(s"unifying $p1 $p2")
assert(constraint.isLess(p1, p2))
val down = constraint.exclusiveLower(p2, p1)
val up = constraint.exclusiveUpper(p1, p2)
constraint = constraint.unify(p1, p2)
val bounds = constraint.nonParamBounds(p1)
val lo = bounds.lo
val hi = bounds.hi
isSubType(lo, hi) &&
down.forall(addOneBound(_, hi, isUpper = true)) &&
up.forall(addOneBound(_, lo, isUpper = false))
}
protected def isSubType(tp1: Type, tp2: Type, whenFrozen: Boolean): Boolean = {
if (whenFrozen)
isSubTypeWhenFrozen(tp1, tp2)
else
isSubType(tp1, tp2)
}
final def isSubTypeWhenFrozen(tp1: Type, tp2: Type): Boolean = {
val saved = frozenConstraint
frozenConstraint = true
try isSubType(tp1, tp2)
finally frozenConstraint = saved
}
final def isSameTypeWhenFrozen(tp1: Type, tp2: Type): Boolean = {
val saved = frozenConstraint
frozenConstraint = true
try isSameType(tp1, tp2)
finally frozenConstraint = saved
}
/** Test whether the lower bounds of all parameters in this
* constraint are a solution to the constraint.
*/
protected final def isSatisfiable: Boolean =
constraint.forallParams { param =>
val TypeBounds(lo, hi) = constraint.entry(param)
isSubType(lo, hi) || {
ctx.log(i"sub fail $lo <:< $hi")
false
}
}
/** Solve constraint set for given type parameter `param`.
* If `fromBelow` is true the parameter is approximated by its lower bound,
* otherwise it is approximated by its upper bound. However, any occurrences
* of the parameter in a refinement somewhere in the bound are removed. Also
* wildcard types in bounds are approximated by their upper or lower bounds.
* (Such occurrences can arise for F-bounded types).
* The constraint is left unchanged.
* @return the instantiating type
* @pre `param` is in the constraint's domain.
*/
final def approximation(param: TypeParamRef, fromBelow: Boolean): Type = {
val avoidParam = new TypeMap {
override def stopAtStatic = true
def apply(tp: Type) = mapOver {
tp match {
case tp: RefinedType if param occursIn tp.refinedInfo => tp.parent
case tp: WildcardType =>
val bounds = tp.optBounds.orElse(TypeBounds.empty).bounds
// Try to instantiate the wildcard to a type that is known to conform to it.
// This means:
// If fromBelow is true, we minimize the type overall
// Hence, if variance < 0, pick the maximal safe type: bounds.lo
// (i.e. the whole bounds range is over the type)
// if variance > 0, pick the minimal safe type: bounds.hi
// (i.e. the whole bounds range is under the type)
// if variance == 0, pick bounds.lo anyway (this is arbitrary but in line with
// the principle that we pick the smaller type when in doubt).
// If fromBelow is false, we maximize the type overall and reverse the bounds
// if variance != 0. For variance == 0, we still minimize.
// In summary we pick the bound given by this table:
//
// variance | -1 0 1
// ------------------------
// from below | lo lo hi
// from above | hi lo lo
//
if (variance == 0 || fromBelow == (variance < 0)) bounds.lo else bounds.hi
case _ => tp
}
}
}
assert(constraint.contains(param))
val bound = if (fromBelow) constraint.fullLowerBound(param) else constraint.fullUpperBound(param)
val inst = avoidParam(bound)
typr.println(s"approx ${param.show}, from below = $fromBelow, bound = ${bound.show}, inst = ${inst.show}")
inst
}
/** The instance type of `param` in the current constraint (which contains `param`).
* If `fromBelow` is true, the instance type is the lub of the parameter's
* lower bounds; otherwise it is 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.
*/
def instanceType(param: TypeParamRef, fromBelow: Boolean): Type = {
def upperBound = constraint.fullUpperBound(param)
def isSingleton(tp: Type): Boolean = tp match {
case tp: SingletonType => true
case AndType(tp1, tp2) => isSingleton(tp1) | isSingleton(tp2)
case OrType(tp1, tp2) => isSingleton(tp1) & isSingleton(tp2)
case _ => false
}
def isFullyDefined(tp: Type): Boolean = tp match {
case tp: TypeVar => tp.isInstantiated && isFullyDefined(tp.instanceOpt)
case tp: TypeProxy => isFullyDefined(tp.underlying)
case tp: AndOrType => isFullyDefined(tp.tp1) && isFullyDefined(tp.tp2)
case _ => true
}
def isOrType(tp: Type): Boolean = tp.stripTypeVar.dealias match {
case tp: OrType => true
case tp: RefinedOrRecType => isOrType(tp.parent)
case AndType(tp1, tp2) => isOrType(tp1) | isOrType(tp2)
case WildcardType(bounds: TypeBounds) => isOrType(bounds.hi)
case _ => false
}
// First, solve the constraint.
var inst = approximation(param, fromBelow)
// Then, approximate by (1.) - (3.) and simplify as follows.
// 1. If instance is from below and is a singleton type, yet
// upper bound is not a singleton type, widen the instance.
if (fromBelow && isSingleton(inst) && !isSingleton(upperBound))
inst = inst.widen
inst = inst.simplified
// 2. If instance is from below and is a fully-defined union type, yet upper bound
// is not a union type, approximate the union type from above by an intersection
// of all common base types.
if (fromBelow && isOrType(inst) && isFullyDefined(inst) && !isOrType(upperBound))
inst = ctx.harmonizeUnion(inst)
inst
}
/** Constraint `c1` subsumes constraint `c2`, if under `c2` as constraint we have
* for all poly params `p` defined in `c2` as `p >: L2 <: U2`:
*
* c1 defines p with bounds p >: L1 <: U1, and
* L2 <: L1, and
* U1 <: U2
*
* Both `c1` and `c2` are required to derive from constraint `pre`, possibly
* narrowing it with further bounds.
*/
protected final def subsumes(c1: Constraint, c2: Constraint, pre: Constraint): Boolean =
if (c2 eq pre) true
else if (c1 eq pre) false
else {
val saved = constraint
try
c2.forallParams(p =>
c1.contains(p) &&
c2.upper(p).forall(c1.isLess(p, _)) &&
isSubTypeWhenFrozen(c1.nonParamBounds(p), c2.nonParamBounds(p)))
finally constraint = saved
}
/** The current bounds of type parameter `param` */
final def bounds(param: TypeParamRef): TypeBounds = {
val e = constraint.entry(param)
if (e.exists) e.bounds else param.binder.paramInfos(param.paramNum)
}
/** Add type lambda `tl`, possibly with type variables `tvars`, to current constraint
* and propagate all bounds.
* @param tvars See Constraint#add
*/
def addToConstraint(tl: TypeLambda, tvars: List[TypeVar]): Unit =
assert {
checkPropagated(i"initialized $tl") {
constraint = constraint.add(tl, tvars)
tl.paramNames.indices.forall { i =>
val param = TypeParamRef(tl, i)
val bounds = constraint.nonParamBounds(param)
val lower = constraint.lower(param)
val upper = constraint.upper(param)
if (lower.nonEmpty && !bounds.lo.isRef(defn.NothingClass) ||
upper.nonEmpty && !bounds.hi.isRef(defn.AnyClass)) constr.println(i"INIT*** $tl")
lower.forall(addOneBound(_, bounds.hi, isUpper = true)) &&
upper.forall(addOneBound(_, bounds.lo, isUpper = false))
}
}
}
/** Can `param` be constrained with new bounds? */
final def canConstrain(param: TypeParamRef): Boolean =
!frozenConstraint && (constraint contains param)
/** Add constraint `param <: bound` if `fromBelow` is false, `param >: bound` otherwise.
* `bound` is assumed to be in normalized form, as specified in `firstTry` and
* `secondTry` of `TypeComparer`. In particular, it should not be an alias type,
* lazy ref, typevar, wildcard type, error type. In addition, upper bounds may
* not be AndTypes and lower bounds may not be OrTypes. This is assured by the
* way isSubType is organized.
*/
protected def addConstraint(param: TypeParamRef, bound: Type, fromBelow: Boolean): Boolean = {
def description = i"constr $param ${if (fromBelow) ">:" else "<:"} $bound:\n$constraint"
//checkPropagated(s"adding $description")(true) // DEBUG in case following fails
checkPropagated(s"added $description") {
addConstraintInvocations += 1
/** When comparing lambdas we might get constraints such as
* `A <: X0` or `A = List[X0]` where `A` is a constrained parameter
* and `X0` is a lambda parameter. The constraint for `A` is not allowed
* to refer to such a lambda parameter because the lambda parameter is
* not visible where `A` is defined. Consequently, we need to
* approximate the bound so that the lambda parameter does not appear in it.
* If `tp` is an upper bound, we need to approximate with something smaller,
* otherwise something larger.
* Test case in pos/i94-nada.scala. This test crashes with an illegal instance
* error in Test2 when the rest of the SI-2712 fix is applied but `pruneLambdaParams` is
* missing.
*/
def pruneLambdaParams(tp: Type) =
if (comparedTypeLambdas.nonEmpty) {
val approx = new ApproximatingTypeMap {
def apply(t: Type): Type = t match {
case t @ TypeParamRef(tl: TypeLambda, n) if comparedTypeLambdas contains tl =>
val effectiveVariance = if (fromBelow) -variance else variance
val bounds = tl.paramInfos(n)
if (effectiveVariance > 0) bounds.lo
else if (effectiveVariance < 0) bounds.hi
else NoType
case _ =>
mapOver(t)
}
}
approx(tp)
}
else tp
def addParamBound(bound: TypeParamRef) =
if (fromBelow) addLess(bound, param) else addLess(param, bound)
/** Drop all constrained parameters that occur at the toplevel in `bound` and
* handle them by `addLess` calls.
* The preconditions make sure that such parameters occur only
* in one of two ways:
*
* 1.
*
* P <: Ts1 | ... | Tsm (m > 0)
* Tsi = T1 & ... Tn (n >= 0)
* Some of the Ti are constrained parameters
*
* 2.
*
* Ts1 & ... & Tsm <: P (m > 0)
* Tsi = T1 | ... | Tn (n >= 0)
* Some of the Ti are constrained parameters
*
* In each case we cannot leave the parameter in place,
* because that would risk making a parameter later a subtype or supertype
* of a bound where the parameter occurs again at toplevel, which leads to cycles
* in the subtyping test. So we intentionally narrow the constraint by
* recording an isLess relationship instead (even though this is not implied
* by the bound).
*
* Narrowing a constraint is better than widening it, because narrowing leads
* to incompleteness (which we face anyway, see for instance eitherIsSubType)
* but widening leads to unsoundness.
*
* A test case that demonstrates the problem is i864.scala.
* Turn Config.checkConstraintsSeparated on to get an accurate diagnostic
* of the cycle when it is created.
*
* @return The pruned type if all `addLess` calls succeed, `NoType` otherwise.
*/
def prune(bound: Type): Type = bound match {
case bound: AndOrType =>
val p1 = prune(bound.tp1)
val p2 = prune(bound.tp2)
if (p1.exists && p2.exists) bound.derivedAndOrType(p1, p2)
else NoType
case bound: TypeVar if constraint contains bound.origin =>
prune(bound.underlying)
case bound: TypeParamRef =>
constraint.entry(bound) match {
case NoType => pruneLambdaParams(bound)
case _: TypeBounds =>
if (!addParamBound(bound)) NoType
else if (fromBelow) defn.NothingType
else defn.AnyType
case inst =>
prune(inst)
}
case _ =>
pruneLambdaParams(bound)
}
try bound match {
case bound: TypeParamRef if constraint contains bound =>
addParamBound(bound)
case _ =>
val pbound = prune(bound)
pbound.exists && (
if (fromBelow) addLowerBound(param, pbound) else addUpperBound(param, pbound))
}
finally addConstraintInvocations -= 1
}
}
/** Instantiate `param` to `tp` if the constraint stays satisfiable */
protected def tryInstantiate(param: TypeParamRef, tp: Type): Boolean = {
val saved = constraint
constraint =
if (addConstraint(param, tp, fromBelow = true) &&
addConstraint(param, tp, fromBelow = false)) constraint.replace(param, tp)
else saved
constraint ne saved
}
/** Check that constraint is fully propagated. See comment in Config.checkConstraintsPropagated */
def checkPropagated(msg: => String)(result: Boolean): Boolean = {
if (Config.checkConstraintsPropagated && result && addConstraintInvocations == 0) {
val saved = frozenConstraint
frozenConstraint = true
for (p <- constraint.domainParams) {
def check(cond: => Boolean, q: TypeParamRef, ordering: String, explanation: String): Unit =
assert(cond, i"propagation failure for $p $ordering $q: $explanation\n$msg")
for (u <- constraint.upper(p))
check(bounds(p).hi <:< bounds(u).hi, u, "<:", "upper bound not propagated")
for (l <- constraint.lower(p)) {
check(bounds(l).lo <:< bounds(p).hi, l, ">:", "lower bound not propagated")
check(constraint.isLess(l, p), l, ">:", "reverse ordering (<:) missing")
}
}
frozenConstraint = saved
}
result
}
}