Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion compiler/src/dotty/tools/dotc/transform/Erasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,9 @@ object Erasure {

val paramAdaptationNeeded =
implParamTypes.lazyZip(samParamTypes).exists((implType, samType) =>
!sameClass(implType, samType) && !autoAdaptedParam(implType))
!sameClass(implType, samType) && (!autoAdaptedParam(implType)
// LambdaMetaFactory cannot auto-adapt between Object and Array types
|| samType.isInstanceOf[JavaArrayType]))
val resultAdaptationNeeded =
!sameClass(implResultType, samResultType) && !autoAdaptedResult

Expand Down
53 changes: 46 additions & 7 deletions compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dotc
package transform

import core.*
import core.TypeErasure.erasure
import Scopes.newScope
import Contexts.*, Symbols.*, Types.*, Flags.*, Decorators.*, StdNames.*, Constants.*
import MegaPhase.*
Expand Down Expand Up @@ -69,14 +70,52 @@ class ExpandSAMs extends MiniPhase:
case _ => tp
val tpe1 = collectAndStripRefinements(tpe)
val Seq(samDenot) = tpe1.possibleSamMethods
cpy.Block(tree)(stats,
transformFollowingDeep:
AnonClass(List(tpe1),
List(samDenot.symbol.asTerm.name -> fn.symbol.asTerm),
refinements.toList,
adaptVarargs = true
val implSym = fn.symbol.asTerm

// Check if SAM param erases to array but impl param doesn't (see #23179).
// E.g. SAM expects Array[AnyRef] (erases to Array[Object]) but impl has
// Array[? >: AnyRef] (erases to Object). In this case we need to create
// a wrapper with the SAM signature that casts arguments before forwarding.
val samInfo = samDenot.info
val implInfo = implSym.info
val needsArrayAdaptation = (samInfo, implInfo) match
case (samMt: MethodType, implMt: MethodType) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if it's hidden behind a PolyType? Or in a second term param list?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a concrete example in mind? Those cases do not qualify as SAMs afaik.
I've added more run tests that involve multiple parameters and a generic return type on the trait.

For generics on the method, the compiler rejects both of these:

trait PolyA { def f[T](a: Array[AnyRef]): T }

object Test {
  def gPoly(a: PolyA) = a.f[Int](Array.empty[AnyRef])

  def main(args: Array[String]): Unit = {
    gPoly([T] => (x: Array[? >: AnyRef]) => x.length.asInstanceOf[T]) // error

    gPoly(new PolyA { // error
      def f[T](x: Array[? >: AnyRef]): T = x.length.asInstanceOf[T]
    })
  }
}

Same goes for curried argument lists:

trait CurriedA { def f(a: Array[AnyRef])(b: Int): Any }

object Test {
  def gCurried(a: CurriedA) = a.f(Array.empty[AnyRef])(42)

  def main(args: Array[String]): Unit = {
    gCurried((x: Array[AnyRef]) => (n: Int) => x.headOption) // error
  }
}

Scala 2 rejects these as well (modulo the polymorphic lambda syntax of course).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those cases do not qualify as SAMs afaik.

Right. I missed that. Seems all good.

samMt.paramInfos.lazyZip(implMt.paramInfos).exists { (samPt, implPt) =>
erasure(samPt).isInstanceOf[JavaArrayType] && !erasure(implPt).isInstanceOf[JavaArrayType]
}
case _ => false

val forwarderSym =
if needsArrayAdaptation then
// Create a wrapper method with SAM's signature that casts args to impl's types
val wrapperSym = newSymbol(
implSym.owner, implSym.name.asTermName, Synthetic | Method,
samInfo, coord = implSym.span).entered.asTerm
val wrapperDef = DefDef(wrapperSym, paramss => {
val implMt = implInfo.asInstanceOf[MethodType]
val adaptedArgs = paramss.head.lazyZip(implMt.paramInfos).map { (arg, implPt) =>
if arg.tpe =:= implPt then arg else arg.cast(implPt)
}
ref(implSym).appliedToTermArgs(adaptedArgs)
})
cpy.Block(tree)(fn :: wrapperDef :: Nil,
transformFollowingDeep:
AnonClass(List(tpe1),
List(samDenot.symbol.asTerm.name -> wrapperSym),
refinements.toList,
adaptVarargs = true
)
)
)
else
cpy.Block(tree)(stats,
transformFollowingDeep:
AnonClass(List(tpe1),
List(samDenot.symbol.asTerm.name -> implSym),
refinements.toList,
adaptVarargs = true
)
)
forwarderSym
}
case _ =>
tree
Expand Down
22 changes: 22 additions & 0 deletions tests/neg/i23179-reverse.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Test that reverse cases of #23179 are properly rejected by the type checker.
// The original issue was about SAM expecting Array[T] but impl having Array[? >: T].
// Due to array invariance, the reverse cases cannot occur.

object TestReverse {
trait A { def f(a: Array[? <: AnyRef]): Any }
def g(a: A) = a.f(Array.empty[AnyRef])

def test(): Unit = {
g((x: Array[AnyRef]) => x.headOption) // error: Array[AnyRef] is not a subtype of Array[? <: AnyRef]
}
}

object TestResult {
trait A { def f(a: Int): Array[AnyRef] }
def g(a: A) = a.f(1)

def test(): Unit = {
val arr: Array[? >: AnyRef] = Array("hello")
g((x: Int) => arr) // error: Array[? >: AnyRef] is not a subtype of Array[AnyRef]
}
}
8 changes: 8 additions & 0 deletions tests/run/i23179.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
object Test {
trait A { def f(a: Array[AnyRef]): Any }
def g(a: A) = a.f(Array.empty[AnyRef])

def main(args: Array[String]): Unit = {
g((x: Array[? >: AnyRef]) => x.headOption)
}
}
Loading