Skip to content

Commit 8c66af2

Browse files
committed
Overloading: always prefer non-varargs to varargs
We recently merged scala#9601 which unified our handling of `Object` coming from Java methods, an unintended consequence of that change is that some existing Java APIs can no longer be called without running into ambiguity errors, for example log4j defines two overloads for `Logger.error`: (x: String, y: Object): Unit (x: String, y: Object*): Unit previously we translated `Object` to `Any` but left `Object*` alone, now they're both treated the same way (translated to a special alias of `Object`) and so neither method ends up being more specific than the other, so `error("foo: {}, 1)` is now ambiguous. Clearly the problem lies with how we handle varargs in overloading resolution, but this has been a source of issues for years with no clear resolution: - scala/bug#8342 - scala/bug#4728 - scala/bug#8344 - scala#6230 This PR cuts the Gordian knot by simply declaring that non-varargs methods are always more specific than varargs. This has several advantages: - It's an easy rule to remember - It matches what Java does (see https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.12.2) - It avoids unnecessary wrapping of arguments The downside is that it doesn't match what Scala 2 does, but our current behavior isn't a perfect match either (also it seems that Scala 2 handles Java varargs and Scala varargs differently in overloading resolution which is another source of complexity best avoided, see `tests/run/overload_repeated`).
1 parent ac850b9 commit 8c66af2

File tree

4 files changed

+96
-36
lines changed

4 files changed

+96
-36
lines changed

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

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,48 +1452,54 @@ trait Applications extends Compatibility {
14521452
/** Is alternative `alt1` with type `tp1` as specific as alternative
14531453
* `alt2` with type `tp2` ?
14541454
*
1455-
* 1. A method `alt1` of type (p1: T1, ..., pn: Tn)U is as specific as `alt2`
1455+
* 1. A non-varargs method is always as specific as a varargs method.
1456+
* 2. A varargs method is never as specific as a non-varargs method.
1457+
* 3. A method `alt1` of type (p1: T1, ..., pn: Tn)U is as specific as `alt2`
14561458
* if `alt2` is applicable to arguments (p1, ..., pn) of types T1,...,Tn
14571459
* or if `alt1` is nullary.
1458-
* 2. A polymorphic member of type [a1 >: L1 <: U1, ..., an >: Ln <: Un]T is as
1460+
* 4. A polymorphic member of type [a1 >: L1 <: U1, ..., an >: Ln <: Un]T is as
14591461
* specific as `alt2` of type `tp2` if T is as specific as `tp2` under the
14601462
* assumption that for i = 1,...,n each ai is an abstract type name bounded
14611463
* from below by Li and from above by Ui.
1462-
* 3. A member of any other type `tp1` is:
1464+
* 5. A member of any other type `tp1` is:
14631465
* a. always as specific as a method or a polymorphic method.
14641466
* b. as specific as a member of any other type `tp2` if `tp1` is compatible
14651467
* with `tp2`.
14661468
*/
1467-
def isAsSpecific(alt1: TermRef, tp1: Type, alt2: TermRef, tp2: Type): Boolean = trace(i"isAsSpecific $tp1 $tp2", overload) { tp1 match {
1468-
case tp1: MethodType => // (1)
1469-
val formals1 =
1470-
if (tp1.isVarArgsMethod && tp2.isVarArgsMethod) tp1.paramInfos.map(_.repeatedToSingle)
1471-
else tp1.paramInfos
1472-
isApplicableMethodRef(alt2, formals1, WildcardType) ||
1473-
tp1.paramInfos.isEmpty && tp2.isInstanceOf[LambdaType]
1474-
case tp1: PolyType => // (2)
1475-
inContext(ctx.fresh.setExploreTyperState()) {
1476-
// Fully define the PolyType parameters so that the infos of the
1477-
// tparams created below never contain TypeRefs whose underling types
1478-
// contain uninstantiated TypeVars, this could lead to cycles in
1479-
// `isSubType` as a TypeVar might get constrained by a TypeRef it's
1480-
// part of.
1481-
val tp1Params = tp1.newLikeThis(tp1.paramNames, tp1.paramInfos, defn.AnyType)
1482-
fullyDefinedType(tp1Params, "type parameters of alternative", alt1.symbol.span)
1483-
1484-
val tparams = newTypeParams(alt1.symbol, tp1.paramNames, EmptyFlags, tp1.instantiateParamInfos(_))
1485-
isAsSpecific(alt1, tp1.instantiate(tparams.map(_.typeRef)), alt2, tp2)
1486-
}
1487-
case _ => // (3)
1488-
tp2 match {
1489-
case tp2: MethodType => true // (3a)
1490-
case tp2: PolyType if tp2.resultType.isInstanceOf[MethodType] => true // (3a)
1491-
case tp2: PolyType => // (3b)
1492-
explore(isAsSpecificValueType(tp1, constrained(tp2).resultType))
1493-
case _ => // (3b)
1494-
isAsSpecificValueType(tp1, tp2)
1495-
}
1496-
}}
1469+
def isAsSpecific(alt1: TermRef, tp1: Type, alt2: TermRef, tp2: Type): Boolean = trace(i"isAsSpecific $tp1 $tp2", overload) {
1470+
val tp1IsVarArgs = tp1.isVarArgsMethod
1471+
val tp2IsVarArgs = tp2.isVarArgsMethod
1472+
if !tp1IsVarArgs && tp2IsVarArgs then true // (1)
1473+
else if tp1IsVarArgs && !tp2IsVarArgs then false // (2)
1474+
else tp1 match
1475+
case tp1: MethodType => // (3)
1476+
val formals1 =
1477+
if tp1IsVarArgs && tp2IsVarArgs then tp1.paramInfos.map(_.repeatedToSingle)
1478+
else tp1.paramInfos
1479+
isApplicableMethodRef(alt2, formals1, WildcardType) ||
1480+
tp1.paramInfos.isEmpty && tp2.isInstanceOf[LambdaType]
1481+
case tp1: PolyType => // (4)
1482+
inContext(ctx.fresh.setExploreTyperState()) {
1483+
// Fully define the PolyType parameters so that the infos of the
1484+
// tparams created below never contain TypeRefs whose underling types
1485+
// contain uninstantiated TypeVars, this could lead to cycles in
1486+
// `isSubType` as a TypeVar might get constrained by a TypeRef it's
1487+
// part of.
1488+
val tp1Params = tp1.newLikeThis(tp1.paramNames, tp1.paramInfos, defn.AnyType)
1489+
fullyDefinedType(tp1Params, "type parameters of alternative", alt1.symbol.span)
1490+
1491+
val tparams = newTypeParams(alt1.symbol, tp1.paramNames, EmptyFlags, tp1.instantiateParamInfos(_))
1492+
isAsSpecific(alt1, tp1.instantiate(tparams.map(_.typeRef)), alt2, tp2)
1493+
}
1494+
case _ => // (5)
1495+
tp2 match
1496+
case tp2: MethodType => true // (5a)
1497+
case tp2: PolyType if tp2.resultType.isInstanceOf[MethodType] => true // (5a)
1498+
case tp2: PolyType => // (5b)
1499+
explore(isAsSpecificValueType(tp1, constrained(tp2).resultType))
1500+
case _ => // (5b)
1501+
isAsSpecificValueType(tp1, tp2)
1502+
}
14971503

14981504
/** Test whether value type `tp1` is as specific as value type `tp2`.
14991505
* Let's abbreviate this to `tp1 <:s tp2`.

tests/run/overload_repeated/A_1.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
class A_1 {
2+
public static int foo1(String x) { return 1; }
3+
public static int foo1(Object... x) { return 2; }
4+
5+
public static int foo2(Object x) { return 1; }
6+
public static int foo2(Object... x) { return 2; }
7+
8+
public static <T> int foo3(T x) { return 1; }
9+
public static <T> int foo3(T... x) { return 2; }
10+
11+
public static <T> int foo4(T x) { return 1; }
12+
public static <T> int foo4(T x, T... y) { return 2; }
13+
14+
public static boolean check() {
15+
// Java prefers non-varargs to varargs:
16+
// https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.12.2
17+
return
18+
foo1("") == 1 &&
19+
foo2("") == 1 &&
20+
foo3("") == 1 &&
21+
foo4("") == 1;
22+
}
23+
}

tests/run/overload_repeated/B_2.scala

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
object Test {
2+
def bar1(x: Any) = 1
3+
def bar1(x: String*) = 2
4+
5+
def bar2(x: Any) = 1
6+
def bar2(x: Any*) = 2
7+
8+
def bar3[T](x: T): Int = 1
9+
def bar3[T](x: T*): Int = 2
10+
11+
def bar4[T](x: T): Int = 1
12+
def bar4[T](x: T, xs: T*): Int = 2
13+
14+
def main(args: Array[String]): Unit = {
15+
// Java, Scala 2 and Dotty all agree that Java varargs are
16+
// less specific than Java non-varargs:
17+
assert(A_1.check())
18+
assert(A_1.foo1("") == 1) // Same as in Java and Scala 2
19+
assert(A_1.foo2("") == 1) // Same as in Java and Scala 2
20+
assert(A_1.foo3("") == 1) // Same as in Java and Scala 2
21+
assert(A_1.foo4("") == 1) // Same as in Java and Scala 2
22+
23+
// ... but Scala 2 seems to treat Scala varargs specially
24+
// (or maybe it's Object coming from Java which is treated
25+
// specially), in Dotty this doesn't make a difference:
26+
assert(bar1("") == 1) // ambiguous in Scala 2
27+
assert(bar2("") == 1) // same in Scala 2
28+
assert(bar3("") == 1) // same in Scala 2
29+
assert(bar4("") == 1) // same in Scala 2
30+
}
31+
}

tests/run/t8197.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class Foo(val x: A = null) {
1010

1111
object Test extends App {
1212
// both constructors of `Foo` are applicable. Overloading resolution
13-
// will eliminate the alternative that uses a default argument, therefore
14-
// the vararg constructor is chosen.
15-
assert((new Foo).x != null)
13+
// used to select the varargs alternative, but we now always
14+
// prefer non-varargs to varargs overloads.
15+
assert((new Foo).x == null)
1616
}

0 commit comments

Comments
 (0)