From 649a6ac7e0a50bf2292547fe5cce6150a91ca1a0 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 30 Mar 2021 11:12:22 +0200 Subject: [PATCH 1/3] Treat functional interfaces specially in overloading resolution Overloading resolution had two stages to determine when an alternative is applicable" 1. Match with subtyping only 2. Match with implicit conversions and SAM conversions If some alternatives are eligible under (1), only those alternatives are considered. If no alternatives are eligible under (1), alternatives are searched using (2). This behavior is different from Scala 2, which seems to use SAM conversions more aggressively. I am not sure what Scala 2 does. One obvious change would be to allow SAM conversions under (1). But I am reluctant to do that, since a SAM conversion can easily be introduced by accident. For instance, we might have two overloaded variants like this: ```scala def foo(x: C) = ... def foo(x: A => B) = ... foo((x: A) => x.toB) ``` Now, if `C` happens to be a SAM type that has an abstract method from A to B, this would be ambiguous. Generally, it feels like a SAM conversion could too easily be considered by accident here. On the other hand, if `C` was marked with `@FunctionalInterface` it would explicitly expect function arguments, so then we should treat it as morally equivalent to a function type in Scala. This is what this commit does. It refines the priority for searching applicable methods as follows: 1. Match with subtyping and with SAM conversions to functional interfaces 2. Match with implicit conversions and SAM conversions to other types --- .../dotty/tools/dotc/typer/Applications.scala | 17 +++++++++-------- tests/run/i11938.scala | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 tests/run/i11938.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 48a5738b4074..7961f74035e8 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -635,15 +635,16 @@ trait Applications extends Compatibility { // matches expected type false case argtpe => - def SAMargOK = formal match { - case SAMType(sam) => argtpe <:< sam.toFunctionType(isJava = formal.classSymbol.is(JavaDefined)) - case _ => false - } + def SAMargOK(onlyFunctionalInterface: Boolean) = + (!onlyFunctionalInterface || formal.classSymbol.hasAnnotation(defn.FunctionalInterfaceAnnot)) + && formal.match + case SAMType(sam) => argtpe <:< sam.toFunctionType(isJava = formal.classSymbol.is(JavaDefined)) + case _ => false if argMatch == ArgMatch.SubType then - argtpe relaxed_<:< formal.widenExpr + (argtpe relaxed_<:< formal.widenExpr) || SAMargOK(onlyFunctionalInterface = true) else isCompatible(argtpe, formal) - || ctx.mode.is(Mode.ImplicitsEnabled) && SAMargOK + || ctx.mode.is(Mode.ImplicitsEnabled) && SAMargOK(onlyFunctionalInterface = false) || argMatch == ArgMatch.CompatibleCAP && { val argtpe1 = argtpe.widen @@ -1877,12 +1878,12 @@ trait Applications extends Compatibility { record("resolveOverloaded.FunProto", alts.length) val alts1 = narrowBySize(alts) - //report.log(i"narrowed by size: ${alts1.map(_.symbol.showDcl)}%, %") + overload.println(i"narrowed by size: ${alts1.map(_.symbol.showDcl)}%, %") if isDetermined(alts1) then alts1 else record("resolveOverloaded.narrowedBySize", alts1.length) val alts2 = narrowByShapes(alts1) - //report.log(i"narrowed by shape: ${alts2.map(_.symbol.showDcl)}%, %") + overload.println(i"narrowed by shape: ${alts2.map(_.symbol.showDcl)}%, %") if isDetermined(alts2) then alts2 else record("resolveOverloaded.narrowedByShape", alts2.length) diff --git a/tests/run/i11938.scala b/tests/run/i11938.scala new file mode 100644 index 000000000000..d68d633932d9 --- /dev/null +++ b/tests/run/i11938.scala @@ -0,0 +1,14 @@ +import java.util.function.Function + +class Future[T](val initial: T) { + def map[V](v: V): Unit = println(v) + //def map(v: String): Unit = println(v) + def map[U](fn: Function[T, U]): Unit = println(fn(initial)) +} + +object Test { + val f = new Future(42) + val fn = (i: Int) => i.toString + def main(args: Array[String]): Unit = + f.map((i: Int) => i.toString) +} \ No newline at end of file From 7508d41c7caf150314c6418b600c4db1b665945f Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 3 Apr 2021 11:57:42 +0200 Subject: [PATCH 2/3] More tests --- tests/neg/i11899.scala | 10 ++++++++++ tests/run/i11938.scala | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 tests/neg/i11899.scala diff --git a/tests/neg/i11899.scala b/tests/neg/i11899.scala new file mode 100644 index 000000000000..3ae8fc496bfa --- /dev/null +++ b/tests/neg/i11899.scala @@ -0,0 +1,10 @@ +import java.util.function.Function + +class Future[T](val initial: T) { + def map[V](v: V): Unit = println(v) + def map[U](fn: Function[T, U]): Unit = println(fn(initial)) +} + +val f = new Future(42) +val fn = (i: Int) => i.toString +def test = f.map(fn) // error diff --git a/tests/run/i11938.scala b/tests/run/i11938.scala index d68d633932d9..1f8acd7fc2fc 100644 --- a/tests/run/i11938.scala +++ b/tests/run/i11938.scala @@ -8,7 +8,8 @@ class Future[T](val initial: T) { object Test { val f = new Future(42) - val fn = (i: Int) => i.toString + def fn(i: Int) = i.toString def main(args: Array[String]): Unit = f.map((i: Int) => i.toString) + f.map(fn) } \ No newline at end of file From 3c88790e06cabf53c98bfa40e7d2f809dc150b5f Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 5 Apr 2021 11:42:51 +0200 Subject: [PATCH 3/3] Different algorithm for handling SAMs in overloading Here we always try SAM conversions alongside subtyping but when comparing alternatives we prefer normal subtyping over SAM conversions. Unfortunately, this also fails zio --- .../dotty/tools/dotc/typer/Applications.scala | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 7961f74035e8..e1c18fd24cbd 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -614,7 +614,8 @@ trait Applications extends Compatibility { /** The degree to which an argument has to match a formal parameter */ enum ArgMatch: - case SubType // argument is a relaxed subtype of formal + case SubType // argument is a relaxed subtype of formal, SAM conversions are also allowed + case Comparing // same as compatible, subtyping in one direction disables SAM conversion in the other case Compatible // argument is compatible with formal case CompatibleCAP // capture-converted argument is compatible with formal @@ -635,22 +636,26 @@ trait Applications extends Compatibility { // matches expected type false case argtpe => - def SAMargOK(onlyFunctionalInterface: Boolean) = - (!onlyFunctionalInterface || formal.classSymbol.hasAnnotation(defn.FunctionalInterfaceAnnot)) - && formal.match - case SAMType(sam) => argtpe <:< sam.toFunctionType(isJava = formal.classSymbol.is(JavaDefined)) - case _ => false - if argMatch == ArgMatch.SubType then - (argtpe relaxed_<:< formal.widenExpr) || SAMargOK(onlyFunctionalInterface = true) - else - isCompatible(argtpe, formal) - || ctx.mode.is(Mode.ImplicitsEnabled) && SAMargOK(onlyFunctionalInterface = false) - || argMatch == ArgMatch.CompatibleCAP - && { - val argtpe1 = argtpe.widen - val captured = captureWildcards(argtpe1) - (captured ne argtpe1) && isCompatible(captured, formal.widenExpr) - } + val argtpe1 = argtpe.widen + + def isSAMargOK = + defn.isFunctionType(argtpe1) + && { + formal match + case SAMType(sam) => argtpe1 <:< sam.toFunctionType(isJava = formal.classSymbol.is(JavaDefined)) + case _ => false + } + && (argMatch != ArgMatch.Comparing || !(formal.widenExpr relaxed_<:< argtpe1)) + + ( if argMatch == ArgMatch.SubType then argtpe relaxed_<:< formal.widenExpr + else isCompatible(argtpe, formal) ) + || isSAMargOK//.showing(i"sam $argtpe, $formal, $argMatch = $result, ${formal.widenExpr relaxed_<:< argtpe1}") + || argMatch == ArgMatch.CompatibleCAP + && { + val captured = captureWildcards(argtpe1) + (captured ne argtpe1) && isCompatible(captured, formal.widenExpr) + } + end argOK /** The type of the given argument */ protected def argType(arg: Arg, formal: Type): Type @@ -1486,9 +1491,9 @@ trait Applications extends Compatibility { || { if tp1.isVarArgsMethod then tp2.isVarArgsMethod - && isApplicableMethodRef(alt2, tp1.paramInfos.map(_.repeatedToSingle), WildcardType, ArgMatch.Compatible) + && isApplicableMethodRef(alt2, tp1.paramInfos.map(_.repeatedToSingle), WildcardType, ArgMatch.Comparing) else - isApplicableMethodRef(alt2, tp1.paramInfos, WildcardType, ArgMatch.Compatible) + isApplicableMethodRef(alt2, tp1.paramInfos, WildcardType, ArgMatch.Comparing) } case tp1: PolyType => // (2) inContext(ctx.fresh.setExploreTyperState()) {