Skip to content

Commit 5fb5ebe

Browse files
committed
Fix signature computation involving nested type variables
Signatures used for overloading/overriding resolution are cached and therefore should not depend on uninstantiated type variables, this is handled in `TypeErasure#sigName` by using the name `tpnme.Uninstantiated` when encountering an uninstantiated type variable (this name is handled specially in `Signature`). But before this commit, `sigName` only checked for type variables at the top-level of the type, even though nested type variables 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 *: ...)
1 parent 9908312 commit 5fb5ebe

File tree

3 files changed

+67
-4
lines changed

3 files changed

+67
-4
lines changed

compiler/src/dotty/tools/dotc/core/TypeErasure.scala

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,14 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst
860860
* Need to ensure correspondence with erasure!
861861
*/
862862
private def sigName(tp: Type)(using Context): TypeName = try
863+
// Signature caching relies on `Signature#isUnderDefined` which itself
864+
// relies on us returning `tpnme.Uninstantiated` if the signature might
865+
// change. Only some part of the types influence its signature, but it's
866+
// simpler to always return `tpnme.Uninstantiated` if any part of the type
867+
// might change (experimentally, this leads to 2% extra signature cache
868+
// misses when compiling Dotty compared to a more precise analysis, but a
869+
// much simpler implementation).
870+
if tp.isProvisional then return tpnme.Uninstantiated
863871
tp match {
864872
case tp: TypeRef =>
865873
if (!tp.denot.exists)
@@ -902,9 +910,6 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst
902910
sigName(underlyingOfTermRef(tp))
903911
case ExprType(rt) =>
904912
sigName(defn.FunctionOf(Nil, rt))
905-
case tp: TypeVar =>
906-
val inst = tp.instanceOpt
907-
if (inst.exists) sigName(inst) else tpnme.Uninstantiated
908913
case tp @ RefinedType(parent, nme.apply, _) if parent.typeSymbol eq defn.PolyFunctionClass =>
909914
// we need this case rather than falling through to the default
910915
// because RefinedTypes <: TypeProxy and it would be caught by

compiler/test/dotty/tools/SignatureTest.scala

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@ import vulpix.TestConfiguration
44

55
import org.junit.Test
66

7-
import dotc.ast.Trees._
7+
import dotc.ast.untpd
88
import dotc.core.Decorators._
99
import dotc.core.Contexts._
10+
import dotc.core.Flags._
1011
import dotc.core.Phases._
1112
import dotc.core.Types._
1213
import dotc.core.Symbols._
14+
import dotc.core.StdNames._
15+
import dotc.core.Signature
16+
import dotc.typer.ProtoTypes.constrained
17+
import dotc.typer.Inferencing.isFullyDefined
18+
import dotc.typer.ForceDegree
19+
import dotc.util.NoSourcePosition
1320

1421
import java.io.File
1522
import java.nio.file._
@@ -38,3 +45,43 @@ class SignatureTest:
3845
|${ref.denot.signature}""".stripMargin)
3946
}
4047
}
48+
49+
/** Ensure that signature computation returns an underdefined signature when
50+
* the signature depends on uninstantiated type variables.
51+
*
52+
* This is important because signatures are considered safe to cache in
53+
* types if they're not underdefined.
54+
*/
55+
@Test def underdefinedSignature: Unit =
56+
inCompilerContext(TestConfiguration.basicClasspath, separateRun = false,
57+
"""trait Foo
58+
|trait Bar
59+
|class A[T <: Tuple]:
60+
| def and(x: T & Foo): Unit = {}
61+
| def andor(x: (T | Bar) & Foo): Unit = {}
62+
| def array(x: Array[(T | Bar) & Foo]): Unit = {}
63+
| def tuple(x: Foo *: T): Unit = {}
64+
| def tuple2(x: Foo *: (T | Tuple) & Foo): Unit = {}
65+
|""".stripMargin) {
66+
val cls = requiredClass("A")
67+
val tvar = constrained(cls.requiredMethod(nme.CONSTRUCTOR).info.asInstanceOf[TypeLambda], untpd.EmptyTree, alwaysAddTypeVars = true)._2.head.tpe
68+
tvar <:< defn.TupleTypeRef
69+
val prefix = cls.typeRef.appliedTo(tvar)
70+
71+
def checkSignatures(expectedIsUnderDefined: Boolean): Unit =
72+
for decl <- cls.info.decls.toList if decl.is(Method) && !decl.isConstructor do
73+
val meth = decl.asSeenFrom(prefix)
74+
val sig = meth.info.signature
75+
val what = if expectedIsUnderDefined then "underdefined" else "fully-defined"
76+
assert(sig.isUnderDefined == expectedIsUnderDefined, i"Signature of `$meth` with prefix `$prefix` and type `${meth.info}` should be $what but is `$sig`")
77+
78+
checkSignatures(expectedIsUnderDefined = true)
79+
80+
inContext(ctx.fresh.setNewTyperState()):
81+
assert(isFullyDefined(tvar, force = ForceDegree.all), s"Could not instantiate $tvar")
82+
// Signatures are still underdefined if the instantiation can be retracted.
83+
checkSignatures(expectedIsUnderDefined = true)
84+
85+
assert(isFullyDefined(tvar, force = ForceDegree.all), s"Could not instantiate $tvar")
86+
checkSignatures(expectedIsUnderDefined = false)
87+
}

tests/pos/scala3mock.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class MockFunction1[T1]:
2+
def expects(v1: T1 | Foo): Any = ???
3+
def expects(matcher: String): Any = ???
4+
5+
def when[T1](f: T1 => Any): MockFunction1[T1] = ???
6+
7+
class Foo
8+
9+
def main =
10+
val f: Foo = new Foo
11+
when((x: Foo) => "").expects(f)

0 commit comments

Comments
 (0)