Skip to content

Commit 9cbbee6

Browse files
Add overloading priority change warnings and documentation
1 parent 304156e commit 9cbbee6

11 files changed

+147
-130
lines changed

compiler/src/dotty/tools/dotc/ast/Trees.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1908,7 +1908,7 @@ object Trees {
19081908
case MethodTpe(_, _, x: MethodType) => !x.isImplicitMethod
19091909
case _ => true
19101910
}}
1911-
val alternatives = ctx.typer.resolveOverloaded()(allAlts, proto, receiver.srcPos)
1911+
val alternatives = ctx.typer.resolveOverloaded(allAlts, proto, receiver.srcPos)
19121912
assert(alternatives.size == 1,
19131913
i"${if (alternatives.isEmpty) "no" else "multiple"} overloads available for " +
19141914
i"$method on ${receiver.tpe.widenDealiasKeepAnnots} with targs: $targs%, %; args: $args%, %; expectedType: $expectedType." +

compiler/src/dotty/tools/dotc/typer/Applications.scala

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2112,15 +2112,71 @@ trait Applications extends Compatibility {
21122112
}
21132113
}
21142114

2115+
/** Overloadoing Priority Schemes, changing in `scala-3.7`.
2116+
*
2117+
* Old Scheme, until 3.7:
2118+
* - First, determines the _candidates_ (subset of the alternatives) by:
2119+
* + looking at the first paramater clause
2120+
* - Second, determine the best candidate by:
2121+
* + doing narrowMostSpecific on the first argument list;
2122+
* + if still ambigous and there is another parameter clause, then
2123+
* restarts narrowMostSpecific by mapping the problem onto the next parameter clause,
2124+
* but still considering all candidates.
2125+
*
2126+
* New Scheme, from 3.7
2127+
* - First, determines the candidates by:
2128+
* + looking at first paramater clause;
2129+
* + if still no determined and there is another parameter clause, then
2130+
* looks at the next argument lists to narrow the candidates (not restarting from alts).
2131+
* If that finds no alternative is applicable, fallback to the previous iteration
2132+
* (see comment in narrowByNextParamClause for details).
2133+
* - Second, determine the best candidate by, restoring all parameter clauses, and:
2134+
* + doing narrowMostSpecific on the first argument list;
2135+
* + if still ambigous and there is another parameter clause, then
2136+
* continue narrowMostSpecific by mapping the problem onto the next parameter clause,
2137+
* but only considering the alternatives found until now.
2138+
*/
2139+
enum ResolveScheme:
2140+
case Old
2141+
case New
2142+
def isNewPriority: Boolean = this == New
2143+
2144+
/** Resolve overloaded alternative `alts`, given expected type `pt`. Determines the current
2145+
* priority scheme and emits warnings for changes in resolution in migration version.
2146+
*/
2147+
def resolveOverloaded(alts: List[TermRef], pt: Type, srcPos: SrcPos)(using Context): List[TermRef] =
2148+
record("resolveOverloaded")
2149+
lazy val oldRes = resolveOverloaded(resolveOverloaded1(ResolveScheme.Old))(alts, pt)
2150+
lazy val newRes = resolveOverloaded(resolveOverloaded1(ResolveScheme.New))(alts, pt)
2151+
2152+
val sv = Feature.sourceVersion
2153+
val isNewPriorityVersion = sv.isAtLeast(SourceVersion.`3.7`)
2154+
val isWarnPriorityChangeVersion = sv == SourceVersion.`3.7-migration`
2155+
2156+
def doWarn(oldChoice: String, newChoice: String): Unit = report.warning(
2157+
em"""Overloading resolution for ${err.expectedTypeStr(pt)} between alternatives
2158+
| ${alts map (_.info)}%\n %
2159+
|has changed.
2160+
|Previous choice : $oldChoice
2161+
|New choice from Scala 3.7: $newChoice""", srcPos)
2162+
2163+
if isWarnPriorityChangeVersion then (oldRes, newRes) match
2164+
case (oldAlt :: Nil, newAlt :: Nil) if oldAlt != newAlt => doWarn(oldAlt.info.show, newAlt.info.show)
2165+
case (oldAlt :: Nil, Nil) => doWarn(oldAlt.info.show, "none")
2166+
case (Nil, newAlt :: Nil) => doWarn("none", newAlt.info.show)
2167+
case _ => // neither scheme has determined an alternative
2168+
2169+
if isNewPriorityVersion then newRes else oldRes
2170+
end resolveOverloaded
2171+
21152172
/** Resolve overloaded alternative `alts`, given expected type `pt`.
21162173
* Two trials: First, without implicits or SAM conversions enabled. Then,
21172174
* if the first finds no eligible candidates, with implicits and SAM conversions enabled.
21182175
* Each trial applies the `resolve` parameter.
21192176
*/
21202177
def resolveOverloaded
2121-
(resolve: (List[TermRef], Type) => Context ?=> List[TermRef] = resolveOverloaded1)
2122-
(alts: List[TermRef], pt: Type, srcPos: SrcPos = NoSourcePosition)(using Context): List[TermRef] =
2123-
record("resolveOverloaded")
2178+
(resolve: (List[TermRef], Type) => Context ?=> List[TermRef])
2179+
(alts: List[TermRef], pt: Type)(using Context): List[TermRef] =
21242180

21252181
/** Is `alt` a method or polytype whose result type after the first value parameter
21262182
* section conforms to the expected type `resultType`? If `resultType`
@@ -2157,7 +2213,7 @@ trait Applications extends Compatibility {
21572213
case Nil => chosen
21582214
case alt2 :: Nil => alt2
21592215
case alts2 =>
2160-
resolveOverloaded(resolve)(alts2, pt, srcPos) match {
2216+
resolveOverloaded(resolve)(alts2, pt) match {
21612217
case alt2 :: Nil => alt2
21622218
case _ => chosen
21632219
}
@@ -2224,7 +2280,7 @@ trait Applications extends Compatibility {
22242280
* It might be called twice from the public `resolveOverloaded` method, once with
22252281
* implicits and SAM conversions enabled, and once without.
22262282
*/
2227-
private def resolveOverloaded1(alts: List[TermRef], pt: Type)(using Context): List[TermRef] =
2283+
private def resolveOverloaded1(scheme: ResolveScheme)(alts: List[TermRef], pt: Type)(using Context): List[TermRef] =
22282284
trace(i"resolve over $alts%, %, pt = $pt", typr, show = true):
22292285
record(s"resolveOverloaded1", alts.length)
22302286

@@ -2414,7 +2470,7 @@ trait Applications extends Compatibility {
24142470
val alts3 = narrowByTrees(alts2, pt.typedArgs(normArg(alts2, _, _)), resultType)
24152471

24162472
resultType.deepenProto match
2417-
case resultType: FunOrPolyProto =>
2473+
case resultType: FunOrPolyProto if scheme.isNewPriority =>
24182474
narrowByNextParamClause(resolveCandidates)(alts3, pt.typedArgs(), resultType)
24192475
.fallbackTo(alts3) // see comment in narrowByNextParamClause
24202476
case _ =>
@@ -2472,35 +2528,31 @@ trait Applications extends Compatibility {
24722528
val deepPt = pt.deepenProto
24732529
deepPt match
24742530
case pt @ FunProto(_, resType: FunOrPolyProto) =>
2475-
narrowByNextParamClause(resolveOverloaded1)(found, pt.typedArgs(), resType)
2531+
val alts1 = if scheme.isNewPriority then found else candidates
2532+
narrowByNextParamClause(resolveOverloaded1(scheme))(alts1, pt.typedArgs(), resType)
24762533
case _ =>
24772534
// prefer alternatives that need no eta expansion
24782535
val noCurried = alts.filterConserve(!resultIsMethod(_))
24792536
val noCurriedCount = noCurried.length
24802537
if noCurriedCount == 1 then
24812538
noCurried
24822539
else if noCurriedCount > 1 && noCurriedCount < alts.length then
2483-
resolveOverloaded1(noCurried, pt)
2540+
resolveOverloaded1(scheme)(noCurried, pt)
24842541
else
24852542
// prefer alternatves that match without default parameters
24862543
val noDefaults = alts.filterConserve(!_.symbol.hasDefaultParams)
24872544
val noDefaultsCount = noDefaults.length
24882545
if noDefaultsCount == 1 then
24892546
noDefaults
24902547
else if noDefaultsCount > 1 && noDefaultsCount < alts.length then
2491-
resolveOverloaded1(noDefaults, pt)
2548+
resolveOverloaded1(scheme)(noDefaults, pt)
24922549
else if deepPt ne pt then
24932550
// try again with a deeper known expected type
2494-
resolveOverloaded1(alts, deepPt)
2551+
resolveOverloaded1(scheme)(alts, deepPt)
24952552
else
24962553
candidates
24972554
end resolveOverloaded2
24982555

2499-
// First, we find the candidates by considering all parameter clauses.
2500-
// Second, we determine the most specific again by considering all parameter clauses;
2501-
// but restarting from the 1st argument list.
2502-
// In both cases, considering subsequent argument lists only narrows the set of alternatives
2503-
// (i.e. we do retry from the complete list of alternative mapped onto there next param clause).
25042556
val candidates = resolveCandidates(alts, pt)
25052557
record("resolveOverloaded.narrowedApplicable", candidates.length)
25062558
resolveOverloaded2(candidates, pt)

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4108,7 +4108,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
41084108
def altRef(alt: SingleDenotation) = TermRef(ref.prefix, ref.name, alt)
41094109
val alts = altDenots.map(altRef)
41104110

4111-
resolveOverloaded()(alts, pt, tree.srcPos) match
4111+
resolveOverloaded(alts, pt, tree.srcPos) match
41124112
case alt :: Nil =>
41134113
readaptSimplified(tree.withType(alt))
41144114
case Nil =>
Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,7 @@
1-
-- [E007] Type Mismatch Error: tests/neg/multiparamlist-overload-3.6.scala:33:21 ---------------------------------------
2-
33 | val r = f(new B)(new A) // error since resolves to R2 in 3.7 (and 3.8) as expected
1+
-- [E007] Type Mismatch Error: tests/neg/multiparamlist-overload-3.6.scala:41:21 ---------------------------------------
2+
41 | val r = f(new B)(new A) // error: resolves to: R2 in 3.6, R1 in 3.7
33
| ^^^^^
44
| Found: A
55
| Required: B
66
|
77
| longer explanation available when compiling with `-explain`
8-
-- Warning: tests/neg/multiparamlist-overload-3.6.scala:20:10 ----------------------------------------------------------
9-
20 | val r = f(new B)(new C) // resolves to: R1 in 3.7, R3 in 3.8
10-
| ^
11-
| Overloading resolution for arguments (B)(C) between alternatives
12-
| (x: B)(y: B): R3
13-
| (x: B)(y: A): R2
14-
| (x: A)(y: C): R1
15-
| will change.
16-
| Current choice : (x: A)(y: C): R1
17-
| New choice from Scala 3.7: (x: B)(y: B): R3
18-
-- Warning: tests/neg/multiparamlist-overload-3.6.scala:40:12 ----------------------------------------------------------
19-
40 | val r = f(new B)(new A) // resolves to: R1 in 3.7, R2 in 3.8 as in Part1
20-
| ^
21-
| Overloading resolution for arguments (B)(A) between alternatives
22-
| (x: B)(y: C): R3
23-
| (x: B)(y: B): R2
24-
| (x: A)(y: A): R1
25-
| will change.
26-
| Current choice : (x: A)(y: A): R1
27-
| New choice from Scala 3.7: none
Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import scala.language.`3.7`
1+
import scala.language.`3.6`
22

33
class A
44
class B extends A
@@ -8,36 +8,37 @@ class R1
88
class R2
99
class R3
1010

11-
// The alternatives are ordered from most genereal to most specific in each test,
11+
// In each test, the alternatives are defined from most genereal to most specific,
1212
// with respect to a lexicographic ordering by parameter list.
13-
13+
// In each of the cases tested here, scala-3.7 resolves to the most specific applicable alternative.
14+
// See tests/pos/multiparamlist-overload-3.7.scala for the comparison.
1415

1516
object Test1:
1617
def f(x: A)(y: C) = new R1
1718
def f(x: B)(y: A) = new R2
1819
def f(x: B)(y: B) = new R3
1920

20-
val r = f(new B)(new C) // resolves to: R1 in 3.7, R3 in 3.8
21+
val r = f(new B)(new C) // all alternatives are applicable; resolves to: R1 in 3.6, R3 in 3.7
2122
val _: R1 = r
2223
end Test1
2324

24-
2525
object Test2:
2626
// R1 is the only applicable alternative in both parts
27-
// but it is only resolved to in Part2 by adding (an unapplicable) R3
27+
// but (in 3.7) it is only resolved to in Part1 by adding (an unapplicable) R3
2828

2929
object Part1:
3030
def f(x: A)(y: A) = new R1
3131
def f(x: B)(y: B) = new R2
32+
def f(x: B)(y: C) = new R3
3233

33-
val r = f(new B)(new A) // error since resolves to R2 in 3.7 (and 3.8) as expected
34+
val r = f(new B)(new A) // resolves to: R1 in 3.6, R1 in 3.7
35+
val _: R1 = r
3436

3537
object Part2:
3638
def f(x: A)(y: A) = new R1
3739
def f(x: B)(y: B) = new R2
38-
def f(x: B)(y: C) = new R3
3940

40-
val r = f(new B)(new A) // resolves to: R1 in 3.7, R2 in 3.8 as in Part1
41-
val _: R1 = r
41+
val r = f(new B)(new A) // error: resolves to: R2 in 3.6, R1 in 3.7
42+
val _: R2 = r
4243

4344
end Test2

tests/neg/multiparamlist-overload-3.7.check

Lines changed: 0 additions & 25 deletions
This file was deleted.

tests/neg/multiparamlist-overload-3.7.scala

Lines changed: 0 additions & 42 deletions
This file was deleted.

tests/warn/scalatest-overload-3.7.scala renamed to tests/pos/scalatest-overload-3.7.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ class Matchers:
1919

2020
class Test extends Matchers:
2121
def test(): Unit =
22-
"hello world" should endWith ("world") // warn: overloading resolution change
22+
"hello world" should endWith ("world") // no overloading resolution change
23+
// related to tests/warn/multiparamlist-overload-3.7.scala
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-- Warning: tests/warn/multiparamlist-overload-3.7.scala:21:10 ---------------------------------------------------------
2+
21 | val r = f(new B)(new C) // warn: all alternatives are applicable; resolves to: R1 in 3.6, R3 in 3.7
3+
| ^
4+
| Overloading resolution for arguments (B)(C) between alternatives
5+
| (x: B)(y: B): R3
6+
| (x: B)(y: A): R2
7+
| (x: A)(y: C): R1
8+
| has changed.
9+
| Previous choice : (x: A)(y: C): R1
10+
| New choice from Scala 3.7: (x: B)(y: B): R3
11+
-- Warning: tests/warn/multiparamlist-overload-3.7.scala:41:12 ---------------------------------------------------------
12+
41 | val r = f(new B)(new A) // warn: resolves to: R2 in 3.6, R1 in 3.7
13+
| ^
14+
| Overloading resolution for arguments (B)(A) between alternatives
15+
| (x: B)(y: B): R2
16+
| (x: A)(y: A): R1
17+
| has changed.
18+
| Previous choice : (x: B)(y: B): R2
19+
| New choice from Scala 3.7: (x: A)(y: A): R1
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import scala.language.`3.7-migration`
2+
3+
class A
4+
class B extends A
5+
class C extends B
6+
7+
class R1
8+
class R2
9+
class R3
10+
11+
// In each test, the alternatives are defined from most genereal to most specific,
12+
// with respect to a lexicographic ordering by parameter list.
13+
// In each of the cases tested here, scala-3.7 resolves to the most specific applicable alternative.
14+
// See tests/neg/multiparamlist-overload-3.6.scala for the comparison.
15+
16+
object Test1:
17+
def f(x: A)(y: C) = new R1
18+
def f(x: B)(y: A) = new R2
19+
def f(x: B)(y: B) = new R3
20+
21+
val r = f(new B)(new C) // warn: all alternatives are applicable; resolves to: R1 in 3.6, R3 in 3.7
22+
val _: R3 = r
23+
end Test1
24+
25+
object Test2:
26+
// R1 is the only applicable alternative in both parts
27+
// but (in 3.7) it is only resolved to in Part1 by adding (an unapplicable) R3
28+
29+
object Part1:
30+
def f(x: A)(y: A) = new R1
31+
def f(x: B)(y: B) = new R2
32+
def f(x: B)(y: C) = new R3
33+
34+
val r = f(new B)(new A) // resolves to: R1 in 3.6, R1 in 3.7
35+
val _: R1 = r
36+
37+
object Part2:
38+
def f(x: A)(y: A) = new R1
39+
def f(x: B)(y: B) = new R2
40+
41+
val r = f(new B)(new A) // warn: resolves to: R2 in 3.6, R1 in 3.7
42+
val _: R1 = r
43+
44+
end Test2

tests/warn/scalatest-overload-3.7.check

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)