diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 7fbdbafad0b8..93f49e9665eb 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -106,51 +106,132 @@ object Types extends TypeUtils { // nextId // } - /** A cache indicating whether the type was still provisional, last time we checked */ - @sharable private var mightBeProvisional = true + type ProvisionalState = util.HashMap[Type, Type] - /** 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 - - private def testProvisional(using Context): Boolean = + def currentProvisionalState(using Context): ProvisionalState = + val state: ProvisionalState = util.HashMap() + // Compared to `testProvisional`, we don't use short-circuiting or, + // because we want to collect all provisional types. class ProAcc extends TypeAccumulator[Boolean]: - override def apply(x: Boolean, t: Type) = x || test(t, this) + override def apply(x: Boolean, t: Type) = x | test(t, this) def test(t: Type, theAcc: TypeAccumulator[Boolean] | Null): Boolean = if t.mightBeProvisional then t.mightBeProvisional = t match case t: TypeRef => - t.currentSymbol.isProvisional || !t.currentSymbol.isStatic && { + if t.currentSymbol.isProvisional then + // When t is a TypeRef and its symbol is provisional, + // t will be considered provisional and its state is always updating. + state(t) = t + true + else if !t.currentSymbol.isStatic then (t: Type).mightBeProvisional = false // break cycles - test(t.prefix, theAcc) - || t.denot.infoOrCompleter.match - case info: LazyType => true - case info: AliasingBounds => test(info.alias, theAcc) - case TypeBounds(lo, hi) => test(lo, theAcc) || test(hi, theAcc) - case _ => false - } + if test(t.prefix, theAcc) then + // If the prefix is provisional, some provisional type from it + // must have been added to state, so we don't need to add t. + true + else t.denot.infoOrCompleter.match + case info: LazyType => + state(t) = info + true + case info: AliasingBounds => + test(info.alias, theAcc) + case TypeBounds(lo, hi) => + test(lo, theAcc) | test(hi, theAcc) + case _ => + // If a TypeRef has been fully completed, it is no longer provisional, + // so we don't need to traverse its info. + false + else false case t: TermRef => !t.currentSymbol.isStatic && test(t.prefix, theAcc) case t: AppliedType => - t.fold(false, (x, tp) => x || test(tp, theAcc)) + t.fold(false, (x, tp) => x | test(tp, theAcc)) case t: TypeVar => - !t.isPermanentlyInstantiated || test(t.permanentInst, theAcc) + if t.isPermanentlyInstantiated then + test(t.permanentInst, theAcc) + else + val inst = t.instanceOpt + if inst.exists then + // We want to store the temporary instance to the state + // in order to reuse the cache when possible. + state(t) = inst + test(inst, theAcc) + else state(t) = t + true case t: LazyRef => - !t.completed || test(t.ref, theAcc) + if !t.completed then + state(t) = t + true + else + test(t.ref, theAcc) case _ => (if theAcc != null then theAcc else ProAcc()).foldOver(false, t) end if t.mightBeProvisional end test test(this, null) - end testProvisional + state + end currentProvisionalState + + def isCacheUpToDate( + currentState: ProvisionalState, + lastState: ProvisionalState | Null) + (using Context): Boolean = + lastState != null + && currentState.size == lastState.size + && currentState.iterator.forall: (tp, info) => + lastState.contains(tp) && { + tp match + case tp: TypeRef => (info ne tp) && (info eq lastState(tp)) + case _=> info eq lastState(tp) + } + + /** A cache indicating whether the type was still provisional, last time we checked */ + @sharable private var mightBeProvisional = true + + /** 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 + def isProvisional(using Context): Boolean = mightBeProvisional && !currentProvisionalState.isEmpty + + // private def testProvisional(using Context): Boolean = + // class ProAcc extends TypeAccumulator[Boolean]: + // override def apply(x: Boolean, t: Type) = x || test(t, this) + // def test(t: Type, theAcc: TypeAccumulator[Boolean] | Null): Boolean = + // if t.mightBeProvisional then + // t.mightBeProvisional = t match + // case t: TypeRef => + // t.currentSymbol.isProvisional || !t.currentSymbol.isStatic && { + // (t: Type).mightBeProvisional = false // break cycles + // test(t.prefix, theAcc) + // || t.denot.infoOrCompleter.match + // case info: LazyType => true + // case info: AliasingBounds => test(info.alias, theAcc) + // case TypeBounds(lo, hi) => test(lo, theAcc) || test(hi, theAcc) + // case _ => false + // } + // case t: TermRef => + // !t.currentSymbol.isStatic && test(t.prefix, theAcc) + // case t: AppliedType => + // t.fold(false, (x, tp) => x || test(tp, theAcc)) + // case t: TypeVar => + // !t.isPermanentlyInstantiated || test(t.permanentInst, theAcc) + // case t: LazyRef => + // !t.completed || test(t.ref, theAcc) + // case _ => + // (if theAcc != null then theAcc else ProAcc()).foldOver(false, t) + // end if + // t.mightBeProvisional + // end test + // test(this, null) + // end testProvisional /** Is this type different from NoType? */ final def exists: Boolean = this.ne(NoType) @@ -2306,10 +2387,12 @@ object Types extends TypeUtils { private var myName: Name | Null = null private var lastDenotation: Denotation | Null = null + private var lastDenotationProvState: ProvisionalState | Null = null private var lastSymbol: Symbol | Null = null private var checkedPeriod: Period = Nowhere private var myStableHash: Byte = 0 private var mySignature: Signature = uninitialized + private var mySignatureProvState: ProvisionalState | Null = null private var mySignatureRunId: Int = NoRunId // Invariants: @@ -2344,9 +2427,11 @@ object Types extends TypeUtils { else if ctx.erasedTypes then atPhase(erasurePhase)(computeSignature) else symbol.asSeenFrom(prefix).signature - if ctx.runId != mySignatureRunId then + if ctx.runId != mySignatureRunId + || !isCacheUpToDate(currentProvisionalState, mySignatureProvState) then mySignature = computeSignature - if !mySignature.isUnderDefined && !isProvisional then mySignatureRunId = ctx.runId + mySignatureProvState = currentProvisionalState + if !mySignature.isUnderDefined then mySignatureRunId = ctx.runId mySignature end signature @@ -2357,7 +2442,9 @@ object Types extends TypeUtils { * some symbols change their signature at erasure. */ private def currentSignature(using Context): Signature = - if ctx.runId == mySignatureRunId then mySignature + if ctx.runId == mySignatureRunId + && isCacheUpToDate(currentProvisionalState, mySignatureProvState) + then mySignature else val lastd = lastDenotation if lastd != null then sigFromDenot(lastd) @@ -2435,7 +2522,10 @@ object Types extends TypeUtils { val lastd = lastDenotation.asInstanceOf[Denotation] // Even if checkedPeriod == now we still need to recheck lastDenotation.validFor // as it may have been mutated by SymDenotation#installAfter - if checkedPeriod.code != NowhereCode && lastd.validFor.contains(ctx.period) then lastd + if checkedPeriod.code != NowhereCode + && isCacheUpToDate(prefix.currentProvisionalState, lastDenotationProvState) + && lastd.validFor.contains(ctx.period) + then lastd else computeDenot private def computeDenot(using Context): Denotation = { @@ -2469,14 +2559,18 @@ object Types extends TypeUtils { finish(symd.current) } + def isLastDenotValid = + checkedPeriod.code != NowhereCode + && isCacheUpToDate(prefix.currentProvisionalState, lastDenotationProvState) + lastDenotation match { case lastd0: SingleDenotation => val lastd = lastd0.skipRemoved - if lastd.validFor.runId == ctx.runId && checkedPeriod.code != NowhereCode then + if lastd.validFor.runId == ctx.runId && isLastDenotValid then finish(lastd.current) else lastd match { case lastd: SymDenotation => - if stillValid(lastd) && checkedPeriod.code != NowhereCode then finish(lastd.current) + if stillValid(lastd) && isLastDenotValid then finish(lastd.current) else finish(memberDenot(lastd.initial.name, allowPrivate = false)) case _ => fromDesignator @@ -2567,7 +2661,8 @@ object Types extends TypeUtils { lastDenotation = denot lastSymbol = denot.symbol - checkedPeriod = if (prefix.isProvisional) Nowhere else ctx.period + lastDenotationProvState = prefix.currentProvisionalState + checkedPeriod = ctx.period designator match { case sym: Symbol if designator ne lastSymbol.nn => designator = lastSymbol.asInstanceOf[Designator{ type ThisName = self.ThisName }] @@ -3850,14 +3945,18 @@ object Types extends TypeUtils { sealed abstract class MethodOrPoly extends UncachedGroundType with LambdaType with MethodicType { // Invariants: - // (1) mySignatureRunId != NoRunId => mySignature != null - // (2) myJavaSignatureRunId != NoRunId => myJavaSignature != null + // (1) mySignatureRunId != NoRunId => mySignature != null + // (2) myJavaSignatureRunId != NoRunId => myJavaSignature != null + // (3) myScala2SignatureRunId != NoRunId => myScala2Signature != null private var mySignature: Signature = uninitialized + private var mySignatureProvState: ProvisionalState | Null = null private var mySignatureRunId: Int = NoRunId private var myJavaSignature: Signature = uninitialized + private var myJavaSignatureProvState: ProvisionalState | Null = null private var myJavaSignatureRunId: Int = NoRunId private var myScala2Signature: Signature = uninitialized + private var myScala2SignatureProvState: ProvisionalState | Null = null private var myScala2SignatureRunId: Int = NoRunId /** If `isJava` is false, the Scala signature of this method. Otherwise, its Java signature. @@ -3895,19 +3994,25 @@ object Types extends TypeUtils { sourceLanguage match case SourceLanguage.Java => - if ctx.runId != myJavaSignatureRunId then + if ctx.runId != myJavaSignatureRunId + || !isCacheUpToDate(currentProvisionalState, myJavaSignatureProvState) then myJavaSignature = computeSignature - if !myJavaSignature.isUnderDefined && !isProvisional then myJavaSignatureRunId = ctx.runId + myJavaSignatureProvState = currentProvisionalState + if !myJavaSignature.isUnderDefined then myJavaSignatureRunId = ctx.runId myJavaSignature case SourceLanguage.Scala2 => - if ctx.runId != myScala2SignatureRunId then + if ctx.runId != myScala2SignatureRunId + || !isCacheUpToDate(currentProvisionalState, myScala2SignatureProvState) then myScala2Signature = computeSignature - if !myScala2Signature.isUnderDefined && !isProvisional then myScala2SignatureRunId = ctx.runId + myScala2SignatureProvState = currentProvisionalState + if !myScala2Signature.isUnderDefined then myScala2SignatureRunId = ctx.runId myScala2Signature case SourceLanguage.Scala3 => - if ctx.runId != mySignatureRunId then + if ctx.runId != mySignatureRunId + || !isCacheUpToDate(currentProvisionalState, mySignatureProvState) then mySignature = computeSignature - if !mySignature.isUnderDefined && !isProvisional then mySignatureRunId = ctx.runId + mySignatureProvState = currentProvisionalState + if !mySignature.isUnderDefined then mySignatureRunId = ctx.runId mySignature end signature diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 00f6f0fe0a1a..42765cd6c0bf 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -2048,14 +2048,6 @@ trait Applications extends Compatibility { def resolveOverloaded(alts: List[TermRef], pt: Type)(using Context): List[TermRef] = record("resolveOverloaded") - // A local cache for widenings of alternatives. - // The logic here heavily relies on `widen` to resolve overloadings. However, `widen` - // would compute the denotation, and the denotation of a provisional type is not cached, - // so we would end up computing the same denotation multiple times. - // Given the denotation will not change in this part, we can safely cache the result. - val altsWidenMap = mutable.HashMap.empty[TermRef, Type] - def widen(ref: TermRef): Type = altsWidenMap.getOrElseUpdate(ref, ref.widen) - /** Is `alt` a method or polytype whose result type after the first value parameter * section conforms to the expected type `resultType`? If `resultType` * is a `IgnoredProto`, pick the underlying type instead. @@ -2103,15 +2095,15 @@ trait Applications extends Compatibility { pt match case pt: FunProto => if pt.applyKind == ApplyKind.Using then - val alts0 = alts.filterConserve(alt => widen(alt).stripPoly.isImplicitMethod) + val alts0 = alts.filterConserve(_.widen.stripPoly.isImplicitMethod) if alts0 ne alts then return resolve(alts0) - else if alts.exists(alt => widen(alt).stripPoly.isContextualMethod) then - return resolveMapped(alts, alt => stripImplicit(widen(alt)), pt) + else if alts.exists(_.widen.stripPoly.isContextualMethod) then + return resolveMapped(alts, alt => stripImplicit(alt.widen), pt) case _ => - var found = withoutMode(Mode.ImplicitsEnabled)(resolveOverloaded1(alts, pt, altsWidenMap)) + var found = withoutMode(Mode.ImplicitsEnabled)(resolveOverloaded1(alts, pt)) if found.isEmpty && ctx.mode.is(Mode.ImplicitsEnabled) then - found = resolveOverloaded1(alts, pt, altsWidenMap) + found = resolveOverloaded1(alts, pt) found match case alt :: Nil => adaptByResult(alt, alts) :: Nil case _ => found @@ -2121,15 +2113,16 @@ trait Applications extends Compatibility { * - the result is applied to value arguments and alternative is not a method, or * - the result is applied to type arguments and alternative is not polymorphic */ - def tryApply(alt: TermRef): Boolean = pt match - case pt: FunProto => !widen(alt).stripPoly.isInstanceOf[MethodType] - case pt: PolyProto => !widen(alt).isInstanceOf[PolyType] + val tryApply: Type => Boolean = alt => pt match { + case pt: FunProto => !alt.widen.stripPoly.isInstanceOf[MethodType] + case pt: PolyProto => !alt.widen.isInstanceOf[PolyType] case _ => false + } /** Replace each alternative by its apply members where necessary */ def applyMembers(alt: TermRef): List[TermRef] = if (tryApply(alt)) { - val qual = widen(alt) match { + val qual = alt.widen match { case pt: PolyType => wildApprox(pt.resultType) case _ => @@ -2157,12 +2150,10 @@ trait Applications extends Compatibility { * It might be called twice from the public `resolveOverloaded` method, once with * implicits and SAM conversions enabled, and once without. */ - private def resolveOverloaded1(alts: List[TermRef], pt: Type, altsWidenMap: mutable.HashMap[TermRef, Type])(using Context): List[TermRef] = + private def resolveOverloaded1(alts: List[TermRef], pt: Type)(using Context): List[TermRef] = trace(i"resolve over $alts%, %, pt = $pt", typr, show = true) { record(s"resolveOverloaded1", alts.length) - def widen(ref: TermRef): Type = altsWidenMap.getOrElseUpdate(ref, ref.widen) - def isDetermined(alts: List[TermRef]) = alts.isEmpty || alts.tail.isEmpty /** The shape of given tree as a type; cannot handle named arguments. */ @@ -2206,7 +2197,7 @@ trait Applications extends Compatibility { // If ref refers to a method whose parameter at index `idx` is a function type, // the arity of that function, otherise -1. def paramCount(ref: TermRef) = - val formals = widen(ref).firstParamTypes + val formals = ref.widen.firstParamTypes if formals.length > idx then formals(idx).dealias match case defn.FunctionNOf(args, _, _) => args.length @@ -2231,7 +2222,7 @@ trait Applications extends Compatibility { val candidates = pt match { case pt @ FunProto(args, resultType) => val numArgs = args.length - def sizeFits(alt: TermRef): Boolean = widen(alt).stripPoly match { + def sizeFits(alt: TermRef): Boolean = alt.widen.stripPoly match { case tp: MethodType => val ptypes = tp.paramInfos val numParams = ptypes.length @@ -2286,12 +2277,12 @@ trait Applications extends Compatibility { val alts1 = alts.filterConserve(pt.canInstantiate) if isDetermined(alts1) then alts1 else - def withinBounds(alt: TermRef) = widen(alt) match + def withinBounds(alt: TermRef) = alt.widen match case tp: PolyType => TypeOps.boundsViolations(targs1, tp.paramInfos, _.substParams(tp, _), NoType).isEmpty val alts2 = alts1.filter(withinBounds) if isDetermined(alts2) then alts2 - else resolveMapped(alts1, alt => widen(alt).appliedTo(targs1.tpes), pt1) + else resolveMapped(alts1, _.widen.appliedTo(targs1.tpes), pt1) case pt => val compat = alts.filterConserve(normalizedCompatible(_, pt, keepConstraint = false)) @@ -2336,9 +2327,9 @@ trait Applications extends Compatibility { case _ => NoType } - skip(widen(alt)) + skip(alt.widen) - def resultIsMethod(tp: TermRef): Boolean = widen(tp).stripPoly match + def resultIsMethod(tp: Type): Boolean = tp.widen.stripPoly match case tp: MethodType => stripInferrable(tp.resultType).isInstanceOf[MethodType] case _ => false @@ -2368,7 +2359,7 @@ trait Applications extends Compatibility { if noCurriedCount == 1 then noCurried else if noCurriedCount > 1 && noCurriedCount < alts.length then - resolveOverloaded1(noCurried, pt, altsWidenMap) + resolveOverloaded1(noCurried, pt) else // prefer alternatves that match without default parameters val noDefaults = alts.filterConserve(!_.symbol.hasDefaultParams) @@ -2376,10 +2367,10 @@ trait Applications extends Compatibility { if noDefaultsCount == 1 then noDefaults else if noDefaultsCount > 1 && noDefaultsCount < alts.length then - resolveOverloaded1(noDefaults, pt, altsWidenMap) + resolveOverloaded1(noDefaults, pt) else if deepPt ne pt then // try again with a deeper known expected type - resolveOverloaded1(alts, deepPt, altsWidenMap) + resolveOverloaded1(alts, deepPt) else candidates }