@@ -1226,8 +1226,15 @@ object Serializers {
12261226
12271227 case TagDebugger => Debugger ()
12281228
1229- case TagNew => New (readClassName(), readMethodIdent(), readTrees())
1230- case TagLoadModule => LoadModule (readClassName())
1229+ case TagNew =>
1230+ val tree = New (readClassName(), readMethodIdent(), readTrees())
1231+ if (hacks.useBelow(19 ))
1232+ anonFunctionNewNodeHackBelow19(tree)
1233+ else
1234+ tree
1235+
1236+ case TagLoadModule =>
1237+ LoadModule (readClassName())
12311238
12321239 case TagStoreModule =>
12331240 if (hacks.useBelow(16 )) {
@@ -1542,6 +1549,129 @@ object Serializers {
15421549 UnaryOp (UnaryOp .CheckNotNull , expr)
15431550 }
15441551
1552+ /** Rewrites `New` nodes of `AnonFunctionN`s coming from before 1.19 into `NewLambda` nodes.
1553+ *
1554+ * Before 1.19, the codegen for `scala.FunctionN` lambda was of the following shape:
1555+ * {{{
1556+ * new scala.scalajs.runtime.AnonFunctionN(arrow-lambda<...captures>(...args: any): any = {
1557+ * body
1558+ * })
1559+ * }}}
1560+ *
1561+ * This function rewrites such calls to `NewLambda` nodes, using the new
1562+ * definition of these classes:
1563+ * {{{
1564+ * <newLambda>(scala.scalajs.runtime.AnonFunctionN,
1565+ * apply;Ljava.lang.Object;...;Ljava.lang.Object,
1566+ * any, any, (typed-lambda<...captures>(...args: any): any = {
1567+ * body
1568+ * }))
1569+ * }}}
1570+ *
1571+ * The rewrite ensures that previously published lambdas get the same
1572+ * optimizations on Wasm as those recompiled with 1.19+.
1573+ *
1574+ * The rewrite also applies to Scala 3's `AnonFunctionXXL` classes, with
1575+ * an additional adaptation of the parameter's type. It rewrites
1576+ * {{{
1577+ * new scala.scalajs.runtime.AnonFunctionXXL(arrow-lambda<...captures>(argArray: any): any = {
1578+ * body
1579+ * })
1580+ * }}}
1581+ * to
1582+ * {{{
1583+ * <newLambda>(scala.scalajs.runtime.AnonFunctionXXL,
1584+ * apply;Ljava.lang.Object[];Ljava.lang.Object,
1585+ * any, any, (typed-lambda<...captures>(argArray: jl.Object[]): any = {
1586+ * newBody
1587+ * }))
1588+ * }}}
1589+ * where `newBody` is `body` transformed to adapt the type of `argArray`
1590+ * everywhere.
1591+ *
1592+ * Tests are in `sbt-plugin/src/sbt-test/linker/anonfunction-compat/`.
1593+ *
1594+ * ---
1595+ *
1596+ * In case the argument is not an arrow-lambda of the expected shape, we
1597+ * use a fallback. This never happens for our published codegens, but
1598+ * could happen for other valid IR. We rewrite
1599+ * {{{
1600+ * new scala.scalajs.runtime.AnonFunctionN(jsFunctionArg)
1601+ * }}}
1602+ * to
1603+ * {{{
1604+ * <newLambda>(scala.scalajs.runtime.AnonFunctionN,
1605+ * apply;Ljava.lang.Object;...;Ljava.lang.Object,
1606+ * any, any, (typed-lambda<f: any = jsFunctionArg>(...args: any): any = {
1607+ * f(...args)
1608+ * }))
1609+ * }}}
1610+ *
1611+ * This code path is not tested in the CI, but can be locally tested by
1612+ * commenting out the `case Closure(...) =>`.
1613+ */
1614+ private def anonFunctionNewNodeHackBelow19 (tree : New ): Tree = {
1615+ tree match {
1616+ case New (cls, _, funArg :: Nil ) =>
1617+ def makeFallbackTypedClosure (paramTypes : List [Type ]): Closure = {
1618+ implicit val pos = funArg.pos
1619+ val fParamDef = ParamDef (LocalIdent (LocalName (" f" )), NoOriginalName , AnyType , mutable = false )
1620+ val xParamDefs = paramTypes.zipWithIndex.map { case (ptpe, i) =>
1621+ ParamDef (LocalIdent (LocalName (s " x $i" )), NoOriginalName , ptpe, mutable = false )
1622+ }
1623+ Closure (ClosureFlags .typed, List (fParamDef), xParamDefs, None , AnyType ,
1624+ JSFunctionApply (fParamDef.ref, xParamDefs.map(_.ref)),
1625+ List (funArg))
1626+ }
1627+
1628+ cls match {
1629+ case HackNames .AnonFunctionClass (arity) =>
1630+ val typedClosure = funArg match {
1631+ // The shape produced by our earlier compilers, which we can optimally rewrite
1632+ case Closure (ClosureFlags .arrow, captureParams, params, None , AnyType , body, captureValues)
1633+ if params.lengthCompare(arity) == 0 =>
1634+ Closure (ClosureFlags .typed, captureParams, params, None , AnyType ,
1635+ body, captureValues)(funArg.pos)
1636+
1637+ // Fallback for other shapes (theoretically required; dead code in practice)
1638+ case _ =>
1639+ makeFallbackTypedClosure(List .fill(arity)(AnyType ))
1640+ }
1641+
1642+ NewLambda (HackNames .anonFunctionDescriptors(arity), typedClosure)(tree.tpe)(tree.pos)
1643+
1644+ case HackNames .AnonFunctionXXLClass =>
1645+ val typedClosure = funArg match {
1646+ // The shape produced by our earlier compilers, which we can optimally rewrite
1647+ case Closure (ClosureFlags .arrow, captureParams, oldParam :: Nil , None , AnyType , body, captureValues) =>
1648+ // Here we need to adapt the type of the parameter from `any` to `jl.Object[]`.
1649+ val newParam = oldParam.copy(ptpe = HackNames .ObjectArrayType )(oldParam.pos)
1650+ val newBody = new Transformers .LocalScopeTransformer {
1651+ override def transform (tree : Tree ): Tree = tree match {
1652+ case tree @ VarRef (newParam.name.name) => tree.copy()(newParam.ptpe)(tree.pos)
1653+ case _ => super .transform(tree)
1654+ }
1655+ }.transform(body)
1656+ Closure (ClosureFlags .typed, captureParams, List (newParam), None , AnyType ,
1657+ newBody, captureValues)(funArg.pos)
1658+
1659+ // Fallback for other shapes (theoretically required; dead code in practice)
1660+ case _ =>
1661+ makeFallbackTypedClosure(List (HackNames .ObjectArrayType ))
1662+ }
1663+
1664+ NewLambda (HackNames .anonFunctionXXLDescriptor, typedClosure)(tree.tpe)(tree.pos)
1665+
1666+ case _ =>
1667+ tree
1668+ }
1669+
1670+ case _ =>
1671+ tree
1672+ }
1673+ }
1674+
15451675 def readTrees (): List [Tree ] =
15461676 List .fill(readInt())(readTree())
15471677
@@ -1641,10 +1771,15 @@ object Serializers {
16411771
16421772 val jsNativeMembers = jsNativeMembersBuilder.result()
16431773
1644- ClassDef (name, originalName, kind, jsClassCaptures, superClass, parents,
1774+ val classDef = ClassDef (name, originalName, kind, jsClassCaptures, superClass, parents,
16451775 jsSuperClass, jsNativeLoadSpec, fields, methods, jsConstructor,
16461776 jsMethodProps, jsNativeMembers, topLevelExportDefs)(
16471777 optimizerHints)
1778+
1779+ if (hacks.useBelow(19 ))
1780+ anonFunctionClassDefHackBelow19(classDef)
1781+ else
1782+ classDef
16481783 }
16491784
16501785 private def jlClassMethodsHackBelow17 (methods : List [MethodDef ]): List [MethodDef ] = {
@@ -1931,6 +2066,88 @@ object Serializers {
19312066 (jsConstructorBuilder.result(), jsMethodPropsBuilder.result())
19322067 }
19332068
2069+ /** Rewrites `scala.scalajs.runtime.AnonFunctionN`s from before 1.19.
2070+ *
2071+ * Before 1.19, these classes were defined as
2072+ * {{{
2073+ * // final in source code
2074+ * class AnonFunctionN extends AbstractFunctionN {
2075+ * val f: any
2076+ * def this(f: any) = {
2077+ * this.f = f;
2078+ * super()
2079+ * }
2080+ * def apply(...args: any): any = f(...args)
2081+ * }
2082+ * }}}
2083+ *
2084+ * Starting with 1.19, they were rewritten to be used as SAM classes for
2085+ * `NewLambda` nodes. The new IR shape is
2086+ * {{{
2087+ * // sealed abstract in source code
2088+ * class AnonFunctionN extends AbstractFunctionN {
2089+ * def this() = super()
2090+ * }
2091+ * }}}
2092+ *
2093+ * This function rewrites those classes to the new shape.
2094+ *
2095+ * The rewrite also applies to Scala 3's `AnonFunctionXXL`.
2096+ *
2097+ * Tests are in `sbt-plugin/src/sbt-test/linker/anonfunction-compat/`.
2098+ */
2099+ private def anonFunctionClassDefHackBelow19 (classDef : ClassDef ): ClassDef = {
2100+ import classDef ._
2101+
2102+ if (! HackNames .allAnonFunctionClasses.contains(className)) {
2103+ classDef
2104+ } else {
2105+ val newCtor : MethodDef = {
2106+ // Find the old constructor to get its position and version
2107+ val oldCtor = methods.find(_.methodName.isConstructor).getOrElse {
2108+ throw new InvalidIRException (classDef,
2109+ s " Did not find a constructor in ${className.nameString}" )
2110+ }
2111+ implicit val pos = oldCtor.pos
2112+
2113+ // constructor def <init>() = this.superClass::<init>()
2114+ MethodDef (
2115+ MemberFlags .empty.withNamespace(MemberNamespace .Constructor ),
2116+ MethodIdent (NoArgConstructorName ),
2117+ NoOriginalName ,
2118+ Nil ,
2119+ VoidType ,
2120+ Some {
2121+ ApplyStatically (
2122+ ApplyFlags .empty.withConstructor(true ),
2123+ This ()(ClassType (className, nullable = false )),
2124+ superClass.get.name,
2125+ MethodIdent (NoArgConstructorName ),
2126+ Nil
2127+ )(VoidType )
2128+ }
2129+ )(OptimizerHints .empty, oldCtor.version)
2130+ }
2131+
2132+ ClassDef (
2133+ name,
2134+ originalName,
2135+ kind,
2136+ jsClassCaptures,
2137+ superClass,
2138+ interfaces,
2139+ jsSuperClass,
2140+ jsNativeLoadSpec,
2141+ fields = Nil , // throws away the `f` field
2142+ methods = List (newCtor), // throws away the old constructor and `apply` method
2143+ jsConstructor,
2144+ jsMethodProps,
2145+ jsNativeMembers,
2146+ topLevelExportDefs
2147+ )(OptimizerHints .empty)(pos) // throws away the `@inline`
2148+ }
2149+ }
2150+
19342151 private def readFieldDef ()(implicit pos : Position ): FieldDef = {
19352152 val flags = MemberFlags .fromBits(readInt())
19362153 val name = readFieldIdentForEnclosingClass()
@@ -2602,6 +2819,8 @@ object Serializers {
26022819
26032820 /** Names needed for hacks. */
26042821 private object HackNames {
2822+ val AnonFunctionXXLClass =
2823+ ClassName (" scala.scalajs.runtime.AnonFunctionXXL" ) // from the Scala 3 library
26052824 val CloneNotSupportedExceptionClass =
26062825 ClassName (" java.lang.CloneNotSupportedException" )
26072826 val SystemModule : ClassName =
@@ -2611,14 +2830,50 @@ object Serializers {
26112830 val ReflectArrayModClass =
26122831 ClassName (" java.lang.reflect.Array$" )
26132832
2833+ val ObjectArrayType = ArrayType (ArrayTypeRef (ObjectRef , 1 ), nullable = true )
2834+
2835+ private val applySimpleName = SimpleMethodName (" apply" )
2836+
26142837 val cloneName : MethodName =
2615- MethodName (" clone" , Nil , ClassRef ( ObjectClass ) )
2838+ MethodName (" clone" , Nil , ObjectRef )
26162839 val identityHashCodeName : MethodName =
2617- MethodName (" identityHashCode" , List (ClassRef ( ObjectClass ) ), IntRef )
2840+ MethodName (" identityHashCode" , List (ObjectRef ), IntRef )
26182841 val newInstanceSingleName : MethodName =
2619- MethodName (" newInstance" , List (ClassRef (ClassClass ), IntRef ), ClassRef ( ObjectClass ) )
2842+ MethodName (" newInstance" , List (ClassRef (ClassClass ), IntRef ), ObjectRef )
26202843 val newInstanceMultiName : MethodName =
2621- MethodName (" newInstance" , List (ClassRef (ClassClass ), ArrayTypeRef (IntRef , 1 )), ClassRef (ObjectClass ))
2844+ MethodName (" newInstance" , List (ClassRef (ClassClass ), ArrayTypeRef (IntRef , 1 )), ObjectRef )
2845+
2846+ private val anonFunctionArities : Map [ClassName , Int ] =
2847+ (0 to 22 ).map(arity => ClassName (s " scala.scalajs.runtime.AnonFunction $arity" ) -> arity).toMap
2848+ val allAnonFunctionClasses : Set [ClassName ] =
2849+ anonFunctionArities.keySet + AnonFunctionXXLClass
2850+
2851+ object AnonFunctionClass {
2852+ def unapply (cls : ClassName ): Option [Int ] =
2853+ anonFunctionArities.get(cls)
2854+ }
2855+
2856+ lazy val anonFunctionDescriptors : IndexedSeq [NewLambda .Descriptor ] = {
2857+ anonFunctionArities.toIndexedSeq.sortBy(_._2).map { case (className, arity) =>
2858+ NewLambda .Descriptor (
2859+ superClass = className,
2860+ interfaces = Nil ,
2861+ methodName = MethodName (applySimpleName, List .fill(arity)(ObjectRef ), ObjectRef ),
2862+ paramTypes = List .fill(arity)(AnyType ),
2863+ resultType = AnyType
2864+ )
2865+ }
2866+ }
2867+
2868+ lazy val anonFunctionXXLDescriptor : NewLambda .Descriptor = {
2869+ NewLambda .Descriptor (
2870+ superClass = AnonFunctionXXLClass ,
2871+ interfaces = Nil ,
2872+ methodName = MethodName (applySimpleName, List (ObjectArrayType .arrayTypeRef), ObjectRef ),
2873+ paramTypes = List (ObjectArrayType ),
2874+ resultType = AnyType
2875+ )
2876+ }
26222877 }
26232878
26242879 private class OptionBuilder [T ] {
0 commit comments