Skip to content

Conversation

@bracevac
Copy link
Contributor

@bracevac bracevac commented Jul 23, 2025

Fixes #23179

Since JDK 9, argument checks for LambdaMetaFactory have become stricter. Which is to say that its JDK 8 version was too lax.

We apply adaptation now in case the samType is an array, but the implType is not.

@bracevac bracevac requested a review from lrytz July 23, 2025 18:01
@sjrd
Copy link
Member

sjrd commented Jul 23, 2025

This seems misguided to me. Why AnyRef but not any of its subtypes? Also, this is not supported by spec text, whether from the Scala 2 or Scala 3 spec.

Changes to erasure are extremely risky, because they basically break tasty and binary compatibility every time. They need a very strong justification. Typically, only fixing unsoundness is a good enough reason to do this.

@bracevac
Copy link
Contributor Author

bracevac commented Jul 23, 2025

Scala 2 certainly erases this array type to Object[]. What should be the general rule?

Edit: Hmm it does not, at least judging by -Xprint:erasure. However in the resulting bytecode, the two relevant arguments to altMetaFactory are consistently array types, whereas in Scala 3 the second one ends up being Object.

BootstrapMethods:
  0: #38 REF_invokeStatic java/lang/invoke/LambdaMetafactory.altMetafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #26 ([Ljava/lang/Object;)Ljava/lang/Object;
      #30 REF_invokeStatic Test$.$anonfun$new$1$adapted:([Ljava/lang/Object;)Ljava/lang/Object;
      #26 ([Ljava/lang/Object;)Ljava/lang/Object;
      #31 0

@bracevac
Copy link
Contributor Author

If we do not want to mess with erasure, could we just uniformly use the erased SAM methodtype for the implementation method type as well in the emitted bytecode? Would this always be safe for a type-correct source program?

@sjrd
Copy link
Member

sjrd commented Jul 24, 2025

Probably, but we would need to do that at the backend level. Basically consider it a bridge method, or an $adapted something something. The extracted private def likely cannot be altered to take a different parameter type.

@bracevac bracevac changed the title Ensure Array[? >: AnyRef] erases to Object[] Refine parameter adaptation logic for arrays Jul 24, 2025
Copy link
Member

@sjrd sjrd left a comment

Choose a reason for hiding this comment

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

The new approach seems reasonable.

It might need similar changes in ExpandSAMs for Scala SAM types that don't translate to target SAM types.

!sameClass(implType, samType) && !autoAdaptedParam(implType)
|| (samType, implType).match {
case (defn.ArrayOf(_), defn.ArrayOf(_)) => false
case (defn.ArrayOf(_), _) => true // see #23179
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't there be the reverse order as well ((_, defn.ArrayOf(_)))? Like if the SAM takes an Array[T] where T <: AnyRef but the lambda is specialized to T = String, for example?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm unable to construct such a reversed example that will be accepted by the compiler.

Copy link
Member

Choose a reason for hiding this comment

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

Do we need something similar for the result type (resultAdaptationNeeded)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure, I also fail here to construct an example that would cause a run time crash (which is of course weak evidence).

@bracevac
Copy link
Contributor Author

The new approach seems reasonable.

It might need similar changes in ExpandSAMs for Scala SAM types that don't translate to target SAM types.

I tried constructing each of the five cases in the comment at the top of ExpandSAMs, but those were either rejected or did not crash at run time.

@lrytz
Copy link
Member

lrytz commented Jul 25, 2025

It might be worth including the other combinations in the test case as regression tests.

@bracevac
Copy link
Contributor Author

This solution doesn't appear to work for ScalaJS, I'll need to dig deeper.

@bracevac bracevac force-pushed the ob-fix-23179 branch 2 times, most recently from c6be3bc to 2c7b12f Compare November 29, 2025 09:30
@bracevac
Copy link
Contributor Author

bracevac commented Nov 29, 2025

@sjrd @lrytz for scalajs, I needed to tweak ExpandSAMs next to the JVM-specific tweak in Erasure. The tests all pass now. I've also added the requested regression tests.

PS: The failing stdlib test is CC-related and not due to this PR. It also fails on main.

Fixes scala#23179

Since JDK 9, argument checks for LambdaMetaFactory have become stricter.
Which is to say that its JDK 8 version was too lax.

We apply adaptation now in case the samType is an array,
but the implType is not.
- 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
The fix for scala#23179 is now localized to ExpandSAMs where the SAM expansion
happens, rather than in the general-purpose tpd.AnonClass function.
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.

@bracevac bracevac merged commit e53d6fc into scala:main Dec 1, 2025
46 checks passed
@bracevac bracevac deleted the ob-fix-23179 branch December 1, 2025 15:41
@WojciechMazur
Copy link
Contributor

Should this change be backported to 3.8.0? It seems to be that it might be useful, since it's now compiled with JDK 17 so potentially might be affected.
I guess that changes here should also help with fixing #24573 which this time was affected by result type checks

@bracevac
Copy link
Contributor Author

bracevac commented Dec 1, 2025

Yes, any JDK newer than 8 would result in a runtime failure for the kind of example this PR addresses.

@WojciechMazur WojciechMazur added the backport:nominated If we agree to backport this PR, replace this tag with "backport:accepted", otherwise delete it. label Dec 1, 2025
@WojciechMazur WojciechMazur added this to the 3.8.0 milestone Dec 1, 2025
WojciechMazur pushed a commit that referenced this pull request Dec 1, 2025
Fixes #23179

Since JDK 9, argument checks for LambdaMetaFactory have become stricter.
Which is to say that its JDK 8 version was too lax.

We apply adaptation now in case the samType is an array, but the
implType is not.
[Cherry-picked e53d6fc]
@WojciechMazur WojciechMazur added backport:accepted This PR needs to be backported, once it's been backported replace this tag by "backport:done" and removed backport:nominated If we agree to backport this PR, replace this tag with "backport:accepted", otherwise delete it. labels Dec 1, 2025
WojciechMazur added a commit that referenced this pull request Dec 2, 2025
…4615)

Backports #23591 to the 3.8.0-RC3.

PR submitted by the release tooling.
[skip ci]
@WojciechMazur WojciechMazur added backport:done This PR was successfully backported. and removed backport:accepted This PR needs to be backported, once it's been backported replace this tag by "backport:done" labels Dec 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:done This PR was successfully backported.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LambdaConversionException: Type mismatch for instantiated parameter

4 participants