Skip to content

Commit

Permalink
Backport "Fix signature computation and caching involving type variab…
Browse files Browse the repository at this point in the history
…les " to LTS (#19107)

Backports #18092 to the LTS branch.

PR submitted by the release tooling.
[skip ci]
  • Loading branch information
Kordyjan authored Dec 8, 2023
2 parents 435daa9 + 5aadde8 commit 76e39ce
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 165 deletions.
342 changes: 202 additions & 140 deletions compiler/src/dotty/tools/dotc/core/TypeErasure.scala

Large diffs are not rendered by default.

17 changes: 13 additions & 4 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -2260,7 +2265,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

Expand Down Expand Up @@ -3767,17 +3772,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

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 0 additions & 18 deletions compiler/src/dotty/tools/dotc/transform/TypeUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
77 changes: 76 additions & 1 deletion compiler/test/dotty/tools/SignatureTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -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)
11 changes: 11 additions & 0 deletions tests/pos/scala3mock.scala
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 76e39ce

Please sign in to comment.