Skip to content

Commit

Permalink
Merge pull request #10648 from som-snytt/issue/3664-case-function
Browse files Browse the repository at this point in the history
Accommodate case companion as function
  • Loading branch information
lrytz committed Feb 5, 2024
2 parents e46dd02 + f57bef9 commit 521cb82
Show file tree
Hide file tree
Showing 13 changed files with 239 additions and 38 deletions.
129 changes: 93 additions & 36 deletions src/compiler/scala/tools/nsc/typechecker/Typers.scala
Expand Up @@ -228,6 +228,8 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
// requiring both the ACCESSOR and the SYNTHETIC bits to trigger the exemption
private def isSyntheticAccessor(sym: Symbol) = sym.isAccessor && (!sym.isLazy || isPastTyper)

private val fixableFunctionMembers = List(nme.tupled, TermName("curried"))

// when type checking during erasure, generate erased types in spots that aren't transformed by erasure
// (it erases in TypeTrees, but not in, e.g., the type a Function node)
def phasedAppliedType(sym: Symbol, args: List[Type]) = {
Expand Down Expand Up @@ -1122,13 +1124,43 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
}
}

// if user wrote case companion C for expected function type, use C.apply or (C.apply _).tupled
def adaptApplyInsertion(): Tree = doAdaptApplyInsertion(retry = false)

def doAdaptApplyInsertion(retry: Boolean): Tree =
if (currentRun.isScala3Cross && !isPastTyper && tree.symbol != null && tree.symbol.isModule && tree.symbol.companion.isCase && isFunctionType(pt))
silent(_.typed(atPos(tree.pos)(Select(tree, nme.apply)), mode, if (retry) WildcardType else pt)) match {
case SilentResultValue(applicator) =>
val arity = definitions.functionArityFromType(applicator.tpe)
if (arity < 0) EmptyTree
else functionOrPfOrSamArgTypes(pt) match {
case arg :: Nil if definitions.isTupleType(arg) && arg.typeArgs.lengthCompare(arity) == 0 =>
val tupled = typed(atPos(tree.pos)(Select(applicator, nme.tupled)), mode, pt)
if (!tupled.isErroneous) {
val msg = s"The method `apply` is inserted. The auto insertion will be deprecated, please write `(${tree.symbol.name}.apply _).tupled` explicitly."
context.deprecationWarning(tree.pos, tree.symbol, msg, "2.13.13")
tupled
}
else EmptyTree
case args if args.lengthCompare(arity) == 0 =>
val msg = s"The method `apply` is inserted. The auto insertion will be deprecated, please write `${tree.symbol.name}.apply` explicitly."
context.deprecationWarning(tree.pos, tree.symbol, msg, "2.13.13")
applicator
case _ => EmptyTree
}
case _ if !retry => doAdaptApplyInsertion(retry = true)
case _ => EmptyTree
}
else EmptyTree

def adaptExprNotFunMode(): Tree = {
def lastTry(err: AbsTypeError = null): Tree = {
debuglog("error tree = " + tree)
if (settings.isDebug && settings.explaintypes.value) explainTypes(tree.tpe, pt)
if (err ne null) context.issue(err)
if (tree.tpe.isErroneous || pt.isErroneous) setError(tree)
else adaptMismatchedSkolems()
else
adaptApplyInsertion() orElse adaptMismatchedSkolems()
}

// TODO: should we even get to fallbackAfterVanillaAdapt for an ill-typed tree?
Expand Down Expand Up @@ -5360,6 +5392,24 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
if (!(context.unit.isJava && cls.isClass)) NoSymbol
else context.javaFindMember(cls.typeOfThis, name, s => s.isStaticMember || s.isStaticModule)._2

// If they try C.tupled, make it (C.apply _).tupled
def fixUpCaseTupled(tree: Tree, qual: Tree, name: Name, mode: Mode): Tree =
if (currentRun.isScala3Cross && !isPastTyper && qual.symbol != null && qual.symbol.isModule && qual.symbol.companion.isCase &&
context.undetparams.isEmpty && fixableFunctionMembers.contains(name)) {
val t2 = {
val t = atPos(tree.pos)(Select(qual, nme.apply))
val t1 = typedSelect(t, qual, nme.apply)
typed(atPos(tree.pos)(Select(etaExpand(t1, context.owner), name)), mode, pt)
}
if (!t2.isErroneous) {
val msg = s"The method `apply` is inserted. The auto insertion will be deprecated, please write `($qual.apply _).$name` explicitly."
context.deprecationWarning(tree.pos, qual.symbol, msg, "2.13.13")
t2
}
else EmptyTree
}
else EmptyTree

/* Attribute a selection where `tree` is `qual.name`.
* `qual` is already attributed.
*/
Expand Down Expand Up @@ -5392,9 +5442,13 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
context.warning(tree.pos, s"dubious usage of ${sel.symbol} with integer value", WarningCategory.LintNumericMethods)
}
val qual1 = adaptToMemberWithArgs(tree, qual, name, mode)
if ((qual1 ne qual) && !qual1.isErrorTyped)
return typed(treeCopy.Select(tree, qual1, name), mode, pt)
.tap(checkDubiousAdaptation)
val fixed =
if ((qual1 ne qual) && !qual1.isErrorTyped)
typed(treeCopy.Select(tree, qual1, name), mode, pt).tap(checkDubiousAdaptation)
else
fixUpCaseTupled(tree, qual, name, mode)
if (!fixed.isEmpty)
return fixed
}

// This special-case complements the logic in `adaptMember` in erasure, it handles selections
Expand Down Expand Up @@ -5614,47 +5668,50 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
val fix = runReporting.codeAction("make reference explicit", tree.pos.focusStart, w.fix, w.msg)
runReporting.warning(tree.pos, w.msg, cat, context.owner, fix)
})
if (currentRun.isScala3) {
if (currentRun.isScala3)
tree.getAndRemoveAttachment[VirtualStringContext.type].foreach(_ =>
if (symbol != definitions.StringContextModule)
runReporting.warning(
tree.pos,
s"String interpolations always use scala.StringContext in Scala 3 (${symbol.fullNameString} is used here)",
Scala3Migration,
context.owner))
}
(// this -> Foo.this
if (symbol.isThisSym)
typed1(This(symbol.owner) setPos tree.pos, mode, pt)
else if (symbol.rawname == nme.classOf && currentRun.runDefinitions.isPredefClassOf(symbol) && pt.typeSymbol == ClassClass && pt.typeArgs.nonEmpty) {
// Inferring classOf type parameter from expected type. Otherwise an
// actual call to the stubbed classOf method is generated, returning null.
typedClassOf(tree, TypeTree(pt.typeArgs.head).setPos(tree.pos.focus))
}
else {
val pre1 = if (symbol.isTopLevel) symbol.owner.thisType else if (qual == EmptyTree) NoPrefix else qual.tpe
if (settings.lintUniversalMethods && !pre1.isInstanceOf[ThisType] && isUniversalMember(symbol))
context.warning(tree.pos, s"${symbol.nameString} not selected from this instance", WarningCategory.LintUniversalMethods)
val tree1 = if (qual == EmptyTree) tree else {
val pos = tree.pos
Select(atPos(pos.focusStart)(qual), name).setPos(pos)
context.owner)
)
val onSuccess =
if (symbol.isThisSym)
typed1(This(symbol.owner).setPos(tree.pos), mode, pt) // this -> Foo.this
else if (symbol.rawname == nme.classOf && currentRun.runDefinitions.isPredefClassOf(symbol) && pt.typeSymbol == ClassClass && pt.typeArgs.nonEmpty) {
// Inferring classOf type parameter from expected type. Otherwise an
// actual call to the stubbed classOf method is generated, returning null.
typedClassOf(tree, TypeTree(pt.typeArgs.head).setPos(tree.pos.focus))
}
var tree2: Tree = null
var pre2: Type = pre1
makeAccessible(tree1, symbol, pre1, qual) match {
case (t: Tree, tp: Type) =>
tree2 = t
pre2 = tp
case t: Tree =>
tree2 = t
case x => throw new MatchError(x)
else {
val pre1 = if (symbol.isTopLevel) symbol.owner.thisType else if (qual == EmptyTree) NoPrefix else qual.tpe
if (settings.lintUniversalMethods && !pre1.isInstanceOf[ThisType] && isUniversalMember(symbol))
context.warning(tree.pos, s"${symbol.nameString} not selected from this instance", WarningCategory.LintUniversalMethods)
val tree1 = if (qual == EmptyTree) tree else {
val pos = tree.pos
Select(atPos(pos.focusStart)(qual), name).setPos(pos)
}
var tree2: Tree = null
var pre2: Type = pre1
makeAccessible(tree1, symbol, pre1, qual) match {
case (t: Tree, tp: Type) =>
tree2 = t
pre2 = tp
case t: Tree =>
tree2 = t
case x => throw new MatchError(x)
}
// scala/bug#5967 Important to replace param type A* with Seq[A] when seen from from a reference,
// to avoid inference errors in pattern matching.
stabilize(tree2, pre2, mode, pt).modifyType(dropIllegalStarTypes)
}
// scala/bug#5967 Important to replace param type A* with Seq[A] when seen from from a reference, to avoid
// inference errors in pattern matching.
stabilize(tree2, pre2, mode, pt) modifyType dropIllegalStarTypes
}) setAttachments tree.attachments
}
if (!isPastTyper && currentRun.isScala3 && !currentRun.isScala3Cross && isFunctionType(pt) && symbol.isModule && symbol.isSynthetic && symbol.companion.isCase)
context.deprecationWarning(tree.pos, symbol, s"Synthetic case companion used as a Function, use explicit object with Function parent", "2.13.13")
onSuccess.setAttachments(tree.attachments)
}
}

def typedIdentOrWildcard(tree: Ident) = {
val name = tree.name
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/scala/tools/nsc/typechecker/Unapplies.scala
Expand Up @@ -113,7 +113,7 @@ trait Unapplies extends ast.TreeDSL {
*/
def caseModuleDef(cdef: ClassDef): ModuleDef = {
val params = constrParamss(cdef)
def inheritFromFun = !cdef.mods.hasAbstractFlag && cdef.tparams.isEmpty && (params match {
def inheritFromFun = !currentRun.isScala3Cross && !cdef.mods.hasAbstractFlag && cdef.tparams.isEmpty && (params match {
case List(ps) if ps.length <= MaxFunctionArity => true
case _ => false
}) && applyAccess(constrMods(cdef)) != Inherit
Expand Down
2 changes: 1 addition & 1 deletion src/reflect/scala/reflect/internal/Definitions.scala
Expand Up @@ -658,7 +658,7 @@ trait Definitions extends api.StandardDefinitions {
class VarArityClass(name: String, maxArity: Int, countFrom: Int = 0, init: Option[ClassSymbol] = None) extends VarArityClassApi {
private[this] val offset = countFrom - init.size
private def isDefinedAt(i: Int) = i < seq.length + offset && i >= offset
val seq: IndexedSeq[ClassSymbol] = (init ++: countFrom.to(maxArity).map { i => getRequiredClass("scala." + name + i) }).toVector
val seq: IndexedSeq[ClassSymbol] = (init ++: countFrom.to(maxArity).map(i => getRequiredClass(s"scala.$name$i"))).toVector
private[this] val symSet = new SymbolSet(seq.toList)
def contains(sym: Symbol): Boolean = symSet.contains(sym)
def apply(i: Int) = if (isDefinedAt(i)) seq(i - offset) else NoSymbol
Expand Down
1 change: 1 addition & 0 deletions src/reflect/scala/reflect/internal/StdNames.scala
Expand Up @@ -901,6 +901,7 @@ trait StdNames {
val tpe : NameType = nameType("tpe")
val tree : NameType = nameType("tree")
val true_ : NameType = nameType("true")
val tupled: NameType = nameType("tupled")
val typedProductIterator: NameType = nameType("typedProductIterator")
val TypeName: NameType = nameType("TypeName")
val typeTagToManifest: NameType = nameType("typeTagToManifest")
Expand Down
6 changes: 6 additions & 0 deletions test/files/neg/t3664.check
@@ -0,0 +1,6 @@
t3664.scala:9: warning: Synthetic case companion used as a Function, use explicit object with Function parent
def f(xs: List[Int]): List[C] = xs.map(C)
^
error: No warnings can be incurred under -Werror.
1 warning
1 error
10 changes: 10 additions & 0 deletions test/files/neg/t3664.scala
@@ -0,0 +1,10 @@
//> using options -Werror -Xlint -Xsource:3

// use -Xsource:3 to warn that implicitly extending Function is deprecated
// use -Xsource:3-cross for dotty behavior: no extend Function, yes adapt C.apply.tupled

case class C(i: Int)

class Test {
def f(xs: List[Int]): List[C] = xs.map(C)
}
6 changes: 6 additions & 0 deletions test/files/neg/t3664b.check
@@ -0,0 +1,6 @@
t3664b.scala:9: warning: The method `apply` is inserted. The auto insertion will be deprecated, please write `C.apply` explicitly.
def f(xs: List[Int]): List[C] = xs.map(C)
^
error: No warnings can be incurred under -Werror.
1 warning
1 error
10 changes: 10 additions & 0 deletions test/files/neg/t3664b.scala
@@ -0,0 +1,10 @@
//> using options -Werror -Xlint -Xsource:3-cross

// use -Xsource:3 to warn that implicitly extending Function is deprecated
// use -Xsource:3-cross for dotty behavior: no extend Function, yes adapt C.apply.tupled

case class C(i: Int)

class Test {
def f(xs: List[Int]): List[C] = xs.map(C)
}
27 changes: 27 additions & 0 deletions test/files/neg/t3664c.check
@@ -0,0 +1,27 @@
t3664c.scala:20: error: type mismatch;
found : C.type
required: ((Int, Int, Int)) => C
def f(xs: List[(Int, Int, Int)]): List[C] = xs.map(C) // hard error
^
t3664c.scala:22: error: type mismatch;
found : ((Int, Int)) => C
required: ((Int, Int, Int)) => C
def g(xs: List[(Int, Int, Int)]): List[C] = xs.map(C.tupled) // hard error
^
t3664c.scala:24: error: type mismatch;
found : D.type
required: ((Int, Int)) => D
def d(xs: List[(Int, Int)]): List[D] = xs.map(D) // hard error
^
t3664c.scala:26: warning: An unapplied 0-arity method was eta-expanded (due to the expected type () => E), rather than applied to `()`.
Write E.apply() to invoke method apply, or change the expected type.
val e: () => E = E
^
t3664c.scala:26: warning: The method `apply` is inserted. The auto insertion will be deprecated, please write `E.apply` explicitly.
val e: () => E = E
^
t3664c.scala:28: warning: The method `apply` is inserted. The auto insertion will be deprecated, please write `F.apply` explicitly.
def ov(xs: List[Int]): List[F] = xs.map(F)
^
3 warnings
3 errors
29 changes: 29 additions & 0 deletions test/files/neg/t3664c.scala
@@ -0,0 +1,29 @@
//> using options -Werror -Xlint -Xsource:3-cross

// use -Xsource:3 to warn that implicitly extending Function is deprecated
// use -Xsource:3-cross for dotty behavior: no extend Function, yes adapt C.apply.tupled

case class C(i: Int, j: Int)

abstract case class D(i: Int, j: Int)

case class E()

case class F(i: Int)
object F {
def apply(): F = apply(42)
def apply(i: Int): F = new F(i)
def apply(i: Int, j: Int): F = new F(i+j)
}

class Test {
def f(xs: List[(Int, Int, Int)]): List[C] = xs.map(C) // hard error

def g(xs: List[(Int, Int, Int)]): List[C] = xs.map(C.tupled) // hard error

def d(xs: List[(Int, Int)]): List[D] = xs.map(D) // hard error

val e: () => E = E

def ov(xs: List[Int]): List[F] = xs.map(F)
}
13 changes: 13 additions & 0 deletions test/files/pos/t3664.scala
@@ -0,0 +1,13 @@

//> using options -Werror -Xlint -Xsource:3-cross

import language.implicitConversions

case class C(i: Int)
object C // no function parent

// use conversion, don't warn about apply insertion
class Test {
implicit def cv(c: C.type): Function[Int, C] = C(_)
def f(xs: List[Int]): List[C] = xs.map(C)
}
12 changes: 12 additions & 0 deletions test/files/run/t3664.check
@@ -0,0 +1,12 @@
t3664.scala:10: warning: The method `apply` is inserted. The auto insertion will be deprecated, please write `B.apply` explicitly.
def mapped(xs: List[Int]): List[B] = xs.map(B)
^
t3664.scala:13: warning: The method `apply` is inserted. The auto insertion will be deprecated, please write `(C.apply _).tupled` explicitly.
def cross(xs: List[(Int, Int)]): List[C] = xs.map(C)
^
t3664.scala:15: warning: The method `apply` is inserted. The auto insertion will be deprecated, please write `(C.apply _).tupled` explicitly.
def f(xs: List[(Int, Int)]): List[C] = xs.map(C.tupled)
^
t3664.scala:17: warning: The method `apply` is inserted. The auto insertion will be deprecated, please write `(C.apply _).curried` explicitly.
def g(xs: List[Int]): List[C] = xs.map(C.curried).map(_(42))
^
30 changes: 30 additions & 0 deletions test/files/run/t3664.scala
@@ -0,0 +1,30 @@
//> using options -Xlint -Xsource:3-cross

// use -Xsource:3 to warn that implicitly extending Function is deprecated
// use -Xsource:3-cross for dotty behavior: no extend Function, yes adapt C.apply.tupled

case class B(i: Int)
case class C(i: Int, j: Int)

class Test {
def mapped(xs: List[Int]): List[B] = xs.map(B)

// accept for cross because dotty has no C.tupled but has fancy untupling adaptation
def cross(xs: List[(Int, Int)]): List[C] = xs.map(C)

def f(xs: List[(Int, Int)]): List[C] = xs.map(C.tupled)

def g(xs: List[Int]): List[C] = xs.map(C.curried).map(_(42))

def f2(xs: List[(Int, Int)]): List[C] = xs.map((C.apply _).tupled)

def g2(xs: List[Int]): List[C] = xs.map((C.apply _).curried).map(_(42))

def g3(xs: List[Int]): List[C] = xs.map(((i: Int, j: Int) => C.apply(i, j)).curried).map(_(42))
}
object Test extends Test with App {
assert(mapped(List(52)) == List(B(52)))
assert(cross(List(27->42)) == List(C(27, 42)))
assert(f(List(27->42)) == List(C(27, 42)))
assert(g(List(27)) == List(C(27, 42)))
}

0 comments on commit 521cb82

Please sign in to comment.