Skip to content

Fix signature computation and caching involving type variables #18092

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 10, 2023
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 @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
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)