From 9688e941cd9f6f7c07c684a605515aca355c672d Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Wed, 28 Jun 2023 18:47:24 +0200 Subject: [PATCH 1/7] Move tupleArity to TypeErasure and document its shortcomings [Cherry-picked 50dc2a2322b5587746a56172888be33ee91fe8aa] --- .../dotty/tools/dotc/core/TypeErasure.scala | 30 +++++++++++++++++-- .../dotc/transform/GenericSignatures.scala | 4 +-- .../tools/dotc/transform/TypeUtils.scala | 18 ----------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 9bcb3eca36bb..cb44adbcaa60 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -74,6 +74,32 @@ object TypeErasure { private def erasureDependsOnArgs(sym: Symbol)(using Context) = sym == defn.ArrayClass || sym == defn.PairClass || isDerivedValueClass(sym) + /** The arity of this tuple type, which can be made up of EmptyTuple, TupleX and `*:` pairs. + * + * NOTE: This method is used to determine how to erase tuples, so it can + * only be changed in very limited ways without breaking + * binary-compatibility. In particular, note that it returns -1 for + * all tuples that end with the `EmptyTuple` type alias instead of + * `EmptyTuple.type` because of a missing dealias, but this is now + * impossible to fix. + * + * @return The arity if it can be determined or -1 otherwise. + */ + def tupleArity(tp: Type)(using Context): Int = tp/*.dealias*/ match { + case AppliedType(tycon, _ :: tl :: Nil) if tycon.isRef(defn.PairClass) => + val arity = tupleArity(tl) + if (arity < 0) arity else arity + 1 + case tp: SingletonType => + if tp.termSymbol == defn.EmptyTupleModule then 0 else -1 + case tp: AndOrType => + val arity1 = tupleArity(tp.tp1) + val arity2 = tupleArity(tp.tp2) + if arity1 == arity2 then arity1 else -1 + case _ => + if defn.isTupleNType(tp) then tp.dealias.argInfos.length + else -1 + } + def normalizeClass(cls: ClassSymbol)(using Context): ClassSymbol = { if (cls.owner == defn.ScalaPackageClass) { if (defn.specialErasure.contains(cls)) @@ -740,9 +766,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst } private def erasePair(tp: Type)(using Context): Type = { - // NOTE: `tupleArity` does not consider TypeRef(EmptyTuple$) equivalent to EmptyTuple.type, - // we fix this for printers, but type erasure should be preserved. - val arity = tp.tupleArity + val arity = tupleArity(tp) if (arity < 0) defn.ProductClass.typeRef else if (arity <= Definitions.MaxTupleArity) defn.TupleType(arity).nn else defn.TupleXXLClass.typeRef diff --git a/compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala b/compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala index a1baeac272b9..21c212e2a28a 100644 --- a/compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala +++ b/compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala @@ -11,7 +11,7 @@ import core.Flags._ import core.Names.Name import core.Symbols._ import core.TypeApplications.{EtaExpansion, TypeParamInfo} -import core.TypeErasure.{erasedGlb, erasure, fullErasure, isGenericArrayElement} +import core.TypeErasure.{erasedGlb, erasure, fullErasure, isGenericArrayElement, tupleArity} import core.Types._ import core.classfile.ClassfileConstants import SymUtils._ @@ -255,7 +255,7 @@ object GenericSignatures { case _ => jsig(elemtp) case RefOrAppliedType(sym, pre, args) => - if (sym == defn.PairClass && tp.tupleArity > Definitions.MaxTupleArity) + if (sym == defn.PairClass && tupleArity(tp) > Definitions.MaxTupleArity) jsig(defn.TupleXXLClass.typeRef) else if (isTypeParameterInSig(sym, sym0)) { assert(!sym.isAliasType, "Unexpected alias type: " + sym) diff --git a/compiler/src/dotty/tools/dotc/transform/TypeUtils.scala b/compiler/src/dotty/tools/dotc/transform/TypeUtils.scala index a897503ef275..779552a3d46f 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeUtils.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeUtils.scala @@ -49,24 +49,6 @@ object TypeUtils { case ps => ps.reduceLeft(AndType(_, _)) } - /** The arity of this tuple type, which can be made up of EmptyTuple, TupleX and `*:` pairs, - * or -1 if this is not a tuple type. - */ - def tupleArity(using Context): Int = self/*.dealias*/ match { // TODO: why does dealias cause a failure in tests/run-deep-subtype/Tuple-toArray.scala - case AppliedType(tycon, _ :: tl :: Nil) if tycon.isRef(defn.PairClass) => - val arity = tl.tupleArity - if (arity < 0) arity else arity + 1 - case self: SingletonType => - if self.termSymbol == defn.EmptyTupleModule then 0 else -1 - case self: AndOrType => - val arity1 = self.tp1.tupleArity - val arity2 = self.tp2.tupleArity - if arity1 == arity2 then arity1 else -1 - case _ => - if defn.isTupleNType(self) then self.dealias.argInfos.length - else -1 - } - /** The element types of this tuple type, which can be made up of EmptyTuple, TupleX and `*:` pairs */ def tupleElementTypes(using Context): Option[List[Type]] = self.dealias match { case AppliedType(tycon, hd :: tl :: Nil) if tycon.isRef(defn.PairClass) => From c5b8b02a1075e78cb455b2c3c054889e74bd4c9f Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Fri, 30 Jun 2023 16:00:54 +0200 Subject: [PATCH 2/7] TypeErasure: Rename `wildcardOK` to `inSigName` Its meaning will be expanded in the next commit. [Cherry-picked 84bc1bd74449ab5edf0c7bf3c8aa5c4f641e1066] --- .../dotty/tools/dotc/core/TypeErasure.scala | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index cb44adbcaa60..7dbe22e62c96 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -169,9 +169,9 @@ object TypeErasure { } } - private def erasureIdx(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, isSymbol: Boolean, wildcardOK: Boolean) = + private def erasureIdx(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, isSymbol: Boolean, inSigName: Boolean) = extension (b: Boolean) def toInt = if b then 1 else 0 - wildcardOK.toInt + inSigName.toInt + (isSymbol.toInt << 1) + (isConstructor.toInt << 2) + (semiEraseVCs.toInt << 3) @@ -184,16 +184,16 @@ object TypeErasure { semiEraseVCs <- List(false, true) isConstructor <- List(false, true) isSymbol <- List(false, true) - wildcardOK <- List(false, true) + inSigName <- List(false, true) do - erasures(erasureIdx(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, wildcardOK)) = - new TypeErasure(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, wildcardOK) + erasures(erasureIdx(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, inSigName)) = + new TypeErasure(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, inSigName) /** Produces an erasure function. See the documentation of the class [[TypeErasure]] * for a description of each parameter. */ - private def erasureFn(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, isSymbol: Boolean, wildcardOK: Boolean): TypeErasure = - erasures(erasureIdx(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, wildcardOK)) + private def erasureFn(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, isSymbol: Boolean, inSigName: Boolean): TypeErasure = + erasures(erasureIdx(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, inSigName)) /** The current context with a phase no later than erasure */ def preErasureCtx(using Context) = @@ -204,7 +204,7 @@ object TypeErasure { * @param tp The type to erase. */ def erasure(tp: Type)(using Context): Type = - erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = false, isConstructor = false, isSymbol = false, wildcardOK = false)(tp)(using preErasureCtx) + erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = false, isConstructor = false, isSymbol = false, inSigName = false)(tp)(using preErasureCtx) /** The value class erasure of a Scala type, where value classes are semi-erased to * ErasedValueType (they will be fully erased in [[ElimErasedValueType]]). @@ -212,11 +212,11 @@ object TypeErasure { * @param tp The type to erase. */ def valueErasure(tp: Type)(using Context): Type = - erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = true, isConstructor = false, isSymbol = false, wildcardOK = false)(tp)(using preErasureCtx) + erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = true, isConstructor = false, isSymbol = false, inSigName = false)(tp)(using preErasureCtx) /** The erasure that Scala 2 would use for this type. */ def scala2Erasure(tp: Type)(using Context): Type = - erasureFn(sourceLanguage = SourceLanguage.Scala2, semiEraseVCs = true, isConstructor = false, isSymbol = false, wildcardOK = false)(tp)(using preErasureCtx) + erasureFn(sourceLanguage = SourceLanguage.Scala2, semiEraseVCs = true, isConstructor = false, isSymbol = false, inSigName = false)(tp)(using preErasureCtx) /** Like value class erasure, but value classes erase to their underlying type erasure */ def fullErasure(tp: Type)(using Context): Type = @@ -226,7 +226,7 @@ object TypeErasure { def sigName(tp: Type, sourceLanguage: SourceLanguage)(using Context): TypeName = { val normTp = tp.translateFromRepeated(toArray = sourceLanguage.isJava) - val erase = erasureFn(sourceLanguage, semiEraseVCs = !sourceLanguage.isJava, isConstructor = false, isSymbol = false, wildcardOK = true) + val erase = erasureFn(sourceLanguage, semiEraseVCs = !sourceLanguage.isJava, isConstructor = false, isSymbol = false, inSigName = true) erase.sigName(normTp)(using preErasureCtx) } @@ -256,7 +256,7 @@ object TypeErasure { def transformInfo(sym: Symbol, tp: Type)(using Context): Type = { val sourceLanguage = SourceLanguage(sym) val semiEraseVCs = !sourceLanguage.isJava // Java sees our value classes as regular classes. - val erase = erasureFn(sourceLanguage, semiEraseVCs, sym.isConstructor, isSymbol = true, wildcardOK = false) + val erase = erasureFn(sourceLanguage, semiEraseVCs, sym.isConstructor, isSymbol = true, inSigName = false) def eraseParamBounds(tp: PolyType): Type = tp.derivedLambdaType( @@ -582,10 +582,10 @@ import TypeErasure._ * If false, they are erased like normal classes. * @param isConstructor Argument forms part of the type of a constructor * @param isSymbol If true, the type being erased is the info of a symbol. - * @param wildcardOK Wildcards are acceptable (true when using the erasure - * for computing a signature name). + * @param inSigName This eraser is used for `TypeErasure.sigName`, + * see `TypeErasure#apply` for more information. */ -class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, isSymbol: Boolean, wildcardOK: Boolean) { +class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, isSymbol: Boolean, inSigName: Boolean) { /** The erasure |T| of a type T. This is: * @@ -673,7 +673,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst TypeComparer.orType(this(tp1), this(tp2), isErased = true) case tp: MethodType => def paramErasure(tpToErase: Type) = - erasureFn(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, wildcardOK)(tpToErase) + erasureFn(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, inSigName)(tpToErase) val (names, formals0) = if tp.hasErasedParams then tp.paramNames .zip(tp.paramInfos) @@ -725,7 +725,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst } case _: ErrorType | JavaArrayType(_) => tp - case tp: WildcardType if wildcardOK => + case tp: WildcardType if inSigName => tp case tp if (tp `eq` NoType) || (tp `eq` NoPrefix) => tp @@ -760,7 +760,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst val defn.ArrayOf(elemtp) = tp: @unchecked if (isGenericArrayElement(elemtp, isScala2 = sourceLanguage.isScala2)) defn.ObjectType else - try JavaArrayType(erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, wildcardOK)(elemtp)) + try JavaArrayType(erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, inSigName)(elemtp)) catch case ex: Throwable => handleRecursive("erase array type", tp.show, ex) } @@ -804,7 +804,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst val underlying = tp.select(unbox).widen.resultType // The underlying part of an ErasedValueType cannot be an ErasedValueType itself - val erase = erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, wildcardOK) + val erase = erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, inSigName) val erasedUnderlying = erase(underlying) // Ideally, we would just use `erasedUnderlying` as the erasure of `tp`, but to @@ -843,7 +843,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst // correctly (see SIP-15 and [[Erasure.Boxing.adaptToType]]), so the result type of a // constructor method should not be semi-erased. if semiEraseVCs && isConstructor && !tp.isInstanceOf[MethodOrPoly] then - erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, wildcardOK).eraseResult(tp) + erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, inSigName).eraseResult(tp) else tp match case tp: TypeRef => val sym = tp.symbol From c294aae46d7590bfe014534d902c457a1ef78cf9 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Mon, 26 Jun 2023 19:05:46 +0200 Subject: [PATCH 3/7] Fix signature computation involving nested type variables During typechecking, we sometimes need to compute the signature of a type involving uninstantiated type variables or wildcards (in particular, because signature matching is doen as part of subtype checking for methods), this is handled with `tpnme.Uninstantiated` which is handled specially in `Signature`. Before this commit, `sigName` only checked for wildcards and type variables at the top-level of the type, even though nested types can still have an impact on type erasure, in particular when they appear as part of: - an intersection - a union - an underlying type of a derived value class - the element type of an array type - an element type of a tuple (... *: X *: ...) We keep track of all these situations by returning `null` in `TypeErasure#apply` when encountering a wildcard or uninstantiated type variable, which we then propage upwards to the `sigName` call. This propagation only happens for `sigName` calls (where `inSigName` is true), otherwise we throw an assertion since erasure shouldn't normally be computed for underdefined types. [Cherry-picked c21133f1c5b39054031aed2f28a82f1e396a3f2d] --- .../dotty/tools/dotc/core/TypeErasure.scala | 116 ++++++++++++------ compiler/test/dotty/tools/SignatureTest.scala | 40 +++++- tests/pos/scala3mock.scala | 11 ++ 3 files changed, 129 insertions(+), 38 deletions(-) create mode 100644 tests/pos/scala3mock.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 7dbe22e62c96..866843814cb0 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -83,9 +83,11 @@ object TypeErasure { * `EmptyTuple.type` because of a missing dealias, but this is now * impossible to fix. * - * @return The arity if it can be determined or -1 otherwise. + * @return The arity if it can be determined, or: + * -1 if this type does not have a fixed arity + * -2 if the arity depends on an uninstantiated type variable or WildcardType. */ - def tupleArity(tp: Type)(using Context): Int = tp/*.dealias*/ match { + def tupleArity(tp: Type)(using Context): Int = tp/*.dealias*/ match case AppliedType(tycon, _ :: tl :: Nil) if tycon.isRef(defn.PairClass) => val arity = tupleArity(tl) if (arity < 0) arity else arity + 1 @@ -94,11 +96,14 @@ object TypeErasure { case tp: AndOrType => val arity1 = tupleArity(tp.tp1) val arity2 = tupleArity(tp.tp2) - if arity1 == arity2 then arity1 else -1 + if arity1 == arity2 then arity1 else math.min(-1, math.min(arity1, arity2)) + case tp: WildcardType => -2 + case tp: TypeVar if !tp.isInstantiated => -2 case _ => if defn.isTupleNType(tp) then tp.dealias.argInfos.length - else -1 - } + else tp.dealias match + case tp: TypeVar if !tp.isInstantiated => -2 + case _ => -1 def normalizeClass(cls: ClassSymbol)(using Context): ClassSymbol = { if (cls.owner == defn.ScalaPackageClass) { @@ -204,7 +209,7 @@ object TypeErasure { * @param tp The type to erase. */ def erasure(tp: Type)(using Context): Type = - erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = false, isConstructor = false, isSymbol = false, inSigName = false)(tp)(using preErasureCtx) + erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = false, isConstructor = false, isSymbol = false, inSigName = false)(tp)(using preErasureCtx).nn /** The value class erasure of a Scala type, where value classes are semi-erased to * ErasedValueType (they will be fully erased in [[ElimErasedValueType]]). @@ -212,11 +217,11 @@ object TypeErasure { * @param tp The type to erase. */ def valueErasure(tp: Type)(using Context): Type = - erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = true, isConstructor = false, isSymbol = false, inSigName = false)(tp)(using preErasureCtx) + erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = true, isConstructor = false, isSymbol = false, inSigName = false)(tp)(using preErasureCtx).nn /** The erasure that Scala 2 would use for this type. */ def scala2Erasure(tp: Type)(using Context): Type = - erasureFn(sourceLanguage = SourceLanguage.Scala2, semiEraseVCs = true, isConstructor = false, isSymbol = false, inSigName = false)(tp)(using preErasureCtx) + erasureFn(sourceLanguage = SourceLanguage.Scala2, semiEraseVCs = true, isConstructor = false, isSymbol = false, inSigName = false)(tp)(using preErasureCtx).nn /** Like value class erasure, but value classes erase to their underlying type erasure */ def fullErasure(tp: Type)(using Context): Type = @@ -265,8 +270,8 @@ object TypeErasure { if (defn.isPolymorphicAfterErasure(sym)) eraseParamBounds(sym.info.asInstanceOf[PolyType]) else if (sym.isAbstractType) TypeAlias(WildcardType) else if sym.is(ConstructorProxy) then NoType - else if (sym.isConstructor) outer.addParam(sym.owner.asClass, erase(tp)(using preErasureCtx)) - else if (sym.is(Label)) erase.eraseResult(sym.info)(using preErasureCtx) + else if (sym.isConstructor) outer.addParam(sym.owner.asClass, erase(tp)(using preErasureCtx).nn) + else if (sym.is(Label)) erase.eraseResult(sym.info)(using preErasureCtx).nn else erase.eraseInfo(tp, sym)(using preErasureCtx) match { case einfo: MethodType => if (sym.isGetter && einfo.resultType.isRef(defn.UnitClass)) @@ -587,8 +592,14 @@ import TypeErasure._ */ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConstructor: Boolean, isSymbol: Boolean, inSigName: Boolean) { - /** The erasure |T| of a type T. This is: + /** The erasure |T| of a type T. + * + * If computing the erasure of T requires erasing a WildcardType or an + * uninstantiated type variable, then an exception signaling an internal + * error will be thrown, unless `inSigName` is set in which case `null` + * will be returned. * + * In all other situations, |T| will be non-null and computed as follow: * - For a refined type scala.Array+[T]: * - if T is Nothing or Null, []Object * - otherwise, if T <: Object, []|T| @@ -620,7 +631,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst * - For NoType or NoPrefix, the type itself. * - For any other type, exception. */ - private def apply(tp: Type)(using Context): Type = tp match { + private def apply(tp: Type)(using Context): Type | Null = (tp match case _: ErasedValueType => tp case tp: TypeRef => @@ -641,13 +652,19 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst case _: ThisType => this(tp.widen) case SuperType(thistpe, supertpe) => - SuperType(this(thistpe), this(supertpe)) + val eThis = this(thistpe) + val eSuper = this(supertpe) + if eThis == null || eSuper == null then null + else SuperType(eThis, eSuper) case ExprType(rt) => defn.FunctionType(0) case RefinedType(parent, nme.apply, refinedInfo) if parent.typeSymbol eq defn.PolyFunctionClass => erasePolyFunctionApply(refinedInfo) case RefinedType(parent, nme.apply, refinedInfo: MethodType) if defn.isErasedFunctionType(parent) => eraseErasedFunctionApply(refinedInfo) + case tp: TypeVar if !tp.isInstantiated => + assert(inSigName, i"Cannot erase uninstantiated type variable $tp") + null case tp: TypeProxy => this(tp.underlying) case tp @ AndType(tp1, tp2) => @@ -656,7 +673,10 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst else if sourceLanguage.isScala2 then this(Scala2Erasure.intersectionDominator(Scala2Erasure.flattenedParents(tp))) else - erasedGlb(this(tp1), this(tp2)) + val e1 = this(tp1) + val e2 = this(tp2) + if e1 == null || e2 == null then null + else erasedGlb(e1, e2) case OrType(tp1, tp2) => if isSymbol && sourceLanguage.isScala2 && ctx.settings.scalajs.value then // In Scala2Unpickler we unpickle Scala.js pseudo-unions as if they were @@ -670,10 +690,13 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst // alone (and this doesn't impact the SJSIR we generate). JSDefinitions.jsdefn.PseudoUnionType else - TypeComparer.orType(this(tp1), this(tp2), isErased = true) + val e1 = this(tp1) + val e2 = this(tp2) + if e1 == null || e2 == null then null + else TypeComparer.orType(e1, e2, isErased = true) case tp: MethodType => def paramErasure(tpToErase: Type) = - erasureFn(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, inSigName)(tpToErase) + erasureFn(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, inSigName = false)(tpToErase).nn val (names, formals0) = if tp.hasErasedParams then tp.paramNames .zip(tp.paramInfos) @@ -700,7 +723,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst else { def eraseParent(tp: Type) = tp.dealias match { // note: can't be opaque, since it's a class parent case tp: AppliedType if tp.tycon.isRef(defn.PairClass) => defn.ObjectType - case _ => apply(tp) + case _ => apply(tp).nn } val erasedParents: List[Type] = if ((cls eq defn.ObjectClass) || cls.isPrimitiveValueClass) Nil @@ -725,11 +748,12 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst } case _: ErrorType | JavaArrayType(_) => tp - case tp: WildcardType if inSigName => - tp + case tp: WildcardType => + assert(inSigName, i"Cannot erase wildcard type $tp") + null case tp if (tp `eq` NoType) || (tp `eq` NoPrefix) => tp - } + ).ensuring(etp => etp != null || inSigName) /** Like translucentSuperType, but issue a fatal error if it does not exist. */ private def checkedSuperType(tp: TypeProxy)(using Context): Type = @@ -760,15 +784,19 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst val defn.ArrayOf(elemtp) = tp: @unchecked if (isGenericArrayElement(elemtp, isScala2 = sourceLanguage.isScala2)) defn.ObjectType else - try JavaArrayType(erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, inSigName)(elemtp)) + try + val eElem = erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, inSigName)(elemtp) + if eElem == null then null + else JavaArrayType(eElem) catch case ex: Throwable => handleRecursive("erase array type", tp.show, ex) } - private def erasePair(tp: Type)(using Context): Type = { + private def erasePair(tp: Type)(using Context): Type | Null = { val arity = tupleArity(tp) - if (arity < 0) defn.ProductClass.typeRef - else if (arity <= Definitions.MaxTupleArity) defn.TupleType(arity).nn + if arity == -2 then null // erasure depends on an uninstantiated type variable or WildcardType + else if arity == -1 then defn.ProductClass.typeRef + else if arity <= Definitions.MaxTupleArity then defn.TupleType(arity).nn else defn.TupleXXLClass.typeRef } @@ -777,12 +805,13 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst * to the underlying type. */ def eraseInfo(tp: Type, sym: Symbol)(using Context): Type = + assert(!inSigName) // therefore apply(...).nn won't fail val tp1 = tp match case tp: MethodicType => integrateContextResults(tp, contextResultCount(sym)) case _ => tp tp1 match case ExprType(rt) => - if sym.is(Param) then apply(tp1) + if sym.is(Param) then apply(tp1).nn // Note that params with ExprTypes are eliminated by ElimByName, // but potentially re-introduced by ResolveSuper, when we add // forwarders to mixin methods. @@ -794,9 +823,9 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst eraseResult(tp1.resultType) match case rt: MethodType => rt case rt => MethodType(Nil, Nil, rt) - case tp1 => this(tp1) + case tp1 => this(tp1).nn - private def eraseDerivedValueClass(tp: Type)(using Context): Type = { + private def eraseDerivedValueClass(tp: Type)(using Context): Type | Null = { val cls = tp.classSymbol.asClass val unbox = valueClassUnbox(cls) if unbox.exists then @@ -806,6 +835,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst // The underlying part of an ErasedValueType cannot be an ErasedValueType itself val erase = erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, inSigName) val erasedUnderlying = erase(underlying) + if erasedUnderlying == null then return null // Ideally, we would just use `erasedUnderlying` as the erasure of `tp`, but to // be binary-compatible with Scala 2 we need two special cases for polymorphic @@ -839,6 +869,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst /** The erasure of a function result type. */ def eraseResult(tp: Type)(using Context): Type = + assert(!inSigName) // therefore apply(...).nn won't fail // For a value class V, "new V(x)" should have type V for type adaptation to work // correctly (see SIP-15 and [[Erasure.Boxing.adaptToType]]), so the result type of a // constructor method should not be semi-erased. @@ -848,18 +879,25 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst case tp: TypeRef => val sym = tp.symbol if (sym eq defn.UnitClass) sym.typeRef - else this(tp) + else apply(tp).nn case tp: AppliedType => val sym = tp.tycon.typeSymbol if (sym.isClass && !erasureDependsOnArgs(sym)) eraseResult(tp.tycon) - else this(tp) + else apply(tp).nn case _ => - this(tp) + apply(tp).nn /** The name of the type as it is used in `Signature`s. - * Need to ensure correspondence with erasure! + * + * If `tp` is null, or if computing its erasure requires erasing a + * WildcardType or an uninstantiated type variable, then the special name + * `tpnme.Uninstantiated` which is used to signal an underdefined signature + * is used. + * + * Note: Need to ensure correspondence with erasure! */ - private def sigName(tp: Type)(using Context): TypeName = try + private def sigName(tp: Type | Null)(using Context): TypeName = try + if tp == null then return tpnme.Uninstantiated tp match { case tp: TypeRef => if (!tp.denot.exists) @@ -873,6 +911,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst } if (semiEraseVCs && isDerivedValueClass(sym)) { val erasedVCRef = eraseDerivedValueClass(tp) + if erasedVCRef == null then return tpnme.Uninstantiated if (erasedVCRef.exists) return sigName(erasedVCRef) } if (defn.isSyntheticFunctionClass(sym)) @@ -897,14 +936,15 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst case ErasedValueType(_, underlying) => sigName(underlying) case JavaArrayType(elem) => - sigName(elem) ++ "[]" + val elemName = sigName(elem) + if elemName eq tpnme.Uninstantiated then elemName + else elemName ++ "[]" case tp: TermRef => sigName(underlyingOfTermRef(tp)) case ExprType(rt) => sigName(defn.FunctionOf(Nil, rt)) - case tp: TypeVar => - val inst = tp.instanceOpt - if (inst.exists) sigName(inst) else tpnme.Uninstantiated + case tp: TypeVar if !tp.isInstantiated => + tpnme.Uninstantiated case tp @ RefinedType(parent, nme.apply, _) if parent.typeSymbol eq defn.PolyFunctionClass => // we need this case rather than falling through to the default // because RefinedTypes <: TypeProxy and it would be caught by @@ -916,7 +956,9 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst sigName(tp.underlying) case tp: WildcardType => tpnme.Uninstantiated - case _: ErrorType | NoType => + case tp: ErrorType => + tpnme.ERROR + case _ if tp eq NoType => // Can't write `case NoType` because of #18083. tpnme.ERROR case _ => val erasedTp = this(tp) diff --git a/compiler/test/dotty/tools/SignatureTest.scala b/compiler/test/dotty/tools/SignatureTest.scala index 43d517417108..ee2130ba4bbf 100644 --- a/compiler/test/dotty/tools/SignatureTest.scala +++ b/compiler/test/dotty/tools/SignatureTest.scala @@ -4,12 +4,19 @@ import vulpix.TestConfiguration import org.junit.Test -import dotc.ast.Trees._ +import dotc.ast.untpd import dotc.core.Decorators._ import dotc.core.Contexts._ +import dotc.core.Flags._ import dotc.core.Phases._ import dotc.core.Types._ import dotc.core.Symbols._ +import dotc.core.StdNames._ +import dotc.core.Signature +import dotc.typer.ProtoTypes.constrained +import dotc.typer.Inferencing.isFullyDefined +import dotc.typer.ForceDegree +import dotc.util.NoSourcePosition import java.io.File import java.nio.file._ @@ -38,3 +45,34 @@ class SignatureTest: |${ref.denot.signature}""".stripMargin) } } + + /** Ensure that signature computation returns an underdefined signature when + * the signature depends on uninstantiated type variables. + */ + @Test def underdefined: Unit = + inCompilerContext(TestConfiguration.basicClasspath, separateRun = false, + """trait Foo + |trait Bar + |class A[T <: Tuple]: + | def and(x: T & Foo): Unit = {} + | def andor(x: (T | Bar) & Foo): Unit = {} + | def array(x: Array[(T | Bar) & Foo]): Unit = {} + | def tuple(x: Foo *: T): Unit = {} + | def tuple2(x: Foo *: (T | Tuple) & Foo): Unit = {} + |""".stripMargin): + val cls = requiredClass("A") + val tvar = constrained(cls.requiredMethod(nme.CONSTRUCTOR).info.asInstanceOf[TypeLambda], untpd.EmptyTree, alwaysAddTypeVars = true)._2.head.tpe + tvar <:< defn.TupleTypeRef + val prefix = cls.typeRef.appliedTo(tvar) + + def checkSignatures(expectedIsUnderDefined: Boolean)(using Context): Unit = + for decl <- cls.info.decls.toList if decl.is(Method) && !decl.isConstructor do + val meth = decl.asSeenFrom(prefix) + val sig = meth.info.signature + val what = if expectedIsUnderDefined then "underdefined" else "fully-defined" + assert(sig.isUnderDefined == expectedIsUnderDefined, i"Signature of `$meth` with prefix `$prefix` and type `${meth.info}` should be $what but is `$sig`") + + checkSignatures(expectedIsUnderDefined = true) + assert(isFullyDefined(tvar, force = ForceDegree.all), s"Could not instantiate $tvar") + checkSignatures(expectedIsUnderDefined = false) + diff --git a/tests/pos/scala3mock.scala b/tests/pos/scala3mock.scala new file mode 100644 index 000000000000..73f25701d1c2 --- /dev/null +++ b/tests/pos/scala3mock.scala @@ -0,0 +1,11 @@ +class MockFunction1[T1]: + def expects(v1: T1 | Foo): Any = ??? + def expects(matcher: String): Any = ??? + +def when[T1](f: T1 => Any): MockFunction1[T1] = ??? + +class Foo + +def main = + val f: Foo = new Foo + when((x: Foo) => "").expects(f) From b8e876c1eb5474f9e55bb4fb13289770a0421820 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Mon, 26 Jun 2023 19:05:46 +0200 Subject: [PATCH 4/7] Fix signature caching involving type variables Signatures should not be cached if they may change. The existing `isUnderDefined` check is not enough to guarantee this because the signature might have been computed based on a type variable instantiated in a TyperState which was subsequently retracted. To guard against this, we now rely on `Type#isProvisional`. This is a stricter check than needed since the provisional part of the type does not always have an impact on its signature, but in practice this is fine: when compiling Dotty, signature cache misses increased by less than 2%. [Cherry-picked 3729960382bf544f6e015dc1e36811dcbcdff19f] --- .../src/dotty/tools/dotc/core/Types.scala | 8 ++-- compiler/test/dotty/tools/SignatureTest.scala | 37 +++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 5d43ebd25319..d044c8672422 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -2260,7 +2260,7 @@ object Types { if ctx.runId != mySignatureRunId then mySignature = computeSignature - if !mySignature.isUnderDefined then mySignatureRunId = ctx.runId + if !mySignature.isUnderDefined && !isProvisional then mySignatureRunId = ctx.runId mySignature end signature @@ -3767,17 +3767,17 @@ object Types { case SourceLanguage.Java => if ctx.runId != myJavaSignatureRunId then myJavaSignature = computeSignature - if !myJavaSignature.isUnderDefined then myJavaSignatureRunId = ctx.runId + if !myJavaSignature.isUnderDefined && !isProvisional then myJavaSignatureRunId = ctx.runId myJavaSignature case SourceLanguage.Scala2 => if ctx.runId != myScala2SignatureRunId then myScala2Signature = computeSignature - if !myScala2Signature.isUnderDefined then myScala2SignatureRunId = ctx.runId + if !myScala2Signature.isUnderDefined && !isProvisional then myScala2SignatureRunId = ctx.runId myScala2Signature case SourceLanguage.Scala3 => if ctx.runId != mySignatureRunId then mySignature = computeSignature - if !mySignature.isUnderDefined then mySignatureRunId = ctx.runId + if !mySignature.isUnderDefined && !isProvisional then mySignatureRunId = ctx.runId mySignature end signature diff --git a/compiler/test/dotty/tools/SignatureTest.scala b/compiler/test/dotty/tools/SignatureTest.scala index ee2130ba4bbf..fdbb1e8f3760 100644 --- a/compiler/test/dotty/tools/SignatureTest.scala +++ b/compiler/test/dotty/tools/SignatureTest.scala @@ -2,6 +2,7 @@ package dotty.tools import vulpix.TestConfiguration +import org.junit.Assert._ import org.junit.Test import dotc.ast.untpd @@ -9,6 +10,7 @@ import dotc.core.Decorators._ import dotc.core.Contexts._ import dotc.core.Flags._ import dotc.core.Phases._ +import dotc.core.Names._ import dotc.core.Types._ import dotc.core.Symbols._ import dotc.core.StdNames._ @@ -76,3 +78,38 @@ class SignatureTest: assert(isFullyDefined(tvar, force = ForceDegree.all), s"Could not instantiate $tvar") checkSignatures(expectedIsUnderDefined = false) + /** Check that signature caching behaves correctly with respect to retracted + * instantiations of type variables. + */ + @Test def cachingWithRetraction: Unit = + inCompilerContext(TestConfiguration.basicClasspath, separateRun = false, + """trait Foo + |trait Bar + |class A[T]: + | def and(x: T & Foo): Unit = {} + |""".stripMargin): + val cls = requiredClass("A") + val tvar = constrained(cls.requiredMethod(nme.CONSTRUCTOR).info.asInstanceOf[TypeLambda], untpd.EmptyTree, alwaysAddTypeVars = true)._2.head.tpe + val prefix = cls.typeRef.appliedTo(tvar) + val ref = prefix.select(cls.requiredMethod("and")).asInstanceOf[TermRef] + + /** Check that the signature of the first parameter of `ref` is equal to `expectedParamSig`. */ + def checkParamSig(ref: TermRef, expectedParamSig: TypeName)(using Context): Unit = + assertEquals(i"Check failed for param signature of $ref", + expectedParamSig, ref.signature.paramsSig.head) + // Both NamedType and MethodOrPoly cache signatures, so check both caches. + assertEquals(i"Check failed for param signature of ${ref.info} (but not for $ref itself)", + expectedParamSig, ref.info.signature.paramsSig.head) + + + // Initially, the param signature is Uninstantiated since it depends on an uninstantiated type variable + checkParamSig(ref, tpnme.Uninstantiated) + + // In this context, the signature is the erasure of `Bar & Foo`. + inContext(ctx.fresh.setNewTyperState()): + tvar =:= requiredClass("Bar").typeRef + assert(isFullyDefined(tvar, force = ForceDegree.all), s"Could not instantiate $tvar") + checkParamSig(ref, "Bar".toTypeName) + + // If our caching logic is working correctly, we should get the original signature here. + checkParamSig(ref, tpnme.Uninstantiated) From 0bcff78903eab6663ed731949da3495c10127824 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Wed, 28 Jun 2023 18:44:16 +0200 Subject: [PATCH 5/7] Add FIXMEs regarding TypeVar#resetInst breaking our caching logic I'm afraid we'll have to get rid of it if we want our caches to be correct, but I don't have the time to look into it right now. [Cherry-picked 7de3663486e82dcbbab3027690e7c005c9b300c1] --- compiler/src/dotty/tools/dotc/core/Types.scala | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index d044c8672422..15952db369ae 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -109,6 +109,11 @@ object Types { /** Is this type still provisional? This is the case if the type contains, or depends on, * uninstantiated type variables or type symbols that have the Provisional flag set. * This is an antimonotonic property - once a type is not provisional, it stays so forever. + * + * FIXME: The semantics of this flag are broken by the existence of `TypeVar#resetInst`, + * a non-provisional type could go back to being provisional after + * a call to `resetInst`. This means all caches that rely on `isProvisional` + * can likely end up returning stale results. */ def isProvisional(using Context): Boolean = mightBeProvisional && testProvisional @@ -4743,6 +4748,10 @@ object Types { * is different from the variable's creation state (meaning unrolls are possible) * in the current typer state. * + * FIXME: the "once" in the statement above is not true anymore now that `resetInst` + * exists, this is problematic for caching (see `Type#isProvisional`), + * we should try getting rid of this method. + * * @param origin the parameter that's tracked by the type variable. * @param creatorState the typer state in which the variable was created. * @param initNestingLevel the initial nesting level of the type variable. (c.f. nestingLevel) From b9f0e2a6a0905da286353f06bbf2911d6c736cc6 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Wed, 5 Jul 2023 15:56:20 +0200 Subject: [PATCH 6/7] TypeErasure#apply: replace ensuring by inlinable assert for performance [Cherry-picked 2666e70b219bea8788c98804d73bf055062afb29] --- .../dotty/tools/dotc/core/TypeErasure.scala | 244 +++++++++--------- 1 file changed, 123 insertions(+), 121 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 866843814cb0..2cadd7c59442 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -631,129 +631,131 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst * - For NoType or NoPrefix, the type itself. * - For any other type, exception. */ - private def apply(tp: Type)(using Context): Type | Null = (tp match - case _: ErasedValueType => - tp - case tp: TypeRef => - val sym = tp.symbol - if !sym.isClass then this(checkedSuperType(tp)) - else if semiEraseVCs && isDerivedValueClass(sym) then eraseDerivedValueClass(tp) - else if defn.isSyntheticFunctionClass(sym) then defn.functionTypeErasure(sym) - else eraseNormalClassRef(tp) - case tp: AppliedType => - val tycon = tp.tycon - if (tycon.isRef(defn.ArrayClass)) eraseArray(tp) - else if (tycon.isRef(defn.PairClass)) erasePair(tp) - else if (tp.isRepeatedParam) apply(tp.translateFromRepeated(toArray = sourceLanguage.isJava)) - else if (semiEraseVCs && isDerivedValueClass(tycon.classSymbol)) eraseDerivedValueClass(tp) - else this(checkedSuperType(tp)) - case tp: TermRef => - this(underlyingOfTermRef(tp)) - case _: ThisType => - this(tp.widen) - case SuperType(thistpe, supertpe) => - val eThis = this(thistpe) - val eSuper = this(supertpe) - if eThis == null || eSuper == null then null - else SuperType(eThis, eSuper) - case ExprType(rt) => - defn.FunctionType(0) - case RefinedType(parent, nme.apply, refinedInfo) if parent.typeSymbol eq defn.PolyFunctionClass => - erasePolyFunctionApply(refinedInfo) - case RefinedType(parent, nme.apply, refinedInfo: MethodType) if defn.isErasedFunctionType(parent) => - eraseErasedFunctionApply(refinedInfo) - case tp: TypeVar if !tp.isInstantiated => - assert(inSigName, i"Cannot erase uninstantiated type variable $tp") - null - case tp: TypeProxy => - this(tp.underlying) - case tp @ AndType(tp1, tp2) => - if sourceLanguage.isJava then - this(tp1) - else if sourceLanguage.isScala2 then - this(Scala2Erasure.intersectionDominator(Scala2Erasure.flattenedParents(tp))) - else - val e1 = this(tp1) - val e2 = this(tp2) - if e1 == null || e2 == null then null - else erasedGlb(e1, e2) - case OrType(tp1, tp2) => - if isSymbol && sourceLanguage.isScala2 && ctx.settings.scalajs.value then - // In Scala2Unpickler we unpickle Scala.js pseudo-unions as if they were - // real unions, but we must still erase them as Scala 2 would to emit - // the correct signatures in SJSIR. - // We only do this when `isSymbol` is true since in other situations we - // cannot distinguish a Scala.js pseudo-union from a Scala 3 union that - // has been substituted into a Scala 2 type (e.g., via `asSeenFrom`), - // erasing these unions as if they were pseudo-unions could have an - // impact on overriding relationships so it's best to leave them - // alone (and this doesn't impact the SJSIR we generate). - JSDefinitions.jsdefn.PseudoUnionType - else - val e1 = this(tp1) - val e2 = this(tp2) - if e1 == null || e2 == null then null - else TypeComparer.orType(e1, e2, isErased = true) - case tp: MethodType => - def paramErasure(tpToErase: Type) = - erasureFn(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, inSigName = false)(tpToErase).nn - val (names, formals0) = if tp.hasErasedParams then - tp.paramNames - .zip(tp.paramInfos) - .zip(tp.erasedParams) - .collect{ case (param, isErased) if !isErased => param } - .unzip - else (tp.paramNames, tp.paramInfos) - val formals = formals0.mapConserve(paramErasure) - eraseResult(tp.resultType) match { - case rt: MethodType => - tp.derivedLambdaType(names ++ rt.paramNames, formals ++ rt.paramInfos, rt.resultType) - case NoType => - // Can happen if we smuggle in a Nothing in the qualifier. Normally we prevent that - // in Checking.checkMembersOK, but compiler-generated code can bypass this test. - // See i15377.scala for a test case. - NoType - case rt => - tp.derivedLambdaType(names, formals, rt) - } - case tp: PolyType => - this(tp.resultType) - case tp @ ClassInfo(pre, cls, parents, decls, _) => - if (cls.is(Package)) tp - else { - def eraseParent(tp: Type) = tp.dealias match { // note: can't be opaque, since it's a class parent - case tp: AppliedType if tp.tycon.isRef(defn.PairClass) => defn.ObjectType - case _ => apply(tp).nn + private def apply(tp: Type)(using Context): Type | Null = + val etp = tp match + case _: ErasedValueType => + tp + case tp: TypeRef => + val sym = tp.symbol + if !sym.isClass then this(checkedSuperType(tp)) + else if semiEraseVCs && isDerivedValueClass(sym) then eraseDerivedValueClass(tp) + else if defn.isSyntheticFunctionClass(sym) then defn.functionTypeErasure(sym) + else eraseNormalClassRef(tp) + case tp: AppliedType => + val tycon = tp.tycon + if (tycon.isRef(defn.ArrayClass)) eraseArray(tp) + else if (tycon.isRef(defn.PairClass)) erasePair(tp) + else if (tp.isRepeatedParam) apply(tp.translateFromRepeated(toArray = sourceLanguage.isJava)) + else if (semiEraseVCs && isDerivedValueClass(tycon.classSymbol)) eraseDerivedValueClass(tp) + else this(checkedSuperType(tp)) + case tp: TermRef => + this(underlyingOfTermRef(tp)) + case _: ThisType => + this(tp.widen) + case SuperType(thistpe, supertpe) => + val eThis = this(thistpe) + val eSuper = this(supertpe) + if eThis == null || eSuper == null then null + else SuperType(eThis, eSuper) + case ExprType(rt) => + defn.FunctionType(0) + case RefinedType(parent, nme.apply, refinedInfo) if parent.typeSymbol eq defn.PolyFunctionClass => + erasePolyFunctionApply(refinedInfo) + case RefinedType(parent, nme.apply, refinedInfo: MethodType) if defn.isErasedFunctionType(parent) => + eraseErasedFunctionApply(refinedInfo) + case tp: TypeVar if !tp.isInstantiated => + assert(inSigName, i"Cannot erase uninstantiated type variable $tp") + null + case tp: TypeProxy => + this(tp.underlying) + case tp @ AndType(tp1, tp2) => + if sourceLanguage.isJava then + this(tp1) + else if sourceLanguage.isScala2 then + this(Scala2Erasure.intersectionDominator(Scala2Erasure.flattenedParents(tp))) + else + val e1 = this(tp1) + val e2 = this(tp2) + if e1 == null || e2 == null then null + else erasedGlb(e1, e2) + case OrType(tp1, tp2) => + if isSymbol && sourceLanguage.isScala2 && ctx.settings.scalajs.value then + // In Scala2Unpickler we unpickle Scala.js pseudo-unions as if they were + // real unions, but we must still erase them as Scala 2 would to emit + // the correct signatures in SJSIR. + // We only do this when `isSymbol` is true since in other situations we + // cannot distinguish a Scala.js pseudo-union from a Scala 3 union that + // has been substituted into a Scala 2 type (e.g., via `asSeenFrom`), + // erasing these unions as if they were pseudo-unions could have an + // impact on overriding relationships so it's best to leave them + // alone (and this doesn't impact the SJSIR we generate). + JSDefinitions.jsdefn.PseudoUnionType + else + val e1 = this(tp1) + val e2 = this(tp2) + if e1 == null || e2 == null then null + else TypeComparer.orType(e1, e2, isErased = true) + case tp: MethodType => + def paramErasure(tpToErase: Type) = + erasureFn(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, inSigName = false)(tpToErase).nn + val (names, formals0) = if tp.hasErasedParams then + tp.paramNames + .zip(tp.paramInfos) + .zip(tp.erasedParams) + .collect{ case (param, isErased) if !isErased => param } + .unzip + else (tp.paramNames, tp.paramInfos) + val formals = formals0.mapConserve(paramErasure) + eraseResult(tp.resultType) match { + case rt: MethodType => + tp.derivedLambdaType(names ++ rt.paramNames, formals ++ rt.paramInfos, rt.resultType) + case NoType => + // Can happen if we smuggle in a Nothing in the qualifier. Normally we prevent that + // in Checking.checkMembersOK, but compiler-generated code can bypass this test. + // See i15377.scala for a test case. + NoType + case rt => + tp.derivedLambdaType(names, formals, rt) } - val erasedParents: List[Type] = - if ((cls eq defn.ObjectClass) || cls.isPrimitiveValueClass) Nil - else parents.mapConserve(eraseParent) match { - case tr :: trs1 => - assert(!tr.classSymbol.is(Trait), i"$cls has bad parents $parents%, %") - val tr1 = if (cls.is(Trait)) defn.ObjectType else tr - tr1 :: trs1.filterNot(_.isAnyRef) - case nil => nil + case tp: PolyType => + this(tp.resultType) + case tp @ ClassInfo(pre, cls, parents, decls, _) => + if (cls.is(Package)) tp + else { + def eraseParent(tp: Type) = tp.dealias match { // note: can't be opaque, since it's a class parent + case tp: AppliedType if tp.tycon.isRef(defn.PairClass) => defn.ObjectType + case _ => apply(tp).nn } - var erasedDecls = decls.filteredScope(sym => !sym.isType || sym.isClass).openForMutations - for dcl <- erasedDecls.iterator do - if dcl.lastKnownDenotation.unforcedAnnotation(defn.TargetNameAnnot).isDefined - && dcl.targetName != dcl.name - then - if erasedDecls eq decls then erasedDecls = erasedDecls.cloneScope - erasedDecls.unlink(dcl) - erasedDecls.enter(dcl.targetName, dcl) - val selfType1 = if cls.is(Module) then cls.sourceModule.termRef else NoType - tp.derivedClassInfo(NoPrefix, erasedParents, erasedDecls, selfType1) - // can't replace selftype by NoType because this would lose the sourceModule link - } - case _: ErrorType | JavaArrayType(_) => - tp - case tp: WildcardType => - assert(inSigName, i"Cannot erase wildcard type $tp") - null - case tp if (tp `eq` NoType) || (tp `eq` NoPrefix) => - tp - ).ensuring(etp => etp != null || inSigName) + val erasedParents: List[Type] = + if ((cls eq defn.ObjectClass) || cls.isPrimitiveValueClass) Nil + else parents.mapConserve(eraseParent) match { + case tr :: trs1 => + assert(!tr.classSymbol.is(Trait), i"$cls has bad parents $parents%, %") + val tr1 = if (cls.is(Trait)) defn.ObjectType else tr + tr1 :: trs1.filterNot(_.isAnyRef) + case nil => nil + } + var erasedDecls = decls.filteredScope(sym => !sym.isType || sym.isClass).openForMutations + for dcl <- erasedDecls.iterator do + if dcl.lastKnownDenotation.unforcedAnnotation(defn.TargetNameAnnot).isDefined + && dcl.targetName != dcl.name + then + if erasedDecls eq decls then erasedDecls = erasedDecls.cloneScope + erasedDecls.unlink(dcl) + erasedDecls.enter(dcl.targetName, dcl) + val selfType1 = if cls.is(Module) then cls.sourceModule.termRef else NoType + tp.derivedClassInfo(NoPrefix, erasedParents, erasedDecls, selfType1) + // can't replace selftype by NoType because this would lose the sourceModule link + } + case _: ErrorType | JavaArrayType(_) => + tp + case tp: WildcardType => + assert(inSigName, i"Cannot erase wildcard type $tp") + null + case tp if (tp `eq` NoType) || (tp `eq` NoPrefix) => + tp + assert(etp != null || inSigName, i"Unexpected null erasure for $tp") + etp /** Like translucentSuperType, but issue a fatal error if it does not exist. */ private def checkedSuperType(tp: TypeProxy)(using Context): Type = From 5aadde8a659b385a3cb68c1b6e52cf93b353e577 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Wed, 5 Jul 2023 16:01:53 +0200 Subject: [PATCH 7/7] TypeErasure#apply: use WildcardType instead of null As requested in the review, this is slightly shorter but means we have to be very careful to not accidentally forget to propagate a WildcardType to the top-level when computing signatures. [Cherry-picked 4499bc04a2c4fcd9c72e70ed85792a6264b92e66] --- .../dotty/tools/dotc/core/TypeErasure.scala | 64 +++++++++---------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 2cadd7c59442..eabb0ed1a17e 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -209,7 +209,7 @@ object TypeErasure { * @param tp The type to erase. */ def erasure(tp: Type)(using Context): Type = - erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = false, isConstructor = false, isSymbol = false, inSigName = false)(tp)(using preErasureCtx).nn + erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = false, isConstructor = false, isSymbol = false, inSigName = false)(tp)(using preErasureCtx) /** The value class erasure of a Scala type, where value classes are semi-erased to * ErasedValueType (they will be fully erased in [[ElimErasedValueType]]). @@ -217,11 +217,11 @@ object TypeErasure { * @param tp The type to erase. */ def valueErasure(tp: Type)(using Context): Type = - erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = true, isConstructor = false, isSymbol = false, inSigName = false)(tp)(using preErasureCtx).nn + erasureFn(sourceLanguage = SourceLanguage.Scala3, semiEraseVCs = true, isConstructor = false, isSymbol = false, inSigName = false)(tp)(using preErasureCtx) /** The erasure that Scala 2 would use for this type. */ def scala2Erasure(tp: Type)(using Context): Type = - erasureFn(sourceLanguage = SourceLanguage.Scala2, semiEraseVCs = true, isConstructor = false, isSymbol = false, inSigName = false)(tp)(using preErasureCtx).nn + erasureFn(sourceLanguage = SourceLanguage.Scala2, semiEraseVCs = true, isConstructor = false, isSymbol = false, inSigName = false)(tp)(using preErasureCtx) /** Like value class erasure, but value classes erase to their underlying type erasure */ def fullErasure(tp: Type)(using Context): Type = @@ -270,8 +270,8 @@ object TypeErasure { if (defn.isPolymorphicAfterErasure(sym)) eraseParamBounds(sym.info.asInstanceOf[PolyType]) else if (sym.isAbstractType) TypeAlias(WildcardType) else if sym.is(ConstructorProxy) then NoType - else if (sym.isConstructor) outer.addParam(sym.owner.asClass, erase(tp)(using preErasureCtx).nn) - else if (sym.is(Label)) erase.eraseResult(sym.info)(using preErasureCtx).nn + else if (sym.isConstructor) outer.addParam(sym.owner.asClass, erase(tp)(using preErasureCtx)) + else if (sym.is(Label)) erase.eraseResult(sym.info)(using preErasureCtx) else erase.eraseInfo(tp, sym)(using preErasureCtx) match { case einfo: MethodType => if (sym.isGetter && einfo.resultType.isRef(defn.UnitClass)) @@ -596,10 +596,10 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst * * If computing the erasure of T requires erasing a WildcardType or an * uninstantiated type variable, then an exception signaling an internal - * error will be thrown, unless `inSigName` is set in which case `null` + * error will be thrown, unless `inSigName` is set in which case WildcardType * will be returned. * - * In all other situations, |T| will be non-null and computed as follow: + * In all other situations, |T| will be computed as follow: * - For a refined type scala.Array+[T]: * - if T is Nothing or Null, []Object * - otherwise, if T <: Object, []|T| @@ -631,7 +631,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst * - For NoType or NoPrefix, the type itself. * - For any other type, exception. */ - private def apply(tp: Type)(using Context): Type | Null = + private def apply(tp: Type)(using Context): Type = val etp = tp match case _: ErasedValueType => tp @@ -655,7 +655,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst case SuperType(thistpe, supertpe) => val eThis = this(thistpe) val eSuper = this(supertpe) - if eThis == null || eSuper == null then null + if eThis.isInstanceOf[WildcardType] || eSuper.isInstanceOf[WildcardType] then WildcardType else SuperType(eThis, eSuper) case ExprType(rt) => defn.FunctionType(0) @@ -665,7 +665,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst eraseErasedFunctionApply(refinedInfo) case tp: TypeVar if !tp.isInstantiated => assert(inSigName, i"Cannot erase uninstantiated type variable $tp") - null + WildcardType case tp: TypeProxy => this(tp.underlying) case tp @ AndType(tp1, tp2) => @@ -676,7 +676,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst else val e1 = this(tp1) val e2 = this(tp2) - if e1 == null || e2 == null then null + if e1.isInstanceOf[WildcardType] || e2.isInstanceOf[WildcardType] then WildcardType else erasedGlb(e1, e2) case OrType(tp1, tp2) => if isSymbol && sourceLanguage.isScala2 && ctx.settings.scalajs.value then @@ -693,11 +693,11 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst else val e1 = this(tp1) val e2 = this(tp2) - if e1 == null || e2 == null then null + if e1.isInstanceOf[WildcardType] || e2.isInstanceOf[WildcardType] then WildcardType else TypeComparer.orType(e1, e2, isErased = true) case tp: MethodType => def paramErasure(tpToErase: Type) = - erasureFn(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, inSigName = false)(tpToErase).nn + erasureFn(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, inSigName = false)(tpToErase) val (names, formals0) = if tp.hasErasedParams then tp.paramNames .zip(tp.paramInfos) @@ -724,7 +724,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst else { def eraseParent(tp: Type) = tp.dealias match { // note: can't be opaque, since it's a class parent case tp: AppliedType if tp.tycon.isRef(defn.PairClass) => defn.ObjectType - case _ => apply(tp).nn + case _ => apply(tp) } val erasedParents: List[Type] = if ((cls eq defn.ObjectClass) || cls.isPrimitiveValueClass) Nil @@ -751,10 +751,10 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst tp case tp: WildcardType => assert(inSigName, i"Cannot erase wildcard type $tp") - null + WildcardType case tp if (tp `eq` NoType) || (tp `eq` NoPrefix) => tp - assert(etp != null || inSigName, i"Unexpected null erasure for $tp") + assert(!etp.isInstanceOf[WildcardType] || inSigName, i"Unexpected WildcardType erasure for $tp") etp /** Like translucentSuperType, but issue a fatal error if it does not exist. */ @@ -788,15 +788,15 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst else try val eElem = erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, inSigName)(elemtp) - if eElem == null then null + if eElem.isInstanceOf[WildcardType] then WildcardType else JavaArrayType(eElem) catch case ex: Throwable => handleRecursive("erase array type", tp.show, ex) } - private def erasePair(tp: Type)(using Context): Type | Null = { + private def erasePair(tp: Type)(using Context): Type = { val arity = tupleArity(tp) - if arity == -2 then null // erasure depends on an uninstantiated type variable or WildcardType + if arity == -2 then WildcardType // erasure depends on an uninstantiated type variable or WildcardType else if arity == -1 then defn.ProductClass.typeRef else if arity <= Definitions.MaxTupleArity then defn.TupleType(arity).nn else defn.TupleXXLClass.typeRef @@ -807,13 +807,12 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst * to the underlying type. */ def eraseInfo(tp: Type, sym: Symbol)(using Context): Type = - assert(!inSigName) // therefore apply(...).nn won't fail val tp1 = tp match case tp: MethodicType => integrateContextResults(tp, contextResultCount(sym)) case _ => tp tp1 match case ExprType(rt) => - if sym.is(Param) then apply(tp1).nn + if sym.is(Param) then apply(tp1) // Note that params with ExprTypes are eliminated by ElimByName, // but potentially re-introduced by ResolveSuper, when we add // forwarders to mixin methods. @@ -825,9 +824,9 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst eraseResult(tp1.resultType) match case rt: MethodType => rt case rt => MethodType(Nil, Nil, rt) - case tp1 => this(tp1).nn + case tp1 => this(tp1) - private def eraseDerivedValueClass(tp: Type)(using Context): Type | Null = { + private def eraseDerivedValueClass(tp: Type)(using Context): Type = { val cls = tp.classSymbol.asClass val unbox = valueClassUnbox(cls) if unbox.exists then @@ -837,7 +836,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst // The underlying part of an ErasedValueType cannot be an ErasedValueType itself val erase = erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, inSigName) val erasedUnderlying = erase(underlying) - if erasedUnderlying == null then return null + if erasedUnderlying.isInstanceOf[WildcardType] then return WildcardType // Ideally, we would just use `erasedUnderlying` as the erasure of `tp`, but to // be binary-compatible with Scala 2 we need two special cases for polymorphic @@ -871,7 +870,6 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst /** The erasure of a function result type. */ def eraseResult(tp: Type)(using Context): Type = - assert(!inSigName) // therefore apply(...).nn won't fail // For a value class V, "new V(x)" should have type V for type adaptation to work // correctly (see SIP-15 and [[Erasure.Boxing.adaptToType]]), so the result type of a // constructor method should not be semi-erased. @@ -881,25 +879,24 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst case tp: TypeRef => val sym = tp.symbol if (sym eq defn.UnitClass) sym.typeRef - else apply(tp).nn + else apply(tp) case tp: AppliedType => val sym = tp.tycon.typeSymbol if (sym.isClass && !erasureDependsOnArgs(sym)) eraseResult(tp.tycon) - else apply(tp).nn + else apply(tp) case _ => - apply(tp).nn + apply(tp) /** The name of the type as it is used in `Signature`s. * - * If `tp` is null, or if computing its erasure requires erasing a + * If `tp` is WildcardType, or if computing its erasure requires erasing a * WildcardType or an uninstantiated type variable, then the special name * `tpnme.Uninstantiated` which is used to signal an underdefined signature * is used. * * Note: Need to ensure correspondence with erasure! */ - private def sigName(tp: Type | Null)(using Context): TypeName = try - if tp == null then return tpnme.Uninstantiated + private def sigName(tp: Type)(using Context): TypeName = try tp match { case tp: TypeRef => if (!tp.denot.exists) @@ -913,7 +910,6 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst } if (semiEraseVCs && isDerivedValueClass(sym)) { val erasedVCRef = eraseDerivedValueClass(tp) - if erasedVCRef == null then return tpnme.Uninstantiated if (erasedVCRef.exists) return sigName(erasedVCRef) } if (defn.isSyntheticFunctionClass(sym)) @@ -958,9 +954,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst sigName(tp.underlying) case tp: WildcardType => tpnme.Uninstantiated - case tp: ErrorType => - tpnme.ERROR - case _ if tp eq NoType => // Can't write `case NoType` because of #18083. + case _: ErrorType | NoType => tpnme.ERROR case _ => val erasedTp = this(tp)