From bff6b093d28bfc6918fa86d640353ba60b1a24e4 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 14 Mar 2014 18:41:55 +0100 Subject: [PATCH 1/7] Add language feature mechanism Add a method "featureEnabled" that checks whether a feature is enabled. Features can be enabled by imports or by command-line options. The Scala 2.10 way of enabling features by implicits got dropped, because the use of the feature mechanism is now different. Previously, features imposed restrictions on what used to work. So it was important to offer way to avoid the restrictions it that was as smooth as possible, and implicits fit the bill. Furthermore, features did not change the way things were compiled, so it was OK to test them only once all types were compiled. Now, features are essentially switches that change compile time behavior. keepUnions and noAutoTupling, if on, will modify the way type inference works. So we need to interprete a switch on the spot, and doing an implicit search to determine a switch value is too dangerous in what concerns causing cyclic references. At the same time, because we are dealing with new functionality, there is less of a concern for being able to set or reset features for large pieces of code with some implicit. You could argue that's not even desirable, and that an explicit import or command line option is preferable. Conflicts: src/dotty/tools/dotc/core/SymDenotations.scala --- src/dotty/language.scala | 16 +++++++++ src/dotty/tools/dotc/core/Definitions.scala | 1 + src/dotty/tools/dotc/core/StdNames.scala | 3 ++ .../tools/dotc/core/SymDenotations.scala | 14 +++++++- src/dotty/tools/dotc/core/TypeOps.scala | 34 +++++++++++++++++++ 5 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 src/dotty/language.scala diff --git a/src/dotty/language.scala b/src/dotty/language.scala new file mode 100644 index 000000000000..169b2604c97e --- /dev/null +++ b/src/dotty/language.scala @@ -0,0 +1,16 @@ +package dotty + +object language { + + class Feature + + /** Allow higher-kinded type syntax (not yet checked) */ + val higherKinds = new Feature + + /** Keep union types */ + val keepUnions = new Feature + + /** No auto tupling */ + val noAutoTupling = new Feature + +} \ No newline at end of file diff --git a/src/dotty/tools/dotc/core/Definitions.scala b/src/dotty/tools/dotc/core/Definitions.scala index 5f6698b33aaa..5c2890f29a47 100644 --- a/src/dotty/tools/dotc/core/Definitions.scala +++ b/src/dotty/tools/dotc/core/Definitions.scala @@ -215,6 +215,7 @@ class Definitions { lazy val JavaSerializableClass = ctx.requiredClass("java.lang.Serializable") lazy val ComparableClass = ctx.requiredClass("java.lang.Comparable") lazy val ProductClass = ctx.requiredClass("scala.Product") + lazy val LanguageModuleClass = ctx.requiredModule("dotty.language").moduleClass // Annotation base classes lazy val AnnotationClass = ctx.requiredClass("scala.annotation.Annotation") diff --git a/src/dotty/tools/dotc/core/StdNames.scala b/src/dotty/tools/dotc/core/StdNames.scala index 0cbcfa5a74b9..6cd9da4b5874 100644 --- a/src/dotty/tools/dotc/core/StdNames.scala +++ b/src/dotty/tools/dotc/core/StdNames.scala @@ -375,6 +375,7 @@ object StdNames { val hashCode_ : N = "hashCode" val hash_ : N = "hash" val head: N = "head" + val higherKinds: N = "higherKinds" val identity: N = "identity" val implicitly: N = "implicitly" val in: N = "in" @@ -387,6 +388,7 @@ object StdNames { val isInstanceOf_ : N = "isInstanceOf" val isInstanceOf_Ob : N = "$isInstanceOf" val java: N = "java" + val keepUnions: N = "keepUnions" val key: N = "key" val lang: N = "lang" val length: N = "length" @@ -415,6 +417,7 @@ object StdNames { val next: N = "next" val nmeNewTermName: N = "newTermName" val nmeNewTypeName: N = "newTypeName" + val noAutoTupling: N = "noAutoTupling" val normalize: N = "normalize" val notifyAll_ : N = "notifyAll" val notify_ : N = "notify" diff --git a/src/dotty/tools/dotc/core/SymDenotations.scala b/src/dotty/tools/dotc/core/SymDenotations.scala index 08566e3db72d..362738caf3e8 100644 --- a/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/src/dotty/tools/dotc/core/SymDenotations.scala @@ -9,7 +9,7 @@ import collection.mutable import collection.immutable.BitSet import scala.reflect.io.AbstractFile import Decorators.SymbolIteratorDecorator -import ast.tpd +import ast._ import annotation.tailrec import util.SimpleMap import util.Stats @@ -195,6 +195,18 @@ object SymDenotations { final def hasAnnotation(cls: Symbol)(implicit ctx: Context) = dropOtherAnnotations(annotations, cls).nonEmpty + /** Optionally, the arguments of the first annotation matching the given class symbol */ + final def getAnnotationArgs(cls: Symbol)(implicit ctx: Context): Option[List[tpd.Tree]] = + dropOtherAnnotations(annotations, cls) match { + case annot :: _ => + Some( + annot.tree match { + case Trees.Apply(_, args) => args + case _ => Nil + }) + case nil => None + } + /** Add given annotation to the annotations of this denotation */ final def addAnnotation(annot: Annotation): Unit = annotations = annot :: myAnnotations diff --git a/src/dotty/tools/dotc/core/TypeOps.scala b/src/dotty/tools/dotc/core/TypeOps.scala index 152f8d1b7a12..4ace0bebec26 100644 --- a/src/dotty/tools/dotc/core/TypeOps.scala +++ b/src/dotty/tools/dotc/core/TypeOps.scala @@ -266,6 +266,40 @@ trait TypeOps { this: Context => } parentRefs } + + /** Is `feature` enabled in class `owner`? + * This is the case if one of the following two alternatives holds: + * + * 1. The feature is imported by a named import + * + * import owner.feature + * + * (the feature may be bunched with others, or renamed, but wildcard imports + * don't count). + * + * 2. The feature is enabled by a compiler option + * + * - language:feature + * + * where is the full name of the owner followed by a "." minus + * the prefix "dotty.language.". + */ + def featureEnabled(owner: ClassSymbol, feature: TermName): Boolean = { + def toPrefix(sym: Symbol): String = + if (sym eq defn.LanguageModuleClass) "" else toPrefix(sym.owner) + sym.name + "." + def featureName = toPrefix(owner) + feature + def hasImport(implicit ctx: Context): Boolean = ( + ctx.importInfo != null + && ( (ctx.importInfo.site.widen.typeSymbol eq owner) + && ctx.importInfo.originals.contains(feature) + || + { var c = ctx.outer + while (c.importInfo eq ctx.importInfo) c = c.outer + hasImport(c) + })) + def hasOption = ctx.base.settings.language.value exists (s => s == featureName || s == "_") + hasImport || hasOption + } } object TypeOps { From 574a148fd561a793ee522c2be18ee02214236d80 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 16 Mar 2014 12:39:56 +0100 Subject: [PATCH 2/7] Added auto-tupling. Auto-tupling should satisfy the following spec. 1. An application `f(args)` where `f` is a non-overloaded method which has a single, non-repeated parameter as its first parameter list and where args consists of two or more arguments is expanded to `f((args))`. 2. A constructor pattern `C(args)` where `C.unapply` is a non-overloaded method which has a single, non-repeated parameter as its first parameter list and where args consists of two or more arguments is expanded to `C((args))`. Auto-tupling can be disabled by language feature "noAutoTupling". Conflicts: test/dotc/tests.scala --- src/dotty/tools/dotc/core/Definitions.scala | 2 +- src/dotty/tools/dotc/core/TypeOps.scala | 5 +++++ src/dotty/tools/dotc/core/Types.scala | 7 +++++++ src/dotty/tools/dotc/typer/Applications.scala | 16 ++++++++++++++-- src/dotty/tools/dotc/typer/ProtoTypes.scala | 14 ++++++++++++++ src/dotty/tools/dotc/typer/Typer.scala | 12 ++++++++++-- test/dotc/tests.scala | 2 ++ tests/neg/autoTuplingTest.scala | 11 +++++++++++ tests/pos/autoTuplingTest.scala | 9 +++++++++ 9 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 tests/neg/autoTuplingTest.scala create mode 100644 tests/pos/autoTuplingTest.scala diff --git a/src/dotty/tools/dotc/core/Definitions.scala b/src/dotty/tools/dotc/core/Definitions.scala index 5c2890f29a47..bcf2bb74c97c 100644 --- a/src/dotty/tools/dotc/core/Definitions.scala +++ b/src/dotty/tools/dotc/core/Definitions.scala @@ -215,7 +215,7 @@ class Definitions { lazy val JavaSerializableClass = ctx.requiredClass("java.lang.Serializable") lazy val ComparableClass = ctx.requiredClass("java.lang.Comparable") lazy val ProductClass = ctx.requiredClass("scala.Product") - lazy val LanguageModuleClass = ctx.requiredModule("dotty.language").moduleClass + lazy val LanguageModuleClass = ctx.requiredModule("dotty.language").moduleClass.asClass // Annotation base classes lazy val AnnotationClass = ctx.requiredClass("scala.annotation.Annotation") diff --git a/src/dotty/tools/dotc/core/TypeOps.scala b/src/dotty/tools/dotc/core/TypeOps.scala index 4ace0bebec26..22a86766bb2b 100644 --- a/src/dotty/tools/dotc/core/TypeOps.scala +++ b/src/dotty/tools/dotc/core/TypeOps.scala @@ -5,6 +5,7 @@ import Contexts._, Types._, Symbols._, Names._, Flags._, Scopes._ import SymDenotations._ import config.Printers._ import Decorators._ +import StdNames._ import util.SimpleMap trait TypeOps { this: Context => @@ -300,6 +301,10 @@ trait TypeOps { this: Context => def hasOption = ctx.base.settings.language.value exists (s => s == featureName || s == "_") hasImport || hasOption } + + /** Is auto-tupling enabled? */ + def canAutoTuple = + !featureEnabled(defn.LanguageModuleClass, nme.noAutoTupling) } object TypeOps { diff --git a/src/dotty/tools/dotc/core/Types.scala b/src/dotty/tools/dotc/core/Types.scala index 6dda937d2c0b..a0293d50a3e8 100644 --- a/src/dotty/tools/dotc/core/Types.scala +++ b/src/dotty/tools/dotc/core/Types.scala @@ -685,6 +685,13 @@ object Types { case _ => Nil } + /** The parameter types in the first parameter section of a PolyType or MethodType, Empty list for others */ + final def firstParamTypes: List[Type] = this match { + case mt: MethodType => mt.paramTypes + case pt: PolyType => pt.resultType.firstParamTypes + case _ => Nil + } + /** Is this either not a method at all, or a parameterless method? */ final def isParameterless: Boolean = this match { case mt: MethodType => false diff --git a/src/dotty/tools/dotc/typer/Applications.scala b/src/dotty/tools/dotc/typer/Applications.scala index feaf678a2210..4d26532d656e 100644 --- a/src/dotty/tools/dotc/typer/Applications.scala +++ b/src/dotty/tools/dotc/typer/Applications.scala @@ -434,8 +434,17 @@ trait Applications extends Compatibility { self: Typer => def typedApply(tree: untpd.Apply, pt: Type)(implicit ctx: Context): Tree = { def realApply(implicit ctx: Context): Tree = track("realApply") { - val proto = new FunProto(tree.args, pt, this) + var proto = new FunProto(tree.args, pt, this) val fun1 = typedExpr(tree.fun, proto) + + // Warning: The following line is dirty and fragile. We record that auto-tupling was demanded as + // a side effect in adapt. If it was, we assume the tupled proto-type in the rest of the application. + // This crucially relies on he fact that `proto` is used only in a single call of `adapt`, + // otherwise we would get possible cross-talk between different `adapt` calls using the same + // prototype. A cleaner alternative would be to return a modified prototype from `adapt` together with + // a modified tree but this would be more convoluted and less efficient. + if (proto.isTupled) proto = proto.tupled + methPart(fun1).tpe match { case funRef: TermRef => tryEither { implicit ctx => @@ -676,7 +685,10 @@ trait Applications extends Compatibility { self: Typer => var argTypes = unapplyArgs(unapplyApp.tpe) for (argType <- argTypes) assert(!argType.isInstanceOf[TypeBounds], unapplyApp.tpe.show) val bunchedArgs = argTypes match { - case argType :: Nil if argType.isRepeatedParam => untpd.SeqLiteral(args) :: Nil + case argType :: Nil => + if (argType.isRepeatedParam) untpd.SeqLiteral(args) :: Nil + else if (args.lengthCompare(1) > 0 && ctx.canAutoTuple) untpd.Tuple(args) :: Nil + else args case _ => args } if (argTypes.length != bunchedArgs.length) { diff --git a/src/dotty/tools/dotc/typer/ProtoTypes.scala b/src/dotty/tools/dotc/typer/ProtoTypes.scala index b4068408b4ab..74be3f4cd1c6 100644 --- a/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -187,6 +187,20 @@ object ProtoTypes { typer.adapt(targ, formal) } + private var myTupled: Type = NoType + + /** The same proto-type but with all arguments combined in a single tuple */ + def tupled: FunProto = myTupled match { + case pt: FunProto => + pt + case _ => + myTupled = new FunProto(untpd.Tuple(args) :: Nil, resultType, typer) + tupled + } + + /** Somebody called the `tupled` method of this prototype */ + def isTupled: Boolean = myTupled.isInstanceOf[FunProto] + override def toString = s"FunProto(${args mkString ","} => $resultType)" def map(tm: TypeMap)(implicit ctx: Context): FunProto = diff --git a/src/dotty/tools/dotc/typer/Typer.scala b/src/dotty/tools/dotc/typer/Typer.scala index 17c77be8994a..f961a4544dd9 100644 --- a/src/dotty/tools/dotc/typer/Typer.scala +++ b/src/dotty/tools/dotc/typer/Typer.scala @@ -1072,8 +1072,16 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit } } - def adaptToArgs(wtp: Type, pt: FunProto) = wtp match { - case _: MethodType | _: PolyType => tree + def adaptToArgs(wtp: Type, pt: FunProto): Tree = wtp match { + case _: MethodType | _: PolyType => + def isUnary = wtp.firstParamTypes match { + case ptype :: Nil => !ptype.isRepeatedParam + case _ => false + } + if (pt.args.lengthCompare(1) > 0 && isUnary && ctx.canAutoTuple) + adaptToArgs(wtp, pt.tupled) + else + tree case _ => tryInsertApply(tree, pt) { val more = tree match { case Apply(_, _) => " more" diff --git a/test/dotc/tests.scala b/test/dotc/tests.scala index 54d9cfca65b1..21db07bd3fb9 100644 --- a/test/dotc/tests.scala +++ b/test/dotc/tests.scala @@ -57,6 +57,8 @@ class tests extends CompilerTest { @Test def neg_i39 = compileFile(negDir, "i39", xerrors = 1) @Test def neg_i50_volatile = compileFile(negDir, "i50-volatile", xerrors = 4) @Test def neg_companions = compileFile(negDir, "companions", xerrors = 1) + @Test def neg_autoTupling = compileFile(posDir, "autoTuplingTest", "-language:noAutoTupling" :: Nil, xerrors = 3) + @Test def neg_autoTupling2 = compileFile(negDir, "autoTuplingTest", xerrors = 3) @Test def dotc = compileDir(dotcDir + "tools/dotc", twice) @Test def dotc_ast = compileDir(dotcDir + "tools/dotc/ast", twice) diff --git a/tests/neg/autoTuplingTest.scala b/tests/neg/autoTuplingTest.scala new file mode 100644 index 000000000000..d4a698271391 --- /dev/null +++ b/tests/neg/autoTuplingTest.scala @@ -0,0 +1,11 @@ +import dotty.language.noAutoTupling + +object autoTuplingNeg { + + val x = Some(1, 2) + + x match { + case Some(a, b) => a + b + case None => + } +} diff --git a/tests/pos/autoTuplingTest.scala b/tests/pos/autoTuplingTest.scala new file mode 100644 index 000000000000..523411a1a410 --- /dev/null +++ b/tests/pos/autoTuplingTest.scala @@ -0,0 +1,9 @@ +object autoTupling { + + val x = Some(1, 2) + + x match { + case Some(a, b) => a + b + case None => + } +} From 7a04b119c9744262bd46c1795b811f56df9516a6 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 7 Mar 2014 11:05:02 +0100 Subject: [PATCH 3/7] Removed explicit tuplings from dotc codebase. Eliminated all "Dotty deviations" which were due to lack of auto-tupling. --- src/dotty/tools/dotc/ast/Desugar.scala | 2 +- src/dotty/tools/dotc/config/Settings.scala | 2 +- src/dotty/tools/dotc/core/Contexts.scala | 2 +- src/dotty/tools/dotc/core/Definitions.scala | 2 +- src/dotty/tools/dotc/core/pickling/PickleBuffer.scala | 10 +++++----- .../tools/dotc/reporting/UniqueMessagePositions.scala | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/dotty/tools/dotc/ast/Desugar.scala b/src/dotty/tools/dotc/ast/Desugar.scala index 027c3238d91c..01aaf0b5ffe7 100644 --- a/src/dotty/tools/dotc/ast/Desugar.scala +++ b/src/dotty/tools/dotc/ast/Desugar.scala @@ -728,7 +728,7 @@ object desugar { */ private object VarPattern { def unapply(tree: Tree)(implicit ctx: Context): Option[VarInfo] = tree match { - case id: Ident => Some((id, TypeTree())) // Dotty deviation: No auto-tupling + case id: Ident => Some(id, TypeTree()) case Typed(id: Ident, tpt) => Some((id, tpt)) case _ => None } diff --git a/src/dotty/tools/dotc/config/Settings.scala b/src/dotty/tools/dotc/config/Settings.scala index 7f2ad2a4fbd7..b3bf2f1348be 100644 --- a/src/dotty/tools/dotc/config/Settings.scala +++ b/src/dotty/tools/dotc/config/Settings.scala @@ -60,7 +60,7 @@ object Settings { copy(aliases = aliases :+ abbrv)(idx) def dependsOn[U](setting: Setting[U], value: U): Setting[T] = - copy(depends = depends :+ ((setting, value)))(idx) // Dotty deviation: no auto-tupling + copy(depends = depends :+ (setting, value))(idx) def valueIn(state: SettingsState): T = state.value(idx).asInstanceOf[T] diff --git a/src/dotty/tools/dotc/core/Contexts.scala b/src/dotty/tools/dotc/core/Contexts.scala index 68daea44059f..8780b0bf6268 100644 --- a/src/dotty/tools/dotc/core/Contexts.scala +++ b/src/dotty/tools/dotc/core/Contexts.scala @@ -273,7 +273,7 @@ object Contexts { newctx.implicitsCache = null newctx.setCreationTrace() // Dotty deviation: Scala2x allows access to private members implicitCache and setCreationTrace - // even from a subclass prefix. Dotty (and Java) do not. I think that's a bug in Scala2x. + // even from a subclass prefix. Dotty (and Java) do not. It's confirmed as a bug in Scala2x. newctx.asInstanceOf[FreshContext] } diff --git a/src/dotty/tools/dotc/core/Definitions.scala b/src/dotty/tools/dotc/core/Definitions.scala index bcf2bb74c97c..22e308062989 100644 --- a/src/dotty/tools/dotc/core/Definitions.scala +++ b/src/dotty/tools/dotc/core/Definitions.scala @@ -292,7 +292,7 @@ class Definitions { lazy val targs = ft.argInfos if ((FunctionClasses contains tsym) && (targs.length - 1 <= MaxFunctionArity) && - (FunctionClass(targs.length - 1) == tsym)) Some((targs.init, targs.last)) // Dotty deviation: no auto-tupling + (FunctionClass(targs.length - 1) == tsym)) Some(targs.init, targs.last) else None } } diff --git a/src/dotty/tools/dotc/core/pickling/PickleBuffer.scala b/src/dotty/tools/dotc/core/pickling/PickleBuffer.scala index 2bedcab92cfb..ef2b4acb2700 100644 --- a/src/dotty/tools/dotc/core/pickling/PickleBuffer.scala +++ b/src/dotty/tools/dotc/core/pickling/PickleBuffer.scala @@ -229,8 +229,8 @@ object PickleBuffer { PARAM -> Param, PACKAGE -> Package, MACRO -> Macro, - BYNAMEPARAM -> ((Method, Covariant)), // Dotty deviation: no auto-tupling - LABEL -> ((Label, Contravariant)), // Dotty deviation: no auto-tupling + BYNAMEPARAM -> (Method, Covariant), + LABEL -> (Label, Contravariant), ABSOVERRIDE -> AbsOverride, LOCAL -> Local, JAVA -> JavaDefined, @@ -238,16 +238,16 @@ object PickleBuffer { STABLE -> Stable, STATIC -> Static, CASEACCESSOR -> CaseAccessor, - DEFAULTPARAM -> ((DefaultParameterized, Trait)), // Dotty deviation: no auto-tupling + DEFAULTPARAM -> (DefaultParameterized, Trait), BRIDGE -> Bridge, ACCESSOR -> Accessor, SUPERACCESSOR -> SuperAccessor, PARAMACCESSOR -> ParamAccessor, MODULEVAR -> Scala2ModuleVar, LAZY -> Lazy, - MIXEDIN -> ((MixedIn, Scala2Existential)), // Dotty deviation: no auto-tupling + MIXEDIN -> (MixedIn, Scala2Existential), EXPANDEDNAME -> ExpandedName, - IMPLCLASS -> ((Scala2PreSuper, ImplClass)), // Dotty deviation: no auto-tupling + IMPLCLASS -> (Scala2PreSuper, ImplClass), SPECIALIZED -> Specialized, DEFAULTINIT -> DefaultInit, VBRIDGE -> VBridge, diff --git a/src/dotty/tools/dotc/reporting/UniqueMessagePositions.scala b/src/dotty/tools/dotc/reporting/UniqueMessagePositions.scala index 483228693d5b..93e24b27f04f 100644 --- a/src/dotty/tools/dotc/reporting/UniqueMessagePositions.scala +++ b/src/dotty/tools/dotc/reporting/UniqueMessagePositions.scala @@ -21,7 +21,7 @@ trait UniqueMessagePositions extends Reporter { override def isHidden(d: Diagnostic)(implicit ctx: Context): Boolean = super.isHidden(d) || { d.pos.exists && { - positions get ((ctx.source, d.pos.point)) match { // Dotty deviation: no autotupling + positions get (ctx.source, d.pos.point) match { case Some(s) if s.level >= d.severity.level => true case _ => positions((ctx.source, d.pos.point)) = d.severity; false } From 88c4a6cddefb6bf3e7d1ac3c61358cc06abd8bd4 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 8 Mar 2014 12:24:34 +0100 Subject: [PATCH 4/7] Allow And/OrTypes in baseType operations Introduce new cases for AndTypes and OrTypes in methods `derivesFrom`, `baseClasses`, and `baseTypeWithArgs`. These cases are ultimately needed so that `baseTypeWithArgs` makes sense for union and intersection types. Also, fixed embarrassing typo in method `TypeBounds#|`. --- .../tools/dotc/core/TypeApplications.scala | 18 ++++++++++----- src/dotty/tools/dotc/core/Types.scala | 22 ++++++++++++++++--- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/dotty/tools/dotc/core/TypeApplications.scala b/src/dotty/tools/dotc/core/TypeApplications.scala index b4c30d902976..0abd28a716ab 100644 --- a/src/dotty/tools/dotc/core/TypeApplications.scala +++ b/src/dotty/tools/dotc/core/TypeApplications.scala @@ -170,17 +170,20 @@ class TypeApplications(val self: Type) extends AnyVal { /** The type arguments of this type's base type instance wrt.`base`. * Existential types in arguments are disallowed. */ - final def baseArgTypes(base: Symbol)(implicit ctx: Context): List[Type] = baseArgInfos(base) mapConserve noBounds + final def baseArgTypes(base: Symbol)(implicit ctx: Context): List[Type] = + baseArgInfos(base) mapConserve noBounds /** The type arguments of this type's base type instance wrt.`base`. * Existential types in arguments are approximanted by their lower bound. */ - final def baseArgTypesLo(base: Symbol)(implicit ctx: Context): List[Type] = baseArgInfos(base) mapConserve boundsToLo + final def baseArgTypesLo(base: Symbol)(implicit ctx: Context): List[Type] = + baseArgInfos(base) mapConserve boundsToLo /** The type arguments of this type's base type instance wrt.`base`. * Existential types in arguments are approximanted by their upper bound. */ - final def baseArgTypesHi(base: Symbol)(implicit ctx: Context): List[Type] = baseArgInfos(base) mapConserve boundsToHi + final def baseArgTypesHi(base: Symbol)(implicit ctx: Context): List[Type] = + baseArgInfos(base) mapConserve boundsToHi /** The first type argument of the base type instance wrt `base` of this type */ final def firstBaseArgInfo(base: Symbol)(implicit ctx: Context): Type = base.typeParams match { @@ -193,8 +196,11 @@ class TypeApplications(val self: Type) extends AnyVal { /** The base type including all type arguments of this type. * Existential types in arguments are returned as TypeBounds instances. */ - final def baseTypeWithArgs(base: Symbol)(implicit ctx: Context): Type = - self.baseTypeRef(base).appliedTo(baseArgInfos(base)) + final def baseTypeWithArgs(base: Symbol)(implicit ctx: Context): Type = self.dealias match { + case AndType(tp1, tp2) => tp1.baseTypeWithArgs(base) & tp2.baseTypeWithArgs(base) + case OrType(tp1, tp2) => tp1.baseTypeWithArgs(base) | tp2.baseTypeWithArgs(base) + case _ => self.baseTypeRef(base).appliedTo(baseArgInfos(base)) + } /** Translate a type of the form From[T] to To[T], keep other types as they are. * `from` and `to` must be static classes, both with one type parameter, and the same variance. @@ -205,7 +211,7 @@ class TypeApplications(val self: Type) extends AnyVal { else self /** If this is an encoding of a (partially) applied type, return its arguments, - * otherwise return Nil. + * otherwise return Nil. * Existential types in arguments are returned as TypeBounds instances. */ final def argInfos(implicit ctx: Context): List[Type] = { diff --git a/src/dotty/tools/dotc/core/Types.scala b/src/dotty/tools/dotc/core/Types.scala index a0293d50a3e8..b06a95dc7934 100644 --- a/src/dotty/tools/dotc/core/Types.scala +++ b/src/dotty/tools/dotc/core/Types.scala @@ -102,8 +102,19 @@ object Types { } /** Is this type an instance of a non-bottom subclass of the given class `cls`? */ - final def derivesFrom(cls: Symbol)(implicit ctx: Context): Boolean = - classSymbol.derivesFrom(cls) + final def derivesFrom(cls: Symbol)(implicit ctx: Context): Boolean = this match { + case tp: TypeRef => + val sym = tp.symbol + if (sym.isClass) sym.derivesFrom(cls) else tp.underlying.derivesFrom(cls) + case tp: TypeProxy => + tp.underlying.derivesFrom(cls) + case tp: AndType => + tp.tp1.derivesFrom(cls) || tp.tp2.derivesFrom(cls) + case tp: OrType => + tp.tp1.derivesFrom(cls) && tp.tp2.derivesFrom(cls) + case _ => + false + } /** A type T is a legal prefix in a type selection T#A if * T is stable or T contains no abstract types @@ -254,6 +265,7 @@ object Types { /** The base classes of this type as determined by ClassDenotation * in linearization order, with the class itself as first element. + * For AndTypes/OrTypes, the union/intersection of the operands' baseclasses. * Inherited by all type proxies. `Nil` for all other types. */ final def baseClasses(implicit ctx: Context): List[ClassSymbol] = track("baseClasses") { @@ -262,6 +274,10 @@ object Types { tp.underlying.baseClasses case tp: ClassInfo => tp.cls.baseClasses + case AndType(tp1, tp2) => + tp1.baseClasses union tp2.baseClasses + case OrType(tp1, tp2) => + tp1.baseClasses intersect tp2.baseClasses case _ => Nil } } @@ -1863,7 +1879,7 @@ object Types { def | (that: TypeBounds)(implicit ctx: Context): TypeBounds = { val v = this commonVariance that - if (v == 0 && (this.lo eq this.hi) && (that.lo eq that.hi)) + if (v != 0 && (this.lo eq this.hi) && (that.lo eq that.hi)) if (v > 0) derivedTypeAlias(this.hi | that.hi, v) else derivedTypeAlias(this.lo & that.lo, v) else derivedTypeBounds(this.lo & that.lo, this.hi | that.hi, v) From 021c251869ddeccc9ff91ab5c0867a11f3c8cea3 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 8 Mar 2014 18:44:58 +0100 Subject: [PATCH 5/7] Merge refined types when distributing via "|". Use the equality (where ~ is any form of refinement) T1 { x ~ R1 } & T2 { x ~ R2 } == T1 & T2 { x ~ R1 & R2 } We already did the same thing when distributing via "&". --- src/dotty/tools/dotc/core/TypeComparer.scala | 35 +++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/dotty/tools/dotc/core/TypeComparer.scala b/src/dotty/tools/dotc/core/TypeComparer.scala index da8263ac1a3f..cfb9477c3a11 100644 --- a/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/src/dotty/tools/dotc/core/TypeComparer.scala @@ -890,6 +890,19 @@ class TypeComparer(initctx: Context) extends DotClass { /** Try to distribute `&` inside type, detect and handle conflicts */ private def distributeAnd(tp1: Type, tp2: Type): Type = tp1 match { + // opportunistically merge same-named refinements + // this does not change anything semantically (i.e. merging or not merging + // gives =:= types), but it keeps the type smaller. + case tp1: RefinedType => + tp2 match { + case tp2: RefinedType if tp1.refinedName == tp2.refinedName => + tp1.derivedRefinedType( + tp1.parent & tp2.parent, + tp1.refinedName, + tp1.refinedInfo & tp2.refinedInfo) + case _ => + NoType + } case tp1: TypeBounds => tp2 match { case tp2: TypeBounds => tp1 & tp2 @@ -935,18 +948,6 @@ class TypeComparer(initctx: Context) extends DotClass { case _ => rt1 & tp2 } - case tp1: RefinedType => - // opportunistically merge same-named refinements - // this does not change anything semantically (i.e. merging or not merging - // gives =:= types), but it keeps the type smaller. - tp2 match { - case tp2: RefinedType if tp1.refinedName == tp2.refinedName => - tp1.derivedRefinedType( - tp1.parent & tp2.parent, tp1.refinedName, - tp1.refinedInfo & tp2.refinedInfo) - case _ => - NoType - } case tp1: TypeVar if tp1.isInstantiated => tp1.underlying & tp2 case tp1: AnnotatedType => @@ -957,6 +958,16 @@ class TypeComparer(initctx: Context) extends DotClass { /** Try to distribute `|` inside type, detect and handle conflicts */ private def distributeOr(tp1: Type, tp2: Type): Type = tp1 match { + case tp1: RefinedType => + tp2 match { + case tp2: RefinedType if tp1.refinedName == tp2.refinedName => + tp1.derivedRefinedType( + tp1.parent | tp2.parent, + tp1.refinedName, + tp1.refinedInfo | tp2.refinedInfo) + case _ => + NoType + } case tp1: TypeBounds => tp2 match { case tp2: TypeBounds => tp1 | tp2 From 4611bdf0972fc01dfdfa647a0e84e3bccf98ea05 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 8 Mar 2014 19:05:54 +0100 Subject: [PATCH 6/7] Appromiximate union types by intersections. Appromiximate union types by intersections of their common base classes. Controlled by option -Xkeep-unions. If option is set, no approximation is done. Motivations for approximating: There are two. First, union types are departure from Scala 2. From time to time they lead to failure of inference. One example experiences in Dotty was in a foldLeft, where the accumulator type was inferred to be Tree before and was now a union of two tree specific kinds. Tree was the correct type, whereas the union type was too specific. These failures are not common (in the Dotty codebase there were 3, I believe), but they cause considerable difficulty to diagnose. So it seems safer to have a compatibility mode with Scala 2. The second motivation is that union types can become large and unwieldy. A function like TreeCopier has a result type consisting of ~ 40 alternatives, where the alternative type would be just Tree. Once we gain more experience with union types, we might consider flipping the option, and making union types the default. But for now it is safer this way, I believe. --- src/dotty/tools/dotc/core/TypeOps.scala | 31 ++++++++ src/dotty/tools/dotc/core/Types.scala | 47 +++++++++++- src/dotty/tools/dotc/typer/Namer.scala | 2 +- test/dotc/tests.scala | 1 + tests/pos/approximateUnion.scala | 96 +++++++++++++++++++++++++ 5 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 tests/pos/approximateUnion.scala diff --git a/src/dotty/tools/dotc/core/TypeOps.scala b/src/dotty/tools/dotc/core/TypeOps.scala index 22a86766bb2b..f9ff4270925a 100644 --- a/src/dotty/tools/dotc/core/TypeOps.scala +++ b/src/dotty/tools/dotc/core/TypeOps.scala @@ -77,6 +77,37 @@ trait TypeOps { this: Context => def apply(tp: Type) = simplify(tp, this) } + /** Approximate union type by intersection of its dominators. + * See Type#approximateUnion for an explanation. + */ + def approximateUnion(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) + } + if (ctx.featureEnabled(defn.LanguageModuleClass, nme.keepUnions)) tp + else tp match { + case tp: OrType => + val commonBaseClasses = tp.mapReduceOr(_.baseClasses)(intersect) + val doms = dominators(commonBaseClasses, Nil) + doms.map(tp.baseTypeWithArgs).reduceLeft(AndType.apply) + 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 + } + } + /** A type is volatile if its DNF contains an alternative of the form * {P1, ..., Pn}, {N1, ..., Nk}, where the Pi are parent typerefs and the * Nj are refinement names, and one the 4 following conditions is met: diff --git a/src/dotty/tools/dotc/core/Types.scala b/src/dotty/tools/dotc/core/Types.scala index b06a95dc7934..9b5e02186199 100644 --- a/src/dotty/tools/dotc/core/Types.scala +++ b/src/dotty/tools/dotc/core/Types.scala @@ -845,6 +845,19 @@ object Types { */ def simplified(implicit ctx: Context) = ctx.simplify(this, null) + /** Approximations of union types: We replace a union type Tn | ... | Tn + * by the smallest intersection type of baseclass 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(implicit ctx: Context) = ctx.approximateUnion(this) + /** customized hash code of this type. * NotCached for uncached types. Cached types * compute hash and use it as the type's hashCode. @@ -1355,6 +1368,10 @@ object Types { if ((tp1 eq this.tp1) && (tp2 eq this.tp2)) this else AndType.make(tp1, tp2) + def derived_& (tp1: Type, tp2: Type)(implicit ctx: Context): Type = + if ((tp1 eq this.tp1) && (tp2 eq this.tp2)) this + else tp1 & tp2 + def derivedAndOrType(tp1: Type, tp2: Type)(implicit ctx: Context): Type = derivedAndType(tp1, tp2) @@ -1735,10 +1752,38 @@ object Types { 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 AndType(tp1, tp2) => isOrType(tp1) | isOrType(tp2) + case RefinedType(parent, _) => isOrType(parent) + case WildcardType(bounds: TypeBounds) => isOrType(bounds.hi) + case _ => false + } + + // First, solve the constraint. var inst = ctx.typeComparer.approximation(origin, fromBelow) + + // Then, approximate by (1.) and (2.) 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 - instantiateWith(inst.simplified) + + 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 = inst.approximateUnion + + instantiateWith(inst) } /** Unwrap to instance (if instantiated) or origin (if not), until result diff --git a/src/dotty/tools/dotc/typer/Namer.scala b/src/dotty/tools/dotc/typer/Namer.scala index 2c5022726098..c318ddf3699c 100644 --- a/src/dotty/tools/dotc/typer/Namer.scala +++ b/src/dotty/tools/dotc/typer/Namer.scala @@ -576,7 +576,7 @@ class Namer { typer: Typer => // println(s"final inherited for $sym: ${inherited.toString}") !!! // println(s"owner = ${sym.owner}, decls = ${sym.owner.info.decls.show}") val rhsCtx = ctx.fresh addMode Mode.InferringReturnType - def rhsType = typedAheadExpr(mdef.rhs, rhsProto)(rhsCtx).tpe.widen + def rhsType = typedAheadExpr(mdef.rhs, rhsProto)(rhsCtx).tpe.widen.approximateUnion def lhsType = fullyDefinedType(rhsType, "right-hand side", mdef.pos) inherited orElse lhsType } diff --git a/test/dotc/tests.scala b/test/dotc/tests.scala index 21db07bd3fb9..0caef3cae20f 100644 --- a/test/dotc/tests.scala +++ b/test/dotc/tests.scala @@ -43,6 +43,7 @@ class tests extends CompilerTest { @Test def pos_structural() = compileFile(posDir, "structural", twice) @Test def pos_i39 = compileFile(posDir, "i39", twice) @Test def pos_overloadedAccess = compileFile(posDir, "overloadedAccess", twice) + @Test def pos_approximateUnion = compileFile(posDir, "approximateUnion", twice) */ @Test def pos_all = compileFiles(posDir, twice) diff --git a/tests/pos/approximateUnion.scala b/tests/pos/approximateUnion.scala new file mode 100644 index 000000000000..c3fe0e1625f5 --- /dev/null +++ b/tests/pos/approximateUnion.scala @@ -0,0 +1,96 @@ +object approximateUnion { + + trait C[+T] + trait D + trait E + trait X[-T] + + { + trait A extends C[A] with D + trait B extends C[B] with D + + val coin = true + val x = if (coin) new A else new B + val y = Some(if (coin) new A else new B) + + val xtest: C[A | B] & D = x + val ytest: Some[C[A | B] & D] = y + } + + { + trait A extends C[X[A]] with D + trait B extends C[X[B]] with D with E + + val coin = true + val x = if (coin) new A else new B + val y = Some(if (coin) new A else new B) + + val xtest: C[X[A & B]] & D = x + val ytest: Some[C[X[A & B]] & D] = y + } +} + +object approximateUnion2 { + + trait C[T] + trait D + trait E + trait X[-T] + + { + trait A extends C[A] with D + trait B extends C[B] with D + + val coin = true + val x = if (coin) new A else new B + val y = Some(if (coin) new A else new B) + + val xtest: C[_ >: A & B <: A | B] & D = x + val ytest: Some[C[_ >: A & B <: A | B] & D] = y + } + + { + trait A extends C[X[A]] with D + trait B extends C[X[B]] with D with E + + val coin = true + val x = if (coin) new A else new B + val y = Some(if (coin) new A else new B) + + val xtest: C[_ >: X[A | B] <: X[A & B]] & D = x + val ytest: Some[C[_ >: X[A | B] <: X[A & B]]] = y + } +} + +object approximateUnion3 { + + trait C[-T] + trait D + trait E + trait X[-T] + + { + trait A extends C[A] with D + trait B extends C[B] with D + + val coin = true + val x = if (coin) new A else new B + val y = Some(if (coin) new A else new B) + + val xtest: C[A & B] & D = x + val ytest: Some[C[A & B] & D] = y + } + + { + trait A extends C[X[A]] with D + trait B extends C[X[B]] with D with E + + val coin = true + val x = if (coin) new A else new B + val y = Some(if (coin) new A else new B) + + val xtest: C[X[A | B]] & D = x + val ytest2: Some[C[X[A | B]] & D] = y + } +} + From 3eab4101f516ececde15df74ca382a9fd7018fa8 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 8 Mar 2014 22:23:24 +0100 Subject: [PATCH 7/7] Removed annotations forced by union types Removed annotations from the dotty codebase that were necessary because the inferred union types caused inference errors later on. --- src/dotty/tools/dotc/ast/Desugar.scala | 5 +++-- src/dotty/tools/dotc/core/Definitions.scala | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/dotty/tools/dotc/ast/Desugar.scala b/src/dotty/tools/dotc/ast/Desugar.scala index 01aaf0b5ffe7..0c741a652c58 100644 --- a/src/dotty/tools/dotc/ast/Desugar.scala +++ b/src/dotty/tools/dotc/ast/Desugar.scala @@ -180,8 +180,9 @@ object desugar { constr1.mods, constr1.name, tparams, vparamss, constr1.tpt, constr1.rhs) // a reference to the class type, with all parameters given. - val classTypeRef: Tree = { - // Dotty deviation: Without type annotation infers Ident | AppliedTypeTree, which + val classTypeRef/*: Tree*/ = { + // -language:keepUnions difference: classTypeRef needs type annotation, otherwise + // infers Ident | AppliedTypeTree, which // renders the :\ in companions below untypable. val tycon = Ident(cdef.name) withPos cdef.pos.startPos val tparams = impl.constr.tparams diff --git a/src/dotty/tools/dotc/core/Definitions.scala b/src/dotty/tools/dotc/core/Definitions.scala index 22e308062989..a58bfc05816d 100644 --- a/src/dotty/tools/dotc/core/Definitions.scala +++ b/src/dotty/tools/dotc/core/Definitions.scala @@ -286,8 +286,9 @@ class Definitions { object FunctionType { def apply(args: List[Type], resultType: Type) = FunctionClass(args.length).typeRef.appliedTo(args ::: resultType :: Nil) - def unapply(ft: Type): Option[(List[Type], Type)] = { // Dotty deviation: Type annotation needed because inferred type - // is Some[(List[Type], Type)] | None, which is not a legal unapply type. + def unapply(ft: Type)/*: Option[(List[Type], Type)]*/ = { + // -language:keepUnions difference: unapply needs result type because inferred type + // is Some[(List[Type], Type)] | None, which is not a legal unapply type. val tsym = ft.typeSymbol lazy val targs = ft.argInfos if ((FunctionClasses contains tsym) &&