diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 50e7e1847adf..8d84c53bd144 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -74,6 +74,37 @@ 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 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 + 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 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 tp.dealias match + case tp: TypeVar if !tp.isInstantiated => -2 + case _ => -1 + def normalizeClass(cls: ClassSymbol)(using Context): ClassSymbol = { if (cls.owner == defn.ScalaPackageClass) { if (defn.specialErasure.contains(cls)) @@ -143,9 +174,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) @@ -158,16 +189,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) = @@ -178,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, 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]]). @@ -186,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, 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 = @@ -200,7 +231,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) } @@ -230,7 +261,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( @@ -556,13 +587,19 @@ 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: + /** 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 WildcardType + * will be returned. + * + * 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| @@ -594,116 +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 = 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) => - SuperType(this(thistpe), this(supertpe)) - 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: 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 - erasedGlb(this(tp1), this(tp2)) - 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 - TypeComparer.orType(this(tp1), this(tp2), isErased = true) - case tp: MethodType => - def paramErasure(tpToErase: Type) = - erasureFn(sourceLanguage, semiEraseVCs, isConstructor, isSymbol, wildcardOK)(tpToErase) - 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) + private def apply(tp: Type)(using Context): Type = + 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.isInstanceOf[WildcardType] || eSuper.isInstanceOf[WildcardType] then WildcardType + 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") + WildcardType + 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.isInstanceOf[WildcardType] || e2.isInstanceOf[WildcardType] then WildcardType + 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.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) + 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) } - 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 if wildcardOK => - tp - case tp if (tp `eq` NoType) || (tp `eq` NoPrefix) => - tp - } + 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") + WildcardType + case tp if (tp `eq` NoType) || (tp `eq` NoPrefix) => + 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. */ private def checkedSuperType(tp: TypeProxy)(using Context): Type = @@ -734,17 +786,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, wildcardOK)(elemtp)) + try + val eElem = erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, inSigName)(elemtp) + 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 = { - // 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 - if (arity < 0) defn.ProductClass.typeRef - else if (arity <= Definitions.MaxTupleArity) defn.TupleType(arity).nn + val arity = tupleArity(tp) + 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 } @@ -780,8 +834,9 @@ 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) + 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 @@ -819,21 +874,27 @@ 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 if (sym eq defn.UnitClass) sym.typeRef - else this(tp) + else apply(tp) case tp: AppliedType => val sym = tp.tycon.typeSymbol if (sym.isClass && !erasureDependsOnArgs(sym)) eraseResult(tp.tycon) - else this(tp) + else apply(tp) case _ => - this(tp) + apply(tp) /** The name of the type as it is used in `Signature`s. - * Need to ensure correspondence with erasure! + * + * 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)(using Context): TypeName = try tp match { @@ -873,14 +934,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 diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index c00ec5eec64d..28b0ac02e095 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 @@ -2272,7 +2277,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 @@ -3784,17 +3789,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 @@ -4760,6 +4765,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) 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) => diff --git a/compiler/test/dotty/tools/SignatureTest.scala b/compiler/test/dotty/tools/SignatureTest.scala index 43d517417108..fdbb1e8f3760 100644 --- a/compiler/test/dotty/tools/SignatureTest.scala +++ b/compiler/test/dotty/tools/SignatureTest.scala @@ -2,14 +2,23 @@ package dotty.tools import vulpix.TestConfiguration +import org.junit.Assert._ 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.Names._ 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 +47,69 @@ 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) + + /** 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) 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)