Skip to content

Commit 29223aa

Browse files
committed
Refactor closure adaptation
As described in details in `Erasure#adaptClosure`, in some situation we need to perform some transformation to make the closure implementation method type match the type of the single abstract method of the functional interface this closure implements, previously the responsability for doing this was split in three places: - The FunctionalInterfaces phase was setting the functional interface of specializable FunctionN instances to the corresponding specialized JFunction*mc* interface - The backend set the functional interface of a Unit-returning FunctionN to JProcedureN - And `adaptClosure` generated a bridge method in all other situations where some adaptation was needed. This commit centralizes all of this logic in `adaptClosure` which allows us to remove one phase, and in general makes it easier to follow what's going on.
1 parent 414956c commit 29223aa

File tree

7 files changed

+128
-146
lines changed

7 files changed

+128
-146
lines changed

compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -318,19 +318,10 @@ trait BCodeBodyBuilder extends BCodeSkelBuilder {
318318
abort(s"Unexpected New(${tpt.tpe.showSummary()}/$tpt) reached GenBCode.\n" +
319319
" Call was genLoad" + ((tree, expectedType)))
320320

321-
case app: Closure =>
322-
val env: List[Tree] = app.env
323-
val call: Tree = app.meth
324-
val functionalInterface: Symbol = {
325-
val t = app.tpt.tpe.typeSymbol
326-
if (t.exists) t
327-
else {
328-
val arity = app.meth.tpe.widenDealias.firstParamTypes.size - env.size
329-
val returnsUnit = app.meth.tpe.widenDealias.resultType.classSymbol == defn.UnitClass
330-
if (returnsUnit) requiredClass(("scala.runtime.function.JProcedure" + arity))
331-
else requiredClass(("scala.Function" + arity))
332-
}
333-
}
321+
case t @ Closure(env, call, tpt) =>
322+
val functionalInterface: Symbol =
323+
if !tpt.isEmpty then tpt.tpe.classSymbol
324+
else t.tpe.classSymbol
334325
val (fun, args) = call match {
335326
case Apply(fun, args) => (fun, args)
336327
case t @ DesugaredSelect(_, _) => (t, Nil) // TODO: use Select

compiler/src/dotty/tools/dotc/Compiler.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ class Compiler {
111111
new CapturedVars) :: // Represent vars captured by closures as heap objects
112112
List(new Constructors, // Collect initialization code in primary constructors
113113
// Note: constructors changes decls in transformTemplate, no InfoTransformers should be added after it
114-
new FunctionalInterfaces, // Rewrites closures to implement @specialized types of Functions.
115114
new Instrumentation) :: // Count calls and allocations under -Yinstrument
116115
List(new LambdaLift, // Lifts out nested functions to class scope, storing free variables in environments
117116
// Note: in this mini-phase block scopes are incorrect. No phases that rely on scopes should be here

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1464,7 +1464,12 @@ class Definitions {
14641464
new PerRun(Function2SpecializedReturnTypes.map(_.symbol))
14651465

14661466
def isSpecializableFunction(cls: ClassSymbol, paramTypes: List[Type], retType: Type)(using Context): Boolean =
1467-
paramTypes.length <= 2 && cls.derivesFrom(FunctionClass(paramTypes.length)) && (paramTypes match {
1467+
paramTypes.length <= 2 && cls.derivesFrom(FunctionClass(paramTypes.length))
1468+
&& isSpecializableFunctionSAM(paramTypes, retType)
1469+
1470+
/** If the Single Abstract Method of a Function class has this type, is it specializable? */
1471+
def isSpecializableFunctionSAM(paramTypes: List[Type], retType: Type)(using Context): Boolean =
1472+
paramTypes.length <= 2 && (paramTypes match {
14681473
case Nil =>
14691474
Function0SpecializedReturnClasses().contains(retType.typeSymbol)
14701475
case List(paramType0) =>

compiler/src/dotty/tools/dotc/core/NameOps.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,15 +300,15 @@ object NameOps {
300300
*
301301
* `<return type><first type><second type><...>`
302302
*/
303-
def specializedFunction(ret: Type, args: List[Type])(using Context): Name =
303+
def specializedFunction(ret: Type, args: List[Type])(using Context): N =
304304
val sb = new StringBuilder
305305
sb.append(name.toString)
306306
sb.append(nme.specializedTypeNames.prefix.toString)
307307
sb.append(nme.specializedTypeNames.separator)
308308
sb.append(defn.typeTag(ret).toString)
309309
args.foreach { arg => sb.append(defn.typeTag(arg)) }
310310
sb.append(nme.specializedTypeNames.suffix)
311-
termName(sb.toString)
311+
likeSpacedN(termName(sb.toString))
312312

313313
/** If name length exceeds allowable limit, replace part of it by hash */
314314
def compactified(using Context): TermName = termName(compactify(name.toString))

compiler/src/dotty/tools/dotc/core/StdNames.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,10 @@ object StdNames {
830830
final val Conforms: TypeName = encode("<:<")
831831

832832
final val Uninstantiated: TypeName = "?$"
833+
834+
val Function: Seq[TypeName] = (0 to 22).map(i => s"scala.Function${i}")
835+
val JFunctionPrefix: Seq[TypeName] = (0 to 2).map(i => s"scala.runtime.java8.JFunction${i}")
836+
val JProcedure: Seq[TypeName] = (0 to 22).map(i => s"scala.runtime.function.JProcedure${i}")
833837
}
834838

835839
abstract class JavaNames[N <: Name] extends DefinedNames[N] {

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

Lines changed: 112 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -376,99 +376,131 @@ object Erasure {
376376
*
377377
* val f: Function1[Int, Any] = x => ...
378378
*
379-
* results in the creation of a closure and a method in the typer:
379+
* results in the creation of a closure and an implementation method in the typer:
380380
*
381381
* def $anonfun(x: Int): Any = ...
382382
* val f: Function1[Int, Any] = closure($anonfun)
383383
*
384-
* Notice that `$anonfun` takes a primitive as argument, but the single abstract method
384+
* Notice that `$anonfun` takes a primitive as argument, but the SAM (Single Abstract Method)
385385
* of `Function1` after erasure is:
386386
*
387387
* def apply(x: Object): Object
388388
*
389-
* which takes a reference as argument. Hence, some form of adaptation is required.
389+
* which takes a reference as argument. Hence, some form of adaptation is
390+
* required. The most reliable way to do this adaptation is to replace the
391+
* closure implementation method by a bridge method that forwards to the
392+
* original method with appropriate boxing/unboxing. For our example above,
393+
* this would be:
390394
*
391-
* If we do nothing, the LambdaMetaFactory bootstrap method will
392-
* automatically do the adaptation. Unfortunately, the result does not
393-
* implement the expected Scala semantics: null should be "unboxed" to
394-
* the default value of the value class, but LMF will throw a
395-
* NullPointerException instead. LMF is also not capable of doing
396-
* adaptation for derived value classes.
395+
* def $anonfun$adapted(x: Object): Object = $anonfun(BoxesRunTime.unboxToInt(x))
396+
* val f: Function1 = closure($anonfun$adapted)
397397
*
398-
* Thus, we need to replace the closure method by a bridge method that
399-
* forwards to the original closure method with appropriate
400-
* boxing/unboxing. For our example above, this would be:
401-
*
402-
* def $anonfun1(x: Object): Object = $anonfun(BoxesRunTime.unboxToInt(x))
403-
* val f: Function1 = closure($anonfun1)
404-
*
405-
* In general a bridge is needed when, after Erasure, one of the
406-
* parameter type or the result type of the closure method has a
407-
* different type, and we cannot rely on auto-adaptation.
408-
*
409-
* Auto-adaptation works in the following cases:
410-
* - If the SAM is replaced by JFunction*mc* in
411-
* [[FunctionalInterfaces]], no bridge is needed: the SAM contains
412-
* default methods to handle adaptation.
413-
* - If a result type of the closure method is a primitive value type
414-
* different from Unit, we can rely on the auto-adaptation done by
415-
* LMF (because it only needs to box, not unbox, so no special
416-
* handling of null is required).
417-
* - If the SAM is replaced by JProcedure* in
418-
* [[DottyBackendInterface]] (this only happens when no explicit SAM
419-
* type is given), no bridge is needed to box a Unit result type:
420-
* the SAM contains a default method to handle that.
398+
* But in some situations we can avoid generating this bridge, either
399+
* because the runtime can perform auto-adaptation, or because we can
400+
* replace the closure functional interface by a specialized sub-interface,
401+
* see comments in this method for details.
421402
*
422403
* See test cases lambda-*.scala and t8017/ for concrete examples.
423404
*/
424-
def adaptClosure(tree: tpd.Closure)(using Context): Tree = {
425-
val implClosure @ Closure(_, meth, _) = tree
426-
427-
implClosure.tpe match {
428-
case SAMType(sam) =>
429-
val implType = meth.tpe.widen.asInstanceOf[MethodType]
430-
431-
val implParamTypes = implType.paramInfos
432-
val List(samParamTypes) = sam.paramInfoss
433-
val implResultType = implType.resultType
434-
val samResultType = sam.resultType
435-
436-
if (!defn.isSpecializableFunction(implClosure.tpe.classSymbol.asClass, implParamTypes, implResultType)) {
437-
def autoAdaptedParam(tp: Type) = !tp.isErasedValueType && !tp.isPrimitiveValueType
438-
val explicitSAMType = implClosure.tpt.tpe.exists
439-
def autoAdaptedResult(tp: Type) = !tp.isErasedValueType &&
440-
(!explicitSAMType || tp.typeSymbol != defn.UnitClass)
441-
def sameSymbol(tp1: Type, tp2: Type) = tp1.typeSymbol == tp2.typeSymbol
442-
443-
val paramAdaptationNeeded =
444-
implParamTypes.lazyZip(samParamTypes).exists((implType, samType) =>
445-
!sameSymbol(implType, samType) && !autoAdaptedParam(implType))
446-
val resultAdaptationNeeded =
447-
!sameSymbol(implResultType, samResultType) && !autoAdaptedResult(implResultType)
448-
449-
if (paramAdaptationNeeded || resultAdaptationNeeded) {
450-
val bridgeType =
451-
if (paramAdaptationNeeded)
452-
if (resultAdaptationNeeded) sam
453-
else implType.derivedLambdaType(paramInfos = samParamTypes)
454-
else implType.derivedLambdaType(resType = samResultType)
455-
val bridge = newSymbol(ctx.owner, AdaptedClosureName(meth.symbol.name.asTermName), Flags.Synthetic | Flags.Method, bridgeType)
456-
Closure(bridge, bridgeParamss =>
457-
inContext(ctx.withOwner(bridge)) {
458-
val List(bridgeParams) = bridgeParamss
459-
assert(ctx.typer.isInstanceOf[Erasure.Typer])
460-
val rhs = Apply(meth, bridgeParams.lazyZip(implParamTypes).map(ctx.typer.adapt(_, _)))
461-
ctx.typer.adapt(rhs, bridgeType.resultType)
462-
},
463-
targetType = implClosure.tpt.tpe)
464-
}
465-
else implClosure
466-
}
467-
else implClosure
468-
case _ =>
469-
implClosure
470-
}
471-
}
405+
def adaptClosure(tree: tpd.Closure)(using Context): Tree =
406+
val Closure(env, meth, tpt) = tree
407+
assert(env.isEmpty, tree)
408+
409+
// The type of the lambda expression
410+
val lambdaType = tree.tpe
411+
// The interface containing the SAM that this closure should implement
412+
val functionalInterface = tpt.tpe
413+
// A lack of an explicit functional interface means we're implementing a scala.FunctionN
414+
val isFunction = !functionalInterface.exists
415+
// The actual type of the implementation method
416+
val implType = meth.tpe.widen.asInstanceOf[MethodType]
417+
val implParamTypes = implType.paramInfos
418+
val implResultType = implType.resultType
419+
val implReturnsUnit = implResultType.classSymbol eq defn.UnitClass
420+
// The SAM that this closure should implement
421+
val SAMType(sam) = lambdaType: @unchecked
422+
val samParamTypes = sam.paramInfos
423+
val samResultType = sam.resultType
424+
425+
/** Can the implementation parameter type `tp` be auto-adapted to a different
426+
* parameter type in the SAM?
427+
*
428+
* For derived value classes, we always need to do the bridging manually.
429+
* For primitives, we cannot rely on auto-adaptation on the JVM because
430+
* the Scala spec requires null to be "unboxed" to the default value of
431+
* the value class, but the adaptation performed by LambdaMetaFactory
432+
* will throw a `NullPointerException` instead. See `lambda-null.scala`
433+
* for test cases.
434+
*
435+
* @see [LambdaMetaFactory](https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/LambdaMetafactory.html)
436+
*/
437+
def autoAdaptedParam(tp: Type) =
438+
!tp.isErasedValueType && !tp.isPrimitiveValueType
439+
440+
/** Can the implementation result type be auto-adapted to a different result
441+
* type in the SAM?
442+
*
443+
* For derived value classes, it's the same story as for parameters.
444+
* For non-Unit primitives, we can actually rely on the `LambdaMetaFactory`
445+
* adaptation, because it only needs to box, not unbox, so no special
446+
* handling of null is required.
447+
*/
448+
def autoAdaptedResult =
449+
!implResultType.isErasedValueType && !implReturnsUnit
450+
451+
def sameClass(tp1: Type, tp2: Type) = tp1.classSymbol == tp2.classSymbol
452+
453+
val paramAdaptationNeeded =
454+
implParamTypes.lazyZip(samParamTypes).exists((implType, samType) =>
455+
!sameClass(implType, samType) && !autoAdaptedParam(implType))
456+
val resultAdaptationNeeded =
457+
!sameClass(implResultType, samResultType) && !autoAdaptedResult
458+
459+
if paramAdaptationNeeded || resultAdaptationNeeded then
460+
// Instead of instantiating `scala.FunctionN`, see if we can instantiate
461+
// a specialized sub-interface where the SAM type matches the
462+
// implementation method type, thus avoiding the need for bridging.
463+
// This optimization is skipped when using Scala.js because its backend
464+
// does not support closures using custom functional interfaces.
465+
if isFunction && !ctx.settings.scalajs.value then
466+
val arity = implParamTypes.length
467+
val specializedFunctionalInterface =
468+
if defn.isSpecializableFunctionSAM(implParamTypes, implResultType) then
469+
// Using these subclasses is critical to avoid boxing since their
470+
// SAM is a specialized method `apply$mc*$sp` whose default
471+
// implementation in FunctionN boxes.
472+
tpnme.JFunctionPrefix(arity).specializedFunction(implResultType, implParamTypes)
473+
else if !paramAdaptationNeeded && implReturnsUnit then
474+
// Here, there is no actual boxing to avoid so we could get by
475+
// without JProcedureN, but Unit-returning functions are very
476+
// common so it seems worth it to not generate bridges for them.
477+
tpnme.JProcedure(arity)
478+
else
479+
EmptyTypeName
480+
if !specializedFunctionalInterface.isEmpty then
481+
return cpy.Closure(tree)(tpt = TypeTree(requiredClass(specializedFunctionalInterface).typeRef))
482+
483+
// Otherwise, generate a new closure implemented with a bridge.
484+
val bridgeType =
485+
if paramAdaptationNeeded then
486+
if resultAdaptationNeeded then
487+
sam
488+
else
489+
implType.derivedLambdaType(paramInfos = samParamTypes)
490+
else
491+
implType.derivedLambdaType(resType = samResultType)
492+
val bridge = newSymbol(ctx.owner, AdaptedClosureName(meth.symbol.name.asTermName), Flags.Synthetic | Flags.Method, bridgeType)
493+
Closure(bridge, bridgeParamss =>
494+
inContext(ctx.withOwner(bridge)) {
495+
val List(bridgeParams) = bridgeParamss
496+
assert(ctx.typer.isInstanceOf[Erasure.Typer])
497+
val rhs = Apply(meth, bridgeParams.lazyZip(implParamTypes).map(ctx.typer.adapt(_, _)))
498+
ctx.typer.adapt(rhs, bridgeType.resultType)
499+
},
500+
targetType = functionalInterface).withSpan(tree.span)
501+
else
502+
tree
503+
end adaptClosure
472504

473505
/** Eta expand given `tree` that has the given method type `mt`, so that
474506
* it conforms to erased result type `pt`.

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

Lines changed: 0 additions & 49 deletions
This file was deleted.

0 commit comments

Comments
 (0)