Skip to content

Commit f951534

Browse files
committed
Take number of type parameters into account when choosing an overload
In Tasty, pickling a selection will pickle its signature using the SIGNED tag to be able to distinguish overloads, this usually works but it turns out that some overloads have the same signature (see tests/pos/poly-signature.scala, but this also affects `Set#++` in the 2.13 collections), resulting in ambiguous overload errors when unpickled. This commit solves this problem by also storing the number of type parameters in the signature. The obvious way to implement this would be to add a field of type Int to the `Signature` case class, but that wouldn't scale to a future where methods may have multiple type parameter lists, so instead we change the existing `paramsSig: List[TypeName]` field to take a list of `TypeName | Int`.
1 parent 1fa8e91 commit f951534

File tree

9 files changed

+134
-45
lines changed

9 files changed

+134
-45
lines changed

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

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,42 @@
11
package dotty.tools.dotc
22
package core
33

4+
import scala.annotation.tailrec
5+
46
import Names._, Types._, Contexts._, StdNames._, Decorators._
57
import TypeErasure.sigName
6-
7-
import scala.annotation.tailrec
8+
import Signature._
89

910
/** The signature of a denotation.
10-
* Overloaded denotations with the same name are distinguished by
11-
* their signatures. A signature of a method (of type PolyType,MethodType, or ExprType) is
12-
* composed of a list of signature names, one for each parameter type, plus a signature for
13-
* the result type. Methods are uncurried before taking their signatures.
14-
* The signature name of a type is the fully qualified name of the type symbol of the type's erasure.
11+
*
12+
* Same-named denotations with different signatures are considered to be
13+
* overloads, see `SingleDenotation#matches` for more details.
14+
*
15+
* A _method signature_ (a value of type `Signature`, excluding `NotAMethod`
16+
* and `OverloadedSignature`) is composed of a list of _parameter signatures_,
17+
* plus a _type signature_ for the final result type.
18+
*
19+
* A _parameter signature_ (a value of type `ParamSig`) is either an integer,
20+
* representing the number of type parameters in a type parameter section, or
21+
* the _type signature_ of a term parameter.
22+
*
23+
* A _type signature_ is the fully qualified name of the type symbol of the
24+
* type's erasure.
1525
*
1626
* For instance a definition
1727
*
18-
* def f(x: Int)(y: List[String]): String
28+
* def f[T, S](x: Int)(y: List[T]): S
1929
*
2030
* would have signature
2131
*
2232
* Signature(
23-
* List("scala.Int".toTypeName, "scala.collection.immutable.List".toTypeName),
24-
* "scala.String".toTypeName)
33+
* List(2, "scala.Int".toTypeName, "scala.collection.immutable.List".toTypeName),
34+
* "java.lang.Object".toTypeName)
35+
*
36+
* Note that `paramsSig` has one entry for *a whole type parameter section* but
37+
* one entry *for each term parameter* (currently, methods in Dotty can only
38+
* have one type parameter section but this encoding leaves the door open for
39+
* supporting multiple sections).
2540
*
2641
* The signatures of non-method types are always `NotAMethod`.
2742
*
@@ -31,19 +46,18 @@ import scala.annotation.tailrec
3146
* - tpnme.WILDCARD Arises from a Wildcard or error type
3247
* - tpnme.Uninstantiated Arises from an uninstantiated type variable
3348
*/
34-
case class Signature(paramsSig: List[TypeName], resSig: TypeName) {
35-
import Signature._
49+
case class Signature(paramsSig: List[ParamSig], resSig: TypeName) {
3650

3751
/** Two names are consistent if they are the same or one of them is tpnme.Uninstantiated */
38-
private def consistent(name1: TypeName, name2: TypeName) =
52+
private def consistent(name1: ParamSig, name2: ParamSig) =
3953
name1 == name2 || name1 == tpnme.Uninstantiated || name2 == tpnme.Uninstantiated
4054

4155
/** Does this signature coincide with that signature on their parameter parts?
42-
* This is the case if all parameter names are _consistent_, i.e. they are either
56+
* This is the case if all parameter signatures are _consistent_, i.e. they are either
4357
* equal or on of them is tpnme.Uninstantiated.
4458
*/
4559
final def consistentParams(that: Signature)(implicit ctx: Context): Boolean = {
46-
@tailrec def loop(names1: List[TypeName], names2: List[TypeName]): Boolean =
60+
@tailrec def loop(names1: List[ParamSig], names2: List[ParamSig]): Boolean =
4761
if (names1.isEmpty) names2.isEmpty
4862
else !names2.isEmpty && consistent(names1.head, names2.head) && loop(names1.tail, names2.tail)
4963
if (ctx.erasedTypes && (this == NotAMethod) != (that == NotAMethod))
@@ -56,22 +70,23 @@ case class Signature(paramsSig: List[TypeName], resSig: TypeName) {
5670

5771
/** `that` signature, but keeping all corresponding parts of `this` signature. */
5872
final def updateWith(that: Signature): Signature = {
59-
def update(name1: TypeName, name2: TypeName): TypeName =
73+
def update[T <: ParamSig](name1: T, name2: T): T =
6074
if (consistent(name1, name2)) name1 else name2
6175
if (this == that) this
6276
else if (!this.paramsSig.hasSameLengthAs(that.paramsSig)) that
6377
else {
6478
val mapped = Signature(
65-
this.paramsSig.zipWithConserve(that.paramsSig)(update),
79+
// DOTTY: we shouldn't have to explicitly pass a type argument to `update`
80+
this.paramsSig.zipWithConserve(that.paramsSig)(update[ParamSig]),
6681
update(this.resSig, that.resSig))
6782
if (mapped == this) this else mapped
6883
}
6984
}
7085

7186
/** The degree to which this signature matches `that`.
72-
* If parameter names are consistent and result types names match (i.e. they are the same
87+
* If parameter signatures are consistent and result types names match (i.e. they are the same
7388
* or one is a wildcard), the result is `FullMatch`.
74-
* If only the parameter names are consistent, the result is `ParamMatch` before erasure and
89+
* If only the parameter signatures are consistent, the result ParamMatch` before erasure and
7590
* `NoMatch` otherwise.
7691
* If the parameters are inconsistent, the result is always `NoMatch`.
7792
*/
@@ -94,8 +109,16 @@ case class Signature(paramsSig: List[TypeName], resSig: TypeName) {
94109
*
95110
* Like Signature#apply, the result is only cacheable if `isUnderDefined == false`.
96111
*/
97-
def prepend(params: List[Type], isJava: Boolean)(implicit ctx: Context): Signature =
98-
Signature(params.map(p => sigName(p, isJava)) ++ paramsSig, resSig)
112+
def prependTermParams(params: List[Type], isJava: Boolean)(implicit ctx: Context): Signature =
113+
Signature(params.map(p => sigName(p, isJava)) ::: paramsSig, resSig)
114+
115+
/** Construct a signature by prepending the length of a type parameter section
116+
* to the parameter part of this signature.
117+
*
118+
* Like Signature#apply, the result is only cacheable if `isUnderDefined == false`.
119+
*/
120+
def prependTypeParams(typeParamSigsSectionLength: Int)(implicit ctx: Context): Signature =
121+
Signature(typeParamSigsSectionLength :: paramsSig, resSig)
99122

100123
/** A signature is under-defined if its paramsSig part contains at least one
101124
* `tpnme.Uninstantiated`. Under-defined signatures arise when taking a signature
@@ -106,6 +129,10 @@ case class Signature(paramsSig: List[TypeName], resSig: TypeName) {
106129
}
107130

108131
object Signature {
132+
/** A parameter signature, see the documentation of `Signature` for more information. */
133+
type ParamSig = TypeName | Int
134+
// Erasure means that our Ints will be boxed, but Integer#valueOf caches
135+
// small values, so the performance hit should be minimal.
109136

110137
enum MatchDegree {
111138
case NoMatch, ParamMatch, FullMatch

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3216,7 +3216,7 @@ object Types {
32163216

32173217
def computeSignature(implicit ctx: Context): Signature = {
32183218
val params = if (isErasedMethod) Nil else paramInfos
3219-
resultSignature.prepend(params, isJavaMethod)
3219+
resultSignature.prependTermParams(params, isJavaMethod)
32203220
}
32213221

32223222
protected def prefixString: String = companion.prefixString
@@ -3402,7 +3402,8 @@ object Types {
34023402
assert(resType.isInstanceOf[TermType], this)
34033403
assert(paramNames.nonEmpty)
34043404

3405-
def computeSignature(implicit ctx: Context): Signature = resultSignature
3405+
def computeSignature(implicit ctx: Context): Signature =
3406+
resultSignature.prependTypeParams(paramNames.length)
34063407

34073408
override def isContextualMethod = resType.isContextualMethod
34083409
override def isImplicitMethod = resType.isImplicitMethod

compiler/src/dotty/tools/dotc/core/tasty/NameBuffer.scala

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ package core
44
package tasty
55

66
import collection.mutable
7-
import Names.{Name, chrs, SimpleName, DerivedName}
7+
import Names.{Name, chrs, SimpleName, DerivedName, TypeName}
88
import NameKinds._
99
import Decorators._
1010
import TastyBuffer._
@@ -23,7 +23,13 @@ class NameBuffer extends TastyBuffer(10000) {
2323
case None =>
2424
name1 match {
2525
case SignedName(original, Signature(params, result)) =>
26-
nameIndex(original); nameIndex(result); params.foreach(nameIndex)
26+
nameIndex(original)
27+
nameIndex(result)
28+
params.foreach {
29+
case param: TypeName =>
30+
nameIndex(param)
31+
case _ =>
32+
}
2733
case AnyQualifiedName(prefix, name) =>
2834
nameIndex(prefix); nameIndex(name)
2935
case AnyUniqueName(original, separator, num) =>
@@ -50,6 +56,16 @@ class NameBuffer extends TastyBuffer(10000) {
5056
def writeNameRef(ref: NameRef): Unit = writeNat(ref.index)
5157
def writeNameRef(name: Name): Unit = writeNameRef(nameRefs(name.toTermName))
5258

59+
def writeParamSig(paramSig: Signature.ParamSig): Unit ={
60+
val encodedValue = paramSig match {
61+
case paramSig: TypeName =>
62+
nameRefs(paramSig.toTermName).index
63+
case paramSig: Int =>
64+
-paramSig
65+
}
66+
writeInt(encodedValue)
67+
}
68+
5369
def pickleNameContents(name: Name): Unit = {
5470
val tag = name.toTermName.info.kind.tag
5571
writeByte(tag)
@@ -70,10 +86,10 @@ class NameBuffer extends TastyBuffer(10000) {
7086
}
7187
case AnyNumberedName(original, num) =>
7288
withLength { writeNameRef(original); writeNat(num) }
73-
case SignedName(original, Signature(params, result)) =>
89+
case SignedName(original, Signature(paramsSig, result)) =>
7490
withLength(
75-
{ writeNameRef(original); writeNameRef(result); params.foreach(writeNameRef) },
76-
if ((params.length + 2) * maxIndexWidth <= maxNumInByte) 1 else 2)
91+
{ writeNameRef(original); writeNameRef(result); paramsSig.foreach(writeParamSig) },
92+
if ((paramsSig.length + 2) * maxIndexWidth <= maxNumInByte) 1 else 2)
7793
case DerivedName(original, _) =>
7894
withLength { writeNameRef(original) }
7995
}

compiler/src/dotty/tools/dotc/core/tasty/TastyFormat.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ Macro-format:
4242
INLINEACCESSOR Length underlying_NameRef -- inline$A
4343
OBJECTCLASS Length underlying_NameRef -- A$ (name of the module class for module A)
4444
45-
SIGNED Length original_NameRef resultSig_NameRef paramSig_NameRef* -- name + signature
45+
SIGNED Length original_NameRef resultSig_NameRef ParamSig* -- name + signature
46+
47+
ParamSig = Int // If negative, the absolute value represents the length of a type parameter section
48+
// If positive, this is a NameRef for the fully qualified name of a term parameter.
4649
4750
NameRef = Nat // ordinal number of name in name table, starting from 1.
4851
@@ -248,7 +251,7 @@ Standard Section: "Comments" Comment*
248251
object TastyFormat {
249252

250253
final val header: Array[Int] = Array(0x5C, 0xA1, 0xAB, 0x1F)
251-
val MajorVersion: Int = 16
254+
val MajorVersion: Int = 17
252255
val MinorVersion: Int = 0
253256

254257
/** Tags used to serialize names */

compiler/src/dotty/tools/dotc/core/tasty/TastyUnpickler.scala

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ class TastyUnpickler(reader: TastyReader) {
3636
private def readName(): TermName = nameAtRef(readNameRef())
3737
private def readString(): String = readName().toString
3838

39+
private def readParamSig(): Signature.ParamSig = {
40+
val ref = readInt()
41+
if (ref < 0)
42+
ref.abs
43+
else
44+
nameAtRef(NameRef(ref)).toTypeName
45+
}
46+
3947
private def readNameContents(): TermName = {
4048
val tag = readByte()
4149
val length = readNat()
@@ -58,8 +66,9 @@ class TastyUnpickler(reader: TastyReader) {
5866
case SIGNED =>
5967
val original = readName()
6068
val result = readName().toTypeName
61-
val params = until(end)(readName().toTypeName)
62-
var sig = Signature(params, result)
69+
// DOTTY: we shouldn't have to give an explicit type to paramsSig
70+
val paramsSig: List[Signature.ParamSig] = until(end)(readParamSig())
71+
val sig = Signature(paramsSig, result)
6372
SignedName(original, sig)
6473
case _ =>
6574
simpleNameKindOfTag(tag)(readName())

compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2152,19 +2152,27 @@ object messages {
21522152
val kind: String = "Duplicate Symbol"
21532153
val msg: String = {
21542154
def nameAnd = if (decl.name != previousDecl.name) " name and" else ""
2155-
val details = if (decl.isRealMethod && previousDecl.isRealMethod) {
2156-
// compare the signatures when both symbols represent methods
2157-
decl.signature.matchDegree(previousDecl.signature) match {
2158-
case Signature.MatchDegree.NoMatch =>
2155+
def details(implicit ctx: Context): String =
2156+
if (decl.isRealMethod && previousDecl.isRealMethod) {
2157+
// compare the signatures when both symbols represent methods
2158+
decl.signature.matchDegree(previousDecl.signature) match {
21592159
// DOTTY problem: Need to qualify MatchDegree enum vals since otherwise exhaustivity fails.
21602160
// To fix this, we need to export vals under singleton types.
2161-
"" // shouldn't be reachable
2162-
case Signature.MatchDegree.ParamMatch =>
2163-
"have matching parameter types."
2164-
case Signature.MatchDegree.FullMatch =>
2165-
i"have the same$nameAnd type after erasure."
2161+
case Signature.MatchDegree.NoMatch =>
2162+
// If the signatures don't match at all at the current phase, then
2163+
// they might match after erasure.
2164+
val elimErasedCtx = ctx.withPhaseNoEarlier(ctx.elimErasedValueTypePhase.next)
2165+
if (elimErasedCtx != ctx)
2166+
details(elimErasedCtx)
2167+
else
2168+
"" // shouldn't be reachable
2169+
case Signature.MatchDegree.ParamMatch =>
2170+
"have matching parameter types."
2171+
case Signature.MatchDegree.FullMatch =>
2172+
i"have the same$nameAnd type after erasure."
2173+
}
21662174
}
2167-
} else ""
2175+
else ""
21682176
def symLocation(sym: Symbol) = {
21692177
val lineDesc =
21702178
if (sym.span.exists && sym.span != sym.owner.span)

compiler/src/dotty/tools/dotc/transform/ExtensionMethods.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import SymDenotations._, Symbols._, StdNames._, Denotations._
1515
import TypeErasure.{ valueErasure, ErasedValueType }
1616
import NameKinds.{ExtMethName, UniqueExtMethName}
1717
import Decorators._
18+
import TypeUtils._
1819

1920
/**
2021
* Perform Step 1 in the inline classes SIP: Creates extension methods for all
@@ -190,11 +191,13 @@ object ExtensionMethods {
190191
val companion = imeth.owner.companionModule
191192
val companionInfo = companion.info
192193
val candidates = companionInfo.decl(extensionName(imeth)).alternatives
193-
val matching = candidates filter (c => FullParameterization.memberSignature(c.info) == imeth.signature)
194+
val matching =
195+
// See the documentation of `memberSignature` to understand why `.stripPoly.ensureMethodic` is needed here.
196+
candidates filter (c => FullParameterization.memberSignature(c.info) == imeth.info.stripPoly.ensureMethodic.signature)
194197
assert(matching.nonEmpty,
195198
i"""no extension method found for:
196199
|
197-
| $imeth:${imeth.info.show} with signature ${imeth.signature} in ${companion.moduleClass}
200+
| $imeth:${imeth.info.show} with signature ${imeth.info.signature} in ${companion.moduleClass}
198201
|
199202
| Candidates:
200203
|

compiler/src/dotty/tools/dotc/transform/FullParameterization.scala

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,16 @@ trait FullParameterization {
247247
object FullParameterization {
248248

249249
/** Assuming `info` is a result of a `fullyParameterizedType` call, the signature of the
250-
* original method type `X` such that `info = fullyParameterizedType(X, ...)`.
250+
* original method type `X` after stripping its leading type parameters section,
251+
* such that:
252+
* info.stripPoly.ensureMethodic = fullyParameterizedType(X, ...).stripPoly.ensureMethodic
253+
*
254+
* NOTE: Keeping the polymorphic part of the signature would be more precise,
255+
* but we cannot distinguish which type parameters of `info` are also type
256+
* parameters of`X`. This could be fixed by using a specific NameKind for the
257+
* extra type parameters, but that wouldn't help for extension methods
258+
* unpickled from Scala 2 (because Scala 2 extmeths phase happens before
259+
* pickling, which is maybe something we should change for 2.14).
251260
*/
252261
def memberSignature(info: Type)(implicit ctx: Context): Signature = info match {
253262
case info: PolyType =>

tests/pos/poly-signature.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
trait P[A] {
2+
def foo[T](x: Int): A = ???
3+
}
4+
5+
class C extends P[Int] {
6+
def foo(x: Int): Int = x
7+
}
8+
9+
object Test {
10+
def test(p: C): Unit = {
11+
p.foo(1)
12+
}
13+
}

0 commit comments

Comments
 (0)