Skip to content

Commit 2c7b12f

Browse files
committed
Fix #23179 for Scala.js and add regression tests
- Guard the Erasure.scala array adaptation check with !scalajs.value since Scala.js custom SAMs are handled differently (via ExpandSAMs) - Add array type adaptation in tpd.AnonClass for Scala.js, which handles custom SAM closures that get expanded to anonymous classes before erasure - Add neg test documenting that reverse cases (impl has Array, SAM erases to Object) are rejected by the type checker due to array invariance
1 parent e70c4a7 commit 2c7b12f

File tree

3 files changed

+55
-9
lines changed

3 files changed

+55
-9
lines changed

compiler/src/dotty/tools/dotc/ast/tpd.scala

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,11 +394,38 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
394394
adaptVarargs: Boolean)(using Context): Block = {
395395
AnonClass(termForwarders.head._2.owner, parents, termForwarders.map(_._2.span).reduceLeft(_ `union` _), adaptVarargs) { cls =>
396396
def forwarder(name: TermName, fn: TermSymbol) = {
397-
val fwdMeth = fn.copy(cls, name, Synthetic | Method | Final).entered.asTerm
397+
// Look up the SAM method from the parent to get its signature
398+
val samMember = parents.head.member(name)
399+
// Use SAM's signature if it exists and the SAM param erases to an array but the fn param
400+
// does not. This handles cases like Array[? >: AnyRef] (erases to Object) vs
401+
// Array[AnyRef] (erases to Array[Object]).
402+
val useParentSig = samMember.exists && {
403+
val samInfo = samMember.info
404+
val fnInfo = fn.info
405+
(samInfo, fnInfo) match
406+
case (samMt: MethodType, fnMt: MethodType) =>
407+
samMt.paramInfos.lazyZip(fnMt.paramInfos).exists { (samPt, fnPt) =>
408+
val samErased = TypeErasure.erasure(samPt)
409+
val fnErased = TypeErasure.erasure(fnPt)
410+
// Only adapt when SAM expects an array but fn takes Object
411+
samErased.isInstanceOf[JavaArrayType] && !fnErased.isInstanceOf[JavaArrayType]
412+
}
413+
case _ => false
414+
}
415+
val fwdMeth =
416+
if useParentSig then
417+
newSymbol(cls, name, Synthetic | Method | Final | Override, samMember.info).entered.asTerm
418+
else
419+
fn.copy(cls, name, Synthetic | Method | Final).entered.asTerm
398420
for overridden <- fwdMeth.allOverriddenSymbols do
399421
if overridden.is(Extension) then fwdMeth.setFlag(Extension)
400422
if !overridden.is(Deferred) then fwdMeth.setFlag(Override)
401-
DefDef(fwdMeth, ref(fn).appliedToArgss(_))
423+
if useParentSig then
424+
// Need to cast args from SAM param types to fn param types
425+
DefDef(fwdMeth, argss =>
426+
ref(fn).appliedToArgss(argss.map(_.map(arg => arg.cast(arg.tpe.widen)))))
427+
else
428+
DefDef(fwdMeth, ref(fn).appliedToArgss(_))
402429
}
403430
termForwarders.map((name, sym) => forwarder(name, sym)) ++
404431
typeMembers.map((name, info) => TypeDef(newSymbol(cls, name, Synthetic, info).entered))

compiler/src/dotty/tools/dotc/transform/Erasure.scala

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -482,13 +482,10 @@ object Erasure {
482482
def sameClass(tp1: Type, tp2: Type) = tp1.classSymbol == tp2.classSymbol
483483

484484
val paramAdaptationNeeded =
485-
implParamTypes.lazyZip(samParamTypes).exists: (implType, samType) =>
486-
!sameClass(implType, samType) && !autoAdaptedParam(implType)
487-
|| (samType, implType).match {
488-
case (defn.ArrayOf(_), defn.ArrayOf(_)) => false
489-
case (defn.ArrayOf(_), _) => true // see #23179
490-
case _ => false
491-
}
485+
implParamTypes.lazyZip(samParamTypes).exists((implType, samType) =>
486+
!sameClass(implType, samType) && (!autoAdaptedParam(implType)
487+
// LambdaMetaFactory cannot auto-adapt between Object and Array types
488+
|| samType.isInstanceOf[JavaArrayType]))
492489
val resultAdaptationNeeded =
493490
!sameClass(implResultType, samResultType) && !autoAdaptedResult
494491

tests/neg/i23179-reverse.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Test that reverse cases of #23179 are properly rejected by the type checker.
2+
// The original issue was about SAM expecting Array[T] but impl having Array[? >: T].
3+
// Due to array invariance, the reverse cases cannot occur.
4+
5+
object TestReverse {
6+
trait A { def f(a: Array[? <: AnyRef]): Any }
7+
def g(a: A) = a.f(Array.empty[AnyRef])
8+
9+
def test(): Unit = {
10+
g((x: Array[AnyRef]) => x.headOption) // error: Array[AnyRef] is not a subtype of Array[? <: AnyRef]
11+
}
12+
}
13+
14+
object TestResult {
15+
trait A { def f(a: Int): Array[AnyRef] }
16+
def g(a: A) = a.f(1)
17+
18+
def test(): Unit = {
19+
val arr: Array[? >: AnyRef] = Array("hello")
20+
g((x: Int) => arr) // error: Array[? >: AnyRef] is not a subtype of Array[AnyRef]
21+
}
22+
}

0 commit comments

Comments
 (0)