Skip to content

Commit 4520117

Browse files
committed
Eta-expand 0-arity method if expected type is Function0
Reverse course from the deprecation introduced in 2.12. (4.3) condition for eta-expansion by -Xsource level: - until 2.13: - for arity > 0: function or sam type is expected - for arity == 0: Function0 is expected -- SAM types do not eta-expand because it could be an accidental SAM scala/bug#9489 - 2.14: - for arity > 0: unconditional - for arity == 0: a function-ish type of arity 0 is expected (including SAM) warnings: - 2.12: eta-expansion of zero-arg methods was deprecated (scala/bug#7187) - 2.13: deprecation dropped in favor of setting the scene for uniform eta-expansion in 3.0 warning still available under -Xlint:eta-zero - 2.14: expected type is a SAM that is not annotated with `@FunctionalInterface` if it's a Java-defined interface (under `-Xlint:eta-sam`) (4.2) condition for auto-application by -Xsource level: - until 2.14: none (assuming condition for (4.3) was not met) - in 3.0: `meth.isJavaDefined` TODO decide -- currently the condition is more involved to give slack to Scala methods overriding Java-defined ones; I think we should resolve that by introducing slack in overriding e.g. a Java-defined `def toString()` by a Scala-defined `def toString`. This also works better for dealing with accessors overriding Java-defined methods. The current strategy in methodSig is problematic: > // Add a () parameter section if this overrides some method with () parameters > val vparamSymssOrEmptyParamsFromOverride = This means an accessor that overrides a Java-defined method gets a MethodType instead of a `NullaryMethodType`, which breaks lots of assumptions about accessors
1 parent eee223b commit 4520117

File tree

14 files changed

+194
-99
lines changed

14 files changed

+194
-99
lines changed

project/ScalaOptionParser.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ object ScalaOptionParser {
108108
"-g" -> List("line", "none", "notailcails", "source", "vars"),
109109
"-target" -> List("jvm-1.5", "jvm-1.6", "jvm-1.7", "jvm-1.8"))
110110
private def multiChoiceSettingNames = Map[String, List[String]](
111-
"-Xlint" -> List("adapted-args", "nullary-unit", "inaccessible", "nullary-override", "infer-any", "missing-interpolator", "doc-detached", "private-shadow", "type-parameter-shadow", "poly-implicit-overload", "option-implicit", "delayedinit-select", "package-object-classes", "stars-align", "constant", "unused"),
111+
"-Xlint" -> List("adapted-args", "nullary-unit", "inaccessible", "nullary-override", "infer-any", "missing-interpolator", "doc-detached", "private-shadow", "type-parameter-shadow", "poly-implicit-overload", "option-implicit", "delayedinit-select", "package-object-classes", "stars-align", "constant", "unused", "eta-zero"),
112112
"-language" -> List("help", "_", "dynamics", "postfixOps", "reflectiveCalls", "implicitConversions", "higherKinds", "existentials", "experimental.macros"),
113113
"-opt" -> List("unreachable-code", "simplify-jumps", "compact-locals", "copy-propagation", "redundant-casts", "box-unbox", "nullness-tracking", "closure-invocations" , "allow-skip-core-module-init", "assume-modules-non-null", "allow-skip-class-loading", "inline", "l:none", "l:default", "l:method", "l:inline", "l:project", "l:classpath"),
114114
"-Ywarn-unused" -> List("imports", "patvars", "privates", "locals", "explicits", "implicits", "params"),

spec/06-expressions.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,16 +1397,19 @@ type $T$ by evaluating the expression to which $m$ is bound.
13971397
If the method takes only implicit parameters, implicit
13981398
arguments are passed following the rules [here](07-implicits.html#implicit-parameters).
13991399

1400-
###### Empty Application
1401-
Otherwise, if $e$ has method type $()T$, it is implicitly applied to the empty
1402-
argument list, yielding $e()$.
1403-
14041400
###### Eta Expansion
14051401
Otherwise, if the method is not a constructor,
1406-
and the expected type $\mathit{pt}$ is a function type (potentially after sam conversion)
1402+
and the expected type $\mathit{pt}$ is a function type, or,
1403+
for methods of non-zero arity, a type [sam-convertible](#sam-conversion) to a function type,
14071404
$(\mathit{Ts}') \Rightarrow T'$, [eta-expansion](#eta-expansion)
14081405
is performed on the expression $e$.
14091406

1407+
(The exception for zero-arity methods is to avoid surprises due to unexpected sam conversion.)
1408+
1409+
###### Empty Application
1410+
Otherwise, if $e$ has method type $()T$, it is implicitly applied to the empty
1411+
argument list, yielding $e()$.
1412+
14101413
### Overloading Resolution
14111414

14121415
If an identifier or selection $e$ references several members of a

src/compiler/scala/tools/nsc/settings/Warnings.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ trait Warnings {
109109
val ImplicitNotFound = LintWarning("implicit-not-found", "Check @implicitNotFound and @implicitAmbiguous messages.")
110110
val Serial = LintWarning("serial", "@SerialVersionUID on traits and non-serializable classes.")
111111
val ValPattern = LintWarning("valpattern", "Enable pattern checks in val definitions.")
112+
val EtaZero = LintWarning("eta-zero", "Warn on eta-expansion (rather than auto-application) of zero-ary method.")
113+
val EtaSam = LintWarning("eta-sam", "Warn on eta-expansion to meet a Java-defined functional interface that is not explicitly annotated with @FunctionalInterface.")
112114

113115
def allLintWarnings = values.toSeq.asInstanceOf[Seq[LintWarning]]
114116
}
@@ -134,6 +136,8 @@ trait Warnings {
134136
def lintImplicitNotFound = lint contains ImplicitNotFound
135137
def warnSerialization = lint contains Serial
136138
def lintValPatterns = lint contains ValPattern
139+
def warnEtaZero = lint contains EtaZero
140+
def warnEtaSam = lint contains EtaSam
137141

138142
// The Xlint warning group.
139143
val lint = MultiChoiceSetting(

src/compiler/scala/tools/nsc/typechecker/Typers.scala

Lines changed: 102 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -785,11 +785,9 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
785785
* store these instances in context.undetparams,
786786
* unless followed by explicit type application.
787787
* (4) Do the following to unapplied methods used as values:
788-
* (4.1) If the method has only implicit parameters pass implicit arguments
789-
* (4.2) otherwise, if the method is nullary with a result type compatible to `pt`
790-
* and it is not a constructor, apply it to ()
791-
* (4.3) otherwise, if `pt` is a function type and method is not a constructor,
792-
* convert to function by eta-expansion,
788+
* (4.1) If the method has only implicit parameters, pass implicit arguments (see adaptToImplicitMethod)
789+
* (4.2) otherwise, if the method is 0-ary and it can be auto-applied (see checkCanAutoApply), apply it to ()
790+
* (4.3) otherwise, if the method is not a constructor, and can be eta-expanded (see checkCanEtaExpand), eta-expand
793791
* otherwise issue an error
794792
* (5) Convert constructors in a pattern as follows:
795793
* (5.1) If constructor refers to a case class factory, set tree's type to the unique
@@ -880,46 +878,109 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
880878
)
881879
}
882880

883-
def instantiateToMethodType(mt: MethodType): Tree = {
884-
val meth = tree match {
885-
// a partial named application is a block (see comment in EtaExpansion)
886-
//
887-
// TODO: document why we need to peel off one layer to begin with, and, if we do, why we don't need to recurse??
888-
// (and why we don't need to look at the stats to make sure it's the block created by eta-expansion)
889-
// I don't see how we call etaExpand on a constructor..
890-
//
891-
// I guess we don't need to recurse because the outer block and a nested block will refer to the same method
892-
// after eta-expansion (if that's what generated these blocks), so we can just look at the outer one.
893-
//
894-
// How about user-written blocks? Can they ever have a MethodType?
895-
case Block(_, tree1) => tree1.symbol
896-
case _ => tree.symbol
881+
def adaptMethodTypeToExpr(mt: MethodType): Tree = {
882+
val meth =
883+
tree match {
884+
// a partial named application is a block (see comment in EtaExpansion)
885+
// How about user-written blocks? Can they ever have a MethodType?
886+
case Block(_, tree1) => tree1.symbol
887+
case _ => tree.symbol
888+
}
889+
890+
891+
val arity = mt.params.length
892+
893+
val sourceLevel2_14 = currentRun.isScala214
894+
895+
def warnTree = original orElse tree
896+
897+
def warnEtaSam() = {
898+
val sam = samOf(pt)
899+
val samClazz = sam.owner
900+
// TODO: we allow a Java class as a SAM type, whereas Java only allows the @FunctionalInterface on interfaces -- align?
901+
if (sam.exists && (!samClazz.hasFlag(JAVA) || samClazz.hasFlag(INTERFACE)) && !samClazz.hasAnnotation(definitions.FunctionalInterfaceClass))
902+
reporter.warning(tree.pos, s"Eta-expansion performed to meet expected type $pt, which is SAM-equivalent to ${samToFunctionType(pt)},\n" +
903+
s"even though ${samClazz} is not annotated with `@FunctionalInterface`;\n" +
904+
s"to suppress warning, add the annotation or write out the equivalent function literal.")
897905
}
898906

899-
def cantAdapt =
900-
if (context.implicitsEnabled) MissingArgsForMethodTpeError(tree, meth) else UnstableTreeError(tree)
907+
// note that isFunctionProto(pt) does not work properly for Function0
908+
lazy val ptUnderlying =
909+
(pt match {
910+
case oapt: OverloadedArgProto => oapt.underlying
911+
case pt => pt
912+
}).dealiasWiden
901913

902-
// constructors do not eta-expand
903-
if (meth.isConstructor) cantAdapt
904-
// (4.2) apply to empty argument list
905-
else if (mt.params.isEmpty && (settings.isScala213 || !isFunctionType(pt))) {
906-
// Starting with 2.13, always insert `()`, regardless of expected type.
907-
// On older versions, expected type must not be a FunctionN (can be a SAM)
908-
//
909-
// scala/bug#7187 deprecates eta-expansion of zero-arg method values (since 2.12; was never introduced for SAM types: scala/bug#9536).
910-
// The next step is to also deprecate insertion of `()` (except for java-defined methods), as dotty already requires explicitly writing them.
911-
// Once explicit application to () is required, we can more aggressively eta-expand method references, even if they are 0-arity
912-
adapt(typed(Apply(tree, Nil) setPos tree.pos), mode, pt, original)
914+
// (4.3) condition for eta-expansion by -Xsource level
915+
//
916+
// until 2.13:
917+
// - for arity > 0: function or sam type is expected
918+
// - for arity == 0: Function0 is expected -- SAM types do not eta-expand because it could be an accidental SAM scala/bug#9489
919+
// 2.14:
920+
// - for arity > 0: unconditional
921+
// - for arity == 0: a function-ish type of arity 0 is expected (including SAM)
922+
//
923+
// warnings:
924+
// - 2.12: eta-expansion of zero-arg methods was deprecated (scala/bug#7187)
925+
// - 2.13: deprecation dropped in favor of setting the scene for uniform eta-expansion in 3.0
926+
// (arity > 0) expected type is a SAM that is not annotated with `@FunctionalInterface`
927+
// - 2.14: (arity == 0) expected type is a SAM that is not annotated with `@FunctionalInterface`
928+
def checkCanEtaExpand(): Boolean = {
929+
def expectingSamOfArity = {
930+
val sam = samOf(ptUnderlying)
931+
sam.exists && sam.info.params.lengthCompare(arity) == 0
932+
}
933+
934+
val expectingFunctionOfArity = {
935+
val ptSym = ptUnderlying.typeSymbolDirect
936+
(ptSym eq FunctionClass(arity)) || (arity > 0 && (ptSym eq FunctionClass(1))) // allowing for tupling conversion
937+
}
938+
939+
val doIt =
940+
if (arity == 0) {
941+
val doEtaZero =
942+
expectingFunctionOfArity || sourceLevel2_14 && expectingSamOfArity
943+
944+
if (doEtaZero && settings.warnEtaZero) {
945+
val ptHelp =
946+
if (expectingFunctionOfArity) pt
947+
else s"$pt, which is SAM-equivalent to ${samToFunctionType(pt)}"
948+
949+
reporter.warning(tree.pos, s"An unapplied 0-arity method was eta-expanded (due to the expected type ${ptHelp}), rather than applied to `()`.\n" +
950+
s"Write ${Apply(warnTree, Nil)} to invoke method ${meth.decodedName}, or change the expected type.")
951+
}
952+
doEtaZero
953+
} else sourceLevel2_14 || expectingFunctionOfArity || expectingSamOfArity
954+
955+
if (doIt && !expectingFunctionOfArity && settings.warnEtaSam) warnEtaSam()
956+
957+
doIt
913958
}
914-
// (4.3) eta-expand method value when function or sam type is expected (for experimentation, always eta-expand under 2.14 source level)
915-
else if (isFunctionProto(pt) || settings.isScala214) { // TODO: decide on `settings.isScala214`
916-
if (settings.isScala212 && mt.params.isEmpty) // implies isFunctionType(pt)
917-
currentRun.reporting.deprecationWarning(tree.pos, NoSymbol, "Eta-expansion of zero-argument methods is deprecated. "+
918-
s"To avoid this warning, write ${Function(Nil, Apply(tree, Nil))}.", "2.12.0")
919959

920-
typedEtaExpansion(tree, mode, pt)
960+
961+
// (4.2) condition for auto-application by -Xsource level
962+
//
963+
// until 2.14: none (assuming condition for (4.3) was not met)
964+
// in 3.0: `meth.isJavaDefined`
965+
// (TODO decide -- currently the condition is more involved to give slack to Scala methods overriding Java-defined ones;
966+
// I think we should resolve that by introducing slack in overriding e.g. a Java-defined `def toString()` by a Scala-defined `def toString`.
967+
// This also works better for dealing with accessors overriding Java-defined methods. The current strategy in methodSig is problematic:
968+
// > // Add a () parameter section if this overrides some method with () parameters
969+
// > val vparamSymssOrEmptyParamsFromOverride =
970+
// This means an accessor that overrides a Java-defined method gets a MethodType instead of a NullaryMethodType, which breaks lots of assumptions about accessors)
971+
def checkCanAutoApply(): Boolean = {
972+
if (sourceLevel2_14 && !meth.isJavaDefined)
973+
context.deprecationWarning(tree.pos, NoSymbol, s"Auto-application to `()` is deprecated. Supply the empty argument list `()` explicitly to invoke method ${meth.decodedName},\n" +
974+
s"or remove the empty argument list from its definition (Java-defined methods are exempt).\n"+
975+
s"In Scala 3, an unapplied method like this will be eta-expanded into a function.", "2.14.0")
976+
true
921977
}
922-
else cantAdapt
978+
979+
if (!meth.isConstructor && checkCanEtaExpand()) typedEtaExpansion(tree, mode, pt)
980+
else if (arity == 0 && checkCanAutoApply()) adapt(typed(Apply(tree, Nil) setPos tree.pos), mode, pt, original)
981+
else
982+
if (context.implicitsEnabled) MissingArgsForMethodTpeError(tree, meth) // `context.implicitsEnabled` implies we are not in a pattern
983+
else UnstableTreeError(tree)
923984
}
924985

925986
def adaptType(): Tree = {
@@ -1209,8 +1270,8 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
12091270

12101271
case mt: MethodType if mode.typingExprNotFunNotLhs && mt.isImplicit => // (4.1)
12111272
adaptToImplicitMethod(mt)
1212-
case mt: MethodType if mode.typingExprNotFunNotLhs && !hasUndetsInMonoMode && !treeInfo.isMacroApplicationOrBlock(tree) =>
1213-
instantiateToMethodType(mt)
1273+
case mt: MethodType if mode.typingExprNotFunNotLhs && !hasUndetsInMonoMode && !treeInfo.isMacroApplicationOrBlock(tree) => // (4.2) - (4.3)
1274+
adaptMethodTypeToExpr(mt)
12141275
case _ =>
12151276
vanillaAdapt(tree)
12161277
}

src/reflect/scala/reflect/internal/Definitions.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -728,7 +728,7 @@ trait Definitions extends api.StandardDefinitions {
728728
// Are we expecting something function-ish? This considers FunctionN / SAM / ProtoType that matches functions
729729
def isFunctionProto(pt: Type): Boolean =
730730
(isFunctionType(pt)
731-
|| (pt match { case pt: ProtoType => pt.expectsFunctionType case _ => false })
731+
|| (pt match { case pt: ProtoType => pt.expectsFunctionType case _ => false }) // TODO: this does not work for Function0
732732
|| samOf(pt).exists
733733
)
734734

test/files/jvm/future-spec/FutureTests.scala

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
/* scalac: -Xsource:3.0 */
2-
31
import scala.concurrent._
42
import scala.concurrent.duration._
53
import scala.concurrent.duration.Duration.Inf
@@ -41,8 +39,7 @@ class FutureTests extends MinimalScalaTest {
4139
val ms = new concurrent.TrieMap[Throwable, Unit]
4240
implicit val ec = scala.concurrent.ExecutionContext.fromExecutor(new java.util.concurrent.ForkJoinPool(), {
4341
t =>
44-
val u = ()
45-
ms += (t -> u)
42+
ms.addOne((t, ()))
4643
})
4744

4845
class ThrowableTest(m: String) extends Throwable(m)

test/files/neg/t7187-2.14.check

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
1-
t7187-2.14.scala:8: error: type mismatch;
1+
t7187-2.14.scala:13: error: type mismatch;
22
found : Int
33
required: () => Any
44
val t1: () => Any = m1 // error
55
^
6-
t7187-2.14.scala:9: error: type mismatch;
7-
found : Int
8-
required: () => Any
9-
val t2: () => Any = m2 // error, no eta-expansion of zero-args methods
6+
t7187-2.14.scala:14: warning: An unapplied 0-arity method was eta-expanded (due to the expected type () => Any), rather than applied to `()`.
7+
Write m2() to invoke method m2, or change the expected type.
8+
val t2: () => Any = m2 // eta-expanded with lint warning
9+
^
10+
t7187-2.14.scala:15: warning: An unapplied 0-arity method was eta-expanded (due to the expected type AcciSamZero, which is SAM-equivalent to () => Int), rather than applied to `()`.
11+
Write m2() to invoke method m2, or change the expected type.
12+
val t2AcciSam: AcciSamZero = m2 // eta-expanded with lint warning + sam warning
13+
^
14+
t7187-2.14.scala:15: warning: Eta-expansion performed to meet expected type AcciSamZero, which is SAM-equivalent to () => Int,
15+
even though trait AcciSamZero is not annotated with `@FunctionalInterface`;
16+
to suppress warning, add the annotation or write out the equivalent function literal.
17+
val t2AcciSam: AcciSamZero = m2 // eta-expanded with lint warning + sam warning
18+
^
19+
t7187-2.14.scala:16: warning: An unapplied 0-arity method was eta-expanded (due to the expected type SamZero, which is SAM-equivalent to () => Int), rather than applied to `()`.
20+
Write m2() to invoke method m2, or change the expected type.
21+
val t2Sam: SamZero = m2 // eta-expanded with lint warning
1022
^
11-
two errors found
23+
four warnings found
24+
one error found

test/files/neg/t7187-2.14.scala

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
// scalac: -Xsource:2.14
1+
// scalac: -Xsource:2.14 -Xlint:eta-zero -Xlint:eta-sam
22
//
3+
trait AcciSamZero { def apply(): Int }
4+
5+
@FunctionalInterface
6+
trait SamZero { def apply(): Int }
7+
38
class EtaExpand214 {
49
def m1 = 1
510
def m2() = 1
611
def m3(x: Int) = x
712

813
val t1: () => Any = m1 // error
9-
val t2: () => Any = m2 // error, no eta-expansion of zero-args methods
14+
val t2: () => Any = m2 // eta-expanded with lint warning
15+
val t2AcciSam: AcciSamZero = m2 // eta-expanded with lint warning + sam warning
16+
val t2Sam: SamZero = m2 // eta-expanded with lint warning
1017
val t3: Int => Any = m3 // ok
1118

1219
val t4 = m1 // apply

0 commit comments

Comments
 (0)