From 79045e2c0e2830818a9be1dfaa2a7f7f83f9c0de Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 1 Oct 2016 21:23:05 +0200 Subject: Fix non-sensical code Replacing or types by their dominators and implicit conversions caused the code to do the right thing anyway, but with the arrival of true or-types, this became a static error. --- src/dotty/tools/dotc/transform/LambdaLift.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/dotty/tools/dotc/transform/LambdaLift.scala b/src/dotty/tools/dotc/transform/LambdaLift.scala index 18b030913..19fb3dd0c 100644 --- a/src/dotty/tools/dotc/transform/LambdaLift.scala +++ b/src/dotty/tools/dotc/transform/LambdaLift.scala @@ -121,7 +121,10 @@ class LambdaLift extends MiniPhase with IdentityDenotTransformer { thisTransform private def symSet(f: LinkedHashMap[Symbol, SymSet], sym: Symbol): SymSet = f.getOrElseUpdate(sym, newSymSet) - def freeVars(sym: Symbol): List[Symbol] = free.getOrElse(sym, Nil).toList + def freeVars(sym: Symbol): List[Symbol] = free get sym match { + case Some(set) => set.toList + case None => Nil + } def proxyOf(sym: Symbol, fv: Symbol) = proxyMap.getOrElse(sym, Map.empty)(fv) -- cgit v1.2.3 From bc791ed3390e954b7338594b771ad09c5150d591 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 1 Oct 2016 21:26:38 +0200 Subject: Reformatting to avoid a long line --- src/dotty/tools/dotc/transform/TailRec.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/dotty/tools/dotc/transform/TailRec.scala b/src/dotty/tools/dotc/transform/TailRec.scala index 065bcb397..d99a48af3 100644 --- a/src/dotty/tools/dotc/transform/TailRec.scala +++ b/src/dotty/tools/dotc/transform/TailRec.scala @@ -252,7 +252,10 @@ class TailRec extends MiniPhaseTransform with DenotTransformer with FullParamete } else targs val method = if (callTargs.nonEmpty) TypeApply(Ident(label.termRef), callTargs) else Ident(label.termRef) - val thisPassed = if(this.method.owner.isClass) method appliedTo(receiver.ensureConforms(method.tpe.widen.firstParamTypes.head)) else method + val thisPassed = + if (this.method.owner.isClass) + method.appliedTo(receiver.ensureConforms(method.tpe.widen.firstParamTypes.head)) + else method val res = if (thisPassed.tpe.widen.isParameterless) thisPassed -- cgit v1.2.3 From f6291e42082708e164ebc3456d84e69f4f29cf59 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 1 Oct 2016 21:29:20 +0200 Subject: Change default of unsafe Config option --- src/dotty/tools/dotc/config/Config.scala | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/dotty/tools/dotc/config/Config.scala b/src/dotty/tools/dotc/config/Config.scala index c188bfab4..7744a5479 100644 --- a/src/dotty/tools/dotc/config/Config.scala +++ b/src/dotty/tools/dotc/config/Config.scala @@ -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 * -- cgit v1.2.3 From 98a69d90b16f0cc997255f097d3d5d9f2a60301b Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 1 Oct 2016 21:35:58 +0200 Subject: Keep or types Don't replace them by their dominators, unless one of the following holds: - language:Scala2 mode is on - we are at the point of findMember selection - we compare with a higher-kinded application This means approximateUnion is now split into harmonizeUnion and orDominator which each implement one of the former's two functionalities. --- src/dotty/tools/dotc/core/ConstraintHandling.scala | 15 ++- src/dotty/tools/dotc/core/StdNames.scala | 1 - src/dotty/tools/dotc/core/TypeOps.scala | 128 ++++++++++++--------- src/dotty/tools/dotc/core/Types.scala | 46 ++++---- src/dotty/tools/dotc/typer/Namer.scala | 2 +- 5 files changed, 110 insertions(+), 82 deletions(-) diff --git a/src/dotty/tools/dotc/core/ConstraintHandling.scala b/src/dotty/tools/dotc/core/ConstraintHandling.scala index 5911af72c..84f531385 100644 --- a/src/dotty/tools/dotc/core/ConstraintHandling.scala +++ b/src/dotty/tools/dotc/core/ConstraintHandling.scala @@ -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` */ @@ -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 } @@ -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. diff --git a/src/dotty/tools/dotc/core/StdNames.scala b/src/dotty/tools/dotc/core/StdNames.scala index c52264637..920c9635e 100644 --- a/src/dotty/tools/dotc/core/StdNames.scala +++ b/src/dotty/tools/dotc/core/StdNames.scala @@ -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" diff --git a/src/dotty/tools/dotc/core/TypeOps.scala b/src/dotty/tools/dotc/core/TypeOps.scala index bee69ae69..5edd61c70 100644 --- a/src/dotty/tools/dotc/core/TypeOps.scala +++ b/src/dotty/tools/dotc/core/TypeOps.scala @@ -173,21 +173,53 @@ 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 @@ -195,78 +227,70 @@ trait TypeOps { this: Context => // TODO: Make standalone object. 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) = { diff --git a/src/dotty/tools/dotc/core/Types.scala b/src/dotty/tools/dotc/core/Types.scala index 2f1b6b829..0f81f8c38 100644 --- a/src/dotty/tools/dotc/core/Types.scala +++ b/src/dotty/tools/dotc/core/Types.scala @@ -436,8 +436,12 @@ object Types { tp.cls.findMember(name, pre, excluded) case AndType(l, r) => goAnd(l, r) - case OrType(l, r) => - goOr(l, r) + case tp: OrType => + // we need to keep the invariant that `pre <: tp`. Branch `union-types-narrow-prefix` + // achieved that by narrowing `pre` to each alternative, but it led to merge errors in + // lots of places. The present strategy is instead of widen `tp` using `join` to be a + // supertype of `pre`. + go(tp.join) case tp: JavaArrayType => defn.ObjectType.findMember(name, pre, excluded) case ErrorType => @@ -556,7 +560,6 @@ object Types { def goAnd(l: Type, r: Type) = { go(l) & (go(r), pre, safeIntersection = ctx.pendingMemberSearches.contains(name)) } - def goOr(l: Type, r: Type) = go(l) | (go(r), pre) { val recCount = ctx.findMemberCount + 1 ctx.findMemberCount = recCount @@ -1230,28 +1233,6 @@ 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` - * - * As a second measure we also homogenize refinements containing - * type variables. For instance, if `A` is an instantiatable type variable, - * then - * - * ArrayBuffer[Int] | ArrayBuffer[A] - * - * is approximated by instantiating `A` to `Int` and returning `ArrayBuffer[Int]` - * instead of `ArrayBuffer[_ >: Int | A <: Int & A]` - */ - 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. @@ -2233,9 +2214,24 @@ object Types { } abstract case class OrType(tp1: Type, tp2: Type) extends CachedGroundType with AndOrType { + assert(tp1.isInstanceOf[ValueType] && tp2.isInstanceOf[ValueType]) def isAnd = false + private[this] var myJoin: Type = _ + private[this] var myJoinPeriod: Period = Nowhere + + /** Replace or type by the closest non-or type above it */ + def join(implicit ctx: Context): Type = { + if (myJoinPeriod != ctx.period) { + myJoin = ctx.orDominator(this) + core.println(i"join of $this == $myJoin") + assert(myJoin != this) + myJoinPeriod = ctx.period + } + myJoin + } + def derivedOrType(tp1: Type, tp2: Type)(implicit ctx: Context): Type = if ((tp1 eq this.tp1) && (tp2 eq this.tp2)) this else OrType.make(tp1, tp2) diff --git a/src/dotty/tools/dotc/typer/Namer.scala b/src/dotty/tools/dotc/typer/Namer.scala index 4f4278468..00e92cbfb 100644 --- a/src/dotty/tools/dotc/typer/Namer.scala +++ b/src/dotty/tools/dotc/typer/Namer.scala @@ -898,7 +898,7 @@ class Namer { typer: Typer => // definition is inline (i.e. final in Scala2). def widenRhs(tp: Type): Type = tp.widenTermRefExpr match { case tp: ConstantType if isInline => tp - case _ => tp.widen.approximateUnion + case _ => ctx.harmonizeUnion(tp.widen) } // Replace aliases to Unit by Unit itself. If we leave the alias in -- cgit v1.2.3 From d47f3280a7d46fa5e65ebfc986ed04b7b739aae2 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 1 Oct 2016 21:48:34 +0200 Subject: Handle feature interaction between subtyping or types and hk types --- src/dotty/tools/dotc/core/TypeComparer.scala | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/dotty/tools/dotc/core/TypeComparer.scala b/src/dotty/tools/dotc/core/TypeComparer.scala index 991dd2664..0a51b896a 100644 --- a/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/src/dotty/tools/dotc/core/TypeComparer.scala @@ -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 _ => -- cgit v1.2.3 From 14551e23d2de2db24864560c0b058306d1c78832 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 1 Oct 2016 21:50:22 +0200 Subject: Refine mergeEntries If entries are type variables, we have to check their instances for equality. This came up onder the new or handling scheme. --- src/dotty/tools/dotc/core/OrderingConstraint.scala | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/dotty/tools/dotc/core/OrderingConstraint.scala b/src/dotty/tools/dotc/core/OrderingConstraint.scala index 458f8b82f..68c7655ef 100644 --- a/src/dotty/tools/dotc/core/OrderingConstraint.scala +++ b/src/dotty/tools/dotc/core/OrderingConstraint.scala @@ -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 } -- cgit v1.2.3 From 3d74bfa72bdc794cfb11b6afe15c77a5357617d1 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 1 Oct 2016 21:50:57 +0200 Subject: Remove unused language option In fact all of dotty.language can be removed. --- src/dotty/language.scala | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/dotty/language.scala diff --git a/src/dotty/language.scala b/src/dotty/language.scala deleted file mode 100644 index 416a4281b..000000000 --- a/src/dotty/language.scala +++ /dev/null @@ -1,9 +0,0 @@ -package dotty - -object language { - - class Feature - - /** Keep union types */ - val keepUnions = new Feature -} -- cgit v1.2.3 From 25c0398b6f07df2449652e66cef8b6a6d3d4c7ce Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 1 Oct 2016 21:51:34 +0200 Subject: Adapt tests --- tests/neg/OrType.scala | 11 +++++++++++ tests/neg/unions.scala | 32 ++++++++++++++++++++++++++++++++ tests/pickling/unions.scala | 33 --------------------------------- tests/pos/i1045.scala | 17 +++++++++++++++++ tests/pos/union.scala | 11 +++++++++++ tests/pos/unions.scala | 32 -------------------------------- tests/run/OrType.scala | 11 ----------- tests/run/OrderingTest.scala | 3 ++- tests/run/flat-flat-flat.scala | 4 ++++ 9 files changed, 77 insertions(+), 77 deletions(-) create mode 100644 tests/neg/OrType.scala create mode 100644 tests/neg/unions.scala delete mode 100644 tests/pickling/unions.scala create mode 100644 tests/pos/union.scala delete mode 100644 tests/pos/unions.scala delete mode 100644 tests/run/OrType.scala diff --git a/tests/neg/OrType.scala b/tests/neg/OrType.scala new file mode 100644 index 000000000..a9860db93 --- /dev/null +++ b/tests/neg/OrType.scala @@ -0,0 +1,11 @@ +class B(val x: Int) +class C(val x: Double) + +object Test{ + def bar(x: B | C): Int | Double = x.x // error + def main(args: Array[String]): Unit = { + val b = new B(1) + val c = new C(1) + bar(if (b.hashCode > c.hashCode) b else c) + } +} diff --git a/tests/neg/unions.scala b/tests/neg/unions.scala new file mode 100644 index 000000000..099a628c9 --- /dev/null +++ b/tests/neg/unions.scala @@ -0,0 +1,32 @@ +object unions { + + class A { + def f: String = "abc" + + def g(x: Int): Int = x + def g(x: Double): Double = x + } + + class B { + def f: String = "bcd" + + def g(x: Int) = -x + def g(x: Double): Double = -x + } + + val x: A | B = if (true) new A else new B + def y: B | A = if (true) new A else new B + println(x.f) // error + println(x.g(2)) // error + println(y.f) // error + println(y.g(1.0)) // error + + class C { + private def foo = 0 + class D extends C { + private def foo = 1 + def test(cd: C | D, dc: D | C) = (cd.foo, dc.foo) + } + } + +} diff --git a/tests/pickling/unions.scala b/tests/pickling/unions.scala deleted file mode 100644 index 22e6391e3..000000000 --- a/tests/pickling/unions.scala +++ /dev/null @@ -1,33 +0,0 @@ -object unions { - - class A { - def f: String = "abc" - - def g(x: Int): Int = x - def g(x: Double): Double = x - } - - class B { - def f: String = "bcd" - - def g(x: Int) = -x - def g(x: Double): Double = -x - } - - val x: A | B = if (true) new A else new B - def y: B | A = if (true) new A else new B - println(x.f) - println(x.g(2)) - println(y.f) - println(y.g(1.0)) - println(y.g(1.0f)) - - class C { - private def foo = 0 - class D extends C { - private def foo = 1 - def test(cd: C | D, dc: D | C) = (cd.foo, dc.foo) - } - } - -} diff --git a/tests/pos/i1045.scala b/tests/pos/i1045.scala index f5985af92..f0cf1df90 100644 --- a/tests/pos/i1045.scala +++ b/tests/pos/i1045.scala @@ -1,7 +1,24 @@ import scala.collection._ + +object EmptyHashMap extends mutable.HashMap[Nothing, Nothing] object T { val newSymbolMap: mutable.HashMap[String, mutable.HashMap[Int, Double]] = mutable.HashMap.empty val map = newSymbolMap.getOrElse("a", mutable.HashMap.empty) map.put(1, 0.0) newSymbolMap.put("a", map) + + /** A map storing free variables of functions and classes */ +// type SymSet = Set[Symbol] +// private val free = new collection.mutable.LinkedHashMap[Symbol, SymSet] +// def freeVars(sym: Symbol): List[Symbol] = free.getOrElse(sym, Nil).toList + + class Tree[X >: Null] { def tpe: X = null } + class Ident[X >: Null] extends Tree[X] + class Apply[X >: Null] extends Tree[X] + + val x: Ident[Symbol] | Apply[Symbol] = ??? + val y = x.tpe + val z: Symbol = y + + } diff --git a/tests/pos/union.scala b/tests/pos/union.scala new file mode 100644 index 000000000..8b20a8458 --- /dev/null +++ b/tests/pos/union.scala @@ -0,0 +1,11 @@ +object Test { + + class A + class B extends A + class C extends A + class D extends A + + val b = true + val x = if (b) new B else new C + val y: B | C = x +} diff --git a/tests/pos/unions.scala b/tests/pos/unions.scala deleted file mode 100644 index e57a96fb9..000000000 --- a/tests/pos/unions.scala +++ /dev/null @@ -1,32 +0,0 @@ -object unions { - - class A { - def f: String = "abc" - - def g(x: Int): Int = x - def g(x: Double): Double = x - } - - class B { - def f: String = "bcd" - - def g(x: Int) = -x - def g(x: Double): Double = -x - } - - val x: A | B = if (true) new A else new B - def y: B | A = if (true) new A else new B - println(x.f) - println(x.g(2)) - println(y.f) - println(y.g(1.0)) - - class C { - private def foo = 0 - class D extends C { - private def foo = 1 - def test(cd: C | D, dc: D | C) = (cd.foo, dc.foo) - } - } - -} diff --git a/tests/run/OrType.scala b/tests/run/OrType.scala deleted file mode 100644 index 9ab805def..000000000 --- a/tests/run/OrType.scala +++ /dev/null @@ -1,11 +0,0 @@ -class B(val x: Int) -class C(val x: Double) - -object Test{ - def bar(x: B | C): Int | Double = x.x - def main(args: Array[String]): Unit = { - val b = new B(1) - val c = new C(1) - bar(if (b.hashCode > c.hashCode) b else c) - } -} diff --git a/tests/run/OrderingTest.scala b/tests/run/OrderingTest.scala index ad5acfa0c..561a3daeb 100644 --- a/tests/run/OrderingTest.scala +++ b/tests/run/OrderingTest.scala @@ -23,7 +23,8 @@ object Test extends dotty.runtime.LegacyApp { testAll(false, true) testAll(1, 2); testAll(1.0, 2.0); - testAll(None, Some(1)); + // testAll(Some(1), Some(2)) // does not work in nsc or dotty + // testAll(None, Some(1)); // does not work in dotty with or-types testAll[Iterable[Int]](List(1), List(1, 2)); testAll[Iterable[Int]](List(1, 2), List(2)); testAll((1, "bar"), (1, "foo")) diff --git a/tests/run/flat-flat-flat.scala b/tests/run/flat-flat-flat.scala index 80868b9c5..e0fabe4e7 100644 --- a/tests/run/flat-flat-flat.scala +++ b/tests/run/flat-flat-flat.scala @@ -2,10 +2,14 @@ object Test { def f1 = List(Iterator(Some(1), None, Some(2)), Iterator(Some(3), None)) def f2 = Iterator(List(Some(1), None, Some(2)), List(Some(3), None), Nil) def f3 = List(Some(Iterator(1)), None, Some(Iterator(2, 3))) + def f4 = List(Some(Iterator(1)), Some(Iterator(2, 3))) + def f5 = Iterator(List(Some(1), Some(2)), List(Some(3)), Nil) def main(args: Array[String]): Unit = { assert(f1.flatten.flatten.toList == List(1, 2, 3)) + assert(f5.flatten.flatten.toList == List(1, 2, 3)) assert(f2.flatten.flatten.toList == List(1, 2, 3)) assert(f3.flatten.flatten.toList == List(1, 2, 3)) + assert(f4.flatten.flatten.toList == List(1, 2, 3)) } } -- cgit v1.2.3 From 797a9385dc6971a4583c989f2a7ddae4d3c3ab50 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 2 Oct 2016 12:43:55 +0200 Subject: Scrutinize selections in TreeChecker Makes sure the symbol in the tree can be approximately reconstructed by calling member on the qualifier type. Approximately means: The two symbols might be different but one still overrides the other. --- src/dotty/tools/dotc/transform/TreeChecker.scala | 27 ++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/dotty/tools/dotc/transform/TreeChecker.scala b/src/dotty/tools/dotc/transform/TreeChecker.scala index fd8e41dc3..808178369 100644 --- a/src/dotty/tools/dotc/transform/TreeChecker.scala +++ b/src/dotty/tools/dotc/transform/TreeChecker.scala @@ -133,10 +133,7 @@ class TreeChecker extends Phase with SymTransformer { catch { case NonFatal(ex) => //TODO CHECK. Check that we are bootstrapped implicit val ctx: Context = checkingCtx - ctx.echo(i"*** error while checking ${ctx.compilationUnit} after phase ${checkingCtx.phase.prev} ***") - ctx.echo(ex.toString) - ctx.echo(ex.getStackTrace.take(30).deep.mkString("\n")) - ctx.echo("<<<") + println(i"*** error while checking ${ctx.compilationUnit} after phase ${checkingCtx.phase.prev} ***") throw ex } } @@ -331,8 +328,30 @@ class TreeChecker extends Phase with SymTransformer { checkNotRepeated(super.typedIdent(tree, pt)) } + /** Makes sure the symbol in the tree can be approximately reconstructed by + * calling `member` on the qualifier type. + * Approximately means: The two symbols might be different but one still overrides the other. + */ override def typedSelect(tree: untpd.Select, pt: Type)(implicit ctx: Context): Tree = { assert(tree.isTerm || !ctx.isAfterTyper, tree.show + " at " + ctx.phase) + val tpe = tree.typeOpt + val sym = tree.symbol + if (!tpe.isInstanceOf[WithFixedSym] && sym.exists && !sym.is(Private)) { + val qualTpe = tree.qualifier.typeOpt + val member = + if (sym.is(Private)) qualTpe.member(tree.name) + else qualTpe.nonPrivateMember(tree.name) + val memberSyms = member.alternatives.map(_.symbol) + assert(memberSyms.exists(mbr => + sym == mbr || + sym.overriddenSymbol(mbr.owner.asClass) == mbr || + mbr.overriddenSymbol(sym.owner.asClass) == sym), + ex"""symbols differ for $tree + |was : $sym + |alternatives by type: $memberSyms%, % of types ${memberSyms.map(_.info)}%, % + |qualifier type : ${tree.qualifier.typeOpt} + |tree type : ${tree.typeOpt} of class ${tree.typeOpt.getClass}""") + } checkNotRepeated(super.typedSelect(tree, pt)) } -- cgit v1.2.3 From 146bc29acaba58391a5462ee26f989debaac9038 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 2 Oct 2016 12:45:41 +0200 Subject: Refactor Splitter functionality Splitting or types is no longer needed with new scheme. Replacing idents with This nodes is better done in ExplicitSelf. So splitter now just distributes applications into and ifs. --- src/dotty/tools/dotc/transform/ExplicitSelf.scala | 9 ++++ src/dotty/tools/dotc/transform/Splitter.scala | 51 ++++++++++------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/dotty/tools/dotc/transform/ExplicitSelf.scala b/src/dotty/tools/dotc/transform/ExplicitSelf.scala index 618a0f108..7bb65e575 100644 --- a/src/dotty/tools/dotc/transform/ExplicitSelf.scala +++ b/src/dotty/tools/dotc/transform/ExplicitSelf.scala @@ -20,12 +20,21 @@ import Flags._ * * where `S` is the self type of `C`. * See run/i789.scala for a test case why this is needed. + * + * Also replaces idents referring to the self type with ThisTypes. */ class ExplicitSelf extends MiniPhaseTransform { thisTransform => import ast.tpd._ override def phaseName = "explicitSelf" + override def transformIdent(tree: Ident)(implicit ctx: Context, info: TransformerInfo) = tree.tpe match { + case tp: ThisType => + ctx.debuglog(s"owner = ${ctx.owner}, context = ${ctx}") + This(tp.cls) withPos tree.pos + case _ => tree + } + override def transformSelect(tree: Select)(implicit ctx: Context, info: TransformerInfo): Tree = tree match { case Select(thiz: This, name) if name.isTermName => val cls = thiz.symbol.asClass diff --git a/src/dotty/tools/dotc/transform/Splitter.scala b/src/dotty/tools/dotc/transform/Splitter.scala index efcf95ede..d62be1a82 100644 --- a/src/dotty/tools/dotc/transform/Splitter.scala +++ b/src/dotty/tools/dotc/transform/Splitter.scala @@ -6,25 +6,34 @@ import ast.Trees._ import core._ import Contexts._, Types._, Decorators._, Denotations._, Symbols._, SymDenotations._, Names._ -/** This transform makes sure every identifier and select node - * carries a symbol. To do this, certain qualifiers with a union type - * have to be "splitted" with a type test. - * - * For now, only self references are treated. +/** Distribute applications into Block and If nodes */ class Splitter extends MiniPhaseTransform { thisTransform => import ast.tpd._ override def phaseName: String = "splitter" - /** Replace self referencing idents with ThisTypes. */ - override def transformIdent(tree: Ident)(implicit ctx: Context, info: TransformerInfo) = tree.tpe match { - case tp: ThisType => - ctx.debuglog(s"owner = ${ctx.owner}, context = ${ctx}") - This(tp.cls) withPos tree.pos - case _ => tree + /** Distribute arguments among splitted branches */ + def distribute(tree: GenericApply[Type], rebuild: (Tree, List[Tree]) => Context => Tree)(implicit ctx: Context) = { + def recur(fn: Tree): Tree = fn match { + case Block(stats, expr) => Block(stats, recur(expr)) + case If(cond, thenp, elsep) => If(cond, recur(thenp), recur(elsep)) + case _ => rebuild(fn, tree.args)(ctx) withPos tree.pos + } + recur(tree.fun) } + override def transformTypeApply(tree: TypeApply)(implicit ctx: Context, info: TransformerInfo) = + distribute(tree, typeApply) + + override def transformApply(tree: Apply)(implicit ctx: Context, info: TransformerInfo) = + distribute(tree, apply) + + private val typeApply = (fn: Tree, args: List[Tree]) => (ctx: Context) => TypeApply(fn, args)(ctx) + private val apply = (fn: Tree, args: List[Tree]) => (ctx: Context) => Apply(fn, args)(ctx) + +/* The following is no longer necessary, since we select members on the join of an or type: + * /** If we select a name, make sure the node has a symbol. * If necessary, split the qualifier with type tests. * Example: Assume: @@ -108,23 +117,5 @@ class Splitter extends MiniPhaseTransform { thisTransform => evalOnce(qual)(qual => choose(qual, candidates(qual.tpe))) } } - - /** Distribute arguments among splitted branches */ - def distribute(tree: GenericApply[Type], rebuild: (Tree, List[Tree]) => Context => Tree)(implicit ctx: Context) = { - def recur(fn: Tree): Tree = fn match { - case Block(stats, expr) => Block(stats, recur(expr)) - case If(cond, thenp, elsep) => If(cond, recur(thenp), recur(elsep)) - case _ => rebuild(fn, tree.args)(ctx) withPos tree.pos - } - recur(tree.fun) - } - - override def transformTypeApply(tree: TypeApply)(implicit ctx: Context, info: TransformerInfo) = - distribute(tree, typeApply) - - override def transformApply(tree: Apply)(implicit ctx: Context, info: TransformerInfo) = - distribute(tree, apply) - - private val typeApply = (fn: Tree, args: List[Tree]) => (ctx: Context) => TypeApply(fn, args)(ctx) - private val apply = (fn: Tree, args: List[Tree]) => (ctx: Context) => Apply(fn, args)(ctx) +*/ } -- cgit v1.2.3 From 2fe7e9220ab12336d4dcddbe9b523a736a6c17e8 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 2 Oct 2016 13:15:35 +0200 Subject: Disallow singleton types in unions For the moment, we do not know how to handle something like 1 | 2 or x.type | y.type correctly. So it's better to disallow these situations until we find a proper solution. --- src/dotty/tools/dotc/typer/Checking.scala | 8 ++++++++ src/dotty/tools/dotc/typer/Typer.scala | 5 +++-- tests/neg/singletonOrs.scala | 6 ++++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 tests/neg/singletonOrs.scala diff --git a/src/dotty/tools/dotc/typer/Checking.scala b/src/dotty/tools/dotc/typer/Checking.scala index 7ba66e3d8..3461facc1 100644 --- a/src/dotty/tools/dotc/typer/Checking.scala +++ b/src/dotty/tools/dotc/typer/Checking.scala @@ -542,6 +542,13 @@ trait Checking { errorTree(tpt, ex"missing type parameter for ${tpt.tpe}") } else tpt + + /** Check that `tpt` does not refer to a singleton type */ + def checkNotSingleton(tpt: Tree, where: String)(implicit ctx: Context): Tree = + if (tpt.tpe.isInstanceOf[SingletonType]) { + errorTree(tpt, ex"Singleton type ${tpt.tpe} is not allowed $where") + } + else tpt } trait NoChecking extends Checking { @@ -556,4 +563,5 @@ trait NoChecking extends Checking { override def checkNoDoubleDefs(cls: Symbol)(implicit ctx: Context): Unit = () override def checkParentCall(call: Tree, caller: ClassSymbol)(implicit ctx: Context) = () override def checkSimpleKinded(tpt: Tree)(implicit ctx: Context): Tree = tpt + override def checkNotSingleton(tpt: Tree, where: String)(implicit ctx: Context): Tree = tpt } diff --git a/src/dotty/tools/dotc/typer/Typer.scala b/src/dotty/tools/dotc/typer/Typer.scala index bbb20bcf5..e423082d5 100644 --- a/src/dotty/tools/dotc/typer/Typer.scala +++ b/src/dotty/tools/dotc/typer/Typer.scala @@ -999,8 +999,9 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit } def typedOrTypeTree(tree: untpd.OrTypeTree)(implicit ctx: Context): OrTypeTree = track("typedOrTypeTree") { - val left1 = typed(tree.left) - val right1 = typed(tree.right) + val where = "in a union type" + val left1 = checkNotSingleton(typed(tree.left), where) + val right1 = checkNotSingleton(typed(tree.right), where) assignType(cpy.OrTypeTree(tree)(left1, right1), left1, right1) } diff --git a/tests/neg/singletonOrs.scala b/tests/neg/singletonOrs.scala new file mode 100644 index 000000000..687e491ef --- /dev/null +++ b/tests/neg/singletonOrs.scala @@ -0,0 +1,6 @@ +object Test { + def foo: 1 | 2 = 1 // error // error + def bar: 3 | 4 = foo // error // error + def foo: 1 | 2 = 1 // error // error + def bar: 1 = foo +} -- cgit v1.2.3 From 2f01c7d1ba4ab4d542c3838e184a5ac9735578f4 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 2 Oct 2016 13:17:04 +0200 Subject: Don't report double def errors if symbol's type is erroneous This happened for singletonOrs, and led to spurious errors there. --- src/dotty/tools/dotc/core/Denotations.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dotty/tools/dotc/core/Denotations.scala b/src/dotty/tools/dotc/core/Denotations.scala index 4f01c43cf..0f95fc591 100644 --- a/src/dotty/tools/dotc/core/Denotations.scala +++ b/src/dotty/tools/dotc/core/Denotations.scala @@ -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) = -- cgit v1.2.3 From da344548d7425368ccd5bf7a98e522d00cdc95aa Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 10 Oct 2016 09:15:42 +0200 Subject: Drop dotty.language from Definitions --- src/dotty/tools/dotc/core/Definitions.scala | 4 +--- src/dotty/tools/dotc/core/TypeOps.scala | 6 +++--- tests/pos/intersection.scala | 1 - 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/dotty/tools/dotc/core/Definitions.scala b/src/dotty/tools/dotc/core/Definitions.scala index 75b75d3d5..b1c2bc535 100644 --- a/src/dotty/tools/dotc/core/Definitions.scala +++ b/src/dotty/tools/dotc/core/Definitions.scala @@ -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") diff --git a/src/dotty/tools/dotc/core/TypeOps.scala b/src/dotty/tools/dotc/core/TypeOps.scala index 5edd61c70..d480a792b 100644 --- a/src/dotty/tools/dotc/core/TypeOps.scala +++ b/src/dotty/tools/dotc/core/TypeOps.scala @@ -517,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 = { @@ -536,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) diff --git a/tests/pos/intersection.scala b/tests/pos/intersection.scala index a4c19b5af..48551920c 100644 --- a/tests/pos/intersection.scala +++ b/tests/pos/intersection.scala @@ -1,4 +1,3 @@ -import dotty.language.keepUnions object intersection { class A -- cgit v1.2.3 From 8067b952875426d640968be865773f6ef3783f3c Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 10 Oct 2016 18:59:44 +0200 Subject: Fix cutting problem Test case: orInf.scala. This showed a problem where an `either` operation had to arbitrarily pick one constraint over another, leading to a type error down the line. What happened was that a `constrainResult` generated the constraint Set[A] <: Set[String] | Set[Int] But this constraint cannot be simplified without a cut and a resulting loss of information. We avoid the problem by not constraining the result if the prototype is a disjunction. --- src/dotty/tools/dotc/core/TypeComparer.scala | 7 +++++-- src/dotty/tools/dotc/typer/ProtoTypes.scala | 7 ++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/dotty/tools/dotc/core/TypeComparer.scala b/src/dotty/tools/dotc/core/TypeComparer.scala index 0a51b896a..52b248abb 100644 --- a/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/src/dotty/tools/dotc/core/TypeComparer.scala @@ -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 @@ -837,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 } diff --git a/src/dotty/tools/dotc/typer/ProtoTypes.scala b/src/dotty/tools/dotc/typer/ProtoTypes.scala index 0e6697fb7..dd5705fbf 100644 --- a/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -43,6 +43,11 @@ object ProtoTypes { isCompatible(normalize(tp, pt)(nestedCtx), pt)(nestedCtx) } + private def disregardProto(pt: Type)(implicit ctx: Context): Boolean = pt.dealias match { + case _: OrType => true + case pt => pt.isRef(defn.UnitClass) + } + /** Check that the result type of the current method * fits the given expected result type. */ @@ -54,7 +59,7 @@ object ProtoTypes { case _ => true } - case _: ValueTypeOrProto if !(pt isRef defn.UnitClass) => + case _: ValueTypeOrProto if !disregardProto(pt) => mt match { case mt: MethodType => mt.isDependent || isCompatible(normalize(mt, pt), pt) -- cgit v1.2.3 From ba18173c4ac655eb07eca036a81a7a8b9e76caa7 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 10 Oct 2016 19:00:38 +0200 Subject: Add test case --- tests/pos/orinf.scala | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tests/pos/orinf.scala diff --git a/tests/pos/orinf.scala b/tests/pos/orinf.scala new file mode 100644 index 000000000..30b7fd2f6 --- /dev/null +++ b/tests/pos/orinf.scala @@ -0,0 +1,6 @@ +object Test { + + def foo(lis: scala.collection.immutable.Set[Int] | scala.collection.immutable.Set[String]) = lis + foo(Set(1)) + foo(Set("")) +} -- cgit v1.2.3