Skip to content

Commit afaa61c

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 - 2.14: expected type is a SAM that is not annotated with `@FunctionalInterface` (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 980fd51 commit afaa61c

File tree

14 files changed

+187
-99
lines changed

14 files changed

+187
-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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ 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.")
112113

113114
def allLintWarnings = values.toSeq.asInstanceOf[Seq[LintWarning]]
114115
}
@@ -134,6 +135,7 @@ trait Warnings {
134135
def lintImplicitNotFound = lint contains ImplicitNotFound
135136
def warnSerialization = lint contains Serial
136137
def lintValPatterns = lint contains ValPattern
138+
def warnEtaZero = lint contains EtaZero
137139

138140
// The Xlint warning group.
139141
val lint = MultiChoiceSetting(

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

Lines changed: 99 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -779,11 +779,9 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
779779
* store these instances in context.undetparams,
780780
* unless followed by explicit type application.
781781
* (4) Do the following to unapplied methods used as values:
782-
* (4.1) If the method has only implicit parameters pass implicit arguments
783-
* (4.2) otherwise, if the method is nullary with a result type compatible to `pt`
784-
* and it is not a constructor, apply it to ()
785-
* (4.3) otherwise, if `pt` is a function type and method is not a constructor,
786-
* convert to function by eta-expansion,
782+
* (4.1) If the method has only implicit parameters, pass implicit arguments (see adaptToImplicitMethod)
783+
* (4.2) otherwise, if the method is 0-ary and it can be auto-applied (see checkCanAutoApply), apply it to ()
784+
* (4.3) otherwise, if the method is not a constructor, and can be eta-expanded (see checkCanEtaExpand), eta-expand
787785
* otherwise issue an error
788786
* (5) Convert constructors in a pattern as follows:
789787
* (5.1) If constructor refers to a case class factory, set tree's type to the unique
@@ -874,46 +872,106 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
874872
)
875873
}
876874

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

893-
def cantAdapt =
894-
if (context.implicitsEnabled) MissingArgsForMethodTpeError(tree, meth) else UnstableTreeError(tree)
898+
// note that isFunctionProto(pt) does not work properly for Function0
899+
lazy val ptUnderlying =
900+
(pt match {
901+
case oapt: OverloadedArgFunProto => oapt.underlying
902+
case pt => pt
903+
}).dealiasWiden
895904

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

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

919977
def adaptType(): Tree = {
@@ -1203,8 +1261,8 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
12031261

12041262
case mt: MethodType if mode.typingExprNotFunNotLhs && mt.isImplicit => // (4.1)
12051263
adaptToImplicitMethod(mt)
1206-
case mt: MethodType if mode.typingExprNotFunNotLhs && !hasUndetsInMonoMode && !treeInfo.isMacroApplicationOrBlock(tree) =>
1207-
instantiateToMethodType(mt)
1264+
case mt: MethodType if mode.typingExprNotFunNotLhs && !hasUndetsInMonoMode && !treeInfo.isMacroApplicationOrBlock(tree) => // (4.2) - (4.3)
1265+
adaptMethodTypeToExpr(mt)
12081266
case _ =>
12091267
vanillaAdapt(tree)
12101268
}

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: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
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`; add annotation to suppress warning.
16+
val t2AcciSam: AcciSamZero = m2 // eta-expanded with lint warning + sam warning
17+
^
18+
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 `()`.
19+
Write m2() to invoke method m2, or change the expected type.
20+
val t2Sam: SamZero = m2 // eta-expanded with lint warning
1021
^
11-
two errors found
22+
four warnings found
23+
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
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)