Skip to content

True union types #1550

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

Merged
merged 15 commits into from
Oct 11, 2016
9 changes: 0 additions & 9 deletions src/dotty/language.scala

This file was deleted.

8 changes: 6 additions & 2 deletions src/dotty/tools/dotc/config/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,12 @@ object Config {
*/
final val checkLambdaVariance = false

/** Check that certain types cannot be created in erasedTypes phases */
final val checkUnerased = true
/** Check that certain types cannot be created in erasedTypes phases.
* Note: Turning this option on will get some false negatives, since it is
* possible that And/Or types are still created during erasure as the result
* of some operation on an existing type.
*/
final val checkUnerased = false

/** In `derivedSelect`, rewrite
*
Expand Down
15 changes: 12 additions & 3 deletions src/dotty/tools/dotc/core/ConstraintHandling.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ trait ConstraintHandling {
/** If the constraint is frozen we cannot add new bounds to the constraint. */
protected var frozenConstraint = false

protected var alwaysFluid = false

/** Perform `op` in a mode where all attempts to set `frozen` to true are ignored */
def fluidly[T](op: => T): T = {
val saved = alwaysFluid
alwaysFluid = true
try op finally alwaysFluid = saved
}

/** We are currently comparing lambdas. Used as a flag for
* optimization: when `false`, no need to do an expensive `pruneLambdaParams`
*/
Expand Down Expand Up @@ -126,14 +135,14 @@ trait ConstraintHandling {

final def isSubTypeWhenFrozen(tp1: Type, tp2: Type): Boolean = {
val saved = frozenConstraint
frozenConstraint = true
frozenConstraint = !alwaysFluid
try isSubType(tp1, tp2)
finally frozenConstraint = saved
}

final def isSameTypeWhenFrozen(tp1: Type, tp2: Type): Boolean = {
val saved = frozenConstraint
frozenConstraint = true
frozenConstraint = !alwaysFluid
try isSameType(tp1, tp2)
finally frozenConstraint = saved
}
Expand Down Expand Up @@ -219,7 +228,7 @@ trait ConstraintHandling {
// 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 = inst.approximateUnion
inst = ctx.harmonizeUnion(inst)

// 3. If instance is from below, and upper bound has open named parameters
// make sure the instance has all named parameters of the bound.
Expand Down
4 changes: 1 addition & 3 deletions src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -430,10 +430,8 @@ class Definitions {
def Product_productArity(implicit ctx: Context) = Product_productArityR.symbol
lazy val Product_productPrefixR = ProductClass.requiredMethodRef(nme.productPrefix)
def Product_productPrefix(implicit ctx: Context) = Product_productPrefixR.symbol
lazy val LanguageModuleRef = ctx.requiredModule("dotty.language")
lazy val LanguageModuleRef = ctx.requiredModule("scala.language")
def LanguageModuleClass(implicit ctx: Context) = LanguageModuleRef.symbol.moduleClass.asClass
lazy val Scala2LanguageModuleRef = ctx.requiredModule("scala.language")
def Scala2LanguageModuleClass(implicit ctx: Context) = Scala2LanguageModuleRef.symbol.moduleClass.asClass
lazy val NonLocalReturnControlType: TypeRef = ctx.requiredClassRef("scala.runtime.NonLocalReturnControl")

lazy val ClassTagType = ctx.requiredClassRef("scala.reflect.ClassTag")
Expand Down
3 changes: 2 additions & 1 deletion src/dotty/tools/dotc/core/Denotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,8 @@ object Denotations {
(!sym2.isAsConcrete(sym1) ||
precedes(sym1.owner, sym2.owner) ||
accessBoundary(sym2).isProperlyContainedIn(accessBoundary(sym1)) ||
sym1.is(Method) && !sym2.is(Method))
sym1.is(Method) && !sym2.is(Method)) ||
sym1.info.isErroneous

/** Sym preference provided types also override */
def prefer(sym1: Symbol, sym2: Symbol, info1: Type, info2: Type) =
Expand Down
5 changes: 5 additions & 0 deletions src/dotty/tools/dotc/core/OrderingConstraint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,11 @@ class OrderingConstraint(private val boundsMap: ParamBounds,
case _ if e1 contains e2 => e2
case _ => mergeError
}
case tv1: TypeVar =>
e2 match {
case tv2: TypeVar if tv1.instanceOpt eq tv2.instanceOpt => e1
case _ => mergeError
}
case _ if e1 eq e2 => e1
case _ => mergeError
}
Expand Down
1 change: 0 additions & 1 deletion src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,6 @@ object StdNames {
val isEmpty: N = "isEmpty"
val isInstanceOf_ : N = "isInstanceOf"
val java: N = "java"
val keepUnions: N = "keepUnions"
val key: N = "key"
val lang: N = "lang"
val length: N = "length"
Expand Down
21 changes: 17 additions & 4 deletions src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import StdNames.{nme, tpnme}
import collection.mutable
import util.{Stats, DotClass, SimpleMap}
import config.Config
import config.Printers.{typr, constr, subtyping}
import config.Printers.{typr, constr, subtyping, noPrinter}
import TypeErasure.{erasedLub, erasedGlb}
import TypeApplications._
import scala.util.control.NonFatal
Expand Down Expand Up @@ -324,8 +324,18 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling {
case AndType(tp11, tp12) =>
if (tp11.stripTypeVar eq tp12.stripTypeVar) isSubType(tp11, tp2)
else thirdTry(tp1, tp2)
case OrType(tp11, tp12) =>
isSubType(tp11, tp2) && isSubType(tp12, tp2)
case tp1 @ OrType(tp11, tp12) =>
def joinOK = tp2.dealias match {
case tp12: HKApply =>
// If we apply the default algorithm for `A[X] | B[Y] <: C[Z]` where `C` is a
// type parameter, we will instantiate `C` to `A` and then fail when comparing
// with `B[Y]`. To do the right thing, we need to instantiate `C` to the
// common superclass of `A` and `B`.
isSubType(tp1.join, tp2)
case _ =>
false
}
joinOK || isSubType(tp11, tp2) && isSubType(tp12, tp2)
case ErrorType =>
true
case _ =>
Expand Down Expand Up @@ -827,8 +837,11 @@ class TypeComparer(initctx: Context) extends DotClass with ConstraintHandling {
op1 && {
val leftConstraint = constraint
constraint = preConstraint
if (!(op2 && subsumes(leftConstraint, constraint, preConstraint)))
if (!(op2 && subsumes(leftConstraint, constraint, preConstraint))) {
if (constr != noPrinter && !subsumes(constraint, leftConstraint, preConstraint))
constr.println(i"CUT - prefer $leftConstraint over $constraint")
constraint = leftConstraint
}
true
} || op2
}
Expand Down
134 changes: 79 additions & 55 deletions src/dotty/tools/dotc/core/TypeOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -173,100 +173,124 @@ trait TypeOps { this: Context => // TODO: Make standalone object.
}

/** Approximate union type by intersection of its dominators.
* See Type#approximateUnion for an explanation.
* That is, replace a union type Tn | ... | Tn
* by the smallest intersection type of base-class instances of T1,...,Tn.
* Example: Given
*
* trait C[+T]
* trait D
* class A extends C[A] with D
* class B extends C[B] with D with E
*
* we approximate `A | B` by `C[A | B] with D`
*/
def approximateUnion(tp: Type): Type = {
def orDominator(tp: Type): Type = {

/** a faster version of cs1 intersect cs2 */
def intersect(cs1: List[ClassSymbol], cs2: List[ClassSymbol]): List[ClassSymbol] = {
val cs2AsSet = new util.HashSet[ClassSymbol](100)
cs2.foreach(cs2AsSet.addEntry)
cs1.filter(cs2AsSet.contains)
}

/** The minimal set of classes in `cs` which derive all other classes in `cs` */
def dominators(cs: List[ClassSymbol], accu: List[ClassSymbol]): List[ClassSymbol] = (cs: @unchecked) match {
case c :: rest =>
val accu1 = if (accu exists (_ derivesFrom c)) accu else c :: accu
if (cs == c.baseClasses) accu1 else dominators(rest, accu1)
}

def mergeRefined(tp1: Type, tp2: Type): Type = {
def fail = throw new AssertionError(i"Failure to join alternatives $tp1 and $tp2")
tp1 match {
case tp1 @ RefinedType(parent1, name1, rinfo1) =>
tp2 match {
case RefinedType(parent2, `name1`, rinfo2) =>
tp1.derivedRefinedType(
mergeRefined(parent1, parent2), name1, rinfo1 | rinfo2)
case _ => fail
}
case tp1 @ TypeRef(pre1, name1) =>
tp2 match {
case tp2 @ TypeRef(pre2, `name1`) =>
tp1.derivedSelect(pre1 | pre2)
case _ => fail
}
case _ => fail
}
}

def approximateOr(tp1: Type, tp2: Type): Type = {
def isClassRef(tp: Type): Boolean = tp match {
case tp: TypeRef => tp.symbol.isClass
case tp: RefinedType => isClassRef(tp.parent)
case _ => false
}

/** If `tp1` and `tp2` are typebounds, try to make one fit into the other
* or to make them equal, by instantiating uninstantiated type variables.
*/
def homogenizedUnion(tp1: Type, tp2: Type): Type = {
tp1 match {
case tp1: TypeBounds =>
tp2 match {
case tp2: TypeBounds =>
def fitInto(tp1: TypeBounds, tp2: TypeBounds): Unit = {
val nestedCtx = ctx.fresh.setNewTyperState
if (tp2.boundsInterval.contains(tp1.boundsInterval)(nestedCtx))
nestedCtx.typerState.commit()
}
fitInto(tp1, tp2)
fitInto(tp2, tp1)
case _ =>
}
case _ =>
}
tp1 | tp2
}

tp1 match {
case tp1: RefinedType =>
tp2 match {
case tp2: RefinedType if tp1.refinedName == tp2.refinedName =>
return tp1.derivedRefinedType(
approximateUnion(OrType(tp1.parent, tp2.parent)),
tp1.refinedName,
homogenizedUnion(tp1.refinedInfo, tp2.refinedInfo))
//.ensuring { x => println(i"approx or $tp1 | $tp2 = $x\n constr = ${ctx.typerState.constraint}"); true } // DEBUG
case _ =>
}
case _ =>
}

tp1 match {
case tp1: RecType =>
tp1.rebind(approximateOr(tp1.parent, tp2))
case tp1: TypeProxy if !isClassRef(tp1) =>
approximateUnion(tp1.superType | tp2)
orDominator(tp1.superType | tp2)
case _ =>
tp2 match {
case tp2: RecType =>
tp2.rebind(approximateOr(tp1, tp2.parent))
case tp2: TypeProxy if !isClassRef(tp2) =>
approximateUnion(tp1 | tp2.superType)
orDominator(tp1 | tp2.superType)
case _ =>
val commonBaseClasses = tp.mapReduceOr(_.baseClasses)(intersect)
val doms = dominators(commonBaseClasses, Nil)
def baseTp(cls: ClassSymbol): Type =
if (tp1.typeParams.nonEmpty) tp.baseTypeRef(cls)
else tp.baseTypeWithArgs(cls)
def baseTp(cls: ClassSymbol): Type = {
val base =
if (tp1.typeParams.nonEmpty) tp.baseTypeRef(cls)
else tp.baseTypeWithArgs(cls)
base.mapReduceOr(identity)(mergeRefined)
}
doms.map(baseTp).reduceLeft(AndType.apply)
}
}
}
if (ctx.featureEnabled(defn.LanguageModuleClass, nme.keepUnions)) tp
else tp match {

tp match {
case tp: OrType =>
approximateOr(tp.tp1, tp.tp2) // Maybe refactor using liftToRec?
case tp @ AndType(tp1, tp2) =>
tp derived_& (approximateUnion(tp1), approximateUnion(tp2))
case tp: RefinedType =>
tp.derivedRefinedType(approximateUnion(tp.parent), tp.refinedName, tp.refinedInfo)
case tp: RecType =>
tp.rebind(approximateUnion(tp.parent))
approximateOr(tp.tp1, tp.tp2)
case _ =>
tp
}
}

/** Given a disjunction T1 | ... | Tn of types with potentially embedded
* type variables, constrain type variables further if this eliminates
* some of the branches of the disjunction. Do this also for disjunctions
* embedded in intersections, as parents in refinements, and in recursive types.
*
* For instance, if `A` is an unconstrained type variable, then
*
* ArrayBuffer[Int] | ArrayBuffer[A]
*
* is approximated by constraining `A` to be =:= to `Int` and returning `ArrayBuffer[Int]`
* instead of `ArrayBuffer[_ >: Int | A <: Int & A]`
*/
def harmonizeUnion(tp: Type): Type = tp match {
case tp: OrType =>
joinIfScala2(typeComparer.fluidly(tp.tp1 | tp.tp2))
case tp @ AndType(tp1, tp2) =>
tp derived_& (harmonizeUnion(tp1), harmonizeUnion(tp2))
case tp: RefinedType =>
tp.derivedRefinedType(harmonizeUnion(tp.parent), tp.refinedName, tp.refinedInfo)
case tp: RecType =>
tp.rebind(harmonizeUnion(tp.parent))
case _ =>
tp
}

/** Under -language:Scala2: Replace or-types with their joins */
private def joinIfScala2(tp: Type) = tp match {
case tp: OrType if scala2Mode => tp.join
case _ => tp
}

/** Not currently needed:
*
def liftToRec(f: (Type, Type) => Type)(tp1: Type, tp2: Type)(implicit ctx: Context) = {
Expand Down Expand Up @@ -493,7 +517,7 @@ trait TypeOps { this: Context => // TODO: Make standalone object.
*/
def featureEnabled(owner: ClassSymbol, feature: TermName): Boolean = {
def toPrefix(sym: Symbol): String =
if (!sym.exists || (sym eq defn.LanguageModuleClass) || (sym eq defn.Scala2LanguageModuleClass)) ""
if (!sym.exists || (sym eq defn.LanguageModuleClass)) ""
else toPrefix(sym.owner) + sym.name + "."
def featureName = toPrefix(owner) + feature
def hasImport(implicit ctx: Context): Boolean = {
Expand All @@ -512,13 +536,13 @@ trait TypeOps { this: Context => // TODO: Make standalone object.

/** Is auto-tupling enabled? */
def canAutoTuple =
!featureEnabled(defn.Scala2LanguageModuleClass, nme.noAutoTupling)
!featureEnabled(defn.LanguageModuleClass, nme.noAutoTupling)

def scala2Mode =
featureEnabled(defn.LanguageModuleClass, nme.Scala2)

def dynamicsEnabled =
featureEnabled(defn.Scala2LanguageModuleClass, nme.dynamics)
featureEnabled(defn.LanguageModuleClass, nme.dynamics)

def testScala2Mode(msg: String, pos: Position) = {
if (scala2Mode) migrationWarning(msg, pos)
Expand Down
Loading